From f0fae25e90558244071ca3c0596571d0c03f7812 Mon Sep 17 00:00:00 2001 From: reputationly <197039020@qq.com> Date: Tue, 23 Jun 2026 20:52:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(option):=20=E9=85=8D=E7=BD=AE=E9=A1=B9?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=97=B6=E6=8C=89?= =?UTF-8?q?=E7=A9=BA=E9=9B=86=E5=90=88=E5=A4=84=E7=90=86=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=20option=20=E5=90=8C=E6=AD=A5=E5=88=B7=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当 DB 中某个 JSON 类型的 option(如 GroupGroupRatio、ModelRatio、PayMethods 等) 被清空为 "",定时 SyncOptions -> updateOptionMap 会对空串做 json.Unmarshal, 每个同步周期(默认 60s)刷一条: [SYS] failed to update option map: unexpected end of JSON input 统一在各 Update*ByJSONString 入口及 types.LoadFromJsonString/WithCallback 中 将「空字符串/纯空白」视为「空集合」直接返回 nil,不再 unmarshal。空配置语义 本就等价于空 map/空数组,行为不变,只是消除噪声日志。 覆盖: - types/rw_map.go: 覆盖全部 9 个 ratio 配置 + GroupGroupRatio - Chats / AutoGroups / TopupGroupRatio / ModelRequestRateLimitGroup / UserUsableGroups / PayMethods 新增 types/rw_map_empty_test.go 验证空串/空白/合法 JSON 三种输入。 --- common/topup-ratio.go | 4 ++ setting/auto_group.go | 5 +++ setting/chat.go | 4 ++ .../operation_setting/payment_setting_old.go | 5 +++ setting/rate_limit.go | 4 ++ setting/user_usable_group.go | 4 ++ types/rw_map.go | 14 +++++++ types/rw_map_empty_test.go | 42 +++++++++++++++++++ 8 files changed, 82 insertions(+) create mode 100644 types/rw_map_empty_test.go diff --git a/common/topup-ratio.go b/common/topup-ratio.go index 2b60cde7d16..16cf3a00f2f 100644 --- a/common/topup-ratio.go +++ b/common/topup-ratio.go @@ -2,6 +2,7 @@ package common import ( "encoding/json" + "strings" "sync" ) @@ -26,6 +27,9 @@ func UpdateTopupGroupRatioByJSONString(jsonStr string) error { topupGroupRatioMutex.Lock() defer topupGroupRatioMutex.Unlock() topupGroupRatio = make(map[string]float64) + if strings.TrimSpace(jsonStr) == "" { + return nil + } return json.Unmarshal([]byte(jsonStr), &topupGroupRatio) } diff --git a/setting/auto_group.go b/setting/auto_group.go index 9261286bca9..36676bedcdc 100644 --- a/setting/auto_group.go +++ b/setting/auto_group.go @@ -1,6 +1,8 @@ package setting import ( + "strings" + "github.com/QuantumNous/new-api/common" ) @@ -21,6 +23,9 @@ func ContainsAutoGroup(group string) bool { func UpdateAutoGroupsByJsonString(jsonString string) error { autoGroups = make([]string, 0) + if strings.TrimSpace(jsonString) == "" { + return nil + } return common.Unmarshal([]byte(jsonString), &autoGroups) } diff --git a/setting/chat.go b/setting/chat.go index bb8a9977193..7e8f4482af8 100644 --- a/setting/chat.go +++ b/setting/chat.go @@ -2,6 +2,7 @@ package setting import ( "encoding/json" + "strings" "github.com/QuantumNous/new-api/common" ) @@ -41,6 +42,9 @@ var Chats = []map[string]string{ func UpdateChatsByJsonString(jsonString string) error { Chats = make([]map[string]string, 0) + if strings.TrimSpace(jsonString) == "" { + return nil + } return json.Unmarshal([]byte(jsonString), &Chats) } diff --git a/setting/operation_setting/payment_setting_old.go b/setting/operation_setting/payment_setting_old.go index d34b6f0b83f..fcd00858b5e 100644 --- a/setting/operation_setting/payment_setting_old.go +++ b/setting/operation_setting/payment_setting_old.go @@ -6,6 +6,8 @@ This file is the old version of the payment settings file. If you need to add ne package operation_setting import ( + "strings" + "github.com/QuantumNous/new-api/common" ) @@ -38,6 +40,9 @@ var PayMethods = []map[string]string{ func UpdatePayMethodsByJsonString(jsonString string) error { PayMethods = make([]map[string]string, 0) + if strings.TrimSpace(jsonString) == "" { + return nil + } return common.Unmarshal([]byte(jsonString), &PayMethods) } diff --git a/setting/rate_limit.go b/setting/rate_limit.go index 413f3958d75..7cc332804cc 100644 --- a/setting/rate_limit.go +++ b/setting/rate_limit.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math" + "strings" "sync" "github.com/QuantumNous/new-api/common" @@ -32,6 +33,9 @@ func UpdateModelRequestRateLimitGroupByJSONString(jsonStr string) error { defer ModelRequestRateLimitMutex.RUnlock() ModelRequestRateLimitGroup = make(map[string][2]int) + if strings.TrimSpace(jsonStr) == "" { + return nil + } return json.Unmarshal([]byte(jsonStr), &ModelRequestRateLimitGroup) } diff --git a/setting/user_usable_group.go b/setting/user_usable_group.go index eb04b7f3053..de4c0d76a1c 100644 --- a/setting/user_usable_group.go +++ b/setting/user_usable_group.go @@ -2,6 +2,7 @@ package setting import ( "encoding/json" + "strings" "sync" "github.com/QuantumNous/new-api/common" @@ -40,6 +41,9 @@ func UpdateUserUsableGroupsByJSONString(jsonStr string) error { defer userUsableGroupsMutex.Unlock() userUsableGroups = make(map[string]string) + if strings.TrimSpace(jsonStr) == "" { + return nil + } return json.Unmarshal([]byte(jsonStr), &userUsableGroups) } diff --git a/types/rw_map.go b/types/rw_map.go index 3d296816f2a..987e4f6569d 100644 --- a/types/rw_map.go +++ b/types/rw_map.go @@ -1,6 +1,7 @@ package types import ( + "strings" "sync" "github.com/QuantumNous/new-api/common" @@ -78,6 +79,11 @@ func LoadFromJsonString[K comparable, V any](m *RWMap[K, V], jsonStr string) err m.mutex.Lock() defer m.mutex.Unlock() m.data = make(map[K]V) + // Empty/blank value means "no entries"; treat as an empty map instead of + // failing with "unexpected end of JSON input". + if strings.TrimSpace(jsonStr) == "" { + return nil + } return common.Unmarshal([]byte(jsonStr), &m.data) } @@ -86,6 +92,14 @@ func LoadFromJsonStringWithCallback[K comparable, V any](m *RWMap[K, V], jsonStr m.mutex.Lock() defer m.mutex.Unlock() m.data = make(map[K]V) + // Empty/blank value means "no entries"; treat as an empty map instead of + // failing with "unexpected end of JSON input". + if strings.TrimSpace(jsonStr) == "" { + if onSuccess != nil { + onSuccess() + } + return nil + } err := common.Unmarshal([]byte(jsonStr), &m.data) if err == nil && onSuccess != nil { onSuccess() diff --git a/types/rw_map_empty_test.go b/types/rw_map_empty_test.go new file mode 100644 index 00000000000..301fafea7b8 --- /dev/null +++ b/types/rw_map_empty_test.go @@ -0,0 +1,42 @@ +package types + +import "testing" + +// Empty/blank option values must be treated as "no entries" rather than +// producing "unexpected end of JSON input" during option sync. +func TestLoadFromJsonStringEmpty(t *testing.T) { + for _, in := range []string{"", " ", "\n\t "} { + m := NewRWMap[string, float64]() + if err := LoadFromJsonString(m, in); err != nil { + t.Fatalf("LoadFromJsonString(%q) returned error: %v", in, err) + } + if got := m.Len(); got != 0 { + t.Fatalf("LoadFromJsonString(%q) expected empty map, got len=%d", in, got) + } + } +} + +func TestLoadFromJsonStringWithCallbackEmpty(t *testing.T) { + called := false + m := NewRWMap[string, float64]() + if err := LoadFromJsonStringWithCallback(m, "", func() { called = true }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !called { + t.Fatal("expected onSuccess callback to be invoked for empty input") + } + if m.Len() != 0 { + t.Fatalf("expected empty map, got len=%d", m.Len()) + } +} + +// Non-empty valid JSON must still load normally. +func TestLoadFromJsonStringValid(t *testing.T) { + m := NewRWMap[string, float64]() + if err := LoadFromJsonString(m, `{"a":1.5}`); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if v, ok := m.Get("a"); !ok || v != 1.5 { + t.Fatalf("expected a=1.5, got %v ok=%v", v, ok) + } +}