From 807d0018ef72251c15d1a0e4234eab2f2612ff6a Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Wed, 11 Feb 2026 22:17:15 +0800 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=9B=B4=E6=96=B0/=E6=89=B9=E9=87=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20API=20=E6=96=87=E6=A1=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20API=20=E6=93=8D=E4=BD=9C=E6=B5=81=E7=A8=8B=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 1.11 更新单个账号(PUT)和 1.12 批量更新账号(POST)接口文档 - 添加 API 操作流程规范:遇到新需求先探索接口、更新文档、再执行操作 --- AGENTS.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- CLAUDE.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0202e94f..3fdd88c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -511,6 +511,18 @@ docker stats sub2api ## Admin API 接口文档 +### ⚠️ API 操作流程规范 + +当收到操作正式环境 Web 界面的新需求,但文档中未记录对应 API 接口时,**必须按以下流程执行**: + +1. **探索接口**:通过代码库搜索路由定义(`backend/internal/server/routes/`)、Handler(`backend/internal/handler/admin/`)和请求结构体,确定正确的 API 端点、请求方法、请求体格式 +2. **更新文档**:将新发现的接口补充到本文档的 Admin API 接口文档章节中,包含端点、参数说明和 curl 示例 +3. **执行操作**:根据最新文档中记录的接口完成用户需求 + +> **目的**:避免每次遇到相同需求都重复探索代码库,确保 API 文档持续完善,后续操作可直接查阅文档执行。 + +--- + ### 认证方式 所有 Admin API 通过 `x-api-key` 请求头传递 Admin API Key 认证。 @@ -681,7 +693,70 @@ GET /api/v1/admin/accounts/:id/usage curl -s "${BASE}/api/v1/admin/accounts/1/usage" -H "x-api-key: ${KEY}" ``` -#### 1.11 批量测试账号(脚本) +#### 1.11 更新单个账号 + +``` +PUT /api/v1/admin/accounts/:id +``` + +**请求体**(JSON,所有字段均为可选,仅传需要更新的字段): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 账号名称 | +| `notes` | *string | 备注 | +| `type` | string | 类型:`oauth` / `setup-token` / `apikey` / `upstream` | +| `credentials` | object | 凭证信息 | +| `extra` | object | 额外配置 | +| `proxy_id` | *int64 | 代理 ID | +| `concurrency` | *int | 并发数 | +| `priority` | *int | 优先级(默认 50) | +| `rate_multiplier` | *float64 | 速率倍数 | +| `status` | string | 状态:`active` / `inactive` | +| `group_ids` | *[]int64 | 分组 ID 列表 | +| `expires_at` | *int64 | 过期时间戳 | +| `auto_pause_on_expired` | *bool | 过期后自动暂停 | + +> 使用指针类型(`*`)的字段可以区分"未提供"和"设置为零值"。 + +```bash +# 示例:更新账号优先级为 100 +curl -X PUT "${BASE}/api/v1/admin/accounts/1" \ + -H "x-api-key: ${KEY}" \ + -H "Content-Type: application/json" \ + -d '{"priority": 100}' +``` + +#### 1.12 批量更新账号 + +``` +POST /api/v1/admin/accounts/bulk-update +``` + +**请求体**(JSON): + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `account_ids` | []int64 | **是** | 要更新的账号 ID 列表 | +| `priority` | *int | 否 | 优先级 | +| `concurrency` | *int | 否 | 并发数 | +| `rate_multiplier` | *float64 | 否 | 速率倍数 | +| `status` | string | 否 | 状态:`active` / `inactive` / `error` | +| `schedulable` | *bool | 否 | 是否可调度 | +| `group_ids` | *[]int64 | 否 | 分组 ID 列表 | +| `proxy_id` | *int64 | 否 | 代理 ID | +| `credentials` | object | 否 | 凭证信息(批量覆盖) | +| `extra` | object | 否 | 额外配置(批量覆盖) | + +```bash +# 示例:批量设置多个账号优先级为 100 +curl -X POST "${BASE}/api/v1/admin/accounts/bulk-update" \ + -H "x-api-key: ${KEY}" \ + -H "Content-Type: application/json" \ + -d '{"account_ids": [1, 2, 3], "priority": 100}' +``` + +#### 1.13 批量测试账号(脚本) 批量测试指定平台所有账号的指定模型连通性: diff --git a/CLAUDE.md b/CLAUDE.md index 737cdf19..4d96070e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -552,6 +552,18 @@ docker stats sub2api ## Admin API 接口文档 +### ⚠️ API 操作流程规范 + +当收到操作正式环境 Web 界面的新需求,但文档中未记录对应 API 接口时,**必须按以下流程执行**: + +1. **探索接口**:通过代码库搜索路由定义(`backend/internal/server/routes/`)、Handler(`backend/internal/handler/admin/`)和请求结构体,确定正确的 API 端点、请求方法、请求体格式 +2. **更新文档**:将新发现的接口补充到本文档的 Admin API 接口文档章节中,包含端点、参数说明和 curl 示例 +3. **执行操作**:根据最新文档中记录的接口完成用户需求 + +> **目的**:避免每次遇到相同需求都重复探索代码库,确保 API 文档持续完善,后续操作可直接查阅文档执行。 + +--- + ### 认证方式 所有 Admin API 通过 `x-api-key` 请求头传递 Admin API Key 认证。 @@ -722,7 +734,70 @@ GET /api/v1/admin/accounts/:id/usage curl -s "${BASE}/api/v1/admin/accounts/1/usage" -H "x-api-key: ${KEY}" ``` -#### 1.11 批量测试账号(脚本) +#### 1.11 更新单个账号 + +``` +PUT /api/v1/admin/accounts/:id +``` + +**请求体**(JSON,所有字段均为可选,仅传需要更新的字段): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 账号名称 | +| `notes` | *string | 备注 | +| `type` | string | 类型:`oauth` / `setup-token` / `apikey` / `upstream` | +| `credentials` | object | 凭证信息 | +| `extra` | object | 额外配置 | +| `proxy_id` | *int64 | 代理 ID | +| `concurrency` | *int | 并发数 | +| `priority` | *int | 优先级(默认 50) | +| `rate_multiplier` | *float64 | 速率倍数 | +| `status` | string | 状态:`active` / `inactive` | +| `group_ids` | *[]int64 | 分组 ID 列表 | +| `expires_at` | *int64 | 过期时间戳 | +| `auto_pause_on_expired` | *bool | 过期后自动暂停 | + +> 使用指针类型(`*`)的字段可以区分"未提供"和"设置为零值"。 + +```bash +# 示例:更新账号优先级为 100 +curl -X PUT "${BASE}/api/v1/admin/accounts/1" \ + -H "x-api-key: ${KEY}" \ + -H "Content-Type: application/json" \ + -d '{"priority": 100}' +``` + +#### 1.12 批量更新账号 + +``` +POST /api/v1/admin/accounts/bulk-update +``` + +**请求体**(JSON): + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `account_ids` | []int64 | **是** | 要更新的账号 ID 列表 | +| `priority` | *int | 否 | 优先级 | +| `concurrency` | *int | 否 | 并发数 | +| `rate_multiplier` | *float64 | 否 | 速率倍数 | +| `status` | string | 否 | 状态:`active` / `inactive` / `error` | +| `schedulable` | *bool | 否 | 是否可调度 | +| `group_ids` | *[]int64 | 否 | 分组 ID 列表 | +| `proxy_id` | *int64 | 否 | 代理 ID | +| `credentials` | object | 否 | 凭证信息(批量覆盖) | +| `extra` | object | 否 | 额外配置(批量覆盖) | + +```bash +# 示例:批量设置多个账号优先级为 100 +curl -X POST "${BASE}/api/v1/admin/accounts/bulk-update" \ + -H "x-api-key: ${KEY}" \ + -H "Content-Type: application/json" \ + -d '{"account_ids": [1, 2, 3], "priority": 100}' +``` + +#### 1.13 批量测试账号(脚本) 批量测试指定平台所有账号的指定模型连通性: From a03c361b041bbb3c7f51f783130ab7e28dab284f Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Wed, 11 Feb 2026 22:31:07 +0800 Subject: [PATCH 2/4] feat: add gemini model mapping whitelist for apikey and bulk edit --- .../service/gateway_multiplatform_test.go | 79 +++++++++++++++++++ backend/internal/service/gateway_service.go | 4 - .../account/BulkEditAccountModal.vue | 52 +++++++----- .../components/account/CreateAccountModal.vue | 32 +------- .../components/account/EditAccountModal.vue | 32 +------- frontend/src/types/index.ts | 1 + 6 files changed, 116 insertions(+), 84 deletions(-) diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index b4b93ace..e257b1a5 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -890,6 +890,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t require.Equal(t, int64(2), acc.ID) } +func TestGatewayService_SelectAccountForModelWithPlatform_GeminiAPIKeyModelMappingFilter(t *testing.T) { + ctx := context.Background() + + repo := &mockAccountRepoForPlatform{ + accounts: []Account{ + { + ID: 1, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Priority: 1, + Status: StatusActive, + Schedulable: true, + Credentials: map[string]any{"model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}}, + }, + { + ID: 2, + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Priority: 2, + Status: StatusActive, + Schedulable: true, + Credentials: map[string]any{"model_mapping": map[string]any{"gemini-2.5-flash": "gemini-2.5-flash"}}, + }, + }, + accountsByID: map[int64]*Account{}, + } + for i := range repo.accounts { + repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i] + } + + cache := &mockGatewayCacheForPlatform{} + + svc := &GatewayService{ + accountRepo: repo, + cache: cache, + cfg: testConfig(), + } + + acc, err := svc.selectAccountForModelWithPlatform(ctx, nil, "", "gemini-2.5-flash", nil, PlatformGemini) + require.NoError(t, err) + require.NotNil(t, acc) + require.Equal(t, int64(2), acc.ID, "应过滤不支持请求模型的 APIKey 账号") + + acc, err = svc.selectAccountForModelWithPlatform(ctx, nil, "", "gemini-3-pro-preview", nil, PlatformGemini) + require.Error(t, err) + require.Nil(t, acc) + require.Contains(t, err.Error(), "supporting model") +} + func TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup(t *testing.T) { ctx := context.Background() groupID := int64(50) @@ -1065,6 +1114,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) { model: "claude-3-5-sonnet-20241022", expected: true, }, + { + name: "Gemini平台-无映射配置-支持所有模型", + account: &Account{Platform: PlatformGemini, Type: AccountTypeAPIKey}, + model: "gemini-2.5-flash", + expected: true, + }, + { + name: "Gemini平台-有映射配置-只支持配置的模型", + account: &Account{ + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}, + }, + }, + model: "gemini-2.5-flash", + expected: false, + }, + { + name: "Gemini平台-有映射配置-支持配置的模型", + account: &Account{ + Platform: PlatformGemini, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "model_mapping": map[string]any{"gemini-2.5-pro": "gemini-2.5-pro"}, + }, + }, + model: "gemini-2.5-pro", + expected: true, + }, } for _, tt := range tests { diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 56af4610..3141f71d 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2547,10 +2547,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo if account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey { requestedModel = claude.NormalizeModelID(requestedModel) } - // Gemini API Key 账户直接透传,由上游判断模型是否支持 - if account.Platform == PlatformGemini && account.Type == AccountTypeAPIKey { - return true - } // 其他平台使用账户的模型支持检查 return account.IsModelSupported(requestedModel) } diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 912eabb3..879a255c 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -654,6 +654,7 @@ import Select from '@/components/common/Select.vue' import ProxySelector from '@/components/common/ProxySelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue' import Icon from '@/components/icons/Icon.vue' +import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist' interface Props { show: boolean @@ -705,7 +706,7 @@ const rateMultiplier = ref(1) const status = ref<'active' | 'inactive'>('active') const groupIds = ref([]) -// All models list (combined Anthropic + OpenAI) +// All models list (combined Anthropic + OpenAI + Gemini) const allModels = [ { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' }, { value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' }, @@ -722,10 +723,15 @@ const allModels = [ { value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' }, { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' }, { value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' }, - { value: 'gpt-5-2025-08-07', label: 'GPT-5' } + { value: 'gpt-5-2025-08-07', label: 'GPT-5' }, + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, + { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, + { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, + { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' }, + { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' } ] -// Preset mappings (combined Anthropic + OpenAI) +// Preset mappings (combined Anthropic + OpenAI + Gemini) const presetMappings = [ { label: 'Sonnet 4', @@ -777,6 +783,24 @@ const presetMappings = [ from: 'gpt-5.1-codex-max', to: 'gpt-5.1-codex', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' + }, + { + label: 'Gemini Flash 2.0', + from: 'gemini-2.0-flash', + to: 'gemini-2.0-flash', + color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' + }, + { + label: 'Gemini 2.5 Flash', + from: 'gemini-2.5-flash', + to: 'gemini-2.5-flash', + color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' + }, + { + label: 'Gemini 2.5 Pro', + from: 'gemini-2.5-pro', + to: 'gemini-2.5-pro', + color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' } ] @@ -866,23 +890,11 @@ const removeErrorCode = (code: number) => { } const buildModelMappingObject = (): Record | null => { - const mapping: Record = {} - - if (modelRestrictionMode.value === 'whitelist') { - for (const model of allowedModels.value) { - mapping[model] = model - } - } else { - for (const m of modelMappings.value) { - const from = m.from.trim() - const to = m.to.trim() - if (from && to) { - mapping[from] = to - } - } - } - - return Object.keys(mapping).length > 0 ? mapping : null + return buildModelMappingPayload( + modelRestrictionMode.value, + allowedModels.value, + modelMappings.value + ) } const buildUpdatePayload = (): Record | null => { diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index af06abca..2d7d745c 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -862,8 +862,8 @@

{{ t('admin.accounts.gemini.tier.aiStudioHint') }}

- -
+ +
@@ -1135,34 +1135,6 @@
- -
-
-
- - - -
-

- {{ t('admin.accounts.gemini.modelPassthrough') }} -

-

- {{ t('admin.accounts.gemini.modelPassthroughDesc') }} -

-
-
-
-
diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 60575f56..3395e82e 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -65,8 +65,8 @@

{{ t('admin.accounts.leaveEmptyToKeep') }}

- -
+ +
@@ -338,34 +338,6 @@
- -
-
-
- - - -
-

- {{ t('admin.accounts.gemini.modelPassthrough') }} -

-

- {{ t('admin.accounts.gemini.modelPassthroughDesc') }} -

-
-
-
-
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 189d8af1..a250820b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -515,6 +515,7 @@ export interface ProxyAccountSummary { export interface GeminiCredentials { // API Key authentication api_key?: string + model_mapping?: Record // OAuth authentication access_token?: string From 51e903c34e092761ece3fa659bc303aec94ef6c3 Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Wed, 11 Feb 2026 23:26:20 +0800 Subject: [PATCH 3/4] =?UTF-8?q?Revert=20"fix:=20=E5=B9=B6=E5=8F=91/?= =?UTF-8?q?=E6=8E=92=E9=98=9F=E9=9D=A2=E6=9D=BF=E6=94=AF=E6=8C=81=20platfo?= =?UTF-8?q?rm/group=20=E8=BF=87=E6=BB=A4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 86e600aa5248163518dc2b549939614910dfcda8. --- .../handler/admin/ops_realtime_handler.go | 17 +---- backend/internal/service/ops_concurrency.go | 70 +------------------ frontend/src/api/admin/ops.ts | 12 +--- .../ops/components/OpsConcurrencyCard.vue | 10 +-- 4 files changed, 6 insertions(+), 103 deletions(-) diff --git a/backend/internal/handler/admin/ops_realtime_handler.go b/backend/internal/handler/admin/ops_realtime_handler.go index 2d3cce4b..c175dcd0 100644 --- a/backend/internal/handler/admin/ops_realtime_handler.go +++ b/backend/internal/handler/admin/ops_realtime_handler.go @@ -65,10 +65,6 @@ func (h *OpsHandler) GetConcurrencyStats(c *gin.Context) { // GetUserConcurrencyStats returns real-time concurrency usage for all active users. // GET /api/v1/admin/ops/user-concurrency -// -// Query params: -// - platform: optional, filter users by allowed platform -// - group_id: optional, filter users by allowed group func (h *OpsHandler) GetUserConcurrencyStats(c *gin.Context) { if h.opsService == nil { response.Error(c, http.StatusServiceUnavailable, "Ops service not available") @@ -88,18 +84,7 @@ func (h *OpsHandler) GetUserConcurrencyStats(c *gin.Context) { return } - platformFilter := strings.TrimSpace(c.Query("platform")) - var groupID *int64 - if v := strings.TrimSpace(c.Query("group_id")); v != "" { - id, err := strconv.ParseInt(v, 10, 64) - if err != nil || id <= 0 { - response.BadRequest(c, "Invalid group_id") - return - } - groupID = &id - } - - users, collectedAt, err := h.opsService.GetUserConcurrencyStats(c.Request.Context(), platformFilter, groupID) + users, collectedAt, err := h.opsService.GetUserConcurrencyStats(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/service/ops_concurrency.go b/backend/internal/service/ops_concurrency.go index faac2d5b..f6541d08 100644 --- a/backend/internal/service/ops_concurrency.go +++ b/backend/internal/service/ops_concurrency.go @@ -344,16 +344,8 @@ func (s *OpsService) getUsersLoadMapBestEffort(ctx context.Context, users []User return out } -// GetUserConcurrencyStats returns real-time concurrency usage for active users. -// -// Optional filters: -// - platformFilter: only include users who have access to groups belonging to that platform -// - groupIDFilter: only include users who have access to that specific group -func (s *OpsService) GetUserConcurrencyStats( - ctx context.Context, - platformFilter string, - groupIDFilter *int64, -) (map[int64]*UserConcurrencyInfo, *time.Time, error) { +// GetUserConcurrencyStats returns real-time concurrency usage for all active users. +func (s *OpsService) GetUserConcurrencyStats(ctx context.Context) (map[int64]*UserConcurrencyInfo, *time.Time, error) { if err := s.RequireMonitoringEnabled(ctx); err != nil { return nil, nil, err } @@ -363,15 +355,6 @@ func (s *OpsService) GetUserConcurrencyStats( return nil, nil, err } - // Build a set of allowed group IDs when filtering is requested. - var allowedGroupIDs map[int64]struct{} - if platformFilter != "" || (groupIDFilter != nil && *groupIDFilter > 0) { - allowedGroupIDs, err = s.buildAllowedGroupIDsForFilter(ctx, platformFilter, groupIDFilter) - if err != nil { - return nil, nil, err - } - } - collectedAt := time.Now() loadMap := s.getUsersLoadMapBestEffort(ctx, users) @@ -382,12 +365,6 @@ func (s *OpsService) GetUserConcurrencyStats( continue } - // Apply group/platform filter: skip users whose AllowedGroups - // have no intersection with the matching group IDs. - if allowedGroupIDs != nil && !userMatchesGroupFilter(u.AllowedGroups, allowedGroupIDs) { - continue - } - load := loadMap[u.ID] currentInUse := int64(0) waiting := int64(0) @@ -417,46 +394,3 @@ func (s *OpsService) GetUserConcurrencyStats( return result, &collectedAt, nil } - -// buildAllowedGroupIDsForFilter returns the set of group IDs that match the given -// platform and/or group ID filter. It reuses listAllAccountsForOps (which already -// supports platform filtering at the DB level) to collect group IDs from accounts. -func (s *OpsService) buildAllowedGroupIDsForFilter(ctx context.Context, platformFilter string, groupIDFilter *int64) (map[int64]struct{}, error) { - // Fast path: only group ID filter, no platform filter needed. - if platformFilter == "" && groupIDFilter != nil && *groupIDFilter > 0 { - return map[int64]struct{}{*groupIDFilter: {}}, nil - } - - // Use the same account-based approach as GetConcurrencyStats to collect group IDs. - accounts, err := s.listAllAccountsForOps(ctx, platformFilter) - if err != nil { - return nil, err - } - - groupIDs := make(map[int64]struct{}) - for _, acc := range accounts { - for _, grp := range acc.Groups { - if grp == nil || grp.ID <= 0 { - continue - } - // If groupIDFilter is set, only include that specific group. - if groupIDFilter != nil && *groupIDFilter > 0 && grp.ID != *groupIDFilter { - continue - } - groupIDs[grp.ID] = struct{}{} - } - } - - return groupIDs, nil -} - -// userMatchesGroupFilter returns true if the user's AllowedGroups contains -// at least one group ID in the allowed set. -func userMatchesGroupFilter(userGroups []int64, allowedGroupIDs map[int64]struct{}) bool { - for _, gid := range userGroups { - if _, ok := allowedGroupIDs[gid]; ok { - return true - } - } - return false -} diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 523fbd00..9f980a12 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -366,16 +366,8 @@ export async function getConcurrencyStats(platform?: string, groupId?: number | return data } -export async function getUserConcurrencyStats(platform?: string, groupId?: number | null): Promise { - const params: Record = {} - if (platform) { - params.platform = platform - } - if (typeof groupId === 'number' && groupId > 0) { - params.group_id = groupId - } - - const { data } = await apiClient.get('/admin/ops/user-concurrency', { params }) +export async function getUserConcurrencyStats(): Promise { + const { data } = await apiClient.get('/admin/ops/user-concurrency') return data } diff --git a/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue b/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue index 0956caa5..ca640ade 100644 --- a/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue +++ b/frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue @@ -265,7 +265,7 @@ async function loadData() { try { if (showByUser.value) { // 用户视图模式只加载用户并发数据 - const userData = await opsAPI.getUserConcurrencyStats(props.platformFilter, props.groupIdFilter) + const userData = await opsAPI.getUserConcurrencyStats() userConcurrency.value = userData } else { // 常规模式加载账号/平台/分组数据 @@ -301,14 +301,6 @@ watch( } ) -// 过滤条件变化时重新加载数据 -watch( - [() => props.platformFilter, () => props.groupIdFilter], - () => { - loadData() - } -) - function getLoadBarClass(loadPct: number): string { if (loadPct >= 90) return 'bg-red-500 dark:bg-red-600' if (loadPct >= 70) return 'bg-orange-500 dark:bg-orange-600' From 9af4a55176d38d19519aa939fc5ad44d99382149 Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Wed, 11 Feb 2026 23:35:26 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20gofmt=20=E6=A0=BC=E5=BC=8F=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20gateway=5Fcache=5Fintegration=5Ftest.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/repository/gateway_cache_integration_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/internal/repository/gateway_cache_integration_test.go b/backend/internal/repository/gateway_cache_integration_test.go index 2fdaa3d1..0eebc33f 100644 --- a/backend/internal/repository/gateway_cache_integration_test.go +++ b/backend/internal/repository/gateway_cache_integration_test.go @@ -104,7 +104,6 @@ func (s *GatewayCacheSuite) TestGetSessionAccountID_CorruptedValue() { require.False(s.T(), errors.Is(err, redis.Nil), "expected parsing error, not redis.Nil") } - func TestGatewayCacheSuite(t *testing.T) { suite.Run(t, new(GatewayCacheSuite)) }