diff --git a/backend/ai-core/cmd/main.go b/backend/ai-core/cmd/main.go index 16c85c4..f89a75b 100644 --- a/backend/ai-core/cmd/main.go +++ b/backend/ai-core/cmd/main.go @@ -547,6 +547,10 @@ func main() { Description string `json:"description"` UserID string `json:"user_id"` SessionID string `json:"session_id"` + Platform string `json:"platform,omitempty"` + ChannelType string `json:"channel_type,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + AdapterName string `json:"adapter_name,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) @@ -580,7 +584,22 @@ func main() { // Push through the message pusher if available. if thinker != nil { - thinker.TriggerReminderMessage(req.UserID, req.SessionID, reminderMsg) + // If group reminder, push through platform-bridge via thinker + if req.Platform != "" && req.ChannelID != "" { + target := background.ProactiveTarget{ + Platform: req.Platform, + ChatType: req.ChannelType, + GroupID: req.ChannelID, + } + if req.AdapterName != "" { + target.Platform = req.AdapterName + } + thinker.PushPlatformMessage(target, reminderMsg) + } + // 只有非群聊提醒才推Web端(群聊提醒已通过platform-bridge发送) + if req.Platform == "" { + thinker.TriggerReminderMessage(req.UserID, req.SessionID, reminderMsg) + } } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) diff --git a/backend/ai-core/internal/background/thinker.go b/backend/ai-core/internal/background/thinker.go index 5413d88..f17e3d2 100644 --- a/backend/ai-core/internal/background/thinker.go +++ b/backend/ai-core/internal/background/thinker.go @@ -397,6 +397,17 @@ func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) { } } +// PushPlatformMessage pushes a message to a platform channel via the platform pusher. +func (t *Thinker) PushPlatformMessage(target ProactiveTarget, message string) { + t.mu.Lock() + pusher := t.platformMessagePusher + t.mu.Unlock() + if pusher != nil { + log.Printf("[平台推送] target=%s/%s group=%s msg=%s", target.Platform, target.ChatType, target.GroupID, message) + pusher(target, message) + } +} + // ThinkerConfig 后台思考配置 type ThinkerConfig struct { Enabled bool diff --git a/backend/ai-core/internal/tools/gateway_client.go b/backend/ai-core/internal/tools/gateway_client.go index 29677e7..f6534d4 100644 --- a/backend/ai-core/internal/tools/gateway_client.go +++ b/backend/ai-core/internal/tools/gateway_client.go @@ -38,7 +38,7 @@ type Reminder struct { } // CreateReminder calls POST /api/v1/internal/reminders on the Gateway. -func (c *GatewayClient) CreateReminder(ctx context.Context, userID, title, description, remindAt, repeatType, sessionID string) (*Reminder, error) { +func (c *GatewayClient) CreateReminder(ctx context.Context, userID, title, description, remindAt, repeatType, sessionID, platform, channelType, channelID, adapterName string) (*Reminder, error) { body := map[string]interface{}{ "user_id": userID, "title": title, @@ -46,6 +46,10 @@ func (c *GatewayClient) CreateReminder(ctx context.Context, userID, title, descr "remind_at": remindAt, "repeat_type": repeatType, "session_id": sessionID, + "platform": platform, + "channel_type": channelType, + "channel_id": channelID, + "adapter_name": adapterName, } reqBody, _ := json.Marshal(body) diff --git a/backend/ai-core/internal/tools/reminder_tool.go b/backend/ai-core/internal/tools/reminder_tool.go index 8b3db9c..35f3a8e 100644 --- a/backend/ai-core/internal/tools/reminder_tool.go +++ b/backend/ai-core/internal/tools/reminder_tool.go @@ -20,7 +20,7 @@ func NewReminderCreateTool(gw *GatewayClient, adminID string) *ReminderCreateToo func (t *ReminderCreateTool) Definition() ToolDefinition { return ToolDefinition{ Name: "reminder_create", - Description: "创建一个定时提醒。可用于提醒用户做某事、定时通知等。支持重复类型(none/daily/weekly/monthly)。时间格式为 ISO8601(如 2026-06-23T08:00:00+08:00)。", + Description: "创建一个定时提醒。时间格式为 ISO8601(如 2026-06-23T08:00:00+08:00)。如果在群聊中创建提醒,请传入 platform/channel_id/channel_type/adapter_name 以便提醒时发回原群聊。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ @@ -28,6 +28,10 @@ func (t *ReminderCreateTool) Definition() ToolDefinition { "description": map[string]string{"type": "string", "description": "提醒详细描述(可选)"}, "remind_at": map[string]string{"type": "string", "description": "提醒时间,ISO8601 格式,如 2026-06-23T08:00:00+08:00"}, "repeat_type": map[string]string{"type": "string", "description": "重复类型:none(不重复), daily(每天), weekly(每周), monthly(每月),默认 none"}, + "platform": map[string]string{"type": "string", "description": "(群聊提醒时必填)平台类型,如 obv11"}, + "channel_type": map[string]string{"type": "string", "description": "(群聊提醒时必填)频道类型:group"}, + "channel_id": map[string]string{"type": "string", "description": "(群聊提醒时必填)群号"}, + "adapter_name": map[string]string{"type": "string", "description": "(群聊提醒时必填)适配器名,如 obv11-main"}, }, "required": []string{"title", "remind_at"}, }, @@ -47,7 +51,11 @@ func (t *ReminderCreateTool) Execute(ctx context.Context, args map[string]interf return &ToolResult{Success: false, Error: "title 和 remind_at 为必填项"}, nil } - reminder, err := t.gw.CreateReminder(ctx, t.adminID, title, description, remindAt, repeatType, "") + platform, _ := args["platform"].(string) + channelType, _ := args["channel_type"].(string) + channelID, _ := args["channel_id"].(string) + adapterName, _ := args["adapter_name"].(string) + reminder, err := t.gw.CreateReminder(ctx, t.adminID, title, description, remindAt, repeatType, "", platform, channelType, channelID, adapterName) if err != nil { return &ToolResult{Success: false, Error: err.Error()}, nil } diff --git a/backend/gateway/internal/handler/reminder_handler.go b/backend/gateway/internal/handler/reminder_handler.go index 0cc017c..4a2f1ed 100644 --- a/backend/gateway/internal/handler/reminder_handler.go +++ b/backend/gateway/internal/handler/reminder_handler.go @@ -35,7 +35,11 @@ type CreateReminderRequest struct { RemindAt string `json:"remind_at" binding:"required"` // ISO 8601 格式 RepeatType string `json:"repeat_type"` // none, daily, weekly, monthly SessionID string `json:"session_id"` - UserID string `json:"user_id,omitempty"` // 内部调用时直接传 userID + UserID string `json:"user_id,omitempty"` + Platform string `json:"platform,omitempty"` + ChannelType string `json:"channel_type,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + AdapterName string `json:"adapter_name,omitempty"` // 内部调用时直接传 userID } // UpdateReminderRequest 更新提醒请求体 @@ -129,6 +133,10 @@ func (h *ReminderHandler) Create(c *gin.Context) { Status: "pending", RepeatType: repeatType, SessionID: req.SessionID, + Platform: req.Platform, + ChannelType: req.ChannelType, + ChannelID: req.ChannelID, + AdapterName: req.AdapterName, Notified: false, } @@ -356,10 +364,14 @@ func calculateNextRemindAt(current time.Time, repeatType string) time.Time { 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, + "title": r.Title, + "description": r.Description, + "user_id": r.UserID, + "session_id": r.SessionID, + "platform": r.Platform, + "channel_type": r.ChannelType, + "channel_id": r.ChannelID, + "adapter_name": r.AdapterName, } reqBody, _ := json.Marshal(body) req, err := http.NewRequest("POST", aiCoreURL+"/api/v1/internal/reminder-trigger", bytes.NewReader(reqBody)) diff --git a/backend/gateway/internal/store/reminder_store.go b/backend/gateway/internal/store/reminder_store.go index 34e6b76..664b41a 100644 --- a/backend/gateway/internal/store/reminder_store.go +++ b/backend/gateway/internal/store/reminder_store.go @@ -19,6 +19,10 @@ type Reminder struct { CompletedAt *time.Time `json:"completed_at,omitempty"` RepeatType string `json:"repeat_type"` // none, daily, weekly, monthly SessionID string `json:"session_id"` + Platform string `json:"platform,omitempty"` // 来源平台 + ChannelType string `json:"channel_type,omitempty"` // 频道类型 + ChannelID string `json:"channel_id,omitempty"` // 频道ID + AdapterName string `json:"adapter_name,omitempty"` // 适配器名 Notified bool `json:"notified"` } @@ -59,6 +63,10 @@ func (s *ReminderStore) migrate() error { `CREATE INDEX IF NOT EXISTS idx_reminders_remind_at ON reminders(remind_at)`, `CREATE INDEX IF NOT EXISTS idx_reminders_status ON reminders(status)`, `CREATE INDEX IF NOT EXISTS idx_reminders_due ON reminders(remind_at, status, notified)`, + `ALTER TABLE reminders ADD COLUMN IF NOT EXISTS platform VARCHAR(32) DEFAULT ''`, + `ALTER TABLE reminders ADD COLUMN IF NOT EXISTS channel_type VARCHAR(16) DEFAULT ''`, + `ALTER TABLE reminders ADD COLUMN IF NOT EXISTS channel_id VARCHAR(64) DEFAULT ''`, + `ALTER TABLE reminders ADD COLUMN IF NOT EXISTS adapter_name VARCHAR(64) DEFAULT ''`, } for _, q := range queries { @@ -75,7 +83,7 @@ func (s *ReminderStore) CreateReminder(r *Reminder) error { r.CreatedAt = time.Now() } _, err := s.db.Exec( - `INSERT INTO reminders (id, user_id, title, description, remind_at, status, created_at, repeat_type, session_id) + `INSERT INTO reminders (id, user_id, title, description, remind_at, status, created_at, repeat_type, session_id, platform, channel_type, channel_id, adapter_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, r.ID, r.UserID, r.Title, r.Description, r.RemindAt, r.Status, r.CreatedAt, r.RepeatType, r.SessionID, ) @@ -99,14 +107,14 @@ func (s *ReminderStore) GetRemindersByUser(userID, status string, limit, offset if status != "" { rows, err = s.db.Query( - `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified + `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified, platform, channel_type, channel_id, adapter_name FROM reminders WHERE user_id = $1 AND status = $2 ORDER BY remind_at ASC LIMIT $3 OFFSET $4`, userID, status, limit, offset, ) } else { rows, err = s.db.Query( - `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified + `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified, platform, channel_type, channel_id, adapter_name FROM reminders WHERE user_id = $1 ORDER BY remind_at ASC LIMIT $2 OFFSET $3`, userID, limit, offset, @@ -121,7 +129,7 @@ func (s *ReminderStore) GetRemindersByUser(userID, status string, limit, offset for rows.Next() { var r Reminder if err := rows.Scan(&r.ID, &r.UserID, &r.Title, &r.Description, &r.RemindAt, - &r.Status, &r.CreatedAt, &r.CompletedAt, &r.RepeatType, &r.SessionID, &r.Notified); err != nil { + &r.Status, &r.CreatedAt, &r.CompletedAt, &r.RepeatType, &r.SessionID, &r.Notified, &r.Platform, &r.ChannelType, &r.ChannelID, &r.AdapterName); err != nil { return nil, fmt.Errorf("扫描提醒行失败: %w", err) } reminders = append(reminders, r) @@ -136,7 +144,7 @@ func (s *ReminderStore) GetRemindersByUser(userID, status string, limit, offset // GetDueReminders 获取所有到期且未通知的提醒 func (s *ReminderStore) GetDueReminders() ([]Reminder, error) { rows, err := s.db.Query( - `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified + `SELECT id, user_id, title, description, remind_at, status, created_at, completed_at, repeat_type, session_id, notified, platform, channel_type, channel_id, adapter_name FROM reminders WHERE remind_at <= NOW() AND status = 'pending' AND notified = FALSE ORDER BY remind_at ASC`, @@ -150,7 +158,7 @@ func (s *ReminderStore) GetDueReminders() ([]Reminder, error) { for rows.Next() { var r Reminder if err := rows.Scan(&r.ID, &r.UserID, &r.Title, &r.Description, &r.RemindAt, - &r.Status, &r.CreatedAt, &r.CompletedAt, &r.RepeatType, &r.SessionID, &r.Notified); err != nil { + &r.Status, &r.CreatedAt, &r.CompletedAt, &r.RepeatType, &r.SessionID, &r.Notified, &r.Platform, &r.ChannelType, &r.ChannelID, &r.AdapterName); err != nil { return nil, fmt.Errorf("扫描到期提醒行失败: %w", err) } reminders = append(reminders, r)