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 修复
This commit is contained in:
root
2026-07-04 12:51:32 +08:00
parent 77e12f7329
commit fab58e709a
30 changed files with 4956 additions and 578 deletions

View File

@ -0,0 +1,294 @@
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}`);
});