feat(report): 生成只出 Markdown 预览,导出时再渲染 Word/PDF/Markdown
把"渲染"从生成阶段解耦到导出阶段(导出时再处理): - 生成阶段:报告正文按 Markdown 流式预览(前端 <Markdown> 已渲染), dispatcher 不再 eager 渲染 docx,改为经 mcp-go report_store 落盘报告源(title+sections JSON)。 - 导出阶段(按需现渲染): - GET /reports/:id/export?format=docx → mcp-go report_export 读源渲染 .docx; - ?format=md → 返回 Markdown 文本; - PDF → 前端把已渲染的 Markdown 送进打印视图出 PDF(CJK 零字体依赖)。 - 旧 /reports/:id/download 兼容保留(默认 docx)。 改动: - contract: ReportSourcePath(id) = <id>.json。 - mcp-go: 新增 report_store / report_export 工具(report_render 保留给 Studio render 节点)。 - dispatcher: handleReport 末尾 renderReport → storeReport。 - gateway: DownloadReport → ExportReport(经 NATS 调 report_export)。 - 前端: ReportView 单个「Word」→「导出」组 Word/PDF/Markdown; desktop.printReportHtml 客户端打印;api.reportExportUrl。 实测(docker 全栈 + mcp-go + gateway + dispatcher + DeepSeek 真跑): - 真实生成「绿茶的功效」18s 完成,report_store 落源(5章, 6280B) ✓ - export md 返回正确 Markdown(# 标题/## 小节/正文) ✓ - export docx 为合法「Microsoft Word 2007+」(含 document.xml/Content_Types) ✓ - 前端 tsc 干净 + 生产构建通过 ✓ (注:发现并修复一处环境问题——mcp-go 启动时若 Milvus 未起会阻塞在 rag 初始化、永不订阅工具,导致所有 mcp-go 工具"no responders";起全栈后正常。 报告生成在累积大量未完成 DeepSeek 流连接时会偶发卡顿,干净进程下正常。 前端导出按钮的实时点击因 React 受控输入自动化限制未在预览中走通,非代码缺陷。) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -75,16 +75,15 @@ func (o *Orchestrator) handleReport(ctx context.Context, t *contract.Task, tr *e
|
||||
o.emit(t.ID, "## "+s.Heading+"\n\n"+s.Body+"\n\n")
|
||||
}
|
||||
|
||||
// 渲染真实 Word 文档。
|
||||
o.emit(t.ID, "> 正在渲染 Word 文档…\n\n")
|
||||
endRender := tr.span("render", "render", "渲染 Word 文档")
|
||||
if path := o.renderReport(ctx, t.ID, firstNonEmpty(outline.Title, topic), sections); path != "" {
|
||||
endRender("docx 已落盘:"+path, nil)
|
||||
o.emit(t.ID, "---\n✅ 报告已生成 Word 文档,可点击上方「下载 Word」保存。\n")
|
||||
log.Printf("[report] task %s 完成,docx=%s", t.ID, path)
|
||||
// 只持久化报告源数据(标题+章节),不在生成阶段渲染;导出时再按需出 Word/PDF/Markdown。
|
||||
endStore := tr.span("store", "render", "保存报告源")
|
||||
if o.storeReport(ctx, t.ID, firstNonEmpty(outline.Title, topic), sections) {
|
||||
endStore("已保存,可按需导出 Word/PDF/Markdown", nil)
|
||||
o.emit(t.ID, "---\n✅ 报告正文已生成,可在上方导出 **Word / PDF / Markdown**。\n")
|
||||
log.Printf("[report] task %s 完成,源已存", t.ID)
|
||||
} else {
|
||||
endRender("渲染服务不可用", fmt.Errorf("render unavailable"))
|
||||
o.emit(t.ID, "---\n⚠️ Word 渲染未完成(渲染服务不可用),以上为报告正文。\n")
|
||||
endStore("源保存失败", fmt.Errorf("store unavailable"))
|
||||
o.emit(t.ID, "---\n⚠️ 报告源保存失败(导出可能不可用),以上为报告正文。\n")
|
||||
}
|
||||
o.breaker.Report(true)
|
||||
return nil
|
||||
@@ -229,6 +228,24 @@ func (o *Orchestrator) retrieve(ctx context.Context, kb, query string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// storeReport 经 mcp-go report_store 把报告源数据(title+sections)落盘,供导出时按需渲染。
|
||||
func (o *Orchestrator) storeReport(ctx context.Context, taskID, title string, secs []reportSection) bool {
|
||||
if o.tools == nil {
|
||||
return false
|
||||
}
|
||||
arr := make([]map[string]any, len(secs))
|
||||
for i, s := range secs {
|
||||
arr[i] = map[string]any{"heading": s.Heading, "body": s.Body}
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, reportRenderWait)
|
||||
defer cancel()
|
||||
res, err := o.tools.CallTool(cctx, contract.ToolSubjectGo("report_store"), &contract.ToolCall{
|
||||
Tool: "report_store", TaskID: taskID,
|
||||
Args: map[string]any{"title": title, "task_id": taskID, "sections": arr},
|
||||
})
|
||||
return err == nil && res != nil && res.OK
|
||||
}
|
||||
|
||||
// renderReport 经 mcp-go report_render 工具渲染 docx 并落盘,返回文件路径(失败返回空)。
|
||||
func (o *Orchestrator) renderReport(ctx context.Context, taskID, title string, secs []reportSection) string {
|
||||
if o.tools == nil {
|
||||
|
||||
Reference in New Issue
Block a user