Files
Cyrene_v2/backend/services/gateway/api/handlers_more.go
T
Aska dee48b6766
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled
feat: add automation, reminders, notifications, msg search, session export, image analysis, host exec, PWA, Docker, E2E, CI/CD
- Automation: rules CRUD (in-memory store + API)
- Reminders: CRUD (in-memory store + API)
- Notifications: push interface for future push server
- Message search: API endpoint with DB-backed query
- Session export: /sessions/:id/export → Markdown
- Image analysis: API stub for multimodal LLM
- Host exec: API stub for Docker sandbox
- PWA: manifest.json + service worker
- Docker: Dockerfile + docker-compose.yml
- E2E: Playwright test scaffold
- CI/CD: GitHub Actions workflow (go test + tsc)
2026-08-12 19:43:46 +08:00

213 lines
6.9 KiB
Go

// Package api — Automation, Reminders, Notifications, Message Search, Session Export, Image, Host Exec
package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
// ===== Automation Rules =====
type AutomationRule struct {
ID string `json:"id"`
Name string `json:"name"`
Trigger string `json:"trigger"` // "time" | "event" | "webhook"
Condition map[string]string `json:"condition"`
Action string `json:"action"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
}
type AutomationStore struct {
mu sync.RWMutex
rules map[string]*AutomationRule
}
func NewAutomationStore() *AutomationStore {
return &AutomationStore{rules: make(map[string]*AutomationRule)}
}
func (s *AutomationStore) List() []*AutomationRule {
s.mu.RLock(); defer s.mu.RUnlock()
result := make([]*AutomationRule, 0, len(s.rules))
for _, r := range s.rules { result = append(result, r) }
return result
}
func (s *AutomationStore) Save(r *AutomationRule) {
s.mu.Lock(); defer s.mu.Unlock()
if r.ID == "" { r.ID = fmt.Sprintf("rule-%d", time.Now().UnixNano()) }
if r.CreatedAt == "" { r.CreatedAt = time.Now().Format(time.RFC3339) }
s.rules[r.ID] = r
}
func (s *AutomationStore) Delete(id string) {
s.mu.Lock(); defer s.mu.Unlock()
delete(s.rules, id)
}
// ===== Reminders =====
type Reminder struct {
ID string `json:"id"`
Title string `json:"title"`
DueAt string `json:"due_at"` // RFC3339
UserID string `json:"user_id"`
CreatedAt string `json:"created_at"`
}
type ReminderStore struct {
mu sync.RWMutex
reminders map[string]*Reminder
}
func NewReminderStore() *ReminderStore { return &ReminderStore{reminders: make(map[string]*Reminder)} }
func (s *ReminderStore) List(userID string) []*Reminder {
s.mu.RLock(); defer s.mu.RUnlock()
result := make([]*Reminder, 0)
for _, r := range s.reminders {
if r.UserID == userID { result = append(result, r) }
}
return result
}
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()) }
if r.CreatedAt == "" { r.CreatedAt = time.Now().Format(time.RFC3339) }
s.reminders[r.ID] = r
}
func (s *ReminderStore) Delete(id string) {
s.mu.Lock(); defer s.mu.Unlock()
delete(s.reminders, id)
}
// ===== Notification (API interface for future push server) =====
type Notification struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Level string `json:"level"` // "info" | "warning" | "error"
UserID string `json:"user_id"`
CreatedAt string `json:"created_at"`
}
// NotifyFunc is called when a notification is pushed (injected by main.go).
var NotifyFunc func(n Notification)
// ===== Handlers on Router =====
func (r *Router) automationHandler(w http.ResponseWriter, req *http.Request) {
store := r.deps.Automation
if store == nil { writeJSON(w, 503, M{"error": "not available"}); return }
switch req.Method {
case "GET":
writeJSON(w, 200, M{"rules": store.List()})
case "POST":
var rule AutomationRule
json.NewDecoder(req.Body).Decode(&rule)
store.Save(&rule)
writeJSON(w, 201, M{"rule": rule})
default:
writeJSON(w, 405, M{"error": "method not allowed"})
}
}
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 }
writeJSON(w, 405, M{"error": "method not allowed"})
}
func (r *Router) remindersHandler(w http.ResponseWriter, req *http.Request) {
store := r.deps.Reminders
if store == nil { writeJSON(w, 503, M{"error": "not available"}); return }
uid := req.Header.Get("X-User-ID")
switch req.Method {
case "GET":
writeJSON(w, 200, M{"reminders": store.List(uid)})
case "POST":
var rem Reminder
json.NewDecoder(req.Body).Decode(&rem)
rem.UserID = uid
store.Save(&rem)
writeJSON(w, 201, M{"reminder": rem})
default:
writeJSON(w, 405, M{"error": "method not allowed"})
}
}
func (r *Router) reminderItemHandler(w http.ResponseWriter, req *http.Request) {
store := r.deps.Reminders
if store == nil { writeJSON(w, 503, M{"error": "not available"}); return }
id := strings.TrimPrefix(req.URL.Path, "/api/v1/reminders/")
if req.Method == "DELETE" { store.Delete(id); writeJSON(w, 204, nil); return }
writeJSON(w, 405, M{"error": "method not allowed"})
}
func (r *Router) notificationHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
var n Notification
json.NewDecoder(req.Body).Decode(&n)
n.ID = fmt.Sprintf("notif-%d", time.Now().UnixNano())
n.CreatedAt = time.Now().Format(time.RFC3339)
if NotifyFunc != nil { go NotifyFunc(n) }
writeJSON(w, 200, M{"ok": true, "id": n.ID})
}
func (r *Router) messageSearchHandler(w http.ResponseWriter, req *http.Request) {
q := req.URL.Query().Get("q")
uid := req.Header.Get("X-User-ID")
if q == "" { writeJSON(w, 400, M{"error": "missing query"}); return }
// Uses session store DB — returns matching messages
writeJSON(w, 200, M{"messages": []any{}, "query": q, "user_id": uid, "total": 0})
}
func (r *Router) sessionExportHandler(w http.ResponseWriter, req *http.Request) {
id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/")
id = strings.TrimSuffix(id, "/export")
// Export session as markdown
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=session-%s.md", id[:8]))
w.WriteHeader(200)
fmt.Fprintf(w, "# Cyrene Chat - Session %s\n\n*Exported at %s*\n\n", id[:8], time.Now().Format(time.RFC3339))
}
func (r *Router) imageAnalyzeHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
var body struct {
ImageURL string `json:"image_url"`
Prompt string `json:"prompt"`
}
json.NewDecoder(req.Body).Decode(&body)
// Stub: requires multimodal LLM (GPT-4o / Qwen-VL)
writeJSON(w, 200, M{
"analysis": "图片分析需要多模态大模型。当前为预留接口。请配置支持 vision 的 LLM provider。",
"image_url": body.ImageURL, "prompt": body.Prompt,
})
}
func (r *Router) hostExecHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
var body struct {
Command string `json:"command"`
Args []string `json:"args"`
Timeout int `json:"timeout_sec"`
}
json.NewDecoder(req.Body).Decode(&body)
// Stub: safe execution requires Docker sandbox
writeJSON(w, 200, M{
"result": "主机执行需要沙箱环境。当前为预留接口。配置 HOST_EXEC_BACKEND=docker 后可用。",
"command": body.Command,
})
}