docs: 首页停在「正在载入账户」的诊断(BUG-936)与死屏兜底任务书
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Opus 5
parent
db5b0a214b
commit
dc1a24ca48
@@ -12229,3 +12229,27 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
## BUG-936 | 首页永远停在「正在载入账户」,没有超时也没有报错
|
||||
|
||||
- 状态:investigating
|
||||
- 首次发现:2026-09-17
|
||||
- 最近更新:2026-09-17
|
||||
- 影响面:`/` 首屏揭幕(`app/page.tsx` 的 `!hydrated || (!account && !accountError)` 门)、`AppLoadingIndicator`、`app/layout.tsx`
|
||||
- 用户现象:产品负责人转述,一位已有账号的用户在 iPhone Safari 打开 `https://staging.jyotisha.chat/`,永远停在「正在载入账户 / 同步个人资料与对话记录」,进不去页面。转圈动画还在动。
|
||||
- 触发条件:尚未定位到具体条件。已排除接口侧。
|
||||
- 已确认事实(2026-09-17,线上 `dc2f2a16`):
|
||||
1. 该用户登录态下 `/api/account`、`/api/sessions?limit=40`、`/api/models`、`/api/rectification/cases/entry-summary` 全部 200,耗时 0.6–1.1 s,不是接口挂起。
|
||||
2. `/` 的预渲染 HTML **本身就含**「正在载入账户」「同步个人资料与对话记录」与 `app-loading` 结构;转圈是 `.app-loading-orbit::after` 的纯 CSS 动画。**因此这一屏在客户端 JS 一行都没执行时也会原样显示并继续转。**
|
||||
3. 兜底逻辑全部活在那段 JS 里:8 秒 `bootstrapTimeout`(写 `accountError` 并 `setHydrated(true)`)、prepare 阶段 4 秒揭幕、401 跳 `/login`。JS 没跑 → 这三条都不会发生 → 永远停住、没有任何报错。这与现象完全吻合。
|
||||
4. 首页 HTML 引用 25 个 `/_next/static/chunks/*.js`,全部同步 `<script>`,任何一个没加载或解析失败,React 就不会 hydrate。实测这 25 个当前都 200。
|
||||
5. 线上 bundle 的语法下限是 **Safari 16.4**:`1stw4tc266s7a.js`(Next 自己的 app-router 运行时)含类静态块 `class y extends Component{static{this.contextType=...}}`;`089-80cjt8-1t.js`(本仓中文断句 `split(/(?<=[。!?;;])\s*/)`)与 `0dmsli_y64717.js`(链接识别依赖)含正则后行断言。两者在 Safari < 16.4 都是**解析期** SyntaxError,core-js 这类运行时 polyfill 救不了。
|
||||
6. 该下限不是新引入:`next: 16.3.1` 从仓库首个提交 `4aa0105f`(2026-07-15)就在。所以如果这台设备以前能用,(5) 不是本次原因。
|
||||
- 待定位(需要用户侧一条信息即可分流):① 设备 iOS 版本 < 16.4 → 命中 (5);② 某个 chunk 在弱网下没下全或被内容拦截器挡掉 → 无痕窗口 / 清除网站数据后恢复;③ Safari 缓存里留着坏掉的 chunk(chunk 带 `cache-control: public, max-age=31536000, immutable`)。
|
||||
- 根因:未确认,不得提前写。
|
||||
- 修复:未做。
|
||||
- 验证:未做。
|
||||
- 防复发(已可确定的一条,独立于根因):首屏不得把「出错了」的唯一出口放在可能失败的模块 bundle 里。根 layout 需要一段与 bundle 无关的内联经典脚本,在 hydrate 超时或语法不支持时把这一屏换成可读的提示与「重新加载」。见 `docs/tasks/TASK-first-paint-dead-screen-fallback-20260917.md`。
|
||||
- 相关记录:BUG-716(`/chart` 白屏 45 秒,另一条链路)
|
||||
- 复发自:无
|
||||
- 修复版本:未发布
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` |
|
||||
| `TASK-session-list-single-source-20260917.md` | `PROGRESS-session-list-single-source-20260917.md` | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store);空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 验收未通过 | `e4e73f56`(实现按任务书落地:多键排序 / `(app)` 路由组常驻外壳 / provider / 空会话不入列 + draft 复用 / 标题类别在前;两条让步任务书允许。**P1:4 条针对 `page.tsx` 的源码合同没跟着外壳搬家,`npm test` 在门禁里 `set -e` 下必红,staging 至今停在 `dc2f2a16`**。另:进度记录报「tsc 0 错」实为 2 错,只跑了定向测试。修复单 `TASK-session-list-single-source-fix-20260917.md`) |
|
||||
| `TASK-session-list-single-source-fix-20260917.md` | `PROGRESS-session-list-single-source-fix-20260917.md` | 验收修复单:F1 四条源码合同搬到 `(app)/layout.tsx` 两端对断(BUG-933);F2 两条陈旧 Python 入口断言(BUG-934,既有欠账);F3 无活跃会话时输入框静默吞发送(BUG-935);交付前必须跑全量测试 | 待验收 | `codex/session-list-single-source-fix-20260917` |
|
||||
| `TASK-first-paint-dead-screen-fallback-20260917.md` | — | 真机:首页永远停在「正在载入账户」,兜底全在没跑起来的 bundle 里(BUG-936 investigating)。根 layout 加与 bundle 无关的内联兜底 + 去掉本仓正则后行断言 | 待领取 | — |
|
||||
| `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` |
|
||||
| `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 |
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 任务书 · 首屏死屏兜底:客户端 bundle 没跑起来时,「正在载入账户」永远转下去(2026-09-17)
|
||||
|
||||
## 0. 基线
|
||||
|
||||
- 基线 commit:`db5b0a21`(`origin/staging` head)。线上 staging 仍是 `dc2f2a16`,今天六个代码提交都没部署。
|
||||
- 分支:`codex/first-paint-dead-screen-fallback-20260917`,`git worktree add -b codex/first-paint-dead-screen-fallback-20260917 .worktrees/first-paint-dead-screen-fallback-20260917 origin/staging`。
|
||||
- 范围:`frontend/src/app/layout.tsx`(内联兜底脚本)、`frontend/src/app/globals.css`(兜底文案样式)、`frontend/src/lib/` 八处正则后行断言、对应测试。不动 Python、不动迁移、不动 Skill、不动业务逻辑。
|
||||
- BUG 段:**BUG-937 起**(BUG-936 是本单的诊断记录,已 `investigating`;开工时核对最大号)。
|
||||
|
||||
## 1. 事故实证
|
||||
|
||||
产品负责人 2026-09-17 转述:一位已有账号的用户在 iPhone Safari 打开 `https://staging.jyotisha.chat/`,永远停在「正在载入账户 / 同步个人资料与对话记录」,进不去页面,转圈还在动。
|
||||
|
||||
已确认(线上 `dc2f2a16`,用该用户登录态只读核对,未写入任何数据):
|
||||
|
||||
| 事实 | 证据 |
|
||||
| --- | --- |
|
||||
| 不是接口问题 | `/api/account` 0.72 s、`/api/sessions?limit=40` 1.07 s、`/api/models` 0.61 s、`/api/rectification/cases/entry-summary` 0.84 s,全部 200 |
|
||||
| 这一屏是预渲染 HTML | `curl https://staging.jyotisha.chat/` 的 HTML 里直接含「正在载入账户」「同步个人资料与对话记录」与 5 处 `app-loading` |
|
||||
| 转圈与 JS 无关 | `.app-loading-orbit::after` 是纯 CSS 动画,`AppLoadingIndicator` 不含任何脚本行为 |
|
||||
| 兜底全在 JS 里 | `page.tsx` 的 8 秒 `bootstrapTimeout`(写 `accountError` + `setHydrated(true)`)、prepare 阶段 4 秒揭幕、401 跳 `/login`,全部写在客户端 bundle 中 |
|
||||
| 首屏要同步跑 25 个 chunk | 首页 HTML 引用 25 个 `/_next/static/chunks/*.js`,都是同步 `<script>`;任何一个没加载或解析失败,React 就不 hydrate |
|
||||
| bundle 语法下限是 Safari 16.4 | `1stw4tc266s7a.js`(Next 自己的 app-router 运行时)含 `class y extends Component{static{this.contextType=…}}`;`089-80cjt8-1t.js`(本仓中文断句)与 `0dmsli_y64717.js`(链接识别依赖)含 `(?<=…)`。类静态块与后行断言在 Safari < 16.4 都是**解析期**错误,core-js 救不了 |
|
||||
| 该下限不是新引入 | `next: 16.3.1` 从首个提交 `4aa0105f`(2026-07-15)就在 |
|
||||
|
||||
把这些串起来:**只要那 25 个 chunk 里有一个没跑起来,用户看到的就正好是这一屏,而且永远不会变**——因为所有能救场的逻辑本身就在没跑起来的那堆 JS 里。8 秒超时救不了它自己。
|
||||
|
||||
具体是哪一种(设备 Safari < 16.4 / 弱网下某个 chunk 没下全 / 内容拦截器 / Safari 缓存里的坏 chunk,chunk 带 `cache-control: public, max-age=31536000, immutable`)需要用户侧一条信息才能分流,见 `docs/BUG_HISTORY.md` BUG-936。**本单不赌根因**,做的是与根因无关的两件事。
|
||||
|
||||
## 2. 根因(本单负责的那一层)
|
||||
|
||||
首屏把「出错了」的唯一出口放在了可能失败的模块 bundle 里。只要 bundle 挂了,用户手上就只剩一个永远转的动画:没有文案、没有重试、没有任何线索,也没法自救。这一层与具体哪个 chunk 失败无关,必须先补上。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
- 产品负责人 2026-09-17 要求查因;本单是查因结论里**不依赖用户回话就能做**的那部分,由 Claude 定范围。
|
||||
- 口径:兜底必须是**与主 bundle 无关的内联经典脚本**(不是 module、不引外部文件、不依赖 React),否则同一场故障会把兜底一起带走。
|
||||
- 兜底只负责说清楚和给出路,不试图修复:显示一句人话 + 「重新加载」按钮 + 一行「如果反复出现,请在 Safari 里清除本站数据,或升级到 iOS 16.4 以上」。**不得**自动反复刷新(会变成刷新循环)。
|
||||
- 本仓八处正则后行断言一并改掉:它们是我们自己能控的那部分语法下限。Next 运行时的类静态块改不了,所以这一项**不会**把下限降到 16.4 以下,只是去掉我们自己的那份,并让「本仓代码不写解析期新语法」成为一条可测的约束。
|
||||
- 不做:为老 Safari 降级整个构建(Next 16 的基线摆在那儿,做不到且代价失控)。不做:把 25 个 chunk 合并优化(另一个题目)。
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. 兜底脚本必须是 `<script>`(非 `type="module"`)、ES5 语法、内联在根 layout 的 `<head>` 或 `<body>` 顶部;不得 import、不得用箭头函数 / `const` / 模板串以外的现代语法(它要在挂掉的那些引擎上也能跑)。
|
||||
2. 正常情况下兜底**不得**闪现:只有在超时且页面仍是未 hydrate 状态时才显示。
|
||||
3. 不得自动重载页面。
|
||||
4. 不得改 `page.tsx` 的揭幕门逻辑、不得动 8 秒 `bootstrapTimeout`。
|
||||
5. `/` 必须仍是 `○ Static`;首屏 gzip ±2%。
|
||||
6. 既有断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
### T1 根 layout 内联死屏兜底(BUG-937)
|
||||
|
||||
- `frontend/src/app/layout.tsx`:在现有 `themePreferenceBootScript` 旁边加第二段内联经典脚本,逻辑:
|
||||
1. 记录启动时刻,`setTimeout` 12000ms(大于 JS 侧 8 秒超时 + 4 秒揭幕,正常路径永远不会命中)。
|
||||
2. 触发时检查页面是否仍停在首屏加载态(推荐用 `document.querySelector(".app-loading")` 仍在,且 `document.documentElement.dataset.hydrated !== "1"`)。
|
||||
3. 仍在 → 把 `.app-loading-content` 的内容替换成兜底文案 + 「重新加载」按钮(`location.reload()`)。文案对照 `frontend/docs/VOICE.md` 后定稿,建议:标题「这个页面没能加载完」,正文「网络中断或浏览器版本过旧都会这样。可以重新加载试一次;如果反复出现,请在 Safari 设置里清除本站数据,或把系统升级到 iOS 16.4 以上。」
|
||||
4. 同时在 `window.onerror` 里记一个标记,兜底文案据此追加一行「(页面脚本未能执行)」,方便真机回报时区分「加载慢」和「脚本挂了」。
|
||||
- hydrate 成功的标记:`page.tsx` 揭幕后(`hydrated` 为真)写 `document.documentElement.dataset.hydrated = "1"`。这是一行 effect,放进现有 hook 或 layout 的客户端组件,`page.tsx` 不增行。
|
||||
- 验收:
|
||||
- 新测试 `frontend/tests/first-paint-fallback-contract.test.ts`:layout 源码里这段脚本不含 `=>`、`const `、`let `、`class `、`${`、`?.`、`??`(ES5 约束,源码级正则断言);不含 `type="module"`;超时值 > `page.tsx` 的 8000 + 4000。
|
||||
- jsdom 行为测试:`.app-loading` 存在且无 `data-hydrated` 时,触发定时器后文案被替换、按钮可点;`data-hydrated="1"` 时不替换。
|
||||
- 手工:`next build` 后把某个 chunk 的 `<script src>` 改错(或断网重放)验证 12 秒后出兜底;写进 `docs/testing/`。
|
||||
|
||||
### T2 去掉本仓八处正则后行断言(BUG-938)
|
||||
|
||||
现存位置(按符号定位,行号会漂):
|
||||
|
||||
| 文件 | 符号 |
|
||||
| --- | --- |
|
||||
| `lib/personal-report-generation.ts` | 段落切分 `split(/(?<=[。!?!?;;])\s*|\n+/u)` |
|
||||
| `lib/rectification-agentic/v9/adopt-narration.ts` | 句子切分 |
|
||||
| `lib/rectification-agentic/user-copy.ts` | `OPENING_SENTENCE` 与另外两处 |
|
||||
| `lib/rectification-agentic/v9/turn-narration.ts` | 两处 |
|
||||
| `lib/rectification-agentic/v9/collect-prompt.ts` | `SENTENCE_SPLIT` |
|
||||
|
||||
- 统一换成一个共用工具 `lib/sentence-split.ts`:用 `match`/手写扫描实现「在句末标点后切开、保留标点」,不使用后行断言。八处全部改为调用它。
|
||||
- **行为必须逐字一致**:给工具补一组表驱动测试,用现有八处的输入输出习惯各取至少两例(中文句号、问号叹号、分号、英文标点、换行、连续标点、结尾无标点)。
|
||||
- 验收:
|
||||
- `npm test` 相关套件全绿,`rectification` 与报告相关的既有断言一条不改。
|
||||
- 源码合同:`git grep -n "(?<=" frontend/src` 无结果;测试里加一条正则断言把这条钉死(允许 `frontend/tests` 自己出现该字符串)。
|
||||
|
||||
### T3 记录
|
||||
|
||||
- `docs/BUG_HISTORY.md`:BUG-937、BUG-938;把 BUG-936 从 `investigating` 更新为「兜底已补、根因待用户侧信息」,**不得**在没有用户回话的情况下把 936 标 `resolved`。
|
||||
- `CHANGELOG.md` 一句;`frontend/DESIGN.md` 加「首屏死屏兜底」一节(出现条件、文案、唯一动作);`frontend/docs/VOICE.md` 加兜底文案;`docs/tasks/PROGRESS-first-paint-dead-screen-fallback-20260917.md`;状态板行。
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
T1 > T2。T1 一项独立可发,是用户当下最需要的。T2 如果表驱动测试铺不完,可先只改 `user-copy.ts` 三处(用户可见文案链路),其余写进 `BLOCKED.md`。
|
||||
|
||||
## 7. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/first-paint-dead-screen-fallback-20260917 .worktrees/first-paint-dead-screen-fallback-20260917 origin/staging
|
||||
cd .worktrees/first-paint-dead-screen-fallback-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
|
||||
```
|
||||
|
||||
## 8. 验收口径
|
||||
|
||||
`tsc --noEmit` 0 错;`npm run lint` 0 error;**全量** `npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build --webpack` 通过且 `/` 仍 `○ Static`、首屏 gzip ±2%;`git grep "(?<=" frontend/src` 无结果。交付前必须跑全量,不得只跑定向(BUG-933 的教训)。
|
||||
Reference in New Issue
Block a user