# PROGRESS · 从其他页面回首页不再放加载动画(2026-09-26) 任务书:`docs/tasks/TASK-home-warm-return-20260926.md`。分支 `codex/home-warm-return-20260926`,worktree `.worktrees/home-warm-return-20260926`。执行方:Claude 子代理(产品授权直接执行)。未推送、未部署。BUG-1040(开工时最大 BUG-1039,1040 空)。 ## 结论 | 项 | 修复前(`origin/staging` 6a22626d) | 修复后 | | --- | --- | --- | | 首页 → 星盘 → 新建对话,所有 `/api/*` 人为延迟 1.5 s(本地 Chrome) | 出加载环,**5530 ms** 才可交互;可交互前等完 5 个请求 | **无加载环,23 ms** 可交互(复跑 18 ms);可交互前 **0** 个请求返回 | | 首页 → 我的报告 → 点历史会话(同上) | 出加载环,**5532 ms** | **无加载环,19 ms**(复跑 18 ms) | | 整页刷新 `/` | 22 ms 出加载环,约 4.1 s 进入 | 22–24 ms 出加载环,约 4.1 s 进入(不变) | | 首次打开 `/` | 64 ms 出加载环,约 4.4 s | 32–63 ms 出加载环,约 4.1–4.4 s(不变) | 冷启动约 4.1 s 是本地虚构数据下的 prepare 揭幕预算,基线与本分支一致,不属本单。 ## 基线 - `origin/staging` = `6a22626d`(BUG-1038 `0a8350cc` 已合入,开工前 `git log` 核对)。实现期间 staging 前进到 `bd669aed`(只改 BUG_HISTORY 与任务索引两行文档),本分支已 rebase 到 `bd669aed`,`docs/tasks/README.md` 冲突一处:保留 staging 的 BUG-1038 行、本单行改「待验收」。 - 全量基线(同一台机器、Node v20.19.2、Linux):协调方给的 `test9.log`,3928 条,61 失败(环境缺口:Node 20 没有 `mock.module`),27 跳过。 - 首屏 gzip(`.next/build-manifest.json` `rootMainFiles`,gzip -9):130933 B。`/` 的 index.html 引用的全部 js/css:654283 B(同代码的 `new-chat-from-people` 工作树构建)。 - `npm run lint`:0 error / 126 warning(同代码工作树实测)。 - `page.tsx` 1329 行;`Home()` useState 33、useRef 37(合同上限 36 / 37,行数上限 1567)。 ## 设计 ### 快照里放什么(T1,D2) `frontend/src/lib/home-warm-snapshot.ts`:模块变量,一次一个账户,纯内存。 | 字段 | 来源 | 说明 | | --- | --- | --- | | `accountId` | 冷启动成功后的账户 | 键;读到别的账户立即清空 | | `modelCatalog` | 冷启动 / 后台刷新 | 缺或为空 → 不算齐全 | | `entrySummary` + `entrySummarySettled` | 校正入口摘要 effect | 未 settled → 不算齐全(揭幕预算先到时,等摘要落定才写) | | `sessionsCursor` + `listBoot` | `useSessionManagement` 的分页游标,与写入时 provider 的 boot 对象身份 | provider 在外重载过(例如换人)则用 provider 自己的游标 | | 今日星语 `{accountId, subjectId, fingerprint, day, card}` | `DailyStarlanguageBinder` 拿到 ready 时 | 人物、出生资料指纹、日期三者全等才用;否则 `pending`(卡片静态句),binder 按新键重取 | **与 D2 的偏离(有意)**:`account`、`profile`、`startGreeting`、`onboardingStep` 不复制进快照。账户本来就在布局层 `SessionListProvider` 的状态里跨客户端导航存活(那就是这份内存快照),而且比快照新(provider 换人重载会重读 `/api/account`,其它页面也不会改它);资料按冷启动同样的方式 `readProfile(account.profile)` 派生,问候语与引导步骤再由资料派生。复制一份只会多一个过期来源。齐全判定照样要求账户存在、同账户、资料完整。 写入:`Home()` 里一个只写快照的 effect,条件 `hydrated && !accountError && accountId && modelCatalog && entrySummarySettled`(不写半成品,BUG-1021)。只增 effect、不增 state / ref。 失效:`SessionListProvider` 的 `/api/account` 401、`/api/sessions` 401、换账户分支;`redirectToLogin()`(所有 401 跳登录的出口);`signOut()`;`readHomeWarmSnapshot(别的账户)`。 ### 暖返回(T2,D3) `frontend/src/lib/home-warm-start.ts`,无 React hook: 1. `Home()` 的 `useState(() => …)` 初始化函数(`activeSessionId`、`modelCatalog`、`dailyStarlanguage`、`rectification`、`hydrated`、`bootstrapPhase`、`onboardingStep`、`startGreeting`,以及 `useProfileOnboarding` 新增的 `initialProfile`)都调 `takeHomeWarmStart()`。同一次渲染(含 StrictMode 双调)共用一个结果,微任务后失效,下次挂载重新判断。 2. 条件全部满足才是暖启动,否则返回 null,走**原封不动**的冷启动:provider 已 settled、未登出、有账户、boot 无错;同账户快照齐全;资料完整(引导中的用户走冷启动);非开发 `?preview=`;落点能在内存里算出;知道要打开的 `/` 的 query。 3. 落点 `resolveWarmLanding()` 用冷启动同一组函数:`resolveBootstrapSessionSelection` → `resolveStarterHomeLandingSessionId` / `starterHomeLandingNeedsConsultation`。`?new=1` 建本地空咨询(替换旧的未保存空对话,BUG-989 不落库);`?c=` 在内存 → `keep`;存根在内存且属当前人物 → `replace-selected`,属别人 → `other-subject`(BUG-1038);需要 lookup(`?c=` 或存根不在内存)→ null → 冷启动(BUG-705 的查找只有冷路径做)。 4. 首帧:`hydrated=true`、`bootstrapPhase="prepare"`、目录 / 资料 / 摘要 / 星语就位;新建的本地空咨询在进列表前由 `pendingWarmLandingSession()` 充当活动会话(`ensureSessionMessages` 同样认它,不去服务端拉)。 5. 启动 effect:挂载时 `hydrated` 为真即暖启动,调 `runHomeWarmRefresh()`(`home-bootstrap-run.ts`):同步提交落点(列表、游标、写 / 清 URL、清存根、缺失提示;存根校正会话在写回 `?c=` 后补一次自动打开,因为自动打开 effect 已在 URL 写回之前跑过),然后后台并行:`/api/models`(变了才替换,下线模型沿用原提示并 PATCH,未保存的本地空对话不 PATCH)、`refreshAccount()`、后台回答恢复(与冷启动共用抽出的 `resolveReservedConsultation`;本标签页的 pending 记录在挂载时读取,因为首页自己的 pending effect 第一轮就会清掉它;新建落点不被恢复抢走)。入口摘要与今日星语走各自原有 effect(`bootstrapPhase !== "account"` 即触发)。全部不 await 在可交互之前,失败保持快照值,不回加载态。 ### 客户端导航读不到目标 URL(浏览器实测才发现) 第一版在真实浏览器里「新建对话」落到了旧会话、地址停在 `/?new=1`。原因:Next 16 的 app router 先渲染新页面,**之后**才在 `HistoryUpdater` 的 `useInsertionEffect` 里 `pushState`(`node_modules/next/dist/client/components/app-router.js`)。所以 `Home` 挂载渲染时 `window.location` 还是 `/chart`,初始化函数读不到 `?new=1`。冷启动没这个问题,因为它在 await 之后才读 URL。 修法:新增 `frontend/src/lib/client-navigation-target.ts`(无依赖,随共享侧栏加载)。`AppLink` 在普通左键点击、未被拦截时记下目标 href,`navigateAppPath` 在客户端跳转前同样记下。首页挂载时:地址栏已是 `/`(刷新、浏览器前进后退)就读地址栏;否则读 10 秒内的那一次记录;都没有 → 冷启动(安全回落,不猜)。侧栏的新建对话、会话行、账户页脚都是 `AppLink`。 lifecycle 测试 harness 也改成 Next 的顺序(先渲染,URL 在 insertion effect 里写),原先「先 pushState 再渲染」的写法测不出这个问题。去掉记录时新测试里新建对话与历史会话两条失败。 ## 改了什么 | 文件 | 内容 | | --- | --- | | `frontend/src/lib/home-warm-snapshot.ts`(新) | 快照存取、按账户清空、齐全判定、今日星语按人物 + 指纹 + 日期存取 | | `frontend/src/lib/home-warm-start.ts`(新) | 暖启动判断、落点、挂载 query、模型对账、StrictMode 幂等提交 | | `frontend/src/lib/client-navigation-target.ts`(新) | 客户端导航目标记录 | | `frontend/src/app/(app)/page.tsx` | 8 个 useState 改初始化函数;`activeSession` 多一个首帧回退;启动 effect 分暖 / 冷;一个写快照 effect。1329 → 1373 行;useState 33 / useRef 37 不变 | | `frontend/src/lib/home-bootstrap-run.ts` | 抽出 `resolveReservedConsultation`(冷启动行为不变);新增 `runHomeWarmRefresh` | | `frontend/src/components/daily-starlanguage-binder.tsx` | 先读内存卡再读 localStorage;ready 时记进内存;原有 guard 与依赖数组不动 | | `frontend/src/hooks/use-profile-onboarding.ts` | `initialProfile` 参数;`signOut` 先清快照 | | `frontend/src/hooks/use-session-management.ts` | `ensureSessionMessages` 认首帧的活动会话 | | `frontend/src/lib/session-list-context.tsx`、`home-cloud-sync.ts` | 401 / 换账户 / `redirectToLogin` 清快照 | | `frontend/src/components/app-link.tsx`、`frontend/src/lib/app-navigation.ts` | 记录导航目标 | | `frontend/tests/home-warm-return-lifecycle.test.tsx`(新,13 条) | 见下 | | `frontend/tests/home-warm-snapshot.test.ts`(新,12 条) | 见下 | | `frontend/tests/new-chat-from-people-lifecycle.test.tsx` | 只在 `openDocument` 里加两行:文档加载时清快照(模块内存本来就随文档重置)。无断言改动 | | 文档 | `docs/BUG_HISTORY.md`(BUG-1040)、`frontend/DESIGN.md` §9 与 Secondary page shell、`CHANGELOG.md`、`BLOCKED.md`、`docs/tasks/README.md`、`docs/testing/home-warm-return-20260926.md` 与两张截图 | ## 测试 ### 新增 `home-warm-return-lifecycle.test.tsx`(真实 `Home` + `AppSidebar` + `SessionListProvider`,与 `(app)/layout.tsx` 同构;用兄弟组件的 layout effect 记录每次挂载的**首个提交帧**): | # | 用例 | 关掉暖启动时 | | --- | --- | --- | | 1–4 | `/chart` `/ephemeris` `/reports` `/people` → 新建对话:导航前把所有接口设为永不返回;首帧无加载环、标题「新对话」、首帧提交时请求数 = 导航前、starter 首页、地址 `/`、存根清掉、校正卡用快照摘要;之后 `/api/models` 与 `/api/consult/status` 在后台发出,挂起也不回加载态 | 失败 | | 5 | `/reports` → 历史会话:首帧即该会话、`?c=` 保留、消息出现 | 失败 | | 6 | 暖返回后整页刷新:首帧是加载环 | 失败(前半段暖返回断言) | | 7 | 快照被清空再回首页:首帧加载环,目录挂起时一直等(BUG-1021) | 通过(本来就冷) | | 8 | 后台目录换成别的模型:首帧不等;随后模型切到新默认、出现原提示「此前选择的模型已下线,已切换为默认模型。」、PATCH 该会话 | 失败 | | 9 | 离开时回答还在生成(pending 记录 + reserved)→ 点该会话回来:出现停止按钮、pending 记录保留 | 失败 | | 10 | 同上但点新建对话:停在新对话,旧回答继续恢复 | 失败 | | 11 | 摘要变了:校正卡从「再次校正」静默换成「开始新的生时校正」 | 失败 | | 12 | 存根校正会话经账户页脚回裸 `/`:写回 `?c=`、校正界面打开(不是 BUG-1038 的锁死页) | 失败 | | 13 | provider 读到另一个账户:快照清空 | 通过 | `home-warm-snapshot.test.ts`(12 条):写入 / 读取;换账户清空(含今日星语);`redirectToLogin` 清空;退出与 provider 三个分支清空(源码合同);快照源码不含任何持久存储;不齐全判定;今日星语跨日 / 换人 / 换账户不命中、无出生分钟无键;齐全时暖启动(新建本地空对话、默认模型、游标取舍);每种缺项都拒绝暖启动;落点与冷规则一致(keep / 存根写回 / 他人存根丢弃 / 裸 `/` 校正头落咨询 / 新建清存根 / 无会话的人只建一个 / 存根不在内存要 lookup);落点替换旧空对话、模型对账不改身份;挂载 query 的三种来源与记录只取一次、过期不信、只收同源路径。 ### 全量 | 项 | 基线 | 本分支 | | --- | --- | --- | | `tsc --noEmit` | 0 | 0 | | `npm run lint` | 0 error / 126 warning | 0 error / 126 warning | | `npm test`(Node 20,Linux) | 3928 / pass 3840 / fail 61 / skip 27 | 3953 / pass 3865 / fail 61 / skip 27 | | 失败名单 diff | — | 逐条一致(61 = 61,0 新增) | | 测试名 diff | — | 0 消失,+25(13 + 12) | | Python 源码合同(`tests/test_birth_time_journey_contract.py` 等 5 个文件) | 25 pass / 2 fail | 25 pass / 2 fail(同两条:`test_jyotish_web_auth.py` 仍查 Supabase 登录源码,基线即失败) | 过程中一次全量多出 1 条失败:`chat-navigation-a11y-contract.test.ts`「auth redirects stay hard document loads…」要求 `await selfHostedOtpActions.signOut();` 与 `window.location.assign("/login")` 紧邻。改为把清快照挪到 `signOut()` 调用之前,断言未动。 ### 改动的既有断言(三栏) 无。唯一触及的既有测试文件 `new-chat-from-people-lifecycle.test.tsx` 只在文档加载的 setup 里加了清快照两行,不改任何断言。 ## 构建 `npm run build -- --webpack`(Linux): | 项 | 基线 | 本分支 | | --- | --- | --- | | `/` | ○ Static | ○ Static | | `/chart` `/ephemeris` `/people` | ○ | ○ | | `/reports` | ƒ | ƒ | | 路由表整体 | — | 与基线构建逐行一致 | | `rootMainFiles` gzip -9 | 130933 B | 130933 B(0.00%) | | `/` index.html 引用的全部 js/css gzip -9 | 654283 B | 656646 B(+2363 B,+0.36%) | ## 浏览器与网络 本地 `next start`(端口 3431,本分支构建;基线用同代码工作树的构建,端口 3432),Chrome 151 `--headless=new`,CDP `Fetch` 拦截 `/api/*` 返回虚构账户(林遥)、一条生时校正、一条普通咨询。页面里用 `Page.addScriptToEvaluateOnNewDocument` 装 MutationObserver 记录「加载环出现」和「首页可交互(标题存在 + 输入框可用 + 路径 `/`)」的时间戳。返回阶段每个 `/api/*` 响应都人为延迟 1500 ms:如果首页等任何一个请求,可交互时间不可能早于 1500 ms。脚本在 scratchpad,未提交。 `/chart` → 新建对话(本分支): ``` interactiveMs 23, title 新对话, loadingRingShown 0, url / POST /api/daily-starlanguage @+15ms → answered +1519ms GET /api/rectification/cases/entry-summary @+16ms → +1519ms GET /api/chart-profiles @+16ms → +1519ms GET /api/synastry-reports @+16ms → +1520ms GET /api/models @+21ms → +1523ms GET /api/consult/status @+22ms → +1523ms GET /api/account @+22ms → +1524ms requestsAnsweredBeforeInteractive: [] ``` 同一路径(基线):`interactiveMs 5530, loadingRingShown 1`,可交互前已返回 `chart-profiles` `synastry-reports` `models` `consult/status` `entry-summary`;`daily-starlanguage` 在 +5526 ms 才发出。 `/reports` → 历史会话:本分支 `interactiveMs 19, loadingRingShown 0, url /?c=…`,可交互前 0 个请求返回;基线 `5532 ms, loadingRingShown 1`。 整页刷新:两边都 22–24 ms 出加载环。控制台 error:0。 截图:`docs/testing/home-warm-return-20260926-chart-new-chat.png`(新建对话暖返回,今日星语卡与校正入口已就位)、`docs/testing/home-warm-return-20260926-reports-history.png`(历史会话暖返回)。 ## 环境缺口 - Node 22:本机只有 v20.19.2,任务书要求的 Node 22 全量待协调方复跑(见 `BLOCKED.md`)。 - 无受控登录账号:登录态真机、iPhone Safari、真实 staging 数据按 `docs/testing/home-warm-return-20260926.md` 由产品走。 - 无 Docker:`npm run test:db` 未跑(不动表)。 - 未部署。