Files
gen2d/docs/backend.md
T
wonder 172914c7f2 docs: 后端采用 Eino 框架编排管线,完善前后端文档对齐
- 后端管线从手写编排改为 Eino compose.Graph,含质检重试分支
- 移除 CORS 中间件
- api.md 补充工程管理接口、任务状态机、管线阶段、WebSocket 消息示例
- frontend.md 组件树和交互流程改用 mermaid 图,stores 对齐 PipelineInput/Output
- CLAUDE.md 修复死链,技术栈补充 Eino
2026-05-24 10:22:15 +08:00

237 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端工程
## 技术选型
- **HTTP 框架**:Gin
- **管线编排**:[Eino](https://github.com/cloudwego/eino) `compose.Graph` — 四阶段管线,含质检→重生成分支
- **AI 组件**:Eino 的 model / tool 组件,统一接口便于替换模型提供商
## 分层结构
```
backend/internal/
├── handler/ # HTTP handlers(薄层,只做参数绑定 + 调用 service)
│ ├── generate.go # 生成任务提交 / 查询 / WebSocket
│ ├── project_style.go # 工程风格 CRUD
│ └── health.go # [已有] 健康检查
├── service/ # 业务逻辑层
│ ├── pipeline.go # Eino compose.Graph 编排:PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter
│ ├── nodes.go # 管线四个节点的实现(每个节点单一职责)
│ ├── types.go # 管线输入/状态/输出的显式结构体定义
│ ├── inference.go # AI 推理 API 调用封装(Eino model 组件)
│ └── project_style.go # 工程风格管理 & 风格合并逻辑
├── model/ # 数据模型 / DTO
│ ├── response.go # [已有] 统一响应
│ ├── task.go # 生成任务 & 素材
│ └── style.go # 工程风格 & 任务风格覆盖
├── middleware/ # 中间件
│ ├── logger.go
│ └── ratelimit.go
├── config/ # [已有] 配置
│ └── config.go
├── queue/ # 异步任务队列
│ └── jobqueue.go
└── cache/ # 缓存层(请求去重 + 结果缓存)
└── cache.go
```
## 分层原则
- **handler**:只做参数绑定、校验、调用 service、返回响应。一个 handler 对应一组 API 路由。
- **service**:承载所有业务逻辑。`pipeline.go` 通过 Eino `compose.Graph` 编排四个节点;`nodes.go` 实现各节点逻辑;`types.go` 定义显式状态结构体。
- **model**:纯数据结构,不含业务逻辑。
## 管线设计(Eino compose.Graph)
四阶段管线,含质检不通过时的重生成分支:
```mermaid
flowchart LR
START --> PromptBuilder --> AssetGenerator --> QualitySupervisor
QualitySupervisor -- pass --> FormatAdapter --> END
QualitySupervisor -- fail --> PromptBuilder
```
### 类型定义(types.go)
```go
// PipelineInput 管线入口输入
type PipelineInput struct {
Prompt string // 用户原始文本
AssetType string // 素材类型:sprite / background / ui / animation
ProjectStyle map[string]string // 工程风格键值对
TaskStyle map[string]string // 任务风格覆盖
Params AssetParams // 技术参数(分辨率、帧数、格式等)
}
// PipelineState Graph 全局状态,通过 WithGenLocalState 注入
type PipelineState struct {
Input PipelineInput
FinalPrompt string // PromptBuilder 输出的三段式提示词
RawImages []GeneratedImage // AssetGenerator 输出的原始图片
PassQuality bool // QualitySupervisor 质检结果
RejectReason string // 质检不通过原因
RetryCount int // 重试次数
}
// PipelineOutput 管线最终输出
type PipelineOutput struct {
Assets []Asset // 生成结果素材列表
Metadata AssetMetadata // 元数据(分辨率、帧数、格式)
}
```
### 编排入口(pipeline.go)
```go
const (
nodePromptBuilder = "prompt_builder"
nodeAssetGenerator = "asset_generator"
nodeQualitySupervisor = "quality_supervisor"
nodeFormatAdapter = "format_adapter"
)
func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
g := compose.NewGraph[PipelineInput, PipelineOutput](
compose.WithGenLocalState(func(ctx context.Context) *PipelineState {
return &PipelineState{}
}),
)
// 添加节点
_ = g.AddLambdaNode(nodePromptBuilder, promptBuilderNode,
compose.WithStatePostHandler(promptBuilderPostHandler))
_ = g.AddLambdaNode(nodeAssetGenerator, assetGeneratorNode,
compose.WithStatePostHandler(assetGeneratorPostHandler))
_ = g.AddLambdaNode(nodeQualitySupervisor, qualitySupervisorNode,
compose.WithStatePostHandler(qualitySupervisorPostHandler))
_ = g.AddLambdaNode(nodeFormatAdapter, formatAdapterNode)
// 连线:正常路径
_ = g.AddEdge(compose.START, nodePromptBuilder)
_ = g.AddEdge(nodePromptBuilder, nodeAssetGenerator)
_ = g.AddEdge(nodeAssetGenerator, nodeQualitySupervisor)
_ = g.AddEdge(nodeFormatAdapter, compose.END)
// 连线:质检分支
_ = g.AddBranch(nodeQualitySupervisor, compose.NewGraphBranch(
func(ctx context.Context, state *PipelineState) (string, error) {
if state.PassQuality {
return nodeFormatAdapter, nil
}
if state.RetryCount >= 3 {
return nodeFormatAdapter, nil // 超过重试次数,降级输出
}
return nodePromptBuilder, nil // 重生成
},
map[string]bool{nodePromptBuilder: true, nodeFormatAdapter: true},
))
return g, nil
}
```
### 节点实现(nodes.go)
每个节点是 Graph 中的 Lambda,通过 `StatePreHandler` / `StatePostHandler` 读写全局状态:
| 节点 | Lambda 输入→输出 | State 交互 | 职责 |
|------|-----------------|-----------|------|
| PromptBuilder | `PipelineInput → string` | PostHandler 写入 FinalPrompt | 合并风格 → 生成三段式提示词 |
| AssetGenerator | `string → []GeneratedImage` | PostHandler 写入 RawImages | 调用 AI 推理 API 出图 |
| QualitySupervisor | `[]GeneratedImage → bool` | PostHandler 写入 PassQuality + RejectReason + RetryCount | 视觉模型质检 |
| FormatAdapter | `[]GeneratedImage → PipelineOutput` | 无 | 格式转换、spritesheet 打包 |
```go
// PromptBuilder 节点:接收输入,输出提示词
var promptBuilderNode = compose.InvokableLambda(func(ctx context.Context, in PipelineInput) (string, error) {
// 合并 projectStyle + taskStyle,生成三段式提示词
return buildPrompt(in), nil
})
// StatePostHandler:将提示词写入全局状态
func promptBuilderPostHandler(ctx context.Context, out string, state *PipelineState) (string, error) {
state.FinalPrompt = out
return out, nil
}
// QualitySupervisor StatePostHandler:写入质检结果和重试计数
func qualitySupervisorPostHandler(ctx context.Context, out bool, state *PipelineState) (bool, error) {
state.PassQuality = out
if !out {
state.RetryCount++
state.RejectReason = "风格不一致" // 实际由视觉模型返回
}
return out, nil
}
```
### 运行入口
```go
func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) {
g, err := NewGenerateGraph()
if err != nil {
return nil, err
}
r, err := g.Compile(ctx)
if err != nil {
return nil, err
}
return r.Invoke(ctx, in)
}
```
## 关键实体
| 实体 | 说明 | 关系 |
|------|------|------|
| Project | 顶层容器,用户创建的项目 | 1──1 ProjectStyle, 1──N Task |
| ProjectStyle | 工程级键值对风格配置,保证同一工程下所有素材风格一致 | 属于 Project |
| Task | 工程下的单次生成请求,包含用户文本、素材类型、任务风格覆盖、技术参数、状态、结果 | 属于 Project, 1──N Asset |
| Prompt | PromptBuilder 输出的三段式提示词(主题+约束+内容),管线中间产物,不持久化 | 由 Task 生成 |
| Asset | 生成结果素材,关联到任务,包含 URL、元数据(分辨率、帧数、格式) | 属于 Task |
```mermaid
erDiagram
Project ||--|| ProjectStyle : has
Project ||--|{ Task : has
Task ||--|{ Asset : has
```
## 风格模型
### 工程风格(Project Style)
工程级别,保证同一工程下所有素材风格一致:
```go
type ProjectStyle struct {
ID string `json:"id"`
ProjectID string `json:"projectId"`
KVPairs map[string]string `json:"kvPairs"` // 如 {"artStyle":"pixel","palette":"warm"}
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
```
### 任务风格覆盖(Task Style Override)
任务级别,仅覆盖需要差异化的键:
```go
type TaskStyle struct {
KVPairs map[string]string `json:"kvPairs"` // 仅记录与工程风格不同的部分
}
```
### 合并逻辑
```
finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs)
```
任务同名键覆盖工程风格,由 PromptBuilder 节点在生成提示词时执行合并。