Merge pull request #464 from touwaeriol/pr/antigravity-scope-ratelimit
feat(antigravity): 支持按配额域(scope)级别限流
This commit is contained in:
@@ -204,6 +204,17 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if scopeLimits := a.GetAntigravityScopeRateLimits(); len(scopeLimits) > 0 {
|
||||||
|
out.ScopeRateLimits = make(map[string]ScopeRateLimitInfo, len(scopeLimits))
|
||||||
|
now := time.Now()
|
||||||
|
for scope, remainingSec := range scopeLimits {
|
||||||
|
out.ScopeRateLimits[scope] = ScopeRateLimitInfo{
|
||||||
|
ResetAt: now.Add(time.Duration(remainingSec) * time.Second),
|
||||||
|
RemainingSec: remainingSec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package dto
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
type ScopeRateLimitInfo struct {
|
||||||
|
ResetAt time.Time `json:"reset_at"`
|
||||||
|
RemainingSec int64 `json:"remaining_sec"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@@ -108,6 +113,9 @@ type Account struct {
|
|||||||
RateLimitResetAt *time.Time `json:"rate_limit_reset_at"`
|
RateLimitResetAt *time.Time `json:"rate_limit_reset_at"`
|
||||||
OverloadUntil *time.Time `json:"overload_until"`
|
OverloadUntil *time.Time `json:"overload_until"`
|
||||||
|
|
||||||
|
// Antigravity scope 级限流状态(从 extra 提取)
|
||||||
|
ScopeRateLimits map[string]ScopeRateLimitInfo `json:"scope_rate_limits,omitempty"`
|
||||||
|
|
||||||
TempUnschedulableUntil *time.Time `json:"temp_unschedulable_until"`
|
TempUnschedulableUntil *time.Time `json:"temp_unschedulable_until"`
|
||||||
TempUnschedulableReason string `json:"temp_unschedulable_reason"`
|
TempUnschedulableReason string `json:"temp_unschedulable_reason"`
|
||||||
|
|
||||||
|
|||||||
@@ -809,12 +809,21 @@ func (r *accountRepository) SetAntigravityQuotaScopeLimit(ctx context.Context, i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
path := "{antigravity_quota_scopes," + string(scope) + "}"
|
scopeKey := string(scope)
|
||||||
client := clientFromContext(ctx, r.client)
|
client := clientFromContext(ctx, r.client)
|
||||||
result, err := client.ExecContext(
|
result, err := client.ExecContext(
|
||||||
ctx,
|
ctx,
|
||||||
"UPDATE accounts SET extra = jsonb_set(COALESCE(extra, '{}'::jsonb), $1::text[], $2::jsonb, true), updated_at = NOW() WHERE id = $3 AND deleted_at IS NULL",
|
`UPDATE accounts SET
|
||||||
path,
|
extra = jsonb_set(
|
||||||
|
jsonb_set(COALESCE(extra, '{}'::jsonb), '{antigravity_quota_scopes}'::text[], COALESCE(extra->'antigravity_quota_scopes', '{}'::jsonb), true),
|
||||||
|
ARRAY['antigravity_quota_scopes', $1]::text[],
|
||||||
|
$2::jsonb,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
updated_at = NOW(),
|
||||||
|
last_used_at = NOW()
|
||||||
|
WHERE id = $3 AND deleted_at IS NULL`,
|
||||||
|
scopeKey,
|
||||||
raw,
|
raw,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
@@ -829,6 +838,7 @@ func (r *accountRepository) SetAntigravityQuotaScopeLimit(ctx context.Context, i
|
|||||||
if affected == 0 {
|
if affected == 0 {
|
||||||
return service.ErrAccountNotFound
|
return service.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
|
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
|
||||||
log.Printf("[SchedulerOutbox] enqueue quota scope failed: account=%d err=%v", id, err)
|
log.Printf("[SchedulerOutbox] enqueue quota scope failed: account=%d err=%v", id, err)
|
||||||
}
|
}
|
||||||
@@ -849,12 +859,19 @@ func (r *accountRepository) SetModelRateLimit(ctx context.Context, id int64, sco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
path := "{model_rate_limits," + scope + "}"
|
|
||||||
client := clientFromContext(ctx, r.client)
|
client := clientFromContext(ctx, r.client)
|
||||||
result, err := client.ExecContext(
|
result, err := client.ExecContext(
|
||||||
ctx,
|
ctx,
|
||||||
"UPDATE accounts SET extra = jsonb_set(COALESCE(extra, '{}'::jsonb), $1::text[], $2::jsonb, true), updated_at = NOW() WHERE id = $3 AND deleted_at IS NULL",
|
`UPDATE accounts SET
|
||||||
path,
|
extra = jsonb_set(
|
||||||
|
jsonb_set(COALESCE(extra, '{}'::jsonb), '{model_rate_limits}'::text[], COALESCE(extra->'model_rate_limits', '{}'::jsonb), true),
|
||||||
|
ARRAY['model_rate_limits', $1]::text[],
|
||||||
|
$2::jsonb,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $3 AND deleted_at IS NULL`,
|
||||||
|
scope,
|
||||||
raw,
|
raw,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1537,7 +1537,11 @@ func sleepAntigravityBackoffWithContext(ctx context.Context, attempt int) bool {
|
|||||||
|
|
||||||
func antigravityUseScopeRateLimit() bool {
|
func antigravityUseScopeRateLimit() bool {
|
||||||
v := strings.ToLower(strings.TrimSpace(os.Getenv(antigravityScopeRateLimitEnv)))
|
v := strings.ToLower(strings.TrimSpace(os.Getenv(antigravityScopeRateLimitEnv)))
|
||||||
return v == "1" || v == "true" || v == "yes" || v == "on"
|
// 默认开启按配额域限流,只有明确设置为禁用值时才关闭
|
||||||
|
if v == "0" || v == "false" || v == "no" || v == "off" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte, quotaScope AntigravityQuotaScope) {
|
func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte, quotaScope AntigravityQuotaScope) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +364,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +379,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
|
||||||
|
|||||||
@@ -56,6 +56,65 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Rate Limit Indicator (429) -->
|
||||||
|
<div v-if="isRateLimited" class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
||||||
|
429
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.status.rateLimitedUntil', { time: formatTime(account.rate_limit_reset_at) }) }}
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scope Rate Limit Indicators (Antigravity) -->
|
||||||
|
<template v-if="activeScopeRateLimits.length > 0">
|
||||||
|
<div v-for="item in activeScopeRateLimits" :key="item.scope" class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 rounded bg-orange-100 px-1.5 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
|
||||||
|
>
|
||||||
|
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
||||||
|
{{ formatScopeName(item.scope) }}
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.status.scopeRateLimitedUntil', { scope: formatScopeName(item.scope), time: formatTime(item.reset_at) }) }}
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Overload Indicator (529) -->
|
||||||
|
<div v-if="isOverloaded" class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||||
|
>
|
||||||
|
<Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
|
||||||
|
529
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.status.overloadedUntil', { time: formatTime(account.overload_until) }) }}
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,7 +122,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { Account } from '@/types'
|
import type { Account } from '@/types'
|
||||||
import { formatCountdownWithSuffix } from '@/utils/format'
|
import { formatCountdownWithSuffix, formatTime } from '@/utils/format'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -81,6 +140,25 @@ const isRateLimited = computed(() => {
|
|||||||
return new Date(props.account.rate_limit_reset_at) > new Date()
|
return new Date(props.account.rate_limit_reset_at) > new Date()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Computed: active scope rate limits (Antigravity)
|
||||||
|
const activeScopeRateLimits = computed(() => {
|
||||||
|
const scopeLimits = props.account.scope_rate_limits
|
||||||
|
if (!scopeLimits) return []
|
||||||
|
const now = new Date()
|
||||||
|
return Object.entries(scopeLimits)
|
||||||
|
.filter(([, info]) => new Date(info.reset_at) > now)
|
||||||
|
.map(([scope, info]) => ({ scope, reset_at: info.reset_at }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatScopeName = (scope: string): string => {
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
claude: 'Claude',
|
||||||
|
gemini_text: 'Gemini',
|
||||||
|
gemini_image: 'Image'
|
||||||
|
}
|
||||||
|
return names[scope] || scope
|
||||||
|
}
|
||||||
|
|
||||||
// Computed: is overloaded (529)
|
// Computed: is overloaded (529)
|
||||||
const isOverloaded = computed(() => {
|
const isOverloaded = computed(() => {
|
||||||
if (!props.account.overload_until) return false
|
if (!props.account.overload_until) return false
|
||||||
|
|||||||
@@ -1191,6 +1191,7 @@ export default {
|
|||||||
overloaded: 'Overloaded',
|
overloaded: 'Overloaded',
|
||||||
tempUnschedulable: 'Temp Unschedulable',
|
tempUnschedulable: 'Temp Unschedulable',
|
||||||
rateLimitedUntil: 'Rate limited until {time}',
|
rateLimitedUntil: 'Rate limited until {time}',
|
||||||
|
scopeRateLimitedUntil: '{scope} rate limited until {time}',
|
||||||
overloadedUntil: 'Overloaded until {time}',
|
overloadedUntil: 'Overloaded until {time}',
|
||||||
viewTempUnschedDetails: 'View temp unschedulable details'
|
viewTempUnschedDetails: 'View temp unschedulable details'
|
||||||
},
|
},
|
||||||
@@ -2840,6 +2841,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'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1313,6 +1313,7 @@ export default {
|
|||||||
overloaded: '过载中',
|
overloaded: '过载中',
|
||||||
tempUnschedulable: '临时不可调度',
|
tempUnschedulable: '临时不可调度',
|
||||||
rateLimitedUntil: '限流中,重置时间:{time}',
|
rateLimitedUntil: '限流中,重置时间:{time}',
|
||||||
|
scopeRateLimitedUntil: '{scope} 限流中,重置时间:{time}',
|
||||||
overloadedUntil: '负载过重,重置时间:{time}',
|
overloadedUntil: '负载过重,重置时间:{time}',
|
||||||
viewTempUnschedDetails: '查看临时不可调度详情'
|
viewTempUnschedDetails: '查看临时不可调度详情'
|
||||||
},
|
},
|
||||||
@@ -2993,6 +2994,7 @@ export default {
|
|||||||
empty: '暂无数据',
|
empty: '暂无数据',
|
||||||
queued: '队列 {count}',
|
queued: '队列 {count}',
|
||||||
rateLimited: '限流 {count}',
|
rateLimited: '限流 {count}',
|
||||||
|
scopeRateLimitedTooltip: '{scope} 限流中 ({count} 个账号)',
|
||||||
errorAccounts: '异常 {count}',
|
errorAccounts: '异常 {count}',
|
||||||
loadFailed: '加载并发数据失败'
|
loadFailed: '加载并发数据失败'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -561,6 +561,9 @@ export interface Account {
|
|||||||
temp_unschedulable_until: string | null
|
temp_unschedulable_until: string | null
|
||||||
temp_unschedulable_reason: string | null
|
temp_unschedulable_reason: string | null
|
||||||
|
|
||||||
|
// Antigravity scope 级限流状态
|
||||||
|
scope_rate_limits?: Record<string, { reset_at: string; remaining_sec: number }>
|
||||||
|
|
||||||
// Session window fields (5-hour window)
|
// Session window fields (5-hour window)
|
||||||
session_window_start: string | null
|
session_window_start: string | null
|
||||||
session_window_end: string | null
|
session_window_end: string | null
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user