Files
2026Technology-Competition/docs/specs/2026-08-28-chat-v4-deepseek-session-design.md
T

246 lines
13 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.
# 概要设计书生成 Agent — 聊天前端 v4(DeepSeek 模式会话)设计文档
- 文档类型:架构/功能设计(Superpowers brainstorming 产物)
- 创建日期:2026-08-28
- 状态:设计已确认,待实现
- 范式步骤:架构设计 → Agent 实现(本轮仅前端聊天 + 后端会话端点微调)
- 关联文档:`docs/design-web-chat-v3.md`(前一轮 v3 设计)
- 不在本轮范围:RAG 化(项目配置目录预存向量库 + 检索)— 列为下一轮独立子项目
---
## 0. 背景与目标
v3 已实现"Web 服务化 + 聊天式交互 + 项目级配置 + 前端美化"。但在前端页面(chat.html)与后端实现之间存在若干不整合点,且会话交互模型与常见产品(DeepSeek 式"进入即空白、首条消息才落库")有体验落差。
本轮目标:
1. **会话生命周期改为 DeepSeek 模式**:进入页面为空白草稿;首次用户动作(消息)才创建会话并落库;顶栏切换项目仅影响"下一次新建会话",不改动已存在会话。
2. **修复 5 条高严重/低风险的不整合点**:A1(删会话不级联消息)、B5(上传后状态不同步)、C1(Esc 关闭后焦点不还原)、B6(抽屉未保存变更静默丢失)、F1(上传失败清空文件输入)。
3. 维持 Python 测试 `>=99%` 覆盖率门槛与提交规范(ASCII 文件名、无硬编码绝对路径、.env 不入库等)。
---
## 1. 业务语义(核心契约)
### 1.1 会话生命周期 v4
| 阶段 | 触发 | 后端动作 | 前端动作 |
|---|---|---|---|
| 进入空白态 | 启动 / 刷新 / 顶栏切项目 / 点"+ 新会话" | 不调 | 欢迎语(或空态提示)+ 空白聊天;`localStorage` 不写;侧边栏显示**当前 draftProject 下的历史会话** |
| 切项目 | 顶栏下拉选项目 | 不调 | 重置聊天区为空白;展示欢迎语或空态提示;`draftProject` 更新;侧边栏列表刷新(按新项目过滤);`sid=null`、清 `localStorage` |
| 首条消息 | 用户在空白态发首条消息 | `POST /api/sessions`(带 `draftProject`)→ 拿 `sid` | 写 `localStorage`;**该会话才进入侧边栏历史**;随后 `POST /api/chat/{sid}/messages` |
| 继续对话 | 已有 `sid` 的会话中发消息 | `POST /api/chat/{sid}/messages` | 正常聊天 |
| 新建空白 | 点"+ 新会话" | 不调 | 清空聊天区 + 欢迎语;`sid=null`、清 `localStorage`**顶栏项目不重置**(保持 `draftProject` |
| 删除会话 | hover 会话 × → confirm | `DELETE /api/sessions/{sid}`(级联 `chat_messages` | 删的是当前 `sid` → 进入空白态;删的不是当前 → 仅刷侧边栏 |
### 1.2 项目归属
- `SessionRecord.project` 在**首条消息时落库**,之后 locked(不可改)。
- `draftProject`(前 `currentProject`):仅作用于下一次空白态→首消息时绑定的项目。
- 切换 `draftProject` **不影响**已存在的会话(包括当前已问答的 `sid`)。
### 1.3 空态提示文案(固定)
-`draftProject``null`("未选择项目")且处于空白态时,内容区**不展示欢迎语**,改为展示固定文案:
> **请新建项目或者选择项目**
-`draftProject` 为具体项目时,展示欢迎语(沿用 v3 文案)。
- 侧边栏在 `draftProject=null` 时无历史会话(后端 `GET /api/sessions` 不传 `project`v4 语义返回空)。
### 1.4 兼容 v3 行为
- 已存在的 v3 `localStorage['genesis_session']` 值:启动时**仍尝试加载**;如该 `sid` 不存在则忽略并进入空白态;如存在则进入"已问答态"。
- 旧 v3 `currentProject` 命名:本次重命名为 `draftProject`(更精确),不与"已问答会话的 `activeProject`"混淆。
### 1.5 上传与发消息的约束(D-v4 裁定)
- **允许**:只发消息不上传(纯对话 / 让后端返回需要件定义提示)。
- **允许**:上传 + 发消息(先发消息建会话 → 上传要件 → 再发"生成")。
- **禁止**:只上传不发消息。
- 实现:空白草稿态(`sid===null`)下点击上传 → 不执行上传,提示「请先发送一条消息以创建会话」并聚焦输入框;`sid` 存在后上传照常。
---
## 2. 后端变更
### 2.1 端点改动一览
| 端点 | 现状 | v4 变更 | 原因 |
|---|---|---|---|
| `POST /api/sessions` | body `name`/`project` | 不变 | 仍接收 project;前端仅在首条消息时调用 |
| `GET /api/sessions?user_id=` | 无 project 过滤 | **新增** `?project=` 可选参数 | 侧边栏按项目过滤 |
| `GET /api/sessions/{sid}` | 完整 record | 不变 | 加载时仍可用 |
| `DELETE /api/sessions/{sid}` | 只删 sessions 行 | **级联**删 `chat_messages`A1 | 防数据泄漏 |
| `POST /api/sessions/{sid}/files` 等其余端点 | — | **0 改动** | 不在本轮 spec 范围 |
### 2.2 `GET /api/sessions?project=` 过滤
`store.py` `list_sessions` 改为支持可选 `project` 参数:
- 现状:`SELECT data FROM sessions WHERE user_id = ?`
- 改为:`WHERE user_id = ?` + 可选 `AND json_extract(data, '$.project') = ?`
- `data` 字段是 JSON 字符串(`store.py` 写入整条 JSON)。SQLite 3.38+ 支持 `json_extract`
- **Fallback 策略**:启动时一次性探测 `SELECT json_extract('{"a":1}', '$.a')`;若抛错则走 Python 端过滤(先取全部 `user_id` 行,`json.loads` 比对 `project` 字段)。一次只几条~几十条,无性能问题。
- 默认排序(`updated_at desc`)保留。
- `project` 为空字符串(`''``None`)的处理:v4 语义下,前端在 `draftProject=null` 时**不传 `project` 参数**,后端按"查全部但仅返 `project` 为空的历史遗留会话"实现——实际即 `project IS NULL OR json_extract(...) = ''`。此分支仅在兼容 legacy 时命中,新 UI 不主动引导。
### 2.3 `DELETE` 级联(A1
`store.py` `delete_session` 改为单连接事务内两步:
```python
def delete_session(self, session_id: str) -> bool:
with self._conn() as c:
c.execute("DELETE FROM chat_messages WHERE session_id = ?", (session_id,))
cur = c.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
return cur.rowcount > 0
```
- 单个 `with self._conn() as c:` 上下文内执行两条 SQL,保证原子性。
- 不引入 `FOREIGN KEY ... ON DELETE CASCADE`:历史数据无 FK,引入需迁移;应用层手动级联已足够。
- 文档标注"删除会话必须经由 `delete_session`"。
### 2.4 `app.py` 改动
```python
@app.get("/api/sessions")
def list_sessions(user_id: str = "default", project: str | None = None):
return [
{"session_id": r.session_id, "name": r.name, "project": r.project, ...}
for r in service.store.list_sessions(user_id, project=project)
]
```
- `project` 为空(`None`)时不加过滤条件(兼容 legacy;v4 前端在 `draftProject=null` 时本就不传,返回空历史)。
- `project` 为具体值(如 `stock`)时,后端走 `json_extract` 或 Python fallback 过滤。
### 2.5 测试(后端,Python
| 用例 | 断言 |
|---|---|
| `test_delete_session_cascades_messages` | 创建会话→发 2 条消息→删会话→`list_messages` 返空 |
| `test_list_sessions_filter_by_project` | 创建项目 A、B 下各一会话→`?project=stock` 只返 stock 那条 |
| `test_list_sessions_no_project_returns_empty_for_v4` | `draftProject=null`(不传 project)→ 返空(v4 语义) |
| `test_list_sessions_python_fallback` | mock `json_extract` 抛错→走 Python 过滤分支仍正确 |
| 旧 `test_delete_session` / `test_list_sessions` | 仍绿 |
---
## 3. 前端状态机(chat.html v4
### 3.1 状态变量(重命名 + 新增)
| 变量 | 含义 | 变更 |
|---|---|---|
| `draftProject` | 下一空白会话将绑定/已绑的项目 | **重命名**(原 `currentProject` |
| `activeProject` | 当前已落库会话绑定的项目(来自 `GET /api/sessions/{sid}` | 不变 |
| `sid` | 当前会话 id`null` = 空白草稿态 | 不变;新增"`null` 时不写 localStorage" |
| `lastTriggerEl` | 打开抽屉/下拉前的 `document.activeElement` | **新增**C1 用) |
| `drawerDirty` | 抽屉表单相对初始快照是否改动 | **新增**B6 用) |
| `drawerSnapshot` | `loadDrawerForm` 时的字段快照 | **新增**B6 用) |
### 3.2 关键流程
**启动**
1. `loadProjects()``draftProject = 首项或 null`
2.`localStorage['genesis_session']` 存在且 `GET /api/sessions/{sid}` 成功 → 进入"已问答态"(加载消息、设 `sid/activeProject`)。
3. 否则进入**空白草稿态**:清空聊天区,按 `draftProject` 决定展示:
- 有值 → 欢迎语;
- `null` → 空态提示文案「请新建项目或者选择项目」(不放欢迎语)。
**顶栏切换项目(核心)**
- 更新 `draftProject`,顶栏名同步。
- 重置聊天区为空白草稿态(欢迎语或空态提示),`sid = null``localStorage``genesis_session`
- **不调任何后端**(静默)。
- 侧边栏按新 `draftProject` 刷新(`GET /api/sessions?project=`)。
- 原会话(无论是否已问答)保留在历史中,仅被过滤隐藏。
**「+ 新会话」按钮**
- 同"顶栏切换"的"重置"动作,但**不改 `draftProject`**(保持当前项目)。
- `sid=null`、清 `localStorage`
**首条消息 → 会话落库(D-v4)**
- `send()`:若 `sid === null`
1. `POST /api/sessions`(带 `{user_id, project: draftProject || null}`)→ 拿 `sid`/`name`/`activeProject`
2.`localStorage['genesis_session'] = sid`
3.`POST /api/chat/{sid}/messages`(发送该首条内容)。
-`sid !== null`:正常 `POST /api/chat/{sid}/messages`
**上传(沿用 v3 强制 requirements 逻辑)**
- `ft = activeProject || draftProject ? 'requirements' : 用户选`
-`sid === null` → 不执行上传,提示「请先发送一条消息以创建会话」并 `inputEl.focus()`
- 上传成功 → 执行 B5 修复(见节 4)。
**侧边栏列表**
- `refreshSessions()``GET /api/sessions?project=<draftProject>``draftProject=null` 时不传参(后端返空)→ 侧边栏空。
- 仅显示当前项目会话。
**删除会话**
- 删当前 `sid``DELETE` 成功后进入空白草稿态(`sid=null`、清 `localStorage`、欢迎语/空态提示)。
- 删其他 → 仅 `refreshSessions()`
---
## 4. 5 条不整合点修复落地
| 编号 | 修复内容 | 落点 |
|---|---|---|
| **A1** | 后端 `delete_session` 事务内先删 `chat_messages` 再删 `sessions`(节 2.3 | 后端 `store.py` |
| **B5** | 上传成功后:`activeProject = rec.project \|\| null``refreshSessions()` 重新渲染状态行/徽章(补全 v3 漏设的 `activeProject` | `chat.html` 上传回调 |
| **C1** | 打开抽屉/下拉前记 `lastTriggerEl = document.activeElement`Esc 关闭时 `lastTriggerEl.focus()` | `chat.html` Esc 处理 |
| **B6** | `loadDrawerForm` 时存 `drawerSnapshot`;切换抽屉项/关闭抽屉/切项目前若 `drawerDirty``confirm('有未保存的修改,确定放弃?')` | `chat.html` 抽屉逻辑 |
| **F1** | 上传**失败**时**不清空** `file-input.value`(成功时才清);错误展示后端 `detail.message` 明细 | `chat.html` 上传回调 |
---
## 5. 测试与验收
### 5.1 后端(Python,维持 `>=99%` 覆盖门槛)
见节 2.5。新增 4 个用例 + 旧用例仍绿。
### 5.2 前端(chat.html,无覆盖门槛;手动 + 轻量自测)
手动验收清单(实现后逐项核对):
1. 启动无会话 → 选具体项目 → 欢迎语;选"未选择" → 显示「请新建项目或者选择项目」。
2. 顶栏切项目 → 聊天区重置、侧边栏过滤、localStorage 清。
3. 空白态上传要件定义 → 提示「请先发送一条消息以创建会话」,**不**上传。
4. 首条消息 → 会话落库、侧边栏出现、localStorage 写入。
5. 已建会话后上传 → 成功,上传后状态行/徽章正确(B5)。
6. 上传失败 → 文件仍选中可重试(F1)。
7. 抽屉改字段未保存切项目/切抽屉项 → 弹"有未保存的修改"B6)。
8. Esc 关抽屉 → 焦点回到打开按钮(C1)。
9. 删除会话 → 历史无孤儿消息(A1);删当前 → 回空白态。
### 5.3 验收门槛
- `python -m pytest` 全绿、覆盖率 `>=99%`
- 前端手动清单全过。
---
## 6. 风险与回滚
| 风险 | 缓解 |
|---|---|
| `json_extract` 在个别部署环境不可用 | 提供 Python 端过滤 fallback(节 2.2 |
| 重命名 `currentProject``draftProject` 漏改导致功能回退 | 实现时全局替换并补前端手动清单核对 |
| 旧 v3 `localStorage` 会话加载异常 | 加载失败 catch 后进入空白态,不影响新流程 |
| 删除会话级联误删 | 单连接事务内两步,先 messages 后 sessions;加回归测试 |
回滚:本轮为独立提交,可整体 revert;数据库 schema 不变(仅 SQL 语句变更),无迁移风险。
---
## 7. 提交与规范遵循
- 文件命名 ASCII 小写(本 spec 文件名 `2026-08-28-chat-v4-deepseek-session-design.md`)。
- 代码不含硬编码绝对路径;项目配置目录经 `ProjectConfig` 解析,不写死。
- `.env` / 密钥不入库;API Key 走环境变量。
- 单一职责:前端状态机改动集中在 chat.html 内相关函数,不跨模块扩散。
- 测试先行(TDD):后端 4 个新用例先红后绿;前端按节 5.2 手动清单验收。
- 日志:在 `_AI_USAGE_LOG.md` 追加一条"架构设计 / Agent 实现"记录。