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

95 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 会话 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 兼容层(等观测确认无量后另一轮)。