const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); const Store = require('electron-store'); const { v4: uuidv4 } = require('uuid'); const { WSClient } = require('@wecom/aibot-node-sdk'); const WebSocket = require('ws'); // 初始化配置存储 const store = new Store({ name: 'config', defaults: { bots: [], // 多 Bot 配置 [{id, botId, secret, name, enabled}] openclaw: { url: 'ws://localhost:18789', token: '', enabled: true } } }); let mainWindow; let wecomConnections = new Map(); // botId -> WeComConnection 实例 let openclawConnection = null; // OpenClawConnection 实例 let messageState = new Map(); // msgId -> { streamId, accumulatedText, frame, botId } let reqIdMap = new Map(); // chatId -> reqId (用于回复消息) // 创建主窗口 function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, icon: path.join(__dirname, '../resources/icon.png') }); const isDev = process.env.NODE_ENV === 'development'; if (isDev) { mainWindow.loadURL('http://localhost:3000'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, '../renderer/build/index.html')); } mainWindow.on('closed', () => { mainWindow = null; }); } // 创建系统托盘 function createTray() { const iconPath = path.join(__dirname, '../resources/icon.png'); const trayIcon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); const tray = new Tray(trayIcon); const contextMenu = Menu.buildFromTemplate([ { label: '显示主窗口', click: () => mainWindow.show() }, { label: '退出', click: () => app.quit() } ]); tray.setToolTip('WeCom OpenClaw Client'); tray.setContextMenu(contextMenu); return tray; } // 生成唯一的请求 ID function generateReqId(prefix = 'msg') { return `${prefix}_${uuidv4().substring(0, 8)}_${Date.now()}`; } // 企业微信 WebSocket 连接管理 class WeComConnection { constructor(botConfig, eventHandler) { this.botId = botConfig.botId; this.secret = botConfig.secret; this.accountId = botConfig.id || botConfig.botId; this.eventHandler = eventHandler; this.client = null; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 100; } async connect() { try { console.log(`[WeCom] Connecting bot ${this.botId}...`); this.client = new WSClient({ botId: this.botId, secret: this.secret, wsUrl: 'wss://openws.work.weixin.qq.com', logger: { debug: (msg, ...args) => console.log(`[WeCom:${this.botId}] DEBUG:`, msg, ...args), info: (msg, ...args) => console.log(`[WeCom:${this.botId}] INFO:`, msg, ...args), warn: (msg, ...args) => console.warn(`[WeCom:${this.botId}] WARN:`, msg, ...args), error: (msg, ...args) => console.error(`[WeCom:${this.botId}] ERROR:`, msg, ...args) }, heartbeatInterval: 30000, // 30 秒心跳 maxReconnectAttempts: this.maxReconnectAttempts }); // 监听连接事件 this.client.on('connected', () => { this.isConnected = true; this.reconnectAttempts = 0; console.log(`[WeCom:${this.botId}] Connected`); wecomConnections.set(this.botId, this); // 存储实例本身 // 检查窗口是否存在 if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('connected', { botId: this.botId }); } }); // 监听认证成功 this.client.on('authenticated', () => { console.log(`[WeCom:${this.botId}] Authenticated`); }); // 监听断开 this.client.on('disconnected', (reason) => { this.isConnected = false; console.log(`[WeCom:${this.botId}] Disconnected: ${reason}`); // 检查窗口是否存在 if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('disconnected', { botId: this.botId, reason }); } this.scheduleReconnect(); }); // 监听错误 this.client.on('error', (error) => { console.error(`[WeCom:${this.botId}] Error:`, error); // 检查窗口是否存在 if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('error', { botId: this.botId, error: error.message }); } // 认证失败时拒绝重连 if (error.message.includes('Authentication failed')) { return; } }); // 监听消息 - 核心:接收企业微信消息并转发到 OpenClaw this.client.on('message', async (frame) => { console.log(`[WeCom:${this.botId}] Received message:`, JSON.stringify(frame, null, 2)); // 检查窗口是否存在 if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('message', { botId: this.botId, frame }); } // 将消息转发到 OpenClaw await this.forwardMessageToOpenClaw(frame); }); // 连接 await this.client.connect(); } catch (error) { console.error(`[WeCom:${this.botId}] Connect error:`, error); this.eventHandler('error', { botId: this.botId, error: error.message }); this.scheduleReconnect(); } } // 转发消息到 OpenClaw async forwardMessageToOpenClaw(frame) { if (!openclawConnection || !openclawConnection.isConnected) { console.log('[WeCom] OpenClaw not connected, skipping forward'); return; } const body = frame.body; const chatId = body.chatid || body.from.userid; const chatType = body.chattype === 'group' ? 'group' : 'direct'; const messageId = body.msgid; const reqId = frame.headers.req_id; // 保存 reqId 用于后续回复 reqIdMap.set(chatId, reqId); // 构建转发到 OpenClaw 的消息 // 参考 /home/wecom 插件的消息处理逻辑 const text = this.extractTextFromFrame(frame); const hasMedia = this.hasMediaInFrame(frame); // 构建 OpenClaw 消息格式 const openclawMessage = { type: 'event', event: 'message.inbound', payload: { channel: 'wecom', accountId: this.accountId, message: { id: messageId, from: body.from.userid, chatId: chatId, chatType: chatType, text: text, hasMedia: hasMedia, timestamp: body.create_time || Date.now() }, reqId: reqId, botId: this.botId } }; // 发送到 OpenClaw openclawConnection.send(JSON.stringify(openclawMessage)); console.log(`[Forward] WeCom -> OpenClaw: ${messageId}`); } // 从 frame 中提取文本 extractTextFromFrame(frame) { const body = frame.body; let text = ''; if (body.text?.content) { text = body.text.content; } else if (body.mixed?.msg_item) { // 图文混排 for (const item of body.mixed.msg_item) { if (item.msgtype === 'text' && item.text?.content) { text += item.text.content + '\n'; } } } else if (body.voice?.content) { // 语音转文字 text = body.voice.content; } // 群聊中移除 @机器人 提及 if (body.chattype === 'group') { text = text.replace(/@\S+/g, '').trim(); } return text.trim(); } // 检查是否有媒体 hasMediaInFrame(frame) { const body = frame.body; return !!(body.image?.url || body.file?.url || (body.mixed?.msg_item?.some(item => item.msgtype === 'image'))); } scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error(`[WeCom:${this.botId}] Max reconnect attempts reached`); return; } this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 60000); console.log(`[WeCom:${this.botId}] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => this.connect(), delay); } // 发送消息到企业微信 async sendMessage(chatId, content, finish = true, streamId = null) { if (!this.client || !this.isConnected) { throw new Error('Not connected'); } const actualStreamId = streamId || generateReqId('stream'); const reqId = reqIdMap.get(chatId) || generateReqId('req'); // 使用流式回复 const response = { cmd: 'aibot_respond_msg', headers: { req_id: reqId }, body: { msgtype: 'stream', stream: { id: actualStreamId, finish: finish, content: content } } }; console.log(`[WeCom:${this.botId}] Sending message to ${chatId}:`, content.substring(0, 100)); await this.client.send(response); return actualStreamId; } disconnect() { if (this.client) { this.client.close(); this.isConnected = false; wecomConnections.delete(this.botId); } } } // OpenClaw Gateway WebSocket 连接 class OpenClawConnection { constructor(url, token, eventHandler) { // 清理 URL - 移除末尾斜杠 this.url = url?.trim().replace(/\/+$/, '') || 'ws://localhost:18789'; this.token = token; this.eventHandler = eventHandler; this.socket = null; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 100; this.messageId = 0; this.protocolVersion = 3; this.deviceId = `wecome-client_${process.platform}_${uuidv4()}`; } async connect() { try { console.log('[OpenClaw] Connecting to', this.url); const isSecure = this.url.startsWith('wss://'); console.log(`[OpenClaw] Using ${isSecure ? 'WSS (SSL)' : 'WS (non-SSL)'} connection`); // 创建 WebSocket 配置 const wsOptions = { rejectUnauthorized: false, // 允许自签名证书 followRedirects: true }; // 如果是 wss://,添加更多 SSL 选项 if (isSecure) { wsOptions.tls = { rejectUnauthorized: false, minVersion: 'TLSv1.2' }; } this.socket = new WebSocket(this.url, wsOptions); this.socket.on('open', () => { this.isConnected = true; this.reconnectAttempts = 0; const logMsg = '[OpenClaw] ✅ WebSocket 连接已建立 | 就绪状态:' + this.socket.readyState + ' (1=OPEN)'; console.log(logMsg); // 检查窗口是否存在 if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('connected'); // 发送详细日志到界面 mainWindow.webContents.send('openclaw-log', { type: 'info', message: logMsg }); } // 主动发送 connect 请求 const sendLog = '[OpenClaw] 📤 发送 connect 请求...'; console.log(sendLog); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('openclaw-log', { type: 'info', message: sendLog }); } this.sendConnect(); }); this.socket.on('message', (data) => { // 窗口已关闭时忽略消息 if (!mainWindow || mainWindow.isDestroyed()) { return; } try { const messageStr = data.toString(); const logMsg = '[OpenClaw] 📥 收到消息 (长度:' + messageStr.length + ' 字节) | 类型:' + messageStr.substring(0, 50); console.log(logMsg); mainWindow.webContents.send('openclaw-log', { type: 'info', message: logMsg }); const message = JSON.parse(messageStr); // 处理响应 if (message.type === 'res') { let errorDetail = ''; if (!message.ok) { if (message.error) { errorDetail = typeof message.error === 'object' ? JSON.stringify(message.error) : String(message.error); } if (message.payload) { errorDetail += ' | payload: ' + JSON.stringify(message.payload); } } const resLog = '[OpenClaw] 响应:' + (message.ok ? '✅ 成功' : '❌ 失败') + (errorDetail ? ' | ' + errorDetail : ''); console.log(resLog); mainWindow.webContents.send('openclaw-log', { type: message.ok ? 'success' : 'error', message: resLog }); } // 处理 connect.challenge 质询 if (message.type === 'event' && message.event === 'connect.challenge') { const challengeLog = '[OpenClaw] 🔐 收到 challenge 质询'; console.log(challengeLog); mainWindow.webContents.send('openclaw-log', { type: 'info', message: challengeLog }); this.sendConnect(message.payload?.nonce); return; } this.handleMessage(message); } catch (error) { const errorLog = '[OpenClaw] ❌ 解析错误:' + error.message; console.error(errorLog); mainWindow.webContents.send('openclaw-log', { type: 'error', message: errorLog }); } }); this.socket.on('close', (event) => { this.isConnected = false; const closeLog = '[OpenClaw] 🔴 连接已关闭 | 代码:' + event.code + ' | 原因:' + (event.reason || '无'); console.log(closeLog); if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('disconnected'); mainWindow.webContents.send('openclaw-log', { type: 'warning', message: closeLog }); } this.scheduleReconnect(); }); this.socket.on('error', (error) => { const errorLog = '[OpenClaw] ❌ 错误:' + error.message; console.error(errorLog); if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('error', { error: error.message }); mainWindow.webContents.send('openclaw-log', { type: 'error', message: errorLog }); if (error.message?.includes('WRONG_VERSION_NUMBER')) { mainWindow.webContents.send('openclaw-log', { type: 'error', message: '🔴 SSL/TLS 协议不匹配!请尝试改用 ws://(非加密)' }); } } }); } catch (error) { this.eventHandler('error', { error: error.message }); this.scheduleReconnect(); } } // 发送 connect 握手(响应 challenge) sendConnect(nonce = null) { const connectMessage = { type: 'req', id: this.nextId(), method: 'connect', params: { minProtocol: this.protocolVersion, maxProtocol: this.protocolVersion, client: { id: 'cli', // 客户端标识:cli, cli-ui, macos-app 等, version: '1.0.0', platform: process.platform, mode: 'operator' }, role: 'operator', scopes: ['operator.read', 'operator.write'], caps: [], commands: [], permissions: {}, auth: this.token ? { token: this.token } : {}, locale: 'zh-CN', userAgent: 'wecome-openclaw-client/1.0.0', device: { id: this.deviceId, publicKey: 'temp_' + require('crypto').randomBytes(16).toString('hex'), // 不能为空 signature: 'sig_' + require('crypto').randomBytes(16).toString('hex'), // 不能为空 signedAt: Date.now(), nonce: nonce || uuidv4() // 使用服务器的 nonce 如果存在 } } }; console.log('[OpenClaw] Sending connect request:', JSON.stringify(connectMessage, null, 2)); this.send(connectMessage); } // 处理来自 OpenClaw 的消息(回复到企业微信) handleMessage(message) { if (message.type === 'res' && message.ok) { // connect 响应 if (message.payload?.type === 'hello-ok') { console.log('[OpenClaw] ✅ Handshake successful! Protocol version:', message.payload.protocol); console.log('[OpenClaw] Policy:', JSON.stringify(message.payload.policy)); // 握手成功后才真正标记为已连接 if (mainWindow && !mainWindow.isDestroyed()) { // 发送一个特殊的握手成功事件 mainWindow.webContents.send('openclaw-event', { eventType: 'handshake-ok', data: { protocol: message.payload.protocol } }); } } } else if (message.type === 'res' && !message.ok) { console.error('[OpenClaw] ❌ Connect failed:', message.error); } else if (message.type === 'event') { // 处理 OpenClaw 的事件 if (message.event === 'message.outbound') { // OpenClaw 回复消息,需要转发到企业微信 this.forwardMessageToWeCom(message); } } } // 转发 OpenClaw 回复到企业微信 async forwardMessageToWeCom(message) { const payload = message.payload; const { channel, text, chatId, botId, streamId, finish } = payload; if (channel !== 'wecom') { return; } const wecomConn = wecomConnections.get(botId); if (!wecomConn) { console.error(`[OpenClaw] Bot ${botId} not connected`); return; } try { await wecomConn.sendMessage(chatId, text, finish, streamId); console.log(`[Forward] OpenClaw -> WeCom: ${chatId}`); } catch (error) { console.error('[Forward] Error:', error); } } send(message) { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } } nextId() { return `msg_${++this.messageId}_${Date.now()}`; } scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('[OpenClaw] Max reconnect attempts reached'); return; } this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), 60000); console.log(`[OpenClaw] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => this.connect(), delay); } disconnect() { if (this.socket) { this.socket.close(); this.isConnected = false; } } } // IPC 处理器 function setupIpcHandlers() { // 获取配置 ipcMain.handle('get-config', () => { return store.store; }); // 保存配置 ipcMain.handle('save-config', (event, config) => { store.set(config); return { success: true }; }); // 连接企业微信 Bot ipcMain.handle('connect-wecom', (event, botConfig) => { const { botId, secret, id } = botConfig; // 如果已存在,先断开 if (wecomConnections.has(botId)) { wecomConnections.get(botId).disconnect(); } const connection = new WeComConnection({ botId, secret, id }, (eventType, data) => { mainWindow.webContents.send('wecom-event', { eventType, data }); }); connection.connect(); return { success: true }; }); // 断开企业微信 Bot ipcMain.handle('disconnect-wecom', (event, botId) => { const conn = wecomConnections.get(botId); if (conn) { conn.disconnect(); wecomConnections.delete(botId); } return { success: true }; }); // 连接 OpenClaw ipcMain.handle('connect-openclaw', (event, config) => { const { url, token } = config; if (openclawConnection) { openclawConnection.disconnect(); } openclawConnection = new OpenClawConnection(url, token, (eventType, data) => { mainWindow.webContents.send('openclaw-event', { eventType, data }); }); openclawConnection.connect(); return { success: true }; }); // 断开 OpenClaw ipcMain.handle('disconnect-openclaw', () => { if (openclawConnection) { openclawConnection.disconnect(); openclawConnection = null; } return { success: true }; }); // 获取连接状态 ipcMain.handle('get-connection-status', () => { const wecomStatus = {}; wecomConnections.forEach((conn, botId) => { wecomStatus[botId] = { connected: conn.isConnected, reconnectAttempts: conn.reconnectAttempts }; }); return { wecom: wecomStatus, openclaw: openclawConnection ? { connected: openclawConnection.isConnected } : { connected: false } }; }); // 测试消息 ipcMain.handle('send-test-message', async (event, botId, chatId, text) => { const conn = wecomConnections.get(botId); if (!conn) { return { success: false, error: 'Bot not connected' }; } try { await conn.sendMessage(chatId, text, true); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }); // 测试 OpenClaw 消息 ipcMain.handle('send-test-openclaw-message', async (event, text) => { if (!openclawConnection || !openclawConnection.isConnected) { return { success: false, message: 'OpenClaw 未连接' }; } try { // 构建测试消息 const testMessage = { type: 'req', id: openclawConnection.nextId(), method: 'chat.send', params: { text: text, sessionKey: `test_${Date.now()}`, echo: true // 要求回显 } }; console.log('[OpenClaw] Sending test message:', testMessage); openclawConnection.send(testMessage); return { success: true, message: `测试消息已发送:"${text}"`, timestamp: new Date().toLocaleTimeString() }; } catch (error) { return { success: false, message: `发送失败:${error.message}` }; } }); } // 应用生命周期 app.whenReady().then(() => { createWindow(); createTray(); setupIpcHandlers(); // 自动加载配置并连接 setTimeout(async () => { const config = store.store; // 连接 OpenClaw if (config.openclaw.enabled) { await ipcMain.emit('connect-openclaw', null, config.openclaw); } // 连接所有启用的 Bot for (const bot of config.bots) { if (bot.enabled) { await ipcMain.emit('connect-wecom', null, bot); } } }, 1000); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { // 清理所有连接 wecomConnections.forEach(conn => conn.disconnect()); if (openclawConnection) { openclawConnection.disconnect(); } if (process.platform !== 'darwin') { app.quit(); } }); app.on('quit', () => { // 清理所有连接 console.log('[App] Quitting, cleaning up connections...'); wecomConnections.forEach(conn => conn.disconnect()); if (openclawConnection) { openclawConnection.disconnect(); } }); // 安全策略 app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');