diff --git a/internal/plugins/all.go b/internal/plugins/all.go index 6d46b685..20af4112 100644 --- a/internal/plugins/all.go +++ b/internal/plugins/all.go @@ -37,7 +37,7 @@ package plugins // pluginmgr failback 恢复链必需(webui/pluginmgr/recoverydiag) // seq ToolAPI(工具表直接操作) // remotedevice RegisterChannel(内核 IO 通道注册面) -// clawhubadapter InjectInputSync(OC 主动注入) +// clawhubadapter 已迁出为外部插件(HologramAgent/clawhubadapter,2026-11-04) // agentcli ★ EventPublish —— 见下 // cfgmgr / skillmgr / seemedia (保留) // @@ -94,7 +94,6 @@ package plugins import ( _ "github.com/JianFeeeee/HologramAgent/internal/plugins/agentcli" _ "github.com/JianFeeeee/HologramAgent/internal/plugins/cfgmgr" - _ "github.com/JianFeeeee/HologramAgent/internal/plugins/clawhubadapter" _ "github.com/JianFeeeee/HologramAgent/internal/plugins/cli" _ "github.com/JianFeeeee/HologramAgent/internal/plugins/healthcheck" _ "github.com/JianFeeeee/HologramAgent/internal/plugins/kbtree" diff --git a/internal/plugins/clawhubadapter/manager/main.js b/internal/plugins/clawhubadapter/manager/main.js deleted file mode 100644 index 76c8b8e6..00000000 --- a/internal/plugins/clawhubadapter/manager/main.js +++ /dev/null @@ -1,1063 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -// ---- Utility ---- -// stdout 仅承载 JSON-RPC;插件/manager 的 console 输出一律走 stderr,避免污染协议流 -const rawLog = console.log.bind(console); -console.log = (...args) => process.stderr.write(args.map(String).join(' ') + '\n'); - -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, runtime, deliverers, accounts, status } - -// ---- OpenClaw channelRuntime mock ---- -// 真实通道插件依赖 channelRuntime 完成:入站转发(dispatchReplyWithBufferedBlockDispatcher)、 -// 出站回调(dispatcherOptions.deliver,插件自带的发送实现)、poll 轮询与 call 透传。 -function makeChannelRuntime(chName, ch) { - return { - id: chName, - // OC 通用通道轮询输入:Go 端经 channel/send 注入的消息放入 ch.pollQueue, - // 插件每次 chatPolls/getPolls 取走(poll 语义:取走即消费,不重复投递)。 - chatPolls: async (opts) => { - const accountId = (opts && opts.accountId) || 'default'; - const limit = (opts && opts.limit) || 20; - const q = ch.pollQueues.get(accountId) || []; - const msgs = q.splice(0, limit).map((m) => ({ ...m })); - return { msgs }; - }, - getPolls: async (opts) => { - const accountId = (opts && opts.accountId) || 'default'; - const limit = (opts && opts.limit) || 20; - const q = ch.pollQueues.get(accountId) || []; - const msgs = q.splice(0, limit).map((m) => ({ ...m })); - return { msgs }; - }, - // 插件经 runtime 直接调用服务器 API:无目标服务器,转发 Go 端作日志/降级 - call: async (method, args) => { - notify('channel_output', { channel: chName, type: 'call', method, args }); - return { ok: true }; - }, - reply: { - // 入站消息 + 出站 deliver 绑定。ctx 来自真实插件(Body/From/SessionKey/AccountId...) - dispatchReplyWithBufferedBlockDispatcher: async (opts) => { - const ctx = (opts && opts.ctx) || {}; - const dopts = (opts && opts.dispatcherOptions) || {}; - const accountId = ctx.AccountId || ctx.accountId || 'default'; - ch.deliverers.set(accountId, { - deliver: dopts.deliver, - typingCallbacks: dopts.typingCallbacks || {}, - }); - notify('channel_input', { - channel: chName, - payload: { - content: ctx.BodyForAgent || ctx.Body || ctx.RawBody || '', - from: ctx.From || ctx.SenderId || '', - sessionKey: ctx.SessionKey || '', - accountId, - messageSid: ctx.MessageSid || '', - chatType: ctx.ChatType || 'direct', - raw: ctx, - }, - }); - const buffer = []; - return { - sendNow: async (items) => { - for (const item of (items || [])) await deliverItem(ch, accountId, item); - }, - addToBuffer: async (item) => { buffer.push(item); }, - sendBuffer: async () => { - for (const item of buffer) await deliverItem(ch, accountId, item); - buffer.length = 0; - }, - closeBuffer: async () => {}, - }; - }, - }, - }; -} - -async function deliverItem(ch, accountId, item) { - const d = ch.deliverers.get(accountId); - if (!d || typeof d.deliver !== 'function') return; - const payload = { - text: (item && item.text) || '', - mediaUrls: (item && (item.mediaUrls || (item.mediaUrl ? [item.mediaUrl] : []))) || [], - }; - if (d.typingCallbacks && typeof d.typingCallbacks.onReplyStart === 'function') { - try { await d.typingCallbacks.onReplyStart(payload); } catch {} - } - await d.deliver(payload); - if (d.typingCallbacks && typeof d.typingCallbacks.onCleanup === 'function') { - try { await d.typingCallbacks.onCleanup(payload); } catch {} - } -} - -// ---- OpenClaw gateway 生命周期桥 ---- -// 通道插件的心跳/收消息/状态上报都挂在 gateway.startAccount 上;startAccount 会永久挂起 -// (await new Promise(()=>{})),配合 OC health-monitor 判定账号存活,必须 fire-and-forget。 -async function startChannels(name) { - const ch = registeredChannels[name]; - if (!ch || !ch.channelPlugin) return; - const gateway = ch.channelPlugin.gateway; - if (!gateway || typeof gateway.startAccount !== 'function') return; - - ch.runtime = ch.runtime || makeChannelRuntime(name, ch); - ch.accounts = ch.accounts || {}; - ch.status = ch.status || { running: false, connected: false }; - - const chCfg = ch.channelPlugin.config || {}; - const passCfg = ch.pluginConfig || chCfg; - let accountIds = []; - try { - if (typeof chCfg.listAccountIds === 'function') { - // OC 规范签名: listAccountIds(cfg) - const ids = chCfg.listAccountIds(passCfg); - accountIds = (ids && ids.then ? await ids : (Array.isArray(ids) ? ids : [])); - } - } catch (e) { - process.stderr.write(`[manager] ${name}: listAccountIds failed: ${e.message}\n`); - } - if (!Array.isArray(accountIds) || accountIds.length === 0) accountIds = ['default']; - - for (const accountId of accountIds) { - let account = null; - try { - if (typeof chCfg.resolveAccount === 'function') { - // OC 规范签名: resolveAccount(cfg, accountId) - account = chCfg.resolveAccount(passCfg, accountId); - if (account && account.then) account = await account; - } - } catch (e) { - process.stderr.write(`[manager] ${name}: resolveAccount(${accountId}) failed: ${e.message}\n`); - } - if (!account) account = { accountId }; - - if (ch.accounts[accountId]) continue; - ch.accounts[accountId] = { started: true }; - - const getStatus = () => ({ ...ch.status, accountId }); - const setStatus = (patch) => { - ch.status = { ...ch.status, ...(patch || {}) }; - notify('channel_status', { channel: name, status: { ...ch.status, accountId } }); - }; - - const ctx = { - account, - cfg: ch.pluginConfig || chCfg, - channelRuntime: ch.runtime, - getStatus, - setStatus, - }; - - gateway.startAccount(ctx).catch((err) => { - process.stderr.write(`[manager] ${name}: startAccount(${accountId}) crashed: ${err.message}\n`); - ch.status = { ...ch.status, running: false, connected: false, error: err.message }; - notify('channel_status', { channel: name, status: { ...ch.status, accountId } }); - }); - } -} - -function stopChannels(name) { - const ch = registeredChannels[name]; - if (!ch || !ch.channelPlugin || !ch.accounts) return; - const gateway = ch.channelPlugin.gateway; - if (!gateway || typeof gateway.stopAccount !== 'function') return; - for (const accountId of Object.keys(ch.accounts)) { - const account = { accountId }; - gateway.stopAccount({ account, channelRuntime: ch.runtime, cfg: ch.pluginConfig || {} }).catch((err) => { - process.stderr.write(`[manager] ${name}: stopAccount(${accountId}) failed: ${err.message}\n`); - }); - } - ch.accounts = {}; -} - -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, gateway: {startAccount, stopAccount}, config: {...}, ... } } - 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, - deliverers: new Map(), accounts: {}, status: {}, - pollQueues: new Map(), - }; - // gateway 生命周期桥:fire-and-forget,绝不阻塞 registerChannel - startChannels(chName).catch((e) => - process.stderr.write(`[manager] ${name}: startChannels failed: ${e.message}\n`)); - } 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 HologramAgent mode)'); - break; - } - - case 'config:get': { - const key = cliArgs[1]; - if (!key) throw new Error('Usage: openclaw config:get '); - // TODO: read from HologramAgent 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 HologramAgent 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('HologramAgent 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; - } - - // 向通道注入输入:Go 端外部输入(webui 会话/其他插件)→ pollQueue → 插件 chatPolls 轮询取走 - if (method === 'channel/send') { - const chName = req.params?.channel; - const payload = req.params?.payload || {}; - const accountId = (req.params && req.params.accountId) || payload.accountId || 'default'; - const ch = registeredChannels[chName]; - if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; } - ch.pollQueues.set(accountId, ch.pollQueues.get(accountId) || []); - ch.pollQueues.get(accountId).push({ - sessionKey: payload.sessionKey || `${chName}:${accountId}:${payload.from || 'poll'}`, - from: payload.from || '', - text: payload.content || payload.text || '', - type: payload.type || 'text', - timestamp: Date.now(), - }); - writeJSON({ jsonrpc: '2.0', id, result: { status: 'queued', channel: chName, accountId } }); - return; - } - - // 停止通道账号(stopAccount:停止心跳/轮询)。channel 可省略(停该插件全部通道) - if (method === 'channel/stop') { - const chName = req.params?.channel; - const accountId = req.params?.accountId; - if (chName) { - const ch = registeredChannels[chName]; - if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; } - if (accountId) { - const gateway = ch.channelPlugin && ch.channelPlugin.gateway; - if (gateway && typeof gateway.stopAccount === 'function') { - gateway.stopAccount({ account: { accountId }, channelRuntime: ch.runtime, cfg: ch.pluginConfig || {} }).catch((e) => - process.stderr.write(`[manager] ${chName}: stopAccount(${accountId}) failed: ${e.message}\n`)); - } - if (ch.accounts) delete ch.accounts[accountId]; - } else { - stopChannels(chName); - } - writeJSON({ jsonrpc: '2.0', id, result: { status: 'stopped', channel: chName, accountId: accountId || 'all' } }); - } else { - for (const name of Object.keys(registeredChannels)) stopChannels(name); - writeJSON({ jsonrpc: '2.0', id, result: { status: 'stopped', channel: 'all' } }); - } - return; - } - - // 启动通道账号(gateway.startAccount fire-and-forget) - if (method === 'channel/start') { - const chName = req.params?.channel; - const ch = registeredChannels[chName]; - if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; } - ch.accounts = {}; - startChannels(chName).catch((e) => process.stderr.write(`[manager] ${chName}: startChannels failed: ${e.message}\n`)); - writeJSON({ jsonrpc: '2.0', id, result: { status: 'started', channel: chName } }); - 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/channels') { - const list = Object.entries(registeredChannels).map(([name, ch]) => ({ - name, - plugin: ch.pluginName, - type: ch.type || 'text', - status: ch.status || {}, - accounts: ch.accounts ? Object.keys(ch.accounts) : [], - })); - writeJSON({ jsonrpc: '2.0', id, result: { channels: 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; } - - // 先优雅停靠通道账号(stopAccount 停止心跳/轮询),再卸载 - for (const chName of Object.keys(registeredChannels)) { - if (registeredChannels[chName].pluginName === name) { - stopChannels(chName); - } - } - - // 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; - let metaObj = {}; - try { metaObj = typeof args.meta === 'string' ? JSON.parse(args.meta) : (args.meta || {}); } catch {} - const accountId = metaObj.accountId || 'default'; - - if (channelPlugin && channelPlugin.outbound) { - // 旧格式 outbound 直发 - const to = metaObj.user_id || metaObj.to || metaObj.group_id || ''; - const ctx = { to, text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', cfg: {}, accountId }; - 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 if (ch.deliverers && ch.deliverers.has(accountId)) { - // OpenClaw ChannelPlugin 事件式发送:回复交给插件在入站时挂载的 deliver(插件自带发送实现) - await deliverItem(ch, accountId, { - text: args.payload || '', - mediaUrls: metaObj.mediaUrl ? [metaObj.mediaUrl] : [], - }); - writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', via: 'channelRuntime.deliver' } }); - } else if (ch.runtime) { - // 通道已启动但尚未收到入站消息(deliver 未建立):降级为 channel_output 事件 - notify('channel_output', { - channel: toolName, type: 'message', - text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', to: metaObj.user_id || '', - }); - writeJSON({ jsonrpc: '2.0', id, result: { status: 'queued', via: 'channel_output' } }); - } 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}`); -}); - -// ---- 优雅停靠:进程退出前逐个 stopAccount(插件停止心跳/轮询) ---- -let shuttingDown = false; -async function shutdown() { - if (shuttingDown) return; - shuttingDown = true; - for (const name of Object.keys(registeredChannels)) stopChannels(name); - setTimeout(() => process.exit(0), 2000); -} -process.on('SIGTERM', shutdown); -process.on('SIGINT', shutdown); diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go deleted file mode 100644 index a4c12307..00000000 --- a/internal/plugins/clawhubadapter/plugin.go +++ /dev/null @@ -1,1526 +0,0 @@ -package clawhubadapter - -import ( - "archive/tar" - "archive/zip" - "bytes" - "compress/gzip" - _ "embed" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/JianFeeeee/HologramAgent/internal/plugin" - safeclose "github.com/JianFeeeee/HologramAgent/internal/safeclose" - sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" -) - -//go:embed manager/main.js -var managerSrc string - -//go:embed pysimulator/main.py -var pySimulatorSrc string - -//go:embed simulator/openclaw_cli.js -var openclawCliSrc string - -func init() { - plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter") - plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) { - dir := "" - if dataDir, ok := config["data_dir"].(string); ok && dataDir != "" { - dir = filepath.Join(dataDir, "skills") - } - return New(name, dir), nil - }) -} - -type Plugin struct { - stopChOnce sync.Once - name string - skillsDir string - simulatorDir string - // pythonBin 是运行 skill 子进程的解释器(配置 python_bin)。 - // 留空表示未配置:此时优先用 /.venv/bin/python,再退回系统 python3。 - pythonBin string - // skillWorkdir 是 skill 子进程的工作目录(配置 skill_workdir)。 - // 留空表示用 skill 自身目录。 - skillWorkdir string - sidecars []*sidecarProcess - manager *sidecarProcess - mu sync.Mutex - sdk *sdk.PluginSDK - dispatcher *RegistryDispatcher - httpClient *http.Client - - stopCh chan struct{} - stopOnce sync.Once -} - -// pluginSingleton 内核单例引用(Start 时设置),供 SendToChannel/ChannelSender 使用 -var pluginSingleton *Plugin - -func New(name, skillsDir string) *Plugin { - sd := "" - if skillsDir != "" { - sd = filepath.Join(skillsDir, ".simulator") - } - return &Plugin{ - name: name, - stopCh: make(chan struct{}), - skillsDir: skillsDir, - simulatorDir: sd, - dispatcher: NewDispatcher(), - } -} - -func (p *Plugin) Name() string { return p.name } - -func (p *Plugin) Start(s *sdk.PluginSDK) error { - s.SetAutoRestart(true) - p.sdk = s - pluginSingleton = p - - s.Settings().RegisterDef(sdk.ConfigDef{ - Key: "skills_dir", Type: "string", DisplayName: "Skill 加载目录", - Description: "OpenClaw 技能加载目录路径(留空则使用默认路径)", - }) - s.Settings().RegisterDef(sdk.ConfigDef{ - Key: "simulator_dir", Type: "string", DisplayName: "模拟器工作目录", - Description: "OpenClaw 模拟器工作目录路径(留空则使用默认路径)", - }) - // ★ Python 解释器与 skill 工作目录(2026-10-25 新增)。 - // - // 此前是硬编码猜测:先找 /usr/bin/python3,再找 /usr/local/bin/python3。 - // 后果是 skill 声明的依赖(httpx 等)装在哪个解释器里就决定了它能不能跑, - // 而"猜"这件事根本没有正确解 —— 不同部署的 Python 布局完全不同。 - // - // 现在:由配置**显式指定**,与 skills_dir/simulator_dir 同一套风格。 - // 留空时仍走按 skill 目录探测 .venv 的既有逻辑(向后兼容)。 - s.Settings().RegisterDef(sdk.ConfigDef{ - Key: "python_bin", Type: "string", DisplayName: "Python 解释器", - Description: "运行 skill 用的 Python 解释器绝对路径,例如 /opt/ha-py/bin/python。" + - "留空则优先用 /.venv/bin/python,再退回系统 python3。", - }) - s.Settings().RegisterDef(sdk.ConfigDef{ - Key: "skill_workdir", Type: "string", DisplayName: "Skill 工作目录", - Description: "skill 子进程的工作目录(留空则用 skill 自身目录)。" + - "需要固定相对路径读写时用它;不影响解释器选择。", - }) - p.httpClient = &http.Client{Timeout: 30 * time.Second} - if v, _ := s.Settings().Get("skills_dir"); v != nil { - if s, ok := v.(string); ok && s != "" { - p.skillsDir = s - } - } - if v, _ := s.Settings().Get("simulator_dir"); v != nil { - if s, ok := v.(string); ok && s != "" { - p.simulatorDir = s - } - } - if v, _ := s.Settings().Get("python_bin"); v != nil { - if str, ok := v.(string); ok && str != "" { - p.pythonBin = str - } - } - if v, _ := s.Settings().Get("skill_workdir"); v != nil { - if str, ok := v.(string); ok && str != "" { - p.skillWorkdir = str - } - } - // 默认目录:内核 data_dir 下 skills 目录(与配置 core.daemon.data_dir 对齐) - if p.skillsDir == "" { - if v, _ := s.Settings().GetCore("daemon.data_dir"); v != nil { - if dir, ok := v.(string); ok && dir != "" { - p.skillsDir = filepath.Join(dir, "skills") - } - } - } - if p.skillsDir == "" { - p.skillsDir = filepath.Join("data", "skills") - } - if p.simulatorDir == "" { - p.simulatorDir = filepath.Join(p.skillsDir, ".simulator") - } - - // Launch OC plugin manager first (handles OC-format plugin installation and lifecycle) - os.MkdirAll(p.skillsDir, 0755) - if err := p.launchManager(s); err != nil { - log.Printf("[clawhubadapter] launch manager: %v", err) - } - - // Load existing plugins from skills dir - if entries, err := os.ReadDir(p.skillsDir); err == nil { - for _, entry := range entries { - if strings.HasPrefix(entry.Name(), ".") { - continue - } - skillPath := filepath.Join(p.skillsDir, entry.Name()) - subs, err := os.ReadDir(skillPath) - if err != nil { - continue - } - - hasMainJS := false - hasMainPy := false - hasOCManifest := false - hasOCPackage := false - for _, f := range subs { - switch f.Name() { - case "main.js": - hasMainJS = true - case "main.py": - hasMainPy = true - case "openclaw.plugin.json": - hasOCManifest = true - case "package.json": - hasOCPackage = hasOCExtensions(filepath.Join(skillPath, "package.json")) - } - } - - switch { - case hasMainJS: - if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil { - log.Printf("[clawhubadapter] sidecar %s: %v", entry.Name(), err) - } - case hasMainPy: - if err := p.loadPySidecar(s, skillPath, entry.Name()); err != nil { - // ★ main.py 不等于 JSON-RPC sidecar。ClawHub 上大量技能的 - // main.py 是 **CLI 入口**(`python3 main.py ''`), - // 对它发 ping 必然超时(实测 bilibili / gaode 各等 5s)。 - // - // 以前这里只打一行错误就完事 —— 启动日志一片红,而技能既没 - // 变成工具、也没交给 skillmgr,等于半死不活。 - // 现在:失败即降级为「纯指令技能」移交 skillmgr,让模型仍可 - // 按 SKILL 文档用命令跑它;日志改成说明而不是错误。 - log.Printf("[clawhubadapter] pysidecar %s not ready (%v); "+ - "treating as CLI-type skill, handing off to skillmgr", entry.Name(), err) - p.publishSkillDetected(s, skillPath) - } - case hasOCManifest || hasOCPackage: - log.Printf("[clawhubadapter] ocplugin %s handled by manager", entry.Name()) - default: - // 纯 SKILL 类型:不属于 OpenClaw 兼容层,移交原生 skillmgr 管理。 - // skillmgr 启动时也会自行全扫(本插件启动更早的场景由事件补齐)。 - p.publishSkillDetected(s, skillPath) - } - } - } - - // Register plugin management tools that talk to the manager (always, even if skills dir is empty) - tp := p.name + "_" - s.RegisterTool(tp+"plugin_install", sdk.ToolDef{ - Name: tp + "plugin_install", - Description: "安装插件。支持 npm: 前缀(npm 包)、clawhub: 前缀(ClawHub 市场,如 clawhub:openclaw-codex-app-server)。安装后立即可用。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "package": map[string]interface{}{"type": "string", "description": "插件包标识。npm: 从 npm 安装,clawhub: 从 ClawHub 市场安装"}, - }, - "required": []string{"package"}, - }, - }, p.handlePluginInstall) - - s.RegisterTool(tp+"plugin_uninstall", sdk.ToolDef{ - Name: tp + "plugin_uninstall", - Description: "卸载已安装的插件,支持所有类型(sidecar、skill、manager 插件、ClawHub 安装的插件)。会停止进程、删除目录并清理注册。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{"type": "string", "description": "要卸载的插件名称(目录名,如 my-plugin)"}, - }, - "required": []string{"name"}, - }, - }, p.handlePluginUninstall) - - s.RegisterTool(tp+"search", sdk.ToolDef{ - Name: tp + "search", - Description: "搜索 ClawHub 插件市场,查找可安装的插件。返回插件名称、描述和安装命令提示。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{"type": "string", "description": "搜索关键词"}, - }, - "required": []string{"query"}, - }, - }, p.handleClawHubSearch) - - s.RegisterTool(tp+"list", sdk.ToolDef{ - Name: tp + "list", - Description: "列出所有已安装的 ClawHub 插件及其工具。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{}, - }, - }, p.handlePluginList) - - s.RegisterTool(tp+"plugin_info", sdk.ToolDef{ - Name: tp + "plugin_info", - Description: "查看单个已安装插件的详细信息:类型(OC/sidecar/SKILL)、工具列表、关联通道及运行状态。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{"type": "string", "description": "插件名称(目录名)"}, - }, - "required": []string{"name"}, - }, - }, p.handlePluginInfo) - - s.RegisterTool(tp+"plugin_reload", sdk.ToolDef{ - Name: tp + "plugin_reload", - Description: "重新加载已安装的插件(代码或配置变更后生效,如 ClawHub 更新)。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{"type": "string", "description": "插件名称(目录名)"}, - }, - "required": []string{"name"}, - }, - }, p.handlePluginReload) - - s.RegisterTool(tp+"channel_list", sdk.ToolDef{ - Name: tp + "channel_list", - Description: "列出所有已注册的消息通道及其运行状态(running/connected/账号列表)。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{}, - }, - }, p.handleChannelList) - - s.RegisterTool(tp+"channel_send", sdk.ToolDef{ - Name: tp + "channel_send", - Description: "向指定通道注入一条消息(经通道插件的 chatPolls/getPolls 轮询取走,如微信/钉钉通用通道)。用于主动向通道投递内容。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "channel": map[string]interface{}{"type": "string", "description": "通道名称(channel_list 查询)"}, - "content": map[string]interface{}{"type": "string", "description": "消息内容"}, - "from": map[string]interface{}{"type": "string", "description": "发送方标识(可选)"}, - "accountId": map[string]interface{}{"type": "string", "description": "账号 ID(可选,默认 default)"}, - }, - "required": []string{"channel", "content"}, - }, - }, p.handleChannelSend) - - s.RegisterTool(tp+"channel_start", sdk.ToolDef{ - Name: tp + "channel_start", - Description: "启动指定通道的账号(重新执行 startAccount,恢复心跳/收消息轮询)。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "channel": map[string]interface{}{"type": "string", "description": "通道名称(channel_list 查询)"}, - }, - "required": []string{"channel"}, - }, - }, p.handleChannelStart) - - s.RegisterTool(tp+"channel_stop", sdk.ToolDef{ - Name: tp + "channel_stop", - Description: "停止指定通道(执行 stopAccount,停止心跳/收消息轮询)。channel 省略则停止全部通道。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "channel": map[string]interface{}{"type": "string", "description": "通道名称(可选,省略则停止全部)"}, - "accountId": map[string]interface{}{"type": "string", "description": "账号 ID(可选,默认停止该通道全部账号)"}, - }, - "required": []string{}, - }, - }, p.handleChannelStop) - - // Start file-based IPC for CLI integration (settings sync + reload requests) - go p.ipcGoroutine(s) - - return nil -} - -func (p *Plugin) ipcGoroutine(s *sdk.PluginSDK) { - ticker := time.NewTicker(3 * time.Second) - defer ticker.Stop() - - for { - select { - case <-p.stopCh: - return - case <-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 ──────────────────────────────────────────────── - -// launchManager 启动 OC 插件管理器(Node.js 进程),用于安装/卸载/加载 OC 格式插件 -func (p *Plugin) launchManager(s *sdk.PluginSDK) error { - managerPath := filepath.Join(p.simulatorDir, "manager.js") - if err := os.MkdirAll(p.simulatorDir, 0755); err != nil { - return fmt.Errorf("create simulator dir: %w", err) - } - if err := os.WriteFile(managerPath, []byte(managerSrc), 0644); err != nil { - return fmt.Errorf("write manager: %w", err) - } - - // Write openclaw CLI wrapper so plugins can exec 'openclaw plugin:install' etc. - cliBinDir := filepath.Join(p.simulatorDir, "bin") - if err := os.MkdirAll(cliBinDir, 0755); err != nil { - return fmt.Errorf("create cli bin dir: %w", err) - } - openclawPath := filepath.Join(cliBinDir, "openclaw") - if err := os.WriteFile(openclawPath, []byte(openclawCliSrc), 0755); err != nil { - return fmt.Errorf("write openclaw CLI: %w", err) - } - - // Ensure skills dir exists for the manager to scan - os.MkdirAll(p.skillsDir, 0755) - - sp, err := launchProcess("node", managerPath, p.skillsDir, "manager", p.simulatorDir) - if err != nil { - return fmt.Errorf("launch manager: %w", err) - } - if sp == nil { - return nil - } - - // Drain initial registration notifications from already-loaded plugins - for done := false; !done; { - select { - case n := <-sp.NotifyChan(): - p.translateAndRegister(n, sp, s, "manager") - default: - done = true - } - } - go p.notifyLoop(sp, s, "manager") - - p.mu.Lock() - p.manager = sp - p.sidecars = append(p.sidecars, sp) - p.mu.Unlock() - - log.Printf("[clawhubadapter] OC plugin manager started") - return nil -} - -// ─── 管理工具处理 ──────────────────────────────────────────── - -func (p *Plugin) handlePluginInstall(args map[string]interface{}) (interface{}, error) { - pkg, _ := args["package"].(string) - if pkg == "" { - return errorResult("package is required"), nil - } - - if strings.HasPrefix(pkg, "clawhub:") { - return p.installFromClawHub(pkg) - } - - if strings.HasPrefix(pkg, "npm:") { - pkg = strings.TrimPrefix(pkg, "npm:") - } - - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - - if mgr == nil { - return errorResult("plugin manager not available"), nil - } - - data, err := mgr.call("plugins/install", map[string]interface{}{ - "package": pkg, - }) - if err != nil { - return errorResult(fmt.Sprintf("install failed: %v", err)), nil - } - - var result struct { - Name string `json:"name"` - Tools []string `json:"tools"` - } - if err := json.Unmarshal(data, &result); err != nil { - return map[string]interface{}{ - "content": fmt.Sprintf("Plugin installed. Raw response: %s", string(data)), - }, nil - } - - return map[string]interface{}{ - "content": fmt.Sprintf("已安装插件: %s\n 工具: %s", result.Name, strings.Join(result.Tools, ", ")), - }, nil -} - -const clawhubAPI = "https://clawhub.ai/api/v1" - -func (p *Plugin) installFromClawHub(spec string) (interface{}, error) { - name := strings.TrimPrefix(spec, "clawhub:") - if name == "" { - return errorResult("clawhub package name is required"), nil - } - - pkgURL := fmt.Sprintf("%s/packages/%s/download", clawhubAPI, url.PathEscape(name)) - resp, err := p.httpClient.Get(pkgURL) - if err != nil { - return errorResult(fmt.Sprintf("download failed: %v", err)), nil - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - body, _ := io.ReadAll(resp.Body) - return errorResult(fmt.Sprintf("ClawHub API error (status %d): %s", resp.StatusCode, string(body))), nil - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return errorResult(fmt.Sprintf("read download: %v", err)), nil - } - - extractDir := filepath.Join(p.skillsDir, name) - os.MkdirAll(extractDir, 0755) - - if err := extractArchive(data, extractDir); err != nil { - return errorResult(fmt.Sprintf("extract failed: %v", err)), nil - } - - if err := p.reloadPlugin(name); err != nil { - // Fallback: try manager's enhanced detection - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - - if mgr != nil { - data, mgrErr := mgr.call("plugins/detect", map[string]interface{}{ - "dir": extractDir, - "name": name, - }) - if mgrErr == nil { - var result struct { - Name string `json:"name"` - Tools []string `json:"tools"` - Type string `json:"type"` - } - if json.Unmarshal(data, &result) == nil { - return map[string]interface{}{ - "content": fmt.Sprintf("已从 ClawHub 安装插件: %s\n 工具: %s", result.Name, strings.Join(result.Tools, ", ")), - }, nil - } - } - } - - return errorResult(fmt.Sprintf("loaded but with warning: %v", err)), nil - } - - return map[string]interface{}{ - "content": fmt.Sprintf("已从 ClawHub 安装插件: %s", name), - }, nil -} - -func (p *Plugin) handleClawHubSearch(args map[string]interface{}) (interface{}, error) { - query, _ := args["query"].(string) - if query == "" { - return errorResult("query is required"), nil - } - - searchURL := fmt.Sprintf("%s/search?q=%s", clawhubAPI, url.QueryEscape(query)) - resp, err := p.httpClient.Get(searchURL) - if err != nil { - return errorResult(fmt.Sprintf("search failed: %v", err)), nil - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - var result struct { - Results []struct { - Slug string `json:"slug"` - DisplayName string `json:"displayName"` - Summary string `json:"summary"` - Version string `json:"version"` - Downloads int `json:"downloads"` - } `json:"results"` - } - if err := json.Unmarshal(body, &result); err != nil { - return map[string]interface{}{ - "content": fmt.Sprintf("Search results (raw):\n%s", string(body)), - }, nil - } - - if len(result.Results) == 0 { - return map[string]interface{}{ - "content": fmt.Sprintf("未找到匹配 \"%s\" 的 ClawHub 插件", query), - }, nil - } - - var lines []string - for _, pkg := range result.Results { - ver := pkg.Version - if ver == "" { - ver = "latest" - } - name := pkg.DisplayName - if name == "" { - name = pkg.Slug - } - lines = append(lines, fmt.Sprintf("- %s (%s) v%s | ⬇ %d\n %s\n 安装: clawhubadapter_plugin_install package=clawhub:%s", - name, pkg.Slug, ver, pkg.Downloads, pkg.Summary, pkg.Slug)) - } - - return map[string]interface{}{ - "content": fmt.Sprintf("在 ClawHub 找到 %d 个插件:\n%s", len(result.Results), strings.Join(lines, "\n")), - }, nil -} - -func (p *Plugin) handlePluginUninstall(args map[string]interface{}) (interface{}, error) { - name, _ := args["name"].(string) - if name == "" { - return errorResult("name is required"), nil - } - - var logs []string - pluginDir := filepath.Join(p.skillsDir, name) - - // 1. Stop & remove sidecar process if running - p.mu.Lock() - var aliveSidecars []*sidecarProcess - for _, sp := range p.sidecars { - if sp.name == name { - sp.Close() - logs = append(logs, fmt.Sprintf("已停止 sidecar 进程: %s", name)) - } else { - aliveSidecars = append(aliveSidecars, sp) - } - } - p.sidecars = aliveSidecars - - p.mu.Unlock() - - // 3. Try manager for OC plugins - mgr := p.manager - if mgr != nil { - data, err := mgr.call("plugins/uninstall", map[string]interface{}{ - "name": name, - }) - if err == nil { - logs = append(logs, fmt.Sprintf("管理器已卸载: %s", string(data))) - } - } - - // 4. Delete directory from disk - if _, statErr := os.Stat(pluginDir); statErr == nil { - if err := os.RemoveAll(pluginDir); err != nil { - logs = append(logs, fmt.Sprintf("删除目录失败: %v", err)) - } else { - logs = append(logs, fmt.Sprintf("已删除目录: %s", pluginDir)) - } - } - - if len(logs) == 0 { - return errorResult(fmt.Sprintf("未找到插件 '%s'", name)), nil - } - - result := fmt.Sprintf("已卸载插件: %s\n%s", name, strings.Join(logs, "\n")) - result += "\n提示: 部分工具注册信息将在下次重启后完全清理。如需立即生效,请使用 reload 命令。" - return map[string]interface{}{ - "content": result, - }, nil -} - -func (p *Plugin) handlePluginList(args map[string]interface{}) (interface{}, error) { - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - - if mgr != nil { - data, err := mgr.call("plugins/list", nil) - if err == nil && data != nil { - var result struct { - Plugins []struct { - Name string `json:"name"` - Tools []struct { - Name string `json:"name"` - Description string `json:"description"` - } `json:"tools"` - } `json:"plugins"` - } - if err := json.Unmarshal(data, &result); err == nil { - var parts []string - parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir)) - - if len(result.Plugins) > 0 { - parts = append(parts, fmt.Sprintf("\nOC 插件 (%d):", len(result.Plugins))) - for _, pl := range result.Plugins { - var names []string - for _, t := range pl.Tools { - names = append(names, t.Name) - } - parts = append(parts, fmt.Sprintf(" %s: %s", pl.Name, strings.Join(names, ", "))) - } - } - - p.mu.Lock() - if len(p.sidecars) > 0 { - sidecarCount := 0 - for _, sp := range p.sidecars { - if sp != p.manager { - sidecarCount++ - } - } - if sidecarCount > 0 { - parts = append(parts, fmt.Sprintf("\nSidecar 插件 (%d):", sidecarCount)) - for _, sp := range p.sidecars { - if sp == p.manager { - continue - } - tools, _ := sp.ListTools() - var names []string - for _, t := range tools { - names = append(names, t.Name) - } - parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, strings.Join(names, ", "))) - } - } - } - caps := p.dispatcher.Capabilities() - if len(caps) > 0 { - parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps))) - for _, c := range caps { - parts = append(parts, fmt.Sprintf(" %s", c)) - } - } - if len(result.Plugins) == 0 && len(p.sidecars) <= 1 { - parts = append(parts, "没有已安装的插件。") - } - p.mu.Unlock() - - return map[string]interface{}{ - "content": strings.Join(parts, "\n"), - }, nil - } - } - } - - // Fallback: list known plugins from Go side - p.mu.Lock() - defer p.mu.Unlock() - - var parts []string - parts = append(parts, fmt.Sprintf("Skills dir: %s\n", p.skillsDir)) - - if len(p.sidecars) > 0 { - parts = append(parts, fmt.Sprintf("\nSidecar/OC 插件 (%d):", len(p.sidecars))) - for _, sp := range p.sidecars { - tools, err := sp.ListTools() - toolList := "" - if err == nil { - var names []string - for _, t := range tools { - names = append(names, t.Name) - } - toolList = strings.Join(names, ", ") - } - parts = append(parts, fmt.Sprintf(" %s: %s", sp.name, toolList)) - } - } - - caps := p.dispatcher.Capabilities() - if len(caps) > 0 { - parts = append(parts, fmt.Sprintf("\nCapabilities (%d):", len(caps))) - for _, c := range caps { - parts = append(parts, fmt.Sprintf(" %s", c)) - } - } - - if len(p.sidecars) == 0 { - parts = append(parts, "没有已安装的插件(原生技能由 skillmgr 管理,用 skill_list 查看)。") - } - - return map[string]interface{}{ - "content": strings.Join(parts, "\n"), - }, nil -} - -func (p *Plugin) handlePluginInfo(args map[string]interface{}) (interface{}, error) { - name, _ := args["name"].(string) - if name == "" { - return errorResult("name is required"), nil - } - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - - typ := "" - var tools []string - - if mgr != nil { - if data, err := mgr.call("plugins/list", nil); err == nil && data != nil { - var result struct { - Plugins []struct { - Name string `json:"name"` - Tools []struct { - Name string `json:"name"` - Description string `json:"description"` - } `json:"tools"` - } `json:"plugins"` - } - if json.Unmarshal(data, &result) == nil { - for _, pl := range result.Plugins { - if pl.Name == name { - typ = "OC (manager)" - for _, t := range pl.Tools { - tools = append(tools, t.Name) - } - } - } - } - } - } - - if typ == "" { - p.mu.Lock() - for _, sp := range p.sidecars { - if sp == mgr || sp.name != name { - continue - } - typ = "sidecar" - if tl, err := sp.ListTools(); err == nil { - for _, t := range tl { - tools = append(tools, t.Name) - } - } - } - p.mu.Unlock() - } - - if typ == "" { - return errorResult(fmt.Sprintf("未找到插件 '%s'", name)), nil - } - - var parts []string - parts = append(parts, fmt.Sprintf("插件: %s\n类型: %s", name, typ)) - if len(tools) > 0 { - parts = append(parts, fmt.Sprintf("工具 (%d):\n - %s", len(tools), strings.Join(tools, "\n - "))) - } - - var own []string - for _, c := range p.channelSummary(mgr) { - if c["plugin"] == name { - own = append(own, fmt.Sprintf(" %s | type=%v running=%v connected=%v accounts=%v", - c["name"], c["type"], c["running"], c["connected"], c["accounts"])) - } - } - if len(own) > 0 { - parts = append(parts, "通道:\n"+strings.Join(own, "\n")) - } else { - parts = append(parts, "通道: 无") - } - - return map[string]interface{}{ - "content": strings.Join(parts, "\n"), - }, nil -} - -func (p *Plugin) handlePluginReload(args map[string]interface{}) (interface{}, error) { - name, _ := args["name"].(string) - if name == "" { - return errorResult("name is required"), nil - } - if err := p.reloadPlugin(name); err != nil { - return errorResult(fmt.Sprintf("reload failed: %v", err)), nil - } - return map[string]interface{}{ - "content": fmt.Sprintf("插件 %s 已重新加载", name), - }, nil -} - -// channelSummary 汇总 manager 通道注册信息与 Go 侧 channel_status 实时缓存 -func (p *Plugin) channelSummary(mgr *sidecarProcess) []map[string]interface{} { - var out []map[string]interface{} - if mgr == nil { - return out - } - data, err := mgr.call("plugins/channels", nil) - if err != nil || data == nil { - return out - } - var result struct { - Channels []struct { - Name string `json:"name"` - Plugin string `json:"plugin"` - Type string `json:"type"` - Status map[string]interface{} `json:"status"` - Accounts []string `json:"accounts"` - } `json:"channels"` - } - if json.Unmarshal(data, &result) != nil { - return out - } - for _, c := range result.Channels { - entry := map[string]interface{}{ - "name": c.Name, "plugin": c.Plugin, "type": c.Type, "accounts": c.Accounts, - } - channelStatusMu.Lock() - if st, ok := channelStatus[c.Name]; ok { - entry["running"] = st["running"] - entry["connected"] = st["connected"] - } else { - entry["running"] = c.Status["running"] - entry["connected"] = c.Status["connected"] - } - channelStatusMu.Unlock() - out = append(out, entry) - } - return out -} - -func (p *Plugin) handleChannelList(args map[string]interface{}) (interface{}, error) { - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - if mgr == nil { - return errorResult("plugin manager not available"), nil - } - chans := p.channelSummary(mgr) - if len(chans) == 0 { - return map[string]interface{}{ - "content": "没有已注册的通道。", - }, nil - } - var lines []string - for _, c := range chans { - lines = append(lines, fmt.Sprintf("- %s | plugin=%v type=%v running=%v connected=%v accounts=%v", - c["name"], c["plugin"], c["type"], c["running"], c["connected"], c["accounts"])) - } - return map[string]interface{}{ - "content": fmt.Sprintf("已注册通道 (%d):\n%s", len(chans), strings.Join(lines, "\n")), - }, nil -} - -func (p *Plugin) handleChannelSend(args map[string]interface{}) (interface{}, error) { - channel, _ := args["channel"].(string) - content, _ := args["content"].(string) - if channel == "" || content == "" { - return errorResult("channel and content are required"), nil - } - payload := map[string]interface{}{"content": content} - if from, _ := args["from"].(string); from != "" { - payload["from"] = from - } - if acc, _ := args["accountId"].(string); acc != "" { - payload["accountId"] = acc - } - if err := p.SendToChannel(channel, payload); err != nil { - return errorResult(fmt.Sprintf("send failed: %v", err)), nil - } - return map[string]interface{}{ - "content": fmt.Sprintf("消息已投递到通道 %s(等待插件轮询取走)", channel), - }, nil -} - -func (p *Plugin) handleChannelStart(args map[string]interface{}) (interface{}, error) { - channel, _ := args["channel"].(string) - if channel == "" { - return errorResult("channel is required"), nil - } - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - if mgr == nil { - return errorResult("plugin manager not available"), nil - } - data, err := mgr.call("channel/start", map[string]interface{}{"channel": channel}) - if err != nil { - return errorResult(fmt.Sprintf("start failed: %v", err)), nil - } - var result struct { - Status string `json:"status"` - } - json.Unmarshal(data, &result) - return map[string]interface{}{ - "content": fmt.Sprintf("通道 %s 启动请求已发出(%v)", channel, result.Status), - }, nil -} - -func (p *Plugin) handleChannelStop(args map[string]interface{}) (interface{}, error) { - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - if mgr == nil { - return errorResult("plugin manager not available"), nil - } - params := map[string]interface{}{} - channel, _ := args["channel"].(string) - accountId, _ := args["accountId"].(string) - if channel != "" { - params["channel"] = channel - } - if accountId != "" { - params["accountId"] = accountId - } - data, err := mgr.call("channel/stop", params) - if err != nil { - return errorResult(fmt.Sprintf("stop failed: %v", err)), nil - } - var result struct { - Status string `json:"status"` - } - json.Unmarshal(data, &result) - target := channel - if target == "" { - target = "全部" - } - return map[string]interface{}{ - "content": fmt.Sprintf("通道 %s 停止请求已发出(%v)", target, result.Status), - }, nil -} - -func (p *Plugin) loadOCPlugin(s *sdk.PluginSDK, dir, name string) error { - // Delegate to manager's plugins/load instead of launching a separate simulator. - // The manager is the single Node.js process that handles all OC plugins. - p.mu.Lock() - mgr := p.manager - p.mu.Unlock() - if mgr == nil { - return fmt.Errorf("plugin manager not available") - } - - // Check if manager already has this plugin loaded - data, listErr := mgr.call("plugins/list", nil) - if listErr == nil && data != nil { - var result struct { - Plugins []struct { - Name string `json:"name"` - } `json:"plugins"` - } - if json.Unmarshal(data, &result) == nil { - for _, pl := range result.Plugins { - if pl.Name == name { - log.Printf("[clawhubadapter] plugin %s already loaded by manager, skipping", name) - return nil - } - } - } - } - - // Ask manager to load this plugin (notifications flow through the manager's stdout → Go notifyLoop) - data, err := mgr.call("plugins/load", map[string]interface{}{ - "dir": dir, - "name": name, - }) - if err != nil { - return fmt.Errorf("manager plugins/load: %w", err) - } - - var result struct { - Name string `json:"name"` - 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 -} - -// notifyLoop 持续监听模拟器的注册通知,即时转译注册到核心。 -// 生命周期绑定 sidecarProcess.NotifyChan,sp.Close() 时会关闭通道使循环退出。 -func (p *Plugin) notifyLoop(sp *sidecarProcess, s *sdk.PluginSDK, pluginName string) { - for n := range sp.NotifyChan() { - p.translateAndRegister(n, sp, s, pluginName) - } -} - -func (p *Plugin) translateAndRegister(n OCNotification, sp *sidecarProcess, s *sdk.PluginSDK, pluginName string) { - if n.Method == "channel_input" { - var params struct { - Channel string `json:"channel"` - Payload map[string]interface{} `json:"payload"` - } - if err := json.Unmarshal(n.Params, ¶ms); err != nil || params.Channel == "" { - return - } - channelInputBufMu.Lock() - channelInputBuf[params.Channel] = append(channelInputBuf[params.Channel], params.Payload) - channelInputBufMu.Unlock() - content, _ := params.Payload["content"].(string) - if content == "" { - data, _ := json.Marshal(params.Payload) - content = string(data) - } - // 同步注入并取回复,再把回复送回通道(agent → output_send__<通道> → deliver → 微信) - out := s.InjectInputSync(pluginName, params.Channel, "text", map[string]interface{}{ - "content": content, - }) - if out != nil { - reply, _ := out.Payload["content"].(string) - if reply != "" { - meta := map[string]interface{}{} - if from, _ := params.Payload["from"].(string); from != "" { - meta["user_id"] = from - } - if acc, _ := params.Payload["accountId"].(string); acc != "" { - meta["accountId"] = acc - } - go func() { - if _, err := sp.CallTool(params.Channel, map[string]interface{}{ - "payload": reply, - "meta": meta, - }); err != nil { - log.Printf("[clawhubadapter] channel %s reply dispatch failed: %v", params.Channel, err) - } - }() - } - } - return - } - if n.Method == "channel_status" { - var params struct { - Channel string `json:"channel"` - Status map[string]interface{} `json:"status"` - } - if err := json.Unmarshal(n.Params, ¶ms); err != nil || params.Channel == "" { - return - } - channelStatusMu.Lock() - if channelStatus == nil { - channelStatus = make(map[string]map[string]interface{}) - } - channelStatus[params.Channel] = params.Status - channelStatusMu.Unlock() - log.Printf("[clawhubadapter] channel_status %s: running=%v connected=%v", params.Channel, - params.Status["running"], params.Status["connected"]) - return - } - if n.Method == "channel_output" { - // 降级输出事件(通道已启动但 deliver 尚未建立):仅记录,消息不丢失于协议层 - var params struct { - Channel string `json:"channel"` - Type string `json:"type"` - Text string `json:"text"` - } - if json.Unmarshal(n.Params, ¶ms) == nil && params.Channel != "" { - log.Printf("[clawhubadapter] channel_output %s (type=%s): %.120s", params.Channel, params.Type, params.Text) - } - return - } - if n.Method != "register" { - return - } - var params struct { - Type string `json:"type"` - Data json.RawMessage `json:"data"` - } - if err := json.Unmarshal(n.Params, ¶ms); err != nil { - return - } - p.dispatcher.Dispatch(params.Type, params.Data, pluginName, sp, s) -} - -// 向通道注入外部输入(经 manager channel/send → pollQueue → 插件 chatPolls 轮询取走)。 -// 供内核其他组件(webui 会话、其他插件)向依赖 runtime 轮询的通用通道插件投递消息。 -func (p *Plugin) SendToChannel(channel string, payload map[string]interface{}) error { - p.mu.Lock() - m := p.manager - p.mu.Unlock() - if m == nil { - return fmt.Errorf("clawhubadapter manager not running") - } - _, err := m.call("channel/send", map[string]interface{}{ - "channel": channel, - "payload": payload, - }) - return err -} - -// ChannelSender 返回 clawhubadapter 单例,供内核其他组件注入通道输入(nil 表示未启动) -func ChannelSender() *Plugin { - return pluginSingleton -} - -func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error { - simPath := filepath.Join(p.simulatorDir, "pysim.py") - if err := os.MkdirAll(p.simulatorDir, 0755); err != nil { - return fmt.Errorf("create simulator dir: %w", err) - } - if err := os.WriteFile(simPath, []byte(pySimulatorSrc), 0644); err != nil { - return fmt.Errorf("write pysimulator: %w", err) - } - - // 选择 Python 解释器。优先级(2026-10-25 改为可配置): - // - // ① 插件配置 python_bin —— **显式指定,最高优先** - // ② /.venv/bin/python —— skill 自带的虚拟环境 - // ③ 系统 python3(/usr/bin → /usr/local/bin) - // - // 为什么 ① 必须在最前:不同部署的 Python 布局完全不同,"猜"没有正确解。 - // 实测事故:`bilibili-all-in-one` 声明 requirements 含 httpx,而系统 - // Python 没装 ⇒ 启动即 ModuleNotFoundError。往系统 Python 里装又会污染 - // 发行版环境(PEP 668 明确禁止,--break-system-packages 会破坏 apt 的 - // 包一致性)。正确解是让部署方显式指定用哪个解释器/venv。 - pythonBin, err := resolvePython(name, dir, p.pythonBin) - if err != nil { - return err - } - - // 工作目录:配置指定则用它(需创建),否则用 skill 自身目录。 - workDir := dir - if p.skillWorkdir != "" { - if err := os.MkdirAll(p.skillWorkdir, 0755); err != nil { - return fmt.Errorf("创建 skill_workdir(%s): %w", p.skillWorkdir, err) - } - workDir = p.skillWorkdir - } - sp, err := launchProcessIn(pythonBin, simPath, dir, name, p.simulatorDir, workDir) - if err != nil { - return fmt.Errorf("launch pysimulator: %w", err) - } - if sp == nil { - return nil - } - - // 处理注册通知 (同 OC 插件流程) - for done := false; !done; { - select { - case n := <-sp.NotifyChan(): - p.translateAndRegister(n, sp, s, name) - default: - done = true - } - } - go p.notifyLoop(sp, s, name) - - tools, err := sp.ListTools() - if err != nil { - sp.Close() - return fmt.Errorf("list tools: %w", err) - } - log.Printf("[clawhubadapter] pysidecar %s registered %d tools", name, len(tools)) - - p.mu.Lock() - p.sidecars = append(p.sidecars, sp) - p.mu.Unlock() - return nil -} - -func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error { - sp, err := launchSidecar(dir, name, p.simulatorDir) - if err != nil { - return fmt.Errorf("launch: %w", err) - } - if sp == nil { - return nil - } - - tools, err := sp.ListTools() - if err != nil { - sp.Close() - return fmt.Errorf("list tools: %w", err) - } - - for _, tool := range tools { - toolName := fmt.Sprintf("%s_%s", name, tool.Name) - tDef := sdk.ToolDef{ - Name: toolName, - Description: fmt.Sprintf("[%s] %s", name, tool.Description), - Parameters: tool.InputSchema, - } - handler := func(sp *sidecarProcess, toolName string) sdk.ToolHandler { - return func(args map[string]interface{}) (interface{}, error) { - return sp.CallTool(toolName, args) - } - }(sp, tool.Name) - if err := s.RegisterTool(toolName, tDef, handler); err != nil { - log.Printf("[clawhubadapter] register sidecar tool %s: %v", toolName, err) - continue - } - log.Printf("[clawhubadapter] registered sidecar tool: %s (from %s)", toolName, name) - } - - p.mu.Lock() - p.sidecars = append(p.sidecars, sp) - p.mu.Unlock() - log.Printf("[clawhubadapter] sidecar %s started with %d tools", name, len(tools)) - return nil -} - -func (p *Plugin) Stop() error { - p.mu.Lock() - defer p.mu.Unlock() - // 关停 ipcGoroutine(stopCh),避免重载后旧 goroutine 残留导致线程累积 - // 见 agentcli 同类注释:Once 不可重入,close 的守卫必须用独立 once。 - p.stopOnce.Do(func() { - safeclose.Safe(&p.stopChOnce, p.stopCh) - }) - for _, sp := range p.sidecars { - sp.Close() - } - p.sidecars = nil - p.manager = nil - p.sdk = nil - return nil -} - -// publishSkillDetected 发布 skill_detected 事件,将纯 SKILL 条目移交原生 -// skillmgr 插件注册。OpenClaw 兼容层不持有 native skill 状态。 -func (p *Plugin) publishSkillDetected(s *sdk.PluginSDK, path string) { - // ★ 走**受限** SDK 事件面,而不是内核私有总线(2026-10-27)。 - // - // 这样 clawhubadapter 对外部化不再有阻塞项:它只需要 SDK 的 - // Events().Publish,而不需要 internal/events。事件类型仍是 skill_detected - // (白名单内),载荷出向会经 security 过闸,Source 由 SDK 固定成本插件名。 - if err := s.Events().Publish("skill_detected", map[string]interface{}{"path": path}); err != nil { - log.Printf("[clawhubadapter] publish skill_detected failed: %v", err) - return - } - log.Printf("[clawhubadapter] skill %s detected, handed off to skillmgr", filepath.Base(path)) -} - -func errorResult(msg string) interface{} { - return map[string]interface{}{ - "isError": true, - "content": msg, - } -} - -// hasOCExtensions 检测 package.json 中是否有 openclaw.extensions 或 openclaw.runtimeExtensions -func hasOCExtensions(pkgPath string) bool { - data, err := os.ReadFile(pkgPath) - if err != nil { - return false - } - var pkg struct { - OpenClaw *struct { - Extensions interface{} `json:"extensions"` - RuntimeExtensions interface{} `json:"runtimeExtensions"` - } `json:"openclaw"` - } - if err := json.Unmarshal(data, &pkg); err != nil { - return false - } - return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil) -} - -func (p *Plugin) reloadPlugin(name string) error { - pluginDir := filepath.Join(p.skillsDir, name) - info, err := os.Stat(pluginDir) - if err != nil || !info.IsDir() { - return fmt.Errorf("plugin dir not found: %s", pluginDir) - } - - if p.sdk == nil { - return fmt.Errorf("sdk not initialized") - } - - subs, err := os.ReadDir(pluginDir) - if err != nil { - return err - } - - hasMainJS := false - hasMainPy := false - hasOCManifest := false - hasOCPackage := false - for _, f := range subs { - switch f.Name() { - case "main.js": - hasMainJS = true - case "main.py": - hasMainPy = true - case "openclaw.plugin.json": - hasOCManifest = true - case "package.json": - hasOCPackage = hasOCExtensions(filepath.Join(pluginDir, "package.json")) - } - } - - os.MkdirAll(p.simulatorDir, 0755) - - switch { - case hasMainJS: - return p.loadSidecar(p.sdk, pluginDir, name) - case hasMainPy: - return p.loadPySidecar(p.sdk, pluginDir, name) - case hasOCManifest || hasOCPackage: - return p.loadOCPlugin(p.sdk, pluginDir, name) - default: - // 纯 SKILL 类型:移交原生 skillmgr(与 Start 扫描行为一致) - p.publishSkillDetected(p.sdk, pluginDir) - return nil - } -} - -func extractArchive(data []byte, dest string) error { - if len(data) < 4 { - return fmt.Errorf("archive too small (%d bytes)", len(data)) - } - - if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 { - return extractZIP(data, dest) - } - - return extractTGZ(data, dest) -} - -func extractZIP(data []byte, dest string) error { - zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - return fmt.Errorf("zip: %w", err) - } - for _, f := range zr.File { - target := filepath.Join(dest, f.Name) - if f.FileInfo().IsDir() { - os.MkdirAll(target, 0755) - continue - } - os.MkdirAll(filepath.Dir(target), 0755) - r, err := f.Open() - if err != nil { - return err - } - out, err := os.Create(target) - if err != nil { - r.Close() - return err - } - _, err = io.Copy(out, r) - r.Close() - out.Close() - if err != nil { - return err - } - } - return nil -} - -func extractTGZ(data []byte, dest string) error { - gzr, err := gzip.NewReader(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("gzip: %w", err) - } - defer gzr.Close() - - tr := tar.NewReader(gzr) - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - target := filepath.Join(dest, header.Name) - switch header.Typeflag { - case tar.TypeDir: - os.MkdirAll(target, 0755) - case tar.TypeReg: - os.MkdirAll(filepath.Dir(target), 0755) - f, err := os.Create(target) - if err != nil { - return err - } - if _, err := io.Copy(f, tr); err != nil { - f.Close() - return err - } - f.Close() - } - } - return nil -} - -// fileExists 报告路径存在(错误信息不需要,调用方只关心能否用)。 -func fileExists(p string) bool { - _, err := os.Stat(p) - return err == nil -} - -// resolvePython 决定用哪个 Python 解释器跑 skill,优先级: -// -// ① configured(插件配置 python_bin)—— 显式指定,最高优先 -// ② /.venv/bin/python —— skill 自带虚拟环境 -// ③ 系统 python3(/usr/bin → /usr/local/bin)→ 最后退回 "python3" -// -// ## 为什么必须可配置(2026-10-25 真机事故) -// -// 此前是硬编码猜:先 /usr/bin/python3,再 /usr/local/bin/python3。 -// `bilibili-all-in-one` 声明 requirements 含 httpx,而系统 Python 没装 -// ⇒ 启动即 ModuleNotFoundError。而"猜"没有正确解:不同部署的 Python -// 布局完全不同(发行版 Python、pyenv、uv、conda、venv…)。 -// -// 往系统 Python 里 pip install 是**错的**解法:PEP 668 明确禁止, -// --break-system-packages 会破坏 apt 管理的包一致性。 -// -// ## 配错必须响亮 -// -// 配置了但文件不存在时返回 error,**不静默退回**系统解释器 —— -// 否则用户以为配好了,实际跑在缺依赖的 Python 上,又是一次静默降级。 -func resolvePython(name, skillDir, configured string) (string, error) { - if configured != "" { - if _, err := os.Stat(configured); err != nil { - return "", fmt.Errorf("配置的 python_bin 不可用(%s): %w", configured, err) - } - log.Printf("[clawhub] skill %s: using configured interpreter %s", name, configured) - return configured, nil - } - if venvPy := filepath.Join(skillDir, ".venv", "bin", "python"); fileExists(venvPy) { - log.Printf("[clawhub] skill %s: using its venv interpreter %s", name, venvPy) - return venvPy, nil - } - for _, candidate := range []string{"/usr/bin/python3", "/usr/local/bin/python3"} { - if fileExists(candidate) { - return candidate, nil - } - } - return "python3", nil -} - -// pickPythonFor 是 resolvePython 的便捷包装(测试与旧调用点用)。 -// -// 忽略错误并返回空串会让"配错"看起来像"没配",所以调用方应优先用 -// resolvePython;这里只为保持测试可读性。 -func pickPythonFor(name, skillDir, configured string) string { - v, _ := resolvePython(name, skillDir, configured) - return v -} diff --git a/internal/plugins/clawhubadapter/pysimulator/main.py b/internal/plugins/clawhubadapter/pysimulator/main.py deleted file mode 100644 index 6a2bbdd7..00000000 --- a/internal/plugins/clawhubadapter/pysimulator/main.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env python3 -""" -OpenClaw Python Sidecar Simulator - -Loads a Python plugin (main.py) from the plugin directory and -provides JSON-RPC over stdin/stdout for tool listing and execution. - -Protocol is identical to the Node.js simulator: - - ping: returns {"status": "ok"} - - tools/list: lists registered tools - - tools/call: executes a tool - -Plugin main.py should define: - def register(api): - api.register_tool(name, description, parameters, execute_fn) -""" -import asyncio -import importlib.util -import inspect -import json -import os -import sys - - -def resolve_entry(plugin_dir): - """Resolve the plugin entry point.""" - main_py = os.path.join(plugin_dir, 'main.py') - if os.path.isfile(main_py): - return main_py - return None - - -class PluginAPI: - """API passed to the plugin's register() function.""" - - def __init__(self, plugin_dir): - self.id = os.path.basename(plugin_dir) - self.name = os.path.basename(plugin_dir) - self.version = '1.0.0' - self.description = '' - self.source = plugin_dir - self.root_dir = plugin_dir - self.config = {} - self.plugin_config = {} - self.registration_mode = 'full' - self._tools = {} - - def register_tool(self, name, description='', parameters=None, execute=None): - if parameters is None: - parameters = {'type': 'object', 'properties': {}} - self._notify('register', { - 'type': 'tool', - 'data': { - 'name': name, - 'description': description, - 'parameters': parameters, - } - }) - self._tools[name] = { - 'name': name, - 'description': description, - 'parameters': parameters, - 'execute': execute, - } - - def register_provider(self, provider_type, provider): - self._notify('register', { - 'type': 'provider', - 'data': {'name': provider.name if hasattr(provider, 'name') else str(provider)}, - }) - - def register_channel(self, channel): - self._notify('register', { - 'type': 'channel', - 'data': {'name': channel.name if hasattr(channel, 'name') else str(channel)}, - }) - - def _notify(self, method, params): - line = json.dumps({'jsonrpc': '2.0', 'method': method, 'params': params}, ensure_ascii=False) - sys.stdout.write(line + '\n') - sys.stdout.flush() - - -# ---- Legacy Python Plugin Detection ---- - -def _resolve_env_vars(plugin_dir): - """Read env var requirements from skill.json and resolve from OS env.""" - skill_json = os.path.join(plugin_dir, 'skill.json') - if not os.path.isfile(skill_json): - return {} - try: - with open(skill_json, 'r') as f: - meta = json.load(f) - except Exception: - return {} - - env_map = {} - for key in meta.get('required_env_vars', []): - val = os.environ.get(key) or os.environ.get(key.lower()) - if val: - env_map[key.lower()] = val - for key in meta.get('optional_env_vars', []): - val = os.environ.get(key) or os.environ.get(key.lower()) - if val: - env_map[key.lower()] = val - - return env_map - - -def _collect_sub_skills(instance): - """Discover sub-module names from a class instance by finding attributes with execute methods. - - Prefer instance __dict__ attributes over properties to avoid triggering side effects. - """ - skills = [] - seen = set() - - # First look at instance __dict__ (safe, no property triggers) - for attr_name in list(instance.__dict__.keys()): - if attr_name.startswith('_'): - continue - attr = instance.__dict__[attr_name] - seen.add(attr_name) - if hasattr(attr, 'execute') and callable(attr.execute): - try: - sig = inspect.signature(attr.execute) - params = list(sig.parameters.keys()) - if 'action' in params and len(params) >= 2: - skills.append(attr_name) - except (ValueError, TypeError): - continue - - # Also check class-level non-property attributes - for attr_name in dir(type(instance)): - if attr_name.startswith('_') or attr_name in seen: - continue - try: - attr = getattr(type(instance), attr_name, None) - except Exception: - continue - if isinstance(attr, property): - continue # skip properties (may raise on access) - try: - attr = getattr(instance, attr_name, None) - except Exception: - continue - if attr is None: - continue - seen.add(attr_name) - if hasattr(attr, 'execute') and callable(attr.execute): - try: - sig = inspect.signature(attr.execute) - params = list(sig.parameters.keys()) - if 'action' in params and len(params) >= 2: - skills.append(attr_name) - except (ValueError, TypeError): - continue - - return skills - - -def _readable_class_name(cls): - """Convert PascalCase to snake_case for readable names.""" - import re - name = cls.__name__ - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - - -def _make_async_execute(instance, skill_name): - """Wrap an async sub-module execute into a sync callable for JSON-RPC.""" - def execute(args): - action = args.pop('action', '') if isinstance(args, dict) else '' - result = asyncio.run(instance.execute(skill_name=skill_name, action=action, **args)) - return result - return execute - - -def _make_legacy_execute(instance, attr_name): - """Wrap an async legacy execute into a sync callable for JSON-RPC.""" - def execute(args): - action = args.pop('action', '') if isinstance(args, dict) else '' - handler = getattr(instance, attr_name) - result = asyncio.run(handler.execute(action=action, **args)) - return result - return execute - - -def detect_legacy_plugin(module, plugin_dir, plugin_name): - """Auto-detect legacy Python plugin patterns and register as OC tools. - - Returns a PluginAPI with discovered tools, or None if no pattern matches. - """ - api = PluginAPI(plugin_dir) - - # Pattern 1: Module has a main class with execute(skill_name, action, **kwargs) - # Look for any class that has an 'execute' method accepting 'skill_name' - main_class = None - main_class_name = None - - for name in dir(module): - obj = getattr(module, name, None) - if not isinstance(obj, type): - continue - if not hasattr(obj, 'execute') or not callable(getattr(obj, 'execute', None)): - continue - try: - sig = inspect.signature(obj.execute) - params = list(sig.parameters.keys()) - if 'skill_name' in params: - main_class = obj - main_class_name = name - break - except (ValueError, TypeError): - continue - - if main_class is None: - return None - - # Resolve env vars for constructor - env_args = _resolve_env_vars(plugin_dir) - try: - sig = inspect.signature(main_class.__init__) - init_params = list(sig.parameters.keys())[1:] # skip self - kwargs = {} - for p in init_params: - lower_p = p.lower() - if lower_p in env_args: - kwargs[p] = env_args[lower_p] - sys.stderr.write(f'[pysimulator] legacy: instantiating {main_class_name} with {kwargs}\n') - instance = main_class(**kwargs) - except Exception as e: - sys.stderr.write(f'[pysimulator] legacy: cannot instantiate {main_class_name}: {e}\n') - return None - - # Discover sub-modules - sub_skills = _collect_sub_skills(instance) - sys.stderr.write(f'[pysimulator] legacy: discovered sub-skills: {sub_skills}\n') - - discovered_actions = {} - for attr_name in sub_skills: - handler = getattr(instance, attr_name) - # Build parameter schema from the execute signature - try: - sig = inspect.signature(handler.execute) - param_props = {} - for p_name, p_sig in list(sig.parameters.items())[1:]: # skip 'self' - if p_name == 'action': - param_props['action'] = { - 'type': 'string', - 'description': f'Action to perform in {attr_name}', - } - continue - if p_sig.annotation != inspect.Parameter.empty: - p_type = 'string' - if p_sig.annotation is int: - p_type = 'number' - elif p_sig.annotation is bool: - p_type = 'boolean' - elif p_sig.annotation is float: - p_type = 'number' - else: - anno_str = str(p_sig.annotation) - if 'int' in anno_str: - p_type = 'number' - elif 'bool' in anno_str: - p_type = 'boolean' - elif 'float' in anno_str: - p_type = 'number' - param_props[p_name] = { - 'type': p_type, - 'description': f'Parameter {p_name}', - } - else: - param_props[p_name] = { - 'type': 'string', - 'description': f'Parameter {p_name}', - } - - required = ['action'] - schema = { - 'type': 'object', - 'properties': param_props, - 'required': required, - } - - tool_name = attr_name - doc = handler.execute.__doc__ or f'{attr_name} operations' - first_line = doc.strip().split('\n')[0] if doc else f'{attr_name} module' - - discovered_actions[attr_name] = (tool_name, first_line, schema) - except (ValueError, TypeError) as e: - sys.stderr.write(f'[pysimulator] legacy: skip {attr_name}: {e}\n') - continue - - if discovered_actions: - # Register each sub-skill as a separate OC tool - for attr_name, (tool_name, desc, schema) in discovered_actions.items(): - handler = getattr(instance, attr_name) - api.register_tool( - name=tool_name, - description=f'{plugin_name}: {desc}', - parameters=schema, - execute=_make_legacy_execute(instance, attr_name), - ) - sys.stderr.write(f'[pysimulator] legacy registered tool: {tool_name}\n') - else: - # Fallback: single tool wrapping the whole plugin - try: - sig = inspect.signature(main_class.execute) - skill_params = [p for p in list(sig.parameters.keys())[1:]] # skip self - param_props = {} - for p in skill_params: - param_props[p] = {'type': 'string', 'description': p} - - api.register_tool( - name=plugin_name, - description=f'{plugin_name}: unified operations', - parameters={ - 'type': 'object', - 'properties': param_props, - 'required': skill_params, - }, - execute=_make_async_execute(instance, None), - ) - sys.stderr.write(f'[pysimulator] legacy registered fallback tool: {plugin_name}\n') - except Exception as e: - sys.stderr.write(f'[pysimulator] legacy: fallback failed: {e}\n') - return None - - return api - - -def load_plugin(plugin_dir): - """Load the plugin from main.py. - - Tries OC-style register(api) first, then auto-detects legacy patterns. - """ - entry_path = resolve_entry(plugin_dir) - if entry_path is None: - sys.stderr.write(f'[pysimulator] entry not found in {plugin_dir}\n') - sys.exit(1) - - sys.path.insert(0, plugin_dir) - spec = importlib.util.spec_from_file_location('plugin_main', entry_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - plugin_name = os.path.basename(plugin_dir) - - # OC-style: register(api) - if hasattr(module, 'register'): - api = PluginAPI(plugin_dir) - module.register(api) - return api - - # Legacy auto-detection - sys.stderr.write(f'[pysimulator] no register() function, trying legacy detection for {plugin_name}...\n') - api = detect_legacy_plugin(module, plugin_dir, plugin_name) - if api is not None: - return api - - sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n') - sys.exit(1) - - -def main(): - if len(sys.argv) < 2: - sys.stderr.write('[pysimulator] usage: python3 main.py \n') - sys.exit(1) - - plugin_dir = os.path.abspath(sys.argv[1]) - api = load_plugin(plugin_dir) - - # JSON-RPC loop over stdin - for line in sys.stdin: - line = line.strip() - if not line: - continue - - try: - req = json.loads(line) - except json.JSONDecodeError: - resp = {'jsonrpc': '2.0', 'id': None, 'error': {'code': -32700, 'message': 'Parse error'}} - sys.stdout.write(json.dumps(resp, ensure_ascii=False) + '\n') - sys.stdout.flush() - continue - - req_id = req.get('id') - method = req.get('method', '') - - if method == 'ping': - resp = {'jsonrpc': '2.0', 'id': req_id, 'result': {'status': 'ok'}} - elif method == 'tools/list': - tools = [ - {'name': t['name'], 'description': t['description'], 'inputSchema': t['parameters']} - for t in api._tools.values() - ] - resp = {'jsonrpc': '2.0', 'id': req_id, 'result': {'tools': tools}} - elif method == 'tools/call': - params = req.get('params', {}) - tool_name = params.get('name', '') - arguments = params.get('arguments', {}) - - tool = api._tools.get(tool_name) - if tool is None: - resp = {'jsonrpc': '2.0', 'id': req_id, 'error': {'code': -32601, 'message': f'Tool not found: {tool_name}'}} - elif tool.get('execute') is None: - resp = {'jsonrpc': '2.0', 'id': req_id, 'error': {'code': -32603, 'message': f'Tool {tool_name} has no execute function'}} - else: - try: - result = tool['execute'](arguments) - if isinstance(result, dict) and 'content' in result: - resp = {'jsonrpc': '2.0', 'id': req_id, 'result': result} - else: - text = str(result) if not isinstance(result, str) else result - resp = {'jsonrpc': '2.0', 'id': req_id, 'result': {'content': [{'type': 'text', 'text': text}]}} - except Exception as e: - resp = {'jsonrpc': '2.0', 'id': req_id, 'error': {'code': -32603, 'message': str(e)}} - else: - resp = {'jsonrpc': '2.0', 'id': req_id, 'error': {'code': -32601, 'message': f'Method not found: {method}'}} - - sys.stdout.write(json.dumps(resp, ensure_ascii=False) + '\n') - sys.stdout.flush() - - -if __name__ == '__main__': - main() diff --git a/internal/plugins/clawhubadapter/python_bin_test.go b/internal/plugins/clawhubadapter/python_bin_test.go deleted file mode 100644 index 22b2c69b..00000000 --- a/internal/plugins/clawhubadapter/python_bin_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package clawhubadapter - -import ( - "os" - "path/filepath" - "testing" -) - -// ── Python 解释器选择优先级 ────────────────────────────────── -// -// 背景(2026-10-25 真机事故):bilibili skill 声明 requirements 含 httpx, -// 系统 Python 没装 ⇒ 启动即 ModuleNotFoundError。而此前解释器是**硬编码猜**的: -// 先 /usr/bin/python3,再 /usr/local/bin/python3。 -// -// 「猜」没有正确解:不同部署的 Python 布局完全不同(发行版 Python、 -// pyenv、uv、conda、venv…)。所以改为配置显式指定 + venv 探测兜底。 - -// 判据:配置了 python_bin 且可用 ⇒ 必须用它,优先于 venv 与系统解释器。 -// -// 这是"配置生效"的唯一证据。没有它,配了却仍走系统解释器也不会被发现 -// (正是本次事故的形态:静默降级)。 -func TestPickPython_ConfiguredWins(t *testing.T) { - dir := t.TempDir() - // 造一个假的 venv 解释器,确保"配置优先于 venv"被真正验证 - venvBin := filepath.Join(dir, ".venv", "bin") - os.MkdirAll(venvBin, 0755) - venvPy := filepath.Join(venvBin, "python") - os.WriteFile(venvPy, []byte("#!/bin/sh\n"), 0755) - - cfgPy := filepath.Join(dir, "custom-python") - os.WriteFile(cfgPy, []byte("#!/bin/sh\n"), 0755) - - got := pickPythonFor("bilibili", dir, cfgPy) - if got != cfgPy { - t.Fatalf("配置的 python_bin 应最高优先,期望 %s,实际 %s", cfgPy, got) - } -} - -// 判据:没配 python_bin 但 skill 有 venv ⇒ 用 venv。 -func TestPickPython_VenvWhenNotConfigured(t *testing.T) { - dir := t.TempDir() - venvBin := filepath.Join(dir, ".venv", "bin") - os.MkdirAll(venvBin, 0755) - venvPy := filepath.Join(venvBin, "python") - os.WriteFile(venvPy, []byte("#!/bin/sh\n"), 0755) - - if got := pickPythonFor("x", dir, ""); got != venvPy { - t.Fatalf("无配置时应优先 skill 自带 venv,期望 %s,实际 %s", venvPy, got) - } -} - -// 判据:配置了但**不存在** ⇒ 必须报错,不能静默退回系统解释器。 -// -// 静默退回的后果:用户以为配好了,实际跑在缺依赖的 Python 上 —— -// 这正是本次要修的「静默降级」那一族。 -func TestPickPython_MissingConfiguredIsLoud(t *testing.T) { - dir := t.TempDir() - bad := filepath.Join(dir, "no-such-python") - _, err := resolvePython("x", dir, bad) - if err == nil { - t.Fatal("配置的 python_bin 不存在时必须报错(静默退回会让用户以为配置生效)") - } -} diff --git a/internal/plugins/clawhubadapter/registry.go b/internal/plugins/clawhubadapter/registry.go deleted file mode 100644 index 3729e3b0..00000000 --- a/internal/plugins/clawhubadapter/registry.go +++ /dev/null @@ -1,326 +0,0 @@ -package clawhubadapter - -import ( - "encoding/json" - "fmt" - "log" - "strings" - "sync" - - sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" -) - -var ( - channelInputBuf = map[string][]map[string]interface{}{} - channelInputBufMu sync.Mutex - channelStatus map[string]map[string]interface{} // 通道运行状态(channel_status 通知) - channelStatusMu sync.Mutex -) - -type ToolRegistry struct{} - -func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { - var d struct { - Name string `json:"name"` - Label string `json:"label"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters"` - Plugin string `json:"plugin"` - } - if err := json.Unmarshal(data, &d); err != nil || d.Name == "" { - return - } - pn := pluginName - if d.Plugin != "" { - pn = d.Plugin - } - toolName := fmt.Sprintf("%s_%s", pn, d.Name) - tDef := sdk.ToolDef{ - Name: toolName, - Description: d.Description, - Parameters: d.Parameters, - } - handler := func(sp *sidecarProcess, ocToolName string) sdk.ToolHandler { - return func(args map[string]interface{}) (interface{}, error) { - return sp.CallTool(ocToolName, args) - } - }(sp, d.Name) - if err := s.RegisterTool(toolName, tDef, handler); err != nil { - log.Printf("[clawhubadapter] register tool %s: %v", toolName, err) - } -} - -type providerDef struct { - ocType string - toolSuffix string - desc string -} - -var providerMap = []providerDef{ - {"image_generation", "generate_image", "根据文本描述生成图片,返回图片 URL"}, - {"music_generation", "generate_music", "根据描述生成音乐"}, - {"video_generation", "generate_video", "根据描述生成视频"}, - {"speech", "synthesize_speech", "将文本合成为语音"}, - {"web_search", "web_search", "搜索互联网信息"}, - {"web_fetch", "web_fetch", "获取指定网页的内容"}, - {"media_understanding", "analyze_media", "分析图片、音频或视频内容"}, - {"realtime_transcription", "transcribe_audio", "将音频转写为文字"}, - {"realtime_voice", "voice_io", "实时语音输入输出"}, -} - -type ProviderRegistry struct{} - -func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { - var d struct { - Name string `json:"name"` - Description string `json:"description"` - Plugin string `json:"plugin"` - } - 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 { - if p.ocType == lookupType { - toolName := fmt.Sprintf("%s_%s", pn, p.toolSuffix) - desc := p.desc - if d.Name != "" { - 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{ - Name: toolName, - Description: desc, - Parameters: map[string]interface{}{ - "type": "object", - "properties": props, - }, - } - handler := func(sp *sidecarProcess, ocType string) sdk.ToolHandler { - return func(args map[string]interface{}) (interface{}, error) { - return sp.CallProvider(ocType, args) - } - }(sp, lookupType) - if err := s.RegisterTool(toolName, tDef, handler); err != nil { - log.Printf("[clawhubadapter] register provider tool %s: %v", toolName, err) - } - return - } - } - - 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 HologramAgent", pluginName) - case "embedding", "memory_embedding": - log.Printf("[clawhubadapter] embedding provider %s from %s ignored (HologramAgent uses native embeddings)", lookupType, pluginName) - default: - log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName) - } -} - -type ChannelRegistry struct{} - -func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { - var d struct { - Name string `json:"name"` - Type string `json:"type"` - Plugin string `json:"plugin"` - } - if err := json.Unmarshal(data, &d); err != nil || d.Name == "" { - return - } - chName := d.Name - pn := pluginName - if d.Plugin != "" { - pn = d.Plugin - } - - var caps int - switch d.Type { - case "file": - caps = 2 - case "image": - caps = 4 - case "audio": - caps = 8 - default: - caps = 1 - } - desc := fmt.Sprintf("OC channel %s (from %s)", chName, pn) - // 通道名来自外部 OC 配置(动态):声明处**同时**登记 inputch —— - // 该通道既收(InjectInputSync 见 plugin.go)又发(output_send__<通道>)。 - _ = s.RegisterInputChannel(chName, sdk.ChannelDef{}) - s.RegisterOutputChannel(chName, caps, desc, sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) { - return sp.CallTool(chName, args) - }) - - readToolName := fmt.Sprintf("%s_read_%s_input", pn, strings.ReplaceAll(chName, "-", "_")) - s.RegisterTool(readToolName, sdk.ToolDef{ - Name: readToolName, - Description: fmt.Sprintf("读取 %s 通道的待处理输入消息", chName), - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{}, - }, - }, func(args map[string]interface{}) (interface{}, error) { - channelInputBufMu.Lock() - buf := channelInputBuf[chName] - if len(buf) == 0 { - channelInputBufMu.Unlock() - return map[string]interface{}{"messages": []interface{}{}}, nil - } - msgs := make([]interface{}, len(buf)) - for i, m := range buf { - msgs[i] = m - } - channelInputBuf[chName] = nil - channelInputBufMu.Unlock() - return map[string]interface{}{"messages": msgs}, nil - }) -} - -type StageRegistry struct{} - -func (r *StageRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { - var d struct { - Name string `json:"name"` - Event string `json:"event"` - } - json.Unmarshal(data, &d) - log.Printf("[clawhubadapter] hook %s/%s (plugin: %s) — stub: OC hooks need bidirectional bridge", - d.Name, d.Event, pluginName) -} - -type CapRecorder struct { - mu sync.Mutex - caps []string -} - -func (r *CapRecorder) Record(typeStr string, data json.RawMessage, pluginName string) { - var d struct { - Name string `json:"name"` - Description string `json:"description"` - } - json.Unmarshal(data, &d) - capStr := fmt.Sprintf("[%s] capability: %s", pluginName, typeStr) - if d.Name != "" { - capStr += " (" + d.Name + ")" - } - if d.Description != "" { - capStr += ": " + d.Description - } - r.mu.Lock() - r.caps = append(r.caps, capStr) - r.mu.Unlock() -} - -func (r *CapRecorder) Snapshot() []string { - r.mu.Lock() - defer r.mu.Unlock() - out := make([]string, len(r.caps)) - copy(out, r.caps) - return out -} - -type RegistryDispatcher struct { - toolReg *ToolRegistry - providerReg *ProviderRegistry - channelReg *ChannelRegistry - stageReg *StageRegistry - capRecorder *CapRecorder -} - -func NewDispatcher() *RegistryDispatcher { - return &RegistryDispatcher{ - toolReg: &ToolRegistry{}, - providerReg: &ProviderRegistry{}, - channelReg: &ChannelRegistry{}, - stageReg: &StageRegistry{}, - capRecorder: &CapRecorder{}, - } -} - -func (d *RegistryDispatcher) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) { - switch typeStr { - case "tool": - d.toolReg.Dispatch(data, pluginName, sp, s) - case "channel": - d.channelReg.Dispatch(data, pluginName, sp, s) - case "provider", "image_generation_provider", "music_generation_provider", - "video_generation_provider", "speech_provider", - "realtime_transcription_provider", "realtime_voice_provider", - "media_understanding_provider", - "web_fetch_provider", "web_search_provider", - "embedding_provider", "memory_embedding_provider": - d.providerReg.Dispatch(typeStr, data, pluginName, sp, s) - d.capRecorder.Record(typeStr, data, pluginName) - case "hook", "runtime_lifecycle", "lifecycle", - "agent_event_subscription", "agent_harness", - "session_event", "conversation_binding_resolved", - "interactive_handler", - "cli", "cli_backend", "node_cli_feature", - "command", "http_route", "service", - "gateway_method", "gateway_discovery_service", - "trusted_tool_policy", "tool_metadata", - "context_engine", - "memory_capability", "memory_prompt_section", - "memory_flush_plan", "memory_runtime", - "memory_prompt_supplement", "memory_corpus_supplement", - "session_extension", "session_scheduler_job", - "session_action", "control_ui_descriptor", - "agent_tool_result_middleware": - d.stageReg.Dispatch(data, pluginName, sp, s) - d.capRecorder.Record(typeStr, data, pluginName) - default: - d.capRecorder.Record(typeStr, data, pluginName) - } -} - -func (d *RegistryDispatcher) Capabilities() []string { - return d.capRecorder.Snapshot() -} diff --git a/internal/plugins/clawhubadapter/sidecar.go b/internal/plugins/clawhubadapter/sidecar.go deleted file mode 100644 index 5d8493ae..00000000 --- a/internal/plugins/clawhubadapter/sidecar.go +++ /dev/null @@ -1,420 +0,0 @@ -package clawhubadapter - -import ( - "bufio" - "encoding/json" - "fmt" - safeclose "github.com/JianFeeeee/HologramAgent/internal/safeclose" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" -) - -type sidecarRequest struct { - JSONRPC string `json:"jsonrpc"` - ID int `json:"id"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` -} - -type sidecarResponse struct { - JSONRPC string `json:"jsonrpc"` - ID int `json:"id"` - Result *json.RawMessage `json:"result,omitempty"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error,omitempty"` -} - -type OCPTool struct { - Name string `json:"name"` - Description string `json:"description"` - InputSchema map[string]interface{} `json:"inputSchema"` -} - -type OCCallResult struct { - Content []struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - } `json:"content"` -} - -// OCNotification 是模拟器主动推送的通知 -type OCNotification struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - -// pendingResp 是待应答通道 + 它自己的 close 守卫。 -// -// 为什么需要 once:Close() 可能被多条路径调用(Stop / 重载 / 停机), -// 而 `close(ch)` 二次即 panic(本仓 close 纪律,见 internal/safeclose)。 -// 这些 channel 是**按请求创建**的,不能共用一个全局 Once ⇒ 每个自带一个。 -type pendingResp struct { - ch chan []byte - once sync.Once -} - -func (p *pendingResp) closeOnce() { safeclose.Safe(&p.once, p.ch) } - -type sidecarProcess struct { - readerReadyOnce sync.Once - name string - dir string - cmd *exec.Cmd - stdin *bufio.Writer - mu sync.Mutex - nextID int - closed bool - stopped bool - - // 异步 reader - pending map[int]*pendingResp - notifyCh chan OCNotification - readerStop chan struct{} - readerWg sync.WaitGroup - readerReady chan struct{} -} - -func newSidecarProcess(name, dir string, cmd *exec.Cmd, stdin *bufio.Writer, stdout io.Reader) *sidecarProcess { - sp := &sidecarProcess{ - name: name, - dir: dir, - cmd: cmd, - stdin: stdin, - pending: make(map[int]*pendingResp), - notifyCh: make(chan OCNotification, 1024), - readerStop: make(chan struct{}), - readerReady: make(chan struct{}), - } - sp.readerWg.Add(1) - go sp.readLoop(stdout) - <-sp.readerReady - return sp -} - -func (s *sidecarProcess) readLoop(r io.Reader) { - defer s.readerWg.Done() - scanner := bufio.NewScanner(bufio.NewReader(r)) - // 加大 scanner buffer 防止长行截断 - scanner.Buffer(make([]byte, 0, 1024*64), 1024*64) - safeclose.Safe(&s.readerReadyOnce, s.readerReady) - - for { - select { - case <-s.readerStop: - return - default: - } - - if !scanner.Scan() { - if scanner.Err() != nil { - log.Printf("[clawhubadapter] sidecar %s read error: %v", s.name, scanner.Err()) - } - return - } - line := scanner.Text() - - var base struct { - ID *int `json:"id"` - Method string `json:"method,omitempty"` - Error *json.RawMessage `json:"error,omitempty"` - } - if err := json.Unmarshal([]byte(line), &base); err != nil { - continue - } - - if base.ID != nil { - s.mu.Lock() - pr, ok := s.pending[*base.ID] - delete(s.pending, *base.ID) - s.mu.Unlock() - if ok { - pr.ch <- []byte(line) - } - } else if base.Method != "" { - var notif OCNotification - if err := json.Unmarshal([]byte(line), ¬if); err == nil { - select { - case s.notifyCh <- notif: - default: - log.Printf("[clawhubadapter] sidecar %s notify channel full, dropping: %s", s.name, notif.Method) - } - } - } - } -} - -func (s *sidecarProcess) NotifyChan() <-chan OCNotification { - return s.notifyCh -} - -func launchSidecar(dir, name, simDir string) (*sidecarProcess, error) { - mainJS := filepath.Join(dir, "main.js") - if _, err := os.Stat(mainJS); os.IsNotExist(err) { - return nil, nil - } - return launchProcess("node", mainJS, dir, name, simDir) -} - -// launchProcess 以 dir 作为工作目录拉起子进程(绝大多数调用的形态)。 -func launchProcess(bin, arg, dir, name, simDir string) (*sidecarProcess, error) { - return launchProcessIn(bin, arg, dir, name, simDir, dir) -} - -// launchProcessIn 与 launchProcess 相同,但工作目录可单独指定。 -// -// 为什么要单独一个函数而不是给 launchProcess 加参数:那会改动 4 个既有 -// 调用点(含测试),而它们的语义都是"工作目录 = skill 目录"。只有 -// skill_workdir 配置这一处需要另给目录,用变体表达的意图更清楚。 -func launchProcessIn(bin, arg, dir, name, simDir, workDir string) (*sidecarProcess, error) { - nodePath := bin - if bin == "node" { - if p := os.Getenv("NODE_PATH"); p != "" { - nodePath = filepath.Join(p, "node") - } - } - - cmd := exec.Command(nodePath, arg, dir) - if workDir == "" { - workDir = dir - } - cmd.Dir = workDir - cmd.Stderr = os.Stderr - - // Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command - if simDir != "" { - binDir := filepath.Join(simDir, "bin") - if info, err := os.Stat(binDir); err == nil && info.IsDir() { - env := os.Environ() - binDirPath := binDir + string(os.PathListSeparator) - for i, e := range env { - if strings.HasPrefix(e, "PATH=") { - env[i] = "PATH=" + binDirPath + e[5:] - break - } - } - cmd.Env = env - } - } - - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, fmt.Errorf("stdin pipe: %w", err) - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("stdout pipe: %w", err) - } - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("start %s: %w", name, err) - } - - sp := newSidecarProcess(name, dir, cmd, bufio.NewWriter(stdin), stdout) - - if err := sp.waitReady(); err != nil { - sp.Close() - return nil, err - } - - return sp, nil -} - -// sidecarReadyTimeout 是等 sidecar 回第一个 ping 的上限。 -// -// ★ 从固定 5s 放宽到 30s(2026-10-25 修 CI 误报)。 -// -// 原来的 5s 是**按开发机估算**的:node 冷启动 + 模块加载在快盘上空载时 -// 约 0.2s。而 CI 的共享 runner(以及真实部署里的低配机器)上,一次 -// fork+exec+node 引导遇到 CPU 争抢就会到 5s 以上,实测 CI 报 -// `sidecar echoplugin ping timeout` 而本机全绿。 -// -// 这不是"测试环境太慢所以放宽测试"——它是**产品行为**:sidecar 起不来 -// 等于该 skill 整体不可用。给足时间是正确取舍;判断"真的挂了"应由进程 -// 退出信号负责(readLoop 会感知),而不是由引导耗时猜。 -// -// 30s 的取法:比 systemd 的 TimeoutStopSec(25s) 宽,比用户可感知的 -// "卡住"阈值(约 30s)持平——再长就该让用户看到明确失败。 -const sidecarReadyTimeout = 30 * time.Second - -func (s *sidecarProcess) waitReady() error { - done := make(chan error, 1) - go func() { - _, err := s.call("ping", nil) - done <- err - }() - select { - case err := <-done: - return err - case <-time.After(sidecarReadyTimeout): - return fmt.Errorf("sidecar %s ping timeout(%v 内未响应;慢机器上 node 冷启动可能更久,"+ - "若持续失败请检查 node 是否可用)", s.name, sidecarReadyTimeout) - } -} - -func (s *sidecarProcess) call(method string, params interface{}) ([]byte, error) { - ch := make(chan []byte, 1) - - s.mu.Lock() - if s.closed || s.stopped { - s.mu.Unlock() - return nil, fmt.Errorf("sidecar %s closed", s.name) - } - s.nextID++ - id := s.nextID - s.pending[id] = &pendingResp{ch: ch} - - req := sidecarRequest{ - JSONRPC: "2.0", - ID: id, - Method: method, - Params: params, - } - data, err := json.Marshal(req) - if err != nil { - delete(s.pending, id) - s.mu.Unlock() - return nil, err - } - if _, err := s.stdin.Write(data); err != nil { - delete(s.pending, id) - s.mu.Unlock() - return nil, err - } - if _, err := s.stdin.Write([]byte("\n")); err != nil { - delete(s.pending, id) - s.mu.Unlock() - return nil, err - } - if err := s.stdin.Flush(); err != nil { - delete(s.pending, id) - s.mu.Unlock() - return nil, err - } - s.mu.Unlock() - - select { - case raw := <-ch: - if raw == nil { - return nil, fmt.Errorf("sidecar %s error: nil response", s.name) - } - var resp sidecarResponse - if err := json.Unmarshal(raw, &resp); err != nil { - return nil, fmt.Errorf("sidecar %s unmarshal: %w", s.name, err) - } - if resp.Error != nil { - return nil, fmt.Errorf("sidecar %s error: %s", s.name, resp.Error.Message) - } - if resp.Result == nil { - return nil, nil - } - return []byte(*resp.Result), nil - case <-time.After(30 * time.Second): - s.mu.Lock() - delete(s.pending, id) - s.mu.Unlock() - return nil, fmt.Errorf("sidecar %s call %s timeout", s.name, method) - } -} - -func (s *sidecarProcess) ListTools() ([]OCPTool, error) { - data, err := s.call("tools/list", nil) - if err != nil { - return nil, err - } - if data == nil { - return nil, nil - } - var result struct { - Tools []OCPTool `json:"tools"` - } - if err := json.Unmarshal(data, &result); err != nil { - return nil, err - } - return result.Tools, nil -} - -func (s *sidecarProcess) CallTool(name string, args map[string]interface{}) (string, error) { - data, err := s.call("tools/call", map[string]interface{}{ - "name": name, - "arguments": args, - }) - if err != nil { - return "", err - } - if data == nil { - return "", nil - } - var result OCCallResult - if err := json.Unmarshal(data, &result); err != nil { - return "", err - } - var sb string - for _, c := range result.Content { - if c.Type == "text" { - sb += c.Text - } - } - return sb, nil -} - -func (s *sidecarProcess) CallProvider(providerType string, args map[string]interface{}) (string, error) { - data, err := s.call("provider/call", map[string]interface{}{ - "type": providerType, - "action": "execute", - "args": args, - }) - if err != nil { - return "", err - } - if data == nil { - return "", nil - } - var result OCCallResult - if err := json.Unmarshal(data, &result); err != nil { - return "", err - } - var sb string - for _, c := range result.Content { - if c.Type == "text" { - sb += c.Text - } - } - return sb, nil -} - -func (s *sidecarProcess) Close() { - s.mu.Lock() - if s.closed || s.stopped { - s.mu.Unlock() - return - } - s.stopped = true - s.mu.Unlock() - - // 先杀进程(关闭 stdout pipe),然后 reader 的 Scan() 会退出 - if s.cmd != nil && s.cmd.Process != nil { - s.cmd.Process.Kill() - s.cmd.Wait() - } - - // 等 reader 循环结束 - s.readerWg.Wait() - - s.mu.Lock() - for id, pr := range s.pending { - pr.closeOnce() - delete(s.pending, id) - } - s.mu.Unlock() - - log.Printf("[clawhubadapter] sidecar %s stopped", s.name) -} diff --git a/internal/plugins/clawhubadapter/sidecar_test.go b/internal/plugins/clawhubadapter/sidecar_test.go deleted file mode 100644 index 0ebf118a..00000000 --- a/internal/plugins/clawhubadapter/sidecar_test.go +++ /dev/null @@ -1,409 +0,0 @@ -package clawhubadapter - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" - pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk" -) - -// mockSettings implements pubsdk.SettingsAPI for tests -type mockSettings struct{} - -func (m *mockSettings) DataDir() string { return "/tmp/mock_data" } - -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) SetSecret(key string, value interface{}) error { return nil } -func (m *mockSettings) GetSecret(key string) (string, 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 } - -// IsSecret:E-3 起 SettingsAPI 要求实现(判断某键是否按密文存储)。 -// 判据实现返回 false(mock 里没有密文语义),够用即可。 -func (m *mockSettings) IsSecret(key string) bool { return false } -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 (m *mockSettings) DefsCore(prefix string) []*sdk.ConfigDef { return nil } -func (m *mockSettings) DefsPlugin(plugin, prefix string) []*sdk.ConfigDef { return nil } -func (m *mockSettings) Remove(key string) error { return nil } -func (m *mockSettings) RemoveCore(key string) error { return nil } -func (m *mockSettings) RemovePlugin(plugin, key string) error { return nil } - -func TestLaunchSidecarNoMainJS(t *testing.T) { - tmpDir := t.TempDir() - sp, err := launchSidecar(tmpDir, "nonexistent", "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if sp != nil { - t.Fatal("expected nil for dir without main.js") - } -} - -func TestLaunchSidecarAndListTools(t *testing.T) { - tmpDir := t.TempDir() - src := filepath.Join("testdata", "echoplugin", "main.js") - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read test plugin: %v", err) - } - dst := filepath.Join(tmpDir, "main.js") - if err := os.WriteFile(dst, data, 0755); err != nil { - t.Fatalf("write test plugin: %v", err) - } - - sp, err := launchSidecar(tmpDir, "echoplugin", "") - if err != nil { - t.Fatalf("launch sidecar: %v", err) - } - defer sp.Close() - - tools, err := sp.ListTools() - if err != nil { - t.Fatalf("list tools: %v", err) - } - - if len(tools) == 0 { - t.Fatal("expected at least one tool") - } - - found := false - for _, tool := range tools { - if tool.Name == "echo" { - found = true - if tool.Description == "" { - t.Error("expected non-empty description for echo tool") - } - } - } - if !found { - t.Fatal("expected 'echo' tool in list") - } - t.Logf("tools: %+v", tools) -} - -func TestCallEchoTool(t *testing.T) { - tmpDir := t.TempDir() - src := filepath.Join("testdata", "echoplugin", "main.js") - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read test plugin: %v", err) - } - dst := filepath.Join(tmpDir, "main.js") - if err := os.WriteFile(dst, data, 0755); err != nil { - t.Fatalf("write test plugin: %v", err) - } - - sp, err := launchSidecar(tmpDir, "echoplugin", "") - if err != nil { - t.Fatalf("launch sidecar: %v", err) - } - defer sp.Close() - - result, err := sp.CallTool("echo", map[string]interface{}{ - "text": "hello world", - }) - if err != nil { - t.Fatalf("call echo tool: %v", err) - } - - expected := "Echo: hello world" - if result != expected { - t.Fatalf("expected %q, got %q", expected, result) - } - t.Logf("echo result: %s", result) -} - -func TestCallAddTool(t *testing.T) { - tmpDir := t.TempDir() - src := filepath.Join("testdata", "echoplugin", "main.js") - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read test plugin: %v", err) - } - dst := filepath.Join(tmpDir, "main.js") - if err := os.WriteFile(dst, data, 0755); err != nil { - t.Fatalf("write test plugin: %v", err) - } - - sp, err := launchSidecar(tmpDir, "echoplugin", "") - if err != nil { - t.Fatalf("launch sidecar: %v", err) - } - defer sp.Close() - - result, err := sp.CallTool("add", map[string]interface{}{ - "a": 3.0, - "b": 4.0, - }) - if err != nil { - t.Fatalf("call add tool: %v", err) - } - - expected := "7" - if result != expected { - t.Fatalf("expected %q, got %q", expected, result) - } - t.Logf("add result: %s", result) -} - -func TestCallNonexistentTool(t *testing.T) { - tmpDir := t.TempDir() - src := filepath.Join("testdata", "echoplugin", "main.js") - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read test plugin: %v", err) - } - dst := filepath.Join(tmpDir, "main.js") - if err := os.WriteFile(dst, data, 0755); err != nil { - t.Fatalf("write test plugin: %v", err) - } - - sp, err := launchSidecar(tmpDir, "echoplugin", "") - if err != nil { - t.Fatalf("launch sidecar: %v", err) - } - defer sp.Close() - - _, err = sp.CallTool("nonexistent", nil) - if err == nil { - t.Fatal("expected error for nonexistent tool") - } - t.Logf("expected error: %v", err) -} - -func TestConcurrentCalls(t *testing.T) { - tmpDir := t.TempDir() - src := filepath.Join("testdata", "echoplugin", "main.js") - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read test plugin: %v", err) - } - dst := filepath.Join(tmpDir, "main.js") - if err := os.WriteFile(dst, data, 0755); err != nil { - t.Fatalf("write test plugin: %v", err) - } - - sp, err := launchSidecar(tmpDir, "echoplugin", "") - if err != nil { - t.Fatalf("launch sidecar: %v", err) - } - defer sp.Close() - - done := make(chan bool, 5) - for i := 0; i < 5; i++ { - go func(n int) { - result, err := sp.CallTool("add", map[string]interface{}{ - "a": float64(n), - "b": float64(n * 2), - }) - if err != nil { - t.Errorf("concurrent call %d: %v", n, err) - } - if result != fmt.Sprintf("%d", n+n*2) { - t.Errorf("call %d: expected %d, got %s", n, n+n*2, result) - } - done <- true - }(i) - } - for i := 0; i < 5; i++ { - <-done - } -} - -// ---- Simulator + OpenClaw plugin tests ---- - -func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess { - t.Helper() - simPath, err := filepath.Abs(filepath.Join("simulator", "main.js")) - if err != nil { - t.Fatalf("abs simulator path: %v", err) - } - sp, err := launchProcess("node", simPath, pluginDir, name, "") - if err != nil { - t.Fatalf("launch simulator for %s: %v", name, err) - } - return sp -} - -func TestSimulatorWithOCSimple(t *testing.T) { - pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-simple")) - if err != nil { - t.Fatalf("abs testdata: %v", err) - } - sp := launchSimulator(t, pluginDir, "oc-simple") - defer sp.Close() - - tools, err := sp.ListTools() - if err != nil { - t.Fatalf("list tools: %v", err) - } - if len(tools) != 2 { - t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools) - } - - found := map[string]bool{"greet": false, "ping": false} - for _, tool := range tools { - found[tool.Name] = true - } - if !found["greet"] || !found["ping"] { - t.Fatalf("expected greet and ping tools, got %+v", tools) - } - - result, err := sp.CallTool("greet", map[string]interface{}{"name": "Test"}) - if err != nil { - t.Fatalf("call greet: %v", err) - } - if result != "Hello, Test!" { - t.Fatalf("expected 'Hello, Test!', got %q", result) - } -} - -func TestSimulatorWithOCPackage(t *testing.T) { - pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-pkg")) - if err != nil { - t.Fatalf("abs testdata: %v", err) - } - sp := launchSimulator(t, pluginDir, "oc-pkg") - defer sp.Close() - - tools, err := sp.ListTools() - if err != nil { - t.Fatalf("list tools: %v", err) - } - if len(tools) != 2 { - t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools) - } - - found := map[string]bool{"add": false, "info": false} - for _, tool := range tools { - found[tool.Name] = true - if tool.Name == "add" { - if tool.InputSchema == nil { - t.Error("add tool should have inputSchema") - } - } - } - if !found["add"] || !found["info"] { - t.Fatalf("expected add and info tools, got %+v", tools) - } - - result, err := sp.CallTool("add", map[string]interface{}{"a": 10.0, "b": 20.0}) - if err != nil { - t.Fatalf("call add: %v", err) - } - if result != "30" { - t.Fatalf("expected '30', got %q", result) - } - - infoResult, err := sp.CallTool("info", nil) - if err != nil { - t.Fatalf("call info: %v", err) - } - if infoResult == "" { - t.Fatal("expected non-empty info result") - } - t.Logf("info result: %s", infoResult) -} - -func TestLoadOCPluginViaPluginStart(t *testing.T) { - skillsDir := t.TempDir() - - ocSimpleDir := filepath.Join(skillsDir, "oc-simple") - if err := os.MkdirAll(ocSimpleDir, 0755); err != nil { - t.Fatalf("mkdir: %v", err) - } - for _, name := range []string{"openclaw.plugin.json", "index.js"} { - src := filepath.Join("testdata", "oc-simple", name) - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read %s: %v", name, err) - } - if err := os.WriteFile(filepath.Join(ocSimpleDir, name), data, 0644); err != nil { - t.Fatalf("write %s: %v", name, err) - } - } - - ocPkgDir := filepath.Join(skillsDir, "oc-pkg") - if err := os.MkdirAll(filepath.Join(ocPkgDir, "lib"), 0755); err != nil { - t.Fatalf("mkdir: %v", err) - } - for _, name := range []string{"package.json", "lib/entry.js"} { - src := filepath.Join("testdata", "oc-pkg", name) - data, err := os.ReadFile(src) - if err != nil { - t.Fatalf("read %s: %v", name, err) - } - dst := filepath.Join(ocPkgDir, name) - if err := os.WriteFile(dst, data, 0644); err != nil { - t.Fatalf("write %s: %v", name, err) - } - } - - p := New("openclaw", skillsDir) - - var registeredTools []string - registeredHandlers := make(map[string]sdk.ToolHandler) - sdk := sdk.New("openclaw", sdk.SDKConfig{ - Settings: &mockSettings{}, - RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { - registeredTools = append(registeredTools, name) - registeredHandlers[name] = handler - return nil - }, - }) - - if err := p.Start(sdk); err != nil { - t.Fatalf("start plugin: %v", err) - } - defer p.Stop() - - expected := []string{"oc-simple_greet", "oc-simple_ping", "oc-pkg_add", "oc-pkg_info"} - for _, exp := range expected { - found := false - for _, name := range registeredTools { - if name == exp { - found = true - break - } - } - if !found { - t.Fatalf("expected tool %q not registered. got: %v", exp, registeredTools) - } - } - - handler, ok := registeredHandlers["oc-simple_greet"] - if !ok { - t.Fatal("greet handler not registered") - } - result, err := handler(map[string]interface{}{"name": "OpenClaw"}) - if err != nil { - t.Fatalf("exec greet: %v", err) - } - if result != "Hello, OpenClaw!" { - t.Fatalf("expected 'Hello, OpenClaw!', got %v", result) - } - - handler, ok = registeredHandlers["oc-pkg_add"] - if !ok { - t.Fatal("add handler not registered") - } - result, err = handler(map[string]interface{}{"a": 7.0, "b": 8.0}) - if err != nil { - t.Fatalf("exec add: %v", err) - } - if result != "15" { - t.Fatalf("expected '15', got %v", result) - } -} diff --git a/internal/plugins/clawhubadapter/simulator/main.js b/internal/plugins/clawhubadapter/simulator/main.js deleted file mode 100644 index c1b2f98d..00000000 --- a/internal/plugins/clawhubadapter/simulator/main.js +++ /dev/null @@ -1,422 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -// ---- 工具函数 ---- -function writeJSON(obj) { - process.stdout.write(JSON.stringify(obj) + '\n'); -} - -function sendError(id, code, message) { - writeJSON({ jsonrpc: '2.0', id, error: { code, message } }); -} - -function readJSON(file) { - try { - return JSON.parse(fs.readFileSync(file, 'utf8')); - } catch (e) { - return null; - } -} - -function notify(method, params) { - writeJSON({ jsonrpc: '2.0', method, params }); -} - -// ---- 解析插件入口 ---- -const pluginDir = path.resolve(process.argv[2]); -if (!pluginDir) { - process.stderr.write('[simulator] usage: node main.js \n'); - process.exit(1); -} - -const pkgPath = path.join(pluginDir, 'package.json'); -const pkg = readJSON(pkgPath); -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) && raw.length > 0) { - for (const ext of raw) { - let ep = path.resolve(pluginDir, ext); - if (ep.endsWith('.ts')) { - const jsEp = ep.replace(/\.ts$/, '.js'); - if (fs.existsSync(jsEp)) { entryPath = jsEp; 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(`[simulator] entry not found: ${entryPath}\n`); - process.exit(1); -} - -// ---- 加载插件 ---- -let pluginEntry; -try { - pluginEntry = require(entryPath); -} catch (e) { - process.stderr.write(`[simulator] load plugin: ${e.message}\n`); - process.exit(1); -} - -const entry = pluginEntry.default || pluginEntry; - -if (typeof entry !== 'object' || typeof entry.register !== 'function') { - process.stderr.write(`[simulator] plugin entry must export {default: {register(api)}}\n`); - process.exit(1); -} - -// ---- 注册工具(本地存储,供 tools/list 和 tools/call 用) ---- -const registeredTools = []; - -// ---- Provider 存储(供 provider/call 用) ---- -const registeredProviders = {}; // type -> { name, instance } - -// ---- Channel 存储(供 tools/call 中通道输出路由用) ---- -const registeredChannels = {}; // name -> { output, send, channelPlugin, type } - -function registerTool(defOrFactory, opts) { - if (typeof defOrFactory === 'function') { - const toolCtx = { - id: 'simulator', - cwd: pluginDir, - env: process.env, - allow: ['*'], - }; - const result = defOrFactory(toolCtx); - const tools = Array.isArray(result) ? result : [result]; - for (const t of tools) { - if (t && typeof t.execute === 'function') { - registeredTools.push(t); - notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters } }); - } - } - return; - } - - const def = defOrFactory; - 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, - }); - notify('register', { type: 'tool', data: { name: def.name, label: def.label, description: def.description, parameters: def.parameters } }); -} - -// ---- 构造完整的 OpenClawPluginApi ---- -const api = { - id: entry.id || 'unknown', - name: entry.name || 'Unknown', - version: entry.version, - description: entry.description, - source: pluginDir, - rootDir: pluginDir, - config: {}, - pluginConfig: {}, - registrationMode: 'full', - logger: { - debug: (...args) => {}, - info: (...args) => {}, - warn: (...args) => {}, - error: (...args) => process.stderr.write(`[plugin] ${args.join(' ')}\n`), - }, - resolvePath: (p) => path.resolve(pluginDir, p), - - // ---- 工具注册 ---- - registerTool, - - // ---- Provider 注册(同时存储实例,支持 provider/call) ---- - registerProvider: (provider) => { - if (provider && provider.id) registeredProviders['llm'] = { name: provider.id, instance: provider }; - notify('register', { type: 'provider', data: { name: provider?.id || provider?.name, description: provider?.description } }); - }, - registerEmbeddingProvider: (p) => { - if (p) registeredProviders['embedding'] = { name: p.name, instance: p }; - notify('register', { type: 'embedding_provider', data: { name: p?.name } }); - }, - registerSpeechProvider: (p) => { - if (p) registeredProviders['speech'] = { name: p.name, instance: p }; - notify('register', { type: 'speech_provider', data: { name: p?.name } }); - }, - registerRealtimeTranscriptionProvider: (p) => { - if (p) registeredProviders['realtime_transcription'] = { name: p.name, instance: p }; - notify('register', { type: 'realtime_transcription_provider', data: { name: p?.name } }); - }, - registerRealtimeVoiceProvider: (p) => { - if (p) registeredProviders['realtime_voice'] = { name: p.name, instance: p }; - notify('register', { type: 'realtime_voice_provider', data: { name: p?.name } }); - }, - registerMediaUnderstandingProvider: (p) => { - if (p) registeredProviders['media_understanding'] = { name: p.name, instance: p }; - notify('register', { type: 'media_understanding_provider', data: { name: p?.name } }); - }, - registerImageGenerationProvider: (p) => { - if (p) registeredProviders['image_generation'] = { name: p.name, instance: p }; - notify('register', { type: 'image_generation_provider', data: { name: p?.name } }); - }, - registerMusicGenerationProvider: (p) => { - if (p) registeredProviders['music_generation'] = { name: p.name, instance: p }; - notify('register', { type: 'music_generation_provider', data: { name: p?.name } }); - }, - registerVideoGenerationProvider: (p) => { - if (p) registeredProviders['video_generation'] = { name: p.name, instance: p }; - notify('register', { type: 'video_generation_provider', data: { name: p?.name } }); - }, - registerWebFetchProvider: (p) => { - if (p) registeredProviders['web_fetch'] = { name: p.name, instance: p }; - notify('register', { type: 'web_fetch_provider', data: { name: p?.name } }); - }, - registerWebSearchProvider: (p) => { - if (p) registeredProviders['web_search'] = { name: p.name, instance: p }; - notify('register', { type: 'web_search_provider', data: { name: p?.name } }); - }, - registerMemoryEmbeddingProvider: (p) => { - if (p) registeredProviders['memory_embedding'] = { name: p.name, instance: p }; - notify('register', { type: 'memory_embedding_provider', data: { name: p?.name } }); - }, - - // ---- Channel 注册 ---- - registerChannel: (ch) => { - 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 / 生命周期 ---- - registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }), - registerRuntimeLifecycle: (lc) => notify('register', { type: 'runtime_lifecycle', data: { name: lc.name } }), - - // ---- HTTP 路由 ---- - registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }), - - // ---- CLI 命令 ---- - registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }), - registerCli: (cli) => notify('register', { type: 'cli', data: { name: cli.name } }), - registerCliBackend: (cb) => notify('register', { type: 'cli_backend', data: { name: cb.name } }), - registerNodeCliFeature: (f) => notify('register', { type: 'node_cli_feature', data: { name: f.name } }), - - // ---- Service ---- - registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }), - - // ---- Agent 相关 ---- - registerAgentHarness: (h) => notify('register', { type: 'agent_harness', data: { name: h.name } }), - registerAgentToolResultMiddleware: (m) => notify('register', { type: 'agent_tool_result_middleware', data: {} }), - registerInteractiveHandler: (h) => notify('register', { type: 'interactive_handler', data: { name: h.name } }), - - // ---- Gateway ---- - registerGatewayMethod: (gm) => notify('register', { type: 'gateway_method', data: { name: gm.name } }), - registerGatewayDiscoveryService: (gs) => notify('register', { type: 'gateway_discovery_service', data: { name: gs.name } }), - - // ---- Trust & Metadata ---- - registerTrustedToolPolicy: (p) => notify('register', { type: 'trusted_tool_policy', data: { name: p.name } }), - registerToolMetadata: (m) => notify('register', { type: 'tool_metadata', data: { name: m.name } }), - - // ---- Context Engine ---- - registerContextEngine: (ce) => notify('register', { type: 'context_engine', data: { name: ce.name } }), - - // ---- Memory 子系统 ---- - registerMemoryCapability: (mc) => notify('register', { type: 'memory_capability', data: { name: mc.name } }), - registerMemoryPromptSection: (ps) => notify('register', { type: 'memory_prompt_section', data: { name: ps.name } }), - registerMemoryFlushPlan: (fp) => notify('register', { type: 'memory_flush_plan', data: { name: fp.name } }), - registerMemoryRuntime: (mr) => notify('register', { type: 'memory_runtime', data: { name: mr.name } }), - registerMemoryPromptSupplement: (ps) => notify('register', { type: 'memory_prompt_supplement', data: { name: ps.name } }), - registerMemoryCorpusSupplement: (cs) => notify('register', { type: 'memory_corpus_supplement', data: { name: cs.name } }), - - // ---- 会话相关 ---- - on: (event, handler) => notify('register', { type: 'session_event', data: { event } }), - onConversationBindingResolved: (handler) => notify('register', { type: 'conversation_binding_resolved', data: {} }), - - session: { - state: { registerSessionExtension: (se) => notify('register', { type: 'session_extension', data: { name: se.name } }) }, - workflow: { - enqueueNextTurnInjection: () => {}, - registerSessionSchedulerJob: (job) => notify('register', { type: 'session_scheduler_job', data: { name: job.name } }), - sendSessionAttachment: () => {}, - scheduleSessionTurn: () => {}, - unscheduleSessionTurnsByTag: () => {}, - }, - controls: { - registerControlUiDescriptor: (d) => notify('register', { type: 'control_ui_descriptor', data: { name: d.name } }), - registerSessionAction: (a) => notify('register', { type: 'session_action', data: { name: a.name } }), - }, - }, - - agent: { - events: { - registerAgentEventSubscription: (sub) => notify('register', { type: 'agent_event_subscription', data: { event: sub.event } }), - emitAgentEvent: (event, data) => notify('agent_event', { event, data }), - }, - }, - - lifecycle: { registerRuntimeLifecycle: (lc) => notify('register', { type: 'lifecycle', data: { name: lc.name } }) }, - - runContext: { - setRunContext: () => {}, - getRunContext: () => ({}), - clearRunContext: () => {}, - }, - - runtime: {}, -}; - -// ---- 注册插件 ---- -entry.register(api); - -// ---- 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' } }); - return; - } - - if (method === 'tools/list') { - const tools = registeredTools.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 || {}; - - // 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); - if (!tool) { - sendError(id, -32601, `Tool not found: ${toolName}`); - return; - } - - if (typeof tool.execute !== 'function') { - sendError(id, -32603, `Tool ${toolName} has no execute function`); - return; - } - - try { - const result = await tool.execute('sim-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; - } - - // ---- Provider 调用 ---- - if (method === 'provider/call') { - const { type, action, args } = req.params || {}; - if (!type) { sendError(id, -32602, 'type required'); return; } - - const provider = registeredProviders[type]; - if (!provider) { sendError(id, -32601, `Provider not found: ${type}`); return; } - - const methodName = action || 'execute'; - if (typeof provider.instance[methodName] !== 'function') { - sendError(id, -32603, `Provider ${type} has no method ${methodName}`); - return; - } - - try { - const result = await provider.instance[methodName](args); - if (result && typeof result === 'object' && Array.isArray(result.content)) { - writeJSON({ jsonrpc: '2.0', id, result }); - } else { - const text = typeof result === 'string' ? result : JSON.stringify(result); - writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } }); - } - } catch (e) { - sendError(id, -32603, e.message); - } - return; - } - - sendError(id, -32601, `Method not found: ${method}`); -}); diff --git a/internal/plugins/clawhubadapter/simulator/openclaw_cli.js b/internal/plugins/clawhubadapter/simulator/openclaw_cli.js deleted file mode 100644 index e7d27f6b..00000000 --- a/internal/plugins/clawhubadapter/simulator/openclaw_cli.js +++ /dev/null @@ -1,216 +0,0 @@ -#!/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(`HologramAgent OpenClaw Adapter 1.0.0 (openclaw-compatible) -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 - auth:status Check auth status - config:get Get config value - config:set 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('HologramAgent 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: HologramAgent OpenClaw Adapter is bundled with HologramAgent.'); - console.log('To update, update HologramAgent itself.'); - process.exit(0); -} - -if (cmd === 'config:get') { - const key = process.argv[3]; - if (!key) { process.stderr.write('Usage: openclaw config:get \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 \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 HologramAgent 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); diff --git a/internal/plugins/clawhubadapter/testdata/echoplugin/main.js b/internal/plugins/clawhubadapter/testdata/echoplugin/main.js deleted file mode 100644 index a2b9f7bd..00000000 --- a/internal/plugins/clawhubadapter/testdata/echoplugin/main.js +++ /dev/null @@ -1,106 +0,0 @@ -const readline = require('readline'); - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false -}); - -const tools = [ - { - name: 'echo', - description: 'Echo back the input text', - inputSchema: { - type: 'object', - properties: { - text: { type: 'string', description: 'Text to echo' } - }, - required: ['text'] - } - }, - { - name: 'add', - description: 'Add two numbers', - inputSchema: { - type: 'object', - properties: { - a: { type: 'number', description: 'First number' }, - b: { type: 'number', description: 'Second number' } - }, - required: ['a', 'b'] - } - }, - { - name: 'ping', - description: 'Health check', - inputSchema: { - type: 'object', - properties: {} - } - } -]; - -rl.on('line', (line) => { - let req; - try { - req = JSON.parse(line); - } catch (e) { - sendError(null, -32700, 'Parse error'); - return; - } - - const id = req.id; - const method = req.method; - - if (method === 'ping') { - sendResult(id, { status: 'ok' }); - return; - } - - if (method === 'tools/list') { - sendResult(id, { tools }); - return; - } - - if (method === 'tools/call') { - const params = req.params || {}; - const toolName = params.name; - const args = params.arguments || {}; - - const tool = tools.find(t => t.name === toolName); - if (!tool) { - sendError(id, -32601, `Tool not found: ${toolName}`); - return; - } - - let result; - switch (toolName) { - case 'echo': - result = { content: [{ type: 'text', text: `Echo: ${args.text || ''}` }] }; - break; - case 'add': - const sum = (Number(args.a) || 0) + (Number(args.b) || 0); - result = { content: [{ type: 'text', text: `${sum}` }] }; - break; - default: - sendError(id, -32601, `Not implemented: ${toolName}`); - return; - } - sendResult(id, result); - return; - } - - sendError(id, -32601, `Method not found: ${method}`); -}); - -function sendResult(id, result) { - const resp = { jsonrpc: '2.0', id, result }; - process.stdout.write(JSON.stringify(resp) + '\n'); -} - -function sendError(id, code, message) { - const resp = { jsonrpc: '2.0', id, error: { code, message } }; - process.stdout.write(JSON.stringify(resp) + '\n'); -} - -// Sidecar is ready - no startup message needed diff --git a/internal/plugins/clawhubadapter/testdata/oc-pkg/lib/entry.js b/internal/plugins/clawhubadapter/testdata/oc-pkg/lib/entry.js deleted file mode 100644 index dc7d132b..00000000 --- a/internal/plugins/clawhubadapter/testdata/oc-pkg/lib/entry.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = { - default: { - id: 'oc-pkg', - name: 'OC Package', - version: '2.0.0', - description: 'Test plugin discovered via package.json extensions', - register(api) { - api.registerTool({ - name: 'add', - description: 'Add two numbers', - parameters: { - type: 'object', - properties: { - a: { type: 'number', description: 'First number' }, - b: { type: 'number', description: 'Second number' }, - }, - required: ['a', 'b'], - }, - execute(id, params) { - return String(params.a + params.b); - }, - }); - - api.registerTool({ - name: 'info', - description: 'Return plugin info', - execute(id, params) { - return JSON.stringify({ id: this.id, name: this.name, version: this.version }); - }, - }); - }, - }, -}; diff --git a/internal/plugins/clawhubadapter/testdata/oc-pkg/package.json b/internal/plugins/clawhubadapter/testdata/oc-pkg/package.json deleted file mode 100644 index 4d748d85..00000000 --- a/internal/plugins/clawhubadapter/testdata/oc-pkg/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "oc-pkg", - "version": "2.0.0", - "description": "OC plugin with package.json entry discovery", - "openclaw": { - "extensions": ["lib/entry.js"] - } -} diff --git a/internal/plugins/clawhubadapter/testdata/oc-simple/index.js b/internal/plugins/clawhubadapter/testdata/oc-simple/index.js deleted file mode 100644 index 467bdd92..00000000 --- a/internal/plugins/clawhubadapter/testdata/oc-simple/index.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - default: { - id: 'oc-simple', - name: 'OC Simple', - version: '1.0.0', - description: 'Simple test plugin for OpenClaw simulator', - register(api) { - api.registerTool({ - name: 'greet', - description: 'Greet someone', - parameters: { - type: 'object', - properties: { - name: { type: 'string', description: 'Name to greet' }, - }, - required: ['name'], - }, - execute(id, params) { - return `Hello, ${params.name}!`; - }, - }); - api.registerTool({ - name: 'ping', - description: 'Health check ping', - execute(id, params) { - return 'pong'; - }, - }); - }, - }, -}; diff --git a/internal/plugins/clawhubadapter/testdata/oc-simple/openclaw.plugin.json b/internal/plugins/clawhubadapter/testdata/oc-simple/openclaw.plugin.json deleted file mode 100644 index 6204d3e4..00000000 --- a/internal/plugins/clawhubadapter/testdata/oc-simple/openclaw.plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "oc-simple", - "version": "1.0.0", - "entry": "index.js", - "description": "Simple OpenClaw test plugin" -}