编排服务 - 串联完整创作流水线


07 - 编排服务 — 串联完整创作流水线


本章目标

  • 用编排模式串联 3 个独立服务
  • 实现"一键生成"的端到端流程
  • 多关键词搜图 + 去重
  • 错误隔离:局部失败不中断整体流程
  • 注册 CreatorService 为前端唯一调用入口

7.1 为什么需要编排层?

回顾一下我们已有的 3 个服务:

AgentService    →  AI 生成文章 + 提取关键词
PexelsService   →  搜索图片 + 下载图片
WordService     →  生成 docx 文件

如果让前端直接调用这 3 个服务,前端需要:

  1. 调用 AgentService → 拿到文章和关键词
  2. 遍历关键词 → 逐个调用 PexelsService
  3. 收集图片 → 调用 WordService
  4. 处理每个步骤的错误

这会导致前端代码变得复杂、状态管理困难、且难以复用。

📌 关键概念: 编排模式(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 而非 Go error。因为 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 额外搜图(结果面板中的"搜更多"按钮)

注意:

  • AgentServiceWordService 未直接注册给前端。前端通过 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() 一键调用

🔧 动手练习

  1. 加入进度回调:每完成一个步骤,通过 Wails Event 向前端发送进度通知
  2. 实现并行搜图:将多个关键词的搜索改为并发执行(goroutine + sync.WaitGroup)
  3. 增加图片去重:下载前通过 URL 去重,避免同一张图片出现多次
  4. 实现取消功能:通过 ctx.Done() 支持用户中途取消创作

👉 下一章:打包分发与扩展方向

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo