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:
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "Посмотреть логи"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
;;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user