Files
HomeAgent/internal/plugins/openclaw/simulator/main.js
root fab58e709a feat: complete P0/P1/P2 — WebUI SPA, OpenClaw sidecar+simulator, healthcheck auto-sched+perf
P0: WebUI重构
- 完整 SPA 仪表盘 (7标签页), //go:embed dashboard.html
P1: OpenClaw兼容 (三通道: SKILL.md / sidecar / simulator)
- Node.js 模拟进程统一加载任意 OpenClaw 插件
- JSON-RPC 2.0 over stdio 协议, go:embed 内嵌
P2: Healthcheck 优化
- 定时自动执行 (startAutoCheck, 30min)
- healthcheck_perf 性能监控工具

其他: agentcli/cmd 插件, integration_test, status.go,
      test_deepseek 清理, 多项 bug 修复
2026-07-04 12:51:32 +08:00

295 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
// ---- 解析插件入口 ----
const pluginDir = path.resolve(process.argv[2]);
if (!pluginDir) {
process.stderr.write('[simulator] usage: node main.js <plugin-dir>\n');
process.exit(1);
}
// 1. 先读 package.json 找 extensions
const pkgPath = path.join(pluginDir, 'package.json');
const pkg = readJSON(pkgPath);
let entryPath = null;
if (pkg && pkg.openclaw) {
// runtimeExtensions > extensions (安装包首选编译后的 JS)
let raw = pkg.openclaw.runtimeExtensions || pkg.openclaw.extensions;
if (typeof raw === 'string') raw = [raw];
if (Array.isArray(raw) && raw.length > 0) {
// 优先选已编译的 JS 入口: .ts 映射到 .js, .js 直接用
for (const ext of raw) {
let ep = path.resolve(pluginDir, ext);
// .ts → 同级 .js
if (ep.endsWith('.ts')) {
const jsEp = ep.replace(/\.ts$/, '.js');
if (fs.existsSync(jsEp)) { entryPath = jsEp; break; }
}
if (fs.existsSync(ep)) { entryPath = ep; break; }
}
}
}
// 2. 回退: openclaw.plugin.json 的 entry/main
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);
}
}
// 3. 最后尝试 index.js
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);
}
// ---- 构造完整的 PluginApi 模拟 ----
const registeredTools = [];
// registerTool 支持两种签名:
// api.registerTool(toolDef, opts?) — 对象形式
// api.registerTool(factory, opts?) — 工厂函数形式
function registerTool(defOrFactory, opts) {
if (typeof defOrFactory === 'function') {
// 工厂形式: 传入 toolContext, 返回工具对象或数组
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);
}
}
return;
}
// 对象形式
const def = defOrFactory;
if (!def || !def.name) return;
// definePluginEntry 的 register 传给 api.registerTool 时是完整工具定义
// defineToolPlugin 包装后传给 api.registerTool 的也是完整工具定义
// 关键是工具必须要有 execute 函数(或 factory 在之前展开)
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,
});
}
// 完整的 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,
// 以下 api 方法留为 no-op保证真实插件调用时不崩溃
registerProvider: () => {},
registerChannel: () => {},
registerEmbeddingProvider: () => {},
registerSpeechProvider: () => {},
registerRealtimeTranscriptionProvider: () => {},
registerRealtimeVoiceProvider: () => {},
registerMediaUnderstandingProvider: () => {},
registerImageGenerationProvider: () => {},
registerMusicGenerationProvider: () => {},
registerVideoGenerationProvider: () => {},
registerWebFetchProvider: () => {},
registerWebSearchProvider: () => {},
registerMemoryEmbeddingProvider: () => {},
registerAgentHarness: () => {},
registerCliBackend: () => {},
registerHook: () => {},
registerHttpRoute: () => {},
registerGatewayMethod: () => {},
registerGatewayDiscoveryService: () => {},
registerCli: () => {},
registerNodeCliFeature: () => {},
registerService: () => {},
registerCommand: () => {},
registerInteractiveHandler: () => {},
registerAgentToolResultMiddleware: () => {},
registerTrustedToolPolicy: () => {},
registerToolMetadata: () => {},
registerContextEngine: () => {},
registerMemoryCapability: () => {},
registerMemoryPromptSection: () => {},
registerMemoryFlushPlan: () => {},
registerMemoryRuntime: () => {},
registerMemoryPromptSupplement: () => {},
registerMemoryCorpusSupplement: () => {},
// 会话相关
on: () => {},
onConversationBindingResolved: () => {},
session: {
state: { registerSessionExtension: () => {} },
workflow: {
enqueueNextTurnInjection: () => {},
registerSessionSchedulerJob: () => {},
sendSessionAttachment: () => {},
scheduleSessionTurn: () => {},
unscheduleSessionTurnsByTag: () => {},
},
controls: {
registerControlUiDescriptor: () => {},
registerSessionAction: () => {},
},
},
agent: {
events: {
registerAgentEventSubscription: () => {},
emitAgentEvent: () => {},
},
},
lifecycle: { registerRuntimeLifecycle: () => {} },
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 {
// OpenClaw 工具 execute 签名: (toolCallId, params, signal, onUpdate) => AgentToolResult
const result = await tool.execute('sim-call-1', args, undefined, undefined);
// 如果返回已经是 AgentToolResult 格式,直接转发
if (result && typeof result === 'object' && Array.isArray(result.content)) {
writeJSON({ jsonrpc: '2.0', id, result });
} else {
// 否则包装为 text result
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}`);
});