diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index 975002f..ecf72cf 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -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, diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js index 99079a1..9133ed1 100644 --- a/src/converters/strategies/OpenAIConverter.js +++ b/src/converters/strategies/OpenAIConverter.js @@ -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)) { diff --git a/src/core/config-manager.js b/src/core/config-manager.js index bac9e53..e724afb 100644 --- a/src/core/config-manager.js +++ b/src/core/config-manager.js @@ -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 链配置 diff --git a/src/core/master.js b/src/core/master.js index ff354f0..8cf208a 100644 --- a/src/core/master.js +++ b/src/core/master.js @@ -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; // 不退出程序,继续运行 + } }); } diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index f8c1367..883d36e 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -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 时,自动添加一个占位工具 diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index 310d5bb..7f2043e 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -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}`); } diff --git a/src/services/api-server.js b/src/services/api-server.js index d0d15df..90543f2 100644 --- a/src/services/api-server.js +++ b/src/services/api-server.js @@ -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; // 不退出程序,继续运行 + } }); } diff --git a/src/ui-modules/auth.js b/src/ui-modules/auth.js index f3cf9d0..2d5aeaf 100644 --- a/src/ui-modules/auth.js +++ b/src/ui-modules/auth.js @@ -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' }); diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index b0e2b37..8bfaf2d 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -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, diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 2597034..7828b25 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -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); diff --git a/static/app/i18n.js b/static/app/i18n.js index f3f11e0..03adce6 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -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', diff --git a/static/components/section-config.html b/static/components/section-config.html index 77fdeb9..357dd73 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -173,7 +173,7 @@ -