import { useEffect, useMemo, useRef, useState } from 'react'; import { open } from '@tauri-apps/plugin-dialog'; import { Cpu, FileCode2, FolderOpen, Gauge, Info, Link2, Trash2, Wand2 } from 'lucide-react'; import { applyProfiles, fetchSingBoxSubscription, forgetSingBoxSubscription, generateSingBoxConfig, getComponents, getProxiFyreSetupStatus, getSavedState, getSingBoxSetupStatus, getSingBoxStatus, installProxiFyre, installSingBox, openConfigLocation, pingAllSingBoxServers, pingProxyTarget, pingSingBoxServer, saveProfile, saveSingBoxSubscription, saveTarget, selectSingBoxServer, startProxiFyreService, startSingBoxService, stopProxiFyreService, stopSingBoxService, uninstallProxiFyre, uninstallSingBox, type ApplyProfilesResponse, type LocalSingBoxStatusResponse, type PingServerResponse, type ProxiFyreSetupStatus, type SingBoxSetupStatus, } from '../api/tauriCommands'; import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, SubscriptionServer, Target } from '../domain/types'; import { Button, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui'; import { getApplyReadiness } from './readiness'; import { serviceControlState } from './viewModel'; type DraftItemType = Extract; type ProxiFyreAction = 'start' | 'stop' | 'install' | 'uninstall'; type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'generate' | 'ping'; type RouteMode = 'external' | 'local-singbox'; type ServiceVisualState = 'active' | 'settling' | null; type PanelId = 'summary' | 'proxifyre' | 'proxy'; type StatusTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted'; type TabTransitionDirection = 'left' | 'right'; interface DraftItem { id: string; type: DraftItemType; value: string; } interface Notice { kind: 'success' | 'error' | 'info'; title: string; text: string; } interface LogEntry extends Notice { id: string; at: number; } interface ConfigSnapshotItem { type: DraftItemType; value: string; } interface ConfigSnapshot { routeMode: RouteMode; proxy: string; selectedServerTag: string; items: ConfigSnapshotItem[]; } const MAIN_TARGET_ID = 'main-proxy'; const MAIN_PROFILE_ID = 'main-profile'; const LOCAL_SINGBOX_TARGET_ID = 'local-singbox'; const LOG_VISIBLE_MS = 6500; const PANEL_ORDER: PanelId[] = ['summary', 'proxifyre', 'proxy']; const fallbackComponents: ComponentStatus[] = [ { id: 'proxyfier', name: 'ProxiFyre', state: 'missing', installed: false, running: false, problems: ['ProxiFyre не найден'], actions: [], }, { id: 'singbox', name: 'Локальный sing-box', state: 'missing', installed: false, running: false, problems: [], actions: [], }, ]; export function App() { const [activePanel, setActivePanel] = useState('summary'); const [tabTransitionDirection, setTabTransitionDirection] = useState('right'); const [proxyInput, setProxyInput] = useState(''); const [routeMode, setRouteMode] = useState('external'); const [profileId, setProfileId] = useState(MAIN_PROFILE_ID); const [targetId, setTargetId] = useState(MAIN_TARGET_ID); const [items, setItems] = useState([]); const [appliedSnapshot, setAppliedSnapshot] = useState(null); const [loadedProfiles, setLoadedProfiles] = useState([]); const [isProcessInputOpen, setIsProcessInputOpen] = useState(false); const [processInput, setProcessInput] = useState(''); const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null); const [components, setComponents] = useState(fallbackComponents); const [setupStatus, setSetupStatus] = useState(null); const [singBoxStatus, setSingBoxStatus] = useState(null); const [singBoxSetupStatus, setSingBoxSetupStatus] = useState(null); const [subscriptionInput, setSubscriptionInput] = useState(''); const [serverPings, setServerPings] = useState>({}); const [proxyPing, setProxyPing] = useState(null); const [isSingBoxSetupOpen, setIsSingBoxSetupOpen] = useState(false); const [isSingBoxInfoOpen, setIsSingBoxInfoOpen] = useState(false); const [generatedConfigPath, setGeneratedConfigPath] = useState(''); const [logEntries, setLogEntries] = useState([]); const [activeLogId, setActiveLogId] = useState(null); const [isLogOpen, setIsLogOpen] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isDetectingComponents, setIsDetectingComponents] = useState(true); const [isApplying, setIsApplying] = useState(false); const [isOpeningConfig, setIsOpeningConfig] = useState(false); const [isProxyChecking, setIsProxyChecking] = useState(false); const [serverPingTag, setServerPingTag] = useState(null); const [serviceAction, setServiceAction] = useState(null); const [singBoxAction, setSingBoxAction] = useState(null); const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); const [isSingBoxMenuOpen, setIsSingBoxMenuOpen] = useState(false); const [serviceVisualState, setServiceVisualState] = useState(null); const serviceVisualTimerRef = useRef(null); const appsSectionRef = useRef(null); const proxyfier = useMemo( () => components.find((component) => component.id === 'proxyfier'), [components], ); const singbox = useMemo( () => singBoxStatus?.component ?? components.find((component) => component.id === 'singbox'), [components, singBoxStatus], ); const activeLog = useMemo( () => logEntries.find((entry) => entry.id === activeLogId) ?? null, [activeLogId, logEntries], ); const isSingBoxInstalled = Boolean(singbox?.installed); const selectedServerTag = singBoxStatus?.config.selectedServerTag; const currentSnapshot = useMemo( () => configSnapshotFromUi(routeMode, proxyInput, items, selectedServerTag), [items, proxyInput, routeMode, selectedServerTag], ); const hasUnappliedChanges = appliedSnapshot ? !sameConfigSnapshot(currentSnapshot, appliedSnapshot) : false; const systemSummary = systemSummaryState({ isLoading, isDetectingComponents, proxyfier, singbox, routeMode, singBoxStatus, proxyPing, hasUnappliedChanges, }); useEffect(() => { void refresh(); }, []); useEffect(() => { return () => { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); } }; }, []); useEffect(() => { if (!activeLogId) return undefined; const timer = window.setTimeout(() => { setActiveLogId((current) => (current === activeLogId ? null : current)); }, LOG_VISIBLE_MS); return () => window.clearTimeout(timer); }, [activeLogId]); async function refresh() { setIsLoading(true); try { const saved = await getSavedState(); applySavedState(saved.profiles, saved.targets, saved.generatedConfigPath); } catch { showNotice({ kind: 'info', title: 'Режим предпросмотра', text: 'Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.', }); } finally { setIsLoading(false); } void refreshComponents(); } async function refreshComponents() { setIsDetectingComponents(true); try { const [detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus] = await Promise.all([ getComponents(), getProxiFyreSetupStatus(), getSingBoxStatus(), getSingBoxSetupStatus(), ]); setComponents(detectedComponents); setSetupStatus(detectedSetupStatus); setSingBoxStatus(detectedSingBoxStatus); setSingBoxSetupStatus(detectedSingBoxSetupStatus); setAppliedSnapshot((current) => { if (!current || current.routeMode !== 'local-singbox' || current.selectedServerTag) return current; return { ...current, selectedServerTag: normalizeServerTag(detectedSingBoxStatus.config.selectedServerTag), }; }); } catch (error) { showNotice({ kind: 'error', title: 'Компоненты не проверены', text: errorMessage(error), }); } finally { setIsDetectingComponents(false); } } function applySavedState( profiles: Profile[], targets: Target[], generatedPath: string, singBoxStatusForSnapshot = singBoxStatus, ) { const activeProfiles = profiles.filter((profile) => profile.enabled); const mainProfile = profiles.find((profile) => profile.id === MAIN_PROFILE_ID); const activeProfile = mainProfile ?? activeProfiles[0]; const activeTarget = targetForUi(targets, activeProfile); const externalTarget = targetForExternalProxy(targets); const editableProfiles = mainProfile ? [mainProfile] : activeProfiles; const savedProxyInput = externalTarget ? formatProxy(externalTarget) : ''; const savedItems = itemsForProfiles(editableProfiles); const savedRouteMode = activeTarget?.id === LOCAL_SINGBOX_TARGET_ID || activeProfile?.targetId === LOCAL_SINGBOX_TARGET_ID ? 'local-singbox' : 'external'; setProxyInput(savedProxyInput); setProxyPing(null); setItems(savedItems); setLoadedProfiles(profiles); setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID); setTargetId(externalTarget?.id ?? MAIN_TARGET_ID); setRouteMode(savedRouteMode); setGeneratedConfigPath(generatedPath); setAppliedSnapshot(configSnapshotFromUi( savedRouteMode, savedProxyInput, savedItems, singBoxStatusForSnapshot?.config.selectedServerTag, )); } function addItem(type: DraftItemType, rawValue: string) { const value = normalizeItemValue(rawValue, type); if (!value) { showNotice({ kind: 'error', title: 'Нечего добавить', text: emptyItemMessage(type), }); return false; } if (items.some((item) => item.type === type && sameValue(item.value, value))) { showNotice({ kind: 'info', title: 'Уже добавлено', text: value, }); return false; } setItems((current) => [ ...current, { id: `${type}-${Date.now()}`, type, value, }, ]); return true; } function addProcess() { if (addItem('process', processInput)) { setProcessInput(''); setIsProcessInputOpen(false); } } function removeItem(id: string) { setItems((current) => current.filter((item) => item.id !== id)); } function changeRouteMode(nextMode: RouteMode) { setRouteMode(nextMode); } function changeProxyInput(nextValue: string) { setProxyInput(nextValue); setProxyPing(null); } function openAppsPanel() { switchPanel('proxifyre'); window.setTimeout(() => { appsSectionRef.current?.scrollIntoView({ block: 'start' }); }, 0); } function switchPanel(nextPanel: PanelId) { if (activePanel === nextPanel) return; const currentIndex = PANEL_ORDER.indexOf(activePanel); const nextIndex = PANEL_ORDER.indexOf(nextPanel); setTabTransitionDirection(nextIndex > currentIndex ? 'right' : 'left'); setActivePanel(nextPanel); } async function pickAndAddItem(type: Extract) { setPickerAction(type); try { const selectedPath = await pickPath(type); if (selectedPath) { addItem(type, selectedPath); } } catch (error) { showNotice({ kind: 'error', title: type === 'exe' ? 'EXE не выбран' : 'Папка не выбрана', text: errorMessage(error), }); } finally { setPickerAction(null); } } async function updateConfig() { let parsedProxy: ParsedProxy | null = null; try { if (!items.length) throw new Error('Добавь хотя бы один процесс, EXE-файл или папку.'); if (routeMode === 'external') { parsedProxy = parseProxy(proxyInput); } else if (!isSingBoxInstalled) { throw new Error('Сначала установи Local sing-box.'); } else if (!singBoxStatus?.config.selectedServerTag) { throw new Error('Выбери сервер Local sing-box.'); } } catch (error) { showNotice({ kind: 'error', title: 'Проверь данные', text: errorMessage(error), }); return; } setIsApplying(true); try { let singBoxGeneratedPath = ''; if (routeMode === 'external') { if (!parsedProxy) throw new Error('Прокси не разобран.'); await saveTarget({ id: targetId, name: 'Основной прокси', kind: 'external', protocol: parsedProxy.protocol, host: parsedProxy.host, port: parsedProxy.port, }); } else { const singBoxResult = await generateSingBoxConfig(); singBoxGeneratedPath = singBoxResult.generatedConfigPath; await ensureSingBoxRunningForApply(); } await saveProfile({ id: profileId, name: 'Приложения через прокси', enabled: true, targetId: routeMode === 'local-singbox' ? LOCAL_SINGBOX_TARGET_ID : targetId, protocols: ['TCP', 'UDP'], items: items.map(profileItemInput), }); await Promise.all( loadedProfiles .filter((profile) => profile.enabled && profile.id !== profileId) .map((profile) => saveProfile(profileInputFromProfile(profile, false))), ); const result = await applyProfiles(); const [saved, detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus] = await Promise.all([ getSavedState(), getComponents(), getProxiFyreSetupStatus(), getSingBoxStatus(), getSingBoxSetupStatus(), ]); applySavedState(saved.profiles, saved.targets, result.generatedConfigPath); setComponents(detectedComponents); setSetupStatus(detectedSetupStatus); setSingBoxStatus(detectedSingBoxStatus); setSingBoxSetupStatus(detectedSingBoxSetupStatus); showNotice(routeMode === 'local-singbox' ? noticeFromLocalApply(result, singBoxGeneratedPath) : noticeFromApply(result)); } catch (error) { showNotice({ kind: 'error', title: 'Конфиг не обновлен', text: errorMessage(error), }); } finally { setIsApplying(false); } } async function ensureSingBoxRunningForApply() { if (routeMode !== 'local-singbox' || !singbox?.installed) return; setIsSingBoxMenuOpen(false); try { await nextFrame(); if (singbox.running) { setSingBoxAction('stop'); const stopped = await stopSingBoxService(); setComponents((current) => upsertComponent(current, stopped)); } setSingBoxAction('start'); const component = await startSingBoxService(); setComponents((current) => upsertComponent(current, component)); const status = await refreshSingBoxState(); if (!status.component.running) { throw new Error('Local sing-box установлен, но служба не запустилась.'); } } finally { setSingBoxAction(null); } } async function openConfig() { setIsOpeningConfig(true); try { const openedPath = await openConfigLocation(); showNotice({ kind: 'info', title: 'Конфиг открыт', text: openedPath, }); } catch (error) { showNotice({ kind: 'error', title: 'Не удалось открыть конфиг', text: errorMessage(error), }); } finally { setIsOpeningConfig(false); } } async function setProxiFyreServiceRunning(shouldRun: boolean) { const action = shouldRun ? 'start' : 'stop'; setServiceAction(action); setIsServiceMenuOpen(false); startServiceVisual(); try { await nextFrame(); const component = shouldRun ? await startProxiFyreService() : await stopProxiFyreService(); setComponents((current) => upsertComponent(current, component)); showNotice({ kind: 'success', title: shouldRun ? 'Служба запущена' : 'Служба остановлена', text: proxyfierDetails(component, false), }); } catch (error) { showNotice({ kind: 'error', title: shouldRun ? 'Служба не запущена' : 'Служба не остановлена', text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function installProxiFyrePackage() { setServiceAction('install'); setIsServiceMenuOpen(false); startServiceVisual(); try { await nextFrame(); const component = await installProxiFyre(); const detectedSetupStatus = await getProxiFyreSetupStatus(); setComponents((current) => upsertComponent(current, component)); setSetupStatus(detectedSetupStatus); showNotice({ kind: 'success', title: 'ProxiFyre установлен', text: proxyfierDetails(component, false), }); } catch (error) { showNotice({ kind: 'error', title: 'ProxiFyre не установлен', text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function uninstallProxiFyrePackage() { const confirmed = window.confirm( 'Удалить ProxiFyre с компьютера? Будет удалена служба и папка установки ProxiFyre.', ); if (!confirmed) return; setServiceAction('uninstall'); setIsServiceMenuOpen(false); startServiceVisual(); try { await nextFrame(); const component = await uninstallProxiFyre(); const detectedSetupStatus = await getProxiFyreSetupStatus(); setComponents((current) => upsertComponent(current, component)); setSetupStatus(detectedSetupStatus); showNotice({ kind: 'success', title: 'ProxiFyre удален', text: 'Служба и папка установки ProxiFyre удалены.', }); } catch (error) { showNotice({ kind: 'error', title: 'ProxiFyre не удален', text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function refreshSingBoxState() { const [detectedSingBoxStatus, detectedSingBoxSetupStatus, detectedComponents] = await Promise.all([ getSingBoxStatus(), getSingBoxSetupStatus(), getComponents(), ]); setSingBoxStatus(detectedSingBoxStatus); setSingBoxSetupStatus(detectedSingBoxSetupStatus); setComponents(detectedComponents); return detectedSingBoxStatus; } async function setSingBoxServiceRunning(shouldRun: boolean) { const action: SingBoxAction = shouldRun ? 'start' : 'stop'; setSingBoxAction(action); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = shouldRun ? await startSingBoxService() : await stopSingBoxService(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); showNotice({ kind: 'success', title: shouldRun ? 'sing-box запущен' : 'sing-box остановлен', text: componentDetails(component, false), }); } catch (error) { showNotice({ kind: 'error', title: shouldRun ? 'sing-box не запущен' : 'sing-box не остановлен', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function installSingBoxPackage() { setSingBoxAction('install'); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = await installSingBox(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); showNotice({ kind: 'success', title: 'Local sing-box установлен', text: componentDetails(component, false), }); } catch (error) { showNotice({ kind: 'error', title: 'Local sing-box не установлен', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function uninstallSingBoxPackage() { const confirmed = window.confirm( 'Удалить Local sing-box с компьютера? Будет удалена служба и папка установки sing-box.', ); if (!confirmed) return; setSingBoxAction('uninstall'); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = await uninstallSingBox(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); showNotice({ kind: 'success', title: 'Local sing-box удален', text: 'Служба и папка установки Local sing-box удалены.', }); } catch (error) { showNotice({ kind: 'error', title: 'Local sing-box не удален', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function syncSingBoxSubscription() { const subscriptionUrl = subscriptionInput.trim(); if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) { showNotice({ kind: 'error', title: 'Ссылка не указана', text: 'Вставь ссылку подписки Local sing-box.', }); return; } setSingBoxAction('fetch'); try { if (subscriptionUrl) { await saveSingBoxSubscription(subscriptionUrl); } const status = await fetchSingBoxSubscription(); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); setSubscriptionInput(''); setServerPings({}); showNotice({ kind: 'success', title: 'Подписка обновлена', text: `Серверов: ${status.cache?.servers.length ?? 0}`, }); } catch (error) { showNotice({ kind: 'error', title: 'Подписка не обновлена', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function forgetSingBoxSubscriptionData() { setSingBoxAction('forget'); setIsSingBoxMenuOpen(false); try { const status = await forgetSingBoxSubscription(); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); setServerPings({}); showNotice({ kind: 'info', title: 'Подписка очищена', text: 'Ссылка, cache и выбранный сервер Local sing-box удалены.', }); } catch (error) { showNotice({ kind: 'error', title: 'Подписка не очищена', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function chooseSingBoxServer(server: SubscriptionServer) { try { const status = await selectSingBoxServer(server); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); } catch (error) { showNotice({ kind: 'error', title: 'Сервер не выбран', text: errorMessage(error), }); } } async function pingSingBoxServers() { setSingBoxAction('ping'); try { const results = await pingAllSingBoxServers(); setServerPings(Object.fromEntries(results.map((result) => [result.tag, result]))); showNotice({ kind: 'info', title: 'Ping завершен', text: pingSummary(results), }); } catch (error) { showNotice({ kind: 'error', title: 'Ping не выполнен', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function pingSingleSingBoxServer(server: SubscriptionServer) { setServerPingTag(server.tag); try { const result = await pingSingBoxServer(server.tag); setServerPings((current) => ({ ...current, [result.tag]: result, })); showNotice({ kind: result.ok ? 'success' : 'error', title: result.ok ? 'Сервер отвечает' : 'Сервер не ответил', text: serverTooltip(server, result), }); } catch (error) { showNotice({ kind: 'error', title: 'Ping не выполнен', text: errorMessage(error), }); } finally { setServerPingTag(null); } } async function pingExternalProxy() { let parsed: ParsedProxy; try { parsed = parseProxy(proxyInput); } catch (error) { showNotice({ kind: 'error', title: 'Прокси не проверен', text: errorMessage(error), }); return; } setIsProxyChecking(true); try { const result = await pingProxyTarget(parsed.host, parsed.port); setProxyPing(result); showNotice({ kind: result.ok ? 'success' : 'error', title: result.ok ? 'Внешний прокси отвечает' : 'Внешний прокси не ответил', text: pingResultText(result), }); } catch (error) { showNotice({ kind: 'error', title: 'Прокси не проверен', text: errorMessage(error), }); } finally { setIsProxyChecking(false); } } async function generateSingBoxNow() { setSingBoxAction('generate'); try { const result = await generateSingBoxConfig(); await refreshSingBoxState(); showNotice({ kind: 'success', title: 'Конфиг sing-box создан', text: result.generatedConfigPath, }); } catch (error) { showNotice({ kind: 'error', title: 'Конфиг sing-box не создан', text: errorMessage(error), }); } finally { setSingBoxAction(null); } } function startServiceVisual() { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); serviceVisualTimerRef.current = null; } setServiceVisualState('active'); } function settleServiceVisual() { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); } setServiceVisualState('settling'); serviceVisualTimerRef.current = window.setTimeout(() => { setServiceVisualState(null); serviceVisualTimerRef.current = null; }, 700); } function showNotice(notice: Notice) { const entry: LogEntry = { ...notice, id: `log-${Date.now()}-${Math.random().toString(36).slice(2)}`, at: Date.now(), }; setLogEntries((current) => [entry, ...current].slice(0, 40)); setActiveLogId(entry.id); } function renderTabs() { const tabs: Array<{ id: PanelId; label: string }> = [ { id: 'summary', label: 'Сводка' }, { id: 'proxifyre', label: 'ProxiFyre' }, { id: 'proxy', label: 'VPN / Прокси' }, ]; return ; } function renderSummaryPanel() { const appliedRouteMode = appliedSnapshot?.routeMode ?? routeMode; const draftHasLocalServer = currentSnapshot.routeMode === 'local-singbox' && currentSnapshot.selectedServerTag; return (
Применено сейчас {appliedSnapshot ? routeModeLabel(appliedRouteMode) : 'Состояние загружается'}
{hasUnappliedChanges ? 'Есть черновик' : 'Совпадает'}
ProxiFyre
{proxyfierTitle(proxyfier, isDetectingComponents)}
Маршрут
{snapshotRouteLine(appliedSnapshot, singBoxStatus)}
VPN сервер
{snapshotServerText(appliedSnapshot)}
Приложения
{snapshotItemsText(appliedSnapshot)}
Конфиг
{generatedConfigPath || 'Примененный config еще не создан.'}
Черновик интерфейса {hasUnappliedChanges ? 'Есть реальные изменения' : 'Изменений нет'}

{hasUnappliedChanges ? 'Текущие настройки отличаются от примененного состояния. Если вернуть значения назад, предупреждение исчезнет.' : 'То, что выбрано в интерфейсе, совпадает с примененной конфигурацией.'}

{hasUnappliedChanges ? (
Будет маршрут
{snapshotRouteLine(currentSnapshot, singBoxStatus)}
VPN сервер
{draftHasLocalServer ? displayServerTag(currentSnapshot.selectedServerTag) : snapshotServerText(currentSnapshot)}
Приложения
{snapshotItemsText(currentSnapshot)}
) : null}
); } function renderProxiFyreCard() { const state = serviceControlState(proxyfier, isDetectingComponents); const visualState = serviceVisualState === 'active' ? 'working' : serviceVisualState === 'settling' ? 'settling' : null; const primaryAction = proxyfier?.installed ? { label: proxyfier.running ? 'Остановить' : 'Запустить', onClick: () => void setProxiFyreServiceRunning(!proxyfier.running), variant: proxyfier.running ? 'danger' as const : 'neutral' as const, loading: serviceAction === 'start' || serviceAction === 'stop', loadingLabel: serviceAction === 'start' ? 'Запускаю' : 'Останавливаю', disabled: isDetectingComponents || Boolean(serviceAction), } : { label: 'Установить', onClick: () => void installProxiFyrePackage(), variant: 'primary' as const, loading: serviceAction === 'install', loadingLabel: 'Устанавливаю', disabled: isDetectingComponents || Boolean(serviceAction), }; return ( void uninstallProxiFyrePackage(), }], } : undefined} /> ); } function renderProxiFyreSetupStrip() { const stripItems = setupStatus?.items ?? proxifyreSetupPlaceholders(); return (
Состав
{stripItems.map((item) => (
))}
); } function renderAppsSection() { return (

Приложения

{items.length}
{renderApplyActions('proxifyre', { showConfigPath: false })}
{isProcessInputOpen ? (
setProcessInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addProcess(); if (event.key === 'Escape') { setProcessInput(''); setIsProcessInputOpen(false); } }} placeholder="Discord" spellCheck={false} autoFocus />
) : (
Новое приложение Добавь процесс, EXE-файл или папку
setIsProcessInputOpen(true)} label="Добавить процесс" icon={} /> void pickAndAddItem('exe')} disabled={Boolean(pickerAction)} loading={pickerAction === 'exe'} label="Добавить EXE-файл" icon={} /> void pickAndAddItem('folder')} disabled={Boolean(pickerAction)} loading={pickerAction === 'folder'} label="Добавить папку" icon={} />
)}
{isLoading ? (
) : items.length ? ( items.map((item) => (
{item.value} {itemTypeLabel(item.type)}
)) ) : (
Список пуст. Добавь первое приложение сверху.
)}
); } function renderApplyActions( context: 'proxifyre' | 'proxy', options: { showConfigPath?: boolean } = {}, ) { const showConfigPath = options.showConfigPath ?? true; const externalProxyError = routeMode === 'external' ? safeProxyError(proxyInput) : null; const readiness = getApplyReadiness({ routeMode, appCount: items.length, proxiFyreInstalled: Boolean(proxyfier?.installed), singBoxInstalled: isSingBoxInstalled, selectedServerTag: singBoxStatus?.config.selectedServerTag, externalProxyValue: proxyInput, externalProxyError, busy: isApplying || Boolean(serviceAction) || Boolean(singBoxAction), }); const showBlocker = !readiness.ready && !isLoading && !isDetectingComponents; return ( <> {showBlocker ? (
{readiness.title} {readiness.text}
) : hasUnappliedChanges ? (
Изменения еще не применены в ProxiFyre {applyStateText(routeMode, isSingBoxInstalled, Boolean(singbox?.running))}
) : null}
{showConfigPath && generatedConfigPath ?

{generatedConfigPath}

: null} ); } function renderProxiFyrePanel() { return (
{renderProxiFyreCard()} {renderProxiFyreSetupStrip()} {renderAppsSection()}
); } function renderExternalProxyControls() { const proxyValidation = proxyInput.trim() ? safeProxyError(proxyInput) : null; return (
{proxyPing ? pingResultTitle(proxyPing) : proxyValidation ? 'Проверь формат' : 'Проверка не запускалась'} {proxyPing ? pingResultText(proxyPing) : proxyValidation ?? 'TCP check покажет, доступен ли host:port.'}
); } function renderSingBoxCard() { const state = serviceControlState(singbox, isDetectingComponents); const primaryAction = singbox?.installed ? { label: singbox.running ? 'Остановить' : 'Запустить', onClick: () => void setSingBoxServiceRunning(!singbox.running), variant: singbox.running ? 'danger' as const : 'neutral' as const, loading: singBoxAction === 'start' || singBoxAction === 'stop', loadingLabel: singBoxAction === 'start' ? 'Запускаю' : 'Останавливаю', disabled: isDetectingComponents || Boolean(singBoxAction), } : { label: 'Установить', onClick: () => void installSingBoxPackage(), variant: 'primary' as const, loading: singBoxAction === 'install', loadingLabel: 'Устанавливаю', disabled: isDetectingComponents || Boolean(singBoxAction), }; return ( void uninstallSingBoxPackage(), }], } : undefined} inlineActions={( <> setIsSingBoxInfoOpen((current) => !current)} label="Подробности Local sing-box" aria-expanded={isSingBoxInfoOpen} title="Подробности" icon={} /> )} > {isSingBoxInfoOpen ? (
Локально {localSingBoxAddress(singBoxStatus)} LAN {lanSingBoxAddress(singBoxStatus) ?? 'недоступен'} Сервер {selectedServerTag ? displayServerTag(selectedServerTag) : 'не выбран'} Файл {singbox?.path ?? 'не найден'} Конфиг {singBoxStatus?.generatedConfigPath ?? 'не создан'}
) : null} {isSingBoxSetupOpen ? (
{singBoxSetupStatus ? ( singBoxSetupStatus.items.map((item) => (
)) ) : (
)}
) : null} {isSingBoxInstalled ? renderSingBoxWorkspace() : (
Local sing-box не установлен Установи компонент, чтобы подключить подписку, выбрать сервер и включить локальный маршрут.
)}
); } function renderSingBoxWorkspace() { return (
setSubscriptionInput(event.target.value)} placeholder={singBoxStatus?.config.subscriptionDisplayUrl ?? 'https://example.com/sub'} spellCheck={false} /> void forgetSingBoxSubscriptionData()} disabled={Boolean(singBoxAction) || !singBoxStatus?.config.hasSubscription} label="Очистить подписку Local sing-box" title="Очистить" icon={} />
{singBoxStatus?.cache?.servers.length ? (
{singBoxStatus.cache.servers.map((server) => { const ping = serverPings[server.tag]; const selected = server.tag === selectedServerTag; return (
void pingSingleSingBoxServer(server)} disabled={Boolean(serverPingTag)} loading={serverPingTag === server.tag} label={`Проверить ${displayServerTag(server.tag)}`} title="Ping" icon={} />
); })}
) : (
Подписка Local sing-box еще не загружена.
)}
); } function renderProxyPanel() { return (
Настройки VPN / Прокси

Маршрут и проверка соединения

{routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'}
{routeMode === 'external' ? renderExternalProxyControls() : renderSingBoxCard()}
Маршрут {routePreviewText(routeMode, proxyInput, singBoxStatus)}
{renderApplyActions('proxy')}
); } function renderActivePanel() { if (activePanel === 'proxifyre') return renderProxiFyrePanel(); if (activePanel === 'proxy') return renderProxyPanel(); return renderSummaryPanel(); } return (

ProxyWarden

Proxy для приложений
{renderTabs()}
{renderActivePanel()}
setIsLogOpen((current) => !current)} formatTime={formatLogTime} />
); } interface ParsedProxy { protocol: 'socks5'; host: string; port: number; } interface SummaryStateInput { isLoading: boolean; isDetectingComponents: boolean; proxyfier: ComponentStatus | undefined; singbox: ComponentStatus | undefined; routeMode: RouteMode; singBoxStatus: LocalSingBoxStatusResponse | null; proxyPing: PingServerResponse | null; hasUnappliedChanges: boolean; } interface SummaryState { tone: StatusTone; title: string; text: string; } function configSnapshotFromUi( routeMode: RouteMode, proxyInput: string, items: Array>, selectedServerTag?: string, ): ConfigSnapshot { return { routeMode, proxy: routeMode === 'external' ? normalizeProxySnapshot(proxyInput) : '', selectedServerTag: routeMode === 'local-singbox' ? normalizeServerTag(selectedServerTag) : '', items: normalizeSnapshotItems(items), }; } function normalizeProxySnapshot(value: string) { try { const parsed = parseProxy(value); return `${parsed.protocol}://${parsed.host.trim().toLowerCase()}:${parsed.port}`; } catch { return value.trim().toLowerCase(); } } function normalizeServerTag(tag: string | undefined) { return tag?.trim() ?? ''; } function normalizeSnapshotItems(items: Array>): ConfigSnapshotItem[] { return items .map((item) => ({ type: item.type, value: normalizeItemValue(item.value, item.type).toLowerCase(), })) .filter((item) => item.value) .sort((left, right) => `${left.type}:${left.value}`.localeCompare(`${right.type}:${right.value}`)); } function sameConfigSnapshot(left: ConfigSnapshot, right: ConfigSnapshot) { if (left.routeMode !== right.routeMode) return false; if (left.proxy !== right.proxy) return false; if (left.selectedServerTag !== right.selectedServerTag) return false; if (left.items.length !== right.items.length) return false; return left.items.every((item, index) => { const other = right.items[index]; return item.type === other.type && item.value === other.value; }); } function routeModeLabel(routeMode: RouteMode) { return routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'; } function snapshotRouteLine(snapshot: ConfigSnapshot | null, status: LocalSingBoxStatusResponse | null) { if (!snapshot) return 'Сохраненное состояние еще не загружено.'; if (snapshot.routeMode === 'local-singbox') { const server = snapshot.selectedServerTag ? displayServerTag(snapshot.selectedServerTag) : 'сервер не выбран'; return `Выбранные приложения -> ProxiFyre -> Local sing-box ${localSingBoxAddress(status)} -> ${server}`; } return `Выбранные приложения -> ProxiFyre -> внешний прокси ${displaySnapshotProxy(snapshot.proxy)}`; } function snapshotServerText(snapshot: ConfigSnapshot | null) { if (!snapshot) return 'Сохраненное состояние еще не загружено.'; if (snapshot.routeMode === 'external') return 'Не используется: применен внешний прокси.'; return snapshot.selectedServerTag ? displayServerTag(snapshot.selectedServerTag) : 'Сервер не выбран.'; } function snapshotItemsText(snapshot: ConfigSnapshot | null) { if (!snapshot) return 'Сохраненное состояние еще не загружено.'; return snapshot.items.length ? appCountText(snapshot.items.length) : 'Приложения не настроены.'; } function displaySnapshotProxy(proxy: string) { return proxy.replace(/^socks5:\/\//, '') || 'не указан'; } function systemSummaryState(input: SummaryStateInput): SummaryState { if (input.isLoading || input.isDetectingComponents) { return { tone: 'checking', title: 'Проверяю', text: 'Обновляю сохраненный маршрут, компоненты и локальный proxy path.', }; } if (!input.proxyfier?.installed) { return { tone: 'error', title: 'Не настроено', text: 'ProxiFyre не найден. Перейди в настройки ProxiFyre и установи компонент.', }; } if (!input.proxyfier.running) { return { tone: 'warning', title: 'Требует внимания', text: 'ProxiFyre установлен, но служба сейчас остановлена.', }; } if (input.routeMode === 'local-singbox') { if (!input.singbox?.installed) { return { tone: 'warning', title: 'Локальный прокси не готов', text: 'Для локального маршрута нужно установить Local sing-box.', }; } if (!input.singbox.running) { return { tone: 'warning', title: 'Локальный прокси остановлен', text: 'Local sing-box установлен, но служба не запущена.', }; } if (!input.singBoxStatus?.config.selectedServerTag) { return { tone: 'warning', title: 'Сервер не выбран', text: 'Выбери сервер Local sing-box на вкладке VPN / Прокси.', }; } } if (input.routeMode === 'external' && input.proxyPing && !input.proxyPing.ok) { return { tone: 'warning', title: 'Прокси не ответил', text: pingResultText(input.proxyPing), }; } if (input.hasUnappliedChanges) { return { tone: 'warning', title: 'Есть непримененные изменения', text: 'Настройки изменены, но ProxiFyre еще не обновлен.', }; } return { tone: 'ok', title: 'Работает', text: 'Сохраненная конфигурация выглядит готовой к маршрутизации выбранных приложений.', }; } function routeEndpointLabel( routeMode: RouteMode, proxyInput: string, status: LocalSingBoxStatusResponse | null, ) { if (routeMode === 'local-singbox') { const lan = lanSingBoxAddress(status); return lan ? `${localSingBoxAddress(status)} · LAN: ${lan}` : localSingBoxAddress(status); } try { const parsed = parseProxy(proxyInput); return formatHostPort(parsed.host, parsed.port); } catch { return proxyInput.trim() || 'не указан'; } } function appCountText(count: number) { if (count === 1) return '1 приложение маршрутизируется через профиль.'; if (count > 1 && count < 5) return `${count} приложения маршрутизируются через профиль.`; return `${count} приложений маршрутизируются через профиль.`; } function safeProxyError(value: string) { try { parseProxy(value); return null; } catch (error) { return errorMessage(error); } } function pingTone(ping: PingServerResponse): StatusTone { return ping.ok ? 'ok' : 'error'; } function pingResultTitle(ping: PingServerResponse) { return ping.ok ? 'Доступен' : 'Недоступен'; } function pingResultText(ping: PingServerResponse) { if (ping.ok) return `${ping.server}:${ping.serverPort} ответил за ${ping.latency ?? 0} ms.`; return `${ping.server}:${ping.serverPort}: ${ping.error ?? 'нет ответа'}`; } function routePreviewText( routeMode: RouteMode, proxyInput: string, status: LocalSingBoxStatusResponse | null, ) { if (routeMode === 'local-singbox') { const server = status?.config.selectedServerTag ? displayServerTag(status.config.selectedServerTag) : 'сервер не выбран'; return `Выбранные приложения -> ProxiFyre -> Local sing-box ${localSingBoxAddress(status)} -> ${server}`; } return `Выбранные приложения -> ProxiFyre -> внешний прокси ${routeEndpointLabel('external', proxyInput, status)}`; } function applyButtonLabel( context: 'proxifyre' | 'proxy', isApplying: boolean, hasUnappliedChanges: boolean, singBoxAction: SingBoxAction | null, ) { if (isApplying) { if (singBoxAction === 'start') return 'Запускаю sing-box...'; if (singBoxAction === 'stop') return 'Перезапускаю sing-box...'; return 'Применяю...'; } if (hasUnappliedChanges) return context === 'proxy' ? 'Применить маршрут' : 'Применить в ProxiFyre'; return context === 'proxy' ? 'Обновить маршрут' : 'Обновить конфиг'; } function parseProxy(rawValue: string): ParsedProxy { const value = rawValue.trim(); if (!value) throw new Error('Введи адрес прокси.'); const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `socks5://${value}`; let parsed: URL; try { parsed = new URL(withProtocol); } catch { throw new Error('Формат: socks5://host:port или host:port.'); } const protocol = parsed.protocol.replace(':', '').toLowerCase(); if (protocol !== 'socks5') { throw new Error('Сейчас поддерживается только SOCKS5.'); } if (parsed.username || parsed.password) { throw new Error('Прокси с логином и паролем пока не поддерживаются.'); } const host = parsed.hostname.replace(/^\[|\]$/g, ''); const port = Number(parsed.port); if (!host || !Number.isInteger(port) || port < 1 || port > 65535) { throw new Error('Укажи хост и порт прокси.'); } return { protocol: 'socks5', host, port }; } function targetForUi(targets: Target[], profile: Profile | undefined) { if (profile) return targets.find((target) => target.id === profile.targetId); return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external'); } function targetForExternalProxy(targets: Target[]) { return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external' && target.id !== LOCAL_SINGBOX_TARGET_ID); } function itemsForProfiles(profiles: Profile[]): DraftItem[] { const seen = new Set(); const items: DraftItem[] = []; for (const profile of profiles) { for (const item of profile.items) { if (item.type !== 'process' && item.type !== 'folder' && item.type !== 'exe') continue; const key = `${item.type}:${item.value.trim().toLowerCase()}`; if (seen.has(key)) continue; seen.add(key); items.push({ id: `${item.type}-${items.length}-${item.value}`, type: item.type, value: item.value, }); } } return items; } function formatProxy(target: Target) { return target.protocol === 'socks5' ? `${target.host}:${target.port}` : `${target.protocol}://${target.host}:${target.port}`; } function normalizeItemValue(value: string, type: DraftItemType) { const clean = value.trim().replace(/^"|"$/g, ''); if (!clean) return ''; if (type === 'folder' || type === 'exe') return clean; return clean .split(/[\\/]/) .pop() ?.replace(/\.exe$/i, '') .trim() ?? ''; } async function pickPath(type: Extract) { const selected = await open( type === 'folder' ? { title: 'Выбери папку', directory: true, multiple: false, } : { title: 'Выбери EXE-файл', directory: false, multiple: false, filters: [{ name: 'EXE-файлы', extensions: ['exe'] }], }, ); if (Array.isArray(selected)) return selected[0] ?? null; return selected; } function nextFrame() { return new Promise((resolve) => { window.requestAnimationFrame(() => resolve()); }); } function profileItemInput(item: DraftItem): ProfileItemInput { return { type: item.type, value: item.value, recursive: item.type === 'folder', }; } function emptyItemMessage(type: DraftItemType) { if (type === 'process') return 'Введи имя процесса.'; if (type === 'folder') return 'Введи путь к папке.'; return 'Введи путь к EXE-файлу.'; } function itemTypeLabel(type: DraftItemType) { if (type === 'process') return 'процесс'; if (type === 'folder') return 'папка'; return 'EXE-файл'; } function itemIcon(type: DraftItemType) { if (type === 'process') return ; if (type === 'folder') return ; return ; } function setupItemDetails(installed: boolean, version: string | undefined, details: string) { if (!installed) return `Нужно установить. ${details}`; if (version) return `${version}. ${details}`; return details; } function proxifyreSetupPlaceholders(): ProxiFyreSetupStatus['items'] { return [ { id: 'vc-runtime', name: 'Среда запуска', installed: false, details: 'Проверяю' }, { id: 'packet-filter', name: 'Сетевой драйвер', installed: false, details: 'Проверяю' }, { id: 'proxifyre', name: 'Клиент ProxiFyre', installed: false, details: 'Проверяю' }, ]; } function setupItemUserName(id: string, fallbackName: string) { if (id === 'vc-runtime') return 'Среда запуска'; if (id === 'packet-filter') return 'Сетевой драйвер'; if (id === 'proxifyre') return 'Клиент ProxiFyre'; return fallbackName; } function setupItemShortStatus(item: ProxiFyreSetupStatus['items'][number]) { if (item.details === 'Проверяю') return 'проверяю'; if (!item.installed) return 'нужно установить'; if (item.id === 'proxifyre') return item.version?.includes('не запущена') ? 'остановлен' : 'запущен'; return 'готово'; } function profileInputFromProfile(profile: Profile, enabled: boolean) { return { id: profile.id, name: profile.name, enabled, targetId: profile.targetId, protocols: profile.protocols, items: profile.items.map((item) => ({ type: item.type, value: item.value, recursive: item.recursive, })), }; } function proxyfierTitle(component: ComponentStatus | undefined, checking: boolean) { if (checking) return 'Проверяю ProxiFyre'; if (!component) return 'ProxiFyre не проверен'; if (component.running) return 'ProxiFyre найден и запущен'; if (component.installed) return 'ProxiFyre найден'; return 'ProxiFyre не найден'; } function proxyfierDetails(component: ComponentStatus | undefined, checking: boolean) { if (checking) return 'Ищу установленный клиент и состояние службы.'; if (!component) return 'Нажми «Обновить», чтобы проверить компьютер.'; if (component.path) return component.path; return component.problems[0] ?? 'Путь установки не найден.'; } function singBoxTitle(component: ComponentStatus | undefined, checking: boolean) { if (checking) return 'Проверяю Local sing-box'; if (!component) return 'Local sing-box не проверен'; if (component.running) return 'Local sing-box найден и запущен'; if (component.installed) return 'Local sing-box найден'; return 'Local sing-box не установлен'; } function singBoxDetails( component: ComponentStatus | undefined, status: LocalSingBoxStatusResponse | null, checking: boolean, ) { if (checking) return 'Ищу sing-box, wrapper и службу.'; if (component?.running && status) { const lanAddress = lanSingBoxAddress(status); return lanAddress ? `Доступен локально: ${localSingBoxAddress(status)} · LAN: ${lanAddress}` : `Доступен локально: ${localSingBoxAddress(status)}`; } if (component?.installed) return 'Служба остановлена. Запусти Local sing-box перед применением маршрута.'; if (status?.config.hasSubscription) { return status.config.subscriptionDisplayUrl ?? 'Подписка сохранена.'; } return component?.problems[0] ?? 'Установи компонент, чтобы подключить подписку и выбрать сервер.'; } function componentDetails(component: ComponentStatus | undefined, checking: boolean) { if (checking) return 'Проверяю состояние службы.'; if (!component) return 'Компонент не проверен.'; if (component.path) return component.path; return component.problems[0] ?? 'Путь установки не найден.'; } function noticeFromApply(result: ApplyProfilesResponse): Notice { return { kind: result.success ? 'success' : 'error', title: result.success ? 'Конфиг обновлен' : 'Конфиг создан, но не применен', text: result.message, }; } function noticeFromLocalApply(result: ApplyProfilesResponse, singBoxGeneratedPath: string): Notice { return { kind: result.success ? 'success' : 'error', title: result.success ? 'Local sing-box применен' : 'Конфиг создан, но не применен', text: singBoxGeneratedPath ? `${result.message} sing-box: ${singBoxGeneratedPath}` : result.message, }; } function applyStateText(routeMode: RouteMode, isSingBoxInstalled: boolean, isSingBoxRunning: boolean) { if (routeMode === 'local-singbox' && isSingBoxInstalled && !isSingBoxRunning) { return 'Local sing-box сейчас остановлен. При применении клиент сначала запустит службу, затем обновит ProxiFyre.'; } if (routeMode === 'local-singbox' && isSingBoxInstalled && isSingBoxRunning) { return 'При применении клиент обновит конфиг, перезапустит Local sing-box и затем обновит ProxiFyre.'; } return 'Нажми «Применить в ProxiFyre». Если служба уже запущена и маршрут не обновился, перезапусти ProxiFyre.'; } function upsertComponent(components: ComponentStatus[], component: ComponentStatus) { const index = components.findIndex((current) => current.id === component.id); if (index === -1) return [...components, component]; return [ ...components.slice(0, index), component, ...components.slice(index + 1), ]; } function sameValue(left: string, right: string) { return left.trim().toLowerCase() === right.trim().toLowerCase(); } function formatLogTime(timestamp: number) { return new Date(timestamp).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit', }); } function serverLabel(server: SubscriptionServer) { return `${server.type} · ${server.server}:${server.serverPort}`; } function serverTooltip(server: SubscriptionServer, ping: PingServerResponse | undefined) { const details = serverLabel(server); if (!ping) return details; return ping.ok ? `${details} · ping ${ping.latency ?? 0} ms` : `${details} · ping fail`; } function displayServerTag(tag: string) { const withoutFlags = tag .replace(/[\u{1f1e6}-\u{1f1ff}]/gu, '') .replace(/\s*->\s*/g, ' -> ') .replace(/\s*->\s*$/g, '') .replace(/^\s*->\s*/g, '') .replace(/\s{2,}/g, ' ') .trim(); return withoutFlags || tag; } function pingSummary(results: PingServerResponse[]) { if (!results.length) return 'Серверов для проверки нет.'; const ok = results.filter((result) => result.ok); if (!ok.length) return `Не ответил ни один сервер из ${results.length}.`; const best = ok.reduce((current, result) => { if ((result.latency ?? Number.MAX_SAFE_INTEGER) < (current.latency ?? Number.MAX_SAFE_INTEGER)) return result; return current; }); return `Ответили ${ok.length}/${results.length}; быстрее ${best.tag}: ${best.latency ?? 0} ms.`; } function localSingBoxAddress(status: LocalSingBoxStatusResponse | null) { if (!status) return 'не загружен'; const host = status.config.listenHost.trim(); const displayHost = host === '0.0.0.0' || host === '::' || !host ? '127.0.0.1' : host; return formatHostPort(displayHost, status.config.listenPort); } function lanSingBoxAddress(status: LocalSingBoxStatusResponse | null) { if (!status) return null; const host = status.config.listenHost.trim().toLowerCase(); if (host === '127.0.0.1' || host === 'localhost' || host === '::1') return null; const lanHost = host === '0.0.0.0' || host === '::' || !host ? status.lanListenHost : status.config.listenHost; if (!lanHost) return null; return formatHostPort(lanHost, status.config.listenPort); } function formatHostPort(host: string, port: number) { return host.includes(':') && !host.startsWith('[') ? `[${host}]:${port}` : `${host}:${port}`; } function errorMessage(error: unknown) { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; if (error && typeof error === 'object' && 'message' in error) { return String((error as { message: unknown }).message); } return 'Неизвестная ошибка.'; }