mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
- registry.go: replace channelDevice+RegisterChannel with RegisterOutputChannel
handler routes output via sp.CallTool to JS manager outbound handlers
register {plugin}_read_{ch}_input tool for agent to poll buffered input
- plugin.go: handle channel_input notifications in translateAndRegister
buffer payload in channelInputBuf + InjectInterruptText to notify agent
- manager/main.js: registerChannel supports {plugin: ChannelPlugin} format
tools/call routes channel names to outbound.sendText/sendMedia
submitInput sends channel_input notification to Go side
734 lines
26 KiB
JavaScript
734 lines
26 KiB
JavaScript
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
|
||
|
||
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 } });
|
||
}
|
||
}
|
||
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 } });
|
||
},
|
||
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 } });
|
||
},
|
||
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 } });
|
||
},
|
||
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) => {},
|
||
};
|
||
|
||
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) {
|
||
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}`);
|
||
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 === '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}`);
|
||
});
|