Skip to content

feat: add special user usable group setting - #2121

Merged
seefs001 merged 1 commit into
mainfrom
feat/special_group
Oct 29, 2025
Merged

feat: add special user usable group setting#2121
seefs001 merged 1 commit into
mainfrom
feat/special_group

Conversation

@Calcium-Ion

@Calcium-Ion Calcium-Ion commented Oct 28, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added group-specific configuration customization for special usable groups.
  • Improvements

    • Enhanced channel selection algorithm with intelligent weight smoothing for better load distribution.
    • Refined auto group handling and group access validation.
    • Updated error messages for unauthorized group access.
  • UI Changes

    • Added new settings interface for configuring special usable groups.
    • Adjusted token configuration form behavior for auto group selection.

@coderabbitai

ghost commented Oct 28, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This 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

Cohort / File(s) Change Summary
Service layer for groups
service/group.go
New file implementing user group utilities: GetUserUsableGroups (with user-specific overrides from ratio settings), GroupInUserUsableGroups, and GetUserAutoGroup with special group mapping support.
Service layer for channel selection
service/channel_select.go
New exported function CacheGetRandomSatisfiedChannel that handles auto-group selection via service layer, validating and iterating through user auto-groups before falling back to direct group lookup.
Controllers using service layer
controller/group.go, controller/model.go, controller/pricing.go, controller/user.go
Updated imports from setting to service package; calls to group retrieval functions now use service layer instead of setting (e.g., service.GetUserUsableGroups, service.GetUserAutoGroup).
Middleware using service layer
middleware/auth.go, middleware/distributor.go
Replaced setting package calls with service equivalents for group validation; auth.go updated error message; distributor.go refactored to use usingGroup variable throughout with service-layer group checks.
Relay controller integration
controller/relay.go
Updated to call CacheGetRandomSatisfiedChannel from service layer instead of model layer.
Channel selection refactor
model/channel_cache.go
Renamed and promoted internal function to public GetRandomSatisfiedChannel; removed gin-context-based CacheGetRandomSatisfiedChannel; added weight-smoothing logic with shouldSmooth and sumWeight to adjust smoothingFactor dynamically (1 or 100) based on average channel weight thresholds.
Model ability refactor
model/ability.go
Renamed exported function from GetRandomSatisfiedChannel to GetChannel (same signature).
Logger enhancement
logger/logger.go
Updated LogDebug signature to accept variadic parameters args ...any and format message with fmt.Sprintf(msg, args...) before logging.
Debug logging in relay
relay/helper/price.go
Replaced common.DebugEnabled print block with logger.LogDebug call for final group output.
Auto-groups configuration
setting/auto_group.go
Made AutoGroups variable private (autoGroups); added getter GetAutoGroups() []string; replaced json.Unmarshal/Marshal with common.Unmarshal/Marshal.
Group ratio settings
setting/ratio_setting/group_ratio.go
New exported type GroupRatioSetting with GroupSpecialUsableGroup field (generic RWMap[string, map[string]string]); added init() function to populate defaults; added GetGroupRatioSetting() accessor.
User usable groups migration
setting/user_usable_group.go
Removed exported functions GetUserUsableGroups and GroupInUserUsableGroups (now in service layer).
Generic thread-safe map
types/rw_map.go
New generic type RWMap[K comparable, V any] with sync.RWMutex protection; methods include Get, Set, AddAll, Clear, ReadAll, Len, UnmarshalJSON, MarshalJSON, and LoadFromJsonString.
Config serialization
setting/config/config.go
Added pointer field handling in configToMap and updateConfigFromMap to serialize/deserialize pointer-contained values; exposed ConfigToMap and UpdateConfigFromMap as public functions.
UI ratio settings
web/src/pages/Setting/Ratio/GroupRatioSettings.jsx
Added new TextArea field for group_ratio_setting.group_special_usable_group with JSON validation.
UI ratio input
web/src/components/settings/RatioSetting.jsx
Added input key group_ratio_setting.group_special_usable_group; relaxed condition in getOptions to apply JSON pretty-printing to all JSON-like values (not just specific keys).
Token modal UI
web/src/components/table/tokens/modals/EditTokenModal.jsx
Removed auto option insertion and commented out automatic form value setting for default_use_auto_group logic.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • Weight-smoothing logic in model/channel_cache.go: New conditional smoothing behavior based on average channel weight thresholds; verify the threshold comparison logic and impact on channel selection distribution.
  • User-specific group overrides in service/group.go: Special prefix handling (-:group, +:group, plain group) for ratio-setting overrides; ensure override merging logic is correct and edge cases (duplicate keys, missing groups) are handled.
  • RWMap generic type in types/rw_map.go: New concurrent-safe map wrapper; verify JSON marshalling/unmarshalling symmetry and proper lock usage in all operations.
  • Public API migrations: Functions moved from setting to service layer (especially GetUserUsableGroups and GroupInUserUsableGroups); ensure all callers across controllers and middleware are correctly updated.
  • Channel selection refactor: Renamed functions and signature changes (GetRandomSatisfiedChannelGetChannel in ability.go; removal of gin-context-based CacheGetRandomSatisfiedChannel); verify all call sites are updated consistently.

Possibly related PRs

  • feat: 分组特殊倍率 #1207: Both PRs modify the group-ratio feature surface (adding/using per-(user,group) ratios and related getters like GetGroupGroupRatio/GetGroupRatioSetting) and introduce/extend group-specific configuration logic.
  • feat: auto分组 #1239: Both PRs modify auto-group and channel-selection logic, including introduction/alteration of CacheGetRandomSatisfiedChannel (context/selectGroup behavior), GetUserUsableGroups/auto-group handling, and related callers across the codebase.

Suggested reviewers

  • creamlike1024

Poem

🐰 The rabbit hops with glee,
Service layers flowing free!
RWMaps keep threads aligned,
Groups and channels, smoothly refined,
A refactor done with care,
Better architecture—quite rare! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: add special user usable group setting" directly corresponds to the primary objective of the changeset. The PR introduces a new GroupSpecialUsableGroup configuration field in the ratio settings (via setting/ratio_setting/group_ratio.go), adds corresponding service layer functions in service/group.go to apply user-specific group overrides, and provides UI controls in the web frontend to configure these special groups. While the PR includes significant supporting changes (service layer refactoring, smoothing logic, and API improvements), the title appropriately focuses on the main user-facing feature. The title is concise, specific, and follows conventional commit conventions, making it clear to teammates that this PR adds user-customizable group settings.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/special_group

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 copy

autoGroups 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 helpers

Current 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.LogDebug here, but lines 122-124 still use println for 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: Guard startsWith for non-string values to avoid runtime errors.

item.value may 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

📥 Commits

Reviewing files that changed from the base of the PR and between 158b46e and 1ded197.

⛔ Files ignored due to path filters (1)
  • web/bun.lock is 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 good

Centralizing group logic under service is the right direction. No issues.


151-159: Auto-group resolution correctly uses service — verified across all controllers

All controller call-sites properly reference service.GetUserAutoGroup for auto-group resolution:

  • controller/model.go:152
  • controller/pricing.go:48

No lingering direct setting.AutoGroups usages 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 appropriate

Letting 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.CacheGetRandomSatisfiedChannel properly 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 setting package to service package 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.GetUserUsableGroups is 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 userGroup to usingGroup better 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 GetRandomSatisfiedChannel was moved to ability.go and renamed to GetChannel, a wrapper function with the original name was intentionally retained in channel_cache.go. This wrapper provides the caching layer and delegates to GetChannel when cache is disabled. All callers in service/channel_select.go are using the wrapper function (model.GetRandomSatisfiedChannel), which correctly invokes the renamed GetChannel function, maintaining proper functionality and caching behavior.

controller/group.go (1)

7-7: Service-layer migration looks good.

Using service.GetUserUsableGroups and 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.

Comment thread logger/logger.go
Comment on lines +69 to 71
func LogDebug(ctx context.Context, msg string, args ...any) {
msg = fmt.Sprintf(msg, args...)
if common.DebugEnabled {

ghost Oct 28, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 %v but passes autoGroup
  • 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.

Comment thread model/channel_cache.go
Comment on lines +145 to 176
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

ghost Oct 28, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.go

Length 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 || true

Length 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 || true

Length 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 || true

Length 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 || true

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Check the full context of the GetRandomSatisfiedChannel function definition
sed -n '96,180p' model/channel_cache.go

Length 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 -20

Length 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 || true

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Search for the fallback GetChannel call mentioned in the scratchpad
rg -nA 15 'GetChannel.*\(' model/channel_cache.go

Length 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.go

Length 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 -120

Length 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/ || true

Length 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 || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Directly extract and display the specific lines mentioned
sed -n '145,176p' model/channel_cache.go

Length 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.go

Length of output: 1962


🏁 Script executed:

#!/bin/bash
# Find all function definitions to understand file structure
rg -n '^func' model/channel_cache.go

Length 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' || true

Length 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 || true

Length 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.go

Length 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 -20

Length 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 || true

Length 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.go

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Get the database selection code path
rg -nA 30 'MemoryCacheEnabled' model/channel_cache.go | head -60

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Simple: just cat the whole file to understand structure
head -250 model/channel_cache.go | tail -150

Length of output: 3925


🏁 Script executed:

#!/bin/bash
# Direct extraction
awk 'NR>=140 && NR<=180' model/channel_cache.go

Length of output: 1162


🏁 Script executed:

#!/bin/bash
# Look at wider context around the indicated lines
head -200 model/channel_cache.go | tail -80

Length of output: 2191


🏁 Script executed:

#!/bin/bash
# Most direct approach - get lines 145-176 and surrounding lines
sed -n '140,185p' model/channel_cache.go

Length of output: 1281


🏁 Script executed:

#!/bin/bash
# Count total lines in file to ensure line numbers are valid
wc -l model/channel_cache.go

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Now get exact range with context
sed -n '136,190p' model/channel_cache.go

Length of output: 1474


🏁 Script executed:

#!/bin/bash
# Show from function definition to beyond the target lines
sed -n '96,180p' model/channel_cache.go

Length 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 -150

Length of output: 2591


🏁 Script executed:

#!/bin/bash
# Simple dump of critical section
sed -n '95,200p' model/channel_cache.go

Length of output: 2973


🏁 Script executed:

#!/bin/bash
# Most basic - just show lines 145-176
sed -n '145,176p' model/channel_cache.go

Length 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.

Suggested change
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.

Comment thread service/channel_select.go
Comment on lines +14 to +42
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
}

ghost Oct 28, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix logger ctx type, format usage, and return errors when no channel found.

  • logger.LogDebug expects context.Context, not *gin.Context. Use c.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.

Suggested change
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
}

@Calcium-Ion Calcium-Ion linked an issue Oct 28, 2025 that may be closed by this pull request
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

关于用户分组的疑问

2 participants