新增功能: - OpenClaw Gateway 配置支持修改保存(地址和 Token) - 新增测试消息通信功能,可发送测试消息验证 Gateway 连接 - 添加 URL 清理按钮(移除末尾斜杠) - 界面显示版本号 v1.0.0-fix7 修复: - OpenClaw WebSocket 握手协议(等待 challenge 响应) - 关闭窗口时事件处理器访问已销毁窗口的错误 - SSL/TLS 错误诊断和提示 - 连接状态管理优化 技术改进: - 使用 challenge-response 握手机制连接 OpenClaw Gateway - 添加窗口销毁检查避免事件发送失败 - 改进错误日志和诊断信息 - 优化连接状态更新逻辑
629 lines
24 KiB
JavaScript
629 lines
24 KiB
JavaScript
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;
|