/** * IMChannels - IM Channel Management UI * * Displays and manages IM channel configurations. * Supports viewing, configuring, and adding new channels. */ import { useState, useEffect } from 'react'; import { Radio, RefreshCw, MessageCircle, Settings2, Plus, X, Check, AlertCircle, ExternalLink } from 'lucide-react'; import { useConnectionStore } from '../../store/connectionStore'; import { useConfigStore, type ChannelInfo } from '../../store/configStore'; import { useAgentStore } from '../../store/agentStore'; const CHANNEL_ICONS: Record = { feishu: '飞', qqbot: 'QQ', wechat: '微', discord: 'D', slack: 'S', telegram: 'T', }; const CHANNEL_CONFIG_FIELDS: Record = { feishu: [ { key: 'appId', label: 'App ID', type: 'text', placeholder: 'cli_xxx', required: true }, { key: 'appSecret', label: 'App Secret', type: 'password', placeholder: '••••••••', required: true }, ], discord: [ { key: 'botToken', label: 'Bot Token', type: 'password', placeholder: 'OTk2NzY4...', required: true }, { key: 'guildId', label: 'Guild ID (可选)', type: 'text', placeholder: '123456789', required: false }, ], slack: [ { key: 'botToken', label: 'Bot Token', type: 'password', placeholder: 'xoxb-...', required: true }, { key: 'appToken', label: 'App Token', type: 'password', placeholder: 'xapp-...', required: false }, ], telegram: [ { key: 'botToken', label: 'Bot Token', type: 'password', placeholder: '123456:ABC...', required: true }, ], qqbot: [ { key: 'appId', label: 'App ID', type: 'text', placeholder: '1234567890', required: true }, { key: 'token', label: 'Token', type: 'password', placeholder: '••••••••', required: true }, ], wechat: [ { key: 'corpId', label: 'Corp ID', type: 'text', placeholder: 'wwxxx', required: true }, { key: 'agentId', label: 'Agent ID', type: 'text', placeholder: '1000001', required: true }, { key: 'secret', label: 'Secret', type: 'password', placeholder: '••••••••', required: true }, ], }; const KNOWN_CHANNELS = [ { type: 'feishu', label: '飞书 (Feishu/Lark)', description: '企业即时通讯平台' }, { type: 'discord', label: 'Discord', description: '游戏社区和语音聊天' }, { type: 'slack', label: 'Slack', description: '团队协作平台' }, { type: 'telegram', label: 'Telegram', description: '加密即时通讯' }, { type: 'qqbot', label: 'QQ 机器人', description: '腾讯QQ官方机器人' }, { type: 'wechat', label: '企业微信', description: '企业微信机器人' }, ]; interface ChannelConfigModalProps { channel: ChannelInfo | null; channelType: string | null; isOpen: boolean; onClose: () => void; onSave: (config: Record) => Promise; isSaving: boolean; } function ChannelConfigModal({ channel, channelType, isOpen, onClose, onSave, isSaving }: ChannelConfigModalProps) { const [config, setConfig] = useState>({}); const [error, setError] = useState(null); const fields = channelType ? CHANNEL_CONFIG_FIELDS[channelType] || [] : []; useEffect(() => { if (channel?.config) { setConfig(channel.config as Record); } else { setConfig({}); } setError(null); }, [channel, channelType]); if (!isOpen || !channelType) return null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); // Validate required fields for (const field of fields) { if (field.required && !config[field.key]?.trim()) { setError(`请填写 ${field.label}`); return; } } try { await onSave(config); onClose(); } catch (err) { setError(err instanceof Error ? err.message : '保存失败'); } }; const channelInfo = KNOWN_CHANNELS.find(c => c.type === channelType); return (

{channel ? `配置 ${channel.label}` : `添加 ${channelInfo?.label || channelType}`}

{channelInfo && (

{channelInfo.description}

)} {fields.length === 0 ? (

该通道类型暂不支持通过 UI 配置

请通过配置文件或 CLI 进行配置

) : ( fields.map((field) => (
setConfig({ ...config, [field.key]: e.target.value })} placeholder={field.placeholder} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
)) )} {error && (
{error}
)} {fields.length > 0 && (
)}
); } export function IMChannels() { const channels = useConfigStore((s) => s.channels); const loadChannels = useConfigStore((s) => s.loadChannels); const createChannel = useConfigStore((s) => s.createChannel); const updateChannel = useConfigStore((s) => s.updateChannel); const connectionState = useConnectionStore((s) => s.connectionState); const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedChannel, setSelectedChannel] = useState(null); const [newChannelType, setNewChannelType] = useState(null); const [isSaving, setIsSaving] = useState(false); const [showAddMenu, setShowAddMenu] = useState(false); const connected = connectionState === 'connected'; const loading = connectionState === 'connecting' || connectionState === 'reconnecting' || connectionState === 'handshaking'; useEffect(() => { if (connected) { loadPluginStatus().then(() => loadChannels()); } }, [connected]); const handleRefresh = () => { loadPluginStatus().then(() => loadChannels()); }; const handleConfigure = (channel: ChannelInfo) => { setSelectedChannel(channel); setNewChannelType(channel.type); setIsModalOpen(true); }; const handleAddChannel = (type: string) => { setSelectedChannel(null); setNewChannelType(type); setIsModalOpen(true); setShowAddMenu(false); }; const handleSaveConfig = async (config: Record) => { setIsSaving(true); try { if (selectedChannel) { await updateChannel(selectedChannel.id, { config }); } else if (newChannelType) { const channelInfo = KNOWN_CHANNELS.find(c => c.type === newChannelType); await createChannel({ type: newChannelType, name: channelInfo?.label || newChannelType, config, enabled: true, }); } await loadChannels(); } finally { setIsSaving(false); } }; const availableChannels = KNOWN_CHANNELS.filter( (channel) => !channels.some((item) => item.type === channel.type) ); return (

IM 频道

{connected ? `${channels.length} 个已识别频道` : loading ? '连接中...' : '未连接 Gateway'}
{!connected ? (
连接 Gateway 后查看真实 IM 频道状态
) : (
{channels.length > 0 ? channels.map((channel) => (
{CHANNEL_ICONS[channel.type] || }
{channel.label}
{channel.status === 'active' ? '已连接' : channel.status === 'error' ? channel.error || '错误' : '未配置'} {channel.accounts !== undefined && channel.accounts > 0 ? ` · ${channel.accounts} 个账号` : ''}
)) : (
尚未识别到可用频道
)}
)} {/* Add Channel Section */} {connected && availableChannels.length > 0 && (
添加新频道
{showAddMenu && (
{availableChannels.map((channel) => ( ))}
)}
)} {/* Planned Channels */}
规划中的接入渠道
{availableChannels.map((channel) => ( {channel.label} ))} {availableChannels.length === 0 && (
所有支持的渠道已配置
)}
{/* External Link Notice */}

高级配置

账号绑定、消息路由等高级功能需要在 Gateway 配置文件中完成。

配置文件路径: ~/.zclaw/zclaw.toml

{/* Config Modal */} { setIsModalOpen(false); setSelectedChannel(null); setNewChannelType(null); }} onSave={handleSaveConfig} isSaving={isSaving} />
); }