docs(tasks): 校正会话输入框守卫单 + 会话列表单一数据源单
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
eea90926a1
commit
8eb1de1fa1
@@ -106,6 +106,7 @@
|
||||
| `TASK-rectification-dead-d9-choice-fix-20260916.md` | `PROGRESS-rectification-dead-d9-choice-fix-20260916.md` | **验收修复单**:自建分盘探针照 `withNakshatraBoundaryProbe` 模式在每个读 state 的地方(盖戳 / 答题 / `previousInferenceFromReceipt` / GET 投影 / idle 过期判定 / 计划承接)从 receipt 确定性重建(BUG-915);测试改走生产路径、删手工塞探针的 fixture;顺带 host 前置挪到会话校验之后、登记 phase(BUG-916);死卡不得配 `collect_waiting` 占位(BUG-917);**产品决策 1c**:风格题默认不作全额计分(沿用 09-09 ±1 拍板),无事件探针就走下一条线。**staging 现状:风格题答不了,先跳过**。§1d 已记 **产品决策 (b)** | 已验收 | `5113d457`(BUG-915~917,产品拍板 (b) 风格题不计分)。Claude 独立验收:生产路径测试(receipt 不含自建探针)答题 applied / GET 出卡 / idle 不误伤;合并候选门禁 tsc 0 / lint 0 error / npm test 3427 条 31 红与基线逐条相同 / `/` Static / 首屏 gzip 620,107→620,193(+0.01%)/ pytest 63 绿 / 快速门 Python 798 绿。真机清单 `docs/testing/rectification-dead-d9-choice-fix-20260916.md` |
|
||||
| `TASK-rectification-mobile-timeline-readout-20260917.md` | `PROGRESS-rectification-mobile-timeline-readout-20260917.md` | 手机截图:时间轴读数第四项被裁成「已…」(nowrap + inset 内边距,BUG-918);「跳到最新」浮层压住选项 C(BUG-919)。灰卡与「再说一件」矛盾归修复单 BUG-915/917 | 已验收 | `e8e98bbd`(合入时重放为本分支提交,BUG-918/919)。时间轴相关 71 条 + 全量同上;手机上隐藏「已对照 N 件」符合 DESIGN §10;桌面是否也移除待产品拍板。真机清单 `docs/testing/rectification-mobile-timeline-readout-20260917.md` |
|
||||
| `TASK-mobile-viewport-scroll-lock-20260917.md` | `PROGRESS-mobile-viewport-scroll-lock-20260917.md` | iPhone 上键盘收起 / 刷新后整页上移、顶栏点不到(BUG-920):iOS 不支持 `interactive-widget`,键盘弹出时 Safari 滚动 window,`html/body overflow: hidden` 让用户拉不回来,reload 又还原 `scrollY`;代码里无任何 window 级复位。补 `ViewportScrollLock`(`scrollRestoration=manual` + `visualViewport` 复位)。与今天两单无关,既有缺陷。**T3 顶栏积分块「有点扁」(BUG-921)**:两枚芯片 44px 塞在 46px 顶栏里、字号/内边距/圆角各一套、积分块强制 64px 最小宽度;统一尺寸并把手机顶栏放到 52px | 待验收 | `codex/mobile-viewport-scroll-lock-20260917` |
|
||||
| `TASK-rectification-session-composer-guard-20260917.md` | — | 校正会话激活但校正面未开(刷新揭幕超时 / 打开失败不重试 / 删除与 popstate 回退落到列表第一条)时露出可用的普通输入框,问题发到 `/api/consult` 回 404「咨询会话不存在」;`send()` 无会话类型守卫;服务端无独立错误码。关联 BUG-505。BUG 段 924 起 | 待领取 | — |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -130,6 +131,7 @@
|
||||
| `TASK-rectification-p0-fix-20260915.md` | `PROGRESS-rectification-p0-fix-20260915.md` | **验收修复单**:`f51e494c` 六条缺陷全部实现且方式正确,但 `page.tsx` 从 1951 涨到 1964 行,撞了 `chart-view-route.test.ts` 的 `<= 1951` 上限(AGENTS.md §6 增长冻结)。全量 fail 32→33,就这一条。门禁红很可能是 staging 停在 `2d7698ea`、6 个提交未部署的原因。修法是把 BUG-705 的十来行接线搬出 page.tsx,不放宽上限 | 待验收 | `codex/rectification-p0-fix-20260915` |
|
||||
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal` 的 `dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
|
||||
| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` |
|
||||
| `TASK-session-list-single-source-20260917.md` | — | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动;空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 待领取 | — |
|
||||
|
||||
### 个人报告
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# 任务书 · 校正会话上露出普通输入框,发问打到 `/api/consult` 报「咨询会话不存在」(2026-09-17)
|
||||
|
||||
## 0. 基线
|
||||
|
||||
- 基线 commit:`eea90926`(`origin/staging` head;staging 已部署 `dc2f2a16`,两者之间只有文档)。
|
||||
- 分支:`codex/rectification-session-composer-guard-20260917`,`git worktree add -b codex/rectification-session-composer-guard-20260917 .worktrees/rectification-session-composer-guard-20260917 origin/staging`。
|
||||
- 范围:前端 `use-consultation-run.ts`、`use-session-management.ts`、`use-rectification-surface.ts`、`page.tsx`(只允许减行)、`api/consult/route.ts` 的一处错误码,以及对应测试。不动 Python、不动迁移、不动 Skill、不动校正 Agent 与校正面组件。
|
||||
- 串行:与 `TASK-session-list-single-source-20260917.md` 同天改 `use-session-management.ts`。**本单先做**;那一单从本单合入后的 staging 起分支。
|
||||
- BUG 段:**BUG-924 起**(基线 `docs/BUG_HISTORY.md` 最大号 BUG-923,开工时再核对一次)。
|
||||
|
||||
## 1. 事故实证
|
||||
|
||||
产品负责人 2026-09-17 19:1x(Asia/Shanghai)在 staging 桌面 Chrome 上,从一条生时校正会话里输入了一个感情问题,浏览器发出 `POST /api/consult`,服务端返回:
|
||||
|
||||
```json
|
||||
{"error":"咨询会话不存在","message":"请重新进入咨询。"}
|
||||
```
|
||||
|
||||
用产品负责人的登录态只读核对 staging(不含任何身份信息):
|
||||
|
||||
| 项目 | 结果 |
|
||||
| --- | --- |
|
||||
| 请求体 | `sessionId` 指向一条会话,`history: []`,`entryMode: direct_chart`,`consultationMode: verified_chart` |
|
||||
| `GET /api/sessions/<id>` | 200,`session_type = birth_time_rectification`,`rectification_case_id = null`(所有校正会话都是 null,绑定在案例表里,正常),`messages = 0`,标题「9月17日 · 生时校正」 |
|
||||
| `GET /api/rectification/cases/entry-summary` | 有一个 `collecting_evidence` 的可续案例,`lastActivityAt` 与该会话 `updated_at` 同一毫秒 |
|
||||
| `GET /api/rectification/cases/<caseId>?sessionId=<id>` | 200,3 轮(开场 + 一问一答,都在 11:11–11:12 UTC 完成),`next_user_action = ask_method_followup` |
|
||||
| `POST /api/rectification/cases/open`(intent=session) | 200,`disposition = resumed`,Skill 10.0.29,0.9 s |
|
||||
|
||||
结论:校正会话与案例都健康,服务端能正常打开;是前端在这条会话激活时渲染了**普通** `ChatComposer`,把问题发到了普通咨询接口。
|
||||
|
||||
代码定位(按符号):
|
||||
|
||||
- `frontend/src/app/api/consult/route.ts` `POST`:读 `chat_sessions` 后 `if (!chatSession || chatSession.session_type !== "consultation")` 统一回 404「咨询会话不存在」。判断正确,但没有独立错误码,前端无法区分「不存在」与「类型不对」。
|
||||
- `frontend/src/app/page.tsx`:`activeRectificationSession = activeSession?.sessionType === "birth_time_rectification"`;`rectificationSurfaceOpen = activeRectificationSession && activeSession.id === rectificationSessionId`。校正面只在 `rectificationSurfaceOpen && rectificationCaseId` 时挂载;否则渲染普通 `ChatComposer`,其 `inputDisabled` / `submitBlocked` 只看 `rectificationSurfaceOpen`,**不看 `activeRectificationSession`、`rectificationLoading`、`rectificationError`**。
|
||||
- `frontend/src/hooks/use-consultation-run.ts` `send()`:只对 `consultEntrypoint === "birth_time_rectification"`(题目文案像校正交接)转去 `openRectificationFromHomepage`;对 `currentSession.sessionType` **没有任何判断**,直接把 `currentSession.id` 发到 `/api/consult`。
|
||||
- 让「校正会话激活但校正面未开」成立的三条路径:
|
||||
1. `page.tsx` 的自动打开 effect(注释「A rectification session named in `?c=`…」):刷新、深链、从 `/chart` 等次级页侧栏 `Link` 回 `/?c=<校正会话>` 都走它。揭幕由 `bootstrapRevealDelayMs` 最多等 `BOOTSTRAP_PREPARE_TIMEOUT_MS`(4 s);打开校正面要 `open` + `hydrateRectificationCase` 两次往返(本次实测各 0.9 s / 1.3 s),网络稍慢就先揭幕成普通对话,输入框可用。
|
||||
2. 同一个 effect 在 `rectificationError` 非空时直接 `return`,不再重试;`openRectificationCase` 的 `catch` 只 `assignOpenError`,不重置 `activeSessionId`。打开失败一次,普通输入框就永久留在校正会话上。
|
||||
3. `use-session-management.ts` `deleteSession` 的 `fallbackId = nextSessions[0]?.id`、`applySessionPopStateRef` 的 `fallbackId = listed[0]?.id`,以及 `page.tsx` 的 `activeSession = sessions.find(...) ?? sessions[0]`:三处都可能直接落到列表第一条;而 `openRectificationCase` 用 `[merged, ...current]` 把刚打开的校正会话插在第一位。回退时不经过 `selectSession` 的「校正会话先打开再切换」逻辑(BUG-505 的修复只覆盖了 `selectSession`)。
|
||||
|
||||
Bug 历史检索:BUG-505(进校正面先闪普通对话)修的是 `selectSession` 一条路径,防复发措施仍在;本单是同类漏洞的另外三条路径,**不是复发**,但关联 BUG-505。BUG-621(历史校正打不开)已排除:`open` 实测 200。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
普通咨询的发送链路以「校正面没开」推断「当前是普通会话」,而「校正面没开」还有三种别的含义(正在打开、打开失败、回退落上去的)。会话类型这个事实在前端从未作为发送前置条件检查,服务端又只用一句「咨询会话不存在」兜底,用户看到的提示与实际状态无关。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
- 产品负责人 2026-09-17 授权出本单(「可以」)。
|
||||
- 口径:**校正会话上永远不出现普通输入框的可用态**。用户在校正会话上打的字,只能进校正面的输入框;校正面还没开时,普通输入框只能是「正在打开生时校正」的禁用态,打开失败时给一个「重新打开」动作。
|
||||
- 不推翻 BUG-505 的一次揭幕合同(`hydrateRectificationCase` 4 s 上限、面板一个会话只挂一次);不推翻 AGENTS.md §6「揭幕后不得出现 spinner / 骨架 / 正在加载」——禁用态占位文案不算加载动画,但不得加 spinner。
|
||||
- 不改校正面组件、不改校正 Agent、不改 Skill。
|
||||
- `page.tsx` 不得增长;新逻辑进 hook 或 lib。
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. `send()` 对 `sessionType === "birth_time_rectification"` 的会话不得发出 `/api/consult`,无论入口(表单提交、`onFollowUp`、排队草稿、刷新恢复重放)。
|
||||
2. 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
|
||||
3. 不顺手改会话列表排序、标题、缓存(那是另一单)。
|
||||
4. Bug 历史与进度记录不得写入会话 ID、案例 ID、用户 ID、出生资料、Cookie。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
### T1 `send()` 加会话类型守卫(BUG-924)
|
||||
|
||||
- `use-consultation-run.ts` `send()`:在 `if (!question || !currentSession …) return false` 之后、扣点与撤回窗口之前,若 `currentSession.sessionType === "birth_time_rectification"`:不发 `/api/consult`,把用户输入保留在草稿(`setDraft(originalQuestion)`,不清空),调用 `openRectificationSession(currentSession.id)`,`return false`。
|
||||
- 刷新恢复重放(`restoreConsultationRecovery` → `send(..., { resumeRequestId })`)同样受守卫约束;pending 存储里若指向校正会话,直接清掉并不重放。
|
||||
- 验收:
|
||||
- 新测试(`frontend/tests/` 下按既有 hook 合同测试的写法):给定 active 会话为校正类型、校正面未开,提交一条普通问题 → `fetch` 未被调用到 `/api/consult`,`openRectificationSession` 被调用且参数为该会话 id,草稿仍是原文。
|
||||
- 既有 `rectification-surface-contract`、`chat-session-authority` 全绿。
|
||||
|
||||
### T2 普通输入框在校正会话上只有禁用态(BUG-924)
|
||||
|
||||
- `page.tsx` 的 `ChatComposer`:`inputDisabled` 与 `submitBlocked` 增加 `activeRectificationSession` 条件;`placeholder` 在该状态下为「正在打开生时校正…」;`rectificationError` 非空且属于当前会话时,`ChatComposer` 上方(沿用现有 `error-message` 位置)显示错误文案与一个「重新打开生时校正」按钮,点击调用 `openRectificationSession(activeSession.id)`。这些派生值请放进现有 hook(`use-rectification-surface.ts` 或 `use-session-management.ts`)返回,`page.tsx` 只消费。
|
||||
- 自动打开 effect:`rectificationError` 非空时仍不自动重试(避免循环),但重新打开按钮点击后先清 `rectificationError`。
|
||||
- 验收:
|
||||
- `home-bootstrap-reveal` 或 `rectification-surface-contract` 加断言:prepare 超时揭幕、校正面未开时,composer `disabled` 为真、placeholder 为「正在打开生时校正…」;`rectificationError` 状态下出现「重新打开生时校正」。
|
||||
- `frontend/DESIGN.md` 侧栏/输入框合同补一句:校正会话上普通输入框只有禁用态。
|
||||
|
||||
### T3 三条回退路径统一走 `selectSession`(BUG-924)
|
||||
|
||||
- `deleteSession`、`applySessionPopStateRef`(两处 `fallbackId`)、以及 `activeSession ?? sessions[0]` 的兜底:回退目标若是校正会话,必须经 `selectSession` 的延迟切换(先 `openRectificationSession` 再 `setActiveSessionId`);不得直接 `setActiveSessionId` 到校正会话。`sessions[0]` 的兜底改为「第一条非校正会话,没有则空」。
|
||||
- 验收:新测试覆盖「删除当前会话、列表第一条是校正会话」与「popstate 回到无参 URL、列表第一条是校正会话」两个场景:`activeSessionId` 不直接变成校正会话 id,`openRectificationSession` 被调用。
|
||||
|
||||
### T4 服务端错误码与前端映射(BUG-925)
|
||||
|
||||
- `api/consult/route.ts`:`session_type !== "consultation"` 时返回 `409 { error: "这是生时校正会话", code: "session_not_consultation", message: "请在生时校正里继续。" }`;真正查不到仍是 404「咨询会话不存在」。`/api/consult/cancel`、status 若有同样判断一并对齐。
|
||||
- `use-consultation-run.ts`:收到 `session_not_consultation` 时不显示错误,不扣点(本来就在预留前),走 T1 同一条打开链路。
|
||||
- 验收:`chat-session-authority` 加一条:校正会话 POST `/api/consult` → 409 + `session_not_consultation`;普通不存在 id → 404。
|
||||
|
||||
### T5 记录
|
||||
|
||||
- `docs/BUG_HISTORY.md` 新增 BUG-924、BUG-925,关联 BUG-505;`CHANGELOG.md` 一句「校正会话上不再出现可用的普通输入框」;`docs/tasks/PROGRESS-rectification-session-composer-guard-20260917.md`;`docs/tasks/README.md` 状态板行改状态。
|
||||
- `docs/testing/` 加真机条目:① 在校正会话里刷新,揭幕后立刻打字 → 输入框禁用、几秒后校正面出现、草稿不丢;② 断网状态下点侧栏校正会话 → 错误文案 + 重新打开按钮,联网后点按钮可开;③ 删除当前普通会话且列表第一条是校正会话 → 不出现普通输入框。
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
T1 > T4 > T2 > T3。T1 一项就能挡住误发;T3 若牵动 `use-session-management.ts` 过大,可只做 `deleteSession` 与 `sessions[0]` 兜底,`popstate` 写进 `BLOCKED.md`。
|
||||
|
||||
## 7. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/rectification-session-composer-guard-20260917 .worktrees/rectification-session-composer-guard-20260917 origin/staging
|
||||
cd .worktrees/rectification-session-composer-guard-20260917/frontend
|
||||
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 # 记下基线失败清单
|
||||
grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1 # 核对 BUG 起点
|
||||
```
|
||||
|
||||
## 8. 验收口径
|
||||
|
||||
`tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败清单与基线逐条一致、新增测试全绿;`next build` 后 `/` 仍 Static;首屏 gzip ±2%;`page.tsx` 行数 ≤ 开工时。无 Docker 的 DB 套件失败照基线记录。
|
||||
@@ -0,0 +1,123 @@
|
||||
# 任务书 · 会话列表一处数据源、服务端排序修正、空会话不入列、标题规则重定(2026-09-17)
|
||||
|
||||
## 0. 基线
|
||||
|
||||
- 基线 commit:`eea90926`(`origin/staging` head)。**串行在 `TASK-rectification-session-composer-guard-20260917.md` 之后**:那一单改 `use-session-management.ts` 的回退路径,本单从它合入后的 staging 起分支,开工时以当时的 `origin/staging` 为准。
|
||||
- 分支:`codex/session-list-single-source-20260917`,`git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging`。
|
||||
- 范围:`frontend/src/lib/db/local-postgres-client-core.ts`(排序)、`api/sessions` 两个路由、`lib/sidebar-data-cache.ts` / `hooks/use-sidebar-data.ts` / `hooks/use-session-management.ts` / `app/(secondary)/layout.tsx` / `components/app-sidebar.tsx` / `components/sidebar-session-row.tsx`、`lib/agent-reply.ts` 的标题函数、`lib/home-profile.ts` 的侧栏标题/副标题、`lib/session-groups.ts`,以及 `page.tsx`(只允许减行)。不动 Python、不动 Skill。**T3 若选服务端过滤方案不动表结构**;若产品另批清理迁移再动表并真跑 `npm run test:db`。
|
||||
- BUG 段:**BUG-926 起**(上一单占 924–925;开工时核对 `docs/BUG_HISTORY.md` 最大号)。
|
||||
|
||||
## 1. 事故实证
|
||||
|
||||
产品负责人 2026-09-17 反馈:`/`(新建对话)、`/chart`、`/ephemeris`、`/reports` 四处的侧栏会话列表不是同一份,每换一页都重新读一次;列表内容还一直在变;排序和标题规则看不出哪个是哪个。
|
||||
|
||||
用产品负责人登录态只读核对 staging(`GET /api/sessions?limit=50`,不含身份信息):
|
||||
|
||||
| 项目 | 结果 |
|
||||
| --- | --- |
|
||||
| 返回顺序 | **严格按 `id` 降序**,`updated_at` 不单调 |
|
||||
| `nextCursor` | `2026-09-03T07:09:23Z,<id>`,按 `updated_at` 构造 |
|
||||
| 首页 50 条里最新的 `updated_at` | 2026-09-17 01:35Z;当天 11:12Z 新建的校正会话**不在首页** |
|
||||
| 首页 50 条标题分布 | 「新对话」28 条、「生时校正」4 条、「9月8日 · 生时校正」2 条、「9月14日 · 生时校正」2 条、其余各 1 |
|
||||
| 各接口耗时 | sessions 0.83 s、account 0.87 s |
|
||||
|
||||
代码定位(按符号):
|
||||
|
||||
- **服务端排序丢了第一键。** `frontend/src/lib/db/local-postgres-client-core.ts` 的 `order(column, options)` 写 `this.ordering = { column, ascending }`,只保存**最后一次**调用;SQL 生成处 `if (this.ordering) sql += " order by …"` 只有一个键。`api/sessions/route.ts` `GET` 链式调用 `.order("updated_at", desc).order("id", desc)`,实际只按 `id` 排。游标 `sessionCursorFilter` 却按 `updated_at, id` 过滤,于是首页不是最近 40 条、翻页与首页重叠或漏掉、每次进入看到的集合都不同。同一缺陷还吃掉了 `api/payment/packages/route.ts` 的 `.order("sort_order").order("created_at")`,套餐顺序变成按创建时间。
|
||||
- **两份数据源。** `/` 由 `page.tsx` 启动 effect 自己拉 `fetchSessions()`(默认 40 条)+ 详情,存进 `Home()` 的 `sessions` state,再经 `visibleSessions`(过滤掉空的普通会话)→ `sidebarSessions` 喂给侧栏;`(secondary)/layout.tsx` 用 `useSidebarData()` 另拉 `/api/sessions?limit=40` + `/api/account`,进 `sidebar-data-cache.ts` 的 60 s 模块缓存,**不做空会话过滤**。`/` 在 `app/` 里不在 `(secondary)` 路由组,离开 `/` 即卸载 `Home()`,回来重跑整个启动(账户、目录、列表、详情、entry-summary、今日星语);`invalidateSidebarCache()` 在 `/` 的每次写之后清空次级页缓存。BUG-745 修掉的是次级页之间的重挂,`/` ↔ 次级页之间没有修。
|
||||
- **空会话堆积。** `page.tsx` 启动时 `starterHomeLandingNeedsConsultation` 为真(落点缺失或是校正会话)就 `createSession` + `writeChatSession(create)` 立即落库;`startNewChat` 也是点了就落库。用户从没发过问题的「新对话」因此在库里累积(首页 50 条里 28 条),`/` 用 `visibleSessions` 把它们藏起来,次级页原样显示,两边列表自然不一样。
|
||||
- **标题规则。** `resolveSessionTitle`:普通会话在第一次回答前叫「新对话」,之后取模型标题或问题前 14 字;校正/今日节奏用 `datedSessionTitle` 生成「M月D日 · 生时校正」;同名靠 `uniquifySessionTitle` 追加「HH:MM」(**新建时的墙钟**,不是会话时间)。旧数据还有无日期的「生时校正」。侧栏行只有标题一行,`/` 的副标题只在他人盘时显示,次级页根本没有副标题字段。结果是一列「新对话 / 9月14日 · 生时校正 / 9月14日 · 生时校正 02:14」。
|
||||
- 服务端 `updated_at` 口径(BUG-553:只由对话活动推进,元数据 PATCH 不写)**仍在**,不是本单问题;客户端 `sortSessions` 置顶 + `updatedAt` 倒序也正确,只是服务端给的集合本身不对。
|
||||
|
||||
Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0 行)、BUG-699(打开旧校正会话改今天标题)、BUG-744~746(侧栏统一、次级页重挂、折叠状态)防复发措施均在,本单四条都是新问题;BUG-745 的「每跳一次重拉列表」在 `/` ↔ 次级页方向属**未覆盖**,关联但非复发。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
1. 本地 PostgreSQL 兼容层的 `order()` 不支持多键,服务端列表从来没有按最近活动排过;游标分页因此与排序不一致。
|
||||
2. 会话列表没有一个跨路由的所有者:`Home()` 与 `(secondary)` 各拉各的,`/` 又是每次进入从零启动。
|
||||
3. 会话在用户还没说一句话时就落库,而两处列表对空会话的处理不一致。
|
||||
4. 标题只承载「叫什么」,不承载「哪一类、哪一天、谁的盘」,同名去重又用墙钟时间。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
- 产品负责人 2026-09-17 授权:列表必须共用一份;排序与标题规则重构。以下具体口径由 Claude 定,产品负责人如不同意在验收时改:
|
||||
- **排序**:置顶在前;组内按 `updated_at` 倒序;`updated_at` 仍只由对话活动推进(BUG-553 不变)。服务端与客户端同一口径。
|
||||
- **哪些会话入列**:只列**有内容**的会话——普通会话至少一条消息;校正会话一律入列(它一打开就有开场轮)。当前正在用的空会话只在 `/` 本地显示,不入服务端列表。
|
||||
- **标题**:普通会话 = 模型标题,否则第一问前 14 字;第一次回答落库前显示「新对话」但不入列。校正会话 = 「生时校正 · M月D日」;今日节奏 = 「今日节奏 · M月D日」——**类别在前,日期在后**,扫一眼能分类。同日多条不再加墙钟「HH:MM」,改为副标题区分。
|
||||
- **副标题**(两处侧栏都显示一行小字):`M月D日 HH:MM`(会话创建时间,Asia/Shanghai)+「 · <盘主名>」(仅他人盘)。校正会话再补状态词:进行中 / 已交付 / 已结束(从 entry-summary 或列表字段取;取不到就不显示,不得猜)。
|
||||
- **存量数据**:旧标题不批量改(BUG-699 已做过一次校正会话标题修复,且手改标题不能动);只对未来新建生效。空会话**不删**,只是不入列;若产品负责人另批清理,另开迁移单。
|
||||
- 不推翻 BUG-553 / BUG-699 / BUG-745 的任何防复发措施。
|
||||
- `page.tsx` 不得增长;`Home()` 拥有的乐观更新与回滚层保留,只是数据源换成共享 store。
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. 兼容层 `order()` 改多键后,所有既有单键调用行为不变;有 SQL 文本级测试。
|
||||
2. 服务端列表排序键与 `sessionCursorFilter` 的键必须一致(`updated_at desc, id desc`),有测试锁住。
|
||||
3. 元数据 PATCH 仍不写 `updated_at`(BUG-553);打开已有会话不改 `title` / `updatedAt`(BUG-699,`session-open-preserves-identity.test.ts` 必须保持绿)。
|
||||
4. 次级页不得再挂第二份 `SidebarProvider`(BUG-745);侧栏源码不得出现 `window.location`。
|
||||
5. 既有测试断言不得静默弱化;改断言写「原值 / 新值 / 原因」三栏。测试总数不低于开工时。
|
||||
6. 无 Docker 时 `npm run test:db` 失败照基线记录,不得写成通过。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
### T1 兼容层多键排序 + 列表接口排序契约(BUG-926)
|
||||
|
||||
- `local-postgres-client-core.ts`:`ordering` 改为数组,`order()` 追加;SQL 生成 `order by a desc, b desc`。
|
||||
- `api/sessions/route.ts` 两处查询(分页与置顶)保持 `.order("updated_at", desc).order("id", desc)`;`api/payment/packages/route.ts` 不改代码,验证顺序恢复为 `sort_order, created_at`。
|
||||
- 验收:
|
||||
- `frontend/tests/local-postgres-query-value.test.ts`(或新建 `local-postgres-order.test.ts`):两次 `order()` 生成的 SQL 含 `order by "updated_at" desc, "id" desc`;单次 `order()` 输出不变。
|
||||
- `frontend/tests/session-cursor.test.ts` 或 `chat-session-authority.test.ts` 加断言:列表路由源码的排序键序列与 `sessionCursorFilter` 的键一致。
|
||||
- 有 Docker 时 `npm run test:db` 加一条:插入三条不同 `updated_at` 的会话,`GET /api/sessions?limit=2` 返回最新两条且 `nextCursor` 翻页拿到第三条、无重叠。无 Docker 写 `BLOCKED.md`。
|
||||
|
||||
### T2 一份会话列表 store,`/` 与次级页共用(BUG-927)
|
||||
|
||||
- 新建 `frontend/src/lib/session-list-store.ts`:模块级 store(`useSyncExternalStore`),按账户 id 分区,保存列表行、游标、账户摘要、`fetchedAt`;提供 `readSessionList()`、`subscribe`、`hydrateFromServer()`、`applyLocalWrite(op)`(create / rename / pin / archive / delete / activityBump)与 `revalidate()`(后台重拉,先渲染现有行)。`sidebar-data-cache.ts` 并入或删除,不留两份。
|
||||
- `Home()` 启动:若 store 已有本账户列表,**先用它**渲染侧栏与落点选择,再后台 `revalidate()`;没有才拉。`Home()` 现有的 `sessions` state 继续作为详情(消息)的所有者,但列表行的来源与写操作统一通过 store;`updateSession` / `persistSession` 成功后同步 `applyLocalWrite`。
|
||||
- `(secondary)/layout.tsx` 的 `useSidebarData()` 改读同一 store;不再自己发 `/api/sessions`,除非 store 为空或过期(60 s 保留)。
|
||||
- `/` ↔ 次级页往返不得再触发 `/api/sessions` 与 `/api/account`(60 s 内);`invalidateSidebarCache()` 改为 `applyLocalWrite`,不清空整份。
|
||||
- 验收:
|
||||
- `sidebar-data-cache.test.ts` 改为 store 测试:写入后两处读同一份;rename 后次级页立刻是新标题;60 s 内 `revalidate()` 不发请求;账户切换隔离。
|
||||
- `sidebar-contract.test.ts` / `home-bootstrap-reveal.test.ts`:`Home()` 有缓存时启动不发 `/api/sessions`(mock fetch 计数为 0),有缓存也不阻塞揭幕。
|
||||
- 真机清单(`docs/testing/`):DevTools Network 观察 `/` → `/chart` → `/ephemeris` → `/reports` → `/` 全程 `/api/sessions` 只出现一次;改名后各页标题一致。
|
||||
|
||||
### T3 空会话不入列,且延迟落库(BUG-928)
|
||||
|
||||
- 服务端:`api/sessions` `GET` 排除 `session_type = 'consultation' AND messages = '[]'`(保留校正会话);置顶查询同样处理。`GET /api/sessions/<id>` 不变(详情仍可读)。
|
||||
- 客户端:`startNewChat` 与启动时的落点会话改为**本地创建、不立即落库**,`ChatSession` 加 `persisted: boolean`;`send()` 在撤回窗口结束、`POST /api/consult` 之前若 `!persisted` 先 `writeChatSession(create)`(失败则回滚并提示,沿用 `startNewChat` 现有回滚文案)。校正会话仍由 `/cases/open` 服务端创建,不受影响。
|
||||
- `visibleSessions` 的过滤保留(当前空会话只在 `/` 本地可见),次级页因服务端已过滤而自然一致。
|
||||
- 让步:若延迟落库牵动 `?c=` 深链、刷新恢复(`pendingConsultationStorageKey`)过多,可只做服务端过滤 + 启动时**复用**已有空会话而不再新建,把延迟落库写进 `BLOCKED.md`。
|
||||
- 验收:
|
||||
- `chat-session-authority.test.ts`:列表路由过滤条件存在;详情路由不过滤。
|
||||
- 新测试:连点三次「新建对话」不发 `POST /api/sessions`;发出第一问前恰好一次 `POST /api/sessions`;`?c=<本地未落库 id>` 刷新后不报「会话不存在」而是回到落点。
|
||||
- 有 Docker:`test:db` 加一条空会话不出现在列表。
|
||||
|
||||
### T4 标题与副标题规则重定(BUG-929)
|
||||
|
||||
- `agent-reply.ts`:`datedSessionTitle` 改为「<类别> · M月D日」;`uniquifySessionTitle` 删除(同名允许,靠副标题区分);`isGenericSessionTitle` 的前缀正则同步改成类别在前,并兼容旧「M月D日 · 生时校正」格式(旧标题仍判为 generic,行为不变)。
|
||||
- 列表接口增加 `created_at`(表里已有;只是没选出来)。`toSidebarSession` 与 `sidebarSessions` 共用一个 `lib/session-sidebar-row.ts`:`title`、`subtitle = 「M月D日 HH:MM」+「 · 盘主」`、校正状态词。`sidebar-session-row.tsx` 副标题已支持,无需新组件;两处侧栏必须走同一个映射函数。
|
||||
- 分组(今天 / 昨天 / 最近 7 天 / 30 天 / 更早)保留,按 `updated_at`。
|
||||
- `frontend/DESIGN.md` Sidebar shell 一节写明标题与副标题规则;`frontend/docs/VOICE.md` 对照「生时校正 · 」「今日节奏 · 」措辞。
|
||||
- 验收:`agent-reply.test.ts` 更新(写三栏);新测试锁映射函数:同一行输入在 `/` 与次级页产出相同 title/subtitle;`session-open-preserves-identity.test.ts` 保持绿。
|
||||
|
||||
### T5 记录
|
||||
|
||||
- `docs/BUG_HISTORY.md` 新增 BUG-926~929(926 关联 packages 路由;927 关联 BUG-745;928 关联 BUG-553;929 关联 BUG-699);`CHANGELOG.md`;`docs/tasks/PROGRESS-session-list-single-source-20260917.md`;`docs/tasks/README.md` 状态板行;`BLOCKED.md`(无 Docker 项)。
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
T1 > T2 > T3(服务端过滤部分)> T4 > T3(延迟落库部分)。T1 一项独立可发,若其余来不及,T1 单独推 staging 也值得。
|
||||
|
||||
## 7. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git log -1 --format='%h %s' origin/staging # 确认上一单已合入
|
||||
git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging
|
||||
cd .worktrees/session-list-single-source-20260917/frontend
|
||||
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 # 记下基线失败清单与测试总数
|
||||
grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1
|
||||
```
|
||||
|
||||
## 8. 验收口径
|
||||
|
||||
`tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build` 后 `/` 仍 Static;首屏 gzip ±2%;`page.tsx` 行数 ≤ 开工时;有 Docker 则 `npm run test:db` 通过,无 Docker 写 `BLOCKED.md`。部署后用 DevTools 复核 T2 的「一次 `/api/sessions`」与 `GET /api/sessions` 的 `updated_at` 单调递减。
|
||||
Reference in New Issue
Block a user