Files
Jyotisha/TASK-session-url-20260901.md
T
Jesse_Chen 48174a3cc5
Independent Staging Quality Gate / validate (push) Successful in 10m45s
Independent Staging Quality Gate / publish (push) Has been cancelled
docs(chat): add session URL routing task brief
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-09-01 12:03:26 +00:00

8.9 KiB
Raw Blame History

任务书 · 会话 URL 化(2026-09-01

基线:origin/staging @ b6989c3e(开工时以 origin/staging 最新为准)。本轮是 BUG-464(消息服务端权威化)的直接后续,依赖该轮交付的 GET /api/sessions/[id] 详情接口与 ensureSessionMessages 按需加载。与任何同期改 frontend/src/app/page.tsx 的轮次不得并行


为什么要做(事故实证)

下面所有行号只是线索,按符号定位,origin/staging 上可能有偏移。

  1. 会话选择是纯内存状态,URL 里没有任何痕迹。 activeSessionIduseState("")page.tsx:1285),全仓 frontend/src 没有一处 pushState / replaceState / useSearchParamsgrep 可证)。后果:
    • 刷新丢位置:无论用户在哪个会话,刷新后回到默认态(启动逻辑 page.tsx:1904 固定选 nextSessions[0],即最近更新的会话)。
    • 浏览器返回键无意义:在会话间切换不产生历史条目,back 直接离开站点。
    • 无深链:用户无法把某个会话的地址发给自己另一台设备或收藏。
  2. 401 跳登录后回不到原会话。 redirectToLoginpage.tsx:1120 附近)window.location.replace("/login"),登录成功后 email-otp-login.tsxsuccessPath 落回 /email-otp-login.tsx:41,类型是 "/" | "/admin" 字面量联合)——中途丢掉一切位置信息。
  3. 上一轮已具备的能力(本轮直接复用,不要重做):GET /api/sessions/[id] 详情接口、ensureSessionMessages 的按需加载与 messagesHydrated 内存缓存、selectSessionpage.tsx:2442)这个唯一的用户切换入口。

决策记录(产品授权,2026-09-01)

  1. 采用 query 参数 ?c=<sessionId>,不采用路径段 /c/[sessionId] 理由:路径段方案要求把 4557 行的 Home 组件抽成两个路由共享的模块,或改造 app 目录结构——重构风险远超本轮收益;query 方案让 / 保持现有路由形态与 ○ Static 构建模式(上一轮优化成果,有合同测试依赖)。将来要好看的路径,可以加一个薄的 /c/[id]/?c=<id> redirect,不在本轮。
  2. 用原生 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 足够且不惊动路由。
  3. 401 回跳用 sessionStorage 暂存,不给登录页加 next 参数。 successPath 的字面量联合类型是有意的防开放重定向设计,不拓宽它。redirectToLogin 前把当前 ?c= 值存 sessionStorage,登录落回 / 后启动逻辑读取并恢复。登录页零改动。
  4. 默认选中不写 URL。 用户没点过任何会话时(启动默认选中最近会话、或 starter home),地址栏保持干净的 /;只有用户主动切换/新建/打开校正时才 pushState。这样 back 键的语义是"回到上一个我主动去过的地方"。

硬红线

  1. / 的构建模式不得回归。 next build/ 必须仍是 ○ Static;不得为读取 query 引入让页面转 ƒ 的用法(读参数用客户端 location.search / popstate,不要在服务端组件层碰 searchParams)。现有锁路由/样式隔离的合同测试必须保持绿灯且不被修改。
  2. 不得改 email-otp-login.tsxsuccessPath 类型或登录页逻辑(决策记录 3 的方案不需要)。
  3. 不得手写 useCallback / useMemo
  4. 不得修改既有测试断言 —— 例外仅限锁住"会话选择无 URL"这一缺陷本身的断言;须在断言上方注释原值与原因,并在 PROGRESS 单列。
  5. 推 staging 前必须 ./node_modules/.bin/tsc --noEmit 通过。不要用 npx tsc,本仓库环境下会装到空包 tsc@2.0.4
  6. 测试总数不得低于基线 2413,且 fail=0、skipped=0(有 Docker 的环境)。无 Docker 时既有缺口为 24 条数据库/部署类失败 + 10 skipped(清单与 b6989c3e 一致),必须逐条比对确认没有新增。本轮不动数据库,test:db 不新增要求。
  7. 不得改 .gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
  8. 本轮无视觉改动;若新增提示文案(如"该对话不存在"),沿用现有 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.tsxactiveSessionId 全部触点:1285state)、1377?? sessions[0] 兜底)、1904(启动默认选中)、1989/3271(恢复流程)、2351/2370(删除)、2413/2424(新建与失败回滚)、2442(selectSession,用户切换唯一入口)、3036(校正打开)、1795uiPreview预览态不碰 URL)。
  • ensureSessionMessages(2298 附近):详情加载已有 in-flight 去重与 hydrated 缓存,URL 恢复直接复用,不要另写加载逻辑。
  • redirectToLogin1120 附近)。

任务分解

任务 1(P0)· URL 读写与历史导航

  • 启动:解析 location.searchc。合法 UUID 且存在于列表 → 选中它(URL 保持不动);不存在/非法 → replaceState 清掉参数、走现有默认选中、composer notice 提示"该对话不存在或已被删除"。
  • 用户主动动作后写 URLselectSessionpushState('?c=<id>')startNewChat 成功后 → pushStatecreate 失败回滚时 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。

任务 2P1)· 401 回跳恢复

  • redirectToLogin 跳转前:当前 ?c= 值(仅校验为 UUID 后)存 sessionStorage(读写包 try/catch)。
  • 登录成功落回 / 后的启动逻辑:URL 无 c 且 sessionStorage 有暂存 → 按暂存 id 走任务 1 的启动选中并 replaceState 写回 URL,随后清暂存;URL 已有 c 则忽略暂存。
  • 暂存的 id 已失效(会话被删)→ 走"不存在"分支,不报错。

任务 3P1)· 合同测试

  • 新增合同测试锁住:启动读参、popstate 不二次 push、删除后 URL 清理、默认选中不写 URL、redirectToLogin 暂存。样式参照本仓库既有的源码正则合同测试。
  • next build 路由模式断言若已有测试锁 / 为 Static,确认它仍绿;没有的话本轮补一条。

总验收

  1. ./node_modules/.bin/tsc --noEmit 通过;测试满足红线 6。
  2. next build/ 仍为 ○ Static,写进 PROGRESS。
  3. 行为实测(有登录态环境逐条做,无登录态则用合同测试覆盖并在 PROGRESS 如实标注哪些没实测):
    • 打开会话 A → 刷新 → 仍在 A,消息经详情接口恢复;
    • 粘贴 /?c=<id> 直达指定会话;
    • A→B→C 后 back 回 B、再 back 回 A、forward 回 B,全程无整页刷新,已 hydrated 的会话不重复请求详情;
    • 删除当前会话后地址栏无死链;伪造/他人会话 id 得到提示并回默认态;
    • 流式生成中切走再 back 回来,生成未中断;
    • 401 跳登录 → 登录成功 → 回到跳转前的会话。

明确不做(不要顺手做)

  • 不做 /c/[sessionId] 路径段或任何 app 目录结构调整。
  • 不拆 page.tsxP2 另立项)。
  • 不动登录页、不加 next 回跳参数。
  • 不给报告页/会员页等其它 surface 加 URL 状态。
  • 不删 BUG-464 的 PATCH 兼容层(等观测确认无量后另一轮)。