编排服务 - 串联完整创作流水线
07 - 编排服务 — 串联完整创作流水线
本章目标
- 用编排模式串联 3 个独立服务
- 实现"一键生成"的端到端流程
- 多关键词搜图 + 去重
- 错误隔离:局部失败不中断整体流程
- 注册 CreatorService 为前端唯一调用入口
7.1 为什么需要编排层?
回顾一下我们已有的 3 个服务:
AgentService → AI 生成文章 + 提取关键词
PexelsService → 搜索图片 + 下载图片
WordService → 生成 docx 文件
如果让前端直接调用这 3 个服务,前端需要:
- 调用 AgentService → 拿到文章和关键词
- 遍历关键词 → 逐个调用 PexelsService
- 收集图片 → 调用 WordService
- 处理每个步骤的错误
这会导致前端代码变得复杂、状态管理困难、且难以复用。
📌 关键概念: 编排模式(Orchestration Pattern)将多个独立服务的调用序列封装在一个编排器中,对外暴露一个简洁的接口。前端只需要调用
CreatorService.Create(),后端负责协调所有步骤。
7.2 创建 CreatorService
创建 backend/creator.go:
package backend
import (
"context"
"fmt"
"log"
)
请求与响应
// CreateRequest 创作请求(前端传入)
type CreateRequest struct {
Topic string `json:"topic"`
PetType string `json:"pet_type"`
Style string `json:"style"`
Keywords string `json:"keywords"`
WordCount int `json:"word_count"`
ImageCount int `json:"image_count"`
}
// CreateResponse 创作响应
type CreateResponse struct {
Title string `json:"title"`
Content string `json:"content"`
Photos []PexelsPhoto `json:"photos"`
WordPath string `json:"word_path"`
Error string `json:"error,omitempty"`
}
💡 知识点:
CreateResponse中的Error字段用了string而非 Goerror。因为 Go 的error接口无法通过 Wails 序列化到前端。当流程中有非致命错误时,我们通过这个字段告知前端。
编排服务结构体
// CreatorService 创作编排服务(统一入口)
type CreatorService struct {
agentService *AgentService
pexelsService *PexelsService
wordService *WordService
}
// NewCreatorService 创建编排服务
func NewCreatorService(as *AgentService, ps *PexelsService, ws *WordService) *CreatorService {
return &CreatorService{
agentService: as,
pexelsService: ps,
wordService: ws,
}
}
📌 关键概念: 这是典型的依赖注入模式。
CreatorService不自己创建子服务,而是通过构造函数接收。这样做的好处是:
- 子服务可以被多个编排器共享
- 便于单元测试(可以注入 mock 对象)
main.go统一管理生命周期
7.3 核心编排逻辑:Create
// Create 一键创作(前端调用)
// 流程:AI 生成文章 → AI 提取关键词 → 搜索配图 → 下载图片 → 生成 Word
func (cs *CreatorService) Create(ctx context.Context, req CreateRequest) (*CreateResponse, error) {
// ========== 步骤 1:AI 生成文章 ==========
artReq := ArtRequest{
Topic: req.Topic,
PetType: req.PetType,
Style: req.Style,
Keywords: req.Keywords,
WordCount: req.WordCount,
ImageCount: req.ImageCount,
}
article, err := cs.agentService.GenerateArticle(ctx, artReq)
if err != nil {
return nil, fmt.Errorf("AI 生成文章失败: %w", err)
}
// ========== 步骤 2:多关键词搜索配图 ==========
imageCount := req.ImageCount
if imageCount <= 0 {
imageCount = 3
}
if imageCount > 10 {
imageCount = 10
}
var allPhotos []PexelsPhoto
for _, tag := range article.SearchTags {
if len(allPhotos) >= imageCount {
break // 已收集足够图片
}
photos, err := cs.pexelsService.SearchImages(tag, imageCount)
if err != nil {
// 某个关键词搜图失败不影响整体流程
log.Printf("搜索图片失败 (tag=%s): %v", tag, err)
continue
}
for _, p := range photos {
if len(allPhotos) >= imageCount {
break
}
allPhotos = append(allPhotos, p)
}
}
// ========== 步骤 3:下载图片 ==========
var imageData [][]byte
for _, photo := range allPhotos {
data, err := cs.pexelsService.DownloadImage(photo.Src.Medium)
if err != nil {
log.Printf("下载图片失败 (%s): %v", photo.Src.Medium, err)
continue
}
imageData = append(imageData, data)
}
// ========== 步骤 4:生成 Word 文档 ==========
var imageURLs []string
for _, p := range allPhotos {
imageURLs = append(imageURLs, p.Src.Medium)
}
wordPath, err := cs.wordService.GenerateDocx(DocxData{
Title: article.Title,
Content: article.Content,
ImageURLs: imageURLs,
}, imageData)
if err != nil {
return nil, fmt.Errorf("生成 Word 文档失败: %w", err)
}
return &CreateResponse{
Title: article.Title,
Content: article.Content,
Photos: allPhotos,
WordPath: wordPath,
}, nil
}
7.4 编排模式详解
流水线架构
CreateRequest
│
├─ 1. AgentService.GenerateArticle()
│ └→ ArticleResult { Title, Content, SearchTags }
│
├─ 2. PexelsService.SearchImages(tag, count)
│ └→ 遍历 SearchTags,按需搜图
│ └→ 收集到 allPhotos
│
├─ 3. PexelsService.DownloadImage(url)
│ └→ 遍历 allPhotos,下载二进制
│ └→ 收集到 imageData
│
└─ 4. WordService.GenerateDocx(...)
└→ 传入 title + content + images
└→ 返回文件路径
│
└→ CreateResponse { Title, Content, Photos, WordPath }
错误处理策略
| 场景 | 处理方式 |
|---|---|
| AI 生成失败 | 终止 — 没有文章后续无法进行 |
| 某个关键词搜图失败 | 跳过 — continue 尝试下一个关键词 |
| 某张图片下载失败 | 跳过 — continue 下载下一张 |
| Word 生成失败 | 终止 — 最后一个环节失败无法挽救 |
💡 知识点: 这种分级容错策略确保:即使 Pexels API 部分失败,用户仍然能拿到文章和部分图片。
图片数量控制
imageCount := req.ImageCount
if imageCount <= 0 {
imageCount = 3 // 默认 3 张
}
if imageCount > 10 {
imageCount = 10 // 最多 10 张
}
⚠️ 为什么限制 10 张: 每张图片嵌入 docx 会让文档体积增加约 200KB-2MB。10 张图片的 docx 可能在 5-20MB,过多会影响打开速度。同时也避免单个 API 调用时间过长。
7.5 注册到 main.go(完整版)
func main() {
// 创建配置服务(共享)
configService := backend.NewConfigService()
// 创建业务服务
agentService := backend.NewAgentService(configService)
pexelsService := backend.NewPexelsService(configService)
wordService := backend.NewWordService()
// 创建编排服务(统一入口)
creatorService := backend.NewCreatorService(agentService, pexelsService, wordService)
// 创建 Wails 应用
app := application.New(application.Options{
Name: "pet-content-creator",
Description: "AI驱动的今日头条宠物内容创作工具",
Services: []application.Service{
application.NewService(configService),
application.NewService(creatorService),
application.NewService(pexelsService),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// 创建主窗口
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "今日头条宠物内容创作工具",
Width: 1200,
Height: 800,
URL: "/",
})
// 运行
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
完整的服务注册表
| 注册的 Service | 暴露方法 | 用途 |
|---|---|---|
ConfigService |
GetConfig / SaveConfig |
配置管理 |
CreatorService |
Create / GetOutputDir |
创作主入口 |
PexelsService |
SearchImages |
额外搜图(结果面板中的"搜更多"按钮) |
注意:
AgentService和WordService未直接注册给前端。前端通过CreatorService间接使用它们PexelsService额外注册,因为结果面板中有独立的"搜索更多图片"功能
7.6 前端调用
import { CreatorService } from "../bindings/pet-content-creator/backend";
const handleCreate = async () => {
setAppState("creating");
const result = await CreatorService.Create({
topic: "如何训练狗狗上厕所",
pet_type: "狗",
style: "指南",
keywords: "",
word_count: 800,
image_count: 3,
});
setResponse(result as unknown as CreateResponse);
setAppState("done");
};
仅一行 CreatorService.Create(...) 调用,背后串联了 AI 生成 → 搜图 → 下载 → Word 生成的全流程。
7.7 完整数据流回顾
用户输入表单
│
├─ topic: "如何训练狗狗上厕所"
├─ pet_type: "狗"
├─ style: "指南"
├─ word_count: 800
└─ image_count: 3
│
▼
CreatorService.Create()
│
├─ [1] AgentService.GenerateArticle()
│ System Prompt: "你是专业的今日头条宠物内容创作者..."
│ User Message: "请创作一篇关于狗的宠物指南文章..."
│ └→ DeepSeek API 返回:
│ 标题: "新手铲屎官必看:7天教会狗狗定点大小便"
│ 正文: "## 为什么狗狗乱尿..." (约 800 字)
│ 标签: [TAGS] puppy training, dog potty, pet care guide
│
├─ [2] PexelsService.SearchImages("puppy training", 3)
│ └→ 返回 3 张图片元数据
│ PexelsService.SearchImages("dog potty", 3)
│ └→ 返回 3 张图片元数据(合并后用前 3 张)
│
├─ [3] PexelsService.DownloadImage(url) × 3
│ └→ 下载 3 张图片的二进制数据
│
├─ [4] WordService.GenerateDocx(...)
│ └→ 构建 ZIP 包 (6 个 XML + 3 张图片)
│ └→ 写入 ~/Documents/PetContentCreator/xxx.docx
│
└→ 返回 CreateResponse
├─ title: "新手铲屎官必看..."
├─ content: "## 为什么狗狗乱尿..."
├─ photos: [3 张 PexelsPhoto]
└─ word_path: "C:/Users/.../Documents/PetContentCreator/xxx.docx"
本章总结
| 你已经学会 | 对应能力 |
|---|---|
| 编排模式 | 串联多服务形成完整业务流 |
| 依赖注入 | 解耦服务依赖关系 |
| 分级容错 | AI 失败终止 vs 图片失败跳过 |
| 多关键词搜图 | 遍历 AI 返回的标签,按需搜图 |
| 服务暴露策略 | 选择性注册(不直接暴露内部服务) |
| 前端单一入口 | CreatorService.Create() 一键调用 |
🔧 动手练习
- 加入进度回调:每完成一个步骤,通过 Wails Event 向前端发送进度通知
- 实现并行搜图:将多个关键词的搜索改为并发执行(goroutine + sync.WaitGroup)
- 增加图片去重:下载前通过 URL 去重,避免同一张图片出现多次
- 实现取消功能:通过
ctx.Done()支持用户中途取消创作