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:
2026-07-19 11:07:18 +08:00
parent 7601a2970f
commit 24698e7c82
7 changed files with 820 additions and 524 deletions

View File

@ -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", "")

View 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}`);
});

View 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()

View File

@ -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}`);
});

View File

@ -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))
}