docs: 新增 chat v4 DeepSeek 会话模式设计文档与实现计划 + 更新 AI 使用日志
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# 概要设计书生成 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 实现"记录。
|
||||
Reference in New Issue
Block a user