mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
feat: Anthropic 账号被动用量采样,页面默认展示被动数据
从上游 /v1/messages 响应头被动采集 5h/7d utilization 并存储到 Account.Extra,页面加载时直接读取本地数据而非调用外部 Usage API。 用户可点击"查询"按钮主动拉取最新数据,主动查询结果自动回写被动缓存。 后端: - UpdateSessionWindow 合并采集 5h + 7d headers 为单次 DB 写入 - 新增 GetPassiveUsage 从 Extra 构建 UsageInfo (复用 estimateSetupTokenUsage) - GetUsage 主动查询后 syncActiveToPassive 回写被动缓存 - passive_usage_ 前缀注册为 scheduler-neutral 前端: - Anthropic 账号 mount/refresh 默认 source=passive - 新增"被动采样"标签和"查询"按钮 (带 loading 动画)
This commit is contained in:
@@ -223,8 +223,10 @@ export async function clearError(id: number): Promise<Account> {
|
||||
* @param id - Account ID
|
||||
* @returns Account usage info
|
||||
*/
|
||||
export async function getUsage(id: number): Promise<AccountUsageInfo> {
|
||||
const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`)
|
||||
export async function getUsage(id: number, source?: 'passive' | 'active'): Promise<AccountUsageInfo> {
|
||||
const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`, {
|
||||
params: source ? { source } : undefined
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,38 @@
|
||||
:resets-at="usageInfo.seven_day_sonnet.resets_at"
|
||||
color="purple"
|
||||
/>
|
||||
|
||||
<!-- Passive sampling label + active query button -->
|
||||
<div class="flex items-center gap-1.5 mt-0.5">
|
||||
<span
|
||||
v-if="usageInfo.source === 'passive'"
|
||||
class="text-[9px] text-gray-400 dark:text-gray-500 italic"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.passiveSampled') }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[9px] font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/30 transition-colors"
|
||||
:disabled="activeQueryLoading"
|
||||
@click="loadActiveUsage"
|
||||
>
|
||||
<svg
|
||||
class="h-2.5 w-2.5"
|
||||
:class="{ 'animate-spin': activeQueryLoading }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.usageWindow.activeQuery') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data yet -->
|
||||
@@ -433,6 +465,7 @@ const props = withDefaults(
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(false)
|
||||
const activeQueryLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const usageInfo = ref<AccountUsageInfo | null>(null)
|
||||
|
||||
@@ -888,14 +921,18 @@ const copyValidationURL = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadUsage = async () => {
|
||||
const isAnthropicOAuthOrSetupToken = computed(() => {
|
||||
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
|
||||
})
|
||||
|
||||
const loadUsage = async (source?: 'passive' | 'active') => {
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id)
|
||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, source)
|
||||
} catch (e: any) {
|
||||
error.value = t('common.error')
|
||||
console.error('Failed to load usage:', e)
|
||||
@@ -904,6 +941,17 @@ const loadUsage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadActiveUsage = async () => {
|
||||
activeQueryLoading.value = true
|
||||
try {
|
||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active')
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load active usage:', e)
|
||||
} finally {
|
||||
activeQueryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== API Key quota progress bars =====
|
||||
|
||||
interface QuotaBarInfo {
|
||||
@@ -993,7 +1041,8 @@ const formatKeyUserCost = computed(() => {
|
||||
|
||||
onMounted(() => {
|
||||
if (!shouldAutoLoadUsageOnMount.value) return
|
||||
loadUsage()
|
||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
||||
loadUsage(source)
|
||||
})
|
||||
|
||||
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
||||
@@ -1011,7 +1060,8 @@ watch(
|
||||
if (nextToken === prevToken) return
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
loadUsage().catch((e) => {
|
||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
||||
loadUsage(source).catch((e) => {
|
||||
console.error('Failed to refresh usage after manual refresh:', e)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2760,7 +2760,9 @@ export default {
|
||||
gemini3Pro: 'G3P',
|
||||
gemini3Flash: 'G3F',
|
||||
gemini3Image: 'G31FI',
|
||||
claude: 'Claude'
|
||||
claude: 'Claude',
|
||||
passiveSampled: 'Passive',
|
||||
activeQuery: 'Query'
|
||||
},
|
||||
tier: {
|
||||
free: 'Free',
|
||||
|
||||
@@ -2163,7 +2163,9 @@ export default {
|
||||
gemini3Pro: 'G3P',
|
||||
gemini3Flash: 'G3F',
|
||||
gemini3Image: 'G31FI',
|
||||
claude: 'Claude'
|
||||
claude: 'Claude',
|
||||
passiveSampled: '被动采样',
|
||||
activeQuery: '查询'
|
||||
},
|
||||
tier: {
|
||||
free: 'Free',
|
||||
|
||||
@@ -781,6 +781,7 @@ export interface AntigravityModelQuota {
|
||||
}
|
||||
|
||||
export interface AccountUsageInfo {
|
||||
source?: 'passive' | 'active'
|
||||
updated_at: string | null
|
||||
five_hour: UsageProgress | null
|
||||
seven_day: UsageProgress | null
|
||||
|
||||
Reference in New Issue
Block a user