执行方案阶段 0 · G0-A:组合根 926 行是全仓第一大超限文件,且被 10+ 条裁决争用 (1.4/12.4.1、03.4.3、06.4.2、07.4.4、10.4.5、12.4.2、13.4.3、14.4.1、14.4.3、16.4.2) ⇒ 不先拆,后续全部被迫串行且必然破 500 行红线。 - wire.go(181):入口与共享 —— const 块、app struct、newApp - wire_runtime.go(362):loop/agent/model/memory/capacity/trace(buildPersonas、wireMemory、 wireRuntime、buildCapacity、onCapacity、onToolAudit、defaultPersonaID、defaultInstructions…) - wire_services.go(158):exec/task/iot/session/history/automations/backthink (wireAuxiliary、pushOutletConfig、close、resolveDataDir、resolveSQLitePath) - wire_channels.go(172):渠道/桥(ownerRegistrar、wireChannels、runtimeChannels、reviewFor…) - wire_frontend.go(117):gateway/webui/观测扇出(capacityEmitter、wireFrontend) 纯搬移机械证明:去 package/import/空行后两侧 **811=811 行、排序后 diff 为空、md5 相同**; 顶层声明集合 30 条守恒;唯一新增 = 各文件的 package 行与 import 块(原 import 块按声明归属 重分配给新文件)。未重命名/重排/改注释。 验证:go build/vet 绿;go test ./cmd/... 绿;linux/amd64 + darwin/arm64 交叉编译绿; 五文件 181/362/158/172/117 行均 ≤500;gofmt -l cmd/cyrene/ 无输出。 注:全量 go test 当前有 1 处红 —— internal/contract/enums 的 TestModeTableMatchesFrontendModes 读 frontend/web/src/lib/types.ts 找 MODES,而该文件 正被并行的前端拆分任务搬走;与本提交无关,随后修正该测试的读取路径。
182 lines
6.3 KiB
Go
182 lines
6.3 KiB
Go
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/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/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/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 and defaultPersonasDir point at the on-disk persona/owner
|
|
// definitions shipped with the repo; they are relative to the working directory.
|
|
const (
|
|
defaultOwnerPath = "configs/users/owner.yaml"
|
|
defaultPersonasDir = "configs/personas"
|
|
defaultSQLitePath = "./data/cyrene.db"
|
|
fallbackInstruction = "You are Cyrene, a warm and attentive companion."
|
|
|
|
// Bound/knob constants. Compile-time so no bare magic number appears in the
|
|
// runtime path (project rule §10).
|
|
defaultMaxRounds = 8
|
|
defaultMaxParallel = 4
|
|
defaultAgentID = "orchestrator"
|
|
defaultToolTimeout = 30 * time.Second
|
|
)
|
|
|
|
// app holds every subsystem the single Cyrene binary assembles. It is built by
|
|
// newApp and torn down by close on shutdown.
|
|
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
|
|
dispatcher *tool.Dispatcher
|
|
memStore *memory.Store
|
|
retriever *memory.Retriever
|
|
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
|
|
// capacitySink broadcasts capacity_awaken transitions to the Web UI (design/14
|
|
// §5). It is satisfied by *gateway.Server (BroadcastCapacityAwaken); held as an
|
|
// interface so the composition root is unit-testable with a fake sink.
|
|
capacitySink capacityEmitter
|
|
// emotion applies each settled turn to the bound persona's emotion machine and
|
|
// affection (design/01 §2) and hands the changes to moodSink. Nil when no
|
|
// persona was registered, in which case no mood is tracked or persisted.
|
|
emotion *emotion.Observer
|
|
// moodSink broadcasts a mood change to the Web UI (design/07 §3). It is
|
|
// satisfied by *gateway.Server (BroadcastMood); held as an interface so the
|
|
// composition root is unit-testable with a fake sink.
|
|
moodSink moodEmitter
|
|
// traceWriter fans finished trace spans out to the JSONL stderr recorder and,
|
|
// once the gateway exists, to the gateway's observability buffer behind
|
|
// GET /api/v1/admin/observability/trace (design/19 §2). It is held so
|
|
// wireFrontend can register the gateway sink on the already-running tracer.
|
|
traceWriter *traceFanout
|
|
}
|
|
|
|
// newApp assembles the full Cyrene runtime from cfg and log. It opens the
|
|
// database, migrates the schema, wires the persona registry, model adapter, tool
|
|
// dispatcher, memory stack, agent loop, orchestration, execution, IoT, channel
|
|
// sessions/adapters and the gateway, and returns the ready app.
|
|
//
|
|
// Fatal wiring failures (database open/migrate, required auth, gateway) are
|
|
// returned as errors; optional subsystems degrade gracefully so the binary can
|
|
// boot even when a model key or external channel is not configured.
|
|
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(),
|
|
}
|
|
|
|
// Release already-opened resources if any later wiring step fails, without
|
|
// the full-service shutdown log (a partial boot is not a clean shutdown).
|
|
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)
|
|
if err := a.wireChannels(); err != nil {
|
|
return a, err
|
|
}
|
|
if err := a.wireFrontend(); err != nil {
|
|
return a, err
|
|
}
|
|
completed = true
|
|
return a, nil
|
|
}
|