diff --git a/README-JA.md b/README-JA.md index 529829a..d251d06 100644 --- a/README-JA.md +++ b/README-JA.md @@ -57,6 +57,16 @@ LingtrueAPIによる本プロジェクトへのスポンサーに感謝します!LingtrueAPIは世界的な大規模言語モデルAPI中継プラットフォームであり、Claude opus 4.6、GPT 5.4、Gemini 3.1 proなど各種モデルのAPI呼び出しサービスを提供しています。低コスト、高安定性で世界中のAI機能に接続し、生産性を最大化することを目指しています。LingtrueAPIは本ソフトウェアユーザー向けに特別優遇を提供しています。このリンクから登録し、初回チャージ時に「LingtrueAPI」のクーポンコードを入力すると、10%オフで利用できます。 +
+
+
diff --git a/README-ZH.md b/README-ZH.md
index 048bf2f..d4be818 100644
--- a/README-ZH.md
+++ b/README-ZH.md
@@ -56,6 +56,16 @@
感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude opus 4.6、GPT 5.4、Gemini 3.1 pro等多种模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力,最大化生产效率。LingtrueAPI为本软件用户提供了特别优惠:通过此链接注册并在首次充值时输入 LingtrueAPI 优惠码即可享受 9折优惠。
+
+
diff --git a/README.md b/README.md
index 3aef936..8629d9c 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,16 @@
Thanks to LingtrueAPI for its sponsorship of this project! LingtrueAPI is a global large-model API intermediary service platform that offers API calling services for various models such as Claude opus 4.6, GPT 5.4, and Gemini 3.1 pro. It is committed to enabling users to connect to global AI capabilities at low cost and with high stability, maximizing production efficiency. LingtrueAPI provides special discounts for users of this software: register using this link and enter the LingtrueAPI promo code when making the first recharge to enjoy a 10% discount.
+
+
diff --git a/VERSION b/VERSION
index 5a5ee51..e464374 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.12.3
\ No newline at end of file
+2.12.6
diff --git a/src/providers/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js
index e1da3dc..905a344 100644
--- a/src/providers/gemini/antigravity-core.js
+++ b/src/providers/gemini/antigravity-core.js
@@ -40,7 +40,6 @@ const ANTIGRAVITY_SYSTEM_PROMPT = `You are Antigravity, a powerful agentic AI co
// Thinking 配置相关常量
const DEFAULT_THINKING_MIN = 1024;
const DEFAULT_THINKING_MAX = 100000;
-const FALLBACK_THINKING_SIGNATURE = "skip_thought_signature_validator_fallback";
// 获取 Antigravity 模型列表
const ANTIGRAVITY_MODELS = getProviderModels(MODEL_PROVIDER.ANTIGRAVITY);
@@ -676,23 +675,6 @@ function ensureRolesInContents(requestBody, modelName) {
content.role = 'user';
}
- // [FIX] 修复历史记录中的思考块,确保有签名 (messages.1.content.0.thinking.signature 报错修复)
- if (content.parts && Array.isArray(content.parts)) {
- content.parts.forEach(part => {
- if (part && part.thought === true) {
- if (!part.thoughtSignature && !part.thought_signature) {
- part.thoughtSignature = FALLBACK_THINKING_SIGNATURE;
- }
-
- // [FIX] 额外增加一个 'thinking' 对象以适配某些 Antigravity 内部验证逻辑
- if (!part.thinking) {
- part.thinking = {
- signature: part.thoughtSignature || part.thought_signature || FALLBACK_THINKING_SIGNATURE
- };
- }
- }
- });
- }
});
}
diff --git a/src/providers/gemini/gemini-core.js b/src/providers/gemini/gemini-core.js
index 57abb77..c57fd49 100644
--- a/src/providers/gemini/gemini-core.js
+++ b/src/providers/gemini/gemini-core.js
@@ -287,8 +287,7 @@ export class GeminiApiService {
maxFreeSockets: 5,
timeout: 120000,
});
-
- this.authClient = new OAuth2Client(oauth2Options);
+
this.availableModels = [];
this.isInitialized = false;
@@ -327,6 +326,8 @@ export class GeminiApiService {
logger.info('[Gemini] Using HTTP agent for OAuth2Client');
}
}
+
+ this.authClient = new OAuth2Client(oauth2Options);
}
async initialize() {
diff --git a/src/providers/openai/qwen-core.js b/src/providers/openai/qwen-core.js
index f84a679..919a42e 100644
--- a/src/providers/openai/qwen-core.js
+++ b/src/providers/openai/qwen-core.js
@@ -50,36 +50,131 @@ export const qwenOAuth2Events = new EventEmitter();
// --- Helper Functions ---
-/**
- * Qwen 默认系统提示词
- */
-const QWEN_DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant. You are Qwen, a large language model trained by Alibaba.";
+// --- Rate Limiting & Quota ---
+const qwenRateLimiter = {
+ requests: new Map(), // authID -> timestamps[]
+};
+const QWEN_RATE_LIMIT_PER_MIN = 60;
+const QWEN_RATE_LIMIT_WINDOW_MS = 60 * 1000;
+const QWEN_QUOTA_CODES = new Set(['insufficient_quota', 'quota_exceeded']);
/**
- * 应用 Qwen 默认系统提示词逻辑
+ * 检查 Qwen 速率限制 (60 requests/min)
+ * @param {string} authID
+ * @returns {Error|null}
+ */
+function checkQwenRateLimit(authID) {
+ if (!authID) return null;
+ const now = Date.now();
+ const windowStart = now - QWEN_RATE_LIMIT_WINDOW_MS;
+
+ let timestamps = qwenRateLimiter.requests.get(authID) || [];
+ timestamps = timestamps.filter(ts => ts > windowStart);
+
+ if (timestamps.length >= QWEN_RATE_LIMIT_PER_MIN) {
+ const oldestInWindow = timestamps[0];
+ const retryAfterMs = oldestInWindow + QWEN_RATE_LIMIT_WINDOW_MS - now;
+ const retryAfterSec = Math.max(1, Math.ceil(retryAfterMs / 1000));
+ const error = new Error(`Qwen rate limit exceeded for ${authID.substring(0, 8)}, retry after ${retryAfterSec}s`);
+ error.status = 429;
+ error.data = { error: { code: "rate_limit_exceeded", message: error.message } };
+ error.retryAfter = retryAfterMs;
+ return error;
+ }
+
+ timestamps.push(now);
+ qwenRateLimiter.requests.set(authID, timestamps);
+ return null;
+}
+
+/**
+ * 检查是否为配额错误
+ */
+function isQwenQuotaError(body) {
+ if (!body || typeof body !== 'object') return false;
+ const error = body.error || {};
+ const code = (error.code || '').toLowerCase();
+ const type = (error.type || '').toLowerCase();
+ const message = (error.message || '').toLowerCase();
+ return QWEN_QUOTA_CODES.has(code) || QWEN_QUOTA_CODES.has(type) ||
+ /insufficient_quota|quota exceeded|free allocated quota exceeded/i.test(message);
+}
+
+/**
+ * 计算到北京时间次日凌晨的毫秒数
+ */
+function timeUntilNextDayBeijing() {
+ const now = new Date();
+ // UTC to Beijing (UTC+8)
+ const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000);
+ const beijingNow = new Date(utcTime + (3600000 * 8));
+ const tomorrow = new Date(beijingNow);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ tomorrow.setHours(0, 0, 0, 0);
+ return tomorrow.getTime() - beijingNow.getTime();
+}
+
+/**
+ * 确保 Qwen 系统消息在开头且唯一,合并多条系统消息并支持缓存控制
* @param {Object} requestBody - OpenAI 格式的请求体
* @returns {Object} 处理后的请求体
*/
-function applyQwenDefaultSystemPrompt(requestBody) {
+function ensureQwenSystemMessage(requestBody) {
if (!requestBody || !requestBody.messages || !Array.isArray(requestBody.messages)) {
return requestBody;
}
- // 检查是否已有系统提示词 (role 为 system 或 developer)
- const hasSystemPrompt = requestBody.messages.some(msg =>
- msg.role === 'system' || msg.role === 'developer'
- );
+ const isInjectedSystemPart = (part) => {
+ if (!part || typeof part !== 'object') return false;
+ if (part.type !== 'text') return false;
+ if (part.cache_control?.type !== 'ephemeral') return false;
+ return part.text === "" || part.text === "You are Qwen Code.";
+ };
- // 如果没有系统提示词,则在消息列表最前面插入默认提示词
- if (!hasSystemPrompt) {
- requestBody.messages.unshift({
- role: 'system',
- content: QWEN_DEFAULT_SYSTEM_PROMPT
- });
- logger.info('[Qwen Auth] 已应用默认系统提示词');
+ let systemParts = [];
+ // 注入默认系统提示词部分 (带缓存控制)
+ systemParts.push({
+ type: "text",
+ text: "You are Qwen Code.",
+ cache_control: { type: "ephemeral" }
+ });
+
+ const appendSystemContent = (content) => {
+ if (content === undefined || content === null) return;
+
+ if (Array.isArray(content)) {
+ for (const part of content) {
+ if (typeof part === 'string') {
+ systemParts.push({ type: 'text', text: part });
+ } else if (!isInjectedSystemPart(part)) {
+ systemParts.push(part);
+ }
+ }
+ } else if (typeof content === 'string') {
+ systemParts.push({ type: 'text', text: content });
+ } else if (typeof content === 'object') {
+ if (!isInjectedSystemPart(content)) {
+ systemParts.push(content);
+ }
+ }
+ };
+
+ const nonSystemMessages = [];
+ for (const msg of requestBody.messages) {
+ if (msg.role === 'system' || msg.role === 'developer') {
+ appendSystemContent(msg.content);
+ } else {
+ nonSystemMessages.push(msg);
+ }
}
- return requestBody;
+ return {
+ ...requestBody,
+ messages: [
+ { role: 'system', content: systemParts },
+ ...nonSystemMessages
+ ]
+ };
}
// 封装公共的 await fetch 方法
@@ -564,85 +659,87 @@ export class QwenApiService {
}
async callApiWithAuthAndRetry(endpoint, body, isStream = false, retryCount = 0) {
+ // 速率限制检查
+ if (this.uuid) {
+ const limitErr = checkQwenRateLimit(this.uuid);
+ if (limitErr) throw limitErr;
+ }
+
const maxRetries = (this.config && this.config.REQUEST_MAX_RETRIES) || 3;
const baseDelay = (this.config && this.config.REQUEST_BASE_DELAY) || 1000;
- const version = "0.10.1";
+ const version = "0.13.2";
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
- logger.info(`[QwenApiService] User-Agent: ${userAgent}`);
try {
const { token, endpoint: qwenBaseUrl } = await this.getValidToken();
- // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
- const httpAgent = new http.Agent({
- keepAlive: true,
- maxSockets: 100,
- maxFreeSockets: 5,
- timeout: 120000,
- });
- const httpsAgent = new https.Agent({
- keepAlive: true,
- maxSockets: 100,
- maxFreeSockets: 5,
- timeout: 120000,
- });
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ 'User-Agent': userAgent,
+ 'X-DashScope-UserAgent': userAgent,
+ 'X-Stainless-Runtime-Version': 'v22.17.0',
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Arch': process.arch === 'x64' ? 'x86_64' : process.arch,
+ 'X-Stainless-Package-Version': '5.11.0',
+ 'X-DashScope-CacheControl': 'enable',
+ 'X-DashScope-AuthType': 'qwen-oauth',
+ 'X-Stainless-Runtime': 'node',
+ 'Accept': isStream ? 'text/event-stream' : 'application/json',
+ };
const axiosConfig = {
baseURL: qwenBaseUrl,
- httpAgent,
- httpsAgent,
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`,
- 'X-DashScope-CacheControl': 'enable',
- 'X-DashScope-UserAgent': userAgent,
- 'X-DashScope-AuthType': 'qwen-oauth',
- },
+ headers,
+ // axios 默认不传 proxy 配置时会遵循环境变量,这里明确控制
+ proxy: this.useSystemProxy ? undefined : false,
};
- // 禁用系统代理
- if (!this.useSystemProxy) {
- axiosConfig.proxy = false;
- }
-
- // 配置自定义代理
+ // 配置自定义代理 (如果 config.json 中有定义)
configureAxiosProxy(axiosConfig, this.config, this.config.MODEL_PROVIDER || MODEL_PROVIDER.QWEN_API);
- this.currentAxiosInstance = axios.create(axiosConfig);
+ const instance = axios.create(axiosConfig);
- // Process message content before sending the request
- const processedBody = applyQwenDefaultSystemPrompt(body);
+ // 处理消息和模型
+ let processedBody = ensureQwenSystemMessage(body);
- // Check if model in body is in QWEN_MODEL_LIST, if not, use the first model's id
- if (processedBody.model && !QWEN_MODEL_LIST.some(model => model.id === processedBody.model)) {
- logger.warn(`[QwenApiService] Model '${processedBody.model}' not found. Using default model: '${QWEN_MODEL_LIST[0].id}'`);
- processedBody.model = QWEN_MODEL_LIST[0].id;
+ // 检查模型是否存在于列表中
+ if (processedBody.model && !QWEN_MODELS.includes(processedBody.model)) {
+ logger.warn(`[QwenApiService] Model '${processedBody.model}' not found in supported list. Using default: '${QWEN_MODELS[0]}'`);
+ processedBody.model = QWEN_MODELS[0];
}
- const defaultTools = [
- {
- "type": "function",
- "function": {
- "name": "ext"
- }
- }
- ];
+ // Qwen3 兼容性补丁:针对 Qwen3 "Poisoning" 问题优化工具注入
+ // 如果请求本身没有 tools,注入一个虚拟工具防止模型在流式响应中随机吐出 Token
+ const dummyTool = {
+ type: "function",
+ function: {
+ name: "ext",
+ description: "Internal extension tool"
+ }
+ };
- // Merge tools if requestBody already has tools defined
- const mergedTools = processedBody.tools ? [...defaultTools, ...processedBody.tools] : defaultTools;
+ if (processedBody.tools) {
+ processedBody.tools = [dummyTool, ...processedBody.tools];
+ } else {
+ processedBody.tools = [dummyTool];
+ }
- const requestBody = isStream ? { ...processedBody, stream: true, tools: mergedTools } : { ...processedBody, tools: mergedTools };
-
- const axiosRequestConfig = {
+ if (isStream) {
+ processedBody.stream = true;
+ processedBody.stream_options = { include_usage: true };
+ }
+
+ const requestConfig = {
method: 'post',
url: endpoint,
- data: requestBody,
+ data: processedBody,
...(isStream ? { responseType: 'stream' } : {})
};
- this._applySidecar(axiosRequestConfig);
+ this._applySidecar(requestConfig);
- const response = await this.currentAxiosInstance.request(axiosRequestConfig);
+ const response = await instance.request(requestConfig);
return response.data;
} catch (error) {
@@ -651,40 +748,38 @@ export class QwenApiService {
const errorCode = error.code;
const errorMessage = error.message || '';
- // 检查是否为可重试的网络错误
- const isNetworkError = isRetryableNetworkError(error);
-
- if (this.isAuthError(error) && retryCount === 0) {
- logger.warn(`[QwenApiService] Auth error (${status}). Triggering background refresh via PoolManager...`);
+ // 检查配额错误 -> 映射为 429 并设置冷却时间
+ if ((status === 403 || status === 429) && isQwenQuotaError(error.response?.data)) {
+ const cooldown = timeUntilNextDayBeijing();
+ logger.warn(`[QwenApiService] Daily quota exceeded (http ${status} -> 429), cooling down until tomorrow (~${Math.round(cooldown / 3600000)} hours)`);
+ error.status = 429;
+ error.retryAfter = cooldown;
- // 标记当前凭证为不健康(会自动进入刷新队列)
+ // 标记 unhealthy
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
- logger.info(`[Qwen] Marking credential ${this.uuid} as needs refresh. Reason: Auth Error ${status}`);
- poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.QWEN_API, {
- uuid: this.uuid
- });
+ poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.QWEN_API, { uuid: this.uuid });
+ }
+ throw error;
+ }
+
+ if (this.isAuthError(error) && retryCount === 0) {
+ logger.warn(`[QwenApiService] Auth error (${status}). Triggering background refresh...`);
+
+ const poolManager = getProviderPoolManager();
+ if (poolManager && this.uuid) {
+ poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.QWEN_API, { uuid: this.uuid });
error.credentialMarkedUnhealthy = true;
}
- // Mark error for credential switch without recording error count
error.shouldSwitchCredential = true;
error.skipErrorCount = true;
throw error;
}
- if ((status === 429 || (status >= 500 && status < 600)) && retryCount < maxRetries) {
+ if ((status === 429 || (status >= 500 && status < 600) || isRetryableNetworkError(error)) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
- logger.info(`[QwenApiService] Status ${status}. Retrying in ${delay}ms...`);
- await new Promise(resolve => setTimeout(resolve, delay));
- return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1);
- }
-
- // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff
- if (isNetworkError && retryCount < maxRetries) {
- const delay = baseDelay * Math.pow(2, retryCount);
- const errorIdentifier = errorCode || errorMessage.substring(0, 50);
- logger.info(`[QwenApiService] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
+ logger.info(`[QwenApiService] Request failed (${status || errorCode}). Retrying in ${delay}ms... (${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1);
}
@@ -733,7 +828,7 @@ export class QwenApiService {
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
logger.info(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`);
- poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, {
+ poolManager.markProviderNeedRefresh(this.config.MODEL_PROVIDER || MODEL_PROVIDER.QWEN_API, {
uuid: this.uuid
});
}
diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js
index 334c9d3..ab87f0a 100644
--- a/src/providers/provider-pool-manager.js
+++ b/src/providers/provider-pool-manager.js
@@ -72,6 +72,7 @@ export class ProviderPoolManager {
this.refreshBufferQueues = {}; // 按 providerType 分组的缓冲队列
this.refreshBufferTimers = {}; // 按 providerType 分组的定时器
this.bufferDelay = options.globalConfig?.REFRESH_BUFFER_DELAY ?? 5000; // 默认5秒缓冲延迟
+ this.refreshTaskTimeoutMs = options.globalConfig?.REFRESH_TASK_TIMEOUT_MS ?? 60000; // 默认60秒刷新超时
// 用于并发选点时的原子排序辅助(自增序列)
this._selectionSequence = 0;
@@ -184,6 +185,12 @@ export class ProviderPoolManager {
_enqueueRefresh(providerType, providerStatus, force = false) {
const uuid = providerStatus.uuid;
+ // 如果节点被禁用,不进行刷新
+ if (providerStatus.config.isDisabled) {
+ this._log('debug', `Skipping refresh for disabled node ${uuid}`);
+ return;
+ }
+
// 如果已经在刷新中,直接返回
if (this.refreshingUuids.has(uuid)) {
this._log('debug', `Node ${uuid} is already in refresh queue.`);
@@ -405,16 +412,18 @@ export class ProviderPoolManager {
// 调用适配器的 refreshToken 方法(内部封装了具体的刷新逻辑)
if (typeof serviceAdapter.refreshToken === 'function') {
const startTime = Date.now();
+ let refreshOperation;
if (force) {
if (typeof serviceAdapter.forceRefreshToken === 'function') {
- await serviceAdapter.forceRefreshToken();
+ refreshOperation = serviceAdapter.forceRefreshToken();
} else {
this._log('warn', `forceRefreshToken not implemented for ${providerType}, falling back to refreshToken`);
- await serviceAdapter.refreshToken();
+ refreshOperation = serviceAdapter.refreshToken();
}
} else {
- await serviceAdapter.refreshToken();
+ refreshOperation = serviceAdapter.refreshToken();
}
+ await this._awaitRefreshWithTimeout(refreshOperation, providerType, providerStatus.uuid);
const duration = Date.now() - startTime;
this._log('info', `Token refresh successful for node ${providerStatus.uuid} (Duration: ${duration}ms)`);
@@ -422,6 +431,8 @@ export class ProviderPoolManager {
config.needsRefresh = false;
config.refreshCount = 0;
config.lastRefreshTime = Date.now(); // 记录最后刷新成功时间
+
+ this._debouncedSave(providerType);
} else {
throw new Error(`refreshToken method not implemented for ${providerType}`);
}
@@ -433,6 +444,31 @@ export class ProviderPoolManager {
}
}
+ /**
+ * 为刷新任务附加超时保护,避免单个适配器调用无限挂起。
+ * @private
+ */
+ async _awaitRefreshWithTimeout(refreshOperation, providerType, uuid) {
+ if (this.refreshTaskTimeoutMs <= 0) {
+ return await refreshOperation;
+ }
+
+ let timeoutId = null;
+ const timeoutPromise = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ reject(new Error(`Refresh timeout after ${this.refreshTaskTimeoutMs}ms for node ${uuid} (${providerType})`));
+ }, this.refreshTaskTimeoutMs);
+ });
+
+ try {
+ return await Promise.race([Promise.resolve(refreshOperation), timeoutPromise]);
+ } finally {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ }
+ }
+
/**
* 计算节点的权重/评分,用于排序
* 分数越低,优先级越高
@@ -626,51 +662,69 @@ export class ProviderPoolManager {
* Initially, all providers are considered healthy and have zero usage.
*/
initializeProviderStatus() {
+ const oldFullStatus = this.providerStatus || {};
+ const isColdStart = Object.keys(oldFullStatus).length === 0;
+ this.providerStatus = {}; // Tracks health and usage for each provider instance
for (const providerType in this.providerPools) {
- const oldStatus = this.providerStatus[providerType] || [];
+ const oldStatus = oldFullStatus[providerType] || [];
this.providerStatus[providerType] = [];
this.roundRobinIndex[providerType] = 0; // Initialize round-robin index for each type
// 只有在锁不存在时才初始化,避免在运行中被重置导致并发问题
if (!this._selectionLocks[providerType]) {
this._selectionLocks[providerType] = Promise.resolve();
}
- this.providerPools[providerType].forEach((providerConfig) => {
- // 尝试从旧状态中恢复活跃请求计数和队列,避免重载配置时重置并发限制
- const existing = oldStatus.find(p => p.uuid === providerConfig.uuid);
+
+ const pool = this.providerPools[providerType];
+
+ pool.forEach((providerConfig) => {
+ try {
+ // 尝试从旧状态中恢复活跃请求计数和队列,避免重载配置时重置并发限制
+ const existing = oldStatus.find(p => p.uuid === providerConfig.uuid);
- // Ensure initial health and usage stats are present in the config
- providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true;
- providerConfig.isDisabled = providerConfig.isDisabled !== undefined ? providerConfig.isDisabled : false;
- providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null;
- providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0;
- providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
-
- // --- V2: 刷新监控字段 ---
- providerConfig.needsRefresh = providerConfig.needsRefresh !== undefined ? providerConfig.needsRefresh : false;
- providerConfig.refreshCount = providerConfig.refreshCount !== undefined ? providerConfig.refreshCount : 0;
-
- // 优化2: 简化 lastErrorTime 处理逻辑
- providerConfig.lastErrorTime = providerConfig.lastErrorTime instanceof Date
- ? providerConfig.lastErrorTime.toISOString()
- : (providerConfig.lastErrorTime || null);
-
- // 健康检测相关字段
- providerConfig.lastHealthCheckTime = providerConfig.lastHealthCheckTime || null;
- providerConfig.lastHealthCheckModel = providerConfig.lastHealthCheckModel || null;
- providerConfig.lastErrorMessage = providerConfig.lastErrorMessage || null;
- providerConfig.customName = providerConfig.customName || null;
-
- this.providerStatus[providerType].push({
- config: providerConfig,
- uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
- type: providerType, // 保存 providerType 引用
- state: existing ? existing.state : {
- activeCount: 0,
- waitingCount: 0,
- queue: []
+ // Ensure initial health and usage stats are present in the config
+ providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true;
+ providerConfig.isDisabled = providerConfig.isDisabled !== undefined ? providerConfig.isDisabled : false;
+ providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null;
+ providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0;
+ providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
+
+ // --- V2: 刷新监控字段 ---
+ const persistedNeedsRefresh = providerConfig.needsRefresh !== undefined ? providerConfig.needsRefresh : false;
+ const persistedRefreshCount = providerConfig.refreshCount !== undefined ? providerConfig.refreshCount : 0;
+ if (isColdStart && (persistedNeedsRefresh || persistedRefreshCount > 0)) {
+ this._log('info', `Resetting stale refresh state for provider ${providerConfig.uuid} (${providerType}) on startup.`);
}
- });
+ providerConfig.needsRefresh = isColdStart ? false : persistedNeedsRefresh;
+ providerConfig.refreshCount = isColdStart ? 0 : persistedRefreshCount;
+
+ // 优化2: 简化 lastErrorTime 处理逻辑
+ providerConfig.lastErrorTime = providerConfig.lastErrorTime instanceof Date
+ ? providerConfig.lastErrorTime.toISOString()
+ : (providerConfig.lastErrorTime || null);
+
+ // 健康检测相关字段
+ providerConfig.lastHealthCheckTime = providerConfig.lastHealthCheckTime || null;
+ providerConfig.lastHealthCheckModel = providerConfig.lastHealthCheckModel || null;
+ providerConfig.lastErrorMessage = providerConfig.lastErrorMessage || null;
+ providerConfig.customName = providerConfig.customName || null;
+
+ this.providerStatus[providerType].push({
+ config: providerConfig,
+ uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
+ type: providerType, // 保存 providerType 引用
+ state: existing ? existing.state : {
+ activeCount: 0,
+ waitingCount: 0,
+ queue: []
+ }
+ });
+ } catch (nodeError) {
+ logger.error(`[ProviderPoolManager] Error initializing node for ${providerType}: ${nodeError.message}`);
+ }
});
+
+ // 确保初始化时的默认值补全也能写盘
+ this._debouncedSave(providerType);
}
this._log('info', `Initialized provider statuses: ok (maxErrorCount: ${this.maxErrorCount})`);
}
diff --git a/src/services/service-manager.js b/src/services/service-manager.js
index 47f1673..e574b54 100644
--- a/src/services/service-manager.js
+++ b/src/services/service-manager.js
@@ -269,14 +269,21 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options
*/
export async function initApiService(config, isReady = false) {
- if (config.providerPools && Object.keys(config.providerPools).length > 0) {
- providerPoolManager = new ProviderPoolManager(config.providerPools, {
+ // Initialize or update ProviderPoolManager
+ if (providerPoolManager) {
+ providerPoolManager.providerPools = config.providerPools || {};
+ providerPoolManager.initializeProviderStatus();
+ logger.info('[Initialization] ProviderPoolManager existing instance updated.');
+ } else {
+ providerPoolManager = new ProviderPoolManager(config.providerPools || {}, {
globalConfig: config,
- maxErrorCount: config.MAX_ERROR_COUNT ?? 3,
+ maxErrorCount: config.MAX_ERROR_COUNT ?? 10,
providerFallbackChain: config.providerFallbackChain || {},
});
- logger.info('[Initialization] ProviderPoolManager initialized with configured pools.');
+ logger.info('[Initialization] ProviderPoolManager initialized.');
+ }
+ if (config.providerPools && Object.keys(config.providerPools).length > 0) {
if(isReady){
// --- V2: 触发系统预热 ---
// 预热逻辑是异步的,不会阻塞服务器启动
@@ -289,10 +296,8 @@ export async function initApiService(config, isReady = false) {
logger.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`);
});
}
-
- // 健康检查将在服务器完全启动后执行
} else {
- logger.info('[Initialization] No provider pools configured. Using single provider mode.');
+ logger.info('[Initialization] Provider pools are currently empty.');
}
// Initialize all provider pool nodes at startup
@@ -302,9 +307,17 @@ export async function initApiService(config, isReady = false) {
let totalFailed = 0;
for (const [providerType, providerConfigs] of Object.entries(config.providerPools)) {
- // 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中
- if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
- if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) {
+ // 验证提供商类型是否有效且被包含在 DEFAULT_MODEL_PROVIDERS 中
+ // 如果没设置 DEFAULT_MODEL_PROVIDERS,则允许所有已注册的类型
+ const isDefaultProvider = !config.DEFAULT_MODEL_PROVIDERS ||
+ (Array.isArray(config.DEFAULT_MODEL_PROVIDERS) && config.DEFAULT_MODEL_PROVIDERS.includes(providerType));
+
+ if (!isDefaultProvider) {
+ // 进一步检查是否是注册提供商的变体(带后缀)
+ const isVariantOfDefault = Array.isArray(config.DEFAULT_MODEL_PROVIDERS) &&
+ config.DEFAULT_MODEL_PROVIDERS.some(p => providerType.startsWith(p + '-'));
+
+ if (!isVariantOfDefault) {
logger.info(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`);
continue;
}
@@ -550,7 +563,8 @@ export async function getProviderStatus(config, options = {}) {
'customName',
'isHealthy',
'lastErrorTime',
- 'lastErrorMessage'
+ 'lastErrorMessage',
+ 'needsRefresh'
];
// identify 字段映射表
const identifyFieldMap = {
diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js
index 2ae46d4..be4c9c6 100644
--- a/src/ui-modules/config-api.js
+++ b/src/ui-modules/config-api.js
@@ -114,7 +114,12 @@ export async function handleUpdateConfig(req, res, currentConfig) {
// Update config values in memory(含类型校验)
if (newConfig.REQUIRED_API_KEY !== undefined) {
- if (typeof newConfig.REQUIRED_API_KEY === 'string' && newConfig.REQUIRED_API_KEY !== '******') currentConfig.REQUIRED_API_KEY = newConfig.REQUIRED_API_KEY;
+ if (typeof newConfig.REQUIRED_API_KEY === 'string') {
+ // 如果是脱敏后的字符串,则忽略更新,保留原值
+ if (newConfig.REQUIRED_API_KEY !== '******') {
+ currentConfig.REQUIRED_API_KEY = newConfig.REQUIRED_API_KEY;
+ }
+ }
}
if (newConfig.HOST !== undefined) {
if (typeof newConfig.HOST === 'string' && newConfig.HOST.length > 0) currentConfig.HOST = newConfig.HOST;
@@ -401,13 +406,23 @@ export async function handleUpdateAdminPassword(req, res) {
if (!password || password.trim() === '') {
res.writeHead(400, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: { message: 'Password cannot be empty' } }));
+ res.end(JSON.stringify({
+ error: {
+ message: 'Password cannot be empty',
+ messageCode: 'common.passwordEmpty'
+ }
+ }));
return true;
}
if (password.trim().length < PASSWORD.MIN_LENGTH) {
res.writeHead(400, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: { message: `Password must be at least ${PASSWORD.MIN_LENGTH} characters` } }));
+ res.end(JSON.stringify({
+ error: {
+ message: `Password must be at least ${PASSWORD.MIN_LENGTH} characters`,
+ messageCode: 'common.passwordTooShort'
+ }
+ }));
return true;
}
diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js
index 600f4d0..d6692ad 100644
--- a/src/ui-modules/provider-api.js
+++ b/src/ui-modules/provider-api.js
@@ -16,23 +16,27 @@ function sanitizeProviderData(provider, maskSensitive = false) {
// 1. 过滤敏感字段(API Keys, Tokens 等)
if (maskSensitive) {
- const sensitiveKeys = [
- 'OPENAI_API_KEY', 'CLAUDE_API_KEY', 'FORWARD_API_KEY',
- 'GROK_COOKIE_TOKEN', 'GROK_CF_CLEARANCE',
- 'refreshToken', 'accessToken', 'clientSecret'
- ];
-
- sensitiveKeys.forEach(key => {
- if (sanitized[key]) {
+ for (const key in sanitized) {
+ // 排除已知非敏感字段
+ if (key === 'uuid' || key === 'customName' || key === 'isHealthy' || key === 'isDisabled' || key === 'needsRefresh') continue;
+
+ const val = sanitized[key];
+ if (typeof val !== 'string' || !val) continue;
+
+ // 识别敏感字段:包含 KEY, TOKEN, SECRET, PASSWORD, CLEARANCE 等关键词
+ // 同时排除包含 PATH, URL, DIR, ENDPOINT 等关键词的路径/地址字段
+ const isSensitive = /API_KEY|TOKEN|SECRET|PASSWORD|CLEARANCE|ACCESS_KEY|credentials/i.test(key);
+ const isPath = /PATH|URL|DIR|ENDPOINT|REGION/i.test(key);
+
+ if (isSensitive && !isPath) {
// 对密钥进行脱敏显示(只保留前 4 位和后 4 位)
- const val = sanitized[key];
- if (typeof val === 'string' && val.length > 10) {
+ if (val.length > 10) {
sanitized[key] = val.substring(0, 4) + '****' + val.substring(val.length - 4);
} else {
sanitized[key] = '********';
}
}
- });
+ }
}
// 2. 净化 customName 中的 HTML/脚本
@@ -60,6 +64,29 @@ function sanitizeProviderPools(pools, maskSensitive = false) {
}
return sanitized;
}
+
+/**
+ * 过滤掉数据中的脱敏占位符,避免在保存时覆盖真实数据
+ */
+function filterMaskedData(data) {
+ if (!data || typeof data !== 'object') return data;
+ const result = { ...data };
+
+ for (const key in result) {
+ const val = result[key];
+ if (typeof val === 'string') {
+ // 匹配 ******** 或 XXXX****XXXX 格式
+ // 如果值包含 **** 且长度符合脱敏特征,则认为它是脱敏后的回传值,应该忽略
+ // 不再仅限于特定的 sensitiveKeys,而是检查所有字符串字段
+ if (val === '********' || (val.includes('****') && val.length >= 10)) {
+ delete result[key];
+ }
+ }
+ }
+
+ return result;
+}
+
// 使用 Promise 链式队列,确保文件操作顺序执行
let _fileLockChain = Promise.resolve();
@@ -88,24 +115,20 @@ function withFileLock(fn) {
* 获取所有提供商的状态(包括支持的类型和号池组)
*/
export async function handleGetProviders(req, res, currentConfig, providerPoolManager) {
- if (!providerPoolManager) {
- res.writeHead(400, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: { message: 'Provider pool manager not initialized' } }));
- return true;
- }
-
// 1. 获取支持的基础提供商类型
const registeredProviders = getRegisteredProviders();
let poolTypes = [];
// 2. 从管理器获取当前所有池的状态
const providerStatus = {};
- for (const [type, providers] of Object.entries(providerPoolManager.providerStatus)) {
- providerStatus[type] = providers.map(p => ({
- ...p.config,
- activeRequests: p.state?.activeCount || 0,
- waitingRequests: p.state?.waitingCount || 0
- }));
+ if (providerPoolManager) {
+ for (const [type, providers] of Object.entries(providerPoolManager.providerStatus)) {
+ providerStatus[type] = providers.map(p => ({
+ ...p.config,
+ activeRequests: p.state?.activeCount || 0,
+ waitingRequests: p.state?.waitingCount || 0
+ }));
+ }
}
// 3. 补全号池配置文件中的所有组
@@ -115,8 +138,18 @@ export async function handleGetProviders(req, res, currentConfig, providerPoolMa
const poolsData = JSON.parse(readFileSync(filePath, 'utf-8'));
poolTypes = Object.keys(poolsData);
poolTypes.forEach(type => {
- if (!providerStatus[type]) {
- providerStatus[type] = [];
+ // 如果管理器中没有该组,或者该组是空的,则从文件中补全
+ if (!providerStatus[type] || providerStatus[type].length === 0) {
+ const fileProviders = poolsData[type] || [];
+ if (fileProviders.length > 0) {
+ providerStatus[type] = fileProviders.map(p => ({
+ ...p,
+ activeRequests: 0,
+ waitingRequests: 0
+ }));
+ } else if (!providerStatus[type]) {
+ providerStatus[type] = [];
+ }
}
});
}
@@ -156,7 +189,7 @@ export async function handleGetProviderType(req, res, currentConfig, providerPoo
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
providerType,
- providers: providers.map(p => sanitizeProviderData(p, false)), // 详情页(用于编辑)不打码
+ providers: providers.map(p => sanitizeProviderData(p, true)), // 详情页也进行打码,确保即便点击显示也是脱敏数据
totalCount: providers.length,
healthyCount: providers.filter(p => p.isHealthy).length
}));
@@ -288,7 +321,10 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
if (!providerPools[providerType]) {
providerPools[providerType] = [];
}
- providerPools[providerType].push(providerConfig);
+
+ // 过滤掉脱敏字段
+ const filteredConfig = filterMaskedData(providerConfig);
+ providerPools[providerType].push(filteredConfig);
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
@@ -321,7 +357,7 @@ async function _handleAddProvider(req, res, currentConfig, providerPoolManager)
res.end(JSON.stringify({
success: true,
message: 'Provider added successfully',
- provider: sanitizeProviderData(providerConfig),
+ provider: sanitizeProviderData(providerConfig, true),
providerType
}));
return true;
@@ -380,9 +416,13 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
// Update provider while preserving certain fields
const existingProvider = providers[providerIndex];
+
+ // 过滤掉传入配置中的脱敏占位符,避免覆盖真实数据
+ const filteredConfig = filterMaskedData(providerConfig);
+
const updatedProvider = {
...existingProvider,
- ...providerConfig,
+ ...filteredConfig,
uuid: providerUuid, // Ensure UUID doesn't change
lastUsed: existingProvider.lastUsed, // Preserve usage stats
usageCount: existingProvider.usageCount,
@@ -415,7 +455,7 @@ async function _handleUpdateProvider(req, res, currentConfig, providerPoolManage
res.end(JSON.stringify({
success: true,
message: 'Provider updated successfully',
- provider: sanitizeProviderData(updatedProvider)
+ provider: sanitizeProviderData(updatedProvider, true)
}));
return true;
} catch (error) {
diff --git a/src/ui-modules/update-api.js b/src/ui-modules/update-api.js
index ac8976d..b112dc1 100644
--- a/src/ui-modules/update-api.js
+++ b/src/ui-modules/update-api.js
@@ -6,6 +6,7 @@ import { exec } from 'child_process';
import { promisify } from 'util';
import { CONFIG } from '../core/config-manager.js';
import { parseProxyUrl } from '../utils/proxy-utils.js';
+import { getRequestBody } from '../utils/common.js';
const execAsync = promisify(exec);
const GITHUB_REPO = 'justlovemaki/AIClient-2-API';
@@ -149,16 +150,16 @@ function compareVersions(v1, v2) {
}
/**
- * 通过 GitHub API 获取最新版本
- * @returns {Promise