mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
refactor: rename openclaw -> clawhubadapter, add ClawHub search/install, fix agent interrupt
- Rename internal/plugins/openclaw/ -> internal/plugins/clawhubadapter/ - Add RegistryDispatcher with 5 sub-registries (Tool, Provider, Channel, Stage, Cap) - Add clawhubadapter_search tool for ClawHub marketplace search - Add clawhub: prefix support for installing from ClawHub (ZIP/tgz auto-detect) - Add CallProvider RPC and provider/call routing - Fix QQ interrupt: check interceptCh before/after each tool execution
This commit is contained in:
@ -30,7 +30,7 @@ import (
|
|||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||||
healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
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"
|
pluginmgr "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
||||||
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
|
|||||||
@ -810,6 +810,21 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range resp.ToolCalls {
|
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)
|
toolsUsed = append(toolsUsed, tc.Name)
|
||||||
pluginName := a.resolveToolPlugin(tc.Name)
|
pluginName := a.resolveToolPlugin(tc.Name)
|
||||||
log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID)
|
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,
|
"result": result,
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// === 工具执行后检查打断通道 ===
|
||||||
|
if len(a.interceptCh) > 0 {
|
||||||
|
for _, interrupt := range a.drainInterrupts() {
|
||||||
|
msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/files"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/files"
|
||||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
||||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/mcp"
|
_ "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/pluginmgr"
|
||||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/timer"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/timer"
|
||||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||||
|
|||||||
@ -19,6 +19,7 @@ function readJSON(file) {
|
|||||||
// ---- Plugin registry ----
|
// ---- Plugin registry ----
|
||||||
const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] }
|
const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] }
|
||||||
const allTools = []; // flat list of all tools across all plugins
|
const allTools = []; // flat list of all tools across all plugins
|
||||||
|
const allProviders = {}; // type -> { name, instance } across all plugins
|
||||||
|
|
||||||
function registerPluginTools(name, tools, api) {
|
function registerPluginTools(name, tools, api) {
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
@ -98,15 +99,27 @@ function loadPlugin(pluginDir, name) {
|
|||||||
if (!def || !def.name) return;
|
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 });
|
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 } }),
|
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 } }),
|
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 } }),
|
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 } }),
|
registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }),
|
||||||
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
|
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
|
||||||
registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }),
|
registerImageGenerationProvider: (p) => {
|
||||||
registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }),
|
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
|
||||||
registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }),
|
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) => {},
|
start: (cb) => {},
|
||||||
stop: (cb) => {},
|
stop: (cb) => {},
|
||||||
};
|
};
|
||||||
@ -319,5 +332,30 @@ rl.on('line', async (line) => {
|
|||||||
return;
|
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}`);
|
sendError(id, -32601, `Method not found: ${method}`);
|
||||||
});
|
});
|
||||||
@ -1,10 +1,17 @@
|
|||||||
package openclaw
|
package clawhubadapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -27,13 +34,13 @@ var SkillsDir string
|
|||||||
var SimulatorDir string
|
var SimulatorDir string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("openclaw", "开放式交互", "OpenClaw")
|
plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter")
|
||||||
plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
dir := SkillsDir
|
dir := SkillsDir
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
dataDir, ok := config["data_dir"].(string)
|
dataDir, ok := config["data_dir"].(string)
|
||||||
if !ok {
|
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")
|
dir = filepath.Join(dataDir, "skills")
|
||||||
}
|
}
|
||||||
@ -42,15 +49,16 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
name string
|
name string
|
||||||
skillsDir string
|
skillsDir string
|
||||||
simulatorDir string
|
simulatorDir string
|
||||||
skills []*plugin.SKILLPlugin
|
skills []*plugin.SKILLPlugin
|
||||||
sidecars []*sidecarProcess
|
sidecars []*sidecarProcess
|
||||||
manager *sidecarProcess
|
manager *sidecarProcess
|
||||||
capabilities []string
|
mu sync.Mutex
|
||||||
mu sync.Mutex
|
sdk *sdk.PluginSDK
|
||||||
sdk *sdk.PluginSDK
|
dispatcher *RegistryDispatcher
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name, skillsDir string) *Plugin {
|
func New(name, skillsDir string) *Plugin {
|
||||||
@ -62,6 +70,7 @@ func New(name, skillsDir string) *Plugin {
|
|||||||
name: name,
|
name: name,
|
||||||
skillsDir: skillsDir,
|
skillsDir: skillsDir,
|
||||||
simulatorDir: sd,
|
simulatorDir: sd,
|
||||||
|
dispatcher: NewDispatcher(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +88,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
Key: "simulator_dir", Type: "string", DisplayName: "模拟器工作目录",
|
Key: "simulator_dir", Type: "string", DisplayName: "模拟器工作目录",
|
||||||
Description: "OpenClaw 模拟器工作目录路径(留空则使用默认路径)",
|
Description: "OpenClaw 模拟器工作目录路径(留空则使用默认路径)",
|
||||||
})
|
})
|
||||||
|
p.httpClient = &http.Client{}
|
||||||
if v, _ := s.Settings().Get("skills_dir"); v != nil {
|
if v, _ := s.Settings().Get("skills_dir"); v != nil {
|
||||||
if s, ok := v.(string); ok && s != "" {
|
if s, ok := v.(string); ok && s != "" {
|
||||||
p.skillsDir = 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)
|
// Launch OC plugin manager first (handles OC-format plugin installation and lifecycle)
|
||||||
os.MkdirAll(p.skillsDir, 0755)
|
os.MkdirAll(p.skillsDir, 0755)
|
||||||
if err := p.launchManager(s); err != nil {
|
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
|
// Load existing plugins from skills dir
|
||||||
@ -125,22 +135,22 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
switch {
|
switch {
|
||||||
case hasMainJS:
|
case hasMainJS:
|
||||||
if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil {
|
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:
|
case hasMainPy:
|
||||||
if err := p.loadPySidecar(s, skillPath, entry.Name()); err != nil {
|
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:
|
case hasOCManifest || hasOCPackage:
|
||||||
log.Printf("[openclaw] ocplugin %s handled by manager", entry.Name())
|
log.Printf("[clawhubadapter] ocplugin %s handled by manager", entry.Name())
|
||||||
default:
|
default:
|
||||||
sk, err := plugin.LoadSKILL(skillPath)
|
sk, err := plugin.LoadSKILL(skillPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[openclaw] load skill %s: %v", entry.Name(), err)
|
log.Printf("[clawhubadapter] load skill %s: %v", entry.Name(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.skills = append(p.skills, sk)
|
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 + "_"
|
tp := p.name + "_"
|
||||||
s.RegisterTool(tp+"npm_install", sdk.ToolDef{
|
s.RegisterTool(tp+"npm_install", sdk.ToolDef{
|
||||||
Name: tp + "npm_install",
|
Name: tp + "npm_install",
|
||||||
Description: "安装 OpenClaw 插件管理器中的 npm 插件。通过 npm 安装包,自动检测并加载到模拟器中。安装后立即可用。",
|
Description: "安装 ClawHub 适配器插件管理器中的插件。支持 npm: 前缀(npm 包)和 clawhub: 前缀(ClawHub 市场)。安装后立即可用。",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"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:<pkg> 从 npm 安装,clawhub:<pkg> 从 ClawHub 市场安装(如 clawhub:openclaw-codex-app-server)"},
|
||||||
},
|
},
|
||||||
"required": []string{"package"},
|
"required": []string{"package"},
|
||||||
},
|
},
|
||||||
@ -161,7 +171,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
|
|
||||||
s.RegisterTool(tp+"npm_uninstall", sdk.ToolDef{
|
s.RegisterTool(tp+"npm_uninstall", sdk.ToolDef{
|
||||||
Name: tp + "npm_uninstall",
|
Name: tp + "npm_uninstall",
|
||||||
Description: "从 OpenClaw 插件管理器中移除已安装的插件。",
|
Description: "从插件管理器中移除已安装的插件。",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
@ -171,9 +181,21 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
},
|
},
|
||||||
}, p.handlePluginUninstall)
|
}, 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{
|
s.RegisterTool(tp+"list", sdk.ToolDef{
|
||||||
Name: tp + "list",
|
Name: tp + "list",
|
||||||
Description: "列出插件管理器中所有已安装的 OpenClaw 插件及其工具。",
|
Description: "列出所有已安装的 ClawHub 插件及其工具。",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
@ -222,7 +244,7 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error {
|
|||||||
p.sidecars = append(p.sidecars, sp)
|
p.sidecars = append(p.sidecars, sp)
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[openclaw] OC plugin manager started")
|
log.Printf("[clawhubadapter] OC plugin manager started")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +256,10 @@ func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{},
|
|||||||
return errorResult("package is required"), nil
|
return errorResult("package is required"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pkg, "clawhub:") {
|
||||||
|
return p.installFromClawHub(pkg)
|
||||||
|
}
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
mgr := p.manager
|
mgr := p.manager
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
@ -264,6 +290,102 @@ func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{},
|
|||||||
}, nil
|
}, 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) {
|
func (p *Plugin) handlePluginUninstall(args map[string]interface{}) (interface{}, error) {
|
||||||
name, _ := args["name"].(string)
|
name, _ := args["name"].(string)
|
||||||
if name == "" {
|
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()))
|
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(p.capabilities) > 0 {
|
caps := p.dispatcher.Capabilities()
|
||||||
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(p.capabilities)))
|
if len(caps) > 0 {
|
||||||
for _, c := range p.capabilities {
|
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
|
||||||
parts = append(parts, fmt.Sprintf(" %s", c))
|
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
|
|
||||||
}
|
}
|
||||||
}
|
if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 {
|
||||||
}
|
parts = append(parts, "没有已安装的插件。")
|
||||||
|
|
||||||
// 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))
|
p.mu.Unlock()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.skills) > 0 {
|
return map[string]interface{}{
|
||||||
parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills)))
|
"content": strings.Join(parts, "\n"),
|
||||||
for _, sk := range p.skills {
|
}, nil
|
||||||
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(p.capabilities) > 0 {
|
// Fallback: list known plugins from Go side
|
||||||
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(p.capabilities)))
|
p.mu.Lock()
|
||||||
for _, c := range p.capabilities {
|
defer p.mu.Unlock()
|
||||||
parts = append(parts, fmt.Sprintf(" %s", c))
|
|
||||||
|
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 {
|
if len(p.sidecars) == 0 && len(p.skills) == 0 {
|
||||||
parts = append(parts, "没有已安装的插件。")
|
parts = append(parts, "没有已安装的插件。")
|
||||||
@ -453,7 +577,7 @@ func (p *Plugin) loadOCPlugin(s *sdk.PluginSDK, dir, name string) error {
|
|||||||
sp.Close()
|
sp.Close()
|
||||||
return fmt.Errorf("list tools: %w", err)
|
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.mu.Lock()
|
||||||
p.sidecars = append(p.sidecars, sp)
|
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 {
|
if err := json.Unmarshal(n.Params, ¶ms); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
p.dispatcher.Dispatch(params.Type, params.Data, pluginName, sp, s)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
|
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()
|
sp.Close()
|
||||||
return fmt.Errorf("list tools: %w", err)
|
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.mu.Lock()
|
||||||
p.sidecars = append(p.sidecars, sp)
|
p.sidecars = append(p.sidecars, sp)
|
||||||
@ -648,16 +685,16 @@ func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error {
|
|||||||
}
|
}
|
||||||
}(sp, tool.Name)
|
}(sp, tool.Name)
|
||||||
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
|
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
|
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.mu.Lock()
|
||||||
p.sidecars = append(p.sidecars, sp)
|
p.sidecars = append(p.sidecars, sp)
|
||||||
p.mu.Unlock()
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,3 +735,134 @@ func hasOCExtensions(pkgPath string) bool {
|
|||||||
}
|
}
|
||||||
return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil)
|
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
|
||||||
|
}
|
||||||
251
internal/plugins/clawhubadapter/registry.go
Normal file
251
internal/plugins/clawhubadapter/registry.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package openclaw
|
package clawhubadapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -101,7 +101,7 @@ func (s *sidecarProcess) readLoop(r io.Reader) {
|
|||||||
|
|
||||||
if !scanner.Scan() {
|
if !scanner.Scan() {
|
||||||
if scanner.Err() != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
@ -131,7 +131,7 @@ func (s *sidecarProcess) readLoop(r io.Reader) {
|
|||||||
select {
|
select {
|
||||||
case s.notifyCh <- notif:
|
case s.notifyCh <- notif:
|
||||||
default:
|
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
|
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() {
|
func (s *sidecarProcess) Close() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.closed || s.stopped {
|
if s.closed || s.stopped {
|
||||||
@ -330,5 +355,5 @@ func (s *sidecarProcess) Close() {
|
|||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[openclaw] sidecar %s stopped", s.name)
|
log.Printf("[clawhubadapter] sidecar %s stopped", s.name)
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package openclaw
|
package clawhubadapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -84,6 +84,9 @@ if (typeof entry !== 'object' || typeof entry.register !== 'function') {
|
|||||||
// ---- 注册工具(本地存储,供 tools/list 和 tools/call 用) ----
|
// ---- 注册工具(本地存储,供 tools/list 和 tools/call 用) ----
|
||||||
const registeredTools = [];
|
const registeredTools = [];
|
||||||
|
|
||||||
|
// ---- Provider 存储(供 provider/call 用) ----
|
||||||
|
const registeredProviders = {}; // type -> { name, instance }
|
||||||
|
|
||||||
function registerTool(defOrFactory, opts) {
|
function registerTool(defOrFactory, opts) {
|
||||||
if (typeof defOrFactory === 'function') {
|
if (typeof defOrFactory === 'function') {
|
||||||
const toolCtx = {
|
const toolCtx = {
|
||||||
@ -138,19 +141,55 @@ const api = {
|
|||||||
// ---- 工具注册 ----
|
// ---- 工具注册 ----
|
||||||
registerTool,
|
registerTool,
|
||||||
|
|
||||||
// ---- Provider 注册 ----
|
// ---- Provider 注册(同时存储实例,支持 provider/call) ----
|
||||||
registerProvider: (provider) => notify('register', { type: 'provider', data: { name: provider.name, description: provider.description } }),
|
registerProvider: (provider) => {
|
||||||
registerEmbeddingProvider: (p) => notify('register', { type: 'embedding_provider', data: { name: p.name } }),
|
if (provider && provider.id) registeredProviders['llm'] = { name: provider.id, instance: provider };
|
||||||
registerSpeechProvider: (p) => notify('register', { type: 'speech_provider', data: { name: p.name } }),
|
notify('register', { type: 'provider', data: { name: provider?.id || provider?.name, description: provider?.description } });
|
||||||
registerRealtimeTranscriptionProvider: (p) => notify('register', { type: 'realtime_transcription_provider', data: { name: p.name } }),
|
},
|
||||||
registerRealtimeVoiceProvider: (p) => notify('register', { type: 'realtime_voice_provider', data: { name: p.name } }),
|
registerEmbeddingProvider: (p) => {
|
||||||
registerMediaUnderstandingProvider: (p) => notify('register', { type: 'media_understanding_provider', data: { name: p.name } }),
|
if (p) registeredProviders['embedding'] = { name: p.name, instance: p };
|
||||||
registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }),
|
notify('register', { type: 'embedding_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 } }),
|
registerSpeechProvider: (p) => {
|
||||||
registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }),
|
if (p) registeredProviders['speech'] = { name: p.name, instance: p };
|
||||||
registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }),
|
notify('register', { type: 'speech_provider', data: { name: p?.name } });
|
||||||
registerMemoryEmbeddingProvider: (p) => notify('register', { type: 'memory_embedding_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 注册 ----
|
// ---- Channel 注册 ----
|
||||||
registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }),
|
registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }),
|
||||||
@ -300,5 +339,33 @@ rl.on('line', async (line) => {
|
|||||||
return;
|
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}`);
|
sendError(id, -32601, `Method not found: ${method}`);
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user