From c31974c913593f0871ecca761e0fdb3de71a3dfb Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sun, 15 Mar 2026 16:06:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=99=90=E9=A2=9D=E5=AD=97=E6=AE=B5=E4=B8=BA=E7=A9=BA=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5=20#1021?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复在填写限额时,如果不填写完整的三个限额额度(日限额、周限额、月限额)就会报错的问题。 变更内容: - 后端:添加 optionalLimitField 类型处理空值和空字符串,兼容部分限额字段为空的情况 - 前端:添加 normalizeOptionalLimit 函数规范化限额输入,将空值、空字符串和无效数字统一处理为 null --- .../internal/handler/admin/group_handler.go | 76 ++++++++++++++++--- frontend/src/views/admin/GroupsView.vue | 23 ++++++ 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 5be66768..4725c3d1 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -1,6 +1,9 @@ package admin import ( + "bytes" + "encoding/json" + "fmt" "strconv" "strings" @@ -16,6 +19,55 @@ type GroupHandler struct { adminService service.AdminService } +type optionalLimitField struct { + set bool + value *float64 +} + +func (f *optionalLimitField) UnmarshalJSON(data []byte) error { + f.set = true + + trimmed := bytes.TrimSpace(data) + if bytes.Equal(trimmed, []byte("null")) { + f.value = nil + return nil + } + + var number float64 + if err := json.Unmarshal(trimmed, &number); err == nil { + f.value = &number + return nil + } + + var text string + if err := json.Unmarshal(trimmed, &text); err == nil { + text = strings.TrimSpace(text) + if text == "" { + f.value = nil + return nil + } + number, err = strconv.ParseFloat(text, 64) + if err != nil { + return fmt.Errorf("invalid numeric limit value %q: %w", text, err) + } + f.value = &number + return nil + } + + return fmt.Errorf("invalid limit value: %s", string(trimmed)) +} + +func (f optionalLimitField) ToServiceInput() *float64 { + if !f.set { + return nil + } + if f.value != nil { + return f.value + } + zero := 0.0 + return &zero +} + // NewGroupHandler creates a new admin group handler func NewGroupHandler(adminService service.AdminService) *GroupHandler { return &GroupHandler{ @@ -31,9 +83,9 @@ type CreateGroupRequest struct { RateMultiplier float64 `json:"rate_multiplier"` IsExclusive bool `json:"is_exclusive"` SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` - DailyLimitUSD *float64 `json:"daily_limit_usd"` - WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` - MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` + DailyLimitUSD optionalLimitField `json:"daily_limit_usd"` + WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"` + MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"` // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置) ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` @@ -69,9 +121,9 @@ type UpdateGroupRequest struct { IsExclusive *bool `json:"is_exclusive"` Status string `json:"status" binding:"omitempty,oneof=active inactive"` SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` - DailyLimitUSD *float64 `json:"daily_limit_usd"` - WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` - MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` + DailyLimitUSD optionalLimitField `json:"daily_limit_usd"` + WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"` + MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"` // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置) ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` @@ -191,9 +243,9 @@ func (h *GroupHandler) Create(c *gin.Context) { RateMultiplier: req.RateMultiplier, IsExclusive: req.IsExclusive, SubscriptionType: req.SubscriptionType, - DailyLimitUSD: req.DailyLimitUSD, - WeeklyLimitUSD: req.WeeklyLimitUSD, - MonthlyLimitUSD: req.MonthlyLimitUSD, + DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(), + WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(), + MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(), ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, @@ -244,9 +296,9 @@ func (h *GroupHandler) Update(c *gin.Context) { IsExclusive: req.IsExclusive, Status: req.Status, SubscriptionType: req.SubscriptionType, - DailyLimitUSD: req.DailyLimitUSD, - WeeklyLimitUSD: req.WeeklyLimitUSD, - MonthlyLimitUSD: req.MonthlyLimitUSD, + DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(), + WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(), + MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(), ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index a78762d6..71fa7a20 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -2368,6 +2368,23 @@ const closeCreateModal = () => { createModelRoutingRules.value = [] } +const normalizeOptionalLimit = (value: number | string | null | undefined): number | null => { + if (value === null || value === undefined) { + return null + } + + if (typeof value === 'string') { + const trimmed = value.trim() + if (!trimmed) { + return null + } + const parsed = Number(trimmed) + return Number.isFinite(parsed) && parsed > 0 ? parsed : null + } + + return Number.isFinite(value) && value > 0 ? value : null +} + const handleCreateGroup = async () => { if (!createForm.name.trim()) { appStore.showError(t('admin.groups.nameRequired')) @@ -2379,6 +2396,9 @@ const handleCreateGroup = async () => { const { sora_storage_quota_gb: createQuotaGb, ...createRest } = createForm const requestData = { ...createRest, + daily_limit_usd: normalizeOptionalLimit(createForm.daily_limit_usd as number | string | null), + weekly_limit_usd: normalizeOptionalLimit(createForm.weekly_limit_usd as number | string | null), + monthly_limit_usd: normalizeOptionalLimit(createForm.monthly_limit_usd as number | string | null), sora_storage_quota_bytes: createQuotaGb ? Math.round(createQuotaGb * 1024 * 1024 * 1024) : 0, model_routing: convertRoutingRulesToApiFormat(createModelRoutingRules.value) } @@ -2457,6 +2477,9 @@ const handleUpdateGroup = async () => { const { sora_storage_quota_gb: editQuotaGb, ...editRest } = editForm const payload = { ...editRest, + daily_limit_usd: normalizeOptionalLimit(editForm.daily_limit_usd as number | string | null), + weekly_limit_usd: normalizeOptionalLimit(editForm.weekly_limit_usd as number | string | null), + monthly_limit_usd: normalizeOptionalLimit(editForm.monthly_limit_usd as number | string | null), sora_storage_quota_bytes: editQuotaGb ? Math.round(editQuotaGb * 1024 * 1024 * 1024) : 0, fallback_group_id: editForm.fallback_group_id === null ? 0 : editForm.fallback_group_id, fallback_group_id_on_invalid_request: From 6fba4ebb13d9e92a36b7930bd8141483ff2aacf2 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sun, 15 Mar 2026 16:37:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E5=9C=A8=20Dockerfile.goreleaser=20?= =?UTF-8?q?=E4=B8=AD=E6=B7=BB=E5=8A=A0=20pg=5Fdump=20=E5=92=8C=20psql=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20#1002?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了支持容器内的数据库备份和恢复功能,在运行时镜像中添加 PostgreSQL 客户端工具。 变更内容: - 使用多阶段构建从 postgres:18-alpine 镜像复制 pg_dump 和 psql 二进制文件 - 添加必要的依赖库(libpq, zstd-libs, lz4-libs, krb5-libs, libldap, libedit) - 升级基础镜像到 alpine:3.21 - 复制 libpq.so.5 共享库以确保工具正常运行 这样可以在运行时容器中直接执行数据库备份和恢复操作,无需访问 Docker socket。 --- Dockerfile.goreleaser | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 2242c162..419994b9 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -5,7 +5,12 @@ # It only packages the pre-built binary, no compilation needed. # ============================================================================= -FROM alpine:3.19 +ARG ALPINE_IMAGE=alpine:3.21 +ARG POSTGRES_IMAGE=postgres:18-alpine + +FROM ${POSTGRES_IMAGE} AS pg-client + +FROM ${ALPINE_IMAGE} LABEL maintainer="Wei-Shaw " LABEL description="Sub2API - AI API Gateway Platform" @@ -16,8 +21,20 @@ RUN apk add --no-cache \ ca-certificates \ tzdata \ curl \ + libpq \ + zstd-libs \ + lz4-libs \ + krb5-libs \ + libldap \ + libedit \ && rm -rf /var/cache/apk/* +# Copy pg_dump and psql from a version-matched PostgreSQL image so backup and +# restore work in the runtime container without requiring Docker socket access. +COPY --from=pg-client /usr/local/bin/pg_dump /usr/local/bin/pg_dump +COPY --from=pg-client /usr/local/bin/psql /usr/local/bin/psql +COPY --from=pg-client /usr/local/lib/libpq.so.5* /usr/local/lib/ + # Create non-root user RUN addgroup -g 1000 sub2api && \ adduser -u 1000 -G sub2api -s /bin/sh -D sub2api From 19d3ecc76f355fbac5026bacdac6aac54716c018 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sun, 15 Mar 2026 16:57:29 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E8=B4=A6=E5=8F=B7=E6=97=B6=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E6=98=BE=E7=A4=BA=E4=B8=8E=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20#982?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复批量编辑账号时,UI 显示的是 plain 模型名(如 GPT-5),但实际落库的是 dated 模型名的问题。 核心改动: 1. 批量编辑白名单不再使用 BulkEditAccountModal.vue 中手写的过期模型列表 - 移除了 allModels 和 presetMappings 的硬编码列表(共 200+ 行) - 直接复用 ModelWhitelistSelector.vue 组件 2. ModelWhitelistSelector 组件支持多平台联合过滤 - 新增 platforms 属性支持传入多个平台 - 添加 normalizedPlatforms 计算属性统一处理单平台和多平台场景 - availableOptions 根据选中的多个平台动态联合过滤模型列表 - fillRelated 功能支持一次性填充多个平台的相关模型 3. 模型映射预设改为动态生成 - filteredPresets 改用 getPresetMappingsByPlatform 从统一模型源按平台动态生成 - 不再依赖弹窗中的手写预设列表 现在的行为: - UI 显示什么模型,勾选什么模型,传给后端的就是什么模型 - 彻底解决了批量编辑链路上"显示与实际不一致"的问题 - 模型列表和映射预设始终与系统定义保持同步 --- .../account/BulkEditAccountModal.vue | 261 ++---------------- .../account/ModelWhitelistSelector.vue | 43 ++- 2 files changed, 58 insertions(+), 246 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index c6e08684..64524d51 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -164,27 +164,10 @@

- -
- -
+

{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} @@ -832,8 +815,12 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import Select from '@/components/common/Select.vue' import ProxySelector from '@/components/common/ProxySelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue' +import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import Icon from '@/components/icons/Icon.vue' -import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist' +import { + buildModelMappingObject as buildModelMappingPayload, + getPresetMappingsByPlatform +} from '@/composables/useModelWhitelist' interface Props { show: boolean @@ -865,26 +852,20 @@ const allAnthropicOAuthOrSetupToken = computed(() => { ) }) -const platformModelPrefix: Record = { - anthropic: ['claude-'], - antigravity: ['claude-', 'gemini-', 'gpt-oss-', 'tab_'], - openai: ['gpt-'], - gemini: ['gemini-'], - sora: [] -} - -const filteredModels = computed(() => { - if (props.selectedPlatforms.length === 0) return allModels - const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))] - if (prefixes.length === 0) return allModels - return allModels.filter(m => prefixes.some(prefix => m.value.startsWith(prefix))) -}) - const filteredPresets = computed(() => { - if (props.selectedPlatforms.length === 0) return presetMappings - const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))] - if (prefixes.length === 0) return presetMappings - return presetMappings.filter(m => prefixes.some(prefix => m.from.startsWith(prefix))) + if (props.selectedPlatforms.length === 0) return [] + + const dedupedPresets = new Map[number]>() + for (const platform of props.selectedPlatforms) { + for (const preset of getPresetMappingsByPlatform(platform)) { + const key = `${preset.from}=>${preset.to}` + if (!dedupedPresets.has(key)) { + dedupedPresets.set(key, preset) + } + } + } + + return Array.from(dedupedPresets.values()) }) // Model mapping type @@ -937,204 +918,6 @@ const umqModeOptions = computed(() => [ { value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') }, ]) -// All models list (combined Anthropic + OpenAI + Gemini) -const allModels = [ - { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' }, - { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' }, - { value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' }, - { value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' }, - { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' }, - { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' }, - { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' }, - { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' }, - { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' }, - { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }, - { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' }, - { value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' }, - { value: 'gpt-5.4', label: 'GPT-5.4' }, - { value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' }, - { value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' }, - { value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' }, - { 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: 'gemini-3.1-flash-image', label: 'Gemini 3.1 Flash Image' }, - { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash Image' }, - { 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-pro-image', label: 'Gemini 3 Pro Image (Legacy)' }, - { 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 + Gemini) -const presetMappings = [ - { - label: 'Sonnet 4', - from: 'claude-sonnet-4-20250514', - to: 'claude-sonnet-4-20250514', - color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' - }, - { - label: 'Sonnet 4.5', - from: 'claude-sonnet-4-5-20250929', - to: 'claude-sonnet-4-5-20250929', - color: - 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' - }, - { - label: 'Opus 4.5', - from: 'claude-opus-4-5-20251101', - to: 'claude-opus-4-5-20251101', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Opus 4.6', - from: 'claude-opus-4-6', - to: 'claude-opus-4-6-thinking', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Opus 4.6-thinking', - from: 'claude-opus-4-6-thinking', - to: 'claude-opus-4-6-thinking', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Sonnet 4.6', - from: 'claude-sonnet-4-6', - to: 'claude-sonnet-4-6', - color: - 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' - }, - { - label: 'Sonnet4→4.6', - from: 'claude-sonnet-4-20250514', - to: 'claude-sonnet-4-6', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'Sonnet4.5→4.6', - from: 'claude-sonnet-4-5-20250929', - to: 'claude-sonnet-4-6', - color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' - }, - { - label: 'Sonnet3.5→4.6', - from: 'claude-3-5-sonnet-20241022', - to: 'claude-sonnet-4-6', - color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' - }, - { - label: 'Opus4.5→4.6', - from: 'claude-opus-4-5-20251101', - to: 'claude-opus-4-6-thinking', - color: - 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' - }, - { - label: 'Opus->Sonnet', - from: 'claude-opus-4-5-20251101', - to: 'claude-sonnet-4-5-20250929', - color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' - }, - { - label: 'Gemini 2.5 Image', - from: 'gemini-2.5-flash-image', - to: 'gemini-2.5-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'Gemini 3.1 Image', - from: 'gemini-3.1-flash-image', - to: 'gemini-3.1-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'G3 Image→3.1', - from: 'gemini-3-pro-image', - to: 'gemini-3.1-flash-image', - color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' - }, - { - label: 'GPT-5.3 Codex', - from: 'gpt-5.3-codex', - to: 'gpt-5.3-codex', - color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' - }, - { - label: 'GPT-5.3 Spark', - from: 'gpt-5.3-codex-spark', - to: 'gpt-5.3-codex-spark', - color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' - }, - { - label: 'GPT-5.4', - from: 'gpt-5.4', - to: 'gpt-5.4', - color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' - }, - { - label: '5.2→5.3', - from: 'gpt-5.2-codex', - to: 'gpt-5.3-codex', - color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' - }, - { - label: 'GPT-5.2', - from: 'gpt-5.2-2025-12-11', - to: 'gpt-5.2-2025-12-11', - color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' - }, - { - label: 'GPT-5.2 Codex', - from: 'gpt-5.2-codex', - to: 'gpt-5.2-codex', - color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' - }, - { - label: 'Max->Codex', - 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: '3-Pro-Preview→3.1-Pro-High', - from: 'gemini-3-pro-preview', - to: 'gemini-3.1-pro-high', - color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' - }, - { - label: '3-Pro-High→3.1-Pro-High', - from: 'gemini-3-pro-high', - to: 'gemini-3.1-pro-high', - color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' - }, - { - label: '3-Pro-Low→3.1-Pro-Low', - from: 'gemini-3-pro-low', - to: 'gemini-3.1-pro-low', - color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' - }, - { - label: '3-Flash透传', - from: 'gemini-3-flash', - to: 'gemini-3-flash', - color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' - }, - { - label: '2.5-Flash-Lite透传', - from: 'gemini-2.5-flash-lite', - to: 'gemini-2.5-flash-lite', - color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' - } -] - // Common HTTP error codes const commonErrorCodes = [ { value: 401, label: 'Unauthorized' }, diff --git a/frontend/src/components/account/ModelWhitelistSelector.vue b/frontend/src/components/account/ModelWhitelistSelector.vue index 16ffa225..ebce3740 100644 --- a/frontend/src/components/account/ModelWhitelistSelector.vue +++ b/frontend/src/components/account/ModelWhitelistSelector.vue @@ -131,7 +131,8 @@ const { t } = useI18n() const props = defineProps<{ modelValue: string[] - platform: string + platform?: string + platforms?: string[] }>() const emit = defineEmits<{ @@ -144,11 +145,36 @@ const showDropdown = ref(false) const searchQuery = ref('') const customModel = ref('') const isComposing = ref(false) +const normalizedPlatforms = computed(() => { + const rawPlatforms = + props.platforms && props.platforms.length > 0 + ? props.platforms + : props.platform + ? [props.platform] + : [] + + return Array.from( + new Set( + rawPlatforms + .map(platform => platform?.trim()) + .filter((platform): platform is string => Boolean(platform)) + ) + ) +}) + const availableOptions = computed(() => { - if (props.platform === 'sora') { - return getModelsByPlatform('sora').map(m => ({ value: m, label: m })) + if (normalizedPlatforms.value.length === 0) { + return allModels } - return allModels + + const allowedModels = new Set() + for (const platform of normalizedPlatforms.value) { + for (const model of getModelsByPlatform(platform)) { + allowedModels.add(model) + } + } + + return allModels.filter(model => allowedModels.has(model.value)) }) const filteredModels = computed(() => { @@ -192,10 +218,13 @@ const handleEnter = () => { } const fillRelated = () => { - const models = getModelsByPlatform(props.platform) const newModels = [...props.modelValue] - for (const model of models) { - if (!newModels.includes(model)) newModels.push(model) + for (const platform of normalizedPlatforms.value) { + for (const model of getModelsByPlatform(platform)) { + if (!newModels.includes(model)) { + newModels.push(model) + } + } } emit('update:modelValue', newModels) } From 90b38381737405e50b8f127aa26bd04ccee613ed Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sun, 15 Mar 2026 17:44:44 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20Gemini=20?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81=E7=9A=84=20patternProperties=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20#795?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/gemini_messages_compat_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index a003f636..e65c838d 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -3235,7 +3235,7 @@ func cleanToolSchema(schema any) any { for key, value := range v { // 跳过不支持的字段 if key == "$schema" || key == "$id" || key == "$ref" || - key == "additionalProperties" || key == "minLength" || + key == "additionalProperties" || key == "patternProperties" || key == "minLength" || key == "maxLength" || key == "minItems" || key == "maxItems" { continue } From 686f890fbf3813f97acf0f156146387a1897a2bd Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Sun, 15 Mar 2026 18:42:07 +0800 Subject: [PATCH 5/5] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=20gofmt=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/handler/admin/group_handler.go | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 4725c3d1..4ffe64ee 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -77,12 +77,12 @@ func NewGroupHandler(adminService service.AdminService) *GroupHandler { // CreateGroupRequest represents create group request type CreateGroupRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"` - RateMultiplier float64 `json:"rate_multiplier"` - IsExclusive bool `json:"is_exclusive"` - SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"` + RateMultiplier float64 `json:"rate_multiplier"` + IsExclusive bool `json:"is_exclusive"` + SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` DailyLimitUSD optionalLimitField `json:"daily_limit_usd"` WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"` MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"` @@ -114,13 +114,13 @@ type CreateGroupRequest struct { // UpdateGroupRequest represents update group request type UpdateGroupRequest struct { - Name string `json:"name"` - Description string `json:"description"` - Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"` - RateMultiplier *float64 `json:"rate_multiplier"` - IsExclusive *bool `json:"is_exclusive"` - Status string `json:"status" binding:"omitempty,oneof=active inactive"` - SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` + Name string `json:"name"` + Description string `json:"description"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"` + RateMultiplier *float64 `json:"rate_multiplier"` + IsExclusive *bool `json:"is_exclusive"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` + SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` DailyLimitUSD optionalLimitField `json:"daily_limit_usd"` WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"` MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`