Files
wecome-openclaw-client/electron/main.js
徐总 afdc9bb4e0 fix: 修复 OpenClaw Gateway 连接参数错误
问题:
- Gateway 返回 INVALID_REQUEST 错误
- schema 验证失败:client.id, publicKey, signature 不符合要求

修复:
1. client.id: 'wecome-client' → 'operator' (必须是常量值)
2. publicKey: 空字符串 → 临时生成的 hex 字符串 (不能为空)
3. signature: 空字符串 → 临时生成的 hex 字符串 (不能为空)

错误日志:
{
  "code": "INVALID_REQUEST",
  "message": "invalid connect params:
    at /client/id: must be equal to constant
    at /device/publicKey: must NOT have fewer than 1 characters
    at /device/signature: must NOT have fewer than 1 characters"
}

现在应该能成功连接到 OpenClaw Gateway 了!
2026-03-10 03:17:05 +08:00

743 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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