refactor: 工具包并入 agent/(weather/imagegen/avatar/scoring,NewCache 泛化为 NewTTLCache)

This commit is contained in:
2026-07-31 15:05:35 +08:00
parent 11459f7342
commit 033860d571
30 changed files with 213 additions and 144 deletions
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# 路由快照:输出 /api.json 的全部路径(排序去重)
# 用法:bash scripts/routes.sh > /tmp/routes-before.txt
set -e
BASE="${BASE:-http://localhost:3007}"
curl -s "$BASE/api.json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for p in sorted(d.get('paths', {}).keys()):
print(p)
"
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# 全路径冒烟:register/login → token → 依次打全部接口,断言 HTTP 200 + code 符合预期
# 用法:bash scripts/smoke.sh
# 约定:check_code 第三参 = 允许的降级 code 列表(逗号分隔,默认只许 0)
set -e
BASE="${BASE:-http://localhost:3007}"
FAIL=0
say() { echo "[smoke] $*"; }
fail() { echo "[smoke] FAIL: $*"; FAIL=1; }
check_code() {
local name="$1" body="$2" allow="$3"
local code
code=$(echo "$body" | python3 -c "import json,sys; print(json.load(sys.stdin).get('code','?'))" 2>/dev/null || echo "?")
if [ "$code" = "0" ] || echo ",$allow," | grep -q ",$code,"; then
say "OK: $name"
else
fail "$name: unexpected code=$code (allow: $allow) body=$(echo "$body" | head -c 200)"
fi
}
# 1. 注册 + 登录拿 token
USER="smoke_$(date +%s)"
REG=$(curl -s -X POST "$BASE/user/register" -H 'Content-Type: application/json' -d "{\"account\":\"$USER\",\"password\":\"smoketest123\"}")
say "register: $(echo "$REG" | head -c 120)"
LOGIN=$(curl -s -X POST "$BASE/user/login" -H 'Content-Type: application/json' -d "{\"account\":\"$USER\",\"password\":\"smoketest123\"}")
TOKEN=$(echo "$LOGIN" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
if [ -z "$TOKEN" ]; then
fail "login: no token in $(echo "$LOGIN" | head -c 200)"
exit 1
fi
say "login OK, token len=${#TOKEN}"
AUTH="Authorization: Bearer $TOKEN"
# 2. GET 接口(第三参允许的 code:50=未开通/降级/无数据)
for item in \
"GET /user/profile 0" \
"GET /user-photo/list 0" \
"GET /wardrobe/list 0" \
"GET /body-measurement/get 0" \
"GET /avatar/get 0" \
"GET /hairstyle/list 0" \
"GET /outfit/task/status?task_id=0 50" \
"GET /outfit/plan/list 0" \
"GET /partner-store/list 0" \
"GET /member/plan/list 0" \
"GET /member/status 0" ; do
set -- $item
METHOD="$1"; PATH_="$2"; ALLOW="${3:-0}"
RESP=$(curl -s -X "$METHOD" "$BASE$PATH_" -H "$AUTH")
check_code "$PATH_" "$RESP" "$ALLOW"
done
# 3. POST 接口
post_check() {
local name="$1" json="$2" allow="${3:-0}"
local resp
resp=$(curl -s -X POST "$BASE$name" -H "$AUTH" -H 'Content-Type: application/json' -d "$json")
check_code "$name" "$resp" "$allow"
}
post_check "/body-measurement/save" '{"height_cm":175,"weight_kg":65}'
post_check "/outfit/plan/review" '{"plan_id":0,"action":"fav"}' "50"
post_check "/member/order/create" '{"plan_id":1}' "50"
post_check "/ad/reward/claim" '{"ad_type":"effect_extra"}' "50"
post_check "/outfit/generate" '{"start_date":"2026-08-01","end_date":"2026-08-07","location":"上海"}' "50"
post_check "/user/change-password" '{"old_password":"smoketest123","new_password":"smoketest456"}'
# 4. 裸回调(无鉴权;no_order 预期返回 fail
NOTIFY=$(curl -s -X POST "$BASE/member/order/notify" -H 'Content-Type: application/x-www-form-urlencoded' -d 'out_trade_no=nonexist&trade_no=x&amount=0&status=paid')
say "notify(no_order)=$NOTIFY"
# 5. workspace 静态文件
WS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspace/nonexist.png" -H "$AUTH")
say "workspace/404: $WS"
if [ "$FAIL" = "0" ]; then
say "ALL SMOKE PASS"
else
say "SMOKE HAS FAILURES"
exit 1
fi
@@ -1,4 +1,4 @@
package avatar
package agent
import "fmt"
@@ -1,4 +1,4 @@
package avatar
package agent
// FaceFeature 从照片+用户填写提取的化身特征(v1:肤色/身高/体重来自身形参数,照片贴图后续增强)
type FaceFeature struct {
+21
View File
@@ -0,0 +1,21 @@
package agent
import "time"
// 效果图 URL 缓存(key: 方案内容 hash:角度,24h TTL,复用泛型 TTL 缓存)
var effectCache = NewTTLCache(24 * time.Hour)
// CacheGet 读取缓存 URL
func CacheGet(key string) (string, bool) {
v, ok := effectCache.Get(key)
if !ok {
return "", false
}
s, _ := v.(string)
return s, s != ""
}
// CacheSet 写入缓存 URL
func CacheSet(key, url string) {
effectCache.Set(key, url)
}
@@ -1,4 +1,4 @@
package imagegen
package agent
import (
"context"
@@ -1,4 +1,4 @@
package imagegen
package agent
import (
"context"
@@ -1,4 +1,4 @@
package imagegen
package agent
import (
"bytes"
@@ -31,14 +31,14 @@ type wanxInput struct {
type wanxResp struct {
Output struct {
TaskID string `json:"task_id"`
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
Results []struct {
Results []struct {
URL string `json:"url"`
} `json:"results"`
} `json:"output"`
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code"`
Message string `json:"message"`
}
type wanxTaskResp struct {
@@ -55,8 +55,8 @@ type wanxTaskResp struct {
// Generate 提交任务并轮询直到完成,失败返回错误(由上层降级 mock)
func (c *wanxClient) Generate(ctx context.Context, req *GenerateReq) (string, error) {
body, err := json.Marshal(wanxSubmitReq{
Model: c.model,
Input: wanxInput{Prompt: buildPrompt(req.Prompt, "", "", req.Angle), BaseImageURL: req.BaseImageURL},
Model: c.model,
Input: wanxInput{Prompt: buildPrompt(req.Prompt, "", "", req.Angle), BaseImageURL: req.BaseImageURL},
Parameters: map[string]any{"n": 1, "size": "768*1024", "seed": req.Seed},
})
if err != nil {
+2 -2
View File
@@ -10,8 +10,8 @@ import (
// CandidateData 预筛候选服装(供 LLM 选择组合)
type CandidateData struct {
SetId int64 `json:"set_id"` // 所属预筛组合编号
ItemId int64 `json:"item_id"` // 衣橱条目 id
SetId int64 `json:"set_id"` // 所属预筛组合编号
ItemId int64 `json:"item_id"` // 衣橱条目 id
Category string `json:"category"`
Name string `json:"name"`
Color string `json:"color"`
+5 -5
View File
@@ -21,11 +21,11 @@ type PlanCandidate struct {
// 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"` // 是否为推荐新服装
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
@@ -1,4 +1,4 @@
package scoring
package agent
import (
"strconv"
@@ -1,4 +1,4 @@
package scoring
package agent
// completenessScore 层次完整度(20 分制):上衣+5 下装+5 鞋+5 配饰+5
func completenessScore(o CandidateOutfit) int {
@@ -1,4 +1,4 @@
package scoring
package agent
// Score 总分(100 分制)
func Score(c *CandidateOutfit, ctx *ScoreContext) int {
@@ -1,4 +1,4 @@
package scoring
package agent
import "strings"
@@ -1,4 +1,4 @@
package scoring
package agent
// WardrobeItem 评分用服装条目(从衣橱 entity 转换)
type WardrobeItem struct {
@@ -1,4 +1,4 @@
package scoring
package agent
// weatherScore 天气适宜度(25 分制)
// 温度匹配每件服装季节 +5;<10℃ 无外套 -10>30℃ 有外套 -8
@@ -1,12 +1,13 @@
package weather
package agent
import (
"sync"
"time"
)
// 通用 TTL 缓存:天气结果(*WeatherResult)、CPS 转链(string)等均可复用
type cacheEntry struct {
data *WeatherResult
data any
expiresAt time.Time
}
@@ -16,11 +17,11 @@ type Cache struct {
items map[string]cacheEntry
}
func NewCache(ttl time.Duration) *Cache {
func NewTTLCache(ttl time.Duration) *Cache {
return &Cache{ttl: ttl, items: make(map[string]cacheEntry)}
}
func (c *Cache) Get(key string) (*WeatherResult, bool) {
func (c *Cache) Get(key string) (any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
@@ -34,7 +35,7 @@ func (c *Cache) Get(key string) (*WeatherResult, bool) {
return e.data, true
}
func (c *Cache) Set(key string, data *WeatherResult) {
func (c *Cache) Set(key string, data any) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = cacheEntry{data: data, expiresAt: time.Now().Add(c.ttl)}
@@ -1,4 +1,4 @@
package weather
package agent
import (
"context"
@@ -13,7 +13,7 @@ import (
)
type amapResp struct {
Status string `json:"status"`
Status string `json:"status"`
Geocodes []struct {
Adcode string `json:"adcode"`
} `json:"geocodes"`
@@ -1,4 +1,4 @@
package weather
package agent
import (
"context"
@@ -20,7 +20,7 @@ type DayWeather struct {
}
type WeatherResult struct {
CityCode string `json:"city_code"`
CityCode string `json:"city_code"`
Days []DayWeather `json:"days"`
// AvgTemp 日期范围平均温度(评分用)
AvgTemp int `json:"avg_temp"`
+4 -1
View File
@@ -37,7 +37,10 @@ func seedHairstyles(ctx context.Context) {
return
}
_ = cnt
items := []struct{ name, tag, face string; sort int }{
items := []struct {
name, tag, face string
sort int
}{
{"清爽短发", "清爽", "all", 1},
{"中分微卷", "温婉", "all", 2},
{"披肩长发", "优雅", "all", 3},
-49
View File
@@ -1,49 +0,0 @@
package imagegen
import (
"sync"
"time"
)
// cache 效果图 URL 缓存(key: 方案内容 hash:角度,24h TTL
type cache struct {
mu sync.Mutex
items map[string]cacheEntry
}
type cacheEntry struct {
url string
expiresAt time.Time
}
var effectCache = &cache{items: make(map[string]cacheEntry)}
// CacheGet 读取缓存 URL
func CacheGet(key string) (string, bool) {
return cacheGet(key)
}
// CacheSet 写入缓存 URL
func CacheSet(key, url string) {
cacheSet(key, url)
}
func cacheGet(key string) (string, bool) {
effectCache.mu.Lock()
defer effectCache.mu.Unlock()
e, ok := effectCache.items[key]
if !ok {
return "", false
}
if time.Now().After(e.expiresAt) {
delete(effectCache.items, key)
return "", false
}
return e.url, true
}
func cacheSet(key, url string) {
effectCache.mu.Lock()
defer effectCache.mu.Unlock()
effectCache.items[key] = cacheEntry{url: url, expiresAt: time.Now().Add(24 * time.Hour)}
}
+12 -12
View File
@@ -3,16 +3,16 @@ package entity
import "github.com/gogf/gf/v2/os/gtime"
type AvatarModel struct {
Id int64 `orm:"id" json:"id"`
UserId int64 `orm:"user_id" json:"user_id"`
FaceTemplateId int `orm:"face_template_id" json:"face_template_id"`
BodyTemplateId int `orm:"body_template_id" json:"body_template_id"`
SkinToneIndex int `orm:"skin_tone_index" json:"skin_tone_index"`
FaceTextureUrl string `orm:"face_texture_url" json:"face_texture_url"`
GlbUrl string `orm:"glb_url" json:"glb_url"`
BuildStatus string `orm:"build_status" json:"build_status"`
Error string `orm:"error" json:"error"`
ParamsSnapshot string `orm:"params_snapshot" json:"params_snapshot"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"`
Id int64 `orm:"id" json:"id"`
UserId int64 `orm:"user_id" json:"user_id"`
FaceTemplateId int `orm:"face_template_id" json:"face_template_id"`
BodyTemplateId int `orm:"body_template_id" json:"body_template_id"`
SkinToneIndex int `orm:"skin_tone_index" json:"skin_tone_index"`
FaceTextureUrl string `orm:"face_texture_url" json:"face_texture_url"`
GlbUrl string `orm:"glb_url" json:"glb_url"`
BuildStatus string `orm:"build_status" json:"build_status"`
Error string `orm:"error" json:"error"`
ParamsSnapshot string `orm:"params_snapshot" json:"params_snapshot"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"`
}
+8 -8
View File
@@ -3,12 +3,12 @@ package entity
import "github.com/gogf/gf/v2/os/gtime"
type HairstyleAsset struct {
Id int64 `orm:"id" json:"id"`
Name string `orm:"name" json:"name"`
StyleTag string `orm:"style_tag" json:"style_tag"`
GlbUrl string `orm:"glb_url" json:"glb_url"`
ThumbUrl string `orm:"thumb_url" json:"thumb_url"`
ApplicableFace string `orm:"applicable_face" json:"applicable_face"`
Sort int `orm:"sort" json:"sort"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
Id int64 `orm:"id" json:"id"`
Name string `orm:"name" json:"name"`
StyleTag string `orm:"style_tag" json:"style_tag"`
GlbUrl string `orm:"glb_url" json:"glb_url"`
ThumbUrl string `orm:"thumb_url" json:"thumb_url"`
ApplicableFace string `orm:"applicable_face" json:"applicable_face"`
Sort int `orm:"sort" json:"sort"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
}
+9 -9
View File
@@ -3,13 +3,13 @@ package entity
import "github.com/gogf/gf/v2/os/gtime"
type PlanOutfitItem struct {
Id int64 `orm:"id" json:"id"`
PlanId int64 `orm:"plan_id" json:"plan_id"`
Slot string `orm:"slot" json:"slot"`
Source string `orm:"source" json:"source"`
WardrobeItemId int64 `orm:"wardrobe_item_id" json:"wardrobe_item_id"`
ProductName string `orm:"product_name" json:"product_name"`
Name string `orm:"name" json:"name"`
Desc string `orm:"desc" json:"desc"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
Id int64 `orm:"id" json:"id"`
PlanId int64 `orm:"plan_id" json:"plan_id"`
Slot string `orm:"slot" json:"slot"`
Source string `orm:"source" json:"source"`
WardrobeItemId int64 `orm:"wardrobe_item_id" json:"wardrobe_item_id"`
ProductName string `orm:"product_name" json:"product_name"`
Name string `orm:"name" json:"name"`
Desc string `orm:"desc" json:"desc"`
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
}
+1 -1
View File
@@ -67,7 +67,7 @@ func CreateOrder(ctx context.Context, orderNo string, amountFen int) (payURL str
Url string `json:"url"`
}
// 注意:Post 的最后一个参数不会自动解析响应体,需手动读取后反序列化
respRaw, err := g.Client().SetTimeout(10 * time.Second).Post(context.Background(), cfg.ApiBase+"/payment/do.html", params)
respRaw, err := g.Client().SetTimeout(10*time.Second).Post(context.Background(), cfg.ApiBase+"/payment/do.html", params)
if err != nil {
return "", fmt.Errorf("虎皮棋下单失败: %w", err)
}
+7 -7
View File
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"slogan-agent/styleagent/avatar"
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/entity"
@@ -41,18 +41,18 @@ func (s *avatarService) Build(ctx context.Context, userId int64) (*entity.Avatar
if err != nil {
return nil, err
}
feature := &avatar.FaceFeature{SkinTone: 3, HeightCm: 170, WeightKg: 60}
feature := &agent.FaceFeature{SkinTone: 3, HeightCm: 170, WeightKg: 60}
if bm != nil {
feature = &avatar.FaceFeature{SkinTone: bm.SkinTone, HeightCm: bm.Height, WeightKg: bm.Weight}
feature = &agent.FaceFeature{SkinTone: bm.SkinTone, HeightCm: bm.Height, WeightKg: bm.Weight}
}
faceId, bodyId, skinIdx := avatar.MatchTemplates(feature)
faceId, bodyId, skinIdx := agent.MatchTemplates(feature)
// 已有化身则重建(更新模板索引),否则新建
existing, _ := dao.AvatarModel.GetByUser(ctx, userId)
if existing != nil {
err := dao.AvatarModel.Update(ctx, existing.Id, map[string]any{
"face_template_id": faceId, "body_template_id": bodyId,
"skin_tone_index": skinIdx, "glb_url": avatar.PackGlbUrl(faceId, bodyId, skinIdx),
"skin_tone_index": skinIdx, "glb_url": agent.PackGlbUrl(faceId, bodyId, skinIdx),
"build_status": consts.AvatarBuildDone, "error": "",
"params_snapshot": mustJSON(map[string]any{
"height_cm": feature.HeightCm, "weight_kg": feature.WeightKg, "skin_tone": skinIdx,
@@ -65,7 +65,7 @@ func (s *avatarService) Build(ctx context.Context, userId int64) (*entity.Avatar
existing.FaceTemplateId = faceId
existing.BodyTemplateId = bodyId
existing.SkinToneIndex = skinIdx
existing.GlbUrl = avatar.PackGlbUrl(faceId, bodyId, skinIdx)
existing.GlbUrl = agent.PackGlbUrl(faceId, bodyId, skinIdx)
existing.BuildStatus = consts.AvatarBuildDone
return existing, nil
}
@@ -75,7 +75,7 @@ func (s *avatarService) Build(ctx context.Context, userId int64) (*entity.Avatar
FaceTemplateId: faceId,
BodyTemplateId: bodyId,
SkinToneIndex: skinIdx,
GlbUrl: avatar.PackGlbUrl(faceId, bodyId, skinIdx),
GlbUrl: agent.PackGlbUrl(faceId, bodyId, skinIdx),
BuildStatus: consts.AvatarBuildDone,
ParamsSnapshot: mustJSON(map[string]any{
"height_cm": feature.HeightCm, "weight_kg": feature.WeightKg, "skin_tone": skinIdx,
+5 -5
View File
@@ -8,9 +8,9 @@ import (
"fmt"
"strings"
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/consts"
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/imagegen"
"slogan-agent/styleagent/model/entity"
"github.com/gogf/gf/v2/frame/g"
@@ -63,10 +63,10 @@ func (s *effectImageService) run(ctx context.Context, planId, userId int64) {
}
}
client := imagegen.NewClient(g.Cfg().MustGet(ctx, "imagegen.supplier", "mock").String())
client := agent.NewClient(g.Cfg().MustGet(ctx, "agent.supplier", "mock").String())
for i, angle := range effectAngles {
cacheKey := effectCacheKey(plan, angle)
if url, ok := imagegen.CacheGet(cacheKey); ok {
if url, ok := agent.CacheGet(cacheKey); ok {
_, _ = dao.PlanEffectImage.Insert(ctx, &entity.PlanEffectImage{
PlanId: planId, Angle: angle, Url: url, Status: consts.EffectStatusDone,
PromptSnapshot: planDesc,
@@ -80,7 +80,7 @@ func (s *effectImageService) run(ctx context.Context, planId, userId int64) {
if err != nil {
continue
}
url, err := client.Generate(ctx, &imagegen.GenerateReq{
url, err := client.Generate(ctx, &agent.GenerateReq{
BaseImageURL: baseImageURL, Prompt: planDesc, Angle: angle, Seed: plan.Id*100 + int64(i),
})
if err != nil {
@@ -88,7 +88,7 @@ func (s *effectImageService) run(ctx context.Context, planId, userId int64) {
_ = dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusFailed, "")
continue
}
imagegen.CacheSet(cacheKey, url)
agent.CacheSet(cacheKey, url)
_ = dao.PlanEffectImage.UpdateStatus(ctx, recId, consts.EffectStatusDone, url)
}
g.Log().Infof(ctx, "方案 %d 效果图生成完成", planId)
+4 -4
View File
@@ -1,8 +1,8 @@
package service
import (
"slogan-agent/styleagent/agent"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/scoring"
)
// candidateSet 一套预筛组合
@@ -55,10 +55,10 @@ func isOuterwear(it *entity.WardrobeItem) bool {
}
// toScoringOutfit 转评分用候选
func toScoringOutfit(set candidateSet) scoring.CandidateOutfit {
o := scoring.CandidateOutfit{HasOuterwear: set.HasOuterwear}
func toScoringOutfit(set candidateSet) agent.CandidateOutfit {
o := agent.CandidateOutfit{HasOuterwear: set.HasOuterwear}
for _, it := range set.Items {
o.Items = append(o.Items, scoring.WardrobeItem{
o.Items = append(o.Items, agent.WardrobeItem{
Category: it.Category,
Season: it.Season,
ColorInfo: it.ColorInfo,
+6 -8
View File
@@ -12,8 +12,6 @@ import (
"slogan-agent/styleagent/dao"
"slogan-agent/styleagent/model/dto"
"slogan-agent/styleagent/model/entity"
"slogan-agent/styleagent/scoring"
"slogan-agent/styleagent/weather"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
@@ -132,7 +130,7 @@ func runGenerateTask(ctx context.Context, taskId, userId int64) {
setTask(consts.TaskStatusScoring, "")
threshold := scoringThreshold(ctx)
plans := out.Plans
ctxScore := scoring.ScoreContext{
ctxScore := agent.ScoreContext{
TempAvg: weatherResult.AvgTemp, Season: weatherResult.Season,
Occasion: occasion, Weekday: weekdayOf(task.StartDate),
}
@@ -197,20 +195,20 @@ func runGenerateTask(ctx context.Context, taskId, userId int64) {
g.Log().Infof(ctx, "任务 %d 完成,共 %d 套方案", taskId, len(plans))
}
func scorePlan(p agent.PlanCandidate, items []*entity.WardrobeItem, ctxScore scoring.ScoreContext) int {
var out scoring.CandidateOutfit
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
}
for _, it := range p.Items {
if w := byId[it.ItemId]; w != nil {
out.Items = append(out.Items, scoring.WardrobeItem{
out.Items = append(out.Items, agent.WardrobeItem{
Category: w.Category, Season: w.Season, ColorInfo: w.ColorInfo, StyleTags: w.StyleTags,
})
}
}
return scoring.Score(&out, &ctxScore)
return agent.Score(&out, &ctxScore)
}
// ==================== 查询/操作 ====================
@@ -319,7 +317,7 @@ func bodyDescText(ctx context.Context, userId int64) string {
return fmt.Sprintf("身高 %dcm,体重 %dkg,肤色 %d 档", bm.Height, bm.Weight, bm.SkinTone)
}
func weatherSummaryText(w *weather.WeatherResult) string {
func weatherSummaryText(w *agent.WeatherResult) string {
return fmt.Sprintf("%s%s),平均 %d℃,%d 天", w.CityCode, w.Season, w.AvgTemp, len(w.Days))
}
+9 -7
View File
@@ -5,22 +5,24 @@ import (
"fmt"
"time"
"slogan-agent/styleagent/weather"
"slogan-agent/styleagent/agent"
)
var weatherCache = weather.NewCache(6 * time.Hour)
var weatherCache = agent.NewTTLCache(6 * time.Hour)
// GetWeather 地点 + 日期范围 → 天气结果(高德地理编码 + 和风 7 天预报,缓存 6 小时)
func GetWeather(ctx context.Context, location, startDate, endDate string) (*weather.WeatherResult, error) {
cityCode, err := weather.GetCityCode(ctx, location)
func GetWeather(ctx context.Context, location, startDate, endDate string) (*agent.WeatherResult, error) {
cityCode, err := agent.GetCityCode(ctx, location)
if err != nil {
return nil, err
}
cacheKey := fmt.Sprintf("%s:%s:%s", cityCode, startDate, endDate)
if result, ok := weatherCache.Get(cacheKey); ok {
return result, nil
if v, ok := weatherCache.Get(cacheKey); ok {
if result, ok := v.(*agent.WeatherResult); ok {
return result, nil
}
}
result, err := weather.GetDaily(ctx, cityCode, startDate, endDate)
result, err := agent.GetDaily(ctx, cityCode, startDate, endDate)
if err != nil {
return nil, err
}