766 lines
21 KiB
Markdown
766 lines
21 KiB
Markdown
# MiniMax 图生图 API 实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 实现异步图生图 API,前端轮询 job 状态,后端调用 MiniMax 生成图片
|
||
|
||
**Architecture:**
|
||
- Gateway 接收 HTTP 请求,创建 job 返回 job_id
|
||
- assetService 异步调用 MiniMax API,完成后更新 job 状态
|
||
- 前端轮询查询 job 状态,COMPLETED 后展示结果
|
||
|
||
**Tech Stack:** Go (Gin, Dubbo RPC), MiniMax API, uni-app (Vue 3)
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
```
|
||
backend/
|
||
├── gateway/
|
||
│ ├── dto/
|
||
│ │ └── image_dto.go # 新增: ImageGenerationRequest, ImageJobResponse
|
||
│ ├── controller/
|
||
│ │ └── asset_controller.go # 修改: 添加 ImageGeneration, GetImageJob
|
||
│ └── router/
|
||
│ └── router.go # 修改: 注册 /mints/image/generation 路由
|
||
└── services/assetService/
|
||
└── service/
|
||
└── minimax_service.go # 新增: 任务管理 + MiniMax API 调用 + 图片压缩
|
||
|
||
frontend/
|
||
└── pages/discover/
|
||
└── generation-loading.vue # 修改: 改为轮询模式
|
||
```
|
||
|
||
---
|
||
|
||
## Task 1: 创建 DTO 文件
|
||
|
||
**Files:**
|
||
- Create: `backend/gateway/dto/image_dto.go`
|
||
- Test: (无独立测试,随 controller 测试)
|
||
|
||
- [ ] **Step 1: 创建 DTO 文件**
|
||
|
||
```go
|
||
package dto
|
||
|
||
// ImageGenerationRequest MiniMax 图生图请求
|
||
type ImageGenerationRequest struct {
|
||
Model string `json:"model" binding:"required"`
|
||
Prompt string `json:"prompt" binding:"required"`
|
||
AspectRatio string `json:"aspect_ratio"`
|
||
SubjectReference []SubjectReference `json:"subject_reference"` // MiniMax API 为数组
|
||
N int `json:"n"` // 1-4
|
||
}
|
||
|
||
type SubjectReference struct {
|
||
Type string `json:"type"`
|
||
ImageFile string `json:"image_file"`
|
||
}
|
||
|
||
// ImageJobResponse 图生图任务响应
|
||
type ImageJobResponse struct {
|
||
JobID string `json:"job_id"`
|
||
Status string `json:"status"`
|
||
Progress int `json:"progress"`
|
||
Images []string `json:"images,omitempty"`
|
||
ErrorMsg string `json:"error_msg,omitempty"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
CompletedAt int64 `json:"completed_at,omitempty"`
|
||
}
|
||
|
||
// ImageJobCreateResponse 创建任务响应
|
||
type ImageJobCreateResponse struct {
|
||
JobID string `json:"job_id"`
|
||
Status string `json:"status"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/gateway/dto/image_dto.go
|
||
git commit -m "feat: 添加图生图 DTO"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: 创建 MiniMax Service (任务管理 + API 调用)
|
||
|
||
**Files:**
|
||
- Create: `backend/services/assetService/service/minimax_service.go`
|
||
- Modify: `backend/services/assetService/go.mod` (添加依赖)
|
||
- Test: (无独立测试)
|
||
|
||
- [ ] **Step 1: 添加依赖**
|
||
|
||
```bash
|
||
cd backend/services/assetService && go get github.com/nfnt/resize
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 minimax_service.go**
|
||
|
||
```go
|
||
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"image"
|
||
"image/gif"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"io"
|
||
"math/rand"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/nfnt/resize"
|
||
"github.com/topfans/backend/services/assetService/config"
|
||
dto "github.com/topfans/backend/gateway/dto"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// JobStatus 任务状态
|
||
type JobStatus string
|
||
|
||
const (
|
||
StatusPending JobStatus = "PENDING"
|
||
StatusProcessing JobStatus = "PROCESSING"
|
||
StatusCompleted JobStatus = "COMPLETED"
|
||
StatusFailed JobStatus = "FAILED"
|
||
)
|
||
|
||
// ImageGenerationJob 图生图任务
|
||
type ImageGenerationJob struct {
|
||
JobID string `json:"job_id"`
|
||
UserID int64 `json:"user_id"`
|
||
StarID int64 `json:"star_id"`
|
||
Status JobStatus `json:"status"`
|
||
Progress int `json:"progress"`
|
||
Images []string `json:"images,omitempty"`
|
||
ErrorMsg string `json:"error_msg,omitempty"`
|
||
Request *dto.ImageGenerationRequest `json:"request,omitempty"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
CompletedAt int64 `json:"completed_at,omitempty"`
|
||
}
|
||
|
||
// MinimaxService MiniMax API 转发服务
|
||
type MinimaxService interface {
|
||
CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error)
|
||
GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error)
|
||
}
|
||
|
||
type minimaxService struct {
|
||
config *config.AssetConfig
|
||
jobs map[string]*ImageGenerationJob
|
||
jobsLock sync.RWMutex
|
||
}
|
||
|
||
// NewMinimaxService 创建 MiniMax 服务
|
||
func NewMinimaxService(cfg *config.AssetConfig) MinimaxService {
|
||
svc := &minimaxService{
|
||
config: cfg,
|
||
jobs: make(map[string]*ImageGenerationJob),
|
||
}
|
||
// 启动清理 goroutine
|
||
go svc.cleanupExpiredJobs()
|
||
return svc
|
||
}
|
||
|
||
// CreateJob 创建图生图任务
|
||
func (s *minimaxService) CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error) {
|
||
jobID := uuid.New().String()
|
||
now := time.Now().UnixMilli()
|
||
|
||
job := &ImageGenerationJob{
|
||
JobID: jobID,
|
||
UserID: userID,
|
||
StarID: starID,
|
||
Status: StatusProcessing,
|
||
Progress: 0,
|
||
Request: req,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
|
||
s.jobsLock.Lock()
|
||
s.jobs[jobID] = job
|
||
s.jobsLock.Unlock()
|
||
|
||
// 异步调用 MiniMax
|
||
go s.processJob(job)
|
||
|
||
return job, nil
|
||
}
|
||
|
||
// GetJob 获取任务
|
||
func (s *minimaxService) GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error) {
|
||
s.jobsLock.RLock()
|
||
job, ok := s.jobs[jobID]
|
||
s.jobsLock.RUnlock()
|
||
|
||
if !ok {
|
||
return nil, fmt.Errorf("job not found")
|
||
}
|
||
if job.UserID != userID || job.StarID != starID {
|
||
return nil, fmt.Errorf("access denied")
|
||
}
|
||
|
||
return job, nil
|
||
}
|
||
|
||
// processJob 异步处理任务
|
||
func (s *minimaxService) processJob(job *ImageGenerationJob) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
job.Status = StatusFailed
|
||
job.ErrorMsg = fmt.Sprintf("panic: %v", r)
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
}
|
||
}()
|
||
|
||
// 1. 校验 SSRF
|
||
for _, ref := range job.Request.SubjectReference {
|
||
if err := validateURL(ref.ImageFile); err != nil {
|
||
job.Status = StatusFailed
|
||
job.ErrorMsg = "invalid image URL: " + err.Error()
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 2. 压缩图片
|
||
processedRefs := make([]dto.SubjectReference, len(job.Request.SubjectReference))
|
||
for i, ref := range job.Request.SubjectReference {
|
||
job.Progress = 10 + i*20
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
|
||
compressed, err := s.compressImageIfNeeded(ref.ImageFile)
|
||
if err != nil {
|
||
compressed = ref.ImageFile
|
||
zap.S().Warnf("Image compression failed, using original: %v", err)
|
||
}
|
||
processedRefs[i] = dto.SubjectReference{
|
||
Type: ref.Type,
|
||
ImageFile: compressed,
|
||
}
|
||
}
|
||
|
||
job.Progress = 50
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
|
||
// 3. 调用 MiniMax API
|
||
images, err := s.callMiniMaxAPI(job.Request.Model, job.Request.Prompt, job.Request.AspectRatio, processedRefs, job.Request.N)
|
||
if err != nil {
|
||
job.Status = StatusFailed
|
||
job.ErrorMsg = "MiniMax API failed: " + err.Error()
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
return
|
||
}
|
||
|
||
job.Progress = 90
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
|
||
// 4. 完成
|
||
job.Status = StatusCompleted
|
||
job.Progress = 100
|
||
job.Images = images
|
||
job.CompletedAt = time.Now().UnixMilli()
|
||
job.UpdatedAt = time.Now().UnixMilli()
|
||
}
|
||
|
||
// callMiniMaxAPI 调用 MiniMax API
|
||
func (s *minimaxService) callMiniMaxAPI(model, prompt, aspectRatio string, refs []dto.SubjectReference, n int) ([]string, error) {
|
||
apiURL := s.config.GetMiniMaxAPIURL()
|
||
apiKey := s.config.GetMiniMaxAPIKey()
|
||
|
||
payload := map[string]interface{}{
|
||
"model": model,
|
||
"prompt": prompt,
|
||
"aspect_ratio": aspectRatio,
|
||
"subject_reference": refs,
|
||
"n": n,
|
||
}
|
||
|
||
jsonData, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
client := &http.Client{Timeout: 120 * time.Second}
|
||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
var result struct {
|
||
Images []struct {
|
||
URL string `json:"url"`
|
||
} `json:"images"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
images := make([]string, len(result.Images))
|
||
for i, img := range result.Images {
|
||
images[i] = img.URL
|
||
}
|
||
return images, nil
|
||
}
|
||
|
||
// compressImageIfNeeded 下载并压缩图片
|
||
func (s *minimaxService) compressImageIfNeeded(imageURL string) (string, error) {
|
||
resp, err := http.Get(imageURL)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
imgData, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
img, format, err := image.Decode(bytes.NewReader(imgData))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
bounds := img.Bounds()
|
||
maxDim := uint(1024)
|
||
newWidth := uint(bounds.Dx())
|
||
newHeight := uint(bounds.Dy())
|
||
|
||
if newWidth > maxDim || newHeight > maxDim {
|
||
if newWidth > newHeight {
|
||
ratio := float64(maxDim) / float64(newWidth)
|
||
newWidth = maxDim
|
||
newHeight = uint(float64(newHeight) * ratio)
|
||
} else {
|
||
ratio := float64(maxDim) / float64(newHeight)
|
||
newHeight = maxDim
|
||
newWidth = uint(float64(newWidth) * ratio)
|
||
}
|
||
}
|
||
|
||
if newWidth == uint(bounds.Dx()) && newHeight == uint(bounds.Dy()) {
|
||
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData), nil
|
||
}
|
||
|
||
resized := resize.Thumbnail(newWidth, newHeight, img, resize.Lanczos)
|
||
|
||
var buf bytes.Buffer
|
||
switch format {
|
||
case "png":
|
||
err = png.Encode(&buf, resized)
|
||
case "gif":
|
||
err = gif.Encode(&buf, resized, nil)
|
||
default:
|
||
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 85})
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||
mimeType := "image/jpeg"
|
||
if format == "png" {
|
||
mimeType = "image/png"
|
||
} else if format == "gif" {
|
||
mimeType = "image/gif"
|
||
}
|
||
return "data:" + mimeType + ";base64," + encoded, nil
|
||
}
|
||
|
||
// validateURL 校验 URL 防止 SSRF
|
||
func validateURL(rawURL string) error {
|
||
if rawURL == "" {
|
||
return nil
|
||
}
|
||
u, err := url.Parse(rawURL)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
host := u.Hostname()
|
||
|
||
// 检查是否是 IP
|
||
ip := net.ParseIP(host)
|
||
if ip != nil {
|
||
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
|
||
return fmt.Errorf("private IP not allowed: %s", host)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 检查是否是内网域名
|
||
lowerHost := strings.ToLower(host)
|
||
if strings.HasSuffix(lowerHost, ".local") ||
|
||
strings.HasSuffix(lowerHost, ".internal") ||
|
||
strings.HasSuffix(lowerHost, ".private") {
|
||
return fmt.Errorf("internal domain not allowed: %s", host)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// cleanupExpiredJobs 清理过期任务
|
||
func (s *minimaxService) cleanupExpiredJobs() {
|
||
ticker := time.NewTicker(1 * time.Hour)
|
||
for range ticker.C {
|
||
s.jobsLock.Lock()
|
||
now := time.Now().UnixMilli()
|
||
expiredThreshold := int64(24 * 60 * 60 * 1000) // 24h
|
||
for jobID, job := range s.jobs {
|
||
if job.Status == StatusCompleted || job.Status == StatusFailed {
|
||
if now-job.UpdatedAt > expiredThreshold {
|
||
delete(s.jobs, jobID)
|
||
}
|
||
}
|
||
}
|
||
s.jobsLock.Unlock()
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add backend/services/assetService/service/minimax_service.go backend/services/assetService/go.mod backend/services/assetService/go.sum
|
||
git commit -m "feat: 添加 MiniMax 图生图服务和任务管理"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 修改 AssetController 添加新接口
|
||
|
||
**Files:**
|
||
- Modify: `backend/gateway/controller/asset_controller.go` (在 AssetController struct 添加 minimaxService 字段,添加两个方法)
|
||
- Test: (无独立测试)
|
||
|
||
- [ ] **Step 1: 添加字段和方法到 AssetController**
|
||
|
||
在 `AssetController` struct 添加:
|
||
```go
|
||
minimaxService service.MinimaxService
|
||
```
|
||
|
||
在 `NewAssetController` 添加:
|
||
```go
|
||
ctrl.minimaxService = service.NewMinimaxService(nil) // TODO: 传入 config
|
||
```
|
||
|
||
添加两个方法:
|
||
|
||
```go
|
||
// ImageGeneration 创建图生图任务
|
||
// @Summary 图生图
|
||
// @Description 创建图生图任务
|
||
// @Tags assets
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param request body dto.ImageGenerationRequest true "图生图请求"
|
||
// @Success 202 {object} response.Response
|
||
// @Router /api/v1/assets/mints/image/generation [post]
|
||
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
|
||
var req dto.ImageGenerationRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.Error(c, 400, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
|
||
userID := c.GetInt64("userID")
|
||
starID := c.GetInt64("starID")
|
||
|
||
job, err := ctrl.minimaxService.CreateJob(c.Request.Context(), userID, starID, &req)
|
||
if err != nil {
|
||
response.Error(c, 500, "Failed to create job: "+err.Error())
|
||
return
|
||
}
|
||
|
||
response.SuccessWithStatus(c, 202, &dto.ImageJobCreateResponse{
|
||
JobID: job.JobID,
|
||
Status: string(job.Status),
|
||
CreatedAt: job.CreatedAt,
|
||
})
|
||
}
|
||
|
||
// GetImageJob 查询图生图任务状态
|
||
// @Summary 查询图生图任务
|
||
// @Description 查询图生图任务状态和结果
|
||
// @Tags assets
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param job_id path string true "任务ID"
|
||
// @Success 200 {object} response.Response
|
||
// @Router /api/v1/assets/mints/image/generation/{job_id} [get]
|
||
func (ctrl *AssetController) GetImageJob(c *gin.Context) {
|
||
jobID := c.Param("job_id")
|
||
if jobID == "" {
|
||
response.Error(c, 400, "job_id is required")
|
||
return
|
||
}
|
||
|
||
userID := c.GetInt64("userID")
|
||
starID := c.GetInt64("starID")
|
||
|
||
job, err := ctrl.minimaxService.GetJob(c.Request.Context(), jobID, userID, starID)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "not found") {
|
||
response.Error(c, 404, "Job not found")
|
||
return
|
||
}
|
||
if strings.Contains(err.Error(), "access denied") {
|
||
response.Error(c, 403, "Access denied")
|
||
return
|
||
}
|
||
response.Error(c, 500, "Failed to get job: "+err.Error())
|
||
return
|
||
}
|
||
|
||
response.Success(c, &dto.ImageJobResponse{
|
||
JobID: job.JobID,
|
||
Status: string(job.Status),
|
||
Progress: job.Progress,
|
||
Images: job.Images,
|
||
ErrorMsg: job.ErrorMsg,
|
||
CreatedAt: job.CreatedAt,
|
||
UpdatedAt: job.UpdatedAt,
|
||
CompletedAt: job.CompletedAt,
|
||
})
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/gateway/controller/asset_controller.go
|
||
git commit -m "feat: 添加 ImageGeneration 和 GetImageJob 接口"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: 注册路由
|
||
|
||
**Files:**
|
||
- Modify: `backend/gateway/router/router.go`
|
||
|
||
- [ ] **Step 1: 在 assets 路由组添加新路由**
|
||
|
||
在 `assets.POST("/mints/precreate", ...)` 附近添加:
|
||
|
||
```go
|
||
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图
|
||
assets.GET("/mints/image/generation/:job_id", assetCtrl.GetImageJob) // 查询任务
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/gateway/router/router.go
|
||
git commit -m "feat: 注册图生图 API 路由"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 修改前端轮询逻辑
|
||
|
||
**Files:**
|
||
- Modify: `frontend/pages/discover/generation-loading.vue`
|
||
|
||
- [ ] **Step 1: 修改 script 部分**
|
||
|
||
将 `callImageGeneration` 函数改为:
|
||
|
||
```javascript
|
||
// job_id 轮询
|
||
let jobId = null;
|
||
|
||
// 调用图生图API - 异步模式
|
||
const callImageGeneration = async () => {
|
||
try {
|
||
const res = await imageGenerationApi(generationData);
|
||
|
||
if (res.data && res.data.job_id) {
|
||
jobId = res.data.job_id;
|
||
console.log('[GenerationLoading] 任务已创建:', jobId);
|
||
// 开始轮询
|
||
pollJobStatus();
|
||
} else {
|
||
uni.showToast({
|
||
title: '创建任务失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
revertProgress();
|
||
}
|
||
} catch (err) {
|
||
console.error('[GenerationLoading] 创建任务失败:', err);
|
||
uni.showToast({
|
||
title: '创建任务失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
revertProgress();
|
||
}
|
||
};
|
||
|
||
// 轮询任务状态
|
||
const pollJobStatus = () => {
|
||
let pollCount = 0;
|
||
const maxPolls = 40; // 120秒 / 3秒 = 40次
|
||
|
||
const poll = async () => {
|
||
if (!jobId) return;
|
||
|
||
try {
|
||
const res = await uni.request({
|
||
url: baseURL + `/api/v1/assets/mints/image/generation/${jobId}`,
|
||
method: 'GET',
|
||
header: {
|
||
'Authorization': `Bearer ${uni.getStorageSync('access_token')}`
|
||
}
|
||
});
|
||
|
||
const data = res.data?.data;
|
||
if (data) {
|
||
// 更新进度
|
||
if (data.progress) {
|
||
progress.value = Math.min(data.progress, 90);
|
||
}
|
||
|
||
if (data.status === 'COMPLETED') {
|
||
// 完成
|
||
const imageUrls = data.images || [];
|
||
if (imageUrls.length > 0) {
|
||
uni.setStorageSync('generated_images', JSON.stringify(imageUrls));
|
||
completeProgress();
|
||
} else {
|
||
uni.showToast({ title: '未生成图片', icon: 'none' });
|
||
revertProgress();
|
||
}
|
||
return;
|
||
} else if (data.status === 'FAILED') {
|
||
// 失败
|
||
uni.showToast({
|
||
title: data.error_msg || '生成失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
revertProgress();
|
||
return;
|
||
}
|
||
}
|
||
|
||
pollCount++;
|
||
if (pollCount >= maxPolls) {
|
||
uni.showToast({ title: '生成超时,请重试', icon: 'none' });
|
||
revertProgress();
|
||
return;
|
||
}
|
||
|
||
// 3秒后继续轮询
|
||
setTimeout(poll, 3000);
|
||
} catch (err) {
|
||
console.error('[GenerationLoading] 轮询失败:', err);
|
||
pollCount++;
|
||
if (pollCount >= maxPolls) {
|
||
uni.showToast({ title: '查询失败,请重试', icon: 'none' });
|
||
revertProgress();
|
||
} else {
|
||
setTimeout(poll, 3000);
|
||
}
|
||
}
|
||
};
|
||
|
||
poll();
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add frontend/pages/discover/generation-loading.vue
|
||
git commit -m "feat: 前端改为轮询模式"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: 添加配置读取 (config)
|
||
|
||
**Files:**
|
||
- Create: `backend/services/assetService/config/minimax_config.go`
|
||
- Modify: `backend/services/assetService/config/asset_config.go` (如果需要)
|
||
|
||
- [ ] **Step 1: 添加配置读取**
|
||
|
||
在 config 包添加 MiniMax 配置读取方法:
|
||
|
||
```go
|
||
// GetMiniMaxAPIURL 获取 MiniMax API URL
|
||
func (c *AssetConfig) GetMiniMaxAPIURL() string {
|
||
return os.Getenv("MINIMAX_API_URL")
|
||
}
|
||
|
||
// GetMiniMaxAPIKey 获取 MiniMax API Key
|
||
func (c *AssetConfig) GetMiniMaxAPIKey() string {
|
||
return os.Getenv("MINIMAX_API_KEY")
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/services/assetService/config/
|
||
git commit -m "feat: 添加 MiniMax 配置读取"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 验证测试
|
||
|
||
- [ ] **Step 1: 启动后端服务**
|
||
|
||
```bash
|
||
cd backend/gateway && go run main.go
|
||
```
|
||
|
||
- [ ] **Step 2: 测试创建任务**
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/v1/assets/mints/image/generation \
|
||
-H "Authorization: Bearer <token>" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model":"image-01","prompt":"test","aspect_ratio":"16:9","n":1,"subject_reference":[]}'
|
||
```
|
||
|
||
- [ ] **Step 3: 测试轮询任务状态**
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/v1/assets/mints/image/generation/<job_id> \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|