Files
2026Technology-Competition/docs/superpowers/plans/2026-09-12-attachment-with-message.md
T

6.4 KiB
Raw Blame History

要件定义书作为附件随消息发送 + 在线查看 — 实施计划

目标文件:

  • 后端: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_dbchat_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_messagesSELECT 增 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 = ""ChatMessageReqattachment: 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.jsonxlsxSheetJSApache-2.0)到 devDependenciesesbuild 打包进 chat.js

main.ts

  • 状态 pendingAttachment: { file: File; name: string } | null
  • file-input.change:校验 .xlsx → 存为待发 → 渲染待发 chip(不立即上传)→ 清空 input.value
  • 待发 chip 渲染在输入框上方(#composer-inner 之前):📄 文件名 ×× 清空。
  • send()(仍要求有文字):
    1. 有附件 → 确保会话(!sidPOST /api/sessions)→ POST /filesfile_type=requirements);
    2. 成功 → 记 attachmentRef={file_type:'requirements',name}、清待发;
    3. POST /api/chat/{sid}/messages body { content, attachment?: attachmentRef }
    4. 上传失败 → 报错并保留待发附件、中止发送。
  • 用户气泡:本消息带附件 → 气泡内渲染附件 chip(点击查看)。
  • 历史(loadSession):消息含 attachment → 用户气泡内渲染 chiptry/catch 解析 JSON、文件名 esc)。
  • 查看:点击 chip → fetch('/api/sessions/{sid}/files/{file_type}')arrayBufferXLSX.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 verifytypecheck + node --test + build)。
  • python -m pytest(覆盖率 ≥99%,含新增测试)。
  • 手动:选文件→待发 chip;无文字时发送键禁用;有文字发送→先上传后发消息;气泡显示附件;点查看→SheetJS 弹层多 sheet;刷新会话后附件仍在对应消息。

评审结论(plan-eng-review

Step 0 假设(已核对)

  • 用户消息在 chat/agent.py:29 入库 → attachment 从此透传。
  • chat_messagesattachment 列 → 需迁移。
  • 无 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已纳入

剩余风险(非阻塞,实施中若触发则询问)

  • R1xlsx 为 CommonJSesbuild 打包时可能尝试解析 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: R1xlsx/esbuild 打包兼容)——实施中若触发即询问
  • NEXT: 无阻塞问题点,开始实施