mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
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 修复
295 lines
8.2 KiB
JavaScript
295 lines
8.2 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;
|
||
}
|
||
}
|
||
|
||
// ---- 解析插件入口 ----
|
||
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}`);
|
||
});
|