// Cyrene v2 — LLM-powered AI companion platform. // Single binary entry point that wires all services together. package main import ( "context" "fmt" "log" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "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) logger.Info("🧠 Cyrene v2 启动中...") // === 1. Platform & Config === plat := platform.Detect() logger.Infof("[platform] %s", plat.Summary()) cfg, err := config.Load() if err != nil { logger.Infof("[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 { logger.Infof("[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() logger.Infof("[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) } logger.Infof("[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, }) logger.Infof("[voice] ready") // === 6. Memory Service === var memSvc *memory.Service if database != nil { memRepo := memory.NewRepository(database) memSvc = memory.NewService(memRepo) logger.Infof("[memory] connected") } else { logger.Infof("[memory] disabled (no database)") } // === 7. AI-Core: Persona === personaLoader, err := persona.NewLoader(cfg.PersonaDir) if err != nil { logger.Infof("[persona] load error: %v (using builtin)", err) } logger.Infof("[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) // Wire LLM call logging to admin panel llmCallLogger := llm.NewCallLogger(200) llmSelector.DefaultAdapter().SetCallLogger(func(c llm.CallRecord) { llmCallLogger.Record(c) }) logger.Infof("[llm] adapter ready (model=%s)", cfg.LLMModel) // === 9. AI-Core: Context Store === ctxStore := aiCoreCtx.NewStore(4000, 50) logger.Infof("[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 }, }, ) logger.Infof("[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) } orch.SetToolExecutor(&toolExecAdapter{reg: reg}) logger.Infof("[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() logger.Infof("[bridge] config store ready (path=%s, %d adapters)", bridgeCfgPath, bridgeStore.Count()) // === 12. Gateway === userStore := api.NewMemUserStore(cfg.AdminUsername, cfg.AdminPassword) authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore) // Build dependencies for gateway handlers llmLogger := &llmLoggerAdapter{CallLogger: llmCallLogger} traceLogger := tracepkg.NewLogger(500) perfMon := perfpkg.NewMonitor(120, 10*time.Second) perfMon.Start() gwDeps := api.Dependencies{ AuthMgr: authMgr, Orchestrator: orch, LLMLogger: llmLogger, TraceLog: &traceAdapter{Logger: traceLogger}, PerfMon: &perfAdapter{Monitor: perfMon}, ModelConfig: api.NewModelConfigStore(), Automation: api.NewAutomationStore(), Reminders: api.NewReminderStore(), 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 === // Register restart handler — forks a new process and exits api.RestartFunc = func() { logger.Infof("[gateway] restarting...") reg.Shutdown() srv.Close() // Re-exec self with same args and env exe, _ := os.Executable() proc, err := os.StartProcess(exe, os.Args, &os.ProcAttr{ Env: os.Environ(), Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, }) if err != nil { logger.Fatalf("[gateway] restart failed: %v", err) } logger.Infof("[gateway] new process PID=%d, exiting...", proc.Pid) os.Exit(0) } go func() { logger.Infof("[gateway] listening on %s", addr) logger.Infof("[gateway] plugins=%d tools=%d", reg.Count(), len(reg.GetAllToolDefinitions())) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatalf("[gateway] %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("[gateway] shutting down...") reg.Shutdown() srv.Close() logger.Info("Cyrene v2 已安全退出") _ = voiceSvc _ = memSvc _ = orch } // llmLoggerAdapter adapts llm.Logger to api.LLMCallLogger interface. type llmLoggerAdapter struct{ *llmlog.CallLogger } func (a *llmLoggerAdapter) Recent(n int) []map[string]any { return a.CallLogger.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 } // toolExecAdapter adapts the plugin registry to orchestrator.ToolExecutor. type toolExecAdapter struct{ reg *plugin.Registry } func (t *toolExecAdapter) Tools() []llm.ToolSpec { defs := t.reg.GetAllToolDefinitions() specs := make([]llm.ToolSpec, len(defs)) for i, d := range defs { specs[i] = llm.ToolSpec{Name: d.Name, Description: d.Description, Parameters: d.Parameters} } return specs } func (t *toolExecAdapter) Execute(ctx context.Context, name string, params map[string]any) (string, error) { return t.reg.ExecuteTool(ctx, name, params) } // 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"], APIKey: ac.Settings["api_key"]})) } } router.SetMessageHandler(func(msg *bridge.UnifiedMessage) { sessionID := "bridge-" + msg.Source tokenCh, _, err := orch.ProcessInput(context.Background(), sessionID, msg.Content) if err != nil { return } var fc string for t := range tokenCh { fc += t.Content } orch.FinishRound(sessionID, msg.SenderID, fc) if fc != "" { router.SendTo(msg.Platform, msg.Source, fc, bridge.SendOptions{MaxLength: 500, MinGap: 3 * time.Second}) } }) }