// Cyrene v2 — LLM-powered AI companion platform. // Single binary entry point that wires all services together. package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/db" "git.yeij.top/AskaEth/Cyrene_v2/internal/nats" "git.yeij.top/AskaEth/Cyrene_v2/internal/platform" "git.yeij.top/AskaEth/Cyrene_v2/internal/plugin" "git.yeij.top/AskaEth/Cyrene_v2/plugins/calculator" "git.yeij.top/AskaEth/Cyrene_v2/plugins/crypto" "git.yeij.top/AskaEth/Cyrene_v2/plugins/datetime" "git.yeij.top/AskaEth/Cyrene_v2/plugins/file" httpplugin "git.yeij.top/AskaEth/Cyrene_v2/plugins/http" iotmock "git.yeij.top/AskaEth/Cyrene_v2/plugins/iot-mock" "git.yeij.top/AskaEth/Cyrene_v2/plugins/json" "git.yeij.top/AskaEth/Cyrene_v2/plugins/markdown" "git.yeij.top/AskaEth/Cyrene_v2/plugins/random" "git.yeij.top/AskaEth/Cyrene_v2/plugins/text" webfetch "git.yeij.top/AskaEth/Cyrene_v2/plugins/web-fetch" websearch "git.yeij.top/AskaEth/Cyrene_v2/plugins/web-search" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm" llmlog "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm" tracepkg "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/trace" perfpkg "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/perf" aiCoreCtx "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/context" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/orchestrator" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/persona" "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/subsession" "git.yeij.top/AskaEth/Cyrene_v2/services/gateway/api" "git.yeij.top/AskaEth/Cyrene_v2/services/gateway/ws" "git.yeij.top/AskaEth/Cyrene_v2/services/memory" bridge "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge" bridgeqq "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/qq" bridgetg "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/telegram" bridgedc "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/discord" bridgewh "git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge/webhook" voicesvc "git.yeij.top/AskaEth/Cyrene_v2/services/voice" ) func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("🧠 Cyrene v2 启动中...") // === 1. Platform & Config === plat := platform.Detect() log.Printf("[platform] %s", plat.Summary()) cfg, err := config.Load() if err != nil { log.Printf("[config] .env load failed: %v", err) } cfg.ValidateOrFatal() cfg.LogSanitized() plat.EnsureDirs() // === 2. Database === database, err := db.Connect(db.Config{ Host: cfg.PostgresHost, Port: cfg.PostgresPort, User: cfg.PostgresUser, Password: cfg.PostgresPassword, DBName: cfg.PostgresDB, SSLMode: cfg.PostgresSSLMode, MaxOpenConns: 25, MaxIdleConns: 5, }) if err != nil { log.Printf("[db] warning: %v (running without persistence)", err) } else { defer database.Close() db.RunMigration(context.Background(), database, "memories", db.SplitSQL(memory.CreateTableSQL())) } // === 3. NATS (in-process) === natsConn := nats.NewInProcess() log.Printf("[nats] ready") _ = natsConn // === 4. Plugin Registry === reg := plugin.NewRegistry() pluginCfg := plugin.PluginConfig{ DataDir: cfg.DataDir, NATSURL: cfg.NATSUrl, DatabaseURL: cfg.GetDatabaseURL(), LogLevel: cfg.LogLevel, } // Load IoT plugin if cfg.IoTProvider == "mock" || cfg.IoTProvider == "" { reg.Register(iotmock.New(), pluginCfg) } // Load tool plugins reg.Register(calculator.New(), pluginCfg) reg.Register(datetime.New(), pluginCfg) reg.Register(crypto.New(), pluginCfg) reg.Register(random.New(), pluginCfg) reg.Register(text.New(), pluginCfg) reg.Register(json.New(), pluginCfg) reg.Register(markdown.New(), pluginCfg) reg.Register(file.New(), pluginCfg) reg.Register(httpplugin.New(), pluginCfg) reg.Register(webfetch.New(), pluginCfg) if cfg.SearXNGURL != "" { reg.Register(websearch.New(cfg.SearXNGURL), pluginCfg) } log.Printf("[plugin] loaded %d plugins: %v", reg.Count(), reg.ListNames()) // === 5. Voice Service === voiceSvc := voicesvc.New(voicesvc.Config{ STTProvider: cfg.ASRProvider, TTSProviders: []string{cfg.TTSProvider}, DashScopeKey: cfg.DashScopeAPIKey, }) log.Printf("[voice] ready") // === 6. Memory Service === var memSvc *memory.Service if database != nil { memRepo := memory.NewRepository(database) memSvc = memory.NewService(memRepo) log.Printf("[memory] connected") } else { log.Printf("[memory] disabled (no database)") } // === 7. AI-Core: Persona === personaLoader, err := persona.NewLoader(cfg.PersonaDir) if err != nil { log.Printf("[persona] load error: %v (using builtin)", err) } log.Printf("[persona] loaded %d persona(s): %v", personaLoader.Count(), personaLoader.List()) personaInjector := persona.NewInjector(personaLoader.Default()) // === 8. AI-Core: LLM Adapter === llmCfg := llm.DefaultConfig() llmCfg.BaseURL = cfg.LLMBaseURL llmCfg.APIKey = cfg.LLMAPIKey llmCfg.Model = cfg.LLMModel llmCfg.FallbackModel = cfg.LLMFallbackModel llmSelector := llm.NewModelSelector(llmCfg) log.Printf("[llm] adapter ready (model=%s)", cfg.LLMModel) // === 9. AI-Core: Context Store === ctxStore := aiCoreCtx.NewStore(4000, 50) log.Printf("[context] sliding window ready (4000 tokens, 50 turns)") // === 10. AI-Core: Sub-session Manager === subMgr := subsession.NewManager( &subsession.GeneralProvider{}, &subsession.MemoryProvider{}, &subsession.IoTProvider{ GetDeviceState: func(ctx context.Context) (string, error) { iot := reg.GetIoTProvider() if iot == nil { return "", fmt.Errorf("no iot provider") } devices, err := iot.ListDevices(ctx) if err != nil { return "", err } states := make([]orchestrator.DeviceState, len(devices)) for i, d := range devices { states[i] = orchestrator.DeviceState{ID: d.ID, Name: d.Name, State: d.State} } return orchestrator.BuildIoTContext(states), nil }, }, ) log.Printf("[subsession] 4 providers ready") // === 11. AI-Core: Orchestrator === semanticCache := llm.NewSemanticCache() orch := orchestrator.NewPipeline(llmSelector, semanticCache, ctxStore, subMgr, personaInjector) log.Printf("[orchestrator] pipeline ready") // === 11.5 Platform Bridge === bridgeRouter := bridge.NewRouter() // QQ adapter — enabled when OBv11 WebSocket URL is configured if qqURL := os.Getenv("QQ_OBV11_WS_URL"); qqURL != "" { qqAdapter := bridgeqq.New(bridgeqq.Config{ Name: "qq-main", WSURL: qqURL, AccessToken: os.Getenv("QQ_ACCESS_TOKEN"), BotQQ: os.Getenv("QQ_BOT_QQ"), }) bridgeRouter.Register(qqAdapter) } // Telegram if tgToken := os.Getenv("TG_BOT_TOKEN"); tgToken != "" { bridgeRouter.Register(bridgetg.New(bridgetg.Config{Name: "tg-main", Token: tgToken})) } // Discord if dcToken := os.Getenv("DC_BOT_TOKEN"); dcToken != "" { bridgeRouter.Register(bridgedc.New(bridgedc.Config{Name: "dc-main", Token: dcToken})) } // Webhook if whAddr := os.Getenv("WEBHOOK_LISTEN_ADDR"); whAddr != "" { bridgeRouter.Register(bridgewh.New(bridgewh.Config{Name: "webhook-main", ListenAddr: whAddr})) } // Route incoming messages to orchestrator bridgeRouter.SetMessageHandler(func(msg *bridge.UnifiedMessage) { log.Printf("[bridge] incoming %s from %s: %s", msg.Platform, msg.SenderName, msg.Content) tokenCh, _, err := orch.ProcessInput(context.Background(), "bridge-"+msg.Source, msg.Content) if err != nil { log.Printf("[bridge] orchestrator error: %v", err) return } var fullContent string for token := range tokenCh { fullContent += token.Content } if fullContent != "" { bridgeRouter.SendTo(msg.Platform, msg.Source, fullContent, bridge.SendOptions{ MaxLength: 500, MinGap: 3 * time.Second, }) } }) bridgeRouter.StartAll(); log.Printf("[bridge] platform bridge ready") // === 12. Gateway === userStore := api.NewMemUserStore(cfg.AdminPassword) authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore) // Build dependencies for gateway handlers llmLogger := llmlog.NewLogger(200) traceLogger := tracepkg.NewLogger(500) perfMon := perfpkg.NewMonitor(120, 10*time.Second) perfMon.Start() gwDeps := api.Dependencies{ AuthMgr: authMgr, Orchestrator: orch, LLMLogger: &llmLoggerAdapter{Logger: llmLogger}, TraceLog: &traceAdapter{Logger: traceLogger}, PerfMon: &perfAdapter{Monitor: perfMon}, ModelConfig: api.NewModelConfigStore(), PluginList: pluginNamesToMap(reg.ListNames()), } if memSvc != nil { gwDeps.MemorySvc = memory.NewAPIAdapter(memSvc) } gwDeps.VoiceSvc = voiceSvc wsHub := ws.NewHub(orch) gwDeps.WSHub = wsHub apiRouter := api.New(gwDeps) wsMux := http.NewServeMux() wsMux.HandleFunc("/ws/chat", wsHub.UpgradeHandler()) mainMux := http.NewServeMux() mainMux.Handle("/api/", apiRouter) mainMux.Handle("/ws/", wsMux) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) srv := &http.Server{Addr: addr, Handler: mainMux} // === 13. Start === go func() { log.Printf("[gateway] listening on %s", addr) log.Printf("[gateway] plugins=%d tools=%d", reg.Count(), len(reg.GetAllToolDefinitions())) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("[gateway] %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("[gateway] shutting down...") reg.Shutdown() srv.Close() log.Println("Cyrene v2 已安全退出") _ = voiceSvc _ = memSvc _ = orch } // llmLoggerAdapter adapts llm.Logger to api.LLMCallLogger interface. type llmLoggerAdapter struct{ *llmlog.Logger } func (a *llmLoggerAdapter) Recent(n int) []map[string]any { return a.Logger.RecentMaps(n) } func pluginNamesToMap(names []string) []map[string]string { result := make([]map[string]string, len(names)) for i, n := range names { result[i] = map[string]string{"name": n, "enabled": "true"} } return result } // traceAdapter adapts trace.Logger to api.TraceLogger interface. type traceAdapter struct{ *tracepkg.Logger } func (a *traceAdapter) Recent(n int) []map[string]any { events := a.Logger.Recent(n) result := make([]map[string]any, len(events)) for i, e := range events { result[i] = map[string]any{ "id": e.ID, "timestamp": e.Timestamp.Format(time.RFC3339), "hop": e.Hop, "label": e.Label, "status": e.Status, "duration_ms": e.DurationMs, "detail": e.Detail, "data": e.Data, } if result[i]["data"] == nil { result[i]["data"] = map[string]any{} } } return result } // perfAdapter adapts perf.Monitor to api.PerfMonitor interface. type perfAdapter struct{ *perfpkg.Monitor } func (a *perfAdapter) Latest() map[string]any { s := a.Monitor.Latest() return map[string]any{ "alloc_mb": s.AllocMB, "sys_mb": s.SysMB, "num_gc": s.NumGC, "goroutines": s.Goroutines, "heap_objects": s.HeapObj, } } func (a *perfAdapter) History() []map[string]any { snapshots := a.Monitor.History() result := make([]map[string]any, len(snapshots)) for i, s := range snapshots { result[i] = map[string]any{ "timestamp": s.Timestamp.Format(time.RFC3339), "alloc_mb": s.AllocMB, "sys_mb": s.SysMB, "num_gc": s.NumGC, "goroutines": s.Goroutines, } } return result }