Files
Cyrene_v2/cmd/cyrene/wire_services.go
T
AskaEth 3ba31b58ff
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(documents): 引用计数 + 文档/侧车 GC(主人 2.2 裁决;S7)
- 引用计数真实维护(documents 表唯一写入面):增减点只有四处 —— history 的 insert / UpdateMessageDocument / DeleteMessage / DeleteSession,且**全在同一事务**内(计数写失败 ⇒ 消息写入整体回滚,有钉住用例);UpdateMessageDocument 按新旧名字**集合差**增减(三阶段回写是常态,重算会重复计数 ⇒ 文件永远回收不掉)。

- GC:候选 = ref_count = 0;判据 = last_referenced_at(NULL 回落 created_at)早于 now − 保留期;动作 = 删 sidecar + 原件 + 元数据行(删行带 AND ref_count = 0 原子再确认);路径双检;汇总日志(含实占字节);单件失败 WARN 并继续;删除顺序 = 先文件后行(失败则行留着下轮重试,不留孤儿)。

- 组合根:启动期清扫一次(不阻塞启动)+ ticker 常驻(可停,close 顺序在关库之前);env DOCUMENT_GC_INTERVAL 默认 1h / DOCUMENT_RETENTION 默认 168h,**任一 ≤0 = 关闭 GC**(RETENTION=0 不解释成立即回收,关闭时记 WARN);.env.example 已同步(AGENTS §10)。

- 主会话独立验收:隔离 worktree(S7 的 18 文件,排除 F2 在途的 mcp/vision)build/vet 绿、全树 59 包 0 FAIL、GC/引用用例 20 PASS、双平台交叉编译绿;**变异亲自复现**(候选查询去掉 ref_count = 0 ⇒ TestGCDoesNotDeleteReferencedDocument 红:candidates = 1, want 0)。

- 未做(任务书允许的「只做后端」):面板手动「清理未引用附件」入口 —— 后端能力已就绪(documents.NewGC + GC.Sweep),接成端点属小切片(含 admin 路由 + 审计 document.gc + 契约登记)。

- 已知窄窗口(已在 gc.go 文件头登记):一轮 GC 与一次落库几乎同时发生时可能删掉刚被引用的文件(行不删、记 WARN);坏 document 列只记 WARN 不阻塞消息写入 ⇒ 计数可能偏低(只会让文件**晚**回收,绝不早删)。
2026-10-04 21:01:30 +08:00

161 lines
4.5 KiB
Go

package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
"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/task"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink"
)
// 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()
a.wireTerminalTool()
a.wireSubagentTool()
a.wireSessionReadTool()
return nil
}
// 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()
if a.backthink != nil {
_ = a.backthink.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
}