From 864cbd234ad36ae3eb943b0d1f04d4af971dba77 Mon Sep 17 00:00:00 2001 From: Murphy Shannon <71172725+PosvdM@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:51:16 +0800 Subject: [PATCH] fix(sub): pass numeric XMUX counts to Clash reuse-settings (#6843) Inbound XMUX counts such as maxConnections are stored as JSON numbers, but buildXhttpClashOpts kept only string values, so the panel default maxConnections: 3 never reached Clash/Mihomo clients. Numeric values are now rendered as plain integers (FormatFloat, so 1000000 does not become 1e+06) and zero values are still dropped. --- internal/sub/clash_service.go | 11 ++++++++-- internal/sub/clash_service_test.go | 32 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index d2e02c8a0..ec29e3101 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -1018,8 +1018,15 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any { {"hMaxRequestTimes", "h-max-request-times"}, {"hMaxReusableSecs", "h-max-reusable-secs"}, } { - if v, ok := xmux[f.src].(string); ok && v != "" { - reuse[f.dst] = v + v, ok := nonZeroShareValue(xmux[f.src]) + if !ok { + continue + } + if n, isFloat := v.(float64); isFloat { + // JSON numbers decode as float64, and %v would render 1000000 as "1e+06". + reuse[f.dst] = strconv.FormatFloat(n, 'f', -1, 64) + } else { + reuse[f.dst] = fmt.Sprint(v) } } if v, ok := nonZeroShareValue(xmux["hKeepAlivePeriod"]); ok { diff --git a/internal/sub/clash_service_test.go b/internal/sub/clash_service_test.go index e9716835a..092058f98 100644 --- a/internal/sub/clash_service_test.go +++ b/internal/sub/clash_service_test.go @@ -1,6 +1,7 @@ package sub import ( + "encoding/json" "fmt" "reflect" "testing" @@ -798,6 +799,37 @@ func TestBuildXhttpClashOpts_XmuxMapsToReuseSettings(t *testing.T) { t.Error("reuse-settings should not appear when only hKeepAlivePeriod=0") } }) + + // Sub-test 5: numeric counts as json.Unmarshal yields them (#6841) + t.Run("NumericXmux", func(t *testing.T) { + cases := []struct { + name, xmux string + want map[string]any + }{ + { + name: "PanelDefaults", + xmux: `{"maxConcurrency":"","maxConnections":3,"cMaxReuseTimes":0,"hMaxRequestTimes":"600-900","hMaxReusableSecs":"1800-3000","hKeepAlivePeriod":0}`, + want: map[string]any{"max-connections": "3", "h-max-request-times": "600-900", "h-max-reusable-secs": "1800-3000"}, + }, + { + name: "LargeCountNotExponent", + xmux: `{"cMaxReuseTimes":1000000}`, + want: map[string]any{"c-max-reuse-times": "1000000"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var xmux map[string]any + if err := json.Unmarshal([]byte(tc.xmux), &xmux); err != nil { + t.Fatal(err) + } + opts := buildXhttpClashOpts(map[string]any{"path": "/", "xmux": xmux}) + if got := opts["reuse-settings"]; !reflect.DeepEqual(got, tc.want) { + t.Errorf("reuse-settings = %#v, want %#v", got, tc.want) + } + }) + } + }) } func TestBuildXhttpClashOpts_ServerOnlyFieldsExcluded(t *testing.T) {