- 用户域:注册/登录(JWT)/修改密码/个人资料 - 照片/衣橱/身形/化身:上传存储 + 3D 化身模板匹配 - 穿搭生成:天气(高德+和风+缓存) → 规则预筛 → LLM 规划(1次调用) → 规则评分(5维100分制) → 全低分触发 LLM 兜底创作 → 异步任务状态机 - 效果图:选主方案后异步生成 3 视角(mock/wanx 供应商 + 内容 hash 缓存 + 每日限额) - 商业化:合作门店列表(seed 4 家) - 冒烟:全链路端到端验证通过(mock LLM/天气),23 个 API 端点 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
72 lines
2.0 KiB
Go
72 lines
2.0 KiB
Go
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// PlanOutput 大模型输出的穿搭方案集合
|
||
type PlanOutput struct {
|
||
Plans []PlanCandidate `json:"plans"`
|
||
}
|
||
|
||
// PlanCandidate 一套穿搭方案
|
||
type PlanCandidate struct {
|
||
Title string `json:"title"`
|
||
Hairstyle string `json:"hairstyle"` // 发型名称(匹配资产库)
|
||
HairColor string `json:"hair_color"` // 如 #A0522D
|
||
Items []PlanItemOut `json:"items"`
|
||
}
|
||
|
||
// PlanItemOut 方案内一件单品
|
||
type PlanItemOut struct {
|
||
Slot string `json:"slot"` // 上衣/下装/鞋/配饰
|
||
ItemId int64 `json:"item_id,omitempty"` // 衣橱条目(wardrobe 来源)
|
||
Name string `json:"name"` // 单品名
|
||
Desc string `json:"desc"` // 搭配说明
|
||
NewItem bool `json:"new_item"` // 是否为推荐新服装
|
||
}
|
||
|
||
// ParsePlanOutput 解析并校验 LLM 输出(去除 markdown 代码围栏后 json.Unmarshal)
|
||
func ParsePlanOutput(raw string) (*PlanOutput, error) {
|
||
text := strings.TrimSpace(raw)
|
||
// 容忍 ```json ... ``` 代码围栏
|
||
if strings.HasPrefix(text, "```") {
|
||
text = strings.TrimPrefix(text, "```")
|
||
if idx := strings.Index(text, "\n"); idx >= 0 {
|
||
text = text[idx+1:]
|
||
}
|
||
text = strings.TrimSuffix(strings.TrimSpace(text), "```")
|
||
}
|
||
var out PlanOutput
|
||
if err := json.Unmarshal([]byte(text), &out); err != nil {
|
||
return nil, fmt.Errorf("方案 JSON 解析失败: %w", err)
|
||
}
|
||
if len(out.Plans) == 0 {
|
||
return nil, fmt.Errorf("方案输出为空(plans 缺失)")
|
||
}
|
||
for i, p := range out.Plans {
|
||
if strings.TrimSpace(p.Title) == "" {
|
||
return nil, fmt.Errorf("方案 %d 缺少 title", i+1)
|
||
}
|
||
if len(p.Items) == 0 {
|
||
return nil, fmt.Errorf("方案 %d 缺少 items", i+1)
|
||
}
|
||
for _, it := range p.Items {
|
||
if !isValidSlot(it.Slot) {
|
||
return nil, fmt.Errorf("方案 %d 含非法 slot: %s", i+1, it.Slot)
|
||
}
|
||
}
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
func isValidSlot(slot string) bool {
|
||
switch slot {
|
||
case "上衣", "下装", "鞋", "配饰":
|
||
return true
|
||
}
|
||
return false
|
||
}
|