mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-30 06:22:53 +00:00
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.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,7 +11,6 @@ data/
|
||||
.go/
|
||||
.local/
|
||||
internal/meta/
|
||||
internal/plugins/openclaw/manager/
|
||||
internal/plugins/openclaw/pysimulator/
|
||||
|
||||
*.hmap
|
||||
dev/
|
||||
|
||||
@ -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", "")
|
||||
|
||||
323
internal/plugins/openclaw/manager/main.js
Normal file
323
internal/plugins/openclaw/manager/main.js
Normal file
@ -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 <skills-dir>\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}`);
|
||||
});
|
||||
164
internal/plugins/openclaw/pysimulator/main.py
Normal file
164
internal/plugins/openclaw/pysimulator/main.py
Normal file
@ -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 <plugin-dir>\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()
|
||||
@ -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 <plugin-dir>\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 <plugin-dir>\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}`);
|
||||
});
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
217
plan.md
217
plan.md
@ -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 桥接 |
|
||||
Reference in New Issue
Block a user