feat(report): 报告生成端到端 — 规划→分章并行检索撰写→渲染真实 Word

- shared: 新增 intent=report 任务约定 + ReportPath(跨进程共享落盘目录,零配置对齐)
- dispatcher: handleReport 专用编排(DeepSeek 规划大纲 → 各章并行 RAG 检索+撰写
  → 汇聚 → report_render),Pool.Chat 非流式聚合;进度与正文经 Token 流实时回流
- mcp-go: 用标准库 archive/zip + OOXML 拼出真实可打开的 .docx(零额外依赖),
  report_render 工具落盘到共享目录;附 docx 有效性测试
- gateway: POST /reports 触发;GET /reports/:id/download 下发 Word
- desktop: 新增「报告」页(主题→实时编排进度→下载 Word),左导航置为就绪

实测:DeepSeek 生成 5 章报告 → 渲染 5KB docx → file 识别为 Microsoft Word 2007+
→ textutil 提取标题/各章正文完整。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-12 14:02:21 +08:00
parent 8469cfc0db
commit ba8c6b3c43
15 changed files with 744 additions and 10 deletions
@@ -0,0 +1,56 @@
package office
import (
"archive/zip"
"bytes"
"context"
"strings"
"testing"
)
func TestRenderReportValidDocx(t *testing.T) {
data, err := NewRenderer().RenderReport(context.Background(), "测试报告 <X&Y>", []Section{
{Heading: "第一章 背景", Body: "这是第一段。\n这是第二段,含特殊字符 < > &。"},
{Heading: "第二章 分析", Body: "结论行。"},
})
if err != nil {
t.Fatal(err)
}
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Fatalf("not a valid zip: %v", err)
}
want := map[string]bool{"[Content_Types].xml": false, "_rels/.rels": false, "word/document.xml": false}
var docXML string
for _, f := range zr.File {
if _, ok := want[f.Name]; ok {
want[f.Name] = true
}
if f.Name == "word/document.xml" {
rc, _ := f.Open()
var b strings.Builder
buf := make([]byte, 4096)
for {
n, e := rc.Read(buf)
b.Write(buf[:n])
if e != nil {
break
}
}
rc.Close()
docXML = b.String()
}
}
for name, found := range want {
if !found {
t.Errorf("missing part %s", name)
}
}
if !strings.Contains(docXML, "&lt; &gt; &amp;") {
t.Errorf("xml not escaped properly: %s", docXML)
}
if !strings.Contains(docXML, "第一章 背景") {
t.Errorf("heading missing")
}
t.Logf("docx ok: %d bytes, %d parts", len(data), len(zr.File))
}