import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd'; import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { FormInstance } from 'antd/es/form'; import type { NamePath } from 'antd/es/form/interface'; import { RandomUtil } from '@/utils'; import { OutboundProtocols } from '@/schemas/primitives'; // Pattern A FinalMaskForm. Renders a Fragment of Form.Items at absolute // paths under `name`; the parent modal owns the Form instance. // // Naming convention inside Form.List: AntD prefixes Form.Item `name` // with the Form.List's own `name`. So Form.Items inside the render // prop use RELATIVE paths (e.g. `[field.name, 'type']`). Nested // Form.Lists also use relative names. Using absolute paths here would // double up the prefix and silently route reads/writes to the wrong // storage path. export interface FinalMaskFormProps { name: NamePath; network: string; protocol: string; form: FormInstance; } const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp']; function asPath(name: NamePath): (string | number)[] { return Array.isArray(name) ? [...name] : [name]; } function defaultTcpMaskSettings(type: string): Record { switch (type) { case 'fragment': return { packets: '1-3', length: '', delay: '', maxSplit: '' }; case 'sudoku': return { password: '', ascii: '', customTable: '', customTables: '', paddingMin: 0, paddingMax: 0, }; case 'header-custom': return { clients: [], servers: [] }; default: return {}; } } function defaultUdpMaskSettings(type: string): Record { switch (type) { case 'salamander': case 'mkcp-aes128gcm': return { password: '' }; case 'header-dns': return { domain: '' }; case 'xdns': return { domains: [] }; case 'xicmp': return { ip: '0.0.0.0', id: 0 }; case 'header-custom': return { client: [], server: [] }; case 'noise': return { reset: 0, noise: [] }; default: return {}; } } function defaultClientServerItem(): Record { return { delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: [] }; } function defaultUdpClientServerItem(): Record { return { rand: 0, randRange: '0-255', type: 'array', packet: [] }; } function defaultNoiseItem(): Record { return { rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20', }; } function defaultQuicParams(): Record { // Seeded with the xray-core / hysteria recommended defaults so the QUIC // Params sub-form doesn't show blank InputNumber fields when first // enabled. The schema declares these as .optional() (no Zod default) // because the wire shape omits them when xray's built-in default // applies — but the panel needs values to render the controls. return { congestion: 'bbr', debug: false, brutalUp: 0, brutalDown: 0, hasUdpHop: false, udpHop: { ports: '20000-50000', interval: '5-10' }, maxIdleTimeout: 30, keepAlivePeriod: 10, disablePathMTUDiscovery: false, maxIncomingStreams: 1024, initStreamReceiveWindow: 8388608, maxStreamReceiveWindow: 8388608, initConnectionReceiveWindow: 20971520, maxConnectionReceiveWindow: 20971520, }; } export default function FinalMaskForm({ name, network, protocol, form }: FinalMaskFormProps) { const base = asPath(name); const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria'; const showTcp = TCP_NETWORKS.includes(network); const showUdp = isHysteria || network === 'kcp'; const showQuic = isHysteria || network === 'xhttp'; const enableQuic = Form.useWatch([...base, 'enableQuicParams'], form); if (!showTcp && !showUdp && !showQuic) return null; return ( <> {showTcp && } {showUdp && } {showQuic && ( <> { if (v) { const current = form.getFieldValue([...base, 'quicParams']); if (!current) form.setFieldValue([...base, 'quicParams'], defaultQuicParams()); } }} /> {enableQuic && } )} ); } function TcpMasksList({ base, form }: { base: (string | number)[]; form: FormInstance }) { return ( {(fields, { add, remove }) => ( <>