feat(generate): 生成管线改为异步,前端接入轮询进度
后端: - POST /api/v1/generate 改为异步模式,立即返回 taskId - 管线在后台 goroutine 执行,状态通过 GET /tasks/:taskId 轮询 - 任务状态流转: pending → running → saving → completed/failed 前端: - generation store: submit 后每 2s 轮询 getTask,完成时自动 getAssets - GeneratePage: 实时显示轮询状态文本 + 进度条 - ResultPage: 挂载时轮询任务,未完成显示骨架屏+进度,完成自动加载素材 - Types: Task.status 增加 submitted / saving 状态
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -31,9 +33,7 @@ type GenerateRequest struct {
|
||||
|
||||
// GenerateResponse 素材生成响应体。
|
||||
type GenerateResponse struct {
|
||||
TaskID string `json:"taskId"`
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Metadata service.AssetMetadata `json:"metadata"`
|
||||
TaskID string `json:"taskId"`
|
||||
}
|
||||
|
||||
// AssetResponse 单个素材响应。
|
||||
@@ -44,21 +44,21 @@ type AssetResponse struct {
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
Assets []AssetResponse `json:"assets"`
|
||||
Metadata service.AssetMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// taskRecord 内存中的任务记录。
|
||||
@@ -72,7 +72,8 @@ var (
|
||||
taskStore = sync.Map{} // taskID → *taskRecord
|
||||
)
|
||||
|
||||
// Generate 素材生成接口。
|
||||
// Generate 素材生成接口(异步)。
|
||||
// 立即返回 taskId,后台执行管线,前端通过 GET /tasks/:taskId 轮询进度。
|
||||
func Generate(c *gin.Context) {
|
||||
var req GenerateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -85,6 +86,31 @@ func Generate(c *gin.Context) {
|
||||
projectID = "default"
|
||||
}
|
||||
taskID := fmt.Sprintf("task-%d", time.Now().UnixMilli())
|
||||
createdAt := time.Now().Format(time.RFC3339)
|
||||
|
||||
// 存入 pending 状态
|
||||
taskStore.Store(taskID, &taskRecord{
|
||||
task: TaskResponse{
|
||||
ID: taskID,
|
||||
ProjectID: projectID,
|
||||
Prompt: req.Prompt,
|
||||
AssetType: req.AssetType,
|
||||
Status: "pending",
|
||||
Progress: 0,
|
||||
CreatedAt: createdAt,
|
||||
},
|
||||
})
|
||||
|
||||
// 返回 taskId
|
||||
c.JSON(http.StatusOK, model.OK(GenerateResponse{TaskID: taskID}))
|
||||
|
||||
// 后台执行管线
|
||||
go runPipelineBg(projectID, taskID, req)
|
||||
}
|
||||
|
||||
// runPipelineBg 后台执行生成管线,更新任务状态。
|
||||
func runPipelineBg(projectID, taskID string, req GenerateRequest) {
|
||||
updateStatus(taskID, "running", 10)
|
||||
|
||||
in := service.PipelineInput{
|
||||
ProjectID: projectID,
|
||||
@@ -105,16 +131,20 @@ func Generate(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
output, err := service.RunPipeline(c.Request.Context(), in)
|
||||
ctx := context.Background()
|
||||
output, err := service.RunPipeline(ctx, in)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "素材生成失败: "+err.Error()))
|
||||
log.Printf("[generate] task %s failed: %v", taskID, err)
|
||||
updateFailed(taskID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus(taskID, "saving", 80)
|
||||
|
||||
// 保存图片到 ../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()))
|
||||
updateFailed(taskID, "创建输出目录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,7 +153,7 @@ func Generate(c *gin.Context) {
|
||||
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()))
|
||||
updateFailed(taskID, "保存图片失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
assets[i] = AssetResponse{
|
||||
@@ -132,7 +162,7 @@ func Generate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 存储任务记录到内存
|
||||
// 更新为完成状态
|
||||
taskStore.Store(taskID, &taskRecord{
|
||||
task: TaskResponse{
|
||||
ID: taskID,
|
||||
@@ -147,11 +177,29 @@ func Generate(c *gin.Context) {
|
||||
metadata: output.Metadata,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, model.OK(GenerateResponse{
|
||||
TaskID: taskID,
|
||||
Assets: assets,
|
||||
Metadata: output.Metadata,
|
||||
}))
|
||||
log.Printf("[generate] task %s completed, %d assets", taskID, len(assets))
|
||||
}
|
||||
|
||||
func updateStatus(taskID, status string, progress int) {
|
||||
rec, ok := taskStore.Load(taskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r := rec.(*taskRecord)
|
||||
r.task.Status = status
|
||||
r.task.Progress = progress
|
||||
taskStore.Store(taskID, r)
|
||||
}
|
||||
|
||||
func updateFailed(taskID, errMsg string) {
|
||||
rec, ok := taskStore.Load(taskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r := rec.(*taskRecord)
|
||||
r.task.Status = "failed"
|
||||
r.task.Error = errMsg
|
||||
taskStore.Store(taskID, r)
|
||||
}
|
||||
|
||||
// GetTask 查询任务信息。
|
||||
@@ -175,6 +223,10 @@ func GetAssets(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
r := rec.(*taskRecord)
|
||||
if r.task.Status != "completed" {
|
||||
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "任务尚未完成,当前状态: "+r.task.Status))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.OK(AssetsResponse{
|
||||
Assets: r.assets,
|
||||
Metadata: r.metadata,
|
||||
|
||||
Reference in New Issue
Block a user