mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
- simulator/main.js: All 30+ register* methods now send JSON-RPC notifications to Go instead of being silent no-ops. Each plugin capability registration (tool, provider, channel, hook, http_route, command, service, etc.) is forwarded to Go via the notify channel. - sidecar.go: Refactored to async reader goroutine. Single goroutine reads all stdout lines, routes responses to pending callers via channel by ID, and dispatches notifications (no-ID messages) to notifyCh for handling. - plugin.go: drainNotify() collects all capabilities registered during plugin init. handleNotify() logs each registered capability for visibility. - All 9 openclaw tests pass including simulator + full pipeline tests.
305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
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}`);
|
|
});
|