diff --git a/Plugin.js b/Plugin.js index 218566d1..d2c2ddc6 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/config.env.example b/Plugin/VCPToolBridge/config.env.example new file mode 100644 index 00000000..08365209 --- /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 00000000..3ee9bb6d --- /dev/null +++ b/Plugin/VCPToolBridge/index.js @@ -0,0 +1,277 @@ +// 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; + this.taskToClientMap = new Map(); // taskId -> serverId + } + + /** + * 初始化插件,接收 PluginManager 注入 + */ + async initialize(config, dependencies) { + this.config = config; + this.debugMode = config.DebugMode === true; + this.log = dependencies.vcpLogFunctions || { pushVcpLog: () => { }, pushVcpInfo: () => { } }; + + // 拿到核心 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); + } + }); + } + + /** + * 注册 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); + + // 如果是异步任务(返回了 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: { + 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 00000000..afc40c49 --- /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 b8bba225..c4438b30 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 diff --git a/server.js b/server.js index 99548e77..dac11e7a 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;