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

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