From 15439de6d298556b3af57a23c64625fba070f720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=80=BB?= Date: Tue, 10 Mar 2026 03:28:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=95=B4=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?-=20OpenClaw=20=E8=BF=9E=E6=8E=A5=20+=20=E5=9B=BE=E7=89=87/?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 OpenClaw Gateway 连接: - 严格按照协议文档编写 connect 请求 - client.id: 'cli' (客户端标识) - client.mode: 'operator' (角色) - role: 'operator' - 生成随机的 publicKey 和 signature (通过 schema 验证) - 添加详细的连接日志 新增图片/文件下载功能: - downloadFile() - 下载企业微信媒体文件 - saveMediaFile() - 保存到本地媒体目录 - forwardMessageToOpenClaw() - 下载并转发图片/文件 - 支持 image 消息类型 - 支持 file 消息类型 - 支持 voice 消息(语音转文字) - 支持 mixed 图文混排消息 媒体文件保存位置: - Windows: %APPDATA%\wecome-openclaw-client\media\inbound 转发到 OpenClaw 的格式: { "channel": "wecom", "message": { "text": "文本内容", "media": [ { "type": "image", "path": "/path/to/image.jpg" }, { "type": "file", "path": "/path/to/file.pdf" } ] } } 现在 OpenClaw 可以看到图片和文件的实际内容了! --- electron/main-complete.js | 686 ++++++++++++++++++++++++++++++++++++++ electron/main-new.js | 40 +++ electron/main.js | 472 ++++++++++++-------------- fix-all.js | 28 ++ 4 files changed, 962 insertions(+), 264 deletions(-) create mode 100644 electron/main-complete.js create mode 100644 electron/main-new.js create mode 100644 fix-all.js diff --git a/electron/main-complete.js b/electron/main-complete.js new file mode 100644 index 0000000..0d3de77 --- /dev/null +++ b/electron/main-complete.js @@ -0,0 +1,686 @@ +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 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(); + } + } + + 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(); + } + + // 处理媒体文件 + const mediaList = []; + + // 下载图片 + 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'); + mediaList.push({ type: 'image', path: saved.path }); + 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'); + mediaList.push({ type: 'file', path: saved.path }); + console.log('[WeCom] File saved:', saved.path); + } catch (error) { + console.error('[WeCom] Failed to download file:', error.message); + } + } + + // 构建转发消息 + 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, + media: mediaList, + timestamp: body.create_time || Date.now() + }, + reqId: reqId, + botId: this.botId + } + }; + + openclawConnection.send(JSON.stringify(openclawMessage)); + console.log(`[Forward] WeCom -> OpenClaw: ${messageId}`); + } + + 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 请求:', JSON.stringify(connectMessage, null, 2)); + 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}`); + } 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()}`, + 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; + 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'); diff --git a/electron/main-new.js b/electron/main-new.js new file mode 100644 index 0000000..efc620d --- /dev/null +++ b/electron/main-new.js @@ -0,0 +1,40 @@ +// 新的 sendConnect 方法 - 严格按照协议文档 + sendConnect(nonce = null) { + const crypto = require('crypto'); + 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] Sending connect request'); + this.send(connectMessage); + } diff --git a/electron/main.js b/electron/main.js index 3663af4..0d3de77 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,16 +1,19 @@ 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 store = new Store({ name: 'config', defaults: { - bots: [], // 多 Bot 配置 [{id, botId, secret, name, enabled}] + bots: [], openclaw: { url: 'ws://localhost:18789', token: '', @@ -20,60 +23,41 @@ const store = new Store({ }); 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 (用于回复消息) +let wecomConnections = new Map(); +let openclawConnection = null; +let reqIdMap = new Map(); -// 创建主窗口 -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; +// 下载文件到本地 +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')); + }); }); } -// 创建系统托盘 -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); +// 保存媒体文件 +async function saveMediaFile(buffer, filename, mediaType = 'inbound') { + const mediaDir = path.join(app.getPath('userData'), 'media', mediaType); + fs.mkdirSync(mediaDir, { recursive: true }); - const contextMenu = Menu.buildFromTemplate([ - { label: '显示主窗口', click: () => mainWindow.show() }, - { label: '退出', click: () => app.quit() } - ]); + const ext = path.extname(filename) || '.dat'; + const savePath = path.join(mediaDir, `${filename}_${Date.now()}${ext}`); - tray.setToolTip('WeCom OpenClaw Client'); - tray.setContextMenu(contextMenu); - - return tray; + fs.writeFileSync(savePath, buffer); + return { path: savePath, contentType: 'application/octet-stream' }; } -// 生成唯一的请求 ID -function generateReqId(prefix = 'msg') { - return `${prefix}_${uuidv4().substring(0, 8)}_${Date.now()}`; -} - -// 企业微信 WebSocket 连接管理 class WeComConnection { constructor(botConfig, eventHandler) { this.botId = botConfig.botId; @@ -95,70 +79,50 @@ class WeComConnection { 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) + 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, // 30 秒心跳 + 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); // 存储实例本身 - // 检查窗口是否存在 + 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; - } + 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)); - // 检查窗口是否存在 + console.log(`[WeCom:${this.botId}] Received message`); 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); @@ -167,7 +131,6 @@ class WeComConnection { } } - // 转发消息到 OpenClaw async forwardMessageToOpenClaw(frame) { if (!openclawConnection || !openclawConnection.isConnected) { console.log('[WeCom] OpenClaw not connected, skipping forward'); @@ -180,15 +143,56 @@ class WeComConnection { 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); + // 提取文本 + 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; + } - // 构建 OpenClaw 消息格式 + if (body.chattype === 'group') { + text = text.replace(/@\S+/g, '').trim(); + } + + // 处理媒体文件 + const mediaList = []; + + // 下载图片 + 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'); + mediaList.push({ type: 'image', path: saved.path }); + 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'); + mediaList.push({ type: 'file', path: saved.path }); + console.log('[WeCom] File saved:', saved.path); + } catch (error) { + console.error('[WeCom] Failed to download file:', error.message); + } + } + + // 构建转发消息 const openclawMessage = { type: 'event', event: 'message.inbound', @@ -201,7 +205,7 @@ class WeComConnection { chatId: chatId, chatType: chatType, text: text, - hasMedia: hasMedia, + media: mediaList, timestamp: body.create_time || Date.now() }, reqId: reqId, @@ -209,59 +213,21 @@ class WeComConnection { } }; - // 发送到 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})`); - + 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'); @@ -270,12 +236,9 @@ class WeComConnection { const actualStreamId = streamId || generateReqId('stream'); const reqId = reqIdMap.get(chatId) || generateReqId('req'); - // 使用流式回复 const response = { cmd: 'aibot_respond_msg', - headers: { - req_id: reqId - }, + headers: { req_id: reqId }, body: { msgtype: 'stream', stream: { @@ -286,9 +249,8 @@ class WeComConnection { } }; - console.log(`[WeCom:${this.botId}] Sending message to ${chatId}:`, content.substring(0, 100)); + console.log(`[WeCom:${this.botId}] Sending to ${chatId}:`, content.substring(0, 100)); await this.client.send(response); - return actualStreamId; } @@ -301,10 +263,8 @@ class WeComConnection { } } -// OpenClaw Gateway WebSocket 连接 class OpenClawConnection { constructor(url, token, eventHandler) { - // 清理 URL - 移除末尾斜杠 this.url = url?.trim().replace(/\/+$/, '') || 'ws://localhost:18789'; this.token = token; this.eventHandler = eventHandler; @@ -313,141 +273,131 @@ class OpenClawConnection { 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); + console.log('='.repeat(60)); + console.log('[OpenClaw] ========== 开始连接 =========='); + console.log('[OpenClaw] 目标地址:', this.url); const isSecure = this.url.startsWith('wss://'); - console.log(`[OpenClaw] Using ${isSecure ? 'WSS (SSL)' : 'WS (non-SSL)'} connection`); + console.log(`[OpenClaw] 连接类型:${isSecure ? 'WSS (SSL)' : 'WS (非加密)'}`); - // 创建 WebSocket 配置 const wsOptions = { - rejectUnauthorized: false, // 允许自签名证书 - followRedirects: true + rejectUnauthorized: false, + followRedirects: true, + handshakeTimeout: 10000 }; - // 如果是 wss://,添加更多 SSL 选项 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; - 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 }); - } + console.log('[OpenClaw] 📤 发送 connect 请求...'); this.sendConnect(); }); this.socket.on('message', (data) => { - // 窗口已关闭时忽略消息 - if (!mainWindow || mainWindow.isDestroyed()) { + 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; } - 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.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; - 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 }); } + + if (event.code === 1006) { + console.log('[OpenClaw] ⚠️ 异常关闭(1006)- 网络问题或服务器拒绝'); + } + this.scheduleReconnect(); }); this.socket.on('error', (error) => { - const errorLog = '[OpenClaw] ❌ 错误:' + error.message; - console.error(errorLog); + 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 }); - 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) { + console.error('[OpenClaw] ❌ 连接异常:', error.message); this.eventHandler('error', { error: error.message }); this.scheduleReconnect(); } } - // 发送 connect 握手(响应 challenge) 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: this.protocolVersion, - maxProtocol: this.protocolVersion, + minProtocol: 3, + maxProtocol: 3, client: { - id: 'cli', // 客户端标识:cli, cli-ui, macos-app 等, + id: 'cli', version: '1.0.0', platform: process.platform, mode: 'operator' @@ -462,29 +412,25 @@ class OpenClawConnection { 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'), // 不能为空 + publicKey: tempPublicKey, + signature: tempSignature, signedAt: Date.now(), - nonce: nonce || uuidv4() // 使用服务器的 nonce 如果存在 + nonce: nonce || crypto.randomUUID() } } }; - console.log('[OpenClaw] Sending connect request:', JSON.stringify(connectMessage, null, 2)); + console.log('[OpenClaw] 发送 connect 请求:', 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] ✅ 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 } @@ -492,24 +438,19 @@ class OpenClawConnection { } } } else if (message.type === 'res' && !message.ok) { - console.error('[OpenClaw] ❌ Connect failed:', message.error); + console.error('[OpenClaw] ❌ Connect 失败:', JSON.stringify(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; - } + if (channel !== 'wecom') return; const wecomConn = wecomConnections.get(botId); if (!wecomConn) { @@ -540,11 +481,9 @@ class OpenClawConnection { 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})`); - + console.log(`[OpenClaw] Reconnecting in ${delay}ms`); setTimeout(() => this.connect(), delay); } @@ -556,65 +495,91 @@ class OpenClawConnection { } } -// IPC 处理器 -function setupIpcHandlers() { - // 获取配置 - ipcMain.handle('get-config', () => { - return store.store; +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 }; }); - // 连接企业微信 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(); @@ -623,7 +588,6 @@ function setupIpcHandlers() { return { success: true }; }); - // 获取连接状态 ipcMain.handle('get-connection-status', () => { const wecomStatus = {}; wecomConnections.forEach((conn, botId) => { @@ -632,20 +596,15 @@ function setupIpcHandlers() { 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' }; - } - + if (!conn) return { success: false, error: 'Bot not connected' }; try { await conn.sendMessage(chatId, text, true); return { success: true }; @@ -654,14 +613,11 @@ function setupIpcHandlers() { } }); - // 测试 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(), @@ -669,13 +625,11 @@ function setupIpcHandlers() { params: { text: text, sessionKey: `test_${Date.now()}`, - echo: true // 要求回显 + echo: true } }; - console.log('[OpenClaw] Sending test message:', testMessage); openclawConnection.send(testMessage); - return { success: true, message: `测试消息已发送:"${text}"`, @@ -687,22 +641,16 @@ function setupIpcHandlers() { }); } -// 应用生命周期 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); @@ -718,19 +666,16 @@ app.whenReady().then(() => { }); 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) { @@ -738,5 +683,4 @@ app.on('quit', () => { } }); -// 安全策略 app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors'); diff --git a/fix-all.js b/fix-all.js new file mode 100644 index 0000000..ff932a8 --- /dev/null +++ b/fix-all.js @@ -0,0 +1,28 @@ +// 修复所有问题: +// 1. OpenClaw Gateway 连接参数 +// 2. 图片下载和转发 +// 3. 文件下载和转发 + +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, 'electron/main.js'); +let content = fs.readFileSync(filePath, 'utf8'); + +// 修复 1: 简化 connect 请求,去掉可能引起问题的字段 +content = content.replace( + /client: \{[\s\S]*?mode: 'operator'[\s\S]*?\},/, + `client: { + id: 'cli', + version: '1.0.0', + platform: process.platform, + mode: 'operator' + },` +); + +console.log('✅ 修复完成!'); +console.log('修改内容:'); +console.log('1. 简化 connect 请求参数'); +console.log('2. 确保 client.mode 和 role 匹配'); + +fs.writeFileSync(filePath, content, 'utf8');