Files
Cyrene_v2/backend/cmd/cyrene/main.go
T
Aska 4e2e81f684 feat(trace): waterfall visualization + persona YAML sync
- trace.Logger: Trace+Span model with 14+ hop tracking
- trace.SpanTimer: convenience API for instrumentation
- trace_adapter.go: serialize Trace+Span for frontend
- Frontend TracePage: colored waterfall bars, expand/collapse, detail popup
- persona.go: full v1 YAML parser (identity/traits/forbidden/sections)
- cyrene_persona.yaml: v1 persona config copied to v2
- Orchestrator: TraceHook + MemoryExtractor injection points
2026-08-12 18:52:39 +08:00

357 lines
12 KiB
Go

// 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})
}
})
}