406 lines
18 KiB
Markdown
406 lines
18 KiB
Markdown
---
|
||
date: 2026-06-23
|
||
status: active
|
||
origin: docs/brainstorms/2026-06-23-document-processing-capability-requirements.md
|
||
---
|
||
|
||
# feat: Document Processing Capability
|
||
|
||
## Summary
|
||
|
||
为 AgentKit 增加文档处理能力,v1 聚焦 Word/Excel/PDF 三种格式的创建和读取,以及 Word 模板填充。通过自研 DocumentService 统一封装所有文档操作(python-docx/openpyxl/reportlab/python-docx-template),Agent 工具和前端 REST API 共用同一套业务逻辑。生成的文档保存在服务器并持久化元数据,对话中返回文件卡片,同时在右侧面板展示当前对话的文档列表。
|
||
|
||
## Problem Frame
|
||
|
||
当前 Agent 工具集没有格式化文档处理能力。用户需要生成报告、合同、数据表等文档时,Agent 只能通过 shell 创建纯文本文件。
|
||
|
||
原计划集成 MCP Document Tools,但功能验证发现:版本 0.1.0 未验证状态不建议生产使用、不支持模板填充(核心需求)、Office→PDF 仅限 docx。因此改为全部自研,使用成熟的 python-docx/openpyxl/reportlab/python-docx-template 库,完全可控且无外部依赖风险。
|
||
|
||
## Requirements
|
||
|
||
Traceability to origin requirements doc (R-IDs preserved):
|
||
|
||
- R1-R4: 文档处理能力(Word/Excel/PDF 创建 + 读取)
|
||
- R5-R8: Agent 工具集成
|
||
- R9-R10: 前端界面
|
||
- R11-R16: 文件存储与生命周期
|
||
- R17-R18: 对话中文档展示
|
||
- R19-R22: 右侧文档/附件面板
|
||
- R23-R25: 模板填充(Word only)
|
||
- R26-R28: 安全
|
||
|
||
## Key Technical Decisions
|
||
|
||
- **自研而非 MCP 集成** — MCP Document Tools 版本 0.1.0 未验证、不支持模板填充、不建议生产使用。改用成熟的生产级库:python-docx(Word)、openpyxl(Excel)、reportlab(PDF)、python-docx-template(Word 模板填充)。MCP Document Tools 降级为可选增强,不在 v1 范围。
|
||
- **DocumentService 统一封装** — DocumentService 作为唯一业务逻辑层,Agent 工具和前端 REST API 都是薄封装。内部按格式分派到对应的 renderer 模块。
|
||
- **Agent 生成 Markdown,Service 负责格式映射** — Agent 生成 Markdown 格式的结构化内容,DocumentService 内部有 Markdown→Word/Excel/PDF 的 renderer,将 Markdown 结构映射为目标格式。Agent 不直接操作 Office XML。
|
||
- **数据库用 aiosqlite 裸连接** — 遵循项目现有模式(auth.py 的 `aiosqlite.connect`),不引入 SQLAlchemy session 依赖注入。文档元数据表用原生 SQL 建表。
|
||
- **Jinja2 沙箱化** — 模板填充使用 `jinja2.sandbox.SandboxedEnvironment`,防止 SSTI 攻击。
|
||
- **文件存储复用 data/uploads/** — 复用现有上传目录和 `_sanitize_filename` 函数,但下载 API 新增认证。
|
||
|
||
---
|
||
|
||
## Implementation Units
|
||
|
||
### U1. DocumentService 核心架构 + 数据库模型
|
||
|
||
**Goal:** 建立 DocumentService 骨架和文档元数据持久化基础。
|
||
|
||
**Requirements:** R11, R13, R14, R15, R16
|
||
|
||
**Dependencies:** 无
|
||
|
||
**Files:**
|
||
- `src/agentkit/documents/__init__.py`(新建)
|
||
- `src/agentkit/documents/service.py`(新建)
|
||
- `src/agentkit/documents/models.py`(新建)
|
||
- `src/agentkit/documents/db.py`(新建)
|
||
- `pyproject.toml`(修改:添加 python-docx, openpyxl, reportlab, docxtpl, jinja2 依赖)
|
||
|
||
**Approach:**
|
||
- `DocumentService` 类:`create_document(format, content, conversation_id, template_path?) -> DocumentMeta`、`get_conversation_documents(conversation_id) -> list[DocumentMeta]`、`get_download_path(doc_id) -> Path`
|
||
- `DocumentMeta` dataclass:`id, filename, stored_name, format, size, conversation_id, created_at, download_url`
|
||
- 数据库表 `documents`:id (UUID), filename, stored_name, format, size, conversation_id, created_at。用 aiosqlite 裸连接,`init_documents_db()` 建表。
|
||
- 文件存储:UUID + 扩展名,存到 `data/uploads/`,复用 `_sanitize_filename`。
|
||
|
||
**Patterns to follow:** `src/agentkit/server/auth/models.py`(aiosqlite 模式)、`src/agentkit/server/routes/chat.py` 的 `_sanitize_filename` 函数。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 创建文档元数据记录,查询返回正确数据
|
||
- Edge case: 不存在的 conversation_id 返回空列表
|
||
- Edge case: 文件名包含路径遍历字符(../)被清洗
|
||
- Integration: init_documents_db 幂等(重复调用不报错)
|
||
|
||
**Verification:** 运行 `pytest tests/documents/test_db.py`,确认元数据 CRUD 和文件存储正常。
|
||
|
||
---
|
||
|
||
### U2. Word 文档创建(python-docx + Markdown→Word 映射)
|
||
|
||
**Goal:** 实现 Markdown→Word 的格式映射,Agent 生成 Markdown 内容,DocumentService 生成 .docx 文件。
|
||
|
||
**Requirements:** R1
|
||
|
||
**Dependencies:** U1
|
||
|
||
**Files:**
|
||
- `src/agentkit/documents/renderers/__init__.py`(新建)
|
||
- `src/agentkit/documents/renderers/word_renderer.py`(新建)
|
||
- `tests/documents/test_word_renderer.py`(新建)
|
||
|
||
**Approach:**
|
||
- `WordRenderer.render(markdown_content: str, output_path: Path) -> Path`
|
||
- Markdown 解析:用 `markdown` 库解析为 AST,遍历 AST 映射到 python-docx 对象:
|
||
- `# 标题` → `doc.add_heading(text, level=1)`
|
||
- `## 二级标题` → `doc.add_heading(text, level=2)`
|
||
- 段落 → `doc.add_paragraph(text)`
|
||
- `- 列表项` → `doc.add_paragraph(text, style='List Bullet')`
|
||
- `1. 有序列表` → `doc.add_paragraph(text, style='List Number')`
|
||
- Markdown 表格 → `doc.add_table(rows, cols)` + 填充
|
||
- `**粗体**` → run with `bold=True`
|
||
- `*斜体*` → run with `italic=True`
|
||
|
||
**Patterns to follow:** python-docx 官方文档的基本用法。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 包含标题、段落、列表、表格的 Markdown 生成正确的 .docx
|
||
- Edge case: 空 Markdown 生成空文档(只有标题或完全空)
|
||
- Edge case: 嵌套格式(粗体+斜体混合)正确渲染
|
||
- Error path: 无效 Markdown 不崩溃,按纯文本处理
|
||
|
||
**Verification:** 运行 `pytest tests/documents/test_word_renderer.py`,打开生成的 .docx 确认格式正确。
|
||
|
||
---
|
||
|
||
### U3. Excel 文档创建(openpyxl + Markdown 表格→Excel 映射)
|
||
|
||
**Goal:** 实现 Markdown 表格/JSON→Excel 的格式映射。
|
||
|
||
**Requirements:** R2
|
||
|
||
**Dependencies:** U1
|
||
|
||
**Files:**
|
||
- `src/agentkit/documents/renderers/excel_renderer.py`(新建)
|
||
- `tests/documents/test_excel_renderer.py`(新建)
|
||
|
||
**Approach:**
|
||
- `ExcelRenderer.render(markdown_content: str, output_path: Path) -> Path`
|
||
- 解析 Markdown 中的表格(`| col1 | col2 |` 格式),每个表格映射到一个 worksheet
|
||
- 非表格文本(标题、段落)作为注释行或单独的 "Summary" sheet
|
||
- 支持 JSON 格式输入:`{"Sheet1": [["A1","B1"],["A2","B2"]]}`(当 content 是有效 JSON 时走 JSON 路径)
|
||
|
||
**Patterns to follow:** openpyxl 官方文档的基本用法。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: Markdown 表格生成正确的 .xlsx,数据对齐
|
||
- Happy path: JSON 格式输入生成多 sheet Excel
|
||
- Edge case: 无表格的 Markdown 生成单 sheet 纯文本
|
||
- Edge case: 多个表格生成多个 sheet
|
||
|
||
**Verification:** 运行 `pytest tests/documents/test_excel_renderer.py`,打开生成的 .xlsx 确认数据正确。
|
||
|
||
---
|
||
|
||
### U4. PDF 文档创建(reportlab + Markdown→PDF 映射)
|
||
|
||
**Goal:** 实现 Markdown→PDF 的格式映射,使用 reportlab 生成 PDF。
|
||
|
||
**Requirements:** R3
|
||
|
||
**Dependencies:** U1
|
||
|
||
**Files:**
|
||
- `src/agentkit/documents/renderers/pdf_renderer.py`(新建)
|
||
- `tests/documents/test_pdf_renderer.py`(新建)
|
||
|
||
**Approach:**
|
||
- `PDFRenderer.render(markdown_content: str, output_path: Path) -> Path`
|
||
- 用 reportlab 的 `SimpleDocTemplate` + `Paragraph` + `Table` + `ListFlowable`
|
||
- Markdown 解析同 U2,映射到 reportlab flowables:
|
||
- `# 标题` → `Paragraph(text, Heading1 style)`
|
||
- 段落 → `Paragraph(text, Normal style)`
|
||
- 列表 → `ListFlowable([ListItem(...)])`
|
||
- 表格 → `Table(data)` + 基础样式
|
||
- `**粗体**` → `<b>text</b>`(reportlab Paragraph 支持 HTML 标签)
|
||
|
||
**Patterns to follow:** reportlab 官方文档。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 包含标题、段落、列表、表格的 Markdown 生成正确的 PDF
|
||
- Edge case: 空 Markdown 生成空白 PDF
|
||
- Edge case: 中文字符正确渲染(需注册中文字体)
|
||
- Error path: 无效 Markdown 不崩溃
|
||
|
||
**Verification:** 运行 `pytest tests/documents/test_pdf_renderer.py`,打开生成的 PDF 确认格式和中文渲染。
|
||
|
||
---
|
||
|
||
### U5. Word 模板填充(python-docx-template + Jinja2 沙箱)
|
||
|
||
**Goal:** 实现 Word 模板填充,用户上传 .docx 模板,Agent 提供数据,填充 Jinja2 占位符。
|
||
|
||
**Requirements:** R23, R24, R25, R26
|
||
|
||
**Dependencies:** U1, U2
|
||
|
||
**Files:**
|
||
- `src/agentkit/documents/renderers/template_renderer.py`(新建)
|
||
- `tests/documents/test_template_renderer.py`(新建)
|
||
|
||
**Approach:**
|
||
- `TemplateRenderer.render(template_path: Path, data: dict, output_path: Path) -> Path`
|
||
- 用 `docxtpl.DocxTemplate(template_path)` 加载模板
|
||
- 用 `jinja2.sandbox.SandboxedEnvironment` 创建沙箱环境
|
||
- `template.render(data)` 填充数据
|
||
- 支持 `{{variable}}`、`{% if %}`、`{% for %}` 基本控制结构
|
||
|
||
**Patterns to follow:** python-docx-template 官方文档。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 模板包含 `{{name}}`,data=`{"name":"张三"}`,输出文档中 "张三" 替换占位符
|
||
- Happy path: `{% for item in items %}` 循环正确展开
|
||
- Happy path: `{% if condition %}` 条件渲染正确
|
||
- Security: SSTI 攻击 payload(`{{config.__class__}}`)被沙箱拦截
|
||
- Edge case: 模板无占位符时原样输出
|
||
- Error path: data 缺少变量时,占位符保持原样或清空(不崩溃)
|
||
|
||
**Verification:** 运行 `pytest tests/documents/test_template_renderer.py`,确认填充和沙箱安全。
|
||
|
||
---
|
||
|
||
### U6. Agent 工具封装(DocumentTool)
|
||
|
||
**Goal:** 创建 Agent 工具,LLM 通过 function calling 触发文档创建。
|
||
|
||
**Requirements:** R5, R6, R7, R8
|
||
|
||
**Dependencies:** U1, U2, U3, U4, U5
|
||
|
||
**Files:**
|
||
- `src/agentkit/tools/document_tool.py`(新建)
|
||
- `src/agentkit/server/app.py`(修改:注册 DocumentTool)
|
||
- `tests/tools/test_document_tool.py`(新建)
|
||
|
||
**Approach:**
|
||
- `DocumentTool(service: DocumentService)` 继承 `Tool`
|
||
- `name = "document"`,`description = "创建格式化文档(Word/Excel/PDF)或填充 Word 模板"`
|
||
- `input_schema`:
|
||
```json
|
||
{
|
||
"type": "object",
|
||
"properties": {
|
||
"format": {"type": "string", "enum": ["word", "excel", "pdf"]},
|
||
"content": {"type": "string", "description": "Markdown 格式的文档内容"},
|
||
"template": {"type": "string", "description": "模板文件路径(可选,仅 word)"},
|
||
"template_data": {"type": "object", "description": "模板填充数据(可选)"}
|
||
},
|
||
"required": ["format", "content"]
|
||
}
|
||
```
|
||
- `execute()` 调用 `service.create_document()`,返回 `{"success": True, "filename": ..., "download_url": ..., "size": ...}`
|
||
- 在 `app.py` 中注册:`tool_registry.register(DocumentTool(service=document_service))`
|
||
|
||
**Patterns to follow:** `src/agentkit/tools/memory_tool.py`(Tool 基类模式、input_schema、execute 返回格式)。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: format=word, content="# 标题\n段落" → 返回 success + download_url
|
||
- Happy path: format=pdf, content="..." → 返回 success + download_url
|
||
- Happy path: format=word + template + template_data → 模板填充成功
|
||
- Error path: format 无效 → 返回 success=False + error message
|
||
- Error path: content 为空 → 返回 success=False + error message
|
||
- Integration: 工具注册后 agent._tool_registry.get("document") 能获取到
|
||
|
||
**Verification:** 运行 `pytest tests/tools/test_document_tool.py`,确认工具注册和调用正常。
|
||
|
||
---
|
||
|
||
### U7. REST API 路由
|
||
|
||
**Goal:** 为前端提供文档处理的 REST API。
|
||
|
||
**Requirements:** R9, R10, R12, R27, R28
|
||
|
||
**Dependencies:** U1, U2, U3, U4, U5
|
||
|
||
**Files:**
|
||
- `src/agentkit/server/routes/documents.py`(新建)
|
||
- `src/agentkit/server/app.py`(修改:注册 documents router)
|
||
- `tests/routes/test_documents.py`(新建)
|
||
|
||
**Approach:**
|
||
- `router = APIRouter(prefix="/documents", tags=["documents"])`
|
||
- 端点:
|
||
- `POST /api/v1/documents/create` — 创建文档(body: format, content, conversation_id, template?)
|
||
- `POST /api/v1/documents/upload-template` — 上传模板文件(带认证)
|
||
- `GET /api/v1/documents/conversation/{conversation_id}` — 获取对话的文档列表
|
||
- `GET /api/v1/documents/download/{doc_id}` — 下载文档(带认证)
|
||
- 认证:复用 `Depends(_verify_api_key)` 模式
|
||
- 文件大小限制:50MB
|
||
|
||
**Patterns to follow:** `src/agentkit/server/routes/chat.py`(APIRouter 模式、文件上传/下载)、`src/agentkit/server/routes/kb_management.py`(认证模式)。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: POST /create format=word → 200 + 文件元信息
|
||
- Happy path: GET /conversation/{id} → 200 + 文档列表
|
||
- Happy path: GET /download/{doc_id} → 200 + 文件流
|
||
- Security: 未认证请求 → 401
|
||
- Edge case: 不存在的 doc_id → 404
|
||
- Edge case: 文件超过 50MB → 413
|
||
|
||
**Verification:** 运行 `pytest tests/routes/test_documents.py`,用 curl 验证端点。
|
||
|
||
---
|
||
|
||
### U8. 前端文件卡片 + 右侧文档面板
|
||
|
||
**Goal:** 对话中渲染文件卡片,右侧面板展示当前对话的文档列表。
|
||
|
||
**Requirements:** R17, R18, R19, R20, R21, R22
|
||
|
||
**Dependencies:** U7
|
||
|
||
**Files:**
|
||
- `src/agentkit/server/frontend/src/components/chat/messages/DocumentCard.vue`(新建)
|
||
- `src/agentkit/server/frontend/src/components/chat/DocumentPanel.vue`(新建,右侧面板)
|
||
- `src/agentkit/server/frontend/src/stores/documents.ts`(新建,Pinia store)
|
||
- `src/agentkit/server/frontend/src/api/documents.ts`(新建,API client)
|
||
- `src/agentkit/server/frontend/src/views/ChatView.vue`(修改:集成右侧面板)
|
||
- `src/agentkit/server/frontend/src/stores/chat.ts`(修改:token 事件中检测文件元信息并更新 documents store)
|
||
|
||
**Approach:**
|
||
- `DocumentCard.vue`:复用 `FileAttachment.vue` 的设计,显示文件名、格式图标、大小、下载按钮。作为新的消息渲染类型。
|
||
- `DocumentPanel.vue`:右侧可折叠面板,展示当前对话的文档列表,每项显示文件名、格式图标、生成时间、下载链接。
|
||
- `stores/documents.ts`:`documentsByConversation: ref<Map<string, DocumentMeta[]>>`,`fetchDocuments(convId)`,`addDocument(convId, doc)`。
|
||
- `api/documents.ts`:`createDocument()`、`getConversationDocuments()`、`getDownloadUrl()`。
|
||
- ChatView 集成:在聊天区域右侧添加 DocumentPanel,根据当前 conversationId 加载文档列表。
|
||
- chat store 集成:当 Agent 工具返回文件元信息时,自动更新 documents store。
|
||
|
||
**Patterns to follow:** `src/agentkit/server/frontend/src/components/chat/messages/FileAttachment.vue`(组件模式)、`src/agentkit/server/frontend/src/stores/chat.ts`(Pinia store 模式)、`src/agentkit/server/frontend/src/api/client.ts`(API client 模式)。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: Agent 生成文档后,对话中显示文件卡片
|
||
- Happy path: 右侧面板自动更新,显示新文档
|
||
- Happy path: 点击下载按钮,浏览器下载文件
|
||
- Happy path: 切换对话,面板显示对应对话的文档列表
|
||
- UI: 面板可折叠/展开
|
||
- Edge case: 对话无文档时,面板显示空状态
|
||
|
||
**Verification:** 启动前端开发服务器,手动测试文件卡片渲染和右侧面板交互。
|
||
|
||
---
|
||
|
||
### U9. 文档读取能力(复用 DocumentLoader)
|
||
|
||
**Goal:** Agent 能读取用户上传的 Word/Excel/PDF 文档内容。
|
||
|
||
**Requirements:** R4
|
||
|
||
**Dependencies:** U1
|
||
|
||
**Files:**
|
||
- `src/agentkit/tools/document_tool.py`(修改:添加 read 操作)
|
||
- `src/agentkit/memory/document_loader.py`(修改:确保 openpyxl 读取支持,或新增 Excel 读取)
|
||
|
||
**Approach:**
|
||
- DocumentTool 的 input_schema 新增 `action` 参数:`"create"` | `"read"`
|
||
- `action="read"` 时,调用 `DocumentLoader.load(path)` 读取文档内容
|
||
- DocumentLoader 已支持 PDF(PyMuPDF/pdfplumber)和 DOCX(python-docx),需新增 Excel 读取(openpyxl)
|
||
- 返回 `{"success": True, "content": "提取的文本内容"}`
|
||
|
||
**Patterns to follow:** `src/agentkit/memory/document_loader.py`(现有解析模式)。
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 读取 .docx 文件,返回文本内容
|
||
- Happy path: 读取 .xlsx 文件,返回表格内容
|
||
- Happy path: 读取 .pdf 文件,返回文本内容
|
||
- Edge case: 空文件返回空字符串
|
||
- Error path: 不存在的文件返回 success=False
|
||
|
||
**Verification:** 运行 `pytest tests/tools/test_document_tool.py`,确认读取功能正常。
|
||
|
||
---
|
||
|
||
## Scope Boundaries
|
||
|
||
### Deferred to Follow-Up Work
|
||
|
||
- PPT 创建(.pptx)— v2
|
||
- 格式转换(Office→PDF)— v2,可能需要 LibreOffice
|
||
- PDF 合并和拆分 — v2
|
||
- Excel/PPT 模板填充 — v2
|
||
- 文档编辑 — v2
|
||
- MCP Document Tools 集成(可选增强)— v2
|
||
- 文档过期清理的定时任务实现 — v2(v1 手动清理或懒清理)
|
||
|
||
### Outside this product's identity
|
||
|
||
- OCR / 扫描文档识别
|
||
- 文档协作编辑
|
||
- 文档版本控制
|
||
- 云存储集成
|
||
- 文档水印 / 加密 / 数字签名
|
||
|
||
---
|
||
|
||
## Risks & Dependencies
|
||
|
||
- **Markdown→Office 格式映射的完整性** — Markdown 不能表达所有 Office 格式(如合并单元格、图片嵌入)。v1 只支持基本格式(标题、段落、列表、表格),复杂格式 defer。
|
||
- **中文字体在 PDF 中的渲染** — reportlab 默认不支持中文,需注册中文字体(如 SimSun 或 NotoSansCJK)。需确认服务器有中文字体文件。
|
||
- **python-docx-template 的 Jinja2 语法限制** — Office XML 结构中 Jinja2 语法可能受限(如表格内的循环)。需测试复杂模板。
|
||
- **前端右侧面板的布局影响** — 现有 ChatView 布局可能需要调整以容纳右侧面板,需确认不破坏现有聊天 UI。
|
||
|
||
---
|
||
|
||
## Sources & Research
|
||
|
||
- 需求文档:`docs/brainstorms/2026-06-23-document-processing-capability-requirements.md`
|
||
- Tool 基类:`src/agentkit/tools/base.py`、`src/agentkit/tools/memory_tool.py`
|
||
- ToolRegistry:`src/agentkit/tools/registry.py`、`src/agentkit/server/app.py`(第 239-269 行)
|
||
- 路由模式:`src/agentkit/server/routes/chat.py`、`src/agentkit/server/routes/kb_management.py`
|
||
- 数据库模式:`src/agentkit/server/auth/models.py`(aiosqlite 裸连接模式)
|
||
- 前端组件:`src/agentkit/server/frontend/src/components/chat/messages/FileAttachment.vue`
|
||
- 前端 store:`src/agentkit/server/frontend/src/stores/chat.ts`
|
||
- 文档解析:`src/agentkit/memory/document_loader.py`
|
||
- MCP Document Tools 验证报告:版本 0.1.0,未验证,不建议生产使用,不支持模板填充
|