- model 层新增可选 UsageSink(nil ⇒ 逐字不变,既有用例零改动) - 组合根注入 UsageWriter:有界队列 1024 + 单写者 + 批量事务(128/批、2s 兜底), 队列满丢弃 + 限流 WARN,绝不阻塞模型调用;sink 挂 RuntimeLLM(热重载不断) - 迁移 0017:表 llm_usage(13 列,session_id 可空)+ idx_created_at + idx_session_id - 新端点 GET /api/v1/admin/llm/usage(owner|admin 读;缺省近 24h、上限 90 天、 超限 400 不静默裁剪;hitRate = cacheHit/(cacheHit+cacheMiss),分母 0 ⇒ 0; 绝不含凭据与正文;未接线/查库失败 ⇒ 500 usage_unavailable) - LLM_USAGE_RETENTION(默认 720h,≤0 = 关闭清理):启动清一次 + 每小时 - 面板 LLM 页「用量(近 24h)」小卡 + Docs 两份同步 v0.64 - 已知缺口(如实登记):session_id 恒 NULL,需 loop 侧在请求上打标,留列后接
163 lines
4.6 KiB
Go
163 lines
4.6 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()
|
|
// 用量落库与保留期清理同样要用连接池:先把队列里剩下的样本写完,再停清理。
|
|
a.stopUsage()
|
|
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
|
|
}
|