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,64 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-shared/contract"
)
// GenerateReport: POST /api/v1/reports —— 触发报告生成。
// 组装一个 intent=report 的任务发到 NATSDispatcher 走专用编排(规划→分章并行→渲染 docx)。
// 返回 task_id;客户端用 GET /tasks/:id/stream 看实时进度,完成后用 /reports/:id/download 取 Word。
func (h *Handler) GenerateReport(c *gin.Context) {
var body struct {
Topic string `json:"topic"`
KB string `json:"kb"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Topic == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "topic required"})
return
}
id := newReportID()
graph, _ := json.Marshal(map[string]any{"topic": body.Topic}) // 占位 DSL,报告编排实际读 Meta
task := &contract.Task{
ID: id,
Graph: graph,
Meta: map[string]any{
contract.MetaIntent: contract.IntentReport,
contract.MetaTopic: body.Topic,
contract.MetaKB: body.KB,
contract.MetaUserID: userID(c),
contract.MetaSessionID: sessionID(c),
},
}
if err := h.bus.PublishTask(c.Request.Context(), task); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"task_id": id})
}
// DownloadReport: GET /api/v1/reports/:id/download —— 下载已渲染的 Word 文档。
func (h *Handler) DownloadReport(c *gin.Context) {
id := c.Param("id")
path := contract.ReportPath(id)
if _, err := os.Stat(path); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "报告尚未生成或已过期"})
return
}
c.Header("Content-Disposition", `attachment; filename="`+id+`.docx"`)
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
c.File(path)
}
func newReportID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return "report_" + hex.EncodeToString(b[:])
}
@@ -28,6 +28,9 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus) *gin.Engine {
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(实时监控)
api.POST("/kb/search", h.KbSearch) // 知识库检索台(→ mcp-go kb_search
api.GET("/kb/graph", h.KbGraph) // 知识图谱三元组(→ mcp-go kb_graphNeo4j
api.POST("/reports", h.GenerateReport) // 报告生成(intent=report 任务 → Dispatcher 专用编排)
api.GET("/reports/:id/download", h.DownloadReport) // 下载渲染好的 Word(.docx)
api.GET("/billing", h.Billing)
// 运维控制面:LLM 模型配置(独立运维控制台调用)。