207 lines
9.9 KiB
TypeScript
207 lines
9.9 KiB
TypeScript
// 创建运行环境:语义对齐 web.archived BrowsersPage.jsx 的 BrowserCreatePage。
|
|
// 高级指纹参数用 antd Collapse 收纳;空值不提交,收敛到 seed 驱动。
|
|
import { useEffect, useState } from 'react';
|
|
import { history } from '@umijs/max';
|
|
import { Alert, Button, Card, Checkbox, Collapse, Form, Input, InputNumber, Select, Space, Typography, message } from 'antd';
|
|
import { create, getList } from '@/services/api';
|
|
import { conflictMessage } from '@/utils/helpers';
|
|
|
|
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'];
|
|
|
|
// 高级字段空值不提交,收敛到 seed 驱动(对齐归档版 fingerprintPayload)。
|
|
function fingerprintPayload(form: any) {
|
|
const payload: Record<string, unknown> = { 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;
|
|
}
|
|
|
|
// 已用 seed 之外的最小可用 seed(对齐归档版 nextFingerprintSeed)。
|
|
function nextFingerprintSeed(runtimes: any[]): string {
|
|
const used = new Set(runtimes.map((runtime) => Number(runtime.fingerprint?.seed)).filter((seed) => Number.isInteger(seed) && seed > 0));
|
|
let seed = 1000;
|
|
while (used.has(seed)) seed += 1;
|
|
return String(seed);
|
|
}
|
|
|
|
export default function Page() {
|
|
const [form] = Form.useForm();
|
|
const [runtimes, setRuntimes] = useState<any[]>([]);
|
|
const [gateways, setGateways] = useState<any[]>([]);
|
|
const [accounts, setAccounts] = useState<any[]>([]);
|
|
const [networkExits, setNetworkExits] = useState<any[]>([]);
|
|
const [submitBusy, setSubmitBusy] = useState(false);
|
|
const [messageApi, contextHolder] = message.useMessage();
|
|
|
|
useEffect(() => {
|
|
Promise.allSettled([
|
|
getList({ resource: 'browsers' }),
|
|
getList({ resource: 'gateways' }),
|
|
getList({ resource: 'accounts' }),
|
|
getList({ resource: 'network-exits' }),
|
|
]).then(([browsers, gatewaysResult, accountsResult, exitsResult]) => {
|
|
if (browsers.status === 'fulfilled') {
|
|
setRuntimes(browsers.value.data ?? []);
|
|
form.setFieldsValue({ seed: nextFingerprintSeed(browsers.value.data ?? []) });
|
|
}
|
|
if (gatewaysResult.status === 'fulfilled') {
|
|
setGateways(gatewaysResult.value.data ?? []);
|
|
const first = (gatewaysResult.value.data ?? [])[0];
|
|
if (first) form.setFieldsValue({ gateway: first.name });
|
|
}
|
|
if (accountsResult.status === 'fulfilled') {
|
|
const paused = (accountsResult.value.data ?? []).filter(
|
|
(account: any) => account.authorization_status === 'authorized' && account.runtime_status === 'paused',
|
|
);
|
|
setAccounts(paused);
|
|
if (paused[0]?.id) form.setFieldsValue({ account_id: paused[0].id });
|
|
}
|
|
if (exitsResult.status === 'fulfilled') {
|
|
setNetworkExits((exitsResult.value.data ?? []).filter((item: any) => item.health_status === 'healthy'));
|
|
}
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const pausedAccounts = accounts;
|
|
const healthyExits = networkExits;
|
|
|
|
async function submit(values: any) {
|
|
setSubmitBusy(true);
|
|
try {
|
|
await create('browsers', {
|
|
alias: values.alias,
|
|
name: values.name.trim(),
|
|
gateway: values.gateway,
|
|
account_id: values.account_id,
|
|
network_exit_id: values.network_exit_id || '',
|
|
fingerprint: fingerprintPayload(values),
|
|
});
|
|
messageApi.success('运行环境已创建。');
|
|
history.push('/browsers');
|
|
} catch (reason: any) {
|
|
messageApi.error(
|
|
reason.status === 503 ? `环境不可用(503):${reason.message}` : conflictMessage(reason, '账号或运行环境已有活动绑定;表单内容已保留。'),
|
|
);
|
|
} finally {
|
|
setSubmitBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{contextHolder}
|
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Typography.Text type="secondary">为已授权且暂停的账号绑定网关、指纹与网络出口</Typography.Text>
|
|
<Button onClick={() => history.push('/browsers')}>返回运行环境</Button>
|
|
</div>
|
|
<Card>
|
|
<Form form={form} layout="vertical" onFinish={submit}>
|
|
<Space size={16} wrap style={{ display: 'flex' }}>
|
|
<Form.Item name="name" label="环境名称" required rules={[{ required: true, message: '请输入环境名称' }]} extra="展示名称,支持中文,如:店铺一号">
|
|
<Input maxLength={64} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="alias"
|
|
label="别名"
|
|
required
|
|
rules={[
|
|
{ required: true, message: '请输入别名' },
|
|
{ pattern: aliasPattern, message: '小写字母、数字与中划线,最长 32 字符' },
|
|
]}
|
|
extra="用于 Profile 标识与 API 路径"
|
|
>
|
|
<Input placeholder="如:shop-01" />
|
|
</Form.Item>
|
|
<Form.Item name="gateway" label="网关" required rules={[{ required: true, message: '请选择网关' }]} extra={gateways.length === 0 ? '请先在「网关管理」注册网关' : '环境由该 native gateway 管理'}>
|
|
<Select style={{ width: 200 }} placeholder="选择网关" disabled={gateways.length === 0} options={gateways.map((item) => ({ value: item.name, label: item.name }))} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="seed"
|
|
label="Fingerprint Seed"
|
|
required
|
|
rules={[
|
|
{ required: true, message: '请输入 seed' },
|
|
{ type: 'integer', min: 1, max: 2147483647, message: '1..2147483647' },
|
|
]}
|
|
extra="1..2147483647,派生指纹参数"
|
|
>
|
|
<InputNumber min={1} max={2147483647} style={{ width: 180 }} />
|
|
</Form.Item>
|
|
<Form.Item name="account_id" label="社媒账号" required rules={[{ required: true, message: '请选择账号' }]} extra={pausedAccounts.length === 0 ? '请先准备已授权且暂停的账号' : '绑定后该账号固定使用此环境'}>
|
|
<Select style={{ width: 240 }} placeholder="选择账号" disabled={pausedAccounts.length === 0} options={pausedAccounts.map((item) => ({ value: item.id, label: `${item.id} · ${item.platform}` }))} />
|
|
</Form.Item>
|
|
<Form.Item name="network_exit_id" label="网络出口" extra="留空时使用当前机器网络出口直连">
|
|
<Select
|
|
style={{ width: 240 }}
|
|
allowClear
|
|
placeholder="当前机器直连"
|
|
options={healthyExits.map((item) => ({ value: item.id, label: `${item.id} · ${item.protocol}://${item.host}:${item.port}` }))}
|
|
/>
|
|
</Form.Item>
|
|
</Space>
|
|
<Collapse
|
|
ghost
|
|
items={[
|
|
{
|
|
key: 'advanced',
|
|
label: '高级指纹参数',
|
|
children: (
|
|
<Space size={16} wrap style={{ display: 'flex' }}>
|
|
<Form.Item name="platform" label="操作系统 platform">
|
|
<Select style={{ width: 140 }} allowClear placeholder="默认" options={platformOptions} />
|
|
</Form.Item>
|
|
<Form.Item name="platform_version" label="系统版本 platform-version" extra="如 11.0.0">
|
|
<Input style={{ width: 140 }} />
|
|
</Form.Item>
|
|
<Form.Item name="brand" label="浏览器品牌 brand">
|
|
<Select style={{ width: 140 }} allowClear placeholder="默认 Chromium" options={brandOptions} />
|
|
</Form.Item>
|
|
<Form.Item name="brand_version" label="品牌版本 brand-version" extra="如 132.0.6834.159">
|
|
<Input style={{ width: 160 }} />
|
|
</Form.Item>
|
|
<Form.Item name="hardware_concurrency" label="CPU 核心数 hardware-concurrency" extra="留空由种子生成">
|
|
<InputNumber min={1} max={128} style={{ width: 100 }} />
|
|
</Form.Item>
|
|
<Form.Item name="lang" label="语言 lang" extra="如 zh-CN">
|
|
<Input style={{ width: 120 }} />
|
|
</Form.Item>
|
|
<Form.Item name="accept_lang" label="接受语言 accept-lang" extra="如 zh-CN,en-US">
|
|
<Input style={{ width: 160 }} />
|
|
</Form.Item>
|
|
<Form.Item name="timezone" label="时区 timezone" extra="如 Asia/Shanghai">
|
|
<Input style={{ width: 160 }} />
|
|
</Form.Item>
|
|
<Form.Item name="disable_spoofing" label="禁用指纹伪装 disable-spoofing">
|
|
<Checkbox.Group options={spoofingFields.map((field) => ({ value: field, label: field }))} />
|
|
</Form.Item>
|
|
</Space>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
<Space>
|
|
<Button type="primary" htmlType="submit" loading={submitBusy}>
|
|
{submitBusy ? '创建中…' : '创建环境'}
|
|
</Button>
|
|
<Button onClick={() => history.push('/browsers')}>取消</Button>
|
|
</Space>
|
|
</Form>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|