Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a799582d8 |
@@ -1,9 +1,5 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-12 — 校正还没打开时,卡片不说已经开始(待验收)
|
||||
|
||||
- 明确要求做校正时,对话里的卡片在真正打开并绑定之前仍是普通提议,按钮是「开始核对出生时间」。打开失败可以再点,不会写成「已经开始」。Skill 版本不变。未推送,未部署。
|
||||
|
||||
## 2026-10-12 — 对话里明说要校正就直接进入(待验收)
|
||||
|
||||
- 用户这一轮明确要求做生时校正时,直接打开校正,不先写解读、不用点卡。推得不准时的提议仍走卡片。做不了会用一句话说明,不再沉默。普通对话不承诺收窄出生时间,申报时段不写大运、子运的起止年月日。
|
||||
|
||||
+3
-17
@@ -17538,7 +17538,7 @@
|
||||
|
||||
## BUG-1306 | 对话里明说「帮我做生时纠正」进不了校正:模型不出提议卡,反而在普通对话里自己「收窄出生时间」
|
||||
|
||||
- 状态:fixed-pending-verify(F3 跟进在 `codex/rectification-offer-opened-state-20261012`,未 push,未部署;不是 resolved。T0 无主机日志,T5 无模型凭据)
|
||||
- 状态:fixed-pending-verify(实现在 `codex/rectification-explicit-entry-20261012`,未 push;T0 无主机日志,T5 无模型凭据)
|
||||
- 首次发现 / 最近更新:2026-10-11 / 2026-10-12
|
||||
- 来源:产品在 staging(`de4cfeb8`)新对话第一句输入「请你帮我进行生时纠正」,回复截图转述。
|
||||
- 现象:回复显示「已完成 3 步」,正文说「收窄出生时间这件事能做」,要用户报有年月的经历,「对得上,区间就能往里收」;全文没有提到提议卡,界面上也没有卡片。正文还按「代表分钟」写出大运 / 子运起止年月。
|
||||
@@ -17551,12 +17551,11 @@
|
||||
- 推断(未证实):正文主动承诺能收窄,不像收到「不要提校正」的拒绝,更像模型没有调用提议工具;无模型凭据、无 staging 运行日志,暂未取证。另:用户若照做在普通对话里报经历,这些经历不会进入校正引擎计分,正文「区间就能往里收」是做不到的承诺;按代表分钟写大运起止,与申报时段指令「精确应期一律不可用」不符。
|
||||
- 待做:取该轮观测记录确认是否调用了工具及服务端结果(任务书 T0;观测只打到 web 容器控制台,需 staging 主机日志)。本轮没有主机和数据库,查法见进度记录,不阻塞修复。
|
||||
- 修复:不新建工具。`offer-birth-time-rectification` 增加 `explicitRequest`。这一轮明确要校正由模型填写;`userAsked` 仍是「提议过之后用户自己再要求才出卡」。服务端判定仍是 `decideRectificationOffer`(本人、开关、无进行中 case、有价)。通过则记下与提议卡同结构、状态直接为 `opened`、尚无 caseId 的记录,`run.completed` 带 `rectificationDirectOpen`,浏览器调用已有的 `openRectificationFromChat`。同一轮已有直达或已有提议时不再开第二次。`bindOfferToCase` 也会绑定这种还没有 caseId 的 `opened` 记录,刷新后按 caseId 找回。进行中的 case 不新开:查到 `agentic_rectification_cases.session_id` 就打开那次,查不到就说可以在历史对话里接着做。`product_disabled` 与 `pricing_unavailable` 对用户是同一句「出生时间核对这会儿还做不了。」家人仍是「给家人校正暂时还不支持」。四种拒绝说明都不再写「不要提校正」。没有明确要求时的拒绝仍走原来的排盘回答。直达和明确拒绝把首轮合同视为就绪,不跑「运行合同不完整」重试、不跑空答重试、不跑 Pass4,已流出的正文换成那一句。这一轮结算走 `complete_consultation_free`,不走 `complete_consultation_response`;新开时再把提议补到会话消息上,不改请求行。规则仍只定义在 `RECTIFICATION_OFFER_RULE`:不承诺收窄出生时间,不在对话里收集经历做校正。申报时段指令补「大运、子运只说在哪一段附近,不写起止年月日」。不改校正引擎、打分、闸门和数据库。
|
||||
- 跟进(F3,2026-10-12,未部署):结算不再把记录写成 `opened`。直达通过仍记同结构提议,状态保持 `offered`,只在要自动开案时带 `autoOpen: true`(省略该字段的旧记录照常解析)。卡片在开案并绑定来源之前是普通提议:价格句和「开始核对出生时间」,不说「已经开始」。浏览器仍按 `rectificationDirectOpen` 自动开案,成功路径不用点。只有 `bindOfferToCase` 在开案成功、来源绑定后把状态改成 `opened`。打开失败不把本地记录改成 `opened`,卡片可再点。不新增第四种状态。`complete_consultation_free` 与五句用户文案未改。
|
||||
- 验证:`npx tsc --noEmit` 0 错。`npm run lint` 0 error(125 条既有 warning)。定向 `rectification-explicit-entry-20261012` 与 `rectification-in-chat-20261007` 26 通过。合同套件 236 条里 235 通过;唯一失败是 Skill 符号链接 EPERM。`CI=1 npm test` 4922 条,通过 4780,失败 101,cancelled 0;失败在本轮没改的文件(无 Docker、符号链接、WSL、路径拼接、junction 下的依赖解析)。telemetry 合同按直达结算改了一条断言,改完 4 条通过。默认 `next build` 因 worktree `node_modules` junction 被 Turbopack 拒绝;`npm run build -- --webpack` 编译与 TypeScript 已过,收集 page data 时 Skill 符号链接 EPERM,没有路由表,`/` Static 与首屏 gzip 未测。T5 未跑模型。
|
||||
- 防复发:`frontend/tests/rectification-explicit-entry-20261012.test.ts` 锁住直达、同一轮不重复开案、无 caseId 的 opened 绑定与找回、四种拒绝说明、两条路径不重试、普通咨询仍重试一次、规则只定义一处、申报时段「在哪一段附近」、结算调用 `complete_consultation_free`。F3 另锁:结算记录是 `offered` 且 `autoOpen: true`、卡片在绑定前不说「已经开始」、绑定后才说已经开始、打开失败仍是 `offered`、旧的 `offered` / `opened` / `returned` 照常解析且 `returned` 不出卡。用户可见句长度 ≤20。真机清单 `docs/testing/rectification-explicit-entry-20261012.md`。
|
||||
- 防复发:`frontend/tests/rectification-explicit-entry-20261012.test.ts` 锁住直达、同一轮不重复开案、无 caseId 的 opened 绑定与找回、四种拒绝说明、两条路径不重试、普通咨询仍重试一次、规则只定义一处、申报时段「在哪一段附近」、结算调用 `complete_consultation_free`。用户可见句长度 ≤20。真机清单 `docs/testing/rectification-explicit-entry-20261012.md`。
|
||||
- 相关记录:BUG-1247、BUG-1248(入口改为提议卡)、BUG-976/977(禁正则判意图)。
|
||||
- 复发自:无。
|
||||
- 修复版本:直达入口已在 staging `b8bdc003`,未标 resolved,未上生产。F3 跟进为本分支提交,未 push,未部署。
|
||||
- 修复版本:本分支提交,未 push,未部署。
|
||||
- 决策日志(2026-10-12,产品):选 A——用户明确要求做生时校正时直接进入校正,不先写解读、不用点卡;意图由模型判断,禁正则 / 关键词。**推翻 BUG-1248 两处**:①明确要求的这一轮不再「只出卡、点卡才进」;②`product_disabled`、`pricing_unavailable`「正文不要提校正」改为必须一句话说明做不了,不得静默。普通对话模型不得承诺收窄出生时间、不得在对话里收集经历假装校正;申报时段下不得写大运 / 子运精确起止。服务端本人 / 开关 / 进行中 case / 价格判定不变。同日另立 BUG-1307(最近分界开场)、BUG-1308(父母静态题研究)。
|
||||
|
||||
## BUG-1307 | 生时校正开场不说记录时间离最近分界多近,首轮选题面向整个窗口,用户觉得步骤多、目的不明
|
||||
@@ -17581,16 +17580,3 @@
|
||||
- 相关记录:BUG-1306、BUG-1307、BUG-1141 / BUG-1240(同类研究均未过门)。
|
||||
- 复发自:无。
|
||||
- 修复版本:不适用(研究)。
|
||||
|
||||
## BUG-1309 | 分盘段扫描端点写死 Raman:非 Raman 用户的盘型段与档位按 Raman 算,引擎打分按用户岁差
|
||||
|
||||
- 状态:open
|
||||
- 首次发现 / 最近更新:2026-10-12 / 2026-10-12
|
||||
- 来源:BUG-1307 分支验收(`b285011a`)时发现;同一端点在 staging 现存。
|
||||
- 现象:`/api/rectification/v5/varga-segments`(`scripts/rectification_varga_segments_api.py`)的 profile、逐分钟排盘与返回体都写死 `ayanamsa: "raman"`;调用方 `v9/segment-scan.ts` 不传岁差。校正引擎打分用 `resolveAyanamsa(snapshot)`。
|
||||
- 影响面:选了非 Raman 岁差的用户,交付卡 D9 / D10 段、档位与「不用校正」判断按 Raman 算,可能与打分所用岁差的段边界不一致(分界随岁差移动,私人样例一例 Raman 与 Lahiri 差约 7 分钟)。默认 Raman 用户不受影响。
|
||||
- 根因:BUG-1115 引入段扫描端点时只按默认岁差实现,任务书未要求透传岁差。
|
||||
- 修复:见 `docs/tasks/TASK-rectification-1012-batch-fix-20261012.md` F1 / F1b(端点收岁差并进缓存键,两处调用传用户岁差,缺省 raman,v5 回放逐字节不变)。
|
||||
- 相关记录:BUG-1115、BUG-1307、BUG-1306。
|
||||
- 复发自:无。
|
||||
- 修复版本:未修复。
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# PROGRESS · 校正提议在开案成功后才标已开始(BUG-1306 F3,2026-10-12)
|
||||
|
||||
分支 `codex/rectification-offer-opened-state-20261012`,基线 `b8bdc003`(`origin/staging`)。未 push,未部署。状态保持 fixed-pending-verify,不是 resolved。
|
||||
|
||||
## 结论
|
||||
|
||||
明确要求做校正时,结算记下的提议仍是普通提议:状态 `offered`,并带 `autoOpen: true`。卡片写价格和题数,按钮是「开始核对出生时间」,不说「已经开始」。浏览器仍按 `rectificationDirectOpen` 自动开案。只有开案成功、`bindOfferToCase` 绑定来源之后,状态才变成 `opened`,卡片才说「已经开始」。打开失败时记录保持 `offered`,卡片可以再点。没有第四种状态。已存的 `offered` / `opened` / `returned` 照常解析。`complete_consultation_free` 和五句用户文案没改。
|
||||
|
||||
## 实现
|
||||
|
||||
`finishExplicitRectificationRequest` 不再把结算记录写成 `opened`。`autoOpen` 是可选字段,只在为真时写入;旧记录省略它也能解析。刷新走 `parseRectificationOfferRecord`,不会丢掉这个字段。`persistOpenedDirectOffer` 仍把这条还没打开的记录挂到免费回复上。客户端新开仍调用 `openRectificationFromChat`,接着做仍调用 `openRectificationSession`。开案函数失败时返回空,调用处不把本地记录改成 `opened`。成功仍由原来的绑定路径改状态。
|
||||
|
||||
## 既有断言
|
||||
|
||||
| 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| `rectification-explicit-entry-20261012.test.ts`「explicit pass…」两处 `deepEqual` 期望 `offer("opened")` | `offer("offered", { autoOpen: true })` | F3:结算保持 offered,用 autoOpen 表示这一轮要自动开案;开案绑定后才是 opened |
|
||||
| 同文件「explicitRequest bypasses…」`rectificationOffer.status` 期望 `"opened"` | `"offered"`,且 `autoOpen === true` | 同上。直达入口 `kind: "new"` 不变 |
|
||||
| 同文件直达流「run.completed」`rectificationOffer.status` 期望 `"opened"` | `"offered"`,且 `autoOpen === true`;`rectificationDirectOpen.mode` 仍是 `"new"` | 完成事件转发的是结算记录,不能再预写 opened |
|
||||
| 同文件两处流式夹具把 `state.rectificationOffer` 设成 `offer("opened")` | `offer("offered", { autoOpen: true })` | 夹具要模拟现在的结算记录,否则事件仍像开案前就已经 opened |
|
||||
|
||||
`rectification-in-chat-20261007.test.ts` 里原来的期望值没有改。普通提议仍是没有 `autoOpen` 的 `offered`。无 caseId 的旧 `opened` 记录仍能绑定。
|
||||
|
||||
## 验证
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `npx tsx --test tests/rectification-explicit-entry-20261012.test.ts` | 16 通过,0 失败,cancelled 0 |
|
||||
| `npx tsx --test tests/rectification-in-chat-20261007.test.ts` | 15 通过,0 失败,cancelled 0 |
|
||||
| `npx tsc --noEmit`(frontend) | 0 错 |
|
||||
|
||||
没有跑 `next build`(Turbopack 拒绝 `node_modules` junction)。没有跑全量 `npm test`。没有 `npm install`,没有删 junction。`Home()` 没有改,`page.tsx` 没有改。
|
||||
@@ -127,7 +127,6 @@
|
||||
| `TASK-rectification-explicit-entry-20261012.md` | `PROGRESS-rectification-explicit-entry-20261012.md` | **明说要校正就直接进入(BUG-1306)**:模型判断意图(禁正则),服务端判定后直接开案并绑定来源对话;首轮排盘合同对此轮豁免;拒绝必须一句话说明(推翻 BUG-1248 静默两条);普通对话不得假装校正、申报时段不写子运起止。**先做** | 待验收 | — |
|
||||
| `TASK-rectification-nearest-boundary-opening-20261012.md` | — | **最近分界开场(BUG-1307)**:开场说记录时间离最近分盘换段点多近、两侧哪些层不同,近分界时首轮只问该分界;不改打分 / 闸门 / 4 件 3 域,77 例回放不降;改 page.tsx / 校正开场组件的部分须在 BUG-1306 合入后 | 待领取 | — |
|
||||
| `TASK-rectification-d12-parent-probes-research-20261012.md` | — | **父母静态是非题(D12)研究(BUG-1308)**:公开案例 ≥60、预登记、乱序与正对照,过门才立实现单;纯研究可并行 | 待领取 | — |
|
||||
| `TASK-rectification-1012-batch-fix-20261012.md` | `PROGRESS-rectification-offer-opened-state-20261012.md` | **10-12 三单验收修复**:entry `6a799582` 通过(合入待产品);boundary `b285011a` 未通过——F1 开场与段汇总岁差写死 Raman(BUG-1307 / 新 BUG-1309)、F2 本命上升换段不说;F3 提议在开案成功后才标 opened(BUG-1306 跟进,本分支待验收);F4 研究报告补编码问题与暂停结论(BUG-1308) | F3 待验收(`codex/rectification-offer-opened-state-20261012`);F1/F2/F4 仍待领取 | — |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# TASK · 生时校正 10-12 三单验收修复(2026-10-12)
|
||||
|
||||
- 基线:`origin/staging` @ `e6baff95`(三条功能分支都从这里分出)。
|
||||
- 受影响分支:
|
||||
- `codex/rectification-explicit-entry-20261012` @ `6a799582`(BUG-1306):**验收通过**,本单 F3 是合入后的跟进项。
|
||||
- `codex/rectification-nearest-boundary-opening-20261012` @ `b285011a`(BUG-1307):**未通过**,F1 修完才能合入。
|
||||
- `codex/rectification-d12-parent-probes-research-20261012` @ `11fee0a3`(BUG-1308):研究结论成立,F4 补齐文档后可合入。
|
||||
- 串行顺序:explicit-entry 先合入 staging(由产品点头)→ 把 F1/F2 做在 nearest-boundary 分支上,然后在 explicit-entry 之上变基(试合只冲突 `CHANGELOG.md`、`docs/tasks/README.md`,都是文档)→ F3 从合入后的 staging 新开分支。F4 与上述并行。
|
||||
- 合入 staging 一律等产品明说,执行方只推各自的功能分支。
|
||||
|
||||
## 1. 验收实证
|
||||
|
||||
| 分支 | 门禁 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| explicit-entry | tsc 0;lint 0 error / 125 warning(同基线);npm test 5013 条,失败 25(24 条同基线,另 1 条 `consult-evidence-lookup-20260927`「a lookup does not reset the answer clock」是负载下计时抖动,单独跑 3/3 通过);build `/` Static;gzip +0.02%;page.tsx 未增长 | 通过;T0 日志取证、T5 模型实测为环境缺口 |
|
||||
| nearest-boundary | tsc 0;lint 同基线;npm test 5006 条,失败 25 逐名同基线;build Static;gzip 0%;Python 定向 136 passed;打分 / decision_policy / 闸门 / api_server diff 为空 | 门禁过,**F1 缺陷不过** |
|
||||
| d12 research | 15 测试过;结果 JSON 两次复跑逐字节一致(除 Python 版本号);预登记 `ad89e410` 早于打分 `11fee0a3`,其后 0 改动;冻结身份 / 隐私 94 项绿 | 结论「欠力」成立;下一步与编码问题未写进报告 |
|
||||
|
||||
## 2. 缺陷与根因
|
||||
|
||||
**F1(P1,BUG-1307)开场分界按 Raman 算,引擎按用户岁差算。**
|
||||
`scripts/rectification_varga_segments_api.py` 三处写死 `"ayanamsa": "raman"`(profile、逐分钟 `compute_chart`、payload);`frontend/src/lib/rectification-agentic/opening-boundary.ts::fetchOpeningBoundary` 请求体不带岁差。校正引擎打分用 `resolveAyanamsa(snapshot)`(`v9/engine-client.ts`)。分界随岁差移动:私人样例一例里 Raman 与 Lahiri 的分界相差约 7 分钟,分界落在记录时间两侧。结果:非 Raman 用户开场说的「差几分钟、两边哪里不同」与引擎实际扫描矛盾;选题开关打开后更会自相矛盾。
|
||||
任务书 `TASK-rectification-nearest-boundary-opening-20261012` 没有写岁差要求,是任务书漏写,不是执行方违规。
|
||||
|
||||
**F1b(P2,新 BUG-1309)同一端点给盘型段汇总用时也写死 Raman。**
|
||||
`v9/segment-scan.ts`(BUG-1115 那批)调同一个 `/api/rectification/v5/varga-segments`,同样不传岁差。非 Raman 用户的交付卡 D9 / D10 段与档位按 Raman 算,引擎打分按用户岁差。这是 staging 现存问题,F1 改端点时一并改。
|
||||
|
||||
**F2(P2,BUG-1307)本命上升也换段时开场不说。**
|
||||
开场只列「婚恋、住处、学业、子女、父母、钱、身体」;D1 也换时这是最大的变化,却不提。
|
||||
|
||||
**F3(P3,BUG-1306 跟进)打开失败时卡片仍说「已经开始」。**
|
||||
提议记录在服务器结算时就写成 `opened`,早于浏览器真正开案;开案失败(断网、冲突)时对话显示「出生时间核对已经开始。」。点卡片「继续核对」可重开,不卡死。
|
||||
|
||||
**F4(文档,BUG-1308)研究报告缺下一步与编码问题。**
|
||||
6 例计入中 3 例编码站不住:奥巴马「母亲照料更多=是」引文只讲上哪所小学;伊梅尔达·马科斯「父亲体制内=是」引文是自己执业律师;索菲亚·罗兰「父亲体制内=是」引文是国家铁路临时工作(预登记规定短期任职不算主业,赫本父亲名誉领事却记「不清楚」,尺度不一)。另李小龙「父亲指导学业=是」依据是教太极(未计入)。维基百科计入率约 8%,凑满预登记的 60 例需 700+ AA 例,现有数据做不到;岁差改判 36–42%(门槛 20%)。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
1. F1:开场与段汇总一律用 `resolveAyanamsa(snapshot)`,与打分同源;岁差进缓存键。不新增「强制选岁差」(产品 10-03 已拒)。
|
||||
2. F2(Claude 定口径):D1 也换段时,开场先说「这里连本命上升都换」,再说其余会变的事;其余不变。
|
||||
3. F3:结算时提议记录写 `opening`(或保持 `offered` 并带 `autoOpen` 标记),浏览器开案成功、来源绑定后才改 `opened`;失败时卡片按 `offered` 显示,可点开。具体状态名由执行方定,须向后兼容已存的 `offered` / `opened` / `returned`。
|
||||
4. F4:本方向**暂停**,不立实现单。报告写明:继续的唯一路线是换数据源(传记 / 访谈)+ 双人独立编码 + 重新预登记;且岁差敏感 36–42% 本身使过门可能性很低。BUG-1308 状态改 `closed_by_design`(研究欠力,方向暂停)。
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. 不改打分、`decision_policy`、闸门、4 件 3 域、`scripts/jyotish_api_server.py`;不改迁移。
|
||||
2. F1 后 v5 77 例(全 Raman)的段汇总与回放结果必须与改前逐字节一致——岁差参数缺省值仍为 raman。
|
||||
3. 用户文案不得出现秒级、「记录时间是错的 / 不准」。
|
||||
4. 改既有断言写「原值 / 新值 / 原因」三栏;文档同提交更新(BUG_HISTORY、CHANGELOG、VOICE、DESIGN)。
|
||||
5. 不得写入真实用户出生资料。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
| 项 | 内容 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| F1 | 端点收 `ayanamsa`(白名单,与 `resolveAyanamsa` 取值一致,缺省 raman),进 profile、`compute_chart`、payload、缓存键;`fetchOpeningBoundary` 与 `segment-scan.ts` 传 `resolveAyanamsa(snapshot)` | Python 测试:同窗口 raman / lahiri 分界不同且缓存键不同;缺省等于显式 raman;前端测试:两处调用的请求体带用户岁差;v5 回放逐字节一致 |
|
||||
| F2 | D1 换段时开场句式 | 前端测试:D1 换 / 不换两种文案;VOICE 记录 |
|
||||
| F3 | 提议状态在开案成功后才转 `opened` | 前端测试:开案失败后卡片可再开、文案不说「已经开始」;旧会话三种状态照常显示 |
|
||||
| F4 | 报告与 BUG-1308 补「编码问题清单 + 下一步二选一结论 + 暂停」 | 文档;`tests/test_repo_privacy_markers.py` 过 |
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
F1 不可让。F2 可延到下一轮(写进 BUG-1307 已知限制)。F3 可单独排期。F4 不可让(不写清下一步,研究等于没收口)。
|
||||
|
||||
## 7. 开工前置
|
||||
|
||||
```
|
||||
git fetch origin --prune
|
||||
git fetch origin +refs/heads/codex/rectification-nearest-boundary-opening-20261012:refs/remotes/origin/codex/rectification-nearest-boundary-opening-20261012
|
||||
git fetch origin +refs/heads/codex/rectification-d12-parent-probes-research-20261012:refs/remotes/origin/codex/rectification-d12-parent-probes-research-20261012
|
||||
git status -sb
|
||||
```
|
||||
|
||||
在原 worktree 续做(或按同名分支新建 worktree);F3 待 explicit-entry 合入后从 `origin/staging` 开 `codex/rectification-offer-opened-state-20261012`。
|
||||
|
||||
## 8. BUG 编号
|
||||
|
||||
F1、F2 记入 BUG-1307;F3 记入 BUG-1306;F4 记入 BUG-1308;F1b 新建 **BUG-1309**(开工时核对最大号,写作时 1308)。
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
不新增等待动画;打开校正沿用既有一次揭幕。
|
||||
|
||||
**明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。开案还没成功时,同一张提议卡仍是普通提议(不说「已经开始」,按钮仍是「开始核对出生时间」)。开案成功并绑定来源后才换成已开始的文案。打开失败时按钮仍可点。
|
||||
**明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。
|
||||
|
||||
## 用量页分段计时(2026-10-05,在途)
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
## 生时校正从对话进入(2026-10-07,BUG-1247~1251;2026-10-12,BUG-1306)
|
||||
|
||||
- **何时提议**:用户说推得不准时先分清读法问题和时间问题。只是不认同某个解释,按纠正规则认下、改读,不提校正。不承诺能收窄出生时间,不在对话里收集经历做校正。说出过去的具体事情和盘对不上、说出生时间是估的或记不准、但没有明确要做校正,才调用提议工具。同一次对话提议过、用户没接,不再主动提;用户自己再要求时传 `userAsked=true`。规则只定义在 `consultation-thinking-plan.ts` 的 `RECTIFICATION_OFFER_RULE`。不用关键词判断。
|
||||
- **明确要求则直接进入**:用户这一轮明确要做校正,由模型判断,不用关键词。服务端放行后直接打开校正:不先写解读,不用点卡。过渡只有一句「好,我们开始核对出生时间。」已经有一次没做完的,直接接着做那一次,不新开。开案还没成功时,回答下面仍是普通提议卡,不说「已经开始」;打开失败可以再点「开始核对出生时间」。开案并绑定来源之后才改成「已经开始」。
|
||||
- **明确要求则直接进入**:用户这一轮明确要做校正,由模型判断,不用关键词。服务端放行后直接打开校正:不先写解读,不用点卡。过渡只有一句「好,我们开始核对出生时间。」已经有一次没做完的,直接接着做那一次,不新开。
|
||||
- **做不了必须说一句**:开关关着,或暂时没有价格,都说「出生时间核对这会儿还做不了。」不写内部原因,不写价格,不得沉默。家人档案说「给家人校正暂时还不支持」,不开案。有进行中的校正但这次打不开原来的会话,说「可在历史对话里接着做。」
|
||||
- **正文怎么提**(没有明确要求、只是时间可能不准):一句,放在最后一段(例:「如果想确认出生时间准不准,可以用下面的卡片,答几道过去经历的题核对一下。」),不写价格和题数——卡片上写。
|
||||
- **卡片**:「可以用几道过去经历的题,核对一下你的出生时间。大约 N 题,会用掉 M 点;开始答第一题时才扣。」按钮「开始核对出生时间」;已开始(开案并绑定来源之后,不是结算当时)「出生时间核对已经开始。」/「继续核对出生时间」。点数来自 `feature_pricing`,不写死。
|
||||
- **卡片**:「可以用几道过去经历的题,核对一下你的出生时间。大约 N 题,会用掉 M 点;开始答第一题时才扣。」按钮「开始核对出生时间」;已开始「出生时间核对已经开始。」/「继续核对出生时间」。点数来自 `feature_pricing`,不写死。
|
||||
- **回来**:分隔线「出生时间改为 HH:MM(生时校正)」/「出生时间没有改」;之前的回答上方「按校正前的时间」;按钮「按新时间重新看:「原问题」」。校正面按钮「回到刚才的对话」。不再说「新建对话」。
|
||||
|
||||
## 生时校正服务器固定句(2026-10-07,BUG-1258)
|
||||
|
||||
@@ -26,8 +26,6 @@ export function RectificationOfferCard({
|
||||
onStart,
|
||||
}: Readonly<{ offer: RectificationOfferRecord; disabled: boolean; onStart: () => void }>) {
|
||||
if (offer.status === "returned") return null;
|
||||
// 「已经开始」 / 继续核对 only after the case is bound. autoOpen still shows
|
||||
// the ordinary offer and 「开始核对出生时间」 until status is opened.
|
||||
const opened = offer.status === "opened";
|
||||
return (
|
||||
<div className="rectification-offer-card" role="group" aria-label="核对出生时间">
|
||||
|
||||
@@ -156,7 +156,7 @@ export type ConsultationRunParams = {
|
||||
setOnboardingPaywallOpen?: (open: boolean) => void;
|
||||
openRectificationSession: (exactSessionId: string) => Promise<void> | void;
|
||||
/** Direct entry from this consultation (BUG-1306). Same opener as the offer card. */
|
||||
openRectificationFromChat: (sourceSessionId: string) => Promise<unknown> | void;
|
||||
openRectificationFromChat: (sourceSessionId: string) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export function useConsultationRun(params: ConsultationRunParams) {
|
||||
@@ -1077,13 +1077,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
if (rectificationDirectOpen?.mode === "resume" && rectificationDirectOpen.resumeSessionId) {
|
||||
void openRectificationSession(rectificationDirectOpen.resumeSessionId);
|
||||
} else if (rectificationDirectOpen?.mode === "new") {
|
||||
// Stored offer stays offered. bindOfferToCase marks opened only after
|
||||
// the case opens. A failed open must not mark it opened here.
|
||||
void Promise.resolve(openRectificationFromChat(rectificationDirectOpen.sourceSessionId)).then((opened) => {
|
||||
if (opened) return;
|
||||
}, () => {
|
||||
// The opener threw. Leave the stored offer offered.
|
||||
});
|
||||
void openRectificationFromChat(rectificationDirectOpen.sourceSessionId);
|
||||
}
|
||||
try {
|
||||
await persistSession(completedSession);
|
||||
|
||||
@@ -369,8 +369,8 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
|
||||
|
||||
/** C4: the offer card under a consultation answer starts (or resumes) the case. */
|
||||
async function openRectificationFromChat(sourceSessionId: string) {
|
||||
if (rectificationLimitedToSelf()) return null;
|
||||
return openRectificationCase("homepage", null, sourceSessionId);
|
||||
if (rectificationLimitedToSelf()) return;
|
||||
await openRectificationCase("homepage", null, sourceSessionId);
|
||||
}
|
||||
|
||||
async function openRectificationSession(exactSessionId: string) {
|
||||
|
||||
@@ -64,11 +64,9 @@ async function writeMessages(client: SupabaseClient, userId: string, sessionId:
|
||||
}
|
||||
|
||||
/**
|
||||
* After a free direct-entry reply is appended, hang the offer on that message.
|
||||
* The record is not opened yet (`offered` + `autoOpen`); bindOfferToCase sets
|
||||
* opened after the case actually opens. Idempotent: an offer already on the
|
||||
* message (including one a later bind gave a case id) is left as it is.
|
||||
* Does not touch consultation_requests.
|
||||
* After a free direct-entry reply is appended, hang the opened offer on that
|
||||
* message. Idempotent: an offer already on the message (including one a later
|
||||
* bind gave a case id) is left as it is. Does not touch consultation_requests.
|
||||
*/
|
||||
export async function persistOpenedDirectOffer(input: {
|
||||
client: SupabaseClient;
|
||||
|
||||
@@ -28,12 +28,6 @@ export const rectificationOfferRecordSchema = z.object({
|
||||
offeredAt: z.string().min(1).max(40),
|
||||
caseId: z.string().uuid().optional(),
|
||||
rectificationSessionId: z.string().uuid().optional(),
|
||||
/**
|
||||
* Present only when this turn should open the case without a click.
|
||||
* Writers omit it unless it is true. Older stored offers omit it too.
|
||||
* The card stays an ordinary offer until status is opened (BUG-1306 F3).
|
||||
*/
|
||||
autoOpen: z.boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export type RectificationOfferRecord = z.infer<typeof rectificationOfferRecordSchema>;
|
||||
@@ -155,9 +149,8 @@ export function bindOfferToCase(
|
||||
for (let index = stored.length - 1; index >= 0; index -= 1) {
|
||||
const offer = parseRectificationOfferRecord(stored[index]!.rectificationOffer);
|
||||
if (!offer || offer.caseId) continue;
|
||||
// Offered cards, including a direct open that is still offered (autoOpen).
|
||||
// An older direct-open row stored as opened before it had a case id still binds.
|
||||
// Returned rows, and a card already bound to another case, are left alone.
|
||||
// A direct open records `opened` before the case id exists (BUG-1306).
|
||||
// An offer already bound to another case is left alone.
|
||||
if (offer.status !== "offered" && offer.status !== "opened") continue;
|
||||
const next = [...stored];
|
||||
next[index] = {
|
||||
@@ -171,8 +164,7 @@ export function bindOfferToCase(
|
||||
|
||||
/**
|
||||
* After `complete_consultation_free` appends the one-sentence reply, hang the
|
||||
* offer on that message so a reload can still find the source. The record is
|
||||
* not opened yet (still `offered`, with `autoOpen` on a direct open). Returns
|
||||
* opened offer on that message so a reload can still find the source. Returns
|
||||
* the next array, "present" when that message already has an offer (do not
|
||||
* overwrite a later case id), or "missing" when the free reply is not there yet.
|
||||
*/
|
||||
|
||||
@@ -1073,7 +1073,6 @@ function offerRecord(
|
||||
hook: RectificationOfferHook,
|
||||
decision: Extract<RectificationOfferDecision, { ok: true }>,
|
||||
status: RectificationOfferRecord["status"],
|
||||
autoOpen = false,
|
||||
): RectificationOfferRecord {
|
||||
return {
|
||||
status,
|
||||
@@ -1081,16 +1080,13 @@ function offerRecord(
|
||||
questionEstimate: decision.questionEstimate,
|
||||
sourceQuestion: hook.revisitQuestion,
|
||||
offeredAt: (hook.now?.() ?? new Date()).toISOString(),
|
||||
// Only when true. Ordinary offers and refusals omit the key.
|
||||
...(autoOpen ? { autoOpen: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit request (BUG-1306): the server already decided. A pass records an
|
||||
* offered card with autoOpen and tells the stream to open. Status stays
|
||||
* offered until the case is bound. A case already in progress resumes that
|
||||
* case. Anything else is one sentence and writes no offer.
|
||||
* opened offer with no case id yet and tells the stream to open. A case
|
||||
* already in progress resumes that case. Anything else is one sentence.
|
||||
*/
|
||||
async function finishExplicitRectificationRequest(
|
||||
state: ConsultationRuntimeState,
|
||||
@@ -1098,7 +1094,7 @@ async function finishExplicitRectificationRequest(
|
||||
decision: RectificationOfferDecision,
|
||||
) {
|
||||
if (decision.ok) {
|
||||
state.rectificationOffer = offerRecord(hook, decision, "offered", true);
|
||||
state.rectificationOffer = offerRecord(hook, decision, "opened");
|
||||
state.rectificationDirectEntry = {
|
||||
kind: "new",
|
||||
sentence: RECTIFICATION_DIRECT_NEW_COPY,
|
||||
|
||||
@@ -4,10 +4,7 @@ import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { createElement } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
|
||||
import { RectificationOfferCard } from "../src/components/rectification-in-chat-cards.tsx";
|
||||
import { consultationAgentPublicEventSchema, createNdjsonParser, type AgentExecutionReceipt, type ConsultationAgentPublicEvent } from "../src/lib/consultation-agent-events.ts";
|
||||
import { RECTIFICATION_OFFER_RULE } from "../src/lib/consultation-thinking-plan.ts";
|
||||
import {
|
||||
@@ -16,14 +13,10 @@ import {
|
||||
RECTIFICATION_DIRECT_RESUME_COPY,
|
||||
RECTIFICATION_DIRECT_RESUME_ELSEWHERE_COPY,
|
||||
RECTIFICATION_DIRECT_UNAVAILABLE_COPY,
|
||||
RECTIFICATION_OFFER_OPENED_COPY,
|
||||
RECTIFICATION_OFFER_START_LABEL,
|
||||
RECTIFICATION_OFFER_TOOL_ID,
|
||||
attachOpenedOfferToFreeMessage,
|
||||
bindOfferToCase,
|
||||
offerForCase,
|
||||
parseRectificationOfferRecord,
|
||||
rectificationOfferCardCopy,
|
||||
sourceFromSessionRows,
|
||||
type RectificationOfferRecord,
|
||||
} from "../src/lib/rectification-in-chat.ts";
|
||||
@@ -186,15 +179,13 @@ test("the free reply can carry the opened offer once, without clobbering a case
|
||||
assert.equal(withCase, "present");
|
||||
});
|
||||
|
||||
test("explicit pass opens once, records an offered auto-open offer, and does not open again", async () => {
|
||||
test("explicit pass opens once, records an opened offer, and does not open again", async () => {
|
||||
const fresh = offerTool();
|
||||
const result = await fresh.tool.execute({ explicitRequest: true, userAsked: true });
|
||||
assert.equal(result.offered, true);
|
||||
assert.equal(result.directEntry, "new");
|
||||
assert.match(String(result.note), /好,我们开始核对出生时间/);
|
||||
assert.deepEqual(fresh.state.rectificationOffer, offer("offered", { autoOpen: true }));
|
||||
assert.equal(fresh.state.rectificationOffer?.status, "offered");
|
||||
assert.equal(fresh.state.rectificationOffer?.autoOpen, true);
|
||||
assert.deepEqual(fresh.state.rectificationOffer, offer("opened"));
|
||||
assert.equal(fresh.state.rectificationOffer?.caseId, undefined);
|
||||
assert.deepEqual(fresh.state.rectificationDirectEntry, {
|
||||
kind: "new",
|
||||
@@ -208,15 +199,14 @@ test("explicit pass opens once, records an offered auto-open offer, and does not
|
||||
assert.match(String(again.note), /已经在打开/);
|
||||
assert.match(String(again.note), /不要再写解读/);
|
||||
assert.equal(fresh.decisions(), 1);
|
||||
assert.deepEqual(fresh.state.rectificationOffer, offer("offered", { autoOpen: true }));
|
||||
assert.deepEqual(fresh.state.rectificationOffer, offer("opened"));
|
||||
});
|
||||
|
||||
test("explicitRequest bypasses an earlier offer, and userAsked alone still only shows a card", async () => {
|
||||
const direct = offerTool({ alreadyOffered: true });
|
||||
const opened = await direct.tool.execute({ explicitRequest: true });
|
||||
assert.equal(opened.directEntry, "new");
|
||||
assert.equal(direct.state.rectificationOffer?.status, "offered");
|
||||
assert.equal(direct.state.rectificationOffer?.autoOpen, true);
|
||||
assert.equal(direct.state.rectificationOffer?.status, "opened");
|
||||
assert.equal(direct.state.rectificationDirectEntry?.kind, "new");
|
||||
|
||||
const card = offerTool({ alreadyOffered: true });
|
||||
@@ -385,7 +375,7 @@ test("natal and window direct entry or explicit refusal skips the runtime-contra
|
||||
sourceSessionId: SOURCE_SESSION,
|
||||
...(kind === "resume" ? { resumeSessionId: RESUME_SESSION } : {}),
|
||||
};
|
||||
if (kind === "new") state.rectificationOffer = offer("offered", { autoOpen: true });
|
||||
if (kind === "new") state.rectificationOffer = offer("opened");
|
||||
yield { type: "text-delta", payload: { text: "然后写一段解读。" } };
|
||||
yield STOP_FINISH;
|
||||
})(),
|
||||
@@ -415,8 +405,7 @@ test("natal and window direct entry or explicit refusal skips the runtime-contra
|
||||
assert.equal(done.type === "run.completed" && done.responseKind, "smalltalk");
|
||||
assert.equal(done.type === "run.completed" && done.receipt, undefined);
|
||||
if (kind === "new") {
|
||||
assert.equal(done.type === "run.completed" && done.rectificationOffer?.status, "offered");
|
||||
assert.equal(done.type === "run.completed" && done.rectificationOffer?.autoOpen, true);
|
||||
assert.equal(done.type === "run.completed" && done.rectificationOffer?.status, "opened");
|
||||
assert.equal(done.type === "run.completed" && done.rectificationOffer?.caseId, undefined);
|
||||
assert.deepEqual(done.type === "run.completed" ? done.rectificationDirectOpen : undefined, {
|
||||
sourceSessionId: SOURCE_SESSION,
|
||||
@@ -461,7 +450,7 @@ test("a reading that already streamed is replaced by the direct-entry sentence",
|
||||
sentence: RECTIFICATION_DIRECT_NEW_COPY,
|
||||
sourceSessionId: SOURCE_SESSION,
|
||||
};
|
||||
state.rectificationOffer = offer("offered", { autoOpen: true });
|
||||
state.rectificationOffer = offer("opened");
|
||||
yield STOP_FINISH;
|
||||
})(),
|
||||
retry: async () => {
|
||||
@@ -579,9 +568,6 @@ test("the offer rule is defined once, and direct entry settles with the free com
|
||||
assert.match(hook, /openRectificationFromChat\(rectificationDirectOpen\.sourceSessionId\)/);
|
||||
assert.match(hook, /openRectificationSession\(rectificationDirectOpen\.resumeSessionId\)/);
|
||||
assert.equal(hook.match(/openRectificationFromChat\(rectificationDirectOpen\.sourceSessionId\)/g)?.length, 1);
|
||||
const directOpen = hook.slice(hook.indexOf("rectificationDirectOpen?.mode"), hook.indexOf("await persistSession(completedSession)"));
|
||||
assert.match(directOpen, /if \(opened\) return/);
|
||||
assert.doesNotMatch(directOpen, /status:\s*"opened"/);
|
||||
assert.match(hook, /responseKind === "smalltalk" \? \{[\s\S]*?rectificationOffer/);
|
||||
|
||||
const page = src("../src/app/(app)/page.tsx");
|
||||
@@ -600,85 +586,4 @@ test("the offer rule is defined once, and direct entry settles with the free com
|
||||
rectificationOffer: offer("opened"),
|
||||
rectificationDirectOpen: { sourceSessionId: SOURCE_SESSION, mode: "new" },
|
||||
});
|
||||
consultationAgentPublicEventSchema.parse({
|
||||
type: "run.completed",
|
||||
responseKind: "smalltalk",
|
||||
rectificationOffer: offer("offered", { autoOpen: true }),
|
||||
rectificationDirectOpen: { sourceSessionId: SOURCE_SESSION, mode: "new" },
|
||||
});
|
||||
|
||||
const surface = src("../src/hooks/use-rectification-surface.ts");
|
||||
const fromChat = surface.slice(surface.indexOf("async function openRectificationFromChat"), surface.indexOf("async function openRectificationSession"));
|
||||
assert.match(fromChat, /return null/);
|
||||
assert.match(fromChat, /return openRectificationCase\("homepage", null, sourceSessionId\)/);
|
||||
});
|
||||
|
||||
function cardHtml(record: RectificationOfferRecord) {
|
||||
return renderToString(createElement(RectificationOfferCard, { offer: record, disabled: false, onStart() {} }));
|
||||
}
|
||||
|
||||
test("a direct-open offer still shows the ordinary card until the case is bound", () => {
|
||||
const pending = offer("offered", { autoOpen: true });
|
||||
const html = cardHtml(pending);
|
||||
assert.match(html, /大约 8 题,会用掉 3 点/);
|
||||
assert.match(html, new RegExp(RECTIFICATION_OFFER_START_LABEL));
|
||||
assert.equal(html.includes(RECTIFICATION_OFFER_OPENED_COPY), false);
|
||||
assert.equal(html.includes("继续核对出生时间"), false);
|
||||
assert.match(html, new RegExp(rectificationOfferCardCopy(pending).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
|
||||
const messages = [{ role: "assistant", text: RECTIFICATION_DIRECT_NEW_COPY, responseKind: "smalltalk", rectificationOffer: pending }];
|
||||
const bound = bindOfferToCase(messages, { caseId: CASE_ID, rectificationSessionId: RECT_SESSION });
|
||||
const opened = bound![0]!.rectificationOffer as RectificationOfferRecord;
|
||||
assert.equal(opened.status, "opened");
|
||||
assert.equal(opened.autoOpen, true);
|
||||
assert.equal(opened.caseId, CASE_ID);
|
||||
const openedHtml = cardHtml(opened);
|
||||
assert.match(openedHtml, new RegExp(RECTIFICATION_OFFER_OPENED_COPY));
|
||||
assert.match(openedHtml, /继续核对出生时间/);
|
||||
assert.equal(openedHtml.includes(RECTIFICATION_OFFER_START_LABEL), false);
|
||||
});
|
||||
|
||||
test("a failed direct open leaves the offer offered and clickable", () => {
|
||||
const pending = offer("offered", { autoOpen: true });
|
||||
const messages = [{ role: "assistant", text: RECTIFICATION_DIRECT_NEW_COPY, responseKind: "smalltalk", rectificationOffer: pending }];
|
||||
const opened = null;
|
||||
const next = opened ? bindOfferToCase(messages, { caseId: CASE_ID, rectificationSessionId: RECT_SESSION }) : null;
|
||||
assert.equal(next, null);
|
||||
assert.equal((messages[0]!.rectificationOffer as RectificationOfferRecord).status, "offered");
|
||||
assert.equal((messages[0]!.rectificationOffer as RectificationOfferRecord).autoOpen, true);
|
||||
const html = cardHtml(pending);
|
||||
assert.equal(html.includes(RECTIFICATION_OFFER_OPENED_COPY), false);
|
||||
assert.match(html, new RegExp(RECTIFICATION_OFFER_START_LABEL));
|
||||
assert.equal(html.includes("disabled=\"\""), false);
|
||||
assert.equal(html.includes("disabled=\"disabled\""), false);
|
||||
});
|
||||
|
||||
test("stored offers offered, opened, and returned still parse; returned hides the card", () => {
|
||||
for (const status of ["offered", "opened", "returned"] as const) {
|
||||
const parsed = parseRectificationOfferRecord(offer(status));
|
||||
assert.equal(parsed?.status, status);
|
||||
assert.equal(parsed?.autoOpen, undefined);
|
||||
assert.equal("autoOpen" in (parsed ?? {}), false);
|
||||
}
|
||||
const flagged = parseRectificationOfferRecord(offer("offered", { autoOpen: true }));
|
||||
assert.equal(flagged?.status, "offered");
|
||||
assert.equal(flagged?.autoOpen, true);
|
||||
assert.equal(parseRectificationOfferRecord({ ...offer("opened"), extra: true }), null);
|
||||
assert.equal(cardHtml(offer("returned")), "");
|
||||
assert.match(cardHtml(offer("opened")), new RegExp(RECTIFICATION_OFFER_OPENED_COPY));
|
||||
assert.match(cardHtml(offer("offered")), new RegExp(RECTIFICATION_OFFER_START_LABEL));
|
||||
});
|
||||
|
||||
test("the free reply stores the direct-open offer still offered, with autoOpen", () => {
|
||||
// The record is not opened yet. The name of the attacher is historical.
|
||||
const sentence = RECTIFICATION_DIRECT_NEW_COPY;
|
||||
const card = offer("offered", { autoOpen: true });
|
||||
const attached = attachOpenedOfferToFreeMessage(
|
||||
[{ role: "user", text: "请帮我核对出生时间" }, { role: "assistant", text: sentence, responseKind: "smalltalk" }],
|
||||
{ sentence, offer: card },
|
||||
);
|
||||
assert.ok(Array.isArray(attached));
|
||||
assert.deepEqual(attached[1]!.rectificationOffer, card);
|
||||
assert.equal((attached[1]!.rectificationOffer as RectificationOfferRecord).status, "offered");
|
||||
assert.equal(attachOpenedOfferToFreeMessage(attached, { sentence, offer: card }), "present");
|
||||
});
|
||||
|
||||
@@ -279,53 +279,6 @@ test("the open route binds the offer only for a consultation source; the source
|
||||
assert.doesNotMatch(server, /createAdminSupabaseClient/);
|
||||
});
|
||||
|
||||
test("an auto-open offer shows the ordinary card until the case is bound, and a reload keeps autoOpen", async () => {
|
||||
const pending = offer("offered", { autoOpen: true });
|
||||
const waiting = [
|
||||
{ role: "user" as const, text: "请帮我核对出生时间" },
|
||||
{ role: "assistant" as const, text: "好,我们开始核对出生时间。", responseKind: "smalltalk" as const, rectificationOffer: pending },
|
||||
];
|
||||
const html = renderToString(createElement(ChatTranscript, transcriptProps(waiting, [])));
|
||||
assert.match(html, /开始核对出生时间/);
|
||||
assert.match(html, /大约 8 题,会用掉 3 点;开始答第一题时才扣。/);
|
||||
assert.doesNotMatch(html, /出生时间核对已经开始/);
|
||||
assert.doesNotMatch(html, /继续核对出生时间/);
|
||||
|
||||
const bound = bindOfferToCase(waiting, { caseId: CASE_ID, rectificationSessionId: RECT_SESSION })!;
|
||||
const openedHtml = renderToString(createElement(ChatTranscript, transcriptProps(bound as ChatMessage[], [])));
|
||||
assert.match(openedHtml, /出生时间核对已经开始/);
|
||||
assert.match(openedHtml, /继续核对出生时间/);
|
||||
assert.equal((bound[1]!.rectificationOffer as RectificationOfferRecord).status, "opened");
|
||||
assert.equal((bound[1]!.rectificationOffer as RectificationOfferRecord).autoOpen, true);
|
||||
|
||||
const failed = waiting;
|
||||
assert.equal((failed[1]!.rectificationOffer as RectificationOfferRecord).status, "offered");
|
||||
|
||||
const { readSessions } = await import("../src/lib/home-cloud-sync.ts");
|
||||
const reloaded = readSessions([{
|
||||
id: SOURCE_SESSION,
|
||||
title: "虚构",
|
||||
theme: "marriage",
|
||||
messages: [{ role: "assistant", text: "还没打开", rectificationOffer: pending }],
|
||||
}], null);
|
||||
assert.equal(reloaded.sessions[0]!.messages[0]!.rectificationOffer?.status, "offered");
|
||||
assert.equal(reloaded.sessions[0]!.messages[0]!.rectificationOffer?.autoOpen, true);
|
||||
const old = readSessions([{
|
||||
id: SOURCE_SESSION,
|
||||
title: "虚构",
|
||||
theme: "marriage",
|
||||
messages: [
|
||||
{ role: "assistant", text: "a", rectificationOffer: offer("offered") },
|
||||
{ role: "assistant", text: "b", rectificationOffer: offer("opened", { caseId: CASE_ID, rectificationSessionId: RECT_SESSION }) },
|
||||
{ role: "assistant", text: "c", rectificationOffer: offer("returned", { caseId: CASE_ID, rectificationSessionId: RECT_SESSION }) },
|
||||
],
|
||||
}], null);
|
||||
assert.deepEqual(old.sessions[0]!.messages.map((message) => message.rectificationOffer?.status), ["offered", "opened", "returned"]);
|
||||
assert.equal(old.sessions[0]!.messages[0]!.rectificationOffer?.autoOpen, undefined);
|
||||
const returnedHtml = renderToString(createElement(ChatTranscript, transcriptProps(old.sessions[0]!.messages.slice(2) as ChatMessage[], [])));
|
||||
assert.doesNotMatch(returnedHtml, /rectification-offer-card/);
|
||||
});
|
||||
|
||||
test("stored sessions keep the offer and the divider when the browser reads them back (BUG-1248/1250)", async () => {
|
||||
const { readSessions } = await import("../src/lib/home-cloud-sync.ts");
|
||||
const bound = bindOfferToCase(conversation(), { caseId: CASE_ID, rectificationSessionId: RECT_SESSION })!;
|
||||
|
||||
Reference in New Issue
Block a user