Files
Jyotisha/docs/tasks/TASK-account-dialog-inert-20260918.md
T

9.6 KiB
Raw Blame History

TASK · 账户弹窗被 inert 罩住点不动 + 校正快照失败两端静默(2026-09-18 第八轮)

基线:origin/staging @ 1061514f(代码部分 = 已部署的 877128ce)。 触发:产品真机反馈「弹窗都没法点击,我都没法点击退出登录」;同一会话里生时校正在「已记录。这题先不计分,换一件事问。」之后再无下文,右侧盘面与时间轴候选也没有。刷新后弹窗仍点不动。 BUG 编号起点:当前最大号 BUG-965TASK-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=trueSidebarInset 整块 inert弹窗自己也在 inert 子树里 → 关闭按钮、表单、「确认退出」全部不可点、不可聚焦。正文区同样全部死掉。window 级的 Esc 监听(page.tsx closeOnEscape)还能关掉弹窗,所以用户按 Esc 能脱困,但退出登录做不了。

回归点:e4e73f56(2026-09-17,「四个页面共用一份会话列表」)把 SidebarInsetpage.tsx 搬进 (app)/layout.tsx。搬之前 page.tsx<SidebarInset inert={modalOpen}>…</SidebarInset> 之后才渲染 <AccountDialogOverlay>,弹窗在 inert 之外;搬之后弹窗变成 children 的一部分,落进了 inert 里。877128ce 含这个提交,staging 上 100% 复现,与标签页新旧无关(BUG-965 那条是另一个问题)。

0.2 校正快照拿不到,用户被留在开场状态(BUG-969P1investigating

真实 Casestaging2026-09-18 17:4117:49)服务端链路逐段核过:

  1. 开场采集题 → 两轮口述经历(各 3 步,末步 rectification-set-focus;第一轮已把采集焦点 resolved,并挂出服务端财务探针 2024-08,绑在该轮回复上)。
  2. 「想不到了」走 route.ts 的 focus+choice 分支:分类器判 answer_current_focus / unsure → 映射 D → applyRectificationChoice 确定性回复「已记录。这题先不计分,换一件事问。」→ persistNextInterviewAfterChoice 同一瞬间落库财务探针 2025-09activeasked_turn_id 绑该回复轮)。模型没跑,日志零告警。
  3. 用 psql 取 get_agentic_rectification_case_dossier / list_agentic_rectification_conversation_focuses / get_agentic_rectification_turn_receipt 真实行,在本地用同版本代码跑 dossierResponsecurrent_questionchoice_cardAD 四项)、turns[].question7 条轮次)、latest_result17 个候选)、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 也不写。
  • 客户端 loadCaseSnapshothydrateRectificationCase 把非 2xx / 抛错一律吞掉,页面不显示任何提示,停在旧状态。
  • Caddy 与 Next 都不记这条 GET 的访问日志;PG track_functions=nonelog_statement=none。事后无法证明请求到没到服务端。
  • 确定性回复只流 ack,题干靠 asked_turn_id 经 GET 挂上(BUG-525 设计)。快照一失败,用户看到的就是「换一件事问」后空白。

补充观察(产品口径,本单不做):「想不到了」在语义上是在答「想到几件说几件」(采集题已被服务端悄悄 resolved),却被绑到用户没在看的财务探针上判成 D,然后又出一道同家族财务题。是否允许"刚记完经历的下一句"优先按采集语义解释,需要产品拍板后另立单。

1. 决策记录

  • BUG-968 是 e4e73f56 引入的回归,不推翻任何既有红线;修法是让弹窗回到 inert 子树之外,不是去掉 inertinert 是 BUG-744~746 侧栏统一时定下的焦点合同,保留)。
  • BUG-969 本轮只做"可观测 + 不静默 + 题干进正文"三件事,不改分类器、不改采集/探针状态机;产品口径另立单。
  • 「刷新即恢复」不再作为任何一条的关闭依据:这次刷新后弹窗依旧点不动。

2. 硬红线

  1. 揭幕后不得出现 spinner / 骨架 / 「正在加载」;快照失败的提示用一句状态文案 + 重试入口,对照 frontend/docs/VOICE.md
  2. 不得为救 BUG-968 去掉 SidebarInsetinert 或改成 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-968P0)弹窗移出 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.tstests/sidebar-contract.test.tstests/chat-navigation-a11y-contract.test.ts 全绿;焦点圈禁(keepFocusWithin)与 Esc 关闭行为不变。
    • 真机清单(docs/testing/):五个账户弹窗每个都能点关闭、能提交、「退出登录」能确认退出;弹窗开着时正文区仍不可点。

3.2 BUG-969P1)快照失败不再静默

  1. GET /api/rectification/cases/[caseId]:每一个非 200 分支 console.warn 一条 JSONeventcase_idstatuscodereason=safeToolErrorCode(error),不含用户资料),包括 401、400、409、404、503。
  2. 客户端 loadCaseSnapshot / hydrateRectificationCase / refreshRectificationCase:非 2xx 或抛错时把结果带回调用方(状态码或 network),页面在校正面显示一句状态文案 + 「重新读取」按钮,不得继续装作无事;已有的 questionGap === "unavailable" 分支可复用文案。
  3. 确定性回复(persistAppliedpersistCollectDenialTurnassistantMessage)在下一题是可渲染选择题时,正文追加一句题干(沿用 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. 开工前置

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.mdBUG-968 转 resolved、BUG-969 按证据定状态)+ frontend/DESIGN.md + docs/testing/account-dialog-inert-20260918.md,与代码同一批推 staging,并把 docs/tasks/README.md 状态板那一行改掉。