27 Commits

Author SHA1 Message Date
wonder b95bcde445 chore: 移除 Gitee 平台相关配置文件 2026-05-29 16:57:56 +08:00
wonder c7b2313351 docs: 新增限流与协程池实施计划 2026-05-29 16:55:37 +08:00
wonder 39cef82eff docs: 补充哔哩哔哩视频链接
Signed-off-by: 何朝晖 <hezhaohui0807@163.com>
2026-05-25 15:39:34 +00:00
wonder 0493d900dc !81 docs: 优化 README
Merge pull request !81 from 何朝晖/N/A
2026-05-25 13:55:05 +00:00
wonder cccd1c5428 docs: 优化 README
Signed-off-by: 何朝晖 <hezhaohui0807@163.com>
2026-05-25 13:54:43 +00:00
Gmarker689 03cecab59b !80 fix: 将帧动画与单独帧分开为独立分类
Merge pull request !80 from 郭永昊/fix/tag-labels-and-click
2026-05-25 13:37:12 +00:00
Gmarker689 9af74408b8 fix: 将帧动画与单独帧分开为独立分类
- 新增「帧动画」分类:匹配 metaType=preview 或 format=gif
- 「帧」分类仅匹配 metaType=frame(静态单帧)
2026-05-25 21:36:04 +08:00
wonder 5a6aba16a0 !79 fix: 详情弹窗使用 Portal 渲染到 body,确保始终居中
Merge pull request !79 from 郭永昊/fix/tag-labels-and-click
2026-05-25 13:35:49 +00:00
wonder 433bf88698 Merge branch 'master' of gitee.com:hezhaohui123/gen2d into fix/tag-labels-and-click
Signed-off-by: 何朝晖 <hezhaohui0807@163.com>
2026-05-25 13:35:26 +00:00
Gmarker689 882e05955b fix: 详情弹窗使用 Portal 渲染到 body,确保始终居中
父级 .page-enter 动画中的 transform 创建了新的 CSS containing block,
导致 position:fixed 相对父级而非视口定位。改用 createPortal 渲染到
document.body 绕过此限制。
2026-05-25 21:31:27 +08:00
Gmarker689 55aaa26549 docs: 在 README 架构章节添加全局/后端/前端架构图 2026-05-25 21:30:35 +08:00
Gmarker689 96285014b1 feat: 首页素材点击查看详情,支持下载
- 后端 GetRecentAssets 增加 projectName/width/height/projectId 字段
- 前端 AssetGallery 点击图片弹出详情弹窗:提示词/工程/分辨率/格式
- 详情弹窗提供下载按钮,走 /api/v1/assets/download?key= 接口
2026-05-25 21:28:54 +08:00
Gmarker689 e650cea11c !77 feat: 首页素材画廊增加分类筛选标签
Merge pull request !77 from 郭永昊/fix/tag-labels-and-click
2026-05-25 13:08:38 +00:00
Gmarker689 1a78744766 feat: 首页素材画廊增加分类筛选标签
- 后端 SQL 增加 json_extract 提取素材 metaType (frame/spritesheet/preview)
- 前端 AssetGallery 新增分类标签栏:全部/精灵表/帧/场景瓦片/背景/UI
- 按 metaType + assetType 组合过滤展示
2026-05-25 21:07:15 +08:00
wonder d611a2c142 !76 docs: 添加全局/后端/前端架构图生成提示词
Merge pull request !76 from 何朝晖/docs/README
2026-05-25 13:04:39 +00:00
Gmarker689 b88f9aaf03 feat: 首页展示最近生成的素材画廊
- 后端新增 GET /api/v1/assets 接口,返回已完成任务的素材列表(按创建时间倒序)
- 前端新增 AssetGallery 组件,网格展示素材缩略图、类型标签和提示词
- HomePage 中工程列表上方展示最近生成素材
2026-05-25 21:02:03 +08:00
wonder 8ac039ef38 docs: 添加全局/后端/前端架构图生成提示词
新增三个多模态 AI 架构图提示词文件,分别对应全局系统架构、
后端分层与管线、前端组件树与数据流的可视图生成。
2026-05-25 20:52:59 +08:00
Gmarker689 5be74e3ce1 feat: 左上角品牌图标更换为自定义 logo
- 文本 "gen2d" 替换为 logo.png 图片
- 同时更新浏览器 favicon
2026-05-25 20:52:06 +08:00
Gmarker689 8e3b576954 feat: 更换网页图标为自定义 favicon 2026-05-25 20:49:47 +08:00
wonder e8abc41c2a docs: 优化 README 与文档索引结构
- README 新增目录导航、参考链接、路线图、文档索引模块
- README 采用居中标题、Badge 行、快速链接栏的顶级开源项目风格
- 技术栈表增加说明列,项目结构细化各模块用途
- API 已实现/规划中拆分展示,合并近期新增接口
- docs/_index 新增 ASCII 系统架构图、核心概念概览、命令行参考
2026-05-25 20:48:50 +08:00
wonder d30bee18a7 !75 !74 fix: 任务侧边栏点击详情后"任务不存在"问题
Merge pull request !75 from 何朝晖/develop
2026-05-25 12:30:29 +00:00
Gmarker689 8657fe5d28 !74 fix: 任务侧边栏点击详情后"任务不存在"问题
Merge pull request !74 from 郭永昊/fix/tag-labels-and-click
2026-05-25 12:29:49 +00:00
Gmarker689 c7bd02ad49 !72 fix: 输出格式根据素材类型自动设置默认值
Merge pull request !72 from 何朝晖/perfect/frontend-tag
2026-05-25 12:29:34 +00:00
Gmarker689 acef46063c fix: 任务侧边栏点击详情后"任务不存在"问题
GetTasks 返回的 TaskResponse.ID 使用了数据库自增 ID (t.ID) 而非 ExternalID,
导致前端用错误 ID 调用 GetTask 接口时 external_id 查询不到记录。
改为返回 t.ExternalID 与实际查询字段一致。
2026-05-25 20:19:09 +08:00
Gmarker689 7921edd71e fix: 任务详情页 JWT 鉴权失败导致回退登录页
- TaskListPanel 用 React Router <Link> 替代原生 <a href>,避免整页刷新
- auth store 初始化时同步读取 token 设置 isAuthenticated,避免 useEffect 异步执行前 ProtectedRoute 误判未登录
2026-05-25 20:15:06 +08:00
Gmarker689 47b31b5c13 !71 !70 fix: 自定义标签融入原生标签栏,点击切换而非消失
Merge pull request !71 from 郭永昊/develop
2026-05-25 12:06:51 +00:00
Gmarker689 7cc472052f !68 !67 fix: 修复管线context取消、添加FIFO任务队列、移除重复提示词优化
Merge pull request !68 from 何朝晖/develop
2026-05-25 11:13:54 +00:00
24 changed files with 1585 additions and 154 deletions
-13
View File
@@ -1,13 +0,0 @@
### 该问题是怎么引起的?
### 重现步骤
### 报错信息
-12
View File
@@ -1,12 +0,0 @@
### 相关的Issue
### 原因(目的、解决的问题等)
### 描述(做了什么,变更了什么)
### 测试用例(新增、改动、可能影响的功能)
-32
View File
@@ -1,32 +0,0 @@
version: '1.0'
name: pipeline-20260525
displayName: pipeline-20260525
triggers:
trigger: auto
push:
branches:
precise:
- master
stages:
- name: stage-b0e54961
displayName: deploy
strategy: naturally
trigger: auto
executor: []
steps:
- step: shell@agent
name: execute_shell
displayName: Shell 脚本执行
schedulerType: DISTRIBUTED
hostGroupID:
ID: i-f8zc4nf8y8u267zwn9vw
hostID:
- 0c13eac4-c35d-455e-a162-ce11a0ab9e37
scriptType: SH
script:
- cd ~/gen2d
- ./deploy.sh
gitClone: false
notify: []
strategy:
retry: '0'
+208 -81
View File
@@ -1,48 +1,83 @@
# gen2d
AI 驱动的 2D 游戏素材生成工具。通过文本提示词或可视化参数,生成风格一致、管线友好的 Sprite、背景、UI 元素与动画帧,无缝融入 Unity / Godot 等主流 2D 游戏引擎工作流。
<p align="center">
<b>AI 驱动的 2D 游戏素材生成工具</b><br/>
文本描述 + 风格参数 → 风格一致、管线友好的 Sprite、背景、UI 与动画帧<br/>
无缝融入 Unity / Godot 等主流 2D 游戏引擎工作流
</p>
[![Go](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Gin](https://img.shields.io/badge/Gin-1.12-008ECF?logo=gin&logoColor=white)](https://gin-gonic.com/)
[![Eino](https://img.shields.io/badge/Eino-0.8-blue)](https://github.com/cloudwego/eino)
[![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=black)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-6-646CFF?logo=vite&logoColor=white)](https://vite.dev/)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
<p align="center">
<a href="https://go.dev/"><img src="https://img.shields.io/badge/Go-1.26-00ADD8?logo=go&logoColor=white" alt="Go" /></a>
<a href="https://gin-gonic.com/"><img src="https://img.shields.io/badge/Gin-1.12-008ECF?logo=gin&logoColor=white" alt="Gin" /></a>
<a href="https://github.com/cloudwego/eino"><img src="https://img.shields.io/badge/Eino-0.8-blue" alt="Eino" /></a>
<a href="https://react.dev/"><img src="https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=black" alt="React" /></a>
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?logo=typescript&logoColor=white" alt="TypeScript" /></a>
<a href="https://vite.dev/"><img src="https://img.shields.io/badge/Vite-6-646CFF?logo=vite&logoColor=white" alt="Vite" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License" /></a>
</p>
### **[在线体验 http://47.121.181.112:10000](http://47.121.181.112:10000)**
<p align="center">
<b><a href="http://47.121.181.112:10000">在线体验</a></b>
&nbsp;|&nbsp;
<b><a href="docs/_index.md">文档</a></b>
&nbsp;|&nbsp;
<b><a href="#快速开始">快速开始</a></b>
&nbsp;|&nbsp;
<b><a href="#api">API</a></b>
</p>
> 🎈 **哔哩哔哩视频**:【七牛云 XEngineer 暑期实训营 | 2D 游戏素材 | 团体作品演示】 https://www.bilibili.com/video/BV1jAGo6DEbC
> 🔥 **在线体验**:http://47.121.181.112:10000/
> 详细架构说明见 [docs/_index.md](docs/_index.md)。
![全局架构](assets/global-architecture.png)
![后端架构](assets/backend-architecture.png)
![前端架构](assets/frontend-architecture.png)
---
## 目录
- [特性](#特性)
- [快速开始](#快速开始)
- [配置](#配置)
- [架构](#架构)
- [生成管线](#生成管线)
- [提示词优化链路](#提示词优化链路)
- [预设风格键](#预设风格键)
- [API](#api)
- [技术栈](#技术栈)
- [项目结构](#项目结构)
- [部署](#部署)
- [文档](#文档)
- [路线图](#路线图)
- [参与贡献](#参与贡献)
- [参考链接](#参考链接)
- [License](#license)
## 特性
- **多智能体协作管线** — PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter,基于 Eino `compose.Graph` 编排
- **多智能体协作管线** — PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter,基于 Eino `compose.Graph` 编排,支持质检重试与降级输出
- **提示词优化** — Eino Chain 驱动的 PromptAgent,支持任意 OpenAI 兼容 API,无 key 时自动回退模板生成
- **风格系统** — 预设美术风格、色调、线条、场景、光照、情绪等维度,支持工程级与任务级风格覆盖
- **实时进度** — WebSocket 推送管线各阶段状态、进度与质检重试信息
- **异步任务** — 内存队列 + 通道模型,支持并发控制、失败重试(最多 3 次)与降级输出
- **请求去重** — 相同 `prompt + assetType + params` 的并发请求自动合并
- **异步任务** — goroutine 模型,支持并发控制、失败重试(最多 3 次)与降级输出
- **请求去重** — 相同 `prompt + assetType + params` 的并发请求自动合并,返回已有 taskId
- **暗色主题** — 前端支持 Light / Dark 双主题,CSS 自定义属性驱动
- **一键部署** — Docker Compose 双容器架构 + `deploy.sh` 脚本 + Gitee CI/CD 流水线
## 技术栈
| 层 | 技术 |
|---|------|
| 后端语言 | Go 1.26 |
| HTTP 框架 | Gin v1.12 |
| 管线编排 | [Eino](https://github.com/cloudwego/eino) v0.8 — `compose.Graph` + `compose.Chain` |
| ORM | GORM + SQLite |
| 认证 | JWT (golang-jwt) + bcrypt |
| 配置 | Viper + godotenv(环境变量 > config.yml > 默认值) |
| 前端框架 | React 18 + TypeScript 5.6 |
| 构建工具 | Vite 6 |
| 状态管理 | Zustand 5 |
| 路由 | react-router-dom v7 |
| 样式 | CSS Modules + CSS 自定义属性 |
| 容器化 | Docker 多阶段构建 + nginx 反向代理 |
| CI/CD | Gitee Workflow + deploy.sh |
- **一键部署** — Docker Compose 双容器架构 + `deploy.sh` 脚本
## 快速开始
### 环境要求
- Go 1.26+
- Node.js 20+
- Docker & Docker Compose(部署用)
### 后端
```bash
@@ -62,14 +97,14 @@ npm install
npm run dev
```
开发服务器默认运行在 `http://localhost:5173`,通过 Vite proxy 转发 `/api` 和 `/auth` 请求到后端。
开发服务器运行在 `http://localhost:5173`,通过 Vite proxy 转发 `/api` 和 `/auth` 请求到后端。
## 配置
配置通过 Viper 读取,优先级从高到低:
| 优先级 | 来源 | 说明 |
|--------|------|------|
|:---:|------|------|
| 1 | 环境变量 | 适合 Docker / CI,敏感字段推荐使用 |
| 2 | `.env` 文件 | 位于 `backend/.env`,通过 godotenv 加载 |
| 3 | `config.yml` | 位于 `backend/internal/config/config.yml`,存放非敏感默认值 |
@@ -80,7 +115,7 @@ npm run dev
| 环境变量 | YAML 路径 | 默认值 | 说明 |
|----------|-----------|--------|------|
| `GEN2D_LLM_BASE_URL` | `llm.base_url` | `https://api.deepseek.com/v1` | LLM API 地址(兼容 OpenAI 接口) |
| `GEN2D_LLM_API_KEY` | `llm.api_key` | (空) | API 密钥,请通过环境变量设置 |
| `GEN2D_LLM_API_KEY` | `llm.api_key` | (空) | API 密钥 |
| `GEN2D_LLM_MODEL` | `llm.model` | `deepseek-v4-pro` | 模型名称 |
| `GEN2D_LLM_TEMPERATURE` | `llm.temperature` | `0.7` | 生成温度 (0-2) |
| `GEN2D_LLM_MAX_TOKENS` | `llm.max_tokens` | `2048` | 最大输出 token 数 |
@@ -90,7 +125,7 @@ npm run dev
| 环境变量 | YAML 路径 | 默认值 | 说明 |
|----------|-----------|--------|------|
| `GEN2D_IMAGE_BASE_URL` | `image_gen.base_url` | `https://api.stability.ai/v1` | 文生图 API 地址 |
| `GEN2D_IMAGE_API_KEY` | `image_gen.api_key` | (空) | API 密钥,请通过环境变量设置 |
| `GEN2D_IMAGE_API_KEY` | `image_gen.api_key` | (空) | API 密钥 |
| `GEN2D_IMAGE_MODEL` | `image_gen.model` | `stable-diffusion-xl` | 模型名称 |
| `GEN2D_IMAGE_WIDTH` | `image_gen.width` | `1024` | 生成图片宽度 |
| `GEN2D_IMAGE_HEIGHT` | `image_gen.height` | `1024` | 生成图片高度 |
@@ -138,6 +173,8 @@ graph TD
C --> D["三段式规范提示词<br/>【主题】【风格】【技术】"]
```
### 预设风格键
| 分类 | 键名 | 可选值示例 |
@@ -149,7 +186,7 @@ graph TD
| 光照 | `lighting` | bright, dim, dramatic, ambient, neon |
| 情绪 | `mood` | cheerful, dark, mysterious, epic, calm |
详细架构说明见 [docs/_index.md](docs/_index.md)。
## API
@@ -169,21 +206,87 @@ graph TD
| POST | `/auth/register` | 用户注册 |
| POST | `/auth/login` | 用户登录(返回 JWT Token) |
| POST | `/api/v1/prompt/optimize` | 提示词优化 |
| GET/POST | `/api/v1/projects` | 工程列表 / 创建工程 |
| GET/PUT/DELETE | `/api/v1/projects/:id` | 工程详情 / 更新 / 删除 |
| GET/PUT | `/api/v1/projects/:id/style` | 工程风格读写 |
| POST | `/api/v1/generate` | 提交生成任务 |
| GET | `/api/v1/tasks/:id` | 任务状态与进度 |
| GET | `/api/v1/tasks/:id/assets` | 生成结果素材列表 |
| GET | `/api/v1/assets/download` | 下载素材(重定向到 CDN) |
### 规划中
| 方法 | 路径 | 说明 |
|------|------|------|
| GET/POST | `/api/v1/projects` | 工程列表 / 创建工程 |
| GET/DELETE | `/api/v1/projects/:id` | 工程详情 / 删除工程 |
| GET/PUT | `/api/v1/projects/:id/style` | 工程风格读写 |
| POST | `/api/v1/generate` | 提交生成任务 |
| GET | `/api/v1/tasks/:id` | 任务状态与进度 |
| GET | `/api/v1/tasks/:id/assets` | 生成结果素材列表 |
| WS | `/api/v1/tasks/:id/ws` | WebSocket 实时进度推送 |
| GET | `/api/v1/auth/me` | 获取当前用户信息 |
| PUT | `/api/v1/auth/password` | 修改密码 |
完整 API 文档见 [docs/api.md](docs/api.md)。
## 技术栈
| 层 | 技术 | 说明 |
|---|------|------|
| 后端语言 | Go 1.26 | — |
| HTTP 框架 | Gin v1.12 | 路由、中间件、JSON 绑定 |
| 管线编排 | [Eino](https://github.com/cloudwego/eino) v0.8 | `compose.Graph` + `compose.Chain` |
| ORM | GORM + SQLite | 零配置数据库 |
| 认证 | JWT (golang-jwt) + bcrypt | 无状态认证 |
| 配置 | Viper + godotenv | 多层配置优先级 |
| 对象存储 | 七牛云 Kodo | CDN 加速、私有 Bucket 签名访问 |
| 前端框架 | React 18 + TypeScript 5.6 | 函数组件 + Hooks |
| 构建工具 | Vite 6 | HMR 开发体验 |
| 状态管理 | Zustand 5 | 轻量、不可变状态 |
| 路由 | react-router-dom v7 | SPA 路由 |
| 样式 | CSS Modules + CSS 自定义属性 | 主题切换 |
| 容器化 | Docker 多阶段构建 | nginx 反向代理 |
| CI/CD | Gitee Workflow | 自动构建部署 |
## 项目结构
```
gen2d/
├── backend/ # Go + Gin API 服务
│ ├── cmd/main.go # 入口:加载配置 → 初始化 DB → 注册路由 → 启动
│ ├── internal/
│ │ ├── config/ # Viper 配置加载 + config.yml
│ │ ├── db/ # SQLite + GORM 初始化
│ │ ├── handler/ # HTTP 处理器(health, prompt, auth, generate, storage)
│ │ ├── model/ # 数据模型(User, Response)
│ │ ├── mildware/ # 中间件(JWT 认证、日志、恢复)
│ │ ├── logger/ # 日志包(基于 log/slog)
│ │ └── service/ # 业务逻辑
│ │ ├── pipeline.go # Eino Graph 四阶段管线编排
│ │ ├── prompt_agent.go # Eino Chain 提示词优化
│ │ ├── nodes.go # 管线节点实现
│ │ ├── inference.go # 文生图推理
│ │ ├── auth.go # 注册 / 登录 / JWT
│ │ └── storage.go # 七牛云对象存储
│ └── pkg/splitsprite/ # 精灵表拆分工具
├── frontend/ # Vite + React 前端
│ └── src/
│ ├── api/ # HTTP 客户端与类型定义
│ ├── components/ # UI 组件
│ ├── pages/ # 页面(Login, Register, Home, Project, Result)
│ ├── stores/ # Zustand 状态管理
│ ├── hooks/ # 自定义 Hooks
│ ├── router/ # 路由配置
│ ├── utils/ # 工具函数
│ └── styles/ # 全局样式与主题变量
├── docs/ # 项目文档
│ ├── _index.md # 文档索引
│ ├── api.md # API 设计
│ ├── backend.md # 后端工程
│ ├── frontend.md # 前端工程
│ ├── database.md # 数据存储
│ ├── async-tasks.md # 异步任务
│ └── style-keys.md # 预设风格键
├── docker-compose.yml # 容器编排
├── deploy.sh # 一键部署脚本
└── .workflow/ # CI/CD 流水线配置
```
## 部署
### Docker Compose
@@ -196,7 +299,7 @@ docker compose up -d --build
```
- 后端:Go 多阶段构建,暴露 8080 端口
- 前端:Node 构建 + nginx 反向代理,默认映射到宿主机 10000 端口
- 前端:Node 构建 + nginx 反向代理,默认映射宿主机 10000 端口
- 数据持久化:`backend-data` 卷挂载 SQLite 数据库与生成素材
### 一键脚本
@@ -207,45 +310,33 @@ bash deploy.sh
自动完成仓库克隆/拉取、环境检查、`.env` 模板复制、`docker compose up`。
## 项目结构
## 文档
```
gen2d/
├── backend/ # Go + Gin API 服务
│ ├── cmd/main.go # 入口:加载配置 → 初始化 DB → 注册路由 → 启动
│ ├── internal/
│ │ ├── config/ # Viper 配置加载 + config.yml
│ │ ├── db/ # SQLite + GORM 初始化
│ │ ├── handler/ # HTTP 处理器(health, prompt, auth)
│ │ ├── model/ # 数据模型(User, Response)
│ │ └── service/ # 业务逻辑
│ │ ├── pipeline.go # Eino Graph 四阶段管线
│ │ ├── prompt_agent.go # Eino Chain 提示词优化
│ │ ├── nodes.go # 管线节点实现
│ │ ├── inference.go # 文生图推理(Mock)
│ │ └── auth.go # 注册 / 登录 / JWT
│ └── pkg/splitsprite/ # 精灵表拆分工具
├── frontend/ # Vite + React 前端
│ └── src/
│ ├── api/ # HTTP 客户端与类型定义
│ ├── components/ # UI 组件(StyleSelector, PromptEditor, GenerateForm...)
│ ├── pages/ # 页面(Login, Register, Project, Generate, Result)
│ ├── stores/ # Zustand 状态管理
│ ├── hooks/ # 自定义 Hooks
│ ├── router/ # 路由配置
│ └── styles/ # 全局样式与主题变量
├── docs/ # 项目文档
│ ├── _index.md # 文档索引
│ ├── api.md # API 设计
│ ├── backend.md # 后端工程
│ ├── frontend.md # 前端工程
│ ├── database.md # 数据存储
│ ├── async-tasks.md # 异步任务
│ └── style-keys.md # 预设风格键
├── docker-compose.yml # 容器编排
├── deploy.sh # 一键部署脚本
└── .workflow/deploy.yml # Gitee CI/CD 流水线
```
详细设计文档见 [docs/](docs/) 目录:
| 文档 | 说明 |
|------|------|
| [docs/_index.md](docs/_index.md) | 文档索引与架构概述 |
| [docs/api.md](docs/api.md) | API 接口设计、请求/响应示例、错误码 |
| [docs/backend.md](docs/backend.md) | 后端分层结构、管线设计、风格模型、缓存层 |
| [docs/frontend.md](docs/frontend.md) | 前端组件树、状态管理、路由、交互流程 |
| [docs/database.md](docs/database.md) | SQLite 选型、表结构、ER 关系、对象存储、去重策略 |
| [docs/async-tasks.md](docs/async-tasks.md) | 任务生命周期、并发控制、重试策略、进度推送 |
| [docs/style-keys.md](docs/style-keys.md) | 预设风格键分类与可选值 |
## 路线图
- [x] 提示词优化 Agent(Eino Chain)
- [x] 多智能体生成管线(Eino Graph)
- [x] 工程风格系统 + 任务级覆盖
- [x] 用户认证(JWT + bcrypt)
- [x] 异步任务 + 实时进度推送
- [x] 素材帧预览、GIF 动画与导出
- [x] 自定义标签
- [ ] WebSocket 实时进度推送(待完成)
- [ ] 批量生成与队列调度
- [ ] 文生图多模型适配(DALL-E, Midjourney)
- [ ] 素材版本管理与历史记录
## 参与贡献
@@ -254,6 +345,42 @@ gen2d/
3. 提交代码
4. 新建 Pull Request
## 参考链接
### 后端
- [Go](https://go.dev/) — 编程语言
- [Gin](https://gin-gonic.com/) — HTTP 框架
- [Eino](https://github.com/cloudwego/eino) — AI 管线编排框架
- [GORM](https://gorm.io/) — Go ORM
- [golang-jwt](https://github.com/golang-jwt/jwt) — JWT 库
- [Viper](https://github.com/spf13/viper) — 配置管理
### 前端
- [React](https://react.dev/) — UI 框架
- [TypeScript](https://www.typescriptlang.org/) — 类型安全
- [Vite](https://vite.dev/) — 构建工具
- [Zustand](https://github.com/pmndrs/zustand) — 状态管理
- [react-router](https://reactrouter.com/) — 路由
### AI / 模型
- [DeepSeek API](https://platform.deepseek.com/) — 默认 LLM
- [Stability AI](https://platform.stability.ai/) — 默认文生图
- [OpenAI API](https://platform.openai.com/) — 兼容接口
### 游戏引擎
- [Unity 2D](https://unity.com/solutions/2d) — 支持精灵图集导入
- [Godot Engine](https://godotengine.org/) — 开源 2D/3D 引擎
### 基础设施
- [Docker](https://www.docker.com/) — 容器化
- [SQLite](https://www.sqlite.org/) — 嵌入式数据库
- [七牛云 Kodo](https://www.qiniu.com/products/kodo) — 对象存储
## License
[MIT](LICENSE)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+1
View File
@@ -78,6 +78,7 @@ func main() {
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
v1Auth.GET("/assets", handler.GetRecentAssets) // 首页最近素材
// 图片编辑
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
}
+72
View File
@@ -306,6 +306,78 @@ func getTaskDBID(ctx context.Context, taskID string) uint {
return task.ID
}
// RecentAssetItem 首页素材展示项。
type RecentAssetItem struct {
Key string `json:"key"`
URL string `json:"url"`
Format string `json:"format"`
Prompt string `json:"prompt"`
AssetType string `json:"assetType"`
MetaType string `json:"metaType"`
TaskID string `json:"taskId"`
ProjectID string `json:"projectId"`
ProjectName string `json:"projectName"`
Width int `json:"width"`
Height int `json:"height"`
CreatedAt string `json:"createdAt"`
}
// GetRecentAssets 获取最近生成的素材列表(已完成任务的全部素材)。
func GetRecentAssets(c *gin.Context) {
limit := 60
var dbRows []struct {
Key string
URL string
Format string
Prompt string
AssetType string
MetaType string
TaskID string
ProjectID uint
ProjectName string
Width int
Height int
CreatedAt string
}
db.GetDB().WithContext(c.Request.Context()).
Raw(`SELECT a.key, a.url, a.format, t.prompt, t.asset_type as asset_type,
COALESCE(json_extract(a.metadata, '$.type'), 'frame') as meta_type,
t.external_id as task_id, t.project_id,
p.name as project_name,
CAST(COALESCE(json_extract(t.metadata, '$.frameWidth'), '0') AS INTEGER) as width,
CAST(COALESCE(json_extract(t.metadata, '$.frameHeight'), '0') AS INTEGER) as height,
t.created_at as created_at
FROM assets a
INNER JOIN tasks t ON a.task_id = t.id
INNER JOIN projects p ON t.project_id = p.id
WHERE t.status = 'completed'
ORDER BY a.created_at DESC
LIMIT ?`, limit).
Scan(&dbRows)
result := make([]RecentAssetItem, len(dbRows))
for i, r := range dbRows {
result[i] = RecentAssetItem{
Key: r.Key,
URL: storageSvc.GetSignedURL(r.Key),
Format: r.Format,
Prompt: r.Prompt,
AssetType: r.AssetType,
MetaType: r.MetaType,
TaskID: r.TaskID,
ProjectID: fmt.Sprintf("%d", r.ProjectID),
ProjectName: r.ProjectName,
Width: r.Width,
Height: r.Height,
CreatedAt: r.CreatedAt,
}
}
c.JSON(http.StatusOK, model.OK(result))
}
// toTaskResponse 转换任务响应格式。
func toTaskResponse(task *model.Task) model.TaskResponse {
return model.TaskResponse{
+1 -1
View File
@@ -198,7 +198,7 @@ func (s *ProjectService) GetTasks(ctx context.Context, userID uint, projectID ui
response := make([]model.TaskResponse, len(tasks))
for i, t := range tasks {
response[i] = model.TaskResponse{
ID: fmt.Sprintf("%d", t.ID),
ID: t.ExternalID,
ProjectID: fmt.Sprintf("%d", t.ProjectID),
Prompt: t.Prompt,
AssetType: t.AssetType,
+431
View File
@@ -0,0 +1,431 @@
# 实施计划:限流 + 协程池 + 消息队列
> 基于简历描述,本项目需要落地两个核心能力:
> 1. 基于 go-redis 的分布式令牌桶限流
> 2. 自定义 IO 密集型协程池 + RabbitMQ 消息队列
## 现状分析
| 维度 | 当前状态 | 目标状态 |
|------|---------|---------|
| 限流 | 无任何限流中间件;API 错误码 429 已预留但未实现 | 令牌桶算法,支持按用户/全局维度限流 |
| 并发控制 | 每个请求裸 `go` 起协程,无上限 | 有界协程池,可控并发度 |
| 任务队列 | 同步阻塞,任务状态通过 DB 轮询 | RabbitMQ 异步队列,生产者-消费者模式 |
| 基础设施 | SQLite + 七牛云,无 Redis / MQ | 新增 Redis + RabbitMQ 依赖 |
---
## 阶段一:基础设施接入
### 1.1 Redis 接入
**新增依赖:** `github.com/redis/go-redis/v9`
**新增配置:** `internal/config/redis.go`
```go
type RedisConfig struct {
Addr string `yaml:"addr"` // 默认 "localhost:6379"
Password string `yaml:"password"`
DB int `yaml:"db"` // 默认 0
}
```
**新增初始化:** `internal/pkg/redis/client.go` — 全局 `*redis.Client` 单例,`InitRedis(cfg)` + `CloseRedis()`,在 `cmd/main.go` 启动时调用。
**配置文件:** `config.yaml` 新增 `redis` 节。
### 1.2 RabbitMQ 接入
**新增依赖:** `github.com/rabbitmq/amqp091-go`
**新增配置:** `internal/config/rabbitmq.go`
```go
type RabbitMQConfig struct {
URL string `yaml:"url"` // 默认 "amqp://guest:guest@localhost:5672/"
Queue string `yaml:"queue"` // 默认 "gen2d:tasks"
}
```
**新增初始化:** `internal/pkg/mq/rabbitmq.go` — 封装连接、Channel、Queue 声明,提供 `Publish(body)` 和 `Consume(handler)` 方法。
**Docker Compose:** 新增 `docker-compose.yml`(或更新已有文件),包含 Redis 和 RabbitMQ 服务。
---
## 阶段二:分布式令牌桶限流
### 2.1 算法选型分析
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| 固定窗口计数器 | 实现简单 | 窗口边界突发 | 对精度要求不高的场景 |
| 滑动窗口 | 解决边界问题 | 内存开销较大 | 中等精度需求 |
| 漏桶 | 平滑输出 | 无法应对突发 | 流量整形 |
| **令牌桶** | 平滑 + 允许突发 | 实现稍复杂 | **API 限流(本项目选用)** |
**选用令牌桶理由:**
- 用户提交生成任务时存在突发行为(连续点几次生成),令牌桶允许一定的突发消费
- go-redis 社区有成熟的 Lua 脚本实现,原子性有保证
- 天然支持分布式,未来多实例部署无缝扩展
### 2.2 实现方案
**新增包:** `internal/pkg/ratelimit/`
```
internal/pkg/ratelimit/
├── limiter.go // 接口定义 + 令牌桶实现
├── middleware.go // Gin 限流中间件
└── config.go // 限流配置
```
**核心接口:**
```go
// limiter.go
type Limiter interface {
// Allow 检查 key 是否允许通过,返回 (是否允许, 剩余令牌数, 重试等待时间)
Allow(ctx context.Context, key string) (bool, int, time.Duration)
}
```
**令牌桶实现要点:**
- 底层使用 Redis + Lua 脚本保证原子性(单次 Redis 请求完成令牌检查 + 扣减)
- Lua 脚本逻辑:计算上次调用到当前的时间差 → 按速率补充令牌 → 判断桶内令牌是否足够 → 扣减并返回
- Key 设计:`ratelimit:{scope}:{identifier}`,例如 `ratelimit:user:123` 或 `ratelimit:global:generate`
- 支持两级限流:用户级(per-user)+ 全局级(per-endpoint)
**配置:**
```go
// config.go
type Config struct {
Rate int // 每秒产生的令牌数
Burst int // 桶容量(允许的突发量)
KeyPrefix string // Redis key 前缀
Expiration time.Duration // key 过期时间,防止冷用户占用内存
}
```
**推荐参数(生成接口):**
- 用户级:Rate=1/s, Burst=3(允许用户连续提交 3 个任务,之后每秒最多 1 个)
- 全局级:Rate=10/s, Burst=20(系统总并发上限)
### 2.3 Gin 中间件集成
**middleware.go:**
```go
func RateLimit(limiter Limiter, keyFunc func(c *gin.Context) string) gin.HandlerFunc
```
- `keyFunc` 负责从请求中提取限流 key(如 JWT 中的 userID,或 endpoint 标识)
- 被限流时返回 HTTP 429 + 统一错误格式 `{"code": 429, "message": "请求过于频繁,请稍后再试", "data": nil}`
- 响应头添加 `X-RateLimit-Remaining` 和 `Retry-After`
**路由注册(修改 `internal/router/router.go`):**
```go
// 用户级限流 — 基于 JWT userID
authGroup.Use(ratelimit.RateLimit(userLimiter, func(c *gin.Context) string {
return c.GetString("userID")
}))
// 生成接口额外加全局限流
v1.POST("/generate", ratelimit.RateLimit(globalLimiter, func(c *gin.Context) string {
return "generate"
}), generateHandler.Create)
```
---
## 阶段三:自定义协程池
### 3.1 设计思路
当前问题:每个 `POST /api/v1/generate` 裸起 `go RunPipeline()`,在高并发下可能耗尽系统资源(goroutine 数、文件描述符、DB 连接)。
**方案选型:**
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| `ants` 库 | 开箱即用,成熟 | 黑盒,简历无亮点 | 快速实现 |
| **手写协程池** | 可控性强,体现设计能力 | 需要维护 | **本项目选用** |
| Semaphore | 极简 | 无排队机制 | 临时方案 |
**选用自定义协程池理由:**
- 简历明确写"自定义 IO 密集型协程池",需要有手写实现
- 可以针对 IO 密集型特征做专门调参(worker 数可设为 CPU 核数的数倍)
- 可以与 RabbitMQ 消费者共用池,统一管理
### 3.2 实现方案
**新增包:** `internal/pkg/workerpool/`
```
internal/pkg/workerpool/
├── pool.go // 协程池核心实现
├── option.go // 配置选项(函数选项模式)
└── task.go // 任务定义
```
**核心结构:**
```go
// pool.go
type Pool struct {
workers int // worker 数量
taskQueue chan Task // 有界任务队列
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
metrics *Metrics // 运行指标
}
type Task struct {
ID string
Fn func(ctx context.Context) error
Priority int // 预留优先级支持
}
type Metrics struct {
ActiveWorkers atomic.Int32
QueuedTasks atomic.Int32
CompletedTasks atomic.Int64
FailedTasks atomic.Int64
}
```
**关键设计点:**
1. **IO 密集型调参:** 默认 worker 数 = `runtime.NumCPU() * 4`(而非 CPU 密集型的 1:1),通过 `WithWorkers(n)` 可覆盖
2. **有界队列:** `taskQueue` channel 容量有上限(默认 100),满时提交阻塞或返回 `ErrPoolFull`,防止任务无限堆积
3. **优雅关闭:** `Shutdown(ctx)` 等待所有正在执行的任务完成或 ctx 超时
4. **指标暴露:** 活跃 worker 数、排队任务数、完成/失败任务数,通过 `GET /api/v1/metrics` 或 Prometheus 格式暴露
**函数选项模式配置:**
```go
// option.go
type Option func(*Pool)
func WithWorkers(n int) Option // 自定义 worker 数
func WithQueueSize(n int) Option // 自定义队列容量
func WithMetrics(reg prometheus.Registerer) Option // 注册 Prometheus 指标
```
### 3.3 接入服务层
**修改 `internal/service/generate.go`:**
```go
// Before:
go svc.runPipeline(ctx, taskID, input)
// After:
err := svc.pool.Submit(workerpool.Task{
ID: taskID,
Fn: func(ctx context.Context) error {
return svc.runPipeline(ctx, taskID, input)
},
})
if err != nil {
// 更新任务状态为 failed,记录原因
return err
}
```
---
## 阶段四:RabbitMQ 消息队列
### 4.1 架构设计
```
POST /api/v1/generate
│
▼
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Producer │────▶│ RabbitMQ │────▶│ Consumer │
│ (API Handler)│ │ gen2d:tasks │ │ (Worker Goroutine)│
└─────────────┘ └──────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ Worker Pool │
│ (协程池执行 Pipeline)│
└─────────────────┘
```
**同步路径(现有,保留):** 简单请求、Prompt 优化等轻量操作仍走 HTTP 同步返回。
**异步路径(新增):** `POST /api/v1/generate` → 生产者发送消息到 RabbitMQ → 消费者从队列取出 → 提交到协程池执行 Pipeline。
### 4.2 消息格式
```go
// internal/pkg/mq/message.go
type TaskMessage struct {
TaskID string `json:"task_id"`
UserID string `json:"user_id"`
Input service.PipelineInput `json:"input"`
CreatedAt time.Time `json:"created_at"`
RetryCount int `json:"retry_count"`
}
```
### 4.3 生产者(API Handler 层)
**修改 `internal/handler/generate.go`:**
```go
func (h *GenerateHandler) Create(c *gin.Context) {
// 1. 参数绑定、校验
// 2. 去重检查(现有逻辑保留)
// 3. 创建 DB 任务记录(status=pending)
// 4. 发布消息到 RabbitMQ(替代原来的 go runPipeline)
err := h.mq.Publish(ctx, mq.TaskMessage{...})
// 5. 返回 taskId
}
```
### 4.4 消费者(独立启动)
**新增:** `internal/worker/consumer.go`
```go
type Consumer struct {
mq *mq.RabbitMQ
pool *workerpool.Pool
service *service.GenerateService
logger *slog.Logger
}
func (c *Consumer) Start(ctx context.Context) error {
// 持续消费 RabbitMQ 消息
// 每条消息提交到协程池执行
return c.mq.Consume(ctx, func(msg mq.TaskMessage) error {
return c.pool.Submit(workerpool.Task{
ID: msg.TaskID,
Fn: func(ctx context.Context) error {
return c.service.RunPipeline(ctx, msg.TaskID, msg.Input)
},
})
})
}
```
**启动方式:** `cmd/main.go` 中,API 服务和消费者在同一个进程内启动(通过配置开关控制是否启用消费者),便于开发调试;生产环境可拆分为独立进程。
### 4.5 消息可靠性
| 环节 | 保障措施 |
|------|---------|
| 生产者发送失败 | 降级为同步执行(fallback),记录告警日志 |
| 消费者处理失败 | RabbitMQ NACK + 重新入队(max retry = 3) |
| 消费者崩溃 | 手动 ACK 模式,未 ACK 的消息自动重新投递 |
| 消息堆积 | 监控队列深度,超阈值触发告警 |
---
## 阶段五:可观测性与配置
### 5.1 运行指标
新增 `GET /api/v1/metrics` 接口(或接入 Prometheus),暴露:
- **限流指标:** 各 key 被限流次数、当前令牌数
- **协程池指标:** 活跃 worker 数、队列深度、任务完成/失败数、平均执行耗时
- **MQ 指标:** 队列深度、生产/消费速率、NACK 次数
### 5.2 配置汇总
新增配置项(`config.yaml`):
```yaml
redis:
addr: "localhost:6379"
password: ""
db: 0
rabbitmq:
url: "amqp://guest:guest@localhost:5672/"
queue: "gen2d:tasks"
ratelimit:
user:
rate: 1
burst: 3
global:
rate: 10
burst: 20
workerpool:
workers: 16 # NumCPU * 4
queue_size: 100
consumer_enabled: true
```
---
## 文件变更清单
### 新增文件
| 文件路径 | 用途 |
|---------|------|
| `internal/config/redis.go` | Redis 配置结构体 |
| `internal/config/rabbitmq.go` | RabbitMQ 配置结构体 |
| `internal/pkg/redis/client.go` | Redis 客户端初始化 |
| `internal/pkg/mq/rabbitmq.go` | RabbitMQ 连接封装 |
| `internal/pkg/mq/message.go` | 消息体定义 |
| `internal/pkg/ratelimit/limiter.go` | 令牌桶限流器 |
| `internal/pkg/ratelimit/middleware.go` | Gin 限流中间件 |
| `internal/pkg/ratelimit/config.go` | 限流配置 |
| `internal/pkg/workerpool/pool.go` | 协程池核心 |
| `internal/pkg/workerpool/option.go` | 协程池配置选项 |
| `internal/pkg/workerpool/task.go` | 任务定义 |
| `internal/worker/consumer.go` | MQ 消费者 |
| `docker-compose.yml` | Redis + RabbitMQ 服务编排 |
### 修改文件
| 文件路径 | 变更内容 |
|---------|---------|
| `cmd/main.go` | 初始化 Redis、RabbitMQ、WorkerPool;启动 Consumer |
| `internal/config/config.go` | 顶层 Config 新增 Redis / RabbitMQ / Ratelimit / WorkerPool 字段 |
| `internal/router/router.go` | 注册限流中间件 |
| `internal/handler/generate.go` | 将 `go runPipeline` 改为发布 MQ 消息 |
| `internal/service/generate.go` | 接受协程池提交,`RunPipeline` 签名不变 |
| `config.yaml` | 新增 redis / rabbitmq / ratelimit / workerpool 配置节 |
| `go.mod` | 新增 go-redis、amqp091-go 依赖 |
---
## 实施顺序建议
```
Phase 1: 基础设施(Redis + RabbitMQ 接入、配置、初始化)
↓
Phase 2: 令牌桶限流(独立模块,可先接入不影响其他功能)
↓
Phase 3: 协程池(独立模块,替换裸 go 调用)
↓
Phase 4: MQ 异步化(依赖协程池,改造生成流程)
↓
Phase 5: 可观测性(指标暴露、告警配置)
```
Phase 2 和 Phase 3 可并行开发,Phase 4 依赖 Phase 3 完成。
---
## 注意事项
1. **向后兼容:** 各阶段独立可部署,不依赖全部完成才能上线。限流可以先上线,协程池和 MQ 可以后续接入。
2. **开发环境:** `docker-compose.yml` 提供 Redis + RabbitMQ,前端开发者无需关心。
3. **降级策略:** Redis 不可用时限流中间件自动放行(fail-open);RabbitMQ 不可用时降级为同步执行。
4. **测试覆盖:** 每个模块需编写单元测试 + 集成测试。限流器可用 `miniredis` 做内存 Redis 测试;协程池测试并发正确性;MQ 测试用 `rabbitmq-testcontainers` 或本地 Docker。
+110 -6
View File
@@ -8,9 +8,113 @@ Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 七牛云对象
## 文档索引
- [后端工程](backend.md) — 分层结构、管线设计(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter)、风格模型
- [前端工程](frontend.md) — 组件树、状态管理、路由、三栏工作台、自定义标签
- [异步任务](async-tasks.md) — 任务队列、状态机、并发控制、失败重试
- [数据存储](database.md) — SQLite 选型、表结构、七牛云对象存储
- [API 设计](api.md) — 接口列表、请求/响应示例、实现状态
- [预设风格键](style-keys.md) — 美术风格、色调、线条等风格键分类与可选值
| 文档 | 说明 |
|------|------|
| [后端工程](backend.md) | 分层结构、管线设计(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter)、风格模型 |
| [前端工程](frontend.md) | 组件树、状态管理、路由、三栏工作台、自定义标签 |
| [API 设计](api.md) | 接口列表、请求/响应示例、错误码、认证 |
| [数据存储](database.md) | SQLite 选型、表结构、ER 关系、对象存储、去重策略 |
| [异步任务](async-tasks.md) | 任务生命周期、并发控制、重试策略、进度推送 |
| [预设风格键](style-keys.md) | 美术风格、色调、线条等风格键分类与可选值 |
| [架构图提示词](prompts/) | 全局/后端/前端架构图生成提示词,供多模态 AI 生成架构图 |
| [实施计划](PLAN.md) | 限流(令牌桶)、协程池、RabbitMQ 异步化的详细设计方案与实施步骤 |
## 系统架构
```
┌─────────────────────────────────────────────────────────┐
│ Frontend (Vite + React) │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │
│ │ 画廊页 │ │ 三栏工作台 │ │ 结果预览页 │ │ 登录注册 │ │
│ └─────────┘ └──────────┘ └────────────┘ └──────────┘ │
│ │ HTTP / WebSocket │
└──────────────────────┼──────────────────────────────────┘
│
┌──────────────────────┼──────────────────────────────────┐
│ Backend (Go + Gin + Eino) │
│ │ │
│ ┌───────────────────▼──────────────────────────────┐ │
│ │ Eino compose.Graph │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │PromptOptimizer│─▶│AssetGenerator│ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ ▲ │ │ │
│ │ │ ┌────────────▼──────────┐ │ │
│ │ │ │ QualitySupervisor │ │ │
│ │ │ └────────────┬──────────┘ │ │
│ │ │ pass │ fail (retry < 3) │ │
│ │ └────────────────┘ │ │
│ │ fail, retry ≥ 3 │ │
│ │ │ ┌──────────────┐ │ │
│ │ └─▶│FormatAdapter │ │ │
│ │ └──────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ │ │ │ │
│ ┌────▼───┐ ┌─────▼────┐ ┌───▼──────┐ │
│ │ SQLite │ │ 七牛云 Kodo│ │ LLM API │ │
│ └────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
```
## 核心概念
### 生成管线(Pipeline)
基于 Eino `compose.Graph` 的四阶段管线。每个阶段是一个 Lambda 节点,通过 `StatePreHandler` / `StatePostHandler` 读写全局状态。质检不通过时,Graph 分支自动触发重优化,最多重试 3 次,超过后降级输出保证任务不阻塞。
详见 [后端工程](backend.md#多阶段生成管线)。
### 风格系统(Style)
风格分为两层:
- **工程风格(Project Style)**:工程级别的 kvPairs,保证同一工程下所有素材风格一致
- **任务风格覆盖(Task Style Override)**:单次生成任务的风格差异,仅覆盖需要变化的键
合并逻辑:`finalStyle = merge(projectStyle, taskStyle)`,任务同名键覆盖工程风格。
详见 [后端工程 — 风格模型](backend.md#风格模型) 和 [预设风格键](style-keys.md)。
### 任务系统(Task)
采用"提交-异步执行-轮询/推送"模式。API 同步返回 taskId,后台 goroutine 执行管线,前端通过 HTTP 轮询或 WebSocket 获取实时进度。
详见 [异步任务](async-tasks.md)。
### 数据模型
```
user 1──N project
project 1──1 project_style
project 1──N task
task 1──N asset
```
详见 [数据存储](database.md#er-关系)。
## 命令行参考
### 后端
```bash
cd backend
go mod tidy # 安装依赖
go run cmd/main.go # 开发服务器
go build -o bin/gen2d cmd/main.go # 构建
go test ./... # 全部测试
go test -run TestX ./internal/service/ # 单测
golangci-lint run # 代码检查
```
### 前端
```bash
cd frontend
npm install # 安装依赖
npm run dev # 开发服务器
npm run build # 生产构建
npm run preview # 预览构建产物
npm run typecheck # 类型检查
```
+180
View File
@@ -0,0 +1,180 @@
# 后端架构图 — 文生图提示词
你是一位技术架构可视化专家。请根据以下描述,生成一张 **gen2d 后端架构详图**,风格:**技术文档风格,白色或浅灰背景,清晰的层次关系和连线**。
## 标题
gen2d Backend Architecture (Go + Gin + Eino)
## 整体布局
自上而下的分层架构图,共 5 层。
---
### 第 1 层:入口 (Entry Point)
```
cmd/main.go → 初始化 Config → 初始化 Logger → 初始化 DB → 注册路由 → 启动 HTTP Server (:8080)
```
用横向箭头串联的流程表示。
---
### 第 2 层:HTTP 层 (Handler + Middleware)
分为两个子块并排放置:
#### 左块:Middlewares (中间件链)
三个中间件纵向排列,用向下箭头串联:
1. **Logger Middleware** — 生成 X-Request-ID,记录 method/path/status/latency
2. **Auth Middleware** — JWT Bearer Token 校验,注入 UserID 到 context
3. **Recovery Middleware** — Panic 恢复 + 堆栈日志
#### 右块:Handlers (路由处理)
按路由分组展示:
| 路由前缀 | Handler | 说明 |
|---------|---------|------|
| `/health` | health.go | 健康检查 |
| `/auth/*` | register.go, login.go | 认证(独立路径) |
| `/api/v1/projects/*` | project handlers | 工程 CRUD + 风格管理 |
| `/api/v1/generate` | generate.go | 提交生成任务 |
| `/api/v1/tasks/*` | task handlers | 任务查询 + 素材获取 |
| `/api/v1/prompt/*` | prompt.go | 提示词优化 |
| `/api/v1/assets/*` | storage.go | 素材下载 (302 重定向) |
在 Handler 块内用小字标注:**"薄层: 仅参数绑定 + 校验 + 调用 Service + 返回统一响应"**
---
### 第 3 层:Service 层 (核心业务逻辑)
这是整个图的核心区域,占比最大。分为四个子块:
#### 3a. Pipeline 编排 (pipeline.go)
用流程图展示 Eino `compose.Graph` 的节点编排:
```
┌─────────────────┐
│ PipelineInput │ (user prompt + assetType + styles + params)
└────────┬────────┘
▼
┌──────────────────────┐
│ ① PromptOptimizer │ 合并工程风格 + 任务风格覆盖
│ 输出: 三段式提示词 │ 调用 LLM 生成规范化提示词
└──────────┬───────────┘
▼
┌──────────────────────┐
│ ② AssetGenerator │ OpenAI 兼容接口 /v1/images/generations
│ 输出: 原始图片列表 │ 支持多图生成 (n 参数)
└──────────┬───────────┘
▼
┌──────────────────────┐
│ ③ QualitySupervisor │ 视觉模型检查风格一致性
│ 输出: pass / fail │
└──────┬──────┬────────┘
│ │
pass fail (retry < 3) ──→ 回到 ① (RejectReason 回填)
│ │
│ fail (retry ≥ 3) ──→ 降级输出 ↓
▼ │
┌──────────────────────┐ │
│ ④ FormatAdapter │ ◄──────┘
│ spritesheet 打包 │
│ 上传七牛云 + 写 DB │
└──────────┬───────────┘
▼
┌──────────────────┐
│ PipelineOutput │ (assets[] + metadata)
└──────────────────┘
```
#### 3b. 节点实现 (nodes.go)
四个节点用一个表格或四个小卡片展示,每个卡片包含:
- **节点名称**
- Lambda 输入 → 输出 类型签名
- StatePreHandler / StatePostHandler 交互说明
| 节点 | 输入 → 输出 | State 交互 |
|------|-----------|-----------|
| PromptOptimizer | PipelineInput → string | Pre: 注入 RejectReason; Post: 写 FinalPrompt |
| AssetGenerator | string → []Image | Post: 写 RawImages |
| QualitySupervisor | []Image → bool | Post: 写 PassQuality, RejectReason, RetryCount |
| FormatAdapter | []Image → PipelineOutput | 无 State 交互,直接输出 |
#### 3c. 类型定义 (types.go)
一个小卡片展示三个核心结构体的字段:
- **PipelineInput**:Prompt, AssetType, ProjectStyle, TaskStyle, Params
- **PipelineState**:Input, FinalPrompt, RawImages, PassQuality, RejectReason, RetryCount
- **PipelineOutput**:Assets[], Metadata
#### 3d. 辅助 Service
并排两个小块:
- **auth.go**:注册 / 登录 / JWT 签发 (HS256) / Token 校验
- **storage.go**:七牛云上传 (token 鉴权) / 下载 URL 生成 / 删除
- **inference.go**:AI API 调用封装 (文生图 + 图片编辑)
- **prompt_agent.go**:LLM 提示词优化 Agent (OpenAI 兼容 Chat Completions, 含回退模板)
---
### 第 4 层:数据访问层
- **db.go**:GORM SQLite 连接初始化 + 自动建表 (CREATE TABLE IF NOT EXISTS)
- **cache.go**:sync.Map 内存缓存,用于请求去重 (hash → taskId)
- **config.go**:环境变量读取 + 配置结构体
---
### 第 5 层:外部依赖 (底部并排)
1. **SQLite** (`data/gen2d.db`)
- 5 张表:user, project, project_style, task, asset
- ER: user 1→N project 1→1 project_style, project 1→N task 1→N asset
- 全部 CASCADE 删除
2. **七牛云 Kodo**
- Bucket 存储 Key 格式:`users/{uid}/projects/{pid}/tasks/{tid}/output/{file}`
- CDN URL + 签名参数 (默认 3600s 过期)
3. **LLM API (外部)**
- OpenAI 兼容 `/v1/images/generations` — 文生图
- OpenAI 兼容 `/v1/chat/completions` — 提示词优化
- 可替换模型提供商
---
## 色彩与排版指南
- **标题横幅**:深蓝 (#1E3A5F),白色文字
- **第 2 层 (Handler)**:浅蓝 (#E8F4FD),深蓝边框
- **第 3 层 (Service)**:浅绿 (#E6F7EE),深绿边框 — 这是核心层,用稍大的面积
- **管线节点**:用不同深浅绿色区分:
- ① 浅绿 #D1FAE5
- ② 中绿 #A7F3D0
- ③ 中绿 #6EE7B7
- ④ 深绿 #34D399
- **第 4 层 (Data)**:浅黄 (#FEF3C7),深黄边框
- **第 5 层 (External)**:浅灰 (#F3F4F6),深灰边框
- **箭头颜色**:
- 正常流程:实线 绿色
- 重试/回退:虚线 橙色
- 降级路径:虚线 红色
- 文字:sans-serif,英文为主,括号内中文辅助说明
- 所有框使用圆角 (border-radius: 8px)
- 层与层之间用虚线分隔,标注层名称在左侧
## 附加约束
- 保持技术文档风格,干净利落
- 所有组件名称与代码库中的实际命名一致 (pipeline.go, nodes.go, types.go 等)
- 画面比例 3:4 或 A4 竖版比例,适合放入文档
- 不要使用 3D 效果或过度装饰
@@ -0,0 +1,184 @@
# 前端架构图 — 文生图提示词
你是一位前端架构可视化专家。请根据以下描述,生成一张 **gen2d 前端架构详图**,风格:**现代前端技术文档风格,深色主题,组件树 + 数据流 + 路由三重展示**。
## 标题
gen2d Frontend Architecture (Vite + React + TypeScript + zustand)
## 整体布局
分为三个主要区域,从左到右排列:
---
## 左区:路由与页面层 (Router & Pages)
### Router 入口
- **react-router-dom v6** (BrowserRouter, SPA 模式)
- **ProtectedRoute** 守卫组件 (检查 token,未登录重定向 /login)
### 页面路由表
用路由匹配图展示:
```
/ → HomePage (项目画廊)
/login → LoginPage (登录)
/register → RegisterPage (注册)
/projects/:projectId → ProjectDetailPage (三栏工作台)
/projects/:projectId/tasks/:taskId → ResultPage (素材预览)
```
### 页面层级关系
```
App
└─ Layout (Navbar + Outlet + ToastContainer)
├─ HomePage
│ ├─ ProjectCard (画廊卡片) × N
│ └─ CreateProjectModal (新建弹窗)
├─ ProjectDetailPage (三栏 CSS Grid)
│ ├─ [左栏 260px] ProjectConfigPanel
│ ├─ [中间 1fr] GenerateForm + ProgressBar
│ └─ [右栏 320px] TaskListPanel
└─ ResultPage
└─ AssetPreview
```
---
## 中区:状态管理 & 数据流 (State Management)
### zustand Stores (7 个)
用 7 个卡片网格展示,每个卡片包含 Store 名称、核心状态字段、主要 Action:
| Store | 核心 State | 主要 Actions | 消费组件 |
|-------|-----------|-------------|---------|
| **auth** | user, token, isAuthenticated | login, register, logout | Layout, ProtectedRoute, LoginPage |
| **projectList** | projects[], loading | loadProjects, createProject, deleteProject | HomePage |
| **project** | projectId, name, style, draftStyle, draftName, hasUnsavedChanges | loadProject, startEditing, updateDraft, commitDraft, discardDraft | ProjectConfigPanel, ProjectDetailPage |
| **task** | prompt, assetType, taskStyle, params, enableAI, optimizedPrompt | setters, runOptimize, reset | GenerateForm |
| **generation** | taskId, stage, progress, status, retryCount, assets[], error | submit, reset | ProgressBar, GenerateForm, ResultPage |
| **theme** | mode (light/dark) | toggle | Layout (全局主题切换) |
| **toast** | toasts[] | addToast, removeToast | ToastContainer |
### 数据流方向
在组件和 Store 之间画出单向数据流箭头:
- **组件 → Store**:dispatch action (用户交互)
- **Store → 组件**:useStore selector 订阅,自动重渲染
- **Store → API**:调用 api/ 封装层
- **API → Store**:响应数据写入 state
### 风格编辑流程 (Draft/Commit 模式)
用一个小流程图展示:
```
startEditing() → copy style → draftStyle
↓
updateDraft(key, value) → draftStyle 变更 → hasUnsavedChanges = true
↓
┌─ commitDraft() → PUT /api/v1/projects/:id/style → style = draftStyle → hasUnsavedChanges = false
└─ discardDraft() → draftStyle = style → hasUnsavedChanges = false
```
---
## 右区:组件树 & 交互流程 (Components & Interactions)
### 组件依赖树
```
Layout
├── Navbar (logo + 导航 + 用户菜单)
└── ToastContainer
└── ToastItem × N
HomePage
├── ProjectCard (缩略图 + 名称 + 任务数 + 最近活动)
└── CreateProjectModal
├── StyleSelector (预设风格 pill 按钮)
└── CustomTagsEditor (添加/删除自定义标签)
ProjectDetailPage (CSS Grid: 260px 1fr 320px)
├── [左栏] ProjectConfigPanel
│ ├── 工程名 (可编辑 input)
│ ├── StyleSelector (全量风格编辑)
│ ├── CustomTagsEditor
│ └── 保存/取消 按钮 (hasUnsavedChanges 控制)
├── [中间] 生成区域
│ ├── GenerateForm
│ │ ├── 素材类型选择 (sprite / background / ui / animation)
│ │ ├── StyleSelector (任务覆盖,仅记录差异)
│ │ ├── PromptEditor (文本输入)
│ │ ├── 技术参数 (分辨率 / 帧数 / 格式)
│ │ └── 提交按钮 + AI 优化按钮
│ └── ProgressBar (提交后显示)
│ └── 四阶段进度条 (prompt_builder → asset_generator → quality_supervisor → format_adapter)
└── [右栏] TaskListPanel
├── StatusBadge (状态标签: pending/running/completed/failed)
├── TaskItem × N (点击跳转 ResultPage)
└── 自动刷新 (15s 间隔)
ResultPage
└── AssetPreview
├── 图片预览 (带帧信息标注)
├── 元数据展示 (分辨率/帧数/方向数/格式)
└── 下载按钮 (GET /api/v1/assets/download?key=...)
```
### 生成任务交互流程 (时序简化)
```
User → GenerateForm (填写 prompt + 选择类型 + 参数) → [提交]
→ POST /api/v1/generate → taskId
→ generation store: status=running
→ ProgressBar 显示
→ 轮询 GET /api/v1/tasks/:taskId (每 1.5s)
→ 更新 stage + progress
→ status=completed
→ GET /api/v1/tasks/:taskId/assets
→ 显示 "查看结果" → 跳转 ResultPage
```
### API 封装层结构 (api/)
在底部用一个窄条展示:
```
api/
├── client.ts — fetch 封装 (统一错误处理, JSON 解包, Bearer Token 注入)
├── auth.ts — login(), register()
├── project.ts — listProjects(), createProject(), getProject(), updateProject(), deleteProject()
├── generate.ts — submitGenerate(), getTask(), getTaskAssets()
├── prompt.ts — optimizePrompt(), extractTags()
├── mock.ts — 多工程 Mock 数据 (开发用)
└── types.ts — Project, Task, Asset, User 等类型定义
```
---
## 色彩与排版指南
- **背景**:深色 (#1a1a2e 或 #0f172a)
- **左区 (Router)**:紫色调 (#8B5CF6 / #7C3AED),紫色边框
- **中区 (State)**:青色调 (#06B6D4 / #0891B2),青色边框
- **右区 (Components)**:绿色调 (#10B981 / #059669),绿色边框
- **API 层**:橙色调 (#F59E0B)
- **组件框**:半透明背景 + 彩色左边框
- **箭头**:白色或浅灰 (#94A3B8),实线 = 同步,虚线 = 异步
- **文字**:白色为主 (#F8FAFC),次要文字浅灰 (#94A3B8)
- **Store 卡片**:微亮边框,内部字段用等宽字体
- **所有框使用圆角** (border-radius: 6px)
- **区域标题**用较大的加粗字体 + 左侧色条
## 附加约束
- 所有组件名、Store 名、文件名与代码库保持一致
- 中文用于辅助标注(括号内),英文为主要标注
- 避免拥挤,分区之间留白充分
- 画面比例 16:9 或更宽,适合横屏展示
- 不要使用 3D 效果
@@ -0,0 +1,81 @@
# 全局架构图 — 文生图提示词
你是一位技术架构可视化专家。请根据以下描述,生成一张 **gen2d 全局系统架构图**,风格:**简洁现代的技术架构图,深色背景,使用清晰的方框和箭头表示组件与数据流**。
## 标题
gen2d 系统架构总览
## 技术栈标注(顶部或底部小字)
Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 七牛云 Kodo
## 架构层次(自上而下)
### 第一层:前端层 (Frontend)
放置一个圆角矩形框,标题为 **Frontend (Vite + React + TypeScript)**,内部包含:
- **Pages (页面层)**:Login, Register, HomePage, ProjectDetailPage, ResultPage,以上下文无关方式排列
- **State & Router (横跨栏)**:
- 左侧标注 **react-router-dom v6** (SPA Routing)
- 右侧标注 **zustand** (7 stores: auth, project, projectList, task, generation, theme, toast)
- **Components (组件层)**:StyleSelector, CustomTagsEditor, PromptEditor, GenerateForm, ProgressBar, AssetPreview
在 Frontend 框内用水平虚线或分组分隔三个子层(页面 → 路由/状态 → 组件),展示分层关系。
### 第二层:通信层
在 Frontend 和 Backend 之间画一个横向箭头带,标注:
- **HTTP REST /api/v1/***(实线)
- **WebSocket /api/v1/tasks/:taskId/ws**(虚线)
- 旁边小字标注 "JWT Bearer Token 认证"
### 第三层:后端层 (Backend)
放置一个圆角矩形框,标题为 **Backend (Go + Gin + Eino)**,内部自左向右分三层:
#### 3a. Handler 层 (最左)
- Gin Router 入口
- 薄层:参数绑定 + 校验 + 调用 Service
- 关键 Handler:health, auth, project, generate, prompt, storage
#### 3b. Service 层 (中间,核心)
- 标题:**Eino compose.Graph (管线编排)**
- 四个节点从上到下,箭头连接:
1. **PromptOptimizer** (提示词优化 + 风格合并)
2. **AssetGenerator** (AI 推理 API 调用)
3. **QualitySupervisor** (视觉模型质检)
4. **FormatAdapter** (格式转换 + spritesheet 打包)
- 在第 3 步右侧标注分支逻辑:pass → FormatAdapter;fail → 回到 PromptOptimizer (最多 3 次)
- 在管线框下方放置 **中间件栏**:Logger (slog), JWT Auth, Panic Recovery
#### 3c. Model 层 (最右)
- 数据模型:PipelineInput, PipelineState, PipelineOutput
### 第四层:数据与基础设施层 (底部)
放置三个并排的框:
1. **SQLite** — 标注 5 张表:user, project, project_style, task, asset;附带 ER 关系简写 "1→N cascade"
2. **七牛云 Kodo (对象存储)** — 标注存储路径 `${user}/${project}/${task}/output/` 和 CDN URL 签名访问
3. **LLM API (外部)** — 标注 OpenAI 兼容接口 `/v1/images/generations` 和 Chat Completions
在前端和后端之间画出与这三个组件的连线,标注数据流方向。
## 色彩建议
- **Frontend 区域**:蓝紫色调 (#4A90D9 或类似)
- **Backend 区域**:青绿色调 (#00B4AB 或类似)
- **管线节点**:用不同深浅区分四个阶段
- **数据层**:灰色调 (#6B7280 或类似)
- **外部服务**:橙色 (#F59E0B 或类似)
- **箭头**:实线 = 同步调用,虚线 = 异步/回调
## 附加约束
- 不要使用过于花哨的 3D 效果或阴影
- 文字要清晰可读,字体大小不小于图中主体元素的 1/20
- 请用英文标注组件名称(与代码库一致),中文可用于辅助说明
- 整体比例适合 16:9 宽屏展示
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/logo.png" />
<title>gen2d</title>
</head>
<body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+5
View File
@@ -1,4 +1,9 @@
import { post, get } from './client'
import type { RecentAssetItem } from './types'
export async function getRecentAssets(): Promise<RecentAssetItem[]> {
return get<RecentAssetItem[]>('/api/v1/assets')
}
import type {
Asset,
AssetsResponse,
+16
View File
@@ -98,6 +98,22 @@ export interface Asset {
index: number
}
// 首页最近素材
export interface RecentAssetItem {
key: string
url: string
format: string
prompt: string
assetType: string
metaType: string
taskId: string
projectId: string
projectName: string
width: number
height: number
createdAt: string
}
// 生成请求 — 对应 POST /api/v1/generate
export interface GenerateRequest {
projectId: string
+276
View File
@@ -0,0 +1,276 @@
import { useEffect, useState, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { getRecentAssets } from '../api/generate'
import type { RecentAssetItem } from '../api/types'
const ASSET_TYPE_LABELS: Record<string, string> = {
sprite: '精灵',
background: '背景',
ui: 'UI',
animation: '动画',
}
const CATEGORIES = [
{ key: 'all', label: '全部' },
{ key: 'spritesheet', label: '精灵表' },
{ key: 'frame', label: '帧' },
{ key: 'animation', label: '帧动画' },
{ key: 'tileset', label: '场景瓦片' },
{ key: 'background', label: '背景' },
{ key: 'ui', label: 'UI' },
]
function matchCategory(item: RecentAssetItem, cat: string): boolean {
switch (cat) {
case 'all':
return true
case 'spritesheet':
return item.metaType === 'spritesheet'
case 'frame':
return item.metaType === 'frame'
case 'animation':
return item.metaType === 'preview' || item.format === 'gif'
case 'tileset':
return item.assetType === 'sprite' && item.metaType === 'spritesheet'
case 'background':
return item.assetType === 'background'
case 'ui':
return item.assetType === 'ui'
default:
return true
}
}
function DetailModal({ item, onClose }: { item: RecentAssetItem; onClose: () => void }) {
const downloadUrl = `/api/v1/assets/download?key=${encodeURIComponent(item.key)}`
return createPortal(
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<div
style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.6)' }}
/>
<div
className="card"
style={{
position: 'relative',
zIndex: 1,
maxWidth: 640,
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
padding: 24,
}}
onClick={e => e.stopPropagation()}
>
<button
onClick={onClose}
style={{
position: 'absolute',
top: 12,
right: 12,
background: 'transparent',
border: 'none',
fontSize: 20,
cursor: 'pointer',
color: 'var(--text-secondary)',
lineHeight: 1,
}}
>
x
</button>
<div
style={{
width: '100%',
background: 'var(--bg-input)',
borderRadius: 'var(--radius)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
minHeight: 200,
}}
>
<img
src={item.url}
alt={item.prompt}
style={{ maxWidth: '100%', maxHeight: 400, objectFit: 'contain' }}
/>
</div>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<tbody>
<Row label="提示词" value={item.prompt} />
<Row label="工程" value={item.projectName} />
<Row label="素材类型" value={`${ASSET_TYPE_LABELS[item.assetType] || item.assetType} / ${item.metaType}`} />
<Row label="分辨率" value={item.width > 0 ? `${item.width} x ${item.height}` : '—'} />
<Row label="格式" value={item.format} />
</tbody>
</table>
<div style={{ marginTop: 20, display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
<a
href={downloadUrl}
className="btn-primary"
style={{ textDecoration: 'none', padding: '8px 24px', fontSize: 14 }}
download
>
下载
</a>
<button className="btn-secondary" onClick={onClose} style={{ padding: '8px 24px', fontSize: 14 }}>
关闭
</button>
</div>
</div>
</div>,
document.body
)
}
function Row({ label, value }: { label: string; value: string }) {
return (
<tr>
<td style={{ padding: '6px 12px 6px 0', color: 'var(--text-secondary)', whiteSpace: 'nowrap', verticalAlign: 'top', width: 80 }}>
{label}
</td>
<td style={{ padding: '6px 0', wordBreak: 'break-all' }}>{value}</td>
</tr>
)
}
export default function AssetGallery() {
const [assets, setAssets] = useState<RecentAssetItem[]>([])
const [loading, setLoading] = useState(true)
const [activeCat, setActiveCat] = useState('all')
const [detailItem, setDetailItem] = useState<RecentAssetItem | null>(null)
useEffect(() => {
getRecentAssets()
.then(setAssets)
.catch(() => {})
.finally(() => setLoading(false))
}, [])
const filtered = useMemo(
() => assets.filter(item => matchCategory(item, activeCat)),
[assets, activeCat]
)
if (loading) return null
if (assets.length === 0) return null
return (
<section style={{ marginBottom: 32 }}>
<h2 style={{ fontSize: 18, marginBottom: 12 }}>最近生成</h2>
{/* 分类标签 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
{CATEGORIES.map(cat => (
<button
key={cat.key}
type="button"
onClick={() => setActiveCat(cat.key)}
style={{
fontSize: 13,
padding: '4px 14px',
borderRadius: 999,
border: `1px solid ${activeCat === cat.key ? 'var(--accent)' : 'var(--border)'}`,
background: activeCat === cat.key ? 'var(--accent-dim)' : 'transparent',
color: activeCat === cat.key ? 'var(--accent)' : 'var(--text-secondary)',
cursor: 'pointer',
fontWeight: activeCat === cat.key ? 600 : 400,
}}
>
{cat.label}
</button>
))}
</div>
{/* 素材网格 */}
{filtered.length === 0 ? (
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>暂无此类素材</p>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: 12,
}}
>
{filtered.map((item, i) => (
<div
key={`${item.taskId}-${i}`}
className="card"
style={{ padding: 8, cursor: 'pointer' }}
onClick={() => setDetailItem(item)}
>
<div
style={{
width: '100%',
aspectRatio: '1',
overflow: 'hidden',
borderRadius: 'var(--radius)',
background: 'var(--bg-input)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
src={item.url}
alt={item.prompt}
loading="lazy"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
/>
</div>
<div style={{ marginTop: 6 }}>
<span
style={{
fontSize: 11,
padding: '1px 6px',
borderRadius: 999,
background: 'var(--accent-dim)',
color: 'var(--accent)',
}}
>
{ASSET_TYPE_LABELS[item.assetType] || item.assetType}
</span>
</div>
<p
style={{
fontSize: 12,
color: 'var(--text-secondary)',
marginTop: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{item.prompt}
</p>
</div>
))}
</div>
)}
{/* 详情弹窗 */}
{detailItem && (
<DetailModal item={detailItem} onClose={() => setDetailItem(null)} />
)}
</section>
)
}
+7 -4
View File
@@ -27,13 +27,16 @@ export default function Layout() {
<Link
to="/"
style={{
fontSize: 20,
fontWeight: 700,
color: 'var(--accent)',
display: 'flex',
alignItems: 'center',
textDecoration: 'none',
}}
>
gen2d
<img
src="/logo.png"
alt="gen2d"
style={{ height: 38, width: 64 }}
/>
</Link>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
+8 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import { Link, useParams } from 'react-router-dom'
import { getTasks } from '../api/project'
import type { Task } from '../api/types'
import StatusBadge from './StatusBadge'
@@ -67,9 +67,9 @@ export default function TaskListPanel() {
</p>
) : (
tasks.map(task => (
<a
<Link
key={task.id}
href={task.status === 'completed' ? `/projects/${projectId}/tasks/${task.id}` : undefined}
to={task.status === 'completed' ? `/projects/${projectId}/tasks/${task.id}` : '#'}
style={{
display: 'block',
padding: '12px 16px',
@@ -77,8 +77,12 @@ export default function TaskListPanel() {
textDecoration: 'none',
color: 'inherit',
cursor: task.status === 'completed' ? 'pointer' : 'default',
pointerEvents: task.status === 'completed' ? 'auto' : 'none',
transition: 'background 0.15s',
}}
onClick={e => {
if (task.status !== 'completed') e.preventDefault()
}}
onMouseEnter={e => {
if (task.status === 'completed')
(e.currentTarget as HTMLElement).style.background = 'var(--accent-dim)'
@@ -115,7 +119,7 @@ export default function TaskListPanel() {
)}
</div>
<PipelineStages currentStage={task.stage ?? null} status={task.status} />
</a>
</Link>
))
)}
</div>
+3
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useProjectListStore } from '../stores/projectList'
import { useToastStore } from '../stores/toast'
import ProjectCard from '../components/ProjectCard'
import AssetGallery from '../components/AssetGallery'
import CreateProjectModal from '../components/CreateProjectModal'
import EmptyState from '../components/EmptyState'
import Skeleton from '../components/Skeleton'
@@ -39,6 +40,8 @@ export default function HomePage() {
return (
<div className="container page-enter" style={{ paddingTop: 32, paddingBottom: 32 }}>
<AssetGallery />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<h1 style={{ fontSize: 22 }}>我的工程</h1>
<button className="btn-primary" onClick={() => setModalOpen(true)}>
+1 -1
View File
@@ -22,7 +22,7 @@ interface AuthState {
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
isAuthenticated: getToken() !== null,
loading: false,
error: null,