48174a3cc5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
8.9 KiB
8.9 KiB
任务书 · 会话 URL 化(2026-09-01)
基线:origin/staging @ b6989c3e(开工时以 origin/staging 最新为准)。本轮是 BUG-464(消息服务端权威化)的直接后续,依赖该轮交付的 GET /api/sessions/[id] 详情接口与 ensureSessionMessages 按需加载。与任何同期改 frontend/src/app/page.tsx 的轮次不得并行。
为什么要做(事故实证)
下面所有行号只是线索,按符号定位,origin/staging 上可能有偏移。
- 会话选择是纯内存状态,URL 里没有任何痕迹。
activeSessionId是useState("")(page.tsx:1285),全仓frontend/src没有一处pushState/replaceState/useSearchParams(grep 可证)。后果:- 刷新丢位置:无论用户在哪个会话,刷新后回到默认态(启动逻辑
page.tsx:1904固定选nextSessions[0],即最近更新的会话)。 - 浏览器返回键无意义:在会话间切换不产生历史条目,back 直接离开站点。
- 无深链:用户无法把某个会话的地址发给自己另一台设备或收藏。
- 刷新丢位置:无论用户在哪个会话,刷新后回到默认态(启动逻辑
- 401 跳登录后回不到原会话。
redirectToLogin(page.tsx:1120附近)window.location.replace("/login"),登录成功后email-otp-login.tsx用successPath落回/(email-otp-login.tsx:41,类型是"/" | "/admin"字面量联合)——中途丢掉一切位置信息。 - 上一轮已具备的能力(本轮直接复用,不要重做):
GET /api/sessions/[id]详情接口、ensureSessionMessages的按需加载与messagesHydrated内存缓存、selectSession(page.tsx:2442)这个唯一的用户切换入口。
决策记录(产品授权,2026-09-01)
- 采用 query 参数
?c=<sessionId>,不采用路径段/c/[sessionId]。 理由:路径段方案要求把 4557 行的Home组件抽成两个路由共享的模块,或改造 app 目录结构——重构风险远超本轮收益;query 方案让/保持现有路由形态与○ Static构建模式(上一轮优化成果,有合同测试依赖)。将来要好看的路径,可以加一个薄的/c/[id]→/?c=<id>redirect,不在本轮。 - 用原生
window.history.pushState/replaceState+popstate,不用router.push。 Next 16 官方文档明确支持原生 History API 并与 Router 集成(见node_modules/next/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md的 "Native History API" 一节,写代码前先读它)。router.push到同路径不同 query 会走一次 Next 导航流程,本页所有状态本来就在客户端,纯 History API 足够且不惊动路由。 - 401 回跳用 sessionStorage 暂存,不给登录页加
next参数。successPath的字面量联合类型是有意的防开放重定向设计,不拓宽它。redirectToLogin前把当前?c=值存 sessionStorage,登录落回/后启动逻辑读取并恢复。登录页零改动。 - 默认选中不写 URL。 用户没点过任何会话时(启动默认选中最近会话、或 starter home),地址栏保持干净的
/;只有用户主动切换/新建/打开校正时才pushState。这样 back 键的语义是"回到上一个我主动去过的地方"。
硬红线
/的构建模式不得回归。next build后/必须仍是○ Static;不得为读取 query 引入让页面转ƒ的用法(读参数用客户端location.search/ popstate,不要在服务端组件层碰searchParams)。现有锁路由/样式隔离的合同测试必须保持绿灯且不被修改。- 不得改
email-otp-login.tsx的successPath类型或登录页逻辑(决策记录 3 的方案不需要)。 - 不得手写
useCallback/useMemo。 - 不得修改既有测试断言 —— 例外仅限锁住"会话选择无 URL"这一缺陷本身的断言;须在断言上方注释原值与原因,并在 PROGRESS 单列。
- 推 staging 前必须
./node_modules/.bin/tsc --noEmit通过。不要用npx tsc,本仓库环境下会装到空包tsc@2.0.4。 - 测试总数不得低于基线 2413,且 fail=0、skipped=0(有 Docker 的环境)。无 Docker 时既有缺口为 24 条数据库/部署类失败 + 10 skipped(清单与
b6989c3e一致),必须逐条比对确认没有新增。本轮不动数据库,test:db不新增要求。 - 不得改
.gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。 - 本轮无视觉改动;若新增提示文案(如"该对话不存在"),沿用现有 composer notice / toast 通道,浅色深色两套下检查。
让步顺序:功能与测试不回归 > 可验证的修复 > 代码整洁。
开工前置
git fetch origin --prune
git worktree add -b codex/session-url-20260901 \
../.worktrees/session-url-20260901 origin/staging
基线必须是 origin/staging。读 pre_work_error_ledger.md,跑 scripts/pre_work_check.py,读 frontend/AGENTS.md。**先读 node_modules/next/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md 的 Native History API 一节再写代码。**改前在 docs/BUG_HISTORY.md 检索同类记录(BUG-464 是直接上游)。
先读这几处再动手:
page.tsx的activeSessionId全部触点:1285(state)、1377(?? sessions[0]兜底)、1904(启动默认选中)、1989/3271(恢复流程)、2351/2370(删除)、2413/2424(新建与失败回滚)、2442(selectSession,用户切换唯一入口)、3036(校正打开)、1795(uiPreview,预览态不碰 URL)。ensureSessionMessages(2298 附近):详情加载已有 in-flight 去重与 hydrated 缓存,URL 恢复直接复用,不要另写加载逻辑。redirectToLogin(1120 附近)。
任务分解
任务 1(P0)· URL 读写与历史导航
- 启动:解析
location.search的c。合法 UUID 且存在于列表 → 选中它(URL 保持不动);不存在/非法 →replaceState清掉参数、走现有默认选中、composer notice 提示"该对话不存在或已被删除"。 - 用户主动动作后写 URL:
selectSession→pushState('?c=<id>');startNewChat成功后 →pushState(create 失败回滚时replaceState回上一个状态);校正打开(3036)→pushState。默认选中与恢复流程(1904/1989/3271)不写 URL(决策记录 4)。 - 删除当前会话(2351/2370):切到兜底会话并
replaceState(有兜底会话写它的 id,没有则清参数),URL 不得留死链。 popstate:按事件里的?c=(无参数 = 默认会话)执行与selectSession相同的切换,但不得再 pushState(用来源标志位区分,防历史条目翻倍);id 已不在列表时按"不存在"处理。- 流式进行中切换会话/back-forward:保持现有行为(生成继续、回来能看到),不得因 URL 逻辑中断 streaming。
uiPreview模式(1795)不读不写 URL。
任务 2(P1)· 401 回跳恢复
redirectToLogin跳转前:当前?c=值(仅校验为 UUID 后)存sessionStorage(读写包 try/catch)。- 登录成功落回
/后的启动逻辑:URL 无c且 sessionStorage 有暂存 → 按暂存 id 走任务 1 的启动选中并replaceState写回 URL,随后清暂存;URL 已有c则忽略暂存。 - 暂存的 id 已失效(会话被删)→ 走"不存在"分支,不报错。
任务 3(P1)· 合同测试
- 新增合同测试锁住:启动读参、popstate 不二次 push、删除后 URL 清理、默认选中不写 URL、
redirectToLogin暂存。样式参照本仓库既有的源码正则合同测试。 next build路由模式断言若已有测试锁/为 Static,确认它仍绿;没有的话本轮补一条。
总验收
./node_modules/.bin/tsc --noEmit通过;测试满足红线 6。next build:/仍为○ Static,写进 PROGRESS。- 行为实测(有登录态环境逐条做,无登录态则用合同测试覆盖并在 PROGRESS 如实标注哪些没实测):
- 打开会话 A → 刷新 → 仍在 A,消息经详情接口恢复;
- 粘贴
/?c=<id>直达指定会话; - A→B→C 后 back 回 B、再 back 回 A、forward 回 B,全程无整页刷新,已 hydrated 的会话不重复请求详情;
- 删除当前会话后地址栏无死链;伪造/他人会话 id 得到提示并回默认态;
- 流式生成中切走再 back 回来,生成未中断;
- 401 跳登录 → 登录成功 → 回到跳转前的会话。
明确不做(不要顺手做)
- 不做
/c/[sessionId]路径段或任何 app 目录结构调整。 - 不拆
page.tsx(P2 另立项)。 - 不动登录页、不加
next回跳参数。 - 不给报告页/会员页等其它 surface 加 URL 状态。
- 不删 BUG-464 的 PATCH 兼容层(等观测确认无量后另一轮)。