feat: 增加登录过期配置并优化错误处理

- 新增 LOGIN_EXPIRY 配置项,支持自定义管理后台登录 Token 有效期
- 优化 provider 错误计数逻辑,当 MAX_ERROR_COUNT 为 0 时禁用自动标记不健康
- 修复工具消息转换中对象内容未序列化的问题
- 增强网络错误处理,可重试的网络错误不再导致进程退出
- 过滤 Claude Kiro provider 中描述为空的工具,避免 API 调用失败
This commit is contained in:
hex2077 2026-02-09 19:50:35 +08:00
parent 26d611dcf7
commit 27acc72dfd
12 changed files with 121 additions and 34 deletions

View file

@ -140,7 +140,12 @@ export class ClaudeConverter extends BaseConverter {
for (const item of msg.content) {
if (item && typeof item === 'object' && item.type === "tool_result") {
const toolUseId = item.tool_use_id || item.id || "";
const contentStr = String(item.content || "");
let contentStr = item.content || "";
if (typeof contentStr === 'object') {
contentStr = JSON.stringify(contentStr);
} else {
contentStr = String(contentStr);
}
tempOpenAIMessages.push({
role: "tool",
tool_call_id: toolUseId,

View file

@ -149,10 +149,14 @@ export class OpenAIConverter extends BaseConverter {
if (message.role === 'tool') {
// 工具结果消息
let toolContent = message.content;
if (typeof toolContent === 'object' && toolContent !== null) {
toolContent = JSON.stringify(toolContent);
}
content.push({
type: 'tool_result',
tool_use_id: message.tool_call_id,
content: safeParseJSON(message.content)
content: toolContent
});
claudeMessages.push({ role: 'user', content: content });
} else if (message.role === 'assistant' && (message.tool_calls?.length || message.function_calls?.length)) {

View file

@ -79,6 +79,7 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
CREDENTIAL_SWITCH_MAX_RETRIES: 5, // 坏凭证切换最大重试次数(用于认证错误后切换凭证)
CRON_NEAR_MINUTES: 15,
CRON_REFRESH_TOKEN: false,
LOGIN_EXPIRY: 3600, // 登录过期时间默认1小时
PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径
MAX_ERROR_COUNT: 10, // 提供商最大错误次数
providerFallbackChain: {}, // 跨类型 Fallback 链配置

View file

@ -16,6 +16,7 @@ import logger from '../utils/logger.js';
import * as http from 'http';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { isRetryableNetworkError } from '../utils/common.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -344,10 +345,25 @@ function setupSignalHandlers() {
// 未捕获的异常
process.on('uncaughtException', (error) => {
logger.error('[Master] Uncaught exception:', error);
// 检查是否为可重试的网络错误
if (isRetryableNetworkError(error)) {
logger.warn('[Master] Network error detected, continuing operation...');
return; // 不退出程序,继续运行
}
// 对于其他严重错误,记录但不退出(由主进程管理子进程)
logger.error('[Master] Fatal error detected in master process');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('[Master] Unhandled rejection at:', promise, 'reason:', reason);
// 检查是否为可重试的网络错误
if (reason && isRetryableNetworkError(reason)) {
logger.warn('[Master] Network error in promise rejection, continuing operation...');
return; // 不退出程序,继续运行
}
});
}

View file

@ -887,35 +887,62 @@ async saveCredentialsToFile(filePath, newData) {
};
toolsContext = { tools: [placeholderTool] };
} else {
const MAX_DESCRIPTION_LENGTH = 9216;
const MAX_DESCRIPTION_LENGTH = 9216;
let truncatedCount = 0;
const kiroTools = filteredTools.map(tool => {
let desc = tool.description || "";
const originalLength = desc.length;
if (desc.length > MAX_DESCRIPTION_LENGTH) {
desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "...";
truncatedCount++;
logger.info(`[Kiro] Truncated tool '${tool.name}' description: ${originalLength} -> ${desc.length} chars`);
}
return {
toolSpecification: {
name: tool.name,
description: desc,
inputSchema: {
json: tool.input_schema || {}
let truncatedCount = 0;
const kiroTools = filteredTools
.filter(tool => {
// 过滤掉描述为空的工具
if (!tool.description || tool.description.trim() === '') {
logger.info(`[Kiro] Ignoring tool with empty description: ${tool.name}`);
return false;
}
}
};
});
if (truncatedCount > 0) {
logger.info(`[Kiro] Truncated ${truncatedCount} tool description(s) to max ${MAX_DESCRIPTION_LENGTH} chars`);
}
return true;
})
.map(tool => {
let desc = tool.description || "";
const originalLength = desc.length;
if (desc.length > MAX_DESCRIPTION_LENGTH) {
desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "...";
truncatedCount++;
logger.info(`[Kiro] Truncated tool '${tool.name}' description: ${originalLength} -> ${desc.length} chars`);
}
return {
toolSpecification: {
name: tool.name,
description: desc,
inputSchema: {
json: tool.input_schema || {}
}
}
};
});
if (truncatedCount > 0) {
logger.info(`[Kiro] Truncated ${truncatedCount} tool description(s) to max ${MAX_DESCRIPTION_LENGTH} chars`);
}
toolsContext = { tools: kiroTools };
// 检查过滤后是否还有有效工具
if (kiroTools.length === 0) {
logger.info('[Kiro] All tools were filtered out (empty descriptions), adding placeholder tool');
const placeholderTool = {
toolSpecification: {
name: "no_tool_available",
description: "This is a placeholder tool when no other tools are available. It does nothing.",
inputSchema: {
json: {
type: "object",
properties: {}
}
}
}
};
toolsContext = { tools: [placeholderTool] };
} else {
toolsContext = { tools: kiroTools };
}
}
} else {
// tools 为空或长度为 0 时,自动添加一个占位工具

View file

@ -900,7 +900,7 @@ export class ProviderPoolManager {
provider.config.lastErrorMessage = errorMessage;
}
if (provider.config.errorCount >= this.maxErrorCount) {
if (this.maxErrorCount > 0 && provider.config.errorCount >= this.maxErrorCount) {
provider.config.isHealthy = false;
this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
}

View file

@ -115,6 +115,7 @@ import { discoverPlugins, getPluginManager } from '../core/plugin-manager.js';
import 'dotenv/config'; // Import dotenv and configure it
import '../converters/register-converters.js'; // 注册所有转换器
import { getProviderPoolManager } from './service-manager.js';
import { isRetryableNetworkError } from '../utils/common.js';
// 检测是否作为子进程运行
const IS_WORKER_PROCESS = process.env.IS_WORKER_PROCESS === 'true';
@ -209,11 +210,26 @@ function setupSignalHandlers() {
process.on('uncaughtException', (error) => {
logger.error('[Server] Uncaught exception:', error);
// 检查是否为可重试的网络错误
if (isRetryableNetworkError(error)) {
logger.warn('[Server] Network error detected, continuing operation...');
return; // 不退出程序,继续运行
}
// 对于其他严重错误,执行优雅关闭
logger.error('[Server] Fatal error detected, initiating shutdown...');
gracefulShutdown();
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('[Server] Unhandled rejection at:', promise, 'reason:', reason);
// 检查是否为可重试的网络错误
if (reason && isRetryableNetworkError(reason)) {
logger.warn('[Server] Network error in promise rejection, continuing operation...');
return; // 不退出程序,继续运行
}
});
}

View file

@ -3,6 +3,7 @@ import logger from '../utils/logger.js';
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
import { CONFIG } from '../core/config-manager.js';
// Token存储到本地文件中
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
@ -83,15 +84,16 @@ function generateToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
/**
* 生成token过期时间
*/
function getExpiryTime() {
const now = Date.now();
const expiry = 60 * 60 * 1000; // 1小时
const expiry = (CONFIG.LOGIN_EXPIRY || 3600) * 1000; // 使用配置的过期时间,默认1小时
return now + expiry;
}
/**
* 读取token存储文件
*/
@ -231,12 +233,12 @@ export async function handleLoginRequest(req, res) {
expiryTime
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Login successful',
token,
expiresIn: '1 hour'
expiresIn: `${CONFIG.LOGIN_EXPIRY || 3600} seconds`
}));
} else {
res.writeHead(401, { 'Content-Type': 'application/json' });

View file

@ -87,6 +87,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
if (newConfig.CREDENTIAL_SWITCH_MAX_RETRIES !== undefined) currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES = newConfig.CREDENTIAL_SWITCH_MAX_RETRIES;
if (newConfig.CRON_NEAR_MINUTES !== undefined) currentConfig.CRON_NEAR_MINUTES = newConfig.CRON_NEAR_MINUTES;
if (newConfig.CRON_REFRESH_TOKEN !== undefined) currentConfig.CRON_REFRESH_TOKEN = newConfig.CRON_REFRESH_TOKEN;
if (newConfig.LOGIN_EXPIRY !== undefined) currentConfig.LOGIN_EXPIRY = newConfig.LOGIN_EXPIRY;
if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH;
if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT;
if (newConfig.WARMUP_TARGET !== undefined) currentConfig.WARMUP_TARGET = newConfig.WARMUP_TARGET;
@ -148,6 +149,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
CREDENTIAL_SWITCH_MAX_RETRIES: currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES,
CRON_NEAR_MINUTES: currentConfig.CRON_NEAR_MINUTES,
CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
LOGIN_EXPIRY: currentConfig.LOGIN_EXPIRY,
PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
WARMUP_TARGET: currentConfig.WARMUP_TARGET,

View file

@ -79,7 +79,9 @@ async function loadConfiguration() {
const requestBaseDelayEl = document.getElementById('requestBaseDelay');
const cronNearMinutesEl = document.getElementById('cronNearMinutes');
const cronRefreshTokenEl = document.getElementById('cronRefreshToken');
const loginExpiryEl = document.getElementById('loginExpiry');
const providerPoolsFilePathEl = document.getElementById('providerPoolsFilePath');
const maxErrorCountEl = document.getElementById('maxErrorCount');
const warmupTargetEl = document.getElementById('warmupTarget');
const refreshConcurrencyPerProviderEl = document.getElementById('refreshConcurrencyPerProvider');
@ -99,6 +101,7 @@ async function loadConfiguration() {
if (cronNearMinutesEl) cronNearMinutesEl.value = data.CRON_NEAR_MINUTES || 1;
if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
if (loginExpiryEl) loginExpiryEl.value = data.LOGIN_EXPIRY || 3600;
if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH;
if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 10;
if (warmupTargetEl) warmupTargetEl.value = data.WARMUP_TARGET || 0;
@ -218,6 +221,7 @@ async function saveConfiguration() {
config.CREDENTIAL_SWITCH_MAX_RETRIES = parseInt(document.getElementById('credentialSwitchMaxRetries')?.value || 5);
config.CRON_NEAR_MINUTES = parseInt(document.getElementById('cronNearMinutes')?.value || 1);
config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false;
config.LOGIN_EXPIRY = parseInt(document.getElementById('loginExpiry')?.value || 3600);
config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || '';
config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 10);
config.WARMUP_TARGET = parseInt(document.getElementById('warmupTarget')?.value || 0);

View file

@ -286,6 +286,8 @@ const translations = {
'config.advanced.refreshConcurrencyPerProviderNote': '每个提供商内部最大并行刷新任务数,默认为 1',
'config.advanced.cronInterval': 'OAuth令牌刷新间隔(分钟)',
'config.advanced.cronEnabled': '启用OAuth令牌自动刷新(需重启服务)',
'config.advanced.loginExpiry': '登录过期时间(秒)',
'config.advanced.loginExpiryNote': '管理后台登录后的 Token 有效期,默认 3600 秒 (1小时)',
'config.advanced.poolFilePath': '提供商池配置文件路径(不能为空)',
'config.advanced.poolFilePathPlaceholder': '默认: configs/provider_pools.json',
'config.advanced.poolNote': '使用默认路径配置需添加一个空节点',
@ -1081,6 +1083,8 @@ const translations = {
'config.advanced.refreshConcurrencyPerProviderNote': 'Max parallel refresh tasks per provider, default 1',
'config.advanced.cronInterval': 'OAuth Token Refresh Interval (minutes)',
'config.advanced.cronEnabled': 'Enable OAuth Token Auto Refresh (requires restart)',
'config.advanced.loginExpiry': 'Login Expiry (seconds)',
'config.advanced.loginExpiryNote': 'Token validity period after management console login, default 3600 seconds (1 hour)',
'config.advanced.poolFilePath': 'Provider Pool Config File Path (required)',
'config.advanced.poolFilePathPlaceholder': 'Default: configs/provider_pools.json',
'config.advanced.poolNote': 'To use default path configuration, add an empty node',

View file

@ -173,7 +173,7 @@
<label for="cronNearMinutes" data-i18n="config.advanced.cronInterval">令牌刷新间隔(分钟)</label>
<input type="number" id="cronNearMinutes" class="form-control" min="1" max="60" value="1">
</div>
<div class="form-group">
<div class="form-group">
<label data-i18n="config.advanced.cronEnabled">启用自动刷新 (需重启)</label>
<label class="toggle-switch">
<input type="checkbox" id="cronRefreshToken">
@ -181,8 +181,14 @@
</label>
</div>
</div>
<div class="form-group">
<label for="loginExpiry" data-i18n="config.advanced.loginExpiry">登录过期时间(秒)</label>
<input type="number" id="loginExpiry" class="form-control" min="60" value="3600">
<small class="form-text" data-i18n="config.advanced.loginExpiryNote">管理后台登录后的 Token 有效期,默认 3600 秒 (1小时)</small>
</div>
</div>
<!-- 日志管理 -->
<div class="config-group-section">
<h3 data-i18n="config.log.title"><i class="fas fa-file-alt"></i> 日志设置</h3>