feat(ops): 运维界面展示 Antigravity 账号 scope 级别限流统计

在运维监控的并发/排队卡片中,为 Antigravity 平台账号显示各 scope
(claude/gemini_text/gemini_image) 的限流数量统计,便于管理员了解
哪些 scope 正在被限流。
This commit is contained in:
song
2026-01-27 09:34:10 +08:00
parent 7cea6b6fc9
commit 08d6dc5227
7 changed files with 98 additions and 18 deletions

View File

@@ -89,3 +89,30 @@ func (a *Account) antigravityQuotaScopeResetAt(scope AntigravityQuotaScope) *tim
} }
return &resetAt return &resetAt
} }
var antigravityAllScopes = []AntigravityQuotaScope{
AntigravityQuotaScopeClaude,
AntigravityQuotaScopeGeminiText,
AntigravityQuotaScopeGeminiImage,
}
func (a *Account) GetAntigravityScopeRateLimits() map[string]int64 {
if a == nil || a.Platform != PlatformAntigravity {
return nil
}
now := time.Now()
result := make(map[string]int64)
for _, scope := range antigravityAllScopes {
resetAt := a.antigravityQuotaScopeResetAt(scope)
if resetAt != nil && now.Before(*resetAt) {
remainingSec := int64(time.Until(*resetAt).Seconds())
if remainingSec > 0 {
result[string(scope)] = remainingSec
}
}
}
if len(result) == 0 {
return nil
}
return result
}

View File

@@ -67,6 +67,8 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
isAvailable := acc.Status == StatusActive && acc.Schedulable && !isRateLimited && !isOverloaded && !isTempUnsched isAvailable := acc.Status == StatusActive && acc.Schedulable && !isRateLimited && !isOverloaded && !isTempUnsched
scopeRateLimits := acc.GetAntigravityScopeRateLimits()
if acc.Platform != "" { if acc.Platform != "" {
if _, ok := platform[acc.Platform]; !ok { if _, ok := platform[acc.Platform]; !ok {
platform[acc.Platform] = &PlatformAvailability{ platform[acc.Platform] = &PlatformAvailability{
@@ -84,6 +86,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
if hasError { if hasError {
p.ErrorCount++ p.ErrorCount++
} }
if len(scopeRateLimits) > 0 {
if p.ScopeRateLimitCount == nil {
p.ScopeRateLimitCount = make(map[string]int64)
}
for scope := range scopeRateLimits {
p.ScopeRateLimitCount[scope]++
}
}
} }
for _, grp := range acc.Groups { for _, grp := range acc.Groups {
@@ -108,6 +118,14 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
if hasError { if hasError {
g.ErrorCount++ g.ErrorCount++
} }
if len(scopeRateLimits) > 0 {
if g.ScopeRateLimitCount == nil {
g.ScopeRateLimitCount = make(map[string]int64)
}
for scope := range scopeRateLimits {
g.ScopeRateLimitCount[scope]++
}
}
} }
displayGroupID := int64(0) displayGroupID := int64(0)
@@ -140,6 +158,9 @@ func (s *OpsService) GetAccountAvailabilityStats(ctx context.Context, platformFi
item.RateLimitRemainingSec = &remainingSec item.RateLimitRemainingSec = &remainingSec
} }
} }
if len(scopeRateLimits) > 0 {
item.ScopeRateLimits = scopeRateLimits
}
if isOverloaded && acc.OverloadUntil != nil { if isOverloaded && acc.OverloadUntil != nil {
item.OverloadUntil = acc.OverloadUntil item.OverloadUntil = acc.OverloadUntil
remainingSec := int64(time.Until(*acc.OverloadUntil).Seconds()) remainingSec := int64(time.Until(*acc.OverloadUntil).Seconds())

View File

@@ -39,22 +39,24 @@ type AccountConcurrencyInfo struct {
// PlatformAvailability aggregates account availability by platform. // PlatformAvailability aggregates account availability by platform.
type PlatformAvailability struct { type PlatformAvailability struct {
Platform string `json:"platform"` Platform string `json:"platform"`
TotalAccounts int64 `json:"total_accounts"` TotalAccounts int64 `json:"total_accounts"`
AvailableCount int64 `json:"available_count"` AvailableCount int64 `json:"available_count"`
RateLimitCount int64 `json:"rate_limit_count"` RateLimitCount int64 `json:"rate_limit_count"`
ErrorCount int64 `json:"error_count"` ScopeRateLimitCount map[string]int64 `json:"scope_rate_limit_count,omitempty"`
ErrorCount int64 `json:"error_count"`
} }
// GroupAvailability aggregates account availability by group. // GroupAvailability aggregates account availability by group.
type GroupAvailability struct { type GroupAvailability struct {
GroupID int64 `json:"group_id"` GroupID int64 `json:"group_id"`
GroupName string `json:"group_name"` GroupName string `json:"group_name"`
Platform string `json:"platform"` Platform string `json:"platform"`
TotalAccounts int64 `json:"total_accounts"` TotalAccounts int64 `json:"total_accounts"`
AvailableCount int64 `json:"available_count"` AvailableCount int64 `json:"available_count"`
RateLimitCount int64 `json:"rate_limit_count"` RateLimitCount int64 `json:"rate_limit_count"`
ErrorCount int64 `json:"error_count"` ScopeRateLimitCount map[string]int64 `json:"scope_rate_limit_count,omitempty"`
ErrorCount int64 `json:"error_count"`
} }
// AccountAvailability represents current availability for a single account. // AccountAvailability represents current availability for a single account.
@@ -72,10 +74,11 @@ type AccountAvailability struct {
IsOverloaded bool `json:"is_overloaded"` IsOverloaded bool `json:"is_overloaded"`
HasError bool `json:"has_error"` HasError bool `json:"has_error"`
RateLimitResetAt *time.Time `json:"rate_limit_reset_at"` RateLimitResetAt *time.Time `json:"rate_limit_reset_at"`
RateLimitRemainingSec *int64 `json:"rate_limit_remaining_sec"` RateLimitRemainingSec *int64 `json:"rate_limit_remaining_sec"`
OverloadUntil *time.Time `json:"overload_until"` ScopeRateLimits map[string]int64 `json:"scope_rate_limits,omitempty"`
OverloadRemainingSec *int64 `json:"overload_remaining_sec"` OverloadUntil *time.Time `json:"overload_until"`
ErrorMessage string `json:"error_message"` OverloadRemainingSec *int64 `json:"overload_remaining_sec"`
TempUnschedulableUntil *time.Time `json:"temp_unschedulable_until,omitempty"` ErrorMessage string `json:"error_message"`
TempUnschedulableUntil *time.Time `json:"temp_unschedulable_until,omitempty"`
} }

View File

@@ -355,6 +355,7 @@ export interface PlatformAvailability {
total_accounts: number total_accounts: number
available_count: number available_count: number
rate_limit_count: number rate_limit_count: number
scope_rate_limit_count?: Record<string, number>
error_count: number error_count: number
} }
@@ -365,6 +366,7 @@ export interface GroupAvailability {
total_accounts: number total_accounts: number
available_count: number available_count: number
rate_limit_count: number rate_limit_count: number
scope_rate_limit_count?: Record<string, number>
error_count: number error_count: number
} }
@@ -379,6 +381,7 @@ export interface AccountAvailability {
is_rate_limited: boolean is_rate_limited: boolean
rate_limit_reset_at?: string rate_limit_reset_at?: string
rate_limit_remaining_sec?: number rate_limit_remaining_sec?: number
scope_rate_limits?: Record<string, number>
is_overloaded: boolean is_overloaded: boolean
overload_until?: string overload_until?: string
overload_remaining_sec?: number overload_remaining_sec?: number

View File

@@ -2617,6 +2617,7 @@ export default {
empty: 'No data', empty: 'No data',
queued: 'Queue {count}', queued: 'Queue {count}',
rateLimited: 'Rate-limited {count}', rateLimited: 'Rate-limited {count}',
scopeRateLimitedTooltip: '{scope} rate-limited ({count} accounts)',
errorAccounts: 'Errors {count}', errorAccounts: 'Errors {count}',
loadFailed: 'Failed to load concurrency data' loadFailed: 'Failed to load concurrency data'
}, },

View File

@@ -2771,6 +2771,7 @@ export default {
empty: '暂无数据', empty: '暂无数据',
queued: '队列 {count}', queued: '队列 {count}',
rateLimited: '限流 {count}', rateLimited: '限流 {count}',
scopeRateLimitedTooltip: '{scope} 限流中 ({count} 个账号)',
errorAccounts: '异常 {count}', errorAccounts: '异常 {count}',
loadFailed: '加载并发数据失败' loadFailed: '加载并发数据失败'
}, },

View File

@@ -49,6 +49,7 @@ interface SummaryRow {
total_accounts: number total_accounts: number
available_accounts: number available_accounts: number
rate_limited_accounts: number rate_limited_accounts: number
scope_rate_limit_count?: Record<string, number>
error_accounts: number error_accounts: number
// 并发统计 // 并发统计
total_concurrency: number total_concurrency: number
@@ -102,6 +103,7 @@ const platformRows = computed((): SummaryRow[] => {
total_accounts: totalAccounts, total_accounts: totalAccounts,
available_accounts: availableAccounts, available_accounts: availableAccounts,
rate_limited_accounts: safeNumber(avail.rate_limit_count), rate_limited_accounts: safeNumber(avail.rate_limit_count),
scope_rate_limit_count: avail.scope_rate_limit_count,
error_accounts: safeNumber(avail.error_count), error_accounts: safeNumber(avail.error_count),
total_concurrency: totalConcurrency, total_concurrency: totalConcurrency,
used_concurrency: usedConcurrency, used_concurrency: usedConcurrency,
@@ -141,6 +143,7 @@ const groupRows = computed((): SummaryRow[] => {
total_accounts: totalAccounts, total_accounts: totalAccounts,
available_accounts: availableAccounts, available_accounts: availableAccounts,
rate_limited_accounts: safeNumber(avail.rate_limit_count), rate_limited_accounts: safeNumber(avail.rate_limit_count),
scope_rate_limit_count: avail.scope_rate_limit_count,
error_accounts: safeNumber(avail.error_count), error_accounts: safeNumber(avail.error_count),
total_concurrency: totalConcurrency, total_concurrency: totalConcurrency,
used_concurrency: usedConcurrency, used_concurrency: usedConcurrency,
@@ -269,6 +272,15 @@ function formatDuration(seconds: number): string {
return `${hours}h` return `${hours}h`
} }
function formatScopeName(scope: string): string {
const names: Record<string, string> = {
claude: 'Claude',
gemini_text: 'Gemini',
gemini_image: 'Image'
}
return names[scope] || scope
}
watch( watch(
() => realtimeEnabled.value, () => realtimeEnabled.value,
async (enabled) => { async (enabled) => {
@@ -387,6 +399,18 @@ watch(
{{ t('admin.ops.concurrency.rateLimited', { count: row.rate_limited_accounts }) }} {{ t('admin.ops.concurrency.rateLimited', { count: row.rate_limited_accounts }) }}
</span> </span>
<!-- Scope 限流 ( Antigravity) -->
<template v-if="row.scope_rate_limit_count && Object.keys(row.scope_rate_limit_count).length > 0">
<span
v-for="(count, scope) in row.scope_rate_limit_count"
:key="scope"
class="rounded-full bg-orange-100 px-1.5 py-0.5 font-semibold text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
:title="t('admin.ops.concurrency.scopeRateLimitedTooltip', { scope, count })"
>
{{ formatScopeName(scope as string) }} {{ count }}
</span>
</template>
<!-- 异常账号 --> <!-- 异常账号 -->
<span <span
v-if="row.error_accounts > 0" v-if="row.error_accounts > 0"