mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-24 16:44:45 +08:00
fix: 修复 OpenAI WS 用量刷新遗漏场景
This commit is contained in:
@@ -406,13 +406,22 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
|
|||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
if isOAuth && s.accountRepo != nil {
|
if isOAuth && s.accountRepo != nil {
|
||||||
if updates, err := extractOpenAICodexProbeUpdates(resp); err == nil && len(updates) > 0 {
|
if updates, err := extractOpenAICodexProbeUpdates(resp); err == nil && len(updates) > 0 {
|
||||||
_ = s.accountRepo.UpdateExtra(ctx, account.ID, updates)
|
_ = s.accountRepo.UpdateExtra(ctx, account.ID, updates)
|
||||||
mergeAccountExtra(account, updates)
|
mergeAccountExtra(account, updates)
|
||||||
}
|
}
|
||||||
|
if snapshot := ParseCodexRateLimitHeaders(resp.Header); snapshot != nil {
|
||||||
|
if resetAt := codexRateLimitResetAtFromSnapshot(snapshot, time.Now()); resetAt != nil {
|
||||||
|
_ = s.accountRepo.SetRateLimited(ctx, account.ID, *resetAt)
|
||||||
|
account.RateLimitResetAt = resetAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if isOAuth && s.accountRepo != nil {
|
||||||
if resetAt := (&RateLimitService{}).calculateOpenAI429ResetTime(resp.Header); resetAt != nil {
|
if resetAt := (&RateLimitService{}).calculateOpenAI429ResetTime(resp.Header); resetAt != nil {
|
||||||
_ = s.accountRepo.SetRateLimited(ctx, account.ID, *resetAt)
|
_ = s.accountRepo.SetRateLimited(ctx, account.ID, *resetAt)
|
||||||
account.RateLimitResetAt = resetAt
|
account.RateLimitResetAt = resetAt
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,6 +32,40 @@ func (r *openAIAccountTestRepo) SetRateLimited(_ context.Context, id int64, rese
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
ctx, recorder := newSoraTestContext()
|
||||||
|
|
||||||
|
resp := newJSONResponse(http.StatusOK, "")
|
||||||
|
resp.Body = io.NopCloser(strings.NewReader(`data: {"type":"response.completed"}
|
||||||
|
|
||||||
|
`))
|
||||||
|
resp.Header.Set("x-codex-primary-used-percent", "88")
|
||||||
|
resp.Header.Set("x-codex-primary-reset-after-seconds", "604800")
|
||||||
|
resp.Header.Set("x-codex-primary-window-minutes", "10080")
|
||||||
|
resp.Header.Set("x-codex-secondary-used-percent", "42")
|
||||||
|
resp.Header.Set("x-codex-secondary-reset-after-seconds", "18000")
|
||||||
|
resp.Header.Set("x-codex-secondary-window-minutes", "300")
|
||||||
|
|
||||||
|
repo := &openAIAccountTestRepo{}
|
||||||
|
upstream := &queuedHTTPUpstream{responses: []*http.Response{resp}}
|
||||||
|
svc := &AccountTestService{accountRepo: repo, httpUpstream: upstream}
|
||||||
|
account := &Account{
|
||||||
|
ID: 89,
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Concurrency: 1,
|
||||||
|
Credentials: map[string]any{"access_token": "test-token"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := svc.testOpenAIAccountConnection(ctx, account, "gpt-5.4")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, repo.updatedExtra)
|
||||||
|
require.Equal(t, 42.0, repo.updatedExtra["codex_5h_used_percent"])
|
||||||
|
require.Equal(t, 88.0, repo.updatedExtra["codex_7d_used_percent"])
|
||||||
|
require.Contains(t, recorder.Body.String(), "test_complete")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimit(t *testing.T) {
|
func TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimit(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
ctx, _ := newSoraTestContext()
|
ctx, _ := newSoraTestContext()
|
||||||
|
|||||||
@@ -412,14 +412,24 @@ const isActiveOpenAIRateLimited = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const preferFetchedOpenAIUsage = computed(() => {
|
const preferFetchedOpenAIUsage = computed(() => {
|
||||||
return isActiveOpenAIRateLimited.value && hasOpenAIUsageFallback.value
|
return (isActiveOpenAIRateLimited.value || isOpenAICodexSnapshotStale.value) && hasOpenAIUsageFallback.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account))
|
const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account))
|
||||||
|
|
||||||
|
const isOpenAICodexSnapshotStale = computed(() => {
|
||||||
|
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return false
|
||||||
|
const extra = props.account.extra as Record<string, unknown> | undefined
|
||||||
|
const updatedAtRaw = extra?.codex_usage_updated_at
|
||||||
|
if (!updatedAtRaw) return true
|
||||||
|
const updatedAt = Date.parse(String(updatedAtRaw))
|
||||||
|
if (Number.isNaN(updatedAt)) return true
|
||||||
|
return Date.now() - updatedAt >= 10 * 60 * 1000
|
||||||
|
})
|
||||||
|
|
||||||
const shouldAutoLoadUsageOnMount = computed(() => {
|
const shouldAutoLoadUsageOnMount = computed(() => {
|
||||||
if (props.account.platform === 'openai' && props.account.type === 'oauth') {
|
if (props.account.platform === 'openai' && props.account.type === 'oauth') {
|
||||||
return isActiveOpenAIRateLimited.value || !hasCodexUsage.value
|
return isActiveOpenAIRateLimited.value || !hasCodexUsage.value || isOpenAICodexSnapshotStale.value
|
||||||
}
|
}
|
||||||
return shouldFetchUsage.value
|
return shouldFetchUsage.value
|
||||||
})
|
})
|
||||||
@@ -807,7 +817,7 @@ onMounted(() => {
|
|||||||
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
||||||
if (!prevKey || nextKey === prevKey) return
|
if (!prevKey || nextKey === prevKey) return
|
||||||
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return
|
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return
|
||||||
if (!isActiveOpenAIRateLimited.value && hasCodexUsage.value) return
|
if (!isActiveOpenAIRateLimited.value && hasCodexUsage.value && !isOpenAICodexSnapshotStale.value) return
|
||||||
|
|
||||||
loadUsage().catch((e) => {
|
loadUsage().catch((e) => {
|
||||||
console.error('Failed to refresh OpenAI usage:', e)
|
console.error('Failed to refresh OpenAI usage:', e)
|
||||||
|
|||||||
@@ -69,6 +69,67 @@ describe('AccountUsageCell', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('OpenAI OAuth 快照已过期时首屏会重新请求 usage', async () => {
|
||||||
|
getUsage.mockResolvedValue({
|
||||||
|
five_hour: {
|
||||||
|
utilization: 15,
|
||||||
|
resets_at: '2026-03-08T12:00:00Z',
|
||||||
|
remaining_seconds: 3600,
|
||||||
|
window_stats: {
|
||||||
|
requests: 3,
|
||||||
|
tokens: 300,
|
||||||
|
cost: 0.03,
|
||||||
|
standard_cost: 0.03,
|
||||||
|
user_cost: 0.03
|
||||||
|
}
|
||||||
|
},
|
||||||
|
seven_day: {
|
||||||
|
utilization: 77,
|
||||||
|
resets_at: '2026-03-13T12:00:00Z',
|
||||||
|
remaining_seconds: 3600,
|
||||||
|
window_stats: {
|
||||||
|
requests: 3,
|
||||||
|
tokens: 300,
|
||||||
|
cost: 0.03,
|
||||||
|
standard_cost: 0.03,
|
||||||
|
user_cost: 0.03
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mount(AccountUsageCell, {
|
||||||
|
props: {
|
||||||
|
account: {
|
||||||
|
id: 2000,
|
||||||
|
platform: 'openai',
|
||||||
|
type: 'oauth',
|
||||||
|
extra: {
|
||||||
|
codex_usage_updated_at: '2026-03-07T00:00:00Z',
|
||||||
|
codex_5h_used_percent: 12,
|
||||||
|
codex_5h_reset_at: '2026-03-08T12:00:00Z',
|
||||||
|
codex_7d_used_percent: 34,
|
||||||
|
codex_7d_reset_at: '2026-03-13T12:00:00Z'
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
UsageProgressBar: {
|
||||||
|
props: ['label', 'utilization', 'resetsAt', 'windowStats', 'color'],
|
||||||
|
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ windowStats?.tokens }}</div>'
|
||||||
|
},
|
||||||
|
AccountQuotaInfo: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(getUsage).toHaveBeenCalledWith(2000)
|
||||||
|
expect(wrapper.text()).toContain('5h|15|300')
|
||||||
|
expect(wrapper.text()).toContain('7d|77|300')
|
||||||
|
})
|
||||||
|
|
||||||
it('OpenAI OAuth 有现成快照且未限额时不会首屏请求 usage', async () => {
|
it('OpenAI OAuth 有现成快照且未限额时不会首屏请求 usage', async () => {
|
||||||
const wrapper = mount(AccountUsageCell, {
|
const wrapper = mount(AccountUsageCell, {
|
||||||
props: {
|
props: {
|
||||||
@@ -77,6 +138,7 @@ describe('AccountUsageCell', () => {
|
|||||||
platform: 'openai',
|
platform: 'openai',
|
||||||
type: 'oauth',
|
type: 'oauth',
|
||||||
extra: {
|
extra: {
|
||||||
|
codex_usage_updated_at: '2099-03-07T10:00:00Z',
|
||||||
codex_5h_used_percent: 12,
|
codex_5h_used_percent: 12,
|
||||||
codex_5h_reset_at: '2099-03-07T12:00:00Z',
|
codex_5h_reset_at: '2099-03-07T12:00:00Z',
|
||||||
codex_7d_used_percent: 34,
|
codex_7d_used_percent: 34,
|
||||||
|
|||||||
Reference in New Issue
Block a user