diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua b/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua index 9bd7ee0c1c..3a1ac2cf89 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua @@ -391,6 +391,12 @@ o = s:taboption("smart", Flag, "auto_smart_switch", font_red..bold_on..translate o.description = font_red..bold_on..translate("Auto Switch Url-test and Load-balance Group to Smart Group")..bold_off..font_off o.default = 0 +o = s:taboption("smart", ListValue, "smart_strategy", translate("Strategy Type")) +o.description = translate("Requires Smart core support for sticky-sessions strategy") +o:value("0", translate("Disable")) +o:value("sticky-sessions", translate("Sticky-sessions")) +o.default = "0" + o = s:taboption("smart", Value, "smart_policy_priority", translate("Policy Priority")) o.default = "" o.placeholder = "Premium:0.9;SG:1.3" diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua b/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua index e59608f608..766dc27221 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua @@ -91,6 +91,12 @@ o:value("consistent-hashing", translate("Consistent-hashing")) o:value("sticky-sessions", translate("Sticky-sessions")) o:depends("type", "load-balance") +o = s:option(ListValue, "strategy_smart", translate("Strategy Type")) +o.rmempty = true +o.description = translate("Requires Smart core support for sticky-sessions strategy") +o:value("sticky-sessions", translate("Sticky-sessions")) +o:depends("type", "smart") + o = s:option(ListValue, "uselightgbm", translate("Uselightgbm")) o.description = translate("Use LightGBM Model For Smart Group Weight Prediction") o:value("false", translate("Disable")) diff --git a/luci-app-openclash/po/es/openclash.es.po b/luci-app-openclash/po/es/openclash.es.po index 00b1249636..d6693e05fb 100644 --- a/luci-app-openclash/po/es/openclash.es.po +++ b/luci-app-openclash/po/es/openclash.es.po @@ -1083,6 +1083,9 @@ msgstr "Sesiones persistentes" msgid "Strategy Type" msgstr "Tipo de estrategia" +msgid "Requires Smart core support for sticky-sessions strategy" +msgstr "Requiere un núcleo Smart compatible con la estrategia sticky-sessions" + msgid "Choose The Load-Balance's Strategy Type" msgstr "Selecciona el tipo de estrategia de balanceo" @@ -4063,4 +4066,4 @@ msgid "Note: oixCloud need a specific core installed, OpenClash will auto downlo msgstr "Nota: oixCloud requiere la instalación de un núcleo específico; OpenClash lo descargará automáticamente" msgid "After the core start, an oixCloud nodes provider will be added, which you can customize as needed" -msgstr "Tras iniciar el núcleo se añadirá un proveedor de nodos oixCloud que puedes personalizar" \ No newline at end of file +msgstr "Tras iniciar el núcleo se añadirá un proveedor de nodos oixCloud que puedes personalizar" diff --git a/luci-app-openclash/po/zh-cn/openclash.zh-cn.po b/luci-app-openclash/po/zh-cn/openclash.zh-cn.po index 69761b8e56..7feb17da86 100644 --- a/luci-app-openclash/po/zh-cn/openclash.zh-cn.po +++ b/luci-app-openclash/po/zh-cn/openclash.zh-cn.po @@ -1083,6 +1083,9 @@ msgstr "粘性会话(相同的来源地址-目标地址使用同一个代理 msgid "Strategy Type" msgstr "策略类型" +msgid "Requires Smart core support for sticky-sessions strategy" +msgstr "需要支持 sticky-sessions 策略的 Smart 内核" + msgid "Choose The Load-Balance's Strategy Type" msgstr "选择负载均衡的策略类型" diff --git a/luci-app-openclash/root/etc/config/openclash b/luci-app-openclash/root/etc/config/openclash index 3694743ce9..42fd2f66db 100644 --- a/luci-app-openclash/root/etc/config/openclash +++ b/luci-app-openclash/root/etc/config/openclash @@ -52,6 +52,7 @@ config openclash 'config' option router_self_proxy '1' option release_branch 'master' option smart_enable '0' + option smart_strategy '0' option dashboard_type 'Official' option yacd_type 'Meta' option append_default_dns '0' diff --git a/luci-app-openclash/root/etc/init.d/openclash b/luci-app-openclash/root/etc/init.d/openclash index 3c1c48d0e5..c138012117 100644 --- a/luci-app-openclash/root/etc/init.d/openclash +++ b/luci-app-openclash/root/etc/init.d/openclash @@ -3158,6 +3158,7 @@ EOF SMART_ENABLE_LGBM:int_bool \ SMART_POLICY_PRIORITY:string \ SMART_PREFER_ASN:int_bool \ + SMART_STRATEGY:string \ SKIP_PROXY_ADDRESS:int_bool \ SMALL_FLASH_MEMORY:int_bool \ SOCKS_PORT:int \ @@ -3585,6 +3586,7 @@ get_config() smart_policy_priority=$(uci_get_config "smart_policy_priority" || echo 0) smart_enable_lgbm=$(uci_get_config "smart_enable_lgbm" || echo 0) smart_prefer_asn=$(uci_get_config "smart_prefer_asn" || echo 0) + smart_strategy=$(uci_get_config "smart_strategy" || echo 0) [ -z "$dns_port" ] && dns_port=7874 && uci -q set openclash.config.dns_port=7874 uci -q commit openclash @@ -3631,7 +3633,7 @@ start_service() /usr/share/openclash/yml_rules_change.sh \ "$enable_custom_clash_rules" "$TMP_CONFIG_FILE"\ "$enable_rule_proxy" "$router_self_proxy" "$lan_ip" "$enable_redirect_dns" "$en_mode"\ - "$auto_smart_switch" "$smart_collect" "$smart_collect_rate" "$smart_policy_priority" "$smart_enable_lgbm" "$smart_prefer_asn" + "$auto_smart_switch" "$smart_collect" "$smart_collect_rate" "$smart_policy_priority" "$smart_enable_lgbm" "$smart_prefer_asn" "$smart_strategy" #Custom overwrite if [ -f "/tmp/yaml_overwrite.sh" ]; then diff --git a/luci-app-openclash/root/usr/share/openclash/yml_groups_set.sh b/luci-app-openclash/root/usr/share/openclash/yml_groups_set.sh index 1df974fea2..a939500ca7 100644 --- a/luci-app-openclash/root/usr/share/openclash/yml_groups_set.sh +++ b/luci-app-openclash/root/usr/share/openclash/yml_groups_set.sh @@ -199,13 +199,14 @@ set_provider_groups() yml_groups_set() { local section="$1" - local enabled config type name disable_udp strategy old_name test_url test_interval tolerance policy_filter uselightgbm collectdata policy_priority other_parameters icon + local enabled config type name disable_udp strategy strategy_smart old_name test_url test_interval tolerance policy_filter uselightgbm collectdata policy_priority other_parameters icon config_get_bool "enabled" "$section" "enabled" "1" config_get "config" "$section" "config" "" config_get "type" "$section" "type" "" config_get "name" "$section" "name" "" config_get "disable_udp" "$section" "disable_udp" "" config_get "strategy" "$section" "strategy" "" + config_get "strategy_smart" "$section" "strategy_smart" "" config_get "old_name" "$section" "old_name" "" config_get "test_url" "$section" "test_url" "" config_get "test_interval" "$section" "test_interval" "" @@ -298,6 +299,9 @@ yml_groups_set() } if [ "$type" = "smart" ]; then + [ "$strategy_smart" = "sticky-sessions" ] && { + echo " strategy: $strategy_smart" >>$GROUP_FILE + } [ -n "$uselightgbm" ] && { echo " uselightgbm: $uselightgbm" >>$GROUP_FILE } diff --git a/luci-app-openclash/root/usr/share/openclash/yml_rules_change.sh b/luci-app-openclash/root/usr/share/openclash/yml_rules_change.sh index ddfcb8eff0..47272ad893 100644 --- a/luci-app-openclash/root/usr/share/openclash/yml_rules_change.sh +++ b/luci-app-openclash/root/usr/share/openclash/yml_rules_change.sh @@ -387,15 +387,19 @@ yml_other_set() # smart auto switch begin - if ('${8}' == '1' or '${9}' == '1' or '${11}' != '0' or '${12}' != '0' or '${12}' == '1' or '${13}' == '1') and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then + if ('${8}' == '1' or '${9}' == '1' or '${11}' != '0' or '${12}' != '0' or '${12}' == '1' or '${13}' == '1' or '${14}' == 'sticky-sessions') and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then Value['proxy-groups'].each{|group| threads << Thread.new { if '${8}' == '1' and ['url-test', 'load-balance'].include?(group['type']) then group['type'] = 'smart'; + group.delete('strategy'); group['uselightgbm'] = true if '${12}' == '1'; group['collectdata'] = true if '${9}' == '1'; group['sample-rate'] = '${10}'.to_f if '${9}' == '1'; end; + if '${14}' == 'sticky-sessions' and group['type'] == 'smart' then + group['strategy'] = '${14}'; + end; if '${9}' == '1' and group['type'] == 'smart' then group['collectdata'] = true; group['sample-rate'] = '${10}'.to_f; @@ -432,4 +436,4 @@ yml_other_set() end" 2>/dev/null >> $LOG_FILE } -yml_other_set "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" "${12}" "${13}" \ No newline at end of file +yml_other_set "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" "${12}" "${13}" "${14}" diff --git a/tests/openclash_smart_strategy_test.sh b/tests/openclash_smart_strategy_test.sh new file mode 100755 index 0000000000..52acef0c1c --- /dev/null +++ b/tests/openclash_smart_strategy_test.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +groups_set="$ROOT_DIR/luci-app-openclash/root/usr/share/openclash/yml_groups_set.sh" +rules_change="$ROOT_DIR/luci-app-openclash/root/usr/share/openclash/yml_rules_change.sh" +init_script="$ROOT_DIR/luci-app-openclash/root/etc/init.d/openclash" +groups_config="$ROOT_DIR/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua" +overwrite_config="$ROOT_DIR/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua" + +assert_contains() { + local file="$1" + local pattern="$2" + local message="$3" + + if ! grep -Fq "$pattern" "$file"; then + echo "not ok - $message" >&2 + echo "missing pattern: $pattern" >&2 + echo "file: $file" >&2 + exit 1 + fi +} + +assert_contains "$groups_set" 'config_get "strategy_smart" "$section" "strategy_smart" ""' "Smart strategy is read from group UCI" +assert_contains "$groups_set" '[ "$strategy_smart" = "sticky-sessions" ] && {' "Smart strategy output is gated to sticky-sessions" +assert_contains "$groups_set" 'echo " strategy: $strategy_smart" >>$GROUP_FILE' "Smart strategy is written to generated YAML" + +assert_contains "$groups_config" 's:option(ListValue, "strategy_smart", translate("Strategy Type"))' "LuCI exposes Smart strategy_smart" +assert_contains "$groups_config" 'Requires Smart core support for sticky-sessions strategy' "Smart group UI warns about core support" +assert_contains "$groups_config" 'o:depends("type", "smart")' "Smart strategy UI only depends on Smart groups" +assert_contains "$overwrite_config" 'Requires Smart core support for sticky-sessions strategy' "Global Smart strategy UI warns about core support" + +assert_contains "$init_script" 'smart_strategy=$(uci_get_config "smart_strategy" || echo 0)' "Global Smart strategy is read from UCI" +assert_contains "$init_script" '"$auto_smart_switch" "$smart_collect" "$smart_collect_rate" "$smart_policy_priority" "$smart_enable_lgbm" "$smart_prefer_asn" "$smart_strategy"' "Global Smart strategy is passed to yml_rules_change" + +assert_contains "$rules_change" "'\${14}' == 'sticky-sessions'" "Auto Smart strategy parameter is checked" +assert_contains "$rules_change" "group.delete('strategy')" "Auto Smart switch removes legacy strategy values" +assert_contains "$rules_change" "if '\${14}' == 'sticky-sessions' and group['type'] == 'smart' then" "Global Smart strategy is gated to Smart groups" +assert_contains "$rules_change" "group['strategy'] = '\${14}'" "Auto Smart strategy is written to Smart groups" +assert_contains "$rules_change" 'yml_other_set "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}" "${11}" "${12}" "${13}" "${14}"' "Auto Smart strategy argument is forwarded" + +ruby -ryaml <<'RUBY' +def convert_groups(groups, auto_smart_switch, smart_strategy) + groups = Marshal.load(Marshal.dump(groups)) + if auto_smart_switch == "1" || smart_strategy == "sticky-sessions" + groups.each do |group| + if auto_smart_switch == "1" && ["url-test", "load-balance"].include?(group["type"]) + group["type"] = "smart" + group.delete("strategy") + end + if smart_strategy == "sticky-sessions" && group["type"] == "smart" + group["strategy"] = smart_strategy + end + end + end + groups +end + +source_groups = [ + {"name" => "Auto", "type" => "url-test"}, + {"name" => "Balance", "type" => "load-balance", "strategy" => "consistent-hashing"}, + {"name" => "Existing Smart", "type" => "smart"} +] + +default_groups = convert_groups(source_groups, "1", "0") +raise "default smart_strategy should not write strategy" if default_groups.any? { |group| group["strategy"] == "sticky-sessions" } +converted_balance = default_groups.find { |group| group["name"] == "Balance" } +raise "auto smart switch should remove load-balance strategy" if converted_balance.key?("strategy") + +sticky_existing_groups = convert_groups(source_groups, "0", "sticky-sessions") +existing = sticky_existing_groups.find { |group| group["name"] == "Existing Smart" } +raise "existing smart group should receive global sticky-sessions strategy" unless existing["strategy"] == "sticky-sessions" +non_smart = sticky_existing_groups.find { |group| group["name"] == "Auto" } +raise "global sticky-sessions strategy should not be written to non-Smart groups" if non_smart.key?("strategy") + +sticky_groups = convert_groups(source_groups, "1", "sticky-sessions") +converted = sticky_groups.select { |group| ["Auto", "Balance"].include?(group["name"]) } +raise "converted groups should all be smart" unless converted.all? { |group| group["type"] == "smart" } +raise "converted groups should receive sticky-sessions" unless converted.all? { |group| group["strategy"] == "sticky-sessions" } +converted_balance = sticky_groups.find { |group| group["name"] == "Balance" } +raise "auto smart switch should replace load-balance strategy with sticky-sessions" unless converted_balance["strategy"] == "sticky-sessions" +existing = sticky_groups.find { |group| group["name"] == "Existing Smart" } +raise "existing smart group should receive global sticky-sessions strategy" unless existing["strategy"] == "sticky-sessions" + +imported_uci = {} +yaml_group = {"name" => "Manual Smart", "type" => "smart", "strategy" => "sticky-sessions"} +if yaml_group.key?("strategy") && yaml_group["type"] == "smart" + imported_uci["strategy_smart"] = yaml_group["strategy"].to_s +end +raise "Smart strategy should import as strategy_smart" unless imported_uci["strategy_smart"] == "sticky-sessions" + +exported_group = {"name" => "Manual Smart", "type" => "smart"} +strategy_smart = imported_uci["strategy_smart"] +exported_group["strategy"] = strategy_smart if strategy_smart == "sticky-sessions" +raise "Smart strategy should export back to YAML" unless exported_group["strategy"] == "sticky-sessions" +RUBY + +echo "ok - Smart sticky strategy wiring"