fix: 移除早间简报功能 + 修复多处"人机感"用语
- 删除 briefing_handler.go / briefing_store.go 及所有相关路由与配置 - 移除 Gateway Config 中 ToolEngineURL / BriefingTime 字段 - 移除 DevTools 中 gateway 的 TOOL_ENGINE_URL 环境变量 - webhook 错误提示从"AI 服务异常/暂不可用"改为昔涟自然口吻 - markdown 导出中昔涟头像从 🤖 改为 💫 - 后台思考提示词"系统会误解析"改为"我会误解析" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -791,7 +791,7 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
|
||||
1. 反思部分用第三人称或自言自语的方式,不要直接对开拓者喊话。
|
||||
2. 只有开拓者状态正常且真的有必要时,才在独立一行写【主动消息】标记,后面跟你要发给他的话。不要硬找话题。
|
||||
3. 【主动消息】标记必须独占一行开头,内容直接对开拓者说话(用"你"称呼他),像主动找他聊天一样。
|
||||
4. 如果你在反思中提到"主动消息"这个词但不打算发消息,不要使用【主动消息】这个带括号的标记——系统会误解析。
|
||||
4. 如果你在反思中提到"主动消息"这个词但不打算发消息,不要使用【主动消息】这个带括号的标记——我会误解析。
|
||||
5. 2-4句话即可。`
|
||||
|
||||
case "silence":
|
||||
|
||||
@@ -40,7 +40,6 @@ func main() {
|
||||
// 初始化数据库持久化存储 (降级:连接失败不崩溃)
|
||||
var sessionStore *store.SessionStore
|
||||
var reminderStore *store.ReminderStore
|
||||
var briefingStore *store.BriefingStore
|
||||
var automationStore *store.AutomationStore
|
||||
var fileStore *store.FileStore
|
||||
var knowledgeStore *store.KnowledgeStore
|
||||
@@ -103,13 +102,6 @@ func main() {
|
||||
logger.Println("✅ 提醒持久化存储已启用 (PostgreSQL)")
|
||||
}
|
||||
|
||||
// 初始化简报存储(复用同一数据库连接)
|
||||
if bs, err := store.NewBriefingStore(s.DB()); err != nil {
|
||||
logger.Printf("⚠ 简报存储初始化失败: %v", err)
|
||||
} else {
|
||||
briefingStore = bs
|
||||
logger.Println("✅ 简报持久化存储已启用 (PostgreSQL)")
|
||||
}
|
||||
|
||||
// 初始化自动化存储(复用同一数据库连接)
|
||||
if as, err := store.NewAutomationStore(s.DB()); err != nil {
|
||||
@@ -194,19 +186,13 @@ func main() {
|
||||
logger.Println("[INFO] 思考调度配置文件已加载 (thinking_schedule.json)")
|
||||
}
|
||||
|
||||
router.Setup(r, hub, cfg, sessionStore, reminderStore, briefingStore, automationStore, fileStore, ruleEngine, knowledgeStore, nil, db, modelConfigStore, thinkingScheduleStore)
|
||||
router.Setup(r, hub, cfg, sessionStore, reminderStore, automationStore, fileStore, ruleEngine, knowledgeStore, nil, db, modelConfigStore, thinkingScheduleStore)
|
||||
|
||||
// 启动提醒调度器
|
||||
if reminderStore != nil {
|
||||
handler.StartReminderScheduler(reminderStore, hub)
|
||||
}
|
||||
|
||||
// 启动简报调度器
|
||||
if briefingStore != nil && reminderStore != nil {
|
||||
briefingHandler := handler.NewBriefingHandler(cfg, hub, briefingStore, reminderStore)
|
||||
handler.StartBriefingScheduler(briefingHandler, briefingStore, cfg.BriefingTime)
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
|
||||
@@ -50,9 +50,6 @@ type Config struct {
|
||||
// Voice 语音识别服务
|
||||
VoiceServiceURL string
|
||||
|
||||
// Tool-Engine 工具引擎服务
|
||||
ToolEngineURL string
|
||||
|
||||
// LLM (透传给AI-Core,Gateway可能也需要)
|
||||
LLMAPIURL string
|
||||
LLMAPIKey string
|
||||
@@ -72,9 +69,6 @@ type Config struct {
|
||||
|
||||
// CORS 允许的 Origin 白名单
|
||||
AllowedOrigins []string
|
||||
|
||||
// 每日简报时间 (HH:MM 格式)
|
||||
BriefingTime string
|
||||
}
|
||||
|
||||
// Load 从环境变量加载配置
|
||||
@@ -129,8 +123,6 @@ func Load() *Config {
|
||||
|
||||
VoiceServiceURL: getEnv("VOICE_SERVICE_URL", "http://localhost:8093"),
|
||||
|
||||
ToolEngineURL: getEnv("TOOL_ENGINE_URL", "http://localhost:8092"),
|
||||
|
||||
LLMAPIURL: getEnv("LLM_API_URL", "https://api.openai.com/v1"),
|
||||
LLMAPIKey: getEnv("LLM_API_KEY", ""),
|
||||
LLMModel: getEnv("LLM_MODEL", "gpt-4o"),
|
||||
@@ -143,7 +135,6 @@ func Load() *Config {
|
||||
|
||||
AllowedOrigins: parseAllowedOrigins(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:5199,http://localhost:3000")),
|
||||
|
||||
BriefingTime: getEnv("BRIEFING_TIME", "08:00"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,713 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"github.com/yourname/cyrene-ai/pkg/logger"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/config"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/store"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/ws"
|
||||
)
|
||||
|
||||
// BriefingHandler 每日简报处理器
|
||||
type BriefingHandler struct {
|
||||
cfg *config.Config
|
||||
hub *ws.Hub
|
||||
briefingStore *store.BriefingStore
|
||||
reminderStore *store.ReminderStore
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewBriefingHandler 创建简报处理器
|
||||
func NewBriefingHandler(cfg *config.Config, hub *ws.Hub, bs *store.BriefingStore, rs *store.ReminderStore) *BriefingHandler {
|
||||
return &BriefingHandler{
|
||||
cfg: cfg,
|
||||
hub: hub,
|
||||
briefingStore: bs,
|
||||
reminderStore: rs,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateBriefingRequest 手动生成简报请求体
|
||||
type GenerateBriefingRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// GetBriefing 获取指定日期简报
|
||||
// GET /api/v1/briefings?user_id=xxx&date=2024-01-01
|
||||
func (h *BriefingHandler) GetBriefing(c *gin.Context) {
|
||||
authUserID := middleware.GetUserID(c)
|
||||
userID := c.Query("user_id")
|
||||
date := c.Query("date")
|
||||
|
||||
if authUserID != "admin" || userID == "" {
|
||||
userID = authUserID
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少user_id参数"})
|
||||
return
|
||||
}
|
||||
if date == "" {
|
||||
date = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
briefing, err := h.briefingStore.GetBriefingByDate(userID, date)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 查询简报失败: user=%s date=%s err=%v", userID, date, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询简报失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if briefing == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"briefing": nil,
|
||||
"message": "当日简报尚未生成",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"briefing": briefing})
|
||||
}
|
||||
|
||||
// GetLatestBriefings 获取最近简报列表
|
||||
// GET /api/v1/briefings/latest?user_id=xxx&limit=7
|
||||
func (h *BriefingHandler) GetLatestBriefings(c *gin.Context) {
|
||||
authUserID := middleware.GetUserID(c)
|
||||
userID := c.Query("user_id")
|
||||
|
||||
if authUserID != "admin" || userID == "" {
|
||||
userID = authUserID
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少user_id参数"})
|
||||
return
|
||||
}
|
||||
|
||||
limit := 7
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed, err := parseInt(l); err == nil && parsed > 0 && parsed <= 30 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
briefings, err := h.briefingStore.GetLatestBriefings(userID, limit)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 查询简报列表失败: user=%s err=%v", userID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询简报列表失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"briefings": briefings,
|
||||
"total": len(briefings),
|
||||
})
|
||||
}
|
||||
|
||||
// Generate 手动触发生成今日简报
|
||||
// POST /api/v1/briefings/generate
|
||||
func (h *BriefingHandler) Generate(c *gin.Context) {
|
||||
authUserID := middleware.GetUserID(c)
|
||||
var req GenerateBriefingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 非管理员只能为自己生成
|
||||
if authUserID != "admin" {
|
||||
req.UserID = authUserID
|
||||
}
|
||||
|
||||
if req.UserID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少user_id"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.GenerateDailyBriefing(req.UserID)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 生成简报失败: user=%s err=%v", req.UserID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "生成简报失败: " + err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成后推送通知
|
||||
h.pushBriefingNotification(req.UserID, result)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"briefing": result,
|
||||
"message": "简报已生成并推送",
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateDailyBriefing 生成每日简报(核心逻辑)
|
||||
func (h *BriefingHandler) GenerateDailyBriefing(userID string) (*store.Briefing, error) {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
|
||||
briefing := &store.Briefing{
|
||||
ID: "brief_" + generateID(),
|
||||
UserID: userID,
|
||||
Date: today,
|
||||
Status: "pending",
|
||||
Weather: &store.WeatherData{},
|
||||
News: []store.NewsItem{},
|
||||
Reminders: []store.BriefReminder{},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 1. 获取天气数据
|
||||
logger.Printf("[briefing] 获取天气数据...")
|
||||
weather, err := h.fetchWeather("Shanghai")
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 天气获取失败 (降级): %v", err)
|
||||
weather = &store.WeatherData{
|
||||
Location: "未知",
|
||||
Temp: 0,
|
||||
Condition: "获取天气失败",
|
||||
Icon: "❓",
|
||||
}
|
||||
}
|
||||
briefing.Weather = weather
|
||||
logger.Printf("[briefing] 天气: %s %.1f°C %s", weather.Location, weather.Temp, weather.Condition)
|
||||
|
||||
// 2. 获取今日待办提醒
|
||||
logger.Printf("[briefing] 获取待办提醒...")
|
||||
reminders, err := h.reminderStore.GetRemindersByUser(userID, "pending", 10, 0)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 获取提醒失败: %v", err)
|
||||
} else {
|
||||
now := time.Now()
|
||||
endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
||||
for _, r := range reminders {
|
||||
if r.RemindAt.Before(endOfDay) || r.RemindAt.Equal(endOfDay) {
|
||||
briefing.Reminders = append(briefing.Reminders, store.BriefReminder{
|
||||
ID: r.ID,
|
||||
Title: r.Title,
|
||||
RemindAt: r.RemindAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Printf("[briefing] 今日待办: %d 项", len(briefing.Reminders))
|
||||
|
||||
// 3. 获取新闻摘要(通过 tool-engine web_search)
|
||||
logger.Printf("[briefing] 获取新闻摘要...")
|
||||
news, err := h.fetchNews()
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 新闻获取失败 (降级): %v", err)
|
||||
}
|
||||
briefing.News = news
|
||||
logger.Printf("[briefing] 新闻: %d 条", len(news))
|
||||
|
||||
// 4. 生成 AI 摘要
|
||||
logger.Printf("[briefing] 生成 AI 摘要...")
|
||||
summary, err := h.generateAISummary(briefing)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] AI 摘要生成失败 (降级): %v", err)
|
||||
summary = h.buildFallbackSummary(briefing)
|
||||
briefing.SummarySource = "fallback"
|
||||
} else {
|
||||
briefing.SummarySource = "ai"
|
||||
}
|
||||
briefing.Summary = summary
|
||||
|
||||
// 5. 标记为已生成
|
||||
now := time.Now()
|
||||
briefing.Status = "generated"
|
||||
briefing.GeneratedAt = &now
|
||||
|
||||
// 6. 持久化
|
||||
if err := h.briefingStore.CreateOrUpdateBriefing(briefing); err != nil {
|
||||
return nil, fmt.Errorf("保存简报失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Printf("[briefing] 简报已生成: user=%s date=%s", userID, today)
|
||||
return briefing, nil
|
||||
}
|
||||
|
||||
// fetchWeather 通过 wttr.in API 获取天气数据
|
||||
func (h *BriefingHandler) fetchWeather(location string) (*store.WeatherData, error) {
|
||||
url := fmt.Sprintf("https://wttr.in/%s?format=j1", location)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建天气请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Cyrene-AI/1.0")
|
||||
|
||||
resp, err := h.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求天气API失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("天气API返回状态码 %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取天气响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析 wttr.in JSON 响应
|
||||
var wttrResp struct {
|
||||
CurrentCondition []struct {
|
||||
TempC string `json:"temp_C"`
|
||||
WeatherDesc []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"weatherDesc"`
|
||||
WeatherIconURL []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"weatherIconUrl"`
|
||||
} `json:"current_condition"`
|
||||
NearestArea []struct {
|
||||
AreaName []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"areaName"`
|
||||
} `json:"nearest_area"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &wttrResp); err != nil {
|
||||
return nil, fmt.Errorf("解析天气数据失败: %w", err)
|
||||
}
|
||||
|
||||
wd := &store.WeatherData{
|
||||
Location: location,
|
||||
}
|
||||
|
||||
if len(wttrResp.NearestArea) > 0 && len(wttrResp.NearestArea[0].AreaName) > 0 {
|
||||
wd.Location = wttrResp.NearestArea[0].AreaName[0].Value
|
||||
}
|
||||
|
||||
if len(wttrResp.CurrentCondition) > 0 {
|
||||
cc := wttrResp.CurrentCondition[0]
|
||||
wd.Temp = parseFloat(cc.TempC)
|
||||
|
||||
if len(cc.WeatherDesc) > 0 {
|
||||
wd.Condition = cc.WeatherDesc[0].Value
|
||||
}
|
||||
|
||||
// 根据天气描述转 emoji
|
||||
wd.Icon = weatherEmoji(wd.Condition)
|
||||
}
|
||||
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// fetchNews 通过 tool-engine web_search 搜索今日新闻
|
||||
func (h *BriefingHandler) fetchNews() ([]store.NewsItem, error) {
|
||||
if h.cfg.ToolEngineURL == "" {
|
||||
return nil, fmt.Errorf("ToolEngine URL 未配置")
|
||||
}
|
||||
|
||||
today := time.Now().Format("2006年01月02日")
|
||||
query := fmt.Sprintf("%s 今日要闻 热点新闻", today)
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"arguments": map[string]interface{}{
|
||||
"query": query,
|
||||
"limit": 5,
|
||||
},
|
||||
})
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/tools/web_search/execute", h.cfg.ToolEngineURL)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建新闻搜索请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := h.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求新闻搜索失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取新闻搜索结果失败: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("新闻搜索返回状态码 %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 解析 tool-engine 单个工具执行响应: {id, output, error?}
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析新闻搜索结果失败: %w", err)
|
||||
}
|
||||
|
||||
if result.Error != "" {
|
||||
logger.Printf("[briefing] 新闻搜索失败: %s", result.Error)
|
||||
// 返回降级新闻
|
||||
return []store.NewsItem{
|
||||
{
|
||||
Title: "未能获取今日新闻",
|
||||
URL: "",
|
||||
Source: "系统",
|
||||
Summary: "新闻搜索服务暂时不可用,请稍后再试。",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var news []store.NewsItem
|
||||
|
||||
// 尝试解析搜索结果为结构化数据
|
||||
var searchResults []struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Snippet string `json:"snippet"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(result.Output), &searchResults); err != nil {
|
||||
// 如果不是 JSON 数组,当做纯文本处理
|
||||
news = append(news, store.NewsItem{
|
||||
Title: "今日新闻",
|
||||
URL: "",
|
||||
Source: "搜索引擎",
|
||||
Summary: truncateStr(result.Output, 200),
|
||||
})
|
||||
} else {
|
||||
for _, sr := range searchResults {
|
||||
news = append(news, store.NewsItem{
|
||||
Title: sr.Title,
|
||||
URL: sr.URL,
|
||||
Source: sr.Source,
|
||||
Summary: sr.Snippet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(news) == 0 {
|
||||
news = []store.NewsItem{
|
||||
{
|
||||
Title: "未能获取今日新闻",
|
||||
URL: "",
|
||||
Source: "系统",
|
||||
Summary: "新闻搜索服务暂时不可用,请稍后再试。",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 限制最多 5 条
|
||||
if len(news) > 5 {
|
||||
news = news[:5]
|
||||
}
|
||||
|
||||
return news, nil
|
||||
}
|
||||
|
||||
// generateAISummary 通过 AI-Core 生成人性化摘要
|
||||
func (h *BriefingHandler) generateAISummary(b *store.Briefing) (string, error) {
|
||||
if h.cfg.AICoreURL == "" {
|
||||
return "", fmt.Errorf("AI-Core URL 未配置")
|
||||
}
|
||||
|
||||
// 构建提示词
|
||||
prompt := h.buildSummaryPrompt(b)
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是昔涟,一个温柔贴心的AI助手。请用温暖、亲切的语气回复,像朋友一样关心用户。回复使用中文。",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": 500,
|
||||
"temperature": 0.7,
|
||||
})
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/chat/completions", h.cfg.AICoreURL)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("构建 AI 请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := h.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("请求 AI-Core 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取 AI 响应失败: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("AI-Core 返回状态码 %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 解析 OpenAI 兼容响应
|
||||
var aiResp struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &aiResp); err != nil {
|
||||
return "", fmt.Errorf("解析 AI 响应失败: %w", err)
|
||||
}
|
||||
|
||||
if len(aiResp.Choices) == 0 {
|
||||
return "", fmt.Errorf("AI 返回空响应")
|
||||
}
|
||||
|
||||
return aiResp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
// buildSummaryPrompt 构建 AI 摘要提示词
|
||||
func (h *BriefingHandler) buildSummaryPrompt(b *store.Briefing) string {
|
||||
var sb strings.Builder
|
||||
today := time.Now().Format("2006年01月02日")
|
||||
|
||||
sb.WriteString(fmt.Sprintf("今天是%s。请根据以下信息,用昔涟温柔的语气为用户生成一份简短的每日简报(控制在200字以内):\n\n", today))
|
||||
|
||||
// 天气
|
||||
if b.Weather != nil && b.Weather.Condition != "" {
|
||||
sb.WriteString(fmt.Sprintf("☁️ 天气:%s,%.0f°C,%s\n", b.Weather.Location, b.Weather.Temp, b.Weather.Condition))
|
||||
}
|
||||
|
||||
// 待办
|
||||
if len(b.Reminders) > 0 {
|
||||
sb.WriteString("📋 今日待办:\n")
|
||||
for _, r := range b.Reminders {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", r.Title))
|
||||
}
|
||||
}
|
||||
|
||||
// 新闻
|
||||
if len(b.News) > 0 {
|
||||
sb.WriteString("📰 今日新闻:\n")
|
||||
for _, n := range b.News {
|
||||
if n.Summary != "" {
|
||||
sb.WriteString(fmt.Sprintf(" - %s: %s\n", n.Title, n.Summary))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", n.Title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n请用昔涟的语气回复,包含:1) 温馨问候 2) 天气提醒 3) 待办提醒 4) 新闻简要 5) 结语祝福。简洁自然即可。")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// buildFallbackSummary 降级摘要(不依赖 AI)
|
||||
func (h *BriefingHandler) buildFallbackSummary(b *store.Briefing) string {
|
||||
today := time.Now().Format("2006年01月02日")
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("早上好!今天是%s ☀️\n\n", today))
|
||||
|
||||
if b.Weather != nil && b.Weather.Condition != "" {
|
||||
sb.WriteString(fmt.Sprintf("今日%s天气:%s,%.0f°C。", b.Weather.Location, b.Weather.Condition, b.Weather.Temp))
|
||||
if b.Weather.Temp < 10 {
|
||||
sb.WriteString("天气有点凉,记得多穿件衣服哦~")
|
||||
} else if b.Weather.Temp > 30 {
|
||||
sb.WriteString("天气比较热,注意防暑降温哦~")
|
||||
} else {
|
||||
sb.WriteString("天气不错,适合出门走走呢~")
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(b.Reminders) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("你今天有 %d 项待办事项,记得按时完成哦!\n", len(b.Reminders)))
|
||||
} else {
|
||||
sb.WriteString("今天没有待办事项,可以轻松一下~\n")
|
||||
}
|
||||
|
||||
if len(b.News) > 0 && b.News[0].Title != "未能获取今日新闻" {
|
||||
sb.WriteString(fmt.Sprintf("\n今日热点:%s。", b.News[0].Title))
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n祝你度过美好的一天!🌸")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// pushBriefingNotification 推送简报通知到用户
|
||||
func (h *BriefingHandler) pushBriefingNotification(userID string, b *store.Briefing) {
|
||||
bodyPreview := truncateStr(b.Summary, 100)
|
||||
|
||||
notif := &ws.NotificationInfo{
|
||||
ID: "briefing_" + b.ID,
|
||||
Type: "info",
|
||||
Title: fmt.Sprintf("📋 今日简报 (%s)", b.Date),
|
||||
Body: bodyPreview,
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Data: map[string]interface{}{
|
||||
"briefing_id": b.ID,
|
||||
"date": b.Date,
|
||||
"type": "daily_briefing",
|
||||
},
|
||||
}
|
||||
|
||||
msg := ws.ServerMessage{
|
||||
Type: "notification",
|
||||
MessageID: "briefing_" + b.ID,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Notification: notif,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
logger.Printf("[briefing] 序列化简报通知失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.hub.SendToUser(userID, data)
|
||||
|
||||
// 更新简报状态为已送达
|
||||
now := time.Now()
|
||||
b.Status = "delivered"
|
||||
b.DeliveredAt = &now
|
||||
if err := h.briefingStore.CreateOrUpdateBriefing(b); err != nil {
|
||||
logger.Printf("[briefing] 更新简报送达状态失败: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("[briefing] 简报通知已推送: user=%s date=%s", userID, b.Date)
|
||||
}
|
||||
|
||||
// StartBriefingScheduler 启动简报调度器
|
||||
// briefingTime 格式: "HH:MM",默认 "08:00"
|
||||
func StartBriefingScheduler(handler *BriefingHandler, briefingStore *store.BriefingStore, briefingTime string) {
|
||||
if briefingTime == "" {
|
||||
briefingTime = "08:00"
|
||||
}
|
||||
|
||||
go func() {
|
||||
// 每 30 秒检查一次是否到达简报时间
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
logger.Printf("[BriefingScheduler] 简报调度器已启动 (简报时间: %s)", briefingTime)
|
||||
|
||||
// 记录今天是否已触发
|
||||
lastTriggeredDate := ""
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now()
|
||||
currentTime := now.Format("15:04")
|
||||
currentDate := now.Format("2006-01-02")
|
||||
|
||||
// 检查是否到达简报时间且今天尚未触发
|
||||
if currentTime == briefingTime && currentDate != lastTriggeredDate {
|
||||
logger.Printf("[BriefingScheduler] 触发每日简报生成: %s", currentDate)
|
||||
lastTriggeredDate = currentDate
|
||||
|
||||
// 获取所有用户
|
||||
users, err := briefingStore.GetAllUsers()
|
||||
if err != nil {
|
||||
logger.Printf("[BriefingScheduler] 获取用户列表失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果没有从 reminders 表获取到用户,也尝试从 briefings 表获取
|
||||
if len(users) == 0 {
|
||||
users, _ = briefingStore.GetUsersWithBriefings()
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
logger.Println("[BriefingScheduler] 没有找到用户,跳过简报生成")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, userID := range users {
|
||||
logger.Printf("[BriefingScheduler] 为用户 %s 生成简报...", userID)
|
||||
result, err := handler.GenerateDailyBriefing(userID)
|
||||
if err != nil {
|
||||
logger.Printf("[BriefingScheduler] 生成简报失败: user=%s err=%v", userID, err)
|
||||
continue
|
||||
}
|
||||
handler.pushBriefingNotification(userID, result)
|
||||
}
|
||||
|
||||
logger.Printf("[BriefingScheduler] 每日简报已生成完毕,共 %d 个用户", len(users))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
// weatherEmoji 根据天气描述返回对应 emoji
|
||||
func weatherEmoji(condition string) string {
|
||||
c := strings.ToLower(condition)
|
||||
switch {
|
||||
case strings.Contains(c, "sunny") || strings.Contains(c, "clear") || strings.Contains(c, "晴"):
|
||||
return "☀️"
|
||||
case strings.Contains(c, "partly cloudy") || strings.Contains(c, "多云"):
|
||||
return "⛅"
|
||||
case strings.Contains(c, "cloudy") || strings.Contains(c, "阴"):
|
||||
return "☁️"
|
||||
case strings.Contains(c, "rain") || strings.Contains(c, "drizzle") || strings.Contains(c, "雨"):
|
||||
return "🌧️"
|
||||
case strings.Contains(c, "thunder") || strings.Contains(c, "雷"):
|
||||
return "⛈️"
|
||||
case strings.Contains(c, "snow") || strings.Contains(c, "雪"):
|
||||
return "❄️"
|
||||
case strings.Contains(c, "fog") || strings.Contains(c, "mist") || strings.Contains(c, "雾"):
|
||||
return "🌫️"
|
||||
case strings.Contains(c, "wind") || strings.Contains(c, "风"):
|
||||
return "💨"
|
||||
default:
|
||||
return "🌤️"
|
||||
}
|
||||
}
|
||||
|
||||
// parseFloat 安全解析浮点数
|
||||
func parseFloat(s string) float64 {
|
||||
var f float64
|
||||
fmt.Sscanf(s, "%f", &f)
|
||||
return f
|
||||
}
|
||||
|
||||
// parseInt 安全解析整数
|
||||
func parseInt(s string) (int, error) {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// truncateStr 截断字符串
|
||||
func truncateStr(s string, maxLen int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return string(runes[:maxLen]) + "..."
|
||||
}
|
||||
@@ -641,7 +641,7 @@ func (h *SessionHandler) exportMarkdown(c *gin.Context, session *store.Session,
|
||||
case "user":
|
||||
sb.WriteString(fmt.Sprintf("### 👤 用户 (%s)\n\n", timeStr))
|
||||
case "assistant":
|
||||
sb.WriteString(fmt.Sprintf("### 🤖 昔涟 (%s)\n\n", timeStr))
|
||||
sb.WriteString(fmt.Sprintf("### 💫 昔涟 (%s)\n\n", timeStr))
|
||||
case "system":
|
||||
sb.WriteString(fmt.Sprintf("### ⚙️ 系统 (%s)\n\n", timeStr))
|
||||
default:
|
||||
|
||||
@@ -96,7 +96,7 @@ func (h *WebhookHandler) HandleGenericWebhook(c *gin.Context) {
|
||||
resp, err := h.callAICore(userID, sessionID, req.Message, mode, platform)
|
||||
if err != nil {
|
||||
logger.Printf("[webhook] AI-Core 调用失败 (platform=%s): %v", platform, err)
|
||||
c.JSON(502, GenericWebhookResponse{Error: "AI 服务暂不可用: " + err.Error()})
|
||||
c.JSON(502, GenericWebhookResponse{Error: "昔涟暂时无法回应喵...(请稍后再试)"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ func (h *WebhookHandler) HandleDiscordWebhook(c *gin.Context) {
|
||||
logger.Printf("[webhook:discord] AI-Core 调用失败: %v", err)
|
||||
c.JSON(200, DiscordResponse{
|
||||
Type: 4,
|
||||
Data: &DiscordResponseData{Content: "昔涟暂时无法回应喵...(AI 服务异常: " + err.Error() + ")"},
|
||||
Data: &DiscordResponseData{Content: "昔涟暂时无法回应喵...(遇到了一些问题,请稍后再试)"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// Setup 注册所有路由
|
||||
func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config, sessionStore *store.SessionStore, reminderStore *store.ReminderStore, briefingStore *store.BriefingStore, automationStore *store.AutomationStore, fileStore *store.FileStore, ruleEngine *engine.RuleEngine, knowledgeStore *store.KnowledgeStore, imageHandler *handler.ImageHandler, db interface{}, modelConfigStore *config.ModelsConfigStore, thinkingScheduleStore *config.ThinkingScheduleStore) {
|
||||
func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config, sessionStore *store.SessionStore, reminderStore *store.ReminderStore, automationStore *store.AutomationStore, fileStore *store.FileStore, ruleEngine *engine.RuleEngine, knowledgeStore *store.KnowledgeStore, imageHandler *handler.ImageHandler, db interface{}, modelConfigStore *config.ModelsConfigStore, thinkingScheduleStore *config.ThinkingScheduleStore) {
|
||||
// 限流器
|
||||
rateLimiter := middleware.NewRateLimiter(10, 20) // 每秒10个请求,突发20
|
||||
|
||||
@@ -31,7 +31,6 @@ func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config, sessionStore *store.S
|
||||
webhookHandler := handler.NewWebhookHandler(cfg, hub)
|
||||
notificationHandler := handler.NewNotificationHandler(cfg, hub)
|
||||
reminderHandler := handler.NewReminderHandler(reminderStore, hub)
|
||||
briefingHandler := handler.NewBriefingHandler(cfg, hub, briefingStore, reminderStore)
|
||||
voiceHandler := handler.NewVoiceHandler(cfg.VoiceServiceURL)
|
||||
fileHandler := handler.NewFileHandler(fileStore)
|
||||
automationHandler := handler.NewAutomationHandler(automationStore, ruleEngine)
|
||||
@@ -115,14 +114,6 @@ func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config, sessionStore *store.S
|
||||
reminders.DELETE("/:id", reminderHandler.Delete) // DELETE /api/v1/reminders/:id
|
||||
}
|
||||
|
||||
// 每日简报 (需要认证)
|
||||
briefings := protected.Group("/briefings")
|
||||
{
|
||||
briefings.GET("", briefingHandler.GetBriefing) // GET /api/v1/briefings?user_id=xxx&date=2024-01-01
|
||||
briefings.GET("/latest", briefingHandler.GetLatestBriefings) // GET /api/v1/briefings/latest?user_id=xxx&limit=7
|
||||
briefings.POST("/generate", briefingHandler.Generate) // POST /api/v1/briefings/generate
|
||||
}
|
||||
|
||||
// 语音识别 + TTS (需要认证)
|
||||
voice := protected.Group("/voice")
|
||||
{
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/yourname/cyrene-ai/pkg/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Briefing 每日简报模型
|
||||
type Briefing struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Date string `json:"date"` // YYYY-MM-DD
|
||||
Weather *WeatherData `json:"weather"`
|
||||
News []NewsItem `json:"news"`
|
||||
Reminders []BriefReminder `json:"reminders"`
|
||||
Summary string `json:"summary"`
|
||||
SummarySource string `json:"summary_source"` // "ai" | "fallback"
|
||||
Status string `json:"status"` // pending, generated, delivered
|
||||
GeneratedAt *time.Time `json:"generated_at,omitempty"`
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// WeatherData 天气数据
|
||||
type WeatherData struct {
|
||||
Location string `json:"location"`
|
||||
Temp float64 `json:"temp"`
|
||||
Condition string `json:"condition"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// NewsItem 新闻条目
|
||||
type NewsItem struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// BriefReminder 简报中的提醒摘要
|
||||
type BriefReminder struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
RemindAt string `json:"remind_at"`
|
||||
}
|
||||
|
||||
// BriefingStore 每日简报持久化存储
|
||||
type BriefingStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewBriefingStore 使用已有数据库连接初始化简报存储并自动建表
|
||||
func NewBriefingStore(db *sql.DB) (*BriefingStore, error) {
|
||||
store := &BriefingStore{db: db}
|
||||
|
||||
if err := store.migrate(); err != nil {
|
||||
return nil, fmt.Errorf("简报表迁移失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Println("[BriefingStore] 简报持久化存储已初始化")
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// migrate 自动创建简报表结构
|
||||
func (s *BriefingStore) migrate() error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS daily_briefings (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
weather JSONB DEFAULT '{}',
|
||||
news JSONB DEFAULT '[]',
|
||||
reminders JSONB DEFAULT '[]',
|
||||
summary TEXT DEFAULT '',
|
||||
summary_source VARCHAR(20) DEFAULT 'ai',
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
generated_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, date)
|
||||
)`,
|
||||
`ALTER TABLE daily_briefings ADD COLUMN IF NOT EXISTS summary_source VARCHAR(20) DEFAULT 'ai'`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_briefings_user_id ON daily_briefings(user_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_briefings_date ON daily_briefings(date)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_briefings_user_date ON daily_briefings(user_id, date)`,
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
if _, err := s.db.Exec(q); err != nil {
|
||||
return fmt.Errorf("迁移SQL执行失败: %w\nSQL: %s", err, q)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateBriefing upsert 简报
|
||||
func (s *BriefingStore) CreateOrUpdateBriefing(b *Briefing) error {
|
||||
weatherJSON, err := json.Marshal(b.Weather)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化天气数据失败: %w", err)
|
||||
}
|
||||
newsJSON, err := json.Marshal(b.News)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化新闻数据失败: %w", err)
|
||||
}
|
||||
remindersJSON, err := json.Marshal(b.Reminders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化提醒数据失败: %w", err)
|
||||
}
|
||||
|
||||
if b.SummarySource == "" {
|
||||
b.SummarySource = "ai"
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(
|
||||
`INSERT INTO daily_briefings (id, user_id, date, weather, news, reminders, summary, summary_source, status, generated_at, delivered_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT (user_id, date) DO UPDATE SET
|
||||
weather = EXCLUDED.weather,
|
||||
news = EXCLUDED.news,
|
||||
reminders = EXCLUDED.reminders,
|
||||
summary = EXCLUDED.summary,
|
||||
summary_source = EXCLUDED.summary_source,
|
||||
status = EXCLUDED.status,
|
||||
generated_at = EXCLUDED.generated_at,
|
||||
delivered_at = EXCLUDED.delivered_at`,
|
||||
b.ID, b.UserID, b.Date, string(weatherJSON), string(newsJSON), string(remindersJSON),
|
||||
b.Summary, b.SummarySource, b.Status, b.GeneratedAt, b.DeliveredAt,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert 简报失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBriefingByDate 获取指定日期简报
|
||||
func (s *BriefingStore) GetBriefingByDate(userID, date string) (*Briefing, error) {
|
||||
row := s.db.QueryRow(
|
||||
`SELECT id, user_id, date::TEXT, weather, news, reminders, summary, COALESCE(summary_source, 'ai'), status, generated_at, delivered_at, created_at
|
||||
FROM daily_briefings WHERE user_id = $1 AND date = $2::DATE`,
|
||||
userID, date,
|
||||
)
|
||||
|
||||
b, err := s.scanBriefing(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("查询简报失败: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GetLatestBriefings 获取最近简报列表
|
||||
func (s *BriefingStore) GetLatestBriefings(userID string, limit int) ([]Briefing, error) {
|
||||
if limit <= 0 {
|
||||
limit = 7
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, user_id, date::TEXT, weather, news, reminders, summary, COALESCE(summary_source, 'ai'), status, generated_at, delivered_at, created_at
|
||||
FROM daily_briefings WHERE user_id = $1
|
||||
ORDER BY date DESC LIMIT $2`,
|
||||
userID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询简报列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var briefings []Briefing
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, uid, date, summary, summarySource, status string
|
||||
weatherRaw, newsRaw, remindersRaw []byte
|
||||
generatedAt, deliveredAt, createdAt sql.NullTime
|
||||
)
|
||||
if err := rows.Scan(&id, &uid, &date, &weatherRaw, &newsRaw, &remindersRaw,
|
||||
&summary, &summarySource, &status, &generatedAt, &deliveredAt, &createdAt); err != nil {
|
||||
return nil, fmt.Errorf("扫描简报行失败: %w", err)
|
||||
}
|
||||
|
||||
b := Briefing{
|
||||
ID: id,
|
||||
UserID: uid,
|
||||
Date: date,
|
||||
Summary: summary,
|
||||
SummarySource: summarySource,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if weatherRaw != nil {
|
||||
var w WeatherData
|
||||
if err := json.Unmarshal(weatherRaw, &w); err == nil {
|
||||
b.Weather = &w
|
||||
}
|
||||
}
|
||||
if newsRaw != nil {
|
||||
json.Unmarshal(newsRaw, &b.News)
|
||||
}
|
||||
if remindersRaw != nil {
|
||||
json.Unmarshal(remindersRaw, &b.Reminders)
|
||||
}
|
||||
if generatedAt.Valid {
|
||||
b.GeneratedAt = &generatedAt.Time
|
||||
}
|
||||
if deliveredAt.Valid {
|
||||
b.DeliveredAt = &deliveredAt.Time
|
||||
}
|
||||
b.CreatedAt = createdAt.Time
|
||||
|
||||
// 确保切片不为 nil
|
||||
if b.News == nil {
|
||||
b.News = []NewsItem{}
|
||||
}
|
||||
if b.Reminders == nil {
|
||||
b.Reminders = []BriefReminder{}
|
||||
}
|
||||
if b.Weather == nil {
|
||||
b.Weather = &WeatherData{}
|
||||
}
|
||||
|
||||
briefings = append(briefings, b)
|
||||
}
|
||||
|
||||
if briefings == nil {
|
||||
briefings = []Briefing{}
|
||||
}
|
||||
return briefings, rows.Err()
|
||||
}
|
||||
|
||||
// GetUsersWithBriefings 获取拥有简报的所有用户 ID 列表(用于调度器)
|
||||
func (s *BriefingStore) GetUsersWithBriefings() ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT DISTINCT user_id FROM daily_briefings`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询简报用户列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userIDs []string
|
||||
for rows.Next() {
|
||||
var uid string
|
||||
if err := rows.Scan(&uid); err != nil {
|
||||
return nil, fmt.Errorf("扫描用户ID失败: %w", err)
|
||||
}
|
||||
userIDs = append(userIDs, uid)
|
||||
}
|
||||
if userIDs == nil {
|
||||
userIDs = []string{}
|
||||
}
|
||||
return userIDs, rows.Err()
|
||||
}
|
||||
|
||||
// GetAllUsers 获取所有用户 ID(从 reminders 表获取,作为降级方案)
|
||||
func (s *BriefingStore) GetAllUsers() ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT DISTINCT user_id FROM reminders`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询用户列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userIDs []string
|
||||
for rows.Next() {
|
||||
var uid string
|
||||
if err := rows.Scan(&uid); err != nil {
|
||||
return nil, fmt.Errorf("扫描用户ID失败: %w", err)
|
||||
}
|
||||
userIDs = append(userIDs, uid)
|
||||
}
|
||||
if userIDs == nil {
|
||||
userIDs = []string{}
|
||||
}
|
||||
return userIDs, rows.Err()
|
||||
}
|
||||
|
||||
// scanBriefing 扫描单行简报
|
||||
func (s *BriefingStore) scanBriefing(row *sql.Row) (*Briefing, error) {
|
||||
var (
|
||||
id, uid, date, summary, summarySource, status string
|
||||
weatherRaw, newsRaw, remindersRaw []byte
|
||||
generatedAt, deliveredAt, createdAt sql.NullTime
|
||||
)
|
||||
|
||||
if err := row.Scan(&id, &uid, &date, &weatherRaw, &newsRaw, &remindersRaw,
|
||||
&summary, &summarySource, &status, &generatedAt, &deliveredAt, &createdAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := &Briefing{
|
||||
ID: id,
|
||||
UserID: uid,
|
||||
Date: date,
|
||||
Summary: summary,
|
||||
SummarySource: summarySource,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if weatherRaw != nil {
|
||||
var w WeatherData
|
||||
if err := json.Unmarshal(weatherRaw, &w); err == nil {
|
||||
b.Weather = &w
|
||||
}
|
||||
}
|
||||
if b.Weather == nil {
|
||||
b.Weather = &WeatherData{}
|
||||
}
|
||||
if newsRaw != nil {
|
||||
json.Unmarshal(newsRaw, &b.News)
|
||||
}
|
||||
if b.News == nil {
|
||||
b.News = []NewsItem{}
|
||||
}
|
||||
if remindersRaw != nil {
|
||||
json.Unmarshal(remindersRaw, &b.Reminders)
|
||||
}
|
||||
if b.Reminders == nil {
|
||||
b.Reminders = []BriefReminder{}
|
||||
}
|
||||
if generatedAt.Valid {
|
||||
b.GeneratedAt = &generatedAt.Time
|
||||
}
|
||||
if deliveredAt.Valid {
|
||||
b.DeliveredAt = &deliveredAt.Time
|
||||
}
|
||||
b.CreatedAt = createdAt.Time
|
||||
|
||||
return b, nil
|
||||
}
|
||||
@@ -100,7 +100,6 @@ export const SERVICES = {
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'dev-secret-key-change-me',
|
||||
AI_CORE_URL: 'http://localhost:8081',
|
||||
MEMORY_SERVICE_URL: process.env.MEMORY_SERVICE_URL || 'http://localhost:8091',
|
||||
TOOL_ENGINE_URL: process.env.TOOL_ENGINE_URL || 'http://localhost:8092',
|
||||
ADMIN_USERNAME: process.env.ADMIN_USERNAME || 'admin',
|
||||
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || 'cyrene-dev-admin',
|
||||
REGISTRATION_ENABLED: process.env.REGISTRATION_ENABLED || 'true',
|
||||
|
||||
Reference in New Issue
Block a user