Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
96 lines
9.6 KiB
Markdown
96 lines
9.6 KiB
Markdown
# TASK · 账户弹窗被 `inert` 罩住点不动 + 校正快照失败两端静默(2026-09-18 第八轮)
|
||
|
||
> 基线:`origin/staging` @ `1061514f`(代码部分 = 已部署的 `877128ce`)。
|
||
> 触发:产品真机反馈「弹窗都没法点击,我都没法点击退出登录」;同一会话里生时校正在「已记录。这题先不计分,换一件事问。」之后再无下文,右侧盘面与时间轴候选也没有。刷新后弹窗仍点不动。
|
||
> BUG 编号起点:当前最大号 **BUG-965**;`TASK-secondary-page-entry-20260918.md` 已占 966~967,本单占 **BUG-968 ~ BUG-969**。
|
||
|
||
## 0. 事故实证
|
||
|
||
### 0.1 弹窗点不动(BUG-968,P0,代码级确认)
|
||
|
||
| 位置 | 事实 |
|
||
| --- | --- |
|
||
| `src/app/(app)/layout.tsx` `AppShell` | `<SidebarInset inert={registration?.insetInert}>{children}</SidebarInset>`,整个页面正文(`children`)都在这个 section 里 |
|
||
| `src/hooks/use-home-shell-registration.ts` `registerShellControls` | `insetInert: modalOpen` |
|
||
| `src/app/(app)/page.tsx` `modalOpen` | `activeAccountDialog !== null \|\| onboardingPaywallOpen` |
|
||
| `src/app/(app)/page.tsx` 渲染 `<AccountDialogOverlay>` | 在页面组件 JSX 里,**也就是在 `SidebarInset` 之内** |
|
||
| `src/components/account-dialog-overlay.tsx` | 纯 `<div className="account-modal-overlay">` + `<section role="dialog">`,没有 `createPortal` |
|
||
| `src/components/ui/sidebar.tsx` `SidebarInset` | `inert={Boolean(inert) \|\| (isMobile && openMobile)}` |
|
||
|
||
链条:点侧栏「个人资料 / 图库 / 设置 / 点数 / 退出登录」→ `activeAccountDialog` 非空 → `modalOpen=true` → `SidebarInset` 整块 `inert` → **弹窗自己也在 `inert` 子树里** → 关闭按钮、表单、「确认退出」全部不可点、不可聚焦。正文区同样全部死掉。`window` 级的 Esc 监听(`page.tsx` `closeOnEscape`)还能关掉弹窗,所以用户按 Esc 能脱困,但退出登录做不了。
|
||
|
||
回归点:`e4e73f56`(2026-09-17,「四个页面共用一份会话列表」)把 `SidebarInset` 从 `page.tsx` 搬进 `(app)/layout.tsx`。搬之前 `page.tsx` 是 `<SidebarInset inert={modalOpen}>…</SidebarInset>` **之后**才渲染 `<AccountDialogOverlay>`,弹窗在 inert 之外;搬之后弹窗变成 `children` 的一部分,落进了 inert 里。`877128ce` 含这个提交,staging 上 100% 复现,与标签页新旧无关(BUG-965 那条是另一个问题)。
|
||
|
||
### 0.2 校正快照拿不到,用户被留在开场状态(BUG-969,P1,investigating)
|
||
|
||
真实 Case(staging,2026-09-18 17:41–17:49)服务端链路逐段核过:
|
||
|
||
1. 开场采集题 → 两轮口述经历(各 3 步,末步 `rectification-set-focus`;第一轮已把采集焦点 `resolved`,并挂出服务端财务探针 2024-08,绑在该轮回复上)。
|
||
2. 「想不到了」走 `route.ts` 的 focus+choice 分支:分类器判 `answer_current_focus / unsure` → 映射 D → `applyRectificationChoice` 确定性回复「已记录。这题先不计分,换一件事问。」→ `persistNextInterviewAfterChoice` 同一瞬间落库财务探针 2025-09(active,`asked_turn_id` 绑该回复轮)。模型没跑,日志零告警。
|
||
3. 用 psql 取 `get_agentic_rectification_case_dossier` / `list_agentic_rectification_conversation_focuses` / `get_agentic_rectification_turn_receipt` 真实行,在本地用同版本代码跑 `dossierResponse`:`current_question`、`choice_card`(A–D 四项)、`turns[].question`(7 条轮次)、`latest_result`(17 个候选)、`question_source=focus` 全部完整;三个 RPC 合计 < 70ms,投影 ~45ms。
|
||
4. 客户端解析函数(`isRectificationCaseSnapshotPayload` / `parsePersistedRectificationTurns` / `parseRectificationChoiceCard` / `parseRectificationCandidateResult` / `currentQuestionFromSnapshot`)对这份载荷全部通过。
|
||
5. 产品截图(17:49,未刷新):无卡;右侧「补充经历后,这里会显示…」占位;时间轴只有 14:00–15:00 无候选点;输入框占位是 `collect_waiting` 才会出现的「再说一件带年月的事」。即客户端仍停在**开场时**的快照(`collect_evidence`、无结果、无当前题),17:46 之后三次 `loadCaseSnapshot` 都没有生效。
|
||
|
||
查不下去的原因(这就是本单要修的):
|
||
|
||
- `GET /api/rectification/cases/[caseId]` 任何异常都返回 503,**不写日志**;`case_session_mismatch` 409 也不写。
|
||
- 客户端 `loadCaseSnapshot` 与 `hydrateRectificationCase` 把非 2xx / 抛错一律吞掉,页面不显示任何提示,停在旧状态。
|
||
- Caddy 与 Next 都不记这条 GET 的访问日志;PG `track_functions=none`、`log_statement=none`。事后无法证明请求到没到服务端。
|
||
- 确定性回复只流 ack,题干靠 `asked_turn_id` 经 GET 挂上(BUG-525 设计)。快照一失败,用户看到的就是「换一件事问」后空白。
|
||
|
||
补充观察(产品口径,本单不做):「想不到了」在语义上是在答「想到几件说几件」(采集题已被服务端悄悄 resolved),却被绑到用户没在看的财务探针上判成 D,然后又出一道同家族财务题。是否允许"刚记完经历的下一句"优先按采集语义解释,需要产品拍板后另立单。
|
||
|
||
## 1. 决策记录
|
||
|
||
- BUG-968 是 `e4e73f56` 引入的回归,不推翻任何既有红线;修法是**让弹窗回到 inert 子树之外**,不是去掉 inert(inert 是 BUG-744~746 侧栏统一时定下的焦点合同,保留)。
|
||
- BUG-969 本轮只做"可观测 + 不静默 + 题干进正文"三件事,不改分类器、不改采集/探针状态机;产品口径另立单。
|
||
- 「刷新即恢复」不再作为任何一条的关闭依据:这次刷新后弹窗依旧点不动。
|
||
|
||
## 2. 硬红线
|
||
|
||
1. 揭幕后不得出现 spinner / 骨架 / 「正在加载」;快照失败的提示用一句状态文案 + 重试入口,对照 `frontend/docs/VOICE.md`。
|
||
2. 不得为救 BUG-968 去掉 `SidebarInset` 的 `inert` 或改成 `pointer-events` 糊弄;移动端抽屉的 `isMobile && openMobile` 分支不动。
|
||
3. 不得动 `.gitea/workflows/**`、DNS、数据库结构;不顺手升级依赖。
|
||
4. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` 失败数不超过基线 31 条且清单一致;测试总数不低于开工实测。
|
||
5. 改 UI 的同一提交更新 `frontend/DESIGN.md`。
|
||
|
||
## 3. 任务分解
|
||
|
||
### 3.1 BUG-968(P0)弹窗移出 inert 子树
|
||
|
||
- 把 `AccountDialogOverlay`(及同类:onboarding 付费墙)渲染到 `SidebarInset` 之外。两种做法二选一,进度记录里写明选了哪个和原因:
|
||
- (a) `createPortal(…, document.body)`,保留现有 DOM 结构与样式;
|
||
- (b) 通过 `registerShellControls` 把弹窗节点交给 `AppShell`,由 layout 在 `SidebarInset` 的兄弟位置渲染。
|
||
- 验收:
|
||
- 合同测试:`modalOpen=true` 时,`role="dialog"` 元素不在带 `inert` 的祖先之内(用 `renderToString` 或 DOM 断言均可);`inert` 仍落在 `SidebarInset` 上。
|
||
- 现有 `tests/account-dialog-overlay.test.ts`、`tests/sidebar-contract.test.ts`、`tests/chat-navigation-a11y-contract.test.ts` 全绿;焦点圈禁(`keepFocusWithin`)与 Esc 关闭行为不变。
|
||
- 真机清单(`docs/testing/`):五个账户弹窗每个都能点关闭、能提交、「退出登录」能确认退出;弹窗开着时正文区仍不可点。
|
||
|
||
### 3.2 BUG-969(P1)快照失败不再静默
|
||
|
||
1. `GET /api/rectification/cases/[caseId]`:每一个非 200 分支 `console.warn` 一条 JSON(`event`、`case_id`、`status`、`code`、`reason=safeToolErrorCode(error)`,不含用户资料),包括 401、400、409、404、503。
|
||
2. 客户端 `loadCaseSnapshot` / `hydrateRectificationCase` / `refreshRectificationCase`:非 2xx 或抛错时把结果带回调用方(状态码或 `network`),页面在校正面显示一句状态文案 + 「重新读取」按钮,不得继续装作无事;已有的 `questionGap === "unavailable"` 分支可复用文案。
|
||
3. 确定性回复(`persistApplied` 与 `persistCollectDenialTurn` 的 `assistantMessage`)在下一题是可渲染选择题时,正文追加一句题干(沿用 `composeCollectSpokenAssistantText` 的口径),流给客户端的文本也带上;卡片仍由 GET 挂上,`stripQuestionSentences` 负责去重,不得出现题干两遍(BUG-559/609 的去重合同要过)。
|
||
4. 验收:
|
||
- 合同测试:GET 路由 503 分支产生 warn;客户端 fetch mock 返回 503 时校正面出现状态文案与按钮;确定性回复文本含题干且 `attachQuestionsToTurns` 后题干只出现一次。
|
||
- 真机清单:在校正会话里连发两轮经历后,卡片、右侧盘面、时间轴候选点必须在回复落定后出现;拔网线/断网再发一轮,页面要出现状态文案而不是空白。
|
||
- 若真机仍复现"快照拿不到",凭第 1 条的服务端日志把 BUG-969 转成 confirmed 并补根因;拿不到日志不得写 resolved。
|
||
|
||
## 4. 让步顺序
|
||
|
||
3.1 必须做完。3.2 按 1 → 2 → 3 的顺序,做不完的写进 `BLOCKED.md` 与进度记录,不得砍 1。
|
||
|
||
## 5. 开工前置
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/account-dialog-inert-20260918 \
|
||
.worktrees/account-dialog-inert-20260918 origin/staging
|
||
cd .worktrees/account-dialog-inert-20260918/frontend
|
||
npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 记下开工基线
|
||
```
|
||
|
||
与 `TASK-secondary-page-entry-20260918.md` 同时改 `(app)/layout.tsx` 时,本单先合,对方 rebase。
|
||
|
||
收工:`docs/tasks/PROGRESS-account-dialog-inert-20260918.md` + `docs/BUG_HISTORY.md`(BUG-968 转 resolved、BUG-969 按证据定状态)+ `frontend/DESIGN.md` + `docs/testing/account-dialog-inert-20260918.md`,与代码同一批推 `staging`,并把 `docs/tasks/README.md` 状态板那一行改掉。
|