Files
Cyrene_v2/cmd/cyrene/wire_services.go
T
AskaEth 6475eb875d feat(skills): 渐进披露接线(L2)—— 清单投影 + 命中才展开 + 工具面只收不放
- 清单只给 name+description(+when-to-use),单条 ≤1536 字符 + 全局条数上限,超限如实标 truncated
- 命中才把正文作为一条消息注入:跨回合保留、按内容指纹缓存不重复读盘、注入 ≤16 KiB 标 .truncated
- invocation 三态与清单逐格对齐(model-only 清单不含 description;user-only 清单有但模型不可调)
- 激活轮的工具面 = allowed-tools ∩ 安全闸 ∩ 注册表 再减 disallowed-tools(owner 也看不见未允许的工具)
- 激活状态进程内按会话(不落库);注入量已界化(压缩器估值未含,超我文件面)

裁定(DECISIONS #3/#4/#5):删掉 paths 启发式(只认显式挂载);多技能并集;收缩作用域=激活的那个回合。
2026-10-06 14:12:24 +08:00

215 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}