feat: 提醒到期 → LLM生成自然提醒语 → 推送
- Gateway ReminderScheduler 到期时调用 ai-core /api/v1/internal/reminder-trigger - ai-core 用 LLM 生成温柔俏皮的提醒语,通过 messagePusher 推送 - Thinker 新增 TriggerReminderMessage 方法 - 环境变量: AI_CORE_URL (Gateway 已有) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -516,6 +516,60 @@ func main() {
|
|||||||
w.Write([]byte(`{"status":"ok"}`))
|
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) {
|
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + chatAdapter.ModelName() + `"}`))
|
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + chatAdapter.ModelName() + `"}`))
|
||||||
|
|||||||
@@ -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 后台思考配置
|
// ThinkerConfig 后台思考配置
|
||||||
type ThinkerConfig struct {
|
type ThinkerConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func main() {
|
|||||||
|
|
||||||
// 启动提醒调度器
|
// 启动提醒调度器
|
||||||
if reminderStore != nil {
|
if reminderStore != nil {
|
||||||
handler.StartReminderScheduler(reminderStore, hub)
|
handler.StartReminderScheduler(reminderStore, hub, cfg.AICoreURL, cfg.InternalServiceToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动服务
|
// 启动服务
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html"
|
"html"
|
||||||
|
"io"
|
||||||
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
|
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -242,7 +244,7 @@ func (h *ReminderHandler) Delete(c *gin.Context) {
|
|||||||
// ========== 提醒调度器 ==========
|
// ========== 提醒调度器 ==========
|
||||||
|
|
||||||
// StartReminderScheduler 启动提醒调度器,每 30 秒检查一次到期提醒
|
// StartReminderScheduler 启动提醒调度器,每 30 秒检查一次到期提醒
|
||||||
func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) {
|
func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub, aiCoreURL, internalToken string) {
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -250,13 +252,13 @@ func StartReminderScheduler(s *store.ReminderStore, hub *ws.Hub) {
|
|||||||
logger.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)")
|
logger.Println("[ReminderScheduler] 提醒调度器已启动 (检查间隔: 30秒)")
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
checkAndNotify(s, hub)
|
checkAndNotify(s, hub, aiCoreURL, internalToken)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAndNotify 检查到期提醒并推送通知
|
// checkAndNotify 检查到期提醒并推送通知 + 触发 LLM 生成自然提醒语
|
||||||
func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
|
func checkAndNotify(s *store.ReminderStore, hub *ws.Hub, aiCoreURL, internalToken string) {
|
||||||
reminders, err := s.GetDueReminders()
|
reminders, err := s.GetDueReminders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err)
|
logger.Printf("[ReminderScheduler] 获取到期提醒失败: %v", err)
|
||||||
@@ -297,7 +299,12 @@ func checkAndNotify(s *store.ReminderStore, hub *ws.Hub) {
|
|||||||
// 2. 通过 Hub 向用户推送
|
// 2. 通过 Hub 向用户推送
|
||||||
hub.SendToUser(r.UserID, data)
|
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 {
|
if err := s.MarkNotified(r.ID); err != nil {
|
||||||
logger.Printf("[ReminderScheduler] 标记已通知失败: id=%s err=%v", r.ID, err)
|
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
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user