mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 07:43:58 +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:
@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user