fix: 修复 OpenAI 账号 5h/7d 使用限制显示错误的问题 (#30)
* fix: 修复 OpenAI 账号 5h/7d 使用限制显示错误的问题 问题描述: - 账号管理页面中,OpenAI OAuth 账号的 5h 列显示 7 天的剩余时间 - 7d 列却显示几小时的剩余时间 - 根本原因: OpenAI 响应头中 primary/secondary 的实际含义与代码假设相反 修复方案: 1. 后端归一化 (openai_gateway_service.go): - 根据 window_minutes 动态判断哪个是 5h/7d 限制 - 新增规范字段 codex_5h_* 和 codex_7d_* - 保留旧字段以兼容性 2. 前端适配 (AccountUsageCell.vue): - 优先使用新的规范字段 - Fallback 到旧字段时基于 window_minutes 动态判断 - 更新 computed 属性命名 3. 类型定义更新 (types/index.ts): - 添加新的规范字段定义 - 更新注释说明实际语义由 window_minutes 决定 🤖 Generated with Claude Code and Codex collaboration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: 改进窗口判断逻辑,修复两个窗口都小于阈值时的bug 问题: 当两个窗口都小于360分钟时(如 primary=180分钟,secondary=300分钟), 之前的逻辑会导致: - primary5h = true, secondary5h = true - 5h 字段会使用 primary(错误) - 7d 字段没有数据(bug) 修复方案: 改用比较策略: 1. 当两个窗口都存在时:较小的分配给5h,较大的分配给7d 2. 当只有一个窗口时:根据大小(<=360分钟)判断是5h还是7d 3. 确保数据不会丢失,逻辑更健壮 示例: - Primary: 180分钟, Secondary: 300分钟 → 5h 使用 Primary(180分钟), 7d 使用 Secondary(300分钟) ✓ 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: 修正窗口大小判断逻辑 - 不能用剩余时间判断窗口类型 **严重bug修复:** 之前的 fallback 逻辑错误地使用 reset_after_seconds 来判断窗口大小。 问题示例: - 周限制(7d)剩余 2h → reset_after_seconds = 7200秒 - 5h限制 剩余 4h → reset_after_seconds = 14400秒 - 错误逻辑:7200/60 < 14400/60,把周限制当成5h ❌ 根本问题: - window_minutes = 窗口的总大小(300 or 10080) - reset_after_seconds = 距离重置的剩余时间(变化的) - 不能用剩余时间来判断窗口类型! 修复方案: 1. **只使用 window_minutes** 来判断窗口大小 2. 移除错误的 reset_after_seconds fallback 3. 如果 window_minutes 都不存在,使用传统假设 4. 添加详细注释说明这个陷阱 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: 修复 lint 问题 - 改进 fallback 逻辑的变量赋值 问题: 第882-883行的简单布尔赋值可能触发 ineffassign 或 staticcheck 警告: use5hFromSecondary = snapshot.SecondaryUsedPercent != nil use7dFromPrimary = snapshot.PrimaryUsedPercent != nil 修复: 改用明确的 if 语句检查任意字段是否存在,更符合代码意图: - 如果 secondary 的任意字段存在,将其视为 5h - 如果 primary 的任意字段存在,将其视为 7d 这样逻辑更清晰,也避免了 lint 警告。 --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
@@ -827,6 +827,112 @@ func (s *OpenAIGatewayService) updateCodexUsageSnapshot(ctx context.Context, acc
|
||||
}
|
||||
updates["codex_usage_updated_at"] = snapshot.UpdatedAt
|
||||
|
||||
// Normalize to canonical 5h/7d fields based on window_minutes
|
||||
// This fixes the issue where OpenAI's primary/secondary naming is reversed
|
||||
// Strategy: Compare the two windows and assign the smaller one to 5h, larger one to 7d
|
||||
|
||||
// IMPORTANT: We can only reliably determine window type from window_minutes field
|
||||
// The reset_after_seconds is remaining time, not window size, so it cannot be used for comparison
|
||||
|
||||
var primaryWindowMins, secondaryWindowMins int
|
||||
var hasPrimaryWindow, hasSecondaryWindow bool
|
||||
|
||||
// Only use window_minutes for reliable window size comparison
|
||||
if snapshot.PrimaryWindowMinutes != nil {
|
||||
primaryWindowMins = *snapshot.PrimaryWindowMinutes
|
||||
hasPrimaryWindow = true
|
||||
}
|
||||
|
||||
if snapshot.SecondaryWindowMinutes != nil {
|
||||
secondaryWindowMins = *snapshot.SecondaryWindowMinutes
|
||||
hasSecondaryWindow = true
|
||||
}
|
||||
|
||||
// Determine which is 5h and which is 7d
|
||||
var use5hFromPrimary, use7dFromPrimary bool
|
||||
var use5hFromSecondary, use7dFromSecondary bool
|
||||
|
||||
if hasPrimaryWindow && hasSecondaryWindow {
|
||||
// Both window sizes known: compare and assign smaller to 5h, larger to 7d
|
||||
if primaryWindowMins < secondaryWindowMins {
|
||||
use5hFromPrimary = true
|
||||
use7dFromSecondary = true
|
||||
} else {
|
||||
use5hFromSecondary = true
|
||||
use7dFromPrimary = true
|
||||
}
|
||||
} else if hasPrimaryWindow {
|
||||
// Only primary window size known: classify by absolute threshold
|
||||
if primaryWindowMins <= 360 {
|
||||
use5hFromPrimary = true
|
||||
} else {
|
||||
use7dFromPrimary = true
|
||||
}
|
||||
} else if hasSecondaryWindow {
|
||||
// Only secondary window size known: classify by absolute threshold
|
||||
if secondaryWindowMins <= 360 {
|
||||
use5hFromSecondary = true
|
||||
} else {
|
||||
use7dFromSecondary = true
|
||||
}
|
||||
} else {
|
||||
// No window_minutes available: cannot reliably determine window types
|
||||
// Fall back to legacy assumption (may be incorrect)
|
||||
// Assume primary=7d, secondary=5h based on historical observation
|
||||
if snapshot.SecondaryUsedPercent != nil || snapshot.SecondaryResetAfterSeconds != nil || snapshot.SecondaryWindowMinutes != nil {
|
||||
use5hFromSecondary = true
|
||||
}
|
||||
if snapshot.PrimaryUsedPercent != nil || snapshot.PrimaryResetAfterSeconds != nil || snapshot.PrimaryWindowMinutes != nil {
|
||||
use7dFromPrimary = true
|
||||
}
|
||||
}
|
||||
|
||||
// Write canonical 5h fields
|
||||
if use5hFromPrimary {
|
||||
if snapshot.PrimaryUsedPercent != nil {
|
||||
updates["codex_5h_used_percent"] = *snapshot.PrimaryUsedPercent
|
||||
}
|
||||
if snapshot.PrimaryResetAfterSeconds != nil {
|
||||
updates["codex_5h_reset_after_seconds"] = *snapshot.PrimaryResetAfterSeconds
|
||||
}
|
||||
if snapshot.PrimaryWindowMinutes != nil {
|
||||
updates["codex_5h_window_minutes"] = *snapshot.PrimaryWindowMinutes
|
||||
}
|
||||
} else if use5hFromSecondary {
|
||||
if snapshot.SecondaryUsedPercent != nil {
|
||||
updates["codex_5h_used_percent"] = *snapshot.SecondaryUsedPercent
|
||||
}
|
||||
if snapshot.SecondaryResetAfterSeconds != nil {
|
||||
updates["codex_5h_reset_after_seconds"] = *snapshot.SecondaryResetAfterSeconds
|
||||
}
|
||||
if snapshot.SecondaryWindowMinutes != nil {
|
||||
updates["codex_5h_window_minutes"] = *snapshot.SecondaryWindowMinutes
|
||||
}
|
||||
}
|
||||
|
||||
// Write canonical 7d fields
|
||||
if use7dFromPrimary {
|
||||
if snapshot.PrimaryUsedPercent != nil {
|
||||
updates["codex_7d_used_percent"] = *snapshot.PrimaryUsedPercent
|
||||
}
|
||||
if snapshot.PrimaryResetAfterSeconds != nil {
|
||||
updates["codex_7d_reset_after_seconds"] = *snapshot.PrimaryResetAfterSeconds
|
||||
}
|
||||
if snapshot.PrimaryWindowMinutes != nil {
|
||||
updates["codex_7d_window_minutes"] = *snapshot.PrimaryWindowMinutes
|
||||
}
|
||||
} else if use7dFromSecondary {
|
||||
if snapshot.SecondaryUsedPercent != nil {
|
||||
updates["codex_7d_used_percent"] = *snapshot.SecondaryUsedPercent
|
||||
}
|
||||
if snapshot.SecondaryResetAfterSeconds != nil {
|
||||
updates["codex_7d_reset_after_seconds"] = *snapshot.SecondaryResetAfterSeconds
|
||||
}
|
||||
if snapshot.SecondaryWindowMinutes != nil {
|
||||
updates["codex_7d_window_minutes"] = *snapshot.SecondaryWindowMinutes
|
||||
}
|
||||
}
|
||||
|
||||
// Update account's Extra field asynchronously
|
||||
go func() {
|
||||
updateCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
@@ -69,21 +69,21 @@
|
||||
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
|
||||
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
|
||||
<div v-if="hasCodexUsage" class="space-y-1">
|
||||
<!-- 5h Window (Secondary) -->
|
||||
<!-- 5h Window -->
|
||||
<UsageProgressBar
|
||||
v-if="codexSecondaryUsedPercent !== null"
|
||||
v-if="codex5hUsedPercent !== null"
|
||||
label="5h"
|
||||
:utilization="codexSecondaryUsedPercent"
|
||||
:resets-at="codexSecondaryResetAt"
|
||||
:utilization="codex5hUsedPercent"
|
||||
:resets-at="codex5hResetAt"
|
||||
color="indigo"
|
||||
/>
|
||||
|
||||
<!-- Weekly Window (Primary) -->
|
||||
<!-- 7d Window -->
|
||||
<UsageProgressBar
|
||||
v-if="codexPrimaryUsedPercent !== null"
|
||||
v-if="codex7dUsedPercent !== null"
|
||||
label="7d"
|
||||
:utilization="codexPrimaryUsedPercent"
|
||||
:resets-at="codexPrimaryResetAt"
|
||||
:utilization="codex7dUsedPercent"
|
||||
:resets-at="codex7dResetAt"
|
||||
color="emerald"
|
||||
/>
|
||||
</div>
|
||||
@@ -125,35 +125,123 @@ const showUsageWindows = computed(() =>
|
||||
const hasCodexUsage = computed(() => {
|
||||
const extra = props.account.extra
|
||||
return extra && (
|
||||
// Check for new canonical fields first
|
||||
extra.codex_5h_used_percent !== undefined ||
|
||||
extra.codex_7d_used_percent !== undefined ||
|
||||
// Fallback to legacy fields
|
||||
extra.codex_primary_used_percent !== undefined ||
|
||||
extra.codex_secondary_used_percent !== undefined
|
||||
)
|
||||
})
|
||||
|
||||
const codexPrimaryUsedPercent = computed(() => {
|
||||
// 5h window usage (prefer canonical field)
|
||||
const codex5hUsedPercent = computed(() => {
|
||||
const extra = props.account.extra
|
||||
if (!extra || extra.codex_primary_used_percent === undefined) return null
|
||||
return extra.codex_primary_used_percent
|
||||
if (!extra) return null
|
||||
|
||||
// Prefer canonical field
|
||||
if (extra.codex_5h_used_percent !== undefined) {
|
||||
return extra.codex_5h_used_percent
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes <= 360) {
|
||||
return extra.codex_primary_used_percent ?? null
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes <= 360) {
|
||||
return extra.codex_secondary_used_percent ?? null
|
||||
}
|
||||
|
||||
// Legacy assumption: secondary = 5h (may be incorrect)
|
||||
return extra.codex_secondary_used_percent ?? null
|
||||
})
|
||||
|
||||
const codexSecondaryUsedPercent = computed(() => {
|
||||
const codex5hResetAt = computed(() => {
|
||||
const extra = props.account.extra
|
||||
if (!extra || extra.codex_secondary_used_percent === undefined) return null
|
||||
return extra.codex_secondary_used_percent
|
||||
if (!extra) return null
|
||||
|
||||
// Prefer canonical field
|
||||
if (extra.codex_5h_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_5h_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes <= 360) {
|
||||
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes <= 360) {
|
||||
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy assumption: secondary = 5h
|
||||
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const codexPrimaryResetAt = computed(() => {
|
||||
// 7d window usage (prefer canonical field)
|
||||
const codex7dUsedPercent = computed(() => {
|
||||
const extra = props.account.extra
|
||||
if (!extra || extra.codex_primary_reset_after_seconds === undefined) return null
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
if (!extra) return null
|
||||
|
||||
// Prefer canonical field
|
||||
if (extra.codex_7d_used_percent !== undefined) {
|
||||
return extra.codex_7d_used_percent
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes >= 10000) {
|
||||
return extra.codex_primary_used_percent ?? null
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes >= 10000) {
|
||||
return extra.codex_secondary_used_percent ?? null
|
||||
}
|
||||
|
||||
// Legacy assumption: primary = 7d (may be incorrect)
|
||||
return extra.codex_primary_used_percent ?? null
|
||||
})
|
||||
|
||||
const codexSecondaryResetAt = computed(() => {
|
||||
const codex7dResetAt = computed(() => {
|
||||
const extra = props.account.extra
|
||||
if (!extra || extra.codex_secondary_reset_after_seconds === undefined) return null
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
if (!extra) return null
|
||||
|
||||
// Prefer canonical field
|
||||
if (extra.codex_7d_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_7d_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes >= 10000) {
|
||||
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes >= 10000) {
|
||||
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy assumption: primary = 7d
|
||||
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const loadUsage = async () => {
|
||||
|
||||
@@ -366,13 +366,24 @@ export interface AccountUsageInfo {
|
||||
|
||||
// OpenAI Codex usage snapshot (from response headers)
|
||||
export interface CodexUsageSnapshot {
|
||||
codex_primary_used_percent?: number; // Weekly limit usage percentage
|
||||
codex_primary_reset_after_seconds?: number; // Seconds until weekly reset
|
||||
codex_primary_window_minutes?: number; // Weekly window in minutes
|
||||
codex_secondary_used_percent?: number; // 5h limit usage percentage
|
||||
codex_secondary_reset_after_seconds?: number; // Seconds until 5h reset
|
||||
codex_secondary_window_minutes?: number; // 5h window in minutes
|
||||
// Legacy fields (kept for backwards compatibility)
|
||||
// NOTE: The naming is ambiguous - actual window type is determined by window_minutes value
|
||||
codex_primary_used_percent?: number; // Usage percentage (check window_minutes for actual window type)
|
||||
codex_primary_reset_after_seconds?: number; // Seconds until reset
|
||||
codex_primary_window_minutes?: number; // Window in minutes
|
||||
codex_secondary_used_percent?: number; // Usage percentage (check window_minutes for actual window type)
|
||||
codex_secondary_reset_after_seconds?: number; // Seconds until reset
|
||||
codex_secondary_window_minutes?: number; // Window in minutes
|
||||
codex_primary_over_secondary_percent?: number; // Overflow ratio
|
||||
|
||||
// Canonical fields (normalized by backend, use these preferentially)
|
||||
codex_5h_used_percent?: number; // 5-hour window usage percentage
|
||||
codex_5h_reset_after_seconds?: number; // Seconds until 5h window reset
|
||||
codex_5h_window_minutes?: number; // 5h window in minutes (should be ~300)
|
||||
codex_7d_used_percent?: number; // 7-day window usage percentage
|
||||
codex_7d_reset_after_seconds?: number; // Seconds until 7d window reset
|
||||
codex_7d_window_minutes?: number; // 7d window in minutes (should be ~10080)
|
||||
|
||||
codex_usage_updated_at?: string; // Last update timestamp
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user