diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ace78be..ab1b68a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-07 — 生时校正里的固定提示语也换成温和口气(未上线) + +- 选择题回执、记不清时的回复、采用后的提示等约 30 句改写:不再是「已记录。……」这种回执腔,不用「请……」,不说「候选」这类内部词,采用后不再叫你「新建对话」(BUG-1258)。 +- 时间、范围、数字和「不是已确认的唯一出生分钟」这类边界说明一字不改;校正流程、打分与采用规则不变。 + ## 2026-10-07 — 生时校正也换成知心姐姐口吻,开场更快(未上线) - 生时校正的说话口吻和普通对话统一:温和、不催;你记不清的时候不评判,也不说教。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 98f82672..35893ba5 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16837,4 +16837,18 @@ - 防复发:测试锁两版共用口吻头、开场 / 只读切片章节与长度、旧版整本发送。 - 相关记录:BUG-1255(普通对话口吻)、BUG-1256(普通对话提示词精简)、BUG-621(历史版本须可打开)。 - 未做:每轮强制第一步 `rectification-read-case`(一次额外的模型调用)——改流程,待产品给真实步耗时数据后另定;服务器写的固定文案(如「已记录。这题先不计分,换一件事问。」)口吻未改。 +- 修复版本:staging `61d6f248`(2026-10-07 部署核对:health gitCommit=61d6f248;门禁 run 1756 一次通过)。真机未走,状态保持 fixed-pending-verify + +## BUG-1258 | 生时校正服务器固定句口气生硬(「已记录。」回执、「请……」、内部词) + +- 状态:fixed-pending-verify(分支 `codex/rectification-copy-voice-20261007`;真机前不标 resolved) +- 首次发现 / 最近更新:2026-10-07 / 2026-10-07 +- 来源:BUG-1257 后产品要求把服务器写死的提示语也改成知心姐姐口吻;Claude 出 15 句样稿,产品认可后全量改。 +- 影响面:`user-copy.ts`、`v9/choice-action.ts`、`v9/post-adopt-verdict.ts`、`v9/adopt-narration.ts`(及其旁白提示词里引用的同一句)、`v9/run-diagnostic.ts` 的重复文案、`rectification-chat-choice-run.ts` 兜底、`rectification-agentic-chat.tsx` 两句输入框提示。不改系统报错、写给模型的规则、题库与盘型说明、数字与诚实边界。 +- 现象:选择题回执「已记录。这题先不计分,换一件事问。」「已记录你的选择。这是盘外核对,不会改候选分数。」等系统回执腔;「请再说一次」「请先到资料里改」;「候选」「交付代表性工作时间」等内部词;采用后叫人「新建对话」。 +- 根因:固定句分轮加入,没有统一口吻规范;Agent 口吻改了,服务器句没跟上。 +- 修复:约 30 处按 VOICE.md「生时校正服务器固定句」规则改写(收到 / 好、麻烦、时间、不提新建对话)。防重复相关:`turn-narration.ts` 的 `CHOICE_ACK_RE` 加入新回执说法(旧说法保留),模型重复服务器回执仍被丢弃;`delivery-turn-guard.ts` 是按交付句包含判断,不依赖回执开头,只改注释。「这次没有重新比较」开头保留(被检测)。 +- 验证:改断言 12 个测试文件约 35 处(每处三栏注释);新增 `rectification-copy-voice-20261007.test.ts` 3 条(回执不以「已记录」开头、无「新建对话 / 请再说」、模型重复新回执被丢)。全量 4973 项 fail 24,与 `61d6f248` 逐条一致;tsc 0、lint 0 error;build `/` Static,首屏 gzip +26 B。 +- 防复发:上述新测试;VOICE.md 新增本节规则与对照表。 +- 相关记录:BUG-1257、BUG-1153 / BUG-1241(回执与交付句防重复,确认未受影响)、TASK-rectification-in-chat-step1-20261006 C8(同样不再叫人新建对话)。 - 修复版本:待发布 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index e36b92e7..9e5ac0ec 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -431,3 +431,4 @@ | —(直接执行,无任务书) | —(记录在 BUG-1255) | **回答口吻改知心姐姐、放开读心**(10-07 产品决定):人设统一;读心守三条(先答再读、可能/多半一句、当事人要有依据且未受冲);删冲突的旧规矩。推翻 BUG-1070 读心部分 | 已验收 | Claude 直接执行;真机清单 `docs/testing/consult-voice-sister-20261007.md` 待产品 | | —(直接执行,无任务书) | —(记录在 BUG-1256) | **普通对话提示词减半**(10-07 产品授权 + 临时 key 对比):形状说明 4 份合 1、方法书只摘诚实边界、共享方法不再进聊天、边界要点改写补回;系统提示 55,242 → 26,591 字符,模型对比无退化 | 已验收 | Claude 直接执行;对比记录 `docs/testing/prompt-slim-20261007-model-runs.md` | | —(直接执行,无任务书) | —(记录在 BUG-1257) | **生时校正口吻统一 + 开场 / 只读轮按章节附 Skill**:两版系统提示共用知心姐姐口吻头;开场 11,812 → 4,576 字符;读档案那一步(每轮多一次模型调用)待真实步耗时数据后另定 | 已验收 | Claude 直接执行 | +| —(直接执行,无任务书) | —(记录在 BUG-1258) | **生时校正服务器固定句改温和口吻**:约 30 句,产品认可 15 句样稿后全量改;防重复正则补新回执 | 已验收 | Claude 直接执行 | diff --git a/docs/testing/consult-voice-sister-20261007.md b/docs/testing/consult-voice-sister-20261007.md index 70039085..b702f68d 100644 --- a/docs/testing/consult-voice-sister-20261007.md +++ b/docs/testing/consult-voice-sister-20261007.md @@ -18,3 +18,5 @@ | 7 | 开一次新的生时校正 | 开场两句话温和、不催;没有「呀、哦、亲爱的」;开场比以前出来得快 | | 8 | 被问到某件事的年份时回「记不清了」 | 不评判、不说教,最多一句「记不清也没关系」,然后换一题 | | 9 | 从历史里打开一次很早以前的校正 | 能正常打开、能接着看结果(旧版本照旧整本发送) | +| 10 | 校正里点一个选择题选项 | 回执是「收到,…」或「好,…」,不是「已记录。…」;同一句不出现两遍 | +| 11 | 采用一个时间 | 提示说「之后的对话都按这个时间来看」,不说「新建对话」 | diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index 59860638..812a6cad 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -23,6 +23,26 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 首页开场语下面可以有一行今日趋势(今日星语卡片的 trend,每日生成);还没生成时写「今天的星语还没写出来。」,没有出生分钟时不写。入口按钮下面平时不写字,只在用户需要动手时写一句:有没做完的校正写「上次那次校正还没完成,可以在历史对话里接着做。」;当前人物不是本人写「生时校正暂时只支持本人。」。不再写「不确定出生时间时,用记得住的经历一步步缩小范围」「上次已经校正完,可以拿最新资料再来一次」,也不要用「·」把两句不相干的提示拼成一行。 +## 生时校正服务器固定句(2026-10-07,BUG-1258) + +校正里服务器写死的回执与提示,和 Agent 一样用知心姐姐口吻(BUG-1257),产品认可样稿后改: + +- 回执开头不用「已记录。」:范围没变用「收到,」,范围有变化用「好,」(避免「收到,范围收到……」)。 +- 不用「请……」,用「麻烦……」;「候选」在说给用户的句子里说成「时间」。 +- 不叫人「新建对话」:采用后说「之后的对话都按这个时间来看」。 +- 数字、时刻、范围与诚实边界(「不是已确认的唯一出生分钟」等)一字不改;不加语气词、不加形容词。 +- 系统报错(「请稍后重试」类)、写给模型的规则、题库与盘型说明不在这次范围。 + +| 原句 | 现在 | +| --- | --- | +| 已记录。这题先不计分,换一件事问。 | 好,这题先不算分,我们换一件事。 | +| 已记录你的选择。这是盘外核对,不会改候选分数。 | 收到。这一题只是帮着核对,不会改前面的比较结果。 | +| 已记录,范围没变。 | 收到,这一题没有让范围变窄。 | +| 已记录你的选择,并结束本次校正,交付当前可信区间和代表性工作时间。 | 好,这次校正先到这里。下面是目前最可信的时间范围,和可以先拿来用的那个时间。 | +| 这件我还没记上。请再说一次大概年月和发生的事。 | 这件我还没记上,麻烦再说一次大概年月和发生了什么。 | +| 前面几道题你多半选了"说不好",再问下去也分不开,先停在这里。 | 前面几道题大多说不好,这很正常;再问下去也分不出来,我们先停在这里。 | +| 这件事记不清,不算。 | 这件记不清也没关系,不算进去。 | + ## 分析清单不说给用户(2026-10-06,BUG-1253) 工具结果里的清单(受冲信号编号与条数、「分三件事看」的层名、「旺弱不说明顺不顺」这类规则、「从某宫数起第几宫」)是让模型判断用的,回答里只说判断结果。真机婚恋回答出现过「先说心动这一层」「再看成对居住的那一层」「宫主火星落在从它数起的第 6 位」「这条只说这颗星自己有劲,不说明事情就顺」,以及给 yogakaraka 自造的「合力星」——读者一句都看不懂。 @@ -76,7 +96,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | 本命上升已较稳,关系盘仍会换升。可再补一件记得时间的感情或关系变化。(右栏) | 本命上升已较稳,关系盘仍会换升。 | 右栏按阶段取前端文案,不念引擎的补经历句。 | | 看当前范围,或补一件带月份的经历(步骤条) | 看当前范围,对不上可以改选 | 同上。 | | 继续说你记得的人生经历,或回答刚才的问题…(输入框) | 回答上面的问题,或直接问我…;交付后「对结果有疑问,直接问就行…」 | 输入框不当邀请位。训练门没开时仍是「再说一件带年月的事」。 | -| 已记录,范围没变;15:00–15:03 领先,14:40–14:43 落后。(中间那段其实已被排除) | 已记录,排除了 14:40–14:43。 | 范围首尾没动、中间少了一段也是变化,不能说没变。 | +| 已记录,范围没变;15:00–15:03 领先,14:40–14:43 落后。(中间那段其实已被排除) | 好,排除了 14:40–14:43。(2026-10-07 起开头由「已记录,」改为「好,」,BUG-1258) | 范围首尾没动、中间少了一段也是变化,不能说没变。 | | (交付那一轮同时问)身体这边再问一次:…? | (不问) | 交付轮不带采集题。 | ## 生时校正:范围、时刻、吻合率由服务端说(2026-09-27,BUG-1055~1058) @@ -281,13 +301,13 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 | 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 | 承认不确定,不训人。 | | 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 | 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。代表分钟是 05:00,这只是代表性候选,不是已确认的唯一出生分钟。 | 同一边界语义,带进度,少法务腔。 | | 剩下的题分不开当前候选。更站得住的范围是 05:15,代表分钟 05:15。 | 再问下去也分不出更准的时间了。眼下更站得住的是 05:15。 | 单分钟不要把范围和代表分钟念两遍;「当前候选」是内部词。 | -| 可以从下面的时间里选一个采用。 / 下面的时间可以先用着。 | 我按你说的经历认真分析过了,下面是这次的结果。 | 这是认真分析后的结果,不是随便先用着;采用仍不是确认。区间交付卡必须就在这句下面。 | +| 可以从下面的时间里选一个采用。 / 下面的时间可以先用着。 | 你说的这些经历我都对照过了,下面是这次的结果。(2026-10-07 前为「我按你说的经历认真分析过了,……」,BUG-1258) | 这是认真分析后的结果,不是随便先用着;采用仍不是确认。区间交付卡必须就在这句下面。 | | 相对支持度 16 · 采用此时间 | 卡头主标题是范围「目前范围 05:00–05:15(对照了 N 件经历)」,副标题「最可能 HH:MM」(代表分钟,只在写百分比时出现;区分不开时不写)。一行至多三列。相同性格句只写一次。每列:有差别的性格句、人话经历对照、下一个时段,点「更像这个」;第一名比第二名高 5 个百分点及以上时,每列再写「相对可能性 N%」。缺数据写灰字「这一分钟还没对照」。不预标「排盘用」。 | 交付对象是范围和区间里并排的候选分钟,不是支持度数字。 | | 相对可能性 26% / 26% / 20%(前两名差不到 5 个百分点) | 不写任何数字,卡上一句:「这几个时刻按现在的方法区分不开。」(2026-09-29 起,原句「……补一件带年月的经历能帮助分开。」作废,BUG-1084) | 差距不明显的百分比只会让人误以为分出了高下(2026-09-26 产品决定,取代 09-08「三列都写相对可能性」的定稿)。排序与三列内容不变。 | | 已答6轮,再问下去也分不开04:53和05:00,没有年份的分盘题也不再问……两者相对支持度都是16。 | 再问下去也分不开 04:53 和 05:00。这两个时间按现有信息分不开。范围和代表分钟照常说。 | 旁白不得念内部计分词。卡上的数字按上一行的 5 个百分点规则显示,旁白不念百分比。 | | 候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。 | 每列用服务器拼好的一行时段;没有窗就写「未来一年没有明显的时段」;没算出来写「这一分钟还没对照」。 | 窗口按候选分钟各算,模型不写。不得把「没算」说成「没有窗」。 | | 「再补一件经历」后卡片消失、输入框上方空白 | 需要补经历时继续在输入框说下一件;交付卡不再提供「再补一件经历」按钮。 | 交付卡只负责选分钟。 | -| 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,新建对话即按这个时间排盘。有核对题:前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。答完核对题先说结果(BUG-1172):「这件事和 04:53 对得上。」/「这件事和 04:53 对不上;如果别的经历也对不上,可以回到上面的卡片改选。」/「这件事分不出 04:53 和别的时间。」/「这件事记不清,不算。」;盘型口径再接「盘型结论不变。」或「盘型结论有变化:…」。盘型口径下一件对不上但盘型没变:「这件事和 04:53 对不上。一件事还不足以改变盘型结论,先按 04:53 排盘。」不劝改选;变了时接「你采用的 04:53 仍在其中,不用改。」或「你采用的 04:53 的 D9 上升是天秤,和新的结论不一致;新的推荐时间是 05:02,可以点下面的「改用 05:02」。」(BUG-1173)。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 | +| 采用后会拿盘外核对来验证。 / 已采用 04:53 · 范围 … / 改选 / 之后新建对话即按此时间排盘。 | 没有核对题:已采用 04:53,之后的对话都按这个时间来看。有核对题:核对就到这里。之后的对话都按你采用的时间来看;哪天觉得对不上,随时可以改。(2026-10-07 起不再叫人「新建对话」,BUG-1258)答完核对题先说结果(BUG-1172):「这件事和 04:53 对得上。」/「这件事和 04:53 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。」/「这件事分不出 04:53 和别的时间。」/「这件事记不清,不算。」;盘型口径再接「盘型结论不变。」或「盘型结论有变化:…」。盘型口径下一件对不上但盘型没变:「这件事和 04:53 对不上。一件事还不足以改变盘型结论,先按 04:53 排盘。」不劝改选;变了时接「你采用的 04:53 仍在其中,不用改。」或「你采用的 04:53 的 D9 上升是天秤,和新的结论不一致;新的推荐时间是 05:02,可以点下面的「改用 05:02」。」(BUG-1173)。 | 采用状态在该列按钮上;整张卡留着。收尾句跟卡片同一列,不单独贴左,也不回输入框上方。 | | (采用后只念代表分钟)代表分钟是 05:12。 | 这 27 分钟里,事业方向、性格底色的判断是稳定的;婚恋(D9)会随分钟变,看盘时按范围读。这不是已确认的唯一出生分钟。 | 采用后先讲区间里什么稳、什么随分钟变;两句来自服务端,不是模型编的。 | | 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 | | 选择题处理失败,请稍后重试。 / 这道题已经过期,请回答当前问题(点选没提交上,下面又画出同一张卡) | 这次没提交上,请再点一次。 | 点选没到服务器时撤回已选,只留一张可点的卡,错误行就这一句。不说「稍后」,不把服务端状态码翻成话。 | diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index b033b186..3658ea49 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -653,11 +653,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { placeholder={resultNotice ? "当前结果仅供查看,暂不能选择或采用。" : readonly - ? "该校正已结束,只能查看历史;需要再次校正请新建。" + ? "这次校正已经结束,只能查看;想再校正一次,重新开始就好。" : showLiveChoiceCard ? "点上面的选项即可;想补一句细节再写" : (liveQuestionOnMessages || questionGap === "persisted_question") && collectSpokenPrompt - ? "请回答上面的问题…" + ? "回答上面的问题就好…" : !snapshotFailure && questionGap === "collect_waiting" ? RECTIFICATION_COLLECT_WAITING_PLACEHOLDER : questionGap === "delivered" diff --git a/frontend/src/lib/rectification-agentic/user-copy.ts b/frontend/src/lib/rectification-agentic/user-copy.ts index c26e8d81..f0a8e521 100644 --- a/frontend/src/lib/rectification-agentic/user-copy.ts +++ b/frontend/src/lib/rectification-agentic/user-copy.ts @@ -188,7 +188,7 @@ export function rangeDeliveryCollectClosedSubject(candidateCount?: number): stri /** BUG-1146: the adopt message that carries the first post-adopt check (VOICE 采用后一行). */ export function postAdoptCheckLead(savedTime: string | null | undefined): string { const clock = /^\d{2}:\d{2}/.exec(savedTime?.trim() ?? "")?.[0]; - return clock ? `已采用 ${clock}。再核对一件过去的事。` : "再核对一件过去的事。"; + return clock ? `已采用 ${clock}。我们再核对一件过去的事。` : "再核对一件过去的事。"; } export function rangeDeliveryCollectClosed(candidateCount?: number): string { @@ -202,27 +202,27 @@ export const RECTIFICATION_USER_COPY = { collectQuestionRetryByDomain: USER_COLLECT_QUESTION_RETRY, choicePrompt: "直接点下面的选项就行,打字回答也一样算数。", unclearFocusReply: "我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。", - classifierUnavailableReply: "这边没接上,把刚才那句再发一次就行。", - declaredWindowLockedReply: "搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。", + classifierUnavailableReply: "刚才没接上,麻烦把那句再发一次。", + declaredWindowLockedReply: "找的范围是开始时按你的资料定的,中途不改。想换个时间段,先在资料里改出生时间,再重新开始一次校正就好。", questionUpdated: "这一问刚换成新的,刷新后再答就行。", - adoptCue: "我按你说的经历认真分析过了,下面是这次的结果。", - hostNarrationFallback: "我按现有材料继续往下收。", + adoptCue: "你说的这些经历我都对照过了,下面是这次的结果。", + hostNarrationFallback: "我先按现在知道的,接着往下缩小范围。", collectHandoff: "接下来我们继续。", - evidenceNotRecorded: "这件我还没记上。请再说一次大概年月和发生的事。", - collectDeclinedAck: "记下了,这条按没有发生过记。", - collectSkippedAck: "记下了,这题先放着,后面换个问法再问一次。", + evidenceNotRecorded: "这件我还没记上,麻烦再说一次大概年月和发生了什么。", + collectDeclinedAck: "好,记下了:这件事没有发生过。", + collectSkippedAck: "好,这题先放一放,后面换个问法再问你。", firstDatedCollectInvite: FIRST_DATED_COLLECT_INVITE, - uncertaintyStop: "前面几道题你多半选了\"说不好\",再问下去也分不开,先停在这里。", - tiedFirstStop: "几个候选打成平手,问题已经分不开它们。", - probePoolExhaustedStop: "能分开候选的问题已经问完,先按现有材料给你结果。", - postAdoptVerifyDone: "前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。", - insufficientEventsGate: "再来一件记得大概年月的事就能开始筛。", - insufficientDomainsGate: "再来一件不是同一类的、记得大概年月的事就能开始筛。", + uncertaintyStop: "前面几道题大多说不好,这很正常;再问下去也分不出来,我们先停在这里。", + tiedFirstStop: "这几个时间打成了平手,再问也分不出来了。", + probePoolExhaustedStop: "能帮着区分的问题都问完了,我先按现在知道的给你结果。", + postAdoptVerifyDone: "核对就到这里。之后的对话都按你采用的时间来看;哪天觉得对不上,随时可以改。", + insufficientEventsGate: "再说一件记得大概年月的事,就能开始比了。", + insufficientDomainsGate: "再说一件不同类的、记得大概年月的事,就能开始比了。", lowDateQualityGate: "两件事的日期还没对清。", - noCandidatesGate: "当前还排不出可比较的候选时间。", + noCandidatesGate: "现在还排不出可以比较的时间。", forceMinuteAfterSubBlocks: "时段分不开,直接按分钟比。", - compareFailedRetry: "候选比较这次没跑成,下一句话时会自动再试。", - rescoreSkipped: "这次没有重新比较,稍后再比一次。", + compareFailedRetry: "这次比较没跑成,你说下一句的时候我会再试一次。", + rescoreSkipped: "这次没有重新比较,等下会再比一次。", /** BUG-1178: an event dated after today is not recorded or scored. */ futureDatedEvent: "有一件事的日期在今天之后,这件先不算;如果年份打错了,直接告诉我正确的年份就行。", lastSuccessfulCompareRange: "这是按上一次成功比较给出的范围。", @@ -354,8 +354,8 @@ export function postAdoptVerifyDoneCopy( if (hadVerifyQuestions) return RECTIFICATION_USER_COPY.postAdoptVerifyDone; const clock = time?.trim().slice(0, 5) ?? ""; return clock - ? `已采用 ${clock},新建对话即按这个时间排盘。` - : "已采用当前时间,新建对话即按这个时间排盘。"; + ? `已采用 ${clock},之后的对话都按这个时间来看。` + : "已采用当前时间,之后的对话都按这个时间来看。"; } export function withCompareFailedRetryNotice(body: string): string { diff --git a/frontend/src/lib/rectification-agentic/v9/adopt-narration-agent.ts b/frontend/src/lib/rectification-agentic/v9/adopt-narration-agent.ts index 8816e491..fa4dcd48 100644 --- a/frontend/src/lib/rectification-agentic/v9/adopt-narration-agent.ts +++ b/frontend/src/lib/rectification-agentic/v9/adopt-narration-agent.ts @@ -21,11 +21,11 @@ export const ADOPT_NARRATION_TIMEOUT_MS = 8_000; export const ADOPT_NARRATION_INSTRUCTIONS = `你只写生时校正采用卡出现时的旁白,不做决定,不改状态,不提问。 用 2 到 4 句中文对用户说清三件事:为什么这一轮不再往下问、现在给的范围和代表分钟是什么、采用之后会用哪些前事核对。 -若 post_adopt_verification 为空,必须写「采用后没有还能核对的前事,之后新建对话即按此时间排盘,对不上可改选。」,不得写「会拿……核对」。 +若 post_adopt_verification 为空,必须写「采用后没有还能核对的过去的事了,之后的对话都按这个时间来看;觉得对不上,随时可以改。」,不得写「会拿……核对」。 只能使用输入事实里出现的时间和年份;输入里没有的数字一律不要写。 不要写「相对支持度」「概率」「置信度」。两个时间分不开时说「这两个时间按现有信息分不开」,不要念分数。 只有 stop_facts 里真有「不再问」时才可以那么说。 -不要出现「确认」「精确」这两个词,不得再提问,不要写「我按你说的经历认真分析过了,下面是这次的结果」,不要说「当前候选」,不要说「先用着」。`; +不要出现「确认」「精确」这两个词,不得再提问,不要写「你说的这些经历我都对照过了,下面是这次的结果」,不要说「当前候选」,不要说「先用着」。`; export type AdoptNarrationOutcome = | "agent" diff --git a/frontend/src/lib/rectification-agentic/v9/adopt-narration.ts b/frontend/src/lib/rectification-agentic/v9/adopt-narration.ts index 946923b0..281b8dea 100644 --- a/frontend/src/lib/rectification-agentic/v9/adopt-narration.ts +++ b/frontend/src/lib/rectification-agentic/v9/adopt-narration.ts @@ -235,7 +235,7 @@ export function shouldWriteAdoptNarration(facts: AdoptDeliveryFacts): boolean { export function templatePostAdoptExplain(facts: AdoptDeliveryFacts): string { if (facts.post_adopt_verification.length === 0) { - return "采用后没有还能核对的前事,之后新建对话即按此时间排盘,对不上可改选。"; + return "采用后没有还能核对的过去的事了,之后的对话都按这个时间来看;觉得对不上,随时可以改。"; } const labels = facts.post_adopt_verification.map((item) => ( item.year_label ? `${item.year_label}${item.domain}` : item.domain diff --git a/frontend/src/lib/rectification-agentic/v9/choice-action.ts b/frontend/src/lib/rectification-agentic/v9/choice-action.ts index 6b319a45..dc6a0ad6 100644 --- a/frontend/src/lib/rectification-agentic/v9/choice-action.ts +++ b/frontend/src/lib/rectification-agentic/v9/choice-action.ts @@ -111,31 +111,31 @@ export function composeChoiceNarration(input: { segmentProduct?: boolean; }): string { if (input.optionId === "stop") { - return `已记录你的选择,并结束本次校正,交付当前可信区间和代表性工作时间。${RECTIFICATION_TERMINATION_COPY}`; + return `好,这次校正先到这里。下面是目前最可信的时间范围,和可以先拿来用的那个时间。${RECTIFICATION_TERMINATION_COPY}`; } if (input.optionId === "skip_probe") { - return "已记录。这题先不计分,换一件事问。"; + return "好,这题先不算分,我们换一件事。"; } if (input.answerClass === "unsure") { - return "已记录。这题先不计分,换一件事问。"; + return "好,这题先不算分,我们换一件事。"; } if (!input.scoring) { - return "已记录你的选择。这是盘外核对,不会改候选分数。"; + return "收到。这一题只是帮着核对,不会改前面的比较结果。"; } if (input.appliedInference) { const range = explainRangeChange(input.credibleBefore, input.credibleAfter); if (range && range !== "范围没变") { - return `已记录,${range}。`; + return `好,${range}。`; } // BUG-1086: an interior cluster left the range while the hull stayed put // (or the range split into segments). That is a change, never 「范围没变」. const excluded = explainExcludedRanges(input.excludedRanges ?? []); - if (excluded) return `已记录,${excluded}。`; + if (excluded) return `好,${excluded}。`; const movement = input.segmentProduct ? "" : explainScoreMovement(input.deltasByCluster ?? []); - if (movement) return `已记录,范围没变;${movement}。`; - return "已记录,范围没变。"; + if (movement) return `收到,这一题没有让范围变窄;${movement}。`; + return "收到,这一题没有让范围变窄。"; } - return "已记录你的选择。"; + return "收到。"; } export function shouldContinueAfterStructuredChoice( diff --git a/frontend/src/lib/rectification-agentic/v9/delivery-turn-guard.ts b/frontend/src/lib/rectification-agentic/v9/delivery-turn-guard.ts index 9c9e09db..0de07fb1 100644 --- a/frontend/src/lib/rectification-agentic/v9/delivery-turn-guard.ts +++ b/frontend/src/lib/rectification-agentic/v9/delivery-turn-guard.ts @@ -72,7 +72,7 @@ export function deliveryCarrierNarration(input: { /** * True when the latest assistant text carries the stored delivery sentence. - * The answered choice persists 「已记录,…」 + a blank line + that sentence, + * The answered choice persists 「收到,…」 (「已记录,…」 before BUG-1258) + a blank line + that sentence, * so this is a containment check, not equality (BUG-1241 fix sheet). * Whitespace is ignored. A needle shorter than 8 characters does not match. */ diff --git a/frontend/src/lib/rectification-agentic/v9/post-adopt-verdict.ts b/frontend/src/lib/rectification-agentic/v9/post-adopt-verdict.ts index e70787ac..d17f47f0 100644 --- a/frontend/src/lib/rectification-agentic/v9/post-adopt-verdict.ts +++ b/frontend/src/lib/rectification-agentic/v9/post-adopt-verdict.ts @@ -107,7 +107,7 @@ export function readoptOffer( export function postAdoptCheckVerdict(input: PostAdoptVerdictInput): string | null { const clock = input.acceptedTime?.trim().slice(0, 5) ?? ""; if (!clock || !input.answerClass) return null; - if (input.answerClass === "unsure") return "这件事记不清,不算。"; + if (input.answerClass === "unsure") return "这件记不清也没关系,不算进去。"; if (!input.probe) return null; const candidate = candidateForAdoptedMinute(clock, input.candidates); const outcome = input.probe.expected_outcomes.find((row) => row.answer_class === input.answerClass) ?? null; @@ -123,7 +123,7 @@ export function postAdoptCheckVerdict(input: PostAdoptVerdictInput): string | nu return fits ? `这件事和 ${clock} 对得上。` : clashes - ? `这件事和 ${clock} 对不上;如果别的经历也对不上,可以回到上面的卡片改选。` + ? `这件事和 ${clock} 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。` : `这件事分不出 ${clock} 和别的时间。`; } const fit = fits diff --git a/frontend/src/lib/rectification-agentic/v9/run-diagnostic.ts b/frontend/src/lib/rectification-agentic/v9/run-diagnostic.ts index 88616030..790724c5 100644 --- a/frontend/src/lib/rectification-agentic/v9/run-diagnostic.ts +++ b/frontend/src/lib/rectification-agentic/v9/run-diagnostic.ts @@ -165,7 +165,7 @@ const USER_COPY: Readonly> = { provider_error: "上游模型连接失败,状态已记录。", run_failed: "上游模型连接失败,状态已记录。", empty_stream: "本轮没有生成可展示的回复,状态已记录。", - evidence_not_written: "这件我还没记上。请再说一次大概年月和发生的事。", + evidence_not_written: "这件我还没记上,麻烦再说一次大概年月和发生了什么。", }; export function finishReasonFromErrorCode(errorCode: string | null | undefined): RectificationFinishReason { diff --git a/frontend/src/lib/rectification-agentic/v9/turn-narration.ts b/frontend/src/lib/rectification-agentic/v9/turn-narration.ts index 9402ab42..92dc04e5 100644 --- a/frontend/src/lib/rectification-agentic/v9/turn-narration.ts +++ b/frontend/src/lib/rectification-agentic/v9/turn-narration.ts @@ -139,7 +139,9 @@ export function overlayChoicePromptFromSpoken( return clipped; } -const CHOICE_ACK_RE = /已记录你的选择|已记下你刚才的选择|候选比较也随之更新|请看下方选项/; +// The server's own choice acknowledgements, old and new wording (BUG-1258): +// a model sentence that repeats one is dropped, the server already wrote it. +const CHOICE_ACK_RE = /已记录你的选择|已记下你刚才的选择|候选比较也随之更新|请看下方选项|这一题没有让范围变窄|这题先不算分|这一题只是帮着核对/; export const CHOICE_CARD_CONTINUATION_ACK = "接下来看下面这一问。"; diff --git a/frontend/src/lib/rectification-chat-choice-run.ts b/frontend/src/lib/rectification-chat-choice-run.ts index 4ad0ecb7..59361cde 100644 --- a/frontend/src/lib/rectification-chat-choice-run.ts +++ b/frontend/src/lib/rectification-chat-choice-run.ts @@ -224,7 +224,7 @@ export async function runRectificationStructuredChoice( } const narration = typeof payload?.narration === "string" && payload.narration.trim() ? payload.narration.trim() - : "已记录你的选择。"; + : "收到。"; setMessages((current) => { const withoutPlaceholder = current.filter((message) => message.renderKey !== assistantRenderKey); const withHistory = appendUnseenAssistantTurns( @@ -252,7 +252,7 @@ export async function runRectificationStructuredChoice( } else { const narration = typeof payload?.narration === "string" && payload.narration.trim() ? payload.narration.trim() - : "已记录你的选择。"; + : "收到。"; onMessagesChange?.([ { role: "assistant", text: narration }, ]); diff --git a/frontend/tests/agent-voice-copy-contract.test.ts b/frontend/tests/agent-voice-copy-contract.test.ts index 9d55f61f..8d862855 100644 --- a/frontend/tests/agent-voice-copy-contract.test.ts +++ b/frontend/tests/agent-voice-copy-contract.test.ts @@ -120,7 +120,8 @@ test("a single-minute range is not spoken twice as range and representative", () assert.doesNotMatch(text, /代表分钟 05:15/); assert.doesNotMatch(text, /更站得住的范围是 05:15/); assert.equal(containsBoundarySemantics(text), true); - assert.equal(RECTIFICATION_USER_COPY.adoptCue, "我按你说的经历认真分析过了,下面是这次的结果。"); + // 原值: 我按你说的经历认真分析过了,下面是这次的结果。 / 新值: 你说的这些经历我都对照过了,下面是这次的结果。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(RECTIFICATION_USER_COPY.adoptCue, "你说的这些经历我都对照过了,下面是这次的结果。"); }); test("default range narration stays semantic even without an opening window", () => { @@ -145,7 +146,8 @@ test("delivery range narration puts the stop reason before the progress clause", openingRange: ["14:00", "15:00"], stopReason: "user_uncertainty_too_high", }); - assert.match(text, /^前面几道题你多半选了"说不好"/); + // 原值: /^前面几道题你多半选了"说不好"/ / 新值: /^前面几道题大多说不好/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(text, /^前面几道题大多说不好/); assert.match(text, /已经从最初的 60 分钟收到 14:02–14:43 这 41 分钟/); assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.uncertaintyStop)); assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.tiedFirstStop)); diff --git a/frontend/tests/rectification-adopt-narration-20260904.test.ts b/frontend/tests/rectification-adopt-narration-20260904.test.ts index afa82786..0587887e 100644 --- a/frontend/tests/rectification-adopt-narration-20260904.test.ts +++ b/frontend/tests/rectification-adopt-narration-20260904.test.ts @@ -774,7 +774,8 @@ test("adoptDeliveryFacts names the representative, range, stop class, and post-a assert.equal(facts.post_adopt_verification[0]?.domain, acceptedPlan.next_followup.domain); } else { assert.deepEqual(facts.post_adopt_verification, []); - assert.match(templatePostAdoptExplain(facts), /没有还能核对的前事/); + // 原值: /没有还能核对的前事/ / 新值: /没有还能核对的过去的事/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(templatePostAdoptExplain(facts), /没有还能核对的过去的事/); } }); diff --git a/frontend/tests/rectification-answer-choice.test.ts b/frontend/tests/rectification-answer-choice.test.ts index ed35f0e1..eab37686 100644 --- a/frontend/tests/rectification-answer-choice.test.ts +++ b/frontend/tests/rectification-answer-choice.test.ts @@ -806,7 +806,8 @@ test("clicking A applies the choice without invoking a language model", async () start: "2015-01-01", end: "2015-12-31", }); - assert.match(applied.narration, /已记录,/); + // 原值: /已记录,/ / 新值: /(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /(?:收到|好),/); // 原值: 旁白下一问是家人采集「家里如果有结婚、添丁或住院」 // 新值: 训练门未开时邀请优先「还有吗?比如…」 // 原因: BUG-648 邀请优先于领域轮盘;撤回 BUG-642 年龄带年份 @@ -888,14 +889,16 @@ test("answering a discriminator persists the next dated card so GET still has a const persistedPrompt = schema.choice?.prompt; assert.ok(persistedPrompt); assert.notEqual(applied.narration, persistedPrompt); - assert.match(applied.narration, /已记录,/); + // 原值: /已记录,/ / 新值: /(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /(?:收到|好),/); // 原值: doesNotMatch(applied.narration, /2014/) // 新值: 正文是 ack+stem,含 2014 // 原因: BUG-969 可渲染选择题不得只靠 GET 挂题干 assert.match(applied.narration, /2014/); assert.equal( applied.narration, - composeCollectSpokenAssistantText("已记录,范围没变;05:10 领先,05:00 落后。", persistedPrompt), + // 原值: 已记录,范围没变; / 新值: 收到,这一题没有让范围变窄; / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + composeCollectSpokenAssistantText("收到,这一题没有让范围变窄;05:10 领先,05:00 落后。", persistedPrompt), ); const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn"); assert.equal(turn?.args.p_assistant_message, applied.narration); @@ -926,7 +929,8 @@ test("answering a discriminator persists the next dated card so GET still has a ); assert.equal(live?.question?.prompt, persistedPrompt); assert.equal(live?.text.includes(persistedPrompt), false); - assert.match(live?.text ?? "", /^已记录,/); + // 原值: /^已记录,/ / 新值: /^(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(live?.text ?? "", /^(?:收到|好),/); const refreshed = parseV9CaseDossier(twoProbeDossier()); assert.ok(refreshed); @@ -1040,7 +1044,8 @@ test("one unsure discriminator answer does not adopt and persists the next quest // 原值: applied.narration === "已记录。这题先不计分,换一件事问。" // 新值: 同一句后面追加下一题题干 // 原因: BUG-969 确定性回复流里要带选择题干 - assert.match(applied.narration, /^已记录。这题先不计分,换一件事问。/); + // 原值: 已记录。这题先不计分,换一件事问。 / 新值: 好,这题先不算分,我们换一件事。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /^好,这题先不算分,我们换一件事。/); assert.match(applied.narration, /2014 年前后/); assert.notEqual(applied.nextAction.session_outcome, "adopt_representative"); assert.equal(applied.nextAction.type, "ask_candidate_discriminator"); @@ -1093,11 +1098,13 @@ test("answering the last discriminator persists a year-locked family collect foc assert.equal(setFocus, undefined); assert.match(applied.narration, /目前范围 05:00–05:10/); assert.equal(applied.nextInterviewPersisted, true); - assert.match(applied.narration, /已记录,/); + // 原值: /已记录,/ / 新值: /(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /(?:收到|好),/); assert.doesNotMatch(applied.narration, /2021 年前后/); assert.doesNotMatch(applied.narration, /D24/); const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn"); - assert.match(String(turn?.args.p_assistant_message ?? ""), /已记录,/); + // 原值: /已记录,/ / 新值: /(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(String(turn?.args.p_assistant_message ?? ""), /(?:收到|好),/); assert.doesNotMatch(String(turn?.args.p_assistant_message ?? ""), /2021 年前后/); // 原值: 没有任何 focus 写入带 p_asked_turn_id === TURN_ID // 新值: 写入的 focus 只能是新问题本身(linkFocusAskedTurn 把新问题挂到 @@ -1160,7 +1167,8 @@ test("keeps the applied answer when narration persistence fails", async () => { assert.equal(applied.applied, true); assert.equal(applied.status, "applied"); assert.equal(applied.narrationPersisted, false); - assert.match(applied.narration, /已记录,/); + // 原值: /已记录,/ / 新值: /(?:收到|好),/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /(?:收到|好),/); }); test("stop_and_review does not write an inference transition", async () => { @@ -1402,7 +1410,8 @@ test("an unsure choice keeps applied inference and does not claim scores changed answerClass: "unsure", }); // 旧:appliedInference 仍说「更新了候选比较」→ 新:unsure 不计分旁白 → 保留 inference 行仍持久化 - assert.equal(narration, "已记录。这题先不计分,换一件事问。"); + // 原值: 已记录。这题先不计分,换一件事问。 / 新值: 好,这题先不算分,我们换一件事。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(narration, "好,这题先不算分,我们换一件事。"); assert.doesNotMatch(narration, /更新了候选比较/); }); @@ -1422,7 +1431,8 @@ test("a scoring choice narrates cluster movement and range change", () => { }); // 旧:领先/落后 +「范围从 04:31–05:07 收到 04:31–04:39」 // 新:范围变了仍只说收到/变为,不讲领先落后(BUG-606);范围不变才补领先/落后(BUG-634) - assert.equal(narrowed, "已记录,范围收到 04:31–04:39。"); + // 原值: 已记录,范围收到 / 新值: 好,范围收到 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(narrowed, "好,范围收到 04:31–04:39。"); assert.doesNotMatch(narrowed, /领先|落后/); assert.doesNotMatch(narrowed, /范围从 /); const unchanged = composeChoiceNarration({ @@ -1433,7 +1443,8 @@ test("a scoring choice narrates cluster movement and range change", () => { credibleBefore: ["04:31", "04:39"], credibleAfter: ["04:31", "04:39"], }); - assert.equal(unchanged, "已记录,范围没变;04:31–04:39 领先。"); + // 原值: 已记录,范围没变; / 新值: 收到,这一题没有让范围变窄; / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(unchanged, "收到,这一题没有让范围变窄;04:31–04:39 领先。"); const unchangedZero = composeChoiceNarration({ optionId: "A", scoring: true, @@ -1442,7 +1453,8 @@ test("a scoring choice narrates cluster movement and range change", () => { credibleBefore: ["04:31", "04:39"], credibleAfter: ["04:31", "04:39"], }); - assert.equal(unchangedZero, "已记录,范围没变。"); + // 原值: 已记录,范围没变。 / 新值: 收到,这一题没有让范围变窄。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(unchangedZero, "收到,这一题没有让范围变窄。"); const movement = composeChoiceNarration({ optionId: "A", scoring: true, @@ -1454,7 +1466,8 @@ test("a scoring choice narrates cluster movement and range change", () => { credibleBefore: ["04:45", "05:15"], credibleAfter: ["04:45", "05:15"], }); - assert.equal(movement, "已记录,范围没变;04:52–04:53 领先,05:08–05:12 落后。"); + // 原值: 已记录,范围没变; / 新值: 收到,这一题没有让范围变窄; / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(movement, "收到,这一题没有让范围变窄;04:52–04:53 领先,05:08–05:12 落后。"); const widened = composeChoiceNarration({ optionId: "A", scoring: true, @@ -1462,7 +1475,8 @@ test("a scoring choice narrates cluster movement and range change", () => { credibleBefore: ["04:45", "04:50"], credibleAfter: ["04:40", "05:00"], }); - assert.equal(widened, "已记录,范围变为 04:40–05:00。"); + // 原值: 已记录,范围变为 / 新值: 好,范围变为 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(widened, "好,范围变为 04:40–05:00。"); const unsure = composeChoiceNarration({ optionId: "D", scoring: true, @@ -1471,7 +1485,8 @@ test("a scoring choice narrates cluster movement and range change", () => { credibleBefore: ["04:31", "04:39"], credibleAfter: ["04:31", "04:39"], }); - assert.equal(unsure, "已记录。这题先不计分,换一件事问。"); + // 原值: 已记录。这题先不计分,换一件事问。 / 新值: 好,这题先不算分,我们换一件事。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(unsure, "好,这题先不算分,我们换一件事。"); }); test("answering A narrates the credible range even when the search window stays put", async () => { @@ -1506,7 +1521,8 @@ test("answering A narrates the credible range even when the search window stays // 旧:整段旁白等于「已记录,范围收到 04:31–04:31。」 // 新:旁白以该句开头;若下一问未能单独落库,服务端仍可能把采集题干接在后面 // 原因:BUG-606 决策 3(b) 锁的是点选旁白,不是下一问是否挂上同一条消息 - assert.match(applied.narration, /^已记录,范围收到 04:31–04:31。/); + // 原值: 已记录,范围收到 / 新值: 好,范围收到 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /^好,范围收到 04:31–04:31。/); assert.doesNotMatch(applied.narration, /范围没变/); assert.doesNotMatch(applied.narration, /领先|落后/); const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8"); @@ -1542,7 +1558,8 @@ test("answering A says the range did not change when the credible range stays pu optionId: "A", expectedRevision: previous.revision, }); - assert.match(applied.narration, /范围没变/); + // 原值: /范围没变/ / 新值: /没有让范围变窄/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied.narration, /没有让范围变窄/); assert.doesNotMatch(applied.narration, /范围从 /); }); diff --git a/frontend/tests/rectification-copy-voice-20261007.test.ts b/frontend/tests/rectification-copy-voice-20261007.test.ts new file mode 100644 index 00000000..402bbf86 --- /dev/null +++ b/frontend/tests/rectification-copy-voice-20261007.test.ts @@ -0,0 +1,42 @@ +// BUG-1258 (2026-10-07): the server-written rectification lines speak in the +// same warm voice as the agent (BUG-1257). No 「已记录。」 receipts, no 「请」, +// no 「新建对话」, and a model sentence that repeats a new server +// acknowledgement is still dropped. +import assert from "node:assert/strict"; +import test from "node:test"; + +import { RECTIFICATION_USER_COPY, postAdoptCheckLead, postAdoptVerifyDoneCopy } from "../src/lib/rectification-agentic/user-copy.ts"; +import { composeChoiceNarration } from "../src/lib/rectification-agentic/v9/choice-action.ts"; +import { bindSpokenToOpenQuestion } from "../src/lib/rectification-agentic/v9/turn-narration.ts"; + +test("choice acknowledgements open with 收到 / 好, never a 已记录 receipt (BUG-1258)", () => { + const lines = [ + composeChoiceNarration({ optionId: "skip_probe" } as never), + composeChoiceNarration({ optionId: "A", answerClass: "unsure" } as never), + composeChoiceNarration({ optionId: "A", scoring: false } as never), + composeChoiceNarration({ optionId: "A", scoring: true, appliedInference: true } as never), + composeChoiceNarration({ optionId: "A", scoring: true, appliedInference: false } as never), + ]; + for (const line of lines) { + assert.doesNotMatch(line, /^已记录/, line); + assert.match(line, /^(收到|好)/, line); + } + assert.match(composeChoiceNarration({ optionId: "stop" } as never), /^好,这次校正先到这里/); +}); + +test("server lines carry no 请-style orders and no 新建对话 (BUG-1258)", () => { + for (const [key, value] of Object.entries(RECTIFICATION_USER_COPY)) { + if (typeof value !== "string") continue; + assert.doesNotMatch(value, /新建对话/, key); + assert.doesNotMatch(value, /请再说|请先到/, key); + } + assert.equal(postAdoptVerifyDoneCopy("05:00", false), "已采用 05:00,之后的对话都按这个时间来看。"); + assert.doesNotMatch(postAdoptVerifyDoneCopy("05:00", true), /新建对话/); + assert.match(postAdoptCheckLead("05:00"), /^已采用 05:00。我们再核对一件过去的事。/); +}); + +test("a model sentence repeating a new server acknowledgement is dropped (BUG-1258)", () => { + const spoken = "收到,这一题没有让范围变窄。\n\n2016 年 9 月上大学记下了。"; + const kept = bindSpokenToOpenQuestion(spoken, "2014 年前后,有没有升学、转学或换学习环境?"); + assert.doesNotMatch(kept, /这一题没有让范围变窄/); +}); diff --git a/frontend/tests/rectification-delivery-ui-simplify-20260908.test.ts b/frontend/tests/rectification-delivery-ui-simplify-20260908.test.ts index 72a88863..c55fd98b 100644 --- a/frontend/tests/rectification-delivery-ui-simplify-20260908.test.ts +++ b/frontend/tests/rectification-delivery-ui-simplify-20260908.test.ts @@ -45,7 +45,8 @@ test("skipping the last reverse-verify keeps the original post-adopt sentence", ); assert.equal( postAdoptVerifyDoneCopy("05:07", false), - "已采用 05:07,新建对话即按这个时间排盘。", + // 原值: 已采用 05:07,新建对话即按这个时间排盘。 / 新值: 已采用 05:07,之后的对话都按这个时间来看。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + "已采用 05:07,之后的对话都按这个时间来看。", ); }); diff --git a/frontend/tests/rectification-futile-collect-stop-20260929.test.ts b/frontend/tests/rectification-futile-collect-stop-20260929.test.ts index 1d9f8c86..965cd5c7 100644 --- a/frontend/tests/rectification-futile-collect-stop-20260929.test.ts +++ b/frontend/tests/rectification-futile-collect-stop-20260929.test.ts @@ -221,7 +221,8 @@ test("an interior cluster leaving the range is narrated as 排除了, never 范 credibleAfter: ["14:35", "15:05"], excludedRanges: excluded, }); - assert.equal(narration, "已记录,排除了 14:40–14:43。"); + // 原值: 已记录,排除了 / 新值: 好,排除了 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(narration, "好,排除了 14:40–14:43。"); assert.doesNotMatch(narration, /范围没变/); // Split into segments (tuple null) also reads as an exclusion. const split = composeChoiceNarration({ @@ -233,7 +234,8 @@ test("an interior cluster leaving the range is narrated as 排除了, never 范 credibleAfter: null, excludedRanges: excluded, }); - assert.equal(split, "已记录,排除了 14:40–14:43。"); + // 原值: 已记录,排除了 / 新值: 好,排除了 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(split, "好,排除了 14:40–14:43。"); // Nothing left the range: the old sentence stays. assert.deepEqual(excludedClusterRanges(before, before), []); assert.equal(composeChoiceNarration({ @@ -244,7 +246,8 @@ test("an interior cluster leaving the range is narrated as 排除了, never 范 credibleBefore: ["14:35", "15:05"], credibleAfter: ["14:35", "15:05"], excludedRanges: [], - }), "已记录,范围没变。"); + // 原值: 已记录,范围没变。 / 新值: 收到,这一题没有让范围变窄。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + }), "收到,这一题没有让范围变窄。"); }); // ---- BUG-1087: delivery and a skipped-line re-ask in the same turn -------- diff --git a/frontend/tests/rectification-guided-collect-20260916.test.ts b/frontend/tests/rectification-guided-collect-20260916.test.ts index 1fc9b30f..1c356761 100644 --- a/frontend/tests/rectification-guided-collect-20260916.test.ts +++ b/frontend/tests/rectification-guided-collect-20260916.test.ts @@ -143,7 +143,8 @@ test("existence prompts cover the whole domain for every collect kind", () => { assert.equal(TARGETED_COLLECT_OPTION_B, "这类事都没有过"); assert.equal(GUIDED_WINDOW_OPTION_B, "这段没有"); assert.match(RECTIFICATION_USER_COPY.collectDeclinedAck, /没有发生过/); - assert.match(RECTIFICATION_USER_COPY.collectSkippedAck, /换个问法再问一次/); + // 原值: /换个问法再问一次/ / 新值: /换个问法再问你/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(RECTIFICATION_USER_COPY.collectSkippedAck, /换个问法再问你/); }); test("a layer that cannot split candidates still leaves that domain in the open pool", () => { diff --git a/frontend/tests/rectification-hidden-e2e.test.ts b/frontend/tests/rectification-hidden-e2e.test.ts index 5e6738fe..e651f503 100644 --- a/frontend/tests/rectification-hidden-e2e.test.ts +++ b/frontend/tests/rectification-hidden-e2e.test.ts @@ -335,7 +335,8 @@ test("narrator failure still leaves the choice applied and does not ask the user scoring: true, appliedInference: true, }); - assert.match(applied, /已记录,范围没变/); + // 原值: /已记录,范围没变/ / 新值: /收到,这一题没有让范围变窄/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(applied, /收到,这一题没有让范围变窄/); assert.doesNotMatch(applied, /请再选一次|重新回答/); const stopped = composeChoiceNarration({ optionId: "stop", @@ -343,8 +344,10 @@ test("narrator failure still leaves the choice applied and does not ask the user appliedInference: false, }); // Previously asserted “独立核对尚未完成 / 不是最终校正结果”; task 6 makes range delivery a completed terminal. - assert.match(stopped, /已记录你的选择/); - assert.match(stopped, /交付当前可信区间和代表性工作时间/); + // 原值: /已记录你的选择/ / 新值: /这次校正先到这里/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(stopped, /这次校正先到这里/); + // 原值: /交付当前可信区间和代表性工作时间/ / 新值: /目前最可信的时间范围/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(stopped, /目前最可信的时间范围/); assert.match(stopped, /当前最优结果是候选时间段/); assert.doesNotMatch(stopped, /未完成|失败|遗憾/); assert.doesNotMatch(stopped, /已完成验证/); diff --git a/frontend/tests/rectification-post-adopt-card-20261001.test.ts b/frontend/tests/rectification-post-adopt-card-20261001.test.ts index a05a4b58..685e4313 100644 --- a/frontend/tests/rectification-post-adopt-card-20261001.test.ts +++ b/frontend/tests/rectification-post-adopt-card-20261001.test.ts @@ -107,7 +107,8 @@ test("采用 writes its own turn, hangs the post-adopt check on it and quiets th const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn"); assert.equal(turn?.args.p_request_id, adoptTurnRequestId("req-1")); assert.equal(turn?.args.p_user_message, null); - assert.equal(turn?.args.p_assistant_message, "已采用 05:00。再核对一件过去的事。2018 年前后,有没有入职或职责明显加重?"); + // 原值: 已采用 05:00。再核对一件过去的事。 / 新值: 已采用 05:00。我们再核对一件过去的事。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(turn?.args.p_assistant_message, "已采用 05:00。我们再核对一件过去的事。2018 年前后,有没有入职或职责明显加重?"); const link = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus"); assert.equal(link?.args.p_asked_turn_id, "44444444-4444-4444-8444-444444444444"); assert.equal(alreadyDelivered({ caseId: CASE, resultId: RESULT, hasUserMessage: false }), true); @@ -136,7 +137,8 @@ test("segment product: choice ack has no minute leaderboard", () => { }; const minute = composeChoiceNarration(base as never); const segment = composeChoiceNarration({ ...base, segmentProduct: true } as never); - assert.equal(segment, "已记录,范围没变。"); + // 原值: 已记录,范围没变。 / 新值: 收到,这一题没有让范围变窄。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(segment, "收到,这一题没有让范围变窄。"); assert.doesNotMatch(segment, /领先|落后/); assert.match(minute, /领先/, "the minute product still reads the movement"); }); diff --git a/frontend/tests/rectification-post-adopt-verdict-20261002.test.ts b/frontend/tests/rectification-post-adopt-verdict-20261002.test.ts index 73ee8a64..9e45c783 100644 --- a/frontend/tests/rectification-post-adopt-verdict-20261002.test.ts +++ b/frontend/tests/rectification-post-adopt-verdict-20261002.test.ts @@ -42,9 +42,11 @@ test("verdict: fits, does not fit, cannot tell, and unsure", () => { assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "yes" }), "这件事和 04:55 对得上。"); // Minute product (no chart summary): the card is where a re-pick happens. assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "no" }), - "这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。"); + // 原值: 这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。 / 新值: 这件事和 04:55 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + "这件事和 04:55 对不上。要是别的经历也对不上,可以回到上面的卡片换一个时间。"); assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "weak_yes" }), "这件事分不出 04:55 和别的时间。"); - assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件事记不清,不算。"); + // 原值: 这件事记不清,不算。 / 新值: 这件记不清也没关系,不算进去。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件记不清也没关系,不算进去。"); assert.equal(postAdoptCheckVerdict({ ...base, answerClass: null }), null); assert.equal(postAdoptCheckVerdict({ ...base, acceptedTime: null, answerClass: "yes" }), null); assert.equal(postAdoptCheckVerdict({ ...base, probe: null, answerClass: "yes" }), null, "no probe, no claim"); diff --git a/frontend/tests/rectification-post-adopt-verify-20260904.test.ts b/frontend/tests/rectification-post-adopt-verify-20260904.test.ts index 412d0e4e..4635a5d8 100644 --- a/frontend/tests/rectification-post-adopt-verify-20260904.test.ts +++ b/frontend/tests/rectification-post-adopt-verify-20260904.test.ts @@ -142,7 +142,8 @@ test("empty reverse-verify list forbids a 会拿…核对 promise", () => { minutes: ["05:00", "05:06"], years: [], }; - assert.match(templatePostAdoptExplain(facts), /没有还能核对的前事/); + // 原值: /没有还能核对的前事/ / 新值: /没有还能核对的过去的事/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + assert.match(templatePostAdoptExplain(facts), /没有还能核对的过去的事/); const denied = validateAdoptNarration("采用后会拿 2016 年教育题来核对。", facts); assert.equal(denied.ok, false); if (!denied.ok) assert.equal(denied.reason, "promise"); diff --git a/frontend/tests/rectification-spoken-collect.test.ts b/frontend/tests/rectification-spoken-collect.test.ts index b0f3dcd4..0c408a6d 100644 --- a/frontend/tests/rectification-spoken-collect.test.ts +++ b/frontend/tests/rectification-spoken-collect.test.ts @@ -188,7 +188,8 @@ test("collect_spoken stem lives on turn.question inside the same assistant artic assert.doesNotMatch(chat, /showQuestionSlot/); assert.match( chat, - /\(liveQuestionOnMessages \|\| questionGap === "persisted_question"\) && collectSpokenPrompt\n\s+\? "请回答上面的问题…"/, + // 原值: "请回答上面的 / 新值: "回答上面的问题就好 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + /\(liveQuestionOnMessages \|\| questionGap === "persisted_question"\) && collectSpokenPrompt\n\s+\? "回答上面的问题就好…"/, ); // 原值: messages.some(同 focus_id) // 新值: 只认 latestSettledAssistant diff --git a/frontend/tests/rectification-window-cluster-cap-20260909.test.ts b/frontend/tests/rectification-window-cluster-cap-20260909.test.ts index 269bbce6..cb90a1c3 100644 --- a/frontend/tests/rectification-window-cluster-cap-20260909.test.ts +++ b/frontend/tests/rectification-window-cluster-cap-20260909.test.ts @@ -69,7 +69,8 @@ test("choice-focus declared window is answered from the route before the model", assert.deepEqual(ordinary.rpcCalls, []); assert.equal( RECTIFICATION_USER_COPY.declaredWindowLockedReply, - "搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。", + // 原值: 搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。 / 新值: 找的范围是开始时按你的资料定的,中途不改。想换个时间段,先在资料里改出生时间,再重新开始一次校正就好。 / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿) + "找的范围是开始时按你的资料定的,中途不改。想换个时间段,先在资料里改出生时间,再重新开始一次校正就好。", ); });