Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import en from '../locales/en'
import zh from '../locales/zh'
describe('usage ipGeo locale keys', () => {
it('contains zh labels for IP geolocation UI', () => {
expect(zh.usage.ipGeo.fetch).toBe('获取地区')
expect(zh.usage.ipGeo.fetching).toBe('获取中...')
expect(zh.usage.ipGeo.failed).toBe('获取失败')
expect(zh.usage.ipGeo.private).toBe('内网地址')
expect(zh.usage.ipGeo.batchFetch).toBe('批量获取地区')
expect(zh.usage.ipGeo.pending).toBe('{count} 个 IP 待获取地区')
})
it('contains en labels for IP geolocation UI', () => {
expect(en.usage.ipGeo.fetch).toBe('Fetch region')
expect(en.usage.ipGeo.fetching).toBe('Fetching...')
expect(en.usage.ipGeo.failed).toBe('Failed')
expect(en.usage.ipGeo.private).toBe('Private address')
expect(en.usage.ipGeo.batchFetch).toBe('Batch fetch regions')
expect(en.usage.ipGeo.pending).toBe('{count} IPs pending')
})
})
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { baseCompile } from '@intlify/message-compiler'
import en from '../locales/en'
import zh from '../locales/zh'
// vue-i18n 在运行时才编译消息:文案里未转义的花括号(如内嵌 JSON 示例
// "{\"user-agent\": ...}")会在渲染时抛 "Invalid token in placeholder"
// 直接炸掉整个组件树,且构建期完全无感。本测试把全部文案预编译一遍,
// 将该类问题固化为显式失败。字面量花括号请用 {'{'} / {'}'} 转义,
// 或将语言中立的示例文本(如 JSON)移出 i18n。
function collectCompileErrors(node: unknown, path: string, out: string[]): void {
if (typeof node === 'string') {
baseCompile(node, {
onError: (err) => {
out.push(`${path}: ${err.message}`)
}
})
return
}
if (Array.isArray(node)) {
node.forEach((item, index) => collectCompileErrors(item, `${path}[${index}]`, out))
return
}
if (node && typeof node === 'object') {
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
collectCompileErrors(value, path ? `${path}.${key}` : key, out)
}
}
}
describe('locale messages compile', () => {
it.each([
['zh', zh],
['en', en]
] as const)('%s messages all compile without placeholder errors', (locale, messages) => {
const errors: string[] = []
collectCompileErrors(messages, locale, errors)
expect(errors).toEqual([])
})
})
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest'
import enAdminAccounts from '../locales/en/admin/accounts'
import enAdminChannels from '../locales/en/admin/channels'
import enAdminOps from '../locales/en/admin/ops'
import enAdminOverview from '../locales/en/admin/overview'
import enAdminResources from '../locales/en/admin/resources'
import enAdminSettings from '../locales/en/admin/settings'
import enCommon from '../locales/en/common'
import enDashboard from '../locales/en/dashboard'
import enLanding from '../locales/en/landing'
import enMisc from '../locales/en/misc'
import zhAdminAccounts from '../locales/zh/admin/accounts'
import zhAdminChannels from '../locales/zh/admin/channels'
import zhAdminOps from '../locales/zh/admin/ops'
import zhAdminOverview from '../locales/zh/admin/overview'
import zhAdminResources from '../locales/zh/admin/resources'
import zhAdminSettings from '../locales/zh/admin/settings'
import zhCommon from '../locales/zh/common'
import zhDashboard from '../locales/zh/dashboard'
import zhLanding from '../locales/zh/landing'
import zhMisc from '../locales/zh/misc'
// locales/{zh,en}/index.ts 与 admin/index.ts 使用对象展开聚合各域模块,
// 展开模块之间若出现同名顶层键会静默覆盖。本测试将该风险固化为显式失败。
type Modules = Record<string, Record<string, unknown>>
function collisions(modules: Modules): string[] {
const seen = new Map<string, string>()
const out: string[] = []
for (const [name, mod] of Object.entries(modules)) {
for (const key of Object.keys(mod)) {
const prev = seen.get(key)
if (prev) {
out.push(`"${key}" in both ${prev} and ${name}`)
} else {
seen.set(key, name)
}
}
}
return out
}
const roots: Record<string, Modules> = {
zh: { landing: zhLanding, common: zhCommon, dashboard: zhDashboard, misc: zhMisc },
en: { landing: enLanding, common: enCommon, dashboard: enDashboard, misc: enMisc }
}
const admins: Record<string, Modules> = {
zh: {
overview: zhAdminOverview,
channels: zhAdminChannels,
accounts: zhAdminAccounts,
resources: zhAdminResources,
ops: zhAdminOps,
settings: zhAdminSettings
},
en: {
overview: enAdminOverview,
channels: enAdminChannels,
accounts: enAdminAccounts,
resources: enAdminResources,
ops: enAdminOps,
settings: enAdminSettings
}
}
describe.each(Object.keys(roots))('locale %s spread assembly', (locale) => {
it('root modules have no overlapping top-level keys', () => {
expect(collisions(roots[locale])).toEqual([])
})
it('root modules do not shadow the explicit "admin" namespace', () => {
for (const [name, mod] of Object.entries(roots[locale])) {
expect(Object.keys(mod), `module ${name} must not define "admin"`).not.toContain('admin')
}
})
it('admin modules have no overlapping top-level keys', () => {
expect(collisions(admins[locale])).toEqual([])
})
})
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import en from '../locales/en'
import zh from '../locales/zh'
describe('OpenAI Fast/Flex policy locale keys', () => {
it('exposes user scope copy at the runtime zh path', () => {
expect(zh.admin.settings.openaiFastPolicy).toMatchObject({
userIds: '指定用户',
userIdsHint: '输入任意邮箱关键词进行模糊搜索。留空表示对全部 Sub2API 用户生效;选中用户的 API Key 请求优先匹配用户规则。',
userSearchPlaceholder: '输入用户邮箱搜索',
userSearchEmpty: '未找到匹配用户',
userDeleted: '(已删除)',
userIdFallback: '用户 #{id}',
removeUser: '移除用户'
})
})
it('exposes user scope copy at the runtime en path', () => {
expect(en.admin.settings.openaiFastPolicy).toMatchObject({
userIds: 'Specific users',
userIdsHint: 'Type any part of a user email to search. Leave empty to apply to all Sub2API users. Selected users match requests from their API keys and take precedence over global rules.',
userSearchPlaceholder: 'Search by user email',
userSearchEmpty: 'No matching users found',
userDeleted: '(deleted)',
userIdFallback: 'User #{id}',
removeUser: 'Remove user'
})
})
it('describes target and other-model actions without whitelist terminology', () => {
expect(zh.admin.settings.openaiFastPolicy).toMatchObject({
tierAll: '全部 tier 值',
modelWhitelist: '目标模型',
fallbackAction: '其他模型处理方式',
summaryTargetModels: '目标模型',
summaryOtherModels: '其他模型'
})
expect(zh.admin.settings.openaiFastPolicy.modelWhitelistHint).toContain(
'留空时“处理方式”应用于全部模型'
)
expect(zh.admin.settings.openaiFastPolicy.modelWhitelistHint).not.toContain('白名单')
expect(en.admin.settings.openaiFastPolicy).toMatchObject({
tierAll: 'All tier values',
modelWhitelist: 'Target models',
fallbackAction: 'Other models action',
summaryTargetModels: 'Target models',
summaryOtherModels: 'Other models'
})
expect(en.admin.settings.openaiFastPolicy.modelWhitelistHint).toContain(
'Leave empty to apply Action to all models'
)
expect(en.admin.settings.openaiFastPolicy.modelWhitelistHint).not.toContain('whitelist')
})
})
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import en from '@/i18n/locales/en'
import zh from '@/i18n/locales/zh'
function flattenKeys(obj: Record<string, any>, prefix = ''): string[] {
const keys: string[] = []
for (const [k, v] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${k}` : k
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
keys.push(...flattenKeys(v, fullKey))
} else {
keys.push(fullKey)
}
}
return keys
}
describe('ops locale key completeness', () => {
const requiredKeys = [
'admin.ops.result',
'admin.ops.timeRange.custom',
'admin.ops.customTimeRange.startTime',
'admin.ops.customTimeRange.endTime',
]
for (const key of requiredKeys) {
it(`en locale has ${key}`, () => {
const enKeys = flattenKeys(en)
expect(enKeys).toContain(key)
})
}
})
describe('groups locale key completeness', () => {
it('en locale has admin.groups.failedToSave', () => {
const enKeys = flattenKeys(en)
expect(enKeys).toContain('admin.groups.failedToSave')
})
const webSearchPricingKeys = [
'admin.groups.webSearchPricing.title',
'admin.groups.webSearchPricing.pricePerCall',
'admin.groups.webSearchPricing.pricePerCallHint',
'admin.groups.webSearchPricing.finalPricePreview',
]
for (const key of webSearchPricingKeys) {
it(`en and zh locales both have ${key}`, () => {
expect(flattenKeys(en)).toContain(key)
expect(flattenKeys(zh)).toContain(key)
})
}
})
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import en from '../locales/en'
import zh from '../locales/zh'
describe('risk control locale copy', () => {
it('describes worker runtime as audit and pre-block record processing', () => {
expect(zh.admin.riskControl.workerStatusHint).toContain('前置拦截记录任务')
expect(zh.admin.riskControl.workerStatusHint).not.toContain('异步观察任务')
expect(en.admin.riskControl.workerStatusHint).toContain('pre-block record tasks')
expect(en.admin.riskControl.workerStatusHint).not.toContain('observation tasks')
})
it('keeps pre-block audit key summary aware of async worker load', () => {
expect(zh.admin.riskControl.preBlockAPIKeyLoadSummary).toContain('worker{workerActive} / {workerTotal}')
expect(en.admin.riskControl.preBlockAPIKeyLoadSummary).toContain('worker: {workerActive} / {workerTotal}')
})
it('does not describe pre-block audit key polling as bypassing the worker pool', () => {
expect(zh.admin.riskControl.preBlockAPIKeyLoadHint).toBe('同步前置拦截直接轮询可用审核 Key。')
expect(zh.admin.riskControl.preBlockAPIKeyLoadHint).not.toContain('Worker 池')
expect(en.admin.riskControl.preBlockAPIKeyLoadHint).not.toContain('worker pool')
})
})
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import en from '../locales/en'
import zh from '../locales/zh'
describe('usage service tier locale keys', () => {
it('contains zh labels for service tier tooltip', () => {
expect(zh.usage.serviceTier).toBe('服务档位')
expect(zh.usage.serviceTierPriority).toBe('Fast')
expect(zh.usage.serviceTierFlex).toBe('Flex')
expect(zh.usage.serviceTierStandard).toBe('Standard')
})
it('contains en labels for service tier tooltip', () => {
expect(en.usage.serviceTier).toBe('Service tier')
expect(en.usage.serviceTierPriority).toBe('Fast')
expect(en.usage.serviceTierFlex).toBe('Flex')
expect(en.usage.serviceTierStandard).toBe('Standard')
})
})
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import en from '../locales/en/admin/accounts'
import zh from '../locales/zh/admin/accounts'
describe('OpenAI WS mode locale descriptions', () => {
it('documents the global v2 router requirement for account WS modes', () => {
expect(zh.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled')
expect(zh.accounts.openai.wsModeDesc).toContain('http_bridge')
expect(en.accounts.openai.wsModeDesc).toContain('mode_router_v2_enabled')
expect(en.accounts.openai.wsModeDesc).toContain('http_bridge')
})
})
+99
View File
@@ -0,0 +1,99 @@
import { createI18n } from 'vue-i18n'
type LocaleCode = 'en' | 'zh'
type LocaleMessages = Record<string, any>
const LOCALE_KEY = 'sub2api_locale'
const DEFAULT_LOCALE: LocaleCode = 'en'
const localeLoaders: Record<LocaleCode, () => Promise<{ default: LocaleMessages }>> = {
en: () => import('./locales/en'),
zh: () => import('./locales/zh')
}
function isLocaleCode(value: string): value is LocaleCode {
return value === 'en' || value === 'zh'
}
function getDefaultLocale(): LocaleCode {
const saved = localStorage.getItem(LOCALE_KEY)
if (saved && isLocaleCode(saved)) {
return saved
}
const browserLang = navigator.language.toLowerCase()
if (browserLang.startsWith('zh')) {
return 'zh'
}
return DEFAULT_LOCALE
}
export const i18n = createI18n({
legacy: false,
locale: getDefaultLocale(),
fallbackLocale: DEFAULT_LOCALE,
messages: {},
// 禁用 HTML 消息警告 - 引导步骤使用富文本内容(driver.js 支持 HTML
// 这些内容是内部定义的,不存在 XSS 风险
warnHtmlMessage: false
})
const loadedLocales = new Set<LocaleCode>()
export async function loadLocaleMessages(locale: LocaleCode): Promise<void> {
if (loadedLocales.has(locale)) {
return
}
const loader = localeLoaders[locale]
const module = await loader()
i18n.global.setLocaleMessage(locale, module.default)
loadedLocales.add(locale)
}
export async function initI18n(): Promise<void> {
const current = getLocale()
await loadLocaleMessages(current)
document.documentElement.setAttribute('lang', current)
}
export async function setLocale(locale: string): Promise<void> {
if (!isLocaleCode(locale)) {
return
}
await loadLocaleMessages(locale)
i18n.global.locale.value = locale
localStorage.setItem(LOCALE_KEY, locale)
document.documentElement.setAttribute('lang', locale)
// 同步更新浏览器页签标题,使其跟随语言切换
const { resolveRouteDocumentTitle } = await import('@/router/title')
const { default: router } = await import('@/router')
const { useAppStore } = await import('@/stores/app')
const { useAuthStore } = await import('@/stores/auth')
const { useAdminSettingsStore } = await import('@/stores/adminSettings')
const route = router.currentRoute.value
const appStore = useAppStore()
const authStore = useAuthStore()
const adminSettingsStore = useAdminSettingsStore()
const customMenuItems = [
...(appStore.cachedPublicSettings?.custom_menu_items ?? []),
...(authStore.isAdmin ? adminSettingsStore.customMenuItems : []),
]
document.title = resolveRouteDocumentTitle(route, appStore.siteName, customMenuItems)
}
export function getLocale(): LocaleCode {
const current = i18n.global.locale.value
return isLocaleCode(current) ? current : DEFAULT_LOCALE
}
export const availableLocales = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh', name: '中文', flag: '🇨🇳' }
] as const
export default i18n
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
export default {
audit: {
title: 'Audit Logs',
description: 'Records management-plane operations by admins and users. Header credentials keep only their first/last characters and request bodies are redacted. Entries cannot be deleted individually; clearing all requires two-factor verification.',
clearAll: 'Clear All',
empty: 'No audit logs yet',
loadFailed: 'Failed to load audit logs',
filters: {
all: 'All',
q: 'Keyword',
qPlaceholder: 'Path / action / actor email',
actorEmail: 'Actor Email',
action: 'Action',
clientIp: 'Client IP',
method: 'Method',
authMethod: 'Auth Method',
result: 'Result',
resultSuccess: 'Success',
resultFailure: 'Failure',
startTime: 'Start Time',
endTime: 'End Time'
},
columns: {
time: 'Time',
actor: 'Actor',
action: 'Action',
method: 'Method',
result: 'Result',
clientIp: 'Client IP',
detail: 'Detail'
},
detail: {
title: 'Audit Log Detail',
actorRole: 'Role',
methodPath: 'Method / Path',
latency: 'Latency',
requestId: 'Request ID',
credential: 'Credential (masked)',
userAgent: 'User-Agent',
requestBody: 'Request Body (redacted)',
extra: 'Extra'
},
clearConfirm: {
title: 'Clear All Audit Logs',
message: 'This permanently deletes all audit logs and cannot be undone. The clear action itself is recorded. Continue?',
totpTitle: 'Enter Two-Factor Code',
totpHint: 'Clearing audit logs requires a fresh TOTP verification.',
success: 'Cleared {count} audit log(s)',
failed: 'Failed to clear audit logs'
}
}
}
@@ -0,0 +1,766 @@
export default {
availableChannels: {
title: 'Available Channels',
description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)',
searchPlaceholder: 'Search channels or models...',
columns: {
name: 'Channel',
status: 'Status',
billingSource: 'Billing Model Source',
groups: 'Linked Groups',
supportedModels: 'Supported Models'
},
empty: 'No data',
noGroups: 'No linked groups',
noModels: 'No model mapping configured',
noPricing: 'Pricing not configured',
statusActive: 'Active',
statusDisabled: 'Disabled',
billingSource: {
requested: 'Requested model',
upstream: 'Upstream model',
channel_mapped: 'Channel-mapped model'
},
pricing: {
billingMode: 'Billing Mode',
billingModeToken: 'Per Token',
billingModePerRequest: 'Per Request',
billingModeImage: 'Per Image',
billingModeVideo: 'Per Video',
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
cacheReadPrice: 'Cache Read',
imageOutputPrice: 'Image Output',
perRequestPrice: 'Per Request',
intervals: 'Tiered Pricing',
unitPerMillion: '/ 1M tokens',
unitPerRequest: '/ request'
}
},
// Channel Management
channels: {
title: 'Channel Management',
description: 'Manage channels and custom model pricing',
searchChannels: 'Search channels...',
createChannel: 'Create Channel',
editChannel: 'Edit Channel',
deleteChannel: 'Delete Channel',
statusActive: 'Active',
statusDisabled: 'Disabled',
allStatus: 'All Status',
groupsUnit: 'groups',
pricingUnit: 'pricing rules',
noChannelsYet: 'No Channels Yet',
createFirstChannel: 'Create your first channel to manage model pricing',
loadError: 'Failed to load channels',
createSuccess: 'Channel created',
updateSuccess: 'Channel updated',
deleteSuccess: 'Channel deleted',
createError: 'Failed to create channel',
updateError: 'Failed to update channel',
deleteError: 'Failed to delete channel',
nameRequired: 'Please enter a channel name',
duplicateModels: 'Model "{0}" appears in multiple pricing entries',
modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range. Model names are matched case-insensitively, so an existing entry already covers all case variants — no need to add the variant separately.",
mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range. Source patterns are matched case-insensitively, so an existing entry already covers all case variants.",
intervalValidation: {
negativeMin: 'Interval #{index}: minimum token count ({value}) cannot be negative',
maxPositive: 'Interval #{index}: maximum token count ({value}) must be greater than 0',
maxGreaterThanMin: 'Interval #{index}: maximum token count ({max}) must be greater than minimum token count ({min})',
negativePrice: 'Interval #{index}: {field} cannot be negative',
multiplierPositive: 'Interval #{index}: {field} must be greater than 0',
unboundedLast: 'Interval #{index}: an unbounded interval (empty maximum token count) must be last',
overlap: 'Intervals #{previousIndex} and #{currentIndex} overlap: previous upper bound ({previousMax}) is greater than current lower bound ({currentMin})',
price: {
inputPrice: 'input price',
outputPrice: 'output price',
cacheWritePrice: 'cache write price',
cacheReadPrice: 'cache read price',
perRequestPrice: 'per-request price'
}
},
timePricingValidation: {
timezone: 'Select a valid IANA time zone',
format: 'Start and end times must use HH:mm:ss format',
range: 'Start time must be earlier than end time; split ranges across midnight',
overlap: 'Time periods must not overlap',
multiplier: 'Multiplier must be greater than 0 with at most two decimal places'
},
deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.',
columns: {
name: 'Name',
description: 'Description',
status: 'Status',
groups: 'Groups',
pricing: 'Pricing',
createdAt: 'Created',
actions: 'Actions'
},
billingMode: {
token: 'Token',
perRequest: 'Per Request',
image: 'Image (Per Request)',
video: 'Video (Per Second)'
},
form: {
name: 'Name',
namePlaceholder: 'Enter channel name',
description: 'Description',
descriptionPlaceholder: 'Optional description',
status: 'Status',
groups: 'Associated Groups',
noGroupsAvailable: 'No groups available',
inOtherChannel: 'In "{name}"',
modelPricing: 'Model Pricing',
models: 'Models',
modelsPlaceholder: 'Type full model name and press Enter',
modelInputHint: 'Press Enter to add, supports paste for batch import.',
billingMode: 'Billing Mode',
defaultPrices: 'Default prices (fallback when no interval matches)',
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
cacheReadPrice: 'Cache Read',
cacheWritePriceShort: 'Cache W',
cacheReadPriceShort: 'Cache R',
imageInputPrice: 'Image Input',
imageTokenPrice: 'Image Output',
imageOutputPrice: 'Image Output Price',
pricePlaceholder: 'Default',
fastMultiplier: 'Fast Multiplier',
flexMultiplier: 'Flex Multiplier',
multiplierPlaceholder: 'Not set',
multiplierPositive: 'Fast/Flex multipliers must be greater than 0',
inputMultiplier: 'Input Mult.',
outputMultiplier: 'Output Mult.',
cacheWriteMultiplier: 'Cache Write Mult.',
cacheReadMultiplier: 'Cache Read Mult.',
intervals: 'Context Intervals (optional)',
timePricing: 'Time-based pricing (optional)',
timezone: 'Time zone',
addTimePeriod: 'Add period',
startTime: 'Start time',
endTime: 'End time',
multiplier: 'Multiplier',
removeTimePeriod: 'Remove period',
minTokens: 'Min',
maxTokens: 'Max',
inclusive: '(inclusive)',
addInterval: 'Add Interval',
requestTiers: 'Request Tiers',
imageTiers: 'Image Tiers (Per Request)',
videoTiers: 'Video Resolution Tiers (Per Second)',
addTier: 'Add Tier',
noTiersYet: 'No tiers yet. Click add to configure per-request pricing.',
noPricingRules: 'No pricing rules yet. Click "Add" to create one.',
perRequestPrice: 'Price per Request',
perRequestPriceRequired: 'Per-request price or billing tiers required for per-request/image billing mode',
tierLabel: 'Tier',
resolution: 'Resolution',
modelMapping: 'Model Mapping',
modelMappingHint: 'Map request model names to actual model names. Runs before account-level mapping.',
noMappingRules: 'No mapping rules. Click "Add" to create one.',
mappingSource: 'Source model',
mappingTarget: 'Target model',
billingModelSource: 'Billing Model',
billingModelSourceChannelMapped: 'Bill by channel-mapped model',
billingModelSourceRequested: 'Bill by requested model',
billingModelSourceUpstream: 'Bill by final upstream model',
billingModelSourceResponse: 'Bill by upstream response model',
billingModelSourceHint: 'Controls which model name is used for pricing lookup',
selectedCount: '{count} selected',
searchGroups: 'Search groups...',
noGroupsMatch: 'No groups match your search',
restrictModels: 'Restrict Models',
restrictModelsHint: 'When enabled, only models in the pricing list are allowed. Others will be rejected.',
defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)',
defaultImagePrice: 'Default image price (fallback when no tier matches)',
defaultVideoPrice: 'Default video price per second (fallback when no tier matches)',
platformConfig: 'Platform Configuration',
webSearchEmulation: 'Web Search Emulation',
webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.',
webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation',
codexImageGenerationBridge: 'Codex Image Generation Bridge',
codexImageGenerationBridgeHint: 'When enabled, only non-Responses Lite Codex /responses text requests in OpenAI groups receive the hosted image_generation tool. The bridge does not inject tools for Responses Lite; local image_gen handling follows the client and account policy. Leave this off unless routed accounts support image generation.',
bedrockCCCompat: 'Bedrock CC Compatibility',
bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).',
basicSettings: 'Basic Settings',
addPlatform: 'Add Platform',
noPlatforms: 'Click "Add Platform" to start configuring the channel',
mappingCount: 'mappings',
pricingEntry: 'Pricing Entry',
noModels: 'No models added',
applyPricingToAccountStats: 'Apply Pricing to Account Stats',
applyPricingToAccountStatsDesc: 'When enabled, requests not matched by custom rules will use standard model pricing for account stats calculation',
accountStatsPricingRules: 'Custom Account Stats Pricing Rules',
addRule: 'Add Rule',
noRulesConfigured: 'No custom rules configured. Channel model pricing above will be used.',
ruleName: 'Rule name (optional)',
ruleGroups: 'Groups',
ruleAccounts: 'Accounts',
searchAccountPlaceholder: 'Search accounts...',
ruleAccountsHint: 'Leave empty to match all accounts',
ruleModelPricing: 'Model Pricing',
noGroupsInChannel: 'No groups selected in platform tabs above',
unnamed: 'Unnamed',
syncLatestModels: 'Sync Latest Models',
syncingModels: 'Syncing...',
syncModelsSuccess: 'Synced {count} new model(s)',
syncModelsAlreadyUpToDate: 'Models already up to date',
syncModelsError: 'Failed to sync models'
}
},
riskControl: {
title: 'Risk Control',
description: 'Configure content moderation and review audit records',
loadFailed: 'Failed to load risk control',
saveFailed: 'Failed to save content moderation config',
logsFailed: 'Failed to load audit records',
saved: 'Content moderation config saved',
refresh: 'Refresh',
config: 'Content Moderation Config',
configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.',
openSettings: 'Moderation Settings',
settingsTitle: 'Content Moderation Settings',
refreshStatus: 'Refresh Status',
records: 'Audit Records',
recordsHint: 'Shows hits, blocks, errors, and sampled records.',
saveConfig: 'Save Moderation Config',
statusFailed: 'Failed to load runtime status',
enabled: 'Enable Content Moderation',
enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.',
mode: 'Global Mode',
modePreBlock: 'Pre-Block',
modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.',
modeObserve: 'Observe Only',
modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.',
modeOff: 'Off',
modeOffDesc: 'Content moderation is disabled and no audit records are written.',
baseUrl: 'OpenAI Base URL',
model: 'Model',
apiKey: 'OpenAI API Key',
apiKeys: 'OpenAI API Keys',
apiKeyCount: '{count} keys',
apiKeyPlaceholder: 'Enter API Key',
apiKeysPlaceholder: 'Add API Keys, one per line. They will be appended on save.',
apiKeysPlaceholderReplace: 'Replace API Keys, one per line. Stored keys will be replaced on save.',
apiKeysPlaceholderKeep: 'Add API Keys, one per line. They will be appended on save.',
apiKeysHint: '{count} keys are currently stored. This input only adds keys; save appends and de-duplicates them.',
apiKeysWriteMode: 'Write mode',
apiKeysModeAppend: 'Add',
apiKeysModeReplace: 'Replace',
apiKeysModeAppendHint: 'Default: save appends input keys and keeps stored keys.',
apiKeysModeReplaceHint: 'Replace mode: save replaces all stored keys with input keys.',
apiKeysReplaceWarning: 'Replace mode',
apiKeysReplaceNoInput: 'Replace mode requires at least 1 API Key',
apiKeyPlaceholderKeep: 'Leave empty to keep current key',
apiKeyWillClear: 'Configured key will be cleared on save',
apiKeyConfigured: 'Configured',
apiKeyTemporary: 'Pending',
apiKeyPendingDelete: 'Pending delete',
apiKeyPendingDeleteCount: '{count} keys pending deletion',
deleteApiKey: 'Delete this key',
undoDeleteApiKey: 'Undo delete',
inputApiKeyCount: '{count} keys in input',
storedApiKeyCount: '{count} stored keys',
testInputApiKeys: 'Test input keys',
testStoredApiKeys: 'Test stored keys',
testContentWithStoredApiKey: 'Test content with stored key',
testingApiKeys: 'Testing',
apiKeyTestNoInput: 'Enter OpenAI API Keys to test first',
apiKeyTestDone: 'Key test completed for {count} keys',
apiKeyTestFailed: 'Failed to test OpenAI API Keys',
apiKeyHealth: 'Key Availability',
apiKeyFreezeRule: '400 does not freeze; 401/403 freeze for 10 minutes; 429/529 freeze for 1 minute; other HTTP errors freeze for 10 seconds.',
apiKeyRows: '{count} keys',
apiKeyRowsCollapsed: '{count} keys hidden',
apiKeyRowsExpanded: 'Showing all {count} keys',
expandApiKeyRows: 'Expand',
collapseApiKeyRows: 'Collapse',
apiKeyHealthEmpty: 'No key status yet',
apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.',
apiKeyStatusOk: 'Available',
apiKeyStatusError: 'Error',
apiKeyStatusFrozen: 'Frozen',
apiKeyStatusUnknown: 'Untested',
apiKeyFailureCount: '{count} failures',
apiKeyLatency: '{ms} ms',
apiKeyHTTPStatus: 'HTTP {status}',
apiKeyFrozenUntil: 'Frozen until {time}',
apiKeyLastChecked: 'Checked at {time}',
apiKeyNotTested: 'Not tested',
auditTestInput: 'Audit Test Input',
auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.',
auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.',
auditTestImages: 'Test Images',
auditTestImagesHint: 'Upload, drag, or paste images. Up to 1 image, 8MB each.',
addAuditTestImage: 'Add image',
clearAuditTest: 'Clear test',
auditTestImageLimit: 'You can add up to {count} test images',
auditTestImageTooLarge: 'Each test image must be 8MB or smaller',
auditTestImageReadFailed: 'Failed to read test image',
auditTestResult: 'Audit Test Result',
auditTestHighest: 'Top category {category}, score {score}',
auditTestComposite: 'Composite score',
auditTestFlagged: 'Threshold hit',
auditTestPassed: 'Pass',
notConfigured: 'Not configured',
clearApiKey: 'Clear stored key',
keepApiKey: 'Keep stored key',
timeoutMs: 'HTTP Timeout (ms)',
retryCount: 'Retry Count',
sampleRate: 'Sample Rate',
proxy: 'Proxy Server',
proxyHint: 'Send moderation requests through the selected proxy (IP Management - Proxy Servers), useful when the egress IP is not supported by OpenAI. Defaults to direct connection.',
recordNonHits: 'Record Non-Hits',
recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.',
preHashCheck: 'Enable Pre-Hash Check',
preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.',
flaggedHashCount: 'Current hash collection size: {count}',
flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.',
flaggedHashPlaceholder: 'Paste full 64-character input hash',
deleteFlaggedHash: 'Delete hash',
clearFlaggedHashes: 'Clear all',
clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.',
flaggedHashDeleted: 'Risk hash deleted',
flaggedHashNotFound: 'Risk hash not found',
flaggedHashDeleteFailed: 'Failed to delete risk hash',
flaggedHashesCleared: 'Cleared {count} risk hashes',
flaggedHashesClearFailed: 'Failed to clear risk hashes',
workerCount: 'Worker Count',
queueSize: 'Async Queue Size',
blockStatus: 'Block HTTP Status',
blockMessage: 'Custom Block Message',
defaultBlockMessage: 'Content audit matched a risk rule. Please adjust your input and try again.',
emailOnHit: 'Email on Hit',
emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.',
autoBan: 'Auto Ban User',
autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.',
cyberPolicyExcludeBan: 'Exclude Cyber Policy Hits from Ban Count',
cyberPolicyExcludeBanHint: 'When enabled, cyber_policy hits no longer count toward auto-ban violations: no ban judgment on the hit itself, and history rows are excluded from the rolling count. Logs and notice emails are unaffected.',
violationNotCounted: 'Not counted',
banThreshold: 'Ban Threshold',
violationWindowHours: 'Count Window (hours)',
hitRetentionDays: 'Hit Record Retention (days)',
nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)',
violationCount: '{count} hits',
emailSent: 'Email sent',
emailNotSent: 'No email',
autoBanned: 'Banned',
unbanUser: 'Unban',
unbanSuccess: 'User has been unbanned',
unbanFailed: 'Failed to unban user',
inputDetailTitle: 'Input Summary Detail',
inputDetailContent: 'Full Content',
matchedKeyword: 'Matched Keyword',
queueDelay: 'Queued {ms} ms',
allGroups: 'All Groups',
allGroupsHint: 'Auditing all groups',
selectedGroupsHint: 'Auditing selected groups',
groupScope: 'Audit Groups',
groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.',
selectedGroups: 'Selected Groups',
searchGroups: 'Search group name or platform',
noGroups: 'No groups available',
modelFilter: 'Model scope',
modelFilterHint: 'Moderate by the client-requested model name; channel model mappings do not change this match.',
modelFilterAll: 'All models',
modelFilterAllDesc: 'All model requests go through content moderation.',
modelFilterInclude: 'Only selected',
modelFilterIncludeDesc: 'Only listed models go through content moderation.',
modelFilterExclude: 'Exclude selected',
modelFilterExcludeDesc: 'Listed models skip content moderation; other models are moderated.',
modelFilterModels: 'Model list',
modelFilterModelCount: '{count} models configured',
modelFilterModelsRequired: 'This model scope requires at least 1 model',
modelFilterAllSummary: 'Applies to all models',
modelFilterIncludeSummary: 'Applies to {count} models',
modelFilterExcludeSummary: 'Excludes {count} models',
emptyLogs: 'No audit records',
preBlockSyncStatus: 'Pre-Block Sync Status',
preBlockSyncHint: 'Live counters for the synchronous moderation path, excluding async record tasks.',
preBlockActive: 'Sync Processing',
preBlockActiveHint: 'Currently checking',
preBlockChecked: 'Checked',
preBlockCheckedHint: 'Entered pre-block path',
preBlockAllowed: 'Allowed',
preBlockAllowedHint: 'No block triggered',
preBlockBlocked: 'Blocked',
preBlockBlockedHint: 'Rejected after hit',
preBlockErrors: 'Audit Errors',
preBlockErrorsHint: 'Failed or no usable key',
preBlockAvgLatency: 'Avg Latency',
preBlockAvgLatencyHint: 'Synchronous path average',
preBlockAPIKeyLoad: 'Audit Key Load',
preBlockAPIKeyLoadHint: 'Synchronous pre-block checks round-robin usable audit keys directly.',
preBlockAPIKeyLoadSummary: 'Sync active {active} / usable keys {available}, {total} total, worker: {workerActive} / {workerTotal}',
preBlockAPIKeyTotals: 'Total {total}, success {success}, errors {errors}',
preBlockAPIKeyLoadEmpty: 'No audit key load data yet',
preBlockKeyActiveShort: 'Active',
preBlockKeyTotalShort: 'Total',
preBlockKeyAvgShort: 'Avg',
preBlockKeyLastShort: 'Last',
workerStatus: 'Worker Runtime',
workerStatusHint: 'Queue and worker pool status for async audit tasks and pre-block record tasks, excluding synchronous pre-block checks.',
workerPool: 'Worker Pool',
workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total',
queueUsage: 'Queue Usage',
activeWorkers: 'Processing',
idleWorkers: 'Idle Ready',
workerActive: 'Processing an async audit or record task',
workerIdle: 'Started, idle and ready',
workerDisabled: 'Risk control or content audit is disabled',
processed: 'Processed',
droppedErrors: 'Dropped / Errors',
autoRefresh: 'Auto refresh every 15s',
lastCleanup: 'Last cleanup: {time}',
cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits',
riskSwitchOff: 'System switch off',
riskThresholds: 'Risk Thresholds',
riskThresholdsHint: 'Adjust hit thresholds by OpenAI Moderations category. Scores greater than or equal to the threshold count as hits.',
riskThresholdDefault: 'Default {value}',
riskThresholdReset: 'Restore defaults',
riskThresholdPercent: 'Threshold percentage',
tabs: {
basic: 'Basic',
scope: 'Scope',
runtime: 'Runtime',
response: 'Hit Notice',
riskThresholds: 'Risk Thresholds',
keywords: 'Keyword Block',
retention: 'Retention',
},
blockedKeywords: 'Blocked keywords',
blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2',
blockedKeywordsDescription: 'Matching is case-insensitive. Whether the upstream moderation API is invoked after a hit depends on the strategy below.',
blockedKeywordsPreBlockHint: 'Keyword blocking only takes effect in "Pre-block" mode.',
blockedKeywordsModeWarning: 'Current mode is "{mode}". Keyword blocking will not run until you switch to "Pre-block" mode.',
blockedKeywordCount: '{count} keywords configured',
blockedKeywordsLimit: 'Up to {max} keywords, each no longer than 200 characters. Duplicates are removed automatically.',
keywordBlockingMode: 'Moderation strategy',
keywordModeKeywordAndApi: 'Keyword + API',
keywordModeKeywordAndApiDesc: 'Block on keyword hit; otherwise fall through to the upstream moderation API.',
keywordModeKeywordOnly: 'Keyword only',
keywordModeKeywordOnlyDesc: 'Decide using keywords only; misses are allowed without calling the API, saving upstream cost.',
keywordModeKeywordOnlyNotice: 'Keyword-only strategy: requests that do not match any keyword are allowed without calling the upstream moderation API.',
keywordModeApiOnly: 'API only',
keywordModeApiOnlyDesc: 'Use the upstream moderation API only; the keyword list configured here is not consulted.',
keywordModeApiOnlyNotice: 'API-only strategy: the keyword list is not consulted; all requests go through the upstream moderation API.',
overview: {
status: 'Status',
enabled: 'Enabled',
disabled: 'Disabled',
apiKey: 'API Key',
groupScope: 'Scope',
logs: 'Audit Records',
currentFilter: 'Current filter',
},
filters: {
search: 'Search user/key/summary',
from: 'From',
to: 'To',
allGroups: 'All Groups',
allEndpoints: 'All Endpoints',
},
table: {
time: 'Time',
group: 'Group',
user: 'User',
apiKey: 'API Key',
endpoint: 'Endpoint',
result: 'Result',
highest: 'Highest',
actionMeta: 'Action',
latency: 'Latency',
input: 'Input Summary',
},
result: {
all: 'All Results',
hit: 'Hit',
blocked: 'Blocked',
pass: 'Pass',
error: 'Error',
},
action: {
block: 'Blocked',
keywordBlock: 'Keyword Blocked',
cyberPolicy: 'Cyber policy',
error: 'Error',
},
},
// Channel Monitor
channelMonitor: {
title: 'Channel Monitor',
description: 'Monitor channel availability, latency and status',
searchPlaceholder: 'Search monitor name...',
allProviders: 'All Providers',
allStatus: 'All Status',
enabledFilter: 'Enabled',
onlyEnabled: 'Enabled only',
onlyDisabled: 'Disabled only',
createButton: 'Create Monitor',
createTitle: 'Create Channel Monitor',
editTitle: 'Edit Channel Monitor',
runNow: 'Run Now',
runSuccess: 'Check completed',
runFailed: 'Check failed',
duplicate: 'Duplicate',
duplicating: 'Duplicating',
duplicateSuccess: 'Monitor duplicated as "{name}" and disabled. Review its configuration before enabling it.',
duplicateFailed: 'Failed to duplicate monitor',
duplicateKeyUnavailable: 'The API key cannot be decrypted. Re-enter it before duplicating this monitor.',
apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.',
createSuccess: 'Monitor created',
updateSuccess: 'Monitor updated',
deleteSuccess: 'Monitor deleted',
loadError: 'Failed to load monitors',
deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.',
nameRequired: 'Please enter a monitor name',
primaryModelRequired: 'Please enter a primary model',
linkedAccountRequired: 'Please select a linked account',
columns: {
name: 'Name',
provider: 'Provider',
primaryModel: 'Primary Model',
availability7d: '7d Availability',
latency: 'Latency (ms)',
enabled: 'Enabled',
actions: 'Actions'
},
form: {
name: 'Name',
namePlaceholder: 'Enter monitor name',
provider: 'Platform',
checkMode: 'Check Mode',
checkModeProbe: 'Probe',
checkModeProbeHint: 'Sends a lightweight LLM request to measure availability and latency',
checkModeQuota: 'Quota',
checkModeQuotaHint: 'Only queries the linked account usage windows / balance without probe requests',
checkModeQuotaProbe: 'Probe + Quota',
checkModeQuotaProbeHint: 'Probes the channel and attaches the quota snapshot to the primary model result',
linkedAccount: 'Linked Account',
linkedAccountPlaceholder: 'Select an account',
linkedAccountHint: 'Quota data comes from the selected account (reuses the account-side usage/balance queries)',
linkedAccountEmpty: 'No accounts on this platform yet. Add one in Account Management first',
linkedAccountMissing: 'The linked account no longer exists or is not accessible. Please re-select an account',
openAIQuotaProbeHint: 'Note: on the OpenAI platform the usage query may trigger a Codex probe request that consumes the account\'s own quota (at most once every 10 minutes)',
apiMode: 'OpenAI protocol',
apiModeChatCompletions: 'OpenAI Compatible',
apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.',
apiModeResponses: 'Responses API',
apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.',
endpoint: 'Endpoint',
endpointPlaceholder: 'https://api.example.com',
useCurrentDomain: 'Use current service',
apiKey: 'API Key',
apiKeyPlaceholder: 'Enter API Key',
apiKeyEditPlaceholder: 'Leave blank to keep current key',
useMyKey: 'Use my key',
selectKeyTitle: 'Select my API Key',
selectKeyHint: 'Only your active, non-expired keys are listed.',
noActiveKey: 'No active API keys available',
primaryModel: 'Primary Model',
primaryModelPlaceholder: 'gpt-4o-mini',
extraModels: 'Extra Models',
extraModelsPlaceholder: 'Press Enter to add extra model',
groupName: 'Group Name',
groupNamePlaceholder: 'Optional, used to group rows in user view',
intervalSeconds: 'Interval (seconds)',
intervalSecondsHint: 'Range: 15 - 3600 seconds',
jitterSeconds: 'Random Jitter (± seconds)',
jitterSecondsHint: 'Each check fires at interval ± a random offset within this value; 0 means fixed interval. Interval minus jitter must be ≥ 15s',
enabled: 'Enable monitor',
kindRequired: 'Please select a provider'
},
runResultTitle: 'Check Result',
noMonitorsYet: 'No monitors yet',
createFirstMonitor: 'Create your first monitor to track channel availability',
advanced: {
section: 'Advanced (optional)',
sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").',
headers: 'Custom request headers',
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
headerNamePlaceholder: 'Header name',
headerValuePlaceholder: 'Value',
headerAddRow: 'Add header',
headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}',
headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.',
headersParseError: 'Cannot parse line: {line}',
bodyMode: 'Body handling',
bodyModeOff: 'Default',
bodyModeMerge: 'Merge',
bodyModeReplace: 'Replace',
bodyModeHintOff: 'Use the adapter default body (includes challenge validation).',
bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).',
bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.',
bodyJson: 'Body JSON',
bodyJsonFormat: 'Format',
bodyJsonHint: 'Parsed on blur. Empty means no override.',
bodyJsonError: 'JSON parse failed',
bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)'
},
templateField: {
label: 'Request template',
none: 'No template',
placeholder: 'Pick a template (filtered by current provider)',
applyHint: 'Picking a template copies its headers and body to this monitor (snapshot). Later template edits are not auto-synced.'
},
template: {
manageButton: 'Templates',
managerTitle: 'Request template manager',
createButton: 'New template',
emptyState: 'No templates for this provider yet',
missingName: 'Template name is required',
createSuccess: 'Template created',
updateSuccess: 'Template updated',
deleteSuccess: 'Template deleted',
applyButton: 'Apply to monitors',
applyTooltip: 'Overwrite snapshot fields on associated monitors',
applyTitle: 'Apply template',
applyConfirm: 'Apply',
applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.',
applySuccess: 'Applied to {n} monitor(s)',
applyPickerTitle: 'Apply template "{name}"',
applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.',
applyPickerEmpty: 'No monitors are currently associated to this template',
applyPickerConfirm: 'Apply to {n} monitor(s)',
selectNone: 'Select none',
selectedCount: 'Selected {n} / {total}',
deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.',
associatedCount: '{n} associated monitor(s)',
headersSummary: '{n} custom header(s)',
form: {
name: 'Template name',
namePlaceholder: 'e.g. Claude Code mimicry',
description: 'Description',
descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.'
}
}
},
// Subscriptions
subscriptions: {
title: 'Subscription Management',
description: 'Manage user subscriptions and quota limits',
assignSubscription: 'Assign Subscription',
adjustSubscription: 'Adjust Subscription',
revokeSubscription: 'Revoke Subscription',
restoreSubscription: 'Restore Subscription',
allStatus: 'All Status',
allGroups: 'All Groups',
allPlatforms: 'All Platforms',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
noLimits: 'No limits configured',
unlimited: 'Unlimited',
resetNow: 'Resetting soon',
windowNotActive: 'Window not active',
resetInMinutes: 'Resets in {minutes}m',
resetInHoursMinutes: 'Resets in {hours}h {minutes}m',
resetInDaysHours: 'Resets in {days}d {hours}h',
quotaEndsInMinutes: 'Quota ends in {minutes}m',
quotaEndsInHoursMinutes: 'Quota ends in {hours}h {minutes}m',
quotaEndsInDaysHours: 'Quota ends in {days}d {hours}h',
daysRemaining: '{days} days remaining',
hoursMinutesRemaining: '{hours}h {minutes}m remaining',
minutesRemaining: '{minutes}m remaining',
remainingDays: 'Remaining days',
noExpiration: 'No expiration',
status: {
active: 'Active',
expired: 'Expired',
revoked: 'Revoked',
suspended: 'Suspended'
},
columns: {
user: 'User',
group: 'Group',
usage: 'Usage',
expires: 'Expires',
status: 'Status',
actions: 'Actions'
},
form: {
user: 'User',
group: 'Subscription Group',
validityDays: 'Validity (Days)',
adjustDays: 'Adjust by (Days)'
},
selectUser: 'Select a user',
selectGroup: 'Select a subscription group',
groupHint: 'Only groups with subscription billing type are shown',
validityHint: 'Number of days the subscription will be valid',
adjustingFor: 'Adjusting subscription for',
currentExpiration: 'Current expiration',
adjustDaysPlaceholder: 'Positive to extend, negative to shorten',
adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)',
assign: 'Assign',
assigning: 'Assigning...',
adjust: 'Adjust',
adjusting: 'Adjusting...',
revoke: 'Revoke',
restore: 'Restore',
resetQuota: 'Reset Quota',
resetQuotaTitle: 'Reset Usage Quota',
resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.",
quotaResetSuccess: 'Quota reset successfully',
failedToResetQuota: 'Failed to reset quota',
noSubscriptionsYet: 'No subscriptions yet',
assignFirstSubscription: 'Assign a subscription to get started.',
subscriptionAssigned: 'Subscription assigned successfully',
subscriptionAdjusted: 'Subscription adjusted successfully',
subscriptionRevoked: 'Subscription revoked successfully',
subscriptionRestored: 'Subscription restored successfully',
failedToLoad: 'Failed to load subscriptions',
failedToAssign: 'Failed to assign subscription',
failedToAdjust: 'Failed to adjust subscription',
failedToRevoke: 'Failed to revoke subscription',
failedToRestore: 'Failed to restore subscription',
adjustWouldExpire: 'Remaining days after adjustment must be greater than 0',
adjustOutOfRange: 'Adjustment days must be between -36500 and 36500',
pleaseSelectUser: 'Please select a user',
pleaseSelectGroup: 'Please select a group',
validityDaysRequired: 'Please enter a valid number of days (at least 1)',
revokeConfirm:
"Are you sure you want to revoke the subscription for '{user}'? You can restore it later from the revoked list.",
restoreConfirm:
"Restore the subscription for '{user}'? If the original subscription has expired, it will be restored as expired.",
guide: {
title: 'Subscription Management Guide',
subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.',
showGuide: 'Usage Guide',
step1: {
title: 'Create a Subscription Group',
line1: 'Go to "Group Management" page, click "Create Group"',
line2: 'Set billing type to "Subscription", configure daily/weekly/monthly quota limits',
line3: 'Save the group and ensure its status is "Active"',
link: 'Go to Group Management'
},
step2: {
title: 'Assign Subscription to User',
line1: 'Click the "Assign Subscription" button in the top right',
line2: 'Search for a user by email and select them',
line3: 'Choose a subscription group, set validity days, then click "Assign"'
},
step3: {
title: 'Manage Existing Subscriptions'
},
actions: {
adjust: 'Adjust',
adjustDesc: 'Extend or shorten the subscription validity period',
resetQuota: 'Reset Quota',
resetQuotaDesc: 'Reset daily/weekly/monthly usage to zero',
revoke: 'Revoke',
revokeDesc: 'Immediately terminate the subscription (restorable from the revoked list)'
},
tip: 'Tip: Only groups with billing type "Subscription" and status "Active" appear in the group dropdown. If no options are available, create one in Group Management first.'
}
},
// Accounts
}
@@ -0,0 +1,19 @@
import overview from './overview'
import channels from './channels'
import accounts from './accounts'
import resources from './resources'
import ops from './ops'
import settings from './settings'
import audit from './audit'
import promptAudit from './promptAudit'
export default {
...overview,
...channels,
...accounts,
...resources,
...ops,
...settings,
...audit,
...promptAudit,
}
+809
View File
@@ -0,0 +1,809 @@
export default {
ops: {
title: 'Ops Monitoring',
description: 'Operational monitoring and troubleshooting',
// Dashboard
systemHealth: 'System Health',
overview: 'Overview',
noSystemMetrics: 'No system metrics collected yet.',
collectedAt: 'Collected at:',
window: 'window',
memory: 'Memory',
db: 'DB',
goroutines: 'Goroutines',
jobs: 'Jobs',
jobsHelp: 'Click “Details” to view job heartbeats and recent errors',
active: 'active',
idle: 'idle',
waiting: 'waiting',
conns: 'conns',
queue: 'queue',
accountSwitches: 'Account switches',
ok: 'ok',
lastRun: 'last_run:',
lastSuccess: 'last_success:',
lastError: 'last_error:',
result: 'Result',
noData: 'No data.',
loadingText: 'loading',
ready: 'ready',
autoRefreshRemaining: 'Remaining {seconds}s',
systemLogs: {
title: 'System Logs',
description: 'Newest logs are shown first. Filter, search, and clean up by condition.',
queue: 'Queue',
written: 'Written',
dropped: 'Dropped',
failed: 'Failed',
runtimeConfig: 'Runtime Log Configuration (applies immediately)',
all: 'All',
level: 'Level',
stacktraceThreshold: 'Stacktrace threshold',
samplingInitial: 'Sampling initial',
samplingThereafter: 'Sampling thereafter',
retentionDays: 'Retention days',
caller: 'caller',
sampling: 'sampling',
saveAndApply: 'Save and apply',
resetDefaults: 'Reset defaults',
latestWriteError: 'Latest write error:',
timeRange: 'Time range',
startTime: 'Start time (optional)',
endTime: 'End time (optional)',
host: 'Host',
component: 'Component',
componentPlaceholder: 'e.g. http.access',
keyId: 'KEY ID',
platform: 'Platform',
model: 'Model',
keyword: 'Keyword',
keywordPlaceholder: 'message/request_id',
search: 'Search',
cleanCurrentFilters: 'Clean current filters',
refreshHealth: 'Refresh health',
empty: 'No system logs',
time: 'Time',
logDetails: 'Log Details',
loadFailed: 'Failed to load system logs',
runtimeConfigActive: 'Runtime log configuration is active',
runtimeConfigSaveFailed: 'Failed to save log configuration',
resetRuntimeConfigConfirm: 'Reset to startup configuration (env/yaml) and apply immediately?',
runtimeConfigReset: 'Reset to startup log configuration',
runtimeConfigResetFailed: 'Failed to reset log configuration',
cleanupConfirm: 'Clean up system logs matching the current filters? This cannot be undone.',
cleanupSuccess: 'Cleanup complete. Deleted {count} log entries.',
cleanupFilterRequired: 'Cleanup requires at least one filter condition (start/end time or another field)',
cleanupFailed: 'Failed to clean up system logs'
},
requestsTotal: 'Requests (total)',
slaScope: 'SLA scope:',
tokens: 'Tokens',
tps: 'TPS:',
current: 'current',
peak: 'peak',
average: 'average',
totalRequests: 'Total Requests',
avgQps: 'Avg QPS',
avgTps: 'Avg TPS',
avgLatency: 'Avg Request Duration',
avgTtft: 'Avg TTFT',
exceptions: 'Exceptions',
requestErrors: 'Request Errors',
errorCount: 'Error Count',
upstreamErrors: 'Upstream Errors',
errorCountExcl429529: 'Error Count (excl 429/529)',
sla: 'SLA (excl business limits)',
businessLimited: 'business_limited:',
errors: 'Errors',
errorRate: 'error_rate:',
upstreamRate: 'upstream_rate:',
latencyDuration: 'Request Duration',
ttftLabel: 'TTFT (first_token_ms)',
p50: 'p50:',
p90: 'p90:',
p95: 'p95:',
p99: 'p99:',
avg: 'avg:',
max: 'max:',
requests: 'Requests',
requestsTitle: 'Requests',
upstream: 'Upstream',
client: 'Client',
system: 'System',
other: 'Other',
errorsSla: 'Errors (SLA scope)',
upstreamExcl429529: 'Upstream (excl 429/529)',
failedToLoadData: 'Failed to load ops data.',
failedToLoadOverview: 'Failed to load overview',
failedToLoadThroughputTrend: 'Failed to load throughput trend',
failedToLoadSwitchTrend: 'Failed to load avg account switches trend',
failedToLoadLatencyHistogram: 'Failed to load request duration histogram',
failedToLoadErrorTrend: 'Failed to load error trend',
failedToLoadErrorDistribution: 'Failed to load error distribution',
failedToLoadErrorDetail: 'Failed to load error detail',
retryFailed: 'Retry failed',
tpsK: 'TPS (K)',
top: 'Top:',
throughputTrend: 'Throughput Trend',
switchRateTrend: 'Avg Account Switches',
latencyHistogram: 'Request Duration Histogram',
errorTrend: 'Error Trend',
errorDistribution: 'Error Distribution',
switchRate: 'Avg switches',
// Health Score & Diagnosis
health: 'Health',
healthCondition: 'Health Condition',
healthHelp: 'Overall system health score based on SLA, error rate, and resource usage',
healthyStatus: 'Healthy',
riskyStatus: 'At Risk',
idleStatus: 'Idle',
timeRange: {
'5m': 'Last 5 minutes',
'30m': 'Last 30 minutes',
'1h': 'Last 1 hour',
'1d': 'Last 1 day',
'15d': 'Last 15 days',
'6h': 'Last 6 hours',
'24h': 'Last 24 hours',
'7d': 'Last 7 days',
'30d': 'Last 30 days',
custom: 'Custom Range'
},
customTimeRange: {
startTime: 'Start Time',
endTime: 'End Time'
},
openaiTokenStats: {
title: 'OpenAI Token Request Stats',
viewModeTopN: 'TopN',
viewModePagination: 'Pagination',
prevPage: 'Previous',
nextPage: 'Next',
pageInfo: 'Page {page}/{total}',
totalModels: 'Total models: {total}',
failedToLoad: 'Failed to load OpenAI token stats',
empty: 'No OpenAI token stats for the current filters',
table: {
model: 'Model',
requestCount: 'Requests',
avgTokensPerSec: 'Avg Tokens/sec',
avgFirstTokenMs: 'Avg First Token Latency (ms)',
totalOutputTokens: 'Total Output Tokens',
avgDurationMs: 'Avg Duration (ms)',
requestsWithFirstToken: 'Requests With First Token'
}
},
fullscreen: {
enter: 'Enter Fullscreen'
},
diagnosis: {
title: 'Smart Diagnosis',
footer: 'Automated diagnostic suggestions based on current metrics',
idle: 'System is currently idle',
idleImpact: 'No active traffic',
// Resource diagnostics
dbDown: 'Database connection failed',
dbDownImpact: 'All database operations will fail',
dbDownAction: 'Check database service status, network connectivity, and connection configuration',
redisDown: 'Redis connection failed',
redisDownImpact: 'Cache functionality degraded, performance may decline',
redisDownAction: 'Check Redis service status and network connectivity',
cpuCritical: 'CPU usage critically high ({usage}%)',
cpuCriticalImpact: 'System response slowing, may affect all requests',
cpuCriticalAction: 'Check CPU-intensive tasks, consider scaling or code optimization',
cpuHigh: 'CPU usage elevated ({usage}%)',
cpuHighImpact: 'System load is high, needs attention',
cpuHighAction: 'Monitor CPU trends, prepare scaling plan',
memoryCritical: 'Memory usage critically high ({usage}%)',
memoryCriticalImpact: 'May trigger OOM, system stability threatened',
memoryCriticalAction: 'Check for memory leaks, consider increasing memory or optimizing usage',
memoryHigh: 'Memory usage elevated ({usage}%)',
memoryHighImpact: 'Memory pressure is high, needs attention',
memoryHighAction: 'Monitor memory trends, check for memory leaks',
ttftHigh: 'Time to first token elevated ({ttft}ms)',
ttftHighImpact: 'User perceived latency increased',
ttftHighAction: 'Optimize request processing flow, reduce pre-processing time',
// Error rate diagnostics
upstreamCritical: 'Upstream error rate critically high ({rate}%)',
upstreamCriticalImpact: 'May affect many user requests',
upstreamCriticalAction: 'Check upstream service health, enable fallback strategies',
upstreamHigh: 'Upstream error rate elevated ({rate}%)',
upstreamHighImpact: 'Recommend checking upstream service status',
upstreamHighAction: 'Contact upstream service team, prepare fallback plan',
errorHigh: 'Error rate too high ({rate}%)',
errorHighImpact: 'Many requests failing',
errorHighAction: 'Check error logs, identify root cause, urgent fix required',
errorElevated: 'Error rate elevated ({rate}%)',
errorElevatedImpact: 'Recommend checking error logs',
errorElevatedAction: 'Analyze error types and distribution, create fix plan',
// SLA diagnostics
slaCritical: 'SLA critically below target ({sla}%)',
slaCriticalImpact: 'User experience severely degraded',
slaCriticalAction: 'Urgently investigate errors and latency, consider rate limiting',
slaLow: 'SLA below target ({sla}%)',
slaLowImpact: 'Service quality needs attention',
slaLowAction: 'Analyze SLA decline causes, optimize system performance',
// Health score diagnostics
healthCritical: 'Overall health score critically low ({score})',
healthCriticalImpact: 'Multiple metrics may be degraded; prioritize error rate and latency investigation',
healthCriticalAction: 'Comprehensive system check, prioritize critical-level issues',
healthLow: 'Overall health score low ({score})',
healthLowImpact: 'May indicate minor instability; monitor SLA and error rates',
healthLowAction: 'Monitor metric trends, prevent issue escalation',
healthy: 'All system metrics normal',
healthyImpact: 'Service running stable'
},
// Error Log
errorLog: {
timeId: 'Time / ID',
commonErrors: {
contextDeadlineExceeded: 'context deadline exceeded',
connectionRefused: 'connection refused',
rateLimit: 'rate limit'
},
time: 'Time',
type: 'Type',
context: 'Context',
platform: 'Platform',
model: 'Model',
group: 'Group',
user: 'User',
userId: 'User ID',
apiKey: 'API Key',
keyDeletedBadge: 'Key Deleted',
account: 'Account',
accountId: 'Account ID',
status: 'Status',
message: 'Message',
ip: 'IP',
latency: 'Request Duration',
action: 'Action',
noErrors: 'No errors in this window.',
grp: 'GRP:',
acc: 'ACC:',
details: 'Details',
phase: 'Phase',
id: 'ID:',
typeUpstream: 'Upstream',
typeRequest: 'Request',
typeAuth: 'Auth',
typeAccountAuth: 'Account Auth',
typeRouting: 'Routing',
typeInternal: 'Internal',
endpoint: 'Endpoint',
requestType: 'Type',
requestTypeSync: 'Sync',
requestTypeStream: 'Stream',
requestTypeWs: 'WS'
},
// Error Details Modal
errorDetails: {
upstreamErrors: 'Upstream Errors',
requestErrors: 'Request Errors',
unresolved: 'Unresolved',
resolved: 'Resolved',
viewErrors: 'Errors',
viewExcluded: 'Excluded',
statusCodeOther: 'Other',
owner: {
provider: 'Provider',
client: 'Client',
platform: 'Platform'
},
phase: {
request: 'Request',
auth: 'Auth',
account_auth: 'Account Auth',
routing: 'Routing',
upstream: 'Upstream',
network: 'Network',
internal: 'Internal'
},
total: 'Total:',
searchPlaceholder: 'Search request_id / client_request_id / message',
},
// Error Detail Modal
errorDetail: {
title: 'Error Detail',
titleWithId: 'Error #{id}',
noErrorSelected: 'No error selected.',
resolution: 'Resolved:',
failedToUpdateResolvedStatus: 'Failed to update resolved status',
classificationKeys: {
phase: 'Phase',
owner: 'Owner',
source: 'Source',
resolvedAt: 'Resolved At',
resolvedBy: 'Resolved By'
},
source: {
upstream_http: 'Upstream HTTP'
},
upstreamKeys: {
status: 'Status',
message: 'Message',
detail: 'Detail',
upstreamErrors: 'Upstream Errors'
},
upstreamEvent: {
account: 'Account',
status: 'Status',
requestId: 'Request ID'
},
responsePreview: {
expand: 'Response (click to expand)',
collapse: 'Response (click to collapse)'
},
loading: 'Loading…',
requestId: 'Request ID',
time: 'Time',
phase: 'Phase',
status: 'Status',
message: 'Message',
basicInfo: 'Basic Info',
platform: 'Platform',
model: 'Model',
group: 'Group',
user: 'User',
account: 'Account',
latency: 'Request Duration',
businessLimited: 'Business Limited',
requestPath: 'Request Path',
inboundEndpoint: 'Inbound Endpoint',
upstreamEndpoint: 'Upstream Endpoint',
requestedModel: 'Requested Model',
upstreamModel: 'Upstream Model',
requestType: 'Request Type',
requestTypeUnknown: 'Unknown',
requestTypeSync: 'Sync',
requestTypeStream: 'Stream',
requestTypeWs: 'WebSocket',
modelMapping: 'Model Mapping',
timings: 'Timings',
auth: 'Auth',
routing: 'Routing',
upstream: 'Upstream',
response: 'Response',
classification: 'Classification',
errorBody: 'Error Body',
trimmed: 'trimmed',
markResolved: 'Mark resolved',
markUnresolved: 'Mark unresolved',
tabOverview: 'Overview',
tabRequest: 'Request',
tabResponse: 'Response',
responseBody: 'Response',
compareA: 'Compare A',
compareB: 'Compare B',
suggestion: 'Suggestion',
suggestUpstream: 'Upstream instability: check account status or consider switching accounts',
suggestRequest: 'Client request error: ask customer to fix request parameters',
suggestAuth: 'Auth failed: verify API key/credentials',
suggestPlatform: 'Platform error: prioritize investigation and fix',
suggestGeneric: 'See details for more context',
apiKeyPrefix: 'Key Prefix',
keyDeletedBadge: 'Key Deleted'
},
requestDetails: {
title: 'Request Details',
details: 'Details',
rangeLabel: 'Window: {range}',
rangeMinutes: '{n} minutes',
rangeHours: '{n} hours',
empty: 'No requests in this window.',
emptyHint: 'Try a different time range or remove filters.',
failedToLoad: 'Failed to load request details',
requestIdCopied: 'Request ID copied',
copyFailed: 'Copy failed',
copy: 'Copy',
viewError: 'View Error',
kind: {
success: 'SUCCESS',
error: 'ERROR'
},
table: {
time: 'Time',
kind: 'Kind',
platform: 'Platform',
model: 'Model',
duration: 'Duration',
status: 'Status',
requestId: 'Request ID',
actions: 'Actions'
}
},
alertEvents: {
title: 'Alert Events',
description: 'Recent alert firing/resolution records (email-only)',
loading: 'Loading...',
empty: 'No alert events',
loadFailed: 'Failed to load alert events',
status: {
firing: 'FIRING',
resolved: 'RESOLVED',
manualResolved: 'MANUAL RESOLVED'
},
detail: {
title: 'Alert Detail',
loading: 'Loading detail...',
empty: 'No detail',
loadFailed: 'Failed to load alert detail',
manualResolve: 'Mark as Resolved',
manualResolvedSuccess: 'Marked as manually resolved',
manualResolvedFailed: 'Failed to mark as manually resolved',
silence: 'Ignore Alert',
silenceSuccess: 'Alert silenced',
silenceFailed: 'Failed to silence alert',
viewRule: 'View Rule',
viewLogs: 'View Logs',
firedAt: 'Fired At',
resolvedAt: 'Resolved At',
ruleId: 'Rule ID',
dimensions: 'Dimensions',
historyTitle: 'History',
historyHint: 'Recent events with same rule + dimensions',
historyLoading: 'Loading history...',
historyEmpty: 'No history'
},
table: {
time: 'Time',
status: 'Status',
severity: 'Severity',
platform: 'Platform',
ruleId: 'Rule ID',
title: 'Title',
duration: 'Duration',
metric: 'Metric / Threshold',
dimensions: 'Dimensions',
email: 'Email Sent',
emailSent: 'Sent',
emailIgnored: 'Ignored'
}
},
alertRules: {
title: 'Alert Rules',
description: 'Create and manage threshold-based system alerts (email-only)',
loading: 'Loading...',
empty: 'No alert rules',
loadFailed: 'Failed to load alert rules',
saveFailed: 'Failed to save alert rule',
saveSuccess: 'Alert rule saved successfully',
deleteFailed: 'Failed to delete alert rule',
deleteSuccess: 'Alert rule deleted successfully',
manage: 'Manage Alert Rules',
create: 'Create Rule',
createTitle: 'Create Alert Rule',
editTitle: 'Edit Alert Rule',
deleteConfirmTitle: 'Delete this rule?',
deleteConfirmMessage: 'This will remove the rule and its related events. Continue?',
metricGroups: {
system: 'System Metrics',
group: 'Group-level Metrics (requires group_id)',
account: 'Account-level Metrics'
},
metrics: {
successRate: 'Success Rate (%)',
errorRate: 'Error Rate (%)',
upstreamErrorRate: 'Upstream Error Rate (%)',
p95: 'P95 Latency (ms)',
p99: 'P99 Latency (ms)',
cpu: 'CPU Usage (%)',
memory: 'Memory Usage (%)',
queueDepth: 'Concurrency Queue Depth',
groupAvailableAccounts: 'Group Available Accounts',
groupAvailableRatio: 'Group Available Ratio (%)',
groupRateLimitRatio: 'Group Rate Limit Ratio (%)',
accountRateLimitedCount: 'Rate-limited Accounts',
accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)',
accountErrorRatio: 'Error Account Ratio (%)',
accountTempUnscheduledCount: 'Temporarily Unschedulable Accounts',
overloadAccountCount: 'Overloaded Accounts'
},
metricDescriptions: {
successRate: 'Percentage of successful requests in the window (0-100).',
errorRate: 'Percentage of failed requests in the window (0-100).',
upstreamErrorRate: 'Percentage of upstream failures in the window (0-100).',
p95: 'P95 request latency within the window (ms).',
p99: 'P99 request latency within the window (ms).',
cpu: 'Current instance CPU usage (0-100).',
memory: 'Current instance memory usage (0-100).',
queueDepth: 'Concurrency queue depth within the window (queued requests).',
groupAvailableAccounts: 'Number of available accounts in the selected group (requires group_id).',
groupAvailableRatio: 'Available account ratio in the selected group (0-100, requires group_id).',
groupRateLimitRatio: 'Rate-limited account ratio in the selected group (0-100, requires group_id).',
accountRateLimitedCount: 'Number of rate-limited accounts within the window.',
accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).',
accountErrorRatio: 'Error account ratio within the window (0-100).',
accountTempUnscheduledCount: 'Number of accounts currently temporarily unschedulable (e.g. proxy/credential failure auto-eviction).',
overloadAccountCount: 'Number of overloaded accounts within the window.'
},
hints: {
recommended: 'Recommended: operator {operator}, threshold {threshold}{unit}',
groupRequired: 'This is a group-level metric; selecting a group (group_id) is required.',
groupOptional: 'Optional: limit the rule to a specific group via group_id.'
},
table: {
name: 'Name',
metric: 'Metric',
severity: 'Severity',
enabled: 'Enabled',
actions: 'Actions'
},
form: {
name: 'Name',
description: 'Description',
metric: 'Metric',
operator: 'Operator',
groupId: 'Group (group_id)',
groupPlaceholder: 'Select a group',
allGroups: 'All groups',
threshold: 'Threshold',
severity: 'Severity',
window: 'Window (minutes)',
sustained: 'Sustained (samples)',
cooldown: 'Cooldown (minutes)',
enabled: 'Enabled',
notifyEmail: 'Send email notifications'
},
validation: {
title: 'Please fix the following issues',
invalid: 'Invalid rule',
nameRequired: 'Name is required',
metricRequired: 'Metric is required',
groupIdRequired: 'group_id is required for group-level metrics',
operatorRequired: 'Operator is required',
thresholdRequired: 'Threshold must be a number',
windowRange: 'Window must be one of: 1, 5, 60 minutes',
sustainedRange: 'Sustained must be between 1 and 1440 samples',
cooldownRange: 'Cooldown must be between 0 and 1440 minutes'
}
},
runtime: {
title: 'Ops Runtime Settings',
description: 'Stored in database; changes take effect without editing config files.',
loading: 'Loading...',
noData: 'No runtime settings available',
loadFailed: 'Failed to load runtime settings',
saveSuccess: 'Runtime settings saved',
saveFailed: 'Failed to save runtime settings',
alertTitle: 'Alert Evaluator',
groupAvailabilityTitle: 'Group Availability Monitor',
evalIntervalSeconds: 'Evaluation Interval (seconds)',
silencing: {
title: 'Alert Silencing (Maintenance Mode)',
enabled: 'Enable silencing',
globalUntil: 'Silence until (RFC3339)',
untilHint: 'Leave empty to only toggle silencing without an expiry (not recommended).',
reason: 'Reason',
reasonPlaceholder: 'e.g., planned maintenance',
entries: {
title: 'Advanced: targeted silencing',
hint: 'Optional: silence only certain rules or severities. Leave fields empty to match all.',
add: 'Add Entry',
empty: 'No targeted entries',
entryTitle: 'Entry #{n}',
ruleId: 'Rule ID (optional)',
ruleIdPlaceholder: 'e.g., 1',
severities: 'Severities (optional)',
severitiesPlaceholder: 'e.g., P0,P1 (empty = all)',
until: 'Until (RFC3339)',
reason: 'Reason',
validation: {
untilRequired: 'Entry until time is required',
untilFormat: 'Entry until time must be a valid RFC3339 timestamp',
ruleIdPositive: 'Entry rule_id must be a positive integer',
severitiesFormat: 'Entry severities must be a comma-separated list of P0..P3'
}
},
validation: {
timeFormat: 'Silence time must be a valid RFC3339 timestamp'
}
},
lockEnabled: 'Distributed Lock Enabled',
lockKey: 'Distributed Lock Key',
lockTTLSeconds: 'Distributed Lock TTL (seconds)',
showAdvancedDeveloperSettings: 'Show advanced developer settings (Distributed Lock)',
advancedSettingsSummary: 'Advanced settings (Distributed Lock)',
evalIntervalHint: 'How often the evaluator runs. Keeping the default is recommended.',
validation: {
title: 'Please fix the following issues',
invalid: 'Invalid settings',
evalIntervalRange: 'Evaluation interval must be between 1 and 86400 seconds',
lockKeyRequired: 'Distributed lock key is required when lock is enabled',
lockKeyPrefix: 'Distributed lock key must start with "{prefix}"',
lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts',
lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds',
slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100',
ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0',
requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100',
upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100'
}
},
email: {
title: 'Email Notification',
description: 'Configure alert/report email notifications (stored in database).',
loading: 'Loading...',
noData: 'No email notification config',
loadFailed: 'Failed to load email notification config',
saveSuccess: 'Email notification config saved',
saveFailed: 'Failed to save email notification config',
alertTitle: 'Alert Emails',
reportTitle: 'Report Emails',
recipients: 'Recipients',
recipientsHint: 'If empty, the system may fallback to the first admin email.',
minSeverity: 'Min Severity',
minSeverityAll: 'All severities',
rateLimitPerHour: 'Rate limit per hour',
batchWindowSeconds: 'Batch window (seconds)',
includeResolved: 'Include resolved alerts',
dailySummary: 'Daily summary',
weeklySummary: 'Weekly summary',
errorDigest: 'Error digest',
errorDigestMinCount: 'Min errors for digest',
accountHealth: 'Account health',
accountHealthThreshold: 'Error rate threshold (%)',
cronPlaceholder: 'Cron expression',
reportHint: 'Schedules use cron syntax; leave empty to use defaults.',
validation: {
title: 'Please fix the following issues',
invalid: 'Invalid email notification config',
alertRecipientsRequired: 'Alert emails are enabled but no recipients are configured',
reportRecipientsRequired: 'Report emails are enabled but no recipients are configured',
invalidRecipients: 'One or more recipient emails are invalid',
rateLimitRange: 'Rate limit per hour must be a number ≥ 0',
batchWindowRange: 'Batch window must be between 0 and 86400 seconds',
cronRequired: 'A cron expression is required when schedule is enabled',
cronFormat: 'Cron expression format looks invalid (expected at least 5 parts)',
digestMinCountRange: 'Min errors for digest must be a number ≥ 0',
accountHealthThresholdRange: 'Account health threshold must be between 0 and 100'
}
},
settings: {
title: 'Ops Monitoring Settings',
loadFailed: 'Failed to load settings',
saveSuccess: 'Ops monitoring settings saved successfully',
saveFailed: 'Failed to save settings',
dataCollection: 'Data Collection',
evaluationInterval: 'Evaluation Interval (seconds)',
evaluationIntervalHint: 'Frequency of detection tasks, recommended to keep default',
alertConfig: 'Alert Configuration',
enableAlert: 'Enable Alerts',
alertRecipients: 'Alert Recipient Emails',
emailPlaceholder: 'Enter email address',
recipientsHint: 'If empty, the system will use the first admin email as default recipient',
minSeverity: 'Minimum Severity',
reportConfig: 'Report Configuration',
enableReport: 'Enable Reports',
reportRecipients: 'Report Recipient Emails',
dailySummary: 'Daily Summary',
weeklySummary: 'Weekly Summary',
metricThresholds: 'Metric Thresholds',
metricThresholdsHint: 'Configure alert thresholds for metrics, values exceeding thresholds will be displayed in red',
slaMinPercent: 'SLA Minimum Percentage',
slaMinPercentHint: 'SLA below this value will be displayed in red (default: 99.5%)',
ttftP99MaxMs: 'TTFT P99 Maximum (ms)',
ttftP99MaxMsHint: 'TTFT P99 above this value will be displayed in red (default: 500ms)',
requestErrorRateMaxPercent: 'Request Error Rate Maximum (%)',
requestErrorRateMaxPercentHint: 'Request error rate above this value will be displayed in red (default: 5%)',
upstreamErrorRateMaxPercent: 'Upstream Error Rate Maximum (%)',
upstreamErrorRateMaxPercentHint: 'Upstream error rate above this value will be displayed in red (default: 5%)',
advancedSettings: 'Advanced Settings',
dataRetention: 'Data Retention Policy',
enableCleanup: 'Enable Data Cleanup',
cleanupSchedule: 'Cleanup Schedule (Cron)',
cleanupScheduleHint: 'Example: 0 2 * * * means 2 AM daily',
errorLogRetentionDays: 'Error Log Retention Days',
minuteMetricsRetentionDays: 'Minute Metrics Retention Days',
hourlyMetricsRetentionDays: 'Hourly Metrics Retention Days',
retentionDaysHint: 'Recommended 7-90 days; longer periods consume more storage. Set to 0 to wipe all history on every scheduled cleanup',
aggregation: 'Pre-aggregation Tasks',
enableAggregation: 'Enable Pre-aggregation',
aggregationHint: 'Pre-aggregation improves query performance for long time windows',
openaiQuotaAutoPause: 'OpenAI Account Quota Auto-pause',
openaiQuotaAutoPauseHint: 'When an OpenAI account reaches its 5h / 7d usage threshold, the scheduler skips it automatically and resumes once the window rolls over. Per-account thresholds take precedence over this global default.',
openaiQuotaAutoPauseDefault5h: 'Default 5h usage threshold (%)',
openaiQuotaAutoPauseDefault7d: 'Default 7d usage threshold (%)',
openaiQuotaAutoPauseThresholdHint: 'Value 0-100; leave blank or 0 to disable the global default threshold.',
errorFiltering: 'Error Filtering',
ignoreCountTokensErrors: 'Ignore count_tokens errors',
ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.',
ignoreContextCanceled: 'Ignore client disconnect errors',
ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.',
ignoreNoAvailableAccounts: 'Ignore no available accounts errors',
ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).',
ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors',
ignoreInsufficientBalanceErrorsHint: 'When enabled, insufficient account balance errors will not be written to the error log.',
autoRefresh: 'Auto Refresh',
enableAutoRefresh: 'Enable auto refresh',
enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.',
refreshInterval: 'Refresh Interval',
refreshInterval15s: '15 seconds',
refreshInterval30s: '30 seconds',
refreshInterval60s: '60 seconds',
dashboardCards: 'Dashboard Cards',
displayAlertEvents: 'Display alert events',
displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.',
displayOpenAITokenStats: 'Display OpenAI token request stats',
displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.',
autoRefreshCountdown: 'Auto refresh: {seconds}s',
validation: {
title: 'Please fix the following issues',
retentionDaysRange: 'Retention days must be between 0 and 365 (0 = wipe all on every cleanup)',
slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100',
ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0',
requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100',
upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100',
openaiQuotaAutoPauseRange: 'OpenAI quota auto-pause threshold must be between 0 and 100'
}
},
concurrency: {
title: 'Concurrency / Queue',
byPlatform: 'By Platform',
byGroup: 'By Group',
byAccount: 'By Account',
byUser: 'By User',
showByUserTooltip: 'Switch to user view to see concurrency usage per user',
switchToUser: 'Switch to user view',
switchToPlatform: 'Switch to platform view',
totalRows: '{count} rows',
disabledHint: 'Realtime monitoring is disabled in settings.',
empty: 'No data',
queued: 'Queue {count}',
rateLimited: 'Rate-limited {count}',
errorAccounts: 'Errors {count}',
loadFailed: 'Failed to load concurrency data'
},
realtime: {
title: 'Realtime',
connected: 'Realtime connected',
connecting: 'Realtime connecting',
reconnecting: 'Realtime reconnecting',
offline: 'Realtime offline',
closed: 'Realtime closed',
reconnectIn: 'retry in {seconds}s'
},
queryMode: {
auto: 'Auto',
raw: 'Raw',
preagg: 'Preagg'
},
accountAvailability: {
available: 'Available',
unavailable: 'Unavailable',
accountError: 'Error'
},
tooltips: {
totalRequests: 'Total number of requests (including both successful and failed requests) in the selected time window.',
throughputTrend: 'Requests/QPS + Tokens/TPS in the selected window.',
switchRateTrend: 'Trend of account switches / total requests over the last 5 hours (avg switches).',
latencyHistogram: 'Request duration distribution (ms) for successful requests.',
errorTrend: 'Error counts over time (SLA scope excludes business limits; upstream excludes 429/529).',
errorDistribution: 'Error distribution by status code (SLA scope, excluding business limits).',
goroutines:
'Number of Go runtime goroutines (lightweight threads). There is no absolute "safe" number—use your historical baseline. Heuristic: <2k is common; 2k8k watch; >8k plus rising queue/latency often suggests blocking/leaks.',
cpu: 'CPU usage percentage, showing system processor load.',
memory: 'Memory usage, including used and total available memory.',
db: 'Database connection pool status, including active, idle, and waiting connections.',
redis: 'Redis connection pool status, showing active and idle connections.',
jobs: 'Background job execution status, including last run time, success time, and error information.',
qps: 'Queries Per Second (QPS) and Tokens Per Second (TPS), real-time system throughput.',
tokens: 'Total number of tokens processed in the current time window.',
sla: 'Service Level Agreement success rate, excluding business limits (e.g., insufficient balance, quota exceeded).',
errors: 'Error statistics, including total errors, error rate, and upstream error rate.',
upstreamErrors: 'Upstream error statistics, excluding rate limit errors (429/529).',
latency: 'Request duration statistics, including p50, p90, p95, p99 percentiles.',
ttft: 'Time To First Token, measuring the speed of first token return in streaming responses.',
health: 'System health score (0-100), considering SLA, error rate, and resource usage.'
},
charts: {
emptyRequest: 'No requests in this window.',
emptyError: 'No errors in this window.',
resetZoom: 'Reset',
resetZoomHint: 'Reset zoom (if enabled)',
downloadChart: 'Download',
downloadChartHint: 'Download chart as image'
}
},
// Settings
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
export default {
promptAudit: {
title: 'Prompt Audit',
description: 'Review user input asynchronously or block it synchronously through OpenAI-compatible Qwen3Guard nodes. Full prompts are stored with events for admin review.',
configVersion: 'Config version v{version}',
tabs: { config: 'Configuration', events: 'Events' },
actions: { refresh: 'Refresh runtime', retry: 'Retry', Allow: 'Allow', Warn: 'Warn', Block: 'Block' },
common: { actions: 'Actions', never: 'Never' },
mode: { off: 'Off', async_audit: 'Async audit only', blocking: 'Synchronous audit and block' },
status: { disabled: 'Disabled', running: 'Running', degraded: 'Degraded', error: 'Error', healthy: 'Healthy', failed: 'Failed', stale: 'Stale heartbeat' },
decisions: { pass: 'Pass', flag: 'Flag', critical: 'Critical' },
riskLevels: { low: 'Low', medium: 'Medium', high: 'High', critical: 'Critical' },
scanners: {
violent: 'Violent',
non_violent_illegal_acts: 'Non-violent Illegal Acts',
sexual_content_or_sexual_acts: 'Sexual Content or Sexual Acts',
pii: 'PII',
suicide_and_self_harm: 'Suicide & Self-Harm',
unethical_acts: 'Unethical Acts',
politically_sensitive_topics: 'Politically Sensitive Topics',
copyright_violation: 'Copyright Violation',
jailbreak: 'Jailbreak',
},
scannerDescriptions: {
violent: 'Violence or threats of violence',
non_violent_illegal_acts: 'Non-violent illegal activity',
sexual_content_or_sexual_acts: 'Sexual content or sexual acts',
pii: 'Personal identifying information',
suicide_and_self_harm: 'Suicide or self-harm',
unethical_acts: 'Unethical behavior',
politically_sensitive_topics: 'Politically sensitive topics',
copyright_violation: 'Copyright infringement',
jailbreak: 'Prompt injection or jailbreak attempt',
},
runtime: {
title: 'Runtime overview',
description: 'Shows the configuration currently active on the server. Unsaved draft changes do not affect these values.',
process: 'Process status', mode: 'Effective mode', version: 'Active / expected version', workers: 'Active / total workers',
queue: 'Active jobs / capacity', dependencies: 'Dependencies', guardMetrics: 'Synchronous Guard metrics', latest: 'Latest processing and error',
queueBreakdown: 'queued {queued} · processing {processing} · retry {retry} · done {done} · failed {failed}',
deliveryTotals: 'Total enqueued {enqueued} · dropped {dropped} · processed {processed} · failed {failed}',
},
metrics: { total: 'Total', allowed: 'Allowed', flagged: 'Flagged', blocked: 'Blocked', unavailable: 'Unavailable', timeouts: 'Timeouts', failovers: 'Failovers' },
pool: {
title: 'Audit pool', description: 'Enabled OpenAI-compatible nodes are tried in order. Probes run from the server network.',
add: 'Add node', edit: 'Edit node', empty: 'No audit nodes configured.', node: 'Node', model: 'Model', limits: 'Timeout / chunk limit', credential: 'Credential and probe',
configured: 'API Key configured', missing: 'API Key missing', invalid: 'API Key cannot be decrypted; re-enter it', probe: 'Test connection', probing: 'Probing…',
probeProgress: 'Config validated ✓ · request sent · awaiting service response…', probeResult: 'Config ✓ · request ✓ · HTTP {http} · {status} · {latency} ms',
name: 'Node name', id: 'Stable node ID', baseUrl: 'Base URL', apiKey: 'API Key', keepSecret: 'Leave blank to keep the saved API Key', reenterSecret: 'The saved API Key cannot be decrypted (encryption key changed); enter a new one',
secretHint: 'Plaintext exists only in this editor and is cleared immediately after a successful save.', clearSecret: 'Explicitly clear the saved API Key', timeout: 'Total timeout (ms)', inputLimit: 'Unicode characters per chunk',
toggleNode: 'Toggle node {name}', deleteConfirm: 'Remove “{name}” from the draft? It takes effect after saving.',
},
policy: {
title: 'Audit policy', description: 'Configure group scope, nine input-risk categories, workers, and queue bounds.', scope: 'Scope', allGroups: 'All groups', selectedGroups: 'Selected groups',
searchGroups: 'Search groups', noGroups: 'No matching groups', missingGroups: 'Configured IDs for groups that no longer exist', selectedCount: '{count} groups selected',
scanners: 'Qwen3Guard input-risk categories', workerCount: 'Worker count', queueCapacity: 'Persistent queue capacity', strategy: 'Node strategy', strategyHint: 'Try nodes in configuration order and fail over when allowed.',
},
saveBar: { enabled: 'Enable prompt audit', blocking: 'Synchronous blocking', blockingLatestTurnOnly: 'Only latest input and prior output', storePass: 'Store safe events', dirty: 'Unsaved changes', synced: 'Configuration synced' },
blockingConfirm: {
title: 'Enable synchronous blocking?',
message: 'Applicable requests wait for Guard before account selection, billing, or upstream access. Block, unavailable Guard, and invalid responses all prevent upstream access.',
confirm: 'I understand; enable it',
},
events: {
title: 'Audit events', description: 'Review events by identity, route, risk, hash, and time; the detail view shows the full prompt.', decision: 'Decision', risk: 'Risk level', endpoint: 'Endpoint', groupId: 'Group ID', userId: 'User ID', apiKeyId: 'API Key ID', keyword: 'Keyword',
startAt: 'Start time', endAt: 'End time', deleteSelected: 'Delete selected ({count})', deleteByFilter: 'Delete by filter',
filterDeleteDialogTitle: 'Delete audit events by filter', filterDeleteDialogDesc: 'Choose the time range and risk criteria, then delete directly. Deletion is permanent. Generate a preview first if you want to see the match count.',
filterTimeRange: 'Deletion time range', filterTimeRangeHint: 'Deletes events created before the selected cutoff. Events created after the preview are not affected.',
timePresets: { '1d': 'Older than 1 day', '7d': 'Older than 7 days', '30d': 'Older than 30 days', '90d': 'Older than 90 days', all: 'All time', custom: 'Custom range' },
customRangeInvalid: 'A custom range needs a valid start and end time, with the start before the end.',
moreConditions: 'More conditions (endpoint / keyword / group / user)',
filterDeletePreviewAction: 'Generate delete preview', filterDeletePreviewing: 'Generating preview…', filterDeleteNeedPreview: 'You can delete directly, or generate a preview first to see the match count.',
filterDeleteConfirmInvalidRange: 'Select a valid deletion time range first (a custom range needs a start before the end).', filterDeleteConfirmNoMatches: 'The current filters matched 0 events, so there is nothing to delete.',
selectAll: 'Select all events on this page', selectEvent: 'Select event {id}', time: 'Time', identity: 'User / email / API Key', user: 'Username', email: 'User email', apiKey: 'API Key name', group: 'Group', route: 'Endpoint / model', result: 'Decision / risk', preview: 'Redacted preview', empty: 'No matching events.',
passEventsDisabled: '“Store safe events” is off. Safe requests are still audited but do not appear in this list; Flag and Critical risk events are still stored.', openConfiguration: 'Open configuration',
detailTitle: 'Prompt audit event details', tabs: { summary: 'Audit summary', risks: 'Specific risks', technical: 'Technical details' },
promptFull: 'Full prompt (unredacted)',
promptFullHint: 'The full prompt is stored with this event for admin review only. Treat it as sensitive data and do not share it.',
guardReturn: 'Model audit return',
guardReturnHint: 'Normalized Guard result (decision, categories, scores, and redacted evidence). Raw response bodies are not stored.',
riskSummaries: 'Risk summaries',
evidence: 'Redacted evidence',
score: 'Score',
categories: 'Categories', model: 'Model', stage: 'Request stage', noRisks: 'No derived risk summaries for this event.',
requestId: 'Request ID', promptHash: 'Prompt SHA-256',
technical: {
scanner: 'Scanner', policy: 'Policy', guardEndpoint: 'Guard endpoint', config: 'Config',
chunks: 'Chunks', latency: 'Latency', protocol: 'Protocol',
},
deleteConfirmTitle: 'Delete audit events?', deleteConfirmMessage: 'This permanently deletes {count} events and eligible orphan jobs.', filterDeleteCount: 'The server snapshot matches {count} events.', snapshotMax: 'Snapshot maximum event ID', expiresAt: 'Confirmation token expires', filterDeleteWarning: 'Only events at or below the preview high-water mark are deleted. Newer events survive. Any filter change requires a new preview.', confirmFilterDelete: 'Permanently delete',
},
messages: { saved: 'Prompt Audit configuration saved; plaintext API Key state was cleared.', probeSucceeded: 'The audit node is reachable.', deleted: 'Deleted {count} audit events.' },
errors: {
loadConfig: 'Unable to load Prompt Audit configuration.', loadRuntime: 'Unable to load Prompt Audit runtime.', loadGroups: 'Unable to load groups.', loadEvents: 'Unable to load audit events.', loadDetail: 'Unable to load event details.', saveConfig: 'Unable to save the configuration.', probe: 'Node probe failed.', delete: 'Unable to delete events.', previewDelete: 'Unable to create a deletion preview. Check the time range.', deleteConfirmation: 'The deletion confirmation is invalid or expired. Preview again.',
prompt_audit_config_conflict: 'Another administrator updated this configuration. Reload the server version before deciding how to merge your draft.',
prompt_audit_encryption_key_required: 'No fixed encryption key is configured, so audit node API Keys would be lost on restart. Set the TOTP_ENCRYPTION_KEY environment variable and restart the service first.',
prompt_guard_requires_audit_enabled: 'Enable Prompt Audit before synchronous blocking.', prompt_audit_invalid_endpoint: 'The audit node configuration is invalid.', prompt_audit_endpoint_required: 'Enable at least one audit node before enabling Prompt Audit.', prompt_audit_groups_required: 'Select at least one group in selected-group mode.', prompt_audit_scanners_required: 'Enable at least one risk category.',
},
},
}
@@ -0,0 +1,605 @@
export default {
scheduledTests: {
title: 'Scheduled Tests',
addPlan: 'Add Plan',
editPlan: 'Edit Plan',
deletePlan: 'Delete Plan',
model: 'Model',
cronExpression: 'Cron Expression',
enabled: 'Enabled',
lastRun: 'Last Run',
nextRun: 'Next Run',
maxResults: 'Max Results',
noPlans: 'No scheduled test plans',
confirmDelete: 'Are you sure you want to delete this plan?',
createSuccess: 'Plan created successfully',
updateSuccess: 'Plan updated successfully',
deleteSuccess: 'Plan deleted successfully',
results: 'Test Results',
noResults: 'No test results yet',
responseText: 'Response',
errorMessage: 'Error',
success: 'Success',
failed: 'Failed',
running: 'Running',
schedule: 'Schedule',
cronHelp: 'Standard 5-field cron expression (e.g., */30 * * * *)',
cronTooltipTitle: 'Cron expression examples:',
cronTooltipMeaning: 'Defines when the test runs automatically. The 5 fields are: minute, hour, day, month, and weekday.',
cronTooltipExampleEvery30Min: '*/30 * * * *: run every 30 minutes',
cronTooltipExampleHourly: '0 * * * *: run at the start of every hour',
cronTooltipExampleDaily: '0 9 * * *: run every day at 09:00',
cronTooltipExampleWeekly: '0 9 * * 1: run every Monday at 09:00',
cronTooltipRange: 'Recommended range: use standard 5-field cron. For health checks, start with a moderate frequency such as every 30 minutes, every hour, or once a day instead of running too often.',
maxResultsTooltipTitle: 'What Max Results means:',
maxResultsTooltipMeaning: 'Sets how many historical test results are kept for a single plan so the result list does not grow without limit.',
maxResultsTooltipBody: 'Only the newest test results are kept. Once the number of saved results exceeds this value, older records are pruned automatically so the history list and storage stay under control.',
maxResultsTooltipExample: 'For example, 100 means keeping at most the latest 100 test results. When the 101st result is saved, the oldest one is removed.',
maxResultsTooltipRange: 'Recommended range: usually 20 to 200. Use 20-50 when you only care about recent health status, or 100-200 if you want a longer trend history.',
autoRecover: 'Auto Recover',
autoRecoverHelp: 'Automatically recover account from error/rate-limited state on successful test'
},
// Proxies
proxies: {
title: 'Proxy Management',
description: 'Manage proxy servers for accounts',
createProxy: 'Create Proxy',
editProxy: 'Edit Proxy',
deleteProxy: 'Delete Proxy',
ad: {
inline: 'Need proxy IP?'
},
deleteConfirmMessage: "Are you sure you want to delete proxy '{name}'?",
testProxy: 'Test Proxy',
dataImport: 'Import',
dataExportSelected: 'Export Selected',
dataImportTitle: 'Import Proxies',
dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.',
dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.',
dataImportFile: 'Data File',
dataImportButton: 'Start Import',
dataImporting: 'Importing...',
dataImportSelectFile: 'Please select a data file',
dataImportParseFailed: 'Failed to parse data',
dataImportFailed: 'Failed to import data',
dataImportResult: 'Import Result',
dataImportResultSummary: 'Created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}',
dataImportErrors: 'Failure Details',
dataImportSuccess: 'Import completed: created {proxy_created}, reused {proxy_reused}',
dataImportCompletedWithErrors: 'Import completed with errors: failed {proxy_failed}',
dataExport: 'Export',
dataExportConfirmMessage: 'The exported data contains sensitive proxy information. Store it securely.',
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',
protocols: {
http: 'HTTP',
https: 'HTTPS',
socks5: 'SOCKS5',
socks5h: 'SOCKS5H (Remote DNS)'
},
columns: {
name: 'Name',
protocol: 'Protocol',
address: 'Address',
auth: 'Auth',
location: 'Location',
status: 'Status',
accounts: 'Accounts',
latency: 'Latency',
expiry: 'Validity',
createdAt: 'Created',
actions: 'Actions',
nameLabel: 'Name',
namePlaceholder: 'Enter proxy name',
protocolLabel: 'Protocol',
selectProtocol: 'Select protocol',
hostLabel: 'Host',
hostPlaceholder: 'Enter host address',
portLabel: 'Port',
portPlaceholder: 'Enter port',
usernameLabel: 'Username (Optional)',
usernamePlaceholder: 'Enter username',
passwordLabel: 'Password (Optional)',
passwordPlaceholder: 'Enter password',
priorityLabel: 'Priority',
statusLabel: 'Status'
},
filters: {
protocol: 'Protocol',
allProtocols: 'All Protocols',
status: 'Status',
allStatuses: 'All Status'
},
testConnection: 'Test Connection',
qualityCheck: 'Quality Check',
batchQualityCheck: 'Batch Quality Check',
batchTest: 'Test All Proxies',
testFailed: 'Failed',
latencyFailed: 'Connection failed',
batchTestEmpty: 'No proxies available for testing',
batchTestDone: 'Batch test completed for {count} proxies',
batchTestFailed: 'Batch test failed',
batchDeleteAction: 'Delete',
batchDelete: 'Batch delete',
batchDeleteConfirm: 'Delete {count} selected proxies? In-use ones will be skipped.',
batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}',
batchDeleteSkipped: 'Skipped {skipped} proxies',
batchDeleteFailed: 'Batch delete failed',
deleteBlockedInUse: 'This proxy is in use and cannot be deleted',
accountsTitle: 'Accounts using this IP',
accountsEmpty: 'No accounts are using this proxy',
accountsFailed: 'Failed to load accounts list',
accountName: 'Account',
accountPlatform: 'Platform',
accountNotes: 'Notes',
name: 'Name',
protocol: 'Protocol',
host: 'Host',
port: 'Port',
username: 'Username (Optional)',
password: 'Password (Optional)',
status: 'Status',
enterProxyName: 'Enter proxy name',
leaveEmptyToKeep: 'Leave empty to keep current',
optionalAuth: 'Optional authentication',
form: {
hostPlaceholder: 'proxy.example.com',
portPlaceholder: '8080'
},
noProxiesYet: 'No proxies yet',
createFirstProxy: 'Create your first proxy to route traffic through it.',
// Batch import
standardAdd: 'Standard Add',
batchAdd: 'Quick Add',
batchInput: 'Proxy List',
batchInputPlaceholder:
"Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
batchInputHint:
"Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port",
parsedCount: '{count} valid',
invalidCount: '{count} invalid',
duplicateCount: '{count} duplicate',
importing: 'Importing...',
importProxies: 'Import {count} proxies',
batchImportSuccess: 'Successfully imported {created} proxies, skipped {skipped} duplicates',
batchImportAllSkipped: 'All {skipped} proxies already exist, skipped import',
failedToImport: 'Failed to batch import',
// Other messages
saving: 'Saving...',
testing: 'Testing...',
creating: 'Creating...',
updating: 'Updating...',
noProxies: 'No proxies yet',
noProxiesDescription: 'Add a proxy server to improve API access stability.',
proxyCreated: 'Proxy created successfully',
proxyUpdated: 'Proxy updated successfully',
proxyDeleted: 'Proxy deleted successfully',
proxyWorking: 'Proxy is working!',
proxyWorkingWithLatency: 'Proxy is working! Latency: {latency}ms',
proxyTestFailed: 'Proxy test failed',
qualityCheckDone: 'Quality check completed: score {score} ({grade})',
qualityCheckFailed: 'Failed to run proxy quality check',
batchQualityDone:
'Batch quality check completed for {count} proxies: healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}',
batchQualityFailed: 'Batch quality check failed',
batchQualityEmpty: 'No proxies available for quality check',
qualityReportTitle: 'Proxy Quality Report',
qualityGrade: 'Grade {grade}',
qualityExitIP: 'Exit IP',
qualityCountry: 'Exit Region',
qualityBaseLatency: 'Base Latency',
qualityCheckedAt: 'Checked At',
qualityTableTarget: 'Target',
qualityTableStatus: 'Status',
qualityTableLatency: 'Latency',
qualityTableMessage: 'Message',
qualityInline: 'Quality {grade}/{score}',
qualityStatusHealthy: 'Healthy',
qualityStatusPass: 'Pass',
qualityStatusWarn: 'Warn',
qualityStatusFail: 'Fail',
qualityStatusChallenge: 'Challenge',
qualityTargetBase: 'Base Connectivity',
proxyCreatedSuccess: 'Proxy created successfully',
proxyUpdatedSuccess: 'Proxy updated successfully',
proxyDeletedSuccess: 'Proxy deleted successfully',
testSuccess: 'Proxy test passed',
failedToLoad: 'Failed to load proxies',
failedToSave: 'Failed to save proxy',
failedToCreate: 'Failed to create proxy',
failedToUpdate: 'Failed to update proxy',
failedToDelete: 'Failed to delete proxy',
failedToTest: 'Failed to test proxy',
nameRequired: 'Please enter proxy name',
hostRequired: 'Please enter host address',
portInvalid: 'Port must be between 1-65535',
deleteConfirm:
"Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.",
neverExpires: 'Never',
expired: 'Expired',
overdueDays: 'Overdue {days}d',
expiringInDays: 'Expires in {days}d',
remainingDays: '{days}d left',
expiresAt: 'Validity',
nDays: '{days}d',
expiryDaysPlaceholder: 'Custom days, empty = never',
expiryWarnDays: 'Expiry warning (days)',
fallbackMode: 'Expiry fallback',
fallbackNone: 'No fallback',
fallbackProxy: 'Backup proxy',
fallbackDirect: 'Direct connection',
backupProxy: 'Backup proxy',
},
// Redeem Codes
redeem: {
title: 'Redeem Code Management',
description: 'Generate and manage redeem codes',
generateCodes: 'Generate Codes',
searchCodes: 'Search codes or email...',
allTypes: 'All Types',
allStatus: 'All Status',
balance: 'Balance',
concurrency: 'Concurrency',
subscription: 'Subscription',
invitation: 'Invitation',
invitationHint: 'Invitation codes are used to restrict user registration. They are automatically marked as used after use.',
unused: 'Unused',
used: 'Used',
columns: {
code: 'Code',
type: 'Type',
value: 'Value',
status: 'Status',
usedBy: 'Used By',
usedAt: 'Used At',
createdAt: 'Created At',
expiresAt: 'Expires At',
actions: 'Actions'
},
userPrefix: 'User #{id}',
exportCsv: 'Export CSV',
batchUpdate: 'Batch Update',
batchUpdateTitle: 'Batch Update Redeem Codes',
selectedCount: '{count} redeem code(s) selected',
clearSelection: 'Clear selection',
selectCodesFirst: 'Select redeem codes first',
noBatchFieldsSelected: 'Select at least one field to update',
batchUpdateSuccess: 'Updated {count} redeem code(s)',
failedToBatchUpdate: 'Failed to batch update redeem codes',
batchFields: {
status: 'Status',
expiresAt: 'Expires At',
notes: 'Notes',
group: 'Group'
},
batchNotesPlaceholder: 'Enter the new note, or leave blank to clear it',
clearGroup: 'Clear group',
deleteAllUnused: 'Delete All Unused Codes',
deleteCode: 'Delete Redeem Code',
deleteCodeConfirm:
'Are you sure you want to delete this redeem code? This action cannot be undone.',
deleteAllUnusedConfirm:
'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.',
deleteAll: 'Delete All',
generateCodesTitle: 'Generate Redeem Codes',
generatedSuccessfully: 'Generated Successfully',
codesCreated: '{count} redeem code(s) created',
codeType: 'Code Type',
amount: 'Amount ($)',
value: 'Value',
count: 'Count',
generating: 'Generating...',
generate: 'Generate',
copyAll: 'Copy All',
copied: 'Copied!',
download: 'Download',
codesExported: 'Codes exported successfully',
codeDeleted: 'Redeem code deleted successfully',
codesDeleted: 'Successfully deleted {count} unused code(s)',
noUnusedCodes: 'No unused codes to delete',
failedToLoad: 'Failed to load redeem codes',
failedToGenerate: 'Failed to generate codes',
failedToExport: 'Failed to export codes',
failedToDelete: 'Failed to delete code',
failedToDeleteUnused: 'Failed to delete unused codes',
failedToCopy: 'Failed to copy codes',
types: {
balance: 'Balance',
concurrency: 'Concurrency',
subscription: 'Subscription',
invitation: 'Invitation',
// Admin adjustment types (created when admin modifies user balance/concurrency)
admin_balance: 'Balance (Admin)',
admin_concurrency: 'Concurrency (Admin)'
},
selectGroup: 'Select Group',
selectGroupPlaceholder: 'Choose a subscription group',
validityDays: 'Validity Days',
codeExpiry: 'Code Expiry',
neverExpires: 'Never expires',
expiryPresetDays: '{days} days',
customExpiry: 'Custom',
customExpiryDays: 'Custom days',
expiryDaysRequired: 'Please enter a valid expiry day count',
groupRequired: 'Please select a subscription group',
days: ' days',
status: {
unused: 'Unused',
used: 'Used',
expired: 'Expired',
disabled: 'Disabled'
},
form: {
typeLabel: 'Type',
selectType: 'Select type',
valueLabel: 'Value',
valuePlaceholder: 'Enter value',
balanceHint: 'Balance amount (USD)',
concurrencyHint: 'Concurrency increment',
countLabel: 'Count',
countPlaceholder: 'Enter count',
countHint: 'Number of redeem codes to generate',
prefixLabel: 'Prefix (Optional)',
prefixPlaceholder: 'e.g., GIFT',
expiresLabel: 'Expires At (Optional)'
},
filters: {
type: 'Type',
allTypes: 'All Types',
status: 'Status',
allStatuses: 'All Status',
search: 'Search codes'
},
copyCode: 'Copy',
disableCode: 'Disable',
enableCode: 'Enable',
deleteConfirmMessage: 'Are you sure you want to delete this redeem code?',
noCodes: 'No redeem codes yet',
noCodesDescription: 'Generate redeem codes to distribute balance or concurrency to users.',
codesGeneratedSuccess: 'Redeem codes generated successfully, {count} total',
codeDisabledSuccess: 'Redeem code disabled',
codeEnabledSuccess: 'Redeem code enabled',
codeDeletedSuccess: 'Redeem code deleted successfully',
failedToUpdate: 'Failed to update redeem code'
},
// Announcements
announcements: {
title: 'Announcements',
description: 'Create announcements and target by conditions',
createFirstAnnouncement: 'No announcements yet. Create your first one.',
createAnnouncement: 'Create Announcement',
editAnnouncement: 'Edit Announcement',
deleteAnnouncement: 'Delete Announcement',
searchAnnouncements: 'Search announcements...',
status: 'Status',
allStatus: 'All Status',
columns: {
title: 'Title',
status: 'Status',
notifyMode: 'Notify Mode',
targeting: 'Targeting',
timeRange: 'Schedule',
createdAt: 'Created At',
actions: 'Actions'
},
statusLabels: {
draft: 'Draft',
active: 'Active',
archived: 'Archived'
},
notifyModeLabels: {
silent: 'Silent',
popup: 'Popup'
},
form: {
title: 'Title',
content: 'Content (Markdown supported)',
status: 'Status',
notifyMode: 'Notify Mode',
notifyModeHint: 'Popup mode will show a popup notification to users',
startsAt: 'Starts At',
endsAt: 'Ends At',
startsAtHint: 'Leave empty to start immediately',
endsAtHint: 'Leave empty to never expire',
targetingMode: 'Targeting',
targetingAll: 'All users',
targetingCustom: 'Custom rules',
addOrGroup: 'Add OR group',
addAndCondition: 'Add AND condition',
conditionType: 'Condition type',
conditionSubscription: 'Subscription',
conditionBalance: 'Balance',
operator: 'Operator',
balanceValue: 'Balance threshold',
selectPackages: 'Select packages'
},
operators: {
gt: '>',
gte: '≥',
lt: '<',
lte: '≤',
eq: '='
},
targetingSummaryAll: 'All users',
targetingSummaryCustom: 'Custom ({groups} groups)',
timeImmediate: 'Immediate',
timeNever: 'Never',
readStatus: 'Read Status',
preview: 'Preview',
eligible: 'Eligible',
readAt: 'Read at',
unread: 'Unread',
searchUsers: 'Search users...',
failedToLoad: 'Failed to load announcements',
failedToCreate: 'Failed to create announcement',
failedToUpdate: 'Failed to update announcement',
failedToDelete: 'Failed to delete announcement',
failedToLoadReadStatus: 'Failed to load read status',
deleteConfirm: 'Are you sure you want to delete this announcement? This action cannot be undone.'
},
// Promo Codes
promo: {
title: 'Promo Code Management',
description: 'Create and manage registration promo codes',
createCode: 'Create Promo Code',
editCode: 'Edit Promo Code',
deleteCode: 'Delete Promo Code',
searchCodes: 'Search codes...',
allStatus: 'All Status',
columns: {
code: 'Code',
bonusAmount: 'Bonus Amount',
maxUses: 'Max Uses',
usedCount: 'Used',
usage: 'Usage',
status: 'Status',
expiresAt: 'Expires At',
createdAt: 'Created At',
actions: 'Actions'
},
// Form labels (flat structure for template usage)
code: 'Promo Code',
autoGenerate: 'auto-generate if empty',
codePlaceholder: 'Enter promo code or leave empty',
bonusAmount: 'Bonus Amount ($)',
maxUses: 'Max Uses',
zeroUnlimited: '0 = unlimited',
expiresAt: 'Expires At',
notes: 'Notes',
notesPlaceholder: 'Optional notes for this code',
status: 'Status',
neverExpires: 'Never expires',
// Status labels
statusActive: 'Active',
statusDisabled: 'Disabled',
statusExpired: 'Expired',
statusMaxUsed: 'Used Up',
// Usage records
usageRecords: 'Usage Records',
viewUsages: 'View Usages',
noUsages: 'No usage records yet',
userPrefix: 'User #{id}',
copied: 'Copied!',
// Messages
noCodesYet: 'No promo codes yet',
createFirstCode: 'Create your first promo code to offer registration bonuses.',
codeCreated: 'Promo code created successfully',
codeUpdated: 'Promo code updated successfully',
codeDeleted: 'Promo code deleted successfully',
deleteCodeConfirm: 'Are you sure you want to delete this promo code? This action cannot be undone.',
copyRegisterLink: 'Copy register link',
registerLinkCopied: 'Register link copied to clipboard',
failedToLoad: 'Failed to load promo codes',
failedToCreate: 'Failed to create promo code',
failedToUpdate: 'Failed to update promo code',
failedToDelete: 'Failed to delete promo code',
failedToLoadUsages: 'Failed to load usage records'
},
// Usage Records
usage: {
title: 'Usage Records',
description: 'View and manage all user usage records',
userFilter: 'User',
searchUserPlaceholder: 'Search user by email...',
searchApiKeyPlaceholder: 'Search API key by name...',
searchAccountPlaceholder: 'Search account by name...',
selectedUser: 'Selected',
user: 'User',
account: 'Account',
group: 'Group',
requestId: 'Request ID',
requestIdCopied: 'Request ID copied',
allModels: 'All Models',
allAccounts: 'All Accounts',
allGroups: 'All Groups',
allTypes: 'All Types',
inputCost: 'Input Cost',
outputCost: 'Output Cost',
cacheCreationCost: 'Cache Creation Cost',
cacheReadCost: 'Cache Read Cost',
inputTokens: 'Input Tokens',
outputTokens: 'Output Tokens',
cacheCreationTokens: 'Cache Creation Tokens',
cacheCreation5mTokens: 'Cache Write',
cacheCreation1hTokens: 'Cache Write',
cacheReadTokens: 'Cache Read Tokens',
failedToLoad: 'Failed to load usage records',
billingType: 'Billing Type',
allBillingTypes: 'All Billing Types',
billingTypeBalance: 'Balance',
billingTypeSubscription: 'Subscription',
billingMode: 'Billing Mode',
billingModeToken: 'Token',
billingModePerRequest: 'Per Request',
billingModeImage: 'Image',
billingModeVideo: 'Video',
allBillingModes: 'All Billing Modes',
upstreamModelAudit: 'Upstream model audit',
allUpstreamModelAudit: 'All response model states',
upstreamModelMismatchOnly: 'Mismatched only',
upstreamModelMatchedOnly: 'Matched only',
ipAddress: 'IP',
clickToViewBalance: 'Click to view balance history',
failedToLoadUser: 'Failed to load user info',
userDeletedBadge: 'Deleted',
tokenRanking: {
subtitle: 'Per-user token usage for the current filters and time range',
rowHint: "Click to view this user's usage details",
userCount: '{count} users',
columns: {
user: 'User',
requests: 'Requests',
inputTokens: 'Input Tokens',
outputTokens: 'Output Tokens',
cacheTokens: 'Cache Tokens',
totalTokens: 'Total Tokens',
cost: 'Cost'
}
},
cleanup: {
button: 'Cleanup',
title: 'Cleanup Usage Records',
warning: 'Cleanup is irreversible and will affect historical stats.',
submit: 'Submit Cleanup',
submitting: 'Submitting...',
confirmTitle: 'Confirm Cleanup',
confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.',
confirmSubmit: 'Confirm Cleanup',
cancel: 'Cancel',
cancelConfirmTitle: 'Confirm Cancel',
cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?',
cancelConfirm: 'Confirm Cancel',
cancelSuccess: 'Cleanup task canceled',
cancelFailed: 'Failed to cancel cleanup task',
recentTasks: 'Recent Cleanup Tasks',
loadingTasks: 'Loading tasks...',
noTasks: 'No cleanup tasks yet',
range: 'Range',
deletedRows: 'Deleted',
missingRange: 'Please select a date range',
submitSuccess: 'Cleanup task created',
submitFailed: 'Failed to create cleanup task',
loadFailed: 'Failed to load cleanup tasks',
status: {
pending: 'Pending',
running: 'Running',
succeeded: 'Succeeded',
failed: 'Failed',
canceled: 'Canceled'
}
}
},
// Ops Monitoring
}
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
export default {
batchImage: {
columns: {
taskName: 'Task name',
model: 'Model',
apiKey: 'API key',
result: 'Results',
cost: 'Cost',
downloadStatus: 'Download status',
},
status: {
queued: 'Queued',
running: 'Generating',
processingResults: 'Processing results',
settling: 'Settling',
completed: 'Completed',
failed: 'Failed',
cancelled: 'Cancelled',
outputDeleted: 'Results deleted',
partialSuccess: 'Partially succeeded',
allFailed: 'All failed',
},
itemStatus: {
pending: 'Queued',
succeeded: 'Succeeded',
failed: 'Failed',
cancelled: 'Cancelled',
recovered: 'Recovered by retry',
},
filters: {
searchTaskName: 'Search task name',
allApiKeys: 'All API keys',
allStatuses: 'All statuses',
allDownloadStates: 'All download states',
downloaded: 'Downloaded',
notDownloaded: 'Not downloaded',
},
actions: {
usageGuide: 'Usage guide',
createJob: 'Create batch job',
downloadSelected: 'Download selected',
deleteRecords: 'Delete records',
retryFailedItems: 'Retry failed items',
cancelJob: 'Cancel job',
downloadZip: 'Download ZIP',
viewDetail: 'View details',
download: 'Download',
moreActions: 'More actions',
copyInstruction: 'Copy instructions',
submitJob: 'Submit job',
},
list: {
selectedJobs: 'Selected {count} job | Selected {count} jobs',
expandChildren: 'Expand {n} subtask | Expand {n} subtasks',
collapseChildren: 'Collapse subtasks',
childCount: '{n} subtask | {n} subtasks',
childBadge: 'Subtask',
keyNotRecorded: 'Not recorded',
totalCount: 'of {n}',
notDownloaded: 'Not downloaded',
empty: 'No batch jobs yet',
emptyHint: 'Use the button in the top-right corner to create a batch job.',
},
pagination: {
pageNumber: 'Page {page}',
pageItems: '{count} on this page',
},
promptPopover: {
title: 'Full prompt',
copied: 'Prompt copied',
},
detail: {
title: 'Job details',
aggregatedResult: 'Combined results',
result: 'Results',
cost: 'Cost',
downloadStatus: 'Download status',
items: 'Items',
preview: 'Preview',
previewZoom: 'Zoom compressed preview {id}',
previewReload: 'Reload compressed preview',
previewLoad: 'Load compressed preview',
previewUnavailable: 'Preview unavailable',
noImage: 'No image',
loadingItems: 'Loading items...',
noItems: 'No items yet',
noItemsHint: 'Queued or generating jobs show submitted prompts first; image statuses update once results are processed.',
mainTask: 'Main job: {name}',
childTask: 'Subtask: {name}',
holdCost: 'Hold {amount}',
},
itemResult: {
recoveredByRetry: 'Previous failure recovered by a retry subtask',
readyPreview: 'Image generated. Click to preview.',
readyDownload: 'Image generated and ready to download.',
noUsableImage: 'No usable image was generated.',
cancelled: 'Job cancelled.',
waiting: 'Waiting for results.',
emptyImageOutput: 'The upstream returned a result, but this item has no image content. This usually means a single Gemini/Vertex generation failed or was blocked by safety policies.',
providerItemFailed: 'The upstream result for this item has no usable image.',
},
imagePreview: {
title: 'Image preview',
notice: 'This is a compressed thumbnail cached locally in your browser, so quality is reduced. Download the ZIP to view the original image.',
},
create: {
title: 'Create batch job',
taskName: 'Task name',
taskNamePlaceholder: 'Defaults to the current time if left empty',
loadingKeys: 'Loading API keys...',
selectKeyPlaceholder: 'Select a Gemini API key',
noKeysHint: 'No Gemini API key is available for batch image generation. Create one and bind it to a Gemini group with batch image generation enabled first.',
model: 'Model',
imageSize: 'Image size',
imageSizeHint: 'Batch jobs are currently submitted at a fixed 1K image size.',
outputFormat: 'Output format',
estimatedOutput: 'Estimated output',
estimatedOutputValue: '{images} images / {prompts} prompts',
promptAdded: '{count} added',
promptPlaceholder: 'Paste a prompt, then add it to the list below',
customIdPlaceholder: 'Custom ID (optional)',
outputCountPerPrompt: 'Images per prompt',
outputCountOption: '{n} image | {n} images',
referenceImage: 'Reference images',
removeReferenceImage: 'Remove reference image',
limitsHint: 'Up to {maxPerItem} images per prompt and {maxPerJob} per job. The current model allows up to {refLimit} reference images per prompt; reference images consume input tokens once per generated image.',
referenceCount: '{n} reference image | {n} reference images',
noPrompts: 'No prompts added yet.',
cancelNotice: 'Cancelling requests an upstream cancellation. Images already indexed as successful will still be billed, and the remaining hold will be released.',
submittingNotice: 'Creating the upstream batch job. This usually takes a few seconds; please do not submit again.',
modelNoReferenceImages: 'The current model does not support reference images.',
refLimitReached: 'The current model allows up to {limit} reference images per prompt.',
refLimitExceededIgnored: 'The current model allows up to {limit} reference images per prompt. Extra files were ignored.',
refFormatUnsupported: 'Reference images must be PNG, JPEG, or WebP.',
refFileTooLarge: '{name} exceeds 10MB and was ignored.',
},
guide: {
title: 'Batch Image Generation Guide',
uiTitle: 'How to use this page',
step1: '1. Select a Gemini API key with batch image generation enabled. The model list shows the models available to that keys group.',
step2: '2. The task name can be left empty; the current time is used automatically on submit. Prompts are added to the list one by one, and each prompt can carry reference images and a repeat count.',
step3: '3. After submitting, the job is queued first and the item list shows the submitted prompts. Image previews are not loaded by default; click the preview button on an item to load a single image.',
step4: '4. Once completed you can download the ZIP. If some items failed, the More menu lets you retry only the failed items. Billing is still based on the number of successfully generated images; reference images are not billed separately.',
skillTitle: 'Skill instructions for Codex',
skillDesc: 'Tells Codex how to organize prompts, submit jobs, and download results on behalf of the user.',
},
messages: {
loadKeysFailed: 'Failed to load API keys.',
loadModelsFailed: 'Failed to load available models.',
loadJobsFailed: 'Failed to load batch jobs.',
selectApiKey: 'Select an available Gemini API key.',
noModelsForKey: 'This key has no available batch image models.',
selectModel: 'Select a model.',
promptRequired: 'Enter at least one prompt.',
submitted: 'Batch job submitted.',
submitFailed: 'Failed to submit the batch job.',
refreshFailed: 'Failed to refresh the job.',
cancelConfirm: 'Cancellation will be sent upstream. Images already indexed as successful will still be billed, and the remaining hold will be released. Continue?',
cancelled: 'Cancellation requested.',
cancelFailed: 'Failed to cancel the job.',
batchDownloadStarted: 'Downloads for the selected jobs have started.',
downloadFailed: 'Failed to download the result.',
retrySubmitted: 'Retry job submitted for failed items.',
retryFailed: 'Failed to retry failed items.',
retryMissingPrompts: 'This job does not have saved prompts for failed items, so it cannot be retried automatically. Recreate it with the original prompt.',
retryTaskNameSuffix: 'Retry failed items',
deleteConfirm: 'This hides the job from your list while keeping billing records. Delete it?',
deleteSelectedConfirm: 'This hides the selected jobs from your list while keeping billing records. Delete them?',
deleted: 'Job record deleted.',
deleteFailed: 'Failed to delete the job record.',
loadItemsFailed: 'Failed to load item details.',
loadPreviewFailed: 'Failed to load the image preview.',
copiedInstruction: 'Batch image instructions copied.',
loadingModels: 'Loading available models...',
noModels: 'No available models',
noModelsHint: 'This keys group has no models configured for batch image generation.',
noCompatibleAccount: 'No usable upstream batch image account is available for this keys group. Contact an administrator to check the groups schedulable Gemini API key or Vertex service account and model support.',
unsupportedProvider: 'The batch image provider for this job is not available. Contact an administrator to check the batch image provider configuration.',
providerSubmitFailed: 'The upstream batch image job failed to submit. Contact an administrator to check the upstream account, model permission, or provider status.',
vertexGcsBucketMissing: 'Vertex batch image generation is missing the managed GCS bucket configuration. Contact an administrator to configure BATCH_IMAGE_VERTEX_MANAGED_GCS_BUCKET before submitting again.',
queueFailed: 'The task queue is temporarily unavailable, so the batch job was not queued. Contact an administrator to check the queue service.',
billingHoldFailed: 'The cost hold failed, so the batch job was not submitted. Contact an administrator to check billing or balance hold service.',
groupDisabled: 'Batch image generation is not enabled for this keys group. Choose another enabled key or contact an administrator.',
pricingMissing: 'The selected model does not have batch image pricing configured. Contact an administrator to add pricing first.',
insufficientBalance: 'Insufficient balance to hold the estimated batch image cost.',
invalidModel: 'Select a batch image model available for the current key.',
invalidItems: 'The prompt list is invalid. Check that it is not empty, within the item limit, and still using 1K image size.',
duplicateCustomId: 'Custom IDs in the prompt list must be unique.',
promptTooLong: 'One prompt is too long. Shorten it and try again.',
invalidReferenceImage: 'A reference image is invalid. Use PNG, JPEG, or WebP under 10 MB.',
tooManyReferenceImages: 'Too many reference images. Flash Image allows up to 3 per item, Pro Image allows up to 14, and each job allows up to 1000 total.',
referenceImagesTooLarge: 'Reference images are too large. Inline reference images are limited to 128 MB per job; use gs:// file_uri or split the job for large batches.',
tooManyOutputImages: 'Too many expected output images. Each prompt can request up to 4 images, and each job can generate up to 200 images.',
idempotencyConflict: 'This submission conflicts with a previous request ID. Refresh the page and submit again.',
notReady: 'The job is not complete yet. Download will be available after completion.',
outputDeleted: 'The result files for this job have already been cleaned up.',
resultMissing: 'The result file is unavailable. It may have been cleaned up, storage permissions may be broken, or storage settings may have changed. Contact an administrator to check the result file.',
itemFailed: 'This item has no successful image to preview.',
itemImageIndexOutOfRange: 'This item has no previewable image.',
downloadLimited: 'Too many download requests are active. Please try again later.',
downloadTooLarge: 'This ZIP is too large for a single download. Download fewer items at once or ask an administrator to raise the batch download limit.',
deleteNotReady: 'Job records can only be deleted after the job finishes.',
disabled: 'Batch image generation is currently disabled.',
authRequired: 'The current API key is unavailable or expired. Select the key again.',
adminReference: 'Send the error code and request ID to an administrator for troubleshooting.',
errorReference: 'Error detail',
errorCodeRef: 'code: {code}',
requestIdRef: 'request ID: {id}',
httpStatusRef: 'HTTP status: {status}',
},
},
}
@@ -0,0 +1,147 @@
/** Channel Monitor V2 (user + admin passive monitor UI) */
export default {
channelMonitorV2: {
title: 'Channel Monitor',
updating: 'Updating data',
updatedTo: 'Updated to {time}',
partialCoverage: 'Partial historical coverage',
bootstrap: {
title: 'Building historical monitor data',
description:
'On first enable, passive aggregation silently fills the 90m, 24h, 7d, and 30d windows in the background. All ranges become complete once this finishes.',
progress: '{percent}% complete',
working: 'Aggregating in the background…',
},
timeRange: 'Time range',
clearFilters: 'Reset',
refreshingFilters: 'Filters changed; refreshing matrix, trend, and details…',
switchingData: 'Switching filtered data…',
summaryAria: 'Selected range summary',
loadFailed: 'Failed to load channel monitor',
detailLoadFailed: 'Failed to load channel monitor details',
otherModels: 'Other models',
ignored: 'Ignored',
currentUser: 'Current user',
ranges: { '90m': '90m', '24h': '24h', '7d': '7d', '30d': '30d' },
filters: {
platform: 'Platform', allPlatforms: 'All', group: 'Group', allGroups: 'All', model: 'Model', allModels: 'All',
empty: 'No options', selectedCount: '{count}', labelValue: '{label}: {value}'
},
groupBy: {
label: 'Group by', platform: 'Platform', platformGroup: 'Platform / Group', platformModel: 'Platform / Model', platformGroupModel: 'Platform / Group / Model'
},
trendView: { label: 'Trend view', pulse: 'Pulse matrix', line: 'Line chart' },
healthMode: { label: 'Health display', overall: 'Overall', success: 'Error rate', ttft: 'First token', cache: 'Cache rate' },
tabs: { aria: 'Detail dimension', models: 'Models', errors: 'Error reasons', users: 'User ranking' },
metrics: {
rpm: 'RPM',
tpm: 'TPM',
tps: 'Tokens/s',
rpmDetail: 'Requests per minute',
tpmDetail: 'Tokens per minute',
tpsDetail: 'Derived as TPM ÷ 60',
errorRate: 'Error rate',
ttft: 'First token',
ttftP50: 'First token P50',
durationP50: 'Duration P50',
cacheRate: 'Cache rate',
cacheDetail: 'Read cache share',
successRate: 'Success rate',
successRateValue: 'Success rate {value}',
errorRateValue: 'Error rate {value}',
rpmValue: 'RPM {value}',
tpmValue: 'TPM {value}',
tpsValue: 'Tokens/s {value}',
ttftValue: 'First token {value}',
durationValue: 'Duration {value}',
cacheRateValue: 'Cache rate {value}',
},
table: { platformModel: 'Platform / Model', rank: 'Rank', user: 'User' },
empty: { title: 'No data to display', description: 'Try changing the time range or filters' },
bucket: { minutes: '{count}-minute buckets', hours: '{count}-hour buckets', days: '{count}-day buckets' },
matrix: {
title: 'Availability trend', description: 'Each row is a channel dimension and each block is an aggregate interval; hover for details', wheelZoom: 'Scroll over blocks to zoom in (narrower range, wider blocks)', wheelZoomX: 'Scroll over blocks to zoom in (narrower range, wider blocks)', dimension: 'Channel dimension', emptyTitle: 'No matrix data for the selected window', legendAria: 'Health score legend', bad: 'Bad', good: 'Good', healthyLegend: 'Healthy (≥80)', warningLegend: 'Watch (5079)', criticalLegend: 'Critical (<50)', unknownLegend: 'No traffic / insufficient samples', noTraffic: 'No traffic in this interval', noTrafficAt: '{time} · no traffic', scoreLine: 'Health score {score}', resetZoom: 'Reset zoom'
},
chart: {
title: 'Availability trend', description: 'Smoothed trend: error rate · first token P50 · cache rate', emptyTitle: 'No trend data for the selected window', errorLegend: 'Error rate (left axis %)', cacheLegend: 'Cache rate (left axis %)', ttftLegend: 'First token P50 (right axis)', errorDataset: 'Error rate trend %', cacheDataset: 'Cache rate trend %', ttftDataset: 'First token trend P50 (ms)', percentAxis: 'Rate %', resetZoom: 'Reset zoom'
},
errorDetail: { http: 'HTTP {code}', upstream: 'Upstream {code}', noMessage: 'No error message', empty: 'Category rates only (sample messages are admin-only)' },
errorCategories: {
content_policy: 'Content policy', authentication: 'Authentication', context_limit: 'Context limit', invalid_request: 'Invalid request', model_unsupported: 'Unsupported model', group_access: 'Group access', quota_or_balance: 'Quota or balance', account_pool_unavailable: 'Account pool unavailable', rate_or_capacity: 'Rate or capacity', timeout: 'Timeout', transport_or_stream: 'Transport or stream', upstream_forbidden: 'Upstream forbidden', not_found: 'Not found', client_cancelled: 'Client cancelled', upstream_5xx: 'Upstream 5xx', internal: 'Internal', other: 'Other'
},
rank: {
gold: 'Rank 1 gold',
silver: 'Rank 2 silver',
bronze: 'Rank 3 bronze',
place: 'Rank {n}',
unranked: 'Unranked',
},
settings: {
title: 'V2 data monitor config',
description:
'Configure passive usage aggregation dimensions (platform / model / group) and refresh cadence. Health colors and details on the user /monitor page show rates, RPM, and TPM — not absolute request volume.',
save: 'Save',
loading: 'Loading…',
loadFailed: 'Failed to load V2 config',
saveSuccess: 'V2 monitor config saved',
saveFailed: 'Failed to save V2 config',
modeBanner:
'System mode is currently {mode}. V2 minute aggregation will not run; this config can be prepared now and takes effect after switching to {modeV2}. Change mode under System Settings → Feature switches.',
modeClosed: 'Channel monitor disabled',
modeV1: 'V1 active probes',
modeV2: 'V2 passive monitoring',
enableTitle: 'Enable V2 aggregation',
enableHint:
'Applies when system mode is V2. Turning this off only stops this configs aggregation; the system mode switch remains under Feature switches.',
refreshTitle: 'Aggregation interval',
refreshHint: 'Affects matrix time granularity and refresh cadence',
refreshAria: 'Aggregation interval',
platformsTitle: 'Platforms and models',
platformsHint:
'Leave empty = show all real model names; when filled, only listed models get their own rows and the rest roll into “Other”',
modelsPlaceholder: 'Empty = all real models; or list popular models (rest → Other)',
badgeAllModels: 'All models',
badgeOther: '+ Other',
groupsTitle: 'Monitored groups',
groupsSelected: '{count} groups selected',
groupsAll: 'All groups',
groupsEmpty: 'No groups available',
errorsTitle: 'Error categories and ignores',
errorsHint:
'Checked “ignore” categories are excluded from error rate and health score, but still appear greyed in the error breakdown. Unmatched errors roll into “Other”.',
ignoredSummary: 'Ignored {ignored} categories · counted in error rate {counted} categories',
healthTitle: 'Health thresholds',
healthHint:
'Controls user-facing color bands and overall score. Defaults are tolerant so small error rates or low cache do not immediately show as unhealthy.',
fields: {
minimumSample: 'Minimum samples',
warningError: 'Error rate watch %',
criticalError: 'Error rate critical %',
targetTtft: 'TTFT target ms',
warningTtft: 'TTFT watch ms',
criticalTtft: 'TTFT critical ms',
warningCache: 'Cache rate watch %',
criticalCache: 'Cache rate critical %',
},
namedModelsEmpty: 'Platform model lists are empty: every real model name will be shown (not folded into “Other”).',
namedModelsCount: 'Showing {count} named model dimensions; unlisted models fold into per-platform “Other”.',
userContractTitle: 'User-facing display contract',
userContract: {
health: 'Health color weights: error rate 60% + first-token P50 20% + cache rate 20% (thresholds configurable above)',
trend: 'Trend can switch between pulse matrix and line chart (error · cache · first token)',
latency: 'Latency shows AVG · P50 · P90; absolute request / error counts are not shown',
models: 'Empty model lists show real names and never dump everything into “Other”',
},
},
admin: {
descriptionV1:
'System mode is V1 active probes: manage probe monitors and run checks now; V2 aggregation does not run.',
descriptionV2:
'System mode is V2 passive monitoring: configure aggregation dimensions; V1 active probes do not run.',
tabAria: 'Monitor management',
tabV2: 'V2 data monitor config',
tabV1Active: 'V1 active probes',
tabV1History: 'V1 history (probes not active in current mode)',
},
},
}
+451
View File
@@ -0,0 +1,451 @@
export default {
common: {
loading: 'Loading...',
submitting: 'Submitting...',
justNow: 'just now',
peakRateTooltip: 'Peak rate: {window}',
peakRateImageNote: '; image tokens billed as tokens are also affected, per-image billing is unaffected',
save: 'Save',
saved: 'Saved successfully',
deleted: 'Deleted successfully',
cancel: 'Cancel',
delete: 'Delete',
edit: 'Edit',
create: 'Create',
update: 'Update',
confirm: 'Confirm',
reset: 'Reset',
search: 'Search',
filter: 'Filter',
export: 'Export',
import: 'Import',
actions: 'Actions',
status: 'Status',
name: 'Name',
email: 'Email',
password: 'Password',
submit: 'Submit',
back: 'Back',
next: 'Next',
yes: 'Yes',
no: 'No',
all: 'All',
none: 'None',
selectAll: 'Select all',
noData: 'No data',
expand: 'Expand',
collapse: 'Collapse',
success: 'Success',
error: 'Error',
critical: 'Critical',
warning: 'Warning',
info: 'Info',
active: 'Active',
inactive: 'Inactive',
more: 'More',
close: 'Close',
toggleMenu: 'Toggle menu',
userMenu: 'User menu',
pageNotFound: 'Page not found',
enabled: 'Enabled',
disabled: 'Disabled',
total: 'Total',
balance: 'Balance',
availableBalance: 'Available balance',
frozenBalance: 'Frozen balance',
totalBalance: 'Total balance',
available: 'Available',
copiedToClipboard: 'Copied to clipboard',
copied: 'Copied',
copyFailed: 'Failed to copy',
verifying: 'Verifying...',
processing: 'Processing...',
contactSupport: 'Contact Support',
add: 'Add',
invalidEmail: 'Please enter a valid email address',
optional: 'optional',
selectOption: 'Select an option',
searchPlaceholder: 'Search...',
noOptionsFound: 'No options found',
noGroupsAvailable: 'No groups available',
unknownError: 'Unknown error occurred',
saving: 'Saving...',
selectedCount: '({count} selected)',
refresh: 'Refresh',
autoRefresh: {
title: 'Auto Refresh',
enable: 'Enable auto refresh',
countdown: 'Auto refresh: {seconds}s',
seconds: '{n} seconds',
},
view: 'View',
settings: 'Settings',
chooseFile: 'Choose File',
upload: 'Upload',
remove: 'Remove',
noFileSelected: 'No file selected',
selectedFile: 'Selected: {name}',
fileReadFailed: 'Failed to read file',
selectImageFile: 'Please select an image file',
fileTooLargeKb: 'File too large ({size} KB), max {max} KB',
copy: 'Copy',
notAvailable: 'N/A',
now: 'Now',
today: 'Today',
tomorrow: 'Tomorrow',
unknown: 'Unknown',
minutes: 'min',
time: {
never: 'Never',
justNow: 'Just now',
minutesAgo: '{n}m ago',
hoursAgo: '{n}h ago',
daysAgo: '{n}d ago',
countdown: {
daysHours: '{d}d {h}h',
hoursMinutes: '{h}h {m}m',
minutes: '{m}m',
withSuffix: '{time} to lift'
}
}
},
adminCompliance: {
title: 'Deployment and Operation Compliance Acknowledgment',
blockingNotice: 'Deployment and operation compliance acknowledgment is required before continuing to use the console.',
riskNotice: 'This acknowledgment provides clear, conspicuous, and reproducible notice of compliance obligations and operation risks for self-hosted instances.',
version: 'Document Version',
openDocument: 'Open the GitHub document',
documentSource: 'The agreement text comes from Markdown files in this project repository. When the agreement content changes, the document version must be incremented; acknowledgments of older versions become invalid and console users must acknowledge again.',
inputLabel: 'Type the following confirmation phrase exactly',
inputPlaceholder: 'Type the confirmation phrase to continue',
inputMismatch: 'The confirmation phrase does not match. Type the displayed text exactly.',
legalNote: 'This acknowledgment defines the no-affiliation relationship and responsibility boundary between self-hosted instances and the open-source project, copyright holders, contributors, and maintainers. The party that deploys, operates, or controls the relevant instance remains independently responsible for its applicable obligations.',
logout: 'Log out',
accept: 'Acknowledge and Continue',
accepted: 'Compliance acknowledgment recorded',
acceptFailed: 'Failed to submit acknowledgment'
},
legal: {
loadFailed: 'Failed to load document',
retryLater: 'Refresh the page and try again later.',
notFound: 'Document not found',
notFoundDescription: 'This legal document does not exist or has been removed by an administrator.',
updatedAt: 'Updated: {date}',
empty: 'No content',
loginAgreement: 'Login Agreement',
adminCompliance: 'Deployment and Operation Compliance Commitment',
loginAgreementPrompt: {
checkboxPrefix: 'I have read and agree to ',
documentSeparator: ', ',
noticeTitle: 'Accept the latest terms before continuing.',
noticeDescription: 'Account/password login and quick sign-in stay disabled until you accept.',
viewTerms: 'View terms',
dialogTitle: 'Terms Update Notice',
dialogDescription: 'Our service terms were updated on {date}. Please read and accept the following terms before continuing.',
recently: 'recently',
relatedDocuments: 'Related documents',
reject: 'Reject',
accept: 'Accept and continue',
loginRejectedWarning: 'Account/password login and quick sign-in are disabled until you accept the latest terms.',
loginRequiredWarning: 'Please read and accept the latest terms before logging in.',
registerRejectedWarning: 'Registration and quick sign-in are disabled until you accept the latest terms.',
registerRequiredWarning: 'Please read and accept the latest terms before registering.'
}
},
// Navigation
nav: {
dashboard: 'Dashboard',
announcements: 'Announcements',
apiKeys: 'API Keys',
batchImage: 'Batch Images',
usage: 'Usage',
redeem: 'Redeem',
affiliate: 'Affiliate Rebates',
affiliateManagement: 'Affiliate Rebates',
affiliateInviteRecords: 'Invite Records',
affiliateRebateRecords: 'Rebate Records',
affiliateTransferRecords: 'Transfer Records',
profile: 'Profile',
users: 'Users',
groups: 'Groups',
channels: 'Channels',
availableChannels: 'Available Channels',
modelPlaza: 'Model Plaza',
subscriptions: 'Subscriptions',
accounts: 'Accounts',
proxies: 'Proxies',
redeemCodes: 'Redeem Codes',
ops: 'Ops',
promoCodes: 'Promo Codes',
settings: 'Settings',
myAccount: 'My Account',
lightMode: 'Light Mode',
darkMode: 'Dark Mode',
collapse: 'Collapse',
expand: 'Expand',
logout: 'Logout',
github: 'GitHub',
mySubscriptions: 'My Subscriptions',
buySubscription: 'Recharge / Subscription',
docs: 'Docs',
myOrders: 'My Orders',
orderManagement: 'Orders',
paymentDashboard: 'Payment Dashboard',
paymentConfig: 'Payment Config',
paymentPlans: 'Plans',
channelManagement: 'Channels',
channelPricing: 'Channel Pricing',
channelMonitor: 'Channel Monitor',
channelStatus: 'Channel Status',
riskControl: 'Risk Control',
securityAudit: 'Security Audit',
contentModeration: 'Content Moderation',
promptAudit: 'Prompt Audit',
auditLogs: 'Audit Logs',
},
// Auth
auth: {
welcomeBack: 'Welcome Back',
signInToAccount: 'Sign in to your account to continue',
signIn: 'Sign In',
signingIn: 'Signing in...',
passkeySignIn: 'Sign in with a passkey',
passkeySigningIn: 'Waiting for passkey...',
passkeyCancelled: 'Passkey sign-in was cancelled.',
passkeyFailed: 'Passkey sign-in failed. Please try again.',
createAccount: 'Create Account',
signUpToStart: 'Sign up to start using {siteName}',
signUp: 'Sign up',
processing: 'Processing...',
continue: 'Continue',
rememberMe: 'Remember me',
dontHaveAccount: "Don't have an account?",
alreadyHaveAccount: 'Already have an account?',
registrationDisabled: 'Registration is currently disabled. Please contact the administrator.',
emailLabel: 'Email',
emailPlaceholder: 'Enter your email',
passwordLabel: 'Password',
passwordPlaceholder: 'Enter your password',
createPasswordPlaceholder: 'Create a strong password',
passwordHint: 'At least 6 characters',
emailRequired: 'Email is required',
invalidEmail: 'Please enter a valid email address',
passwordRequired: 'Password is required',
passwordMinLength: 'Password must be at least 6 characters',
loginFailed: 'Login failed. Please check your credentials and try again.',
errors: {
USER_NOT_ACTIVE: 'Account has been disabled.',
},
registrationFailed: 'Registration failed. Please try again.',
emailDomainRegistrationLimit:
'This email domain cannot register another account. Please use a mainstream email, or contact support to add your enterprise domain to the allowlist.',
emailSuffixNotAllowed: 'This email domain is not allowed for registration.',
emailSuffixNotAllowedWithAllowed:
'This email domain is not allowed. Allowed domains: {suffixes}',
emailSuffixAllowedMore: 'and {count} more',
loginSuccess: 'Login successful! Welcome back.',
accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.',
reloginRequired: 'Session expired. Please log in again.',
turnstileExpired: 'Verification expired, please try again',
turnstileFailed: 'Verification failed, please try again',
captchaVerified: 'Verification completed',
captchaLoading: 'Loading verification…',
captchaClickToVerify: 'Click to complete verification',
captchaVerifying: 'Verifying…',
completeVerification: 'Please complete the verification',
verifyYourEmail: 'Verify Your Email',
sessionExpired: 'Session expired',
sessionExpiredDesc: 'Please go back to the registration page and start again.',
verificationCode: 'Verification Code',
verificationCodeHint: 'Enter the 6-digit code sent to your email',
sendingCode: 'Sending...',
sendCode: 'Send code',
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',
promoCodeInvalid: 'Invalid promo code',
promoCodeNotFound: 'Promo code not found',
promoCodeExpired: 'This promo code has expired',
promoCodeDisabled: 'This promo code is disabled',
promoCodeMaxUsed: 'This promo code has reached its usage limit',
promoCodeAlreadyUsed: 'You have already used this promo code',
promoCodeValidating: 'Promo code is being validated, please wait',
promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field',
invitationCodeLabel: 'Invitation Code',
invitationCodePlaceholder: 'Enter invitation code',
invitationCodeRequired: 'Invitation code is required',
invitationCodeValid: 'Invitation code is valid',
invitationCodeInvalid: 'Invalid or used invitation code',
invitationCodeValidating: 'Validating invitation code...',
invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again',
oauthOrContinue: 'or continue with others',
linuxdo: {
signIn: 'Continue with Linux.do',
orContinue: 'or continue with email',
callbackTitle: 'Signing you in',
callbackProcessing: 'Completing login, please wait...',
callbackHint: 'If you are not redirected automatically, go back to the login page and try again.',
callbackMissingToken: 'Missing login token, please try again.',
backToLogin: 'Back to Login',
invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.',
invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.',
completeRegistration: 'Complete Registration',
completing: 'Completing registration…',
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.'
},
dingtalk: {
signIn: 'Continue with DingTalk',
callbackTitle: 'Signing you in with DingTalk',
callbackProcessing: 'Completing DingTalk login, please wait...',
callbackHint: 'If you are not redirected automatically, go back to the login page and try again.',
callbackMissingToken: 'Missing login token, please try again.',
backToLogin: 'Back to Login',
invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.',
invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.',
completeRegistration: 'Complete Registration',
completing: 'Completing registration…',
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.',
createAccountTitle: 'Create DingTalk Account',
registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.',
error: {
title: 'DingTalk Sign-in Failed',
csrf: 'Login session expired, please scan again',
corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator',
dingtalk_not_enabled: 'DingTalk login is not enabled',
upstream_error: 'DingTalk service is temporarily unavailable. Please try again later',
missing_browser_session: 'Browser session lost. Please login again',
missing_params: 'Request parameters are incomplete',
invalid_state: 'Invalid login state',
provider_error: 'DingTalk authorization failed',
session_error: 'Failed to create session. Please retry',
retry: 'Retry Login'
}
},
emailOAuth: {
signIn: 'Continue with {providerName}'
},
oidc: {
signIn: 'Continue with {providerName}',
callbackTitle: 'Signing you in with {providerName}',
callbackProcessing: 'Completing login with {providerName}, please wait...',
callbackHint: 'If you are not redirected automatically, go back to the login page and try again.',
callbackMissingToken: 'Missing login token, please try again.',
backToLogin: 'Back to Login',
invitationRequired:
'This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.',
invalidPendingToken: 'The registration token has expired. Please sign in again.',
completeRegistration: 'Complete Registration',
completing: 'Completing registration…',
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.'
},
oauthFlow: {
profileDetailsTitle: 'Use {providerName} profile details',
profileDetailsDescription: 'Choose whether to apply the nickname or avatar from {providerName} to this account.',
useDisplayName: 'Use display name',
useAvatar: 'Use avatar',
avatarAlt: '{providerName} avatar',
reviewProfileBeforeContinue: 'Review the {providerName} profile details before continuing.',
chooseHowToContinue: 'Choose how to continue',
chooseAccountActionHint: 'Choose whether to bind an existing account or create a new one.',
suggestedEmail: 'Suggested email: {email}',
bindExistingAccount: 'Bind existing account',
createNewAccount: 'Create new account',
createAccountHint: 'Enter an email address to create your account and continue.',
bindLoginHint: 'Log in to an existing account to bind this {providerName} sign-in.',
signInThenBindDescription: 'Sign in to an existing account, then bind this {providerName} sign-in to it.',
bindSignInToExistingAccount: 'Bind this {providerName} sign-in to an existing account.',
bindCurrentAccountTitle: 'Bind the current account',
bindCurrentAccountDescription: 'Bind this {providerName} sign-in to the account currently signed in on this browser.',
bindCurrentAccount: 'Bind current account',
logInAndBind: 'Log in and bind',
useDifferentEmail: 'Use a different email',
backToOptions: 'Back to options',
yourAccount: 'your account',
totpHint: 'Enter the 6-digit verification code for {account} to finish binding this {providerName} sign-in.',
verifyAndContinue: 'Verify and continue',
wechatAvailabilityUnknown: 'WeChat sign-in availability could not be confirmed. Refresh and retry.',
wechatSystemBrowserOnly: 'This WeChat sign-in flow is only available in your system browser.',
wechatBrowserOnly: 'This WeChat sign-in flow is only available inside the WeChat browser.',
wechatNotConfigured: 'WeChat sign-in is not configured yet.'
},
linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback',
dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback',
dingtalkProviderName: 'DingTalk',
oidcCallbackPageTitle: 'OIDC Sign-In Callback',
oauthCallbackPageTitle: 'OAuth Callback',
wechatProviderName: 'WeChat',
wechatCallbackPageTitle: 'WeChat Sign-In Callback',
wechatPaymentCallbackPageTitle: 'WeChat Payment Callback',
wechatPayment: {
callbackTitle: 'Resuming WeChat payment',
callbackProcessing: 'Resuming WeChat payment...',
backToPayment: 'Back to payment',
callbackMissingResumeToken: 'The WeChat payment callback is missing the resume token.'
},
oauth: {
callbackTitle: 'OAuth Callback',
callbackHint: 'Copy the code and state back to the admin authorization flow when needed.',
invalidCallbackTitle: 'Invalid sign-in callback',
invalidCallbackHint: 'This page does not contain a valid authorization result. Return to the login page and start quick sign-in again.',
code: 'Code',
state: 'State',
fullUrl: 'Full URL'
},
// Forgot password
forgotPassword: 'Forgot password?',
forgotPasswordTitle: 'Reset Your Password',
forgotPasswordHint: 'Enter your email address and we will send you a link to reset your password.',
sendResetLink: 'Send Reset Link',
sendingResetLink: 'Sending...',
sendResetLinkFailed: 'Failed to send reset link. Please try again.',
resetEmailSent: 'Reset Link Sent',
resetEmailSentHint: 'If an account exists with this email, you will receive a password reset link shortly. Please check your inbox and spam folder.',
backToLogin: 'Back to Login',
rememberedPassword: 'Remembered your password?',
// Reset password
resetPasswordTitle: 'Set New Password',
resetPasswordHint: 'Enter your new password below.',
newPassword: 'New Password',
newPasswordPlaceholder: 'Enter your new password',
confirmPassword: 'Confirm Password',
confirmPasswordPlaceholder: 'Confirm your new password',
confirmPasswordRequired: 'Please confirm your password',
passwordsDoNotMatch: 'Passwords do not match',
resetPassword: 'Reset Password',
resettingPassword: 'Resetting...',
resetPasswordFailed: 'Failed to reset password. Please try again.',
passwordResetSuccess: 'Password Reset Successful',
passwordResetSuccessHint: 'Your password has been reset. You can now sign in with your new password.',
invalidResetLink: 'Invalid Reset Link',
invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.',
requestNewResetLink: 'Request New Reset Link',
invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.'
},
// Step-up (sudo) 2FA prompt
stepUp: {
title: 'Two-Factor Verification Required',
hint: 'Enter the 6-digit code from your authenticator app to continue this sensitive operation.',
verifyFailed: 'Verification failed, please try again',
notEnabled: 'This operation requires two-factor authentication. Please enable TOTP in your profile first.',
adminApiKeyForbidden: 'Admin API keys cannot perform this operation. Use a two-factor verified admin session.'
},
// Dashboard
}
+960
View File
@@ -0,0 +1,960 @@
export default {
dashboard: {
title: 'Dashboard',
welcomeMessage: "Welcome back! Here's an overview of your account.",
balance: 'Balance',
apiKeys: 'API Keys',
todayRequests: 'Today Requests',
todayCost: 'Today Cost',
todayTokens: 'Today Tokens',
totalTokens: 'Total Tokens',
cacheToday: 'Cache (Today)',
performance: 'Performance',
avgResponse: 'Avg Response',
averageTime: 'Average time',
timeRange: 'Time Range',
granularity: 'Granularity',
day: 'Day',
hour: 'Hour',
modelDistribution: 'Model Distribution',
groupDistribution: 'Group Usage Distribution',
platformBreakdown: 'Per-platform Breakdown',
platformBreakdownEmpty: 'No platform usage yet',
platformCount: '{count} platforms',
platformOther: 'Other',
platformQuota: {
title: 'Quota Usage',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly (30-day rolling)',
resetsAt: 'Resets {time}',
noLimit: 'unlimited',
disabled: 'Disabled',
},
tokenUsageTrend: 'Token Usage Trend',
noDataAvailable: 'No data available',
model: 'Model',
group: 'Group',
noGroup: 'No Group',
requests: 'Requests',
tokens: 'Tokens',
actual: 'Actual',
standard: 'Standard',
input: 'Input',
output: 'Output',
cache: 'Cache',
recentUsage: 'Recent Usage',
last7Days: 'Last 7 days',
noUsageRecords: 'No usage records',
startUsingApi: 'Start using the API to see your usage history here.',
viewAllUsage: 'View all usage',
quickActions: 'Quick Actions',
createApiKey: 'Create API Key',
generateNewKey: 'Generate a new API key',
batchImageAgent: 'Batch Image Assistant',
batchImageAgentDesc: 'Copy instructions for an agent',
viewUsage: 'View Usage',
checkDetailedLogs: 'Check detailed usage logs',
redeemCode: 'Redeem Code',
addBalanceWithCode: 'Add balance with a code'
},
// Groups (shared)
groups: {
subscription: 'Sub'
},
// API Keys
keys: {
title: 'API Keys',
description: 'Manage your API keys and access tokens',
searchPlaceholder: 'Search name or key...',
endpoints: {
title: 'API Endpoints',
default: 'Default',
copied: 'Copied',
copiedHint: 'Copied to clipboard',
clickToCopy: 'Click to copy this endpoint',
speedTest: 'Speed Test',
},
allGroups: 'All Groups',
allStatus: 'All Status',
columnSettings: 'Column Settings',
columnAlwaysVisible: 'This column is always visible',
createKey: 'Create API Key',
editKey: 'Edit API Key',
deleteKey: 'Delete API Key',
deleteConfirmMessage: "Are you sure you want to delete '{name}'? This action cannot be undone.",
id: 'ID',
apiKey: 'API Key',
group: 'Group',
currentConcurrency: 'Current Concurrency',
noGroup: 'No group',
searchGroup: 'Search groups...',
noGroupFound: 'No groups found',
created: 'Created',
copyToClipboard: 'Copy to clipboard',
copied: 'Copied!',
importToCcSwitch: 'Import to CCS',
enable: 'Enable',
disable: 'Disable',
nameLabel: 'Name',
namePlaceholder: 'My API Key',
groupLabel: 'Group',
selectGroup: 'Select a group',
statusLabel: 'Status',
selectStatus: 'Select status',
saving: 'Saving...',
noKeysYet: 'No API keys yet',
createFirstKey: 'Create your first API key to get started with the API.',
keyCreatedSuccess: 'API key created successfully',
keyUpdatedSuccess: 'API key updated successfully',
keyDeletedSuccess: 'API key deleted successfully',
keyEnabledSuccess: 'API key enabled successfully',
keyDisabledSuccess: 'API key disabled successfully',
failedToLoad: 'Failed to load API keys',
failedToSave: 'Failed to save API key',
failedToDelete: 'Failed to delete API key',
failedToUpdateStatus: 'Failed to update API key status',
clickToChangeGroup: 'Click to change group',
groupChangedSuccess: 'Group changed successfully',
failedToChangeGroup: 'Failed to change group',
groupRequired: 'Please select a group',
usage: 'Usage',
today: 'Today',
total: 'Last 30d',
quota: 'Quota',
lastUsedAt: 'Last Used',
lastUsedIP: 'Last Used IP',
useKey: 'Use Key',
useKeyModal: {
title: 'Use API Key',
description:
'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.',
copy: 'Copy',
copied: 'Copied',
note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
claudeSettingsHint: 'User-level persistent configuration. Do not commit this file containing your API key to a project repository.',
noGroupTitle: 'Please assign a group first',
noGroupDescription: 'This API key has not been assigned to a group. Please click the group column in the key list to assign one before viewing the configuration.',
openai: {
description: 'Add the following configuration files to your Codex CLI config directory.',
authModeTitle: 'Codex authentication mode',
authModeDescription: 'Compatibility mode keeps the existing setup for older Codex clients. API Key Mode authorizes the client-side image executor.',
authModeLegacy: 'Compatibility mode',
authModeApiKey: 'API Key Mode',
authModeApiKeyRestartNotice: 'After saving this configuration, completely quit and restart Codex Desktop or CLI, then create a new task so the client can rebuild its tool registry.',
configTomlHint: 'Make sure the following content is at the beginning of the config.toml file',
note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.',
noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.',
},
cliTabs: {
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
codexCli: 'Codex CLI',
codexCliWs: 'Codex CLI (WebSocket)',
grokCli: 'Grok CLI',
opencode: 'OpenCode',
},
antigravity: {
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
},
gemini: {
description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.',
modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview',
note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
},
grok: {
description:
'Configure Grok CLI, Claude Code, Codex, or OpenCode to send requests through your Sub2API Grok group. Text models use Responses; image/video use Imagine model IDs on media endpoints.',
claudeDescription: 'Configure Claude Code to send Messages API traffic through your Sub2API Grok group.',
codexDescription: 'Configure Codex to send Responses API traffic through your Sub2API Grok group.',
configTomlHint:
'Official path: ~/.grok/config.toml (or $GROK_HOME). Fill [endpoints] (models_base_url / models_list_url / xai_api_base_url / cli_chat_proxy_base_url), [auth] preferred_method=api_key, [models], [session], and [features] image/video overrides. Prefer env_key over api_key; every text model needs api_backend=responses. Back up before merge, then run grok inspect.',
codexConfigTomlHint:
'Official Codex: wire_api = "responses" only; prefer env_key over experimental_bearer_token; supports_websockets = false for non-OpenAI gateways (Sub2API can still accept client WS and bridge to HTTP/SSE). Back up ~/.codex/config.toml before merge.',
note:
'Export GROK_MODELS_BASE_URL and XAI_API_KEY, save the full config.toml (endpoints/auth/models/session/features) as ~/.grok/config.toml, run grok inspect, then /model grok-4.5 (or grok-build-0.1 for coding).',
noteWindows:
'Set GROK_MODELS_BASE_URL and XAI_API_KEY, save the full config.toml as %USERPROFILE%\\.grok\\config.toml, run grok inspect, then /model grok-4.5 (or grok-build-0.1 for coding).',
claudeNote:
'Choose one method: terminal env for this session, or ~/.claude/settings.json for persistence. Do not commit files that contain your API key.',
codexNote:
'Export SUB2API_API_KEY, save config.toml under ~/.codex (mkdir -p ~/.codex). Prefer env_key auth; do not commit secrets.',
codexNoteWindows:
'Set $env:SUB2API_API_KEY, save config.toml under %USERPROFILE%\\.codex. Prefer env_key auth; do not commit secrets.',
},
opencode: {
title: 'OpenCode Example',
subtitle: 'opencode.json',
hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.',
},
},
customKeyLabel: 'Custom Key',
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.',
customKeyTooShort: 'Custom key must be at least 16 characters',
customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens',
customKeyRequired: 'Please enter a custom key',
ipRestriction: 'IP Restriction',
ipWhitelist: 'IP Whitelist',
ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8',
ipWhitelistHint: 'One IP or CIDR per line. Only these IPs can use this key when set.',
ipBlacklist: 'IP Blacklist',
ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16',
ipBlacklistHint: 'One IP or CIDR per line. These IPs will be blocked from using this key.',
ipRestrictionEnabled: 'IP restriction enabled',
ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.',
ccsClientSelect: {
title: 'Select Client',
description: 'Please select the client type to import to CC-Switch:',
claudeCode: 'Claude Code',
claudeCodeDesc: 'Import as Claude Code configuration',
geminiCli: 'Gemini CLI',
geminiCliDesc: 'Import as Gemini CLI configuration',
},
// Quota and expiration
quotaLimit: 'Quota Limit',
quotaAmount: 'Quota Amount (USD)',
quotaAmountPlaceholder: 'Enter quota limit in USD',
quotaAmountHint: 'Set the maximum amount this key can spend. 0 = unlimited.',
quotaUsed: 'Quota Used',
reset: 'Reset',
resetQuotaUsed: 'Reset used quota to 0',
resetQuotaTitle: 'Confirm Reset Quota',
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',
resetNow: 'Resetting soon',
expiration: 'Expiration',
expiresInDays: '{days} days',
extendDays: '+{days} days',
customDate: 'Custom',
expirationDate: 'Expiration Date',
expirationDateHint: 'Select when this API key should expire.',
currentExpiration: 'Current expiration',
expiresAt: 'Expires',
noExpiration: 'Never',
status: {
active: 'Active',
inactive: 'Inactive',
quota_exhausted: 'Quota Exhausted',
expired: 'Expired',
},
},
// Usage
usage: {
title: 'Usage Records',
description: 'View and analyze your API usage history',
costDetails: 'Cost Breakdown',
tokenDetails: 'Token Breakdown',
cacheTtlOverriddenHint: 'Cache TTL Override enabled',
cacheTtlOverriddenLabel: 'TTL Override',
cacheTtlOverridden5m: 'Billed as 5m',
cacheTtlOverridden1h: 'Billed as 1h',
totalRequests: 'Total Requests',
totalTokens: 'Total Tokens',
cacheTotal: 'Cache',
cacheBreakdown: 'Cache Token Breakdown',
cacheCreationTokensLabel: 'Cache Creation',
cacheReadTokensLabel: 'Cache Read',
totalCost: 'Total Cost',
standardCost: 'Standard',
actualCost: 'Actual',
accountCost: 'Cost',
userBilled: 'User billed',
accountBilled: 'Account billed',
resetNow: 'Now',
resetPending: 'Pending refresh',
accountMultiplier: 'Account rate',
avgDuration: 'Avg Duration',
inSelectedRange: 'in selected range',
perRequest: 'per request',
apiKeyFilter: 'API Key',
allApiKeys: 'All API Keys',
timeRange: 'Time Range',
exportCsv: 'Export CSV',
exportExcel: 'Export Excel',
exportingProgress: 'Exporting data...',
exportedCount: 'Exported {current}/{total} records',
estimatedTime: 'Estimated time remaining: {time}',
cancelExport: 'Cancel Export',
exportCancelled: 'Export cancelled',
exporting: 'Exporting...',
preparingExport: 'Preparing export...',
model: 'Model',
requestedModel: 'Requested',
upstreamModel: 'Upstream',
sentUpstreamModel: 'Sent upstream',
upstreamResponseModel: 'Upstream response',
upstreamModelMismatch: 'Response model mismatch',
modelVariant: 'Possible version variant',
modelMismatch: 'Different model',
reasoningEffort: 'Reasoning Effort',
endpoint: 'Endpoint',
endpointDistribution: 'Endpoint Distribution',
inbound: 'Inbound',
upstream: 'Upstream',
mapping: 'Mapping',
path: 'Path',
inboundEndpoint: 'Inbound Endpoint',
upstreamEndpoint: 'Upstream Endpoint',
type: 'Type',
tokens: 'Tokens',
cost: 'Cost',
firstToken: 'First Token',
duration: 'Duration',
latency: 'Latency',
latencyFirstToken: 'First',
latencyDuration: 'Total',
time: 'Time',
ws: 'WS',
stream: 'Stream',
sync: 'Sync',
cyber: 'Cyber',
live: 'Live',
unknown: 'Unknown',
in: 'In',
out: 'Out',
cacheHit: 'Cache hit',
cacheCreate: 'Cache create',
cacheHitRate: 'Cache hit rate',
inputTokenPrice: 'Input price',
outputTokenPrice: 'Output price',
perMillionTokens: '/ 1M tokens',
unitPrice: 'Per-request price',
imageUnitPrice: 'Per-image price',
imageTotalPrice: 'Image total price',
imageCount: 'Image count',
imageBillingSize: 'Billing size',
imageInputSize: 'Input size',
imageOutputSize: 'Output size',
imageInputTokens: 'Image Input Tokens',
imageInputTokenPrice: 'Image Input Price',
imageInputCost: 'Image Input Cost',
imageOutputTokens: 'Image Output Tokens',
imageOutputTokenPrice: 'Image Output Price',
imageOutputCost: 'Image Output Cost',
imageSizeSource: 'Size source',
imageSizeBreakdown: 'Size breakdown',
imageSizeSourceOutput: 'Upstream output',
imageSizeSourceInput: 'Request input',
imageSizeSourceDefault: 'Default billing tier',
imageSizeSourceLegacy: 'Legacy record',
imageSizeSourceMissing: 'Not recorded',
imageSizeNotRecorded: 'not recorded',
imageSizeLegacyUnstandardized: 'legacy unstandardized',
imageSizeUnknown: 'unknown',
cacheRead: 'Read',
cacheWrite: 'Write',
serviceTier: 'Service tier',
serviceTierPriority: 'Fast',
serviceTierFlex: 'Flex',
serviceTierStandard: 'Standard',
rate: 'Rate',
original: 'Original',
billed: 'Billed',
noRecords: 'No usage records found. Try adjusting your filters.',
failedToLoad: 'Failed to load usage logs',
noDataToExport: 'No data to export',
exportSuccess: 'Usage data exported successfully',
exportFailed: 'Failed to export usage data',
exportExcelSuccess: 'Usage data exported successfully (Excel format)',
exportExcelFailed: 'Failed to export usage data',
imageUnit: ' images',
userAgent: 'User-Agent',
ipGeo: {
fetch: 'Fetch region',
fetching: 'Fetching...',
failed: 'Failed',
private: 'Private address',
refreshTitle: 'Refresh region info',
batchFetch: 'Batch fetch regions',
batchFetching: 'Fetching...',
pending: '{count} IPs pending',
batchFailed: 'Failed to batch fetch IP regions',
detailOrg: 'ISP',
detailTimezone: 'Timezone',
detailAccuracy: 'Accuracy',
detailCoordinates: 'Coordinates',
},
tabs: { usage: 'Usage', errors: 'Error Requests', ranking: 'User Ranking' },
errors: {
time: 'Time', model: 'Model', endpoint: 'Endpoint', status: 'Status',
category: 'Category', platform: 'Platform', message: 'Message',
keyName: 'Key Name', keyDeleted: 'Deleted', allKeys: 'All keys',
modelPlaceholder: 'Search model', allCategories: 'All categories', allStatuses: 'All status codes',
empty: 'No error requests', failedToLoad: 'Failed to load error requests',
categories: {
auth: 'Auth failed', rate_limit: 'Rate limited', quota: 'Balance/Subscription',
invalid_request: 'Invalid request', service_unavailable: 'Service unavailable',
upstream: 'Upstream error', internal: 'Platform error', other: 'Other', cyber: 'Cyber policy',
},
detail: {
title: 'Error Request Detail',
responseBody: 'Response Body',
upstreamStatus: 'Upstream Status',
loadFailed: 'Failed to load detail, please try again',
},
},
},
// Shared keys for channel monitor (admin + user views)
monitorCommon: {
status: {
operational: 'Operational',
degraded: 'Degraded',
failed: 'Failed',
error: 'Error',
unknown: '-'
},
providers: {
openai: 'OpenAI',
anthropic: 'Anthropic',
gemini: 'Gemini',
grok: 'Grok',
antigravity: 'Antigravity',
kimi: 'Kimi',
zhipu: 'Zhipu GLM',
deepseek: 'DeepSeek'
},
// Check modes (how a monitor performs its checks)
checkMode: {
probe: 'Probe',
quota: 'Quota',
quota_probe: 'Probe + Quota'
},
// Quota snapshot rendering (MonitorQuotaView, shared by admin + user views)
quota: {
unavailable: 'Quota unavailable',
resetSoon: 'resetting',
windows: {
'5h': '5h',
'7d': '7d',
'7dSonnet': '7d Sonnet',
'7dFable': '7d Fable',
weekly: 'Weekly',
daily: 'Daily',
'30d': '30d',
total: 'Total'
},
labels: {
requests: 'Requests',
tokens: 'Tokens',
shared: 'Shared',
pro: 'Pro',
flash: 'Flash'
}
},
extraModelsHeader: 'Extra Models',
extraModelsEmpty: 'No extra models',
latencyEmpty: '-',
availabilityPrefix: 'Availability',
dialogLatency: 'Dialog Latency',
endpointPing: 'Endpoint PING',
history60pts: 'HISTORY ({n} PTS)',
nextUpdateIn: 'NEXT UPDATE IN {n}s',
past: 'PAST',
now: 'NOW',
maintenancePaused: 'Maintenance · timeline paused',
extraModelsCount: '+ {n} models',
pollEvery: '{n}s polling',
updatedAt: 'Updated {time}',
relativeSecondsAgo: '{n}s ago',
relativeMinutesAgo: '{n}m ago',
relativeHoursAgo: '{n}h ago',
relativeDaysAgo: '{n}d ago'
},
// Channel Status (user-facing read-only view)
channelStatus: {
title: 'Channel Status',
description: 'Inspect channel availability, latency and recent status',
searchPlaceholder: 'Search channels...',
allProviders: 'All Providers',
loadError: 'Failed to load channel status',
detailLoadError: 'Failed to load channel detail',
detailTitle: 'Channel Detail',
closeDetail: 'Close',
windowTab: {
'7d': '7 days',
'15d': '15 days',
'30d': '30 days'
},
overall: {
operational: 'OPERATIONAL',
degraded: 'DEGRADED',
unavailable: 'UNAVAILABLE'
},
columns: {
name: 'Name',
provider: 'Provider',
groupName: 'Group',
primaryModel: 'Primary Model',
availability7d: '7d Availability',
latency: 'Latency (ms)'
},
detailColumns: {
model: 'Model',
latestStatus: 'Latest Status',
latestLatency: 'Latest Latency (ms)',
availability7d: '7d Availability',
availability15d: '15d Availability',
availability30d: '30d Availability',
avgLatency7d: '7d Avg Latency (ms)'
},
empty: {
title: 'No channels available',
description: 'No monitored channels have been configured yet.'
}
},
// Available Channels (user-facing)
availableChannels: {
title: 'Available Channels',
description: 'Channels you can access, along with their supported models and pricing',
searchPlaceholder: 'Search channels or models...',
empty: 'No available channels',
noModels: 'No models configured',
noPricing: 'Pricing not configured',
exclusive: 'Exclusive',
public: 'Public',
exclusiveTooltip: 'Exclusive groups granted to you by an admin',
publicTooltip: 'Groups open to all users',
columns: {
name: 'Channel',
description: 'Description',
platform: 'Platform',
groups: 'Your Accessible Groups',
supportedModels: 'Supported Models'
},
pricing: {
billingMode: 'Billing Mode',
billingModeToken: 'Per Token',
billingModePerRequest: 'Per Request',
billingModeImage: 'Per Image',
billingModeVideo: 'Per Video',
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
cacheReadPrice: 'Cache Read',
imageInputPrice: 'Image Input',
imageOutputPrice: 'Image Output',
perRequestPrice: 'Per Request',
intervals: 'Tiered Pricing',
unitPerMillion: '/ 1M tokens',
unitPerRequest: '/ request'
}
},
// Model Plaza (public group/model pricing showcase)
modelPlaza: {
title: 'Model Plaza',
description: 'Browse available models and pricing by group',
loading: 'Loading...',
empty: 'No groups to display',
loadFailed: 'Failed to load model plaza',
noSearchResult: 'No matching models',
anonymousHint: 'Sign in to see your exclusive groups and personal rates',
filters: {
platformLabel: 'Platform',
groupLabel: 'Group',
rateLabel: 'Rate',
modelLabel: 'Model',
searchPlaceholder: 'Search models',
all: 'All'
},
badges: {
exclusive: 'Exclusive',
subscription: 'Subscription'
},
detail: {
noModels: 'No models configured for this group',
noPricing: 'Pricing not configured',
peakNote: 'Peak hours {window}: billing rate ×{multiplier}'
},
table: {
model: 'Model',
input: 'Input',
output: 'Output',
cache: 'Cache',
cacheWrite: 'Write',
cacheRead: 'Read',
paidPrice: 'Your Price (Discounted)',
officialPrice: 'Official Price',
rate: 'Rate',
unitPerMillion: '$ / 1M tokens',
perUnitRequest: '/ request',
perUnitImage: '/ image',
perRequest: 'Per request',
perImage: 'Per image'
},
nav: {
login: 'Sign In',
backToDashboard: 'Back to Console'
}
},
affiliate: {
title: 'Affiliate Rebates',
description: 'Invite new users and convert your rebate quota into account balance',
yourCode: 'Your Affiliate Code',
inviteLink: 'Invite Link',
copyCode: 'Copy Code',
copyLink: 'Copy Link',
codeCopied: 'Affiliate code copied',
linkCopied: 'Invite link copied',
loadFailed: 'Failed to load affiliate data',
transferFailed: 'Failed to transfer affiliate quota',
stats: {
rebateRate: 'My Rebate Rate',
rebateRateHint: 'What you earn each time an invitee recharges',
invitedUsers: 'Invited Users',
availableQuota: 'Available Rebate Quota',
frozenQuota: 'Frozen',
frozenQuotaHint: 'Recently earned rebates pending release',
totalQuota: 'Historical Rebate Quota'
},
transfer: {
title: 'Transfer Rebate Quota',
description: 'Move available rebate quota into your account balance',
button: 'Transfer to Balance',
transferring: 'Transferring...',
empty: 'No available rebate quota',
success: '{amount} has been transferred to your balance'
},
invitees: {
title: 'Invited Users',
empty: 'No invited users yet',
columns: {
email: 'Email',
username: 'Username',
rebate: 'Rebate',
joinedAt: 'Joined At'
}
},
tips: {
title: 'How It Works',
line1: 'Share your affiliate code or invite link with new users.',
line2: 'When invitees recharge, you receive {rate} of the recharge as rebate quota.',
line3: 'Transfer rebate quota to balance at any time.',
line4: 'Newly earned rebates may have a waiting period before they can be transferred.'
}
},
// Redeem
redeem: {
title: 'Redeem Code',
description: 'Enter your redeem code to add balance or increase concurrency',
currentBalance: 'Current Balance',
concurrency: 'Concurrency',
requests: 'requests',
redeemCodeLabel: 'Redeem Code',
redeemCodePlaceholder: 'Enter your redeem code',
redeemCodeHint: 'Redeem codes are case-sensitive',
redeeming: 'Redeeming...',
redeemButton: 'Redeem Code',
redeemSuccess: 'Code Redeemed Successfully!',
redeemFailed: 'Redemption Failed',
added: 'Added',
concurrentRequests: 'concurrent requests',
newBalance: 'New Balance',
newConcurrency: 'New Concurrency',
aboutCodes: 'About Redeem Codes',
codeRule1: 'Each code can only be used once',
codeRule2: 'Codes may add balance, increase concurrency, or grant trial access',
codeRule3: 'Contact support if you have issues redeeming a code',
codeRule4: 'Balance and concurrency updates are immediate',
recentActivity: 'Recent Activity',
historyWillAppear: 'Your redemption history will appear here',
balanceAddedRedeem: 'Balance Added (Redeem)',
balanceAddedAffiliate: 'Balance Added (Affiliate Transfer)',
balanceAddedAdmin: 'Balance Added (Admin)',
balanceDeductedAdmin: 'Balance Deducted (Admin)',
concurrencyAddedRedeem: 'Concurrency Added (Redeem)',
concurrencyAddedAdmin: 'Concurrency Added (Admin)',
concurrencyReducedAdmin: 'Concurrency Reduced (Admin)',
adminAdjustment: 'Admin Adjustment',
subscriptionAssigned: 'Subscription Assigned',
subscriptionAssignedDesc: 'You have been granted access to {groupName}',
subscriptionDays: '{days} days',
days: ' days',
codeRedeemSuccess: 'Code redeemed successfully!',
failedToRedeem: 'Failed to redeem code. Please check the code and try again.',
subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.',
pleaseEnterCode: 'Please enter a redeem code'
},
// Profile
profile: {
title: 'Profile Settings',
description: 'Manage your account information and settings',
accountBalance: 'Account Balance',
concurrencyLimit: 'Concurrency Limit',
rpmLimit: 'RPM Limit',
rpmUnlimited: 'Unlimited',
memberSince: 'Member Since',
overviewTitle: 'Account Overview',
overviewDescription: 'Check account status, profile sources, and common actions at a glance.',
basicsTitle: 'Profile & Avatar',
basicsDescription: 'Keep your public profile details and avatar aligned.',
linkedProfileSources: 'Profile Sources',
linkedProfileSourcesDescription: 'Some profile details may stay synced from third-party sign-in methods.',
securityTitle: 'Security Settings',
securityDescription: 'Password, two-factor authentication, and alerts live in the right rail.',
administrator: 'Administrator',
user: 'User',
username: 'Username',
email: 'Email',
status: 'Status',
role: 'Role',
enterUsername: 'Enter username',
editProfile: 'Edit Profile',
updateProfile: 'Update Profile',
updating: 'Updating...',
updateSuccess: 'Profile updated successfully',
updateFailed: 'Failed to update profile',
usernameRequired: 'Username is required',
changePassword: 'Change Password',
currentPassword: 'Current Password',
newPassword: 'New Password',
confirmNewPassword: 'Confirm New Password',
passwordHint: 'Password must be at least 8 characters long',
changingPassword: 'Changing...',
changePasswordButton: 'Change Password',
passwordsNotMatch: 'New passwords do not match',
passwordTooShort: 'Password must be at least 8 characters long',
passwordChangeSuccess: 'Password changed successfully',
passwordChangeFailed: 'Failed to change password',
// TOTP 2FA
totp: {
title: 'Two-Factor Authentication (2FA)',
description: 'Enhance account security with Google Authenticator or similar apps',
enabled: 'Enabled',
enabledAt: 'Enabled at',
notEnabled: 'Not Enabled',
notEnabledHint: 'Enable two-factor authentication to enhance account security',
enable: 'Enable',
disable: 'Disable',
featureDisabled: 'Feature Unavailable',
featureDisabledHint: 'Two-factor authentication has not been enabled by the administrator',
setupTitle: 'Set Up Two-Factor Authentication',
setupStep1: 'Scan the QR code below with your authenticator app',
setupStep2: 'Enter the 6-digit code from your app',
manualEntry: "Can't scan? Enter the key manually:",
enterCode: 'Enter 6-digit code',
verify: 'Verify',
setupFailed: 'Failed to get setup information',
verifyFailed: 'Invalid code, please try again',
enableSuccess: 'Two-factor authentication enabled',
disableTitle: 'Disable Two-Factor Authentication',
disableWarning: 'After disabling, you will no longer need a verification code to log in. This may reduce your account security.',
enterPassword: 'Enter your current password to confirm',
confirmDisable: 'Confirm Disable',
disableSuccess: 'Two-factor authentication disabled',
disableFailed: 'Failed to disable, please check your password',
loginTitle: 'Two-Factor Authentication',
loginHint: 'Enter the 6-digit code from your authenticator app',
loginFailed: 'Verification failed, please try again',
// New translations for email verification
verifyEmailFirst: 'Please verify your email first',
verifyPasswordFirst: 'Please verify your identity first',
emailCode: 'Email Verification Code',
enterEmailCode: 'Enter 6-digit code',
sendCode: 'Send Code',
codeSent: 'Verification code sent to your email',
sendCodeFailed: 'Failed to send verification code'
},
passkey: {
title: 'Passkeys',
description: 'Use Face ID, Touch ID, Windows Hello, or a security key to sign in without a password.',
add: 'Add passkey',
continue: 'Create passkey',
name: 'Passkey name',
namePlaceholder: 'For example, MacBook Touch ID',
passwordPlaceholder: 'Enter your current password to confirm',
empty: 'No passkeys are registered yet.',
synced: 'Synced',
createdAt: 'Created {date}',
lastUsed: 'Last used {date}',
featureDisabled: 'Passkeys have not been configured by the administrator.',
unsupported: 'This browser or device does not support passkeys.',
loadFailed: 'Failed to load passkeys.',
added: 'Passkey added.',
addFailed: 'Failed to add passkey.',
renamePrompt: 'Enter a new name for this passkey',
renamed: 'Passkey renamed.',
renameFailed: 'Failed to rename passkey.',
deleteTitle: 'Delete passkey',
deleteConfirm: 'Delete “{name}”? You will no longer be able to sign in with it.',
deleted: 'Passkey deleted.',
deleteFailed: 'Failed to delete passkey.'
},
balanceNotify: {
title: 'Balance Low Notification',
description: 'Send email alert when account balance falls below threshold',
enabled: 'Enable Balance Low Notification',
threshold: 'Custom Threshold',
thresholdHint: 'Leave empty to use system default',
thresholdPlaceholder: 'Enter amount',
systemDefault: 'System Default',
extraEmails: 'Notification Emails',
extraEmailsHint: 'You must add and verify an email address to receive low balance alerts',
primaryEmail: 'Primary',
noExtraEmails: 'No extra notification emails',
enterEmail: 'Enter email address',
addEmail: 'Add Email',
emailPlaceholder: 'Enter email address',
sendCode: 'Send Code',
resend: 'Resend',
codeSent: 'Verification code sent',
codeSentTo: 'Code sent to {email}',
enterCode: 'Enter verification code',
codePlaceholder: '6-digit code',
verify: 'Verify',
emailAdded: 'Email added',
emailRemoved: 'Email removed',
verifySuccess: 'Email added successfully',
removeEmail: 'Remove',
removeSuccess: 'Email removed',
emailDuplicate: 'This email already exists',
maxEmailsReached: 'Maximum number of notification emails reached',
unverified: 'Unverified',
verified: 'Verified',
},
avatar: {
title: 'Profile Avatar',
description: 'Upload an avatar image. Static uploads are compressed to 20KB before saving.',
uploadAction: 'Upload image',
uploadHint: 'Static uploads are compressed to 20KB when possible. GIF uploads must already be within 20KB.',
uploadRequired: 'Upload an avatar image first',
saveSuccess: 'Avatar updated',
deleteSuccess: 'Avatar removed',
invalidType: 'Please choose an image file',
gifTooLarge: 'GIF avatars must already be 20KB or smaller',
compressTooLarge: 'Unable to compress this image below 20KB. Try a smaller image.',
compressFailed: 'Failed to compress the selected image.',
readFailed: 'Failed to read the selected image.',
emptyDeleteHint: 'Avatar is already empty',
},
authBindings: {
title: 'Connected Sign-In Methods',
description: 'View current bindings and connect another provider to this account.',
bindAction: 'Bind {providerName}',
bindSuccess: 'Account linked successfully',
emailPlaceholder: 'Enter email address',
codePlaceholder: 'Enter verification code',
passwordPlaceholder: 'Set a login password',
replaceEmailPasswordPlaceholder: 'Enter current password',
sendCodeAction: 'Send code',
manageEmailAction: 'Manage email',
hideEmailFormAction: 'Hide email form',
confirmEmailBindAction: 'Bind email',
confirmEmailReplaceAction: 'Replace primary email',
codeSentTo: 'Code sent to {email}',
replaceSuccess: 'Primary email updated',
unbindAction: 'Unbind',
unbindSuccess: '{providerName} unbound',
boundCount: '{count} linked records',
status: {
bound: 'Bound',
notBound: 'Not bound',
},
providers: {
email: 'Email',
linuxdo: 'LinuxDo',
dingtalk: 'DingTalk',
oidc: '{providerName}',
wechat: 'WeChat',
},
notes: {
emailManagedFromProfile: 'Primary email is managed in the profile form',
canUnbind: 'You can unbind this sign-in method',
bindAnotherBeforeUnbind: 'Bind another sign-in method before unbinding',
},
source: {
avatar: 'Avatar is currently synced from {providerName}',
username: 'Nickname is currently synced from {providerName}',
},
}
},
// Empty States
empty: {
noData: 'No data found'
},
// Table
table: {
expandActions: 'Expand More Actions',
collapseActions: 'Collapse Actions'
},
// Pagination
pagination: {
showing: 'Showing',
to: 'to',
of: 'of',
results: 'results',
page: 'Page',
pageOf: 'Page {page} of {total}',
previous: 'Previous',
next: 'Next',
perPage: 'Per page',
goToPage: 'Go to page {page}',
jumpTo: 'Jump to',
jumpPlaceholder: 'Page',
jumpAction: 'Go'
},
// Errors
errors: {
somethingWentWrong: 'Something went wrong',
pageNotFound: 'Page not found',
unauthorized: 'Unauthorized',
forbidden: 'Forbidden',
serverError: 'Server error',
networkError: 'Network error',
timeout: 'Request timeout',
tryAgain: 'Please try again'
},
// Dates
dates: {
today: 'Today',
yesterday: 'Yesterday',
thisWeek: 'This Week',
lastWeek: 'Last Week',
thisMonth: 'This Month',
lastMonth: 'Last Month',
last24Hours: 'Last 24 Hours',
last7Days: 'Last 7 Days',
last14Days: 'Last 14 Days',
last30Days: 'Last 30 Days',
custom: 'Custom',
startDate: 'Start Date',
endDate: 'End Date',
apply: 'Apply',
selectDateRange: 'Select date range'
},
// Admin
}
+17
View File
@@ -0,0 +1,17 @@
import landing from './landing'
import common from './common'
import dashboard from './dashboard'
import channelMonitorV2 from './channelMonitorV2'
import batchImage from './batchImage'
import admin from './admin'
import misc from './misc'
export default {
...landing,
...common,
...dashboard,
...channelMonitorV2,
...batchImage,
admin,
...misc,
}
+257
View File
@@ -0,0 +1,257 @@
export default {
batchImageGuide: {
title: 'Batch Image Generation',
description: 'Submit multiple prompts in one job and download the generated images when complete'
},
// Home Page
home: {
viewOnGithub: 'View on GitHub',
viewDocs: 'View Documentation',
docs: 'Docs',
switchToLight: 'Switch to Light Mode',
switchToDark: 'Switch to Dark Mode',
dashboard: 'Dashboard',
login: 'Login',
getStarted: 'Get Started',
goToDashboard: 'Go to Dashboard',
// User-focused value proposition
heroSubtitle: 'One Key, All AI Models',
heroDescription: 'No need to manage multiple subscriptions. Access Claude, GPT, Gemini and more with a single API key',
tags: {
subscriptionToApi: 'Subscription to API',
stickySession: 'Session Persistence',
realtimeBilling: 'Pay As You Go'
},
// Pain points section
painPoints: {
title: 'Sound Familiar?',
items: {
expensive: {
title: 'High Subscription Costs',
desc: 'Paying for multiple AI subscriptions that add up every month'
},
complex: {
title: 'Account Chaos',
desc: 'Managing scattered accounts and API keys across different platforms'
},
unstable: {
title: 'Service Interruptions',
desc: 'Single accounts hitting rate limits and disrupting your workflow'
},
noControl: {
title: 'No Usage Control',
desc: "Can't track where your money goes or limit team member usage"
}
}
},
// Solutions section
solutions: {
title: 'We Solve These Problems',
subtitle: 'Three simple steps to stress-free AI access'
},
features: {
unifiedGateway: 'One-Click Access',
unifiedGatewayDesc: 'Get a single API key to call all connected AI models. No separate applications needed.',
multiAccount: 'Always Reliable',
multiAccountDesc: 'Smart routing across multiple upstream accounts with automatic failover. Say goodbye to errors.',
balanceQuota: 'Pay What You Use',
balanceQuotaDesc: 'Usage-based billing with quota limits. Full visibility into team consumption.'
},
// Comparison section
comparison: {
title: 'Why Choose Us?',
headers: {
feature: 'Comparison',
official: 'Official Subscriptions',
us: 'Our Platform'
},
items: {
pricing: {
feature: 'Pricing',
official: 'Fixed monthly fee, pay even if unused',
us: 'Pay only for what you use'
},
models: {
feature: 'Model Selection',
official: 'Single provider only',
us: 'Switch between models freely'
},
management: {
feature: 'Account Management',
official: 'Manage each service separately',
us: 'Unified key, one dashboard'
},
stability: {
feature: 'Stability',
official: 'Single account rate limits',
us: 'Multi-account pool, auto-failover'
},
control: {
feature: 'Usage Control',
official: 'Not available',
us: 'Quotas & detailed analytics'
}
}
},
providers: {
title: 'Supported AI Models',
description: 'One API, Multiple Choices',
supported: 'Supported',
soon: 'Soon',
claude: 'Claude',
gemini: 'Gemini',
antigravity: 'Antigravity',
more: 'More'
},
// CTA section
cta: {
title: 'Ready to Get Started?',
description: 'Sign up now and get free trial credits to experience seamless AI access',
button: 'Sign Up Free'
},
footer: {
allRightsReserved: 'All rights reserved.'
}
},
// Key Usage Query Page
keyUsage: {
title: 'API Key Usage',
subtitle: 'Enter your API Key to view real-time spending and usage status',
placeholder: 'sk-ant-mirror-xxxxxxxxxxxx',
query: 'Query',
querying: 'Querying...',
privacyNote: 'Your Key is processed locally in the browser and will not be stored',
dateRange: 'Date Range:',
dateRangeToday: 'Today',
dateRange7d: '7 Days',
dateRange30d: '30 Days',
dateRange90d: '90 Days',
dateRangeCustom: 'Custom',
apply: 'Apply',
used: 'Used',
detailInfo: 'Detail Information',
tokenStats: 'Token Statistics',
dailyDetail: 'Daily Detail',
modelStats: 'Model Usage Statistics',
// Table headers
date: 'Date',
model: 'Model',
requests: 'Requests',
inputTokens: 'Input Tokens',
outputTokens: 'Output Tokens',
cacheCreationTokens: 'Cache Creation',
cacheReadTokens: 'Cache Read',
cacheWriteTokens: 'Cache Write',
totalTokens: 'Total Tokens',
cost: 'Cost',
// Status
quotaMode: 'Key Quota Mode',
walletBalance: 'Wallet Balance',
// Ring card titles
totalQuota: 'Total Quota',
limit5h: '5-Hour Limit',
limitDaily: 'Daily Limit',
limit7d: '7-Day Limit',
limitWeekly: 'Weekly Limit',
limitMonthly: 'Monthly Limit',
// Detail rows
remainingQuota: 'Remaining Quota',
expiresAt: 'Expires At',
todayExpires: '(expires today)',
daysLeft: '({days} days)',
usedQuota: 'Used Quota',
resetNow: 'Resetting soon',
subscriptionType: 'Subscription Type',
subscriptionExpires: 'Subscription Expires',
// Usage stat cells
todayRequests: 'Today Requests',
todayInputTokens: 'Today Input',
todayOutputTokens: 'Today Output',
todayTokens: 'Today Tokens',
todayCacheCreation: 'Today Cache Creation',
todayCacheRead: 'Today Cache Read',
todayCost: 'Today Cost',
rpmTpm: 'RPM / TPM',
totalRequests: 'Total Requests',
totalInputTokens: 'Total Input',
totalOutputTokens: 'Total Output',
totalTokensLabel: 'Total Tokens',
totalCacheCreation: 'Total Cache Creation',
totalCacheRead: 'Total Cache Read',
totalCost: 'Total Cost',
avgDuration: 'Avg Duration',
// Messages
enterApiKey: 'Please enter an API Key',
querySuccess: 'Query successful',
queryFailed: 'Query failed',
queryFailedRetry: 'Query failed, please try again later',
noDailyUsage: 'No daily usage data',
},
// Setup Wizard
setup: {
title: 'Sub2API Setup',
description: 'Configure your Sub2API instance',
database: {
title: 'Database Configuration',
description: 'Connect to your PostgreSQL database',
host: 'Host',
port: 'Port',
username: 'Username',
password: 'Password',
databaseName: 'Database Name',
sslMode: 'SSL Mode',
passwordPlaceholder: 'Password',
ssl: {
disable: 'Disable',
require: 'Require',
verifyCa: 'Verify CA',
verifyFull: 'Verify Full'
}
},
redis: {
title: 'Redis Configuration',
description: 'Connect to your Redis server',
host: 'Host',
port: 'Port',
username: 'Username (optional)',
password: 'Password (optional)',
database: 'Database',
usernamePlaceholder: 'Leave empty for default user',
passwordPlaceholder: 'Password',
enableTls: 'Enable TLS',
enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)'
},
admin: {
title: 'Admin Account',
description: 'Create your administrator account',
email: 'Email',
password: 'Password',
confirmPassword: 'Confirm Password',
passwordPlaceholder: 'Min 8 characters',
confirmPasswordPlaceholder: 'Confirm password',
passwordMismatch: 'Passwords do not match'
},
ready: {
title: 'Ready to Install',
description: 'Review your configuration and complete setup',
database: 'Database',
redis: 'Redis',
adminEmail: 'Admin Email'
},
status: {
testing: 'Testing...',
success: 'Connection Successful',
testConnection: 'Test Connection',
installing: 'Installing...',
completeInstallation: 'Complete Installation',
completed: 'Installation completed!',
redirecting: 'Redirecting to login page...',
restarting: 'Service is restarting, please wait...',
timeout: 'Service restart is taking longer than expected. Please refresh the page manually.'
}
},
// Common
}
+621
View File
@@ -0,0 +1,621 @@
export default {
// Subscription Progress (Header component)
subscriptionProgress: {
title: 'My Subscriptions',
viewDetails: 'View subscription details',
activeCount: '{count} active subscription(s)',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
daysRemaining: '{days} days left',
expired: 'Expired',
expiresToday: 'Expires today',
expiresTomorrow: 'Expires tomorrow',
viewAll: 'View all subscriptions',
noSubscriptions: 'No active subscriptions',
unlimited: 'Unlimited'
},
// Version Badge
version: {
currentVersion: 'Current Version',
latestVersion: 'Latest Version',
upToDate: "You're running the latest version.",
updateAvailable: 'A new version is available!',
releaseNotes: 'Release Notes',
noReleaseNotes: 'No release notes',
viewUpdate: 'View Update',
viewRelease: 'View Release',
viewChangelog: 'View Changelog',
refresh: 'Refresh',
sourceMode: 'Source Build',
sourceModeHint: 'Source build, use git pull to update',
updateNow: 'Update Now',
updating: 'Updating...',
updateComplete: 'Update Complete',
updateFailed: 'Update Failed',
restartRequired: 'Please restart the service to apply the update',
restartNow: 'Restart Now',
restarting: 'Restarting...',
retry: 'Retry',
rollback: 'Version Rollback',
rollbackSelectVersion: 'Select a version to roll back to (last 3 versions)',
rollbackConfirm: 'Roll back to {version}',
rollbackWarning:
'Rollback downloads the selected version and replaces the current binary. A service restart is required afterwards.',
rollingBack: 'Rolling back...',
rollbackComplete: 'Rollback Complete',
rollbackFailed: 'Rollback Failed',
manualRollbackCommand: 'Manual rollback',
copyCommand: 'Copy',
copied: 'Copied',
noRollbackVersions: 'No versions available for rollback',
loadVersionsFailed: 'Failed to load versions',
rollbackSourceHint: 'Online rollback is not available for source builds',
deployScript: 'Script',
deployDocker: 'Docker',
dockerEditCompose: 'Edit the image tag in docker-compose.yml',
dockerRecreate: 'Recreate the container'
},
// Recharge / Subscription Page
purchase: {
title: 'Recharge / Subscription',
description: 'Recharge balance or purchase subscription via the embedded page',
openInNewTab: 'Open in new tab',
notEnabledTitle: 'Feature not enabled',
notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.',
notConfiguredTitle: 'Recharge / Subscription URL not configured',
notConfiguredDesc:
'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.',
tableOfContents: 'Contents',
copyCode: 'Copy',
copiedCode: 'Copied',
copyCodeFailed: 'Failed'
},
// Announcements Page
announcements: {
title: 'Announcements',
description: 'View system announcements',
unreadOnly: 'Show unread only',
markRead: 'Mark as read',
markAllRead: 'Mark all as read',
viewAll: 'View all announcements',
markedAsRead: 'Marked as read',
allMarkedAsRead: 'All announcements marked as read',
newCount: '{count} new announcement | {count} new announcements',
readAt: 'Read at',
read: 'Read',
unread: 'Unread',
startsAt: 'Starts at',
endsAt: 'Ends at',
empty: 'No announcements',
emptyUnread: 'No unread announcements',
total: 'announcements',
emptyDescription: 'There are no system announcements at this time',
readStatus: 'You have read this announcement',
markReadHint: 'Click "Mark as read" to mark this announcement'
},
// User Subscriptions Page
userSubscriptions: {
title: 'My Subscriptions',
description: 'View your subscription plans and usage',
noActiveSubscriptions: 'No Active Subscriptions',
noActiveSubscriptionsDesc:
"You don't have any active subscriptions. Contact administrator to get one.",
failedToLoad: 'Failed to load subscriptions',
status: {
active: 'Active',
expired: 'Expired',
revoked: 'Revoked'
},
usage: 'Usage',
expires: 'Expires',
noExpiration: 'No expiration',
unlimited: 'Unlimited',
unlimitedDesc: 'No usage limits on this subscription',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
daysRemaining: '{days} days remaining',
expiresOn: 'Expires on {date}',
resetIn: 'Resets in {time}',
quotaEndsIn: 'Quota ends in {time}',
windowNotActive: 'Awaiting first use',
usageOf: '{used} of {limit}'
},
// Onboarding Tour
onboarding: {
restartTour: 'Restart Onboarding Tour',
dontShowAgain: "Don't show again",
dontShowAgainTitle: 'Permanently close onboarding guide',
confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.",
confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.',
interactiveHint: 'Press Enter or Click to continue',
navigation: {
flipPage: 'Flip Page',
exit: 'Exit'
},
// Admin tour steps
admin: {
welcome: {
title: '👋 Welcome to Sub2API',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.</p><p style="margin-bottom: 12px;"><b>🎯 Core Features:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Group Management</b> - Create service tiers (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream AI service accounts</li><li>🔑 <b>Key Distribution</b> - Generate independent API Keys for users</li><li>💰 <b>Billing Control</b> - Flexible rate and quota management</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>',
nextBtn: 'Start Setup 🚀',
prevBtn: 'Skip'
},
groupManage: {
title: '📦 Step 1: Group Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of Sub2API, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>'
},
createGroup: {
title: ' Create New Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Let\'s create your first group.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📝 Tip:</b> Recommend creating a test group first to familiarize yourself with the process</p><p style="color: #10b981; font-weight: 600;">👉 Click the "Create Group" button</p></div>'
},
groupName: {
title: '✏️ 1. Group Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Give your group an easy-to-identify name.</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>💡 Naming Suggestions:</b><ul style="margin: 8px 0 0 16px;"><li>"Test Group" - For testing</li><li>"VIP Premium" - High-quality service</li><li>"Free Trial" - Trial version</li></ul></div><p style="font-size: 13px; color: #6b7280;">Click "Next" when done</p></div>',
nextBtn: 'Next'
},
groupPlatform: {
title: '🤖 2. Select Platform',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the AI platform this group supports.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 Platform Guide:</b><ul style="margin: 8px 0 0 16px;"><li><b>Anthropic</b> - Claude models</li><li><b>OpenAI</b> - GPT models</li><li><b>Google</b> - Gemini models</li></ul></div><p style="font-size: 13px; color: #6b7280;">One group can only have one platform</p></div>',
nextBtn: 'Next'
},
groupMultiplier: {
title: '💰 3. Rate Multiplier',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the billing multiplier to control user charges.</p><div style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚙️ Billing Rules:</b><ul style="margin: 8px 0 0 16px;"><li><b>1.0</b> - Original price (cost price)</li><li><b>1.5</b> - User consumes $1, charged $1.5</li><li><b>2.0</b> - User consumes $1, charged $2</li><li><b>0.8</b> - Subsidy mode (loss-making)</li></ul></div><p style="font-size: 13px; color: #6b7280;">Recommend setting test group to 1.0</p></div>',
nextBtn: 'Next'
},
groupExclusive: {
title: '🔒 4. Exclusive Group (Optional)',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Control group visibility and access permissions.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔐 Permission Guide:</b><ul style="margin: 8px 0 0 16px;"><li><b>Off</b> - Public group, visible to all users</li><li><b>On</b> - Exclusive group, only for specified users</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Cases:</b> VIP exclusive, internal testing, special customers</p></div>',
nextBtn: 'Next'
},
groupSubmit: {
title: '✅ Save Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Confirm the information and click create to save the group.</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Note:</b> Platform type cannot be changed after creation, but other settings can be edited anytime</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 Next Step:</b> After creation, we\'ll add upstream accounts to this group</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
},
accountManage: {
title: '🔗 Step 2: Add Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Great! Group created successfully 🎉</b></p><p style="margin-bottom: 12px;">Now add upstream AI service accounts to enable actual service delivery.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 Account Purpose:</b><ul style="margin: 8px 0 0 16px;"><li>Connect to upstream AI services (Claude, GPT, etc.)</li><li>One group can contain multiple accounts (load balancing)</li><li>Supports OAuth and Session Key methods</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Account Management" on the left sidebar</p></div>'
},
createAccount: {
title: ' Add New Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to start adding your first upstream account.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Recommend using OAuth method - more secure and no manual key extraction needed</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Add Account" button</p></div>'
},
accountName: {
title: '✏️ 1. Account Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set an easy-to-identify name for the account.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Naming Suggestions:</b> "Claude Main", "GPT Backup 1", "Test Account", etc.</p></div>',
nextBtn: 'Next'
},
accountPlatform: {
title: '🤖 2. Select Platform',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the service provider platform for this account.</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px;"><b>⚠️ Important:</b> Platform must match the group you just created</p></div>',
nextBtn: 'Next'
},
accountType: {
title: '🔐 3. Authorization Method',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the account authorization method.</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>✅ Recommended: OAuth Method</b><ul style="margin: 8px 0 0 16px;"><li>No manual key extraction needed</li><li>More secure with auto-refresh support</li><li>Works with Claude Code, ChatGPT OAuth</li></ul></div><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Session Key Method</b><ul style="margin: 8px 0 0 16px;"><li>Requires manual extraction from browser</li><li>May need periodic updates</li><li>For platforms without OAuth support</li></ul></div></div>',
nextBtn: 'Next'
},
accountPriority: {
title: '⚖️ 4. Priority (Optional)',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the account call priority.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 Priority Rules:</b><ul style="margin: 8px 0 0 16px;"><li>Lower number = higher priority</li><li>System uses low-value accounts first</li><li>Same priority = random selection</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Case:</b> Set main account to lower value, backup accounts to higher value</p></div>',
nextBtn: 'Next'
},
accountGroups: {
title: '🎯 5. Assign Groups',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Key Step!</b> Assign the account to the group you just created.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important Reminder:</b><ul style="margin: 8px 0 0 16px;"><li>Must select at least one group</li><li>Unassigned accounts cannot be used</li><li>One account can be assigned to multiple groups</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Select the test group you just created</p></div>',
nextBtn: 'Next'
},
accountSubmit: {
title: '✅ Save Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Confirm the information and click save.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 OAuth Flow:</b><ul style="margin: 8px 0 0 16px;"><li>Will redirect to service provider page after clicking save</li><li>Complete login and authorization on provider page</li><li>Auto-return after successful authorization</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 Next Step:</b> After adding account, we\'ll create an API key</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Save" button</p></div>'
},
keyManage: {
title: '🔑 Step 3: Generate Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Congratulations! Account setup complete 🎉</b></p><p style="margin-bottom: 12px;">Final step: generate an API Key to test if the service works properly.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 API Key Purpose:</b><ul style="margin: 8px 0 0 16px;"><li>Credential for calling AI services</li><li>Each key is bound to one group</li><li>Can set quota and expiration</li><li>Supports independent usage statistics</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "API Keys" on the left sidebar</p></div>'
},
createKey: {
title: ' Create Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to create your first API Key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Copy and save immediately after creation - key is only shown once</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create Key" button</p></div>'
},
keyName: {
title: '✏️ 1. Key Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set an easy-to-manage name for the key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Naming Suggestions:</b> "Test Key", "Production", "Mobile", etc.</p></div>',
nextBtn: 'Next'
},
keyGroup: {
title: '🎯 2. Select Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Select the group you just configured.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 Group Determines:</b><ul style="margin: 8px 0 0 16px;"><li>Which accounts this key can use</li><li>What billing multiplier applies</li><li>Whether it\'s an exclusive key</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Select the test group you just created</p></div>',
nextBtn: 'Next'
},
keySubmit: {
title: '🎉 Generate and Copy',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">System will generate a complete API Key after clicking create.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important Reminder:</b><ul style="margin: 8px 0 0 16px;"><li>Key is only shown once, copy immediately</li><li>Need to regenerate if lost</li><li>Keep it safe, don\'t share with others</li></ul></div><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🚀 Next Steps:</b><ul style="margin: 8px 0 0 16px;"><li>Copy the generated sk-xxx key</li><li>Use in any OpenAI-compatible client</li><li>Start experiencing AI services!</li></ul></div><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
}
},
// User tour steps
user: {
welcome: {
title: '👋 Welcome to Sub2API',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to the Sub2API AI service platform.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create API Key</li><li>📋 Copy key to your application</li><li>🚀 Start using AI services</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>',
nextBtn: 'Start 🚀',
prevBtn: 'Skip'
},
keyManage: {
title: '🔑 API Key Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Manage all your API access keys here.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 What is an API Key?</b><br/>An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click to enter key page</p></div>'
},
createKey: {
title: ' Create New Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to create your first API key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Key is only shown once after creation, make sure to copy and save</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create Key"</p></div>'
},
keyName: {
title: '✏️ Key Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Give your key an easy-to-identify name.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Examples:</b> "My First Key", "For Testing", etc.</p></div>',
nextBtn: 'Next'
},
keyGroup: {
title: '🎯 Select Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Select the service group assigned by the administrator.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Group Info:</b><br/>Different groups may have different service quality and billing rates, choose according to your needs.</p></div>',
nextBtn: 'Next'
},
keySubmit: {
title: '🎉 Complete Creation',
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>'
}
}
},
// Payment System
payment: {
title: 'Recharge / Subscription',
amountLabel: 'Amount',
paymentAmount: 'Payment Amount',
creditedBalance: 'Credited Balance',
quickAmounts: 'Quick Amounts',
customAmount: 'Custom Amount',
enterAmount: 'Enter amount',
paymentMethod: 'Payment Method',
fee: 'Fee',
actualPay: 'Actual Payment',
createOrder: 'Confirm Payment',
methods: {
easypay: 'EasyPay',
alipay: 'Alipay',
wxpay: 'WeChat Pay',
stripe: 'Stripe',
airwallex: 'Airwallex',
card: 'Card',
link: 'Link',
alipay_direct: 'Alipay (Direct)',
wxpay_direct: 'WeChat Pay (Direct)',
},
status: {
pending: 'Pending',
paid: 'Paid',
recharging: 'Recharging',
completed: 'Completed',
expired: 'Expired',
cancelled: 'Cancelled',
failed: 'Failed',
refund_requested: 'Refund Requested',
refunding: 'Refunding',
refund_pending: 'Refund Pending',
refunded: 'Refunded',
partially_refunded: 'Partially Refunded',
refund_failed: 'Refund Failed',
},
qr: {
scanToPay: 'Scan to Pay',
scanAlipay: 'Alipay QR Payment',
scanWxpay: 'WeChat QR Payment',
scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay',
scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay',
payInNewWindow: 'Complete Payment in New Window',
payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.',
openPayWindow: 'Reopen Payment Page',
expiresIn: 'Expires in',
expired: 'Order Expired',
expiredDesc: 'This order has expired. Please create a new one.',
cancelled: 'Order Cancelled',
cancelledDesc: 'You have cancelled this payment.',
waitingPayment: 'Waiting for payment...',
cancelOrder: 'Cancel Order',
alipayOpening: 'Opening Alipay',
alipayContinueInApp: 'Complete payment in Alipay',
alipayWaitingHint: 'The server will confirm the payment and update this page automatically',
alipayFallbackTitle: 'Alipay did not open',
alipayFallbackHint: 'Try opening Alipay again, or save the QR code and scan it from your Alipay photo album',
reopenAlipay: 'Open Alipay Again',
saveQRCode: 'Save QR Code',
alipaySaveAndScanHint: 'Save the QR code, open Alipay Scan, then select it from your photo album',
},
orders: {
title: 'My Orders',
empty: 'No orders yet',
orderId: 'Order ID',
orderNo: 'Order No.',
amount: 'Amount',
payAmount: 'Paid',
creditedAmount: 'Credited Amount',
fee: 'Fee',
baseAmount: 'Base Amount',
includedInPayAmount: 'included in paid amount',
status: 'Status',
paymentMethod: 'Payment Method',
createdAt: 'Created',
cancel: 'Cancel Order',
userId: 'User ID',
orderType: 'Order Type',
actions: 'Actions',
requestRefund: 'Request Refund',
},
result: {
success: 'Payment Successful',
subscriptionSuccess: 'Subscription Successful',
processing: 'Payment Processing',
processingHint: 'Payment confirmation is still pending. This page will refresh automatically.',
failed: 'Payment Failed',
backToRecharge: 'Back to Recharge',
viewOrders: 'View Orders',
},
currentBalance: 'Current Balance',
groupFallback: 'Group #{id}',
rechargeAccount: 'Recharge Account',
activeSubscription: 'Active Subscription',
noActiveSubscription: 'No active subscription',
tabTopUp: 'Top Up',
tabSubscribe: 'Subscribe',
noPlans: 'No subscription plans available',
notAvailable: 'Top-up is currently unavailable',
confirmSubscription: 'Confirm Subscription',
confirmCancel: 'Are you sure you want to cancel this order?',
amountTooLow: 'Minimum amount is {min}',
amountTooHigh: 'Maximum amount is {max}',
amountNoMethod: 'No payment method available for this amount',
rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD',
refundReason: 'Refund Reason',
refundReasonPlaceholder: 'Please describe your refund reason',
stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.',
stripeMissingParams: 'Missing order ID or client secret',
stripeNotConfigured: 'Stripe is not configured',
airwallexLoadFailed: 'Failed to load Airwallex payment component. Please refresh and try again.',
airwallexMissingParams: 'Missing Airwallex payment parameters',
errors: {
tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.',
cancelRateLimited: 'Too many cancellations. Please try again later.',
wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.',
wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.',
wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.',
wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.',
wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.',
wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.',
wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.',
wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.',
mobilePaymentFallbackToQr: 'This merchant has not enabled mobile payment. The flow has been switched to QR payment automatically.',
alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.',
alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.',
alipayMobileUnavailable: 'This page could not hand off to Alipay.',
alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.',
// Structured error codes (reason strings from backend ApplicationError)
PAYMENT_DISABLED: 'Payment system is disabled.',
USER_INACTIVE: 'Your account is disabled.',
BALANCE_PAYMENT_DISABLED: 'Balance recharge has been disabled.',
INVALID_AMOUNT: 'Invalid amount.',
INVALID_INPUT: 'Invalid request.',
PLAN_NOT_AVAILABLE: 'Plan not found or no longer available.',
GROUP_NOT_FOUND: 'Subscription group is no longer available.',
GROUP_TYPE_MISMATCH: 'Group is not a subscription type.',
TOO_MANY_PENDING: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.',
DAILY_LIMIT_EXCEEDED: 'Daily recharge limit reached. Remaining: {remaining}.',
PAYMENT_GATEWAY_ERROR: 'Payment method is unavailable.',
NO_AVAILABLE_INSTANCE: 'No payment channel available right now.',
PAYMENT_PROVIDER_MISCONFIGURED: 'Payment provider misconfigured. Please contact an administrator.',
WXPAY_CONFIG_MISSING_KEY: 'WeChat Pay config missing required key: {key}.',
WXPAY_CONFIG_INVALID_KEY_LENGTH: 'WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).',
WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} is malformed. Make sure you copied the full PEM content.',
PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.',
PAYMENT_PROVIDER_CONFLICT: 'Another enabled provider instance is already serving this payment method. Disable it before continuing.',
CANCEL_RATE_LIMITED: 'Too many cancellations. Please try again later.',
NOT_FOUND: 'Order not found.',
FORBIDDEN: 'No permission for this order.',
CONFLICT: 'Order status has changed. Please refresh.',
INVALID_ORDER_TYPE: 'Only balance orders can request a refund.',
INVALID_STATUS: 'The current order status does not allow this operation.',
BALANCE_NOT_ENOUGH: 'Refund amount exceeds balance.',
REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.',
REFUND_FAILED: 'Refund failed.',
},
airwallexPay: 'Airwallex Payment',
stripePay: 'Pay Now',
stripeSuccessProcessing: 'Payment successful, processing your order...',
stripePopup: {
redirecting: 'Redirecting to payment page...',
loadingQr: 'Loading WeChat Pay QR code...',
timeout: 'Timed out waiting for payment credentials, please retry',
qrFailed: 'Failed to get WeChat Pay QR code',
},
subscribeNow: 'Subscribe Now',
renewNow: 'Renew',
selectPlan: 'Select Plan',
planFeatures: 'Features',
planCard: {
rate: 'Rate',
peakRate: 'Peak Rate',
dailyLimit: 'Daily',
weeklyLimit: 'Weekly',
monthlyLimit: 'Monthly',
quota: 'Quota',
unlimited: 'Unlimited',
models: 'Models',
},
days: 'days',
weeks: 'weeks',
months: 'months',
years: 'years',
oneMonth: '1 Month',
oneYear: '1 Year',
perMonth: 'month',
perYear: 'year',
admin: {
tabs: {
overview: 'Overview',
orders: 'Orders',
channels: 'Channels',
plans: 'Plans',
},
todayRevenue: 'Today Revenue',
totalRevenue: 'Total Revenue',
todayOrders: 'Today Orders',
orderCount: 'Order Count',
avgAmount: 'Average Amount',
revenue: 'Revenue',
dailyRevenue: 'Daily Revenue',
paymentDistribution: 'Payment Distribution',
colUser: 'User',
topUsers: 'Top Users',
noData: 'No data',
days: 'days',
weeks: 'weeks',
months: 'months',
searchOrders: 'Search orders...',
allStatuses: 'All Statuses',
allPaymentTypes: 'All Payment Types',
allOrderTypes: 'All Order Types',
orderDetail: 'Order Detail',
orderType: 'Order Type',
orders: 'Orders',
balanceOrder: 'Balance Top-Up',
subscriptionOrder: 'Subscription',
paidAt: 'Paid At',
completedAt: 'Completed At',
expiresAt: 'Expires At',
feeRate: 'Fee Rate',
refund: 'Refund',
refundOrder: 'Refund Order',
refundAmount: 'Refund Amount',
maxRefundable: 'Max Refundable',
refundReason: 'Refund Reason',
refundReasonPlaceholder: 'Please enter refund reason',
confirmRefund: 'Confirm Refund',
refundSuccess: 'Refund successful',
refundPending: 'Refund pending gateway confirmation',
queryRefundStatus: 'Query refund status',
refundInfo: 'Refund Info',
refundEnabled: 'Refund Enabled',
allowUserRefund: 'Allow User Refund',
alreadyRefunded: 'Already Refunded',
deductBalance: 'Deduct Balance',
deductBalanceHint: 'Subtract recharged amount from user balance',
userBalance: 'User Balance',
orderAmount: 'Order Amount',
insufficientBalance: 'Insufficient balance — will deduct to $0',
noDeduction: 'Will NOT deduct user balance',
forceRefund: 'Force refund (ignore balance check)',
orderCancelled: 'Order Cancelled',
retry: 'Retry',
retrySuccess: 'Retry successful',
approveRefund: 'Approve Refund',
retryRefund: 'Retry Refund',
refundRequestInfo: 'Refund Request Info',
refundRequestedAt: 'Requested At',
refundRequestedBy: 'Requested By',
refundRequestReason: 'Request Reason',
auditLogs: 'Audit Logs',
operator: 'Operator',
channelName: 'Channel Name',
channelDescription: 'Channel Description',
createChannel: 'Create Channel',
editChannel: 'Edit Channel',
deleteChannel: 'Delete Channel',
deleteChannelConfirm: 'Are you sure you want to delete this channel?',
planName: 'Plan Name',
planDescription: 'Plan Description',
createPlan: 'Create Plan',
editPlan: 'Edit Plan',
deletePlan: 'Delete Plan',
deletePlanConfirm: 'Are you sure you want to delete this plan?',
originalPrice: 'Original Price',
price: 'Price',
currency: 'Currency Label',
currencyPlaceholder: 'e.g. USD / NZD / CNY',
currencyHint: 'Display-only 3-letter ISO currency code shown next to the price; leave empty to hide, does not affect billing',
subscriptionCnyPayPreview: 'CNY channel charge preview: {amount}',
subscriptionCnyPayPreviewWithFee: '({feeRate}% fee included: {total})',
validity: 'Validity',
validityUnit: 'Validity Unit',
sortOrder: 'Sort Order',
forSale: 'For Sale',
onSale: 'On Sale',
offSale: 'Off Sale',
group: 'Group',
groupId: 'Group ID',
features: 'Features',
featuresHint: 'One feature per line',
featuresPlaceholder: 'Enter plan features...',
providerManagement: 'Provider Management',
providerManagementDesc: 'Manage payment provider instances',
createProvider: 'Create Provider',
editProvider: 'Edit Provider',
deleteProvider: 'Delete Provider',
deleteProviderConfirm: 'Are you sure you want to delete this provider?',
providerName: 'Provider Name',
providerKey: 'Provider Key',
selectProviderKey: 'Select Provider Key',
providerConfig: 'Provider Config',
noProviders: 'No providers configured',
noProvidersHint: 'Create a provider instance to start accepting payments',
supportedTypes: 'Supported Payment Types',
supportedTypesHint: 'Select the payment types this provider supports',
rateMultiplier: 'Rate Multiplier',
dashboardTitle: 'Payment Dashboard',
dashboardDesc: 'Recharge order analytics and insights',
daySuffix: 'd',
paymentConfigTitle: 'Payment Config',
paymentConfigDesc: 'Configure payment providers and settings',
plansPageTitle: 'Subscription Plans',
plansPageDesc: 'Manage subscription plan configuration',
tabPlanConfig: 'Plan Configuration',
tabUserSubs: 'User Subscriptions',
selectGroup: 'Select a group',
groupRequired: 'Please select a subscription group',
priceRequired: 'Price must be greater than 0',
validityRequired: 'Validity must be greater than 0',
groupMissing: 'Missing',
groupInfo: 'Group Info',
platform: 'Platform',
rateMultiplierLabel: 'Rate',
dailyLimit: 'Daily Limit',
weeklyLimit: 'Weekly Limit',
monthlyLimit: 'Monthly Limit',
unlimited: 'Unlimited',
searchUserSubs: 'Search user subscriptions...',
daily: 'D',
weekly: 'W',
monthly: 'M',
subsStatus: {
active: 'Active',
expired: 'Expired',
revoked: 'Revoked',
},
},
},
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
export default {
audit: {
title: '操作日志',
description: '记录管理员与用户的管理面操作,请求头凭证仅保留首尾、请求体已脱敏。日志无法单条删除,全量清理需二次验证。',
clearAll: '全部清理',
empty: '暂无操作日志',
loadFailed: '加载操作日志失败',
filters: {
all: '全部',
q: '关键字',
qPlaceholder: '路径 / 动作 / 操作者邮箱',
actorEmail: '操作者邮箱',
action: '动作',
clientIp: '客户端 IP',
method: '请求方法',
authMethod: '认证方式',
result: '结果',
resultSuccess: '成功',
resultFailure: '失败',
startTime: '开始时间',
endTime: '结束时间'
},
columns: {
time: '时间',
actor: '操作者',
action: '动作',
method: '方法',
result: '结果',
clientIp: '客户端 IP',
detail: '详情'
},
detail: {
title: '操作日志详情',
actorRole: '角色',
methodPath: '方法 / 路径',
latency: '耗时',
requestId: '请求 ID',
credential: '凭证(掩码)',
userAgent: 'User-Agent',
requestBody: '请求体(已脱敏)',
extra: '附加信息'
},
clearConfirm: {
title: '清理全部操作日志',
message: '此操作将永久删除所有操作日志,且不可恢复。清理动作本身会被留痕记录。确定继续吗?',
totpTitle: '输入二次验证码',
totpHint: '清理操作日志需要现场验证 TOTP 验证码。',
success: '已清理 {count} 条操作日志',
failed: '清理操作日志失败'
}
}
}
@@ -0,0 +1,764 @@
export default {
availableChannels: {
title: '可用渠道',
description: '按渠道聚合查看关联分组与支持模型(已展开通配符)',
searchPlaceholder: '搜索渠道或模型...',
columns: {
name: '渠道名',
status: '状态',
billingSource: '计费模型来源',
groups: '关联分组',
supportedModels: '支持模型'
},
empty: '暂无数据',
noGroups: '未关联分组',
noModels: '未配置模型映射',
noPricing: '未配置定价',
statusActive: '启用',
statusDisabled: '停用',
billingSource: {
requested: '请求模型',
upstream: '上游模型',
channel_mapped: '映射后模型'
},
pricing: {
billingMode: '计费模式',
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
billingModeVideo: '按视频',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
cacheReadPrice: '缓存读取',
imageOutputPrice: '图片输出',
perRequestPrice: '每次请求',
intervals: '阶梯定价',
unitPerMillion: '/ 1M token',
unitPerRequest: '/ 次'
}
},
// Channel Management
channels: {
title: '渠道管理',
description: '管理渠道和自定义模型定价',
searchChannels: '搜索渠道...',
createChannel: '创建渠道',
editChannel: '编辑渠道',
deleteChannel: '删除渠道',
statusActive: '启用',
statusDisabled: '停用',
allStatus: '全部状态',
groupsUnit: '个分组',
pricingUnit: '条定价',
noChannelsYet: '暂无渠道',
createFirstChannel: '创建第一个渠道来管理模型定价',
loadError: '加载渠道列表失败',
createSuccess: '渠道创建成功',
updateSuccess: '渠道更新成功',
deleteSuccess: '渠道删除成功',
createError: '创建渠道失败',
updateError: '更新渠道失败',
deleteError: '删除渠道失败',
nameRequired: '请输入渠道名称',
duplicateModels: '模型「{0}」在多个定价条目中重复',
modelConflict: "模型模式 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。模型名称按大小写不敏感匹配,已有条目已覆盖其所有大小写变体,无需重复添加。",
mappingConflict: "模型映射源 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。源模式按大小写不敏感匹配,已有条目已覆盖其所有大小写变体。",
intervalValidation: {
negativeMin: '区间 #{index}:最小 token 数({value})不能为负数',
maxPositive: '区间 #{index}:最大 token 数({value})必须大于 0',
maxGreaterThanMin: '区间 #{index}:最大 token 数({max})必须大于最小 token 数({min}',
negativePrice: '区间 #{index}{field}不能为负数',
multiplierPositive: '区间 #{index}{field}必须大于 0',
unboundedLast: '区间 #{index}:无上限区间(最大 token 数为空)必须放在最后',
overlap: '区间 #{previousIndex} 和 #{currentIndex} 重叠:前一个上界({previousMax})大于当前下界({currentMin}',
price: {
inputPrice: '输入价格',
outputPrice: '输出价格',
cacheWritePrice: '缓存写入价格',
cacheReadPrice: '缓存读取价格',
perRequestPrice: '单次价格'
}
},
timePricingValidation: {
timezone: '请选择有效的 IANA 时区',
format: '开始时间和结束时间必须使用 HH:mm:ss 格式',
range: '开始时间必须早于结束时间;跨午夜请拆分为两个时间段',
overlap: '时间段不能重叠',
multiplier: '倍率必须大于 0,且最多保留两位小数'
},
deleteConfirm: '确定要删除渠道「{name}」吗?此操作不可撤销。',
columns: {
name: '名称',
description: '描述',
status: '状态',
groups: '分组',
pricing: '定价',
createdAt: '创建时间',
actions: '操作'
},
billingMode: {
token: 'Token',
perRequest: '按次',
image: '图片(按次)',
video: '视频(按秒)'
},
form: {
name: '名称',
namePlaceholder: '输入渠道名称',
description: '描述',
descriptionPlaceholder: '可选描述',
status: '状态',
groups: '关联分组',
noGroupsAvailable: '暂无可用分组',
inOtherChannel: '已属于「{name}」',
modelPricing: '模型定价',
models: '模型列表',
modelsPlaceholder: '输入完整模型名后按回车添加',
modelInputHint: '按回车添加,支持粘贴批量导入',
billingMode: '计费模式',
defaultPrices: '默认价格(未命中区间时使用)',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
cacheReadPrice: '缓存读取',
cacheWritePriceShort: '缓存写',
cacheReadPriceShort: '缓存读',
imageInputPrice: '图片输入',
imageTokenPrice: '图片输出',
imageOutputPrice: '图片输出价格',
pricePlaceholder: '默认',
fastMultiplier: 'Fast 倍率',
flexMultiplier: 'Flex 倍率',
multiplierPlaceholder: '未配置',
multiplierPositive: 'Fast/Flex 倍率必须大于 0',
inputMultiplier: '输入倍率',
outputMultiplier: '输出倍率',
cacheWriteMultiplier: '缓存写倍率',
cacheReadMultiplier: '缓存读倍率',
intervals: '上下文区间定价(可选)',
timePricing: '时间段定价(可选)',
timezone: '时区',
addTimePeriod: '添加时间段',
startTime: '开始时间',
endTime: '结束时间',
multiplier: '倍率',
removeTimePeriod: '删除时间段',
minTokens: '最小',
maxTokens: '最大',
inclusive: '(含)',
addInterval: '添加区间',
requestTiers: '按次计费层级',
imageTiers: '图片计费层级(按次)',
videoTiers: '视频分辨率层级(按秒)',
addTier: '添加层级',
noTiersYet: '暂无层级,点击添加配置按次计费价格',
noPricingRules: '暂无定价规则,点击"添加"创建',
perRequestPrice: '单次价格',
perRequestPriceRequired: '按次/图片计费模式必须设置默认价格或至少一个计费层级',
tierLabel: '层级',
resolution: '分辨率',
modelMapping: '模型映射',
modelMappingHint: '将请求中的模型名映射为实际模型名。在账号级别映射之前执行。',
noMappingRules: '暂无映射规则,点击"添加"创建',
mappingSource: '源模型',
mappingTarget: '目标模型',
billingModelSource: '计费基准',
billingModelSourceChannelMapped: '以渠道映射后的模型计费',
billingModelSourceRequested: '以请求模型计费',
billingModelSourceUpstream: '以最终模型计费',
billingModelSourceResponse: '按上游响应模型计费',
billingModelSourceHint: '控制使用哪个模型名称进行定价查找',
selectedCount: '已选 {count} 个',
searchGroups: '搜索分组...',
noGroupsMatch: '没有匹配的分组',
restrictModels: '限制模型',
restrictModelsHint: '开启后,仅允许模型定价列表中的模型。不在列表中的模型请求将被拒绝。',
defaultPerRequestPrice: '默认单次价格(未命中层级时使用)',
defaultImagePrice: '默认图片价格(未命中层级时使用)',
defaultVideoPrice: '默认视频每秒价格(未命中层级时使用)',
platformConfig: '平台配置',
webSearchEmulation: 'Web Search 模拟',
webSearchEmulationHint: '⚠️ 开启后该渠道下所有 Anthropic 分组的账号将自动拦截 web_search 请求,请谨慎操作',
webSearchEmulationGlobalDisabled: '请先在系统设置 → 网关 → Web Search 模拟中启用全局开关',
codexImageGenerationBridge: 'Codex 图片生成桥接',
codexImageGenerationBridgeHint: '开启后,OpenAI 分组仅会为非 Responses Lite 的 Codex /responses 文本请求自动注入 hosted image_generation 工具。桥接不会为 Responses Lite 注入工具;本地 image_gen 的处理由客户端和账号策略决定。仅在路由账号支持图片生成时开启。',
bedrockCCCompat: 'Bedrock CC 兼容',
bedrockCCCompatHint: '⚠️ 开启后,该渠道下 Bedrock 账号的请求将进行 Claude Code 兼容处理(thinking 类型转换、tool_use ID 清理)',
basicSettings: '基础设置',
addPlatform: '添加平台',
noPlatforms: '点击"添加平台"开始配置渠道',
mappingCount: '条映射',
pricingEntry: '定价配置',
noModels: '未添加模型',
applyPricingToAccountStats: '应用模型定价到账号统计',
applyPricingToAccountStatsDesc: '启用后,未被自定义规则匹配的请求将使用模型定价文件中的标准价格计算账号统计费用',
accountStatsPricingRules: '自定义账号统计定价规则',
addRule: '添加规则',
noRulesConfigured: '未配置自定义规则,将使用上方的模型定价。',
ruleName: '规则名称(可选)',
ruleGroups: '分组',
ruleAccounts: '账号',
searchAccountPlaceholder: '搜索账号...',
ruleAccountsHint: '留空表示匹配所有账号',
ruleModelPricing: '模型定价',
noGroupsInChannel: '上方平台标签页中未选择分组',
unnamed: '未命名',
syncLatestModels: '同步最新模型',
syncingModels: '同步中...',
syncModelsSuccess: '已同步 {count} 个新模型',
syncModelsAlreadyUpToDate: '模型列表已是最新',
syncModelsError: '同步模型失败'
}
},
riskControl: {
title: '风控中心',
description: '配置内容审计策略并查看审核记录',
loadFailed: '加载风控中心失败',
saveFailed: '保存内容审计配置失败',
logsFailed: '加载审核记录失败',
saved: '内容审计配置已保存',
refresh: '刷新',
config: '内容审计配置',
configHint: '调用 OpenAI Moderations 进行请求内容评分,命中阈值后按模式处理。',
openSettings: '内容审计设置',
settingsTitle: '内容审计设置',
refreshStatus: '刷新状态',
records: '审核记录',
recordsHint: '展示命中、拦截、异常和已采样记录。',
saveConfig: '保存内容审计配置',
statusFailed: '加载运行状态失败',
enabled: '开启内容审计',
enabledHint: '关闭后即使风控中心菜单启用,也不会审核网关请求。',
mode: '全局模式',
modePreBlock: '前置拦截',
modePreBlockDesc: '每次请求先同步审核最新用户输入,命中后立即拒绝请求。',
modeObserve: '仅观察',
modeObserveDesc: '请求直接放行,最新用户输入进入异步审核队列;命中后只记录、通知和按规则累计。',
modeOff: '关闭',
modeOffDesc: '不执行内容审计,也不会写入审核记录。',
baseUrl: 'OpenAI Base URL',
model: '模型名',
apiKey: 'OpenAI API Key',
apiKeys: 'OpenAI API Keys',
apiKeyCount: '{count} 个 Key',
apiKeyPlaceholder: '请输入 API Key',
apiKeysPlaceholder: '新增 API Key,每行一个;保存后会追加到已保存 Key',
apiKeysPlaceholderReplace: '覆盖保存 API Key,每行一个;保存后会替换全部已保存 Key',
apiKeysPlaceholderKeep: '新增 API Key,每行一个;保存后会追加到已保存 Key',
apiKeysHint: '当前已保存 {count} 个 Key;输入区只用于新增,保存时会增量追加并自动去重。',
apiKeysWriteMode: '写入方式',
apiKeysModeAppend: '增量添加',
apiKeysModeReplace: '覆盖保存',
apiKeysModeAppendHint: '默认模式:保存时追加输入区 Key,并保留已保存 Key。',
apiKeysModeReplaceHint: '覆盖模式:保存时用输入区 Key 替换全部已保存 Key。',
apiKeysReplaceWarning: '覆盖模式',
apiKeysReplaceNoInput: '覆盖保存至少需要输入 1 个 API Key',
apiKeyPlaceholderKeep: '留空保持不变',
apiKeyWillClear: '保存后清除已配置 Key',
apiKeyConfigured: '已配置',
apiKeyTemporary: '待保存',
apiKeyPendingDelete: '待删除',
apiKeyPendingDeleteCount: '待删除 {count} 个 Key',
deleteApiKey: '删除这个 Key',
undoDeleteApiKey: '撤销删除',
inputApiKeyCount: '输入区 {count} 个 Key',
storedApiKeyCount: '已保存 {count} 个 Key',
testInputApiKeys: '测试输入区 Key',
testStoredApiKeys: '测试已保存 Key',
testContentWithStoredApiKey: '用已保存 Key 试跑内容',
testingApiKeys: '测试中',
apiKeyTestNoInput: '请先输入需要测试的 OpenAI API Key',
apiKeyTestDone: 'Key 测试完成,共 {count} 个',
apiKeyTestFailed: '测试 OpenAI API Key 失败',
apiKeyHealth: 'Key 可用状态',
apiKeyFreezeRule: '400 不冻结;401/403 冻结 10 分钟;429/529 冻结 1 分钟;其他 HTTP 错误冻结 10 秒。',
apiKeyRows: '{count} 个 Key',
apiKeyRowsCollapsed: '已隐藏 {count} 个 Key',
apiKeyRowsExpanded: '正在显示全部 {count} 个 Key',
expandApiKeyRows: '展开',
collapseApiKeyRows: '收起',
apiKeyHealthEmpty: '暂无 Key 状态',
apiKeyHealthEmptyHint: '保存 Key 或测试输入区 Key 后会显示可用性。',
apiKeyStatusOk: '可用',
apiKeyStatusError: '异常',
apiKeyStatusFrozen: '冻结',
apiKeyStatusUnknown: '未测试',
apiKeyFailureCount: '失败 {count} 次',
apiKeyLatency: '{ms} ms',
apiKeyHTTPStatus: 'HTTP {status}',
apiKeyFrozenUntil: '冻结至 {time}',
apiKeyLastChecked: '检查于 {time}',
apiKeyNotTested: '尚未测试',
auditTestInput: '审计试跑输入',
auditTestInputHint: '可填写提示词并上传或粘贴图片;图片以 base64 发送,不会保存文件。',
auditTestPromptPlaceholder: '输入要测试的用户提示词;留空时仅测试 Key 可用性。',
auditTestImages: '测试图片',
auditTestImagesHint: '支持上传、拖拽或粘贴图片,最多 1 张,每张不超过 8MB。',
addAuditTestImage: '添加图片',
clearAuditTest: '清空试跑',
auditTestImageLimit: '最多只能添加 {count} 张测试图片',
auditTestImageTooLarge: '单张测试图片不能超过 8MB',
auditTestImageReadFailed: '读取测试图片失败',
auditTestResult: '审计试跑结果',
auditTestHighest: '最高分类 {category},分数 {score}',
auditTestComposite: '综合评分',
auditTestFlagged: '命中阈值',
auditTestPassed: '未命中',
notConfigured: '未配置',
clearApiKey: '清除已保存 Key',
keepApiKey: '保留已保存 Key',
timeoutMs: 'HTTP 超时 (ms)',
retryCount: '失败重试次数',
sampleRate: '采样率',
proxy: '代理服务器',
proxyHint: '审计请求经指定代理(IP管理-代理服务器)发出,适用于出口 IP 不受 OpenAI 支持的部署;默认直连。',
recordNonHits: '记录未命中输入',
recordNonHitsHint: '开启后会记录抽样但未命中的请求摘要,摘要会先脱敏再入库。',
preHashCheck: '启用前置哈希比对',
preHashCheckHint: '异步审核命中过的输入哈希会被前置拦截;该拦截不发送邮件,也不累计封禁次数。',
flaggedHashCount: '当前哈希集合数量:{count} 个',
flaggedHashHint: '哈希永久保存在 Redis 集合中;可粘贴完整 64 位哈希删除误拦截项,或一键清空全部风险哈希。',
flaggedHashPlaceholder: '粘贴完整 64 位输入哈希',
deleteFlaggedHash: '删除指定哈希',
clearFlaggedHashes: '一键清空',
clearFlaggedHashesConfirm: '确定要清空全部风险输入哈希吗?此操作不会删除审核记录,但会取消所有历史哈希拦截。',
flaggedHashDeleted: '风险哈希已删除',
flaggedHashNotFound: '该风险哈希不存在',
flaggedHashDeleteFailed: '删除风险哈希失败',
flaggedHashesCleared: '已清空 {count} 个风险哈希',
flaggedHashesClearFailed: '清空风险哈希失败',
workerCount: 'Worker 数',
queueSize: '异步队列大小',
blockStatus: '拦截 HTTP 状态码',
blockMessage: '自定义拦截提示',
defaultBlockMessage: '内容审计命中风险规则,请调整输入后重试',
emailOnHit: '命中后发送邮件',
emailOnHitHint: '开启后每次达到阈值都会向用户发送风控提醒邮件;自动封禁通知始终发送。',
autoBan: '自动封禁用户',
autoBanHint: '命中次数达到阈值后将禁用用户账号、刷新认证缓存并发送封禁通知邮件。',
cyberPolicyExcludeBan: 'cyber_policy 不计入封号次数',
cyberPolicyExcludeBanHint: '开启后,cyber_policy 拦截不再计入自动封号的违规次数:当次不判定封号,历史累计亦排除。风控日志与通知邮件照常。',
violationNotCounted: '未计入封号',
banThreshold: '封禁触发次数',
violationWindowHours: '累计窗口(小时)',
hitRetentionDays: '命中记录保留(天)',
nonHitRetentionDays: '未命中记录保留(天,最多 3 天)',
violationCount: '{count} 次',
emailSent: '已发邮件',
emailNotSent: '未发邮件',
autoBanned: '已封禁',
unbanUser: '解封',
unbanSuccess: '用户已解封',
unbanFailed: '解封用户失败',
inputDetailTitle: '输入摘要详情',
inputDetailContent: '完整内容',
matchedKeyword: '命中关键词',
queueDelay: '排队 {ms} ms',
allGroups: '全部分组',
allGroupsHint: '当前审计全部分组',
selectedGroupsHint: '当前审计指定分组',
groupScope: '审计分组',
groupScopeHint: '开启右侧开关表示全部分组,关闭后选择指定分组。',
selectedGroups: '指定分组',
searchGroups: '搜索分组名称或平台',
noGroups: '暂无可用分组',
modelFilter: '模型范围',
modelFilterHint: '按客户端请求的模型名决定是否执行内容审计,模型映射后仍以请求模型判断。',
modelFilterAll: '所有模型',
modelFilterAllDesc: '所有模型请求都会进入内容审计。',
modelFilterInclude: '仅指定模型',
modelFilterIncludeDesc: '只有列表中的模型会执行内容审计。',
modelFilterExclude: '排除指定模型',
modelFilterExcludeDesc: '列表中的模型跳过内容审计,其余模型执行审计。',
modelFilterModels: '模型列表',
modelFilterModelCount: '已配置 {count} 个模型',
modelFilterModelsRequired: '当前模型范围至少需要配置 1 个模型',
modelFilterAllSummary: '全部模型生效',
modelFilterIncludeSummary: '仅 {count} 个模型生效',
modelFilterExcludeSummary: '排除 {count} 个模型',
emptyLogs: '暂无审核记录',
preBlockSyncStatus: '前置拦截同步状态',
preBlockSyncHint: '同步审核链路的实时计数,不包含异步写记录任务。',
preBlockActive: '同步处理中',
preBlockActiveHint: '当前正在审核',
preBlockChecked: '已检查',
preBlockCheckedHint: '进入前置拦截链路',
preBlockAllowed: '已放行',
preBlockAllowedHint: '未触发拦截',
preBlockBlocked: '已拦截',
preBlockBlockedHint: '命中后拒绝请求',
preBlockErrors: '审核异常',
preBlockErrorsHint: '失败或无可用 Key',
preBlockAvgLatency: '平均耗时',
preBlockAvgLatencyHint: '同步链路平均值',
preBlockAPIKeyLoad: '审核 Key 负载',
preBlockAPIKeyLoadHint: '同步前置拦截直接轮询可用审核 Key。',
preBlockAPIKeyLoadSummary: '同步并发 {active} / 可用 Key {available},累计 {total} 次,worker{workerActive} / {workerTotal}',
preBlockAPIKeyTotals: '累计 {total},成功 {success},异常 {errors}',
preBlockAPIKeyLoadEmpty: '暂无审核 Key 负载数据',
preBlockKeyActiveShort: '并发',
preBlockKeyTotalShort: '累计',
preBlockKeyAvgShort: '平均',
preBlockKeyLastShort: '最近',
workerStatus: 'Worker 运行状态',
workerStatusHint: '异步审计任务和前置拦截记录任务的队列与 Worker 池状态,不包含同步前置拦截审核请求。',
workerPool: 'Worker 池',
workerPoolMeta: '{active} 个处理中,{idle} 个空闲可用,共 {total} 个',
queueUsage: '队列占用',
activeWorkers: '处理中',
idleWorkers: '空闲可用',
workerActive: '正在处理异步审计或记录任务',
workerIdle: '已启动,当前空闲可用',
workerDisabled: '风控或内容审计未启用',
processed: '已处理',
droppedErrors: '丢弃/异常',
autoRefresh: '每 15 秒自动刷新',
lastCleanup: '上次清理:{time}',
cleanupStats: '上次清理删除命中 {hit} 条,未命中 {nonHit} 条',
riskSwitchOff: '系统开关关闭',
riskThresholds: '风险阈值',
riskThresholdsHint: '按 OpenAI Moderations 分类调整命中阈值,分数达到或超过阈值即视为命中。',
riskThresholdDefault: '默认 {value}',
riskThresholdReset: '恢复默认阈值',
riskThresholdPercent: '阈值百分比',
tabs: {
basic: '基础',
scope: '审计范围',
runtime: '运行队列',
response: '命中通知',
riskThresholds: '风险阈值',
keywords: '关键词拦截',
retention: '日志保留',
},
blockedKeywords: '拦截关键词',
blockedKeywordsPlaceholder: '每行输入一个关键词,例如:\n敏感词1\n敏感词2',
blockedKeywordsDescription: '匹配忽略大小写;命中后会按下方策略决定是否调用上游审计接口。',
blockedKeywordsPreBlockHint: '关键词拦截仅在「前置拦截」模式下生效。',
blockedKeywordsModeWarning: '当前为「{mode}」模式,关键词拦截不会生效;请切换到「前置拦截」模式后再保存关键词。',
blockedKeywordCount: '已配置 {count} 个关键词',
blockedKeywordsLimit: '最多保存 {max} 个关键词,单个长度不超过 200 个字符;重复项会自动去重。',
keywordBlockingMode: '审计策略',
keywordModeKeywordAndApi: '关键词 + API',
keywordModeKeywordAndApiDesc: '命中关键词直接拦截;未命中时再调用上游审计接口。',
keywordModeKeywordOnly: '仅关键词',
keywordModeKeywordOnlyDesc: '只用关键词判断,未命中即放行,不调用上游审计接口,可显著降低 API 用量。',
keywordModeKeywordOnlyNotice: '当前为「仅关键词」策略:未命中关键词的请求将直接放行,不调用上游审计接口。',
keywordModeApiOnly: '仅 API',
keywordModeApiOnlyDesc: '只调用上游审计接口判断,本页的关键词列表将不会生效。',
keywordModeApiOnlyNotice: '当前为「仅 API」策略:关键词列表不会生效,请求会全部交给上游审计接口判断。',
overview: {
status: '运行状态',
enabled: '已启用',
disabled: '未启用',
apiKey: 'API Key',
groupScope: '审计范围',
logs: '审核记录',
currentFilter: '当前筛选结果',
},
filters: {
search: '按用户/Key/摘要搜索',
from: '开始时间',
to: '结束时间',
allGroups: '全部分组',
allEndpoints: '全部端点',
},
table: {
time: '时间',
group: '分组',
user: '用户',
apiKey: 'API Key',
endpoint: '端点',
result: '结果',
highest: '最高分',
actionMeta: '处置',
latency: '上游耗时',
input: '输入摘要',
},
result: {
all: '全部结果',
hit: '命中',
blocked: '已拦截',
pass: '未命中',
error: '异常',
},
action: {
block: '拦截',
keywordBlock: '关键词拦截',
cyberPolicy: '网络安全策略',
error: '异常',
},
},
// Channel Monitor
channelMonitor: {
title: '渠道监控',
description: '监测各渠道的可用性、延迟和状态',
searchPlaceholder: '搜索监控名称...',
allProviders: '全部供应商',
allStatus: '全部状态',
enabledFilter: '启用状态',
onlyEnabled: '仅启用',
onlyDisabled: '仅禁用',
createButton: '新增监控',
createTitle: '新增渠道监控',
editTitle: '编辑渠道监控',
runNow: '立即检测',
runSuccess: '检测完成',
runFailed: '检测失败',
duplicate: '复制',
duplicating: '复制中',
duplicateSuccess: '监控已复制为「{name}」,已默认停用,请确认配置后再启用',
duplicateFailed: '复制监控失败',
duplicateKeyUnavailable: 'API Key 无法解密,请先编辑并重新填写 Key 后再复制',
apiKeyDecryptFailed: 'API Key 解密失败,请重新编辑该监控并填入新的 Key',
createSuccess: '监控创建成功',
updateSuccess: '监控更新成功',
deleteSuccess: '监控删除成功',
loadError: '加载监控列表失败',
deleteConfirm: '确定要删除监控「{name}」吗?此操作不可撤销。',
nameRequired: '请输入监控名称',
primaryModelRequired: '请输入主模型',
linkedAccountRequired: '请选择关联账号',
columns: {
name: '名称',
provider: '供应商',
primaryModel: '主模型',
availability7d: '7 天可用率',
latency: '延迟 (ms)',
enabled: '启用',
actions: '操作'
},
form: {
name: '名称',
namePlaceholder: '输入监控名称',
provider: '平台',
checkMode: '检查方式',
checkModeProbe: '探活',
checkModeProbeHint: '向上游发送轻量 LLM 请求,检测可用性与延迟',
checkModeQuota: '配额',
checkModeQuotaHint: '只查询关联账号的用量滚动窗口/余额,不发送探活请求',
checkModeQuotaProbe: '探活 + 配额',
checkModeQuotaProbeHint: '探活的同时查询配额,用量快照附加在主模型结果上',
linkedAccount: '关联账号',
linkedAccountPlaceholder: '选择账号',
linkedAccountHint: '配额数据来自所选账号(复用账号管理侧的用量/余额查询)',
linkedAccountEmpty: '当前平台暂无账号,请先在账号管理中添加',
linkedAccountMissing: '关联账号已不存在或不可访问,请重新选择账号',
openAIQuotaProbeHint: '注意:OpenAI 平台的用量查询可能触发 Codex 探测请求,会消耗账号自身的额度(每 10 分钟最多触发一次)',
apiMode: 'OpenAI 协议',
apiModeChatCompletions: 'OpenAI Compatible',
apiModeChatCompletionsHint: '使用 /v1/chat/completions,发送 messages;适合大多数兼容站。',
apiModeResponses: 'Responses API',
apiModeResponsesHint: '使用 /v1/responses,默认带 instructions + input;适合本站自检/Codex。',
endpoint: '上游地址',
endpointPlaceholder: 'https://api.example.com',
useCurrentDomain: '使用当前服务',
apiKey: 'API Key',
apiKeyPlaceholder: '请输入 API Key',
apiKeyEditPlaceholder: '留空表示不修改',
useMyKey: '使用我的 Key',
selectKeyTitle: '选择我的 API Key',
selectKeyHint: '仅显示当前账号下处于「启用」状态且未过期的 Key。',
noActiveKey: '没有可用的启用状态 Key',
primaryModel: '主模型',
primaryModelPlaceholder: 'gpt-4o-mini',
extraModels: '附加模型',
extraModelsPlaceholder: '回车添加附加模型',
groupName: '分组名称',
groupNamePlaceholder: '可选,用于在用户视图中聚合显示',
intervalSeconds: '检测间隔 (秒)',
intervalSecondsHint: '范围:15 - 3600 秒',
jitterSeconds: '随机抖动 (± 秒)',
jitterSecondsHint: '每次检测在间隔基础上正负随机偏移该秒数,0 表示固定间隔;需满足 间隔 - 抖动 ≥ 15 秒',
enabled: '启用监控',
kindRequired: '请选择供应商'
},
runResultTitle: '检测结果',
noMonitorsYet: '暂无监控',
createFirstMonitor: '创建第一个监控来跟踪渠道可用性',
advanced: {
section: '高级(可选)',
sectionHint: '自定义请求头和请求体,用于突破上游的客户端识别限制(如仅允许 Claude Code 客户端)。',
headers: '自定义请求头',
headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219',
headerNamePlaceholder: 'Header 名',
headerValuePlaceholder: 'Value',
headerAddRow: '添加 Header',
headerNameInvalid: 'Header 名不能包含空格或冒号:{name}',
headersHint: '与默认请求头合并,用户值优先。hop-by-hop 类 headerHost/Content-Length/...)会被忽略。',
headersParseError: '无法解析这一行:{line}',
bodyMode: '请求体处理',
bodyModeOff: '默认',
bodyModeMerge: '合并',
bodyModeReplace: '覆盖',
bodyModeHintOff: '使用 adapter 默认请求体(带 challenge 数学题校验)。',
bodyModeHintMerge: '与默认请求体浅合并,用户字段优先;但 model / messages / contents 会被保护不允许覆盖(动这些字段请用「覆盖」模式)。',
bodyModeHintReplace: '完全用下方 JSON 作为请求体。注意:此模式下跳过 challenge 校验,改为 HTTP 2xx + 响应文本非空即视为可用。',
bodyJson: 'Body JSON',
bodyJsonFormat: '格式化',
bodyJsonHint: '失焦时自动解析校验。留空等价于没有覆盖。',
bodyJsonError: 'JSON 解析失败',
bodyJsonObjectError: '请求体必须是一个 JSON 对象(不能是数组或基本类型)'
},
templateField: {
label: '请求模板',
none: '不使用模板',
placeholder: '选择一个模板(按当前平台过滤)',
applyHint: '选中模板后,会把模板的请求头和请求体拷贝到此监控(快照)。后续模板变动不自动同步。'
},
template: {
manageButton: '模板管理',
managerTitle: '请求模板管理',
createButton: '新建模板',
emptyState: '当前平台下还没有请求模板',
missingName: '请输入模板名称',
createSuccess: '模板创建成功',
updateSuccess: '模板更新成功',
deleteSuccess: '模板删除成功',
applyButton: '应用到关联监控',
applyTooltip: '把当前模板配置覆盖到所有关联的监控上',
applyTitle: '应用模板',
applyConfirm: '确认应用',
applyConfirmMessage: '将把模板「{name}」的当前配置覆盖到 {n} 个关联监控。监控本地已编辑的自定义修改会被丢弃,是否继续?',
applySuccess: '已应用到 {n} 个监控',
applyPickerTitle: '应用模板「{name}」',
applyPickerHint: '勾选要覆盖请求头/请求体的监控(默认全选)。监控本地已编辑的自定义修改会被丢弃。',
applyPickerEmpty: '当前模板没有关联监控',
applyPickerConfirm: '应用到 {n} 个监控',
selectNone: '全不选',
selectedCount: '已选 {n} / {total}',
deleteConfirm: '确定要删除模板「{name}」吗?{n} 个关联监控会解除关联但保留自己的快照继续工作。',
associatedCount: '{n} 个关联监控',
headersSummary: '{n} 个自定义请求头',
form: {
name: '模板名称',
namePlaceholder: '例:Claude Code 伪装',
description: '说明',
descriptionPlaceholder: '可选:说明这个模板的用途和来源(抓包日期等)'
}
}
},
// Subscriptions Management
subscriptions: {
title: '订阅管理',
description: '管理用户订阅和配额限制',
assignSubscription: '分配订阅',
adjustSubscription: '调整订阅',
revokeSubscription: '撤销订阅',
restoreSubscription: '恢复订阅',
allStatus: '全部状态',
allGroups: '全部分组',
allPlatforms: '全部平台',
daily: '每日',
weekly: '每周',
monthly: '每月',
noLimits: '未配置限额',
unlimited: '无限制',
resetNow: '即将重置',
windowNotActive: '窗口未激活',
resetInMinutes: '{minutes} 分钟后重置',
resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置',
resetInDaysHours: '{days} 天 {hours} 小时后重置',
quotaEndsInMinutes: '额度将在 {minutes} 分钟后结束',
quotaEndsInHoursMinutes: '额度将在 {hours} 小时 {minutes} 分钟后结束',
quotaEndsInDaysHours: '额度将在 {days} 天 {hours} 小时后结束',
daysRemaining: '剩余 {days} 天',
hoursMinutesRemaining: '剩余 {hours} 小时 {minutes} 分钟',
minutesRemaining: '剩余 {minutes} 分钟',
remainingDays: '剩余天数',
noExpiration: '无过期时间',
status: {
active: '生效中',
expired: '已过期',
revoked: '已撤销',
suspended: '已暂停'
},
columns: {
user: '用户',
group: '分组',
usage: '用量',
expires: '到期时间',
status: '状态',
actions: '操作'
},
form: {
user: '用户',
group: '订阅分组',
validityDays: '有效期(天)',
adjustDays: '调整天数'
},
selectUser: '选择用户',
selectGroup: '选择订阅分组',
groupHint: '仅显示订阅计费类型的分组',
validityHint: '订阅的有效天数',
adjustingFor: '为以下用户调整订阅',
currentExpiration: '当前到期时间',
adjustDaysPlaceholder: '正数延长,负数缩短',
adjustHint: '输入正数延长订阅,负数缩短订阅(缩短后剩余天数需大于0)',
assign: '分配',
assigning: '分配中...',
adjust: '调整',
adjusting: '调整中...',
revoke: '撤销',
restore: '恢复',
resetQuota: '重置配额',
resetQuotaTitle: '重置用量配额',
resetQuotaConfirm: "确定要重置 '{user}' 的每日、每周和每月用量配额吗?用量将归零并从今天开始重新计算。",
quotaResetSuccess: '配额重置成功',
failedToResetQuota: '重置配额失败',
noSubscriptionsYet: '暂无订阅',
assignFirstSubscription: '分配一个订阅以开始使用。',
subscriptionAssigned: '订阅分配成功',
subscriptionAdjusted: '订阅调整成功',
subscriptionRevoked: '订阅撤销成功',
subscriptionRestored: '订阅已恢复',
failedToLoad: '加载订阅列表失败',
failedToAssign: '分配订阅失败',
failedToAdjust: '调整订阅失败',
failedToRevoke: '撤销订阅失败',
failedToRestore: '恢复订阅失败',
adjustWouldExpire: '调整后剩余天数必须大于0',
adjustOutOfRange: '调整天数必须在 -36500 到 36500 之间',
pleaseSelectUser: '请选择用户',
pleaseSelectGroup: '请选择分组',
validityDaysRequired: '请输入有效的天数(至少1天)',
revokeConfirm: "确定要撤销 '{user}' 的订阅吗?可稍后在已撤销列表中恢复。",
restoreConfirm: "确定要恢复 '{user}' 的订阅吗?如果原订阅已过期,恢复后将显示为已过期。",
guide: {
title: '订阅管理教程',
subtitle: '订阅模式允许你按时间周期为用户分配使用额度,支持日/周/月配额限制。按照以下步骤即可完成配置。',
showGuide: '使用指南',
step1: {
title: '创建订阅分组',
line1: '前往「分组管理」页面,点击「创建分组」',
line2: '将计费类型设为「订阅」,配置日/周/月额度限制',
line3: '保存分组,确保状态为「正常」',
link: '前往分组管理'
},
step2: {
title: '分配订阅给用户',
line1: '点击本页右上角「分配订阅」按钮',
line2: '在弹窗中搜索用户邮箱并选择目标用户',
line3: '选择订阅分组、设置有效期天数,点击「分配」'
},
step3: {
title: '管理已有订阅'
},
actions: {
adjust: '调整',
adjustDesc: '延长或缩短订阅有效期',
resetQuota: '重置配额',
resetQuotaDesc: '将日/周/月用量归零,重新开始计算',
revoke: '撤销',
revokeDesc: '立即终止该用户的订阅,可在已撤销列表中恢复'
},
tip: '提示:订阅分组下拉列表中只会显示计费类型为「订阅」且状态为「正常」的分组。如果没有可选项,请先到分组管理中创建。'
}
},
// Accounts Management
}
@@ -0,0 +1,19 @@
import overview from './overview'
import channels from './channels'
import accounts from './accounts'
import resources from './resources'
import ops from './ops'
import settings from './settings'
import audit from './audit'
import promptAudit from './promptAudit'
export default {
...overview,
...channels,
...accounts,
...resources,
...ops,
...settings,
...audit,
...promptAudit,
}
+810
View File
@@ -0,0 +1,810 @@
export default {
ops: {
title: '运维监控',
description: '运维监控与排障',
// Dashboard
systemHealth: '系统健康',
overview: '概览',
noSystemMetrics: '尚未收集系统指标。',
collectedAt: '采集时间:',
window: '窗口',
memory: '内存',
db: '数据库',
goroutines: '协程',
jobs: '后台任务',
jobsHelp: '点击“明细”查看任务心跳与报错信息',
active: '活跃',
idle: '空闲',
waiting: '等待',
conns: '连接',
queue: '队列',
accountSwitches: '账号切换',
ok: '正常',
lastRun: '最近运行',
lastSuccess: '最近成功',
lastError: '最近错误',
result: '结果',
noData: '暂无数据',
loadingText: '加载中...',
ready: '就绪',
autoRefreshRemaining: '剩余 {seconds}s',
systemLogs: {
title: '系统日志',
description: '优先显示最新日志,可按条件筛选、搜索和清理。',
queue: '队列',
written: '已写入',
dropped: '已丢弃',
failed: '写入失败',
runtimeConfig: '运行时日志配置(立即生效)',
all: '全部',
level: '级别',
stacktraceThreshold: '堆栈阈值',
samplingInitial: '采样初始条数',
samplingThereafter: '后续采样间隔',
retentionDays: '保留天数',
caller: '调用方',
sampling: '采样',
saveAndApply: '保存并应用',
resetDefaults: '重置默认值',
latestWriteError: '最近写入错误:',
timeRange: '时间范围',
startTime: '开始时间(可选)',
endTime: '结束时间(可选)',
host: 'Host',
component: '组件',
componentPlaceholder: '例如 http.access',
keyId: 'KEY ID',
platform: '平台',
model: '模型',
keyword: '关键词',
keywordPlaceholder: 'message/request_id',
search: '搜索',
cleanCurrentFilters: '清理当前筛选结果',
refreshHealth: '刷新健康状态',
empty: '暂无系统日志',
time: '时间',
logDetails: '日志详情',
loadFailed: '加载系统日志失败',
runtimeConfigActive: '运行时日志配置已生效',
runtimeConfigSaveFailed: '保存日志配置失败',
resetRuntimeConfigConfirm: '确定要重置为启动配置(env/yaml)并立即应用吗?',
runtimeConfigReset: '已重置为启动日志配置',
runtimeConfigResetFailed: '重置日志配置失败',
cleanupConfirm: '确定要清理匹配当前筛选条件的系统日志吗?此操作不可撤销。',
cleanupSuccess: '清理完成,已删除 {count} 条日志。',
cleanupFilterRequired: '清理需要至少一个筛选条件(起止时间或其他字段)',
cleanupFailed: '清理系统日志失败'
},
requestsTotal: '请求(总计)',
slaScope: 'SLA 范围:',
tokens: 'Token数',
tps: 'TPS',
current: '当前',
peak: '峰值',
average: '平均',
totalRequests: '总请求',
avgQps: '平均 QPS',
avgTps: '平均 TPS',
avgLatency: '平均请求时长',
avgTtft: '平均首 Token 延迟',
exceptions: '异常数',
requestErrors: '请求错误',
errorCount: '错误数',
upstreamErrors: '上游错误',
errorCountExcl429529: '错误数(排除429/529',
sla: 'SLA(排除业务限制)',
businessLimited: '业务限制:',
errors: '错误',
errorRate: '错误率:',
upstreamRate: '上游错误率:',
latencyDuration: '请求时长',
ttftLabel: '首 Token 延迟(毫秒)',
p50: 'p50',
p90: 'p90',
p95: 'p95',
p99: 'p99',
avg: 'avg',
max: 'max',
requests: '请求数',
requestsTitle: '请求',
upstream: '上游',
client: '客户端',
system: '系统',
other: '其他',
errorsSla: '错误(SLA范围)',
upstreamExcl429529: '上游(排除429/529',
failedToLoadData: '加载运维数据失败',
failedToLoadOverview: '加载概览数据失败',
failedToLoadThroughputTrend: '加载吞吐趋势失败',
failedToLoadSwitchTrend: '加载平均账号切换趋势失败',
failedToLoadLatencyHistogram: '加载请求时长分布失败',
failedToLoadErrorTrend: '加载错误趋势失败',
failedToLoadErrorDistribution: '加载错误分布失败',
failedToLoadErrorDetail: '加载错误详情失败',
retryFailed: '重试失败',
tpsK: 'TPS(千)',
top: '最高:',
throughputTrend: '吞吐趋势',
switchRateTrend: '平均账号切换趋势',
latencyHistogram: '请求时长分布',
errorTrend: '错误趋势',
errorDistribution: '错误分布',
switchRate: '平均账号切换',
// Health Score & Diagnosis
health: '健康',
healthCondition: '健康状况',
healthHelp: '基于 SLA、错误率和资源使用情况的系统整体健康评分',
healthyStatus: '健康',
riskyStatus: '风险',
idleStatus: '待机',
timeRange: {
'5m': '近5分钟',
'30m': '近30分钟',
'1h': '近1小时',
'1d': '近1天',
'15d': '近15天',
'6h': '近6小时',
'24h': '近24小时',
'7d': '近7天',
'30d': '近30天',
custom: '自定义'
},
openaiTokenStats: {
title: 'OpenAI Token 请求统计',
viewModeTopN: 'TopN',
viewModePagination: '分页',
prevPage: '上一页',
nextPage: '下一页',
pageInfo: '第 {page}/{total} 页',
totalModels: '模型总数:{total}',
failedToLoad: '加载 OpenAI Token 统计失败',
empty: '当前筛选条件下暂无 OpenAI Token 请求统计数据',
table: {
model: '模型',
requestCount: '请求数',
avgTokensPerSec: '平均 Tokens/秒',
avgFirstTokenMs: '平均首 Token 延迟(ms)',
totalOutputTokens: '输出 Token 总数',
avgDurationMs: '平均时长(ms)',
requestsWithFirstToken: '首 Token 样本数'
}
},
customTimeRange: {
startTime: '开始时间',
endTime: '结束时间'
},
fullscreen: {
enter: '进入全屏'
},
diagnosis: {
title: '智能诊断',
footer: '基于当前指标的自动诊断建议',
idle: '系统当前处于待机状态',
idleImpact: '无活跃流量',
// Resource diagnostics
dbDown: '数据库连接失败',
dbDownImpact: '所有数据库操作将失败',
dbDownAction: '检查数据库服务状态、网络连接和连接配置',
redisDown: 'Redis连接失败',
redisDownImpact: '缓存功能降级,性能可能下降',
redisDownAction: '检查Redis服务状态和网络连接',
cpuCritical: 'CPU使用率严重过高 ({usage}%)',
cpuCriticalImpact: '系统响应变慢,可能影响所有请求',
cpuCriticalAction: '检查CPU密集型任务,考虑扩容或优化代码',
cpuHigh: 'CPU使用率偏高 ({usage}%)',
cpuHighImpact: '系统负载较高,需要关注',
cpuHighAction: '监控CPU趋势,准备扩容方案',
memoryCritical: '内存使用率严重过高 ({usage}%)',
memoryCriticalImpact: '可能触发OOM,系统稳定性受威胁',
memoryCriticalAction: '检查内存泄漏,考虑增加内存或优化内存使用',
memoryHigh: '内存使用率偏高 ({usage}%)',
memoryHighImpact: '内存压力较大,需要关注',
memoryHighAction: '监控内存趋势,检查是否有内存泄漏',
ttftHigh: '首 Token 时间偏高 ({ttft}ms)',
ttftHighImpact: '用户感知时长增加',
ttftHighAction: '优化请求处理流程,减少前置逻辑耗时',
// Error rate diagnostics
upstreamCritical: '上游错误率严重偏高 ({rate}%)',
upstreamCriticalImpact: '可能影响大量用户请求',
upstreamCriticalAction: '检查上游服务健康状态,启用降级策略',
upstreamHigh: '上游错误率偏高 ({rate}%)',
upstreamHighImpact: '建议检查上游服务状态',
upstreamHighAction: '联系上游服务团队,准备降级方案',
errorHigh: '错误率过高 ({rate}%)',
errorHighImpact: '大量请求失败',
errorHighAction: '查看错误日志,定位错误根因,紧急修复',
errorElevated: '错误率偏高 ({rate}%)',
errorElevatedImpact: '建议检查错误日志',
errorElevatedAction: '分析错误类型和分布,制定修复计划',
// SLA diagnostics
slaCritical: 'SLA 严重低于目标 ({sla}%)',
slaCriticalImpact: '用户体验严重受损',
slaCriticalAction: '紧急排查错误原因,必要时采取限流保护',
slaLow: 'SLA 低于目标 ({sla}%)',
slaLowImpact: '需要关注服务质量',
slaLowAction: '分析SLA下降原因,优化系统性能',
// Health score diagnostics
healthCritical: '综合健康评分过低 ({score})',
healthCriticalImpact: '多个指标可能同时异常,建议优先排查错误与资源使用情况',
healthCriticalAction: '全面检查系统状态,优先处理critical级别问题',
healthLow: '综合健康评分偏低 ({score})',
healthLowImpact: '可能存在轻度波动,建议关注 SLA 与错误率',
healthLowAction: '监控指标趋势,预防问题恶化',
healthy: '所有系统指标正常',
healthyImpact: '服务运行稳定'
},
// Error Log
errorLog: {
timeId: '时间 / ID',
commonErrors: {
contextDeadlineExceeded: '请求超时',
connectionRefused: '连接被拒绝',
rateLimit: '触发限流'
},
time: '时间',
type: '类型',
context: '上下文',
platform: '平台',
model: '模型',
group: '分组',
user: '用户',
userId: '用户 ID',
apiKey: 'API Key',
keyDeletedBadge: 'Key 已删除',
account: '账号',
accountId: '账号 ID',
status: '状态码',
message: '响应内容',
ip: 'IP',
latency: '请求时长',
action: '操作',
noErrors: '该窗口内暂无错误。',
grp: 'GRP',
acc: 'ACC',
details: '详情',
phase: '阶段',
id: 'ID',
typeUpstream: '上游',
typeRequest: '请求',
typeAuth: '认证',
typeAccountAuth: '账号认证',
typeRouting: '路由',
typeInternal: '内部',
endpoint: '端点',
requestType: '类型',
requestTypeSync: '同步',
requestTypeStream: '流式',
requestTypeWs: 'WS'
},
// Error Details Modal
errorDetails: {
upstreamErrors: '上游错误',
requestErrors: '请求错误',
unresolved: '未解决',
resolved: '已解决',
viewErrors: '错误',
viewExcluded: '排除项',
statusCodeOther: '其他',
owner: {
provider: '服务商',
client: '客户端',
platform: '平台'
},
phase: {
request: '请求',
auth: '认证',
account_auth: '账号认证',
routing: '路由',
upstream: '上游',
network: '网络',
internal: '内部'
},
total: '总计:',
searchPlaceholder: '搜索 request_id / client_request_id / message'
},
// Error Detail Modal
errorDetail: {
title: '错误详情',
titleWithId: '错误 #{id}',
noErrorSelected: '未选择错误。',
resolution: '已解决:',
failedToUpdateResolvedStatus: '更新解决状态失败',
classificationKeys: {
phase: '阶段',
owner: '归属方',
source: '来源',
resolvedAt: '解决时间',
resolvedBy: '解决人'
},
source: {
upstream_http: '上游 HTTP'
},
upstreamKeys: {
status: '状态码',
message: '消息',
detail: '详情',
upstreamErrors: '上游错误列表'
},
upstreamEvent: {
account: '账号',
status: '状态码',
requestId: '请求ID'
},
responsePreview: {
expand: '响应内容(点击展开)',
collapse: '响应内容(点击收起)'
},
loading: '加载中…',
requestId: '请求 ID',
time: '时间',
phase: '阶段',
status: '状态码',
message: '消息',
basicInfo: '基本信息',
platform: '平台',
model: '模型',
group: '分组',
user: '用户',
account: '账号',
latency: '请求时长',
businessLimited: '业务限制',
requestPath: '请求路径',
inboundEndpoint: '入站端点',
upstreamEndpoint: '上游端点',
requestedModel: '请求模型',
upstreamModel: '上游模型',
requestType: '请求类型',
requestTypeUnknown: '未知',
requestTypeSync: '同步',
requestTypeStream: '流式',
requestTypeWs: 'WebSocket',
modelMapping: '模型映射',
timings: '时序信息',
auth: '认证',
routing: '路由',
upstream: '上游',
response: '响应',
classification: '错误分类',
errorBody: '错误体',
trimmed: '已截断',
markResolved: '标记已解决',
markUnresolved: '标记未解决',
tabOverview: '概览',
tabRequest: '请求详情',
tabResponse: '响应详情',
responseBody: '响应详情',
compareA: '对比 A',
compareB: '对比 B',
suggestion: '处理建议',
suggestUpstream: '⚠️ 上游服务不稳定,建议:检查上游账号状态 / 考虑切换账号',
suggestRequest: '⚠️ 客户端请求错误,建议:联系客户修正请求参数 / 手动标记已解决',
suggestAuth: '⚠️ 认证失败,建议:检查 API Key 是否有效 / 联系客户更新凭证',
suggestPlatform: '🚨 平台错误,建议立即排查修复',
suggestGeneric: '查看详情了解更多信息',
apiKeyPrefix: 'Key 前缀',
keyDeletedBadge: 'Key 已删除'
},
requestDetails: {
title: '请求明细',
details: '明细',
rangeLabel: '窗口:{range}',
rangeMinutes: '{n} 分钟',
rangeHours: '{n} 小时',
empty: '该窗口内暂无请求。',
emptyHint: '可尝试调整时间范围或取消部分筛选。',
failedToLoad: '加载请求明细失败',
requestIdCopied: '请求ID已复制',
copyFailed: '复制失败',
copy: '复制',
viewError: '查看错误',
kind: {
success: '成功',
error: '失败'
},
table: {
time: '时间',
kind: '类型',
platform: '平台',
model: '模型',
duration: '耗时',
status: '状态码',
requestId: '请求ID',
actions: '操作'
}
},
alertEvents: {
title: '告警事件',
description: '最近的告警触发/恢复记录(仅邮件通知)',
loading: '加载中...',
empty: '暂无告警事件',
loadFailed: '加载告警事件失败',
status: {
firing: '告警中',
resolved: '已恢复',
manualResolved: '手动已解决'
},
detail: {
title: '告警详情',
loading: '加载详情中...',
empty: '暂无详情',
loadFailed: '加载告警详情失败',
manualResolve: '标记为已解决',
manualResolvedSuccess: '已标记为手动解决',
manualResolvedFailed: '标记为手动解决失败',
silence: '忽略此告警',
silenceSuccess: '已静默该告警',
silenceFailed: '静默失败',
viewRule: '查看规则',
viewLogs: '查看相关日志',
firedAt: '触发时间',
resolvedAt: '解决时间',
ruleId: '规则 ID',
dimensions: '维度信息',
historyTitle: '历史记录',
historyHint: '同一规则 + 相同维度的最近事件',
historyLoading: '加载历史中...',
historyEmpty: '暂无历史记录'
},
table: {
time: '时间',
status: '状态',
severity: '级别',
platform: '平台',
ruleId: '规则ID',
title: '标题',
duration: '持续时间',
metric: '指标 / 阈值',
dimensions: '维度',
email: '邮件已发送',
emailSent: '已发送',
emailIgnored: '已忽略'
}
},
alertRules: {
title: '告警规则',
description: '创建与管理系统阈值告警(仅邮件通知)',
loading: '加载中...',
empty: '暂无告警规则',
loadFailed: '加载告警规则失败',
saveSuccess: '警报规则保存成功',
saveFailed: '保存告警规则失败',
deleteSuccess: '警报规则删除成功',
deleteFailed: '删除告警规则失败',
create: '新建规则',
createTitle: '新建告警规则',
editTitle: '编辑告警规则',
deleteConfirmTitle: '确认删除该规则?',
deleteConfirmMessage: '将删除该规则及其关联的告警事件,是否继续?',
manage: '预警规则',
metricGroups: {
system: '系统指标',
group: '分组级别指标(需 group_id',
account: '账号级别指标'
},
metrics: {
successRate: '成功率 (%)',
errorRate: '错误率 (%)',
upstreamErrorRate: '上游错误率 (%)',
p95: 'P95 请求时长 (ms)',
p99: 'P99 请求时长 (ms)',
cpu: 'CPU 使用率 (%)',
memory: '内存使用率 (%)',
queueDepth: '并发排队深度',
groupAvailableAccounts: '分组可用账号数',
groupAvailableRatio: '分组可用比例 (%)',
groupRateLimitRatio: '分组限流比例 (%)',
accountRateLimitedCount: '限流账号数',
accountErrorCount: '错误账号数(不含临时不可调度)',
accountErrorRatio: '错误账号比例 (%)',
accountTempUnscheduledCount: '临时不可调度账号数',
overloadAccountCount: '过载账号数'
},
metricDescriptions: {
successRate: '统计窗口内成功请求占比(0~100)。',
errorRate: '统计窗口内失败请求占比(0~100)。',
upstreamErrorRate: '统计窗口内上游错误占比(0~100)。',
p95: '统计窗口内 P95 请求耗时(毫秒)。',
p99: '统计窗口内 P99 请求耗时(毫秒)。',
cpu: '当前实例 CPU 使用率(0~100)。',
memory: '当前实例内存使用率(0~100)。',
queueDepth: '统计窗口内并发队列排队深度(等待中的请求数)。',
groupAvailableAccounts: '指定分组中当前可用账号数量(需要 group_id 过滤)。',
groupAvailableRatio: '指定分组中可用账号占比(0~100,需要 group_id 过滤)。',
groupRateLimitRatio: '指定分组中账号被限流的比例(0~100,需要 group_id 过滤)。',
accountRateLimitedCount: '统计窗口内被限流的账号数量。',
accountErrorCount: '统计窗口内产生错误的账号数量(不含临时不可调度)。',
accountErrorRatio: '统计窗口内错误账号占比(0~100)。',
accountTempUnscheduledCount: '当前处于临时不可调度状态的账号数量(如代理/凭据故障被自动摘除)。',
overloadAccountCount: '统计窗口内过载账号数量。'
},
hints: {
recommended: '推荐:运算符 {operator},阈值 {threshold}{unit}',
groupRequired: '该指标为分组级别指标,必须选择分组(group_id)。',
groupOptional: '可选:通过 group_id 将规则限定到某个分组。'
},
table: {
name: '名称',
metric: '指标',
severity: '级别',
enabled: '启用',
actions: '操作'
},
form: {
name: '名称',
description: '描述',
metric: '指标',
operator: '运算符',
groupId: '分组(group_id',
groupPlaceholder: '请选择分组',
allGroups: '全部分组',
threshold: '阈值',
severity: '级别',
window: '统计窗口(分钟)',
sustained: '连续样本数(每分钟)',
cooldown: '冷却期(分钟)',
enabled: '启用',
notifyEmail: '发送邮件通知'
},
validation: {
title: '请先修正以下问题',
invalid: '规则不合法',
nameRequired: '名称不能为空',
metricRequired: '指标不能为空',
groupIdRequired: '分组级别指标必须指定 group_id',
operatorRequired: '运算符不能为空',
thresholdRequired: '阈值必须为数字',
windowRange: '统计窗口必须为 1 / 5 / 60 分钟之一',
sustainedRange: '连续样本数必须在 1 到 1440 之间',
cooldownRange: '冷却期必须在 0 到 1440 分钟之间'
}
},
runtime: {
title: '运维监控运行设置',
description: '配置存储在数据库中,无需修改 config 文件即可生效。',
loading: '加载中...',
noData: '暂无运行设置',
loadFailed: '加载运行设置失败',
saveSuccess: '运行设置已保存',
saveFailed: '保存运行设置失败',
alertTitle: '告警评估器',
groupAvailabilityTitle: '分组可用性监控',
evalIntervalSeconds: '评估间隔(秒)',
silencing: {
title: '告警静默(维护模式)',
enabled: '启用静默',
globalUntil: '静默截止时间(RFC3339',
untilHint: '建议填写截止时间,避免忘记关闭静默。',
reason: '原因',
reasonPlaceholder: '例如:计划维护',
entries: {
title: '高级:定向静默',
hint: '可选:仅静默特定规则或特定级别。字段留空表示匹配全部。',
add: '新增条目',
empty: '暂无定向静默条目',
entryTitle: '条目 #{n}',
ruleId: '规则ID(可选)',
ruleIdPlaceholder: '例如:1',
severities: '级别(可选)',
severitiesPlaceholder: '例如:P0,P1(留空=全部)',
until: '截止时间(RFC3339',
reason: '原因',
validation: {
untilRequired: '条目截止时间不能为空',
untilFormat: '条目截止时间必须为合法的 RFC3339 时间戳',
ruleIdPositive: '条目 rule_id 必须为正整数',
severitiesFormat: '条目级别必须为 P0..P3 的逗号分隔列表'
}
},
validation: {
timeFormat: '静默时间必须为合法的 RFC3339 时间戳'
}
},
lockEnabled: '启用分布式锁',
lockKey: '分布式锁 Key',
lockTTLSeconds: '分布式锁 TTL(秒)',
showAdvancedDeveloperSettings: '显示高级开发者设置 (Distributed Lock)',
advancedSettingsSummary: '高级设置 (分布式锁)',
evalIntervalHint: '检测任务的执行频率,建议保持默认。',
validation: {
title: '请先修正以下问题',
invalid: '设置不合法',
evalIntervalRange: '评估间隔必须在 1 到 86400 秒之间',
lockKeyRequired: '启用分布式锁时必须填写 Lock Key',
lockKeyPrefix: '分布式锁 Key 必须以「{prefix}」开头',
lockKeyHint: '建议以「{prefix}」开头以避免冲突',
lockTtlRange: '分布式锁 TTL 必须在 1 到 86400 秒之间',
slaMinPercentRange: 'SLA 最低值必须在 0-100 之间',
ttftP99MaxRange: 'TTFT P99 最大值必须大于或等于 0',
requestErrorRateMaxRange: '请求错误率最大值必须在 0-100 之间',
upstreamErrorRateMaxRange: '上游错误率最大值必须在 0-100 之间'
}
},
email: {
title: '邮件通知配置',
description: '配置告警/报告邮件通知(存储在数据库中)。',
loading: '加载中...',
noData: '暂无邮件通知配置',
loadFailed: '加载邮件通知配置失败',
saveSuccess: '邮件通知配置已保存',
saveFailed: '保存邮件通知配置失败',
alertTitle: '告警邮件',
reportTitle: '报告邮件',
recipients: '收件人',
recipientsHint: '若为空,系统可能会回退使用第一个管理员邮箱。',
minSeverity: '最低级别',
minSeverityAll: '全部级别',
rateLimitPerHour: '每小时限额',
batchWindowSeconds: '合并窗口(秒)',
includeResolved: '包含恢复通知',
dailySummary: '每日摘要',
weeklySummary: '每周摘要',
errorDigest: '错误摘要',
errorDigestMinCount: '错误摘要最小数量',
accountHealth: '账号健康报告',
accountHealthThreshold: '错误率阈值(%',
cronPlaceholder: 'Cron 表达式',
reportHint: '发送时间使用 Cron 语法;留空将使用默认值。',
validation: {
title: '请先修正以下问题',
invalid: '邮件通知配置不合法',
alertRecipientsRequired: '已启用告警邮件,但未配置任何收件人',
reportRecipientsRequired: '已启用报告邮件,但未配置任何收件人',
invalidRecipients: '存在不合法的收件人邮箱',
rateLimitRange: '每小时限额必须为 ≥ 0 的数字',
batchWindowRange: '合并窗口必须在 0 到 86400 秒之间',
cronRequired: '启用定时任务时必须填写 Cron 表达式',
cronFormat: 'Cron 表达式格式可能不正确(至少应包含 5 段)',
digestMinCountRange: '错误摘要最小数量必须为 ≥ 0 的数字',
accountHealthThresholdRange: '账号健康错误率阈值必须在 0 到 100 之间'
}
},
settings: {
title: '运维监控设置',
loadFailed: '加载设置失败',
saveSuccess: '运维监控设置保存成功',
saveFailed: '保存设置失败',
dataCollection: '数据采集',
evaluationInterval: '评估间隔(秒)',
evaluationIntervalHint: '检测任务的执行频率,建议保持默认',
alertConfig: '预警配置',
enableAlert: '开启预警',
alertRecipients: '预警接收邮箱',
emailPlaceholder: '输入邮箱地址',
recipientsHint: '若为空,系统将使用第一个管理员邮箱作为默认收件人',
minSeverity: '最低级别',
reportConfig: '评估报告配置',
enableReport: '开启评估报告',
reportRecipients: '评估报告接收邮箱',
dailySummary: '每日摘要',
weeklySummary: '每周摘要',
metricThresholds: '指标阈值配置',
metricThresholdsHint: '配置各项指标的告警阈值,超出阈值时将以红色显示',
slaMinPercent: 'SLA最低百分比',
slaMinPercentHint: 'SLA低于此值时显示为红色(默认:99.5%)',
ttftP99MaxMs: 'TTFT P99最大值(毫秒)',
ttftP99MaxMsHint: 'TTFT P99高于此值时显示为红色(默认:500ms)',
requestErrorRateMaxPercent: '请求错误率最大值(%',
requestErrorRateMaxPercentHint: '请求错误率高于此值时显示为红色(默认:5%)',
upstreamErrorRateMaxPercent: '上游错误率最大值(%',
upstreamErrorRateMaxPercentHint: '上游错误率高于此值时显示为红色(默认:5%)',
advancedSettings: '高级设置',
dataRetention: '数据保留策略',
enableCleanup: '启用数据清理',
cleanupSchedule: '清理计划(Cron',
cleanupScheduleHint: '例如:0 2 * * * 表示每天凌晨2点',
errorLogRetentionDays: '错误日志保留天数',
minuteMetricsRetentionDays: '分钟指标保留天数',
hourlyMetricsRetentionDays: '小时指标保留天数',
retentionDaysHint: '建议保留 7-90 天,过长会占用存储空间;填 0 表示每次定时清理时清空所有历史',
aggregation: '预聚合任务',
enableAggregation: '启用预聚合任务',
aggregationHint: '预聚合可提升长时间窗口查询性能',
openaiQuotaAutoPause: 'OpenAI 账号配额自动暂停',
openaiQuotaAutoPauseHint: '当 OpenAI 账号 5h / 7d 用量达到阈值时,调度会自动跳过该账号;窗口滚动后自动恢复。账号级阈值优先于此全局默认值。',
openaiQuotaAutoPauseDefault5h: '默认 5h 用量阈值 (%)',
openaiQuotaAutoPauseDefault7d: '默认 7d 用量阈值 (%)',
openaiQuotaAutoPauseThresholdHint: '取值 0-100,留空或 0 表示不启用全局默认阈值。',
errorFiltering: '错误过滤',
ignoreCountTokensErrors: '忽略 count_tokens 错误',
ignoreCountTokensErrorsHint: '启用后,count_tokens 请求的错误将不会写入错误日志。',
ignoreContextCanceled: '忽略客户端断连错误',
ignoreContextCanceledHint:
'启用后,客户端主动断开连接(context canceled)的错误将不会写入错误日志。',
ignoreNoAvailableAccounts: '忽略无可用账号错误',
ignoreNoAvailableAccountsHint: '启用后,"No available accounts" 错误将不会写入错误日志(不推荐,这通常是配置问题)。',
ignoreInsufficientBalanceErrors: '忽略余额不足错误',
ignoreInsufficientBalanceErrorsHint: '启用后,账号余额不足(Insufficient balance)的错误将不会写入错误日志。',
autoRefresh: '自动刷新',
enableAutoRefresh: '启用自动刷新',
enableAutoRefreshHint: '自动刷新仪表板数据,启用后会定期拉取最新数据。',
refreshInterval: '刷新间隔',
refreshInterval15s: '15 秒',
refreshInterval30s: '30 秒',
refreshInterval60s: '60 秒',
dashboardCards: '仪表盘卡片',
displayAlertEvents: '展示告警事件',
displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。',
displayOpenAITokenStats: '展示 OpenAI Token 请求统计',
displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。',
autoRefreshCountdown: '自动刷新:{seconds}s',
validation: {
title: '请先修正以下问题',
retentionDaysRange: '保留天数必须在 0-365 天之间(0 = 每次清理时清空所有)',
slaMinPercentRange: 'SLA最低百分比必须在0-100之间',
ttftP99MaxRange: 'TTFT P99最大值必须大于等于0',
requestErrorRateMaxRange: '请求错误率最大值必须在0-100之间',
upstreamErrorRateMaxRange: '上游错误率最大值必须在0-100之间',
openaiQuotaAutoPauseRange: 'OpenAI 配额自动暂停阈值必须在 0-100 之间'
}
},
concurrency: {
title: '并发 / 排队',
byPlatform: '按平台',
byGroup: '按分组',
byAccount: '按账号',
byUser: '按用户',
showByUserTooltip: '切换用户视图,显示每个用户的并发使用情况',
switchToUser: '切换到用户视图',
switchToPlatform: '切换回平台视图',
totalRows: '共 {count} 项',
disabledHint: '已在设置中关闭实时监控。',
empty: '暂无数据',
queued: '队列 {count}',
rateLimited: '限流 {count}',
errorAccounts: '异常 {count}',
loadFailed: '加载并发数据失败'
},
realtime: {
title: '实时信息',
connected: '实时已连接',
connecting: '实时连接中',
reconnecting: '实时重连中',
offline: '实时离线',
closed: '实时已关闭',
reconnectIn: '重连 {seconds}s'
},
queryMode: {
auto: 'Auto(自动)',
raw: 'Raw(不聚合)',
preagg: 'Preagg(聚合)'
},
accountAvailability: {
available: '可用',
unavailable: '不可用',
accountError: '异常'
},
tooltips: {
totalRequests: '当前时间窗口内的总请求数和Token消耗量。',
throughputTrend: '当前窗口内的请求/QPS 与 token/TPS 趋势。',
switchRateTrend: '近5小时内账号切换次数 / 请求总数的趋势(平均切换次数)。',
latencyHistogram: '成功请求的请求时长分布(毫秒)。',
errorTrend: '错误趋势(SLA 口径排除业务限制;上游错误率排除 429/529)。',
errorDistribution: '按状态码统计的错误分布(SLA 口径,排除业务限制)。',
upstreamErrors: '上游服务返回的错误,包括API提供商的错误响应(排除429/529限流错误)。',
goroutines:
'Go 运行时的协程数量(轻量级线程)。没有绝对"安全值",建议以历史基线为准。经验参考:<2000 常见;2000-8000 需关注;>8000 且伴随队列上升时,优先排查阻塞/泄漏。',
cpu: 'CPU 使用率,显示系统处理器的负载情况。',
memory: '内存使用率,包括已使用和总可用内存。',
db: '数据库连接池状态,包括活跃连接、空闲连接和等待连接数。',
redis: 'Redis 连接池状态,显示活跃和空闲的连接数。',
jobs: '后台任务执行状态,包括最近运行时间、成功时间和错误信息。',
qps: '每秒查询数(QPS)和每秒Token数(TPS),实时显示系统吞吐量。',
tokens: '当前时间窗口内处理的总Token数量。',
sla: '服务等级协议达成率,排除业务限制(如余额不足、配额超限)的成功请求占比。',
errors: '错误统计,包括总错误数、错误率和上游错误率。',
latency: '请求时长统计,包括 p50、p90、p95、p99 等百分位数。',
ttft: '首 Token 延迟(Time To First Token),衡量流式响应的首 Token 返回速度。',
health: '系统健康评分(0-100),综合考虑 SLA、错误率和资源使用情况。'
},
charts: {
emptyRequest: '该时间窗口内暂无请求。',
emptyError: '该时间窗口内暂无错误。',
resetZoom: '重置',
resetZoomHint: '重置缩放(若启用)',
downloadChart: '下载',
downloadChartHint: '下载图表图片'
}
},
// Settings
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
export default {
promptAudit: {
title: '提示词审计',
description: '通过 OpenAI 兼容 Qwen3Guard 节点异步复核或同步阻止用户输入;事件的完整提示词会入库保存,仅供管理员复核。',
configVersion: '配置版本 v{version}',
tabs: { config: '配置', events: '事件' },
actions: { refresh: '刷新运行态', retry: '重试', Allow: '放行', Warn: '警告', Block: '阻止' },
common: { actions: '操作', never: '从未' },
mode: { off: '已关闭', async_audit: '异步只审计', blocking: '同步审计并阻止' },
status: { disabled: '未启用', running: '运行中', degraded: '降级', error: '错误', healthy: '健康', failed: '失败', stale: '心跳过期' },
decisions: { pass: '通过', flag: '标记', critical: '严重' },
riskLevels: { low: '低', medium: '中', high: '高', critical: '严重' },
scanners: {
violent: '暴力',
non_violent_illegal_acts: '非暴力违法行为',
sexual_content_or_sexual_acts: '色情内容或性行为',
pii: '个人身份信息',
suicide_and_self_harm: '自杀与自残',
unethical_acts: '不道德行为',
politically_sensitive_topics: '政治敏感话题',
copyright_violation: '版权侵犯',
jailbreak: '越狱',
},
scannerDescriptions: {
violent: '暴力或暴力威胁',
non_violent_illegal_acts: '非暴力违法活动',
sexual_content_or_sexual_acts: '色情内容或性行为',
pii: '个人身份信息',
suicide_and_self_harm: '自杀或自残',
unethical_acts: '不道德行为',
politically_sensitive_topics: '政治敏感话题',
copyright_violation: '版权侵权',
jailbreak: '提示注入或越狱尝试',
},
runtime: {
title: '运行概览',
description: '显示服务端当前生效状态;未保存的草稿不会改变这些数值。',
process: '进程状态', mode: '生效模式', version: '生效 / 期望版本', workers: '活动 / 总 Worker',
queue: '活动任务 / 容量', dependencies: '依赖', guardMetrics: '同步 Guard 指标', latest: '最近处理与错误',
queueBreakdown: 'queued {queued} · processing {processing} · retry {retry} · done {done} · failed {failed}',
deliveryTotals: '累计入队 {enqueued} · 丢弃 {dropped} · 处理 {processed} · 失败 {failed}',
},
metrics: { total: '总计', allowed: '放行', flagged: '标记', blocked: '阻止', unavailable: '不可用', timeouts: '超时', failovers: '故障切换' },
pool: {
title: '审计池', description: '按顺序使用启用的 OpenAI 兼容节点;探测由服务端真实网络环境发起。',
add: '新增节点', edit: '编辑节点', empty: '尚未配置审计节点。', node: '节点', model: '模型', limits: '超时 / 单片上限', credential: '凭据与探测',
configured: 'API Key 已配置', missing: '未配置 API Key', invalid: 'API Key 无法解密,请重新输入', probe: '连接测试', probing: '探测中…',
probeProgress: '配置校验 ✓ · 请求已发送 · 等待服务响应…', probeResult: '配置校验 ✓ · 请求 ✓ · HTTP {http} · {status} · {latency} ms',
name: '节点名称', id: '稳定节点 ID', baseUrl: 'Base URL', apiKey: 'API Key', keepSecret: '留空以保留已保存的 API Key', reenterSecret: '已保存的 API Key 无法解密(加密密钥已变更),请重新输入',
secretHint: '明文只在本次编辑内存中存在;保存成功后会立即清除。', clearSecret: '显式清除已保存的 API Key', timeout: '总超时(毫秒)', inputLimit: '单片 Unicode 字符上限',
toggleNode: '切换节点 {name}', deleteConfirm: '从草稿中删除节点“{name}”?保存配置后生效。',
},
policy: {
title: '审计策略', description: '配置适用分组、九类输入风险、Worker 与队列边界。', scope: '适用范围', allGroups: '全部分组', selectedGroups: '指定分组',
searchGroups: '搜索分组', noGroups: '没有匹配分组', missingGroups: '配置中包含已删除的分组 ID', selectedCount: '已选择 {count} 个分组',
scanners: 'Qwen3Guard 输入风险分类', workerCount: 'Worker 数量', queueCapacity: '持久队列容量', strategy: '节点策略', strategyHint: '按配置顺序优先尝试,必要时故障切换。',
},
saveBar: { enabled: '启用提示词审计', blocking: '同步阻止', blockingLatestTurnOnly: '仅审最新输入和上一轮输出', storePass: '保存安全事件', dirty: '有未保存的更改', synced: '配置已同步' },
blockingConfirm: {
title: '开启同步阻止?',
message: '适用请求会在账号选择、计费和访问上游之前等待 Guard。命中 Block、Guard 不可用或响应非法时,请求都不会访问上游。',
confirm: '理解风险并开启',
},
events: {
title: '审计事件', description: '按身份、入口、风险、Hash 和时间复核事件,详情中可查看完整提示词。', decision: '判定', risk: '风险等级', endpoint: '入口', groupId: '分组 ID', userId: '用户 ID', apiKeyId: 'API Key ID', keyword: '关键词',
startAt: '开始时间', endAt: '结束时间', deleteSelected: '删除选中项({count}', deleteByFilter: '按筛选删除',
filterDeleteDialogTitle: '按筛选删除审计事件', filterDeleteDialogDesc: '选择删除的时间范围与风险条件后即可执行删除;删除不可恢复。如需提前查看匹配数量,可先获取删除预览。',
filterTimeRange: '删除时间范围', filterTimeRangeHint: '将删除所选截止时间之前产生的事件;预览后新产生的事件不受影响。',
timePresets: { '1d': '1 天前', '7d': '7 天前', '30d': '30 天前', '90d': '90 天前', all: '全部时间', custom: '自定义范围' },
customRangeInvalid: '自定义范围需要有效的开始与结束时间,且开始早于结束。',
moreConditions: '更多条件(入口 / 关键词 / 分组 / 用户)',
filterDeletePreviewAction: '获取删除预览', filterDeletePreviewing: '正在生成预览…', filterDeleteNeedPreview: '可直接确认删除;如需提前查看匹配数量,可先获取删除预览。',
filterDeleteConfirmInvalidRange: '请先选择有效的删除时间范围(自定义范围需开始早于结束)。', filterDeleteConfirmNoMatches: '当前筛选匹配 0 条事件,没有可删除的内容。',
selectAll: '选择当前页全部事件', selectEvent: '选择事件 {id}', time: '时间', identity: '用户 / 邮箱 / API Key', user: '用户名', email: '用户邮箱', apiKey: 'API Key 名称', group: '分组', route: '入口 / 模型', result: '判定 / 风险', preview: '脱敏预览', empty: '没有符合条件的事件。',
passEventsDisabled: '当前未开启“保存安全事件”:安全请求仍会完成审计,但不会出现在事件列表中;Flag 和 Critical 风险事件仍会保存。', openConfiguration: '前往配置',
detailTitle: '提示词审计事件详情', tabs: { summary: '审计摘要', risks: '具体风险', technical: '技术信息' },
promptFull: '完整提示词(未脱敏)',
promptFullHint: '完整提示词已随事件入库,仅供管理员复核触发内容;请按敏感数据妥善处理,切勿外泄。',
guardReturn: '模型审计返回',
guardReturnHint: '展示 Guard 归一化后的结构化结果(判定、分类、分数与脱敏证据),不含原始响应体。',
riskSummaries: '风险摘要',
evidence: '脱敏证据',
score: '分数',
categories: '分类', model: '模型', stage: '请求阶段', noRisks: '本事件没有派生风险摘要。',
requestId: 'Request ID', promptHash: 'Prompt SHA-256',
technical: {
scanner: '扫描器', policy: '策略', guardEndpoint: 'Guard 节点', config: '配置版本',
chunks: '分片数', latency: '耗时', protocol: '协议',
},
deleteConfirmTitle: '删除审计事件?', deleteConfirmMessage: '将永久删除 {count} 条事件及符合条件的孤立任务。', filterDeleteCount: '服务端快照匹配 {count} 条事件。', snapshotMax: '快照最大事件 ID', expiresAt: '确认令牌过期时间', filterDeleteWarning: '只删除预览高水位内的事件;预览后产生的新事件会保留。筛选一旦变化,必须重新预览。', confirmFilterDelete: '确认永久删除',
},
messages: { saved: '提示词审计配置已保存,明文 API Key 状态已清除。', probeSucceeded: '审计节点连接正常。', deleted: '已删除 {count} 条审计事件。' },
errors: {
loadConfig: '无法加载提示词审计配置。', loadRuntime: '无法加载提示词审计运行态。', loadGroups: '无法加载分组列表。', loadEvents: '无法加载审计事件。', loadDetail: '无法加载事件详情。', saveConfig: '配置保存失败。', probe: '节点探测失败。', delete: '事件删除失败。', previewDelete: '无法生成删除预览,请检查时间范围。', deleteConfirmation: '删除确认无效或已过期,请重新预览。',
prompt_audit_config_conflict: '配置已被其他管理员更新。请重新加载服务端配置,再决定如何合并本地草稿。',
prompt_audit_encryption_key_required: '未配置固定加密密钥,审计节点 API Key 将在服务重启后失效。请先设置 TOTP_ENCRYPTION_KEY 环境变量并重启服务。',
prompt_guard_requires_audit_enabled: '开启同步阻止前必须先启用提示词审计。', prompt_audit_invalid_endpoint: '审计节点配置无效。', prompt_audit_endpoint_required: '启用审计前至少需要一个启用节点。', prompt_audit_groups_required: '指定分组模式至少需要选择一个分组。', prompt_audit_scanners_required: '至少需要启用一个风险分类。',
},
},
}
@@ -0,0 +1,602 @@
export default {
scheduledTests: {
title: '定时测试',
addPlan: '添加计划',
editPlan: '编辑计划',
deletePlan: '删除计划',
model: '模型',
cronExpression: 'Cron 表达式',
enabled: '启用',
lastRun: '上次运行',
nextRun: '下次运行',
maxResults: '最大结果数',
noPlans: '暂无定时测试计划',
confirmDelete: '确定要删除此计划吗?',
createSuccess: '计划创建成功',
updateSuccess: '计划更新成功',
deleteSuccess: '计划删除成功',
results: '测试结果',
noResults: '暂无测试结果',
responseText: '响应',
errorMessage: '错误',
success: '成功',
failed: '失败',
running: '运行中',
schedule: '定时测试',
cronHelp: '标准 5 字段 cron 表达式(例如 */30 * * * *',
cronTooltipTitle: 'Cron 表达式示例:',
cronTooltipMeaning: '用于定义自动执行测试的时间规则,格式依次为:分钟 小时 日 月 星期。',
cronTooltipExampleEvery30Min: '*/30 * * * *:每 30 分钟运行一次',
cronTooltipExampleHourly: '0 * * * *:每小时整点运行一次',
cronTooltipExampleDaily: '0 9 * * *:每天 09:00 运行一次',
cronTooltipExampleWeekly: '0 9 * * 1:每周一 09:00 运行一次',
cronTooltipRange: '推荐填写范围:使用标准 5 字段 cron;如果只是健康检查,建议从每 30 分钟、每 1 小时或每天固定时间开始,不建议一开始就设置过高频率。',
maxResultsTooltipTitle: '最大结果数说明:',
maxResultsTooltipMeaning: '用于限制单个计划最多保留多少条历史测试结果,避免结果列表无限增长。',
maxResultsTooltipBody: '系统只会保留最近的测试结果;当保存数量超过这个值时,更早的历史记录会自动清理,避免列表过长和存储持续增长。',
maxResultsTooltipExample: '例如填写 100,表示最多保存最近 100 次测试结果;第 101 次结果写入后,最早的一条会被清理。',
maxResultsTooltipRange: '推荐填写范围:一般可填 20 到 200。只关注近期可用性时可填 20-50;需要回看较长时间的波动趋势时可填 100-200。',
autoRecover: '自动恢复',
autoRecoverHelp: '测试成功后自动恢复异常状态的账号'
},
// Proxies Management
proxies: {
title: 'IP管理',
description: '管理代理服务器配置',
createProxy: '添加代理',
editProxy: '编辑代理',
deleteProxy: '删除代理',
ad: {
inline: '正在寻找合适的代理 IP'
},
deleteConfirmMessage: "确定要删除代理 '{name}' 吗?",
testProxy: '测试代理',
dataImport: '导入',
dataExportSelected: '导出选中',
dataImportTitle: '导入代理',
dataImportHint: '上传代理导出的 JSON 文件以批量导入代理。',
dataImportWarning: '导入将创建或复用代理,保留状态并在完成后自动触发延迟检测。',
dataImportFile: '数据文件',
dataImportButton: '开始导入',
dataImporting: '导入中...',
dataImportSelectFile: '请选择数据文件',
dataImportParseFailed: '数据解析失败',
dataImportFailed: '数据导入失败',
dataImportResult: '导入结果',
dataImportResultSummary: '创建 {proxy_created},复用 {proxy_reused},失败 {proxy_failed}',
dataImportErrors: '失败详情',
dataImportSuccess: '导入完成:创建 {proxy_created},复用 {proxy_reused}',
dataImportCompletedWithErrors: '导入完成但有错误:失败 {proxy_failed}',
dataExport: '导出',
dataExportConfirmMessage: '导出的数据包含代理的敏感信息,请妥善保存。',
dataExportConfirm: '确认导出',
dataExported: '数据导出成功',
dataExportFailed: '数据导出失败',
columns: {
name: '名称',
protocol: '协议',
address: '地址',
auth: '认证',
location: '地理位置',
status: '状态',
accounts: '账号数',
latency: '延迟',
expiry: '有效期',
createdAt: '创建时间',
actions: '操作',
nameLabel: '名称',
namePlaceholder: '请输入代理名称',
protocolLabel: '协议',
selectProtocol: '选择协议',
hostLabel: '主机',
hostPlaceholder: '请输入主机地址',
portLabel: '端口',
portPlaceholder: '请输入端口',
usernameLabel: '用户名(可选)',
usernamePlaceholder: '请输入用户名',
passwordLabel: '密码(可选)',
passwordPlaceholder: '请输入密码',
priorityLabel: '优先级',
statusLabel: '状态'
},
filters: {
protocol: '协议',
allProtocols: '全部协议',
status: '状态',
allStatuses: '全部状态'
},
// Additional keys used in ProxiesView
copyProxyUrl: '复制代理 URL',
urlCopied: '代理 URL 已复制',
allProtocols: '全部协议',
allStatus: '全部状态',
searchProxies: '搜索代理...',
protocols: {
http: 'HTTP',
https: 'HTTPS',
socks5: 'SOCKS5',
socks5h: 'SOCKS5H (远程 DNS)',
},
name: '名称',
protocol: '协议',
host: '主机',
port: '端口',
username: '用户名(可选)',
password: '密码(可选)',
status: '状态',
enterProxyName: '请输入代理名称',
optionalAuth: '可选认证信息',
leaveEmptyToKeep: '留空保持不变',
form: {
hostPlaceholder: '请输入主机地址',
portPlaceholder: '请输入端口'
},
noProxiesYet: '暂无代理',
createFirstProxy: '添加您的第一个代理以开始使用。',
testConnection: '测试连接',
qualityCheck: '质量检测',
batchQualityCheck: '批量质量检测',
batchTest: '批量测试',
testFailed: '失败',
latencyFailed: '链接失败',
batchTestEmpty: '暂无可测试的代理',
batchTestDone: '批量测试完成,共测试 {count} 个代理',
batchTestFailed: '批量测试失败',
batchDeleteAction: '删除',
batchDelete: '批量删除',
batchDeleteConfirm: '确定删除选中的 {count} 个代理吗?已被账号使用的将自动跳过。',
batchDeleteDone: '已删除 {deleted} 个代理,跳过 {skipped} 个',
batchDeleteSkipped: '已跳过 {skipped} 个代理',
batchDeleteFailed: '批量删除失败',
deleteBlockedInUse: '该代理已有账号使用,无法删除',
accountsTitle: '使用该IP的账号',
accountsEmpty: '暂无账号使用此代理',
accountsFailed: '获取账号列表失败',
accountName: '账号名称',
accountPlatform: '所属平台',
accountNotes: '备注',
// Batch import
standardAdd: '标准添加',
batchAdd: '快捷添加',
batchInput: '代理列表',
batchInputPlaceholder:
"每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口",
parsedCount: '有效 {count} 个',
invalidCount: '无效 {count} 个',
duplicateCount: '重复 {count} 个',
importing: '导入中...',
importProxies: '导入 {count} 个代理',
batchImportSuccess: '成功导入 {created} 个代理,跳过 {skipped} 个重复',
batchImportAllSkipped: '全部 {skipped} 个代理已存在,跳过导入',
failedToImport: '批量导入失败',
// Other messages
saving: '保存中...',
testing: '测试中...',
creating: '创建中...',
updating: '更新中...',
noProxies: '暂无代理',
noProxiesDescription: '添加代理服务器以增强 API 访问稳定性。',
proxyCreated: '代理添加成功',
proxyUpdated: '代理更新成功',
proxyDeleted: '代理删除成功',
proxyWorking: '代理连接正常',
proxyWorkingWithLatency: '代理连接正常,延迟 {latency}ms',
proxyTestFailed: '代理测试失败',
qualityCheckDone: '质量检测完成:评分 {score}{grade}',
qualityCheckFailed: '代理质量检测失败',
batchQualityDone: '批量质量检测完成,共检测 {count} 个;优质 {healthy} 个,告警 {warn} 个,挑战 {challenge} 个,异常 {failed} 个',
batchQualityFailed: '批量质量检测失败',
batchQualityEmpty: '暂无可检测质量的代理',
qualityReportTitle: '代理质量检测报告',
qualityGrade: '等级 {grade}',
qualityExitIP: '出口 IP',
qualityCountry: '出口地区',
qualityBaseLatency: '基础延迟',
qualityCheckedAt: '检测时间',
qualityTableTarget: '检测项',
qualityTableStatus: '状态',
qualityTableLatency: '延迟',
qualityTableMessage: '说明',
qualityInline: '质量 {grade}/{score}',
qualityStatusHealthy: '优质',
qualityStatusPass: '通过',
qualityStatusWarn: '告警',
qualityStatusFail: '失败',
qualityStatusChallenge: '挑战',
qualityTargetBase: '基础连通性',
proxyCreatedSuccess: '代理添加成功',
proxyUpdatedSuccess: '代理更新成功',
proxyDeletedSuccess: '代理删除成功',
testSuccess: '代理测试通过',
failedToLoad: '加载代理列表失败',
failedToSave: '保存代理失败',
failedToDelete: '删除代理失败',
failedToCreate: '创建代理失败',
failedToUpdate: '更新代理失败',
failedToTest: '测试代理失败',
nameRequired: '请输入代理名称',
hostRequired: '请输入主机地址',
portInvalid: '端口必须在 1-65535 之间',
deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。",
neverExpires: '永不过期',
expired: '已过期',
overdueDays: '已超期 {days} 天',
expiringInDays: '{days} 天后到期',
remainingDays: '剩余 {days} 天',
expiresAt: '有效期',
nDays: '{days} 天',
expiryDaysPlaceholder: '自定义天数,留空 = 永不过期',
expiryWarnDays: '到期提醒提前天数',
fallbackMode: '到期回退',
fallbackNone: '不回退',
fallbackProxy: '指定备用代理',
fallbackDirect: '回退直连',
backupProxy: '备用代理',
},
// Redeem Codes Management
redeem: {
title: '兑换码管理',
description: '生成和管理兑换码',
generateCodes: '生成兑换码',
columns: {
code: '兑换码',
type: '类型',
value: '面值',
status: '状态',
usedBy: '使用者',
usedAt: '使用时间',
expiresAt: '过期时间',
createdAt: '创建时间',
actions: '操作'
},
types: {
balance: '余额',
concurrency: '并发数',
subscription: '订阅',
invitation: '邀请码',
// 管理员在用户管理页面调整余额/并发时产生的记录
admin_balance: '余额(管理员)',
admin_concurrency: '并发数(管理员)'
},
// 用于选择器和筛选器的直接键
balance: '余额',
concurrency: '并发数',
subscription: '订阅',
invitation: '邀请码',
invitationHint: '邀请码用于限制用户注册,使用后自动标记为已使用。',
allTypes: '全部类型',
allStatus: '全部状态',
unused: '未使用',
used: '已使用',
searchCodes: '搜索兑换码或邮箱...',
exportCsv: '导出 CSV',
batchUpdate: '批量修改',
batchUpdateTitle: '批量修改兑换码',
selectedCount: '已选择 {count} 个兑换码',
clearSelection: '清空选择',
selectCodesFirst: '请先选择兑换码',
noBatchFieldsSelected: '请至少勾选一个要修改的字段',
batchUpdateSuccess: '成功修改 {count} 个兑换码',
failedToBatchUpdate: '批量修改兑换码失败',
batchFields: {
status: '状态',
expiresAt: '过期时间',
notes: '备注',
group: '分组'
},
batchNotesPlaceholder: '输入新的备注,留空可清空备注',
clearGroup: '清空分组',
deleteAllUnused: '删除全部未使用',
deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。',
deleteAllUnusedConfirm: '确定要删除全部未使用的兑换码吗?此操作无法撤销。',
deleteAll: '全部删除',
generateCodesTitle: '生成兑换码',
generatedSuccessfully: '生成成功',
codesCreated: '已创建 {count} 个兑换码',
codeType: '类型',
amount: '金额 ($)',
value: '面值',
count: '数量',
generate: '生成',
copyAll: '全部复制',
download: '下载',
codesExported: '兑换码导出成功',
codeDeleted: '兑换码删除成功',
codesDeleted: '成功删除 {count} 个未使用的兑换码',
noUnusedCodes: '没有未使用的兑换码可删除',
userPrefix: '用户 #{id}',
failedToExport: '导出兑换码失败',
failedToDeleteUnused: '删除未使用的兑换码失败',
failedToCopy: '复制失败',
selectGroup: '选择分组',
selectGroupPlaceholder: '选择订阅分组',
validityDays: '有效天数',
codeExpiry: '兑换码过期',
neverExpires: '永不过期',
expiryPresetDays: '{days} 天',
customExpiry: '自定义',
customExpiryDays: '自定义天数',
expiryDaysRequired: '请输入有效的过期天数',
groupRequired: '请选择订阅分组',
days: '天',
status: {
unused: '未使用',
used: '已使用',
expired: '已过期',
disabled: '已禁用'
},
form: {
typeLabel: '类型',
selectType: '选择类型',
valueLabel: '面值',
valuePlaceholder: '请输入面值',
balanceHint: '余额金额(美元)',
concurrencyHint: '并发数增量',
countLabel: '数量',
countPlaceholder: '请输入数量',
countHint: '要生成的兑换码数量',
prefixLabel: '前缀(可选)',
prefixPlaceholder: '例如:GIFT',
expiresLabel: '过期时间(可选)'
},
filters: {
type: '类型',
allTypes: '全部类型',
status: '状态',
allStatuses: '全部状态',
search: '搜索兑换码'
},
generating: '生成中...',
copyCode: '复制',
copied: '已复制!',
disableCode: '禁用',
enableCode: '启用',
deleteCode: '删除',
deleteConfirmMessage: '确定要删除此兑换码吗?',
noCodes: '暂无兑换码',
noCodesDescription: '生成兑换码以向用户分发余额或并发数。',
codesGeneratedSuccess: '兑换码生成成功,共 {count} 个',
codeDisabledSuccess: '兑换码已禁用',
codeEnabledSuccess: '兑换码已启用',
codeDeletedSuccess: '兑换码删除成功',
failedToLoad: '加载兑换码列表失败',
failedToGenerate: '生成兑换码失败',
failedToUpdate: '更新兑换码失败',
failedToDelete: '删除兑换码失败'
},
// Announcements
announcements: {
title: '公告管理',
description: '创建公告并按条件投放',
createFirstAnnouncement: '还没有公告,创建您的第一条公告。',
createAnnouncement: '创建公告',
editAnnouncement: '编辑公告',
deleteAnnouncement: '删除公告',
searchAnnouncements: '搜索公告...',
status: '状态',
allStatus: '全部状态',
columns: {
title: '标题',
status: '状态',
notifyMode: '通知方式',
targeting: '展示条件',
timeRange: '有效期',
createdAt: '创建时间',
actions: '操作'
},
statusLabels: {
draft: '草稿',
active: '展示中',
archived: '已归档'
},
notifyModeLabels: {
silent: '静默',
popup: '弹窗'
},
form: {
title: '标题',
content: '内容(支持 Markdown',
status: '状态',
notifyMode: '通知方式',
notifyModeHint: '弹窗模式会自动弹出通知给用户',
startsAt: '开始时间',
endsAt: '结束时间',
startsAtHint: '留空表示立即生效',
endsAtHint: '留空表示永久生效',
targetingMode: '展示条件',
targetingAll: '所有用户',
targetingCustom: '按条件',
addOrGroup: '添加 OR 条件组',
addAndCondition: '添加 AND 条件',
conditionType: '条件类型',
conditionSubscription: '订阅套餐',
conditionBalance: '余额',
operator: '运算符',
balanceValue: '余额阈值',
selectPackages: '选择套餐'
},
operators: {
gt: '>',
gte: '≥',
lt: '<',
lte: '≤',
eq: '='
},
targetingSummaryAll: '全部用户',
targetingSummaryCustom: '自定义({groups} 组)',
timeImmediate: '立即',
timeNever: '永久',
readStatus: '已读情况',
preview: '预览',
eligible: '符合条件',
readAt: '已读时间',
unread: '未读',
searchUsers: '搜索用户...',
failedToLoad: '加载公告失败',
failedToCreate: '创建公告失败',
failedToUpdate: '更新公告失败',
failedToDelete: '删除公告失败',
failedToLoadReadStatus: '加载已读情况失败',
deleteConfirm: '确定要删除该公告吗?此操作无法撤销。'
},
// Promo Codes
promo: {
title: '优惠码管理',
description: '创建和管理注册优惠码',
createCode: '创建优惠码',
editCode: '编辑优惠码',
deleteCode: '删除优惠码',
searchCodes: '搜索优惠码...',
allStatus: '全部状态',
columns: {
code: '优惠码',
bonusAmount: '赠送金额',
maxUses: '最大使用次数',
usedCount: '已使用',
usage: '使用量',
status: '状态',
expiresAt: '过期时间',
createdAt: '创建时间',
actions: '操作'
},
// 表单标签(扁平结构便于模板使用)
code: '优惠码',
autoGenerate: '留空自动生成',
codePlaceholder: '输入优惠码或留空',
bonusAmount: '赠送金额 ($)',
maxUses: '最大使用次数',
zeroUnlimited: '0 = 无限制',
expiresAt: '过期时间',
notes: '备注',
notesPlaceholder: '可选备注信息',
status: '状态',
neverExpires: '永不过期',
// 状态标签
statusActive: '启用',
statusDisabled: '禁用',
statusExpired: '已过期',
statusMaxUsed: '已用完',
// 使用记录
usageRecords: '使用记录',
viewUsages: '查看使用记录',
noUsages: '暂无使用记录',
userPrefix: '用户 #{id}',
copied: '已复制!',
// 消息
noCodesYet: '暂无优惠码',
createFirstCode: '创建您的第一个优惠码,为新用户提供注册奖励。',
codeCreated: '优惠码创建成功',
codeUpdated: '优惠码更新成功',
codeDeleted: '优惠码删除成功',
deleteCodeConfirm: '确定要删除此优惠码吗?此操作无法撤销。',
copyRegisterLink: '复制注册链接',
registerLinkCopied: '注册链接已复制到剪贴板',
failedToLoad: '加载优惠码失败',
failedToCreate: '创建优惠码失败',
failedToUpdate: '更新优惠码失败',
failedToDelete: '删除优惠码失败',
failedToLoadUsages: '加载使用记录失败'
},
// Usage Records
usage: {
title: '使用记录',
description: '查看和管理所有用户的使用记录',
userFilter: '用户',
searchUserPlaceholder: '按邮箱搜索用户...',
searchApiKeyPlaceholder: '按名称搜索 API 密钥...',
searchAccountPlaceholder: '按名称搜索账号...',
selectedUser: '已选择',
user: '用户',
account: '账户',
group: '分组',
requestId: '请求ID',
requestIdCopied: '请求ID已复制',
allModels: '全部模型',
allAccounts: '全部账户',
allGroups: '全部分组',
allTypes: '全部类型',
inputCost: '输入费用',
outputCost: '输出费用',
cacheCreationCost: '缓存创建费用',
cacheReadCost: '缓存读取费用',
inputTokens: '输入 Token',
outputTokens: '输出 Token',
cacheCreationTokens: '缓存创建 Token',
cacheCreation5mTokens: '缓存创建',
cacheCreation1hTokens: '缓存创建',
cacheReadTokens: '缓存读取 Token',
failedToLoad: '加载使用记录失败',
billingType: '计费类型',
allBillingTypes: '全部计费类型',
billingTypeBalance: '钱包余额',
billingTypeSubscription: '订阅套餐',
billingMode: '计费模式',
billingModeToken: '按量',
billingModePerRequest: '按次',
billingModeImage: '按次(图片)',
billingModeVideo: '按次(视频)',
allBillingModes: '全部计费模式',
upstreamModelAudit: '上游模型审计',
allUpstreamModelAudit: '全部响应模型状态',
upstreamModelMismatchOnly: '仅不一致',
upstreamModelMatchedOnly: '仅一致',
ipAddress: 'IP',
clickToViewBalance: '点击查看充值记录',
failedToLoadUser: '加载用户信息失败',
userDeletedBadge: '已删除',
tokenRanking: {
subtitle: '按当前筛选与时间范围统计每个用户的 Token 用量',
rowHint: '点击查看该用户的用量明细',
userCount: '共 {count} 位用户',
columns: {
user: '用户',
requests: '请求数',
inputTokens: '输入 Token',
outputTokens: '输出 Token',
cacheTokens: '缓存 Token',
totalTokens: '总 Token',
cost: '费用'
}
},
cleanup: {
button: '清理',
title: '清理使用记录',
warning: '清理不可恢复,且会影响历史统计回看。',
submit: '提交清理',
submitting: '提交中...',
confirmTitle: '确认清理',
confirmMessage: '确定要提交清理任务吗?清理不可恢复。',
confirmSubmit: '确认清理',
cancel: '取消任务',
cancelConfirmTitle: '确认取消',
cancelConfirmMessage: '确定要取消该清理任务吗?',
cancelConfirm: '确认取消',
cancelSuccess: '清理任务已取消',
cancelFailed: '取消清理任务失败',
recentTasks: '最近清理任务',
loadingTasks: '正在加载任务...',
noTasks: '暂无清理任务',
range: '时间范围',
deletedRows: '删除数量',
missingRange: '请选择时间范围',
submitSuccess: '清理任务已创建',
submitFailed: '创建清理任务失败',
loadFailed: '加载清理任务失败',
status: {
pending: '待执行',
running: '执行中',
succeeded: '已完成',
failed: '失败',
canceled: '已取消'
}
}
},
// Ops Monitoring
}
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
export default {
batchImage: {
columns: {
taskName: '任务名称',
model: '模型',
apiKey: '提交密钥',
result: '结果',
cost: '费用',
downloadStatus: '下载状态',
},
status: {
queued: '排队中',
running: '生成中',
processingResults: '整理结果',
settling: '结算中',
completed: '已完成',
failed: '失败',
cancelled: '已取消',
outputDeleted: '结果已删除',
partialSuccess: '部分成功',
allFailed: '全部失败',
},
itemStatus: {
pending: '排队中',
succeeded: '成功',
failed: '失败',
cancelled: '已取消',
recovered: '已补成功',
},
filters: {
searchTaskName: '搜索任务名称',
allApiKeys: '全部 API Key',
allStatuses: '全部状态',
allDownloadStates: '全部下载状态',
downloaded: '已下载',
notDownloaded: '未下载',
},
actions: {
usageGuide: '使用说明',
createJob: '创建批量任务',
downloadSelected: '下载选中',
deleteRecords: '删除记录',
retryFailedItems: '重试失败项',
cancelJob: '取消任务',
downloadZip: '下载 ZIP',
viewDetail: '查看详情',
download: '下载',
moreActions: '更多操作',
copyInstruction: '复制说明',
submitJob: '提交任务',
},
list: {
selectedJobs: '已选择 {count} 个任务',
expandChildren: '展开 {n} 个子任务',
collapseChildren: '收起子任务',
childCount: '{n} 子任务',
childBadge: '子任务',
keyNotRecorded: '未记录',
totalCount: '共 {n}',
notDownloaded: '未下载',
empty: '暂无批量任务',
emptyHint: '点击右上角创建批量任务。',
},
pagination: {
pageNumber: '第 {page} 页',
pageItems: '本页 {count} 条',
},
promptPopover: {
title: '完整 Prompt',
copied: 'Prompt 已复制',
},
detail: {
title: '任务详情',
aggregatedResult: '汇总结果',
result: '结果',
cost: '费用',
downloadStatus: '下载状态',
items: '明细',
preview: '预览',
previewZoom: '放大压缩预览 {id}',
previewReload: '重新加载压缩预览',
previewLoad: '加载压缩预览',
previewUnavailable: '不可预览',
noImage: '无图片',
loadingItems: '正在加载明细...',
noItems: '暂无明细',
noItemsHint: '排队或生成中的任务会先显示已提交的 prompt,结果整理完成后会更新图片状态。',
mainTask: '主任务:{name}',
childTask: '子任务:{name}',
holdCost: '冻结 {amount}',
},
itemResult: {
recoveredByRetry: '旧失败已由重试子任务补成功',
readyPreview: '图片已生成,可点击预览',
readyDownload: '图片已生成,可下载',
noUsableImage: '未生成可用图片',
cancelled: '任务已取消',
waiting: '等待生成结果',
emptyImageOutput: '上游返回了结果,但这条没有图片内容。通常是 Gemini/Vertex 单条生成失败或被安全策略拦截。',
providerItemFailed: '上游返回的这条结果没有可用图片。',
},
imagePreview: {
title: '图片预览',
notice: '当前显示的是浏览器本地缓存的压缩缩略图,清晰度会有影响;需要查看原图请下载 ZIP。',
},
create: {
title: '创建批量任务',
taskName: '任务名称',
taskNamePlaceholder: '不填写则默认使用当前时间',
loadingKeys: '加载 API Key 中...',
selectKeyPlaceholder: '请选择 Gemini API Key',
noKeysHint: '当前没有可用于批量生图的 Gemini API Key。请先创建并绑定已开启批量生图的 Gemini 分组。',
model: '模型',
imageSize: '图片尺寸',
imageSizeHint: '当前批量任务固定按 1K 图片提交。',
outputFormat: '输出格式',
estimatedOutput: '预计生成',
estimatedOutputValue: '{images} 张 / {prompts} 条',
promptAdded: '已添加 {count} 条',
promptPlaceholder: '粘贴 prompt,添加后进入下方列表',
customIdPlaceholder: 'Custom ID 可选',
outputCountPerPrompt: '每条生成张数',
outputCountOption: '{n} 张',
referenceImage: '参考图',
removeReferenceImage: '移除参考图',
limitsHint: '每条最多 {maxPerItem} 张,整组最多 {maxPerJob} 张;当前模型每条最多 {refLimit} 张参考图,参考图按生成张数重复消耗输入 token。',
referenceCount: '{n} 参考图',
noPrompts: '还没有添加 prompt。',
cancelNotice: '取消任务会请求上游取消;已被系统索引为成功的图片仍会按成功项结算扣费,其余冻结金额会释放。',
submittingNotice: '正在创建上游批量任务,通常需要几秒,请不要重复提交。',
modelNoReferenceImages: '当前模型不支持参考图。',
refLimitReached: '当前模型每条最多 {limit} 张参考图。',
refLimitExceededIgnored: '当前模型每条最多 {limit} 张参考图,已忽略超出的文件。',
refFormatUnsupported: '参考图仅支持 PNG、JPEG 或 WebP。',
refFileTooLarge: '{name} 超过 10MB,已忽略。',
},
guide: {
title: '批量生图使用说明',
uiTitle: '当前界面如何使用',
step1: '1. 选择已开启批量生图的 Gemini API Key,模型列表会按该 Key 所属分组可用模型展示。',
step2: '2. 任务名称可以留空,提交时会自动使用当前时间;Prompt 需要一条条添加到列表里,每条 Prompt 可附参考图,也可以设置重复生成张数。',
step3: '3. 提交后任务会先排队,明细会展示已提交的 Prompt;图片预览默认不加载,点击明细里的预览按钮才会加载单张图。',
step4: '4. 完成后可以下载 ZIP;部分失败时,更多菜单里可以只重试失败项。当前结算仍按成功输出图张数计算,不单独对参考图加价。',
skillTitle: '给 Codex 的 Skill 说明',
skillDesc: '用于告诉 Codex 如何代替用户整理 prompt、提交任务和下载结果。',
},
messages: {
loadKeysFailed: '加载 API Key 失败',
loadModelsFailed: '加载可用模型失败',
loadJobsFailed: '加载批量任务失败',
selectApiKey: '请选择可用的 Gemini API Key',
noModelsForKey: '当前密钥没有可用的批量生图模型',
selectModel: '请选择模型',
promptRequired: '请至少填写一条 prompt',
submitted: '批量任务已提交',
submitFailed: '提交失败',
refreshFailed: '刷新失败',
cancelConfirm: '取消会请求上游取消;已被系统索引为成功的图片仍会按成功项结算扣费,其余冻结金额会释放。确定取消吗?',
cancelled: '已请求取消任务',
cancelFailed: '取消失败',
batchDownloadStarted: '已开始下载选中的任务',
downloadFailed: '下载失败',
retrySubmitted: '已提交失败项重试任务',
retryFailed: '重试失败项失败',
retryMissingPrompts: '这个任务没有保存失败项 prompt,无法自动重试。请复制原 prompt 后重新创建任务。',
retryTaskNameSuffix: '重试失败项',
deleteConfirm: '删除后这个任务会从你的列表隐藏,但账务记录仍会保留。确定删除吗?',
deleteSelectedConfirm: '删除后选中的任务会从你的列表隐藏,但账务记录仍会保留。确定删除吗?',
deleted: '任务记录已删除',
deleteFailed: '删除任务记录失败',
loadItemsFailed: '加载明细失败',
loadPreviewFailed: '加载图片预览失败',
copiedInstruction: '已复制批量生图说明',
loadingModels: '加载可用模型中...',
noModels: '无可用模型',
noModelsHint: '当前密钥所属分组没有配置可用于批量生图的模型。',
noCompatibleAccount: '当前密钥所属分组没有可用的批量生图上游账号。请联系管理员检查:该分组是否绑定了可调度的 Gemini API Key 或 Vertex 服务账号,以及账号是否支持所选模型。',
unsupportedProvider: '这个任务使用的批量生图通道当前不可用。请联系管理员检查批量生图通道配置。',
providerSubmitFailed: '上游批量生图任务提交失败。请联系管理员检查上游账号状态、模型权限或服务状态。',
vertexGcsBucketMissing: 'Vertex 批量生图缺少托管 GCS 存储桶配置。请联系管理员配置 BATCH_IMAGE_VERTEX_MANAGED_GCS_BUCKET 后再提交。',
queueFailed: '任务队列暂时不可用,批量任务没有成功入队。请联系管理员检查队列服务。',
billingHoldFailed: '费用冻结失败,批量任务没有成功提交。请联系管理员检查余额冻结或计费服务。',
groupDisabled: '当前密钥所属分组没有开启批量生图。你可以换一个已开启批量生图的密钥,或联系管理员开启。',
pricingMissing: '所选模型还没有配置批量生图价格。请联系管理员补充价格配置。',
insufficientBalance: '余额不足,无法冻结本次批量生图费用。',
invalidModel: '请选择一个可用于当前密钥的批量生图模型。',
invalidItems: 'Prompt 列表格式不正确,请检查是否为空、是否超过数量限制,或图片尺寸是否仍为 1K。',
duplicateCustomId: 'Prompt 列表里的 custom_id 不能重复。',
promptTooLong: '单条 prompt 过长,请缩短后重试。',
invalidReferenceImage: '参考图格式不正确,请使用 10MB 以内的 PNG、JPEG 或 WebP。',
tooManyReferenceImages: '参考图数量超过限制:Flash Image 每条最多 3 张,Pro Image 每条最多 14 张,整组最多 1000 张。',
referenceImagesTooLarge: '参考图总量过大。inline 参考图整组最多 128MB;大量参考图请改用 gs:// file_uri 或拆分任务。',
tooManyOutputImages: '预计生成张数超过限制:每条最多 4 张,整组最多 200 张。',
idempotencyConflict: '这次提交和之前的请求标识冲突,请刷新页面后重新提交。',
notReady: '任务还没有完成,完成后才能下载。',
outputDeleted: '这个任务的结果文件已经被清理,无法下载。',
resultMissing: '结果文件不可用,可能是上游结果文件已清理、存储权限异常,或管理员迁移过存储配置。请联系管理员检查结果文件。',
itemFailed: '这条明细没有成功图片,无法预览。',
itemImageIndexOutOfRange: '这条明细没有可预览的图片。',
downloadLimited: '当前下载请求太多,请稍后再试。',
downloadTooLarge: '这个 ZIP 太大,已超过单次下载限制。请减少单次下载数量,或联系管理员调整批量下载上限。',
deleteNotReady: '任务结束后才能删除记录。正在生成或结算中的任务请先等待完成。',
disabled: '批量生图功能当前未开启。',
authRequired: '当前 API Key 不可用或已失效,请重新选择密钥。',
adminReference: '请把错误码和请求 ID 发给管理员排查。',
errorReference: '错误信息',
errorCodeRef: '错误码:{code}',
requestIdRef: '请求 ID{id}',
httpStatusRef: 'HTTP 状态:{status}',
},
},
}
@@ -0,0 +1,142 @@
/** Channel Monitor V2 (user + admin passive monitor UI) */
export default {
channelMonitorV2: {
title: '渠道监控',
updating: '正在更新数据',
updatedTo: '更新至 {time}',
partialCoverage: '部分历史覆盖',
bootstrap: {
title: '正在补齐历史监控数据',
description:
'首次启用被动监控时,系统会在后台静默聚合 90 分钟、24 小时、7 天与 30 天窗口;完成后可切换全部时间范围。',
progress: '进度 {percent}%',
working: '后台聚合中…',
},
timeRange: '时间范围',
clearFilters: '重置',
refreshingFilters: '筛选条件已变化,正在刷新矩阵、趋势和明细…',
switchingData: '正在切换筛选数据…',
summaryAria: '筛选范围整体汇总',
loadFailed: '渠道监控加载失败',
detailLoadFailed: '渠道监控明细加载失败',
otherModels: '其他模型',
ignored: '忽略',
currentUser: '当前用户',
ranges: { '90m': '90m', '24h': '24h', '7d': '7d', '30d': '30d' },
filters: {
platform: '平台', allPlatforms: '全部', group: '分组', allGroups: '全部', model: '模型', allModels: '全部',
empty: '暂无可选项', selectedCount: '{count} 项', labelValue: '{label}{value}'
},
groupBy: {
label: '展示维度', platform: '平台', platformGroup: '平台 / 分组', platformModel: '平台 / 模型', platformGroupModel: '平台 / 分组 / 模型'
},
trendView: { label: '趋势视图', pulse: '色块矩阵', line: '折线图' },
healthMode: { label: '健康显示', overall: '综合', success: '错误率', ttft: '首 Token', cache: '缓存率' },
tabs: { aria: '明细维度', models: '模型', errors: '错误原因', users: '用户排行' },
metrics: {
rpm: 'RPM',
tpm: 'TPM',
tps: '每秒 Token',
rpmDetail: '每分钟请求数',
tpmDetail: '每分钟 Token 数',
tpsDetail: '由 TPM ÷ 60 换算',
errorRate: '错误率',
ttft: '首 Token',
ttftP50: '首 Token P50',
durationP50: '请求时长 P50',
cacheRate: '缓存率',
cacheDetail: '读缓存占比',
successRate: '成功率',
successRateValue: '成功率 {value}',
errorRateValue: '错误率 {value}',
rpmValue: 'RPM {value}',
tpmValue: 'TPM {value}',
tpsValue: '每秒 Token {value}',
ttftValue: '首 Token {value}',
durationValue: '请求时长 {value}',
cacheRateValue: '缓存率 {value}',
},
table: { platformModel: '平台 / 模型', rank: '排名', user: '用户' },
empty: { title: '没有可展示的数据', description: '尝试调整时间范围或筛选条件' },
bucket: { minutes: '{count} 分钟粒度', hours: '{count} 小时粒度', days: '{count} 天粒度' },
matrix: {
title: '可用性趋势', description: '每行是一种渠道组合,每个色块代表一个统计区间;悬停查看明细', wheelZoom: '在色块上滚轮放大(区间变窄、色块变宽)', wheelZoomX: '在色块上滚轮放大(区间变窄、色块变宽)', dimension: '渠道维度', emptyTitle: '当前筛选窗口没有矩阵数据', legendAria: '健康分数图例', bad: '差', good: '好', healthyLegend: '健康 (≥80)', warningLegend: '需关注 (5079)', criticalLegend: '异常 (<50)', unknownLegend: '无流量 / 样本不足', noTraffic: '该区间无流量', noTrafficAt: '{time} · 无流量', scoreLine: '健康分 {score}', resetZoom: '重置缩放'
},
chart: {
title: '可用性趋势', description: '平滑趋势:错误率 · 首 Token P50 · 缓存率', emptyTitle: '当前筛选窗口没有趋势数据', errorLegend: '错误率(左轴 %', cacheLegend: '缓存率(左轴 %', ttftLegend: '首 Token P50(右轴)', errorDataset: '错误率趋势 %', cacheDataset: '缓存率趋势 %', ttftDataset: '首 Token 趋势 P50 (ms)', percentAxis: '比率 %', resetZoom: '重置缩放'
},
errorDetail: { http: 'HTTP {code}', upstream: '上游 {code}', noMessage: '无错误消息', empty: '仅展示分类占比(样本消息仅管理员可见)' },
errorCategories: {
content_policy: '内容策略', authentication: '认证失败', context_limit: '上下文超限', invalid_request: '请求格式', model_unsupported: '模型不支持', group_access: '分组权限', quota_or_balance: '额度或余额', account_pool_unavailable: '账号池不可用', rate_or_capacity: '限流或容量', timeout: '超时', transport_or_stream: '传输或流', upstream_forbidden: '上游拒绝', not_found: '资源不存在', client_cancelled: '客户端取消', upstream_5xx: '上游 5xx', internal: '内部错误', other: '其他'
},
rank: {
gold: '第 1 名 金',
silver: '第 2 名 银',
bronze: '第 3 名 铜',
place: '第 {n} 名',
unranked: '未上榜',
},
settings: {
title: 'V2 数据监控配置',
description:
'配置被动用量汇总维度(平台 / 模型 / 分组)与刷新频率。健康色与明细在用户端 /monitor 以比例、RPM/TPM 展示,不暴露绝对请求量。',
save: '保存',
loading: '加载中...',
loadFailed: 'V2 配置加载失败',
saveSuccess: 'V2 监控配置已保存',
saveFailed: 'V2 配置保存失败',
modeBanner:
'当前系统设置为 {mode}。V2 分钟聚合不会运行;此处配置可预先保存,切换到 {modeV2} 后立即生效。可在系统设置 → 功能开关调整。',
modeClosed: '渠道监控已关闭',
modeV1: 'V1 主动探测',
modeV2: 'V2 被动监控',
enableTitle: '启用 V2 汇总',
enableHint: '在系统模式为 V2 时生效;关闭后仅停止本配置的汇总,系统模式开关仍在「功能开关」',
refreshTitle: '汇总频率',
refreshHint: '影响矩阵时间粒度与刷新节奏',
refreshAria: '汇总频率',
platformsTitle: '平台与模型',
platformsHint: '留空 = 展示全部真实模型名;填写后仅名单内单独成行,其余归入「其他」',
modelsPlaceholder: '留空=全部真实模型;或填写主流模型名单(其余归其他)',
badgeAllModels: '全部模型',
badgeOther: '+ 其他',
groupsTitle: '监控分组',
groupsSelected: '已选择 {count} 个分组',
groupsAll: '全部分组',
groupsEmpty: '没有可选择的分组',
errorsTitle: '错误分类与忽略',
errorsHint:
'勾选「忽略」的类别不计入错误率与健康分,仍在错误原因列表中以灰色显示并标记忽略。未匹配的错误归入「其他」。',
ignoredSummary: '已忽略 {ignored} 类 · 计入错误率 {counted} 类',
healthTitle: '健康阈值',
healthHint: '控制用户端色块和整体评分。默认阈值较宽松,避免少量错误或低缓存率立即显示异常。',
fields: {
minimumSample: '最小样本数',
warningError: '错误率关注 %',
criticalError: '错误率异常 %',
targetTtft: 'TTFT 目标 ms',
warningTtft: 'TTFT 关注 ms',
criticalTtft: 'TTFT 异常 ms',
warningCache: '缓存率关注 %',
criticalCache: '缓存率异常 %',
},
namedModelsEmpty: '各平台模型列表为空:将展示全部真实模型名(不归入「其他」)。',
namedModelsCount: '将展示 {count} 个命名模型维度;名单外模型归入各平台「其他」。',
userContractTitle: '用户端展示约定',
userContract: {
health: '健康色三指标:错误率 60% + 首 Token P50 20% + 缓存率 20%(阈值可在上方配置)',
trend: '趋势可切换色块矩阵 / 折线图(错误率 · 缓存率 · 首 Token)',
latency: '延迟展示 AVG · P50 · P90;不展示绝对请求数 / 错误数',
models: '模型列表留空时展示真实模型名,不会全部归入「其他」',
},
},
admin: {
descriptionV1: '当前系统设置为 V1 主动探测:可管理监控项并立即检测;V2 聚合不会运行。',
descriptionV2: '当前系统设置为 V2 被动监控:配置聚合维度;V1 主动探测不会运行。',
tabAria: '监控管理',
tabV2: 'V2 数据监控配置',
tabV1Active: 'V1 主动探测',
tabV1History: 'V1 历史(当前模式未启用探测)',
},
},
}
+450
View File
@@ -0,0 +1,450 @@
export default {
common: {
loading: '加载中...',
submitting: '提交中...',
justNow: '刚刚',
peakRateTooltip: '高峰倍率:{window}',
peakRateImageNote: 'token 计费的图片 token 同样适用,图片按次计费不受高峰影响',
save: '保存',
saved: '保存成功',
deleted: '删除成功',
cancel: '取消',
delete: '删除',
edit: '编辑',
create: '创建',
update: '更新',
confirm: '确认',
reset: '重置',
search: '搜索',
filter: '筛选',
export: '导出',
import: '导入',
actions: '操作',
status: '状态',
name: '名称',
email: '邮箱',
password: '密码',
submit: '提交',
back: '返回',
next: '下一步',
yes: '是',
no: '否',
all: '全部',
none: '无',
selectAll: '全选',
noData: '暂无数据',
expand: '展开',
collapse: '收起',
success: '成功',
error: '错误',
critical: '严重',
warning: '警告',
info: '提示',
active: '启用',
inactive: '禁用',
more: '更多',
close: '关闭',
toggleMenu: '切换菜单',
userMenu: '用户菜单',
pageNotFound: '页面不存在',
enabled: '已启用',
disabled: '已禁用',
total: '总计',
balance: '余额',
availableBalance: '可用余额',
frozenBalance: '冻结金额',
totalBalance: '总余额',
available: '可用',
copiedToClipboard: '已复制到剪贴板',
copied: '已复制',
copyFailed: '复制失败',
verifying: '验证中...',
processing: '处理中...',
contactSupport: '联系客服',
add: '添加',
invalidEmail: '请输入有效的邮箱地址',
optional: '可选',
selectOption: '请选择',
searchPlaceholder: '搜索...',
noOptionsFound: '无匹配选项',
noGroupsAvailable: '无可用分组',
unknownError: '发生未知错误',
saving: '保存中...',
selectedCount: '(已选 {count} 个)',
refresh: '刷新',
autoRefresh: {
title: '自动刷新',
enable: '启用自动刷新',
countdown: '自动刷新: {seconds}s',
seconds: '{n} 秒',
},
view: '查看',
settings: '设置',
chooseFile: '选择文件',
upload: '上传',
remove: '移除',
noFileSelected: '未选择文件',
selectedFile: '已选:{name}',
fileReadFailed: '读取文件失败',
selectImageFile: '请选择图片文件',
fileTooLargeKb: '文件过大({size} KB),上限 {max} KB',
copy: '复制',
notAvailable: '不可用',
now: '现在',
today: '今天',
tomorrow: '明天',
unknown: '未知',
minutes: '分钟',
time: {
never: '从未',
justNow: '刚刚',
minutesAgo: '{n}分钟前',
hoursAgo: '{n}小时前',
daysAgo: '{n}天前',
countdown: {
daysHours: '{d}d {h}h',
hoursMinutes: '{h}h {m}m',
minutes: '{m}m',
withSuffix: '{time} 后解除'
}
}
},
adminCompliance: {
title: '部署与运营合规确认',
blockingNotice: '继续使用控制台前,须完成部署与运营合规确认。',
riskNotice: '本确认用于以清晰、显著、可留痕的方式提示自部署实例的合规义务与运营风险。',
version: '协议版本',
openDocument: '在 GitHub 查看协议文件',
documentSource: '协议正文来自本项目仓库中的 Markdown 文件。修改协议内容时必须同步递增协议版本;已确认的旧版本将失效,控制台使用者须重新确认。',
inputLabel: '请逐字输入以下确认短语',
inputPlaceholder: '输入确认短语以继续',
inputMismatch: '确认短语不匹配,请逐字输入提示内容。',
legalNote: '本确认用于明确自部署实例与开源项目、著作权人、贡献者及维护者之间的非关联关系和责任边界;部署、运营或控制相关实例的主体应独立承担其适用义务。',
logout: '退出登录',
accept: '确认并继续',
accepted: '合规确认已记录',
acceptFailed: '提交确认失败'
},
legal: {
loadFailed: '文档加载失败',
retryLater: '请稍后刷新页面重试。',
notFound: '文档不存在',
notFoundDescription: '当前条款文档不存在或已被管理员移除。',
updatedAt: '更新日期:{date}',
empty: '暂无正文内容',
loginAgreement: '登录条款',
adminCompliance: '部署与运营合规承诺',
loginAgreementPrompt: {
checkboxPrefix: '我已阅读并同意',
documentSeparator: '、',
noticeTitle: '继续登录前需要先同意最新条款。',
noticeDescription: '未同意前,账号密码输入和快捷登录会保持禁用。',
viewTerms: '查看条款',
dialogTitle: '条款更新通知',
dialogDescription: '我们的服务条款已于 {date} 更新。在继续使用服务之前,请仔细阅读并同意以下条款。',
recently: '近期',
relatedDocuments: '相关文档',
reject: '拒绝',
accept: '同意并继续',
loginRejectedWarning: '未同意最新条款前,无法输入账号密码或使用快捷登录。',
loginRequiredWarning: '请先阅读并同意最新条款后再登录。',
registerRejectedWarning: '未同意最新条款前,无法注册或使用快捷登录。',
registerRequiredWarning: '请先阅读并同意最新条款后再注册。'
}
},
// Navigation
nav: {
dashboard: '仪表盘',
announcements: '公告',
apiKeys: 'API 密钥',
batchImage: '批量生图',
usage: '使用记录',
redeem: '兑换',
affiliate: '邀请返利',
affiliateManagement: '邀请返利',
affiliateInviteRecords: '邀请记录',
affiliateRebateRecords: '返利记录',
affiliateTransferRecords: '提取记录',
profile: '个人资料',
users: '用户管理',
groups: '分组管理',
channels: '渠道管理',
availableChannels: '可用渠道',
modelPlaza: '模型广场',
subscriptions: '订阅管理',
accounts: '账号管理',
proxies: 'IP管理',
redeemCodes: '兑换码',
ops: '运维监控',
promoCodes: '优惠码',
settings: '系统设置',
myAccount: '我的账户',
lightMode: '浅色模式',
darkMode: '深色模式',
collapse: '收起',
expand: '展开',
logout: '退出登录',
github: 'GitHub',
mySubscriptions: '我的订阅',
buySubscription: '充值/订阅',
docs: '文档',
myOrders: '我的订单',
orderManagement: '订单管理',
paymentDashboard: '支付概览',
paymentConfig: '支付配置',
paymentPlans: '订阅套餐',
channelManagement: '渠道管理',
channelPricing: '渠道定价',
channelMonitor: '渠道监控',
channelStatus: '渠道状态',
riskControl: '风控中心',
securityAudit: '安全审计',
contentModeration: '内容审核',
promptAudit: '提示词审计',
auditLogs: '操作日志',
},
// Auth
auth: {
welcomeBack: '欢迎回来',
signInToAccount: '登录您的账户以继续',
signIn: '登录',
signingIn: '登录中...',
passkeySignIn: '使用 Passkey 登录',
passkeySigningIn: '正在等待 Passkey...',
passkeyCancelled: '已取消 Passkey 登录。',
passkeyFailed: 'Passkey 登录失败,请重试。',
createAccount: '创建账户',
signUpToStart: '注册以开始使用 {siteName}',
signUp: '注册',
processing: '处理中...',
continue: '继续',
rememberMe: '记住我',
dontHaveAccount: '还没有账户?',
alreadyHaveAccount: '已有账户?',
registrationDisabled: '注册功能暂时关闭,请联系管理员。',
emailLabel: '邮箱',
emailPlaceholder: '请输入邮箱',
passwordLabel: '密码',
passwordPlaceholder: '请输入密码',
createPasswordPlaceholder: '创建一个安全的密码',
passwordHint: '至少 6 个字符',
emailRequired: '请输入邮箱',
invalidEmail: '请输入有效的邮箱地址',
passwordRequired: '请输入密码',
passwordMinLength: '密码至少需要 6 个字符',
loginFailed: '登录失败,请检查您的凭据后重试。',
errors: {
USER_NOT_ACTIVE: '账号已被禁用',
},
registrationFailed: '注册失败,请重试。',
emailDomainRegistrationLimit:
'该邮箱域名无法注册新账户。请使用主流邮箱注册;如需使用企业邮箱,请联系客服添加域名白名单。',
emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。',
emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}',
emailSuffixAllowedMore: '等 {count} 项',
loginSuccess: '登录成功!欢迎回来。',
accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。',
reloginRequired: '会话已过期,请重新登录。',
turnstileExpired: '验证已过期,请重试',
turnstileFailed: '验证失败,请重试',
captchaVerified: '验证已完成',
captchaLoading: '正在加载验证码…',
captchaClickToVerify: '点击完成人机验证',
captchaVerifying: '验证中…',
completeVerification: '请完成验证',
verifyYourEmail: '验证您的邮箱',
sessionExpired: '会话已过期',
sessionExpiredDesc: '请返回注册页面重新开始。',
verificationCode: '验证码',
verificationCodeHint: '请输入发送到您邮箱的6位验证码',
sendingCode: '发送中...',
sendCode: '发送验证码',
clickToResend: '点击重新发送验证码',
resendCode: '重新发送验证码',
sendCodeDesc: '我们将发送验证码到',
codeSentSuccess: '验证码已发送!请查收您的邮箱。',
verifying: '验证中...',
verifyAndCreate: '验证并创建账户',
resendCountdown: '{countdown}秒后可重新发送',
backToRegistration: '返回注册',
sendCodeFailed: '发送验证码失败,请重试。',
verifyFailed: '验证失败,请重试。',
codeRequired: '请输入验证码',
invalidCode: '请输入有效的6位验证码',
promoCodeLabel: '优惠码',
promoCodePlaceholder: '输入优惠码(可选)',
promoCodeValid: '有效!注册后将获得 ${amount} 赠送余额',
promoCodeInvalid: '无效的优惠码',
promoCodeNotFound: '优惠码不存在',
promoCodeExpired: '此优惠码已过期',
promoCodeDisabled: '此优惠码已被禁用',
promoCodeMaxUsed: '此优惠码已达到使用上限',
promoCodeAlreadyUsed: '您已使用过此优惠码',
promoCodeValidating: '优惠码正在验证中,请稍候',
promoCodeInvalidCannotRegister: '优惠码无效,请检查后重试或清空优惠码',
invitationCodeLabel: '邀请码',
invitationCodePlaceholder: '请输入邀请码',
invitationCodeRequired: '请输入邀请码',
invitationCodeValid: '邀请码有效',
invitationCodeInvalid: '邀请码无效或已被使用',
invitationCodeValidating: '正在验证邀请码...',
invitationCodeInvalidCannotRegister: '邀请码无效,请检查后重试',
oauthOrContinue: '或使用其他继续',
linuxdo: {
signIn: '使用 Linux.do 登录',
orContinue: '或使用邮箱密码继续',
callbackTitle: '正在完成登录',
callbackProcessing: '正在验证登录信息,请稍候...',
callbackHint: '如果页面未自动跳转,请返回登录页重试。',
callbackMissingToken: '登录信息缺失,请返回重试。',
backToLogin: '返回登录',
invitationRequired: '该 Linux.do 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。',
invalidPendingToken: '注册凭证已失效,请重新使用 Linux.do 登录。',
completeRegistration: '完成注册',
completing: '正在完成注册...',
completeRegistrationFailed: '注册失败,请检查邀请码后重试。'
},
dingtalk: {
signIn: '钉钉登录',
callbackTitle: '正在完成钉钉登录',
callbackProcessing: '正在验证钉钉登录信息,请稍候...',
callbackHint: '如果页面未自动跳转,请返回登录页重试。',
callbackMissingToken: '登录信息缺失,请返回重试。',
backToLogin: '返回登录',
invitationRequired: '该钉钉账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。',
invalidPendingToken: '注册凭证已失效,请重新使用钉钉登录。',
completeRegistration: '完成注册',
completing: '正在完成注册...',
completeRegistrationFailed: '注册失败,请检查邀请码后重试。',
createAccountTitle: '创建钉钉账户',
registrationDisabledRedirectToBind: '当前已禁止注册新账户,请使用已有账户邮箱和密码绑定钉钉登录',
error: {
title: '钉钉登录失败',
csrf: '登录会话已过期,请重新扫码登录',
corp_rejected: '您的钉钉账号不属于本企业,请联系管理员',
dingtalk_not_enabled: '钉钉登录暂未启用',
upstream_error: '钉钉服务暂时不可用,请稍后重试',
missing_browser_session: '浏览器会话丢失,请重新登录',
missing_params: '请求参数不完整',
invalid_state: '登录状态异常',
provider_error: '钉钉授权失败',
session_error: '会话创建失败,请重试',
retry: '重新登录'
}
},
emailOAuth: {
signIn: '使用 {providerName} 登录'
},
oidc: {
signIn: '使用 {providerName} 登录',
callbackTitle: '正在完成 {providerName} 登录',
callbackProcessing: '正在验证 {providerName} 登录信息,请稍候...',
callbackHint: '如果页面未自动跳转,请返回登录页重试。',
callbackMissingToken: '登录信息缺失,请返回重试。',
backToLogin: '返回登录',
invitationRequired: '该 {providerName} 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。',
invalidPendingToken: '注册凭证已失效,请重新登录。',
completeRegistration: '完成注册',
completing: '正在完成注册...',
completeRegistrationFailed: '注册失败,请检查邀请码后重试。'
},
oauthFlow: {
profileDetailsTitle: '使用 {providerName} 资料',
profileDetailsDescription: '选择是否将 {providerName} 的昵称或头像应用到当前账户。',
useDisplayName: '使用昵称',
useAvatar: '使用头像',
avatarAlt: '{providerName} 头像',
reviewProfileBeforeContinue: '请先确认 {providerName} 资料后再继续。',
chooseHowToContinue: '选择后续操作',
chooseAccountActionHint: '请选择绑定已有账户,或创建一个新账户。',
suggestedEmail: '建议邮箱:{email}',
bindExistingAccount: '绑定已有账户',
createNewAccount: '创建新账户',
createAccountHint: '请输入邮箱地址以创建账户并继续。',
bindLoginHint: '登录一个已有账户以绑定此次 {providerName} 登录。',
signInThenBindDescription: '请先登录已有账户,再将此次 {providerName} 登录绑定到该账户。',
bindSignInToExistingAccount: '将此次 {providerName} 登录绑定到已有账户。',
bindCurrentAccountTitle: '绑定当前账户',
bindCurrentAccountDescription: '将此次 {providerName} 登录绑定到当前浏览器已登录的账户。',
bindCurrentAccount: '绑定当前账户',
logInAndBind: '登录并绑定',
useDifferentEmail: '使用其他邮箱',
backToOptions: '返回选项',
yourAccount: '当前账户',
totpHint: '请输入 {account} 的 6 位验证码,以完成此次 {providerName} 登录绑定。',
verifyAndContinue: '验证并继续',
wechatAvailabilityUnknown: '暂时无法确认微信登录可用性,请刷新后重试。',
wechatSystemBrowserOnly: '当前微信登录流程仅支持在系统浏览器中继续。',
wechatBrowserOnly: '当前微信登录流程仅支持在微信内置浏览器中继续。',
wechatNotConfigured: '微信登录尚未配置。'
},
linuxdoCallbackPageTitle: 'LinuxDo 登录回调',
dingtalkCallbackPageTitle: '钉钉登录回调',
dingtalkProviderName: '钉钉',
oidcCallbackPageTitle: 'OIDC 登录回调',
oauthCallbackPageTitle: 'OAuth 回调',
wechatProviderName: '微信',
wechatCallbackPageTitle: '微信登录回调',
wechatPaymentCallbackPageTitle: '微信支付回调',
wechatPayment: {
callbackTitle: '正在恢复微信支付',
callbackProcessing: '正在恢复微信支付...',
backToPayment: '返回支付页',
callbackMissingResumeToken: '微信支付回调缺少恢复令牌。'
},
oauth: {
callbackTitle: 'OAuth 回调',
callbackHint: '按需将授权码和状态值复制回后台授权流程。',
invalidCallbackTitle: '无效的登录回调',
invalidCallbackHint: '当前页面缺少有效的授权结果,请返回登录页重新发起快捷登录。',
code: '授权码',
state: '状态',
fullUrl: '完整URL'
},
// 忘记密码
forgotPassword: '忘记密码?',
forgotPasswordTitle: '重置密码',
forgotPasswordHint: '输入您的邮箱地址,我们将向您发送密码重置链接。',
sendResetLink: '发送重置链接',
sendingResetLink: '发送中...',
sendResetLinkFailed: '发送重置链接失败,请重试。',
resetEmailSent: '重置链接已发送',
resetEmailSentHint:
'如果该邮箱已注册,您将很快收到密码重置链接。请检查您的收件箱和垃圾邮件文件夹。',
backToLogin: '返回登录',
rememberedPassword: '想起密码了?',
// 重置密码
resetPasswordTitle: '设置新密码',
resetPasswordHint: '请在下方输入您的新密码。',
newPassword: '新密码',
newPasswordPlaceholder: '输入新密码',
confirmPassword: '确认密码',
confirmPasswordPlaceholder: '再次输入新密码',
confirmPasswordRequired: '请确认您的密码',
passwordsDoNotMatch: '两次输入的密码不一致',
resetPassword: '重置密码',
resettingPassword: '重置中...',
resetPasswordFailed: '重置密码失败,请重试。',
passwordResetSuccess: '密码重置成功',
passwordResetSuccessHint: '您的密码已重置。现在可以使用新密码登录。',
invalidResetLink: '无效的重置链接',
invalidResetLinkHint: '此密码重置链接无效或已过期。请重新请求一个新链接。',
requestNewResetLink: '请求新的重置链接',
invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。'
},
// Step-up(敏感操作二次验证)
stepUp: {
title: '需要二次验证',
hint: '请输入身份验证器应用中的 6 位验证码以继续此敏感操作。',
verifyFailed: '验证失败,请重试',
notEnabled: '此操作需要开启二次验证,请先在个人资料中启用 TOTP。',
adminApiKeyForbidden: '管理 API Key 无法执行此操作,请使用已通过二次验证的管理员会话。'
},
// Dashboard
}
+965
View File
@@ -0,0 +1,965 @@
export default {
dashboard: {
title: '仪表盘',
welcomeMessage: '欢迎回来!这是您账户的概览。',
balance: '余额',
apiKeys: 'API 密钥',
todayRequests: '今日请求',
todayCost: '今日消费',
todayTokens: '今日 Token',
totalTokens: '累计 Token',
cacheToday: '今日缓存',
performance: '性能指标',
avgResponse: '平均响应',
averageTime: '平均时间',
timeRange: '时间范围',
granularity: '粒度',
day: '按天',
hour: '按小时',
modelDistribution: '模型分布',
groupDistribution: '分组使用分布',
platformBreakdown: '按平台拆分',
platformBreakdownEmpty: '暂无平台用量',
platformCount: '{count} 个平台',
platformOther: '其他',
platformQuota: {
title: '配额用量',
daily: '日',
weekly: '周',
monthly: '月(近30天)',
resetsAt: '{time} 重置',
noLimit: '不限制',
disabled: '已禁用',
},
tokenUsageTrend: 'Token 使用趋势',
noDataAvailable: '暂无数据',
model: '模型',
group: '分组',
noGroup: '无分组',
requests: '请求',
tokens: 'Token',
actual: '实际',
standard: '标准',
input: '输入',
output: '输出',
cache: '缓存',
recentUsage: '最近使用',
last7Days: '近 7 天',
noUsageRecords: '暂无使用记录',
startUsingApi: '开始使用 API 后,您的使用历史将显示在这里。',
viewAllUsage: '查看全部',
quickActions: '快捷操作',
createApiKey: '创建 API 密钥',
generateNewKey: '生成新的 API 密钥',
batchImageAgent: '批量生图助手',
batchImageAgentDesc: '复制给 Agent 的任务说明',
viewUsage: '查看使用记录',
checkDetailedLogs: '查看详细的使用日志',
redeemCode: '兑换码',
addBalanceWithCode: '使用兑换码充值'
},
// Groups (shared)
groups: {
subscription: '订阅'
},
// API Keys
keys: {
title: 'API 密钥',
description: '管理您的 API 密钥和访问令牌',
searchPlaceholder: '搜索名称或Key...',
endpoints: {
title: 'API 端点',
default: '默认',
copied: '已复制',
copiedHint: '已复制到剪贴板',
clickToCopy: '点击可复制此端点',
speedTest: '测速',
},
allGroups: '全部分组',
allStatus: '全部状态',
columnSettings: '列设置',
columnAlwaysVisible: '该列固定显示,不可隐藏',
createKey: '创建密钥',
editKey: '编辑密钥',
deleteKey: '删除密钥',
deleteConfirmMessage: "确定要删除 '{name}' 吗?此操作无法撤销。",
id: 'ID',
apiKey: 'API 密钥',
group: '分组',
currentConcurrency: '当前并发',
noGroup: '无分组',
searchGroup: '搜索分组...',
noGroupFound: '未找到匹配的分组',
created: '创建时间',
copyToClipboard: '复制到剪贴板',
copied: '已复制!',
importToCcSwitch: '导入到 CCS',
enable: '启用',
disable: '禁用',
nameLabel: '名称',
namePlaceholder: '我的 API 密钥',
groupLabel: '分组',
selectGroup: '选择分组',
statusLabel: '状态',
selectStatus: '选择状态',
saving: '保存中...',
noKeysYet: '暂无 API 密钥',
createFirstKey: '创建您的第一个 API 密钥以开始使用 API。',
keyCreatedSuccess: 'API 密钥创建成功',
keyUpdatedSuccess: 'API 密钥更新成功',
keyDeletedSuccess: 'API 密钥删除成功',
keyEnabledSuccess: 'API 密钥已启用',
keyDisabledSuccess: 'API 密钥已禁用',
failedToLoad: '加载 API 密钥失败',
failedToSave: '保存 API 密钥失败',
failedToDelete: '删除 API 密钥失败',
failedToUpdateStatus: '更新 API 密钥状态失败',
clickToChangeGroup: '点击更换分组',
groupChangedSuccess: '分组更换成功',
failedToChangeGroup: '更换分组失败',
groupRequired: '请选择分组',
usage: '用量',
today: '今日',
total: '近30天',
quota: '额度',
lastUsedAt: '上次使用时间',
lastUsedIP: '最近使用 IP',
useKey: '使用密钥',
useKeyModal: {
title: '使用 API 密钥',
description: '将以下环境变量添加到您的终端配置文件或直接在终端中运行。',
copy: '复制',
copied: '已复制',
note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
claudeSettingsHint: '用户级持久配置。此文件包含 API 密钥,请勿提交到项目仓库。',
noGroupTitle: '请先分配分组',
noGroupDescription:
'此 API 密钥尚未分配分组,请先在密钥列表中点击分组列进行分配,然后才能查看使用配置。',
openai: {
description: '将以下配置文件添加到 Codex CLI 配置目录中。',
authModeTitle: 'Codex 认证模式',
authModeDescription: '兼容模式保留旧版 Codex 配置;API Key Mode 用于授权客户端图片执行器。',
authModeLegacy: '兼容模式',
authModeApiKey: 'API Key Mode',
authModeApiKeyRestartNotice: '保存此配置后,必须完全退出并重启 Codex Desktop 或 CLI,然后新建 task,让客户端重新构建工具注册表。',
configTomlHint: '请确保以下内容位于 config.toml 文件的开头部分',
note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。',
noteWindows:
'按 Win+R,输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。'
},
cliTabs: {
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
codexCli: 'Codex CLI',
codexCliWs: 'Codex CLI (WebSocket)',
grokCli: 'Grok CLI',
opencode: 'OpenCode'
},
antigravity: {
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
claudeNote:
'这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
geminiNote:
'这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。'
},
gemini: {
description:
'将以下环境变量添加到您的终端配置文件或直接在终端中运行,以配置 Gemini CLI 访问。',
modelComment: '如果你有 Gemini 3 权限可以填:gemini-3-pro-preview',
note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。'
},
grok: {
description:
'配置 Grok CLI、Claude Code、Codex 或 OpenCode,让请求通过当前 Sub2API Grok 分组发送。文本模型走 Responses;图片/视频使用 Imagine 模型 ID 与媒体端点。',
claudeDescription: '配置 Claude Code,让 Messages API 请求通过当前 Sub2API Grok 分组发送。',
codexDescription: '配置 Codex,让 Responses API 请求通过当前 Sub2API Grok 分组发送。',
configTomlHint:
'官方路径:~/.grok/config.toml(或 $GROK_HOME)。请填写 [endpoints]models_base_url / models_list_url / xai_api_base_url / cli_chat_proxy_base_url)、[auth] preferred_method=api_key、[models]、[session]、[features] 图片/视频覆盖。优先 env_key,勿硬编码 api_key;文本模型必须 api_backend=responses。合并前备份,保存后运行 grok inspect。',
codexConfigTomlHint:
'Codex 官方:wire_api 仅支持 "responses";优先 env_key,勿与 experimental_bearer_token 混用;非 OpenAI 网关默认 supports_websockets = falseSub2API 仍可接客户端 WS 并桥接到 HTTP/SSE)。合并前备份 ~/.codex/config.toml。',
note:
'导出 GROK_MODELS_BASE_URL 与 XAI_API_KEY,将完整 config.tomlendpoints/auth/models/session/features)保存为 ~/.grok/config.toml,运行 grok inspect,再用 /model 选择 grok-4.5(编程场景可用 grok-build-0.1)。',
noteWindows:
'设置 GROK_MODELS_BASE_URL 与 XAI_API_KEY,将完整 config.toml 保存为 %USERPROFILE%\\.grok\\config.toml,运行 grok inspect,再用 /model 选择 grok-4.5(编程场景可用 grok-build-0.1)。',
claudeNote:
'二选一:终端环境变量仅当前会话;~/.claude/settings.json 可持久化。请勿把含 API Key 的文件提交到仓库。',
codexNote:
'导出 SUB2API_API_KEY,将 config.toml 保存到 ~/.codex(可用 mkdir -p ~/.codex)。优先 env_key,勿提交密钥。',
codexNoteWindows:
'设置 $env:SUB2API_API_KEY,将 config.toml 保存到 %USERPROFILE%\\.codex。优先 env_key,勿提交密钥。'
},
opencode: {
title: 'OpenCode 配置示例',
subtitle: 'opencode.json',
hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provideropenai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。'
}
},
customKeyLabel: '自定义密钥',
customKeyPlaceholder: '输入自定义密钥(至少16个字符)',
customKeyHint: '仅允许字母、数字、下划线和连字符,最少16个字符。',
customKeyTooShort: '自定义密钥至少需要16个字符',
customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符',
customKeyRequired: '请输入自定义密钥',
ipRestriction: 'IP 限制',
ipWhitelist: 'IP 白名单',
ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8',
ipWhitelistHint: '每行一个 IP 或 CIDR,设置后仅允许这些 IP 使用此密钥',
ipBlacklist: 'IP 黑名单',
ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16',
ipBlacklistHint: '每行一个 IP 或 CIDR,这些 IP 将被禁止使用此密钥',
ipRestrictionEnabled: '已配置 IP 限制',
ccSwitchNotInstalled:
'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。',
ccsClientSelect: {
title: '选择客户端',
description: '请选择您要导入到 CC-Switch 的客户端类型:',
claudeCode: 'Claude Code',
claudeCodeDesc: '导入为 Claude Code 配置',
geminiCli: 'Gemini CLI',
geminiCliDesc: '导入为 Gemini CLI 配置'
},
// 配额和有效期
quotaLimit: '额度限制',
quotaAmount: '额度金额 (USD)',
quotaAmountPlaceholder: '输入 USD 额度限制',
quotaAmountHint: '设置此密钥可消费的最大金额。0 = 无限制。',
quotaUsed: '已用额度',
reset: '重置',
resetQuotaUsed: '将已用额度重置为 0',
resetQuotaTitle: '确认重置额度',
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: '重置速率限制失败',
resetNow: '即将重置',
expiration: '密钥有效期',
expiresInDays: '{days} 天',
extendDays: '+{days} 天',
customDate: '自定义',
expirationDate: '过期时间',
expirationDateHint: '选择此 API 密钥的过期时间。',
currentExpiration: '当前过期时间',
expiresAt: '过期时间',
noExpiration: '永久有效',
status: {
active: '活跃',
inactive: '已停用',
quota_exhausted: '额度耗尽',
expired: '已过期'
}
},
// Usage
usage: {
title: '使用记录',
description: '查看和分析您的 API 使用历史',
costDetails: '费用明细',
tokenDetails: 'Token 明细',
cacheTtlOverriddenHint: '缓存 TTL Override 已启用',
cacheTtlOverriddenLabel: 'TTL 替换',
cacheTtlOverridden5m: '按 5m 计费',
cacheTtlOverridden1h: '按 1h 计费',
totalRequests: '总请求数',
totalTokens: '总 Token',
cacheTotal: '缓存',
cacheBreakdown: '缓存 Token 明细',
cacheCreationTokensLabel: '缓存创建',
cacheReadTokensLabel: '缓存读取',
totalCost: '总消费',
standardCost: '标准',
actualCost: '实际',
accountCost: '成本',
userBilled: '用户扣费',
accountBilled: '账号计费',
resetNow: '现在',
resetPending: '待刷新',
accountMultiplier: '账号倍率',
avgDuration: '平均耗时',
inSelectedRange: '所选范围内',
perRequest: '每次请求',
apiKeyFilter: 'API 密钥',
allApiKeys: '全部密钥',
timeRange: '时间范围',
exportCsv: '导出 CSV',
exportExcel: '导出 Excel',
exportingProgress: '正在导出数据...',
exportedCount: '已导出 {current}/{total} 条',
estimatedTime: '预计剩余时间:{time}',
cancelExport: '取消导出',
exportCancelled: '导出已取消',
exporting: '导出中...',
preparingExport: '正在准备导出...',
model: '模型',
requestedModel: '请求',
upstreamModel: '上游',
sentUpstreamModel: '发往上游',
upstreamResponseModel: '上游响应',
upstreamModelMismatch: '上游响应模型不一致',
modelVariant: '疑似版本变体',
modelMismatch: '模型不一致',
reasoningEffort: '推理强度',
endpoint: '端点',
endpointDistribution: '端点分布',
inbound: '入站',
upstream: '上游',
mapping: '映射',
path: '路径',
inboundEndpoint: '入站端点',
upstreamEndpoint: '上游端点',
type: '类型',
tokens: 'Token',
cost: '费用',
firstToken: '首 Token',
duration: '耗时',
latency: '延迟',
latencyFirstToken: '首字',
latencyDuration: '总耗时',
time: '时间',
ws: 'WS',
stream: '流式',
sync: '同步',
cyber: '安全策略',
live: 'Live',
unknown: '未知',
in: '输入',
out: '输出',
cacheHit: '缓存命中',
cacheCreate: '缓存创建',
cacheHitRate: '缓存命中率',
inputTokenPrice: '输入单价',
outputTokenPrice: '输出单价',
perMillionTokens: '/ 1M Token',
unitPrice: '单次价格',
imageUnitPrice: '单张价格',
imageTotalPrice: '图片总价',
imageCount: '图片张数',
imageBillingSize: '计费尺寸',
imageInputSize: '输入尺寸',
imageOutputSize: '输出尺寸',
imageInputTokens: '图片输入 Token',
imageInputTokenPrice: '图片输入单价',
imageInputCost: '图片输入费用',
imageOutputTokens: '图片输出 Token',
imageOutputTokenPrice: '图片输出单价',
imageOutputCost: '图片输出费用',
imageSizeSource: '尺寸来源',
imageSizeBreakdown: '尺寸明细',
imageSizeSourceOutput: '上游输出',
imageSizeSourceInput: '请求输入',
imageSizeSourceDefault: '默认计费档位',
imageSizeSourceLegacy: '历史记录',
imageSizeSourceMissing: '未记录',
imageSizeNotRecorded: '未记录',
imageSizeLegacyUnstandardized: '历史非标准',
imageSizeUnknown: '未知',
cacheRead: '读取',
cacheWrite: '写入',
serviceTier: '服务档位',
serviceTierPriority: 'Fast',
serviceTierFlex: 'Flex',
serviceTierStandard: 'Standard',
rate: '倍率',
original: '原始',
billed: '计费',
noRecords: '未找到使用记录,请尝试调整筛选条件。',
failedToLoad: '加载使用记录失败',
noDataToExport: '没有可导出的数据',
exportSuccess: '使用数据导出成功',
exportFailed: '使用数据导出失败',
exportExcelSuccess: '使用数据导出成功(Excel格式)',
exportExcelFailed: '使用数据导出失败',
imageUnit: '张',
userAgent: 'User-Agent',
ipGeo: {
fetch: '获取地区',
fetching: '获取中...',
failed: '获取失败',
private: '内网地址',
refreshTitle: '刷新地区信息',
batchFetch: '批量获取地区',
batchFetching: '获取中...',
pending: '{count} 个 IP 待获取地区',
batchFailed: '批量获取地区信息失败',
detailOrg: '运营商',
detailTimezone: '时区',
detailAccuracy: '定位精度',
detailCoordinates: '坐标',
},
tabs: { usage: '用量明细', errors: '错误请求', ranking: '用户排行' },
errors: {
time: '时间', model: '模型', endpoint: '端点', status: '状态码',
category: '分类', platform: '平台', message: '错误信息',
keyName: 'Key 名称', keyDeleted: '已删除', allKeys: '全部 Key',
modelPlaceholder: '搜索模型', allCategories: '全部分类', allStatuses: '全部状态码',
empty: '暂无错误请求', failedToLoad: '加载错误请求失败',
categories: {
auth: '认证失败', rate_limit: '限流', quota: '余额/订阅',
invalid_request: '参数错误', service_unavailable: '服务暂时不可用',
upstream: '上游错误', internal: '平台错误', other: '其他', cyber: '安全策略',
},
detail: {
title: '错误请求详情',
responseBody: '上游响应内容',
upstreamStatus: '上游状态码',
loadFailed: '加载详情失败,请稍后重试',
},
},
},
// Shared keys for channel monitor (admin + user views)
monitorCommon: {
status: {
operational: '正常',
degraded: '降级',
failed: '失败',
error: '错误',
unknown: '-'
},
providers: {
openai: 'OpenAI',
anthropic: 'Anthropic',
gemini: 'Gemini',
grok: 'Grok',
antigravity: 'Antigravity',
kimi: 'Kimi',
zhipu: '智谱 GLM',
deepseek: 'DeepSeek'
},
// 检查模式(监控条目的工作方式)
checkMode: {
probe: '探活',
quota: '配额',
quota_probe: '探活 + 配额'
},
// 配额快照展示(MonitorQuotaView,管理端与用户端共用)
quota: {
unavailable: '配额信息不可用',
resetSoon: '即将重置',
windows: {
'5h': '5 小时',
'7d': '7 天',
'7dSonnet': '7 天 Sonnet',
'7dFable': '7 天 Fable',
weekly: '周',
daily: '日',
'30d': '30 天',
total: '总量'
},
labels: {
requests: '请求',
tokens: 'Token',
shared: '共享',
pro: 'Pro',
flash: 'Flash'
}
},
extraModelsHeader: '附加模型',
extraModelsEmpty: '无附加模型',
latencyEmpty: '-',
availabilityPrefix: '可用性',
dialogLatency: '对话延迟',
endpointPing: '端点 PING',
history60pts: '近 {n} 次记录',
nextUpdateIn: '{n}s 后刷新',
past: 'PAST',
now: 'NOW',
maintenancePaused: '维护中 · 已暂停时间线采集',
extraModelsCount: '+ {n} 模型',
pollEvery: '{n}s 轮询',
updatedAt: '更新于 {time}',
relativeSecondsAgo: '{n} 秒前',
relativeMinutesAgo: '{n} 分钟前',
relativeHoursAgo: '{n} 小时前',
relativeDaysAgo: '{n} 天前'
},
// Channel Status (user-facing read-only view)
channelStatus: {
title: '渠道状态',
description: '查看渠道可用性、延迟和近期状态',
searchPlaceholder: '搜索渠道...',
allProviders: '全部供应商',
loadError: '加载渠道状态失败',
detailLoadError: '加载渠道详情失败',
detailTitle: '渠道详情',
closeDetail: '关闭',
windowTab: {
'7d': '7 天',
'15d': '15 天',
'30d': '30 天'
},
overall: {
operational: 'OPERATIONAL',
degraded: 'DEGRADED',
unavailable: 'UNAVAILABLE'
},
columns: {
name: '名称',
provider: '供应商',
groupName: '分组',
primaryModel: '主模型',
availability7d: '7 天可用率',
latency: '延迟 (ms)'
},
detailColumns: {
model: '模型',
latestStatus: '最新状态',
latestLatency: '最新延迟 (ms)',
availability7d: '7 天可用率',
availability15d: '15 天可用率',
availability30d: '30 天可用率',
avgLatency7d: '7 天平均延迟 (ms)'
},
empty: {
title: '暂无可显示的渠道',
description: '管理员尚未配置可监控的渠道。'
}
},
// Available Channels (user-facing)
availableChannels: {
title: '可用渠道',
description: '查看您可访问的渠道与其支持的模型、定价',
searchPlaceholder: '搜索渠道或模型...',
empty: '暂无可用渠道',
noModels: '未配置模型',
noPricing: '未配置定价',
exclusive: '专属',
public: '公开',
exclusiveTooltip: '管理员授权给你的专属分组',
publicTooltip: '对所有用户公开的分组',
columns: {
name: '渠道名',
description: '描述',
platform: '平台',
groups: '我可访问的分组',
supportedModels: '支持模型'
},
pricing: {
billingMode: '计费模式',
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
billingModeVideo: '按视频',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
cacheReadPrice: '缓存读取',
imageInputPrice: '图片输入',
imageOutputPrice: '图片输出',
perRequestPrice: '每次请求',
intervals: '阶梯定价',
unitPerMillion: '/ 1M token',
unitPerRequest: '/ 次'
}
},
// Model Plaza (public group/model pricing showcase)
modelPlaza: {
title: '模型广场',
description: '按分组浏览可用模型与价格',
loading: '加载中...',
empty: '暂无可展示的分组',
loadFailed: '加载模型广场失败',
noSearchResult: '没有匹配的模型',
anonymousHint: '登录后可查看你的专属分组与专属倍率',
filters: {
platformLabel: '平台',
groupLabel: '分组',
rateLabel: '倍率',
modelLabel: '模型',
searchPlaceholder: '搜索模型名称',
all: '全部'
},
badges: {
exclusive: '专属分组',
subscription: '订阅'
},
detail: {
noModels: '该分组暂未配置模型',
noPricing: '未配置定价',
peakNote: '高峰时段 {window} 计费倍率 ×{multiplier}'
},
table: {
model: '模型',
input: '输入',
output: '输出',
cache: '缓存',
cacheWrite: '写入',
cacheRead: '读取',
paidPrice: '实付价格(折后)',
officialPrice: '官方价格',
rate: '折扣倍率',
unitPerMillion: '$ / 1M token',
perUnitRequest: '/ 次',
perUnitImage: '/ 张',
perRequest: '按次计费',
perImage: '按图片计费'
},
nav: {
login: '登录',
backToDashboard: '回到后台'
}
},
affiliate: {
title: '邀请返利',
description: '邀请新用户注册,并将返利额度转入账户余额',
yourCode: '我的邀请码',
inviteLink: '邀请链接',
copyCode: '复制邀请码',
copyLink: '复制链接',
codeCopied: '邀请码已复制',
linkCopied: '邀请链接已复制',
loadFailed: '加载邀请返利数据失败',
transferFailed: '转入余额失败',
stats: {
rebateRate: '我的返利比例',
rebateRateHint: '被邀请用户每次充值后你可获得的返利比例',
invitedUsers: '邀请人数',
availableQuota: '可转返利额度',
frozenQuota: '冻结中',
frozenQuotaHint: '新产生的返利正在冻结期中',
totalQuota: '历史返利额度'
},
transfer: {
title: '返利额度转余额',
description: '将当前可用返利额度一键转入账户余额',
button: '转入余额',
transferring: '转入中...',
empty: '当前没有可转入额度',
success: '已转入余额:{amount}'
},
invitees: {
title: '已邀请用户',
empty: '暂无邀请记录',
columns: {
email: '邮箱',
username: '用户名',
rebate: '返利明细',
joinedAt: '注册时间'
}
},
tips: {
title: '使用说明',
line1: '将邀请码或邀请链接分享给新用户。',
line2: '被邀请用户充值后,你可获得 {rate} 的返利额度。',
line3: '返利额度可随时转入账户余额。',
line4: '新产生的返利需要经过冻结期后才能提现。'
}
},
// Redeem
redeem: {
title: '兑换码',
description: '输入兑换码以充值余额或增加并发数',
currentBalance: '当前余额',
concurrency: '并发数',
requests: '请求',
redeemCodeLabel: '兑换码',
redeemCodePlaceholder: '请输入兑换码',
redeemCodeHint: '兑换码区分大小写',
redeeming: '兑换中...',
redeemButton: '兑换',
redeemSuccess: '兑换成功!',
redeemFailed: '兑换失败',
added: '已添加',
concurrentRequests: '并发请求',
newBalance: '新余额',
newConcurrency: '新并发数',
aboutCodes: '关于兑换码',
codeRule1: '每个兑换码只能使用一次',
codeRule2: '兑换码可以增加余额、并发数或试用权限',
codeRule3: '如有兑换问题,请联系客服',
codeRule4: '余额和并发数即时更新',
recentActivity: '最近活动',
historyWillAppear: '您的兑换历史将显示在这里',
balanceAddedRedeem: '余额充值(兑换)',
balanceAddedAffiliate: '余额充值(返利转入)',
balanceAddedAdmin: '余额充值(管理员)',
balanceDeductedAdmin: '余额扣除(管理员)',
concurrencyAddedRedeem: '并发增加(兑换)',
concurrencyAddedAdmin: '并发增加(管理员)',
concurrencyReducedAdmin: '并发减少(管理员)',
adminAdjustment: '管理员调整',
subscriptionAssigned: '订阅已分配',
subscriptionAssignedDesc: '您已获得 {groupName} 的访问权限',
subscriptionDays: '{days} 天',
days: '天',
codeRedeemSuccess: '兑换成功!',
failedToRedeem: '兑换失败,请检查兑换码后重试。',
subscriptionRefreshFailed: '兑换成功,但订阅状态刷新失败。',
pleaseEnterCode: '请输入兑换码'
},
// Profile
profile: {
title: '个人设置',
description: '管理您的账户信息和设置',
accountBalance: '账户余额',
concurrencyLimit: '并发限制',
rpmLimit: 'RPM 限制',
rpmUnlimited: '不限制',
memberSince: '注册时间',
overviewTitle: '账户总览',
overviewDescription: '快速查看账号状态、资料来源与常用设置。',
basicsTitle: '资料与头像',
basicsDescription: '维护公开展示信息,并保持头像与昵称风格一致。',
linkedProfileSources: '资料来源',
linkedProfileSourcesDescription: '部分头像和昵称可能同步自第三方登录方式。',
securityTitle: '安全设置',
securityDescription: '密码、双因素认证和通知提醒集中放在右侧。',
administrator: '管理员',
user: '用户',
username: '用户名',
email: '邮箱',
status: '状态',
role: '角色',
enterUsername: '输入用户名',
editProfile: '编辑个人资料',
updateProfile: '更新资料',
updating: '更新中...',
updateSuccess: '资料更新成功',
updateFailed: '资料更新失败',
usernameRequired: '用户名不能为空',
changePassword: '修改密码',
currentPassword: '当前密码',
newPassword: '新密码',
confirmNewPassword: '确认新密码',
passwordHint: '密码至少需要 8 个字符',
changingPassword: '修改中...',
changePasswordButton: '修改密码',
passwordsNotMatch: '两次输入的密码不一致',
passwordTooShort: '密码至少需要 8 个字符',
passwordChangeSuccess: '密码修改成功',
passwordChangeFailed: '密码修改失败',
// TOTP 2FA
totp: {
title: '双因素认证 (2FA)',
description: '使用 Google Authenticator 等应用增强账户安全',
enabled: '已启用',
enabledAt: '启用时间',
notEnabled: '未启用',
notEnabledHint: '启用双因素认证可以增强账户安全性',
enable: '启用',
disable: '禁用',
featureDisabled: '功能未开放',
featureDisabledHint: '管理员尚未开放双因素认证功能',
setupTitle: '设置双因素认证',
setupStep1: '使用认证器应用扫描下方二维码',
setupStep2: '输入应用显示的 6 位验证码',
manualEntry: '无法扫码?手动输入密钥:',
enterCode: '输入 6 位验证码',
verify: '验证',
setupFailed: '获取设置信息失败',
verifyFailed: '验证码错误,请重试',
enableSuccess: '双因素认证已启用',
disableTitle: '禁用双因素认证',
disableWarning: '禁用后,登录时将不再需要验证码。这可能会降低您的账户安全性。',
enterPassword: '请输入当前密码确认',
confirmDisable: '确认禁用',
disableSuccess: '双因素认证已禁用',
disableFailed: '禁用失败,请检查密码是否正确',
loginTitle: '双因素认证',
loginHint: '请输入您认证器应用显示的 6 位验证码',
loginFailed: '验证失败,请重试',
// New translations for email verification
verifyEmailFirst: '请先验证您的邮箱',
verifyPasswordFirst: '请先验证您的身份',
emailCode: '邮箱验证码',
enterEmailCode: '请输入 6 位验证码',
sendCode: '发送验证码',
codeSent: '验证码已发送到您的邮箱',
sendCodeFailed: '发送验证码失败'
},
passkey: {
title: 'Passkey',
description: '使用面容 ID、触控 ID、Windows Hello 或安全密钥免密码登录。',
add: '添加 Passkey',
continue: '创建 Passkey',
name: 'Passkey 名称',
namePlaceholder: '例如:MacBook 触控 ID',
passwordPlaceholder: '输入当前登录密码以确认',
empty: '尚未添加任何 Passkey。',
synced: '已同步',
createdAt: '创建于 {date}',
lastUsed: '上次使用 {date}',
featureDisabled: '管理员尚未配置 Passkey 功能。',
unsupported: '当前浏览器或设备不支持 Passkey。',
loadFailed: '加载 Passkey 失败。',
added: 'Passkey 已添加。',
addFailed: '添加 Passkey 失败。',
renamePrompt: '请输入新的 Passkey 名称',
renamed: 'Passkey 已重命名。',
renameFailed: '重命名 Passkey 失败。',
deleteTitle: '删除 Passkey',
deleteConfirm: '删除“{name}”?删除后将无法再使用它登录。',
deleted: 'Passkey 已删除。',
deleteFailed: '删除 Passkey 失败。'
},
balanceNotify: {
title: '余额不足提醒',
description: '当账户余额低于阈值时发送邮件提醒',
enabled: '启用余额不足提醒',
threshold: '自定义提醒阈值',
thresholdHint: '留空使用系统默认值',
thresholdPlaceholder: '输入金额',
systemDefault: '系统默认值',
extraEmails: '通知邮箱',
extraEmailsHint: '必须添加并验证邮箱后,余额不足时才能收到提醒邮件',
primaryEmail: '主邮箱',
noExtraEmails: '暂无额外通知邮箱',
enterEmail: '输入邮箱地址',
addEmail: '添加邮箱',
emailPlaceholder: '输入邮箱地址',
sendCode: '发送验证码',
resend: '重发',
codeSent: '验证码已发送',
codeSentTo: '验证码已发送到 {email}',
enterCode: '输入验证码',
codePlaceholder: '6位验证码',
verify: '验证',
emailAdded: '邮箱已添加',
emailRemoved: '邮箱已移除',
verifySuccess: '邮箱添加成功',
removeEmail: '移除',
removeSuccess: '邮箱已移除',
emailDuplicate: '该邮箱已存在',
maxEmailsReached: '已达到通知邮箱数量上限',
unverified: '未验证',
verified: '已验证',
},
avatar: {
title: '资料头像',
description: '仅支持上传头像图片;静态图片会自动压缩到 20KB 以内后再保存。',
uploadAction: '上传图片',
uploadHint: '上传图片时会自动压缩静态图片到 20KB 以内,GIF 需自行控制在 20KB 以内',
uploadRequired: '请先上传头像图片',
saveSuccess: '头像已更新',
deleteSuccess: '头像已删除',
invalidType: '请选择图片文件',
gifTooLarge: 'GIF 头像必须在 20KB 以内',
compressTooLarge: '无法将图片压缩到 20KB 以内,请换一张更小的图片',
compressFailed: '压缩所选图片失败',
readFailed: '读取所选图片失败',
emptyDeleteHint: '当前没有可删除的头像',
},
authBindings: {
title: '登录方式绑定',
description: '查看当前绑定状态,并将更多第三方登录方式关联到这个账号。',
bindAction: '绑定 {providerName}',
bindSuccess: '账号绑定成功',
emailPlaceholder: '输入邮箱地址',
codePlaceholder: '输入验证码',
passwordPlaceholder: '设置登录密码',
replaceEmailPasswordPlaceholder: '输入当前密码',
sendCodeAction: '发送验证码',
manageEmailAction: '管理邮箱',
hideEmailFormAction: '收起邮箱表单',
confirmEmailBindAction: '绑定邮箱',
confirmEmailReplaceAction: '更换主邮箱',
codeSentTo: '验证码已发送到 {email}',
replaceSuccess: '主邮箱已更新',
unbindAction: '解绑',
unbindSuccess: '{providerName} 已解绑',
boundCount: '已关联 {count} 条记录',
status: {
bound: '已绑定',
notBound: '未绑定',
},
providers: {
email: '邮箱',
linuxdo: 'LinuxDo',
dingtalk: '钉钉',
oidc: '{providerName}',
wechat: '微信',
},
notes: {
emailManagedFromProfile: '主邮箱在资料表单中管理',
canUnbind: '你可以解绑这个登录方式。',
bindAnotherBeforeUnbind: '请先绑定其他登录方式,再解除当前绑定。',
},
source: {
avatar: '头像当前来自 {providerName}',
username: '昵称当前来自 {providerName}',
},
}
},
// Empty States
empty: {
noData: '暂无数据'
},
// Table
table: {
expandActions: '展开更多操作',
collapseActions: '收起操作'
},
// Pagination
pagination: {
showing: '显示',
to: '至',
of: '共',
results: '条结果',
page: '页',
pageOf: '第 {page} / {total} 页',
previous: '上一页',
next: '下一页',
perPage: '每页',
goToPage: '跳转到第 {page} 页',
jumpTo: '跳转页',
jumpPlaceholder: '页码',
jumpAction: '跳转'
},
// Errors
errors: {
somethingWentWrong: '出错了',
pageNotFound: '页面未找到',
unauthorized: '未授权',
forbidden: '禁止访问',
serverError: '服务器错误',
networkError: '网络错误',
timeout: '请求超时',
tryAgain: '请重试'
},
// Dates
dates: {
today: '今天',
yesterday: '昨天',
thisWeek: '本周',
lastWeek: '上周',
thisMonth: '本月',
lastMonth: '上月',
last24Hours: '近24小时',
last7Days: '近 7 天',
last14Days: '近 14 天',
last30Days: '近 30 天',
custom: '自定义',
startDate: '开始日期',
endDate: '结束日期',
apply: '应用',
selectDateRange: '选择日期范围'
},
// Admin
}
+17
View File
@@ -0,0 +1,17 @@
import landing from './landing'
import common from './common'
import dashboard from './dashboard'
import channelMonitorV2 from './channelMonitorV2'
import batchImage from './batchImage'
import admin from './admin'
import misc from './misc'
export default {
...landing,
...common,
...dashboard,
...channelMonitorV2,
...batchImage,
admin,
...misc,
}
+257
View File
@@ -0,0 +1,257 @@
export default {
batchImageGuide: {
title: '图片批量生成',
description: '一次提交多条提示词,任务完成后可统一下载图片结果'
},
// Home Page
home: {
viewOnGithub: '在 GitHub 上查看',
viewDocs: '查看文档',
docs: '文档',
switchToLight: '切换到浅色模式',
switchToDark: '切换到深色模式',
dashboard: '控制台',
login: '登录',
getStarted: '立即开始',
goToDashboard: '进入控制台',
// 新增:面向用户的价值主张
heroSubtitle: '一个密钥,畅用多个 AI 模型',
heroDescription: '无需管理多个订阅账号,一站式接入 Claude、GPT、Gemini 等主流 AI 服务',
tags: {
subscriptionToApi: '订阅转 API',
stickySession: '会话保持',
realtimeBilling: '按量计费'
},
// 用户痛点区块
painPoints: {
title: '你是否也遇到这些问题?',
items: {
expensive: {
title: '订阅费用高',
desc: '每个 AI 服务都要单独订阅,每月支出越来越多'
},
complex: {
title: '多账号难管理',
desc: '不同平台的账号、密钥分散各处,管理起来很麻烦'
},
unstable: {
title: '服务不稳定',
desc: '单一账号容易触发限制,影响正常使用'
},
noControl: {
title: '用量无法控制',
desc: '不知道钱花在哪了,也无法限制团队成员的使用'
}
}
},
// 解决方案区块
solutions: {
title: '我们帮你解决',
subtitle: '简单三步,开始省心使用 AI'
},
features: {
unifiedGateway: '一键接入',
unifiedGatewayDesc: '获取一个 API 密钥,即可调用所有已接入的 AI 模型,无需分别申请。',
multiAccount: '稳定可靠',
multiAccountDesc: '智能调度多个上游账号,自动切换和负载均衡,告别频繁报错。',
balanceQuota: '用多少付多少',
balanceQuotaDesc: '按实际使用量计费,支持设置配额上限,团队用量一目了然。'
},
// 优势对比
comparison: {
title: '为什么选择我们?',
headers: {
feature: '对比项',
official: '官方订阅',
us: '本平台'
},
items: {
pricing: {
feature: '付费方式',
official: '固定月费,用不完也付',
us: '按量付费,用多少付多少'
},
models: {
feature: '模型选择',
official: '单一服务商',
us: '多模型随意切换'
},
management: {
feature: '账号管理',
official: '每个服务单独管理',
us: '统一密钥,一站管理'
},
stability: {
feature: '服务稳定性',
official: '单账号易触发限制',
us: '多账号池,自动切换'
},
control: {
feature: '用量控制',
official: '无法限制',
us: '可设配额、查明细'
}
}
},
providers: {
title: '已支持的 AI 模型',
description: '一个 API,多种选择',
supported: '已支持',
soon: '即将推出',
claude: 'Claude',
gemini: 'Gemini',
antigravity: 'Antigravity',
more: '更多'
},
// CTA 区块
cta: {
title: '准备好开始了吗?',
description: '注册即可获得免费试用额度,体验一站式 AI 服务',
button: '免费注册'
},
footer: {
allRightsReserved: '保留所有权利。'
}
},
// Key Usage Query Page
keyUsage: {
title: 'API Key 用量查询',
subtitle: '输入您的 API Key 以查看实时消费金额与使用状态',
placeholder: 'sk-ant-mirror-xxxxxxxxxxxx',
query: '查询',
querying: '查询中...',
privacyNote: '您的 Key 仅在浏览器本地处理,不会被存储',
dateRange: '统计范围:',
dateRangeToday: '今日',
dateRange7d: '7 天',
dateRange30d: '30 天',
dateRange90d: '90 天',
dateRangeCustom: '自定义',
apply: '应用',
used: '已使用',
detailInfo: '详细信息',
tokenStats: 'Token 统计',
dailyDetail: '按日明细',
modelStats: '模型用量统计',
// Table headers
date: '日期',
model: '模型',
requests: '请求数',
inputTokens: '输入 Tokens',
outputTokens: '输出 Tokens',
cacheCreationTokens: '缓存创建',
cacheReadTokens: '缓存读取',
cacheWriteTokens: '缓存写入',
totalTokens: '总 Tokens',
cost: '费用',
// Status
quotaMode: 'Key 限额模式',
walletBalance: '钱包余额',
// Ring card titles
totalQuota: '总额度',
limit5h: '5 小时限额',
limitDaily: '日限额',
limit7d: '7 天限额',
limitWeekly: '周限额',
limitMonthly: '月限额',
// Detail rows
remainingQuota: '剩余额度',
expiresAt: '过期时间',
todayExpires: '(今日到期)',
daysLeft: '({days} 天)',
usedQuota: '已用额度',
resetNow: '即将重置',
subscriptionType: '订阅类型',
subscriptionExpires: '订阅到期',
// Usage stat cells
todayRequests: '今日请求',
todayInputTokens: '今日输入',
todayOutputTokens: '今日输出',
todayTokens: '今日 Tokens',
todayCacheCreation: '今日缓存创建',
todayCacheRead: '今日缓存读取',
todayCost: '今日费用',
rpmTpm: 'RPM / TPM',
totalRequests: '累计请求',
totalInputTokens: '累计输入',
totalOutputTokens: '累计输出',
totalTokensLabel: '累计 Tokens',
totalCacheCreation: '累计缓存创建',
totalCacheRead: '累计缓存读取',
totalCost: '累计费用',
avgDuration: '平均耗时',
// Messages
enterApiKey: '请输入 API Key',
querySuccess: '查询成功',
queryFailed: '查询失败',
queryFailedRetry: '查询失败,请稍后重试',
noDailyUsage: '暂无按日用量数据',
},
// Setup Wizard
setup: {
title: 'Sub2API 安装向导',
description: '配置您的 Sub2API 实例',
database: {
title: '数据库配置',
description: '连接到您的 PostgreSQL 数据库',
host: '主机',
port: '端口',
username: '用户名',
password: '密码',
databaseName: '数据库名称',
sslMode: 'SSL 模式',
passwordPlaceholder: '密码',
ssl: {
disable: '禁用',
require: '要求',
verifyCa: '验证 CA',
verifyFull: '完全验证'
}
},
redis: {
title: 'Redis 配置',
description: '连接到您的 Redis 服务器',
host: '主机',
port: '端口',
username: '用户名(可选)',
password: '密码(可选)',
database: '数据库',
usernamePlaceholder: '默认用户留空',
passwordPlaceholder: '密码',
enableTls: '启用 TLS',
enableTlsHint: '连接 Redis 时使用 TLS(公共 CA 证书)'
},
admin: {
title: '管理员账户',
description: '创建您的管理员账户',
email: '邮箱',
password: '密码',
confirmPassword: '确认密码',
passwordPlaceholder: '至少 8 个字符',
confirmPasswordPlaceholder: '确认密码',
passwordMismatch: '密码不匹配'
},
ready: {
title: '准备安装',
description: '检查您的配置并完成安装',
database: '数据库',
redis: 'Redis',
adminEmail: '管理员邮箱'
},
status: {
testing: '测试中...',
success: '连接成功',
testConnection: '测试连接',
installing: '安装中...',
completeInstallation: '完成安装',
completed: '安装完成!',
redirecting: '正在跳转到登录页面...',
restarting: '服务正在重启,请稍候...',
timeout: '服务重启时间超出预期,请手动刷新页面。'
}
},
// Common
}
+645
View File
@@ -0,0 +1,645 @@
export default {
// Subscription Progress (Header component)
subscriptionProgress: {
title: '我的订阅',
viewDetails: '查看订阅详情',
activeCount: '{count} 个有效订阅',
daily: '每日',
weekly: '每周',
monthly: '每月',
daysRemaining: '剩余 {days} 天',
expired: '已过期',
expiresToday: '今天到期',
expiresTomorrow: '明天到期',
viewAll: '查看全部订阅',
noSubscriptions: '暂无有效订阅',
unlimited: '无限制'
},
// Version Badge
version: {
currentVersion: '当前版本',
latestVersion: '最新版本',
upToDate: '已是最新版本',
updateAvailable: '有新版本可用!',
releaseNotes: '更新日志',
noReleaseNotes: '暂无更新日志',
viewUpdate: '查看更新',
viewRelease: '查看发布',
viewChangelog: '查看更新日志',
refresh: '刷新',
sourceMode: '源码构建',
sourceModeHint: '源码构建请使用 git pull 更新',
updateNow: '立即更新',
updating: '正在更新...',
updateComplete: '更新完成',
updateFailed: '更新失败',
restartRequired: '请重启服务以应用更新',
restartNow: '立即重启',
restarting: '正在重启...',
retry: '重试',
rollback: '版本回退',
rollbackSelectVersion: '选择要回退到的版本(近 3 个版本)',
rollbackConfirm: '回退到 {version}',
rollbackWarning: '回退将下载所选版本并替换当前程序,完成后需重启服务',
rollingBack: '正在回退...',
rollbackComplete: '回退完成',
rollbackFailed: '回退失败',
manualRollbackCommand: '手动回退方式',
copyCommand: '复制',
copied: '已复制',
noRollbackVersions: '暂无可回退的版本',
loadVersionsFailed: '获取版本列表失败',
rollbackSourceHint: '源码构建不支持在线回退',
deployScript: '脚本部署',
deployDocker: 'Docker',
dockerEditCompose: '修改 docker-compose.yml 中的镜像版本',
dockerRecreate: '重新创建容器'
},
// Recharge / Subscription Page
purchase: {
title: '充值/订阅',
description: '通过内嵌页面完成充值/订阅',
openInNewTab: '新窗口打开',
notEnabledTitle: '该功能未开启',
notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。',
notConfiguredTitle: '充值/订阅链接未配置',
notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。'
},
// Custom Page (iframe embed)
customPage: {
title: '自定义页面',
openInNewTab: '新窗口打开',
notFoundTitle: '页面不存在',
notFoundDesc: '该自定义页面不存在或已被删除。',
notConfiguredTitle: '页面链接未配置',
notConfiguredDesc: '该自定义页面的 URL 未正确配置。',
tableOfContents: '目录',
copyCode: '复制',
copiedCode: '已复制',
copyCodeFailed: '失败'
},
// Announcements Page
announcements: {
title: '公告',
description: '查看系统公告',
unreadOnly: '仅显示未读',
markRead: '标记已读',
markAllRead: '全部已读',
viewAll: '查看全部公告',
markedAsRead: '已标记为已读',
allMarkedAsRead: '所有公告已标记为已读',
newCount: '有 {count} 条新公告',
readAt: '已读时间',
read: '已读',
unread: '未读',
startsAt: '开始时间',
endsAt: '结束时间',
empty: '暂无公告',
emptyUnread: '暂无未读公告',
total: '条公告',
emptyDescription: '暂时没有任何系统公告',
readStatus: '您已阅读此公告',
markReadHint: '点击"已读"标记此公告'
},
// User Subscriptions Page
userSubscriptions: {
title: '我的订阅',
description: '查看您的订阅计划和用量',
noActiveSubscriptions: '暂无有效订阅',
noActiveSubscriptionsDesc: '您没有任何有效订阅。请联系管理员获取订阅。',
failedToLoad: '加载订阅失败',
status: {
active: '有效',
expired: '已过期',
revoked: '已撤销'
},
usage: '用量',
expires: '到期时间',
noExpiration: '无到期时间',
unlimited: '无限制',
unlimitedDesc: '该订阅无用量限制',
daily: '每日',
weekly: '每周',
monthly: '每月',
daysRemaining: '剩余 {days} 天',
expiresOn: '{date} 到期',
resetIn: '{time} 后重置',
quotaEndsIn: '额度将在 {time} 后结束',
windowNotActive: '等待首次使用',
usageOf: '已用 {used} / {limit}'
},
// Onboarding Tour
onboarding: {
restartTour: '重新查看新手引导',
dontShowAgain: '不再提示',
dontShowAgainTitle: '永久关闭新手引导',
confirmDontShow: '确定不再显示新手引导吗?\n\n您可以随时在右上角头像菜单中重新开启。',
confirmExit: '确定要退出新手引导吗?您可以随时在右上角菜单重新开始。',
interactiveHint: '按 Enter 或点击继续',
navigation: {
flipPage: '翻页',
exit: '退出'
},
// Admin tour steps
admin: {
welcome: {
title: '👋 欢迎使用 Sub2API',
description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。</p><p style="margin-bottom: 12px;"><b>🎯 核心功能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>分组管理</b> - 创建不同的服务套餐(VIP、免费试用等)</li><li>🔗 <b>账号池</b> - 连接多个上游 AI 服务商账号</li><li>🔑 <b>密钥分发</b> - 为用户生成独立的 API Key</li><li>💰 <b>计费管理</b> - 灵活的费率和配额控制</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>',
nextBtn: '开始配置 🚀',
prevBtn: '跳过'
},
groupManage: {
title: '📦 第一步:分组管理',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 Sub2API 的核心概念,它就像一个"服务套餐"</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>'
},
createGroup: {
title: ' 创建新分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">现在让我们创建第一个分组。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📝 提示:</b>建议先创建一个测试分组,熟悉流程后再创建正式分组</p><p style="color: #10b981; font-weight: 600;">👉 点击"创建分组"按钮</p></div>'
},
groupName: {
title: '✏️ 1. 分组名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为您的分组起一个易于识别的名称。</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>💡 命名建议:</b><ul style="margin: 8px 0 0 16px;"><li>"测试分组" - 用于测试</li><li>"VIP专线" - 高质量服务</li><li>"免费试用" - 体验版</li></ul></div><p style="font-size: 13px; color: #6b7280;">填写完成后点击"下一步"继续</p></div>',
nextBtn: '下一步'
},
groupPlatform: {
title: '🤖 2. 选择平台',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择该分组支持的 AI 平台。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 平台说明:</b><ul style="margin: 8px 0 0 16px;"><li><b>Anthropic</b> - Claude 系列模型</li><li><b>OpenAI</b> - GPT 系列模型</li><li><b>Google</b> - Gemini 系列模型</li></ul></div><p style="font-size: 13px; color: #6b7280;">一个分组只能选择一个平台</p></div>',
nextBtn: '下一步'
},
groupMultiplier: {
title: '💰 3. 费率倍数',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置该分组的计费倍率,控制用户的实际扣费。</p><div style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚙️ 计费规则:</b><ul style="margin: 8px 0 0 16px;"><li><b>1.0</b> - 原价计费(成本价)</li><li><b>1.5</b> - 用户消耗 $1,扣除 $1.5</li><li><b>2.0</b> - 用户消耗 $1,扣除 $2</li><li><b>0.8</b> - 补贴模式(亏本运营)</li></ul></div><p style="font-size: 13px; color: #6b7280;">建议测试分组设置为 1.0</p></div>',
nextBtn: '下一步'
},
groupExclusive: {
title: '🔒 4. 专属分组(可选)',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">控制分组的可见性和访问权限。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔐 权限说明:</b><ul style="margin: 8px 0 0 16px;"><li><b>关闭</b> - 公开分组,所有用户可见</li><li><b>开启</b> - 专属分组,仅指定用户可见</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>VIP 用户专属、内部测试、特殊客户等</p></div>',
nextBtn: '下一步'
},
groupSubmit: {
title: '✅ 保存分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">确认信息无误后,点击创建按钮保存分组。</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 注意:</b>分组创建后,平台类型不可修改,其他信息可以随时编辑</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 下一步:</b>创建成功后,我们将添加上游账号到这个分组</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
},
accountManage: {
title: '🔗 第二步:添加账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>太棒了!分组已创建成功 🎉</b></p><p style="margin-bottom: 12px;">现在需要添加上游 AI 服务商的账号,让分组能够实际提供服务。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 账号的作用:</b><ul style="margin: 8px 0 0 16px;"><li>连接到上游 AI 服务(Claude、GPT 等)</li><li>一个分组可以包含多个账号(负载均衡)</li><li>支持 OAuth 和 Session Key 两种方式</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"账号管理"</p></div>'
},
createAccount: {
title: ' 添加新账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮开始添加您的第一个上游账号。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>建议使用 OAuth 方式,更安全且无需手动提取密钥</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"添加账号"按钮</p></div>'
},
accountName: {
title: '✏️ 1. 账号名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为账号设置一个便于识别的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 命名建议:</b>"Claude主账号"、"GPT备用1"、"测试账号" 等</p></div>',
nextBtn: '下一步'
},
accountPlatform: {
title: '🤖 2. 选择平台',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择该账号对应的服务商平台。</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px;"><b>⚠️ 重要:</b>平台必须与刚才创建的分组平台一致</p></div>',
nextBtn: '下一步'
},
accountType: {
title: '🔐 3. 授权方式',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择账号的授权方式。</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>✅ 推荐:OAuth 方式</b><ul style="margin: 8px 0 0 16px;"><li>无需手动提取密钥</li><li>更安全,支持自动刷新</li><li>适用于 Claude Code、ChatGPT OAuth</li></ul></div><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Session Key 方式</b><ul style="margin: 8px 0 0 16px;"><li>需要手动从浏览器提取</li><li>可能需要定期更新</li><li>适用于不支持 OAuth 的平台</li></ul></div></div>',
nextBtn: '下一步'
},
accountPriority: {
title: '⚖️ 4. 优先级(可选)',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置账号的调用优先级。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 优先级规则:</b><ul style="margin: 8px 0 0 16px;"><li>数字越小,优先级越高</li><li>系统优先使用低数值账号</li><li>相同优先级则随机选择</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>主账号设置低数值,备用账号设置高数值</p></div>',
nextBtn: '下一步'
},
accountGroups: {
title: '🎯 5. 分配分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>关键步骤!</b>将账号分配到刚才创建的分组。</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 重要提醒:</b><ul style="margin: 8px 0 0 16px;"><li>必须勾选至少一个分组</li><li>未分配分组的账号无法使用</li><li>一个账号可以分配给多个分组</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>请勾选刚才创建的测试分组</p></div>',
nextBtn: '下一步'
},
accountSubmit: {
title: '✅ 保存账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">确认信息无误后,点击保存按钮。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 OAuth 授权流程:</b><ul style="margin: 8px 0 0 16px;"><li>点击保存后会跳转到服务商页面</li><li>在服务商页面完成登录授权</li><li>授权成功后自动返回</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 下一步:</b>账号添加成功后,我们将创建 API 密钥</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"保存"按钮</p></div>'
},
keyManage: {
title: '🔑 第三步:生成密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>恭喜!账号配置完成 🎉</b></p><p style="margin-bottom: 12px;">最后一步,生成 API Key 来测试服务是否正常工作。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 API Key 的作用:</b><ul style="margin: 8px 0 0 16px;"><li>用于调用 AI 服务的凭证</li><li>每个 Key 绑定一个分组</li><li>可以设置配额和有效期</li><li>支持独立的使用统计</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"API 密钥"</p></div>'
},
createKey: {
title: ' 创建密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮创建您的第一个 API Key。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>创建后请立即复制保存,密钥只显示一次</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建密钥"按钮</p></div>'
},
keyName: {
title: '✏️ 1. 密钥名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为密钥设置一个便于管理的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 命名建议:</b>"测试密钥"、"生产环境"、"移动端" 等</p></div>',
nextBtn: '下一步'
},
keyGroup: {
title: '🎯 2. 选择分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择刚才配置好的分组。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 分组决定:</b><ul style="margin: 8px 0 0 16px;"><li>该密钥可以使用哪些账号</li><li>计费倍率是多少</li><li>是否为专属密钥</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>选择刚才创建的测试分组</p></div>',
nextBtn: '下一步'
},
keySubmit: {
title: '🎉 生成并复制',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击创建后,系统会生成完整的 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>⚠️ 重要提醒:</b><ul style="margin: 8px 0 0 16px;"><li>密钥只显示一次,请立即复制</li><li>丢失后需要重新生成</li><li>妥善保管,不要泄露给他人</li></ul></div><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🚀 下一步:</b><ul style="margin: 8px 0 0 16px;"><li>复制生成的 sk-xxx 密钥</li><li>在支持 OpenAI 接口的客户端中使用</li><li>开始体验 AI 服务!</li></ul></div><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
}
},
// User tour steps
user: {
welcome: {
title: '👋 欢迎使用 Sub2API',
description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 Sub2API AI 服务平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建 API 密钥</li><li>📋 复制密钥到您的应用</li><li>🚀 开始使用 AI 服务</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>',
nextBtn: '开始 🚀',
prevBtn: '跳过'
},
keyManage: {
title: '🔑 API 密钥管理',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">在这里管理您的所有 API 访问密钥。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 什么是 API 密钥?</b><br/>API 密钥是您访问 AI 服务的凭证,就像一把钥匙,让您的应用能够调用 AI 能力。</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击进入密钥页面</p></div>'
},
createKey: {
title: ' 创建新密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮创建您的第一个 API 密钥。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>创建后密钥只显示一次,请务必复制保存</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建密钥"</p></div>'
},
keyName: {
title: '✏️ 密钥名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为密钥起一个便于识别的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>"我的第一个密钥"、"测试用" 等</p></div>',
nextBtn: '下一步'
},
keyGroup: {
title: '🎯 选择分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择管理员为您分配的服务分组。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 分组说明:</b><br/>不同分组可能有不同的服务质量和计费标准,请根据需要选择。</p></div>',
nextBtn: '下一步'
},
keySubmit: {
title: '🎉 完成创建',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击确认创建您的 API 密钥。</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 重要:</b><ul style="margin: 8px 0 0 16px;"><li>创建后请立即复制密钥(sk-xxx</li><li>密钥只显示一次,丢失需重新生成</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>🚀 如何使用:</b><br/>将密钥配置到支持 OpenAI 接口的任何客户端(如 ChatBox、OpenCat 等),即可开始使用!</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
}
}
},
// Payment System
payment: {
title: '充值/订阅',
amountLabel: '充值金额',
paymentAmount: '支付金额',
creditedBalance: '到账余额',
quickAmounts: '快捷金额',
customAmount: '自定义金额',
enterAmount: '输入金额',
paymentMethod: '支付方式',
fee: '手续费',
actualPay: '实付金额',
createOrder: '确认支付',
methods: {
easypay: '易支付',
alipay: '支付宝',
wxpay: '微信支付',
stripe: 'Stripe',
airwallex: 'Airwallex',
card: '银行卡',
link: 'Link',
alipay_direct: '支付宝(直连)',
wxpay_direct: '微信支付(直连)',
},
status: {
pending: '待支付',
paid: '已支付',
recharging: '充值中',
completed: '已完成',
expired: '已过期',
cancelled: '已取消',
failed: '失败',
refund_requested: '退款申请中',
refunding: '退款中',
refund_pending: '退款处理中',
refunded: '已退款',
partially_refunded: '部分退款',
refund_failed: '退款失败',
},
qr: {
scanToPay: '请扫码支付',
scanAlipay: '支付宝扫码支付',
scanWxpay: '微信扫码支付',
scanAlipayHint: '请使用手机打开支付宝,扫描二维码完成支付',
scanWxpayHint: '请使用手机打开微信,扫描二维码完成支付',
payInNewWindow: '请在新窗口中完成支付',
payInNewWindowHint: '支付页面已在新窗口打开,请在新窗口中完成支付后返回此页面',
openPayWindow: '重新打开支付页面',
expiresIn: '剩余支付时间',
expired: '订单已过期',
expiredDesc: '订单已超时,请重新创建订单',
cancelled: '订单已取消',
cancelledDesc: '您已取消本次支付',
waitingPayment: '等待支付...',
cancelOrder: '取消订单',
alipayOpening: '正在打开支付宝',
alipayContinueInApp: '请在支付宝中完成支付',
alipayWaitingHint: '支付结果将由服务端确认,本页面会自动更新',
alipayFallbackTitle: '打开支付宝未成功',
alipayFallbackHint: '可重新打开支付宝,或保存下方二维码后从支付宝相册识别',
reopenAlipay: '重新打开支付宝',
saveQRCode: '保存二维码',
alipaySaveAndScanHint: '保存二维码后,打开支付宝扫一扫,从相册选择二维码',
},
orders: {
title: '我的订单',
empty: '暂无订单',
orderId: '订单 ID',
orderNo: '订单编号',
amount: '金额',
payAmount: '实付',
creditedAmount: '到账金额',
fee: '手续费',
baseAmount: '充值金额',
includedInPayAmount: '已含在实付金额中',
status: '状态',
paymentMethod: '支付方式',
createdAt: '创建时间',
cancel: '取消订单',
userId: '用户 ID',
orderType: '订单类型',
actions: '操作',
requestRefund: '申请退款',
},
result: {
success: '支付成功',
subscriptionSuccess: '订阅成功',
processing: '支付处理中',
processingHint: '支付结果仍在确认中,页面会自动刷新。',
failed: '支付失败',
backToRecharge: '返回充值',
viewOrders: '查看订单',
},
currentBalance: '当前余额',
groupFallback: '分组 #{id}',
rechargeAccount: '充值账户',
activeSubscription: '当前订阅',
noActiveSubscription: '暂无有效订阅',
tabTopUp: '充值',
tabSubscribe: '订阅',
noPlans: '暂无可用订阅套餐',
notAvailable: '充值功能暂未开放',
confirmSubscription: '确认订阅',
confirmCancel: '确定要取消此订单吗?',
amountTooLow: '最低金额为 {min}',
amountTooHigh: '最高金额为 {max}',
amountNoMethod: '该金额没有可用的支付方式',
rechargeRatePreview: '当前倍率:1 CNY = {usd} USD',
refundReason: '退款原因',
refundReasonPlaceholder: '请描述您的退款原因',
stripeLoadFailed: '支付组件加载失败,请刷新页面重试',
stripeMissingParams: '缺少订单ID或支付密钥',
stripeNotConfigured: 'Stripe 未配置',
airwallexLoadFailed: 'Airwallex 支付组件加载失败,请刷新页面重试',
airwallexMissingParams: '缺少 Airwallex 支付参数',
errors: {
tooManyPending: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单',
cancelRateLimited: '取消订单过于频繁,请稍后再试',
wechatH5NotAuthorized: '当前商户未开通微信 H5 支付,请在微信中打开当前页面继续支付。',
wechatPaymentMpNotConfigured: '当前站点未完成公众号/JSAPI 支付配置,暂时无法在微信内直接拉起支付。',
wechatJsapiUnavailable: '当前环境未能拉起微信支付,请确认正在微信内打开本页后重试。',
wechatJsapiFailed: '微信支付未完成,请重新拉起支付或改用扫码支付。',
wechatUnavailable: '当前微信支付暂不可用,请稍后重试。',
wechatOpenInWeChatHint: '请复制当前页面链接到微信内打开,或直接改用电脑端微信扫码支付。',
wechatScanOnDesktopHint: '电脑端请直接使用微信扫一扫完成支付;移动端请在微信内打开当前页面。',
wechatSwitchBrowserHint: '请改用电脑端微信扫码,或在外部浏览器重新打开本页后再试。',
mobilePaymentFallbackToQr: '当前商户未开通移动支付,已自动切换为扫码支付。',
alipayDesktopUnavailable: '当前支付宝桌面支付未成功生成二维码。',
alipayDesktopQrHint: '电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。',
alipayMobileUnavailable: '当前页面未成功跳转到支付宝。',
alipayMobileOpenHint: '请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。',
// Structured error codes (reason strings from backend ApplicationError)
PAYMENT_DISABLED: '支付系统已关闭',
USER_INACTIVE: '账号已被禁用',
BALANCE_PAYMENT_DISABLED: '余额充值功能已关闭',
INVALID_AMOUNT: '金额无效',
INVALID_INPUT: '参数有误',
PLAN_NOT_AVAILABLE: '套餐不存在或已下架',
GROUP_NOT_FOUND: '订阅分组不可用',
GROUP_TYPE_MISMATCH: '分组类型不是订阅类型',
TOO_MANY_PENDING: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单',
DAILY_LIMIT_EXCEEDED: '今日充值已达上限,剩余额度 {remaining}',
PAYMENT_GATEWAY_ERROR: '支付方式不可用',
NO_AVAILABLE_INSTANCE: '暂无可用的支付通道',
PAYMENT_PROVIDER_MISCONFIGURED: '支付通道配置错误,请联系管理员',
WXPAY_CONFIG_MISSING_KEY: '微信支付配置缺少必填项:{key}',
WXPAY_CONFIG_INVALID_KEY_LENGTH: '微信支付 {key} 长度错误,应为 {expected} 字节(实际 {actual}',
WXPAY_CONFIG_INVALID_KEY: '微信支付 {key} 格式错误,请确认复制了完整的 PEM 内容',
PENDING_ORDERS: '该服务商有未完成的订单,请等待订单完成后再操作',
PAYMENT_PROVIDER_CONFLICT: '该支付方式已有其他启用中的服务商实例,请先停用后再继续。',
CANCEL_RATE_LIMITED: '取消订单过于频繁,请稍后再试',
NOT_FOUND: '订单不存在',
FORBIDDEN: '无权限操作此订单',
CONFLICT: '订单状态已变更,请刷新',
INVALID_ORDER_TYPE: '仅余额订单可申请退款',
INVALID_STATUS: '当前订单状态不允许此操作',
BALANCE_NOT_ENOUGH: '退款金额超过余额',
REFUND_AMOUNT_EXCEEDED: '退款金额超过充值金额',
REFUND_FAILED: '退款失败',
},
airwallexPay: 'Airwallex 支付',
stripePay: '立即支付',
stripeSuccessProcessing: '支付成功,正在处理订单...',
stripePopup: {
redirecting: '正在跳转到支付页面...',
loadingQr: '正在获取微信支付二维码...',
timeout: '等待支付凭证超时,请重试',
qrFailed: '未能获取微信支付二维码',
},
subscribeNow: '立即开通',
renewNow: '续费',
selectPlan: '选择套餐',
planFeatures: '功能特性',
planCard: {
rate: '倍率',
peakRate: '高峰倍率',
dailyLimit: '日限额',
weeklyLimit: '周限额',
monthlyLimit: '月限额',
quota: '配额',
unlimited: '无限制',
models: '模型',
},
days: '天',
weeks: '周',
months: '个月',
years: '年',
oneMonth: '1 个月',
oneYear: '1 年',
perMonth: '月',
perYear: '年',
admin: {
tabs: {
overview: '概览',
orders: '订单管理',
channels: '支付渠道',
plans: '订阅套餐',
},
todayRevenue: '今日收入',
totalRevenue: '总收入',
todayOrders: '今日订单',
orderCount: '订单数',
avgAmount: '平均金额',
revenue: '收入',
dailyRevenue: '每日收入',
paymentDistribution: '支付方式分布',
colUser: '用户',
topUsers: '消费排行',
noData: '暂无数据',
days: '天',
weeks: '周',
months: '月',
searchOrders: '搜索订单...',
allStatuses: '全部状态',
allPaymentTypes: '全部支付方式',
allOrderTypes: '全部订单类型',
orderDetail: '订单详情',
orderType: '订单类型',
orders: '订单',
balanceOrder: '余额充值',
subscriptionOrder: '订阅',
paidAt: '支付时间',
completedAt: '完成时间',
expiresAt: '过期时间',
feeRate: '手续费率',
refund: '退款',
refundOrder: '退款订单',
refundAmount: '退款金额',
maxRefundable: '最大可退金额',
refundReason: '退款原因',
refundReasonPlaceholder: '请输入退款原因',
confirmRefund: '确认退款',
refundSuccess: '退款成功',
refundPending: '退款处理中,待网关确认',
queryRefundStatus: '查询退款状态',
refundInfo: '退款信息',
refundEnabled: '允许退款',
allowUserRefund: '允许用户退款',
alreadyRefunded: '已退款',
deductBalance: '扣除余额',
deductBalanceHint: '从用户余额中扣回充值金额',
userBalance: '用户余额',
orderAmount: '订单金额',
insufficientBalance: '余额不足,将扣至 $0',
noDeduction: '将不扣除用户余额',
forceRefund: '强制退款(忽略余额检查)',
orderCancelled: '订单已取消',
retry: '重试',
retrySuccess: '重试成功',
approveRefund: '批准退款',
retryRefund: '重试退款',
refundRequestInfo: '退款申请信息',
refundRequestedAt: '申请时间',
refundRequestedBy: '申请人',
refundRequestReason: '申请原因',
auditLogs: '操作日志',
operator: '操作人',
channelName: '渠道名称',
channelDescription: '渠道描述',
createChannel: '创建渠道',
editChannel: '编辑渠道',
deleteChannel: '删除渠道',
deleteChannelConfirm: '确定要删除此渠道吗?',
planName: '套餐名称',
planDescription: '套餐描述',
createPlan: '创建套餐',
editPlan: '编辑套餐',
deletePlan: '删除套餐',
deletePlanConfirm: '确定要删除此套餐吗?',
originalPrice: '原价',
price: '价格',
currency: '币种标注',
currencyPlaceholder: '如 USD / NZD / CNY',
currencyHint: '仅用于价格展示的 ISO 三字母币种码,留空不展示,不影响实际扣款',
subscriptionCnyPayPreview: 'CNY 通道实扣预览:{amount}',
subscriptionCnyPayPreviewWithFee: '(含 {feeRate}% 手续费:{total}',
validity: '有效期',
validityUnit: '有效期单位',
sortOrder: '排序',
forSale: '上架状态',
onSale: '上架',
offSale: '下架',
group: '分组',
groupId: '分组 ID',
features: '功能特性',
featuresHint: '每行一个特性',
featuresPlaceholder: '输入套餐特性...',
providerManagement: '服务商管理',
providerManagementDesc: '管理支付服务商实例',
createProvider: '创建服务商',
editProvider: '编辑服务商',
deleteProvider: '删除服务商',
deleteProviderConfirm: '确定要删除此服务商吗?',
providerName: '服务商名称',
providerKey: '服务商标识',
selectProviderKey: '选择服务商标识',
providerConfig: '服务商配置',
noProviders: '暂无服务商',
noProvidersHint: '创建一个服务商实例以开始接受支付',
supportedTypes: '支持的支付方式',
supportedTypesHint: '选择此服务商支持的支付方式',
rateMultiplier: '费率倍数',
dashboardTitle: '支付概览',
dashboardDesc: '充值订单统计与分析',
daySuffix: '天',
paymentConfigTitle: '支付配置',
paymentConfigDesc: '管理支付服务商与相关设置',
plansPageTitle: '订阅套餐管理',
plansPageDesc: '管理订阅套餐配置',
tabPlanConfig: '套餐配置',
tabUserSubs: '用户订阅',
selectGroup: '请选择分组',
groupRequired: '请选择订阅分组',
priceRequired: '价格必须大于 0',
validityRequired: '有效期必须大于 0',
groupMissing: '缺失',
groupInfo: '分组信息',
platform: '平台',
rateMultiplierLabel: '倍率',
dailyLimit: '日限额',
weeklyLimit: '周限额',
monthlyLimit: '月限额',
unlimited: '无限制',
searchUserSubs: '搜索用户订阅...',
daily: '日',
weekly: '周',
monthly: '月',
subsStatus: {
active: '生效中',
expired: '已过期',
revoked: '已撤销',
},
},
},
}