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

113 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 校正会话上露出普通输入框,发问打到 `/api/consult` 报「咨询会话不存在」(2026-09-17)
## 0. 基线
- 基线 commit`eea90926``origin/staging` headstaging 已部署 `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:1xAsia/Shanghai)在 staging 桌面 Chrome 上,从一条生时校正会话里输入了一个感情问题,浏览器发出 `POST /api/consult`,服务端返回:
```json
{"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.290.9 s |
结论:校正会话与案例都健康,服务端能正常打开;是前端在这条会话激活时渲染了**普通** `ChatComposer`,把问题发到了普通咨询接口。
代码定位(按符号):
- `frontend/src/app/api/consult/route.ts` `POST`:读 `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.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_MS`4 s);打开校正面要 `open` + `hydrateRectificationCase` 两次往返(本次实测各 0.9 s / 1.3 s),网络稍慢就先揭幕成普通对话,输入框可用。
2. 同一个 effect 在 `rectificationError` 非空时直接 `return`,不再重试;`openRectificationCase``catch``assignOpenError`,不重置 `activeSessionId`。打开失败一次,普通输入框就永久留在校正会话上。
3. `use-session-management.ts` `deleteSession``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 的一次揭幕合同(`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`
- 刷新恢复重放(`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 超时揭幕、校正面未开时,composer `disabled` 为真、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. 开工前置命令
```bash
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 套件失败照基线记录。