Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
12 KiB
12 KiB
任务书 · 校正会话上露出普通输入框,发问打到 /api/consult 报「咨询会话不存在」(2026-09-17)
0. 基线
- 基线 commit:
eea90926(origin/staginghead;staging 已部署dc2f2a16,两者之间只有文档)。 - 分支:
codex/rectification-session-composer-guard-20260917,git worktree add -b codex/rectification-session-composer-guard-20260917 .worktrees/rectification-session-composer-guard-20260917 origin/staging。 - 范围:前端
use-consultation-run.ts、use-session-management.ts、use-rectification-surface.ts、page.tsx(只允许减行)、api/consult/route.ts的一处错误码,以及对应测试。不动 Python、不动迁移、不动 Skill、不动校正 Agent 与校正面组件。 - 串行:与
TASK-session-list-single-source-20260917.md同天改use-session-management.ts。本单先做;那一单从本单合入后的 staging 起分支。 - BUG 段:BUG-924 起(基线
docs/BUG_HISTORY.md最大号 BUG-923,开工时再核对一次)。
1. 事故实证
产品负责人 2026-09-17 19:1x(Asia/Shanghai)在 staging 桌面 Chrome 上,从一条生时校正会话里输入了一个感情问题,浏览器发出 POST /api/consult,服务端返回:
{"error":"咨询会话不存在","message":"请重新进入咨询。"}
用产品负责人的登录态只读核对 staging(不含任何身份信息):
| 项目 | 结果 |
|---|---|
| 请求体 | sessionId 指向一条会话,history: [],entryMode: direct_chart,consultationMode: verified_chart |
GET /api/sessions/<id> |
200,session_type = birth_time_rectification,rectification_case_id = null(所有校正会话都是 null,绑定在案例表里,正常),messages = 0,标题「9月17日 · 生时校正」 |
GET /api/rectification/cases/entry-summary |
有一个 collecting_evidence 的可续案例,lastActivityAt 与该会话 updated_at 同一毫秒 |
GET /api/rectification/cases/<caseId>?sessionId=<id> |
200,3 轮(开场 + 一问一答,都在 11:11–11:12 UTC 完成),next_user_action = ask_method_followup |
POST /api/rectification/cases/open(intent=session) |
200,disposition = resumed,Skill 10.0.29,0.9 s |
结论:校正会话与案例都健康,服务端能正常打开;是前端在这条会话激活时渲染了普通 ChatComposer,把问题发到了普通咨询接口。
代码定位(按符号):
frontend/src/app/api/consult/route.tsPOST:读chat_sessions后if (!chatSession || chatSession.session_type !== "consultation")统一回 404「咨询会话不存在」。判断正确,但没有独立错误码,前端无法区分「不存在」与「类型不对」。frontend/src/app/page.tsx:activeRectificationSession = activeSession?.sessionType === "birth_time_rectification";rectificationSurfaceOpen = activeRectificationSession && activeSession.id === rectificationSessionId。校正面只在rectificationSurfaceOpen && rectificationCaseId时挂载;否则渲染普通ChatComposer,其inputDisabled/submitBlocked只看rectificationSurfaceOpen,不看activeRectificationSession、rectificationLoading、rectificationError。frontend/src/hooks/use-consultation-run.tssend():只对consultEntrypoint === "birth_time_rectification"(题目文案像校正交接)转去openRectificationFromHomepage;对currentSession.sessionType没有任何判断,直接把currentSession.id发到/api/consult。- 让「校正会话激活但校正面未开」成立的三条路径:
page.tsx的自动打开 effect(注释「A rectification session named in?c=…」):刷新、深链、从/chart等次级页侧栏Link回/?c=<校正会话>都走它。揭幕由bootstrapRevealDelayMs最多等BOOTSTRAP_PREPARE_TIMEOUT_MS(4 s);打开校正面要open+hydrateRectificationCase两次往返(本次实测各 0.9 s / 1.3 s),网络稍慢就先揭幕成普通对话,输入框可用。- 同一个 effect 在
rectificationError非空时直接return,不再重试;openRectificationCase的catch只assignOpenError,不重置activeSessionId。打开失败一次,普通输入框就永久留在校正会话上。 use-session-management.tsdeleteSession的fallbackId = nextSessions[0]?.id、applySessionPopStateRef的fallbackId = listed[0]?.id,以及page.tsx的activeSession = sessions.find(...) ?? sessions[0]:三处都可能直接落到列表第一条;而openRectificationCase用[merged, ...current]把刚打开的校正会话插在第一位。回退时不经过selectSession的「校正会话先打开再切换」逻辑(BUG-505 的修复只覆盖了selectSession)。
Bug 历史检索:BUG-505(进校正面先闪普通对话)修的是 selectSession 一条路径,防复发措施仍在;本单是同类漏洞的另外三条路径,不是复发,但关联 BUG-505。BUG-621(历史校正打不开)已排除:open 实测 200。
2. 根因
普通咨询的发送链路以「校正面没开」推断「当前是普通会话」,而「校正面没开」还有三种别的含义(正在打开、打开失败、回退落上去的)。会话类型这个事实在前端从未作为发送前置条件检查,服务端又只用一句「咨询会话不存在」兜底,用户看到的提示与实际状态无关。
3. 决策记录
- 产品负责人 2026-09-17 授权出本单(「可以」)。
- 口径:校正会话上永远不出现普通输入框的可用态。用户在校正会话上打的字,只能进校正面的输入框;校正面还没开时,普通输入框只能是「正在打开生时校正」的禁用态,打开失败时给一个「重新打开」动作。
- 不推翻 BUG-505 的一次揭幕合同(
hydrateRectificationCase4 s 上限、面板一个会话只挂一次);不推翻 AGENTS.md §6「揭幕后不得出现 spinner / 骨架 / 正在加载」——禁用态占位文案不算加载动画,但不得加 spinner。 - 不改校正面组件、不改校正 Agent、不改 Skill。
page.tsx不得增长;新逻辑进 hook 或 lib。
4. 硬红线
send()对sessionType === "birth_time_rectification"的会话不得发出/api/consult,无论入口(表单提交、onFollowUp、排队草稿、刷新恢复重放)。- 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
- 不顺手改会话列表排序、标题、缓存(那是另一单)。
- Bug 历史与进度记录不得写入会话 ID、案例 ID、用户 ID、出生资料、Cookie。
5. 任务分解
T1 send() 加会话类型守卫(BUG-924)
use-consultation-run.tssend():在if (!question || !currentSession …) return false之后、扣点与撤回窗口之前,若currentSession.sessionType === "birth_time_rectification":不发/api/consult,把用户输入保留在草稿(setDraft(originalQuestion),不清空),调用openRectificationSession(currentSession.id),return false。- 刷新恢复重放(
restoreConsultationRecovery→send(..., { resumeRequestId }))同样受守卫约束;pending 存储里若指向校正会话,直接清掉并不重放。 - 验收:
- 新测试(
frontend/tests/下按既有 hook 合同测试的写法):给定 active 会话为校正类型、校正面未开,提交一条普通问题 →fetch未被调用到/api/consult,openRectificationSession被调用且参数为该会话 id,草稿仍是原文。 - 既有
rectification-surface-contract、chat-session-authority全绿。
- 新测试(
T2 普通输入框在校正会话上只有禁用态(BUG-924)
page.tsx的ChatComposer:inputDisabled与submitBlocked增加activeRectificationSession条件;placeholder在该状态下为「正在打开生时校正…」;rectificationError非空且属于当前会话时,ChatComposer上方(沿用现有error-message位置)显示错误文案与一个「重新打开生时校正」按钮,点击调用openRectificationSession(activeSession.id)。这些派生值请放进现有 hook(use-rectification-surface.ts或use-session-management.ts)返回,page.tsx只消费。- 自动打开 effect:
rectificationError非空时仍不自动重试(避免循环),但重新打开按钮点击后先清rectificationError。 - 验收:
home-bootstrap-reveal或rectification-surface-contract加断言:prepare 超时揭幕、校正面未开时,composerdisabled为真、placeholder 为「正在打开生时校正…」;rectificationError状态下出现「重新打开生时校正」。frontend/DESIGN.md侧栏/输入框合同补一句:校正会话上普通输入框只有禁用态。
T3 三条回退路径统一走 selectSession(BUG-924)
deleteSession、applySessionPopStateRef(两处fallbackId)、以及activeSession ?? sessions[0]的兜底:回退目标若是校正会话,必须经selectSession的延迟切换(先openRectificationSession再setActiveSessionId);不得直接setActiveSessionId到校正会话。sessions[0]的兜底改为「第一条非校正会话,没有则空」。- 验收:新测试覆盖「删除当前会话、列表第一条是校正会话」与「popstate 回到无参 URL、列表第一条是校正会话」两个场景:
activeSessionId不直接变成校正会话 id,openRectificationSession被调用。
T4 服务端错误码与前端映射(BUG-925)
api/consult/route.ts:session_type !== "consultation"时返回409 { error: "这是生时校正会话", code: "session_not_consultation", message: "请在生时校正里继续。" };真正查不到仍是 404「咨询会话不存在」。/api/consult/cancel、status 若有同样判断一并对齐。use-consultation-run.ts:收到session_not_consultation时不显示错误,不扣点(本来就在预留前),走 T1 同一条打开链路。- 验收:
chat-session-authority加一条:校正会话 POST/api/consult→ 409 +session_not_consultation;普通不存在 id → 404。
T5 记录
docs/BUG_HISTORY.md新增 BUG-924、BUG-925,关联 BUG-505;CHANGELOG.md一句「校正会话上不再出现可用的普通输入框」;docs/tasks/PROGRESS-rectification-session-composer-guard-20260917.md;docs/tasks/README.md状态板行改状态。docs/testing/加真机条目:① 在校正会话里刷新,揭幕后立刻打字 → 输入框禁用、几秒后校正面出现、草稿不丢;② 断网状态下点侧栏校正会话 → 错误文案 + 重新打开按钮,联网后点按钮可开;③ 删除当前普通会话且列表第一条是校正会话 → 不出现普通输入框。
6. 让步顺序
T1 > T4 > T2 > T3。T1 一项就能挡住误发;T3 若牵动 use-session-management.ts 过大,可只做 deleteSession 与 sessions[0] 兜底,popstate 写进 BLOCKED.md。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-session-composer-guard-20260917 .worktrees/rectification-session-composer-guard-20260917 origin/staging
cd .worktrees/rectification-session-composer-guard-20260917/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 # 记下基线失败清单
grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1 # 核对 BUG 起点
8. 验收口径
tsc --noEmit 0 错;npm run lint 0 error;npm test 失败清单与基线逐条一致、新增测试全绿;next build 后 / 仍 Static;首屏 gzip ±2%;page.tsx 行数 ≤ 开工时。无 Docker 的 DB 套件失败照基线记录。