diff --git a/cmd/homed/main.go b/cmd/homed/main.go index 89f65b6..e30f186 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -30,7 +30,7 @@ import ( "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/openclaw" + openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" pluginmgr "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" diff --git a/internal/agent/core/agent.go b/internal/agent/core/agent.go index f133203..5012f77 100644 --- a/internal/agent/core/agent.go +++ b/internal/agent/core/agent.go @@ -810,6 +810,21 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri } for _, tc := range resp.ToolCalls { + // === 工具执行前检查打断通道 === + if len(a.interceptCh) > 0 { + for _, interrupt := range a.drainInterrupts() { + msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt}) + } + a.publishEvent(events.EventToolCall, map[string]interface{}{ + "tool": tc.Name, + "plugin": a.resolveToolPlugin(tc.Name), + "args": tc.Arguments, + "status": "interrupted", + "reason": "user interrupt before execution", + }) + break + } + toolsUsed = append(toolsUsed, tc.Name) pluginName := a.resolveToolPlugin(tc.Name) log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID) @@ -866,6 +881,14 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri "result": result, "status": "ok", }) + + // === 工具执行后检查打断通道 === + if len(a.interceptCh) > 0 { + for _, interrupt := range a.drainInterrupts() { + msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt}) + } + break + } } } } diff --git a/internal/plugins/all.go b/internal/plugins/all.go index fad11f1..f707c47 100644 --- a/internal/plugins/all.go +++ b/internal/plugins/all.go @@ -8,7 +8,7 @@ import ( _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/files" _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/mcp" - _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/openclaw" + _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/timer" _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui" diff --git a/internal/plugins/openclaw/manager/main.js b/internal/plugins/clawhubadapter/manager/main.js similarity index 85% rename from internal/plugins/openclaw/manager/main.js rename to internal/plugins/clawhubadapter/manager/main.js index 6779e04..0efdc0d 100644 --- a/internal/plugins/openclaw/manager/main.js +++ b/internal/plugins/clawhubadapter/manager/main.js @@ -19,6 +19,7 @@ function readJSON(file) { // ---- Plugin registry ---- const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] } const allTools = []; // flat list of all tools across all plugins +const allProviders = {}; // type -> { name, instance } across all plugins function registerPluginTools(name, tools, api) { for (const t of tools) { @@ -98,15 +99,27 @@ function loadPlugin(pluginDir, name) { if (!def || !def.name) return; registeredTools.push({ name: def.name, label: def.label || def.name, description: def.description || '', parameters: def.parameters || { type: 'object', properties: {} }, execute: typeof def.execute === 'function' ? def.execute : undefined }); }, - registerProvider: (p) => notify('register', { type: 'provider', data: { name: p.name } }), + registerProvider: (p) => { + if (p && p.id) allProviders['llm'] = { name: p.id, instance: p }; + notify('register', { type: 'provider', data: { name: p?.id || p?.name } }); + }, registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }), registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }), registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }), registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }), registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }), - registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }), - registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }), - registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }), + registerImageGenerationProvider: (p) => { + if (p) allProviders['image_generation'] = { name: p.name, instance: p }; + notify('register', { type: 'image_generation_provider', data: { name: p?.name } }); + }, + registerWebFetchProvider: (p) => { + if (p) allProviders['web_fetch'] = { name: p.name, instance: p }; + notify('register', { type: 'web_fetch_provider', data: { name: p?.name } }); + }, + registerWebSearchProvider: (p) => { + if (p) allProviders['web_search'] = { name: p.name, instance: p }; + notify('register', { type: 'web_search_provider', data: { name: p?.name } }); + }, start: (cb) => {}, stop: (cb) => {}, }; @@ -319,5 +332,30 @@ rl.on('line', async (line) => { return; } + if (method === 'provider/call') { + const { type, action, args } = req.params || {}; + if (!type) { sendError(id, -32602, 'type required'); return; } + + const provider = allProviders[type]; + if (!provider) { sendError(id, -32601, `Provider not found: ${type}`); return; } + + const methodName = action || 'execute'; + if (typeof provider.instance[methodName] !== 'function') { + sendError(id, -32603, `Provider ${type} has no method ${methodName}`); + return; + } + + try { + const result = await provider.instance[methodName](args); + if (result && typeof result === 'object' && Array.isArray(result.content)) { + writeJSON({ jsonrpc: '2.0', id, result }); + } else { + const text = typeof result === 'string' ? result : JSON.stringify(result); + writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } }); + } + } catch (e) { sendError(id, -32603, e.message); } + return; + } + sendError(id, -32601, `Method not found: ${method}`); }); diff --git a/internal/plugins/openclaw/plugin.go b/internal/plugins/clawhubadapter/plugin.go similarity index 59% rename from internal/plugins/openclaw/plugin.go rename to internal/plugins/clawhubadapter/plugin.go index 0e1cbad..ed9a548 100644 --- a/internal/plugins/openclaw/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -1,10 +1,17 @@ -package openclaw +package clawhubadapter import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" _ "embed" "encoding/json" "fmt" + "io" "log" + "net/http" + "net/url" "os" "path/filepath" "strings" @@ -27,13 +34,13 @@ var SkillsDir string var SimulatorDir string func init() { - plugin.RegisterPluginMeta("openclaw", "开放式交互", "OpenClaw") - plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) { + 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("openclaw plugin: config missing 'data_dir' or not a string") + return nil, fmt.Errorf("clawhubadapter plugin: config missing 'data_dir' or not a string") } dir = filepath.Join(dataDir, "skills") } @@ -42,15 +49,16 @@ func init() { } type Plugin struct { - name string - skillsDir string - simulatorDir string - skills []*plugin.SKILLPlugin - sidecars []*sidecarProcess - manager *sidecarProcess - capabilities []string - mu sync.Mutex - sdk *sdk.PluginSDK + name string + skillsDir string + simulatorDir string + skills []*plugin.SKILLPlugin + sidecars []*sidecarProcess + manager *sidecarProcess + mu sync.Mutex + sdk *sdk.PluginSDK + dispatcher *RegistryDispatcher + httpClient *http.Client } func New(name, skillsDir string) *Plugin { @@ -62,6 +70,7 @@ func New(name, skillsDir string) *Plugin { name: name, skillsDir: skillsDir, simulatorDir: sd, + dispatcher: NewDispatcher(), } } @@ -79,6 +88,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { Key: "simulator_dir", Type: "string", DisplayName: "模拟器工作目录", Description: "OpenClaw 模拟器工作目录路径(留空则使用默认路径)", }) + p.httpClient = &http.Client{} if v, _ := s.Settings().Get("skills_dir"); v != nil { if s, ok := v.(string); ok && s != "" { p.skillsDir = s @@ -93,7 +103,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { // Launch OC plugin manager first (handles OC-format plugin installation and lifecycle) os.MkdirAll(p.skillsDir, 0755) if err := p.launchManager(s); err != nil { - log.Printf("[openclaw] launch manager: %v", err) + log.Printf("[clawhubadapter] launch manager: %v", err) } // Load existing plugins from skills dir @@ -125,22 +135,22 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { switch { case hasMainJS: if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil { - log.Printf("[openclaw] sidecar %s: %v", entry.Name(), err) + log.Printf("[clawhubadapter] sidecar %s: %v", entry.Name(), err) } case hasMainPy: if err := p.loadPySidecar(s, skillPath, entry.Name()); err != nil { - log.Printf("[openclaw] pysidecar %s: %v", entry.Name(), err) + log.Printf("[clawhubadapter] pysidecar %s: %v", entry.Name(), err) } case hasOCManifest || hasOCPackage: - log.Printf("[openclaw] ocplugin %s handled by manager", entry.Name()) + log.Printf("[clawhubadapter] ocplugin %s handled by manager", entry.Name()) default: sk, err := plugin.LoadSKILL(skillPath) if err != nil { - log.Printf("[openclaw] load skill %s: %v", entry.Name(), err) + log.Printf("[clawhubadapter] load skill %s: %v", entry.Name(), err) continue } p.skills = append(p.skills, sk) - log.Printf("[openclaw] loaded skill: %s v%s", sk.Name(), sk.Version()) + log.Printf("[clawhubadapter] loaded skill: %s v%s", sk.Name(), sk.Version()) } } } @@ -149,11 +159,11 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { tp := p.name + "_" s.RegisterTool(tp+"npm_install", sdk.ToolDef{ Name: tp + "npm_install", - Description: "安装 OpenClaw 插件管理器中的 npm 插件。通过 npm 安装包,自动检测并加载到模拟器中。安装后立即可用。", + Description: "安装 ClawHub 适配器插件管理器中的插件。支持 npm: 前缀(npm 包)和 clawhub: 前缀(ClawHub 市场)。安装后立即可用。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "package": map[string]interface{}{"type": "string", "description": "npm 包名或 git 地址 (例如 @openclaw/voice-call, npm:@openclaw/matrix)"}, + "package": map[string]interface{}{"type": "string", "description": "插件包标识。npm: 从 npm 安装,clawhub: 从 ClawHub 市场安装(如 clawhub:openclaw-codex-app-server)"}, }, "required": []string{"package"}, }, @@ -161,7 +171,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { s.RegisterTool(tp+"npm_uninstall", sdk.ToolDef{ Name: tp + "npm_uninstall", - Description: "从 OpenClaw 插件管理器中移除已安装的插件。", + Description: "从插件管理器中移除已安装的插件。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ @@ -171,9 +181,21 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { }, }, p.handlePluginUninstall) + s.RegisterTool(tp+"search", sdk.ToolDef{ + Name: tp + "search", + Description: "搜索 ClawHub 插件市场,查找可安装的插件。返回插件名称、描述和安装命令提示。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "query": map[string]interface{}{"type": "string", "description": "搜索关键词"}, + }, + "required": []string{"query"}, + }, + }, p.handleClawHubSearch) + s.RegisterTool(tp+"list", sdk.ToolDef{ Name: tp + "list", - Description: "列出插件管理器中所有已安装的 OpenClaw 插件及其工具。", + Description: "列出所有已安装的 ClawHub 插件及其工具。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{}, @@ -222,7 +244,7 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error { p.sidecars = append(p.sidecars, sp) p.mu.Unlock() - log.Printf("[openclaw] OC plugin manager started") + log.Printf("[clawhubadapter] OC plugin manager started") return nil } @@ -234,6 +256,10 @@ func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{}, return errorResult("package is required"), nil } + if strings.HasPrefix(pkg, "clawhub:") { + return p.installFromClawHub(pkg) + } + p.mu.Lock() mgr := p.manager p.mu.Unlock() @@ -264,6 +290,102 @@ func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{}, }, nil } +const clawhubAPI = "https://clawhub.ai/api/v1" + +func (p *Plugin) installFromClawHub(spec string) (interface{}, error) { + name := strings.TrimPrefix(spec, "clawhub:") + if name == "" { + return errorResult("clawhub package name is required"), nil + } + + pkgURL := fmt.Sprintf("%s/packages/%s/download", clawhubAPI, url.PathEscape(name)) + resp, err := p.httpClient.Get(pkgURL) + if err != nil { + return errorResult(fmt.Sprintf("download failed: %v", err)), nil + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return errorResult(fmt.Sprintf("ClawHub API error (status %d): %s", resp.StatusCode, string(body))), nil + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return errorResult(fmt.Sprintf("read download: %v", err)), nil + } + + extractDir := filepath.Join(p.skillsDir, name) + os.MkdirAll(extractDir, 0755) + + if err := extractArchive(data, extractDir); err != nil { + return errorResult(fmt.Sprintf("extract failed: %v", err)), nil + } + + if err := p.reloadPlugin(name); err != nil { + return errorResult(fmt.Sprintf("loaded but with warning: %v", err)), nil + } + + return map[string]interface{}{ + "content": fmt.Sprintf("已从 ClawHub 安装插件: %s", name), + }, nil +} + +func (p *Plugin) handleClawHubSearch(args map[string]interface{}) (interface{}, error) { + query, _ := args["query"].(string) + if query == "" { + return errorResult("query is required"), nil + } + + searchURL := fmt.Sprintf("%s/search?q=%s", clawhubAPI, url.QueryEscape(query)) + resp, err := p.httpClient.Get(searchURL) + if err != nil { + return errorResult(fmt.Sprintf("search failed: %v", err)), nil + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + var result struct { + Results []struct { + Slug string `json:"slug"` + DisplayName string `json:"displayName"` + Summary string `json:"summary"` + Version string `json:"version"` + Downloads int `json:"downloads"` + } `json:"results"` + } + if err := json.Unmarshal(body, &result); err != nil { + return map[string]interface{}{ + "content": fmt.Sprintf("Search results (raw):\n%s", string(body)), + }, nil + } + + if len(result.Results) == 0 { + return map[string]interface{}{ + "content": fmt.Sprintf("未找到匹配 \"%s\" 的 ClawHub 插件", query), + }, nil + } + + var lines []string + for _, pkg := range result.Results { + ver := pkg.Version + if ver == "" { + ver = "latest" + } + name := pkg.DisplayName + if name == "" { + name = pkg.Slug + } + lines = append(lines, fmt.Sprintf("- %s (%s) v%s | ⬇ %d\n %s\n 安装: clawhubadapter_npm_install package=clawhub:%s", + name, pkg.Slug, ver, pkg.Downloads, pkg.Summary, pkg.Slug)) + } + + return map[string]interface{}{ + "content": fmt.Sprintf("在 ClawHub 找到 %d 个插件:\n%s", len(result.Results), strings.Join(lines, "\n")), + }, nil +} + func (p *Plugin) handlePluginUninstall(args map[string]interface{}) (interface{}, error) { name, _ := args["name"].(string) if name == "" { @@ -351,60 +473,62 @@ func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, err parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version())) } } - if len(p.capabilities) > 0 { - parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(p.capabilities))) - for _, c := range p.capabilities { - parts = append(parts, fmt.Sprintf(" %s", c)) - } + caps := p.dispatcher.Capabilities() + if len(caps) > 0 { + parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps))) + for _, c := range caps { + parts = append(parts, fmt.Sprintf(" %s", c)) } - if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 { - parts = append(parts, "没有已安装的插件。") - } - p.mu.Unlock() - - return map[string]interface{}{ - "content": strings.Join(parts, "\n"), - }, nil } - } - } - - // Fallback: list known plugins from Go side - p.mu.Lock() - defer p.mu.Unlock() - - var parts []string - parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir)) - - if len(p.sidecars) > 0 { - parts = append(parts, fmt.Sprintf("\nSidecar/OC 插件 (%d):", len(p.sidecars))) - for _, sp := range p.sidecars { - tools, err := sp.ListTools() - toolList := "" - if err == nil { - var names []string - for _, t := range tools { - names = append(names, t.Name) - } - toolList = strings.Join(names, ", ") + if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 { + parts = append(parts, "没有已安装的插件。") } - parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, toolList)) - } - } + p.mu.Unlock() - if len(p.skills) > 0 { - parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills))) - for _, sk := range p.skills { - parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version())) + return map[string]interface{}{ + "content": strings.Join(parts, "\n"), + }, nil } } +} - if len(p.capabilities) > 0 { - parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(p.capabilities))) - for _, c := range p.capabilities { - parts = append(parts, fmt.Sprintf(" %s", c)) +// Fallback: list known plugins from Go side +p.mu.Lock() +defer p.mu.Unlock() + +var parts []string +parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir)) + +if len(p.sidecars) > 0 { + parts = append(parts, fmt.Sprintf("\nSidecar/OC 插件 (%d):", len(p.sidecars))) + for _, sp := range p.sidecars { + tools, err := sp.ListTools() + toolList := "" + if err == nil { + var names []string + for _, t := range tools { + names = append(names, t.Name) + } + toolList = strings.Join(names, ", ") } + parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, toolList)) } +} + +if len(p.skills) > 0 { + parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills))) + for _, sk := range p.skills { + parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version())) + } +} + +caps := p.dispatcher.Capabilities() +if len(caps) > 0 { + parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps))) + for _, c := range caps { + parts = append(parts, fmt.Sprintf(" %s", c)) + } +} if len(p.sidecars) == 0 && len(p.skills) == 0 { parts = append(parts, "没有已安装的插件。") @@ -453,7 +577,7 @@ func (p *Plugin) loadOCPlugin(s *sdk.PluginSDK, dir, name string) error { sp.Close() return fmt.Errorf("list tools: %w", err) } - log.Printf("[openclaw] ocplugin %s verified %d tools via ListTools", name, len(tools)) + log.Printf("[clawhubadapter] ocplugin %s verified %d tools via ListTools", name, len(tools)) p.mu.Lock() p.sidecars = append(p.sidecars, sp) @@ -480,94 +604,7 @@ func (p *Plugin) translateAndRegister(n OCNotification, sp *sidecarProcess, s *s if err := json.Unmarshal(n.Params, ¶ms); err != nil { return } - - switch params.Type { - - case "tool": - var d struct { - Name string `json:"name"` - Label string `json:"label"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters"` - } - if err := json.Unmarshal(params.Data, &d); err != nil || d.Name == "" { - return - } - toolName := fmt.Sprintf("%s_%s", pluginName, d.Name) - tDef := sdk.ToolDef{ - Name: toolName, - Description: d.Description, - Parameters: d.Parameters, - } - handler := func(sp *sidecarProcess, ocToolName string) sdk.ToolHandler { - return func(args map[string]interface{}) (interface{}, error) { - return sp.CallTool(ocToolName, args) - } - }(sp, d.Name) - if err := s.RegisterTool(toolName, tDef, handler); err != nil { - log.Printf("[openclaw] translate register tool %s: %v", toolName, err) - } - - case "provider": - var d struct { - Name string `json:"name"` - Description string `json:"description"` - } - json.Unmarshal(params.Data, &d) - cap := fmt.Sprintf("[%s] provides %s provider", pluginName, d.Name) - if d.Description != "" { - cap += ": " + d.Description - } - p.mu.Lock() - p.capabilities = append(p.capabilities, cap) - p.mu.Unlock() - - case "channel": - var d struct { - Name string `json:"name"` - Type string `json:"type"` - } - json.Unmarshal(params.Data, &d) - cap := fmt.Sprintf("[%s] registers channel: %s (type: %s)", pluginName, d.Name, d.Type) - p.mu.Lock() - p.capabilities = append(p.capabilities, cap) - p.mu.Unlock() - - case "image_generation_provider": - var d struct{ Name string `json:"name"` } - json.Unmarshal(params.Data, &d) - p.mu.Lock() - p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] image generation provider: %s", pluginName, d.Name)) - p.mu.Unlock() - - case "web_fetch_provider": - var d struct{ Name string `json:"name"` } - json.Unmarshal(params.Data, &d) - p.mu.Lock() - p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] web fetch provider: %s", pluginName, d.Name)) - p.mu.Unlock() - - case "web_search_provider": - var d struct{ Name string `json:"name"` } - json.Unmarshal(params.Data, &d) - p.mu.Lock() - p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] web search provider: %s", pluginName, d.Name)) - p.mu.Unlock() - - default: - var d struct { - Name string `json:"name"` - Description string `json:"description"` - } - json.Unmarshal(params.Data, &d) - cap := fmt.Sprintf("[%s] capability: %s", pluginName, params.Type) - if d.Name != "" { - cap += " (" + d.Name + ")" - } - p.mu.Lock() - p.capabilities = append(p.capabilities, cap) - p.mu.Unlock() - } + p.dispatcher.Dispatch(params.Type, params.Data, pluginName, sp, s) } func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error { @@ -612,7 +649,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error { sp.Close() return fmt.Errorf("list tools: %w", err) } - log.Printf("[openclaw] pysidecar %s registered %d tools", name, len(tools)) + log.Printf("[clawhubadapter] pysidecar %s registered %d tools", name, len(tools)) p.mu.Lock() p.sidecars = append(p.sidecars, sp) @@ -648,16 +685,16 @@ func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error { } }(sp, tool.Name) if err := s.RegisterTool(toolName, tDef, handler); err != nil { - log.Printf("[openclaw] register sidecar tool %s: %v", toolName, err) + log.Printf("[clawhubadapter] register sidecar tool %s: %v", toolName, err) continue } - log.Printf("[openclaw] registered sidecar tool: %s (from %s)", toolName, name) + log.Printf("[clawhubadapter] registered sidecar tool: %s (from %s)", toolName, name) } p.mu.Lock() p.sidecars = append(p.sidecars, sp) p.mu.Unlock() - log.Printf("[openclaw] sidecar %s started with %d tools", name, len(tools)) + log.Printf("[clawhubadapter] sidecar %s started with %d tools", name, len(tools)) return nil } @@ -698,3 +735,134 @@ func hasOCExtensions(pkgPath string) bool { } return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil) } + +func (p *Plugin) reloadPlugin(name string) error { + pluginDir := filepath.Join(p.skillsDir, name) + info, err := os.Stat(pluginDir) + if err != nil || !info.IsDir() { + return fmt.Errorf("plugin dir not found: %s", pluginDir) + } + + if p.sdk == nil { + return fmt.Errorf("sdk not initialized") + } + + subs, err := os.ReadDir(pluginDir) + if err != nil { + return err + } + + hasMainJS := false + hasMainPy := false + hasOCManifest := false + for _, f := range subs { + switch f.Name() { + case "main.js": + hasMainJS = true + case "main.py": + hasMainPy = true + case "openclaw.plugin.json": + hasOCManifest = true + } + } + + os.MkdirAll(p.simulatorDir, 0755) + + switch { + case hasMainJS: + return p.loadSidecar(p.sdk, pluginDir, name) + case hasMainPy: + return p.loadPySidecar(p.sdk, pluginDir, name) + case hasOCManifest: + return p.loadOCPlugin(p.sdk, pluginDir, name) + default: + sk, err := plugin.LoadSKILL(pluginDir) + if err != nil { + return fmt.Errorf("load skill: %w", err) + } + p.mu.Lock() + p.skills = append(p.skills, sk) + p.mu.Unlock() + return nil + } +} + +func extractArchive(data []byte, dest string) error { + if len(data) < 4 { + return fmt.Errorf("archive too small (%d bytes)", len(data)) + } + + if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 { + return extractZIP(data, dest) + } + + return extractTGZ(data, dest) +} + +func extractZIP(data []byte, dest string) error { + zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return fmt.Errorf("zip: %w", err) + } + for _, f := range zr.File { + target := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(target, 0755) + continue + } + os.MkdirAll(filepath.Dir(target), 0755) + r, err := f.Open() + if err != nil { + return err + } + out, err := os.Create(target) + if err != nil { + r.Close() + return err + } + _, err = io.Copy(out, r) + r.Close() + out.Close() + if err != nil { + return err + } + } + return nil +} + +func extractTGZ(data []byte, dest string) error { + gzr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("gzip: %w", err) + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(dest, header.Name) + switch header.Typeflag { + case tar.TypeDir: + os.MkdirAll(target, 0755) + case tar.TypeReg: + os.MkdirAll(filepath.Dir(target), 0755) + f, err := os.Create(target) + if err != nil { + return err + } + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } + return nil +} diff --git a/internal/plugins/openclaw/pysimulator/main.py b/internal/plugins/clawhubadapter/pysimulator/main.py similarity index 100% rename from internal/plugins/openclaw/pysimulator/main.py rename to internal/plugins/clawhubadapter/pysimulator/main.py diff --git a/internal/plugins/clawhubadapter/registry.go b/internal/plugins/clawhubadapter/registry.go new file mode 100644 index 0000000..7adff38 --- /dev/null +++ b/internal/plugins/clawhubadapter/registry.go @@ -0,0 +1,251 @@ +package clawhubadapter + +import ( + "encoding/json" + "fmt" + "log" + "sync" + + agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" +) + +type ToolRegistry struct{} + +func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { + var d struct { + Name string `json:"name"` + Label string `json:"label"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + } + if err := json.Unmarshal(data, &d); err != nil || d.Name == "" { + return + } + toolName := fmt.Sprintf("%s_%s", pluginName, d.Name) + tDef := sdk.ToolDef{ + Name: toolName, + Description: d.Description, + Parameters: d.Parameters, + } + handler := func(sp *sidecarProcess, ocToolName string) sdk.ToolHandler { + return func(args map[string]interface{}) (interface{}, error) { + return sp.CallTool(ocToolName, args) + } + }(sp, d.Name) + if err := s.RegisterTool(toolName, tDef, handler); err != nil { + log.Printf("[clawhubadapter] register tool %s: %v", toolName, err) + } +} + +type providerDef struct { + ocType string + toolSuffix string + desc string +} + +var providerMap = []providerDef{ + {"image_generation", "generate_image", "根据文本描述生成图片,返回图片 URL"}, + {"music_generation", "generate_music", "根据描述生成音乐"}, + {"video_generation", "generate_video", "根据描述生成视频"}, + {"speech", "synthesize_speech", "将文本合成为语音"}, + {"web_search", "web_search", "搜索互联网信息"}, + {"web_fetch", "web_fetch", "获取指定网页的内容"}, + {"media_understanding", "analyze_media", "分析图片、音频或视频内容"}, + {"realtime_transcription", "transcribe_audio", "将音频转写为文字"}, + {"realtime_voice", "voice_io", "实时语音输入输出"}, +} + +type ProviderRegistry struct{} + +func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { + var d struct { + Name string `json:"name"` + Description string `json:"description"` + } + json.Unmarshal(data, &d) + + for _, p := range providerMap { + if p.ocType == typeStr { + toolName := fmt.Sprintf("%s_%s", pluginName, p.toolSuffix) + desc := p.desc + if d.Name != "" { + desc = fmt.Sprintf("[%s] %s", d.Name, desc) + } + tDef := sdk.ToolDef{ + Name: toolName, + Description: desc, + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + } + handler := func(sp *sidecarProcess, ocType string) sdk.ToolHandler { + return func(args map[string]interface{}) (interface{}, error) { + return sp.CallProvider(ocType, args) + } + }(sp, typeStr) + if err := s.RegisterTool(toolName, tDef, handler); err != nil { + log.Printf("[clawhubadapter] register provider tool %s: %v", toolName, err) + } + return + } + } + + log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName) +} + +type channelDevice struct { + name string + pluginName string + sp *sidecarProcess + ocType string +} + +func (d *channelDevice) Name() string { return d.name } +func (d *channelDevice) Type() agentIO.DeviceType { return agentIO.DeviceIO } +func (d *channelDevice) Description() string { return fmt.Sprintf("OC channel %s (from %s)", d.name, d.pluginName) } +func (d *channelDevice) OutputCapabilities() agentIO.OutputCapability { return ocTypeToCap(d.ocType) } +func (d *channelDevice) Start() error { return nil } +func (d *channelDevice) Stop() error { return nil } +func (d *channelDevice) Tools() []agentIO.ToolDef { return nil } +func (d *channelDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) { + return d.sp.CallTool(tool, args) +} + +func ocTypeToCap(t string) agentIO.OutputCapability { + switch t { + case "text": + return agentIO.CapText + case "file": + return agentIO.CapFile + case "image": + return agentIO.CapImage + case "audio": + return agentIO.CapAudio + default: + return agentIO.CapText + } +} + +type ChannelRegistry struct{} + +func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { + var d struct { + Name string `json:"name"` + Type string `json:"type"` + } + if err := json.Unmarshal(data, &d); err != nil || d.Name == "" { + return + } + dev := &channelDevice{ + name: d.Name, + pluginName: pluginName, + sp: sp, + ocType: d.Type, + } + if err := s.RegisterChannel(d.Name, dev); err != nil { + log.Printf("[clawhubadapter] register channel %s: %v", d.Name, err) + } +} + +type StageRegistry struct{} + +func (r *StageRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { + var d struct { + Name string `json:"name"` + Event string `json:"event"` + } + json.Unmarshal(data, &d) + log.Printf("[clawhubadapter] hook %s/%s (plugin: %s) — stub: OC hooks need bidirectional bridge", + d.Name, d.Event, pluginName) +} + +type CapRecorder struct { + mu sync.Mutex + caps []string +} + +func (r *CapRecorder) Record(typeStr string, data json.RawMessage, pluginName string) { + var d struct { + Name string `json:"name"` + Description string `json:"description"` + } + json.Unmarshal(data, &d) + capStr := fmt.Sprintf("[%s] capability: %s", pluginName, typeStr) + if d.Name != "" { + capStr += " (" + d.Name + ")" + } + if d.Description != "" { + capStr += ": " + d.Description + } + r.mu.Lock() + r.caps = append(r.caps, capStr) + r.mu.Unlock() +} + +func (r *CapRecorder) Snapshot() []string { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]string, len(r.caps)) + copy(out, r.caps) + return out +} + +type RegistryDispatcher struct { + toolReg *ToolRegistry + providerReg *ProviderRegistry + channelReg *ChannelRegistry + stageReg *StageRegistry + capRecorder *CapRecorder +} + +func NewDispatcher() *RegistryDispatcher { + return &RegistryDispatcher{ + toolReg: &ToolRegistry{}, + providerReg: &ProviderRegistry{}, + channelReg: &ChannelRegistry{}, + stageReg: &StageRegistry{}, + capRecorder: &CapRecorder{}, + } +} + +func (d *RegistryDispatcher) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { + switch typeStr { + case "tool": + d.toolReg.Dispatch(data, pluginName, sp, s) + case "channel": + d.channelReg.Dispatch(data, pluginName, sp, s) + case "provider", "image_generation_provider", "music_generation_provider", + "video_generation_provider", "speech_provider", + "realtime_transcription_provider", "realtime_voice_provider", + "media_understanding_provider", + "web_fetch_provider", "web_search_provider", + "embedding_provider", "memory_embedding_provider": + d.providerReg.Dispatch(typeStr, data, pluginName, sp, s) + d.capRecorder.Record(typeStr, data, pluginName) + case "hook", "runtime_lifecycle", "lifecycle", + "agent_event_subscription", "agent_harness", + "session_event", "conversation_binding_resolved", + "interactive_handler", + "cli", "cli_backend", "node_cli_feature", + "command", "http_route", "service", + "gateway_method", "gateway_discovery_service", + "trusted_tool_policy", "tool_metadata", + "context_engine", + "memory_capability", "memory_prompt_section", + "memory_flush_plan", "memory_runtime", + "memory_prompt_supplement", "memory_corpus_supplement", + "session_extension", "session_scheduler_job", + "session_action", "control_ui_descriptor", + "agent_tool_result_middleware": + d.stageReg.Dispatch(data, pluginName, sp, s) + d.capRecorder.Record(typeStr, data, pluginName) + default: + d.capRecorder.Record(typeStr, data, pluginName) + } +} + +func (d *RegistryDispatcher) Capabilities() []string { + return d.capRecorder.Snapshot() +} diff --git a/internal/plugins/openclaw/sidecar.go b/internal/plugins/clawhubadapter/sidecar.go similarity index 89% rename from internal/plugins/openclaw/sidecar.go rename to internal/plugins/clawhubadapter/sidecar.go index 1f6a620..d54c178 100644 --- a/internal/plugins/openclaw/sidecar.go +++ b/internal/plugins/clawhubadapter/sidecar.go @@ -1,4 +1,4 @@ -package openclaw +package clawhubadapter import ( "bufio" @@ -101,7 +101,7 @@ func (s *sidecarProcess) readLoop(r io.Reader) { if !scanner.Scan() { if scanner.Err() != nil { - log.Printf("[openclaw] sidecar %s read error: %v", s.name, scanner.Err()) + log.Printf("[clawhubadapter] sidecar %s read error: %v", s.name, scanner.Err()) } return } @@ -131,7 +131,7 @@ func (s *sidecarProcess) readLoop(r io.Reader) { select { case s.notifyCh <- notif: default: - log.Printf("[openclaw] sidecar %s notify channel full, dropping: %s", s.name, notif.Method) + log.Printf("[clawhubadapter] sidecar %s notify channel full, dropping: %s", s.name, notif.Method) } } } @@ -305,6 +305,31 @@ func (s *sidecarProcess) CallTool(name string, args map[string]interface{}) (str return sb, nil } +func (s *sidecarProcess) CallProvider(providerType string, args map[string]interface{}) (string, error) { + data, err := s.call("provider/call", map[string]interface{}{ + "type": providerType, + "action": "execute", + "args": args, + }) + if err != nil { + return "", err + } + if data == nil { + return "", nil + } + var result OCCallResult + if err := json.Unmarshal(data, &result); err != nil { + return "", err + } + var sb string + for _, c := range result.Content { + if c.Type == "text" { + sb += c.Text + } + } + return sb, nil +} + func (s *sidecarProcess) Close() { s.mu.Lock() if s.closed || s.stopped { @@ -330,5 +355,5 @@ func (s *sidecarProcess) Close() { } s.mu.Unlock() - log.Printf("[openclaw] sidecar %s stopped", s.name) + log.Printf("[clawhubadapter] sidecar %s stopped", s.name) } diff --git a/internal/plugins/openclaw/sidecar_test.go b/internal/plugins/clawhubadapter/sidecar_test.go similarity index 99% rename from internal/plugins/openclaw/sidecar_test.go rename to internal/plugins/clawhubadapter/sidecar_test.go index 815a8d9..a4df2ab 100644 --- a/internal/plugins/openclaw/sidecar_test.go +++ b/internal/plugins/clawhubadapter/sidecar_test.go @@ -1,4 +1,4 @@ -package openclaw +package clawhubadapter import ( "fmt" diff --git a/internal/plugins/openclaw/simulator/main.js b/internal/plugins/clawhubadapter/simulator/main.js similarity index 70% rename from internal/plugins/openclaw/simulator/main.js rename to internal/plugins/clawhubadapter/simulator/main.js index 1a61eb7..28bb948 100644 --- a/internal/plugins/openclaw/simulator/main.js +++ b/internal/plugins/clawhubadapter/simulator/main.js @@ -84,6 +84,9 @@ if (typeof entry !== 'object' || typeof entry.register !== 'function') { // ---- 注册工具(本地存储,供 tools/list 和 tools/call 用) ---- const registeredTools = []; +// ---- Provider 存储(供 provider/call 用) ---- +const registeredProviders = {}; // type -> { name, instance } + function registerTool(defOrFactory, opts) { if (typeof defOrFactory === 'function') { const toolCtx = { @@ -138,19 +141,55 @@ const api = { // ---- 工具注册 ---- registerTool, - // ---- Provider 注册 ---- - registerProvider: (provider) => notify('register', { type: 'provider', data: { name: provider.name, description: provider.description } }), - registerEmbeddingProvider: (p) => notify('register', { type: 'embedding_provider', data: { name: p.name } }), - registerSpeechProvider: (p) => notify('register', { type: 'speech_provider', data: { name: p.name } }), - registerRealtimeTranscriptionProvider: (p) => notify('register', { type: 'realtime_transcription_provider', data: { name: p.name } }), - registerRealtimeVoiceProvider: (p) => notify('register', { type: 'realtime_voice_provider', data: { name: p.name } }), - registerMediaUnderstandingProvider: (p) => notify('register', { type: 'media_understanding_provider', data: { name: p.name } }), - registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }), - registerMusicGenerationProvider: (p) => notify('register', { type: 'music_generation_provider', data: { name: p.name } }), - registerVideoGenerationProvider: (p) => notify('register', { type: 'video_generation_provider', data: { name: p.name } }), - registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }), - registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }), - registerMemoryEmbeddingProvider: (p) => notify('register', { type: 'memory_embedding_provider', data: { name: p.name } }), + // ---- Provider 注册(同时存储实例,支持 provider/call) ---- + registerProvider: (provider) => { + if (provider && provider.id) registeredProviders['llm'] = { name: provider.id, instance: provider }; + notify('register', { type: 'provider', data: { name: provider?.id || provider?.name, description: provider?.description } }); + }, + registerEmbeddingProvider: (p) => { + if (p) registeredProviders['embedding'] = { name: p.name, instance: p }; + notify('register', { type: 'embedding_provider', data: { name: p?.name } }); + }, + registerSpeechProvider: (p) => { + if (p) registeredProviders['speech'] = { name: p.name, instance: p }; + notify('register', { type: 'speech_provider', data: { name: p?.name } }); + }, + registerRealtimeTranscriptionProvider: (p) => { + if (p) registeredProviders['realtime_transcription'] = { name: p.name, instance: p }; + notify('register', { type: 'realtime_transcription_provider', data: { name: p?.name } }); + }, + registerRealtimeVoiceProvider: (p) => { + if (p) registeredProviders['realtime_voice'] = { name: p.name, instance: p }; + notify('register', { type: 'realtime_voice_provider', data: { name: p?.name } }); + }, + registerMediaUnderstandingProvider: (p) => { + if (p) registeredProviders['media_understanding'] = { name: p.name, instance: p }; + notify('register', { type: 'media_understanding_provider', data: { name: p?.name } }); + }, + registerImageGenerationProvider: (p) => { + if (p) registeredProviders['image_generation'] = { name: p.name, instance: p }; + notify('register', { type: 'image_generation_provider', data: { name: p?.name } }); + }, + registerMusicGenerationProvider: (p) => { + if (p) registeredProviders['music_generation'] = { name: p.name, instance: p }; + notify('register', { type: 'music_generation_provider', data: { name: p?.name } }); + }, + registerVideoGenerationProvider: (p) => { + if (p) registeredProviders['video_generation'] = { name: p.name, instance: p }; + notify('register', { type: 'video_generation_provider', data: { name: p?.name } }); + }, + registerWebFetchProvider: (p) => { + if (p) registeredProviders['web_fetch'] = { name: p.name, instance: p }; + notify('register', { type: 'web_fetch_provider', data: { name: p?.name } }); + }, + registerWebSearchProvider: (p) => { + if (p) registeredProviders['web_search'] = { name: p.name, instance: p }; + notify('register', { type: 'web_search_provider', data: { name: p?.name } }); + }, + registerMemoryEmbeddingProvider: (p) => { + if (p) registeredProviders['memory_embedding'] = { name: p.name, instance: p }; + notify('register', { type: 'memory_embedding_provider', data: { name: p?.name } }); + }, // ---- Channel 注册 ---- registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }), @@ -300,5 +339,33 @@ rl.on('line', async (line) => { return; } + // ---- Provider 调用 ---- + if (method === 'provider/call') { + const { type, action, args } = req.params || {}; + if (!type) { sendError(id, -32602, 'type required'); return; } + + const provider = registeredProviders[type]; + if (!provider) { sendError(id, -32601, `Provider not found: ${type}`); return; } + + const methodName = action || 'execute'; + if (typeof provider.instance[methodName] !== 'function') { + sendError(id, -32603, `Provider ${type} has no method ${methodName}`); + return; + } + + try { + const result = await provider.instance[methodName](args); + if (result && typeof result === 'object' && Array.isArray(result.content)) { + writeJSON({ jsonrpc: '2.0', id, result }); + } else { + const text = typeof result === 'string' ? result : JSON.stringify(result); + writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } }); + } + } catch (e) { + sendError(id, -32603, e.message); + } + return; + } + sendError(id, -32601, `Method not found: ${method}`); }); diff --git a/internal/plugins/openclaw/testdata/echoplugin/main.js b/internal/plugins/clawhubadapter/testdata/echoplugin/main.js similarity index 100% rename from internal/plugins/openclaw/testdata/echoplugin/main.js rename to internal/plugins/clawhubadapter/testdata/echoplugin/main.js diff --git a/internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js b/internal/plugins/clawhubadapter/testdata/oc-pkg/lib/entry.js similarity index 100% rename from internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js rename to internal/plugins/clawhubadapter/testdata/oc-pkg/lib/entry.js diff --git a/internal/plugins/openclaw/testdata/oc-pkg/package.json b/internal/plugins/clawhubadapter/testdata/oc-pkg/package.json similarity index 100% rename from internal/plugins/openclaw/testdata/oc-pkg/package.json rename to internal/plugins/clawhubadapter/testdata/oc-pkg/package.json diff --git a/internal/plugins/openclaw/testdata/oc-simple/index.js b/internal/plugins/clawhubadapter/testdata/oc-simple/index.js similarity index 100% rename from internal/plugins/openclaw/testdata/oc-simple/index.js rename to internal/plugins/clawhubadapter/testdata/oc-simple/index.js diff --git a/internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json b/internal/plugins/clawhubadapter/testdata/oc-simple/openclaw.plugin.json similarity index 100% rename from internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json rename to internal/plugins/clawhubadapter/testdata/oc-simple/openclaw.plugin.json