refactor: 全面集成 slog 日志到 handler、service、middleware

- cmd/main.go: 集成 logger.Init 和日志中间件,替换标准 log 包
- handler 层: 5xx 错误记录完整日志,返回通用消息(防内部信息泄露)
- service 层: LLM/图片生成/存储/认证等关键操作补充结构化日志
- auth 中间件: 记录认证失败原因
- generate.go: 后台管线任务使用带 task_id 的 logger
This commit is contained in:
2026-05-25 14:31:04 +08:00
parent 681bc8f472
commit 25c11ed649
13 changed files with 159 additions and 39 deletions
+18 -12
View File
@@ -3,11 +3,12 @@ package main
import ( import (
"fmt" "fmt"
"log" "log/slog"
"gen2d/internal/config" "gen2d/internal/config"
"gen2d/internal/db" "gen2d/internal/db"
"gen2d/internal/handler" "gen2d/internal/handler"
"gen2d/internal/logger"
"gen2d/internal/mildware" "gen2d/internal/mildware"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -18,11 +19,15 @@ import (
func main() { func main() {
cfg := config.Load() cfg := config.Load()
// 初始化日志
logger.Init(cfg.Log.Level, cfg.Log.Format)
gin.SetMode(cfg.Server.Mode) gin.SetMode(cfg.Server.Mode)
// 初始化 SQLite 数据库 // 初始化 SQLite 数据库
if err := db.Init(cfg.Database.DSN, &model.User{}); err != nil { if err := db.Init(cfg.Database.DSN, &model.User{}); err != nil {
log.Fatalf("db init failed: %v", err) slog.Error("db init failed", "error", err)
return
} }
// 初始化 AuthService,注入 JWT 配置 // 初始化 AuthService,注入 JWT 配置
@@ -37,24 +42,25 @@ func main() {
handler.InitStorageService(storageSvc) handler.InitStorageService(storageSvc)
r := gin.New() r := gin.New()
r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机 r.Use(mildware.Logger())
r.Use(mildware.Recovery())
// API v1 路由组 — 公开接口 // API v1 路由组 — 公开接口
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
{ {
v1.GET("/health", handler.Health) // 健康检查 v1.GET("/health", handler.Health) // 健康检查
v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化 v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化
v1.GET("/assets/download", handler.DownloadAsset) // 素材下载 v1.GET("/assets/download", handler.DownloadAsset) // 素材下载
} }
// API v1 路由组 — 需认证 // API v1 路由组 — 需认证
v1Auth := r.Group("/api/v1") v1Auth := r.Group("/api/v1")
v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret)) v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret))
{ {
v1Auth.POST("/generate", handler.Generate) // 素材生成管线 v1Auth.POST("/generate", handler.Generate) // 素材生成管线
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务 v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材 v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑 v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
} }
// Auth 路由组 // Auth 路由组
@@ -65,8 +71,8 @@ func main() {
} }
addr := fmt.Sprintf(":%d", cfg.Server.Port) addr := fmt.Sprintf(":%d", cfg.Server.Port)
log.Printf("gen2d backend starting on %s", addr) slog.Info("gen2d backend starting", "addr", addr, "mode", cfg.Server.Mode)
if err := r.Run(addr); err != nil { if err := r.Run(addr); err != nil {
log.Fatalf("server failed: %v", err) slog.Error("server failed", "error", err)
} }
} }
+7 -5
View File
@@ -4,6 +4,7 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -12,9 +13,9 @@ import (
// EditImageRequest 图片编辑请求。 // EditImageRequest 图片编辑请求。
type EditImageRequest struct { type EditImageRequest struct {
Image string `json:"image" binding:"required"` // 底图 base64 编码 Image string `json:"image" binding:"required"` // 底图 base64 编码
Prompt string `json:"prompt" binding:"required"` // 编辑指令 Prompt string `json:"prompt" binding:"required"` // 编辑指令
Count int `json:"count"` // 生成数量,默认 1 Count int `json:"count"` // 生成数量,默认 1
} }
// editAssetResponse 编辑结果素材(返回 base64)。 // editAssetResponse 编辑结果素材(返回 base64)。
@@ -38,7 +39,7 @@ func EditImage(c *gin.Context) {
imageData, err := base64.StdEncoding.DecodeString(req.Image) imageData, err := base64.StdEncoding.DecodeString(req.Image)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "图片 base64 解码失败: "+err.Error())) c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "图片 base64 解码失败"))
return return
} }
@@ -49,7 +50,8 @@ func EditImage(c *gin.Context) {
images, err := service.EditImages(c.Request.Context(), imageData, req.Prompt, count) images, err := service.EditImages(c.Request.Context(), imageData, req.Prompt, count)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "图片编辑失败: "+err.Error())) logger.FromCtx(c.Request.Context()).Error("图片编辑失败", "error", err)
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "图片编辑失败"))
return return
} }
+12 -10
View File
@@ -3,11 +3,11 @@ package handler
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -55,8 +55,8 @@ type TaskResponse struct {
// AssetsResponse 素材列表响应。 // AssetsResponse 素材列表响应。
type AssetsResponse struct { type AssetsResponse struct {
Assets []AssetResponse `json:"assets"` Assets []AssetResponse `json:"assets"`
Metadata service.AssetMetadata `json:"metadata"` Metadata service.AssetMetadata `json:"metadata"`
} }
// taskRecord 内存中的任务记录。 // taskRecord 内存中的任务记录。
@@ -103,11 +103,13 @@ func Generate(c *gin.Context) {
c.JSON(http.StatusOK, model.OK(GenerateResponse{TaskID: taskID})) c.JSON(http.StatusOK, model.OK(GenerateResponse{TaskID: taskID}))
// 后台执行管线 // 后台执行管线
go runPipelineBg(projectID, taskID, req) go runPipelineBg(c.Request.Context(), projectID, taskID, req)
} }
// runPipelineBg 后台执行生成管线,更新任务状态。 // runPipelineBg 后台执行生成管线,更新任务状态。
func runPipelineBg(projectID, taskID string, req GenerateRequest) { func runPipelineBg(ctx context.Context, projectID, taskID string, req GenerateRequest) {
l := logger.With("task_id", taskID, "project_id", projectID)
updateStatus(taskID, "running", 10) updateStatus(taskID, "running", 10)
in := service.PipelineInput{ in := service.PipelineInput{
@@ -129,11 +131,10 @@ func runPipelineBg(projectID, taskID string, req GenerateRequest) {
}, },
} }
ctx := context.Background()
output, err := service.RunPipeline(ctx, in) output, err := service.RunPipeline(ctx, in)
if err != nil { if err != nil {
log.Printf("[generate] task %s failed: %v", taskID, err) l.Error("task pipeline failed", "error", err)
updateFailed(taskID, err.Error()) updateFailed(taskID, "生成管线执行失败")
return return
} }
@@ -144,7 +145,8 @@ func runPipelineBg(projectID, taskID string, req GenerateRequest) {
key := fmt.Sprintf("generation/%s/%s/%d.%s", projectID, taskID, i, a.Format) key := fmt.Sprintf("generation/%s/%s/%d.%s", projectID, taskID, i, a.Format)
cdnURL, err := storageSvc.Upload(ctx, key, a.Data) cdnURL, err := storageSvc.Upload(ctx, key, a.Data)
if err != nil { if err != nil {
updateFailed(taskID, "上传素材失败: "+err.Error()) l.Error("upload asset failed", "index", i, "error", err)
updateFailed(taskID, "上传素材失败")
return return
} }
assets[i] = AssetResponse{ assets[i] = AssetResponse{
@@ -168,7 +170,7 @@ func runPipelineBg(projectID, taskID string, req GenerateRequest) {
metadata: output.Metadata, metadata: output.Metadata,
}) })
log.Printf("[generate] task %s completed, %d assets", taskID, len(assets)) l.Info("task completed", "asset_count", len(assets))
} }
func updateStatus(taskID, status string, progress int) { func updateStatus(taskID, status string, progress int) {
+2
View File
@@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -31,6 +32,7 @@ func Login(c *gin.Context) {
token, expiresIn, user, err := authSvc.Login(c.Request.Context(), req.Username, req.Password) token, expiresIn, user, err := authSvc.Login(c.Request.Context(), req.Username, req.Password)
if err != nil { if err != nil {
logger.FromCtx(c.Request.Context()).Warn("登录失败", "username", req.Username, "error", err)
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, err.Error())) c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, err.Error()))
return return
} }
+3 -1
View File
@@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -34,7 +35,8 @@ func PromptOptimize(c *gin.Context) {
output, err := service.RunPromptAgent(c.Request.Context(), in) output, err := service.RunPromptAgent(c.Request.Context(), in)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "提示词优化失败: "+err.Error())) logger.FromCtx(c.Request.Context()).Error("提示词优化失败", "error", err)
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "提示词优化失败"))
return return
} }
+2
View File
@@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -39,6 +40,7 @@ func Register(c *gin.Context) {
user, err := authSvc.Register(c.Request.Context(), req.Username, req.Password, req.Email) user, err := authSvc.Register(c.Request.Context(), req.Username, req.Password, req.Email)
if err != nil { if err != nil {
logger.FromCtx(c.Request.Context()).Warn("注册失败", "username", req.Username, "error", err)
c.JSON(http.StatusConflict, model.Fail(http.StatusConflict, err.Error())) c.JSON(http.StatusConflict, model.Fail(http.StatusConflict, err.Error()))
return return
} }
+2
View File
@@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"gen2d/internal/service" "gen2d/internal/service"
@@ -27,6 +28,7 @@ func DownloadAsset(c *gin.Context) {
downloadURL, err := storageSvc.GetDownloadURL(c.Request.Context(), key) downloadURL, err := storageSvc.GetDownloadURL(c.Request.Context(), key)
if err != nil { if err != nil {
logger.FromCtx(c.Request.Context()).Error("生成下载链接失败", "key", key, "error", err)
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "生成下载链接失败")) c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "生成下载链接失败"))
return return
} }
+5
View File
@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -15,12 +16,14 @@ func AuthMiddleware(jwtSecret string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader == "" { if authHeader == "" {
logger.FromCtx(c.Request.Context()).Warn("auth failed: no token")
c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未提供认证令牌")) c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未提供认证令牌"))
return return
} }
tokenString := strings.TrimPrefix(authHeader, "Bearer ") tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader { if tokenString == authHeader {
logger.FromCtx(c.Request.Context()).Warn("auth failed: invalid format")
c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "认证格式错误,需为 Bearer <token>")) c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "认证格式错误,需为 Bearer <token>"))
return return
} }
@@ -29,12 +32,14 @@ func AuthMiddleware(jwtSecret string) gin.HandlerFunc {
return []byte(jwtSecret), nil return []byte(jwtSecret), nil
}) })
if err != nil || !token.Valid { if err != nil || !token.Valid {
logger.FromCtx(c.Request.Context()).Warn("auth failed: invalid token", "error", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "令牌无效或已过期")) c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "令牌无效或已过期"))
return return
} }
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
if !ok { if !ok {
logger.FromCtx(c.Request.Context()).Warn("auth failed: parse claims")
c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "令牌解析失败")) c.AbortWithStatusJSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "令牌解析失败"))
return return
} }
+14
View File
@@ -6,6 +6,7 @@ import (
"time" "time"
"gen2d/internal/db" "gen2d/internal/db"
"gen2d/internal/logger"
"gen2d/internal/model" "gen2d/internal/model"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -29,8 +30,11 @@ func NewAuthService(jwtSecret string, jwtExpire int64) *AuthService {
// Register 注册新用户,密码使用 bcrypt 加密存储。 // Register 注册新用户,密码使用 bcrypt 加密存储。
func (s *AuthService) Register(ctx context.Context, username, password, email string) (*model.User, error) { func (s *AuthService) Register(ctx context.Context, username, password, email string) (*model.User, error) {
l := logger.FromCtx(ctx)
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
l.Error("bcrypt failed", "error", err)
return nil, err return nil, err
} }
@@ -42,25 +46,33 @@ func (s *AuthService) Register(ctx context.Context, username, password, email st
if err := db.DB.WithContext(ctx).Create(&user).Error; err != nil { if err := db.DB.WithContext(ctx).Create(&user).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
l.Warn("register failed: duplicate username", "username", username)
return nil, errors.New("用户名已存在") return nil, errors.New("用户名已存在")
} }
l.Error("register db failed", "username", username, "error", err)
return nil, err return nil, err
} }
l.Info("user registered", "username", username, "user_id", user.ID)
return &user, nil return &user, nil
} }
// Login 校验用户名密码,成功返回 JWT token 及用户信息。 // Login 校验用户名密码,成功返回 JWT token 及用户信息。
func (s *AuthService) Login(ctx context.Context, username, password string) (string, int64, *model.User, error) { func (s *AuthService) Login(ctx context.Context, username, password string) (string, int64, *model.User, error) {
l := logger.FromCtx(ctx)
var user model.User var user model.User
if err := db.DB.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil { if err := db.DB.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
l.Warn("login failed: user not found", "username", username)
return "", 0, nil, errors.New("用户名或密码错误") return "", 0, nil, errors.New("用户名或密码错误")
} }
l.Error("login db query failed", "username", username, "error", err)
return "", 0, nil, err return "", 0, nil, err
} }
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
l.Warn("login failed: wrong password", "username", username)
return "", 0, nil, errors.New("用户名或密码错误") return "", 0, nil, errors.New("用户名或密码错误")
} }
@@ -72,8 +84,10 @@ func (s *AuthService) Login(ctx context.Context, username, password string) (str
} }
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.jwtSecret) token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.jwtSecret)
if err != nil { if err != nil {
l.Error("jwt sign failed", "error", err)
return "", 0, nil, err return "", 0, nil, err
} }
l.Info("login succeeded", "username", username, "user_id", user.ID)
return token, int64(s.jwtExpire.Seconds()), &user, nil return token, int64(s.jwtExpire.Seconds()), &user, nil
} }
+50 -6
View File
@@ -11,13 +11,13 @@ import (
"image/color" "image/color"
"image/png" "image/png"
"io" "io"
"log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"gen2d/internal/config" "gen2d/internal/config"
"gen2d/internal/logger"
) )
// imgCfg 保存文生图配置,由 main 通过 InitImageGenConfig 注入。 // imgCfg 保存文生图配置,由 main 通过 InitImageGenConfig 注入。
@@ -64,21 +64,33 @@ func GenerateImages(ctx context.Context, prompt string, params AssetParams) ([]G
count = params.Frames.Directions * params.Frames.FramesPerDirection count = params.Frames.Directions * params.Frames.FramesPerDirection
} }
l := logger.FromCtx(ctx)
if imgCfg.APIKey != "" { if imgCfg.APIKey != "" {
width, height := imgCfg.Width, imgCfg.Height width, height := imgCfg.Width, imgCfg.Height
if params.Resolution > 0 { if params.Resolution > 0 {
width = params.Resolution width = params.Resolution
height = params.Resolution height = params.Resolution
} }
log.Println("[inference] calling image gen API") l.Info("calling image gen API",
return callImageAPI(ctx, prompt, count, width, height) "model", imgCfg.Model,
"count", count,
"size", fmt.Sprintf("%dx%d", width, height),
)
images, err := callImageAPI(ctx, prompt, count, width, height)
if err != nil {
l.Error("image gen API failed", "error", err)
return nil, err
}
l.Info("image gen API succeeded", "image_count", len(images))
return images, nil
} }
size := params.Resolution size := params.Resolution
if size <= 0 { if size <= 0 {
size = 64 size = 64
} }
log.Println("[inference] image API key not configured, using mock") l.Warn("image API key not configured, using mock")
return generateMockImages(size, count) return generateMockImages(size, count)
} }
@@ -107,10 +119,15 @@ func callImageAPI(ctx context.Context, prompt string, count, width, height int)
retryDelay = 5 * time.Second retryDelay = 5 * time.Second
} }
l := logger.FromCtx(ctx)
var lastErr error var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ { for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 { if attempt > 0 {
log.Printf("[inference] retrying image API (attempt %d/%d)", attempt, maxRetries) l.Warn("retrying image API",
"attempt", attempt,
"max_retries", maxRetries,
)
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, fmt.Errorf("context cancelled during retry: %w", ctx.Err()) return nil, fmt.Errorf("context cancelled during retry: %w", ctx.Err())
@@ -128,6 +145,7 @@ func callImageAPI(ctx context.Context, prompt string, count, width, height int)
resp, err := imageHTTPClient.Do(req) resp, err := imageHTTPClient.Do(req)
if err != nil { if err != nil {
lastErr = fmt.Errorf("send request: %w", err) lastErr = fmt.Errorf("send request: %w", err)
l.Warn("image API request failed", "error", lastErr)
continue continue
} }
@@ -140,8 +158,18 @@ func callImageAPI(ctx context.Context, prompt string, count, width, height int)
if resp.StatusCode >= 500 { if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b)) lastErr = fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b))
l.Error("image API server error",
"status", resp.StatusCode,
"body", string(b),
"attempt", attempt,
)
continue continue
} }
// 4xx 不重试
l.Error("image API client error",
"status", resp.StatusCode,
"body", string(b),
)
return nil, fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b)) return nil, fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b))
} }
@@ -205,10 +233,17 @@ func downloadImage(ctx context.Context, url string) ([]byte, error) {
// EditImages 图片编辑接口,调用 OpenAI 兼容 Images Edits API(multipart/form-data)。 // EditImages 图片编辑接口,调用 OpenAI 兼容 Images Edits API(multipart/form-data)。
func EditImages(ctx context.Context, imageData []byte, prompt string, count int) ([]GeneratedImage, error) { func EditImages(ctx context.Context, imageData []byte, prompt string, count int) ([]GeneratedImage, error) {
l := logger.FromCtx(ctx)
if imgCfg.APIKey == "" { if imgCfg.APIKey == "" {
return nil, fmt.Errorf("image API key not configured") return nil, fmt.Errorf("image API key not configured")
} }
l.Info("calling image edit API",
"model", imgCfg.Model,
"count", count,
)
var buf bytes.Buffer var buf bytes.Buffer
writer := multipart.NewWriter(&buf) writer := multipart.NewWriter(&buf)
@@ -239,16 +274,25 @@ func EditImages(ctx context.Context, imageData []byte, prompt string, count int)
resp, err := imageHTTPClient.Do(req) resp, err := imageHTTPClient.Do(req)
if err != nil { if err != nil {
l.Error("image edit API request failed", "error", err)
return nil, fmt.Errorf("send request: %w", err) return nil, fmt.Errorf("send request: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
l.Error("image edit API error", "status", resp.StatusCode, "body", string(b))
return nil, fmt.Errorf("image edit api error %d: %s", resp.StatusCode, string(b)) return nil, fmt.Errorf("image edit api error %d: %s", resp.StatusCode, string(b))
} }
return parseImageResponse(ctx, resp.Body, imgCfg.Width, imgCfg.Height) images, err := parseImageResponse(ctx, resp.Body, imgCfg.Width, imgCfg.Height)
if err != nil {
l.Error("image edit response parse failed", "error", err)
return nil, err
}
l.Info("image edit API succeeded", "image_count", len(images))
return images, nil
} }
// ======================== 质检 ======================== // ======================== 质检 ========================
+14
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"gen2d/internal/logger"
"github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/compose"
) )
@@ -82,20 +84,32 @@ func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
// RunPipeline 编译并执行生成管线。 // RunPipeline 编译并执行生成管线。
func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) { func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) {
l := logger.FromCtx(ctx)
l.Info("pipeline started",
"asset_type", in.AssetType,
"tags", in.Tags,
"resolution", in.Params.Resolution,
)
g, err := NewGenerateGraph() g, err := NewGenerateGraph()
if err != nil { if err != nil {
l.Error("pipeline create graph failed", "error", err)
return nil, fmt.Errorf("create graph: %w", err) return nil, fmt.Errorf("create graph: %w", err)
} }
r, err := g.Compile(ctx, compose.WithMaxRunSteps(20)) r, err := g.Compile(ctx, compose.WithMaxRunSteps(20))
if err != nil { if err != nil {
l.Error("pipeline compile failed", "error", err)
return nil, fmt.Errorf("compile graph: %w", err) return nil, fmt.Errorf("compile graph: %w", err)
} }
output, err := r.Invoke(ctx, in) output, err := r.Invoke(ctx, in)
if err != nil { if err != nil {
l.Error("pipeline invoke failed", "error", err)
return nil, fmt.Errorf("invoke pipeline: %w", err) return nil, fmt.Errorf("invoke pipeline: %w", err)
} }
l.Info("pipeline completed", "asset_count", len(output.Assets))
return &output, nil return &output, nil
} }
+10 -3
View File
@@ -7,11 +7,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"strings" "strings"
"gen2d/internal/config" "gen2d/internal/config"
"gen2d/internal/logger"
"github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/compose"
) )
@@ -130,8 +130,10 @@ type chatResponse struct {
// callLLMRefine 调用 LLM 生成规范化提示词。未配置 API key 时回退到模板生成。 // callLLMRefine 调用 LLM 生成规范化提示词。未配置 API key 时回退到模板生成。
func callLLMRefine(ctx context.Context, metaPrompt string) (PromptAgentOutput, error) { func callLLMRefine(ctx context.Context, metaPrompt string) (PromptAgentOutput, error) {
l := logger.FromCtx(ctx)
if llmCfg.APIKey == "" { if llmCfg.APIKey == "" {
log.Println("[prompt_agent] LLM API key not configured, using template fallback") l.Warn("LLM API key not configured, using template fallback")
return fallbackRefine(metaPrompt), nil return fallbackRefine(metaPrompt), nil
} }
@@ -140,12 +142,14 @@ func callLLMRefine(ctx context.Context, metaPrompt string) (PromptAgentOutput, e
{Role: "user", Content: metaPrompt}, {Role: "user", Content: metaPrompt},
} }
l.Info("calling LLM API", "model", llmCfg.Model)
result, err := chatCompletion(ctx, messages) result, err := chatCompletion(ctx, messages)
if err != nil { if err != nil {
log.Printf("[prompt_agent] LLM API call failed: %v, using template fallback", err) l.Error("LLM API call failed, using template fallback", "error", err)
return fallbackRefine(metaPrompt), nil return fallbackRefine(metaPrompt), nil
} }
l.Info("LLM API succeeded", "response_length", len(result))
return PromptAgentOutput{ return PromptAgentOutput{
Prompt: result, Prompt: result,
RawText: result, RawText: result,
@@ -175,6 +179,8 @@ func chatCompletion(ctx context.Context, messages []chatMessage) (string, error)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+llmCfg.APIKey) req.Header.Set("Authorization", "Bearer "+llmCfg.APIKey)
l := logger.FromCtx(ctx)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("send request: %w", err) return "", fmt.Errorf("send request: %w", err)
@@ -183,6 +189,7 @@ func chatCompletion(ctx context.Context, messages []chatMessage) (string, error)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
l.Error("LLM API error", "status", resp.StatusCode, "body", string(b))
return "", fmt.Errorf("llm api error %d: %s", resp.StatusCode, string(b)) return "", fmt.Errorf("llm api error %d: %s", resp.StatusCode, string(b))
} }
+20 -2
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"gen2d/internal/config" "gen2d/internal/config"
"gen2d/internal/logger"
"github.com/qiniu/go-sdk/v7/storagev2/credentials" "github.com/qiniu/go-sdk/v7/storagev2/credentials"
"github.com/qiniu/go-sdk/v7/storagev2/objects" "github.com/qiniu/go-sdk/v7/storagev2/objects"
@@ -39,8 +40,11 @@ func NewStorageService(cfg config.QiniuConfig) *StorageService {
// Upload 上传图片字节到七牛云,返回 CDN 访问 URL。 // Upload 上传图片字节到七牛云,返回 CDN 访问 URL。
func (s *StorageService) Upload(ctx context.Context, key string, data []byte) (string, error) { func (s *StorageService) Upload(ctx context.Context, key string, data []byte) (string, error) {
l := logger.FromCtx(ctx)
putPolicy, err := uptoken.NewPutPolicy(s.bucket, time.Now().Add(1*time.Hour)) putPolicy, err := uptoken.NewPutPolicy(s.bucket, time.Now().Add(1*time.Hour))
if err != nil { if err != nil {
l.Error("qiniu put policy failed", "key", key, "error", err)
return "", fmt.Errorf("qiniu put policy: %w", err) return "", fmt.Errorf("qiniu put policy: %w", err)
} }
upToken := uptoken.NewSigner(putPolicy, s.creds) upToken := uptoken.NewSigner(putPolicy, s.creds)
@@ -49,14 +53,19 @@ func (s *StorageService) Upload(ctx context.Context, key string, data []byte) (s
UpTokenProvider: upToken, UpTokenProvider: upToken,
}) })
l.Info("uploading to qiniu", "key", key, "size", len(data))
err = uploadMgr.UploadReader(ctx, bytes.NewReader(data), &uploader.ObjectOptions{ err = uploadMgr.UploadReader(ctx, bytes.NewReader(data), &uploader.ObjectOptions{
BucketName: s.bucket, BucketName: s.bucket,
ObjectName: &key, ObjectName: &key,
}, nil) }, nil)
if err != nil { if err != nil {
l.Error("qiniu upload failed", "key", key, "error", err)
return "", fmt.Errorf("qiniu upload: %w", err) return "", fmt.Errorf("qiniu upload: %w", err)
} }
return s.buildURL(key), nil
cdnURL := s.buildURL(key)
l.Info("upload succeeded", "key", key, "url", cdnURL)
return cdnURL, nil
} }
// GetDownloadURL 生成素材的下载 URL。 // GetDownloadURL 生成素材的下载 URL。
@@ -67,8 +76,17 @@ func (s *StorageService) GetDownloadURL(_ context.Context, key string) (string,
// Delete 从七牛云删除指定对象。 // Delete 从七牛云删除指定对象。
func (s *StorageService) Delete(ctx context.Context, key string) error { func (s *StorageService) Delete(ctx context.Context, key string) error {
l := logger.FromCtx(ctx)
bucket := s.objectsMgr.Bucket(s.bucket) bucket := s.objectsMgr.Bucket(s.bucket)
return bucket.Object(key).Delete().Call(ctx) err := bucket.Object(key).Delete().Call(ctx)
if err != nil {
l.Error("qiniu delete failed", "key", key, "error", err)
return err
}
l.Info("delete succeeded", "key", key)
return nil
} }
// buildURL 根据配置构建完整的 CDN URL。 // buildURL 根据配置构建完整的 CDN URL。