feat: 新增 Gateway 配置保存和测试消息功能

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

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

技术改进:
- 使用 challenge-response 握手机制连接 OpenClaw Gateway
- 添加窗口销毁检查避免事件发送失败
- 改进错误日志和诊断信息
- 优化连接状态更新逻辑
This commit is contained in:
2026-03-10 00:20:18 +08:00
parent 9cce1e76fc
commit fddd2b2e6b
7 changed files with 972 additions and 132 deletions

View File

@@ -2,6 +2,7 @@
"name": "renderer",
"version": "0.1.0",
"private": true,
"homepage": "./",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -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() {
<div className="app">
<div className="header">
<h1>🤖 WeCom OpenClaw Client</h1>
<p>企业微信智能机器人图形配置客户端</p>
<p>企业微信智能机器人 - 双向消息桥接 <span style={{ color: '#00d8ff', fontSize: '0.8em' }}>v1.0.0-fix7</span></p>
</div>
<div className="container">
@@ -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"
/>
</div>
<div className="form-group">
@@ -191,7 +270,17 @@ function App() {
/>
<label htmlFor="openclaw-enabled">启用 OpenClaw 连接</label>
</div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<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}>
断开连接
@@ -206,15 +295,122 @@ function App() {
<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>
<button className="btn btn-primary" onClick={() => setShowAddBot(true)}>
+ 添加机器人
</button>
<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 ? (
@@ -231,6 +427,7 @@ function App() {
<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">
@@ -256,9 +453,40 @@ function App() {
)}
</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>
<h2 className="section-title">实时日志</h2>
<div className="log-console">
{logs.length === 0 ? (
<div style={{ color: '#666' }}>暂无日志</div>
@@ -305,6 +533,15 @@ function App() {
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)}>
取消
@@ -316,6 +553,74 @@ function App() {
</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>
);
}

View File

@@ -96,6 +96,39 @@ code {
border-color: #00d8ff;
}
.form-group textarea {
width: 100%;
padding: 12px;
background: #0f3460;
border: 1px solid #1a1a2e;
border-radius: 6px;
color: #fff;
font-size: 1em;
transition: border-color 0.3s;
resize: vertical;
}
.form-group textarea:focus {
outline: none;
border-color: #00d8ff;
}
.form-group select {
width: 100%;
padding: 12px;
background: #0f3460;
border: 1px solid #1a1a2e;
border-radius: 6px;
color: #fff;
font-size: 1em;
cursor: pointer;
}
.form-group select:focus {
outline: none;
border-color: #00d8ff;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;