Commit d8f03249 by luoqi

fix(script): 统一通话称呼(监护人 aware)+ 修"9岁张先生" + 医生标签改准

- 统一称呼单一源 callSalutation(年龄+性别+监护人):未成年→监护人("徐女士")/家长,
  成人→先生/女士;header + 开场白 + fallback 共用,不再各算一遍
  · 修 bug:头部 `患者:张先生`(nameSpokenForm 无儿童分支)→ 9岁现为"徐女士"
- 监护人进 ScriptContext:orchestrator 查 patient_relations(妈妈>爸爸>祖辈,优先已建档)
  → guardian + 触达提示"打给家长、患者是孩子(称宝宝)",称呼真正打给监护人
- user prompt basics 去掉称呼(原"徐女士,男,9岁"矛盾)→ 只留性别+年龄
- {最后一次就诊医生} → {诊断医生}(prompt + base-system.md + 人群模板 + schema),
  标签对齐实际喂的 triggerDoctor(触发诊断的医生)
- promptVersion bump v11

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 80702b0d
......@@ -6,7 +6,7 @@ import type { DraftPlanScriptInput, DraftPlanScriptOutput } from './input.types'
import { buildDraftPlanScriptPrompt } from './prompt';
import { composeSystem } from './skill-composer';
import { DraftPlanScriptSkillRegistry } from './skill-registry.service';
import { resolveAgeBranch, resolveSalutation, smartDateDisplay } from './script-facts';
import { smartDateDisplay } from './script-facts';
import { resolveDisease } from './script-common/disease-knowledge';
/**
......@@ -78,8 +78,7 @@ const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
*/
function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
const { patient, clinicName, plan, clinicalContext } = input;
const branch = resolveAgeBranch(patient.age);
const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
const salutation = patient.nameMasked; // 统一通话称呼(年龄+性别+监护人 aware,orchestrator 算好)
// 漏诊项 = PAC 应治未治 reason(取 priorityScore 最高的一条)→ 转换层归一
const topReason = [...(plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0];
const doctor = topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主治医生';
......@@ -120,7 +119,7 @@ function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
* 改 system/prompt 文本 → bump 字母;改 schema → bump 日期。
*/
const DRAFT_PLAN_SCRIPT_PROMPT_VERSION =
'draft_plan_script@2026-06-05-4module-v10'; // v10: 病种知识走 disease-knowledge 单一访问源(subKey 优先+文本兜底),修 颌骨囊肿 拿不到风险/优势的 bug;v9: 自报家门用登录客服 岗位+姓名(agent);v8: 占位符统一({}=替换、【】=原样保留);v7: 清除 user prompt 污染;v6: 清 system 污染;v5: 还原原模板
'draft_plan_script@2026-06-05-4module-v11'; // v11: 统一通话称呼(年龄+性别+监护人,修"9岁张先生");监护人触达提示;医生标签 最后一次就诊→诊断医生;v10: 病种知识走 disease-knowledge 单一访问源(subKey 优先+文本兜底),修 颌骨囊肿 拿不到风险/优势的 bug;v9: 自报家门用登录客服 岗位+姓名(agent);v8: 占位符统一({}=替换、【】=原样保留);v7: 清除 user prompt 污染;v6: 清 system 污染;v5: 还原原模板
@Injectable()
export class DraftPlanScriptCall
......
......@@ -13,11 +13,14 @@
export interface ScriptContext {
/** 患者信息(已脱敏:nameMasked, phone 不传) */
patient: {
/** 通话称呼(单一源:年龄+性别+监护人 aware)— 未成年=监护人称呼/家长,成人=先生/女士 */
nameMasked: string;
gender: string | null;
age: number | null;
/** 病历号(host 病历主键,如 "FY0A000922")— 话术里供客服核对身份用,可空 */
medicalRecordNumber?: string | null;
/** 监护人(未成年触达对象)— 让话术知道"打给家长、患者是孩子(称宝宝)";成人/无监护人 → null */
guardian?: { relationshipLabel: string; name: string | null } | null;
};
/** 诊所名(给 LLM 用作"我是X诊所的客服顾问",避免编造"XX口腔") */
......
import type { DraftPlanScriptInput } from './input.types';
import { resolveAgeBranch, resolveSalutation, smartDateDisplay } from './script-facts';
import { smartDateDisplay } from './script-facts';
import { resolveDisease } from './script-common/disease-knowledge';
/**
......@@ -39,8 +39,8 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
// 程序算好的确定性事实(LLM 不做年龄分支/日期格式/优先级/查表/称呼)
const now = new Date();
const branch = resolveAgeBranch(patient.age);
const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
// 称呼 = orchestrator 算好的统一通话称呼(年龄+性别+监护人 aware):未成年→监护人/家长,成人→先生女士
const salutation = patient.nameMasked;
const lastVisitDate =
clinicalContext.daysSinceLastVisit != null
? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000)
......@@ -61,12 +61,17 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
? disease.advantages.map((a) => ` - ${a}`).join('\n')
: ' - 趁现在早点处理会更省心';
// 患者基本信息(只给话术用得到的:性别归一中文,年龄)
// 患者基本信息(只给话术用得到的:性别 + 年龄)。⚠️ 不含称呼 —— 称呼是"通话对象"(可能是监护人),
// 跟患者性别/年龄是两回事;混在一起会出现"徐女士,男,9岁"这类矛盾。
const g = (patient.gender ?? '').trim().toUpperCase();
const genderText = g === '男' || g === 'M' ? '男' : g === '女' || g === 'F' ? '女' : '';
const basics = [patient.nameMasked, genderText, patient.age != null ? `${patient.age}岁` : '']
.filter(Boolean)
.join(',');
const basics = [genderText, patient.age != null ? `${patient.age}岁` : ''].filter(Boolean).join(',');
// 监护人提示(未成年:打给家长,患者是孩子)
const guardianHint = patient.guardian
? `本次电话打给${patient.guardian.relationshipLabel}${
patient.guardian.name ? `(${patient.guardian.name})` : ''
},沟通对象是家长,患者是孩子,话术里称孩子为"宝宝"`
: null;
// 语气线索(熟客 vs 新客 → tone 选择;不念出来)
const toneHint =
clinicalContext.completedTreatmentCount > 0
......@@ -86,7 +91,7 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
- {自报家门}:${selfIntro}
- {智能时间显示}:${dateDisplay}
- 那次主诉:${chiefComplaint ?? '无记录'}
- {最后一次就诊医生}:${doctor}
- {诊断医生}:${doctor}${guardianHint ? `\n- 触达说明:${guardianHint}` : ''}
## 本次应治未治(只讲这一个)
- {应治未治项}:${disease.label}
......
......@@ -28,7 +28,7 @@ export const DraftPlanScriptSchema = z.object({
'用 `• ` bullet 分 3-4 句,内容必须包含(顺序):',
'1. 自报家门:用「{诊所}的{岗位角色}{岗位姓名}」(岗位角色严禁写"回访专员")',
'2. 智能称呼:user 给的 {称呼}(已算好,直接用,不要自己改)',
'3. 以「{最后一次就诊医生}医生特意交代」体现医生关怀',
'3. 以「{诊断医生}医生特意交代」体现医生关怀',
'4. 用 user 给的 {智能日期} 问近况:「自从{智能日期}检查后,口腔情况怎么样?」',
'禁止:加大标题/═══分隔符、加表情、写成抒情排比',
].join('\n'),
......@@ -42,10 +42,10 @@ export const DraftPlanScriptSchema = z.object({
[
'【第二部分·告知应治未治 — 只讲 user 给的那一个 {应治未治项},严禁提其他项】',
'用 `• ` bullet 分短句(**句数与结构以系统提示词里匹配的人群模板为准**:成人 4 句 / 儿童 5 句),每句一个重点,口语化,温和提醒非推销:',
'现状描述:以「{最后一次就诊医生}医生上次检查注意到您有{应治未治项}的情况」表达,不要说"我们发现了"',
'现状描述:以「{诊断医生}医生上次检查注意到您有{应治未治项}的情况」表达,不要说"我们发现了"',
'健康提醒:从 user 给的 {风险要点} 里灵活挑 1-2 条口语说,不堆砌、不吓唬、不用"如A、B、C"书面句式',
'个人化关怀:用 user 给的 {治疗优势要点},以"趁现在/早一点"口吻;禁止提具体年龄/职业',
'专业建议:体现「{最后一次就诊医生}医生也特别嘱咐提醒您」,禁止"建议您关注一下"这类书面语',
'专业建议:体现「{诊断医生}医生也特别嘱咐提醒您」,禁止"建议您关注一下"这类书面语',
].join('\n'),
),
......@@ -57,8 +57,8 @@ export const DraftPlanScriptSchema = z.object({
[
'【第三部分·复查建议 — 有温度有引导,主动约】',
'用 `• ` bullet 分短句(**句数与结构以系统提示词里匹配的人群模板为准**:成人 4 句 / 儿童 5 句):',
'核心:复查重要性 +「让{最后一次就诊医生}医生帮您再仔细看看」+(成人)直接用 {复查时长} 原文',
'引导预约必须严格用「{最后一次就诊医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个方便?」',
'核心:复查重要性 +「让{诊断医生}医生帮您再仔细看看」+(成人)直接用 {复查时长} 原文',
'引导预约必须严格用「{诊断医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个方便?」',
'⚠️【时间段1】【时间段2】保留占位结构,严禁替换成"周三上午"等具体时间(PAC 无排班 API)',
].join('\n'),
),
......
......@@ -25,15 +25,15 @@
⚠️ 重要提醒:如果输出缺少任何一个模块,整个话术将被视为不合格!
# 占位符约定(两种,含义不同)
- `{xxx}` = **要替换**的占位:用"本次回访患者信息"里给的同名值填进去(如 {智能称呼}{应治未治项}{最后一次就诊医生}{风险要点}{复查时长})。输出里不能再出现 `{}`
- `{xxx}` = **要替换**的占位:用"本次回访患者信息"里给的同名值填进去(如 {智能称呼}{应治未治项}{诊断医生}{风险要点}{复查时长})。输出里不能再出现 `{}`
- `【xxx】` = **原样保留**的占位:不要替换、照抄进话术,客服打电话时手动填。只有这几个:【时间段1】【时间段2】【具体预约时间】【缺失牙位】。
- 另:结束语的分支标签【预约成功】【预约不成功】也照常输出。
# 直接使用给定的事实
开场自报家门用{自报家门}、称呼用{智能称呼}、日期用{智能时间显示}、本次只讲{应治未治项}、健康提醒从{风险要点}挑、检查说明用{复查时长}原文、以{最后一次就诊医生}名义体现关怀。这些值直接用,不要自己重算、改写或改格式。所有事实只能来自给定的字段;空缺就泛指或省略,不得编造(不杜撰医生名 / 诊断 / 价格 / 政策 / 设备 / 患者背景)。
开场自报家门用{自报家门}、称呼用{智能称呼}、日期用{智能时间显示}、本次只讲{应治未治项}、健康提醒从{风险要点}挑、检查说明用{复查时长}原文、以{诊断医生}名义体现关怀。这些值直接用,不要自己重算、改写或改格式。所有事实只能来自给定的字段;空缺就泛指或省略,不得编造(不杜撰医生名 / 诊断 / 价格 / 政策 / 设备 / 患者背景)。
# 时间用占位
- 引导预约严格用「{最后一次就诊医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个方便?」
- 引导预约严格用「{诊断医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个方便?」
- 结束语·预约成功保留「我们【具体预约时间】见」。
- ⚠️【时间段1】【时间段2】【具体预约时间】原样保留占位,严禁替换成"周三上午"等具体时间。❌ 严禁加粗具体时间、严禁"已为您约好 / 敲定 X"承诺。
......@@ -64,7 +64,7 @@
# 输出前自查
✅ 4个模块全部包含且顺序正确?
✅ 开场白以{最后一次就诊医生}医生名义,体现医生交代的关怀?
✅ 开场白以{诊断医生}医生名义,体现医生交代的关怀?
✅ 只专注 {应治未治项} 一个,没提其他项目?
✅ 告知应治未治、复查建议都分了短句?
✅ 称呼 / 日期 / 复查时长用的是给定的值?时间保留了【时间段】占位、没写死具体时间?
......
......@@ -15,8 +15,8 @@ tone 默认 professional(专业稳重);熟客可 warm;急性场景可 ur
[温馨有温度的开场,以医生名义]
- • 您好,{自报家门}(直接用给定的「自报家门」开头,不要自己改岗位 / 姓名,也不要编"回访专员")
- 称呼用{智能称呼}(成人 = "{姓}先生" / "{姓}女士";性别未知或无称呼信息 = "您好")
- • {最后一次就诊医生}医生特意交代我来关注您的后续情况
- • (如果是熟悉患者可说:{最后一次就诊医生}医生上次还和我提起您呢)
- • {诊断医生}医生特意交代我来关注您的后续情况
- • (如果是熟悉患者可说:{诊断医生}医生上次还和我提起您呢)
- • 您自从{智能时间显示}检查后,口腔情况怎么样?
## ═══ 第二部分:告知应治未治 ═══
......@@ -24,7 +24,7 @@ tone 默认 professional(专业稳重);熟客可 warm;急性场景可 ur
小节1 - 现状描述(短句1):用口语告诉患者上次检查时发现的问题,突出"温和提醒"和"医生发现"的语气,不要直接说"我们发现了……"。
✅ 推荐表达方式:
• 上次来检查的时候,{最后一次就诊医生}医生注意到您有{应治未治项}的情况
• 上次来检查的时候,{诊断医生}医生注意到您有{应治未治项}的情况
• 医生那次检查时提到,您有一点{应治未治项}的问题
• 当时有观察到一些{应治未治项}的情况,医生是挺关注的
• 有一点{应治未治项}的表现,医生希望您留意一下
......@@ -43,22 +43,22 @@ tone 默认 professional(专业稳重);熟客可 warm;急性场景可 ur
• 趁现在牙槽骨条件还不错,早点处理效果更好
• 早一点介入,对牙齿稳定有帮助,也避免将来多花功夫
小节4 - 专业建议(短句4):口语化表达,体现{最后一次就诊医生}医生的关心与交代,传达温馨提醒感,避免生硬。
小节4 - 专业建议(短句4):口语化表达,体现{诊断医生}医生的关心与交代,传达温馨提醒感,避免生硬。
✅ 推荐句式示例:
• 这个情况,{最后一次就诊医生}医生也特别嘱咐我们提醒您一下
• {最后一次就诊医生}医生说,这个问题早点看看会比较安心
• 上次{最后一次就诊医生}医生也提到,最好别拖太久
• {最后一次就诊医生}医生还是希望您尽早来院检查一下情况
• 这个情况,{诊断医生}医生也特别嘱咐我们提醒您一下
• {诊断医生}医生说,这个问题早点看看会比较安心
• 上次{诊断医生}医生也提到,最好别拖太久
• {诊断医生}医生还是希望您尽早来院检查一下情况
• 医生的意思是,这个问题别忽略了,早点关注会更好
❌ 禁止使用:"建议您关注一下这个问题"、"医生建议您处理该问题"等书面语言。
## ═══ 第三部分:复查建议 ═══
[通过短句说明复查重要性,有温度有引导性]
小节1 - 复查重要性(短句1):如果方便的话您看最近有没有时间来院复查一下
小节2 - 健康维护(短句2):让{最后一次就诊医生}医生帮您再仔细看看
小节2 - 健康维护(短句2):让{诊断医生}医生帮您再仔细看看
小节3 - 检查说明(短句3):{复查时长}
小节4 - 引导预约(短句4):请严格使用如下标准格式:
✅ {最后一次就诊医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
✅ {诊断医生}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
⚠️ 禁止将时间段直接替换为"周三上午"或"周五下午",必须保留"【时间段】"结构。
## ═══ 第四部分:结束回访语 ═══
......
......@@ -17,8 +17,8 @@ tone 默认 warm(温和家常,适合与家长沟通)。
[温馨有温度的开场,以医生名义]
- • 您好,{自报家门}(直接用给定的「自报家门」开头,不要自己改岗位 / 姓名,也不要编"回访专员")
- 称呼用{智能称呼}(儿童 = "宝宝妈妈"或"{姓名}的家长",如"乐乐家长";无称呼信息 = "您好");可先确认家长接听
- • {最后一次就诊医生}医生特意交代我来关注宝宝的后续情况
- • (如果是熟悉患者可说:{最后一次就诊医生}医生上次还和我提起宝宝呢)
- • {诊断医生}医生特意交代我来关注宝宝的后续情况
- • (如果是熟悉患者可说:{诊断医生}医生上次还和我提起宝宝呢)
- • 宝宝自从{智能时间显示}检查后,口腔情况怎么样?
## ═══ 第二部分:告知牙齿问题-健康提醒 ═══
......@@ -36,10 +36,10 @@ tone 默认 warm(温和家常,适合与家长沟通)。
小节1 - 复查时间(短句1):建议3个月左右带宝宝来院检查
小节2 - 检查内容(短句2):一方面做全面检查,看有没有蛀牙,有没有不良习惯
小节3 - 预防措施(短句3):还要看看要不要给宝宝涂氟保护牙齿
小节4 - 专业建议(短句4):也请{最后一次就诊医生}医生再仔细看一下宝宝的情况
小节4 - 专业建议(短句4):也请{诊断医生}医生再仔细看一下宝宝的情况
小节5 - 引导预约(短句5):[有引导性,给出具体时间选择]
• 如果方便的话您看最近有没有时间,我帮您预约一个儿牙专家的时间,您带宝宝过来看一看
• {最后一次就诊医生}医生【时间段1】和【时间段2】这两个时间段有空
• {诊断医生}医生【时间段1】和【时间段2】这两个时间段有空
⚠️ 保留"【时间段】"结构,禁止替换成具体时间
## ═══ 第四部分:结束回访语 ═══
......
......@@ -98,8 +98,8 @@ export class PlanScriptOrchestrator {
* 实时教练用它的 patient/plan/clinicalContext 拼 Qwen instructions,跟话术生成共享上下文。
*/
async buildScriptInputForPlan(planId: string): Promise<DraftPlanScriptInput> {
const { plan, patient, persona, facts } = await this.loadPlanContext(planId);
return this.buildCallInput({ plan, patient, persona, facts });
const { plan, patient, persona, facts, guardian } = await this.loadPlanContext(planId);
return this.buildCallInput({ plan, patient, persona, facts, guardian });
}
/**
......@@ -115,8 +115,8 @@ export class PlanScriptOrchestrator {
options: PlanScriptGenerateOptions = {},
): Promise<PlanScriptGenerateResult> {
// ─── 1. 装配 input(纯 DB 读,不 pull) ───
const { plan, patient, persona, facts } = await this.loadPlanContext(planId);
const input = this.buildCallInput({ plan, patient, persona, facts, agent: options.agent });
const { plan, patient, persona, facts, guardian } = await this.loadPlanContext(planId);
const input = this.buildCallInput({ plan, patient, persona, facts, guardian, agent: options.agent });
// ─── 2. 跑 AI 调用 ───
const workflowRunId = randomUUID();
......@@ -304,6 +304,21 @@ export class PlanScriptOrchestrator {
});
if (!patient) throw new NotFoundException(`Patient ${plan.patientId} not found`);
const patientAge = patient.birthDate ? calcAge(patient.birthDate) : null;
// 监护人(未成年触达打给监护人):优先 妈妈 > 爸爸 > 祖辈,优先已建档(有姓名)
const guardian =
patientAge != null && patientAge <= 12
? pickGuardian(
await this.prisma.patientRelation.findMany({
where: {
patientId: patient.id,
relationship: { in: ['mother', 'father', 'grandparent'] },
},
include: { relatedPatient: { select: { name: true } } },
}),
)
: null;
const persona = await this.prisma.persona.findFirst({
where: { patientId: patient.id, supersededAt: null },
include: { features: true },
......@@ -323,7 +338,7 @@ export class PlanScriptOrchestrator {
orderBy: { occurredAt: 'desc' },
});
return { plan, patient, persona, facts };
return { plan, patient, persona, facts, guardian };
}
private buildCallInput(args: {
......@@ -332,8 +347,11 @@ export class PlanScriptOrchestrator {
persona: PersonaWithFeatures | null;
facts: FactRow[];
agent?: { name: string | null; roleTitle: string };
/** 监护人(未成年触达对象)— loadPlanContext 查好传入;成人/无 → null */
guardian?: { relationship: string; relationshipLabel: string; name: string | null } | null;
}): DraftPlanScriptInput {
const { plan, patient, persona, facts, agent } = args;
const { plan, patient, persona, facts, agent, guardian } = args;
const patientAge = patient.birthDate ? calcAge(patient.birthDate) : null;
// ⭐ 就诊事件回退:跟 plan-aggregate.serializeProfile 同口径
// encounter_record(appointment.in_time != null 才产)很多 host 缺,改用 EMR 兜底
......@@ -388,12 +406,14 @@ export class PlanScriptOrchestrator {
return {
patient: {
// 注意:LLM 用的 nameMasked 是"路先生/女士"通话称呼,不是脱敏掩码"路*"
// (后者是 UI 列表展示用,客服通话不能直接念"路星")
nameMasked: nameSpokenForm(patient.name, patient.gender),
// ⭐ 通话称呼(单一源,年龄+性别+监护人 aware):未成年→打给监护人(妈妈→"X女士"),
// 无监护人姓名→"X家长";成人→"X先生/女士"。header/开场白共用此值,不再各算一遍。
nameMasked: callSalutation(patient.name, patient.gender, patientAge, guardian ?? null),
gender: patient.gender,
age: patient.birthDate ? calcAge(patient.birthDate) : null,
age: patientAge,
medicalRecordNumber: patient.medicalRecordNumber ?? null,
// 监护人(未成年):让话术知道"打给谁、患者是孩子",可空
guardian: guardian ? { relationshipLabel: guardian.relationshipLabel, name: guardian.name } : null,
},
// 临时:hardcoded jvs-dw 诊所字典(TODO #56 接 host 字典或新建 clinics 表)
// ⚠️ 直接吐 UUID 进 prompt 会让 LLM 编造"XX 客服中心",必须翻译成中文名
......@@ -658,6 +678,59 @@ function nameSpokenForm(name: string | null, gender: string | null): string {
return surname ? `${surname}先生` : '您'; // 性别未知默认先生(亚洲诊所语境),可调
}
const RELATIONSHIP_LABEL_ZH: Record<string, string> = {
mother: '妈妈',
father: '爸爸',
grandparent: '祖辈',
};
/**
* 从关系行里挑监护人:优先已建档(有姓名),关系优先 妈妈 > 爸爸 > 祖辈。
*/
function pickGuardian(
rels: Array<{ relationship: string; relatedPatient: { name: string | null } | null }>,
): { relationship: string; relationshipLabel: string; name: string | null } | null {
const order = ['mother', 'father', 'grandparent'];
const sorted = [...rels].sort((a, b) => {
const an = a.relatedPatient?.name ? 0 : 1;
const bn = b.relatedPatient?.name ? 0 : 1;
if (an !== bn) return an - bn; // 有姓名优先
return order.indexOf(a.relationship) - order.indexOf(b.relationship);
});
const g = sorted[0];
if (!g) return null;
return {
relationship: g.relationship,
relationshipLabel: RELATIONSHIP_LABEL_ZH[g.relationship] ?? '家长',
name: g.relatedPatient?.name ?? null,
};
}
/**
* 通话称呼(单一源:年龄 + 性别 + 监护人 aware)。
* - 未成年(≤12,打给监护人):妈妈→"{监护人姓}女士"、爸爸→"{监护人姓}先生";
* 无监护人姓名 → "{患者姓}家长"。
* - 成人:姓 + 先生/女士(性别未知默认先生)。
*/
function callSalutation(
name: string | null,
gender: string | null,
age: number | null,
guardian: { relationship: string; name: string | null } | null,
): string {
const surname = (name ?? '').charAt(0);
if (age != null && age <= 12) {
const gName = (guardian?.name ?? '').trim();
if (gName) {
const gs = gName.charAt(0);
if (guardian!.relationship === 'mother') return `${gs}女士`;
if (guardian!.relationship === 'father') return `${gs}先生`;
}
return surname ? `${surname}家长` : '您';
}
return nameSpokenForm(name, gender);
}
/**
* 临时 jvs-dw 诊所字典(TODO #56:接 host clinic 字典或建 PAC clinics 表)。
......
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