package main import ( "context" "fmt" "path/filepath" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/platform" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec" "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/task" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // wireAuxiliary 组装执行提供者、任务管理器、IoT 管理器、渠道会话管理器、 // 自动化服务与后台思考服务。 func (a *app) wireAuxiliary() error { provider, err := exec.New(a.cfg.ExecSandbox, a.log) if err != nil { return fmt.Errorf("exec provider: %w", err) } a.execProv = provider tmgr, err := task.New(a.cfg, provider, a.log, task.WithBus(a.bus)) if err != nil { return fmt.Errorf("task manager: %w", err) } a.tasks = tmgr iotMgr, err := iot.New(a.cfg.IoT, a.log, iot.WithBus(a.bus)) if err != nil { return fmt.Errorf("iot: %w", err) } a.iot = iotMgr sess, err := session.New(context.Background(), a.db, a.log) if err != nil { return fmt.Errorf("session manager: %w", err) } a.sessions = sess hist, herr := history.NewStore(context.Background(), a.db, a.log, history.WithDocumentRefs(documents.NewRefCounter(a.log))) if herr != nil { return fmt.Errorf("history store: %w", herr) } a.history = hist automation, err := automations.NewService(automations.Config{}, automations.Deps{ Bus: a.bus, Loop: a.loop, Log: logger.WithSource(a.log, "services/automations"), }) if err != nil { return fmt.Errorf("automations: %w", err) } a.automation = automation bt, err := backthink.New(backthink.ConfigFromBackground(a.cfg.Background), backthink.Deps{ Bus: a.bus, Thinker: backthink.NewLoopThinker(a.loop, a.history), Recent: a.history, State: backthink.NewIoTStateProvider(iotMgr, a.log), Exporter: backthink.NewPushOutlet(a.pushOutletConfig()), DB: a.db, Log: logger.WithSource(a.log, "services/backthink"), }) if err != nil { return fmt.Errorf("backthink: %w", err) } a.backthink = bt a.wireVoice() a.wireMCP() a.wireSkills() // SK-5(design/09 §6.3):技能注册表填好之后,把技能面接到循环的模型面装配上 // —— 清单投影 + 命中才展开正文 + 技能轮次的工具面收缩。技能来源就是上面刚 // 注册进去的能力注册表(每轮实时读,因此面板热装的技能下一轮即出现在清单里)。 // 必须排在 wireSkills 之后、且在任何 Run 之前(wireAuxiliary 仍在装配期)。 a.loop.SetSkillFace(a.buildSkillFace()) a.wireTerminalTool() a.wireSubagentTool() a.wireSessionReadTool() // X1(裁决 15):把外围子系统(IoT)以 KindIoT 放进能力面,并注册只读的 // capability_list 工具。排在最后,是为了让它列出的清单已经包含前面各步 // 注册进来的工具(terminal / subagent / session_read 等)。 a.wireCapabilitySurface() return nil } // buildSkillFace 构造技能面(SK-5,design/09 §6.3):来源是能力注册表里的 // KindSkill 条目 —— 启动时 wireSkills 载入的与面板热装的技能都在同一处,且 // **每轮实时读取**,因此热装技能下一轮就出现在模型清单里,无需重启。 // // 这里只给来源与日志,不配任何旋钮:单条清单上限(1536 字符)、清单条数上限、 // 正文注入字节上限都由代码常量给出(技能包的 listing.go 与 loop 的 // assemble_skill.go 各有带依据的注释),既不新增 `.env` 键,也不引入第二套 // 配置面(主人 2026-10-06 红线:.env 冻结为最小化引导面)。 func (a *app) buildSkillFace() *loop.SkillFace { return loop.NewSkillFace(loop.SkillFaceOptions{ Source: func() []sdk.Skill { if a.caps == nil { return nil } caps := a.caps.List(registry.KindSkill) out := make([]sdk.Skill, 0, len(caps)) for _, cap := range caps { // 注册表里不是技能的条目(理论上不会有,KindSkill 条目按契约就是 // sdk.Skill)直接跳过:宁可少一个技能,也不让类型断言失败的条目 // 把整轮装配拖垮。 if s, ok := cap.Value.(sdk.Skill); ok && s != nil { out = append(out, s) } } return out }, Log: logger.WithSource(a.log, "cmd/cyrene/skills"), }) } // pushOutletConfig 把环境变量里的推送出口配置块映射为 backthink 的导出器配置。 // 个推(Getui)凭据只被 config 读取、并不消费:备用 SDK 尚未接入,因此出口在 // 待接入该档位时记日志,而不是伪造一次推送。token 只透传,绝不写日志。 func (a *app) pushOutletConfig() backthink.PushOutletConfig { po := a.cfg.PushOutlet return backthink.PushOutletConfig{ Enabled: po.Enabled, URL: po.URL, Token: po.Token, Timeout: time.Duration(po.TimeoutSec) * time.Second, Log: a.log, } } // close 按依赖顺序关闭每个子系统并释放资源。 func (a *app) close(ctx context.Context) { // 文档回收循环要先用连接池(每轮一次查询),因此排在关库之前。 a.docGC.stop() // 用量落库与保留期清理同样要用连接池:先把队列里剩下的样本写完,再停清理。 a.stopUsage() if a.backthink != nil { _ = a.backthink.Stop(ctx) } // 记忆整理与 backthink 同属后台定时任务:先停它,再关库(它每轮只有 // 查询与候选写入,且 Stop 会等本轮跑完)。 if a.memoryTidy != nil { _ = a.memoryTidy.Stop(ctx) } // 入库前审查与整理同属记忆后台任务:先停它(Stop 会等正在审的那一条跑完), // 再关库 —— 否则正在写 memory_items 的审查会撞上已关闭的连接池。 if a.memoryReview != nil { _ = a.memoryReview.Stop(ctx) } if a.automation != nil { _ = a.automation.Stop(ctx) } if a.channelBridge != nil { a.channelBridge.Stop(ctx) } if a.scheduler != nil { _ = a.scheduler.CancelAll(ctx) } if a.gw != nil { a.gw.Close() } for _, reg := range a.mcpRegs { _ = reg.Unregister(ctx) } if a.plugins != nil { a.plugins.Close(ctx) } if a.tasks != nil { _ = a.tasks.Close() } if a.vindex != nil { _ = a.vindex.Close() } if a.bus != nil { _ = a.bus.Close() } if a.db != nil { _ = a.db.Close() } a.log.Info("cyrene shutdown complete") } // resolveDataDir 返回配置的数据目录(来自 .env / 环境变量,经 cfg.DataDir); // 只有在没有配置目录时才回落到与操作系统相称的默认值。 func resolveDataDir(cfg *config.Config) string { if cfg.DataDir != "" { return cfg.DataDir } return platform.DefaultDataDir() } // resolveSQLitePath 返回实际使用的 SQLite 路径:当配置里仍是默认值时, // 把它推导到数据目录下。 func resolveSQLitePath(cfg *config.Config, dataDir string) string { p := cfg.DB.SQLitePath if strings.TrimSpace(p) == "" || p == defaultSQLitePath { return filepath.Join(dataDir, "cyrene.db") } return p }