diff --git a/.gitignore b/.gitignore index 67474ef..34ddd47 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ third_party/homeagent-sdk/package/ third_party/homeagent-sdk/.gitignore third_party/homeagent-sdk/README* third_party/homeagent-sdk/example/ +.codegraph/ diff --git a/cmd/homed/main.go b/cmd/homed/main.go index 8a139a9..956588d 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -31,10 +31,10 @@ import ( "gitcode.com/JianFeeeee/HomeAgent/internal/nlp" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli" - healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" - openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" - pluginmgr "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" - webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui" + _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" + _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" + _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" + _ "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" @@ -284,7 +284,7 @@ func main() { Temperature: cfg.LLM.Temperature, MaxTokens: cfg.LLM.MaxTokens, ContextWindow: src.ContextWindow, - }, luaVM, src.Adapter) + }, luaVM, src.Name, src.Adapter) providerMgr.Register(src.Name, luaProvider) } if cfg.LLM.Provider != "" { @@ -412,25 +412,30 @@ func main() { InputProcessing: cfg.InputProcessing, }) + // 通过 Registry 将内核依赖注入每个插件的 PluginSDK(阶段6 将替换遗留的 util.Configure) + pluginReg.SetLuaVM(luaVM) + pluginReg.SetBaseAPIKey(baseAPIKey) + pluginReg.SetSupervisor(supervisor.NewSDKAdapter(sup)) + pluginReg.SetSkillManager(skMgr) + pluginReg.SetTracker(trk) + pluginReg.SetConfig(cfg) + pluginReg.SetStageHost(stageHost) + pluginReg.SetIndexer(memIdx) + pluginReg.SetStatusProvider(agent) + // 为内置插件注入内核依赖(各插件通过 init() 自注册工厂) cli.DefaultSocket = *cliSocket - openclaw.SkillsDir = filepath.Join(cfg.Daemon.DataDir, "skills") + // webui 插件作为内置插件经 Registry 启动,读取自身 settings["addr"](默认 :8080)。 + // 保留 CLI --webui 与 webui.listen_addr 配置对监听地址的覆盖。 webuiListenAddr := *httpAddr if webuiListenAddr == "" { webuiListenAddr = cfgReg.GetString("webui.listen_addr", ":8080") } - webui.Configure(webuiListenAddr, - sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg, evBus, agent, - providerMgr, baseAPIKey, - ) - healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, providerMgr, agent) - - // Wire pluginmgr dependencies - pluginmgr.PluginDir = cfg.Plugin.Dir - pluginmgr.Reg = pluginReg - - // CLI 插件结构化命令 — 直接注入内核依赖,不依赖 HTTP - cli.Configure(pluginReg, cfgReg, agent, cfg.Plugin.Dir) + if ps := cfgReg.PluginConfig("webui"); ps != nil { + if v, _ := ps.Get("addr"); v == nil { + _ = ps.Set("addr", webuiListenAddr) + } + } // ======================================================================== // 依存句法分析器(内嵌 ONNX 模型 / 规则引擎) diff --git a/docs/zh/plan.md b/docs/zh/plan.md deleted file mode 100644 index 2e094e2..0000000 --- a/docs/zh/plan.md +++ /dev/null @@ -1,377 +0,0 @@ -# 三元组提取系统 — 施工方案 - -## 一、背景与目标 - -### 现状 - -- HomeAgent 已通过 systemd 托管运行,数据目录 `/home/newqqagent` -- 已积累 **62 万条原始对话记录**(125 个 raw TSV 文件) -- 当前三元组提取通过 `extractKeyTriples()` 硬编码 5 条规则完成(姓名/年龄/喜好/居住地/职业) -- `docToTriples()` 用相邻词机械拼接三元组,语义噪音大 - -### 目标 - -构建 **"句法定界 + 向量验义"** 双路三元组提取系统: - -1. 用本机积累的对话语料训练一个依存句法分析模型 -2. 模型以 ONNX 格式发布到 HuggingFace,Go 运行时启动时拉取 -3. 依赖:Go 侧仅需 `onnxruntime_go`(纯 Go binding,无 CGO/Python) -4. 降级:模型不可用时退回现有 gojieba POS + 模板方案 - ---- - -## 二、整体架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 训练流水线 (Python,一次性) │ -│ │ -│ /home/newqqagent/memory/raw/*.tsv │ -│ │ │ -│ ▼ │ -│ 数据导出 → 提取 user 语句 → 去重 → 句长过滤 │ -│ │ │ -│ ▼ │ -│ Baidu DDParser (教师模型) → 银标依存树 │ -│ │ │ -│ ▼ │ -│ UD Chinese Treebank (金标) + 银标混合 → supar 训练 │ -│ │ │ -│ ▼ │ -│ ONNX 导出 → 上传 HuggingFace (your-org/chinese-dep-parser) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 推理流水线 (Go,运行时) │ -│ │ -│ HomeAgent 启动 │ -│ │ │ -│ ▼ │ -│ HuggingFace 下载 ONNX 模型 → onnxruntime_go 加载 │ -│ │ │ -│ ▼ │ -│ 用户输入 → gojieba 分词 + POS │ -│ │ │ -│ ▼ │ -│ ONNX 推理 → 依存树解码 (head index + dep label) │ -│ │ │ -│ ▼ │ -│ 句法模板提取三元组 (SBV-VOB / SBV-IOB / ATT-VOB / ...) │ -│ │ │ -│ ▼ │ -│ 融合现有向量验证层 (StaticEmbedder + TransE) → 输出三元组 │ -│ │ -│ 模型缺失/加载失败 → 降级 gojieba POS + 模板 │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 三、阶段一:数据导出与探索 - -### 3.1 数据位置 - -``` -/home/newqqagent/memory/raw/raw_*.tsv -格式: id \t session_id \t role \t content \t timestamp -``` - -### 3.2 导出脚本 - -脚本:`tools/export_conversations.py` - -功能: -- 扫描所有 raw_*.tsv,提取 `role=user` 的语句 -- 基础过滤:去除纯标点/极短句(<4 字),按 MD5 去重 -- 输出 JSONL:`{text, session_id, timestamp, length}` -- 统计输出:句长分布直方图、总句数、唯一句数 - -### 3.3 DDParser 快速验证 - -在导出后的数据中随机抽 500 条,用 DDParser 标注后人工抽样检查: -- 依存树的句法合理性(主语/谓语/宾语是否能对齐) -- 常见错误模式(疑问句、省略句、口语化表达) -- 决定需过滤的句式黑名单(如有) - ---- - -## 四、阶段二:训练流水线搭建 - -### 4.1 教师模型标注 - -```python -# 使用 Baidu LAC + DDParser 联合标注 -# LAC:分词 + 词性标注 -# DDParser:依存句法分析 - -文本: "我在杭州读书" -LAC → ['我', '在', '杭州', '读书'] / ['r', 'p', 'ns', 'v'] -DDParser → [{'id':0,'head':2,'deprel':'SBV'}, # 我 → 在(主语) - {'id':1,'head':3,'deprel':'ADV'}, # 在 → 杭州(状语) - {'id':2,'head':3,'deprel':'ADV'}, # 杭州 → 读书(状语) - {'id':3,'head':0,'deprel':'ROOT'}] # 读书 → ROOT -``` - -产出格式:标准 CoNLL-U -``` -1 我 _ r _ _ 2 SBV _ _ -2 在 _ p _ _ 3 ADV _ _ -3 杭州 _ ns _ _ 4 ADV _ _ -4 读书 _ v _ _ 0 ROOT _ _ -``` - -### 4.2 训练方案 - -**框架**: [supar](https://github.com/yzhangcs/parser) (PyTorch, BiLSTM Biaffine) - -**数据组成**: - -| 来源 | 句数 | 标签 | 用途 | -|------|------|------|------| -| UD_Chinese-GSD | ~4K | 金标 | dev/test 锚点 | -| UD_Chinese-HK | ~1K | 金标 | dev/test 锚点 | -| DDParser 标注本机对话 | 10K-20K | 银标 | train 主体 | - -**模型配置**: - -| 参数 | 值 | -|------|-----| -| encoder | BiLSTM | -| hidden | 200 | -| layers | 3 | -| embed_dim | 50 | -| dropout | 0.33 | -| epochs | 50 (early stop) | -| batch_size | 32 | - -**预期指标**: -- LAS (标注依存): ≥80 (金标测试集) -- UAS (未标注依存): ≥85 (金标测试集) - -### 4.3 ONNX 导出 - -```python -torch.onnx.export( - model, - (input_ids, pos_ids, char_ids), - "dep_parser.onnx", - input_names=["input_ids", "pos_ids", "char_ids"], - output_names=["head_logits", "label_logits"], - dynamic_axes={"input_ids": {0: "batch", 1: "seq"}}, -) -``` - -模型包结构: - -``` -dep_parser.onnx # ~15MB -vocab.json # token → id 映射 -pos_vocab.json # POS tag → id 映射 -config.json # 模型超参 + 版本信息 -``` - -### 4.4 发布到 HuggingFace - -```bash -huggingface-cli upload your-org/chinese-dep-parser \ - dep_parser.onnx \ - vocab.json \ - pos_vocab.json \ - config.json \ - --repo-type model -``` - -模型页面附加信息: -- 训练数据来源(UD + HomeAgent 对话语料) -- 模型结构与超参 -- 已验证的输入/输出格式 -- 降级建议 - ---- - -## 五、阶段三:Go 推理集成 - -### 5.1 目录结构 - -``` -internal/nlp/ -├── dep_parser.go # ONNX 模型管理 + 推理 -├── decode.go # 依存解码算法(argmax + MST) -├── triple_extractor.go # 句法模板 → 三元组 -├── fallback.go # gojieba POS + 模板降级 -└── model.go # 数据模型定义 -``` - -### 5.2 模型生命周期管理 - -```go -// 启动时: -// 1. 检查 {dataDir}/models/dep_parser.onnx 是否存在 -// 2. 不存在 → 从 HuggingFace 下载 -// GET https://huggingface.co/your-org/chinese-dep-parser/resolve/main/dep_parser.onnx -// 3. onnxruntime_go.NewDynamicAdvancedModel() 加载 -// 4. 加载失败 → 启用 fallback,日志告警 -// 5. 检查可选的版本更新(按 config.json 的 version 字段) -``` - -### 5.3 推理接口 - -```go -type DepParseResult struct { - Tokens []string // 分词结果 - POS []string // 词性标签 - Heads []int // 每个词的父节点索引(0=ROOT) - DepRels []string // 依存关系标签 -} - -type Triple struct { - Subject string - Relation string - Object string - Score float64 -} - -type Extractor struct { - parser *DepParser - embed *memory.StaticEmbedder -} - -func (e *Extractor) Extract(text string) []Triple { - // 1. DepParser.Parse(text) → DepParseResult - // 2. 句法模板匹配 → 候选三元组 - // 3. 向量验证(cos(h+r, t))→ 过滤 - // 4. 融合打分 → 输出 -} -``` - -### 5.4 句法模板(初版) - -| 模板 | 依存模式 | 先验置信度 | -|------|----------|-----------| -| SBV-VOB | `(SBV) → VOB` | 0.9 | -| SBV-IOB | `(SBV) → IOB → VOB` | 0.85 | -| ATT-VOB | `(ATT) → VOB` | 0.8 | -| SBV-POB | `(SBV) → POB` | 0.75 | -| COO 链 | 并列结构扩展 | 0.6 | - -### 5.5 降级策略 - -| 故障场景 | 行为 | -|---------|------| -| ONNX 模型文件不存在 | 启动时下载,下载失败则进 fallback | -| onnxruntime_go 加载失败 | 日志告警 + 进 fallback | -| 单句推理超时/panic | 返回空三元组,不中断流水线 | -| 全部正常 | 优先 ONNX 模式 | - -Fallback 模式沿用现有的 gojieba POS 局部模板提取(POS 序列匹配),不需要额外依赖。 - ---- - -## 六、阶段四:集成到现有蒸馏管线 - -### 6.1 修改点 - -| 文件 | 改动 | -|------|------| -| `internal/agent/core/distill.go` | `docToTriples()` 改用新 Extractor | -| `internal/memory/pipeline/pipeline.go` | `extractKeyTriples()` 替换为新 Extract | -| `internal/agent/core/process.go` | 系统提示注入时走新提取器(可选) | - -### 6.2 蒸馏管线的三个触发点 - -``` -1. 实时 (process.go): 用户输入经过 NLU 时,即时提取三元组写入 Graph -2. 周期蒸馏 (pipeline.go): 10 分钟心跳,批量处理 7天前的原始记录 -3. 冷文档归档 (distill.go): 72h 未访问的文档 → docToTriples -``` - -新的 `Extractor` 在三个触发点统一使用,上游调用方无需感知底层是 ONNX 还是 fallback。 - ---- - -## 七、时间线 - -| 阶段 | 内容 | 预估工时 | -|------|------|----------| -| 一 | 数据导出 + DDParser 快速验证 | 1 天 | -| 二 | 训练流水线搭建 + v0.1 训练 + ONNX 导出 | 2 天 | -| 三 | Go 推理集成 + 句法模板 | 2 天 | -| 四 | 蒸馏管线接入 + 降级测试 | 1 天 | -| 五 | HuggingFace 发布 + 文档 + 回测 | 1 天 | -| **总计** | | **7 天** | - ---- - -## 八、模型维护策略 - -### 8.1 版本迭代 - -| 版本 | 触发条件 | 训练数据 | -|------|---------|---------| -| v0.1 | 初始版 | UD + 10K 本机对话 | -| v0.2 | 累计 50K 新对话 | 增量合并 retrain | -| v1.0 | 对话域 LAS ≥85 | 全量 + 人工抽检 | - -### 8.2 更新机制 - -``` -HomeAgent 启动 → 检查 HuggingFace 模型版本 - ├── 本地版本 < 远端版本 → 后台下载新模型,下次重启生效 - └── 本地版本 == 远端版本 → 跳过 -``` - -通过 `config.json` 中的 `version` 字段比对,采用先下载后原子替换的策略。 - -### 8.3 回滚 - -``` -/data/newqqagent/models/ -├── dep_parser.onnx # 当前版本 (symlink) -├── dep_parser_v0.1.onnx # 历史版本 -└── dep_parser_v0.2.onnx # 历史版本 -``` - -启动失败时自动 rollback 到上一个可用版本。 - ---- - -## 九、与现有系统的交互 - -### 9.1 Context 向量层关联 - -之前讨论的 **TF-IDF 加权词向量平均** 与三元组提取是两条独立优化线路: - -``` -三元组提取 (本计划) Context 向量 (之前已改完) -───────────────── ──────────────────────── -句法定界 + 向量验义 jieba 精确模式 + TF-IDF 加权 -输出: (sub, rel, obj) 输出: 300d 语义向量 -用于: GraphDB 写入 用于: Context 裁剪评分 -``` - -两者共享 gojieba 分词结果和 StaticEmbedder 词向量,但不直接耦合。 - -### 9.2 向量验证层的复用 - -`StaticEmbedder` 的 `Vectorize()` 可以直接用于 TransE 验证: -```go -h := embed.Vectorize(subject) -r := embed.Vectorize(relation) // 谓语子树语义中心 -t := embed.Vectorize(object) -score := CosineSimilarity(h + r, t) -``` - -无需额外加载词向量模型,与 Context 层在同一向量空间。 - ---- - -## 十、风险与缓解 - -| 风险 | 概率 | 影响 | 缓解 | -|------|------|------|------| -| DDParser 标注质量低 | 中 | 模型学偏 | 混入 UD 金标 + 抽检 500 条先行验证 | -| 对话语料句式单一 | 中 | 泛化差 | 数据增强(依存树扰动/回译) | -| onnxruntime_go 兼容问题 | 低 | Go 侧无法加载 | fallback 模式独立完整,不影响已有功能 | -| 模型体积大 | 低 | 启动慢/占用高 | ~15MB ONNX,可接受 | -| HuggingFace 下载失败 | 低 | 首次启动受阻 | 支持本地预下载 + fallback | diff --git a/internal/agent/api/provider.go b/internal/agent/api/provider.go index e4197d2..275a933 100644 --- a/internal/agent/api/provider.go +++ b/internal/agent/api/provider.go @@ -211,7 +211,7 @@ type LuaAdaptedProvider struct { client *http.Client } -func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAdaptedProvider { +func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, name, adapter string) *LuaAdaptedProvider { if cfg.Temperature == 0 { cfg.Temperature = 0.7 } @@ -219,7 +219,7 @@ func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAda cfg.MaxTokens = 4096 } return &LuaAdaptedProvider{ - name: fmt.Sprintf("lua_%s", adapter), + name: name, cfg: cfg, vm: vm, adapter: adapter, diff --git a/internal/agent/api/quickchat_test.go b/internal/agent/api/quickchat_test.go index b0e1ec6..586b625 100644 --- a/internal/agent/api/quickchat_test.go +++ b/internal/agent/api/quickchat_test.go @@ -28,7 +28,7 @@ func TestQuickChatWithRealKey(t *testing.T) { Model: "deepseek-v4-flash", BaseURL: "https://api.deepseek.com", APIKey: apiKey, - }, vm, "deepseek")) + }, vm, "deepseek", "deepseek")) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/internal/agent/core/status.go b/internal/agent/core/status.go index 77d8808..de243f1 100644 --- a/internal/agent/core/status.go +++ b/internal/agent/core/status.go @@ -21,43 +21,20 @@ type StatusProvider interface { GetKernelStatus() *KernelStatus } -// KernelStatus 内核各子系统运行状态的聚合快照。 -type KernelStatus struct { - Uptime string `json:"uptime"` - StartTime string `json:"start_time"` - - AgentID string `json:"agent_id"` - - Plugins []PluginInfo `json:"plugins"` - Tools []sdk.ToolDef `json:"tools"` - Channels []ChannelInfo `json:"channels"` - - Memory MemoryStatus `json:"memory"` - Knowledge KnowledgeStatus `json:"knowledge"` - Documents DocumentStatus `json:"documents"` - TextMemory TextMemoryStatus `json:"text_memory"` - Social SocialStatus `json:"social"` - Skills SkillsStatus `json:"skills"` - - LLM LLMStatus `json:"llm"` - - Context ContextStatus `json:"context"` - - Runtime RuntimeStatus `json:"runtime"` - - Tracker TrackerStatus `json:"tracker"` -} - -type PluginInfo struct { - Name string `json:"name"` - Loaded bool `json:"loaded"` -} - -type ChannelInfo struct { - Name string `json:"name"` - Type string `json:"type"` - Ready bool `json:"ready"` -} +// 状态 DTO 使用内置 SDK 的中立类型,保证与插件层解耦。 +type KernelStatus = sdk.KernelStatus +type PluginInfo = sdk.PluginInfo +type ChannelInfo = sdk.ChannelInfo +type MemoryStatus = sdk.MemoryStatus +type KnowledgeStatus = sdk.KnowledgeStatus +type DocumentStatus = sdk.DocumentStatus +type TextMemoryStatus = sdk.TextMemoryStatus +type SocialStatus = sdk.SocialStatus +type SkillsStatus = sdk.SkillsStatus +type LLMStatus = sdk.LLMStatus +type ContextStatus = sdk.ContextStatus +type RuntimeStatus = sdk.RuntimeStatus +type TrackerStatus = sdk.TrackerStatus func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo { return ChannelInfo{ @@ -67,61 +44,6 @@ func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo { } } -type MemoryStatus struct { - Available bool `json:"available"` - EntityCount int `json:"entity_count"` - RelationCount int `json:"relation_count"` - EntityTypes int `json:"entity_types"` -} - -type KnowledgeStatus struct { - Available bool `json:"available"` - ItemCount int `json:"item_count"` - Items []string `json:"items,omitempty"` -} - -type DocumentStatus struct { - Available bool `json:"available"` - DocCount int `json:"doc_count"` - VectorCount int `json:"vector_count"` -} - -type TextMemoryStatus struct { - Available bool `json:"available"` - FileCount int `json:"file_count"` -} - -type SocialStatus struct { - Available bool `json:"available"` - PersonCount int `json:"person_count"` -} - -type SkillsStatus struct { - Available bool `json:"available"` - SkillList []string `json:"skill_list,omitempty"` -} - -type LLMStatus struct { - Available bool `json:"available"` - Provider string `json:"provider,omitempty"` - Sources int `json:"sources,omitempty"` -} - -type ContextStatus struct { - EventCount int `json:"event_count,omitempty"` -} - -type RuntimeStatus struct { - Goroutines int `json:"goroutines"` - MemoryMB int64 `json:"memory_mb"` - GoVersion string `json:"go_version"` -} - -type TrackerStatus struct { - Available bool `json:"available"` - Dir string `json:"dir,omitempty"` -} - // collectKernelStatus 聚合内核各子系统状态快照。 // 接收所有子系统引用(均为可选——nil 表示不可用),返回统一的状态报告。 func collectKernelStatus( @@ -300,3 +222,4 @@ func (a *Agent) GetKernelStatus() *KernelStatus { } var _ StatusProvider = (*Agent)(nil) +var _ sdk.StatusAPI = (*Agent)(nil) diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index 04ea414..633377c 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -18,21 +18,23 @@ import ( sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" + luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" + "gitcode.com/JianFeeeee/HomeAgent/internal/skill" + "gitcode.com/JianFeeeee/HomeAgent/internal/tracker" + "gitcode.com/JianFeeeee/HomeAgent/pkg/types" ) type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error) // PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta。 -type PluginMeta struct { - NameZh string `json:"name_zh"` - NameEn string `json:"name_en"` -} +// 与内置 SDK 中的 PluginMeta 保持一致,供 PluginManager 接口返回。 +type PluginMeta = sdk.PluginMeta var globalPluginMeta sync.Map // name -> PluginMeta // RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。 func RegisterPluginMeta(name, nameZh, nameEn string) { - globalPluginMeta.Store(name, PluginMeta{NameZh: nameZh, NameEn: nameEn}) + globalPluginMeta.Store(name, sdk.PluginMeta{NameZh: nameZh, NameEn: nameEn}) } // GetPluginMeta 查询插件的显示名称。 @@ -76,6 +78,8 @@ type Registry struct { mgr *agentAPI.ProviderManager cfgReg *internalConfig.ConfigRegistry plgDir string + lua *luaVM.VM + baseKey string regTool sdk.ToolRegistrar regStage sdk.StageRegistrar @@ -83,6 +87,14 @@ type Registry struct { toolCleaner PluginToolCleaner + status sdk.StatusAPI + sup sdk.SupervisorAPI + skMgr *skill.Manager + trk *tracker.Tracker + cfg *types.Config + stageHost sdk.ToolSource + idx *memory.Indexer + knownDisabled map[string]bool } @@ -104,10 +116,19 @@ func (r *Registry) SetKnowledge(ks *knowledge.Store) { r.ks = ks } func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr } func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg } func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir } +func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm } +func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key } func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn } func (r *Registry) SetStageRegistrar(fn sdk.StageRegistrar) { r.regStage = fn } func (r *Registry) SetAPIRegistrar(fn sdk.APIRegistrar) { r.regAPI = fn } func (r *Registry) SetToolCleaner(tc PluginToolCleaner) { r.toolCleaner = tc } +func (r *Registry) SetStatusProvider(sp sdk.StatusAPI) { r.status = sp } +func (r *Registry) SetSupervisor(sup sdk.SupervisorAPI) { r.sup = sup } +func (r *Registry) SetSkillManager(skMgr *skill.Manager) { r.skMgr = skMgr } +func (r *Registry) SetTracker(trk *tracker.Tracker) { r.trk = trk } +func (r *Registry) SetConfig(cfg *types.Config) { r.cfg = cfg } +func (r *Registry) SetStageHost(sh sdk.ToolSource) { r.stageHost = sh } +func (r *Registry) SetIndexer(idx *memory.Indexer) { r.idx = idx } func (r *Registry) RegisterNative(name string, factory NativeFactory) { r.mu.Lock() @@ -182,7 +203,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK { TextMemory: sdk.NewTextMemory(r.textMem), DocMemory: sdk.NewDocMemory(r.docStore), Knowledge: sdk.NewKnowledge(r.ks), - LLM: sdk.NewLLM(r.mgr), + LLM: sdk.NewLLM(r.mgr, r.cfgReg, r.lua, r.baseKey), Settings: sett, RegTool: regTool, RegStage: regStage, @@ -190,6 +211,15 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK { RegOutput: regOutput, RegInput: regInput, PluginMgr: r, + + Status: r.status, + Supervisor: r.sup, + Skill: r.skMgr, + Adapter: sdk.NewAdapter(r.lua), + Tracker: r.trk, + Config: sdk.NewConfig(r.cfg), + Tool: sdk.NewTool(r.stageHost, r.iom), + Indexer: sdk.NewIndexer(r.idx), }) } @@ -578,15 +608,19 @@ func (r *Registry) ListKnown() []string { return list } -func (r *Registry) PluginMetas() map[string]PluginMeta { - metas := make(map[string]PluginMeta) +func (r *Registry) PluginMetas() map[string]sdk.PluginMeta { + metas := make(map[string]sdk.PluginMeta) globalPluginMeta.Range(func(key, val interface{}) bool { - metas[key.(string)] = val.(PluginMeta) + metas[key.(string)] = val.(sdk.PluginMeta) return true }) return metas } +func (r *Registry) PluginDir() string { + return r.plgDir +} + func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) { // 尝试顺序:.so (Go plugin on Linux) → .dll (Windows) → .lua (跨平台) for _, try := range []struct { diff --git a/internal/plugins/agentcli/plugin_test.go b/internal/plugins/agentcli/plugin_test.go index 5ae77ff..c3e575e 100644 --- a/internal/plugins/agentcli/plugin_test.go +++ b/internal/plugins/agentcli/plugin_test.go @@ -31,7 +31,12 @@ func (tc *toolCapture) RegisterAPI(name string) error { func setupPlugin() (*Plugin, *toolCapture, error) { p := New("agentcli") tc := newToolCapture() - sdk := sdk.New("agentcli", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) + sdk := sdk.New("agentcli", sdk.SDKConfig{ + RegTool: tc.RegisterTool, + RegStage: tc.RegisterStage, + RegAPI: tc.RegisterAPI, + Settings: sdk.NewSettings("agentcli", nil), + }) if err := p.Start(sdk); err != nil { return nil, nil, err } diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go index d9936bd..cb7ae65 100644 --- a/internal/plugins/clawhubadapter/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -31,18 +31,11 @@ var pySimulatorSrc string //go:embed simulator/openclaw_cli.js var openclawCliSrc string -var SkillsDir string -var SimulatorDir string - func init() { plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter") plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) { - dir := SkillsDir - if dir == "" { - dataDir, ok := config["data_dir"].(string) - if !ok { - return nil, fmt.Errorf("clawhubadapter plugin: config missing 'data_dir' or not a string") - } + dir := "" + if dataDir, ok := config["data_dir"].(string); ok && dataDir != "" { dir = filepath.Join(dataDir, "skills") } return New(name, dir), nil @@ -63,8 +56,8 @@ type Plugin struct { } func New(name, skillsDir string) *Plugin { - sd := SimulatorDir - if sd == "" { + sd := "" + if skillsDir != "" { sd = filepath.Join(skillsDir, ".simulator") } return &Plugin{ @@ -100,6 +93,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { p.simulatorDir = s } } + // 默认目录:内核 data_dir 下 skills 目录(与配置 core.daemon.data_dir 对齐) + if p.skillsDir == "" { + if v, _ := s.Settings().GetCore("daemon.data_dir"); v != nil { + if dir, ok := v.(string); ok && dir != "" { + p.skillsDir = filepath.Join(dir, "skills") + } + } + } + if p.skillsDir == "" { + p.skillsDir = filepath.Join("data", "skills") + } + if p.simulatorDir == "" { + p.simulatorDir = filepath.Join(p.skillsDir, ".simulator") + } // Launch OC plugin manager first (handles OC-format plugin installation and lifecycle) os.MkdirAll(p.skillsDir, 0755) @@ -283,7 +290,7 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error { // Ensure skills dir exists for the manager to scan os.MkdirAll(p.skillsDir, 0755) - sp, err := launchProcess("node", managerPath, p.skillsDir, "manager") + sp, err := launchProcess("node", managerPath, p.skillsDir, "manager", p.simulatorDir) if err != nil { return fmt.Errorf("launch manager: %w", err) } @@ -775,7 +782,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error { } } - sp, err := launchProcess(pythonBin, simPath, dir, name) + sp, err := launchProcess(pythonBin, simPath, dir, name, p.simulatorDir) if err != nil { return fmt.Errorf("launch pysimulator: %w", err) } @@ -808,7 +815,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error { } func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error { - sp, err := launchSidecar(dir, name) + sp, err := launchSidecar(dir, name, p.simulatorDir) if err != nil { return fmt.Errorf("launch: %w", err) } diff --git a/internal/plugins/clawhubadapter/sidecar.go b/internal/plugins/clawhubadapter/sidecar.go index 6f694e8..039d88f 100644 --- a/internal/plugins/clawhubadapter/sidecar.go +++ b/internal/plugins/clawhubadapter/sidecar.go @@ -143,15 +143,15 @@ func (s *sidecarProcess) NotifyChan() <-chan OCNotification { return s.notifyCh } -func launchSidecar(dir, name string) (*sidecarProcess, error) { +func launchSidecar(dir, name, simDir string) (*sidecarProcess, error) { mainJS := filepath.Join(dir, "main.js") if _, err := os.Stat(mainJS); os.IsNotExist(err) { return nil, nil } - return launchProcess("node", mainJS, dir, name) + return launchProcess("node", mainJS, dir, name, simDir) } -func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) { +func launchProcess(bin, arg, dir, name, simDir string) (*sidecarProcess, error) { nodePath := bin if bin == "node" { if p := os.Getenv("NODE_PATH"); p != "" { @@ -164,8 +164,8 @@ func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) { cmd.Stderr = os.Stderr // Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command - if SimulatorDir != "" { - binDir := filepath.Join(SimulatorDir, "bin") + if simDir != "" { + binDir := filepath.Join(simDir, "bin") if info, err := os.Stat(binDir); err == nil && info.IsDir() { env := os.Environ() binDirPath := binDir + string(os.PathListSeparator) diff --git a/internal/plugins/clawhubadapter/sidecar_test.go b/internal/plugins/clawhubadapter/sidecar_test.go index 04fb02e..d84063a 100644 --- a/internal/plugins/clawhubadapter/sidecar_test.go +++ b/internal/plugins/clawhubadapter/sidecar_test.go @@ -26,10 +26,12 @@ func (m *mockSettings) RegisterDef(def pubsdk.ConfigDef) {} func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil } func (m *mockSettings) Dump() map[string]interface{} { return nil } func (m *mockSettings) Plugins() []string { return nil } +func (m *mockSettings) DefsCore(prefix string) []*sdk.ConfigDef { return nil } +func (m *mockSettings) DefsPlugin(plugin, prefix string) []*sdk.ConfigDef { return nil } func TestLaunchSidecarNoMainJS(t *testing.T) { tmpDir := t.TempDir() - sp, err := launchSidecar(tmpDir, "nonexistent") + sp, err := launchSidecar(tmpDir, "nonexistent", "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -50,7 +52,7 @@ func TestLaunchSidecarAndListTools(t *testing.T) { t.Fatalf("write test plugin: %v", err) } - sp, err := launchSidecar(tmpDir, "echoplugin") + sp, err := launchSidecar(tmpDir, "echoplugin", "") if err != nil { t.Fatalf("launch sidecar: %v", err) } @@ -92,7 +94,7 @@ func TestCallEchoTool(t *testing.T) { t.Fatalf("write test plugin: %v", err) } - sp, err := launchSidecar(tmpDir, "echoplugin") + sp, err := launchSidecar(tmpDir, "echoplugin", "") if err != nil { t.Fatalf("launch sidecar: %v", err) } @@ -124,7 +126,7 @@ func TestCallAddTool(t *testing.T) { t.Fatalf("write test plugin: %v", err) } - sp, err := launchSidecar(tmpDir, "echoplugin") + sp, err := launchSidecar(tmpDir, "echoplugin", "") if err != nil { t.Fatalf("launch sidecar: %v", err) } @@ -157,7 +159,7 @@ func TestCallNonexistentTool(t *testing.T) { t.Fatalf("write test plugin: %v", err) } - sp, err := launchSidecar(tmpDir, "echoplugin") + sp, err := launchSidecar(tmpDir, "echoplugin", "") if err != nil { t.Fatalf("launch sidecar: %v", err) } @@ -182,7 +184,7 @@ func TestConcurrentCalls(t *testing.T) { t.Fatalf("write test plugin: %v", err) } - sp, err := launchSidecar(tmpDir, "echoplugin") + sp, err := launchSidecar(tmpDir, "echoplugin", "") if err != nil { t.Fatalf("launch sidecar: %v", err) } @@ -217,7 +219,7 @@ func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess { if err != nil { t.Fatalf("abs simulator path: %v", err) } - sp, err := launchProcess("node", simPath, pluginDir, name) + sp, err := launchProcess("node", simPath, pluginDir, name, "") if err != nil { t.Fatalf("launch simulator for %s: %v", name, err) } @@ -338,8 +340,6 @@ func TestLoadOCPluginViaPluginStart(t *testing.T) { } } - SimulatorDir = filepath.Join(t.TempDir(), ".simulator") - p := New("openclaw", skillsDir) var registeredTools []string diff --git a/internal/plugins/cli/plugin.go b/internal/plugins/cli/plugin.go index 01fb925..420aeae 100644 --- a/internal/plugins/cli/plugin.go +++ b/internal/plugins/cli/plugin.go @@ -12,8 +12,6 @@ import ( "strings" "sync" - agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core" - internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) @@ -21,22 +19,6 @@ import ( // DefaultSocket 由 main.go 在 Load() 前设置,覆盖默认 socket 路径。 var DefaultSocket string -// 以下通过 Configure() 注入内核依赖 -var ( - pluginReg *plugin.Registry - cfgReg *internalConfig.ConfigRegistry - statusProv agentCore.StatusProvider - pluginDir string -) - -// Configure 由 main.go 在 Load() 前调用,注入内核依赖供结构化命令使用。 -func Configure(pr *plugin.Registry, cr *internalConfig.ConfigRegistry, sp agentCore.StatusProvider, pDir string) { - pluginReg = pr - cfgReg = cr - statusProv = sp - pluginDir = pDir -} - func init() { plugin.RegisterPluginMeta("cli", "CLI", "CLI") plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) { @@ -189,18 +171,16 @@ func (p *Plugin) cliAPIKey(s *sdk.PluginSDK) string { } } } - return p.webuiAPIKey() + return p.webuiAPIKey(s) } -func (p *Plugin) webuiAPIKey() string { - if cfgReg == nil { +func (p *Plugin) webuiAPIKey(s *sdk.PluginSDK) string { + if s == nil { return "" } - ps := cfgReg.PluginConfig("webui") - if v, _ := ps.Get("api_key"); v != nil { - if s, ok := v.(string); ok { - return s - } + v, _ := s.Settings().GetPlugin("webui", "api_key") + if k, ok := v.(string); ok { + return k } return "" } @@ -215,11 +195,11 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo case "/help": p.cmdHelp(conn) case "/status": - p.cmdStatus(conn) + p.cmdStatus(conn, s) case "/kernel": - p.cmdKernel(conn) + p.cmdKernel(conn, s) case "/settings": - p.cmdSettings(conn, parts) + p.cmdSettings(conn, parts, s) case "/plugin": p.cmdPlugin(conn, parts, s) case "/memory": @@ -227,7 +207,7 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo case "/knowledge": p.cmdKnowledge(conn, s) case "/agents": - p.cmdAgents(conn) + p.cmdAgents(conn, s) default: return false } @@ -260,12 +240,13 @@ func (p *Plugin) cmdHelp(conn net.Conn) { // ======== /status ======== -func (p *Plugin) cmdStatus(conn net.Conn) { - if statusProv == nil { +func (p *Plugin) cmdStatus(conn net.Conn, s *sdk.PluginSDK) { + st := s.Status() + if st == nil { writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"}) return } - ks := statusProv.GetKernelStatus() + ks := st.GetKernelStatus() llmStatus := "不可用" if ks.LLM.Available { @@ -294,19 +275,21 @@ func (p *Plugin) cmdStatus(conn net.Conn) { // ======== /kernel ======== -func (p *Plugin) cmdKernel(conn net.Conn) { - if statusProv == nil { +func (p *Plugin) cmdKernel(conn net.Conn, s *sdk.PluginSDK) { + st := s.Status() + if st == nil { writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"}) return } - data, _ := json.MarshalIndent(statusProv.GetKernelStatus(), "", " ") + data, _ := json.MarshalIndent(st.GetKernelStatus(), "", " ") writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)}) } // ======== /settings ======== -func (p *Plugin) cmdSettings(conn net.Conn, parts []string) { - if cfgReg == nil { +func (p *Plugin) cmdSettings(conn net.Conn, parts []string, s *sdk.PluginSDK) { + sett := s.Settings() + if sett == nil { writeLine(conn, map[string]interface{}{"type": "error", "error": "config registry not available"}) return } @@ -318,7 +301,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) { } key := parts[2] val := strings.Join(parts[3:], " ") - if err := cfgReg.Set(key, val); err != nil { + if err := sett.SetCore(key, val); err != nil { writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()}) return } @@ -330,7 +313,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) { if len(parts) >= 2 { prefix = parts[1] } - keys := cfgReg.List(prefix) + keys, _ := sett.ListCore(prefix) sort.Strings(keys) if len(keys) == 0 { writeLine(conn, map[string]interface{}{"type": "response", "content": "无匹配配置项"}) @@ -338,7 +321,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) { } var lines []string for _, k := range keys { - v, _ := cfgReg.Get(k) + v, _ := sett.GetCore(k) lines = append(lines, fmt.Sprintf(" %s = %v", k, v)) } writeLine(conn, map[string]interface{}{ @@ -409,20 +392,22 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) { writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin remove "}) return } + if pmgr == nil { + writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"}) + return + } name := parts[2] - if pluginDir == "" { + dir := pmgr.PluginDir() + if dir == "" { writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin dir not configured"}) return } - dir := filepath.Join(pluginDir, name) - if err := os.RemoveAll(dir); err != nil { + if err := os.RemoveAll(filepath.Join(dir, name)); err != nil { writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()}) return } // 同步清理禁用表 - if pmgr != nil { - pmgr.EnablePlugin(name) - } + _ = pmgr.EnablePlugin(name) writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)}) case "info": @@ -430,20 +415,33 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) { writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin info "}) return } - if pluginReg == nil { - writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"}) + if pmgr == nil { + writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"}) return } - plg := pluginReg.Get(parts[2]) - if plg == nil { - if pmgr != nil && pmgr.IsPluginDisabled(parts[2]) { - writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", parts[2])}) - return + name := parts[2] + metas := pmgr.PluginMetas() + meta, hasMeta := metas[name] + loaded := false + for _, n := range pmgr.ListLoadedPlugins() { + if n == name { + loaded = true + break } - writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", parts[2])}) + } + if loaded { + display := name + if hasMeta && meta.NameZh != "" { + display = meta.NameZh + } + writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s (%s)\n状态: 已加载", display, name)}) return } - writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s\n状态: 已加载", plg.Name())}) + if pmgr.IsPluginDisabled(name) { + writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", name)}) + return + } + writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", name)}) case "disable": if len(parts) < 3 { @@ -541,12 +539,13 @@ func (p *Plugin) cmdKnowledge(conn net.Conn, s *sdk.PluginSDK) { // ======== /agents ======== -func (p *Plugin) cmdAgents(conn net.Conn) { - if statusProv == nil { +func (p *Plugin) cmdAgents(conn net.Conn, s *sdk.PluginSDK) { + st := s.Status() + if st == nil { writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"}) return } - ks := statusProv.GetKernelStatus() + ks := st.GetKernelStatus() data, _ := json.MarshalIndent(map[string]string{"agent_id": ks.AgentID}, "", " ") writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)}) } diff --git a/internal/plugins/cmd/plugin_test.go b/internal/plugins/cmd/plugin_test.go index 0271c2c..5dfbe38 100644 --- a/internal/plugins/cmd/plugin_test.go +++ b/internal/plugins/cmd/plugin_test.go @@ -34,7 +34,12 @@ func (tc *toolCapture) RegisterAPI(name string) error { return nil } func setupPlugin() (*Plugin, *toolCapture, error) { p := New("cmd") tc := newToolCapture() - sdk := sdk.New("cmd", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) + sdk := sdk.New("cmd", sdk.SDKConfig{ + RegTool: tc.RegisterTool, + RegStage: tc.RegisterStage, + RegAPI: tc.RegisterAPI, + Settings: sdk.NewSettings("cmd", nil), + }) if err := p.Start(sdk); err != nil { return nil, nil, err } diff --git a/internal/plugins/healthcheck/plugin.go b/internal/plugins/healthcheck/plugin.go index e3b8e91..d23d46f 100644 --- a/internal/plugins/healthcheck/plugin.go +++ b/internal/plugins/healthcheck/plugin.go @@ -9,27 +9,10 @@ import ( "sync" "time" - agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" - agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core" - agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" - "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" - "gitcode.com/JianFeeeee/HomeAgent/internal/memory" - doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) -var ( - hcStageHost *agentCore.StageHost - hcIOMgr *agentIO.IOManager - hcPluginReg *plugin.Registry - hcMemory *memory.GraphDB - hcKnowledge *knowledge.Store - hcDocStore *doc.Store - hcProviderMgr *agentAPI.ProviderManager - hcStatusProvider agentCore.StatusProvider -) - type toolInfo struct { Name string `json:"name"` Source string `json:"source"` @@ -50,24 +33,9 @@ type llmReport struct { Detail string `json:"detail,omitempty"` } -func Configure(sh *agentCore.StageHost, iom *agentIO.IOManager, pr *plugin.Registry, - mem *memory.GraphDB, ks *knowledge.Store, ds *doc.Store, pm *agentAPI.ProviderManager, sp agentCore.StatusProvider) { - hcStageHost = sh - hcIOMgr = iom - hcPluginReg = pr - hcMemory = mem - hcKnowledge = ks - hcDocStore = ds - hcProviderMgr = pm - hcStatusProvider = sp -} - func init() { plugin.RegisterPluginMeta("healthcheck", "健康检查", "Health Check") plugin.RegisterFactory("healthcheck", func(name string, config map[string]interface{}) (sdk.Plugin, error) { - if hcStageHost == nil { - return nil, nil - } return New(name), nil }) } @@ -215,7 +183,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { "properties": map[string]interface{}{}, }, }, func(args map[string]interface{}) (interface{}, error) { - return p.listAllTools() + return p.listAllTools(s) }) p.selfToolNames["healthcheck_memory"] = true @@ -227,7 +195,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { "properties": map[string]interface{}{}, }, }, func(args map[string]interface{}) (interface{}, error) { - return p.checkMemory() + return p.checkMemory(s) }) p.selfToolNames["healthcheck_report"] = true @@ -255,7 +223,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { return map[string]interface{}{"ok": true, "received": count}, nil }) - if hcStatusProvider != nil { + if s.Status() != nil { p.selfToolNames["healthcheck_kernel"] = true s.RegisterTool("healthcheck_kernel", sdk.ToolDef{ Name: "healthcheck_kernel", @@ -265,7 +233,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { "properties": map[string]interface{}{}, }, }, func(args map[string]interface{}) (interface{}, error) { - return hcStatusProvider.GetKernelStatus(), nil + return s.Status().GetKernelStatus(), nil }) } @@ -300,9 +268,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { p.startAutoCheck(s, p.autoInterval) } - log.Printf("[healthcheck] ready (stageHost=%v iom=%v reg=%v mem=%v ks=%v ds=%v pm=%v sp=%v)", - hcStageHost != nil, hcIOMgr != nil, hcPluginReg != nil, - hcMemory != nil, hcKnowledge != nil, hcDocStore != nil, hcProviderMgr != nil, hcStatusProvider != nil) + log.Printf("[healthcheck] ready (tool=%v mem=%v ks=%v ds=%v llm=%v plugins=%v status=%v)", + s.Tool() != nil, s.Memory() != nil, s.Knowledge() != nil, + s.DocMemory() != nil, s.LLM() != nil, s.PluginMgr() != nil, s.Status() != nil) return nil } @@ -367,36 +335,32 @@ func (p *Plugin) runAutoCheck(s *sdk.PluginSDK) { func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) { results := []checkResult{} - pluginResult := p.checkPluginsRaw() + pluginResult := p.checkPluginsRaw(s) results = append(results, pluginResult...) - toolResult := p.checkToolsRaw() + toolResult := p.checkToolsRaw(s) results = append(results, toolResult...) - if hcMemory != nil { - r := p.testMemoryRaw() - results = append(results, r) + if s.Memory() != nil { + results = append(results, p.testMemoryRaw(s)) } else { results = append(results, checkResult{Name: "memory", Status: "skip", Detail: "图记忆未初始化", Pass: true}) } - if hcKnowledge != nil { - r := p.testKnowledgeRaw() - results = append(results, r) + if s.Knowledge() != nil { + results = append(results, p.testKnowledgeRaw(s)) } else { results = append(results, checkResult{Name: "knowledge", Status: "skip", Detail: "知识库未初始化", Pass: true}) } - if hcDocStore != nil { - r := p.testDocStoreRaw() - results = append(results, r) + if s.DocMemory() != nil { + results = append(results, p.testDocStoreRaw(s)) } else { results = append(results, checkResult{Name: "documents", Status: "skip", Detail: "文档记忆未初始化", Pass: true}) } - if hcProviderMgr != nil { - r := p.testLLMDriven() - results = append(results, r) + if s.LLM() != nil { + results = append(results, p.testLLMDriven(s)) } else { results = append(results, checkResult{Name: "llm_discovery", Status: "skip", Detail: "LLM Provider 未初始化", Pass: true}) } @@ -424,7 +388,7 @@ func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) { } func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) { - results := p.checkPluginsRaw() + results := p.checkPluginsRaw(s) return map[string]interface{}{ "status": "ok", "plugins": results, @@ -432,12 +396,12 @@ func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) { }, nil } -func (p *Plugin) checkPluginsRaw() []checkResult { - if hcPluginReg == nil { +func (p *Plugin) checkPluginsRaw(s *sdk.PluginSDK) []checkResult { + if s.PluginMgr() == nil { return []checkResult{{Name: "plugins", Status: "skip", Detail: "插件注册表未初始化", Pass: true}} } - names := hcPluginReg.List() + names := s.PluginMgr().ListLoadedPlugins() if names == nil { names = []string{} } @@ -449,8 +413,8 @@ func (p *Plugin) checkPluginsRaw() []checkResult { }} } -func (p *Plugin) listAllTools() (interface{}, error) { - tools := p.collectAllTools() +func (p *Plugin) listAllTools(s *sdk.PluginSDK) (interface{}, error) { + tools := p.collectAllTools(s) return map[string]interface{}{ "status": "ok", "count": len(tools), @@ -458,8 +422,8 @@ func (p *Plugin) listAllTools() (interface{}, error) { }, nil } -func (p *Plugin) checkToolsRaw() []checkResult { - tools := p.collectAllTools() +func (p *Plugin) checkToolsRaw(s *sdk.PluginSDK) []checkResult { + tools := p.collectAllTools(s) return []checkResult{{ Name: "tools", Status: "ok", @@ -468,7 +432,7 @@ func (p *Plugin) checkToolsRaw() []checkResult { }} } -func (p *Plugin) collectAllTools() []toolInfo { +func (p *Plugin) collectAllTools(s *sdk.PluginSDK) []toolInfo { seen := map[string]bool{} var tools []toolInfo @@ -480,14 +444,12 @@ func (p *Plugin) collectAllTools() []toolInfo { tools = append(tools, toolInfo{Name: name, Source: source, Description: desc}) } - if hcStageHost != nil { - for _, def := range hcStageHost.GetToolDefs() { + if s.Tool() != nil { + for _, def := range s.Tool().GetToolDefs() { addTool(def.Name, "plugin", def.Description) } - } - if hcIOMgr != nil { - for _, def := range hcIOMgr.GetAllTools() { + for _, def := range s.Tool().GetAllTools() { addTool(def.Name, "device", def.Description) } } @@ -495,23 +457,18 @@ func (p *Plugin) collectAllTools() []toolInfo { return tools } -func (p *Plugin) testMemoryRaw() checkResult { +func (p *Plugin) testMemoryRaw(s *sdk.PluginSDK) checkResult { marker := fmt.Sprintf("_hc_%d", time.Now().UnixNano()) - triples := []memory.Triple{ + triples := []sdk.Triple{ {Subject: marker, Relation: "is", Object: "healthcheck_test", SubjectType: "System", ObjectType: "Flag"}, } start := time.Now() - ec, rc, err := hcMemory.Commit(triples, "healthcheck", 0) - if err != nil { + if err := s.Memory().Commit(triples); err != nil { return checkResult{Name: "memory_write", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false} } - if _, _, err := hcMemory.Commit(triples, "healthcheck_cleanup", 0); err != nil { - log.Printf("[healthcheck] memory cleanup error: %v", err) - } - - n, err := hcMemory.Purge(map[string]string{"subject_contains": marker}, "hard") + n, err := s.Memory().Purge(map[string]string{"subject_contains": marker}, "hard") if err != nil { return checkResult{Name: "memory_purge", Status: "fail", Detail: fmt.Sprintf("清理失败: %v", err), Pass: false} } @@ -520,25 +477,29 @@ func (p *Plugin) testMemoryRaw() checkResult { return checkResult{ Name: "memory", Status: "ok", - Detail: fmt.Sprintf("写入 %d 实体/%d 关系, 清理 %d 条, 耗时 %v", ec, rc, n, elapsed.Round(time.Millisecond)), + Detail: fmt.Sprintf("写入+清理 %d 条, 耗时 %v", n, elapsed.Round(time.Millisecond)), Pass: true, } } -func (p *Plugin) testKnowledgeRaw() checkResult { +func (p *Plugin) testKnowledgeRaw(s *sdk.PluginSDK) checkResult { marker := fmt.Sprintf("_hc_knowledge_test_%d", time.Now().UnixNano()) start := time.Now() - if err := hcKnowledge.Add(marker, "健康检查测试标记,可忽略"); err != nil { + if err := s.Knowledge().Add(marker, "健康检查测试标记,可忽略"); err != nil { return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false} } - results := hcKnowledge.Search("健康检查测试标记", 3) + results, err := s.Knowledge().Search("健康检查测试标记", 3) + if err != nil { + s.Knowledge().Remove(marker) + return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("查询失败: %v", err), Pass: false} + } elapsed := time.Since(start) // 清理测试条目,避免积累 - hcKnowledge.Remove(marker) + s.Knowledge().Remove(marker) if len(results) > 0 { return checkResult{ @@ -557,20 +518,21 @@ func (p *Plugin) testKnowledgeRaw() checkResult { } } -func (p *Plugin) testDocStoreRaw() checkResult { +func (p *Plugin) testDocStoreRaw(s *sdk.PluginSDK) checkResult { start := time.Now() - doc := &doc.Doc{ - Summary: "健康检查测试文档", + doc := &sdk.Doc{ + Title: fmt.Sprintf("健康检查测试文档 %d", time.Now().UnixNano()), Content: "这是一条由 healthcheck 插件创建的测试文档,用于验证文档记忆系统是否正常工作。", - Tags: []string{"healthcheck", "test"}, - Source: "healthcheck", } - if err := hcDocStore.Insert(doc); err != nil { + if err := s.DocMemory().Insert(doc); err != nil { return checkResult{Name: "documents", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false} } - if doc.ID != "" { - hcDocStore.Remove(doc.ID) + // 清理测试文档,避免积累(SDK Insert 不回填 ID,经 Query 按标题定位) + for _, d := range s.DocMemory().Query("健康检查测试文档", 10) { + if d.ID != "" && strings.HasPrefix(d.Title, "健康检查测试文档") { + s.DocMemory().Remove(d.ID) + } } elapsed := time.Since(start) @@ -582,9 +544,9 @@ func (p *Plugin) testDocStoreRaw() checkResult { } } -func (p *Plugin) testLLMDriven() checkResult { - provider := hcProviderMgr.Default() - if provider == nil { +func (p *Plugin) testLLMDriven(s *sdk.PluginSDK) checkResult { + llmName := s.LLM().CurrentSource() + if llmName == "" { return checkResult{Name: "llm_discovery", Status: "skip", Detail: "无可用 LLM Provider", Pass: true} } @@ -593,7 +555,7 @@ func (p *Plugin) testLLMDriven() checkResult { defer cancel() // 收集所有工具定义(排除健康检查自身的工具以避免循环测试) - toolDefs := p.collectToolDefsForLLM() + toolDefs := p.collectToolDefsForLLM(s) if len(toolDefs) == 0 { return checkResult{Name: "llm_discovery", Status: "skip", Detail: "没有可测试的工具", Pass: true} @@ -608,15 +570,14 @@ func (p *Plugin) testLLMDriven() checkResult { // 构建 prompt prompt := p.buildDiscoveryPrompt(toolDefs) - msgs := []agentAPI.Message{{Role: "user", Content: prompt}} + msgs := []sdk.LLMMessage{{Role: "user", Content: prompt}} tools := convertToolDefs(toolDefs) - llmName := provider.Name() turnCount := 0 toolCallCount := 0 for turn := 0; turn < p.llmMaxTurns; turn++ { - resp, err := provider.Chat(ctx, &agentAPI.CompletionRequest{ + resp, err := s.LLM().Chat(ctx, &sdk.LLMCompletionRequest{ Messages: msgs, MaxTokens: p.llmMaxTokens, Tools: tools, @@ -638,12 +599,12 @@ func (p *Plugin) testLLMDriven() checkResult { break } - msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls}) + msgs = append(msgs, sdk.LLMMessage{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls}) for _, tc := range resp.ToolCalls { toolCallCount++ - content := p.executeToolForLLM(tc) - msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: content}) + content := p.executeToolForLLM(s, tc) + msgs = append(msgs, sdk.LLMMessage{Role: "tool", ToolCallID: tc.ID, Content: content}) } } @@ -666,7 +627,7 @@ func (p *Plugin) testLLMDriven() checkResult { // collectToolDefsForLLM 收集全部已注册的工具定义供 LLM 发现和测试。 // 动态排除本插件自身注册的工具(通过 selfToolNames),避免 LLM 自我循环调用。 -func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef { +func (p *Plugin) collectToolDefsForLLM(s *sdk.PluginSDK) []sdk.ToolDef { seen := map[string]bool{} var defs []sdk.ToolDef @@ -678,14 +639,12 @@ func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef { defs = append(defs, d) } - if hcStageHost != nil { - for _, d := range hcStageHost.GetToolDefs() { + if s.Tool() != nil { + for _, d := range s.Tool().GetToolDefs() { addDef(d) } - } - if hcIOMgr != nil { - for _, d := range hcIOMgr.GetAllTools() { - addDef(sdk.ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters}) + for _, d := range s.Tool().GetAllTools() { + addDef(d) } } @@ -715,10 +674,10 @@ func (p *Plugin) buildDiscoveryPrompt(toolDefs []sdk.ToolDef) string { } // executeToolForLLM 在 LLM 工具循环中执行工具调用。 -// healthcheck_report 通过 StageHost 路由到自身注册的 handler,负责收集 LLM 上报。 -func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string { - if hcStageHost != nil { - result, err := hcStageHost.ExecuteTool(tc.Name, tc.Arguments) +// healthcheck_report 经 SDK ToolAPI 路由到自身注册的 handler,负责收集 LLM 上报。 +func (p *Plugin) executeToolForLLM(s *sdk.PluginSDK, tc sdk.LLMToolCall) string { + if s.Tool() != nil { + result, err := s.Tool().ExecuteTool(tc.Name, tc.Arguments) if err != nil { return fmt.Sprintf("调用工具 %s 失败: %v", tc.Name, err) } @@ -726,7 +685,7 @@ func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string { return string(data) } - return fmt.Sprintf("工具 %s 不可执行(StageHost 未初始化)", tc.Name) + return fmt.Sprintf("工具 %s 不可执行(工具注册表未初始化)", tc.Name) } func convertToolDefs(defs []sdk.ToolDef) []interface{} { @@ -744,11 +703,11 @@ func convertToolDefs(defs []sdk.ToolDef) []interface{} { return tools } -func (p *Plugin) checkMemory() (interface{}, error) { - if hcMemory == nil { +func (p *Plugin) checkMemory(s *sdk.PluginSDK) (interface{}, error) { + if s.Memory() == nil { return map[string]interface{}{"status": "skip", "pass": true, "detail": "图记忆未初始化"}, nil } - r := p.testMemoryRaw() + r := p.testMemoryRaw(s) c := map[string]interface{}{ "status": r.Status, "pass": r.Pass, diff --git a/internal/plugins/healthcheck/plugin_test.go b/internal/plugins/healthcheck/plugin_test.go index e42b650..12208c8 100644 --- a/internal/plugins/healthcheck/plugin_test.go +++ b/internal/plugins/healthcheck/plugin_test.go @@ -10,7 +10,6 @@ import ( "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" "gitcode.com/JianFeeeee/HomeAgent/internal/memory" doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document" - "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) @@ -34,16 +33,28 @@ func (tc *toolCapture) RegisterTool(name string, def sdk.ToolDef, handler sdk.To func (tc *toolCapture) RegisterStage(stage sdk.Stage, handler sdk.StageHandler) {} func (tc *toolCapture) RegisterAPI(name string) error { return nil } -func setupPlugin() (*Plugin, *toolCapture, error) { - sh := agentCore.NewStageHost() - iom := agentIO.NewIOManager() - pr := plugin.NewRegistry() +func newTestSDK(cfg sdk.SDKConfig) *sdk.PluginSDK { + if cfg.Settings == nil { + cfg.Settings = sdk.NewSettings("healthcheck", nil) + } + if cfg.Tool == nil { + cfg.Tool = sdk.NewTool(agentCore.NewStageHost(), agentIO.NewIOManager()) + } + return sdk.New("healthcheck", cfg) +} - Configure(sh, iom, pr, nil, nil, nil, nil, nil) - p := New("healthcheck") +func setupPlugin() (*Plugin, *toolCapture, error) { + return setupPluginWith(sdk.SDKConfig{}) +} + +func setupPluginWith(cfg sdk.SDKConfig) (*Plugin, *toolCapture, error) { tc := newToolCapture() - sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) - if err := p.Start(sdk); err != nil { + cfg.RegTool = tc.RegisterTool + cfg.RegStage = tc.RegisterStage + cfg.RegAPI = tc.RegisterAPI + p := New("healthcheck") + s := newTestSDK(cfg) + if err := p.Start(s); err != nil { return nil, nil, err } return p, tc, nil @@ -185,15 +196,8 @@ func TestHealthcheckWithMemory(t *testing.T) { } defer memDB.Close() - sh := agentCore.NewStageHost() - iom := agentIO.NewIOManager() - pr := plugin.NewRegistry() - - Configure(sh, iom, pr, memDB, nil, nil, nil, nil) - p := New("healthcheck") - tc := newToolCapture() - sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) - if err := p.Start(sdk); err != nil { + _, tc, err := setupPluginWith(sdk.SDKConfig{Memory: sdk.NewGraphMemory(memDB)}) + if err != nil { t.Fatal(err) } @@ -228,15 +232,8 @@ func TestHealthcheckWithKnowledge(t *testing.T) { } defer ks.Stop() - sh := agentCore.NewStageHost() - iom := agentIO.NewIOManager() - pr := plugin.NewRegistry() - - Configure(sh, iom, pr, nil, ks, nil, nil, nil) - p := New("healthcheck") - tc := newToolCapture() - sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) - if err := p.Start(sdk); err != nil { + _, tc, err := setupPluginWith(sdk.SDKConfig{Knowledge: sdk.NewKnowledge(ks)}) + if err != nil { t.Fatal(err) } @@ -282,15 +279,8 @@ func TestHealthcheckWithDocStore(t *testing.T) { } defer ds.Stop() - sh := agentCore.NewStageHost() - iom := agentIO.NewIOManager() - pr := plugin.NewRegistry() - - Configure(sh, iom, pr, nil, nil, ds, nil, nil) - p := New("healthcheck") - tc := newToolCapture() - sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI}) - if err := p.Start(sdk); err != nil { + _, tc, err := setupPluginWith(sdk.SDKConfig{DocMemory: sdk.NewDocMemory(ds)}) + if err != nil { t.Fatal(err) } @@ -340,11 +330,4 @@ func TestLLMReportCollection(t *testing.T) { } } -func TestConfigureNilStageHost(t *testing.T) { - Configure(nil, nil, nil, nil, nil, nil, nil, nil) - if hcStageHost != nil { - t.Fatal("expected hcStageHost to be nil") - } -} - diff --git a/internal/plugins/integration_test.go b/internal/plugins/integration_test.go index 6fba740..70d3293 100644 --- a/internal/plugins/integration_test.go +++ b/internal/plugins/integration_test.go @@ -10,15 +10,13 @@ import ( agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core" agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" + internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" "gitcode.com/JianFeeeee/HomeAgent/internal/memory" doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli" - healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" - openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" - webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) @@ -77,11 +75,17 @@ func setupIntegrationWithProvider(t *testing.T, pm *agentAPI.ProviderManager) *t t.Fatal(err) } + pluginReg.SetStageHost(stageHost) + pluginReg.SetProviderManager(pm) + pluginReg.SetKnowledge(ks) + pluginReg.SetDocStore(docStore) + cli.DefaultSocket = filepath.Join(tmpDir, "cli.sock") - openclaw.SkillsDir = filepath.Join(tmpDir, "skills") - os.MkdirAll(openclaw.SkillsDir, 0755) - webui.Configure(":0", nil, memDB, nil, nil, nil, iom, nil, ks, nil, nil, pluginReg, nil, nil) - healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, pm, nil) + + // 经 ConfigRegistry 装配内核路径配置(clawhubadapter/pluginmgr 等经 SDK settings 读取) + cfgReg := internalConfig.NewConfigRegistry("") + cfgReg.SeedDefaults(tmpDir) + pluginReg.SetConfigRegistry(cfgReg) plgDir := filepath.Join(tmpDir, "plugins") os.MkdirAll(plgDir, 0755) @@ -229,7 +233,7 @@ func TestIntegrationCmdRunStderr(t *testing.T) { defer env.cleanup() result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{ - "command": "echo stderr_test >&2", + "command": "sh -c \"echo stderr_test >&2\"", }) if err != nil { t.Fatal(err) @@ -520,7 +524,7 @@ func TestIntegrationLLMDrivenDiscoveryWithRealKey(t *testing.T) { Model: "deepseek-v4-flash", BaseURL: "https://api.deepseek.com", APIKey: apiKey, - }, vm, "deepseek")) + }, vm, "deepseek", "deepseek")) // Setup — 加载所有真实内置插件 env := setupIntegrationWithProvider(t, pm) diff --git a/internal/plugins/pluginmgr/plugin.go b/internal/plugins/pluginmgr/plugin.go index 460b62c..2adc1b4 100644 --- a/internal/plugins/pluginmgr/plugin.go +++ b/internal/plugins/pluginmgr/plugin.go @@ -66,11 +66,7 @@ var downloadClient = &http.Client{ }, } -var ( - PluginDir string // 由 main.go 设置 - Reg *plugin.Registry // 由 main.go 设置 - HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 main.go 覆写或 settings 配置 -) +var HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 settings 配置 func init() { plugin.RegisterPluginMeta("pluginmgr", "插件管理", "Plugin Manager") @@ -80,12 +76,14 @@ func init() { } type Plugin struct { - name string - mu sync.Mutex - server *http.Server - mux *http.ServeMux - listen net.Listener - httpURL string + name string + mu sync.Mutex + server *http.Server + mux *http.ServeMux + listen net.Listener + httpURL string + sdk *sdk.PluginSDK + pluginDir string } func New(name string) *Plugin { @@ -96,6 +94,7 @@ func (p *Plugin) Name() string { return p.name } func (p *Plugin) Start(s *sdk.PluginSDK) error { s.SetAutoRestart(true) + p.sdk = s s.Settings().RegisterDef(sdk.ConfigDef{ Key: "http_addr", Default: HTTPAddr, @@ -111,6 +110,12 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { } } + if v, _ := s.Settings().GetCore("plugin.dir"); v != nil { + if dir, ok := v.(string); ok && dir != "" { + p.pluginDir = dir + } + } + p.registerTools(s) if HTTPAddr != "" { @@ -374,7 +379,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) { }, nil } - dir := PluginDir + dir := p.pluginDir if dir == "" { return map[string]interface{}{"error": "plugin dir not configured"}, nil } @@ -409,7 +414,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) { } func (p *Plugin) listPlugins() (interface{}, error) { - dir := PluginDir + dir := p.pluginDir if dir == "" { return []map[string]interface{}{}, nil } @@ -446,7 +451,7 @@ func (p *Plugin) listPlugins() (interface{}, error) { } func (p *Plugin) removePlugin(name string) (interface{}, error) { - dir := filepath.Join(PluginDir, name) + dir := filepath.Join(p.pluginDir, name) if _, err := os.Stat(dir); os.IsNotExist(err) { return map[string]interface{}{"error": "plugin not found", "name": name}, nil } @@ -456,8 +461,10 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) { } // 同步清理禁用表 - if Reg != nil { - Reg.EnablePlugin(name) + if p.sdk != nil && p.sdk.PluginMgr() != nil { + if err := p.sdk.PluginMgr().EnablePlugin(name); err != nil { + log.Printf("[pluginmgr] enable %s after remove: %v", name, err) + } } return map[string]interface{}{ @@ -468,7 +475,7 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) { } func (p *Plugin) pluginInfo(name string) (interface{}, error) { - dir := filepath.Join(PluginDir, name) + dir := filepath.Join(p.pluginDir, name) m, err := plugin.ReadManifest(dir) if err != nil { return nil, fmt.Errorf("plugin %q not found", name) diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go index 6c67449..6ea8f48 100644 --- a/internal/plugins/webui/handler.go +++ b/internal/plugins/webui/handler.go @@ -9,28 +9,13 @@ import ( "io" "log" "net/http" - "os" "sort" "strconv" "strings" "sync" "time" - agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" - agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core" - agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" - internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" - "gitcode.com/JianFeeeee/HomeAgent/internal/events" - "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" - "gitcode.com/JianFeeeee/HomeAgent/internal/meta" - luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" - "gitcode.com/JianFeeeee/HomeAgent/internal/memory" - "gitcode.com/JianFeeeee/HomeAgent/internal/memory/text" - "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" 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/pkg/types" ) @@ -49,26 +34,24 @@ func init() { } type Handler struct { - supervisor *supervisor.Daemon - memory *memory.GraphDB - indexer *memory.Indexer - skills *skill.Manager - lua *luaVM.VM - config *types.Config - startTime time.Time - iom *agentIO.IOManager - textMem *text.Memory - knowledge *knowledge.Store - tracker *tracker.Tracker - cfgReg *internalConfig.ConfigRegistry - pluginReg *plugin.Registry - pluginMgr sdk.PluginManager - eventBus *events.Bus - statusProvider agentCore.StatusProvider - providerMgr *agentAPI.ProviderManager - baseAPIKey string - sessionMu sync.Mutex - sessions map[string]time.Time + sdk *sdk.PluginSDK + supervisor sdk.SupervisorAPI + memory sdk.MemoryAPI + indexer sdk.IndexerAPI + skills sdk.SkillAPI + adapter sdk.AdapterAPI + config sdk.ConfigAPI + startTime time.Time + textMem sdk.TextMemoryAPI + knowledge sdk.KnowledgeAPI + tracker sdk.TrackerAPI + settings sdk.SettingsAPI + pluginMgr sdk.PluginManager + status sdk.StatusAPI + llm sdk.LLMAPI + + sessionMu sync.Mutex + sessions map[string]time.Time chatMu sync.Mutex chatHistory []ChatMsg @@ -104,51 +87,64 @@ type termState struct { created time.Time } -func (h *Handler) SetPluginMgr(mgr sdk.PluginManager) { h.pluginMgr = mgr } - const maxChatHistory = 200 const maxCmdHistory = 100 const maxTerminals = 50 -func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string) *Handler { - var idx *memory.Indexer - if mem != nil { - idx = memory.NewIndexer(mem) +func NewHandler(s *sdk.PluginSDK) *Handler { + var ( + sup sdk.SupervisorAPI + mem sdk.MemoryAPI + idx sdk.IndexerAPI + sk sdk.SkillAPI + ad sdk.AdapterAPI + cfg sdk.ConfigAPI + tm sdk.TextMemoryAPI + ks sdk.KnowledgeAPI + tr sdk.TrackerAPI + se sdk.SettingsAPI + pm sdk.PluginManager + st sdk.StatusAPI + llm sdk.LLMAPI + ) + if s != nil { + sup, mem, idx = s.Supervisor(), s.Memory(), s.Indexer() + sk, ad, cfg = s.Skill(), s.Adapter(), s.Config() + tm, ks, tr = s.TextMemory(), s.Knowledge(), s.Tracker() + se, pm = s.Settings(), s.PluginMgr() + st, llm = s.Status(), s.LLM() } h := &Handler{ - supervisor: sup, - memory: mem, - indexer: idx, - skills: sk, - lua: lua, - config: cfg, - startTime: time.Now(), - iom: iom, - textMem: tm, - knowledge: ks, - tracker: tr, - cfgReg: cr, - pluginReg: pr, - eventBus: evBus, - statusProvider: sp, - providerMgr: pm, - baseAPIKey: baseKey, - sessions: make(map[string]time.Time), - termStates: make(map[string]*termState), + sdk: s, + supervisor: sup, + memory: mem, + indexer: idx, + skills: sk, + adapter: ad, + config: cfg, + startTime: time.Now(), + textMem: tm, + knowledge: ks, + tracker: tr, + settings: se, + pluginMgr: pm, + status: st, + llm: llm, + sessions: make(map[string]time.Time), + termStates: make(map[string]*termState), } h.loadChatHistory() - if evBus != nil { + if s != nil { go h.trackToolEvents() } return h } func (h *Handler) loadChatHistory() { - if h.cfgReg == nil { + if h.settings == nil { return } - ps := h.cfgReg.PluginConfig("webui") - v, err := ps.Get("chathistory") + v, err := h.settings.Get("chathistory") if err != nil || v == nil { return } @@ -166,12 +162,15 @@ func (h *Handler) loadChatHistory() { } func (h *Handler) trackToolEvents() { - h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) { + if h.sdk == nil { + return + } + h.sdk.Subscribe(sdk.EventToolCall, func(ev *sdk.Event) { h.handleToolEvent(ev) }) } -func (h *Handler) handleToolEvent(ev *events.Event) { +func (h *Handler) handleToolEvent(ev *sdk.Event) { payload := ev.Payload tool, _ := payload["tool"].(string) args, _ := payload["args"].(map[string]interface{}) @@ -235,20 +234,19 @@ func getStr(m map[string]interface{}, key string) string { func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) { ttl = 24 * time.Hour - if h.cfgReg == nil { + if h.settings == nil { return } - ps := h.cfgReg.PluginConfig("webui") - if v, _ := ps.Get("api_key"); v != nil { + if v, _ := h.settings.Get("api_key"); v != nil { apiKey, _ = v.(string) } - if v, _ := ps.Get("username"); v != nil { + if v, _ := h.settings.Get("username"); v != nil { username, _ = v.(string) } - if v, _ := ps.Get("password"); v != nil { + if v, _ := h.settings.Get("password"); v != nil { password, _ = v.(string) } - if v, _ := ps.Get("session_ttl_hours"); v != nil { + if v, _ := h.settings.Get("session_ttl_hours"); v != nil { switch n := v.(type) { case float64: if n > 0 { ttl = time.Duration(n) * time.Hour } @@ -433,12 +431,15 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - agents := h.supervisor.ListAgents() + agentCount := 0 + if h.supervisor != nil { + agentCount = len(h.supervisor.ListAgents()) + } writeJSON(w, http.StatusOK, map[string]interface{}{ "status": "running", "uptime": time.Since(h.startTime).Round(time.Second).String(), - "agents": len(agents), - "version": meta.Version, + "agents": agentCount, + "version": sdk.SDKVersion, "startedAt": h.startTime, }) } @@ -448,16 +449,20 @@ func (h *Handler) handleKernel(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - if h.statusProvider == nil { + if h.status == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "kernel status provider not available"}) return } - writeJSON(w, http.StatusOK, h.statusProvider.GetKernelStatus()) + writeJSON(w, http.StatusOK, h.status.GetKernelStatus()) } func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: + if h.supervisor == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"}) + return + } agents := h.supervisor.ListAgents() writeJSON(w, http.StatusOK, map[string]interface{}{"agents": agents}) case http.MethodPost: @@ -470,7 +475,10 @@ func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent id is required"}) return } - h.config.Agents = append(h.config.Agents, cfg) + if h.config != nil { + kcfg := h.config.Get() + kcfg.Agents = append(kcfg.Agents, cfg) + } writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -484,7 +492,11 @@ func (h *Handler) handleAgentByID(w http.ResponseWriter, r *http.Request) { if len(parts) == 1 { switch r.Method { case http.MethodGet: - status, err := h.supervisor.GetAgentStatus(agentID) + if h.supervisor == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"}) + return + } + status, err := h.supervisor.GetAgentStatus(string(agentID)) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()}) return @@ -515,7 +527,11 @@ func (h *Handler) handleSnapshots(w http.ResponseWriter, r *http.Request, agentI case http.MethodGet: writeJSON(w, http.StatusOK, map[string]interface{}{"agent_id": agentID, "snapshots": []map[string]interface{}{}}) case http.MethodPost: - snap, err := h.supervisor.PreActionSnapshot(agentID) + if h.supervisor == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"}) + return + } + snap, err := h.supervisor.PreActionSnapshot(string(agentID)) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return @@ -531,8 +547,12 @@ func (h *Handler) handleRollback(w http.ResponseWriter, r *http.Request, agentID http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if h.supervisor == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"}) + return + } snapID := types.SnapshotID(parts[2]) - if err := h.supervisor.RollbackAgent(agentID, snapID); err != nil { + if err := h.supervisor.RollbackAgent(string(agentID), string(snapID)); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } @@ -598,28 +618,25 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) { if depth <= 0 { depth = 2 } - result, err := h.memory.Recall(keywords, nil, depth, "") + entities, relations, err := h.memory.Recall(keywords, depth) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - writeJSON(w, http.StatusOK, result) + writeJSON(w, http.StatusOK, map[string]interface{}{"entities": entities, "relations": relations}) case http.MethodPost: var req struct { - Triples []memory.Triple `json:"triples"` - SessionID string `json:"session_id"` - TurnID int `json:"turn_id"` + Triples []sdk.Triple `json:"triples"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) return } - ec, rc, err := h.memory.Commit(req.Triples, req.SessionID, req.TurnID) - if err != nil { + if err := h.memory.Commit(req.Triples); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - writeJSON(w, http.StatusCreated, map[string]int{"entities_created": ec, "relations_created": rc}) + writeJSON(w, http.StatusCreated, map[string]interface{}{"status": "committed", "committed": len(req.Triples)}) case http.MethodDelete: var req struct { Criteria map[string]string `json:"criteria"` @@ -650,7 +667,11 @@ func (h *Handler) handleMemoryContext(w http.ResponseWriter, r *http.Request) { return } userInput := r.URL.Query().Get("q") - injected := h.indexer.BuildContext(userInput) + injected, err := h.indexer.BuildContext(userInput) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } writeJSON(w, http.StatusOK, map[string]interface{}{ "context": h.indexer.FormatContext(injected), "summary": injected.Summary, @@ -701,12 +722,13 @@ func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) { } query := r.URL.Query().Get("q") if query != "" { - results := h.knowledge.Search(query, 10) + results, _ := h.knowledge.Search(query, 10) writeJSON(w, http.StatusOK, map[string]interface{}{"results": results}) return } + categories, _ := h.knowledge.List() writeJSON(w, http.StatusOK, map[string]interface{}{ - "categories": h.knowledge.List(), + "categories": categories, "stats": h.knowledge.Stats(), }) @@ -795,13 +817,13 @@ func (h *Handler) handleTextMemory(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) { - if h.lua == nil { + if h.adapter == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"}) return } switch r.Method { case http.MethodGet: - writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.lua.ListAdapters()}) + writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.adapter.List()}) case http.MethodPost: var req struct { Name string `json:"name"` @@ -811,12 +833,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) return } - path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), req.Name) - if err := os.WriteFile(path, []byte(req.Code), 0644); err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - if err := h.lua.LoadAdapter(path); err != nil { + if err := h.adapter.Load(req.Name, req.Code); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } @@ -827,7 +844,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) { - if h.lua == nil { + if h.adapter == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"}) return } @@ -838,7 +855,7 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) { } switch r.Method { case http.MethodGet: - for _, a := range h.lua.ListAdapters() { + for _, a := range h.adapter.List() { if a.Name == name { writeJSON(w, http.StatusOK, a) return @@ -846,12 +863,10 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) { } http.NotFound(w, r) case http.MethodDelete: - path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), name) - if err := os.Remove(path); err != nil { + if err := h.adapter.Remove(name); err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"}) return } - h.lua.RemoveAdapter(name) writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -865,7 +880,7 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusOK, map[string]interface{}{ "network_status": "monitoring", - "endpoints": h.config.Defaults.LLMEndpoints, + "endpoints": h.config.Get().Defaults.LLMEndpoints, }) } @@ -876,10 +891,9 @@ func (h *Handler) addChatMsg(msg ChatMsg) { h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:] } // persist to webui config table as compact JSON - if h.cfgReg != nil { - ps := h.cfgReg.PluginConfig("webui") + if h.settings != nil { b, _ := json.Marshal(h.chatHistory) - ps.Set("chathistory", string(b)) + _ = h.settings.Set("chathistory", string(b)) } h.chatMu.Unlock() } @@ -929,7 +943,11 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) { } h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)}) - resp := h.iom.InjectTextSync("cli", body.Message) + if h.sdk == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"}) + return + } + resp := h.sdk.InjectTextSync("cli", "cli", body.Message) if resp == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"}) return @@ -965,7 +983,7 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { flusher.Flush() done := r.Context().Done() - if h.eventBus == nil { + if h.sdk == nil { fmt.Fprintf(w, "event: error\ndata: {\"msg\":\"event bus unavailable\"}\n\n") flusher.Flush() return @@ -994,15 +1012,15 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { var unsubs []func() for _, t := range subTypes { t2 := t - unsub := h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) { - if evt.Type == events.EventToolCall { + unsub := h.sdk.Subscribe(sdk.EventType(t2), func(evt *sdk.Event) { + if evt.Type == sdk.EventToolCall { toolName, _ := evt.Payload["tool"].(string) log.Printf("[SSE] received tool_call event: tool=%s", toolName) } data, _ := json.Marshal(evt) select { case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)): - if evt.Type == events.EventToolCall { + if evt.Type == sdk.EventToolCall { toolName, _ := evt.Payload["tool"].(string) log.Printf("[SSE] wrote tool_call to writeCh: tool=%s", toolName) } @@ -1031,16 +1049,20 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { + if h.config == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "config not available"}) + return + } switch r.Method { case http.MethodGet: - writeJSON(w, http.StatusOK, h.config) + writeJSON(w, http.StatusOK, h.config.Get()) case http.MethodPut: var cfg types.Config if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid config"}) return } - h.config = &cfg + h.config.Put(&cfg) writeJSON(w, http.StatusOK, map[string]string{"status": "config_updated"}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -1048,7 +1070,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) { - if h.cfgReg == nil { + if h.settings == nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "config registry not available"}) return } @@ -1056,30 +1078,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) { case http.MethodGet: prefix := r.URL.Query().Get("prefix") values := make(map[string]interface{}) - meta := make(map[string]*internalConfig.ConfigDef) + meta := make(map[string]*sdk.ConfigDef) if strings.HasPrefix(prefix, "plugin.") { // 插件配置:从插件自身 config_ 表读取 pluginName := prefix[7:] - ps := h.cfgReg.PluginConfig(pluginName) - keys, _ := ps.List("") + keys, _ := h.settings.ListPlugin(pluginName, "") for _, k := range keys { - v, _ := ps.Get(k) + v, _ := h.settings.GetPlugin(pluginName, k) fullKey := prefix + "." + k values[fullKey] = v - if def := h.cfgReg.GetDef(fullKey); def != nil { - meta[fullKey] = def - } + } + for _, def := range h.settings.DefsPlugin(pluginName, "") { + fullKey := prefix + "." + def.Key + meta[fullKey] = def } } else { - // 核心配置:从 core config 表读取 - keys := h.cfgReg.List(prefix) - for _, k := range keys { - v, _ := h.cfgReg.Get(k) - values[k] = v + // 核心配置:从 core config 表读取(键可为任意前缀,如 core.llm.*、webui.*) + all := h.settings.Dump() + var keys []string + for k := range all { + if strings.HasPrefix(k, prefix) { + keys = append(keys, k) + } } - defs := h.cfgReg.ListDefs(prefix) - for _, d := range defs { + sort.Strings(keys) + for _, k := range keys { + values[k] = all[k] + } + for _, d := range h.settings.DefsCore(prefix) { meta[d.Key] = d // 有 def 但 DB 中尚无值的 key,用 default 填充以便在 WebUI 中显示和编辑 if _, exists := values[d.Key]; !exists { @@ -1087,29 +1114,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) { } } // 无前缀时同时加载所有插件配置 - if prefix == "" && h.pluginReg != nil { - for _, p := range h.pluginReg.List() { - ps := h.cfgReg.PluginConfig(p) - pkeys, _ := ps.List("") + if prefix == "" { + for _, p := range h.settings.Plugins() { + if p == "core" { + continue + } + pkeys, _ := h.settings.ListPlugin(p, "") for _, k := range pkeys { - v, _ := ps.Get(k) + v, _ := h.settings.GetPlugin(p, k) fullKey := "plugin." + p + "." + k values[fullKey] = v - if def := h.cfgReg.GetDef(fullKey); def != nil { - meta[fullKey] = def - } + } + for _, def := range h.settings.DefsPlugin(p, "") { + fullKey := "plugin." + p + "." + def.Key + meta[fullKey] = def } } } } plugins := []string{"core"} - pm := h.pluginReg.PluginMetas() - if h.pluginReg != nil { - for _, p := range h.pluginReg.List() { + for _, p := range h.settings.Plugins() { + if p != "core" { plugins = append(plugins, "plugin."+p) } } + var pm map[string]sdk.PluginMeta + if h.pluginMgr != nil { + pm = h.pluginMgr.PluginMetas() + } var disabledPlugins []sdk.DisabledPluginInfo if h.pluginMgr != nil { disabledPlugins = h.pluginMgr.ListDisabledPlugins() @@ -1134,20 +1167,21 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(body.Key, "plugin.") { parts := strings.SplitN(body.Key, ".", 3) if len(parts) >= 3 { - ps := h.cfgReg.PluginConfig(parts[1]) - if err := ps.Set(parts[2], body.Value); err != nil { + if err := h.settings.SetPlugin(parts[1], parts[2], body.Value); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } } } else { - if err := h.cfgReg.Set(body.Key, body.Value); err != nil { + if err := h.settings.SetCore(body.Key, body.Value); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } } - if strings.HasPrefix(body.Key, "core.llm.") && h.providerMgr != nil && h.lua != nil { - h.reloadLLMProviders() + if strings.HasPrefix(body.Key, "core.llm.") && h.llm != nil { + if err := h.llm.ReloadFromConfig(); err != nil { + log.Printf("[webui] failed to reload LLM providers: %v", err) + } } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) default: @@ -1155,36 +1189,13 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) reloadLLMProviders() { - cfg := h.cfgReg.ToConfig() - h.providerMgr.Reset() - for _, src := range cfg.LLM.Sources { - key := src.APIKey - if key == "" { - key = h.baseAPIKey - } - provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{ - Model: src.Model, - BaseURL: src.BaseURL, - APIKey: key, - Temperature: cfg.LLM.Temperature, - MaxTokens: cfg.LLM.MaxTokens, - ContextWindow: src.ContextWindow, - }, h.lua, src.Adapter) - h.providerMgr.Register(src.Name, provider) - } - if cfg.LLM.Provider != "" { - _ = h.providerMgr.SetDefault(cfg.LLM.Provider) - } -} - func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - if h.iom == nil { + if h.sdk == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"}) return } @@ -1211,9 +1222,9 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request return } - response := h.iom.InjectTextSync("http", lastMsg.Content) + response := h.sdk.InjectTextSync("http", "http", lastMsg.Content) if response == nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "no response from agent"}) + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"}) return } @@ -1375,11 +1386,10 @@ func (h *Handler) handleTracker(w http.ResponseWriter, r *http.Request) { func (h *Handler) pluginmgrAddr() string { addr := "127.0.0.1:9876" - if h.cfgReg == nil { + if h.settings == nil { return addr } - ps := h.cfgReg.PluginConfig("pluginmgr") - if v, err := ps.Get("http_addr"); err == nil { + if v, err := h.settings.GetPlugin("pluginmgr", "http_addr"); err == nil { if s, ok := v.(string); ok && s != "" { addr = s } @@ -1436,17 +1446,11 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) { } if path == "reload" && r.Method == http.MethodPost { - if h.pluginReg == nil { + if h.pluginMgr == nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin registry not available"}) return } - dir := "" - if h.cfgReg != nil { - if v, _ := h.cfgReg.Get("core.plugin.dir"); v != nil { - dir, _ = v.(string) - } - } - if _, err := h.pluginReg.Reload(dir); err != nil { + if _, err := h.pluginMgr.ReloadPlugins(); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } diff --git a/internal/plugins/webui/handler_test.go b/internal/plugins/webui/handler_test.go index 8c56d3b..77d8fa4 100644 --- a/internal/plugins/webui/handler_test.go +++ b/internal/plugins/webui/handler_test.go @@ -17,12 +17,19 @@ import ( "gitcode.com/JianFeeeee/HomeAgent/internal/events" "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" "gitcode.com/JianFeeeee/HomeAgent/internal/memory" - "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" "gitcode.com/JianFeeeee/HomeAgent/internal/supervisor" "gitcode.com/JianFeeeee/HomeAgent/internal/tracker" "gitcode.com/JianFeeeee/HomeAgent/pkg/types" ) +func testSDK(cfg sdk.SDKConfig) *sdk.PluginSDK { + if cfg.EventBus == nil { + cfg.EventBus = events.NewBus() + } + return sdk.New("webui", cfg) +} + func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) { t.Helper() cfg := &types.Config{ @@ -34,7 +41,11 @@ func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) { sup := supervisor.New(cfg) sup.Start() - return NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, ""), sup + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Config: sdk.NewConfig(cfg), + }) + return NewHandler(s), sup } func TestAuthMiddleware(t *testing.T) { @@ -44,9 +55,21 @@ func TestAuthMiddleware(t *testing.T) { cfgReg.PluginConfig("webui").Set("password", "secret-pass") cfgReg.PluginConfig("webui").Set("session_ttl_hours", "24") - h, sup := newTestHandler(t) + sup := supervisor.New(&types.Config{ + Daemon: types.DaemonConfig{ + CheckInterval: time.Minute, + HeartbeatInterval: 30 * time.Second, + }, + }) + sup.Start() defer sup.Shutdown() - h.cfgReg = cfgReg + + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Settings: sdk.NewSettings("webui", cfgReg), + Config: sdk.NewConfig(&types.Config{}), + }) + h := NewHandler(s) mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -194,7 +217,12 @@ func TestHandleKnowledgeSearch(t *testing.T) { sup.Start() defer sup.Shutdown() - h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Knowledge: sdk.NewKnowledge(ks), + Config: sdk.NewConfig(cfg), + }) + h := NewHandler(s) req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil) w := httptest.NewRecorder() @@ -226,7 +254,12 @@ func TestHandleKnowledgeCreate(t *testing.T) { sup.Start() defer sup.Shutdown() - h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Knowledge: sdk.NewKnowledge(ks), + Config: sdk.NewConfig(cfg), + }) + h := NewHandler(s) body := `{"name":"new_doc","content":"fresh content"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body)) @@ -291,7 +324,12 @@ func TestHandleTrackerStats(t *testing.T) { sup.Start() defer sup.Shutdown() - h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, tr, nil, nil, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Tracker: tr, + Config: sdk.NewConfig(cfg), + }) + h := NewHandler(s) req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil) w := httptest.NewRecorder() @@ -305,8 +343,6 @@ func TestHandleTrackerStats(t *testing.T) { func TestHandleOpenAICompletionsNoMessages(t *testing.T) { h, sup := newTestHandler(t) defer sup.Shutdown() - // 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验 - h.iom = agentIO.NewIOManager() body := `{"model":"test"}` req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)) @@ -322,8 +358,6 @@ func TestHandleOpenAICompletionsNoMessages(t *testing.T) { func TestHandleOpenAICompletionsLastMsgNotUser(t *testing.T) { h, sup := newTestHandler(t) defer sup.Shutdown() - // 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验 - h.iom = agentIO.NewIOManager() body := `{"messages":[{"role":"assistant","content":"hi"}]}` req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)) @@ -392,9 +426,27 @@ func TestHandleConfigGet(t *testing.T) { } func TestRegisterRoutes(t *testing.T) { - h, sup := newTestHandler(t) + cfgReg := internalConfig.NewConfigRegistry("") + cfgReg.PluginConfig("webui").Set("api_key", "test-api-key") + cfgReg.PluginConfig("webui").Set("username", "admin") + cfgReg.PluginConfig("webui").Set("password", "secret-pass") + + sup := supervisor.New(&types.Config{ + Daemon: types.DaemonConfig{ + CheckInterval: time.Minute, + HeartbeatInterval: 30 * time.Second, + }, + }) + sup.Start() defer sup.Shutdown() + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Settings: sdk.NewSettings("webui", cfgReg), + Config: sdk.NewConfig(&types.Config{}), + }) + h := NewHandler(s) + mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -407,7 +459,7 @@ func TestRegisterRoutes(t *testing.T) { {"/api/v1/agents", http.MethodGet, http.StatusOK}, {"/api/v1/config", http.MethodGet, http.StatusOK}, {"/api/v1/network", http.MethodGet, http.StatusOK}, - {"/", http.MethodGet, http.StatusOK}, + {"/", http.MethodGet, http.StatusFound}, {"/api/v1/memory", http.MethodGet, http.StatusServiceUnavailable}, {"/api/v1/knowledge", http.MethodGet, http.StatusServiceUnavailable}, {"/api/v1/tracker", http.MethodGet, http.StatusServiceUnavailable}, @@ -416,6 +468,9 @@ func TestRegisterRoutes(t *testing.T) { for _, tt := range tests { req := httptest.NewRequest(tt.method, tt.path, nil) + if strings.HasPrefix(tt.path, "/api/v1/") { + req.Header.Set("X-API-Key", "test-api-key") + } w := httptest.NewRecorder() mux.ServeHTTP(w, req) @@ -470,8 +525,12 @@ func TestSettingsAPIFlow(t *testing.T) { sup.Start() defer sup.Shutdown() - pluginReg := plugin.NewRegistry() - h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Settings: sdk.NewSettings("webui", cfgReg), + Config: sdk.NewConfig(&types.Config{}), + }) + h := NewHandler(s) t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) @@ -578,7 +637,11 @@ func TestSettingsAPIFlow(t *testing.T) { }) t.Run("settings_not_available_without_registry", func(t *testing.T) { - h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "") + s2 := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Config: sdk.NewConfig(&types.Config{}), + }) + h2 := NewHandler(s2) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) w := httptest.NewRecorder() h2.handleSettings(w, req) @@ -603,8 +666,12 @@ func TestSettingsWithPluginRegistry(t *testing.T) { sup.Start() defer sup.Shutdown() - pluginReg := plugin.NewRegistry() - h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + Settings: sdk.NewSettings("webui", cfgReg), + Config: sdk.NewConfig(&types.Config{}), + }) + h := NewHandler(s) req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil) w := httptest.NewRecorder() @@ -633,7 +700,7 @@ type echoProvider struct{ name string } func (p *echoProvider) Name() string { return p.name } func (p *echoProvider) MaxContextTokens() int { return 8192 } func (p *echoProvider) Chat(ctx context.Context, req *agentAPI.CompletionRequest) (*agentAPI.CompletionResponse, error) { - content := "echo: " + req.Messages[len(req.Messages)-1].Content + content := "echo: " + lastUserContent(req.Messages) return &agentAPI.CompletionResponse{Content: content, FinishReason: "stop"}, nil } func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionRequest) (<-chan agentAPI.StreamChunk, error) { @@ -642,6 +709,15 @@ func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionR return ch, nil } +func lastUserContent(msgs []agentAPI.Message) string { + for i := len(msgs) - 1; i >= 0; i-- { + if msgs[i].Role == "user" { + return msgs[i].Content + } + } + return "" +} + func init() { // 避免测试时自动输出 } @@ -656,19 +732,22 @@ func TestHandleCompletionsEndToEnd(t *testing.T) { } defer memDB.Close() + pm := agentAPI.NewProviderManager() + pm.Register("echo", &echoProvider{name: "echo"}) + agent := agentCore.New(agentCore.AgentConfig{ - ID: "test", - SystemPrompt: "你是测试助手", - Provider: &echoProvider{name: "echo"}, - IO: iom, - Memory: memDB, - Indexer: nil, + ID: "test", + SystemPrompt: "你是测试助手", + Provider: &echoProvider{name: "echo"}, + ProviderManager: pm, + IO: iom, + Memory: memDB, + Indexer: nil, ContextSavePath: "", }) agent.Start() defer agent.Stop() - // Handler 需要 iom sup := supervisor.New(&types.Config{ Daemon: types.DaemonConfig{ CheckInterval: time.Minute, @@ -678,7 +757,12 @@ func TestHandleCompletionsEndToEnd(t *testing.T) { sup.Start() defer sup.Shutdown() - h := NewHandler(sup, nil, nil, nil, &types.Config{}, iom, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "") + s := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + IOManager: iom, + Config: sdk.NewConfig(&types.Config{}), + }) + h := NewHandler(s) t.Run("POST_chat_completions_returns_echo", func(t *testing.T) { body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}` @@ -711,7 +795,10 @@ func TestHandleCompletionsEndToEnd(t *testing.T) { }) t.Run("POST_chat_completions_no_iom_returns_503", func(t *testing.T) { - h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "") + s2 := testSDK(sdk.SDKConfig{ + Supervisor: supervisor.NewSDKAdapter(sup), + }) + h2 := NewHandler(s2) body := `{"messages":[{"role":"user","content":"hi"}]}` req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") diff --git a/internal/plugins/webui/plugin.go b/internal/plugins/webui/plugin.go index 0ac6c81..f6b81b8 100644 --- a/internal/plugins/webui/plugin.go +++ b/internal/plugins/webui/plugin.go @@ -7,117 +7,28 @@ import ( "log" "net/http" - agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" - agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core" - agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" - internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" - "gitcode.com/JianFeeeee/HomeAgent/internal/events" - "gitcode.com/JianFeeeee/HomeAgent/internal/knowledge" - luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" - "gitcode.com/JianFeeeee/HomeAgent/internal/memory" - "gitcode.com/JianFeeeee/HomeAgent/internal/memory/text" "gitcode.com/JianFeeeee/HomeAgent/internal/plugin" - "gitcode.com/JianFeeeee/HomeAgent/internal/skill" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" - "gitcode.com/JianFeeeee/HomeAgent/internal/supervisor" - "gitcode.com/JianFeeeee/HomeAgent/internal/tracker" - "gitcode.com/JianFeeeee/HomeAgent/pkg/types" ) -// 包级依赖注入 — 由 main.go 在 Load() 前调用 Configure() 设置。 -var ( - webuiAddr string - webuiSup *supervisor.Daemon - webuiMem *memory.GraphDB - webuiSK *skill.Manager - webuiLua *luaVM.VM - webuiCfg *types.Config - webuiIOM *agentIO.IOManager - webuiTM *text.Memory - webuiKS *knowledge.Store - webuiTR *tracker.Tracker - webuiCR *internalConfig.ConfigRegistry - webuiPR *plugin.Registry - webuiEvBus *events.Bus - webuiStatusProvider agentCore.StatusProvider - webuiProviderMgr *agentAPI.ProviderManager - webuiBaseAPIKey string -) - -// Configure 注入 WebUI 插件需要的内核依赖。必须在 Load() 之前调用。 -func Configure(addr string, - sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, - lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, - tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, - cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, - sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string, -) { - webuiAddr = addr - webuiSup, webuiMem, webuiSK, webuiLua = sup, mem, sk, lua - webuiCfg, webuiIOM, webuiTM, webuiKS = cfg, iom, tm, ks - webuiTR, webuiCR, webuiPR, webuiEvBus = tr, cr, pr, evBus - webuiStatusProvider = sp - webuiProviderMgr = pm - webuiBaseAPIKey = baseKey -} - func init() { plugin.RegisterPluginMeta("webui", "Web 控制台", "WebUI") plugin.RegisterFactory("webui", func(name string, config map[string]interface{}) (sdk.Plugin, error) { - if webuiSup == nil { - return nil, nil // 未 Configure 则跳过(不给日志警告) - } - addr := webuiAddr - if a, ok := config["addr"].(string); ok { - addr = a - } - return New(name, addr, - webuiSup, webuiMem, webuiSK, webuiLua, - webuiCfg, webuiIOM, webuiTM, webuiKS, - webuiTR, webuiCR, webuiPR, webuiEvBus, webuiStatusProvider, - webuiProviderMgr, webuiBaseAPIKey, - ), nil + return New(name), nil }) } type Plugin struct { name string - addr string handler *Handler server *http.Server mux *http.ServeMux - - sup *supervisor.Daemon - mem *memory.GraphDB - sk *skill.Manager - lua *luaVM.VM - cfg *types.Config - iom *agentIO.IOManager - tm *text.Memory - ks *knowledge.Store - tr *tracker.Tracker - cr *internalConfig.ConfigRegistry - pr *plugin.Registry - evBus *events.Bus - statusProvider agentCore.StatusProvider - providerMgr *agentAPI.ProviderManager - baseAPIKey string } -func New(name, addr string, - sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, - lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, - tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, - cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, - sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string, -) *Plugin { +func New(name string) *Plugin { return &Plugin{ name: name, - addr: addr, mux: http.NewServeMux(), - sup: sup, mem: mem, sk: sk, lua: lua, cfg: cfg, - iom: iom, tm: tm, ks: ks, tr: tr, cr: cr, pr: pr, evBus: evBus, - statusProvider: sp, providerMgr: pm, baseAPIKey: baseKey, } } @@ -157,11 +68,18 @@ func (p *Plugin) Name() string { return p.name } func (p *Plugin) Start(s *sdk.PluginSDK) error { s.SetAutoRestart(true) + addr := ":8080" + if v, _ := s.Settings().Get("addr"); v != nil { + if s2, ok := v.(string); ok && s2 != "" { + addr = s2 + } + } + s.RegisterOutputChannel("webui", 1, "Web 控制台", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) { payload, _ := args["payload"].(string) if payload != "" { - p.evBus.Publish(&events.Event{ - Type: events.EventAgentOutput, + s.Publish(&sdk.Event{ + Type: sdk.EventAgentOutput, Payload: map[string]interface{}{ "content": payload, "channel": "webui", @@ -171,6 +89,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { return map[string]interface{}{"status": "ok"}, nil }) + s.Settings().RegisterDef(sdk.ConfigDef{Key: "addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "Web 控制台监听地址", Category: "webui"}) s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"}) s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"}) s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"}) @@ -178,7 +97,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { p.ensureAuthBootstrap(s) s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error { - p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}}) + s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}}) return nil }) s.RegisterStage(sdk.StageBeforeToolcall, func(ctx *sdk.StageContext) error { @@ -186,22 +105,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { if len(ctx.ToolCalls) > 0 { tool = ctx.ToolCalls[0].Name } - p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}}) + s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}}) return nil }) s.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error { - p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}}) + s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}}) return nil }) - h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey) - h.SetPluginMgr(s.PluginMgr()) - p.handler = h - h.RegisterRoutes(p.mux) + p.handler = NewHandler(s) + p.handler.RegisterRoutes(p.mux) - p.server = &http.Server{Addr: p.addr, Handler: p.mux} + p.server = &http.Server{Addr: addr, Handler: p.mux} go func() { - log.Printf("[webui] HTTP server listening on %s", p.addr) + log.Printf("[webui] HTTP server listening on %s", addr) if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("[webui] server error: %v", err) } diff --git a/internal/sdk/adapter.go b/internal/sdk/adapter.go new file mode 100644 index 0000000..814855c --- /dev/null +++ b/internal/sdk/adapter.go @@ -0,0 +1,16 @@ +package sdk + +// AdapterAPI provides access to the Lua LLM protocol adapters. +type AdapterAPI interface { + List() []APIAdapter + // Load writes the adapter source file and hot-reloads it. + Load(name, code string) error + Remove(name string) error + AdapterDir() string +} + +// APIAdapter describes a loaded Lua protocol adapter. +type APIAdapter struct { + Name string `json:"name"` + Version string `json:"version"` +} diff --git a/internal/sdk/adapter_impl.go b/internal/sdk/adapter_impl.go new file mode 100644 index 0000000..1a30ca5 --- /dev/null +++ b/internal/sdk/adapter_impl.go @@ -0,0 +1,62 @@ +package sdk + +import ( + "errors" + "os" + "path/filepath" + + luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" +) + +// adapterImpl 桥接 Lua VM 的协议适配器管理。 +type adapterImpl struct { + vm *luaVM.VM +} + +func NewAdapter(vm *luaVM.VM) AdapterAPI { + return &adapterImpl{vm: vm} +} + +func (a *adapterImpl) List() []APIAdapter { + if a.vm == nil { + return nil + } + got := a.vm.ListAdapters() + out := make([]APIAdapter, len(got)) + for i, ad := range got { + out[i] = APIAdapter{Name: ad.Name, Version: ad.Version} + } + return out +} + +func (a *adapterImpl) Load(name, code string) error { + if a.vm == nil { + return errors.New("lua vm not available") + } + path := filepath.Join(a.vm.AdapterDir(), name+".lua") + if err := os.WriteFile(path, []byte(code), 0644); err != nil { + return err + } + return a.vm.LoadAdapter(path) +} + +func (a *adapterImpl) Remove(name string) error { + if a.vm == nil { + return errors.New("lua vm not available") + } + path := filepath.Join(a.vm.AdapterDir(), name+".lua") + if err := os.Remove(path); err != nil { + return err + } + a.vm.RemoveAdapter(name) + return nil +} + +func (a *adapterImpl) AdapterDir() string { + if a.vm == nil { + return "" + } + return a.vm.AdapterDir() +} + +var _ AdapterAPI = (*adapterImpl)(nil) diff --git a/internal/sdk/config.go b/internal/sdk/config.go new file mode 100644 index 0000000..ba1e782 --- /dev/null +++ b/internal/sdk/config.go @@ -0,0 +1,10 @@ +package sdk + +import "gitcode.com/JianFeeeee/HomeAgent/pkg/types" + +// ConfigAPI provides read/write access to the runtime config object +// (unrelated to the config database). +type ConfigAPI interface { + Get() *types.Config + Put(cfg *types.Config) +} diff --git a/internal/sdk/config_impl.go b/internal/sdk/config_impl.go new file mode 100644 index 0000000..1257561 --- /dev/null +++ b/internal/sdk/config_impl.go @@ -0,0 +1,22 @@ +package sdk + +import "gitcode.com/JianFeeeee/HomeAgent/pkg/types" + +// configImpl 持有运行期配置对象的引用(与配置数据库无关)。 +type configImpl struct { + cfg *types.Config +} + +func NewConfig(cfg *types.Config) ConfigAPI { + return &configImpl{cfg: cfg} +} + +func (c *configImpl) Get() *types.Config { + return c.cfg +} + +func (c *configImpl) Put(cfg *types.Config) { + c.cfg = cfg +} + +var _ ConfigAPI = (*configImpl)(nil) diff --git a/internal/sdk/events.go b/internal/sdk/events.go new file mode 100644 index 0000000..407d75c --- /dev/null +++ b/internal/sdk/events.go @@ -0,0 +1,20 @@ +package sdk + +import "gitcode.com/JianFeeeee/HomeAgent/internal/events" + +// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件, +// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。 + +type EventType = events.EventType +type Event = events.Event + +const ( + EventRawInput = events.EventRawInput + EventAgentOutput = events.EventAgentOutput + EventAgentLLMChain = events.EventAgentLLMChain + EventToolCall = events.EventToolCall + EventReasoning = events.EventReasoning + EventStage = events.EventStage + EventSystem = events.EventSystem + EventAll = events.EventAll +) diff --git a/internal/sdk/indexer.go b/internal/sdk/indexer.go new file mode 100644 index 0000000..c78b16a --- /dev/null +++ b/internal/sdk/indexer.go @@ -0,0 +1,17 @@ +package sdk + +// IndexerAPI exposes the memory context indexer. +type IndexerAPI interface { + BuildContext(q string) (*IndexContext, error) + FormatContext(ctx *IndexContext) string + GetToolDefinitions() []map[string]interface{} + BuildToolPrompt() string +} + +// IndexContext is a neutral snapshot of the injected memory context. +type IndexContext struct { + Entities []Entity `json:"entities"` + Relations []Relation `json:"relations"` + Summary string `json:"summary"` + TokenEstimate int `json:"token_estimate"` +} diff --git a/internal/sdk/indexer_impl.go b/internal/sdk/indexer_impl.go new file mode 100644 index 0000000..f89d487 --- /dev/null +++ b/internal/sdk/indexer_impl.go @@ -0,0 +1,71 @@ +package sdk + +import ( + "errors" + + "gitcode.com/JianFeeeee/HomeAgent/internal/memory" +) + +// indexerImpl 桥接 memory.Indexer 到中立的 IndexerAPI。 +type indexerImpl struct { + idx *memory.Indexer +} + +func NewIndexer(idx *memory.Indexer) IndexerAPI { + return &indexerImpl{idx: idx} +} + +func (i *indexerImpl) BuildContext(q string) (*IndexContext, error) { + if i.idx == nil { + return nil, errors.New("indexer not available") + } + ctx := i.idx.BuildContext(q) + if ctx == nil { + return &IndexContext{}, nil + } + entities := make([]Entity, len(ctx.Entities)) + for j, e := range ctx.Entities { + entities[j] = Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount} + } + relations := make([]Relation, len(ctx.Relations)) + for j, r := range ctx.Relations { + relations[j] = Relation{SourceName: r.SourceName, TargetName: r.TargetName, RelationType: r.RelationType} + } + return &IndexContext{ + Entities: entities, + Relations: relations, + Summary: ctx.Summary, + TokenEstimate: ctx.TokenEstimate, + }, nil +} + +func (i *indexerImpl) FormatContext(ctx *IndexContext) string { + if i.idx == nil || ctx == nil { + return "" + } + entities := make([]memory.Entity, len(ctx.Entities)) + for j, e := range ctx.Entities { + entities[j] = memory.Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount} + } + return i.idx.FormatContext(&memory.InjectedContext{ + Entities: entities, + Summary: ctx.Summary, + TokenEstimate: ctx.TokenEstimate, + }) +} + +func (i *indexerImpl) GetToolDefinitions() []map[string]interface{} { + if i.idx == nil { + return nil + } + return i.idx.GetToolDefinitions() +} + +func (i *indexerImpl) BuildToolPrompt() string { + if i.idx == nil { + return "" + } + return i.idx.BuildToolPrompt() +} + +var _ IndexerAPI = (*indexerImpl)(nil) diff --git a/internal/sdk/knowledge.go b/internal/sdk/knowledge.go index fa72a2e..2cbe059 100644 --- a/internal/sdk/knowledge.go +++ b/internal/sdk/knowledge.go @@ -2,5 +2,13 @@ package sdk import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" -type KnowledgeAPI = pubsdk.KnowledgeAPI +// KnowledgeAPI 是内置插件使用的全量知识库接口。 +type KnowledgeAPI interface { + pubsdk.KnowledgeAPI + // Stats 返回知识库的运行统计。 + Stats() map[string]interface{} + // Remove 按名称删除一条知识。 + Remove(name string) error +} + type Knowledge = pubsdk.Knowledge diff --git a/internal/sdk/knowledge_impl.go b/internal/sdk/knowledge_impl.go index c4525ff..2452a7c 100644 --- a/internal/sdk/knowledge_impl.go +++ b/internal/sdk/knowledge_impl.go @@ -26,4 +26,14 @@ func (k *knowledgeImpl) List() ([]string, error) { return k.ks.List(), nil } +func (k *knowledgeImpl) Stats() map[string]interface{} { + if k.ks == nil { return map[string]interface{}{} } + return k.ks.Stats() +} + +func (k *knowledgeImpl) Remove(name string) error { + if k.ks == nil { return nil } + return k.ks.Remove(name) +} + var _ KnowledgeAPI = (*knowledgeImpl)(nil) diff --git a/internal/sdk/llm.go b/internal/sdk/llm.go index 1511275..6e1baf5 100644 --- a/internal/sdk/llm.go +++ b/internal/sdk/llm.go @@ -1,5 +1,62 @@ package sdk -import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +import ( + "context" -type LLMAPI = pubsdk.LLMAPI + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// LLMAPI 是内置插件使用的全量 LLM Provider 接口。 +// 内置 SDK 为全量接口,外部 SDK 仅暴露其中的安全子集(pubsdk.LLMAPI)。 +type LLMAPI interface { + pubsdk.LLMAPI + // ReloadFromConfig 从内核配置重建所有 LLM Provider。 + ReloadFromConfig() error + // Chat 向默认 Provider 发起一次补全调用(非记忆管线)。 + Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error) +} + +// LLMMessage 是面向 LLM Provider 的中立对话消息。 +type LLMMessage struct { + Role string `json:"role"` + Content string `json:"content,omitempty"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + ToolCalls []LLMToolCall `json:"tool_calls,omitempty"` +} + +// LLMToolCall 是中立的工具调用请求。 +type LLMToolCall struct { + ID string `json:"id"` + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments"` +} + +// LLMTokenUsage 报告一次补全的 token 消耗。 +type LLMTokenUsage struct { + Prompt int `json:"prompt"` + Completion int `json:"completion"` + Total int `json:"total"` +} + +// LLMCompletionRequest 是中立的补全请求。 +// Tools/ToolChoice 使用 OpenAI 风格载荷,便于 Provider 直接转发。 +type LLMCompletionRequest struct { + Model string `json:"model,omitempty"` + Messages []LLMMessage `json:"messages"` + Temperature float64 `json:"temperature,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + Tools []interface{} `json:"tools,omitempty"` + ToolChoice interface{} `json:"tool_choice,omitempty"` + DisableThinking bool `json:"disable_thinking,omitempty"` +} + +// LLMCompletionResponse 是中立的补全响应。 +type LLMCompletionResponse struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` + TokenUsage LLMTokenUsage `json:"token_usage,omitempty"` + ToolCalls []LLMToolCall `json:"tool_calls,omitempty"` +} diff --git a/internal/sdk/llm_impl.go b/internal/sdk/llm_impl.go index 8ba0b4c..0f10a83 100644 --- a/internal/sdk/llm_impl.go +++ b/internal/sdk/llm_impl.go @@ -1,26 +1,133 @@ package sdk -import agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" +import ( + "context" + "fmt" -type llmImpl struct{ mgr *agentAPI.ProviderManager } + agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" + internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config" + luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" +) -func NewLLM(mgr *agentAPI.ProviderManager) LLMAPI { return &llmImpl{mgr: mgr} } +type llmImpl struct { + mgr *agentAPI.ProviderManager + cfgReg *internalConfig.ConfigRegistry + lua *luaVM.VM + baseAPIKey string +} + +func NewLLM(mgr *agentAPI.ProviderManager, cfgReg *internalConfig.ConfigRegistry, lua *luaVM.VM, baseAPIKey string) LLMAPI { + return &llmImpl{mgr: mgr, cfgReg: cfgReg, lua: lua, baseAPIKey: baseAPIKey} +} func (l *llmImpl) ListSources() []string { - if l.mgr == nil { return nil } + if l.mgr == nil { + return nil + } return l.mgr.List() } func (l *llmImpl) SetSource(name string) error { - if l.mgr == nil { return nil } + if l.mgr == nil { + return nil + } return l.mgr.SetDefault(name) } func (l *llmImpl) CurrentSource() string { - if l.mgr == nil { return "" } + if l.mgr == nil { + return "" + } p := l.mgr.Default() - if p == nil { return "" } + if p == nil { + return "" + } return p.Name() } +func (l *llmImpl) Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error) { + if l.mgr == nil { + return nil, fmt.Errorf("llm: provider manager not available") + } + p := l.mgr.Default() + if p == nil { + return nil, fmt.Errorf("llm: no default provider") + } + apiReq := &agentAPI.CompletionRequest{ + Model: req.Model, + Temperature: req.Temperature, + MaxTokens: req.MaxTokens, + Stream: req.Stream, + Tools: req.Tools, + ToolChoice: req.ToolChoice, + DisableThinking: req.DisableThinking, + } + if len(req.Messages) > 0 { + apiReq.Messages = make([]agentAPI.Message, len(req.Messages)) + for i, m := range req.Messages { + msg := agentAPI.Message{ + Role: m.Role, + Content: m.Content, + ReasoningContent: m.ReasoningContent, + ToolCallID: m.ToolCallID, + } + if len(m.ToolCalls) > 0 { + msg.ToolCalls = make([]agentAPI.ToolCall, len(m.ToolCalls)) + for j, tc := range m.ToolCalls { + msg.ToolCalls[j] = agentAPI.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments} + } + } + apiReq.Messages[i] = msg + } + } + resp, err := p.Chat(ctx, apiReq) + if err != nil { + return nil, err + } + out := &LLMCompletionResponse{ + Content: resp.Content, + ReasoningContent: resp.ReasoningContent, + FinishReason: resp.FinishReason, + TokenUsage: LLMTokenUsage{ + Prompt: resp.TokenUsage.Prompt, + Completion: resp.TokenUsage.Completion, + Total: resp.TokenUsage.Total, + }, + } + for _, tc := range resp.ToolCalls { + out.ToolCalls = append(out.ToolCalls, LLMToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}) + } + return out, nil +} + +func (l *llmImpl) ReloadFromConfig() error { + if l.mgr == nil || l.cfgReg == nil || l.lua == nil { + return nil + } + cfg := l.cfgReg.ToConfig() + if cfg == nil { + return nil + } + l.mgr.Reset() + for _, src := range cfg.LLM.Sources { + key := src.APIKey + if key == "" { + key = l.baseAPIKey + } + provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{ + Model: src.Model, + BaseURL: src.BaseURL, + APIKey: key, + Temperature: cfg.LLM.Temperature, + MaxTokens: cfg.LLM.MaxTokens, + ContextWindow: src.ContextWindow, + }, l.lua, src.Name, src.Adapter) + l.mgr.Register(src.Name, provider) + } + if cfg.LLM.Provider != "" { + _ = l.mgr.SetDefault(cfg.LLM.Provider) + } + return nil +} + var _ LLMAPI = (*llmImpl)(nil) diff --git a/internal/sdk/memory.go b/internal/sdk/memory.go index 405ac0c..59bcac7 100644 --- a/internal/sdk/memory.go +++ b/internal/sdk/memory.go @@ -2,12 +2,26 @@ package sdk import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" -type MemoryAPI = pubsdk.MemoryAPI +// MemoryAPI 是内置插件使用的全量图记忆接口。 +type MemoryAPI interface { + pubsdk.MemoryAPI + // GraphData 返回整个知识图谱的完整快照。 + GraphData() (map[string]interface{}, error) +} + type Entity = pubsdk.Entity type Relation = pubsdk.Relation type Triple = pubsdk.Triple -type TextMemoryAPI = pubsdk.TextMemoryAPI +// TextMemoryAPI 是内置插件使用的全量文本记忆接口。 +type TextMemoryAPI interface { + pubsdk.TextMemoryAPI + // RecentEvents 返回最近 n 条记忆事件。 + RecentEvents(n int) ([]TextEvent, error) + // Stats 返回文本记忆的运行统计。 + Stats() map[string]interface{} +} + type TextEvent = pubsdk.TextEvent type DocMemoryAPI = pubsdk.DocMemoryAPI diff --git a/internal/sdk/memory_impl.go b/internal/sdk/memory_impl.go index 60c554a..8c8d233 100644 --- a/internal/sdk/memory_impl.go +++ b/internal/sdk/memory_impl.go @@ -50,6 +50,11 @@ func (m *graphMemory) Purge(criteria map[string]string, mode string) (int, error return m.db.Purge(criteria, mode) } +func (m *graphMemory) GraphData() (map[string]interface{}, error) { + if m.db == nil { return map[string]interface{}{}, nil } + return m.db.GraphData() +} + type textMemoryImpl struct{ tm *text.Memory } func NewTextMemory(tm *text.Memory) TextMemoryAPI { return &textMemoryImpl{tm: tm} } @@ -61,6 +66,22 @@ func (m *textMemoryImpl) Append(evt TextEvent) error { }) } +func (m *textMemoryImpl) RecentEvents(n int) ([]TextEvent, error) { + if m.tm == nil { return nil, nil } + got, err := m.tm.RecentEvents(n) + if err != nil { return nil, err } + out := make([]TextEvent, len(got)) + for i, e := range got { + out[i] = TextEvent{Role: e.Source, Content: e.Input, Timestamp: e.Timestamp, Channel: e.AgentID} + } + return out, nil +} + +func (m *textMemoryImpl) Stats() map[string]interface{} { + if m.tm == nil { return map[string]interface{}{} } + return m.tm.Stats() +} + type docMemoryImpl struct{ ds *doc.Store } func NewDocMemory(ds *doc.Store) DocMemoryAPI { return &docMemoryImpl{ds: ds} } diff --git a/internal/sdk/plugin.go b/internal/sdk/plugin.go index df6f02d..509c472 100644 --- a/internal/sdk/plugin.go +++ b/internal/sdk/plugin.go @@ -57,6 +57,12 @@ type DisabledPluginInfo struct { DisabledBy string `json:"disabled_by"` } +// PluginMeta is the display-name metadata for a plugin (from plg.json / RegisterPluginMeta). +type PluginMeta struct { + NameZh string `json:"name_zh"` + NameEn string `json:"name_en"` +} + type PluginManager interface { ListLoadedPlugins() []string ListDisabledPlugins() []DisabledPluginInfo @@ -64,18 +70,45 @@ type PluginManager interface { DisablePlugin(name, by string) error EnablePlugin(name string) error ReloadPlugins() (string, error) + PluginMetas() map[string]PluginMeta + PluginDir() string } type PluginSDK struct { *pubsdk.PluginSDK + settings SettingsAPI + memory MemoryAPI + textMem TextMemoryAPI + docMem DocMemoryAPI + know KnowledgeAPI + llm LLMAPI + iom *agentIO.IOManager eventBus *events.Bus logger *log.Logger pluginMgr PluginManager + + status StatusAPI + supervisor SupervisorAPI + skill SkillAPI + adapter AdapterAPI + tracker TrackerAPI + config ConfigAPI + tool ToolAPI + indexer IndexerAPI } func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr } +// 以下访问器遮蔽公共 SDK 的同名方法,返回内置插件可用的全量接口。 + +func (s *PluginSDK) Settings() SettingsAPI { return s.settings } +func (s *PluginSDK) Memory() MemoryAPI { return s.memory } +func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem } +func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem } +func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know } +func (s *PluginSDK) LLM() LLMAPI { return s.llm } + // ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口, // 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。 type ioAdapter struct{ iom *agentIO.IOManager } @@ -114,6 +147,15 @@ type SDKConfig struct { RegOutput OutputChannelRegistrar RegInput InputChannelRegistrar PluginMgr PluginManager + + Status StatusAPI + Supervisor SupervisorAPI + Skill SkillAPI + Adapter AdapterAPI + Tracker TrackerAPI + Config ConfigAPI + Tool ToolAPI + Indexer IndexerAPI } func New(name string, cfg SDKConfig) *PluginSDK { @@ -131,13 +173,38 @@ func New(name string, cfg SDKConfig) *PluginSDK { base.SetLLMAPI(cfg.LLM) return &PluginSDK{ PluginSDK: base, + settings: cfg.Settings, + memory: cfg.Memory, + textMem: cfg.TextMemory, + docMem: cfg.DocMemory, + know: cfg.Knowledge, + llm: cfg.LLM, + iom: cfg.IOManager, eventBus: cfg.EventBus, logger: log.Default(), pluginMgr: cfg.PluginMgr, + + status: cfg.Status, + supervisor: cfg.Supervisor, + skill: cfg.Skill, + adapter: cfg.Adapter, + tracker: cfg.Tracker, + config: cfg.Config, + tool: cfg.Tool, + indexer: cfg.Indexer, } } +func (s *PluginSDK) Status() StatusAPI { return s.status } +func (s *PluginSDK) Supervisor() SupervisorAPI { return s.supervisor } +func (s *PluginSDK) Skill() SkillAPI { return s.skill } +func (s *PluginSDK) Adapter() AdapterAPI { return s.adapter } +func (s *PluginSDK) Tracker() TrackerAPI { return s.tracker } +func (s *PluginSDK) Config() ConfigAPI { return s.config } +func (s *PluginSDK) Tool() ToolAPI { return s.tool } +func (s *PluginSDK) Indexer() IndexerAPI { return s.indexer } + func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) { if s.iom != nil { s.iom.InjectInputTo(source, channel, eventType, payload) diff --git a/internal/sdk/settings.go b/internal/sdk/settings.go index 67e63e6..29b5df2 100644 --- a/internal/sdk/settings.go +++ b/internal/sdk/settings.go @@ -2,5 +2,13 @@ package sdk import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" -type SettingsAPI = pubsdk.SettingsAPI +// SettingsAPI 是内置插件使用的全量配置接口。 +type SettingsAPI interface { + pubsdk.SettingsAPI + // DefsCore 返回核心配置表中匹配前缀的配置定义。 + DefsCore(prefix string) []*ConfigDef + // DefsPlugin 返回另一个插件的配置定义。 + DefsPlugin(plugin, prefix string) []*ConfigDef +} + type ConfigDef = pubsdk.ConfigDef diff --git a/internal/sdk/settings_impl.go b/internal/sdk/settings_impl.go index 3c21410..600c5a6 100644 --- a/internal/sdk/settings_impl.go +++ b/internal/sdk/settings_impl.go @@ -79,7 +79,7 @@ func (s *settingsImpl) Defs(prefix string) []*ConfigDef { // ConfigDef = pubsdk.ConfigDef (type alias), so direct conversion works cpy := ConfigDef{ Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description, - Category: d.Category, Options: d.Options, + Category: d.Category, Options: d.Options, Default: d.Default, } out[i] = &cpy } @@ -97,6 +97,25 @@ func (s *settingsImpl) Plugins() []string { result = append(result, names...) return result } +func (s *settingsImpl) DefsCore(prefix string) []*ConfigDef { + if s.reg == nil { return nil } + return mapDefs(s.reg.ListDefs(prefix)) +} +func (s *settingsImpl) DefsPlugin(plugin, prefix string) []*ConfigDef { + if s.reg == nil { return nil } + return mapDefs(s.reg.PluginConfig(plugin).ListDefs(prefix)) +} + +func mapDefs(defs []*internalConfig.ConfigDef) []*ConfigDef { + out := make([]*ConfigDef, len(defs)) + for i, d := range defs { + out[i] = &ConfigDef{ + Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description, + Category: d.Category, Options: d.Options, Default: d.Default, + } + } + return out +} func stringifyDefault(v interface{}) string { if v == nil { return "" } diff --git a/internal/sdk/skill.go b/internal/sdk/skill.go new file mode 100644 index 0000000..2c73248 --- /dev/null +++ b/internal/sdk/skill.go @@ -0,0 +1,22 @@ +package sdk + +// SkillAPI provides access to the skill manager. +type SkillAPI interface { + List() []*Skill + Get(name string) *Skill + Install(name, content string) error + Uninstall(name string) error + Toggle(name string, enabled bool) error +} + +// Skill is a neutral description of an installed skill. +type Skill struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + Author string `json:"author,omitempty"` + Entry string `json:"entry,omitempty"` + Source string `json:"source,omitempty"` + Enabled bool `json:"enabled"` + RawContent string `json:"-"` +} diff --git a/internal/sdk/status.go b/internal/sdk/status.go new file mode 100644 index 0000000..5772c94 --- /dev/null +++ b/internal/sdk/status.go @@ -0,0 +1,99 @@ +package sdk + +// StatusAPI provides a snapshot of the kernel runtime status. +type StatusAPI interface { + GetKernelStatus() *KernelStatus +} + +// KernelStatus is the aggregated runtime snapshot of all kernel subsystems. +type KernelStatus struct { + Uptime string `json:"uptime"` + StartTime string `json:"start_time"` + + AgentID string `json:"agent_id"` + + Plugins []PluginInfo `json:"plugins"` + Tools []ToolDef `json:"tools"` + Channels []ChannelInfo `json:"channels"` + + Memory MemoryStatus `json:"memory"` + Knowledge KnowledgeStatus `json:"knowledge"` + Documents DocumentStatus `json:"documents"` + TextMemory TextMemoryStatus `json:"text_memory"` + Social SocialStatus `json:"social"` + Skills SkillsStatus `json:"skills"` + + LLM LLMStatus `json:"llm"` + + Context ContextStatus `json:"context"` + + Runtime RuntimeStatus `json:"runtime"` + + Tracker TrackerStatus `json:"tracker"` +} + +type PluginInfo struct { + Name string `json:"name"` + Loaded bool `json:"loaded"` +} + +type ChannelInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Ready bool `json:"ready"` +} + +type MemoryStatus struct { + Available bool `json:"available"` + EntityCount int `json:"entity_count"` + RelationCount int `json:"relation_count"` + EntityTypes int `json:"entity_types"` +} + +type KnowledgeStatus struct { + Available bool `json:"available"` + ItemCount int `json:"item_count"` + Items []string `json:"items,omitempty"` +} + +type DocumentStatus struct { + Available bool `json:"available"` + DocCount int `json:"doc_count"` + VectorCount int `json:"vector_count"` +} + +type TextMemoryStatus struct { + Available bool `json:"available"` + FileCount int `json:"file_count"` +} + +type SocialStatus struct { + Available bool `json:"available"` + PersonCount int `json:"person_count"` +} + +type SkillsStatus struct { + Available bool `json:"available"` + SkillList []string `json:"skill_list,omitempty"` +} + +type LLMStatus struct { + Available bool `json:"available"` + Provider string `json:"provider,omitempty"` + Sources int `json:"sources,omitempty"` +} + +type ContextStatus struct { + EventCount int `json:"event_count,omitempty"` +} + +type RuntimeStatus struct { + Goroutines int `json:"goroutines"` + MemoryMB int64 `json:"memory_mb"` + GoVersion string `json:"go_version"` +} + +type TrackerStatus struct { + Available bool `json:"available"` + Dir string `json:"dir,omitempty"` +} diff --git a/internal/sdk/supervisor.go b/internal/sdk/supervisor.go new file mode 100644 index 0000000..b764103 --- /dev/null +++ b/internal/sdk/supervisor.go @@ -0,0 +1,25 @@ +package sdk + +import ( + "time" + + "gitcode.com/JianFeeeee/HomeAgent/pkg/types" +) + +// SupervisorAPI provides access to agent lifecycle management. +type SupervisorAPI interface { + ListAgents() []AgentStatus + GetAgentStatus(id string) (*AgentStatus, error) + PreActionSnapshot(id string) (*types.Snapshot, error) + RollbackAgent(id string, snapID string) error +} + +// AgentStatus is a neutral snapshot of a supervised agent. +type AgentStatus struct { + ID types.AgentID `json:"id"` + State types.AgentState `json:"state"` + Health types.HealthStatus `json:"health"` + Uptime time.Duration `json:"uptime,omitempty"` + Network types.NetworkCheckResult `json:"network,omitempty"` + TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"` +} diff --git a/internal/sdk/tool.go b/internal/sdk/tool.go new file mode 100644 index 0000000..8542d56 --- /dev/null +++ b/internal/sdk/tool.go @@ -0,0 +1,20 @@ +package sdk + +// ToolSource 描述内核工具注册表/执行器的只读视图(由 StageHost 实现)。 +// 用接口而非具体类型,避免 sdk 依赖内核包(内核包反向依赖 sdk)。 +type ToolSource interface { + GetToolDefs() []ToolDef + ToolDef(name string) *ToolDef + ExecuteTool(name string, args map[string]interface{}) (interface{}, error) +} + +// ToolAPI exposes the kernel tool registry and executor +// (StageHost for plugin tools + IOManager for device/channel tools). +type ToolAPI interface { + // GetToolDefs returns all tools registered on the stage host (plugin tools). + GetToolDefs() []ToolDef + // GetAllTools returns all tools exposed by IO devices/channels. + GetAllTools() []ToolDef + // ExecuteTool executes a tool by name, resolving across the stage host first. + ExecuteTool(name string, args map[string]interface{}) (interface{}, error) +} diff --git a/internal/sdk/tool_impl.go b/internal/sdk/tool_impl.go new file mode 100644 index 0000000..45745f3 --- /dev/null +++ b/internal/sdk/tool_impl.go @@ -0,0 +1,50 @@ +package sdk + +import ( + "fmt" + + agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" +) + +// toolImpl 桥接 StageHost(插件工具)与 IOManager(设备/通道工具)。 +type toolImpl struct { + stageHost ToolSource + iom *agentIO.IOManager +} + +func NewTool(sh ToolSource, iom *agentIO.IOManager) ToolAPI { + return &toolImpl{stageHost: sh, iom: iom} +} + +func (t *toolImpl) GetToolDefs() []ToolDef { + if t.stageHost == nil { + return nil + } + return t.stageHost.GetToolDefs() +} + +func (t *toolImpl) GetAllTools() []ToolDef { + if t.iom == nil { + return nil + } + defs := t.iom.GetAllTools() + out := make([]ToolDef, 0, len(defs)) + for _, d := range defs { + out = append(out, ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters}) + } + return out +} + +func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) { + if t.stageHost != nil { + if def := t.stageHost.ToolDef(name); def != nil { + return t.stageHost.ExecuteTool(name, args) + } + } + if t.iom != nil { + return t.iom.ExecuteTool(name, args) + } + return nil, fmt.Errorf("tool %s not found", name) +} + +var _ ToolAPI = (*toolImpl)(nil) diff --git a/internal/sdk/tracker.go b/internal/sdk/tracker.go new file mode 100644 index 0000000..24dfd40 --- /dev/null +++ b/internal/sdk/tracker.go @@ -0,0 +1,36 @@ +package sdk + +import "time" + +// TrackerAPI provides access to the file change tracker / rollback. +type TrackerAPI interface { + ChangeSets() []*ChangeSet + Rollback() error + Stats() map[string]interface{} + HasChanges() bool +} + +type ChangeType string + +const ( + ChangeFileCreated ChangeType = "created" + ChangeFileModified ChangeType = "modified" + ChangeFileDeleted ChangeType = "deleted" +) + +type FileChange struct { + Path string `json:"path"` + Type ChangeType `json:"type"` + SizeBefore int64 `json:"size_before,omitempty"` + SizeAfter int64 `json:"size_after,omitempty"` + HashBefore string `json:"hash_before,omitempty"` + HashAfter string `json:"hash_after,omitempty"` + Content []byte `json:"-"` // 回滚用原始内容,不参与 JSON 序列化 +} + +type ChangeSet struct { + ID string `json:"id"` + Action string `json:"action"` + Timestamp time.Time `json:"timestamp"` + Files []FileChange `json:"files"` +} diff --git a/internal/skill/manager.go b/internal/skill/manager.go index 87ca0db..67eb635 100644 --- a/internal/skill/manager.go +++ b/internal/skill/manager.go @@ -8,18 +8,12 @@ import ( "sort" "strings" "sync" + + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) -type Skill struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - Author string `json:"author,omitempty"` - Entry string `json:"entry,omitempty"` - Source string `json:"source,omitempty"` - Enabled bool `json:"enabled"` - RawContent string `json:"-"` -} +// Skill 已迁入内置 SDK,此处保留别名以兼容现有调用方。 +type Skill = sdk.Skill type Manager struct { mu sync.RWMutex @@ -195,6 +189,9 @@ func (m *Manager) GetInjectedPrompt() string { return strings.Join(parts, "\n\n") } +// Manager 直接满足内置 SDK 的 SkillAPI(复用优先,无需独立适配器)。 +var _ sdk.SkillAPI = (*Manager)(nil) + func extractDescription(content string) string { lines := strings.Split(content, "\n") for _, line := range lines { diff --git a/internal/supervisor/daemon.go b/internal/supervisor/daemon.go index 7235474..0ddfb7f 100644 --- a/internal/supervisor/daemon.go +++ b/internal/supervisor/daemon.go @@ -9,6 +9,7 @@ import ( "gitcode.com/JianFeeeee/HomeAgent/internal/network" "gitcode.com/JianFeeeee/HomeAgent/internal/tracker" + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" "gitcode.com/JianFeeeee/HomeAgent/pkg/types" ) @@ -224,11 +225,5 @@ func (d *Daemon) RollbackAgent(id types.AgentID, snapID types.SnapshotID) error return fmt.Errorf("no tracker available for rollback") } -type AgentStatus struct { - ID types.AgentID `json:"id"` - State types.AgentState `json:"state"` - Health types.HealthStatus `json:"health"` - Uptime time.Duration `json:"uptime,omitempty"` - Network types.NetworkCheckResult `json:"network,omitempty"` - TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"` -} +// AgentStatus 已迁入内置 SDK,此处保留别名以兼容现有调用方。 +type AgentStatus = sdk.AgentStatus diff --git a/internal/supervisor/sdk_adapter.go b/internal/supervisor/sdk_adapter.go new file mode 100644 index 0000000..a39a7f7 --- /dev/null +++ b/internal/supervisor/sdk_adapter.go @@ -0,0 +1,48 @@ +package supervisor + +import ( + "errors" + + "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" + "gitcode.com/JianFeeeee/HomeAgent/pkg/types" +) + +// sdkAdapter 实现 sdk.SupervisorAPI,将字符串签名的中立接口桥接到 +// Daemon 的强类型签名(types.AgentID / types.SnapshotID)。 +type sdkAdapter struct { + d *Daemon +} + +func NewSDKAdapter(d *Daemon) sdk.SupervisorAPI { + return &sdkAdapter{d: d} +} + +func (a *sdkAdapter) ListAgents() []sdk.AgentStatus { + if a.d == nil { + return nil + } + return a.d.ListAgents() +} + +func (a *sdkAdapter) GetAgentStatus(id string) (*sdk.AgentStatus, error) { + if a.d == nil { + return nil, errors.New("supervisor not available") + } + return a.d.GetAgentStatus(types.AgentID(id)) +} + +func (a *sdkAdapter) PreActionSnapshot(id string) (*types.Snapshot, error) { + if a.d == nil { + return nil, errors.New("supervisor not available") + } + return a.d.PreActionSnapshot(types.AgentID(id)) +} + +func (a *sdkAdapter) RollbackAgent(id string, snapID string) error { + if a.d == nil { + return errors.New("supervisor not available") + } + return a.d.RollbackAgent(types.AgentID(id), types.SnapshotID(snapID)) +} + +var _ sdk.SupervisorAPI = (*sdkAdapter)(nil) diff --git a/internal/tracker/changeset.go b/internal/tracker/changeset.go index 1dabc03..a866e4c 100644 --- a/internal/tracker/changeset.go +++ b/internal/tracker/changeset.go @@ -7,32 +7,21 @@ import ( "os" "path/filepath" "time" + + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) -type ChangeType string +// DTO 已迁入内置 SDK,此处保留别名以兼容现有调用方。 +type ChangeType = sdk.ChangeType const ( - ChangeFileCreated ChangeType = "created" - ChangeFileModified ChangeType = "modified" - ChangeFileDeleted ChangeType = "deleted" + ChangeFileCreated = sdk.ChangeFileCreated + ChangeFileModified = sdk.ChangeFileModified + ChangeFileDeleted = sdk.ChangeFileDeleted ) -type FileChange struct { - Path string `json:"path"` - Type ChangeType `json:"type"` - SizeBefore int64 `json:"size_before,omitempty"` - SizeAfter int64 `json:"size_after,omitempty"` - HashBefore string `json:"hash_before,omitempty"` - HashAfter string `json:"hash_after,omitempty"` - Content []byte `json:"-"` // stored separately, not in JSON -} - -type ChangeSet struct { - ID string `json:"id"` - Action string `json:"action"` - Timestamp time.Time `json:"timestamp"` - Files []FileChange `json:"files"` -} +type FileChange = sdk.FileChange +type ChangeSet = sdk.ChangeSet func NewChangeSet(action string) *ChangeSet { return &ChangeSet{ diff --git a/internal/tracker/tracker.go b/internal/tracker/tracker.go index e732609..d6ea30e 100644 --- a/internal/tracker/tracker.go +++ b/internal/tracker/tracker.go @@ -11,6 +11,8 @@ import ( "strings" "sync" "time" + + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) type Tracker struct { @@ -302,3 +304,6 @@ func (t *Tracker) Stats() map[string]interface{} { "max_changeset_age": t.maxChangesetAge.String(), } } + +// Tracker 直接满足内置 SDK 的 TrackerAPI(复用优先,无需独立适配器)。 +var _ sdk.TrackerAPI = (*Tracker)(nil)