Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
113 lines
12 KiB
Markdown
113 lines
12 KiB
Markdown
# 任务书 · 校正会话上露出普通输入框,发问打到 `/api/consult` 报「咨询会话不存在」(2026-09-17)
|
||
|
||
## 0. 基线
|
||
|
||
- 基线 commit:`eea90926`(`origin/staging` head;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`,服务端返回:
|
||
|
||
```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.29,0.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 套件失败照基线记录。
|