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, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from '@mui/material' import ContentCopyOutlined from '@mui/icons-material/ContentCopyOutlined' import DeleteOutlined from '@mui/icons-material/DeleteOutlined' import ExpandMoreOutlined from '@mui/icons-material/ExpandMoreOutlined' import PlayArrowOutlined from '@mui/icons-material/PlayArrowOutlined' 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 emptyForm = { name: '', alias: '', gateway: '', image_version: '', seed: '1000', platform: '', platform_version: '', brand: '', brand_version: '', hardware_concurrency: '', lang: '', accept_lang: '', timezone: '', proxy_server: '', disable_non_proxied_udp: false, 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, proxy_server: form.proxy_server, } 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_non_proxied_udp) fingerprint.disable_non_proxied_udp = true 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' return ( ) } function Copyable({ value }) { const copy = () => navigator.clipboard?.writeText(value) return ( {value} ) } function CreateForm({ gateways, images, onSubmit, busy }) { const [form, setForm] = useState(emptyForm) const [advanced, setAdvanced] = useState(false) const enabledImages = images.filter(image => image.enabled) 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]) const seedNumber = Number(form.seed) const valid = form.name.trim() !== '' && aliasPattern.test(form.alias) && form.gateway !== '' && form.image_version !== '' && Number.isInteger(seedNumber) && seedNumber >= 1 && seedNumber <= 2147483647 const submit = event => { event.preventDefault() if (!valid) return onSubmit({ alias: form.alias, name: form.name.trim(), gateway: form.gateway, image_version: form.image_version, fingerprint: buildFingerprint(form), }) setForm(current => ({ ...emptyForm, gateway: current.gateway, image_version: current.image_version })) } 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)} /> 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" /> update('proxy_server', event.target.value)} slotProps={{ htmlInput: { 'aria-label': '代理 proxy-server' } }} placeholder="如 socks5://127.0.0.1:1080" /> update('disable_non_proxied_udp', event.target.checked)} slotProps={{ input: { 'aria-label': '禁用非代理 UDP' } }} />} label="禁用非代理 UDP(WebRTC)" sx={{ gridColumn: { md: 'span 2' } }} /> 禁用指纹伪装 disable-spoofing {spoofingOptions.map(option => ( toggleSpoofing(option)} slotProps={{ input: { 'aria-label': `禁用伪装 ${option}` } }} />} label={option} /> ))} ) } 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.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.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') useEffect(() => { document.title = 'CreatorHub · 运行环境' }, []) async function createRuntime(data) { setBusy('create') setLocalError('') try { await dataProvider.create('browsers', { data }) await refetch() } catch (reason) { setLocalError(reason.message) } 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 仅由受限网关访问,镜像与参数由平台下发 ) }