// 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" ) // --- 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, 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 } // 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 ModelConfig *ModelConfigStore Automation *AutomationStore Reminders *ReminderStore 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/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) { switch req.Method { case "GET": writeJSON(w, 200, M{"sessions": []any{}}) case "POST": writeJSON(w, 201, M{"session_id": randID()}) default: writeJSON(w, 405, M{"error": "method not allowed"}) } } func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) { id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/") // Handle export if strings.HasSuffix(id, "/export") { id = strings.TrimSuffix(id, "/export") r.sessionExportHandler(w, req) return } switch req.Method { case "DELETE": writeJSON(w, 204, nil) case "GET": writeJSON(w, 200, M{"session_id": id}) 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 } writeJSON(w, 200, M{"audio_size": len(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, }, }, }) } // --- 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