docs: 适配单机场景,简化存储与认证方案
- 数据库:MySQL → SQLite,去掉连接池配置和 golang-migrate - 文件存储:OSS 对象存储 → 本地文件系统,统一 Gin 静态文件服务 - ID 生成:Sonyflake → 前缀+随机字符串 - 认证:Bearer Token → httpOnly Cookie,WebSocket 同步改为 Cookie 认证 - 质检重试:PromptBuilder 重试时注入 RejectReason 改进提示词 - 合并 multi-agent-pipeline.md 到 backend.md,消除文档重复 - 移除缓存管理 API(暴露内部实现细节)
This commit is contained in:
+3
-4
@@ -4,14 +4,13 @@ gen2d — AI 驱动的 2D 游戏素材生成工具。通过文本提示词生成
|
||||
|
||||
## 技术栈
|
||||
|
||||
Go + Gin + Eino / Vite + React + TypeScript + zustand / 可替换 AI 推理模型
|
||||
Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 可替换 AI 推理模型
|
||||
|
||||
## 文档索引
|
||||
|
||||
- [多智能体生成管线](multi-agent-pipeline.md) — PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter 四阶段流水线
|
||||
- [后端工程](backend.md) — 分层结构、目录组织、分层原则
|
||||
- [后端工程](backend.md) — 分层结构、管线设计(PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter)、风格模型
|
||||
- [前端工程](frontend.md) — 组件树、状态管理、路由、WebSocket 通信
|
||||
- [异步任务](async-tasks.md) — 任务队列、状态机、并发控制、失败重试
|
||||
- [数据存储](database.md) — 数据库选型、表结构、素材文件存储
|
||||
- [数据存储](database.md) — SQLite 选型、表结构、本地文件存储
|
||||
- [API 设计](api.md) — 接口列表、请求/响应示例、实现状态
|
||||
- [预设风格键](style-keys.md) — 美术风格、色调、线条等风格键分类与可选值
|
||||
|
||||
+16
-55
@@ -8,13 +8,16 @@
|
||||
|
||||
## 认证
|
||||
|
||||
除健康检查和注册登录外,所有接口需在请求头携带 Bearer Token:
|
||||
除健康检查和注册登录外,所有接口需通过 httpOnly Cookie 携带 JWT。登录/注册成功后,服务端通过 `Set-Cookie` 响应头写入 Token,后续请求自动携带。
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
Cookie 属性:
|
||||
- `Name`: `token`
|
||||
- `HttpOnly`: true
|
||||
- `SameSite`: Lax
|
||||
- `Path`: `/`
|
||||
- `MaxAge`: 由 `GEN2D_JWT_EXPIRE` 控制(默认 7200 秒)
|
||||
|
||||
Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默认 7200 秒)。Token 过期或无效时返回:
|
||||
Token 过期或无效时返回:
|
||||
|
||||
```json
|
||||
{ "code": 401, "message": "未登录或 Token 已过期", "data": null }
|
||||
@@ -77,7 +80,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
| `email` | string | 是 | 邮箱地址 |
|
||||
| `password` | string | 是 | 8-128 字符 |
|
||||
|
||||
响应:
|
||||
响应(Token 通过 `Set-Cookie` 响应头写入,不在 body 中返回):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -86,9 +89,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
"data": {
|
||||
"id": "user_a1B2c3D4",
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresAt": "2026-05-24T12:00:00Z"
|
||||
"email": "player1@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -121,7 +122,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
响应(Token 通过 `Set-Cookie` 响应头写入,不在 body 中返回):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -130,9 +131,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
"data": {
|
||||
"id": "user_a1B2c3D4",
|
||||
"username": "player1",
|
||||
"email": "player1@example.com",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresAt": "2026-05-24T12:00:00Z"
|
||||
"email": "player1@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -173,7 +172,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
|
||||
## 工程管理
|
||||
|
||||
需认证。以下接口均需携带 Bearer Token,工程归属于当前登录用户。
|
||||
需认证。以下接口均需通过 Cookie 携带 Token,工程归属于当前登录用户。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
@@ -260,7 +259,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
|
||||
### DELETE /api/v1/projects/:projectId
|
||||
|
||||
级联删除工程下的所有任务、素材及 OSS 文件。
|
||||
级联删除工程下的所有任务、素材及本地文件。
|
||||
|
||||
响应:
|
||||
|
||||
@@ -443,7 +442,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
"assets": [
|
||||
{
|
||||
"id": "asset_001",
|
||||
"url": "https://cdn.example.com/users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png",
|
||||
"url": "users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png",
|
||||
"format": "png",
|
||||
"width": 256,
|
||||
"height": 64,
|
||||
@@ -461,7 +460,7 @@ Token 通过登录接口获取,过期时间由 `GEN2D_JWT_EXPIRE` 控制(默
|
||||
|
||||
### WebSocket 消息格式
|
||||
|
||||
连接路径:`ws://host/api/v1/tasks/:taskId/ws?token=<jwt>`(通过 query 参数传递 Token)
|
||||
连接路径:`ws://host/api/v1/tasks/:taskId/ws`(通过 httpOnly Cookie 自动携带认证信息)
|
||||
|
||||
```typescript
|
||||
interface PipelineProgress {
|
||||
@@ -502,41 +501,3 @@ interface PipelineProgress {
|
||||
{"stage":"format_adapter","status":"completed","progress":100,"result":{"assets":[...]}}
|
||||
```
|
||||
|
||||
## 缓存管理
|
||||
|
||||
需认证。用于管理应用层去重缓存(详见 [数据存储 — 去重策略](database.md#去重策略)),主要用于调试和运维场景。
|
||||
|
||||
| 状态 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| [ ] | DELETE | `/api/v1/cache/:key` | 清除特定去重缓存条目 |
|
||||
| [ ] | POST | `/api/v1/cache/clear` | 批量清除所有去重缓存 |
|
||||
|
||||
### DELETE /api/v1/cache/:key
|
||||
|
||||
清除指定去重键的缓存条目。`:key` 为 `hash(prompt + assetType + params)` 生成的去重键,仅在需要手动解除去重锁定时使用。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/cache/clear
|
||||
|
||||
清除所有去重缓存条目,不影响正在执行的任务。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"cleared": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+7
-7
@@ -20,7 +20,7 @@ stateDiagram-v2
|
||||
|------|------|--------|
|
||||
| `pending` | 已入队,等待 Worker 领取 | `task.status = 'pending'` |
|
||||
| `running` | 管线执行中,`task.stage` 记录当前阶段 | `task.status = 'running'` |
|
||||
| `completed` | 管线完成,素材已上传至 OSS | `task.status = 'completed'`, 写入 `asset` 表 |
|
||||
| `completed` | 管线完成,素材已保存到本地 | `task.status = 'completed'`, 写入 `asset` 表 |
|
||||
| `failed` | 执行失败或超过重试上限 | `task.status = 'failed'`, `task.error` 记录原因 |
|
||||
|
||||
## 任务队列
|
||||
@@ -79,7 +79,7 @@ JobQueue.Pop() 取出 taskId
|
||||
│ ├── AssetGenerator → stage 更新
|
||||
│ ├── QualitySupervisor → stage 更新(可能触发重试分支)
|
||||
│ └── FormatAdapter → stage 更新
|
||||
├── 5. 上传素材至 OSS,写入 asset 表
|
||||
├── 5. 保存素材到本地,写入 asset 表
|
||||
├── 6. 更新 task.status = completed
|
||||
└── 7. 异常时更新 task.status = failed, task.error = 错误信息
|
||||
```
|
||||
@@ -128,7 +128,7 @@ func (s *TaskService) StartWorkers(n int) {
|
||||
| 提示词构建 | `prompt_builder` | 0-20% | 合并风格,生成三段式提示词 |
|
||||
| 素材生成 | `asset_generator` | 20-60% | 调用 AI 推理 API 出图 |
|
||||
| 质量检查 | `quality_supervisor` | 60-80% | 视觉模型质检 |
|
||||
| 格式适配 | `format_adapter` | 80-100% | 格式转换、spritesheet 打包、上传 OSS |
|
||||
| 格式适配 | `format_adapter` | 80-100% | 格式转换、spritesheet 打包、保存素材 |
|
||||
|
||||
进度更新通过 WebSocket 实时推送给前端(参见 [API 设计 - WebSocket 消息格式](api.md))。
|
||||
|
||||
@@ -152,7 +152,7 @@ QualitySupervisor -- pass --> FormatAdapter
|
||||
|
||||
### 任务级重试(管线外重试)
|
||||
|
||||
管线执行过程中发生不可恢复的错误(如 AI API 超时、OSS 上传失败):
|
||||
管线执行过程中发生不可恢复的错误(如 AI API 超时、文件写入失败):
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
@@ -176,10 +176,10 @@ GET /api/v1/tasks/:taskId
|
||||
### WebSocket
|
||||
|
||||
```
|
||||
ws://host/api/v1/tasks/:taskId/ws?token=<jwt>
|
||||
ws://host/api/v1/tasks/:taskId/ws
|
||||
```
|
||||
|
||||
长连接实时推送,每个阶段的状态变更立即通知前端。消息格式参见 [API 设计](api.md)。
|
||||
长连接实时推送,通过 httpOnly Cookie 自动认证,每个阶段的状态变更立即通知前端。消息格式参见 [API 设计](api.md)。
|
||||
|
||||
## 去重
|
||||
|
||||
@@ -187,4 +187,4 @@ ws://host/api/v1/tasks/:taskId/ws?token=<jwt>
|
||||
|
||||
## 文件存储
|
||||
|
||||
任务完成后,FormatAdapter 将素材上传至 OSS。素材文件的存储结构与访问方式详见 [数据存储 — 文件存储](database.md#文件存储)。
|
||||
任务完成后,FormatAdapter 将素材保存到本地文件系统。素材文件的存储结构与访问方式详见 [数据存储 — 文件存储](database.md#文件存储)。
|
||||
|
||||
+75
-10
@@ -30,13 +30,13 @@ backend/internal/
|
||||
│ └── style.go # 工程风格 & 任务风格覆盖
|
||||
├── middleware/ # 中间件
|
||||
│ ├── logger.go
|
||||
│ ├── auth.go # JWT 认证中间件
|
||||
│ ├── auth.go # JWT 认证中间件(从 Cookie 读取 Token)
|
||||
│ └── ratelimit.go
|
||||
├── config/ # [已有] 配置
|
||||
│ └── config.go
|
||||
├── queue/ # 异步任务队列
|
||||
│ └── jobqueue.go
|
||||
└── cache/ # 缓存层(请求去重 + 结果缓存)
|
||||
└── cache/ # 缓存层(请求去重)
|
||||
└── cache.go
|
||||
```
|
||||
|
||||
@@ -46,17 +46,71 @@ backend/internal/
|
||||
- **service**:承载所有业务逻辑。`pipeline.go` 通过 Eino `compose.Graph` 编排四个节点;`nodes.go` 实现各节点逻辑;`types.go` 定义显式状态结构体。
|
||||
- **model**:纯数据结构,不含业务逻辑。
|
||||
|
||||
## 管线设计(Eino compose.Graph)
|
||||
## 多阶段生成管线
|
||||
|
||||
四阶段管线,含质检不通过时的重生成分支:
|
||||
gen2d 的核心生成流程采用 Eino `compose.Graph` 编排的四阶段管线,含质检不通过时的重生成分支。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
START --> PromptBuilder --> AssetGenerator --> QualitySupervisor
|
||||
QualitySupervisor -- pass --> FormatAdapter --> END
|
||||
QualitySupervisor -- fail --> PromptBuilder
|
||||
A["PromptBuilder\n(提示词工程)"] --> B["AssetGenerator\n(AI 出图)"]
|
||||
B --> C["QualitySupervisor\n(质检)"]
|
||||
C -- pass --> D["FormatAdapter\n(格式适配)"]
|
||||
C -- fail --> A
|
||||
```
|
||||
|
||||
### 各阶段职责
|
||||
|
||||
#### 1. PromptBuilder
|
||||
|
||||
- **输入**:用户文本 + 素材类型标签 + 工程风格 + 任务风格覆盖 + 技术参数
|
||||
- **输出**:三段式完整生成提示词
|
||||
- **职责**:
|
||||
1. 解析用户文本,提取核心内容作为【主题】
|
||||
2. 合并工程风格与任务风格覆盖(任务同名键覆盖工程),生成【约束】(含负面提示词)
|
||||
3. 注入素材类型、分辨率等技术参数作为【内容】
|
||||
4. 拼接为最终提示词
|
||||
5. **重试时**:将上次质检的 RejectReason 注入【约束】段,指导 AI 修正问题
|
||||
|
||||
**风格合并**:
|
||||
|
||||
```go
|
||||
finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs)
|
||||
```
|
||||
|
||||
工程风格保证同一工程下所有素材风格一致;任务风格仅覆盖需要差异化的键。
|
||||
|
||||
**三段式提示词结构**:
|
||||
|
||||
```
|
||||
【主题】用户的原始描述核心内容
|
||||
【约束】风格键值对生成的约束条件 + 负面提示词 + [重试时] 上次质检问题:{rejectReason}
|
||||
【内容】素材类型、分辨率、帧数等技术参数
|
||||
```
|
||||
|
||||
#### 2. AssetGenerator
|
||||
|
||||
- **输入**:PromptBuilder 的输出
|
||||
- **输出**:原始生成图片(单张或多张)
|
||||
- **职责**:调用 AI 推理 API 出图。不同素材类型的差异化需求已在上一步注入提示词中。
|
||||
- **接口规范**:统一采用 OpenAI 兼容的 `/v1/images/generations` 格式。多张生成通过 `n` 参数请求,实际支持数量取决于后端模型(如 DALL-E 3 仅支持 `n=1`,需多次调用模拟多张)。
|
||||
|
||||
#### 3. QualitySupervisor
|
||||
|
||||
- **输入**:原始生成图片 + 风格配置
|
||||
- **输出**:通过 / 不通过 + 不通过的原因
|
||||
- **职责**:通过视觉模型检查素材是否符合提示词要求与风格约束。不通过时,Graph 分支将 RejectReason 回填到 PipelineState,路由回 PromptBuilder 重新生成(最多 3 次,超过则降级输出)。
|
||||
|
||||
#### 4. FormatAdapter
|
||||
|
||||
- **输入**:通过质检的图片 + 素材类型
|
||||
- **输出**:游戏引擎可用的素材文件 + 元数据
|
||||
- **职责**:格式转换、spritesheet 打包、元数据生成。
|
||||
- **示例**:输入 4 张 64×64 的角色行走帧 PNG → 输出一张 256×64 的 spritesheet + JSON 元数据(帧尺寸、帧数、锚点偏移),可直接拖入 Unity 2D Animation 使用。
|
||||
|
||||
---
|
||||
|
||||
## 管线设计(Eino compose.Graph)
|
||||
|
||||
### 类型定义(types.go)
|
||||
|
||||
```go
|
||||
@@ -105,6 +159,7 @@ func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
|
||||
|
||||
// 添加节点
|
||||
_ = g.AddLambdaNode(nodePromptBuilder, promptBuilderNode,
|
||||
compose.WithStatePreHandler(promptBuilderPreHandler), // 重试时注入 RejectReason
|
||||
compose.WithStatePostHandler(promptBuilderPostHandler))
|
||||
_ = g.AddLambdaNode(nodeAssetGenerator, assetGeneratorNode,
|
||||
compose.WithStatePostHandler(assetGeneratorPostHandler))
|
||||
@@ -142,7 +197,7 @@ func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
|
||||
|
||||
| 节点 | Lambda 输入→输出 | State 交互 | 职责 |
|
||||
|------|-----------------|-----------|------|
|
||||
| PromptBuilder | `PipelineInput → string` | PostHandler 写入 FinalPrompt | 合并风格 → 生成三段式提示词 |
|
||||
| PromptBuilder | `PipelineInput → string` | PreHandler 读取 RejectReason,PostHandler 写入 FinalPrompt | 合并风格 → 生成三段式提示词(重试时注入质检问题) |
|
||||
| AssetGenerator | `string → []GeneratedImage` | PostHandler 写入 RawImages | 调用 AI 推理 API 出图 |
|
||||
| QualitySupervisor | `[]GeneratedImage → bool` | PostHandler 写入 PassQuality + RejectReason + RetryCount | 视觉模型质检 |
|
||||
| FormatAdapter | `[]GeneratedImage → PipelineOutput` | 无 | 格式转换、spritesheet 打包 |
|
||||
@@ -150,10 +205,18 @@ func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
|
||||
```go
|
||||
// PromptBuilder 节点:接收输入,输出提示词
|
||||
var promptBuilderNode = compose.InvokableLambda(func(ctx context.Context, in PipelineInput) (string, error) {
|
||||
// 合并 projectStyle + taskStyle,生成三段式提示词
|
||||
return buildPrompt(in), nil
|
||||
})
|
||||
|
||||
// StatePreHandler:重试时将 RejectReason 注入输入
|
||||
func promptBuilderPreHandler(ctx context.Context, in PipelineInput, state *PipelineState) (PipelineInput, error) {
|
||||
if state.RetryCount > 0 && state.RejectReason != "" {
|
||||
// 将上次质检问题附加到输入中,供 buildPrompt 注入【约束】段
|
||||
in.RejectReason = state.RejectReason
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// StatePostHandler:将提示词写入全局状态
|
||||
func promptBuilderPostHandler(ctx context.Context, out string, state *PipelineState) (string, error) {
|
||||
state.FinalPrompt = out
|
||||
@@ -189,6 +252,8 @@ func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键实体
|
||||
|
||||
| 实体 | 说明 | 关系 |
|
||||
@@ -242,4 +307,4 @@ finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs)
|
||||
- **数据结构**:`sync.Map`,key 为 `hash(prompt + assetType + params)`,value 为 taskId
|
||||
- **生命周期**:任务提交时写入,任务完成或失败后清除;可设置 TTL 过期兜底
|
||||
- **作用域**:单实例内存,不跨实例共享
|
||||
- **接口**:提供 `Get`/`Set`/`Delete`/`Clear` 方法,同时供 handler 层(缓存管理 API)和 service 层(去重检查)调用
|
||||
- **接口**:提供 `Get`/`Set`/`Delete`/`Clear` 方法,供 service 层(去重检查)调用
|
||||
|
||||
+100
-111
@@ -4,12 +4,12 @@
|
||||
|
||||
| 用途 | 方案 | 说明 |
|
||||
|------|------|------|
|
||||
| 持久化存储 | MySQL 8.0+ | 工程、任务、素材元数据、风格配置全部落库 |
|
||||
| 文件存储 | OSS 对象存储 | 生成的图片与 spritesheet 文件上传至 S3 兼容存储桶,通过 CDN URL 访问 |
|
||||
| 认证 | JWT | 简单注册登录,Bearer Token 鉴权 |
|
||||
| 缓存 / 去重 | MySQL + 应用层内存 | 请求去重通过数据库唯一约束 + 应用层 sync.Map 实现短期去重窗口,不引入 Redis |
|
||||
| 持久化存储 | SQLite 3 | 工程、任务、素材元数据、风格配置全部落库,单文件部署 |
|
||||
| 文件存储 | 本地文件系统 | 生成的图片与 spritesheet 文件保存到本地目录,通过 Gin 静态文件服务访问 |
|
||||
| 认证 | JWT + Cookie | 简单注册登录,httpOnly Cookie 鉴权 |
|
||||
| 缓存 / 去重 | 应用层内存 | 请求去重通过应用层 `sync.Map` 实现短期去重窗口,无需外部依赖 |
|
||||
|
||||
**不用 Redis 的理由**:gen2d 是单实例部署的工具型应用,并发量有限,任务状态轮询即可满足实时性需求。MySQL 完全能覆盖缓存、去重、队列语义,引入 Redis 增加运维复杂度但收益不大。
|
||||
**选用 SQLite 的理由**:gen2d 是单实例部署的工具型应用,SQLite 零配置、单文件、性能足够,无需额外数据库服务。
|
||||
|
||||
---
|
||||
|
||||
@@ -18,32 +18,30 @@
|
||||
### user — 用户
|
||||
|
||||
```sql
|
||||
CREATE TABLE `user` (
|
||||
`id` CHAR(20) NOT NULL, -- 用户 ID,如 user_a1B2c3
|
||||
`username` VARCHAR(64) NOT NULL, -- 登录用户名
|
||||
`email` VARCHAR(128) NOT NULL, -- 邮箱
|
||||
`password_hash` VARCHAR(128) NOT NULL, -- bcrypt 哈希
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
UNIQUE KEY `uk_email` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE user (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 用户 ID,如 user_a1B2c3
|
||||
username TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL, -- bcrypt 哈希
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_username ON user (username);
|
||||
CREATE UNIQUE INDEX uk_email ON user (email);
|
||||
```
|
||||
|
||||
### project — 工程
|
||||
|
||||
```sql
|
||||
CREATE TABLE `project` (
|
||||
`id` CHAR(20) NOT NULL, -- 项目 ID,如 proj_abc123
|
||||
`user_id` CHAR(20) NOT NULL, -- 所属用户
|
||||
`name` VARCHAR(128) NOT NULL, -- 工程名称
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user` (`user_id`, `created_at`),
|
||||
CONSTRAINT `fk_project_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE project (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 项目 ID,如 proj_abc123
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_project_user ON project (user_id, created_at);
|
||||
```
|
||||
|
||||
### project_style — 工程风格
|
||||
@@ -51,58 +49,55 @@ CREATE TABLE `project` (
|
||||
工程级键值对,保证同一工程下所有素材风格一致。每个工程恰好一行。
|
||||
|
||||
```sql
|
||||
CREATE TABLE `project_style` (
|
||||
`id` CHAR(20) NOT NULL,
|
||||
`project_id` CHAR(20) NOT NULL,
|
||||
`kv_pairs` JSON NOT NULL, -- {"artStyle":"pixel","palette":"warm",...}
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_project` (`project_id`),
|
||||
CONSTRAINT `fk_style_project` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE project_style (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
kv_pairs TEXT NOT NULL, -- JSON: {"artStyle":"pixel","palette":"warm",...}
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_style_project ON project_style (project_id);
|
||||
```
|
||||
|
||||
### task — 生成任务
|
||||
|
||||
```sql
|
||||
CREATE TABLE `task` (
|
||||
`id` CHAR(20) NOT NULL, -- 任务 ID,如 task_xyz789
|
||||
`project_id` CHAR(20) NOT NULL,
|
||||
`prompt` TEXT NOT NULL, -- 用户原始文本
|
||||
`asset_type` VARCHAR(32) NOT NULL, -- sprite / background / ui / animation
|
||||
`task_style` JSON NULL, -- 任务级风格覆盖,可为 NULL
|
||||
`params` JSON NOT NULL DEFAULT '{}', -- {"resolution":64,"frames":{...},"format":"spritesheet"}
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending / running / completed / failed
|
||||
`stage` VARCHAR(32) NULL, -- 当前管线阶段:prompt_builder / asset_generator / quality_supervisor / format_adapter
|
||||
`progress` TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0-100
|
||||
`retry_count` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`error` TEXT NULL, -- 失败原因
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_project` (`project_id`, `created_at`),
|
||||
KEY `idx_status` (`status`),
|
||||
CONSTRAINT `fk_task_project` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE task (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 任务 ID,如 task_xyz789
|
||||
project_id TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL, -- 用户原始文本
|
||||
asset_type TEXT NOT NULL, -- sprite / background / ui / animation
|
||||
task_style TEXT, -- JSON: 任务级风格覆盖,可为 NULL
|
||||
params TEXT NOT NULL DEFAULT '{}', -- JSON: {"resolution":64,"frames":{...},"format":"spritesheet"}
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending / running / completed / failed
|
||||
stage TEXT, -- 当前管线阶段
|
||||
progress INTEGER NOT NULL DEFAULT 0, -- 0-100
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT, -- 失败原因
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_task_project ON task (project_id, created_at);
|
||||
CREATE INDEX idx_task_status ON task (status);
|
||||
```
|
||||
|
||||
### asset — 生成素材
|
||||
|
||||
```sql
|
||||
CREATE TABLE `asset` (
|
||||
`id` CHAR(20) NOT NULL, -- 素材 ID,如 asset_001
|
||||
`task_id` CHAR(20) NOT NULL,
|
||||
`url` VARCHAR(512) NOT NULL, -- OSS 完整 URL 或相对路径
|
||||
`format` VARCHAR(16) NOT NULL DEFAULT 'png', -- png / json
|
||||
`width` INT NOT NULL DEFAULT 0,
|
||||
`height` INT NOT NULL DEFAULT 0,
|
||||
`metadata` JSON NULL, -- {"frameWidth":64,"frameHeight":64,"frameCount":4,"directions":1}
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_task` (`task_id`),
|
||||
CONSTRAINT `fk_asset_task` FOREIGN KEY (`task_id`) REFERENCES `task` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE asset (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- 素材 ID,如 asset_001
|
||||
task_id TEXT NOT NULL,
|
||||
url TEXT NOT NULL, -- 相对路径,如 tasks/task_xyz/output/spritesheet.png
|
||||
format TEXT NOT NULL DEFAULT 'png', -- png / json
|
||||
width INTEGER NOT NULL DEFAULT 0,
|
||||
height INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT, -- JSON: {"frameWidth":64,"frameHeight":64,"frameCount":4,"directions":1}
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_asset_task ON asset (task_id);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -122,26 +117,28 @@ task 1 ── N asset
|
||||
|
||||
## ID 生成
|
||||
|
||||
使用 **Sonyflake**(或类似分布式 ID 方案)生成 20 字符的字符串 ID,格式为 `{前缀}_{base62}`:
|
||||
使用前缀 + 随机字符串生成 ID,格式为 `{prefix}_{random}`:
|
||||
|
||||
| 实体 | 前缀 | 示例 |
|
||||
|------|------|------|
|
||||
| user | `user` | `user_a1B2c3D4` |
|
||||
| project | `proj` | `proj_3kF9a2Bc` |
|
||||
| project_style | `sty` | `sty_7xM2pQ1d` |
|
||||
| task | `task` | `task_9bN4vR8e` |
|
||||
| asset | `asset` | `asset_2wK6tY5f` |
|
||||
| user | `user` | `user_a1B2c3` |
|
||||
| project | `proj` | `proj_3kF9a2` |
|
||||
| project_style | `sty` | `sty_7xM2pQ` |
|
||||
| task | `task` | `task_9bN4vR` |
|
||||
| asset | `asset` | `asset_2wK6tY` |
|
||||
|
||||
随机部分使用 `crypto/rand` 生成 6 字节 base62 编码,兼顾可读性与唯一性。
|
||||
|
||||
---
|
||||
|
||||
## 文件存储
|
||||
|
||||
使用 S3 兼容对象存储(如阿里云 OSS、MinIO),用户生成的素材持久化保存,便于重复利用。
|
||||
生成的素材文件保存到本地文件系统,通过 Gin 静态文件服务访问。
|
||||
|
||||
### OSS Key 结构
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
{bucket}/
|
||||
{dataDir}/
|
||||
└── users/
|
||||
└── {userId}/
|
||||
└── projects/
|
||||
@@ -158,15 +155,22 @@ task 1 ── N asset
|
||||
└── pipeline.log # 管线执行日志
|
||||
```
|
||||
|
||||
`dataDir` 通过环境变量 `GEN2D_DATA_DIR` 配置,默认 `./data`。
|
||||
|
||||
### 访问方式
|
||||
|
||||
- **开发环境**:Gin 静态文件服务,路由 `/files/*` 映射到本地 `data/` 目录,无需 OSS。
|
||||
- **生产环境**:文件上传至 OSS 存储桶,`asset.url` 存储完整 CDN URL,前端直接访问。
|
||||
Gin 静态文件服务,路由 `/files/*` 映射到 `{dataDir}/` 目录:
|
||||
|
||||
```go
|
||||
router.Static("/files", dataDir)
|
||||
```
|
||||
|
||||
### asset 表的 url 字段
|
||||
|
||||
- 开发环境:相对路径,如 `projects/proj_abc/tasks/task_xyz/output/spritesheet.png`,前端通过 `/files/{url}` 拼接。
|
||||
- 生产环境:完整 URL,如 `https://cdn.example.com/users/user_xxx/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`。
|
||||
存储相对路径,前端通过 `/files/{url}` 拼接访问:
|
||||
|
||||
- 如 `users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`
|
||||
- 前端拼接为 `/files/users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`
|
||||
|
||||
---
|
||||
|
||||
@@ -180,8 +184,6 @@ task 1 ── N asset
|
||||
- 若不存在,写入内存并提交任务。
|
||||
3. 任务完成或失败后,从内存中清除(可设置 TTL 过期兜底)。
|
||||
|
||||
不依赖数据库唯一约束做去重,因为相同输入在不同时间点应该允许重新生成。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
@@ -191,17 +193,10 @@ task 1 ── N asset
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `GEN2D_SERVER_PORT` | `8080` | HTTP 服务监听端口 |
|
||||
| `GEN2D_DB_DSN` | `root:@tcp(127.0.0.1:3306)/gen2d?charset=utf8mb4&parseTime=True&loc=Local` | MySQL DSN |
|
||||
| `GEN2D_DB_MAX_OPEN` | `25` | 最大连接数 |
|
||||
| `GEN2D_DB_MAX_IDLE` | `5` | 最大空闲连接数 |
|
||||
| `GEN2D_DB_MAX_LIFETIME` | `300` | 连接最大存活时间(秒) |
|
||||
| `GEN2D_DB_PATH` | `./data/gen2d.db` | SQLite 数据库文件路径 |
|
||||
| `GEN2D_DATA_DIR` | `./data` | 本地文件存储根目录 |
|
||||
| `GEN2D_JWT_SECRET` | — | JWT 签名密钥(必填) |
|
||||
| `GEN2D_JWT_EXPIRE` | `7200` | Token 过期时间(秒) |
|
||||
| `GEN2D_OSS_ENDPOINT` | — | OSS Endpoint(生产环境必填) |
|
||||
| `GEN2D_OSS_BUCKET` | — | 存储桶名称 |
|
||||
| `GEN2D_OSS_ACCESS_KEY` | — | Access Key ID |
|
||||
| `GEN2D_OSS_SECRET_KEY` | — | Access Key Secret |
|
||||
| `GEN2D_OSS_CDN_DOMAIN` | — | CDN 域名(可选,用于拼接公开访问 URL) |
|
||||
|
||||
在 `config.go` 中扩展字段即可,无需额外依赖。
|
||||
|
||||
@@ -209,23 +204,17 @@ task 1 ── N asset
|
||||
|
||||
## 迁移
|
||||
|
||||
使用 [golang-migrate](https://github.com/golang-migrate/migrate) 管理 schema 版本:
|
||||
应用启动时自动执行建表 SQL,保证 schema 与代码版本一致:
|
||||
|
||||
```
|
||||
backend/internal/migrations/
|
||||
├── 000001_init_user.up.sql
|
||||
├── 000001_init_user.down.sql
|
||||
├── 000002_init_project.up.sql
|
||||
├── 000002_init_project.down.sql
|
||||
├── 000003_init_task.up.sql
|
||||
├── 000003_init_task.down.sql
|
||||
├── 000004_init_asset.up.sql
|
||||
└── 000004_init_asset.down.sql
|
||||
```go
|
||||
// 启动时执行
|
||||
db.Exec(createUserTableSQL)
|
||||
db.Exec(createProjectTableSQL)
|
||||
db.Exec(createTaskTableSQL)
|
||||
db.Exec(createAssetTableSQL)
|
||||
```
|
||||
|
||||
启动时自动执行 `migrate.Up()`,保证 schema 与代码版本一致。
|
||||
|
||||
> 注:`project_style` 表的创建包含在 `000002_init_project.up.sql` 中,与 `project` 表同批迁移。
|
||||
使用 `CREATE TABLE IF NOT EXISTS` 保证幂等性。
|
||||
|
||||
---
|
||||
|
||||
@@ -235,8 +224,8 @@ backend/internal/migrations/
|
||||
|----|------|------|
|
||||
| `user` | `uk_username (username)` | 用户名唯一约束 |
|
||||
| `user` | `uk_email (email)` | 邮箱唯一约束 |
|
||||
| `project` | `idx_user (user_id, created_at)` | 用户下工程列表分页查询 |
|
||||
| `project_style` | `uk_project (project_id)` | 一个工程一个风格,唯一约束 |
|
||||
| `task` | `idx_project (project_id, created_at)` | 工程下任务列表分页查询 |
|
||||
| `task` | `idx_status (status)` | 按状态筛选任务(如查找所有 pending 任务) |
|
||||
| `asset` | `idx_task (task_id)` | 任务下素材列表查询 |
|
||||
| `project` | `idx_project_user (user_id, created_at)` | 用户下工程列表分页查询 |
|
||||
| `project_style` | `uk_style_project (project_id)` | 一个工程一个风格,唯一约束 |
|
||||
| `task` | `idx_task_project (project_id, created_at)` | 工程下任务列表分页查询 |
|
||||
| `task` | `idx_task_status (status)` | 按状态筛选任务 |
|
||||
| `asset` | `idx_asset_task (task_id)` | 任务下素材列表查询 |
|
||||
|
||||
+5
-4
@@ -10,7 +10,7 @@ Vite 6 + React 18 + TypeScript + zustand + react-router-dom
|
||||
| UI 框架 | React 18 | 函数组件 + Hooks |
|
||||
| 状态管理 | zustand | 轻量,支持 devtools 中间件 |
|
||||
| 路由 | react-router-dom v6 | SPA 模式 |
|
||||
| 实时通信 | 原生 WebSocket | 连接后端 `/api/v1/tasks/:taskId/ws` |
|
||||
| 实时通信 | 原生 WebSocket | 连接后端 `/api/v1/tasks/:taskId/ws`(Cookie 认证) |
|
||||
| HTTP 请求 | fetch + 封装层 | 统一错误处理、响应解包 |
|
||||
|
||||
## 目录结构
|
||||
@@ -97,7 +97,7 @@ sequenceDiagram
|
||||
User->>GP: 点击提交
|
||||
GP->>API: POST /api/v1/generate
|
||||
API-->>GP: 返回 taskId
|
||||
GP->>WS: ws://host/api/v1/tasks/:taskId/ws
|
||||
GP->>WS: ws://host/api/v1/tasks/:taskId/ws(Cookie 自动携带)
|
||||
loop 管线执行中
|
||||
WS-->>GP: PipelineProgress(stage + progress)
|
||||
GP->>GP: ProgressBar 更新阶段和进度
|
||||
@@ -141,7 +141,7 @@ sequenceDiagram
|
||||
### API 请求错误
|
||||
|
||||
- `api/client.ts` 统一拦截 `code !== 0` 的响应,抛出业务异常
|
||||
- 401:自动清除本地 Token,跳转到登录页(或提示重新登录)
|
||||
- 401:跳转到登录页(Token 已过期或无效,Cookie 会被服务端清除)
|
||||
- 403:提示无权访问,不自动跳转
|
||||
- 429:提示请求过于频繁,稍后重试
|
||||
- 500:展示通用错误提示
|
||||
@@ -224,9 +224,10 @@ type PipelineStage =
|
||||
`api/client.ts` 统一封装:
|
||||
|
||||
- baseURL 从环境变量读取,开发模式默认 `/api/v1`
|
||||
- 所有 fetch 请求设置 `credentials: 'include'`,自动携带 Cookie
|
||||
- 所有响应按 `{ code, message, data }` 解包,`code !== 0` 时抛错
|
||||
- 统一 401/403/500 错误处理
|
||||
- WebSocket 连接封装为 `createTaskSocket(taskId)` 返回可订阅对象
|
||||
- WebSocket 连接封装为 `createTaskSocket(taskId)` 返回可订阅对象(浏览器自动携带 Cookie)
|
||||
|
||||
### API 类型定义(api/types.ts)
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# 多阶段生成管线
|
||||
|
||||
gen2d 的核心生成流程采用 Eino `compose.Graph` 编排的四阶段管线,含质检不通过时的重生成分支。管线编排与节点实现详见 [后端工程 — 管线设计](backend.md#管线设计eino-composegraph)。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["PromptBuilder\n(提示词工程)"] --> B["AssetGenerator\n(AI 出图)"]
|
||||
B --> C["QualitySupervisor\n(质检)"]
|
||||
C -- pass --> D["FormatAdapter\n(格式适配)"]
|
||||
C -- fail --> A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. PromptBuilder
|
||||
|
||||
- **输入**:用户文本 + 素材类型标签 + 工程风格 + 任务风格覆盖 + 技术参数
|
||||
- **输出**:三段式完整生成提示词
|
||||
- **职责**:
|
||||
1. 解析用户文本,提取核心内容作为【主题】
|
||||
2. 合并工程风格与任务风格覆盖(任务同名键覆盖工程),生成【约束】(含负面提示词)
|
||||
3. 注入素材类型、分辨率等技术参数作为【内容】
|
||||
4. 拼接为最终提示词
|
||||
|
||||
### 风格合并
|
||||
|
||||
```go
|
||||
finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs)
|
||||
```
|
||||
|
||||
工程风格保证同一工程下所有素材风格一致;任务风格仅覆盖需要差异化的键。
|
||||
|
||||
### 三段式提示词结构
|
||||
|
||||
```
|
||||
【主题】用户的原始描述核心内容
|
||||
【约束】风格键值对生成的约束条件 + 负面提示词
|
||||
【内容】素材类型、分辨率、帧数等技术参数
|
||||
```
|
||||
|
||||
## 2. AssetGenerator
|
||||
|
||||
- **输入**:PromptBuilder 的输出
|
||||
- **输出**:原始生成图片(单张或多张)
|
||||
- **职责**:调用 AI 推理 API 出图。不同素材类型的差异化需求已在上一步注入提示词中。
|
||||
- **接口规范**:统一采用 OpenAI 兼容的 `/v1/images/generations` 格式。多张生成通过 `n` 参数请求,实际支持数量取决于后端模型(如 DALL-E 3 仅支持 `n=1`,需多次调用模拟多张)。
|
||||
|
||||
## 3. QualitySupervisor
|
||||
|
||||
- **输入**:原始生成图片 + 风格配置
|
||||
- **输出**:通过 / 不通过 + 不通过的原因
|
||||
- **职责**:通过视觉模型检查素材是否符合提示词要求与风格约束。不通过时,Graph 分支将 RejectReason 回填到 PipelineState,路由回 PromptBuilder 重新生成(最多 3 次,超过则降级输出)。
|
||||
|
||||
## 4. FormatAdapter
|
||||
|
||||
- **输入**:通过质检的图片 + 素材类型
|
||||
- **输出**:游戏引擎可用的素材文件 + 元数据
|
||||
- **职责**:格式转换、spritesheet 打包、元数据生成。
|
||||
- **示例**:输入 4 张 64×64 的角色行走帧 PNG → 输出一张 256×64 的 spritesheet + JSON 元数据(帧尺寸、帧数、锚点偏移),可直接拖入 Unity 2D Animation 使用。
|
||||
|
||||
---
|
||||
|
||||
管线执行过程中的进度推送与重试机制详见 [异步任务](async-tasks.md)。
|
||||
|
||||
Reference in New Issue
Block a user