package main import ( "context" "database/sql" "fmt" "log/slog" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/bus" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/mcp" pluginmgr "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/plugin" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/db" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion" personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/events" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/flowgraph" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/vector" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/channelbridge" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/gateway" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) // defaultOwnerPath 与 defaultPersonasDir 指向随仓库分发的磁盘上人格/主人定义, // 路径相对工作目录。 const ( defaultOwnerPath = "configs/users/owner.yaml" defaultPersonasDir = "configs/personas" defaultSQLitePath = "./data/cyrene.db" fallbackInstruction = "You are Cyrene, a warm and attentive companion." // 上界与旋钮常量。编译期定值,运行时路径上不出现裸魔法数字(项目规范 §10)。 defaultMaxRounds = 8 defaultMaxParallel = 4 defaultAgentID = "orchestrator" defaultToolTimeout = 30 * time.Second ) // app 持有单二进制 Cyrene 组装的全部子系统:由 newApp 构建,关闭时由 close 拆除。 type app struct { cfg *config.Config log *slog.Logger db *sql.DB dbPath string bus bus.Bus modelAdapter model.Adapter llm *model.RuntimeLLM personas *personareg.Registry caps *registry.Registry // transcoder 是本地转码引擎(design/25):进程内、同步、纯本地,供渠道与其它 // 模块经依赖注入调用;同一个引擎也以 `transcode` 工具注册进 caps。装配只告警、 // 绝不阻塞启动,见 wire_transcode.go。 transcoder transcode.Transcoder dispatcher *tool.Dispatcher memStore *memory.Store retriever *memory.Retriever // embedder 与 memoryTidy 属于记忆整理(design/23):整理只在 embedder // 上算召回相似度,绝不接触会话/历史读数。 embedder embedding.Embedder memoryTidy *memory.TidyService // memoryReview 是 D3「入库前审查」(design/23 §15.1 D3):新记忆落库后由 // 独立的记忆管理会话判定 merge/modify/create/skip。它同样只依赖记忆存储 // 与 embedder,拿不到任何会话/历史读数。未启用时为 nil(写入路径不变)。 memoryReview *memory.PreInsertReviewer projStore *projectindex.Store loop *loop.Loop scheduler *orchestration.Scheduler tasks *task.Manager execProv exec.Provider iot *iot.Manager voice *voice.Chain mcpRegs []*mcp.Registry plugins *pluginmgr.Manager sessions *session.SessionManager automation *automations.Service backthink *backthink.Backthink vindex vector.Index owner *user.OwnerProfile history *history.Store gw *gateway.Server channelBridge *channelbridge.Bridge channels gateway.ChannelConfigStore // docGC 是文档回收的常驻任务(切片 S7):未被引用且超过保留期的文档连同 // sidecar 与元数据行一起回收。GC 被配置关闭时为 nil(见 wire_document_gc.go)。 docGC *documentGC // channelEvents 是渠道事件留存面(design/26 §4;切片 E3+E4):非消息事件逐条 // 落行 / T1 计数都经它,桥接层由 wire_channels.go 注入。库未接时为 nil // (桥接层随即走内存计数降级,见 wire_channel_events.go)。 channelEvents *events.SQLStore // channelEventKeeper 是事件留存的常驻维护任务(保留期清扫 + 计数落盘), // 由 main 的 startChannelEvents 拉起(见 wire_channel_events.go)。 channelEventKeeper *channelEventKeeper // usage 是 LLM 用量的异步落库出口(切片 U1):注入模型栈后,每次成功模型 // 调用都会把非敏感计数样本塞进它的有界队列。库未接时为 nil(= 只记日志)。 usage *model.UsageWriter // usageGC 是用量样本的保留期清理常驻任务。`LLM_USAGE_RETENTION ≤ 0` 时为 // nil(清理关闭,见 wire_usage.go)。 usageGC *usageRetentionGC // capacitySink 把 capacity_awaken 状态迁移广播给 Web UI(design/14 §5)。 // 由 *gateway.Server 满足(BroadcastCapacityAwaken);以接口形式持有, // 组合根才能用假出口做单元测试。 capacitySink capacityEmitter // emotion 把每一轮落定的对话施加到所绑人格的情绪机与好感度上(design/01 §2), // 并把变化交给 moodSink。没有注册人格时为 nil,此时不追踪、不持久化任何心情。 emotion *emotion.Observer // moodSink 把心情变化广播给 Web UI(design/07 §3)。由 *gateway.Server 满足 // (BroadcastMood);以接口形式持有,组合根才能用假出口做单元测试。 moodSink moodEmitter // traceWriter 把完成的 trace span 扇出到 JSONL stderr 记录器,网关出现后也送往 // 网关的可观测性缓冲(挂在 GET /api/v1/admin/observability/trace 后面, // design/19 §2)。持有它是为了让 wireFrontend 能往已在运行的 tracer 上注册 // 网关出口。 traceWriter *traceFanout // flow 是 loop 的实时链路图发布器(design/19 §1)。持有它是因为网关还要一个 // **只读**出口:`GET /api/v1/admin/flow/graph` 回「最近一轮」的快照,补上 // 「轮次结束后才打开的页面」看不到图的那个洞(契约 v0.71)。组合根把**同一个** // 发布器同时交给 loop/orchestration(写入面)与网关(查询面),因此查询到的 // 就是实时发布的那张图,不会出现两份状态。 flow *loop.FlowPublisher // flowStore 是链路图的**本地落盘**(design/19 §4 P3;切片 F2;迁移 0022):发布器 // 每轮收尾经它落一条,网关按 turnId / turns 读回。库未接时为 nil(发布器随即走 // 纯内存路径,见 wire_flow_graphs.go)。它与 flow 是两件事:flow 是实时内存图, // flowStore 是收尾存档。 flowStore *flowgraph.SQLStore // flowGraphKeeper 是链路图留存的常驻维护任务(保留期清扫),由 main 的 // startFlowGraphs 拉起(见 wire_flow_graphs.go)。 flowGraphKeeper *flowGraphKeeper } // newApp 依据 cfg 与 log 组装完整的 Cyrene 运行时:打开数据库、迁移表结构,接线 // 人格注册表、模型适配器、工具分发器、记忆栈、agent 循环、编排、执行、IoT、 // 渠道会话/适配器与网关,最后返回就绪的 app。 // // 致命的接线失败(数据库打开/迁移、必需的认证、网关)以错误返回;可选子系统则 // 优雅降级,使得未配置模型密钥或外部渠道时二进制仍能启动。 func newApp(ctx context.Context, cfg *config.Config, log *slog.Logger) (*app, error) { dataDir := resolveDataDir(cfg) databasePath := resolveSQLitePath(cfg, dataDir) dbCfg := cfg.DB dbCfg.SQLitePath = databasePath sqlDB, err := db.Open(ctx, databasePath, log) if err != nil { return nil, fmt.Errorf("open database: %w", err) } if err := db.MigrateUp(ctx, sqlDB, log); err != nil { _ = sqlDB.Close() return nil, fmt.Errorf("migrate database: %w", err) } eventBus := bus.New() a := &app{ cfg: cfg, log: log, db: sqlDB, dbPath: databasePath, bus: eventBus, caps: registry.New(), } // 后续任一接线步骤失败时,释放已经打开的资源,但不打整套服务关闭日志 // (半途启动不等同于一次干净退出)。 completed := false defer func() { if completed { return } if a.vindex != nil { _ = a.vindex.Close() } if a.tasks != nil { _ = a.tasks.Close() } if a.bus != nil { _ = a.bus.Close() } if a.db != nil { _ = a.db.Close() } }() if err := a.buildPersonas(); err != nil { return a, err } if err := a.wireMemory(ctx); err != nil { return a, err } if err := a.wireRuntime(); err != nil { return a, err } if err := a.wireAuxiliary(); err != nil { return a, err } a.wirePlugins(ctx) // 渠道事件留存面(design/26 §4;切片 E3+E4):必须在 wireChannels 之前构造 —— // 桥接层在构造时就把它注入(Options.EventStore),运行时不再重取。 a.wireChannelEvents() if err := a.wireChannels(); err != nil { return a, err } if err := a.wireFrontend(); err != nil { return a, err } completed = true return a, nil }