package engine import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "strings" "sync" "time" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model" ) // Reminder is a due notification for a user. type Reminder struct { ID string Title string DueAt time.Time UserID string } // ReminderSource provides due reminders and marks them delivered. type ReminderSource interface { // ListDue returns reminders that are due and not yet delivered. ListDue(now time.Time) []Reminder // MarkDone marks a reminder as delivered. MarkDone(id string) } // ReminderNotifier delivers a reminder to the user (e.g. via WebSocket). type ReminderNotifier func(userID, title, body string) // Copywriter polishes reminder text via the LLM (optional). type Copywriter interface { Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) } // ReminderScheduler scans for due reminders and delivers them. type ReminderScheduler struct { src ReminderSource notify ReminderNotifier llm Copywriter interval time.Duration stop chan struct{} stopOnce sync.Once } // NewReminderScheduler creates a scheduler scanning at the given interval. func NewReminderScheduler(src ReminderSource, notify ReminderNotifier, llm Copywriter, interval time.Duration) *ReminderScheduler { if interval <= 0 { interval = 30 * time.Second } return &ReminderScheduler{src: src, notify: notify, llm: llm, interval: interval, stop: make(chan struct{})} } // Start begins the scan loop. func (s *ReminderScheduler) Start(ctx context.Context) { go func() { ticker := time.NewTicker(s.interval) defer ticker.Stop() for { select { case <-s.stop: return case <-ctx.Done(): return case t := <-ticker.C: s.deliver(ctx, t) } } }() logger.Infof("[reminder] scheduler started (interval=%v)", s.interval) } // Stop shuts down the scheduler. func (s *ReminderScheduler) Stop() { s.stopOnce.Do(func() { close(s.stop) }) } func (s *ReminderScheduler) deliver(ctx context.Context, now time.Time) { for _, r := range s.src.ListDue(now) { body := s.polish(ctx, r.Title) logger.Infof("[reminder] due: %q for %s", r.Title, r.UserID) if s.notify != nil { s.notify(r.UserID, r.Title, body) } s.src.MarkDone(r.ID) } } // polish asks the LLM for a warmer reminder text; falls back to the title. func (s *ReminderScheduler) polish(ctx context.Context, title string) string { if s.llm == nil { return title } c, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() resp, err := s.llm.Chat(c, []model.LLMMessage{ {Role: "system", Content: "你是昔涟。用一两句温柔自然的话提醒主人一件事,不要解释,直接输出提醒内容。"}, {Role: "user", Content: fmt.Sprintf("请提醒我:%s", title)}, }) if err != nil { return title } if text := strings.TrimSpace(resp.Content); text != "" && len([]rune(text)) <= 120 { return text } return title }