feat(antigravity): 添加配额窗口显示功能
后端: - 新增 AntigravityQuotaRefresher 定时刷新配额 - Client 添加 FetchAvailableModels 方法获取模型配额 - 配额数据存入 account.extra.quota 字段 前端: - AccountUsageCell 支持显示 Antigravity 账户配额 - UsageProgressBar 新增 amber 颜色 - 显示 G3P/G3F/G3I/C4.5 四个配额进度条
This commit is contained in:
@@ -71,6 +71,7 @@ func provideCleanup(
|
|||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
antigravityOAuth *service.AntigravityOAuthService,
|
antigravityOAuth *service.AntigravityOAuthService,
|
||||||
|
antigravityQuota *service.AntigravityQuotaRefresher,
|
||||||
) func() {
|
) func() {
|
||||||
return func() {
|
return func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@@ -109,6 +110,10 @@ func provideCleanup(
|
|||||||
antigravityOAuth.Stop()
|
antigravityOAuth.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AntigravityQuotaRefresher", func() error {
|
||||||
|
antigravityQuota.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"Redis", func() error {
|
{"Redis", func() error {
|
||||||
return rdb.Close()
|
return rdb.Close()
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
||||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
||||||
v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
antigravityQuotaRefresher := service.ProvideAntigravityQuotaRefresher(accountRepository, proxyRepository, antigravityOAuthService, configConfig)
|
||||||
|
v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, antigravityQuotaRefresher)
|
||||||
application := &Application{
|
application := &Application{
|
||||||
Server: httpServer,
|
Server: httpServer,
|
||||||
Cleanup: v,
|
Cleanup: v,
|
||||||
@@ -168,6 +169,7 @@ func provideCleanup(
|
|||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
antigravityOAuth *service.AntigravityOAuthService,
|
antigravityOAuth *service.AntigravityOAuthService,
|
||||||
|
antigravityQuota *service.AntigravityQuotaRefresher,
|
||||||
) func() {
|
) func() {
|
||||||
return func() {
|
return func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@@ -205,6 +207,10 @@ func provideCleanup(
|
|||||||
antigravityOAuth.Stop()
|
antigravityOAuth.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AntigravityQuotaRefresher", func() error {
|
||||||
|
antigravityQuota.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"Redis", func() error {
|
{"Redis", func() error {
|
||||||
return rdb.Close()
|
return rdb.Close()
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -214,3 +214,64 @@ func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadC
|
|||||||
|
|
||||||
return &loadResp, nil
|
return &loadResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ModelQuotaInfo 模型配额信息
|
||||||
|
type ModelQuotaInfo struct {
|
||||||
|
RemainingFraction float64 `json:"remainingFraction"`
|
||||||
|
ResetTime string `json:"resetTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelInfo 模型信息
|
||||||
|
type ModelInfo struct {
|
||||||
|
QuotaInfo *ModelQuotaInfo `json:"quotaInfo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchAvailableModelsRequest fetchAvailableModels 请求
|
||||||
|
type FetchAvailableModelsRequest struct {
|
||||||
|
Project string `json:"project"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchAvailableModelsResponse fetchAvailableModels 响应
|
||||||
|
type FetchAvailableModelsResponse struct {
|
||||||
|
Models map[string]ModelInfo `json:"models"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchAvailableModels 获取可用模型和配额信息
|
||||||
|
func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectID string) (*FetchAvailableModelsResponse, error) {
|
||||||
|
reqBody := FetchAvailableModelsRequest{Project: projectID}
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := BaseURL + "/v1internal:fetchAvailableModels"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(bodyBytes)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetchAvailableModels 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
respBodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("fetchAvailableModels 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelsResp FetchAvailableModelsResponse
|
||||||
|
if err := json.Unmarshal(respBodyBytes, &modelsResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("响应解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &modelsResp, nil
|
||||||
|
}
|
||||||
|
|||||||
203
backend/internal/service/antigravity_quota_refresher.go
Normal file
203
backend/internal/service/antigravity_quota_refresher.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AntigravityQuotaRefresher 定时刷新 Antigravity 账户的配额信息
|
||||||
|
type AntigravityQuotaRefresher struct {
|
||||||
|
accountRepo AccountRepository
|
||||||
|
proxyRepo ProxyRepository
|
||||||
|
oauthSvc *AntigravityOAuthService
|
||||||
|
cfg *config.TokenRefreshConfig
|
||||||
|
|
||||||
|
stopCh chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAntigravityQuotaRefresher 创建配额刷新器
|
||||||
|
func NewAntigravityQuotaRefresher(
|
||||||
|
accountRepo AccountRepository,
|
||||||
|
proxyRepo ProxyRepository,
|
||||||
|
oauthSvc *AntigravityOAuthService,
|
||||||
|
cfg *config.Config,
|
||||||
|
) *AntigravityQuotaRefresher {
|
||||||
|
return &AntigravityQuotaRefresher{
|
||||||
|
accountRepo: accountRepo,
|
||||||
|
proxyRepo: proxyRepo,
|
||||||
|
oauthSvc: oauthSvc,
|
||||||
|
cfg: &cfg.TokenRefresh,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动后台配额刷新服务
|
||||||
|
func (r *AntigravityQuotaRefresher) Start() {
|
||||||
|
if !r.cfg.Enabled {
|
||||||
|
log.Println("[AntigravityQuota] Service disabled by configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.wg.Add(1)
|
||||||
|
go r.refreshLoop()
|
||||||
|
|
||||||
|
log.Printf("[AntigravityQuota] Service started (check every %d minutes)", r.cfg.CheckIntervalMinutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止服务
|
||||||
|
func (r *AntigravityQuotaRefresher) Stop() {
|
||||||
|
close(r.stopCh)
|
||||||
|
r.wg.Wait()
|
||||||
|
log.Println("[AntigravityQuota] Service stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshLoop 刷新循环
|
||||||
|
func (r *AntigravityQuotaRefresher) refreshLoop() {
|
||||||
|
defer r.wg.Done()
|
||||||
|
|
||||||
|
checkInterval := time.Duration(r.cfg.CheckIntervalMinutes) * time.Minute
|
||||||
|
if checkInterval < time.Minute {
|
||||||
|
checkInterval = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(checkInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// 启动时立即执行一次
|
||||||
|
r.processRefresh()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
r.processRefresh()
|
||||||
|
case <-r.stopCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processRefresh 执行一次刷新
|
||||||
|
func (r *AntigravityQuotaRefresher) processRefresh() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 查询所有 active 的账户,然后过滤 antigravity 平台
|
||||||
|
allAccounts, err := r.accountRepo.ListActive(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AntigravityQuota] Failed to list accounts: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤 antigravity 平台账户
|
||||||
|
var accounts []Account
|
||||||
|
for _, acc := range allAccounts {
|
||||||
|
if acc.Platform == PlatformAntigravity {
|
||||||
|
accounts = append(accounts, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accounts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshed, failed := 0, 0
|
||||||
|
|
||||||
|
for i := range accounts {
|
||||||
|
account := &accounts[i]
|
||||||
|
|
||||||
|
if err := r.refreshAccountQuota(ctx, account); err != nil {
|
||||||
|
log.Printf("[AntigravityQuota] Account %d (%s) failed: %v", account.ID, account.Name, err)
|
||||||
|
failed++
|
||||||
|
} else {
|
||||||
|
refreshed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[AntigravityQuota] Cycle complete: total=%d, refreshed=%d, failed=%d",
|
||||||
|
len(accounts), refreshed, failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshAccountQuota 刷新单个账户的配额
|
||||||
|
func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, account *Account) error {
|
||||||
|
accessToken := account.GetCredential("access_token")
|
||||||
|
projectID := account.GetCredential("project_id")
|
||||||
|
|
||||||
|
if accessToken == "" || projectID == "" {
|
||||||
|
return nil // 没有有效凭证,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 token 是否过期,过期则刷新
|
||||||
|
if r.isTokenExpired(account) {
|
||||||
|
tokenInfo, err := r.oauthSvc.RefreshAccountToken(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
accessToken = tokenInfo.AccessToken
|
||||||
|
// 更新凭证
|
||||||
|
account.Credentials = r.oauthSvc.BuildAccountCredentials(tokenInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取代理 URL
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
proxy, err := r.proxyRepo.GetByID(ctx, *account.ProxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 API 获取配额
|
||||||
|
client := antigravity.NewClient(proxyURL)
|
||||||
|
modelsResp, err := client.FetchAvailableModels(ctx, accessToken, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配额数据并更新 extra 字段
|
||||||
|
r.updateAccountQuota(account, modelsResp)
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
return r.accountRepo.Update(ctx, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTokenExpired 检查 token 是否过期
|
||||||
|
func (r *AntigravityQuotaRefresher) isTokenExpired(account *Account) bool {
|
||||||
|
expiresAt := parseAntigravityExpiresAt(account)
|
||||||
|
if expiresAt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提前 5 分钟认为过期
|
||||||
|
return time.Now().Add(5 * time.Minute).After(*expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateAccountQuota 更新账户的配额信息
|
||||||
|
func (r *AntigravityQuotaRefresher) updateAccountQuota(account *Account, modelsResp *antigravity.FetchAvailableModelsResponse) {
|
||||||
|
if account.Extra == nil {
|
||||||
|
account.Extra = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
quota := make(map[string]any)
|
||||||
|
|
||||||
|
for modelName, modelInfo := range modelsResp.Models {
|
||||||
|
if modelInfo.QuotaInfo == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 remainingFraction (0.0-1.0) 为百分比 (0-100)
|
||||||
|
remaining := int(modelInfo.QuotaInfo.RemainingFraction * 100)
|
||||||
|
|
||||||
|
quota[modelName] = map[string]any{
|
||||||
|
"remaining": remaining,
|
||||||
|
"reset_time": modelInfo.QuotaInfo.ResetTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Extra["quota"] = quota
|
||||||
|
account.Extra["last_quota_check"] = time.Now().Format(time.RFC3339)
|
||||||
|
}
|
||||||
@@ -54,6 +54,18 @@ func ProvideTimingWheelService() *TimingWheelService {
|
|||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideAntigravityQuotaRefresher creates and starts AntigravityQuotaRefresher
|
||||||
|
func ProvideAntigravityQuotaRefresher(
|
||||||
|
accountRepo AccountRepository,
|
||||||
|
proxyRepo ProxyRepository,
|
||||||
|
oauthSvc *AntigravityOAuthService,
|
||||||
|
cfg *config.Config,
|
||||||
|
) *AntigravityQuotaRefresher {
|
||||||
|
svc := NewAntigravityQuotaRefresher(accountRepo, proxyRepo, oauthSvc, cfg)
|
||||||
|
svc.Start()
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideDeferredService creates and starts DeferredService
|
// ProvideDeferredService creates and starts DeferredService
|
||||||
func ProvideDeferredService(accountRepo AccountRepository, timingWheel *TimingWheelService) *DeferredService {
|
func ProvideDeferredService(accountRepo AccountRepository, timingWheel *TimingWheelService) *DeferredService {
|
||||||
svc := NewDeferredService(accountRepo, timingWheel, 10*time.Second)
|
svc := NewDeferredService(accountRepo, timingWheel, 10*time.Second)
|
||||||
@@ -102,4 +114,5 @@ var ProviderSet = wire.NewSet(
|
|||||||
ProvideTokenRefreshService,
|
ProvideTokenRefreshService,
|
||||||
ProvideTimingWheelService,
|
ProvideTimingWheelService,
|
||||||
ProvideDeferredService,
|
ProvideDeferredService,
|
||||||
|
ProvideAntigravityQuotaRefresher,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,48 @@
|
|||||||
<div v-else class="text-xs text-gray-400">-</div>
|
<div v-else class="text-xs text-gray-400">-</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Antigravity OAuth accounts: show quota from extra field -->
|
||||||
|
<template v-else-if="account.platform === 'antigravity' && account.type === 'oauth'">
|
||||||
|
<div v-if="hasAntigravityQuota" class="space-y-1">
|
||||||
|
<!-- Gemini 3 Pro -->
|
||||||
|
<UsageProgressBar
|
||||||
|
v-if="antigravity3ProUsage !== null"
|
||||||
|
:label="t('admin.accounts.usageWindow.gemini3Pro')"
|
||||||
|
:utilization="antigravity3ProUsage.utilization"
|
||||||
|
:resets-at="antigravity3ProUsage.resetTime"
|
||||||
|
color="indigo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Gemini 3 Flash -->
|
||||||
|
<UsageProgressBar
|
||||||
|
v-if="antigravity3FlashUsage !== null"
|
||||||
|
:label="t('admin.accounts.usageWindow.gemini3Flash')"
|
||||||
|
:utilization="antigravity3FlashUsage.utilization"
|
||||||
|
:resets-at="antigravity3FlashUsage.resetTime"
|
||||||
|
color="emerald"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Gemini 3 Image -->
|
||||||
|
<UsageProgressBar
|
||||||
|
v-if="antigravity3ImageUsage !== null"
|
||||||
|
:label="t('admin.accounts.usageWindow.gemini3Image')"
|
||||||
|
:utilization="antigravity3ImageUsage.utilization"
|
||||||
|
:resets-at="antigravity3ImageUsage.resetTime"
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Claude 4.5 -->
|
||||||
|
<UsageProgressBar
|
||||||
|
v-if="antigravityClaude45Usage !== null"
|
||||||
|
:label="t('admin.accounts.usageWindow.claude45')"
|
||||||
|
:utilization="antigravityClaude45Usage.utilization"
|
||||||
|
:resets-at="antigravityClaude45Usage.resetTime"
|
||||||
|
color="amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-xs text-gray-400">-</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Other accounts: no usage window -->
|
<!-- Other accounts: no usage window -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-xs text-gray-400">-</div>
|
<div class="text-xs text-gray-400">-</div>
|
||||||
@@ -273,6 +315,82 @@ const codex7dResetAt = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Antigravity quota types
|
||||||
|
interface AntigravityModelQuota {
|
||||||
|
remaining: number // 剩余百分比 0-100
|
||||||
|
reset_time: string // ISO 8601 重置时间
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AntigravityQuotaData {
|
||||||
|
[model: string]: AntigravityModelQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AntigravityUsageResult {
|
||||||
|
utilization: number
|
||||||
|
resetTime: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antigravity quota computed properties
|
||||||
|
const hasAntigravityQuota = computed(() => {
|
||||||
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
||||||
|
return extra && typeof extra.quota === 'object' && extra.quota !== null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从配额数据中获取使用率(多模型取最低剩余 = 最高使用)
|
||||||
|
const getAntigravityUsage = (
|
||||||
|
modelNames: string[]
|
||||||
|
): AntigravityUsageResult | null => {
|
||||||
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
||||||
|
if (!extra || typeof extra.quota !== 'object' || extra.quota === null) return null
|
||||||
|
|
||||||
|
const quota = extra.quota as AntigravityQuotaData
|
||||||
|
|
||||||
|
let minRemaining = 100
|
||||||
|
let earliestReset: string | null = null
|
||||||
|
|
||||||
|
for (const model of modelNames) {
|
||||||
|
const modelQuota = quota[model]
|
||||||
|
if (!modelQuota) continue
|
||||||
|
|
||||||
|
if (modelQuota.remaining < minRemaining) {
|
||||||
|
minRemaining = modelQuota.remaining
|
||||||
|
}
|
||||||
|
if (modelQuota.reset_time) {
|
||||||
|
if (!earliestReset || modelQuota.reset_time < earliestReset) {
|
||||||
|
earliestReset = modelQuota.reset_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到任何匹配的模型
|
||||||
|
if (minRemaining === 100 && earliestReset === null) {
|
||||||
|
// 检查是否至少有一个模型有数据
|
||||||
|
const hasAnyData = modelNames.some((m) => quota[m])
|
||||||
|
if (!hasAnyData) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
utilization: 100 - minRemaining,
|
||||||
|
resetTime: earliestReset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini 3 Pro: gemini-3-pro-low, gemini-3-pro-high, gemini-3-pro-preview
|
||||||
|
const antigravity3ProUsage = computed(() =>
|
||||||
|
getAntigravityUsage(['gemini-3-pro-low', 'gemini-3-pro-high', 'gemini-3-pro-preview'])
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gemini 3 Flash: gemini-3-flash
|
||||||
|
const antigravity3FlashUsage = computed(() => getAntigravityUsage(['gemini-3-flash']))
|
||||||
|
|
||||||
|
// Gemini 3 Image: gemini-3-pro-image
|
||||||
|
const antigravity3ImageUsage = computed(() => getAntigravityUsage(['gemini-3-pro-image']))
|
||||||
|
|
||||||
|
// Claude 4.5: claude-sonnet-4-5, claude-opus-4-5-thinking
|
||||||
|
const antigravityClaude45Usage = computed(() =>
|
||||||
|
getAntigravityUsage(['claude-sonnet-4-5', 'claude-opus-4-5-thinking'])
|
||||||
|
)
|
||||||
|
|
||||||
const loadUsage = async () => {
|
const loadUsage = async () => {
|
||||||
// Fetch usage for Anthropic OAuth and Setup Token accounts
|
// Fetch usage for Anthropic OAuth and Setup Token accounts
|
||||||
// OpenAI usage comes from account.extra field (updated during forwarding)
|
// OpenAI usage comes from account.extra field (updated during forwarding)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const props = defineProps<{
|
|||||||
label: string
|
label: string
|
||||||
utilization: number // Percentage (0-100+)
|
utilization: number // Percentage (0-100+)
|
||||||
resetsAt?: string | null
|
resetsAt?: string | null
|
||||||
color: 'indigo' | 'emerald' | 'purple'
|
color: 'indigo' | 'emerald' | 'purple' | 'amber'
|
||||||
windowStats?: WindowStats | null
|
windowStats?: WindowStats | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -69,7 +69,8 @@ const labelClass = computed(() => {
|
|||||||
const colors = {
|
const colors = {
|
||||||
indigo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
|
indigo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
|
||||||
emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
||||||
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300'
|
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
||||||
|
amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
|
||||||
}
|
}
|
||||||
return colors[props.color]
|
return colors[props.color]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1157,7 +1157,11 @@ export default {
|
|||||||
noData: 'No usage data available for this account'
|
noData: 'No usage data available for this account'
|
||||||
},
|
},
|
||||||
usageWindow: {
|
usageWindow: {
|
||||||
statsTitle: '5-Hour Window Usage Statistics'
|
statsTitle: '5-Hour Window Usage Statistics',
|
||||||
|
gemini3Pro: 'G3P',
|
||||||
|
gemini3Flash: 'G3F',
|
||||||
|
gemini3Image: 'G3I',
|
||||||
|
claude45: 'C4.5'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -966,7 +966,11 @@ export default {
|
|||||||
cooldown: '冷却中'
|
cooldown: '冷却中'
|
||||||
},
|
},
|
||||||
usageWindow: {
|
usageWindow: {
|
||||||
statsTitle: '5小时窗口用量统计'
|
statsTitle: '5小时窗口用量统计',
|
||||||
|
gemini3Pro: 'G3P',
|
||||||
|
gemini3Flash: 'G3F',
|
||||||
|
gemini3Image: 'G3I',
|
||||||
|
claude45: 'C4.5'
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
nameLabel: '账号名称',
|
nameLabel: '账号名称',
|
||||||
|
|||||||
Reference in New Issue
Block a user