Files
2026-09-11 01:41:52 +08:00

207 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"},
}
}