feat(gateway): automation rule engine and reminder scheduler
- engine: 5-field cron parser (*/n, ranges, lists) firing at most once per minute; device-state triggers with edge detection; manual trigger via POST /automation/rules/:id/trigger; actions run through the AI pipeline so rules can call any tool (IoT control, search, ...) - reminders: due-scan scheduler delivering via WebSocket broadcast, optional LLM-polished reminder text with plain-title fallback; Reminder gains Done flag with ListDue/MarkDone - tests: cron matching and validation, once-per-minute dedup, device edge triggering, manual trigger, reminder delivery
This commit is contained in:
1 parent
9f30cdd086
commit
d80710d85a
7 files changed
+679
-2
No files matched your search
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/engine"
|
||||
)
|
||||
|
||||
// ===== Automation Rules =====
|
||||
@@ -57,6 +59,7 @@ type Reminder struct {
|
||||
Title string `json:"title"`
|
||||
DueAt string `json:"due_at"` // RFC3339
|
||||
UserID string `json:"user_id"`
|
||||
Done bool `json:"done"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -76,6 +79,25 @@ func (s *ReminderStore) List(userID string) []*Reminder {
|
||||
return result
|
||||
}
|
||||
|
||||
// ListDue returns undelivered reminders whose time has come.
|
||||
func (s *ReminderStore) ListDue(now time.Time) []engine.Reminder {
|
||||
s.mu.RLock(); defer s.mu.RUnlock()
|
||||
var due []engine.Reminder
|
||||
for _, r := range s.reminders {
|
||||
if r.Done { continue }
|
||||
t, err := time.Parse(time.RFC3339, r.DueAt)
|
||||
if err != nil || t.After(now) { continue }
|
||||
due = append(due, engine.Reminder{ID: r.ID, Title: r.Title, DueAt: t, UserID: r.UserID})
|
||||
}
|
||||
return due
|
||||
}
|
||||
|
||||
// MarkDone marks a reminder as delivered.
|
||||
func (s *ReminderStore) MarkDone(id string) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
if r, ok := s.reminders[id]; ok { r.Done = true }
|
||||
}
|
||||
|
||||
func (s *ReminderStore) Save(r *Reminder) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
if r.ID == "" { r.ID = fmt.Sprintf("rem-%d", time.Now().UnixNano()) }
|
||||
@@ -123,8 +145,22 @@ func (r *Router) automationHandler(w http.ResponseWriter, req *http.Request) {
|
||||
func (r *Router) automationItemHandler(w http.ResponseWriter, req *http.Request) {
|
||||
store := r.deps.Automation
|
||||
if store == nil { writeJSON(w, 503, M{"error": "not available"}); return }
|
||||
id := strings.TrimPrefix(req.URL.Path, "/api/v1/automation/rules/")
|
||||
if req.Method == "DELETE" { store.Delete(id); writeJSON(w, 204, nil); return }
|
||||
rest := strings.TrimPrefix(req.URL.Path, "/api/v1/automation/rules/")
|
||||
|
||||
// POST /api/v1/automation/rules/{id}/trigger — fire immediately
|
||||
if strings.HasSuffix(rest, "/trigger") {
|
||||
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
|
||||
if r.deps.AutomationEng == nil { writeJSON(w, 503, M{"error": "automation engine not running"}); return }
|
||||
id := strings.TrimSuffix(rest, "/trigger")
|
||||
if err := r.deps.AutomationEng.Trigger(req.Context(), id); err != nil {
|
||||
writeJSON(w, 404, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{"ok": true})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "DELETE" { store.Delete(rest); writeJSON(w, 204, nil); return }
|
||||
writeJSON(w, 405, M{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ type BridgeConfigManager interface {
|
||||
DeleteAdapter(name string) error
|
||||
}
|
||||
|
||||
// AutomationEngine fires automation rules immediately.
|
||||
type AutomationEngine interface {
|
||||
Trigger(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// ChatStorer persists chat sessions and messages.
|
||||
type ChatStorer interface {
|
||||
CreateSession(ctx context.Context, id, userID, title string) (*store.Session, error)
|
||||
@@ -110,6 +115,7 @@ type Dependencies struct {
|
||||
ChatStore ChatStorer
|
||||
ModelConfig *ModelConfigStore
|
||||
Automation *AutomationStore
|
||||
AutomationEng AutomationEngine
|
||||
Reminders *ReminderStore
|
||||
PluginList []map[string]string
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user