Files
Jyotisha/docs/tasks/TASK-rectification-session-composer-guard-20260917.md
T

12 KiB
Raw Blame History

任务书 · 校正会话上露出普通输入框,发问打到 /api/consult 报「咨询会话不存在」(2026-09-17)

0. 基线

  • 基线 commiteea90926origin/staging headstaging 已部署 dc2f2a16,两者之间只有文档)。
  • 分支:codex/rectification-session-composer-guard-20260917git worktree add -b codex/rectification-session-composer-guard-20260917 .worktrees/rectification-session-composer-guard-20260917 origin/staging
  • 范围:前端 use-consultation-run.tsuse-session-management.tsuse-rectification-surface.tspage.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:1xAsia/Shanghai)在 staging 桌面 Chrome 上,从一条生时校正会话里输入了一个感情问题,浏览器发出 POST /api/consult,服务端返回:

{"error":"咨询会话不存在","message":"请重新进入咨询。"}

用产品负责人的登录态只读核对 staging(不含任何身份信息):

项目 结果
请求体 sessionId 指向一条会话,history: []entryMode: direct_chartconsultationMode: verified_chart
GET /api/sessions/<id> 200session_type = birth_time_rectificationrectification_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/openintent=session 200disposition = resumedSkill 10.0.290.9 s

结论:校正会话与案例都健康,服务端能正常打开;是前端在这条会话激活时渲染了普通 ChatComposer,把问题发到了普通咨询接口。

代码定位(按符号):

  • frontend/src/app/api/consult/route.ts POST:读 chat_sessionsif (!chatSession || chatSession.session_type !== "consultation") 统一回 404「咨询会话不存在」。判断正确,但没有独立错误码,前端无法区分「不存在」与「类型不对」。
  • frontend/src/app/page.tsxactiveRectificationSession = activeSession?.sessionType === "birth_time_rectification"rectificationSurfaceOpen = activeRectificationSession && activeSession.id === rectificationSessionId。校正面只在 rectificationSurfaceOpen && rectificationCaseId 时挂载;否则渲染普通 ChatComposer,其 inputDisabled / submitBlocked 只看 rectificationSurfaceOpen不看 activeRectificationSessionrectificationLoadingrectificationError
  • frontend/src/hooks/use-consultation-run.ts send():只对 consultEntrypoint === "birth_time_rectification"(题目文案像校正交接)转去 openRectificationFromHomepage;对 currentSession.sessionType 没有任何判断,直接把 currentSession.id 发到 /api/consult
  • 让「校正会话激活但校正面未开」成立的三条路径:
    1. page.tsx 的自动打开 effect(注释「A rectification session named in ?c=…」):刷新、深链、从 /chart 等次级页侧栏 Link/?c=<校正会话> 都走它。揭幕由 bootstrapRevealDelayMs 最多等 BOOTSTRAP_PREPARE_TIMEOUT_MS4 s);打开校正面要 open + hydrateRectificationCase 两次往返(本次实测各 0.9 s / 1.3 s),网络稍慢就先揭幕成普通对话,输入框可用。
    2. 同一个 effect 在 rectificationError 非空时直接 return,不再重试;openRectificationCasecatchassignOpenError,不重置 activeSessionId。打开失败一次,普通输入框就永久留在校正会话上。
    3. use-session-management.ts deleteSessionfallbackId = nextSessions[0]?.idapplySessionPopStateReffallbackId = listed[0]?.id,以及 page.tsxactiveSession = 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 的一次揭幕合同(hydrateRectificationCase 4 s 上限、面板一个会话只挂一次);不推翻 AGENTS.md §6「揭幕后不得出现 spinner / 骨架 / 正在加载」——禁用态占位文案不算加载动画,但不得加 spinner。
  • 不改校正面组件、不改校正 Agent、不改 Skill。
  • page.tsx 不得增长;新逻辑进 hook 或 lib。

4. 硬红线

  1. send()sessionType === "birth_time_rectification" 的会话不得发出 /api/consult,无论入口(表单提交、onFollowUp、排队草稿、刷新恢复重放)。
  2. 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
  3. 不顺手改会话列表排序、标题、缓存(那是另一单)。
  4. Bug 历史与进度记录不得写入会话 ID、案例 ID、用户 ID、出生资料、Cookie。

5. 任务分解

T1 send() 加会话类型守卫(BUG-924

  • use-consultation-run.ts send():在 if (!question || !currentSession …) return false 之后、扣点与撤回窗口之前,若 currentSession.sessionType === "birth_time_rectification":不发 /api/consult,把用户输入保留在草稿(setDraft(originalQuestion),不清空),调用 openRectificationSession(currentSession.id)return false
  • 刷新恢复重放(restoreConsultationRecoverysend(..., { resumeRequestId }))同样受守卫约束;pending 存储里若指向校正会话,直接清掉并不重放。
  • 验收:
    • 新测试(frontend/tests/ 下按既有 hook 合同测试的写法):给定 active 会话为校正类型、校正面未开,提交一条普通问题 → fetch 未被调用到 /api/consultopenRectificationSession 被调用且参数为该会话 id,草稿仍是原文。
    • 既有 rectification-surface-contractchat-session-authority 全绿。

T2 普通输入框在校正会话上只有禁用态(BUG-924)

  • page.tsxChatComposerinputDisabledsubmitBlocked 增加 activeRectificationSession 条件;placeholder 在该状态下为「正在打开生时校正…」;rectificationError 非空且属于当前会话时,ChatComposer 上方(沿用现有 error-message 位置)显示错误文案与一个「重新打开生时校正」按钮,点击调用 openRectificationSession(activeSession.id)。这些派生值请放进现有 hookuse-rectification-surface.tsuse-session-management.ts)返回,page.tsx 只消费。
  • 自动打开 effectrectificationError 非空时仍不自动重试(避免循环),但重新打开按钮点击后先清 rectificationError
  • 验收:
    • home-bootstrap-revealrectification-surface-contract 加断言:prepare 超时揭幕、校正面未开时,composer disabled 为真、placeholder 为「正在打开生时校正…」;rectificationError 状态下出现「重新打开生时校正」。
    • frontend/DESIGN.md 侧栏/输入框合同补一句:校正会话上普通输入框只有禁用态。

T3 三条回退路径统一走 selectSessionBUG-924

  • deleteSessionapplySessionPopStateRef(两处 fallbackId)、以及 activeSession ?? sessions[0] 的兜底:回退目标若是校正会话,必须经 selectSession 的延迟切换(先 openRectificationSessionsetActiveSessionId);不得直接 setActiveSessionId 到校正会话。sessions[0] 的兜底改为「第一条非校正会话,没有则空」。
  • 验收:新测试覆盖「删除当前会话、列表第一条是校正会话」与「popstate 回到无参 URL、列表第一条是校正会话」两个场景:activeSessionId 不直接变成校正会话 idopenRectificationSession 被调用。

T4 服务端错误码与前端映射(BUG-925)

  • api/consult/route.tssession_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-505CHANGELOG.md 一句「校正会话上不再出现可用的普通输入框」;docs/tasks/PROGRESS-rectification-session-composer-guard-20260917.mddocs/tasks/README.md 状态板行改状态。
  • docs/testing/ 加真机条目:① 在校正会话里刷新,揭幕后立刻打字 → 输入框禁用、几秒后校正面出现、草稿不丢;② 断网状态下点侧栏校正会话 → 错误文案 + 重新打开按钮,联网后点按钮可开;③ 删除当前普通会话且列表第一条是校正会话 → 不出现普通输入框。

6. 让步顺序

T1 > T4 > T2 > T3。T1 一项就能挡住误发;T3 若牵动 use-session-management.ts 过大,可只做 deleteSessionsessions[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 errornpm test 失败清单与基线逐条一致、新增测试全绿;next build/ 仍 Static;首屏 gzip ±2%page.tsx 行数 ≤ 开工时。无 Docker 的 DB 套件失败照基线记录。