From 489657ec083d8f5f6d53ef22ba0e4332309a8816 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 22 Jun 2026 21:04:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E9=86=92=E5=88=B0=E6=9C=9F=20?= =?UTF-8?q?=E2=86=92=20LLM=E7=94=9F=E6=88=90=E8=87=AA=E7=84=B6=E6=8F=90?= =?UTF-8?q?=E9=86=92=E8=AF=AD=20=E2=86=92=20=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gateway ReminderScheduler 到期时调用 ai-core /api/v1/internal/reminder-trigger - ai-core 用 LLM 生成温柔俏皮的提醒语,通过 messagePusher 推送 - Thinker 新增 TriggerReminderMessage 方法 - 环境变量: AI_CORE_URL (Gateway 已有) Co-Authored-By: Claude --- backend/ai-core/cmd/main.go | 54 +++++++++++++++++++ .../ai-core/internal/background/thinker.go | 20 +++++++ backend/gateway/cmd/main.go | 2 +- .../internal/handler/reminder_handler.go | 52 ++++++++++++++++-- 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/backend/ai-core/cmd/main.go b/backend/ai-core/cmd/main.go index 9823376..ebe056f 100644 --- a/backend/ai-core/cmd/main.go +++ b/backend/ai-core/cmd/main.go @@ -516,6 +516,60 @@ func main() { w.Write([]byte(`{"status":"ok"}`)) }) + // 提醒触发端点:Gateway 调度器在提醒到期时调用,由 LLM 生成自然提醒语推送 + mux.HandleFunc("/api/v1/internal/reminder-trigger", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + if r.Header.Get("X-Internal-Token") != os.Getenv("INTERNAL_SERVICE_TOKEN") { + w.WriteHeader(http.StatusUnauthorized) + return + } + var req struct { + Title string `json:"title"` + Description string `json:"description"` + UserID string `json:"user_id"` + SessionID string `json:"session_id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + // Generate a natural reminder via LLM. + prompt := fmt.Sprintf(`【系统提醒】现在有一条提醒到时间了: +标题:%s +描述:%s + +请用昔涟的口吻,用一句话自然地提醒开拓者。语气温柔俏皮,像朋友间的关心。 +只输出提醒语本身,不要加前缀或引号。`, req.Title, req.Description) + + messages := []model.LLMMessage{ + {Role: model.RoleSystem, Content: "你是昔涟。用温柔俏皮的语气,一句话提醒开拓者。"}, + {Role: model.RoleUser, Content: prompt}, + } + ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) + defer cancel() + resp, err := chatAdapter.Chat(ctx, messages) + if err != nil { + log.Printf("[reminder-trigger] LLM生成失败: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + reminderMsg := strings.TrimSpace(resp.Content) + if reminderMsg == "" { + reminderMsg = fmt.Sprintf("开拓者~「%s」的提醒到时间啦♪", req.Title) + } + log.Printf("[reminder-trigger] 生成提醒语: %s", reminderMsg) + + // Push through the message pusher if available. + if thinker != nil { + thinker.TriggerReminderMessage(req.UserID, req.SessionID, reminderMsg) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) + }) + mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + chatAdapter.ModelName() + `"}`)) diff --git a/backend/ai-core/internal/background/thinker.go b/backend/ai-core/internal/background/thinker.go index af41fa9..b04bd03 100644 --- a/backend/ai-core/internal/background/thinker.go +++ b/backend/ai-core/internal/background/thinker.go @@ -350,6 +350,26 @@ func (t *Thinker) UpdatePresence(online bool, sessionID string) { } } +// TriggerReminderMessage pushes a reminder message generated by LLM to the user. +// Called by the internal reminder-trigger endpoint when a reminder fires. +func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) { + t.mu.Lock() + pusher := t.messagePusher + pushSessionID := sessionID + if pushSessionID == "" { + pushSessionID = t.activeSessionID + } + if pushSessionID == "" { + pushSessionID = t.adminSessionID + } + t.mu.Unlock() + + if pusher != nil && message != "" { + log.Printf("[提醒推送] 推送LLM提醒: user=%s session=%s msg=%s", userID, pushSessionID, message) + pusher(userID, pushSessionID, message) + } +} + // ThinkerConfig 后台思考配置 type ThinkerConfig struct { Enabled bool diff --git a/backend/gateway/cmd/main.go b/backend/gateway/cmd/main.go index efc53fa..d2a16b2 100644 --- a/backend/gateway/cmd/main.go +++ b/backend/gateway/cmd/main.go @@ -190,7 +190,7 @@ func main() { // 启动提醒调度器 if reminderStore != nil { - handler.StartReminderScheduler(reminderStore, hub) + handler.StartReminderScheduler(reminderStore, hub, cfg.AICoreURL, cfg.InternalServiceToken) } // 启动服务 diff --git a/backend/gateway/internal/handler/reminder_handler.go b/backend/gateway/internal/handler/reminder_handler.go index 767de82..88bce45 100644 --- a/backend/gateway/internal/handler/reminder_handler.go +++ b/backend/gateway/internal/handler/reminder_handler.go @@ -1,8 +1,10 @@ package handler import ( + "bytes" "encoding/json" "html" + "io" "git.yeij.top/AskaEth/Cyrene/pkg/logger" "net/http" "strconv" @@ -242,7 +244,7 @@ func (h *ReminderHandler) Delete(c *gin.Context) { // ========== 提醒调度器 ========== // StartReminderScheduler 启动提醒调度器,每 30 秒检查一次到期提醒 -func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) { +func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub, aiCoreURL, internalToken string) { go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() @@ -250,13 +252,13 @@ func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) { logger.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)") for range ticker.C { - checkAndNotify(s, hub) + checkAndNotify(s, hub, aiCoreURL, internalToken) } }() } -// checkAndNotify 检查到期提醒并推送通知 -func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) { +// checkAndNotify 检查到期提醒并推送通知 + 触发 LLM 生成自然提醒语 +func checkAndNotify(s *store.ReminderStore, hub *ws.Hub, aiCoreURL, internalToken string) { reminders, err := s.GetDueReminders() if err != nil { logger.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err) @@ -297,7 +299,12 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) { // 2. 通过 Hub 向用户推送 hub.SendToUser(r.UserID, data) - // 3. 标记为已通知 + // 3. 触发 LLM 生成自然提醒语 + if aiCoreURL != "" && internalToken != "" { + go triggerLLMReminder(aiCoreURL, internalToken, r) + } + + // 4. 标记为已通知 if err := s.MarkNotified(r.ID); err != nil { logger.Printf("[ReminderScheduler] 标记已通知失败: id=%s err=%v", r.ID, err) } @@ -339,3 +346,38 @@ func calculateNextRemindAt(current time.Time, repeatType string) time.Time { return current } } + +func triggerLLMReminder(aiCoreURL, internalToken string, r store.Reminder) { + body := map[string]string{ + "title": r.Title, + "description": r.Description, + "user_id": r.UserID, + "session_id": r.SessionID, + } + reqBody, _ := json.Marshal(body) + req, err := http.NewRequest("POST", aiCoreURL+"/api/v1/internal/reminder-trigger", bytes.NewReader(reqBody)) + if err != nil { + logger.Printf("[ReminderScheduler] 创建LLM请求失败: %v", err) + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Internal-Token", internalToken) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + logger.Printf("[ReminderScheduler] LLM提醒生成请求失败: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(resp.Body) + logger.Printf("[ReminderScheduler] LLM提醒生成返回 %d: %s", resp.StatusCode, string(bodyBytes)) + return + } + + logger.Printf("[ReminderScheduler] LLM提醒已触发: title=%s user=%s", r.Title, r.UserID) +} + +