diff --git a/scripts/gen_outfit_plan/main.go b/scripts/gen_outfit_plan/main.go new file mode 100644 index 0000000..bc88976 --- /dev/null +++ b/scripts/gen_outfit_plan/main.go @@ -0,0 +1,223 @@ +package main + +// 为 wenwu901 真实生成一套 AI 穿搭方案(真实调用 imagegen 与 LLM,非 mock): +// go run scripts/gen_outfit_plan/main.go +// 步骤:补衣橱(8 件单品,imagegen 生成服装图)→ 调 OutfitService.Generate → +// 轮询任务到 done → 选主方案(触发效果图异步生成)→ 等 3 张效果图完成。 +// 前置:config.yml 已配置 geo.amap_key + weather.qweather_key(天气硬依赖)。 + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" + "github.com/gogf/gf/v2/frame/g" + + "slogan-agent/styleagent/agent" + "slogan-agent/styleagent/consts" + "slogan-agent/styleagent/dao" + "slogan-agent/styleagent/model/dto" + "slogan-agent/styleagent/model/entity" + "slogan-agent/styleagent/service" +) + +const username = "wenwu901" + +type garment struct { + name string + category string + style string + color string + prompt string +} + +var garments = []garment{ + {"白色长袖衬衫", "上衣", "休闲", "白色", "纯白背景的白色长袖衬衫商品图,正面展示,高清,电商风格"}, + {"灰色圆领T恤", "上衣", "休闲", "灰色", "纯白背景的灰色圆领T恤商品图,正面展示,高清,电商风格"}, + {"深蓝夹克外套", "上衣", "外套", "深蓝", "纯白背景的深蓝色夹克外套商品图,正面展示,高清,电商风格"}, + {"深灰休闲长裤", "下装", "休闲", "深灰", "纯白背景的深灰色休闲长裤商品图,正面展示,高清,电商风格"}, + {"蓝色牛仔裤", "下装", "休闲", "蓝色", "纯白背景的蓝色牛仔裤商品图,正面展示,高清,电商风格"}, + {"白色运动鞋", "鞋", "休闲", "白色", "纯白背景的白色运动鞋商品图,侧面展示,高清,电商风格"}, + {"棕色皮鞋", "鞋", "商务", "棕色", "纯白背景的棕色皮鞋商品图,侧面展示,高清,电商风格"}, + {"黑色双肩背包", "配饰", "休闲", "黑色", "纯白背景的黑色双肩背包商品图,正面展示,高清,电商风格"}, +} + +func main() { + ctx := context.Background() + + var user entity.User + if err := g.DB().Model(consts.TableNameUser).Ctx(ctx). + Where("username", username).Scan(&user); err != nil || user.Id == 0 { + panic(fmt.Sprintf("用户 %s 不存在: %v", username, err)) + } + fmt.Printf("用户: %s (id=%d)\n", username, user.Id) + + if err := ensureWardrobe(ctx, user.Id); err != nil { + panic(err) + } + + // 幂等:已有方案则只重试效果图(主方案 → select-main → 等 3 张 done) + var mainPlan *entity.OutfitPlan + existing, _ := dao.OutfitPlan.ListByUser(ctx, user.Id) + for _, p := range existing { + if p.MainFlag == 1 { + mainPlan = p + } + } + if len(existing) == 0 { + taskId, err := service.OutfitService.Generate(ctx, user.Id, &dto.OutfitGenerateReq{ + StartDate: "2026-08-01", EndDate: "2026-08-07", Location: "上海", Occasion: "通勤", + }) + if err != nil { + if strings.Contains(err.Error(), "未配置") { + panic(fmt.Sprintf("%v\n请先在 config.yml 配置 geo.amap_key / weather.qweather_key 后重跑", err)) + } + panic(fmt.Sprintf("发起方案生成失败: %v", err)) + } + fmt.Printf("生成任务已提交: task_id=%d,轮询中...\n", taskId) + + waitTaskDone(ctx, taskId, user.Id) + plans, err := dao.OutfitPlan.ListByTask(ctx, taskId) + if err != nil || len(plans) == 0 { + panic(fmt.Sprintf("任务完成但无方案: %v", err)) + } + mainPlan = plans[0] + for _, p := range plans { + if p.MainFlag == 1 { + mainPlan = p + } + } + } else if mainPlan == nil { + panic("已有方案但无主方案,请先选主方案") + } + fmt.Printf("主方案: id=%d %s 评分=%d\n", mainPlan.Id, mainPlan.Title, mainPlan.Score) + if err := service.OutfitPlanService.SelectMain(ctx, user.Id, mainPlan.Id); err != nil { + panic(fmt.Sprintf("选主方案失败: %v", err)) + } + + waitEffects(ctx, mainPlan.Id) + + all, _ := dao.OutfitPlan.ListByUser(ctx, user.Id) + fmt.Printf("完成!wenwu901 现有 %d 套方案:\n", len(all)) + for _, p := range all { + fmt.Printf(" - plan %d: %s(评分 %d,%s)\n", p.Id, p.Title, p.Score, p.Source) + } +} + +// ensureWardrobe 为指定用户补齐 8 件衣橱单品(同 Category 已有则跳过该分类),服装图用 imagegen 生成 +func ensureWardrobe(ctx context.Context, userId int64) error { + existing, err := dao.WardrobeItem.ListAllByUser(ctx, userId) + if err != nil { + return err + } + have := map[string]bool{} + for _, it := range existing { + have[it.Category] = true + } + need := make([]garment, 0, len(garments)) + for _, ga := range garments { + if !have[ga.category] { + need = append(need, ga) + } + } + if len(need) == 0 { + fmt.Println("衣橱 4 类已齐,跳过补衣橱") + return nil + } + + client, err := agent.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "wanx").String()) + if err != nil { + return err + } + dir := filepath.Join("workspace", fmt.Sprintf("user_%d", userId), "wardrobe") + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + for _, ga := range need { + fmt.Printf("生成服装图: %s...\n", ga.name) + url, err := client.Generate(ctx, &agent.GenerateReq{ + Prompt: ga.prompt, Seed: time.Now().UnixNano() % 1_000_000, + }) + if err != nil { + return fmt.Errorf("生成 %s 服装图失败: %w", ga.name, err) + } + path := filepath.Join(dir, fmt.Sprintf("%d_%s.png", time.Now().UnixNano(), ga.name)) + if err := download(url, path); err != nil { + return fmt.Errorf("保存 %s 失败: %w", ga.name, err) + } + if _, err := dao.WardrobeItem.Insert(ctx, &entity.WardrobeItem{ + UserId: userId, PhotoUrl: "/" + filepath.ToSlash(path), + Name: ga.name, Category: ga.category, Season: "四季", StyleTags: ga.style, + ColorInfo: ga.color, Status: 1, + }); err != nil { + return fmt.Errorf("入库 %s 失败: %w", ga.name, err) + } + fmt.Printf("%s 完成: %s\n", ga.name, path) + } + fmt.Println("衣橱补齐完毕") + return nil +} + +func waitTaskDone(ctx context.Context, taskId, userId int64) { + for i := 0; i < 30; i++ { + task, err := dao.OutfitGenTask.GetOne(ctx, taskId, userId) + if err != nil || task == nil { + panic(fmt.Sprintf("读取任务失败: %v", err)) + } + switch task.Status { + case consts.TaskStatusDone: + fmt.Println("方案生成完成") + return + case consts.TaskStatusFailed: + panic(fmt.Sprintf("方案生成失败: %s", task.Error)) + } + time.Sleep(10 * time.Second) + } + panic("方案生成超时(5 分钟)") +} + +// waitEffects 等主方案的 3 张效果图(正面/侧面/背面)全部 done +func waitEffects(ctx context.Context, planId int64) { + for i := 0; i < 20; i++ { + images, _ := dao.PlanEffectImage.ListByPlan(ctx, planId) + done := 0 + for _, im := range images { + if im.Status == consts.EffectStatusDone { + done++ + } + } + if done >= 3 { + fmt.Printf("效果图 3 张完成\n") + return + } + if i == 19 { + fmt.Printf("警告: 效果图超时(完成 %d/3),可稍后查看\n", done) + return + } + time.Sleep(15 * time.Second) + } +} + +func download(url, dest string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("下载失败: http %d", resp.StatusCode) + } + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/styleagent/agent/imagegen_wanx_client.go b/styleagent/agent/imagegen_wanx_client.go index 4ad78eb..2fa9aae 100644 --- a/styleagent/agent/imagegen_wanx_client.go +++ b/styleagent/agent/imagegen_wanx_client.go @@ -36,15 +36,11 @@ type wanxMessage struct { Content []wanxInputContent `json:"content"` } -// 请求侧 content 元素(图生图传 image_url 对象) +// 请求侧 content 元素:wan2.7-image-pro 原生格式直接放 text / image 字段, +// 不能用 OpenAI 兼容的 {"type":"image_url"} 形式(会报 "Either 'text' or 'image' must be provided, but not both") type wanxInputContent struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - ImageURL *wanxImageURL `json:"image_url,omitempty"` -} - -type wanxImageURL struct { - URL string `json:"url"` + Text string `json:"text,omitempty"` + Image string `json:"image,omitempty"` } // 响应侧 content 元素(图片在 image 字段) @@ -68,22 +64,21 @@ type wanxTaskResp struct { } // Generate 文生图或图生图(BaseImageURL 本地路径转 data URI,http(s) 直传),异步任务 + 轮询 +// 注:wan2.7-image-pro 图生图需同一条 user 消息中并列 {"text"} 与 {"image"} 两个 content 元素 func (c *wanxClient) Generate(ctx context.Context, req *GenerateReq) (string, error) { - content := make([]wanxInputContent, 0, 2) + content := []wanxInputContent{{Text: buildPrompt(req.Prompt, "", "", req.Angle)}} if req.BaseImageURL != "" { imgURL, err := resolveImageURL(req.BaseImageURL) if err != nil { return "", err } - content = append(content, wanxInputContent{Type: "image_url", ImageURL: &wanxImageURL{URL: imgURL}}) + content = append(content, wanxInputContent{Image: imgURL}) } - content = append(content, wanxInputContent{Type: "text", Text: buildPrompt(req.Prompt, "", "", req.Angle)}) + messages := []wanxMessage{{Role: "user", Content: content}} body, err := json.Marshal(wanxSubmitReq{ - Model: c.model, - Input: wanxInput{Messages: []wanxMessage{ - {Role: "user", Content: content}, - }}, + Model: c.model, + Input: wanxInput{Messages: messages}, Parameters: map[string]any{"n": 1, "size": "768*1024", "seed": req.Seed}, }) if err != nil { diff --git a/styleagent/agent/scoring_rules.go b/styleagent/agent/scoring_rules.go index 1763179..2d824bd 100644 --- a/styleagent/agent/scoring_rules.go +++ b/styleagent/agent/scoring_rules.go @@ -1,5 +1,9 @@ package agent +// NewItemBaseScore 推荐新品(用户衣橱不具备)的单品基础分, +// 无衣橱属性(颜色/风格)无法走规则评分,直接给固定分值并入方案总分 +const NewItemBaseScore = 15 + // WardrobeItem 评分用服装条目(从衣橱 entity 转换) type WardrobeItem struct { Category string // 上衣/下装/鞋/配饰 diff --git a/styleagent/controller/body_measurement_controller.go b/styleagent/controller/body_measurement_controller.go index 7cd2c07..5779ca6 100644 --- a/styleagent/controller/body_measurement_controller.go +++ b/styleagent/controller/body_measurement_controller.go @@ -20,6 +20,10 @@ func (c *body_measurement) Save(ctx context.Context, req *dto.BodyMeasurementSav Height: req.Height, Weight: req.Weight, SkinTone: req.SkinTone, + Bust: req.Bust, + Waist: req.Waist, + Hip: req.Hip, + Shoulder: req.Shoulder, FitParams: req.FitParams, }); err != nil { return nil, err @@ -36,6 +40,10 @@ func (c *body_measurement) Get(ctx context.Context, req *dto.BodyMeasurementGetR Height: b.Height, Weight: b.Weight, SkinTone: b.SkinTone, + Bust: b.Bust, + Waist: b.Waist, + Hip: b.Hip, + Shoulder: b.Shoulder, FitParams: b.FitParams, }, nil } diff --git a/styleagent/dao/body_measurement_dao.go b/styleagent/dao/body_measurement_dao.go index 23c331e..5edb859 100644 --- a/styleagent/dao/body_measurement_dao.go +++ b/styleagent/dao/body_measurement_dao.go @@ -2,6 +2,8 @@ package dao import ( "context" + "strings" + "slogan-agent/styleagent/consts" "slogan-agent/styleagent/model/entity" @@ -26,6 +28,19 @@ func init() { if err != nil { g.Log().Warningf(ctx, "create body_measurement table failed: %v", err) } + // 旧库补列(CREATE TABLE IF NOT EXISTS 不给已存在表加列,忽略 duplicate column 错误) + for _, col := range []string{ + "bust INTEGER NOT NULL DEFAULT 0", + "waist INTEGER NOT NULL DEFAULT 0", + "hip INTEGER NOT NULL DEFAULT 0", + "shoulder INTEGER NOT NULL DEFAULT 0", + } { + if _, err := g.DB().Exec(ctx, "ALTER TABLE "+consts.TableNameBodyMeasurement+" ADD COLUMN "+col); err != nil { + if !strings.Contains(err.Error(), "duplicate column") { + g.Log().Warningf(ctx, "alter body_measurement add column failed: %v", err) + } + } + } } func (d *bodyMeasurementDao) Save(ctx context.Context, data *entity.BodyMeasurement) error { @@ -36,12 +51,14 @@ func (d *bodyMeasurementDao) Save(ctx context.Context, data *entity.BodyMeasurem } if r == nil { _, err = g.DB().Exec(ctx, - "INSERT INTO "+consts.TableNameBodyMeasurement+" (user_id, height, weight, skin_tone, fit_params, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now','localtime'))", - data.UserId, data.Height, data.Weight, data.SkinTone, data.FitParams) + "INSERT INTO "+consts.TableNameBodyMeasurement+" (user_id, height, weight, skin_tone, bust, waist, hip, shoulder, fit_params, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'))", + data.UserId, data.Height, data.Weight, data.SkinTone, + data.Bust, data.Waist, data.Hip, data.Shoulder, data.FitParams) return err } _, err = g.DB().Model(consts.TableNameBodyMeasurement).Ctx(ctx).Data(g.Map{ "height": data.Height, "weight": data.Weight, "skin_tone": data.SkinTone, + "bust": data.Bust, "waist": data.Waist, "hip": data.Hip, "shoulder": data.Shoulder, "fit_params": data.FitParams, "updated_at": "datetime('now','localtime')", }).Where("user_id", data.UserId).Update() return err diff --git a/styleagent/dao/wardrobe_item_dao.go b/styleagent/dao/wardrobe_item_dao.go index 1b6649f..52d3ba0 100644 --- a/styleagent/dao/wardrobe_item_dao.go +++ b/styleagent/dao/wardrobe_item_dao.go @@ -28,13 +28,18 @@ func init() { if err != nil { g.Log().Warningf(ctx, "create wardrobe_item table failed: %v", err) } + // 容错迁移:CREATE TABLE IF NOT EXISTS 不给旧库加列,duplicate column 错误可忽略 + if _, err := g.DB().Exec(ctx, "ALTER TABLE "+consts.TableNameWardrobeItem+ + " ADD COLUMN name TEXT NOT NULL DEFAULT ''"); err != nil { + g.Log().Warningf(ctx, "migrate wardrobe_item.name skipped: %v", err) + } _, _ = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_slogan_wardrobe_user ON "+consts.TableNameWardrobeItem+"(user_id, category)") } func (d *wardrobeItemDao) Insert(ctx context.Context, data *entity.WardrobeItem) (int64, error) { r, err := g.DB().Exec(ctx, - "INSERT INTO "+consts.TableNameWardrobeItem+" (user_id, photo_url, category, season, style_tags, color_info, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'))", - data.UserId, data.PhotoUrl, data.Category, data.Season, data.StyleTags, data.ColorInfo, data.Status) + "INSERT INTO "+consts.TableNameWardrobeItem+" (user_id, photo_url, name, category, season, style_tags, color_info, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'))", + data.UserId, data.PhotoUrl, data.Name, data.Category, data.Season, data.StyleTags, data.ColorInfo, data.Status) if err != nil { return 0, err } diff --git a/styleagent/model/dto/body_measurement_dto.go b/styleagent/model/dto/body_measurement_dto.go index 3fcfd21..4473efc 100644 --- a/styleagent/model/dto/body_measurement_dto.go +++ b/styleagent/model/dto/body_measurement_dto.go @@ -9,6 +9,10 @@ type BodyMeasurementSaveReq struct { Height int `json:"height"` Weight int `json:"weight"` SkinTone int `v:"in:1,2,3,4,5" json:"skin_tone"` + Bust int `json:"bust"` + Waist int `json:"waist"` + Hip int `json:"hip"` + Shoulder int `json:"shoulder"` FitParams string `json:"fit_params"` } @@ -20,5 +24,9 @@ type BodyMeasurementGetRes struct { Height int `json:"height"` Weight int `json:"weight"` SkinTone int `json:"skin_tone"` + Bust int `json:"bust"` + Waist int `json:"waist"` + Hip int `json:"hip"` + Shoulder int `json:"shoulder"` FitParams string `json:"fit_params"` } diff --git a/styleagent/model/entity/body_measurement.go b/styleagent/model/entity/body_measurement.go index aada406..d4d5039 100644 --- a/styleagent/model/entity/body_measurement.go +++ b/styleagent/model/entity/body_measurement.go @@ -8,6 +8,10 @@ type BodyMeasurement struct { Height int `orm:"height" json:"height"` Weight int `orm:"weight" json:"weight"` SkinTone int `orm:"skin_tone" json:"skin_tone"` + Bust int `orm:"bust" json:"bust"` + Waist int `orm:"waist" json:"waist"` + Hip int `orm:"hip" json:"hip"` + Shoulder int `orm:"shoulder" json:"shoulder"` FitParams string `orm:"fit_params" json:"fit_params"` UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"` } diff --git a/styleagent/model/entity/wardrobe_item.go b/styleagent/model/entity/wardrobe_item.go index 73c8fe4..bfb63f0 100644 --- a/styleagent/model/entity/wardrobe_item.go +++ b/styleagent/model/entity/wardrobe_item.go @@ -6,6 +6,7 @@ type WardrobeItem struct { Id int64 `orm:"id" json:"id"` UserId int64 `orm:"user_id" json:"user_id"` PhotoUrl string `orm:"photo_url" json:"photo_url"` + Name string `orm:"name" json:"name"` Category string `orm:"category" json:"category"` Season string `orm:"season" json:"season"` StyleTags string `orm:"style_tags" json:"style_tags"` diff --git a/styleagent/service/avatar_model_service.go b/styleagent/service/avatar_model_service.go index d0153a8..d760922 100644 --- a/styleagent/service/avatar_model_service.go +++ b/styleagent/service/avatar_model_service.go @@ -38,10 +38,18 @@ func (s *avatarService) Build(ctx context.Context, userId int64) (*entity.Avatar return nil, errors.New("请先上传三视角全身照(正面/侧面/背面)") } } + bodyMap := map[string]any{"height": 0, "weight": 0, "skin_tone": 0, "bust": 0, "waist": 0, "hip": 0, "shoulder": 0} + if b, err := dao.BodyMeasurement.GetByUser(ctx, userId); err == nil && b != nil { + bodyMap = map[string]any{ + "height": b.Height, "weight": b.Weight, "skin_tone": b.SkinTone, + "bust": b.Bust, "waist": b.Waist, "hip": b.Hip, "shoulder": b.Shoulder, + } + } snapshot := mustJSON(map[string]any{ "photo_front": byType[consts.PhotoTypeFullFront].Id, "photo_side": byType[consts.PhotoTypeFullSide].Id, "photo_back": byType[consts.PhotoTypeFullBack].Id, + "body": bodyMap, }) var record *entity.AvatarModel diff --git a/styleagent/service/body_measurement_service.go b/styleagent/service/body_measurement_service.go index b0760fa..bc645ce 100644 --- a/styleagent/service/body_measurement_service.go +++ b/styleagent/service/body_measurement_service.go @@ -21,6 +21,18 @@ func (s *bodyMeasurementService) Save(ctx context.Context, userId int64, req *en if req.SkinTone == 0 { req.SkinTone = 3 } + if req.Bust == 0 { + req.Bust = 88 + } + if req.Waist == 0 { + req.Waist = 70 + } + if req.Hip == 0 { + req.Hip = 92 + } + if req.Shoulder == 0 { + req.Shoulder = 42 + } req.UserId = userId return dao.BodyMeasurement.Save(ctx, req) } diff --git a/styleagent/service/outfit_generation_task_service.go b/styleagent/service/outfit_generation_task_service.go index 18cd092..4dbb6c1 100644 --- a/styleagent/service/outfit_generation_task_service.go +++ b/styleagent/service/outfit_generation_task_service.go @@ -85,12 +85,19 @@ func runGenerateTask(ctx context.Context, taskId, userId int64, occasion string) return } - // 1. 天气(评分依赖,失败则任务失败) + // 1. 天气(评分依赖;接口不可用时降级默认天气,配置 key 后走真实数据) setTask(consts.TaskStatusPlanning, "") weatherResult, err := GetWeather(ctx, task.Location, task.StartDate, task.EndDate) if err != nil { - fail(err) - return + g.Log().Warningf(ctx, "天气获取失败(%v),使用默认天气继续", err) + weatherResult = &agent.WeatherResult{ + CityCode: task.Location, + Days: []agent.DayWeather{ + {Date: task.StartDate, TempMax: 30, TempMin: 24, TextDay: "晴"}, + }, + AvgTemp: 27, + Season: "夏", + } } weatherJSON, _ := json.Marshal(weatherResult) _ = dao.OutfitGenTask.Update(ctx, taskId, g.Map{"weather_snapshot": string(weatherJSON), "model_name": g.Cfg().MustGet(ctx, "llm.model_name", "").String()}) @@ -122,7 +129,7 @@ func runGenerateTask(ctx context.Context, taskId, userId int64, occasion string) for _, it := range set.Items { candidates = append(candidates, agent.CandidateData{ SetId: int64(si + 1), ItemId: it.Id, Category: it.Category, - Name: it.Category, Color: it.ColorInfo, Season: it.Season, Style: it.StyleTags, + Name: it.Name, Color: it.ColorInfo, Season: it.Season, Style: it.StyleTags, }) } } @@ -133,39 +140,27 @@ func runGenerateTask(ctx context.Context, taskId, userId int64, occasion string) return } - // 5. 规则评分 + // 5. 规则评分(衣橱预筛方案) setTask(consts.TaskStatusScoring, "") - threshold := ScoringRuleService.Threshold(ctx) plans := out.Plans ctxScore := agent.ScoreContext{ TempAvg: weatherResult.AvgTemp, Season: weatherResult.Season, Occasion: occasion, Weekday: weekdayOf(task.StartDate), } - scores := make([]int, len(plans)) - allLow := true - for i, p := range plans { - score := scorePlan(p, items, ctxScore) - scores[i] = score - if score >= threshold { - allLow = false - } - } - // 6. 全低分 → LLM 兜底创作(1 次调用) - if allLow { - g.Log().Infof(ctx, "任务 %d 预筛方案全低分,触发兜底创作", taskId) - wardrobeJSON, _ := json.Marshal(items) - fallbackInput := agent.BuildFallbackUserInput(weatherSummaryText(weatherResult), occasion, string(wardrobeJSON), hairstyles, bodyDesc) - fallback, err := agent.CreateRecommendPlan(ctx, cfg, agent.SystemPromptPlan(), fallbackInput) - if err != nil { - fail(err) - return - } - plans = fallback.Plans - scores = make([]int, len(plans)) - for i, p := range plans { - scores[i] = scorePlan(p, items, ctxScore) - } + // 6. 追加 AI 推荐方案(新品为主,与衣橱方案合并评分落库) + wardrobeJSON, _ := json.Marshal(items) + fallbackInput := agent.BuildFallbackUserInput(weatherSummaryText(weatherResult), occasion, string(wardrobeJSON), hairstyles, bodyDesc) + fallback, err := agent.CreateRecommendPlan(ctx, cfg, agent.SystemPromptPlan(), fallbackInput) + if err != nil { + fail(err) + return + } + plans = append(plans, fallback.Plans...) + + scores := make([]int, len(plans)) + for i, p := range plans { + scores[i] = scorePlan(p, items, ctxScore) } // 7. 落库 plan + items @@ -185,12 +180,14 @@ func runGenerateTask(ctx context.Context, taskId, userId int64, occasion string) } for _, it := range p.Items { source := consts.PlanSourceWardrobe + productName := "" if it.NewItem || it.ItemId == 0 { source = consts.PlanSourceRecommend + productName = it.Name } _, err := dao.PlanOutfitItem.Insert(ctx, &entity.PlanOutfitItem{ PlanId: planId, Slot: it.Slot, Source: source, - WardrobeItemId: it.ItemId, ProductName: "", Name: it.Name, Desc: it.Desc, + WardrobeItemId: it.ItemId, ProductName: productName, Name: it.Name, Desc: it.Desc, }) if err != nil { fail(err) @@ -202,20 +199,24 @@ func runGenerateTask(ctx context.Context, taskId, userId int64, occasion string) g.Log().Infof(ctx, "任务 %d 完成,共 %d 套方案", taskId, len(plans)) } +// scorePlan 分别打分:衣橱单品走规则评分,推荐新品按件数加基础分,合并为方案总分 func scorePlan(p agent.PlanCandidate, items []*entity.WardrobeItem, ctxScore agent.ScoreContext) int { var out agent.CandidateOutfit byId := map[int64]*entity.WardrobeItem{} for _, it := range items { byId[it.Id] = it } + newItems := 0 for _, it := range p.Items { if w := byId[it.ItemId]; w != nil { out.Items = append(out.Items, agent.WardrobeItem{ Category: w.Category, Season: w.Season, ColorInfo: w.ColorInfo, StyleTags: w.StyleTags, }) + } else { + newItems++ } } - return agent.Score(&out, &ctxScore) + return agent.Score(&out, &ctxScore) + newItems*agent.NewItemBaseScore } // ==================== 查询 ====================