mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
refactor(clawhubadapter): merge manager/simulator, fix provider dispatch, add OC plugin load delegation
- Manager now handles OC plugin loading via plugins/load JSON-RPC instead of launching a separate simulator process - Added 8 missing provider registration methods to manager - Added explicit registeredChannels declaration in manager - Provider dispatch now strips _provider suffix correctly (lookupType) - Skip .simulator directory in skills scanning - detect OC via package.json openclaw field (hasOCPackage) - All registration notifications include plugin name for correct tool prefix - Fix ToolRegistry/ProviderRegistry/ChannelRegistry to use plugin name from notification data instead of hardcoded 'manager' - test: add mockSettings, fix TestLoadOCPluginViaPluginStart panic
This commit is contained in:
@ -93,13 +93,14 @@ function tryDetectOCPackage(pkgDir, pkgName) {
|
|||||||
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
|
const allProviders = {}; // type -> { name, instance } across all plugins
|
||||||
|
const registeredChannels = {}; // name -> { pluginName, channelPlugin, output, send, type }
|
||||||
|
|
||||||
function registerPluginTools(name, tools, api) {
|
function registerPluginTools(name, tools, api) {
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
if (t && t.name) {
|
if (t && t.name) {
|
||||||
t._plugin = name;
|
t._plugin = name;
|
||||||
allTools.push(t);
|
allTools.push(t);
|
||||||
notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters } });
|
notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters, plugin: name } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadedPlugins[name] = { tools, api };
|
loadedPlugins[name] = { tools, api };
|
||||||
@ -174,7 +175,7 @@ function loadPlugin(pluginDir, name) {
|
|||||||
},
|
},
|
||||||
registerProvider: (p) => {
|
registerProvider: (p) => {
|
||||||
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
|
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
|
||||||
notify('register', { type: 'provider', data: { name: p?.id || p?.name } });
|
notify('register', { type: 'provider', data: { name: p?.id || p?.name, plugin: name } });
|
||||||
},
|
},
|
||||||
registerChannel: (ch) => {
|
registerChannel: (ch) => {
|
||||||
let chName = ch.name;
|
let chName = ch.name;
|
||||||
@ -191,7 +192,7 @@ function loadPlugin(pluginDir, name) {
|
|||||||
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
|
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
|
||||||
}
|
}
|
||||||
|
|
||||||
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id } });
|
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id, plugin: name } });
|
||||||
},
|
},
|
||||||
submitInput: (msg) => {
|
submitInput: (msg) => {
|
||||||
notify('channel_input', { channel: name, payload: msg });
|
notify('channel_input', { channel: name, payload: msg });
|
||||||
@ -202,15 +203,47 @@ function loadPlugin(pluginDir, name) {
|
|||||||
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
|
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
|
||||||
registerImageGenerationProvider: (p) => {
|
registerImageGenerationProvider: (p) => {
|
||||||
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
|
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
|
||||||
notify('register', { type: 'image_generation_provider', data: { name: p?.name } });
|
notify('register', { type: 'image_generation_provider', data: { name: p?.name, plugin: name } });
|
||||||
},
|
},
|
||||||
registerWebFetchProvider: (p) => {
|
registerWebFetchProvider: (p) => {
|
||||||
if (p) allProviders['web_fetch'] = { name: p.name, instance: p };
|
if (p) allProviders['web_fetch'] = { name: p.name, instance: p };
|
||||||
notify('register', { type: 'web_fetch_provider', data: { name: p?.name } });
|
notify('register', { type: 'web_fetch_provider', data: { name: p?.name, plugin: name } });
|
||||||
},
|
},
|
||||||
registerWebSearchProvider: (p) => {
|
registerWebSearchProvider: (p) => {
|
||||||
if (p) allProviders['web_search'] = { name: p.name, instance: p };
|
if (p) allProviders['web_search'] = { name: p.name, instance: p };
|
||||||
notify('register', { type: 'web_search_provider', data: { name: p?.name } });
|
notify('register', { type: 'web_search_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerSpeechProvider: (p) => {
|
||||||
|
if (p) allProviders['speech'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'speech_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerRealtimeTranscriptionProvider: (p) => {
|
||||||
|
if (p) allProviders['realtime_transcription'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'realtime_transcription_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerRealtimeVoiceProvider: (p) => {
|
||||||
|
if (p) allProviders['realtime_voice'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'realtime_voice_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerMediaUnderstandingProvider: (p) => {
|
||||||
|
if (p) allProviders['media_understanding'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'media_understanding_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerMusicGenerationProvider: (p) => {
|
||||||
|
if (p) allProviders['music_generation'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'music_generation_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerVideoGenerationProvider: (p) => {
|
||||||
|
if (p) allProviders['video_generation'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'video_generation_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerEmbeddingProvider: (p) => {
|
||||||
|
if (p) allProviders['embedding'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'embedding_provider', data: { name: p?.name, plugin: name } });
|
||||||
|
},
|
||||||
|
registerMemoryEmbeddingProvider: (p) => {
|
||||||
|
if (p) allProviders['memory_embedding'] = { name: p.name, instance: p };
|
||||||
|
notify('register', { type: 'memory_embedding_provider', data: { name: p?.name, plugin: name } });
|
||||||
},
|
},
|
||||||
start: (cb) => {},
|
start: (cb) => {},
|
||||||
stop: (cb) => {},
|
stop: (cb) => {},
|
||||||
@ -224,6 +257,8 @@ function loadPlugin(pluginDir, name) {
|
|||||||
|
|
||||||
// ---- Install npm package ----
|
// ---- Install npm package ----
|
||||||
function installNPMPackage(spec, skillsDir) {
|
function installNPMPackage(spec, skillsDir) {
|
||||||
|
// Strip npm: prefix if present
|
||||||
|
if (spec.startsWith('npm:')) spec = spec.slice(4);
|
||||||
process.stderr.write(`[manager] installing: ${spec}\n`);
|
process.stderr.write(`[manager] installing: ${spec}\n`);
|
||||||
const installDir = path.join(skillsDir, '.npm_install_' + Date.now());
|
const installDir = path.join(skillsDir, '.npm_install_' + Date.now());
|
||||||
fs.mkdirSync(installDir, { recursive: true });
|
fs.mkdirSync(installDir, { recursive: true });
|
||||||
@ -342,6 +377,9 @@ async function runCLI(skillsDir, cliArgs) {
|
|||||||
const result = installNPMPackage(spec, skillsDir);
|
const result = installNPMPackage(spec, skillsDir);
|
||||||
if (result.error) throw new Error(result.error);
|
if (result.error) throw new Error(result.error);
|
||||||
console.log(`Installed: ${result.name}`);
|
console.log(`Installed: ${result.name}`);
|
||||||
|
// Write install result for CLI wrapper to pick up
|
||||||
|
const simDir = path.dirname(process.argv[1]);
|
||||||
|
fs.writeFileSync(path.join(simDir, '.install-result'), JSON.stringify({ name: result.name }));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,6 +678,25 @@ rl.on('line', async (line) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'plugins/load') {
|
||||||
|
const dir = req.params?.dir;
|
||||||
|
const pluginName = req.params?.name || (dir ? path.basename(dir) : '');
|
||||||
|
if (!dir) { sendError(id, -32602, 'dir required'); return; }
|
||||||
|
const resolvedDir = path.resolve(dir);
|
||||||
|
if (!fs.existsSync(resolvedDir)) { sendError(id, -32601, `directory not found: ${resolvedDir}`); return; }
|
||||||
|
|
||||||
|
if (loadedPlugins[pluginName]) {
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { name: pluginName, tools: loadedPlugins[pluginName].tools.map(t => t.name), type: 'already_loaded' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = loadPlugin(resolvedDir, pluginName);
|
||||||
|
if (!ok) { sendError(id, -32603, `failed to load plugin: ${pluginName}`); return; }
|
||||||
|
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { name: pluginName, tools: loadedPlugins[pluginName].tools.map(t => t.name), type: 'loaded' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (method === 'tools/list') {
|
if (method === 'tools/list') {
|
||||||
const tools = allTools.map(t => ({
|
const tools = allTools.map(t => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const { fork } = require('child_process');
|
|
||||||
|
|
||||||
const binDir = __dirname;
|
|
||||||
const skillsdirPath = path.join(binDir, '..', '.skillsdir');
|
|
||||||
|
|
||||||
if (!fs.existsSync(skillsdirPath)) {
|
|
||||||
process.stderr.write('Error: OpenClaw manager not running (no .skillsdir found)\n');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillsDir = fs.readFileSync(skillsdirPath, 'utf8').trim();
|
|
||||||
const managerPath = path.join(binDir, '..', 'manager.js');
|
|
||||||
|
|
||||||
const proc = fork(managerPath, [skillsDir, ...process.argv.slice(2)], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: { ...process.env, OPENCLAW_CLI: '1' },
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('exit', (code) => process.exit(code));
|
|
||||||
@ -16,21 +16,19 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed simulator/main.js
|
|
||||||
var simulatorSrc string
|
|
||||||
|
|
||||||
//go:embed manager/main.js
|
//go:embed manager/main.js
|
||||||
var managerSrc string
|
var managerSrc string
|
||||||
|
|
||||||
//go:embed pysimulator/main.py
|
//go:embed pysimulator/main.py
|
||||||
var pySimulatorSrc string
|
var pySimulatorSrc string
|
||||||
|
|
||||||
//go:embed manager/openclaw_cli.js
|
//go:embed simulator/openclaw_cli.js
|
||||||
var openclawCliSrc string
|
var openclawCliSrc string
|
||||||
|
|
||||||
var SkillsDir string
|
var SkillsDir string
|
||||||
@ -112,6 +110,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
// Load existing plugins from skills dir
|
// Load existing plugins from skills dir
|
||||||
if entries, err := os.ReadDir(p.skillsDir); err == nil {
|
if entries, err := os.ReadDir(p.skillsDir); err == nil {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
if strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
skillPath := filepath.Join(p.skillsDir, entry.Name())
|
skillPath := filepath.Join(p.skillsDir, entry.Name())
|
||||||
subs, err := os.ReadDir(skillPath)
|
subs, err := os.ReadDir(skillPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -205,9 +206,58 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
},
|
},
|
||||||
}, p.handlePluginList)
|
}, p.handlePluginList)
|
||||||
|
|
||||||
|
// Start file-based IPC for CLI integration (settings sync + reload requests)
|
||||||
|
go p.ipcGoroutine(s)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) ipcGoroutine(s *sdk.PluginSDK) {
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
p.mu.Lock()
|
||||||
|
simDir := p.simulatorDir
|
||||||
|
p.mu.Unlock()
|
||||||
|
if simDir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Write settings snapshot for CLI config:get/config:list
|
||||||
|
settings := s.Settings().Dump()
|
||||||
|
if data, err := json.MarshalIndent(settings, "", " "); err == nil {
|
||||||
|
os.WriteFile(filepath.Join(simDir, ".settings.json"), data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process pending settings changes from CLI config:set
|
||||||
|
pendingPath := filepath.Join(simDir, ".settings-pending.json")
|
||||||
|
if data, err := os.ReadFile(pendingPath); err == nil {
|
||||||
|
var pending map[string]interface{}
|
||||||
|
if json.Unmarshal(data, &pending) == nil {
|
||||||
|
for k, v := range pending {
|
||||||
|
s.Settings().Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Remove(pendingPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Process reload requests from CLI plugin:install
|
||||||
|
reloadPath := filepath.Join(simDir, ".reload-request")
|
||||||
|
if data, err := os.ReadFile(reloadPath); err == nil {
|
||||||
|
name := strings.TrimSpace(string(data))
|
||||||
|
if name != "" {
|
||||||
|
if err := p.reloadPlugin(name); err != nil {
|
||||||
|
log.Printf("[clawhubadapter] reload from CLI: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[clawhubadapter] reloaded plugin from CLI request: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Remove(reloadPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Manager ────────────────────────────────────────────────
|
// ─── Manager ────────────────────────────────────────────────
|
||||||
|
|
||||||
// launchManager 启动 OC 插件管理器(Node.js 进程),用于安装/卸载/加载 OC 格式插件
|
// launchManager 启动 OC 插件管理器(Node.js 进程),用于安装/卸载/加载 OC 格式插件
|
||||||
@ -273,6 +323,10 @@ func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{},
|
|||||||
return p.installFromClawHub(pkg)
|
return p.installFromClawHub(pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pkg, "npm:") {
|
||||||
|
pkg = strings.TrimPrefix(pkg, "npm:")
|
||||||
|
}
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
mgr := p.manager
|
mgr := p.manager
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
@ -549,62 +603,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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
caps := p.dispatcher.Capabilities()
|
caps := p.dispatcher.Capabilities()
|
||||||
if len(caps) > 0 {
|
if len(caps) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
|
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
|
||||||
for _, c := range caps {
|
for _, c := range caps {
|
||||||
parts = append(parts, fmt.Sprintf(" %s", c))
|
parts = append(parts, fmt.Sprintf(" %s", c))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 {
|
||||||
if len(result.Plugins) == 0 && len(p.sidecars) <= 1 && len(p.skills) == 0 {
|
parts = append(parts, "没有已安装的插件。")
|
||||||
parts = append(parts, "没有已安装的插件。")
|
}
|
||||||
}
|
p.mu.Unlock()
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": strings.Join(parts, "\n"),
|
"content": strings.Join(parts, "\n"),
|
||||||
}, nil
|
}, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: list known plugins from Go side
|
// Fallback: list known plugins from Go side
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir))
|
parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir))
|
||||||
|
|
||||||
if len(p.sidecars) > 0 {
|
if len(p.sidecars) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("\nSidecar/OC 插件 (%d):", len(p.sidecars)))
|
parts = append(parts, fmt.Sprintf("\nSidecar/OC 插件 (%d):", len(p.sidecars)))
|
||||||
for _, sp := range p.sidecars {
|
for _, sp := range p.sidecars {
|
||||||
tools, err := sp.ListTools()
|
tools, err := sp.ListTools()
|
||||||
toolList := ""
|
toolList := ""
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var names []string
|
var names []string
|
||||||
for _, t := range tools {
|
for _, t := range tools {
|
||||||
names = append(names, t.Name)
|
names = append(names, t.Name)
|
||||||
|
}
|
||||||
|
toolList = strings.Join(names, ", ")
|
||||||
}
|
}
|
||||||
toolList = strings.Join(names, ", ")
|
parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, toolList))
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, toolList))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.skills) > 0 {
|
if len(p.skills) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills)))
|
parts = append(parts, fmt.Sprintf("\nSKILL 插件 (%d):", len(p.skills)))
|
||||||
for _, sk := range p.skills {
|
for _, sk := range p.skills {
|
||||||
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
|
parts = append(parts, fmt.Sprintf(" %s v%s", sk.Name(), sk.Version()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
caps := p.dispatcher.Capabilities()
|
caps := p.dispatcher.Capabilities()
|
||||||
if len(caps) > 0 {
|
if len(caps) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
|
parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps)))
|
||||||
for _, c := range caps {
|
for _, c := range caps {
|
||||||
parts = append(parts, fmt.Sprintf(" %s", c))
|
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, "没有已安装的插件。")
|
||||||
@ -616,48 +670,49 @@ if len(caps) > 0 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) loadOCPlugin(s *sdk.PluginSDK, dir, name string) error {
|
func (p *Plugin) loadOCPlugin(s *sdk.PluginSDK, dir, name string) error {
|
||||||
simPath := filepath.Join(p.simulatorDir, "main.js")
|
// Delegate to manager's plugins/load instead of launching a separate simulator.
|
||||||
if err := os.MkdirAll(p.simulatorDir, 0755); err != nil {
|
// The manager is the single Node.js process that handles all OC plugins.
|
||||||
return fmt.Errorf("create simulator dir: %w", err)
|
p.mu.Lock()
|
||||||
}
|
mgr := p.manager
|
||||||
if err := os.WriteFile(simPath, []byte(simulatorSrc), 0644); err != nil {
|
p.mu.Unlock()
|
||||||
return fmt.Errorf("write simulator: %w", err)
|
if mgr == nil {
|
||||||
|
return fmt.Errorf("plugin manager not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchProcess("node", simPath, dir, name)
|
// Check if manager already has this plugin loaded
|
||||||
if err != nil {
|
data, listErr := mgr.call("plugins/list", nil)
|
||||||
return fmt.Errorf("launch simulator: %w", err)
|
if listErr == nil && data != nil {
|
||||||
}
|
var result struct {
|
||||||
if sp == nil {
|
Plugins []struct{ Name string `json:"name"` } `json:"plugins"`
|
||||||
return nil
|
}
|
||||||
}
|
if json.Unmarshal(data, &result) == nil {
|
||||||
|
for _, pl := range result.Plugins {
|
||||||
// 通知是唯一注册路径。
|
if pl.Name == name {
|
||||||
// 插件 register(api) 期间模拟器将所有 register* 调用以通知推送给 Go。
|
log.Printf("[clawhubadapter] plugin %s already loaded by manager, skipping", name)
|
||||||
// waitReady 之后所有初始化通知已缓冲在 notifyCh 中,同步排空处理。
|
return nil
|
||||||
for done := false; !done; {
|
}
|
||||||
select {
|
}
|
||||||
case n := <-sp.NotifyChan():
|
|
||||||
p.translateAndRegister(n, sp, s, name)
|
|
||||||
default:
|
|
||||||
done = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动持久通知协程:后续模拟器推送的注册通知持续转译注册到核心
|
// Ask manager to load this plugin (notifications flow through the manager's stdout → Go notifyLoop)
|
||||||
go p.notifyLoop(sp, s, name)
|
data, err := mgr.call("plugins/load", map[string]interface{}{
|
||||||
|
"dir": dir,
|
||||||
// ListTools 仅验证日志,不参与注册
|
"name": name,
|
||||||
tools, err := sp.ListTools()
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sp.Close()
|
return fmt.Errorf("manager plugins/load: %w", err)
|
||||||
return fmt.Errorf("list tools: %w", err)
|
|
||||||
}
|
}
|
||||||
log.Printf("[clawhubadapter] ocplugin %s verified %d tools via ListTools", name, len(tools))
|
|
||||||
|
|
||||||
p.mu.Lock()
|
var result struct {
|
||||||
p.sidecars = append(p.sidecars, sp)
|
Name string `json:"name"`
|
||||||
p.mu.Unlock()
|
Tools []string `json:"tools"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(data, &result) == nil {
|
||||||
|
log.Printf("[clawhubadapter] ocplugin %s loaded via manager with %d tools", name, len(result.Tools))
|
||||||
|
} else {
|
||||||
|
log.Printf("[clawhubadapter] ocplugin %s loaded via manager, raw: %s", name, string(data))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -678,7 +733,9 @@ func (p *Plugin) translateAndRegister(n OCNotification, sp *sidecarProcess, s *s
|
|||||||
if err := json.Unmarshal(n.Params, ¶ms); err != nil || params.Channel == "" {
|
if err := json.Unmarshal(n.Params, ¶ms); err != nil || params.Channel == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
channelInputBufMu.Lock()
|
||||||
channelInputBuf[params.Channel] = append(channelInputBuf[params.Channel], params.Payload)
|
channelInputBuf[params.Channel] = append(channelInputBuf[params.Channel], params.Payload)
|
||||||
|
channelInputBufMu.Unlock()
|
||||||
content, _ := params.Payload["content"].(string)
|
content, _ := params.Payload["content"].(string)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
data, _ := json.Marshal(params.Payload)
|
data, _ := json.Marshal(params.Payload)
|
||||||
@ -848,6 +905,7 @@ func (p *Plugin) reloadPlugin(name string) error {
|
|||||||
hasMainJS := false
|
hasMainJS := false
|
||||||
hasMainPy := false
|
hasMainPy := false
|
||||||
hasOCManifest := false
|
hasOCManifest := false
|
||||||
|
hasOCPackage := false
|
||||||
for _, f := range subs {
|
for _, f := range subs {
|
||||||
switch f.Name() {
|
switch f.Name() {
|
||||||
case "main.js":
|
case "main.js":
|
||||||
@ -856,6 +914,8 @@ func (p *Plugin) reloadPlugin(name string) error {
|
|||||||
hasMainPy = true
|
hasMainPy = true
|
||||||
case "openclaw.plugin.json":
|
case "openclaw.plugin.json":
|
||||||
hasOCManifest = true
|
hasOCManifest = true
|
||||||
|
case "package.json":
|
||||||
|
hasOCPackage = hasOCExtensions(filepath.Join(pluginDir, "package.json"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -866,7 +926,7 @@ func (p *Plugin) reloadPlugin(name string) error {
|
|||||||
return p.loadSidecar(p.sdk, pluginDir, name)
|
return p.loadSidecar(p.sdk, pluginDir, name)
|
||||||
case hasMainPy:
|
case hasMainPy:
|
||||||
return p.loadPySidecar(p.sdk, pluginDir, name)
|
return p.loadPySidecar(p.sdk, pluginDir, name)
|
||||||
case hasOCManifest:
|
case hasOCManifest || hasOCPackage:
|
||||||
return p.loadOCPlugin(p.sdk, pluginDir, name)
|
return p.loadOCPlugin(p.sdk, pluginDir, name)
|
||||||
default:
|
default:
|
||||||
sk, err := plugin.LoadSKILL(pluginDir)
|
sk, err := plugin.LoadSKILL(pluginDir)
|
||||||
|
|||||||
@ -10,7 +10,10 @@ import (
|
|||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
var channelInputBuf = map[string][]map[string]interface{}{}
|
var (
|
||||||
|
channelInputBuf = map[string][]map[string]interface{}{}
|
||||||
|
channelInputBufMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
type ToolRegistry struct{}
|
type ToolRegistry struct{}
|
||||||
|
|
||||||
@ -20,11 +23,16 @@ func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sid
|
|||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Parameters map[string]interface{} `json:"parameters"`
|
Parameters map[string]interface{} `json:"parameters"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toolName := fmt.Sprintf("%s_%s", pluginName, d.Name)
|
pn := pluginName
|
||||||
|
if d.Plugin != "" {
|
||||||
|
pn = d.Plugin
|
||||||
|
}
|
||||||
|
toolName := fmt.Sprintf("%s_%s", pn, d.Name)
|
||||||
tDef := sdk.ToolDef{
|
tDef := sdk.ToolDef{
|
||||||
Name: toolName,
|
Name: toolName,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
@ -64,29 +72,75 @@ func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, plugin
|
|||||||
var d struct {
|
var d struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
}
|
}
|
||||||
json.Unmarshal(data, &d)
|
json.Unmarshal(data, &d)
|
||||||
|
|
||||||
|
pn := pluginName
|
||||||
|
if d.Plugin != "" {
|
||||||
|
pn = d.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip _provider suffix: "image_generation_provider" -> "image_generation"
|
||||||
|
lookupType := typeStr
|
||||||
|
if lookupType != "provider" {
|
||||||
|
lookupType = strings.TrimSuffix(lookupType, "_provider")
|
||||||
|
}
|
||||||
|
if lookupType == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range providerMap {
|
for _, p := range providerMap {
|
||||||
if p.ocType == typeStr {
|
if p.ocType == lookupType {
|
||||||
toolName := fmt.Sprintf("%s_%s", pluginName, p.toolSuffix)
|
toolName := fmt.Sprintf("%s_%s", pn, p.toolSuffix)
|
||||||
desc := p.desc
|
desc := p.desc
|
||||||
if d.Name != "" {
|
if d.Name != "" {
|
||||||
desc = fmt.Sprintf("[%s] %s", d.Name, desc)
|
desc = fmt.Sprintf("[%s] %s", d.Name, desc)
|
||||||
}
|
}
|
||||||
|
props := map[string]interface{}{
|
||||||
|
"prompt": map[string]interface{}{"type": "string", "description": "Prompt for generation or search query"},
|
||||||
|
}
|
||||||
|
switch p.ocType {
|
||||||
|
case "image_generation":
|
||||||
|
props["prompt"] = map[string]interface{}{"type": "string", "description": "Image description prompt"}
|
||||||
|
props["size"] = map[string]interface{}{"type": "string", "description": "Image size (e.g. 1024x1024)", "enum": []interface{}{"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}}
|
||||||
|
case "web_search":
|
||||||
|
props["query"] = map[string]interface{}{"type": "string", "description": "Search query"}
|
||||||
|
delete(props, "prompt")
|
||||||
|
case "web_fetch":
|
||||||
|
props["url"] = map[string]interface{}{"type": "string", "description": "URL to fetch"}
|
||||||
|
delete(props, "prompt")
|
||||||
|
case "media_understanding":
|
||||||
|
props["url"] = map[string]interface{}{"type": "string", "description": "Media URL to analyze"}
|
||||||
|
props["media_type"] = map[string]interface{}{"type": "string", "description": "Media type", "enum": []interface{}{"image", "audio", "video"}}
|
||||||
|
case "speech":
|
||||||
|
props["text"] = map[string]interface{}{"type": "string", "description": "Text to synthesize"}
|
||||||
|
props["voice"] = map[string]interface{}{"type": "string", "description": "Voice identifier"}
|
||||||
|
case "music_generation":
|
||||||
|
props["prompt"] = map[string]interface{}{"type": "string", "description": "Music description prompt"}
|
||||||
|
props["duration"] = map[string]interface{}{"type": "number", "description": "Duration in seconds"}
|
||||||
|
case "video_generation":
|
||||||
|
props["prompt"] = map[string]interface{}{"type": "string", "description": "Video description prompt"}
|
||||||
|
props["duration"] = map[string]interface{}{"type": "number", "description": "Duration in seconds"}
|
||||||
|
case "realtime_transcription":
|
||||||
|
props["audio_url"] = map[string]interface{}{"type": "string", "description": "Audio URL to transcribe"}
|
||||||
|
case "realtime_voice":
|
||||||
|
props["text"] = map[string]interface{}{"type": "string", "description": "Text to speak"}
|
||||||
|
props["voice"] = map[string]interface{}{"type": "string", "description": "Voice identifier"}
|
||||||
|
}
|
||||||
tDef := sdk.ToolDef{
|
tDef := sdk.ToolDef{
|
||||||
Name: toolName,
|
Name: toolName,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{},
|
"properties": props,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
handler := func(sp *sidecarProcess, ocType string) sdk.ToolHandler {
|
handler := func(sp *sidecarProcess, ocType string) sdk.ToolHandler {
|
||||||
return func(args map[string]interface{}) (interface{}, error) {
|
return func(args map[string]interface{}) (interface{}, error) {
|
||||||
return sp.CallProvider(ocType, args)
|
return sp.CallProvider(ocType, args)
|
||||||
}
|
}
|
||||||
}(sp, typeStr)
|
}(sp, lookupType)
|
||||||
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
|
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
|
||||||
log.Printf("[clawhubadapter] register provider tool %s: %v", toolName, err)
|
log.Printf("[clawhubadapter] register provider tool %s: %v", toolName, err)
|
||||||
}
|
}
|
||||||
@ -94,20 +148,35 @@ func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName)
|
if lookupType != typeStr {
|
||||||
|
log.Printf("[clawhubadapter] provider %s -> lookup %s (plugin: %s)", typeStr, lookupType, pluginName)
|
||||||
|
}
|
||||||
|
switch lookupType {
|
||||||
|
case "provider":
|
||||||
|
log.Printf("[clawhubadapter] generic LLM provider from %s handled natively by HomeAgent", pluginName)
|
||||||
|
case "embedding", "memory_embedding":
|
||||||
|
log.Printf("[clawhubadapter] embedding provider %s from %s ignored (HomeAgent uses native embeddings)", lookupType, pluginName)
|
||||||
|
default:
|
||||||
|
log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelRegistry struct{}
|
type ChannelRegistry struct{}
|
||||||
|
|
||||||
func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
||||||
var d struct {
|
var d struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chName := d.Name
|
chName := d.Name
|
||||||
|
pn := pluginName
|
||||||
|
if d.Plugin != "" {
|
||||||
|
pn = d.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
var caps int
|
var caps int
|
||||||
switch d.Type {
|
switch d.Type {
|
||||||
@ -120,12 +189,12 @@ func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *
|
|||||||
default:
|
default:
|
||||||
caps = 1
|
caps = 1
|
||||||
}
|
}
|
||||||
desc := fmt.Sprintf("OC channel %s (from %s)", chName, pluginName)
|
desc := fmt.Sprintf("OC channel %s (from %s)", chName, pn)
|
||||||
s.RegisterOutputChannel(chName, caps, desc, func(args map[string]interface{}) (interface{}, error) {
|
s.RegisterOutputChannel(chName, caps, desc, func(args map[string]interface{}) (interface{}, error) {
|
||||||
return sp.CallTool(chName, args)
|
return sp.CallTool(chName, args)
|
||||||
})
|
})
|
||||||
|
|
||||||
readToolName := fmt.Sprintf("%s_read_%s_input", pluginName, strings.ReplaceAll(chName, "-", "_"))
|
readToolName := fmt.Sprintf("%s_read_%s_input", pn, strings.ReplaceAll(chName, "-", "_"))
|
||||||
s.RegisterTool(readToolName, sdk.ToolDef{
|
s.RegisterTool(readToolName, sdk.ToolDef{
|
||||||
Name: readToolName,
|
Name: readToolName,
|
||||||
Description: fmt.Sprintf("读取 %s 通道的待处理输入消息", chName),
|
Description: fmt.Sprintf("读取 %s 通道的待处理输入消息", chName),
|
||||||
@ -134,8 +203,10 @@ func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *
|
|||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
}, func(args map[string]interface{}) (interface{}, error) {
|
}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
|
channelInputBufMu.Lock()
|
||||||
buf := channelInputBuf[chName]
|
buf := channelInputBuf[chName]
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
|
channelInputBufMu.Unlock()
|
||||||
return map[string]interface{}{"messages": []interface{}{}}, nil
|
return map[string]interface{}{"messages": []interface{}{}}, nil
|
||||||
}
|
}
|
||||||
msgs := make([]interface{}, len(buf))
|
msgs := make([]interface{}, len(buf))
|
||||||
@ -143,6 +214,7 @@ func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *
|
|||||||
msgs[i] = m
|
msgs[i] = m
|
||||||
}
|
}
|
||||||
channelInputBuf[chName] = nil
|
channelInputBuf[chName] = nil
|
||||||
|
channelInputBufMu.Unlock()
|
||||||
return map[string]interface{}{"messages": msgs}, nil
|
return map[string]interface{}{"messages": msgs}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,26 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
|
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mockSettings implements pubsdk.SettingsAPI for tests
|
||||||
|
type mockSettings struct{}
|
||||||
|
|
||||||
|
func (m *mockSettings) Get(key string) (interface{}, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) Set(key string, value interface{}) error { return nil }
|
||||||
|
func (m *mockSettings) List(prefix string) ([]string, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) GetCore(key string) (interface{}, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) SetCore(key string, value interface{}) error { return nil }
|
||||||
|
func (m *mockSettings) ListCore(prefix string) ([]string, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) GetPlugin(plugin, key string) (interface{}, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) SetPlugin(plugin, key string, value interface{}) error { return nil }
|
||||||
|
func (m *mockSettings) ListPlugin(plugin, prefix string) ([]string, error) { return nil, nil }
|
||||||
|
func (m *mockSettings) RegisterDef(def pubsdk.ConfigDef) {}
|
||||||
|
func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil }
|
||||||
|
func (m *mockSettings) Dump() map[string]interface{} { return nil }
|
||||||
|
func (m *mockSettings) Plugins() []string { return nil }
|
||||||
|
|
||||||
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
sp, err := launchSidecar(tmpDir, "nonexistent")
|
sp, err := launchSidecar(tmpDir, "nonexistent")
|
||||||
@ -327,6 +345,7 @@ func TestLoadOCPluginViaPluginStart(t *testing.T) {
|
|||||||
var registeredTools []string
|
var registeredTools []string
|
||||||
registeredHandlers := make(map[string]sdk.ToolHandler)
|
registeredHandlers := make(map[string]sdk.ToolHandler)
|
||||||
sdk := sdk.New("openclaw", sdk.SDKConfig{
|
sdk := sdk.New("openclaw", sdk.SDKConfig{
|
||||||
|
Settings: &mockSettings{},
|
||||||
RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||||
registeredTools = append(registeredTools, name)
|
registeredTools = append(registeredTools, name)
|
||||||
registeredHandlers[name] = handler
|
registeredHandlers[name] = handler
|
||||||
|
|||||||
@ -87,6 +87,9 @@ const registeredTools = [];
|
|||||||
// ---- Provider 存储(供 provider/call 用) ----
|
// ---- Provider 存储(供 provider/call 用) ----
|
||||||
const registeredProviders = {}; // type -> { name, instance }
|
const registeredProviders = {}; // type -> { name, instance }
|
||||||
|
|
||||||
|
// ---- Channel 存储(供 tools/call 中通道输出路由用) ----
|
||||||
|
const registeredChannels = {}; // name -> { output, send, channelPlugin, type }
|
||||||
|
|
||||||
function registerTool(defOrFactory, opts) {
|
function registerTool(defOrFactory, opts) {
|
||||||
if (typeof defOrFactory === 'function') {
|
if (typeof defOrFactory === 'function') {
|
||||||
const toolCtx = {
|
const toolCtx = {
|
||||||
@ -192,7 +195,22 @@ const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ---- Channel 注册 ----
|
// ---- Channel 注册 ----
|
||||||
registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }),
|
registerChannel: (ch) => {
|
||||||
|
let chName = ch.name;
|
||||||
|
let chType = ch.type || 'text';
|
||||||
|
const chPlugin = ch.plugin;
|
||||||
|
if (chPlugin && typeof chPlugin === 'object') {
|
||||||
|
registeredChannels[chName] = { channelPlugin: chPlugin, type: chType };
|
||||||
|
} else {
|
||||||
|
registeredChannels[chName] = { output: ch.output, send: ch.send, type: chType };
|
||||||
|
}
|
||||||
|
notify('register', { type: 'channel', data: { name: chName, type: chType } });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- 输入提交(submitInput -> channel_input notification) ----
|
||||||
|
submitInput: (msg) => {
|
||||||
|
notify('channel_input', { channel: api.name, payload: msg });
|
||||||
|
},
|
||||||
|
|
||||||
// ---- Hook / 生命周期 ----
|
// ---- Hook / 生命周期 ----
|
||||||
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 } }),
|
||||||
@ -314,6 +332,39 @@ rl.on('line', async (line) => {
|
|||||||
const toolName = params.name;
|
const toolName = params.name;
|
||||||
const args = params.arguments || {};
|
const args = params.arguments || {};
|
||||||
|
|
||||||
|
// Channel output routing: if toolName matches a registered channel, use channel's output handler
|
||||||
|
const ch = registeredChannels[toolName];
|
||||||
|
if (ch) {
|
||||||
|
try {
|
||||||
|
const channelPlugin = ch.channelPlugin;
|
||||||
|
if (channelPlugin && channelPlugin.outbound) {
|
||||||
|
const meta = args.meta || '';
|
||||||
|
let metaObj = {};
|
||||||
|
try { metaObj = typeof meta === 'string' ? JSON.parse(meta) : meta; } catch {}
|
||||||
|
const to = metaObj.user_id || metaObj.to || metaObj.group_id || '';
|
||||||
|
const ctx = { to, text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', cfg: {}, accountId: metaObj.accountId || null };
|
||||||
|
let result;
|
||||||
|
if (ctx.mediaUrl && channelPlugin.outbound.sendMedia) {
|
||||||
|
result = await channelPlugin.outbound.sendMedia(ctx);
|
||||||
|
} else if (channelPlugin.outbound.sendText) {
|
||||||
|
result = await channelPlugin.outbound.sendText(ctx);
|
||||||
|
} else {
|
||||||
|
throw new Error(`channel ${toolName} has no sendText/sendMedia handler`);
|
||||||
|
}
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else if (typeof ch.output === 'function') {
|
||||||
|
const result = await ch.output(args.payload, args.type, args.meta);
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else if (typeof ch.send === 'function') {
|
||||||
|
const result = await ch.send(args.payload, args.meta);
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else {
|
||||||
|
sendError(id, -32601, `channel ${toolName} has no output handler`);
|
||||||
|
}
|
||||||
|
} catch (e) { sendError(id, -32603, e.message); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tool = registeredTools.find(t => t.name === toolName);
|
const tool = registeredTools.find(t => t.name === toolName);
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
sendError(id, -32601, `Tool not found: ${toolName}`);
|
sendError(id, -32601, `Tool not found: ${toolName}`);
|
||||||
|
|||||||
216
internal/plugins/clawhubadapter/simulator/openclaw_cli.js
Normal file
216
internal/plugins/clawhubadapter/simulator/openclaw_cli.js
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { fork, execSync } = require('child_process');
|
||||||
|
|
||||||
|
const binDir = __dirname;
|
||||||
|
const simDir = path.resolve(binDir, '..');
|
||||||
|
|
||||||
|
function readJSON(file) {
|
||||||
|
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Resolve skills directory ----
|
||||||
|
function resolveSkillsDir() {
|
||||||
|
const skillsdirPath = path.join(simDir, '.skillsdir');
|
||||||
|
if (fs.existsSync(skillsdirPath)) {
|
||||||
|
return fs.readFileSync(skillsdirPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
const envDir = process.env.OPENCLAW_SKILLS_DIR;
|
||||||
|
if (envDir) return path.resolve(envDir);
|
||||||
|
const defaultDir = path.resolve(simDir, '..', 'skills');
|
||||||
|
if (fs.existsSync(defaultDir)) return defaultDir;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillsDir = resolveSkillsDir();
|
||||||
|
|
||||||
|
// ---- Detect system openclaw conflict ----
|
||||||
|
if (process.env.OPENCLAW_CLI !== '1') {
|
||||||
|
const PATH = process.env.PATH || '';
|
||||||
|
const pathEntries = PATH.split(path.delimiter);
|
||||||
|
for (const entry of pathEntries) {
|
||||||
|
if (entry === binDir) continue;
|
||||||
|
const otherPath = path.join(entry, 'openclaw');
|
||||||
|
try {
|
||||||
|
if (fs.statSync(otherPath).isFile() && fs.realpathSync(otherPath) !== __filename) {
|
||||||
|
process.stderr.write(`[openclaw] warning: found another openclaw at ${otherPath}\n`);
|
||||||
|
process.stderr.write(`[openclaw] using: ${__filename}\n`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Settings helpers ----
|
||||||
|
function getSettings() {
|
||||||
|
return readJSON(path.join(simDir, '.settings.json')) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePendingSetting(key, value) {
|
||||||
|
const pendingPath = path.join(simDir, '.settings-pending.json');
|
||||||
|
let pending = readJSON(pendingPath) || {};
|
||||||
|
pending[key] = value;
|
||||||
|
fs.writeFileSync(pendingPath, JSON.stringify(pending, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Permission check for plugin commands ----
|
||||||
|
function requireSkillsDir() {
|
||||||
|
if (!skillsDir) {
|
||||||
|
process.stderr.write('Error: cannot determine skills directory. Start the manager first or set OPENCLAW_SKILLS_DIR.\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(skillsDir)) {
|
||||||
|
fs.mkdirSync(skillsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Main ----
|
||||||
|
const cmd = process.argv[2];
|
||||||
|
|
||||||
|
if (!cmd || cmd === 'help' || cmd === '--help') {
|
||||||
|
console.log(`HomeAgent OpenClaw Adapter 1.0.0 (openclaw-compatible)
|
||||||
|
Usage: openclaw <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
plugin:install <spec> Install a plugin (npm:xxx, clawhub:xxx, or path)
|
||||||
|
plugin:uninstall <name> Uninstall a plugin
|
||||||
|
plugin:list List installed plugins
|
||||||
|
auth:login [url] Show QR code for login
|
||||||
|
auth:status Check auth status
|
||||||
|
config:get <key> Get config value
|
||||||
|
config:set <key> <val> Set config value
|
||||||
|
config:list List all config
|
||||||
|
doctor Run diagnostics
|
||||||
|
update Check for updates
|
||||||
|
env Show runtime info
|
||||||
|
--version Show version
|
||||||
|
help Show this help`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === '--version' || cmd === 'version') {
|
||||||
|
console.log('HomeAgent OpenClaw Adapter 1.0.0 (openclaw-compatible)');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'env') {
|
||||||
|
requireSkillsDir();
|
||||||
|
const info = {
|
||||||
|
homeAgent: true,
|
||||||
|
openclawVersion: 'compatible',
|
||||||
|
platform: process.platform,
|
||||||
|
nodeVersion: process.version,
|
||||||
|
skillsDir: skillsDir,
|
||||||
|
simulatorDir: simDir,
|
||||||
|
openclawCli: __filename,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(info, null, 2));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'doctor') {
|
||||||
|
console.log('OpenClaw Adapter diagnostics:');
|
||||||
|
console.log(` Simulator dir: ${simDir}`);
|
||||||
|
console.log(` Skills dir: ${skillsDir || '(not set)'}`);
|
||||||
|
console.log(` Node: ${process.version}`);
|
||||||
|
console.log(` Platform: ${process.platform}`);
|
||||||
|
if (skillsDir) {
|
||||||
|
const entries = fs.readdirSync(skillsDir).filter(e => !e.startsWith('.'));
|
||||||
|
console.log(` Installed plugins: ${entries.length}`);
|
||||||
|
}
|
||||||
|
console.log(' Status: OK');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'update') {
|
||||||
|
console.log('Update check: HomeAgent OpenClaw Adapter is bundled with HomeAgent.');
|
||||||
|
console.log('To update, update HomeAgent itself.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'config:get') {
|
||||||
|
const key = process.argv[3];
|
||||||
|
if (!key) { process.stderr.write('Usage: openclaw config:get <key>\n'); process.exit(1); }
|
||||||
|
const settings = getSettings();
|
||||||
|
if (key in settings) {
|
||||||
|
console.log(settings[key]);
|
||||||
|
} else {
|
||||||
|
process.stderr.write(`(not set: ${key})\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'config:set') {
|
||||||
|
const key = process.argv[3];
|
||||||
|
const value = process.argv[4];
|
||||||
|
if (!key || value === undefined) { process.stderr.write('Usage: openclaw config:set <key> <value>\n'); process.exit(1); }
|
||||||
|
writePendingSetting(key, value);
|
||||||
|
console.log(`Set: ${key}=${value} (will apply on next sync)`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'config:list') {
|
||||||
|
const settings = getSettings();
|
||||||
|
const keys = Object.keys(settings);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
console.log('(no settings)');
|
||||||
|
} else {
|
||||||
|
for (const k of keys) {
|
||||||
|
console.log(` ${k}=${settings[k]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'auth:login' || cmd === 'auth:qrcode') {
|
||||||
|
const url = process.argv[3] || 'openclaw://auth';
|
||||||
|
try {
|
||||||
|
const qrcode = require('qrcode');
|
||||||
|
qrcode.generate(url, { small: true }, (qr) => process.stdout.write(qr + '\n'));
|
||||||
|
} catch {
|
||||||
|
console.log('QR not available (install qrcode package). URL:');
|
||||||
|
}
|
||||||
|
console.log(` ${url}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === 'auth:status') {
|
||||||
|
console.log('Auth status: running in HomeAgent mode (not applicable)');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Plugin lifecycle commands - needs manager.js fork ----
|
||||||
|
if (cmd === 'plugin:install' || cmd === 'plugin:uninstall' || cmd === 'plugin:list') {
|
||||||
|
requireSkillsDir();
|
||||||
|
const managerPath = path.join(simDir, 'manager.js');
|
||||||
|
if (!fs.existsSync(managerPath)) {
|
||||||
|
process.stderr.write('Error: manager.js not found at ' + managerPath + '\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const proc = fork(managerPath, [skillsDir, ...process.argv.slice(2)], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, OPENCLAW_CLI: '1' },
|
||||||
|
});
|
||||||
|
proc.on('exit', (code) => {
|
||||||
|
// After plugin:install, notify Go side via .reload-request
|
||||||
|
if (cmd === 'plugin:install' && code === 0) {
|
||||||
|
const spec = process.argv[3] || '';
|
||||||
|
const installLog = path.join(simDir, '.install-result');
|
||||||
|
if (fs.existsSync(installLog)) {
|
||||||
|
const result = readJSON(installLog);
|
||||||
|
if (result && result.name) {
|
||||||
|
fs.writeFileSync(path.join(simDir, '.reload-request'), result.name);
|
||||||
|
console.log(`[openclaw] queued reload for plugin: ${result.name}`);
|
||||||
|
}
|
||||||
|
fs.rmSync(installLog, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stderr.write(`Unknown command: ${cmd}\n`);
|
||||||
|
process.stderr.write("Run 'openclaw help' for usage.\n");
|
||||||
|
process.exit(1);
|
||||||
Reference in New Issue
Block a user