feat(oauth): 添加OAuth令牌自动刷新功能

为Gemini和Kiro服务添加令牌过期检测和自动刷新功能
在API服务器中添加定时任务检查并刷新临近过期的令牌
更新README文档说明新增的CRON配置参数
This commit is contained in:
hex2077 2025-08-07 20:24:40 +08:00
parent d4b4b91b45
commit a68ff027b9
6 changed files with 121 additions and 7 deletions

View file

@ -224,6 +224,8 @@ The following table details all supported parameters in `config.json`:
| `PROMPT_LOG_BASE_NAME` | string | Base name for log files when `PROMPT_LOG_MODE` is `file`. | Defaults to `"prompt_log"` |
| `REQUEST_MAX_RETRIES` | number | Maximum number of automatic retries for failed API requests. | Defaults to `3` |
| `REQUEST_BASE_DELAY` | number | Base delay (milliseconds) between automatic retries. Delay increases with each retry. | Defaults to `1000` |
| `CRON_NEAR_MINUTES` | number | (Gemini-CLI Mode) Interval in minutes for the OAuth token refresh task. | Defaults to `15` |
| `CRON_REFRESH_TOKEN` | boolean | (Gemini-CLI Mode) Whether to enable automatic OAuth token refresh task. | Defaults to `true` |
### 3. Start the Service

View file

@ -224,6 +224,8 @@ claude-kiro-oauth。
| `PROMPT_LOG_BASE_NAME` | string | 当 `PROMPT_LOG_MODE``file` 时,生成的日志文件的基础名称。 | 默认为 `"prompt_log"` |
| `REQUEST_MAX_RETRIES` | number | 当 API 请求失败时,自动重试的最大次数。 | 默认为 `3` |
| `REQUEST_BASE_DELAY` | number | 自动重试之间的基础延迟时间(毫秒)。每次重试后延迟会增加。 | 默认为 `1000` |
| `CRON_NEAR_MINUTES` | number | (Gemini-CLI 模式) OAuth 令牌刷新任务计划的间隔时间(分钟)。 | 默认为 `15` |
| `CRON_REFRESH_TOKEN` | boolean | (Gemini-CLI 模式) 是否开启 OAuth 令牌自动刷新任务。 | 默认为 `true` |
### 3. 启动服务

View file

@ -40,6 +40,14 @@ export class ApiServiceAdapter {
async listModels() {
throw new Error("Method 'listModels()' must be implemented.");
}
/**
* 刷新认证令牌
* @returns {Promise<void>}
*/
async refreshToken() {
throw new Error("Method 'refreshToken()' must be implemented.");
}
}
// Gemini API 服务适配器
@ -76,6 +84,14 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
// Gemini Core API 的 listModels 已经返回符合 Gemini 格式的数据,所以不需要额外转换
return this.geminiApiService.listModels();
}
async refreshToken() {
if(this.geminiApiService.isExpiryDateNear()===true){
console.log(`[Gemini] Expiry date is near, refreshing token...`);
return this.geminiApiService.initializeAuth(true);
}
return Promise.resolve();
}
}
// OpenAI API 服务适配器
@ -102,6 +118,11 @@ export class OpenAIApiServiceAdapter extends ApiServiceAdapter {
// The adapter now returns the native model list from the underlying service.
return this.openAIApiService.listModels();
}
async refreshToken() {
// OpenAI API keys are typically static and do not require refreshing.
return Promise.resolve();
}
}
// Claude API 服务适配器
@ -126,6 +147,10 @@ export class ClaudeApiServiceAdapter extends ApiServiceAdapter {
// The adapter now returns the native model list from the underlying service.
return this.claudeApiService.listModels();
}
async refreshToken() {
return Promise.resolve();
}
}
// Kiro API 服务适配器
@ -153,10 +178,18 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
// Returns the native model list from the Kiro service
return this.kiroApiService.listModels();
}
async refreshToken() {
if(this.kiroApiService.isExpiryDateNear()===true){
console.log(`[Kiro] Expiry date is near, refreshing token...`);
return this.kiroApiService.initializeAuth(true);
}
return Promise.resolve();
}
}
// 用于存储服务适配器单例的映射
const serviceInstances = {};
export const serviceInstances = {};
// 服务适配器工厂
export function getServiceAdapter(config) {

View file

@ -94,6 +94,10 @@
* --system-prompt-mode <mode> 系统提示模式 / System prompt mode: overwrite or append (default: overwrite)
* --log-prompts <mode> 提示日志模式 / Prompt logging mode: console, file, or none (default: none)
* --prompt-log-base-name <name> 提示日志文件基础名称 / Base name for prompt log files (default: prompt_log)
* --request-max-retries <number> API 请求失败时自动重试的最大次数 / Max retries for API requests on failure (default: 3)
* --request-base-delay <number> 自动重试之间的基础延迟时间毫秒每次重试后延迟会增加 / Base delay in milliseconds between retries, increases with each retry (default: 1000)
* --cron-near-minutes <number> OAuth 令牌刷新任务计划的间隔时间分钟 / Interval for OAuth token refresh task in minutes (default: 15)
* --cron-refresh-token <boolean> 是否开启 OAuth 令牌自动刷新任务 / Whether to enable automatic OAuth token refresh task (default: true)
*
*/
@ -105,7 +109,7 @@ import { promises as pfs } from 'fs';
import 'dotenv/config'; // Import dotenv and configure it
import deepmerge from 'deepmerge';
import { getServiceAdapter } from './adapter.js';
import { getServiceAdapter, serviceInstances} from './adapter.js';
import {
INPUT_SYSTEM_PROMPT_FILE,
API_ACTIONS,
@ -155,7 +159,9 @@ async function initializeConfig(args = process.argv.slice(2), configFilePath = '
PROMPT_LOG_BASE_NAME: "prompt_log",
PROMPT_LOG_MODE: "none",
REQUEST_MAX_RETRIES: 3,
REQUEST_BASE_DELAY: 1000
REQUEST_BASE_DELAY: 1000,
CRON_NEAR_MINUTES: 15,
CRON_REFRESH_TOKEN: true
};
console.log('[Config] Using default configuration.');
}
@ -293,6 +299,20 @@ async function initializeConfig(args = process.argv.slice(2), configFilePath = '
} else {
console.warn(`[Config Warning] --kiro-oauth-creds-file flag requires a value.`);
}
} else if (args[i] === '--cron-near-minutes') {
if (i + 1 < args.length) {
currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10);
i++;
} else {
console.warn(`[Config Warning] --cron-near-minutes flag requires a value.`);
}
} else if (args[i] === '--cron-refresh-token') {
if (i + 1 < args.length) {
currentConfig.CRON_REFRESH_TOKEN = args[i + 1].toLowerCase() === 'true';
i++;
} else {
console.warn(`[Config Warning] --cron-refresh-token flag requires a value.`);
}
}
}
@ -346,16 +366,17 @@ async function getSystemPromptFileContent(filePath) {
}
}
async function initApiService(config) { // Make getApiService exportable and accept config
async function initApiService(config) {
// Initialize all known service adapters at startup
for (const provider of Object.values(MODEL_PROVIDER)) {
try {
console.log(`[Initialization] Initializing service adapter for ${provider}...`);
getServiceAdapter({ ...config, MODEL_PROVIDER: provider });
getServiceAdapter({ ...config, MODEL_PROVIDER: provider }); // This call populates serviceInstances
} catch (error) {
console.warn(`[Initialization Warning] Failed to initialize service adapter for ${provider}: ${error.message}`);
}
}
return serviceInstances; // Return the collection of initialized service instances
}
async function getApiService(config) {
@ -475,11 +496,25 @@ function createRequestHandler(config) {
// --- Server Initialization ---
async function startServer() {
await initializeConfig(); // Initialize CONFIG globally
await initApiService(CONFIG); // Get service instance with the initialized CONFIG
const services = await initApiService(CONFIG); // Get service instance with the initialized CONFIG
const requestHandlerInstance = createRequestHandler(CONFIG); // Create request handler with CONFIG and service
const server = http.createServer(requestHandlerInstance);
// 定义心跳和令牌刷新函数
const heartbeatAndRefreshToken = async () => {
console.log(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`);
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
for (const providerKey in services) {
const serviceAdapter = services[providerKey];
try {
await serviceAdapter.refreshToken();
console.log(`[Token Refresh] Refreshed token for ${providerKey}`);
} catch (error) {
console.error(`[Token Refresh Error] Failed to refresh token for ${providerKey}: ${error.message}`);
}
}
};
const server = http.createServer(requestHandlerInstance);
server.listen(CONFIG.SERVER_PORT, CONFIG.HOST, () => {
console.log(`--- Unified API Server Configuration ---`);
console.log(` Model Provider: ${CONFIG.MODEL_PROVIDER}`);
@ -508,6 +543,13 @@ async function startServer() {
console.log(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`);
console.log(` • Claude-compatible: /v1/messages`);
console.log(` • Health check: /health`);
if (CONFIG.CRON_REFRESH_TOKEN) {
console.log(` • Cron Near Minutes: ${CONFIG.CRON_NEAR_MINUTES}`);
console.log(` • Cron Refresh Token: ${CONFIG.CRON_REFRESH_TOKEN}`);
// 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新
setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000);
}
});
return server; // Return the server instance for testing purposes
}

View file

@ -356,6 +356,7 @@ async initializeAuth(forceRefresh = false) {
const filePath = path.join(dirPath, file);
const credentials = await loadCredentialsFromFile(filePath);
if (credentials) {
credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded credentials from ${file}`);
}
@ -1062,4 +1063,22 @@ async initializeAuth(forceRefresh = false) {
return { models: models };
}
/**
* Checks if the given expiresAt timestamp is within 10 minutes from now.
* @returns {boolean} - True if expiresAt is less than 10 minutes from now, false otherwise.
*/
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();
} catch (error) {
console.error(`[Kiro] Error checking expiry date: ${this.expiresAt}, Error: ${error.message}`);
return false; // Treat as expired if parsing fails
}
}
}

View file

@ -324,4 +324,20 @@ export class GeminiApiService {
yield toGeminiApiResponse(chunk.response);
}
}
/**
* Checks if the given expiry date is within the next 10 minutes from now.
* @returns {boolean} True if the expiry date is within the next 10 minutes, false otherwise.
*/
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);
} catch (error) {
console.error(`[Gemini] Error checking expiry date: ${error.message}`);
return false;
}
}
}