Skip to content

Pr/key enabled switch upstream#422

Open
FLCnhzmry wants to merge 2 commits into
tbphp:mainfrom
FLCnhzmry:pr/key-enabled-switch-upstream
Open

Pr/key enabled switch upstream#422
FLCnhzmry wants to merge 2 commits into
tbphp:mainfrom
FLCnhzmry:pr/key-enabled-switch-upstream

Conversation

@FLCnhzmry

@FLCnhzmry FLCnhzmry commented Jun 6, 2026

Copy link
Copy Markdown

添加一个针对每个密钥的启用开关,用于控制活动密钥能否进入路由池,同时不改变现有的无效/黑名单状态逻辑。

Summary by CodeRabbit

  • New Features
    • Added an enable/disable toggle switch for each API key, allowing users to control whether keys are active in routing without deletion.
    • Disabled keys are automatically excluded from all routing operations.
    • New UI controls and labels added in English, Japanese, and Chinese.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR implements a complete API key enable/disable feature. Keys can be toggled between enabled and disabled states without deletion, controlling whether they participate in request routing. The implementation spans backend pool synchronization, HTTP API, and frontend UI with multi-language support.

Changes

API Key Enable/Disable Toggle

Layer / File(s) Summary
Data Model Enhancement
internal/models/types.go, web/src/types/models.ts
APIKey gains an Enabled boolean field (default true) persisted by GORM and exposed in JSON/TypeScript interfaces.
Backend Pool Availability Filtering
internal/keypool/provider.go
KeyProvider implements enabled-aware availability: SelectKey parses enabled state from store, handleSuccess and pool loading use isKeyAvailableForPool (checks both active status and enabled), new SetKeyEnabled method updates the database and removes/re-adds keys from the active pool based on eligibility.
Service Layer and HTTP Handler
internal/services/key_service.go, internal/handler/key_handler.go, internal/router/router.go
KeyService initializes enabled=true on key creation and delegates toggling to KeyProvider; new UpdateKeyEnabled handler parses route ID, binds request body, calls service, and returns updated key with error mapping; routes PUT /api/keys/:id/enabled.
Frontend API Client
web/src/api/keys.ts
keysApi.updateKeyEnabled issues PUT request to toggle enabled state with message suppression and returns response containing id, enabled, and status.
Frontend UI with Toggle Switch
web/src/components/keys/KeyTable.vue
Imports NSwitch, tracks toggling state per key, implements updateKeyEnabled with optimistic UI update and rollback logic; template applies is-disabled class and includes switch in status row; responsive layout refactoring and disabled-state opacity styling.
Internationalization
web/src/locales/en-US.ts, web/src/locales/ja-JP.ts, web/src/locales/zh-CN.ts
Six new i18n keys added across three languages: key enabled/disabled labels, routing switch label, enable/disable action text, and routing-disabled short form.

Sequence Diagram

sequenceDiagram
  participant User
  participant KeyTable
  participant keysApi
  participant UpdateKeyEnabled
  participant KeyService
  participant KeyProvider
  participant Redis
  participant Database
  User->>KeyTable: click toggle switch
  KeyTable->>KeyTable: optimistic: set key.enabled
  KeyTable->>keysApi: updateKeyEnabled(id, false)
  keysApi->>UpdateKeyEnabled: PUT /api/keys/:id/enabled {enabled: false}
  UpdateKeyEnabled->>KeyService: SetKeyEnabled(id, false)
  KeyService->>KeyProvider: SetKeyEnabled(id, false)
  KeyProvider->>Database: update key enabled field
  KeyProvider->>Redis: update hash enabled field
  KeyProvider->>Redis: remove from active_keys
  Redis-->>KeyProvider: deletion confirmed
  KeyProvider-->>KeyService: return updated key
  KeyService-->>UpdateKeyEnabled: return updated key
  UpdateKeyEnabled-->>keysApi: {id, enabled: false, status}
  keysApi-->>KeyTable: response
  KeyTable->>KeyTable: show success, trigger sync refresh
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

enhancement

Suggested reviewers

  • tbphp

Poem

🐰 A switch to toggle keys with grace,
No deletion, just a disabled space,
Through three tongues the labels sing,
Pool stays synced with everything! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing required sections from the template, including related issue number, checkboxes for change type, and self-review checklist. Fill in the complete template with issue number (Closes #...), select the change type checkbox, provide detailed description, and complete the self-review checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and uses non-descriptive terminology that doesn't clearly convey the main change to someone scanning history. Use a more descriptive title like 'Add per-key enabled switch for route pool control' to clearly summarize the feature being added.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot requested a review from tbphp June 6, 2026 19:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
web/src/components/keys/KeyTable.vue (1)

342-344: ⚡ Quick win

Consider showing an error message to the user when the toggle fails.

Currently, when the API call fails, the key's enabled state is rolled back (which provides visual feedback), but no error message is shown to the user—only a console log. Consider adding window.$message.error(...) in the catch block to inform the user why the toggle failed, similar to how other operations in this file handle errors.

💡 Suggested improvement
  } catch (error) {
    key.enabled = previousEnabled;
    console.error("Update key enabled failed", error);
+   window.$message.error(t("keys.updateEnabledFailed"));
  } finally {

Note: You'll need to add the corresponding i18n key updateEnabledFailed to the locale files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/keys/KeyTable.vue` around lines 342 - 344, In the catch
block that rolls back the toggle (the block containing "key.enabled =
previousEnabled" and "console.error('Update key enabled failed', error)") add a
user-facing error message via window.$message.error(...) (or the app's message
helper) using the i18n key "updateEnabledFailed" so users see why the toggle
failed; keep the existing rollback logic and console.error, and also add the
"updateEnabledFailed" entry to the locale files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/handler/key_handler.go`:
- Around line 507-509: The UpdateKeyEnabledRequest struct currently uses Enabled
bool which treats a missing field as false; change it to Enabled *bool and
update the UpdateKeyEnabled handler to return a 400 Bad Request when req.Enabled
is nil (reject requests that omit enabled). Also locate the database error
handling in the same handler where it checks for "record not found" via string
matching and replace that with a proper errors.Is(err, gorm.ErrRecordNotFound)
check (or use app_errors.ParseDBError if preferred) to correctly detect wrapped
not-found errors.
- Around line 568-574: Replace the string-based not-found check in
UpdateKeyEnabled's error handling with a proper errors.Is check: after calling
s.KeyService.SetKeyEnabled, detect a missing record by using errors.Is(err,
gorm.ErrRecordNotFound) (and add the missing "errors" import and ensure "gorm"
is imported) and map that case to response.Error(c,
app_errors.ErrResourceNotFound); otherwise continue to call response.Error(c,
app_errors.ParseDBError(err)).

In `@internal/keypool/provider.go`:
- Around line 642-653: The active-list sync currently performs
p.store.HSet(keyHashKey, p.apiKeyToMap(&key)) before removing the key from the
active list, which can leave a disabled key in group:*:active_keys if LRem
fails; change the logic so that on the disable path you call
p.store.LRem(activeKeysListKey, 0, key.ID) first and return on error, then
update the hash with p.store.HSet; conversely, on the enable path update the
hash via p.store.HSet(...) first and only then call
p.store.LPush(activeKeysListKey, key.ID) (using isKeyAvailableForPool to decide
the push) while preserving the existing error messages and returns for
p.store.LRem, p.store.HSet and p.store.LPush.

---

Nitpick comments:
In `@web/src/components/keys/KeyTable.vue`:
- Around line 342-344: In the catch block that rolls back the toggle (the block
containing "key.enabled = previousEnabled" and "console.error('Update key
enabled failed', error)") add a user-facing error message via
window.$message.error(...) (or the app's message helper) using the i18n key
"updateEnabledFailed" so users see why the toggle failed; keep the existing
rollback logic and console.error, and also add the "updateEnabledFailed" entry
to the locale files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 267f700a-08a7-4366-89fa-2c56ab9ad5e2

📥 Commits

Reviewing files that changed from the base of the PR and between 5579930 and 15578a2.

📒 Files selected for processing (11)
  • internal/handler/key_handler.go
  • internal/keypool/provider.go
  • internal/models/types.go
  • internal/router/router.go
  • internal/services/key_service.go
  • web/src/api/keys.ts
  • web/src/components/keys/KeyTable.vue
  • web/src/locales/en-US.ts
  • web/src/locales/ja-JP.ts
  • web/src/locales/zh-CN.ts
  • web/src/types/models.ts

Comment on lines +507 to +509
type UpdateKeyEnabledRequest struct {
Enabled bool `json:"enabled"`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"

echo "== Context around UpdateKeyEnabledRequest =="
sed -n '470,650p' "$file" | nl -ba | sed -n '1,220p'

echo
echo "== Exact lines 500-590 (matches requested ranges) =="
sed -n '495,590p' "$file" | nl -ba

echo
echo "== Search for UpdateKeyEnabled and binding/error mapping =="
rg -n "UpdateKeyEnabled|ShouldBindJSON|record not found|strings\.Contains\(err\.Error|errors\.Is" "$file"

Repository: tbphp/gpt-load

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"
if [ ! -f "$file" ]; then
  echo "Missing file: $file" >&2
  exit 1
fi

python3 - <<'PY'
import itertools

path = "internal/handler/key_handler.go"
start, end = 470, 650
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:5d}: {line.rstrip()}")
PY

echo
echo "== Search: UpdateKeyEnabled binding and error mapping =="
rg -n "UpdateKeyEnabledRequest|type UpdateKeyEnabledRequest|UpdateKeyEnabled\(|ShouldBindJSON|record not found|strings\.Contains\(|errors\.Is" "$file"

Repository: tbphp/gpt-load

Length of output: 5380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "SetKeyEnabled\\(" -S .
rg -n "type .*KeyService|KeyService" -S internal .
rg -n "record not found" -S .
rg -n "ParseDBError\\(" -S internal .

Repository: tbphp/gpt-load

Length of output: 16179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show KeyService.SetKeyEnabled implementation
sed -n '230,320p' internal/services/key_service.go | cat

echo
echo "----"

# Show KeyProvider.SetKeyEnabled implementation
sed -n '580,720p' internal/keypool/provider.go | cat

echo
echo "----"

# Show ParseDBError implementation
sed -n '1,140p' internal/errors/errors.go | cat

Repository: tbphp/gpt-load

Length of output: 10432


Reject requests that omit enabled (UpdateKeyEnabled).
Enabled bool treats {} as enabled=false, causing an unintended disable; use *bool and return 400 when enabled is missing. Also replace the brittle strings.Contains(err.Error(), "record not found") check with errors.Is(err, gorm.ErrRecordNotFound) (or rely on app_errors.ParseDBError), since the provider wraps underlying GORM errors with %w.

Suggested fix
 type UpdateKeyEnabledRequest struct {
-	Enabled bool `json:"enabled"`
+	Enabled *bool `json:"enabled"`
 }
@@
 	var req UpdateKeyEnabledRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
 		return
 	}
+	if req.Enabled == nil {
+		response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "enabled is required"))
+		return
+	}
 
-	key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
+	key, err := s.KeyService.SetKeyEnabled(uint(keyID), *req.Enabled)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/key_handler.go` around lines 507 - 509, The
UpdateKeyEnabledRequest struct currently uses Enabled bool which treats a
missing field as false; change it to Enabled *bool and update the
UpdateKeyEnabled handler to return a 400 Bad Request when req.Enabled is nil
(reject requests that omit enabled). Also locate the database error handling in
the same handler where it checks for "record not found" via string matching and
replace that with a proper errors.Is(err, gorm.ErrRecordNotFound) check (or use
app_errors.ParseDBError if preferred) to correctly detect wrapped not-found
errors.

Comment on lines +568 to +574
key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
if err != nil {
if strings.Contains(err.Error(), "record not found") {
response.Error(c, app_errors.ErrResourceNotFound)
} else {
response.Error(c, app_errors.ParseDBError(err))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print SetKeyEnabled implementation in internal/keypool/provider.go
file="internal/keypool/provider.go"
if [ -f "$file" ]; then
  echo "== $file =="
  # Print around function definition
  line=$(rg -n "func \\(p \\*KeyProvider\\) SetKeyEnabled\\b" "$file" | head -n1 | cut -d: -f1)
  if [ -n "${line:-}" ]; then
    start=$((line-30))
    end=$((line+120))
    sed -n "${start},${end}p" "$file" | nl -ba | sed -n '1,200p'
  else
    echo "SetKeyEnabled not found in $file"
  fi
else
  echo "File not found: $file"
fi

# Also inspect internal/services/key_service.go just in case it's implemented there
file2="internal/services/key_service.go"
if [ -f "$file2" ]; then
  echo "== $file2 =="
  line=$(rg -n "func \\(s \\*KeyService\\) SetKeyEnabled\\b" "$file2" | head -n1 | cut -d: -f1)
  if [ -n "${line:-}" ]; then
    start=$((line-20))
    end=$((line+80))
    sed -n "${start},${end}p" "$file2" | nl -ba | sed -n '1,200p'
  else
    echo "SetKeyEnabled not found in $file2"
  fi
else
  echo "File not found: $file2"
fi

Repository: tbphp/gpt-load

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# helper to print with line numbers (no nl dependency)
print_with_numbers() {
  local f="$1"; local start="$2"; local end="$3"
  local i=1
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%5d\t%s\n", NR, $0 }' "$f"
}

file="internal/keypool/provider.go"
echo "== $file =="
if [ -f "$file" ]; then
  line=$(rg -n "func \\(p \\*KeyProvider\\) SetKeyEnabled\\b" "$file" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-30))
    end=$((line+160))
    print_with_numbers "$file" "$start" "$end"
  else
    echo "SetKeyEnabled not found"
  fi
else
  echo "File not found: $file"
fi

file2="internal/services/key_service.go"
echo "== $file2 =="
if [ -f "$file2" ]; then
  line=$(rg -n "func \\(s \\*KeyService\\) SetKeyEnabled\\b" "$file2" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-20))
    end=$((line+120))
    print_with_numbers "$file2" "$start" "$end"
  else
    echo "SetKeyEnabled not found"
  fi
else
  echo "File not found: $file2"
fi

Repository: tbphp/gpt-load

Length of output: 9725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"
echo "== $file (imports) =="
# Print import block (best-effort)
start=$(rg -n "^import \\(" "$file" | head -n1 | cut -d: -f1 || true)
if [ -n "${start:-}" ]; then
  # find closing paren
  end=$(awk -v s="$start" 'NR>=s{ if($0 ~ /^\\)/){ print NR; exit } }' "$file")
  sed -n "${start},${end}p" "$file" | nl -ba | head -n 200
else
  echo "No multi-line import block found"
fi

echo
echo "== $file (around SetKeyEnabled handler error mapping) =="
# Print around provided line numbers
sed -n '540,610p' "$file" | nl -ba | sed -n '1,200p'

Repository: tbphp/gpt-load

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"

echo "== $file (import block) =="
# find import block start
start=$(rg -n "^import \\(" "$file" | head -n1 | cut -d: -f1 || true)
if [ -z "${start:-}" ]; then
  # single-line import fallback
  rg -n "^import " "$file" | head -n 50
else
  end=$(awk -v s="$start" 'NR>=s{ if($0 ~ /^\\)/){ print NR; exit } }' "$file")
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%5d\t%s\n", NR, $0 }' "$file"
fi

echo
echo "== $file (around SetKeyEnabled mapping) =="
awk 'NR>=540 && NR<=610 { printf "%5d\t%s\n", NR, $0 }' "$file"

Repository: tbphp/gpt-load

Length of output: 1544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"
awk 'NR<=140 { printf "%5d\t%s\n", NR, $0 }' "$file" | sed -n '1,140p'

Repository: tbphp/gpt-load

Length of output: 5124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="internal/handler/key_handler.go"
echo "== search for errors import/usages =="
rg -n "\"errors\"" "$file" || true
rg -n "\berrors\." "$file" || true

Repository: tbphp/gpt-load

Length of output: 96


Use errors.Is for the not-found mapping.

UpdateKeyEnabled maps not-found by checking strings.Contains(err.Error(), "record not found"), but KeyProvider.SetKeyEnabled wraps the underlying GORM error with %w, so a stable check is errors.Is(err, gorm.ErrRecordNotFound) (and add the missing errors import).

Suggested fix
 import (
+	"errors"
 	"fmt"
 	app_errors "gpt-load/internal/errors"
@@
 	key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
 	if err != nil {
-		if strings.Contains(err.Error(), "record not found") {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			response.Error(c, app_errors.ErrResourceNotFound)
 		} else {
 			response.Error(c, app_errors.ParseDBError(err))
 		}
📝 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
key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
if err != nil {
if strings.Contains(err.Error(), "record not found") {
response.Error(c, app_errors.ErrResourceNotFound)
} else {
response.Error(c, app_errors.ParseDBError(err))
}
key, err := s.KeyService.SetKeyEnabled(uint(keyID), req.Enabled)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, app_errors.ErrResourceNotFound)
} else {
response.Error(c, app_errors.ParseDBError(err))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/key_handler.go` around lines 568 - 574, Replace the
string-based not-found check in UpdateKeyEnabled's error handling with a proper
errors.Is check: after calling s.KeyService.SetKeyEnabled, detect a missing
record by using errors.Is(err, gorm.ErrRecordNotFound) (and add the missing
"errors" import and ensure "gorm" is imported) and map that case to
response.Error(c, app_errors.ErrResourceNotFound); otherwise continue to call
response.Error(c, app_errors.ParseDBError(err)).

Comment on lines +642 to +653
if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil {
return fmt.Errorf("failed to update key enabled flag in store: %w", err)
}

if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
return fmt.Errorf("failed to LRem key from active list: %w", err)
}

if isKeyAvailableForPool(&key) {
if err := p.store.LPush(activeKeysListKey, key.ID); err != nil {
return fmt.Errorf("failed to LPush key to active list: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when syncing the active pool on disable.

This updates the hash before removing the key from group:*:active_keys. If HSet succeeds and LRem fails, this call returns an error but the list still contains the key, so SelectKey can keep routing a supposedly disabled key. Remove from the active list first on the disable path, and only LPush after the hash says enabled on the enable path.

Suggested direction
-		if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil {
-			return fmt.Errorf("failed to update key enabled flag in store: %w", err)
-		}
-
-		if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
-			return fmt.Errorf("failed to LRem key from active list: %w", err)
-		}
-
-		if isKeyAvailableForPool(&key) {
+		if !enabled {
+			if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
+				return fmt.Errorf("failed to LRem key from active list: %w", err)
+			}
+		}
+
+		if err := p.store.HSet(keyHashKey, p.apiKeyToMap(&key)); err != nil {
+			return fmt.Errorf("failed to update key enabled flag in store: %w", err)
+		}
+
+		if enabled && isKeyAvailableForPool(&key) {
 			if err := p.store.LPush(activeKeysListKey, key.ID); err != nil {
 				return fmt.Errorf("failed to LPush key to active list: %w", err)
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/keypool/provider.go` around lines 642 - 653, The active-list sync
currently performs p.store.HSet(keyHashKey, p.apiKeyToMap(&key)) before removing
the key from the active list, which can leave a disabled key in
group:*:active_keys if LRem fails; change the logic so that on the disable path
you call p.store.LRem(activeKeysListKey, 0, key.ID) first and return on error,
then update the hash with p.store.HSet; conversely, on the enable path update
the hash via p.store.HSet(...) first and only then call
p.store.LPush(activeKeysListKey, key.ID) (using isKeyAvailableForPool to decide
the push) while preserving the existing error messages and returns for
p.store.LRem, p.store.HSet and p.store.LPush.

@tbphp

tbphp commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Codex Review(通过 Codex 的 review 生成)

结论:这个 PR 的功能是给每个 API key 增加独立的 enabled 路由开关。enabled=false 时,key 应从 group:*:active_keys 路由池中排除;它不替代现有 status=active/invalid 和 blacklist 逻辑,而是提供一个手动临时下线 key 的运维能力。

合并建议:功能有实际价值,适合需要临时停用单个 key、保留 key 记录、不改变 invalid/blacklist 状态的场景。但当前补丁不建议直接合并,至少需要先修复下面的问题并跑完质量门禁。

必须修复:

  1. internal/keypool/provider.gohandleSuccess 在事务外从缓存读取 isEnabled,并在事务内仍用旧值判断是否 LPush。如果用户并发禁用了同一个 invalid key,而成功校验处理随后拿到锁继续执行,就可能把已经 enabled=false 的 key 重新放回 active_keys,导致已禁用 key 仍被路由。建议在锁内使用最新的 key.Enabled 判断是否回池,例如更新状态后按 key.Enabled && status == active 决定是否 LPush

  2. web/src/api/keys.ts:新增的 updateKeyEnabled 调用不符合当前 web/.prettierrc 格式,npm run format:check 会失败。需要按 Prettier 格式化后再提交。

建议一并修复:

  1. internal/handler/key_handler.goUpdateKeyEnabledRequest.Enabled 目前是 bool,请求体缺失 enabled 时会被零值解析成 false,可能造成误禁用。建议改成 *bool,并在缺失时返回 400。

  2. internal/handler/key_handler.go:not found 判断不应依赖字符串 record not found,建议使用 errors.Is(err, gorm.ErrRecordNotFound) 或统一交给现有 DB error parser。

  3. internal/keypool/provider.goSetKeyEnabled 的 DB/store 同步顺序建议按启用/禁用路径分别处理,降低中间失败时 store 与路由池不一致的窗口。禁用时优先从 active_keys 移除;启用时先更新 hash,再按 isKeyAvailableForPool 回池。

  4. web/src/components/keys/KeyTable.vue:前端 toggle 失败时当前只回滚和 console.error,用户无明确错误反馈。建议增加 i18n 错误提示,例如 keys.updateEnabledFailed

验证建议:

  • Backend:go build -o gpt-load
  • Frontend:在 web/ 下运行 npm run lint:check && npm run format:check && npm run type-check

整体判断:修复 P2 并发语义和 P3 格式问题后,这个功能可以考虑合并;如果项目暂时没有“单 key 临时下线”的明确需求,也可以延后合并。

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