Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions internal/handler/key_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,11 @@ type UpdateKeyNotesRequest struct {
Notes string `json:"notes"`
}

// UpdateKeyEnabledRequest defines the payload for toggling a key's manual enabled switch.
type UpdateKeyEnabledRequest struct {
Enabled bool `json:"enabled"`
}
Comment on lines +507 to +509

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.


// UpdateKeyNotes handles updating the notes of a specific API key.
func (s *Server) UpdateKeyNotes(c *gin.Context) {
keyIDStr := c.Param("id")
Expand Down Expand Up @@ -544,3 +549,35 @@ func (s *Server) UpdateKeyNotes(c *gin.Context) {

response.Success(c, nil)
}

// UpdateKeyEnabled handles toggling whether a key can enter the active routing pool.
func (s *Server) UpdateKeyEnabled(c *gin.Context) {
keyIDStr := c.Param("id")
keyID, err := strconv.Atoi(keyIDStr)
if err != nil || keyID <= 0 {
response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "invalid key ID format"))
return
}

var req UpdateKeyEnabledRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
return
}

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))
}
Comment on lines +568 to +574

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

return
}

response.Success(c, gin.H{
"id": key.ID,
"enabled": key.Enabled,
"status": key.Status,
})
}
72 changes: 65 additions & 7 deletions internal/keypool/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (p *KeyProvider) SelectKey(groupID uint) (*models.APIKey, error) {
ID: uint(keyID),
KeyValue: decryptedKeyValue,
Status: keyDetails["status"],
Enabled: parseEnabled(keyDetails),
FailureCount: failureCount,
GroupID: groupID,
CreatedAt: time.Unix(createdAt, 0),
Expand Down Expand Up @@ -147,6 +148,7 @@ func (p *KeyProvider) handleSuccess(keyID uint, keyHashKey, activeKeysListKey st

failureCount, _ := strconv.ParseInt(keyDetails["failure_count"], 10, 64)
isActive := keyDetails["status"] == models.KeyStatusActive
isEnabled := parseEnabled(keyDetails)

if failureCount == 0 && isActive {
return nil
Expand All @@ -171,7 +173,7 @@ func (p *KeyProvider) handleSuccess(keyID uint, keyHashKey, activeKeysListKey st
return fmt.Errorf("failed to update key details in store: %w", err)
}

if !isActive {
if !isActive && isEnabled {
logrus.WithField("keyID", keyID).Debug("Key has recovered and is being restored to active pool.")
if err := p.store.LRem(activeKeysListKey, 0, keyID); err != nil {
return fmt.Errorf("failed to LRem key before LPush on recovery: %w", err)
Expand Down Expand Up @@ -265,7 +267,7 @@ func (p *KeyProvider) LoadKeysFromDB() error {
}
}

if key.Status == models.KeyStatusActive {
if isKeyAvailableForPool(key) {
allActiveKeyIDs[key.GroupID] = append(allActiveKeyIDs[key.GroupID], key.ID)
}
}
Expand Down Expand Up @@ -560,7 +562,7 @@ func (p *KeyProvider) addKeyToStore(key *models.APIKey) error {
}

// 2. If active, add to the active LIST
if key.Status == models.KeyStatusActive {
if isKeyAvailableForPool(key) {
activeKeysListKey := fmt.Sprintf("group:%d:active_keys", key.GroupID)
if err := p.store.LRem(activeKeysListKey, 0, key.ID); err != nil {
return fmt.Errorf("failed to LRem key %d before LPush for group %d: %w", key.ID, key.GroupID, err)
Expand Down Expand Up @@ -601,19 +603,65 @@ func (p *KeyProvider) addKeysToCacheBatch(groupID uint, keys []models.APIKey) er

// 2. 收集所有密钥 ID
activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID)
activeKeyIDs := make([]any, len(keys))
activeKeyIDs := make([]any, 0, len(keys))
for i := range keys {
activeKeyIDs[i] = keys[i].ID
if isKeyAvailableForPool(&keys[i]) {
activeKeyIDs = append(activeKeyIDs, keys[i].ID)
}
}

// 3. 批量 LPush 活跃密钥
if err := p.store.LPush(activeKeysListKey, activeKeyIDs...); err != nil {
return fmt.Errorf("failed to batch LPush keys to group %d: %w", groupID, err)
if len(activeKeyIDs) > 0 {
if err := p.store.LPush(activeKeysListKey, activeKeyIDs...); err != nil {
return fmt.Errorf("failed to batch LPush keys to group %d: %w", groupID, err)
}
}

return nil
}

// SetKeyEnabled updates a key's manual enabled switch and keeps the active pool in sync.
func (p *KeyProvider) SetKeyEnabled(keyID uint, enabled bool) (*models.APIKey, error) {
var updatedKey models.APIKey

err := p.executeTransactionWithRetry(func(tx *gorm.DB) error {
var key models.APIKey
if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&key, keyID).Error; err != nil {
return fmt.Errorf("failed to lock key %d for enabled update: %w", keyID, err)
}

if err := tx.Model(&key).Update("enabled", enabled).Error; err != nil {
return fmt.Errorf("failed to update key enabled flag in DB: %w", err)
}
key.Enabled = enabled
updatedKey = key

keyHashKey := fmt.Sprintf("key:%d", key.ID)
activeKeysListKey := fmt.Sprintf("group:%d:active_keys", key.GroupID)

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)
}
Comment on lines +642 to +653

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.

}

return nil
})
if err != nil {
return nil, err
}

return &updatedKey, nil
}

// removeKeyFromStore is a helper to remove a single key from the cache.
func (p *KeyProvider) removeKeyFromStore(keyID, groupID uint) error {
activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID)
Expand All @@ -634,12 +682,22 @@ func (p *KeyProvider) apiKeyToMap(key *models.APIKey) map[string]any {
"id": fmt.Sprint(key.ID),
"key_string": key.KeyValue,
"status": key.Status,
"enabled": key.Enabled,
"failure_count": key.FailureCount,
"group_id": key.GroupID,
"created_at": key.CreatedAt.Unix(),
}
}

func isKeyAvailableForPool(key *models.APIKey) bool {
return key.Enabled && key.Status == models.KeyStatusActive
}

func parseEnabled(details map[string]string) bool {
enabled, ok := details["enabled"]
return !ok || enabled == "" || enabled == "true" || enabled == "1"
}

// pluckIDs extracts IDs from a slice of APIKey.
func pluckIDs(keys []models.APIKey) []uint {
ids := make([]uint, len(keys))
Expand Down
1 change: 1 addition & 0 deletions internal/models/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type APIKey struct {
KeyHash string `gorm:"type:varchar(128);index" json:"key_hash"`
GroupID uint `gorm:"not null;index;index:idx_api_keys_group_last_used_id,priority:1" json:"group_id"`
Status string `gorm:"type:varchar(50);not null;default:'active';index" json:"status"`
Enabled bool `gorm:"not null;default:true;index" json:"enabled"`
Notes string `gorm:"type:varchar(255);default:''" json:"notes"`
RequestCount int64 `gorm:"not null;default:0" json:"request_count"`
FailureCount int64 `gorm:"not null;default:0" json:"failure_count"`
Expand Down
1 change: 1 addition & 0 deletions internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
keys.POST("/validate-group", serverHandler.ValidateGroupKeys)
keys.POST("/test-multiple", serverHandler.TestMultipleKeys)
keys.PUT("/:id/notes", serverHandler.UpdateKeyNotes)
keys.PUT("/:id/enabled", serverHandler.UpdateKeyEnabled)
}

// Tasks
Expand Down
6 changes: 6 additions & 0 deletions internal/services/key_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func (s *KeyService) processAndCreateKeys(
KeyValue: encryptedKey,
KeyHash: keyHash,
Status: models.KeyStatusActive,
Enabled: true,
})
}

Expand Down Expand Up @@ -251,6 +252,11 @@ func (s *KeyService) ClearAllKeys(groupID uint) (int64, error) {
return s.KeyProvider.RemoveAllKeys(groupID)
}

// SetKeyEnabled updates whether a key is allowed to participate in request routing.
func (s *KeyService) SetKeyEnabled(keyID uint, enabled bool) (*models.APIKey, error) {
return s.KeyProvider.SetKeyEnabled(keyID, enabled)
}

// DeleteMultipleKeys handles the business logic of deleting keys from a text block.
func (s *KeyService) DeleteMultipleKeys(groupID uint, keysText string) (*DeleteKeysResult, error) {
keysToDelete := s.ParseKeysFromText(keysText)
Expand Down
13 changes: 13 additions & 0 deletions web/src/api/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ export const keysApi = {
await http.put(`/keys/${keyId}/notes`, { notes }, { hideMessage: true });
},

// 更新密钥启用状态
async updateKeyEnabled(
keyId: number,
enabled: boolean
): Promise<Pick<APIKey, "id" | "enabled" | "status">> {
const res = await http.put(
`/keys/${keyId}/enabled`,
{ enabled },
{ hideMessage: true }
);
return res.data;
},

// 测试密钥
async testKeys(
group_id: number,
Expand Down
Loading