6.4 KiB
6.4 KiB
要件定义书作为附件随消息发送 + 在线查看 — 实施计划
目标文件:
- 后端:
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 后立即上传。改为:
- 选择
.xlsx→ 不清空为上传,而是作为“待发附件”chip 停在输入框上方(可×移除/替换)。 - 输入文字后点发送 → 先建会话(如需)→ 上传附件 → 再发送文字消息;用户气泡内显示附件。
- 附件与消息绑定,刷新会话后仍显示在对应消息上。
- 点击附件可在线查看(前端 SheetJS 解析原 xlsx 渲染,多 sheet)。
已确认决策
- D1 必须有文字才能发送(有附件但无文字也不可发送)。
- D2 查看 = 前端 SheetJS 在线解析原 xlsx 渲染(多 sheet 切换;样式有限)。
- D3 附件与消息绑定、刷新后仍显示(后端消息加
attachment字段 + 旧库迁移)。 - D4 单个要件定义书(再选替换待发附件)。
后端改动
store.py
_init_db:chat_messages建表增列attachment TEXT;对旧库执行迁移: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()(仍要求有文字):- 有附件 → 确保会话(
!sid→POST /api/sessions)→POST /files(file_type=requirements); - 成功 → 记
attachmentRef={file_type:'requirements',name}、清待发; POST /api/chat/{sid}/messagesbody{ content, attachment?: attachmentRef };- 上传失败 → 报错并保留待发附件、中止发送。
- 有附件 → 确保会话(
- 用户气泡:本消息带附件 → 气泡内渲染附件 chip(点击查看)。
- 历史(
loadSession):消息含attachment→ 用户气泡内渲染 chip(try/catch解析 JSON、文件名esc)。 - 查看:点击 chip →
fetch('/api/sessions/{sid}/files/{file_type}')→arrayBuffer→XLSX.read(new Uint8Array(buf), {type:'array'})→ 各 sheetXLSX.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
[email protected](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: 无阻塞问题点,开始实施