Merge tag 'v0.1.90' into merge/upstream-v0.1.90

注册邮箱域名白名单策略上线,后台大数据场景性能大幅优化。

- 注册邮箱域名白名单:支持管理员配置允许注册的邮箱域名策略
- Keys 页面表单筛选:用户 /keys 页面支持按条件筛选 API Key
- Settings 页面分 Tab 拆分:管理后台设置页面按功能模块分 Tab 展示

- 后台大数据场景加载性能优化:仪表盘/用户/账号/Ops 页面大数据集加载显著提速
- Usage 大表分页优化:默认避免全量 COUNT(*),大幅降低分页查询耗时
- 消除重复的 normalizeAccountIDList,补充新增组件的单元测试
- 清理无用文件和过时文档,精简项目结构
- EmailVerifyView 硬编码英文字符串替换为 i18n 调用

- 修复 Anthropic 平台无限流重置时间的 429 误标记账号限流问题
- 修复自定义菜单页面管理员视角菜单不生效问题
- 修复 Ops 错误详情弹窗未展示真实上游 payload 的问题
- 修复充值/订阅菜单 icon 显示问题

# Conflicts:
#	.gitignore
#	backend/cmd/server/VERSION
#	backend/ent/group.go
#	backend/ent/runtime/runtime.go
#	backend/ent/schema/group.go
#	backend/go.sum
#	backend/internal/handler/admin/account_handler.go
#	backend/internal/handler/admin/dashboard_handler.go
#	backend/internal/pkg/usagestats/usage_log_types.go
#	backend/internal/repository/group_repo.go
#	backend/internal/repository/usage_log_repo.go
#	backend/internal/server/middleware/security_headers.go
#	backend/internal/server/router.go
#	backend/internal/service/account_usage_service.go
#	backend/internal/service/admin_service_bulk_update_test.go
#	backend/internal/service/dashboard_service.go
#	backend/internal/service/gateway_service.go
#	frontend/src/api/admin/dashboard.ts
#	frontend/src/components/account/BulkEditAccountModal.vue
#	frontend/src/components/charts/GroupDistributionChart.vue
#	frontend/src/components/layout/AppSidebar.vue
#	frontend/src/i18n/locales/en.ts
#	frontend/src/i18n/locales/zh.ts
#	frontend/src/views/admin/GroupsView.vue
#	frontend/src/views/admin/SettingsView.vue
#	frontend/src/views/admin/UsageView.vue
#	frontend/src/views/user/PurchaseSubscriptionView.vue
This commit is contained in:
erio
2026-03-04 19:58:38 +08:00
461 changed files with 63392 additions and 6617 deletions

View File

@@ -270,6 +270,7 @@ export default {
redeemCodes: 'Redeem Codes',
ops: 'Ops',
promoCodes: 'Promo Codes',
dataManagement: 'Data Management',
settings: 'Settings',
myAccount: 'My Account',
lightMode: 'Light Mode',
@@ -311,6 +312,9 @@ export default {
passwordMinLength: 'Password must be at least 6 characters',
loginFailed: 'Login failed. Please check your credentials and try again.',
registrationFailed: 'Registration failed. Please try again.',
emailSuffixNotAllowed: 'This email domain is not allowed for registration.',
emailSuffixNotAllowedWithAllowed:
'This email domain is not allowed. Allowed domains: {suffixes}',
loginSuccess: 'Login successful! Welcome back.',
accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.',
reloginRequired: 'Session expired. Please log in again.',
@@ -325,6 +329,16 @@ export default {
sendingCode: 'Sending...',
clickToResend: 'Click to resend code',
resendCode: 'Resend verification code',
sendCodeDesc: "We'll send a verification code to",
codeSentSuccess: 'Verification code sent! Please check your inbox.',
verifying: 'Verifying...',
verifyAndCreate: 'Verify & Create Account',
resendCountdown: 'Resend code in {countdown}s',
backToRegistration: 'Back to registration',
sendCodeFailed: 'Failed to send verification code. Please try again.',
verifyFailed: 'Verification failed. Please try again.',
codeRequired: 'Verification code is required',
invalidCode: 'Please enter a valid 6-digit code',
promoCodeLabel: 'Promo Code',
promoCodePlaceholder: 'Enter promo code (optional)',
promoCodeValid: 'Valid! You will receive ${amount} bonus balance',
@@ -407,9 +421,12 @@ export default {
day: 'Day',
hour: 'Hour',
modelDistribution: 'Model Distribution',
groupDistribution: 'Group Usage Distribution',
tokenUsageTrend: 'Token Usage Trend',
noDataAvailable: 'No data available',
model: 'Model',
group: 'Group',
noGroup: 'No Group',
requests: 'Requests',
tokens: 'Tokens',
actual: 'Actual',
@@ -440,6 +457,9 @@ export default {
keys: {
title: 'API Keys',
description: 'Manage your API keys and access tokens',
searchPlaceholder: 'Search name or key...',
allGroups: 'All Groups',
allStatus: 'All Status',
createKey: 'Create API Key',
editKey: 'Edit API Key',
deleteKey: 'Delete API Key',
@@ -500,6 +520,7 @@ export default {
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
codexCli: 'Codex CLI',
codexCliWs: 'Codex CLI (WebSocket)',
opencode: 'OpenCode',
},
antigravity: {
@@ -555,6 +576,19 @@ export default {
resetQuotaConfirmMessage: 'Are you sure you want to reset the used quota (${used}) for key "{name}" to 0? This action cannot be undone.',
quotaResetSuccess: 'Quota reset successfully',
failedToResetQuota: 'Failed to reset quota',
rateLimitColumn: 'Rate Limit',
rateLimitSection: 'Rate Limit',
resetUsage: 'Reset',
rateLimit5h: '5-Hour Limit (USD)',
rateLimit1d: 'Daily Limit (USD)',
rateLimit7d: '7-Day Limit (USD)',
rateLimitHint: 'Set the maximum spending for this key within each time window. 0 = unlimited.',
rateLimitUsage: 'Rate Limit Usage',
resetRateLimitUsage: 'Reset Rate Limit Usage',
resetRateLimitTitle: 'Confirm Reset Rate Limit',
resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.',
rateLimitResetSuccess: 'Rate limit usage reset successfully',
failedToResetRateLimit: 'Failed to reset rate limit usage',
expiration: 'Expiration',
expiresInDays: '{days} days',
extendDays: '+{days} days',
@@ -613,8 +647,10 @@ export default {
firstToken: 'First Token',
duration: 'Duration',
time: 'Time',
ws: 'WS',
stream: 'Stream',
sync: 'Sync',
unknown: 'Unknown',
in: 'In',
out: 'Out',
cacheRead: 'Read',
@@ -828,11 +864,12 @@ export default {
day: 'Day',
hour: 'Hour',
modelDistribution: 'Model Distribution',
groupDistribution: 'Group Distribution',
groupDistribution: 'Group Usage Distribution',
tokenUsageTrend: 'Token Usage Trend',
userUsageTrend: 'User Usage Trend (Top 12)',
model: 'Model',
group: 'Group',
noGroup: 'No Group',
requests: 'Requests',
tokens: 'Tokens',
actual: 'Actual',
@@ -842,6 +879,181 @@ export default {
failedToLoad: 'Failed to load dashboard statistics'
},
dataManagement: {
title: 'Data Management',
description: 'Manage data management agent status, object storage settings, and backup jobs in one place',
agent: {
title: 'Data Management Agent Status',
description: 'The system probes a fixed Unix socket and enables data management only when reachable.',
enabled: 'Data management agent is ready. Data management operations are available.',
disabled: 'Data management agent is unavailable. Only diagnostic information is available now.',
socketPath: 'Socket Path',
version: 'Version',
status: 'Status',
uptime: 'Uptime',
reasonLabel: 'Unavailable Reason',
reason: {
DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Data management socket file is missing',
DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Data management agent is unreachable',
BACKUP_AGENT_SOCKET_MISSING: 'Backup socket file is missing',
BACKUP_AGENT_UNAVAILABLE: 'Backup agent is unreachable',
UNKNOWN: 'Unknown reason'
}
},
sections: {
config: {
title: 'Backup Configuration',
description: 'Configure backup source, retention policy, and S3 settings.'
},
s3: {
title: 'S3 Object Storage',
description: 'Configure and test uploads of backup artifacts to a standard S3-compatible storage.'
},
backup: {
title: 'Backup Operations',
description: 'Trigger PostgreSQL, Redis, and full backup jobs.'
},
history: {
title: 'Backup History',
description: 'Review backup job status, errors, and artifact metadata.'
}
},
form: {
sourceMode: 'Source Mode',
backupRoot: 'Backup Root',
activePostgresProfile: 'Active PostgreSQL Profile',
activeRedisProfile: 'Active Redis Profile',
activeS3Profile: 'Active S3 Profile',
retentionDays: 'Retention Days',
keepLast: 'Keep Last Jobs',
uploadToS3: 'Upload to S3',
useActivePostgresProfile: 'Use Active PostgreSQL Profile',
useActiveRedisProfile: 'Use Active Redis Profile',
useActiveS3Profile: 'Use Active Profile',
idempotencyKey: 'Idempotency Key (Optional)',
secretConfigured: 'Configured already, leave empty to keep unchanged',
source: {
profileID: 'Profile ID (Unique)',
profileName: 'Profile Name',
setActive: 'Set as active after creation'
},
postgres: {
title: 'PostgreSQL',
host: 'Host',
port: 'Port',
user: 'User',
password: 'Password',
database: 'Database',
sslMode: 'SSL Mode',
containerName: 'Container Name (docker_exec mode)'
},
redis: {
title: 'Redis',
addr: 'Address (host:port)',
username: 'Username',
password: 'Password',
db: 'Database Index',
containerName: 'Container Name (docker_exec mode)'
},
s3: {
enabled: 'Enable S3 Upload',
profileID: 'Profile ID (Unique)',
profileName: 'Profile Name',
endpoint: 'Endpoint (Optional)',
region: 'Region',
bucket: 'Bucket',
accessKeyID: 'Access Key ID',
secretAccessKey: 'Secret Access Key',
prefix: 'Object Prefix',
forcePathStyle: 'Force Path Style',
useSSL: 'Use SSL',
setActive: 'Set as active after creation'
}
},
sourceProfiles: {
createTitle: 'Create Source Profile',
editTitle: 'Edit Source Profile',
empty: 'No source profiles yet, create one first',
deleteConfirm: 'Delete source profile {profileID}?',
columns: {
profile: 'Profile',
active: 'Active',
connection: 'Connection',
database: 'Database',
updatedAt: 'Updated At',
actions: 'Actions'
}
},
s3Profiles: {
createTitle: 'Create S3 Profile',
editTitle: 'Edit S3 Profile',
empty: 'No S3 profiles yet, create one first',
editHint: 'Click "Edit" to modify profile details in the right drawer.',
deleteConfirm: 'Delete S3 profile {profileID}?',
columns: {
profile: 'Profile',
active: 'Active',
storage: 'Storage',
updatedAt: 'Updated At',
actions: 'Actions'
}
},
history: {
total: '{count} jobs',
empty: 'No backup jobs yet',
columns: {
jobID: 'Job ID',
type: 'Type',
status: 'Status',
triggeredBy: 'Triggered By',
pgProfile: 'PostgreSQL Profile',
redisProfile: 'Redis Profile',
s3Profile: 'S3 Profile',
finishedAt: 'Finished At',
artifact: 'Artifact',
error: 'Error'
},
status: {
queued: 'Queued',
running: 'Running',
succeeded: 'Succeeded',
failed: 'Failed',
partial_succeeded: 'Partial Succeeded'
}
},
actions: {
refresh: 'Refresh Status',
disabledHint: 'Start datamanagementd first and ensure the socket is reachable.',
reloadConfig: 'Reload Config',
reloadSourceProfiles: 'Reload Source Profiles',
reloadProfiles: 'Reload Profiles',
newSourceProfile: 'New Source Profile',
saveConfig: 'Save Config',
configSaved: 'Configuration saved',
testS3: 'Test S3 Connection',
s3TestOK: 'S3 connection test succeeded',
s3TestFailed: 'S3 connection test failed',
newProfile: 'New Profile',
saveProfile: 'Save Profile',
activateProfile: 'Activate',
profileIDRequired: 'Profile ID is required',
profileNameRequired: 'Profile name is required',
profileSelectRequired: 'Select a profile to edit first',
profileCreated: 'S3 profile created',
profileSaved: 'S3 profile saved',
profileActivated: 'S3 profile activated',
profileDeleted: 'S3 profile deleted',
sourceProfileCreated: 'Source profile created',
sourceProfileSaved: 'Source profile saved',
sourceProfileActivated: 'Source profile activated',
sourceProfileDeleted: 'Source profile deleted',
createBackup: 'Create Backup Job',
jobCreated: 'Backup job created: {jobID} ({status})',
refreshJobs: 'Refresh Jobs',
loadMore: 'Load More'
}
},
// Users
users: {
title: 'User Management',
@@ -900,6 +1112,9 @@ export default {
noApiKeys: 'This user has no API keys',
group: 'Group',
none: 'None',
groupChangedSuccess: 'Group updated successfully',
groupChangedWithGrant: 'Group updated. User auto-granted access to "{group}"',
groupChangeFailed: 'Failed to update group',
noUsersYet: 'No users yet',
createFirstUser: 'Create your first user to get started.',
userCreated: 'User created successfully',
@@ -915,6 +1130,8 @@ export default {
failedToLoadApiKeys: 'Failed to load user API keys',
emailRequired: 'Please enter email',
concurrencyMin: 'Concurrency must be at least 1',
soraStorageQuota: 'Sora Storage Quota',
soraStorageQuotaHint: 'In GB, 0 means use group or system default quota',
amountRequired: 'Please enter a valid amount',
insufficientBalance: 'Insufficient balance',
deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.",
@@ -1144,7 +1361,9 @@ export default {
image360: 'Image 360px ($)',
image540: 'Image 540px ($)',
video: 'Video (standard) ($)',
videoHd: 'Video (Pro-HD) ($)'
videoHd: 'Video (Pro-HD) ($)',
storageQuota: 'Storage Quota',
storageQuotaHint: 'In GB, set the Sora storage quota for users in this group. 0 means use system default'
},
claudeCode: {
title: 'Claude Code Client Restriction',
@@ -1389,6 +1608,10 @@ export default {
codeAssist: 'Code Assist',
antigravityOauth: 'Antigravity OAuth',
antigravityApikey: 'Connect via Base URL + API Key',
soraApiKey: 'API Key / Upstream',
soraApiKeyHint: 'Connect to another Sub2API or compatible API',
soraBaseUrlRequired: 'Sora API Key account requires a Base URL',
soraBaseUrlInvalidScheme: 'Base URL must start with http:// or https://',
upstream: 'Upstream',
upstreamDesc: 'Connect via Base URL + API Key'
},
@@ -1437,7 +1660,19 @@ export default {
sessions: {
full: 'Active sessions full, new sessions must wait (idle timeout: {idle} min)',
normal: 'Active sessions normal (idle timeout: {idle} min)'
}
},
rpm: {
full: 'RPM limit reached',
warning: 'RPM approaching limit',
normal: 'RPM normal',
tieredNormal: 'RPM limit (Tiered) - Normal',
tieredWarning: 'RPM limit (Tiered) - Approaching limit',
tieredStickyOnly: 'RPM limit (Tiered) - Sticky only | Buffer: {buffer}',
tieredBlocked: 'RPM limit (Tiered) - Blocked | Buffer: {buffer}',
stickyExemptNormal: 'RPM limit (Sticky Exempt) - Normal',
stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit',
stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only'
},
},
tempUnschedulable: {
title: 'Temp Unschedulable',
@@ -1554,6 +1789,24 @@ export default {
oauthPassthrough: 'Auto passthrough (auth only)',
oauthPassthroughDesc:
'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.',
responsesWebsocketsV2: 'Responses WebSocket v2',
responsesWebsocketsV2Desc:
'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).',
wsMode: 'WS mode',
wsModeDesc: 'Only applies to the current OpenAI account type.',
wsModeOff: 'Off (off)',
wsModeShared: 'Shared (shared)',
wsModeDedicated: 'Dedicated (dedicated)',
wsModeConcurrencyHint:
'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.',
oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode',
oauthResponsesWebsocketsV2Desc:
'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.',
apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode',
apiKeyResponsesWebsocketsV2Desc:
'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.',
responsesWebsocketsV2PassthroughHint:
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
codexCLIOnly: 'Codex official clients only',
codexCLIOnlyDesc:
'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.',
@@ -1634,6 +1887,27 @@ export default {
idleTimeoutPlaceholder: '5',
idleTimeoutHint: 'Sessions will be released after idle timeout'
},
rpmLimit: {
label: 'RPM Limit',
hint: 'Limit requests per minute to protect upstream accounts',
baseRpm: 'Base RPM',
baseRpmPlaceholder: '15',
baseRpmHint: 'Max requests per minute, 0 or empty means no limit',
strategy: 'RPM Strategy',
strategyTiered: 'Tiered Model',
strategyStickyExempt: 'Sticky Exempt',
strategyTieredHint: 'Green → Yellow → Sticky only → Blocked, progressive throttling',
strategyStickyExemptHint: 'Only sticky sessions allowed when over limit',
strategyHint: 'Tiered: gradually restrict when exceeded; Sticky Exempt: existing sessions unrestricted',
stickyBuffer: 'Sticky Buffer',
stickyBufferPlaceholder: 'Default: 20% of base RPM',
stickyBufferHint: 'Extra requests allowed for sticky sessions after exceeding base RPM. Leave empty to use default (20% of base RPM, min 1)',
userMsgQueue: 'User Message Rate Control',
userMsgQueueHint: 'Rate-limit user messages to avoid triggering upstream RPM limits',
umqModeOff: 'Off',
umqModeThrottle: 'Throttle',
umqModeSerialize: 'Serialize',
},
tlsFingerprint: {
label: 'TLS Fingerprint Simulation',
hint: 'Simulate Node.js/Claude Code client TLS fingerprint'
@@ -1763,6 +2037,15 @@ export default {
sessionTokenAuth: 'Manual ST Input',
sessionTokenDesc: 'Enter your existing Sora Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
sessionTokenPlaceholder: 'Paste your Sora Session Token...\nSupports multiple, one per line',
sessionTokenRawLabel: 'Raw Input',
sessionTokenRawPlaceholder: 'Paste /api/auth/session raw payload or Session Token...',
sessionTokenRawHint: 'You can paste full JSON. The system will auto-parse ST and AT.',
openSessionUrl: 'Open Fetch URL',
copySessionUrl: 'Copy URL',
sessionUrlHint: 'This URL usually returns AT. If sessionToken is absent, copy __Secure-next-auth.session-token from browser cookies as ST.',
parsedSessionTokensLabel: 'Parsed ST',
parsedSessionTokensEmpty: 'No ST parsed. Please check your input.',
parsedAccessTokensLabel: 'Parsed AT',
validating: 'Validating...',
validateAndCreate: 'Validate & Create Account',
pleaseEnterRefreshToken: 'Please enter Refresh Token',
@@ -2013,6 +2296,7 @@ export default {
selectTestModel: 'Select Test Model',
testModel: 'Test model',
testPrompt: 'Prompt: "hi"',
soraUpstreamBaseUrlHint: 'Upstream Sora service URL (another Sub2API instance or compatible API)',
soraTestHint: 'Sora test runs connectivity and capability checks (/backend/me, subscription, Sora2 invite and remaining quota).',
soraTestTarget: 'Target: Sora account capability',
soraTestMode: 'Mode: Connectivity + Capability checks',
@@ -2103,6 +2387,8 @@ export default {
dataExportConfirm: 'Confirm Export',
dataExported: 'Data exported successfully',
dataExportFailed: 'Failed to export data',
copyProxyUrl: 'Copy Proxy URL',
urlCopied: 'Proxy URL copied',
searchProxies: 'Search proxies...',
allProtocols: 'All Protocols',
allStatus: 'All Status',
@@ -2116,6 +2402,7 @@ export default {
name: 'Name',
protocol: 'Protocol',
address: 'Address',
auth: 'Auth',
location: 'Location',
status: 'Status',
accounts: 'Accounts',
@@ -3255,6 +3542,15 @@ export default {
settings: {
title: 'System Settings',
description: 'Manage registration, email verification, default values, and SMTP settings',
tabs: {
general: 'General',
security: 'Security',
users: 'Users',
gateway: 'Gateway',
email: 'Email',
},
emailTabDisabledTitle: 'Email Verification Not Enabled',
emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.',
registration: {
title: 'Registration Settings',
description: 'Control user registration and verification',
@@ -3262,6 +3558,11 @@ export default {
enableRegistrationHint: 'Allow new users to register',
emailVerification: 'Email Verification',
emailVerificationHint: 'Require email verification for new registrations',
emailSuffixWhitelist: 'Email Domain Whitelist',
emailSuffixWhitelistHint:
"Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com)",
emailSuffixWhitelistPlaceholder: 'example.com',
emailSuffixWhitelistInputHint: 'Leave empty for no restriction',
promoCode: 'Promo Code',
promoCodeHint: 'Allow users to use promo codes during registration',
invitationCode: 'Invitation Code Registration',
@@ -3310,7 +3611,29 @@ export default {
defaultBalance: 'Default Balance',
defaultBalanceHint: 'Initial balance for new users',
defaultConcurrency: 'Default Concurrency',
defaultConcurrencyHint: 'Maximum concurrent requests for new users'
defaultConcurrencyHint: 'Maximum concurrent requests for new users',
defaultSubscriptions: 'Default Subscriptions',
defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered',
addDefaultSubscription: 'Add Default Subscription',
defaultSubscriptionsEmpty: 'No default subscriptions configured.',
defaultSubscriptionsDuplicate:
'Duplicate subscription group: {groupId}. Each group can only appear once.',
subscriptionGroup: 'Subscription Group',
subscriptionValidityDays: 'Validity (days)'
},
claudeCode: {
title: 'Claude Code Settings',
description: 'Control Claude Code client access requirements',
minVersion: 'Minimum Version',
minVersionPlaceholder: 'e.g. 2.1.63',
minVersionHint:
'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.'
},
scheduling: {
title: 'Gateway Scheduling Settings',
description: 'Control API Key scheduling behavior',
allowUngroupedKey: 'Allow Ungrouped Key Scheduling',
allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.'
},
site: {
title: 'Site Settings',
@@ -3358,6 +3681,33 @@ export default {
integrationDoc: 'Payment Integration Docs',
integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples'
},
soraClient: {
title: 'Sora Client',
description: 'Control whether to show the Sora client entry in the sidebar',
enabled: 'Enable Sora Client',
enabledHint: 'When enabled, the Sora entry will be shown in the sidebar for users to access Sora features'
},
customMenu: {
title: 'Custom Menu Pages',
description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.',
itemLabel: 'Menu Item #{n}',
name: 'Menu Name',
namePlaceholder: 'e.g. Help Center',
url: 'Page URL',
urlPlaceholder: 'https://example.com/page',
iconSvg: 'SVG Icon',
iconSvgPlaceholder: '<svg>...</svg>',
iconPreview: 'Icon Preview',
uploadSvg: 'Upload SVG',
removeSvg: 'Remove',
visibility: 'Visible To',
visibilityUser: 'Regular Users',
visibilityAdmin: 'Administrators',
add: 'Add Menu Item',
remove: 'Remove',
moveUp: 'Move Up',
moveDown: 'Move Down',
},
smtp: {
title: 'SMTP Settings',
description: 'Configure email sending for verification codes',
@@ -3429,6 +3779,60 @@ export default {
securityWarning: 'Warning: This key provides full admin access. Keep it secure.',
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',
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',
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',
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}?',
columns: {
profile: 'Profile',
active: 'Active',
endpoint: 'Endpoint',
bucket: 'Bucket',
quota: 'Default Quota',
updatedAt: 'Updated At',
actions: 'Actions'
},
enabled: 'Enable S3 Storage',
enabledHint: 'When enabled, Sora generated media files will be automatically uploaded to S3 storage',
endpoint: 'S3 Endpoint',
region: 'Region',
bucket: 'Bucket',
prefix: 'Object Prefix',
accessKeyId: 'Access Key ID',
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',
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'
},
streamTimeout: {
title: 'Stream Timeout Handling',
description: 'Configure account handling strategy when upstream response times out',
@@ -3592,6 +3996,16 @@ export default {
'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.'
},
// Custom Page (iframe embed)
customPage: {
title: 'Custom Page',
openInNewTab: 'Open in new tab',
notFoundTitle: 'Page not found',
notFoundDesc: 'This custom page does not exist or has been removed.',
notConfiguredTitle: 'Page URL not configured',
notConfiguredDesc: 'The URL for this custom page has not been properly configured.',
},
// Announcements Page
announcements: {
title: 'Announcements',
@@ -3787,5 +4201,93 @@ export default {
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click to confirm and create your API key.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important:</b><ul style="margin: 8px 0 0 16px;"><li>Copy the key (sk-xxx) immediately after creation</li><li>Key is only shown once, need to regenerate if lost</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>🚀 How to Use:</b><br/>Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
}
}
},
// Sora Studio
sora: {
title: 'Sora Studio',
description: 'Generate videos and images with Sora AI',
notEnabled: 'Feature Not Available',
notEnabledDesc: 'The Sora Studio feature has not been enabled by the administrator. Please contact your admin.',
tabGenerate: 'Generate',
tabLibrary: 'Library',
noActiveGenerations: 'No active generations',
startGenerating: 'Enter a prompt below to start creating',
storage: 'Storage',
promptPlaceholder: 'Describe what you want to create...',
generate: 'Generate',
generating: 'Generating...',
selectModel: 'Select Model',
statusPending: 'Pending',
statusGenerating: 'Generating',
statusCompleted: 'Completed',
statusFailed: 'Failed',
statusCancelled: 'Cancelled',
cancel: 'Cancel',
delete: 'Delete',
save: 'Save to Cloud',
saved: 'Saved',
retry: 'Retry',
download: 'Download',
justNow: 'Just now',
minutesAgo: '{n} min ago',
hoursAgo: '{n} hr ago',
noSavedWorks: 'No saved works',
saveWorksHint: 'Save your completed generations to the library',
filterAll: 'All',
filterVideo: 'Video',
filterImage: 'Image',
confirmDelete: 'Are you sure you want to delete this work?',
loading: 'Loading...',
loadMore: 'Load More',
noStorageWarningTitle: 'No Storage Configured',
noStorageWarningDesc: 'Generated content is only available via temporary upstream links that expire in ~15 minutes. Consider configuring S3 storage.',
mediaTypeVideo: 'Video',
mediaTypeImage: 'Image',
notificationCompleted: 'Generation Complete',
notificationFailed: 'Generation Failed',
notificationCompletedBody: 'Your {model} task has completed',
notificationFailedBody: 'Your {model} task has failed',
upstreamExpiresSoon: 'Expiring soon',
upstreamExpired: 'Link expired',
upstreamCountdown: '{time} remaining',
previewTitle: 'Preview',
closePreview: 'Close',
beforeUnloadWarning: 'You have unsaved generated content. Are you sure you want to leave?',
downloadTitle: 'Download Generated Content',
downloadExpirationWarning: 'This link expires in approximately 15 minutes. Please download and save promptly.',
downloadNow: 'Download Now',
referenceImage: 'Reference Image',
removeImage: 'Remove',
imageTooLarge: 'Image size cannot exceed 20MB',
// Sora dark theme additions
welcomeTitle: 'Turn your imagination into video',
welcomeSubtitle: 'Enter a description and Sora will create realistic videos or images for you. Try the examples below to get started.',
queueTasks: 'tasks',
queueWaiting: 'Queued',
waiting: 'Waiting',
waited: 'Waited',
errorCategory: 'Content Policy Violation',
savedToCloud: 'Saved to Cloud',
downloadLocal: 'Download',
canDownload: 'to download',
regenrate: 'Regenerate',
creatorPlaceholder: 'Describe the video or image you want to create...',
videoModels: 'Video Models',
imageModels: 'Image Models',
noStorageConfigured: 'No Storage',
selectCredential: 'Select Credential',
apiKeys: 'API Keys',
subscriptions: 'Subscriptions',
subscription: 'Subscription',
noCredentialHint: 'Please create an API Key or contact admin for subscription',
uploadReference: 'Upload reference image',
generatingCount: 'Generating {current}/{max}',
noStorageToastMessage: 'Cloud storage is not configured. Please use "Download" to save files after generation, otherwise they will be lost.',
galleryCount: '{count} works',
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'
}
}

View File

@@ -312,6 +312,8 @@ export default {
passwordMinLength: '密码至少需要 6 个字符',
loginFailed: '登录失败,请检查您的凭据后重试。',
registrationFailed: '注册失败,请重试。',
emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。',
emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}',
loginSuccess: '登录成功!欢迎回来。',
accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。',
reloginRequired: '会话已过期,请重新登录。',
@@ -326,6 +328,16 @@ export default {
sendingCode: '发送中...',
clickToResend: '点击重新发送验证码',
resendCode: '重新发送验证码',
sendCodeDesc: '我们将发送验证码到',
codeSentSuccess: '验证码已发送!请查收您的邮箱。',
verifying: '验证中...',
verifyAndCreate: '验证并创建账户',
resendCountdown: '{countdown}秒后可重新发送',
backToRegistration: '返回注册',
sendCodeFailed: '发送验证码失败,请重试。',
verifyFailed: '验证失败,请重试。',
codeRequired: '请输入验证码',
invalidCode: '请输入有效的6位验证码',
promoCodeLabel: '优惠码',
promoCodePlaceholder: '输入优惠码(可选)',
promoCodeValid: '有效!注册后将获得 ${amount} 赠送余额',
@@ -414,6 +426,7 @@ export default {
noDataAvailable: '暂无数据',
model: '模型',
group: '分组',
noGroup: '无分组',
requests: '请求',
tokens: 'Token',
actual: '实际',
@@ -444,6 +457,9 @@ export default {
keys: {
title: 'API 密钥',
description: '管理您的 API 密钥和访问令牌',
searchPlaceholder: '搜索名称或Key...',
allGroups: '全部分组',
allStatus: '全部状态',
createKey: '创建密钥',
editKey: '编辑密钥',
deleteKey: '删除密钥',
@@ -565,6 +581,19 @@ export default {
resetQuotaConfirmMessage: '确定要将密钥 "{name}" 的已用额度(${used})重置为 0 吗?此操作不可撤销。',
quotaResetSuccess: '额度重置成功',
failedToResetQuota: '重置额度失败',
rateLimitColumn: '速率限制',
rateLimitSection: '速率限制',
resetUsage: '重置',
rateLimit5h: '5小时限额 (USD)',
rateLimit1d: '日限额 (USD)',
rateLimit7d: '7天限额 (USD)',
rateLimitHint: '设置此密钥在指定时间窗口内的最大消费额。0 = 无限制。',
rateLimitUsage: '速率限制用量',
resetRateLimitUsage: '重置速率限制用量',
resetRateLimitTitle: '确认重置速率限制',
resetRateLimitConfirmMessage: '确定要重置密钥 "{name}" 的速率限制用量吗?所有时间窗口的已用额度将归零。此操作不可撤销。',
rateLimitResetSuccess: '速率限制已重置',
failedToResetRateLimit: '重置速率限制失败',
expiration: '密钥有效期',
expiresInDays: '{days} 天',
extendDays: '+{days} 天',
@@ -853,6 +882,7 @@ export default {
noDataAvailable: '暂无数据',
model: '模型',
group: '分组',
noGroup: '无分组',
requests: '请求',
tokens: 'Token',
cache: '缓存',
@@ -2005,7 +2035,12 @@ export default {
strategyHint: '三区模型: 超限后逐步限制; 粘性豁免: 已有会话不受限',
stickyBuffer: '粘性缓冲区',
stickyBufferPlaceholder: '默认: base RPM 的 20%',
stickyBufferHint: '超过 base RPM 后粘性会话额外允许的请求数。为空则使用默认值base RPM 的 20%,最小为 1'
stickyBufferHint: '超过 base RPM 后粘性会话额外允许的请求数。为空则使用默认值base RPM 的 20%,最小为 1',
userMsgQueue: '用户消息限速',
userMsgQueueHint: '对用户消息施加发送限制,避免触发上游 RPM 限制',
umqModeOff: '关闭',
umqModeThrottle: '软性限速',
umqModeSerialize: '串行队列',
},
tlsFingerprint: {
label: 'TLS 指纹模拟',
@@ -2457,6 +2492,7 @@ export default {
name: '名称',
protocol: '协议',
address: '地址',
auth: '认证',
location: '地理位置',
status: '状态',
accounts: '账号数',
@@ -2484,6 +2520,8 @@ export default {
allStatuses: '全部状态'
},
// Additional keys used in ProxiesView
copyProxyUrl: '复制代理 URL',
urlCopied: '代理 URL 已复制',
allProtocols: '全部协议',
allStatus: '全部状态',
searchProxies: '搜索代理...',
@@ -3665,6 +3703,15 @@ export default {
settings: {
title: '系统设置',
description: '管理注册、邮箱验证、默认值和 SMTP 设置',
tabs: {
general: '通用设置',
security: '安全与认证',
users: '用户默认值',
gateway: '网关服务',
email: '邮件设置',
},
emailTabDisabledTitle: '邮箱验证未启用',
emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。',
registration: {
title: '注册设置',
description: '控制用户注册和验证',
@@ -3672,6 +3719,11 @@ export default {
enableRegistrationHint: '允许新用户注册',
emailVerification: '邮箱验证',
emailVerificationHint: '新用户注册时需要验证邮箱',
emailSuffixWhitelist: '邮箱域名白名单',
emailSuffixWhitelistHint:
"仅允许使用指定域名的邮箱注册账号(例如 {'@'}qq.com, {'@'}gmail.com",
emailSuffixWhitelistPlaceholder: 'example.com',
emailSuffixWhitelistInputHint: '留空则不限制',
promoCode: '优惠码',
promoCodeHint: '允许用户在注册时使用优惠码',
invitationCode: '邀请码注册',
@@ -3720,7 +3772,27 @@ export default {
defaultBalance: '默认余额',
defaultBalanceHint: '新用户的初始余额',
defaultConcurrency: '默认并发数',
defaultConcurrencyHint: '新用户的最大并发请求数'
defaultConcurrencyHint: '新用户的最大并发请求数',
defaultSubscriptions: '默认订阅列表',
defaultSubscriptionsHint: '新用户创建或注册时自动分配这些订阅',
addDefaultSubscription: '添加默认订阅',
defaultSubscriptionsEmpty: '未配置默认订阅。新用户不会自动获得订阅套餐。',
defaultSubscriptionsDuplicate: '默认订阅存在重复分组:{groupId}。每个分组只能出现一次。',
subscriptionGroup: '订阅分组',
subscriptionValidityDays: '有效期(天)'
},
claudeCode: {
title: 'Claude Code 设置',
description: '控制 Claude Code 客户端访问要求',
minVersion: '最低版本号',
minVersionPlaceholder: '例如 2.1.63',
minVersionHint: '拒绝低于此版本的 Claude Code 客户端请求semver 格式)。留空则不检查版本。'
},
scheduling: {
title: '网关调度设置',
description: '控制 API Key 的调度行为',
allowUngroupedKey: '允许未分组 Key 调度',
allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403。建议保持关闭以确保所有 Key 都归属明确的分组。'
},
site: {
title: '站点设置',
@@ -3776,6 +3848,27 @@ export default {
enabled: '启用 Sora 客户端',
enabledHint: '开启后,侧边栏将显示 Sora 入口,用户可访问 Sora 功能'
},
customMenu: {
title: '自定义菜单页面',
description: '添加自定义 iframe 页面到侧边栏导航。每个页面可以设置为普通用户或管理员可见。',
itemLabel: '菜单项 #{n}',
name: '菜单名称',
namePlaceholder: '如:帮助中心',
url: '页面 URL',
urlPlaceholder: 'https://example.com/page',
iconSvg: 'SVG 图标',
iconSvgPlaceholder: '<svg>...</svg>',
iconPreview: '图标预览',
uploadSvg: '上传 SVG',
removeSvg: '清除',
visibility: '可见角色',
visibilityUser: '普通用户',
visibilityAdmin: '管理员',
add: '添加菜单项',
remove: '删除',
moveUp: '上移',
moveDown: '下移',
},
smtp: {
title: 'SMTP 设置',
description: '配置用于发送验证码的邮件服务',
@@ -4062,6 +4155,16 @@ export default {
notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。'
},
// Custom Page (iframe embed)
customPage: {
title: '自定义页面',
openInNewTab: '新窗口打开',
notFoundTitle: '页面不存在',
notFoundDesc: '该自定义页面不存在或已被删除。',
notConfiguredTitle: '页面链接未配置',
notConfiguredDesc: '该自定义页面的 URL 未正确配置。',
},
// Announcements Page
announcements: {
title: '公告',