From d8ec86918f88f2e37585c7f55c07cb72ac66a30e Mon Sep 17 00:00:00 2001 From: hex2077 Date: Thu, 22 Jan 2026 17:30:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(provider):=20=E6=B7=BB=E5=8A=A0=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=97=B6=E5=87=AD=E8=AF=81=E4=B8=B4=E8=BF=91=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在多个provider的核心代码中添加凭证过期检查逻辑,当检测到凭证即将过期时自动标记为需要刷新 新增formatLog和formatExpiryLog工具函数统一日志格式 修改provider-pool-manager以支持带优先级的刷新队列 --- src/providers/claude/claude-kiro.js | 71 ++++++++++-------------- src/providers/gemini/antigravity-core.js | 32 +++++++++-- src/providers/gemini/gemini-core.js | 33 +++++++++-- src/providers/openai/codex-core.js | 31 +++++++++-- src/providers/openai/iflow-core.js | 47 ++++++++-------- src/providers/openai/qwen-core.js | 32 +++++++++-- src/providers/provider-pool-manager.js | 7 +-- src/utils/common.js | 47 ++++++++++++++++ 8 files changed, 212 insertions(+), 88 deletions(-) diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index e77ce82..4aa48b8 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -9,7 +9,7 @@ import * as https from 'https'; import { getProviderModels } from '../provider-models.js'; import { countTokens } from '@anthropic-ai/tokenizer'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; +import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; const KIRO_THINKING = { @@ -1347,12 +1347,12 @@ async saveCredentialsToFile(filePath, newData) { } // Handle 402 (Payment Required / Quota Exceeded) - verify usage and mark as unhealthy with recovery time - if (status === 402) { + if (status === 402 && !isRetry) { await this._handle402Error(error, 'callApi'); } // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry - if (status === 403) { + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403. Marking credential as need refresh...'); // 检查是否为 temporarily suspended 错误 @@ -1364,11 +1364,11 @@ async saveCredentialsToFile(filePath, newData) { this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error); } else { // 其他 403 错误:先刷新 UUID,然后标记需要刷新 - const newUuid = this._refreshUuid(); - if (newUuid) { - console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); - this.uuid = newUuid; - } + // const newUuid = this._refreshUuid(); + // if (newUuid) { + // console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + // this.uuid = newUuid; + // } this._markCredentialNeedRefresh('403 Forbidden', error); } @@ -1595,16 +1595,11 @@ async saveCredentialsToFile(filePath, newData) { async generateContent(model, requestBody) { if (!this.isInitialized) await this.initialize(); - // Token 刷新策略: - // 1. 已过期 → 必须等待刷新 - // 2. 即将过期但还能用 → 后台异步刷新,不阻塞当前请求 - // if (this.isTokenExpired()) { - // console.log('[Kiro] Token is expired, must refresh before generateContent request...'); - // await this.initializeAuth(true); - // } else if (this.isExpiryDateNear()) { - // console.log('[Kiro] Token is near expiry, triggering background refresh...'); - // this.triggerBackgroundRefresh(); - // } + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + console.log('[Kiro] Token is near expiry, marking credential as need refresh...'); + this._markCredentialNeedRefresh('Token near expiry in generateContent'); + } const finalModel = MODEL_MAPPING[model] ? model : this.modelName; console.log(`[Kiro] Calling generateContent with model: ${finalModel}`); @@ -1872,12 +1867,12 @@ async saveCredentialsToFile(filePath, newData) { } // Handle 402 (Payment Required / Quota Exceeded) - verify usage and mark as unhealthy with recovery time - if (status === 402) { + if (status === 402 && !isRetry) { await this._handle402Error(error, 'stream'); } // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry - if (status === 403) { + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403 in stream. Marking credential as need refresh...'); // 检查是否为 temporarily suspended 错误 @@ -1889,11 +1884,11 @@ async saveCredentialsToFile(filePath, newData) { this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error); } else { // 其他 403 错误:先刷新 UUID,然后标记需要刷新 - const newUuid = this._refreshUuid(); - if (newUuid) { - console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); - this.uuid = newUuid; - } + // const newUuid = this._refreshUuid(); + // if (newUuid) { + // console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + // this.uuid = newUuid; + // } this._markCredentialNeedRefresh('403 Forbidden', error); } @@ -1956,17 +1951,12 @@ async saveCredentialsToFile(filePath, newData) { // 真正的流式传输实现 async * generateContentStream(model, requestBody) { if (!this.isInitialized) await this.initialize(); - - // Token 刷新策略: - // 1. 已过期 → 必须等待刷新 - // 2. 即将过期但还能用 → 后台异步刷新,不阻塞当前请求 - // if (this.isTokenExpired()) { - // console.log('[Kiro] Token is expired, must refresh before generateContentStream request...'); - // await this.initializeAuth(true); - // } else if (this.isExpiryDateNear()) { - // console.log('[Kiro] Token is near expiry, triggering background refresh...'); - // this.triggerBackgroundRefresh(); - // } + + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + console.log('[Kiro] Token is near expiry, marking credential as need refresh...'); + this._markCredentialNeedRefresh('Token near expiry in generateContentStream'); + } const finalModel = MODEL_MAPPING[model] ? model : this.modelName; console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); @@ -2611,11 +2601,10 @@ async saveCredentialsToFile(filePath, newData) { isExpiryDateNear() { try { const expirationTime = new Date(this.expiresAt); - const currentTime = new Date(); - const cronNearMinutesInMillis = (this.config.CRON_NEAR_MINUTES || 10) * 60 * 1000; - const thresholdTime = new Date(currentTime.getTime() + cronNearMinutesInMillis); - console.log(`[Kiro] Expiry date: ${expirationTime.getTime()}, Current time: ${currentTime.getTime()}, ${this.config.CRON_NEAR_MINUTES || 10} minutes from now: ${thresholdTime.getTime()}`); - return expirationTime.getTime() <= thresholdTime.getTime(); + const nearMinutes = 30; + const { message, isNearExpiry } = formatExpiryLog('Kiro', expirationTime.getTime(), nearMinutes); + console.log(message); + return isNearExpiry; } catch (error) { console.error(`[Kiro] Error checking expiry date: ${this.expiresAt}, Error: ${error.message}`); return false; // Treat as expired if parsing fails diff --git a/src/providers/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js index 6817904..32b5d85 100644 --- a/src/providers/gemini/antigravity-core.js +++ b/src/providers/gemini/antigravity-core.js @@ -9,7 +9,7 @@ import * as os from 'os'; import * as readline from 'readline'; import { v4 as uuidv4 } from 'uuid'; import open from 'open'; -import { formatExpiryTime, isRetryableNetworkError } from '../../utils/common.js'; +import { formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js'; @@ -1304,6 +1304,17 @@ export class AntigravityApiService { async generateContent(model, requestBody) { console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { + uuid: this.uuid + }); + } + } + let selectedModel = model; if (!this.availableModels.includes(model)) { console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); @@ -1360,6 +1371,17 @@ export class AntigravityApiService { async * generateContentStream(model, requestBody) { console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { + uuid: this.uuid + }); + } + } + let selectedModel = model; if (!this.availableModels.includes(model)) { console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); @@ -1384,10 +1406,10 @@ export class AntigravityApiService { isExpiryDateNear() { try { - const currentTime = Date.now(); - const cronNearMinutesInMillis = (this.config.CRON_NEAR_MINUTES || 10) * 60 * 1000; - console.log(`[Antigravity] Expiry date: ${this.authClient.credentials.expiry_date}, Current time: ${currentTime}, ${this.config.CRON_NEAR_MINUTES || 10} minutes from now: ${currentTime + cronNearMinutesInMillis}`); - return this.authClient.credentials.expiry_date <= (currentTime + cronNearMinutesInMillis); + const nearMinutes = 20; + const { message, isNearExpiry } = formatExpiryLog('Antigravity', this.authClient.credentials.expiry_date, nearMinutes); + console.log(message); + return isNearExpiry; } catch (error) { console.error(`[Antigravity] Error checking expiry date: ${error.message}`); return false; diff --git a/src/providers/gemini/gemini-core.js b/src/providers/gemini/gemini-core.js index 2359249..3fb3e1a 100644 --- a/src/providers/gemini/gemini-core.js +++ b/src/providers/gemini/gemini-core.js @@ -6,7 +6,7 @@ import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import open from 'open'; -import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError } from '../../utils/common.js'; +import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils/proxy-utils.js'; @@ -638,6 +638,18 @@ export class GeminiApiService { async generateContent(model, requestBody) { console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { + uuid: this.uuid + }); + } + } + let selectedModel = model; if (!GEMINI_MODELS.includes(model)) { console.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); @@ -652,6 +664,17 @@ export class GeminiApiService { async * generateContentStream(model, requestBody) { console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { + uuid: this.uuid + }); + } + } + // 检查是否为防截断模型 if (is_anti_truncation_model(model)) { // 从防截断模型名中提取实际模型名 @@ -681,10 +704,10 @@ export class GeminiApiService { */ isExpiryDateNear() { try { - const currentTime = Date.now(); - const cronNearMinutesInMillis = (this.config.CRON_NEAR_MINUTES || 10) * 60 * 1000; - console.log(`[Gemini] Expiry date: ${this.authClient.credentials.expiry_date}, Current time: ${currentTime}, ${this.config.CRON_NEAR_MINUTES || 10} minutes from now: ${currentTime + cronNearMinutesInMillis}`); - return this.authClient.credentials.expiry_date <= (currentTime + cronNearMinutesInMillis); + const nearMinutes = 20; + const { message, isNearExpiry } = formatExpiryLog('Gemini', this.authClient.credentials.expiry_date, nearMinutes); + console.log(message); + return isNearExpiry; } catch (error) { console.error(`[Gemini] Error checking expiry date: ${error.message}`); return false; diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index bb0e033..e69f740 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -5,7 +5,7 @@ import path from 'path'; import os from 'os'; import { refreshCodexTokensWithRetry } from '../../auth/oauth-handlers.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; -import { MODEL_PROVIDER } from '../../utils/common.js'; +import { MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; /** * Codex API 服务类 @@ -133,6 +133,17 @@ export class CodexApiService { await this.initialize(); } + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { + uuid: this.uuid + }); + } + } + const url = `${this.baseUrl}/responses`; const body = this.prepareRequestBody(model, requestBody, false); const headers = this.buildHeaders(body.prompt_cache_key); @@ -175,6 +186,17 @@ export class CodexApiService { await this.initialize(); } + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { + uuid: this.uuid + }); + } + } + const url = `${this.baseUrl}/responses`; const body = this.prepareRequestBody(model, requestBody, true); const headers = this.buildHeaders(body.prompt_cache_key); @@ -283,10 +305,11 @@ export class CodexApiService { */ isExpiryDateNear() { if (!this.expiresAt) return true; - const now = Date.now(); const expiry = this.expiresAt.getTime(); - const bufferMs = 5 * 60 * 1000; // 5 分钟缓冲 - return expiry <= now + bufferMs; + const nearMinutes = 20; + const { message, isNearExpiry } = formatExpiryLog('Codex', expiry, nearMinutes); + console.log(message); + return isNearExpiry; } /** diff --git a/src/providers/openai/iflow-core.js b/src/providers/openai/iflow-core.js index 8e13086..5a662cf 100644 --- a/src/providers/openai/iflow-core.js +++ b/src/providers/openai/iflow-core.js @@ -23,7 +23,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; +import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; // iFlow API 端点 @@ -687,10 +687,8 @@ export class IFlowApiService { return false; } - const currentTime = Date.now(); // 授权文件时效48小时,判断是否过期或接近过期 (45小时) const cronNearMinutes = 60 * 45; - const cronNearMinutesInMillis = cronNearMinutes * 60 * 1000; // 解析过期时间 let expireTime; @@ -720,23 +718,10 @@ export class IFlowApiService { return false; } - // 计算剩余时间 - const timeRemaining = expireTime - currentTime; + const { message, isNearExpiry } = formatExpiryLog('iFlow', expireTime, cronNearMinutes); + console.log(message); - // 判断是否已过期或接近过期 - // 已过期:timeRemaining <= 0 - // 接近过期:timeRemaining > 0 && timeRemaining <= cronNearMinutesInMillis - const isExpired = timeRemaining <= 0; - const isNear = timeRemaining > 0 && timeRemaining <= cronNearMinutesInMillis; - const needsRefresh = isExpired || isNear; - - const expireDateStr = new Date(expireTime).toISOString(); - const timeRemainingMinutes = Math.floor(timeRemaining / 60000); - const timeRemainingHours = (timeRemaining / 3600000).toFixed(2); - - console.log(`[iFlow] Token expiry check: Expiry=${expireDateStr}, Remaining=${timeRemainingHours}h (${timeRemainingMinutes}min), Threshold=${cronNearMinutes}min, Expired=${isExpired}, Near=${isNear}, NeedsRefresh=${needsRefresh}`); - - return needsRefresh; + return isNearExpiry; } catch (error) { console.error(`[iFlow] Error checking expiry date: ${error.message}`); return false; @@ -1012,8 +997,16 @@ export class IFlowApiService { await this.initialize(); } - // 在 API 调用前不再同步检查是否需要刷新 Token (V2 架构) - // await this._checkAndRefreshTokenIfNeeded(); + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { + uuid: this.uuid + }); + } + } return this.callApi('/chat/completions', requestBody, model); } @@ -1026,8 +1019,16 @@ export class IFlowApiService { await this.initialize(); } - // 在 API 调用前不再同步检查是否需要刷新 Token (V2 架构) - // await this._checkAndRefreshTokenIfNeeded(); + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { + uuid: this.uuid + }); + } + } yield* this.streamApi('/chat/completions', requestBody, model); } diff --git a/src/providers/openai/qwen-core.js b/src/providers/openai/qwen-core.js index 4d5b454..c91c1e4 100644 --- a/src/providers/openai/qwen-core.js +++ b/src/providers/openai/qwen-core.js @@ -11,7 +11,7 @@ import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; import { handleQwenOAuth } from '../../auth/oauth-handlers.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; +import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; // --- Constants --- @@ -650,10 +650,32 @@ export class QwenApiService { } async generateContent(model, requestBody) { + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, { + uuid: this.uuid + }); + } + } + return this.callApiWithAuthAndRetry('/chat/completions', requestBody, false); } async *generateContentStream(model, requestBody) { + // 检查 token 是否即将过期,如果是则推送到刷新队列 + if (this.isExpiryDateNear()) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); + poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, { + uuid: this.uuid + }); + } + } + const stream = await this.callApiWithAuthAndRetry('/chat/completions', requestBody, true); let buffer = ''; for await (const chunk of stream) { @@ -689,10 +711,10 @@ export class QwenApiService { if (!credentials || !credentials.expiry_date) { return false; } - const currentTime = Date.now(); - const cronNearMinutesInMillis = (this.config.CRON_NEAR_MINUTES || 10) * 60 * 1000; - console.log(`[Qwen] Expiry date: ${credentials.expiry_date}, Current time: ${currentTime}, ${this.config.CRON_NEAR_MINUTES || 10} minutes from now: ${currentTime + cronNearMinutesInMillis}`); - return credentials.expiry_date <= (currentTime + cronNearMinutesInMillis); + const nearMinutes = 20; + const { message, isNearExpiry } = formatExpiryLog('Qwen', credentials.expiry_date, nearMinutes); + console.log(message); + return isNearExpiry; } catch (error) { console.error(`[Qwen] Error checking expiry date: ${error.message}`); return false; diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index c430a1d..ce631fd 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -834,7 +834,7 @@ export class ProviderPoolManager { this._log('info', `Marked provider ${providerConfig.uuid} as needsRefresh. Enqueuing...`); // 推入异步刷新队列 - this._enqueueRefresh(providerType, provider); + this._enqueueRefresh(providerType, provider, true); this._debouncedSave(providerType); } @@ -910,10 +910,7 @@ export class ProviderPoolManager { } this._log('warn', `Immediately marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Reason: ${errorMessage || 'Authentication error'}`); - - // --- V2: 触发自动刷新 --- - // this._enqueueRefresh(providerType, provider); - + this._debouncedSave(providerType); } } diff --git a/src/utils/common.js b/src/utils/common.js index 7b0996e..ab69952 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -116,6 +116,53 @@ export function formatExpiryTime(expiryTimestamp) { return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`; } +/** + * 格式化日志输出,统一日志格式 + * @param {string} tag - 日志标签,如 'Qwen', 'Kiro' 等 + * @param {string} message - 日志消息 + * @param {Object} [data] - 可选的数据对象,将被格式化输出 + * @returns {string} 格式化后的日志字符串 + */ +export function formatLog(tag, message, data = null) { + let logMessage = `[${tag}] ${message}`; + + if (data !== null && data !== undefined) { + if (typeof data === 'object') { + const dataStr = Object.entries(data) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + logMessage += ` | ${dataStr}`; + } else { + logMessage += ` | ${data}`; + } + } + + return logMessage; +} + +/** + * 格式化凭证过期时间日志 + * @param {string} tag - 日志标签,如 'Qwen', 'Kiro' 等 + * @param {number} expiryDate - 过期时间戳 + * @param {number} nearMinutes - 临近过期的分钟数 + * @returns {{message: string, isNearExpiry: boolean}} 格式化后的日志字符串和是否临近过期 + */ +export function formatExpiryLog(tag, expiryDate, nearMinutes) { + const currentTime = Date.now(); + const nearMinutesInMillis = nearMinutes * 60 * 1000; + const thresholdTime = currentTime + nearMinutesInMillis; + const isNearExpiry = expiryDate <= thresholdTime; + + const message = formatLog(tag, 'Checking expiry date', { + 'Expiry date': expiryDate, + 'Current time': currentTime, + [`${nearMinutes} minutes from now`]: thresholdTime, + 'Is near expiry': isNearExpiry + }); + + return { message, isNearExpiry }; +} + /** * Reads the entire request body from an HTTP request. * @param {http.IncomingMessage} req - The HTTP request object.