'use client' import Link from 'next/link' import { useCallback, useEffect, useMemo, useState } from 'react' import { ArrowLeft, Bot, CheckCircle2, Eye, EyeOff, Info, Loader2, RefreshCw, Save, Trash2, XCircle, } from 'lucide-react' import { toast } from 'sonner' // PLATFORM:TYPE_UNION:START type Platform = 'discord' | 'dingtalk' | 'lark' | 'telegram' | 'slack' // PLATFORM:TYPE_UNION:END type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' type CSSVarName = | '--background' | '--foreground' | '--card' | '--card-foreground' | '--primary' | '--primary-foreground' | '--muted' | '--muted-foreground' | '--border' | '--input' | '--success' | '--warning' | '--destructive' const themeVar = (name: CSSVarName) => `var(${name})` interface RemoteControlConfig { // PLATFORM:DINGTALK:CONFIG_INTERFACE:START dingtalk: { enabled: boolean clientId: string clientSecret: string } // PLATFORM:DINGTALK:CONFIG_INTERFACE:END // PLATFORM:DISCORD:CONFIG_INTERFACE:START discord: { enabled: boolean botToken: string } // PLATFORM:DISCORD:CONFIG_INTERFACE:END // PLATFORM:LARK:CONFIG_INTERFACE:START lark: { enabled: boolean appId: string appSecret: string } // PLATFORM:LARK:CONFIG_INTERFACE:END // PLATFORM:TELEGRAM:CONFIG_INTERFACE:START telegram: { enabled: boolean botToken: string } // PLATFORM:TELEGRAM:CONFIG_INTERFACE:END // PLATFORM:SLACK:CONFIG_INTERFACE:START slack: { enabled: boolean botToken: string appToken: string } // PLATFORM:SLACK:CONFIG_INTERFACE:END } interface PlatformStatus { platform?: Platform status: ConnectionStatus messagesProcessed?: number activeSessions?: number lastConnectedAt?: string uptime?: number error?: string } interface LogsResponse { logs: LogEntry[] } interface LogEntry { id?: string timestamp: string platform: Platform eventType: string severity: 'info' | 'warning' | 'error' details?: string | Record message?: string } interface AgentInfoResponse { agentId: string baseUrl: string stats?: { totalMessages?: number activeConnections?: number avgResponseTime?: number } totalMessages?: number activeConnections?: number averageResponseTime?: number } const DEFAULT_CONFIG: RemoteControlConfig = { // PLATFORM:DINGTALK:DEFAULT_CONFIG:START dingtalk: { enabled: false, clientId: '', clientSecret: '' }, // PLATFORM:DINGTALK:DEFAULT_CONFIG:END // PLATFORM:DISCORD:DEFAULT_CONFIG:START discord: { enabled: false, botToken: '' }, // PLATFORM:DISCORD:DEFAULT_CONFIG:END // PLATFORM:LARK:DEFAULT_CONFIG:START lark: { enabled: false, appId: '', appSecret: '' }, // PLATFORM:LARK:DEFAULT_CONFIG:END // PLATFORM:TELEGRAM:DEFAULT_CONFIG:START telegram: { enabled: false, botToken: '' }, // PLATFORM:TELEGRAM:DEFAULT_CONFIG:END // PLATFORM:SLACK:DEFAULT_CONFIG:START slack: { enabled: false, botToken: '', appToken: '' }, // PLATFORM:SLACK:DEFAULT_CONFIG:END } const DEFAULT_STATUS: Record = { // PLATFORM:DINGTALK:DEFAULT_STATUS:START dingtalk: { platform: 'dingtalk', status: 'disconnected', messagesProcessed: 0, activeSessions: 0 }, // PLATFORM:DINGTALK:DEFAULT_STATUS:END // PLATFORM:DISCORD:DEFAULT_STATUS:START discord: { platform: 'discord', status: 'disconnected', messagesProcessed: 0, activeSessions: 0 }, // PLATFORM:DISCORD:DEFAULT_STATUS:END // PLATFORM:LARK:DEFAULT_STATUS:START lark: { platform: 'lark', status: 'disconnected', messagesProcessed: 0, activeSessions: 0 }, // PLATFORM:LARK:DEFAULT_STATUS:END // PLATFORM:TELEGRAM:DEFAULT_STATUS:START telegram: { platform: 'telegram', status: 'disconnected', messagesProcessed: 0, activeSessions: 0 }, // PLATFORM:TELEGRAM:DEFAULT_STATUS:END // PLATFORM:SLACK:DEFAULT_STATUS:START slack: { platform: 'slack', status: 'disconnected', messagesProcessed: 0, activeSessions: 0 }, // PLATFORM:SLACK:DEFAULT_STATUS:END } function statusMeta(status: ConnectionStatus) { if (status === 'connected') { return { label: '已连接', dot: 'var(--success)' } } if (status === 'connecting') { return { label: '连接中...', dot: 'var(--warning)' } } return { label: '断开连接', dot: 'var(--destructive)' } } function formatDate(value?: string) { if (!value) return '-' const date = new Date(value) if (Number.isNaN(date.getTime())) return value return date.toLocaleString('zh-CN', { hour12: false }) } function formatDetails(log: LogEntry) { if (typeof log.details === 'string') return log.details if (log.message) return log.message if (log.details) return JSON.stringify(log.details) return '-' } function formatDuration(seconds?: number) { if (!seconds || seconds <= 0) return '-' const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) const s = seconds % 60 if (h > 0) return `${h}小时${m}分` if (m > 0) return `${m}分${s}秒` return `${s}秒` } export default function RemoteControlPage() { const [config, setConfig] = useState(DEFAULT_CONFIG) const [status, setStatus] = useState>(DEFAULT_STATUS) const [logs, setLogs] = useState([]) const [agentInfo, setAgentInfo] = useState(null) // PLATFORM:SHOW_SECRETS:START const [showSecrets, setShowSecrets] = useState({ // PLATFORM:DINGTALK:SHOW_SECRETS:START dingtalkSecret: false, // PLATFORM:DINGTALK:SHOW_SECRETS:END // PLATFORM:DISCORD:SHOW_SECRETS:START discordToken: false, // PLATFORM:DISCORD:SHOW_SECRETS:END // PLATFORM:LARK:SHOW_SECRETS:START larkSecret: false, // PLATFORM:LARK:SHOW_SECRETS:END // PLATFORM:TELEGRAM:SHOW_SECRETS:START telegramToken: false, // PLATFORM:TELEGRAM:SHOW_SECRETS:END // PLATFORM:SLACK:SHOW_SECRETS:START slackBotToken: false, slackAppToken: false, // PLATFORM:SLACK:SHOW_SECRETS:END }) // PLATFORM:SHOW_SECRETS:END const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) // PLATFORM:TESTING:START const [testing, setTesting] = useState>({ // PLATFORM:DINGTALK:TESTING:START dingtalk: false, // PLATFORM:DINGTALK:TESTING:END // PLATFORM:DISCORD:TESTING:START discord: false, // PLATFORM:DISCORD:TESTING:END // PLATFORM:LARK:TESTING:START lark: false, // PLATFORM:LARK:TESTING:END // PLATFORM:TELEGRAM:TESTING:START telegram: false, // PLATFORM:TELEGRAM:TESTING:END // PLATFORM:SLACK:TESTING:START slack: false, // PLATFORM:SLACK:TESTING:END }) // PLATFORM:TESTING:END const [refreshingLogs, setRefreshingLogs] = useState(false) const [clearingLogs, setClearingLogs] = useState(false) const loadStatus = useCallback(async () => { const response = await fetch('/api/remote-control/status', { cache: 'no-store' }) if (!response.ok) { throw new Error('状态加载失败') } const data = (await response.json()) as Partial> setStatus({ // PLATFORM:DINGTALK:LOAD_STATUS:START dingtalk: { ...DEFAULT_STATUS.dingtalk, ...(data.dingtalk ?? {}) }, // PLATFORM:DINGTALK:LOAD_STATUS:END // PLATFORM:DISCORD:LOAD_STATUS:START discord: { ...DEFAULT_STATUS.discord, ...(data.discord ?? {}) }, // PLATFORM:DISCORD:LOAD_STATUS:END // PLATFORM:LARK:LOAD_STATUS:START lark: { ...DEFAULT_STATUS.lark, ...(data.lark ?? {}) }, // PLATFORM:LARK:LOAD_STATUS:END // PLATFORM:TELEGRAM:LOAD_STATUS:START telegram: { ...DEFAULT_STATUS.telegram, ...(data.telegram ?? {}) }, // PLATFORM:TELEGRAM:LOAD_STATUS:END // PLATFORM:SLACK:LOAD_STATUS:START slack: { ...DEFAULT_STATUS.slack, ...(data.slack ?? {}) }, // PLATFORM:SLACK:LOAD_STATUS:END }) }, []) const loadLogs = useCallback(async () => { const response = await fetch('/api/remote-control/logs?limit=50', { cache: 'no-store' }) if (!response.ok) { throw new Error('日志加载失败') } const data = (await response.json()) as LogsResponse setLogs(Array.isArray(data.logs) ? data.logs : []) }, []) const loadAgentInfo = useCallback(async () => { const response = await fetch('/api/remote-control/agent-info', { cache: 'no-store' }) if (!response.ok) { throw new Error('Agent 信息加载失败') } const data = (await response.json()) as AgentInfoResponse setAgentInfo(data) }, []) const loadInitialData = useCallback(async () => { setLoading(true) try { const configResponse = await fetch('/api/remote-control/config', { cache: 'no-store' }) if (!configResponse.ok) { throw new Error('配置加载失败') } const configData = (await configResponse.json()) as Partial setConfig({ // PLATFORM:DINGTALK:LOAD_INITIAL:START dingtalk: { ...DEFAULT_CONFIG.dingtalk, ...(configData.dingtalk ?? {}) }, // PLATFORM:DINGTALK:LOAD_INITIAL:END // PLATFORM:DISCORD:LOAD_INITIAL:START discord: { ...DEFAULT_CONFIG.discord, ...(configData.discord ?? {}) }, // PLATFORM:DISCORD:LOAD_INITIAL:END // PLATFORM:LARK:LOAD_INITIAL:START lark: { ...DEFAULT_CONFIG.lark, ...(configData.lark ?? {}) }, // PLATFORM:LARK:LOAD_INITIAL:END // PLATFORM:TELEGRAM:LOAD_INITIAL:START telegram: { ...DEFAULT_CONFIG.telegram, ...(configData.telegram ?? {}) }, // PLATFORM:TELEGRAM:LOAD_INITIAL:END // PLATFORM:SLACK:LOAD_INITIAL:START slack: { ...DEFAULT_CONFIG.slack, ...(configData.slack ?? {}) }, // PLATFORM:SLACK:LOAD_INITIAL:END }) await Promise.all([loadStatus(), loadLogs(), loadAgentInfo()]) } catch { toast.error('加载远程控制配置失败') } finally { setLoading(false) } }, [loadAgentInfo, loadLogs, loadStatus]) useEffect(() => { void loadInitialData() }, [loadInitialData]) const saveConfig = async () => { setSaving(true) try { const response = await fetch('/api/remote-control/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config), }) const data = (await response.json()) as { success?: boolean; message?: string; error?: string } if (!response.ok || !data.success) { throw new Error(data.error || '保存失败') } toast.success(data.message || '配置已保存并生效') window.setTimeout(() => { void Promise.all([loadStatus(), loadLogs(), loadAgentInfo()]) }, 3000) } catch (error) { const message = error instanceof Error ? error.message : '保存失败' toast.error(message) } finally { setSaving(false) } } const testConnection = async (platform: Platform) => { setTesting(prev => ({ ...prev, [platform]: true })) try { const response = await fetch('/api/remote-control/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ platform }), }) const data = (await response.json()) as { success?: boolean; message?: string; error?: string } if (!response.ok || !data.success) { throw new Error(data.error || data.message || '连接测试失败') } // PLATFORM:TEST_CONNECTION_SUCCESS:START const platformNames: Record = { discord: 'Discord', dingtalk: '钉钉', lark: '飞书', telegram: 'Telegram', slack: 'Slack', } toast.success(`${platformNames[platform]}连接测试成功`) // PLATFORM:TEST_CONNECTION_SUCCESS:END await Promise.all([loadStatus(), loadLogs()]) } catch (error) { const message = error instanceof Error ? error.message : '连接测试失败' toast.error(message) await Promise.all([loadStatus(), loadLogs()]) } finally { setTesting(prev => ({ ...prev, [platform]: false })) } } const refreshLogs = async () => { setRefreshingLogs(true) try { await loadLogs() toast.success('日志已刷新') } catch { toast.error('刷新日志失败') } finally { setRefreshingLogs(false) } } const clearLogs = async () => { setClearingLogs(true) try { const response = await fetch('/api/remote-control/logs', { method: 'DELETE' }) if (!response.ok) { throw new Error('清空失败') } setLogs([]) toast.success('日志已清空') } catch { toast.error('清空日志失败') } finally { setClearingLogs(false) } } const mergedStats = useMemo(() => { const totalMessages = agentInfo?.stats?.totalMessages ?? agentInfo?.totalMessages ?? 0 const activeConnections = agentInfo?.stats?.activeConnections ?? agentInfo?.activeConnections ?? 0 const avgResponseTime = agentInfo?.stats?.avgResponseTime ?? agentInfo?.averageResponseTime ?? 0 return { totalMessages, activeConnections, avgResponseTime } }, [agentInfo]) if (loading) { return (
加载远程控制配置中...
) } return (
返回主页

远程控制配置

{/* PLATFORM:DINGTALK:CARD:START */} setConfig(prev => ({ ...prev, dingtalk: { ...prev.dingtalk, enabled } }))} fields={[ { label: 'Client ID', type: 'text', value: config.dingtalk.clientId, onChange: value => setConfig(prev => ({ ...prev, dingtalk: { ...prev.dingtalk, clientId: value } })), placeholder: '请输入钉钉 Client ID', }, { label: 'Client Secret', type: showSecrets.dingtalkSecret ? 'text' : 'password', value: config.dingtalk.clientSecret, onChange: value => setConfig(prev => ({ ...prev, dingtalk: { ...prev.dingtalk, clientSecret: value } })), placeholder: '请输入钉钉 Client Secret', secretVisible: showSecrets.dingtalkSecret, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, dingtalkSecret: !prev.dingtalkSecret })), }, ]} onTest={() => void testConnection('dingtalk')} testing={testing.dingtalk} docUrl="https://open.dingtalk.com/document/robots/custom-robot-access" /> {/* PLATFORM:DINGTALK:CARD:END */} {/* PLATFORM:DISCORD:CARD:START */} setConfig(prev => ({ ...prev, discord: { ...prev.discord, enabled } }))} fields={[ { label: 'Bot Token', type: showSecrets.discordToken ? 'text' : 'password', value: config.discord.botToken, onChange: value => setConfig(prev => ({ ...prev, discord: { ...prev.discord, botToken: value } })), placeholder: '请输入 Discord Bot Token', secretVisible: showSecrets.discordToken, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, discordToken: !prev.discordToken })), }, ]} onTest={() => void testConnection('discord')} testing={testing.discord} docUrl="https://docs.discord.com/developers/topics/oauth2#bots" /> {/* PLATFORM:DISCORD:CARD:END */} {/* PLATFORM:LARK:CARD:START */} setConfig(prev => ({ ...prev, lark: { ...prev.lark, enabled } }))} fields={[ { label: 'App ID', type: 'text', value: config.lark.appId, onChange: value => setConfig(prev => ({ ...prev, lark: { ...prev.lark, appId: value } })), placeholder: '请输入飞书 App ID', }, { label: 'App Secret', type: showSecrets.larkSecret ? 'text' : 'password', value: config.lark.appSecret, onChange: value => setConfig(prev => ({ ...prev, lark: { ...prev.lark, appSecret: value } })), placeholder: '请输入飞书 App Secret', secretVisible: showSecrets.larkSecret, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, larkSecret: !prev.larkSecret })), }, ]} onTest={() => void testConnection('lark')} testing={testing.lark} docUrl="https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process" /> {/* PLATFORM:LARK:CARD:END */} {/* PLATFORM:TELEGRAM:CARD:START */} setConfig(prev => ({ ...prev, telegram: { ...prev.telegram, enabled } }))} fields={[ { label: 'Bot Token', type: showSecrets.telegramToken ? 'text' : 'password', value: config.telegram.botToken, onChange: value => setConfig(prev => ({ ...prev, telegram: { ...prev.telegram, botToken: value } })), placeholder: '请输入 Telegram Bot Token (通过 @BotFather 获取)', secretVisible: showSecrets.telegramToken, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, telegramToken: !prev.telegramToken })), }, ]} onTest={() => void testConnection('telegram')} testing={testing.telegram} docUrl="https://core.telegram.org/bots#how-do-i-create-a-bot" /> {/* PLATFORM:TELEGRAM:CARD:END */} {/* PLATFORM:SLACK:CARD:START */} setConfig(prev => ({ ...prev, slack: { ...prev.slack, enabled } }))} fields={[ { label: 'Bot Token (xoxb-)', type: showSecrets.slackBotToken ? 'text' : 'password', value: config.slack.botToken, onChange: value => setConfig(prev => ({ ...prev, slack: { ...prev.slack, botToken: value } })), placeholder: '请输入 Slack Bot Token (xoxb-...)', secretVisible: showSecrets.slackBotToken, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, slackBotToken: !prev.slackBotToken })), }, { label: 'App-Level Token (xapp-)', type: showSecrets.slackAppToken ? 'text' : 'password', value: config.slack.appToken, onChange: value => setConfig(prev => ({ ...prev, slack: { ...prev.slack, appToken: value } })), placeholder: '请输入 Slack App Token (xapp-...)', secretVisible: showSecrets.slackAppToken, onToggleSecret: () => setShowSecrets(prev => ({ ...prev, slackAppToken: !prev.slackAppToken })), }, ]} onTest={() => void testConnection('slack')} testing={testing.slack} docUrl="https://api.slack.com/start/quickstart" /> {/* PLATFORM:SLACK:CARD:END */}

Bot 事件日志(最近 50 条)

{logs.length === 0 ? (
暂无日志数据
) : (
{logs.map((log, index) => (
{formatDate(log.timestamp)} {log.platform} {log.eventType} {formatDetails(log)}
))}
)}

Nova Agent 信息(只读)

Agent ID: {agentInfo?.agentId || '-'}
Base URL: {agentInfo?.baseUrl || '-'}
活跃连接数: {mergedStats.activeConnections}
消息总数: {mergedStats.totalMessages}
平均响应时间: {mergedStats.avgResponseTime} ms
) } interface PlatformField { label: string type: 'text' | 'password' value: string placeholder: string onChange: (value: string) => void secretVisible?: boolean onToggleSecret?: () => void } interface PlatformCardProps { title: string enabled: boolean onEnabledChange: (enabled: boolean) => void status: PlatformStatus fields: PlatformField[] onTest: () => void testing: boolean docUrl: string } function PlatformCard({ title, enabled, onEnabledChange, status, fields, onTest, testing, docUrl }: PlatformCardProps) { const meta = statusMeta(status.status) return (

{title}

{meta.label}
{fields.map(field => ( ))}
最后连接时间: {formatDate(status.lastConnectedAt)} 运行时长: {formatDuration(status.uptime)} 已处理消息数: {status.messagesProcessed ?? 0} 活跃会话数: {status.activeSessions ?? 0}
{status.error && (
{status.error}
)} {!status.error && status.status === 'connected' && (
连接状态正常
)}
) } function SeverityTag({ severity }: { severity: LogEntry['severity'] }) { if (severity === 'error') { return error } if (severity === 'warning') { return warning } return info }