refactor: 后端仓库层收口(A1:课程/产品/素材)+ 收进工作区既有对象化重构

本提交含两部分。第一部分是本轮工作;第二部分是此前一直留在工作区、
从未提交的对象化重构,与第一部分在文件上互相咬合(internal/repository
整个包都是未跟踪状态,且 api 层已有文件引用它),无法拆成两个可编译的提交。

一、仓库层收口 A1 批(本轮工作)

把 api 层手写的 store.DB 查询收进具名仓库方法,只给真正获益的对象做方法,
不机械包裹全量。本批迁移 22 处裸查询(courses.go 9 / media.go 12 / products.go 1),
新增方法:

- MediaFileRepo.ListByBind / ListForAudit / MarkExtracted
- KnowledgeChunkRepo.CountByMediaFile
- ProductRepo.GetVisibleByID

两条业务口径改由仓库单点持有,避免各处手写漂移:
「只有 approved 素材出现在课程详情」与「已停用产品不在课程详情露出」。

修掉两个真实缺陷:
- ProductRepo.GetByID 缺 Where 条件。此前 GET /api/products/{id} 对任意 id 都返回
  第一条产品、对不存在的 id 返回 200,且 PUT /api/products/{id} 会覆盖第一条产品
  —— 数据损坏级。全仓扫描确认这是唯一一处同型写法。
- ProductRepo.Delete 写 status="deleted",而 DELETE 处理器文档与回包都声称
  "inactive",接口在说谎;管理员用 status=all 拉列表会看到前端不认识的状态。
  已对齐为 inactive(与 CourseRepo.Delete 一致)。

删除 8 个零调用且列名不存在的死方法(一调即 SQL 报错):
- media_file 上的 file_path / file_type / approval_status 三列并不存在,
  GetByPath / ListByType / UpdateStatus 全废
- knowledge_chunk 上的 space_id 列不存在(模型早已改为 knowledge_space_key),
  List / Total / ListBySpaceIDs / DeleteBySpace / SearchByVector 全废
取舍边界:能对当前 schema 跑通的死方法保留,跑不通的删或修。

CourseRepo.List 补齐 status=all 档(此前传给它会当作 status='all' 过滤出空列表)。
该方法此前零调用,现与产品列表语义对齐。

验证:go build ./... 与 go test ./... 全绿;另用真实 HTTP 请求验证 34 项
(课程 17 / 产品 3 / 素材 14),跑在数据库副本与独立 KB_DATA_DIR 上,
含 multipart 真上传 → 审批 → pdftotext 提取 → 分片入库的完整链路。

二、此前未提交的对象化重构(非本轮工作)

- 新增 internal/repository 仓库层、connectors、skills、specialists、xapps、jsonutil,
  model/task_record|task_run|task_artifact、api/task_runtime|action_definition|chat_message
- 删除 api/app_definition、connectors、my_app_center、notification、office_skill、
  export_docx|pptx|xlsx、official_account_* 等,随 XApp/Skill/Specialist/Connector
  可插拔打包方向(AR10/AR11)调整
- 资产目录归位:backend-go/knowledge_source → assets/knowledge/source、
  training_materials → assets/training/materials;README 内相对路径同步加深两级;
  deploy env 补 ASSET_ROOT_DIR 并改 KNOWLEDGE_SOURCE_DIR / TRAINING_MATERIALS_DIR
- 前端新增 skills/ specialists/ connectors/ xapps/ 目录与对应页面

验证:前端 npm run build 通过(7.26s)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-19 01:23:51 +08:00
co-authored by Claude Code
parent ddd2d2cbd8
commit 14f303459e
448 changed files with 18792 additions and 17027 deletions
@@ -0,0 +1,11 @@
package audiotranscribe
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "audio-transcribe",
Label: "语音转写",
OwnedDefinitionKeys: []string{"skill.audio-transcribe.definition"},
OwnedCatalogEntries: []string{"catalog.skill.audio-transcribe"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package batchextract
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "batch-extract",
Label: "批量提取",
OwnedDefinitionKeys: []string{"skill.batch-extract.definition"},
OwnedCatalogEntries: []string{"catalog.skill.batch-extract"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package contractbrief
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "contract-brief",
Label: "合同要点提炼",
OwnedDefinitionKeys: []string{"skill.contract-brief.definition"},
OwnedCatalogEntries: []string{"catalog.skill.contract-brief"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package contractbrief
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "contract-brief",
Label: "合同要点提炼",
PlanTitle: "合同要点提炼流程",
PlanSteps: []string{"识别合同结构", "提炼核心条款", "标记风险事项", "输出摘要版本"},
ResultTitle: "合同要点摘要",
Summary: "已提炼出合同核心条款、主要风险和待确认事项,方便快速预读和继续审查。",
Highlights: []string{"适合采购、合作、服务类合同", "自动压缩核心条款与风险点", "默认沉淀摘要、风险和待确认项"},
ActionType: "summary",
}
@@ -0,0 +1,11 @@
package contractreview
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "contract-review",
Label: "合同审查",
OwnedDefinitionKeys: []string{"skill.contract-review.definition"},
OwnedCatalogEntries: []string{"catalog.skill.contract-review"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package copyproofreading
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "copy-proofreading",
Label: "文案校对",
OwnedDefinitionKeys: []string{"skill.copy-proofreading.definition"},
OwnedCatalogEntries: []string{"catalog.skill.copy-proofreading"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package documenttranslate
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "document-translate",
Label: "文档翻译",
OwnedDefinitionKeys: []string{"skill.document-translate.definition"},
OwnedCatalogEntries: []string{"catalog.skill.document-translate"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package emaildrafting
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "email-drafting",
Label: "邮件起草",
OwnedDefinitionKeys: []string{"skill.email-drafting.definition"},
OwnedCatalogEntries: []string{"catalog.skill.email-drafting"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package emaildrafting
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "email-drafting",
Label: "邮件起草",
PlanTitle: "邮件起草流程",
PlanSteps: []string{"识别沟通目的", "整理背景与重点", "生成正式邮件", "补充简短跟进版本"},
ResultTitle: "邮件草稿",
Summary: "已生成一版可直接发送的邮件正文,并补了简短跟进版本和要点摘要。",
Highlights: []string{"适合汇报、催办、确认和通知", "兼顾正式语气与清晰表达", "默认沉淀长邮件和短消息版本"},
ActionType: "communication",
}
@@ -0,0 +1,11 @@
package interviewsummary
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "interview-summary",
Label: "访谈整理",
OwnedDefinitionKeys: []string{"skill.interview-summary.definition"},
OwnedCatalogEntries: []string{"catalog.skill.interview-summary"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package interviewsummary
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "interview-summary",
Label: "访谈整理",
PlanTitle: "访谈整理流程",
PlanSteps: []string{"归并访谈内容", "提炼主题观点", "总结关键洞察", "输出整理版本"},
ResultTitle: "访谈整理稿",
Summary: "已把访谈内容收成主题摘要、洞察结论和后续建议,方便继续分析或汇报。",
Highlights: []string{"适合用户访谈、需求调研、深度沟通", "自动提炼主题与洞察", "默认沉淀摘要、洞察和行动建议"},
ActionType: "document",
}
@@ -0,0 +1,11 @@
package longformwriting
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "longform-writing",
Label: "长文创作",
OwnedDefinitionKeys: []string{"skill.longform-writing.definition"},
OwnedCatalogEntries: []string{"catalog.skill.longform-writing"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package longformwriting
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "longform-writing",
Label: "长文创作",
PlanTitle: "长文创作流程",
PlanSteps: []string{"明确文体与受众", "规划章节大纲", "生成正文骨架", "整理摘要与交付稿"},
ResultTitle: "长文草稿",
Summary: "已输出一版长文大纲和正文骨架,方便继续扩写为正式稿。",
Highlights: []string{"先给大纲再给正文草稿", "适合方案、制度和长文章", "结果可下载继续改写"},
ActionType: "document",
}
@@ -0,0 +1,11 @@
package meetingminutes
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "meeting-minutes",
Label: "会议纪要",
OwnedDefinitionKeys: []string{"skill.meeting-minutes.definition"},
OwnedCatalogEntries: []string{"catalog.skill.meeting-minutes"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package meetingminutes
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "meeting-minutes",
Label: "会议纪要",
PlanTitle: "会议纪要整理流程",
PlanSteps: []string{"识别会议主题", "提炼关键结论", "整理行动项", "输出同步版本"},
ResultTitle: "会议纪要草稿",
Summary: "已按会议纪要结构整理出主题、结论和待办,方便直接同步给参会人。",
Highlights: []string{"自动提炼结论与待办", "适合例会、评审会和复盘会", "默认沉淀纪要正文和行动项清单"},
ActionType: "document",
}
@@ -0,0 +1,11 @@
package mindmap
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "mind-map",
Label: "思维导图",
OwnedDefinitionKeys: []string{"skill.mind-map.definition"},
OwnedCatalogEntries: []string{"catalog.skill.mind-map"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package mindmap
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "mind-map",
Label: "思维导图",
PlanTitle: "思维导图生成流程",
PlanSteps: []string{"识别主题主线", "拆分一级分支", "补齐关键节点", "整理导图源结构"},
ResultTitle: "导图结构草案",
Summary: "已把输入内容压成导图结构,并生成可继续分享和编辑的导图结果。",
Highlights: []string{"可下载导图 SVG", "保留导图文字结构", "适合任务拆解与知识梳理"},
ActionType: "mindmap",
}
@@ -0,0 +1,11 @@
package ocrunderstanding
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "ocr-understanding",
Label: "OCR / 图文理解",
OwnedDefinitionKeys: []string{"skill.ocr-understanding.definition"},
OwnedCatalogEntries: []string{"catalog.skill.ocr-understanding"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package ocrunderstanding
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "ocr-understanding",
Label: "OCR / 图文理解",
PlanTitle: "图文理解流程",
PlanSteps: []string{"识别图像内容", "抽取文字与字段", "整理结构摘要", "输出可复用结果"},
ResultTitle: "识别结果摘要",
Summary: "上传图片后,会优先使用 PaddleOCR 识别文字,并把结果写入当前任务产物。",
Highlights: []string{"走后端正式 OCR 链路", "识别结果会写入任务产物", "上传图片会长期保留到任务输入"},
ActionType: "ocr",
}
@@ -0,0 +1,11 @@
package policyrewrite
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "policy-rewrite",
Label: "制度改写",
OwnedDefinitionKeys: []string{"skill.policy-rewrite.definition"},
OwnedCatalogEntries: []string{"catalog.skill.policy-rewrite"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package policyrewrite
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "policy-rewrite",
Label: "制度改写",
PlanTitle: "制度改写流程",
PlanSteps: []string{"识别制度目标", "统一结构口径", "重写正式正文", "补充修订说明"},
ResultTitle: "制度改写稿",
Summary: "已将原制度改写为更清楚统一的正式版本,并补了修订说明和执行提示。",
Highlights: []string{"适合制度、流程、规范文档", "兼顾正式表达与清晰可读", "默认沉淀改写正文和修订说明"},
ActionType: "document",
}
@@ -0,0 +1,11 @@
package pptgeneration
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "ppt-generation",
Label: "PPT 生成",
OwnedDefinitionKeys: []string{"skill.ppt-generation.definition"},
OwnedCatalogEntries: []string{"catalog.skill.ppt-generation"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package pptgeneration
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "ppt-generation",
Label: "PPT 生成",
PlanTitle: "PPT 生成流程",
PlanSteps: []string{"确认汇报目标", "规划页面结构", "提炼每页要点", "整理讲稿备注"},
ResultTitle: "演示文稿骨架",
Summary: "已按输入内容整理出演示逻辑,并生成可继续下载和细化的 PPT 结果。",
Highlights: []string{"支持直接下载 .pptx", "同时保留大纲与讲稿备注", "适合汇报、方案和复盘"},
ActionType: "presentation",
}
@@ -0,0 +1,11 @@
package progressreport
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "progress-report",
Label: "周报 / 日报",
OwnedDefinitionKeys: []string{"skill.progress-report.definition"},
OwnedCatalogEntries: []string{"catalog.skill.progress-report"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package progressreport
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "progress-report",
Label: "周报 / 日报",
PlanTitle: "进展汇报流程",
PlanSteps: []string{"归并本期工作", "提炼关键进展", "整理风险事项", "输出汇报版本"},
ResultTitle: "进展汇报草稿",
Summary: "已整理出本期进展、风险事项和下一步计划,方便直接同步给负责人。",
Highlights: []string{"适合日报、周报和阶段同步", "自动补齐风险与下步计划", "默认沉淀正式汇报版和简短摘要版"},
ActionType: "report",
}
@@ -0,0 +1,11 @@
package projectplanning
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "project-planning",
Label: "项目规划",
OwnedDefinitionKeys: []string{"skill.project-planning.definition"},
OwnedCatalogEntries: []string{"catalog.skill.project-planning"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package projectplanning
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "project-planning",
Label: "项目规划",
PlanTitle: "项目规划流程",
PlanSteps: []string{"明确目标边界", "拆解阶段路径", "整理里程碑", "补齐风险与依赖"},
ResultTitle: "项目规划草案",
Summary: "已把目标拆成阶段计划、里程碑和风险项,方便直接进入执行或评审。",
Highlights: []string{"适合启动、排期和推进对齐", "默认沉淀阶段计划与风险清单", "结果适合继续流转到任务治理"},
ActionType: "plan",
}
@@ -0,0 +1,11 @@
package proposalsummary
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "proposal-summary",
Label: "方案摘要",
OwnedDefinitionKeys: []string{"skill.proposal-summary.definition"},
OwnedCatalogEntries: []string{"catalog.skill.proposal-summary"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package proposalsummary
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "proposal-summary",
Label: "方案摘要",
PlanTitle: "方案摘要流程",
PlanSteps: []string{"识别方案目标", "提炼核心亮点", "压缩实施路径", "输出摘要版本"},
ResultTitle: "方案摘要稿",
Summary: "已提炼出方案核心结论、价值亮点和实施建议,适合快速汇报或同步。",
Highlights: []string{"适合方案汇报和管理层预读", "自动压缩价值点与实施路径", "默认沉淀摘要正文和亮点清单"},
ActionType: "summary",
}
@@ -0,0 +1,11 @@
package reportgeneration
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "report-generation",
Label: "报告生成",
OwnedDefinitionKeys: []string{"skill.report-generation.definition"},
OwnedCatalogEntries: []string{"catalog.skill.report-generation"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,300 @@
package reportgeneration
import (
"encoding/json"
"fmt"
"strings"
"time"
"eai_agentplatform/backend/internal/ai"
"eai_agentplatform/backend/internal/config"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/store"
)
// ReportGenRequest 报告生成请求
type ReportGenRequest struct {
Topic string `json:"topic"` // 报告主题,必填
Summary string `json:"summary"` // 报告摘要/背景
Context map[string]any `json:"context"` // 附加上下文
Sections []string `json:"sections"` // 期望包含的章节(可选)
Knowledge string `json:"knowledge_key"` // 关联知识库 key
}
// ReportChapter 报告章节结构
type ReportChapter struct {
Title string `json:"title"`
Body string `json:"body"`
}
// ReportContent 完整报告结构
type ReportContent struct {
Topic string `json:"topic"`
Summary string `json:"summary"`
Chapters []ReportChapter `json:"chapters"`
CreatedAt string `json:"created_at"`
}
// RetrieveReportKnowledge 为报告生成检索知识库片段
func RetrieveReportKnowledge(topic string, spaceKey string) string {
if spaceKey != "" {
// 按空间检索
var space model.KnowledgeSpace
if err := store.DB.Where("key = ?", spaceKey).First(&space).Error; err != nil {
return ""
}
var chunks []model.KnowledgeChunk
store.DB.Where("space_id = ? AND approved = ?", space.ID, true).
Order("importance DESC").
Limit(10).
Find(&chunks)
parts := make([]string, 0, len(chunks))
for _, ch := range chunks {
parts = append(parts, ch.Content)
}
return strings.Join(parts, "\n\n")
}
// 全局检索(按 title 匹配)
var chunks []model.KnowledgeChunk
store.DB.Where("approved = ?", true).
Order("importance DESC").
Limit(15).
Find(&chunks)
parts := make([]string, 0, len(chunks))
for _, ch := range chunks {
parts = append(parts, ch.Content)
}
return strings.Join(parts, "\n\n")
}
// PromptReportWithSections 使用用户指定章节生成报告
func PromptReportWithSections(req ReportGenRequest, knowledge string) []ReportChapter {
sectionsStr := strings.Join(req.Sections, "、")
systemPrompt := `你是 EAI 数字员工平台的报告生成引擎。你的任务是输出一份结构化的正式报告,语言为中文。`
userPrompt := fmt.Sprintf(`请围绕以下主题撰写一份正式报告。
主题:%s
报告要求:
1. 内容完整、逻辑清晰、避免空话
2. 使用正式的书面语言,适合内部汇报或培训
3. 报告必须包含以下章节(按顺序):%s
4. 每个章节下至少 3-5 个要点
5. 如有知识库资料可引用,请融入内容中
当前报告摘要/背景:%s
知识库相关资料(如有):
%s`,
req.Topic,
sectionsStr,
strings.TrimSpace(req.Summary),
knowledge,
)
aiRoute, _ := config.GetRoute("title_gen")
aiContent, err := ai.GenerateWithFallback(aiRoute, []ai.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userPrompt},
})
if err != nil {
return fallbackReport(req.Topic)
}
return parseReportMarkdown(aiContent, req.Sections)
}
// PromptReportAutoChapters 让 LLM 自动规划报告章节
func PromptReportAutoChapters(req ReportGenRequest, knowledge string) []ReportChapter {
systemPrompt := `你是 EAI 数字员工平台的报告生成引擎。先规划报告章节,再撰写内容。输出必须严格为 JSON 数组,不要输出其他内容。`
userPrompt := fmt.Sprintf(`请围绕以下主题撰写一份正式报告。
主题:%s
报告要求:
1. 先规划 5-8 个合适的章节
2. 每个章节包含:标题(title)和正文(body)
3. 正文不少于 300 字
4. 使用正式的书面语言
当前报告摘要/背景:%s
知识库相关资料(如有):
%s
请输出 JSON 格式,每个条目包含 "title" 和 "body" 两个字段。`,
req.Topic,
strings.TrimSpace(req.Summary),
knowledge,
)
aiRoute, _ := config.GetRoute("title_gen")
aiContent, err := ai.GenerateWithFallback(aiRoute, []ai.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userPrompt},
})
if err != nil {
return fallbackReport(req.Topic)
}
return parseReportJSON(aiContent)
}
// parseReportMarkdown 从 Markdown 格式解析报告为章节数组
func parseReportMarkdown(content string, defaultSections []string) []ReportChapter {
if len(defaultSections) == 0 {
defaultSections = []string{"背景概述", "核心内容", "关键数据", "实施方案", "总结建议"}
}
lines := strings.Split(content, "\n")
chapters := make([]ReportChapter, 0, len(defaultSections))
currentTitle := ""
currentBody := ""
isChapterTitle := func(line string) bool {
if strings.HasPrefix(line, "# ") || strings.HasPrefix(line, "## ") {
return true
}
runes := []rune(line)
if len(runes) >= 2 {
// 数字序号:1、 1. 一、
if (runes[0] >= '0' && runes[0] <= '9') && (runes[1] == '、' || runes[1] == '.') {
return true
}
if runes[0] == '一' && runes[1] == '、' {
return true
}
}
// 短标题带冒号(排除 URL)
if len(runes) < 30 && len(runes) > 3 && strings.Contains(line, ":") && !strings.Contains(line, "https") {
// 排除 markdown 行内代码
if !strings.Contains(line, "```") {
return true
}
}
return false
}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "---") {
continue
}
if len(chapters) < 10 && isChapterTitle(line) {
if currentTitle != "" && currentBody != "" {
chapters = append(chapters, ReportChapter{
Title: strings.TrimSpace(currentTitle),
Body: strings.TrimSpace(currentBody),
})
}
currentTitle = strings.TrimLeft(line, "# \t")
currentTitle = strings.TrimRight(currentTitle, " ")
currentBody = ""
continue
}
if currentTitle != "" {
currentBody += line + "\n"
}
}
if currentTitle != "" && currentBody != "" {
chapters = append(chapters, ReportChapter{
Title: strings.TrimSpace(currentTitle),
Body: strings.TrimSpace(currentBody),
})
}
if len(chapters) == 0 {
for _, title := range defaultSections {
chapters = append(chapters, ReportChapter{
Title: title,
Body: content,
})
}
}
if len(chapters) > 15 {
chapters = chapters[:15]
}
return chapters
}
// parseReportJSON 从 LLM 返回的 JSON 解析报告
func parseReportJSON(content string) []ReportChapter {
// 提取 JSON 块
jsonStr := content
if idx := strings.Index(content, "["); idx >= 0 {
jsonStr = content[idx:]
}
if idx := strings.LastIndex(jsonStr, "]"); idx >= 0 {
jsonStr = jsonStr[:idx+1]
}
var chapters []ReportChapter
if err := json.Unmarshal([]byte(jsonStr), &chapters); err == nil && len(chapters) > 0 {
return chapters
}
// 提取数组对象块
start := strings.Index(content, "{")
end := strings.LastIndex(content, "}")
if start >= 0 && end > start {
jsonStr2 := content[start : end+1]
if err := json.Unmarshal([]byte(jsonStr2), &chapters); err == nil && len(chapters) > 0 {
return chapters
}
}
// 回退:生成默认章节
return fallbackReport("")
}
// fallbackReport 无 LLM 时的默认报告模板
func fallbackReport(topic string) []ReportChapter {
title := topic
if title == "" {
title = "分析报告"
}
return []ReportChapter{
{
Title: "一、背景概述",
Body: fmt.Sprintf("本报告围绕「%s」主题展开,旨在梳理相关背景信息与核心要点,为内部学习与汇报提供参考。", title),
},
{
Title: "二、核心内容",
Body: fmt.Sprintf("在「%s」领域,当前主流实践聚焦于以下几个维度:知识体系构建、流程标准化、工具赋能与效果评估。通过系统化的培训与考核机制,帮助团队提升专业能力。", title),
},
{
Title: "三、关键数据",
Body: "关键指标包括:产品覆盖率、培训完成率、考试通过率、知识入库率、AI 使用频次等。具体数据需根据实际运营情况定期更新。",
},
{
Title: "四、实施方案",
Body: "建议分阶段推进:第一阶段建立知识体系与培训框架;第二阶段完善考核与评估机制;第三阶段引入 AI 辅助工具提升效率;第四阶段持续优化与迭代。",
},
{
Title: "五、总结建议",
Body: fmt.Sprintf("整体而言,「%s」是当前业务发展的核心方向之一。建议持续投入资源,建立系统化培训体系,结合 AI 工具提升效率,确保团队能力与业务发展同步。", title),
},
}
}
// PromptReportPreview 预览模式:返回精简报告
func PromptReportPreview(req ReportGenRequest) ReportContent {
var chapters []ReportChapter
if len(req.Sections) > 0 {
chapters = PromptReportWithSections(req, RetrieveReportKnowledge(req.Topic, req.Knowledge))
} else {
chapters = PromptReportAutoChapters(req, RetrieveReportKnowledge(req.Topic, req.Knowledge))
}
return ReportContent{
Topic: req.Topic,
Summary: strings.TrimSpace(req.Summary),
Chapters: chapters,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
}
}
@@ -0,0 +1,258 @@
package reportgeneration
import (
"archive/zip"
"bytes"
"fmt"
"os"
"strings"
"github.com/jung-kurt/gofpdf/v2"
)
// ExportReportPDF 导出报告为 PDF 字节
func ExportReportPDF(report ReportContent) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
fontLoaded := false
for _, candidate := range []string{
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
} {
if _, err := os.Stat(candidate); err == nil {
pdf.AddUTF8Font("czk", "", candidate)
fontLoaded = true
break
}
}
pdf.AddPage()
// 标题
if fontLoaded {
pdf.SetFont("czk", "B", 20)
} else {
pdf.SetFont("Helvetica", "B", 20)
}
pdf.CellFormat(170, 12, report.Topic, "", 1, "C", false, 0, "")
pdf.Ln(8)
// 摘要
if strings.TrimSpace(report.Summary) != "" {
if fontLoaded {
pdf.SetFont("czk", "", 10)
} else {
pdf.SetFont("Helvetica", "", 10)
}
pdf.MultiCell(170, 5, report.Summary, "", "L", false)
pdf.Ln(6)
}
// 目录
if fontLoaded {
pdf.SetFont("czk", "B", 14)
} else {
pdf.SetFont("Helvetica", "B", 14)
}
pdf.Cell(0, 8, "目录")
pdf.Ln(4)
for _, ch := range report.Chapters {
if fontLoaded {
pdf.SetFont("czk", "", 10)
} else {
pdf.SetFont("Helvetica", "", 10)
}
pdf.Cell(0, 6, " "+ch.Title)
pdf.Ln(5)
}
pdf.Ln(6)
// 章节
for _, ch := range report.Chapters {
pdf.AddPage()
if fontLoaded {
pdf.SetFont("czk", "B", 14)
} else {
pdf.SetFont("Helvetica", "B", 14)
}
pdf.Cell(0, 8, ch.Title)
pdf.Ln(2)
if fontLoaded {
pdf.SetFont("czk", "", 10)
} else {
pdf.SetFont("Helvetica", "", 10)
}
pdf.MultiCell(170, 5, ch.Body, "", "L", false)
pdf.Ln(6)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// zipWriteFile helper: create a file in zip and write content
func zipWriteFile(zw *zip.Writer, name, content string) error {
f, err := zw.Create(name)
if err != nil {
return err
}
_, err = f.Write([]byte(content))
return err
}
// escapeXML escapes HTML/XML special characters
func escapeXML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "'", "&apos;")
s = strings.ReplaceAll(s, `"`, "&quot;")
return s
}
// timeStr returns a fixed string for pptx sldId
func timeStr() string {
return "20060102T150405"
}
func ExportReportDOCX(report ReportContent) ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
zipWriteFile(zw, "[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`)
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`)
var doc strings.Builder
doc.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>`)
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Title"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="48"/></w:rPr><w:t>` + escapeXML(report.Topic) + `</w:t></w:r></w:p>`)
doc.WriteString(`<w:p><w:r><w:rPr><w:b/><w:sz w:val="24"/></w:rPr><w:t>摘要</w:t></w:r></w:p>`)
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>` + escapeXML(report.Summary) + `</w:t></w:r></w:p>`)
for _, ch := range report.Chapters {
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="28"/></w:rPr><w:t>` + escapeXML(ch.Title) + `</w:t></w:r></w:p>`)
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="22"/></w:rPr><w:t>` + escapeXML(ch.Body) + `</w:t></w:r></w:p>`)
}
doc.WriteString(`</w:body></w:document>`)
zipWriteFile(zw, "word/document.xml", doc.String())
zipWriteFile(zw, "word/styles.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><style type="paragraph" styleId="Title"><name val="Title"/><uiPriority uiPriority="9"/><rPr><rStyle val="Title"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="48"/></rPr></style><style type="paragraph" styleId="Heading1"><name val="Heading 1"/><rPr><rStyle val="Heading1"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="28"/></rPr></style></styles>`)
zipWriteFile(zw, "word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`)
zipWriteFile(zw, "docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>`+escapeXML(report.Topic)+`</dc:title></cp:coreProperties>`)
zipWriteFile(zw, "docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Word</Application></Properties>`)
zw.Close()
return buf.Bytes(), nil
}
func ExportReportPPTX(report ReportContent) ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
layoutRefs := make([]string, 0)
for i := 0; i < len(report.Chapters)+1; i++ {
layoutRefs = append(layoutRefs, "rId"+fmt.Sprint(i+1))
}
slideRels := strings.Join(layoutRefs, "")
zipWriteFile(zw, "[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/></Types>`)
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>`)
preXML := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" changedTime="` + timeStr() + `">` +
`<p:sldMasterId href="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"/>` +
`<p:sldIds>` + slideRels + `</p:sldIds></p:presentation>`
zipWriteFile(zw, "ppt/presentation.xml", preXML)
zipWriteFile(zw, "ppt/_rels/presentation.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMaster/slideMaster1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayouts" Target="slideLayouts/slideLayout1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/></Relationships>`)
titleSlide := buildSlideTitle(report.Topic, report.Summary)
zipWriteFile(zw, "ppt/slides/slide1.xml", titleSlide)
zipWriteFile(zw, "ppt/slides/_rels/slide1.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>`)
for i, ch := range report.Chapters {
slideNum := i + 2
slidePath := fmt.Sprintf("ppt/slides/slide%d.xml", slideNum)
relnPath := fmt.Sprintf("ppt/slides/_rels/slide%d.xml.rels", slideNum)
zipWriteFile(zw, slidePath, buildSlideChapter(ch.Title, ch.Body))
zipWriteFile(zw, relnPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>`)
}
zipWriteFile(zw, "ppt/slideLayouts/slideLayout1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" type="title"><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:t>标题</a:t></a:r><a:endParaRPr lang="zh-CN" sz="4400" dirty="0"/></a:p></a:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:t>内容</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2400" dirty="0"/></a:p></a:txBody></p:sp></p:spTree></p:cSld></p:sldLayout>`)
zipWriteFile(zw, "ppt/theme/theme1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Design Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="E8E8E8"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="4472C4"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="9563C1"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Cambria" script="latin"/></a:majorFont><a:minorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Cambria" script="latin"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"/></a:themeElements></a:theme>`)
zipWriteFile(zw, "ppt/media/", "")
zw.Close()
return buf.Bytes(), nil
}
func buildSlideTitle(title, summary string) string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<p:slide xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" p:sldId="` + timeStr() + `">` +
`<p:cSld><p:spTree>` +
`<p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr>` +
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" anchor="ctr"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:b/><a:t>` + escapeXML(title) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="4400" dirty="0"/></a:p></a:txBody></p:sp>` +
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="摘要"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr>` +
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:t>` + escapeXML(summary) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2400" dirty="0"/></a:p></a:txBody></p:sp>` +
`</p:spTree></p:cSld></p:slide>`
}
func buildSlideChapter(title, body string) string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<p:slide xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" p:sldId="` + timeStr() + `">` +
`<p:cSld><p:spTree>` +
`<p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="914400"/></p:xfrm></p:spPr>` +
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" anchor="ctr"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr/><a:r><a:rPr lang="zh-CN" sz="3200" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:b/><a:t>` + escapeXML(title) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="3200" dirty="0"/></a:p></a:txBody></p:sp>` +
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1219200"/><p:ext cx="6400800" cy="5791200"/></p:xfrm></p:spPr>` +
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2000" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:t>` + escapeXML(body) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2000" dirty="0"/></a:p></a:txBody></p:sp>` +
`</p:spTree></p:cSld></p:slide>`
}
func ExportReportXLSX(report ReportContent) ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
ss := []string{}
addSS := func(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return len(ss)
}
ss = append(ss, s)
return len(ss) - 1
}
addSS(report.Topic)
addSS(report.Summary)
for _, ch := range report.Chapters {
addSS(ch.Title)
addSS(ch.Body)
}
zipWriteFile(zw, "[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/></Types>`)
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`)
zipWriteFile(zw, "xl/workbook.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><workbookPr date1904="false"/><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="25600" windowHeight="14400"/></bookViews><sheets><sheet name="报告" sheetId="1" r:id="rId1"/></sheets></workbook>`)
zipWriteFile(zw, "xl/_rels/workbook.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/></Relationships>`)
var sb strings.Builder
sb.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="` + fmt.Sprint(len(ss)) + `" uniqueCount="` + fmt.Sprint(len(ss)) + `">`)
for _, s := range ss {
sb.WriteString(`<si><t>` + escapeXML(s) + `</t></si>`)
}
sb.WriteString(`</sst>`)
zipWriteFile(zw, "xl/sharedStrings.xml", sb.String())
var sb2 strings.Builder
sb2.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>`)
row := 1
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A1" t="inlineStr"><is><t>` + escapeXML(report.Topic) + `</t></is></c></row>`)
row = 2
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A2" t="inlineStr"><is><t>摘要</t></is></c><c r="B2" t="inlineStr"><is><t>` + escapeXML(report.Summary) + `</t></is></c></row>`)
row = 3
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A3" t="inlineStr"><is><t>章节</t></is></c><c r="B3" t="inlineStr"><is><t>内容</t></is></c></row>`)
for _, ch := range report.Chapters {
row++
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A` + fmt.Sprint(row) + `" t="inlineStr"><is><t>` + escapeXML(ch.Title) + `</t></is></c><c r="B` + fmt.Sprint(row) + `" t="inlineStr"><is><t>` + escapeXML(ch.Body) + `</t></is></c></row>`)
}
sb2.WriteString(`</sheetData></worksheet>`)
zipWriteFile(zw, "xl/worksheets/sheet1.xml", sb2.String())
zw.Close()
return buf.Bytes(), nil
}
@@ -0,0 +1,11 @@
package smartassistant
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "smart-assistant",
Label: "通用助手",
OwnedDefinitionKeys: []string{"skill.smart-assistant.definition"},
OwnedCatalogEntries: []string{"catalog.skill.smart-assistant"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,11 @@
package surveysummary
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "survey-summary",
Label: "表单问卷汇总",
OwnedDefinitionKeys: []string{"skill.survey-summary.definition"},
OwnedCatalogEntries: []string{"catalog.skill.survey-summary"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package surveysummary
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "survey-summary",
Label: "表单问卷汇总",
PlanTitle: "问卷汇总流程",
PlanSteps: []string{"归并表单结果", "提炼重点发现", "整理问题与建议", "输出汇总版本"},
ResultTitle: "问卷汇总稿",
Summary: "已整理出表单结果汇总、重点发现和改进建议,方便直接同步给负责人。",
Highlights: []string{"适合反馈表、报名表、调查问卷", "自动压缩重点发现和改进建议", "默认沉淀汇总报告和行动建议"},
ActionType: "report",
}
@@ -0,0 +1,11 @@
package tablecleanup
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "table-cleanup",
Label: "表格整理",
OwnedDefinitionKeys: []string{"skill.table-cleanup.definition"},
OwnedCatalogEntries: []string{"catalog.skill.table-cleanup"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package tablecleanup
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "table-cleanup",
Label: "表格整理",
PlanTitle: "表格整理流程",
PlanSteps: []string{"识别字段结构", "清洗原始数据", "整理表格内容", "补充字段说明"},
ResultTitle: "表格整理结果",
Summary: "已把输入内容整理成结构化表格,并补了字段说明和清洗备注。",
Highlights: []string{"适合名单、台账、记录整理", "默认沉淀表格正文和字段说明", "便于继续导入 Excel 或系统"},
ActionType: "table",
}
@@ -0,0 +1,11 @@
package texttoolkit
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
var Manifest = skillcontracts.Manifest{
Key: "text-toolkit",
Label: "文本处理",
OwnedDefinitionKeys: []string{"skill.text-toolkit.definition"},
OwnedCatalogEntries: []string{"catalog.skill.text-toolkit"},
ReferencePolicy: skillcontracts.SharedReferenceReadonlyHistory,
}
@@ -0,0 +1,14 @@
package texttoolkit
import officecontracts "eai_agentplatform/backend/internal/skills/runtime/office/contracts"
var OfficeRuntime = officecontracts.Runtime{
Key: "text-toolkit",
Label: "文本处理",
PlanTitle: "文本处理流程",
PlanSteps: []string{"识别清洗目标", "去除噪音符号", "统一格式与标点", "输出可复用文本"},
ResultTitle: "清洗后文本",
Summary: "已完成文本清洗、格式整理和统计,可直接复制或下载结果。",
Highlights: []string{"去 Markdown 噪音", "输出文本统计", "适合高频复制到 Word 和 IM"},
ActionType: "text",
}