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
12 KiB
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等)。
复现与根因
怎么复现的
两套手段,结论一致:
- 真实浏览器:本地
next build --webpack+next start,Chrome 151 无头,用 CDPFetch拦截/api/*返回虚构账户(林遥)、一条生时校正会话(「生时校正 · 04:45–05:15」)、一条普通咨询、一个他人(阿澈)。脚本在 scratchpad,未提交。 - 真实 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」时拿它当落点。
三处缺口:
- 第二套新建意图:「和 TA 对话」用
/?newChat=1,由首页里挂载的NewChatDeepLink在启动之后解析。启动的 new-chat 分支(会忽略并清掉存根)从未走到,存根照常胜出。 - 存根 lookup 落地不写
?c=:存根 id 不在当前人物已加载的列表里时,resolveLookupBootstrap去服务端找,找到返回urlAction: "keep"。keep是给「地址栏已经带?c=」设计的;存根来源时地址栏没有?c=,shouldAutoOpenRectificationSession永远不成立 → 校正会话成了活动会话却不打开 →composerLocksAsRectification锁住输入框、占位「正在打开生时校正…」,中间因为该会话没有消息显示开场问候。存根还不区分人物:当前人物换成他人后,本人的校正会话照样被拿来落地。 - 首页内新建不清存根: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%) |
失败名单对照命令:
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未跑;本单不动表。