const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); const https = require('https'); const http = require('http'); const Store = require('electron-store'); const { v4: uuidv4 } = require('uuid'); const { WSClient } = require('@wecom/aibot-node-sdk'); const WebSocket = require('ws'); const crypto = require('crypto'); const messageStore = require('./messageStore'); // 初始化配置存储 const store = new Store({ name: 'config', defaults: { bots: [], openclaw: { url: 'ws://localhost:18789', token: '', enabled: true } } }); let mainWindow; let wecomConnections = new Map(); let openclawConnection = null; let reqIdMap = new Map(); // 下载文件到本地 async function downloadFile(url, aesKey = null) { return new Promise((resolve, reject) => { const chunks = []; const req = (url.startsWith('https') ? https : http).get(url, (res) => { res.on('data', chunk => chunks.push(chunk)); res.on('end', () => { const buffer = Buffer.concat(chunks); resolve({ buffer, filename: `file_${Date.now()}` }); }); }); req.on('error', reject); req.setTimeout(30000, () => { req.destroy(); reject(new Error('Download timeout')); }); }); } // 保存媒体文件 async function saveMediaFile(buffer, filename, mediaType = 'inbound') { const mediaDir = path.join(app.getPath('userData'), 'media', mediaType); fs.mkdirSync(mediaDir, { recursive: true }); const ext = path.extname(filename) || '.dat'; const savePath = path.join(mediaDir, `${filename}_${Date.now()}${ext}`); fs.writeFileSync(savePath, buffer); return { path: savePath, contentType: 'application/octet-stream' }; } 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) => console.log(`[WeCom:${this.botId}] DEBUG:`, msg), info: (msg) => console.log(`[WeCom:${this.botId}] INFO:`, msg), warn: (msg) => console.warn(`[WeCom:${this.botId}] WARN:`, msg), error: (msg) => console.error(`[WeCom:${this.botId}] ERROR:`, msg) }, heartbeatInterval: 30000, 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('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; }); this.client.on('message', async (frame) => { console.log(`[WeCom:${this.botId}] Received message`); if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('message', { botId: this.botId, frame }); } 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 - 使用 chat.send API 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; reqIdMap.set(chatId, reqId); // 提取文本 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(); } // 构建 sessionKey(格式:botId:chatId) const sessionKey = `${this.botId}:${chatId}`; // 处理媒体文件 const attachments = []; // 下载图片 if (body.image?.url) { try { console.log('[WeCom] Downloading image:', body.image.url); const { buffer } = await downloadFile(body.image.url, body.image.aeskey); const saved = await saveMediaFile(buffer, 'image', 'inbound'); attachments.push({ type: 'image', path: saved.path, contentType: saved.contentType }); console.log('[WeCom] Image saved:', saved.path); } catch (error) { console.error('[WeCom] Failed to download image:', error.message); } } // 下载文件 if (body.file?.url) { try { console.log('[WeCom] Downloading file:', body.file.url); const { buffer } = await downloadFile(body.file.url, body.file.aeskey); const saved = await saveMediaFile(buffer, 'file', 'inbound'); attachments.push({ type: 'file', path: saved.path, contentType: saved.contentType }); console.log('[WeCom] File saved:', saved.path); } catch (error) { console.error('[WeCom] Failed to download file:', error.message); } } // 使用 chat.send 方法发送消息到 OpenClaw const chatSendMessage = { type: 'req', id: this.nextId(), method: 'chat.send', params: { sessionKey: sessionKey, message: text, attachments: attachments.length > 0 ? attachments : undefined, deliver: true, // 立即投递到 AI idempotencyKey: messageId // 使用消息 ID 作为幂等键 } }; console.log('[OpenClaw] Sending via chat.send:', sessionKey); openclawConnection.send(JSON.stringify(chatSendMessage)); console.log(`[Forward] WeCom -> OpenClaw: ${messageId}`); // 记录消息到存储 messageStore.saveMessage({ direction: 'inbound', source: 'wecom', sessionId: sessionKey, chatId: chatId, senderId: body.from?.userid || '', senderName: body.from?.name || body.from?.userid || '', messageType: body.image ? 'image' : body.file ? 'file' : body.voice ? 'voice' : 'text', content: text, attachments: attachments, metadata: { wecomMsgId: messageId, chatType: chatType, reqId: reqId } }); } 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`); 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 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); } } } class OpenClawConnection { constructor(url, token, eventHandler) { 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.deviceId = `wecome-client_${process.platform}_${uuidv4()}`; } async connect() { try { console.log('='.repeat(60)); console.log('[OpenClaw] ========== 开始连接 =========='); console.log('[OpenClaw] 目标地址:', this.url); const isSecure = this.url.startsWith('wss://'); console.log(`[OpenClaw] 连接类型:${isSecure ? 'WSS (SSL)' : 'WS (非加密)'}`); const wsOptions = { rejectUnauthorized: false, followRedirects: true, handshakeTimeout: 10000 }; if (isSecure) { wsOptions.tls = { rejectUnauthorized: false, minVersion: 'TLSv1.2' }; console.log('[OpenClaw] SSL 配置:已启用(允许自签名证书)'); } console.log('[OpenClaw] 创建 WebSocket 连接...'); this.socket = new WebSocket(this.url, wsOptions); this.socket.on('open', () => { console.log('[OpenClaw] ✅ WebSocket 连接已建立'); console.log('[OpenClaw] 就绪状态:', this.socket.readyState, '(1=OPEN)'); this.isConnected = true; this.reconnectAttempts = 0; if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('connected'); } console.log('[OpenClaw] 📤 发送 connect 请求...'); this.sendConnect(); }); this.socket.on('message', (data) => { if (!mainWindow || mainWindow.isDestroyed()) return; const messageStr = data.toString(); console.log('[OpenClaw] 📥 收到消息 (长度:', messageStr.length, '字节)'); console.log('[OpenClaw] 原始数据:', messageStr.substring(0, 500)); const message = JSON.parse(messageStr); console.log('[OpenClaw] 消息类型:', message.type); if (message.type === 'event') { console.log('[OpenClaw] 事件名称:', message.event); } else if (message.type === 'res') { console.log('[OpenClaw] 响应 ID:', message.id); console.log('[OpenClaw] 响应状态:', message.ok ? '✅ 成功' : '❌ 失败'); if (!message.ok) { console.log('[OpenClaw] 错误信息:', JSON.stringify(message.error)); } } if (message.type === 'event' && message.event === 'connect.challenge') { console.log('[OpenClaw] 🔐 收到 challenge 质询'); console.log('[OpenClaw] Nonce:', message.payload?.nonce); this.sendConnect(message.payload?.nonce); return; } this.handleMessage(message); }); this.socket.on('close', (event) => { console.log('[OpenClaw] 🔴 连接已关闭'); console.log('[OpenClaw] 关闭代码:', event.code); console.log('[OpenClaw] 关闭原因:', event.reason || '无'); console.log('[OpenClaw] 是否干净:', event.wasClean); this.isConnected = false; if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('disconnected'); } if (event.code === 1006) { console.log('[OpenClaw] ⚠️ 异常关闭(1006)- 网络问题或服务器拒绝'); } this.scheduleReconnect(); }); this.socket.on('error', (error) => { console.error('[OpenClaw] ❌ WebSocket 错误:', error.message); if (error.message?.includes('WRONG_VERSION_NUMBER')) { console.error('[OpenClaw] 🔴 SSL/TLS 协议不匹配!'); console.error('[OpenClaw] 建议:尝试改用 ws://(非加密)'); } if (mainWindow && !mainWindow.isDestroyed()) { this.eventHandler('error', { error: error.message }); } }); } catch (error) { console.error('[OpenClaw] ❌ 连接异常:', error.message); this.eventHandler('error', { error: error.message }); this.scheduleReconnect(); } } sendConnect(nonce = null) { const tempPublicKey = crypto.randomBytes(32).toString('hex'); const tempSignature = crypto.randomBytes(64).toString('hex'); const connectMessage = { type: 'req', id: this.nextId(), method: 'connect', params: { minProtocol: 3, maxProtocol: 3, client: { id: 'cli', 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: tempPublicKey, signature: tempSignature, signedAt: Date.now(), nonce: nonce || crypto.randomUUID() } } }; console.log('[OpenClaw] 发送 connect 请求'); this.send(connectMessage); } handleMessage(message) { if (message.type === 'res' && message.ok) { if (message.payload?.type === 'hello-ok') { console.log('[OpenClaw] ✅ Handshake 成功!协议版本:', 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 失败:', JSON.stringify(message.error)); } else if (message.type === 'event') { if (message.event === 'message.outbound') { this.forwardMessageToWeCom(message); } } } 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}`); // 记录消息到存储 const sessionKey = `${botId}:${chatId}`; messageStore.saveMessage({ direction: 'outbound', source: 'openclaw', sessionId: sessionKey, chatId: chatId, senderId: 'openclaw', senderName: 'OpenClaw AI', messageType: 'text', content: text, metadata: { streamId: streamId, finish: finish, botId: botId } }); } 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`); setTimeout(() => this.connect(), delay); } disconnect() { if (this.socket) { this.socket.close(); this.isConnected = false; } } } function generateReqId(prefix = 'msg') { return `${prefix}_${uuidv4().substring(0, 8)}_${Date.now()}`; } 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; } function setupIpcHandlers() { ipcMain.handle('get-config', () => store.store); ipcMain.handle('save-config', (event, config) => { store.set(config); return { success: true }; }); 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 }; }); ipcMain.handle('disconnect-wecom', (event, botId) => { const conn = wecomConnections.get(botId); if (conn) { conn.disconnect(); } return { success: true }; }); 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 }; }); 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 }; } }); 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()}`, deliver: true, idempotencyKey: uuidv4() } }; 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}` }; } }); // ============ 消息存储相关 IPC 处理器 ============ // 获取消息列表 ipcMain.handle('get-messages', (event, options = {}) => { return messageStore.getMessages(options); }); // 获取会话列表 ipcMain.handle('get-sessions', () => { return messageStore.getSessions(); }); // 搜索消息 ipcMain.handle('search-messages', (event, query, options = {}) => { return messageStore.searchMessages(query, options); }); // 获取统计数据 ipcMain.handle('get-message-stats', () => { return messageStore.getStats(); }); // 标记消息为已读 ipcMain.handle('mark-messages-read', (event, messageIds) => { return messageStore.markAsRead(messageIds); }); // 导出消息 ipcMain.handle('export-messages', (event, options = {}) => { return messageStore.exportMessages(options); }); // 清空消息 ipcMain.handle('clear-messages', (event, options = {}) => { messageStore.clear(options); return { success: true }; }); } app.whenReady().then(() => { createWindow(); createTray(); setupIpcHandlers(); setTimeout(async () => { const config = store.store; if (config.openclaw.enabled) { await ipcMain.emit('connect-openclaw', null, config.openclaw); } 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');