xray
This commit is contained in:
@@ -3,7 +3,10 @@ import { chromium, Browser, BrowserContext } from "playwright";
|
||||
let browser: Browser | null = null;
|
||||
|
||||
export async function getBrowser(): Promise<Browser> {
|
||||
if (!browser) {
|
||||
if (!browser || !browser.isConnected()) {
|
||||
if (browser) {
|
||||
try { await browser.close().catch(() => {}); } catch {}
|
||||
}
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const exitHandler = () => browser?.close().catch(() => {});
|
||||
process.on("exit", exitHandler);
|
||||
|
||||
@@ -5,29 +5,21 @@ export interface PanelConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function getPanels(): PanelConfig[] {
|
||||
const panels: PanelConfig[] = [];
|
||||
|
||||
const url1 = process.env.XRAY_URL;
|
||||
const user1 = process.env.XRAY_USERNAME;
|
||||
const pass1 = process.env.XRAY_PASSWORD;
|
||||
if (url1 && user1 && pass1) {
|
||||
panels.push({ url: url1, username: user1, password: pass1, label: "xray1" });
|
||||
export function getPanel(): PanelConfig | null {
|
||||
const url = process.env.XRAY_URL;
|
||||
const user = process.env.XRAY_USERNAME;
|
||||
const pass = process.env.XRAY_PASSWORD;
|
||||
if (url && user && pass) {
|
||||
return { url, username: user, password: pass, label: "xray" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const url2 = process.env.XRAY_URL2;
|
||||
const user2 = process.env.XRAY_USERNAME2;
|
||||
const pass2 = process.env.XRAY_PASSWORD2;
|
||||
if (url2 && user2 && pass2) {
|
||||
panels.push({ url: url2, username: user2, password: pass2, label: "xray2" });
|
||||
}
|
||||
|
||||
return panels;
|
||||
export function getConnectionNamePrefix(): string {
|
||||
return process.env.name_url || "novavps";
|
||||
}
|
||||
|
||||
export function validCredentials(username: string, password: string): boolean {
|
||||
const panels = getPanels();
|
||||
return panels.some(
|
||||
(p) => p.username === username && p.password === password
|
||||
);
|
||||
const panel = getPanel();
|
||||
return panel !== null && panel.username === username && panel.password === password;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { existsSync } from "fs";
|
||||
const DATA_DIR = path.resolve(__dirname, "..", "data");
|
||||
|
||||
export interface AppConfig {
|
||||
novavpsUrl: string;
|
||||
subscriptionUrl: string;
|
||||
}
|
||||
|
||||
export interface PingResult {
|
||||
@@ -34,15 +34,14 @@ export interface AppState {
|
||||
status: "idle" | "running" | "completed" | "error";
|
||||
currentStep: string;
|
||||
novavps: PanelStatus;
|
||||
xray1: PanelStatus;
|
||||
xray2: PanelStatus;
|
||||
xray: PanelStatus;
|
||||
}
|
||||
|
||||
const CONFIG_FILE = path.join(DATA_DIR, "config.json");
|
||||
const STATE_FILE = path.join(DATA_DIR, "state.json");
|
||||
|
||||
const defaultConfig: AppConfig = {
|
||||
novavpsUrl: "",
|
||||
subscriptionUrl: "",
|
||||
};
|
||||
|
||||
const defaultState: AppState = {
|
||||
@@ -50,8 +49,7 @@ const defaultState: AppState = {
|
||||
status: "idle",
|
||||
currentStep: "",
|
||||
novavps: { status: "idle", count: 0, timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
xray: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
};
|
||||
|
||||
async function ensureDataDir(): Promise<void> {
|
||||
@@ -73,10 +71,10 @@ async function writeJson<T>(filePath: string, data: T): Promise<void> {
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export async function loadConfig(envNovavpsUrl: string): Promise<AppConfig> {
|
||||
export async function loadConfig(envSubscriptionUrl: string): Promise<AppConfig> {
|
||||
const saved = await readJson<AppConfig>(CONFIG_FILE, defaultConfig);
|
||||
if (!saved.novavpsUrl && envNovavpsUrl) {
|
||||
saved.novavpsUrl = envNovavpsUrl;
|
||||
if (!saved.subscriptionUrl && envSubscriptionUrl) {
|
||||
saved.subscriptionUrl = envSubscriptionUrl;
|
||||
await writeJson(CONFIG_FILE, saved);
|
||||
}
|
||||
return saved;
|
||||
@@ -101,8 +99,7 @@ export async function resetState(): Promise<AppState> {
|
||||
status: "running",
|
||||
currentStep: "initializing",
|
||||
novavps: { status: "idle", count: 0, timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
xray: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
};
|
||||
await saveState(state);
|
||||
return state;
|
||||
|
||||
52
src/index.ts
52
src/index.ts
@@ -2,21 +2,21 @@ import dotenv from "dotenv";
|
||||
import { parseNovavps, getValidConnections } from "./novavps";
|
||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||
import { closeBrowser } from "./browser";
|
||||
import { getPanels } from "./config";
|
||||
import { getPanel } from "./config";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function main() {
|
||||
const novavpsUrl = process.env.NOVAVPS_URL;
|
||||
const novavpsUrl = process.env.subscription_url;
|
||||
|
||||
if (!novavpsUrl) {
|
||||
console.error("NOVAVPS_URL not set in .env");
|
||||
console.error("subscription_url not set in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const panels = getPanels();
|
||||
if (panels.length === 0) {
|
||||
console.error("Нет настроенных Xray панелей (XRAY_URL или XRAY_URL2)");
|
||||
const panel = getPanel();
|
||||
if (!panel) {
|
||||
console.error("Нет настроенной Xray панели (XRAY_URL)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -33,30 +33,26 @@ async function main() {
|
||||
const valid = getValidConnections(novavpsConnections);
|
||||
console.log(`[result] valid (без auto): ${valid.length} connections\n`);
|
||||
|
||||
for (const panel of panels) {
|
||||
console.log(`--- ${panel.label}: ${panel.url} ---`);
|
||||
|
||||
try {
|
||||
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
console.log(`[${panel.label}] Найдено outbounds: ${xray.length}`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
if (valid.length > 0) {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
console.log(`[${panel.label}] Синхронизация завершена`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(`--- ${panel.label}: ${panel.url} ---`);
|
||||
try {
|
||||
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
console.log(`[${panel.label}] Найдено outbounds: ${xray.length}`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
if (valid.length > 0) {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
console.log(`[${panel.label}] Синхронизация завершена`);
|
||||
} catch (err) {
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
await closeBrowser();
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function parseNovavps(url: string): Promise<NovavpsConnection[]> {
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const linksJson = await page.evaluate(() => {
|
||||
|
||||
277
src/server.ts
277
src/server.ts
@@ -20,7 +20,7 @@ import {
|
||||
resetState,
|
||||
AppState,
|
||||
} from "./data-store";
|
||||
import { getPanels, validCredentials, PanelConfig } from "./config";
|
||||
import { getPanel, getConnectionNamePrefix, validCredentials, PanelConfig } from "./config";
|
||||
|
||||
// Prometheus Metrics Initialization
|
||||
const prometheusRegistry = new promClient.Registry();
|
||||
@@ -31,12 +31,6 @@ const parserRunsTotal = new promClient.Counter({
|
||||
labelNames: ['status'],
|
||||
});
|
||||
|
||||
const xray2SyncRunsTotal = new promClient.Counter({
|
||||
name: 'xray2_sync_runs_total',
|
||||
help: 'Total attempts to synchronize Xray2 panel settings.',
|
||||
labelNames: ['status'],
|
||||
});
|
||||
|
||||
const novavpsConnectionsCount = new promClient.Gauge({
|
||||
name: 'novavps_connections_count',
|
||||
help: 'Current number of valid Novavps connections loaded.',
|
||||
@@ -113,13 +107,14 @@ setInterval(() => {
|
||||
async function syncPanelWithPing(
|
||||
panel: PanelConfig,
|
||||
valid: NovavpsConnection[],
|
||||
state: AppState,
|
||||
stateKey: "xray1" | "xray2"
|
||||
state: AppState
|
||||
): Promise<void> {
|
||||
if (valid.length === 0) return;
|
||||
|
||||
const namePrefix = getConnectionNamePrefix();
|
||||
|
||||
try {
|
||||
state.currentStep = stateKey === "xray1" ? "pinging" : "xray2_pinging";
|
||||
state.currentStep = "pinging";
|
||||
await saveState(state);
|
||||
|
||||
const pingResults: Array<{
|
||||
@@ -128,7 +123,7 @@ async function syncPanelWithPing(
|
||||
}> = [];
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const conn = valid[i];
|
||||
const tag = `novavps${i + 1}`;
|
||||
const tag = `${namePrefix}${i + 1}`;
|
||||
const port = parseInt(conn.port) || 443;
|
||||
const latency = await tcpPing(conn.address, port);
|
||||
pingResults.push({
|
||||
@@ -136,8 +131,8 @@ async function syncPanelWithPing(
|
||||
latency, error: latency === null ? "Таймаут или ошибка" : null,
|
||||
});
|
||||
}
|
||||
state[stateKey].pingResults = pingResults;
|
||||
state[stateKey].allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
state.xray.pingResults = pingResults;
|
||||
state.xray.allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
|
||||
let bestPingIndex = -1;
|
||||
let bestLatency = Infinity;
|
||||
@@ -149,24 +144,24 @@ async function syncPanelWithPing(
|
||||
});
|
||||
|
||||
let syncConnections = [...valid];
|
||||
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
let syncedTags = valid.map((_, idx) => `${namePrefix}${idx + 1}`);
|
||||
if (bestPingIndex >= 0) {
|
||||
syncConnections = [valid[bestPingIndex], ...valid];
|
||||
syncedTags = ["novavps0", ...syncedTags];
|
||||
syncedTags = [`${namePrefix}0`, ...syncedTags];
|
||||
}
|
||||
|
||||
state.currentStep = stateKey === "xray1" ? "xray1_syncing" : "xray2_syncing";
|
||||
state.currentStep = "syncing";
|
||||
await saveState(state);
|
||||
|
||||
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
|
||||
state[stateKey].synced = true;
|
||||
state[stateKey].outboundsCount = syncedTags.length;
|
||||
state[stateKey].syncedOutbounds = syncedTags;
|
||||
state.xray.synced = true;
|
||||
state.xray.outboundsCount = syncedTags.length;
|
||||
state.xray.syncedOutbounds = syncedTags;
|
||||
panelStatus.set({ label: panel.label }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[${panel.label}] Ошибка синхронизации:`, errMsg);
|
||||
state[stateKey].error = errMsg;
|
||||
state.xray.error = errMsg;
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
@@ -186,23 +181,23 @@ async function runParser(): Promise<void> {
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const novavpsUrl = config.novavpsUrl;
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
const subscriptionUrl = config.subscriptionUrl;
|
||||
|
||||
if (!novavpsUrl) {
|
||||
if (!subscriptionUrl) {
|
||||
state.status = "error";
|
||||
state.currentStep = "novavps_url_missing";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Novavps URL не настроен" };
|
||||
state.currentStep = "subscription_url_missing";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Ссылка подписки не настроена" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const panels = getPanels();
|
||||
if (panels.length === 0) {
|
||||
const panel = getPanel();
|
||||
if (!panel) {
|
||||
state.status = "error";
|
||||
state.currentStep = "no_xray_panels";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроены панели Xray" };
|
||||
state.currentStep = "no_xray_panel";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроена панель Xray" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
@@ -213,7 +208,7 @@ async function runParser(): Promise<void> {
|
||||
|
||||
let novavpsConnections: Awaited<ReturnType<typeof parseNovavps>> = [];
|
||||
try {
|
||||
novavpsConnections = await parseNovavps(novavpsUrl);
|
||||
novavpsConnections = await parseNovavps(subscriptionUrl);
|
||||
|
||||
if (novavpsConnections.length === 0) {
|
||||
try {
|
||||
@@ -249,49 +244,41 @@ async function runParser(): Promise<void> {
|
||||
|
||||
const valid = getValidConnections(novavpsConnections);
|
||||
|
||||
for (const panel of panels) {
|
||||
state.currentStep = `${panel.label}_parsing`;
|
||||
await saveState(state);
|
||||
state.currentStep = "parsing";
|
||||
await saveState(state);
|
||||
|
||||
try {
|
||||
await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
state[panelStateKey] = {
|
||||
status: "success",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
||||
allPingsOk: panelStateKey === "xray1" ? false : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
panelStatus.set({ label: panel.label }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, errMsg);
|
||||
const errorState: import("./data-store").PanelStatus = {
|
||||
status: "error",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: errMsg,
|
||||
};
|
||||
if (panel.label === "xray1") {
|
||||
state.xray1 = errorState;
|
||||
} else {
|
||||
state.xray2 = errorState;
|
||||
}
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
try {
|
||||
await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
state.xray = {
|
||||
status: "success",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
panelStatus.set({ label: panel.label }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, errMsg);
|
||||
state.xray = {
|
||||
status: "error",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: errMsg,
|
||||
};
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
|
||||
if (valid.length === 0) continue;
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
|
||||
await syncPanelWithPing(panel, valid, state, panelStateKey);
|
||||
if (valid.length > 0) {
|
||||
await syncPanelWithPing(panel, valid, state);
|
||||
}
|
||||
|
||||
state.status = "completed";
|
||||
@@ -316,119 +303,7 @@ async function runParser(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let isXray2Running = false;
|
||||
|
||||
async function runXray2Sync(): Promise<void> {
|
||||
if (isXray2Running) return;
|
||||
isXray2Running = true;
|
||||
|
||||
xray2SyncRunsTotal.inc({ status: 'started' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
|
||||
const state = await loadState();
|
||||
state.currentStep = "xray2_syncing";
|
||||
state.xray2.status = "idle";
|
||||
state.xray2.synced = false;
|
||||
state.xray2.error = null;
|
||||
await saveState(state);
|
||||
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
let connections: NovavpsConnection[] = [];
|
||||
try {
|
||||
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
|
||||
connections = JSON.parse(raw);
|
||||
} catch {
|
||||
console.warn("[xray2] Нет сохранённых подключений Novavps");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = getValidConnections(connections);
|
||||
if (valid.length === 0) {
|
||||
console.warn("[xray2] Нет валидных подключений Novavps");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = getPanels().find((p) => p.label === "xray2");
|
||||
if (!panel) {
|
||||
console.warn("[xray2] Панель Xray2 не настроена в переменных окружения");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, "xray2");
|
||||
|
||||
state.xray2 = {
|
||||
status: "success",
|
||||
outboundsCount: (await parseXrayPanel(panel.url, panel.username, panel.password)).length,
|
||||
synced: true,
|
||||
syncedOutbounds: valid.map((_, idx) => `novavps${idx + 1}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'success' });
|
||||
panelStatus.set({ label: "xray2" }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[xray2] Критическая ошибка синхронизации:", errMsg);
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: errMsg,
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
} finally {
|
||||
await closeBrowser();
|
||||
isXray2Running = false;
|
||||
}
|
||||
}
|
||||
|
||||
app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response) => {
|
||||
if (isXray2Running) {
|
||||
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
res.status(409).json({ error: "Парсер запущен, дождитесь завершения" });
|
||||
return;
|
||||
}
|
||||
const panel = getPanels().find((p) => p.label === "xray2");
|
||||
if (!panel) {
|
||||
console.warn("[api/sync-xray2] Xray2 панель не настроена");
|
||||
res.status(400).json({ error: "Xray2 панель не настроена. Проверьте переменные XRAY_URL2, XRAY_USERNAME2, XRAY_PASSWORD2" });
|
||||
return;
|
||||
}
|
||||
runXray2Sync().catch((err) => console.error("[server] Xray2 sync error:", err));
|
||||
res.json({ ok: true, message: "Синхронизация Xray2 запущена" });
|
||||
});
|
||||
|
||||
app.post("/api/login", async (req: Request, res: Response) => {
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
@@ -483,9 +358,10 @@ app.post("/api/logout", (_req: Request, res: Response) => {
|
||||
|
||||
app.get("/api/status", authMiddleware, async (_req: Request, res: Response) => {
|
||||
const state = await loadState();
|
||||
const panel = getPanel();
|
||||
res.json({
|
||||
...state,
|
||||
configuredPanels: getPanels().map(p => p.label),
|
||||
configuredPanel: panel ? panel.label : null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -499,28 +375,28 @@ app.post("/api/run", authMiddleware, async (_req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
app.get("/api/config", authMiddleware, async (_req: Request, res: Response) => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
res.json(config);
|
||||
});
|
||||
|
||||
app.put("/api/config", authMiddleware, async (req: Request, res: Response) => {
|
||||
const { novavpsUrl } = req.body;
|
||||
if (!novavpsUrl) {
|
||||
res.status(400).json({ error: "novavpsUrl required" });
|
||||
const { subscriptionUrl } = req.body;
|
||||
if (!subscriptionUrl) {
|
||||
res.status(400).json({ error: "subscriptionUrl required" });
|
||||
return;
|
||||
}
|
||||
if (typeof novavpsUrl !== "string" || novavpsUrl.length < 10 || novavpsUrl.length > 500) {
|
||||
res.status(400).json({ error: "novavpsUrl must be a string between 10 and 500 characters" });
|
||||
if (typeof subscriptionUrl !== "string" || subscriptionUrl.length < 10 || subscriptionUrl.length > 500) {
|
||||
res.status(400).json({ error: "subscriptionUrl must be a string between 10 and 500 characters" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new URL(novavpsUrl);
|
||||
new URL(subscriptionUrl);
|
||||
} catch {
|
||||
res.status(400).json({ error: "novavpsUrl must be a valid URL" });
|
||||
res.status(400).json({ error: "subscriptionUrl must be a valid URL" });
|
||||
return;
|
||||
}
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
config.novavpsUrl = novavpsUrl;
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
config.subscriptionUrl = subscriptionUrl;
|
||||
await saveConfig(config);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
@@ -561,7 +437,7 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
const config = await loadConfig(process.env.subscription_url || "");
|
||||
|
||||
const state = await loadState();
|
||||
if (state.status === "running") {
|
||||
@@ -571,16 +447,11 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.log("[server] Сброшено состояние 'running' после перезапуска");
|
||||
}
|
||||
|
||||
if (state.xray2.status === "error" && !process.env.XRAY_URL2) {
|
||||
state.xray2 = { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null };
|
||||
await saveState(state);
|
||||
console.log("[server] Сброшена ошибка Xray2 — панель не настроена");
|
||||
}
|
||||
|
||||
const panel = getPanel();
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`[server] Web interface running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`[server] NOVAVPS_URL: ${config.novavpsUrl || "(not set)"}`);
|
||||
console.log(`[server] Panels: ${getPanels().map((p) => p.label).join(", ") || "(none)"}`);
|
||||
console.log(`[server] subscription_url: ${config.subscriptionUrl || "(not set)"}`);
|
||||
console.log(`[server] Panel: ${panel ? panel.label : "(none)"}`);
|
||||
});
|
||||
|
||||
const PARSE_INTERVAL_MS = (parseInt(process.env.PARSE_INTERVAL || "1800", 10)) * 1000;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readdirSync, unlinkSync } from "fs";
|
||||
import type { Page } from "playwright";
|
||||
import { newContext, getBrowser } from "./browser";
|
||||
import type { NovavpsConnection } from "./novavps";
|
||||
import { getConnectionNamePrefix } from "./config";
|
||||
|
||||
const DATA_DIR = path.resolve(__dirname, "..", "data");
|
||||
const BACKUP_DIR = path.join(DATA_DIR, "xray-backups");
|
||||
@@ -247,6 +248,7 @@ export async function syncNovavpsToXray(
|
||||
return;
|
||||
}
|
||||
|
||||
const namePrefix = getConnectionNamePrefix();
|
||||
console.log(`[${panelLabel}] Синхронизирую ${connections.length} novavps подключений...`);
|
||||
|
||||
const context = await newContext();
|
||||
@@ -314,11 +316,11 @@ export async function syncNovavpsToXray(
|
||||
|
||||
const outbounds = (config.outbounds as Record<string, unknown>[]) || [];
|
||||
console.log(`[${panelLabel}] Всего outbound'ов в конфиге ДО: ${outbounds.length}`);
|
||||
const existingNovavpsTags = outbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
||||
const existingNovavpsTags = outbounds.filter((o) => String(o.tag || "").startsWith(namePrefix));
|
||||
console.log(`[${panelLabel}] Из них с тегом ${namePrefix}*: ${existingNovavpsTags.length}`);
|
||||
|
||||
connections.forEach((conn, idx) => {
|
||||
const tag = `novavps${startIndex + idx}`;
|
||||
const tag = `${namePrefix}${startIndex + idx}`;
|
||||
const newOutbound = buildVlessOutbound(conn, tag);
|
||||
|
||||
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
||||
@@ -333,8 +335,8 @@ export async function syncNovavpsToXray(
|
||||
|
||||
config.outbounds = outbounds;
|
||||
const modifiedJson = JSON.stringify(config, null, 2);
|
||||
const novavpsCountAfter = (outbounds.filter((o) => String(o.tag || "").startsWith("novavps"))).length;
|
||||
console.log(`[${panelLabel}] После модификации: всего outbound'ов ${outbounds.length}, novavps*: ${novavpsCountAfter}`);
|
||||
const novavpsCountAfter = (outbounds.filter((o) => String(o.tag || "").startsWith(namePrefix))).length;
|
||||
console.log(`[${panelLabel}] После модификации: всего outbound'ов ${outbounds.length}, ${namePrefix}*: ${novavpsCountAfter}`);
|
||||
|
||||
await page.evaluate((json) => {
|
||||
const cm = document.querySelector(".CodeMirror") as unknown as { CodeMirror?: { setValue: (v: string) => void } };
|
||||
@@ -387,8 +389,8 @@ export async function syncNovavpsToXray(
|
||||
try {
|
||||
const verifyConfig = JSON.parse(verifyJson);
|
||||
const verifyOutbounds = (verifyConfig.outbounds as Record<string, unknown>[]) || [];
|
||||
const verifyNovavps = verifyOutbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] После сохранения: outbound'ов ${verifyOutbounds.length}, novavps*: ${verifyNovavps.length}`);
|
||||
const verifyNovavps = verifyOutbounds.filter((o) => String(o.tag || "").startsWith(namePrefix));
|
||||
console.log(`[${panelLabel}] После сохранения: outbound'ов ${verifyOutbounds.length}, ${namePrefix}*: ${verifyNovavps.length}`);
|
||||
verifyNovavps.forEach((o) => {
|
||||
const settings = (o as Record<string, unknown>).settings as Record<string, unknown> | undefined;
|
||||
const vnext = settings?.vnext as Record<string, unknown>[] | undefined;
|
||||
|
||||
Reference in New Issue
Block a user