Add device HWID support to sing-box subscriptions

This commit is contained in:
2026-07-08 20:31:21 +03:00
parent 7316e932f0
commit 42b85cc8fa
12 changed files with 450 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
import { Cpu, FileCode2, Fingerprint, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
import {
applyProfiles,
fetchSingBoxSubscription,
@@ -19,6 +19,7 @@ import {
pingSingBoxServer,
restartAsAdmin,
saveProfile,
saveSingBoxDeviceHwid,
saveSingBoxSubscription,
saveTarget,
selectSingBoxServer,
@@ -44,7 +45,7 @@ import { serviceControlState } from './viewModel';
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
type ProxiFyreAction = 'start' | 'stop' | 'restart' | 'install' | 'uninstall';
type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'generate' | 'ping';
type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'identity' | 'generate' | 'ping';
type RouteMode = 'external' | 'local-singbox';
type ServiceVisualState = 'active' | 'settling' | null;
type PanelId = 'summary' | 'proxifyre' | 'proxy';
@@ -184,6 +185,7 @@ export function App() {
const [singBoxStatus, setSingBoxStatus] = useState<LocalSingBoxStatusResponse | null>(null);
const [singBoxSetupStatus, setSingBoxSetupStatus] = useState<SingBoxSetupStatus | null>(null);
const [subscriptionInput, setSubscriptionInput] = useState('');
const [deviceHwidInput, setDeviceHwidInput] = useState('');
const [serverPings, setServerPings] = useState<Record<string, PingServerResponse>>({});
const [proxyCheck, setProxyCheck] = useState<ProxyTargetCheckResponse | null>(null);
const [adminStatus, setAdminStatus] = useState<AdminStatusResponse | null>(null);
@@ -711,6 +713,7 @@ export function App() {
async function syncSingBoxSubscription() {
const subscriptionUrl = subscriptionInput.trim();
const deviceHwid = deviceHwidInput.trim();
if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) {
showNotice({
kind: 'error',
@@ -723,12 +726,15 @@ export function App() {
setSingBoxAction('fetch');
try {
if (subscriptionUrl) {
await saveSingBoxSubscription(subscriptionUrl);
await saveSingBoxSubscription(subscriptionUrl, deviceHwid || undefined);
} else if (deviceHwid) {
await saveSingBoxDeviceHwid(deviceHwid);
}
const status = await fetchSingBoxSubscription();
setSingBoxStatus(status);
setComponents((current) => upsertComponent(current, status.component));
setSubscriptionInput('');
setDeviceHwidInput('');
setServerPings({});
setProxyCheck(null);
showNotice({
@@ -747,6 +753,64 @@ export function App() {
}
}
async function saveSingBoxDeviceHwidData() {
const deviceHwid = deviceHwidInput.trim();
if (!deviceHwid) {
showNotice({
kind: 'error',
title: 'HWID не указан',
text: 'Вставь hard ID устройства для заголовка X-Hwid.',
});
return;
}
setSingBoxAction('identity');
try {
const status = await saveSingBoxDeviceHwid(deviceHwid);
setSingBoxStatus(status);
setComponents((current) => upsertComponent(current, status.component));
setDeviceHwidInput('');
showNotice({
kind: 'success',
title: 'HWID сохранен',
text: status.config.deviceHwidDisplay
? `Для подписки будет отправляться X-Hwid: ${status.config.deviceHwidDisplay}`
: 'Для подписки будет отправляться X-Hwid.',
});
} catch (error) {
showNotice({
kind: 'error',
title: 'HWID не сохранен',
text: errorMessage(error),
});
} finally {
setSingBoxAction(null);
}
}
async function forgetSingBoxDeviceHwidData() {
setSingBoxAction('identity');
try {
const status = await saveSingBoxDeviceHwid(undefined);
setSingBoxStatus(status);
setComponents((current) => upsertComponent(current, status.component));
setDeviceHwidInput('');
showNotice({
kind: 'info',
title: 'HWID очищен',
text: 'Заголовок X-Hwid больше не будет отправляться при загрузке подписки.',
});
} catch (error) {
showNotice({
kind: 'error',
title: 'HWID не очищен',
text: errorMessage(error),
});
} finally {
setSingBoxAction(null);
}
}
async function forgetSingBoxSubscriptionData() {
setSingBoxAction('forget');
setIsSingBoxMenuOpen(false);
@@ -759,7 +823,7 @@ export function App() {
showNotice({
kind: 'info',
title: 'Подписка очищена',
text: 'Ссылка, cache и выбранный сервер Local sing-box удалены.',
text: 'Ссылка, HWID, cache и выбранный сервер Local sing-box удалены.',
});
} catch (error) {
showNotice({
@@ -1413,6 +1477,7 @@ export function App() {
loading={singBoxAction === 'fetch'}
loadingLabel="Загружаю"
variant="neutral"
disabled={Boolean(singBoxAction)}
>
{subscriptionInput.trim() || !singBoxStatus?.config.hasSubscription ? 'Загрузить' : 'Обновить'}
</Button>
@@ -1427,6 +1492,38 @@ export function App() {
/>
</div>
<div className="subscription-line identity-line">
<span className="subscription-icon" aria-hidden="true">
<Fingerprint size={18} strokeWidth={1.9} />
</span>
<input
value={deviceHwidInput}
onChange={(event) => setDeviceHwidInput(event.target.value)}
placeholder={singBoxStatus?.config.deviceHwidDisplay ?? 'X-Hwid hard ID'}
spellCheck={false}
aria-label="Hard ID устройства для X-Hwid"
/>
<Button
type="button"
onClick={() => void saveSingBoxDeviceHwidData()}
loading={singBoxAction === 'identity'}
loadingLabel="Сохраняю"
variant="neutral"
disabled={Boolean(singBoxAction) || !deviceHwidInput.trim()}
>
{singBoxStatus?.config.hasDeviceHwid ? 'Заменить' : 'Сохранить'}
</Button>
<IconButton
type="button"
variant="danger"
onClick={() => void forgetSingBoxDeviceHwidData()}
disabled={Boolean(singBoxAction) || !singBoxStatus?.config.hasDeviceHwid}
label="Очистить hard ID устройства"
tooltip="Очистить HWID"
icon={<Trash2 size={18} strokeWidth={1.9} />}
/>
</div>
<div className="singbox-workspace-head">
<span>Серверы подписки</span>
<div className="singbox-workspace-actions">
@@ -2565,6 +2662,7 @@ function singBoxDetailLines(
`Файл: ${component?.path ?? 'не найден'}`,
`Конфиг: ${status?.generatedConfigPath ?? 'не создан'}`,
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
`X-Hwid: ${status?.config.deviceHwidDisplay ?? (status?.config.hasDeviceHwid ? 'сохранен' : 'не задан')}`,
`Состав: ${setupDetails}`,
];
}