diff --git a/public/js/monitor.js b/public/js/monitor.js
index 152bced6..f02d5b43 100644
--- a/public/js/monitor.js
+++ b/public/js/monitor.js
@@ -30,6 +30,20 @@ function formatTokenNumber(value) {
return formatNumber(number);
}
+
+function formatAverageMinuteBasis(minutes) {
+ const number = Number(minutes) || 0;
+ if (number <= 0) return '暂无请求数据';
+ if (number >= 24 * 60) return `按 ${formatNumber(number / (24 * 60), 1)} 天有请求时长统计`;
+ if (number >= 60) return `按 ${formatNumber(number / 60, 1)} 小时有请求时长统计`;
+ return `按 ${formatNumber(Math.max(number, 1), 0)} 分钟有请求时长统计`;
+}
+
+function formatAverageDayBasis(days) {
+ const number = Number(days) || 0;
+ return number > 0 ? `按 ${formatNumber(number, 0)} 个有请求日期统计` : '暂无请求数据';
+}
+
function initMonitorPage() {
const savedDays = Number(localStorage.getItem('monitorRangeDays')) || 7;
monitorState.rangeDays = [7, 14, 30].includes(savedDays) ? savedDays : 7;
@@ -106,6 +120,9 @@ function renderMonitorCards(data) {
const totals = data.totals || {};
const averages = data.averages || {};
+ const averageBasis = data.averageBasis || {};
+ const minuteBasisText = formatAverageMinuteBasis(averageBasis.activeMinutes);
+ const dayBasisText = formatAverageDayBasis(averageBasis.activeDays);
cards.innerHTML = `
请求数
@@ -126,17 +143,17 @@ function renderMonitorCards(data) {
平均 TPM
${formatNumber(averages.tpm, 2)}
-
每分钟 Token 数
+
${minuteBasisText}
平均 RPM
${formatNumber(averages.rpm, 2)}
-
每分钟请求数
+
${minuteBasisText}
日均 RDP
${formatNumber(averages.rdp, 2)}
-
每日请求数
+
${dayBasisText}
`;
}
diff --git a/src/server/handlers/common/retry.js b/src/server/handlers/common/retry.js
index 367d5fb3..6043aa73 100644
--- a/src/server/handlers/common/retry.js
+++ b/src/server/handlers/common/retry.js
@@ -287,15 +287,15 @@ export async function with429Retry(fn, maxRetries, options = {}, legacyOnAttempt
}
return await fn(attempt, shouldUseCredits);
} catch (error) {
- if (typeof retryOptions.onAttemptFailure === 'function') {
- retryOptions.onAttemptFailure(error, attempt);
- }
-
const status = getStatus(error);
if (status !== 429 && status !== 503) {
throw error;
}
+ if (typeof retryOptions.onAttemptFailure === 'function') {
+ retryOptions.onAttemptFailure(error, attempt);
+ }
+
const hint = getRetryHint(error);
const errorType = status === 503 ? '503' : '429';
const modelId = retryOptions.modelId || null;
diff --git a/src/utils/usageStats.js b/src/utils/usageStats.js
index 528ce79f..bbe81bc3 100644
--- a/src/utils/usageStats.js
+++ b/src/utils/usageStats.js
@@ -61,6 +61,8 @@ function normalizeUsage(usage = {}) {
function createBucket(day) {
return {
day,
+ firstSeen: null,
+ lastSeen: null,
...emptyTotals(),
models: {}
};
@@ -79,7 +81,12 @@ class UsageStatsStore {
const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
for (const bucket of raw.buckets || []) {
if (bucket?.day) {
- this.buckets.set(Number(bucket.day), bucket);
+ const day = Number(bucket.day);
+ this.buckets.set(day, {
+ ...createBucket(day),
+ ...bucket,
+ models: bucket.models || {}
+ });
}
}
this.prune();
@@ -112,6 +119,8 @@ class UsageStatsStore {
record({ timestamp = Date.now(), statusCode = 0, model = 'unknown', usage = {}, successCount, failedCount } = {}) {
const day = startOfUtcDay(timestamp);
const bucket = this.buckets.get(day) || createBucket(day);
+ bucket.firstSeen = bucket.firstSeen === null ? timestamp : Math.min(bucket.firstSeen, timestamp);
+ bucket.lastSeen = bucket.lastSeen === null ? timestamp : Math.max(bucket.lastSeen, timestamp);
const normalizedUsage = normalizeUsage(usage);
const modelName = typeof model === 'string' && model.trim() ? model.trim() : 'unknown';
const failed = statusCode >= 400 || statusCode === 0;
@@ -185,7 +194,18 @@ class UsageStatsStore {
}
}
- const minutes = safeDays * 24 * 60;
+ const activeDailyBuckets = daily
+ .map(item => this.buckets.get(Date.parse(`${item.date}T00:00:00.000Z`)))
+ .filter(bucket => bucket && bucket.requests > 0);
+ const activeDays = activeDailyBuckets.length;
+ const activeMinutes = activeDailyBuckets.reduce((sum, bucket) => {
+ const dayStart = bucket.day;
+ const dayEnd = Math.min(dayStart + DAY_MS, now);
+ const observedEnd = bucket.lastSeen === null ? dayEnd : Math.min(bucket.lastSeen, dayEnd);
+ const observedStart = bucket.firstSeen === null ? dayStart : Math.min(Math.max(bucket.firstSeen, dayStart), dayEnd);
+ const elapsedMs = Math.max(60 * 1000, observedEnd - observedStart);
+ return sum + elapsedMs / (60 * 1000);
+ }, 0);
const models = Array.from(modelMap.entries())
.map(([model, stats]) => ({ model, ...stats }))
.sort((a, b) => b.totalTokens - a.totalTokens || b.requests - a.requests);
@@ -194,9 +214,13 @@ class UsageStatsStore {
rangeDays: safeDays,
totals,
averages: {
- tpm: totals.totalTokens / minutes,
- rpm: totals.requests / minutes,
- rdp: totals.requests / safeDays
+ tpm: activeMinutes > 0 ? totals.totalTokens / activeMinutes : 0,
+ rpm: activeMinutes > 0 ? totals.requests / activeMinutes : 0,
+ rdp: activeDays > 0 ? totals.requests / activeDays : 0
+ },
+ averageBasis: {
+ activeDays,
+ activeMinutes
},
models,
daily,