docs(chat): add session URL routing task brief
Independent Staging Quality Gate / validate (push) Successful in 10m45s
Independent Staging Quality Gate / publish (push) Has been cancelled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
This commit is contained in:
Jesse_Chen
2026-09-01 12:03:26 +00:00
parent b6989c3eea
commit 48174a3cc5
+94
View File
@@ -0,0 +1,94 @@
# 任务书 · 会话 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 里没有任何痕迹。** `activeSessionId``useState("")``page.tsx:1285`),全仓 `frontend/src` 没有一处 `pushState` / `replaceState` / `useSearchParams`grep 可证)。后果:
- **刷新丢位置**:无论用户在哪个会话,刷新后回到默认态(启动逻辑 `page.tsx:1904` 固定选 `nextSessions[0]`,即最近更新的会话)。
- **浏览器返回键无意义**:在会话间切换不产生历史条目,back 直接离开站点。
- **无深链**:用户无法把某个会话的地址发给自己另一台设备或收藏。
2. **401 跳登录后回不到原会话。** `redirectToLogin``page.tsx:1120` 附近)`window.location.replace("/login")`,登录成功后 `email-otp-login.tsx``successPath` 落回 `/``email-otp-login.tsx:41`,类型是 `"/" | "/admin"` 字面量联合)——中途丢掉一切位置信息。
3. 上一轮已具备的能力(本轮直接复用,不要重做):`GET /api/sessions/[id]` 详情接口、`ensureSessionMessages` 的按需加载与 `messagesHydrated` 内存缓存、`selectSession``page.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.tsx` 的 `successPath` 类型或登录页逻辑**(决策记录 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 通道,浅色深色两套下检查。
让步顺序:功能与测试不回归 > 可验证的修复 > 代码整洁。
## 开工前置
```bash
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` 全部触点:1285state)、1377`?? sessions[0]` 兜底)、1904(启动默认选中)、1989/3271(恢复流程)、2351/2370(删除)、2413/2424(新建与失败回滚)、2442(`selectSession`,用户切换唯一入口)、3036(校正打开)、1795uiPreview**预览态不碰 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。
### 任务 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.tsx`P2 另立项)。
- 不动登录页、不加 `next` 回跳参数。
- 不给报告页/会员页等其它 surface 加 URL 状态。
- 不删 BUG-464 的 PATCH 兼容层(等观测确认无量后另一轮)。