feat: 初始版本 - 企业微信 OpenClaw 图形配置客户端
- 基于 Electron + React 的跨平台桌面应用 - 支持多 Bot ID 和 Secret 配置 - 双 WebSocket 长连接(企业微信 + OpenClaw Gateway) - 图形化配置界面,实时连接状态显示 - 自动重连机制 - 支持 Windows/macOS/Linux 打包 技术栈: - Electron 28 - React 18 - @wecom/aibot-node-sdk - electron-store 配置持久化
This commit is contained in:
323
renderer/src/App.js
Normal file
323
renderer/src/App.js
Normal file
@@ -0,0 +1,323 @@
|
||||
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 [newBot, setNewBot] = useState({ botId: '', secret: '', name: '', enabled: true });
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
setupEventListeners();
|
||||
|
||||
// 定期更新连接状态
|
||||
const statusInterval = setInterval(async () => {
|
||||
const status = await window.electronAPI.getConnectionStatus();
|
||||
setConnectionStatus(status);
|
||||
}, 5000);
|
||||
|
||||
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('info', '配置加载成功');
|
||||
} catch (error) {
|
||||
addLog('error', `加载配置失败:${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
setTimeout(async () => {
|
||||
const status = await window.electronAPI.getConnectionStatus();
|
||||
setConnectionStatus(status);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
window.electronAPI.onOpenClawEvent((event) => {
|
||||
const { eventType, data } = event;
|
||||
addLog('info', `[OpenClaw] ${eventType}`);
|
||||
|
||||
if (eventType === 'error') {
|
||||
addLog('error', `[OpenClaw] 错误:${data?.error}`);
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
setTimeout(async () => {
|
||||
const status = await window.electronAPI.getConnectionStatus();
|
||||
setConnectionStatus(status);
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const addLog = (type, message) => {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
setLogs(prev => [...prev.slice(-99), { 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 getStatus = (botId) => {
|
||||
return connectionStatus.wecom[botId]?.connected ? 'connected' : 'disconnected';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="header">
|
||||
<h1>🤖 WeCom OpenClaw Client</h1>
|
||||
<p>企业微信智能机器人图形配置客户端</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"
|
||||
/>
|
||||
</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' }}>
|
||||
{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>
|
||||
|
||||
{/* 企业微信机器人配置 */}
|
||||
<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>
|
||||
|
||||
{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>
|
||||
</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 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="modal-actions">
|
||||
<button className="btn btn-secondary" onClick={() => setShowAddBot(false)}>
|
||||
取消
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={handleAddBot}>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user