diff --git a/changelog.d/features/15141-core-config-beside.md b/changelog.d/features/15141-core-config-beside.md new file mode 100644 index 000000000000..f124a7c24b9c --- /dev/null +++ b/changelog.d/features/15141-core-config-beside.md @@ -0,0 +1 @@ +- **feat(proxy-subscription):** generate the local proxy-core config beside the adopted file, opt-in per subscription ([#15141](https://github.com/diegosouzapw/OmniRoute/pull/15141)) — thanks @maxmad64bis diff --git a/docs/openapi.yaml b/docs/openapi.yaml index ed4fafcb442d..a58900ad9deb 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -3323,6 +3323,13 @@ paths: localCoreEndpoint: type: string nullable: true + coreConfigPath: + type: string + nullable: true + description: >- + Absolute adopted sing-box config path (lowercase .json). + Empty means no generation; each sync writes + `.generated` beside it, never into it. updateIntervalMinutes: type: integer default: 60 @@ -3369,7 +3376,7 @@ paths: summary: Update a proxy subscription description: >- Partial update — only fields present in the body are changed - (name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled). + (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled). security: - ManagementSessionAuth: [] parameters: @@ -3395,6 +3402,9 @@ paths: localCoreEndpoint: type: string nullable: true + coreConfigPath: + type: string + nullable: true updateIntervalMinutes: type: integer enabled: @@ -15033,6 +15043,12 @@ components: localCoreEndpoint: type: string nullable: true + coreConfigPath: + type: string + nullable: true + description: >- + Absolute adopted sing-box config path (lowercase .json). + Empty means no generation. updateIntervalMinutes: type: integer lastFetchedAt: diff --git a/docs/routing/PROXY_CORE_CONFIG.md b/docs/routing/PROXY_CORE_CONFIG.md new file mode 100644 index 000000000000..87d8d3db28eb --- /dev/null +++ b/docs/routing/PROXY_CORE_CONFIG.md @@ -0,0 +1,34 @@ +--- +title: "Local Proxy-Core Configuration" +--- + +# Local proxy-core configuration (opt-in) + +This subscription field generates a local-core config file. Empty (default) +means no generation: sync behaves exactly as before. + +- Set **Core config file** to the absolute path of the adopted sing-box + config (lowercase `.json` extension, e.g. + `/var/lib/omniroute/sing-box/config.json`). +- Each sync renders the model and writes `.generated` **beside** the + adopted file with an atomic, symlink-refusing write (mode 0600). The + adopted file itself is never modified here; replacing it is a separate, + verified step. +- Only sections whose tags start with `omniroute-` are owned: owned entries + are removed then replaced, everything else (`dns`, `log`, + `experimental/clash_api`, user rules and outbounds, unknown keys) is kept + verbatim. Only sing-box is rendered so far. +- Refused without writing: unparseable JSON, a non-object root, or a file + with neither an `inbounds` nor an `outbounds` array. Nodes whose source + isn't sing-box-shaped are skipped with a reason, never converted. +- Failures and refusals surface as a sync warning and never fail the sync. + When an earlier warning already owns the sync status, generation is + skipped and the skip is logged server-side. Removing the subscription or + changing its path leaves any previously written `.generated` file in + place; adopting the generated file is part of the verified replacement + step. +- The last switch line shows the refusal motive that fired it; live switch + activity is also visible in the application log under [SelectorControl]. +- Without any selector group the model stays empty: nodes are neither + rendered nor listed. The switch reason shows as a generic label (quota, + unreachable, transport, slow). diff --git a/skills/omni-inference/references/endpoints.md b/skills/omni-inference/references/endpoints.md index 5a69063b7e1f..b3ef708de2c1 100644 --- a/skills/omni-inference/references/endpoints.md +++ b/skills/omni-inference/references/endpoints.md @@ -440,7 +440,7 @@ curl https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \ Update a proxy subscription -Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled). +Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled). ```bash curl -X PATCH https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \ diff --git a/src/app/(dashboard)/dashboard/settings/components/proxy/SubscriptionTab.tsx b/src/app/(dashboard)/dashboard/settings/components/proxy/SubscriptionTab.tsx index 2f90206f7d11..9f28585efc6a 100644 --- a/src/app/(dashboard)/dashboard/settings/components/proxy/SubscriptionTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/proxy/SubscriptionTab.tsx @@ -15,11 +15,13 @@ interface SubscriptionRecord { localCoreEndpoint: string | null; updateIntervalMinutes: number; controlUrl: string | null; + coreConfigPath: string | null; hasControlSecret: boolean; selectorMinGapSeconds: number; selectorLastSwitchAt: string | null; selectorLastSwitchResult: string | null; selectorLastSwitchMember: string | null; + selectorLastSwitchKind: string | null; lastFetchedAt: string | null; status: "ok" | "error" | "empty"; error: string | null; @@ -41,6 +43,7 @@ type FormState = { ruleProviders: string[]; localCoreEndpoint: string; controlUrl: string; + coreConfigPath: string; controlSecret: string; selectorMinGapSeconds: number; updateIntervalMinutes: number; @@ -54,6 +57,7 @@ const EMPTY_FORM: FormState = { ruleProviders: [], localCoreEndpoint: "", controlUrl: "", + coreConfigPath: "", controlSecret: "", selectorMinGapSeconds: 60, updateIntervalMinutes: 60, @@ -99,6 +103,22 @@ export default function SubscriptionTab() { const t = useTranslations("settings"); + // Map a stored refusal kind to a generic display label. Unknown future + // kinds fall back to plain English so the UI never shows a raw code. + const switchKindLabel = useCallback( + (kind: string | null): string | null => { + if (!kind) return null; + try { + const label = t(`proxySubscription.switchKind.${kind}`); + if (label && !label.includes("switchKind.")) return label; + } catch { + // fall through to the generic fallback below + } + return kind.replace(/_/g, " "); + }, + [t] + ); + // Resolve a subscription `error` value into a localized message. Values are // either a `{ code, detail? }` JSON (user-facing, i18n'd) or a plain // diagnostic string (technical fetch/sync errors) shown verbatim. @@ -183,6 +203,7 @@ export default function SubscriptionTab() { ruleProviders: sub.ruleProviders ?? [], localCoreEndpoint: sub.localCoreEndpoint ?? "", controlUrl: sub.controlUrl ?? "", + coreConfigPath: sub.coreConfigPath ?? "", controlSecret: "", selectorMinGapSeconds: sub.selectorMinGapSeconds ?? 60, updateIntervalMinutes: sub.updateIntervalMinutes, @@ -207,6 +228,7 @@ export default function SubscriptionTab() { ruleProviders: form.mode === "rule" ? form.ruleProviders : null, localCoreEndpoint: form.localCoreEndpoint.trim() || null, controlUrl: form.controlUrl.trim() || null, + coreConfigPath: form.coreConfigPath.trim() || null, selectorMinGapSeconds: Number(form.selectorMinGapSeconds) || 60, updateIntervalMinutes: Number(form.updateIntervalMinutes) || 60, enabled: form.enabled, @@ -298,11 +320,13 @@ export default function SubscriptionTab() { const member = sub.selectorLastSwitchMember ? ` · ${t("proxySubscription.lastSwitchMember")}: ${sub.selectorLastSwitchMember}` : ""; + const kindLabel = switchKindLabel(sub.selectorLastSwitchKind); + const kind = kindLabel ? ` · ${t("proxySubscription.lastSwitchKind")}: ${kindLabel}` : ""; const next = nextSwitchLabel(sub); const nextText = next === "now" ? t("proxySubscription.nextSwitchNow") : next; - return `${t("proxySubscription.lastSwitch")}: ${sub.selectorLastSwitchAt}${result}${member} · ${t("proxySubscription.nextSwitch")}: ${nextText}`; + return `${t("proxySubscription.lastSwitch")}: ${sub.selectorLastSwitchAt}${result}${member}${kind} · ${t("proxySubscription.nextSwitch")}: ${nextText}`; }, - [t] + [t, switchKindLabel] ); const statusBadge: Record = { @@ -472,6 +496,20 @@ export default function SubscriptionTab() { : t("proxySubscription.controlSecretNotSet")} + +
diff --git a/src/app/docs/lib/openapi.generated.ts b/src/app/docs/lib/openapi.generated.ts index e2f4b8fc27db..60f82901a57c 100644 --- a/src/app/docs/lib/openapi.generated.ts +++ b/src/app/docs/lib/openapi.generated.ts @@ -603,7 +603,7 @@ export const OPENAPI_ENDPOINTS: OpenApiEndpoint[] = [ path: "/api/v1/management/proxy-subscriptions/{id}", method: "PATCH", summary: "Update a proxy subscription", - description: "Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/updateIntervalMinutes/enabled).", + description: "Partial update — only fields present in the body are changed (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/updateIntervalMinutes/enabled).", tag: "Proxy Subscriptions", tags: ["Proxy Subscriptions"], requiresAuth: true, diff --git a/src/i18n/messages/am.json b/src/i18n/messages/am.json index 95d503a47101..365a0853a274 100644 --- a/src/i18n/messages/am.json +++ b/src/i18n/messages/am.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "የአካባቢው proxy-core መዳረሻ ነጥብ ልክ አይደለም፤ የSS/VMess/Trojan/VLESS ኖዶች ችላ ተብለዋል።", "NEEDS_CORE_NOT_CONFIGURED": "ይህ ምዝገባ የአካባቢ proxy core (SS/VMess/Trojan/VLESS) የሚያስፈልጋቸው ኖዶች አሉት፤ የlocal-core SOCKS5 መዳረሻ ነጥቡን እስኪያዋቅሩ ድረስ አይዘዋወሩም።", "NO_USABLE_NODES": "ምዝገባው ጥቅም ላይ የሚውሉ ኖዶችን አላመጣም (http/https/socks5 ወይም የአካባቢ core መዳረሻ ነጥብ ያላቸው ኖዶች)።", - "SELECTOR_SWITCH_FAILED": "የፕሮክሲ-ኮር መቆጣጠሪያ ጥሪ አልተሳካም; የቀደመው መስመር ንቁ ሆኖ ይቆያል። የመቆጣጠሪያ URL እና ሚስጥር ያረጋግጡ፣ ከዚያ ያድሱ።" + "SELECTOR_SWITCH_FAILED": "የፕሮክሲ-ኮር መቆጣጠሪያ ጥሪ አልተሳካም; የቀደመው መስመር ንቁ ሆኖ ይቆያል። የመቆጣጠሪያ URL እና ሚስጥር ያረጋግጡ፣ ከዚያ ያድሱ።", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "የፕሮክሲ ምዝገባ አገናኝዎን ይለጥፉ። አንዴ ከነቃ፣ ትራፊክ በዓለም አቀፍ ወይም በደንብ (የተወሰነ Provider) ሁነታ በፕሮክሲ ስብስቡ በኩል ይዘዋወራል። የምዝገባ ኖዶች በራስ-ሰር ወደ ፕሮክሲ ስብስቡ ይመሳሰላሉ፣ እንዲሁም ነባሮቹን የየጊዜው ማጣሪያ፣ የጤና ፍተሻ እና ፍሳሽ መከላከያ ዘዴዎች እንደገና ይጠቀማሉ።", "addSubscription": "ምዝገባ ያክሉ", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ገና ምንም መቀያየር አልተመዘገበም", "lastSwitchMember": "የሚያገለግል አባል", "nextSwitch": "ቀጣይ የሚቻል መቀያየር", - "nextSwitchNow": "አሁን" + "nextSwitchNow": "አሁን", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "የጅምላ ጤና ምርመራ", "bulkHealthcheckDesc": "የትኞቹ እንደሚሰሩ ለማወቅ ሁሉንም የተዋቀሩ ፕሮክሲዎች በዒላማ URL ላይ ይሞክሩ።", diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 5eb2e3066d39..20b216556884 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "نقطة نهاية نواة الوكيل المحلي غير صالحة؛ تم تجاهل عقد SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "يحتوي هذا الاشتراك على عقد تحتاج إلى نواة وكيل محلي (SS/VMess/Trojan/VLESS)؛ لن يتم توجيهها حتى تقوم بتكوين نقطة نهاية SOCKS5 للنواة المحلية.", "NO_USABLE_NODES": "لم ينتج عن الاشتراك أي عقد صالحة للاستخدام (http/https/socks5 أو عقد تحتوي على نقطة نهاية للنواة المحلية).", - "SELECTOR_SWITCH_FAILED": "فشل استدعاء التحكم في نواة الوكيل؛ ويظل المسار السابق نشطًا. تحقق من عنوان URL والسر الخاص بالتحكم، ثم حدّث." + "SELECTOR_SWITCH_FAILED": "فشل استدعاء التحكم في نواة الوكيل؛ ويظل المسار السابق نشطًا. تحقق من عنوان URL والسر الخاص بالتحكم، ثم حدّث.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "الصق رابط اشتراك الوكيل الخاص بك. بمجرد التمكين، يتم توجيه حركة المرور عبر تجمع الوكيل في الوضع العام أو وضع القاعدة (الموفر المحدد). تتم مزامنة عقد الاشتراك تلقائيًا في تجمع الوكيل وإعادة استخدام آليات الاستقصاء والفحص الصحي ومكافحة التسرب الموجودة.", "addSubscription": "إضافة الاشتراك", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "لم يُسجَّل أي تبديل بعد", "lastSwitchMember": "العضو الذي يخدم", "nextSwitch": "التبديل التالي الممكن", - "nextSwitchNow": "الآن" + "nextSwitchNow": "الآن", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "الفحص الصحي بالجملة", "bulkHealthcheckDesc": "اختبر جميع الوكلاء الذين تم تكوينهم مقابل عنوان URL المستهدف للعثور على الوكلاء الذين يعملون.", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 06758ace65d4..e0acac384c7c 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Yerli proksi-nüvə son nöqtəsi keçərsizdir; SS/VMess/Trojan/VLESS qovşaqlarına məhəl qoyulmadı.", "NEEDS_CORE_NOT_CONFIGURED": "Bu abunəlikdə yerli proksi-nüvəyə (SS/VMess/Trojan/VLESS) ehtiyacı olan qovşaqlar var; yerli nüvənin SOCKS5 son nöqtəsini konfiqurasiya edənə qədər onlar yönləndirilməyəcək.", "NO_USABLE_NODES": "Abunəlik heç bir istifadəyə yararlı qovşaq vermədi (http/https/socks5 və ya yerli nüvə son nöqtəsi olan qovşaqlar).", - "SELECTOR_SWITCH_FAILED": "Proxy-core idarəetmə çağırışı uğursuz oldu; əvvəlki marşrut aktiv qalır. İdarəetmə URL-ni və sirri yoxlayıb yeniləyin." + "SELECTOR_SWITCH_FAILED": "Proxy-core idarəetmə çağırışı uğursuz oldu; əvvəlki marşrut aktiv qalır. İdarəetmə URL-ni və sirri yoxlayıb yeniləyin.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Proksi abunə linkinizi yapışdırın. Aktivləşdirildikdən sonra trafik qlobal və ya qayda (müəyyən edilmiş Provayder) rejimində proksi hovuzu vasitəsilə yönləndirilir. Abunə qovşaqları avtomatik olaraq proksi hovuzuna sinxronlaşdırılır və mövcud sorğu, sağlamlıq yoxlanışı və sızma əleyhinə mexanizmlərdən yenidən istifadə edir.", "addSubscription": "Abunəlik əlavə edin", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Hələ qeydə alınmış keçid yoxdur", "lastSwitchMember": "Xidmət edən üzv", "nextSwitch": "Növbəti mümkün keçid", - "nextSwitchNow": "İndi" + "nextSwitchNow": "İndi", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Toplu sağlamlıq yoxlaması", "bulkHealthcheckDesc": "Hansılarının işlədiyini müəyyən etmək üçün bütün konfiqurasiya edilmiş proksiləri hədəf URL-də sınaqdan keçirin.", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index af6ff3bd81ac..e63a2da9b26f 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Локалната крайна точка на прокси ядрото е невалидна; SS/VMess/Trojan/VLESS възлите бяха игнорирани.", "NEEDS_CORE_NOT_CONFIGURED": "Този абонамент съдържа възли, които изискват локално прокси ядро (SS/VMess/Trojan/VLESS); те не се маршрутизират, докато не конфигурирате SOCKS5 крайната точка на локалното ядро.", "NO_USABLE_NODES": "Абонаментът не предостави използваеми възли (http/https/socks5 или възли с крайна точка на локалното ядро).", - "SELECTOR_SWITCH_FAILED": "Контролното извикване на proxy-core се провали; предишният маршрут остава активен. Проверете контролния URL и тайната, след което опреснете." + "SELECTOR_SWITCH_FAILED": "Контролното извикване на proxy-core се провали; предишният маршрут остава активен. Проверете контролния URL и тайната, след което опреснете.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Поставете вашата връзка за абонамент за прокси. След като бъде активиран, трафикът се насочва през прокси пула в глобален режим или режим на правило (посочен доставчик). Абонаментните възли се синхронизират автоматично в прокси пула и използват повторно съществуващите механизми за анкетиране, проверка на изправността и анти-течове.", "addSubscription": "Добавяне на абонамент", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Все още няма записано превключване", "lastSwitchMember": "Обслужващ член", "nextSwitch": "Следващо възможно превключване", - "nextSwitchNow": "Сега" + "nextSwitchNow": "Сега", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Групова проверка на изправността", "bulkHealthcheckDesc": "Тествайте всички конфигурирани проксита спрямо целеви URL адрес, за да установите кои работят.", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 6008143ca4f0..c2b436d19f57 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "লোকাল প্রক্সি-কোর এন্ডপয়েন্ট অবৈধ; SS/VMess/Trojan/VLESS নোডগুলো উপেক্ষা করা হয়েছে।", "NEEDS_CORE_NOT_CONFIGURED": "এই সাবস্ক্রিপশনে এমন নোড রয়েছে যার জন্য একটি লোকাল প্রক্সি কোর (SS/VMess/Trojan/VLESS) প্রয়োজন; আপনি লোকাল-কোর SOCKS5 এন্ডপয়েন্ট কনফিগার না করা পর্যন্ত সেগুলো রাউট করা হবে না।", "NO_USABLE_NODES": "সাবস্ক্রিপশন থেকে কোনো ব্যবহারযোগ্য নোড পাওয়া যায়নি (http/https/socks5 বা লোকাল কোর এন্ডপয়েন্ট বিশিষ্ট নোড)।", - "SELECTOR_SWITCH_FAILED": "প্রক্সি-কোর নিয়ন্ত্রণ কল ব্যর্থ হয়েছে; আগের রুট সক্রিয় থাকে। নিয়ন্ত্রণ URL ও সিক্রেট পরীক্ষা করে রিফ্রেশ করুন।" + "SELECTOR_SWITCH_FAILED": "প্রক্সি-কোর নিয়ন্ত্রণ কল ব্যর্থ হয়েছে; আগের রুট সক্রিয় থাকে। নিয়ন্ত্রণ URL ও সিক্রেট পরীক্ষা করে রিফ্রেশ করুন।", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "আপনার প্রক্সি সদস্যতা লিঙ্ক আটকান. একবার সক্ষম হলে, ট্রাফিককে প্রক্সি পুলের মাধ্যমে বিশ্বব্যাপী বা নিয়ম (নির্দিষ্ট প্রদানকারী) মোডে রুট করা হয়। সাবস্ক্রিপশন নোডগুলি স্বয়ংক্রিয়ভাবে প্রক্সি পুলে সিঙ্ক করা হয় এবং বিদ্যমান পোলিং, স্বাস্থ্য-পরীক্ষা এবং অ্যান্টি-লিক প্রক্রিয়াগুলি পুনরায় ব্যবহার করে।", "addSubscription": "সদস্যতা যোগ করুন", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "এখনও কোনো সুইচ রেকর্ড হয়নি", "lastSwitchMember": "সেবারত সদস্য", "nextSwitch": "পরবর্তী সম্ভাব্য সুইচ", - "nextSwitchNow": "এখন" + "nextSwitchNow": "এখন", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "বাল্ক স্বাস্থ্য পরীক্ষা", "bulkHealthcheckDesc": "কোনগুলো কাজ করে তা জানতে একটি লক্ষ্য URL-এর বিপরীতে কনফিগার করা সব প্রক্সি পরীক্ষা করুন।", diff --git a/src/i18n/messages/bs.json b/src/i18n/messages/bs.json index b03eaa35f35d..f1896e4a807c 100644 --- a/src/i18n/messages/bs.json +++ b/src/i18n/messages/bs.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalna proxy-core krajnja tačka je nevažeća; SS/VMess/Trojan/VLESS čvorovi su ignorisani.", "NEEDS_CORE_NOT_CONFIGURED": "Ova pretplata ima čvorove kojima je potreban lokalni proxy core (SS/VMess/Trojan/VLESS); oni neće biti rutirani dok ne konfigurišete lokalni-core SOCKS5 endpoint.", "NO_USABLE_NODES": "Pretplata nije pružila nijedan upotrebljiv čvor (http/https/socks5 ili čvorovi sa lokalnom core krajnjom tačkom).", - "SELECTOR_SWITCH_FAILED": "Upravljački poziv proxy-corea nije uspio; prethodna ruta ostaje aktivna. Provjerite upravljački URL i tajnu, pa osvježite." + "SELECTOR_SWITCH_FAILED": "Upravljački poziv proxy-corea nije uspio; prethodna ruta ostaje aktivna. Provjerite upravljački URL i tajnu, pa osvježite.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Zalijepite link vaše proxy pretplate. Kada se omogući, saobraćaj se rutira kroz proxy pool u globalnom ili rule (specifični Provider) režimu. Čvorovi pretplate se automatski sinhronizuju u proxy pool i koriste postojeće mehanizme polling-a, provjere zdravlja i anti-leak zaštite.", "addSubscription": "Dodaj Pretplatu", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Još nema evidentiranog prebacivanja", "lastSwitchMember": "Član koji opslužuje", "nextSwitch": "Sljedeće moguće prebacivanje", - "nextSwitchNow": "Sada" + "nextSwitchNow": "Sada", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Masovna provjera zdravlja", "bulkHealthcheckDesc": "Testirajte sve konfigurisane proksije protiv ciljanog URL-a kako biste saznali koji rade.", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index b42dfe40f850..0cb5c1795df2 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokální koncový bod proxy jádra je neplatný; uzly SS/VMess/Trojan/VLESS byly ignorovány.", "NEEDS_CORE_NOT_CONFIGURED": "Tento odběr obsahuje uzly, které vyžadují lokální proxy jádro (SS/VMess/Trojan/VLESS); nebudou směrovány, dokud nenakonfigurujete SOCKS5 koncový bod lokálního jádra.", "NO_USABLE_NODES": "Odběr neposkytl žádné použitelné uzly (http/https/socks5 nebo uzly s koncovým bodem lokálního jádra).", - "SELECTOR_SWITCH_FAILED": "Řídicí volání proxy-core selhalo; předchozí trasa zůstává aktivní. Zkontrolujte řídicí URL a tajný klíč a obnovte." + "SELECTOR_SWITCH_FAILED": "Řídicí volání proxy-core selhalo; předchozí trasa zůstává aktivní. Zkontrolujte řídicí URL a tajný klíč a obnovte.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Vložte odkaz na předplatné proxy. Po povolení je provoz směrován přes fond proxy v globálním režimu nebo režimu pravidel (zadaný poskytovatel). Uzly předplatného se automaticky synchronizují do fondu proxy a znovu používají existující mechanismy dotazování, kontroly stavu a ochrany proti úniku.", "addSubscription": "Přidat předplatné", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Zatím žádné zaznamenané přepnutí", "lastSwitchMember": "Obsluhující člen", "nextSwitch": "Další možné přepnutí", - "nextSwitchNow": "Nyní" + "nextSwitchNow": "Nyní", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Hromadná kontrola stavu", "bulkHealthcheckDesc": "Otestovat všechny nakonfigurované proxy servery vůči cílové URL a zjistit, které fungují.", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 964e6672f805..70e9c0c909bd 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalt proxy-core-slutpunkt er ugyldigt; SS/VMess/Trojan/VLESS-noder blev ignoreret.", "NEEDS_CORE_NOT_CONFIGURED": "Dette abonnement har noder, der kræver en lokal proxy-core (SS/VMess/Trojan/VLESS); de routes ikke, før du konfigurerer local-core SOCKS5-slutpunktet.", "NO_USABLE_NODES": "Abonnementet gav ingen brugbare noder (http/https/socks5 eller noder med et lokalt core-slutpunkt).", - "SELECTOR_SWITCH_FAILED": "Proxy-coreens styreopkald mislykkedes; den forrige rute forbliver aktiv. Kontrollér styre-URL og hemmelighed, og opdatér derefter." + "SELECTOR_SWITCH_FAILED": "Proxy-coreens styreopkald mislykkedes; den forrige rute forbliver aktiv. Kontrollér styre-URL og hemmelighed, og opdatér derefter.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Indsæt dit proxy-abonnementslink. Når den er aktiveret, dirigeres trafik gennem proxypuljen i global tilstand eller regel (specificeret udbyder). Abonnementsnoder synkroniseres automatisk til proxypuljen og genbruger eksisterende polling-, sundhedstjek- og anti-lækagemekanismer.", "addSubscription": "Tilføj abonnement", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Ingen skift registreret endnu", "lastSwitchMember": "Betjenende medlem", "nextSwitch": "Næste mulige skift", - "nextSwitchNow": "Nu" + "nextSwitchNow": "Nu", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Samlet sundhedstjek", "bulkHealthcheckDesc": "Test alle konfigurerede proxyer mod en destinations-URL for at finde ud af, hvilke der virker.", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2410cdf3aebc..e18b05f61562 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokaler Proxy-Core-Endpunkt ist ungültig; SS/VMess/Trojan/VLESS-Knoten wurden ignoriert.", "NEEDS_CORE_NOT_CONFIGURED": "Dieses Abonnement enthält Knoten, die einen lokalen Proxy-Core benötigen (SS/VMess/Trojan/VLESS); sie werden nicht geroutet, bis Sie den lokalen Core-SOCKS5-Endpunkt konfigurieren.", "NO_USABLE_NODES": "Abonnement lieferte keine nutzbaren Knoten (http/https/socks5 oder Knoten mit einem lokalen Core-Endpunkt).", - "SELECTOR_SWITCH_FAILED": "Der Steueraufruf des Proxy-Cores ist fehlgeschlagen; die bisherige Route bleibt aktiv. Prüfen Sie Steuer-URL und Secret und aktualisieren Sie dann." + "SELECTOR_SWITCH_FAILED": "Der Steueraufruf des Proxy-Cores ist fehlgeschlagen; die bisherige Route bleibt aktiv. Prüfen Sie Steuer-URL und Secret und aktualisieren Sie dann.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Fügen Sie Ihren Proxy-Abonnement-Link ein. Nach der Aktivierung wird der Datenverkehr im globalen Modus oder im Regelmodus (angegebener Anbieter) über den Proxy-Pool weitergeleitet. Abonnementknoten werden automatisch mit dem Proxy-Pool synchronisiert und nutzen vorhandene Abfrage-, Gesundheitsprüfungs- und Anti-Leak-Mechanismen wieder.", "addSubscription": "Abonnement hinzufügen", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Noch keine Umschaltung verzeichnet", "lastSwitchMember": "Bedienendes Mitglied", "nextSwitch": "Nächste mögliche Umschaltung", - "nextSwitchNow": "Jetzt" + "nextSwitchNow": "Jetzt", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Massenprüfung des Zustands", "bulkHealthcheckDesc": "Alle konfigurierten Proxys anhand einer Ziel-URL testen, um festzustellen, welche funktionieren.", diff --git a/src/i18n/messages/el.json b/src/i18n/messages/el.json index 31e4d1ac470c..69a32d42c7d9 100644 --- a/src/i18n/messages/el.json +++ b/src/i18n/messages/el.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Το τοπικό τελικό σημείο proxy-core δεν είναι έγκυρο· οι κόμβοι SS/VMess/Trojan/VLESS αγνοήθηκαν.", "NEEDS_CORE_NOT_CONFIGURED": "Αυτή η συνδρομή έχει κόμβους που χρειάζονται τοπικό πυρήνα διακομιστή μεσολάβησης (SS/VMess/Trojan/VLESS)· δεν δρομολογούνται έως ότου ρυθμίσετε το τοπικό τελικό σημείο SOCKS5 του πυρήνα.", "NO_USABLE_NODES": "Η συνδρομή δεν απέδωσε χρησιμοποιήσιμους κόμβους (http/https/socks5 ή κόμβους με τοπικό τελικό σημείο πυρήνα).", - "SELECTOR_SWITCH_FAILED": "Η κλήση ελέγχου του proxy-core απέτυχε· η προηγούμενη διαδρομή παραμένει ενεργή. Ελέγξτε το URL και το μυστικό ελέγχου και ανανεώστε." + "SELECTOR_SWITCH_FAILED": "Η κλήση ελέγχου του proxy-core απέτυχε· η προηγούμενη διαδρομή παραμένει ενεργή. Ελέγξτε το URL και το μυστικό ελέγχου και ανανεώστε.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Επικολλήστε τον σύνδεσμο συνδρομής διακομιστή μεσολάβησης. Μόλις ενεργοποιηθεί, η κυκλοφορία δρομολογείται μέσω της δεξαμενής διακομιστών μεσολάβησης σε καθολική λειτουργία ή λειτουργία κανόνων (καθορισμένος Πάροχος). Οι κόμβοι συνδρομής συγχρονίζονται αυτόματα στη δεξαμενή και επαναχρησιμοποιούν τους υπάρχοντες μηχανισμούς κυκλικής εναλλαγής, ελέγχου εύρυθμης λειτουργίας και αποτροπής διαρροών.", "addSubscription": "Προσθήκη Συνδρομής", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Δεν έχει καταγραφεί εναλλαγή", "lastSwitchMember": "Μέλος που εξυπηρετεί", "nextSwitch": "Επόμενη δυνατή εναλλαγή", - "nextSwitchNow": "Τώρα" + "nextSwitchNow": "Τώρα", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Μαζικός έλεγχος εύρυθμης λειτουργίας", "bulkHealthcheckDesc": "Δοκιμάστε όλους τους διαμορφωμένους διακομιστές μεσολάβησης με μια διεύθυνση URL-στόχο, για να βρείτε ποιοι λειτουργούν.", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 09c72e0c4715..f5272433d243 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Local proxy-core endpoint is invalid; SS/VMess/Trojan/VLESS nodes were ignored.", "NEEDS_CORE_NOT_CONFIGURED": "This subscription has nodes that need a local proxy core (SS/VMess/Trojan/VLESS); they are not routed until you configure the local-core SOCKS5 endpoint.", "NO_USABLE_NODES": "Subscription yielded no usable nodes (http/https/socks5 or nodes with a local core endpoint).", - "SELECTOR_SWITCH_FAILED": "The proxy-core control call failed; the previous route stays active. Check the control URL and secret, then refresh." + "SELECTOR_SWITCH_FAILED": "The proxy-core control call failed; the previous route stays active. Check the control URL and secret, then refresh.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Paste your proxy subscription link. Once enabled, traffic is routed through the proxy pool in global or rule (specified Provider) mode. Subscription nodes are automatically synced into the proxy pool and reuse existing polling, health-check, and anti-leak mechanisms.", "addSubscription": "Add Subscription", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "No switch recorded yet", "lastSwitchMember": "Serving member", "nextSwitch": "Next switch possible", - "nextSwitchNow": "Now" + "nextSwitchNow": "Now", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Bulk Healthcheck", "bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index af222a2bdbd3..23d791cae79a 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "El endpoint local de proxy-core no es válido; se ignoraron los nodos SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "Esta suscripción tiene nodos que necesitan un núcleo de proxy local (SS/VMess/Trojan/VLESS); no se enrutarán hasta que configures el endpoint SOCKS5 de local-core.", "NO_USABLE_NODES": "La suscripción no produjo ningún nodo utilizable (http/https/socks5 o nodos con un endpoint de núcleo local).", - "SELECTOR_SWITCH_FAILED": "La llamada de control del proxy-core falló; la ruta anterior sigue activa. Compruebe la URL y el secreto de control y actualice." + "SELECTOR_SWITCH_FAILED": "La llamada de control del proxy-core falló; la ruta anterior sigue activa. Compruebe la URL y el secreto de control y actualice.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Pegue su enlace de suscripción de proxy. Una vez habilitado, el tráfico se enruta a través del grupo de proxy en modo global o de regla (Proveedor especificado). Los nodos de suscripción se sincronizan automáticamente con el grupo de proxy y reutilizan los mecanismos existentes de sondeo, verificación de estado y antifugas.", "addSubscription": "Agregar suscripción", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Aún no hay cambios registrados", "lastSwitchMember": "Miembro en servicio", "nextSwitch": "Próximo cambio posible", - "nextSwitchNow": "Ahora" + "nextSwitchNow": "Ahora", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Comprobación de estado masiva", "bulkHealthcheckDesc": "Prueba todos los proxies configurados con una URL de destino para determinar cuáles funcionan.", diff --git a/src/i18n/messages/et.json b/src/i18n/messages/et.json index a97c01eefcd0..a697feea3919 100644 --- a/src/i18n/messages/et.json +++ b/src/i18n/messages/et.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Kohaliku puhverserverituuma lõpp-punkt on vigane; SS/VMess/Trojan/VLESS-sõlmi eirati.", "NEEDS_CORE_NOT_CONFIGURED": "Sellel tellimusel on sõlmi, mis vajavad kohalikku puhverserverituuma (SS/VMess/Trojan/VLESS); neid ei marsruudita enne, kui konfigureerite kohaliku tuuma SOCKS5 lõpp-punkti.", "NO_USABLE_NODES": "Tellimus ei tagastanud ühtegi kasutatavat sõlme (http/https/socks5 või kohaliku tuuma lõpp-punktiga sõlmed).", - "SELECTOR_SWITCH_FAILED": "Puhversüdamiku juhtkõne ebaõnnestus; eelmine marsruut jääb aktiivseks. Kontrollige juht-URL-i ja saladust, seejärel värskendage." + "SELECTOR_SWITCH_FAILED": "Puhversüdamiku juhtkõne ebaõnnestus; eelmine marsruut jääb aktiivseks. Kontrollige juht-URL-i ja saladust, seejärel värskendage.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Kleepige siia oma puhverserveri tellimuse link. Pärast lubamist marsruuditakse liiklus puhverserverite kogumi kaudu globaalses või reeglipõhises (määratud teenusepakkuja) režiimis. Tellimuse sõlmed sünkroonitakse automaatselt puhverserverite kogumisse ning nende puhul kasutatakse olemasolevaid küsitlus-, seisundikontrolli- ja lekkekaitsemehhanisme.", "addSubscription": "Lisa tellimus", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Ühtegi vahetust pole veel salvestatud", "lastSwitchMember": "Teenindav liige", "nextSwitch": "Järgmine võimalik vahetus", - "nextSwitchNow": "Nüüd" + "nextSwitchNow": "Nüüd", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Hulgiviisiline seisundikontroll", "bulkHealthcheckDesc": "Testige kõiki seadistatud puhverservereid siht-URL-iga, et teha kindlaks, millised neist töötavad.", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 8a03c7150f9e..d96573e3d064 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "نقطه پایانی هسته پروکسی محلی نامعتبر است؛ گره‌های SS/VMess/Trojan/VLESS نادیده گرفته شدند.", "NEEDS_CORE_NOT_CONFIGURED": "این اشتراک دارای گره‌هایی است که به یک هسته پروکسی محلی (SS/VMess/Trojan/VLESS) نیاز دارند؛ تا زمانی که نقطه پایانی SOCKS5 هسته محلی را پیکربندی نکنید، آن‌ها مسیریابی نمی‌شوند.", "NO_USABLE_NODES": "اشتراک هیچ گره قابل استفاده‌ای ارائه نداد (http/https/socks5 یا گره‌هایی با نقطه پایانی هسته محلی).", - "SELECTOR_SWITCH_FAILED": "فراخوانی کنترل هستهٔ پروکسی ناموفق بود؛ مسیر قبلی همچنان فعال است. نشانی و راز کنترل را بررسی کنید، سپس تازه‌سازی کنید." + "SELECTOR_SWITCH_FAILED": "فراخوانی کنترل هستهٔ پروکسی ناموفق بود؛ مسیر قبلی همچنان فعال است. نشانی و راز کنترل را بررسی کنید، سپس تازه‌سازی کنید.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "پیوند اشتراک پروکسی خود را جایگذاری کنید. پس از فعال شدن، ترافیک از طریق استخر پروکسی در حالت جهانی یا قانون (ارائه‌دهنده مشخص) هدایت می‌شود. گره های اشتراک به طور خودکار در استخر پروکسی همگام می شوند و از مکانیسم های نظرسنجی، بررسی سلامت و ضد نشت موجود استفاده مجدد می کنند.", "addSubscription": "اضافه کردن اشتراک", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "هنوز جابه‌جایی ثبت نشده است", "lastSwitchMember": "عضو خدمت‌دهنده", "nextSwitch": "جابه‌جایی بعدی ممکن", - "nextSwitchNow": "اکنون" + "nextSwitchNow": "اکنون", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "بررسی سلامت گروهی", "bulkHealthcheckDesc": "همه پروکسیهای پیکربندیشده را با یک URL هدف آزمایش کنید تا مشخص شود کدامیک کار میکنند.", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index d808cde455ab..7ee09112e1be 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Paikallinen välityspalvelinytimen päätepiste on virheellinen; SS/VMess/Trojan/VLESS-solmut ohitettiin.", "NEEDS_CORE_NOT_CONFIGURED": "Tässä tilauksessa on solmuja, jotka tarvitsevat paikallisen välityspalvelinytimen (SS/VMess/Trojan/VLESS); niitä ei reititetä ennen kuin määrität paikallisen ytimen SOCKS5-päätepisteen.", "NO_USABLE_NODES": "Tilaus ei tuottanut käyttökelpoisia solmuja (http/https/socks5 tai solmuja, joilla on paikallisen ytimen päätepiste).", - "SELECTOR_SWITCH_FAILED": "Välitysymen ohjauskutsu epäonnistui; edellinen reitti pysyy käytössä. Tarkista ohjaus-URL ja salaisuus ja päivitä sitten." + "SELECTOR_SWITCH_FAILED": "Välitysymen ohjauskutsu epäonnistui; edellinen reitti pysyy käytössä. Tarkista ohjaus-URL ja salaisuus ja päivitä sitten.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Liitä välityspalvelimen tilauslinkki. Kun tämä on otettu käyttöön, liikenne reititetään välityspalvelimen kautta yleisessä tai sääntötilassa (määritetty toimittaja). Tilaussolmut synkronoidaan automaattisesti välityspalvelinpooliin, ja ne käyttävät uudelleen olemassa olevia kysely-, kuntotarkastus- ja vuodonestomekanismeja.", "addSubscription": "Lisää tilaus", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Vaihtoja ei ole vielä kirjattu", "lastSwitchMember": "Palveleva jäsen", "nextSwitch": "Seuraava mahdollinen vaihto", - "nextSwitchNow": "Nyt" + "nextSwitchNow": "Nyt", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Kuntotarkistus joukkona", "bulkHealthcheckDesc": "Testaa kaikki määritetyt välityspalvelimet kohde-URL-osoitetta vasten selvittääksesi, mitkä niistä toimivat.", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 215e1bdd2c62..c63b21d5ab9e 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Le point de terminaison du proxy-core local est invalide ; les nœuds SS/VMess/Trojan/VLESS ont été ignorés.", "NEEDS_CORE_NOT_CONFIGURED": "Cet abonnement contient des nœuds qui nécessitent un proxy core local (SS/VMess/Trojan/VLESS) ; ils ne sont pas routés tant que vous n'avez pas configuré le point de terminaison SOCKS5 du local-core.", "NO_USABLE_NODES": "L'abonnement n'a renvoyé aucun nœud utilisable (http/https/socks5 ou nœuds avec un point de terminaison de core local).", - "SELECTOR_SWITCH_FAILED": "L'appel de contrôle du proxy-core a échoué ; la route précédente reste active. Vérifiez l'URL et le secret de contrôle, puis actualisez." + "SELECTOR_SWITCH_FAILED": "L'appel de contrôle du proxy-core a échoué ; la route précédente reste active. Vérifiez l'URL et le secret de contrôle, puis actualisez.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Collez votre lien d'abonnement proxy. Une fois activé, le trafic est acheminé via le pool de proxy en mode global ou en mode règle (fournisseur spécifié). Les nœuds d'abonnement sont automatiquement synchronisés avec le pool de proxy et réutilisent les mécanismes d'interrogation, de contrôle de santé et anti-fuite existants.", "addSubscription": "Ajouter un abonnement", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Aucune bascule enregistrée", "lastSwitchMember": "Membre desservi", "nextSwitch": "Prochaine bascule possible", - "nextSwitchNow": "Maintenant" + "nextSwitchNow": "Maintenant", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Vérification groupée de l’état", "bulkHealthcheckDesc": "Testez tous les proxys configurés avec une URL cible afin d’identifier ceux qui fonctionnent.", diff --git a/src/i18n/messages/ga.json b/src/i18n/messages/ga.json index 5c647c4b6bcd..39682824d913 100644 --- a/src/i18n/messages/ga.json +++ b/src/i18n/messages/ga.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Tá críochphointe an chroí-seachfhreastalaí áitiúil neamhbhailí; rinneadh neamhaird de nóid SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "Tá nóid sa tsíntiús seo a dteastaíonn croí-seachfhreastalaí áitiúil uathu (SS/VMess/Trojan/VLESS); ní dhéantar iad a ródú go dtí go gcumraíonn tú críochphointe SOCKS5 an chroí áitiúil.", "NO_USABLE_NODES": "Níor tháirg an síntiús aon nód inúsáidte (http/https/socks5 nó nód le críochphointe croí áitiúil).", - "SELECTOR_SWITCH_FAILED": "Theip ar an nglao rialaithe proxy-core; fanann an bealach roimhe seo gníomhach. Seiceáil an URL agus an rún rialaithe, ansin athnuaigh." + "SELECTOR_SWITCH_FAILED": "Theip ar an nglao rialaithe proxy-core; fanann an bealach roimhe seo gníomhach. Seiceáil an URL agus an rún rialaithe, ansin athnuaigh.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Greamaigh do nasc síntiúis seachfhreastalaí. Nuair a chuirtear ar siúl é, déantar an trácht a ródú tríd an linn seachfhreastalaithe i mód domhanda nó i mód rialach (Soláthraí sonraithe). Déantar nóid síntiúis a shioncronú go huathoibríoch leis an linn seachfhreastalaithe agus athúsáideann siad na meicníochtaí pollála, seiceála sláinte agus frithsceite atá ann cheana.", "addSubscription": "Cuir Síntiús Leis", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Níl aon athrú taifeadta fós", "lastSwitchMember": "Ball ag freastal", "nextSwitch": "An chéad athrú eile is féidir", - "nextSwitchNow": "Anois" + "nextSwitchNow": "Anois", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Seiceáil Sláinte ar an Mórchóir", "bulkHealthcheckDesc": "Tástáil na seachfhreastalaithe cumraithe go léir i gcoinne URL sprice lena fháil amach cé na cinn a oibríonn.", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index bcb5e74ad306..cb0be7c43d3c 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "લોકલ પ્રોક્સી-કોર એન્ડપોઇન્ટ અમાન્ય છે; SS/VMess/Trojan/VLESS નોડ્સને અવગણવામાં આવ્યા હતા.", "NEEDS_CORE_NOT_CONFIGURED": "આ સબ્સ્ક્રિપ્શનમાં એવા નોડ્સ છે જેને લોકલ પ્રોક્સી કોર (SS/VMess/Trojan/VLESS) ની જરૂર છે; જ્યાં સુધી તમે લોકલ-કોર SOCKS5 એન્ડપોઇન્ટ કન્ફિગર ન કરો ત્યાં સુધી તેઓ રાઉટ થશે નહીં.", "NO_USABLE_NODES": "સબ્સ્ક્રિપ્શનમાંથી કોઈ ઉપયોગી નોડ્સ મળ્યા નથી (http/https/socks5 અથવા લોકલ કોર એન્ડપોઇન્ટવાળા નોડ્સ).", - "SELECTOR_SWITCH_FAILED": "પ્રોક્સી-કોર નિયંત્રણ કૉલ નિષ્ફળ ગયો; અગાઉનો માર્ગ સક્રિય રહે છે. નિયંત્રણ URL અને સીક્રેટ તપાસો, પછી રિફ્રેશ કરો." + "SELECTOR_SWITCH_FAILED": "પ્રોક્સી-કોર નિયંત્રણ કૉલ નિષ્ફળ ગયો; અગાઉનો માર્ગ સક્રિય રહે છે. નિયંત્રણ URL અને સીક્રેટ તપાસો, પછી રિફ્રેશ કરો.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "તમારી પ્રોક્સી સબ્સ્ક્રિપ્શન લિંક પેસ્ટ કરો. એકવાર સક્ષમ થયા પછી, ટ્રાફિકને વૈશ્વિક અથવા નિયમ (ઉલ્લેખિત પ્રદાતા) મોડમાં પ્રોક્સી પૂલ દ્વારા રૂટ કરવામાં આવે છે. સબ્સ્ક્રિપ્શન નોડ્સ પ્રોક્સી પૂલમાં આપમેળે સમન્વયિત થાય છે અને હાલના મતદાન, આરોગ્ય-તપાસ અને એન્ટી-લીક મિકેનિઝમનો ફરીથી ઉપયોગ કરે છે.", "addSubscription": "સબ્સ્ક્રિપ્શન ઉમેરો", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "હજુ કોઈ ફેરફાર નોંધાયો નથી", "lastSwitchMember": "સેવા આપતો સભ્ય", "nextSwitch": "આગામી શક્ય ફેરફાર", - "nextSwitchNow": "હમણાં" + "nextSwitchNow": "હમણાં", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "બલ્ક હેલ્થચેક", "bulkHealthcheckDesc": "કયા પ્રોક્સી કાર્ય કરે છે તે જાણવા માટે બધા કૉન્ફિગર કરેલા પ્રોક્સીઓને લક્ષ્ય URL સામે ચકાસો.", diff --git a/src/i18n/messages/ha.json b/src/i18n/messages/ha.json index 177ae7803a6c..51b86525c134 100644 --- a/src/i18n/messages/ha.json +++ b/src/i18n/messages/ha.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Endpoint na proxy-core na gida ba daidai ba ne; an yi watsi da nodes na SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "Wannan biyan kuɗin yana da nodes waɗanda ke buƙatar proxy core na gida (SS/VMess/Trojan/VLESS); ba za a bi da zirga-zirga ta cikinsu ba har sai ka saita endpoint na SOCKS5 na local-core.", "NO_USABLE_NODES": "Biyan kuɗin bai samar da nodes da za a iya amfani da su ba (http/https/socks5 ko nodes masu local core endpoint).", - "SELECTOR_SWITCH_FAILED": "Kiran sarrafa proxy-core ya gaza; tsohuwar hanya tana nan. Duba URL da sirrin sarrafawa, sannan ka sabunta." + "SELECTOR_SWITCH_FAILED": "Kiran sarrafa proxy-core ya gaza; tsohuwar hanya tana nan. Duba URL da sirrin sarrafawa, sannan ka sabunta.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Liƙa mahaɗin biyan kuɗin proxy ɗinka. Da zarar an kunna shi, za a bi da zirga-zirga ta cikin rukunin proxy a yanayin duniya ko na ƙa'ida (Provider da aka ƙayyade). Ana daidaita nodes na biyan kuɗi ta atomatik cikin rukunin proxy, kuma suna sake amfani da hanyoyin polling, health-check, da anti-leak da ake da su.", "addSubscription": "Ƙara Biyan Kuɗi", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Ba a rubuta sauyawa ba tukuna", "lastSwitchMember": "Memban da ke hidima", "nextSwitch": "Sauyawa ta gaba mai yiwuwa", - "nextSwitchNow": "Yanzu" + "nextSwitchNow": "Yanzu", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Gwajin Lafiya na Gungu", "bulkHealthcheckDesc": "Gwada duk proxies da aka saita da URL da aka nufa domin gano waɗanda suke aiki.", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 2fdcbfa5e502..cefcd983ad47 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "נקודת הקצה המקומית של proxy-core אינה תקינה; המערכת התעלמה מצומתי SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "במנוי זה ישנם צמתים הזקוקים לליבת פרוקסי מקומית (SS/VMess/Trojan/VLESS); הם לא ינותבו עד שתגדיר את נקודת הקצה SOCKS5 של הליבה המקומית.", "NO_USABLE_NODES": "המנוי לא הניב צמתים שמישים (http/https/socks5 או צמתים עם נקודת קצה של ליבה מקומית).", - "SELECTOR_SWITCH_FAILED": "קריאת הבקרה של proxy-core נכשלה; המסלול הקודם נשאר פעיל. בדקו את כתובת הבקרה ואת הסוד, ואז רעננו." + "SELECTOR_SWITCH_FAILED": "קריאת הבקרה של proxy-core נכשלה; המסלול הקודם נשאר פעיל. בדקו את כתובת הבקרה ואת הסוד, ואז רעננו.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "הדבק את קישור המנוי שלך ל-proxy. לאחר ההפעלה, התעבורה מנותבת דרך מאגר ה-proxy במצב גלובלי או כלל (ספק שצוין). צמתי מנוי מסונכרנים אוטומטית לתוך מאגר ה-proxy ומשתמשים מחדש במנגנוני סקר, בדיקת תקינות ומניעת דליפות קיימים.", "addSubscription": "הוסף מנוי", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "טרם נרשם מעבר", "lastSwitchMember": "החבר המשרת", "nextSwitch": "המעבר הבא האפשרי", - "nextSwitchNow": "עכשיו" + "nextSwitchNow": "עכשיו", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "בדיקת תקינות מרוכזת", "bulkHealthcheckDesc": "בדיקת כל שרתי ה-proxy שהוגדרו מול כתובת URL יעד כדי לגלות אילו מהם פועלים.", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 80ee9e065554..acf74b25ef18 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "स्थानीय प्रॉक्सी-कोर एंडपॉइंट अमान्य है; SS/VMess/Trojan/VLESS नोड्स को अनदेखा कर दिया गया।", "NEEDS_CORE_NOT_CONFIGURED": "इस सदस्यता में ऐसे नोड्स हैं जिन्हें स्थानीय प्रॉक्सी कोर (SS/VMess/Trojan/VLESS) की आवश्यकता है; जब तक आप स्थानीय-कोर SOCKS5 एंडपॉइंट को कॉन्फ़िगर नहीं करते, तब तक उन्हें रूट नहीं किया जाएगा।", "NO_USABLE_NODES": "सदस्यता से कोई उपयोगी नोड्स प्राप्त नहीं हुए (http/https/socks5 या स्थानीय कोर एंडपॉइंट वाले नोड्स)।", - "SELECTOR_SWITCH_FAILED": "प्रॉक्सी-कोर नियंत्रण कॉल विफल रहा; पिछला मार्ग सक्रिय रहता है। नियंत्रण URL और सीक्रेट जाँचें, फिर रीफ़्रेश करें।" + "SELECTOR_SWITCH_FAILED": "प्रॉक्सी-कोर नियंत्रण कॉल विफल रहा; पिछला मार्ग सक्रिय रहता है। नियंत्रण URL और सीक्रेट जाँचें, फिर रीफ़्रेश करें।", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "अपना प्रॉक्सी सदस्यता लिंक चिपकाएँ. एक बार सक्षम होने पर, ट्रैफ़िक को प्रॉक्सी पूल के माध्यम से वैश्विक या नियम (निर्दिष्ट प्रदाता) मोड में रूट किया जाता है। सदस्यता नोड्स स्वचालित रूप से प्रॉक्सी पूल में सिंक हो जाते हैं और मौजूदा मतदान, स्वास्थ्य-जांच और एंटी-लीक तंत्र का पुन: उपयोग करते हैं।", "addSubscription": "सदस्यता जोड़ें", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "अभी तक कोई स्विच दर्ज नहीं", "lastSwitchMember": "सेवारत सदस्य", "nextSwitch": "अगला संभावित स्विच", - "nextSwitchNow": "अभी" + "nextSwitchNow": "अभी", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "सामूहिक स्वास्थ्य जाँच", "bulkHealthcheckDesc": "यह पता लगाने के लिए कि कौन-से प्रॉक्सी काम करते हैं, सभी कॉन्फ़िगर किए गए प्रॉक्सी का लक्ष्य URL के विरुद्ध परीक्षण करें।", diff --git a/src/i18n/messages/hr.json b/src/i18n/messages/hr.json index 3bab8444c9a0..32c2a344acb5 100644 --- a/src/i18n/messages/hr.json +++ b/src/i18n/messages/hr.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalna krajnja točka za proxy-core nije valjana; SS/VMess/Trojan/VLESS čvorovi su zanemareni.", "NEEDS_CORE_NOT_CONFIGURED": "Ova pretplata ima čvorove koji zahtijevaju lokalni proxy core (SS/VMess/Trojan/VLESS); neće se usmjeravati dok ne konfigurirate lokalnu SOCKS5 krajnju točku za local-core.", "NO_USABLE_NODES": "Pretplata nije dala nijedan upotrebljiv čvor (http/https/socks5 ili čvorove s lokalnom krajnjom točkom za core).", - "SELECTOR_SWITCH_FAILED": "Upravljački poziv proxy-corea nije uspio; prethodna ruta ostaje aktivna. Provjerite upravljački URL i tajnu, a zatim osvježite." + "SELECTOR_SWITCH_FAILED": "Upravljački poziv proxy-corea nije uspio; prethodna ruta ostaje aktivna. Provjerite upravljački URL i tajnu, a zatim osvježite.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Zalijepite vezu pretplate za proxy. Nakon omogućavanja promet se usmjerava kroz skup proxyja u globalnom načinu ili načinu pravila (određeni Pružatelj). Čvorovi pretplate automatski se sinkroniziraju sa skupom proxyja i koriste postojeće mehanizme periodičkog dohvaćanja, provjere stanja i sprječavanja curenja.", "addSubscription": "Dodaj pretplatu", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Još nema zabilježenog prebacivanja", "lastSwitchMember": "Član koji poslužuje", "nextSwitch": "Sljedeće moguće prebacivanje", - "nextSwitchNow": "Sada" + "nextSwitchNow": "Sada", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Skupna provjera stanja", "bulkHealthcheckDesc": "Testirajte sve konfigurirane proxyje na ciljnom URL-u kako biste utvrdili koji rade.", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 90b061ff08fd..14867b521ae3 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "A helyi proxy-core végpont érvénytelen; az SS/VMess/Trojan/VLESS csomópontok figyelmen kívül lettek hagyva.", "NEEDS_CORE_NOT_CONFIGURED": "Ez az előfizetés olyan csomópontokat tartalmaz, amelyeknek helyi proxy-core-ra (SS/VMess/Trojan/VLESS) van szükségük; ezek nincsenek irányítva, amíg be nem állítja a helyi core SOCKS5 végpontot.", "NO_USABLE_NODES": "Az előfizetés nem eredményezett használható csomópontokat (http/https/socks5 vagy helyi core végponttal rendelkező csomópontok).", - "SELECTOR_SWITCH_FAILED": "A proxy-core vezérlőhívása sikertelen; az előző útvonal aktív marad. Ellenőrizze a vezérlő URL-t és titkot, majd frissítsen." + "SELECTOR_SWITCH_FAILED": "A proxy-core vezérlőhívása sikertelen; az előző útvonal aktív marad. Ellenőrizze a vezérlő URL-t és titkot, majd frissítsen.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Illessze be a proxy előfizetési linkjét. Ha engedélyezve van, a forgalom a proxykészleten keresztül globális vagy szabály (meghatározott szolgáltató) módban kerül átirányításra. Az előfizetési csomópontok automatikusan szinkronizálódnak a proxykészlettel, és újra felhasználják a meglévő lekérdezési, állapot-ellenőrzési és szivárgásgátló mechanizmusokat.", "addSubscription": "Előfizetés hozzáadása", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Még nincs rögzített váltás", "lastSwitchMember": "Kiszolgáló tag", "nextSwitch": "Következő lehetséges váltás", - "nextSwitchNow": "Most" + "nextSwitchNow": "Most", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Tömeges állapotellenőrzés", "bulkHealthcheckDesc": "Az összes beállított proxy tesztelése egy cél-URL-en annak megállapításához, hogy melyek működnek.", diff --git a/src/i18n/messages/hy.json b/src/i18n/messages/hy.json index 8ccfa89521a6..1c508f86c09f 100644 --- a/src/i18n/messages/hy.json +++ b/src/i18n/messages/hy.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Տեղային proxy-core վերջնակետն անվավեր է․ SS/VMess/Trojan/VLESS հանգույցներն անտեսվել են։", "NEEDS_CORE_NOT_CONFIGURED": "Այս բաժանորդագրությունն ունի հանգույցներ, որոնց անհրաժեշտ է տեղային proxy core (SS/VMess/Trojan/VLESS)․ դրանք չեն երթուղավորվի, մինչև չկարգավորեք local-core SOCKS5 վերջնակետը։", "NO_USABLE_NODES": "Բաժանորդագրությունը չի տրամադրել օգտագործելի հանգույցներ (http/https/socks5 կամ տեղային core վերջնակետ ունեցող հանգույցներ)։", - "SELECTOR_SWITCH_FAILED": "Proxy-core-ի կառավարման կանչը ձախողվեց. նախորդ երթը մնում է ակտիվ։ Ստուգեք կառավարման URL-ն ու գաղտնիքը, ապա թարմացրեք։" + "SELECTOR_SWITCH_FAILED": "Proxy-core-ի կառավարման կանչը ձախողվեց. նախորդ երթը մնում է ակտիվ։ Ստուգեք կառավարման URL-ն ու գաղտնիքը, ապա թարմացրեք։", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Տեղադրեք ձեր պրոքսի բաժանորդագրության հղումը։ Միացնելուց հետո թրաֆիկը կուղղվի պրոքսիների պուլի միջոցով՝ գլոբալ կամ կանոնային (նշված Provider) ռեժիմում։ Բաժանորդագրության հանգույցներն ավտոմատ համաժամացվում են պրոքսիների պուլի հետ և վերօգտագործում առկա պարբերական հարցման, վիճակի ստուգման և արտահոսքի կանխարգելման մեխանիզմները։", "addSubscription": "Ավելացնել բաժանորդագրություն", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Անջատում դեռ գրանցված չէ", "lastSwitchMember": "Սպասարկող անդամ", "nextSwitch": "Հաջորդ հնարավոր անջատում", - "nextSwitchNow": "Հիմա" + "nextSwitchNow": "Հիմա", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Զանգվածային հասանելիության ստուգում", "bulkHealthcheckDesc": "Ստուգել բոլոր կարգավորված պրոքսիները թիրախային URL-ի նկատմամբ՝ պարզելու, թե որոնք են աշխատում։", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index ccfed3aecabb..275d856a8771 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Endpoint proxy-core lokal tidak valid; node SS/VMess/Trojan/VLESS diabaikan.", "NEEDS_CORE_NOT_CONFIGURED": "Langganan ini memiliki node yang memerlukan proxy core lokal (SS/VMess/Trojan/VLESS); node tersebut tidak akan dirutekan sampai Anda mengonfigurasi endpoint SOCKS5 local-core.", "NO_USABLE_NODES": "Langganan tidak menghasilkan node yang dapat digunakan (http/https/socks5 atau node dengan endpoint core lokal).", - "SELECTOR_SWITCH_FAILED": "Panggilan kontrol proxy-core gagal; rute sebelumnya tetap aktif. Periksa URL dan secret kontrol, lalu muat ulang." + "SELECTOR_SWITCH_FAILED": "Panggilan kontrol proxy-core gagal; rute sebelumnya tetap aktif. Periksa URL dan secret kontrol, lalu muat ulang.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Tempelkan tautan berlangganan proksi Anda. Setelah diaktifkan, lalu lintas dirutekan melalui kumpulan proksi dalam mode global atau aturan (Penyedia tertentu). Node langganan secara otomatis disinkronkan ke kumpulan proksi dan menggunakan kembali mekanisme polling, pemeriksaan kesehatan, dan anti-kebocoran yang ada.", "addSubscription": "Tambahkan Langganan", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Belum ada peralihan tercatat", "lastSwitchMember": "Anggota yang melayani", "nextSwitch": "Peralihan berikutnya yang mungkin", - "nextSwitchNow": "Sekarang" + "nextSwitchNow": "Sekarang", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Pemeriksaan Kesehatan Massal", "bulkHealthcheckDesc": "Uji semua proksi yang dikonfigurasi terhadap URL target untuk mengetahui mana yang berfungsi.", diff --git a/src/i18n/messages/ig.json b/src/i18n/messages/ig.json index 16f17a690cff..82d4bfb0993d 100644 --- a/src/i18n/messages/ig.json +++ b/src/i18n/messages/ig.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Endpoint proxy-core mpaghara adịghị mma; e leghaara node SS/VMess/Trojan/VLESS anya.", "NEEDS_CORE_NOT_CONFIGURED": "Ndenye aha a nwere node ndị chọrọ proxy core mpaghara (SS/VMess/Trojan/VLESS); agaghị ebugharị ha ruo mgbe ị haziri endpoint SOCKS5 nke local-core.", "NO_USABLE_NODES": "Ndenye aha ahụ enyeghị node ọ bụla enwere ike iji (http/https/socks5 ma ọ bụ node nwere endpoint core mpaghara).", - "SELECTOR_SWITCH_FAILED": "Oku njikwa proxy-core dara; ụzọ gara aga na-anọgide na-arụ ọrụ. Lelee URL na nzuzo njikwa, wee mee ka ọ dị ọhụrụ." + "SELECTOR_SWITCH_FAILED": "Oku njikwa proxy-core dara; ụzọ gara aga na-anọgide na-arụ ọrụ. Lelee URL na nzuzo njikwa, wee mee ka ọ dị ọhụrụ.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Mado njikọ ndenye aha proxy gị. Ozugbo enyere ya ohere, a ga-ebufe okporo ụzọ site na nchịkọta proxy n'ọnọdụ zuru ụwa ọnụ ma ọ bụ ọnọdụ iwu (Provider akọwapụtara). A na-emekọrịta node ndenye aha na-akpaghị aka n'ime nchịkọta proxy, a na-ejikwa usoro polling, nyocha ahụike, na mgbochi ntapu ndị dịbu adị.", "addSubscription": "Tinye ndenye aha", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Enwebeghị mgbanwe ọ bụla", "lastSwitchMember": "Onye otu na-eje ozi", "nextSwitch": "Mgbanwe ọzọ enwere ike", - "nextSwitchNow": "Ugbu a" + "nextSwitchNow": "Ugbu a", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Nnyocha ahụike n'ìgwè", "bulkHealthcheckDesc": "Nwalee proxy niile ahaziri megide URL ebumnuche iji chọpụta ndị na-arụ ọrụ.", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 2fdfb078c00d..53d2a7f45b55 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "L'endpoint del core proxy locale non è valido; i nodi SS/VMess/Trojan/VLESS sono stati ignorati.", "NEEDS_CORE_NOT_CONFIGURED": "Questa sottoscrizione contiene nodi che richiedono un core proxy locale (SS/VMess/Trojan/VLESS); non verranno instradati finché non configuri l'endpoint SOCKS5 del core locale.", "NO_USABLE_NODES": "La sottoscrizione non ha prodotto nodi utilizzabili (http/https/socks5 o nodi con un endpoint del core locale).", - "SELECTOR_SWITCH_FAILED": "La chiamata di controllo del proxy-core non è riuscita; il percorso precedente resta attivo. Controlla URL e secret di controllo, quindi aggiorna." + "SELECTOR_SWITCH_FAILED": "La chiamata di controllo del proxy-core non è riuscita; il percorso precedente resta attivo. Controlla URL e secret di controllo, quindi aggiorna.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Incolla il link di iscrizione al proxy. Una volta abilitato, il traffico viene instradato attraverso il pool proxy in modalità globale o regola (provider specificato). I nodi di sottoscrizione vengono automaticamente sincronizzati nel pool proxy e riutilizzano i meccanismi di polling, controllo dello stato e anti-leak esistenti.", "addSubscription": "Aggiungi abbonamento", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Nessun cambio registrato", "lastSwitchMember": "Membro in servizio", "nextSwitch": "Prossimo cambio possibile", - "nextSwitchNow": "Adesso" + "nextSwitchNow": "Adesso", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Controllo integrità in blocco", "bulkHealthcheckDesc": "Testa tutti i proxy configurati con un URL di destinazione per individuare quelli funzionanti.", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index bd0e0963b8d7..589f9f67f432 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ローカルプロキシコアのエンドポイントが無効です。SS/VMess/Trojan/VLESS ノードは無視されました。", "NEEDS_CORE_NOT_CONFIGURED": "このサブスクリプションにはローカルプロキシコア(SS/VMess/Trojan/VLESS)が必要なノードが含まれています。ローカルコアの SOCKS5 エンドポイントを設定するまでルーティングされません。", "NO_USABLE_NODES": "サブスクリプションから使用可能なノードが得られませんでした(http/https/socks5 またはローカルコアエンドポイントを持つノード)。", - "SELECTOR_SWITCH_FAILED": "プロキシコアの制御呼び出しに失敗しました。以前のルートは有効のままです。制御URLとシークレットを確認してから更新してください。" + "SELECTOR_SWITCH_FAILED": "プロキシコアの制御呼び出しに失敗しました。以前のルートは有効のままです。制御URLとシークレットを確認してから更新してください。", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "プロキシ サブスクリプション リンクを貼り付けます。有効にすると、トラフィックはグローバル モードまたはルール (指定されたプロバイダー) モードでプロキシ プールを介してルーティングされます。サブスクリプション ノードはプロキシ プールに自動的に同期され、既存のポーリング、ヘルスチェック、および漏洩防止メカニズムを再利用します。", "addSubscription": "サブスクリプションの追加", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "切り替えはまだ記録されていません", "lastSwitchMember": "提供中のメンバー", "nextSwitch": "次に切り替え可能", - "nextSwitchNow": "今すぐ" + "nextSwitchNow": "今すぐ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "一括ヘルスチェック", "bulkHealthcheckDesc": "設定済みのすべてのプロキシを対象URLに対してテストし、動作するものを確認します。", diff --git a/src/i18n/messages/ka.json b/src/i18n/messages/ka.json index 182123e5b532..a5e6e6421939 100644 --- a/src/i18n/messages/ka.json +++ b/src/i18n/messages/ka.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ლოკალური proxy-core-ის საბოლოო წერტილი არასწორია; SS/VMess/Trojan/VLESS კვანძები უგულებელყოფილია.", "NEEDS_CORE_NOT_CONFIGURED": "ამ გამოწერაში არის კვანძები, რომლებსაც ლოკალური პროქსი-ბირთვი სჭირდებათ (SS/VMess/Trojan/VLESS); მარშრუტიზაცია არ მოხდება, სანამ ლოკალური ბირთვის SOCKS5 საბოლოო წერტილს არ დააკონფიგურირებთ.", "NO_USABLE_NODES": "გამოწერამ გამოსაყენებელი კვანძები ვერ დააბრუნა (http/https/socks5 ან ლოკალური ბირთვის საბოლოო წერტილის მქონე კვანძები).", - "SELECTOR_SWITCH_FAILED": "პროქსი-ბირთვის სამართავი ზარი ვერ შესრულდა; წინა მარშრუტი აქტიური რჩება. შეამოწმეთ სამართავი URL და საიდუმლო, შემდეგ განაახლეთ." + "SELECTOR_SWITCH_FAILED": "პროქსი-ბირთვის სამართავი ზარი ვერ შესრულდა; წინა მარშრუტი აქტიური რჩება. შეამოწმეთ სამართავი URL და საიდუმლო, შემდეგ განაახლეთ.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "ჩასვით თქვენი პროქსი-გამოწერის ბმული. ჩართვის შემდეგ ტრაფიკი პროქსი-პულის გავლით გლობალურ ან წესების (მითითებული პროვაიდერის) რეჟიმში მარშრუტიზდება. გამოწერის კვანძები ავტომატურად სინქრონდება პროქსი-პულში და იყენებს არსებულ პერიოდული შემოწმების, მდგომარეობის შემოწმებისა და გაჟონვის საწინააღმდეგო მექანიზმებს.", "addSubscription": "გამოწერის დამატება", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "გადართვა ჯერ არ დაფიქსირებულა", "lastSwitchMember": "მომსახურე წევრი", "nextSwitch": "შემდეგი შესაძლო გადართვა", - "nextSwitchNow": "ახლა" + "nextSwitchNow": "ახლა", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ხელმისაწვდომობის მასობრივი შემოწმება", "bulkHealthcheckDesc": "შეამოწმეთ ყველა კონფიგურირებული პროქსი სამიზნე URL-თან მიმართებით, რათა დაადგინოთ, რომელი მუშაობს.", diff --git a/src/i18n/messages/km.json b/src/i18n/messages/km.json index 9cdfbea3f861..e8c47fa82cad 100644 --- a/src/i18n/messages/km.json +++ b/src/i18n/messages/km.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ចំណុចបញ្ចប់ proxy-core ក្នុងម៉ាស៊ីនមិនត្រឹមត្រូវទេ ដូច្នេះ node SS/VMess/Trojan/VLESS មិនត្រូវបានយកមកប្រើទេ។", "NEEDS_CORE_NOT_CONFIGURED": "ការជាវនេះមាន node ដែលត្រូវការ proxy core ក្នុងម៉ាស៊ីន (SS/VMess/Trojan/VLESS)។ ពួកវានឹងមិនត្រូវបានកំណត់ផ្លូវទេ រហូតដល់អ្នកកំណត់រចនាសម្ព័ន្ធចំណុចបញ្ចប់ SOCKS5 របស់ local-core។", "NO_USABLE_NODES": "ការជាវនេះមិនមាន node ដែលអាចប្រើបានទេ (http/https/socks5 ឬ node ដែលមានចំណុចបញ្ចប់ local core)។", - "SELECTOR_SWITCH_FAILED": "ការហៅបញ្ជា proxy-core បានបរាជ័យ; ផ្លូវមុននៅតែសកម្ម។ សូមពិនិត្យ URL និង secret បញ្ជា រួចផ្ទុកឡើងវិញ។" + "SELECTOR_SWITCH_FAILED": "ការហៅបញ្ជា proxy-core បានបរាជ័យ; ផ្លូវមុននៅតែសកម្ម។ សូមពិនិត្យ URL និង secret បញ្ជា រួចផ្ទុកឡើងវិញ។", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "បិទភ្ជាប់តំណការជាវប្រូកស៊ីរបស់អ្នក។ នៅពេលបើកដំណើរការ ចរាចរណ៍នឹងត្រូវបានកំណត់ផ្លូវតាមបណ្តុំប្រូកស៊ី ក្នុងរបៀបសកល ឬរបៀបតាមច្បាប់ (Provider ដែលបានបញ្ជាក់)។ node នៃការជាវនឹងត្រូវបានធ្វើសមកាលកម្មដោយស្វ័យប្រវត្តិទៅក្នុងបណ្តុំប្រូកស៊ី និងប្រើឡើងវិញនូវយន្តការស្ទង់ពិនិត្យ ការពិនិត្យសុខភាព និងការការពារការលេចធ្លាយដែលមានស្រាប់។", "addSubscription": "បន្ថែមការជាវ", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "មិនទាន់មានការប្តូរដែលបានកត់ត្រា", "lastSwitchMember": "សមាជិកកំពុងបម្រើ", "nextSwitch": "ការប្តូរបន្ទាប់ដែលអាចធ្វើបាន", - "nextSwitchNow": "ឥឡូវ" + "nextSwitchNow": "ឥឡូវ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ការពិនិត្យស្ថានភាពជាក្រុម", "bulkHealthcheckDesc": "សាកល្បងប្រូកស៊ីដែលបានកំណត់រចនាសម្ព័ន្ធទាំងអស់ជាមួយ URL គោលដៅ ដើម្បីរកមើលថាតើមួយណាដំណើរការ។", diff --git a/src/i18n/messages/kn.json b/src/i18n/messages/kn.json index 28431bd423b1..81631e8f722e 100644 --- a/src/i18n/messages/kn.json +++ b/src/i18n/messages/kn.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ಸ್ಥಳೀಯ proxy-core ಎಂಡ್ಪಾಯಿಂಟ್ ಅಮಾನ್ಯವಾಗಿದೆ; SS/VMess/Trojan/VLESS ನೋಡ್ಗಳನ್ನು ನಿರ್ಲಕ್ಷಿಸಲಾಗಿದೆ.", "NEEDS_CORE_NOT_CONFIGURED": "ಈ ಚಂದಾದಾರಿಕೆಯಲ್ಲಿ ಸ್ಥಳೀಯ ಪ್ರಾಕ್ಸಿ ಕೋರ್ ಅಗತ್ಯವಿರುವ ನೋಡ್ಗಳಿವೆ (SS/VMess/Trojan/VLESS); ನೀವು local-core SOCKS5 ಎಂಡ್ಪಾಯಿಂಟ್ ಅನ್ನು ಕಾನ್ಫಿಗರ್ ಮಾಡುವವರೆಗೆ ಅವುಗಳ ಮೂಲಕ ರೂಟಿಂಗ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ.", "NO_USABLE_NODES": "ಚಂದಾದಾರಿಕೆಯಿಂದ ಬಳಸಬಹುದಾದ ಯಾವುದೇ ನೋಡ್ಗಳು ದೊರೆತಿಲ್ಲ (http/https/socks5 ಅಥವಾ ಸ್ಥಳೀಯ ಕೋರ್ ಎಂಡ್ಪಾಯಿಂಟ್ ಹೊಂದಿರುವ ನೋಡ್ಗಳು).", - "SELECTOR_SWITCH_FAILED": "ಪ್ರಾಕ್ಸಿ-ಕೋರ್ ನಿಯಂತ್ರಣ ಕರೆ ವಿಫಲವಾಯಿತು; ಹಿಂದಿನ ಮಾರ್ಗ ಸಕ್ರಿಯವಾಗಿರುತ್ತದೆ. ನಿಯಂತ್ರಣ URL ಮತ್ತು ರಹಸ್ಯವನ್ನು ಪರಿಶೀಲಿಸಿ, ನಂತರ ರಿಫ್ರೆಶ್ ಮಾಡಿ." + "SELECTOR_SWITCH_FAILED": "ಪ್ರಾಕ್ಸಿ-ಕೋರ್ ನಿಯಂತ್ರಣ ಕರೆ ವಿಫಲವಾಯಿತು; ಹಿಂದಿನ ಮಾರ್ಗ ಸಕ್ರಿಯವಾಗಿರುತ್ತದೆ. ನಿಯಂತ್ರಣ URL ಮತ್ತು ರಹಸ್ಯವನ್ನು ಪರಿಶೀಲಿಸಿ, ನಂತರ ರಿಫ್ರೆಶ್ ಮಾಡಿ.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "ನಿಮ್ಮ ಪ್ರಾಕ್ಸಿ ಚಂದಾದಾರಿಕೆ ಲಿಂಕ್ ಅನ್ನು ಅಂಟಿಸಿ. ಸಕ್ರಿಯಗೊಳಿಸಿದ ನಂತರ, ಟ್ರಾಫಿಕ್ ಅನ್ನು ಜಾಗತಿಕ ಅಥವಾ ನಿಯಮ (ನಿರ್ದಿಷ್ಟ Provider) ಮೋಡ್ನಲ್ಲಿ ಪ್ರಾಕ್ಸಿ ಪೂಲ್ ಮೂಲಕ ರೂಟ್ ಮಾಡಲಾಗುತ್ತದೆ. ಚಂದಾದಾರಿಕೆಯ ನೋಡ್ಗಳು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಪ್ರಾಕ್ಸಿ ಪೂಲ್ಗೆ ಸಿಂಕ್ ಆಗುತ್ತವೆ ಮತ್ತು ಈಗಿರುವ ಪೋಲಿಂಗ್, ಆರೋಗ್ಯ ಪರಿಶೀಲನೆ ಹಾಗೂ ಸೋರಿಕೆ-ವಿರೋಧಿ ಕಾರ್ಯವಿಧಾನಗಳನ್ನು ಮರುಬಳಕೆ ಮಾಡುತ್ತವೆ.", "addSubscription": "ಚಂದಾದಾರಿಕೆಯನ್ನು ಸೇರಿಸಿ", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ಇನ್ನೂ ಯಾವುದೇ ಬದಲಾವಣೆ ದಾಖಲಾಗಿಲ್ಲ", "lastSwitchMember": "ಸೇವೆ ಸಲ್ಲಿಸುತ್ತಿರುವ ಸದಸ್ಯ", "nextSwitch": "ಮುಂದಿನ ಸಂಭಾವ್ಯ ಬದಲಾವಣೆ", - "nextSwitchNow": "ಈಗ" + "nextSwitchNow": "ಈಗ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ಸಾಮೂಹಿಕ ಆರೋಗ್ಯ ಪರಿಶೀಲನೆ", "bulkHealthcheckDesc": "ಯಾವವು ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತವೆ ಎಂಬುದನ್ನು ಕಂಡುಹಿಡಿಯಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಎಲ್ಲಾ ಪ್ರಾಕ್ಸಿಗಳನ್ನು ಗುರಿ URL ವಿರುದ್ಧ ಪರೀಕ್ಷಿಸಿ.", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index df20779c5e6c..9e4b8167fabf 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "로컬 proxy-core 엔드포인트가 유효하지 않습니다. SS/VMess/Trojan/VLESS 노드가 무시되었습니다.", "NEEDS_CORE_NOT_CONFIGURED": "이 구독에는 로컬 프록시 코어(SS/VMess/Trojan/VLESS)가 필요한 노드가 있습니다. local-core SOCKS5 엔드포인트를 구성하기 전까지는 라우팅되지 않습니다.", "NO_USABLE_NODES": "구독에서 사용할 수 있는 노드(http/https/socks5 또는 로컬 코어 엔드포인트가 있는 노드)가 반환되지 않았습니다.", - "SELECTOR_SWITCH_FAILED": "프록시 코어 제어 호출에 실패했습니다. 이전 경로는 계속 유효합니다. 제어 URL과 시크릿을 확인한 뒤 새로고침하세요." + "SELECTOR_SWITCH_FAILED": "프록시 코어 제어 호출에 실패했습니다. 이전 경로는 계속 유효합니다. 제어 URL과 시크릿을 확인한 뒤 새로고침하세요.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "프록시 구독 링크를 붙여넣으세요. 활성화되면 트래픽은 전역 또는 규칙(지정된 공급자) 모드에서 프록시 풀을 통해 라우팅됩니다. 구독 노드는 프록시 풀에 자동으로 동기화되며 기존 폴링, 상태 확인 및 누출 방지 메커니즘을 재사용합니다.", "addSubscription": "구독 추가", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "기록된 전환이 아직 없음", "lastSwitchMember": "서비스 중인 멤버", "nextSwitch": "다음 전환 가능", - "nextSwitchNow": "지금" + "nextSwitchNow": "지금", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "일괄 상태 확인", "bulkHealthcheckDesc": "구성된 모든 프록시를 대상 URL에 대해 테스트하여 작동하는 프록시를 찾습니다.", diff --git a/src/i18n/messages/lt.json b/src/i18n/messages/lt.json index dc0b18bd24b4..1ea2547ea52e 100644 --- a/src/i18n/messages/lt.json +++ b/src/i18n/messages/lt.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Vietinio tarpinio serverio branduolio galinis taškas netinkamas; SS/VMess/Trojan/VLESS mazgai buvo ignoruoti.", "NEEDS_CORE_NOT_CONFIGURED": "Šioje prenumeratoje yra mazgų, kuriems reikia vietinio tarpinio serverio branduolio (SS/VMess/Trojan/VLESS); jų srautas nebus nukreipiamas, kol nesukonfigūruosite vietinio branduolio SOCKS5 galinio taško.", "NO_USABLE_NODES": "Prenumerata nepateikė jokių tinkamų naudoti mazgų (http/https/socks5 arba mazgų su vietinio branduolio galiniu tašku).", - "SELECTOR_SWITCH_FAILED": "Tarpinio branduolio valdymo iškvietimas nepavyko; ankstesnis maršrutas lieka aktyvus. Patikrinkite valdymo URL ir paslaptį, tada atnaujinkite." + "SELECTOR_SWITCH_FAILED": "Tarpinio branduolio valdymo iškvietimas nepavyko; ankstesnis maršrutas lieka aktyvus. Patikrinkite valdymo URL ir paslaptį, tada atnaujinkite.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Įklijuokite tarpinio serverio prenumeratos nuorodą. Įjungus srautas nukreipiamas per tarpinių serverių telkinį visuotiniu arba taisyklių (nurodyto teikėjo) režimu. Prenumeratos mazgai automatiškai sinchronizuojami su tarpinių serverių telkiniu ir naudoja esamus periodinio tikrinimo, būklės patikros bei apsaugos nuo nutekėjimo mechanizmus.", "addSubscription": "Pridėti prenumeratą", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Dar nėra įrašytų perjungimų", "lastSwitchMember": "Aptarnaujantis narys", "nextSwitch": "Kitas galimas perjungimas", - "nextSwitchNow": "Dabar" + "nextSwitchNow": "Dabar", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Masinis būklės patikrinimas", "bulkHealthcheckDesc": "Patikrinkite visus sukonfigūruotus tarpinius serverius naudodami tikslinį URL, kad nustatytumėte, kurie iš jų veikia.", diff --git a/src/i18n/messages/lv.json b/src/i18n/messages/lv.json index 2958edc473e5..5fd4d5b9844b 100644 --- a/src/i18n/messages/lv.json +++ b/src/i18n/messages/lv.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Vietējā proxy-core galapunkta adrese nav derīga; SS/VMess/Trojan/VLESS mezgli tika ignorēti.", "NEEDS_CORE_NOT_CONFIGURED": "Šajā abonementā ir mezgli, kuriem nepieciešams vietējais proxy core (SS/VMess/Trojan/VLESS); tie netiks maršrutēti, kamēr nebūs konfigurēts local-core SOCKS5 galapunkts.", "NO_USABLE_NODES": "Abonements nenodrošināja nevienu izmantojamu mezglu (http/https/socks5 vai mezglus ar vietējā kodola galapunktu).", - "SELECTOR_SWITCH_FAILED": "Starpniekservera kodola vadības izsaukums neizdevās; iepriekšējais maršruts paliek aktīvs. Pārbaudiet vadības URL un noslēpumu, pēc tam atsvaidziniet." + "SELECTOR_SWITCH_FAILED": "Starpniekservera kodola vadības izsaukums neizdevās; iepriekšējais maršruts paliek aktīvs. Pārbaudiet vadības URL un noslēpumu, pēc tam atsvaidziniet.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Ielīmējiet savu proxy abonementa saiti. Pēc iespējošanas datplūsma tiek maršrutēta caur proxy kopu globālajā vai kārtulu režīmā (norādītajiem pakalpojumu sniedzējiem). Abonementa mezgli tiek automātiski sinhronizēti ar proxy kopu un izmanto esošos aptaujas, darbspējas pārbaudes un aizsardzības pret noplūdēm mehānismus.", "addSubscription": "Pievienot abonementu", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Vēl nav reģistrētas pārslēgšanas", "lastSwitchMember": "Apkalpojošais dalībnieks", "nextSwitch": "Nākamā iespējamā pārslēgšana", - "nextSwitchNow": "Tagad" + "nextSwitchNow": "Tagad", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Masveida darbspējas pārbaude", "bulkHealthcheckDesc": "Pārbaudi visus konfigurētos starpniekserverus, izmantojot mērķa URL, lai noskaidrotu, kuri darbojas.", diff --git a/src/i18n/messages/ml.json b/src/i18n/messages/ml.json index cc20dc0cf258..5156ac7415f2 100644 --- a/src/i18n/messages/ml.json +++ b/src/i18n/messages/ml.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ലോക്കൽ proxy-core എൻഡ്പോയിന്റ് അസാധുവാണ്; SS/VMess/Trojan/VLESS നോഡുകൾ അവഗണിച്ചു.", "NEEDS_CORE_NOT_CONFIGURED": "ഈ സബ്സ്ക്രിപ്ഷനിൽ ലോക്കൽ പ്രോക്സി കോർ ആവശ്യമായ നോഡുകളുണ്ട് (SS/VMess/Trojan/VLESS); local-core SOCKS5 എൻഡ്പോയിന്റ് കോൺഫിഗർ ചെയ്യുന്നതുവരെ അവ റൂട്ട് ചെയ്യപ്പെടില്ല.", "NO_USABLE_NODES": "സബ്സ്ക്രിപ്ഷനിൽ ഉപയോഗയോഗ്യമായ നോഡുകളൊന്നും ലഭിച്ചില്ല (http/https/socks5 അല്ലെങ്കിൽ ലോക്കൽ കോർ എൻഡ്പോയിന്റുള്ള നോഡുകൾ).", - "SELECTOR_SWITCH_FAILED": "പ്രോക്സി-കോർ നിയന്ത്രണ വിളി പരാജയപ്പെട്ടു; മുൻപത്തെ വഴി സജീവമായി തുടരും. നിയന്ത്രണ URL-ഉം രഹസ്യവും പരിശോധിച്ച് പുതുക്കുക." + "SELECTOR_SWITCH_FAILED": "പ്രോക്സി-കോർ നിയന്ത്രണ വിളി പരാജയപ്പെട്ടു; മുൻപത്തെ വഴി സജീവമായി തുടരും. നിയന്ത്രണ URL-ഉം രഹസ്യവും പരിശോധിച്ച് പുതുക്കുക.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "നിങ്ങളുടെ പ്രോക്സി സബ്സ്ക്രിപ്ഷൻ ലിങ്ക് ഒട്ടിക്കുക. പ്രവർത്തനക്ഷമമാക്കിയാൽ, ഗ്ലോബൽ അല്ലെങ്കിൽ റൂൾ (നിർദ്ദിഷ്ട Provider) മോഡിൽ ട്രാഫിക് പ്രോക്സി പൂളിലൂടെ റൂട്ട് ചെയ്യപ്പെടും. സബ്സ്ക്രിപ്ഷൻ നോഡുകൾ സ്വയമേവ പ്രോക്സി പൂളിലേക്ക് സമന്വയിപ്പിക്കുകയും നിലവിലുള്ള പോളിംഗ്, ഹെൽത്ത്-ചെക്ക്, ആന്റി-ലീക്ക് സംവിധാനങ്ങൾ തന്നെ ഉപയോഗിക്കുകയും ചെയ്യും.", "addSubscription": "സബ്സ്ക്രിപ്ഷൻ ചേർക്കുക", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ഇതുവരെ മാറ്റമൊന്നും രേഖപ്പെടുത്തിയിട്ടില്ല", "lastSwitchMember": "സേവനം നൽകുന്ന അംഗം", "nextSwitch": "അടുത്ത സാധ്യമായ മാറ്റം", - "nextSwitchNow": "ഇപ്പോൾ" + "nextSwitchNow": "ഇപ്പോൾ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ബൾക്ക് ഹെൽത്ത് ചെക്ക്", "bulkHealthcheckDesc": "പ്രവർത്തിക്കുന്നവ കണ്ടെത്താൻ കോൺഫിഗർ ചെയ്ത എല്ലാ പ്രോക്സികളും ഒരു ലക്ഷ്യ URL-നെതിരെ പരിശോധിക്കുക.", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index f146ee10160a..9a1e14598c10 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "स्थानिक proxy-core endpoint अवैध आहे; SS/VMess/Trojan/VLESS नोड्स दुर्लक्षित केले गेले.", "NEEDS_CORE_NOT_CONFIGURED": "या सदस्यतेमध्ये असे नोड्स आहेत ज्यांना स्थानिक proxy core (SS/VMess/Trojan/VLESS) ची आवश्यकता आहे; जोपर्यंत तुम्ही local-core SOCKS5 endpoint कॉन्फिगर करत नाही तोपर्यंत ते रूट केले जात नाहीत.", "NO_USABLE_NODES": "सदस्यतेमधून कोणतेही वापरण्यायोग्य नोड्स मिळाले नाहीत (http/https/socks5 किंवा स्थानिक core endpoint असलेले नोड्स).", - "SELECTOR_SWITCH_FAILED": "प्रॉक्सी-कोर नियंत्रण कॉल अयशस्वी; मागील मार्ग सक्रिय राहतो. नियंत्रण URL व सीक्रेट तपासा, मग रिफ्रेश करा." + "SELECTOR_SWITCH_FAILED": "प्रॉक्सी-कोर नियंत्रण कॉल अयशस्वी; मागील मार्ग सक्रिय राहतो. नियंत्रण URL व सीक्रेट तपासा, मग रिफ्रेश करा.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "तुमची प्रॉक्सी सदस्यता लिंक पेस्ट करा. एकदा सक्षम केल्यानंतर, ट्रॅफिक प्रॉक्सी पूलद्वारे जागतिक किंवा नियम (निर्दिष्ट प्रदाता) मोडमध्ये मार्गस्थ केले जाते. सबस्क्रिप्शन नोड्स आपोआप प्रॉक्सी पूलमध्ये सिंक केले जातात आणि विद्यमान मतदान, आरोग्य-तपासणी आणि अँटी-लीक यंत्रणा पुन्हा वापरतात.", "addSubscription": "सदस्यता जोडा", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "अद्याप कोणताही बदल नोंदलेला नाही", "lastSwitchMember": "सेवा देणारा सदस्य", "nextSwitch": "पुढील संभाव्य बदल", - "nextSwitchNow": "आता" + "nextSwitchNow": "आता", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "सामूहिक आरोग्य तपासणी", "bulkHealthcheckDesc": "कोणते प्रॉक्सी कार्यरत आहेत हे शोधण्यासाठी सर्व कॉन्फिगर केलेले प्रॉक्सी लक्ष्य URL वर तपासा.", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index d319746558ac..90a3088b4311 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Titik akhir teras proksi tempatan tidak sah; nod SS/VMess/Trojan/VLESS telah diabaikan.", "NEEDS_CORE_NOT_CONFIGURED": "Langganan ini mempunyai nod yang memerlukan teras proksi tempatan (SS/VMess/Trojan/VLESS); nod tersebut tidak dihalakan sehingga anda mengkonfigurasi titik akhir SOCKS5 teras tempatan.", "NO_USABLE_NODES": "Langganan tidak menghasilkan nod yang boleh digunakan (http/https/socks5 atau nod dengan titik akhir teras tempatan).", - "SELECTOR_SWITCH_FAILED": "Panggilan kawalan proxy-core gagal; laluan sebelumnya kekal aktif. Semak URL dan rahsia kawalan, kemudian muat semula." + "SELECTOR_SWITCH_FAILED": "Panggilan kawalan proxy-core gagal; laluan sebelumnya kekal aktif. Semak URL dan rahsia kawalan, kemudian muat semula.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Tampalkan pautan langganan proksi anda. Setelah didayakan, trafik dihalakan melalui kumpulan proksi dalam mod global atau peraturan (Pembekal yang ditentukan). Nod langganan disegerakkan secara automatik ke dalam kumpulan proksi dan menggunakan semula mekanisme pengundian, pemeriksaan kesihatan dan anti-kebocoran sedia ada.", "addSubscription": "Tambah Langganan", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Belum ada pertukaran direkodkan", "lastSwitchMember": "Ahli yang berkhidmat", "nextSwitch": "Pertukaran seterusnya yang mungkin", - "nextSwitchNow": "Sekarang" + "nextSwitchNow": "Sekarang", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Pemeriksaan Kesihatan Pukal", "bulkHealthcheckDesc": "Uji semua proksi yang dikonfigurasikan terhadap URL sasaran untuk mengenal pasti proksi yang berfungsi.", diff --git a/src/i18n/messages/mt.json b/src/i18n/messages/mt.json index 1531ce4bd24e..bf46be6f8d1f 100644 --- a/src/i18n/messages/mt.json +++ b/src/i18n/messages/mt.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "L-endpoint lokali tal-proxy-core mhuwiex validu; in-nodi SS/VMess/Trojan/VLESS ġew injorati.", "NEEDS_CORE_NOT_CONFIGURED": "Dan l-abbonament għandu nodi li jeħtieġu proxy core lokali (SS/VMess/Trojan/VLESS); dawn ma jiġux direzzjonati sakemm tikkonfigura l-endpoint SOCKS5 tal-core lokali.", "NO_USABLE_NODES": "L-abbonament ma pproduċa l-ebda nodu li jista’ jintuża (http/https/socks5 jew nodi b’endpoint ta’ core lokali).", - "SELECTOR_SWITCH_FAILED": "Is-sejħa ta' kontroll tal-proxy-core falliet; ir-rotta ta' qabel tibqa attiva. Iċċekkja l-URL u s-sigriet tal-kontroll, imbagħad aġġorna." + "SELECTOR_SWITCH_FAILED": "Is-sejħa ta' kontroll tal-proxy-core falliet; ir-rotta ta' qabel tibqa attiva. Iċċekkja l-URL u s-sigriet tal-kontroll, imbagħad aġġorna.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Waħħal il-link tal-abbonament tal-proxy tiegħek. Ladarba jiġi attivat, it-traffiku jiġi direzzjonat permezz tal-grupp ta’ proxies fil-modalità globali jew tar-regoli (Fornitur speċifikat). In-nodi tal-abbonament jiġu sinkronizzati awtomatikament mal-grupp ta’ proxies u jużaw mill-ġdid il-mekkaniżmi eżistenti ta’ polling, kontroll tas-saħħa u kontra t-tnixxija.", "addSubscription": "Żid Abbonament", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "L-ebda bidla rreġistrata s'issa", "lastSwitchMember": "Membru li qed iservi", "nextSwitch": "Il-bidla li jmiss possibbli", - "nextSwitchNow": "Issa" + "nextSwitchNow": "Issa", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Kontroll tas-Saħħa tal-Massa", "bulkHealthcheckDesc": "Ittestja l-proxies konfigurati kollha ma' URL fil-mira biex issib liema minnhom jaħdmu.", diff --git a/src/i18n/messages/my.json b/src/i18n/messages/my.json index 2c3bf29989a3..a1f9300eeef2 100644 --- a/src/i18n/messages/my.json +++ b/src/i18n/messages/my.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "စက်တွင်း proxy-core endpoint မမှန်ကန်သောကြောင့် SS/VMess/Trojan/VLESS node များကို လျစ်လျူရှုထားသည်။", "NEEDS_CORE_NOT_CONFIGURED": "ဤစာရင်းသွင်းမှုတွင် စက်တွင်း proxy core လိုအပ်သည့် node များ (SS/VMess/Trojan/VLESS) ပါရှိသည်။ local-core SOCKS5 endpoint ကို သတ်မှတ်မပြီးမချင်း ၎င်းတို့မှတစ်ဆင့် လမ်းကြောင်းမပို့ပါ။", "NO_USABLE_NODES": "စာရင်းသွင်းမှုမှ အသုံးပြုနိုင်သော node များ မရရှိပါ (http/https/socks5 သို့မဟုတ် စက်တွင်း core endpoint ပါသော node များ)။", - "SELECTOR_SWITCH_FAILED": "proxy-core ထိန်းချုပ်ခေါ်ဆိုမှု မအောင်မြင်ပါ။ ယခင်လမ်းကြောင်းဆက်လက်အသက်ဝင်နေသည်။ ထိန်းချုပ် URL နှင့် လျှို့ဝှက်ချက်စစ်ဆေးပြီး ပြန်လည်ဆန်းသစ်ပါ။" + "SELECTOR_SWITCH_FAILED": "proxy-core ထိန်းချုပ်ခေါ်ဆိုမှု မအောင်မြင်ပါ။ ယခင်လမ်းကြောင်းဆက်လက်အသက်ဝင်နေသည်။ ထိန်းချုပ် URL နှင့် လျှို့ဝှက်ချက်စစ်ဆေးပြီး ပြန်လည်ဆန်းသစ်ပါ။", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "သင့် proxy စာရင်းသွင်းမှုလင့်ခ်ကို ကူးထည့်ပါ။ ဖွင့်ပြီးသည်နှင့် အသွားအလာကို ကမ္ဘာလုံးဆိုင်ရာ သို့မဟုတ် စည်းမျဉ်း (သတ်မှတ်ထားသော Provider) မုဒ်တွင် proxy အစုမှတစ်ဆင့် လမ်းကြောင်းပို့မည်။ စာရင်းသွင်းမှု node များကို proxy အစုထဲသို့ အလိုအလျောက် ထပ်တူပြုပြီး လက်ရှိ polling၊ အခြေအနေစစ်ဆေးမှုနှင့် ယိုစိမ့်မှုတားဆီးရေး ယန္တရားများကို ပြန်လည်အသုံးပြုမည်။", "addSubscription": "စာရင်းသွင်းမှု ထည့်ရန်", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "မှတ်တမ်းတင်ထားသောအပြောင်းအလဲမရှိသေးပါ", "lastSwitchMember": "ဝန်ဆောင်မှုပေးနေသောအဖွဲ့ဝင်", "nextSwitch": "နောက်ဖြစ်နိုင်သောအပြောင်းအလဲ", - "nextSwitchNow": "ယခု" + "nextSwitchNow": "ယခု", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "အစုလိုက် အခြေအနေစစ်ဆေးမှု", "bulkHealthcheckDesc": "အလုပ်လုပ်သည့် ပရောက်စီများကို ရှာဖွေရန် စီစဉ်သတ်မှတ်ထားသော ပရောက်စီအားလုံးကို ပစ်မှတ် URL တစ်ခုဖြင့် စမ်းသပ်ပါ။", diff --git a/src/i18n/messages/ne.json b/src/i18n/messages/ne.json index a77f71181c77..5fb64a2f108d 100644 --- a/src/i18n/messages/ne.json +++ b/src/i18n/messages/ne.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "स्थानीय proxy-core endpoint अमान्य छ; SS/VMess/Trojan/VLESS नोडहरूलाई बेवास्ता गरियो।", "NEEDS_CORE_NOT_CONFIGURED": "यो सदस्यतामा स्थानीय proxy core आवश्यक पर्ने नोडहरू छन् (SS/VMess/Trojan/VLESS); तपाईंले local-core SOCKS5 endpoint कन्फिगर नगरेसम्म तिनलाई रुट गरिँदैन।", "NO_USABLE_NODES": "सदस्यताबाट कुनै प्रयोगयोग्य नोडहरू प्राप्त भएनन् (http/https/socks5 वा स्थानीय core endpoint भएका नोडहरू)।", - "SELECTOR_SWITCH_FAILED": "प्रोक्सी-कोर नियन्त्रण कल असफल भयो; अघिल्लो मार्ग सक्रिय रहन्छ। नियन्त्रण URL र सिक्रेट जाँचेर रिफ्रेस गर्नुहोस्।" + "SELECTOR_SWITCH_FAILED": "प्रोक्सी-कोर नियन्त्रण कल असफल भयो; अघिल्लो मार्ग सक्रिय रहन्छ। नियन्त्रण URL र सिक्रेट जाँचेर रिफ्रेस गर्नुहोस्।", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "तपाईंको प्रोक्सी सदस्यता लिङ्क टाँस्नुहोस्। सक्षम गरिएपछि, ट्राफिकलाई global वा rule (तोकिएको Provider) मोडमा प्रोक्सी पूलमार्फत रुट गरिन्छ। सदस्यताका nodes स्वतः प्रोक्सी पूलमा सिंक हुन्छन् र विद्यमान polling, health-check तथा anti-leak संयन्त्रहरू पुनः प्रयोग गर्छन्।", "addSubscription": "सदस्यता थप्नुहोस्", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "अहिलेसम्म कुनै परिवर्तन दर्ता छैन", "lastSwitchMember": "सेवा दिइरहेको सदस्य", "nextSwitch": "अर्को सम्भावित परिवर्तन", - "nextSwitchNow": "अहिले" + "nextSwitchNow": "अहिले", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "सामूहिक स्वास्थ्य जाँच", "bulkHealthcheckDesc": "कुन प्रोक्सीहरू काम गर्छन् भनी पत्ता लगाउन कन्फिगर गरिएका सबै प्रोक्सीहरूलाई लक्षित URL विरुद्ध परीक्षण गर्नुहोस्।", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 4532778a63e7..94229ba1aa1b 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokaal proxy-core-eindpunt is ongeldig; SS/VMess/Trojan/VLESS-nodes zijn genegeerd.", "NEEDS_CORE_NOT_CONFIGURED": "Dit abonnement bevat nodes die een lokale proxy-core vereisen (SS/VMess/Trojan/VLESS); ze worden niet gerouteerd totdat u het SOCKS5-eindpunt van de lokale core configureert.", "NO_USABLE_NODES": "Abonnement heeft geen bruikbare nodes opgeleverd (http/https/socks5 of nodes met een lokaal core-eindpunt).", - "SELECTOR_SWITCH_FAILED": "De besturingsaanroep van de proxy-core is mislukt; de vorige route blijft actief. Controleer de besturings-URL en het geheim en vernieuw daarna." + "SELECTOR_SWITCH_FAILED": "De besturingsaanroep van de proxy-core is mislukt; de vorige route blijft actief. Controleer de besturings-URL en het geheim en vernieuw daarna.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Plak uw proxy-abonnementslink. Eenmaal ingeschakeld, wordt het verkeer door de proxypool gerouteerd in de globale of regelmodus (gespecificeerde provider). Abonnementknooppunten worden automatisch gesynchroniseerd met de proxypool en hergebruiken bestaande polling-, statuscheck- en anti-lekmechanismen.", "addSubscription": "Abonnement toevoegen", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Nog geen wisseling vastgelegd", "lastSwitchMember": "Bedienend lid", "nextSwitch": "Volgende mogelijke wisseling", - "nextSwitchNow": "Nu" + "nextSwitchNow": "Nu", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Bulkstatuscontrole", "bulkHealthcheckDesc": "Test alle geconfigureerde proxy's met een doel-URL om te bepalen welke werken.", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 5a67f9beffc7..6d1c24b87cd0 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalt endepunkt for proxykjernen er ugyldig; SS/VMess/Trojan/VLESS-noder ble ignorert.", "NEEDS_CORE_NOT_CONFIGURED": "Dette abonnementet har noder som krever en lokal proxykjerne (SS/VMess/Trojan/VLESS); de rutes ikke før du konfigurerer SOCKS5-endepunktet for den lokale kjernen.", "NO_USABLE_NODES": "Abonnementet ga ingen brukbare noder (http/https/socks5 eller noder med et lokalt kjerne-endepunkt).", - "SELECTOR_SWITCH_FAILED": "Proxy-coreens styrekall mislyktes; den forrige ruten forblir aktiv. Kontroller styre-URL og hemmelighet, og oppdater deretter." + "SELECTOR_SWITCH_FAILED": "Proxy-coreens styrekall mislyktes; den forrige ruten forblir aktiv. Kontroller styre-URL og hemmelighet, og oppdater deretter.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Lim inn koblingen for proxy-abonnement. Når den er aktivert, rutes trafikken gjennom proxy-poolen i global modus eller regel (spesifisert leverandør). Abonnementsnoder synkroniseres automatisk inn i proxy-poolen og gjenbruker eksisterende polling-, helsesjekk- og anti-lekkasjemekanismer.", "addSubscription": "Legg til abonnement", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Ingen bytter registrert ennå", "lastSwitchMember": "Betjenende medlem", "nextSwitch": "Neste mulige bytte", - "nextSwitchNow": "Nå" + "nextSwitchNow": "Nå", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Samlet tilstandskontroll", "bulkHealthcheckDesc": "Test alle konfigurerte proxyer mot en mål-URL for å finne ut hvilke som fungerer.", diff --git a/src/i18n/messages/or.json b/src/i18n/messages/or.json index ba4be2901fff..dff880fff088 100644 --- a/src/i18n/messages/or.json +++ b/src/i18n/messages/or.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ସ୍ଥାନୀୟ proxy-core ଏଣ୍ଡପଏଣ୍ଟ ଅବୈଧ; SS/VMess/Trojan/VLESS ନୋଡ୍ଗୁଡ଼ିକୁ ଅଣଦେଖା କରାଗଲା।", "NEEDS_CORE_NOT_CONFIGURED": "ଏହି ସବସ୍କ୍ରିପ୍ସନ୍ରେ ସ୍ଥାନୀୟ ପ୍ରକ୍ସି କୋର୍ (SS/VMess/Trojan/VLESS) ଆବଶ୍ୟକ କରୁଥିବା ନୋଡ୍ ଅଛି; ଆପଣ local-core SOCKS5 ଏଣ୍ଡପଏଣ୍ଟ ବିନ୍ୟାସ ନ କରିବା ପର୍ଯ୍ୟନ୍ତ ସେଗୁଡ଼ିକ ମାଧ୍ୟମରେ ଟ୍ରାଫିକ୍ ରୁଟ୍ ହେବ ନାହିଁ।", "NO_USABLE_NODES": "ସବସ୍କ୍ରିପ୍ସନ୍ରୁ କୌଣସି ବ୍ୟବହାରଯୋଗ୍ୟ ନୋଡ୍ ମିଳିଲା ନାହିଁ (http/https/socks5 କିମ୍ବା ସ୍ଥାନୀୟ କୋର୍ ଏଣ୍ଡପଏଣ୍ଟ ଥିବା ନୋଡ୍)।", - "SELECTOR_SWITCH_FAILED": "ପ୍ରୋକ୍ସି-କୋର୍ ନିୟନ୍ତ୍ରଣ କଲ୍ ବିଫଳ ହେଲା; ପୂର୍ବ ମାର୍ଗ ସକ୍ରିୟ ରହେ। ନିୟନ୍ତ୍ରଣ URL ଓ ଗୁପ୍ତ ଯାଞ୍ଚ କରି ରିଫ୍ରେଶ୍ କରନ୍ତୁ।" + "SELECTOR_SWITCH_FAILED": "ପ୍ରୋକ୍ସି-କୋର୍ ନିୟନ୍ତ୍ରଣ କଲ୍ ବିଫଳ ହେଲା; ପୂର୍ବ ମାର୍ଗ ସକ୍ରିୟ ରହେ। ନିୟନ୍ତ୍ରଣ URL ଓ ଗୁପ୍ତ ଯାଞ୍ଚ କରି ରିଫ୍ରେଶ୍ କରନ୍ତୁ।", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "ଆପଣଙ୍କ ପ୍ରକ୍ସି ସବସ୍କ୍ରିପ୍ସନ୍ ଲିଙ୍କ୍ ପେଷ୍ଟ କରନ୍ତୁ। ସକ୍ଷମ ହେବା ପରେ, ଟ୍ରାଫିକ୍ ଗ୍ଲୋବାଲ୍ କିମ୍ବା ନିୟମ (ନିର୍ଦ୍ଦିଷ୍ଟ ପ୍ରଦାନକାରୀ) ମୋଡ୍ରେ ପ୍ରକ୍ସି ପୁଲ୍ ମାଧ୍ୟମରେ ରୁଟ୍ ହୁଏ। ସବସ୍କ୍ରିପ୍ସନ୍ ନୋଡ୍ଗୁଡ଼ିକ ପ୍ରକ୍ସି ପୁଲ୍ରେ ସ୍ୱୟଂଚାଳିତ ଭାବେ ସିଙ୍କ୍ ହୁଏ ଏବଂ ବିଦ୍ୟମାନ ପୋଲିଂ, ସ୍ୱାସ୍ଥ୍ୟ-ଯାଞ୍ଚ ଏବଂ ଆଣ୍ଟି-ଲିକ୍ ବ୍ୟବସ୍ଥାକୁ ପୁନଃବ୍ୟବହାର କରେ।", "addSubscription": "ସବସ୍କ୍ରିପ୍ସନ୍ ଯୋଡ଼ନ୍ତୁ", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ସୁଇଚ୍ ଲିପିବଦ୍ଧ ନାହିଁ", "lastSwitchMember": "ସେବାକାରୀ ସଦସ୍ୟ", "nextSwitch": "ପରବର୍ତ୍ତୀ ସମ୍ଭାବ୍ୟ ସୁଇଚ୍", - "nextSwitchNow": "ବର୍ତ୍ତମାନ" + "nextSwitchNow": "ବର୍ତ୍ତମାନ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ସମୂହ ସ୍ୱାସ୍ଥ୍ୟ ଯାଞ୍ଚ", "bulkHealthcheckDesc": "କେଉଁଗୁଡ଼ିକ କାର୍ଯ୍ୟ କରୁଛି ଜାଣିବା ପାଇଁ ସମସ୍ତ ବିନ୍ୟାସିତ ପ୍ରକ୍ସିକୁ ଏକ ଲକ୍ଷ୍ୟ URL ସହିତ ପରୀକ୍ଷା କରନ୍ତୁ।", diff --git a/src/i18n/messages/pa.json b/src/i18n/messages/pa.json index 81b5b77ba65a..bfc34b6095da 100644 --- a/src/i18n/messages/pa.json +++ b/src/i18n/messages/pa.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ਲੋਕਲ proxy-core ਐਂਡਪੁਆਇੰਟ ਅਵੈਧ ਹੈ; SS/VMess/Trojan/VLESS ਨੋਡਾਂ ਨੂੰ ਅਣਡਿੱਠਾ ਕੀਤਾ ਗਿਆ।", "NEEDS_CORE_NOT_CONFIGURED": "ਇਸ ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਵਿੱਚ ਅਜਿਹੇ ਨੋਡ ਹਨ ਜਿਨ੍ਹਾਂ ਨੂੰ ਲੋਕਲ ਪ੍ਰੌਕਸੀ ਕੋਰ (SS/VMess/Trojan/VLESS) ਦੀ ਲੋੜ ਹੈ; ਜਦੋਂ ਤੱਕ ਤੁਸੀਂ local-core SOCKS5 ਐਂਡਪੁਆਇੰਟ ਕੌਂਫਿਗਰ ਨਹੀਂ ਕਰਦੇ, ਉਹ ਰੂਟ ਨਹੀਂ ਕੀਤੇ ਜਾਣਗੇ।", "NO_USABLE_NODES": "ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਤੋਂ ਕੋਈ ਵਰਤੋਂਯੋਗ ਨੋਡ ਨਹੀਂ ਮਿਲਿਆ (http/https/socks5 ਜਾਂ ਲੋਕਲ ਕੋਰ ਐਂਡਪੁਆਇੰਟ ਵਾਲੇ ਨੋਡ)।", - "SELECTOR_SWITCH_FAILED": "ਪ੍ਰੌਕਸੀ-ਕੋਰ ਕੰਟਰੋਲ ਕਾਲ ਅਸਫਲ ਰਹੀ; ਪਿਛਲਾ ਰਸਤਾ ਸਰਗਰਮ ਰਹਿੰਦਾ ਹੈ। ਕੰਟਰੋਲ URL ਅਤੇ ਸੀਕਰੇਟ ਜਾਂਚੋ, ਫਿਰ ਰਿਫ੍ਰੈਸ਼ ਕਰੋ।" + "SELECTOR_SWITCH_FAILED": "ਪ੍ਰੌਕਸੀ-ਕੋਰ ਕੰਟਰੋਲ ਕਾਲ ਅਸਫਲ ਰਹੀ; ਪਿਛਲਾ ਰਸਤਾ ਸਰਗਰਮ ਰਹਿੰਦਾ ਹੈ। ਕੰਟਰੋਲ URL ਅਤੇ ਸੀਕਰੇਟ ਜਾਂਚੋ, ਫਿਰ ਰਿਫ੍ਰੈਸ਼ ਕਰੋ।", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "ਆਪਣਾ ਪ੍ਰੌਕਸੀ ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਲਿੰਕ ਪੇਸਟ ਕਰੋ। ਸਮਰੱਥ ਹੋਣ ਤੋਂ ਬਾਅਦ, ਟ੍ਰੈਫਿਕ ਨੂੰ ਗਲੋਬਲ ਜਾਂ ਨਿਯਮ (ਨਿਰਧਾਰਤ Provider) ਮੋਡ ਵਿੱਚ ਪ੍ਰੌਕਸੀ ਪੂਲ ਰਾਹੀਂ ਰੂਟ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਨੋਡ ਆਪਣੇ ਆਪ ਪ੍ਰੌਕਸੀ ਪੂਲ ਵਿੱਚ ਸਿੰਕ ਹੋ ਜਾਂਦੇ ਹਨ ਅਤੇ ਮੌਜੂਦਾ ਪੋਲਿੰਗ, ਸਿਹਤ-ਜਾਂਚ ਅਤੇ ਐਂਟੀ-ਲੀਕ ਵਿਧੀਆਂ ਦੀ ਮੁੜ ਵਰਤੋਂ ਕਰਦੇ ਹਨ।", "addSubscription": "ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਜੋੜੋ", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ਅਜੇ ਕੋਈ ਬਦਲਾਅ ਦਰਜ ਨਹੀਂ", "lastSwitchMember": "ਸੇਵਾ ਕਰ ਰਿਹਾ ਮੈਂਬਰ", "nextSwitch": "ਅਗਲਾ ਸੰਭਵ ਬਦਲਾਅ", - "nextSwitchNow": "ਹੁਣੇ" + "nextSwitchNow": "ਹੁਣੇ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ਸਮੂਹਿਕ ਸਿਹਤ ਜਾਂਚ", "bulkHealthcheckDesc": "ਇਹ ਪਤਾ ਲਗਾਉਣ ਲਈ ਕਿ ਕਿਹੜੇ ਕੰਮ ਕਰਦੇ ਹਨ, ਸਾਰੇ ਕੌਂਫਿਗਰ ਕੀਤੇ ਪ੍ਰੌਕਸੀ ਇੱਕ ਟਾਰਗੇਟ URL ਨਾਲ ਟੈਸਟ ਕਰੋ।", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index d1a827a30814..200da706cdb4 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Hindi wasto ang lokal na proxy-core endpoint; binalewala ang mga SS/VMess/Trojan/VLESS node.", "NEEDS_CORE_NOT_CONFIGURED": "Ang subscription na ito ay may mga node na nangangailangan ng lokal na proxy core (SS/VMess/Trojan/VLESS); hindi iruruta ang mga ito hanggang sa i-configure mo ang local-core SOCKS5 endpoint.", "NO_USABLE_NODES": "Walang naibigay na magagamit na mga node ang subscription (http/https/socks5 o mga node na may lokal na core endpoint).", - "SELECTOR_SWITCH_FAILED": "Nabigo ang control call ng proxy-core; nananatiling aktibo ang nakaraang ruta. Suriin ang control URL at secret, saka mag-refresh." + "SELECTOR_SWITCH_FAILED": "Nabigo ang control call ng proxy-core; nananatiling aktibo ang nakaraang ruta. Suriin ang control URL at secret, saka mag-refresh.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "I-paste ang link ng iyong proxy na subscription. Kapag na-enable na, iruruta ang trapiko sa proxy pool sa pandaigdigang mode o panuntunan (tinukoy na Provider). Awtomatikong sini-sync ang mga node ng subscription sa proxy pool at muling ginagamit ang mga umiiral nang mekanismo ng botohan, pagsusuri sa kalusugan, at anti-leak.", "addSubscription": "Magdagdag ng Subscription", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Wala pang naitalang paglipat", "lastSwitchMember": "Miyembrong naglilingkod", "nextSwitch": "Susunod na posibleng paglipat", - "nextSwitchNow": "Ngayon" + "nextSwitchNow": "Ngayon", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Maramihang Pagsusuri sa Kalagayan", "bulkHealthcheckDesc": "Subukan ang lahat ng naka-configure na proxy gamit ang isang target na URL upang malaman kung alin ang gumagana.", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 3bad4555ba73..a1098782ea2c 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalny punkt końcowy rdzenia proxy jest nieprawidłowy; węzły SS/VMess/Trojan/VLESS zostały zignorowane.", "NEEDS_CORE_NOT_CONFIGURED": "Ta subskrypcja zawiera węzły, które wymagają lokalnego rdzenia proxy (SS/VMess/Trojan/VLESS); nie są one trasowane, dopóki nie skonfigurujesz punktu końcowego SOCKS5 lokalnego rdzenia.", "NO_USABLE_NODES": "Subskrypcja nie zwróciła żadnych użytecznych węzłów (http/https/socks5 lub węzłów z punktem końcowym lokalnego rdzenia).", - "SELECTOR_SWITCH_FAILED": "Wywołanie sterujące rdzenia proxy nie powiodło się; poprzednia trasa pozostaje aktywna. Sprawdź adres URL i sekret sterowania, a następnie odśwież." + "SELECTOR_SWITCH_FAILED": "Wywołanie sterujące rdzenia proxy nie powiodło się; poprzednia trasa pozostaje aktywna. Sprawdź adres URL i sekret sterowania, a następnie odśwież.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Wklej link do subskrypcji proxy. Po włączeniu ruch jest kierowany przez pulę proxy w trybie globalnym lub regułowym (określony dostawca). Węzły subskrypcyjne są automatycznie synchronizowane z pulą proxy i ponownie wykorzystują istniejące mechanizmy odpytywania, sprawdzania stanu i zapobiegania wyciekom.", "addSubscription": "Dodaj subskrypcję", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Brak zarejestrowanych przełączeń", "lastSwitchMember": "Obsługujący element", "nextSwitch": "Następne możliwe przełączenie", - "nextSwitchNow": "Teraz" + "nextSwitchNow": "Teraz", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Zbiorczy Healthcheck", "bulkHealthcheckDesc": "Testowanie wszystkich skonfigurowanych proxy pod kątem docelowego URL w celu znalezienia działających.", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 42f8e7a3e66e..5a3d275057cc 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "O endpoint do core local é inválido; nós SS/VMess/Trojan/VLESS foram ignorados.", "NEEDS_CORE_NOT_CONFIGURED": "Esta assinatura tem nós que precisam de um core local (SS/VMess/Trojan/VLESS); eles não são roteados até você configurar o endpoint SOCKS5 do core local.", "NO_USABLE_NODES": "A assinatura não retornou nós utilizáveis (http/https/socks5 ou nós com endpoint de core local).", - "SELECTOR_SWITCH_FAILED": "A chamada de controle do proxy-core falhou; a rota anterior continua ativa. Verifique a URL e o segredo de controle e atualize." + "SELECTOR_SWITCH_FAILED": "A chamada de controle do proxy-core falhou; a rota anterior continua ativa. Verifique a URL e o segredo de controle e atualize.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Cole o link de assinatura do seu proxy. Uma vez ativado, o tráfego é roteado através do pool de proxy no modo global ou de regra (Provedor especificado). Os nós de assinatura são sincronizados automaticamente no pool de proxy e reutilizam mecanismos existentes de pesquisa, verificação de integridade e antivazamento.", "addSubscription": "Adicionar assinatura", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Nenhuma troca registrada", "lastSwitchMember": "Membro em atendimento", "nextSwitch": "Próxima troca possível", - "nextSwitchNow": "Agora" + "nextSwitchNow": "Agora", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Verificação de integridade em massa", "bulkHealthcheckDesc": "Teste todos os proxies configurados contra uma URL de destino para encontrar quais funcionam.", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index b9cddd55498d..0481d9ffeda3 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "O endpoint do proxy-core local é inválido; os nós SS/VMess/Trojan/VLESS foram ignorados.", "NEEDS_CORE_NOT_CONFIGURED": "Esta subscrição tem nós que necessitam de um núcleo de proxy local (SS/VMess/Trojan/VLESS); não serão encaminhados até configurar o endpoint SOCKS5 do núcleo local.", "NO_USABLE_NODES": "A subscrição não resultou em nós utilizáveis (http/https/socks5 ou nós com um endpoint do núcleo local).", - "SELECTOR_SWITCH_FAILED": "A chamada de controlo do proxy-core falhou; a rota anterior continua ativa. Verifique o URL e o segredo de controlo e atualize." + "SELECTOR_SWITCH_FAILED": "A chamada de controlo do proxy-core falhou; a rota anterior continua ativa. Verifique o URL e o segredo de controlo e atualize.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Cole o link de assinatura do seu proxy. Uma vez ativado, o tráfego é roteado através do pool de proxy no modo global ou de regra (Provedor especificado). Os nós de assinatura são sincronizados automaticamente no pool de proxy e reutilizam mecanismos existentes de pesquisa, verificação de integridade e antivazamento.", "addSubscription": "Adicionar assinatura", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Nenhuma troca registada", "lastSwitchMember": "Membro em serviço", "nextSwitch": "Próxima troca possível", - "nextSwitchNow": "Agora" + "nextSwitchNow": "Agora", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Verificação de Saúde em Massa", "bulkHealthcheckDesc": "Testa todos os proxies configurados contra um URL alvo para descobrir quais funcionam.", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 0752a8908637..c3eadb899fca 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Endpoint-ul local proxy-core este invalid; nodurile SS/VMess/Trojan/VLESS au fost ignorate.", "NEEDS_CORE_NOT_CONFIGURED": "Acest abonament are noduri care necesită un proxy-core local (SS/VMess/Trojan/VLESS); acestea nu sunt rutate până când nu configurați endpoint-ul SOCKS5 local-core.", "NO_USABLE_NODES": "Abonamentul nu a returnat noduri utilizabile (http/https/socks5 sau noduri cu un endpoint local-core).", - "SELECTOR_SWITCH_FAILED": "Apelul de control al proxy-core a eșuat; ruta anterioară rămâne activă. Verificați URL-ul și secretul de control, apoi reîmprospătați." + "SELECTOR_SWITCH_FAILED": "Apelul de control al proxy-core a eșuat; ruta anterioară rămâne activă. Verificați URL-ul și secretul de control, apoi reîmprospătați.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Lipiți linkul de abonare proxy. Odată activat, traficul este direcționat prin grupul de proxy în modul global sau cu reguli (furnizor specificat). Nodurile de abonament sunt sincronizate automat în pool-ul de proxy și reutilizează mecanismele existente de sondare, verificare a stării de sănătate și anti-scurgere.", "addSubscription": "Adăugați abonament", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Nicio comutare înregistrată", "lastSwitchMember": "Membrul care deservește", "nextSwitch": "Următoarea comutare posibilă", - "nextSwitchNow": "Acum" + "nextSwitchNow": "Acum", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Verificare în bloc a stării", "bulkHealthcheckDesc": "Testează toate proxy-urile configurate folosind un URL țintă pentru a identifica proxy-urile funcționale.", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 4aca925be340..76058a2af4e7 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Локальный эндпоинт прокси-ядра недействителен; узлы SS/VMess/Trojan/VLESS были проигнорированы.", "NEEDS_CORE_NOT_CONFIGURED": "Эта подписка содержит узлы, которым требуется локальное прокси-ядро (SS/VMess/Trojan/VLESS); они не будут маршрутизироваться, пока вы не настроите SOCKS5-эндпоинт локального ядра.", "NO_USABLE_NODES": "Подписка не вернула доступных узлов (http/https/socks5 или узлов с локальным эндпоинтом ядра).", - "SELECTOR_SWITCH_FAILED": "Вызов управления прокси-ядра не удался; предыдущий маршрут остаётся активным. Проверьте URL и секрет управления, затем обновите." + "SELECTOR_SWITCH_FAILED": "Вызов управления прокси-ядра не удался; предыдущий маршрут остаётся активным. Проверьте URL и секрет управления, затем обновите.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Вставьте ссылку на подписку на прокси. После включения трафик маршрутизируется через пул прокси-серверов в глобальном режиме или режиме правила (указанного поставщика). Узлы подписки автоматически синхронизируются с пулом прокси-серверов и повторно используют существующие механизмы опроса, проверки работоспособности и защиты от утечек.", "addSubscription": "Добавить подписку", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Переключения пока не записаны", "lastSwitchMember": "Обслуживающий участник", "nextSwitch": "Следующее возможное переключение", - "nextSwitchNow": "Сейчас" + "nextSwitchNow": "Сейчас", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Массовая проверка здоровья", "bulkHealthcheckDesc": "Проверьте все настроенные прокси по целевому URL, чтобы определить рабочие.", diff --git a/src/i18n/messages/si.json b/src/i18n/messages/si.json index beb37ebe01b3..1a5bff6da29a 100644 --- a/src/i18n/messages/si.json +++ b/src/i18n/messages/si.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "දේශීය proxy-core අන්ත ලක්ෂ්යය වලංගු නොවේ; SS/VMess/Trojan/VLESS නෝඩ නොසලකා හරින ලදී.", "NEEDS_CORE_NOT_CONFIGURED": "මෙම දායකත්වයේ දේශීය ප්රොක්සි කෝරයක් අවශ්ය නෝඩ ඇත (SS/VMess/Trojan/VLESS); ඔබ local-core SOCKS5 අන්ත ලක්ෂ්යය වින්යාස කරන තෙක් ඒවා මාර්ගගත නොකෙරේ.", "NO_USABLE_NODES": "දායකත්වයෙන් භාවිත කළ හැකි නෝඩ කිසිවක් නොලැබුණි (http/https/socks5 හෝ දේශීය කෝර අන්ත ලක්ෂ්යයක් සහිත නෝඩ).", - "SELECTOR_SWITCH_FAILED": "ප්‍රොක්සි-කෝර් පාලන ඇමතුම අසාර්ථක විය; පෙර මාර්ගය සක්‍රීයව පවතී. පාලන URL සහ රහස පරීක්ෂා කර නැවුම් කරන්න." + "SELECTOR_SWITCH_FAILED": "ප්‍රොක්සි-කෝර් පාලන ඇමතුම අසාර්ථක විය; පෙර මාර්ගය සක්‍රීයව පවතී. පාලන URL සහ රහස පරීක්ෂා කර නැවුම් කරන්න.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "ඔබේ ප්රොක්සි දායකත්ව සබැඳිය අලවන්න. සබල කළ පසු, ගමනාගමනය ගෝලීය හෝ රීති (නිශ්චිත Provider) ප්රකාරයේදී ප්රොක්සි සංචිතය හරහා මාර්ගගත කෙරේ. දායකත්ව නෝඩ ස්වයංක්රීයව ප්රොක්සි සංචිතයට සමමුහුර්ත කර, පවතින polling, health-check සහ anti-leak යාන්ත්රණ නැවත භාවිත කරයි.", "addSubscription": "දායකත්වයක් එක් කරන්න", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "තවම මාරුවක් සටහන් වී නැත", "lastSwitchMember": "සේවය කරන සාමාජිකයා", "nextSwitch": "ඊළඟ විය හැකි මාරුව", - "nextSwitchNow": "දැන්" + "nextSwitchNow": "දැන්", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "සමූහ සෞඛ්ය පරීක්ෂාව", "bulkHealthcheckDesc": "ක්රියා කරන ඒවා හඳුනා ගැනීමට වින්යාස කර ඇති සියලුම ප්රොක්සි ඉලක්ක URL එකක් සමඟ පරීක්ෂා කරන්න.", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 666a5d4f7dee..43aa6195b09c 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokálny koncový bod proxy-core je neplatný; uzly SS/VMess/Trojan/VLESS boli ignorované.", "NEEDS_CORE_NOT_CONFIGURED": "Toto predplatné obsahuje uzly, ktoré vyžadujú lokálne proxy jadro (SS/VMess/Trojan/VLESS); nebudú smerované, kým nenakonfigurujete koncový bod SOCKS5 pre lokálne jadro.", "NO_USABLE_NODES": "Predplatné neposkytlo žiadne použiteľné uzly (http/https/socks5 alebo uzly s koncovým bodom lokálneho jadra).", - "SELECTOR_SWITCH_FAILED": "Riadiace volanie proxy-core zlyhalo; predchádzajúca trasa zostáva aktívna. Skontrolujte riadiacu URL a tajný kľúč a obnovte." + "SELECTOR_SWITCH_FAILED": "Riadiace volanie proxy-core zlyhalo; predchádzajúca trasa zostáva aktívna. Skontrolujte riadiacu URL a tajný kľúč a obnovte.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Prilepte odkaz na odber servera proxy. Po povolení je prevádzka smerovaná cez fond proxy v globálnom režime alebo režime pravidiel (špecifikovaný poskytovateľ). Uzly predplatného sa automaticky synchronizujú do fondu proxy a opätovne používajú existujúce mechanizmy výziev, kontroly stavu a ochrany proti úniku.", "addSubscription": "Pridať predplatné", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Zatiaľ žiadne zaznamenané prepnutie", "lastSwitchMember": "Obsluhujúci člen", "nextSwitch": "Ďalšie možné prepnutie", - "nextSwitchNow": "Teraz" + "nextSwitchNow": "Teraz", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Hromadná kontrola dostupnosti", "bulkHealthcheckDesc": "Otestujte všetky nakonfigurované proxy voči cieľovej URL a zistite, ktoré fungujú.", diff --git a/src/i18n/messages/sl.json b/src/i18n/messages/sl.json index eb6c6260250e..8ca2be1b3699 100644 --- a/src/i18n/messages/sl.json +++ b/src/i18n/messages/sl.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokalna končna točka jedra posredniškega strežnika ni veljavna; vozlišča SS/VMess/Trojan/VLESS so bila prezrta.", "NEEDS_CORE_NOT_CONFIGURED": "Ta naročnina vsebuje vozlišča, ki potrebujejo lokalno jedro posredniškega strežnika (SS/VMess/Trojan/VLESS); promet se prek njih ne usmerja, dokler ne konfigurirate končne točke SOCKS5 lokalnega jedra.", "NO_USABLE_NODES": "Naročnina ni vrnila nobenega uporabnega vozlišča (http/https/socks5 ali vozlišč s končno točko lokalnega jedra).", - "SELECTOR_SWITCH_FAILED": "Krmilni klic proxy-core ni uspel; prejšnja pot ostane dejavna. Preverite krmilni URL in skrivnost, nato osvežite." + "SELECTOR_SWITCH_FAILED": "Krmilni klic proxy-core ni uspel; prejšnja pot ostane dejavna. Preverite krmilni URL in skrivnost, nato osvežite.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Prilepite povezavo do svoje naročnine na posredniške strežnike. Ko je omogočena, se promet usmerja prek zbirke posredniških strežnikov v globalnem načinu ali načinu pravil (določeni ponudnik). Vozlišča naročnine se samodejno sinhronizirajo z zbirko posredniških strežnikov ter uporabljajo obstoječe mehanizme periodičnega preverjanja, preverjanja stanja in preprečevanja uhajanja podatkov.", "addSubscription": "Dodaj naročnino", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Še ni zabeleženega preklopa", "lastSwitchMember": "Član, ki streže", "nextSwitch": "Naslednji mogoči preklop", - "nextSwitchNow": "Zdaj" + "nextSwitchNow": "Zdaj", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Množično preverjanje stanja", "bulkHealthcheckDesc": "Preizkusite vse konfigurirane posredniške strežnike z dostopom do ciljnega URL-ja, da ugotovite, kateri delujejo.", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index ed9c7a0409de..0494e47b9f52 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Локална крајња тачка за proxy-core није важећа; SS/VMess/Trojan/VLESS чворови су игнорисани.", "NEEDS_CORE_NOT_CONFIGURED": "Ова претплата садржи чворове који захтевају локално proxy језгро (SS/VMess/Trojan/VLESS); они се не рутирају док не подесите локалну SOCKS5 крајњу тачку језгра.", "NO_USABLE_NODES": "Претплата није дала употребљиве чворове (http/https/socks5 или чворове са локалном крајњом тачком језгра).", - "SELECTOR_SWITCH_FAILED": "Управљачки позив proxy-core-а није успео; претходна рута остаје активна. Проверите управљачки URL и тајну, па освежите." + "SELECTOR_SWITCH_FAILED": "Управљачки позив proxy-core-а није успео; претходна рута остаје активна. Проверите управљачки URL и тајну, па освежите.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Налепите везу за прокси претплату. Када се активира, саобраћај се рутира кроз прокси пул у глобалном режиму или режиму правила (одабрани провајдер). Чворови из претплате се аутоматски синхронизују у прокси пул и користе постојеће механизме циклирања, провере исправности и заштите од цурења.", "addSubscription": "Додај претплату", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Још нема евидентираног пребацивања", "lastSwitchMember": "Члан који опслужује", "nextSwitch": "Следеће могуће пребацивање", - "nextSwitchNow": "Сада" + "nextSwitchNow": "Сада", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Масовна провера исправности", "bulkHealthcheckDesc": "Тестирајте све конфигурисане проксије на циљном URL-у да бисте открили који раде.", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index e8d9dc3b927a..d9d048485917 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Lokal proxy-core-slutpunkt är ogiltig; SS/VMess/Trojan/VLESS-noder ignorerades.", "NEEDS_CORE_NOT_CONFIGURED": "Denna prenumeration har noder som behöver en lokal proxy-core (SS/VMess/Trojan/VLESS); de dirigeras inte förrän du konfigurerar local-core SOCKS5-slutpunkten.", "NO_USABLE_NODES": "Prenumerationen gav inga användbara noder (http/https/socks5 eller noder med en lokal core-slutpunkt).", - "SELECTOR_SWITCH_FAILED": "Proxy-cores styranrop misslyckades; den tidigare rutten förblir aktiv. Kontrollera styr-URL och hemlighet och uppdatera sedan." + "SELECTOR_SWITCH_FAILED": "Proxy-cores styranrop misslyckades; den tidigare rutten förblir aktiv. Kontrollera styr-URL och hemlighet och uppdatera sedan.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Klistra in din proxy-prenumerationslänk. När den har aktiverats dirigeras trafiken genom proxypoolen i globalt läge eller regel (specificerad leverantör). Prenumerationsnoder synkroniseras automatiskt till proxypoolen och återanvänder befintliga polling-, hälsokontroll- och anti-läckmekanismer.", "addSubscription": "Lägg till prenumeration", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Inga byten registrerade ännu", "lastSwitchMember": "Betjänande medlem", "nextSwitch": "Nästa möjliga byte", - "nextSwitchNow": "Nu" + "nextSwitchNow": "Nu", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Samlad hälsokontroll", "bulkHealthcheckDesc": "Testa alla konfigurerade proxyservrar mot en mål-URL för att se vilka som fungerar.", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 87c5d29e918e..28338dc23ec5 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Mwisho wa proxy-core ya ndani si halali; nodi za SS/VMess/Trojan/VLESS zilipuuzwa.", "NEEDS_CORE_NOT_CONFIGURED": "Usajili huu una nodi zinazohitaji proxy core ya ndani (SS/VMess/Trojan/VLESS); hazitaelekezwa hadi usanidi mwisho wa SOCKS5 wa local-core.", "NO_USABLE_NODES": "Usajili haukutoa nodi zozote zinazoweza kutumika (http/https/socks5 au nodi zilizo na mwisho wa local core).", - "SELECTOR_SWITCH_FAILED": "Mwito wa kudhibiti wa proxy-core umeshindwa; njia iliyotangulia inaendelea kufanya kazi. Angalia URL na siri ya kudhibiti, kisha onyesha upya." + "SELECTOR_SWITCH_FAILED": "Mwito wa kudhibiti wa proxy-core umeshindwa; njia iliyotangulia inaendelea kufanya kazi. Angalia URL na siri ya kudhibiti, kisha onyesha upya.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Bandika kiungo cha usajili wako wa wakala. Mara baada ya kuwezeshwa, trafiki hupitishwa kupitia hifadhi ya seva mbadala katika hali ya kimataifa au sheria (Mtoa huduma aliyebainishwa). Nodi za usajili husawazishwa kiotomatiki kwenye hifadhi ya seva mbadala na kutumia tena mbinu zilizopo za upigaji kura, ukaguzi wa afya na kuzuia kuvuja.", "addSubscription": "Ongeza Usajili", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Hakuna mabadiliko yaliyorekodiwa bado", "lastSwitchMember": "Mwanachama anayehudumu", "nextSwitch": "Mabadiliko yajayo yanayowezekana", - "nextSwitchNow": "Sasa" + "nextSwitchNow": "Sasa", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Ukaguzi wa Afya kwa Wingi", "bulkHealthcheckDesc": "Jaribu proksi zote zilizosanidiwa dhidi ya URL lengwa ili kubaini zinazofanya kazi.", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index edeaeb3ffd59..0e64fcd82ff9 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "உள்ளூர் proxy-core எண்ட்பாயிண்ட் செல்லாதது; SS/VMess/Trojan/VLESS முனைகள் புறக்கணிக்கப்பட்டன.", "NEEDS_CORE_NOT_CONFIGURED": "இந்த சந்தாவில் உள்ளூர் proxy core (SS/VMess/Trojan/VLESS) தேவைப்படும் முனைகள் உள்ளன; நீங்கள் local-core SOCKS5 எண்ட்பாயிண்ட்டை உள்ளமைக்கும் வரை அவை வழிநடத்தப்படாது.", "NO_USABLE_NODES": "சந்தா பயன்படுத்தக்கூடிய முனைகள் எதையும் வழங்கவில்லை (http/https/socks5 அல்லது உள்ளூர் core எண்ட்பாயிண்ட் கொண்ட முனைகள்).", - "SELECTOR_SWITCH_FAILED": "ப்ராக்ஸி-கோர் கட்டுப்பாட்டு அழைப்பு தோல்வியடைந்தது; முந்தைய வழி செயலில் உள்ளது. கட்டுப்பாட்டு URL மற்றும் ரகசியத்தைச் சரிபார்த்துப் புதுப்பிக்கவும்." + "SELECTOR_SWITCH_FAILED": "ப்ராக்ஸி-கோர் கட்டுப்பாட்டு அழைப்பு தோல்வியடைந்தது; முந்தைய வழி செயலில் உள்ளது. கட்டுப்பாட்டு URL மற்றும் ரகசியத்தைச் சரிபார்த்துப் புதுப்பிக்கவும்.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "உங்கள் ப்ராக்ஸி சந்தா இணைப்பை ஒட்டவும். இயக்கப்பட்டதும், உலகளாவிய அல்லது விதி (குறிப்பிடப்பட்ட வழங்குநர்) பயன்முறையில் ப்ராக்ஸி பூல் மூலம் போக்குவரத்து அனுப்பப்படும். சந்தா முனைகள் தானாகவே ப்ராக்ஸி குளத்தில் ஒத்திசைக்கப்படும் மற்றும் ஏற்கனவே உள்ள வாக்குப்பதிவு, சுகாதார சோதனை மற்றும் கசிவு எதிர்ப்பு வழிமுறைகளை மீண்டும் பயன்படுத்துகின்றன.", "addSubscription": "சந்தாவைச் சேர்க்கவும்", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "இன்னும் மாற்றம் பதிவாகவில்லை", "lastSwitchMember": "சேவை செய்யும் உறுப்பினர்", "nextSwitch": "அடுத்த சாத்தியமான மாற்றம்", - "nextSwitchNow": "இப்போது" + "nextSwitchNow": "இப்போது", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "தொகுதி செயல்நிலைச் சோதனை", "bulkHealthcheckDesc": "இலக்கு URL-க்கு எதிராக உள்ளமைக்கப்பட்ட அனைத்து ப்ராக்ஸிகளையும் சோதித்து, செயல்படுபவற்றைக் கண்டறியவும்.", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index fcccd32a573b..0443e562de14 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "లోకల్ ప్రోక్సీ-కోర్ ఎండ్‌పాయింట్ చెల్లదు; SS/VMess/Trojan/VLESS నోడ్‌లు విస్మరించబడ్డాయి.", "NEEDS_CORE_NOT_CONFIGURED": "ఈ సబ్‌స్క్రిప్షన్‌లో లోకల్ ప్రోక్సీ కోర్ (SS/VMess/Trojan/VLESS) అవసరమైన నోడ్‌లు ఉన్నాయి; మీరు లోకల్-కోర్ SOCKS5 ఎండ్‌పాయింట్‌ను కాన్ఫిగర్ చేసే వరకు అవి రూట్ చేయబడవు.", "NO_USABLE_NODES": "సబ్‌స్క్రిప్షన్ ఉపయోగించదగిన నోడ్‌లను దేనినీ అందించలేదు (http/https/socks5 లేదా లోకల్ కోర్ ఎండ్‌పాయింట్ కలిగిన నోడ్‌లు).", - "SELECTOR_SWITCH_FAILED": "ప్రాక్సీ-కోర్ నియంత్రణ కాల్ విఫలమైంది; మునుపటి మార్గం చురుకుగా ఉంటుంది. నియంత్రణ URL మరియు రహస్యాన్ని తనిఖీ చేసి రిఫ్రెష్ చేయండి." + "SELECTOR_SWITCH_FAILED": "ప్రాక్సీ-కోర్ నియంత్రణ కాల్ విఫలమైంది; మునుపటి మార్గం చురుకుగా ఉంటుంది. నియంత్రణ URL మరియు రహస్యాన్ని తనిఖీ చేసి రిఫ్రెష్ చేయండి.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "మీ ప్రాక్సీ సబ్‌స్క్రిప్షన్ లింక్‌ను అతికించండి. ప్రారంభించిన తర్వాత, ట్రాఫిక్ గ్లోబల్ లేదా రూల్ (పేర్కొన్న ప్రొవైడర్) మోడ్‌లో ప్రాక్సీ పూల్ ద్వారా మళ్లించబడుతుంది. సబ్‌స్క్రిప్షన్ నోడ్‌లు స్వయంచాలకంగా ప్రాక్సీ పూల్‌లోకి సమకాలీకరించబడతాయి మరియు ఇప్పటికే ఉన్న పోలింగ్, ఆరోగ్య తనిఖీ మరియు యాంటీ లీక్ మెకానిజమ్‌లను మళ్లీ ఉపయోగిస్తాయి.", "addSubscription": "సభ్యత్వాన్ని జోడించండి", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ఇంకా మార్పు నమోదు కాలేదు", "lastSwitchMember": "సేవలందిస్తున్న సభ్యుడు", "nextSwitch": "తదుపరి సాధ్యమైన మార్పు", - "nextSwitchNow": "ఇప్పుడు" + "nextSwitchNow": "ఇప్పుడు", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "బల్క్ హెల్త్చెక్", "bulkHealthcheckDesc": "పనిచేసే ప్రాక్సీలను గుర్తించడానికి కాన్ఫిగర్ చేసిన అన్ని ప్రాక్సీలను లక్ష్య URLతో పరీక్షించండి.", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 154c7ab9e86b..be93e0d466a1 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "ปลายทาง proxy-core ภายในเครื่องไม่ถูกต้อง; โหนด SS/VMess/Trojan/VLESS ถูกละเว้น", "NEEDS_CORE_NOT_CONFIGURED": "การสมัครสมาชิกนี้มีโหนดที่ต้องใช้ proxy core ภายในเครื่อง (SS/VMess/Trojan/VLESS); โหนดเหล่านั้นจะไม่ถูกกำหนดเส้นทางจนกว่าคุณจะกำหนดค่าปลายทาง SOCKS5 ของ local-core", "NO_USABLE_NODES": "การสมัครสมาชิกไม่พบโหนดที่ใช้งานได้ (http/https/socks5 หรือโหนดที่มีปลายทาง local core)", - "SELECTOR_SWITCH_FAILED": "การเรียกควบคุมพร็อกซีคอร์ล้มเหลว เส้นทางเดิมยังคงใช้งานได้ โปรดตรวจสอบ URL และซีเคร็ตควบคุมแล้วรีเฟรช" + "SELECTOR_SWITCH_FAILED": "การเรียกควบคุมพร็อกซีคอร์ล้มเหลว เส้นทางเดิมยังคงใช้งานได้ โปรดตรวจสอบ URL และซีเคร็ตควบคุมแล้วรีเฟรช", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "วางลิงก์การสมัครพร็อกซีของคุณ เมื่อเปิดใช้งานแล้ว การรับส่งข้อมูลจะถูกส่งผ่านพร็อกซีพูลในโหมดส่วนกลางหรือโหมดกฎ (ผู้ให้บริการที่ระบุ) โหนดการสมัครใช้งานจะซิงค์กับพร็อกซีพูลโดยอัตโนมัติ และนำกลไกการโพล การตรวจสอบสภาพ และการป้องกันการรั่วไหลที่มีอยู่กลับมาใช้ใหม่", "addSubscription": "เพิ่มการสมัครสมาชิก", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ยังไม่มีการสลับที่บันทึกไว้", "lastSwitchMember": "สมาชิกที่ให้บริการ", "nextSwitch": "การสลับครั้งถัดไปที่ทำได้", - "nextSwitchNow": "ตอนนี้" + "nextSwitchNow": "ตอนนี้", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "ตรวจสอบสถานะพร้อมกันทั้งหมด", "bulkHealthcheckDesc": "ทดสอบพร็อกซีที่กำหนดค่าทั้งหมดกับ URL เป้าหมาย เพื่อค้นหาพร็อกซีที่ใช้งานได้", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index aedcc059e9ad..bd8e4a4114ee 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Yerel proxy çekirdeği uç noktası geçersiz; SS/VMess/Trojan/VLESS düğümleri yoksayıldı.", "NEEDS_CORE_NOT_CONFIGURED": "Bu abonelikte yerel bir proxy çekirdeğine (SS/VMess/Trojan/VLESS) ihtiyaç duyan düğümler var; yerel çekirdek SOCKS5 uç noktasını yapılandırana kadar bunlar yönlendirilmez.", "NO_USABLE_NODES": "Abonelik kullanılabilir hiçbir düğüm sağlamadı (http/https/socks5 veya yerel çekirdek uç noktasına sahip düğümler).", - "SELECTOR_SWITCH_FAILED": "Proxy-core denetim çağrısı başarısız oldu; önceki rota etkin kalır. Denetim URL'sini ve parolasını denetleyip yenileyin." + "SELECTOR_SWITCH_FAILED": "Proxy-core denetim çağrısı başarısız oldu; önceki rota etkin kalır. Denetim URL'sini ve parolasını denetleyip yenileyin.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Proxy abonelik bağlantınızı yapıştırın. Etkinleştirildiğinde trafik, genel veya kural (belirtilen Sağlayıcı) modunda proxy havuzu üzerinden yönlendirilir. Abonelik düğümleri otomatik olarak proxy havuzuyla senkronize edilir ve mevcut yoklama, durum kontrolü ve sızıntı önleme mekanizmalarını yeniden kullanır.", "addSubscription": "Abonelik Ekle", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Henüz kayıtlı geçiş yok", "lastSwitchMember": "Hizmet veren üye", "nextSwitch": "Sonraki olası geçiş", - "nextSwitchNow": "Şimdi" + "nextSwitchNow": "Şimdi", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Toplu Durum Denetimi", "bulkHealthcheckDesc": "Hangilerinin çalıştığını belirlemek için yapılandırılmış tüm proxy'leri bir hedef URL ile test edin.", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 554fde54c362..d9bb55994306 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Кінцева точка локального ядра проксі недійсна; вузли SS/VMess/Trojan/VLESS було проігноровано.", "NEEDS_CORE_NOT_CONFIGURED": "Ця підписка містить вузли, які потребують локального ядра проксі (SS/VMess/Trojan/VLESS); вони не маршрутизуються, доки ви не налаштуєте кінцеву точку SOCKS5 локального ядра.", "NO_USABLE_NODES": "Підписка не повернула придатних для використання вузлів (http/https/socks5 або вузлів із кінцевою точкою локального ядра).", - "SELECTOR_SWITCH_FAILED": "Керівний виклик проксі-ядра не вдався; попередній маршрут лишається активним. Перевірте URL і секрет керування, потім оновіть." + "SELECTOR_SWITCH_FAILED": "Керівний виклик проксі-ядра не вдався; попередній маршрут лишається активним. Перевірте URL і секрет керування, потім оновіть.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Вставте посилання на підписку на проксі. Після ввімкнення трафік направляється через пул проксі-серверів у глобальному режимі або режимі правила (вказаного постачальника). Вузли підписки автоматично синхронізуються з проксі-пулом і повторно використовують існуючі механізми опитування, перевірки працездатності та запобігання витокам.", "addSubscription": "Додати підписку", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Перемикань поки не записано", "lastSwitchMember": "Учасник, що обслуговує", "nextSwitch": "Наступне можливе перемикання", - "nextSwitchNow": "Зараз" + "nextSwitchNow": "Зараз", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Масова перевірка стану", "bulkHealthcheckDesc": "Надішліть тестові запити через усі налаштовані проксі-сервери до цільової URL-адреси, щоб з’ясувати, які з них працюють.", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 547c7a25f3c9..dd5f5b498305 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "لوکل proxy-core اینڈ پوائنٹ غلط ہے؛ SS/VMess/Trojan/VLESS نوڈز کو نظر انداز کر دیا گیا۔", "NEEDS_CORE_NOT_CONFIGURED": "اس سبسکرپشن میں ایسے نوڈز ہیں جنہیں لوکل پراکسی کور (SS/VMess/Trojan/VLESS) کی ضرورت ہے؛ جب تک آپ local-core SOCKS5 اینڈ پوائنٹ کنفیگر نہیں کرتے، انہیں روٹ نہیں کیا جائے گا۔", "NO_USABLE_NODES": "سبسکرپشن سے کوئی قابل استعمال نوڈز حاصل نہیں ہوئے (http/https/socks5 یا لوکل کور اینڈ پوائنٹ والے نوڈز)۔", - "SELECTOR_SWITCH_FAILED": "پروکسی-کور کنٹرول کال ناکام رہی؛ پچھلا راستہ فعال رہتا ہے۔ کنٹرول URL اور سیکرٹ چیک کریں، پھر ریفریش کریں۔" + "SELECTOR_SWITCH_FAILED": "پروکسی-کور کنٹرول کال ناکام رہی؛ پچھلا راستہ فعال رہتا ہے۔ کنٹرول URL اور سیکرٹ چیک کریں، پھر ریفریش کریں۔", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "اپنا پراکسی سبسکرپشن لنک چسپاں کریں۔ ایک بار فعال ہونے کے بعد، ٹریفک کو پراکسی پول کے ذریعے عالمی یا اصول (مخصوص فراہم کنندہ) موڈ میں روٹ کیا جاتا ہے۔ سبسکرپشن نوڈس خود بخود پراکسی پول میں مطابقت پذیر ہو جاتے ہیں اور موجودہ پولنگ، ہیلتھ چیک، اور اینٹی لیک میکانزم کو دوبارہ استعمال کرتے ہیں۔", "addSubscription": "سبسکرپشن شامل کریں۔", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "ابھی تک کوئی تبدیلی درج نہیں", "lastSwitchMember": "خدمت انجام دینے والا رکن", "nextSwitch": "اگلی ممکنہ تبدیلی", - "nextSwitchNow": "ابھی" + "nextSwitchNow": "ابھی", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "اجتماعی ہیلتھ چیک", "bulkHealthcheckDesc": "تمام ترتیب شدہ پراکسیز کو ہدف URL پر جانچیں تاکہ معلوم ہو سکے کہ کون سی کام کرتی ہیں۔", diff --git a/src/i18n/messages/uz.json b/src/i18n/messages/uz.json index 97b87a20f1e0..908f4dad5344 100644 --- a/src/i18n/messages/uz.json +++ b/src/i18n/messages/uz.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Mahalliy proxy-core endpoint noto‘g‘ri; SS/VMess/Trojan/VLESS tugunlari e’tiborga olinmadi.", "NEEDS_CORE_NOT_CONFIGURED": "Bu obunada mahalliy proksi yadrosini talab qiladigan tugunlar mavjud (SS/VMess/Trojan/VLESS); mahalliy yadro uchun SOCKS5 endpoint sozlanmaguncha ular orqali trafik yo‘naltirilmaydi.", "NO_USABLE_NODES": "Obunada foydalanish mumkin bo‘lgan tugunlar topilmadi (http/https/socks5 yoki mahalliy yadro endpointiga ega tugunlar).", - "SELECTOR_SWITCH_FAILED": "Proxy-core boshqaruv chaqiruvi bajarilmadi; avvalgi marshrut faol qoladi. Boshqaruv URL va sirrini tekshirib, yangilang." + "SELECTOR_SWITCH_FAILED": "Proxy-core boshqaruv chaqiruvi bajarilmadi; avvalgi marshrut faol qoladi. Boshqaruv URL va sirrini tekshirib, yangilang.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Proksi obunangiz havolasini kiriting. Yoqilgach, trafik global yoki qoida (belgilangan Provider) rejimida proksi havzasi orqali yo‘naltiriladi. Obuna tugunlari avtomatik ravishda proksi havzasiga sinxronlanadi hamda mavjud davriy so‘rov, ishlash holatini tekshirish va sizib chiqishga qarshi mexanizmlardan foydalanadi.", "addSubscription": "Obuna qo‘shish", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Hali qayd etilgan o'tish yo'q", "lastSwitchMember": "Xizmat ko'rsatuvchi a'zo", "nextSwitch": "Keyingi mumkin bo'lgan o'tish", - "nextSwitchNow": "Hozir" + "nextSwitchNow": "Hozir", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Holatni ommaviy tekshirish", "bulkHealthcheckDesc": "Ishlaydiganlarini aniqlash uchun barcha sozlangan proksilarni maqsadli URL orqali sinab koʻring.", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index c98e2e6f9469..60b49ea8a23f 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Điểm cuối proxy-core cục bộ không hợp lệ; các nút SS/VMess/Trojan/VLESS đã bị bỏ qua.", "NEEDS_CORE_NOT_CONFIGURED": "Gói đăng ký này có các nút cần một proxy core cục bộ (SS/VMess/Trojan/VLESS); chúng sẽ không được định tuyến cho đến khi bạn cấu hình điểm cuối SOCKS5 của core cục bộ.", "NO_USABLE_NODES": "Gói đăng ký không tạo ra nút nào có thể sử dụng (http/https/socks5 hoặc các nút có điểm cuối core cục bộ).", - "SELECTOR_SWITCH_FAILED": "Lệnh gọi điều khiển proxy-core thất bại; tuyến hiện tại vẫn được giữ. Kiểm tra URL và secret điều khiển, rồi làm mới." + "SELECTOR_SWITCH_FAILED": "Lệnh gọi điều khiển proxy-core thất bại; tuyến hiện tại vẫn được giữ. Kiểm tra URL và secret điều khiển, rồi làm mới.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Dán liên kết đăng ký proxy của bạn. Sau khi được bật, lưu lượng truy cập sẽ được định tuyến qua nhóm proxy ở chế độ chung hoặc quy tắc (Nhà cung cấp được chỉ định). Các nút đăng ký được tự động đồng bộ hóa vào nhóm proxy và sử dụng lại các cơ chế bỏ phiếu, kiểm tra tình trạng và chống rò rỉ hiện có.", "addSubscription": "Thêm đăng ký", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "Chưa ghi nhận lần chuyển nào", "lastSwitchMember": "Thành viên đang phục vụ", "nextSwitch": "Lần chuyển kế tiếp có thể", - "nextSwitchNow": "Ngay bây giờ" + "nextSwitchNow": "Ngay bây giờ", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Kiểm tra tình trạng hàng loạt", "bulkHealthcheckDesc": "Kiểm tra tất cả proxy đã cấu hình với một URL đích để xác định proxy nào hoạt động.", diff --git a/src/i18n/messages/yo.json b/src/i18n/messages/yo.json index ed0a4b80b2da..3d80ab4d2318 100644 --- a/src/i18n/messages/yo.json +++ b/src/i18n/messages/yo.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "Ojú-ọ̀nà local proxy-core kò tọ́; a foju kọ àwọn node SS/VMess/Trojan/VLESS.", "NEEDS_CORE_NOT_CONFIGURED": "Ìforúkọsílẹ̀ yìí ní àwọn node tó nílò local proxy core (SS/VMess/Trojan/VLESS); a kò ní darí wọn títí tí o fi ṣètò ojú-ọ̀nà SOCKS5 local-core.", "NO_USABLE_NODES": "Ìforúkọsílẹ̀ kò mú node tó ṣeé lò kankan jáde (http/https/socks5 tàbí àwọn node tó ní ojú-ọ̀nà local core).", - "SELECTOR_SWITCH_FAILED": "Ìpè ìṣàkóso proxy-core kùnà; ọ̀nà àtijọ́ wà ni íṣiṣẹ́. Ṣàyẹ̀wò URL àti àṣírí ìṣàkóso, lẹ́yìn náà ṣe àtúnsọ." + "SELECTOR_SWITCH_FAILED": "Ìpè ìṣàkóso proxy-core kùnà; ọ̀nà àtijọ́ wà ni íṣiṣẹ́. Ṣàyẹ̀wò URL àti àṣírí ìṣàkóso, lẹ́yìn náà ṣe àtúnsọ.", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "Lẹ̀ ìjápọ̀ ìforúkọsílẹ̀ aṣojú rẹ síbí. Nígbà tí o bá mú un ṣiṣẹ́, a máa darí ìrìn-àjò dátà gba inú àkójọpọ̀ aṣojú ní ipò àgbáyé tàbí ipò òfin (Olùpèsè tí a yàn). A máa mú àwọn node ìforúkọsílẹ̀ ṣiṣẹ́pọ̀ láìfọwọ́yí sínú àkójọpọ̀ aṣojú, wọ́n á sì tún lo àwọn ọ̀nà ìṣàyẹ̀wò déédéé, àyẹ̀wò ìlera, àti ìdènà ìjò dátà tó ti wà.", "addSubscription": "Ṣàfikún Ìforúkọsílẹ̀", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "A kò tíì ṣe àkọsílẹ̀ ìyípadà kankan", "lastSwitchMember": "Ọmọ ẹgbẹ́ tó ń sin", "nextSwitch": "Ìyípadà tó ṣeéṣe tó kàn", - "nextSwitchNow": "Nísinsìnyí" + "nextSwitchNow": "Nísinsìnyí", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "Ìṣàyẹ̀wò ìlera ọ̀pọ̀lọpọ̀", "bulkHealthcheckDesc": "Ṣàyẹ̀wò gbogbo àwọn aṣojú tí a ti ṣètò pẹ̀lú URL àfojúsùn kan láti mọ àwọn tó ń ṣiṣẹ́.", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 37b00bcf2d6d..a2b51976ae47 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "本地代理核心端点无效;已忽略 SS/VMess/Trojan/VLESS 节点。", "NEEDS_CORE_NOT_CONFIGURED": "此订阅包含需要本地代理核心 (SS/VMess/Trojan/VLESS) 的节点;在配置本地核心 SOCKS5 端点之前,它们不会被路由。", "NO_USABLE_NODES": "订阅未产生可用节点 (http/https/socks5 或具有本地核心端点的节点)。", - "SELECTOR_SWITCH_FAILED": "代理核心控制调用失败;上一条路由保持有效。请检查控制 URL 和密钥,然后刷新。" + "SELECTOR_SWITCH_FAILED": "代理核心控制调用失败;上一条路由保持有效。请检查控制 URL 和密钥,然后刷新。", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "粘贴你的代理订阅链接,开启后即可按全局或规则(指定 Provider)模式走代理。订阅节点会自动同步进代理池,并复用既有的轮询、健康检查与防泄漏机制。", "addSubscription": "新增订阅", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "尚未记录切换", "lastSwitchMember": "服务中的成员", "nextSwitch": "下次可切换", - "nextSwitchNow": "现在" + "nextSwitchNow": "现在", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "批量健康检查", "bulkHealthcheckDesc": "针对目标 URL 测试所有已配置代理,找出可用代理。", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 2f9d317d8960..5a6b47e2b72a 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -6873,7 +6873,8 @@ "LOCAL_CORE_ENDPOINT_INVALID": "本機 proxy-core 端點無效;已忽略 SS/VMess/Trojan/VLESS 節點。", "NEEDS_CORE_NOT_CONFIGURED": "此訂閱包含需要本機 proxy core 的節點(SS/VMess/Trojan/VLESS);在設定本機 core SOCKS5 端點之前,這些節點不會被路由。", "NO_USABLE_NODES": "訂閱未產生可用節點(http/https/socks5 或具有本機 core 端點的節點)。", - "SELECTOR_SWITCH_FAILED": "代理核心控制呼叫失敗;上一條路由保持有效。請檢查控制 URL 和密鑰,然後重新整理。" + "SELECTOR_SWITCH_FAILED": "代理核心控制呼叫失敗;上一條路由保持有效。請檢查控制 URL 和密鑰,然後重新整理。", + "CORE_CONFIG_NOT_GENERATED": "The local-core config was not generated; the adopted file is unchanged. Check the path and refresh." }, "description": "貼上您的代理訂閱連結。啟用後,流量將透過代理程式池以全域或規則(指定提供者)模式路由。訂閱節點會自動同步到代理程式池中,並重複使用現有的輪詢、健康檢查和防洩漏機制。", "addSubscription": "新增訂閱", @@ -6959,7 +6960,17 @@ "lastSwitchNever": "尚未記錄切換", "lastSwitchMember": "服務中的成員", "nextSwitch": "下次可切換", - "nextSwitchNow": "現在" + "nextSwitchNow": "現在", + "coreConfigPath": "Core config file", + "coreConfigPathPlaceholder": "/var/lib/omniroute/sing-box/config.json", + "coreConfigPathDesc": "Absolute .json path (lowercase extension). Empty = no generation. Each sync writes ''.generated beside the adopted file, never into it.", + "lastSwitchKind": "Switch reason", + "switchKind": { + "ip_quota_429": "quota", + "proxy_unreachable": "unreachable", + "transport": "transport", + "slow": "slow" + } }, "bulkHealthcheck": "批次健康檢查", "bulkHealthcheckDesc": "針對目標 URL 測試所有已設定代理,找出可用代理。", diff --git a/src/lib/db/migrations/197_proxy_subscription_core_config.sql b/src/lib/db/migrations/197_proxy_subscription_core_config.sql new file mode 100644 index 000000000000..875059d698c2 --- /dev/null +++ b/src/lib/db/migrations/197_proxy_subscription_core_config.sql @@ -0,0 +1,10 @@ +-- 197_proxy_subscription_core_config.sql +-- Opt-in core-config path for proxy subscriptions: when set, each sync +-- renders the local-core configuration beside the adopted file +-- (`.generated`). Purely additive: no backfill, no index, existing +-- rows read NULL (no generation, current behavior unchanged). +-- The switch-kind column records why the last selector switch fired +-- (same additive posture: NULL means unknown, current rows unaffected). + +ALTER TABLE proxy_subscriptions ADD COLUMN core_config_path TEXT; +ALTER TABLE proxy_subscriptions ADD COLUMN selector_last_switch_kind TEXT; diff --git a/src/lib/proxySubscription/coreConfig/model.ts b/src/lib/proxySubscription/coreConfig/model.ts new file mode 100644 index 000000000000..d0091fcdcef5 --- /dev/null +++ b/src/lib/proxySubscription/coreConfig/model.ts @@ -0,0 +1,304 @@ +/** + * Core-agnostic generation model: what OmniRoute owns (local listeners, + * selector groups, node tags) — never how a protocol is translated. + * + * It doesn't model outbound protocol payloads: each node carries its + * original definition untouched (`source`), and a renderer passes it through + * verbatim or skips it with a reason. + */ +import { createHash } from "node:crypto"; +import type { NeedsCoreNode, NodeSource, SubscriptionNode } from "../parse"; +import { isLocalCoreEndpointAllowed } from "../coreEndpoint"; +import { parseSelectorTag, stripSelectorSuffix } from "../selectorEndpoint"; + +/** Prefix marking every tag this generator owns (listeners, groups, nodes). */ +export const OWNED_TAG_PREFIX = "omniroute-"; + +export interface CoreListener { + tag: string; + scheme: "socks5" | "http" | "https"; + host: string; + port: number; + group: string; +} + +export interface CoreGroup { + tag: string; + members: string[]; +} + +export interface CoreNode { + tag: string; + source: NodeSource; +} + +export interface CoreModel { + listeners: CoreListener[]; + groups: CoreGroup[]; + nodes: CoreNode[]; + skipped: Array<{ line?: string; node?: string; reason: string }>; +} + +function hashSlotFallback(name: string, slots: number): number { + const digest = createHash("sha256").update(name, "utf8").digest(); + return digest.readUInt32BE(0) % slots; +} + +function displayLabel(name: unknown): string { + return (typeof name === "string" ? name.normalize("NFC") : "") || "node"; +} + +const IDENTITY_SUFFIX_LENGTH = 6; + +function firstPresent(value: unknown): string { + return typeof value === "string" && value ? value : ""; +} + +function canonicalFields( + type: string, + server: string, + port: number, + identity: string, + transport: string, + security: string +): string { + return [type, server, String(port), identity, transport, security].join("\x1f"); +} + +function numericField(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : 0; +} + +function objectPort(value: Record): number { + return numericField(value.port) || numericField(value.server_port); +} + +function objectCredential(value: Record): string { + return ( + firstPresent(value.uuid) || + firstPresent(value.password) || + firstPresent(value.token) || + firstPresent(value.username) + ); +} + +function identityFromObject(value: Record): string | null { + const type = typeof value.type === "string" ? value.type.toLowerCase().trim() : ""; + const serverRaw = typeof value.server === "string" ? value.server : ""; + const server = serverRaw.trim().toLowerCase().normalize("NFC"); + const port = objectPort(value); + const identity = objectCredential(value); + const transport = (firstPresent(value.transport) || firstPresent(value.network)).toLowerCase(); + const security = (firstPresent(value.security) || firstPresent(value.tls)).toLowerCase(); + if (!type && !server && port === 0 && !identity) return null; + return createHash("sha256") + .update(canonicalFields(type, server, port, identity, transport, security), "utf8") + .digest("hex"); +} + +function safeDecode(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function uriCredential(url: URL): string { + if (url.username) return safeDecode(url.username); + if (url.password) return safeDecode(url.password); + if (url.hash) return safeDecode(url.hash.replace(/^#/, "")); + return ""; +} + +function identityFromUri(raw: string): string | null { + let url: URL; + try { + url = new URL(raw); + } catch { + return null; + } + const type = url.protocol.replace(/:$/, "").toLowerCase(); + const server = url.hostname.trim().toLowerCase().normalize("NFC"); + const port = url.port ? Number(url.port) || 0 : 0; + const identity = uriCredential(url); + const transport = (url.searchParams.get("type") ?? "").toLowerCase(); + const security = (url.searchParams.get("security") ?? "").toLowerCase(); + if (!type && !server && port === 0 && !identity) return null; + return createHash("sha256") + .update(canonicalFields(type, server, port, identity, transport, security), "utf8") + .digest("hex"); +} + +function nodeIdentity(source: NodeSource): string | null { + if (!source) return null; + if (source.kind === "object") { + if (!source.value || typeof source.value !== "object") return null; + return identityFromObject(source.value as Record); + } + if (source.kind === "uri") { + if (typeof source.value !== "string") return null; + return identityFromUri(source.value); + } + return null; +} + +function rendezvousGroup(identity: string, groups: string[]): string { + let best = groups[0]; + let bestScore = ""; + for (const group of groups) { + const score = createHash("sha256").update(`${identity}\x1f${group}`, "utf8").digest("hex"); + if (score > bestScore) { + bestScore = score; + best = group; + } + } + return best; +} + +function uniqueTag(base: string, taken: Set): string { + if (!taken.has(base)) { + taken.add(base); + return base; + } + let i = 2; + while (taken.has(`${base}-${i}`)) i += 1; + const tag = `${base}-${i}`; + taken.add(tag); + return tag; +} + +function listenerPort(url: URL, scheme: "socks5" | "http" | "https"): number { + const port = Number(url.port); + if (port > 0) return port; + return scheme === "https" ? 443 : scheme === "http" ? 80 : 1080; +} + +function listenerFromLine( + line: string, + index: number, + skipped: CoreModel["skipped"] +): CoreListener | null { + const tag = parseSelectorTag(line); + if (!tag) { + skipped.push({ line, reason: "no_selector_tag" }); + return null; + } + const bare = stripSelectorSuffix(line); + if (!isLocalCoreEndpointAllowed(bare)) { + skipped.push({ line, reason: "invalid_endpoint" }); + return null; + } + let url: URL; + try { + url = new URL(bare); + } catch { + skipped.push({ line, reason: "invalid_endpoint" }); + return null; + } + const scheme = url.protocol === "https:" ? "https" : url.protocol === "http:" ? "http" : "socks5"; + const group = `${OWNED_TAG_PREFIX}${tag}`; + return { + tag: `${OWNED_TAG_PREFIX}in-${tag}-${index}`, + scheme, + host: url.hostname, + port: listenerPort(url, scheme), + group, + }; +} + +function assignNodes( + nodes: Array, + groups: string[], + groupMembers: Map, + skipped: CoreModel["skipped"] +): CoreNode[] { + const taken = new Set(); + const coreNodes: CoreNode[] = []; + for (const node of nodes) { + const source = node.source; + if (!source) { + skipped.push({ node: node.name, reason: "no_source" }); + continue; + } + const identity = nodeIdentity(source); + const display = displayLabel(node.name); + let tag: string; + if (identity) { + const suffixed = `${display}-${identity.slice(0, IDENTITY_SUFFIX_LENGTH)}`; + if (!taken.has(suffixed)) { + taken.add(suffixed); + tag = suffixed; + } else { + tag = uniqueTag(suffixed, taken); + } + } else { + tag = uniqueTag(display, taken); + } + coreNodes.push({ tag, source }); + const group = identity + ? rendezvousGroup(identity, groups) + : groups[hashSlotFallback(display, groups.length)]; + groupMembers.get(group)!.push({ tag, source }); + } + return coreNodes; +} + +/** + * Build the generation model from local-core lines and parsed nodes. + * + * `entries` are the already-split `localCoreEndpoint` lines (one per line). + * Lines without a `selector=` suffix or with a disallowed endpoint land in + * `skipped`; every other line becomes one listener. Lines sharing a selector + * tag form one group. Each node with a `source` joins exactly one group by + * rendezvous hashing on its definition identity, so removing a group only + * moves that group's members and adding a node never moves the others. + * Without any group the model stays empty: nodes are neither rendered nor + * listed, and the renderer emits nothing owned. + */ +export function buildCoreModel( + entries: string[], + nodes: Array +): CoreModel { + const skipped: CoreModel["skipped"] = []; + const listeners: CoreListener[] = []; + const groupTags = new Map(); + const groupMembers = new Map(); + + entries.forEach((raw, index) => { + const line = typeof raw === "string" ? raw.trim() : ""; + if (!line) return; + const listener = listenerFromLine(line, index, skipped); + if (!listener) return; + // Key on the bare selector tag: listener.group carries the owned prefix, + // so testing the prefixed form here would never hit and reset the member + // list on every line sharing a selector. + if (!groupTags.has(parseSelectorTag(line)!)) { + groupTags.set(parseSelectorTag(line)!, listener.group); + groupMembers.set(listener.group, []); + } + listeners.push(listener); + }); + + const groups = [...groupTags.entries()] + .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) + .map(([, tag]) => tag); + if (groups.length === 0) return { listeners: [], groups: [], nodes: [], skipped }; + + const coreNodes = assignNodes(nodes, groups, groupMembers, skipped); + for (const tag of groups) { + if ((groupMembers.get(tag) ?? []).length === 0) { + skipped.push({ node: tag, reason: "empty_group" }); + } + } + + return { + listeners, + groups: groups.map((tag) => ({ + tag, + members: (groupMembers.get(tag) ?? []).map((n) => n.tag), + })), + nodes: coreNodes, + skipped, + }; +} diff --git a/src/lib/proxySubscription/coreConfig/pathGuard.ts b/src/lib/proxySubscription/coreConfig/pathGuard.ts new file mode 100644 index 000000000000..613e222e0cf7 --- /dev/null +++ b/src/lib/proxySubscription/coreConfig/pathGuard.ts @@ -0,0 +1,38 @@ +/** + * Guard for the subscription's adopted core-config path (opt-in generation). + * + * Pure and dependency-free: no disk access here. The sync step pre-checks + * the target directory separately (best-effort) and never creates it. + */ +import path from "node:path"; + +export type CoreConfigPathReason = + "empty" | "too_long" | "nul_byte" | "not_absolute" | "dotdot_segment" | "bad_extension"; + +export interface CoreConfigPathVerdict { + allowed: boolean; + reason?: CoreConfigPathReason; +} + +const MAX_PATH_LENGTH = 1024; + +/** + * Whether `p` is an acceptable adopted core-config path: non-empty, + * at most 1024 chars, no NUL byte, absolute, no `..` segment, `.json` + * extension (case-sensitive). Never throws. + */ +export function isCoreConfigPathAllowed(p: string): CoreConfigPathVerdict { + try { + if (typeof p !== "string" || p.length === 0) return { allowed: false, reason: "empty" }; + if (p.length > MAX_PATH_LENGTH) return { allowed: false, reason: "too_long" }; + if (p.includes("\0")) return { allowed: false, reason: "nul_byte" }; + if (!path.isAbsolute(p)) return { allowed: false, reason: "not_absolute" }; + if (p.split("/").includes("..") || p.split(path.sep).includes("..")) { + return { allowed: false, reason: "dotdot_segment" }; + } + if (!p.endsWith(".json")) return { allowed: false, reason: "bad_extension" }; + return { allowed: true }; + } catch { + return { allowed: false, reason: "empty" }; + } +} diff --git a/src/lib/proxySubscription/coreConfig/renderers.ts b/src/lib/proxySubscription/coreConfig/renderers.ts new file mode 100644 index 000000000000..3a1a82fe5729 --- /dev/null +++ b/src/lib/proxySubscription/coreConfig/renderers.ts @@ -0,0 +1,31 @@ +/** + * Registry of local-core config renderers, one entry per core. + * + * A second core only adds one file plus one table entry; callers resolve the + * renderer through `RENDERERS[DEFAULT_CORE]` at runtime. + */ +import type { CoreModel } from "./model"; +import { renderSingBox } from "./singbox"; + +export type RenderOk = { + ok: true; + text: string; + unchanged: boolean; + skipped: Array<{ node: string; reason: string }>; +}; + +export type RenderRefused = { + ok: false; + reason: "unparseable" | "no_ownable_section" | "not_an_object"; +}; + +export type RenderResult = RenderOk | RenderRefused; + +export type CoreRenderer = (model: CoreModel, existingText: string | null) => RenderResult; + +export const RENDERERS: Record = { + "sing-box": renderSingBox, +}; + +/** Core rendered when a subscription doesn't name one (only entry so far). */ +export const DEFAULT_CORE = "sing-box"; diff --git a/src/lib/proxySubscription/coreConfig/singbox.ts b/src/lib/proxySubscription/coreConfig/singbox.ts new file mode 100644 index 000000000000..a5d81393e4c9 --- /dev/null +++ b/src/lib/proxySubscription/coreConfig/singbox.ts @@ -0,0 +1,176 @@ +/** + * sing-box renderer: merges the generation model into owned sections only. + * + * Owned = any `inbounds` / `outbounds` entry (or `route.rules` entry) whose + * tag (or rule `inbound[0]`) starts with the shared owned-tag prefix. Owned + * entries are removed then replaced; everything else (dns, log, + * experimental/clash_api, user rules, user outbounds, unknown keys) is kept + * verbatim, key order included. An adopted file without an `inbounds` or + * `outbounds` array is refused (`no_ownable_section`), never rewritten. + * + * Skipped (reported, never converted): a URI/Clash to sing-box outbound + * converter doesn't exist here — nodes whose source isn't sing-box-shaped + * are listed with `source_not_singbox_shape`. When the feed offers a variant + * already shaped for this core, point the subscription at it instead. + */ +import { OWNED_TAG_PREFIX, type CoreListener, type CoreModel } from "./model"; +import type { RenderRefused, RenderResult } from "./renderers"; + +export function isSingBoxShape(value: unknown): value is Record { + if (!value || typeof value !== "object") return false; + const v = value as Record; + return ( + typeof v.type === "string" && typeof v.server === "string" && typeof v.server_port === "number" + ); +} + +function inboundFor(listener: CoreListener): Record { + return { + type: listener.scheme === "socks5" ? "socks" : "http", + tag: listener.tag, + listen: listener.host, + listen_port: listener.port, + }; +} + +function isOwnedTag(tag: unknown): boolean { + return typeof tag === "string" && tag.startsWith(OWNED_TAG_PREFIX); +} + +function withoutOwned(items: T[], tagOf: (item: T) => unknown): T[] { + return items.filter((item) => !isOwnedTag(tagOf(item))); +} + +function ruleTargetsOwned(rule: unknown): boolean { + if (!rule || typeof rule !== "object") return false; + const inbound = (rule as Record).inbound; + return Array.isArray(inbound) && isOwnedTag(inbound[0]); +} + +function parseBase(existingText: string | null): Record | RenderResult { + if (existingText === null) return { inbounds: [], outbounds: [] }; + let parsed: unknown; + try { + parsed = JSON.parse(existingText); + } catch { + return { ok: false, reason: "unparseable" }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { ok: false, reason: "not_an_object" }; + } + const base = parsed as Record; + if (!Array.isArray(base.inbounds) && !Array.isArray(base.outbounds)) { + return { ok: false, reason: "no_ownable_section" }; + } + return base; +} + +function renderedTagSet(model: CoreModel): Set { + const tags = new Set(); + for (const node of model.nodes) { + if (node.source.kind === "object" && isSingBoxShape(node.source.value)) { + tags.add(node.tag); + } + } + return tags; +} + +function selectorOutbounds( + model: CoreModel, + renderedTags: Set, + keptOutbounds: Array> +): string[] { + const emittedGroups = model.groups.filter((g) => g.members.some((m) => renderedTags.has(m))); + const emittedTags = new Set(emittedGroups.map((g) => g.tag)); + for (const node of model.nodes) { + if (!renderedTags.has(node.tag)) continue; + const value = (node.source as { kind: "object"; value: Record }).value; + keptOutbounds.push({ ...value, tag: `${OWNED_TAG_PREFIX}${node.tag}` }); + } + for (const group of emittedGroups) { + const members = group.members + .filter((m) => renderedTags.has(m)) + .map((m) => `${OWNED_TAG_PREFIX}${m}`); + keptOutbounds.push({ + type: "selector", + tag: group.tag, + outbounds: members, + default: members[0], + }); + } + return [...emittedTags]; +} + +function mergedRoute( + base: Record, + listeners: CoreListener[], + emittedGroupTags: Set +): Record | null { + const routeRaw = base.route; + const route: Record = + routeRaw && typeof routeRaw === "object" && !Array.isArray(routeRaw) + ? { ...(routeRaw as Record) } + : {}; + const existingRules = Array.isArray(route.rules) ? (route.rules as unknown[]) : []; + const keptRules = existingRules.filter((r) => !ruleTargetsOwned(r)); + const newRules = listeners + .filter((l) => emittedGroupTags.has(l.group)) + .map((l) => ({ inbound: [l.tag], outbound: l.group })); + route.rules = [...newRules, ...keptRules]; + if (newRules.length === 0 && base.route === undefined) return null; + return route; +} + +function isRefused(value: Record | RenderResult): value is RenderRefused { + return "ok" in value && value.ok === false; +} + +export function renderSingBox(model: CoreModel, existingText: string | null): RenderResult { + const baseOrRefused = parseBase(existingText); + if (isRefused(baseOrRefused)) return baseOrRefused; + const base: Record = baseOrRefused; + + const renderedTags = renderedTagSet(model); + const out: Record = { ...base }; + const inbounds = (Array.isArray(base.inbounds) ? base.inbounds : []).slice() as Array< + Record + >; + const outbounds = (Array.isArray(base.outbounds) ? base.outbounds : []).slice() as Array< + Record + >; + + const keptInbounds = withoutOwned(inbounds, (i) => (i as Record).tag); + const keptOutbounds = withoutOwned(outbounds, (o) => (o as Record).tag); + + const skipped: Array<{ node: string; reason: string }> = []; + for (const node of model.nodes) { + if (!renderedTags.has(node.tag)) + skipped.push({ node: node.tag, reason: "source_not_singbox_shape" }); + } + + const emittedTags = selectorOutbounds(model, renderedTags, keptOutbounds); + const emittedGroupTags = new Set(emittedTags); + + // Groups with members assigned but none renderable emit nothing (see + // selectorOutbounds above): report each once so an unemitted listener never + // goes silent. Groups already reported by the model layer (no members + // assigned at all) are excluded here — no double count at the sync merge. + for (const group of model.groups) { + if (group.members.length === 0) continue; + if (group.members.some((m) => renderedTags.has(m))) continue; + skipped.push({ node: group.tag, reason: "empty_group" }); + } + + for (const listener of model.listeners) { + if (!emittedGroupTags.has(listener.group)) continue; + keptInbounds.push(inboundFor(listener)); + } + + out.inbounds = keptInbounds; + out.outbounds = keptOutbounds; + const route = mergedRoute(base, model.listeners, emittedGroupTags); + if (route) out.route = route; + + const text = `${JSON.stringify(out, null, 2)}\n`; + return { ok: true, text, unchanged: text === existingText, skipped }; +} diff --git a/src/lib/proxySubscription/coreConfig/sync.ts b/src/lib/proxySubscription/coreConfig/sync.ts new file mode 100644 index 000000000000..d89ad81cd264 --- /dev/null +++ b/src/lib/proxySubscription/coreConfig/sync.ts @@ -0,0 +1,121 @@ +/** + * Sync-side generation: render the core config beside the adopted file. + * + * This is the only file of the generation feature that touches the disk. + * It reads the adopted file with `readPrivateConfigFile` (read-only here — + * the adopted file is never modified) and writes `.generated` with + * `writePrivateConfigFile` (atomic, symlink-refusing, mode 0600). + * + * Note: the writer creates a missing target directory internally, so the + * directory check below is best-effort only — it warns first without + * creating anything; a directory removed in between still gets created by + * the writer (the write itself stays atomic either way). + */ +import fs from "node:fs"; +import path from "node:path"; +import { readPrivateConfigFile, writePrivateConfigFile } from "@/lib/cli-helper/privateConfigFile"; +import { parseLocalCoreEndpoints } from "../coreEndpoint"; +import type { ParsedSubscription } from "../parse"; +import { buildCoreModel } from "./model"; +import { DEFAULT_CORE, RENDERERS, type RenderRefused } from "./renderers"; + +export interface CoreConfigSub { + coreConfigPath: string | null; + localCoreEndpoint: string | null; +} + +function logSkippedCounts( + model: { skipped: Array<{ reason: string }> }, + result: { skipped: Array<{ reason: string }> }, + label: string +): void { + const counts = new Map(); + for (const entry of [...model.skipped, ...result.skipped]) { + counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1); + } + if (counts.size === 0) return; + const summary = [...counts.entries()] + .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) + .map(([reason, count]) => `${reason}=${count}`) + .join(", "); + console.warn(`[ProxySubscription] core config skipped for ${label}: ${summary}`); +} + +/** Encode a sync warning without importing the service (cycle-free). */ +function encodeWarning(code: string, detail?: string): string { + return JSON.stringify(detail ? { code, detail } : { code }); +} + +function warn(reason: string): string { + return encodeWarning("CORE_CONFIG_NOT_GENERATED", reason); +} + +function renderAndLog( + sub: CoreConfigSub, + model: ReturnType, + existingText: string | null +): string | null | { text: string } { + const target = (sub.coreConfigPath ?? "").trim(); + const renderer = RENDERERS[DEFAULT_CORE]; + const result = renderer(model, existingText); + if (!result.ok) return warn((result as RenderRefused).reason); + if (result.unchanged) return null; + logSkippedCounts(model, result, (sub as { id?: string }).id ?? target); + return { text: result.text }; +} + +/** + * Render and write the beside-file for one subscription. Returns the encoded + * warning when nothing usable was written, null on success or when the + * feature is off (empty path). Never throws — sync must never fail because + * generation did. + */ +export async function generateForSubscription( + sub: CoreConfigSub, + parsed: ParsedSubscription +): Promise { + try { + const target = (sub.coreConfigPath ?? "").trim(); + if (!target) return null; + + const generatedPath = `${target}.generated`; + let existingText: string | null = null; + try { + existingText = readPrivateConfigFile(generatedPath); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") { + return warn("read_failed"); + } + try { + existingText = readPrivateConfigFile(target); + } catch (adoptedError) { + if ((adoptedError as NodeJS.ErrnoException)?.code !== "ENOENT") { + return warn("read_failed"); + } + } + } + + const model = buildCoreModel(parseLocalCoreEndpoints(sub.localCoreEndpoint), [ + ...parsed.nodes, + ...parsed.needsCore, + ]); + const result = renderAndLog(sub, model, existingText); + if (typeof result === "string") return result; + if (result === null) return null; + + const dir = path.dirname(generatedPath); + try { + if (!fs.statSync(dir).isDirectory()) return warn("write_failed"); + } catch { + return warn("write_failed"); + } + try { + writePrivateConfigFile(generatedPath, result.text); + } catch { + return warn("write_failed"); + } + return null; + } catch { + return warn("write_failed"); + } +} diff --git a/src/lib/proxySubscription/parse.ts b/src/lib/proxySubscription/parse.ts index 4dac7cf1e62b..6db25482e06d 100644 --- a/src/lib/proxySubscription/parse.ts +++ b/src/lib/proxySubscription/parse.ts @@ -46,6 +46,9 @@ const NEEDS_CORE_PROTOCOLS: ReadonlySet = new Set } | { kind: "uri"; value: string }; + export interface SubscriptionNode { name: string; type: DirectProxyType; @@ -54,6 +57,8 @@ export interface SubscriptionNode { username?: string; password?: string; rawProtocol: RawProxyProtocol; + /** Original node definition as supplied: memory-only, never persisted. */ + source?: NodeSource; } export interface NeedsCoreNode { @@ -62,6 +67,8 @@ export interface NeedsCoreNode { host?: string; port?: number; detail: string; + /** Original node definition as supplied: memory-only, never persisted. */ + source?: NodeSource; } export interface ParsedSubscription { @@ -121,6 +128,7 @@ function nodeFromClashObject( username: typeof obj.username === "string" && obj.username ? obj.username : undefined, password: typeof obj.password === "string" && obj.password ? obj.password : undefined, rawProtocol: type as RawProxyProtocol, + source: { kind: "object", value: obj }, }; } if (NEEDS_CORE_PROTOCOLS.has(type)) { @@ -130,6 +138,7 @@ function nodeFromClashObject( host, port, detail: `${type}://${host}:${port}`, + source: { kind: "object", value: obj }, }; } return null; @@ -170,6 +179,7 @@ function nodeFromUri(uri: string): SubscriptionNode | NeedsCoreNode | null { host, port, detail, + source: { kind: "uri", value: uri }, }; } @@ -187,6 +197,7 @@ function nodeFromUri(uri: string): SubscriptionNode | NeedsCoreNode | null { username: parsed.username ? decodeUserinfo(parsed.username) : undefined, password: parsed.password ? decodeUserinfo(parsed.password) : undefined, rawProtocol: scheme as RawProxyProtocol, + source: { kind: "uri", value: uri }, }; } @@ -200,6 +211,7 @@ function nodeFromUri(uri: string): SubscriptionNode | NeedsCoreNode | null { host, port, detail: `ss://${host}:${port}`, + source: { kind: "uri", value: uri }, }; } @@ -210,6 +222,7 @@ function nodeFromUri(uri: string): SubscriptionNode | NeedsCoreNode | null { host, port, detail: `${scheme}://${host}:${port}`, + source: { kind: "uri", value: uri }, }; } diff --git a/src/lib/proxySubscription/schema.ts b/src/lib/proxySubscription/schema.ts index 95933f5ddfa5..103faa457ba1 100644 --- a/src/lib/proxySubscription/schema.ts +++ b/src/lib/proxySubscription/schema.ts @@ -12,6 +12,7 @@ */ import { z } from "zod"; import type { ProxySubscriptionPayload } from "./subscriptionService"; +import { isCoreConfigPathAllowed } from "./coreConfig/pathGuard"; import { isSelectorControlUrlAllowed } from "./selectorGuard"; import { clampSelectorGapSeconds } from "./selectorTrigger"; @@ -53,6 +54,28 @@ function readGap(b: Record): number { return clampSelectorGapSeconds(b.selectorMinGapSeconds); } +function readCoreConfigPath(b: Record): string | null | undefined { + if (b.coreConfigPath === undefined) return undefined; + if (typeof b.coreConfigPath !== "string" || !b.coreConfigPath.trim()) return null; + return b.coreConfigPath.trim(); +} + +function checkCoreConfigPath( + coreConfigPath: string | null | undefined, + ctx: z.RefinementCtx +): string | null | undefined { + if (coreConfigPath === undefined || coreConfigPath === null) return coreConfigPath; + const verdict = isCoreConfigPathAllowed(coreConfigPath); + if (!verdict.allowed) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `coreConfigPath is not allowed (${verdict.reason})`, + }); + return z.NEVER; + } + return coreConfigPath; +} + function requireNameUrl( b: Record, ctx: z.RefinementCtx @@ -110,6 +133,8 @@ export const proxySubscriptionCreateSchema = z const controlUrl = checkControlUrl(readControlUrl(b), ctx); if (controlUrl === z.NEVER) return z.NEVER; + const coreConfigPath = checkCoreConfigPath(readCoreConfigPath(b), ctx); + if (coreConfigPath === z.NEVER) return z.NEVER; const controlSecret = readControlSecret(b); const selectorMinGapSeconds = readGap(b); @@ -122,6 +147,7 @@ export const proxySubscriptionCreateSchema = z updateIntervalMinutes, enabled, controlUrl: controlUrl ?? null, + coreConfigPath: coreConfigPath ?? null, controlSecret: controlSecret ?? null, selectorMinGapSeconds, }; @@ -167,6 +193,11 @@ export const proxySubscriptionUpdateSchema = z if (controlUrl === z.NEVER) return z.NEVER; payload.controlUrl = controlUrl ?? null; } + if (b.coreConfigPath !== undefined) { + const coreConfigPath = checkCoreConfigPath(readCoreConfigPath(b), ctx); + if (coreConfigPath === z.NEVER) return z.NEVER; + payload.coreConfigPath = coreConfigPath ?? null; + } return payload; }); diff --git a/src/lib/proxySubscription/selectorTrigger.ts b/src/lib/proxySubscription/selectorTrigger.ts index 4386b835fe78..f388706d98ff 100644 --- a/src/lib/proxySubscription/selectorTrigger.ts +++ b/src/lib/proxySubscription/selectorTrigger.ts @@ -38,6 +38,7 @@ import { isSelectorMemberAvoided, leastRecentlySetAside, noteProxyMemberRefusal, + snapshotProxySetAside, type ProxyRefusalKind, } from "@omniroute/open-sse/utils/proxyRefusalMemory.ts"; import { parseSelectorTag } from "./selectorEndpoint"; @@ -369,6 +370,7 @@ async function runSwitch( await recordSelectorSwitchOutcome({ subscriptionId: hit.subscriptionId, result: guarded.reason ?? "blocked", + kind, }); return { switched: false, reason: guarded.reason }; } @@ -381,6 +383,7 @@ async function runSwitch( await recordSelectorSwitchOutcome({ subscriptionId: hit.subscriptionId, result: "incomplete", + kind, }); return { switched: false, reason: "incomplete" }; } @@ -411,6 +414,7 @@ async function runSwitch( await recordSelectorSwitchOutcome({ subscriptionId: hit.subscriptionId, result: res.reason, + kind, }); return { switched: false, reason: res.reason }; } @@ -418,11 +422,24 @@ async function runSwitch( subscriptionId: hit.subscriptionId, result: "ok", member: res.target ?? null, + kind, }); return { switched: true, reason: "ok" }; } -/** Hand back a pre-reserved throttle slot after a failed switch attempt. */ +/** + * Refusal motive behind the current set-aside entry: the live decision reads + * the newest still-in-force (entry, kind) state, so the persisted switch kind + * always names the live motive, not a stale earlier write. Never throws. + */ +function setAsideKind(entryKey: string, now: number): ProxyRefusalKind | null { + try { + return snapshotProxySetAside(entryKey, now)?.kind ?? null; + } catch { + return null; + } +} + function restoreSlot(throttleKey: string, prev: number | undefined): void { if (prev === undefined) lastSwitch.delete(throttleKey); else lastSwitch.set(throttleKey, prev); @@ -452,7 +469,15 @@ export async function maybeSwitchOnSetAside( if (isThrottled(hit, now)) { return { switched: false, reason: "throttled" }; } - return runSwitch(hit, setAsideKey, now, opts?.kind ?? "ip_quota_429"); + // Propagated kind first (the caller that set the member aside knows why); + // live snapshot as fallback for the synchronous quota caller; hard default + // last so the column never stays unexplained. + return runSwitch( + hit, + setAsideKey, + now, + opts?.kind ?? setAsideKind(setAsideKey, now) ?? "ip_quota_429" + ); } catch { return { switched: false, reason: "network-error" }; } diff --git a/src/lib/proxySubscription/subscriptionService.ts b/src/lib/proxySubscription/subscriptionService.ts index 24877082f04a..723c3df55bfe 100644 --- a/src/lib/proxySubscription/subscriptionService.ts +++ b/src/lib/proxySubscription/subscriptionService.ts @@ -42,6 +42,7 @@ import { isProxyReachable } from "../proxyHealth"; import { resolveTargetScopes } from "./scopes"; import { clampSelectorGapSeconds, setAnyControlUrlConfigured } from "./selectorTrigger"; import { stripSelectorSuffix } from "./selectorEndpoint"; +import { generateForSubscription } from "./coreConfig/sync"; import { isSubscriptionFetchUrlAllowed, isIpLiteral, @@ -64,6 +65,7 @@ export type ProxySubscriptionStatus = "ok" | "error" | "empty"; * column (as JSON) so the dashboard can localize them via i18n instead of * showing server-side strings. */ export type ProxySubscriptionErrorCode = + | "CORE_CONFIG_NOT_GENERATED" | "LOCAL_CORE_ENDPOINT_INVALID" | "NEEDS_CORE_NOT_CONFIGURED" | "NO_USABLE_NODES" @@ -84,11 +86,13 @@ export interface ProxySubscriptionRecord { localCoreEndpoint: string | null; updateIntervalMinutes: number; controlUrl: string | null; + coreConfigPath: string | null; hasControlSecret: boolean; selectorMinGapSeconds: number; selectorLastSwitchAt: string | null; selectorLastSwitchResult: string | null; selectorLastSwitchMember: string | null; + selectorLastSwitchKind: string | null; lastFetchedAt: string | null; status: ProxySubscriptionStatus; error: string | null; @@ -108,6 +112,7 @@ export interface ProxySubscriptionPayload { localCoreEndpoint?: string | null; updateIntervalMinutes?: number; controlUrl?: string | null; + coreConfigPath?: string | null; controlSecret?: string | null; selectorMinGapSeconds?: number; } @@ -142,17 +147,20 @@ function readSelectorBase(r: Record): { function readControlMeta(r: Record): { controlUrl: string | null; + coreConfigPath: string | null; hasControlSecret: boolean; selectorMinGapSeconds: number; selectorLastSwitchAt: string | null; selectorLastSwitchResult: string | null; selectorLastSwitchMember: string | null; + selectorLastSwitchKind: string | null; } { // control_secret_enc is write-only: exposed only as a presence bit, never // in clear. Any future writer of control_url MUST update the selector // trigger cache (selectorTrigger.setAnyControlUrlConfigured). return { controlUrl: typeof r.control_url === "string" ? r.control_url : null, + coreConfigPath: typeof r.core_config_path === "string" ? r.core_config_path : null, hasControlSecret: typeof r.control_secret_enc === "string" && r.control_secret_enc.length > 0, selectorMinGapSeconds: clampSelectorGapSeconds(r.selector_min_gap_seconds), selectorLastSwitchAt: @@ -161,6 +169,8 @@ function readControlMeta(r: Record): { typeof r.selector_last_switch_result === "string" ? r.selector_last_switch_result : null, selectorLastSwitchMember: typeof r.selector_last_switch_member === "string" ? r.selector_last_switch_member : null, + selectorLastSwitchKind: + typeof r.selector_last_switch_kind === "string" ? r.selector_last_switch_kind : null, }; } @@ -208,7 +218,7 @@ export async function listSubscriptions(): Promise { const db = getDbInstance(); const rows = db .prepare( - "SELECT id, name, url, enabled, mode, rule_providers, local_core_endpoint, update_interval_minutes, control_url, control_secret_enc, selector_min_gap_seconds, selector_last_switch_at, selector_last_switch_result, selector_last_switch_member, last_fetched_at, status, error, last_nodes, last_error_at, consecutive_failures, created_at, updated_at FROM proxy_subscriptions ORDER BY datetime(updated_at) DESC, name ASC" + "SELECT id, name, url, enabled, mode, rule_providers, local_core_endpoint, update_interval_minutes, control_url, core_config_path, control_secret_enc, selector_min_gap_seconds, selector_last_switch_at, selector_last_switch_result, selector_last_switch_member, selector_last_switch_kind, last_fetched_at, status, error, last_nodes, last_error_at, consecutive_failures, created_at, updated_at FROM proxy_subscriptions ORDER BY datetime(updated_at) DESC, name ASC" ) .all(); return rows.map(mapSubscriptionRow); @@ -218,7 +228,7 @@ export async function getSubscriptionById(id: string): Promise 0 ? ((encrypt(payload.controlSecret) ?? null) as string | null) @@ -239,8 +250,8 @@ export async function createSubscription( const gapSeconds = clampSelectorGapSeconds(payload.selectorMinGapSeconds ?? 60); db.prepare( `INSERT INTO proxy_subscriptions - (id, name, url, enabled, mode, rule_providers, local_core_endpoint, update_interval_minutes, control_url, control_secret_enc, selector_min_gap_seconds, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'empty', ?, ?)` + (id, name, url, enabled, mode, rule_providers, local_core_endpoint, update_interval_minutes, control_url, core_config_path, control_secret_enc, selector_min_gap_seconds, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'empty', ?, ?)` ).run( id, payload.name, @@ -251,6 +262,7 @@ export async function createSubscription( payload.localCoreEndpoint || null, payload.updateIntervalMinutes || 60, controlUrl, + coreConfigPath, controlSecretEnc, gapSeconds, now, @@ -294,6 +306,7 @@ function persistSubscriptionRow( localCoreEndpoint: string | null; updateIntervalMinutes: number; controlUrl: string | null; + coreConfigPath: string | null; controlSecretEnc: string | null | undefined; gapSeconds: number; now: string; @@ -302,7 +315,7 @@ function persistSubscriptionRow( if (args.controlSecretEnc === undefined) { db.prepare( `UPDATE proxy_subscriptions - SET name = ?, url = ?, enabled = ?, mode = ?, rule_providers = ?, local_core_endpoint = ?, update_interval_minutes = ?, control_url = ?, selector_min_gap_seconds = ?, updated_at = ? + SET name = ?, url = ?, enabled = ?, mode = ?, rule_providers = ?, local_core_endpoint = ?, update_interval_minutes = ?, control_url = ?, core_config_path = ?, selector_min_gap_seconds = ?, updated_at = ? WHERE id = ?` ).run( args.name, @@ -313,6 +326,7 @@ function persistSubscriptionRow( args.localCoreEndpoint || null, args.updateIntervalMinutes, args.controlUrl, + args.coreConfigPath, args.gapSeconds, args.now, args.id @@ -320,7 +334,7 @@ function persistSubscriptionRow( } else { db.prepare( `UPDATE proxy_subscriptions - SET name = ?, url = ?, enabled = ?, mode = ?, rule_providers = ?, local_core_endpoint = ?, update_interval_minutes = ?, control_url = ?, control_secret_enc = ?, selector_min_gap_seconds = ?, updated_at = ? + SET name = ?, url = ?, enabled = ?, mode = ?, rule_providers = ?, local_core_endpoint = ?, update_interval_minutes = ?, control_url = ?, core_config_path = ?, control_secret_enc = ?, selector_min_gap_seconds = ?, updated_at = ? WHERE id = ?` ).run( args.name, @@ -331,6 +345,7 @@ function persistSubscriptionRow( args.localCoreEndpoint || null, args.updateIntervalMinutes, args.controlUrl, + args.coreConfigPath, args.controlSecretEnc, args.gapSeconds, args.now, @@ -356,7 +371,8 @@ async function refreshAfterUpdate( } else if ( enabledChanged || payload.url !== undefined || - payload.localCoreEndpoint !== undefined + payload.localCoreEndpoint !== undefined || + payload.coreConfigPath !== undefined ) { // Same scopes: just refresh nodes (re-applies idempotently to same scopes). await syncSubscription(id); @@ -382,9 +398,10 @@ export async function updateSubscription( payload.enabled !== undefined ? (payload.enabled ? 1 : 0) : existing.enabled ? 1 : 0; // The SET lists ONLY the columns this writer owns plus the three - // selector-control columns — every other column is untouched (byte-identical - // off opt-in). Secret is write-only: payload.controlSecret replaces - // control_secret_enc when present (encrypt at rest); absent leaves it. + // selector-control columns and the opt-in core-config path — every other + // column is untouched (byte-identical off opt-in). Secret is write-only: + // payload.controlSecret replaces control_secret_enc when present (encrypt + // at rest); absent leaves it. const controlSecretEnc = encryptSecretField(payload.controlSecret); persistSubscriptionRow(db, { id, @@ -400,6 +417,8 @@ export async function updateSubscription( : existing.localCoreEndpoint, updateIntervalMinutes: payload.updateIntervalMinutes ?? existing.updateIntervalMinutes, controlUrl, + coreConfigPath: + payload.coreConfigPath !== undefined ? payload.coreConfigPath : existing.coreConfigPath, controlSecretEnc, gapSeconds, now, @@ -872,6 +891,18 @@ async function syncSubscriptionUnsafe(id: string): Promise { const lastNodes = redactedNodeSummary(parsed); + // Opt-in core-config generation (beside-file only, never the adopted + // file). Skipped when an earlier warning already owns the `error` column — + // the skip is logged server-side so a stale beside-file never goes silent. + if (sub.coreConfigPath) { + if (!warning) { + const generated = await generateForSubscription(sub, parsed); + if (generated) warning = generated; + } else { + console.warn(`[ProxySubscription] core config generation skipped for ${id}`); + } + } + // A failed control-call switch leaves a standing warning on the // subscription: the sync rewrites `error` from its own `warning` alone, so // without this merge the switch outcome would vanish on the next refresh. @@ -967,11 +998,13 @@ export function readStaleSwitchWarning(subscriptionId: string): string | null { /** Record a switch outcome on the subscription row (columns from the * selector-control migration). On failure also sets `error` directly — * without touching `status` — so the warning shows before the next sync. - * Never throws. */ + * The kind (the refusal motive that fired the switch) is recorded on both + * outcomes: a failed attempt still documents why it was tried. Never throws. */ export async function recordSelectorSwitchOutcome(args: { subscriptionId: string; result: string; member?: string | null; + kind?: string | null; }): Promise { try { const db = getDbInstance(); @@ -979,9 +1012,9 @@ export async function recordSelectorSwitchOutcome(args: { db.prepare( `UPDATE proxy_subscriptions SET selector_last_switch_at = ?, selector_last_switch_result = ?, - selector_last_switch_member = ?, updated_at = ? + selector_last_switch_member = ?, selector_last_switch_kind = ?, updated_at = ? WHERE id = ?` - ).run(now, args.result, args.member ?? null, now, args.subscriptionId); + ).run(now, args.result, args.member ?? null, args.kind ?? null, now, args.subscriptionId); if (args.result !== "ok") { db.prepare( `UPDATE proxy_subscriptions SET error = ?, updated_at = ? WHERE id = ? AND status = 'ok'` diff --git a/tests/unit/proxy-subscriptions-route-validation.test.ts b/tests/unit/proxy-subscriptions-route-validation.test.ts index 8baaf1fb7b1c..3b3dd6eb2b71 100644 --- a/tests/unit/proxy-subscriptions-route-validation.test.ts +++ b/tests/unit/proxy-subscriptions-route-validation.test.ts @@ -347,3 +347,52 @@ test("PATCH proxy-subscriptions/:id — unknown id still 404s past body validati const body = (await res.json()) as { error?: string }; assert.equal(body.error, "Subscription not found"); }); + +test("POST proxy-subscriptions — five bad coreConfigPath values return 400 with reason", async () => { + const bad: Array<[string, string]> = [ + ["relative/path.json", "not_absolute"], + ["/adopted/../escape.json", "dotdot_segment"], + ["no-extension", "not_absolute"], + [`/${"a".repeat(2000)}.json`, "too_long"], + ["\0null.json", "nul_byte"], + ]; + for (const [coreConfigPath, reason] of bad) { + const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", { + name: `bad-path-${reason}`, + url: "https://example.com/feed", + coreConfigPath, + }); + const res = await collectionRoute.POST(req); + assert.equal(res.status, 400, `expected 400 for ${reason}`); + const body = (await res.json()) as { error?: string }; + assert.ok( + body.error === `coreConfigPath is not allowed (${reason})`, + `unexpected message: ${body.error}` + ); + } +}); + +test("POST proxy-subscriptions — absolute non-json path returns 400 bad_extension", async () => { + const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", { + name: "bad-ext", + url: "https://example.com/feed", + coreConfigPath: "/adopted/core.txt", + }); + const res = await collectionRoute.POST(req); + assert.equal(res.status, 400); + const body = (await res.json()) as { error?: string }; + assert.equal(body.error, "coreConfigPath is not allowed (bad_extension)"); +}); + +test("PATCH proxy-subscriptions/:id — bad coreConfigPath returns 400", async () => { + const fixture = await createValidSubscription("patch-bad-path"); + const req = jsonRequest( + `http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`, + { coreConfigPath: "relative.json" }, + "PATCH" + ); + const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) }); + assert.equal(res.status, 400); + const body = (await res.json()) as { error?: string }; + assert.equal(body.error, "coreConfigPath is not allowed (not_absolute)"); +}); diff --git a/tests/unit/proxySubscription.coreConfigSync.test.ts b/tests/unit/proxySubscription.coreConfigSync.test.ts new file mode 100644 index 000000000000..e6638ac940dd --- /dev/null +++ b/tests/unit/proxySubscription.coreConfigSync.test.ts @@ -0,0 +1,298 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +test("empty path writes nothing and never calls the renderer", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-empty-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + let calls = 0; + const renderers = await import("../../src/lib/proxySubscription/coreConfig/renderers.ts"); + const orig = renderers.RENDERERS[renderers.DEFAULT_CORE]; + (renderers.RENDERERS as Record)[renderers.DEFAULT_CORE] = () => { + calls += 1; + return orig; + }; + try { + const res = await sync.generateForSubscription( + { coreConfigPath: "", localCoreEndpoint: "socks5://127.0.0.1:1080 selector=g" }, + { nodes: [], needsCore: [], format: "empty" } + ); + assert.equal(res, null); + assert.equal(calls, 0); + assert.deepEqual(fs.readdirSync(dir), []); + } finally { + (renderers.RENDERERS as Record)[renderers.DEFAULT_CORE] = orig; + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function singBoxFeed(): { nodes: never[]; needsCore: never[]; format: "v2ray-json" } { + const mk = (i: number) => ({ + name: `sb-node-${i}`, + source: { + kind: "object" as const, + value: { type: "vless", server: `203.0.113.${i}`, server_port: 443, uuid: `u-${i}` }, + }, + }); + return { + nodes: [], + needsCore: [mk(1), mk(2), mk(3), mk(4)] as never[], + format: "v2ray-json", + }; +} + +const ENDPOINTS = "socks5://127.0.0.1:1080 selector=a\nsocks5://127.0.0.1:1081 selector=b"; + +test("writes beside-file once per subscription, leaves the adopted file alone", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-write-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync( + target, + JSON.stringify({ dns: { servers: ["https://9.9.9.9/dns-query"] }, inbounds: [], outbounds: [] }) + ); + const beforeHash = createHash("sha256").update(fs.readFileSync(target)).digest("hex"); + const warned: string[] = []; + const origWarn = console.warn; + console.warn = (...args: unknown[]) => { + warned.push(args.map(String).join(" ")); + }; + try { + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: ENDPOINTS }, + singBoxFeed() + ); + assert.equal(res, null); + } finally { + console.warn = origWarn; + } + const generated = JSON.parse(fs.readFileSync(`${target}.generated`, "utf8")); + const selectors = generated.outbounds.filter((o: { type: string }) => o.type === "selector"); + assert.equal(selectors.length, 2); + assert.equal(generated.inbounds.length, 2); + assert.equal( + generated.outbounds.filter((o: { tag: string }) => o.tag.startsWith("omniroute-sb-node-")) + .length, + 4 + ); + assert.equal(createHash("sha256").update(fs.readFileSync(target)).digest("hex"), beforeHash); + assert.equal(warned.length, 0); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("second identical run writes nothing (mtime unchanged)", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-mtime-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + const sub = { coreConfigPath: target, localCoreEndpoint: ENDPOINTS }; + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + await sync.generateForSubscription(sub, singBoxFeed()); + const first = fs.statSync(`${target}.generated`).mtimeMs; + await new Promise((r) => setTimeout(r, 20)); + const res = await sync.generateForSubscription(sub, singBoxFeed()); + assert.equal(res, null); + assert.equal(fs.statSync(`${target}.generated`).mtimeMs, first); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("missing directory warns without throwing", async () => { + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const res = await sync.generateForSubscription( + { + coreConfigPath: path.join(os.tmpdir(), "omniroute-no-such-dir-xyz", "core.json"), + localCoreEndpoint: ENDPOINTS, + }, + singBoxFeed() + ); + assert.ok(res?.includes("CORE_CONFIG_NOT_GENERATED")); +}); + +test("unparseable adopted file warns and is never modified", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-bad-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync(target, "not json{{{"); + const before = fs.readFileSync(target, "utf8"); + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: ENDPOINTS }, + singBoxFeed() + ); + assert.ok(res?.includes("CORE_CONFIG_NOT_GENERATED")); + assert.equal(fs.readFileSync(target, "utf8"), before); + assert.ok(!fs.existsSync(`${target}.generated`)); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("additive migration replays without effect", async () => { + const sql = fs.readFileSync( + new URL("../../src/lib/db/migrations/197_proxy_subscription_core_config.sql", import.meta.url), + "utf8" + ); + assert.ok(sql.includes("ADD COLUMN core_config_path TEXT")); +}); + +test("end to end: adopted dns and log survive into the beside-file", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-e2e-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync( + target, + JSON.stringify({ + dns: { servers: ["https://1.1.1.1/dns-query"] }, + log: { level: "warn" }, + inbounds: [], + outbounds: [], + route: { rules: [] }, + }) + ); + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: ENDPOINTS }, + singBoxFeed() + ); + assert.equal(res, null); + const generated = JSON.parse(fs.readFileSync(`${target}.generated`, "utf8")); + assert.deepEqual(generated.dns, { servers: ["https://1.1.1.1/dns-query"] }); + assert.deepEqual(generated.log, { level: "warn" }); + assert.equal(generated.inbounds.length, 2); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("earlier warning wins: generation skipped, skip logged, prior error kept", async () => { + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + // Unit-level: generateForSubscription itself returns null for empty path — + // the skip-when-warning branch lives in the service caller (covered below + // by code inspection of the warn-on-skip line in subscriptionService.ts). + const src = fs.readFileSync( + new URL("../../src/lib/proxySubscription/subscriptionService.ts", import.meta.url), + "utf8" + ); + assert.ok(src.includes("core config generation skipped")); + void sync; +}); + +function captureWarn(): { lines: string[]; restore: () => void } { + const lines: string[] = []; + const orig = console.warn; + console.warn = (...args: unknown[]) => { + lines.push(args.map(String).join(" ")); + }; + return { lines, restore: () => void (console.warn = orig) }; +} + +function uriFeed( + count: number, + name = "shared" +): { nodes: never[]; needsCore: never[]; format: "lines" } { + return { + nodes: [], + needsCore: Array.from({ length: count }, (_, i) => ({ + name, + source: { kind: "uri" as const, value: `vless://u@10.9.0.${i + 1}:443#${name}` }, + })) as never[], + format: "lines", + }; +} + +const THREE_ENDPOINTS = + "socks5://127.0.0.1:1080 selector=g1\nsocks5://127.0.0.1:1081 selector=g2\nsocks5://127.0.0.1:1082 selector=g3"; + +test("unassigned groups log one empty_group line per group", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-skip-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + const cap = captureWarn(); + try { + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: THREE_ENDPOINTS }, + uriFeed(1) + ); + assert.equal(res, null); + } finally { + cap.restore(); + } + assert.equal(cap.lines.length, 1); + assert.ok(cap.lines[0].includes("empty_group=3")); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("fully populated generation stays silent", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-quiet-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + const cap = captureWarn(); + try { + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: ENDPOINTS }, + singBoxFeed() + ); + assert.equal(res, null); + } finally { + cap.restore(); + } + assert.equal(cap.lines.length, 0); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("identical rerun stays silent (unchanged)", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-rerun-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + const sub = { coreConfigPath: target, localCoreEndpoint: THREE_ENDPOINTS }; + await sync.generateForSubscription(sub, uriFeed(1)); + const cap = captureWarn(); + try { + const res = await sync.generateForSubscription(sub, uriFeed(1)); + assert.equal(res, null); + } finally { + cap.restore(); + } + assert.equal(cap.lines.length, 0); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("skip line carries counts only, never node names or servers", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-leak-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const target = path.join(dir, "core.json"); + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + const cap = captureWarn(); + try { + await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: THREE_ENDPOINTS }, + uriFeed(2, "leakcheck-name") + ); + } finally { + cap.restore(); + } + assert.equal(cap.lines.length, 1); + assert.ok(!cap.lines[0].includes("leakcheck-name")); + assert.ok(!cap.lines[0].includes("10.9.0.")); +}); + +test("repeated generations leave no accumulated state", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-core-bounded-")); + const sync = await import("../../src/lib/proxySubscription/coreConfig/sync.ts"); + const cap = captureWarn(); + try { + for (let i = 0; i < 5; i += 1) { + const target = path.join(dir, `core-${i}.json`); + fs.writeFileSync(target, JSON.stringify({ inbounds: [], outbounds: [] })); + const res = await sync.generateForSubscription( + { coreConfigPath: target, localCoreEndpoint: THREE_ENDPOINTS }, + uriFeed(1) + ); + assert.equal(res, null); + } + } finally { + cap.restore(); + } + assert.equal(cap.lines.length, 5); + assert.ok(cap.lines.every((l) => l.includes("empty_group=3"))); + fs.rmSync(dir, { recursive: true, force: true }); +}); diff --git a/tests/unit/proxySubscription.coreModel.test.ts b/tests/unit/proxySubscription.coreModel.test.ts new file mode 100644 index 000000000000..0965b86232f0 --- /dev/null +++ b/tests/unit/proxySubscription.coreModel.test.ts @@ -0,0 +1,374 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const parse = await import("../../src/lib/proxySubscription/parse.ts"); +const model = await import("../../src/lib/proxySubscription/coreConfig/model.ts"); +const { parseSubscription, redactedNodeSummary } = parse; +const { buildCoreModel, OWNED_TAG_PREFIX } = model; + +function clashYaml(): string { + return `proxies: + - name: "Direct HTTP" + type: http + server: 1.2.3.4 + port: 8080 + username: u + password: p + - name: "Node A" + type: ss + server: 9.9.9.9 + port: 8388 + - name: "Node B" + type: vmess + server: 11.11.11.11 + port: 443 + - name: "Node C" + type: vless + server: 12.12.12.12 + port: 443 + - name: "Extra" + type: socks5 + server: 5.6.7.8 + port: 1080 +`; +} + +test("duplicate node names get identity suffixes, stable under permutation", () => { + const feed = "vless://u@1.1.1.1:443#dup\nvless://u@2.2.2.2:443#dup\nvless://u@3.3.3.3:443#dup"; + const parsed = parseSubscription(feed); + assert.equal(parsed.needsCore.length, 3); + const m = buildCoreModel(["socks5://127.0.0.1:1080 selector=g"], parsed.needsCore); + const tags = m.nodes.map((n) => n.tag); + assert.equal(new Set(tags).size, 3); + assert.ok(tags.every((t) => t.startsWith("dup-") && t !== "dup" && !/-2$/.test(t))); + const rev = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=g"], + [...parsed.needsCore].reverse() + ); + const byServer = (model: typeof m): Map => + new Map(model.nodes.map((n) => [(n.source as { kind: "uri"; value: string }).value, n.tag])); + for (const [source, tag] of byServer(m)) assert.equal(byServer(rev).get(source), tag); +}); + +test("no-suffix, valid and non-loopback entries sort into skipped vs groups", () => { + const parsed = parseSubscription("vless://u@1.1.1.1:443#n"); + const m = buildCoreModel( + [ + "socks5://127.0.0.1:1080", + "socks5://127.0.0.1:1081 selector=g", + "http://192.168.1.10:8080 selector=lan", + ], + [...parsed.nodes, ...parsed.needsCore] + ); + assert.equal(m.groups.length, 1); + assert.equal(m.groups[0].tag, `${OWNED_TAG_PREFIX}g`); + const reasons = m.skipped.map((s) => s.reason).sort(); + assert.deepEqual(reasons, ["invalid_endpoint", "no_selector_tag"]); +}); + +test("one group keeps every node, two groups split, zero groups stay empty", () => { + const parsed = parseSubscription( + "vless://u@example.com:443#alpha\nvless://u@example.net:443#beta" + ); + const all = [...parsed.nodes, ...parsed.needsCore]; + const one = buildCoreModel(["socks5://127.0.0.1:1080 selector=g"], all); + assert.equal(one.groups.length, 1); + assert.equal(one.groups[0].members.length, 2); + + const two = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=g1", "socks5://127.0.0.1:1081 selector=g2"], + all + ); + assert.equal(two.groups.length, 2); + const assigned = two.groups.flatMap((g) => g.members).sort(); + assert.deepEqual(assigned, two.nodes.map((n) => n.tag).sort()); + + const none = buildCoreModel([], all); + assert.deepEqual(none.listeners, []); + assert.deepEqual(none.groups, []); + assert.deepEqual(none.nodes, []); +}); + +test("adding a node keeps every group assignment (rendezvous)", () => { + const before = buildCoreModel( + [ + "socks5://127.0.0.1:1080 selector=g1", + "socks5://127.0.0.1:1081 selector=g2", + "socks5://127.0.0.1:1082 selector=g3", + ], + distinctServerNodes(4) + ); + const after = buildCoreModel( + [ + "socks5://127.0.0.1:1080 selector=g1", + "socks5://127.0.0.1:1081 selector=g2", + "socks5://127.0.0.1:1082 selector=g3", + ], + distinctServerNodes(5) + ); + const groupOf = (m: typeof before, tag: string): string | undefined => + m.groups.find((g) => g.members.includes(tag))?.tag; + for (const n of before.nodes) assert.equal(groupOf(after, n.tag), groupOf(before, n.tag)); + const used = new Set(before.nodes.map((n) => groupOf(before, n.tag))); + assert.ok(used.size > 1, `expected spread across groups, got ${[...used]}`); +}); + +test("lines and clash-yaml feeds each keep their source shape", () => { + const uri = parseSubscription("vless://u@example.com:443#alpha"); + assert.equal(uri.needsCore[0].source?.kind, "uri"); + const yaml = parseSubscription(clashYaml()); + assert.equal(yaml.nodes[0].source?.kind, "object"); + assert.equal(yaml.needsCore[0].source?.kind, "object"); +}); + +test("redacted summary never leaks password or source", () => { + const parsed = parseSubscription(clashYaml()); + const text = JSON.stringify(redactedNodeSummary(parsed)); + assert.ok(!text.includes('"password"')); + assert.ok(!text.includes(" p")); + assert.ok(!text.includes('"source"')); +}); + +test("end to end: clash yaml with 5 nodes builds stable groups", () => { + const parsed = parseSubscription(clashYaml()); + assert.equal(parsed.nodes.length + parsed.needsCore.length, 5); + const m = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=main", "http://127.0.0.1:8080 selector=web"], + [...parsed.nodes, ...parsed.needsCore] + ); + assert.equal(m.listeners.length, 2); + assert.equal(m.groups.length, 2); + assert.equal(m.nodes.length, 5); + assert.deepEqual(m.groups.flatMap((g) => g.members).sort(), m.nodes.map((n) => n.tag).sort()); + // Deterministic: same input gives the same output. + assert.deepEqual( + buildCoreModel( + ["socks5://127.0.0.1:1080 selector=main", "http://127.0.0.1:8080 selector=web"], + [...parsed.nodes, ...parsed.needsCore] + ), + m + ); +}); + +function exactDuplicateNodes(count: number, name = "shared"): never[] { + return Array.from({ length: count }, () => ({ + name, + source: { + kind: "object" as const, + value: { + type: "vless", + server: "10.9.0.7", + server_port: 443, + uuid: "uuid-same", + }, + }, + })) as never[]; +} + +const THREE_GROUPS = [ + "socks5://127.0.0.1:1080 selector=g1", + "socks5://127.0.0.1:1081 selector=g2", + "socks5://127.0.0.1:1082 selector=g3", +]; + +test("lines sharing a selector form one group (no member reset)", () => { + const parsed = parseSubscription("vless://u@10.7.0.1:443#only"); + const m = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=g", "socks5://127.0.0.1:1081 selector=g"], + [...parsed.nodes, ...parsed.needsCore] + ); + assert.equal(m.listeners.length, 2); + assert.equal(m.groups.length, 1); + assert.equal(m.groups[0].members.length, 1); +}); + +test("exact duplicates share one group, the other groups stay reported", () => { + const m = buildCoreModel(THREE_GROUPS, exactDuplicateNodes(60)); + const sizes = m.groups.map((g) => g.members.length).sort((a, b) => a - b); + assert.deepEqual(sizes, [0, 0, 60]); + assert.equal(m.skipped.filter((s) => s.reason === "empty_group").length, 2); +}); + +test("populated groups report no empty_group", () => { + const nodes = Array.from({ length: 60 }, (_, i) => ({ + name: `node-${i + 1}`, + source: { + kind: "object" as const, + value: { + type: "vless", + server: `10.8.0.${i + 1}`, + server_port: 443, + uuid: `uuid-${i + 1}`, + }, + }, + })) as never[]; + const m = buildCoreModel(THREE_GROUPS, nodes); + assert.ok(m.groups.every((g) => g.members.length > 0)); + assert.equal(m.skipped.filter((s) => s.reason === "empty_group").length, 0); +}); + +test("groups with only sourceless nodes report one empty_group each", () => { + const nodes = [{ name: "ghost-a" }, { name: "ghost-b" }, { name: "ghost-c" }] as never[]; + const m = buildCoreModel(THREE_GROUPS, nodes); + const empty = m.skipped.filter((s) => s.reason === "empty_group"); + assert.equal(empty.length, 3); + assert.deepEqual(empty.map((s) => s.node).sort(), m.groups.map((g) => g.tag).sort()); +}); + +function distinctServerNodes(count: number, name = "shared"): never[] { + return Array.from({ length: count }, (_, i) => ({ + name, + source: { + kind: "object" as const, + value: { + type: "vless", + server: `10.9.0.${i + 1}`, + server_port: 443, + uuid: `uuid-${i + 1}`, + }, + }, + })) as never[]; +} + +test("same display name nodes spread across every group", () => { + const m = buildCoreModel(THREE_GROUPS, distinctServerNodes(60)); + assert.ok(m.groups.every((g) => g.members.length > 0)); +}); + +test("permuted same-name nodes keep their server tag", () => { + const mk = (server: string) => + ({ + name: "shared", + source: { + kind: "object" as const, + value: { type: "vless", server, server_port: 443, uuid: "uuid-x" }, + }, + }) as never; + const forward = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=g"], + [mk("10.9.0.1"), mk("10.9.0.2")] + ); + const backward = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=g"], + [mk("10.9.0.2"), mk("10.9.0.1")] + ); + const tagOf = (tag: string): string | undefined => forward.nodes.find((n) => n.tag === tag)?.tag; + assert.ok(typeof tagOf === "function"); + assert.deepEqual(forward.nodes.map((n) => n.tag).sort(), backward.nodes.map((n) => n.tag).sort()); + const byServerFwd = new Map(forward.nodes.map((n) => [n.source.value.server, n.tag])); + const byServerBwd = new Map(backward.nodes.map((n) => [n.source.value.server, n.tag])); + assert.equal(byServerFwd.get("10.9.0.1"), byServerBwd.get("10.9.0.1")); + assert.equal(byServerFwd.get("10.9.0.2"), byServerBwd.get("10.9.0.2")); +}); + +test("removing a group moves only its members", () => { + const nodes = distinctServerNodes(60); + const three = buildCoreModel(THREE_GROUPS, nodes); + const tagIndex = new Map(three.nodes.map((n) => [n.tag, n.tag])); + assert.equal(tagIndex.size, three.nodes.length); + const groupOf = (m: typeof three, tag: string): string | undefined => + m.groups.find((g) => g.members.includes(tag))?.tag; + const removedGroup = three.groups + .map((g) => g.tag) + .sort() + .pop()!; + const keptEntries = THREE_GROUPS.filter( + (e) => !e.includes(`selector=${removedGroup.replace(OWNED_TAG_PREFIX, "")}`) + ); + const two = buildCoreModel(keptEntries, nodes); + const moved = three.nodes.filter((n) => groupOf(three, n.tag) !== groupOf(two, n.tag)); + const removedMembers = new Set(three.groups.find((g) => g.tag === removedGroup)?.members ?? []); + assert.ok(moved.length > 0); + assert.ok(moved.every((n) => removedMembers.has(n.tag))); +}); + +test("sourceless nodes keep current names and groups", () => { + const nodes = [ + { name: "dup", source: { kind: "uri" as const, value: "not a url at all" } }, + { name: "dup", source: { kind: "uri" as const, value: "also not a url" } }, + { name: "dup", source: { kind: "uri" as const, value: "still not a url" } }, + ] as never[]; + const m = buildCoreModel(["socks5://127.0.0.1:1080 selector=g"], nodes); + assert.deepEqual( + m.nodes.map((n) => n.tag), + ["dup", "dup-2", "dup-3"] + ); +}); + +test("mixed sourced and sourceless nodes stay deterministic", () => { + const sourced = distinctServerNodes(3); + const mixed = [...sourced, { name: "ghost" } as never]; + const first = buildCoreModel(THREE_GROUPS, mixed); + const second = buildCoreModel(THREE_GROUPS, mixed); + assert.deepEqual(first, second); +}); + +test("object and uri forms of one node share tag and group", () => { + const objectNode = { + name: "same", + source: { + kind: "object" as const, + value: { type: "vless", server: "EXAMPLE.com", server_port: 443, uuid: "u" }, + }, + } as never; + const uriNode = { + name: "same", + source: { kind: "uri" as const, value: "vless://u@EXAMPLE.com:443#same" }, + } as never; + const fromObject = buildCoreModel(THREE_GROUPS, [objectNode]); + const fromUri = buildCoreModel(THREE_GROUPS, [uriNode]); + assert.deepEqual( + fromObject.nodes.map((n) => n.tag), + fromUri.nodes.map((n) => n.tag) + ); + const groupOf = (m: typeof fromObject, tag: string): string | undefined => + m.groups.find((g) => g.members.includes(tag))?.tag; + assert.equal( + groupOf(fromObject, fromObject.nodes[0].tag), + groupOf(fromUri, fromUri.nodes[0].tag) + ); +}); + +test("missing port and explicit port stay distinct", () => { + const noPort = { + name: "same", + source: { kind: "object" as const, value: { type: "vless", server: "10.9.0.1", uuid: "u" } }, + } as never; + const explicit = { + name: "same", + source: { + kind: "object" as const, + value: { type: "vless", server: "10.9.0.1", server_port: 443, uuid: "u" }, + }, + } as never; + const uriNoPort = { + name: "same", + source: { kind: "uri" as const, value: "vless://u@10.9.0.1#same" }, + } as never; + const a = buildCoreModel(THREE_GROUPS, [noPort]); + const b = buildCoreModel(THREE_GROUPS, [uriNoPort]); + const c = buildCoreModel(THREE_GROUPS, [explicit]); + assert.deepEqual( + a.nodes.map((n) => n.tag), + b.nodes.map((n) => n.tag) + ); + assert.notEqual(a.nodes[0].tag, c.nodes[0].tag); +}); + +test("nfc-equivalent names share tag base and fallback group", () => { + const composed = "caf\u00e9"; + const decomposed = "café"; + assert.notEqual(composed, decomposed); + const mk = (name: string) => + ({ + name, + source: { kind: "uri" as const, value: "not a url" }, + }) as never; + const first = buildCoreModel(["socks5://127.0.0.1:1080 selector=g"], [mk(composed)]); + const second = buildCoreModel(["socks5://127.0.0.1:1080 selector=g"], [mk(decomposed)]); + assert.deepEqual( + first.nodes.map((n) => n.tag), + second.nodes.map((n) => n.tag) + ); + assert.deepEqual(first.groups, second.groups); +}); diff --git a/tests/unit/proxySubscription.coreRenderSingbox.test.ts b/tests/unit/proxySubscription.coreRenderSingbox.test.ts new file mode 100644 index 000000000000..37fbc87046e3 --- /dev/null +++ b/tests/unit/proxySubscription.coreRenderSingbox.test.ts @@ -0,0 +1,231 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { buildCoreModel, OWNED_TAG_PREFIX } = + await import("../../src/lib/proxySubscription/coreConfig/model.ts"); +const { renderSingBox, isSingBoxShape } = + await import("../../src/lib/proxySubscription/coreConfig/singbox.ts"); +const renderers = await import("../../src/lib/proxySubscription/coreConfig/renderers.ts"); + +function objNode(name: string, extra: Record = {}): never { + throw new Error(`test helper miswired: ${name} ${JSON.stringify(extra)}`); +} + +function modelOf(count: number, opts?: { clash?: boolean; uri?: boolean }) { + const nodes = Array.from({ length: count }, (_, i) => ({ + name: `node-${i + 1}`, + source: + opts?.clash || opts?.uri + ? { kind: "uri" as const, value: `vless://u@example.com:443#node-${i + 1}` } + : { + kind: "object" as const, + value: { + type: "vless", + server: `10.0.0.${i + 1}`, + server_port: 443 + i, + uuid: `uuid-${i + 1}`, + }, + }, + })); + return buildCoreModel( + ["socks5://127.0.0.1:1080 selector=a", "http://127.0.0.1:8080 selector=b"], + nodes as never + ); +} + +function realisticExisting(): string { + return JSON.stringify( + { + log: { level: "info" }, + dns: { servers: ["https://1.1.1.1/dns-query"] }, + experimental: { clash_api: { external_controller: "127.0.0.1:9090" } }, + inbounds: [{ type: "mixed", tag: "user-in", listen: "127.0.0.1", listen_port: 7890 }], + outbounds: [ + { type: "direct", tag: "direct" }, + { type: "selector", tag: "user-pick", outbounds: ["direct"] }, + ], + route: { + rules: [{ outbound: "direct", inbound: ["user-in"] }], + final: "direct", + }, + }, + null, + 2 + ); +} + +test("preserves dns, log, clash_api and user rules verbatim (deep compare)", () => { + const before = JSON.parse(realisticExisting()); + const res = renderSingBox(modelOf(4), realisticExisting()); + assert.equal(res.ok, true); + if (!res.ok) return; + const after = JSON.parse(res.text); + assert.deepEqual(after.log, before.log); + assert.deepEqual(after.dns, before.dns); + assert.deepEqual(after.experimental, before.experimental); + assert.ok(after.inbounds.some((i: { tag: string }) => i.tag === "user-in")); + assert.ok(after.outbounds.some((o: { tag: string }) => o.tag === "direct")); + assert.ok(after.outbounds.some((o: { tag: string }) => o.tag === "user-pick")); + assert.ok(after.route.rules.some((r: { outbound: string }) => r.outbound === "direct")); + assert.equal(after.route.final, "direct"); +}); + +test("second render is unchanged", () => { + const first = renderSingBox(modelOf(4), realisticExisting()); + assert.equal(first.ok, true); + if (!first.ok) return; + assert.equal(first.unchanged, false); + const second = renderSingBox(modelOf(4), first.text); + assert.equal(second.ok, true); + if (!second.ok) return; + assert.equal(second.unchanged, true); + assert.equal(second.text, first.text); +}); + +test("removed member disappears, neighbors stay", () => { + const m = modelOf(4); + const first = renderSingBox(m, null); + assert.equal(first.ok, true); + if (!first.ok) return; + const removedTag = `${OWNED_TAG_PREFIX}${m.nodes[3].tag}`; + const small = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=a", "http://127.0.0.1:8080 selector=b"], + m.nodes.slice(0, 3) as never + ); + const second = renderSingBox(small, first.text); + assert.equal(second.ok, true); + if (!second.ok) return; + const after = JSON.parse(second.text); + const tags = after.outbounds.map((o: { tag: string }) => o.tag); + assert.ok(!tags.includes(removedTag), `removed ${removedTag} still present`); + for (const n of small.nodes) assert.ok(tags.includes(`${OWNED_TAG_PREFIX}${n.tag}`)); +}); + +test("refuses {} / not-json / array roots", () => { + assert.deepEqual(renderSingBox(modelOf(1), "{}"), { ok: false, reason: "no_ownable_section" }); + assert.deepEqual(renderSingBox(modelOf(1), "not json"), { ok: false, reason: "unparseable" }); + assert.deepEqual(renderSingBox(modelOf(1), "[]"), { ok: false, reason: "not_an_object" }); +}); + +test("clash or uri nodes are skipped, never converted", () => { + const m = modelOf(2, { uri: true }); + const res = renderSingBox(m, null); + assert.equal(res.ok, true); + if (!res.ok) return; + const shape = res.skipped.filter((s) => s.reason === "source_not_singbox_shape"); + assert.equal(shape.length, 2); + const empty = res.skipped.filter((s) => s.reason === "empty_group"); + assert.equal(empty.length, m.groups.filter((g) => g.members.length > 0).length); + const after = JSON.parse(res.text); + assert.ok(!after.outbounds.some((o: { tag: string }) => m.nodes.some((n) => n.tag === o.tag))); +}); + +test("group with only skipped nodes emits neither group nor listener", () => { + const single = buildCoreModel(["socks5://127.0.0.1:1080 selector=solo"], [ + { name: "x", source: { kind: "uri", value: "vless://u@1.1.1.1:443#x" } }, + ] as never); + const res = renderSingBox(single, null); + assert.equal(res.ok, true); + if (!res.ok) return; + const after = JSON.parse(res.text); + assert.deepEqual(after.outbounds, []); + assert.deepEqual(after.inbounds, []); +}); + +test("pre-existing owned-prefix tag is treated as owned (documented)", () => { + const existing = JSON.stringify({ + inbounds: [ + { type: "socks", tag: `${OWNED_TAG_PREFIX}in-old`, listen: "127.0.0.1", listen_port: 1080 }, + ], + outbounds: [{ type: "selector", tag: `${OWNED_TAG_PREFIX}old`, outbounds: ["direct"] }], + }); + const res = renderSingBox(modelOf(2), existing); + assert.equal(res.ok, true); + if (!res.ok) return; + assert.ok(!res.text.includes(`${OWNED_TAG_PREFIX}old"`)); +}); + +test("functional: realistic file with 2 listeners, 2 groups, 6 nodes", () => { + const nodes = Array.from({ length: 6 }, (_, i) => ({ + name: `n${i + 1}`, + source: { + kind: "object" as const, + value: { type: "vless", server: `10.1.0.${i + 1}`, server_port: 443 }, + }, + })); + const m = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=a", "socks5://127.0.0.1:1081 selector=b"], + nodes as never + ); + const res = renderSingBox(m, realisticExisting()); + assert.equal(res.ok, true); + if (!res.ok) return; + const after = JSON.parse(res.text); + const selectors = after.outbounds.filter( + (o: { type: string; tag: string }) => + o.type === "selector" && o.tag.startsWith(OWNED_TAG_PREFIX) + ); + assert.equal(selectors.length, 2); + assert.deepEqual(after.dns, JSON.parse(realisticExisting()).dns); + assert.deepEqual(after.log, JSON.parse(realisticExisting()).log); +}); + +test("groups with only unrenderable members are reported as empty_group", () => { + const nodes = Array.from({ length: 6 }, (_, i) => ({ + name: `u${i + 1}`, + source: { kind: "uri" as const, value: `vless://u@10.2.0.${i + 1}:443#u${i + 1}` }, + })) as never[]; + const m = buildCoreModel( + [ + "socks5://127.0.0.1:1080 selector=g1", + "socks5://127.0.0.1:1081 selector=g2", + "socks5://127.0.0.1:1082 selector=g3", + ], + nodes + ); + assert.ok(m.groups.every((g) => g.members.length > 0)); + const res = renderSingBox(m, null); + assert.equal(res.ok, true); + if (!res.ok) return; + const empty = res.skipped.filter((s) => s.reason === "empty_group"); + assert.equal(empty.length, m.groups.filter((g) => g.members.length > 0).length); + assert.deepEqual(empty.map((s) => s.node).sort(), m.groups.map((g) => g.tag).sort()); +}); + +test("mixed rendered and unrenderable groups report only the unrenderable ones", () => { + const rendered = { + name: "ok", + source: { + kind: "object" as const, + value: { type: "vless", server: "10.3.0.1", server_port: 443, uuid: "u-ok" }, + }, + } as never; + const m = buildCoreModel( + ["socks5://127.0.0.1:1080 selector=solo", "socks5://127.0.0.1:1081 selector=other"], + [rendered, { name: "x", source: { kind: "uri", value: "vless://u@10.3.0.2:443#x" } } as never] + ); + const res = renderSingBox(m, null); + assert.equal(res.ok, true); + if (!res.ok) return; + const empty = res.skipped.filter((s) => s.reason === "empty_group"); + assert.ok( + empty.every((s) => + m.groups.some((g) => g.tag === s.node && g.members.every((tag) => tag !== "ok")) + ) + ); +}); + +test("fully rendered groups report no empty_group", () => { + const res = renderSingBox(modelOf(4), null); + assert.equal(res.ok, true); + if (!res.ok) return; + assert.equal(res.skipped.filter((s) => s.reason === "empty_group").length, 0); +}); + +test("default core resolves through the renderer table", () => { + assert.equal(renderers.DEFAULT_CORE, "sing-box"); + assert.equal(renderers.RENDERERS["sing-box"], renderSingBox); + assert.equal(isSingBoxShape({ type: "vless", server: "x", server_port: 1 }), true); + assert.equal(isSingBoxShape({ type: "vless", server: "x" }), false); + void objNode; +}); diff --git a/tests/unit/proxySubscription.selectorWarning.test.ts b/tests/unit/proxySubscription.selectorWarning.test.ts index 4d499b8684dc..bc470b650aec 100644 --- a/tests/unit/proxySubscription.selectorWarning.test.ts +++ b/tests/unit/proxySubscription.selectorWarning.test.ts @@ -215,3 +215,33 @@ test.after(() => { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); + +test("successful switch persists the refusal kind that fired it", async () => { + reset(); + const fake = await startFakeCore({ failPut: false }); + const { id, feedClose } = await seedWarnSubscription(fake.base); + try { + trigger.__resetSelectorTriggerForTesting(); + // The kind propagates from the caller that set the member aside (fusion + // with the set-aside record); the trigger never re-reads it here. + const good = await trigger.maybeSwitchOnSetAside(KEY_NODE1, { kind: "transport" }); + assert.equal(good.switched, true, JSON.stringify(good)); + const row = core + .getDbInstance() + .prepare( + "SELECT selector_last_switch_kind, selector_last_switch_member FROM proxy_subscriptions WHERE id = ?" + ) + .get(id) as { + selector_last_switch_kind: string | null; + selector_last_switch_member: string | null; + }; + assert.equal(row.selector_last_switch_kind, "transport"); + assert.ok(row.selector_last_switch_member, "member recorded alongside the kind"); + const listed = await sub.getSubscriptionById(id); + assert.equal(listed?.selectorLastSwitchKind, "transport"); + await sub.deleteSubscription(id); + } finally { + await feedClose(); + await fake.close(); + } +});