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:
@@ -60,6 +60,10 @@ func (g *Gateway) dispatch(ctx context.Context, call *contract.ToolCall) *contra
|
||||
return g.kbGraph(ctx, call)
|
||||
case "report_render":
|
||||
return g.reportRender(ctx, call)
|
||||
case "report_store":
|
||||
return g.reportStore(ctx, call)
|
||||
case "report_export":
|
||||
return g.reportExport(ctx, call)
|
||||
case "health":
|
||||
data, _ := json.Marshal(g.rag.Status())
|
||||
return &contract.ToolResult{OK: true, Content: string(data)}
|
||||
@@ -213,6 +217,90 @@ func (g *Gateway) reportRender(ctx context.Context, call *contract.ToolCall) *co
|
||||
return &contract.ToolResult{OK: true, Content: path}
|
||||
}
|
||||
|
||||
// reportSource 是报告的可序列化源数据(标题 + 章节),导出时据此渲染各格式。
|
||||
type reportSource struct {
|
||||
Title string `json:"title"`
|
||||
Sections []office.Section `json:"sections"`
|
||||
}
|
||||
|
||||
// reportStore 把报告源数据(title + sections)落盘为 JSON,供导出时按需渲染 Word/PDF/Markdown。
|
||||
// 生成阶段只存源、不渲染("导出时再处理")。
|
||||
func (g *Gateway) reportStore(_ context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
id, _ := call.Args["task_id"].(string)
|
||||
if id == "" {
|
||||
id = call.TaskID
|
||||
}
|
||||
if id == "" {
|
||||
return &contract.ToolResult{OK: false, Error: "report_store: task_id 必填"}
|
||||
}
|
||||
title, _ := call.Args["title"].(string)
|
||||
var secs []office.Section
|
||||
if raw, err := json.Marshal(call.Args["sections"]); err == nil {
|
||||
_ = json.Unmarshal(raw, &secs)
|
||||
}
|
||||
data, _ := json.Marshal(reportSource{Title: title, Sections: secs})
|
||||
path := contract.ReportSourcePath(id)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_store: mkdir " + err.Error()}
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_store: write " + err.Error()}
|
||||
}
|
||||
log.Printf("[mcp_go] report_store 已存源 %s (%d 章节)", path, len(secs))
|
||||
return &contract.ToolResult{OK: true, Content: path}
|
||||
}
|
||||
|
||||
// reportExport 按需把已存报告源渲染为指定格式:
|
||||
// docx → 渲染并落盘,返回 .docx 路径;md → 返回 Markdown 文本。
|
||||
func (g *Gateway) reportExport(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
id, _ := call.Args["task_id"].(string)
|
||||
if id == "" {
|
||||
id = call.TaskID
|
||||
}
|
||||
if id == "" {
|
||||
return &contract.ToolResult{OK: false, Error: "report_export: task_id 必填"}
|
||||
}
|
||||
format, _ := call.Args["format"].(string)
|
||||
raw, err := os.ReadFile(contract.ReportSourcePath(id))
|
||||
if err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_export: 报告尚未生成或已过期"}
|
||||
}
|
||||
var src reportSource
|
||||
if err := json.Unmarshal(raw, &src); err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_export: 源解析失败"}
|
||||
}
|
||||
switch format {
|
||||
case "md", "markdown":
|
||||
return &contract.ToolResult{OK: true, Content: reportMarkdown(src)}
|
||||
default: // docx
|
||||
data, rerr := office.NewRenderer().RenderReport(ctx, src.Title, src.Sections)
|
||||
if rerr != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_export: " + rerr.Error()}
|
||||
}
|
||||
path := contract.ReportPath(id)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "report_export: write " + err.Error()}
|
||||
}
|
||||
log.Printf("[mcp_go] report_export 已渲染 docx %s (%d 字节)", path, len(data))
|
||||
return &contract.ToolResult{OK: true, Content: path}
|
||||
}
|
||||
}
|
||||
|
||||
// reportMarkdown 把报告源拼为 Markdown(标题 + 各章 ## 小标题 + 正文)。
|
||||
func reportMarkdown(src reportSource) string {
|
||||
var b strings.Builder
|
||||
if src.Title != "" {
|
||||
b.WriteString("# " + src.Title + "\n\n")
|
||||
}
|
||||
for _, s := range src.Sections {
|
||||
if s.Heading != "" {
|
||||
b.WriteString("## " + s.Heading + "\n\n")
|
||||
}
|
||||
b.WriteString(strings.TrimSpace(s.Body) + "\n\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// kbIngest 把文本入库(切块→embedding→Milvus+Bleve)。
|
||||
// 带 job_id 时逐阶段把进度发到 sundynix.streams.<job_id>,供 UI 实时入库监控。
|
||||
func (g *Gateway) kbIngest(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
|
||||
Reference in New Issue
Block a user