import { useId, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Divider, Form, Input, Select, Space, Typography } from 'antd'; import { ThunderboltOutlined } from '@ant-design/icons'; import { DEFAULT_DOH_ENDPOINT, DEFAULT_ECH_DOMAIN, DEFAULT_ECH_PAIR, dohEndpointOptions, echDomainOptions, formatDynamicEch, isSupportedDohUrl, pairPresetOptions, parseDynamicEch, } from '@/lib/xray/ech-presets'; interface DynamicEchHelperProps { /** Current echConfigList, so an existing dynamic pair can be recovered. */ value?: string; /** Node SNI, used by the "resolve from the proxied hostname" target. */ sni?: string; /** True when a locally generated ECH keypair is present. */ hasLocalKeys?: boolean; onApply?: (value: string) => void; /** Called when the operator accepts dropping CDN-invalid local keys. */ onClearLocalKeys?: () => void; disabled?: boolean; } const CUSTOM = '__custom__'; const AUTO = '__auto__'; const ADVANCED = '__advanced__'; /* Only echConfigList can hold the dynamic `domain+DoH` form; clearing local keys * is a separate, explicitly confirmed step because behind a CDN they are dead. */ export default function DynamicEchHelper({ value = '', sni = '', hasLocalKeys = false, onApply, onClearLocalKeys, disabled = false, }: DynamicEchHelperProps) { const { t } = useTranslation(); const uid = useId(); const parsed = parseDynamicEch(value); const presetValues = new Set( pairPresetOptions(t).flatMap((group) => group.options.map((o) => o.value)), ); const [preset, setPreset] = useState(() => { const current = value.trim(); if (presetValues.has(current)) return current; if (!current || !parsed) return DEFAULT_ECH_PAIR; return ADVANCED; }); const [domainChoice, setDomainChoice] = useState( () => parsed?.domain ?? DEFAULT_ECH_DOMAIN, ); const [domainInput, setDomainInput] = useState(() => parsed?.domain ?? ''); const [dohChoice, setDohChoice] = useState(() => parsed?.dohUrl ?? DEFAULT_DOH_ENDPOINT); const [dohInput, setDohInput] = useState(parsed?.dohUrl ?? ''); const [preview, setPreview] = useState(null); const domain = domainChoice === CUSTOM ? domainInput : domainChoice === AUTO ? sni.trim() : domainChoice; const doh = dohChoice === CUSTOM ? dohInput : dohChoice; const advancedBuilt = formatDynamicEch(domain, doh); const built = preset === ADVANCED ? advancedBuilt : preset; const apply = () => { if (!built) return; if (hasLocalKeys) setPreview(built); else onApply?.(built); }; const pickPreset = (next: string) => { setPreset(next); setPreview(null); }; const pickDomain = (next: string) => { setDomainChoice(next); setPreview(null); }; const pickDoh = (next: string) => { setDohChoice(next); setPreview(null); }; const confirmOverwrite = () => { if (!built) return; onApply?.(built); onClearLocalKeys?.(); setPreview(null); }; return (
{t('pages.inbounds.form.echDynamicTitle')}
{t('pages.inbounds.form.echDynamicIntro')} {value.trim() !== '' && !parsed && ( {t('pages.inbounds.form.echExistingNotDynamic')} )} {/* component={false}: rendered inside the inbound/host
, and a nested element is invalid HTML. */} {domainChoice === CUSTOM && ( { setDomainInput(e.target.value); setPreview(null); }} /> )} { setDohInput(e.target.value); setPreview(null); }} /> )} {dohChoice === CUSTOM && dohInput && !isSupportedDohUrl(dohInput) && ( {t('pages.inbounds.form.echDynamicUnsupportedScheme')} )} )} {t('pages.inbounds.form.echMainlandAdvisory')}
{built ? ( {built} ) : ( {t('pages.inbounds.form.echDynamicInvalid')} )} {preview && ( <>
{t('pages.inbounds.form.echDynamicOverwriteBody')}
{preview}
} action={ } /> )}
); }