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:
2026-07-21 22:47:14 +08:00
parent 906d117bdd
commit 76a4619ed5
15 changed files with 758 additions and 186 deletions

View File

@ -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"

View File

@ -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
}
}
}
}

View File

@ -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"

View File

@ -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}`);
});

View File

@ -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:<pkg> 从 npm 安装clawhub:<pkg> 从 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, &params); 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
}

View 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()
}

View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package openclaw
package clawhubadapter
import (
"fmt"

View File

@ -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}`);
});