diff --git a/src/api-server.js b/src/api-server.js
index 55096e9..fe33f71 100644
--- a/src/api-server.js
+++ b/src/api-server.js
@@ -80,7 +80,7 @@
* --host
服务器监听地址 / Server listening address (default: localhost)
* --port 服务器监听端口 / Server listening port (default: 3000)
* --api-key 身份验证所需的 API 密钥 / Required API key for authentication (default: 123456)
- * --model-provider AI 模型提供商 / AI model provider: openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth
+ * --model-provider AI 模型提供商 / AI model provider: openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth
* --openai-api-key OpenAI API 密钥 / OpenAI API key (for openai-custom provider)
* --openai-base-url OpenAI API 基础 URL / OpenAI API base URL (for openai-custom provider)
* --claude-api-key Claude API 密钥 / Claude API key (for claude-custom provider)
@@ -127,6 +127,47 @@ import {
let CONFIG = {}; // Make CONFIG exportable
let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable
+const ALL_MODEL_PROVIDERS = Object.values(MODEL_PROVIDER);
+
+function normalizeConfiguredProviders(config) {
+ const fallbackProvider = MODEL_PROVIDER.GEMINI_CLI;
+ const dedupedProviders = [];
+
+ const addProvider = (value) => {
+ if (typeof value !== 'string') {
+ return;
+ }
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return;
+ }
+ const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase());
+ if (!matched) {
+ console.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`);
+ return;
+ }
+ if (!dedupedProviders.includes(matched)) {
+ dedupedProviders.push(matched);
+ }
+ };
+
+ const rawValue = config.MODEL_PROVIDER;
+ if (Array.isArray(rawValue)) {
+ rawValue.forEach((entry) => addProvider(typeof entry === 'string' ? entry : String(entry)));
+ } else if (typeof rawValue === 'string') {
+ rawValue.split(',').forEach(addProvider);
+ } else if (rawValue != null) {
+ addProvider(String(rawValue));
+ }
+
+ if (dedupedProviders.length === 0) {
+ dedupedProviders.push(fallbackProvider);
+ }
+
+ config.DEFAULT_MODEL_PROVIDERS = dedupedProviders;
+ config.MODEL_PROVIDER = dedupedProviders[0];
+}
+
/**
* Initializes the server configuration from config.json and command-line arguments.
* @param {string[]} args - Command-line arguments.
@@ -335,6 +376,8 @@ async function initializeConfig(args = process.argv.slice(2), configFilePath = '
}
}
+ normalizeConfiguredProviders(currentConfig);
+
if (!currentConfig.SYSTEM_PROMPT_FILE_PATH) {
currentConfig.SYSTEM_PROMPT_FILE_PATH = INPUT_SYSTEM_PROMPT_FILE;
}
@@ -412,23 +455,75 @@ async function initApiService(config) {
console.log('[Initialization] No provider pools configured. Using single provider mode.');
}
- // Initialize all known service adapters at startup
- // 当存在号池时,这里不再提前初始化所有 provider 的实例,而是按需从号池中选择和初始化
- // 而是通过 providerPoolManager.selectProvider 来动态选择配置并初始化服务
- for (const provider of Object.values(MODEL_PROVIDER)) {
- if (!config.providerPools || !config.providerPools[provider] || config.providerPools[provider].length === 0) {
- try {
- // 对于没有配置号池的提供者,仍然按原来的方式初始化一个单例
- console.log(`[Initialization] Initializing single service adapter for ${provider}...`);
- getServiceAdapter({ ...config, MODEL_PROVIDER: provider }); // This call populates serviceInstances
- } catch (error) {
- console.warn(`[Initialization Warning] Failed to initialize single service adapter for ${provider}: ${error.message}`);
- }
+ // Initialize configured service adapters at startup
+ // 对于未纳入号池的提供者,提前初始化以避免首个请求的额外延迟
+ const providersToInit = new Set();
+ if (Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
+ config.DEFAULT_MODEL_PROVIDERS.forEach((provider) => providersToInit.add(provider));
+ }
+ if (config.providerPools) {
+ Object.keys(config.providerPools).forEach((provider) => providersToInit.add(provider));
+ }
+ if (providersToInit.size === 0) {
+ ALL_MODEL_PROVIDERS.forEach((provider) => providersToInit.add(provider));
+ }
+
+ for (const provider of providersToInit) {
+ if (!ALL_MODEL_PROVIDERS.includes(provider)) {
+ console.warn(`[Initialization Warning] Skipping unknown model provider '${provider}' during adapter initialization.`);
+ continue;
+ }
+ if (config.providerPools && config.providerPools[provider] && config.providerPools[provider].length > 0) {
+ // 由号池管理器负责按需初始化
+ continue;
+ }
+ try {
+ console.log(`[Initialization] Initializing single service adapter for ${provider}...`);
+ getServiceAdapter({ ...config, MODEL_PROVIDER: provider });
+ } catch (error) {
+ console.warn(`[Initialization Warning] Failed to initialize single service adapter for ${provider}: ${error.message}`);
}
}
return serviceInstances; // Return the collection of initialized service instances
}
+function logProviderSpecificDetails(provider, config) {
+ switch (provider) {
+ case MODEL_PROVIDER.OPENAI_CUSTOM:
+ console.log(` [openai-custom] API Key: ${config.OPENAI_API_KEY ? '******' : 'Not Set'}`);
+ console.log(` [openai-custom] Base URL: ${config.OPENAI_BASE_URL || 'Default'}`);
+ break;
+ case MODEL_PROVIDER.CLAUDE_CUSTOM:
+ console.log(` [claude-custom] API Key: ${config.CLAUDE_API_KEY ? '******' : 'Not Set'}`);
+ console.log(` [claude-custom] Base URL: ${config.CLAUDE_BASE_URL || 'Default'}`);
+ break;
+ case MODEL_PROVIDER.GEMINI_CLI:
+ if (config.GEMINI_OAUTH_CREDS_FILE_PATH) {
+ console.log(` [gemini-cli-oauth] OAuth Creds File Path: ${config.GEMINI_OAUTH_CREDS_FILE_PATH}`);
+ } else if (config.GEMINI_OAUTH_CREDS_BASE64) {
+ console.log(` [gemini-cli-oauth] OAuth Creds Source: Provided via Base64 string`);
+ } else {
+ console.log(` [gemini-cli-oauth] OAuth Creds: Default discovery`);
+ }
+ console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`);
+ break;
+ case MODEL_PROVIDER.KIRO_API:
+ if (config.KIRO_OAUTH_CREDS_FILE_PATH) {
+ console.log(` [claude-kiro-oauth] OAuth Creds File Path: ${config.KIRO_OAUTH_CREDS_FILE_PATH}`);
+ } else if (config.KIRO_OAUTH_CREDS_BASE64) {
+ console.log(` [claude-kiro-oauth] OAuth Creds Source: Provided via Base64 string`);
+ } else {
+ console.log(` [claude-kiro-oauth] OAuth Creds: Default`);
+ }
+ break;
+ case MODEL_PROVIDER.QWEN_API:
+ console.log(` [openai-qwen-oauth] OAuth Creds File Path: ${config.QWEN_OAUTH_CREDS_FILE_PATH || 'Default'}`);
+ break;
+ default:
+ console.log(` [${provider}] Provider initialized.`);
+ }
+}
+
async function getApiService(config) {
let serviceConfig = config;
if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) {
@@ -606,21 +701,15 @@ async function startServer() {
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}`);
- if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.OPENAI_CUSTOM) {
- console.log(` OpenAI API Key: ${CONFIG.OPENAI_API_KEY ? '******' : 'Not Set'}`);
- console.log(` OpenAI Base URL: ${CONFIG.OPENAI_BASE_URL}`);
- } else if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.CLAUDE_CUSTOM) {
- console.log(` Claude API Key: ${CONFIG.CLAUDE_API_KEY ? '******' : 'Not Set'}`);
- console.log(` Claude Base URL: ${CONFIG.CLAUDE_BASE_URL}`);
- } else if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.GEMINI_CLI) {
- console.log(` Gemini OAuth Creds File Path: ${CONFIG.GEMINI_OAUTH_CREDS_FILE_PATH || 'Default'}`);
- console.log(` Project ID: ${CONFIG.PROJECT_ID || 'Auto-discovered'}`);
- } else if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.KIRO_API) {
- console.log(` Kiro OAuth Creds File Path: ${CONFIG.KIRO_OAUTH_CREDS_FILE_PATH || 'Default'}`);
- } else if (CONFIG.MODEL_PROVIDER === MODEL_PROVIDER.QWEN_API) {
- console.log(` Qwen OAuth Creds File Path: ${CONFIG.QWEN_OAUTH_CREDS_FILE_PATH || 'Default'}`);
- }
+ const configuredProviders = Array.isArray(CONFIG.DEFAULT_MODEL_PROVIDERS) && CONFIG.DEFAULT_MODEL_PROVIDERS.length > 0
+ ? CONFIG.DEFAULT_MODEL_PROVIDERS
+ : [CONFIG.MODEL_PROVIDER];
+ const uniqueProviders = [...new Set(configuredProviders)];
+ console.log(` Primary Model Provider: ${CONFIG.MODEL_PROVIDER}`);
+ if (uniqueProviders.length > 1) {
+ console.log(` Additional Model Providers: ${uniqueProviders.slice(1).join(', ')}`);
+ }
+ uniqueProviders.forEach((provider) => logProviderSpecificDetails(provider, CONFIG));
console.log(` System Prompt File: ${CONFIG.SYSTEM_PROMPT_FILE_PATH || 'Default'}`);
console.log(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`);
console.log(` Host: ${CONFIG.HOST}`);
diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js
index 00eb823..5347149 100644
--- a/src/openai/qwen-core.js
+++ b/src/openai/qwen-core.js
@@ -119,6 +119,7 @@ export class QwenApiService {
this.qwenClient = new QwenOAuth2Client();
this.sharedManager = SharedTokenManager.getInstance();
this.currentAxiosInstance = null;
+ this.tokenManagerOptions = { credentialFilePath: this._getQwenCachedCredentialPath() };
}
async initialize() {
@@ -140,7 +141,11 @@ export class QwenApiService {
async _initializeAuth(forceRefresh = false) {
try {
- const credentials = await this.sharedManager.getValidCredentials(this.qwenClient, forceRefresh);
+ const credentials = await this.sharedManager.getValidCredentials(
+ this.qwenClient,
+ forceRefresh,
+ this.tokenManagerOptions,
+ );
// console.log('credentials', credentials);
this.qwenClient.setCredentials(credentials);
} catch (error) {
@@ -162,14 +167,13 @@ export class QwenApiService {
}
}
+ // If cached credentials are present and still valid, use them directly.
if (await this._loadCachedQwenCredentials(this.qwenClient)) {
- const result = await this._authWithQwenDeviceFlow(this.qwenClient, this.config);
- if (!result.success) {
- throw new Error('Qwen OAuth authentication failed');
- }
+ console.log('[Qwen] Using cached OAuth credentials.');
return;
}
+ // Otherwise, run device authorization flow to obtain fresh credentials.
const result = await this._authWithQwenDeviceFlow(this.qwenClient, this.config);
if (!result.success) {
if (result.reason === 'timeout') {
@@ -310,7 +314,7 @@ export class QwenApiService {
_getQwenCachedCredentialPath() {
if (this.config && this.config.QWEN_OAUTH_CREDS_FILE_PATH) {
- return this.config.QWEN_OAUTH_CREDS_FILE_PATH;
+ return path.resolve(this.config.QWEN_OAUTH_CREDS_FILE_PATH);
}
return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME);
}
@@ -321,8 +325,10 @@ export class QwenApiService {
const creds = await fs.readFile(keyFile, 'utf-8');
const credentials = JSON.parse(creds);
client.setCredentials(credentials);
- const { token } = await client.getAccessToken();
- return !!token;
+ // Consider credentials usable only if access_token exists and not near expiry
+ const hasToken = !!credentials?.access_token;
+ const notExpired = !!credentials?.expiry_date && (Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS);
+ return hasToken && notExpired;
} catch (_) {
return false;
}
@@ -368,7 +374,11 @@ export class QwenApiService {
async getValidToken() {
try {
- const credentials = await this.sharedManager.getValidCredentials(this.qwenClient);
+ const credentials = await this.sharedManager.getValidCredentials(
+ this.qwenClient,
+ false,
+ this.tokenManagerOptions,
+ );
if (!credentials.access_token) throw new Error('No access token available');
return {
token: credentials.access_token,
@@ -464,7 +474,11 @@ export class QwenApiService {
if (this.isAuthError(error) && retryCount === 0) {
console.warn(`[QwenApiService] Auth error (${status}). Refreshing token...`);
try {
- await this.sharedManager.getValidCredentials(this.qwenClient, true);
+ await this.sharedManager.getValidCredentials(
+ this.qwenClient,
+ true,
+ this.tokenManagerOptions,
+ );
return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1);
} catch (refreshError) {
console.error(`[QwenApiService] Token refresh failed:`, refreshError);
@@ -539,13 +553,13 @@ export class QwenApiService {
class SharedTokenManager {
static instance = null;
- memoryCache = { credentials: null, fileModTime: 0, lastCheck: 0 };
- refreshPromise = null;
- cleanupHandlersRegistered = false;
- cleanupFunction = null;
- lockConfig = DEFAULT_LOCK_CONFIG;
constructor() {
+ this.contexts = new Map();
+ this.lockPaths = new Set();
+ this.cleanupHandlersRegistered = false;
+ this.cleanupFunction = null;
+ this.sigintHandler = null;
this.registerCleanupHandlers();
}
@@ -556,13 +570,49 @@ class SharedTokenManager {
return SharedTokenManager.instance;
}
+ getContext(options = {}) {
+ const credentialFilePath = this.resolveCredentialFilePath(options.credentialFilePath);
+ const lockFilePath = this.resolveLockFilePath(credentialFilePath, options.lockFilePath);
+ let context = this.contexts.get(credentialFilePath);
+ if (!context) {
+ context = {
+ credentialFilePath,
+ lockFilePath,
+ lockConfig: options.lockConfig || DEFAULT_LOCK_CONFIG,
+ memoryCache: { credentials: null, fileModTime: 0, lastCheck: 0 },
+ refreshPromise: null,
+ };
+ this.contexts.set(credentialFilePath, context);
+ this.lockPaths.add(lockFilePath);
+ } else if (options.lockConfig) {
+ context.lockConfig = options.lockConfig;
+ }
+ return context;
+ }
+
+ resolveCredentialFilePath(customPath) {
+ if (customPath) {
+ return path.resolve(customPath);
+ }
+ return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME);
+ }
+
+ resolveLockFilePath(credentialFilePath, customLockPath) {
+ if (customLockPath) {
+ return path.resolve(customLockPath);
+ }
+ return `${credentialFilePath}.lock`;
+ }
+
registerCleanupHandlers() {
if (this.cleanupHandlersRegistered) return;
this.cleanupFunction = () => {
- try { unlinkSync(this.getLockFilePath()); } catch (_error) { /* Ignore */ }
+ for (const lockPath of this.lockPaths) {
+ try { unlinkSync(lockPath); } catch (_error) { /* ignore */ }
+ }
};
this.sigintHandler = () => {
- try { unlinkSync(this.getLockFilePath()); } catch (_error) { /* Ignore */ }
+ this.cleanupFunction();
process.exit(0);
};
process.on('exit', this.cleanupFunction);
@@ -570,70 +620,81 @@ class SharedTokenManager {
this.cleanupHandlersRegistered = true;
}
- async getValidCredentials(qwenClient, forceRefresh = false) {
+ async getValidCredentials(qwenClient, forceRefresh = false, options = {}) {
+ const context = this.getContext(options);
try {
- await this.checkAndReloadIfNeeded();
- if (!forceRefresh && this.memoryCache.credentials && this.isTokenValid(this.memoryCache.credentials)) {
- return this.memoryCache.credentials;
+ await this.checkAndReloadIfNeeded(context);
+ if (!forceRefresh && context.memoryCache.credentials && this.isTokenValid(context.memoryCache.credentials)) {
+ return context.memoryCache.credentials;
}
- if (this.refreshPromise) return this.refreshPromise;
-
- qwenClient.setCredentials(this.memoryCache.credentials);
- this.refreshPromise = this.performTokenRefresh(qwenClient, forceRefresh);
- const credentials = await this.refreshPromise;
- this.refreshPromise = null;
+ if (context.refreshPromise) {
+ return context.refreshPromise;
+ }
+
+ qwenClient.setCredentials(context.memoryCache.credentials);
+ context.refreshPromise = this.performTokenRefresh(context, qwenClient, forceRefresh);
+ const credentials = await context.refreshPromise;
+ context.refreshPromise = null;
return credentials;
} catch (error) {
- this.refreshPromise = null;
+ context.refreshPromise = null;
if (error instanceof TokenManagerError) throw error;
- throw new TokenManagerError(TokenError.REFRESH_FAILED,`Failed to get valid credentials: ${error.message}`, error);
+ throw new TokenManagerError(
+ TokenError.REFRESH_FAILED,
+ `Failed to get valid credentials: ${error.message}`,
+ error,
+ );
}
}
- async checkAndReloadIfNeeded() {
+ async checkAndReloadIfNeeded(context) {
const now = Date.now();
- if (now - this.memoryCache.lastCheck < CACHE_CHECK_INTERVAL_MS) return;
- this.memoryCache.lastCheck = now;
+ if (now - context.memoryCache.lastCheck < CACHE_CHECK_INTERVAL_MS) return;
+ context.memoryCache.lastCheck = now;
try {
- const stats = await fs.stat(this.getCredentialFilePath());
- if (stats.mtimeMs > this.memoryCache.fileModTime) {
- await this.reloadCredentialsFromFile();
- this.memoryCache.fileModTime = stats.mtimeMs;
+ const stats = await fs.stat(context.credentialFilePath);
+ if (stats.mtimeMs > context.memoryCache.fileModTime) {
+ await this.reloadCredentialsFromFile(context);
+ context.memoryCache.fileModTime = stats.mtimeMs;
}
} catch (error) {
if (error.code !== 'ENOENT') {
- this.memoryCache.credentials = null;
- this.memoryCache.fileModTime = 0;
- throw new TokenManagerError(TokenError.FILE_ACCESS_ERROR, `Failed to access credentials file: ${error.message}`, error);
+ context.memoryCache.credentials = null;
+ context.memoryCache.fileModTime = 0;
+ throw new TokenManagerError(
+ TokenError.FILE_ACCESS_ERROR,
+ `Failed to access credentials file: ${error.message}`,
+ error,
+ );
}
- this.memoryCache.fileModTime = 0;
+ context.memoryCache.credentials = null;
+ context.memoryCache.fileModTime = 0;
}
}
- async reloadCredentialsFromFile() {
+ async reloadCredentialsFromFile(context) {
try {
- const content = await fs.readFile(this.getCredentialFilePath(), 'utf-8');
- this.memoryCache.credentials = JSON.parse(content);
- } catch (error) {
- this.memoryCache.credentials = null;
+ const content = await fs.readFile(context.credentialFilePath, 'utf-8');
+ context.memoryCache.credentials = JSON.parse(content);
+ } catch (_error) {
+ context.memoryCache.credentials = null;
}
}
- async performTokenRefresh(qwenClient, forceRefresh = false) {
- const lockPath = this.getLockFilePath();
+ async performTokenRefresh(context, qwenClient, forceRefresh = false) {
try {
- const currentCredentials = qwenClient.getCredentials();
- if (!currentCredentials.refresh_token) {
+ const currentCredentials = qwenClient.getCredentials() || context.memoryCache.credentials;
+ if (!currentCredentials || !currentCredentials.refresh_token) {
throw new TokenManagerError(TokenError.NO_REFRESH_TOKEN, 'No refresh token available');
}
- await this.acquireLock(lockPath);
- await this.checkAndReloadIfNeeded();
+ await this.acquireLock(context);
+ await this.checkAndReloadIfNeeded(context);
- if (!forceRefresh && this.memoryCache.credentials && this.isTokenValid(this.memoryCache.credentials)) {
- qwenClient.setCredentials(this.memoryCache.credentials);
- return this.memoryCache.credentials;
+ if (!forceRefresh && context.memoryCache.credentials && this.isTokenValid(context.memoryCache.credentials)) {
+ qwenClient.setCredentials(context.memoryCache.credentials);
+ return context.memoryCache.credentials;
}
const response = await qwenClient.refreshAccessToken();
@@ -651,67 +712,75 @@ class SharedTokenManager {
expiry_date: Date.now() + response.expires_in * 1000,
};
- this.memoryCache.credentials = credentials;
+ context.memoryCache.credentials = credentials;
qwenClient.setCredentials(credentials);
- await this.saveCredentialsToFile(credentials);
+ await this.saveCredentialsToFile(context, credentials);
console.log('[Qwen Auth] Token refresh response: ok');
return credentials;
} catch (error) {
if (error instanceof TokenManagerError) throw error;
- throw new TokenManagerError(TokenError.REFRESH_FAILED, `Unexpected error during token refresh: ${error.message}`, error);
+ // If refresh token is invalid/expired, remove the corresponding credential file for this context
+ if (error && (error.status === 400 || /expired|invalid/i.test(error.message || ''))) {
+ try { await fs.unlink(context.credentialFilePath); } catch (_) { /* ignore */ }
+ }
+ throw new TokenManagerError(
+ TokenError.REFRESH_FAILED,
+ `Unexpected error during token refresh: ${error.message}`,
+ error,
+ );
} finally {
- await this.releaseLock(lockPath);
+ await this.releaseLock(context);
}
}
- async saveCredentialsToFile(credentials) {
- const filePath = this.getCredentialFilePath();
- await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
- await fs.writeFile(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
- const stats = await fs.stat(filePath);
- this.memoryCache.fileModTime = stats.mtimeMs;
+ async saveCredentialsToFile(context, credentials) {
+ await fs.mkdir(path.dirname(context.credentialFilePath), { recursive: true, mode: 0o700 });
+ await fs.writeFile(context.credentialFilePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
+ const stats = await fs.stat(context.credentialFilePath);
+ context.memoryCache.fileModTime = stats.mtimeMs;
}
isTokenValid(credentials) {
return credentials?.expiry_date && Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS;
}
- getCredentialFilePath() {
- return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME);
- }
-
- getLockFilePath() {
- return path.join(os.homedir(), QWEN_DIR, QWEN_LOCK_FILENAME);
- }
-
- async acquireLock(lockPath) {
- const { maxAttempts, attemptInterval } = this.lockConfig;
+ async acquireLock(context) {
+ const { maxAttempts, attemptInterval } = context.lockConfig || DEFAULT_LOCK_CONFIG;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
- await fs.writeFile(lockPath, randomUUID(), { flag: 'wx' });
+ await fs.writeFile(context.lockFilePath, randomUUID(), { flag: 'wx' });
return;
} catch (error) {
if (error.code === 'EEXIST') {
try {
- const stats = await fs.stat(lockPath);
+ const stats = await fs.stat(context.lockFilePath);
if (Date.now() - stats.mtimeMs > LOCK_TIMEOUT_MS) {
- await fs.unlink(lockPath);
+ await fs.unlink(context.lockFilePath);
continue;
}
- } catch (statError) { /* ignore */ }
+ } catch (_statError) { /* ignore */ }
await new Promise(resolve => setTimeout(resolve, attemptInterval));
} else {
- throw new TokenManagerError(TokenError.FILE_ACCESS_ERROR,`Failed to create lock file: ${error.message}`,error);
+ throw new TokenManagerError(
+ TokenError.FILE_ACCESS_ERROR,
+ `Failed to create lock file: ${error.message}`,
+ error,
+ );
}
}
}
throw new TokenManagerError(TokenError.LOCK_TIMEOUT, 'Lock acquisition timeout');
}
- async releaseLock(lockPath) {
- try { await fs.unlink(lockPath); }
- catch (error) { if (error.code !== 'ENOENT') console.warn(`Failed to release lock: ${error.message}`);}
+ async releaseLock(context) {
+ try {
+ await fs.unlink(context.lockFilePath);
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ console.warn(`Failed to release lock: ${error.message}`);
+ }
+ }
}
}
@@ -738,8 +807,9 @@ class QwenOAuth2Client {
});
if (!response.ok) {
if (response.status === 400) {
- await fs.unlink(path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME)).catch(() => {});
- throw new Error("Refresh token expired or invalid.");
+ const err = new Error("Refresh token expired or invalid.");
+ err.status = 400;
+ throw err;
}
throw new Error(`Token refresh failed: ${response.status}`);
}
@@ -788,4 +858,4 @@ class QwenOAuth2Client {
}
return await response.json();
}
-}
\ No newline at end of file
+}