feat: 增加登录过期配置并优化错误处理
- 新增 LOGIN_EXPIRY 配置项,支持自定义管理后台登录 Token 有效期 - 优化 provider 错误计数逻辑,当 MAX_ERROR_COUNT 为 0 时禁用自动标记不健康 - 修复工具消息转换中对象内容未序列化的问题 - 增强网络错误处理,可重试的网络错误不再导致进程退出 - 过滤 Claude Kiro provider 中描述为空的工具,避免 API 调用失败
This commit is contained in:
parent
26d611dcf7
commit
27acc72dfd
12 changed files with 121 additions and 34 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 链配置
|
||||
|
|
|
|||
|
|
@ -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; // 不退出程序,继续运行
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 时,自动添加一个占位工具
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; // 不退出程序,继续运行
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue