"git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "785a7397f864a377cf5d4fc573df85299cdbf905"
Unverified Commit 474165d7 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1043 from touwaeriol/pr/antigravity-credits-overages

feat: Antigravity AI Credits overages handling & balance display
parents 94e067a2 552a4b99
import { describe, expect, it, vi, beforeEach } from 'vitest' import { describe, expect, it, vi, beforeEach } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils' import { flushPromises, mount } from '@vue/test-utils'
import AccountUsageCell from '../AccountUsageCell.vue' import AccountUsageCell from '../AccountUsageCell.vue'
import type { Account } from '@/types'
const { getUsage } = vi.hoisted(() => ({ const { getUsage } = vi.hoisted(() => ({
getUsage: vi.fn() getUsage: vi.fn()
...@@ -24,6 +25,35 @@ vi.mock('vue-i18n', async () => { ...@@ -24,6 +25,35 @@ vi.mock('vue-i18n', async () => {
} }
}) })
function makeAccount(overrides: Partial<Account>): Account {
return {
id: 1,
name: 'account',
platform: 'antigravity',
type: 'oauth',
proxy_id: null,
concurrency: 1,
priority: 1,
status: 'active',
error_message: null,
last_used_at: null,
expires_at: null,
auto_pause_on_expired: true,
created_at: '2026-03-15T00:00:00Z',
updated_at: '2026-03-15T00:00:00Z',
schedulable: true,
rate_limited_at: null,
rate_limit_reset_at: null,
overload_until: null,
temp_unschedulable_until: null,
temp_unschedulable_reason: null,
session_window_start: null,
session_window_end: null,
session_window_status: null,
...overrides,
}
}
describe('AccountUsageCell', () => { describe('AccountUsageCell', () => {
beforeEach(() => { beforeEach(() => {
getUsage.mockReset() getUsage.mockReset()
...@@ -49,12 +79,12 @@ describe('AccountUsageCell', () => { ...@@ -49,12 +79,12 @@ describe('AccountUsageCell', () => {
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 1001, id: 1001,
platform: 'antigravity', platform: 'antigravity',
type: 'oauth', type: 'oauth',
extra: {} extra: {}
} as any })
}, },
global: { global: {
stubs: { stubs: {
...@@ -72,6 +102,40 @@ describe('AccountUsageCell', () => { ...@@ -72,6 +102,40 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('admin.accounts.usageWindow.gemini3Image|70|2026-03-01T09:00:00Z') expect(wrapper.text()).toContain('admin.accounts.usageWindow.gemini3Image|70|2026-03-01T09:00:00Z')
}) })
it('Antigravity 会显示 AI Credits 余额信息', async () => {
getUsage.mockResolvedValue({
ai_credits: [
{
credit_type: 'GOOGLE_ONE_AI',
amount: 25,
minimum_balance: 5
}
]
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 1002,
platform: 'antigravity',
type: 'oauth',
extra: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.aiCreditsBalance')
expect(wrapper.text()).toContain('25')
})
it('OpenAI OAuth 快照已过期时首屏会重新请求 usage', async () => { it('OpenAI OAuth 快照已过期时首屏会重新请求 usage', async () => {
getUsage.mockResolvedValue({ getUsage.mockResolvedValue({
...@@ -103,7 +167,7 @@ describe('AccountUsageCell', () => { ...@@ -103,7 +167,7 @@ describe('AccountUsageCell', () => {
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 2000, id: 2000,
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
...@@ -114,7 +178,7 @@ describe('AccountUsageCell', () => { ...@@ -114,7 +178,7 @@ describe('AccountUsageCell', () => {
codex_7d_used_percent: 34, codex_7d_used_percent: 34,
codex_7d_reset_at: '2026-03-13T12:00:00Z' codex_7d_reset_at: '2026-03-13T12:00:00Z'
} }
} as any })
}, },
global: { global: {
stubs: { stubs: {
...@@ -137,7 +201,7 @@ describe('AccountUsageCell', () => { ...@@ -137,7 +201,7 @@ describe('AccountUsageCell', () => {
it('OpenAI OAuth 有现成快照且未限额时不会首屏请求 usage', async () => { it('OpenAI OAuth 有现成快照且未限额时不会首屏请求 usage', async () => {
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 2001, id: 2001,
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
...@@ -148,7 +212,7 @@ describe('AccountUsageCell', () => { ...@@ -148,7 +212,7 @@ describe('AccountUsageCell', () => {
codex_7d_used_percent: 34, codex_7d_used_percent: 34,
codex_7d_reset_at: '2099-03-13T12:00:00Z' codex_7d_reset_at: '2099-03-13T12:00:00Z'
} }
} as any })
}, },
global: { global: {
stubs: { stubs: {
...@@ -196,15 +260,15 @@ describe('AccountUsageCell', () => { ...@@ -196,15 +260,15 @@ describe('AccountUsageCell', () => {
} }
}) })
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 2002, id: 2002,
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
extra: {} extra: {}
} as any })
}, },
global: { global: {
stubs: { stubs: {
UsageProgressBar: { UsageProgressBar: {
...@@ -256,16 +320,16 @@ describe('AccountUsageCell', () => { ...@@ -256,16 +320,16 @@ describe('AccountUsageCell', () => {
seven_day: null seven_day: null
}) })
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 2003, id: 2003,
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
updated_at: '2026-03-07T10:00:00Z', updated_at: '2026-03-07T10:00:00Z',
extra: {} extra: {}
} as any })
}, },
global: { global: {
stubs: { stubs: {
UsageProgressBar: { UsageProgressBar: {
...@@ -324,19 +388,19 @@ describe('AccountUsageCell', () => { ...@@ -324,19 +388,19 @@ describe('AccountUsageCell', () => {
} }
}) })
const wrapper = mount(AccountUsageCell, { const wrapper = mount(AccountUsageCell, {
props: { props: {
account: { account: makeAccount({
id: 2004, id: 2004,
platform: 'openai', platform: 'openai',
type: 'oauth', type: 'oauth',
rate_limit_reset_at: '2099-03-07T12:00:00Z', rate_limit_reset_at: '2099-03-07T12:00:00Z',
extra: { extra: {
codex_5h_used_percent: 0, codex_5h_used_percent: 0,
codex_7d_used_percent: 0 codex_7d_used_percent: 0
} }
} as any })
}, },
global: { global: {
stubs: { stubs: {
UsageProgressBar: { UsageProgressBar: {
......
...@@ -245,6 +245,7 @@ export default { ...@@ -245,6 +245,7 @@ export default {
// Common // Common
common: { common: {
loading: 'Loading...', loading: 'Loading...',
justNow: 'just now',
save: 'Save', save: 'Save',
cancel: 'Cancel', cancel: 'Cancel',
delete: 'Delete', delete: 'Delete',
...@@ -1655,6 +1656,14 @@ export default { ...@@ -1655,6 +1656,14 @@ export default {
enabled: 'Enabled', enabled: 'Enabled',
disabled: 'Disabled' disabled: 'Disabled'
}, },
claudeMaxSimulation: {
title: 'Claude Max Usage Simulation',
tooltip:
'When enabled, for Claude models without upstream cache-write usage, the system deterministically maps tokens to a small input plus 1h cache creation while keeping total tokens unchanged.',
enabled: 'Enabled (simulate 1h cache)',
disabled: 'Disabled',
hint: 'Only token categories in usage billing logs are adjusted. No per-request mapping state is persisted.'
},
supportedScopes: { supportedScopes: {
title: 'Supported Model Families', title: 'Supported Model Families',
tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.',
...@@ -1867,6 +1876,9 @@ export default { ...@@ -1867,6 +1876,9 @@ export default {
rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}', rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}',
rateLimitedAutoResume: 'Auto resumes in {time}', rateLimitedAutoResume: 'Auto resumes in {time}',
modelRateLimitedUntil: '{model} rate limited until {time}', modelRateLimitedUntil: '{model} rate limited until {time}',
modelCreditOveragesUntil: '{model} using AI Credits until {time}',
creditsExhausted: 'Credits Exhausted',
creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}',
overloadedUntil: 'Overloaded until {time}', overloadedUntil: 'Overloaded until {time}',
viewTempUnschedDetails: 'View temp unschedulable details' viewTempUnschedDetails: 'View temp unschedulable details'
}, },
...@@ -1968,7 +1980,7 @@ export default { ...@@ -1968,7 +1980,7 @@ export default {
resetQuota: 'Reset Quota', resetQuota: 'Reset Quota',
quotaLimit: 'Quota Limit', quotaLimit: 'Quota Limit',
quotaLimitPlaceholder: '0 means unlimited', quotaLimitPlaceholder: '0 means unlimited',
quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Account will be paused when any limit is reached. Changing limits won\'t reset usage.', quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.',
quotaLimitToggle: 'Enable Quota Limit', quotaLimitToggle: 'Enable Quota Limit',
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
quotaDailyLimit: 'Daily Limit', quotaDailyLimit: 'Daily Limit',
...@@ -2165,7 +2177,7 @@ export default { ...@@ -2165,7 +2177,7 @@ export default {
// Quota control (Anthropic OAuth/SetupToken only) // Quota control (Anthropic OAuth/SetupToken only)
quotaControl: { quotaControl: {
title: 'Quota Control', title: 'Quota Control',
hint: 'Only applies to Anthropic OAuth/Setup Token accounts', hint: 'Configure cost window, session limits, client affinity and other scheduling controls.',
windowCost: { windowCost: {
label: '5h Window Cost Limit', label: '5h Window Cost Limit',
hint: 'Limit account cost usage within the 5-hour window', hint: 'Limit account cost usage within the 5-hour window',
...@@ -2220,8 +2232,26 @@ export default { ...@@ -2220,8 +2232,26 @@ export default {
hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)', hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)',
target: 'Target TTL', target: 'Target TTL',
targetHint: 'Select the TTL tier for billing' targetHint: 'Select the TTL tier for billing'
},
clientAffinity: {
label: 'Client Affinity Scheduling',
hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching'
} }
}, },
affinityNoClients: 'No affinity clients',
affinityClients: '{count} affinity clients:',
affinitySection: 'Client Affinity',
affinitySectionHint: 'Control how clients are distributed across accounts. Configure zone thresholds to balance load.',
affinityToggle: 'Enable Client Affinity',
affinityToggleHint: 'New sessions prefer accounts previously used by this client',
affinityBase: 'Base Limit (Green Zone)',
affinityBasePlaceholder: 'Empty = no limit',
affinityBaseHint: 'Max clients in green zone (full priority scheduling)',
affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.',
affinityBuffer: 'Buffer (Yellow Zone)',
affinityBufferPlaceholder: 'e.g. 3',
affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)',
affinityBufferInfinite: 'Unlimited',
expired: 'Expired', expired: 'Expired',
proxy: 'Proxy', proxy: 'Proxy',
noProxy: 'No Proxy', noProxy: 'No Proxy',
...@@ -2239,6 +2269,10 @@ export default { ...@@ -2239,6 +2269,10 @@ export default {
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling', mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
mixedSchedulingTooltip: mixedSchedulingTooltip:
'!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!', '!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!',
aiCreditsBalance: 'AI Credits',
allowOverages: 'Allow Overages (AI Credits)',
allowOveragesTooltip:
'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.',
creating: 'Creating...', creating: 'Creating...',
updating: 'Updating...', updating: 'Updating...',
accountCreated: 'Account created successfully', accountCreated: 'Account created successfully',
...@@ -2672,7 +2706,7 @@ export default { ...@@ -2672,7 +2706,7 @@ export default {
geminiFlashDaily: 'Flash', geminiFlashDaily: 'Flash',
gemini3Pro: 'G3P', gemini3Pro: 'G3P',
gemini3Flash: 'G3F', gemini3Flash: 'G3F',
gemini3Image: 'GImage', gemini3Image: 'G31FI',
claude: 'Claude' claude: 'Claude'
}, },
tier: { tier: {
...@@ -4185,40 +4219,55 @@ export default { ...@@ -4185,40 +4219,55 @@ export default {
usage: 'Usage: Add to request header - x-api-key: <your-admin-api-key>' usage: 'Usage: Add to request header - x-api-key: <your-admin-api-key>'
}, },
soraS3: { soraS3: {
title: 'Sora S3 Storage', title: 'Sora Storage',
description: 'Manage multiple Sora S3 endpoints and switch the active profile', description: 'Manage Sora media storage profiles with S3 and Google Drive support',
newProfile: 'New Profile', newProfile: 'New Profile',
reloadProfiles: 'Reload Profiles', reloadProfiles: 'Reload Profiles',
empty: 'No Sora S3 profiles yet, create one first', empty: 'No storage profiles yet, create one first',
createTitle: 'Create Sora S3 Profile', createTitle: 'Create Storage Profile',
editTitle: 'Edit Sora S3 Profile', editTitle: 'Edit Storage Profile',
selectProvider: 'Select Storage Type',
providerS3Desc: 'S3-compatible object storage',
providerGDriveDesc: 'Google Drive cloud storage',
profileID: 'Profile ID', profileID: 'Profile ID',
profileName: 'Profile Name', profileName: 'Profile Name',
setActive: 'Set as active after creation', setActive: 'Set as active after creation',
saveProfile: 'Save Profile', saveProfile: 'Save Profile',
activateProfile: 'Activate', activateProfile: 'Activate',
profileCreated: 'Sora S3 profile created', profileCreated: 'Storage profile created',
profileSaved: 'Sora S3 profile saved', profileSaved: 'Storage profile saved',
profileDeleted: 'Sora S3 profile deleted', profileDeleted: 'Storage profile deleted',
profileActivated: 'Sora S3 active profile switched', profileActivated: 'Active storage profile switched',
profileIDRequired: 'Profile ID is required', profileIDRequired: 'Profile ID is required',
profileNameRequired: 'Profile name is required', profileNameRequired: 'Profile name is required',
profileSelectRequired: 'Please select a profile first', profileSelectRequired: 'Please select a profile first',
endpointRequired: 'S3 endpoint is required when enabled', endpointRequired: 'S3 endpoint is required when enabled',
bucketRequired: 'Bucket is required when enabled', bucketRequired: 'Bucket is required when enabled',
accessKeyRequired: 'Access Key ID is required when enabled', accessKeyRequired: 'Access Key ID is required when enabled',
deleteConfirm: 'Delete Sora S3 profile {profileID}?', deleteConfirm: 'Delete storage profile {profileID}?',
columns: { columns: {
profile: 'Profile', profile: 'Profile',
profileId: 'Profile ID',
name: 'Name',
provider: 'Type',
active: 'Active', active: 'Active',
endpoint: 'Endpoint', endpoint: 'Endpoint',
bucket: 'Bucket', storagePath: 'Storage Path',
capacityUsage: 'Capacity / Used',
capacityUnlimited: 'Unlimited',
videoCount: 'Videos',
videoCompleted: 'completed',
videoInProgress: 'in progress',
quota: 'Default Quota', quota: 'Default Quota',
updatedAt: 'Updated At', updatedAt: 'Updated At',
actions: 'Actions' actions: 'Actions',
rootFolder: 'Root folder',
testInTable: 'Test',
testingInTable: 'Testing...',
testTimeout: 'Test timed out (15s)'
}, },
enabled: 'Enable S3 Storage', enabled: 'Enable Storage',
enabledHint: 'When enabled, Sora generated media files will be automatically uploaded to S3 storage', enabledHint: 'When enabled, Sora generated media files will be automatically uploaded',
endpoint: 'S3 Endpoint', endpoint: 'S3 Endpoint',
region: 'Region', region: 'Region',
bucket: 'Bucket', bucket: 'Bucket',
...@@ -4227,16 +4276,38 @@ export default { ...@@ -4227,16 +4276,38 @@ export default {
secretAccessKey: 'Secret Access Key', secretAccessKey: 'Secret Access Key',
secretConfigured: '(Configured, leave blank to keep)', secretConfigured: '(Configured, leave blank to keep)',
cdnUrl: 'CDN URL', cdnUrl: 'CDN URL',
cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL instead of presigned URLs', cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL',
forcePathStyle: 'Force Path Style', forcePathStyle: 'Force Path Style',
defaultQuota: 'Default Storage Quota', defaultQuota: 'Default Storage Quota',
defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited',
testConnection: 'Test Connection', testConnection: 'Test Connection',
testing: 'Testing...', testing: 'Testing...',
testSuccess: 'S3 connection test successful', testSuccess: 'Connection test successful',
testFailed: 'S3 connection test failed', testFailed: 'Connection test failed',
saved: 'Sora S3 settings saved successfully', saved: 'Storage settings saved successfully',
saveFailed: 'Failed to save Sora S3 settings' saveFailed: 'Failed to save storage settings',
gdrive: {
authType: 'Authentication Method',
serviceAccount: 'Service Account',
clientId: 'Client ID',
clientSecret: 'Client Secret',
clientSecretConfigured: '(Configured, leave blank to keep)',
refreshToken: 'Refresh Token',
refreshTokenConfigured: '(Configured, leave blank to keep)',
serviceAccountJson: 'Service Account JSON',
serviceAccountConfigured: '(Configured, leave blank to keep)',
folderId: 'Folder ID (optional)',
authorize: 'Authorize Google Drive',
authorizeHint: 'Get Refresh Token via OAuth2',
oauthFieldsRequired: 'Please fill in Client ID and Client Secret first',
oauthSuccess: 'Google Drive authorization successful',
oauthFailed: 'Google Drive authorization failed',
closeWindow: 'This window will close automatically',
processing: 'Processing authorization...',
testStorage: 'Test Storage',
testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)',
testFailed: 'Google Drive storage test failed'
}
}, },
streamTimeout: { streamTimeout: {
title: 'Stream Timeout Handling', title: 'Stream Timeout Handling',
...@@ -4707,6 +4778,7 @@ export default { ...@@ -4707,6 +4778,7 @@ export default {
downloadLocal: 'Download', downloadLocal: 'Download',
canDownload: 'to download', canDownload: 'to download',
regenrate: 'Regenerate', regenrate: 'Regenerate',
regenerate: 'Regenerate',
creatorPlaceholder: 'Describe the video or image you want to create...', creatorPlaceholder: 'Describe the video or image you want to create...',
videoModels: 'Video Models', videoModels: 'Video Models',
imageModels: 'Image Models', imageModels: 'Image Models',
...@@ -4723,6 +4795,13 @@ export default { ...@@ -4723,6 +4795,13 @@ export default {
galleryEmptyTitle: 'No works yet', galleryEmptyTitle: 'No works yet',
galleryEmptyDesc: 'Your creations will be displayed here. Go to the generate page to start your first creation.', galleryEmptyDesc: 'Your creations will be displayed here. Go to the generate page to start your first creation.',
startCreating: 'Start Creating', startCreating: 'Start Creating',
yesterday: 'Yesterday' yesterday: 'Yesterday',
landscape: 'Landscape',
portrait: 'Portrait',
square: 'Square',
examplePrompt1: 'A golden Shiba Inu walking through the streets of Shibuya, Tokyo, camera following, cinematic shot, 4K',
examplePrompt2: 'Drone aerial view, green aurora reflecting on a glacial lake in Iceland, slow push-in',
examplePrompt3: 'Cyberpunk futuristic city, neon lights reflected in rain puddles, nightscape, cinematic colors',
examplePrompt4: 'Chinese ink painting style, a small boat drifting among misty mountains and rivers, classical atmosphere'
} }
} }
...@@ -245,6 +245,7 @@ export default { ...@@ -245,6 +245,7 @@ export default {
// Common // Common
common: { common: {
loading: '加载中...', loading: '加载中...',
justNow: '刚刚',
save: '保存', save: '保存',
cancel: '取消', cancel: '取消',
delete: '删除', delete: '删除',
...@@ -1974,7 +1975,7 @@ export default { ...@@ -1974,7 +1975,7 @@ export default {
resetQuota: '重置配额', resetQuota: '重置配额',
quotaLimit: '配额限制', quotaLimit: '配额限制',
quotaLimitPlaceholder: '0 表示不限制', quotaLimitPlaceholder: '0 表示不限制',
quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。修改限额不会重置已用额度。', quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。Anthropic API Key 账号还可配置客户端亲和。修改限额不会重置已用额度。',
quotaLimitToggle: '启用配额限制', quotaLimitToggle: '启用配额限制',
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度', quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
quotaDailyLimit: '日限额', quotaDailyLimit: '日限额',
...@@ -2052,6 +2053,9 @@ export default { ...@@ -2052,6 +2053,9 @@ export default {
rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复', rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复',
rateLimitedAutoResume: '{time} 自动恢复', rateLimitedAutoResume: '{time} 自动恢复',
modelRateLimitedUntil: '{model} 限流至 {time}', modelRateLimitedUntil: '{model} 限流至 {time}',
modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}',
creditsExhausted: '积分已用尽',
creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复',
overloadedUntil: '负载过重,重置时间:{time}', overloadedUntil: '负载过重,重置时间:{time}',
viewTempUnschedDetails: '查看临时不可调度详情' viewTempUnschedDetails: '查看临时不可调度详情'
}, },
...@@ -2105,7 +2109,7 @@ export default { ...@@ -2105,7 +2109,7 @@ export default {
geminiFlashDaily: 'Flash', geminiFlashDaily: 'Flash',
gemini3Pro: 'G3P', gemini3Pro: 'G3P',
gemini3Flash: 'G3F', gemini3Flash: 'G3F',
gemini3Image: 'GImage', gemini3Image: 'G31FI',
claude: 'Claude' claude: 'Claude'
}, },
tier: { tier: {
...@@ -2315,7 +2319,7 @@ export default { ...@@ -2315,7 +2319,7 @@ export default {
// Quota control (Anthropic OAuth/SetupToken only) // Quota control (Anthropic OAuth/SetupToken only)
quotaControl: { quotaControl: {
title: '配额控制', title: '配额控制',
hint: '仅适用于 Anthropic OAuth/Setup Token 账号', hint: '配置费用窗口、会话限制、客户端亲和等调度控制。',
windowCost: { windowCost: {
label: '5h窗口费用控制', label: '5h窗口费用控制',
hint: '限制账号在5小时窗口内的费用使用', hint: '限制账号在5小时窗口内的费用使用',
...@@ -2370,8 +2374,26 @@ export default { ...@@ -2370,8 +2374,26 @@ export default {
hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费', hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费',
target: '目标 TTL', target: '目标 TTL',
targetHint: '选择计费使用的 TTL 类型' targetHint: '选择计费使用的 TTL 类型'
},
clientAffinity: {
label: '客户端亲和调度',
hint: '启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号'
} }
}, },
affinityNoClients: '无亲和客户端',
affinityClients: '{count} 个亲和客户端:',
affinitySection: '客户端亲和',
affinitySectionHint: '控制客户端在账号间的分布。通过配置区域阈值来平衡负载。',
affinityToggle: '启用客户端亲和',
affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号',
affinityBase: '基础限额(绿区)',
affinityBasePlaceholder: '留空表示不限制',
affinityBaseHint: '绿区最大客户端数量(完整优先级调度)',
affinityBaseOffHint: '未开启绿区限制,所有客户端均享受完整优先级调度',
affinityBuffer: '缓冲区(黄区)',
affinityBufferPlaceholder: '例如 3',
affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)',
affinityBufferInfinite: '不限制',
expired: '已过期', expired: '已过期',
proxy: '代理', proxy: '代理',
noProxy: '无代理', noProxy: '无代理',
...@@ -2389,6 +2411,10 @@ export default { ...@@ -2389,6 +2411,10 @@ export default {
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度', mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
mixedSchedulingTooltip: mixedSchedulingTooltip:
'!!注意!! Antigravity Claude 和 Anthropic Claude 无法在同个上下文中使用,如果你同时有 Anthropic 账号和 Antigravity 账号,开启此选项会导致经常 400 报错。开启后,请用分组功能做好 Antigravity 账号和 Anthropic 账号的隔离。一定要弄明白再开启!!', '!!注意!! Antigravity Claude 和 Anthropic Claude 无法在同个上下文中使用,如果你同时有 Anthropic 账号和 Antigravity 账号,开启此选项会导致经常 400 报错。开启后,请用分组功能做好 Antigravity 账号和 Anthropic 账号的隔离。一定要弄明白再开启!!',
aiCreditsBalance: 'AI Credits',
allowOverages: '允许超量请求 (AI Credits)',
allowOveragesTooltip:
'仅在免费配额被明确判定为耗尽后才会使用 AI Credits。普通并发 429 限流不会切换到超量请求。',
creating: '创建中...', creating: '创建中...',
updating: '更新中...', updating: '更新中...',
accountCreated: '账号创建成功', accountCreated: '账号创建成功',
...@@ -4358,40 +4384,55 @@ export default { ...@@ -4358,40 +4384,55 @@ export default {
usage: '使用方法:在请求头中添加 x-api-key: <your-admin-api-key>' usage: '使用方法:在请求头中添加 x-api-key: <your-admin-api-key>'
}, },
soraS3: { soraS3: {
title: 'Sora S3 存储配置', title: 'Sora 存储配置',
description: '以多配置列表方式管理 Sora S3 端点,并可切换生效配置', description: '以多配置列表管理 Sora 媒体存储,支持 S3 和 Google Drive',
newProfile: '新建配置', newProfile: '新建配置',
reloadProfiles: '刷新列表', reloadProfiles: '刷新列表',
empty: '暂无 Sora S3 配置,请先创建', empty: '暂无存储配置,请先创建',
createTitle: '新建 Sora S3 配置', createTitle: '新建存储配置',
editTitle: '编辑 Sora S3 配置', editTitle: '编辑存储配置',
selectProvider: '选择存储类型',
providerS3Desc: 'S3 兼容对象存储',
providerGDriveDesc: 'Google Drive 云盘',
profileID: '配置 ID', profileID: '配置 ID',
profileName: '配置名称', profileName: '配置名称',
setActive: '创建后设为生效', setActive: '创建后设为生效',
saveProfile: '保存配置', saveProfile: '保存配置',
activateProfile: '设为生效', activateProfile: '设为生效',
profileCreated: 'Sora S3 配置创建成功', profileCreated: '存储配置创建成功',
profileSaved: 'Sora S3 配置保存成功', profileSaved: '存储配置保存成功',
profileDeleted: 'Sora S3 配置删除成功', profileDeleted: '存储配置删除成功',
profileActivated: 'Sora S3 生效配置已切换', profileActivated: '生效配置已切换',
profileIDRequired: '请填写配置 ID', profileIDRequired: '请填写配置 ID',
profileNameRequired: '请填写配置名称', profileNameRequired: '请填写配置名称',
profileSelectRequired: '请先选择配置', profileSelectRequired: '请先选择配置',
endpointRequired: '启用时必须填写 S3 端点', endpointRequired: '启用时必须填写 S3 端点',
bucketRequired: '启用时必须填写存储桶', bucketRequired: '启用时必须填写存储桶',
accessKeyRequired: '启用时必须填写 Access Key ID', accessKeyRequired: '启用时必须填写 Access Key ID',
deleteConfirm: '确定删除 Sora S3 配置 {profileID} 吗?', deleteConfirm: '确定删除存储配置 {profileID} 吗?',
columns: { columns: {
profile: '配置', profile: '配置',
profileId: 'Profile ID',
name: '名称',
provider: '存储类型',
active: '生效状态', active: '生效状态',
endpoint: '端点', endpoint: '端点',
bucket: '存储桶', storagePath: '存储路径',
capacityUsage: '容量 / 已用',
capacityUnlimited: '无限制',
videoCount: '视频数',
videoCompleted: '完成',
videoInProgress: '进行中',
quota: '默认配额', quota: '默认配额',
updatedAt: '更新时间', updatedAt: '更新时间',
actions: '操作' actions: '操作',
rootFolder: '根目录',
testInTable: '测试',
testingInTable: '测试中...',
testTimeout: '测试超时(15秒)'
}, },
enabled: '启用 S3 存储', enabled: '启用存储',
enabledHint: '启用后,Sora 生成的媒体文件将自动上传到 S3 存储', enabledHint: '启用后,Sora 生成的媒体文件将自动上传到存储',
endpoint: 'S3 端点', endpoint: 'S3 端点',
region: '区域', region: '区域',
bucket: '存储桶', bucket: '存储桶',
...@@ -4400,16 +4441,38 @@ export default { ...@@ -4400,16 +4441,38 @@ export default {
secretAccessKey: 'Secret Access Key', secretAccessKey: 'Secret Access Key',
secretConfigured: '(已配置,留空保持不变)', secretConfigured: '(已配置,留空保持不变)',
cdnUrl: 'CDN URL', cdnUrl: 'CDN URL',
cdnUrlHint: '可选,配置后使用 CDN URL 访问文件,否则使用预签名 URL', cdnUrlHint: '可选,配置后使用 CDN URL 访问文件',
forcePathStyle: '强制路径风格(Path Style)', forcePathStyle: '强制路径风格(Path Style)',
defaultQuota: '默认存储配额', defaultQuota: '默认存储配额',
defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制', defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制',
testConnection: '测试连接', testConnection: '测试连接',
testing: '测试中...', testing: '测试中...',
testSuccess: 'S3 连接测试成功', testSuccess: '连接测试成功',
testFailed: 'S3 连接测试失败', testFailed: '连接测试失败',
saved: 'Sora S3 设置保存成功', saved: '存储设置保存成功',
saveFailed: '保存 Sora S3 设置失败' saveFailed: '保存存储设置失败',
gdrive: {
authType: '认证方式',
serviceAccount: '服务账号',
clientId: 'Client ID',
clientSecret: 'Client Secret',
clientSecretConfigured: '(已配置,留空保持不变)',
refreshToken: 'Refresh Token',
refreshTokenConfigured: '(已配置,留空保持不变)',
serviceAccountJson: '服务账号 JSON',
serviceAccountConfigured: '(已配置,留空保持不变)',
folderId: 'Folder ID(可选)',
authorize: '授权 Google Drive',
authorizeHint: '通过 OAuth2 获取 Refresh Token',
oauthFieldsRequired: '请先填写 Client ID 和 Client Secret',
oauthSuccess: 'Google Drive 授权成功',
oauthFailed: 'Google Drive 授权失败',
closeWindow: '此窗口将自动关闭',
processing: '正在处理授权...',
testStorage: '测试存储',
testSuccess: 'Google Drive 存储测试成功(上传、访问、删除均正常)',
testFailed: 'Google Drive 存储测试失败'
}
}, },
streamTimeout: { streamTimeout: {
title: '流超时处理', title: '流超时处理',
...@@ -4905,6 +4968,7 @@ export default { ...@@ -4905,6 +4968,7 @@ export default {
downloadLocal: '本地下载', downloadLocal: '本地下载',
canDownload: '可下载', canDownload: '可下载',
regenrate: '重新生成', regenrate: '重新生成',
regenerate: '重新生成',
creatorPlaceholder: '描述你想要生成的视频或图片...', creatorPlaceholder: '描述你想要生成的视频或图片...',
videoModels: '视频模型', videoModels: '视频模型',
imageModels: '图片模型', imageModels: '图片模型',
...@@ -4921,6 +4985,13 @@ export default { ...@@ -4921,6 +4985,13 @@ export default {
galleryEmptyTitle: '还没有任何作品', galleryEmptyTitle: '还没有任何作品',
galleryEmptyDesc: '你的创作成果将会展示在这里。前往生成页,开始你的第一次创作吧。', galleryEmptyDesc: '你的创作成果将会展示在这里。前往生成页,开始你的第一次创作吧。',
startCreating: '开始创作', startCreating: '开始创作',
yesterday: '昨天' yesterday: '昨天',
landscape: '横屏',
portrait: '竖屏',
square: '方形',
examplePrompt1: '一只金色的柴犬在东京涩谷街头散步,镜头跟随,电影感画面,4K 高清',
examplePrompt2: '无人机航拍视角,冰岛极光下的冰川湖面反射绿色光芒,慢速推进',
examplePrompt3: '赛博朋克风格的未来城市,霓虹灯倒映在雨后积水中,夜景,电影级色彩',
examplePrompt4: '水墨画风格,一叶扁舟在山水间漂泊,薄雾缭绕,中国古典意境'
} }
} }
...@@ -403,6 +403,8 @@ export interface AdminGroup extends Group { ...@@ -403,6 +403,8 @@ export interface AdminGroup extends Group {
// MCP XML 协议注入(仅 antigravity 平台使用) // MCP XML 协议注入(仅 antigravity 平台使用)
mcp_xml_inject: boolean mcp_xml_inject: boolean
// Claude usage 模拟开关(仅 anthropic 平台使用)
simulate_claude_max_enabled: boolean
// 支持的模型系列(仅 antigravity 平台使用) // 支持的模型系列(仅 antigravity 平台使用)
supported_model_scopes?: string[] supported_model_scopes?: string[]
...@@ -497,6 +499,7 @@ export interface CreateGroupRequest { ...@@ -497,6 +499,7 @@ export interface CreateGroupRequest {
fallback_group_id?: number | null fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null fallback_group_id_on_invalid_request?: number | null
mcp_xml_inject?: boolean mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[] supported_model_scopes?: string[]
// 从指定分组复制账号 // 从指定分组复制账号
copy_accounts_from_group_ids?: number[] copy_accounts_from_group_ids?: number[]
...@@ -525,6 +528,7 @@ export interface UpdateGroupRequest { ...@@ -525,6 +528,7 @@ export interface UpdateGroupRequest {
fallback_group_id?: number | null fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null fallback_group_id_on_invalid_request?: number | null
mcp_xml_inject?: boolean mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[] supported_model_scopes?: string[]
copy_accounts_from_group_ids?: number[] copy_accounts_from_group_ids?: number[]
} }
...@@ -720,6 +724,12 @@ export interface Account { ...@@ -720,6 +724,12 @@ export interface Account {
cache_ttl_override_enabled?: boolean | null cache_ttl_override_enabled?: boolean | null
cache_ttl_override_target?: string | null cache_ttl_override_target?: string | null
// 客户端亲和调度(仅 Anthropic/Antigravity 平台有效)
// 启用后新会话会优先调度到客户端之前使用过的账号
client_affinity_enabled?: boolean | null
affinity_client_count?: number | null
affinity_clients?: string[] | null
// API Key 账号配额限制 // API Key 账号配额限制
quota_limit?: number | null quota_limit?: number | null
quota_used?: number | null quota_used?: number | null
...@@ -780,6 +790,11 @@ export interface AccountUsageInfo { ...@@ -780,6 +790,11 @@ export interface AccountUsageInfo {
gemini_pro_minute?: UsageProgress | null gemini_pro_minute?: UsageProgress | null
gemini_flash_minute?: UsageProgress | null gemini_flash_minute?: UsageProgress | null
antigravity_quota?: Record<string, AntigravityModelQuota> | null antigravity_quota?: Record<string, AntigravityModelQuota> | null
ai_credits?: Array<{
credit_type?: string
amount?: number
minimum_balance?: number
}> | null
// Antigravity 403 forbidden 状态 // Antigravity 403 forbidden 状态
is_forbidden?: boolean is_forbidden?: boolean
forbidden_reason?: string forbidden_reason?: string
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment