mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-22 23:54:45 +08:00
feat: enhance Antigravity account overages handling and improve UI credit display
This commit is contained in:
@@ -1538,6 +1538,13 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
account.Extra = input.Extra
|
account.Extra = input.Extra
|
||||||
|
if account.Platform == PlatformAntigravity && wasOveragesEnabled && !account.IsOveragesEnabled() {
|
||||||
|
delete(account.Extra, antigravityCreditsOveragesKey)
|
||||||
|
}
|
||||||
|
if account.Platform == PlatformAntigravity && !wasOveragesEnabled && account.IsOveragesEnabled() {
|
||||||
|
delete(account.Extra, modelRateLimitsKey)
|
||||||
|
delete(account.Extra, antigravityCreditsOveragesKey)
|
||||||
|
}
|
||||||
// 校验并预计算固定时间重置的下次重置时间
|
// 校验并预计算固定时间重置的下次重置时间
|
||||||
if err := ValidateQuotaResetConfig(account.Extra); err != nil {
|
if err := ValidateQuotaResetConfig(account.Extra); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1623,9 +1630,6 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
if account.Platform == PlatformAntigravity {
|
if account.Platform == PlatformAntigravity {
|
||||||
if !account.IsOveragesEnabled() && wasOveragesEnabled {
|
if !account.IsOveragesEnabled() && wasOveragesEnabled {
|
||||||
clearCreditsExhausted(account.ID)
|
clearCreditsExhausted(account.ID)
|
||||||
if err := clearAntigravityCreditsOveragesState(ctx, s.accountRepo, account.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if account.IsOveragesEnabled() && !wasOveragesEnabled {
|
if account.IsOveragesEnabled() && !wasOveragesEnabled {
|
||||||
clearCreditsExhausted(account.ID)
|
clearCreditsExhausted(account.ID)
|
||||||
|
|||||||
111
backend/internal/service/admin_service_overages_test.go
Normal file
111
backend/internal/service/admin_service_overages_test.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateAccountOveragesRepoStub struct {
|
||||||
|
mockAccountRepoForGemini
|
||||||
|
account *Account
|
||||||
|
updateCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateAccountOveragesRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) {
|
||||||
|
return r.account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateAccountOveragesRepoStub) Update(ctx context.Context, account *Account) error {
|
||||||
|
r.updateCalls++
|
||||||
|
r.account = account
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateAccount_DisableOveragesClearsRuntimeStateBeforePersist(t *testing.T) {
|
||||||
|
accountID := int64(101)
|
||||||
|
repo := &updateAccountOveragesRepoStub{
|
||||||
|
account: &Account{
|
||||||
|
ID: accountID,
|
||||||
|
Platform: PlatformAntigravity,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Status: StatusActive,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"allow_overages": true,
|
||||||
|
"mixed_scheduling": true,
|
||||||
|
antigravityCreditsOveragesKey: map[string]any{
|
||||||
|
"claude-sonnet-4-5": map[string]any{
|
||||||
|
"activated_at": "2026-03-15T00:00:00Z",
|
||||||
|
"active_until": "2099-03-15T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setCreditsExhausted(accountID, time.Now().Add(time.Minute))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
clearCreditsExhausted(accountID)
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := &adminServiceImpl{accountRepo: repo}
|
||||||
|
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
||||||
|
Extra: map[string]any{
|
||||||
|
"mixed_scheduling": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updated)
|
||||||
|
require.Equal(t, 1, repo.updateCalls)
|
||||||
|
require.False(t, updated.IsOveragesEnabled())
|
||||||
|
require.False(t, isCreditsExhausted(accountID))
|
||||||
|
|
||||||
|
_, exists := repo.account.Extra[antigravityCreditsOveragesKey]
|
||||||
|
require.False(t, exists, "关闭 overages 时应在持久化前移除运行态")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateAccount_EnableOveragesClearsModelRateLimitsBeforePersist(t *testing.T) {
|
||||||
|
accountID := int64(102)
|
||||||
|
repo := &updateAccountOveragesRepoStub{
|
||||||
|
account: &Account{
|
||||||
|
ID: accountID,
|
||||||
|
Platform: PlatformAntigravity,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Status: StatusActive,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"mixed_scheduling": true,
|
||||||
|
modelRateLimitsKey: map[string]any{
|
||||||
|
"claude-sonnet-4-5": map[string]any{
|
||||||
|
"rate_limited_at": "2026-03-15T00:00:00Z",
|
||||||
|
"rate_limit_reset_at": "2099-03-15T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setCreditsExhausted(accountID, time.Now().Add(time.Minute))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
clearCreditsExhausted(accountID)
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := &adminServiceImpl{accountRepo: repo}
|
||||||
|
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
||||||
|
Extra: map[string]any{
|
||||||
|
"mixed_scheduling": true,
|
||||||
|
"allow_overages": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updated)
|
||||||
|
require.Equal(t, 1, repo.updateCalls)
|
||||||
|
require.True(t, updated.IsOveragesEnabled())
|
||||||
|
require.False(t, isCreditsExhausted(accountID))
|
||||||
|
|
||||||
|
_, exists := repo.account.Extra[modelRateLimitsKey]
|
||||||
|
require.False(t, exists, "开启 overages 时应在持久化前清掉旧模型限流")
|
||||||
|
}
|
||||||
@@ -290,30 +290,34 @@
|
|||||||
color="amber"
|
color="amber"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="antigravityAICreditsDisplay.length > 0" class="mt-1 space-y-0.5 text-[10px] text-gray-500 dark:text-gray-400">
|
<div v-if="antigravityAICreditsDisplay.length > 0" class="mt-1 flex flex-wrap gap-1">
|
||||||
<div
|
<div
|
||||||
v-for="credit in antigravityAICreditsDisplay"
|
v-for="credit in antigravityAICreditsDisplay"
|
||||||
:key="credit.creditType"
|
:key="credit.creditType"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-800/50 dark:bg-amber-950/40 dark:text-amber-300"
|
||||||
>
|
>
|
||||||
{{ t('admin.accounts.aiCreditsBalance') }}:
|
<span>⚡</span>
|
||||||
{{ credit.creditType }}
|
<span>{{ t('admin.accounts.aiCreditsBalance') }}</span>
|
||||||
{{ credit.amount }}
|
<span>{{ credit.label }}</span>
|
||||||
<span v-if="credit.minimumBalance !== null">
|
<span class="font-mono">{{ credit.amount }}</span>
|
||||||
(min {{ credit.minimumBalance }})
|
<span v-if="credit.minimumBalance !== null" class="text-[9px] opacity-75">
|
||||||
|
min {{ credit.minimumBalance }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="antigravityAICreditsDisplay.length > 0" class="space-y-0.5 text-[10px] text-gray-500 dark:text-gray-400">
|
<div v-else-if="antigravityAICreditsDisplay.length > 0" class="flex flex-wrap gap-1">
|
||||||
<div
|
<div
|
||||||
v-for="credit in antigravityAICreditsDisplay"
|
v-for="credit in antigravityAICreditsDisplay"
|
||||||
:key="credit.creditType"
|
:key="credit.creditType"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-800/50 dark:bg-amber-950/40 dark:text-amber-300"
|
||||||
>
|
>
|
||||||
{{ t('admin.accounts.aiCreditsBalance') }}:
|
<span>⚡</span>
|
||||||
{{ credit.creditType }}
|
<span>{{ t('admin.accounts.aiCreditsBalance') }}</span>
|
||||||
{{ credit.amount }}
|
<span>{{ credit.label }}</span>
|
||||||
<span v-if="credit.minimumBalance !== null">
|
<span class="font-mono">{{ credit.amount }}</span>
|
||||||
(min {{ credit.minimumBalance }})
|
<span v-if="credit.minimumBalance !== null" class="text-[9px] opacity-75">
|
||||||
|
min {{ credit.minimumBalance }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -615,6 +619,7 @@ const antigravityAICreditsDisplay = computed(() => {
|
|||||||
.filter((credit) => (credit.amount ?? 0) > 0)
|
.filter((credit) => (credit.amount ?? 0) > 0)
|
||||||
.map((credit) => ({
|
.map((credit) => ({
|
||||||
creditType: credit.credit_type || 'UNKNOWN',
|
creditType: credit.credit_type || 'UNKNOWN',
|
||||||
|
label: formatAICreditTypeLabel(credit.credit_type || 'UNKNOWN'),
|
||||||
amount: Number(credit.amount ?? 0).toFixed(0),
|
amount: Number(credit.amount ?? 0).toFixed(0),
|
||||||
minimumBalance: typeof credit.minimum_balance === 'number'
|
minimumBalance: typeof credit.minimum_balance === 'number'
|
||||||
? Number(credit.minimum_balance).toFixed(0)
|
? Number(credit.minimum_balance).toFixed(0)
|
||||||
@@ -622,6 +627,11 @@ const antigravityAICreditsDisplay = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function formatAICreditTypeLabel(creditType: string): string {
|
||||||
|
if (creditType === 'GOOGLE_ONE_AI') return 'Google One AI'
|
||||||
|
return creditType
|
||||||
|
}
|
||||||
|
|
||||||
// Antigravity 账户类型(从 load_code_assist 响应中提取)
|
// Antigravity 账户类型(从 load_code_assist 响应中提取)
|
||||||
const antigravityTier = computed(() => {
|
const antigravityTier = computed(() => {
|
||||||
const extra = props.account.extra as Record<string, unknown> | undefined
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
||||||
|
|||||||
@@ -133,9 +133,9 @@ describe('AccountUsageCell', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('admin.accounts.aiCreditsBalance')
|
expect(wrapper.text()).toContain('admin.accounts.aiCreditsBalance')
|
||||||
expect(wrapper.text()).toContain('GOOGLE_ONE_AI')
|
expect(wrapper.text()).toContain('Google One AI')
|
||||||
expect(wrapper.text()).toContain('25')
|
expect(wrapper.text()).toContain('25')
|
||||||
expect(wrapper.text()).toContain('(min 5)')
|
expect(wrapper.text()).toContain('min 5')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user