初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,797 @@
'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<string, unknown>
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, PlatformStatus> = {
// 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<RemoteControlConfig>(DEFAULT_CONFIG)
const [status, setStatus] = useState<Record<Platform, PlatformStatus>>(DEFAULT_STATUS)
const [logs, setLogs] = useState<LogEntry[]>([])
const [agentInfo, setAgentInfo] = useState<AgentInfoResponse | null>(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<Record<Platform, boolean>>({
// 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<Record<Platform, PlatformStatus>>
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<RemoteControlConfig>
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<Platform, string> = {
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 (
<div className="fanling-theme min-h-screen flex items-center justify-center bg-background">
<div className="flex items-center gap-2 text-primary">
<Loader2 className="h-5 w-5 animate-spin" />
<span>...</span>
</div>
</div>
)
}
return (
<div className="fanling-theme min-h-screen px-4 py-6 md:px-6" style={{ backgroundColor: themeVar('--background') }}>
<div className="mx-auto w-full max-w-5xl space-y-5">
<div className="flex items-center justify-between rounded-2xl border bg-card px-4 py-3 shadow-sm" style={{ borderColor: themeVar('--border') }}>
<div className="flex items-center gap-3">
<Link
href="/"
className="inline-flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm transition-colors"
style={{ color: themeVar('--muted-foreground'), border: `1px solid ${themeVar('--border')}` }}
>
<ArrowLeft className="h-4 w-4" />
</Link>
<h1 className="text-lg font-semibold md:text-xl" style={{ color: themeVar('--foreground') }}>
</h1>
</div>
<Bot className="h-5 w-5" style={{ color: themeVar('--primary') }} />
</div>
{/* PLATFORM:DINGTALK:CARD:START */}
<PlatformCard
title="钉钉 Bot"
status={status.dingtalk}
enabled={config.dingtalk.enabled}
onEnabledChange={enabled => 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 */}
<PlatformCard
title="Discord Bot"
status={status.discord}
enabled={config.discord.enabled}
onEnabledChange={enabled => 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 */}
<PlatformCard
title="飞书 Bot"
status={status.lark}
enabled={config.lark.enabled}
onEnabledChange={enabled => 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 */}
<PlatformCard
title="Telegram Bot"
status={status.telegram}
enabled={config.telegram.enabled}
onEnabledChange={enabled => 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 */}
<PlatformCard
title="Slack Bot"
status={status.slack}
enabled={config.slack.enabled}
onEnabledChange={enabled => 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 */}
<section className="rounded-2xl border bg-card p-4 shadow-sm md:p-5" style={{ borderColor: themeVar('--border') }}>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="text-base font-semibold" style={{ color: themeVar('--foreground') }}>Bot 50 </h2>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => void refreshLogs()}
disabled={refreshingLogs}
className="inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm"
style={{ border: `1px solid ${themeVar('--border')}`, color: themeVar('--muted-foreground') }}
>
<RefreshCw className={`h-4 w-4 ${refreshingLogs ? 'animate-spin' : ''}`} />
</button>
<button
type="button"
onClick={() => void clearLogs()}
disabled={clearingLogs}
className="inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm text-white"
style={{ backgroundColor: themeVar('--destructive') }}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div className="max-h-[380px] overflow-auto rounded-xl border" style={{ borderColor: themeVar('--border') }}>
{logs.length === 0 ? (
<div className="p-4 text-sm" style={{ color: themeVar('--muted-foreground') }}></div>
) : (
<div className="divide-y" style={{ borderColor: themeVar('--border') }}>
{logs.map((log, index) => (
<div key={log.id ?? `${log.timestamp}-${index}`} className="grid gap-1 px-4 py-3 text-sm md:grid-cols-[168px_88px_120px_80px_1fr]">
<span style={{ color: themeVar('--muted-foreground') }}>{formatDate(log.timestamp)}</span>
<span className="font-medium" style={{ color: themeVar('--foreground') }}>{log.platform}</span>
<span style={{ color: themeVar('--muted-foreground') }}>{log.eventType}</span>
<span>
<SeverityTag severity={log.severity} />
</span>
<span className="break-all" style={{ color: themeVar('--card-foreground') }}>{formatDetails(log)}</span>
</div>
))}
</div>
)}
</div>
</section>
<section className="rounded-2xl border bg-card p-4 shadow-sm md:p-5" style={{ borderColor: themeVar('--border') }}>
<h2 className="mb-3 text-base font-semibold" style={{ color: themeVar('--foreground') }}>Nova Agent </h2>
<div className="grid gap-3 text-sm md:grid-cols-2" style={{ color: themeVar('--card-foreground') }}>
<div className="rounded-xl border p-3" style={{ borderColor: themeVar('--border') }}>
<div>Agent ID: <span className="font-medium">{agentInfo?.agentId || '-'}</span></div>
<div className="mt-1 break-all">Base URL: <span className="font-medium">{agentInfo?.baseUrl || '-'}</span></div>
</div>
<div className="rounded-xl border p-3" style={{ borderColor: themeVar('--border') }}>
<div>: <span className="font-medium">{mergedStats.activeConnections}</span></div>
<div className="mt-1">: <span className="font-medium">{mergedStats.totalMessages}</span></div>
<div className="mt-1">: <span className="font-medium">{mergedStats.avgResponseTime} ms</span></div>
</div>
</div>
</section>
<div className="sticky bottom-4 z-10 flex justify-end">
<button
type="button"
onClick={() => void saveConfig()}
disabled={saving}
className="inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-medium text-white shadow-sm transition-colors"
style={{ backgroundColor: themeVar('--primary') }}
>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
</button>
</div>
</div>
</div>
)
}
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 (
<section className="rounded-2xl border bg-card p-4 shadow-sm md:p-5" style={{ borderColor: themeVar('--border') }}>
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<h2 className="text-base font-semibold" style={{ color: themeVar('--foreground') }}>{title}</h2>
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex rounded-full p-1"
title="查看官方配置文档"
style={{ color: themeVar('--muted-foreground') }}
>
<Info className="h-4 w-4" />
</a>
</div>
<div className="flex items-center gap-4">
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => onEnabledChange(!enabled)}
className="inline-flex items-center gap-2 text-sm"
style={{ color: themeVar('--card-foreground') }}
>
<span
className="relative h-6 w-11 rounded-full transition-colors"
style={{ backgroundColor: enabled ? themeVar('--primary') : themeVar('--muted') }}
>
<span
className="absolute top-0.5 h-5 w-5 rounded-full transition-all"
style={{ backgroundColor: themeVar('--card'), left: enabled ? '22px' : '2px' }}
/>
</span>
{enabled ? '已启用' : '已禁用'}
</button>
<div className="flex items-center gap-2 text-sm" style={{ color: themeVar('--card-foreground') }}>
<span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: meta.dot }} />
{meta.label}
</div>
</div>
</div>
<div className="space-y-3">
{fields.map(field => (
<label key={field.label} className="block text-sm">
<span className="mb-1.5 block" style={{ color: themeVar('--card-foreground') }}>{field.label}</span>
<div className="relative">
<input
type={field.type}
value={field.value}
onChange={event => field.onChange(event.target.value)}
placeholder={field.placeholder}
className="h-10 w-full rounded-lg border px-3 pr-10 text-sm outline-none transition-colors focus:border-primary"
style={{ borderColor: themeVar('--border'), color: themeVar('--foreground'), backgroundColor: themeVar('--card') }}
/>
{field.onToggleSecret && (
<button
type="button"
onClick={field.onToggleSecret}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1"
style={{ color: themeVar('--muted-foreground') }}
aria-label={field.secretVisible ? '隐藏密钥' : '显示密钥'}
>
{field.secretVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
)}
</div>
</label>
))}
</div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<div className="grid gap-1 text-sm md:grid-cols-2 md:gap-x-6" style={{ color: themeVar('--muted-foreground') }}>
<span>: {formatDate(status.lastConnectedAt)}</span>
<span>: {formatDuration(status.uptime)}</span>
<span>: {status.messagesProcessed ?? 0}</span>
<span>: {status.activeSessions ?? 0}</span>
</div>
<button
type="button"
onClick={onTest}
disabled={testing || !enabled}
className="inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
style={{ backgroundColor: themeVar('--primary') }}
title={!enabled ? '请先启用该渠道' : undefined}
>
{testing ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
</button>
</div>
{status.error && (
<div className="mt-3 inline-flex items-center gap-1 text-sm" style={{ color: themeVar('--destructive') }}>
<XCircle className="h-4 w-4" />
{status.error}
</div>
)}
{!status.error && status.status === 'connected' && (
<div className="mt-3 inline-flex items-center gap-1 text-sm" style={{ color: themeVar('--success') }}>
<CheckCircle2 className="h-4 w-4" />
</div>
)}
</section>
)
}
function SeverityTag({ severity }: { severity: LogEntry['severity'] }) {
if (severity === 'error') {
return <span className="rounded-full px-2 py-0.5 text-xs text-white" style={{ backgroundColor: themeVar('--destructive') }}>error</span>
}
if (severity === 'warning') {
return <span className="rounded-full px-2 py-0.5 text-xs" style={{ backgroundColor: themeVar('--warning'), color: themeVar('--foreground') }}>warning</span>
}
return <span className="rounded-full px-2 py-0.5 text-xs" style={{ backgroundColor: themeVar('--success'), color: themeVar('--foreground') }}>info</span>
}