Files
Jyotisha/docs/tasks/PROGRESS-new-chat-from-people-20260926.md
T
Jesse_ChenandClaude Opus 5.5 0a8350cc01 fix(home): one new-chat intent and a scoped login-return stash (BUG-1038)
Coming back to / from /people could show the previous rectification session
as a locked page: its title in the header, the composer stuck on
"正在打开生时校正…", and the new-chat greeting in the middle.

The state that survived between pages is the sessionStorage login-return
stash that secondary-page sidebar links write from the current ?c=:
- /people「和 TA 对话」used a second intent (?newChat=1) parsed by a
  component mounted inside Home after bootstrap, so the bootstrap new-chat
  branch never ran and the stash won.
- A stash id not in the current person's loaded list was looked up and
  landed with urlAction "keep", which assumes ?c= is already in the address
  bar. It was not, so the rectification auto-open never fired. The stash
  also ignored which person was current.
- An in-page new chat left the stash in place.

Fix: delete NewChatDeepLink / ?newChat and route「和 TA 对话」through
newChatHref(); a looked-up stash writes ?c= back (replace-selected) and is
dropped when it belongs to another person; startNewChat and
openChatBoundToProfile clear the stash. ?c= deep links, BUG-989 and BUG-705
are unchanged.

Tests: new real-lifecycle suite mounting the real Home, sidebar and people
page (10 cases: four secondary pages + mobile drawer, 和 TA 对话 for self and
another person, out-of-scope stash, same-person stash beyond the first page,
in-page new chat), plus two contract/unit tests. Six fail on origin/staging,
all pass here. Full suite 3928 / 61 failing, failure names identical to the
0ab061b9 baseline (3916 / 61).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 07:23:50 +08:00

125 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · 从星盘档案页回首页落到上一次生时校正(2026-09-26)
任务书:`docs/tasks/TASK-new-chat-from-people-20260926.md`。分支 `codex/new-chat-from-people-20260926`,worktree `.worktrees/new-chat-from-people-20260926`。执行方:Claude 子代理(产品授权直接执行)。未推送、未部署。BUG-1038(开工时最大 BUG-1037;并行单已用 BUG-1039,1038 仍空)。
## 基线
- 开工基线 `65260ca7`;实现期间 `origin/staging` 前进到 `0ab061b9`(BUG-1039 档案页样式 + 星历改版),本分支已 rebase 到 `0ab061b9`。`people-page.tsx` 冲突一处:保留 staging 的新布局,只把「和 TA 对话」那一行的跳转改成 `newChatHref()`。
- 基线全量(`0ab061b9`,同一台机器、Node v20.19.2、Linux):3916 条,61 失败,27 跳过。61 条都是环境缺口(Node 20 没有 `mock.module` 等)。
## 复现与根因
### 怎么复现的
两套手段,结论一致:
1. **真实浏览器**:本地 `next build --webpack` + `next start`,Chrome 151 无头,用 CDP `Fetch` 拦截 `/api/*` 返回虚构账户(林遥)、一条生时校正会话(「生时校正 · 04:45–05:15」)、一条普通咨询、一个他人(阿澈)。脚本在 scratchpad,未提交。
2. **真实 React 生命周期测试**(新增 `frontend/tests/new-chat-from-people-lifecycle.test.tsx`):挂载真实 `Home`(`app/(app)/page.tsx` 默认导出)、真实 `SessionListProvider` + `AppSidebar` + `SidebarInset`(与 `(app)/layout.tsx` 同构;layout 本身引 CSS,不能在 tsx 里 import)、真实 `PeoplePage`。客户端导航 = 保留 provider 与侧栏、换路由子树;文档加载(`window.location.assign`)= 关掉 root、重置 current-subject 模块状态、保留 sessionStorage / localStorage。
| 路径 | 修复前(`origin/staging`) | 说明 |
| --- | --- | --- |
| 开校正 → 侧栏「星盘档案」→ 选他人「看星盘」→ `/chart` 左下角账户(回裸 `/`) | **标题「生时校正 · 04:45–05:15」、占位「正在打开生时校正…」、发送禁用、中间「又见面了,林遥」+ 今日星语 / 生时校正两个入口,永久停住** | 与产品截图逐项一致,见 `docs/testing/new-chat-from-people-20260926-before.png` |
| 开校正 → 「星盘档案」→ 本人「和 TA 对话」(`/?newChat=1`) | 地址被写成 `/?c=<校正>`,后台再 POST 一次 open,约 2 秒后从新对话翻回旧校正界面 | 慢打开(6 s)时可见先新对话、后翻回 |
| 开校正 → 「星盘档案」→ 他人「和 TA 对话」 | 最终是新对话(`NewChatDeepLink` 抢回),但启动先按存根 lookup 了旧校正 | 时序侥幸 |
| 开校正 → 「星盘档案」→ 侧栏「新建对话」(`/?new=1`) | **未复现**:新对话、地址 `/`、不再 open | 浏览器 4 种变体(默认 / 慢网 500 ms / 慢打开 6 s / 只有校正会话)+ lifecycle 四个次级页与手机抽屉 |
产品描述的入口(`/people` 点侧栏「新建对话」)本机没有复现;和截图一模一样的画面出现在「在档案页换成他人后经不带新建意图的 `/` 回首页」。已写进 BLOCKED,请产品真机确认当时的点击路径(`docs/testing/secondary-new-chat-20260923.md` 追加节 A1–A6)。
### 残留状态是哪一个(D3)
**`sessionStorage["jyotisha.session-url-return"]`(登录返回存根)**。只读侧栏的次级页链接 `onClick={leaveChat}` → `persistLoginSessionReturn()` 把当前 `?c=`(那条校正会话)存进去;它跨路由、跨文档加载存活,下一次首页启动在「没有 `new`、没有 `c`」时拿它当落点。
三处缺口:
1. **第二套新建意图**:「和 TA 对话」用 `/?newChat=1`,由首页里挂载的 `NewChatDeepLink` 在启动**之后**解析。启动的 new-chat 分支(会忽略并清掉存根)从未走到,存根照常胜出。
2. **存根 lookup 落地不写 `?c=`**:存根 id 不在当前人物已加载的列表里时,`resolveLookupBootstrap` 去服务端找,找到返回 `urlAction: "keep"`。`keep` 是给「地址栏已经带 `?c=`」设计的;存根来源时地址栏没有 `?c=`,`shouldAutoOpenRectificationSession` 永远不成立 → 校正会话成了活动会话却不打开 → `composerLocksAsRectification` 锁住输入框、占位「正在打开生时校正…」,中间因为该会话没有消息显示开场问候。存根还不区分人物:当前人物换成他人后,本人的校正会话照样被拿来落地。
3. **首页内新建不清存根**:lookup 暂时失败(`unavailable`)时存根保留;之后在首页点「新建对话」,再去次级页、回裸 `/`,仍会落回旧会话。
任务书候选里的「外壳注册 `openingSessionId`」「校正面模块级缓存」「current-subject emit 触发 `activateFallbackSession`」都查过:外壳注册在 Home 卸载时 `registerShellControls(null)`;校正面没有模块级状态;`activateFallbackSession` 只在删除会话时调用。都不是。
## 改了什么
| 任务 | 文件 | 内容 |
| --- | --- | --- |
| T2 / D1 | `frontend/src/components/people-home-links.tsx` | 删除 `NewChatDeepLink`(和它唯一用到的 `useCurrentSubjectReady` import)。 |
| T2 / D1 | `frontend/src/app/(app)/page.tsx` | 去掉 `NewChatDeepLink` 的 import 与挂载(-1 行净;`Home()` 的 useState / useRef 数不变)。 |
| T2 / D1 | `frontend/src/components/people/people-page.tsx` | 「和 TA 对话」`openPerson(selected.id, newChatHref())`。人物仍由 `setCurrentSubject(id)` 带过去(写 localStorage,文档加载后由 `bindCurrentSubjectAccount` 读回),启动新建分支按当前人物建本地空咨询,所以 `newChatHref` 不需要带 `subjectId` 参数。 |
| T2 | `frontend/src/lib/chat-session-url.ts` | `newChatHref` 注释写明它是唯一新建意图;`bootstrapSelectionFromLookup` 加 `origin` 参数(默认 `"url"`,行为不变):`stored-return` + found → `replace-selected`(写回 `?c=`);新状态 `other-subject` → 默认落点、清存根。 |
| T2 / D3 | `frontend/src/lib/home-cloud-sync.ts` | `resolveLookupBootstrap` 判断来源(地址栏有无 `?c=`),加可选 `inSubjectScope` 判定;存根会话不属于当前人物时走 `other-subject`,不合并进列表。 |
| T2 / D3 | `frontend/src/lib/home-bootstrap-run.ts` | `resolveLanding` 传 `inSubjectScope: sessionMatchesSubject(session, readCurrentSubjectId())`(放这里而不是 home-cloud-sync,避免 home-cloud-sync ↔ current-subject 循环依赖)。 |
| T2 / D3 | `frontend/src/hooks/use-session-management.ts` | `startNewChat`、`openChatBoundToProfile` 调 `clearLoginSessionReturn()`。 |
| T1 / T3 | `frontend/tests/new-chat-from-people-lifecycle.test.tsx`(新) | 10 条真实生命周期测试,见下。 |
| T3 | `frontend/tests/chat-session-url.test.ts` | 新增 1 条源码合同:「和 TA 对话」走 `newChatHref()`,people-page / people-home-links / page.tsx 里不再有 `newChat=`、`"newChat"`、`NewChatDeepLink`。 |
| T2 | `frontend/tests/session-lookup-unlisted.test.ts` | 新增 1 条:`stored-return` found → `replace-selected`;`other-subject` → 默认 + 清存根;`url` found 仍 `keep`。 |
| T4 | `docs/BUG_HISTORY.md`、`CHANGELOG.md`、`BLOCKED.md`、`docs/tasks/README.md`、`docs/testing/secondary-new-chat-20260923.md`(追加节 A1–A6)、`docs/testing/new-chat-from-people-20260926-{before,after,after-talk}.png` | 记录。不改 UI 样式与文案,`frontend/DESIGN.md` / `VOICE.md` 不动。 |
不变的口径:`?c=` 深链找到仍 `keep`;BUG-989 首问前不落库、不写 `?c=`;BUG-705 lookup 与 404 / 5xx 处理;无参数 `/` 回到同一人物的上一条会话(现在会把 `?c=` 写回,校正会话照常打开)。
## 新增测试(lifecycle 10 条)
| # | 名称(节选) | 修复前 `0ab061b9` | 修复后 |
| --- | --- | --- | --- |
| 1–4 | from /chart · /ephemeris · /reports · /people,侧栏「新建对话」 | 通过 | 通过 |
| 5 | from /people (mobile drawer),侧栏「新建对话」,抽屉关闭 | 通过 | 通过 |
| 6 | 「和 TA 对话」本人 | **失败**(地址 `/?c=<校正>`、多一次 open) | 通过 |
| 7 | 「和 TA 对话」他人,标题旁是 TA 的名字 | 通过(时序侥幸) | 通过 |
| 8 | 他人存根不落锁死页 | **失败**(占位「正在打开生时校正…」) | 通过 |
| 9 | 同人存根超出首页列表:带 `?c=` 打开校正界面 | **失败**(锁死页) | 通过 |
| 10 | 首页内新建清存根(真实 `useSessionManagement`) | **失败** | 通过 |
加上两条纯函数 / 源码合同(修复前都失败),共新增 12 条。1–5 在修复前就通过:它们锁的是侧栏新建入口不回退(D2),不是复现用例。
## 验证
| 项 | 结果 |
| --- | --- |
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error,126 warning(基线 127:删掉的 `NewChatDeepLink` 带走一条 exhaustive-deps) |
| 定向 `tsx --test` lifecycle + chat-session-url + session-lookup-unlisted + people-archive-view + new-chat-recovery + home-bootstrap-run + sidebar-contract + session-list-lifecycle | 全部通过 |
| 全量 `npm test`(Node 20) | 3928 条,61 失败,27 跳过(基线 3916 / 61 / 27) |
| 失败名单对照 | 逐条相同,新增失败 0 |
| 测试名对照 | 消失 0,新增 12 |
| `next build --webpack` | `/` ○、`/chart` ○、`/ephemeris` ○、`/people` ○、`/reports` ƒ、`/reports/[reportId]` ƒ,与基线一致 |
| 首屏 gzip(`/` 预渲染 HTML 引用的 36 个 js/css,gzip -9 求和) | 基线 656,329 B → 654,283 B(-0.31%) |
失败名单对照命令:
```bash
grep -E '^not ok' base.log | sed 's/^not ok [0-9]* - //; s/ # .*//' | sort > fail-base.txt
grep -E '^not ok' new.log | sed 's/^not ok [0-9]* - //; s/ # .*//' | sort > fail-new.txt
diff fail-base.txt fail-new.txt && echo SAME-FAILS # → SAME-FAILS
grep -E "^(not )?ok [0-9]+ - " base.log | sed -E 's/^(not )?ok [0-9]+ - //; s/ # .*//' | sort > names-base.txt
grep -E "^(not )?ok [0-9]+ - " new.log | sed -E 's/^(not )?ok [0-9]+ - //; s/ # .*//' | sort > names-new.txt
comm -23 names-base.txt names-new.txt # → 空(无消失)
comm -13 names-base.txt names-new.txt | wc -l # → 12
```
## 改动的既有断言
无。`chat-session-url.test.ts`、`session-lookup-unlisted.test.ts` 只新增测试,没有改、删任何既有断言;`sidebar-contract.test.ts` 锁的新建链接 `href={newChatHref()} … onClick={closeDrawer}` 未动。
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| — | — | 本轮没有修改既有断言 |
## 浏览器证据
本地 `next start`(rebase 后的构建)+ Chrome 151,1280×900,虚构数据:
| 路径 | 修复后结果 |
| --- | --- |
| 他人「看星盘」→ 左下角账户回 `/` | 标题「新对话 阿澈」,占位「想聊什么都可以」,可输入,无第二次 open(`new-chat-from-people-20260926-after.png`) |
| 本人「和 TA 对话」 | 地址 `/`,新对话,5 秒内不翻回,无第二次 open |
| 他人「和 TA 对话」 | 标题「新对话 阿澈」(`new-chat-from-people-20260926-after-talk.png`) |
| 侧栏「新建对话」(默认 / 慢网 500 ms + 慢打开 6 s) | 新对话,地址 `/`,无第二次 open |
修复前同路径截图:`new-chat-from-people-20260926-before.png`(基线构建)。右下角「合盘历史暂时无法读取」提示是 CDP 桩没有模拟 `/api/synastry-reports` 造成的,与本单无关。
## 环境缺口(未写成通过)
- **无受控登录账号**:没有在 staging 或 iPhone Safari 上走;真机清单见 `docs/testing/secondary-new-chat-20260923.md` 追加节 A1–A6。BUG-1038 保持 investigating。
- **产品描述的入口未复现**:见上。需要产品真机确认。
- **浏览器里没做手机抽屉**:CDP 脚本找抽屉触发器没对上;手机抽屉由 lifecycle 测试 #5(innerWidth 390,真实 `SidebarProvider` / `AppSidebar`)覆盖。
- **Node 22**:本机只有 Node 20,全量以失败名单逐条对照代替,Linux Node 22 全量待协调方复跑。
- **Docker**:无,`npm run test:db` 未跑;本单不动表。