// Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway. package api import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model" "git.yeij.top/AskaEth/Cyrene_v2/services/gateway/store" ) // --- Interfaces for injected services --- // AuthManager validates JWT tokens and handles login/register. type AuthManager interface { ValidateToken(token string) (userID, username, role string, err error) Login(username, password string) (token string, err error) Register(username, password string) (token string, err error) } // MemoryEntry is a simplified memory entry for API responses. type MemoryEntry struct { ID string `json:"id"` UserID string `json:"user_id"` Content string `json:"content"` Summary string `json:"summary"` Keywords []string `json:"keywords"` Category string `json:"category"` Priority float64 `json:"priority"` } // MemorySearcher handles memory search queries. type MemorySearcher interface { Search(ctx context.Context, userID, query string, limit int) (any, error) List(ctx context.Context, userID string, limit int) (any, error) Add(ctx context.Context, userID, content, category string, priority float64) (any, error) Get(ctx context.Context, id string) (any, error) Delete(ctx context.Context, id string) error } // VoiceService handles STT and TTS. type VoiceService interface { Recognize(ctx context.Context, audioData []byte) (string, error) Synthesize(ctx context.Context, text string, voice string) ([]byte, error) } // OrchestratorPipeline handles the main dialogue flow. type OrchestratorPipeline interface { ProcessInput(ctx context.Context, sessionID, userID, userMessage string) (<-chan model.StreamToken, *model.Intent, error) } // WebSocketHub manages WS connections. type WebSocketHub interface { UpgradeHandler() http.HandlerFunc Count() int } // LLMCallLogger provides LLM call history. type LLMCallLogger interface { Recent(n int) []map[string]any Stats() map[string]any } // TraceLogger provides full-chain trace events. type TraceLogger interface { Recent(n int) []map[string]any Clear() // Remove old traces } // PerfMonitor provides performance snapshot history. type PerfMonitor interface { Latest() map[string]any History() []map[string]any } // BridgeConfigManager manages platform adapter configurations. type BridgeConfigManager interface { GetAdapters() []map[string]any SaveAdapter(name, adapterType string, enabled bool, settings map[string]string) error DeleteAdapter(name string) error } // AutomationEngine fires automation rules immediately. type AutomationEngine interface { Trigger(ctx context.Context, id string) error } // VisionAnalyzer analyzes images with a multimodal model. type VisionAnalyzer interface { AnalyzeImage(ctx context.Context, prompt, imageRef string) (string, error) } // ChatStorer persists chat sessions and messages. type ChatStorer interface { CreateSession(ctx context.Context, id, userID, title string) (*store.Session, error) ListSessions(ctx context.Context, userID string) ([]store.Session, error) DeleteSession(ctx context.Context, id, userID string) error ListMessages(ctx context.Context, sessionID string, limit int) ([]store.Message, error) ClearMessages(ctx context.Context, sessionID string) error SearchMessages(ctx context.Context, userID, query string, limit int) ([]store.Message, error) } // Dependencies holds all injectable services. type Dependencies struct { AuthMgr AuthManager MemorySvc MemorySearcher VoiceSvc VoiceService Orchestrator OrchestratorPipeline WSHub WebSocketHub LLMLogger LLMCallLogger TraceLog TraceLogger PerfMon PerfMonitor BridgeCfg BridgeConfigManager ChatStore ChatStorer ModelConfig *ModelConfigStore Automation *AutomationStore AutomationEng AutomationEngine Reminders *ReminderStore Vision VisionAnalyzer PluginList []map[string]string } // Router handles HTTP routing with middleware support. type Router struct { mux *http.ServeMux deps Dependencies } // New creates a new Router with all routes configured. func New(deps Dependencies) *Router { r := &Router{mux: http.NewServeMux(), deps: deps} r.setupRoutes() return r } func (r *Router) setupRoutes() { // Public r.mux.HandleFunc("/api/v1/health", r.healthHandler) r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler) r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler) // Protected r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler)) r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler)) // Memory r.mux.HandleFunc("/api/v1/memory/search", r.authMW(r.memorySearchHandler)) r.mux.HandleFunc("/api/v1/memory", r.authMW(r.memoryHandler)) r.mux.HandleFunc("/api/v1/memory/", r.authMW(r.memoryHandler)) // Voice r.mux.HandleFunc("/api/v1/voice/stt", r.authMW(r.voiceSTTHandler)) r.mux.HandleFunc("/api/v1/voice/tts", r.authMW(r.voiceTTSHandler)) // Admin r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler))) r.mux.HandleFunc("/api/v1/admin/restart", r.authMW(r.adminMW(r.adminRestartHandler))) r.mux.HandleFunc("/api/v1/admin/llm-calls", r.authMW(r.adminMW(r.llmCallsHandler))) r.mux.HandleFunc("/api/v1/admin/system", r.authMW(r.adminMW(r.systemInfoHandler))) r.mux.HandleFunc("/api/v1/admin/plugins", r.authMW(r.adminMW(r.pluginListHandler))) r.mux.HandleFunc("/api/v1/admin/sessions", r.authMW(r.adminMW(r.adminSessionsHandler))) r.mux.HandleFunc("/api/v1/admin/models/config", r.authMW(r.adminMW(r.modelConfigHandler))) r.mux.HandleFunc("/api/v1/admin/models/config/", r.authMW(r.adminMW(r.modelConfigItemHandler))) r.mux.HandleFunc("/api/v1/admin/trace", r.authMW(r.adminMW(r.traceHandler))) r.mux.HandleFunc("/api/v1/admin/perf", r.authMW(r.adminMW(r.perfHandler))) r.mux.HandleFunc("/api/v1/admin/bridge/config", r.authMW(r.adminMW(r.bridgeConfigHandler))) r.mux.HandleFunc("/api/v1/admin/bridge/config/", r.authMW(r.adminMW(r.bridgeConfigItemHandler))) // Automation & Reminders r.mux.HandleFunc("/api/v1/automation/rules", r.authMW(r.automationHandler)) r.mux.HandleFunc("/api/v1/automation/rules/", r.authMW(r.automationItemHandler)) r.mux.HandleFunc("/api/v1/reminders", r.authMW(r.remindersHandler)) r.mux.HandleFunc("/api/v1/reminders/", r.authMW(r.reminderItemHandler)) // Notifications (push interface) r.mux.HandleFunc("/api/v1/notifications/push", r.authMW(r.adminMW(r.notificationHandler))) // Message search & session export r.mux.HandleFunc("/api/v1/messages/search", r.authMW(r.messageSearchHandler)) // Image analysis r.mux.HandleFunc("/api/v1/images/analyze", r.authMW(r.imageAnalyzeHandler)) // Host execution sandbox r.mux.HandleFunc("/api/v1/tools/host-exec", r.authMW(r.adminMW(r.hostExecHandler))) } // ServeHTTP implements http.Handler. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") if req.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } r.mux.ServeHTTP(w, req) } // --- Middleware --- func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { h := req.Header.Get("Authorization") if h == "" || !strings.HasPrefix(strings.ToLower(h), "bearer ") { writeJSON(w, 401, M{"error": "unauthorized"}) return } uid, uname, role, err := r.deps.AuthMgr.ValidateToken(h[7:]) if err != nil { writeJSON(w, 401, M{"error": "invalid token"}) return } req.Header.Set("X-User-ID", uid) req.Header.Set("X-Username", uname) req.Header.Set("X-Role", role) next(w, req) } } func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if req.Header.Get("X-Role") != "admin" { writeJSON(w, 403, M{"error": "admin only"}) return } next(w, req) } } // --- Handlers --- func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, M{"status": "ok", "service": "cyrene-gateway", "version": "2.0.0-dev"}) } func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}) return } var body struct { Username string `json:"username"` Password string `json:"password"` } json.NewDecoder(req.Body).Decode(&body) token, err := r.deps.AuthMgr.Login(body.Username, body.Password) if err != nil { writeJSON(w, 401, M{"error": "invalid credentials"}) return } writeJSON(w, 200, M{"token": token}) } func (r *Router) registerHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}) return } var body struct { Username string `json:"username"` Password string `json:"password"` } json.NewDecoder(req.Body).Decode(&body) token, err := r.deps.AuthMgr.Register(body.Username, body.Password) if err != nil { writeJSON(w, 400, M{"error": err.Error()}) return } writeJSON(w, 201, M{"token": token}) } // --- Sessions --- func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) { if r.deps.ChatStore == nil { writeJSON(w, 503, M{"error": "chat store unavailable"}) return } uid := r.userID(req) switch req.Method { case "GET": sessions, err := r.deps.ChatStore.ListSessions(req.Context(), uid) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"sessions": sessions}) case "POST": var body struct { Title string `json:"title"` } json.NewDecoder(req.Body).Decode(&body) sess, err := r.deps.ChatStore.CreateSession(req.Context(), randID(), uid, body.Title) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 201, M{"session_id": sess.ID, "title": sess.Title}) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) { if r.deps.ChatStore == nil { writeJSON(w, 503, M{"error": "chat store unavailable"}) return } rest := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/") // Sub-resources: /sessions/{id}/export and /sessions/{id}/messages if strings.HasSuffix(rest, "/export") { r.sessionExportHandler(w, req) return } if strings.HasSuffix(rest, "/messages") { r.sessionMessagesHandler(w, req) return } id := rest switch req.Method { case "DELETE": if err := r.deps.ChatStore.DeleteSession(req.Context(), id, r.userID(req)); err != nil { writeJSON(w, 404, M{"error": err.Error()}) return } writeJSON(w, 204, nil) case "GET": messages, err := r.deps.ChatStore.ListMessages(req.Context(), id, 100) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"session_id": id, "messages": messages}) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } // sessionMessagesHandler serves GET (recent history) and DELETE (clear). func (r *Router) sessionMessagesHandler(w http.ResponseWriter, req *http.Request) { id := strings.TrimSuffix(strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/"), "/messages") switch req.Method { case "GET": limit := 0 fmt.Sscanf(req.URL.Query().Get("limit"), "%d", &limit) messages, err := r.deps.ChatStore.ListMessages(req.Context(), id, limit) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"messages": messages}) case "DELETE": if err := r.deps.ChatStore.ClearMessages(req.Context(), id); err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 204, nil) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } // --- Memory --- func (r *Router) memorySearchHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { writeJSON(w, 405, M{"error": "GET required"}) return } if r.deps.MemorySvc == nil { writeJSON(w, 503, M{"error": "memory service unavailable"}) return } q := req.URL.Query().Get("q") uid := req.Header.Get("X-User-ID") result, err := r.deps.MemorySvc.Search(req.Context(), uid, q, 10) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"data": result}) } func (r *Router) memoryHandler(w http.ResponseWriter, req *http.Request) { if r.deps.MemorySvc == nil { writeJSON(w, 503, M{"error": "memory service unavailable"}) return } // Extract ID from path: /api/v1/memory/{id} path := strings.TrimPrefix(req.URL.Path, "/api/v1/memory") path = strings.TrimPrefix(path, "/") userID := req.Header.Get("X-User-ID") switch { case path == "" && req.Method == "GET": result, _ := r.deps.MemorySvc.List(req.Context(), userID, 20) writeJSON(w, 200, M{"data": result}) case path == "" && req.Method == "POST": var body struct { Content string `json:"content"` Category string `json:"category"` Priority float64 `json:"priority"` } json.NewDecoder(req.Body).Decode(&body) if body.Category == "" { body.Category = "fact" } mem, err := r.deps.MemorySvc.Add(req.Context(), userID, body.Content, body.Category, body.Priority) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 201, M{"data": mem}) case path != "" && req.Method == "DELETE": r.deps.MemorySvc.Delete(req.Context(), path) writeJSON(w, 204, nil) case path != "" && req.Method == "GET": mem, err := r.deps.MemorySvc.Get(req.Context(), path) if err != nil { writeJSON(w, 404, M{"error": "memory not found"}) return } writeJSON(w, 200, M{"data": mem}) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } // --- Voice --- func (r *Router) voiceSTTHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}) return } if r.deps.VoiceSvc == nil { writeJSON(w, 503, M{"error": "voice service unavailable"}) return } var body struct{ AudioData []byte `json:"audio_data"` } json.NewDecoder(req.Body).Decode(&body) text, err := r.deps.VoiceSvc.Recognize(req.Context(), body.AudioData) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"text": text}) } func (r *Router) voiceTTSHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}) return } if r.deps.VoiceSvc == nil { writeJSON(w, 503, M{"error": "voice service unavailable"}) return } var body struct { Text string `json:"text"` Voice string `json:"voice"` } json.NewDecoder(req.Body).Decode(&body) if body.Voice == "" { body.Voice = "zh-CN-XiaoxiaoNeural" } audio, err := r.deps.VoiceSvc.Synthesize(req.Context(), body.Text, body.Voice) if err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } if len(audio) == 0 { writeJSON(w, 503, M{"error": "no tts provider available"}) return } w.Header().Set("Content-Type", "audio/mpeg") w.WriteHeader(200) w.Write(audio) } // --- Admin --- var startTime = time.Now() func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, M{ "dashboard": M{ "status": "running", "version": "2.0.0-dev", "uptime_sec": int(time.Since(startTime).Seconds()), "services": M{ "memory": r.deps.MemorySvc != nil, "voice": r.deps.VoiceSvc != nil, "ai_core": r.deps.Orchestrator != nil, }, }, }) } // RestartFunc is injected by main.go — called to restart the process. var RestartFunc func() func (r *Router) adminRestartHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, M{"restart": "ok", "message": "Cyrene is restarting..."}) go func() { time.Sleep(200 * time.Millisecond) // Let the response flush if RestartFunc != nil { RestartFunc() } }() } // --- Admin extra handlers --- func (r *Router) llmCallsHandler(w http.ResponseWriter, req *http.Request) { if r.deps.LLMLogger == nil { writeJSON(w, 200, M{"calls": []any{}, "stats": M{}}) return } writeJSON(w, 200, M{ "calls": r.deps.LLMLogger.Recent(50), "stats": r.deps.LLMLogger.Stats(), }) } func (r *Router) systemInfoHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, M{ "system": M{ "version": "2.0.0-dev", "go_version": "1.26", "uptime_sec": int(time.Since(startTime).Seconds()), "ws_clients": r.deps.WSHub.Count(), }, }) } func (r *Router) pluginListHandler(w http.ResponseWriter, _ *http.Request) { plugins := r.deps.PluginList if plugins == nil { plugins = []map[string]string{} } writeJSON(w, 200, M{"plugins": plugins}) } func (r *Router) adminSessionsHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, M{"sessions": []any{}, "total": 0}) } // --- Model Config --- func (r *Router) modelConfigHandler(w http.ResponseWriter, req *http.Request) { if r.deps.ModelConfig == nil { writeJSON(w, 503, M{"error": "model config unavailable"}) return } switch req.Method { case "GET": writeJSON(w, 200, M{ "providers": r.deps.ModelConfig.GetProviders(), "models": r.deps.ModelConfig.GetModels(), "routing": r.deps.ModelConfig.GetRouting(), }) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } func (r *Router) modelConfigItemHandler(w http.ResponseWriter, req *http.Request) { if r.deps.ModelConfig == nil { writeJSON(w, 503, M{"error": "model config unavailable"}) return } // Path: /api/v1/admin/models/config/{type}/{name} path := strings.TrimPrefix(req.URL.Path, "/api/v1/admin/models/config/") parts := strings.SplitN(path, "/", 2) if len(parts) < 2 { writeJSON(w, 400, M{"error": "invalid path"}) return } kind, name := parts[0], parts[1] switch req.Method { case "POST", "PUT": var body map[string]any json.NewDecoder(req.Body).Decode(&body) switch kind { case "providers": r.deps.ModelConfig.SaveProvider(&ModelProvider{ Name: name, BaseURL: str(body, "base_url"), APIKey: str(body, "api_key"), Timeout: num(body, "timeout_sec"), Retries: num(body, "max_retries"), }) case "models": var tags []string if t, ok := body["tags"].([]any); ok { for _, v := range t { tags = append(tags, fmt.Sprint(v)) } } r.deps.ModelConfig.SaveModel(&ModelEntry{ ID: name, Name: str(body, "name"), Provider: str(body, "provider"), Description: str(body, "description"), Tags: tags, Params: body["params"].(map[string]any), Enabled: true, }) case "routing": var chain []string if c, ok := body["fallback_chain"].([]any); ok { for _, v := range c { chain = append(chain, fmt.Sprint(v)) } } r.deps.ModelConfig.SaveRouting(&RoutingRule{ Purpose: name, FallbackChain: chain, Required: body["required"].(bool), }) } writeJSON(w, 200, M{"ok": true}) case "DELETE": switch kind { case "providers": r.deps.ModelConfig.DeleteProvider(name) case "models": r.deps.ModelConfig.DeleteModel(name) case "routing": r.deps.ModelConfig.DeleteRouting(name) } writeJSON(w, 204, nil) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } func str(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } func num(m map[string]any, key string) int { if v, ok := m[key].(float64); ok { return int(v) } return 0 } // --- Trace & Perf --- func (r *Router) traceHandler(w http.ResponseWriter, req *http.Request) { if r.deps.TraceLog == nil { writeJSON(w, 200, M{"traces": []any{}}) return } if req.Method == "DELETE" { r.deps.TraceLog.Clear() writeJSON(w, 200, M{"ok": true}) return } writeJSON(w, 200, M{"traces": r.deps.TraceLog.Recent(50)}) } func (r *Router) perfHandler(w http.ResponseWriter, req *http.Request) { if r.deps.PerfMon == nil { writeJSON(w, 200, M{"latest": M{}, "history": []any{}}) return } writeJSON(w, 200, M{ "latest": r.deps.PerfMon.Latest(), "history": r.deps.PerfMon.History(), }) } // --- Bridge Config --- func (r *Router) bridgeConfigHandler(w http.ResponseWriter, req *http.Request) { if r.deps.BridgeCfg == nil { writeJSON(w, 503, M{"error": "bridge config unavailable"}) return } switch req.Method { case "GET": writeJSON(w, 200, M{"adapters": r.deps.BridgeCfg.GetAdapters()}) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } func (r *Router) bridgeConfigItemHandler(w http.ResponseWriter, req *http.Request) { if r.deps.BridgeCfg == nil { writeJSON(w, 503, M{"error": "bridge config unavailable"}) return } name := strings.TrimPrefix(req.URL.Path, "/api/v1/admin/bridge/config/") if name == "" { writeJSON(w, 400, M{"error": "missing adapter name"}) return } switch req.Method { case "PUT", "POST": var body struct { Type string `json:"type"` Enabled bool `json:"enabled"` Settings map[string]string `json:"settings"` } json.NewDecoder(req.Body).Decode(&body) if err := r.deps.BridgeCfg.SaveAdapter(name, body.Type, body.Enabled, body.Settings); err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 200, M{"ok": true}) case "DELETE": if err := r.deps.BridgeCfg.DeleteAdapter(name); err != nil { writeJSON(w, 500, M{"error": err.Error()}) return } writeJSON(w, 204, nil) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } // --- Helpers --- type M = map[string]any func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if data != nil { json.NewEncoder(w).Encode(data) } } func randID() string { b := make([]byte, 8) rand.Read(b) return hex.EncodeToString(b) } func (r *Router) userID(req *http.Request) string { return req.Header.Get("X-User-ID") } // Ensure unused imports don't cause issues // Debug: this will be overwritten