Files
wecome-openclaw-client/renderer/src/App.js
徐总 fddd2b2e6b feat: 新增 Gateway 配置保存和测试消息功能
新增功能:
- OpenClaw Gateway 配置支持修改保存(地址和 Token)
- 新增测试消息通信功能,可发送测试消息验证 Gateway 连接
- 添加 URL 清理按钮(移除末尾斜杠)
- 界面显示版本号 v1.0.0-fix7

修复:
- OpenClaw WebSocket 握手协议(等待 challenge 响应)
- 关闭窗口时事件处理器访问已销毁窗口的错误
- SSL/TLS 错误诊断和提示
- 连接状态管理优化

技术改进:
- 使用 challenge-response 握手机制连接 OpenClaw Gateway
- 添加窗口销毁检查避免事件发送失败
- 改进错误日志和诊断信息
- 优化连接状态更新逻辑
2026-03-10 00:20:18 +08:00

629 lines
24 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.

import React, { useState, useEffect } from 'react';
function App() {
const [config, setConfig] = useState({
bots: [],
openclaw: {
url: 'ws://localhost:18789',
token: '',
enabled: true
}
});
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(() => {
loadConfig();
setupEventListeners();
// 定期更新连接状态
const statusInterval = setInterval(async () => {
const status = await window.electronAPI.getConnectionStatus();
setConnectionStatus(status);
}, 3000);
return () => {
clearInterval(statusInterval);
window.electronAPI.removeAllListeners('wecom-event');
window.electronAPI.removeAllListeners('openclaw-event');
};
}, []);
const loadConfig = async () => {
try {
const savedConfig = await window.electronAPI.getConfig();
setConfig(savedConfig);
addLog('success', '配置加载成功');
} catch (error) {
addLog('error', `加载配置失败:${error.message}`);
}
};
const setupEventListeners = () => {
window.electronAPI.onWeComEvent((event) => {
const { eventType, data } = event;
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(updateConnectionStatus, 100);
});
window.electronAPI.onOpenClawEvent((event) => {
const { eventType, data } = event;
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(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(-199), { type, message, timestamp }]);
};
const saveConfig = async () => {
try {
await window.electronAPI.saveConfig(config);
addLog('success', '配置保存成功');
} catch (error) {
addLog('error', `保存配置失败:${error.message}`);
}
};
const handleAddBot = async () => {
if (!newBot.botId || !newBot.secret) {
addLog('warning', '请填写 Bot ID 和 Secret');
return;
}
const updatedBots = [...config.bots, { ...newBot, id: Date.now().toString() }];
setConfig({ ...config, bots: updatedBots });
setNewBot({ botId: '', secret: '', name: '', enabled: true });
setShowAddBot(false);
await saveConfig();
addLog('success', `添加机器人:${newBot.name || newBot.botId}`);
};
const handleDeleteBot = async (botId) => {
await window.electronAPI.disconnectWeCom(botId);
const updatedBots = config.bots.filter(b => b.id !== botId);
setConfig({ ...config, bots: updatedBots });
await saveConfig();
addLog('info', `删除机器人:${botId}`);
};
const handleConnectWeCom = async (bot) => {
try {
await window.electronAPI.connectWeCom(bot);
addLog('info', `正在连接企业微信 Bot: ${bot.botId}`);
} catch (error) {
addLog('error', `连接失败:${error.message}`);
}
};
const handleDisconnectWeCom = async (botId) => {
await window.electronAPI.disconnectWeCom(botId);
addLog('info', `断开企业微信 Bot: ${botId}`);
};
const handleConnectOpenClaw = async () => {
try {
await window.electronAPI.connectOpenClaw(config.openclaw);
addLog('info', '正在连接 OpenClaw Gateway');
} catch (error) {
addLog('error', `连接失败:${error.message}`);
}
};
const handleDisconnectOpenClaw = async () => {
await window.electronAPI.disconnectOpenClaw();
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';
};
return (
<div className="app">
<div className="header">
<h1>🤖 WeCom OpenClaw Client</h1>
<p>企业微信智能机器人 - 双向消息桥接 <span style={{ color: '#00d8ff', fontSize: '0.8em' }}>v1.0.0-fix7</span></p>
</div>
<div className="container">
{/* OpenClaw 配置 */}
<div className="section">
<h2 className="section-title">OpenClaw Gateway 配置</h2>
<div className="form-row">
<div className="form-group">
<label>Gateway 地址</label>
<input
type="text"
value={config.openclaw.url}
onChange={(e) => setConfig({
...config,
openclaw: { ...config.openclaw, url: e.target.value }
})}
placeholder="ws://localhost:18789 或 wss://your-server.com"
/>
</div>
<div className="form-group">
<label>Token (可选)</label>
<input
type="password"
value={config.openclaw.token}
onChange={(e) => setConfig({
...config,
openclaw: { ...config.openclaw, token: e.target.value }
})}
placeholder="Gateway Token"
/>
</div>
</div>
<div className="checkbox-group" style={{ marginBottom: '20px' }}>
<input
type="checkbox"
id="openclaw-enabled"
checked={config.openclaw.enabled}
onChange={(e) => setConfig({
...config,
openclaw: { ...config.openclaw, enabled: e.target.checked }
})}
/>
<label htmlFor="openclaw-enabled">启用 OpenClaw 连接</label>
</div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap' }}>
<button className="btn btn-primary" onClick={handleSaveOpenClawConfig}>
💾 保存配置
</button>
<button className="btn btn-secondary" onClick={() => {
const url = config.openclaw.url.replace(/\/+$/, '');
setConfig({ ...config, openclaw: { ...config.openclaw, url } });
addLog('info', '已清理 URL 末尾斜杠');
}}>
🧹 清理 URL 斜杠
</button>
{connectionStatus.openclaw.connected ? (
<button className="btn btn-danger" onClick={handleDisconnectOpenClaw}>
断开连接
</button>
) : (
<button className="btn btn-success" onClick={handleConnectOpenClaw}>
连接 OpenClaw
</button>
)}
<div className="status-item">
<span className={`status-indicator ${connectionStatus.openclaw.connected ? 'connected' : 'disconnected'}`}></span>
<span>{connectionStatus.openclaw.connected ? '已连接' : '未连接'}</span>
</div>
</div>
{/* 测试消息 */}
<div style={{
background: '#0f3460',
padding: '16px',
borderRadius: '8px',
marginTop: '16px',
border: '1px solid #00d8ff'
}}>
<h3 style={{ color: '#00d8ff', margin: '0 0 12px 0', fontSize: '1.1em' }}>📡 测试消息通信</h3>
<div className="form-group" style={{ marginBottom: '12px' }}>
<input
type="text"
value={testOpenClawMessage}
onChange={(e) => 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'
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="btn btn-secondary"
onClick={handleSendTestOpenClawMessage}
disabled={!connectionStatus.openclaw.connected}
style={{ opacity: connectionStatus.openclaw.connected ? 1 : 0.5, cursor: connectionStatus.openclaw.connected ? 'pointer' : 'not-allowed' }}
>
🚀 发送测试到 OpenClaw
</button>
<span style={{ color: '#888', fontSize: '0.85em' }}>连接后才能发送测试</span>
</div>
{testOpenClawResult && (
<div style={{
marginTop: '12px',
padding: '10px',
borderRadius: '4px',
background: testOpenClawResult.success ? 'rgba(0, 200, 83, 0.15)' : 'rgba(255, 82, 82, 0.15)',
border: `1px solid ${testOpenClawResult.success ? '#00c853' : '#ff5252'}`,
color: testOpenClawResult.success ? '#00c853' : '#ff5252',
fontSize: '0.9em'
}}>
<strong>{testOpenClawResult.success ? '✅ 成功' : '❌ 失败'}</strong> - {testOpenClawResult.message}
{testOpenClawResult.timestamp && <span style={{ marginLeft: '12px', color: '#888' }}>[{testOpenClawResult.timestamp}]</span>}
</div>
)}
</div>
{/* 测试消息 */}
<div style={{
background: '#0f3460',
padding: '16px',
borderRadius: '8px',
marginTop: '16px'
}}>
<h3 style={{ color: '#00d8ff', margin: '0 0 12px 0', fontSize: '1.1em' }}>📡 测试消息通信</h3>
<div className="form-group" style={{ marginBottom: '12px' }}>
<input
type="text"
value={testOpenClawMessage}
onChange={(e) => 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()}
/>
</div>
<button
className="btn btn-secondary"
onClick={handleSendTestOpenClawMessage}
disabled={!connectionStatus.openclaw.connected}
style={{ opacity: connectionStatus.openclaw.connected ? 1 : 0.5 }}
>
🚀 发送测试到 OpenClaw
</button>
{testOpenClawResult && (
<div style={{
marginTop: '12px',
padding: '8px',
borderRadius: '4px',
background: testOpenClawResult.success ? 'rgba(0, 200, 83, 0.2)' : 'rgba(255, 82, 82, 0.2)',
color: testOpenClawResult.success ? '#00c853' : '#ff5252',
fontSize: '0.9em'
}}>
{testOpenClawResult.message}
</div>
)}
</div>
</div>
{/* 企业微信机器人配置 */}
<div className="section">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 className="section-title" style={{ margin: 0 }}>企业微信机器人配置</h2>
<div style={{ display: 'flex', gap: '12px' }}>
<button className="btn btn-secondary" onClick={() => setShowTestMessage(true)}>
📝 测试消息
</button>
<button className="btn btn-primary" onClick={() => setShowAddBot(true)}>
+ 添加机器人
</button>
</div>
</div>
{config.bots.length === 0 ? (
<div className="empty-state">
<p>暂无配置的机器人</p>
<button className="btn btn-primary" onClick={() => setShowAddBot(true)}>
添加第一个机器人
</button>
</div>
) : (
<div className="bot-list">
{config.bots.map(bot => (
<div key={bot.id} className="bot-card">
<div className="bot-info">
<h3>{bot.name || bot.botId}</h3>
<p>Bot ID: {bot.botId}</p>
{bot.enabled && <span style={{ color: '#00c853', fontSize: '0.8em' }}> 自动连接</span>}
</div>
<div className="bot-actions">
<div className="bot-status">
<span className={`status-indicator ${getStatus(bot.botId)}`}></span>
<span>{getStatus(bot.botId) === 'connected' ? '已连接' : '未连接'}</span>
</div>
{getStatus(bot.botId) === 'connected' ? (
<button className="btn btn-danger" onClick={() => handleDisconnectWeCom(bot.botId)}>
断开
</button>
) : (
<button className="btn btn-success" onClick={() => handleConnectWeCom(bot)}>
连接
</button>
)}
<button className="btn btn-secondary" onClick={() => handleDeleteBot(bot.id)}>
删除
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* 连接架构说明 */}
<div className="section">
<h2 className="section-title">连接架构</h2>
<div style={{
background: '#0f3460',
padding: '20px',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '0.9em',
lineHeight: '1.8'
}}>
<div style={{ color: '#00d8ff', marginBottom: '10px' }}>📡 WebSocket 双连接架构</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
<span style={{ color: '#00c853' }}>🟢 企业微信用户</span>
<span></span>
<span style={{ color: '#ffc107' }}>📱 企业微信 WebSocket</span>
<span></span>
<span style={{ color: '#00d8ff' }}>💻 本客户端</span>
<span></span>
<span style={{ color: '#00d8ff' }}>🔧 OpenClaw Gateway</span>
<span></span>
<span style={{ color: '#9c27b0' }}>🤖 AI 智能体</span>
</div>
<div style={{ marginTop: '15px', color: '#888' }}>
心跳保活30 /<br/>
自动重连指数退避最大 100 <br/>
消息转发reqId 关联确保回复正确会话
</div>
</div>
</div>
{/* 日志控制台 */}
<div className="section">
<h2 className="section-title">实时日志</h2>
<div className="log-console">
{logs.length === 0 ? (
<div style={{ color: '#666' }}>暂无日志</div>
) : (
logs.map((log, index) => (
<div key={index} className={`log-entry ${log.type}`}>
[{log.timestamp}] {log.message}
</div>
))
)}
</div>
</div>
</div>
{/* 添加机器人模态框 */}
{showAddBot && (
<div className="modal-overlay" onClick={() => setShowAddBot(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>添加企业微信机器人</h2>
<div className="form-group">
<label>机器人名称 (可选)</label>
<input
type="text"
value={newBot.name}
onChange={(e) => setNewBot({ ...newBot, name: e.target.value })}
placeholder="例如:客服机器人"
/>
</div>
<div className="form-group">
<label>Bot ID *</label>
<input
type="text"
value={newBot.botId}
onChange={(e) => setNewBot({ ...newBot, botId: e.target.value })}
placeholder="从企业微信管理后台获取"
/>
</div>
<div className="form-group">
<label>Secret *</label>
<input
type="password"
value={newBot.secret}
onChange={(e) => setNewBot({ ...newBot, secret: e.target.value })}
placeholder="从企业微信管理后台获取"
/>
</div>
<div className="checkbox-group" style={{ marginBottom: '20px' }}>
<input
type="checkbox"
id="bot-enabled"
checked={newBot.enabled}
onChange={(e) => setNewBot({ ...newBot, enabled: e.target.checked })}
/>
<label htmlFor="bot-enabled">启动时自动连接</label>
</div>
<div className="modal-actions">
<button className="btn btn-secondary" onClick={() => setShowAddBot(false)}>
取消
</button>
<button className="btn btn-primary" onClick={handleAddBot}>
添加
</button>
</div>
</div>
</div>
)}
{/* 测试消息模态框 */}
{showTestMessage && (
<div className="modal-overlay" onClick={() => setShowTestMessage(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>发送测试消息</h2>
<div className="form-group">
<label>选择机器人</label>
<select
value={testMessage.botId}
onChange={(e) => setTestMessage({ ...testMessage, botId: e.target.value })}
style={{
width: '100%',
padding: '12px',
background: '#0f3460',
border: '1px solid #1a1a2e',
borderRadius: '6px',
color: '#fff',
fontSize: '1em'
}}
>
<option value="">请选择</option>
{config.bots.map(bot => (
<option key={bot.id} value={bot.botId}>
{bot.name || bot.botId} {getStatus(bot.botId) === 'connected' ? '✓' : ''}
</option>
))}
</select>
</div>
<div className="form-group">
<label>聊天 ID (userid 群聊 ID)</label>
<input
type="text"
value={testMessage.chatId}
onChange={(e) => setTestMessage({ ...testMessage, chatId: e.target.value })}
placeholder="例如zhangsan 或 1234567890"
/>
</div>
<div className="form-group">
<label>消息内容</label>
<textarea
value={testMessage.text}
onChange={(e) => setTestMessage({ ...testMessage, text: e.target.value })}
placeholder="输入测试消息..."
rows={4}
style={{
width: '100%',
padding: '12px',
background: '#0f3460',
border: '1px solid #1a1a2e',
borderRadius: '6px',
color: '#fff',
fontSize: '1em',
resize: 'vertical'
}}
/>
</div>
<div className="modal-actions">
<button className="btn btn-secondary" onClick={() => setShowTestMessage(false)}>
取消
</button>
<button className="btn btn-primary" onClick={handleSendTestMessage}>
发送
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default App;