207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
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"},
|
||
}
|
||
}
|