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

12 KiB
Raw Blame History

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%)

失败名单对照命令:

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 未跑;本单不动表。