Files
HomeAgent/cmd/homed/main.go
HomeAgent Agent 0beb389223 fix(webui): 设置接口不再吐内部数据;--webui 覆盖生效;端口占用不再静默成功
三处实测确认的缺陷:

① 设置接口整块吐出聊天记录
   plugin.webui.chathistory 是 webui 自己持久化的整段聊天记录(生产实例
   实测 5,176,016 字节),躺在插件配置表里被设置接口当普通配置项整块返回,
   前端还会把它渲染成一个巨大的文本框。
   修复:GET 跳过该键(按插件+键精确判定),PUT 直接 400,避免误改。

② CLI --webui 与 webui.listen_addr 一直是死配置
   内核原本在插件加载前写 settings["addr"],但那时 config_<name> 表还没建
   (表只在插件注册 def 时创建),PluginSettings.Set 的 INSERT 失败,而错误被
   "_ =" 忽略了;随后插件 Start 里 RegisterDef 才建表并写入默认 :8080。
   实测:传 "-webui 127.0.0.1:18099" 仍然监听 :8080。
   修复:覆盖值改由插件自己接收(webui.SetListenOverride,loadPlugins 前调用),
   优先级 CLI > webui.listen_addr(非默认值才算显式配置)> settings["addr"]。
   实测修复后:"-webui 127.0.0.1:18099" 正确监听 18099,与生产的 :8080 并存。

③ 端口被占时 webui 静默死亡
   Start 在后台 goroutine 里 ListenAndServe,先打印 "listening on" 再尝试绑定,
   失败只留一行日志,Start 永远返回 nil → 插件仍被当成加载成功。
   修复:net.Listen 同步做,失败即返回 error(交给加载器/守护),
   成功后才起 Serve,并打印真实绑定地址。
   A/B 实测(两个实例都撞生产的 :8080):
     修复前:"listening on :8080" + "server error: address already in use" + LOADED: webui
     修复后:"[plugin] start webui: webui: 监听 :8080 失败: ...",不再有 LOADED: webui

效果实测(同一实例,先注入 5,271,690 字节 chathistory):
  GET /api/v1/settings   8,244,108 → 28,652 字节(约 1/288)
  meta 条数              5,208 → 105,幻影键 0 条
  设置页仍正常:?prefix=plugin.webui 返回 8 条 def;普通键 PUT 落库;
  校验:GET/PUT 内部键被拒;-webui 覆盖真实生效。

新增测试:TestSettingsNoCrossPluginLeak(跨插件泄漏/幻影键/chathistory 读写)、
TestListenOverrideAndBindFailure(覆盖生效 + 端口占用必须报错)、
TestResolveListenAddrPrecedence(优先级)。
2026-09-14 06:54:16 +08:00

182 lines
6.5 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()
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)
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)
}