fix(chat): make the server the only writer of session messages
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

List GET no longer ships transcripts; consult appends questions after reserve and ignores client history so dual-tab last-write-wins cannot erase messages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-01 19:51:13 +08:00
parent b98777aa5c
commit b6989c3eea
23 changed files with 871 additions and 150 deletions
+22 -1
View File
@@ -1069,4 +1069,25 @@
- 建议在数据库 migration 成功后运行独立的环境 readiness 脚本,不放在应用启动期,避免每个实例重复探测或因业务配置缺失阻止无关只读能力启动。
- readiness 应枚举当前环境所有已发布模型的 `model_tier`,逐一检查 `rectification``chat.standard``report.full` 三个 feature 是否存在 `status='published' AND enabled=true` 的定价;任何组合缺失即以脱敏、可行动的运维错误 fail closed。
- readiness 只负责验证,不写入或 seed 价格;部署后仍必须通过 admin flow 为实际 tier 发布定价,随后再重跑 readiness。
- 本地验证:TypeScript、改动文件 ESLint、rectification `705/705`、consult/report 聚焦 `262/262` 通过;全量 `2368/2369`,唯一失败是默认 `python3` 缺少 PyYAML,改用已安装 PyYAML 的 `/opt/homebrew/bin/python3` 后对应文件 `39/39` 通过。
- 本地验证:
- `./node_modules/.bin/tsc --noEmit` 通过。
- 非数据库前端测试 **2377 通过 / fail=0 / skipped=0**
- `npm run test:db -- --test-concurrency=1`**34 通过 / fail=0 / skipped=0**(含 `append_consultation_question` 幂等、双 request_id 叠加、`session_full`)。
- `npm test` 会并行拉起多个 Postgres fixture,本机 Docker address pool 只能同时容纳 2 个 compose network,并行时会超时,不是本轮断言回归。验收用上面两项拆开跑。
- 浅色/深色:加载态 `.session-messages-loading` 使用 `color: var(--color-ink-secondary)`spinner 的 `currentColor` 跟随该 token(浅色 `#5f5f59`,深色 `#b3afa4`)。`session_full` 走 sonner toast,背景/文字用 `--color-canvas` / `--color-ink`,错误语气由「已写满」命中 `failedNotice`。未登录所以没有做带真实会话的页面点击;主题检查落在 token 与合同测试上。
## 2026-09-01 - TASK-chat-message-authority:服务端收回 messages 写权
- 列表 `GET /api/sessions` 不再选择 `messages`;新增 `GET /api/sessions/[id]` 按会话加载完整消息。前端启动只拉列表,切换时拉详情,已加载的会话内存缓存,正在 streaming 的会话不重拉。
- `append_consultation_question` 在 reserve 成功后写入用户提问;满员返回 `session_full` 并取消预扣。consult 忽略客户端 `history`,改读库内最近 12 条。
- `persistSession` 只 PATCH 元数据。旧全量 `messages` PATCH 返回 200 且不覆盖库。中止/失败部分回答不再落库。
- 断言例外(锁住的正是本轮要修的缺陷):
- `consultation-recovery.test.ts`:不再要求发问前 `persistSession(userSession)`,也不再要求中止/截断路径持久化部分回答;完成恢复改拉详情而不是整表列表。
- `chat-session-write.test.ts`create schema `messages.max(0)`update 请求体不含 `messages`;页面不再把 thinkingText 映射进 PATCH。
- `application-billing-contract.test.ts` / `rectification-agentic-entry.test.ts`:校正 agent select 去掉未使用的 `messages`consult select 增加 `messages` 以便服务端读历史。
- `consultation-stream-recovery.test.ts` / `consultation-workflow-contract.test.ts`agentic 函数切片终点从 `parsed.data.history` 改为 `storedHistory`
- `chat-notice-and-scroll-contract.test.ts`:toast 支持一键开新对话的 action。
- 实测:
- Payload:列表响应列集合为 `id,title,theme,model_id,session_type,rectification_case_id,chart_profile_* ,updated_at`,消息文本只能出现在详情 GET。多会话账户的字节对比取决于该账户存量消息,合同测试锁的是“列表不含消息文本”这一结构,而不是某一个登录态的绝对字节数。
- `session_full`:本地 PostgreSQL 把会话填到 200 条后再 append,返回 `error_code=session_full`,行内消息数仍为 200。
- 双标签页:同一会话两次 `append_consultation_question`(不同 request_id)后两条用户消息都在;这是改前 last-write-wins 必丢、改后必留的核心形状。