import { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router'
import { useDataProvider, useList, useOne } from '@refinedev/core'
import {
Alert, Button, Card, CardContent, Copyable, DetailList, Field, Input, Modal, PageHeader,
PageState, Select, StatusPill, Table,
} from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
const aliasPattern = /^[a-z0-9][a-z0-9-]{0,31}$/
const platformOptions = ['windows', 'linux', 'macos'].map(value => ({ value, label: value }))
const brandOptions = ['Chrome', 'Edge', 'Opera', 'Vivaldi'].map(value => ({ value, label: value }))
const spoofingFields = ['font', 'audio', 'canvas', 'clientrects', 'gpu']
const stateText = { created: '已创建', running: '运行中', exited: '已停止', restarting: '重启中', paused: '已暂停', missing: '未部署' }
const blockText = {
account_revoked: '授权已撤销', account_paused: '账号已暂停', binding_missing: '绑定缺失',
network_exit_missing: '固定出口缺失', network_exit_unhealthy: '固定出口不健康',
runtime_stop_pending: '停止结果待确认', runtime_missing: '运行实例缺失',
}
const scheduleBlock = runtime => runtime.schedule_status === 'ready'
? ''
: blockText[runtime.schedule_block_reason] || runtime.schedule_block_reason || '不可调度'
const createInitial = {
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: [],
}
// 高级字段空值不提交,收敛到 seed 驱动。
function fingerprintPayload(form) {
const payload = { seed: Number(form.seed) }
const advanced = { 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(advanced)) if (value !== '') payload[key] = value
if (form.hardware_concurrency !== '') payload.hardware_concurrency = Number(form.hardware_concurrency)
if (form.disable_spoofing.length > 0) payload.disable_spoofing = form.disable_spoofing.join(',')
return payload
}
function StatePill({ state }) {
const known = Object.hasOwn(stateText, state)
return
}
function RuntimeActions({ runtime, busy, onAction }) {
const running = runtime.state === 'running'
const block = runtime.schedule_block_reason === 'runtime_missing' ? '' : scheduleBlock(runtime)
const startLabel = `启动 ${runtime.name}${block ? `(${block})` : ''}`
return (
)
}
function SchedulePill({ runtime }) {
const block = scheduleBlock(runtime)
return
}
function UpgradeDialog({ target, images, busy, onClose, onConfirm }) {
const [version, setVersion] = useState('')
const enabled = images.filter(image => image.enabled)
useEffect(() => { setVersion('') }, [target])
return (
>}>
容器将按新镜像重建,Profile 数据卷与指纹参数保持不变。
)
}
function RuntimeTable({ runtimes, busy, onAction, onRowClick }) {
return (
| 环境 |
状态 |
固定绑定 |
CDP |
操作 |
{runtimes.map(runtime => (
| onRowClick(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 navigate = useNavigate()
const dataProvider = useDataProvider()('default')
const [busy, setBusy] = useState('')
const [actionError, setActionError] = useState('')
const [upgradeTarget, setUpgradeTarget] = useState(null)
const { result, query } = useList({ resource: 'browsers', queryOptions: { refetchInterval: 3000 } })
const error = query.error
const isPending = query.isPending
const runtimes = result.data ?? []
const { result: imageResult, query: imagesQuery } = useList({ resource: 'browser-images' })
const images = imageResult.data ?? []
useTitle('CreatorHub · 运行环境')
async function handleAction(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, data) {
setBusy(runtime.alias)
setActionError('')
try {
await dataProvider.browserAction(runtime.alias, action, data)
await query.refetch()
} catch (reason) {
setActionError(reason.message)
} finally {
setBusy('')
}
}
async function upgrade(version) {
await runAction(upgradeTarget, 'upgrade', { version })
setUpgradeTarget(null)
}
return (
{actionError ? (
{actionError}
) : null}
navigate(`/browsers/${encodeURIComponent(runtime.alias)}`)} />
setUpgradeTarget(null)} onConfirm={upgrade} />
Docker socket 仅由受限网关访问,镜像与参数由平台下发
)
}
export function BrowserCreatePage() {
const navigate = useNavigate()
const dataProvider = useDataProvider()('default')
const [form, setForm] = useState(createInitial)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [submitBusy, setSubmitBusy] = useState(false)
const [submitError, setSubmitError] = useState('')
const { query: runtimesQuery } = useList({ resource: 'browsers' })
const { result: gatewayResult } = useList({ resource: 'gateways' })
const gateways = gatewayResult.data ?? []
const { result: imageResult } = useList({ resource: 'browser-images' })
const images = imageResult.data ?? []
const { result: accountResult } = useList({ resource: 'accounts' })
const accounts = accountResult.data ?? []
const { result: exitResult } = useList({ resource: 'network-exits' })
const networkExits = exitResult.data ?? []
useTitle('CreatorHub · 创建运行环境')
const enabledImages = images.filter(image => image.enabled)
const pausedAccounts = accounts.filter(account => account.authorization_status === 'authorized' && account.runtime_status === 'paused')
const healthyExits = networkExits.filter(item => item.health_status === 'healthy')
const update = (key, value) => setForm(current => ({ ...current, [key]: value }))
const toggleSpoofing = field => setForm(current => ({ ...current, disable_spoofing: current.disable_spoofing.includes(field) ? current.disable_spoofing.filter(item => item !== field) : [...current.disable_spoofing, field] }))
// 选中项为空且列表就绪时,默认选中第一个可选项,减少必填噪音。
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 === '' && pausedAccounts[0]?.id) update('account_id', pausedAccounts[0].id) }, [pausedAccounts, form.account_id])
const seed = Number(form.seed)
const valid = form.name.trim() !== '' && aliasPattern.test(form.alias) && form.gateway !== '' && form.image_version !== '' && form.account_id !== '' && Number.isInteger(seed) && seed >= 1 && seed <= 2147483647
async function submit(event) {
event.preventDefault()
if (!valid) return
setSubmitBusy(true)
setSubmitError('')
try {
await dataProvider.create({
resource: 'browsers',
variables: {
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: fingerprintPayload(form),
},
})
await runtimesQuery.refetch()
navigate('/browsers')
} catch (reason) {
setSubmitError(reason.status === 409 ? '冲突(409):账号或运行环境已有活动绑定;表单内容已保留。' : reason.status === 503 ? `环境不可用(503):${reason.message}` : reason.message)
} finally {
setSubmitBusy(false)
}
}
return (
{submitError ?
{submitError} : null}
)
}
export function BrowserDetail() {
const { id } = useParams()
const { result, error, isPending } = useOne({ resource: 'browsers', id, queryOptions: { retry: false } })
const runtime = result
useTitle('CreatorHub · 环境详情')
if (isPending) return
if (error || !runtime) return {error?.message || '运行环境不存在'}
return (
{runtime.account_id} · {runtime.image_version}],
['固定出口', {runtime.network_exit?.id || '未绑定'} · 健康:{runtime.network_exit?.health_status || '未知'}],
['绑定版本 / 运行实例', {runtime.binding_version} · {runtime.runtime_instance_id || '无'}],
]} />
{runtime.endpoint ? (
CDP
) : null}
{runtime.network_exit?.id ? : null}
)
}