Files
HomeAgent/cmd/homed/main.go
JianFeeeee c4ba7b148c feat(kb-migrate): 存量知识库目录名迁移工具 + 启动期只报告
背景:旧版 Add 整串 sanitize 名字、逐段 sanitize 建目录,留下 tech/_go_/note、
Tech/Upper、a/b with space 这类「知识名与盘上目录不一致」的目录。修复后
normalizeName 要求二者逐字一致,故需一次性改名。

- 迁移逻辑放在 internal/knowledge/migrate_names.go(PlanMigration/
  ApplyMigration),CLI 与 homed 启动**共用同一份实现**,避免口径漂移。
- 三条硬约束:
  1. 默认只报告(-apply 才真改名)—— 批量 os.Rename 不可逆。
  2. 检出目标名冲突(两条迁到同一目标 / 目标已存在)则**整批拒绝**,
     不做部分迁移:半迁移状态比不迁移更难收拾。
  3. 逐条失败不中断,最后统一报告;执行前重查冲突(计划生成与执行之间
     可能有人改过盘上状态),并校验目标不越出知识根。
- homed 启动在 initKnowledgeStore **之前**调 initKnowledgeMigration:改名后
  扫盘一次到位,避免先以旧名建索引再改名造成内存键与盘上目录短暂不一致。
  defaultApply=false ⇒ 启动只扫描+报告+打印可执行命令行,不替人决定。
  单次改名上限 200 条,防失控目录规模拖住启动。

实测:报告模式零改动;冲突场景整批拒绝且盘上原封不动;迁移后 Store
正确载入 4 条并可按规范名逐条删除。
2026-09-26 14:20:19 +08:00

189 lines
6.8 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"
"log"
"path/filepath"
"strings"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
// 空白导入内置 provider:它们各自在 init 里注册到 pkg/embedding。
// 想把核心换成自己的模型,只需替换这一行(或另建一个发行版 main)。
_ "gitcode.com/JianFeeeee/HomeAgent/providers/chineseclip"
_ "gitcode.com/JianFeeeee/HomeAgent/providers/qwen3vl"
)
// main 是 worker 进程的启动序列。
//
// 形状约定:本函数只保留「顺序编排 + 就地交接」——
// - 每个阶段一行调用,参数即该阶段的全部依赖(依赖顺序即调用顺序);
// - 阶段实现体在同包 bootstrap.go,与这里的调用一一对应;
// - 需要逆序释放的资源由阶段函数返回 cleanup,在**原位** defer 注册,
// 因此释放顺序与拆分前完全一致。
func main() {
// 平台门放在最前面:比 flag 解析还早,因为原生 Windows 上根本不应进入任何
// 初始化路径(会去建共享段、拉插件进程)。理由与 WSL 指引见
// platform_windows.go。
requireSupportedPlatform()
opt := parseFlags()
// 父守护模式:只负责拉起/守护 worker,不初始化 agent 内核
if opt.role == "guard" {
runGuard(resolveDataDir(opt.dataDir))
return
}
log.Printf("[homed] role=agent boot=%s", opt.boot)
if opt.dataDir == "" {
opt.dataDir = resolveDataDir(opt.dataDir)
}
if opt.cliSocket == "" {
opt.cliSocket = filepath.Join(opt.dataDir, "cli.sock")
}
logDir := setupLogging(opt.dataDir)
log.Printf("[homed] starting %s", meta.FullVersion())
agentWorkDir := ensureDataDirs(opt.dataDir)
// ---- 基础设施层:记忆、技能 ----
mem, closeMem := initMemoryStack(opt.dataDir)
defer closeMem()
// ---- 配置中心(SQLite 持久化,唯一配置源) ----
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(opt.dataDir, "config.db"))
defer cfgReg.Close()
cfgReg.SeedDefaults(opt.dataDir)
// LLM 配置写前留档(config_set 写 core.llm.* 前自动快照),guard 恢复用基线
cfgReg.SetLLMSnapshotFile(filepath.Join(opt.dataDir, "llm_snapshot.json"))
cfg := cfgReg.ToConfig()
// 共享词嵌入:蒸馏提取(Phase 3 TransE 验证)与 Agent 上下文复用同一实例,
// 避免同一模型被二次加载(约 200k×300 维 ≈ 数百 MB 内存)。
embedder := memory.NewStaticEmbedder(strings.Split(cfgReg.GetString("core.agent.embedding_model_path", ""), ",")...)
mem.distiller.SetEmbedder(embedder)
// ---- Lua VM(LLM 协议适配) ----
luaVM, closeLuaVM := initLuaVM(cfg)
defer closeLuaVM()
// ---- 守护管理(代理生命周期管理) ----
sup := initSupervisor(cfg)
// ---- 变更追踪(overlayfs) ----
trk := initTracker(cfg, agentWorkDir)
// ========================================================================
// 内核 API:IOManager(IO 抽象层) + EventBus(事件总线)
// 所有插件通过这两个通道与核心交互
// ========================================================================
iom, evBus := initKernelAPI()
// ---- 文本记忆 + 记忆蒸馏管线 ----
textMem, closeTextMem := initTextMemory(cfg)
defer closeTextMem()
ctx, stop := context.WithCancel(context.Background())
defer stop()
startMemoryCandidateConsumer(ctx, iom, textMem, mem.db, mem.distiller)
// ---- LLM Provider 管理(多源,通过 Lua 适配器协议转换) ----
baseAPIKey := resolveBaseAPIKey(cfg)
providerMgr := initLLMProviders(cfg, luaVM, baseAPIKey)
provider := providerMgr.Default()
// L1 failback:受限 worker 启动即跑恢复梯子(probe→还原DNS/proxy→还原config+ReloadFromConfig→probe),
// 结果以退出码 exitRecovered=43 / exitRecoveryFailed=44 交回 guard,不进入主 agent 循环。
if opt.boot == "failback" {
runFailbackRecovery(opt.dataDir, cfgReg, luaVM, providerMgr, baseAPIKey)
}
// ---- 文档记忆 + 知识库 ----
docStore, closeDocStore := initDocStore(cfg)
defer closeDocStore()
mediaStore, closeMediaStore := initMediaStore(cfgReg, cfg)
defer closeMediaStore()
multimodalSpace, mmProviderName, mmErr, closeMultimodal := initMultimodalSpace(cfgReg)
defer closeMultimodal()
// 迁移必须在 store 扫盘**之前**:改名后扫盘一次到位,
// 避免先以旧名建索引、再改名造成内存键与盘上目录短暂不一致。
initKnowledgeMigration(cfg)
ks := initKnowledgeStore(cfg)
// ---- 人格设定 ----
personality := loadPersonality(cfg, cfgReg)
// ---- 阶段管道(StageHost)+ 插件系统(Registry) ----
stageHost, pluginReg := newStageAndRegistry(cfg, cfgReg, iom, evBus, mem.db, textMem,
docStore, mediaStore, ks, providerMgr, opt.dataDir)
// ---- Agent Core (需在插件加载前创建,因为插件 Configure 需要 StatusProvider) ----
agent := newMainAgent(cfg, cfgReg, provider, providerMgr, iom, mem.db, mem.indexer, trk,
docStore, ks, mem.social, textMem, mediaStore, personality, pluginReg, embedder,
multimodalSpace, mmProviderName, mmErr, stageHost, evBus)
wirePluginSDK(pluginReg, luaVM, baseAPIKey, sup, trk, cfg, stageHost, mem.indexer, agent)
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
cli.DefaultSocket = opt.cliSocket
// webui 监听地址覆盖:必须在 loadPlugins 之前设置,插件 Start 时会读它。
webui.SetListenOverride(resolveWebUIOverride(cfgReg, opt.httpAddr))
// ---- 依存句法分析器(内嵌 ONNX 模型 / 规则引擎) ----
initONNXParser(cfg, cfgReg)
loadPlugins(cfg, cfgReg, pluginReg, stageHost, opt.boot, opt.dataDir)
// 插件加载完成后再回收空闲页:大值(如老版聊天记录)可能在这一步被搬走/删除,
// 而 SQLite 的 DELETE 不会缩小文件。
compactConfigDB(cfgReg)
stopRuntime := startAgentRuntime(cfgReg, pluginReg, agent, logDir, ctx)
defer stopRuntime()
_, closeIPC := startIPCServer(opt.dataDir, opt.boot, agent)
defer closeIPC()
restartCh := startSupervisorRuntime(sup, trk, agent)
log.Printf("[homed] main agent started, model=%s base=%s sources=%d adapters=%d",
cfg.LLM.Model, cfg.LLM.BaseURL, len(cfg.LLM.Sources), len(luaVM.ListAdapters()))
log.Printf("[homed] kernel ready, waiting for plugin IO...")
// ---- 等待退出信号 ----
stopHeartbeat := startHeartbeat(opt.dataDir, ctx)
waitForShutdown(ctx, opt.dataDir, restartCh, stopHeartbeat, pluginReg, trk, cfgReg, sup)
}