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

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

- 正式快照优先 usage,quota 仅兜底;计划字段只收非空字符串
- buildGrokUsageRefreshKey 接入自动刷新
- 注明网关 Extra 写入依赖每请求独立 Account 副本
This commit is contained in:
IanShaw027
2026-08-13 08:38:10 +08:00
parent b830bc14d6
commit 0ae151a238
5 changed files with 308 additions and 17 deletions
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { buildOpenAIUsageRefreshKey } from '../accountUsageRefresh'
import { buildGrokUsageRefreshKey, buildOpenAIUsageRefreshKey } from '../accountUsageRefresh'
describe('buildOpenAIUsageRefreshKey', () => {
it('会在 codex 快照变化时生成不同 key', () => {
@@ -61,3 +61,101 @@ describe('buildOpenAIUsageRefreshKey', () => {
} as any)).toBe('')
})
})
describe('buildGrokUsageRefreshKey', () => {
it('changes when a canonical Grok billing or usage snapshot changes', () => {
const base = {
platform: 'grok',
extra: {
grok_billing_snapshot: { plan: 'Free', usage_percent: 0 },
grok_usage_snapshot: { subscription_tier: 'Free', status_code: 200 }
}
} as any
expect(buildGrokUsageRefreshKey(base)).not.toBe(buildGrokUsageRefreshKey({
...base,
extra: {
...base.extra,
grok_billing_snapshot: { plan: 'SuperGrok', usage_percent: 0 }
}
}))
expect(buildGrokUsageRefreshKey(base)).not.toBe(buildGrokUsageRefreshKey({
...base,
extra: {
...base.extra,
grok_usage_snapshot: { subscription_tier: 'SuperGrok', status_code: 200 }
}
}))
})
it('ignores object key order and a legacy alias shadowed by canonical usage', () => {
const first = {
platform: 'grok',
extra: {
grok_billing_snapshot: {
plan: 'SuperGrok',
limits: { monthly: 100, weekly: 25 }
},
grok_usage_snapshot: { status_code: 200, subscription_tier: 'SuperGrok' },
grok_quota_snapshot: { subscription_tier: 'Free' }
}
} as any
const reordered = {
platform: 'grok',
extra: {
grok_quota_snapshot: { subscription_tier: 'SuperGrok Heavy' },
grok_usage_snapshot: { subscription_tier: 'SuperGrok', status_code: 200 },
grok_billing_snapshot: {
limits: { weekly: 25, monthly: 100 },
plan: 'SuperGrok'
}
}
} as any
expect(buildGrokUsageRefreshKey(first)).toBe(buildGrokUsageRefreshKey(reordered))
})
it('uses the legacy quota alias only when the canonical snapshot is absent', () => {
const base = {
platform: 'grok',
extra: { grok_quota_snapshot: { subscription_tier: 'Free' } }
} as any
const next = {
platform: 'grok',
extra: { grok_quota_snapshot: { subscription_tier: 'SuperGrok' } }
} as any
expect(buildGrokUsageRefreshKey(base)).not.toBe(buildGrokUsageRefreshKey(next))
})
it('tracks the legacy tier when the canonical snapshot has no usable tier', () => {
for (const canonicalSnapshot of [
{ status_code: 200 },
{ status_code: 200, subscription_tier: ' ' },
]) {
const base = {
platform: 'grok',
extra: {
grok_usage_snapshot: canonicalSnapshot,
grok_quota_snapshot: { subscription_tier: 'Free' },
},
} as any
const next = {
...base,
extra: {
...base.extra,
grok_quota_snapshot: { subscription_tier: 'SuperGrok' },
},
}
expect(buildGrokUsageRefreshKey(base)).not.toBe(buildGrokUsageRefreshKey(next))
}
})
it('returns an empty key for non-Grok accounts', () => {
expect(buildGrokUsageRefreshKey({
platform: 'openai',
extra: { grok_usage_snapshot: { subscription_tier: 'SuperGrok' } }
} as any)).toBe('')
})
})
+42
View File
@@ -5,6 +5,30 @@ const normalizeUsageRefreshValue = (value: unknown): string => {
return String(value)
}
const normalizeSnapshotRefreshValue = (value: unknown): unknown => {
if (Array.isArray(value)) {
return value.map(normalizeSnapshotRefreshValue)
}
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, normalizeSnapshotRefreshValue(entry)])
)
}
return value
}
const serializeSnapshotRefreshValue = (value: unknown): string => {
if (value == null) return ''
return JSON.stringify(normalizeSnapshotRefreshValue(value)) ?? ''
}
const isNonBlankString = (value: unknown): value is string => (
typeof value === 'string' && value.trim().length > 0
)
export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platform' | 'type' | 'updated_at' | 'last_used_at' | 'rate_limit_reset_at' | 'extra'>): string => {
if (account.platform !== 'openai' || account.type !== 'oauth') {
return ''
@@ -27,3 +51,21 @@ export const buildOpenAIUsageRefreshKey = (account: Pick<Account, 'id' | 'platfo
extra.codex_7d_window_minutes
].map(normalizeUsageRefreshValue).join('|')
}
export const buildGrokUsageRefreshKey = (account: Pick<Account, 'platform' | 'extra'>): string => {
if (account.platform !== 'grok') {
return ''
}
const extra = account.extra ?? {}
const usageSnapshot = extra.grok_usage_snapshot
const canonicalTier = (usageSnapshot as Record<string, unknown> | null | undefined)?.subscription_tier
const legacyQuotaFallback = isNonBlankString(canonicalTier)
? undefined
: extra.grok_quota_snapshot
return [
serializeSnapshotRefreshValue(extra.grok_billing_snapshot),
serializeSnapshotRefreshValue(usageSnapshot),
serializeSnapshotRefreshValue(legacyQuotaFallback)
].join('|')
}