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 '); 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 '); 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 '); // 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 '); // 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 [args] Commands: plugin:install Install a plugin (npm:xxx, clawhub:xxx, or path) plugin:uninstall Uninstall a plugin plugin:list List installed plugins auth:login [url] Show QR code for login/binding auth:status Check authentication status config:get Get config value config:set 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 [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}`); });