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:
shaw
2026-03-19 17:29:21 +08:00
parent a6764e82f2
commit 525cdb8830
9 changed files with 183 additions and 17 deletions

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -2760,7 +2760,9 @@ export default {
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G31FI',
claude: 'Claude'
claude: 'Claude',
passiveSampled: 'Passive',
activeQuery: 'Query'
},
tier: {
free: 'Free',

View File

@@ -2163,7 +2163,9 @@ export default {
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G31FI',
claude: 'Claude'
claude: 'Claude',
passiveSampled: '被动采样',
activeQuery: '查询'
},
tier: {
free: 'Free',

View File

@@ -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