Files
zclaw_openfang/desktop/src/components/ChannelList.tsx
iven 48a430fc97 refactor(skills): add skill-adapter and refactor SkillMarket
- Add skill-adapter.ts to bridge configStore and UI skill formats
- Refactor SkillMarket to use new skill-adapter instead of skill-discovery
- Add health check state to connectionStore
- Update multiple components with improved typing
- Clean up test artifacts and add new test results
- Update README and add skill-market-mvp plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 00:28:03 +08:00

133 lines
4.8 KiB
TypeScript

import { useEffect } from 'react';
import { useConnectionStore } from '../store/connectionStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { Radio, RefreshCw, MessageCircle, Settings } from 'lucide-react';
const CHANNEL_ICONS: Record<string, string> = {
feishu: '飞',
qqbot: 'QQ',
wechat: '微',
};
// 可用频道类型(用于显示未配置的频道)
const AVAILABLE_CHANNEL_TYPES = [
{ type: 'feishu', name: '飞书 (Feishu)' },
{ type: 'wechat', name: '微信' },
{ type: 'qqbot', name: 'QQ 机器人' },
];
interface ChannelListProps {
onOpenSettings?: () => void;
}
export function ChannelList({ onOpenSettings }: ChannelListProps) {
const connectionState = useConnectionStore((s) => s.connectionState);
const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
const channels = useConfigStore((s) => s.channels);
const loadChannels = useConfigStore((s) => s.loadChannels);
const connected = connectionState === 'connected';
useEffect(() => {
if (connected) {
loadPluginStatus().then(() => loadChannels());
}
}, [connected]);
const handleRefresh = () => {
loadPluginStatus().then(() => loadChannels());
};
// 去重:基于 channel id
const uniqueChannels = channels.filter((ch, index, self) =>
index === self.findIndex(c => c.id === ch.id)
);
// 获取已配置的频道类型
const configuredTypes = new Set(uniqueChannels.map(c => c.type));
// 未配置的频道类型
const unconfiguredTypes = AVAILABLE_CHANNEL_TYPES.filter(ct => !configuredTypes.has(ct.type));
if (!connected) {
return (
<div className="flex flex-col items-center justify-center h-full text-gray-400 text-xs px-4 text-center">
<Radio className="w-8 h-8 mb-2 opacity-30" />
<p>IM </p>
<p className="mt-1"> Gateway </p>
</div>
);
}
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
<span className="text-xs font-medium text-gray-500"></span>
<button
onClick={handleRefresh}
className="p-1 text-gray-400 hover:text-orange-500 rounded"
title="刷新"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{/* Configured channels */}
{uniqueChannels.map((ch) => (
<div
key={ch.id}
className="flex items-center gap-3 px-3 py-3 hover:bg-gray-100 border-b border-gray-50"
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center text-white text-xs font-bold flex-shrink-0 ${
ch.status === 'active'
? 'bg-gradient-to-br from-blue-500 to-indigo-500'
: 'bg-gray-300'
}`}>
{CHANNEL_ICONS[ch.type] || <MessageCircle className="w-4 h-4" />}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-gray-900 truncate">{ch.label}</div>
<div className={`text-[11px] ${
ch.status === 'active' ? 'text-green-500' : ch.status === 'error' ? 'text-red-500' : 'text-gray-400'
}`}>
{ch.status === 'active' ? '已连接' : ch.status === 'error' ? ch.error || '错误' : '未配置'}
{ch.accounts !== undefined && ch.accounts > 0 && ` · ${ch.accounts} 个账号`}
</div>
</div>
</div>
))}
{/* Unconfigured channels - 只显示一次 */}
{unconfiguredTypes.map((ct) => (
<div key={ct.type} className="flex items-center gap-3 px-3 py-3 hover:bg-gray-100 border-b border-gray-50 opacity-60">
<div className="w-8 h-8 rounded-lg flex items-center justify-center text-white text-xs font-bold flex-shrink-0 bg-gray-300">
{CHANNEL_ICONS[ct.type] || <MessageCircle className="w-4 h-4" />}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-gray-600">{ct.name}</div>
<div className="text-[11px] text-gray-400"></div>
</div>
</div>
))}
{/* Help text */}
<div className="px-3 py-4 text-center">
<p className="text-[11px] text-gray-400 mb-2"> IM </p>
{onOpenSettings && (
<button
onClick={onOpenSettings}
className="inline-flex items-center gap-1 text-xs text-orange-500 hover:text-orange-600"
>
<Settings className="w-3 h-3" />
</button>
)}
</div>
</div>
</div>
);
}