Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7fde9ebb
Commit
7fde9ebb
authored
Mar 16, 2026
by
Ethan0x0000
Browse files
fix: zero expired codex windows in backend, use /usage API as single frontend data source
parent
aa5846b2
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/account_usage_service.go
View file @
7fde9ebb
...
@@ -1042,6 +1042,11 @@ func buildCodexUsageProgressFromExtra(extra map[string]any, window string, now t
...
@@ -1042,6 +1042,11 @@ func buildCodexUsageProgressFromExtra(extra map[string]any, window string, now t
}
}
}
}
// 窗口已过期(resetAt 在 now 之前)→ 额度已重置,归零
if
progress
.
ResetsAt
!=
nil
&&
!
now
.
Before
(
*
progress
.
ResetsAt
)
{
progress
.
Utilization
=
0
}
return
progress
return
progress
}
}
...
...
backend/internal/service/account_usage_service_test.go
View file @
7fde9ebb
...
@@ -148,3 +148,54 @@ func TestAccountUsageService_PersistOpenAICodexProbeSnapshotSetsRateLimit(t *tes
...
@@ -148,3 +148,54 @@ func TestAccountUsageService_PersistOpenAICodexProbeSnapshotSetsRateLimit(t *tes
t
.
Fatal
(
"waiting for codex probe rate limit persistence timed out"
)
t
.
Fatal
(
"waiting for codex probe rate limit persistence timed out"
)
}
}
}
}
func
TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow
(
t
*
testing
.
T
)
{
t
.
Parallel
()
now
:=
time
.
Date
(
2026
,
3
,
16
,
12
,
0
,
0
,
0
,
time
.
UTC
)
t
.
Run
(
"expired 5h window zeroes utilization"
,
func
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"codex_5h_used_percent"
:
42.0
,
"codex_5h_reset_at"
:
"2026-03-16T10:00:00Z"
,
// 2h ago
}
progress
:=
buildCodexUsageProgressFromExtra
(
extra
,
"5h"
,
now
)
if
progress
==
nil
{
t
.
Fatal
(
"expected non-nil progress"
)
}
if
progress
.
Utilization
!=
0
{
t
.
Fatalf
(
"expected Utilization=0 for expired window, got %v"
,
progress
.
Utilization
)
}
if
progress
.
RemainingSeconds
!=
0
{
t
.
Fatalf
(
"expected RemainingSeconds=0, got %v"
,
progress
.
RemainingSeconds
)
}
})
t
.
Run
(
"active 5h window keeps utilization"
,
func
(
t
*
testing
.
T
)
{
resetAt
:=
now
.
Add
(
2
*
time
.
Hour
)
.
Format
(
time
.
RFC3339
)
extra
:=
map
[
string
]
any
{
"codex_5h_used_percent"
:
42.0
,
"codex_5h_reset_at"
:
resetAt
,
}
progress
:=
buildCodexUsageProgressFromExtra
(
extra
,
"5h"
,
now
)
if
progress
==
nil
{
t
.
Fatal
(
"expected non-nil progress"
)
}
if
progress
.
Utilization
!=
42.0
{
t
.
Fatalf
(
"expected Utilization=42, got %v"
,
progress
.
Utilization
)
}
})
t
.
Run
(
"expired 7d window zeroes utilization"
,
func
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"codex_7d_used_percent"
:
88.0
,
"codex_7d_reset_at"
:
"2026-03-15T00:00:00Z"
,
// yesterday
}
progress
:=
buildCodexUsageProgressFromExtra
(
extra
,
"7d"
,
now
)
if
progress
==
nil
{
t
.
Fatal
(
"expected non-nil progress"
)
}
if
progress
.
Utilization
!=
0
{
t
.
Fatalf
(
"expected Utilization=0 for expired 7d window, got %v"
,
progress
.
Utilization
)
}
})
}
frontend/src/components/account/AccountUsageCell.vue
View file @
7fde9ebb
...
@@ -73,7 +73,7 @@
...
@@ -73,7 +73,7 @@
<div
v-else
class=
"text-xs text-gray-400"
>
-
</div>
<div
v-else
class=
"text-xs text-gray-400"
>
-
</div>
</template>
</template>
<!-- OpenAI OAuth accounts:
prefer fresh usage query for active rate-limited rows
-->
<!-- OpenAI OAuth accounts:
single source from /usage API
-->
<
template
v-else-if=
"account.platform === 'openai' && account.type === 'oauth'"
>
<
template
v-else-if=
"account.platform === 'openai' && account.type === 'oauth'"
>
<div
v-if=
"hasOpenAIUsageFallback"
class=
"space-y-1"
>
<div
v-if=
"hasOpenAIUsageFallback"
class=
"space-y-1"
>
<UsageProgressBar
<UsageProgressBar
...
@@ -93,37 +93,6 @@
...
@@ -93,37 +93,6 @@
color=
"emerald"
color=
"emerald"
/>
/>
</div>
</div>
<div
v-else-if=
"isActiveOpenAIRateLimited && loading"
class=
"space-y-1.5"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
<div
class=
"h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"
></div>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
</div>
<div
class=
"flex items-center gap-1"
>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
<div
class=
"h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"
></div>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
</div>
</div>
<div
v-else-if=
"hasCodexUsage"
class=
"space-y-1"
>
<!-- 5h Window -->
<UsageProgressBar
v-if=
"codex5hUsedPercent !== null"
label=
"5h"
:utilization=
"codex5hUsedPercent"
:resets-at=
"codex5hResetAt"
color=
"indigo"
/>
<!-- 7d Window -->
<UsageProgressBar
v-if=
"codex7dUsedPercent !== null"
label=
"7d"
:utilization=
"codex7dUsedPercent"
:resets-at=
"codex7dResetAt"
color=
"emerald"
/>
</div>
<div
v-else-if=
"loading"
class=
"space-y-1.5"
>
<div
v-else-if=
"loading"
class=
"space-y-1.5"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
<div
class=
"h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"
></div>
...
@@ -441,7 +410,6 @@ import { useI18n } from 'vue-i18n'
...
@@ -441,7 +410,6 @@ import { useI18n } from 'vue-i18n'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
AccountUsageInfo
,
GeminiCredentials
,
WindowStats
}
from
'
@/types
'
import
type
{
Account
,
AccountUsageInfo
,
GeminiCredentials
,
WindowStats
}
from
'
@/types
'
import
{
buildOpenAIUsageRefreshKey
}
from
'
@/utils/accountUsageRefresh
'
import
{
buildOpenAIUsageRefreshKey
}
from
'
@/utils/accountUsageRefresh
'
import
{
resolveCodexUsageWindow
}
from
'
@/utils/codexUsage
'
import
{
formatCompactNumber
}
from
'
@/utils/format
'
import
{
formatCompactNumber
}
from
'
@/utils/format
'
import
UsageProgressBar
from
'
./UsageProgressBar.vue
'
import
UsageProgressBar
from
'
./UsageProgressBar.vue
'
import
AccountQuotaInfo
from
'
./AccountQuotaInfo.vue
'
import
AccountQuotaInfo
from
'
./AccountQuotaInfo.vue
'
...
@@ -500,37 +468,17 @@ const geminiUsageAvailable = computed(() => {
...
@@ -500,37 +468,17 @@ const geminiUsageAvailable = computed(() => {
)
)
})
})
const
codex5hWindow
=
computed
(()
=>
resolveCodexUsageWindow
(
props
.
account
.
extra
,
'
5h
'
))
const
codex7dWindow
=
computed
(()
=>
resolveCodexUsageWindow
(
props
.
account
.
extra
,
'
7d
'
))
// OpenAI Codex usage computed properties
const
hasCodexUsage
=
computed
(()
=>
{
return
codex5hWindow
.
value
.
usedPercent
!==
null
||
codex7dWindow
.
value
.
usedPercent
!==
null
})
const
hasOpenAIUsageFallback
=
computed
(()
=>
{
const
hasOpenAIUsageFallback
=
computed
(()
=>
{
if
(
props
.
account
.
platform
!==
'
openai
'
||
props
.
account
.
type
!==
'
oauth
'
)
return
false
if
(
props
.
account
.
platform
!==
'
openai
'
||
props
.
account
.
type
!==
'
oauth
'
)
return
false
return
!!
usageInfo
.
value
?.
five_hour
||
!!
usageInfo
.
value
?.
seven_day
return
!!
usageInfo
.
value
?.
five_hour
||
!!
usageInfo
.
value
?.
seven_day
})
})
const
isActiveOpenAIRateLimited
=
computed
(()
=>
{
if
(
props
.
account
.
platform
!==
'
openai
'
||
props
.
account
.
type
!==
'
oauth
'
)
return
false
if
(
!
props
.
account
.
rate_limit_reset_at
)
return
false
const
resetAt
=
Date
.
parse
(
props
.
account
.
rate_limit_reset_at
)
return
!
Number
.
isNaN
(
resetAt
)
&&
resetAt
>
Date
.
now
()
})
const
openAIUsageRefreshKey
=
computed
(()
=>
buildOpenAIUsageRefreshKey
(
props
.
account
))
const
openAIUsageRefreshKey
=
computed
(()
=>
buildOpenAIUsageRefreshKey
(
props
.
account
))
const
shouldAutoLoadUsageOnMount
=
computed
(()
=>
{
const
shouldAutoLoadUsageOnMount
=
computed
(()
=>
{
return
shouldFetchUsage
.
value
return
shouldFetchUsage
.
value
})
})
const
codex5hUsedPercent
=
computed
(()
=>
codex5hWindow
.
value
.
usedPercent
)
const
codex5hResetAt
=
computed
(()
=>
codex5hWindow
.
value
.
resetAt
)
const
codex7dUsedPercent
=
computed
(()
=>
codex7dWindow
.
value
.
usedPercent
)
const
codex7dResetAt
=
computed
(()
=>
codex7dWindow
.
value
.
resetAt
)
// Antigravity quota types (用于 API 返回的数据)
// Antigravity quota types (用于 API 返回的数据)
interface
AntigravityUsageResult
{
interface
AntigravityUsageResult
{
utilization
:
number
utilization
:
number
...
...
frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
View file @
7fde9ebb
...
@@ -198,7 +198,7 @@ describe('AccountUsageCell', () => {
...
@@ -198,7 +198,7 @@ describe('AccountUsageCell', () => {
expect
(
wrapper
.
text
()).
toContain
(
'
7d|77|300
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
7d|77|300
'
)
})
})
it
(
'
OpenAI OAuth 有
现成快照时首屏先显示快照再加载 usage 覆盖
'
,
async
()
=>
{
it
(
'
OpenAI OAuth 有
codex 快照时仍然使用 /usage API 数据渲染
'
,
async
()
=>
{
getUsage
.
mockResolvedValue
({
getUsage
.
mockResolvedValue
({
five_hour
:
{
five_hour
:
{
utilization
:
18
,
utilization
:
18
,
...
@@ -254,8 +254,8 @@ describe('AccountUsageCell', () => {
...
@@ -254,8 +254,8 @@ describe('AccountUsageCell', () => {
await
flushPromises
()
await
flushPromises
()
// 始终拉 usage,fetched data 优先显示(包含 window_stats)
expect
(
getUsage
).
toHaveBeenCalledWith
(
2001
)
expect
(
getUsage
).
toHaveBeenCalledWith
(
2001
)
// 单一数据源:始终使用 /usage API 返回值,忽略 codex 快照
expect
(
wrapper
.
text
()).
toContain
(
'
5h|18|900
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
5h|18|900
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
7d|36|900
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
7d|36|900
'
)
})
})
...
@@ -326,7 +326,7 @@ describe('AccountUsageCell', () => {
...
@@ -326,7 +326,7 @@ describe('AccountUsageCell', () => {
// 手动刷新再拉一次
// 手动刷新再拉一次
expect
(
getUsage
).
toHaveBeenCalledTimes
(
2
)
expect
(
getUsage
).
toHaveBeenCalledTimes
(
2
)
expect
(
getUsage
).
toHaveBeenCalledWith
(
2010
)
expect
(
getUsage
).
toHaveBeenCalledWith
(
2010
)
//
fetched data 优先显示,包含 window_stats
//
单一数据源:始终使用 /usage API 值
expect
(
wrapper
.
text
()).
toContain
(
'
5h|18|900
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
5h|18|900
'
)
})
})
...
@@ -458,7 +458,7 @@ describe('AccountUsageCell', () => {
...
@@ -458,7 +458,7 @@ describe('AccountUsageCell', () => {
expect
(
wrapper
.
text
()).
toContain
(
'
5h|0|200
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
5h|0|200
'
)
})
})
it
(
'
OpenAI OAuth 已限额时
首屏优先展示重新查询后的 usage,而不是旧 codex 快照
'
,
async
()
=>
{
it
(
'
OpenAI OAuth 已限额时
显示 /usage API 返回的限额数据
'
,
async
()
=>
{
getUsage
.
mockResolvedValue
({
getUsage
.
mockResolvedValue
({
five_hour
:
{
five_hour
:
{
utilization
:
100
,
utilization
:
100
,
...
@@ -515,7 +515,6 @@ describe('AccountUsageCell', () => {
...
@@ -515,7 +515,6 @@ describe('AccountUsageCell', () => {
expect
(
getUsage
).
toHaveBeenCalledWith
(
2004
)
expect
(
getUsage
).
toHaveBeenCalledWith
(
2004
)
expect
(
wrapper
.
text
()).
toContain
(
'
5h|100|106540000
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
5h|100|106540000
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
7d|100|106540000
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
7d|100|106540000
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
5h|0|
'
)
})
})
it
(
'
Key 账号会展示 today stats 徽章并带 A/U 提示
'
,
async
()
=>
{
it
(
'
Key 账号会展示 today stats 徽章并带 A/U 提示
'
,
async
()
=>
{
...
...
frontend/src/utils/__tests__/codexUsage.spec.ts
deleted
100644 → 0
View file @
aa5846b2
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
resolveCodexUsageWindow
}
from
'
@/utils/codexUsage
'
describe
(
'
resolveCodexUsageWindow
'
,
()
=>
{
it
(
'
快照为空时返回空窗口
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
null
,
'
5h
'
,
new
Date
(
'
2026-02-20T08:00:00Z
'
))
expect
(
result
).
toEqual
({
usedPercent
:
null
,
resetAt
:
null
})
})
it
(
'
优先使用后端提供的绝对重置时间
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T08:00:00Z
'
)
const
result
=
resolveCodexUsageWindow
(
{
codex_5h_used_percent
:
55
,
codex_5h_reset_at
:
'
2026-02-20T10:00:00Z
'
,
codex_5h_reset_after_seconds
:
1
},
'
5h
'
,
now
)
expect
(
result
.
usedPercent
).
toBe
(
55
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T10:00:00.000Z
'
)
})
it
(
'
窗口已过期时自动归零
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T08:00:00Z
'
)
const
result
=
resolveCodexUsageWindow
(
{
codex_7d_used_percent
:
100
,
codex_7d_reset_at
:
'
2026-02-20T07:00:00Z
'
},
'
7d
'
,
now
)
expect
(
result
.
usedPercent
).
toBe
(
0
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T07:00:00.000Z
'
)
})
it
(
'
无绝对时间时使用 updated_at + seconds 回退计算
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T07:00:00Z
'
)
const
result
=
resolveCodexUsageWindow
(
{
codex_5h_used_percent
:
20
,
codex_5h_reset_after_seconds
:
3600
,
codex_usage_updated_at
:
'
2026-02-20T06:30:00Z
'
},
'
5h
'
,
now
)
expect
(
result
.
usedPercent
).
toBe
(
20
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T07:30:00.000Z
'
)
})
it
(
'
支持 legacy primary/secondary 字段映射
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T07:05:00Z
'
)
const
result5h
=
resolveCodexUsageWindow
(
{
codex_primary_window_minutes
:
10080
,
codex_primary_used_percent
:
70
,
codex_primary_reset_after_seconds
:
86400
,
codex_secondary_window_minutes
:
300
,
codex_secondary_used_percent
:
15
,
codex_secondary_reset_after_seconds
:
1200
,
codex_usage_updated_at
:
'
2026-02-20T07:00:00Z
'
},
'
5h
'
,
now
)
const
result7d
=
resolveCodexUsageWindow
(
{
codex_primary_window_minutes
:
10080
,
codex_primary_used_percent
:
70
,
codex_primary_reset_after_seconds
:
86400
,
codex_secondary_window_minutes
:
300
,
codex_secondary_used_percent
:
15
,
codex_secondary_reset_after_seconds
:
1200
,
codex_usage_updated_at
:
'
2026-02-20T07:00:00Z
'
},
'
7d
'
,
now
)
expect
(
result5h
.
usedPercent
).
toBe
(
15
)
expect
(
result5h
.
resetAt
).
toBe
(
'
2026-02-20T07:20:00.000Z
'
)
expect
(
result7d
.
usedPercent
).
toBe
(
70
)
expect
(
result7d
.
resetAt
).
toBe
(
'
2026-02-21T07:00:00.000Z
'
)
})
it
(
'
legacy 5h 在 primary<=360 时优先 primary 并支持字符串数字
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
{
codex_primary_window_minutes
:
'
300
'
,
codex_primary_used_percent
:
'
21
'
,
codex_primary_reset_after_seconds
:
'
1800
'
,
codex_secondary_window_minutes
:
'
10080
'
,
codex_secondary_used_percent
:
'
99
'
,
codex_secondary_reset_after_seconds
:
'
99999
'
,
codex_usage_updated_at
:
'
2026-02-20T08:00:00Z
'
},
'
5h
'
,
new
Date
(
'
2026-02-20T08:10:00Z
'
)
)
expect
(
result
.
usedPercent
).
toBe
(
21
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T08:30:00.000Z
'
)
})
it
(
'
legacy 5h 在无窗口信息时回退 secondary
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
{
codex_secondary_used_percent
:
19
,
codex_secondary_reset_after_seconds
:
120
,
codex_usage_updated_at
:
'
2026-02-20T08:00:00Z
'
},
'
5h
'
,
new
Date
(
'
2026-02-20T08:00:01Z
'
)
)
expect
(
result
.
usedPercent
).
toBe
(
19
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T08:02:00.000Z
'
)
})
it
(
'
legacy 场景下 secondary 为 7d 时能正确识别
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T07:30:00Z
'
)
const
result
=
resolveCodexUsageWindow
(
{
codex_primary_window_minutes
:
300
,
codex_primary_used_percent
:
5
,
codex_primary_reset_after_seconds
:
600
,
codex_secondary_window_minutes
:
10080
,
codex_secondary_used_percent
:
66
,
codex_secondary_reset_after_seconds
:
7200
,
codex_usage_updated_at
:
'
2026-02-20T07:00:00Z
'
},
'
7d
'
,
now
)
expect
(
result
.
usedPercent
).
toBe
(
66
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T09:00:00.000Z
'
)
})
it
(
'
绝对时间非法时回退到 updated_at + seconds
'
,
()
=>
{
const
now
=
new
Date
(
'
2026-02-20T07:40:00Z
'
)
const
result
=
resolveCodexUsageWindow
(
{
codex_5h_used_percent
:
33
,
codex_5h_reset_at
:
'
not-a-date
'
,
codex_5h_reset_after_seconds
:
900
,
codex_usage_updated_at
:
'
2026-02-20T07:30:00Z
'
},
'
5h
'
,
now
)
expect
(
result
.
usedPercent
).
toBe
(
33
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T07:45:00.000Z
'
)
})
it
(
'
updated_at 非法且无绝对时间时 resetAt 返回 null
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
{
codex_5h_used_percent
:
10
,
codex_5h_reset_after_seconds
:
123
,
codex_usage_updated_at
:
'
invalid-time
'
},
'
5h
'
,
new
Date
(
'
2026-02-20T08:00:00Z
'
)
)
expect
(
result
.
usedPercent
).
toBe
(
10
)
expect
(
result
.
resetAt
).
toBeNull
()
})
it
(
'
reset_after_seconds 为负数时按 0 秒处理
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
{
codex_5h_used_percent
:
80
,
codex_5h_reset_after_seconds
:
-
30
,
codex_usage_updated_at
:
'
2026-02-20T08:00:00Z
'
},
'
5h
'
,
new
Date
(
'
2026-02-20T07:59:00Z
'
)
)
expect
(
result
.
usedPercent
).
toBe
(
80
)
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T08:00:00.000Z
'
)
})
it
(
'
百分比缺失时仍可计算 resetAt 供倒计时展示
'
,
()
=>
{
const
result
=
resolveCodexUsageWindow
(
{
codex_7d_reset_after_seconds
:
60
,
codex_usage_updated_at
:
'
2026-02-20T08:00:00Z
'
},
'
7d
'
,
new
Date
(
'
2026-02-20T08:00:01Z
'
)
)
expect
(
result
.
usedPercent
).
toBeNull
()
expect
(
result
.
resetAt
).
toBe
(
'
2026-02-20T08:01:00.000Z
'
)
})
})
frontend/src/utils/codexUsage.ts
deleted
100644 → 0
View file @
aa5846b2
import
type
{
CodexUsageSnapshot
}
from
'
@/types
'
export
interface
ResolvedCodexUsageWindow
{
usedPercent
:
number
|
null
resetAt
:
string
|
null
}
type
WindowKind
=
'
5h
'
|
'
7d
'
function
asNumber
(
value
:
unknown
):
number
|
null
{
if
(
typeof
value
===
'
number
'
&&
Number
.
isFinite
(
value
))
return
value
if
(
typeof
value
===
'
string
'
&&
value
.
trim
()
!==
''
)
{
const
n
=
Number
(
value
)
if
(
Number
.
isFinite
(
n
))
return
n
}
return
null
}
function
asString
(
value
:
unknown
):
string
|
null
{
if
(
typeof
value
!==
'
string
'
)
return
null
const
trimmed
=
value
.
trim
()
return
trimmed
===
''
?
null
:
trimmed
}
function
asISOTime
(
value
:
unknown
):
string
|
null
{
const
raw
=
asString
(
value
)
if
(
!
raw
)
return
null
const
date
=
new
Date
(
raw
)
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
null
return
date
.
toISOString
()
}
function
resolveLegacy5h
(
snapshot
:
Record
<
string
,
unknown
>
):
{
used
:
number
|
null
;
resetAfterSeconds
:
number
|
null
}
{
const
primaryWindow
=
asNumber
(
snapshot
.
codex_primary_window_minutes
)
const
secondaryWindow
=
asNumber
(
snapshot
.
codex_secondary_window_minutes
)
const
primaryUsed
=
asNumber
(
snapshot
.
codex_primary_used_percent
)
const
secondaryUsed
=
asNumber
(
snapshot
.
codex_secondary_used_percent
)
const
primaryReset
=
asNumber
(
snapshot
.
codex_primary_reset_after_seconds
)
const
secondaryReset
=
asNumber
(
snapshot
.
codex_secondary_reset_after_seconds
)
if
(
primaryWindow
!=
null
&&
primaryWindow
<=
360
)
{
return
{
used
:
primaryUsed
,
resetAfterSeconds
:
primaryReset
}
}
if
(
secondaryWindow
!=
null
&&
secondaryWindow
<=
360
)
{
return
{
used
:
secondaryUsed
,
resetAfterSeconds
:
secondaryReset
}
}
return
{
used
:
secondaryUsed
,
resetAfterSeconds
:
secondaryReset
}
}
function
resolveLegacy7d
(
snapshot
:
Record
<
string
,
unknown
>
):
{
used
:
number
|
null
;
resetAfterSeconds
:
number
|
null
}
{
const
primaryWindow
=
asNumber
(
snapshot
.
codex_primary_window_minutes
)
const
secondaryWindow
=
asNumber
(
snapshot
.
codex_secondary_window_minutes
)
const
primaryUsed
=
asNumber
(
snapshot
.
codex_primary_used_percent
)
const
secondaryUsed
=
asNumber
(
snapshot
.
codex_secondary_used_percent
)
const
primaryReset
=
asNumber
(
snapshot
.
codex_primary_reset_after_seconds
)
const
secondaryReset
=
asNumber
(
snapshot
.
codex_secondary_reset_after_seconds
)
if
(
primaryWindow
!=
null
&&
primaryWindow
>=
10000
)
{
return
{
used
:
primaryUsed
,
resetAfterSeconds
:
primaryReset
}
}
if
(
secondaryWindow
!=
null
&&
secondaryWindow
>=
10000
)
{
return
{
used
:
secondaryUsed
,
resetAfterSeconds
:
secondaryReset
}
}
return
{
used
:
primaryUsed
,
resetAfterSeconds
:
primaryReset
}
}
function
resolveFromSeconds
(
snapshot
:
Record
<
string
,
unknown
>
,
resetAfterSeconds
:
number
|
null
):
string
|
null
{
if
(
resetAfterSeconds
==
null
)
return
null
const
baseRaw
=
asString
(
snapshot
.
codex_usage_updated_at
)
const
base
=
baseRaw
?
new
Date
(
baseRaw
)
:
new
Date
()
if
(
Number
.
isNaN
(
base
.
getTime
()))
{
return
null
}
const
sec
=
Math
.
max
(
0
,
resetAfterSeconds
)
const
resetAt
=
new
Date
(
base
.
getTime
()
+
sec
*
1000
)
return
resetAt
.
toISOString
()
}
function
applyExpiredRule
(
window
:
ResolvedCodexUsageWindow
,
now
:
Date
):
ResolvedCodexUsageWindow
{
if
(
window
.
usedPercent
==
null
||
!
window
.
resetAt
)
return
window
const
resetDate
=
new
Date
(
window
.
resetAt
)
if
(
Number
.
isNaN
(
resetDate
.
getTime
()))
return
window
if
(
resetDate
.
getTime
()
<=
now
.
getTime
())
{
return
{
usedPercent
:
0
,
resetAt
:
resetDate
.
toISOString
()
}
}
return
window
}
export
function
resolveCodexUsageWindow
(
snapshot
:
(
CodexUsageSnapshot
&
Record
<
string
,
unknown
>
)
|
null
|
undefined
,
window
:
WindowKind
,
now
:
Date
=
new
Date
()
):
ResolvedCodexUsageWindow
{
if
(
!
snapshot
)
{
return
{
usedPercent
:
null
,
resetAt
:
null
}
}
const
typedSnapshot
=
snapshot
as
Record
<
string
,
unknown
>
let
usedPercent
:
number
|
null
let
resetAfterSeconds
:
number
|
null
let
resetAt
:
string
|
null
if
(
window
===
'
5h
'
)
{
usedPercent
=
asNumber
(
typedSnapshot
.
codex_5h_used_percent
)
resetAfterSeconds
=
asNumber
(
typedSnapshot
.
codex_5h_reset_after_seconds
)
resetAt
=
asISOTime
(
typedSnapshot
.
codex_5h_reset_at
)
if
(
usedPercent
==
null
||
(
resetAfterSeconds
==
null
&&
!
resetAt
))
{
const
legacy
=
resolveLegacy5h
(
typedSnapshot
)
if
(
usedPercent
==
null
)
usedPercent
=
legacy
.
used
if
(
resetAfterSeconds
==
null
)
resetAfterSeconds
=
legacy
.
resetAfterSeconds
}
}
else
{
usedPercent
=
asNumber
(
typedSnapshot
.
codex_7d_used_percent
)
resetAfterSeconds
=
asNumber
(
typedSnapshot
.
codex_7d_reset_after_seconds
)
resetAt
=
asISOTime
(
typedSnapshot
.
codex_7d_reset_at
)
if
(
usedPercent
==
null
||
(
resetAfterSeconds
==
null
&&
!
resetAt
))
{
const
legacy
=
resolveLegacy7d
(
typedSnapshot
)
if
(
usedPercent
==
null
)
usedPercent
=
legacy
.
used
if
(
resetAfterSeconds
==
null
)
resetAfterSeconds
=
legacy
.
resetAfterSeconds
}
}
if
(
!
resetAt
)
{
resetAt
=
resolveFromSeconds
(
typedSnapshot
,
resetAfterSeconds
)
}
return
applyExpiredRule
({
usedPercent
,
resetAt
},
now
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment