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"`