diff --git a/src/providers/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js index 1a9e3bf..bbef8c0 100644 --- a/src/providers/gemini/antigravity-core.js +++ b/src/providers/gemini/antigravity-core.js @@ -1468,29 +1468,32 @@ export class AntigravityApiService { }; const res = await this.authClient.request(requestOptions); - logger.info(`[Antigravity] fetchAvailableModels success`); - if (res.data && res.data.models) { - const modelsData = res.data.models; - - // 遍历模型数据,提取配额信息 - for (const [modelId, modelData] of Object.entries(modelsData)) { - const aliasName = modelName2Alias(modelId); - if (aliasName == null || aliasName === '') continue; // 跳过不支持的模型 + // logger.info(`[Antigravity] fetchAvailableModels success: ${JSON.stringify(res.data)}`); + if (res.data) { + + if (res.data.models) { + const modelsData = res.data.models; - const modelInfo = { - remaining: 0, - resetTime: null, - resetTimeRaw: null - }; - - // 从 quotaInfo 中提取配额信息 - if (modelData.quotaInfo) { - modelInfo.remaining = modelData.quotaInfo.remainingFraction || modelData.quotaInfo.remaining || 0; - modelInfo.resetTime = modelData.quotaInfo.resetTime || null; - modelInfo.resetTimeRaw = modelData.quotaInfo.resetTime; + // 遍历模型数据,提取配额信息 + for (const [modelId, modelData] of Object.entries(modelsData)) { + const aliasName = modelName2Alias(modelId); + if (aliasName == null || aliasName === '') continue; // 跳过不支持的模型 + + const modelInfo = { + remaining: 0, + resetTime: null, + resetTimeRaw: null + }; + + // 从 quotaInfo 中提取配额信息 + if (modelData.quotaInfo) { + modelInfo.remaining = modelData.quotaInfo.remainingFraction !== undefined ? modelData.quotaInfo.remainingFraction : (modelData.quotaInfo.remaining || 0); + modelInfo.resetTime = modelData.quotaInfo.resetTime || null; + modelInfo.resetTimeRaw = modelData.quotaInfo.resetTime; + } + + result.models[aliasName] = modelInfo; } - - result.models[aliasName] = modelInfo; } // 对模型按名称排序 diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index d756b28..fb1930e 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -542,7 +542,7 @@ export class CodexApiService { if (primaryWindow) { // remaining = 1 - (used_percent / 100) const remaining = 1 - (primaryWindow.used_percent || 0) / 100; - const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toISOString() : null; + const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toDateString() : null; // 为所有 Codex 模型设置相同的配额信息 const codexModels = ['default']; diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index 93f3246..7603a55 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -359,9 +359,9 @@ export class ProviderPoolManager { async _refreshNodeToken(providerType, providerStatus, force = false) { const config = providerStatus.config; - // 检查刷新次数是否已达上限(最大3次) + // 检查刷新次数是否已达上限(最大5次) const currentRefreshCount = config.refreshCount || 0; - if (currentRefreshCount >= 3 && !force) { + if (currentRefreshCount >= 5 && !force) { this._log('warn', `Node ${providerStatus.uuid} has reached maximum refresh count (3), marking as unhealthy`); // 标记为不健康 this.markProviderUnhealthyImmediately(providerType, config, 'Maximum refresh count (3) reached'); diff --git a/src/services/usage-service.js b/src/services/usage-service.js index fb838c8..a4e30b5 100644 --- a/src/services/usage-service.js +++ b/src/services/usage-service.js @@ -5,7 +5,7 @@ import { getProviderPoolManager } from './service-manager.js'; import { serviceInstances } from '../providers/adapter.js'; -import { MODEL_PROVIDER } from '../utils/common.js'; +import { MODEL_PROVIDER, formatToLocal } from '../utils/common.js'; /** * 用量查询服务类 @@ -314,31 +314,6 @@ export function formatGeminiUsage(usageData) { return null; } - const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset - - /** - * 将 UTC 时间转换为北京时间 - * @param {string} utcString - UTC 时间字符串 - * @returns {string} 北京时间字符串 - */ - function utcToBeijing(utcString) { - try { - if (!utcString) return '--'; - const utcDate = new Date(utcString); - const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET); - return beijingTime - .toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) - .replace(/\//g, '-'); - } catch (e) { - return '--'; - } - } - const result = { // 基本信息 - 映射到 Kiro 结构 daysUntilReset: null, @@ -377,16 +352,20 @@ export function formatGeminiUsage(usageData) { // 解析模型配额信息 if (usageData.models && typeof usageData.models === 'object') { - for (const [modelName, modelInfo] of Object.entries(usageData.models)) { - // Gemini 返回的数据结构:{ remaining, resetTime, resetTimeRaw } + for (const [modelKey, modelInfo] of Object.entries(usageData.models)) { + // Gemini 返回的数据结构:{ remaining, resetTime, resetTimeRaw, tokenType } // remaining 是 0-1 之间的比例值,表示剩余配额百分比 const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1; const usedPercent = 1 - remainingPercent; + // 解析 modelKey (modelId:tokenType) + const [modelId, tokenType] = modelKey.split(':'); + const displayName = tokenType ? `${modelId} (${tokenType})` : modelId; + const item = { resourceType: 'MODEL_USAGE', - displayName: modelInfo.displayName || modelName, - displayNamePlural: modelInfo.displayName || modelName, + displayName: displayName, + displayNamePlural: displayName, unit: 'quota', currency: null, @@ -411,13 +390,14 @@ export function formatGeminiUsage(usageData) { bonuses: [], // 额外的 Gemini 特有信息 - modelName: modelName, + modelName: modelId, + tokenType: tokenType, inputTokenLimit: modelInfo.inputTokenLimit || 0, outputTokenLimit: modelInfo.outputTokenLimit || 0, remaining: remainingPercent, remainingPercent: Math.round(remainingPercent * 100), // 剩余百分比 resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ? - utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--', + formatToLocal(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--', resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null }; @@ -438,31 +418,6 @@ export function formatAntigravityUsage(usageData) { return null; } - const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset - - /** - * 将 UTC 时间转换为北京时间 - * @param {string} utcString - UTC 时间字符串 - * @returns {string} 北京时间字符串 - */ - function utcToBeijing(utcString) { - try { - if (!utcString) return '--'; - const utcDate = new Date(utcString); - const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET); - return beijingTime - .toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) - .replace(/\//g, '-'); - } catch (e) { - return '--'; - } - } - const result = { // 基本信息 - 映射到 Kiro 结构 daysUntilReset: null, @@ -507,6 +462,10 @@ export function formatAntigravityUsage(usageData) { const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1; const usedPercent = 1 - remainingPercent; + // 优先使用模型自己的重置时间,如果没有则使用全局重置时间 + const resetTimeRaw = modelInfo.resetTimeRaw || (usageData.quotaInfo ? usageData.quotaInfo.quotaResetTime : null); + const resetTimeFormatted = resetTimeRaw ? formatToLocal(resetTimeRaw) : (modelInfo.resetTime || '--'); + const item = { resourceType: 'MODEL_USAGE', displayName: modelInfo.displayName || modelName, @@ -515,7 +474,7 @@ export function formatAntigravityUsage(usageData) { currency: null, // 当前用量 - Antigravity 返回的是剩余比例,转换为已用比例(百分比形式) - currentUsage: usedPercent * 100, + currentUsage: Math.round(usedPercent * 100 * 100) / 100, usageLimit: 100, // 以百分比表示,总量为 100% // 超额信息 @@ -525,8 +484,7 @@ export function formatAntigravityUsage(usageData) { overageCharges: 0, // 下次重置时间 - nextDateReset: modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw).toISOString() : - (modelInfo.resetTime ? new Date(modelInfo.resetTime).toISOString() : null), + nextDateReset: resetTimeRaw ? (typeof resetTimeRaw === 'number' ? new Date(resetTimeRaw * 1000).toISOString() : new Date(resetTimeRaw).toISOString()) : null, // 免费试用信息 freeTrial: null, @@ -539,10 +497,9 @@ export function formatAntigravityUsage(usageData) { inputTokenLimit: modelInfo.inputTokenLimit || 0, outputTokenLimit: modelInfo.outputTokenLimit || 0, remaining: remainingPercent, - remainingPercent: remainingPercent * 100, // 剩余百分比 - resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ? - utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--', - resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null + remainingPercent: Math.round(remainingPercent * 100 * 100) / 100, // 剩余百分比 + resetTime: resetTimeFormatted, + resetTimeRaw: resetTimeRaw }; result.usageBreakdown.push(item); @@ -562,31 +519,6 @@ export function formatCodexUsage(usageData) { return null; } - const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset - - /** - * 将 UTC 时间转换为北京时间 - * @param {string} utcString - UTC 时间字符串 - * @returns {string} 北京时间字符串 - */ - function utcToBeijing(utcString) { - try { - if (!utcString) return '--'; - const utcDate = new Date(utcString); - const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET); - return beijingTime - .toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) - .replace(/\//g, '-'); - } catch (e) { - return '--'; - } - } - const result = { // 基本信息 - 映射到 Kiro 结构 daysUntilReset: null, @@ -661,7 +593,7 @@ export function formatCodexUsage(usageData) { remaining: remainingPercent, remainingPercent: Math.round(remainingPercent * 100), // 剩余百分比 resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ? - utcToBeijing(modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw * 1000).toISOString() : modelInfo.resetTime) : '--', + formatToLocal(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--', resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null, // 注入 raw 窗口信息以便前端使用 diff --git a/src/utils/common.js b/src/utils/common.js index b529eb8..4d65d28 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -1155,6 +1155,34 @@ export function getMD5Hash(obj) { return crypto.createHash('md5').update(jsonString).digest('hex'); } +/** + * 将日期转换为系统本地时间格式 + * @param {string|number} dateInput - 日期字符串或时间戳 + * @returns {string} 格式化后的时间字符串 + */ +export function formatToLocal(dateInput) { + try { + if (!dateInput) return '--'; + // 处理数值型时间戳(秒 -> 毫秒) + let finalInput = dateInput; + if (typeof dateInput === 'number' && dateInput < 10000000000) { + finalInput = dateInput * 1000; + } + const date = new Date(finalInput); + if (isNaN(date.getTime())) return '--'; + + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false + }).replace(/\//g, '-'); + } catch (e) { + return '--'; + } +} + /** * 创建符合 fromProvider 格式的错误响应(非流式) diff --git a/static/app/i18n.js b/static/app/i18n.js index 872fa82..92a848f 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -528,11 +528,22 @@ const translations = { 'usage.card.status.healthy': '健康', 'usage.card.status.unhealthy': '异常', 'usage.card.totalUsage': '总用量', + 'usage.card.resetAt': '将在 {time} 重置', 'usage.card.freeTrial': '免费试用', 'usage.card.bonus': '奖励', 'usage.card.expires': '到期: {time}', + 'usage.doubleClickToRefresh': '双击刷新该提供商用量', + 'usage.refreshingProvider': '正在刷新 {name} 用量...', 'usage.group.expandAll': '展开所有卡片', 'usage.group.collapseAll': '折叠所有卡片', + 'usage.failedToLoad': '加载失败', + 'usage.loadFailed': '获取支持的提供商列表失败', + 'usage.resetInfo': '将在 {time} 后重置', + 'usage.weeklyLimit': '每周限制', + 'usage.time.days': '{days}天{hours}小时', + 'usage.time.hours': '{hours}小时{minutes}分', + 'usage.time.minutes': '{minutes}分钟', + 'usage.time.soon': '即将', // Logs 'logs.title': '实时日志', @@ -1287,11 +1298,22 @@ const translations = { 'usage.card.status.healthy': 'Healthy', 'usage.card.status.unhealthy': 'Abnormal', 'usage.card.totalUsage': 'Total Usage', + 'usage.card.resetAt': 'Resets at {time}', 'usage.card.freeTrial': 'Free Trial', 'usage.card.bonus': 'Bonus', 'usage.card.expires': 'Expires: {time}', + 'usage.doubleClickToRefresh': 'Double click to refresh this provider', + 'usage.refreshingProvider': 'Refreshing {name} usage...', 'usage.group.expandAll': 'Expand All Cards', 'usage.group.collapseAll': 'Collapse All Cards', + 'usage.failedToLoad': 'Failed to load', + 'usage.loadFailed': 'Failed to load supported providers', + 'usage.resetInfo': 'Resets in {time}', + 'usage.weeklyLimit': 'Weekly Limit', + 'usage.time.days': '{days}d {hours}h', + 'usage.time.hours': '{hours}h {minutes}m', + 'usage.time.minutes': '{minutes}m', + 'usage.time.soon': 'Soon', // Logs 'logs.title': 'Real-time Logs', diff --git a/static/app/usage-manager.js b/static/app/usage-manager.js index 4f416e7..31ecfbc 100644 --- a/static/app/usage-manager.js +++ b/static/app/usage-manager.js @@ -42,11 +42,19 @@ async function loadSupportedProviders() { const tag = document.createElement('span'); tag.className = 'provider-tag'; tag.textContent = getProviderDisplayName(provider); + tag.title = t('usage.doubleClickToRefresh') || '双击刷新该提供商用量'; + tag.setAttribute('data-i18n-title', 'usage.doubleClickToRefresh'); + + // 添加双击事件 + tag.addEventListener('dblclick', () => { + refreshProviderUsage(provider); + }); + listEl.appendChild(tag); }); } catch (error) { console.error('获取支持的提供商列表失败:', error); - listEl.innerHTML = 'Failed to load'; + listEl.innerHTML = `${t('usage.failedToLoad')}`; } } @@ -105,7 +113,7 @@ export async function loadUsage() { errorEl.style.display = 'block'; const errorMsgEl = document.getElementById('usageErrorMessage'); if (errorMsgEl) { - errorMsgEl.textContent = error.message || t('usage.title') + '失败'; + errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed')); } } } @@ -164,7 +172,7 @@ export async function refreshUsage() { errorEl.style.display = 'block'; const errorMsgEl = document.getElementById('usageErrorMessage'); if (errorMsgEl) { - errorMsgEl.textContent = error.message || t('usage.title') + '失败'; + errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed')); } } @@ -203,7 +211,7 @@ function renderUsageData(data, container) { const validInstances = []; for (const instance of providerData.instances) { // 过滤掉服务实例未初始化的 - if (instance.error === '服务实例未初始化') { + if (instance.error === '服务实例未初始化' || instance.error === 'Service instance not initialized') { continue; } // 过滤掉已禁用的提供商 @@ -235,6 +243,51 @@ function renderUsageData(data, container) { } } +/** + * 刷新特定提供商类型的用量数据 + * @param {string} providerType - 提供商类型 + */ +export async function refreshProviderUsage(providerType) { + const loadingEl = document.getElementById('usageLoading'); + const refreshBtn = document.getElementById('refreshUsageBtn'); + const contentEl = document.getElementById('usageContent'); + + // 显示加载状态 + if (loadingEl) loadingEl.style.display = 'block'; + if (refreshBtn) refreshBtn.disabled = true; + + try { + const providerName = getProviderDisplayName(providerType); + showToast(t('common.info'), t('usage.refreshingProvider', { name: providerName }), 'info'); + + // 调用按提供商刷新的 API + const response = await fetch(`/api/usage/${providerType}?refresh=true`, { + method: 'GET', + headers: getAuthHeaders() + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const providerData = await response.json(); + + // 获取当前完整数据并更新其中一个提供商的数据 + // 注意:这里为了保持页面一致性,我们重新获取一次完整数据(走缓存)来重新渲染 + // 或者手动在当前 DOM 中更新该提供商的部分。 + // 为了简单可靠,我们重新 loadUsage(),它会读取刚刚更新过的后端缓存 + await loadUsage(); + + showToast(t('common.success'), t('common.refresh.success'), 'success'); + } catch (error) { + console.error(`刷新提供商 ${providerType} 失败:`, error); + showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error'); + } finally { + if (loadingEl) loadingEl.style.display = 'none'; + if (refreshBtn) refreshBtn.disabled = false; + } +} + /** * 创建提供商分组容器 * @param {string} providerType - 提供商类型 @@ -349,6 +402,10 @@ function createInstanceUsageCard(instance, providerType) { // 显示名称:优先自定义名称,其次 uuid const displayName = instance.name || instance.uuid; + + const displayUsageText = totalUsage.isCodex + ? `${totalUsage.percent.toFixed(1)}%` + : `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`; collapsedSummary.innerHTML = `
@@ -362,8 +419,8 @@ function createInstanceUsageCard(instance, providerType) {
${totalUsage.percent.toFixed(1)}% - ${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)} - ` : (instance.error ? `${t('common.error')}` : '')} + ${displayUsageText} + ` : (instance.error ? `${t('common.error')}` : '')} `; @@ -459,18 +516,43 @@ function renderUsageDetails(usage) { const progressClass = totalUsage.percent >= 90 ? 'danger' : (totalUsage.percent >= 70 ? 'warning' : 'normal'); + // 提取第一个有重置时间的条目(通常是总配额) + let resetTimeHTML = ''; + if (totalUsage.isCodex && totalUsage.resetAfterSeconds !== undefined) { + const resetTimeText = formatTimeRemaining(totalUsage.resetAfterSeconds); + resetTimeHTML = ` +
+ ${t('usage.resetInfo', { time: resetTimeText })} +
+ `; + } else { + const resetTimeEntry = usage.usageBreakdown.find(b => b.resetTime && b.resetTime !== '--'); + resetTimeHTML = resetTimeEntry ? ` +
+ ${t('usage.card.resetAt', { time: resetTimeEntry.resetTime })} +
+ ` : ''; + } + + const displayValue = totalUsage.isCodex + ? `${totalUsage.percent.toFixed(1)}%` + : `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`; + totalSection.innerHTML = `
${t('usage.card.totalUsage')} - ${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)} + ${displayValue}
-
${totalUsage.percent.toFixed(2)}%
+ `; container.appendChild(totalSection); @@ -522,6 +604,19 @@ function createUsageBreakdownHTML(breakdown) { `; + // 如果有重置时间,则显示 + if (breakdown.resetTime && breakdown.resetTime !== '--') { + const resetText = t('usage.card.resetAt', { time: breakdown.resetTime }); + html += ` +
+ + + ${resetText} + +
+ `; + } + // 免费试用信息 if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') { html += ` @@ -559,52 +654,28 @@ function createUsageBreakdownHTML(breakdown) { */ function createCodexUsageBreakdownHTML(breakdown) { const rl = breakdown.rateLimit; - const primary = rl.primary_window; const secondary = rl.secondary_window; - const primaryPercent = primary.used_percent || 0; - const primaryProgressClass = primaryPercent >= 90 ? 'danger' : (primaryPercent >= 70 ? 'warning' : 'normal'); - - const primaryLimitHours = Math.round(primary.limit_window_seconds / 3600); - const primaryResetText = formatTimeRemaining(primary.reset_after_seconds); + if (!secondary) return ''; - let html = ` + const secondaryPercent = secondary.used_percent || 0; + const secondaryProgressClass = secondaryPercent >= 90 ? 'danger' : (secondaryPercent >= 70 ? 'warning' : 'normal'); + const secondaryResetText = formatTimeRemaining(secondary.reset_after_seconds); + + return `
- ${primaryLimitHours} 小时限制 - ${primaryPercent}% + ${t('usage.weeklyLimit')} + ${secondaryPercent}%
-
-
+
+
-
- 将在 ${primaryResetText} 后重置 +
+ ${t('usage.resetInfo', { time: secondaryResetText })}
+
`; - - if (secondary) { - const secondaryPercent = secondary.used_percent || 0; - const secondaryProgressClass = secondaryPercent >= 90 ? 'danger' : (secondaryPercent >= 70 ? 'warning' : 'normal'); - const secondaryResetText = formatTimeRemaining(secondary.reset_after_seconds); - - html += ` -
-
- 每周限制 - ${secondaryPercent}% -
-
-
-
-
- 将在 ${secondaryResetText} 后重置 -
-
- `; - } - - html += '
'; - return html; } /** @@ -613,15 +684,15 @@ function createCodexUsageBreakdownHTML(breakdown) { * @returns {string} 格式化后的时间 */ function formatTimeRemaining(seconds) { - if (seconds <= 0) return '即将'; + if (seconds <= 0) return t('usage.time.soon'); const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); - if (days > 0) return `${days}天${hours}小时`; - if (hours > 0) return `${hours}小时${minutes}分`; - return `${minutes}分钟`; + if (days > 0) return t('usage.time.days', { days, hours }); + if (hours > 0) return t('usage.time.hours', { hours, minutes }); + return t('usage.time.minutes', { minutes }); } /** @@ -634,6 +705,30 @@ function calculateTotalUsage(usageBreakdown) { return { hasData: false, used: 0, limit: 0, percent: 0 }; } + // 特殊处理 Codex + const codexEntry = usageBreakdown.find(b => b.rateLimit && b.rateLimit.secondary_window); + if (codexEntry) { + const secondary = codexEntry.rateLimit.secondary_window; + const secondaryPercent = secondary.used_percent || 0; + + // 只有当周限制达到 100% 时,总用量才显示 100% + // 否则按正常逻辑计算(或者这里可以理解为非 100% 时不改变原有的总用量逻辑, + // 但根据用户反馈,Codex 应该主要关注周限制) + // 重新审视需求:达到周限制时,总用量直接100%,重置时间设置为周限制时间 + + if (secondaryPercent >= 100) { + return { + hasData: true, + used: 100, + limit: 100, + percent: 100, + isCodex: true, + resetAfterSeconds: secondary.reset_after_seconds + }; + } + // 如果未达到 100%,则继续执行下面的常规计算逻辑 + } + let totalUsed = 0; let totalLimit = 0; diff --git a/static/components/section-usage.css b/static/components/section-usage.css index 06e5509..8d0f0f4 100644 --- a/static/components/section-usage.css +++ b/static/components/section-usage.css @@ -43,16 +43,6 @@ gap: 0.5rem; } -.provider-tag { - background: var(--bg-primary); - border: 1px solid var(--border-color); - padding: 0.125rem 0.5rem; - border-radius: 9999px; - font-size: 0.75rem; - color: var(--text-primary); - font-weight: 500; -} - .loading-inline { display: inline-flex; align-items: center; @@ -315,6 +305,28 @@ .total-label { font-size: 0.8rem; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 0.375rem; } .total-value { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); font-family: monospace; } +.total-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.5rem; +} + +.total-percent { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); +} + +.total-reset-info { + font-size: 0.65rem; + color: var(--text-tertiary); + font-style: italic; + display: flex; + align-items: center; + gap: 0.25rem; +} + .reset-info-compact { background: var(--bg-secondary); padding: 0.5rem 0.75rem; border-radius: 0.375rem; border: 1px solid var(--border-color); } .reset-info-row { display: flex; justify-content: space-between; align-items: center; padding: 0.25rem 0; } .reset-info-row:first-child { border-bottom: 1px solid var(--border-color); padding-bottom: 0.375rem; margin-bottom: 0.25rem; } @@ -408,11 +420,6 @@ [data-theme="dark"] .page-btn { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); } [data-theme="dark"] .page-jump-input { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); } -/* Codex 专用样式 */ -.codex-usage-item { - border-left: 3px solid var(--primary-color); -} - .codex-reset-info { font-size: 0.65rem; color: var(--text-tertiary);