mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
重构: 插件自注册 + .so 动态加载 + 中断打断机制
- 所有内置插件 init() 自注册 (plugin.RegisterFactory), 移除 main.go 硬编码 - 新增 .so 动态加载器 (internal/plugin/dynamic.go), 插件可编译为 plugin.so - 新增 plugin.json 元数据 (internal/plugin/manifest.go) - 新增 interceptLoop 独立 goroutine: (a) cancelLLM() 取消进行中的 HTTP 请求 (b) interceptCh → drainInterrupt() 注入 [打断消息] 到 LLM 上下文 (c) InjectInput 空闲时触发新处理循环 - 新增 internal/plugins/all.go 空白导入触发所有内置插件 init() - internal/sdk/ 作为 PluginSDK 正式 Go API - internal/api/ → internal/plugins/webui/ 迁移 - 删除旧 cmd/cli/, 使用 cmd/waiter/ 替代 - 更新 PLAN.md / ARCHITECTURE.md / README.md 文档
This commit is contained in:
@ -13,12 +13,8 @@ import (
|
||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/api"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/config"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugins/test_deepseek"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
luapkg "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
@ -26,38 +22,40 @@ import (
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/onebot"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||
openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/openclaw"
|
||||
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", config.DefaultConfigPath, "path to config file")
|
||||
dataDir := flag.String("data", "/var/lib/homeagent", "data directory")
|
||||
httpAddr := flag.String("webui", ":8080", "webui listen address")
|
||||
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
log.Printf("[homed] starting HomeAgent v0.1.0")
|
||||
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
if *cliSocket == "" {
|
||||
*cliSocket = filepath.Join(*dataDir, "cli.sock")
|
||||
}
|
||||
|
||||
cfg.Daemon.DataDir = *dataDir
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
log.Printf("[homed] starting HomeAgent v0.1.0 (pure kernel)")
|
||||
|
||||
agentWorkDir := filepath.Join(cfg.Daemon.DataDir, "agentfs")
|
||||
agentWorkDir := filepath.Join(*dataDir, "agentfs")
|
||||
dirs := []string{
|
||||
cfg.Daemon.DataDir,
|
||||
filepath.Join(cfg.Daemon.DataDir, "snapshots"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "skills"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "plugins"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "changesets"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "memory"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "memory", "raw"),
|
||||
filepath.Join(cfg.Daemon.DataDir, "adapters"),
|
||||
*dataDir,
|
||||
filepath.Join(*dataDir, "snapshots"),
|
||||
filepath.Join(*dataDir, "skills"),
|
||||
filepath.Join(*dataDir, "plugins"),
|
||||
filepath.Join(*dataDir, "changesets"),
|
||||
filepath.Join(*dataDir, "memory"),
|
||||
filepath.Join(*dataDir, "memory", "raw"),
|
||||
filepath.Join(*dataDir, "adapters"),
|
||||
agentWorkDir,
|
||||
}
|
||||
for _, d := range dirs {
|
||||
@ -66,8 +64,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// === Graph Memory ===
|
||||
memDB, err := memory.NewGraphDB(filepath.Join(cfg.Daemon.DataDir, "memory", "graph.db"))
|
||||
// ========================================================================
|
||||
// 基础设施层:记忆、技能
|
||||
// ========================================================================
|
||||
|
||||
memDB, err := memory.NewGraphDB(filepath.Join(*dataDir, "memory", "graph.db"))
|
||||
if err != nil {
|
||||
log.Printf("[homed] warning: memory init failed: %v", err)
|
||||
memDB = nil
|
||||
@ -79,15 +80,9 @@ func main() {
|
||||
}
|
||||
|
||||
memIdx := memory.NewIndexer(memDB)
|
||||
|
||||
// === Social Store(人物特质与关系网)===
|
||||
socialStore := social.New(memDB)
|
||||
if memDB != nil {
|
||||
log.Printf("[homed] social store initialized")
|
||||
}
|
||||
|
||||
// === Memory Pipeline ===
|
||||
distiller := pipeline.NewDistiller(memDB, cfg.Daemon.DataDir, pipeline.DistillerConfig{
|
||||
distiller := pipeline.NewDistiller(memDB, *dataDir, pipeline.DistillerConfig{
|
||||
Interval: 10 * time.Minute,
|
||||
RetentionDays: 7,
|
||||
BatchSize: 50,
|
||||
@ -97,41 +92,24 @@ func main() {
|
||||
defer distiller.Stop()
|
||||
}
|
||||
|
||||
// === Skills ===
|
||||
skMgr := skill.NewManager(filepath.Join(cfg.Daemon.DataDir, "skills"))
|
||||
skMgr := skill.NewManager(filepath.Join(*dataDir, "skills"))
|
||||
if err := skMgr.Init(); err != nil {
|
||||
log.Printf("[homed] warning: skill init failed: %v", err)
|
||||
}
|
||||
|
||||
// === Plugin Registry (OpenClaw SKILL.md compatible) ===
|
||||
pluginReg := plugin.NewRegistry()
|
||||
// 注册内置原生插件工厂
|
||||
pluginReg.RegisterNative("qq", func(name string, config map[string]interface{}, iom *agentIO.IOManager) (agentIO.Device, error) {
|
||||
wsURL, _ := config["entry"].(string)
|
||||
if wsURL == "" {
|
||||
wsURL = "ws://127.0.0.1:6700"
|
||||
}
|
||||
accessToken, _ := config["access_token"].(string)
|
||||
return onebot.NewDevice(name, wsURL, accessToken, iom), nil
|
||||
})
|
||||
// ========================================================================
|
||||
// 配置中心(SQLite 持久化,唯一配置源)
|
||||
// ========================================================================
|
||||
|
||||
// === Config Registry (统一配置中心,SQLite 持久化) ===
|
||||
// 所有配置收敛到 SQLite,YAML 仅作首次 seed
|
||||
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(cfg.Daemon.DataDir, "config.db"))
|
||||
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(*dataDir, "config.db"))
|
||||
defer cfgReg.Close()
|
||||
cfgReg.SeedFrom(cfg)
|
||||
cfg = cfgReg.ToConfig() // 此后全从 DB 读取
|
||||
cfgReg.SeedDefaults(*dataDir)
|
||||
cfg := cfgReg.ToConfig()
|
||||
|
||||
pluginReg.SetConfigRegistry(cfgReg)
|
||||
log.Printf("[homed] config registry seeded from YAML, %d keys in SQLite", len(cfgReg.List("")))
|
||||
// ========================================================================
|
||||
// Lua VM(LLM 协议适配)
|
||||
// ========================================================================
|
||||
|
||||
// === Supervisor ===
|
||||
sup := supervisor.New(cfg)
|
||||
if err := sup.Start(); err != nil {
|
||||
log.Fatalf("start supervisor: %v", err)
|
||||
}
|
||||
|
||||
// === Lua VM ===
|
||||
luaVM := luapkg.NewVM(filepath.Join(cfg.Daemon.DataDir, "adapters"))
|
||||
if err := luaVM.Start(); err != nil {
|
||||
log.Printf("[homed] warning: lua vm init failed: %v", err)
|
||||
@ -139,66 +117,19 @@ func main() {
|
||||
defer luaVM.Stop()
|
||||
}
|
||||
|
||||
// === IO Abstraction Layer (唯一输入路径) ===
|
||||
iom := agentIO.NewIOManager()
|
||||
// ========================================================================
|
||||
// 守护管理(代理生命周期管理)
|
||||
// ========================================================================
|
||||
|
||||
// 插件绑定 IO 管理器 → 插件自动注册为 IO 设备
|
||||
pluginReg.SetIOManager(iom)
|
||||
// 首次加载插件
|
||||
if result, err := pluginReg.Reload(filepath.Join(cfg.Daemon.DataDir, "plugins")); err != nil {
|
||||
log.Printf("[homed] warning: load plugins: %v", err)
|
||||
} else {
|
||||
log.Printf("[homed] %s", result)
|
||||
sup := supervisor.New(cfg)
|
||||
if err := sup.Start(); err != nil {
|
||||
log.Fatalf("start supervisor: %v", err)
|
||||
}
|
||||
|
||||
// === Text Memory (三层记忆: Context → Text → Graph) ===
|
||||
textMem := text.New(filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
||||
if err := textMem.Start(); err != nil {
|
||||
log.Printf("[homed] warning: text memory start: %v", err)
|
||||
} else {
|
||||
defer textMem.Stop()
|
||||
log.Printf("[homed] text memory active at %s", filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
||||
}
|
||||
// ========================================================================
|
||||
// 变更追踪(overlayfs)
|
||||
// ========================================================================
|
||||
|
||||
// Wire IO output events → TextMemory + distiller → GraphMemory
|
||||
if distiller != nil {
|
||||
go func() {
|
||||
for evt := range iom.OutputChan() {
|
||||
if evt.Target == "memory" && evt.Type == "memory_candidate" {
|
||||
source, _ := evt.Payload["source"].(string)
|
||||
input, _ := evt.Payload["input"].(string)
|
||||
response, _ := evt.Payload["response"].(string)
|
||||
toolsUsed, _ := evt.Payload["tools_used"].([]string)
|
||||
agentID, _ := evt.Payload["agent_id"].(string)
|
||||
|
||||
// 1. 写文本记忆(持久化原始日志)
|
||||
if input != "" && textMem != nil {
|
||||
te := text.Event{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Source: source,
|
||||
Input: input,
|
||||
Response: response,
|
||||
ToolsUsed: toolsUsed,
|
||||
AgentID: agentID,
|
||||
}
|
||||
if err := textMem.Append(te); err != nil {
|
||||
log.Printf("[homed] text memory append: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 喂蒸馏器(生成三元组 → 图记忆)
|
||||
if input != "" {
|
||||
distiller.Append("agent", "user", input)
|
||||
}
|
||||
if response != "" {
|
||||
distiller.Append("agent", "assistant", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// === Change Tracker (overlayfs-based, 追踪所有修改) ===
|
||||
trk := tracker.NewTracker(cfg.Daemon.DataDir, agentWorkDir)
|
||||
if err := trk.Init(); err != nil {
|
||||
log.Printf("[homed] warning: tracker init: %v", err)
|
||||
@ -210,7 +141,64 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// === API Provider Manager(多 LLM 源,通过 Lua 适配器兼容不同 API)===
|
||||
// ========================================================================
|
||||
// 内核 API:IOManager(IO 抽象层) + EventBus(事件总线)
|
||||
// 所有插件通过这两个通道与核心交互
|
||||
// ========================================================================
|
||||
|
||||
iom := agentIO.NewIOManager()
|
||||
evBus := events.NewBus()
|
||||
log.Printf("[homed] kernel API ready: IOManager + EventBus")
|
||||
|
||||
// ========================================================================
|
||||
// 文本记忆 + 记忆蒸馏管线
|
||||
// ========================================================================
|
||||
|
||||
textMem := text.New(filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
||||
if err := textMem.Start(); err != nil {
|
||||
log.Printf("[homed] warning: text memory start: %v", err)
|
||||
} else {
|
||||
defer textMem.Stop()
|
||||
log.Printf("[homed] text memory active at %s", filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
||||
}
|
||||
|
||||
go func() {
|
||||
for evt := range iom.OutputChan() {
|
||||
if evt.Target == "memory" && evt.Type == "memory_candidate" {
|
||||
source, _ := evt.Payload["source"].(string)
|
||||
input, _ := evt.Payload["input"].(string)
|
||||
response, _ := evt.Payload["response"].(string)
|
||||
toolsUsed, _ := evt.Payload["tools_used"].([]string)
|
||||
agentID, _ := evt.Payload["agent_id"].(string)
|
||||
|
||||
if input != "" && textMem != nil {
|
||||
te := text.Event{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Source: source,
|
||||
Input: input,
|
||||
Response: response,
|
||||
ToolsUsed: toolsUsed,
|
||||
AgentID: agentID,
|
||||
}
|
||||
if err := textMem.Append(te); err != nil {
|
||||
log.Printf("[homed] text memory append: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input != "" {
|
||||
distiller.Append("agent", "user", input)
|
||||
}
|
||||
if response != "" {
|
||||
distiller.Append("agent", "assistant", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// ========================================================================
|
||||
// LLM Provider 管理(多源,通过 Lua 适配器协议转换)
|
||||
// ========================================================================
|
||||
|
||||
apiKey := cfg.LLM.APIKey
|
||||
if apiKey == "" {
|
||||
apiKey = os.Getenv("DEEPSEEK_API_KEY")
|
||||
@ -232,13 +220,31 @@ func main() {
|
||||
}, luaVM, src.Adapter)
|
||||
providerMgr.Register(src.Name, luaProvider)
|
||||
}
|
||||
// 默认源由 config 指定
|
||||
if cfg.LLM.Provider != "" {
|
||||
providerMgr.SetDefault(cfg.LLM.Provider)
|
||||
}
|
||||
provider := providerMgr.Default()
|
||||
|
||||
// === Personality (固定人格内核) ===
|
||||
// ========================================================================
|
||||
// 文档记忆 + 知识库
|
||||
// ========================================================================
|
||||
|
||||
docStore := document.NewStore(filepath.Join(cfg.Daemon.DataDir, "memory", "documents"))
|
||||
if err := docStore.Start(); err != nil {
|
||||
log.Printf("[homed] warning: document store: %v", err)
|
||||
}
|
||||
|
||||
ks := knowledge.NewStore(filepath.Join(cfg.Daemon.DataDir, "knowledge"))
|
||||
if err := ks.Start(); err != nil {
|
||||
log.Printf("[homed] warning: knowledge store: %v", err)
|
||||
} else {
|
||||
log.Printf("[homed] knowledge store active with %d items", len(ks.List()))
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 人格设定
|
||||
// ========================================================================
|
||||
|
||||
personalPath := filepath.Join(cfg.Daemon.DataDir, "personal", "personal.md")
|
||||
personality, err := agentPkg.LoadPersonality(personalPath)
|
||||
if err != nil {
|
||||
@ -248,41 +254,55 @@ func main() {
|
||||
log.Printf("[homed] personality loaded (%d bytes)", len(personality.Content))
|
||||
}
|
||||
|
||||
// === Document Memory (第二层记忆:上下文→文档) ===
|
||||
docStore := document.NewStore(filepath.Join(cfg.Daemon.DataDir, "memory", "documents"))
|
||||
if err := docStore.Start(); err != nil {
|
||||
log.Printf("[homed] warning: document store: %v", err)
|
||||
}
|
||||
// ========================================================================
|
||||
// 阶段管道(StageHost)+ 插件系统(Registry)
|
||||
// ========================================================================
|
||||
|
||||
// === Knowledge Store (知识库) ===
|
||||
ks := knowledge.NewStore(filepath.Join(cfg.Daemon.DataDir, "knowledge"))
|
||||
if err := ks.Start(); err != nil {
|
||||
log.Printf("[homed] warning: knowledge store: %v", err)
|
||||
} else {
|
||||
log.Printf("[homed] knowledge store active with %d items", len(ks.List()))
|
||||
}
|
||||
|
||||
// === Event Bus (系统事件总线) ===
|
||||
evBus := events.NewBus()
|
||||
log.Printf("[homed] event bus initialized")
|
||||
|
||||
// === Stage Host (阶段管道编排) ===
|
||||
stageHost := agentCore.NewStageHost()
|
||||
stageHost.SyncFromRegistry(pluginReg)
|
||||
log.Printf("[homed] stage host initialized with %d plugin sdks", pluginReg.SDKPluginCount())
|
||||
|
||||
// === Test DeepSeek Plugin ===
|
||||
tdBus := sdk.NewInProcessBus()
|
||||
tdPlugin := test_deepseek.New(tdBus)
|
||||
// 插件作用域的配置表(RegisterPluginAPI 自动设置 config_test_deepseek 表)
|
||||
if err := pluginReg.RegisterPluginAPI(tdPlugin); err != nil {
|
||||
log.Printf("[homed] warning: register test_deepseek plugin: %v", err)
|
||||
} else {
|
||||
stageHost.RegisterPlugin(tdPlugin)
|
||||
log.Printf("[homed] test_deepseek plugin registered (config table: config_test_deepseek)")
|
||||
pluginReg := plugin.NewRegistry()
|
||||
pluginReg.SetIOManager(iom)
|
||||
pluginReg.SetEventBus(evBus)
|
||||
pluginReg.SetMemory(memDB)
|
||||
pluginReg.SetTextMemory(textMem)
|
||||
pluginReg.SetDocStore(docStore)
|
||||
pluginReg.SetKnowledge(ks)
|
||||
pluginReg.SetProviderManager(providerMgr)
|
||||
pluginReg.SetConfigRegistry(cfgReg)
|
||||
pluginReg.SetPluginDir(filepath.Join(cfg.Daemon.DataDir, "plugins"))
|
||||
|
||||
// Wire registration callbacks: plugins' RegisterTool/RegisterStage → StageHost
|
||||
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
return stageHost.RegisterTool(name, def, handler)
|
||||
})
|
||||
pluginReg.SetStageRegistrar(func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
stageHost.RegisterStage(stage, handler)
|
||||
})
|
||||
pluginReg.SetAPIRegistrar(func(name string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
|
||||
cli.DefaultSocket = *cliSocket
|
||||
openclaw.SkillsDir = filepath.Join(cfg.Daemon.DataDir, "skills")
|
||||
webui.Configure(*httpAddr,
|
||||
sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg, evBus,
|
||||
)
|
||||
|
||||
// Auto-create plugins directory (without hardcoding plugin names)
|
||||
plgDir := filepath.Join(cfg.Daemon.DataDir, "plugins")
|
||||
os.MkdirAll(plgDir, 0755)
|
||||
|
||||
// Load all plugins — each scans its own dir and is loaded via factory or .so
|
||||
if err := pluginReg.Load(plgDir); err != nil {
|
||||
log.Printf("[homed] warning: load plugins: %v", err)
|
||||
}
|
||||
log.Printf("[homed] stage host ready with %d registered tools", stageHost.ToolCount())
|
||||
|
||||
// ========================================================================
|
||||
// Agent Core
|
||||
// ========================================================================
|
||||
|
||||
// === Single Agent Core ===
|
||||
agent := agentCore.New(agentCore.AgentConfig{
|
||||
ID: "main",
|
||||
SystemPrompt: `你是 HomeAgent,一个持续运行的个人管家。
|
||||
@ -306,25 +326,20 @@ func main() {
|
||||
12. llm_list_sources — 列出所有可用的 LLM 源
|
||||
13. llm_set_source — 切换到指定 LLM 源
|
||||
|
||||
当用户问及个人信息或历史时,调用 memory_recall。
|
||||
当用户告诉了你新的个人信息时,调用 memory_commit。
|
||||
当用户提到某个人的性格、喜好或人际关系时,使用 person_set_trait 和 person_relate 记录。
|
||||
需要查询知识时使用 knowledge_search。
|
||||
如需切换 LLM 供应商(如从 DeepSeek 切到 OpenAI),使用 llm_list_sources 查看可用源,再用 llm_set_source 切换。
|
||||
回复你的真实想法,用自然语言与用户交流。`,
|
||||
Provider: provider,
|
||||
ProviderManager: providerMgr,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
Indexer: memIdx,
|
||||
Skills: skMgr,
|
||||
Tracker: trk,
|
||||
MaxToolTurns: 10,
|
||||
DocStore: docStore,
|
||||
Knowledge: ks,
|
||||
SocialStore: socialStore,
|
||||
TextMemory: textMem,
|
||||
Personality: personality,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
Indexer: memIdx,
|
||||
Skills: skMgr,
|
||||
Tracker: trk,
|
||||
MaxToolTurns: 10,
|
||||
DocStore: docStore,
|
||||
Knowledge: ks,
|
||||
SocialStore: socialStore,
|
||||
TextMemory: textMem,
|
||||
Personality: personality,
|
||||
PluginReg: pluginReg,
|
||||
PluginDir: filepath.Join(cfg.Daemon.DataDir, "plugins"),
|
||||
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
||||
@ -334,27 +349,23 @@ func main() {
|
||||
agent.Start()
|
||||
defer agent.Stop()
|
||||
|
||||
// Wire supervisor with tracker + agent registration (after both exist)
|
||||
sup.SetTracker(trk)
|
||||
sup.RegisterAgent("main")
|
||||
|
||||
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...")
|
||||
|
||||
// === Built-in HTTP API & WebUI Plugin ===
|
||||
webui := api.NewWebUIPlugin(
|
||||
"webui", cfg.Daemon.ListenAddr,
|
||||
sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg,
|
||||
)
|
||||
iom.RegisterDevice(webui)
|
||||
webui.Start()
|
||||
defer webui.Stop()
|
||||
// ========================================================================
|
||||
// 等待退出信号
|
||||
// ========================================================================
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
log.Printf("[homed] shutting down...")
|
||||
pluginReg.StopAll()
|
||||
if trk != nil {
|
||||
trk.Stop()
|
||||
}
|
||||
@ -364,5 +375,3 @@ func main() {
|
||||
sup.Shutdown()
|
||||
log.Printf("[homed] stopped")
|
||||
}
|
||||
|
||||
|
||||
|
||||
180
cmd/waiter/main.go
Normal file
180
cmd/waiter/main.go
Normal file
@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
socket := flag.String("socket", "/var/lib/homeagent/cli.sock", "unix socket path")
|
||||
remote := flag.String("remote", "", "remote webui URL (e.g. http://127.0.0.1:8080)")
|
||||
say := flag.String("say", "", "send a message and print response (one-shot, no TUI)")
|
||||
flag.Parse()
|
||||
|
||||
if *say != "" {
|
||||
if *remote != "" {
|
||||
sayRemote(*remote, *say)
|
||||
} else {
|
||||
sayLocal(*socket, *say)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *remote != "" {
|
||||
runRemote(*remote)
|
||||
} else {
|
||||
runLocal(*socket)
|
||||
}
|
||||
}
|
||||
|
||||
// sayLocal sends one message via Unix socket and prints the response
|
||||
func sayLocal(socketPath, message string) {
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect to %s: %v", socketPath, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprintf(conn, "%s\n", message)
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Scan()
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("read: %v", err)
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
||||
fmt.Println(scanner.Text())
|
||||
return
|
||||
}
|
||||
switch resp.Type {
|
||||
case "response":
|
||||
fmt.Println(resp.Content)
|
||||
case "error":
|
||||
log.Fatalf("error: %s", resp.Error)
|
||||
default:
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
// sayRemote sends one message via HTTP and prints the response
|
||||
func sayRemote(baseURL, message string) {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
body := fmt.Sprintf(`{"message":%q}`, message)
|
||||
resp, err := http.Post(baseURL+"/api/v1/chat", "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
log.Fatalf("http post: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
if content, ok := result["response"].(string); ok {
|
||||
fmt.Println(content)
|
||||
}
|
||||
}
|
||||
|
||||
// runLocal starts an interactive TUI via Unix socket
|
||||
func runLocal(socketPath string) {
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect to %s: %v", socketPath, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
var resp struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
||||
fmt.Println(scanner.Text())
|
||||
continue
|
||||
}
|
||||
switch resp.Type {
|
||||
case "response":
|
||||
fmt.Println(resp.Content)
|
||||
case "error":
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", resp.Error)
|
||||
default:
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
fmt.Print("> ")
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
fmt.Print("> ")
|
||||
continue
|
||||
}
|
||||
if line == "/exit" || line == "/quit" {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(conn, "%s\n", line)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(60 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
// runRemote starts an interactive TUI via HTTP
|
||||
func runRemote(baseURL string) {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
fmt.Print("> ")
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
fmt.Print("> ")
|
||||
continue
|
||||
}
|
||||
if line == "/exit" || line == "/quit" {
|
||||
break
|
||||
}
|
||||
body := fmt.Sprintf(`{"message":%q}`, line)
|
||||
resp, err := http.Post(baseURL+"/api/v1/chat", "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
fmt.Print("> ")
|
||||
continue
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if content, ok := result["response"].(string); ok {
|
||||
fmt.Println(content)
|
||||
}
|
||||
fmt.Print("> ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user