diff --git a/docs/_index.md b/docs/_index.md index d76ac41..abd1d6f 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -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) — 美术风格、色调、线条等风格键分类与可选值 diff --git a/docs/api.md b/docs/api.md index 26136c0..9a77646 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,13 +8,16 @@ ## 认证 -除健康检查和注册登录外,所有接口需在请求头携带 Bearer Token: +除健康检查和注册登录外,所有接口需通过 httpOnly Cookie 携带 JWT。登录/注册成功后,服务端通过 `Set-Cookie` 响应头写入 Token,后续请求自动携带。 -``` -Authorization: Bearer -``` +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=`(通过 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 - } -} -``` diff --git a/docs/async-tasks.md b/docs/async-tasks.md index 2cbd0f5..89e6e40 100644 --- a/docs/async-tasks.md +++ b/docs/async-tasks.md @@ -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= +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= ## 文件存储 -任务完成后,FormatAdapter 将素材上传至 OSS。素材文件的存储结构与访问方式详见 [数据存储 — 文件存储](database.md#文件存储)。 +任务完成后,FormatAdapter 将素材保存到本地文件系统。素材文件的存储结构与访问方式详见 [数据存储 — 文件存储](database.md#文件存储)。 diff --git a/docs/backend.md b/docs/backend.md index 7178701..104b562 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -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 层(去重检查)调用 diff --git a/docs/database.md b/docs/database.md index be11f9b..1658709 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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)` | 任务下素材列表查询 | diff --git a/docs/frontend.md b/docs/frontend.md index 2cb7afe..3c56e22 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -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) diff --git a/docs/multi-agent-pipeline.md b/docs/multi-agent-pipeline.md deleted file mode 100644 index 606b2b1..0000000 --- a/docs/multi-agent-pipeline.md +++ /dev/null @@ -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)。 -