import { useEffect, useState } from 'react' import { useDataProvider, useGetList } from 'ra-core' import { Alert, Box, Button, Checkbox, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, InputAdornment, MenuItem, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from '@mui/material' import ContentCopyOutlined from '@mui/icons-material/ContentCopyOutlined' import CheckCircleOutlined from '@mui/icons-material/CheckCircleOutlined' import DeleteOutlined from '@mui/icons-material/DeleteOutlined' import ExpandMoreOutlined from '@mui/icons-material/ExpandMoreOutlined' import PlayArrowOutlined from '@mui/icons-material/PlayArrowOutlined' import ReportProblemOutlined from '@mui/icons-material/ReportProblemOutlined' import SecurityOutlined from '@mui/icons-material/SecurityOutlined' import StopOutlined from '@mui/icons-material/StopOutlined' import UpgradeOutlined from '@mui/icons-material/UpgradeOutlined' const aliasPattern = /^[a-z0-9][a-z0-9-]{0,31}$/ const platformOptions = ['windows', 'linux', 'macos'] const brandOptions = ['Chrome', 'Edge', 'Opera', 'Vivaldi'] const spoofingOptions = ['font', 'audio', 'canvas', 'clientrects', 'gpu'] const statusLabels = { created: '已创建', running: '运行中', exited: '已停止', restarting: '重启中', paused: '已暂停', missing: '未部署', } const scheduleReasonLabels = { account_revoked: '授权已撤销', account_paused: '账号已暂停', binding_missing: '绑定缺失', network_exit_missing: '固定出口缺失', network_exit_unhealthy: '固定出口不健康', runtime_stop_pending: '停止结果待确认', runtime_missing: '运行实例缺失', } const scheduleBlockLabel = runtime => runtime.schedule_status === 'ready' ? '' : (scheduleReasonLabels[runtime.schedule_block_reason] || runtime.schedule_block_reason || '不可调度') const emptyForm = { name: '', alias: '', gateway: '', image_version: '', account_id: '', network_exit_id: '', seed: '1000', platform: '', platform_version: '', brand: '', brand_version: '', hardware_concurrency: '', lang: '', accept_lang: '', timezone: '', disable_spoofing: [], } function buildFingerprint(form) { const fingerprint = { seed: Number(form.seed) } const optionalText = { platform: form.platform, platform_version: form.platform_version, brand: form.brand, brand_version: form.brand_version, lang: form.lang, accept_lang: form.accept_lang, timezone: form.timezone, } for (const [key, value] of Object.entries(optionalText)) { if (value !== '') fingerprint[key] = value } if (form.hardware_concurrency !== '') fingerprint.hardware_concurrency = Number(form.hardware_concurrency) if (form.disable_spoofing.length > 0) fingerprint.disable_spoofing = form.disable_spoofing.join(',') return fingerprint } function Status({ state }) { const label = Object.hasOwn(statusLabels, state) ? statusLabels[state] : undefined const color = state === 'running' ? 'success.main' : state === 'exited' ? 'warning.main' : 'text.secondary' return {label || '未知状态'}{label ? null : {state}} } function RuntimeActions({ runtime, busy, onAction }) { const running = runtime.state === 'running' const startBlockReason = runtime.schedule_block_reason === 'runtime_missing' ? '' : scheduleBlockLabel(runtime) return ( ) } function Copyable({ value }) { const copy = () => navigator.clipboard?.writeText(value) return ( {value} ) } function CreateForm({ gateways, images, accounts, networkExits, onSubmit, busy }) { const [form, setForm] = useState(emptyForm) const [advanced, setAdvanced] = useState(false) const enabledImages = images.filter(image => image.enabled) const availableAccounts = accounts.filter(account => account.authorization_status === 'authorized' && account.runtime_status === 'paused') const healthyExits = networkExits.filter(exit => exit.health_status === 'healthy') const defaultAccountID = availableAccounts[0]?.id ?? '' const defaultExitID = healthyExits[0]?.id ?? '' const update = (key, value) => setForm(current => ({ ...current, [key]: value })) const toggleSpoofing = option => setForm(current => ({ ...current, disable_spoofing: current.disable_spoofing.includes(option) ? current.disable_spoofing.filter(item => item !== option) : [...current.disable_spoofing, option], })) useEffect(() => { if (form.gateway === '' && gateways.length > 0) update('gateway', gateways[0].name) }, [gateways, form.gateway]) useEffect(() => { if (form.image_version === '' && enabledImages.length > 0) update('image_version', enabledImages[0].version) }, [enabledImages, form.image_version]) useEffect(() => { if (form.account_id === '' && defaultAccountID !== '') update('account_id', defaultAccountID) }, [defaultAccountID, form.account_id]) useEffect(() => { if (form.network_exit_id === '' && defaultExitID !== '') update('network_exit_id', defaultExitID) }, [defaultExitID, form.network_exit_id]) const seedNumber = Number(form.seed) const valid = form.name.trim() !== '' && aliasPattern.test(form.alias) && form.gateway !== '' && form.image_version !== '' && form.account_id !== '' && form.network_exit_id !== '' && Number.isInteger(seedNumber) && seedNumber >= 1 && seedNumber <= 2147483647 const submit = async event => { event.preventDefault() if (!valid) return const created = await onSubmit({ alias: form.alias, name: form.name.trim(), gateway: form.gateway, image_version: form.image_version, account_id: form.account_id, network_exit_id: form.network_exit_id, fingerprint: buildFingerprint(form), }) if (created) setForm(current => ({ ...emptyForm, gateway: current.gateway, image_version: current.image_version, account_id: current.account_id, network_exit_id: current.network_exit_id })) } const label = (htmlFor, text, required = false) => ( {text} {required ? : null} ) return ( {label('env-name', '环境名称', true)} update('name', event.target.value)} placeholder="支持中文,如:店铺一号" /> {label('env-alias', '别名(容器标识)', true)} update('alias', event.target.value)} placeholder="如:shop-01" helperText="用于容器名、数据卷与 API 路径" /> {label('env-gateway', '网关', true)} update('gateway', event.target.value)} disabled={gateways.length === 0} helperText={gateways.length === 0 ? '请先在「网关管理」注册网关' : ' '}> {gateways.map(gateway => {gateway.name})} {label('env-version', '镜像版本', true)} update('image_version', event.target.value)} disabled={enabledImages.length === 0} helperText={enabledImages.length === 0 ? '请先在「镜像版本」添加并启用' : ' '}> {enabledImages.map(image => {image.version})} {label('env-seed', 'Fingerprint Seed', true)} update('seed', event.target.value)} /> {label('env-account', '社媒账号', true)} update('account_id', event.target.value)} disabled={availableAccounts.length === 0} helperText={availableAccounts.length === 0 ? '请先准备已授权且暂停的账号' : ' '}> {availableAccounts.map(account => {account.id} · {account.platform})} {label('env-exit', '网络出口', true)} update('network_exit_id', event.target.value)} disabled={healthyExits.length === 0} helperText={healthyExits.length === 0 ? '请先检查并启用健康出口' : ' '}> {healthyExits.map(exit => {exit.id} · {exit.protocol}://{exit.host}:{exit.port})} update('platform', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '操作系统 platform' } }}> 默认 {platformOptions.map(option => {option})} update('platform_version', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '系统版本 platform-version' } }} placeholder="如 11.0.0" /> update('brand', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '浏览器品牌 brand' } }}> 默认 Chromium {brandOptions.map(option => {option})} update('brand_version', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '品牌版本 brand-version' } }} placeholder="如 132.0.6834.159" /> update('hardware_concurrency', event.target.value)} slotProps={{ htmlInput: { 'aria-label': 'CPU 核心数 hardware-concurrency', min: 1, max: 128 } }} placeholder="留空由种子生成" /> update('lang', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '语言 lang' } }} placeholder="如 zh-CN" /> update('accept_lang', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '接受语言 accept-lang' } }} placeholder="如 zh-CN,en-US" /> update('timezone', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '时区 timezone' } }} placeholder="如 Asia/Shanghai" /> 禁用指纹伪装 disable-spoofing {spoofingOptions.map(option => ( toggleSpoofing(option)} slotProps={{ input: { 'aria-label': `禁用伪装 ${option}` } }} />} label={option} /> ))} ) } function BindingReadiness({ runtime }) { const blockReason = scheduleBlockLabel(runtime) return {blockReason ? : }{blockReason || '可调度'} } function UpgradeDialog({ target, images, busy, onClose, onConfirm }) { const [version, setVersion] = useState('') const enabledImages = images.filter(image => image.enabled) useEffect(() => { setVersion('') }, [target]) return ( 升级 {target ? `${target.name}(${target.alias})` : ''} 容器将按新镜像重建,Profile 数据卷与指纹参数保持不变。 setVersion(event.target.value)} slotProps={{ htmlInput: { 'aria-label': '目标镜像版本' } }}> {enabledImages.map(image => {image.version}{image.version === target?.image_version ? '(当前)' : ''})} ) } function RuntimeTable({ runtimes, busy, onAction }) { return ( 环境状态固定绑定CDP操作 {runtimes.map(runtime => ( {runtime.name}{runtime.alias} · seed {runtime.fingerprint?.seed} {runtime.account_id || '未绑定账号'}{runtime.network_exit_id || '未绑定出口'} · {runtime.image_version} {runtime.endpoint ? : —} ))}
) } function RuntimeCards({ runtimes, busy, onAction }) { return {runtimes.map(runtime => {runtime.name}{runtime.alias} · {runtime.image_version} · seed {runtime.fingerprint?.seed}账号:{runtime.account_id || '未绑定'} · 固定出口:{runtime.network_exit_id || '未绑定'}{runtime.endpoint ? : null})} } export function BrowserList() { const dataProvider = useDataProvider() const [busy, setBusy] = useState('') const [localError, setLocalError] = useState('') const [upgradeTarget, setUpgradeTarget] = useState(null) const { data: runtimes = [], error, isPending, refetch } = useGetList('browsers', {}, { refetchInterval: 3000 }) const { data: gateways = [] } = useGetList('gateways') const { data: images = [] } = useGetList('browser-images') const { data: accounts = [] } = useGetList('accounts') const { data: networkExits = [] } = useGetList('network-exits') useEffect(() => { document.title = 'CreatorHub · 运行环境' }, []) async function createRuntime(data) { setBusy('create') setLocalError('') try { await dataProvider.create('browsers', { data }) await refetch() return true } catch (reason) { setLocalError(reason.status === 409 ? '冲突(409):账号或运行环境已有活动绑定;表单内容已保留。' : reason.status === 503 ? `环境不可用(503):${reason.message}` : reason.message) return false } finally { setBusy('') } } async function act(runtime, action) { if (action === 'recycle' && !window.confirm(`回收 ${runtime.name}(${runtime.alias})?容器将被删除,Profile 数据卷保留。`)) return if (action === 'upgrade') { setUpgradeTarget(runtime) return } await runAction(runtime, action) } async function runAction(runtime, action, extra) { setBusy(runtime.alias) setLocalError('') try { await dataProvider.browserAction(runtime.alias, action, extra) await refetch() } catch (reason) { setLocalError(reason.message) return reason } finally { setBusy('') } } async function confirmUpgrade(version) { const target = upgradeTarget const failure = await runAction(target, 'upgrade', { version }) if (!failure) setUpgradeTarget(null) } const message = localError || error?.message return ( <> 运行环境启动、停止、升级并回收隔离的指纹浏览器 {message ? { setLocalError(''); refetch() }}>重试} sx={{ mb: 2.5 }}>{message} : null} {isPending ? : null} {!isPending && runtimes.length === 0 ? 暂无运行环境,创建第一个隔离 Profile。 : null} {!isPending && runtimes.length > 0 ? <> : null} setUpgradeTarget(null)} onConfirm={confirmUpgrade} /> Docker socket 仅由受限网关访问,镜像与参数由平台下发 ) }