mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +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"
|
||||
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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}`);
|
||||
});
|
||||
@ -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, ¶ms); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch params.Type {
|
||||
|
||||
case "tool":
|
||||
var d struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
if err := json.Unmarshal(params.Data, &d); err != nil || d.Name == "" {
|
||||
return
|
||||
}
|
||||
toolName := fmt.Sprintf("%s_%s", pluginName, d.Name)
|
||||
tDef := sdk.ToolDef{
|
||||
Name: toolName,
|
||||
Description: d.Description,
|
||||
Parameters: d.Parameters,
|
||||
}
|
||||
handler := func(sp *sidecarProcess, ocToolName string) sdk.ToolHandler {
|
||||
return func(args map[string]interface{}) (interface{}, error) {
|
||||
return sp.CallTool(ocToolName, args)
|
||||
}
|
||||
}(sp, d.Name)
|
||||
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
|
||||
log.Printf("[openclaw] translate register tool %s: %v", toolName, err)
|
||||
}
|
||||
|
||||
case "provider":
|
||||
var d struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
json.Unmarshal(params.Data, &d)
|
||||
cap := fmt.Sprintf("[%s] provides %s provider", pluginName, d.Name)
|
||||
if d.Description != "" {
|
||||
cap += ": " + d.Description
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, cap)
|
||||
p.mu.Unlock()
|
||||
|
||||
case "channel":
|
||||
var d struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
json.Unmarshal(params.Data, &d)
|
||||
cap := fmt.Sprintf("[%s] registers channel: %s (type: %s)", pluginName, d.Name, d.Type)
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, cap)
|
||||
p.mu.Unlock()
|
||||
|
||||
case "image_generation_provider":
|
||||
var d struct{ Name string `json:"name"` }
|
||||
json.Unmarshal(params.Data, &d)
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] image generation provider: %s", pluginName, d.Name))
|
||||
p.mu.Unlock()
|
||||
|
||||
case "web_fetch_provider":
|
||||
var d struct{ Name string `json:"name"` }
|
||||
json.Unmarshal(params.Data, &d)
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] web fetch provider: %s", pluginName, d.Name))
|
||||
p.mu.Unlock()
|
||||
|
||||
case "web_search_provider":
|
||||
var d struct{ Name string `json:"name"` }
|
||||
json.Unmarshal(params.Data, &d)
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, fmt.Sprintf("[%s] web search provider: %s", pluginName, d.Name))
|
||||
p.mu.Unlock()
|
||||
|
||||
default:
|
||||
var d struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
json.Unmarshal(params.Data, &d)
|
||||
cap := fmt.Sprintf("[%s] capability: %s", pluginName, params.Type)
|
||||
if d.Name != "" {
|
||||
cap += " (" + d.Name + ")"
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.capabilities = append(p.capabilities, cap)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
p.dispatcher.Dispatch(params.Type, params.Data, pluginName, sp, s)
|
||||
}
|
||||
|
||||
func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
|
||||
@ -612,7 +649,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
|
||||
sp.Close()
|
||||
return fmt.Errorf("list tools: %w", err)
|
||||
}
|
||||
log.Printf("[openclaw] pysidecar %s registered %d tools", name, len(tools))
|
||||
log.Printf("[clawhubadapter] pysidecar %s registered %d tools", name, len(tools))
|
||||
|
||||
p.mu.Lock()
|
||||
p.sidecars = append(p.sidecars, sp)
|
||||
@ -648,16 +685,16 @@ func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error {
|
||||
}
|
||||
}(sp, tool.Name)
|
||||
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
|
||||
log.Printf("[openclaw] register sidecar tool %s: %v", toolName, err)
|
||||
log.Printf("[clawhubadapter] register sidecar tool %s: %v", toolName, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[openclaw] registered sidecar tool: %s (from %s)", toolName, name)
|
||||
log.Printf("[clawhubadapter] registered sidecar tool: %s (from %s)", toolName, name)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.sidecars = append(p.sidecars, sp)
|
||||
p.mu.Unlock()
|
||||
log.Printf("[openclaw] sidecar %s started with %d tools", name, len(tools))
|
||||
log.Printf("[clawhubadapter] sidecar %s started with %d tools", name, len(tools))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -698,3 +735,134 @@ func hasOCExtensions(pkgPath string) bool {
|
||||
}
|
||||
return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil)
|
||||
}
|
||||
|
||||
func (p *Plugin) reloadPlugin(name string) error {
|
||||
pluginDir := filepath.Join(p.skillsDir, name)
|
||||
info, err := os.Stat(pluginDir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return fmt.Errorf("plugin dir not found: %s", pluginDir)
|
||||
}
|
||||
|
||||
if p.sdk == nil {
|
||||
return fmt.Errorf("sdk not initialized")
|
||||
}
|
||||
|
||||
subs, err := os.ReadDir(pluginDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasMainJS := false
|
||||
hasMainPy := false
|
||||
hasOCManifest := false
|
||||
for _, f := range subs {
|
||||
switch f.Name() {
|
||||
case "main.js":
|
||||
hasMainJS = true
|
||||
case "main.py":
|
||||
hasMainPy = true
|
||||
case "openclaw.plugin.json":
|
||||
hasOCManifest = true
|
||||
}
|
||||
}
|
||||
|
||||
os.MkdirAll(p.simulatorDir, 0755)
|
||||
|
||||
switch {
|
||||
case hasMainJS:
|
||||
return p.loadSidecar(p.sdk, pluginDir, name)
|
||||
case hasMainPy:
|
||||
return p.loadPySidecar(p.sdk, pluginDir, name)
|
||||
case hasOCManifest:
|
||||
return p.loadOCPlugin(p.sdk, pluginDir, name)
|
||||
default:
|
||||
sk, err := plugin.LoadSKILL(pluginDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load skill: %w", err)
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.skills = append(p.skills, sk)
|
||||
p.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractArchive(data []byte, dest string) error {
|
||||
if len(data) < 4 {
|
||||
return fmt.Errorf("archive too small (%d bytes)", len(data))
|
||||
}
|
||||
|
||||
if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 {
|
||||
return extractZIP(data, dest)
|
||||
}
|
||||
|
||||
return extractTGZ(data, dest)
|
||||
}
|
||||
|
||||
func extractZIP(data []byte, dest string) error {
|
||||
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("zip: %w", err)
|
||||
}
|
||||
for _, f := range zr.File {
|
||||
target := filepath.Join(dest, f.Name)
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(target, 0755)
|
||||
continue
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(target), 0755)
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(out, r)
|
||||
r.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractTGZ(data []byte, dest string) error {
|
||||
gzr, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip: %w", err)
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := filepath.Join(dest, header.Name)
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
os.MkdirAll(target, 0755)
|
||||
case tar.TypeReg:
|
||||
os.MkdirAll(filepath.Dir(target), 0755)
|
||||
f, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
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 (
|
||||
"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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package openclaw
|
||||
package clawhubadapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user