// 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) if memSvc != nil { memAdapter := llmSelector.DefaultAdapter() memExtractor := memory.NewExtractor(memSvc, &memory.LLMAdapterBridge{Adapter: memAdapter}) orch.SetMemoryExtractor(memExtractor) } log.Printf("[orchestrator] pipeline ready") // === 11.5 Platform Bridge === bridgeCfgPath := "configs/bridge.json" bridgeRouter := bridge.NewRouter() bridgeStore := bridge.NewConfigStore(bridgeCfgPath, func(cfg *bridge.BridgeConfig) { bridgeRouter.StopAll() bridgeRouter = bridge.NewRouter() loadAdaptersFromConfig(bridgeRouter, cfg, orch) bridgeRouter.StartAll() }) bridgeStore.StartWatching(5 * time.Second) // Load initial adapters loadAdaptersFromConfig(bridgeRouter, &bridge.BridgeConfig{Adapters: bridgeStore.GetAdapters()}, orch) bridgeRouter.StartAll() log.Printf("[bridge] config store ready (path=%s, %d adapters)", bridgeCfgPath, bridgeStore.Count()) // === 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(), BridgeCfg: &bridgeConfigAdapter{ConfigStore: bridgeStore}, 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 } // 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 } // bridgeConfigAdapter adapts bridge.ConfigStore to api.BridgeConfigManager. type bridgeConfigAdapter struct{ *bridge.ConfigStore } func (a *bridgeConfigAdapter) GetAdapters() []map[string]any { adapters := a.ConfigStore.GetAdapters() result := make([]map[string]any, len(adapters)) for i, ac := range adapters { result[i] = map[string]any{ "name": ac.Name, "type": ac.Type, "enabled": ac.Enabled, "settings": ac.Settings, "created_at": ac.CreatedAt, "updated_at": ac.UpdatedAt, } } return result } func (a *bridgeConfigAdapter) SaveAdapter(name, adapterType string, enabled bool, settings map[string]string) error { return a.ConfigStore.SaveAdapter(bridge.AdapterConfig{ Name: name, Type: adapterType, Enabled: enabled, Settings: settings, }) } func (a *bridgeConfigAdapter) DeleteAdapter(name string) error { return a.ConfigStore.DeleteAdapter(name) } func loadAdaptersFromConfig(router *bridge.Router, cfg *bridge.BridgeConfig, orch *orchestrator.Pipeline) { for _, ac := range cfg.Adapters { if !ac.Enabled { continue } switch ac.Type { case "qq-obv11": router.Register(bridgeqq.New(bridgeqq.Config{ Name: ac.Name, WSURL: ac.Settings["ws_url"], AccessToken: ac.Settings["access_token"], BotQQ: ac.Settings["bot_qq"], })) case "telegram": router.Register(bridgetg.New(bridgetg.Config{Name: ac.Name, Token: ac.Settings["token"]})) case "discord": router.Register(bridgedc.New(bridgedc.Config{Name: ac.Name, Token: ac.Settings["token"]})) case "webhook": router.Register(bridgewh.New(bridgewh.Config{Name: ac.Name, ListenAddr: ac.Settings["listen_addr"]})) } } router.SetMessageHandler(func(msg *bridge.UnifiedMessage) { tokenCh, _, err := orch.ProcessInput(context.Background(), "bridge-"+msg.Source, msg.Content) if err != nil { return } var fc string for t := range tokenCh { fc += t.Content } if fc != "" { router.SendTo(msg.Platform, msg.Source, fc, bridge.SendOptions{MaxLength: 500, MinGap: 3 * time.Second}) } }) }