mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-10 18:14:48 +08:00
refactor: replace sync.Map credits state with AICredits rate limit key
Replace process-memory sync.Map + per-model runtime state with a single
"AICredits" key in model_rate_limits, making credits exhaustion fully
isomorphic with model-level rate limiting.
Scheduler: rate-limited accounts with overages enabled + credits available
are now scheduled instead of excluded.
Forwarding: when model is rate-limited + credits available, inject credits
proactively without waiting for a 429 round trip.
Storage: credits exhaustion stored as model_rate_limits["AICredits"] with
5h duration, reusing SetModelRateLimit/isRateLimitActiveForKey.
Frontend: show credits_active (yellow ⚡) when model rate-limited but
credits available, credits_exhausted (red) when AICredits key active.
Tests: add unit tests for shouldMarkCreditsExhausted, injectEnabledCreditTypes,
clearCreditsExhausted, and update existing overages tests.
This commit is contained in:
@@ -88,14 +88,25 @@
|
||||
]"
|
||||
>
|
||||
<div v-for="item in activeModelStatuses" :key="`${item.kind}-${item.model}`" class="group relative mb-1 break-inside-avoid">
|
||||
<!-- 积分已用尽 -->
|
||||
<span
|
||||
v-if="item.kind === 'overages'"
|
||||
v-if="item.kind === 'credits_exhausted'"
|
||||
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
||||
{{ t('admin.accounts.status.creditsExhausted') }}
|
||||
<span class="text-[10px] opacity-70">{{ formatModelResetTime(item.reset_at) }}</span>
|
||||
</span>
|
||||
<!-- 正在走积分(模型限流但积分可用)-->
|
||||
<span
|
||||
v-else-if="item.kind === 'credits_active'"
|
||||
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
>
|
||||
<span>⚡</span>
|
||||
{{ formatScopeName(item.model) }}
|
||||
<span class="text-[10px] opacity-70">{{ formatModelResetTime(item.reset_at) }}</span>
|
||||
</span>
|
||||
<!-- 普通模型限流 -->
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
@@ -109,9 +120,11 @@
|
||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-56 -translate-x-1/2 whitespace-normal rounded bg-gray-900 px-3 py-2 text-center text-xs leading-relaxed text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||
>
|
||||
{{
|
||||
item.kind === 'overages'
|
||||
? t('admin.accounts.status.modelCreditOveragesUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
|
||||
: t('admin.accounts.status.modelRateLimitedUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
|
||||
item.kind === 'credits_exhausted'
|
||||
? t('admin.accounts.status.creditsExhaustedUntil', { time: formatTime(item.reset_at) })
|
||||
: item.kind === 'credits_active'
|
||||
? t('admin.accounts.status.modelCreditOveragesUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
|
||||
: t('admin.accounts.status.modelRateLimitedUntil', { model: formatScopeName(item.model), time: formatTime(item.reset_at) })
|
||||
}}
|
||||
<div
|
||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||
@@ -165,12 +178,12 @@ const isRateLimited = computed(() => {
|
||||
})
|
||||
|
||||
type AccountModelStatusItem = {
|
||||
kind: 'rate_limit' | 'overages'
|
||||
kind: 'rate_limit' | 'credits_exhausted' | 'credits_active'
|
||||
model: string
|
||||
reset_at: string
|
||||
}
|
||||
|
||||
// Computed: active model statuses (普通模型限流 + 超量请求运行态)
|
||||
// Computed: active model statuses (普通模型限流 + 积分耗尽 + 走积分中)
|
||||
const activeModelStatuses = computed<AccountModelStatusItem[]>(() => {
|
||||
const extra = props.account.extra as Record<string, unknown> | undefined
|
||||
const modelLimits = extra?.model_rate_limits as
|
||||
@@ -179,19 +192,26 @@ const activeModelStatuses = computed<AccountModelStatusItem[]>(() => {
|
||||
const now = new Date()
|
||||
const items: AccountModelStatusItem[] = []
|
||||
|
||||
if (modelLimits) {
|
||||
items.push(...Object.entries(modelLimits)
|
||||
.filter(([, info]) => new Date(info.rate_limit_reset_at) > now)
|
||||
.map(([model, info]) => ({ kind: 'rate_limit' as const, model, reset_at: info.rate_limit_reset_at })))
|
||||
}
|
||||
if (!modelLimits) return items
|
||||
|
||||
const overagesStates = extra?.antigravity_credits_overages as
|
||||
| Record<string, { activated_at?: string; active_until: string }>
|
||||
| undefined
|
||||
if (overagesStates) {
|
||||
items.push(...Object.entries(overagesStates)
|
||||
.filter(([, info]) => new Date(info.active_until) > now)
|
||||
.map(([model, info]) => ({ kind: 'overages' as const, model, reset_at: info.active_until })))
|
||||
// 检查 AICredits key 是否生效(积分是否耗尽)
|
||||
const aiCreditsEntry = modelLimits['AICredits']
|
||||
const hasActiveAICredits = aiCreditsEntry && new Date(aiCreditsEntry.rate_limit_reset_at) > now
|
||||
const allowOverages = !!(extra?.allow_overages)
|
||||
|
||||
for (const [model, info] of Object.entries(modelLimits)) {
|
||||
if (new Date(info.rate_limit_reset_at) <= now) continue
|
||||
|
||||
if (model === 'AICredits') {
|
||||
// AICredits key → 积分已用尽
|
||||
items.push({ kind: 'credits_exhausted', model, reset_at: info.rate_limit_reset_at })
|
||||
} else if (allowOverages && !hasActiveAICredits) {
|
||||
// 普通模型限流 + overages 启用 + 积分可用 → 正在走积分
|
||||
items.push({ kind: 'credits_active', model, reset_at: info.rate_limit_reset_at })
|
||||
} else {
|
||||
// 普通模型限流
|
||||
items.push({ kind: 'rate_limit', model, reset_at: info.rate_limit_reset_at })
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
@@ -216,7 +236,7 @@ const formatScopeName = (scope: string): string => {
|
||||
'gemini-3.1-pro-high': 'G3PH',
|
||||
'gemini-3.1-pro-low': 'G3PL',
|
||||
'gemini-3-pro-image': 'G3PI',
|
||||
'gemini-3.1-flash-image': 'GImage',
|
||||
'gemini-3.1-flash-image': 'G31FI',
|
||||
// 其他
|
||||
'gpt-oss-120b-medium': 'GPT120',
|
||||
'tab_flash_lite_preview': 'TabFL',
|
||||
|
||||
@@ -43,17 +43,18 @@ function makeAccount(overrides: Partial<Account>): Account {
|
||||
}
|
||||
|
||||
describe('AccountStatusIndicator', () => {
|
||||
it('会将超量请求中的模型显示为独立状态', () => {
|
||||
it('模型限流 + overages 启用 + 无 AICredits key → 显示 ⚡ (credits_active)', () => {
|
||||
const wrapper = mount(AccountStatusIndicator, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 1,
|
||||
name: 'ag-1',
|
||||
extra: {
|
||||
antigravity_credits_overages: {
|
||||
allow_overages: true,
|
||||
model_rate_limits: {
|
||||
'claude-sonnet-4-5': {
|
||||
activated_at: '2026-03-15T00:00:00Z',
|
||||
active_until: '2099-03-15T00:00:00Z'
|
||||
rate_limited_at: '2026-03-15T00:00:00Z',
|
||||
rate_limit_reset_at: '2099-03-15T00:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +71,7 @@ describe('AccountStatusIndicator', () => {
|
||||
expect(wrapper.text()).toContain('CSon45')
|
||||
})
|
||||
|
||||
it('普通模型限流仍显示原有限流状态', () => {
|
||||
it('模型限流 + overages 未启用 → 普通限流样式(无 ⚡)', () => {
|
||||
const wrapper = mount(AccountStatusIndicator, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
@@ -96,4 +97,66 @@ describe('AccountStatusIndicator', () => {
|
||||
expect(wrapper.text()).toContain('CSon45')
|
||||
expect(wrapper.text()).not.toContain('⚡')
|
||||
})
|
||||
|
||||
it('AICredits key 生效 → 显示积分已用尽 (credits_exhausted)', () => {
|
||||
const wrapper = mount(AccountStatusIndicator, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 3,
|
||||
name: 'ag-3',
|
||||
extra: {
|
||||
allow_overages: true,
|
||||
model_rate_limits: {
|
||||
'AICredits': {
|
||||
rate_limited_at: '2026-03-15T00:00:00Z',
|
||||
rate_limit_reset_at: '2099-03-15T00:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Icon: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('account.creditsExhausted')
|
||||
})
|
||||
|
||||
it('模型限流 + overages 启用 + AICredits key 生效 → 普通限流样式(积分耗尽,无 ⚡)', () => {
|
||||
const wrapper = mount(AccountStatusIndicator, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 4,
|
||||
name: 'ag-4',
|
||||
extra: {
|
||||
allow_overages: true,
|
||||
model_rate_limits: {
|
||||
'claude-sonnet-4-5': {
|
||||
rate_limited_at: '2026-03-15T00:00:00Z',
|
||||
rate_limit_reset_at: '2099-03-15T00:00:00Z'
|
||||
},
|
||||
'AICredits': {
|
||||
rate_limited_at: '2026-03-15T00:00:00Z',
|
||||
rate_limit_reset_at: '2099-03-15T00:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Icon: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 模型限流 + 积分耗尽 → 不应显示 ⚡
|
||||
expect(wrapper.text()).toContain('CSon45')
|
||||
expect(wrapper.text()).not.toContain('⚡')
|
||||
// AICredits 积分耗尽状态应显示
|
||||
expect(wrapper.text()).toContain('account.creditsExhausted')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -245,6 +245,7 @@ export default {
|
||||
// Common
|
||||
common: {
|
||||
loading: 'Loading...',
|
||||
justNow: 'just now',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
@@ -1655,6 +1656,14 @@ export default {
|
||||
enabled: 'Enabled',
|
||||
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: {
|
||||
title: 'Supported Model Families',
|
||||
tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.',
|
||||
@@ -1868,6 +1877,8 @@ export default {
|
||||
rateLimitedAutoResume: 'Auto resumes in {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}',
|
||||
viewTempUnschedDetails: 'View temp unschedulable details'
|
||||
},
|
||||
@@ -1969,7 +1980,7 @@ export default {
|
||||
resetQuota: 'Reset Quota',
|
||||
quotaLimit: 'Quota Limit',
|
||||
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',
|
||||
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
|
||||
quotaDailyLimit: 'Daily Limit',
|
||||
@@ -2166,7 +2177,7 @@ export default {
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
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: {
|
||||
label: '5h Window Cost Limit',
|
||||
hint: 'Limit account cost usage within the 5-hour window',
|
||||
@@ -2221,8 +2232,26 @@ export default {
|
||||
hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)',
|
||||
target: 'Target TTL',
|
||||
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',
|
||||
proxy: 'Proxy',
|
||||
noProxy: 'No Proxy',
|
||||
@@ -2677,7 +2706,7 @@ export default {
|
||||
geminiFlashDaily: 'Flash',
|
||||
gemini3Pro: 'G3P',
|
||||
gemini3Flash: 'G3F',
|
||||
gemini3Image: 'GImage',
|
||||
gemini3Image: 'G31FI',
|
||||
claude: 'Claude'
|
||||
},
|
||||
tier: {
|
||||
@@ -4190,40 +4219,55 @@ export default {
|
||||
usage: 'Usage: Add to request header - x-api-key: <your-admin-api-key>'
|
||||
},
|
||||
soraS3: {
|
||||
title: 'Sora S3 Storage',
|
||||
description: 'Manage multiple Sora S3 endpoints and switch the active profile',
|
||||
title: 'Sora Storage',
|
||||
description: 'Manage Sora media storage profiles with S3 and Google Drive support',
|
||||
newProfile: 'New Profile',
|
||||
reloadProfiles: 'Reload Profiles',
|
||||
empty: 'No Sora S3 profiles yet, create one first',
|
||||
createTitle: 'Create Sora S3 Profile',
|
||||
editTitle: 'Edit Sora S3 Profile',
|
||||
empty: 'No storage profiles yet, create one first',
|
||||
createTitle: 'Create Storage Profile',
|
||||
editTitle: 'Edit Storage Profile',
|
||||
selectProvider: 'Select Storage Type',
|
||||
providerS3Desc: 'S3-compatible object storage',
|
||||
providerGDriveDesc: 'Google Drive cloud storage',
|
||||
profileID: 'Profile ID',
|
||||
profileName: 'Profile Name',
|
||||
setActive: 'Set as active after creation',
|
||||
saveProfile: 'Save Profile',
|
||||
activateProfile: 'Activate',
|
||||
profileCreated: 'Sora S3 profile created',
|
||||
profileSaved: 'Sora S3 profile saved',
|
||||
profileDeleted: 'Sora S3 profile deleted',
|
||||
profileActivated: 'Sora S3 active profile switched',
|
||||
profileCreated: 'Storage profile created',
|
||||
profileSaved: 'Storage profile saved',
|
||||
profileDeleted: 'Storage profile deleted',
|
||||
profileActivated: 'Active storage profile switched',
|
||||
profileIDRequired: 'Profile ID is required',
|
||||
profileNameRequired: 'Profile name is required',
|
||||
profileSelectRequired: 'Please select a profile first',
|
||||
endpointRequired: 'S3 endpoint is required when enabled',
|
||||
bucketRequired: 'Bucket is required when enabled',
|
||||
accessKeyRequired: 'Access Key ID is required when enabled',
|
||||
deleteConfirm: 'Delete Sora S3 profile {profileID}?',
|
||||
deleteConfirm: 'Delete storage profile {profileID}?',
|
||||
columns: {
|
||||
profile: 'Profile',
|
||||
profileId: 'Profile ID',
|
||||
name: 'Name',
|
||||
provider: 'Type',
|
||||
active: 'Active',
|
||||
endpoint: 'Endpoint',
|
||||
bucket: 'Bucket',
|
||||
storagePath: 'Storage Path',
|
||||
capacityUsage: 'Capacity / Used',
|
||||
capacityUnlimited: 'Unlimited',
|
||||
videoCount: 'Videos',
|
||||
videoCompleted: 'completed',
|
||||
videoInProgress: 'in progress',
|
||||
quota: 'Default Quota',
|
||||
updatedAt: 'Updated At',
|
||||
actions: 'Actions'
|
||||
actions: 'Actions',
|
||||
rootFolder: 'Root folder',
|
||||
testInTable: 'Test',
|
||||
testingInTable: 'Testing...',
|
||||
testTimeout: 'Test timed out (15s)'
|
||||
},
|
||||
enabled: 'Enable S3 Storage',
|
||||
enabledHint: 'When enabled, Sora generated media files will be automatically uploaded to S3 storage',
|
||||
enabled: 'Enable Storage',
|
||||
enabledHint: 'When enabled, Sora generated media files will be automatically uploaded',
|
||||
endpoint: 'S3 Endpoint',
|
||||
region: 'Region',
|
||||
bucket: 'Bucket',
|
||||
@@ -4232,16 +4276,38 @@ export default {
|
||||
secretAccessKey: 'Secret Access Key',
|
||||
secretConfigured: '(Configured, leave blank to keep)',
|
||||
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',
|
||||
defaultQuota: 'Default Storage Quota',
|
||||
defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited',
|
||||
testConnection: 'Test Connection',
|
||||
testing: 'Testing...',
|
||||
testSuccess: 'S3 connection test successful',
|
||||
testFailed: 'S3 connection test failed',
|
||||
saved: 'Sora S3 settings saved successfully',
|
||||
saveFailed: 'Failed to save Sora S3 settings'
|
||||
testSuccess: 'Connection test successful',
|
||||
testFailed: 'Connection test failed',
|
||||
saved: 'Storage settings saved successfully',
|
||||
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: {
|
||||
title: 'Stream Timeout Handling',
|
||||
@@ -4712,6 +4778,7 @@ export default {
|
||||
downloadLocal: 'Download',
|
||||
canDownload: 'to download',
|
||||
regenrate: 'Regenerate',
|
||||
regenerate: 'Regenerate',
|
||||
creatorPlaceholder: 'Describe the video or image you want to create...',
|
||||
videoModels: 'Video Models',
|
||||
imageModels: 'Image Models',
|
||||
@@ -4728,6 +4795,13 @@ export default {
|
||||
galleryEmptyTitle: 'No works yet',
|
||||
galleryEmptyDesc: 'Your creations will be displayed here. Go to the generate page to start your first creation.',
|
||||
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 {
|
||||
// Common
|
||||
common: {
|
||||
loading: '加载中...',
|
||||
justNow: '刚刚',
|
||||
save: '保存',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
@@ -1974,7 +1975,7 @@ export default {
|
||||
resetQuota: '重置配额',
|
||||
quotaLimit: '配额限制',
|
||||
quotaLimitPlaceholder: '0 表示不限制',
|
||||
quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。修改限额不会重置已用额度。',
|
||||
quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。Anthropic API Key 账号还可配置客户端亲和。修改限额不会重置已用额度。',
|
||||
quotaLimitToggle: '启用配额限制',
|
||||
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
|
||||
quotaDailyLimit: '日限额',
|
||||
@@ -2053,6 +2054,8 @@ export default {
|
||||
rateLimitedAutoResume: '{time} 自动恢复',
|
||||
modelRateLimitedUntil: '{model} 限流至 {time}',
|
||||
modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}',
|
||||
creditsExhausted: '积分已用尽',
|
||||
creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复',
|
||||
overloadedUntil: '负载过重,重置时间:{time}',
|
||||
viewTempUnschedDetails: '查看临时不可调度详情'
|
||||
},
|
||||
@@ -2106,7 +2109,7 @@ export default {
|
||||
geminiFlashDaily: 'Flash',
|
||||
gemini3Pro: 'G3P',
|
||||
gemini3Flash: 'G3F',
|
||||
gemini3Image: 'GImage',
|
||||
gemini3Image: 'G31FI',
|
||||
claude: 'Claude'
|
||||
},
|
||||
tier: {
|
||||
@@ -2316,7 +2319,7 @@ export default {
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
title: '配额控制',
|
||||
hint: '仅适用于 Anthropic OAuth/Setup Token 账号',
|
||||
hint: '配置费用窗口、会话限制、客户端亲和等调度控制。',
|
||||
windowCost: {
|
||||
label: '5h窗口费用控制',
|
||||
hint: '限制账号在5小时窗口内的费用使用',
|
||||
@@ -2371,8 +2374,26 @@ export default {
|
||||
hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费',
|
||||
target: '目标 TTL',
|
||||
targetHint: '选择计费使用的 TTL 类型'
|
||||
},
|
||||
clientAffinity: {
|
||||
label: '客户端亲和调度',
|
||||
hint: '启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号'
|
||||
}
|
||||
},
|
||||
affinityNoClients: '无亲和客户端',
|
||||
affinityClients: '{count} 个亲和客户端:',
|
||||
affinitySection: '客户端亲和',
|
||||
affinitySectionHint: '控制客户端在账号间的分布。通过配置区域阈值来平衡负载。',
|
||||
affinityToggle: '启用客户端亲和',
|
||||
affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号',
|
||||
affinityBase: '基础限额(绿区)',
|
||||
affinityBasePlaceholder: '留空表示不限制',
|
||||
affinityBaseHint: '绿区最大客户端数量(完整优先级调度)',
|
||||
affinityBaseOffHint: '未开启绿区限制,所有客户端均享受完整优先级调度',
|
||||
affinityBuffer: '缓冲区(黄区)',
|
||||
affinityBufferPlaceholder: '例如 3',
|
||||
affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)',
|
||||
affinityBufferInfinite: '不限制',
|
||||
expired: '已过期',
|
||||
proxy: '代理',
|
||||
noProxy: '无代理',
|
||||
@@ -4363,40 +4384,55 @@ export default {
|
||||
usage: '使用方法:在请求头中添加 x-api-key: <your-admin-api-key>'
|
||||
},
|
||||
soraS3: {
|
||||
title: 'Sora S3 存储配置',
|
||||
description: '以多配置列表方式管理 Sora S3 端点,并可切换生效配置',
|
||||
title: 'Sora 存储配置',
|
||||
description: '以多配置列表管理 Sora 媒体存储,支持 S3 和 Google Drive',
|
||||
newProfile: '新建配置',
|
||||
reloadProfiles: '刷新列表',
|
||||
empty: '暂无 Sora S3 配置,请先创建',
|
||||
createTitle: '新建 Sora S3 配置',
|
||||
editTitle: '编辑 Sora S3 配置',
|
||||
empty: '暂无存储配置,请先创建',
|
||||
createTitle: '新建存储配置',
|
||||
editTitle: '编辑存储配置',
|
||||
selectProvider: '选择存储类型',
|
||||
providerS3Desc: 'S3 兼容对象存储',
|
||||
providerGDriveDesc: 'Google Drive 云盘',
|
||||
profileID: '配置 ID',
|
||||
profileName: '配置名称',
|
||||
setActive: '创建后设为生效',
|
||||
saveProfile: '保存配置',
|
||||
activateProfile: '设为生效',
|
||||
profileCreated: 'Sora S3 配置创建成功',
|
||||
profileSaved: 'Sora S3 配置保存成功',
|
||||
profileDeleted: 'Sora S3 配置删除成功',
|
||||
profileActivated: 'Sora S3 生效配置已切换',
|
||||
profileCreated: '存储配置创建成功',
|
||||
profileSaved: '存储配置保存成功',
|
||||
profileDeleted: '存储配置删除成功',
|
||||
profileActivated: '生效配置已切换',
|
||||
profileIDRequired: '请填写配置 ID',
|
||||
profileNameRequired: '请填写配置名称',
|
||||
profileSelectRequired: '请先选择配置',
|
||||
endpointRequired: '启用时必须填写 S3 端点',
|
||||
bucketRequired: '启用时必须填写存储桶',
|
||||
accessKeyRequired: '启用时必须填写 Access Key ID',
|
||||
deleteConfirm: '确定删除 Sora S3 配置 {profileID} 吗?',
|
||||
deleteConfirm: '确定删除存储配置 {profileID} 吗?',
|
||||
columns: {
|
||||
profile: '配置',
|
||||
profileId: 'Profile ID',
|
||||
name: '名称',
|
||||
provider: '存储类型',
|
||||
active: '生效状态',
|
||||
endpoint: '端点',
|
||||
bucket: '存储桶',
|
||||
storagePath: '存储路径',
|
||||
capacityUsage: '容量 / 已用',
|
||||
capacityUnlimited: '无限制',
|
||||
videoCount: '视频数',
|
||||
videoCompleted: '完成',
|
||||
videoInProgress: '进行中',
|
||||
quota: '默认配额',
|
||||
updatedAt: '更新时间',
|
||||
actions: '操作'
|
||||
actions: '操作',
|
||||
rootFolder: '根目录',
|
||||
testInTable: '测试',
|
||||
testingInTable: '测试中...',
|
||||
testTimeout: '测试超时(15秒)'
|
||||
},
|
||||
enabled: '启用 S3 存储',
|
||||
enabledHint: '启用后,Sora 生成的媒体文件将自动上传到 S3 存储',
|
||||
enabled: '启用存储',
|
||||
enabledHint: '启用后,Sora 生成的媒体文件将自动上传到存储',
|
||||
endpoint: 'S3 端点',
|
||||
region: '区域',
|
||||
bucket: '存储桶',
|
||||
@@ -4405,16 +4441,38 @@ export default {
|
||||
secretAccessKey: 'Secret Access Key',
|
||||
secretConfigured: '(已配置,留空保持不变)',
|
||||
cdnUrl: 'CDN URL',
|
||||
cdnUrlHint: '可选,配置后使用 CDN URL 访问文件,否则使用预签名 URL',
|
||||
cdnUrlHint: '可选,配置后使用 CDN URL 访问文件',
|
||||
forcePathStyle: '强制路径风格(Path Style)',
|
||||
defaultQuota: '默认存储配额',
|
||||
defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制',
|
||||
testConnection: '测试连接',
|
||||
testing: '测试中...',
|
||||
testSuccess: 'S3 连接测试成功',
|
||||
testFailed: 'S3 连接测试失败',
|
||||
saved: 'Sora S3 设置保存成功',
|
||||
saveFailed: '保存 Sora S3 设置失败'
|
||||
testSuccess: '连接测试成功',
|
||||
testFailed: '连接测试失败',
|
||||
saved: '存储设置保存成功',
|
||||
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: {
|
||||
title: '流超时处理',
|
||||
@@ -4910,6 +4968,7 @@ export default {
|
||||
downloadLocal: '本地下载',
|
||||
canDownload: '可下载',
|
||||
regenrate: '重新生成',
|
||||
regenerate: '重新生成',
|
||||
creatorPlaceholder: '描述你想要生成的视频或图片...',
|
||||
videoModels: '视频模型',
|
||||
imageModels: '图片模型',
|
||||
@@ -4926,6 +4985,13 @@ export default {
|
||||
galleryEmptyTitle: '还没有任何作品',
|
||||
galleryEmptyDesc: '你的创作成果将会展示在这里。前往生成页,开始你的第一次创作吧。',
|
||||
startCreating: '开始创作',
|
||||
yesterday: '昨天'
|
||||
yesterday: '昨天',
|
||||
landscape: '横屏',
|
||||
portrait: '竖屏',
|
||||
square: '方形',
|
||||
examplePrompt1: '一只金色的柴犬在东京涩谷街头散步,镜头跟随,电影感画面,4K 高清',
|
||||
examplePrompt2: '无人机航拍视角,冰岛极光下的冰川湖面反射绿色光芒,慢速推进',
|
||||
examplePrompt3: '赛博朋克风格的未来城市,霓虹灯倒映在雨后积水中,夜景,电影级色彩',
|
||||
examplePrompt4: '水墨画风格,一叶扁舟在山水间漂泊,薄雾缭绕,中国古典意境'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +403,8 @@ export interface AdminGroup extends Group {
|
||||
|
||||
// MCP XML 协议注入(仅 antigravity 平台使用)
|
||||
mcp_xml_inject: boolean
|
||||
// Claude usage 模拟开关(仅 anthropic 平台使用)
|
||||
simulate_claude_max_enabled: boolean
|
||||
|
||||
// 支持的模型系列(仅 antigravity 平台使用)
|
||||
supported_model_scopes?: string[]
|
||||
@@ -497,6 +499,7 @@ export interface CreateGroupRequest {
|
||||
fallback_group_id?: number | null
|
||||
fallback_group_id_on_invalid_request?: number | null
|
||||
mcp_xml_inject?: boolean
|
||||
simulate_claude_max_enabled?: boolean
|
||||
supported_model_scopes?: string[]
|
||||
// 从指定分组复制账号
|
||||
copy_accounts_from_group_ids?: number[]
|
||||
@@ -525,6 +528,7 @@ export interface UpdateGroupRequest {
|
||||
fallback_group_id?: number | null
|
||||
fallback_group_id_on_invalid_request?: number | null
|
||||
mcp_xml_inject?: boolean
|
||||
simulate_claude_max_enabled?: boolean
|
||||
supported_model_scopes?: string[]
|
||||
copy_accounts_from_group_ids?: number[]
|
||||
}
|
||||
@@ -664,7 +668,6 @@ export interface Account {
|
||||
// Extra fields including Codex usage and model-level rate limits (Antigravity smart retry)
|
||||
extra?: (CodexUsageSnapshot & {
|
||||
model_rate_limits?: Record<string, { rate_limited_at: string; rate_limit_reset_at: string }>
|
||||
antigravity_credits_overages?: Record<string, { activated_at: string; active_until: string }>
|
||||
} & Record<string, unknown>)
|
||||
proxy_id: number | null
|
||||
concurrency: number
|
||||
@@ -721,6 +724,12 @@ export interface Account {
|
||||
cache_ttl_override_enabled?: boolean | 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 账号配额限制
|
||||
quota_limit?: number | null
|
||||
quota_used?: number | null
|
||||
|
||||
Reference in New Issue
Block a user