Files
HomeAgent/internal/plugins/clawhubadapter/manager/main.js
jianf d4cb9cb8cc 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
2026-07-23 16:34:26 +08:00

791 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ---- Utility ----
function writeJSON(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
function sendError(id, code, message) {
writeJSON({ jsonrpc: '2.0', id, error: { code, message } });
}
function notify(method, params) {
writeJSON({ jsonrpc: '2.0', method, params });
}
function readJSON(file) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return null; }
}
// ---- Enhanced OC Plugin Detection ----
function resolvePackageEntry(dir, pkg) {
const candidates = [];
if (pkg) {
if (pkg.main) candidates.push(path.resolve(dir, pkg.main));
if (pkg.exports) {
const exp = pkg.exports;
if (typeof exp === 'string') candidates.push(path.resolve(dir, exp));
if (exp['.']) {
const dot = exp['.'];
if (typeof dot === 'string') candidates.push(path.resolve(dir, dot));
if (dot.require) candidates.push(path.resolve(dir, dot.require));
if (dot.default) candidates.push(path.resolve(dir, dot.default));
}
}
}
if (!pkg || !pkg.main) {
for (const name of ['index.js', 'main.js', 'src/index.js', 'lib/index.js']) {
candidates.push(path.join(dir, name));
}
}
for (const cp of candidates) {
if (fs.existsSync(cp)) return cp;
}
return null;
}
function ensureOCManifest(dir, name) {
const manifestPath = path.join(dir, 'openclaw.plugin.json');
if (fs.existsSync(manifestPath)) return;
const pkg = readJSON(path.join(dir, 'package.json'));
const entry = resolvePackageEntry(dir, pkg);
const relEntry = entry ? path.relative(dir, entry) : 'index.js';
const manifest = {
name: name,
version: (pkg && pkg.version) || '1.0.0',
entry: relEntry,
description: (pkg && pkg.description) || 'OpenClaw plugin (auto-detected)'
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
process.stderr.write(`[manager] created synthetic manifest: ${manifestPath}\n`);
}
function tryDetectOCPackage(pkgDir, pkgName) {
const pkg = readJSON(path.join(pkgDir, 'package.json'));
if (!pkg) return null;
const entryPath = resolvePackageEntry(pkgDir, pkg);
if (!entryPath) return null;
try {
delete require.cache[require.resolve(entryPath)];
const mod = require(entryPath);
const entry = mod.default || mod;
if (entry && typeof entry === 'object' && typeof entry.register === 'function') {
const name = pkgName || (pkg.openclaw ? (pkg.openclaw.name || pkg.name) : pkg.name) || path.basename(pkgDir);
ensureOCManifest(pkgDir, name);
process.stderr.write(`[manager] enhanced detection found OC plugin: ${name} (via require)\n`);
return { dir: pkgDir, name };
}
} catch (e) {
process.stderr.write(`[manager] require detect failed for ${pkgDir}: ${e.message}\n`);
}
return null;
}
// ---- 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
const registeredChannels = {}; // name -> { pluginName, channelPlugin, output, send, type }
function registerPluginTools(name, tools, api) {
for (const t of tools) {
if (t && t.name) {
t._plugin = name;
allTools.push(t);
notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters, plugin: name } });
}
}
loadedPlugins[name] = { tools, api };
}
function loadPlugin(pluginDir, name) {
// Resolve entry
const pkg = readJSON(path.join(pluginDir, 'package.json'));
let entryPath = null;
if (pkg && pkg.openclaw) {
let raw = pkg.openclaw.runtimeExtensions || pkg.openclaw.extensions;
if (typeof raw === 'string') raw = [raw];
if (Array.isArray(raw)) {
for (const ext of raw) {
let ep = path.resolve(pluginDir, ext);
if (ep.endsWith('.ts')) { const js = ep.replace(/\.ts$/, '.js'); if (fs.existsSync(js)) { entryPath = js; break; } }
if (fs.existsSync(ep)) { entryPath = ep; break; }
}
}
}
if (!entryPath) {
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
if (manifest) {
const ep = manifest.entry || manifest.main || 'index.js';
entryPath = path.join(pluginDir, ep);
}
}
if (!entryPath) entryPath = path.join(pluginDir, 'index.js');
if (!fs.existsSync(entryPath)) {
process.stderr.write(`[manager] entry not found for ${name}: ${entryPath}\n`);
return false;
}
let pluginEntry;
try { pluginEntry = require(entryPath); } catch (e) {
process.stderr.write(`[manager] load ${name}: ${e.message}\n`);
return false;
}
const entry = pluginEntry.default || pluginEntry;
if (typeof entry !== 'object' || typeof entry.register !== 'function') {
process.stderr.write(`[manager] ${name}: entry must export {register(api)}\n`);
return false;
}
const registeredTools = [];
const api = {
id: name,
name,
version: (pkg && pkg.version) || '1.0.0',
description: (pkg && pkg.description) || '',
source: pluginDir,
rootDir: pluginDir,
config: {},
pluginConfig: {},
registrationMode: 'full',
logger: { debug: () => {}, info: () => {}, warn: () => {}, error: (...args) => process.stderr.write(`[${name}] ${args.join(' ')}\n`) },
resolvePath: (p) => path.resolve(pluginDir, p),
registerTool: (def, opts) => {
if (typeof def === 'function') {
const toolCtx = { id: name, cwd: pluginDir, env: process.env, allow: ['*'] };
const result = def(toolCtx);
const tools = Array.isArray(result) ? result : [result];
for (const t of tools) { if (t && typeof t.execute === 'function') registeredTools.push(t); }
return;
}
if (!def || !def.name) return;
registeredTools.push({ name: def.name, label: def.label || def.name, description: def.description || '', parameters: def.parameters || { type: 'object', properties: {} }, execute: typeof def.execute === 'function' ? def.execute : undefined });
},
registerProvider: (p) => {
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
notify('register', { type: 'provider', data: { name: p?.id || p?.name, plugin: name } });
},
registerChannel: (ch) => {
let chName = ch.name;
let chType = ch.type || 'text';
const chPlugin = ch.plugin;
// OpenClaw ChannelPlugin 格式: { plugin: { id, outbound: { sendText, sendMedia }, ... } }
if (chPlugin && typeof chPlugin === 'object') {
chName = chName || chPlugin.id || chPlugin.meta?.id || name + '-channel';
chType = chType || (chPlugin.capabilities?.media ? 'io' : 'text');
registeredChannels[chName] = { pluginName: name, channelPlugin: chPlugin, type: chType };
} else {
// 简单格式: { name, type, output }
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
}
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id, plugin: name } });
},
submitInput: (msg) => {
notify('channel_input', { channel: name, payload: msg });
},
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) => {
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
notify('register', { type: 'image_generation_provider', data: { name: p?.name, plugin: name } });
},
registerWebFetchProvider: (p) => {
if (p) allProviders['web_fetch'] = { name: p.name, instance: p };
notify('register', { type: 'web_fetch_provider', data: { name: p?.name, plugin: name } });
},
registerWebSearchProvider: (p) => {
if (p) allProviders['web_search'] = { name: p.name, instance: p };
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) => {},
stop: (cb) => {},
};
entry.register(api);
registerPluginTools(name, registeredTools, api);
process.stderr.write(`[manager] loaded plugin: ${name} (${registeredTools.length} tools)\n`);
return true;
}
// ---- Install npm package ----
function installNPMPackage(spec, skillsDir) {
// Strip npm: prefix if present
if (spec.startsWith('npm:')) spec = spec.slice(4);
process.stderr.write(`[manager] installing: ${spec}\n`);
const installDir = path.join(skillsDir, '.npm_install_' + Date.now());
fs.mkdirSync(installDir, { recursive: true });
try {
execSync(`npm install ${spec} --no-save --prefix "${installDir}"`, {
cwd: installDir, stdio: ['pipe', 'pipe', 'pipe'],
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(installDir, 'node_modules') }
});
} catch (e) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: e.stderr ? e.stderr.toString() : e.message };
}
const nm = path.join(installDir, 'node_modules');
if (!fs.existsSync(nm)) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: 'node_modules not created' };
}
let foundPluginDir = null;
let foundName = null;
const entries = fs.readdirSync(nm);
for (const entry of entries) {
const dir = path.join(nm, entry);
if (!fs.statSync(dir).isDirectory()) continue;
if (entry.startsWith('@')) {
const subs = fs.readdirSync(dir);
for (const sub of subs) {
const subDir = path.join(dir, sub);
if (fs.existsSync(path.join(subDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(subDir, 'package.json')) && readJSON(path.join(subDir, 'package.json'))?.openclaw)) {
foundPluginDir = subDir;
foundName = entry + '/' + sub;
}
}
continue;
}
if (fs.existsSync(path.join(dir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(dir, 'package.json')) && readJSON(path.join(dir, 'package.json'))?.openclaw)) {
foundPluginDir = dir;
foundName = entry;
}
}
if (!foundPluginDir) {
process.stderr.write(`[manager] standard scan failed, trying enhanced detection...\n`);
for (const entry of entries) {
if (foundPluginDir) break;
const dir = path.join(nm, entry);
if (!fs.statSync(dir).isDirectory()) continue;
if (entry.startsWith('@')) {
for (const sub of fs.readdirSync(dir)) {
if (foundPluginDir) break;
const subDir = path.join(dir, sub);
const detected = tryDetectOCPackage(subDir, entry + '/' + sub);
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
}
} else {
const detected = tryDetectOCPackage(dir, entry);
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
}
}
}
if (!foundPluginDir) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: `no OC plugin found in installed package "${spec}"` };
}
// Copy to skills dir
const targetDir = path.join(skillsDir, foundName);
if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
cpSync(foundPluginDir, targetDir);
fs.rmSync(installDir, { recursive: true, force: true });
return { name: foundName, dir: targetDir };
}
function cpSync(src, dst) {
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src)) {
const s = path.join(src, entry);
const d = path.join(dst, entry);
if (fs.statSync(s).isDirectory()) {
cpSync(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
// ---- OpenClaw CLI compatibility ----
function showQR(text) {
try {
const qrcode = require('qrcode');
qrcode.generate(text, { small: true }, (qr) => process.stdout.write(qr + '\n'));
} catch {
process.stdout.write(`QR: ${text}\n`);
process.stdout.write('(install qrcode package for QR display: npm install qrcode)\n');
}
}
async function runCLI(skillsDir, cliArgs) {
const cmd = cliArgs[0] || '';
switch (cmd) {
case 'plugin:install': {
const spec = cliArgs[1];
if (!spec) throw new Error('Usage: openclaw plugin:install <npm:package|clawhub:name|path>');
const result = installNPMPackage(spec, skillsDir);
if (result.error) throw new Error(result.error);
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;
}
case 'plugin:uninstall': {
const name = cliArgs[1];
if (!name) throw new Error('Usage: openclaw plugin:uninstall <name>');
const targetDir = path.join(skillsDir, name);
if (!fs.existsSync(targetDir)) throw new Error(`Plugin not found: ${name}`);
fs.rmSync(targetDir, { recursive: true, force: true });
console.log(`Uninstalled: ${name}`);
break;
}
case 'plugin:list': {
if (!fs.existsSync(skillsDir)) { console.log('(no plugins)'); break; }
let count = 0;
for (const entry of fs.readdirSync(skillsDir)) {
if (entry.startsWith('.')) continue;
const pluginDir = path.join(skillsDir, entry);
if (!fs.statSync(pluginDir).isDirectory()) continue;
const pkg = readJSON(path.join(pluginDir, 'package.json'));
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
if (pkg || manifest) {
const version = pkg?.version || manifest?.version || '?';
const desc = pkg?.description || manifest?.description || '';
console.log(` ${entry} v${version}${desc ? ' — ' + desc : ''}`);
count++;
}
}
if (count === 0) console.log('(no OpenClaw plugins)');
break;
}
case 'auth:login':
case 'auth:qrcode': {
const url = cliArgs[1] || 'openclaw://auth';
console.log('Scan the QR code to log in:');
showQR(url);
console.log('\nOr open this URL:');
console.log(` ${url}`);
break;
}
case 'auth:status': {
console.log('Auth status: not implemented (running in HomeAgent mode)');
break;
}
case 'config:get': {
const key = cliArgs[1];
if (!key) throw new Error('Usage: openclaw config:get <key>');
// TODO: read from HomeAgent config system when bridged
console.log(`(not available in CLI mode: ${key})`);
break;
}
case 'config:set': {
const key = cliArgs[1];
const value = cliArgs[2];
if (!key || value === undefined) throw new Error('Usage: openclaw config:set <key> <value>');
// TODO: write to HomeAgent config system when bridged
console.log(`(not available in CLI mode: ${key}=${value})`);
break;
}
case 'config:list':
console.log('(not available in CLI mode)');
break;
case 'env': {
const info = {
homeAgent: true,
openclawVersion: 'compatible',
platform: process.platform,
nodeVersion: process.version,
skillsDir,
};
console.log(JSON.stringify(info, null, 2));
break;
}
case '--version':
case 'version':
console.log('HomeAgent OpenClaw Adapter 1.0.0 (openclaw-compatible)');
break;
case 'help':
case '--help':
console.log(`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/binding
auth:status Check authentication status
config:get <key> Get config value
config:set <key> <val> Set config value
config:list List all config
env Show runtime environment info
--version Show version
help Show this help`);
break;
default:
throw new Error(`Unknown command: ${cmd}\nRun 'openclaw help' for usage.`);
}
}
// ---- Main ----
const args = process.argv.slice(2);
if (args.length < 1) {
process.stderr.write('[manager] usage: node main.js <skills-dir> [openclaw-command...]\n');
process.exit(1);
}
const skillsDir = path.resolve(args[0]);
// CLI mode: if additional args provided, run as openclaw CLI command and exit
if (args.length > 1) {
const cliArgs = args.slice(1);
runCLI(skillsDir, cliArgs).then(() => process.exit(0)).catch(e => {
process.stderr.write(`Error: ${e.message}\n`);
process.exit(1);
});
return;
}
// Server mode: persist skills dir reference for CLI wrapper
try {
const simDir = path.dirname(process.argv[1]);
fs.writeFileSync(path.join(simDir, '.skillsdir'), skillsDir);
} catch (e) {
process.stderr.write(`[manager] warning: could not write .skillsdir: ${e.message}\n`);
}
// Add our bin directory to PATH so subprocesses can find 'openclaw' CLI
try {
const binDir = path.join(path.dirname(process.argv[1]), 'bin');
if (fs.existsSync(binDir)) {
const PATH = process.env.PATH || '';
if (!PATH.includes(binDir)) {
process.env.PATH = binDir + path.delimiter + PATH;
}
}
} catch (e) {
process.stderr.write(`[manager] warning: could not update PATH: ${e.message}\n`);
}
// Load existing plugins on startup
process.stderr.write(`[manager] scanning: ${skillsDir}\n`);
if (fs.existsSync(skillsDir)) {
for (const entry of fs.readdirSync(skillsDir)) {
if (entry.startsWith('.')) continue;
const pluginDir = path.join(skillsDir, entry);
if (!fs.statSync(pluginDir).isDirectory()) continue;
if (fs.existsSync(path.join(pluginDir, 'main.js')) || fs.existsSync(path.join(pluginDir, 'main.py'))) {
continue;
}
if (fs.existsSync(path.join(pluginDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(pluginDir, 'package.json')) && readJSON(path.join(pluginDir, 'package.json'))?.openclaw)) {
loadPlugin(pluginDir, entry);
} else {
const detected = tryDetectOCPackage(pluginDir, entry);
if (detected) {
loadPlugin(detected.dir, detected.name);
}
}
}
}
// ---- JSON-RPC ----
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
rl.on('line', async (line) => {
let req;
try { req = JSON.parse(line); } catch { sendError(null, -32700, 'Parse error'); return; }
const id = req.id;
const method = req.method;
if (method === 'ping') {
writeJSON({ jsonrpc: '2.0', id, result: { status: 'ok', plugins: Object.keys(loadedPlugins).length } });
return;
}
if (method === 'plugins/list') {
const list = Object.entries(loadedPlugins).map(([name, p]) => ({
name,
tools: p.tools.map(t => ({ name: t.name, description: t.description })),
}));
writeJSON({ jsonrpc: '2.0', id, result: { plugins: list } });
return;
}
if (method === 'plugins/install') {
const pkg = req.params?.package;
if (!pkg) { sendError(id, -32602, 'package required'); return; }
const result = installNPMPackage(pkg, skillsDir);
if (result.error) {
sendError(id, -32603, result.error);
return;
}
// Load the newly installed plugin
const ok = loadPlugin(result.dir, result.name);
if (!ok) {
sendError(id, -32603, `failed to load installed plugin: ${result.name}`);
return;
}
writeJSON({ jsonrpc: '2.0', id, result: { name: result.name, tools: loadedPlugins[result.name].tools.map(t => t.name) } });
return;
}
if (method === 'plugins/uninstall') {
const name = req.params?.name;
if (!name) { sendError(id, -32602, 'name required'); return; }
if (!loadedPlugins[name]) { sendError(id, -32601, `plugin not found: ${name}`); return; }
// Remove tools
const idxs = [];
for (let i = allTools.length - 1; i >= 0; i--) {
if (allTools[i]._plugin === name) allTools.splice(i, 1);
}
delete loadedPlugins[name];
// Remove directory
const pluginDir = path.join(skillsDir, name);
if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
writeJSON({ jsonrpc: '2.0', id, result: { status: 'uninstalled', name } });
return;
}
if (method === 'plugins/detect') {
const pluginDir = req.params?.dir;
const name = req.params?.name || (pluginDir ? path.basename(pluginDir) : '');
if (!pluginDir) { sendError(id, -32602, 'dir required'); return; }
const resolvedDir = path.resolve(pluginDir);
if (!fs.existsSync(resolvedDir)) {
sendError(id, -32601, `directory not found: ${resolvedDir}`);
return;
}
if (loadedPlugins[name]) {
writeJSON({ jsonrpc: '2.0', id, result: { name, tools: loadedPlugins[name].tools.map(t => t.name), type: 'loaded' } });
return;
}
// Install npm dependencies if package.json exists with deps
const pkgPath = path.join(resolvedDir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = readJSON(pkgPath);
if (pkg) {
const hasDeps = (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) ||
(pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0);
if (hasDeps) {
try {
execSync(`npm install --no-save --prefix "${resolvedDir}"`, {
cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'],
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(resolvedDir, 'node_modules') }
});
process.stderr.write(`[manager] installed dependencies for ${name}\n`);
} catch (e) {
process.stderr.write(`[manager] npm install failed for ${name}: ${e.message}\n`);
}
}
}
}
let detected = null;
if (fs.existsSync(path.join(resolvedDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(resolvedDir, 'package.json')) && readJSON(path.join(resolvedDir, 'package.json'))?.openclaw)) {
detected = { dir: resolvedDir, name };
} else {
detected = tryDetectOCPackage(resolvedDir, name);
}
if (!detected) {
sendError(id, -32601, `no OC plugin detected in: ${resolvedDir}`);
return;
}
const ok = loadPlugin(detected.dir, detected.name);
if (!ok) {
sendError(id, -32603, `failed to load detected plugin: ${detected.name}`);
return;
}
writeJSON({ jsonrpc: '2.0', id, result: { name: detected.name, tools: loadedPlugins[detected.name].tools.map(t => t.name), type: detected === Object(detected) && detected.dir === resolvedDir ? 'detected' : 'standard' } });
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') {
const tools = allTools.map(t => ({
name: t.name,
description: t.description || '',
inputSchema: t.parameters || { type: 'object', properties: {} },
}));
writeJSON({ jsonrpc: '2.0', id, result: { tools } });
return;
}
if (method === 'tools/call') {
const params = req.params || {};
const toolName = params.name;
const args = params.arguments || {};
// 通道输出路由toolName 匹配已注册通道名时,调通道的输出 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 = allTools.find(t => t.name === toolName);
if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; }
if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; }
try {
const result = await tool.execute('mgr-call-1', args, undefined, undefined);
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;
}
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}`);
});