```
docs(health-profile): 添加防编造加固修订记录到导出健康档案设计文档 补充了关于导出摘要出现虚构病例问题的详细分析和修复方案, 包括检索策略优化、空数据兜底处理和prompt重写等三层防护措施。 ```
This commit is contained in:
@@ -26,6 +26,12 @@ struct DiaryQuickSheet: View {
|
||||
/// 累积已覆盖的问诊维度(question.dim),回传下一轮 prompt 用于按维度去重。
|
||||
@State private var coveredDims: Set<String> = []
|
||||
@State private var suggestTask: Task<Void, Never>?
|
||||
/// 当前正在「就地填空」的 question id;nil = 没有展开的填空面板。
|
||||
@State private var fillingId: UUID?
|
||||
/// 当前填空面板各占位槽的输入值,长度 = 该模板占位数。
|
||||
@State private var fillValues: [String] = []
|
||||
/// 上一轮「再问一轮」没问出任何新维度(全被去重)时为 true,提示用户已覆盖主要维度。
|
||||
@State private var exhaustedNote = false
|
||||
/// sheet detent。默认 large,确保建议面板有足够展示空间。
|
||||
/// 仍保留 medium,用户可手动下拉收回为半屏(纯写文本时更轻量)。
|
||||
@State private var detent: PresentationDetent = .large
|
||||
@@ -76,6 +82,7 @@ struct DiaryQuickSheet: View {
|
||||
text: $content, axis: .vertical)
|
||||
.lineLimit(3...8)
|
||||
.focused($contentFocused)
|
||||
.onChange(of: content) { _, _ in exhaustedNote = false }
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
@@ -177,6 +184,19 @@ struct DiaryQuickSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
if exhaustedNote {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Tj.Palette.leaf)
|
||||
Text("已覆盖主要问诊维度;补充原文后可再追问")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
// 底部主操作按钮(状态机驱动)
|
||||
phaseFooter
|
||||
}
|
||||
@@ -318,6 +338,7 @@ struct DiaryQuickSheet: View {
|
||||
|
||||
private func questionRow(index: Int, question: DiaryAssistService.Question) -> some View {
|
||||
let adopted = question.adopted
|
||||
let filling = fillingId == question.id
|
||||
return VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("\(index).")
|
||||
@@ -341,7 +362,7 @@ struct DiaryQuickSheet: View {
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 5)
|
||||
.background(Capsule().fill(Tj.Palette.leafSoft))
|
||||
} else {
|
||||
} else if !filling {
|
||||
Button { adopt(question) } label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
@@ -357,7 +378,14 @@ struct DiaryQuickSheet: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
if !question.fill.isEmpty && !adopted {
|
||||
if filling {
|
||||
QuestionFillPanel(
|
||||
template: question.fill,
|
||||
values: $fillValues,
|
||||
onCommit: { assembled in commitAdoption(question, text: assembled) },
|
||||
onCancel: { closeFill() }
|
||||
)
|
||||
} else if !question.fill.isEmpty && !adopted {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
Text("将追加:")
|
||||
.font(.system(size: 11))
|
||||
@@ -405,6 +433,7 @@ struct DiaryQuickSheet: View {
|
||||
detent = .large
|
||||
}
|
||||
}
|
||||
exhaustedNote = false
|
||||
phase = .loading
|
||||
suggestTask = Task { @MainActor in
|
||||
do {
|
||||
@@ -413,21 +442,34 @@ struct DiaryQuickSheet: View {
|
||||
coveredDimensions: covered
|
||||
)
|
||||
if Task.isCancelled { return }
|
||||
// 客户端字面兜底(防 LLM 不听话);跨轮去重主要靠 prompt 的维度排除。
|
||||
let existing = Set(questions.map { Self.normalize($0.q) })
|
||||
// 客户端硬去重(不依赖 1.7B 听话):
|
||||
// ① 维度已在往轮覆盖 → 丢;② 本轮内维度重复 → 丢;③ 文本与已有近似 → 丢。
|
||||
let coveredSnapshot = coveredDims
|
||||
var acceptedNorms = questions.map { Self.normalize($0.q) }
|
||||
var batchDims = Set<String>()
|
||||
let nextRound = currentRound + 1
|
||||
let fresh = result.questions
|
||||
.filter { !existing.contains(Self.normalize($0.q)) }
|
||||
.map { q -> DiaryAssistService.Question in
|
||||
var stamped = q
|
||||
stamped.round = nextRound
|
||||
return stamped
|
||||
}
|
||||
let fresh = result.questions.compactMap { q -> DiaryAssistService.Question? in
|
||||
let dim = q.dim.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let norm = Self.normalize(q.q)
|
||||
if !dim.isEmpty, coveredSnapshot.contains(dim) { return nil }
|
||||
if !dim.isEmpty, batchDims.contains(dim) { return nil }
|
||||
if acceptedNorms.contains(where: { Self.isSimilar($0, norm) }) { return nil }
|
||||
if !dim.isEmpty { batchDims.insert(dim) }
|
||||
acceptedNorms.append(norm)
|
||||
var stamped = q
|
||||
stamped.round = nextRound
|
||||
return stamped
|
||||
}
|
||||
withAnimation(.snappy(duration: 0.2)) {
|
||||
questions.append(contentsOf: fresh)
|
||||
for q in fresh where !q.dim.isEmpty { coveredDims.insert(q.dim) }
|
||||
if fresh.isEmpty {
|
||||
exhaustedNote = true // 这轮没问出任何新维度
|
||||
} else {
|
||||
questions.append(contentsOf: fresh)
|
||||
for q in fresh where !q.dim.isEmpty { coveredDims.insert(q.dim) }
|
||||
currentRound = nextRound
|
||||
exhaustedNote = false
|
||||
}
|
||||
lastRate = result.decodeRate
|
||||
currentRound = nextRound
|
||||
phase = .ready
|
||||
}
|
||||
} catch is CancellationError {
|
||||
@@ -449,20 +491,59 @@ struct DiaryQuickSheet: View {
|
||||
.replacingOccurrences(of: "?", with: "?")
|
||||
}
|
||||
|
||||
/// 近似判重:归一化后相等,或字符集 Jaccard ≥ 0.8(抓「会/下」这类换一两字的重复)。
|
||||
private static func isSimilar(_ a: String, _ b: String) -> Bool {
|
||||
if a == b { return true }
|
||||
let sa = Set(a), sb = Set(b)
|
||||
guard !sa.isEmpty, !sb.isEmpty else { return false }
|
||||
let inter = sa.intersection(sb).count
|
||||
let union = sa.union(sb).count
|
||||
return union > 0 && Double(inter) / Double(union) >= 0.8
|
||||
}
|
||||
|
||||
private func cancelSuggestions() {
|
||||
suggestTask?.cancel()
|
||||
phase = hasQuestions ? .ready : .idle
|
||||
}
|
||||
|
||||
/// 把 question.fill 追加到 textfield 末尾,并把该 question 标记为 adopted。
|
||||
/// 采纳:模板含 `[占位]` 时展开就地填空面板;无占位则直接把整句追加(并标记 adopted)。
|
||||
/// 已采纳的 q 不会从列表里消失;其维度已在生成时计入 coveredDims,下一轮 prompt 会避开。
|
||||
private func adopt(_ question: DiaryAssistService.Question) {
|
||||
guard !question.fill.isEmpty, DiaryFillTemplate.slotCount(question.fill) > 0 else {
|
||||
// 无占位:直接采纳整句(空 fill 时退回到追加问题本身)。
|
||||
commitAdoption(question, text: question.fill.isEmpty ? question.q : question.fill)
|
||||
return
|
||||
}
|
||||
withAnimation(.snappy(duration: 0.18)) {
|
||||
fillingId = question.id
|
||||
fillValues = Array(repeating: "", count: DiaryFillTemplate.slotCount(question.fill))
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭填空面板(取消)。
|
||||
private func closeFill() {
|
||||
withAnimation(.snappy(duration: 0.18)) {
|
||||
fillingId = nil
|
||||
fillValues = []
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交采纳:把(填好的)整句追加到正文,标记 adopted,收起面板。
|
||||
private func commitAdoption(_ question: DiaryAssistService.Question, text: String) {
|
||||
if let idx = questions.firstIndex(where: { $0.id == question.id }) {
|
||||
withAnimation(.snappy(duration: 0.18)) {
|
||||
questions[idx].adopted = true
|
||||
}
|
||||
}
|
||||
let toAppend = question.fill.isEmpty ? question.q : question.fill
|
||||
appendToContent(text)
|
||||
fillingId = nil
|
||||
fillValues = []
|
||||
}
|
||||
|
||||
/// 把一段补充文本追加到正文末尾(自动补换行,空文本忽略)。
|
||||
private func appendToContent(_ text: String) {
|
||||
let toAppend = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !toAppend.isEmpty else { return }
|
||||
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
content = toAppend
|
||||
|
||||
Reference in New Issue
Block a user