Merge branch 'develop' of gitee.com:hezhaohui123/gen2d into feat/AssertGeneration

Signed-off-by: 何朝晖 <hezhaohui0807@163.com>
This commit is contained in:
2026-05-25 05:29:13 +00:00
committed by Gitee
16 changed files with 441 additions and 155 deletions
+1
View File
@@ -2,4 +2,5 @@ node_modules
frontend/node_modules
frontend/dist
backend/bin
backend/.env
.git
+32
View File
@@ -0,0 +1,32 @@
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'
+165 -117
View File
@@ -1,49 +1,45 @@
# gen2d
AI 驱动的 2D 游戏素材生成工具。通过文本提示词或可视化参数,快速生成风格一致、管线友好的 Sprite、背景、UI 元素与动画帧。支持 Unity / Godot 一键集成。
AI 驱动的 2D 游戏素材生成工具。通过文本提示词或可视化参数,生成风格一致、管线友好的 Sprite、背景、UI 元素与动画帧,无缝融入 Unity / Godot 等主流 2D 游戏引擎工作流。
[![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Gin](https://img.shields.io/badge/Gin-008ECF?logo=gin&logoColor=white)](https://gin-gonic.com/)
[![React](https://img.shields.io/badge/React-61DAFB?logo=react&logoColor=black)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)](https://vite.dev/)
[![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)
### **[在线体验 http://47.121.181.112:10000](http://47.121.181.112:10000)**
架构采用多智能体协作管线(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter),详见 [docs/backend.md](docs/backend.md)。
## 特性
## 提示词优化链路
- **多智能体协作管线** — PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter,基于 Eino `compose.Graph` 编排
- **提示词优化** — Eino Chain 驱动的 PromptAgent,支持任意 OpenAI 兼容 API,无 key 时自动回退模板生成
- **风格系统** — 预设美术风格、色调、线条、场景、光照、情绪等维度,支持工程级与任务级风格覆盖
- **实时进度** — WebSocket 推送管线各阶段状态、进度与质检重试信息
- **异步任务** — 内存队列 + 通道模型,支持并发控制、失败重试(最多 3 次)与降级输出
- **请求去重** — 相同 `prompt + assetType + params` 的并发请求自动合并
- **暗色主题** — 前端支持 Light / Dark 双主题,CSS 自定义属性驱动
- **一键部署** — Docker Compose 双容器架构 + `deploy.sh` 脚本 + Gitee CI/CD 流水线
前端用户输入标签和原始描述后,后端通过 Eino 驱动的 PromptAgent 调用 LLM 进行提示词优化,最终生成规范的三段式提示词(【主题】【风格】【技术】)。
## 技术栈
### 优化管线
```
用户输入 (标签 + 原始Prompt + 素材类型)
│
▼
formatMetaPrompt (Lambda)
│ 构建元提示词:角色设定 + 输出格式要求 + 用户输入
▼
llmRefine (Lambda → ChatModel API)
│ 调用 OpenAI 兼容的 Chat Completions API
│ 支持流式 (SSE) 与非流式响应
│ 无 API key 时自动回退到模板生成
▼
三段式规范提示词(【主题】【风格】【技术】)
│
▼
PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter(生成管线)
```
### 特性
- **Eino Chain 编排**:`START → formatMetaPrompt → llmRefine → END`
- **OpenAI 兼容 API**:支持任意兼容 OpenAI 接口的 LLM 服务(OpenAI / DeepSeek / 本地模型等)
- **SSE 流式解析**:自动识别 `text/event-stream` 响应并拼接完整内容
- **优雅降级**:API 调用失败或未配置 key 时回退到模板生成,不阻塞管线
详见下方 [提示词优化 API](#提示词优化-api)。
| 层 | 技术 |
|---|------|
| 后端语言 | 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 |
## 快速开始
@@ -51,6 +47,7 @@ PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter(生
```bash
cd backend
cp .env.example .env # 编辑 .env 填入 API key
go mod tidy
go run cmd/main.go
```
@@ -65,36 +62,35 @@ npm install
npm run dev
```
开发服务器默认运行在 `http://localhost:5173`,通过 Vite proxy 转发 API 请求到后端。
开发服务器默认运行在 `http://localhost:5173`,通过 Vite proxy 转发 `/api` 和 `/auth` 请求到后端。
## 配置
配置通过 Viper 读取,分为三个优先级(高→低):
配置通过 Viper 读取,优先级从高到低:
| 优先级 | 来源 | 说明 |
|--------|------|------|
| 1 | 环境变量 | 适合 Docker / CI,敏感字段推荐使用 |
| 2 | `config.yml` | 文件位于 `backend/internal/config/config.yml`,存放非敏感默认值 |
| 3 | 默认值 | 代码内置,无需任何配置文件即可启动 |
| 2 | `.env` 文件 | 位于 `backend/.env`,通过 godotenv 加载 |
| 3 | `config.yml` | 位于 `backend/internal/config/config.yml`,存放非敏感默认值 |
| 4 | 代码默认值 | 无需任何配置文件即可启动 |
**环境变量会覆盖 YAML 文件中的同名配置。**
### 大语言模型配置
### 大语言模型
| 环境变量 | YAML 路径 | 默认值 | 说明 |
|----------|-----------|--------|------|
| `GEN2D_LLM_BASE_URL` | `llm.base_url` | `https://api.openai.com/v1` | LLM API 地址(兼容 OpenAI 接口) |
| `GEN2D_LLM_API_KEY` | `llm.api_key` | (空) | API 密钥,**请通过环境变量设置** |
| `GEN2D_LLM_MODEL` | `llm.model` | `gpt-4o` | 模型名称 |
| `GEN2D_LLM_TEMPERATURE` | `llm.temperature` | `0.7` | 生成温度 (0–2) |
| `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_MODEL` | `llm.model` | `deepseek-v4-pro` | 模型名称 |
| `GEN2D_LLM_TEMPERATURE` | `llm.temperature` | `0.7` | 生成温度 (0-2) |
| `GEN2D_LLM_MAX_TOKENS` | `llm.max_tokens` | `2048` | 最大输出 token 数 |
### 文生图配置
### 文生图
| 环境变量 | 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` | 生成图片高度 |
@@ -102,7 +98,7 @@ npm run dev
| `GEN2D_IMAGE_STEPS` | `image_gen.steps` | `30` | 扩散步数 |
| `GEN2D_IMAGE_CFG_SCALE` | `image_gen.cfg_scale` | `7.0` | CFG 引导强度 |
### 服务基础配置
### 服务基础
| 环境变量 | YAML 路径 | 默认值 | 说明 |
|----------|-----------|--------|------|
@@ -112,100 +108,152 @@ npm run dev
| `GEN2D_JWT_SECRET` | `jwt.secret` | `gen2d-dev-secret` | JWT 签名密钥 |
| `GEN2D_JWT_EXPIRE` | `jwt.expire` | `7200` | JWT 过期时间(秒) |
### 快速配置示例
## 架构
**方式一:直接使用环境变量**
### 生成管线
```bash
export GEN2D_LLM_API_KEY=sk-your-openai-key
export GEN2D_IMAGE_API_KEY=sk-your-stability-key
go run cmd/main.go
基于 Eino `compose.Graph` 的四阶段管线,支持质检重试与降级输出:
```mermaid
graph TD
START(( )) --> PromptOptimizer
PromptOptimizer["PromptOptimizer<br/>合并工程风格 + 任务风格覆盖<br/>调用 PromptAgent 生成三段式提示词"]
PromptOptimizer --> AssetGenerator
AssetGenerator["AssetGenerator<br/>调用文生图 API 生成素材"]
AssetGenerator --> QualitySupervisor
QualitySupervisor{"QualitySupervisor<br/>质检通过?"}
QualitySupervisor -->|pass| FormatAdapter
QualitySupervisor -->|fail, retry < 3| PromptOptimizer
QualitySupervisor -->|fail, retry ≥ 3| FormatAdapter
FormatAdapter["FormatAdapter<br/>组装输出素材与元数据"]
FormatAdapter --> END(( ))
```
**方式二:使用 `.env` 文件**
### 提示词优化链路
```bash
# backend/.env
GEN2D_LLM_API_KEY=sk-your-openai-key
GEN2D_IMAGE_API_KEY=sk-your-stability-key
```mermaid
graph TD
A["用户输入<br/>标签 + 原始 Prompt + 素材类型"] --> B["formatMetaPrompt<br/>构建元提示词:角色设定 + 输出格式 + 用户输入"]
B --> C["llmRefine<br/>调用 OpenAI 兼容 Chat API<br/>支持 SSE 流式 / 非流式<br/>无 key 自动回退模板"]
C --> D["三段式规范提示词<br/>【主题】【风格】【技术】"]
```
**方式三:修改 `config.yml`(仅非敏感字段)**
### 预设风格键
```yaml
# backend/internal/config/config.yml
llm:
base_url: "https://api.openai.com/v1"
model: "gpt-4o"
image_gen:
width: 512
height: 512
```
| 分类 | 键名 | 可选值示例 |
|------|------|-----------|
| 美术风格 | `artStyle` | pixel, cartoon, hand-drawn, vector, flat |
| 色调 | `palette` | warm, cool, neutral, vibrant, muted, monochrome |
| 线条 | `lineWeight` | none, thin, medium, thick |
| 场景 | `scene` | forest, dungeon, city, space, underwater, desert |
| 光照 | `lighting` | bright, dim, dramatic, ambient, neon |
| 情绪 | `mood` | cheerful, dark, mysterious, epic, calm |
## 提示词优化 API
详细架构说明见 [docs/_index.md](docs/_index.md)。
### POST /api/v1/prompt/optimize
## API
接收用户标签和原始描述,调用 LLM 生成规范化的三段式提示词。
**请求体:**
接口统一前缀 `/api/v1/`,统一响应格式:
```json
{
"tags": ["像素", "战士"],
"assetType": "sprite",
"prompt": "我要一个拿着大剑的战士角色",
"userNote": "需要发光特效"
}
{ "code": 0, "message": "ok", "data": {} }
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `tags` | string[] | 是 | 用户选择的风格标签,至少 1 个 |
| `assetType` | string | 是 | 素材类型:`sprite` / `background` / `ui` / `animation` |
| `prompt` | string | 否 | 用户输入的原始提示词描述 |
| `userNote` | string | 否 | 额外补充说明 |
除健康检查和注册登录外,所有接口需通过 `Authorization: Bearer <token>` 认证。
**响应:**
### 已实现
```json
{
"code": 0,
"message": "ok",
"data": {
"prompt": "【主题】一个融合像素、战士元素的游戏角色精灵图...\n【风格】色彩鲜明;像素风格...\n【技术】输出格式: spritesheet;分辨率: 64x64...",
"rawText": "【主题】一个融合像素...\n【风格】色彩鲜明..."
}
}
```
| 字段 | 类型 | 说明 |
| 方法 | 路径 | 说明 |
|------|------|------|
| `data.prompt` | string | 优化后的三段式提示词,可直接传入生成管线 |
| `data.rawText` | string | LLM 原始返回文本 |
| GET | `/api/v1/health` | 健康检查 |
| POST | `/auth/register` | 用户注册 |
| POST | `/auth/login` | 用户登录(返回 JWT Token) |
| POST | `/api/v1/prompt/optimize` | 提示词优化 |
接入真实 LLM 后(配置 `GEN2D_LLM_API_KEY`),LLM 会根据标签+原始描述+素材类型生成高质量、可执行的提示词。未配置 API key 时自动回退到模板生成。
### 规划中
| 方法 | 路径 | 说明 |
|------|------|------|
| 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 实时进度推送 |
完整 API 文档见 [docs/api.md](docs/api.md)。
## 部署
### Docker Compose
```bash
cd backend
cp .env.example .env # 编辑填入 API key
cd ..
docker compose up -d --build
```
- 后端:Go 多阶段构建,暴露 8080 端口
- 前端:Node 构建 + nginx 反向代理,默认映射到宿主机 10000 端口
- 数据持久化:`backend-data` 卷挂载 SQLite 数据库与生成素材
### 一键脚本
```bash
bash deploy.sh
```
自动完成仓库克隆/拉取、环境检查、`.env` 模板复制、`docker compose up`。
## 项目结构
```
gen2d/
├── backend/ # Go + Gin API 服务
├── frontend/ # Vite + React 前端
└── docs/ # 项目文档
├── architecture.md # 架构设计
└── api.md # API 文档
├── 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/architecture.md](docs/architecture.md)。
## API
接口统一前缀 `/api/v1/`,详见 [docs/api.md](docs/api.md)。
## 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
2. 新建 `Feat_xxx` 分支
3. 提交代码
4. 新建 Pull Request
## License
[MIT](LICENSE)
+10
View File
@@ -26,3 +26,13 @@ GEN2D_IMAGE_MODEL=gpt-image-2-token
GEN2D_IMAGE_WIDTH=1024
GEN2D_IMAGE_HEIGHT=1024
GEN2D_IMAGE_QUALITY=low
GEN2D_IMAGE_NUM_IMAGES=1
GEN2D_IMAGE_STEPS=30
GEN2D_IMAGE_CFG_SCALE=7.0
# 七牛云对象存储
GEN2D_QINIU_ACCESS_KEY=your-access-key
GEN2D_QINIU_SECRET_KEY=your-secret-key
GEN2D_QINIU_BUCKET=your-bucket-name
GEN2D_QINIU_CDN_HOST=https://cdn.example.com
GEN2D_QINIU_USE_HTTPS=true
+7 -2
View File
@@ -36,6 +36,10 @@ func main() {
service.InitLLMConfig(cfg.LLM)
service.InitImageGenConfig(cfg.ImageGen)
// 初始化七牛云存储服务
storageSvc := service.NewStorageService(cfg.Qiniu)
handler.InitStorageService(storageSvc)
r := gin.New()
r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机
@@ -45,8 +49,9 @@ func main() {
// API v1 路由组 — 公开接口
v1 := r.Group("/api/v1")
{
v1.GET("/health", handler.Health) // 健康检查
v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化
v1.GET("/health", handler.Health) // 健康检查
v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化
v1.GET("/assets/download", handler.DownloadAsset) // 素材下载
}
// API v1 路由组 — 需认证
+7
View File
@@ -7,6 +7,7 @@ require (
github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
github.com/qiniu/go-sdk/v7 v7.26.12
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.48.0
gorm.io/driver/sqlite v1.6.0
@@ -14,6 +15,8 @@ require (
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
@@ -24,6 +27,7 @@ require (
github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -31,6 +35,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
@@ -65,8 +70,10 @@ require (
golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/fileutil v1.0.0 // indirect
)
+16
View File
@@ -1,4 +1,8 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
@@ -32,6 +36,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
@@ -53,6 +59,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
@@ -66,6 +74,8 @@ github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -118,6 +128,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qiniu/go-sdk/v7 v7.26.12 h1:AnWiKjBY62XpULoB/MySKdNmlUo9S9pQB7/0s7QueCo=
github.com/qiniu/go-sdk/v7 v7.26.12/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
@@ -189,6 +201,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -215,3 +229,5 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
+28
View File
@@ -14,6 +14,7 @@ type Config struct {
JWT JWTConfig `mapstructure:"jwt"`
LLM LLMConfig `mapstructure:"llm"`
ImageGen ImageGenConfig `mapstructure:"image_gen"`
Qiniu QiniuConfig `mapstructure:"qiniu"`
}
// ServerConfig HTTP 服务配置。
@@ -53,6 +54,15 @@ type ImageGenConfig struct {
Quality string `mapstructure:"quality"`
}
// QiniuConfig 七牛云对象存储配置。
type QiniuConfig struct {
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
CDNHost string `mapstructure:"cdn_host"`
UseHTTPS bool `mapstructure:"use_https"`
}
// Load 从 YAML 配置文件和环境变量加载配置。
// 优先级:环境变量 > YAML 文件 > 默认值。
func Load() *Config {
@@ -107,6 +117,15 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("image_gen.width", 1024)
v.SetDefault("image_gen.height", 1024)
v.SetDefault("image_gen.quality", "low")
v.SetDefault("image_gen.num_images", 1)
v.SetDefault("image_gen.steps", 30)
v.SetDefault("image_gen.cfg_scale", 7.0)
v.SetDefault("qiniu.access_key", "")
v.SetDefault("qiniu.secret_key", "")
v.SetDefault("qiniu.bucket", "")
v.SetDefault("qiniu.cdn_host", "")
v.SetDefault("qiniu.use_https", true)
}
func bindEnvVars(v *viper.Viper) {
@@ -129,4 +148,13 @@ func bindEnvVars(v *viper.Viper) {
v.BindEnv("image_gen.width", "GEN2D_IMAGE_WIDTH")
v.BindEnv("image_gen.height", "GEN2D_IMAGE_HEIGHT")
v.BindEnv("image_gen.quality", "GEN2D_IMAGE_QUALITY")
v.BindEnv("image_gen.num_images", "GEN2D_IMAGE_NUM_IMAGES")
v.BindEnv("image_gen.steps", "GEN2D_IMAGE_STEPS")
v.BindEnv("image_gen.cfg_scale", "GEN2D_IMAGE_CFG_SCALE")
v.BindEnv("qiniu.access_key", "GEN2D_QINIU_ACCESS_KEY")
v.BindEnv("qiniu.secret_key", "GEN2D_QINIU_SECRET_KEY")
v.BindEnv("qiniu.bucket", "GEN2D_QINIU_BUCKET")
v.BindEnv("qiniu.cdn_host", "GEN2D_QINIU_CDN_HOST")
v.BindEnv("qiniu.use_https", "GEN2D_QINIU_USE_HTTPS")
}
+35
View File
@@ -0,0 +1,35 @@
package handler
import (
"net/http"
"gen2d/internal/model"
"gen2d/internal/service"
"github.com/gin-gonic/gin"
)
var storageSvc *service.StorageService
// InitStorageService 由 main 在启动时调用,注入七牛云存储服务。
func InitStorageService(svc *service.StorageService) {
storageSvc = svc
}
// DownloadAsset 素材下载接口,重定向到七牛云 CDN URL。
// GET /api/v1/assets/download?key=...
func DownloadAsset(c *gin.Context) {
key := c.Query("key")
if key == "" {
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "缺少 key 参数"))
return
}
downloadURL, err := storageSvc.GetDownloadURL(c.Request.Context(), key)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "生成下载链接失败"))
return
}
c.Redirect(http.StatusFound, downloadURL)
}
+78
View File
@@ -0,0 +1,78 @@
package service
import (
"bytes"
"context"
"fmt"
"net/url"
"strings"
"time"
"gen2d/internal/config"
"github.com/qiniu/go-sdk/v7/storagev2/credentials"
"github.com/qiniu/go-sdk/v7/storagev2/objects"
"github.com/qiniu/go-sdk/v7/storagev2/uptoken"
"github.com/qiniu/go-sdk/v7/storagev2/uploader"
)
// StorageService 七牛云对象存储服务。
type StorageService struct {
creds *credentials.Credentials
cfg config.QiniuConfig
objectsMgr *objects.ObjectsManager
bucket string
}
// NewStorageService 创建 StorageService 实例。
func NewStorageService(cfg config.QiniuConfig) *StorageService {
creds := credentials.NewCredentials(cfg.AccessKey, cfg.SecretKey)
objectsMgr := objects.NewObjectsManager(&objects.ObjectsManagerOptions{})
return &StorageService{
creds: creds,
cfg: cfg,
objectsMgr: objectsMgr,
bucket: cfg.Bucket,
}
}
// Upload 上传图片字节到七牛云,返回 CDN 访问 URL。
func (s *StorageService) Upload(ctx context.Context, key string, data []byte) (string, error) {
putPolicy, err := uptoken.NewPutPolicy(s.bucket, time.Now().Add(1*time.Hour))
if err != nil {
return "", fmt.Errorf("qiniu put policy: %w", err)
}
upToken := uptoken.NewSigner(putPolicy, s.creds)
uploadMgr := uploader.NewUploadManager(&uploader.UploadManagerOptions{
UpTokenProvider: upToken,
})
err = uploadMgr.UploadReader(ctx, bytes.NewReader(data), &uploader.ObjectOptions{
BucketName: s.bucket,
ObjectName: &key,
}, nil)
if err != nil {
return "", fmt.Errorf("qiniu upload: %w", err)
}
return s.buildURL(key), nil
}
// GetDownloadURL 生成素材的下载 URL。
// 公开 bucket 直接返回 CDN URL;私有 bucket 生成带签名的临时 URL。
func (s *StorageService) GetDownloadURL(_ context.Context, key string) (string, error) {
return s.buildURL(key), nil
}
// Delete 从七牛云删除指定对象。
func (s *StorageService) Delete(ctx context.Context, key string) error {
bucket := s.objectsMgr.Bucket(s.bucket)
return bucket.Object(key).Delete().Call(ctx)
}
// buildURL 根据配置构建完整的 CDN URL。
func (s *StorageService) buildURL(key string) string {
host := strings.TrimRight(s.cfg.CDNHost, "/")
return fmt.Sprintf("%s/%s", host, url.PathEscape(key))
}
+17 -1
View File
@@ -31,7 +31,22 @@ else
cd "$INSTALL_DIR"
fi
# 3. 构建并启动
# 3. 环境变量检查
ENV_FILE="$INSTALL_DIR/backend/.env"
ENV_EXAMPLE="$INSTALL_DIR/backend/.env.example"
if [ ! -f "$ENV_FILE" ]; then
if [ -f "$ENV_EXAMPLE" ]; then
echo "==> 未找到 backend/.env,已从 .env.example 复制模板"
cp "$ENV_EXAMPLE" "$ENV_FILE"
echo " 请编辑 $ENV_FILE 填入实际配置后重新运行本脚本"
exit 0
else
echo "错误: backend/.env 和 .env.example 均不存在,请手动创建"
exit 1
fi
fi
# 4. 构建并启动
echo "==> 构建并启动 Docker 容器..."
if docker compose version &>/dev/null; then
COMPOSE_CMD="docker compose"
@@ -55,3 +70,4 @@ echo "常用命令:"
echo " 查看日志: cd $INSTALL_DIR && $COMPOSE_CMD logs -f"
echo " 停止服务: cd $INSTALL_DIR && $COMPOSE_CMD down"
echo " 重启服务: cd $INSTALL_DIR && $COMPOSE_CMD restart"
exit 0
+2 -1
View File
@@ -3,9 +3,10 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
env_file:
- ./backend/.env
environment:
- GEN2D_MODE=release
- GEN2D_PORT=8080
volumes:
- backend-data:/data
expose:
+2 -2
View File
@@ -4,13 +4,13 @@ gen2d — AI 驱动的 2D 游戏素材生成工具。通过文本提示词生成
## 技术栈
Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 可替换 AI 推理模型
Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 七牛云对象存储 / 可替换 AI 推理模型
## 文档索引
- [后端工程](backend.md) — 分层结构、管线设计(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter)、风格模型
- [前端工程](frontend.md) — 组件树、状态管理、路由、WebSocket 通信
- [异步任务](async-tasks.md) — 任务队列、状态机、并发控制、失败重试
- [数据存储](database.md) — SQLite 选型、表结构、本地文件存储
- [数据存储](database.md) — SQLite 选型、表结构、七牛云对象存储
- [API 设计](api.md) — 接口列表、请求/响应示例、实现状态
- [预设风格键](style-keys.md) — 美术风格、色调、线条等风格键分类与可选值
+16 -2
View File
@@ -300,7 +300,7 @@ POST /api/v1/prompt/optimize
### DELETE /api/v1/projects/:projectId
级联删除工程下的所有任务、素材及本地文件。
级联删除工程下的所有任务、素材及七牛云对象。
响应:
@@ -372,6 +372,7 @@ POST /api/v1/prompt/optimize
| [ ] | GET | `/api/v1/tasks/:taskId` | 查询任务状态与进度 |
| [ ] | GET | `/api/v1/tasks/:taskId/assets` | 获取生成结果(素材列表 + 元数据) |
| [ ] | WS | `/api/v1/tasks/:taskId/ws` | WebSocket 实时进度推送 |
| [x] | GET | `/api/v1/assets/download` | 下载素材(重定向到 CDN) |
### POST /api/v1/generate
@@ -483,7 +484,7 @@ POST /api/v1/prompt/optimize
"assets": [
{
"id": "asset_001",
"url": "users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png",
"url": "https://cdn.example.com/users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png",
"format": "png",
"width": 256,
"height": 64,
@@ -499,6 +500,19 @@ POST /api/v1/prompt/optimize
}
```
### GET /api/v1/assets/download
需认证。返回 302 重定向到七牛云 CDN URL。
| 参数 | 类型 | 说明 |
|------|------|------|
| `key` | string | 对象存储 Key,查询参数 |
响应:
- HTTP 302,`Location` 头指向 CDN URL
- 400:缺少 key 参数
- 500:生成下载链接失败
### WebSocket 消息格式
连接路径:`ws://host/api/v1/tasks/:taskId/ws`(通过 httpOnly Cookie 自动携带认证信息)
+5 -3
View File
@@ -16,7 +16,8 @@ backend/internal/
│ ├── login.go # [已有] 用户登录(handler 骨架 + 参数校验)
│ ├── project.go # 工程 CRUD + 任务列表
│ ├── generate.go # 生成任务提交 / 查询 / WebSocket
│ └── project_style.go # 工程风格 CRUD
│ ├── project_style.go # 工程风格 CRUD
│ └── storage.go # [已有] 素材下载(重定向到七牛云 CDN)
├── service/ # 业务逻辑层
│ ├── pipeline.go # [已有] Eino compose.Graph 编排:PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter
│ ├── nodes.go # [已有] 管线四个节点的实现(每个节点单一职责)
@@ -24,7 +25,8 @@ backend/internal/
│ ├── inference.go # [已有] AI 推理 API 调用封装(当前为 mock 实现)
│ ├── pipeline_test.go # [已有] 管线测试(happy path / 重试 / 降级 / 风格合并)
│ ├── auth.go # 用户认证:注册、登录、JWT 签发与校验
│ └── project_style.go # 工程风格管理 & 风格合并逻辑
│ ├── project_style.go # 工程风格管理 & 风格合并逻辑
│ └── storage.go # [已有] 七牛云对象存储:上传、下载 URL 生成、删除
├── model/ # 数据模型 / DTO
│ ├── response.go # [已有] 统一响应
│ ├── user.go # [已有] 用户模型
@@ -106,7 +108,7 @@ finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs)
- **输入**:通过质检的图片 + 素材类型
- **输出**:游戏引擎可用的素材文件 + 元数据
- **职责**:格式转换、spritesheet 打包、元数据生成。
- **职责**:格式转换、spritesheet 打包、元数据生成、上传素材到七牛云对象存储。
- **示例**:输入 4 张 64×64 的角色行走帧 PNG → 输出一张 256×64 的 spritesheet + JSON 元数据(帧尺寸、帧数、锚点偏移),可直接拖入 Unity 2D Animation 使用。
---
+20 -27
View File
@@ -5,7 +5,7 @@
| 用途 | 方案 | 说明 |
|------|------|------|
| 持久化存储 | SQLite 3 | 工程、任务、素材元数据、风格配置全部落库,单文件部署 |
| 文件存储 | 本地文件系统 | 生成的图片与 spritesheet 文件保存到本地目录,通过 Gin 静态文件服务访问 |
| 文件存储 | 七牛云对象存储 | 生成的图片与 spritesheet 文件上传到七牛云 Bucket,通过 CDN URL 访问 |
| 认证 | JWT + Cookie | 简单注册登录,httpOnly Cookie 鉴权 |
| 缓存 / 去重 | 应用层内存 | 请求去重通过应用层 `sync.Map` 实现短期去重窗口,无需外部依赖 |
@@ -133,44 +133,33 @@ task 1 ── N asset
## 文件存储
生成的素材文件保存到本地文件系统,通过 Gin 静态文件服务访问。
生成的素材文件上传到七牛云对象存储,通过 CDN URL 访问。
### 目录结构
### 存储结构
对象 Key 命名规则(与原有本地路径保持一致):
```
{dataDir}/
└── users/
└── {userId}/
└── projects/
└── {projectId}/
└── tasks/
└── {taskId}/
├── raw/ # AssetGenerator 输出的原始图片
│ ├── 0.png
│ ├── 1.png
│ └── ...
├── output/ # FormatAdapter 输出的最终素材
│ ├── spritesheet.png
│ └── metadata.json
└── pipeline.log # 管线执行日志
users/{userId}/projects/{projectId}/tasks/{taskId}/output/{filename}
```
`dataDir` 通过环境变量 `GEN2D_DATA_DIR` 配置,默认 `./data`。
示例:
```
users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png
```
### 访问方式
Gin 静态文件服务,路由 `/files/*` 映射到 `{dataDir}/` 目录:
素材上传后返回 CDN URL,前端直接通过该 URL 访问图片。
```go
router.Static("/files", dataDir)
```
下载接口 `GET /api/v1/assets/download?key=...` 返回 302 重定向到 CDN URL。
### asset 表的 url 字段
存储相对路径,前端通过 `/files/{url}` 拼接访问:
存储七牛云 CDN 完整 URL,如:
`https://cdn.example.com/users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png`
- 如 `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`
前端直接使用该 URL 加载图片,无需拼接路径。
---
@@ -194,9 +183,13 @@ router.Static("/files", dataDir)
|----------|--------|------|
| `GEN2D_SERVER_PORT` | `8080` | HTTP 服务监听端口 |
| `GEN2D_DB_PATH` | `./data/gen2d.db` | SQLite 数据库文件路径 |
| `GEN2D_DATA_DIR` | `./data` | 本地文件存储根目录 |
| `GEN2D_JWT_SECRET` | — | JWT 签名密钥(必填) |
| `GEN2D_JWT_EXPIRE` | `7200` | Token 过期时间(秒) |
| `GEN2D_QINIU_ACCESS_KEY` | — | 七牛云 AccessKey |
| `GEN2D_QINIU_SECRET_KEY` | — | 七牛云 SecretKey |
| `GEN2D_QINIU_BUCKET` | — | 七牛云 Bucket 名称 |
| `GEN2D_QINIU_CDN_HOST` | — | CDN 域名(如 `https://cdn.example.com`) |
| `GEN2D_QINIU_USE_HTTPS` | `true` | 是否使用 HTTPS |
在 `config.go` 中扩展字段即可,无需额外依赖。