98 lines
6.4 KiB
Markdown
98 lines
6.4 KiB
Markdown
# 要件定义书作为附件随消息发送 + 在线查看 — 实施计划
|
||
|
||
目标文件:
|
||
- 后端:`src/genesis/server/{store.py, service.py, app.py}`、`src/genesis/chat/agent.py`
|
||
- 前端:`frontend/{package.json, src/main.ts, src/chat.css}`
|
||
- 测试:`tests/{test_server_store.py, test_server_api.py, test_server_service.py}`
|
||
|
||
## 背景 / 目标
|
||
现状:选择 `.xlsx` 后**立即上传**。改为:
|
||
1. 选择 `.xlsx` → **不清空为上传,而是作为“待发附件”chip 停在输入框上方**(可 `×` 移除/替换)。
|
||
2. 输入文字后点发送 → **先建会话(如需)→ 上传附件 → 再发送文字消息**;用户气泡内显示附件。
|
||
3. **附件与消息绑定**,刷新会话后仍显示在对应消息上。
|
||
4. 点击附件可**在线查看**(前端 SheetJS 解析原 xlsx 渲染,多 sheet)。
|
||
|
||
## 已确认决策
|
||
- D1 **必须有文字才能发送**(有附件但无文字也不可发送)。
|
||
- D2 查看 = **前端 SheetJS 在线解析原 xlsx 渲染**(多 sheet 切换;样式有限)。
|
||
- D3 附件与消息绑定、**刷新后仍显示**(后端消息加 `attachment` 字段 + 旧库迁移)。
|
||
- D4 **单个**要件定义书(再选替换待发附件)。
|
||
|
||
## 后端改动
|
||
|
||
### `store.py`
|
||
- `_init_db`:`chat_messages` 建表增列 `attachment TEXT`;对旧库执行迁移:
|
||
```python
|
||
cols = {r["name"] for r in c.execute("PRAGMA table_info(chat_messages)")}
|
||
if "attachment" not in cols:
|
||
c.execute("ALTER TABLE chat_messages ADD COLUMN attachment TEXT")
|
||
```
|
||
- `add_message(session_id, role, content, action=None, attachment: str | None = None)`;INSERT 增 `attachment`。
|
||
- `list_messages`:SELECT 增 `attachment`,返回 dict 增该字段(原样字符串)。
|
||
|
||
### `chat/agent.py`
|
||
- `handle_message(session_id, content, attachment: str | None = None)`;`self.store.add_message(session_id, "user", content, attachment=attachment)`(L29)。
|
||
|
||
### `app.py`
|
||
- 新增 `class AttachmentRef(BaseModel): file_type: str = "requirements"; name: str = ""`;`ChatMessageReq` 增 `attachment: AttachmentRef | None = None`。
|
||
- `POST /api/chat/{sid}/messages`:把 `body.attachment` 序列化为 JSON 传给 `handle_message`。
|
||
- 新增 `GET /api/sessions/{sid}/files/{file_type}` → `FileResponse`(供前端取字节)。
|
||
- `service.file_path(sid, file_type) -> str | None`(校验 `ALLOWED_FILE_TYPES` 与存在性)。
|
||
|
||
### `service.py`
|
||
- 新增 `file_path(sid, file_type)`:从 `session.files[file_type]["path"]` 取,校验存在;非法类型/缺失返回 None。
|
||
|
||
## 前端改动
|
||
|
||
### 依赖
|
||
- `frontend/package.json` 增 `xlsx`(SheetJS,Apache-2.0)到 `devDependencies`;esbuild 打包进 `chat.js`。
|
||
|
||
### `main.ts`
|
||
- 状态 `pendingAttachment: { file: File; name: string } | null`。
|
||
- `file-input.change`:校验 `.xlsx` → 存为待发 → 渲染待发 chip(**不立即上传**)→ 清空 `input.value`。
|
||
- 待发 chip 渲染在输入框上方(`#composer-inner` 之前):`📄 文件名 ×`;`×` 清空。
|
||
- `send()`(仍要求有文字):
|
||
1. 有附件 → 确保会话(`!sid` → `POST /api/sessions`)→ `POST /files`(`file_type=requirements`);
|
||
2. 成功 → 记 `attachmentRef={file_type:'requirements',name}`、清待发;
|
||
3. `POST /api/chat/{sid}/messages` body `{ content, attachment?: attachmentRef }`;
|
||
4. 上传失败 → 报错并**保留**待发附件、中止发送。
|
||
- 用户气泡:本消息带附件 → 气泡内渲染附件 chip(点击查看)。
|
||
- 历史(`loadSession`):消息含 `attachment` → 用户气泡内渲染 chip(`try/catch` 解析 JSON、文件名 `esc`)。
|
||
- 查看:点击 chip → `fetch('/api/sessions/{sid}/files/{file_type}')` → `arrayBuffer` → `XLSX.read(new Uint8Array(buf), {type:'array'})` → 各 sheet `XLSX.utils.sheet_to_html` → 渲染进**页面内弹层**(sheet 标签切换 + 关闭;DOM 由 JS 动态创建,无需改 chat.html)。
|
||
- `updateSendState()` 不变(仍以文字为准)。
|
||
|
||
### `chat.css`
|
||
- 待发 chip、气泡内附件 chip、查看弹层(overlay + sheet tabs + 表格滚动)样式。
|
||
|
||
## 验证
|
||
- `cd frontend && npm run verify`(typecheck + node --test + build)。
|
||
- `python -m pytest`(覆盖率 ≥99%,含新增测试)。
|
||
- 手动:选文件→待发 chip;无文字时发送键禁用;有文字发送→先上传后发消息;气泡显示附件;点查看→SheetJS 弹层多 sheet;刷新会话后附件仍在对应消息。
|
||
|
||
## 评审结论(plan-eng-review)
|
||
### Step 0 假设(已核对)
|
||
- 用户消息在 `chat/agent.py:29` 入库 → attachment 从此透传。✅
|
||
- `chat_messages` 无 `attachment` 列 → 需迁移。✅
|
||
- 无 xlsx 查看能力 → 前端 SheetJS。✅
|
||
- 无上传文件读取端点 → 新增 GET。✅
|
||
|
||
### 发现
|
||
- **F1(体积)**:SheetJS 打包进 `chat.js` 会显著增大(约数百 KB)。为保持“单文件 + mtime 版本”简单性,本轮直接打包;后续可改动态 `import()` 懒加载。**接受**。
|
||
- **F2(依赖/许可)**:npm `xlsx@0.18.5`(SheetJS 新版走自有 CDN);Apache-2.0,合规;仅用读取功能。**接受**。
|
||
- **F3(迁移)**:PRAGMA 检测后再 ALTER,兼容新老库;补迁移测试。**已纳入**。
|
||
- **F4(顺序)**:先上传后发消息;上传成功而发消息失败时,附件已在会话、消息未绑定,重试覆盖重发。**接受**。
|
||
- **F5(安全)**:`GET /files/{file_type}` 限 `ALLOWED_FILE_TYPES`,路径取自会话存储,防穿越。**已纳入**。
|
||
- **F6(首条消息带附件)**:先建会话→上传(会话名取文件名)→发消息,agent 可读 requirements。✅
|
||
- **F7(兼容)**:`attachment` 为 JSON 字符串,前端 try/catch 解析;文件名一律 `esc`。**已纳入**。
|
||
|
||
### 剩余风险(非阻塞,实施中若触发则询问)
|
||
- **R1**:`xlsx` 为 CommonJS,esbuild 打包时可能尝试解析 `fs`/`crypto` 等 Node 内建。若报错,改用其浏览器发行版或配置 `external` 处理。
|
||
|
||
## GSTACK REVIEW REPORT
|
||
- STATUS: DONE
|
||
- REVIEWED: docs/superpowers/plans/2026-09-12-attachment-with-message.md
|
||
- FINDINGS: F1 体积(接受) / F2 依赖许可(接受) / F3 迁移(纳入) / F4 顺序(接受) / F5 安全(纳入) / F6/F7 已覆盖
|
||
- KEY DECISION: 附件待发 → 发送时先上传后发消息;消息绑定 attachment 并持久;SheetJS 在线查看
|
||
- RISK: R1(xlsx/esbuild 打包兼容)——实施中若触发即询问
|
||
- NEXT: 无阻塞问题点,开始实施
|