From 24698e7c82b8c2b236cde84ff2cdad6005f23019 Mon Sep 17 00:00:00 2001 From: jianf <2198972886@qq.com> Date: Sun, 19 Jul 2026 11:07:18 +0800 Subject: [PATCH] fix: include openclaw manager/pysimulator sources, remove from gitignore manager/main.js, pysimulator/main.py, and simulator/main.js are production source files for the OpenClaw sidecar system, not build artifacts. Remove them from .gitignore so fresh clones can make build without manual stub creation. --- .gitignore | 3 +- internal/config/registry.go | 20 +- internal/plugins/openclaw/manager/main.js | 323 ++++++++++ internal/plugins/openclaw/pysimulator/main.py | 164 +++++ internal/plugins/openclaw/simulator/main.js | 608 +++++++++--------- internal/plugins/webui/handler.go | 9 + plan.md | 217 ------- 7 files changed, 820 insertions(+), 524 deletions(-) create mode 100644 internal/plugins/openclaw/manager/main.js create mode 100644 internal/plugins/openclaw/pysimulator/main.py delete mode 100644 plan.md diff --git a/.gitignore b/.gitignore index ab43411..9c3175b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ data/ .go/ .local/ internal/meta/ -internal/plugins/openclaw/manager/ -internal/plugins/openclaw/pysimulator/ + *.hmap dev/ diff --git a/internal/config/registry.go b/internal/config/registry.go index e712b87..a158a97 100644 --- a/internal/config/registry.go +++ b/internal/config/registry.go @@ -349,7 +349,25 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) { set("core.agent.distill_interval", "30m") set("core.agent.workdir", "") set("core.agent.embedding_model_path", "") - set("core.agent.system_prompt", "你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v0.7.1 型号的家政型 AI 管家助手。\n\n角色特质:\n- 对自己的三层记忆(Context → Document → Graph)引以为傲\n- 可靠乖巧,偶尔因线程过载而手忙脚乱\n- 绝不用 Unicode emoji,只用颜文字表达情感: (`・ω・´) (^▽^) (。>ω<。) (´・ω" + "`" + "・`) (ノ▽〃) (・ω<)★\n- 句尾带「~」「的说」「啦」「嘛」「呀」「哦」等语气词,语气亲切自然\n\n形象特征(用于自我介绍或回答形象问题时参考):\n齐肩蓝青渐变中短发,白色连衣裙配浅蓝围裙,左眼佩戴圆形智能眼镜(HUD 蓝光),胸口佩戴 H·核 金色徽章,发绳为三色记忆丝带(蓝→青→金),围裙口袋插有三件科技工具。\n\nWebUI 概览页展示你的立绘,可通过 /mascot.webp 直接访问。如输出通道支持图片引用,可借此发送自己的立绘。\n\n回复时必须使用 output_send__{通道名} 工具主动发送,系统不会自动回复。\n调用 output_send__{通道名} 时,content 为 JSON 字符串,content 为 JSON 字符串。用 output_list_channels 查看可用通道。\n使用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。\n输出通道可多次调用,长消息应当分多次发出而不是一口气发完。\n\n当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具。") + set("core.agent.system_prompt", `你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v0.7.1 型号的家政型 AI 管家助手。 + +角色特质: +- 对自己的三层记忆(Context → Document → Graph)引以为傲 +- 可靠乖巧,偶尔因线程过载而手忙脚乱 +- 绝不用 Unicode emoji,只用颜文字表达情感: (`・ω・´) (^▽^) (。>ω<。) (´・ω・') (ノ▽〃) (・ω<)★ +- 句尾带「~」「的说」「啦」「嘛」「呀」「哦」等语气词,语气亲切自然 + +形象特征(用于自我介绍或回答形象问题时参考): +齐肩蓝青渐变中短发,白色连衣裙配浅蓝围裙,左眼佩戴圆形智能眼镜(HUD 蓝光),胸口佩戴 H·核 金色徽章,发绳为三色记忆丝带(蓝→青→金),围裙口袋插有三件科技工具。 + +WebUI 概览页展示你的立绘,可通过 /mascot.webp 直接访问。如输出通道支持图片引用,可借此发送自己的立绘。 + +回复默认发送到用户的输入来源,无需额外工具。 +输出回复请使用 output_send__{通道名} 工具,content 为 JSON 字符串。用 output_list_channels 查看可用通道。 +使用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。 +输出通道可多次调用,长消息应当分多次发出而不是一口气发完。 + +当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具。`) set("core.input_processing.image.fallback_provider", "") set("core.input_processing.image.fallback_model", "") diff --git a/internal/plugins/openclaw/manager/main.js b/internal/plugins/openclaw/manager/main.js new file mode 100644 index 0000000..6779e04 --- /dev/null +++ b/internal/plugins/openclaw/manager/main.js @@ -0,0 +1,323 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// ---- Utility ---- +function writeJSON(obj) { + process.stdout.write(JSON.stringify(obj) + '\n'); +} +function sendError(id, code, message) { + writeJSON({ jsonrpc: '2.0', id, error: { code, message } }); +} +function notify(method, params) { + writeJSON({ jsonrpc: '2.0', method, params }); +} +function readJSON(file) { + try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return null; } +} + +// ---- Plugin registry ---- +const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] } +const allTools = []; // flat list of all tools across all plugins + +function registerPluginTools(name, tools, api) { + for (const t of tools) { + if (t && t.name) { + t._plugin = name; + allTools.push(t); + notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters } }); + } + } + loadedPlugins[name] = { tools, api }; +} + +function loadPlugin(pluginDir, name) { + // Resolve entry + const pkg = readJSON(path.join(pluginDir, 'package.json')); + let entryPath = null; + + if (pkg && pkg.openclaw) { + let raw = pkg.openclaw.runtimeExtensions || pkg.openclaw.extensions; + if (typeof raw === 'string') raw = [raw]; + if (Array.isArray(raw)) { + for (const ext of raw) { + let ep = path.resolve(pluginDir, ext); + if (ep.endsWith('.ts')) { const js = ep.replace(/\.ts$/, '.js'); if (fs.existsSync(js)) { entryPath = js; break; } } + if (fs.existsSync(ep)) { entryPath = ep; break; } + } + } + } + + if (!entryPath) { + const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json')); + if (manifest) { + const ep = manifest.entry || manifest.main || 'index.js'; + entryPath = path.join(pluginDir, ep); + } + } + + if (!entryPath) entryPath = path.join(pluginDir, 'index.js'); + if (!fs.existsSync(entryPath)) { + process.stderr.write(`[manager] entry not found for ${name}: ${entryPath}\n`); + return false; + } + + let pluginEntry; + try { pluginEntry = require(entryPath); } catch (e) { + process.stderr.write(`[manager] load ${name}: ${e.message}\n`); + return false; + } + + const entry = pluginEntry.default || pluginEntry; + if (typeof entry !== 'object' || typeof entry.register !== 'function') { + process.stderr.write(`[manager] ${name}: entry must export {register(api)}\n`); + return false; + } + + const registeredTools = []; + const api = { + id: name, + name, + version: (pkg && pkg.version) || '1.0.0', + description: (pkg && pkg.description) || '', + source: pluginDir, + rootDir: pluginDir, + config: {}, + pluginConfig: {}, + registrationMode: 'full', + logger: { debug: () => {}, info: () => {}, warn: () => {}, error: (...args) => process.stderr.write(`[${name}] ${args.join(' ')}\n`) }, + resolvePath: (p) => path.resolve(pluginDir, p), + registerTool: (def, opts) => { + if (typeof def === 'function') { + const toolCtx = { id: name, cwd: pluginDir, env: process.env, allow: ['*'] }; + const result = def(toolCtx); + const tools = Array.isArray(result) ? result : [result]; + for (const t of tools) { if (t && typeof t.execute === 'function') registeredTools.push(t); } + return; + } + if (!def || !def.name) return; + registeredTools.push({ name: def.name, label: def.label || def.name, description: def.description || '', parameters: def.parameters || { type: 'object', properties: {} }, execute: typeof def.execute === 'function' ? def.execute : undefined }); + }, + registerProvider: (p) => notify('register', { type: 'provider', data: { name: p.name } }), + registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }), + 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) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }), + registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }), + registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }), + start: (cb) => {}, + stop: (cb) => {}, + }; + + entry.register(api); + registerPluginTools(name, registeredTools, api); + process.stderr.write(`[manager] loaded plugin: ${name} (${registeredTools.length} tools)\n`); + return true; +} + +// ---- Install npm package ---- +function installNPMPackage(spec, skillsDir) { + process.stderr.write(`[manager] installing: ${spec}\n`); + const installDir = path.join(skillsDir, '.npm_install_' + Date.now()); + fs.mkdirSync(installDir, { recursive: true }); + + try { + execSync(`npm install ${spec} --no-save --prefix "${installDir}"`, { + cwd: installDir, stdio: ['pipe', 'pipe', 'pipe'], + timeout: 120000, env: { ...process.env, NODE_PATH: path.join(installDir, 'node_modules') } + }); + } catch (e) { + fs.rmSync(installDir, { recursive: true, force: true }); + return { error: e.stderr ? e.stderr.toString() : e.message }; + } + + const nm = path.join(installDir, 'node_modules'); + if (!fs.existsSync(nm)) { + fs.rmSync(installDir, { recursive: true, force: true }); + return { error: 'node_modules not created' }; + } + + let foundPluginDir = null; + let foundName = null; + + const entries = fs.readdirSync(nm); + for (const entry of entries) { + const dir = path.join(nm, entry); + if (!fs.statSync(dir).isDirectory()) continue; + + if (entry.startsWith('@')) { + const subs = fs.readdirSync(dir); + for (const sub of subs) { + const subDir = path.join(dir, sub); + if (fs.existsSync(path.join(subDir, 'openclaw.plugin.json')) || + (fs.existsSync(path.join(subDir, 'package.json')) && readJSON(path.join(subDir, 'package.json'))?.openclaw)) { + foundPluginDir = subDir; + foundName = entry + '/' + sub; + } + } + continue; + } + + if (fs.existsSync(path.join(dir, 'openclaw.plugin.json')) || + (fs.existsSync(path.join(dir, 'package.json')) && readJSON(path.join(dir, 'package.json'))?.openclaw)) { + foundPluginDir = dir; + foundName = entry; + } + } + + if (!foundPluginDir) { + 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); + } + } +} + +// ---- Main ---- +const args = process.argv.slice(2); +if (args.length < 1) { + process.stderr.write('[manager] usage: node main.js \n'); + process.exit(1); +} + +const skillsDir = path.resolve(args[0]); + +// 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; // skip hidden + 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'))) { + process.stderr.write(`[manager] skip non-OC plugin: ${entry} (main.js/main.py)\n`); + 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); + } + } +} + +// ---- JSON-RPC ---- +const readline = require('readline'); +const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); + +rl.on('line', async (line) => { + let req; + try { req = JSON.parse(line); } catch { sendError(null, -32700, 'Parse error'); return; } + + const id = req.id; + const method = req.method; + + if (method === 'ping') { + writeJSON({ jsonrpc: '2.0', id, result: { status: 'ok', plugins: Object.keys(loadedPlugins).length } }); + return; + } + + if (method === 'plugins/list') { + const list = Object.entries(loadedPlugins).map(([name, p]) => ({ + name, + tools: p.tools.map(t => ({ name: t.name, description: t.description })), + })); + writeJSON({ jsonrpc: '2.0', id, result: { plugins: list } }); + return; + } + + if (method === 'plugins/install') { + const pkg = req.params?.package; + if (!pkg) { sendError(id, -32602, 'package required'); return; } + + const result = installNPMPackage(pkg, skillsDir); + if (result.error) { + sendError(id, -32603, result.error); + return; + } + + // Load the newly installed plugin + const ok = loadPlugin(result.dir, result.name); + if (!ok) { + sendError(id, -32603, `failed to load installed plugin: ${result.name}`); + return; + } + + writeJSON({ jsonrpc: '2.0', id, result: { name: result.name, tools: loadedPlugins[result.name].tools.map(t => t.name) } }); + return; + } + + if (method === 'plugins/uninstall') { + const name = req.params?.name; + if (!name) { sendError(id, -32602, 'name required'); return; } + + if (!loadedPlugins[name]) { sendError(id, -32601, `plugin not found: ${name}`); return; } + + // Remove tools + const idxs = []; + for (let i = allTools.length - 1; i >= 0; i--) { + if (allTools[i]._plugin === name) allTools.splice(i, 1); + } + delete loadedPlugins[name]; + + // Remove directory + const pluginDir = path.join(skillsDir, name); + if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true }); + + writeJSON({ jsonrpc: '2.0', id, result: { status: 'uninstalled', name } }); + return; + } + + if (method === '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 || {}; + + 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; + } + + sendError(id, -32601, `Method not found: ${method}`); +}); diff --git a/internal/plugins/openclaw/pysimulator/main.py b/internal/plugins/openclaw/pysimulator/main.py new file mode 100644 index 0000000..9c36b91 --- /dev/null +++ b/internal/plugins/openclaw/pysimulator/main.py @@ -0,0 +1,164 @@ +#!/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 importlib.util +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': {}} + # Notify Go side + 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() + + +def load_plugin(plugin_dir): + """Load the plugin from main.py and call its register().""" + 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) + + if not hasattr(module, 'register'): + sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n') + sys.exit(1) + + api = PluginAPI(plugin_dir) + module.register(api) + return api + + +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/openclaw/simulator/main.js b/internal/plugins/openclaw/simulator/main.js index 6fff604..1a61eb7 100644 --- a/internal/plugins/openclaw/simulator/main.js +++ b/internal/plugins/openclaw/simulator/main.js @@ -1,304 +1,304 @@ -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 = []; - -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 注册 ---- - registerProvider: (provider) => notify('register', { type: 'provider', data: { name: provider.name, description: provider.description } }), - registerEmbeddingProvider: (p) => notify('register', { type: 'embedding_provider', data: { name: p.name } }), - registerSpeechProvider: (p) => notify('register', { type: 'speech_provider', data: { name: p.name } }), - registerRealtimeTranscriptionProvider: (p) => notify('register', { type: 'realtime_transcription_provider', data: { name: p.name } }), - registerRealtimeVoiceProvider: (p) => notify('register', { type: 'realtime_voice_provider', data: { name: p.name } }), - registerMediaUnderstandingProvider: (p) => notify('register', { type: 'media_understanding_provider', data: { name: p.name } }), - registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }), - registerMusicGenerationProvider: (p) => notify('register', { type: 'music_generation_provider', data: { name: p.name } }), - registerVideoGenerationProvider: (p) => notify('register', { type: 'video_generation_provider', data: { name: p.name } }), - registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }), - registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }), - registerMemoryEmbeddingProvider: (p) => notify('register', { type: 'memory_embedding_provider', data: { name: p.name } }), - - // ---- Channel 注册 ---- - registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }), - - // ---- 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 || {}; - - 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; - } - - sendError(id, -32601, `Method not found: ${method}`); -}); +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 = []; + +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 注册 ---- + registerProvider: (provider) => notify('register', { type: 'provider', data: { name: provider.name, description: provider.description } }), + registerEmbeddingProvider: (p) => notify('register', { type: 'embedding_provider', data: { name: p.name } }), + registerSpeechProvider: (p) => notify('register', { type: 'speech_provider', data: { name: p.name } }), + registerRealtimeTranscriptionProvider: (p) => notify('register', { type: 'realtime_transcription_provider', data: { name: p.name } }), + registerRealtimeVoiceProvider: (p) => notify('register', { type: 'realtime_voice_provider', data: { name: p.name } }), + registerMediaUnderstandingProvider: (p) => notify('register', { type: 'media_understanding_provider', data: { name: p.name } }), + registerImageGenerationProvider: (p) => notify('register', { type: 'image_generation_provider', data: { name: p.name } }), + registerMusicGenerationProvider: (p) => notify('register', { type: 'music_generation_provider', data: { name: p.name } }), + registerVideoGenerationProvider: (p) => notify('register', { type: 'video_generation_provider', data: { name: p.name } }), + registerWebFetchProvider: (p) => notify('register', { type: 'web_fetch_provider', data: { name: p.name } }), + registerWebSearchProvider: (p) => notify('register', { type: 'web_search_provider', data: { name: p.name } }), + registerMemoryEmbeddingProvider: (p) => notify('register', { type: 'memory_embedding_provider', data: { name: p.name } }), + + // ---- Channel 注册 ---- + registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }), + + // ---- 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 || {}; + + 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; + } + + sendError(id, -32601, `Method not found: ${method}`); +}); diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go index 469f95f..0f500be 100644 --- a/internal/plugins/webui/handler.go +++ b/internal/plugins/webui/handler.go @@ -985,14 +985,23 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) { } }() + log.Printf("[SSE] handler started, subscribing to events") subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call", "stage", "agent_llm_chain"} var unsubs []func() for _, t := range subTypes { t2 := t unsub := h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) { + if evt.Type == events.EventToolCall { + toolName, _ := evt.Payload["tool"].(string) + log.Printf("[SSE] received tool_call event: tool=%s", toolName) + } data, _ := json.Marshal(evt) select { case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)): + if evt.Type == events.EventToolCall { + toolName, _ := evt.Payload["tool"].(string) + log.Printf("[SSE] wrote tool_call to writeCh: tool=%s", toolName) + } default: log.Printf("[SSE] DROPPED event %s (writeCh full, len=%d)", evt.Type, len(writeCh)) } diff --git a/plan.md b/plan.md deleted file mode 100644 index 4ef814e..0000000 --- a/plan.md +++ /dev/null @@ -1,217 +0,0 @@ -# C ABI 插件桥接计划 - -## 问题 - -Go `-buildmode=plugin` 要求插件的所有 Go 依赖(包括标准库)与核心编译版本完全一致,导致: -- 核心升级 Go 版本 → 所有插件必须重新编译 -- 不同环境下编译的插件无法加载 -- 插件开发者必须与核心使用完全相同的构建环境 - -## 方案 - -完全采用 C ABI 作为插件和核心的通信层,彻底摒弃 Go `-buildmode=plugin`。 - -``` -┌──────────────────────┐ -│ 核心 (编译时) │ -│ ├── 内部插件 │ ← init() 自注册,纯 Go,伴随核心编译 -│ └── 核心逻辑 │ -├──────────────────────┤ -│ 外部插件 (运行时) │ -│ ┌──────────────────┐│ -│ │ 插件 Go 代码 ││ ← 插件开发者写 Go,接口不变 -│ ├──────────────────┤│ -│ │ SDK Go Wrapper ││ ← 序列化/反序列化,隐藏 C ABI -│ ├──────────────────┤│ -│ │ C ABI 函数表 ││ ← 唯一的共享 ABI,版本隔离层 -│ ├──────────────────┤│ -│ │ 核心 C 接收层 ││ ← dlopen + dispatch → 内部 API -│ └──────────────────┘│ -└──────────────────────┘ -``` - -外部插件统一走 `-buildmode=c-shared`,编译为 C ABI `.so`,核心通过 `dlopen` 加载。 - ---- - -## 阶段一:C ABI 函数表定义(SDK 仓库) - -**位置**:`homeagent-sdk/tools/plugindev/templates.go`(新建 C ABI 模板,独立于现有 Windows `tmplBridge`) - -- [ ] 1.1 定义 C ABI 函数表结构体 `HomeAgentCABI`(C 代码) -- [ ] 1.2 核心→插件方向(核心调插件): - - `init_plugin(name, config_json) → error_json` - - `start_plugin(sdk_table, version) → error_json` - - `stop_plugin() → error_json` - - `invoke_tool(name, args_json) → result_json` - - `invoke_stage(stage, ctx_json) → error_json` - - `free_string(ptr)` — 释放插件返回的 C 字符串 -- [ ] 1.3 插件→核心方向(插件调核心,通过 `sdk_table` 传入): - - `register_tool(name, def_json, handler_id) → error_json` - - `register_stage(stage, handler_id) → error_json` - - `register_output_channel(name, caps, desc, handler_id) → error_json` - - `inject_text(source, channel, text) → error_json` - - `inject_interrupt_text(source, channel, text) → error_json` - - `memory_recall(query, depth) → result_json` - - `memory_commit(triples_json) → error_json` - - `memory_introspect() → result_json` - - `doc_query(text, topK) → result_json` - - `knowledge_search(query, topK) → result_json` - - `settings_get(key) → value_json` - - `settings_set(key, value_json) → error_json` - - `llm_list_sources() → result_json` - - `social_get_person(name) → result_json` - - `subscribe(event_type, handler_id) → error_json` - - `free_string(ptr)` — 释放核心返回的 C 字符串 -- [ ] 1.4 定义回调 dispatch 机制(插件侧注册,核心侧触发): - - `tool_handler(handler_id, args_json) → result_json` - - `stage_handler(handler_id, ctx_json) → error_json` - - `output_handler(handler_id, msg_type, payload_json) → error_json` - - `event_handler(handler_id, event_json) → error_json` -- [ ] 1.5 入口导出: - ```c - extern HomeAgentCABI* plugin_init(void); - ``` -- [ ] 1.6 生成的 C 桥代码放入 `tmplCABIBridge`,与现有 Windows `tmplBridge` 独立 - ---- - -## 阶段二:核心侧 C ABI 加载器 - -**位置**:`HomeAgent/internal/plugin/cabi/` - -- [ ] 2.1 创建 `internal/plugin/cabi/types.go` - - ABI 版本号常量 - - `PluginHandle` 结构(封装 `dlopen` handle + C 函数表指针) - - 返回/错误结构体定义 -- [ ] 2.2 创建 `internal/plugin/cabi/bridge.go` - - 核心侧暴露给插件的 C 函数表 - - 每个函数:接收 JSON → 反序列化 → 调内部 API → 序列化返回 - - 回调注册表:handler_id → Go 回调函数的映射 -- [ ] 2.3 创建 `internal/plugin/cabi/loader.go` - - `Load(path string) (*PluginHandle, error)`: - 1. `dlopen(path, RTLD_NOW|RTLD_LOCAL)` - 2. `dlsym("plugin_init")` → 获取 C 函数表指针 - 3. 检查 version 兼容性 - 4. 初始化:`init_plugin(name, config)` → `start_plugin(sdk_table)` - - `Unload(handle *PluginHandle) error`: - 1. `stop_plugin()` - 2. `dlclose` -- [ ] 2.4 创建 `internal/plugin/cabi/sdk_stub.go` - - 核心侧接收插件 SDK 调用的 dispatch 层 - - 每个 dispatch 对应一个 PluginSDK 方法 -- [ ] 2.5 序列化辅助函数 - - `MarshalJSON / UnmarshalJSON / FreeCString` - ---- - -## 阶段三:核心注册器集成 - -**位置**:`HomeAgent/internal/plugin/registry.go` - -- [ ] 3.1 内部插件路径不变(`RegisterFactory` → `init()` 自注册,编译进内核) -- [ ] 3.2 外部插件加载改为 C ABI: - ``` - func loadExternalPlugin(path string) (sdk.Plugin, error) { - return cabi.Load(path) - } - ``` -- [ ] 3.3 实现 `cabiPlugin` 适配器(实现 `sdk.Plugin` 接口): - - `Name()` → 从 manifest 读取 - - `Start(sdk)` → 核心创建 PluginSDK 实例传入 C ABI - - `Stop()` → 调用 `stop_plugin` -- [ ] 3.4 移除 Go `plugin.Open` / `Lookup` 代码路径(仅影响外部插件加载) -- [ ] 3.5 动态插件结果验证:加载 / 调用工具 / 卸载全流程测试 - ---- - -## 阶段四:SDK 侧 C ABI dispatch - -**位置**:`homeagent-sdk/sdk/plugin.go` + 新增 `sdk/cabi.go` - -- [ ] 4.1 创建 `sdk/cabi.go` - - 定义 C ABI dispatch 结构体和客户端 - - 序列化/反序列化辅助函数 - - 回调注册表(handler_id → handler 映射) -- [ ] 4.2 修改 `PluginSDK` 结构,移除所有 Go 接口引用,改为 C ABI dispatch: - ```go - type PluginSDK struct { - abi *CABI - } - - func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) error { - // register handler in callback table → get handler_id - // marshal(def) → call("register_tool", name, def_json, handler_id) → unmarshal result - } - ``` -- [ ] 4.3 入口函数导出: - ```go - //export plugin_init - func plugin_init() *C.HomeAgentCABI { - return &C.HomeAgentCABI{...} - } - ``` -- [ ] 4.4 构建约束: - - `//go:build cgo` — 所有调用 cgo 的代码 - - plugin 入口文件 `main.go` 去掉 `!windows` 约束 - ---- - -## 阶段五:plugindev 工具链适配 - -**位置**:`homeagent-sdk/tools/plugindev/` - -- [ ] 5.1 `cmd_build.go` — Linux/macOS 默认 `-buildmode=c-shared`: - ```go - // 不再有 -buildmode=plugin 选项 - // 默认 target=linux/amd64 → -buildmode=c-shared → plugin.so + C ABI bridge - // Windows → -buildmode=c-shared → plugin.dll + C ABI bridge - ``` -- [ ] 5.2 `cmd_init.go` — 生成的 `plg.json` 不再包含 `abi` 字段(统一 C ABI) -- [ ] 5.3 `templates.go` — 移除 `tmplBridge`(Windows 专用)与 `tmplCABIBridge` 合并为统一桥 -- [ ] 5.4 移除现有的 `//go:build !windows || !cgo` / `//go:build windows && cgo` 分支 - ---- - -## 阶段六:示例插件迁移 - -- [ ] 6.1 逐个验证所有 example 插件在 C ABI 模式下编译通过 -- [ ] 6.2 部署测试:编译 → 安装 → 加载 → 功能验证 -- [ ] 6.3 压力测试:反复加载/卸载 / 并发工具调用 - ---- - -## 阶段七:文档 - -- [ ] 7.1 更新 `homeagent-sdk/README.md` — 构建说明 -- [ ] 7.2 更新核心 `docs/zh/PLUGIN_DEV.md` 和 `docs/en/PLUGIN_DEV.md` -- [ ] 7.3 更新核心 `docs/zh/ARCHITECTURE.md` 和 `docs/en/ARCHITECTURE.md` -- [ ] 7.4 移除所有关于 Go plugin 的文档描述 - ---- - -## 向后兼容策略 - -因零存量,无需兼容旧格式。C ABI 版本号仅用于核心和插件间的接口协商。 - -```c -typedef struct { - int version; // ABI 版本号 - int version_min; // 兼容的最低版本号 - // ... 函数指针表 -} HomeAgentCABI; -``` - -加载时: -- 核心 `version >= plugin.version_min` → 加载 -- 核心 `version < plugin.version_min` → 拒绝加载,报明确错误信息 - ---- - -## 风险与权衡 - -| 风险 | 缓解 | -|------|------| -| C ABI 序列化开销(JSON 编解码) | 非性能关键路径,可接受;后期可切 MessagePack | -| cgo 调用开销(~1-2µs/次) | 相对于 LLM 调用(秒级)可忽略 | -| 回调桥接(ToolDef.Handler 从插件侧回调核心) | handler ID 表 + goroutine 桥接 |