From f57f12c6cceb0448d2ca3b2ace6b5b9099016986 Mon Sep 17 00:00:00 2001 From: hi_yueban Date: Thu, 25 Dec 2025 17:00:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20OpenAI=20=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=205h/7d=20=E4=BD=BF=E7=94=A8=E9=99=90=E5=88=B6?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-Authored-By: OpenAI Codex * 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 * 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 * 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 Co-authored-by: OpenAI Codex --- .../service/openai_gateway_service.go | 106 ++++++++++++++ .../components/account/AccountUsageCell.vue | 132 +++++++++++++++--- frontend/src/types/index.ts | 23 ++- 3 files changed, 233 insertions(+), 28 deletions(-) diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 98399e51..ffd61a92 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -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) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 6b91d29f..57e21fbf 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -69,21 +69,21 @@