first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
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"},
}
}