Commit 4c3f9c58 by luoqi

fix(plan-detail): 深度/标准话术刷新丢失 + 主治医生误显影像AI

- parseScriptMarkdownToSections 改通用 H2 解析:原只认稳健 4 固定标题(开场白/...),
  深度/标准的自由标题反 parse 全落空 → plan_scripts 存了内容但刷新显示'尚未生成'。
  现按任意 ## 标题切段(稳健固定标题映射已知 id,标准/深度自由标题用 s{n}+原标题),三档通用。
- KeyFactsCard 主治医生:影像AI(image_ai 触发诊断)不当主治医生显示,退回真实最高频医生
  (排除影像AI/image_ai),真无人类医生 → '—'。话术侧 extractPrimaryDoctor 本就排除,一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 5816aca8
......@@ -567,42 +567,36 @@ const SECTION_META: Record<
};
function parseScriptMarkdownToSections(md: string) {
// 按 H2 切分:`^## (xxx)$` 之后到下一个 H2 之间为内容
const ids: Array<'opening' | 'informMissed' | 'reviewAdvice' | 'closing'> = [
'opening',
'informMissed',
'reviewAdvice',
'closing',
];
const result = ids.map((id) => ({
id,
label: SECTION_META[id].label,
durationHint: SECTION_META[id].durationHint,
markdown: '',
}));
if (!md) return result;
// ⭐ 通用 H2 切分:每个 `## 标题` 起一段,到下一个 H2 之间为内容。
// - 稳健档:4 个固定标题(开场白/告知应治未治/复查建议/结束回访语)→ 映射到已知 id + 固定 label。
// - 标准/深度档:自由标题(段数不定)→ id=`s{序号}`、label=原标题。
// 旧实现只认稳健 4 固定标题 → 深度/标准的自由标题全 currentId=null、内容丢弃 → 刷新后空白
// (plan_scripts 存了内容,但反 parse 解不出 → 前端"尚未生成参考话术")。本版按任意 H2 解析,三档通用。
// H2 之前的前言(`> 患者:… · 语气:…`)在首个 H2 前,自然跳过。
if (!md) return [];
const lines = md.split('\n');
let currentId: 'opening' | 'informMissed' | 'reviewAdvice' | 'closing' | null = null;
const buf: Record<string, string[]> = {};
const sections: Array<{ id: string; label: string; durationHint: string; markdown: string }> = [];
let cur: { id: string; label: string; durationHint: string; buf: string[] } | null = null;
let idx = 0;
const flush = () => {
if (cur) sections.push({ id: cur.id, label: cur.label, durationHint: cur.durationHint, markdown: cur.buf.join('\n').trim() });
};
for (const line of lines) {
const m = /^##\s+(.+?)\s*$/.exec(line);
if (m) {
flush();
const head = (m[1] ?? '').trim();
const matched = SECTION_HEAD_TO_ID[head];
currentId = matched ?? null;
if (currentId) buf[currentId] = [];
const fixedId = SECTION_HEAD_TO_ID[head];
cur = fixedId
? { id: fixedId, label: SECTION_META[fixedId].label, durationHint: SECTION_META[fixedId].durationHint, buf: [] }
: { id: `s${idx}`, label: head, durationHint: '', buf: [] };
idx++;
continue;
}
if (currentId) {
buf[currentId] = buf[currentId] ?? [];
buf[currentId]!.push(line);
}
}
for (const sec of result) {
sec.markdown = (buf[sec.id] ?? []).join('\n').trim();
if (cur) cur.buf.push(line);
}
return result;
flush();
return sections;
}
function serializeFact(f: {
......
......@@ -1190,33 +1190,33 @@ function KeyFactsCard({
onOpenDetail: () => void;
onOpenTeeth: () => void;
}) {
// ─ 主治医生(口径 A)─ = 触发本次召回的诊断的医生(focusedReason 的证据 fact)。
// ─ 主治医生(口径 A)─ = 触发本次召回的诊断的【真实】医生(focusedReason 证据 fact)。
// 而非"全量最高频医生"——召回是冲某诊断来的,该露出做出该诊断的医生。
// 兜底:① 触发诊断只来自影像 AI(无人类医生)→ '影像AI';
// ② 无 focusedReason(异常)→ 退回全量最高频医生;③ 都没有 → '—'。
// ⚠️ 影像 AI 诊断(code_source=image_ai,doctor=影像AI)不是真人,不当主治医生显示:
// 退回"全量最高频【真实】医生"(排除影像AI);真没人类医生过 → '—'。
// (避免缺牙这种常被影像 AI 触发的召回把"主治医生"显示成"影像AI"。)
const attendingDoctor = (() => {
const ev = focusedReason?.evidence ?? [];
if (ev.length > 0) {
const evidenceIds = new Set(ev.map((e) => e.id));
let sawImageDx = false;
for (const f of facts) {
if (f.type !== 'diagnosis_record' || !evidenceIds.has(f.id)) continue;
const c = f.content as Record<string, unknown> | null;
const name = c?.doctor_name ? String(c.doctor_name) : '';
if (name) return name; // A:触发诊断的医生
if (String(c?.code_source ?? '') === 'image_ai') sawImageDx = true;
const name = c?.doctor_name ? String(c.doctor_name).trim() : '';
if (name && name !== '影像AI') return name; // A:触发诊断的真实医生
}
if (sawImageDx) return '影像AI'; // 只被影像分析诊断,无人类主诊
// 触发诊断只来自影像 AI / 无人类医生 → 不显示"影像AI",落到下方真实医生兜底
}
// 兜底:全量 fact 最高频医生(同 chain-composer.buildDoctorMap)
// 兜底:全量 fact 最高频【真实】医生(排除影像 AI;同 chain-composer.buildDoctorMap)
const idCount = new Map<string, number>();
const idToName = new Map<string, string>();
for (const f of facts) {
const c = f.content as Record<string, unknown> | null;
const id = c?.doctor_id ? String(c.doctor_id) : '';
if (!id) continue;
const name = c?.doctor_name ? String(c.doctor_name).trim() : '';
if (!id || name === '影像AI' || String(c?.code_source ?? '') === 'image_ai') continue;
idCount.set(id, (idCount.get(id) ?? 0) + 1);
if (c?.doctor_name) idToName.set(id, String(c.doctor_name));
if (name) idToName.set(id, name);
}
if (idCount.size === 0) return '—';
const topId = [...idCount.entries()].sort((a, b) => b[1] - a[1])[0]![0];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment