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