Commit ee4bfcbb authored by Elysia's avatar Elysia
Browse files

Merge remote-tracking branch 'origin/main'

parents 32d619a5 cac23020
...@@ -133,6 +133,8 @@ export default { ...@@ -133,6 +133,8 @@ export default {
requests: '请求数', requests: '请求数',
inputTokens: '输入 Tokens', inputTokens: '输入 Tokens',
outputTokens: '输出 Tokens', outputTokens: '输出 Tokens',
cacheCreationTokens: '缓存创建',
cacheReadTokens: '缓存读取',
totalTokens: '总 Tokens', totalTokens: '总 Tokens',
cost: '费用', cost: '费用',
// Status // Status
...@@ -155,11 +157,19 @@ export default { ...@@ -155,11 +157,19 @@ export default {
subscriptionExpires: '订阅到期', subscriptionExpires: '订阅到期',
// Usage stat cells // Usage stat cells
todayRequests: '今日请求', todayRequests: '今日请求',
todayInputTokens: '今日输入',
todayOutputTokens: '今日输出',
todayTokens: '今日 Tokens', todayTokens: '今日 Tokens',
todayCacheCreation: '今日缓存创建',
todayCacheRead: '今日缓存读取',
todayCost: '今日费用', todayCost: '今日费用',
rpmTpm: 'RPM / TPM', rpmTpm: 'RPM / TPM',
totalRequests: '累计请求', totalRequests: '累计请求',
totalInputTokens: '累计输入',
totalOutputTokens: '累计输出',
totalTokensLabel: '累计 Tokens', totalTokensLabel: '累计 Tokens',
totalCacheCreation: '累计缓存创建',
totalCacheRead: '累计缓存读取',
totalCost: '累计费用', totalCost: '累计费用',
avgDuration: '平均耗时', avgDuration: '平均耗时',
// Messages // Messages
...@@ -1774,8 +1784,20 @@ export default { ...@@ -1774,8 +1784,20 @@ export default {
stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值', stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值',
stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话' stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话'
}, },
quota: {
exceeded: '配额已用完,账号暂停调度',
normal: '配额正常'
},
}, },
clearRateLimit: '清除速率限制', clearRateLimit: '清除速率限制',
resetQuota: '重置配额',
quotaLimit: '配额限制',
quotaLimitPlaceholder: '0 表示不限制',
quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。',
quotaLimitToggle: '启用配额限制',
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
quotaLimitAmount: '限额金额',
quotaLimitAmountHint: '账号最大可用额度(美元),达到后自动暂停。修改限额不会重置已用额度。',
testConnection: '测试连接', testConnection: '测试连接',
reAuthorize: '重新授权', reAuthorize: '重新授权',
refreshToken: '刷新令牌', refreshToken: '刷新令牌',
...@@ -2123,10 +2145,12 @@ export default { ...@@ -2123,10 +2145,12 @@ export default {
proxy: '代理', proxy: '代理',
noProxy: '无代理', noProxy: '无代理',
concurrency: '并发数', concurrency: '并发数',
loadFactor: '负载因子',
loadFactorHint: '提高负载因子可以提高对账号的调度频率',
priority: '优先级', priority: '优先级',
priorityHint: '优先级越小的账号优先使用', priorityHint: '优先级越小的账号优先使用',
billingRateMultiplier: '账号计费倍率', billingRateMultiplier: '账号计费倍率',
billingRateMultiplierHint: '>=0,0 表示该账号计费为 0;仅影响账号计费口径', billingRateMultiplierHint: '0 表示不计费,仅影响账号计费',
expiresAt: '过期时间', expiresAt: '过期时间',
expiresAtHint: '留空表示不过期', expiresAtHint: '留空表示不过期',
higherPriorityFirst: '数值越小优先级越高', higherPriorityFirst: '数值越小优先级越高',
...@@ -2142,6 +2166,7 @@ export default { ...@@ -2142,6 +2166,7 @@ export default {
accountUpdated: '账号更新成功', accountUpdated: '账号更新成功',
failedToCreate: '创建账号失败', failedToCreate: '创建账号失败',
failedToUpdate: '更新账号失败', failedToUpdate: '更新账号失败',
pleaseSelectStatus: '请选择有效的账号状态',
mixedChannelWarningTitle: '混合渠道警告', mixedChannelWarningTitle: '混合渠道警告',
mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?', mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?',
pleaseEnterAccountName: '请输入账号名称', pleaseEnterAccountName: '请输入账号名称',
......
...@@ -653,6 +653,7 @@ export interface Account { ...@@ -653,6 +653,7 @@ export interface Account {
} & Record<string, unknown>) } & Record<string, unknown>)
proxy_id: number | null proxy_id: number | null
concurrency: number concurrency: number
load_factor?: number | null
current_concurrency?: number // Real-time concurrency count from Redis current_concurrency?: number // Real-time concurrency count from Redis
priority: number priority: number
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free) rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
...@@ -705,6 +706,10 @@ export interface Account { ...@@ -705,6 +706,10 @@ export interface Account {
cache_ttl_override_enabled?: boolean | null cache_ttl_override_enabled?: boolean | null
cache_ttl_override_target?: string | null cache_ttl_override_target?: string | null
// API Key 账号配额限制
quota_limit?: number | null
quota_used?: number | null
// 运行时状态(仅当启用对应限制时返回) // 运行时状态(仅当启用对应限制时返回)
current_window_cost?: number | null // 当前窗口费用 current_window_cost?: number | null // 当前窗口费用
active_sessions?: number | null // 当前活跃会话数 active_sessions?: number | null // 当前活跃会话数
...@@ -783,6 +788,7 @@ export interface CreateAccountRequest { ...@@ -783,6 +788,7 @@ export interface CreateAccountRequest {
extra?: Record<string, unknown> extra?: Record<string, unknown>
proxy_id?: number | null proxy_id?: number | null
concurrency?: number concurrency?: number
load_factor?: number | null
priority?: number priority?: number
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free) rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
group_ids?: number[] group_ids?: number[]
...@@ -799,6 +805,7 @@ export interface UpdateAccountRequest { ...@@ -799,6 +805,7 @@ export interface UpdateAccountRequest {
extra?: Record<string, unknown> extra?: Record<string, unknown>
proxy_id?: number | null proxy_id?: number | null
concurrency?: number concurrency?: number
load_factor?: number | null
priority?: number priority?: number
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free) rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
schedulable?: boolean schedulable?: boolean
...@@ -1098,7 +1105,8 @@ export interface TrendDataPoint { ...@@ -1098,7 +1105,8 @@ export interface TrendDataPoint {
requests: number requests: number
input_tokens: number input_tokens: number
output_tokens: number output_tokens: number
cache_tokens: number cache_creation_tokens: number
cache_read_tokens: number
total_tokens: number total_tokens: number
cost: number // 标准计费 cost: number // 标准计费
actual_cost: number // 实际扣除 actual_cost: number // 实际扣除
...@@ -1109,6 +1117,8 @@ export interface ModelStat { ...@@ -1109,6 +1117,8 @@ export interface ModelStat {
requests: number requests: number
input_tokens: number input_tokens: number
output_tokens: number output_tokens: number
cache_creation_tokens: number
cache_read_tokens: number
total_tokens: number total_tokens: number
cost: number // 标准计费 cost: number // 标准计费
actual_cost: number // 实际扣除 actual_cost: number // 实际扣除
......
...@@ -302,6 +302,8 @@ ...@@ -302,6 +302,8 @@
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.requests') }}</th> <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.requests') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.inputTokens') }}</th> <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.inputTokens') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.outputTokens') }}</th> <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.outputTokens') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.cacheCreationTokens') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.cacheReadTokens') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.totalTokens') }}</th> <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.totalTokens') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.cost') }}</th> <th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-dark-400">{{ t('keyUsage.cost') }}</th>
</tr> </tr>
...@@ -316,6 +318,8 @@ ...@@ -316,6 +318,8 @@
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.requests) }}</td> <td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.requests) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.input_tokens) }}</td> <td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.input_tokens) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.output_tokens) }}</td> <td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.output_tokens) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.cache_creation_tokens) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.cache_read_tokens) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.total_tokens) }}</td> <td class="px-4 py-3 text-sm tabular-nums text-right text-gray-700 dark:text-dark-200">{{ fmtNum(m.total_tokens) }}</td>
<td class="px-4 py-3 text-sm tabular-nums text-right font-medium text-gray-900 dark:text-white">{{ usd(m.actual_cost != null ? m.actual_cost : m.cost) }}</td> <td class="px-4 py-3 text-sm tabular-nums text-right font-medium text-gray-900 dark:text-white">{{ usd(m.actual_cost != null ? m.actual_cost : m.cost) }}</td>
</tr> </tr>
...@@ -694,11 +698,19 @@ const usageStatCells = computed<StatCell[]>(() => { ...@@ -694,11 +698,19 @@ const usageStatCells = computed<StatCell[]>(() => {
return [ return [
{ label: t('keyUsage.todayRequests'), value: fmtNum(today.requests) }, { label: t('keyUsage.todayRequests'), value: fmtNum(today.requests) },
{ label: t('keyUsage.todayInputTokens'), value: fmtNum(today.input_tokens) },
{ label: t('keyUsage.todayOutputTokens'), value: fmtNum(today.output_tokens) },
{ label: t('keyUsage.todayTokens'), value: fmtNum(today.total_tokens) }, { label: t('keyUsage.todayTokens'), value: fmtNum(today.total_tokens) },
{ label: t('keyUsage.todayCacheCreation'), value: fmtNum(today.cache_creation_tokens) },
{ label: t('keyUsage.todayCacheRead'), value: fmtNum(today.cache_read_tokens) },
{ label: t('keyUsage.todayCost'), value: usd(today.actual_cost) }, { label: t('keyUsage.todayCost'), value: usd(today.actual_cost) },
{ label: t('keyUsage.rpmTpm'), value: `${usage.rpm || 0} / ${usage.tpm || 0}` }, { label: t('keyUsage.rpmTpm'), value: `${usage.rpm || 0} / ${usage.tpm || 0}` },
{ label: t('keyUsage.totalRequests'), value: fmtNum(total.requests) }, { label: t('keyUsage.totalRequests'), value: fmtNum(total.requests) },
{ label: t('keyUsage.totalInputTokens'), value: fmtNum(total.input_tokens) },
{ label: t('keyUsage.totalOutputTokens'), value: fmtNum(total.output_tokens) },
{ label: t('keyUsage.totalTokensLabel'), value: fmtNum(total.total_tokens) }, { label: t('keyUsage.totalTokensLabel'), value: fmtNum(total.total_tokens) },
{ label: t('keyUsage.totalCacheCreation'), value: fmtNum(total.cache_creation_tokens) },
{ label: t('keyUsage.totalCacheRead'), value: fmtNum(total.cache_read_tokens) },
{ label: t('keyUsage.totalCost'), value: usd(total.actual_cost) }, { label: t('keyUsage.totalCost'), value: usd(total.actual_cost) },
{ label: t('keyUsage.avgDuration'), value: usage.average_duration_ms ? `${Math.round(usage.average_duration_ms)} ms` : '-' }, { label: t('keyUsage.avgDuration'), value: usage.average_duration_ms ? `${Math.round(usage.average_duration_ms)} ms` : '-' },
] ]
......
...@@ -261,7 +261,7 @@ ...@@ -261,7 +261,7 @@
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" /> <AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" /> <AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" /> <ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" /> <AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" @reset-quota="handleResetQuota" />
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" /> <SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" /> <ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" /> <BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
...@@ -1125,6 +1125,16 @@ const handleClearRateLimit = async (a: Account) => { ...@@ -1125,6 +1125,16 @@ const handleClearRateLimit = async (a: Account) => {
console.error('Failed to clear rate limit:', error) console.error('Failed to clear rate limit:', error)
} }
} }
const handleResetQuota = async (a: Account) => {
try {
const updated = await adminAPI.accounts.resetAccountQuota(a.id)
patchAccountInList(updated)
enterAutoRefreshSilentWindow()
appStore.showSuccess(t('common.success'))
} catch (error) {
console.error('Failed to reset quota:', error)
}
}
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true } const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } } const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
const handleToggleSchedulable = async (a: Account) => { const handleToggleSchedulable = async (a: Account) => {
......
...@@ -113,6 +113,9 @@ ...@@ -113,6 +113,9 @@
<!-- Actions --> <!-- Actions -->
<div class="ml-auto flex items-center gap-3"> <div class="ml-auto flex items-center gap-3">
<button @click="applyFilters" :disabled="loading" class="btn btn-secondary">
{{ t('common.refresh') }}
</button>
<button @click="resetFilters" class="btn btn-secondary"> <button @click="resetFilters" class="btn btn-secondary">
{{ t('common.reset') }} {{ t('common.reset') }}
</button> </button>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment