Skip to content

Commit d771d19

Browse files
committed
feat(accounts): organize bulk account actions
1 parent 8fa4087 commit d771d19

6 files changed

Lines changed: 349 additions & 57 deletions

File tree

admin/batch_test_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package admin
33
import (
44
"net/http"
55
"testing"
6+
7+
"github.com/codex2api/auth"
68
)
79

810
func TestShouldMarkBatchTestAccountError(t *testing.T) {
@@ -52,3 +54,35 @@ func TestShouldMarkBatchTestAccountError(t *testing.T) {
5254
})
5355
}
5456
}
57+
58+
func TestResolveBatchTestAccountsDefaultsToAllAccounts(t *testing.T) {
59+
store := auth.NewStore(nil, nil, nil)
60+
store.AddAccount(&auth.Account{DBID: 1, AccessToken: "token-1", Status: auth.StatusReady})
61+
store.AddAccount(&auth.Account{DBID: 2, AccessToken: "token-2", Status: auth.StatusReady})
62+
63+
accounts, missing := resolveBatchTestAccounts(store, nil)
64+
if missing != 0 {
65+
t.Fatalf("missing = %d, want 0", missing)
66+
}
67+
if len(accounts) != 2 {
68+
t.Fatalf("len(accounts) = %d, want 2", len(accounts))
69+
}
70+
}
71+
72+
func TestResolveBatchTestAccountsUsesSelectedIDs(t *testing.T) {
73+
store := auth.NewStore(nil, nil, nil)
74+
store.AddAccount(&auth.Account{DBID: 1, AccessToken: "token-1", Status: auth.StatusReady})
75+
store.AddAccount(&auth.Account{DBID: 2, AccessToken: "token-2", Status: auth.StatusReady})
76+
77+
ids := []int64{2, 99, 2, 1}
78+
accounts, missing := resolveBatchTestAccounts(store, &ids)
79+
if missing != 1 {
80+
t.Fatalf("missing = %d, want 1", missing)
81+
}
82+
if len(accounts) != 2 {
83+
t.Fatalf("len(accounts) = %d, want 2", len(accounts))
84+
}
85+
if accounts[0].DBID != 2 || accounts[1].DBID != 1 {
86+
t.Fatalf("account order = [%d, %d], want [2, 1]", accounts[0].DBID, accounts[1].DBID)
87+
}
88+
}

admin/test_connection.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,53 @@ func (h *Handler) connectionTestModel(ctx context.Context) string {
378378
return "gpt-5.4"
379379
}
380380

381-
// BatchTest 批量测试所有账号连接
381+
type batchTestRequest struct {
382+
IDs *[]int64 `json:"ids"`
383+
}
384+
385+
func resolveBatchTestAccounts(store *auth.Store, ids *[]int64) ([]*auth.Account, int) {
386+
if store == nil {
387+
return nil, 0
388+
}
389+
if ids == nil {
390+
return store.Accounts(), 0
391+
}
392+
393+
accounts := make([]*auth.Account, 0, len(*ids))
394+
missing := 0
395+
seen := make(map[int64]struct{}, len(*ids))
396+
for _, id := range *ids {
397+
if _, ok := seen[id]; ok {
398+
continue
399+
}
400+
seen[id] = struct{}{}
401+
acc := store.FindByID(id)
402+
if acc == nil {
403+
missing++
404+
continue
405+
}
406+
accounts = append(accounts, acc)
407+
}
408+
return accounts, missing
409+
}
410+
411+
// BatchTest 批量测试账号连接;未传 ids 时测试所有账号,传 ids 时仅测试指定账号。
382412
// POST /api/admin/accounts/batch-test
383413
func (h *Handler) BatchTest(c *gin.Context) {
384-
accounts := h.store.Accounts()
385-
if len(accounts) == 0 {
414+
var req batchTestRequest
415+
if c.Request.Body != nil && c.Request.ContentLength != 0 {
416+
if err := c.ShouldBindJSON(&req); err != nil {
417+
writeError(c, http.StatusBadRequest, "请求格式错误")
418+
return
419+
}
420+
}
421+
if req.IDs != nil && len(*req.IDs) == 0 {
422+
writeError(c, http.StatusBadRequest, "请提供要测试的账号 ID 列表")
423+
return
424+
}
425+
426+
accounts, missingCount := resolveBatchTestAccounts(h.store, req.IDs)
427+
if len(accounts) == 0 && missingCount == 0 {
386428
c.JSON(http.StatusOK, gin.H{"total": 0, "success": 0, "failed": 0, "banned": 0, "rate_limited": 0})
387429
return
388430
}
@@ -393,7 +435,7 @@ func (h *Handler) BatchTest(c *gin.Context) {
393435

394436
var (
395437
successCount int64
396-
failedCount int64
438+
failedCount = int64(missingCount)
397439
bannedCount int64
398440
rateLimitCount int64
399441
wg sync.WaitGroup
@@ -457,7 +499,7 @@ func (h *Handler) BatchTest(c *gin.Context) {
457499
wg.Wait()
458500

459501
c.JSON(http.StatusOK, gin.H{
460-
"total": len(accounts),
502+
"total": len(accounts) + missingCount,
461503
"success": successCount,
462504
"failed": failedCount,
463505
"banned": bannedCount,

frontend/src/api.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,11 @@ export const api = {
274274
request<PromptFilterRulesResponse>('/prompt-filter/rules'),
275275
getModels: () => request<ModelsResponse>('/models'),
276276
syncModels: () => request<ModelSyncResponse>('/models/sync', { method: 'POST' }),
277-
batchTestAccounts: () =>
278-
request<{ total: number; success: number; failed: number; banned: number; rate_limited: number }>('/accounts/batch-test', { method: 'POST' }),
277+
batchTestAccounts: (ids?: number[]) =>
278+
request<{ total: number; success: number; failed: number; banned: number; rate_limited: number }>('/accounts/batch-test', {
279+
method: 'POST',
280+
body: ids ? JSON.stringify({ ids }) : undefined,
281+
}),
279282
cleanBanned: () =>
280283
request<{ message: string; cleaned: number }>('/accounts/clean-banned', { method: 'POST' }),
281284
cleanRateLimited: () =>

frontend/src/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@
146146
"batchTest": "Batch Test",
147147
"batchTesting": "Testing…",
148148
"batchRefresh": "Batch Refresh",
149+
"refreshTokens": "Refresh Tokens",
149150
"batchDelete": "Batch Delete",
151+
"maintenanceActions": "Maintenance",
152+
"cleanupActions": "Cleanup",
153+
"dataActions": "Import / Export",
150154
"cleanBanned": "Clear Banned",
151155
"cleanRateLimited": "Clear Rate-Limited",
152156
"cleanError": "Clear Errors",
@@ -384,6 +388,11 @@
384388
"lockFailed": "Operation failed: {{error}}",
385389
"lockHint": "Locked accounts are skipped by auto-cleanup",
386390
"unlockHint": "Unlocked accounts will be subject to auto-cleanup",
391+
"lockSubscriptionAccounts": "Lock Paid Accounts",
392+
"lockingSubscriptionAccounts": "Locking…",
393+
"lockSubscriptionAccountsHint": "Lock currently unlocked paid accounts in the account pool ({{count}} pending)",
394+
"lockSubscriptionAccountsDone": "Paid account lock complete: {{success}} succeeded, {{fail}} failed",
395+
"noSubscriptionAccountsToLock": "No paid accounts need locking",
387396
"filterDisabled": "Disabled",
388397
"filterLocked": "Locked",
389398
"batchEnableDone": "Batch enable complete: {{success}} succeeded, {{fail}} failed",

frontend/src/locales/zh.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@
146146
"batchTest": "批量测试",
147147
"batchTesting": "测试中…",
148148
"batchRefresh": "批量刷新",
149+
"refreshTokens": "刷新令牌",
149150
"batchDelete": "批量删除",
151+
"maintenanceActions": "账号维护",
152+
"cleanupActions": "清理",
153+
"dataActions": "导入导出",
150154
"cleanBanned": "清理封禁",
151155
"cleanRateLimited": "清理限流",
152156
"cleanError": "清理错误",
@@ -384,6 +388,11 @@
384388
"lockFailed": "操作失败:{{error}}",
385389
"lockHint": "锁定后,自动清理将跳过此账号",
386390
"unlockHint": "解锁后,账号将正常参与自动清理",
391+
"lockSubscriptionAccounts": "一键锁定订阅账号",
392+
"lockingSubscriptionAccounts": "锁定中…",
393+
"lockSubscriptionAccountsHint": "锁定当前账号池中未锁定的订阅账号({{count}} 个待锁定)",
394+
"lockSubscriptionAccountsDone": "订阅账号锁定完成:成功 {{success}},失败 {{fail}}",
395+
"noSubscriptionAccountsToLock": "没有需要锁定的订阅账号",
387396
"filterDisabled": "已禁用",
388397
"filterLocked": "已锁定",
389398
"batchEnableDone": "批量启用完成:成功 {{success}},失败 {{fail}}",

0 commit comments

Comments
 (0)