From 6ec8efd5d23771390037b54dce19abfc26eb769d Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 7 Oct 2026 11:39:34 +0800 Subject: [PATCH] feat(rectification): server-written lines in the same warm voice (BUG-1258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit About 30 fixed rectification lines lose the receipt tone: no 「已记录。」 openings (收到 when the range holds, 好 when it moves), 麻烦 instead of 请, 时间 instead of 候选 in what the user reads, and no 「新建对话」 after adoption. Numbers, clocks, ranges and the honesty boundaries are unchanged. CHOICE_ACK_RE also recognises the new acknowledgements, so a model sentence that repeats one is still dropped; the delivery guard matches the delivery sentence by containment and does not depend on the opening. Full suite 4973 / fail 24, identical to 61d6f248; build keeps / Static. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- CHANGELOG.md | 5 ++ docs/BUG_HISTORY.md | 14 +++++ docs/tasks/README.md | 1 + docs/testing/consult-voice-sister-20261007.md | 2 + frontend/docs/VOICE.md | 26 ++++++++-- .../components/rectification-agentic-chat.tsx | 4 +- .../lib/rectification-agentic/user-copy.ts | 38 +++++++------- .../v9/adopt-narration-agent.ts | 4 +- .../v9/adopt-narration.ts | 2 +- .../rectification-agentic/v9/choice-action.ts | 18 +++---- .../v9/delivery-turn-guard.ts | 2 +- .../v9/post-adopt-verdict.ts | 4 +- .../v9/run-diagnostic.ts | 2 +- .../v9/turn-narration.ts | 4 +- .../src/lib/rectification-chat-choice-run.ts | 4 +- .../tests/agent-voice-copy-contract.test.ts | 6 ++- ...ification-adopt-narration-20260904.test.ts | 3 +- .../tests/rectification-answer-choice.test.ts | 51 ++++++++++++------- .../rectification-copy-voice-20261007.test.ts | 42 +++++++++++++++ ...tion-delivery-ui-simplify-20260908.test.ts | 3 +- ...ation-futile-collect-stop-20260929.test.ts | 9 ++-- ...tification-guided-collect-20260916.test.ts | 3 +- .../tests/rectification-hidden-e2e.test.ts | 9 ++-- ...ification-post-adopt-card-20261001.test.ts | 6 ++- ...cation-post-adopt-verdict-20261002.test.ts | 6 ++- ...ication-post-adopt-verify-20260904.test.ts | 3 +- .../rectification-spoken-collect.test.ts | 3 +- ...cation-window-cluster-cap-20260909.test.ts | 3 +- 28 files changed, 199 insertions(+), 78 deletions(-) create mode 100644 frontend/tests/rectification-copy-voice-20261007.test.ts 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 认可样稿) + "找的范围是开始时按你的资料定的,中途不改。想换个时间段,先在资料里改出生时间,再重新开始一次校正就好。", ); });