diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go
index 7ad51660670..cbba4db1571 100644
--- a/backend/internal/handler/admin/setting_handler.go
+++ b/backend/internal/handler/admin/setting_handler.go
@@ -197,6 +197,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
TablePageSizeOptions: settings.TablePageSizeOptions,
CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
+ DefaultProxyID: settings.DefaultProxyID,
DefaultConcurrency: settings.DefaultConcurrency,
DefaultBalance: settings.DefaultBalance,
RiskControlEnabled: settings.RiskControlEnabled,
@@ -442,6 +443,7 @@ type UpdateSettingsRequest struct {
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
+ DefaultProxyID *int64 `json:"default_proxy_id"`
// 默认配置
DefaultConcurrency int `json:"default_concurrency"`
@@ -1348,25 +1350,35 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
TablePageSizeOptions: req.TablePageSizeOptions,
CustomMenuItems: customMenuJSON,
CustomEndpoints: customEndpointsJSON,
- DefaultConcurrency: req.DefaultConcurrency,
- DefaultBalance: req.DefaultBalance,
- AffiliateRebateRate: affiliateRebateRate,
- AffiliateRebateFreezeHours: affiliateRebateFreezeHours,
- AffiliateRebateDurationDays: affiliateRebateDurationDays,
- AffiliateRebatePerInviteeCap: affiliateRebatePerInviteeCap,
- DefaultUserRPMLimit: req.DefaultUserRPMLimit,
- DefaultSubscriptions: defaultSubscriptions,
- EnableModelFallback: req.EnableModelFallback,
- FallbackModelAnthropic: req.FallbackModelAnthropic,
- FallbackModelOpenAI: req.FallbackModelOpenAI,
- FallbackModelGemini: req.FallbackModelGemini,
- FallbackModelAntigravity: req.FallbackModelAntigravity,
- EnableIdentityPatch: req.EnableIdentityPatch,
- IdentityPatchPrompt: req.IdentityPatchPrompt,
- MinClaudeCodeVersion: req.MinClaudeCodeVersion,
- MaxClaudeCodeVersion: req.MaxClaudeCodeVersion,
- AllowUngroupedKeyScheduling: req.AllowUngroupedKeyScheduling,
- BackendModeEnabled: req.BackendModeEnabled,
+ DefaultProxyID: func() *int64 {
+ if req.DefaultProxyID != nil {
+ if *req.DefaultProxyID <= 0 {
+ return nil
+ }
+ value := *req.DefaultProxyID
+ return &value
+ }
+ return previousSettings.DefaultProxyID
+ }(),
+ DefaultConcurrency: req.DefaultConcurrency,
+ DefaultBalance: req.DefaultBalance,
+ AffiliateRebateRate: affiliateRebateRate,
+ AffiliateRebateFreezeHours: affiliateRebateFreezeHours,
+ AffiliateRebateDurationDays: affiliateRebateDurationDays,
+ AffiliateRebatePerInviteeCap: affiliateRebatePerInviteeCap,
+ DefaultUserRPMLimit: req.DefaultUserRPMLimit,
+ DefaultSubscriptions: defaultSubscriptions,
+ EnableModelFallback: req.EnableModelFallback,
+ FallbackModelAnthropic: req.FallbackModelAnthropic,
+ FallbackModelOpenAI: req.FallbackModelOpenAI,
+ FallbackModelGemini: req.FallbackModelGemini,
+ FallbackModelAntigravity: req.FallbackModelAntigravity,
+ EnableIdentityPatch: req.EnableIdentityPatch,
+ IdentityPatchPrompt: req.IdentityPatchPrompt,
+ MinClaudeCodeVersion: req.MinClaudeCodeVersion,
+ MaxClaudeCodeVersion: req.MaxClaudeCodeVersion,
+ AllowUngroupedKeyScheduling: req.AllowUngroupedKeyScheduling,
+ BackendModeEnabled: req.BackendModeEnabled,
OpsMonitoringEnabled: func() bool {
if req.OpsMonitoringEnabled != nil {
return *req.OpsMonitoringEnabled
@@ -1720,6 +1732,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
+ DefaultProxyID: updatedSettings.DefaultProxyID,
DefaultConcurrency: updatedSettings.DefaultConcurrency,
DefaultBalance: updatedSettings.DefaultBalance,
AffiliateRebateRate: updatedSettings.AffiliateRebateRate,
@@ -2047,6 +2060,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.HideCcsImportButton != after.HideCcsImportButton {
changed = append(changed, "hide_ccs_import_button")
}
+ if !equalInt64Ptr(before.DefaultProxyID, after.DefaultProxyID) {
+ changed = append(changed, "default_proxy_id")
+ }
if before.DefaultConcurrency != after.DefaultConcurrency {
changed = append(changed, "default_concurrency")
}
@@ -2298,6 +2314,13 @@ func defaultSubscriptionsValueOrDefault(input *[]dto.DefaultSubscriptionSetting,
return result
}
+func equalInt64Ptr(a, b *int64) bool {
+ if a == nil || b == nil {
+ return a == nil && b == nil
+ }
+ return *a == *b
+}
+
func systemSettingsResponseData(settings dto.SystemSettings, authSourceDefaults *service.AuthSourceDefaultSettings) map[string]any {
data := make(map[string]any)
raw, err := json.Marshal(settings)
diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go
index 2d4cefa1595..6dab9e3af5a 100644
--- a/backend/internal/handler/dto/settings.go
+++ b/backend/internal/handler/dto/settings.go
@@ -121,6 +121,7 @@ type SystemSettings struct {
TablePageSizeOptions []int `json:"table_page_size_options"`
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
+ DefaultProxyID *int64 `json:"default_proxy_id"`
DefaultConcurrency int `json:"default_concurrency"`
DefaultBalance float64 `json:"default_balance"`
diff --git a/backend/internal/repository/billing_cache.go b/backend/internal/repository/billing_cache.go
index 6922b4c8bbd..044d9467105 100644
--- a/backend/internal/repository/billing_cache.go
+++ b/backend/internal/repository/billing_cache.go
@@ -33,7 +33,7 @@ func jitteredTTL() time.Duration {
if billingCacheJitter <= 0 {
return billingCacheTTL
}
- jitter := time.Duration(rand.IntN(int(billingCacheJitter)))
+ jitter := time.Duration(rand.Int64N(int64(billingCacheJitter)))
return billingCacheTTL - jitter
}
diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go
index 8eb90a6ba24..b62dd49120f 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -206,6 +206,7 @@ const (
SettingKeyTablePageSizeOptions = "table_page_size_options" // 表格可选每页条数(JSON 数组)
SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组)
SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组)
+ SettingKeyDefaultProxyID = "default_proxy_id" // 添加账号时默认选中的代理 ID
// 默认配置
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
diff --git a/backend/internal/service/gateway_request_test.go b/backend/internal/service/gateway_request_test.go
index 40bd1186728..7152edb1e0c 100644
--- a/backend/internal/service/gateway_request_test.go
+++ b/backend/internal/service/gateway_request_test.go
@@ -981,7 +981,7 @@ func TestParseGatewayRequest_MaxTokensBoundary(t *testing.T) {
{
name: "超大值不 panic",
body: `{"max_tokens":9999999999999999}`,
- wantMaxTokens: 10000000000000000, // float64 精度导致 9999999999999999 → 1e16
+ wantMaxTokens: 0, // 超出 math.MaxInt 时应被忽略,但不能 panic
},
{
name: "null 值被忽略",
diff --git a/backend/internal/service/gemini_oauth_service.go b/backend/internal/service/gemini_oauth_service.go
index 08a74a37245..50eba209d69 100644
--- a/backend/internal/service/gemini_oauth_service.go
+++ b/backend/internal/service/gemini_oauth_service.go
@@ -39,8 +39,8 @@ const (
)
const (
- GB = 1024 * 1024 * 1024
- TB = 1024 * GB
+ GB int64 = 1024 * 1024 * 1024
+ TB int64 = 1024 * GB
StorageTierUnlimited = 100 * TB // 100TB
StorageTierAIPremium = 2 * TB // 2TB
diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go
index 283a239b8d1..98ab44d6ac2 100644
--- a/backend/internal/service/setting_service.go
+++ b/backend/internal/service/setting_service.go
@@ -1504,6 +1504,11 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyTablePageSizeOptions] = string(tablePageSizeOptionsJSON)
updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
+ if settings.DefaultProxyID != nil && *settings.DefaultProxyID > 0 {
+ updates[SettingKeyDefaultProxyID] = strconv.FormatInt(*settings.DefaultProxyID, 10)
+ } else {
+ updates[SettingKeyDefaultProxyID] = ""
+ }
// 默认配置
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
@@ -2233,6 +2238,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyTablePageSizeOptions: "[10,20,50,100]",
SettingKeyCustomMenuItems: "[]",
SettingKeyCustomEndpoints: "[]",
+ SettingKeyDefaultProxyID: "",
SettingKeyWeChatConnectEnabled: "false",
SettingKeyWeChatConnectAppID: "",
SettingKeyWeChatConnectAppSecret: "",
@@ -2412,6 +2418,9 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
CustomEndpoints: settings[SettingKeyCustomEndpoints],
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
}
+ if defaultProxyID, err := strconv.ParseInt(strings.TrimSpace(settings[SettingKeyDefaultProxyID]), 10, 64); err == nil && defaultProxyID > 0 {
+ result.DefaultProxyID = &defaultProxyID
+ }
result.TableDefaultPageSize, result.TablePageSizeOptions = parseTablePreferences(
settings[SettingKeyTableDefaultPageSize],
settings[SettingKeyTablePageSizeOptions],
diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go
index 9dc0ca59a3f..56fa3425602 100644
--- a/backend/internal/service/setting_service_update_test.go
+++ b/backend/internal/service/setting_service_update_test.go
@@ -224,6 +224,39 @@ func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) {
require.Equal(t, "[20,100]", repo.updates[SettingKeyTablePageSizeOptions])
}
+func TestSettingService_UpdateSettings_DefaultProxyID(t *testing.T) {
+ repo := &settingUpdateRepoStub{}
+ svc := NewSettingService(repo, &config.Config{})
+ defaultProxyID := int64(42)
+
+ err := svc.UpdateSettings(context.Background(), &SystemSettings{
+ DefaultProxyID: &defaultProxyID,
+ })
+ require.NoError(t, err)
+ require.Equal(t, "42", repo.updates[SettingKeyDefaultProxyID])
+
+ err = svc.UpdateSettings(context.Background(), &SystemSettings{
+ DefaultProxyID: nil,
+ })
+ require.NoError(t, err)
+ require.Equal(t, "", repo.updates[SettingKeyDefaultProxyID])
+}
+
+func TestSettingService_ParseSettings_DefaultProxyID(t *testing.T) {
+ svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
+
+ got := svc.parseSettings(map[string]string{
+ SettingKeyDefaultProxyID: "88",
+ })
+ require.NotNil(t, got.DefaultProxyID)
+ require.EqualValues(t, 88, *got.DefaultProxyID)
+
+ got = svc.parseSettings(map[string]string{
+ SettingKeyDefaultProxyID: "",
+ })
+ require.Nil(t, got.DefaultProxyID)
+}
+
func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler(t *testing.T) {
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})
diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go
index 80b8b32a901..bfae8f7fdff 100644
--- a/backend/internal/service/settings_view.go
+++ b/backend/internal/service/settings_view.go
@@ -121,6 +121,7 @@ type SystemSettings struct {
TablePageSizeOptions []int
CustomMenuItems string // JSON array of custom menu items
CustomEndpoints string // JSON array of custom endpoints
+ DefaultProxyID *int64
DefaultConcurrency int
DefaultBalance float64
diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts
index 01d6969db5b..9ca8e337400 100644
--- a/frontend/src/api/admin/settings.ts
+++ b/frontend/src/api/admin/settings.ts
@@ -377,6 +377,7 @@ export interface SystemSettings {
backend_mode_enabled: boolean;
custom_menu_items: CustomMenuItem[];
custom_endpoints: CustomEndpoint[];
+ default_proxy_id?: number | null;
// SMTP settings
smtp_host: string;
smtp_port: number;
@@ -593,6 +594,7 @@ export interface UpdateSettingsRequest {
backend_mode_enabled?: boolean;
custom_menu_items?: CustomMenuItem[];
custom_endpoints?: CustomEndpoint[];
+ default_proxy_id?: number | null;
smtp_host?: string;
smtp_port?: number;
smtp_username?: string;
diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue
index 9ef6c9d2451..398aa559d80 100644
--- a/frontend/src/components/account/CreateAccountModal.vue
+++ b/frontend/src/components/account/CreateAccountModal.vue
@@ -2441,6 +2441,9 @@
+
+ {{ t('admin.accounts.defaultProxyAppliedHint') }}
+
@@ -2509,6 +2512,88 @@
+
+
+
+
+
+ {{ t('admin.accounts.openai.quotaStrategyDesc') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('admin.accounts.openai.quotaStopThresholdDesc') }}
+
+
+
+
+
('auto')
const openaiOAuthResponsesWebSocketV2Mode = ref
(OPENAI_WS_MODE_OFF)
const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false)
+const openAIQuotaStrategyEnabled = ref(true)
+const openAIQuotaStrategy = ref<'prefer_5h' | 'prefer_7d'>('prefer_7d')
+const openAIQuotaStopThresholdPercent = ref(10)
const anthropicPassthroughEnabled = ref(false)
const webSearchEmulationMode = ref('default')
const webSearchGlobalEnabled = ref(false)
+const defaultProxyId = ref(null)
const {
globalEnabled: quotaNotifyGlobalEnabled,
state: quotaNotifyState,
@@ -3538,11 +3627,50 @@ const canExchangeCode = computed(() => {
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
})
+const normalizeQuotaStopThreshold = (value: number) => {
+ if (!Number.isFinite(value)) {
+ return 10
+ }
+ return Math.min(100, Math.max(1, Math.trunc(value)))
+}
+
+const resolveDefaultProxyId = () => {
+ if (defaultProxyId.value == null) {
+ return null
+ }
+ const matched = props.proxies.find((proxy) => proxy.id === defaultProxyId.value && proxy.status === 'active')
+ return matched ? matched.id : null
+}
+
+const applyDefaultProxySelection = () => {
+ if (form.proxy_id != null) {
+ return
+ }
+ const resolved = resolveDefaultProxyId()
+ if (resolved != null) {
+ form.proxy_id = resolved
+ }
+}
+
+const loadCreateDefaults = async () => {
+ try {
+ const settings = await adminAPI.settings.getSettings()
+ defaultProxyId.value =
+ typeof settings.default_proxy_id === 'number' && settings.default_proxy_id > 0
+ ? settings.default_proxy_id
+ : null
+ applyDefaultProxySelection()
+ } catch {
+ defaultProxyId.value = null
+ }
+}
+
// Watchers
watch(
() => props.show,
(newVal) => {
if (newVal) {
+ loadCreateDefaults()
// Load TLS fingerprint profiles
adminAPI.tlsFingerprintProfiles.list()
.then(profiles => { tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name })) })
@@ -3567,6 +3695,15 @@ watch(
}
)
+watch(
+ () => props.proxies,
+ () => {
+ if (props.show) {
+ applyDefaultProxySelection()
+ }
+ }
+)
+
// Sync form.type based on accountCategory, addMethod, and platform-specific type
watch(
[accountCategory, addMethod, antigravityAccountType, () => form.platform],
@@ -3648,6 +3785,9 @@ watch(
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
+ openAIQuotaStrategyEnabled.value = true
+ openAIQuotaStrategy.value = 'prefer_7d'
+ openAIQuotaStopThresholdPercent.value = 10
}
if (newPlatform !== 'anthropic') {
anthropicPassthroughEnabled.value = false
@@ -4045,6 +4185,9 @@ const resetForm = () => {
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
+ openAIQuotaStrategyEnabled.value = true
+ openAIQuotaStrategy.value = 'prefer_7d'
+ openAIQuotaStopThresholdPercent.value = 10
anthropicPassthroughEnabled.value = false
webSearchEmulationMode.value = 'default'
// Reset quota control state
@@ -4128,6 +4271,13 @@ const buildOpenAIExtra = (base?: Record): Record 0 ? extra : undefined
}
diff --git a/frontend/src/components/account/__tests__/CreateAccountModal.spec.ts b/frontend/src/components/account/__tests__/CreateAccountModal.spec.ts
new file mode 100644
index 00000000000..470ec7c4757
--- /dev/null
+++ b/frontend/src/components/account/__tests__/CreateAccountModal.spec.ts
@@ -0,0 +1,211 @@
+import { defineComponent, ref } from 'vue'
+import { flushPromises, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import CreateAccountModal from '../CreateAccountModal.vue'
+
+const {
+ getSettingsMock,
+ getWebSearchEmulationConfigMock,
+ listTlsProfilesMock
+} = vi.hoisted(() => ({
+ getSettingsMock: vi.fn(),
+ getWebSearchEmulationConfigMock: vi.fn(),
+ listTlsProfilesMock: vi.fn()
+}))
+
+vi.mock('@/stores/app', () => ({
+ useAppStore: () => ({
+ showError: vi.fn(),
+ showSuccess: vi.fn(),
+ showInfo: vi.fn()
+ })
+}))
+
+vi.mock('@/stores/auth', () => ({
+ useAuthStore: () => ({
+ isSimpleMode: false
+ })
+}))
+
+vi.mock('@/composables/useModelWhitelist', () => ({
+ claudeModels: ['claude-3-7-sonnet'],
+ getPresetMappingsByPlatform: vi.fn(() => []),
+ getModelsByPlatform: vi.fn(() => ['claude-3-7-sonnet']),
+ commonErrorCodes: [],
+ buildModelMappingObject: vi.fn(() => undefined),
+ fetchAntigravityDefaultMappings: vi.fn().mockResolvedValue([]),
+ isValidWildcardPattern: vi.fn(() => true)
+}))
+
+vi.mock('@/composables/useQuotaNotifyState', () => ({
+ useQuotaNotifyState: () => ({
+ globalEnabled: ref(false),
+ state: ref({}),
+ loadGlobalState: vi.fn(),
+ writeToExtra: vi.fn()
+ })
+}))
+
+function createOAuthStub() {
+ return {
+ authUrl: ref(''),
+ sessionId: ref(''),
+ loading: ref(false),
+ error: ref(''),
+ resetState: vi.fn(),
+ parseSessionKeys: vi.fn(() => []),
+ getCapabilities: vi.fn().mockResolvedValue({ ai_studio_oauth_enabled: false })
+ }
+}
+
+vi.mock('@/composables/useAccountOAuth', () => ({
+ useAccountOAuth: () => createOAuthStub()
+}))
+
+vi.mock('@/composables/useOpenAIOAuth', () => ({
+ useOpenAIOAuth: () => createOAuthStub()
+}))
+
+vi.mock('@/composables/useGeminiOAuth', () => ({
+ useGeminiOAuth: () => createOAuthStub()
+}))
+
+vi.mock('@/composables/useAntigravityOAuth', () => ({
+ useAntigravityOAuth: () => createOAuthStub()
+}))
+
+vi.mock('@/api/admin', () => ({
+ adminAPI: {
+ settings: {
+ getSettings: getSettingsMock,
+ getWebSearchEmulationConfig: getWebSearchEmulationConfigMock
+ },
+ tlsFingerprintProfiles: {
+ list: listTlsProfilesMock
+ }
+ }
+}))
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual('vue-i18n')
+ return {
+ ...actual,
+ useI18n: () => ({
+ t: (key: string) => key
+ })
+ }
+})
+
+const BaseDialogStub = defineComponent({
+ name: 'BaseDialogStub',
+ props: {
+ show: {
+ type: Boolean,
+ default: false
+ }
+ },
+ template: '
'
+})
+
+const ProxySelectorStub = defineComponent({
+ name: 'ProxySelectorStub',
+ props: {
+ modelValue: {
+ type: [Number, null],
+ default: null
+ }
+ },
+ emits: ['update:modelValue'],
+ template: '{{ modelValue ?? "" }}
'
+})
+
+const SelectStub = defineComponent({
+ name: 'SelectStub',
+ props: {
+ modelValue: {
+ type: [String, Number, Boolean, null],
+ default: ''
+ },
+ options: {
+ type: Array,
+ default: () => []
+ }
+ },
+ emits: ['update:modelValue'],
+ template: `
+
+ `
+})
+
+const createWrapper = () =>
+ mount(CreateAccountModal, {
+ props: {
+ show: false,
+ proxies: [
+ {
+ id: 7,
+ name: 'Default Proxy',
+ protocol: 'http',
+ host: 'proxy.example.com',
+ port: 8080,
+ username: null,
+ password: null,
+ status: 'active',
+ created_at: '2026-04-27T00:00:00Z',
+ updated_at: '2026-04-27T00:00:00Z'
+ }
+ ] as any,
+ groups: []
+ },
+ global: {
+ stubs: {
+ BaseDialog: BaseDialogStub,
+ ConfirmDialog: true,
+ Select: SelectStub,
+ Icon: true,
+ ProxySelector: ProxySelectorStub,
+ GroupSelector: true,
+ ModelWhitelistSelector: true,
+ QuotaLimitCard: true,
+ OAuthAuthorizationFlow: true
+ }
+ }
+ })
+
+describe('CreateAccountModal', () => {
+ beforeEach(() => {
+ getSettingsMock.mockReset()
+ getWebSearchEmulationConfigMock.mockReset()
+ listTlsProfilesMock.mockReset()
+
+ getSettingsMock.mockResolvedValue({ default_proxy_id: 7 })
+ getWebSearchEmulationConfigMock.mockResolvedValue({ enabled: false, providers: [] })
+ listTlsProfilesMock.mockResolvedValue([])
+ })
+
+ it('打开时自动选中默认代理,并在 OpenAI OAuth 下默认启用 7D/10% 额度策略', async () => {
+ const wrapper = createWrapper()
+
+ await wrapper.setProps({ show: true })
+ await flushPromises()
+
+ expect(wrapper.get('[data-testid="proxy-selector"]').text()).toBe('7')
+ expect(wrapper.text()).toContain('admin.accounts.defaultProxyAppliedHint')
+
+ const openAIButton = wrapper
+ .findAll('button')
+ .find((button) => button.text().includes('OpenAI'))
+
+ expect(openAIButton).toBeTruthy()
+ await openAIButton!.trigger('click')
+ await flushPromises()
+
+ expect((wrapper.get('#create-openai-quota-stop-threshold').element as HTMLInputElement).value).toBe('10')
+ expect(wrapper.text()).toContain('admin.accounts.openai.quotaStrategyPrefer7d')
+ })
+})
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index d18a895c007..3f98990cbc1 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -3335,6 +3335,7 @@ export default {
expired: 'Expired',
proxy: 'Proxy',
noProxy: 'No Proxy',
+ defaultProxyAppliedHint: 'Default proxy has been preselected. You can still change it manually.',
concurrency: 'Concurrency',
loadFactor: 'Load Factor',
loadFactorHint: 'Higher load factor increases scheduling frequency',
@@ -3930,6 +3931,13 @@ export default {
batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}',
batchDeleteSkipped: 'Skipped {skipped} proxies',
batchDeleteFailed: 'Batch delete failed',
+ defaultBadge: 'Default Proxy',
+ defaultBadgeShort: 'Default',
+ setAsDefault: 'Set Default',
+ settingDefault: 'Saving...',
+ defaultProxySet: 'Default proxy updated',
+ failedToSetDefault: 'Failed to set default proxy',
+ failedToClearDefault: 'Failed to clear default proxy',
deleteBlockedInUse: 'This proxy is in use and cannot be deleted',
accountsTitle: 'Accounts using this IP',
accountsEmpty: 'No accounts are using this proxy',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 4f473f946b7..6c49b9c9132 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -3476,6 +3476,7 @@ export default {
expired: '已过期',
proxy: '代理',
noProxy: '无代理',
+ defaultProxyAppliedHint: '已自动选中默认代理,仍可手动切换。',
concurrency: '并发数',
loadFactor: '负载因子',
loadFactorHint: '提高负载因子可以提高对账号的调度频率',
@@ -4064,6 +4065,13 @@ export default {
batchDeleteDone: '已删除 {deleted} 个代理,跳过 {skipped} 个',
batchDeleteSkipped: '已跳过 {skipped} 个代理',
batchDeleteFailed: '批量删除失败',
+ defaultBadge: '默认代理',
+ defaultBadgeShort: '默认',
+ setAsDefault: '设默认',
+ settingDefault: '设置中',
+ defaultProxySet: '默认代理已更新',
+ failedToSetDefault: '设置默认代理失败',
+ failedToClearDefault: '清理默认代理失败',
deleteBlockedInUse: '该代理已有账号使用,无法删除',
accountsTitle: '使用该IP的账号',
accountsEmpty: '暂无账号使用此代理',
diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue
index 1e4df356096..8d2c4bd79f0 100644
--- a/frontend/src/views/admin/ProxiesView.vue
+++ b/frontend/src/views/admin/ProxiesView.vue
@@ -118,8 +118,16 @@
/>
-
- {{ value }}
+
+
+ {{ value }}
+
+ {{ t('admin.proxies.defaultBadge') }}
+
+
@@ -252,6 +260,26 @@
+