feat: add special user usable group setting - #2121
Conversation
WalkthroughThis PR refactors group and channel selection logic by introducing a service layer abstraction, migrating group-related functionality from the setting package to a new service package. It adds a generic thread-safe map wrapper (RWMap), implements user-specific group overrides via ratio settings, and enhances channel selection with weight-smoothing logic. Changes
Sequence Diagram(s)sequenceDiagram
participant Controller
participant Service
participant Setting
participant Model
Controller->>Service: GetUserUsableGroups(userGroup)
Service->>Setting: GetUserUsableGroupsCopy()
Service->>Setting: GetGroupRatioSetting()
Note over Service: Apply user-specific<br/>overrides from ratio_setting
Service-->>Controller: map[string]string
Controller->>Service: GetUserAutoGroup(userGroup)
Service->>Setting: GetAutoGroups()
Note over Service: Filter by<br/>usable groups
Service-->>Controller: []string
sequenceDiagram
participant Relay
participant Service
participant Model
Relay->>Service: CacheGetRandomSatisfiedChannel(ctx, group, model, retry)
alt group == "auto"
Service->>Service: GetUserAutoGroup(userGroup)
loop for each auto group
Service->>Model: GetRandomSatisfiedChannel(autoGroup, model, retry)
Model-->>Service: *Channel or error
alt Channel found
Service->>Relay: return Channel, selectGroup, nil
end
end
else specific group
Service->>Model: GetRandomSatisfiedChannel(group, model, retry)
Model-->>Service: *Channel, error
Service-->>Relay: Channel, selectGroup, error
end
sequenceDiagram
participant Model as model/channel_cache
participant Selection as Channel Selection
Note over Model: GetRandomSatisfiedChannel(group, model, retry)
Model->>Model: Fetch all channels for group/model
Model->>Selection: Calculate average weight
Model->>Selection: Determine shouldSmooth<br/>(avg < threshold)
alt shouldSmooth
Model->>Selection: smoothingFactor = 1
else
Model->>Selection: smoothingFactor = 100
end
Model->>Selection: Accumulate totals with<br/>multiplied weights
Model->>Selection: Random-weight selection
Model-->>Model: Return selected Channel
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
left a comment
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
setting/auto_group.go (1)
7-37: Data race on autoGroups and unsafe exposure; guard with RWMutex and return a copyautoGroups is mutated/read concurrently without locks and GetAutoGroups leaks the backing slice. This can cause races and accidental mutation.
Apply this diff:
@@ -import ( - "github.com/QuantumNous/new-api/common" -) +import ( + "github.com/QuantumNous/new-api/common" + "sync" +) -var autoGroups = []string{ +var autoGroupsMu sync.RWMutex +var autoGroups = []string{ "default", } @@ func ContainsAutoGroup(group string) bool { - for _, autoGroup := range autoGroups { + autoGroupsMu.RLock() + defer autoGroupsMu.RUnlock() + for _, autoGroup := range autoGroups { if autoGroup == group { return true } } return false } @@ func UpdateAutoGroupsByJsonString(jsonString string) error { - autoGroups = make([]string, 0) - return common.Unmarshal([]byte(jsonString), &autoGroups) + var tmp []string + if err := common.Unmarshal([]byte(jsonString), &tmp); err != nil { + return err + } + autoGroupsMu.Lock() + autoGroups = tmp + autoGroupsMu.Unlock() + return nil } @@ func AutoGroups2JsonString() string { - jsonBytes, err := common.Marshal(autoGroups) + autoGroupsMu.RLock() + jsonBytes, err := common.Marshal(autoGroups) + autoGroupsMu.RUnlock() if err != nil { return "[]" } return string(jsonBytes) } func GetAutoGroups() []string { - return autoGroups + autoGroupsMu.RLock() + defer autoGroupsMu.RUnlock() + return append([]string(nil), autoGroups...) }
🧹 Nitpick comments (6)
setting/config/config.go (1)
134-146: Pointer (de)serialization is correct; tighten input validation and unify JSON helpersCurrent logic works. Two small improvements:
- Return an error if config isn’t a pointer to struct (avoids silent no-ops).
- Consider using common.Marshal/Unmarshal for consistency with other settings modules.
- if val.Kind() != reflect.Ptr { - return nil - } + if val.Kind() != reflect.Ptr { + return fmt.Errorf("updateConfigFromMap requires pointer to struct") + } @@ - if val.Kind() != reflect.Struct { - return nil - } + if val.Kind() != reflect.Struct { + return fmt.Errorf("updateConfigFromMap requires pointer to struct, got %s", val.Kind()) + }Optionally replace json.Marshal/Unmarshal calls with common.Marshal/Unmarshal for uniformity.
Also applies to: 230-244
relay/helper/price.go (1)
26-26: Consider standardizing debug logging throughout the file.The code now uses
logger.LogDebughere, but lines 122-124 still useprintlnfor debug output. For consistency, consider updating the remaining debug statements to use the logger as well.if common.DebugEnabled { - println(fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting())) + logger.LogDebug(c, fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting())) }web/src/components/settings/RatioSetting.jsx (1)
61-63: GuardstartsWithfor non-string values to avoid runtime errors.
item.valuemay be non-string/empty; add type and trim checks.- if ( - item.value.startsWith('{') || item.value.startsWith('[') - ) { + if ( + typeof item.value === 'string' && + (item.value.trim().startsWith('{') || item.value.trim().startsWith('[')) + ) {service/channel_select.go (1)
23-34: Minor: error message wording.Consider “auto groups are not enabled” for grammar consistency.
service/group.go (1)
16-28: Trim prefixes robustly and centralize markers.Defensive trim helps tolerate accidental spaces; constants avoid magic strings.
- for specialGroup, desc := range specialSettings { + for specialGroup, desc := range specialSettings { + specialGroup = strings.TrimSpace(specialGroup) if strings.HasPrefix(specialGroup, "-:") {Optionally:
const ( prefixRemove = "-:" prefixAdd = "+:" )types/rw_map.go (1)
77-82: Optional: add Delete/Keys helpers.These often help callers avoid copying maps just to remove/read keys.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
web/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (21)
controller/group.go(2 hunks)controller/model.go(2 hunks)controller/pricing.go(3 hunks)controller/relay.go(1 hunks)controller/user.go(2 hunks)logger/logger.go(1 hunks)middleware/auth.go(2 hunks)middleware/distributor.go(3 hunks)model/ability.go(1 hunks)model/channel_cache.go(2 hunks)relay/helper/price.go(2 hunks)service/channel_select.go(1 hunks)service/group.go(1 hunks)setting/auto_group.go(2 hunks)setting/config/config.go(2 hunks)setting/ratio_setting/group_ratio.go(2 hunks)setting/user_usable_group.go(0 hunks)types/rw_map.go(1 hunks)web/src/components/settings/RatioSetting.jsx(2 hunks)web/src/components/table/tokens/modals/EditTokenModal.jsx(1 hunks)web/src/pages/Setting/Ratio/GroupRatioSettings.jsx(2 hunks)
💤 Files with no reviewable changes (1)
- setting/user_usable_group.go
🧰 Additional context used
🧬 Code graph analysis (17)
controller/model.go (1)
service/group.go (1)
GetUserAutoGroup(45-54)
controller/group.go (2)
service/group.go (1)
GetUserUsableGroups(10-37)setting/ratio_setting/group_ratio.go (1)
GetGroupRatioCopy(66-75)
middleware/auth.go (1)
service/group.go (1)
GetUserUsableGroups(10-37)
controller/relay.go (1)
service/channel_select.go (1)
CacheGetRandomSatisfiedChannel(14-42)
service/channel_select.go (7)
model/channel.go (1)
Channel(21-58)common/gin.go (1)
GetContextKeyString(69-71)constant/context_key.go (1)
ContextKeyUserGroup(45-45)setting/auto_group.go (1)
GetAutoGroups(35-37)service/group.go (1)
GetUserAutoGroup(45-54)logger/logger.go (1)
LogDebug(69-74)model/channel_cache.go (1)
GetRandomSatisfiedChannel(96-181)
setting/auto_group.go (1)
common/json.go (2)
Unmarshal(9-11)Marshal(21-23)
web/src/pages/Setting/Ratio/GroupRatioSettings.jsx (1)
web/src/components/settings/RatioSetting.jsx (1)
inputs(35-50)
controller/pricing.go (1)
service/group.go (2)
GetUserUsableGroups(10-37)GetUserAutoGroup(45-54)
service/group.go (3)
setting/user_usable_group.go (1)
GetUserUsableGroupsCopy(16-25)setting/ratio_setting/group_ratio.go (1)
GetGroupRatioSetting(58-64)setting/auto_group.go (1)
GetAutoGroups(35-37)
model/ability.go (1)
model/channel.go (1)
Channel(21-58)
setting/config/config.go (1)
common/json.go (2)
Marshal(21-23)Unmarshal(9-11)
relay/helper/price.go (1)
logger/logger.go (1)
LogDebug(69-74)
setting/ratio_setting/group_ratio.go (2)
types/rw_map.go (2)
RWMap(9-12)NewRWMap(27-31)setting/config/config.go (1)
GlobalConfig(19-19)
middleware/distributor.go (5)
common/gin.go (1)
GetContextKeyString(69-71)constant/context_key.go (1)
ContextKeyUsingGroup(46-46)service/group.go (1)
GroupInUserUsableGroups(39-42)service/channel_select.go (1)
CacheGetRandomSatisfiedChannel(14-42)types/error.go (1)
ErrorCodeModelNotFound(75-75)
types/rw_map.go (1)
common/json.go (2)
Unmarshal(9-11)Marshal(21-23)
controller/user.go (1)
service/group.go (1)
GetUserUsableGroups(10-37)
model/channel_cache.go (3)
model/channel.go (1)
Channel(21-58)common/constants.go (1)
MemoryCacheEnabled(72-72)model/ability.go (1)
GetChannel(106-144)
🔇 Additional comments (18)
controller/model.go (2)
18-18: Service layer import switch looks goodCentralizing group logic under service is the right direction. No issues.
151-159: Auto-group resolution correctly uses service — verified across all controllersAll controller call-sites properly reference
service.GetUserAutoGroupfor auto-group resolution:
controller/model.go:152✓controller/pricing.go:48✓No lingering direct
setting.AutoGroupsusages in the controller layer for this logic. Consistency confirmed.web/src/components/table/tokens/modals/EditTokenModal.jsx (1)
137-145: Removing client-side forced 'auto' is appropriateLetting the backend decide availability/defaults reduces UI coupling. Keep the sort-to-top for 'auto' — it’s a good UX hint.
Please confirm: when group is unset on create/edit, the server defaults to the user’s group (and returns 'auto' in /api/user/self/groups only when allowed).
controller/relay.go (1)
227-238: LGTM! Service layer integration is clean.The refactoring to use
service.CacheGetRandomSatisfiedChannelproperly handles the new return value (selectGroup) and uses it in error messages for better diagnostics.controller/pricing.go (1)
33-48: LGTM! Service layer refactoring is correct.The migration from
settingpackage toservicepackage for group-related functions is consistent and properly implemented.web/src/pages/Setting/Ratio/GroupRatioSettings.jsx (1)
189-212: LGTM! New configuration field follows established patterns.The new "分组特殊可用分组" field is properly integrated with JSON validation, clear documentation, and examples. The implementation is consistent with existing form fields.
controller/user.go (1)
583-583: LGTM! Service layer refactoring is correct.The migration to
service.GetUserUsableGroupsis properly implemented and consistent with the broader refactoring effort.middleware/auth.go (1)
269-270: LGTM! Error message is now more accurate.The error message change from "令牌分组 %s 已被禁用" to "无权访问 %s 分组" better reflects the actual validation being performed—checking user permissions rather than group status.
middleware/distributor.go (1)
82-116: LGTM! Variable rename clarifies intent.The rename from
userGrouptousingGroupbetter conveys that this is the group being used for the current request (which may differ from the user's default group). All references are consistently updated, and the service layer integration is correct.model/ability.go (1)
106-106: No caller updates needed; wrapper function maintains backward compatibility.The function refactoring is complete and correctly implemented. While
GetRandomSatisfiedChannelwas moved to ability.go and renamed toGetChannel, a wrapper function with the original name was intentionally retained in channel_cache.go. This wrapper provides the caching layer and delegates toGetChannelwhen cache is disabled. All callers in service/channel_select.go are using the wrapper function (model.GetRandomSatisfiedChannel), which correctly invokes the renamedGetChannelfunction, maintaining proper functionality and caching behavior.controller/group.go (1)
7-7: Service-layer migration looks good.Using
service.GetUserUsableGroupsand checking"auto"presence via the computed map is correct and keeps semantics intact.Also applies to: 31-31, 41-46
web/src/components/settings/RatioSetting.jsx (1)
49-50: New option key wired correctly.Key
'group_ratio_setting.group_special_usable_group'matches the backend field path; no issues.service/group.go (2)
39-42: LGTM: simple membership helper.
45-54: LGTM: auto-group intersection logic is correct.types/rw_map.go (2)
27-31: Good: constructor initializes map avoiding nil marshals.
60-69: ReadAll correctly returns a copy.Prevents external mutation; good defensive design.
setting/ratio_setting/group_ratio.go (2)
45-56: Init/registration looks correct; RWMap seeded with defaults.
58-64: Nil-guard in getter prevents panics after deserialization.Solid safeguard.
| func LogDebug(ctx context.Context, msg string, args ...any) { | ||
| msg = fmt.Sprintf(msg, args...) | ||
| if common.DebugEnabled { |
There was a problem hiding this comment.
🧩 Analysis chain
Avoid formatting when debug is disabled; also verify callsites use printf-style.
fmt.Sprintf runs even if debug logs are off. Guard it to avoid work on hot paths. Also ensure callers provide format verbs now that args are variadic.
Apply:
-func LogDebug(ctx context.Context, msg string, args ...any) {
- msg = fmt.Sprintf(msg, args...)
- if common.DebugEnabled {
- logHelper(ctx, loggerDebug, msg)
- }
-}
+func LogDebug(ctx context.Context, msg string, args ...any) {
+ if !common.DebugEnabled {
+ return
+ }
+ if len(args) > 0 {
+ msg = fmt.Sprintf(msg, args...)
+ }
+ logHelper(ctx, loggerDebug, msg)
+}Find callsites that pass extra args but no format verbs:
🏁 Script executed:
#!/bin/bash
# Calls with extra args and a string literal without '%' in the format.
rg -nP 'logger\.LogDebug\([^,]+,\s*"[^"%]*"\s*,'
# Calls passing a likely gin Context as first arg.
rg -nP 'logger\.LogDebug\(\s*c\s*,'Length of output: 1486
Fix format strings at callsites before applying LogDebug refactor.
Verification found 2 critical mismatches in service/channel_select.go:
- Line 24:
logger.LogDebug(c, "Auto selecting group:", autoGroup)— format string has no%vbut passesautoGroup - Line 31:
logger.LogDebug(c, "Auto selected group:", autoGroup)— same issue
These must be converted to printf-style:
logger.LogDebug(c, "Auto selecting group: %v", autoGroup)
logger.LogDebug(c, "Auto selected group: %v", autoGroup)After fixing these 2 callsites, the proposed refactor can be safely applied.
🤖 Prompt for AI Agents
In logger/logger.go around lines 69 to 71, the new LogDebug expects printf-style
format strings but two callsites in service/channel_select.go (line 24 and line
31) pass values without format verbs; update those calls to use printf
formatting (e.g., change "Auto selecting group:" with autoGroup to "Auto
selecting group: %v" and "Auto selected group:" with autoGroup to "Auto selected
group: %v") so the arguments match the format string, then proceed with the
LogDebug refactor.
| var shouldSmooth = false | ||
| var sumWeight = 0 | ||
| var targetChannels []*Channel | ||
| for _, channelId := range channels { | ||
| if channel, ok := channelsIDM[channelId]; ok { | ||
| if channel.GetPriority() == targetPriority { | ||
| sumWeight += channel.GetWeight() | ||
| targetChannels = append(targetChannels, channel) | ||
| } | ||
| } else { | ||
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | ||
| } | ||
| } | ||
| if sumWeight/len(targetChannels) < 10 { | ||
| shouldSmooth = true | ||
| } | ||
|
|
||
| // 平滑系数 | ||
| smoothingFactor := 10 | ||
| // Calculate the total weight of all channels up to endIdx | ||
| totalWeight := 0 | ||
| for _, channel := range targetChannels { | ||
| totalWeight += channel.GetWeight() + smoothingFactor | ||
| smoothingFactor := 1 | ||
| if shouldSmooth { | ||
| smoothingFactor = 100 | ||
| } | ||
| // Calculate the total weight of all channels up to endIdx | ||
| totalWeight := sumWeight * smoothingFactor | ||
| // Generate a random value in the range [0, totalWeight) | ||
| randomWeight := rand.Intn(totalWeight) | ||
|
|
||
| // Find a channel based on its weight | ||
| for _, channel := range targetChannels { | ||
| randomWeight -= channel.GetWeight() + smoothingFactor | ||
| randomWeight -= channel.GetWeight() * smoothingFactor | ||
| if randomWeight < 0 { | ||
| return channel, nil |
There was a problem hiding this comment.
🧩 Analysis chain
Fix possible panic (rand.Intn(0)) and align weighting with DB path
If all target channel weights are zero, totalWeight becomes 0, causing rand.Intn(0) to panic. DB path uses a +10 baseline; mirror it to keep parity.
Apply this diff:
- var shouldSmooth = false
- var sumWeight = 0
+ var shouldSmooth = false
+ var sumWeight = 0
...
- if sumWeight/len(targetChannels) < 10 {
+ if sumWeight/len(targetChannels) < 10 {
shouldSmooth = true
}
// 平滑系数
- smoothingFactor := 1
+ smoothingFactor := 1
if shouldSmooth {
smoothingFactor = 100
}
- // Calculate the total weight of all channels up to endIdx
- totalWeight := sumWeight * smoothingFactor
- // Generate a random value in the range [0, totalWeight)
- randomWeight := rand.Intn(totalWeight)
+ // Add baseline to match DB selection logic (+10 per channel)
+ const base = 10
+ totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor
+ // Guard: if somehow still zero (shouldn't), fallback to uniform
+ if totalWeight <= 0 {
+ return targetChannels[rand.Intn(len(targetChannels))], nil
+ }
+ randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight
for _, channel := range targetChannels {
- randomWeight -= channel.GetWeight() * smoothingFactor
+ randomWeight -= (channel.GetWeight() + base) * smoothingFactor
if randomWeight < 0 {
return channel, nil
}
}🏁 Script executed:
#!/bin/bash
# Sanity: search for any stale call sites to old channel selection APIs.
rg -nP -C2 '\b(CacheGetRandomSatisfiedChannel|getRandomSatisfiedChannel\()' || true
# Optional: check all usages of GetRandomSatisfiedChannel now reference the exported symbol.
rg -nP -C2 '\bGetRandomSatisfiedChannel\('Length of output: 2501
🏁 Script executed:
#!/bin/bash
# Examine the actual code at lines 145-176 in model/channel_cache.go
sed -n '90,200p' model/channel_cache.goLength of output: 3102
🏁 Script executed:
#!/bin/bash
# Search for DB-related channel selection logic that uses baseline weighting
rg -nP -C5 'GetChannel|DatabaseGetRandomSatisfiedChannel' model/channel_cache.go || trueLength of output: 1020
🏁 Script executed:
#!/bin/bash
# Search for references to +10 baseline or weight baseline in the file
rg -nP -C3 '\+\s*10|baseline' model/channel_cache.go || trueLength of output: 0
🏁 Script executed:
#!/bin/bash
# Check if there's a pattern where weight + 10 is used elsewhere
rg -nP 'weight.*\+.*10|10.*weight' model/channel_cache.go || trueLength of output: 0
🏁 Script executed:
#!/bin/bash
# Search the entire channel_cache file for GetChannel method to understand the DB fallback
rg -nA 10 'func.*GetChannel\(' model/channel_cache.go || trueLength of output: 304
🏁 Script executed:
#!/bin/bash
# Check the full context of the GetRandomSatisfiedChannel function definition
sed -n '96,180p' model/channel_cache.goLength of output: 2539
🏁 Script executed:
#!/bin/bash
# Look for any constants or baseline definitions near the function
rg -nP 'const|var.*=.*10' model/channel_cache.go | head -20Length of output: 239
🏁 Script executed:
#!/bin/bash
# Check if there's database selection logic in the same file
rg -nP 'database|db\.' model/channel_cache.go | head -15 || trueLength of output: 174
🏁 Script executed:
#!/bin/bash
# Search for the fallback GetChannel call mentioned in the scratchpad
rg -nA 15 'GetChannel.*\(' model/channel_cache.goLength of output: 1326
🏁 Script executed:
#!/bin/bash
# Check the structure to see if there's an old implementation or fallback path
rg -nP 'rand\.Intn|randomWeight' model/channel_cache.goLength of output: 131
🏁 Script executed:
#!/bin/bash
# Look broadly at the entire function to understand its structure
cat model/channel_cache.go | grep -n "func GetRandomSatisfiedChannel" -A 100 | head -120Length of output: 3221
🏁 Script executed:
#!/bin/bash
# Search for channel weight baseline logic in related files
rg -rn '\+\s*10.*weight|weight.*\+.*10' service/ model/ || trueLength of output: 43
🏁 Script executed:
#!/bin/bash
# Check if there's a comment or code mentioning database selection with baseline
rg -n 'database\|GetChannel\|db selection' model/channel_cache.go -C 3 || trueLength of output: 0
🏁 Script executed:
#!/bin/bash
# Directly extract and display the specific lines mentioned
sed -n '145,176p' model/channel_cache.goLength of output: 923
🏁 Script executed:
#!/bin/bash
# Get full context: 20 lines before and after the target lines
sed -n '125,196p' model/channel_cache.goLength of output: 1962
🏁 Script executed:
#!/bin/bash
# Find all function definitions to understand file structure
rg -n '^func' model/channel_cache.goLength of output: 381
🏁 Script executed:
#!/bin/bash
# Search for any place where a weight baseline is added
rg -nP 'weight\s*\+\s*10|base\s*=\s*10' . --type go -g '!**/test' || trueLength of output: 0
🏁 Script executed:
#!/bin/bash
# Look for the database fallback implementation pattern
rg -nA 20 'MemoryCacheEnabled.*false|database.*select' model/channel_cache.go || trueLength of output: 0
🏁 Script executed:
#!/bin/bash
# Check what comes before and after the problematic code section
awk 'NR>=130 && NR<=185' model/channel_cache.goLength of output: 1606
🏁 Script executed:
#!/bin/bash
# Search for channel weight calculations in the db layer
find . -name "*.go" -type f | xargs grep -l "GetChannel\|database" | xargs grep -n "weight" | grep -v test | head -20Length of output: 1491
🏁 Script executed:
#!/bin/bash
# Let's search for any DB layer channel selection
rg -n 'GetChannel|FromDatabase' model/ -C 2 | head -50 || trueLength of output: 2027
🏁 Script executed:
#!/bin/bash
# Check the entire flow to see what happens when MemoryCacheEnabled is false
sed -n '96,120p' model/channel_cache.goLength of output: 780
🏁 Script executed:
#!/bin/bash
# Get the database selection code path
rg -nA 30 'MemoryCacheEnabled' model/channel_cache.go | head -60Length of output: 1992
🏁 Script executed:
#!/bin/bash
# Simple: just cat the whole file to understand structure
head -250 model/channel_cache.go | tail -150Length of output: 3925
🏁 Script executed:
#!/bin/bash
# Direct extraction
awk 'NR>=140 && NR<=180' model/channel_cache.goLength of output: 1162
🏁 Script executed:
#!/bin/bash
# Look at wider context around the indicated lines
head -200 model/channel_cache.go | tail -80Length of output: 2191
🏁 Script executed:
#!/bin/bash
# Most direct approach - get lines 145-176 and surrounding lines
sed -n '140,185p' model/channel_cache.goLength of output: 1281
🏁 Script executed:
#!/bin/bash
# Count total lines in file to ensure line numbers are valid
wc -l model/channel_cache.goLength of output: 26
🏁 Script executed:
#!/bin/bash
# Now get exact range with context
sed -n '136,190p' model/channel_cache.goLength of output: 1474
🏁 Script executed:
#!/bin/bash
# Show from function definition to beyond the target lines
sed -n '96,180p' model/channel_cache.goLength of output: 2539
🏁 Script executed:
#!/bin/bash
# View complete GetRandomSatisfiedChannel function
sed -n '/^func GetRandomSatisfiedChannel/,/^func [a-zA-Z]/p' model/channel_cache.go | head -150Length of output: 2591
🏁 Script executed:
#!/bin/bash
# Simple dump of critical section
sed -n '95,200p' model/channel_cache.goLength of output: 2973
🏁 Script executed:
#!/bin/bash
# Most basic - just show lines 145-176
sed -n '145,176p' model/channel_cache.goLength of output: 923
Fix possible panic (rand.Intn(0)) and align weighting with DB path
If all target channel weights are zero, totalWeight becomes 0, causing rand.Intn(0) to panic. The database fallback path uses a +10 baseline per channel; mirror it in cache selection to maintain consistent behavior.
// get the priority for the given retry number
var shouldSmooth = false
var sumWeight = 0
var targetChannels []*Channel
for _, channelId := range channels {
if channel, ok := channelsIDM[channelId]; ok {
if channel.GetPriority() == targetPriority {
sumWeight += channel.GetWeight()
targetChannels = append(targetChannels, channel)
}
} else {
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
}
}
if sumWeight/len(targetChannels) < 10 {
shouldSmooth = true
}
// 平滑系数
smoothingFactor := 1
if shouldSmooth {
smoothingFactor = 100
}
- // Calculate the total weight of all channels up to endIdx
- totalWeight := sumWeight * smoothingFactor
- // Generate a random value in the range [0, totalWeight)
- randomWeight := rand.Intn(totalWeight)
+ // Add baseline to match DB selection logic (+10 per channel)
+ const base = 10
+ totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor
+ // Guard: if somehow still zero (shouldn't), fallback to uniform
+ if totalWeight <= 0 {
+ return targetChannels[rand.Intn(len(targetChannels))], nil
+ }
+ randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight
for _, channel := range targetChannels {
- randomWeight -= channel.GetWeight() * smoothingFactor
+ randomWeight -= (channel.GetWeight() + base) * smoothingFactor
if randomWeight < 0 {
return channel, nil
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var shouldSmooth = false | |
| var sumWeight = 0 | |
| var targetChannels []*Channel | |
| for _, channelId := range channels { | |
| if channel, ok := channelsIDM[channelId]; ok { | |
| if channel.GetPriority() == targetPriority { | |
| sumWeight += channel.GetWeight() | |
| targetChannels = append(targetChannels, channel) | |
| } | |
| } else { | |
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | |
| } | |
| } | |
| if sumWeight/len(targetChannels) < 10 { | |
| shouldSmooth = true | |
| } | |
| // 平滑系数 | |
| smoothingFactor := 10 | |
| // Calculate the total weight of all channels up to endIdx | |
| totalWeight := 0 | |
| for _, channel := range targetChannels { | |
| totalWeight += channel.GetWeight() + smoothingFactor | |
| smoothingFactor := 1 | |
| if shouldSmooth { | |
| smoothingFactor = 100 | |
| } | |
| // Calculate the total weight of all channels up to endIdx | |
| totalWeight := sumWeight * smoothingFactor | |
| // Generate a random value in the range [0, totalWeight) | |
| randomWeight := rand.Intn(totalWeight) | |
| // Find a channel based on its weight | |
| for _, channel := range targetChannels { | |
| randomWeight -= channel.GetWeight() + smoothingFactor | |
| randomWeight -= channel.GetWeight() * smoothingFactor | |
| if randomWeight < 0 { | |
| return channel, nil | |
| var shouldSmooth = false | |
| var sumWeight = 0 | |
| var targetChannels []*Channel | |
| for _, channelId := range channels { | |
| if channel, ok := channelsIDM[channelId]; ok { | |
| if channel.GetPriority() == targetPriority { | |
| sumWeight += channel.GetWeight() | |
| targetChannels = append(targetChannels, channel) | |
| } | |
| } else { | |
| return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId) | |
| } | |
| } | |
| if sumWeight/len(targetChannels) < 10 { | |
| shouldSmooth = true | |
| } | |
| // 平滑系数 | |
| smoothingFactor := 1 | |
| if shouldSmooth { | |
| smoothingFactor = 100 | |
| } | |
| // Add baseline to match DB selection logic (+10 per channel) | |
| const base = 10 | |
| totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor | |
| // Guard: if somehow still zero (shouldn't), fallback to uniform | |
| if totalWeight <= 0 { | |
| return targetChannels[rand.Intn(len(targetChannels))], nil | |
| } | |
| randomWeight := rand.Intn(totalWeight) | |
| // Find a channel based on its weight | |
| for _, channel := range targetChannels { | |
| randomWeight -= (channel.GetWeight() + base) * smoothingFactor | |
| if randomWeight < 0 { | |
| return channel, nil |
🤖 Prompt for AI Agents
In model/channel_cache.go around lines 145-176, totalWeight can be zero (causing
rand.Intn(0) panic) and the cache selection uses raw weights while the DB
fallback adds a +10 baseline per channel; fix by computing sumWeight using
(channel.GetWeight() + 10) for each target channel (and apply the same
smoothingFactor to those adjusted weights), ensure totalWeight is > 0 before
calling rand.Intn (return a clear error or fall back to a deterministic choice
if it somehow remains 0), and use the same adjusted weights when decrementing
randomWeight during selection so cache and DB paths behave consistently.
| func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) { | ||
| var channel *model.Channel | ||
| var err error | ||
| selectGroup := group | ||
| userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) | ||
| if group == "auto" { | ||
| if len(setting.GetAutoGroups()) == 0 { | ||
| return nil, selectGroup, errors.New("auto groups is not enabled") | ||
| } | ||
| for _, autoGroup := range GetUserAutoGroup(userGroup) { | ||
| logger.LogDebug(c, "Auto selecting group:", autoGroup) | ||
| channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry) | ||
| if channel == nil { | ||
| continue | ||
| } else { | ||
| c.Set("auto_group", autoGroup) | ||
| selectGroup = autoGroup | ||
| logger.LogDebug(c, "Auto selected group:", autoGroup) | ||
| break | ||
| } | ||
| } | ||
| } else { | ||
| channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry) | ||
| if err != nil { | ||
| return nil, group, err | ||
| } | ||
| } | ||
| return channel, selectGroup, nil | ||
| } |
There was a problem hiding this comment.
Fix logger ctx type, format usage, and return errors when no channel found.
logger.LogDebugexpectscontext.Context, not*gin.Context. Usec.Request.Context().- Provide format verbs; args are currently ignored.
- Return a non-nil error when no channel is found (both auto and direct paths).
- Optionally log per-iteration errors.
Apply:
@@
-import (
- "errors"
+import (
+ "errors"
+ "fmt"
@@
func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
@@
if group == "auto" {
if len(setting.GetAutoGroups()) == 0 {
return nil, selectGroup, errors.New("auto groups is not enabled")
}
- for _, autoGroup := range GetUserAutoGroup(userGroup) {
- logger.LogDebug(c, "Auto selecting group:", autoGroup)
- channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
+ for _, autoGroup := range GetUserAutoGroup(userGroup) {
+ logger.LogDebug(c.Request.Context(), "Auto selecting group: %s", autoGroup)
+ channel, err = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
+ if err != nil {
+ logger.LogDebug(c.Request.Context(), "Auto group %s selection error: %v", autoGroup, err)
+ }
if channel == nil {
continue
} else {
c.Set("auto_group", autoGroup)
selectGroup = autoGroup
- logger.LogDebug(c, "Auto selected group:", autoGroup)
+ logger.LogDebug(c.Request.Context(), "Auto selected group: %s", autoGroup)
break
}
}
+ if channel == nil {
+ return nil, selectGroup, fmt.Errorf("no available channel found in auto groups for model %s", modelName)
+ }
} else {
channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
if err != nil {
return nil, group, err
}
+ if channel == nil {
+ return nil, group, fmt.Errorf("no available channel found in group %s for model %s", group, modelName)
+ }
}
return channel, selectGroup, nil
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) { | |
| var channel *model.Channel | |
| var err error | |
| selectGroup := group | |
| userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) | |
| if group == "auto" { | |
| if len(setting.GetAutoGroups()) == 0 { | |
| return nil, selectGroup, errors.New("auto groups is not enabled") | |
| } | |
| for _, autoGroup := range GetUserAutoGroup(userGroup) { | |
| logger.LogDebug(c, "Auto selecting group:", autoGroup) | |
| channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry) | |
| if channel == nil { | |
| continue | |
| } else { | |
| c.Set("auto_group", autoGroup) | |
| selectGroup = autoGroup | |
| logger.LogDebug(c, "Auto selected group:", autoGroup) | |
| break | |
| } | |
| } | |
| } else { | |
| channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry) | |
| if err != nil { | |
| return nil, group, err | |
| } | |
| } | |
| return channel, selectGroup, nil | |
| } | |
| func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) { | |
| var channel *model.Channel | |
| var err error | |
| selectGroup := group | |
| userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) | |
| if group == "auto" { | |
| if len(setting.GetAutoGroups()) == 0 { | |
| return nil, selectGroup, errors.New("auto groups is not enabled") | |
| } | |
| for _, autoGroup := range GetUserAutoGroup(userGroup) { | |
| logger.LogDebug(c.Request.Context(), "Auto selecting group: %s", autoGroup) | |
| channel, err = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry) | |
| if err != nil { | |
| logger.LogDebug(c.Request.Context(), "Auto group %s selection error: %v", autoGroup, err) | |
| } | |
| if channel == nil { | |
| continue | |
| } else { | |
| c.Set("auto_group", autoGroup) | |
| selectGroup = autoGroup | |
| logger.LogDebug(c.Request.Context(), "Auto selected group: %s", autoGroup) | |
| break | |
| } | |
| } | |
| if channel == nil { | |
| return nil, selectGroup, fmt.Errorf("no available channel found in auto groups for model %s", modelName) | |
| } | |
| } else { | |
| channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry) | |
| if err != nil { | |
| return nil, group, err | |
| } | |
| if channel == nil { | |
| return nil, group, fmt.Errorf("no available channel found in group %s for model %s", group, modelName) | |
| } | |
| } | |
| return channel, selectGroup, nil | |
| } |
Summary by CodeRabbit
New Features
Improvements
UI Changes