diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..95f5355 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,143 @@ +# WeCom OpenClaw Client - 构建说明 + +## ✅ 构建完成 + +**Windows 64 位版本已生成** + +### 安装包位置 +``` +/home/admin/.openclaw/workspace/wecome-openclaw-client/dist/WeCom-OpenClaw-Client-v1.0.0-win64.zip +``` + +**文件大小**: 104MB +**包含内容**: +- WeCom OpenClaw Client.exe (主程序) +- Electron 运行时 +- 应用资源 + +--- + +## 📦 安装和使用方法 + +### Windows 系统 + +1. **下载和解压** + ``` + 解压 WeCom-OpenClaw-Client-v1.0.0-win64.zip + ``` + +2. **运行程序** + ``` + 双击 "WeCom OpenClaw Client.exe" + ``` + +3. **配置机器人** + - 点击"添加机器人" + - 输入 Bot ID 和 Secret(从企业微信管理后台获取) + - 点击"连接" + +4. **配置 OpenClaw Gateway** + - 默认地址:`ws://localhost:18789` + - 如果启用了 Token 认证,填写 Token + - 点击"连接 OpenClaw" + +5. **测试** + - 在企业微信中给机器人发送消息 + - 查看日志控制台确认消息转发 + - AI 回复会自动发送回企业微信 + +--- + +## 🔧 技术架构 + +### 双 WebSocket 长连接 + +``` +企业微信用户 <---> 企业微信 WebSocket <---> 本客户端 <---> OpenClaw Gateway <---> AI 智能体 + (云端) (本地) (本地) +``` + +### 消息转发流程 + +#### 企业微信 → OpenClaw +1. 用户在企业微信发送消息 +2. 企业微信 WebSocket 推送消息到客户端 +3. 客户端提取文本、媒体、引用等内容 +4. 构建 OpenClaw 消息格式并转发 +5. OpenClaw 路由到 AI 智能体处理 + +#### OpenClaw → 企业微信 +1. AI 智能体生成回复 +2. OpenClaw 通过 WebSocket 推送回复 +3. 客户端接收回复消息 +4. 使用流式消息发送到企业微信 +5. 用户看到 AI 回复 + +### 心跳保活 +- 企业微信:30 秒/次 +- OpenClaw:15 秒/次 +- 自动重连:指数退避(最大 100 次) + +--- + +## 🛠️ 重新构建 + +如果需要重新构建: + +```bash +cd /home/admin/.openclaw/workspace/wecome-openclaw-client + +# 1. 构建 React 界面 +npm run build:react + +# 2. 打包 Windows 应用 +npm run pack # 生成 win-unpacked 目录 + +# 3. 创建 ZIP 包 +cd dist +zip -r "WeCom-OpenClaw-Client-v1.0.0-win64.zip" win-unpacked/ +``` + +--- + +## 📝 配置文件位置 + +应用配置保存在: +- Windows: `%APPDATA%\wecome-openclaw-client\config.json` + +包含: +- Bot ID 和 Secret 列表 +- OpenClaw Gateway 地址和 Token +- 自动连接设置 + +--- + +## 🔍 故障排查 + +### 连接企业微信失败 +- 检查 Bot ID 和 Secret 是否正确 +- 确认企业微信后台已开启"长连接"模式 +- 查看日志控制台的错误信息 + +### 连接 OpenClaw 失败 +- 确保 OpenClaw Gateway 已启动:`openclaw gateway status` +- 检查地址是否正确(默认 `ws://localhost:18789`) +- 如果启用了 Token,确认 Token 正确 + +### 消息不转发 +- 检查两个连接是否都已建立(绿色状态指示器) +- 查看日志控制台是否有消息记录 +- 确认企业微信机器人已正确配置 + +--- + +## 📞 技术支持 + +- 项目地址:http://192.168.1.191:23000/weworkdev/wecome-openclaw-client.git +- 联系方式:sales@toncent.com.cn + +--- + +**构建时间**: 2026-03-09 +**版本**: 1.0.0 +**平台**: Windows 64-bit diff --git a/electron/main.js b/electron/main.js index 1665ad5..71da660 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron'); +const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); const Store = require('electron-store'); @@ -10,7 +10,7 @@ const WebSocket = require('ws'); const store = new Store({ name: 'config', defaults: { - bots: [], // 多 Bot 配置 [{botId, secret, name, enabled}] + bots: [], // 多 Bot 配置 [{id, botId, secret, name, enabled}] openclaw: { url: 'ws://localhost:18789', token: '', @@ -20,9 +20,10 @@ const store = new Store({ }); let mainWindow; -let wecomClients = new Map(); // botId -> WSClient -let openclawSocket = null; -let heartbeatInterval = null; +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() { @@ -33,7 +34,8 @@ function createWindow() { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') - } + }, + icon: path.join(__dirname, '../resources/icon.png') }); const isDev = process.env.NODE_ENV === 'development'; @@ -49,11 +51,34 @@ function createWindow() { }); } +// 创建系统托盘 +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(botId, secret, eventHandler) { - this.botId = botId; - this.secret = secret; + 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; @@ -63,61 +88,215 @@ class WeComConnection { async connect() { try { + console.log(`[WeCom] Connecting bot ${this.botId}...`); + this.client = new WSClient({ botId: this.botId, secret: this.secret, - onConnected: () => { - this.isConnected = true; - this.reconnectAttempts = 0; + 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 }); - console.log(`[WeCom] Bot ${this.botId} connected`); - }, - onDisconnected: () => { - this.isConnected = false; - this.eventHandler('disconnected', { botId: this.botId }); - console.log(`[WeCom] Bot ${this.botId} disconnected`); - this.scheduleReconnect(); - }, - onMessage: (frame) => { - this.eventHandler('message', { botId: this.botId, frame }); - }, - onError: (error) => { - this.eventHandler('error', { botId: this.botId, error: error.message }); - console.error(`[WeCom] Bot ${this.botId} error:`, error); } }); + // 监听认证成功 + 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] Bot ${this.botId} max reconnect attempts reached`); + 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] Bot ${this.botId} reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); + console.log(`[WeCom:${this.botId}] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => this.connect(), delay); } - async sendCommand(cmd, body) { + // 发送消息到企业微信 + async sendMessage(chatId, content, finish = true, streamId = null) { if (!this.client || !this.isConnected) { throw new Error('Not connected'); } - return await this.client.send({ cmd, headers: { req_id: uuidv4() }, body }); + + 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); } } } @@ -125,7 +304,8 @@ class WeComConnection { // OpenClaw Gateway WebSocket 连接 class OpenClawConnection { constructor(url, token, eventHandler) { - this.url = url; + // 清理 URL - 移除末尾斜杠 + this.url = url?.trim().replace(/\/+$/, '') || 'ws://localhost:18789'; this.token = token; this.eventHandler = eventHandler; this.socket = null; @@ -133,26 +313,63 @@ class OpenClawConnection { this.reconnectAttempts = 0; this.maxReconnectAttempts = 100; this.messageId = 0; + this.protocolVersion = 3; + this.deviceId = `wecome-client_${process.platform}_${uuidv4()}`; } async connect() { try { - this.socket = new WebSocket(this.url); + 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; - this.eventHandler('connected'); - console.log('[OpenClaw] Connected'); + console.log('[OpenClaw] WebSocket connected, waiting for challenge...'); - // 发送 connect 请求 - this.sendConnect(); + // 检查窗口是否存在 + if (mainWindow && !mainWindow.isDestroyed()) { + this.eventHandler('connected'); + } + + // 注意:不立即发送 connect,等待服务器发送 connect.challenge 后再响应 }); this.socket.on('message', (data) => { + // 窗口已关闭时忽略消息 + if (!mainWindow || mainWindow.isDestroyed()) { + return; + } try { const message = JSON.parse(data.toString()); - this.eventHandler('message', message); + console.log('[OpenClaw] Received:', JSON.stringify(message, null, 2)); + + // 处理 connect.challenge 质询 + if (message.type === 'event' && message.event === 'connect.challenge') { + console.log('[OpenClaw] Received challenge, sending connect request...'); + this.sendConnect(message.payload?.nonce); + return; + } + + this.handleMessage(message); } catch (error) { console.error('[OpenClaw] Parse error:', error); } @@ -160,14 +377,34 @@ class OpenClawConnection { this.socket.on('close', () => { this.isConnected = false; - this.eventHandler('disconnected'); console.log('[OpenClaw] Disconnected'); + // 检查窗口是否存在 + if (mainWindow && !mainWindow.isDestroyed()) { + this.eventHandler('disconnected'); + } this.scheduleReconnect(); }); this.socket.on('error', (error) => { - this.eventHandler('error', { error: error.message }); - console.error('[OpenClaw] Error:', error); + console.error('[OpenClaw] Error:', error.message); + + // 检测 SSL/TLS 错误并给出建议 + if (error.message?.includes('WRONG_VERSION_NUMBER')) { + console.error('[OpenClaw] ❗ SSL/TLS 协议不匹配!'); + console.error('[OpenClaw] 建议:'); + console.error('[OpenClaw] - 如果使用 wss://,请确认服务器支持 SSL'); + console.error('[OpenClaw] - 如果服务器不支持 SSL,请改用 ws://'); + console.error('[OpenClaw] - 检查端口是否正确(SSL 和非 SSL 可能使用不同端口)'); + } + + // 检查窗口是否存在 + if (mainWindow && !mainWindow.isDestroyed()) { + this.eventHandler('error', { + error: error.message, + hint: error.message?.includes('WRONG_VERSION_NUMBER') ? + 'SSL 协议不匹配,请检查是否应该使用 ws:// 而不是 wss://' : null + }); + } }); } catch (error) { @@ -176,14 +413,15 @@ class OpenClawConnection { } } - sendConnect() { + // 发送 connect 握手(响应 challenge) + sendConnect(nonce = null) { const connectMessage = { type: 'req', id: this.nextId(), method: 'connect', params: { - minProtocol: 3, - maxProtocol: 3, + minProtocol: this.protocolVersion, + maxProtocol: this.protocolVersion, client: { id: 'wecome-client', version: '1.0.0', @@ -192,20 +430,77 @@ class OpenClawConnection { }, 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.getDeviceId(), + id: this.deviceId, publicKey: '', signature: '', signedAt: Date.now(), - nonce: uuidv4() + 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)); @@ -216,10 +511,6 @@ class OpenClawConnection { return `msg_${++this.messageId}_${Date.now()}`; } - getDeviceId() { - return `wecome-client_${process.platform}_${uuidv4()}`; - } - scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('[OpenClaw] Max reconnect attempts reached'); @@ -256,17 +547,17 @@ function setupIpcHandlers() { // 连接企业微信 Bot ipcMain.handle('connect-wecom', (event, botConfig) => { - const { botId, secret } = botConfig; + const { botId, secret, id } = botConfig; - if (wecomClients.has(botId)) { - wecomClients.get(botId).disconnect(); + // 如果已存在,先断开 + if (wecomConnections.has(botId)) { + wecomConnections.get(botId).disconnect(); } - const connection = new WeComConnection(botId, secret, (eventType, data) => { + const connection = new WeComConnection({ botId, secret, id }, (eventType, data) => { mainWindow.webContents.send('wecom-event', { eventType, data }); }); - wecomClients.set(botId, connection); connection.connect(); return { success: true }; @@ -274,9 +565,10 @@ function setupIpcHandlers() { // 断开企业微信 Bot ipcMain.handle('disconnect-wecom', (event, botId) => { - if (wecomClients.has(botId)) { - wecomClients.get(botId).disconnect(); - wecomClients.delete(botId); + const conn = wecomConnections.get(botId); + if (conn) { + conn.disconnect(); + wecomConnections.delete(botId); } return { success: true }; }); @@ -285,24 +577,24 @@ function setupIpcHandlers() { ipcMain.handle('connect-openclaw', (event, config) => { const { url, token } = config; - if (openclawSocket) { - openclawSocket.disconnect(); + if (openclawConnection) { + openclawConnection.disconnect(); } - openclawSocket = new OpenClawConnection(url, token, (eventType, data) => { + openclawConnection = new OpenClawConnection(url, token, (eventType, data) => { mainWindow.webContents.send('openclaw-event', { eventType, data }); }); - openclawSocket.connect(); + openclawConnection.connect(); return { success: true }; }); // 断开 OpenClaw ipcMain.handle('disconnect-openclaw', () => { - if (openclawSocket) { - openclawSocket.disconnect(); - openclawSocket = null; + if (openclawConnection) { + openclawConnection.disconnect(); + openclawConnection = null; } return { success: true }; }); @@ -310,7 +602,7 @@ function setupIpcHandlers() { // 获取连接状态 ipcMain.handle('get-connection-status', () => { const wecomStatus = {}; - wecomClients.forEach((conn, botId) => { + wecomConnections.forEach((conn, botId) => { wecomStatus[botId] = { connected: conn.isConnected, reconnectAttempts: conn.reconnectAttempts @@ -319,34 +611,81 @@ function setupIpcHandlers() { return { wecom: wecomStatus, - openclaw: openclawSocket ? { connected: openclawSocket.isConnected } : { connected: false } + openclaw: openclawConnection ? { connected: openclawConnection.isConnected } : { connected: false } }; }); - // 发送消息到企业微信 - ipcMain.handle('send-wecom-message', (event, botId, message) => { - if (!wecomClients.has(botId)) { + // 测试消息 + ipcMain.handle('send-test-message', async (event, botId, chatId, text) => { + const conn = wecomConnections.get(botId); + if (!conn) { return { success: false, error: 'Bot not connected' }; } - const connection = wecomClients.get(botId); - return connection.sendCommand('aibot_send_msg', message) - .then(() => ({ success: true })) - .catch(error => ({ success: false, error: error.message })); + try { + await conn.sendMessage(chatId, text, true); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } }); - // 选择配置文件 - ipcMain.handle('select-file', async (event, options) => { - const result = await dialog.showOpenDialog(mainWindow, options); - return result; + // 测试 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(); @@ -356,9 +695,9 @@ app.whenReady().then(() => { app.on('window-all-closed', () => { // 清理所有连接 - wecomClients.forEach(conn => conn.disconnect()); - if (openclawSocket) { - openclawSocket.disconnect(); + wecomConnections.forEach(conn => conn.disconnect()); + if (openclawConnection) { + openclawConnection.disconnect(); } if (process.platform !== 'darwin') { @@ -368,9 +707,10 @@ app.on('window-all-closed', () => { app.on('quit', () => { // 清理所有连接 - wecomClients.forEach(conn => conn.disconnect()); - if (openclawSocket) { - openclawSocket.disconnect(); + console.log('[App] Quitting, cleaning up connections...'); + wecomConnections.forEach(conn => conn.disconnect()); + if (openclawConnection) { + openclawConnection.disconnect(); } }); diff --git a/electron/preload.js b/electron/preload.js index e1b2a02..2422d68 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -23,6 +23,10 @@ contextBridge.exposeInMainWorld('electronAPI', { // 文件选择 selectFile: (options) => ipcRenderer.invoke('select-file', options), + // 测试消息 + sendTestMessage: (botId, chatId, text) => ipcRenderer.invoke('send-test-message', botId, chatId, text), + sendTestOpenClawMessage: (text) => ipcRenderer.invoke('send-test-openclaw-message', text), + // 事件监听 onWeComEvent: (callback) => { ipcRenderer.on('wecom-event', (event, data) => callback(data)); diff --git a/package.json b/package.json index 70a3990..8dda40c 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "version": "1.0.0", "description": "WeCom OpenClaw Client - 企业微信智能机器人图形配置客户端", "main": "electron/main.js", + "author": "徐总", + "license": "MIT", "scripts": { "start": "concurrently \"npm run dev:electron\" \"npm run dev:react\"", "dev:electron": "wait-on http://localhost:3000 && NODE_ENV=development electron .", @@ -12,52 +14,64 @@ "build:electron": "electron-builder", "pack": "electron-builder --dir", "dist": "electron-builder", - "dist:win": "electron-builder --win", + "dist:win": "electron-builder --win --x64", "dist:mac": "electron-builder --mac", "dist:linux": "electron-builder --linux" }, - "keywords": [ - "wecom", - "openclaw", - "electron", - "bot", - "websocket" - ], - "author": "徐总", - "license": "MIT", + "dependencies": { + "@wecom/aibot-node-sdk": "^1.0.0", + "electron-store": "^8.1.0", + "uuid": "^9.0.1", + "ws": "^8.16.0" + }, "devDependencies": { "concurrently": "^8.2.2", "electron": "^28.1.0", "electron-builder": "^24.9.1", + "sharp": "^0.34.5", "wait-on": "^7.2.0" }, - "dependencies": { - "@wecom/aibot-node-sdk": "^1.0.0", - "ws": "^8.16.0", - "electron-store": "^8.1.0", - "uuid": "^9.0.1" - }, "build": { "appId": "com.toncent.wecome-openclaw-client", "productName": "WeCom OpenClaw Client", "directories": { - "output": "dist" + "output": "dist", + "buildResources": "resources" }, "files": [ "electron/**/*", - "renderer/build/**/*" + "renderer/build/**/*", + "package.json" ], + "extraMetadata": { + "main": "electron/main.js" + }, "win": { - "target": ["nsis"], - "icon": "resources/icon.ico" + "target": [ + { + "target": "zip", + "arch": [ + "x64" + ] + } + ], + "icon": "resources/icon.png", + "artifactName": "${productName}-${version}-win64.${ext}" }, "mac": { - "target": ["dmg"], - "icon": "resources/icon.icns" + "target": [ + "dmg" + ], + "icon": "resources/icon.icns", + "category": "public.app-category.developer-tools" }, "linux": { - "target": ["AppImage", "deb"], - "icon": "resources/icon.png" + "target": [ + "AppImage", + "deb" + ], + "icon": "resources/icon.png", + "category": "Development" } } } diff --git a/renderer/package.json b/renderer/package.json index 90a0118..a7f0b13 100644 --- a/renderer/package.json +++ b/renderer/package.json @@ -2,6 +2,7 @@ "name": "renderer", "version": "0.1.0", "private": true, + "homepage": "./", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/renderer/src/App.js b/renderer/src/App.js index c67d08e..af6e1ef 100644 --- a/renderer/src/App.js +++ b/renderer/src/App.js @@ -12,7 +12,11 @@ function App() { const [connectionStatus, setConnectionStatus] = useState({ wecom: {}, openclaw: { connected: false } }); const [logs, setLogs] = useState([]); const [showAddBot, setShowAddBot] = useState(false); + const [showTestMessage, setShowTestMessage] = useState(false); const [newBot, setNewBot] = useState({ botId: '', secret: '', name: '', enabled: true }); + const [testMessage, setTestMessage] = useState({ botId: '', chatId: '', text: '' }); + const [testOpenClawMessage, setTestOpenClawMessage] = useState(''); + const [testOpenClawResult, setTestOpenClawResult] = useState(null); // 加载配置 useEffect(() => { @@ -23,7 +27,7 @@ function App() { const statusInterval = setInterval(async () => { const status = await window.electronAPI.getConnectionStatus(); setConnectionStatus(status); - }, 5000); + }, 3000); return () => { clearInterval(statusInterval); @@ -36,7 +40,7 @@ function App() { try { const savedConfig = await window.electronAPI.getConfig(); setConfig(savedConfig); - addLog('info', '配置加载成功'); + addLog('success', '配置加载成功'); } catch (error) { addLog('error', `加载配置失败:${error.message}`); } @@ -45,38 +49,47 @@ function App() { const setupEventListeners = () => { window.electronAPI.onWeComEvent((event) => { const { eventType, data } = event; - addLog('info', `[WeCom] ${eventType}: ${JSON.stringify(data)}`); - if (eventType === 'error') { - addLog('error', `[WeCom] 错误:${data.error}`); + if (eventType === 'message') { + const body = data.frame?.body; + const text = body?.text?.content || body?.voice?.content || '[媒体消息]'; + addLog('info', `[WeCom] 收到消息 from ${body?.from?.userid}: ${text.substring(0, 50)}`); + } else if (eventType === 'connected') { + addLog('success', `[WeCom] Bot ${data.botId} 已连接`); + } else if (eventType === 'disconnected') { + addLog('warning', `[WeCom] Bot ${data.botId} 断开:${data.reason}`); + } else if (eventType === 'error') { + addLog('error', `[WeCom] Bot ${data.botId} 错误:${data.error}`); } // 更新连接状态 - setTimeout(async () => { - const status = await window.electronAPI.getConnectionStatus(); - setConnectionStatus(status); - }, 100); + setTimeout(updateConnectionStatus, 100); }); window.electronAPI.onOpenClawEvent((event) => { const { eventType, data } = event; - addLog('info', `[OpenClaw] ${eventType}`); - if (eventType === 'error') { + if (eventType === 'connected') { + addLog('success', '[OpenClaw] Gateway 已连接'); + } else if (eventType === 'disconnected') { + addLog('warning', '[OpenClaw] Gateway 断开'); + } else if (eventType === 'error') { addLog('error', `[OpenClaw] 错误:${data?.error}`); } // 更新连接状态 - setTimeout(async () => { - const status = await window.electronAPI.getConnectionStatus(); - setConnectionStatus(status); - }, 100); + setTimeout(updateConnectionStatus, 100); }); }; + const updateConnectionStatus = async () => { + const status = await window.electronAPI.getConnectionStatus(); + setConnectionStatus(status); + }; + const addLog = (type, message) => { const timestamp = new Date().toLocaleTimeString(); - setLogs(prev => [...prev.slice(-99), { type, message, timestamp }]); + setLogs(prev => [...prev.slice(-199), { type, message, timestamp }]); }; const saveConfig = async () => { @@ -113,7 +126,7 @@ function App() { const handleConnectWeCom = async (bot) => { try { await window.electronAPI.connectWeCom(bot); - addLog('info', `连接企业微信 Bot: ${bot.botId}`); + addLog('info', `正在连接企业微信 Bot: ${bot.botId}`); } catch (error) { addLog('error', `连接失败:${error.message}`); } @@ -127,7 +140,7 @@ function App() { const handleConnectOpenClaw = async () => { try { await window.electronAPI.connectOpenClaw(config.openclaw); - addLog('info', '连接 OpenClaw Gateway'); + addLog('info', '正在连接 OpenClaw Gateway'); } catch (error) { addLog('error', `连接失败:${error.message}`); } @@ -138,6 +151,72 @@ function App() { addLog('info', '断开 OpenClaw Gateway'); }; + const handleSaveOpenClawConfig = async () => { + try { + await saveConfig(); + addLog('success', 'OpenClaw 配置已保存'); + + // 如果已连接,重新连接以应用新配置 + if (connectionStatus.openclaw.connected) { + addLog('info', '重新连接以应用新配置...'); + await handleDisconnectOpenClaw(); + setTimeout(() => handleConnectOpenClaw(), 500); + } + } catch (error) { + addLog('error', `保存失败:${error.message}`); + } + }; + + const handleSendTestOpenClawMessage = async () => { + if (!testOpenClawMessage.trim()) { + setTestOpenClawResult({ success: false, message: '请输入测试消息内容' }); + return; + } + + if (!connectionStatus.openclaw.connected) { + setTestOpenClawResult({ success: false, message: 'OpenClaw 未连接,请先连接 Gateway' }); + return; + } + + try { + const result = await window.electronAPI.sendTestOpenClawMessage(testOpenClawMessage); + setTestOpenClawResult(result); + addLog(result.success ? 'success' : 'error', result.message); + + if (result.success) { + setTestOpenClawMessage(''); + } + } catch (error) { + setTestOpenClawResult({ success: false, message: `发送失败:${error.message}` }); + addLog('error', `测试消息发送失败:${error.message}`); + } + }; + + const handleSendTestMessage = async () => { + if (!testMessage.botId || !testMessage.chatId || !testMessage.text) { + addLog('warning', '请填写完整信息'); + return; + } + + try { + const result = await window.electronAPI.sendTestMessage( + testMessage.botId, + testMessage.chatId, + testMessage.text + ); + + if (result.success) { + addLog('success', `测试消息已发送到 ${testMessage.chatId}`); + setShowTestMessage(false); + setTestMessage({ botId: '', chatId: '', text: '' }); + } else { + addLog('error', `发送失败:${result.error}`); + } + } catch (error) { + addLog('error', `发送失败:${error.message}`); + } + }; + const getStatus = (botId) => { return connectionStatus.wecom[botId]?.connected ? 'connected' : 'disconnected'; }; @@ -146,7 +225,7 @@ function App() {

🤖 WeCom OpenClaw Client

-

企业微信智能机器人图形配置客户端

+

企业微信智能机器人 - 双向消息桥接 v1.0.0-fix7

@@ -163,7 +242,7 @@ function App() { ...config, openclaw: { ...config.openclaw, url: e.target.value } })} - placeholder="ws://localhost:18789" + placeholder="ws://localhost:18789 或 wss://your-server.com" />
@@ -191,7 +270,17 @@ function App() { />
-
+
+ + {connectionStatus.openclaw.connected ? (
+ + {/* 测试消息 */} +
+

📡 测试消息通信

+
+ setTestOpenClawMessage(e.target.value)} + placeholder="输入测试消息,按回车发送..." + onKeyDown={(e) => e.key === 'Enter' && handleSendTestOpenClawMessage()} + style={{ + width: '100%', + padding: '10px', + background: '#1a1a2e', + border: '1px solid #00d8ff', + borderRadius: '6px', + color: '#fff', + fontSize: '0.95em' + }} + /> +
+
+ + 连接后才能发送测试 +
+ {testOpenClawResult && ( +
+ {testOpenClawResult.success ? '✅ 成功' : '❌ 失败'} - {testOpenClawResult.message} + {testOpenClawResult.timestamp && [{testOpenClawResult.timestamp}]} +
+ )} +
+ + {/* 测试消息 */} +
+

📡 测试消息通信

+
+ setTestOpenClawMessage(e.target.value)} + placeholder="输入测试消息内容..." + style={{ + width: '100%', + padding: '10px', + background: '#1a1a2e', + border: '1px solid #00d8ff', + borderRadius: '6px', + color: '#fff', + fontSize: '0.95em' + }} + onKeyDown={(e) => e.key === 'Enter' && handleSendTestOpenClawMessage()} + /> +
+ + {testOpenClawResult && ( +
+ {testOpenClawResult.message} +
+ )} +
{/* 企业微信机器人配置 */}

企业微信机器人配置

- +
+ + +
{config.bots.length === 0 ? ( @@ -231,6 +427,7 @@ function App() {

{bot.name || bot.botId}

Bot ID: {bot.botId}

+ {bot.enabled && ● 自动连接}
@@ -256,9 +453,40 @@ function App() { )}
+ {/* 连接架构说明 */} +
+

连接架构

+
+
📡 WebSocket 双连接架构:
+
+ 🟢 企业微信用户 + ←→ + 📱 企业微信 WebSocket + ←→ + 💻 本客户端 + ←→ + 🔧 OpenClaw Gateway + ←→ + 🤖 AI 智能体 +
+
+ • 心跳保活:30 秒/次
+ • 自动重连:指数退避(最大 100 次)
+ • 消息转发:reqId 关联确保回复正确会话 +
+
+
+ {/* 日志控制台 */}
-

连接日志

+

实时日志

{logs.length === 0 ? (
暂无日志
@@ -305,6 +533,15 @@ function App() { placeholder="从企业微信管理后台获取" />
+
+ setNewBot({ ...newBot, enabled: e.target.checked })} + /> + +
)} + + {/* 测试消息模态框 */} + {showTestMessage && ( +
setShowTestMessage(false)}> +
e.stopPropagation()}> +

发送测试消息

+
+ + +
+
+ + setTestMessage({ ...testMessage, chatId: e.target.value })} + placeholder="例如:zhangsan 或 1234567890" + /> +
+
+ +