fix: 徽章改读 grok_usage_snapshot,增量刷新比较 Grok 快照

后端写入 grok_usage_snapshot,列表却读无 writer 的 grok_quota_snapshot;
增量刷新不比 Grok 快照,档位更新后行不替换。

- 正式快照优先 usage,quota 仅兜底;计划字段只收非空字符串
- buildGrokUsageRefreshKey 接入自动刷新
- 注明网关 Extra 写入依赖每请求独立 Account 副本
This commit is contained in:
IanShaw027 committed 2026-08-13 08:38:10 +08:00
1 parent b830bc14d6
commit 0ae151a238
5 files changed
+308 -17

No files matched your search

+37 -14
View File
@@ -525,7 +525,7 @@ import Icon from '@/components/icons/Icon.vue'
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfilesModal.vue'
import { fetchAllAccountIds } from '@/utils/accountSelection'
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { buildGrokUsageRefreshKey, buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { formatDateTime, formatRelativeTime } from '@/utils/format'
import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry'
import { extractApiErrorMessage } from '@/utils/apiError'
@@ -1300,7 +1300,8 @@ const shouldReplaceAutoRefreshRow = (current: Account, next: Account) => {
current.rate_limit_reset_at !== next.rate_limit_reset_at ||
current.overload_until !== next.overload_until ||
current.temp_unschedulable_until !== next.temp_unschedulable_until ||
buildOpenAIUsageRefreshKey(current) !== buildOpenAIUsageRefreshKey(next)
buildOpenAIUsageRefreshKey(current) !== buildOpenAIUsageRefreshKey(next) ||
buildGrokUsageRefreshKey(current) !== buildGrokUsageRefreshKey(next)
)
}
@@ -1484,13 +1485,32 @@ const { pause: pauseAutoRefresh, resume: resumeAutoRefresh } = useIntervalFn(
const GROK_QUOTA_SIGNAL_MAX_AGE_MS = 24 * 60 * 60 * 1000
const GROK_QUOTA_SIGNAL_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000
function firstNonBlankString(...values: unknown[]): string | undefined {
return values.find((value): value is string => (
typeof value === 'string' && value.trim().length > 0
))
}
function normalizeGrokPlanKey(value: unknown): string {
return String(value ?? '')
if (typeof value !== 'string') return ''
return value
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, '')
}
function grokPersistedQuotaSnapshot(extra: Record<string, any>): Record<string, any> | undefined {
const usage = extra.grok_usage_snapshot
if (usage && typeof usage === 'object' && !Array.isArray(usage)) {
return usage as Record<string, any>
}
const legacy = extra.grok_quota_snapshot
if (legacy && typeof legacy === 'object' && !Array.isArray(legacy)) {
return legacy as Record<string, any>
}
return undefined
}
function isGrokQuotaTimestampFresh(raw: unknown): boolean {
const value = String(raw || '').trim()
if (!value) return false
@@ -1536,8 +1556,10 @@ function getAccountPlanType(row: any): string | undefined {
if (row.platform === 'grok') {
const extra = (row.extra || {}) as Record<string, any>
const billing = extra.grok_billing_snapshot as Record<string, any> | undefined
const quota = (extra.grok_quota_snapshot || extra.grok_usage_snapshot) as Record<string, any> | undefined
const cred = row.credentials?.subscription_tier
const usage = extra.grok_usage_snapshot as Record<string, any> | undefined
const legacyQuota = extra.grok_quota_snapshot as Record<string, any> | undefined
const quota = grokPersistedQuotaSnapshot(extra)
const cred = firstNonBlankString(row.credentials?.subscription_tier)
const credKey = normalizeGrokPlanKey(cred)
if (credKey && credKey !== 'supergrokpro') {
return cred
@@ -1551,18 +1573,18 @@ function getAccountPlanType(row: any): string | undefined {
return 'SuperGrok Heavy'
}
if (credKey === 'supergrokpro') {
return billing?.plan || 'SuperGrok'
return firstNonBlankString(billing?.plan) || 'SuperGrok'
}
return (
billing?.plan ||
quota?.subscription_tier ||
extra.subscription_tier ||
row.credentials?.plan_type ||
row.parent_plan_type ||
undefined
return firstNonBlankString(
billing?.plan,
usage?.subscription_tier,
legacyQuota?.subscription_tier,
extra.subscription_tier,
row.credentials?.plan_type,
row.parent_plan_type
)
}
return row.credentials?.plan_type || row.parent_plan_type || undefined
return firstNonBlankString(row.credentials?.plan_type, row.parent_plan_type)
}
function getOpenAIAuthMode(row: any): string | undefined {
@@ -2418,6 +2440,7 @@ const handleClickOutside = (event: MouseEvent) => {
onMounted(async () => {
if (typeof window !== 'undefined') {
loadSavedAutoRefresh()
desktopViewportMediaQuery = window.matchMedia(desktopViewportQuery)
isDesktopViewport.value = desktopViewportMediaQuery.matches
desktopViewportListener = (event: MediaQueryListEvent) => {
@@ -270,6 +270,8 @@ describe('admin AccountsView — 账号行展示', () => {
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
@@ -404,12 +406,18 @@ describe('admin AccountsView — 账号行展示', () => {
credentials: { subscription_tier: 'SuperGrokPro' },
extra: {
grok_billing_snapshot: { plan: 'SuperGrok' },
grok_quota_snapshot: {
grok_usage_snapshot: {
model: 'grok-4.5',
last_headers_seen_at: new Date().toISOString(),
requests: { limit: 8300 },
tokens: { limit: 53_000_000 },
},
grok_quota_snapshot: {
model: 'grok-4.6',
last_headers_seen_at: new Date().toISOString(),
requests: { limit: 8300 },
tokens: { limit: 53_000_000 },
},
},
},
{
@@ -420,7 +428,7 @@ describe('admin AccountsView — 账号行展示', () => {
credentials: { subscription_tier: 'SuperGrokPro' },
extra: {
grok_billing_snapshot: { plan: 'SuperGrok' },
grok_quota_snapshot: {
grok_usage_snapshot: {
model: 'grok-4.6',
last_headers_seen_at: new Date().toISOString(),
requests: { limit: 8300 },
@@ -428,6 +436,25 @@ describe('admin AccountsView — 账号行展示', () => {
},
},
},
{
id: 208,
name: 'usage-over-legacy-quota',
platform: 'grok',
type: 'oauth',
credentials: {},
extra: {
grok_usage_snapshot: { subscription_tier: 'SuperGrok' },
grok_quota_snapshot: { subscription_tier: 'Free' },
},
},
{
id: 209,
name: 'legacy-quota-alias',
platform: 'grok',
type: 'oauth',
credentials: {},
extra: { grok_quota_snapshot: { subscription_tier: 'SuperGrok' } },
},
]
listAccounts.mockResolvedValue({
@@ -450,8 +477,106 @@ describe('admin AccountsView — 账号行展示', () => {
'SuperGrok',
'SuperGrok Heavy',
'SuperGrok',
'SuperGrok',
'SuperGrok',
])
wrapper.unmount()
})
it('skips malformed Grok plan fields and safely uses the next valid fallback', async () => {
const grokAccounts = [
{
id: 210,
name: 'legacy-fallback',
platform: 'grok',
type: 'oauth',
credentials: {},
extra: {
grok_usage_snapshot: { subscription_tier: { name: 'SuperGrok Heavy' } },
grok_quota_snapshot: { subscription_tier: 'SuperGrok' },
},
},
{
id: 211,
name: 'credential-plan-fallback',
platform: 'grok',
type: 'oauth',
credentials: { subscription_tier: 0, plan_type: 'SuperGrok Heavy' },
extra: {
grok_billing_snapshot: { plan: {} },
grok_usage_snapshot: { subscription_tier: 1 },
grok_quota_snapshot: { subscription_tier: [] },
subscription_tier: ' ',
},
},
{
id: 212,
name: 'no-valid-plan',
platform: 'grok',
type: 'oauth',
credentials: { subscription_tier: {}, plan_type: 2 },
parent_plan_type: [],
extra: {
grok_billing_snapshot: { plan: [] },
grok_usage_snapshot: { subscription_tier: 1 },
grok_quota_snapshot: { subscription_tier: {} },
subscription_tier: null,
},
},
]
listAccounts.mockResolvedValue({
items: grokAccounts,
total: grokAccounts.length,
page: 1,
page_size: 20,
pages: 1,
})
const wrapper = mountViewWithRow()
await flushPromises()
expect(wrapper.findAllComponents(PlatformTypeBadge).map((badge) => badge.props('planType'))).toEqual([
'SuperGrok',
'SuperGrok Heavy',
undefined,
])
wrapper.unmount()
})
it('replaces a Grok row when auto refresh returns a changed canonical usage snapshot', async () => {
vi.useFakeTimers()
vi.spyOn(document, 'hidden', 'get').mockReturnValue(false)
localStorage.setItem('account-auto-refresh', JSON.stringify({ enabled: true, interval_seconds: 5 }))
const initialAccount = {
id: 213,
name: 'refresh-tier',
platform: 'grok',
type: 'oauth',
extra: { grok_usage_snapshot: { subscription_tier: 'Free', status_code: 200 } },
}
const refreshedAccount = {
...initialAccount,
extra: { grok_usage_snapshot: { subscription_tier: 'SuperGrok', status_code: 200 } },
}
listAccounts.mockResolvedValue({ items: [initialAccount], total: 1, page: 1, page_size: 20, pages: 1 })
listWithEtag.mockResolvedValueOnce({
notModified: false,
etag: 'grok-snapshot-2',
data: { items: [refreshedAccount], total: 1, page: 1, page_size: 20, pages: 1 },
})
const wrapper = mountViewWithRow()
await flushPromises()
expect(wrapper.findComponent(PlatformTypeBadge).props('planType')).toBe('Free')
await vi.advanceTimersByTimeAsync(6000)
await flushPromises()
expect(listWithEtag).toHaveBeenCalledTimes(1)
expect(wrapper.findComponent(PlatformTypeBadge).props('planType')).toBe('SuperGrok')
wrapper.unmount()
})
})