diff --git a/src/pages/Channels/index.tsx b/src/pages/Channels/index.tsx index 63b67ec..251f1e0 100644 --- a/src/pages/Channels/index.tsx +++ b/src/pages/Channels/index.tsx @@ -1,207 +1,14 @@ -import { useEffect, useMemo, useState } from 'react'; -import { - AlertCircle, - Bot, - Link2, - Pencil, - Plus, - Power, - PowerOff, - RefreshCw, - RotateCcw, - Star, - Trash2, -} from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { RefreshCw } from 'lucide-react'; import ChannelConfigModal from '../../components/channels/ChannelConfigModal'; -import { - getChannelMeta, - getPrimaryChannelOptions, - PRIMARY_CHANNEL_TYPES, -} from '../../lib/channel-meta'; -import type { - ChannelAccountCatalogGroup, - ChannelAccountsCatalogResponse, - ChannelConfigFieldValueMap, - ChannelConnectionStatus, -} from '../../lib/channel-types'; -import { onGatewayEvent } from '../../lib/gateway-client'; +import { getChannelMeta, getPrimaryChannelOptions, type ChannelMeta } from '../../lib/channel-meta'; +import type { ChannelConfigFieldValueMap } from '../../lib/channel-types'; import { hostApiFetch } from '../../lib/host-api'; -import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../../lib/runtime-events'; -import { agentsStore, useAgentsStore } from '../../stores'; - -type GatewayHealthResponse = { - ok?: boolean; - status?: 'connected' | 'disconnected' | 'reconnecting'; - initialized?: boolean; - mode?: string; - summary?: { - status?: ChannelConnectionStatus; - gateway?: { - ok?: boolean; - status?: 'connected' | 'disconnected' | 'reconnecting'; - initialized?: boolean; - mode?: string; - }; - channels?: { - status?: ChannelConnectionStatus; - groupCount?: number; - accountCount?: number; - counts?: Partial>; - }; - }; -}; - -type ModalMode = 'create-channel' | 'create-account' | 'edit-account'; - -const PRIMARY_CHANNEL_TYPE_SET = new Set(PRIMARY_CHANNEL_TYPES); -const CHANNEL_STATUS_LABELS: Record = { - connected: '运行正常', - connecting: '连接中', - disconnected: '未连接', - degraded: '部分可用', - error: '异常', -}; function cn(...tokens: Array): string { return tokens.filter(Boolean).join(' '); } -function formatChannelLabel(channelType: string, fallback?: string): string { - if (fallback) return fallback; - - const parts = String(channelType ?? '') - .split(/[-_]/) - .map((part) => part.trim()) - .filter(Boolean); - - if (parts.length === 0) return channelType; - - return parts - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); -} - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} - -function normalizeGroups(payload: unknown): ChannelAccountCatalogGroup[] { - const groups = Array.isArray(payload) - ? payload - : isRecord(payload) && Array.isArray(payload.channels) - ? payload.channels - : []; - - return groups - .filter((group): group is Record => isRecord(group)) - .map((group) => ({ - channelType: String(group.channelType ?? '').trim(), - channelLabel: formatChannelLabel(String(group.channelType ?? '').trim(), String(group.channelLabel ?? '').trim() || undefined), - defaultAccountId: String(group.defaultAccountId ?? '').trim(), - enabled: group.enabled !== false, - status: normalizeStatus(group.status), - accounts: Array.isArray(group.accounts) - ? group.accounts - .filter((account): account is Record => isRecord(account)) - .map((account) => ({ - accountId: String(account.accountId ?? '').trim(), - name: String(account.name ?? account.accountId ?? '').trim(), - configured: account.configured !== false, - enabled: account.enabled !== false, - status: normalizeStatus(account.status), - lastError: typeof account.lastError === 'string' ? account.lastError : undefined, - isDefault: Boolean(account.isDefault), - agentId: typeof account.agentId === 'string' ? account.agentId : undefined, - bindingScope: account.bindingScope === 'account' || account.bindingScope === 'channel' ? account.bindingScope : undefined, - channelUrl: typeof account.channelUrl === 'string' ? account.channelUrl : undefined, - })) - .filter((account) => account.accountId) - : [], - })) - .filter((group) => group.channelType) - .sort((left, right) => left.channelLabel.localeCompare(right.channelLabel, 'zh-CN')); -} - -function normalizeStatus(value: unknown): ChannelConnectionStatus { - const normalized = String(value ?? '').trim().toLowerCase(); - if ( - normalized === 'connected' - || normalized === 'connecting' - || normalized === 'disconnected' - || normalized === 'degraded' - || normalized === 'error' - ) { - return normalized; - } - - return 'disconnected'; -} - -function normalizeGatewayHealth(payload: unknown): GatewayHealthResponse | null { - if (!isRecord(payload)) return null; - - const summary = isRecord(payload.summary) ? payload.summary : {}; - const gateway = isRecord(summary.gateway) ? summary.gateway : {}; - const channels = isRecord(summary.channels) ? summary.channels : {}; - const counts = isRecord(channels.counts) ? channels.counts : {}; - - return { - ok: payload.ok !== false, - status: payload.status === 'disconnected' || payload.status === 'reconnecting' ? payload.status : 'connected', - initialized: payload.initialized !== false, - mode: typeof payload.mode === 'string' ? payload.mode : undefined, - summary: { - status: normalizeStatus(summary.status), - gateway: { - ok: gateway.ok !== false, - status: gateway.status === 'disconnected' || gateway.status === 'reconnecting' ? gateway.status : 'connected', - initialized: gateway.initialized !== false, - mode: typeof gateway.mode === 'string' ? gateway.mode : undefined, - }, - channels: { - status: normalizeStatus(channels.status), - groupCount: Number(channels.groupCount ?? 0) || 0, - accountCount: Number(channels.accountCount ?? 0) || 0, - counts: { - connected: Number(counts.connected ?? 0) || 0, - connecting: Number(counts.connecting ?? 0) || 0, - disconnected: Number(counts.disconnected ?? 0) || 0, - degraded: Number(counts.degraded ?? 0) || 0, - error: Number(counts.error ?? 0) || 0, - }, - }, - }, - }; -} - -function resolveAgentName(agentId: string | null | undefined, agentNames: Map): string { - const resolvedId = String(agentId ?? '').trim(); - if (!resolvedId) return '未分配'; - return agentNames.get(resolvedId) || resolvedId; -} - -function getStatusBadgeClass(status: ChannelConnectionStatus): string { - return cn( - 'border', - status === 'connected' && 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-900/20 dark:text-emerald-300', - status === 'connecting' && 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/60 dark:bg-sky-900/20 dark:text-sky-300', - status === 'disconnected' && 'border-[#E5E8EE] bg-white text-[#6B7280] dark:border-[#303036] dark:bg-[#17171a] dark:text-gray-400', - status === 'degraded' && 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-900/20 dark:text-amber-300', - status === 'error' && 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-900/20 dark:text-red-300', - ); -} - -function getStatusDotClass(status: ChannelConnectionStatus): string { - return cn( - 'inline-block h-2 w-2 rounded-full', - status === 'connected' && 'bg-emerald-500', - status === 'connecting' && 'bg-sky-500 animate-pulse', - status === 'disconnected' && 'bg-gray-400', - status === 'degraded' && 'bg-amber-500', - status === 'error' && 'bg-red-500', - ); -} - function getChannelMonogram(channelType: string): string { const meta = getChannelMeta(channelType); const initials = meta.name @@ -215,15 +22,6 @@ function getChannelMonogram(channelType: string): string { return initials || channelType.slice(0, 2).toUpperCase(); } -function getAccountDisplayName(group: ChannelAccountCatalogGroup, accountId: string, fallbackName: string): string { - const normalizedName = String(fallbackName ?? '').trim(); - if (accountId === group.defaultAccountId && (!normalizedName || normalizedName === accountId || normalizedName === 'default')) { - return `${group.channelLabel} 默认账号`; - } - - return normalizedName || accountId; -} - function buildSyntheticChannelUrl(channelType: string, accountId: string): string { return `channel://${encodeURIComponent(channelType)}/${encodeURIComponent(accountId || 'default')}`; } @@ -262,139 +60,69 @@ function validateRequiredFields(channelType: string, values: ChannelConfigFieldV return `请填写 ${missingField.label}`; } -function createNewAccountId(channelType: string, existingAccounts: string[]): string { - const normalizedExisting = new Set(existingAccounts.map((item) => item.trim()).filter(Boolean)); - let nextAccountId = `${channelType}-${crypto.randomUUID().slice(0, 8)}`; - while (normalizedExisting.has(nextAccountId)) { - nextAccountId = `${channelType}-${crypto.randomUUID().slice(0, 8)}`; - } - return nextAccountId; +function getConnectionLabel(connectionType: string): string { + if (connectionType === 'qr') return '扫码'; + if (connectionType === 'webhook') return 'Webhook'; + return 'Token'; } export default function ChannelsPage() { - const agents = useAgentsStore((state) => state.agents); - const agentsLoading = useAgentsStore((state) => state.loading); - const agentsError = useAgentsStore((state) => state.error); - const channelOwners = useAgentsStore((state) => state.channelOwners); - const channelAccountOwners = useAgentsStore((state) => state.channelAccountOwners); - - const [groups, setGroups] = useState([]); - const [gatewayHealth, setGatewayHealth] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [supportedChannels, setSupportedChannels] = useState([]); + const [refreshing, setRefreshing] = useState(false); const [feedback, setFeedback] = useState(null); - const [pendingKey, setPendingKey] = useState(null); const [modalOpen, setModalOpen] = useState(false); - const [modalMode, setModalMode] = useState('create-channel'); const [modalChannelType, setModalChannelType] = useState('telegram'); const [modalAccountId, setModalAccountId] = useState(''); const [modalValues, setModalValues] = useState({}); const [modalError, setModalError] = useState(null); const [modalSubmitting, setModalSubmitting] = useState(false); - const [modalExistingAccounts, setModalExistingAccounts] = useState([]); - const primaryChannelOptions = useMemo(() => getPrimaryChannelOptions(), []); - const agentNames = useMemo( - () => new Map(agents.map((agent) => [agent.id, agent.name])), - [agents], - ); - - async function loadPageData(): Promise { - setLoading(true); - setError(null); - - try { - const [channelsResponse, gatewayResponse] = await Promise.all([ - hostApiFetch('/api/channels/accounts'), - hostApiFetch('/api/gateway/health'), - ]); - - setGroups(normalizeGroups(channelsResponse)); - setGatewayHealth(normalizeGatewayHealth(gatewayResponse)); - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : String(requestError)); - setGroups([]); - setGatewayHealth(null); - } finally { - setLoading(false); + function loadSupportedChannels(nextFeedback?: string): void { + const channels = getPrimaryChannelOptions(); + setSupportedChannels(channels); + setModalChannelType((current) => current || channels[0]?.type || 'telegram'); + if (nextFeedback) { + setFeedback(nextFeedback); } } - async function refreshPage(): Promise { - setFeedback(null); - await Promise.allSettled([agentsStore.refresh(), loadPageData()]); - } - function resetModalState(): void { setModalOpen(false); - setModalMode('create-channel'); - setModalChannelType(primaryChannelOptions[0]?.type ?? 'telegram'); + setModalChannelType(supportedChannels[0]?.type ?? 'telegram'); setModalAccountId(''); setModalValues({}); setModalError(null); setModalSubmitting(false); - setModalExistingAccounts([]); } - async function openCreateChannelModal(channelType: string): Promise { - setModalMode('create-channel'); + function openCreateChannelModal(channelType: string): void { setModalChannelType(channelType); setModalAccountId(''); setModalValues({}); setModalError(null); - setModalExistingAccounts([]); setModalOpen(true); } - async function openCreateAccountModal(group: ChannelAccountCatalogGroup): Promise { - setModalMode('create-account'); - setModalChannelType(group.channelType); - setModalAccountId(createNewAccountId(group.channelType, group.accounts.map((item) => item.accountId))); - setModalValues({}); - setModalError(null); - setModalExistingAccounts(group.accounts.map((item) => item.accountId)); - setModalOpen(true); - } - - async function openEditAccountModal(group: ChannelAccountCatalogGroup, accountId: string): Promise { - setPendingKey(`config:${group.channelType}:${accountId}`); - setModalError(null); - - try { - const params = new URLSearchParams({ accountId }); - const response = await hostApiFetch<{ success?: boolean; values?: Record }>( - `/api/channels/config/${encodeURIComponent(group.channelType)}?${params.toString()}`, - ); - - setModalMode('edit-account'); - setModalChannelType(group.channelType); - setModalAccountId(accountId); - setModalValues(response.values ?? {}); - setModalExistingAccounts(group.accounts.map((item) => item.accountId)); - setModalOpen(true); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } + function refreshPage(): void { + setRefreshing(true); + window.setTimeout(() => { + loadSupportedChannels('支持频道列表已刷新。'); + setRefreshing(false); + }, 0); } async function handleSaveModal(): Promise { const trimmedAccountId = modalAccountId.trim(); - const accountIdForSave = modalMode === 'create-channel' && !trimmedAccountId ? '' : trimmedAccountId; + const accountIdForSave = trimmedAccountId || 'default'; const requiredError = validateRequiredFields(modalChannelType, modalValues); if (requiredError) { setModalError(requiredError); return; } - if (modalMode === 'create-account' && trimmedAccountId && modalExistingAccounts.includes(trimmedAccountId)) { - setModalError(`账号 ID "${trimmedAccountId}" 已存在,请更换后再保存。`); - return; - } - setModalSubmitting(true); setModalError(null); + try { const config = Object.fromEntries( Object.entries(modalValues) @@ -406,10 +134,10 @@ export default function ChannelsPage() { method: 'POST', body: JSON.stringify({ channelType: modalChannelType, - accountId: accountIdForSave || undefined, + accountId: trimmedAccountId || undefined, channelLabel: getChannelMeta(modalChannelType).name, - accountName: buildSavedAccountName(modalChannelType, accountIdForSave || 'default'), - channelUrl: deriveChannelUrl(modalChannelType, accountIdForSave || 'default', config), + accountName: buildSavedAccountName(modalChannelType, accountIdForSave), + channelUrl: deriveChannelUrl(modalChannelType, accountIdForSave, config), enabled: true, config, metadata: { @@ -419,13 +147,8 @@ export default function ChannelsPage() { }), }); - setFeedback( - modalMode === 'edit-account' - ? `${getChannelMeta(modalChannelType).name} / ${accountIdForSave || 'default'} 配置已更新。` - : `${getChannelMeta(modalChannelType).name} 配置已保存。`, - ); + setFeedback(`${getChannelMeta(modalChannelType).name} 配置已保存。`); resetModalState(); - await loadPageData(); } catch (requestError) { setModalError(requestError instanceof Error ? requestError.message : String(requestError)); } finally { @@ -433,598 +156,45 @@ export default function ChannelsPage() { } } - async function handleChannelOwnerChange(channelType: string, nextOwnerId: string): Promise { - const currentOwnerId = channelOwners[channelType] || ''; - if (currentOwnerId === nextOwnerId) return; - - setPendingKey(`channel:${channelType}`); - setFeedback(null); - try { - if (nextOwnerId) { - await agentsStore.assignChannel(nextOwnerId, channelType); - setFeedback(`${formatChannelLabel(channelType)} 的频道级归属已更新。`); - } else if (currentOwnerId) { - await agentsStore.removeChannel(currentOwnerId, channelType); - setFeedback(`${formatChannelLabel(channelType)} 的频道级归属已清除。`); - } - await loadPageData(); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - - async function handleAccountOwnerChange(channelType: string, accountId: string, nextOwnerId: string): Promise { - const bindingKey = `${channelType}:${accountId}`; - const currentOwnerId = channelAccountOwners[bindingKey] || ''; - if (currentOwnerId === nextOwnerId) return; - - setPendingKey(bindingKey); - setFeedback(null); - try { - if (nextOwnerId) { - await agentsStore.assignChannelAccount(nextOwnerId, channelType, accountId); - setFeedback(`${formatChannelLabel(channelType)} / ${accountId} 的账号归属已更新。`); - } else { - await agentsStore.clearChannelBinding(channelType, accountId); - setFeedback(`${formatChannelLabel(channelType)} / ${accountId} 的账号归属已清除。`); - } - await loadPageData(); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - - async function handleSetDefaultAccount(channelType: string, accountId: string): Promise { - setPendingKey(`default:${channelType}:${accountId}`); - setFeedback(null); - try { - await hostApiFetch('/api/channels/default-account', { - method: 'PUT', - body: JSON.stringify({ channelType, accountId }), - }); - setFeedback(`${formatChannelLabel(channelType)} 的默认账号已切换为 ${accountId}。`); - await loadPageData(); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - - async function handleToggleChannelEnabled(channelType: string, enabled: boolean): Promise { - setPendingKey(`enabled:${channelType}`); - setFeedback(null); - try { - await hostApiFetch('/api/channels/config/enabled', { - method: 'PUT', - body: JSON.stringify({ channelType, enabled }), - }); - setFeedback(`${formatChannelLabel(channelType)} 已${enabled ? '启用' : '禁用'}。`); - await loadPageData(); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - - async function handleDeleteConfig(channelType: string, accountId?: string): Promise { - const scopeLabel = accountId ? `${formatChannelLabel(channelType)} / ${accountId}` : formatChannelLabel(channelType); - const confirmed = window.confirm(`确认删除 ${scopeLabel} 的消息频道配置吗?删除后将同步清理绑定关系。`); - if (!confirmed) return; - - setPendingKey(`delete:${channelType}:${accountId ?? 'channel'}`); - setFeedback(null); - try { - const suffix = accountId ? `?accountId=${encodeURIComponent(accountId)}` : ''; - await hostApiFetch(`/api/channels/config/${encodeURIComponent(channelType)}${suffix}`, { - method: 'DELETE', - }); - setFeedback(`${scopeLabel} 已删除。`); - await Promise.allSettled([loadPageData(), agentsStore.refresh()]); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - - async function handleRestartGateway(): Promise { - setPendingKey('gateway:restart'); - setFeedback(null); - try { - await hostApiFetch('/api/gateway/restart', { method: 'POST' }); - setFeedback('Gateway 重启指令已发送,正在等待频道状态收敛。'); - await loadPageData(); - window.setTimeout(() => { - void loadPageData(); - }, 1200); - } catch (requestError) { - setFeedback(requestError instanceof Error ? requestError.message : String(requestError)); - } finally { - setPendingKey(null); - } - } - useEffect(() => { - setModalChannelType(primaryChannelOptions[0]?.type ?? 'telegram'); - }, [primaryChannelOptions]); - - useEffect(() => { - void Promise.all([agentsStore.init(), loadPageData()]); + loadSupportedChannels(); }, []); - useEffect(() => { - return onGatewayEvent((event) => { - if (!isRuntimeChangedGatewayEvent(event)) return; - if (!runtimeEventHasTopic(event, 'channels', 'agents', 'providers', 'channel-targets')) return; - void loadPageData(); - }); - }, []); - - const configuredGroups = useMemo(() => { - const groupedByType = new Map(groups.map((group) => [group.channelType, group])); - const ordered: ChannelAccountCatalogGroup[] = []; - - for (const channelType of PRIMARY_CHANNEL_TYPES) { - const match = groupedByType.get(channelType); - if (match) ordered.push(match); - } - - for (const group of groups) { - if (!PRIMARY_CHANNEL_TYPE_SET.has(group.channelType)) { - ordered.push(group); - } - } - - return ordered; - }, [groups]); - - const supportedChannels = useMemo(() => { - const configuredTypes = new Set(groups.map((group) => group.channelType)); - return primaryChannelOptions.filter((meta) => !configuredTypes.has(meta.type)); - }, [groups, primaryChannelOptions]); - - const healthSummary = gatewayHealth?.summary; - const gatewayBannerVisible = Boolean( - gatewayHealth - && ( - gatewayHealth.ok === false - || gatewayHealth.status !== 'connected' - || healthSummary?.status !== 'connected' - ), - ); - - const dashboardCards = [ - { - label: '已配置频道', - value: String(configuredGroups.length), - hint: '频道组', - }, - { - label: '账号总数', - value: String(healthSummary?.channels?.accountCount ?? configuredGroups.reduce((sum, group) => sum + group.accounts.length, 0)), - hint: '含默认账号', - }, - { - label: '运行健康', - value: CHANNEL_STATUS_LABELS[healthSummary?.status ?? 'disconnected'], - hint: `Gateway ${gatewayHealth?.status ?? 'disconnected'}`, - }, - ]; - return (
-
-
-
-

+
+
+
+

+ 支持的频道 +

+
+ 统一管理消息频道、账号、账号与智能体的绑定关系,以及频道默认账号 +
+
+ +

-

- 统一管理消息频道、账号、频道与 Agent 的绑定关系,以及频道默认账号与运行态健康度,持续向 ClawX 的频道控制台体验对齐。 -

+ + 刷新 +
- -
- -
- {dashboardCards.map((card) => ( -
-
{card.label}
-
{card.value}
-
{card.hint}
-
- ))} -
- -
{feedback ? (
{feedback}
) : null} - {error ? ( -
-
- - {error} -
-
- ) : null} - - {agentsError ? ( -
-
- - {`Agents 数据加载失败:${agentsError}`} -
-
- ) : null} - - {gatewayBannerVisible ? ( -
-
-
- -
-
- {`Gateway ${gatewayHealth?.status === 'reconnecting' ? '正在重连' : gatewayHealth?.status === 'disconnected' ? '已断开' : '运行中'},频道总体状态为 ${CHANNEL_STATUS_LABELS[healthSummary?.status ?? 'disconnected']}`} -
-
- {`已发现 ${healthSummary?.channels?.groupCount ?? 0} 个频道组、${healthSummary?.channels?.accountCount ?? 0} 个账号。`} -
-
-
- - -
-
- ) : null} - - {loading && groups.length === 0 ? ( -
- 正在加载消息频道控制台... -
- ) : null} - - {configuredGroups.length > 0 ? ( -
-
-
-

- 已配置的频道 -

-
- 这里集中处理默认账号、Agent 归属、多账号扩展和生命周期操作。 -
-
-
- -
- {configuredGroups.map((group) => { - const meta = getChannelMeta(group.channelType); - const channelOwnerId = channelOwners[group.channelType] || ''; - const channelPending = pendingKey === `channel:${group.channelType}`; - - return ( -
-
-
-
-
- {getChannelMonogram(group.channelType)} -
- -
-
-

- {meta.name} -

- {meta.isPlugin ? ( - - 插件 - - ) : null} - {!group.enabled ? ( - - 已禁用 - - ) : null} - - - - {CHANNEL_STATUS_LABELS[group.status]} - - -
- -
- {group.channelType} - {`${group.accounts.length} 个账号`} - {`默认账号:${group.defaultAccountId || 'default'}`} -
- -
- {meta.description} -
-
-
- -
- - - - -
-
- -
-
-
- - 频道级兜底归属 -
-
- 当某个账号没有单独指定 Agent 时,将继承这里的归属。 -
- -
-
-
- -
- {group.accounts.map((account) => { - const bindingKey = `${group.channelType}:${account.accountId}`; - const explicitOwnerId = channelAccountOwners[bindingKey] || ''; - const effectiveOwnerId = explicitOwnerId || channelOwnerId; - const bindingScope = explicitOwnerId ? '账号归属' : channelOwnerId ? '继承频道' : '未分配'; - const accountPending = pendingKey === bindingKey; - const displayName = getAccountDisplayName(group, account.accountId, account.name); - - return ( -
-
-
-
-
- {displayName} -
- {account.isDefault ? ( - - 默认账号 - - ) : null} - {!account.enabled ? ( - - 不可用 - - ) : null} - - - - {CHANNEL_STATUS_LABELS[account.status]} - - - - {bindingScope} - -
- -
- {`当前归属:${resolveAgentName(effectiveOwnerId, agentNames)}`} - {`账号 ID:${account.accountId}`} - {account.channelUrl ? {account.channelUrl} : null} -
- - {account.lastError ? ( -
- {account.lastError} -
- ) : null} -
- -
-
- - 账号归属 -
- - -
- {!account.isDefault ? ( - - ) : null} - - -
-
-
-
- ); - })} -
-
- ); - })} -
-
- ) : !loading ? ( -
-
暂无已配置频道
-
- 从下方支持的频道中任选一个开始配置,系统会自动生成默认账号并进入统一管理视图。 -
-
- ) : null} - -
-
-
-

- 支持的频道 -

-
- 优先展示 ClawX 的主频道矩阵;未展示在这里的频道仍可通过配置弹窗 schema 接入。 -
-
-
- + {supportedChannels.length > 0 ? (
{supportedChannels.map((meta) => (
-
+ +
{meta.description}
+
开始配置 - {meta.connectionType === 'qr' ? '扫码' : meta.connectionType === 'webhook' ? 'Webhook' : 'Token'} + {getConnectionLabel(meta.connectionType)}
))}
-
+ ) : ( +
+ 暂无可配置的频道模块。 +
+ )}
{ setModalChannelType(value); + setModalAccountId(''); setModalValues({}); setModalError(null); }} @@ -1094,7 +270,9 @@ export default function ChannelsPage() { onConfirm={handleSaveModal} error={modalError} submitting={modalSubmitting} - confirmLabel={modalMode === 'edit-account' ? '保存修改' : modalMode === 'create-account' ? '新增账号' : '保存频道'} + title="配置频道" + description="选择一个支持的频道模块并填写接入信息。" + confirmLabel="保存频道" />
);