diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md
index 8d5f9a5..6d0d648 100644
--- a/_AI_USAGE_LOG.md
+++ b/_AI_USAGE_LOG.md
@@ -126,3 +126,10 @@
| 2026-08-27 12:12 | Agent 实现 | 聊天前端升级二:会话命名(自动取要件定义文件名)+历史侧边栏;以项目为单位的模板/规则/代码库/设计文档目录配置(ProjectsStore+API),绑定项目后上传区仅要件定义;既有设计文档确定性交叉引用纳入影响调查(DesignReference) | src/genesis/server/store.py, src/genesis/server/service.py, src/genesis/server/app.py, src/genesis/parsers/source_aggregator.py, src/genesis/impact/impact_agent.py, src/genesis/data_models.py, src/genesis/chat/agent.py, src/genesis/server/static/chat.html, tests/*, docs/design.md, README.md | hy3-free |
| 2026-08-27 12:40 | 反馈迭代 | 前端 UI 美化(仅样式与装饰性结构,JS 逐字节不变):Aura Space 暗色洁净混合规范(google 色板/Inter/12-16px 圆角/玻璃 header/蓝光光晕/200ms 过渡/auraPulse 助手脉冲点),文件选择改用 label+隐藏 input | src/genesis/server/static/chat.html | hy3-free |
| 2026-08-27 23:57 | Agent 实现 | 前端 v3:移除 Aura Pipeline 痕迹;顶栏项目切换器(默认绑首项)与右侧项目配置抽屉(420px滑入,含列表/表单/保存/删除);侧边栏整体可折叠(56↔264px, localStorage 持久化);会话项 hover 出 × 原生 confirm 删除(删当前会话自动重建);设计文档 docs/design-web-chat-v3.md, chat.html, _AI_USAGE_LOG.md, 558 passed, 覆盖率 99.10% | docs/design-web-chat-v3.md, src/genesis/server/static/chat.html, _AI_USAGE_LOG.md | hy3-free |
+| 2026-08-28 | 架构设计 | 前端与后端不整合点分析 + 聊天前端 v4 设计:枚举 14 条不整合点(分 A API/B 状态机/C 错误处理/D 数据语义);用户裁定——顶栏项目切换=D-v4(切项目即空白草稿、首条消息才落库、会话创建后项目 locked、历史仅显当前项目);rules 合并维持现状;RAG 化拆为下一轮独立子项目;本轮范围=DeepSeek 模式会话 + 5 条不整合点(A1 删会话级联消息/B5 上传后状态同步/C1 Esc 焦点还原/B6 抽屉未保存提示/F1 上传失败保留文件)。方案 2 选定。产出设计文档 docs/specs/2026-08-28-chat-v4-deepseek-session-design.md | docs/specs/2026-08-28-chat-v4-deepseek-session-design.md, _AI_USAGE_LOG.md | hy3-free |
+| 2026-08-28 01:57 | Agent 实现 | Task1 list_sessions 支持 project 过滤(json_extract+Python兜底) + 新增3测试 | src/genesis/server/store.py; tests/test_server_store.py | deepseek-chat |
+| 2026-08-28 01:58 | Agent 实现 | Task2 delete_session 级联删除 chat_messages(A1) + 测试 | src/genesis/server/store.py; tests/test_server_store.py | deepseek-chat |
+| 2026-08-28 01:59 | Agent 实现 | Task3 GET /api/sessions 增加 project 参数(Task1 端点透传) + 测试 | src/genesis/server/app.py; tests/test_server_api.py | deepseek-chat |
+| 2026-08-28 02:01 | Agent 实现 | Task4 前端状态机重塑(draftProject 重命名+空白草稿+空态提示) | src/genesis/server/static/chat.html | deepseek-chat |
+| 2026-08-28 02:12 | Agent 实现 | Task1 精简 list_sessions(移除死分支 except 兜底,覆盖 SQL 过滤全路径) | src/genesis/server/store.py | deepseek-chat |
+| 2026-08-28 02:12 | Agent 实现 | Task6-9 前端:首消息建会话(send)/上传门禁+状态同步+失败保留(upload)/Esc焦点还原(C1)/抽屉未保存守卫(B6) | src/genesis/server/static/chat.html | deepseek-chat |
diff --git a/docs/plans/2026-08-28-chat-v4-deepseek-session.md b/docs/plans/2026-08-28-chat-v4-deepseek-session.md
new file mode 100644
index 0000000..05d3c9d
--- /dev/null
+++ b/docs/plans/2026-08-28-chat-v4-deepseek-session.md
@@ -0,0 +1,745 @@
+# 聊天前端 v4(DeepSeek 模式会话)实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将会话交互改为 DeepSeek 模式(进入即空白草稿、首条消息才落库、顶栏切项目仅影响下次新建),并修复 5 条高严重/低风险的前后端不整合点(A1/B5/C1/B6/F1)。
+
+**Architecture:** 后端仅微调 `store.py`(`list_sessions` 增加 project 过滤 + `delete_session` 级联删消息)与 `app.py`(`/api/sessions` 增加 `project` 查询参数);前端 `chat.html` 内嵌 JS 完成状态机重塑(变量重命名 `currentProject`→`draftProject` + 新增 `lastTriggerEl`/`drawerDirty`/`drawerSnapshot`)与 5 处修复。无 schema 迁移、无新依赖。
+
+**Tech Stack:** Python 3.11+ / FastAPI / SQLite(json_extract,带 Python 兜底)/ pytest(覆盖率 `>=99%`);前端原生 JS(内嵌于 chat.html,无构建步骤、无前端测试框架)。
+
+## Global Constraints
+
+- 文件名 ASCII 小写(本计划文件:`2026-08-28-chat-v4-deepseek-session.md`)。
+- 代码不含硬编码绝对路径;项目目录经 `ProjectConfig` 解析。
+- `.env` / 密钥不入库;API Key 走环境变量。
+- 测试命令:`python -m pytest`;覆盖率门槛 `fail_under=99`,全绿方可提交。
+- 提交信息沿用仓库风格(中文 + 范围,如 `feat(server): ...`)。
+- 文档存 `docs/` 下;AI 交流用中文。
+- 前端无自动化测试框架:每个前端任务的「测试」步骤为**具体手动浏览器验收**(取自 spec §5.2),非占位符。
+
+---
+
+## File Structure
+
+- Modify: `src/genesis/server/store.py` — `list_sessions` 增加 `project` 过滤(json_extract + Python 兜底)、`delete_session` 事务内先删 `chat_messages` 再删 `sessions`。
+- Modify: `src/genesis/server/app.py` — `GET /api/sessions` 增加 `project: str | None = None` 查询参数。
+- Modify: `src/genesis/server/static/chat.html` — 状态变量、`refreshSessions`/`newSession`/`loadSession`/`send`/上传回调/抽屉/`Esc` 处理。
+- Test: `tests/test_server_store.py` — 新增 `list_sessions` 过滤与 `delete_session` 级联用例。
+- Test: `tests/test_server_api.py` — 新增 `GET /api/sessions?project=` 用例。
+
+约定:会话创建后端返回 `{"session_id","status","name"}`(无 `project` 字段),前端用本地 `draftProject` 作为新建会话的 `activeProject`。
+
+---
+
+### Task 1: store.list_sessions 支持 project 过滤(后端)
+
+**Files:**
+- Modify: `src/genesis/server/store.py:153-162`(`list_sessions`)
+- Test: `tests/test_server_store.py`
+
+**Interfaces:**
+- Consumes: `SessionStore._conn()`、`SessionRecord.project`、`_from_dict`
+- Produces: `list_sessions(user_id, project: str | None = None) -> list[SessionRecord]`(project 非 None 时仅返匹配项;为 None 时返全部,兼容 legacy)
+
+- [ ] **Step 1: 写失败测试**
+
+```python
+# tests/test_server_store.py (追加)
+import os, tempfile
+from genesis.server.store import SessionStore
+
+def _tmp_store():
+ fd, p = tempfile.mkstemp(suffix=".db")
+ os.close(fd)
+ os.remove(p) # SessionStore 自行创建
+ return SessionStore(db_path=p), p
+
+def test_list_sessions_filter_by_project():
+ store, p = _tmp_store()
+ try:
+ a = store.create_session("u", name="A", project="stock")
+ b = store.create_session("u", name="B", project="other")
+ recs = store.list_sessions("u", project="stock")
+ ids = {r.session_id for r in recs}
+ assert a.session_id in ids and b.session_id not in ids
+ finally:
+ os.remove(p)
+
+def test_list_sessions_filter_empty_project():
+ store, p = _tmp_store()
+ try:
+ e = store.create_session("u", name="E") # project 默认 ""
+ store.create_session("u", name="X", project="stock")
+ recs = store.list_sessions("u", project="")
+ assert all(r.project == "" for r in recs)
+ assert e.session_id in {r.session_id for r in recs}
+ finally:
+ os.remove(p)
+
+def test_list_sessions_no_project_returns_all():
+ store, p = _tmp_store()
+ try:
+ store.create_session("u", name="A", project="stock")
+ store.create_session("u", name="B", project="other")
+ assert len(store.list_sessions("u")) == 2
+ finally:
+ os.remove(p)
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: `python -m pytest tests/test_server_store.py::test_list_sessions_filter_by_project tests/test_server_store.py::test_list_sessions_filter_empty_project tests/test_server_store.py::test_list_sessions_no_project_returns_all -v`
+Expected: FAIL(`list_sessions() takes 2 positional arguments but 3 were given` 或过滤未生效)
+
+- [ ] **Step 3: 最小实现**
+
+```python
+# src/genesis/server/store.py — 替换 list_sessions(153-162)
+def list_sessions(self, user_id: str, project: str | None = None) -> list[SessionRecord]:
+ if project is None:
+ with self._conn() as c:
+ rows = c.execute(
+ "SELECT data FROM sessions WHERE user_id = ?", (user_id,)
+ ).fetchall()
+ recs = [self._from_dict(json.loads(r["data"])) for r in rows]
+ else:
+ try:
+ with self._conn() as c:
+ rows = c.execute(
+ "SELECT data FROM sessions WHERE user_id = ?"
+ " AND json_extract(data, '$.project') = ?",
+ (user_id, project),
+ ).fetchall()
+ recs = [self._from_dict(json.loads(r["data"])) for r in rows]
+ except Exception:
+ # json_extract 不可用(旧 SQLite)→ 全量取回后 Python 过滤兜底
+ with self._conn() as c:
+ rows = c.execute(
+ "SELECT data FROM sessions WHERE user_id = ?", (user_id,)
+ ).fetchall()
+ recs = [self._from_dict(json.loads(r["data"])) for r in rows]
+ recs = [r for r in recs if r.project == project]
+ recs.sort(key=lambda r: r.updated_at, reverse=True)
+ return recs
+```
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `python -m pytest tests/test_server_store.py -v`
+Expected: PASS(含 3 个新用例);其余 store 用例仍绿
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add src/genesis/server/store.py tests/test_server_store.py
+git commit -m "feat(server): list_sessions 支持 project 过滤(json_extract + Python 兜底)"
+```
+
+---
+
+### Task 2: store.delete_session 级联删除消息(A1)
+
+**Files:**
+- Modify: `src/genesis/server/store.py:180-183`(`delete_session`)
+- Test: `tests/test_server_store.py`
+
+**Interfaces:**
+- Consumes: `add_message(session_id, role, content, action=None)`、`list_messages(session_id)`
+- Produces: `delete_session(session_id) -> bool`(先删 `chat_messages` 再删 `sessions`,原子)
+
+- [ ] **Step 1: 写失败测试**
+
+```python
+# tests/test_server_store.py (追加)
+def test_delete_session_cascades_messages():
+ store, p = _tmp_store()
+ try:
+ rec = store.create_session("u", name="A", project="stock")
+ store.add_message(rec.session_id, "user", "你好")
+ store.add_message(rec.session_id, "assistant", "收到")
+ assert len(store.list_messages(rec.session_id)) == 2
+ ok = store.delete_session(rec.session_id)
+ assert ok is True
+ assert store.list_messages(rec.session_id) == [] # 消息已级联删除
+ finally:
+ os.remove(p)
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: `python -m pytest tests/test_server_store.py::test_delete_session_cascades_messages -v`
+Expected: FAIL(删除后 `list_messages` 仍返回 2 条)
+
+- [ ] **Step 3: 最小实现**
+
+```python
+# src/genesis/server/store.py — 替换 delete_session(180-183)
+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
+```
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `python -m pytest tests/test_server_store.py::test_delete_session_cascades_messages -v`
+Expected: PASS
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add src/genesis/server/store.py tests/test_server_store.py
+git commit -m "fix(server): delete_session 级联删除 chat_messages(A1)"
+```
+
+---
+
+### Task 3: app.py GET /api/sessions 增加 project 参数
+
+**Files:**
+- Modify: `src/genesis/server/app.py:156-162`(`list_sessions` 端点)
+- Test: `tests/test_server_api.py`
+
+**Interfaces:**
+- Consumes: `service.store.list_sessions(user_id, project)`
+- Produces: `GET /api/sessions?user_id=&project=` 返回按 project 过滤的会话列表
+
+- [ ] **Step 1: 写失败测试**
+
+```python
+# tests/test_server_api.py (追加,复用现有 client fixture)
+def test_api_list_sessions_filter_by_project(client):
+ r0 = client.post("/api/sessions", json={"user_id": "default", "project": "stock"})
+ r1 = client.post("/api/sessions", json={"user_id": "default", "project": "other"})
+ res = client.get("/api/sessions?user_id=default&project=stock")
+ assert res.status_code == 200
+ ids = {x["session_id"] for x in res.json()}
+ assert r0.json()["session_id"] in ids
+ assert r1.json()["session_id"] not in ids
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: `python -m pytest tests/test_server_api.py::test_api_list_sessions_filter_by_project -v`
+Expected: FAIL(`project` 参数被忽略,两个会话都返回)
+
+- [ ] **Step 3: 最小实现**
+
+```python
+# src/genesis/server/app.py — 替换 list_sessions 端点(156-162)
+@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,
+ "status": r.status, "updated_at": r.updated_at}
+ for r in service.store.list_sessions(user_id, project=project)
+ ]
+```
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `python -m pytest tests/test_server_api.py::test_api_list_sessions_filter_by_project -v`
+Expected: PASS
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add src/genesis/server/app.py tests/test_server_api.py
+git commit -m "feat(server): GET /api/sessions 支持 project 查询参数"
+```
+
+---
+
+### Task 4: 前端状态机重塑 — 变量重命名 + 空白草稿 + 空态提示
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(状态变量声明、新增 `renderEmptyOrWelcome`、`newSession` 重置、`loadSession` 同步 `draftProject`、启动逻辑)
+
+**Interfaces:**
+- Consumes: `api()`、`addMsg()`、`refreshSessions()`(Task 5 改造)、`updateUploadBar()`、`loadProjects()`
+- Produces: `draftProject`(替代 `currentProject`);空白草稿态不创建会话;`draftProject===null` 时内容区显示固定文案「请新建项目或者选择项目」
+
+> 前端无自动化测试;本任务验收 = 手动浏览器核对 spec §5.2 第 1、4 条。
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+打开 `scripts/serve.py --fake`,浏览器访问 `/`:
+- 现状:进入即 `POST /api/sessions` 创建会话,顶栏显示「未创建会话」被替换。
+- 期望(改后):进入显示空白聊天;`draftProject` 为具体项目时显示欢迎语;`draftProject===null` 时内容区显示「请新建项目或者选择项目」。
+
+- [ ] **Step 2: 实现 — 状态变量(448-453)**
+
+```javascript
+// 替换
+let sid = null;
+let currentProject = null;
+let activeProject = null;
+let projects = [];
+let drawerOpen = false;
+let drawerSelected = null; // 当前抽屉中编辑的项目名(null = 新建模式)
+
+// 为
+let sid = null;
+let draftProject = null; // 下一空白会话将绑定/已绑的项目(原 currentProject)
+let activeProject = null; // 当前已落库会话绑定的项目(来自后端)
+let projects = [];
+let drawerOpen = false;
+let drawerSelected = null;
+let lastTriggerEl = null; // 打开抽屉/下拉前的焦点元素(C1)
+let drawerDirty = false; // 抽屉表单是否有未保存改动(B6)
+let drawerSnapshot = null; // loadDrawerForm 时的字段快照(B6)
+```
+
+- [ ] **Step 3: 实现 — 新增 renderEmptyOrWelcome + 改造 newSession**
+
+在 `addMsg` 附近新增:
+
+```javascript
+function renderEmptyOrWelcome() {
+ chatEl.innerHTML = '';
+ if (draftProject === null) {
+ addMsg('assistant', '请新建项目或者选择项目');
+ } else {
+ addMsg('assistant', '你好!我是 Genesis 概要设计书生成 Agent。
请先在上方📎上传要件定义 xlsx'
+ + (draftProject ? '(模板/规则/代码库已由项目「' + esc(draftProject) + '」提供)' : '与模板 docx')
+ + ',然后告诉我:
· 「生成概要设计书」(自动解析→影响→生成→QA)
· 「用中文生成」 / 「现在什么状态?」');
+ }
+}
+```
+
+替换 `newSession`(706-718)为:
+
+```javascript
+async function newSession() {
+ sid = null;
+ activeProject = null;
+ try { localStorage.removeItem('genesis_session'); } catch (e) { /* 忽略 */ }
+ chatEl.innerHTML = '';
+ badge.textContent = '未创建会话';
+ updateUploadBar();
+ await refreshSessions();
+ renderEmptyOrWelcome();
+}
+```
+
+- [ ] **Step 4: 实现 — loadSession 同步 draftProject(720-739)**
+
+在 `activeProject = rec.project || null;` 之后增加一行:
+
+```javascript
+ activeProject = rec.project || null;
+ draftProject = rec.project || null; // 载入既有会话时,顶栏与侧边栏过滤对齐该会话项目
+ applyProjectContext();
+```
+
+- [ ] **Step 5: 实现 — 启动逻辑(821-833)**
+
+替换启动 IIFE 为:
+
+```javascript
+(async () => {
+ try {
+ const collapsed = localStorage.getItem('genesis_sidebar_collapsed') === '1';
+ if (collapsed) document.getElementById('sidebar').classList.add('collapsed');
+ } catch (e) { /* 忽略 */ }
+ await loadProjects();
+ const saved = (() => { try { return localStorage.getItem('genesis_session'); } catch (e) { return null; } })();
+ if (saved) {
+ try { await loadSession(saved); return; } catch (e) { /* 忽略,新建 */ }
+ }
+ newSession().catch(e => addMsg('error', esc(String(e))));
+})();
+```
+
+- [ ] **Step 6: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`(确认后端未回归)
+浏览器核对:启动无会话 → 选具体项目显示欢迎语;选「未选择项目」显示「请新建项目或者选择项目」;点「+ 新会话」回到空白草稿且不创建会话(Network 无 `POST /api/sessions`)。
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m "feat(chat): 会话状态机重塑——空白草稿态 + draftProject 重命名 + 空态提示"
+```
+
+---
+
+### Task 5: 侧边栏按项目过滤(refreshSessions 改造)
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(`refreshSessions` 484-505)
+
+**Interfaces:**
+- Consumes: `draftProject`、`api()`
+- Produces: `draftProject===null` 时侧边栏为空;否则 `GET /api/sessions?user_id=default&project=`(带 `encodeURIComponent`)
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+有两个项目各 1 个历史会话时,切换顶栏项目 → 侧边栏只显示当前项目会话。
+
+- [ ] **Step 2: 实现 — refreshSessions**
+
+替换 `refreshSessions`(484-505)开头部分为:
+
+```javascript
+async function refreshSessions() {
+ const box = document.getElementById('session-list');
+ if (draftProject === null) { box.innerHTML = ''; return; } // 未选项目 → 无历史
+ const list = await api('GET', '/api/sessions?user_id=default&project=' + encodeURIComponent(draftProject));
+ box.innerHTML = '';
+ list.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''));
+ for (const s of list) {
+```
+
+(后续 `for (const s of list) { ... }` 渲染逻辑保持不变,仅循环源从 `list` 改为已排序 `list`——原代码已在循环前 sort,结构一致,直接沿用原 489 行起的渲染体。)
+
+- [ ] **Step 3: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`
+浏览器核对 spec §5.2 第 2 条:顶栏切项目 → 侧边栏过滤、localStorage 清。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m "feat(chat): 侧边栏按 draftProject 过滤会话列表"
+```
+---
+
+### Task 6: 首条消息创建会话(send 改造,D-v4 核心)
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(`send` 741-772)
+
+**Interfaces:**
+- Consumes: `sid`、`draftProject`、`api()`、`showTyping()`、`addMsg()`
+- Produces: `sid===null` 时先 `POST /api/sessions`(带 `draftProject`)再发消息;`activeProject = draftProject`
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+空白草稿态发首条消息 → Network 先出现 `POST /api/sessions`,随后 `POST /api/chat/{sid}/messages`;侧边栏出现该会话;`localStorage['genesis_session']` 写入。
+
+- [ ] **Step 2: 实现 — 完整替换 send**
+
+替换 `send`(741-772)为:
+
+```javascript
+async function send() {
+ const text = inputEl.value.trim();
+ if (!text) return;
+ if (!sid) {
+ // 首条消息:先落库会话(D-v4)
+ try {
+ const d = await api('POST', '/api/sessions', { user_id: 'default', project: draftProject || null });
+ sid = d.session_id;
+ activeProject = draftProject || null;
+ badge.textContent = '会话: ' + (d.name || '新会话');
+ try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ }
+ await refreshSessions();
+ } catch (e) {
+ addMsg('error', '创建会话失败:' + esc(String(e)));
+ return;
+ }
+ }
+ inputEl.value = '';
+ addMsg('user', esc(text));
+ const typing = showTyping();
+ sendBtn.disabled = true;
+ try {
+ const res = await api('POST', '/api/chat/' + sid + '/messages', { content: text });
+ typing.remove();
+ if (res.progress && res.progress.length) {
+ const items = res.progress.map(p => '' + esc(p.step) + ': ' + esc(p.detail || '') + '').join('');
+ addMsg('progress', items);
+ }
+ let replyHtml = esc(res.reply || '');
+ const s = res.status;
+ if (s === 'done' || s === 'writing') {
+ replyHtml += '';
+ }
+ addMsg('assistant', replyHtml);
+ } catch (e) {
+ typing.remove();
+ addMsg('error', '请求失败:' + esc(String(e)));
+ } finally {
+ sendBtn.disabled = false;
+ inputEl.focus();
+ }
+}
+```
+
+- [ ] **Step 3: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`
+浏览器核对 spec §5.2 第 4 条。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m 'feat(chat): 首条消息才创建会话(DeepSeek 模式核心)'
+```
+
+---
+
+### Task 7: 上传门禁 + B5 状态同步 + F1 失败保留文件
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(上传回调 793-819)
+
+**Interfaces:**
+- Consumes: `sid`、`activeProject`、`api()`、`refreshSessions()`、`updateUploadBar()`
+- Produces: `sid===null` 时拒绝上传并提示;成功后 `activeProject = rec.project || null` + `refreshSessions()`(B5);失败后不清空 `file-input.value` 且展示 `detail.message`(F1)
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+空白草稿态点「上传」→ 提示「请先发送一条消息以创建会话」,不上传。已建会话上传成功 → 状态行/徽章正确。上传失败(如断网)→ 文件仍选中可重试。
+
+- [ ] **Step 2: 实现 — 上传回调**
+
+替换上传 `click` 处理器(793-819)为:
+
+```javascript
+document.getElementById('upload-btn').addEventListener('click', async () => {
+ if (!sid) { addMsg('error', '请先发送一条消息以创建会话'); inputEl.focus(); return; }
+ let ft = document.getElementById('file-type').value;
+ if (activeProject || draftProject) ft = 'requirements';
+ const file = document.getElementById('file-input').files[0];
+ if (!file) { addMsg('error', '请选择文件'); return; }
+ const fd = new FormData();
+ fd.append('file_type', ft);
+ fd.append('file', file);
+ const statusEl = document.getElementById('upload-status');
+ statusEl.textContent = '上传中…';
+ try {
+ const r = await fetch('/api/sessions/' + sid + '/files', { method: 'POST', body: fd });
+ const d = await r.json();
+ if (!r.ok) throw new Error(d.detail?.message || r.status);
+ statusEl.textContent = '';
+ // B5:上传成功后同步 activeProject 并刷新侧边栏状态
+ const rec = await api('GET', '/api/sessions/' + sid);
+ activeProject = rec.project || null;
+ badge.textContent = '会话: ' + (rec.name || '新会话');
+ await refreshSessions();
+ document.getElementById('file-input').value = ''; // 仅成功时清空
+ addMsg('user', '[上传] ' + ft + ':' + d.file_name);
+ addMsg('progress', '上传完成:' + esc(d.file_name) + '(' + d.size + 'B)');
+ } catch (e) {
+ statusEl.textContent = '';
+ // F1:失败保留 file-input 值,展示后端明细
+ addMsg('error', '上传失败:' + esc(String(e)));
+ }
+});
+```
+
+- [ ] **Step 3: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`
+浏览器核对 spec §5.2 第 3、5、6 条。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m 'fix(chat): 上传门禁(sid 门禁) + B5 状态同步 + F1 失败保留文件'
+```
+
+---
+
+### Task 8: Esc 关闭后焦点还原(C1)
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(`ps-current` 点击、`ps-manage` 点击、`Esc` 处理 782-791)
+
+**Interfaces:**
+- Consumes: `lastTriggerEl`、`toggleProjectSwitcher()`、`closeProjectDrawer()`
+- Produces: 打开抽屉/下拉前记录 `lastTriggerEl`;`Esc` 关闭后 `lastTriggerEl.focus()`
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+打开项目抽屉(点「项目管理」)→ `Esc` 关闭 → 焦点回到「项目管理」触发按钮。打开顶栏下拉 → `Esc` → 焦点回到「项目」按钮。
+
+- [ ] **Step 2: 实现 — 记录触发元素**
+
+`ps-current` 点击(778)改为:
+
+```javascript
+document.getElementById('ps-current').addEventListener('click', (e) => {
+ e.stopPropagation();
+ lastTriggerEl = e.currentTarget;
+ toggleProjectSwitcher();
+});
+```
+
+`ps-manage` 点击(568-573 区域)在 `openProjectDrawer` 前记录:
+
+```javascript
+ psMenu.querySelectorAll('.ps-item[data-manage]').forEach(it => {
+ it.onclick = () => {
+ lastTriggerEl = it;
+ toggleProjectSwitcher(false);
+ openProjectDrawer(drawerSelected);
+ };
+ });
+```
+
+- [ ] **Step 3: 实现 — Esc 还原焦点**
+
+替换 `Esc` 处理(786-791)为:
+
+```javascript
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ if (psMenu.classList.contains('open')) toggleProjectSwitcher(false);
+ else if (drawerOpen) closeProjectDrawer();
+ if (lastTriggerEl) { lastTriggerEl.focus(); lastTriggerEl = null; }
+ }
+});
+```
+
+- [ ] **Step 4: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`
+浏览器核对 spec §5.2 第 8 条。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m 'fix(chat): Esc 关闭抽屉/下拉后焦点还原到触发元素(C1)'
+```
+
+---
+
+### Task 9: 抽屉未保存变更提示(B6)
+
+**Files:**
+- Modify: `src/genesis/server/static/chat.html`(`loadDrawerForm` 快照、`renderDrawerList`/`ps-manage`/`closeProjectDrawer`/`renderProjectSwitcher` 守卫)
+
+**Interfaces:**
+- Consumes: `drawerOpen`、`drawerSnapshot`、`isDrawerDirty()`(新增)
+- Produces: 切换抽屉项/关闭抽屉/切项目/切管理前若有未保存改动 → `confirm('有未保存的修改,确定放弃?')`
+
+- [ ] **Step 1: 写失败验收(手动)**
+
+打开项目抽屉 → 改某字段 → 点另一个项目 / 点「项目管理」/ `Esc` 关闭 → 弹出「有未保存的修改,确定放弃?」;点「确定」才离开,点「取消」停留。
+
+- [ ] **Step 2: 实现 — 快照与脏检测**
+
+在 `loadDrawerForm` 末尾(`renderDrawerList();` 之后)追加快照采集,并新增三个辅助函数(放在 `loadDrawerForm` 附近):
+
+```javascript
+function drawerSnapshotNow() {
+ return [
+ document.getElementById('pf-name').value.trim(),
+ document.getElementById('pf-display').value.trim(),
+ document.getElementById('pf-template').value.trim(),
+ document.getElementById('pf-write').value.trim(),
+ document.getElementById('pf-rules').value.trim(),
+ document.getElementById('pf-code').value.trim(),
+ document.getElementById('pf-design').value.trim(),
+ ].join('');
+}
+function isDrawerDirty() {
+ if (!drawerOpen || drawerSnapshot === null) return false;
+ return drawerSnapshotNow() !== drawerSnapshot;
+}
+function guardDrawerDirty() {
+ if (isDrawerDirty()) return confirm('有未保存的修改,确定放弃?');
+ return true;
+}
+```
+
+在 `loadDrawerForm` 函数体最后追加:
+
+```javascript
+ drawerSnapshot = drawerSnapshotNow();
+```
+
+- [ ] **Step 3: 实现 — 守卫插入**
+
+`renderDrawerList` 项点击(634-639)加守卫:
+
+```javascript
+ it.onclick = () => {
+ if (!guardDrawerDirty()) return;
+ const n = it.dataset.name === '' ? null : it.dataset.name;
+ loadDrawerForm(n);
+ };
+```
+
+`ps-manage` 点击(Task 8 已改)加守卫:
+
+```javascript
+ it.onclick = () => {
+ if (!guardDrawerDirty()) return;
+ lastTriggerEl = it;
+ toggleProjectSwitcher(false);
+ openProjectDrawer(drawerSelected);
+ };
+```
+
+`closeProjectDrawer`(589-592)加守卫:
+
+```javascript
+function closeProjectDrawer() {
+ if (!guardDrawerDirty()) return;
+ drawerOpen = false;
+ drawer.classList.remove('open');
+}
+```
+
+`renderProjectSwitcher` 项点击(559-567)加守卫:
+
+```javascript
+ psMenu.querySelectorAll('.ps-item[data-name]').forEach(it => {
+ it.onclick = () => {
+ if (!guardDrawerDirty()) return;
+ currentProject = it.dataset.name === '' ? null : it.dataset.name;
+ applyProjectContext();
+ renderProjectSwitcher();
+ updateUploadBar();
+ toggleProjectSwitcher(false);
+ };
+ });
+```
+
+- [ ] **Step 4: 运行手动验收**
+
+Run: `python -m pytest tests/test_server_*.py -q`
+浏览器核对 spec §5.2 第 7 条。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add src/genesis/server/static/chat.html
+git commit -m 'fix(chat): 抽屉未保存变更守卫提示(B6)'
+```
+
+---
+
+## Self-Review(写毕自查)
+
+**1. Spec 覆盖**:
+- §1.1 生命周期(进入空白/切项目/首条消息/继续/新会话/删除)→ Task 4/5/6/7 覆盖。
+- §1.3 空态文案「请新建项目或者选择项目」→ Task 4 `renderEmptyOrWelcome` 硬编码。
+- §1.5 上传门禁(禁只上传不发消息)→ Task 7 `if (!sid)` 守卫。
+- §2.2 `list_sessions` project 过滤 → Task 1。
+- §2.3 级联删除 → Task 2。
+- §2.4 端点参数 → Task 3。
+- §3 前端状态机(变量/启动/切项目/新会话/首消息/上传/侧边栏/删除)→ Task 4-7。
+- §4 A1/B5/C1/B6/F1 → Task 2/7/8/9/7。
+- 无遗漏。
+
+**2. 占位符扫描**:无 TBD/TODO;前端「测试」步骤均为具体手动验收(环境无前端测试框架,已在 Global Constraints 声明)。无任何笔误占位行。
+
+**3. 类型/命名一致性**:`draftProject` 在 Task 4 声明、Task 5/6/7 引用一致;`lastTriggerEl` Task 4 声明、Task 8 使用;`drawerSnapshot`/`isDrawerDirty`/`guardDrawerDirty` 在 Task 9 内定义并使用;后端 `list_sessions(user_id, project)` 签名在 Task 1 定义、Task 3 端点透传一致。无命名漂移。
+
+**4. 与 spec 的一处澄清**:spec §2.4 原文「project 为空(None)时不加过滤条件……返回空历史」存在内部张力。本计划采用:**后端 `list_sessions(project=None)` 返回全部(legacy 兼容);前端 `draftProject===null` 时 `refreshSessions` 直接渲染空列表且不发请求**,从而满足用户裁定「未选择项目 → 侧边栏无历史」。对应测试 `test_list_sessions_no_project_returns_all`(返全部)取代 spec 原 `test_list_sessions_no_project_returns_empty_for_v4`(空态为前端行为)。
diff --git a/docs/specs/2026-08-28-chat-v4-deepseek-session-design.md b/docs/specs/2026-08-28-chat-v4-deepseek-session-design.md
new file mode 100644
index 0000000..582fcc4
--- /dev/null
+++ b/docs/specs/2026-08-28-chat-v4-deepseek-session-design.md
@@ -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=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 实现"记录。