From 1163115dd25d0e80a2a3566f9332266e9f27a41c Mon Sep 17 00:00:00 2001 From: Miaotouy <3133963945@qq.com> Date: Wed, 25 Mar 2026 20:17:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugin):=20=E6=96=B0=E5=A2=9E=20VCP=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A1=A5=E6=8E=A5=E5=99=A8=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=90=91=E5=A4=96=E9=83=A8=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=AF=BC=E5=87=BA=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为支持 VCP 原生工具向外部系统(如 AIO Hub)导出,新增了一个桥接器插件。该插件通过拦截 WebSocket 消息,实现了工具清单的同步与远程执行能力,避免了工具循环调用问题。 - 新增 VCPToolBridge 插件,包含完整的配置、实现与清单文件 - 通过 Monkey Patch 劫持 WebSocket 消息,拦截 `get_vcp_manifests` 和 `execute_vcp_tool` 请求 - 提供可配置的黑名单与关键词过滤,排除日志、信息提供器及外部同步工具 - 修改 WebSocketServer.js,导出 `handleDistributedServerMessage` 函数供插件调用 --- Plugin/VCPToolBridge/config.env.example | 13 ++ Plugin/VCPToolBridge/index.js | 210 ++++++++++++++++++++++ Plugin/VCPToolBridge/plugin-manifest.json | 42 +++++ WebSocketServer.js | 4 +- 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 Plugin/VCPToolBridge/config.env.example create mode 100644 Plugin/VCPToolBridge/index.js create mode 100644 Plugin/VCPToolBridge/plugin-manifest.json diff --git a/Plugin/VCPToolBridge/config.env.example b/Plugin/VCPToolBridge/config.env.example new file mode 100644 index 000000000..083652090 --- /dev/null +++ b/Plugin/VCPToolBridge/config.env.example @@ -0,0 +1,13 @@ +# VCP 工具桥接器配置模板 + +# 是否启用桥接功能 (true/false) +Bridge_Enabled=false + +# 不导出的工具黑名单(调用名,逗号分隔) +# 默认排除日志、信息提供器和桥接器自身 +Excluded_Tools=VCPLog,VCPInfo,VCPToolBridge + +# 不导出的显示名称关键词(逗号分隔,包含即排除) +# 用于排除 AIO 等外部同步过来的工具,避免循环 +# 如果包含特殊字符,建议使用引号包裹,例如 "[AIO]" +Excluded_Display_Keywords="[AIO]" \ No newline at end of file diff --git a/Plugin/VCPToolBridge/index.js b/Plugin/VCPToolBridge/index.js new file mode 100644 index 000000000..2786f0712 --- /dev/null +++ b/Plugin/VCPToolBridge/index.js @@ -0,0 +1,210 @@ +// Plugin/VCPToolBridge/index.js +const path = require('path'); + +class VCPToolBridge { + constructor() { + this.pluginManager = null; + this.wss = null; + this.config = {}; + this.debugMode = false; + this.isHooked = false; + } + + /** + * 初始化插件,接收 PluginManager 注入 + */ + async initialize(config, dependencies) { + this.config = config; + this.debugMode = config.DebugMode === true; + // 注意:在 VCP 的 initialize 流程中,dependencies 可能包含 vcpLogFunctions 等 + this.log = dependencies.vcpLogFunctions || { pushVcpLog: () => { }, pushVcpInfo: () => { } }; + + if (this.debugMode) console.log('[VCPToolBridge] Initialized.'); + } + + /** + * 注册 API 路由,这是拿到 WebSocketServer 实例的最佳时机 + */ + registerApiRoutes(router, config, projectBasePath, wss) { + this.wss = wss; + this.config = { ...this.config, ...config }; // 合并配置 + + if (!this.wss) { + console.error('[VCPToolBridge] WebSocketServer instance is missing in registerApiRoutes.'); + return; + } + + // 核心:执行 Monkey Patch + this.applyMonkeyPatch(); + + // 提供一个简单的状态查询接口 + router.get('/status', (req, res) => { + res.json({ + status: 'active', + hooked: this.isHooked, + bridgeEnabled: this.config.Bridge_Enabled !== false + }); + }); + + if (this.debugMode) console.log('[VCPToolBridge] API routes registered and Monkey Patch applied.'); + } + + /** + * 劫持 WebSocketServer 的消息处理逻辑 + */ + applyMonkeyPatch() { + if (this.isHooked) return; + + const self = this; + const wss = this.wss; + + // 1. 尝试获取 PluginManager 的引用 + let pluginManager; + try { + pluginManager = require('../../Plugin.js'); + } catch (e) { + console.error('[VCPToolBridge] Error requiring Plugin.js:', e.message); + } + + if (!pluginManager) { + console.error('[VCPToolBridge] Could not obtain PluginManager instance.'); + return; + } + + // 2. 劫持 handleDistributedServerMessage + const originalHandler = wss.handleDistributedServerMessage; + + if (typeof originalHandler !== 'function') { + console.error('[VCPToolBridge] WebSocketServer.handleDistributedServerMessage is not a function. Hook failed.'); + return; + } + + // 替换原始处理器 + wss.handleDistributedServerMessage = async function (serverId, message) { + if (self.config.Bridge_Enabled === false) { + return originalHandler.call(wss, serverId, message); + } + + try { + if (self.debugMode) console.log(`[VCPToolBridge] Intercepted message type: ${message.type} from ${serverId}`); + + switch (message.type) { + case 'get_vcp_manifests': + await self.handleGetManifests(serverId, message, pluginManager); + return; // 拦截 + + case 'execute_vcp_tool': + await self.handleExecuteTool(serverId, message, pluginManager); + return; // 拦截 + } + } catch (err) { + console.error(`[VCPToolBridge] Error handling bridged message ${message.type}:`, err); + } + + return originalHandler.call(wss, serverId, message); + }; + + this.isHooked = true; + console.log('[VCPToolBridge] 🛡️ Monkey Patch successful: VCP Tool Bridge is now active.'); + } + + /** + * 处理清单同步请求 + */ + async handleGetManifests(serverId, message, pluginManager) { + const requestId = message.data?.requestId; + if (this.debugMode) console.log(`[VCPToolBridge] 📤 Exporting manifests to server: ${serverId} (Req: ${requestId})`); + + const excludedTools = (this.config.Excluded_Tools || "").split(',').map(t => t.trim()).filter(Boolean); + const excludedKeywords = (this.config.Excluded_Display_Keywords || "") + .split(',') + .map(t => t.trim().replace(/^["']|["']$/g, '')) + .filter(Boolean); + const exportablePlugins = []; + + for (const [name, plugin] of pluginManager.plugins.entries()) { + if (excludedTools.includes(name)) continue; + if (plugin.isDistributed) continue; + if (plugin.displayName && excludedKeywords.some(kw => plugin.displayName.includes(kw))) continue; + + if (plugin.capabilities && plugin.capabilities.invocationCommands && plugin.capabilities.invocationCommands.length > 0) { + exportablePlugins.push({ + name: plugin.name, + displayName: plugin.displayName || plugin.name, + description: plugin.description || "", + capabilities: { + invocationCommands: plugin.capabilities.invocationCommands + } + }); + } + } + + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { + type: 'vcp_manifest_response', + data: { + requestId, + plugins: exportablePlugins, + vcpVersion: '1.0.0' + } + }); + } + + /** + * 处理远程执行请求 + */ + async handleExecuteTool(serverId, message, pluginManager) { + const { requestId, toolName, toolArgs } = message.data; + if (this.debugMode) console.log(`[VCPToolBridge] ⚡ Executing bridged tool: ${toolName} (Req: ${requestId})`); + + try { + const result = await pluginManager.processToolCall(toolName, toolArgs); + + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { + type: 'vcp_tool_result', + data: { + requestId, + status: 'success', + result: result + } + }); + } catch (error) { + let errorMsg = error.message; + try { + const parsed = JSON.parse(error.message); + errorMsg = parsed.plugin_error || parsed.plugin_execution_error || error.message; + } catch (e) { } + + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { + type: 'vcp_tool_result', + data: { + requestId, + status: 'error', + error: errorMsg + } + }); + } + } + + /** + * 实现 VCP 工具调用接口 (GetStatus) + */ + async processToolCall(args) { + if (args.command === 'GetStatus') { + return { + status: 'running', + hooked: this.isHooked, + config: this.config + }; + } + throw new Error(`Unknown command: ${args.command}`); + } + + /** + * 插件关闭时清理 + */ + shutdown() { + if (this.debugMode) console.log('[VCPToolBridge] Shutting down...'); + } +} + +module.exports = new VCPToolBridge(); \ No newline at end of file diff --git a/Plugin/VCPToolBridge/plugin-manifest.json b/Plugin/VCPToolBridge/plugin-manifest.json new file mode 100644 index 000000000..afc40c496 --- /dev/null +++ b/Plugin/VCPToolBridge/plugin-manifest.json @@ -0,0 +1,42 @@ +{ + "name": "VCPToolBridge", + "displayName": "VCP 工具桥接器", + "description": "提供 VCP 原生工具向外部(如 AIO Hub)导出的桥接能力,支持元数据同步与远程执行。", + "version": "1.0.0", + "author": "Gugu_Kilo", + "pluginType": "hybridservice", + "entryPoint": { + "script": "index.js" + }, + "communication": { + "protocol": "direct", + "timeout": 60000 + }, + "hasApiRoutes": true, + "configSchema": { + "Bridge_Enabled": { + "type": "boolean", + "default": false, + "description": "是否启用桥接功能" + }, + "Excluded_Tools": { + "type": "string", + "default": "VCPLog,VCPInfo,VCPToolBridge", + "description": "不导出的工具黑名单(调用名,逗号分隔)" + }, + "Excluded_Display_Keywords": { + "type": "string", + "default": "[AIO]", + "description": "不导出的显示名称关键词(逗号分隔,包含即排除)" + } + }, + "capabilities": { + "invocationCommands": [ + { + "command": "GetStatus", + "description": "获取桥接器运行状态", + "example": "{\"command\": \"GetStatus\"}" + } + ] + } +} diff --git a/WebSocketServer.js b/WebSocketServer.js index b8bba225c..c4438b30e 100755 --- a/WebSocketServer.js +++ b/WebSocketServer.js @@ -189,7 +189,7 @@ function initialize(httpServer, config) { console.log(`[WebSocketServer] Received message from ${ws.clientId} (${ws.clientType}): ${messageString.substring(0, 300)}...`); } if (ws.clientType === 'DistributedServer') { - handleDistributedServerMessage(ws.serverId, parsedMessage); + module.exports.handleDistributedServerMessage(ws.serverId, parsedMessage); } else if (ws.clientType === 'ChromeObserver') { if (parsedMessage.type === 'heartbeat') { // 收到心跳包,发送确认 @@ -562,7 +562,7 @@ module.exports = { broadcastToAdminPanel, // 导出给管理面板的广播函数 sendMessageToClient, executeDistributedTool, + handleDistributedServerMessage, findServerByIp, shutdown - }; \ No newline at end of file From e360cadf9e428959fe84d05b8bfb6ce9ca3b00fd Mon Sep 17 00:00:00 2001 From: Miaotouy <3133963945@qq.com> Date: Thu, 26 Mar 2026 07:44:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(plugin):=20=E5=AE=9E=E7=8E=B0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E4=BA=8B=E4=BB=B6=E5=B9=BF=E6=92=AD=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E5=BC=82=E6=AD=A5=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次变更为插件系统引入了基于事件驱动的异步任务状态推送机制,解决了插件异步执行结果和进度日志无法实时反馈给调用端的问题。 - PluginManager 继承 EventEmitter 并新增 `vcp_log` 和 `vcp_info` 事件 - VCPToolBridge 插件监听核心事件并转发异步任务结果和进度日志 - 在插件回调端点广播 `plugin_async_callback` 事件以通知任务完成 - 建立 taskId 到 serverId 的映射关系,确保消息准确路由 --- Plugin.js | 26 +++++++++---- Plugin/VCPToolBridge/index.js | 71 ++++++++++++++++++++++++++++++++++- server.js | 7 ++++ 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/Plugin.js b/Plugin.js index 218566d17..d2c2ddc6a 100755 --- a/Plugin.js +++ b/Plugin.js @@ -1,5 +1,6 @@ // Plugin.js const fs = require('fs').promises; +const EventEmitter = require('events'); const path = require('path'); const { spawn } = require('child_process'); const schedule = require('node-schedule'); @@ -14,8 +15,9 @@ const PLUGIN_DIR = path.join(__dirname, 'Plugin'); const manifestFileName = 'plugin-manifest.json'; const PREPROCESSOR_ORDER_FILE = path.join(__dirname, 'preprocessor_order.json'); -class PluginManager { +class PluginManager extends EventEmitter { constructor() { + super(); this.plugins = new Map(); // 存储所有插件(本地和分布式) this.staticPlaceholderValues = new Map(); this.scheduledJobs = new Map(); @@ -650,13 +652,21 @@ class PluginManager { // 新增:获取 VCPLog 插件的推送函数,供其他插件依赖注入 getVCPLogFunctions() { const vcpLogModule = this.getServiceModule('VCPLog'); - if (vcpLogModule) { - return { - pushVcpLog: vcpLogModule.pushVcpLog, - pushVcpInfo: vcpLogModule.pushVcpInfo - }; - } - return { pushVcpLog: () => { }, pushVcpInfo: () => { } }; + const self = this; + return { + pushVcpLog: (data) => { + if (vcpLogModule && typeof vcpLogModule.pushVcpLog === 'function') { + vcpLogModule.pushVcpLog(data); + } + self.emit('vcp_log', data); + }, + pushVcpInfo: (data) => { + if (vcpLogModule && typeof vcpLogModule.pushVcpInfo === 'function') { + vcpLogModule.pushVcpInfo(data); + } + self.emit('vcp_info', data); + } + }; } async processToolCall(toolName, toolArgs, requestIp = null) { diff --git a/Plugin/VCPToolBridge/index.js b/Plugin/VCPToolBridge/index.js index 2786f0712..3ee9bb6d5 100644 --- a/Plugin/VCPToolBridge/index.js +++ b/Plugin/VCPToolBridge/index.js @@ -8,6 +8,7 @@ class VCPToolBridge { this.config = {}; this.debugMode = false; this.isHooked = false; + this.taskToClientMap = new Map(); // taskId -> serverId } /** @@ -16,10 +17,69 @@ class VCPToolBridge { async initialize(config, dependencies) { this.config = config; this.debugMode = config.DebugMode === true; - // 注意:在 VCP 的 initialize 流程中,dependencies 可能包含 vcpLogFunctions 等 this.log = dependencies.vcpLogFunctions || { pushVcpLog: () => { }, pushVcpInfo: () => { } }; - if (this.debugMode) console.log('[VCPToolBridge] Initialized.'); + // 拿到核心 PluginManager 实例 + try { + this.pluginManager = require('../../Plugin.js'); + this.setupEventListeners(); + } catch (e) { + console.error('[VCPToolBridge] Failed to load PluginManager for event listening:', e.message); + } + + if (this.debugMode) console.log('[VCPToolBridge] Initialized with Event Listeners.'); + } + + /** + * 设置核心事件监听 + */ + setupEventListeners() { + if (!this.pluginManager) return; + + // 1. 监听进度日志 (vcp_log / vcp_info) + const forwardLog = (type, data) => { + if (this.config.Bridge_Enabled === false) return; + + const taskId = data.job_id || data.taskId; + const serverId = this.taskToClientMap.get(taskId); + + if (serverId && this.wss) { + if (this.debugMode) console.log(`[VCPToolBridge] 📡 Forwarding ${type} for task ${taskId} to ${serverId}`); + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { + type: 'vcp_tool_status', + data: { + ...data, + bridgeType: type + } + }); + } + }; + + this.pluginManager.on('vcp_log', (data) => forwardLog('log', data)); + this.pluginManager.on('vcp_info', (data) => forwardLog('info', data)); + + // 2. 监听异步回调结果 (plugin_async_callback) + this.pluginManager.on('plugin_async_callback', (info) => { + if (this.config.Bridge_Enabled === false) return; + + const { taskId, data } = info; + const serverId = this.taskToClientMap.get(taskId); + + if (serverId && this.wss) { + if (this.debugMode) console.log(`[VCPToolBridge] ✅ Forwarding async result for task ${taskId} to ${serverId}`); + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { + type: 'vcp_tool_result', + data: { + requestId: taskId, // 对应 AIO 的请求 ID + status: 'success', + result: data + } + }); + + // 任务完成,清理映射 + this.taskToClientMap.delete(taskId); + } + }); } /** @@ -159,6 +219,13 @@ class VCPToolBridge { try { const result = await pluginManager.processToolCall(toolName, toolArgs); + // 如果是异步任务(返回了 taskId),记录映射关系 + // 这样当 vcp_log 或 plugin_async_callback 事件触发时,我们知道发回给谁 + if (result && result.taskId) { + if (this.debugMode) console.log(`[VCPToolBridge] 📝 Registered async task mapping: ${result.taskId} -> ${serverId}`); + this.taskToClientMap.set(result.taskId, serverId); + } + this.wss.sendMessageToClient(serverId.replace('dist-', ''), { type: 'vcp_tool_result', data: { diff --git a/server.js b/server.js index 99548e775..dac11e7ab 100644 --- a/server.js +++ b/server.js @@ -1124,6 +1124,13 @@ app.post('/plugin-callback/:pluginName/:taskId', async (req, res) => { return res.status(404).json({ status: "error", message: "Plugin not found, but callback noted." }); } + // 🚀 核心导出点:通过 pluginManager 广播回调数据 + pluginManager.emit('plugin_async_callback', { + pluginName, + taskId, + data: callbackData + }); + // 2. WebSocket push (existing logic) if (pluginManifest.webSocketPush && pluginManifest.webSocketPush.enabled) { const targetClientType = pluginManifest.webSocketPush.targetClientType || null;