'use client'; import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import { CheckCircle2, XCircle, AlertCircle, RefreshCw, Server, Cpu, Radio, Key, Loader2, Settings2, ScrollText, QrCode, PlugZap, } from 'lucide-react'; import { getChannelConfig, getChannelConnectorSession, getStatus, listChannelConnections, listChannelConnectors, listChannelEvents, restartRuntime, startChannelConnectorSession, updateAgentConfig, updateChannelConfig, updateProviderConfig, } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import type { ChannelConfigDetail, ChannelConnectorDescriptor, ChannelConnectionView, ChannelEventRecord, ChannelStatus, ConnectorSessionResponse, ProviderStatus, SystemStatus, } from '@/types'; import { AppLocale, pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { connectorChannelForKind, visibleConnectorCards } from '@/lib/channel-connector-state'; type ProviderFormState = { enabled: boolean; model: string; apiKey: string; apiBase: string; requestTimeoutSeconds: string; }; type AgentFormState = { maxTokens: string; temperature: string; maxToolIterations: string; }; type ChannelFormState = { enabled: boolean; kind: string; mode: string; accountId: string; displayName: string; botToken: string; appId: string; appSecret: string; clientSecret: string; token: string; domain: string; connectionMode: string; botUsername: string; botOpenId: string; webhookUrl: string; webhookSecret: string; requireMentionInGroups: boolean; allowFrom: string; groupAllowFrom: string; dmPolicy: string; groupPolicy: string; markdownSupport: boolean; baseUrl: string; cdnBaseUrl: string; maxMessageChars: string; textBatchDelaySeconds: string; }; const EMPTY_CHANNEL_FORM: ChannelFormState = { enabled: false, kind: 'telegram', mode: 'polling', accountId: '', displayName: '', botToken: '', appId: '', appSecret: '', clientSecret: '', token: '', domain: 'feishu', connectionMode: 'websocket', botUsername: '', botOpenId: '', webhookUrl: '', webhookSecret: '', requireMentionInGroups: true, allowFrom: '', groupAllowFrom: '', dmPolicy: 'open', groupPolicy: 'allowlist', markdownSupport: false, baseUrl: '', cdnBaseUrl: '', maxMessageChars: '', textBatchDelaySeconds: '', }; const CONFIGURABLE_CHANNEL_KINDS = new Set(['telegram', 'feishu', 'qqbot', 'weixin']); const SESSION_CONNECTOR_KINDS = new Set(['weixin', 'feishu']); const VISIBLE_PROVIDER_IDS = new Set(['openai', 'deepseek', 'dashscope', 'vllm']); const LOCAL_CONNECTOR_KINDS = new Set(['terminal']); type ConnectorWizardForm = { kind: string; displayName: string; domain: string; mode: 'create' | 'link'; appId: string; appSecret: string; verificationToken: string; requireMentionInGroups: boolean; respondToMentionAll: boolean; dmMode: 'open' | 'allowlist' | 'pair' | 'disabled'; allowFrom: string; groupAllowFrom: string; maxMessageChars: string; }; const EMPTY_CONNECTOR_WIZARD: ConnectorWizardForm = { kind: '', displayName: '', domain: 'feishu', mode: 'create', appId: '', appSecret: '', verificationToken: '', requireMentionInGroups: true, respondToMentionAll: false, dmMode: 'open', allowFrom: '', groupAllowFrom: '', maxMessageChars: '20000', }; export default function StatusPage() { const { locale } = useAppI18n(); const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [selectedProvider, setSelectedProvider] = useState(null); const [providerForm, setProviderForm] = useState(() => ({ enabled: false, model: '', apiKey: '', apiBase: '', requestTimeoutSeconds: '', })); const [savingProvider, setSavingProvider] = useState(false); const [providerError, setProviderError] = useState(null); const [agentForm, setAgentForm] = useState(() => ({ maxTokens: '', temperature: '0.2', maxToolIterations: '30', })); const [savingAgent, setSavingAgent] = useState(false); const [agentError, setAgentError] = useState(null); const [selectedChannel, setSelectedChannel] = useState(null); const [channelConfig, setChannelConfig] = useState(null); const [channelForm, setChannelForm] = useState(() => ({ ...EMPTY_CHANNEL_FORM })); const [channelEvents, setChannelEvents] = useState([]); const [loadingChannelConfig, setLoadingChannelConfig] = useState(false); const [loadingChannelEvents, setLoadingChannelEvents] = useState(false); const [savingChannel, setSavingChannel] = useState(false); const [channelError, setChannelError] = useState(null); const [channelRestartRequired, setChannelRestartRequired] = useState(false); const [restartOpen, setRestartOpen] = useState(false); const [restarting, setRestarting] = useState(false); const [restartError, setRestartError] = useState(null); const [connectors, setConnectors] = useState([]); const [channelConnections, setChannelConnections] = useState([]); const [loadingConnectors, setLoadingConnectors] = useState(false); const [connectorDialogOpen, setConnectorDialogOpen] = useState(false); const [connectorForm, setConnectorForm] = useState(() => ({ ...EMPTY_CONNECTOR_WIZARD })); const [connectorSession, setConnectorSession] = useState(null); const [startingConnector, setStartingConnector] = useState(false); const [pollingConnector, setPollingConnector] = useState(false); const [connectorError, setConnectorError] = useState(null); const loadStatus = async () => { setLoading(true); setError(null); try { const data = await getStatus(); setStatus(data); setAgentForm({ maxTokens: data.max_tokens == null ? '' : String(data.max_tokens), temperature: String(data.temperature), maxToolIterations: String(data.max_tool_iterations), }); } catch (err: any) { setError(err.message || pickAppText(locale, '连接后端失败', 'Failed to connect to the backend')); } finally { setLoading(false); } }; useEffect(() => { loadStatus(); }, []); const loadConnectors = async () => { setLoadingConnectors(true); try { setConnectors(await listChannelConnectors()); } catch { setConnectors([]); } finally { setLoadingConnectors(false); } }; const loadChannelConnections = async () => { try { setChannelConnections(await listChannelConnections()); } catch { setChannelConnections([]); } }; useEffect(() => { loadConnectors(); void loadChannelConnections(); }, []); useEffect(() => { const sessionId = connectorSession?.session.sessionId; const status = connectorSession?.session.status; if (!sessionId || connectorSessionDone(status)) return; const timer = window.setInterval(() => { void pollConnectorSession(sessionId); }, 2500); return () => window.clearInterval(timer); }, [connectorSession?.session.sessionId, connectorSession?.session.status]); const openProviderDialog = (provider: ProviderStatus) => { setSelectedProvider(provider); setProviderError(null); setProviderForm({ enabled: Boolean(provider.enabled || provider.has_key), model: status?.model || '', apiKey: '', apiBase: provider.api_base || provider.default_api_base || provider.detail || '', requestTimeoutSeconds: '', }); }; const handleSaveProvider = async () => { if (!selectedProvider) return; const providerId = selectedProvider.id || selectedProvider.name; setSavingProvider(true); setProviderError(null); try { const timeout = providerForm.requestTimeoutSeconds.trim() ? Number(providerForm.requestTimeoutSeconds.trim()) : undefined; if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) { throw new Error(pickAppText(locale, '请求超时必须是正数', 'Request timeout must be a positive number')); } await updateProviderConfig(providerId, { enabled: providerForm.enabled, model: providerForm.model.trim() || undefined, api_key: providerForm.apiKey.trim() || undefined, api_base: providerForm.apiBase.trim() || undefined, request_timeout_seconds: timeout, }); await loadStatus(); setSelectedProvider(null); } catch (err: any) { setProviderError(err.message || pickAppText(locale, '保存提供商配置失败', 'Failed to save provider settings')); } finally { setSavingProvider(false); } }; const handleSaveAgentConfig = async () => { setSavingAgent(true); setAgentError(null); try { const maxTokensText = agentForm.maxTokens.trim(); const maxTokens = maxTokensText ? Number(maxTokensText) : null; const temperature = Number(agentForm.temperature.trim()); const maxToolIterations = Number(agentForm.maxToolIterations.trim()); if ( maxTokens !== null && (!Number.isInteger(maxTokens) || maxTokens <= 0) ) { throw new Error(pickAppText(locale, '最大令牌数必须为空或正整数', 'Max tokens must be blank or a positive integer')); } if (!Number.isFinite(temperature) || temperature < 0 || temperature > 2) { throw new Error(pickAppText(locale, '温度必须在 0 到 2 之间', 'Temperature must be between 0 and 2')); } if (!Number.isInteger(maxToolIterations) || maxToolIterations < 0) { throw new Error(pickAppText(locale, '最大工具迭代次数必须是非负整数', 'Max tool iterations must be a non-negative integer')); } await updateAgentConfig({ max_tokens: maxTokens, temperature, max_tool_iterations: maxToolIterations, }); await loadStatus(); } catch (err: any) { setAgentError(err.message || pickAppText(locale, '保存智能体配置失败', 'Failed to save agent configuration')); } finally { setSavingAgent(false); } }; const openChannelDetails = async (channel: ChannelStatus) => { setSelectedChannel(channel); setChannelConfig(null); setChannelForm(channelFormFromStatus(channel)); setChannelError(null); setChannelRestartRequired(false); setChannelEvents([]); setLoadingChannelConfig(channel.kind !== 'terminal'); setLoadingChannelEvents(true); if (channel.kind !== 'terminal') { try { const config = await getChannelConfig(channel.channel_id); setChannelConfig(config); setChannelForm(channelFormFromConfig(config)); } catch (err: any) { setChannelError(err.message || pickAppText(locale, '加载通道配置失败', 'Failed to load channel configuration')); } finally { setLoadingChannelConfig(false); } } else { setLoadingChannelConfig(false); } try { setChannelEvents(await listChannelEvents(channel.channel_id, 20)); } catch { setChannelEvents([]); } finally { setLoadingChannelEvents(false); } }; const handleSaveChannel = async () => { if (!selectedChannel) return; setSavingChannel(true); setChannelError(null); try { const payload = channelPayloadFromForm(channelForm); const result = await updateChannelConfig(selectedChannel.channel_id, payload); setChannelConfig(result.channel); setChannelForm(channelFormFromConfig(result.channel)); setChannelRestartRequired(Boolean(result.restart_required)); await loadStatus(); } catch (err: any) { setChannelError(err.message || pickAppText(locale, '保存通道配置失败', 'Failed to save channel configuration')); } finally { setSavingChannel(false); } }; const handleRestart = async () => { setRestarting(true); setRestartError(null); try { await restartRuntime(); setRestartOpen(false); window.setTimeout(() => { void loadStatus(); }, 5000); } catch (err: any) { setRestartError(err.message || pickAppText(locale, '重启失败', 'Restart failed')); } finally { setRestarting(false); } }; const openConnectorDialog = (connector: ChannelConnectorDescriptor) => { const kind = connector.kind; setConnectorDialogOpen(true); setConnectorSession(null); setConnectorError(null); setConnectorForm({ ...EMPTY_CONNECTOR_WIZARD, kind, displayName: connectorDisplayName(connector), domain: kind === 'feishu' ? 'feishu' : '', }); }; const openLocalConnectorDetails = (kind: string, channel?: ChannelStatus) => { if (kind !== 'terminal') return; void openChannelDetails(channel || terminalFallbackChannel(locale)); }; const handleStartConnectorSession = async () => { if (!connectorForm.kind || !SESSION_CONNECTOR_KINDS.has(connectorForm.kind)) return; setStartingConnector(true); setConnectorError(null); try { const options: Record = {}; if (connectorForm.kind === 'feishu') { options.domain = connectorForm.domain || 'feishu'; options.mode = connectorForm.mode; options.requireMentionInGroups = connectorForm.requireMentionInGroups; options.respondToMentionAll = connectorForm.respondToMentionAll; options.dmMode = connectorForm.dmMode; const allowFrom = parseList(connectorForm.allowFrom); const groupAllowFrom = parseList(connectorForm.groupAllowFrom); if (allowFrom.length) options.allowFrom = allowFrom; if (groupAllowFrom.length) options.groupAllowFrom = groupAllowFrom; if (connectorForm.maxMessageChars.trim()) options.maxMessageChars = Number(connectorForm.maxMessageChars.trim()); if (connectorForm.appId.trim()) options.appId = connectorForm.appId.trim(); if (connectorForm.appSecret.trim()) options.appSecret = connectorForm.appSecret.trim(); if (connectorForm.verificationToken.trim()) options.verificationToken = connectorForm.verificationToken.trim(); } const response = await startChannelConnectorSession({ kind: connectorForm.kind, displayName: connectorForm.displayName.trim() || connectorDisplayName({ kind: connectorForm.kind }), options, }); setConnectorSession(response); if (response.session.status === 'connected') { await loadStatus(); await loadChannelConnections(); } if (!connectorSessionDone(response.session.status)) { window.setTimeout(() => { void pollConnectorSession(response.session.sessionId); }, 1000); } } catch (err: any) { setConnectorError(err.message || pickAppText(locale, '启动连接失败', 'Failed to start connector session')); } finally { setStartingConnector(false); } }; const pollConnectorSession = async (sessionId: string) => { setPollingConnector(true); try { const response = await getChannelConnectorSession(sessionId); setConnectorSession(response); if (response.session.status === 'connected') { await loadStatus(); await loadChannelConnections(); } } catch (err: any) { setConnectorError(err.message || pickAppText(locale, '刷新连接状态失败', 'Failed to refresh connector status')); } finally { setPollingConnector(false); } }; if (loading) { return (
); } if (error) { return (

{pickAppText(locale, '无法连接到 Boardware Agent Sandbox 后端', 'Unable to connect to the Boardware Agent Sandbox backend')}

{error}

{pickAppText(locale, '请确认后端服务已启动,并且当前页面可以访问它。', 'Please confirm the backend service is running and reachable from this page.')}

); } if (!status) return null; const visibleProviders = status.providers.filter(visibleProvider); const connectorCards = visibleConnectorCards(connectors); return (

{pickAppText(locale, '配置', 'Settings')}

{pickAppText( locale, '集中管理模型、工具、集成和实例运行状态。Task 和通知只在各自页面处理。', 'Manage models, tools, integrations, and instance runtime status. Tasks and notifications stay in their own pages.' )}

{/* System Info */} {pickAppText(locale, '实例运行', 'Instance runtime')}

{pickAppText(locale, '运行与调试', 'Runtime and debugging')}

{pickAppText(locale, '查看每次对话的运行日志和当前实例运行状态。', 'Inspect per-chat runtime logs and current instance status.')}

{status.runtime_controls?.self_restart !== false ? ( ) : null}
{/* Model Config */} {pickAppText(locale, '智能体配置', 'Agent configuration')}
setAgentForm((prev) => ({ ...prev, maxTokens: event.target.value }))} placeholder={pickAppText(locale, '模型默认', 'Model default')} />
setAgentForm((prev) => ({ ...prev, temperature: event.target.value }))} />
setAgentForm((prev) => ({ ...prev, maxToolIterations: event.target.value }))} />
{agentError || ''}
{/* Providers */} {pickAppText(locale, '提供商', 'Providers')}
{visibleProviders.map((p) => ( ))}
!open && setSelectedProvider(null)}> {pickAppText(locale, '配置提供商', 'Configure provider')} {selectedProvider ? ` · ${providerLabel(selectedProvider)}` : ''} {pickAppText(locale, '启用后会把它设为当前实例默认提供商。API Key 留空会保留已保存的值。', 'When enabled, this becomes the default provider for this instance. Leave API key empty to keep the saved value.')}

{pickAppText(locale, '关闭会从配置中移除这个提供商', 'Turning this off removes this provider from config')}

setProviderForm((prev) => ({ ...prev, enabled: checked }))} />
setProviderForm((prev) => ({ ...prev, model: event.target.value }))} placeholder="qwen-plus" disabled={!providerForm.enabled} />
setProviderForm((prev) => ({ ...prev, apiKey: event.target.value }))} placeholder={selectedProvider?.api_key_masked || pickAppText(locale, '留空保持不变', 'Leave blank to keep existing')} disabled={!providerForm.enabled || Boolean(selectedProvider?.is_oauth)} />
setProviderForm((prev) => ({ ...prev, apiBase: event.target.value }))} placeholder={selectedProvider?.default_api_base || 'https://api.example.com/v1'} disabled={!providerForm.enabled || Boolean(selectedProvider?.is_oauth)} />
setProviderForm((prev) => ({ ...prev, requestTimeoutSeconds: event.target.value }))} placeholder={pickAppText(locale, '默认', 'Default')} disabled={!providerForm.enabled} />
{providerError ? (

{providerError}

) : null}
!open && setSelectedChannel(null)}> {selectedChannel?.display_name || selectedChannel?.channel_id} {selectedChannel ? `${selectedChannel.kind}/${selectedChannel.mode} · ${selectedChannel.channel_id}` : ''} {selectedChannel ? (
{selectedChannel.kind === 'terminal' ? ( ) : null} {CONFIGURABLE_CHANNEL_KINDS.has(channelForm.kind) ? (

{pickAppText(locale, '连接配置', 'Connection settings')}

{pickAppText(locale, '凭据留空会保留已保存的值。保存后重启实例才会重新连接通道。', 'Leave credentials blank to keep saved values. Restart the instance after saving to reconnect channels.')}

setChannelForm((prev) => ({ ...prev, enabled: checked }))} disabled={loadingChannelConfig || savingChannel} />
setChannelForm((prev) => ({ ...prev, displayName: event.target.value }))} placeholder={selectedChannel.display_name || selectedChannel.channel_id} /> setChannelForm((prev) => ({ ...prev, accountId: event.target.value }))} placeholder="bot-main" />
{channelError ?

{channelError}

: null} {channelRestartRequired ? (
{pickAppText(locale, '配置已保存。重启实例后通道会按新配置启动。', 'Configuration saved. Restart the instance to start channels with the new settings.')}
) : null}
) : null}

{pickAppText(locale, '最近事件', 'Recent events')}

{loadingChannelEvents ? ( ) : (
{channelEvents.map((event) => (
{event.kind} {event.created_at}
{event.status}{event.error ? ` · ${event.error}` : ''}
))} {channelEvents.length === 0 ? (

{pickAppText(locale, '暂无事件', 'No events yet')}

) : null}
)}
) : null}
{pickAppText(locale, '重启实例?', 'Restart instance?')} {pickAppText( locale, '应用会短暂不可用,正在运行的对话和通道请求可能会中断。', 'The app will be unavailable briefly. Running chats and channel requests may be interrupted.' )} {restartError ?

{restartError}

: null}
{ setConnectorDialogOpen(open); if (!open) { setConnectorSession(null); setConnectorError(null); } }}> {connectorForm.kind ? connectorDisplayName({ kind: connectorForm.kind }) : pickAppText(locale, '连接通道', 'Connect channel')} {connectorForm.kind === 'weixin' ? pickAppText(locale, '使用扫码连接当前实例。', 'Connect this instance with QR login.') : connectorForm.kind === 'feishu' ? pickAppText(locale, '启动飞书/Lark 插件连接流程。', 'Start the Feishu/Lark plugin connection flow.') : pickAppText(locale, '选择连接方式。', 'Choose a connection method.')}
setConnectorForm((prev) => ({ ...prev, displayName: event.target.value }))} disabled={Boolean(connectorSession) || startingConnector} /> {connectorForm.kind === 'feishu' ? (
{!connectorSession ? (
{(connectorForm.mode === 'create' ? feishuCreateGuide(locale) : feishuLinkGuide(locale) ).map((item) => (

{item}

))}
) : null}

{pickAppText(locale, '群聊必须 @ Beaver', 'Require @ in groups')}

{pickAppText(locale, '默认开启,避免群聊所有消息触发智能体。', 'Enabled by default to avoid processing every group message.')}

setConnectorForm((prev) => ({ ...prev, requireMentionInGroups: checked }))} disabled={Boolean(connectorSession) || startingConnector} />

{pickAppText(locale, '响应 @所有人', 'Respond to @all')}

{pickAppText(locale, '默认关闭,避免群公告式消息触发。', 'Disabled by default to avoid broadcast-style triggers.')}

setConnectorForm((prev) => ({ ...prev, respondToMentionAll: checked }))} disabled={Boolean(connectorSession) || startingConnector} />
setConnectorForm((prev) => ({ ...prev, maxMessageChars: event.target.value }))} disabled={Boolean(connectorSession) || startingConnector} placeholder="20000" />