Refine VPN client instructions and subscription refresh flow
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 12:34:14 +03:00
parent 6f565ded2e
commit fa3b455fab
11 changed files with 730 additions and 36 deletions

View File

@@ -11,14 +11,17 @@ import {
removeSingboxConfig,
writeSingboxConfig,
} from './singbox.js';
import { fetchSubscription, fetchSubscriptionInfo } from './subscription.js';
import { fetchSubscription, selectRefreshedServer } from './subscription.js';
const MAX_BODY_BYTES = 1_000_000;
const SUBSCRIPTION_REFRESH_INTERVAL_MS = 15 * 60 * 1000;
fs.mkdirSync(settings.dataDir, { recursive: true });
let singboxProcess = null;
let singboxStartedAt = null;
let subscriptionRefreshPromise = null;
let subscriptionRefreshTimer = null;
function readJson(filePath, fallback) {
try {
@@ -195,6 +198,69 @@ async function applySelectedServer(selectedTag) {
});
}
function refreshSavedSubscription() {
if (subscriptionRefreshPromise) return subscriptionRefreshPromise;
subscriptionRefreshPromise = (async () => {
const initialState = readJson(settings.statePath, {});
if (!initialState.subscriptionUrl) {
const error = new Error('Подписка не настроена');
error.statusCode = 400;
throw error;
}
const subscriptionUrl = initialState.subscriptionUrl;
const parsed = await fetchSubscription(subscriptionUrl);
const currentState = readJson(settings.statePath, {});
if (currentState.subscriptionUrl !== subscriptionUrl) {
throw new Error('Подписка была изменена во время обновления');
}
const selectedTag = selectRefreshedServer(currentState.selectedTag, parsed.servers);
const previousCache = readJson(settings.subscriptionCachePath, null);
const previousConfig = fs.existsSync(settings.configPath)
? fs.readFileSync(settings.configPath, 'utf8')
: null;
writeJson(settings.subscriptionCachePath, { url: subscriptionUrl, ...parsed });
try {
if (singboxProcess && selectedTag) await applySelectedServer(selectedTag);
else if (selectedTag) {
writeSingboxConfig(buildGatewayConfig(parsed.config, selectedTag));
} else {
removeSingboxConfig();
}
} catch (error) {
if (previousCache) writeJson(settings.subscriptionCachePath, previousCache);
else fs.rmSync(settings.subscriptionCachePath, { force: true });
if (previousConfig === null) removeSingboxConfig();
else fs.writeFileSync(settings.configPath, previousConfig, 'utf8');
throw error;
}
writeJson(settings.statePath, {
...readJson(settings.statePath, {}),
subscriptionUrl,
servers: parsed.servers,
userInfo: parsed.userInfo,
fetchedAt: parsed.fetchedAt,
selectedTag,
});
return {
success: true,
servers: parsed.servers,
userInfo: parsed.userInfo,
fetchedAt: parsed.fetchedAt,
selectedTag,
};
})().finally(() => {
subscriptionRefreshPromise = null;
});
return subscriptionRefreshPromise;
}
async function handleApi(req, res) {
if (req.method === 'GET' && req.url === '/api/state') {
return sendJson(res, 200, publicState());
@@ -236,16 +302,8 @@ async function handleApi(req, res) {
return sendJson(res, 200, { success: true, ...parsed });
}
if (req.method === 'POST' && req.url === '/api/subscription/refresh-info') {
const state = readJson(settings.statePath, {});
if (!state.subscriptionUrl) {
return sendJson(res, 400, { success: false, error: 'Подписка не настроена' });
}
const info = await fetchSubscriptionInfo(state.subscriptionUrl);
writeJson(settings.statePath, { ...state, ...info });
const cached = readJson(settings.subscriptionCachePath, null);
if (cached) writeJson(settings.subscriptionCachePath, { ...cached, ...info });
return sendJson(res, 200, { success: true, ...info });
if (req.method === 'POST' && req.url === '/api/subscription/refresh') {
return sendJson(res, 200, await refreshSavedSubscription());
}
if (req.method === 'DELETE' && req.url === '/api/subscription') {
@@ -319,6 +377,7 @@ const server = http.createServer(async (req, res) => {
});
async function shutdown() {
clearInterval(subscriptionRefreshTimer);
await stopSingbox();
process.exit(0);
}
@@ -336,3 +395,10 @@ await startSingbox().catch((error) => console.warn(`[control] sing-box не за
server.listen(settings.port, '0.0.0.0', () => {
console.log(`[control] ${settings.appMode} UI слушает :${settings.port}`);
});
subscriptionRefreshTimer = setInterval(() => {
if (!readJson(settings.statePath, {}).subscriptionUrl) return;
refreshSavedSubscription()
.catch((error) => console.warn(`[control] подписка не обновлена: ${error.message}`));
}, SUBSCRIPTION_REFRESH_INTERVAL_MS);
subscriptionRefreshTimer.unref();