Refine ProxyWarden routing and config flows
This commit is contained in:
+122
-54
@@ -7,6 +7,7 @@ import {
|
||||
forgetSingBoxSubscription,
|
||||
generateSingBoxConfig,
|
||||
getComponents,
|
||||
getProxiFyreSetupProgress,
|
||||
getProxiFyreSetupStatus,
|
||||
getSavedState,
|
||||
getSingBoxSetupStatus,
|
||||
@@ -34,11 +35,13 @@ import {
|
||||
type PingServerResponse,
|
||||
type ProxyProbeResponse,
|
||||
type ProxyTargetCheckResponse,
|
||||
type ProxiFyreSetupProgress,
|
||||
type ProxiFyreSetupStatus,
|
||||
type SingBoxSetupStatus,
|
||||
} from '../api/tauriCommands';
|
||||
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, SubscriptionServer, Target } from '../domain/types';
|
||||
import { BusyRing, Button, DetailsPopover, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui';
|
||||
import { ProxiFyreSetupStrip } from './components/ProxiFyreSetupStrip';
|
||||
import { parseProxy, type ParsedProxy } from './lib/parseProxy';
|
||||
import { getApplyReadiness } from './readiness';
|
||||
import { serviceControlState } from './viewModel';
|
||||
@@ -183,6 +186,7 @@ export function App() {
|
||||
const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null);
|
||||
const [components, setComponents] = useState<ComponentStatus[]>(fallbackComponents);
|
||||
const [setupStatus, setSetupStatus] = useState<ProxiFyreSetupStatus | null>(null);
|
||||
const [setupProgress, setSetupProgress] = useState<ProxiFyreSetupProgress | null>(null);
|
||||
const [singBoxStatus, setSingBoxStatus] = useState<LocalSingBoxStatusResponse | null>(null);
|
||||
const [singBoxSetupStatus, setSingBoxSetupStatus] = useState<SingBoxSetupStatus | null>(null);
|
||||
const [subscriptionInput, setSubscriptionInput] = useState('');
|
||||
@@ -269,14 +273,39 @@ export function App() {
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [activeLogId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serviceAction !== 'install' && serviceAction !== 'uninstall') return undefined;
|
||||
|
||||
let cancelled = false;
|
||||
const pollProgress = async () => {
|
||||
try {
|
||||
const progress = await getProxiFyreSetupProgress();
|
||||
if (!cancelled) setSetupProgress(progress);
|
||||
} catch {
|
||||
// Progress is best-effort; the main install/uninstall action still reports the real error.
|
||||
}
|
||||
};
|
||||
|
||||
void pollProgress();
|
||||
const timer = window.setInterval(() => void pollProgress(), 650);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [serviceAction]);
|
||||
|
||||
async function refresh() {
|
||||
setIsLoading(true);
|
||||
setIsDetectingComponents(true);
|
||||
try {
|
||||
const snapshot = await getStartupSnapshot();
|
||||
const [snapshot, progress] = await Promise.all([
|
||||
getStartupSnapshot(),
|
||||
getProxiFyreSetupProgress(),
|
||||
]);
|
||||
setAdminStatus(snapshot.adminStatus);
|
||||
setComponents(snapshot.components);
|
||||
setSetupStatus(snapshot.proxifyreSetupStatus);
|
||||
setSetupProgress(progress);
|
||||
setSingBoxStatus(snapshot.singboxStatus);
|
||||
setSingBoxSetupStatus(snapshot.singboxSetupStatus);
|
||||
applySavedState(
|
||||
@@ -562,19 +591,25 @@ export function App() {
|
||||
async function installProxiFyrePackage() {
|
||||
setServiceAction('install');
|
||||
setIsServiceMenuOpen(false);
|
||||
setSetupProgress(localSetupProgress('install', 'packet-filter', 1, 'Готовлю установку сетевого драйвера.'));
|
||||
startServiceVisual();
|
||||
try {
|
||||
await nextFrame();
|
||||
const component = await installProxiFyre();
|
||||
const detectedSetupStatus = await getProxiFyreSetupStatus();
|
||||
const [detectedSetupStatus, detectedProgress] = await Promise.all([
|
||||
getProxiFyreSetupStatus(),
|
||||
getProxiFyreSetupProgress(),
|
||||
]);
|
||||
setComponents((current) => upsertComponent(current, component));
|
||||
setSetupStatus(detectedSetupStatus);
|
||||
setSetupProgress(detectedProgress);
|
||||
showNotice({
|
||||
kind: 'success',
|
||||
title: 'ProxiFyre установлен',
|
||||
text: proxyfierDetails(component, false),
|
||||
});
|
||||
} catch (error) {
|
||||
void getProxiFyreSetupProgress().then(setSetupProgress).catch(() => undefined);
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'ProxiFyre не установлен',
|
||||
@@ -588,25 +623,31 @@ export function App() {
|
||||
|
||||
async function uninstallProxiFyrePackage() {
|
||||
const confirmed = window.confirm(
|
||||
'Удалить ProxiFyre с компьютера? Будет удалена служба и папка установки ProxiFyre.',
|
||||
'Удалить ProxiFyre и Windows Packet Filter с компьютера? Это остановит службу, удалит папку ProxiFyre и сетевой драйвер. Другие программы WireSock могут перестать работать до повторной установки драйвера.',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
setServiceAction('uninstall');
|
||||
setIsServiceMenuOpen(false);
|
||||
setSetupProgress(localSetupProgress('uninstall', 'proxifyre', 1, 'Готовлю удаление ProxiFyre и сетевого драйвера.'));
|
||||
startServiceVisual();
|
||||
try {
|
||||
await nextFrame();
|
||||
const component = await uninstallProxiFyre();
|
||||
const detectedSetupStatus = await getProxiFyreSetupStatus();
|
||||
const [detectedSetupStatus, detectedProgress] = await Promise.all([
|
||||
getProxiFyreSetupStatus(),
|
||||
getProxiFyreSetupProgress(),
|
||||
]);
|
||||
setComponents((current) => upsertComponent(current, component));
|
||||
setSetupStatus(detectedSetupStatus);
|
||||
setSetupProgress(detectedProgress);
|
||||
showNotice({
|
||||
kind: 'success',
|
||||
title: 'ProxiFyre удален',
|
||||
text: 'Служба и папка установки ProxiFyre удалены.',
|
||||
text: 'Служба, папка установки ProxiFyre и Windows Packet Filter удалены.',
|
||||
});
|
||||
} catch (error) {
|
||||
void getProxiFyreSetupProgress().then(setSetupProgress).catch(() => undefined);
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'ProxiFyre не удален',
|
||||
@@ -995,8 +1036,21 @@ export function App() {
|
||||
function renderProxiFyreCard() {
|
||||
const state = serviceControlState(proxyfier, isDetectingComponents);
|
||||
const visualState = serviceVisualState === 'active' ? 'working' : serviceVisualState === 'settling' ? 'settling' : null;
|
||||
const primaryAction = proxyfier?.installed
|
||||
const packetFilterInstalled = Boolean(
|
||||
setupStatus?.items.some((item) => item.id === 'packet-filter' && item.installed),
|
||||
);
|
||||
const canCleanupSetup = Boolean(proxyfier?.installed || packetFilterInstalled);
|
||||
const shouldInstallProxiFyre = !proxyfier?.installed || !proxyfier.serviceStatus;
|
||||
const primaryAction = shouldInstallProxiFyre
|
||||
? {
|
||||
label: proxyfier?.installed ? 'Переустановить' : 'Установить',
|
||||
onClick: () => void installProxiFyrePackage(),
|
||||
variant: 'primary' as const,
|
||||
loading: serviceAction === 'install',
|
||||
loadingLabel: 'Устанавливаю',
|
||||
disabled: isDetectingComponents || Boolean(serviceAction),
|
||||
}
|
||||
: {
|
||||
label: proxyfier.running ? 'Остановить' : 'Запустить',
|
||||
onClick: () => void setProxiFyreServiceRunning(!proxyfier.running),
|
||||
variant: proxyfier.running ? 'danger' as const : 'neutral' as const,
|
||||
@@ -1005,14 +1059,6 @@ export function App() {
|
||||
? 'Перезапускаю'
|
||||
: serviceAction === 'start' ? 'Запускаю' : 'Останавливаю',
|
||||
disabled: isDetectingComponents || Boolean(serviceAction),
|
||||
}
|
||||
: {
|
||||
label: 'Установить',
|
||||
onClick: () => void installProxiFyrePackage(),
|
||||
variant: 'primary' as const,
|
||||
loading: serviceAction === 'install',
|
||||
loadingLabel: 'Устанавливаю',
|
||||
disabled: isDetectingComponents || Boolean(serviceAction),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1023,13 +1069,13 @@ export function App() {
|
||||
title={proxyfierTitle(proxyfier, isDetectingComponents)}
|
||||
detail={proxyfierDetails(proxyfier, isDetectingComponents)}
|
||||
primaryAction={primaryAction}
|
||||
menu={proxyfier?.installed ? {
|
||||
menu={canCleanupSetup ? {
|
||||
label: 'Дополнительные действия ProxiFyre',
|
||||
open: isServiceMenuOpen,
|
||||
onOpenChange: setIsServiceMenuOpen,
|
||||
disabled: isDetectingComponents || Boolean(serviceAction),
|
||||
items: [{
|
||||
label: serviceAction === 'uninstall' ? 'Удаляю...' : 'Удалить ProxiFyre',
|
||||
label: serviceAction === 'uninstall' ? 'Удаляю...' : 'Удалить ProxiFyre и драйвер',
|
||||
danger: true,
|
||||
disabled: Boolean(serviceAction),
|
||||
onClick: () => void uninstallProxiFyrePackage(),
|
||||
@@ -1040,22 +1086,7 @@ export function App() {
|
||||
}
|
||||
|
||||
function renderProxiFyreSetupStrip() {
|
||||
const stripItems = setupStatus?.items ?? proxifyreSetupPlaceholders();
|
||||
|
||||
return (
|
||||
<div className={`setup-strip ${setupStatus?.ready ? 'ready' : 'attention'}`} aria-label="Состав ProxiFyre">
|
||||
<span className="setup-strip-title">Состав</span>
|
||||
<div className="setup-strip-items">
|
||||
{stripItems.map((item) => (
|
||||
<div className={`setup-strip-item ${item.installed ? 'installed' : 'missing'}`} key={item.id}>
|
||||
<span className="setup-strip-dot" aria-hidden="true" />
|
||||
<strong>{setupItemUserName(item.id, item.name)}</strong>
|
||||
<span>{setupItemShortStatus(item)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <ProxiFyreSetupStrip setupStatus={setupStatus} progress={setupProgress} />;
|
||||
}
|
||||
|
||||
function renderAppsSection() {
|
||||
@@ -2096,9 +2127,7 @@ function summaryRouteChainSegments(input: RouteChainInput, flow: SummaryRouteFlo
|
||||
value: 'напрямую',
|
||||
tone: 'warning',
|
||||
details: [
|
||||
input.proxyfier?.installed
|
||||
? 'ProxiFyre остановлен, поэтому выбранные приложения не перехватываются.'
|
||||
: 'ProxiFyre не найден, поэтому выбранные приложения не перехватываются.',
|
||||
proxyfierBypassReason(input.proxyfier),
|
||||
'Пакеты идут обычным системным маршрутом без SOCKS5.',
|
||||
],
|
||||
},
|
||||
@@ -2136,7 +2165,7 @@ function routeChainSegments(input: RouteChainInput): RouteChainSegment[] {
|
||||
{
|
||||
id: 'proxifyre',
|
||||
label: 'ProxiFyre',
|
||||
value: input.isDetectingComponents ? 'проверяю' : input.proxyfier?.running ? 'запущен' : input.proxyfier?.installed ? 'остановлен' : 'не найден',
|
||||
value: proxyfierCompactStatus(input.proxyfier, input.isDetectingComponents),
|
||||
tone: componentChainTone(input.proxyfier, input.isDetectingComponents),
|
||||
details: [
|
||||
proxyfierTitle(input.proxyfier, input.isDetectingComponents),
|
||||
@@ -2449,25 +2478,26 @@ function itemIcon(type: DraftItemType) {
|
||||
return <FileCode2 size={18} strokeWidth={1.9} />;
|
||||
}
|
||||
|
||||
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 localSetupProgress(
|
||||
operation: 'install' | 'uninstall',
|
||||
activeStep: string,
|
||||
percent: number,
|
||||
message: string,
|
||||
): ProxiFyreSetupProgress {
|
||||
return {
|
||||
operation,
|
||||
status: 'running',
|
||||
activeStep,
|
||||
percent,
|
||||
message,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function setupItemShortStatus(item: ProxiFyreSetupStatus['items'][number]) {
|
||||
if (item.details === 'Проверяю') return 'проверяю';
|
||||
if (!item.installed) return 'нужно установить';
|
||||
if (item.id === 'proxifyre') return item.version?.includes('не запущена') ? 'остановлен' : 'запущен';
|
||||
if (item.id === 'proxifyre') return proxifyreSetupServiceSummary(item.version);
|
||||
return 'готово';
|
||||
}
|
||||
|
||||
@@ -2489,16 +2519,54 @@ function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
||||
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 найден';
|
||||
if (component.installed) return `ProxiFyre найден, служба ${serviceStatusLabel(component.serviceStatus)}`;
|
||||
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] ?? 'Путь установки не найден.';
|
||||
const service = proxyfierServiceDetails(component);
|
||||
if (component.path) return service ? `${component.path} · ${service}` : component.path;
|
||||
const problem = component.problems[0] ?? 'Путь установки не найден.';
|
||||
return service ? `${problem} · ${service}` : problem;
|
||||
}
|
||||
|
||||
function proxyfierCompactStatus(component: ComponentStatus | undefined, checking: boolean) {
|
||||
if (checking) return 'проверяю';
|
||||
if (!component?.installed) return 'не найден';
|
||||
return `служба ${serviceStatusLabel(component.serviceStatus)}`;
|
||||
}
|
||||
|
||||
function proxyfierBypassReason(component: ComponentStatus | undefined) {
|
||||
if (!component?.installed) {
|
||||
return 'ProxiFyre не найден, поэтому выбранные приложения не перехватываются.';
|
||||
}
|
||||
return `Служба ProxiFyre ${serviceStatusLabel(component.serviceStatus)}, поэтому выбранные приложения не перехватываются.`;
|
||||
}
|
||||
|
||||
function proxyfierServiceDetails(component: ComponentStatus) {
|
||||
const serviceName = component.serviceName ?? 'ProxiFyreService';
|
||||
return `служба ${serviceName}: ${serviceStatusLabel(component.serviceStatus)}`;
|
||||
}
|
||||
|
||||
function serviceStatusLabel(status: string | undefined) {
|
||||
const normalized = status?.trim().toLowerCase();
|
||||
if (!normalized) return 'не установлена';
|
||||
if (normalized === 'running') return 'запущена';
|
||||
if (normalized === 'stopped') return 'остановлена';
|
||||
if (normalized === 'startpending' || normalized === 'start_pending') return 'запускается';
|
||||
if (normalized === 'stoppending' || normalized === 'stop_pending') return 'останавливается';
|
||||
if (normalized === 'paused') return 'на паузе';
|
||||
return status;
|
||||
}
|
||||
|
||||
function proxifyreSetupServiceSummary(version: string | undefined) {
|
||||
const normalized = version?.trim().toLowerCase() ?? '';
|
||||
if (normalized.includes('не установлена')) return 'служба не установлена';
|
||||
if (normalized.includes('остановлена') || normalized.includes('не запущена')) return 'служба остановлена';
|
||||
if (normalized.includes('запущена')) return 'служба запущена';
|
||||
return 'готово';
|
||||
}
|
||||
|
||||
function singBoxDetails(
|
||||
|
||||
Reference in New Issue
Block a user