feat: expand subscription formats and dashboard UX

Add a subscription parser pipeline for sing-box JSON, URI lists, base64 subscriptions, and Clash/Mihomo proxies. Improve dashboard proxy refresh and copy-link handling. Fix subscription update scheduling, stale cache handling, and Podkop lifecycle state reporting for restart/reload.
This commit is contained in:
ushan0v
2026-05-12 13:06:46 +03:00
parent d3ec8aad87
commit ba8b268d17
22 changed files with 3357 additions and 927 deletions
+55 -40
View File
@@ -3,25 +3,25 @@
"call": "✔ Running",
"key": "✔ Running",
"places": [
"src/podkop/tabs/dashboard/initController.ts:345",
"src/podkop/tabs/dashboard/initController.ts:356",
"src/podkop/tabs/dashboard/initController.ts:369"
"src/podkop/tabs/dashboard/initController.ts:387",
"src/podkop/tabs/dashboard/initController.ts:398",
"src/podkop/tabs/dashboard/initController.ts:411"
]
},
{
"call": "✘ Not installed",
"key": "✘ Not installed",
"places": [
"src/podkop/tabs/dashboard/initController.ts:367"
"src/podkop/tabs/dashboard/initController.ts:409"
]
},
{
"call": "✘ Stopped",
"key": "✘ Stopped",
"places": [
"src/podkop/tabs/dashboard/initController.ts:346",
"src/podkop/tabs/dashboard/initController.ts:357",
"src/podkop/tabs/dashboard/initController.ts:370"
"src/podkop/tabs/dashboard/initController.ts:388",
"src/podkop/tabs/dashboard/initController.ts:399",
"src/podkop/tabs/dashboard/initController.ts:412"
]
},
{
@@ -64,7 +64,7 @@
"call": "Active Connections",
"key": "Active Connections",
"places": [
"src/podkop/tabs/dashboard/initController.ts:307"
"src/podkop/tabs/dashboard/initController.ts:349"
]
},
{
@@ -219,11 +219,19 @@
"src/podkop/tabs/diagnostic/checks/runZapretCheck.ts:27"
]
},
{
"call": "Check unavailable",
"key": "Check unavailable",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:503",
"src/podkop/tabs/diagnostic/initController.ts:516"
]
},
{
"call": "Checking",
"key": "Checking",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:472"
"src/podkop/tabs/diagnostic/initController.ts:481"
]
},
{
@@ -334,6 +342,14 @@
"src/partials/modal/renderModal.ts:190"
]
},
{
"call": "Copy proxy link",
"key": "Copy proxy link",
"places": [
"src/podkop/tabs/dashboard/partials/renderSections.ts:93",
"src/podkop/tabs/dashboard/partials/renderSections.ts:94"
]
},
{
"call": "Currently unavailable",
"key": "Currently unavailable",
@@ -352,7 +368,7 @@
"call": "Dashboard currently unavailable",
"key": "Dashboard currently unavailable",
"places": [
"src/podkop/tabs/dashboard/partials/renderSections.ts:19"
"src/podkop/tabs/dashboard/partials/renderSections.ts:24"
]
},
{
@@ -373,8 +389,8 @@
"call": "Dev",
"key": "Dev",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:483",
"src/podkop/tabs/diagnostic/initController.ts:534"
"src/podkop/tabs/diagnostic/initController.ts:492",
"src/podkop/tabs/diagnostic/initController.ts:543"
]
},
{
@@ -548,8 +564,8 @@
"call": "Downlink",
"key": "Downlink",
"places": [
"src/podkop/tabs/dashboard/initController.ts:241",
"src/podkop/tabs/dashboard/initController.ts:275"
"src/podkop/tabs/dashboard/initController.ts:283",
"src/podkop/tabs/dashboard/initController.ts:317"
]
},
{
@@ -684,8 +700,8 @@
]
},
{
"call": "Enter the subscription URL to fetch proxy configurations from your provider",
"key": "Enter the subscription URL to fetch proxy configurations from your provider",
"call": "Enter the subscription URL",
"key": "Enter the subscription URL",
"places": [
"../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1822"
]
@@ -783,9 +799,9 @@
"call": "Fastest",
"key": "Fastest",
"places": [
"src/podkop/methods/custom/getDashboardSections.ts:188",
"src/podkop/methods/custom/getDashboardSections.ts:220",
"src/podkop/methods/custom/getDashboardSections.ts:257",
"src/podkop/methods/custom/getDashboardSections.ts:193",
"src/podkop/methods/custom/getDashboardSections.ts:225",
"src/podkop/methods/custom/getDashboardSections.ts:263",
"src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59"
]
},
@@ -1221,7 +1237,7 @@
"call": "Latest",
"key": "Latest",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:544"
"src/podkop/tabs/diagnostic/initController.ts:553"
]
},
{
@@ -1320,7 +1336,7 @@
"call": "Memory Usage",
"key": "Memory Usage",
"places": [
"src/podkop/tabs/dashboard/initController.ts:311"
"src/podkop/tabs/dashboard/initController.ts:353"
]
},
{
@@ -1405,7 +1421,7 @@
"call": "not installed",
"key": "not installed",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:567"
"src/podkop/tabs/diagnostic/initController.ts:576"
]
},
{
@@ -1478,7 +1494,7 @@
"call": "Outdated",
"key": "Outdated",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:523"
"src/podkop/tabs/diagnostic/initController.ts:532"
]
},
{
@@ -1598,6 +1614,13 @@
"../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/node.js:31"
]
},
{
"call": "Proxy link is unavailable",
"key": "Proxy link is unavailable",
"places": [
"src/podkop/tabs/dashboard/initController.ts:202"
]
},
{
"call": "Proxy traffic is not routed via FakeIP",
"key": "Proxy traffic is not routed via FakeIP",
@@ -1875,7 +1898,7 @@
"call": "Services info",
"key": "Services info",
"places": [
"src/podkop/tabs/dashboard/initController.ts:340"
"src/podkop/tabs/dashboard/initController.ts:382"
]
},
{
@@ -2052,7 +2075,7 @@
"call": "System info",
"key": "System info",
"places": [
"src/podkop/tabs/dashboard/initController.ts:304"
"src/podkop/tabs/dashboard/initController.ts:346"
]
},
{
@@ -2073,7 +2096,7 @@
"call": "Test latency",
"key": "Test latency",
"places": [
"src/podkop/tabs/dashboard/partials/renderSections.ts:108"
"src/podkop/tabs/dashboard/partials/renderSections.ts:139"
]
},
{
@@ -2143,14 +2166,14 @@
"call": "Traffic",
"key": "Traffic",
"places": [
"src/podkop/tabs/dashboard/initController.ts:238"
"src/podkop/tabs/dashboard/initController.ts:280"
]
},
{
"call": "Traffic Total",
"key": "Traffic Total",
"places": [
"src/podkop/tabs/dashboard/initController.ts:268"
"src/podkop/tabs/dashboard/initController.ts:310"
]
},
{
@@ -2235,7 +2258,7 @@
"src/podkop/tabs/diagnostic/initController.ts:34",
"src/podkop/tabs/diagnostic/initController.ts:36",
"src/podkop/tabs/diagnostic/initController.ts:37",
"src/podkop/tabs/diagnostic/initController.ts:457"
"src/podkop/tabs/diagnostic/initController.ts:466"
]
},
{
@@ -2263,8 +2286,8 @@
"call": "Uplink",
"key": "Uplink",
"places": [
"src/podkop/tabs/dashboard/initController.ts:240",
"src/podkop/tabs/dashboard/initController.ts:271"
"src/podkop/tabs/dashboard/initController.ts:282",
"src/podkop/tabs/dashboard/initController.ts:313"
]
},
{
@@ -2397,14 +2420,6 @@
"../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:480"
]
},
{
"call": "Check unavailable",
"key": "Check unavailable",
"places": [
"src/podkop/tabs/diagnostic/initController.ts:494",
"src/podkop/tabs/diagnostic/initController.ts:507"
]
},
{
"call": "View logs",
"key": "View logs",
@@ -2506,4 +2521,4 @@
"src/podkop/tabs/diagnostic/checks/runZapretCheck.ts:82"
]
}
]
]
+46 -37
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-07 12:24+0300\n"
"PO-Revision-Date: 2026-05-07 12:24+0300\n"
"POT-Creation-Date: 2026-05-11 21:41+0300\n"
"PO-Revision-Date: 2026-05-11 21:41+0300\n"
"Last-Translator: ushan0v <142658494+ushan0v@users.noreply.github.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -16,19 +16,19 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/podkop/tabs/dashboard/initController.ts:345
#: src/podkop/tabs/dashboard/initController.ts:356
#: src/podkop/tabs/dashboard/initController.ts:369
#: src/podkop/tabs/dashboard/initController.ts:387
#: src/podkop/tabs/dashboard/initController.ts:398
#: src/podkop/tabs/dashboard/initController.ts:411
msgid "✔ Running"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:367
#: src/podkop/tabs/dashboard/initController.ts:409
msgid "✘ Not installed"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:346
#: src/podkop/tabs/dashboard/initController.ts:357
#: src/podkop/tabs/dashboard/initController.ts:370
#: src/podkop/tabs/dashboard/initController.ts:388
#: src/podkop/tabs/dashboard/initController.ts:399
#: src/podkop/tabs/dashboard/initController.ts:412
msgid "✘ Stopped"
msgstr ""
@@ -53,7 +53,7 @@ msgstr ""
msgid "Action"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:307
#: src/podkop/tabs/dashboard/initController.ts:349
msgid "Active Connections"
msgstr ""
@@ -146,7 +146,12 @@ msgstr ""
msgid "Cannot receive checks result"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:472
#: src/podkop/tabs/diagnostic/initController.ts:503
#: src/podkop/tabs/diagnostic/initController.ts:516
msgid "Check unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:481
msgid "Checking"
msgstr ""
@@ -216,6 +221,11 @@ msgstr ""
msgid "Copy"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:93
#: src/podkop/tabs/dashboard/partials/renderSections.ts:94
msgid "Copy proxy link"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22
msgid "Currently unavailable"
msgstr ""
@@ -224,7 +234,7 @@ msgstr ""
msgid "Dashboard"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:19
#: src/podkop/tabs/dashboard/partials/renderSections.ts:24
msgid "Dashboard currently unavailable"
msgstr ""
@@ -236,8 +246,8 @@ msgstr ""
msgid "Delay value cannot be empty"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:483
#: src/podkop/tabs/diagnostic/initController.ts:534
#: src/podkop/tabs/diagnostic/initController.ts:492
#: src/podkop/tabs/diagnostic/initController.ts:543
msgid "Dev"
msgstr ""
@@ -339,8 +349,8 @@ msgstr ""
msgid "Dont Touch My DHCP!"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:241
#: src/podkop/tabs/dashboard/initController.ts:275
#: src/podkop/tabs/dashboard/initController.ts:283
#: src/podkop/tabs/dashboard/initController.ts:317
msgid "Downlink"
msgstr ""
@@ -422,7 +432,7 @@ msgid "Enter a complete sing-box outbound object"
msgstr ""
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1822
msgid "Enter the subscription URL to fetch proxy configurations from your provider"
msgid "Enter the subscription URL"
msgstr ""
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/node.js:103
@@ -478,9 +488,9 @@ msgstr ""
msgid "Failed to execute!"
msgstr ""
#: src/podkop/methods/custom/getDashboardSections.ts:188
#: src/podkop/methods/custom/getDashboardSections.ts:220
#: src/podkop/methods/custom/getDashboardSections.ts:257
#: src/podkop/methods/custom/getDashboardSections.ts:193
#: src/podkop/methods/custom/getDashboardSections.ts:225
#: src/podkop/methods/custom/getDashboardSections.ts:263
#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59
msgid "Fastest"
msgstr ""
@@ -733,7 +743,7 @@ msgstr ""
msgid "Keyword must not contain spaces"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:544
#: src/podkop/tabs/diagnostic/initController.ts:553
msgid "Latest"
msgstr ""
@@ -790,7 +800,7 @@ msgstr ""
msgid "Maximum response time delta in milliseconds"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:311
#: src/podkop/tabs/dashboard/initController.ts:353
msgid "Memory Usage"
msgstr ""
@@ -839,7 +849,7 @@ msgstr ""
msgid "Not implement yet"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:567
#: src/podkop/tabs/diagnostic/initController.ts:576
msgid "not installed"
msgstr ""
@@ -885,7 +895,7 @@ msgstr ""
msgid "Outbound JSON"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:523
#: src/podkop/tabs/diagnostic/initController.ts:532
msgid "Outdated"
msgstr ""
@@ -958,6 +968,10 @@ msgstr ""
msgid "Proxy Configuration URL"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:202
msgid "Proxy link is unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:68
msgid "Proxy traffic is not routed via FakeIP"
msgstr ""
@@ -1114,7 +1128,7 @@ msgstr ""
msgid "Service list"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:340
#: src/podkop/tabs/dashboard/initController.ts:382
msgid "Services info"
msgstr ""
@@ -1216,7 +1230,7 @@ msgstr ""
msgid "Successfully copied!"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:304
#: src/podkop/tabs/dashboard/initController.ts:346
msgid "System info"
msgstr ""
@@ -1228,7 +1242,7 @@ msgstr ""
msgid "Table exist"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:108
#: src/podkop/tabs/dashboard/partials/renderSections.ts:139
msgid "Test latency"
msgstr ""
@@ -1268,11 +1282,11 @@ msgstr ""
msgid "Time in seconds for DNS record caching (default: 60)"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:238
#: src/podkop/tabs/dashboard/initController.ts:280
msgid "Traffic"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:268
#: src/podkop/tabs/dashboard/initController.ts:310
msgid "Traffic Total"
msgstr ""
@@ -1324,7 +1338,7 @@ msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:34
#: src/podkop/tabs/diagnostic/initController.ts:36
#: src/podkop/tabs/diagnostic/initController.ts:37
#: src/podkop/tabs/diagnostic/initController.ts:457
#: src/podkop/tabs/diagnostic/initController.ts:466
msgid "unknown"
msgstr ""
@@ -1340,8 +1354,8 @@ msgstr ""
msgid "Update interval"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:240
#: src/podkop/tabs/dashboard/initController.ts:271
#: src/podkop/tabs/dashboard/initController.ts:282
#: src/podkop/tabs/dashboard/initController.ts:313
msgid "Uplink"
msgstr ""
@@ -1427,11 +1441,6 @@ msgstr ""
msgid "Validation errors:"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:494
#: src/podkop/tabs/diagnostic/initController.ts:507
msgid "Check unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:282
#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107
msgid "View logs"
+13 -7
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-07 15:24+0300\n"
"PO-Revision-Date: 2026-05-07 15:24+0300\n"
"POT-Creation-Date: 2026-05-11 00:41+0300\n"
"PO-Revision-Date: 2026-05-11 00:41+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -107,6 +107,9 @@ msgstr "Путь к файлу кэша не может быть пустым"
msgid "Cannot receive checks result"
msgstr "Не удалось получить результаты проверки"
msgid "Check unavailable"
msgstr "Проверка недоступна"
msgid "Checking"
msgstr "Проверка"
@@ -152,6 +155,9 @@ msgstr "URL подключения"
msgid "Copy"
msgstr "Копировать"
msgid "Copy proxy link"
msgstr "Скопировать ссылку прокси"
msgid "Currently unavailable"
msgstr "Временно недоступно"
@@ -296,8 +302,8 @@ msgstr "Включить доступ YACD WAN"
msgid "Enter a complete sing-box outbound object"
msgstr "Введите полный объект outbound sing-box"
msgid "Enter the subscription URL to fetch proxy configurations from your provider"
msgstr "Введите URL подписки для получения конфигураций прокси от провайдера"
msgid "Enter the subscription URL"
msgstr "Введите URL подписки"
msgid "Every 3 minutes"
msgstr "Каждые 3 минуты"
@@ -674,6 +680,9 @@ msgstr "Порт локального смешанного прокси для
msgid "Proxy Configuration URL"
msgstr "URL конфигурации прокси"
msgid "Proxy link is unavailable"
msgstr "Ссылка прокси недоступна"
msgid "Proxy traffic is not routed via FakeIP"
msgstr "Прокси-трафик не маршрутизируется через FakeIP"
@@ -1004,9 +1013,6 @@ msgstr "Валидно"
msgid "Validation errors:"
msgstr "Ошибки валидации:"
msgid "Check unavailable"
msgstr "Проверка недоступна"
msgid "View logs"
msgstr "Посмотреть логи"
+1
View File
@@ -16,3 +16,4 @@ export * from './renderSquareChartGanttIcon24';
export * from './renderCogIcon24';
export * from './renderSearchIcon24';
export * from './renderBookOpenTextIcon24';
export * from './renderCopyIcon24';
@@ -0,0 +1,32 @@
import { svgEl } from '../helpers/svgEl';
export function renderCopyIcon24() {
return svgEl(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
width: '24',
height: '24',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
class: 'lucide lucide-copy-icon lucide-copy',
},
[
svgEl('rect', {
width: '14',
height: '14',
x: '8',
y: '8',
rx: '2',
ry: '2',
}),
svgEl('path', {
d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2',
}),
],
);
}
@@ -189,7 +189,7 @@ export function renderModal(
classNames: ['cbi-button-apply'],
text: _('Copy'),
onClick: () =>
copyToClipboard(` \`\`\`${name} \n ${currentText} \n \`\`\``),
copyToClipboard(`\`\`\`${name}\n${currentText}\n\`\`\``),
}),
renderButton({
classNames: ['cbi-button-remove'],
@@ -59,12 +59,10 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
value,
}),
);
const data = configSections
.filter(
(section) =>
section.enabled !== '0' &&
getSectionAction(section) === 'proxy',
section.enabled !== '0' && getSectionAction(section) === 'proxy',
)
.map((section) => {
const displayName = getDisplayName(section);
@@ -82,6 +80,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [
{
@@ -90,6 +89,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
link: activeConfigs?.[0] || '',
},
],
};
@@ -113,6 +113,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [
{
@@ -147,11 +148,13 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
latency: item?.outbound?.value?.history?.[0]?.delay || 0,
type: item?.outbound?.value?.type || '',
selected: selector?.value?.now === item?.outbound?.code,
link: item.link,
}));
return {
withTagSelect: true,
code: selector?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds,
};
@@ -176,11 +179,13 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
latency: item?.value?.history?.[0]?.delay || 0,
type: item?.value?.type || '',
selected: selector?.value?.now === item?.code,
link: section.urltest_proxy_links?.[index] || '',
}));
return {
withTagSelect: true,
code: selector?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [
{
@@ -250,6 +255,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: true,
code: selector?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [
{
@@ -267,6 +273,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: true,
code: selector?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds,
};
@@ -280,6 +287,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [
{
@@ -296,6 +304,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
return {
withTagSelect: false,
code: section['.name'],
sectionName: section['.name'],
displayName,
outbounds: [],
};
@@ -20,6 +20,11 @@ export const PodkopShellMethods = {
),
getStatus: async () =>
callBaseMethod<Podkop.GetStatus>(Podkop.AvailableMethods.GET_STATUS),
getOutboundLink: async (section: string, tag: string) =>
callBaseMethod<Podkop.GetOutboundLink>(
Podkop.AvailableMethods.GET_OUTBOUND_LINK,
[section, tag],
),
checkSingBox: async () =>
callBaseMethod<Podkop.SingBoxCheckResult>(
Podkop.AvailableMethods.CHECK_SING_BOX,
@@ -3,39 +3,55 @@ import {
onMount,
preserveScrollForPage,
} from '../../../helpers';
import { copyToClipboard } from '../../../helpers/copyToClipboard';
import { showToast } from '../../../helpers/showToast';
import { prettyBytes } from '../../../helpers/prettyBytes';
import { CustomPodkopMethods, PodkopShellMethods } from '../../methods';
import { logger, socket, store, StoreType } from '../../services';
import { renderSections, renderWidget } from './partials';
import { fetchServicesInfo } from '../../fetchers';
import { getClashApiSecret } from '../../methods/custom/getClashApiSecret';
import { Podkop } from '../../types';
const SECTIONS_REFRESH_INTERVAL_MS = 5000;
let sectionsRefreshTimer: ReturnType<typeof setInterval> | null = null;
let sectionsRefreshInFlight = false;
// Fetchers
async function fetchDashboardSections() {
const prev = store.get().sectionsWidget;
store.set({
sectionsWidget: {
...prev,
failed: false,
},
});
const { data, success } = await CustomPodkopMethods.getDashboardSections();
if (!success) {
logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch');
if (sectionsRefreshInFlight) {
return;
}
store.set({
sectionsWidget: {
latencyFetching: false,
loading: false,
failed: !success,
data,
},
});
sectionsRefreshInFlight = true;
try {
const prev = store.get().sectionsWidget;
store.set({
sectionsWidget: {
...prev,
failed: false,
},
});
const { data, success } = await CustomPodkopMethods.getDashboardSections();
if (!success) {
logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch');
}
store.set({
sectionsWidget: {
latencyFetching: false,
loading: false,
failed: !success,
data,
},
});
} finally {
sectionsRefreshInFlight = false;
}
}
async function connectToClashSockets() {
@@ -164,6 +180,28 @@ async function handleTestProxyLatency(tag: string) {
});
}
async function handleCopyOutbound(
section: Podkop.OutboundGroup,
outbound: Podkop.Outbound,
) {
if (outbound.link) {
copyToClipboard(outbound.link);
return;
}
const response = await PodkopShellMethods.getOutboundLink(
section.sectionName,
outbound.code,
);
if (response.success && response.data.link) {
copyToClipboard(response.data.link);
return;
}
showToast(_('Proxy link is unavailable'), 'error');
}
// Renderer
async function renderSectionsWidget() {
@@ -183,6 +221,7 @@ async function renderSectionsWidget() {
},
onTestLatency: () => {},
onChooseOutbound: () => {},
onCopyOutbound: () => {},
latencyFetching: sectionsWidget.latencyFetching,
});
@@ -207,6 +246,9 @@ async function renderSectionsWidget() {
onChooseOutbound: (selector, tag) => {
handleChooseOutbound(selector, tag);
},
onCopyOutbound: (section, outbound) => {
void handleCopyOutbound(section, outbound);
},
}),
);
@@ -419,9 +461,18 @@ async function onPageMount() {
await fetchDashboardSections();
await fetchServicesInfo();
await connectToClashSockets();
sectionsRefreshTimer = setInterval(() => {
void fetchDashboardSections();
}, SECTIONS_REFRESH_INTERVAL_MS);
}
function onPageUnmount() {
if (sectionsRefreshTimer) {
clearInterval(sectionsRefreshTimer);
sectionsRefreshTimer = null;
}
sectionsRefreshInFlight = false;
// Remove old listener
store.unsubscribe(onStoreUpdate);
// Clear store
@@ -1,3 +1,4 @@
import { renderCopyIcon24 } from '../../../../icons';
import { Podkop } from '../../../types';
interface IRenderSectionsProps {
@@ -6,6 +7,10 @@ interface IRenderSectionsProps {
section: Podkop.OutboundGroup;
onTestLatency: (tag: string) => void;
onChooseOutbound: (selector: string, tag: string) => void;
onCopyOutbound: (
section: Podkop.OutboundGroup,
outbound: Podkop.Outbound,
) => void;
latencyFetching: boolean;
}
@@ -31,6 +36,7 @@ function renderLoadingState() {
export function renderDefaultState({
section,
onChooseOutbound,
onCopyOutbound,
onTestLatency,
latencyFetching,
}: IRenderSectionsProps) {
@@ -61,6 +67,10 @@ export function renderDefaultState({
return 'pdk_dashboard-page__outbound-grid__item__latency--red';
}
const canCopyLink =
outbound.type?.toLowerCase() !== 'urltest' &&
Boolean(outbound.code || outbound.link);
return E(
'div',
{
@@ -70,7 +80,28 @@ export function renderDefaultState({
onChooseOutbound(section.code, outbound.code),
},
[
E('b', {}, outbound.displayName),
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__header' }, [
E('b', {}, outbound.displayName),
...(canCopyLink
? [
E(
'button',
{
type: 'button',
class:
'btn pdk_dashboard-page__outbound-grid__item__copy-button',
title: _('Copy proxy link'),
'aria-label': _('Copy proxy link'),
click: (event: MouseEvent) => {
event.stopPropagation();
onCopyOutbound(section, outbound);
},
},
renderCopyIcon24(),
),
]
: []),
]),
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [
E(
'div',
@@ -9,7 +9,7 @@ export const styles = `
#cbi-${PODKOP_CBI_PREFIX}-dashboard > h3 {
display: none;
}
.pdk_dashboard-page {
width: 100%;
--dashboard-grid-columns: 4;
@@ -94,6 +94,37 @@ export const styles = `
border-color: var(--success-color-medium, green);
}
.pdk_dashboard-page__outbound-grid__item__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.pdk_dashboard-page__outbound-grid__item__header b {
min-width: 0;
line-height: 1.25;
overflow-wrap: anywhere;
}
.pdk_dashboard-page__outbound-grid__item__copy-button {
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
padding: 1px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
line-height: 1;
}
.pdk_dashboard-page__outbound-grid__item__copy-button svg {
width: 13px;
height: 13px;
}
.pdk_dashboard-page__outbound-grid__item__footer {
display: flex;
align-items: center;
@@ -367,7 +367,9 @@ function renderDiagnosticAvailableActionsWidget() {
const isStopping = lifecycleBusy && lifecycleAction === 'stop';
const isRestarting = lifecycleBusy && lifecycleAction === 'restart';
const isReloading = lifecycleBusy && lifecycleAction === 'reload';
const restartLoading = diagnosticsActions.restart.loading;
const backendRestartLikeLoading = isRestarting || isReloading;
const restartLoading =
diagnosticsActions.restart.loading || backendRestartLikeLoading;
const atLeastOneMutatingActionLoading =
restartLoading ||
diagnosticsActions.start.loading ||
@@ -388,13 +390,14 @@ function renderDiagnosticAvailableActionsWidget() {
(podkopRunning && !isStarting && !isRestarting && !isReloading);
const frozenStartStop =
restartLoading &&
(restartStartStopSnapshot || (podkopRunning ? 'stop' : 'start'));
(restartStartStopSnapshot ||
(backendRestartLikeLoading ? 'stop' : podkopRunning ? 'stop' : 'start'));
const container = document.getElementById('pdk_diagnostic-page-actions');
const renderedActions = renderAvailableActions({
restart: {
loading: restartLoading || isRestarting || isReloading,
loading: restartLoading,
visible: true,
onClick: handleRestart,
disabled: serviceControlsDisabled,
+7
View File
@@ -55,6 +55,7 @@ export namespace Podkop {
CHECK_NFT_RULES = 'check_nft_rules',
CHECK_ZAPRET_RUNTIME = 'check_zapret_runtime',
GET_STATUS = 'get_status',
GET_OUTBOUND_LINK = 'get_outbound_link',
CHECK_SING_BOX = 'check_sing_box',
GET_SING_BOX_STATUS = 'get_sing_box_status',
GET_ZAPRET_STATUS = 'get_zapret_status',
@@ -85,11 +86,13 @@ export namespace Podkop {
latency: number;
type: string;
selected: boolean;
link?: string;
}
export interface OutboundGroup {
withTagSelect: boolean;
code: string;
sectionName: string;
displayName: string;
outbounds: Outbound[];
}
@@ -190,6 +193,10 @@ export namespace Podkop {
lifecycle_pid?: number;
}
export interface GetOutboundLink {
link: string;
}
export interface GetSingBoxStatus {
running: number;
enabled: number;
File diff suppressed because it is too large Load Diff
@@ -382,7 +382,7 @@ function parseRequiredValueOnSave(section_id) {
const error = this.getValidationError(section_id);
return Promise.reject(
new TypeError(
`${_("Option \"%s\" contains an invalid input value.").format(title || this.option)} ${error}`,
`${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${error}`,
),
);
}
@@ -392,7 +392,9 @@ function parseRequiredValueOnSave(section_id) {
const normalized = formValue ? `${formValue}`.trim() : "";
if (!normalized.length) {
return Promise.reject(new TypeError(_("Subscription URL cannot be empty")));
return Promise.reject(
new TypeError(_("Subscription URL cannot be empty")),
);
}
return Promise.resolve(this.write(section_id, normalized));
@@ -1601,7 +1603,9 @@ function validateCustomRulesetReference(value) {
return validateFileReference(
value,
[".srs", ".json"],
_("Rule set must be a direct .srs / .json URL or a local .srs / .json path"),
_(
"Rule set must be a direct .srs / .json URL or a local .srs / .json path",
),
);
}
@@ -1819,7 +1823,7 @@ function createSectionContent(section) {
form.Value,
"subscription_url",
_("Subscription URL"),
_("Enter the subscription URL to fetch proxy configurations from your provider"),
_("Enter the subscription URL"),
);
o.depends({ action: "proxy", proxy_config_type: "subscription" });
o.rmempty = true;
@@ -1830,7 +1834,7 @@ function createSectionContent(section) {
return true;
}
const validation = main.validateUrl(value);
const validation = main.validateUrl(value.trim());
return validation.valid ? true : validation.message;
};
@@ -1839,7 +1843,9 @@ function createSectionContent(section) {
form.Value,
"subscription_update_interval",
_("Subscription update interval"),
_("Use sing-box duration format. Leave empty to disable automatic updates."),
_(
"Use sing-box duration format. Leave empty to disable automatic updates.",
),
);
o.default = "1h";
o.placeholder = "1h";
@@ -1874,7 +1880,9 @@ function createSectionContent(section) {
form.Flag,
"subscription_group_by_countries",
_("Group subscription proxies by country"),
_("Group subscription proxies by the country flag at the beginning of the outbound tag"),
_(
"Group subscription proxies by the country flag at the beginning of the outbound tag",
),
);
o.default = "0";
o.rmempty = false;
@@ -1922,7 +1930,9 @@ function createSectionContent(section) {
form.Value,
"urltest_check_interval",
_("URLTest interval"),
_("Use sing-box duration format. Leave empty to disable periodic URLTest checks."),
_(
"Use sing-box duration format. Leave empty to disable periodic URLTest checks.",
),
);
o.default = "3m";
o.placeholder = "3m";
+13 -7
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-07 15:24+0300\n"
"PO-Revision-Date: 2026-05-07 15:24+0300\n"
"POT-Creation-Date: 2026-05-11 00:41+0300\n"
"PO-Revision-Date: 2026-05-11 00:41+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -107,6 +107,9 @@ msgstr "Путь к файлу кэша не может быть пустым"
msgid "Cannot receive checks result"
msgstr "Не удалось получить результаты проверки"
msgid "Check unavailable"
msgstr "Проверка недоступна"
msgid "Checking"
msgstr "Проверка"
@@ -152,6 +155,9 @@ msgstr "URL подключения"
msgid "Copy"
msgstr "Копировать"
msgid "Copy proxy link"
msgstr "Скопировать ссылку прокси"
msgid "Currently unavailable"
msgstr "Временно недоступно"
@@ -296,8 +302,8 @@ msgstr "Включить доступ YACD WAN"
msgid "Enter a complete sing-box outbound object"
msgstr "Введите полный объект outbound sing-box"
msgid "Enter the subscription URL to fetch proxy configurations from your provider"
msgstr "Введите URL подписки для получения конфигураций прокси от провайдера"
msgid "Enter the subscription URL"
msgstr "Введите URL подписки"
msgid "Every 3 minutes"
msgstr "Каждые 3 минуты"
@@ -674,6 +680,9 @@ msgstr "Порт локального смешанного прокси для
msgid "Proxy Configuration URL"
msgstr "URL конфигурации прокси"
msgid "Proxy link is unavailable"
msgstr "Ссылка прокси недоступна"
msgid "Proxy traffic is not routed via FakeIP"
msgstr "Прокси-трафик не маршрутизируется через FakeIP"
@@ -1004,9 +1013,6 @@ msgstr "Валидно"
msgid "Validation errors:"
msgstr "Ошибки валидации:"
msgid "Check unavailable"
msgstr "Проверка недоступна"
msgid "View logs"
msgstr "Посмотреть логи"
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-07 12:24+0300\n"
"PO-Revision-Date: 2026-05-07 12:24+0300\n"
"POT-Creation-Date: 2026-05-11 21:41+0300\n"
"PO-Revision-Date: 2026-05-11 21:41+0300\n"
"Last-Translator: ushan0v <142658494+ushan0v@users.noreply.github.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -16,19 +16,19 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/podkop/tabs/dashboard/initController.ts:345
#: src/podkop/tabs/dashboard/initController.ts:356
#: src/podkop/tabs/dashboard/initController.ts:369
#: src/podkop/tabs/dashboard/initController.ts:387
#: src/podkop/tabs/dashboard/initController.ts:398
#: src/podkop/tabs/dashboard/initController.ts:411
msgid "✔ Running"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:367
#: src/podkop/tabs/dashboard/initController.ts:409
msgid "✘ Not installed"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:346
#: src/podkop/tabs/dashboard/initController.ts:357
#: src/podkop/tabs/dashboard/initController.ts:370
#: src/podkop/tabs/dashboard/initController.ts:388
#: src/podkop/tabs/dashboard/initController.ts:399
#: src/podkop/tabs/dashboard/initController.ts:412
msgid "✘ Stopped"
msgstr ""
@@ -53,7 +53,7 @@ msgstr ""
msgid "Action"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:307
#: src/podkop/tabs/dashboard/initController.ts:349
msgid "Active Connections"
msgstr ""
@@ -146,7 +146,12 @@ msgstr ""
msgid "Cannot receive checks result"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:472
#: src/podkop/tabs/diagnostic/initController.ts:503
#: src/podkop/tabs/diagnostic/initController.ts:516
msgid "Check unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:481
msgid "Checking"
msgstr ""
@@ -216,6 +221,11 @@ msgstr ""
msgid "Copy"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:93
#: src/podkop/tabs/dashboard/partials/renderSections.ts:94
msgid "Copy proxy link"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22
msgid "Currently unavailable"
msgstr ""
@@ -224,7 +234,7 @@ msgstr ""
msgid "Dashboard"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:19
#: src/podkop/tabs/dashboard/partials/renderSections.ts:24
msgid "Dashboard currently unavailable"
msgstr ""
@@ -236,8 +246,8 @@ msgstr ""
msgid "Delay value cannot be empty"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:483
#: src/podkop/tabs/diagnostic/initController.ts:534
#: src/podkop/tabs/diagnostic/initController.ts:492
#: src/podkop/tabs/diagnostic/initController.ts:543
msgid "Dev"
msgstr ""
@@ -339,8 +349,8 @@ msgstr ""
msgid "Dont Touch My DHCP!"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:241
#: src/podkop/tabs/dashboard/initController.ts:275
#: src/podkop/tabs/dashboard/initController.ts:283
#: src/podkop/tabs/dashboard/initController.ts:317
msgid "Downlink"
msgstr ""
@@ -422,7 +432,7 @@ msgid "Enter a complete sing-box outbound object"
msgstr ""
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1822
msgid "Enter the subscription URL to fetch proxy configurations from your provider"
msgid "Enter the subscription URL"
msgstr ""
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/node.js:103
@@ -478,9 +488,9 @@ msgstr ""
msgid "Failed to execute!"
msgstr ""
#: src/podkop/methods/custom/getDashboardSections.ts:188
#: src/podkop/methods/custom/getDashboardSections.ts:220
#: src/podkop/methods/custom/getDashboardSections.ts:257
#: src/podkop/methods/custom/getDashboardSections.ts:193
#: src/podkop/methods/custom/getDashboardSections.ts:225
#: src/podkop/methods/custom/getDashboardSections.ts:263
#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59
msgid "Fastest"
msgstr ""
@@ -733,7 +743,7 @@ msgstr ""
msgid "Keyword must not contain spaces"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:544
#: src/podkop/tabs/diagnostic/initController.ts:553
msgid "Latest"
msgstr ""
@@ -790,7 +800,7 @@ msgstr ""
msgid "Maximum response time delta in milliseconds"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:311
#: src/podkop/tabs/dashboard/initController.ts:353
msgid "Memory Usage"
msgstr ""
@@ -839,7 +849,7 @@ msgstr ""
msgid "Not implement yet"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:567
#: src/podkop/tabs/diagnostic/initController.ts:576
msgid "not installed"
msgstr ""
@@ -885,7 +895,7 @@ msgstr ""
msgid "Outbound JSON"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:523
#: src/podkop/tabs/diagnostic/initController.ts:532
msgid "Outdated"
msgstr ""
@@ -958,6 +968,10 @@ msgstr ""
msgid "Proxy Configuration URL"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:202
msgid "Proxy link is unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:68
msgid "Proxy traffic is not routed via FakeIP"
msgstr ""
@@ -1114,7 +1128,7 @@ msgstr ""
msgid "Service list"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:340
#: src/podkop/tabs/dashboard/initController.ts:382
msgid "Services info"
msgstr ""
@@ -1216,7 +1230,7 @@ msgstr ""
msgid "Successfully copied!"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:304
#: src/podkop/tabs/dashboard/initController.ts:346
msgid "System info"
msgstr ""
@@ -1228,7 +1242,7 @@ msgstr ""
msgid "Table exist"
msgstr ""
#: src/podkop/tabs/dashboard/partials/renderSections.ts:108
#: src/podkop/tabs/dashboard/partials/renderSections.ts:139
msgid "Test latency"
msgstr ""
@@ -1268,11 +1282,11 @@ msgstr ""
msgid "Time in seconds for DNS record caching (default: 60)"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:238
#: src/podkop/tabs/dashboard/initController.ts:280
msgid "Traffic"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:268
#: src/podkop/tabs/dashboard/initController.ts:310
msgid "Traffic Total"
msgstr ""
@@ -1324,7 +1338,7 @@ msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:34
#: src/podkop/tabs/diagnostic/initController.ts:36
#: src/podkop/tabs/diagnostic/initController.ts:37
#: src/podkop/tabs/diagnostic/initController.ts:457
#: src/podkop/tabs/diagnostic/initController.ts:466
msgid "unknown"
msgstr ""
@@ -1340,8 +1354,8 @@ msgstr ""
msgid "Update interval"
msgstr ""
#: src/podkop/tabs/dashboard/initController.ts:240
#: src/podkop/tabs/dashboard/initController.ts:271
#: src/podkop/tabs/dashboard/initController.ts:282
#: src/podkop/tabs/dashboard/initController.ts:313
msgid "Uplink"
msgstr ""
@@ -1427,11 +1441,6 @@ msgstr ""
msgid "Validation errors:"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:494
#: src/podkop/tabs/diagnostic/initController.ts:507
msgid "Check unavailable"
msgstr ""
#: src/podkop/tabs/diagnostic/initController.ts:282
#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107
msgid "View logs"
+67 -1
View File
@@ -10,6 +10,8 @@ CONFIG_NAME="podkop-plus"
PODKOP_BIN="/usr/bin/podkop-plus"
PODKOP_SERVICE_INIT="/etc/init.d/podkop-plus"
PODKOP_RELOAD_LOCK_DIR="/var/run/podkop-plus.reload.lock"
PODKOP_RUNTIME_STATE_DIR="/var/run/podkop-plus"
PODKOP_PENDING_RELOAD_FILE="$PODKOP_RUNTIME_STATE_DIR/reload.pending"
PODKOP_INTERNAL_CONFIG_TRIGGER_GUARD="/var/run/podkop-plus.internal-config-change"
config_load "$CONFIG_NAME"
@@ -18,13 +20,57 @@ reload_lock_is_active() {
}
acquire_reload_lock() {
mkdir "$PODKOP_RELOAD_LOCK_DIR" 2> /dev/null
local owner_pid
if mkdir "$PODKOP_RELOAD_LOCK_DIR" 2> /dev/null; then
printf '%s\n' "$$" > "$PODKOP_RELOAD_LOCK_DIR/pid"
return 0
fi
owner_pid="$(sed -n '1p' "$PODKOP_RELOAD_LOCK_DIR/pid" 2> /dev/null)"
if [ -n "$owner_pid" ] && kill -0 "$owner_pid" 2> /dev/null; then
return 1
fi
rm -f "$PODKOP_RELOAD_LOCK_DIR/pid" 2> /dev/null
rmdir "$PODKOP_RELOAD_LOCK_DIR" 2> /dev/null || return 1
mkdir "$PODKOP_RELOAD_LOCK_DIR" 2> /dev/null || return 1
printf '%s\n' "$$" > "$PODKOP_RELOAD_LOCK_DIR/pid"
}
release_reload_lock() {
rm -f "$PODKOP_RELOAD_LOCK_DIR/pid" 2> /dev/null
rmdir "$PODKOP_RELOAD_LOCK_DIR" 2> /dev/null
}
mark_pending_reload() {
local reason="${1:-pending}"
local now
mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*)
now=0
;;
esac
{
printf 'reason=%s\n' "$reason"
printf 'updated_at=%s\n' "$now"
} > "$PODKOP_PENDING_RELOAD_FILE"
}
consume_pending_reload() {
[ -f "$PODKOP_PENDING_RELOAD_FILE" ] || return 1
rm -f "$PODKOP_PENDING_RELOAD_FILE" 2>/dev/null
}
run_pending_reload_if_requested() {
consume_pending_reload || return 0
"$PODKOP_SERVICE_INIT" reload pending >/dev/null 2>&1 &
}
consume_internal_config_guard() {
rm -f "$PODKOP_INTERNAL_CONFIG_TRIGGER_GUARD" 2> /dev/null
}
@@ -84,6 +130,24 @@ stop_service() {
"$PODKOP_BIN" stop
}
restart() {
echo "Restart Podkop Plus"
if ! acquire_reload_lock; then
mark_pending_reload "restart_busy"
return 0
fi
trap 'release_reload_lock' EXIT INT TERM
"$PODKOP_BIN" restart > /dev/null 2>&1
local status="$?"
trap - EXIT INT TERM
release_reload_lock
run_pending_reload_if_requested
return "$status"
}
reload_service() {
local reason="$1"
@@ -92,6 +156,7 @@ reload_service() {
fi
if ! acquire_reload_lock; then
mark_pending_reload "${reason:-reload_busy}"
return 0
fi
@@ -100,6 +165,7 @@ reload_service() {
local status="$?"
trap - EXIT INT TERM
release_reload_lock
run_pending_reload_if_requested
return "$status"
}
+355 -71
View File
@@ -19,6 +19,7 @@ check_required_file "$PODKOP_LIB/helpers.sh"
check_required_file "$PODKOP_LIB/zapret.sh"
check_required_file "$PODKOP_LIB/sing_box_config_manager.sh"
check_required_file "$PODKOP_LIB/sing_box_config_facade.sh"
check_required_file "$PODKOP_LIB/subscription_parser.sh"
check_required_file "$PODKOP_LIB/logging.sh"
check_required_file "$PODKOP_LIB/rulesets.sh"
. /lib/functions.sh
@@ -30,6 +31,7 @@ check_required_file "$PODKOP_LIB/rulesets.sh"
. "$PODKOP_LIB/zapret.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
. "$PODKOP_LIB/sing_box_config_facade.sh"
. "$PODKOP_LIB/subscription_parser.sh"
. "$PODKOP_LIB/logging.sh"
. "$PODKOP_LIB/rulesets.sh"
@@ -38,8 +40,12 @@ PODKOP_RUNTIME_STATE_DIR="/var/run/podkop-plus"
PODKOP_RUNTIME_STATUS_FILE="$PODKOP_RUNTIME_STATE_DIR/status"
PODKOP_SYSTEM_INFO_CACHE_FILE="$PODKOP_RUNTIME_STATE_DIR/system-info.json"
PODKOP_RELOAD_STATE_FILE="$PODKOP_RUNTIME_STATE_DIR/reload-state"
PODKOP_PENDING_RELOAD_FILE="$PODKOP_RUNTIME_STATE_DIR/reload.pending"
PODKOP_LIST_UPDATE_STATE_FILE="$PODKOP_RUNTIME_STATE_DIR/list-update.timestamp"
PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update"
PODKOP_SUBSCRIPTION_LINKS_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-links"
PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update.lock"
PODKOP_RELOAD_LOCK_DIR="/var/run/podkop-plus.reload.lock"
PODKOP_LIST_UPDATE_CRON_MARKER="# podkop-plus-list-update"
PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER="# podkop-plus-subscription-update"
# sing-box falls back to 1d when update_interval is empty, so use a large
@@ -60,6 +66,7 @@ ensure_runtime_dirs() {
mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
mkdir -p "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR"
mkdir -p "$PODKOP_SUBSCRIPTION_LINKS_DIR"
}
get_subscription_json_path() {
@@ -80,6 +87,37 @@ get_subscription_update_timestamp_path() {
echo "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR/${section}.timestamp"
}
get_subscription_links_path() {
local section="$1"
echo "$PODKOP_SUBSCRIPTION_LINKS_DIR/${section}.json"
}
write_subscription_outbound_links() {
local section="$1"
local links_json="$2"
local links_path tmpfile
mkdir -p "$PODKOP_SUBSCRIPTION_LINKS_DIR"
links_path="$(get_subscription_links_path "$section")"
tmpfile="${links_path}.$$"
if [ -n "$links_json" ] && printf '%s' "$links_json" | jq -e 'type == "object"' >/dev/null 2>&1; then
printf '%s\n' "$links_json" > "$tmpfile" && mv "$tmpfile" "$links_path"
else
rm -f "$links_path" "$tmpfile"
fi
}
subscription_url_is_current() {
local section="$1"
local subscription_url="$2"
local current_subscription_url
current_subscription_url="$(uci -q get "$PODKOP_CONFIG_NAME.$section.subscription_url" 2>/dev/null)"
[ "$current_subscription_url" = "$subscription_url" ]
}
subscription_cache_is_usable() {
local subscription_json_path="$1"
@@ -92,7 +130,7 @@ wait_for_subscription_connectivity() {
local section="$1"
local subscription_url="$2"
local service_proxy_address="$3"
local attempts="${4:-12}"
local attempts="${4:-3}"
local wait="${5:-5}"
local timeout="${6:-5}"
local attempt
@@ -117,34 +155,51 @@ download_subscription_into_cache() {
local subscription_json_path="$3"
local subscription_url_cache_path="$4"
local service_proxy_address="$5"
local tmpfile
local raw_tmpfile normalized_tmpfile
mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.download.XXXXXX")" || return 1
raw_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.download.XXXXXX")" || return 1
normalized_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.normalized.XXXXXX")" || {
rm -f "$raw_tmpfile"
return 1
}
if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10; then
rm -f "$tmpfile"
if ! download_subscription "$subscription_url" "$raw_tmpfile" "$service_proxy_address" 3 2 10; then
rm -f "$raw_tmpfile" "$normalized_tmpfile"
return 1
fi
if ! validate_subscription_file "$tmpfile"; then
if ! normalize_subscription_file "$raw_tmpfile" "$normalized_tmpfile" "$section"; then
log "Downloaded subscription for rule '$section' is invalid" "error"
rm -f "$tmpfile"
rm -f "$raw_tmpfile" "$normalized_tmpfile"
return 1
fi
if [ -f "$subscription_json_path" ] && cmp -s "$tmpfile" "$subscription_json_path"; then
rm -f "$tmpfile"
if ! validate_subscription_file "$normalized_tmpfile"; then
log "Normalized subscription for rule '$section' is invalid" "error"
rm -f "$raw_tmpfile" "$normalized_tmpfile"
return 1
fi
if ! subscription_url_is_current "$section" "$subscription_url"; then
log "Subscription URL changed while updating rule '$section'; discarding superseded download" "warn"
rm -f "$raw_tmpfile" "$normalized_tmpfile"
return 4
fi
if [ -f "$subscription_json_path" ] && cmp -s "$normalized_tmpfile" "$subscription_json_path"; then
rm -f "$raw_tmpfile" "$normalized_tmpfile"
printf '%s' "$subscription_url" > "$subscription_url_cache_path"
log "Subscription for rule '$section' is unchanged" "info"
return 2
fi
mv "$tmpfile" "$subscription_json_path" || {
rm -f "$tmpfile"
mv "$normalized_tmpfile" "$subscription_json_path" || {
rm -f "$raw_tmpfile" "$normalized_tmpfile"
return 1
}
rm -f "$raw_tmpfile"
printf '%s' "$subscription_url" > "$subscription_url_cache_path"
return 0
}
@@ -165,7 +220,7 @@ rule_is_subscription_proxy() {
prepare_subscription_cache_for_startup() {
local section="$1"
local subscription_url subscription_json_path subscription_url_cache_path cached_subscription_url
local service_proxy_address had_usable_cache cache_needs_refresh
local had_usable_cache cache_needs_refresh
rule_is_subscription_proxy "$section" || return 0
@@ -197,17 +252,13 @@ prepare_subscription_cache_for_startup() {
[ "$cache_needs_refresh" -eq 0 ] && return 0
service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')"
download_subscription_into_cache \
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" ""
case "$?" in
0 | 2) return 0 ;;
esac
if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address"; then
download_subscription_into_cache \
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"
case "$?" in
0 | 2) return 0 ;;
esac
fi
if [ "$had_usable_cache" -eq 1 ]; then
if [ "$had_usable_cache" -eq 1 ] && [ "$cached_subscription_url" = "$subscription_url" ]; then
log "Keeping cached subscription for rule '$section' until a fresh download succeeds" "warn"
return 0
fi
@@ -228,6 +279,60 @@ close_inherited_service_lock_fd() {
exec 1000>&- 2>/dev/null || true
}
acquire_runtime_dir_lock() {
local lock_dir="$1"
local owner_pid
if mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$$" > "$lock_dir/pid"
return 0
fi
owner_pid="$(sed -n '1p' "$lock_dir/pid" 2>/dev/null)"
if [ -n "$owner_pid" ] && kill -0 "$owner_pid" 2>/dev/null; then
return 1
fi
rm -f "$lock_dir/pid" 2>/dev/null
rmdir "$lock_dir" 2>/dev/null || return 1
mkdir "$lock_dir" 2>/dev/null || return 1
printf '%s\n' "$$" > "$lock_dir/pid"
}
release_runtime_dir_lock() {
local lock_dir="$1"
rm -f "$lock_dir/pid" 2>/dev/null
rmdir "$lock_dir" 2>/dev/null
}
mark_pending_reload() {
local reason="${1:-pending}"
local now
ensure_runtime_dirs
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) now=0 ;;
esac
{
printf 'reason=%s\n' "$reason"
printf 'updated_at=%s\n' "$now"
} > "$PODKOP_PENDING_RELOAD_FILE"
}
consume_pending_reload() {
[ -f "$PODKOP_PENDING_RELOAD_FILE" ] || return 1
rm -f "$PODKOP_PENDING_RELOAD_FILE" 2>/dev/null
}
run_pending_reload_if_requested() {
consume_pending_reload || return 0
echolog "Applying pending Podkop reload"
"$PODKOP_SERVICE_INIT" reload pending >/dev/null 2>&1 &
}
sync_time_if_needed() {
local current_year
@@ -313,7 +418,7 @@ runtime_status_read_value() {
runtime_status_is_transition() {
case "$1" in
starting | stopping | restarting | reloading)
starting | stopping | restarting | reloading | updating_subscription)
return 0
;;
esac
@@ -338,8 +443,19 @@ runtime_status_transition_is_active() {
'' | *[!0-9]*) now=0 ;;
esac
[ "$now" -gt 0 ] || return 0
case "$pid" in
'' | *[!0-9]*)
[ "$now" -gt 0 ] || return 0
;;
*)
if [ "$state" = "updating_subscription" ]; then
kill -0 "$pid" 2>/dev/null
return "$?"
fi
;;
esac
[ "$now" -gt 0 ] || return 0
age=$((now - updated_at))
[ "$age" -ge 0 ] || return 1
[ "$age" -le "$PODKOP_RUNTIME_TRANSITION_MAX_AGE" ] || return 1
@@ -2505,7 +2621,77 @@ build_list_update_cron_job() {
}
build_subscription_update_cron_job() {
printf '* * * * * %s subscription_update_if_due %s\n' "$PODKOP_BIN" "$PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER"
local min_interval_seconds cron_schedule
SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS=0
config_foreach collect_subscription_update_interval_seconds "rule"
[ "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" -gt 0 ] || return 1
min_interval_seconds="$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS"
cron_schedule="$(seconds_to_subscription_cron_schedule "$min_interval_seconds")"
printf '%s %s subscription_update_if_due %s\n' "$cron_schedule" "$PODKOP_BIN" "$PODKOP_SUBSCRIPTION_UPDATE_CRON_MARKER"
}
collect_subscription_update_interval_seconds() {
local section="$1"
local subscription_update_interval interval_seconds
rule_is_subscription_proxy "$section" || return 0
subscription_update_interval="$(get_subscription_update_interval_for_rule "$section")"
[ -n "$subscription_update_interval" ] || return 0
interval_seconds="$(duration_to_seconds "$subscription_update_interval")" || {
log "Invalid subscription_update_interval value for rule '$section': $subscription_update_interval" "error"
return 0
}
if [ "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" -eq 0 ] ||
[ "$interval_seconds" -lt "$SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS" ]; then
SUBSCRIPTION_MIN_UPDATE_INTERVAL_SECONDS="$interval_seconds"
fi
}
seconds_to_subscription_cron_schedule() {
local seconds="$1"
local minutes hours
if [ "$seconds" -le 60 ]; then
printf '* * * * *\n'
return 0
fi
if [ $((seconds % 86400)) -eq 0 ]; then
printf '0 0 * * *\n'
return 0
fi
if [ $((seconds % 3600)) -eq 0 ]; then
hours=$((seconds / 3600))
if [ "$hours" -ge 1 ] && [ "$hours" -le 23 ]; then
if [ "$hours" -eq 1 ]; then
printf '0 * * * *\n'
else
printf '0 */%s * * *\n' "$hours"
fi
return 0
fi
fi
if [ $((seconds % 60)) -eq 0 ]; then
minutes=$((seconds / 60))
if [ "$minutes" -ge 1 ] && [ "$minutes" -le 59 ]; then
if [ "$minutes" -eq 1 ]; then
printf '* * * * *\n'
else
printf '*/%s * * * *\n' "$minutes"
fi
return 0
fi
fi
printf '* * * * *\n'
}
remove_cron_job() {
@@ -2786,7 +2972,7 @@ subscription_update_section_is_due() {
subscription_update_section() {
local section="$1"
local force="$2"
local subscription_url subscription_json_path subscription_url_cache_path service_proxy_address update_result
local subscription_url subscription_json_path subscription_url_cache_path update_result
local outbounds_count now
rule_is_subscription_proxy "$section" || return 3
@@ -2809,27 +2995,21 @@ subscription_update_section() {
mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
subscription_json_path="$(get_subscription_json_path "$section")"
subscription_url_cache_path="$(get_subscription_url_cache_path "$section")"
service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')"
echolog "Updating subscription for rule '$section'..."
if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then
echolog "Failed to download subscription for rule '$section'"
return 1
fi
download_subscription_into_cache \
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" ""
update_result=$?
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) ;;
*) write_subscription_update_timestamp "$section" "$now" ;;
esac
case "$update_result" in
0)
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) ;;
*) write_subscription_update_timestamp "$section" "$now" ;;
esac
outbounds_count=$(jq -r '[.outbounds[] | select(
.type != "selector" and
.type != "urltest" and
@@ -2841,9 +3021,19 @@ subscription_update_section() {
return 0
;;
2)
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) ;;
*) write_subscription_update_timestamp "$section" "$now" ;;
esac
echolog "Subscription for rule '$section' is unchanged"
return 2
;;
4)
echolog "Subscription update for rule '$section' was superseded by a newer URL"
return 4
;;
*)
echolog "Failed to download subscription for rule '$section'"
return 1
@@ -2862,24 +3052,28 @@ subscription_update_handler() {
0) SUBSCRIPTION_UPDATED_SECTIONS=$((SUBSCRIPTION_UPDATED_SECTIONS + 1)) ;;
1) SUBSCRIPTION_FAILED_SECTIONS=$((SUBSCRIPTION_FAILED_SECTIONS + 1)) ;;
2) SUBSCRIPTION_UNCHANGED_SECTIONS=$((SUBSCRIPTION_UNCHANGED_SECTIONS + 1)) ;;
4) SUBSCRIPTION_SUPERSEDED_SECTIONS=$((SUBSCRIPTION_SUPERSEDED_SECTIONS + 1)) ;;
3) ;;
esac
}
subscription_update_common() {
subscription_update_common_locked() {
local force="$1"
config_load "$PODKOP_CONFIG_NAME"
ensure_runtime_dirs
SUBSCRIPTION_UPDATE_FORCE="$force"
SUBSCRIPTION_UPDATED_SECTIONS=0
SUBSCRIPTION_FAILED_SECTIONS=0
SUBSCRIPTION_UNCHANGED_SECTIONS=0
SUBSCRIPTION_SUPERSEDED_SECTIONS=0
config_foreach subscription_update_handler "rule"
if [ "$SUBSCRIPTION_UPDATED_SECTIONS" -eq 0 ]; then
if [ "$SUBSCRIPTION_SUPERSEDED_SECTIONS" -gt 0 ]; then
echolog "Subscription update was superseded by newer configuration"
return 0
fi
if [ "$SUBSCRIPTION_FAILED_SECTIONS" -gt 0 ]; then
echolog "Subscription update finished with errors; keeping the last working cache"
return 1
@@ -2894,6 +3088,7 @@ subscription_update_common() {
fi
echolog "Reloading sing-box to apply updated subscriptions..."
config_load "$PODKOP_CONFIG_NAME"
sing_box_configure_service
PODKOP_NFT_POPULATE_ENABLED=0
sing_box_init_config
@@ -2909,6 +3104,41 @@ subscription_update_common() {
fi
}
subscription_update_common() {
local force="$1"
local result
config_load "$PODKOP_CONFIG_NAME"
ensure_runtime_dirs
if ! acquire_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"; then
echolog "Subscription update is already running"
[ "$force" -eq 1 ] && mark_pending_reload "subscription_update_busy"
return 0
fi
if ! acquire_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"; then
release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
echolog "Podkop reload is already running; skipping subscription update"
[ "$force" -eq 1 ] && mark_pending_reload "reload_busy"
return 0
fi
runtime_status_write "updating_subscription" "subscription_update"
trap 'runtime_status_mark_failure "subscription_update"' EXIT INT TERM
subscription_update_common_locked "$force"
result=$?
trap - EXIT INT TERM
runtime_status_finish_action "subscription_update" "$result"
release_runtime_dir_lock "$PODKOP_RELOAD_LOCK_DIR"
release_runtime_dir_lock "$PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR"
run_pending_reload_if_requested
return "$result"
}
subscription_update_if_due() {
echolog "Starting due subscription update..."
subscription_update_common 0
@@ -2956,11 +3186,11 @@ sing_box_init_config() {
configured_rulesets=""
sing_box_configure_log
sing_box_configure_inbounds
sing_box_configure_outbounds
sing_box_configure_dns
sing_box_configure_route
sing_box_configure_experimental
sing_box_additional_inbounds
sing_box_configure_outbounds
sing_box_save_config
}
@@ -3038,6 +3268,57 @@ sing_box_build_subscription_country_groups() {
' 2>/dev/null
}
json_array_append_string() {
local array_json="$1"
local value="$2"
local array_tmp result jq_status
array_tmp="$(mktemp)" || return 1
printf '%s' "$array_json" > "$array_tmp" || {
rm -f "$array_tmp"
return 1
}
result="$(jq -cn --slurpfile array "$array_tmp" --arg value "$value" \
'($array[0] // []) + [$value]' 2>/dev/null)"
jq_status=$?
rm -f "$array_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$result" ] || return 1
printf '%s\n' "$result"
}
json_arrays_concat() {
local first_json="$1"
local second_json="$2"
local first_tmp second_tmp result jq_status
first_tmp="$(mktemp)" || return 1
second_tmp="$(mktemp)" || {
rm -f "$first_tmp"
return 1
}
printf '%s' "$first_json" > "$first_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
printf '%s' "$second_json" > "$second_tmp" || {
rm -f "$first_tmp" "$second_tmp"
return 1
}
result="$(jq -cn --slurpfile first "$first_tmp" --slurpfile second "$second_tmp" \
'($first[0] // []) + ($second[0] // [])' 2>/dev/null)"
jq_status=$?
rm -f "$first_tmp" "$second_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$result" ] || return 1
printf '%s\n' "$result"
}
is_truthy_option() {
local value="$1"
@@ -3190,7 +3471,7 @@ configure_outbound_handler() {
;;
subscription)
local subscription_url subscription_json_path subscription_url_cache_path cached_subscription_url \
should_download had_usable_cache service_proxy_address urltest_tag selector_tag urltest_outbounds \
should_download had_usable_cache urltest_tag selector_tag urltest_outbounds \
selector_outbounds urltest_check_interval urltest_idle_timeout urltest_tolerance urltest_testing_url \
subscription_group_by_countries_raw subscription_group_by_countries subscription_outbound_tags_json \
update_result
@@ -3239,29 +3520,19 @@ configure_outbound_handler() {
log "Subscription URL changed for rule '$section'" "warn"
fi
if [ "$had_usable_cache" -eq 0 ]; then
should_download=1
else
log "Using cached subscription for rule '$section' until a fresh download succeeds" "warn"
fi
should_download=1
fi
if [ "$should_download" -eq 1 ]; then
log "Downloading subscription for rule '$section'"
service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')"
if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then
download_subscription_into_cache \
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"
update_result="$?"
else
update_result=1
fi
download_subscription_into_cache \
"$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" ""
update_result="$?"
case "$update_result" in
0 | 2) ;;
*)
if [ "$had_usable_cache" -eq 1 ]; then
if [ "$had_usable_cache" -eq 1 ] && [ "$cached_subscription_url" = "$subscription_url" ]; then
log "Failed to refresh subscription for rule '$section', continuing with cached data" "warn"
else
log "Failed to download subscription for rule '$section'. Aborted." "fatal"
@@ -3276,6 +3547,7 @@ configure_outbound_handler() {
exit 1
fi
config="$SING_BOX_CF_LAST_CONFIG"
write_subscription_outbound_links "$section" "$SUBSCRIPTION_OUTBOUND_LINKS_JSON"
if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then
log "No proxy outbounds found in subscription for rule '$section'. Aborted." "fatal"
@@ -3310,19 +3582,14 @@ configure_outbound_handler() {
config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance" "$urltest_idle_timeout")"
selector_outbounds_json=$(
printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null
)
selector_outbounds_json="$(json_array_append_string "$selector_outbounds_json" "$country_group_tag")"
done
[ -n "$selector_outbounds_json" ] || selector_outbounds_json="[]"
ungrouped_outbounds_json="$(echo "$grouping_json" | jq -c '.ungrouped // []' 2>/dev/null)"
if [ -n "$ungrouped_outbounds_json" ] && [ "$ungrouped_outbounds_json" != "[]" ]; then
selector_outbounds_json=$(
jq -acn --argjson selector "$selector_outbounds_json" --argjson ungrouped "$ungrouped_outbounds_json" \
'$selector + $ungrouped' 2>/dev/null
)
selector_outbounds_json="$(json_arrays_concat "$selector_outbounds_json" "$ungrouped_outbounds_json")"
fi
if [ -z "$selector_outbounds_json" ] || [ "$selector_outbounds_json" = "[]" ]; then
@@ -3340,10 +3607,7 @@ configure_outbound_handler() {
else
urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
urltest_outbounds="$subscription_outbound_tags_json"
selector_outbounds=$(
jq -acn --argjson outbounds "$subscription_outbound_tags_json" --arg tag "$urltest_tag" \
'$outbounds + [$tag]' 2>/dev/null
)
selector_outbounds="$(json_array_append_string "$subscription_outbound_tags_json" "$urltest_tag")"
if [ -z "$selector_outbounds" ]; then
log "Failed to build selector outbounds for subscription rule '$section'. Aborted." "fatal"
exit 1
@@ -5522,6 +5786,22 @@ get_status() {
}'
}
get_outbound_link() {
local section="$1"
local outbound_tag="$2"
local links_path link
ensure_runtime_dirs
links_path="$(get_subscription_links_path "$section")"
link=""
if [ -f "$links_path" ]; then
link="$(jq -r --arg tag "$outbound_tag" '.[$tag] // empty' "$links_path" 2>/dev/null)"
fi
jq -cn --arg link "$link" '{link: $link}'
}
validate_nfqws_strategy_json() {
local raw_opt="${1:-}"
@@ -6312,6 +6592,7 @@ Available commands:
show_sing_box_version Show sing-box version
show_system_info Show system information
get_status Get Podkop Plus service status
get_outbound_link Get source proxy link for a generated outbound
get_sing_box_status Get sing-box service status
get_zapret_status Get zapret provider and Podkop-managed nfqws status
get_system_info Get system information in JSON format
@@ -6407,6 +6688,9 @@ show_system_info)
get_status)
get_status
;;
get_outbound_link)
get_outbound_link "$2" "$3"
;;
get_sing_box_status)
get_sing_box_status
;;
+263 -92
View File
@@ -101,6 +101,10 @@ sing_box_cf_add_proxy_outbound() {
uuid=$(url_get_userinfo "$url")
flow=$(url_get_query_param "$url" "flow")
packet_encoding=$(url_get_query_param "$url" "packetEncoding")
case "$packet_encoding" in
xudp | packetaddr) ;;
*) packet_encoding="" ;;
esac
config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow" "" "$packet_encoding")
config=$(_add_outbound_security "$config" "$tag" "$url")
@@ -329,13 +333,252 @@ sing_box_cf_add_single_key_reject_rule() {
echo "$config"
}
sing_box_cf_subscription_candidate_outbounds() {
local subscription_json_path="$1"
jq -c '[.outbounds[]? | select(
type == "object" and
.type != "selector" and
.type != "urltest" and
.type != "direct" and
.type != "dns" and
.type != "block"
)]' "$subscription_json_path" 2>/dev/null
}
sing_box_cf_prepare_subscription_batch() {
local config="$1"
local outbounds_json="$2"
local existing_tags_json existing_tags_tmp outbounds_tmp prepared_json jq_status
existing_tags_json="$(printf '%s' "$config" | jq -c '[.outbounds[]?.tag // empty]' 2>/dev/null)"
[ -n "$existing_tags_json" ] || existing_tags_json="[]"
existing_tags_tmp="$(mktemp)" || return 1
outbounds_tmp="$(mktemp)" || {
rm -f "$existing_tags_tmp"
return 1
}
printf '%s' "$existing_tags_json" > "$existing_tags_tmp" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp"
return 1
}
printf '%s' "$outbounds_json" > "$outbounds_tmp" || {
rm -f "$existing_tags_tmp" "$outbounds_tmp"
return 1
}
prepared_json="$(jq -c --slurpfile existing_tags "$existing_tags_tmp" '
($existing_tags[0] // []) as $existing_tags
|
def safe_string($value; $fallback):
(($value // $fallback) | tostring) as $result
| if $result == "" or $result == "null" then $fallback else $result end;
def unique_tag($base; $taken):
if (($taken[$base] // false) | not) then
$base
else
first(range(1; 100000) as $n
| "\($base)-\($n)"
| select((($taken[.] // false) | not)))
end;
reduce .[] as $outbound (
{
outbounds: [],
tags: [],
names: [],
links: [],
skipped: 0,
taken: (reduce $existing_tags[] as $tag ({}; .[$tag] = true))
};
((.outbounds | length) + 1) as $index
| safe_string($outbound.remark // $outbound.tag; "server-\($index)") as $display_name
| if ($outbound.type == "shadowsocks" and (($outbound.tls.enabled // false) == true)) then
.skipped += 1
else
safe_string($outbound.tag // $outbound.remark; "server-\($index)") as $base_tag
| (.taken as $taken | unique_tag($base_tag; $taken)) as $tag
| .outbounds += [($outbound | del(.tag, .remark, .share_link) + {tag: $tag})]
| .tags += [$tag]
| .names += [$display_name]
| .links += [($outbound.share_link // "")]
| .taken[$tag] = true
end
)
| del(.taken)
' "$outbounds_tmp" 2>/dev/null)"
jq_status=$?
rm -f "$existing_tags_tmp" "$outbounds_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$prepared_json" ] || return 1
printf '%s\n' "$prepared_json"
}
sing_box_cf_apply_subscription_batch() {
local config="$1"
local prepared_json="$2"
local new_outbounds new_outbounds_tmp config_tmp validation_tmp validation_config updated_config jq_status
new_outbounds="$(printf '%s' "$prepared_json" | jq -c '.outbounds // []' 2>/dev/null)"
[ -n "$new_outbounds" ] || return 1
[ "$(printf '%s' "$new_outbounds" | jq -r 'length' 2>/dev/null)" -gt 0 ] || return 1
new_outbounds_tmp="$(mktemp)" || return 1
config_tmp="$(mktemp)" || {
rm -f "$new_outbounds_tmp"
return 1
}
printf '%s' "$new_outbounds" > "$new_outbounds_tmp" || {
rm -f "$new_outbounds_tmp" "$config_tmp"
return 1
}
printf '%s' "$config" > "$config_tmp" || {
rm -f "$new_outbounds_tmp" "$config_tmp"
return 1
}
updated_config="$(jq -c --slurpfile new_outbounds "$new_outbounds_tmp" \
'.outbounds += ($new_outbounds[0] // [])' "$config_tmp" 2>/dev/null)"
jq_status=$?
rm -f "$new_outbounds_tmp" "$config_tmp"
[ "$jq_status" -eq 0 ] || return 1
[ -n "$updated_config" ] || return 1
validation_config="$(printf '%s' "$updated_config" | jq -c '
(first(.outbounds[]? | select(.type == "direct") | .tag) // "direct-out") as $direct
| .route = {rules: [], rule_set: [], final: $direct}
' 2>/dev/null)" || return 1
[ -n "$validation_config" ] || return 1
validation_tmp="$(mktemp)" || return 1
sing_box_cm_save_config_to_file "$validation_config" "$validation_tmp"
if ! sing-box -c "$validation_tmp" check > /dev/null 2>&1; then
rm -f "$validation_tmp"
return 1
fi
rm -f "$validation_tmp"
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$(printf '%s' "$prepared_json" | jq -c '.tags // []' 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ] || SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]"
SUBSCRIPTION_OUTBOUND_TAGS="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'join(",")' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_NAMES="$(printf '%s' "$prepared_json" | jq -r '.names[]?' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_LINKS_JSON="$(printf '%s' "$prepared_json" | jq -c '
(.tags // []) as $tags
| (.links // []) as $links
| reduce range(0; ($tags | length)) as $index (
{};
.[$tags[$index]] = ($links[$index] // "")
)
' 2>/dev/null)"
[ -n "$SUBSCRIPTION_OUTBOUND_LINKS_JSON" ] || SUBSCRIPTION_OUTBOUND_LINKS_JSON="{}"
SING_BOX_CF_LAST_CONFIG="$updated_config"
return 0
}
sing_box_cf_apply_subscription_outbounds_individually() {
local config="$1"
local prepared_json="$2"
local working_config="$config"
local outbounds_count index outbound_tag display_name raw_outbound try_config validation_config validation_tmp
local added_count skipped_count tags_json links_json names source_link
outbounds_count="$(printf '%s' "$prepared_json" | jq -r '(.outbounds // []) | length' 2>/dev/null)"
[ -n "$outbounds_count" ] || return 1
[ "$outbounds_count" -gt 0 ] || return 1
index=0
added_count=0
skipped_count=0
tags_json="[]"
links_json="{}"
names=""
while [ "$index" -lt "$outbounds_count" ]; do
outbound_tag="$(printf '%s' "$prepared_json" | jq -r --argjson index "$index" '.outbounds[$index].tag // empty' 2>/dev/null)"
display_name="$(printf '%s' "$prepared_json" | jq -r --argjson index "$index" '.names[$index] // .outbounds[$index].tag // ("server-" + (($index + 1) | tostring))' 2>/dev/null)"
raw_outbound="$(printf '%s' "$prepared_json" | jq -c --argjson index "$index" '.outbounds[$index] | del(.tag)' 2>/dev/null)"
source_link="$(printf '%s' "$prepared_json" | jq -r --argjson index "$index" '.links[$index] // empty' 2>/dev/null)"
if [ -z "$outbound_tag" ] || [ -z "$raw_outbound" ] || [ "$raw_outbound" = "null" ]; then
skipped_count=$((skipped_count + 1))
index=$((index + 1))
continue
fi
try_config="$(sing_box_cm_add_raw_outbound "$working_config" "$outbound_tag" "$raw_outbound")"
if [ -z "$try_config" ]; then
log "Skipped invalid subscription outbound '$display_name'" "warn"
skipped_count=$((skipped_count + 1))
index=$((index + 1))
continue
fi
validation_config="$(printf '%s' "$try_config" | jq -c '
(first(.outbounds[]? | select(.type == "direct") | .tag) // "direct-out") as $direct
| .route = {rules: [], rule_set: [], final: $direct}
' 2>/dev/null)"
if [ -z "$validation_config" ]; then
log "Skipped invalid subscription outbound '$display_name'" "warn"
skipped_count=$((skipped_count + 1))
index=$((index + 1))
continue
fi
validation_tmp="$(mktemp)" || return 1
sing_box_cm_save_config_to_file "$validation_config" "$validation_tmp"
if sing-box -c "$validation_tmp" check > /dev/null 2>&1; then
working_config="$try_config"
tags_json="$(printf '%s' "$tags_json" | jq -c --arg tag "$outbound_tag" '. + [$tag]' 2>/dev/null)"
links_json="$(printf '%s' "$links_json" | jq -c --arg tag "$outbound_tag" --arg link "$source_link" '.[$tag] = $link' 2>/dev/null)"
if [ -z "$names" ]; then
names="$display_name"
else
names="$names
$display_name"
fi
added_count=$((added_count + 1))
else
log "Skipped unsupported subscription outbound '$display_name'" "warn"
skipped_count=$((skipped_count + 1))
fi
rm -f "$validation_tmp"
index=$((index + 1))
done
if [ "$added_count" -eq 0 ]; then
return 1
fi
if [ "$skipped_count" -gt 0 ]; then
log "Skipped $skipped_count unsupported subscription outbounds during fallback validation" "warn"
fi
SUBSCRIPTION_OUTBOUND_TAGS_JSON="$tags_json"
SUBSCRIPTION_OUTBOUND_TAGS="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'join(",")' 2>/dev/null)"
SUBSCRIPTION_OUTBOUND_LINKS_JSON="$links_json"
SUBSCRIPTION_OUTBOUND_NAMES="$names"
SING_BOX_CF_LAST_CONFIG="$working_config"
return 0
}
sing_box_cf_add_subscription_outbounds() {
local config="$1"
local section="$2"
local subscription_json_path="$3"
local outbounds_json outbounds_count prepared_json skipped_count
SUBSCRIPTION_OUTBOUND_TAGS=""
SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]"
SUBSCRIPTION_OUTBOUND_LINKS_JSON="{}"
SUBSCRIPTION_OUTBOUND_NAMES=""
SING_BOX_CF_LAST_CONFIG="$config"
@@ -345,14 +588,8 @@ sing_box_cf_add_subscription_outbounds() {
return 1
fi
local outbounds_count
outbounds_count=$(jq -r '[.outbounds[] | select(
.type != "selector" and
.type != "urltest" and
.type != "direct" and
.type != "dns" and
.type != "block"
)] | length' "$subscription_json_path" 2>/dev/null)
outbounds_json="$(sing_box_cf_subscription_candidate_outbounds "$subscription_json_path")"
outbounds_count="$(printf '%s' "$outbounds_json" | jq -r 'length' 2>/dev/null)"
if [ -z "$outbounds_count" ] || [ "$outbounds_count" -eq 0 ]; then
log "No proxy outbounds found in subscription JSON" "error"
@@ -362,95 +599,29 @@ sing_box_cf_add_subscription_outbounds() {
log "Found $outbounds_count proxy outbounds in subscription" "info"
local i added_count
local outbound_json display_name outbound_tag outbound_type outbound_tls_enabled preferred_tag base_tag tag_suffix
i=1
added_count=0
while [ "$i" -le "$outbounds_count" ]; do
outbound_json=$(jq -c "[.outbounds[] | select(
.type != \"selector\" and
.type != \"urltest\" and
.type != \"direct\" and
.type != \"dns\" and
.type != \"block\"
)][$i - 1]" "$subscription_json_path" 2>/dev/null)
if [ -z "$outbound_json" ] || [ "$outbound_json" = "null" ]; then
i=$((i + 1))
continue
prepared_json="$(sing_box_cf_prepare_subscription_batch "$config" "$outbounds_json")"
if [ -n "$prepared_json" ]; then
skipped_count="$(printf '%s' "$prepared_json" | jq -r '.skipped // 0' 2>/dev/null)"
if [ "${skipped_count:-0}" -gt 0 ]; then
log "Skipped $skipped_count unsupported subscription outbounds before validation" "warn"
fi
display_name=$(echo "$outbound_json" | jq -r '.remark // .tag // "server-'"$i"'"' 2>/dev/null)
outbound_type=$(echo "$outbound_json" | jq -r '.type // ""' 2>/dev/null)
outbound_tls_enabled=$(echo "$outbound_json" | jq -r '.tls.enabled // false' 2>/dev/null)
if [ "$outbound_type" = "shadowsocks" ] && [ "$outbound_tls_enabled" = "true" ]; then
log "Skip unsupported Shadowsocks outbound with tls: '$display_name'" "warn"
i=$((i + 1))
continue
if sing_box_cf_apply_subscription_batch "$config" "$prepared_json"; then
log "Added $(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null) subscription outbounds for rule '$section'" "info"
echo "$SING_BOX_CF_LAST_CONFIG"
return 0
fi
fi
preferred_tag=$(echo "$outbound_json" | jq -r '.tag // .remark // "server-'"$i"'"' 2>/dev/null)
if [ -z "$preferred_tag" ] || [ "$preferred_tag" = "null" ]; then
preferred_tag="server-$i"
fi
log "Batch subscription validation failed for rule '$section', trying per-outbound validation" "warn"
if [ -n "$prepared_json" ] && sing_box_cf_apply_subscription_outbounds_individually "$config" "$prepared_json"; then
log "Added $(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null) subscription outbounds for rule '$section'" "info"
echo "$SING_BOX_CF_LAST_CONFIG"
return 0
fi
base_tag="$preferred_tag"
outbound_tag="$base_tag"
tag_suffix=1
while printf '%s' "$config" | jq -e --arg tag "$outbound_tag" '.outbounds[]? | select(.tag == $tag)' > /dev/null 2>&1; do
outbound_tag="${base_tag}-${tag_suffix}"
tag_suffix=$((tag_suffix + 1))
done
local clean_outbound updated_config validation_tmp
clean_outbound=$(echo "$outbound_json" | jq -c 'del(.tag) | del(.remark)' 2>/dev/null)
updated_config=$(sing_box_cm_add_raw_outbound "$config" "$outbound_tag" "$clean_outbound" 2>/dev/null)
if [ -z "$updated_config" ]; then
log "Skip invalid outbound from subscription: '$display_name'" "warn"
i=$((i + 1))
continue
fi
validation_tmp="$(mktemp)"
sing_box_cm_save_config_to_file "$updated_config" "$validation_tmp"
if ! sing-box -c "$validation_tmp" check > /dev/null 2>&1; then
rm -f "$validation_tmp"
log "Skip unsupported outbound for current sing-box: '$display_name'" "warn"
i=$((i + 1))
continue
fi
rm -f "$validation_tmp"
config="$updated_config"
if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then
SUBSCRIPTION_OUTBOUND_TAGS="$outbound_tag"
else
SUBSCRIPTION_OUTBOUND_TAGS="$SUBSCRIPTION_OUTBOUND_TAGS,$outbound_tag"
fi
SUBSCRIPTION_OUTBOUND_TAGS_JSON=$(
printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -ac --arg tag "$outbound_tag" '. + [$tag]' 2>/dev/null
)
if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ]; then
SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]"
fi
if [ -z "$SUBSCRIPTION_OUTBOUND_NAMES" ]; then
SUBSCRIPTION_OUTBOUND_NAMES="$display_name"
else
SUBSCRIPTION_OUTBOUND_NAMES="$(printf '%s\n%s' "$SUBSCRIPTION_OUTBOUND_NAMES" "$display_name")"
fi
added_count=$((added_count + 1))
i=$((i + 1))
done
log "Added $added_count subscription outbounds for rule '$section'" "info"
SING_BOX_CF_LAST_CONFIG="$config"
log "No valid subscription outbounds remained after validation for rule '$section'" "error"
echo "$config"
return 1
}
@@ -969,10 +969,17 @@ sing_box_cm_add_urltest_outbound() {
local tolerance="$6"
local idle_timeout="$7"
local interrupt_exist_connections="$8"
local outbounds_tmp jq_status
echo "$config" | jq \
outbounds_tmp="$(mktemp)" || return 1
printf '%s' "$outbounds" > "$outbounds_tmp" || {
rm -f "$outbounds_tmp"
return 1
}
printf '%s' "$config" | jq \
--arg tag "$tag" \
--argjson outbounds "$outbounds" \
--slurpfile outbounds "$outbounds_tmp" \
--arg url "$url" \
--arg interval "$interval" \
--arg tolerance "$tolerance" \
@@ -982,7 +989,7 @@ sing_box_cm_add_urltest_outbound() {
{
type: "urltest",
tag: $tag,
outbounds: $outbounds
outbounds: ($outbounds[0] // [])
}
+ (if $url != "" then {url: $url} else {} end)
+ (if $interval != "" then {interval: $interval} else {} end)
@@ -990,6 +997,9 @@ sing_box_cm_add_urltest_outbound() {
+ (if $idle_timeout != "" then {idle_timeout: $idle_timeout} else {} end)
+ (if $interrupt_exist_connections == "true" then {interrupt_exist_connections: true} else {} end)
]'
jq_status=$?
rm -f "$outbounds_tmp"
return "$jq_status"
}
#######################################
@@ -1011,21 +1021,31 @@ sing_box_cm_add_selector_outbound() {
local outbounds="$3"
local default="$4"
local interrupt_exist_connections="$5"
local outbounds_tmp jq_status
echo "$config" | jq \
outbounds_tmp="$(mktemp)" || return 1
printf '%s' "$outbounds" > "$outbounds_tmp" || {
rm -f "$outbounds_tmp"
return 1
}
printf '%s' "$config" | jq \
--arg tag "$tag" \
--argjson outbounds "$outbounds" \
--slurpfile outbounds "$outbounds_tmp" \
--arg default "$default" \
--arg interrupt_exist_connections "$interrupt_exist_connections" \
'.outbounds += [
{
type: "selector",
tag: $tag,
outbounds: $outbounds,
outbounds: ($outbounds[0] // []),
default: $default
}
+ (if $interrupt_exist_connections == "true" then {interrupt_exist_connections: true} else {} end)
]'
jq_status=$?
rm -f "$outbounds_tmp"
return "$jq_status"
}
#######################################
File diff suppressed because it is too large Load Diff