feat(generate): 图片持久化到本地 generation/ 目录,前端对接真实 API
后端:
- POST /api/v1/generate 接收 projectId,生成 taskId,将图片保存至 generation/{projectId}/{taskId}/
- 新增 GET /api/v1/tasks/:taskId 和 GET /api/v1/tasks/:taskId/assets 查询端点
- 添加 /generation 静态文件服务,前端可通过 URL 直接访问生成的图片
- PipelineInput 增加 ProjectID/TaskID 字段
前端:
- generate.ts 对接真实后端 API,移除 mock 模式
- 401 时不再强制登出跳转,改为抛错由调用方处理
- GeneratePage 加载工程风格,传递 projectId 和完整参数
- generation store 简化为同步 API 模式,移除 WebSocket mock
- ResultPage 使用 getTask/getAssets 按 taskId 查询结果
This commit is contained in:
Regular → Executable
+1
@@ -1 +1,2 @@
|
||||
test_output/
|
||||
generation/
|
||||
|
||||
Regular → Executable
+11
-2
@@ -4,6 +4,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gen2d/internal/config"
|
||||
"gen2d/internal/db"
|
||||
@@ -20,6 +21,9 @@ func main() {
|
||||
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
// 确保 generation 输出目录存在(项目根级别)
|
||||
_ = os.MkdirAll("../generation", 0755)
|
||||
|
||||
// 初始化 SQLite 数据库
|
||||
if err := db.Init(cfg.Database.DSN, &model.User{}); err != nil {
|
||||
log.Fatalf("db init failed: %v", err)
|
||||
@@ -35,6 +39,9 @@ func main() {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机
|
||||
|
||||
// 静态文件服务 — 生成的图片
|
||||
r.Static("/generation", "../generation")
|
||||
|
||||
// API v1 路由组 — 公开接口
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
@@ -46,8 +53,10 @@ func main() {
|
||||
v1Auth := r.Group("/api/v1")
|
||||
v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret))
|
||||
{
|
||||
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
|
||||
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
|
||||
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
|
||||
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
|
||||
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
|
||||
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
|
||||
}
|
||||
|
||||
// Auth 路由组
|
||||
|
||||
Regular → Executable
+9
-3
@@ -17,9 +17,15 @@ type EditImageRequest struct {
|
||||
Count int `json:"count"` // 生成数量,默认 1
|
||||
}
|
||||
|
||||
// editAssetResponse 编辑结果素材(返回 base64)。
|
||||
type editAssetResponse struct {
|
||||
Data string `json:"data"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
// EditImageResponse 图片编辑响应体。
|
||||
type EditImageResponse struct {
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Assets []editAssetResponse `json:"assets"`
|
||||
}
|
||||
|
||||
// EditImage 图片编辑接口,基于已有图片和文本指令生成修改后的图片。
|
||||
@@ -47,9 +53,9 @@ func EditImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
assets := make([]AssetResponse, len(images))
|
||||
assets := make([]editAssetResponse, len(images))
|
||||
for i, img := range images {
|
||||
assets[i] = AssetResponse{
|
||||
assets[i] = editAssetResponse{
|
||||
Data: base64.StdEncoding.EncodeToString(img.Data),
|
||||
Format: img.Format,
|
||||
}
|
||||
|
||||
Regular → Executable
+108
-10
@@ -1,8 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gen2d/internal/model"
|
||||
"gen2d/internal/service"
|
||||
@@ -12,6 +16,7 @@ import (
|
||||
|
||||
// GenerateRequest 素材生成请求。
|
||||
type GenerateRequest struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
Prompt string `json:"prompt"`
|
||||
AssetType string `json:"assetType" binding:"required"`
|
||||
Tags []string `json:"tags"`
|
||||
@@ -26,18 +31,48 @@ type GenerateRequest struct {
|
||||
|
||||
// GenerateResponse 素材生成响应体。
|
||||
type GenerateResponse struct {
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Metadata service.AssetMetadata `json:"metadata"`
|
||||
TaskID string `json:"taskId"`
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Metadata service.AssetMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// AssetResponse 单个素材响应(二进制 Data 转 base64)。
|
||||
// AssetResponse 单个素材响应。
|
||||
type AssetResponse struct {
|
||||
Data string `json:"data"`
|
||||
Format string `json:"format"`
|
||||
URL string `json:"url"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
// Generate 素材生成接口,调用完整生成管线(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter)。
|
||||
// TaskResponse 任务查询响应。
|
||||
type TaskResponse struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"projectId"`
|
||||
Prompt string `json:"prompt"`
|
||||
AssetType string `json:"assetType"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
RetryCount int `json:"retryCount"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// AssetsResponse 素材列表响应。
|
||||
type AssetsResponse struct {
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Metadata service.AssetMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// taskRecord 内存中的任务记录。
|
||||
type taskRecord struct {
|
||||
task TaskResponse
|
||||
assets []AssetResponse
|
||||
metadata service.AssetMetadata
|
||||
}
|
||||
|
||||
var (
|
||||
taskStore = sync.Map{} // taskID → *taskRecord
|
||||
)
|
||||
|
||||
// Generate 素材生成接口。
|
||||
func Generate(c *gin.Context) {
|
||||
var req GenerateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -45,7 +80,15 @@ func Generate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
projectID := req.ProjectID
|
||||
if projectID == "" {
|
||||
projectID = "default"
|
||||
}
|
||||
taskID := fmt.Sprintf("task-%d", time.Now().UnixMilli())
|
||||
|
||||
in := service.PipelineInput{
|
||||
ProjectID: projectID,
|
||||
TaskID: taskID,
|
||||
Prompt: req.Prompt,
|
||||
AssetType: req.AssetType,
|
||||
Tags: req.Tags,
|
||||
@@ -55,7 +98,7 @@ func Generate(c *gin.Context) {
|
||||
Params: service.AssetParams{
|
||||
Resolution: req.Resolution,
|
||||
Frames: service.FrameParams{
|
||||
Directions: req.Directions,
|
||||
Directions: req.Directions,
|
||||
FramesPerDirection: req.FramesPerDir,
|
||||
},
|
||||
Format: req.Format,
|
||||
@@ -68,17 +111,72 @@ func Generate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存图片到 ../generation/{projectId}/{taskId}/
|
||||
genDir := filepath.Join("..", "generation", projectID, taskID)
|
||||
if err := os.MkdirAll(genDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "创建输出目录失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
assets := make([]AssetResponse, len(output.Assets))
|
||||
for i, a := range output.Assets {
|
||||
filename := fmt.Sprintf("%d.%s", i, a.Format)
|
||||
filePath := filepath.Join(genDir, filename)
|
||||
if err := os.WriteFile(filePath, a.Data, 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "保存图片失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
assets[i] = AssetResponse{
|
||||
Data: base64.StdEncoding.EncodeToString(a.Data),
|
||||
URL: fmt.Sprintf("/generation/%s/%s/%s", projectID, taskID, filename),
|
||||
Format: a.Format,
|
||||
URL: a.URL,
|
||||
}
|
||||
}
|
||||
|
||||
// 存储任务记录到内存
|
||||
taskStore.Store(taskID, &taskRecord{
|
||||
task: TaskResponse{
|
||||
ID: taskID,
|
||||
ProjectID: projectID,
|
||||
Prompt: req.Prompt,
|
||||
AssetType: req.AssetType,
|
||||
Status: "completed",
|
||||
Progress: 100,
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
assets: assets,
|
||||
metadata: output.Metadata,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, model.OK(GenerateResponse{
|
||||
TaskID: taskID,
|
||||
Assets: assets,
|
||||
Metadata: output.Metadata,
|
||||
}))
|
||||
}
|
||||
|
||||
// GetTask 查询任务信息。
|
||||
func GetTask(c *gin.Context) {
|
||||
taskID := c.Param("taskId")
|
||||
rec, ok := taskStore.Load(taskID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "任务不存在"))
|
||||
return
|
||||
}
|
||||
r := rec.(*taskRecord)
|
||||
c.JSON(http.StatusOK, model.OK(r.task))
|
||||
}
|
||||
|
||||
// GetAssets 查询任务素材列表。
|
||||
func GetAssets(c *gin.Context) {
|
||||
taskID := c.Param("taskId")
|
||||
rec, ok := taskStore.Load(taskID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "任务不存在"))
|
||||
return
|
||||
}
|
||||
r := rec.(*taskRecord)
|
||||
c.JSON(http.StatusOK, model.OK(AssetsResponse{
|
||||
Assets: r.assets,
|
||||
Metadata: r.metadata,
|
||||
}))
|
||||
}
|
||||
|
||||
Regular → Executable
+2
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
// PipelineInput 管线入口输入
|
||||
type PipelineInput struct {
|
||||
ProjectID string // 工程 ID,用于输出目录
|
||||
TaskID string // 任务 ID,用于输出目录
|
||||
Prompt string // 用户原始文本
|
||||
AssetType string // 素材类型:sprite / background / ui / animation
|
||||
ProjectStyle map[string]string // 工程风格键值对
|
||||
|
||||
Reference in New Issue
Block a user