package tokenhub import ( "context" "encoding/json" "fmt" "strings" "time" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" ) // FortuneItem 单个星座运势。 type FortuneItem struct { Zodiac string `json:"zodiac"` Overall int8 `json:"overall"` Love int8 `json:"love"` Work int8 `json:"work"` Money int8 `json:"money"` LuckyColor string `json:"lucky_color"` LuckyNumber string `json:"lucky_number"` Description string `json:"description"` } // ToHoroscope 将 LLM 返回的运势项转换为 Horoscope 模型。 func (f FortuneItem) ToHoroscope(statDate int64) *mysqlmodel.Horoscope { return &mysqlmodel.Horoscope{ StatDate: statDate, Zodiac: f.Zodiac, OverallScore: f.Overall, LoveScore: f.Love, CareerScore: f.Work, WealthScore: f.Money, LuckyColor: f.LuckyColor, LuckyNumber: f.LuckyNumber, DailyText: f.Description, } } // FortuneResponse Tokenhub 返回的运势 JSON。 type FortuneResponse struct { Date string `json:"date"` Fortunes []FortuneItem `json:"fortunes"` } // GenerateDailyFortunes 调用 Tokenhub 生成今日十二星座运势。 func (c *Client) GenerateDailyFortunes(ctx context.Context) (*FortuneResponse, error) { today := time.Now().Format("2006-01-02") messages := []Message{ {Role: "user", Content: buildFortunePrompt(today)}, } responseFormat := &ResponseFormat{ Type: "json_schema", JSONSchema: &JSONSchema{ Name: "daily_fortunes", Schema: buildFortuneSchema(), }, } data, err := c.ChatCompletion(ctx, messages, responseFormat) if err != nil { return nil, err } // 解析混元外层响应 var cr chatResponse if err := json.Unmarshal(data, &cr); err != nil { return nil, fmt.Errorf("unmarshal chat response: %w", err) } if len(cr.Choices) == 0 { return nil, fmt.Errorf("no choices in response") } // 解析内层内容。注意:部分模型(如 hy3-preview)不严格遵守 json_schema, // 可能返回中文键名的 map 结构,这里做容错解析。 content := sanitizeJSONContent(cr.Choices[0].Message.Content) result, err := parseFortuneContent(content) if err != nil { return nil, err } // 兜底:模型未返回日期时,使用当天日期 if result.Date == "" { result.Date = time.Now().Format("2006-01-02") } return result, nil } // cnFortuneEntry 兼容模型返回的中文键名结构:{"白羊座":{"综合运势":4,...}}。 type cnFortuneEntry struct { Overall int8 `json:"综合运势"` Love int8 `json:"爱情运势"` Work int8 `json:"工作运势"` Money int8 `json:"财富运势"` LuckyColor string `json:"幸运色"` LuckyNumber string `json:"幸运数字"` Description string `json:"今日运势描述"` } // sanitizeJSONContent 去除可能的 markdown 代码围栏与首尾空白。 func sanitizeJSONContent(s string) string { s = strings.TrimSpace(s) if strings.HasPrefix(s, "```") { if idx := strings.IndexByte(s, '\n'); idx != -1 { s = s[idx+1:] } s = strings.TrimSuffix(strings.TrimSpace(s), "```") } return strings.TrimSpace(s) } // parseFortuneContent 容错解析运势内容,兼容两种结构: // 1. 标准结构 {"date":..,"fortunes":[{"zodiac":..,"overall":..}]} // 2. 中文键名结构 {"白羊座":{"综合运势":..}} func parseFortuneContent(content string) (*FortuneResponse, error) { // 1. 标准结构 var std FortuneResponse if err := json.Unmarshal([]byte(content), &std); err == nil && len(std.Fortunes) > 0 { return &std, nil } // 2. 中文键名 map 结构 var cn map[string]cnFortuneEntry if err := json.Unmarshal([]byte(content), &cn); err == nil && len(cn) > 0 { items := make([]FortuneItem, 0, len(cn)) for zodiac, v := range cn { // 过滤非星座键(无有效运势内容) if v.Description == "" && v.Overall == 0 { continue } items = append(items, FortuneItem{ Zodiac: zodiac, Overall: v.Overall, Love: v.Love, Work: v.Work, Money: v.Money, LuckyColor: v.LuckyColor, LuckyNumber: v.LuckyNumber, Description: v.Description, }) } if len(items) > 0 { return &FortuneResponse{Fortunes: items}, nil } } return nil, fmt.Errorf("no fortunes parsed from content, raw=%s", content) } // buildFortunePrompt 构造运势生成 prompt。 func buildFortunePrompt(today string) string { return fmt.Sprintf(`请为十二星座生成%s的每日运势。 要求: 1. 十二星座运势要有差异,星数分布要合理,不能全部一样 2. 描述要个性化,不要套模板,50 字以内,生动有趣,可结合当前季节和月份特点 3. 整体运势要乐观积极 4. 描述中不要出现星号(*)或 markdown 标记 5. 每个星座给出幸运色(lucky_color)和幸运数字(lucky_number) 必须严格按以下 JSON 结构输出(英文键名,overall/love/work/money 为 1-5 的整数),不要输出任何多余文字或代码围栏: { "date": "%s", "fortunes": [ {"zodiac": "白羊座", "overall": 4, "love": 3, "work": 5, "money": 4, "lucky_color": "红色", "lucky_number": "6", "description": "……"} ] } 其中 fortunes 数组必须恰好包含 12 个星座(白羊座、金牛座、双子座、巨蟹座、狮子座、处女座、天秤座、天蝎座、射手座、摩羯座、水瓶座、双鱼座)。`, today, today) } // buildFortuneSchema 构造 JSON Schema 让混元按结构返回。 func buildFortuneSchema() map[string]interface{} { return map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "date": map[string]interface{}{ "type": "string", "description": "日期,格式 YYYY-MM-DD", }, "fortunes": map[string]interface{}{ "type": "array", "description": "十二星座运势列表,必须恰好 12 条", "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "zodiac": map[string]interface{}{"type": "string", "description": "星座名称,如:白羊座"}, "overall": map[string]interface{}{"type": "integer", "description": "综合运势,1-5 星"}, "love": map[string]interface{}{"type": "integer", "description": "爱情运势,1-5 星"}, "work": map[string]interface{}{"type": "integer", "description": "工作运势,1-5 星"}, "money": map[string]interface{}{"type": "integer", "description": "财富运势,1-5 星"}, "lucky_color": map[string]interface{}{"type": "string", "description": "幸运色"}, "lucky_number": map[string]interface{}{"type": "string", "description": "幸运数字"}, "description": map[string]interface{}{"type": "string", "description": "今日运势描述,50 字以内"}, }, "required": []string{"zodiac", "overall", "love", "work", "money", "lucky_color", "lucky_number", "description"}, }, }, }, "required": []string{"date", "fortunes"}, } }