Merge branch 'temp-upstream-rebrand' into main
Resolves merge conflicts: - install.sh, build.sh, .github/workflows/build.yml: Reverted to Dushnilin's versions to avoid installing SourceForge mirrors. - fe-app-tachyon: Resolved type, formatting and copyOutbound conflicts, keeping both Dushnilin's custom features and ushan0v's frontend performance optimizations. - backend: Kept Dushnilin's ucode refactorings, Telegram bot integration, and release repository URL (Dushnilin/tachyon) while merging updates.uc cache format v8 & persistent cache format v7. - tests: Merged tests from both branches, preserving ucode -L wrappers for local test execution.
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
- 'tachyon/Makefile'
|
||||
- 'build.sh'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/build.yml'
|
||||
- '.github/workflows/backend-ci.yml'
|
||||
push:
|
||||
branches:
|
||||
@@ -16,6 +17,7 @@ on:
|
||||
- 'tachyon/Makefile'
|
||||
- 'build.sh'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/build.yml'
|
||||
- '.github/workflows/backend-ci.yml'
|
||||
|
||||
jobs:
|
||||
@@ -54,8 +56,4 @@ jobs:
|
||||
- name: Run backend tests
|
||||
shell: bash
|
||||
run: |
|
||||
for test_script in tests/*.sh; do
|
||||
echo "::group::$test_script"
|
||||
bash "$test_script"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
printf '%s\0' tests/*.sh | xargs -0 -n1 -P4 bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import glob from 'fast-glob';
|
||||
import { parse } from '@babel/parser';
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<<<<<<<< HEAD:fe-app-tachyon/locales/tachyon.ru.po
|
||||
# RU translations for TACHYON package.
|
||||
# Copyright (C) 2026 THE TACHYON COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the TACHYON package.
|
||||
# Dushnilin, 2026.
|
||||
========
|
||||
# RU translations for TACHYON package.
|
||||
# Copyright (C) 2026 THE TACHYON COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the TACHYON package.
|
||||
# ushan0v, 2026.
|
||||
>>>>>>>> temp-upstream-rebrand:luci-app-tachyon/po/ru/tachyon.po
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
<<<<<<<< HEAD:fe-app-tachyon/locales/tachyon.ru.po
|
||||
"POT-Creation-Date: 2026-07-13 23:43+0300\n"
|
||||
"PO-Revision-Date: 2026-07-13 23:43+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
========
|
||||
"POT-Creation-Date: 2026-07-13 01:05+0300\n"
|
||||
"PO-Revision-Date: 2026-07-13 01:05+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
>>>>>>>> temp-upstream-rebrand:luci-app-tachyon/po/ru/tachyon.po
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -77,6 +90,9 @@ msgstr "Добавьте URL или локальные пути к списка
|
||||
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
||||
|
||||
msgid "Add URLTest / Priority group"
|
||||
msgstr "Добавить URLTest / Приоритет группу"
|
||||
|
||||
msgid "Additional marking rules found"
|
||||
msgstr "Найдены дополнительные правила маркировки"
|
||||
|
||||
@@ -86,6 +102,12 @@ msgstr "Дополнительно исключать серверы по про
|
||||
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters."
|
||||
msgstr "Дополнительно скрывать серверы по протоколу, транспорту и защите. Скрывать серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||
msgstr "Дополнительно показывать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Address"
|
||||
msgstr "Адрес"
|
||||
|
||||
@@ -245,6 +267,9 @@ msgstr "Не удалось получить результаты проверк
|
||||
msgid "Cannot save server"
|
||||
msgstr "Не удалось сохранить сервер"
|
||||
|
||||
msgid "Cannot save settings"
|
||||
msgstr "Не удалось сохранить настройки"
|
||||
|
||||
msgid "Cascade connection"
|
||||
msgstr "Каскадное подключение"
|
||||
|
||||
@@ -665,6 +690,9 @@ msgstr "Исключить из этого уровня серверы с одн
|
||||
msgid "Exclude servers with one of the selected transports from this level."
|
||||
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Exclude URLTest / Priority group"
|
||||
msgstr "Исключить URLTest / Приоритет группу"
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидается: %s"
|
||||
|
||||
@@ -716,6 +744,9 @@ msgstr "Интервал поиска более быстрого сервера
|
||||
msgid "Fastest"
|
||||
msgstr "Самый быстрый"
|
||||
|
||||
msgid "Filter the servers that will be displayed on the dashboard."
|
||||
msgstr "Отфильтровать серверы, которые будут отображаться в дашборде."
|
||||
|
||||
msgid "Firewall WAN port"
|
||||
msgstr "Порт в firewall для WAN"
|
||||
|
||||
@@ -725,6 +756,66 @@ msgstr "Исправьте подсвеченные поля и сохранит
|
||||
msgid "Forced device routing"
|
||||
msgstr "Принудительная маршрутизация устройств"
|
||||
|
||||
<<<<<<<< HEAD:fe-app-tachyon/locales/tachyon.ru.po
|
||||
========
|
||||
msgid "Tachyon Error"
|
||||
msgstr "Ошибка Tachyon"
|
||||
|
||||
msgid "Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here."
|
||||
msgstr "Tachyon сам управляет pid-файлами ciadpi, поэтому параметры pidfile здесь запрещены."
|
||||
|
||||
msgid "Tachyon manages the ciadpi process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом ciadpi, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon manages the nfqws process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом nfqws, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом nfqws2, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display connections."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения подключений."
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display the dashboard."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения дашборда."
|
||||
|
||||
msgid "Tachyon Settings"
|
||||
msgstr "Настройки Tachyon"
|
||||
|
||||
msgid "Tachyon will not modify your DHCP configuration"
|
||||
msgstr "Tachyon не будет изменять вашу конфигурацию DHCP"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime has restarted"
|
||||
msgstr "ciadpi Tachyon перезапускался"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is not ready"
|
||||
msgstr "ciadpi Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is not running"
|
||||
msgstr "ciadpi Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is ready"
|
||||
msgstr "ciadpi Tachyon готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is not ready"
|
||||
msgstr "NFQWS Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is not running"
|
||||
msgstr "NFQWS Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is ready"
|
||||
msgstr "NFQWS Tachyon готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is not ready"
|
||||
msgstr "NFQWS2 Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is not running"
|
||||
msgstr "NFQWS2 Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is ready"
|
||||
msgstr "NFQWS2 Tachyon готов"
|
||||
|
||||
>>>>>>>> temp-upstream-rebrand:luci-app-tachyon/po/ru/tachyon.po
|
||||
msgid "Fronting IP"
|
||||
msgstr "IP фронтинга"
|
||||
|
||||
@@ -755,24 +846,36 @@ msgstr "gRPC-сервис"
|
||||
msgid "Handshake timeout"
|
||||
msgstr "Таймаут рукопожатия"
|
||||
|
||||
msgid "Hide added servers"
|
||||
msgstr "Скрывать добавленные серверы"
|
||||
|
||||
msgid "Hide cascade connection nodes"
|
||||
msgstr "Скрывать узлы каскадных подключений"
|
||||
|
||||
msgid "Hide dashboard servers added to Priority"
|
||||
msgstr "Скрывать серверы, добавленные в Priority, из дашборда"
|
||||
|
||||
msgid "Hide dashboard servers added to URLTest"
|
||||
msgstr "Скрывать серверы из дашборда, которые добавлены в URLTest"
|
||||
|
||||
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
||||
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
||||
|
||||
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
||||
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
||||
|
||||
msgid "Hide selected servers."
|
||||
msgstr "Скрывать выбранные серверы."
|
||||
|
||||
msgid "Hide servers from selected groups without repeating their filtering criteria."
|
||||
msgstr "Скрывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||
|
||||
msgid "Hide servers from the specified countries."
|
||||
msgstr "Скрывать серверы из указанных стран."
|
||||
|
||||
msgid "Hide servers whose names match the expression."
|
||||
msgstr "Скрывать серверы, имена которых совпадают с выражением."
|
||||
|
||||
msgid "Hide servers with one of the selected protocols."
|
||||
msgstr "Скрывать серверы с одним из выбранных протоколов."
|
||||
|
||||
msgid "Hide servers with one of the selected security types."
|
||||
msgstr "Скрывать серверы с одним из выбранных типов защиты."
|
||||
|
||||
msgid "Hide servers with one of the selected transports."
|
||||
msgstr "Скрывать серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Hide URLTest group nodes"
|
||||
msgstr "Скрывать узлы URLTest групп"
|
||||
|
||||
@@ -866,14 +969,14 @@ msgstr "Задержка при мониторинге интерфейсов"
|
||||
msgid "Interface monitoring for Bad WAN"
|
||||
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||
|
||||
msgid "Interrupt existing connections"
|
||||
msgstr "Прерывать существующие соединения"
|
||||
msgid "Interrupt connections"
|
||||
msgstr "Прерывать соединения"
|
||||
|
||||
msgid "Interrupt existing connections when priority failover switches server"
|
||||
msgstr "Разрывать существующие соединения при переключении сервера приоритетом"
|
||||
msgid "Interrupt connections when priority failover switches server"
|
||||
msgstr "Разрывать соединения при переключении сервера приоритетом"
|
||||
|
||||
msgid "Interrupt existing connections when URLTest switches the selected server"
|
||||
msgstr "Прерывать существующие соединения, когда URLTest переключает выбранный сервер"
|
||||
msgid "Interrupt connections when URLTest switches the selected server"
|
||||
msgstr "Прерывать соединения, когда URLTest переключает выбранный сервер"
|
||||
|
||||
msgid "Interval"
|
||||
msgstr "Интервал"
|
||||
@@ -1640,6 +1743,9 @@ msgstr "Выберите транзитную секцию"
|
||||
msgid "Select an existing network interface"
|
||||
msgstr "Выберите существующий сетевой интерфейс"
|
||||
|
||||
msgid "Select an existing URLTest or Priority group"
|
||||
msgstr "Выберите существующую URLTest или Приоритет группу"
|
||||
|
||||
msgid "Select an intermediate section"
|
||||
msgstr "Выберите промежуточную секцию"
|
||||
|
||||
@@ -1733,6 +1839,9 @@ msgstr "server_port должен быть числом от 1 до 65535"
|
||||
msgid "Servers"
|
||||
msgstr "Серверы"
|
||||
|
||||
msgid "Servers on dashboard"
|
||||
msgstr "Серверы в дашборде"
|
||||
|
||||
msgid "Service action failed"
|
||||
msgstr "Действие сервиса не выполнено"
|
||||
|
||||
@@ -1748,6 +1857,27 @@ msgstr "Настройки"
|
||||
msgid "Show metadata on dashboard"
|
||||
msgstr "Отображать метаданные в дашборде"
|
||||
|
||||
msgid "Show only selected servers."
|
||||
msgstr "Показывать только выбранные серверы."
|
||||
|
||||
msgid "Show only servers with one of the selected protocols."
|
||||
msgstr "Показывать только серверы с одним из выбранных протоколов."
|
||||
|
||||
msgid "Show only servers with one of the selected security types."
|
||||
msgstr "Показывать только серверы с одним из выбранных типов защиты."
|
||||
|
||||
msgid "Show only servers with one of the selected transports."
|
||||
msgstr "Показывать только серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Show servers from selected groups without repeating their filtering criteria."
|
||||
msgstr "Показывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||
|
||||
msgid "Show servers only from the specified countries."
|
||||
msgstr "Показывать серверы только из указанных стран."
|
||||
|
||||
msgid "Show servers whose names match the expression."
|
||||
msgstr "Показывать серверы, имена которых совпадают с выражением."
|
||||
|
||||
msgid "Show sing-box config"
|
||||
msgstr "Показать sing-box конфигурацию"
|
||||
|
||||
@@ -2093,6 +2223,18 @@ msgstr "Таймаут недоступности"
|
||||
msgid "Unexpected ByeDPI strategy argument: %s"
|
||||
msgstr "Неожиданный аргумент стратегии ByeDPI: %s"
|
||||
|
||||
<<<<<<<< HEAD:fe-app-tachyon/locales/tachyon.ru.po
|
||||
========
|
||||
msgid "Unexpected Tachyon-managed ciadpi runtime is running"
|
||||
msgstr "Обнаружен лишний ciadpi Tachyon"
|
||||
|
||||
msgid "Unexpected Tachyon-managed nfqws runtime is running"
|
||||
msgstr "Обнаружен лишний NFQWS Tachyon"
|
||||
|
||||
msgid "Unexpected Tachyon-managed nfqws2 runtime is running"
|
||||
msgstr "Обнаружен лишний NFQWS2 Tachyon"
|
||||
|
||||
>>>>>>>> temp-upstream-rebrand:luci-app-tachyon/po/ru/tachyon.po
|
||||
msgid "Unexpected standalone token. Use explicit flags such as --name or --name=value."
|
||||
msgstr "Неожиданный отдельный токен. Используйте явные флаги вида --имя или --имя=значение."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"format:js": "prettier --write ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"build": "tsup src/main.ts",
|
||||
"build": "tsc --noEmit && tsup src/main.ts",
|
||||
"dev": "tsup src/main.ts --watch",
|
||||
"test": "vitest",
|
||||
"ci": "npm run format && npm run lint --max-warnings=0 && npm run test -- --run && npm run build",
|
||||
|
||||
@@ -2,7 +2,6 @@ export * from './parseValueList';
|
||||
export * from './injectGlobalStyles';
|
||||
export * from './withTimeout';
|
||||
export * from './executeShellCommand';
|
||||
export * from './maskIP';
|
||||
export * from './getProxyUrlName';
|
||||
export * from './onMount';
|
||||
export * from './getClashApiUrl';
|
||||
|
||||
@@ -3,24 +3,6 @@ const COPYABLE_PROXY_URI_RE =
|
||||
const HTTP_PROXY_URI_RE =
|
||||
/^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i;
|
||||
|
||||
const COPYABLE_PROXY_OUTBOUND_TYPES = new Set([
|
||||
'vless',
|
||||
'vmess',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'ss',
|
||||
'shadowsocksr',
|
||||
'ssr',
|
||||
'hysteria2',
|
||||
'hy2',
|
||||
'tuic',
|
||||
'socks',
|
||||
'socks4',
|
||||
'socks4a',
|
||||
'socks5',
|
||||
'http',
|
||||
]);
|
||||
|
||||
export function isCopyableProxyLink(link?: string) {
|
||||
const value = (link || '').trim();
|
||||
if (COPYABLE_PROXY_URI_RE.test(value)) {
|
||||
@@ -35,7 +17,3 @@ export function isCopyableProxyLink(link?: string) {
|
||||
const port = Number(httpProxy[1]);
|
||||
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
||||
}
|
||||
|
||||
export function isCopyableProxyOutboundType(type?: string) {
|
||||
return COPYABLE_PROXY_OUTBOUND_TYPES.has((type || '').trim().toLowerCase());
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export function maskIP(ip: string = ''): string {
|
||||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
||||
|
||||
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isCopyableProxyLink,
|
||||
isCopyableProxyOutboundType,
|
||||
} from '../isCopyableProxyLink';
|
||||
import { isCopyableProxyLink } from '../isCopyableProxyLink';
|
||||
|
||||
const copyableLinks = [
|
||||
'vless://uuid@example.com:443',
|
||||
@@ -44,20 +41,3 @@ describe('isCopyableProxyLink', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCopyableProxyOutboundType', () => {
|
||||
it('accepts protocol outbound types from imported proxy links', () => {
|
||||
expect(isCopyableProxyOutboundType('VLESS')).toBe(true);
|
||||
expect(isCopyableProxyOutboundType('shadowsocks')).toBe(true);
|
||||
expect(isCopyableProxyOutboundType('hysteria2')).toBe(true);
|
||||
expect(isCopyableProxyOutboundType('socks')).toBe(true);
|
||||
expect(isCopyableProxyOutboundType('http')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects group, service and routing-only outbound types', () => {
|
||||
expect(isCopyableProxyOutboundType('urltest')).toBe(false);
|
||||
expect(isCopyableProxyOutboundType('selector')).toBe(false);
|
||||
expect(isCopyableProxyOutboundType('direct')).toBe(false);
|
||||
expect(isCopyableProxyOutboundType('block')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { maskIP } from '../maskIP';
|
||||
|
||||
export const validIPs = [
|
||||
['Standard private IP', '192.168.0.1', 'XX.XX.XX.1'],
|
||||
['Public IP', '8.8.8.8', 'XX.XX.XX.8'],
|
||||
['Mixed digits', '10.0.255.99', 'XX.XX.XX.99'],
|
||||
['Edge values', '255.255.255.255', 'XX.XX.XX.255'],
|
||||
['Zeros', '0.0.0.0', 'XX.XX.XX.0'],
|
||||
];
|
||||
|
||||
export const invalidIPs = [
|
||||
['Empty string', '', ''],
|
||||
['Missing octets', '192.168.1', '192.168.1'],
|
||||
['Extra octets', '1.2.3.4.5', '1.2.3.4.5'],
|
||||
['Letters inside', 'abc.def.ghi.jkl', 'abc.def.ghi.jkl'],
|
||||
['Spaces inside', '1. 2.3.4', '1. 2.3.4'],
|
||||
['Just dots', '...', '...'],
|
||||
['IP with port', '127.0.0.1:8080', '127.0.0.1:8080'],
|
||||
['IP with text', 'ip=192.168.0.1', 'ip=192.168.0.1'],
|
||||
];
|
||||
|
||||
describe('maskIP', () => {
|
||||
describe.each(validIPs)('Valid IPv4: %s', (_desc, ip, expected) => {
|
||||
it(`masks "${ip}" → "${expected}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPs)(
|
||||
'Invalid or malformed IP: %s',
|
||||
(_desc, ip, expected) => {
|
||||
it(`returns original string for "${ip}"`, () => {
|
||||
expect(maskIP(ip)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('defaults to empty string if no param passed', () => {
|
||||
expect(maskIP()).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse } from '@babel/parser';
|
||||
|
||||
Vendored
+2
-1
@@ -9,6 +9,7 @@ type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
|
||||
'aria-busy'?: string;
|
||||
'aria-disabled'?: string;
|
||||
'aria-label'?: string;
|
||||
'data-latency-section'?: string;
|
||||
click?: (event: MouseEvent) => void;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ declare global {
|
||||
const _ = (_key: string) => string;
|
||||
|
||||
const ui = {
|
||||
showModal: (_title: string, _content: HtmlElement) => undefined,
|
||||
showModal: (_title: string, _content: HTMLElement) => undefined,
|
||||
hideModal: () => undefined,
|
||||
addNotification: (
|
||||
_title: string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require uci';
|
||||
@@ -7,11 +7,7 @@
|
||||
if (typeof structuredClone !== 'function')
|
||||
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export {
|
||||
validateIP,
|
||||
validateIPV4,
|
||||
validateIPv6,
|
||||
} from './validators/validateIp';
|
||||
export { validateIP } from './validators/validateIp';
|
||||
export { validateDomain } from './validators/validateDomain';
|
||||
export { validateDNS } from './validators/validateDns';
|
||||
export { validateUrl } from './validators/validateUrl';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfigSections } from './getConfigSections';
|
||||
import { getConfigSections } from './getConfigSections';
|
||||
import { ClashAPI, Tachyon } from '../../types';
|
||||
import {
|
||||
canUseDirectClashApi,
|
||||
@@ -27,7 +27,6 @@ type DashboardSectionCache = {
|
||||
version?: number;
|
||||
section?: string;
|
||||
links?: Record<string, string>;
|
||||
linkRefs?: Record<string, unknown>;
|
||||
outboundMetadata?: Tachyon.GetOutboundMetadata;
|
||||
urltestGroups?: Record<string, UrlTestCacheGroup>;
|
||||
priorityGroups?: Record<string, PriorityCacheGroup>;
|
||||
@@ -70,7 +69,6 @@ type PriorityCacheGroup = {
|
||||
fastest_check_interval?: string;
|
||||
interrupt_exist_connections?: boolean;
|
||||
pin_dashboard?: boolean;
|
||||
hide_added_outbounds?: boolean;
|
||||
outbounds?: string[];
|
||||
levels?: PriorityCacheLevel[];
|
||||
};
|
||||
@@ -100,7 +98,6 @@ type UrlTestConfig = {
|
||||
displayName: string;
|
||||
settings: ItemSettings;
|
||||
pinDashboard: boolean;
|
||||
hideAddedOutbounds: boolean;
|
||||
showDetectedCountries: boolean;
|
||||
};
|
||||
|
||||
@@ -110,7 +107,6 @@ type PriorityConfig = {
|
||||
displayName: string;
|
||||
settings: ItemSettings;
|
||||
pinDashboard: boolean;
|
||||
hideAddedOutbounds: boolean;
|
||||
healthUrl: string;
|
||||
activeCheckInterval: string;
|
||||
checkTimeout: string;
|
||||
@@ -136,10 +132,6 @@ function getDisplayName(section: Tachyon.ConfigSection) {
|
||||
return section.label || section['.name'];
|
||||
}
|
||||
|
||||
function getSectionAction(section: Tachyon.ConfigSection) {
|
||||
return section.action || '';
|
||||
}
|
||||
|
||||
function getSettingsSection(configSections: Tachyon.ConfigSection[]) {
|
||||
return configSections.find((section) => section['.type'] === 'settings');
|
||||
}
|
||||
@@ -286,7 +278,6 @@ function hydrateConfigSections(configSections: Tachyon.ConfigSection[]) {
|
||||
idle_timeout: item.idle_timeout,
|
||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||
pin_dashboard: item.pin_dashboard,
|
||||
hide_added_outbounds: item.hide_added_outbounds,
|
||||
urltest_filter_mode: item.filter_mode,
|
||||
detect_server_country: item.detect_server_country,
|
||||
urltest_include_countries: item.include_countries,
|
||||
@@ -339,7 +330,6 @@ function hydrateConfigSections(configSections: Tachyon.ConfigSection[]) {
|
||||
fastest_check_interval: item.fastest_check_interval,
|
||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||
pin_dashboard: item.pin_dashboard,
|
||||
hide_added_outbounds: item.hide_added_outbounds,
|
||||
levels,
|
||||
};
|
||||
});
|
||||
@@ -364,8 +354,10 @@ function getJsonOutbounds(section: Tachyon.ConfigSection) {
|
||||
return values.length ? values : getListValues(section.outbound_json);
|
||||
}
|
||||
|
||||
function isConnectionAction(action: string) {
|
||||
return ['connection', 'proxy', 'outbound', 'vpn'].includes(action);
|
||||
function isConnectionAction(action?: string) {
|
||||
return Boolean(
|
||||
action && ['connection', 'proxy', 'outbound', 'vpn'].includes(action),
|
||||
);
|
||||
}
|
||||
|
||||
function hasSubscriptionSources(section: Tachyon.ConfigSection) {
|
||||
@@ -661,11 +653,6 @@ function getUrlTestConfigs(section: Tachyon.ConfigSection): UrlTestConfig[] {
|
||||
displayName: getUrlTestDisplayName(section, id, settings),
|
||||
settings,
|
||||
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
||||
hideAddedOutbounds: itemSettingBoolean(
|
||||
settings,
|
||||
'hide_added_outbounds',
|
||||
false,
|
||||
),
|
||||
showDetectedCountries:
|
||||
filteringEnabled &&
|
||||
itemSettingString(settings, 'detect_server_country', 'flag_emoji') ===
|
||||
@@ -748,11 +735,6 @@ function getPriorityConfigs(section: Tachyon.ConfigSection): PriorityConfig[] {
|
||||
displayName: itemSettingString(settings, 'name', id),
|
||||
settings,
|
||||
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
||||
hideAddedOutbounds: itemSettingBoolean(
|
||||
settings,
|
||||
'hide_added_outbounds',
|
||||
false,
|
||||
),
|
||||
healthUrl: itemSettingString(
|
||||
settings,
|
||||
'health_url',
|
||||
@@ -862,7 +844,6 @@ function buildUrlTestInfo({
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries,
|
||||
}: {
|
||||
code: string;
|
||||
@@ -873,7 +854,6 @@ function buildUrlTestInfo({
|
||||
manualLinkByCode: Map<string, string>;
|
||||
cachedProxyLinks: Map<string, string>;
|
||||
outboundMetadata?: Tachyon.GetOutboundMetadata;
|
||||
subscriptionCopyableCodes: Set<string>;
|
||||
showDetectedCountries: boolean;
|
||||
}): Tachyon.UrlTestInfo {
|
||||
const childCodes = uniqueCodes(
|
||||
@@ -889,8 +869,7 @@ function buildUrlTestInfo({
|
||||
manualLinkByCode.get(childCode) ||
|
||||
cachedProxyLinks.get(childCode) ||
|
||||
'';
|
||||
const canCopyLink =
|
||||
isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -900,7 +879,7 @@ function buildUrlTestInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode),
|
||||
cachedProxyLinks.has(childCode),
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
@@ -940,7 +919,6 @@ function buildPriorityInfo({
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries,
|
||||
}: {
|
||||
config: PriorityConfig;
|
||||
@@ -950,7 +928,6 @@ function buildPriorityInfo({
|
||||
manualLinkByCode: Map<string, string>;
|
||||
cachedProxyLinks: Map<string, string>;
|
||||
outboundMetadata?: Tachyon.GetOutboundMetadata;
|
||||
subscriptionCopyableCodes: Set<string>;
|
||||
showDetectedCountries: boolean;
|
||||
}): Tachyon.PriorityInfo {
|
||||
const selectedCode = entry?.value.now || '';
|
||||
@@ -1006,8 +983,7 @@ function buildPriorityInfo({
|
||||
manualLinkByCode.get(childCode) ||
|
||||
cachedProxyLinks.get(childCode) ||
|
||||
'';
|
||||
const canCopyLink =
|
||||
isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
|
||||
return {
|
||||
code: childCode,
|
||||
@@ -1016,7 +992,7 @@ function buildPriorityInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode),
|
||||
cachedProxyLinks.has(childCode),
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
@@ -1081,7 +1057,6 @@ function buildProxyGroupOutbounds(
|
||||
outboundMetadata?: Tachyon.GetOutboundMetadata,
|
||||
urltestGroups: Record<string, UrlTestCacheGroup> = {},
|
||||
priorityGroups: Record<string, PriorityCacheGroup> = {},
|
||||
subscriptionCopyableCodes: Set<string> = new Set(),
|
||||
cachedProxyLinks: Map<string, string> = new Map(),
|
||||
) {
|
||||
const sectionName = section['.name'];
|
||||
@@ -1108,31 +1083,6 @@ function buildProxyGroupOutbounds(
|
||||
const selectorCodes = selector?.value?.all ?? [];
|
||||
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
||||
const priorityCodes = priorityConfigs.map((config) => config.code);
|
||||
const urlTestCodeSet = new Set(urlTestCodes);
|
||||
const priorityCodeSet = new Set(priorityCodes);
|
||||
const hideAddedCodeSet = new Set<string>();
|
||||
urlTestEntries.forEach(({ config, entry }) => {
|
||||
if (!config.hideAddedOutbounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childCodes = urltestGroups[config.code]?.outbounds?.length
|
||||
? urltestGroups[config.code].outbounds || []
|
||||
: entry?.value.all || [];
|
||||
|
||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
||||
});
|
||||
priorityEntries.forEach(({ config, entry }) => {
|
||||
if (!config.hideAddedOutbounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childCodes = priorityGroups[config.code]?.outbounds?.length
|
||||
? priorityGroups[config.code].outbounds || []
|
||||
: entry?.value.all || [];
|
||||
|
||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
||||
});
|
||||
const showDetectedCountries =
|
||||
urlTestConfigs.some((config) => config.showDetectedCountries) ||
|
||||
priorityConfigs.some((config) => config.showDetectedCountries);
|
||||
@@ -1147,17 +1097,7 @@ function buildProxyGroupOutbounds(
|
||||
...(selectorCodes.length ? selectorCodes : fallbackCodes),
|
||||
...urlTestCodes,
|
||||
...priorityCodes,
|
||||
]).filter((code) => {
|
||||
if (!hideAddedCodeSet.has(code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
urlTestCodeSet.has(code) ||
|
||||
priorityCodeSet.has(code) ||
|
||||
isUrlTestProxyEntry(proxyByCode.get(code))
|
||||
);
|
||||
});
|
||||
]);
|
||||
|
||||
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
||||
const item = proxyByCode.get(code);
|
||||
@@ -1169,8 +1109,7 @@ function buildProxyGroupOutbounds(
|
||||
}
|
||||
|
||||
const link = manualLinkByCode.get(code) || cachedProxyLinks.get(code) || '';
|
||||
const canCopyLink =
|
||||
isCopyableProxyLink(link) || subscriptionCopyableCodes.has(code);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
const displayName =
|
||||
priorityConfig?.displayName ||
|
||||
urlTestConfig?.displayName ||
|
||||
@@ -1179,7 +1118,7 @@ function buildProxyGroupOutbounds(
|
||||
item,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(code),
|
||||
cachedProxyLinks.has(code),
|
||||
);
|
||||
const isRuntimeUrlTest = isUrlTestProxyEntry(item);
|
||||
|
||||
@@ -1207,7 +1146,6 @@ function buildProxyGroupOutbounds(
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries:
|
||||
urlTestConfig?.showDetectedCountries || showDetectedCountries,
|
||||
})
|
||||
@@ -1221,7 +1159,6 @@ function buildProxyGroupOutbounds(
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries: priorityConfig.showDetectedCountries,
|
||||
})
|
||||
: undefined,
|
||||
@@ -1402,22 +1339,6 @@ function getOutboundMetadata(dashboardCache?: DashboardSectionCache) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSubscriptionCopyableCodes(dashboardCache?: DashboardSectionCache) {
|
||||
const legacyLinks = objectMap(dashboardCache?.links);
|
||||
const linkRefs = dashboardCache?.linkRefs;
|
||||
const codes = new Set(
|
||||
Object.entries(legacyLinks)
|
||||
.filter(([, link]) => isCopyableProxyLink(link))
|
||||
.map(([code]) => code),
|
||||
);
|
||||
|
||||
if (linkRefs && typeof linkRefs === 'object' && !Array.isArray(linkRefs)) {
|
||||
Object.keys(linkRefs).forEach((code) => codes.add(code));
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
function getCachedProxyLinks(dashboardCache?: DashboardSectionCache) {
|
||||
return new Map(
|
||||
Object.entries(objectMap(dashboardCache?.links)).filter(([, link]) =>
|
||||
@@ -1451,13 +1372,12 @@ export async function getDashboardSections(
|
||||
configSections
|
||||
.filter(
|
||||
(section) =>
|
||||
section.enabled !== '0' &&
|
||||
isConnectionAction(getSectionAction(section)),
|
||||
section.enabled !== '0' && isConnectionAction(section.action),
|
||||
)
|
||||
.map(async (section) => {
|
||||
const displayName = getDisplayName(section);
|
||||
const sectionName = section['.name'];
|
||||
const sectionAction = getSectionAction(section);
|
||||
const sectionAction = section.action;
|
||||
const proxyConfigType = getSectionProxyConfigType(section);
|
||||
|
||||
if (isConnectionAction(sectionAction) && shouldUseProxyGroup(section)) {
|
||||
@@ -1472,9 +1392,6 @@ export async function getDashboardSections(
|
||||
dashboardCache,
|
||||
)
|
||||
: undefined;
|
||||
const subscriptionCopyableCodes = includeSubscriptionCopyState
|
||||
? getSubscriptionCopyableCodes(dashboardCache)
|
||||
: new Set<string>();
|
||||
const cachedProxyLinks = includeSubscriptionCopyState
|
||||
? getCachedProxyLinks(dashboardCache)
|
||||
: new Map<string, string>();
|
||||
@@ -1487,7 +1404,6 @@ export async function getDashboardSections(
|
||||
outboundMetadata,
|
||||
urltestGroups,
|
||||
priorityGroups,
|
||||
subscriptionCopyableCodes,
|
||||
cachedProxyLinks,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfigSections } from './getConfigSections';
|
||||
import { getConfigSections } from './getConfigSections';
|
||||
import { getDashboardSections } from './getDashboardSections';
|
||||
import { getClashApiSecret } from './getClashApiSecret';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getConfigSections: vi.fn(),
|
||||
@@ -87,7 +87,6 @@ function priorityGroup(
|
||||
fastest_check_interval: '3m',
|
||||
interrupt_exist_connections: '1',
|
||||
pin_dashboard: '1',
|
||||
hide_added_outbounds: '0',
|
||||
...options,
|
||||
};
|
||||
}
|
||||
@@ -423,59 +422,6 @@ describe('getDashboardSections', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides servers already added to the Priority group when enabled', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({ urltests: [], urltest_settings: undefined }),
|
||||
priorityGroup('pg_main', { hide_added_outbounds: '1' }),
|
||||
priorityLevel('pl_de'),
|
||||
]);
|
||||
mocks.getClashApiProxies.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
proxies: {
|
||||
...clashProxies,
|
||||
'main-out': proxy('Selector', {
|
||||
name: 'main-out',
|
||||
now: 'main-priority-pg_main-out',
|
||||
all: ['main-1-out', 'main-2-out', 'main-priority-pg_main-out'],
|
||||
}),
|
||||
'main-priority-pg_main-out': proxy('Selector', {
|
||||
name: 'main-priority-pg_main-out',
|
||||
now: 'main-1-out',
|
||||
all: ['main-1-out', 'main-2-out'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.fsRead.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
priorityGroups: {
|
||||
'main-priority-pg_main-out': {
|
||||
displayName: 'Codex Priority',
|
||||
outbounds: ['main-1-out', 'main-2-out'],
|
||||
levels: [
|
||||
{
|
||||
id: 'pl_de',
|
||||
displayName: 'Germany',
|
||||
order: 0,
|
||||
outbounds: ['main-1-out', 'main-2-out'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await getDashboardSections();
|
||||
const [section] = result.data;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
||||
'main-priority-pg_main-out',
|
||||
]);
|
||||
expect(section.latencyTestCodes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps an empty Priority group visible when sing-box skipped the selector', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
@@ -522,31 +468,6 @@ describe('getDashboardSections', () => {
|
||||
expect(priority?.priorityInfo?.outbounds).toEqual([]);
|
||||
});
|
||||
|
||||
it('hides servers already added to the URLTest group when enabled', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
urltest_settings: urlTestSettings('urltest', {
|
||||
display_name: 'Fastest',
|
||||
hide_added_outbounds: '1',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await getDashboardSections();
|
||||
const [section] = result.data;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(section.latencyTestCode).toBe('main-out');
|
||||
expect(section.latencyTestCodes).toEqual([
|
||||
'main-urltest-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
||||
'main-urltest-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps legacy URLTest sections compatible before migration', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
@@ -636,31 +557,6 @@ describe('getDashboardSections', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['exclude', 'include', 'mixed', 'disabled'] as const)(
|
||||
'hides added URLTest members independently from %s filter mode',
|
||||
async (urltest_filter_mode) => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
urltest_settings: urlTestSettings('urltest', {
|
||||
display_name: 'Fastest',
|
||||
urltest_filter_mode,
|
||||
hide_added_outbounds: '1',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await getDashboardSections();
|
||||
const [section] = result.data;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(section.latencyTestCode).toBe('main-out');
|
||||
expect(section.latencyTestCodes).toEqual([
|
||||
'main-urltest-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps non-built-in URLTest groups in selector order when latency sorting is disabled', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
||||
mocks.getClashApiProxies.mockResolvedValue({
|
||||
@@ -822,72 +718,6 @@ describe('getDashboardSections', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps visible URLTest groups when added leaf servers are hidden', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
urltest_settings: urlTestSettings('urltest', {
|
||||
display_name: 'Fastest',
|
||||
hide_added_outbounds: '1',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
mocks.getClashApiProxies.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
proxies: {
|
||||
'main-out': proxy('Selector', {
|
||||
name: 'main-out',
|
||||
now: 'main-1-out',
|
||||
all: [
|
||||
'main-1-out',
|
||||
'main-provider-urltest-out',
|
||||
'main-2-out',
|
||||
'main-3-out',
|
||||
'main-urltest-out',
|
||||
],
|
||||
}),
|
||||
'main-urltest-out': proxy('URLTest', {
|
||||
name: 'main-urltest-out',
|
||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 10 }],
|
||||
all: ['main-1-out', 'main-3-out'],
|
||||
}),
|
||||
'main-provider-urltest-out': proxy('URLTest', {
|
||||
name: 'Provider URLTest',
|
||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 150 }],
|
||||
all: ['provider-hidden-1-out'],
|
||||
}),
|
||||
'main-1-out': proxy('VLESS', {
|
||||
name: 'Included 1',
|
||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 100 }],
|
||||
}),
|
||||
'main-2-out': proxy('VLESS', {
|
||||
name: 'Filtered 2',
|
||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 200 }],
|
||||
}),
|
||||
'main-3-out': proxy('VLESS', {
|
||||
name: 'Included 3',
|
||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 300 }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getDashboardSections();
|
||||
const [section] = result.data;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(section.latencyTestCodes).toEqual([
|
||||
'main-urltest-out',
|
||||
'main-provider-urltest-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
||||
'main-urltest-out',
|
||||
'main-provider-urltest-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not expose flag-emoji detected countries on dashboard outbounds', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
@@ -944,7 +774,7 @@ describe('getDashboardSections', () => {
|
||||
).toBe('US');
|
||||
});
|
||||
|
||||
it('marks subscription outbounds as copyable when section cache has link refs', async () => {
|
||||
it('ignores legacy link refs without a resolved proxy link', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([
|
||||
proxySection({
|
||||
subscription_urls: ['https://subscription.example/list'],
|
||||
@@ -970,7 +800,7 @@ describe('getDashboardSections', () => {
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(subscriptionOutbound?.link).toBe('');
|
||||
expect(subscriptionOutbound?.canCopyLink).toBe(true);
|
||||
expect(subscriptionOutbound?.canCopyLink).toBe(false);
|
||||
});
|
||||
|
||||
it('uses cached subscription links without a click-time backend lookup', async () => {
|
||||
@@ -989,12 +819,6 @@ describe('getDashboardSections', () => {
|
||||
'main-2-out':
|
||||
'vless://00000000-0000-4000-8000-000000000002@example.com:443#Subscription',
|
||||
},
|
||||
linkRefs: {
|
||||
'main-2-out': {
|
||||
sourceSection: 'main-subscription-1',
|
||||
sourceIndex: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -235,25 +235,16 @@ export const TachyonShellMethods = {
|
||||
),
|
||||
getStatus: async () =>
|
||||
callBaseMethod<Tachyon.GetStatus>(Tachyon.AvailableMethods.GET_STATUS),
|
||||
getOutboundLink: async (section: string, tag: string) =>
|
||||
callBaseMethod<Tachyon.GetOutboundLink>(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_LINK,
|
||||
[section, tag],
|
||||
),
|
||||
getOutboundLinkStates: async (section: string) =>
|
||||
callBaseMethod<Tachyon.GetOutboundLinkStates>(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_LINK_STATES,
|
||||
[section],
|
||||
),
|
||||
getOutboundMetadata: async (section: string) =>
|
||||
callBaseMethod<Tachyon.GetOutboundMetadata>(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_METADATA,
|
||||
[section],
|
||||
),
|
||||
getSubscriptionMetadata: async (section: string) =>
|
||||
callBaseMethod<
|
||||
Tachyon.SubscriptionMetadata | Tachyon.SubscriptionMetadata[]
|
||||
>(Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section]),
|
||||
callBaseMethod<Tachyon.SubscriptionMetadata | Tachyon.SubscriptionMetadata[]>(
|
||||
Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA,
|
||||
[section],
|
||||
),
|
||||
checkSingBox: async () =>
|
||||
callBaseMethod<Tachyon.SingBoxCheckResult>(
|
||||
Tachyon.AvailableMethods.CHECK_SING_BOX,
|
||||
@@ -546,11 +537,7 @@ export const TachyonShellMethods = {
|
||||
) => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [
|
||||
Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC,
|
||||
component,
|
||||
action,
|
||||
],
|
||||
args: [Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC, component, action],
|
||||
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||
});
|
||||
const parsedResponse = parseComponentActionStartResult(response);
|
||||
@@ -758,8 +745,7 @@ export const TachyonShellMethods = {
|
||||
while (true) {
|
||||
await sleep(SUBSCRIPTION_UPDATE_POLL_INTERVAL_MS);
|
||||
|
||||
const response =
|
||||
await TachyonShellMethods.subscriptionUpdateStatus(jobId);
|
||||
const response = await TachyonShellMethods.subscriptionUpdateStatus(jobId);
|
||||
|
||||
if (!response.success) {
|
||||
if (transientRpc.shouldContinue(response.error)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TabServiceInstance } from './tab.service';
|
||||
import { TabServiceInstance } from './tab.service';
|
||||
import { store } from './store.service';
|
||||
import { logger } from './logger.service';
|
||||
import { TachyonLogWatcher } from './tachyonLogWatcher.service';
|
||||
|
||||
@@ -105,8 +105,8 @@ class SocketManager {
|
||||
|
||||
this.sockets.set(url, ws);
|
||||
this.connected.set(url, false);
|
||||
this.listeners.set(url, new Set());
|
||||
this.errorListeners.set(url, new Set());
|
||||
if (!this.listeners.has(url)) this.listeners.set(url, new Set());
|
||||
if (!this.errorListeners.has(url)) this.errorListeners.set(url, new Set());
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
this.connected.set(url, true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from './logger.service';
|
||||
import { logger } from './logger.service';
|
||||
|
||||
type LogFetcher = () => Promise<string> | string;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getLogNotificationKey,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { socket } from '../socket.service';
|
||||
|
||||
class FakeWebSocket {
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static instances: FakeWebSocket[] = [];
|
||||
|
||||
readyState = FakeWebSocket.CONNECTING;
|
||||
private listeners = new Map<string, Array<(event: Event) => void>>();
|
||||
|
||||
constructor(_url: string) {
|
||||
FakeWebSocket.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: (event: Event) => void) {
|
||||
const listeners = this.listeners.get(type) || [];
|
||||
listeners.push(listener);
|
||||
this.listeners.set(type, listeners);
|
||||
}
|
||||
|
||||
emit(type: string) {
|
||||
for (const listener of this.listeners.get(type) || []) {
|
||||
listener(new Event(type));
|
||||
}
|
||||
}
|
||||
|
||||
close() {}
|
||||
send() {}
|
||||
}
|
||||
|
||||
describe('socket service', () => {
|
||||
beforeEach(() => {
|
||||
socket.resetAll();
|
||||
FakeWebSocket.instances = [];
|
||||
vi.stubGlobal('WebSocket', FakeWebSocket);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
socket.resetAll();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('keeps the initial subscriber when the first connection fails', () => {
|
||||
const onError = vi.fn();
|
||||
|
||||
socket.subscribe('ws://router.test', vi.fn(), onError);
|
||||
FakeWebSocket.instances[0].emit('error');
|
||||
|
||||
expect(onError).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { TachyonLogWatcher } from '../tachyonLogWatcher.service';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { applyUiStateToStore } from '../uiState.service';
|
||||
import { store } from '../store.service';
|
||||
import { Tachyon } from '../../types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getComponentActionKey } from '../helpers/getComponentActionKey';
|
||||
import { getComponentActionKey } from '../helpers/getComponentActionKey';
|
||||
import { normalizeSingBoxVariantFields } from '../helpers/singBoxVariant';
|
||||
import type { Tachyon } from '../types';
|
||||
import { getLocalActionOverlay } from './localActionOverlay.service';
|
||||
|
||||
@@ -737,27 +737,13 @@ async function handleTestLatency(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyOutbound(
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) {
|
||||
function handleCopyOutbound(outbound: Tachyon.Outbound) {
|
||||
const link = outbound.link;
|
||||
|
||||
if (link && isCopyableProxyLink(link)) {
|
||||
copyToClipboard(link);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await TachyonShellMethods.getOutboundLink(
|
||||
section.sectionName,
|
||||
outbound.code,
|
||||
);
|
||||
|
||||
if (response.success && isCopyableProxyLink(response.data.link)) {
|
||||
copyToClipboard(response.data.link);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(_('Proxy link is unavailable'), 'error');
|
||||
}
|
||||
|
||||
@@ -899,7 +885,6 @@ function renderUrlTestCopyButton(
|
||||
|
||||
function renderCommonDetailsModal(
|
||||
info: any,
|
||||
section: Tachyon.OutboundGroup,
|
||||
fields: Array<{
|
||||
label: string;
|
||||
value?: unknown;
|
||||
@@ -1009,7 +994,7 @@ function renderCommonDetailsModal(
|
||||
member.canCopyLink
|
||||
? renderUrlTestCopyButton(_('Copy proxy link'), (event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
void handleCopyOutbound(member);
|
||||
})
|
||||
: E('span', {
|
||||
class:
|
||||
@@ -1043,10 +1028,7 @@ function renderCommonDetailsModal(
|
||||
]);
|
||||
}
|
||||
|
||||
function renderUrlTestInfoModal(
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) {
|
||||
function renderUrlTestInfoModal(outbound: Tachyon.Outbound) {
|
||||
const info = outbound.urlTestInfo;
|
||||
|
||||
if (!info) {
|
||||
@@ -1067,24 +1049,20 @@ function renderUrlTestInfoModal(
|
||||
{ label: _('Tolerance'), value: info.tolerance },
|
||||
{ label: _('Idle timeout'), value: info.idleTimeout },
|
||||
{
|
||||
label: _('Interrupt existing connections'),
|
||||
label: _('Interrupt connections'),
|
||||
value: info.interruptExistConnections,
|
||||
},
|
||||
];
|
||||
|
||||
return renderCommonDetailsModal(
|
||||
info,
|
||||
section,
|
||||
fields,
|
||||
(member) => renderDetailsMemberName(member),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
function handleShowUrlTestInfo(
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) {
|
||||
function handleShowUrlTestInfo(outbound: Tachyon.Outbound) {
|
||||
if (!outbound.urlTestInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -1093,7 +1071,7 @@ function handleShowUrlTestInfo(
|
||||
`${_('URLTest details')}: ${
|
||||
outbound.urlTestInfo.displayName || outbound.displayName
|
||||
}`,
|
||||
renderUrlTestInfoModal(section, outbound),
|
||||
renderUrlTestInfoModal(outbound),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1171,10 +1149,7 @@ function renderPriorityMemberName(member: Tachyon.PriorityMember) {
|
||||
];
|
||||
}
|
||||
|
||||
function renderPriorityInfoModal(
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) {
|
||||
function renderPriorityInfoModal(outbound: Tachyon.Outbound) {
|
||||
const info = outbound.priorityInfo;
|
||||
|
||||
if (!info) {
|
||||
@@ -1217,24 +1192,20 @@ function renderPriorityInfoModal(
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: _('Interrupt existing connections'),
|
||||
label: _('Interrupt connections'),
|
||||
value: info.interruptExistConnections,
|
||||
},
|
||||
];
|
||||
|
||||
return renderCommonDetailsModal(
|
||||
info,
|
||||
section,
|
||||
fields,
|
||||
(member) => renderPriorityMemberName(member),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function handleShowPriorityInfo(
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) {
|
||||
function handleShowPriorityInfo(outbound: Tachyon.Outbound) {
|
||||
if (!outbound.priorityInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -1243,7 +1214,7 @@ function handleShowPriorityInfo(
|
||||
`${_('Priority details')}: ${
|
||||
outbound.priorityInfo.displayName || outbound.displayName
|
||||
}`,
|
||||
renderPriorityInfoModal(section, outbound),
|
||||
renderPriorityInfoModal(outbound),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1454,14 +1425,14 @@ async function renderSectionsWidget() {
|
||||
onChooseOutbound: (sectionName, selector, tag) => {
|
||||
void handleChooseOutbound(sectionName, selector, tag);
|
||||
},
|
||||
onCopyOutbound: (section, outbound) => {
|
||||
void handleCopyOutbound(section, outbound);
|
||||
onCopyOutbound: (_section, outbound) => {
|
||||
handleCopyOutbound(outbound);
|
||||
},
|
||||
onShowUrlTestInfo: (section, outbound) => {
|
||||
handleShowUrlTestInfo(section, outbound);
|
||||
onShowUrlTestInfo: (outbound) => {
|
||||
handleShowUrlTestInfo(outbound);
|
||||
},
|
||||
onShowPriorityInfo: (section, outbound) => {
|
||||
handleShowPriorityInfo(section, outbound);
|
||||
onShowPriorityInfo: (outbound) => {
|
||||
handleShowPriorityInfo(outbound);
|
||||
},
|
||||
onUpdateSubscription: (section) => {
|
||||
void handleUpdateSubscription(section);
|
||||
|
||||
@@ -22,14 +22,8 @@ interface IRenderSectionsProps {
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) => void;
|
||||
onShowUrlTestInfo: (
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) => void;
|
||||
onShowPriorityInfo: (
|
||||
section: Tachyon.OutboundGroup,
|
||||
outbound: Tachyon.Outbound,
|
||||
) => void;
|
||||
onShowUrlTestInfo: (outbound: Tachyon.Outbound) => void;
|
||||
onShowPriorityInfo: (outbound: Tachyon.Outbound) => void;
|
||||
onUpdateSubscription: (section: Tachyon.OutboundGroup) => void;
|
||||
latencyFetching: boolean;
|
||||
latencyProgress?: Tachyon.LatencyActionProgress;
|
||||
@@ -391,7 +385,7 @@ function renderDefaultState({
|
||||
'aria-label': _('URLTest details'),
|
||||
click: (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onShowUrlTestInfo(section, outbound);
|
||||
onShowUrlTestInfo(outbound);
|
||||
},
|
||||
},
|
||||
renderInfoIcon24(),
|
||||
@@ -410,7 +404,7 @@ function renderDefaultState({
|
||||
'aria-label': _('Priority details'),
|
||||
click: (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onShowPriorityInfo(section, outbound);
|
||||
onShowPriorityInfo(outbound);
|
||||
},
|
||||
},
|
||||
renderInfoIcon24(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderSections, renderWidget } from './partials';
|
||||
import { renderSections, renderWidget } from './partials';
|
||||
|
||||
export function render() {
|
||||
return E(
|
||||
|
||||
@@ -39,15 +39,15 @@ vi.mock('../../../services/runtimeUiState.service', () => ({
|
||||
describe('dashboard initController', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalThis.E = vi
|
||||
(globalThis as any).E = vi
|
||||
.fn()
|
||||
.mockImplementation((tag) => document.createElement(tag));
|
||||
globalThis.ui = {
|
||||
(globalThis as any).ui = {
|
||||
showModal: vi.fn(),
|
||||
hideModal: vi.fn(),
|
||||
addNotification: vi.fn(),
|
||||
} as any;
|
||||
globalThis.uci = {
|
||||
(globalThis as any).uci = {
|
||||
sections: vi.fn().mockResolvedValue([]),
|
||||
} as any;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
import { TachyonShellMethods } from '../../../methods';
|
||||
import type { IDiagnosticsChecksItem } from '../../../services';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
import { TachyonShellMethods, RemoteFakeIPMethods } from '../../../methods';
|
||||
import type { IDiagnosticsChecksItem } from '../../../services';
|
||||
|
||||
+4
@@ -4,9 +4,13 @@ import { getDnsCheckPresentation } from '../getDnsCheckPresentation';
|
||||
const baseDnsResult = {
|
||||
dns_type: 'udp' as const,
|
||||
dns_server: '77.88.8.8',
|
||||
dns_server_index: 0,
|
||||
dns_server_count: 1,
|
||||
dns_status: 1 as const,
|
||||
dns_on_router: 1 as const,
|
||||
bootstrap_dns_server: '77.88.8.8',
|
||||
bootstrap_dns_server_index: 0,
|
||||
bootstrap_dns_server_count: 1,
|
||||
bootstrap_dns_status: 1 as const,
|
||||
dhcp_config_status: 0 as const,
|
||||
dont_touch_dhcp: 0 as const,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getZapretStatus: vi.fn(),
|
||||
@@ -58,7 +58,7 @@ describe('provider diagnostics checks', () => {
|
||||
|
||||
await expect(runZapretCheck()).resolves.toBeUndefined();
|
||||
|
||||
const result = mocks.updateCheckStore.mock.calls.at(-1)?.[0];
|
||||
const result = mocks.updateCheckStore.mock.calls.slice(-1)[0]?.[0];
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: 'success',
|
||||
@@ -95,7 +95,7 @@ describe('provider diagnostics checks', () => {
|
||||
|
||||
await expect(runZapretCheck()).resolves.toBeUndefined();
|
||||
|
||||
const result = mocks.updateCheckStore.mock.calls.at(-1)?.[0];
|
||||
const result = mocks.updateCheckStore.mock.calls.slice(-1)[0]?.[0];
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: 'error',
|
||||
@@ -122,7 +122,7 @@ describe('provider diagnostics checks', () => {
|
||||
|
||||
await expect(runZapret2Check()).resolves.toBeUndefined();
|
||||
|
||||
const result = mocks.updateCheckStore.mock.calls.at(-1)?.[0];
|
||||
const result = mocks.updateCheckStore.mock.calls.slice(-1)[0]?.[0];
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: 'success',
|
||||
@@ -171,7 +171,7 @@ describe('provider diagnostics checks', () => {
|
||||
|
||||
await expect(runByedpiCheck()).resolves.toBeUndefined();
|
||||
|
||||
const result = mocks.updateCheckStore.mock.calls.at(-1)?.[0];
|
||||
const result = mocks.updateCheckStore.mock.calls.slice(-1)[0]?.[0];
|
||||
|
||||
expect(result).toMatchObject({
|
||||
state: 'success',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getDashboardSections: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import {
|
||||
DIAGNOSTICS_CHECKS,
|
||||
DIAGNOSTICS_CHECKS_MAP,
|
||||
} from './checks/contstants';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StoreType } from '../../services';
|
||||
import type { StoreType } from '../../services';
|
||||
import type { DiagnosticsProviderOptions } from './diagnostic.store';
|
||||
import { DIAGNOSTICS_CHECKS } from './checks/contstants';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const MASKED_VALUE = 'MASKED';
|
||||
const MASKED_VALUE = 'MASKED';
|
||||
|
||||
const SING_BOX_MASKED_KEYS = new Set([
|
||||
'auth_key',
|
||||
@@ -30,6 +30,7 @@ const SING_BOX_MASKED_KEYS = new Set([
|
||||
|
||||
const TACHYON_MASK_AFTER_TOKEN = [
|
||||
'option proxy_string',
|
||||
'option hwid',
|
||||
'option subscription_url',
|
||||
'list subscription_urls',
|
||||
'list urltest_proxy_links',
|
||||
|
||||
@@ -764,8 +764,7 @@ async function handleShowSingBoxConfig() {
|
||||
setDiagnosticActionLoading('showSingBoxConfig', true);
|
||||
|
||||
try {
|
||||
const showSingBoxConfig =
|
||||
await TachyonShellMethods.showSingBoxConfig(false);
|
||||
const showSingBoxConfig = await TachyonShellMethods.showSingBoxConfig(false);
|
||||
|
||||
if (showSingBoxConfig.success) {
|
||||
const rawSingBoxConfigText = stringifySingBoxConfig(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderButton } from '../../../../partials';
|
||||
import { renderButton } from '../../../../partials';
|
||||
import {
|
||||
renderCircleCheckBigIcon24,
|
||||
renderCirclePlayIcon24,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type LoadingActionState = {
|
||||
type LoadingActionState = {
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
clearPersistedDiagnosticRun,
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('diagnostic masking', () => {
|
||||
const raw = [
|
||||
"config section 'main'",
|
||||
"\toption proxy_string 'vless://secret@example.com:443'",
|
||||
"\toption hwid 'device-secret'",
|
||||
"\tlist domain 'example.com'",
|
||||
"\toption outbound_json '{",
|
||||
' "server": "example.com",',
|
||||
@@ -77,6 +78,7 @@ describe('diagnostic masking', () => {
|
||||
|
||||
expect(masked.split('\n')).toHaveLength(raw.split('\n').length);
|
||||
expect(masked).not.toContain('vless://secret');
|
||||
expect(masked).not.toContain('device-secret');
|
||||
expect(masked).not.toContain('example.com",');
|
||||
expect(masked).not.toContain('192.168.1.1');
|
||||
expect(masked).not.toContain('provider-password');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getAvailableActionsDisabledState,
|
||||
getServiceTransition,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { canUseDirectClashApi, getClashWsUrl, onMount } from '../../../helpers';
|
||||
import { canUseDirectClashApi, getClashWsUrl, onMount } from '../../../helpers';
|
||||
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||
import { showToast } from '../../../helpers/showToast';
|
||||
import {
|
||||
|
||||
@@ -532,9 +532,7 @@ async function completeComponentActionJob(
|
||||
});
|
||||
}
|
||||
|
||||
async function followComponentActionState(
|
||||
state: Tachyon.ComponentActionResult,
|
||||
) {
|
||||
async function followComponentActionState(state: Tachyon.ComponentActionResult) {
|
||||
const jobId = state.job_id;
|
||||
const key = getComponentActionKey(state.component, state.action);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ClashAPI {
|
||||
interface ProxyHistoryEntry {
|
||||
time: string;
|
||||
@@ -60,8 +60,6 @@ export namespace Tachyon {
|
||||
CHECK_BYEDPI_RUNTIME = 'check_byedpi_runtime',
|
||||
CHECK_INBOUNDS_CONFIG = 'check_inbounds_config',
|
||||
GET_STATUS = 'get_status',
|
||||
GET_OUTBOUND_LINK = 'get_outbound_link',
|
||||
GET_OUTBOUND_LINK_STATES = 'get_outbound_link_states',
|
||||
GET_OUTBOUND_METADATA = 'get_outbound_metadata',
|
||||
GET_SUBSCRIPTION_METADATA = 'get_subscription_metadata',
|
||||
CHECK_SING_BOX = 'check_sing_box',
|
||||
@@ -268,6 +266,24 @@ export namespace Tachyon {
|
||||
urltest_settings?: string;
|
||||
priority_groups?: string[];
|
||||
priority_group_settings?: string;
|
||||
dashboard_filter_mode?: 'disabled' | 'exclude' | 'include' | 'mixed';
|
||||
dashboard_detect_server_country?: 'flag_emoji' | 'country_is';
|
||||
dashboard_include_countries?: string[];
|
||||
dashboard_include_outbounds?: string[];
|
||||
dashboard_include_regex?: string[];
|
||||
dashboard_include_proxy_parameters?: '0' | '1';
|
||||
dashboard_include_protocols?: string[];
|
||||
dashboard_include_transports?: string[];
|
||||
dashboard_include_securities?: string[];
|
||||
dashboard_include_groups?: string[];
|
||||
dashboard_exclude_countries?: string[];
|
||||
dashboard_exclude_outbounds?: string[];
|
||||
dashboard_exclude_regex?: string[];
|
||||
dashboard_exclude_proxy_parameters?: '0' | '1';
|
||||
dashboard_exclude_protocols?: string[];
|
||||
dashboard_exclude_transports?: string[];
|
||||
dashboard_exclude_securities?: string[];
|
||||
dashboard_exclude_groups?: string[];
|
||||
urltest_proxy_links?: string[];
|
||||
subscription_url?: string;
|
||||
subscription_user_agent?: string;
|
||||
@@ -299,7 +315,6 @@ export namespace Tachyon {
|
||||
idle_timeout?: string;
|
||||
interrupt_exist_connections?: '0' | '1';
|
||||
pin_dashboard?: '0' | '1';
|
||||
hide_added_outbounds?: '0' | '1';
|
||||
filter_mode?: 'disabled' | 'exclude' | 'include' | 'mixed';
|
||||
include_countries?: string[];
|
||||
include_outbounds?: string[];
|
||||
@@ -438,12 +453,6 @@ export namespace Tachyon {
|
||||
dns_configured?: number;
|
||||
}
|
||||
|
||||
export interface GetOutboundLink {
|
||||
link: string;
|
||||
}
|
||||
|
||||
export type GetOutboundLinkStates = Record<string, boolean>;
|
||||
|
||||
export interface GetOutboundMetadata {
|
||||
names?: Record<string, string>;
|
||||
countries?: Record<string, string>;
|
||||
@@ -623,6 +632,8 @@ export namespace Tachyon {
|
||||
legacy_runtime_present: 0 | 1;
|
||||
ready: 0 | 1;
|
||||
conflict: 0 | 1;
|
||||
outbounds_configured: 0 | 1;
|
||||
routes_configured: 0 | 1;
|
||||
status_message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ export function isValidHost(host: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidPort(port: unknown): boolean {
|
||||
const normalized = String(port ?? '');
|
||||
const value = Number(normalized);
|
||||
return /^\d+$/.test(normalized) && value >= 1 && value <= 65535;
|
||||
}
|
||||
|
||||
export function parseHostPort(value: string): ParsedHostPort | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -35,10 +41,11 @@ export function parseHostPort(value: string): ParsedHostPort | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const parsed = {
|
||||
host: value.slice(1, end),
|
||||
port: value.slice(end + 2),
|
||||
};
|
||||
return isValidHost(parsed.host) ? parsed : null;
|
||||
}
|
||||
|
||||
const firstColon = value.indexOf(':');
|
||||
@@ -47,8 +54,9 @@ export function parseHostPort(value: string): ParsedHostPort | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const parsed = {
|
||||
host: value.slice(0, firstColon),
|
||||
port: value.slice(firstColon + 1),
|
||||
};
|
||||
return isValidHost(parsed.host) ? parsed : null;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,13 @@ export const additionalValidDns = [
|
||||
|
||||
const validDns = [...validIPs, ...validDomains, ...additionalValidDns];
|
||||
|
||||
const invalidDns = [...invalidIPs, ...invalidDomains];
|
||||
const invalidDns = [
|
||||
...invalidIPs,
|
||||
...invalidDomains,
|
||||
['Non-numeric port', '8.8.8.8:abc'],
|
||||
['Port too high', 'dns.example.com:99999'],
|
||||
['Zero IPv6 port', '[2606:4700:4700::1111]:0'],
|
||||
];
|
||||
|
||||
describe('validateDns', () => {
|
||||
describe.each(validDns)('Valid dns: %s', (_desc, domain) => {
|
||||
|
||||
@@ -46,6 +46,7 @@ const invalidUrls = [
|
||||
['Missing password', 'hysteria2://@example.com:443/'],
|
||||
['Missing host', 'hysteria2://pw@:443/'],
|
||||
['Missing port', 'hysteria2://pw@example.com/'],
|
||||
['Invalid host', 'hysteria2://pw@bad_host:443/'],
|
||||
['Non-numeric port', 'hysteria2://pw@example.com:port/'],
|
||||
['Port out of range', 'hysteria2://pw@example.com:99999/'],
|
||||
['Invalid port range order', 'hysteria2://pw@example.com:6000-5000/'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateIP, validateIPV4, validateIPv6 } from '../validateIp';
|
||||
import { validateIP } from '../validateIp';
|
||||
|
||||
export const validIPs = [
|
||||
['Private LAN', '192.168.1.1'],
|
||||
@@ -21,23 +21,23 @@ export const invalidIPs = [
|
||||
['Trailing dot', '1.2.3.'],
|
||||
];
|
||||
|
||||
describe('validateIPV4', () => {
|
||||
describe('validateIP IPv4', () => {
|
||||
describe.each(validIPs)('Valid IP: %s', (_desc, ip) => {
|
||||
it(`returns {valid:true} for "${ip}"`, () => {
|
||||
const res = validateIPV4(ip);
|
||||
const res = validateIP(ip);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPs)('Invalid IP: %s', (_desc, ip) => {
|
||||
it(`returns {valid:false} for "${ip}"`, () => {
|
||||
const res = validateIPV4(ip);
|
||||
const res = validateIP(ip);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateIPv6', () => {
|
||||
describe('validateIP IPv6', () => {
|
||||
const validIPv6Addresses = [
|
||||
['Loopback', '::1'],
|
||||
['ULA', 'fc00::1'],
|
||||
@@ -53,14 +53,13 @@ describe('validateIPv6', () => {
|
||||
|
||||
describe.each(validIPv6Addresses)('Valid IPv6: %s', (_desc, ip) => {
|
||||
it(`returns {valid:true} for "${ip}"`, () => {
|
||||
expect(validateIPv6(ip).valid).toBe(true);
|
||||
expect(validateIP(ip).valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPv6Addresses)('Invalid IPv6: %s', (_desc, ip) => {
|
||||
it(`returns {valid:false} for "${ip}"`, () => {
|
||||
expect(validateIPv6(ip).valid).toBe(false);
|
||||
expect(validateIP(ip).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,4 +67,12 @@ describe('validateProxyUrl', () => {
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toContain('path');
|
||||
});
|
||||
|
||||
it('rejects VMess configs with an invalid server host', () => {
|
||||
const config = Buffer.from(
|
||||
JSON.stringify({ add: 'bad_host', port: '443', id: 'uuid' }),
|
||||
).toString('base64');
|
||||
|
||||
expect(validateProxyUrl(`vmess://${config}`).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ const invalidUrls = [
|
||||
['No prefix', 'uuid@127.0.0.1:443?type=tcp'],
|
||||
['No host', 'ss://password@:443?type=tcp'],
|
||||
['No port', 'ss://password@127.0.0.1?type=tcp'],
|
||||
['Invalid host', 'ss://password@bad_host:443?type=tcp'],
|
||||
['Invalid port', 'ss://password@127.0.0.1:abc?type=tcp'],
|
||||
['Missing type', 'ss://password@127.0.0.1:443'],
|
||||
['Contains space', 'ss://password@127.0.0.1:443?type=tcp #extra'],
|
||||
|
||||
@@ -104,6 +104,7 @@ const invalidUrls = [
|
||||
['No password', 'trojan://@127.0.0.1:443?type=tcp&security=tls'],
|
||||
['No host', 'trojan://pass@:443?type=tcp&security=tls'],
|
||||
['No port', 'trojan://pass@127.0.0.1?type=tcp&security=tls'],
|
||||
['Invalid host', 'trojan://pass@bad_host:443?type=tcp&security=tls'],
|
||||
['Invalid port', 'trojan://pass@127.0.0.1:abc?type=tcp&security=tls'],
|
||||
[
|
||||
'Unbracketed IPv6 with port',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateUrl } from '../validateUrl';
|
||||
|
||||
const validUrls = [
|
||||
|
||||
@@ -67,6 +67,7 @@ const invalidUrls = [
|
||||
['No uuid', 'vless://@127.0.0.1:443?type=tcp&security=tls'],
|
||||
['No host', 'vless://uuid@:443?type=tcp&security=tls'],
|
||||
['No port', 'vless://uuid@127.0.0.1?type=tcp&security=tls'],
|
||||
['Invalid host', 'vless://uuid@bad_host:443?type=tcp&security=tls'],
|
||||
['Invalid port', 'vless://uuid@127.0.0.1:abc?type=tcp&security=tls'],
|
||||
[
|
||||
'Unbracketed IPv6 with port',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { validateDomain } from './validateDomain';
|
||||
import { parseHostPort, unbracketHost } from './hostPort';
|
||||
import { isValidPort, parseHostPort, unbracketHost } from './hostPort';
|
||||
import { validateIP } from './validateIp';
|
||||
import { ValidationResult } from './types';
|
||||
|
||||
@@ -17,6 +17,10 @@ export function validateDNS(value: string): ValidationResult {
|
||||
? host + (pathParts.length > 0 ? `/${pathParts.join('/')}` : '')
|
||||
: value.replace(/:(\d+)(?=\/|$)/, '');
|
||||
|
||||
if (parsedHostPort && !isValidPort(parsedHostPort.port)) {
|
||||
return { valid: false, message: _('Invalid DNS server port') };
|
||||
}
|
||||
|
||||
if (validateIP(host).valid) {
|
||||
return { valid: true, message: _('Valid') };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { isValidHost, parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
export function validateHttpProxyUrl(url: string): ValidationResult {
|
||||
try {
|
||||
@@ -73,20 +73,12 @@ export function validateHttpProxyUrl(url: string): ValidationResult {
|
||||
};
|
||||
}
|
||||
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HTTP proxy URL: invalid port number'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HTTP proxy URL: invalid host format'),
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
valid: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { parseQueryString } from '../helpers/parseQueryString';
|
||||
import { parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
export function validateHysteria2Url(url: string): ValidationResult {
|
||||
try {
|
||||
@@ -64,22 +64,13 @@ export function validateHysteria2Url(url: string): ValidationResult {
|
||||
const cleanedPort = port.replace('/', '');
|
||||
const portEntries = cleanedPort.split(',');
|
||||
|
||||
const isValidPortNumber = (value: string) => {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const portNum = Number(value);
|
||||
return Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535;
|
||||
};
|
||||
|
||||
const isValidPortEntry = (entry: string) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entry.includes('-')) {
|
||||
return isValidPortNumber(entry);
|
||||
return isValidPort(entry);
|
||||
}
|
||||
|
||||
const rangeParts = entry.split('-');
|
||||
@@ -89,9 +80,7 @@ export function validateHysteria2Url(url: string): ValidationResult {
|
||||
|
||||
const [start, end] = rangeParts;
|
||||
return (
|
||||
isValidPortNumber(start) &&
|
||||
isValidPortNumber(end) &&
|
||||
Number(start) <= Number(end)
|
||||
isValidPort(start) && isValidPort(end) && Number(start) <= Number(end)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,22 +20,6 @@ export function isIPv6(ip: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function validateIPV4(ip: string): ValidationResult {
|
||||
if (isIPv4(ip)) {
|
||||
return { valid: true, message: _('Valid') };
|
||||
}
|
||||
|
||||
return { valid: false, message: _('Invalid IP address') };
|
||||
}
|
||||
|
||||
export function validateIPv6(ip: string): ValidationResult {
|
||||
if (isIPv6(ip)) {
|
||||
return { valid: true, message: _('Valid') };
|
||||
}
|
||||
|
||||
return { valid: false, message: _('Invalid IP address') };
|
||||
}
|
||||
|
||||
export function validateIP(ip: string): ValidationResult {
|
||||
if (isIPv4(ip) || isIPv6(ip)) {
|
||||
return { valid: true, message: _('Valid') };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
// TODO refactor current validation and add tests
|
||||
export function validateShadowsocksUrl(url: string): ValidationResult {
|
||||
@@ -85,9 +85,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
||||
};
|
||||
}
|
||||
|
||||
const portNum = parseInt(port, 10);
|
||||
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid port number. Must be between 1 and 65535'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { isValidHost, parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
export function validateSocksUrl(url: string): ValidationResult {
|
||||
try {
|
||||
@@ -62,20 +62,12 @@ export function validateSocksUrl(url: string): ValidationResult {
|
||||
return { valid: false, message: _('Invalid SOCKS URL: missing port') };
|
||||
}
|
||||
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid SOCKS URL: invalid port number'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid SOCKS URL: invalid host format'),
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
return { valid: false, message: _('Invalid SOCKS URL: parsing failed') };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
export function validateTrojanUrl(url: string): ValidationResult {
|
||||
try {
|
||||
@@ -52,9 +52,7 @@ export function validateTrojanUrl(url: string): ValidationResult {
|
||||
if (!port)
|
||||
return { valid: false, message: 'Invalid Trojan URL: missing port' };
|
||||
|
||||
const portNum = Number(port);
|
||||
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||
if (!isValidPort(port))
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid Trojan URL: invalid port number',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { isValidHost } from './hostPort';
|
||||
import { isValidHost, isValidPort } from './hostPort';
|
||||
|
||||
export function validateUrl(
|
||||
url: string,
|
||||
@@ -25,11 +25,9 @@ export function validateUrl(
|
||||
const host = parsed.hostname.startsWith('[')
|
||||
? parsed.hostname.slice(1, -1)
|
||||
: parsed.hostname;
|
||||
const portNum = parsed.port ? Number(parsed.port) : 0;
|
||||
if (
|
||||
(isValidHost(host) || host === 'localhost') &&
|
||||
(!parsed.port ||
|
||||
(Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535))
|
||||
(!parsed.port || isValidPort(parsed.port))
|
||||
) {
|
||||
return { valid: true, message: _('Valid') };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { parseQueryString } from '../helpers/parseQueryString';
|
||||
import { parseHostPort } from './hostPort';
|
||||
import { isValidPort, parseHostPort } from './hostPort';
|
||||
|
||||
export function validateVlessUrl(url: string): ValidationResult {
|
||||
try {
|
||||
@@ -51,8 +51,7 @@ export function validateVlessUrl(url: string): ValidationResult {
|
||||
return { valid: false, message: 'Invalid VLESS URL: missing port' };
|
||||
|
||||
const cleanedPort = port.replace('/', '');
|
||||
const portNum = Number(cleanedPort);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||
if (!isValidPort(cleanedPort))
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid VLESS URL: invalid port number',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { isValidHost, isValidPort } from './hostPort';
|
||||
|
||||
function decodeBase64Json(value: string): unknown {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
@@ -47,12 +48,11 @@ export function validateVmessUrl(url: string): ValidationResult {
|
||||
}
|
||||
|
||||
const { add, port, id } = config as Record<string, unknown>;
|
||||
if (!add || typeof add !== 'string') {
|
||||
return { valid: false, message: 'Invalid VMess URL: missing server' };
|
||||
if (!add || typeof add !== 'string' || !isValidHost(add)) {
|
||||
return { valid: false, message: 'Invalid VMess URL: invalid server' };
|
||||
}
|
||||
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid VMess URL: invalid port number',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { defineConfig } from 'tsup';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
|
||||
@@ -12,27 +12,37 @@ const config = {
|
||||
port: Number(process.env.SFTP_PORT || 22),
|
||||
username: process.env.SFTP_USER,
|
||||
...(process.env.SFTP_PRIVATE_KEY
|
||||
? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) }
|
||||
: { password: process.env.SFTP_PASS }),
|
||||
? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) }
|
||||
: { password: process.env.SFTP_PASS }),
|
||||
};
|
||||
|
||||
const syncDirs = [
|
||||
{
|
||||
local: path.resolve(process.env.LOCAL_DIR_FE ?? '../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon'),
|
||||
remote: process.env.REMOTE_DIR_FE ?? '/www/luci-static/resources/view/tachyon',
|
||||
local: path.resolve(
|
||||
process.env.LOCAL_DIR_FE ??
|
||||
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon',
|
||||
),
|
||||
remote:
|
||||
process.env.REMOTE_DIR_FE ?? '/www/luci-static/resources/view/tachyon',
|
||||
},
|
||||
{
|
||||
local: path.resolve(process.env.LOCAL_DIR_BIN ?? '../tachyon/files/usr/bin/'),
|
||||
local: path.resolve(
|
||||
process.env.LOCAL_DIR_BIN ?? '../tachyon/files/usr/bin/',
|
||||
),
|
||||
remote: process.env.REMOTE_DIR_BIN ?? '/usr/bin/',
|
||||
},
|
||||
{
|
||||
local: path.resolve(process.env.LOCAL_DIR_LIB ?? '../tachyon/files/usr/lib/'),
|
||||
local: path.resolve(
|
||||
process.env.LOCAL_DIR_LIB ?? '../tachyon/files/usr/lib/',
|
||||
),
|
||||
remote: process.env.REMOTE_DIR_LIB ?? '/usr/lib/tachyon/',
|
||||
},
|
||||
{
|
||||
local: path.resolve(process.env.LOCAL_DIR_INIT ?? '../tachyon/files/etc/init.d/'),
|
||||
local: path.resolve(
|
||||
process.env.LOCAL_DIR_INIT ?? '../tachyon/files/etc/init.d/',
|
||||
),
|
||||
remote: process.env.REMOTE_DIR_INIT ?? '/etc/init.d/',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
async function uploadFile(filePath, baseDir, remoteBase) {
|
||||
@@ -79,13 +89,15 @@ async function main() {
|
||||
await uploadAllFiles();
|
||||
|
||||
for (const { local, remote } of syncDirs) {
|
||||
chokidar.watch(local, { ignoreInitial: true }).on('all', async (event, filePath) => {
|
||||
if (event === 'add' || event === 'change') {
|
||||
await uploadFile(filePath, local, remote);
|
||||
} else if (event === 'unlink') {
|
||||
await deleteFile(filePath, local, remote);
|
||||
}
|
||||
});
|
||||
chokidar
|
||||
.watch(local, { ignoreInitial: true })
|
||||
.on('all', async (event, filePath) => {
|
||||
if (event === 'add' || event === 'change') {
|
||||
await uploadFile(filePath, local, remote);
|
||||
} else if (event === 'unlink') {
|
||||
await deleteFile(filePath, local, remote);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
@@ -96,5 +108,6 @@ async function main() {
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.exitCode = 1;
|
||||
console.error('💥 Fatal:', err);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require ui";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require ui";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require rpc";
|
||||
"require ui";
|
||||
|
||||
@@ -21,18 +21,6 @@ function isIPv6(ip) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function validateIPV4(ip) {
|
||||
if (isIPv4(ip)) {
|
||||
return { valid: true, message: _("Valid") };
|
||||
}
|
||||
return { valid: false, message: _("Invalid IP address") };
|
||||
}
|
||||
function validateIPv6(ip) {
|
||||
if (isIPv6(ip)) {
|
||||
return { valid: true, message: _("Valid") };
|
||||
}
|
||||
return { valid: false, message: _("Invalid IP address") };
|
||||
}
|
||||
function validateIP(ip) {
|
||||
if (isIPv4(ip) || isIPv6(ip)) {
|
||||
return { valid: true, message: _("Valid") };
|
||||
@@ -104,6 +92,11 @@ function isValidHost(host) {
|
||||
const normalizedHost = unbracketHost(host);
|
||||
return isIPv4(normalizedHost) || isIPv6(normalizedHost) || validateDomain(normalizedHost).valid;
|
||||
}
|
||||
function isValidPort(port) {
|
||||
const normalized = String(port ?? "");
|
||||
const value = Number(normalized);
|
||||
return /^\d+$/.test(normalized) && value >= 1 && value <= 65535;
|
||||
}
|
||||
function parseHostPort(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -113,20 +106,22 @@ function parseHostPort(value) {
|
||||
if (end <= 1 || value.slice(end + 1, end + 2) !== ":") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
const parsed2 = {
|
||||
host: value.slice(1, end),
|
||||
port: value.slice(end + 2)
|
||||
};
|
||||
return isValidHost(parsed2.host) ? parsed2 : null;
|
||||
}
|
||||
const firstColon = value.indexOf(":");
|
||||
const lastColon = value.lastIndexOf(":");
|
||||
if (firstColon < 0 || firstColon !== lastColon) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
const parsed = {
|
||||
host: value.slice(0, firstColon),
|
||||
port: value.slice(firstColon + 1)
|
||||
};
|
||||
return isValidHost(parsed.host) ? parsed : null;
|
||||
}
|
||||
|
||||
// src/validators/validateDns.ts
|
||||
@@ -138,6 +133,9 @@ function validateDNS(value) {
|
||||
const parsedHostPort = parseHostPort(addressPart);
|
||||
const host = parsedHostPort ? parsedHostPort.host : unbracketHost(addressPart);
|
||||
const domainValue = parsedHostPort ? host + (pathParts.length > 0 ? `/${pathParts.join("/")}` : "") : value.replace(/:(\d+)(?=\/|$)/, "");
|
||||
if (parsedHostPort && !isValidPort(parsedHostPort.port)) {
|
||||
return { valid: false, message: _("Invalid DNS server port") };
|
||||
}
|
||||
if (validateIP(host).valid) {
|
||||
return { valid: true, message: _("Valid") };
|
||||
}
|
||||
@@ -166,8 +164,7 @@ function validateUrl(url, protocols = ["http:", "https:"]) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname.startsWith("[") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
||||
const portNum = parsed.port ? Number(parsed.port) : 0;
|
||||
if ((isValidHost(host) || host === "localhost") && (!parsed.port || Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535)) {
|
||||
if ((isValidHost(host) || host === "localhost") && (!parsed.port || isValidPort(parsed.port))) {
|
||||
return { valid: true, message: _("Valid") };
|
||||
}
|
||||
} catch (_e) {
|
||||
@@ -396,8 +393,7 @@ function validateShadowsocksUrl(url) {
|
||||
message: _("Invalid Shadowsocks URL: missing port")
|
||||
};
|
||||
}
|
||||
const portNum = parseInt(port, 10);
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid port number. Must be between 1 and 65535")
|
||||
@@ -467,8 +463,7 @@ function validateVlessUrl(url) {
|
||||
if (!port)
|
||||
return { valid: false, message: "Invalid VLESS URL: missing port" };
|
||||
const cleanedPort = port.replace("/", "");
|
||||
const portNum = Number(cleanedPort);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||
if (!isValidPort(cleanedPort))
|
||||
return {
|
||||
valid: false,
|
||||
message: "Invalid VLESS URL: invalid port number"
|
||||
@@ -566,11 +561,10 @@ function validateVmessUrl(url) {
|
||||
return { valid: false, message: "Invalid VMess URL: invalid config" };
|
||||
}
|
||||
const { add, port, id } = config;
|
||||
if (!add || typeof add !== "string") {
|
||||
return { valid: false, message: "Invalid VMess URL: missing server" };
|
||||
if (!add || typeof add !== "string" || !isValidHost(add)) {
|
||||
return { valid: false, message: "Invalid VMess URL: invalid server" };
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Invalid VMess URL: invalid port number"
|
||||
@@ -627,8 +621,7 @@ function validateTrojanUrl(url) {
|
||||
return { valid: false, message: "Invalid Trojan URL: missing hostname" };
|
||||
if (!port)
|
||||
return { valid: false, message: "Invalid Trojan URL: missing port" };
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||
if (!isValidPort(port))
|
||||
return {
|
||||
valid: false,
|
||||
message: "Invalid Trojan URL: invalid port number"
|
||||
@@ -691,19 +684,12 @@ function validateSocksUrl(url) {
|
||||
if (!port) {
|
||||
return { valid: false, message: _("Invalid SOCKS URL: missing port") };
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid SOCKS URL: invalid port number")
|
||||
};
|
||||
}
|
||||
if (!isValidHost(host)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid SOCKS URL: invalid host format")
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
return { valid: false, message: _("Invalid SOCKS URL: parsing failed") };
|
||||
}
|
||||
@@ -760,26 +746,19 @@ function validateHysteria2Url(url) {
|
||||
}
|
||||
const cleanedPort = port.replace("/", "");
|
||||
const portEntries = cleanedPort.split(",");
|
||||
const isValidPortNumber = (value) => {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const portNum = Number(value);
|
||||
return Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535;
|
||||
};
|
||||
const isValidPortEntry = (entry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
if (!entry.includes("-")) {
|
||||
return isValidPortNumber(entry);
|
||||
return isValidPort(entry);
|
||||
}
|
||||
const rangeParts = entry.split("-");
|
||||
if (rangeParts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
const [start, end] = rangeParts;
|
||||
return isValidPortNumber(start) && isValidPortNumber(end) && Number(start) <= Number(end);
|
||||
return isValidPort(start) && isValidPort(end) && Number(start) <= Number(end);
|
||||
};
|
||||
if (!portEntries.every(isValidPortEntry)) {
|
||||
return {
|
||||
@@ -894,19 +873,12 @@ function validateHttpProxyUrl(url) {
|
||||
message: _("Invalid HTTP proxy URL: missing port")
|
||||
};
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
if (!isValidPort(port)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HTTP proxy URL: invalid port number")
|
||||
};
|
||||
}
|
||||
if (!isValidHost(host)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HTTP proxy URL: invalid host format")
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -2167,7 +2139,7 @@ function renderDefaultState({
|
||||
"aria-label": _("URLTest details"),
|
||||
click: (event) => {
|
||||
event.stopPropagation();
|
||||
onShowUrlTestInfo(section, outbound);
|
||||
onShowUrlTestInfo(outbound);
|
||||
}
|
||||
},
|
||||
renderInfoIcon24()
|
||||
@@ -2183,7 +2155,7 @@ function renderDefaultState({
|
||||
"aria-label": _("Priority details"),
|
||||
click: (event) => {
|
||||
event.stopPropagation();
|
||||
onShowPriorityInfo(section, outbound);
|
||||
onShowPriorityInfo(outbound);
|
||||
}
|
||||
},
|
||||
renderInfoIcon24()
|
||||
@@ -2576,8 +2548,6 @@ var Tachyon;
|
||||
AvailableMethods2["CHECK_BYEDPI_RUNTIME"] = "check_byedpi_runtime";
|
||||
AvailableMethods2["CHECK_INBOUNDS_CONFIG"] = "check_inbounds_config";
|
||||
AvailableMethods2["GET_STATUS"] = "get_status";
|
||||
AvailableMethods2["GET_OUTBOUND_LINK"] = "get_outbound_link";
|
||||
AvailableMethods2["GET_OUTBOUND_LINK_STATES"] = "get_outbound_link_states";
|
||||
AvailableMethods2["GET_OUTBOUND_METADATA"] = "get_outbound_metadata";
|
||||
AvailableMethods2["GET_SUBSCRIPTION_METADATA"] = "get_subscription_metadata";
|
||||
AvailableMethods2["CHECK_SING_BOX"] = "check_sing_box";
|
||||
@@ -2800,19 +2770,14 @@ var TachyonShellMethods = {
|
||||
Tachyon.AvailableMethods.CHECK_INBOUNDS_CONFIG
|
||||
),
|
||||
getStatus: async () => callBaseMethod(Tachyon.AvailableMethods.GET_STATUS),
|
||||
getOutboundLink: async (section, tag) => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_LINK,
|
||||
[section, tag]
|
||||
),
|
||||
getOutboundLinkStates: async (section) => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_LINK_STATES,
|
||||
[section]
|
||||
),
|
||||
getOutboundMetadata: async (section) => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_OUTBOUND_METADATA,
|
||||
[section]
|
||||
),
|
||||
getSubscriptionMetadata: async (section) => callBaseMethod(Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section]),
|
||||
getSubscriptionMetadata: async (section) => callBaseMethod(
|
||||
Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA,
|
||||
[section]
|
||||
),
|
||||
checkSingBox: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.CHECK_SING_BOX
|
||||
),
|
||||
@@ -3040,11 +3005,7 @@ var TachyonShellMethods = {
|
||||
componentActionStart: async (component, action) => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [
|
||||
Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC,
|
||||
component,
|
||||
action
|
||||
],
|
||||
args: [Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC, component, action],
|
||||
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
|
||||
});
|
||||
const parsedResponse = parseComponentActionStartResult(response);
|
||||
@@ -3219,9 +3180,6 @@ var DASHBOARD_SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
|
||||
function getDisplayName(section) {
|
||||
return section.label || section[".name"];
|
||||
}
|
||||
function getSectionAction(section) {
|
||||
return section.action || "";
|
||||
}
|
||||
function getSettingsSection(configSections) {
|
||||
return configSections.find((section) => section[".type"] === "settings");
|
||||
}
|
||||
@@ -3328,7 +3286,6 @@ function hydrateConfigSections(configSections) {
|
||||
idle_timeout: item.idle_timeout,
|
||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||
pin_dashboard: item.pin_dashboard,
|
||||
hide_added_outbounds: item.hide_added_outbounds,
|
||||
urltest_filter_mode: item.filter_mode,
|
||||
detect_server_country: item.detect_server_country,
|
||||
urltest_include_countries: item.include_countries,
|
||||
@@ -3375,7 +3332,6 @@ function hydrateConfigSections(configSections) {
|
||||
fastest_check_interval: item.fastest_check_interval,
|
||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||
pin_dashboard: item.pin_dashboard,
|
||||
hide_added_outbounds: item.hide_added_outbounds,
|
||||
levels
|
||||
};
|
||||
});
|
||||
@@ -3396,7 +3352,9 @@ function getJsonOutbounds(section) {
|
||||
return values.length ? values : getListValues(section.outbound_json);
|
||||
}
|
||||
function isConnectionAction(action) {
|
||||
return ["connection", "proxy", "outbound", "vpn"].includes(action);
|
||||
return Boolean(
|
||||
action && ["connection", "proxy", "outbound", "vpn"].includes(action)
|
||||
);
|
||||
}
|
||||
function hasSubscriptionSources(section) {
|
||||
return getSubscriptionSourceCount(section) > 0;
|
||||
@@ -3591,11 +3549,6 @@ function getUrlTestConfigs(section) {
|
||||
displayName: getUrlTestDisplayName(section, id, settings),
|
||||
settings,
|
||||
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
||||
hideAddedOutbounds: itemSettingBoolean(
|
||||
settings,
|
||||
"hide_added_outbounds",
|
||||
false
|
||||
),
|
||||
showDetectedCountries: filteringEnabled && itemSettingString(settings, "detect_server_country", "flag_emoji") === "country_is"
|
||||
};
|
||||
});
|
||||
@@ -3654,11 +3607,6 @@ function getPriorityConfigs(section) {
|
||||
displayName: itemSettingString(settings, "name", id),
|
||||
settings,
|
||||
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
||||
hideAddedOutbounds: itemSettingBoolean(
|
||||
settings,
|
||||
"hide_added_outbounds",
|
||||
false
|
||||
),
|
||||
healthUrl: itemSettingString(
|
||||
settings,
|
||||
"health_url",
|
||||
@@ -3742,7 +3690,6 @@ function buildUrlTestInfo({
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries
|
||||
}) {
|
||||
const childCodes = uniqueCodes(
|
||||
@@ -3753,7 +3700,7 @@ function buildUrlTestInfo({
|
||||
childCodes.flatMap((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || cachedProxyLinks.get(childCode) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
return [
|
||||
{
|
||||
code: childCode,
|
||||
@@ -3762,7 +3709,7 @@ function buildUrlTestInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode)
|
||||
cachedProxyLinks.has(childCode)
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
@@ -3796,7 +3743,6 @@ function buildPriorityInfo({
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries
|
||||
}) {
|
||||
const selectedCode = entry?.value.now || "";
|
||||
@@ -3832,7 +3778,7 @@ function buildPriorityInfo({
|
||||
const members = uniqueCodes(level.outbounds || []).map((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || cachedProxyLinks.get(childCode) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
return {
|
||||
code: childCode,
|
||||
displayName: getOutboundDisplayName(
|
||||
@@ -3840,7 +3786,7 @@ function buildPriorityInfo({
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(childCode)
|
||||
cachedProxyLinks.has(childCode)
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
@@ -3878,7 +3824,7 @@ function buildPriorityInfo({
|
||||
outbounds
|
||||
};
|
||||
}
|
||||
function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGroups = {}, priorityGroups = {}, subscriptionCopyableCodes = /* @__PURE__ */ new Set(), cachedProxyLinks = /* @__PURE__ */ new Map()) {
|
||||
function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGroups = {}, priorityGroups = {}, cachedProxyLinks = /* @__PURE__ */ new Map()) {
|
||||
const sectionName = section[".name"];
|
||||
const proxyByCode = getProxyEntryByCode(proxies);
|
||||
const selectorTag = getOutboundTagBySection(sectionName);
|
||||
@@ -3903,23 +3849,6 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
const selectorCodes = selector?.value?.all ?? [];
|
||||
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
||||
const priorityCodes = priorityConfigs.map((config) => config.code);
|
||||
const urlTestCodeSet = new Set(urlTestCodes);
|
||||
const priorityCodeSet = new Set(priorityCodes);
|
||||
const hideAddedCodeSet = /* @__PURE__ */ new Set();
|
||||
urlTestEntries.forEach(({ config, entry }) => {
|
||||
if (!config.hideAddedOutbounds) {
|
||||
return;
|
||||
}
|
||||
const childCodes = urltestGroups[config.code]?.outbounds?.length ? urltestGroups[config.code].outbounds || [] : entry?.value.all || [];
|
||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
||||
});
|
||||
priorityEntries.forEach(({ config, entry }) => {
|
||||
if (!config.hideAddedOutbounds) {
|
||||
return;
|
||||
}
|
||||
const childCodes = priorityGroups[config.code]?.outbounds?.length ? priorityGroups[config.code].outbounds || [] : entry?.value.all || [];
|
||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
||||
});
|
||||
const showDetectedCountries = urlTestConfigs.some((config) => config.showDetectedCountries) || priorityConfigs.some((config) => config.showDetectedCountries);
|
||||
const builtInUrltestCode = urlTestCodes[0] || "";
|
||||
const fallbackCodes = uniqueCodes([
|
||||
@@ -3932,12 +3861,7 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
...selectorCodes.length ? selectorCodes : fallbackCodes,
|
||||
...urlTestCodes,
|
||||
...priorityCodes
|
||||
]).filter((code) => {
|
||||
if (!hideAddedCodeSet.has(code)) {
|
||||
return true;
|
||||
}
|
||||
return urlTestCodeSet.has(code) || priorityCodeSet.has(code) || isUrlTestProxyEntry(proxyByCode.get(code));
|
||||
});
|
||||
]);
|
||||
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
||||
const item = proxyByCode.get(code);
|
||||
const urlTestConfig = urlTestConfigByCode.get(code);
|
||||
@@ -3946,13 +3870,13 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
return [];
|
||||
}
|
||||
const link = manualLinkByCode.get(code) || cachedProxyLinks.get(code) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(code);
|
||||
const canCopyLink = isCopyableProxyLink(link);
|
||||
const displayName = priorityConfig?.displayName || urlTestConfig?.displayName || getOutboundDisplayName(
|
||||
code,
|
||||
item,
|
||||
link,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes.has(code)
|
||||
cachedProxyLinks.has(code)
|
||||
);
|
||||
const isRuntimeUrlTest = isUrlTestProxyEntry(item);
|
||||
return [
|
||||
@@ -3975,7 +3899,6 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries: urlTestConfig?.showDetectedCountries || showDetectedCountries
|
||||
}) : void 0,
|
||||
priorityInfo: priorityConfig ? buildPriorityInfo({
|
||||
@@ -3986,7 +3909,6 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
||||
manualLinkByCode,
|
||||
cachedProxyLinks,
|
||||
outboundMetadata,
|
||||
subscriptionCopyableCodes,
|
||||
showDetectedCountries: priorityConfig.showDetectedCountries
|
||||
}) : void 0
|
||||
}
|
||||
@@ -4091,17 +4013,6 @@ function getOutboundMetadata(dashboardCache) {
|
||||
countries: objectMap(metadata.countries)
|
||||
};
|
||||
}
|
||||
function getSubscriptionCopyableCodes(dashboardCache) {
|
||||
const legacyLinks = objectMap(dashboardCache?.links);
|
||||
const linkRefs = dashboardCache?.linkRefs;
|
||||
const codes = new Set(
|
||||
Object.entries(legacyLinks).filter(([, link]) => isCopyableProxyLink(link)).map(([code]) => code)
|
||||
);
|
||||
if (linkRefs && typeof linkRefs === "object" && !Array.isArray(linkRefs)) {
|
||||
Object.keys(linkRefs).forEach((code) => codes.add(code));
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
function getCachedProxyLinks(dashboardCache) {
|
||||
return new Map(
|
||||
Object.entries(objectMap(dashboardCache?.links)).filter(
|
||||
@@ -4127,11 +4038,11 @@ async function getDashboardSections(options = {}) {
|
||||
);
|
||||
const data = await Promise.all(
|
||||
configSections.filter(
|
||||
(section) => section.enabled !== "0" && isConnectionAction(getSectionAction(section))
|
||||
(section) => section.enabled !== "0" && isConnectionAction(section.action)
|
||||
).map(async (section) => {
|
||||
const displayName = getDisplayName(section);
|
||||
const sectionName = section[".name"];
|
||||
const sectionAction = getSectionAction(section);
|
||||
const sectionAction = section.action;
|
||||
const proxyConfigType = getSectionProxyConfigType(section);
|
||||
if (isConnectionAction(sectionAction) && shouldUseProxyGroup(section)) {
|
||||
const subscriptionSourceCount = getSubscriptionSourceCount(section);
|
||||
@@ -4143,7 +4054,6 @@ async function getDashboardSections(options = {}) {
|
||||
subscriptionSourceCount,
|
||||
dashboardCache
|
||||
) : void 0;
|
||||
const subscriptionCopyableCodes = includeSubscriptionCopyState ? getSubscriptionCopyableCodes(dashboardCache) : /* @__PURE__ */ new Set();
|
||||
const cachedProxyLinks = includeSubscriptionCopyState ? getCachedProxyLinks(dashboardCache) : /* @__PURE__ */ new Map();
|
||||
const urltestGroups = getUrlTestGroups(dashboardCache);
|
||||
const priorityGroups = getPriorityGroups(dashboardCache);
|
||||
@@ -4153,7 +4063,6 @@ async function getDashboardSections(options = {}) {
|
||||
outboundMetadata,
|
||||
urltestGroups,
|
||||
priorityGroups,
|
||||
subscriptionCopyableCodes,
|
||||
cachedProxyLinks
|
||||
);
|
||||
return {
|
||||
@@ -5410,8 +5319,8 @@ var SocketManager = class _SocketManager {
|
||||
}
|
||||
this.sockets.set(url, ws);
|
||||
this.connected.set(url, false);
|
||||
this.listeners.set(url, /* @__PURE__ */ new Set());
|
||||
this.errorListeners.set(url, /* @__PURE__ */ new Set());
|
||||
if (!this.listeners.has(url)) this.listeners.set(url, /* @__PURE__ */ new Set());
|
||||
if (!this.errorListeners.has(url)) this.errorListeners.set(url, /* @__PURE__ */ new Set());
|
||||
ws.addEventListener("open", () => {
|
||||
this.connected.set(url, true);
|
||||
this.reconnectAttempts.set(url, 0);
|
||||
@@ -6217,20 +6126,12 @@ async function handleTestLatency(latencyType, sectionName, tag, timeout) {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function handleCopyOutbound(section, outbound) {
|
||||
function handleCopyOutbound(outbound) {
|
||||
const link = outbound.link;
|
||||
if (link && isCopyableProxyLink(link)) {
|
||||
copyToClipboard(link);
|
||||
return;
|
||||
}
|
||||
const response = await TachyonShellMethods.getOutboundLink(
|
||||
section.sectionName,
|
||||
outbound.code
|
||||
);
|
||||
if (response.success && isCopyableProxyLink(response.data.link)) {
|
||||
copyToClipboard(response.data.link);
|
||||
return;
|
||||
}
|
||||
showToast(_("Proxy link is unavailable"), "error");
|
||||
}
|
||||
function formatUrlTestModalValue(value) {
|
||||
@@ -6342,7 +6243,7 @@ function renderUrlTestCopyButton(title, onClick) {
|
||||
renderCopyIcon24()
|
||||
);
|
||||
}
|
||||
function renderCommonDetailsModal(info, section, fields, renderMemberName, isPriority) {
|
||||
function renderCommonDetailsModal(info, fields, renderMemberName, isPriority) {
|
||||
return E("div", { class: "tachyon_dashboard-page__urltest-details" }, [
|
||||
E(
|
||||
"dl",
|
||||
@@ -6427,7 +6328,7 @@ function renderCommonDetailsModal(info, section, fields, renderMemberName, isPri
|
||||
),
|
||||
member.canCopyLink ? renderUrlTestCopyButton(_("Copy proxy link"), (event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
void handleCopyOutbound(member);
|
||||
}) : E("span", {
|
||||
class: "tachyon_dashboard-page__urltest-details__copy-placeholder"
|
||||
})
|
||||
@@ -6457,7 +6358,7 @@ function renderCommonDetailsModal(info, section, fields, renderMemberName, isPri
|
||||
])
|
||||
]);
|
||||
}
|
||||
function renderUrlTestInfoModal(section, outbound) {
|
||||
function renderUrlTestInfoModal(outbound) {
|
||||
const info = outbound.urlTestInfo;
|
||||
if (!info) {
|
||||
return E("div", {}, _("URLTest details are unavailable"));
|
||||
@@ -6472,25 +6373,24 @@ function renderUrlTestInfoModal(section, outbound) {
|
||||
{ label: _("Tolerance"), value: info.tolerance },
|
||||
{ label: _("Idle timeout"), value: info.idleTimeout },
|
||||
{
|
||||
label: _("Interrupt existing connections"),
|
||||
label: _("Interrupt connections"),
|
||||
value: info.interruptExistConnections
|
||||
}
|
||||
];
|
||||
return renderCommonDetailsModal(
|
||||
info,
|
||||
section,
|
||||
fields,
|
||||
(member) => renderDetailsMemberName(member),
|
||||
false
|
||||
);
|
||||
}
|
||||
function handleShowUrlTestInfo(section, outbound) {
|
||||
function handleShowUrlTestInfo(outbound) {
|
||||
if (!outbound.urlTestInfo) {
|
||||
return;
|
||||
}
|
||||
ui.showModal(
|
||||
`${_("URLTest details")}: ${outbound.urlTestInfo.displayName || outbound.displayName}`,
|
||||
renderUrlTestInfoModal(section, outbound)
|
||||
renderUrlTestInfoModal(outbound)
|
||||
);
|
||||
}
|
||||
function renderPrioritySelectedValue(info) {
|
||||
@@ -6553,7 +6453,7 @@ function renderPriorityMemberName(member) {
|
||||
)
|
||||
];
|
||||
}
|
||||
function renderPriorityInfoModal(section, outbound) {
|
||||
function renderPriorityInfoModal(outbound) {
|
||||
const info = outbound.priorityInfo;
|
||||
if (!info) {
|
||||
return E("div", {}, _("Priority details are unavailable"));
|
||||
@@ -6588,25 +6488,24 @@ function renderPriorityInfoModal(section, outbound) {
|
||||
}
|
||||
] : [],
|
||||
{
|
||||
label: _("Interrupt existing connections"),
|
||||
label: _("Interrupt connections"),
|
||||
value: info.interruptExistConnections
|
||||
}
|
||||
];
|
||||
return renderCommonDetailsModal(
|
||||
info,
|
||||
section,
|
||||
fields,
|
||||
(member) => renderPriorityMemberName(member),
|
||||
true
|
||||
);
|
||||
}
|
||||
function handleShowPriorityInfo(section, outbound) {
|
||||
function handleShowPriorityInfo(outbound) {
|
||||
if (!outbound.priorityInfo) {
|
||||
return;
|
||||
}
|
||||
ui.showModal(
|
||||
`${_("Priority details")}: ${outbound.priorityInfo.displayName || outbound.displayName}`,
|
||||
renderPriorityInfoModal(section, outbound)
|
||||
renderPriorityInfoModal(outbound)
|
||||
);
|
||||
}
|
||||
async function handleUpdateSubscription(section) {
|
||||
@@ -6772,14 +6671,14 @@ async function renderSectionsWidget() {
|
||||
onChooseOutbound: (sectionName, selector, tag) => {
|
||||
void handleChooseOutbound(sectionName, selector, tag);
|
||||
},
|
||||
onCopyOutbound: (section2, outbound) => {
|
||||
void handleCopyOutbound(section2, outbound);
|
||||
onCopyOutbound: (_section, outbound) => {
|
||||
handleCopyOutbound(outbound);
|
||||
},
|
||||
onShowUrlTestInfo: (section2, outbound) => {
|
||||
handleShowUrlTestInfo(section2, outbound);
|
||||
onShowUrlTestInfo: (outbound) => {
|
||||
handleShowUrlTestInfo(outbound);
|
||||
},
|
||||
onShowPriorityInfo: (section2, outbound) => {
|
||||
handleShowPriorityInfo(section2, outbound);
|
||||
onShowPriorityInfo: (outbound) => {
|
||||
handleShowPriorityInfo(outbound);
|
||||
},
|
||||
onUpdateSubscription: (section2) => {
|
||||
void handleUpdateSubscription(section2);
|
||||
@@ -9639,6 +9538,7 @@ var SING_BOX_MASKED_KEYS = /* @__PURE__ */ new Set([
|
||||
]);
|
||||
var TACHYON_MASK_AFTER_TOKEN = [
|
||||
"option proxy_string",
|
||||
"option hwid",
|
||||
"option subscription_url",
|
||||
"list subscription_urls",
|
||||
"list urltest_proxy_links",
|
||||
@@ -14343,8 +14243,6 @@ return baseclass.extend({
|
||||
validateDNS,
|
||||
validateDomain,
|
||||
validateIP,
|
||||
validateIPV4,
|
||||
validateIPv6,
|
||||
validateOutboundJson,
|
||||
validatePath,
|
||||
validateProxyUrl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require view.tachyon.local_devices as localDevices";
|
||||
|
||||
@@ -431,6 +431,8 @@ let actionProvidersAvailabilityLoader = null;
|
||||
const outboundNameChoicesCache = new Map();
|
||||
const outboundNameChoicesInflight = new Map();
|
||||
const outboundNameSourceOptions = new Map();
|
||||
const sectionGroupSourceOptions = new Map();
|
||||
const dashboardFilterChoiceRefreshers = new Map();
|
||||
const SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
|
||||
const COUNTRY_CODES =
|
||||
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW XK".split(
|
||||
@@ -1323,7 +1325,14 @@ const SettingsDynamicList = form.DynamicList.extend({
|
||||
},
|
||||
});
|
||||
|
||||
return widget.render();
|
||||
const node = widget.render();
|
||||
if (typeof this.onListChange === "function") {
|
||||
node.addEventListener("cbi-dynlist-change", () => {
|
||||
this.onListChange(section_id);
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
parse(section_id) {
|
||||
@@ -1415,6 +1424,78 @@ const ButtonAddSettingsDynamicList = SettingsDynamicList.extend({
|
||||
buttonAdd: true,
|
||||
});
|
||||
|
||||
function configureLiveDynamicListChoices(option, getChoices) {
|
||||
option.renderWidget = function (section_id, _option_index, cfgvalue) {
|
||||
const values = L.toArray(cfgvalue != null ? cfgvalue : this.default);
|
||||
const choices = getChoices(section_id, values);
|
||||
const labels = {};
|
||||
choices.forEach((choice) => {
|
||||
labels[choice.value] = choice.label;
|
||||
});
|
||||
refreshOptionChoices(this, choices);
|
||||
let choiceSignature = JSON.stringify(
|
||||
choices.map((choice) => [choice.value, choice.label]),
|
||||
);
|
||||
// form.DynamicList passes null when there are no choices, which permanently
|
||||
// renders a plain input. An empty object keeps the stock combobox alive so
|
||||
// later live choices can be added without reopening the modal.
|
||||
const widget = new ui.DynamicList(values, labels, {
|
||||
id: this.cbid(section_id),
|
||||
sort: this.keylist,
|
||||
allowduplicates: this.allowduplicates,
|
||||
optional: this.optional || this.rmempty,
|
||||
datatype: this.datatype,
|
||||
placeholder: this.placeholder,
|
||||
validate: L.bind(this.validate, this, section_id),
|
||||
disabled: this.readonly != null ? this.readonly : this.map.readonly,
|
||||
});
|
||||
const node = widget.render();
|
||||
const refreshChoices = () => {
|
||||
if (!node.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentValues = widget.getValue();
|
||||
const currentChoices = getChoices(section_id, currentValues);
|
||||
const currentLabels = {};
|
||||
currentChoices.forEach((choice) => {
|
||||
currentLabels[choice.value] = choice.label;
|
||||
});
|
||||
const currentSignature = JSON.stringify(
|
||||
currentChoices.map((choice) => [choice.value, choice.label]),
|
||||
);
|
||||
|
||||
if (currentSignature === choiceSignature) {
|
||||
return;
|
||||
}
|
||||
choiceSignature = currentSignature;
|
||||
|
||||
refreshOptionChoices(this, currentChoices);
|
||||
widget.choices = currentLabels;
|
||||
widget.clearChoices();
|
||||
widget.addChoices(
|
||||
currentChoices.map((choice) => choice.value),
|
||||
currentLabels,
|
||||
);
|
||||
return true;
|
||||
};
|
||||
const refreshBeforeOpening = (event) => {
|
||||
if (event.target && event.target.closest(".add-item")) {
|
||||
refreshChoices();
|
||||
}
|
||||
};
|
||||
node.addEventListener("mousedown", refreshBeforeOpening, true);
|
||||
node.addEventListener("focusin", refreshBeforeOpening, true);
|
||||
|
||||
if (!dashboardFilterChoiceRefreshers.has(section_id)) {
|
||||
dashboardFilterChoiceRefreshers.set(section_id, new Set());
|
||||
}
|
||||
dashboardFilterChoiceRefreshers.get(section_id).add(refreshChoices);
|
||||
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
function countryChoices() {
|
||||
return COUNTRY_CODES.map((code) => ({
|
||||
value: code,
|
||||
@@ -1529,6 +1610,94 @@ function currentDraftOutboundNames(section_id) {
|
||||
return names;
|
||||
}
|
||||
|
||||
function currentSectionGroupValues(section_id, typeName) {
|
||||
const liveValues = currentLiveDynamicListValues(section_id, typeName);
|
||||
if (liveValues != null) {
|
||||
return liveValues;
|
||||
}
|
||||
|
||||
const option = sectionGroupSourceOptions.get(typeName);
|
||||
if (option && typeof option.formvalue === "function") {
|
||||
try {
|
||||
const value = option.formvalue(section_id);
|
||||
if (value != null) {
|
||||
return normalizeDynamicListItems(value);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Fall back to the child sections when the widget is not mounted.
|
||||
}
|
||||
}
|
||||
|
||||
return getChildItemIds(section_id, typeName);
|
||||
}
|
||||
|
||||
function sectionGroupDisplayName(section_id, typeName, value) {
|
||||
const itemId = `${value || ""}`.trim();
|
||||
if (isExistingChildItem(section_id, itemId, typeName)) {
|
||||
return `${uci.get(UCI_PACKAGE, itemId, "name") || itemId}`.trim();
|
||||
}
|
||||
|
||||
const option = sectionGroupSourceOptions.get(typeName);
|
||||
const pending =
|
||||
option && option.pendingChildSettings
|
||||
? option.pendingChildSettings[section_id]
|
||||
: null;
|
||||
const settings = pending && pending[itemId];
|
||||
return `${(settings && settings.name) || itemId}`.trim();
|
||||
}
|
||||
|
||||
function currentSectionGroupChoices(section_id, selectedValues = []) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
const append = (typeName, value) => {
|
||||
value = `${value || ""}`.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = sectionGroupDisplayName(section_id, typeName, value) || value;
|
||||
if (seen.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(name);
|
||||
result.push({ value: name, label: name });
|
||||
};
|
||||
|
||||
currentSectionGroupValues(section_id, "urltest").forEach((value) =>
|
||||
append("urltest", value),
|
||||
);
|
||||
currentSectionGroupValues(section_id, "priority_group").forEach((value) =>
|
||||
append("priority_group", value),
|
||||
);
|
||||
normalizeDynamicListItems(selectedValues).forEach((value) => {
|
||||
value = `${value || ""}`.trim();
|
||||
if (value && !seen.has(value)) {
|
||||
seen.add(value);
|
||||
result.push({ value, label: value });
|
||||
}
|
||||
});
|
||||
|
||||
return result.sort((left, right) => left.label.localeCompare(right.label));
|
||||
}
|
||||
|
||||
function refreshDashboardFilterChoiceWidgets(section_id) {
|
||||
const refreshers = dashboardFilterChoiceRefreshers.get(section_id);
|
||||
if (!refreshers) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshers.forEach((refresh) => {
|
||||
if (refresh() === false) {
|
||||
refreshers.delete(refresh);
|
||||
}
|
||||
});
|
||||
|
||||
if (refreshers.size === 0) {
|
||||
dashboardFilterChoiceRefreshers.delete(section_id);
|
||||
}
|
||||
}
|
||||
|
||||
function isDownloadThroughTargetSection(section, currentSectionId) {
|
||||
const sectionName = getUciSectionName(section);
|
||||
const action = (section && section.action) || "";
|
||||
@@ -1916,7 +2085,6 @@ function urlTestSettingsKeys() {
|
||||
"idle_timeout",
|
||||
"interrupt_exist_connections",
|
||||
"pin_dashboard",
|
||||
"hide_added_outbounds",
|
||||
"filter_mode",
|
||||
"detect_server_country",
|
||||
"include_countries",
|
||||
@@ -1945,7 +2113,6 @@ function defaultUrlTestSettings(name) {
|
||||
idle_timeout: "30m",
|
||||
interrupt_exist_connections: "1",
|
||||
pin_dashboard: "1",
|
||||
hide_added_outbounds: "0",
|
||||
filter_mode: "disabled",
|
||||
detect_server_country: "flag_emoji",
|
||||
};
|
||||
@@ -1959,7 +2126,6 @@ function urlTestChildDefaults() {
|
||||
idle_timeout: "30m",
|
||||
interrupt_exist_connections: "1",
|
||||
pin_dashboard: "1",
|
||||
hide_added_outbounds: "0",
|
||||
filter_mode: "disabled",
|
||||
detect_server_country: "flag_emoji",
|
||||
};
|
||||
@@ -1977,7 +2143,6 @@ function priorityGroupSettingsKeys() {
|
||||
"fastest_check_interval",
|
||||
"interrupt_exist_connections",
|
||||
"pin_dashboard",
|
||||
"hide_added_outbounds",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1993,7 +2158,6 @@ function defaultPriorityGroupSettings() {
|
||||
fastest_check_interval: "3m",
|
||||
interrupt_exist_connections: "1",
|
||||
pin_dashboard: "1",
|
||||
hide_added_outbounds: "0",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2008,7 +2172,6 @@ function priorityGroupChildDefaults() {
|
||||
fastest_check_interval: "3m",
|
||||
interrupt_exist_connections: "1",
|
||||
pin_dashboard: "1",
|
||||
hide_added_outbounds: "0",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2351,9 +2514,9 @@ function addUrlTestItemOptions(itemSection, options = {}) {
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"interrupt_exist_connections",
|
||||
_("Interrupt existing connections"),
|
||||
_("Interrupt connections"),
|
||||
_(
|
||||
"Interrupt existing connections when URLTest switches the selected server",
|
||||
"Interrupt connections when URLTest switches the selected server",
|
||||
),
|
||||
);
|
||||
o.default = "1";
|
||||
@@ -2368,15 +2531,6 @@ function addUrlTestItemOptions(itemSection, options = {}) {
|
||||
o.default = "1";
|
||||
o.rmempty = false;
|
||||
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"hide_added_outbounds",
|
||||
_("Hide added servers"),
|
||||
_("Hide dashboard servers added to URLTest"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = itemSection.option(
|
||||
form.ListValue,
|
||||
"filter_mode",
|
||||
@@ -2507,6 +2661,9 @@ function addUrlTestItemOptions(itemSection, options = {}) {
|
||||
});
|
||||
};
|
||||
list.placeholder = _("-- Select --");
|
||||
configureLiveDynamicListChoices(list, (itemId, values) =>
|
||||
currentOutboundNameChoices(parentSectionForItem(itemId), values),
|
||||
);
|
||||
}
|
||||
if (validator) {
|
||||
list.validate = function (_itemId, value) {
|
||||
@@ -2717,6 +2874,9 @@ function addPriorityLevelItemOptions(itemSection, options = {}) {
|
||||
});
|
||||
};
|
||||
list.placeholder = _("-- Select --");
|
||||
configureLiveDynamicListChoices(list, (itemId, values) =>
|
||||
currentOutboundNameChoices(parentSectionForItem(itemId), values),
|
||||
);
|
||||
}
|
||||
if (validator) {
|
||||
list.validate = function (_itemId, value) {
|
||||
@@ -2842,8 +3002,8 @@ function addPriorityGroupItemOptions(itemSection, options = {}) {
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"interrupt_exist_connections",
|
||||
_("Interrupt existing connections"),
|
||||
_("Interrupt existing connections when priority failover switches server"),
|
||||
_("Interrupt connections"),
|
||||
_("Interrupt connections when priority failover switches server"),
|
||||
);
|
||||
o.default = "1";
|
||||
o.rmempty = false;
|
||||
@@ -2857,15 +3017,6 @@ function addPriorityGroupItemOptions(itemSection, options = {}) {
|
||||
o.default = "1";
|
||||
o.rmempty = false;
|
||||
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"hide_added_outbounds",
|
||||
_("Hide added servers"),
|
||||
_("Hide dashboard servers added to Priority"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = itemSection.option(
|
||||
ButtonAddSettingsDynamicList,
|
||||
"priority_level",
|
||||
@@ -2937,6 +3088,229 @@ function addPriorityGroupItemOptions(itemSection, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function addDashboardGroupFilterOption(
|
||||
optionSection,
|
||||
key,
|
||||
label,
|
||||
description,
|
||||
modes,
|
||||
) {
|
||||
const list = optionSection.option(form.DynamicList, key, label, description);
|
||||
modes.forEach((mode) => {
|
||||
list.depends({
|
||||
action: "connection",
|
||||
dashboard_filter_mode: mode,
|
||||
urltest: /.+/,
|
||||
});
|
||||
list.depends({
|
||||
action: "connection",
|
||||
dashboard_filter_mode: mode,
|
||||
priority_group: /.+/,
|
||||
});
|
||||
});
|
||||
list.rmempty = true;
|
||||
list.placeholder = _("-- Select --");
|
||||
list.load = function (section_id) {
|
||||
const values = getConfigListValues(section_id, key);
|
||||
const choices = currentSectionGroupChoices(section_id, values);
|
||||
refreshOptionChoices(this, choices);
|
||||
return values;
|
||||
};
|
||||
list.validate = function (section_id, value) {
|
||||
const selected = new Set(
|
||||
currentSectionGroupChoices(section_id).map((choice) => choice.value),
|
||||
);
|
||||
return normalizeDynamicListItems(value).every((item) => selected.has(item))
|
||||
? true
|
||||
: _("Select an existing URLTest or Priority group");
|
||||
};
|
||||
configureLiveDynamicListChoices(list, currentSectionGroupChoices);
|
||||
}
|
||||
|
||||
function addDashboardServerFilterOptions(section) {
|
||||
const optionSection = {
|
||||
option: (optionType, ...args) => {
|
||||
const option = section.taboption("settings", optionType, ...args);
|
||||
option.modalonly = true;
|
||||
return option;
|
||||
},
|
||||
};
|
||||
let o = optionSection.option(
|
||||
form.ListValue,
|
||||
"dashboard_filter_mode",
|
||||
_("Servers on dashboard"),
|
||||
_("Filter the servers that will be displayed on the dashboard."),
|
||||
);
|
||||
urlTestFilterModeChoices().forEach((choice) =>
|
||||
o.value(choice.value, choice.label),
|
||||
);
|
||||
o.default = "disabled";
|
||||
o.depends("action", "connection");
|
||||
|
||||
o = optionSection.option(
|
||||
form.ListValue,
|
||||
"dashboard_detect_server_country",
|
||||
_("Detect server country"),
|
||||
);
|
||||
["exclude", "include", "mixed"].forEach((mode) =>
|
||||
o.depends({ action: "connection", dashboard_filter_mode: mode }),
|
||||
);
|
||||
serverCountryDetectionChoices().forEach((choice) =>
|
||||
o.value(choice.value, choice.label),
|
||||
);
|
||||
o.default = "flag_emoji";
|
||||
|
||||
const includeProxyParameterOptions = {
|
||||
prefix: "dashboard_include",
|
||||
toggleLabel: _("Include by proxy parameters"),
|
||||
toggleDescription: _(
|
||||
"Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters.",
|
||||
),
|
||||
dependencies: [
|
||||
{ action: "connection", dashboard_filter_mode: "include" },
|
||||
{ action: "connection", dashboard_filter_mode: "mixed" },
|
||||
],
|
||||
protocolDescription: _(
|
||||
"Show only servers with one of the selected protocols.",
|
||||
),
|
||||
transportDescription: _(
|
||||
"Show only servers with one of the selected transports.",
|
||||
),
|
||||
securityDescription: _(
|
||||
"Show only servers with one of the selected security types.",
|
||||
),
|
||||
};
|
||||
const excludeProxyParameterOptions = {
|
||||
prefix: "dashboard_exclude",
|
||||
toggleLabel: _("Exclude by proxy parameters"),
|
||||
toggleDescription: _(
|
||||
"Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters.",
|
||||
),
|
||||
dependencies: [
|
||||
{ action: "connection", dashboard_filter_mode: "exclude" },
|
||||
{ action: "connection", dashboard_filter_mode: "mixed" },
|
||||
],
|
||||
protocolDescription: _("Hide servers with one of the selected protocols."),
|
||||
transportDescription: _(
|
||||
"Hide servers with one of the selected transports.",
|
||||
),
|
||||
securityDescription: _(
|
||||
"Hide servers with one of the selected security types.",
|
||||
),
|
||||
};
|
||||
|
||||
[
|
||||
[
|
||||
"dashboard_include_countries",
|
||||
_("Include countries"),
|
||||
_("Show servers only from the specified countries."),
|
||||
countryChoices(),
|
||||
validateCountryCode,
|
||||
["include", "mixed"],
|
||||
],
|
||||
[
|
||||
"dashboard_include_outbounds",
|
||||
_("Include servers"),
|
||||
_("Show only selected servers."),
|
||||
null,
|
||||
null,
|
||||
["include", "mixed"],
|
||||
],
|
||||
[
|
||||
"dashboard_include_regex",
|
||||
_("Include by regular expression"),
|
||||
_("Show servers whose names match the expression."),
|
||||
null,
|
||||
validateRegex,
|
||||
["include", "mixed"],
|
||||
],
|
||||
[
|
||||
"dashboard_exclude_countries",
|
||||
_("Exclude countries"),
|
||||
_("Hide servers from the specified countries."),
|
||||
countryChoices(),
|
||||
validateCountryCode,
|
||||
["exclude", "mixed"],
|
||||
],
|
||||
[
|
||||
"dashboard_exclude_outbounds",
|
||||
_("Exclude servers"),
|
||||
_("Hide selected servers."),
|
||||
null,
|
||||
null,
|
||||
["exclude", "mixed"],
|
||||
],
|
||||
[
|
||||
"dashboard_exclude_regex",
|
||||
_("Exclude by regular expression"),
|
||||
_("Hide servers whose names match the expression."),
|
||||
null,
|
||||
validateRegex,
|
||||
["exclude", "mixed"],
|
||||
],
|
||||
].forEach(([key, label, description, choices, validator, modes]) => {
|
||||
const list = optionSection.option(
|
||||
form.DynamicList,
|
||||
key,
|
||||
label,
|
||||
description,
|
||||
);
|
||||
modes.forEach((mode) =>
|
||||
list.depends({ action: "connection", dashboard_filter_mode: mode }),
|
||||
);
|
||||
list.rmempty = true;
|
||||
if (choices) {
|
||||
choices.forEach((choice) => list.value(choice.value, choice.label));
|
||||
list.placeholder = _("-- Select --");
|
||||
}
|
||||
if (key.endsWith("_outbounds")) {
|
||||
list.load = function (section_id) {
|
||||
const values = getConfigListValues(section_id, key);
|
||||
loadOutboundNameChoices(section_id).then(() => {
|
||||
refreshDashboardFilterChoiceWidgets(section_id);
|
||||
});
|
||||
return values;
|
||||
};
|
||||
list.placeholder = _("-- Select --");
|
||||
configureLiveDynamicListChoices(list, currentOutboundNameChoices);
|
||||
}
|
||||
if (validator) {
|
||||
list.validate = function (_section_id, value) {
|
||||
return validator(null, value);
|
||||
};
|
||||
}
|
||||
if (key === "dashboard_include_regex") {
|
||||
addDashboardGroupFilterOption(
|
||||
optionSection,
|
||||
"dashboard_include_groups",
|
||||
_("Add URLTest / Priority group"),
|
||||
_(
|
||||
"Show servers from selected groups without repeating their filtering criteria.",
|
||||
),
|
||||
["include", "mixed"],
|
||||
);
|
||||
addProxyParameterFilterOptions(
|
||||
optionSection,
|
||||
includeProxyParameterOptions,
|
||||
);
|
||||
} else if (key === "dashboard_exclude_regex") {
|
||||
addDashboardGroupFilterOption(
|
||||
optionSection,
|
||||
"dashboard_exclude_groups",
|
||||
_("Exclude URLTest / Priority group"),
|
||||
_(
|
||||
"Hide servers from selected groups without repeating their filtering criteria.",
|
||||
),
|
||||
["exclude", "mixed"],
|
||||
);
|
||||
addProxyParameterFilterOptions(
|
||||
optionSection,
|
||||
excludeProxyParameterOptions,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function settingValueEquals(left, right) {
|
||||
const normalize = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
@@ -3017,6 +3391,39 @@ function renderStackedJsonSettingsModal(title, map, onSave) {
|
||||
const originalButtonNodes = Array.from(buttonRow.childNodes);
|
||||
let closed = false;
|
||||
let saveButton;
|
||||
let validationSummary;
|
||||
|
||||
const clearValidationSummary = () => {
|
||||
if (validationSummary && validationSummary.parentNode) {
|
||||
validationSummary.parentNode.removeChild(validationSummary);
|
||||
}
|
||||
validationSummary = null;
|
||||
};
|
||||
|
||||
const showValidationSummary = (error) => {
|
||||
clearValidationSummary();
|
||||
|
||||
const message = error?.message || "";
|
||||
validationSummary = E(
|
||||
"div",
|
||||
{
|
||||
class:
|
||||
"alert-message warning fkp-stacked-settings-validation-summary",
|
||||
},
|
||||
[
|
||||
E("strong", {}, _("Cannot save settings")),
|
||||
E("div", {}, _("Fix the highlighted fields and save again.")),
|
||||
message ? E("small", {}, message) : "",
|
||||
],
|
||||
);
|
||||
buttonRow.parentNode.insertBefore(validationSummary, buttonRow);
|
||||
|
||||
const invalidInput = nodes.querySelector(".cbi-input-invalid");
|
||||
if (invalidInput) {
|
||||
invalidInput.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
invalidInput.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
const restoreButtonRow = () => {
|
||||
buttonRow.textContent = "";
|
||||
@@ -3030,6 +3437,7 @@ function renderStackedJsonSettingsModal(title, map, onSave) {
|
||||
}
|
||||
|
||||
closed = true;
|
||||
clearValidationSummary();
|
||||
|
||||
if (nodes.parentNode) {
|
||||
nodes.parentNode.removeChild(nodes);
|
||||
@@ -3046,6 +3454,7 @@ function renderStackedJsonSettingsModal(title, map, onSave) {
|
||||
if (saveButton) {
|
||||
saveButton.disabled = true;
|
||||
}
|
||||
clearValidationSummary();
|
||||
|
||||
return map
|
||||
.parse()
|
||||
@@ -3053,10 +3462,11 @@ function renderStackedJsonSettingsModal(title, map, onSave) {
|
||||
onSave(cleanFormSectionData(map.data.get(map.config, "settings")));
|
||||
close();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (saveButton) {
|
||||
saveButton.disabled = false;
|
||||
}
|
||||
showValidationSummary(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3685,20 +4095,6 @@ function populateActionOptionValues(option) {
|
||||
}
|
||||
}
|
||||
|
||||
function setFlagOptionWidgetValue(section_id, optionName, enabled) {
|
||||
const frame = document.getElementById(
|
||||
`cbid.${UCI_PACKAGE}.${section_id}.${optionName}`,
|
||||
);
|
||||
const checkbox = frame ? frame.querySelector('input[type="checkbox"]') : null;
|
||||
|
||||
if (!checkbox || checkbox.checked === Boolean(enabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.checked = Boolean(enabled);
|
||||
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
|
||||
function getConfigListValues(section_id, key) {
|
||||
return normalizeOptionValues(uci.get(UCI_PACKAGE, section_id, key));
|
||||
}
|
||||
@@ -3988,21 +4384,7 @@ function validateKeyword(_section_id, value) {
|
||||
}
|
||||
|
||||
function isSingBoxDuration(value) {
|
||||
return /^([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
|
||||
}
|
||||
|
||||
function readDurationOptionWithDefault(section_id, key, defaultValue) {
|
||||
if (uci.get(UCI_PACKAGE, section_id, `${key}_disabled`) === "1") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const rawValue = uci.get(UCI_PACKAGE, section_id, key);
|
||||
|
||||
if (rawValue == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return `${rawValue}`;
|
||||
return /^(?=.*[1-9])([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
|
||||
}
|
||||
|
||||
function writeOptionalDurationOption(section_id, key, value) {
|
||||
@@ -4018,10 +4400,6 @@ function writeOptionalDurationOption(section_id, key, value) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeOptionalDurationOption(section_id, key) {
|
||||
writeOptionalDurationOption(section_id, key, "");
|
||||
}
|
||||
|
||||
function validateOptionalSingBoxDuration(value) {
|
||||
const normalized = value ? `${value}`.trim() : "";
|
||||
|
||||
@@ -4081,39 +4459,6 @@ function validateSubscriptionUrlEntry(_section_id, value) {
|
||||
return validation.valid ? true : validation.message;
|
||||
}
|
||||
|
||||
function parseRequiredValueOnSave(section_id) {
|
||||
const active = this.isActive(section_id);
|
||||
|
||||
if (active && !this.isValid(section_id)) {
|
||||
const title = this.stripTags(this.title).trim();
|
||||
const error = this.getValidationError(section_id);
|
||||
return Promise.reject(
|
||||
new TypeError(
|
||||
`${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${error}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
const formValue = this.formvalue(section_id);
|
||||
const normalized = formValue ? `${formValue}`.trim() : "";
|
||||
|
||||
if (!normalized.length) {
|
||||
return Promise.reject(
|
||||
new TypeError(_("Subscription URL cannot be empty")),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.write(section_id, normalized));
|
||||
}
|
||||
|
||||
if (!this.retain) {
|
||||
return Promise.resolve(this.remove(section_id));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function getDuplicateTextListErrors(values, normalizeValue, duplicateMessage) {
|
||||
const seen = new Set();
|
||||
const duplicates = [];
|
||||
@@ -4134,57 +4479,6 @@ function getDuplicateTextListErrors(values, normalizeValue, duplicateMessage) {
|
||||
return duplicates.map((item) => `${item}: ${duplicateMessage}`);
|
||||
}
|
||||
|
||||
function validateTextList(
|
||||
_section_id,
|
||||
value,
|
||||
validateItem,
|
||||
emptyMessage,
|
||||
options = {},
|
||||
) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const values = main.parseValueList(value);
|
||||
|
||||
if (!values.length) {
|
||||
return emptyMessage;
|
||||
}
|
||||
|
||||
if (!validateItem) {
|
||||
const duplicateErrors = getDuplicateTextListErrors(
|
||||
values,
|
||||
options.normalizeDuplicateValue,
|
||||
options.duplicateMessage || _("Duplicate value"),
|
||||
);
|
||||
|
||||
if (!duplicateErrors.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [_("Validation errors:"), ...duplicateErrors].join("\n");
|
||||
}
|
||||
|
||||
const { valid, results } = main.bulkValidate(values, validateItem);
|
||||
|
||||
const duplicateErrors = getDuplicateTextListErrors(
|
||||
values,
|
||||
options.normalizeDuplicateValue,
|
||||
options.duplicateMessage || _("Duplicate value"),
|
||||
);
|
||||
|
||||
if (valid && !duplicateErrors.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const errors = results
|
||||
.filter((item) => !item.valid)
|
||||
.map((item) => `${item.value}: ${item.message}`)
|
||||
.concat(duplicateErrors);
|
||||
|
||||
return [_("Validation errors:"), ...errors].join("\n");
|
||||
}
|
||||
|
||||
function getValidationHeaderText() {
|
||||
return _("Validation errors:");
|
||||
}
|
||||
@@ -6855,6 +7149,9 @@ function createSectionContent(section) {
|
||||
const validation = main.validateProxyUrl(value);
|
||||
return validation.valid ? true : validation.message;
|
||||
};
|
||||
o.onchange = function (_event, section_id) {
|
||||
refreshDashboardFilterChoiceWidgets(section_id);
|
||||
};
|
||||
outboundNameSourceOptions.set("selector_proxy_links", o);
|
||||
|
||||
o = section.taboption(
|
||||
@@ -6943,6 +7240,7 @@ function createSectionContent(section) {
|
||||
delete this.pendingChildSettings[section_id];
|
||||
}
|
||||
};
|
||||
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||
outboundNameSourceOptions.set("interfaces", o);
|
||||
|
||||
o = section.taboption(
|
||||
@@ -6969,6 +7267,7 @@ function createSectionContent(section) {
|
||||
const validation = main.validateOutboundJson(value);
|
||||
return validation.valid ? true : validation.message;
|
||||
};
|
||||
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||
outboundNameSourceOptions.set("outbound_jsons", o);
|
||||
|
||||
o = section.taboption(
|
||||
@@ -7027,6 +7326,8 @@ function createSectionContent(section) {
|
||||
this.inputValueForItem(section_id, itemId),
|
||||
);
|
||||
};
|
||||
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||
sectionGroupSourceOptions.set("urltest", o);
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
@@ -7064,6 +7365,8 @@ function createSectionContent(section) {
|
||||
this.inputValueForItem(section_id, itemId),
|
||||
);
|
||||
};
|
||||
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||
sectionGroupSourceOptions.set("priority_group", o);
|
||||
|
||||
o = section.taboption(
|
||||
"settings",
|
||||
@@ -7444,6 +7747,8 @@ function createSectionContent(section) {
|
||||
),
|
||||
);
|
||||
ruleSetOption.modalonly = true;
|
||||
// Both widgets map to rule_set, so neither inactive view may erase shared storage.
|
||||
ruleSetOption.retain = true;
|
||||
dependsOnRoutingAction(ruleSetOption);
|
||||
ruleSetOption.renderItemSettingsModal = showRuleSetSettingsModal;
|
||||
ruleSetOption.load = function (section_id) {
|
||||
@@ -7472,6 +7777,7 @@ function createSectionContent(section) {
|
||||
);
|
||||
dnsRuleSetOption.depends("action", "dns");
|
||||
dnsRuleSetOption.modalonly = true;
|
||||
dnsRuleSetOption.retain = true;
|
||||
dnsRuleSetOption.load = function (section_id) {
|
||||
return getCustomRulesetReferences(section_id);
|
||||
};
|
||||
@@ -7527,6 +7833,8 @@ function createSectionContent(section) {
|
||||
dynamicValidate: validatePortCondition,
|
||||
});
|
||||
dependsOnRoutingAction(portsOption);
|
||||
|
||||
addDashboardServerFilterOptions(section);
|
||||
}
|
||||
|
||||
function loadSectionTableOptions(sectionRef) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require form";
|
||||
"require baseclass";
|
||||
"require dom";
|
||||
@@ -496,10 +496,6 @@ function getFirstListValue(sectionId, option) {
|
||||
return asList(uci.get(UCI_PACKAGE, sectionId, option))[0] || "";
|
||||
}
|
||||
|
||||
function serverInboundTag(sectionId) {
|
||||
return `server-${sectionId}-in`;
|
||||
}
|
||||
|
||||
function getProtocol(sectionId) {
|
||||
return uci.get(UCI_PACKAGE, sectionId, "protocol") || DEFAULT_SERVER_PROTOCOL;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require form";
|
||||
"require uci";
|
||||
"require baseclass";
|
||||
@@ -8,7 +8,7 @@
|
||||
const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE;
|
||||
|
||||
function isSingBoxDuration(value) {
|
||||
return /^([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
|
||||
return /^(?=.*[1-9])([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
|
||||
}
|
||||
|
||||
function latencyTestUrlChoices() {
|
||||
|
||||
@@ -442,7 +442,6 @@ const EntryPoint = {
|
||||
return ["telegram"];
|
||||
};
|
||||
settings.createTelegramContent(telegramSection);
|
||||
|
||||
const diagnosticSection = tachyonMap.section(
|
||||
form.TypedSection,
|
||||
"diagnostic",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use strict";
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require ui";
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<<<<<<<< HEAD:luci-app-tachyon/po/ru/tachyon.po
|
||||
# RU translations for TACHYON package.
|
||||
# Copyright (C) 2026 THE TACHYON COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the TACHYON package.
|
||||
# Dushnilin, 2026.
|
||||
========
|
||||
# RU translations for TACHYON package.
|
||||
# Copyright (C) 2026 THE TACHYON COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the TACHYON package.
|
||||
# ushan0v, 2026.
|
||||
>>>>>>>> temp-upstream-rebrand:fe-app-tachyon/locales/tachyon.ru.po
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
<<<<<<<< HEAD:luci-app-tachyon/po/ru/tachyon.po
|
||||
"POT-Creation-Date: 2026-07-13 23:43+0300\n"
|
||||
"PO-Revision-Date: 2026-07-13 23:43+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
========
|
||||
"POT-Creation-Date: 2026-07-13 01:05+0300\n"
|
||||
"PO-Revision-Date: 2026-07-13 01:05+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
>>>>>>>> temp-upstream-rebrand:fe-app-tachyon/locales/tachyon.ru.po
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -77,6 +90,9 @@ msgstr "Добавьте URL или локальные пути к списка
|
||||
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
||||
|
||||
msgid "Add URLTest / Priority group"
|
||||
msgstr "Добавить URLTest / Приоритет группу"
|
||||
|
||||
msgid "Additional marking rules found"
|
||||
msgstr "Найдены дополнительные правила маркировки"
|
||||
|
||||
@@ -86,6 +102,12 @@ msgstr "Дополнительно исключать серверы по про
|
||||
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters."
|
||||
msgstr "Дополнительно скрывать серверы по протоколу, транспорту и защите. Скрывать серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||
msgstr "Дополнительно показывать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||
|
||||
msgid "Address"
|
||||
msgstr "Адрес"
|
||||
|
||||
@@ -245,6 +267,9 @@ msgstr "Не удалось получить результаты проверк
|
||||
msgid "Cannot save server"
|
||||
msgstr "Не удалось сохранить сервер"
|
||||
|
||||
msgid "Cannot save settings"
|
||||
msgstr "Не удалось сохранить настройки"
|
||||
|
||||
msgid "Cascade connection"
|
||||
msgstr "Каскадное подключение"
|
||||
|
||||
@@ -665,6 +690,9 @@ msgstr "Исключить из этого уровня серверы с одн
|
||||
msgid "Exclude servers with one of the selected transports from this level."
|
||||
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Exclude URLTest / Priority group"
|
||||
msgstr "Исключить URLTest / Приоритет группу"
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидается: %s"
|
||||
|
||||
@@ -716,6 +744,9 @@ msgstr "Интервал поиска более быстрого сервера
|
||||
msgid "Fastest"
|
||||
msgstr "Самый быстрый"
|
||||
|
||||
msgid "Filter the servers that will be displayed on the dashboard."
|
||||
msgstr "Отфильтровать серверы, которые будут отображаться в дашборде."
|
||||
|
||||
msgid "Firewall WAN port"
|
||||
msgstr "Порт в firewall для WAN"
|
||||
|
||||
@@ -725,6 +756,66 @@ msgstr "Исправьте подсвеченные поля и сохранит
|
||||
msgid "Forced device routing"
|
||||
msgstr "Принудительная маршрутизация устройств"
|
||||
|
||||
<<<<<<<< HEAD:luci-app-tachyon/po/ru/tachyon.po
|
||||
========
|
||||
msgid "Tachyon Error"
|
||||
msgstr "Ошибка Tachyon"
|
||||
|
||||
msgid "Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here."
|
||||
msgstr "Tachyon сам управляет pid-файлами ciadpi, поэтому параметры pidfile здесь запрещены."
|
||||
|
||||
msgid "Tachyon manages the ciadpi process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом ciadpi, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon manages the nfqws process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом nfqws, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon сам управляет жизненным циклом nfqws2, поэтому режим daemon здесь запрещён."
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display connections."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения подключений."
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display the dashboard."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения дашборда."
|
||||
|
||||
msgid "Tachyon Settings"
|
||||
msgstr "Настройки Tachyon"
|
||||
|
||||
msgid "Tachyon will not modify your DHCP configuration"
|
||||
msgstr "Tachyon не будет изменять вашу конфигурацию DHCP"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime has restarted"
|
||||
msgstr "ciadpi Tachyon перезапускался"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is not ready"
|
||||
msgstr "ciadpi Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is not running"
|
||||
msgstr "ciadpi Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed ciadpi runtime is ready"
|
||||
msgstr "ciadpi Tachyon готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is not ready"
|
||||
msgstr "NFQWS Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is not running"
|
||||
msgstr "NFQWS Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed nfqws runtime is ready"
|
||||
msgstr "NFQWS Tachyon готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is not ready"
|
||||
msgstr "NFQWS2 Tachyon не готов"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is not running"
|
||||
msgstr "NFQWS2 Tachyon не запущен"
|
||||
|
||||
msgid "Tachyon-managed nfqws2 runtime is ready"
|
||||
msgstr "NFQWS2 Tachyon готов"
|
||||
|
||||
>>>>>>>> temp-upstream-rebrand:fe-app-tachyon/locales/tachyon.ru.po
|
||||
msgid "Fronting IP"
|
||||
msgstr "IP фронтинга"
|
||||
|
||||
@@ -755,24 +846,36 @@ msgstr "gRPC-сервис"
|
||||
msgid "Handshake timeout"
|
||||
msgstr "Таймаут рукопожатия"
|
||||
|
||||
msgid "Hide added servers"
|
||||
msgstr "Скрывать добавленные серверы"
|
||||
|
||||
msgid "Hide cascade connection nodes"
|
||||
msgstr "Скрывать узлы каскадных подключений"
|
||||
|
||||
msgid "Hide dashboard servers added to Priority"
|
||||
msgstr "Скрывать серверы, добавленные в Priority, из дашборда"
|
||||
|
||||
msgid "Hide dashboard servers added to URLTest"
|
||||
msgstr "Скрывать серверы из дашборда, которые добавлены в URLTest"
|
||||
|
||||
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
||||
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
||||
|
||||
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
||||
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
||||
|
||||
msgid "Hide selected servers."
|
||||
msgstr "Скрывать выбранные серверы."
|
||||
|
||||
msgid "Hide servers from selected groups without repeating their filtering criteria."
|
||||
msgstr "Скрывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||
|
||||
msgid "Hide servers from the specified countries."
|
||||
msgstr "Скрывать серверы из указанных стран."
|
||||
|
||||
msgid "Hide servers whose names match the expression."
|
||||
msgstr "Скрывать серверы, имена которых совпадают с выражением."
|
||||
|
||||
msgid "Hide servers with one of the selected protocols."
|
||||
msgstr "Скрывать серверы с одним из выбранных протоколов."
|
||||
|
||||
msgid "Hide servers with one of the selected security types."
|
||||
msgstr "Скрывать серверы с одним из выбранных типов защиты."
|
||||
|
||||
msgid "Hide servers with one of the selected transports."
|
||||
msgstr "Скрывать серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Hide URLTest group nodes"
|
||||
msgstr "Скрывать узлы URLTest групп"
|
||||
|
||||
@@ -866,14 +969,14 @@ msgstr "Задержка при мониторинге интерфейсов"
|
||||
msgid "Interface monitoring for Bad WAN"
|
||||
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||
|
||||
msgid "Interrupt existing connections"
|
||||
msgstr "Прерывать существующие соединения"
|
||||
msgid "Interrupt connections"
|
||||
msgstr "Прерывать соединения"
|
||||
|
||||
msgid "Interrupt existing connections when priority failover switches server"
|
||||
msgstr "Разрывать существующие соединения при переключении сервера приоритетом"
|
||||
msgid "Interrupt connections when priority failover switches server"
|
||||
msgstr "Разрывать соединения при переключении сервера приоритетом"
|
||||
|
||||
msgid "Interrupt existing connections when URLTest switches the selected server"
|
||||
msgstr "Прерывать существующие соединения, когда URLTest переключает выбранный сервер"
|
||||
msgid "Interrupt connections when URLTest switches the selected server"
|
||||
msgstr "Прерывать соединения, когда URLTest переключает выбранный сервер"
|
||||
|
||||
msgid "Interval"
|
||||
msgstr "Интервал"
|
||||
@@ -1640,6 +1743,9 @@ msgstr "Выберите транзитную секцию"
|
||||
msgid "Select an existing network interface"
|
||||
msgstr "Выберите существующий сетевой интерфейс"
|
||||
|
||||
msgid "Select an existing URLTest or Priority group"
|
||||
msgstr "Выберите существующую URLTest или Приоритет группу"
|
||||
|
||||
msgid "Select an intermediate section"
|
||||
msgstr "Выберите промежуточную секцию"
|
||||
|
||||
@@ -1733,6 +1839,9 @@ msgstr "server_port должен быть числом от 1 до 65535"
|
||||
msgid "Servers"
|
||||
msgstr "Серверы"
|
||||
|
||||
msgid "Servers on dashboard"
|
||||
msgstr "Серверы в дашборде"
|
||||
|
||||
msgid "Service action failed"
|
||||
msgstr "Действие сервиса не выполнено"
|
||||
|
||||
@@ -1748,6 +1857,27 @@ msgstr "Настройки"
|
||||
msgid "Show metadata on dashboard"
|
||||
msgstr "Отображать метаданные в дашборде"
|
||||
|
||||
msgid "Show only selected servers."
|
||||
msgstr "Показывать только выбранные серверы."
|
||||
|
||||
msgid "Show only servers with one of the selected protocols."
|
||||
msgstr "Показывать только серверы с одним из выбранных протоколов."
|
||||
|
||||
msgid "Show only servers with one of the selected security types."
|
||||
msgstr "Показывать только серверы с одним из выбранных типов защиты."
|
||||
|
||||
msgid "Show only servers with one of the selected transports."
|
||||
msgstr "Показывать только серверы с одним из выбранных транспортов."
|
||||
|
||||
msgid "Show servers from selected groups without repeating their filtering criteria."
|
||||
msgstr "Показывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||
|
||||
msgid "Show servers only from the specified countries."
|
||||
msgstr "Показывать серверы только из указанных стран."
|
||||
|
||||
msgid "Show servers whose names match the expression."
|
||||
msgstr "Показывать серверы, имена которых совпадают с выражением."
|
||||
|
||||
msgid "Show sing-box config"
|
||||
msgstr "Показать sing-box конфигурацию"
|
||||
|
||||
@@ -2093,6 +2223,18 @@ msgstr "Таймаут недоступности"
|
||||
msgid "Unexpected ByeDPI strategy argument: %s"
|
||||
msgstr "Неожиданный аргумент стратегии ByeDPI: %s"
|
||||
|
||||
<<<<<<<< HEAD:luci-app-tachyon/po/ru/tachyon.po
|
||||
========
|
||||
msgid "Unexpected Tachyon-managed ciadpi runtime is running"
|
||||
msgstr "Обнаружен лишний ciadpi Tachyon"
|
||||
|
||||
msgid "Unexpected Tachyon-managed nfqws runtime is running"
|
||||
msgstr "Обнаружен лишний NFQWS Tachyon"
|
||||
|
||||
msgid "Unexpected Tachyon-managed nfqws2 runtime is running"
|
||||
msgstr "Обнаружен лишний NFQWS2 Tachyon"
|
||||
|
||||
>>>>>>>> temp-upstream-rebrand:fe-app-tachyon/locales/tachyon.ru.po
|
||||
msgid "Unexpected standalone token. Use explicit flags such as --name or --name=value."
|
||||
msgstr "Неожиданный отдельный токен. Используйте явные флаги вида --имя или --имя=значение."
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
config settings 'settings'
|
||||
config settings 'settings'
|
||||
option config_version '1.0.2'
|
||||
list applied_migrations 'interface_sections'
|
||||
list applied_migrations 'enable_component_checks'
|
||||
@@ -44,6 +44,12 @@ config settings 'settings'
|
||||
# list community_lists 'russia_inside'
|
||||
# list rule_set 'https://example.com/rules.srs'
|
||||
# list rule_set_with_subnets 'https://example.com/rules-with-subnets.json'
|
||||
# option dashboard_filter_mode 'disabled'
|
||||
# option dashboard_detect_server_country 'flag_emoji'
|
||||
# #list dashboard_include_outbounds 'Example server name'
|
||||
# #list dashboard_include_groups 'Fastest'
|
||||
# #list dashboard_exclude_outbounds 'Backup server name'
|
||||
# #list dashboard_exclude_groups 'Backup'
|
||||
# option outbound_detour_enabled '0'
|
||||
# #option outbound_detour_section 'main'
|
||||
# option sort_by_latency '0'
|
||||
@@ -81,7 +87,6 @@ config settings 'settings'
|
||||
# option idle_timeout '30m'
|
||||
# option interrupt_exist_connections '1'
|
||||
# option pin_dashboard '1'
|
||||
# option hide_added_outbounds '0'
|
||||
# option filter_mode 'disabled'
|
||||
# option detect_server_country 'flag_emoji'
|
||||
# list exclude_countries 'US'
|
||||
|
||||
Executable → Regular
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#!/bin/sh /etc/rc.common
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
START=99
|
||||
|
||||
@@ -91,7 +91,7 @@ function show_help() {
|
||||
print(" show_sing_box_config Show sing-box configuration\n");
|
||||
print(" show_sing_box_version Show sing-box version\n");
|
||||
print(" get_status Get Tachyon service status\n");
|
||||
print(" get_outbound_link Get source proxy link for a generated outbound\n");
|
||||
|
||||
print(" get_outbound_metadata Get generated outbound display names and countries\n");
|
||||
print(" get_subscription_metadata\n");
|
||||
print(" Get UI-only metadata for a subscription rule\n");
|
||||
@@ -168,8 +168,6 @@ function command_spec(command) {
|
||||
show_sing_box_config: [ "diagnostics/runtime.uc", "show-sing-box-config", 1 ],
|
||||
show_sing_box_version: [ "diagnostics/runtime.uc", "show-sing-box-version", 0 ],
|
||||
get_status: [ "diagnostics/runtime.uc", "get-status", 0 ],
|
||||
get_outbound_link: [ "diagnostics/runtime.uc", "get-outbound-link", 2 ],
|
||||
get_outbound_link_states: [ "diagnostics/runtime.uc", "get-outbound-link-states", 1 ],
|
||||
get_outbound_metadata: [ "diagnostics/runtime.uc", "get-outbound-metadata", 1 ],
|
||||
get_subscription_metadata: [ "diagnostics/runtime.uc", "get-subscription-metadata", 1 ],
|
||||
get_sing_box_status: [ "diagnostics/runtime.uc", "get-sing-box-status", 0 ],
|
||||
@@ -201,7 +199,8 @@ function command_spec(command) {
|
||||
validate_nfqws_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws-strategy-json", 1 ],
|
||||
validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ],
|
||||
validate_byedpi_strategy_json: [ "providers/byedpi/validator.uc", "validate-json", 1 ],
|
||||
package_prerm: [ "service/package.uc", "prerm", 0 ],
|
||||
package_prerm: [ "service/package.uc", "prerm", 1 ],
|
||||
package_postinst: [ "service/package.uc", "postinst", 0 ],
|
||||
luci_postinst: [ "service/package.uc", "luci-postinst", 0 ]
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
|
||||
@@ -92,19 +92,6 @@ function path_basename(value) {
|
||||
return length(parts) > 0 ? as_string(parts[length(parts) - 1]) : "";
|
||||
}
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function arg_bool(value) {
|
||||
return value === true || value == "true" || value == "1" || value == 1;
|
||||
}
|
||||
@@ -264,7 +251,7 @@ function openwrt_release_value(path, key) {
|
||||
continue;
|
||||
|
||||
let rest = substr(line, length(prefix));
|
||||
let quote = str_last_index(rest, "'");
|
||||
let quote = rindex(rest, "'");
|
||||
if (quote >= 0)
|
||||
print(substr(rest, 0, quote), "\n");
|
||||
return;
|
||||
@@ -282,7 +269,7 @@ function openwrt_release_series(path) {
|
||||
continue;
|
||||
|
||||
let value = substr(line, length(prefix));
|
||||
let quote = str_last_index(value, "'");
|
||||
let quote = rindex(value, "'");
|
||||
if (quote >= 0)
|
||||
value = substr(value, 0, quote);
|
||||
|
||||
|
||||
@@ -33,9 +33,10 @@ const SUBSCRIPTION_METADATA_DIR = getenv("TACHYON_SUBSCRIPTION_METADATA_DIR") ||
|
||||
const OUTBOUND_METADATA_DIR = getenv("TACHYON_OUTBOUND_METADATA_DIR") || RUNTIME_STATE_DIR + "/outbound-metadata";
|
||||
const SECTION_CACHE_DIR = getenv("TACHYON_SECTION_CACHE_DIR") || RUNTIME_STATE_DIR + "/section-cache";
|
||||
const RUNTIME_CACHE_FORMAT_FILE = getenv("TACHYON_RUNTIME_CACHE_FORMAT_FILE") || RUNTIME_STATE_DIR + "/cache-format";
|
||||
const RUNTIME_CACHE_FORMAT = getenv("TACHYON_RUNTIME_CACHE_FORMAT") || "7";
|
||||
const RUNTIME_CACHE_FORMAT = getenv("TACHYON_RUNTIME_CACHE_FORMAT") || "8";
|
||||
const PERSISTENT_SUBSCRIPTION_CACHE_DIR = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR") || "/etc/tachyon/subscription-cache";
|
||||
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE") || PERSISTENT_SUBSCRIPTION_CACHE_DIR + "/cache-format";
|
||||
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT") || "7";
|
||||
const PENDING_RELOAD_FILE = getenv("TACHYON_PENDING_RELOAD_FILE") || RUNTIME_STATE_DIR + "/reload.pending";
|
||||
const RELOAD_STATE_FILE = getenv("TACHYON_RELOAD_STATE_FILE") || RUNTIME_STATE_DIR + "/reload-state";
|
||||
const RELOAD_STATE_FORMAT = getenv("TACHYON_RELOAD_STATE_FORMAT") || "1";
|
||||
@@ -330,10 +331,6 @@ function module_output(args) {
|
||||
return command_output(module_command(args));
|
||||
}
|
||||
|
||||
function module_env_status(env, args) {
|
||||
return command_status(command_env(env) + " " + module_command(args));
|
||||
}
|
||||
|
||||
function nft_module_success(args) {
|
||||
let command_args = [ LIB_DIR + "/nft/apply.uc" ];
|
||||
for (let arg in args)
|
||||
@@ -1020,7 +1017,8 @@ function ensure_subscription_runtime_dirs() {
|
||||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT
|
||||
}) + " " + command_from_args([
|
||||
"ucode",
|
||||
"-L", LIB_DIR,
|
||||
@@ -1166,6 +1164,7 @@ function subscription_worker_env() {
|
||||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||||
TACHYON_SUBSCRIPTION_UPDATE_JOB_FINISHED_TTL_MINUTES: SUBSCRIPTION_JOB_FINISHED_TTL_MINUTES,
|
||||
TACHYON_SUBSCRIPTION_UPDATE_JOB_ORPHAN_OUTPUT_TTL_MINUTES: SUBSCRIPTION_JOB_ORPHAN_OUTPUT_TTL_MINUTES,
|
||||
TACHYON_UI_ACTION_STALE_GRACE_SECONDS: SUBSCRIPTION_JOB_STALE_GRACE_SECONDS
|
||||
@@ -2510,6 +2509,7 @@ function subscription_cache_env() {
|
||||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||||
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
||||
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
||||
SB_SERVICE_MIXED_INBOUND_ADDRESS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let common = require("core.common");
|
||||
let uci_core = require("core.uci");
|
||||
@@ -358,6 +358,78 @@ function priority_groups(section) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function dashboard_filter_mode(section) {
|
||||
return option(section, "dashboard_filter_mode", "disabled") || "disabled";
|
||||
}
|
||||
|
||||
function dashboard_detect_server_country(section) {
|
||||
return option(section, "dashboard_detect_server_country", "flag_emoji") || "flag_emoji";
|
||||
}
|
||||
|
||||
function dashboard_include_countries(section) {
|
||||
return list_value(section, "dashboard_include_countries");
|
||||
}
|
||||
|
||||
function dashboard_include_outbounds(section) {
|
||||
return list_value(section, "dashboard_include_outbounds");
|
||||
}
|
||||
|
||||
function dashboard_include_regex(section) {
|
||||
return list_value(section, "dashboard_include_regex");
|
||||
}
|
||||
|
||||
function dashboard_include_proxy_parameters(section) {
|
||||
return bool_option(section, "dashboard_include_proxy_parameters", false);
|
||||
}
|
||||
|
||||
function dashboard_include_protocols(section) {
|
||||
return list_value(section, "dashboard_include_protocols");
|
||||
}
|
||||
|
||||
function dashboard_include_transports(section) {
|
||||
return list_value(section, "dashboard_include_transports");
|
||||
}
|
||||
|
||||
function dashboard_include_securities(section) {
|
||||
return list_value(section, "dashboard_include_securities");
|
||||
}
|
||||
|
||||
function dashboard_include_groups(section) {
|
||||
return list_value(section, "dashboard_include_groups");
|
||||
}
|
||||
|
||||
function dashboard_exclude_countries(section) {
|
||||
return list_value(section, "dashboard_exclude_countries");
|
||||
}
|
||||
|
||||
function dashboard_exclude_outbounds(section) {
|
||||
return list_value(section, "dashboard_exclude_outbounds");
|
||||
}
|
||||
|
||||
function dashboard_exclude_regex(section) {
|
||||
return list_value(section, "dashboard_exclude_regex");
|
||||
}
|
||||
|
||||
function dashboard_exclude_proxy_parameters(section) {
|
||||
return bool_option(section, "dashboard_exclude_proxy_parameters", false);
|
||||
}
|
||||
|
||||
function dashboard_exclude_protocols(section) {
|
||||
return list_value(section, "dashboard_exclude_protocols");
|
||||
}
|
||||
|
||||
function dashboard_exclude_transports(section) {
|
||||
return list_value(section, "dashboard_exclude_transports");
|
||||
}
|
||||
|
||||
function dashboard_exclude_securities(section) {
|
||||
return list_value(section, "dashboard_exclude_securities");
|
||||
}
|
||||
|
||||
function dashboard_exclude_groups(section) {
|
||||
return list_value(section, "dashboard_exclude_groups");
|
||||
}
|
||||
|
||||
function priority_group_child(section, value) {
|
||||
return owned_child_section(section, "priority_group", value);
|
||||
}
|
||||
@@ -672,13 +744,6 @@ function urltest_detect_server_country(section, value) {
|
||||
option(section, "detect_server_country", "flag_emoji") || "flag_emoji");
|
||||
}
|
||||
|
||||
function urltest_hide_added_outbounds(section, value) {
|
||||
let child = urltest_child(section, value);
|
||||
if (child != null)
|
||||
return child_bool(child, "hide_added_outbounds", false);
|
||||
return item_bool(section, "urltest_settings", value, "hide_added_outbounds", false);
|
||||
}
|
||||
|
||||
function urltest_pin_dashboard(section, value) {
|
||||
let child = urltest_child(section, value);
|
||||
if (child != null)
|
||||
@@ -819,11 +884,6 @@ function priority_group_interrupt_exist_connections(section, value) {
|
||||
return child != null ? child_bool(child, "interrupt_exist_connections", true) : true;
|
||||
}
|
||||
|
||||
function priority_group_hide_added_outbounds(section, value) {
|
||||
let child = priority_group_child(section, value);
|
||||
return child != null ? child_bool(child, "hide_added_outbounds", false) : false;
|
||||
}
|
||||
|
||||
function priority_group_pin_dashboard(section, value) {
|
||||
let child = priority_group_child(section, value);
|
||||
return child != null ? child_bool(child, "pin_dashboard", true) : true;
|
||||
@@ -995,6 +1055,24 @@ return {
|
||||
interfaces,
|
||||
outbound_jsons,
|
||||
urltests,
|
||||
dashboard_filter_mode,
|
||||
dashboard_detect_server_country,
|
||||
dashboard_include_countries,
|
||||
dashboard_include_outbounds,
|
||||
dashboard_include_regex,
|
||||
dashboard_include_proxy_parameters,
|
||||
dashboard_include_protocols,
|
||||
dashboard_include_transports,
|
||||
dashboard_include_securities,
|
||||
dashboard_include_groups,
|
||||
dashboard_exclude_countries,
|
||||
dashboard_exclude_outbounds,
|
||||
dashboard_exclude_regex,
|
||||
dashboard_exclude_proxy_parameters,
|
||||
dashboard_exclude_protocols,
|
||||
dashboard_exclude_transports,
|
||||
dashboard_exclude_securities,
|
||||
dashboard_exclude_groups,
|
||||
community_lists,
|
||||
rule_sets,
|
||||
rule_sets_with_subnets,
|
||||
@@ -1029,7 +1107,6 @@ return {
|
||||
urltest_interrupt_exist_connections,
|
||||
urltest_filter_mode,
|
||||
urltest_detect_server_country,
|
||||
urltest_hide_added_outbounds,
|
||||
urltest_pin_dashboard,
|
||||
urltest_include_countries,
|
||||
urltest_include_outbounds,
|
||||
@@ -1057,7 +1134,6 @@ return {
|
||||
priority_group_switch_to_faster_same_priority,
|
||||
priority_group_fastest_check_interval,
|
||||
priority_group_interrupt_exist_connections,
|
||||
priority_group_hide_added_outbounds,
|
||||
priority_group_pin_dashboard,
|
||||
priority_level_display_name,
|
||||
priority_level_order,
|
||||
|
||||
@@ -184,19 +184,6 @@ function strip_first_scheme_marker(value) {
|
||||
return marker >= 0 ? substr(value, marker + 3) : value;
|
||||
}
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function url_get_scheme(value) {
|
||||
value = as_string(value);
|
||||
let marker = index(value, "://");
|
||||
@@ -268,7 +255,7 @@ function url_get_query_param(value, param) {
|
||||
|
||||
function url_file_extension(value) {
|
||||
let basename = as_string(value);
|
||||
let slash = str_last_index(basename, "/");
|
||||
let slash = rindex(basename, "/");
|
||||
if (slash >= 0)
|
||||
basename = substr(basename, slash + 1);
|
||||
|
||||
@@ -280,7 +267,7 @@ function url_file_extension(value) {
|
||||
if (fragment >= 0)
|
||||
basename = substr(basename, 0, fragment);
|
||||
|
||||
let dot = str_last_index(basename, ".");
|
||||
let dot = rindex(basename, ".");
|
||||
if (dot >= 0)
|
||||
print(substr(basename, dot + 1), "\n");
|
||||
else
|
||||
|
||||
@@ -5,22 +5,6 @@ let as_string = common.as_string;
|
||||
|
||||
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
let result = -1;
|
||||
let start = 0;
|
||||
|
||||
while (true) {
|
||||
let offset = index(substr(value, start), needle);
|
||||
if (offset < 0)
|
||||
return result;
|
||||
|
||||
result = start + offset;
|
||||
start = result + length(needle);
|
||||
}
|
||||
}
|
||||
|
||||
function decimal_text(value, strict) {
|
||||
value = as_string(value);
|
||||
if (value == "" || match(value, /^[0-9]+$/) == null)
|
||||
|
||||
@@ -86,6 +86,31 @@ function apk_list_version(package_name, output) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function apk_query_version(package_name, output) {
|
||||
package_name = as_string(package_name);
|
||||
let packages;
|
||||
|
||||
try {
|
||||
packages = json(as_string(output));
|
||||
}
|
||||
catch (e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (type(packages) != "array")
|
||||
return "";
|
||||
|
||||
for (let package in packages) {
|
||||
if (type(package) != "object" || as_string(package.name) != package_name)
|
||||
continue;
|
||||
|
||||
let version = as_string(package.version);
|
||||
if (version != "")
|
||||
return version;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function apk_version(package_name) {
|
||||
package_name = as_string(package_name);
|
||||
if (!apk_installed(package_name))
|
||||
@@ -110,9 +135,9 @@ function apk_available_version(package_name) {
|
||||
package_name = as_string(package_name);
|
||||
if (!command_exists("apk"))
|
||||
return "";
|
||||
return apk_manifest_version(
|
||||
return apk_query_version(
|
||||
package_name,
|
||||
command_output([ "apk", "list", "--available", "--manifest", package_name ])
|
||||
command_output([ "apk", "query", "--from", "repositories", "--available", "--format", "json", "--fields", "name,version", package_name ])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let common = require("core.common");
|
||||
|
||||
@@ -4,19 +4,6 @@ let common = require("core.common");
|
||||
|
||||
let as_string = common.as_string;
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function first_index_any(value, needles, start) {
|
||||
value = as_string(value);
|
||||
start = int(start || 0);
|
||||
@@ -104,7 +91,7 @@ function strip_first_scheme_marker(value) {
|
||||
|
||||
function authority(value) {
|
||||
value = strip_first_scheme_marker(value);
|
||||
let at = str_last_index(value, "@");
|
||||
let at = rindex(value, "@");
|
||||
if (at >= 0)
|
||||
value = substr(value, at + 1);
|
||||
|
||||
@@ -123,7 +110,7 @@ function host(value) {
|
||||
if (colon < 0)
|
||||
return value_authority;
|
||||
|
||||
return str_last_index(value_authority, ":") == colon ? substr(value_authority, 0, colon) : value_authority;
|
||||
return rindex(value_authority, ":") == colon ? substr(value_authority, 0, colon) : value_authority;
|
||||
}
|
||||
|
||||
function port(value) {
|
||||
@@ -136,7 +123,7 @@ function port(value) {
|
||||
}
|
||||
|
||||
let colon = index(value_authority, ":");
|
||||
if (colon < 0 || str_last_index(value_authority, ":") != colon)
|
||||
if (colon < 0 || rindex(value_authority, ":") != colon)
|
||||
return "";
|
||||
|
||||
return substr(value_authority, colon + 1);
|
||||
@@ -146,7 +133,7 @@ function userinfo(value) {
|
||||
let value_authority = strip_first_scheme_marker(strip_fragment(value));
|
||||
let end = first_index_any(value_authority, ["/", "?", "#"], 0);
|
||||
value_authority = end >= 0 ? substr(value_authority, 0, end) : value_authority;
|
||||
let at = str_last_index(value_authority, "@");
|
||||
let at = rindex(value_authority, "@");
|
||||
return at >= 0 ? decode(substr(value_authority, 0, at)) : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let core_ip = require("core.ip");
|
||||
@@ -323,6 +323,7 @@ function mask_option_path(line, token) {
|
||||
|
||||
function tachyon_config_masked_line(line) {
|
||||
line = mask_after_token(line, "option proxy_string");
|
||||
line = mask_after_token(line, "option hwid");
|
||||
line = mask_after_token(line, "option subscription_url");
|
||||
line = mask_after_token(line, "list subscription_urls");
|
||||
line = mask_after_token(line, "list urltest_proxy_links");
|
||||
@@ -602,19 +603,6 @@ function str_remove_suffix(value, suffix) {
|
||||
return str_endswith(value, suffix) ? substr(value, 0, length(value) - length(suffix)) : value;
|
||||
}
|
||||
|
||||
function str_last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
if (needle == "")
|
||||
return length(value);
|
||||
|
||||
for (let i = length(value) - length(needle); i >= 0; i--)
|
||||
if (substr(value, i, length(needle)) == needle)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function contains(values, needle) {
|
||||
needle = as_string(needle);
|
||||
for (let value in array_or_empty(values))
|
||||
@@ -630,7 +618,7 @@ function netstat_fields(line) {
|
||||
|
||||
function netstat_addr_port(addr) {
|
||||
addr = as_string(addr);
|
||||
let colon = str_last_index(addr, ":");
|
||||
let colon = rindex(addr, ":");
|
||||
return colon >= 0 ? substr(addr, colon + 1) : addr;
|
||||
}
|
||||
|
||||
@@ -640,7 +628,7 @@ function netstat_addr_host(addr) {
|
||||
let end = index(addr, "]");
|
||||
return end > 0 ? substr(addr, 1, end - 1) : addr;
|
||||
}
|
||||
let colon = str_last_index(addr, ":");
|
||||
let colon = rindex(addr, ":");
|
||||
return colon >= 0 ? substr(addr, 0, colon) : addr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let uci = require("core.uci");
|
||||
@@ -15,19 +15,6 @@ function run(command) {
|
||||
return system(command) == 0;
|
||||
}
|
||||
|
||||
function output(command) {
|
||||
let pipe = fs.popen(command, "r");
|
||||
if (!pipe)
|
||||
return "";
|
||||
|
||||
let data = pipe.read("all");
|
||||
let status = pipe.close();
|
||||
if (status != 0 || data == null)
|
||||
return "";
|
||||
|
||||
return replace(as_string(data), /[\r\n]+$/g, "");
|
||||
}
|
||||
|
||||
function uci_available() {
|
||||
return uci.available();
|
||||
}
|
||||
|
||||
@@ -22,32 +22,12 @@ function trim_left_space(value) {
|
||||
return substr(value, i);
|
||||
}
|
||||
|
||||
function last_index(value, needle) {
|
||||
value = as_string(value);
|
||||
needle = as_string(needle);
|
||||
let result = -1;
|
||||
let offset = 0;
|
||||
|
||||
if (needle == "")
|
||||
return -1;
|
||||
|
||||
while (offset <= length(value)) {
|
||||
let found = index(substr(value, offset), needle);
|
||||
if (found < 0)
|
||||
break;
|
||||
result = offset + found;
|
||||
offset = result + length(needle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function strip_binary_prefix(line, binary) {
|
||||
line = as_string(line);
|
||||
binary = as_string(binary || "nfqws");
|
||||
|
||||
let path_marker = "/" + binary + ":";
|
||||
let path_index = last_index(line, path_marker);
|
||||
let path_index = rindex(line, path_marker);
|
||||
if (path_index >= 0)
|
||||
return trim_left_space(substr(line, path_index + length(path_marker)));
|
||||
|
||||
@@ -109,7 +89,7 @@ function validation_summary(binary) {
|
||||
}
|
||||
|
||||
function value_after_last_colon(summary) {
|
||||
let colon = last_index(summary, ":");
|
||||
let colon = rindex(summary, ":");
|
||||
if (colon < 0)
|
||||
return "";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env ucode
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let common = require("core.common");
|
||||
let as_string = common.as_string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user