mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(pool): 记录并展示 Provider Key 累计 Token 与费用
Closes #219 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -29,7 +29,7 @@ export interface PoolStatusResponse {
|
||||
* 获取 Provider 的号池状态
|
||||
*/
|
||||
export async function getPoolStatus(providerId: string): Promise<PoolStatusResponse> {
|
||||
const response = await client.get(`/api/admin/providers/${providerId}/pool-status`)
|
||||
const response = await client.get<PoolStatusResponse>(`/api/admin/providers/${providerId}/pool-status`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function clearPoolCooldown(
|
||||
providerId: string,
|
||||
keyId: string,
|
||||
): Promise<{ message: string }> {
|
||||
const response = await client.post(
|
||||
const response = await client.post<{ message: string }>(
|
||||
`/api/admin/providers/${providerId}/pool/clear-cooldown/${keyId}`,
|
||||
)
|
||||
return response.data
|
||||
@@ -53,7 +53,7 @@ export async function resetPoolCost(
|
||||
providerId: string,
|
||||
keyId: string,
|
||||
): Promise<{ message: string }> {
|
||||
const response = await client.post(
|
||||
const response = await client.post<{ message: string }>(
|
||||
`/api/admin/providers/${providerId}/pool/reset-cost/${keyId}`,
|
||||
)
|
||||
return response.data
|
||||
@@ -125,6 +125,8 @@ export interface PoolKeyDetail {
|
||||
cost_window_usage: number
|
||||
cost_limit: number | null
|
||||
request_count: number
|
||||
total_tokens: number
|
||||
total_cost_usd: string
|
||||
sticky_sessions: number
|
||||
lru_score: number | null
|
||||
created_at: string | null
|
||||
|
||||
@@ -543,13 +543,25 @@
|
||||
>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 px-2 align-middle">
|
||||
<div class="w-[136px] mx-auto text-[10px] leading-4">
|
||||
<div class="grid grid-rows-3 gap-0.5 w-[136px] mx-auto text-[10px] leading-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">请求</span>
|
||||
<span class="tabular-nums text-foreground/90">
|
||||
{{ formatStatInteger(key.request_count) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">Token</span>
|
||||
<span class="tabular-nums text-foreground/90">
|
||||
{{ formatTokenCount(key.total_tokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">费用</span>
|
||||
<span class="tabular-nums text-foreground/90">
|
||||
{{ formatStatUsd(key.total_cost_usd) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
@@ -912,11 +924,19 @@
|
||||
<div class="text-muted-foreground mb-0.5">
|
||||
统计
|
||||
</div>
|
||||
<div class="text-[10px]">
|
||||
<div class="space-y-0.5 text-[10px]">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">请求</span>
|
||||
<span class="tabular-nums">{{ formatStatInteger(key.request_count) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">Token</span>
|
||||
<span class="tabular-nums">{{ formatTokenCount(key.total_tokens) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted-foreground">费用</span>
|
||||
<span class="tabular-nums">{{ formatStatUsd(key.total_cost_usd) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -1195,6 +1215,7 @@ async function loadOverview() {
|
||||
selectedProviderData.value = null
|
||||
showAccountBatchDialog.value = false
|
||||
closeProviderProxyPopovers()
|
||||
resetKeyPage()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1382,6 +1403,7 @@ const desktopColumnWidths = computed(() => {
|
||||
async function selectProvider(id: string) {
|
||||
const requestId = ++selectProviderRequestId
|
||||
selectedProviderId.value = id
|
||||
selectedProviderData.value = null
|
||||
editingKeyDetail.value = null
|
||||
showAccountBatchDialog.value = false
|
||||
keyPermissionsDialogOpen.value = false
|
||||
@@ -1399,6 +1421,7 @@ async function selectProvider(id: string) {
|
||||
clearTimeout(keysSearchDebounceTimer)
|
||||
keysSearchDebounceTimer = null
|
||||
}
|
||||
resetKeyPage(1, pageSize.value)
|
||||
const keysTask = loadKeys()
|
||||
// Provider summary is non-blocking for key list rendering.
|
||||
void loadProviderData(id)
|
||||
@@ -1423,7 +1446,11 @@ async function refresh() {
|
||||
}
|
||||
|
||||
// --- Keys ---
|
||||
const keyPage = ref<PoolKeysPageResponse>({ total: 0, page: 1, page_size: 50, keys: [] })
|
||||
function createEmptyKeyPage(page = 1, pageSizeValue = 50): PoolKeysPageResponse {
|
||||
return { total: 0, page, page_size: pageSizeValue, keys: [] }
|
||||
}
|
||||
|
||||
const keyPage = ref<PoolKeysPageResponse>(createEmptyKeyPage())
|
||||
const keysLoading = ref(false)
|
||||
const refreshingCurrentPageQuota = ref(false)
|
||||
const searchQuery = ref('')
|
||||
@@ -1432,7 +1459,6 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
const MANUAL_QUOTA_REFRESH_COOLDOWN_SECONDS = 5 * 60
|
||||
const refreshingOAuthKeyId = ref<string | null>(null)
|
||||
const revealedKeys = ref<Map<string, string>>(new Map())
|
||||
const recoveringHealthKeyId = ref<string | null>(null)
|
||||
const savingProxyKeyId = ref<string | null>(null)
|
||||
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||
@@ -1473,6 +1499,14 @@ const refreshCurrentPageLoading = computed(() => {
|
||||
return keysLoading.value || refreshingCurrentPageQuota.value
|
||||
})
|
||||
|
||||
function resetKeyPage(page = currentPage.value, pageSizeValue = pageSize.value): void {
|
||||
keyPage.value = createEmptyKeyPage(page, pageSizeValue)
|
||||
}
|
||||
|
||||
function refreshOverviewInBackground(): void {
|
||||
void loadOverview()
|
||||
}
|
||||
|
||||
function normalizeQuotaUpdatedAt(raw: number | null | undefined): number | null {
|
||||
const value = Number(raw ?? 0)
|
||||
if (!Number.isFinite(value) || value <= 0) return null
|
||||
@@ -1608,6 +1642,7 @@ async function loadKeys() {
|
||||
keyPage.value = nextPage
|
||||
} catch (err) {
|
||||
if (requestId !== keysRequestId || selectedProviderId.value !== providerId) return
|
||||
resetKeyPage(page, pageSizeValue)
|
||||
showError(parseApiError(err))
|
||||
} finally {
|
||||
if (requestId === keysRequestId) {
|
||||
@@ -1879,6 +1914,7 @@ async function handleDeleteKey(key: PoolKeyDetail) {
|
||||
if (keyPage.value.keys.length === 0 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
refreshOverviewInBackground()
|
||||
} catch (err) {
|
||||
showError(parseApiError(err, '删除账号失败'))
|
||||
} finally {
|
||||
@@ -1887,12 +1923,6 @@ async function handleDeleteKey(key: PoolKeyDetail) {
|
||||
}
|
||||
|
||||
async function copyFullKey(key: PoolKeyDetail) {
|
||||
const cached = revealedKeys.value.get(key.key_id)
|
||||
if (cached) {
|
||||
await copyToClipboard(cached)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await revealEndpointKey(key.key_id)
|
||||
let textToCopy = ''
|
||||
@@ -1912,7 +1942,6 @@ async function copyFullKey(key: PoolKeyDetail) {
|
||||
return
|
||||
}
|
||||
|
||||
revealedKeys.value.set(key.key_id, textToCopy)
|
||||
await copyToClipboard(textToCopy)
|
||||
} catch (err) {
|
||||
showError(parseApiError(err, '获取密钥失败'))
|
||||
@@ -1969,6 +1998,7 @@ async function clearCooldown(keyId: string) {
|
||||
const res = await clearPoolCooldown(selectedProviderId.value, keyId)
|
||||
success(res.message)
|
||||
await loadKeys()
|
||||
refreshOverviewInBackground()
|
||||
} catch (err) {
|
||||
showError(parseApiError(err))
|
||||
}
|
||||
@@ -1994,6 +2024,7 @@ async function toggleKeyActive(key: PoolKeyDetail) {
|
||||
}
|
||||
success(nextStatus ? '账号已启用' : '账号已停用')
|
||||
await loadKeys()
|
||||
refreshOverviewInBackground()
|
||||
} catch (err) {
|
||||
showError(parseApiError(err))
|
||||
} finally {
|
||||
@@ -2496,6 +2527,23 @@ function formatStatInteger(value: number | null | undefined): string {
|
||||
return Math.round(n).toLocaleString('en-US')
|
||||
}
|
||||
|
||||
function formatTokenCount(value: number | null | undefined): string {
|
||||
const n = Number(value ?? 0)
|
||||
if (!Number.isFinite(n) || n <= 0) return '0'
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
||||
return String(Math.round(n))
|
||||
}
|
||||
|
||||
function formatStatUsd(value: number | string | null | undefined): string {
|
||||
const n = Number(value ?? 0)
|
||||
if (!Number.isFinite(n) || n <= 0) return '$0.00'
|
||||
if (n < 0.01) return `$${n.toFixed(4)}`
|
||||
if (n < 1) return `$${n.toFixed(3)}`
|
||||
if (n < 1000) return `$${n.toFixed(2)}`
|
||||
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoStr: string): string {
|
||||
const date = new Date(isoStr)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
|
||||
Reference in New Issue
Block a user