From d02ee70f30fa4e5bd0b1833543838a750204ccea Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Fri, 13 Mar 2026 19:29:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../methods/custom/getDashboardSections.ts | 78 ++++++++---- fe-app-podkop/src/podkop/types.ts | 1 + .../luci-static/resources/view/podkop/main.js | 66 ++++++++--- .../resources/view/podkop/section.js | 10 ++ podkop/files/etc/config/podkop | 3 +- podkop/files/usr/bin/podkop | 112 ++++++++++++++++-- 6 files changed, 223 insertions(+), 47 deletions(-) diff --git a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts index 554caa6..771ae8a 100644 --- a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts @@ -159,34 +159,72 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, ); - const outbound = proxies.find( + const fallbackUrltest = proxies.find( (proxy) => proxy.code === `${section['.name']}-urltest-out`, ); + const selectorOutbounds = (selector?.value?.all ?? []).flatMap((code) => { + const item = proxies.find((proxy) => proxy.code === code); + if (!item) { + return []; + } - const outbounds = (outbound?.value?.all ?? []) - .map((code) => proxies.find((item) => item.code === code)) - .map((item) => ({ - code: item?.code || '', - displayName: item?.value?.name || '', - latency: item?.value?.history?.[0]?.delay || 0, - type: item?.value?.type || '', - selected: selector?.value?.now === item?.code, - })); + const isLegacyFastest = item.code === `${section['.name']}-urltest-out`; + + return [ + { + code: item.code, + displayName: isLegacyFastest + ? _('Fastest') + : item?.value?.name || '', + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || '', + selected: selector?.value?.now === item.code, + }, + ]; + }); + + const outbounds = [ + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() === 'urltest', + ), + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() !== 'urltest', + ), + ]; + + if (outbounds.length === 0 && fallbackUrltest) { + const fallbackOutbounds = (fallbackUrltest?.value?.all ?? []) + .map((code) => proxies.find((item) => item.code === code)) + .map((item) => ({ + code: item?.code || '', + displayName: item?.value?.name || '', + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || '', + selected: selector?.value?.now === item?.code, + })); + + return { + withTagSelect: true, + code: selector?.code || section['.name'], + displayName: section['.name'], + outbounds: [ + { + code: fallbackUrltest?.code || '', + displayName: _('Fastest'), + latency: fallbackUrltest?.value?.history?.[0]?.delay || 0, + type: fallbackUrltest?.value?.type || '', + selected: selector?.value?.now === fallbackUrltest?.code, + }, + ...fallbackOutbounds, + ], + }; + } return { withTagSelect: true, code: selector?.code || section['.name'], displayName: section['.name'], - outbounds: [ - { - code: outbound?.code || '', - displayName: _('Fastest'), - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || '', - selected: selector?.value?.now === outbound?.code, - }, - ...outbounds, - ], + outbounds, }; } } diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts index e93ab03..98673bd 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-podkop/src/podkop/types.ts @@ -119,6 +119,7 @@ export namespace Podkop { proxy_config_type: 'subscription'; subscription_url: string; subscription_update_interval?: string; + subscription_group_by_countries?: '0' | '1'; } export interface ConfigVpnSection { diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index dfeb897..21ca915 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -825,30 +825,60 @@ async function getDashboardSections() { const selector = proxies.find( (proxy) => proxy.code === `${section[".name"]}-out` ); - const outbound = proxies.find( + const fallbackUrltest = proxies.find( (proxy) => proxy.code === `${section[".name"]}-urltest-out` ); - const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item) => ({ - code: item?.code || "", - displayName: item?.value?.name || "", - latency: item?.value?.history?.[0]?.delay || 0, - type: item?.value?.type || "", - selected: selector?.value?.now === item?.code - })); + const selectorOutbounds = (selector?.value?.all ?? []).flatMap((code) => { + const item = proxies.find((proxy) => proxy.code === code); + if (!item) { + return []; + } + const isLegacyFastest = item.code === `${section[".name"]}-urltest-out`; + return [{ + code: item.code, + displayName: isLegacyFastest ? _("Fastest") : item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item.code + }]; + }); + const outbounds = [ + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() === "urltest" + ), + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() !== "urltest" + ) + ]; + if (outbounds.length === 0 && fallbackUrltest) { + const fallbackOutbounds = (fallbackUrltest?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item) => ({ + code: item?.code || "", + displayName: item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item?.code + })); + return { + withTagSelect: true, + code: selector?.code || section[".name"] + "-out", + displayName: section[".name"], + outbounds: [ + { + code: fallbackUrltest?.code || "", + displayName: _("Fastest"), + latency: fallbackUrltest?.value?.history?.[0]?.delay || 0, + type: fallbackUrltest?.value?.type || "", + selected: selector?.value?.now === fallbackUrltest?.code + }, + ...fallbackOutbounds + ] + }; + } return { withTagSelect: true, code: selector?.code || section[".name"] + "-out", displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || "", - displayName: _("Fastest"), - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: selector?.value?.now === outbound?.code - }, - ...outbounds - ] + outbounds }; } } diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index d5ddc19..b09bed6 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -121,6 +121,16 @@ function createSectionContent(section) { o.default = "1h"; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( + form.Flag, + "subscription_group_by_countries", + _("Группировать по странам"), + _("Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы"), + ); + o.default = "0"; + o.rmempty = false; + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( form.DynamicList, "selector_proxy_links", diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index ff8d976..53e2ed9 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -44,7 +44,8 @@ config section 'main' # option proxy_config_type 'subscription' # option subscription_url 'https://example.com/api/sub' # option subscription_update_interval '1h' +# #option subscription_group_by_countries '0' # #option urltest_check_interval '3m' # #option urltest_tolerance '50' # #option urltest_testing_url 'https://www.gstatic.com/generate_204' -# list community_lists 'russia_inside' \ No newline at end of file +# list community_lists 'russia_inside' diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 3d1a214..b88fa6b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -818,6 +818,52 @@ sing_box_configure_outbounds() { config_foreach configure_outbound_handler "section" } +sing_box_get_unique_outbound_tag() { + local config="$1" + local base_tag="$2" + local candidate="$base_tag" + local tag_suffix=1 + + while printf '%s' "$config" | jq -e --arg tag "$candidate" '.outbounds[]? | select(.tag == $tag)' > /dev/null 2>&1; do + candidate="${base_tag}-${tag_suffix}" + tag_suffix=$((tag_suffix + 1)) + done + + echo "$candidate" +} + +sing_box_build_subscription_country_groups() { + local subscription_outbound_tags="$1" + + printf '%s' "$subscription_outbound_tags" | jq -Rrc ' + def is_regional_indicator: . >= 127462 and . <= 127487; + def extract_country_flag: + (. | explode) as $codepoints + | if ($codepoints | length) >= 2 + and ($codepoints[0] | is_regional_indicator) + and ($codepoints[1] | is_regional_indicator) + then ($codepoints[0:2] | implode) + else "" + end; + + (split(",") | map(select(length > 0))) as $tags + | reduce $tags[] as $tag ( + {country_order: [], country_groups: {}, ungrouped: []}; + ($tag | extract_country_flag) as $country_flag + | if $country_flag == "" then + .ungrouped += [$tag] + else + .country_groups[$country_flag] = ((.country_groups[$country_flag] // []) + [$tag]) + | if (.country_order | index($country_flag)) == null then + .country_order += [$country_flag] + else + . + end + end + ) + ' 2>/dev/null +} + configure_outbound_handler() { local section="$1" @@ -915,12 +961,14 @@ configure_outbound_handler() { subscription) log "Detected proxy configuration type: subscription" "debug" local subscription_url subscription_json_path urltest_tag selector_tag \ - urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url + urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ + urltest_testing_url subscription_group_by_countries config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" config_get urltest_tolerance "$section" "urltest_tolerance" 50 config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" + config_get_bool subscription_group_by_countries "$section" "subscription_group_by_countries" 0 if [ -z "$subscription_url" ]; then log "Subscription URL is not set. Aborted." "fatal" @@ -977,14 +1025,62 @@ configure_outbound_handler() { exit 1 fi - # Create urltest + selector (like urltest proxy_config_type) - urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" selector_tag="$(get_outbound_tag_by_section "$section")" - urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" - selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" - config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ - "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + + if [ "$subscription_group_by_countries" -eq 1 ]; then + local grouping_json country_flag country_group_outbounds country_group_tag \ + selector_outbound_tags selector_default ungrouped_outbound_tags + + grouping_json="$(sing_box_build_subscription_country_groups "$SUBSCRIPTION_OUTBOUND_TAGS")" + if [ -z "$grouping_json" ]; then + log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi + + for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do + country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" + if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then + continue + fi + + country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + + if [ -z "$selector_outbound_tags" ]; then + selector_outbound_tags="$country_group_tag" + selector_default="$country_group_tag" + else + selector_outbound_tags="$selector_outbound_tags,$country_group_tag" + fi + done + + ungrouped_outbound_tags="$(echo "$grouping_json" | jq -r '.ungrouped | join(",")' 2>/dev/null)" + if [ -n "$ungrouped_outbound_tags" ]; then + if [ -z "$selector_outbound_tags" ]; then + selector_outbound_tags="$ungrouped_outbound_tags" + selector_default="${ungrouped_outbound_tags%%,*}" + else + selector_outbound_tags="$selector_outbound_tags,$ungrouped_outbound_tags" + fi + fi + + if [ -z "$selector_outbound_tags" ]; then + log "No selector outbounds available after grouping subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi + + selector_outbounds="$(comma_string_to_json_array "$selector_outbound_tags")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$selector_default" "true")" + else + # Create urltest + selector (default subscription behaviour) + urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" + urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" + selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + fi ;; *) log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal"