// src/js/toolHealthChecker.js /** * [重构] 工具健康检查器 * 实现真实的 ping 和 API 测试,检查工具的健康状态 */ import configManager from './configManager.js'; import { toolRegistry } from './tool-registry.js'; import toolStatusManager from './toolStatusManager.js'; import i18n from './i18n.js'; class ToolHealthChecker { constructor() { this.checkResults = {}; this.isChecking = false; this.isBackgroundChecking = false; this.checkProgress = { total: 0, checked: 0, success: 0, failed: 0 }; this.currentCheckTool = null; this.checkStartTime = null; this.checkPromise = null; // [修复] 添加检查 Promise 引用 // 从数据库加载上次检查日期 this.loadLastCheckDate(); this.lockedTools = new Set(); this.loadLockedTools(); // 检查进度回调 this.progressCallbacks = []; // 初始化时加载上次检查日期 this.loadLastCheckDate().catch(error => { console.error('[ToolHealthChecker] 初始化加载检查日期失败:', error); }); } /** * 注册进度回调 * @param {Function} callback - 回调函数 (progress) => {} */ onProgress(callback) { this.progressCallbacks.push(callback); } /** * 移除进度回调 * @param {Function} callback - 回调函数 */ offProgress(callback) { const index = this.progressCallbacks.indexOf(callback); if (index > -1) { this.progressCallbacks.splice(index, 1); } } /** * 触发进度更新 * @param {Object} progress - 进度信息 */ _emitProgress(progress) { this.progressCallbacks.forEach(callback => { try { callback(progress); } catch (error) { console.error('[ToolHealthChecker] 进度回调执行失败:', error); } }); } /** * 从数据库加载上次检查日期 */ async loadLastCheckDate() { try { // 优先从汇总表获取 if (window.electronAPI && window.electronAPI.getLastHealthCheckDate) { const result = await window.electronAPI.getLastHealthCheckDate(); if (result && result.success && result.data) { this.lastCheckDate = result.data; return; } } // 兼容旧系统:从配置获取 const savedDate = configManager.config?.tool_health_last_check || null; if (savedDate) { this.lastCheckDate = savedDate; } } catch (error) { console.error('[ToolHealthChecker] 加载上次检查日期失败:', error); // 从配置获取 const savedDate = configManager.config?.tool_health_last_check || null; if (savedDate) { this.lastCheckDate = savedDate; } } } /** * 从配置中加载已锁定的工具 */ loadLockedTools() { const locked = configManager.config?.locked_tools || []; this.lockedTools = new Set(locked); } /** * 保存已锁定的工具到配置 */ saveLockedTools() { try { if (!configManager.config) { configManager.config = {}; } configManager.config.locked_tools = Array.from(this.lockedTools); // 尝试保存到数据库(如果 electronAPI 可用) if (window.electronAPI && window.electronAPI.setConfig) { window.electronAPI.setConfig('locked_tools', Array.from(this.lockedTools)); } } catch (error) { console.error('[ToolHealthChecker] 保存锁定工具失败:', error); } } /** * 判断工具是否需要网络检查 */ isNetworkTool(toolId) { // 定义需要网络检查的工具列表 const networkTools = [ 'earthquake-info', 'car-info', 'cctv-news', 'oil-price', 'history-today', 'domain-price', 'tech-news', 'gold-price', 'zhihu-hot', 'movie-box-office', 'football-news', 'train-query', 'baidu-hot', 'bili-hot-ranking', 'ip-query', 'ip-info', 'dns-query', 'wx-domain-check', 'smart-search', 'hotboard', 'ai-translation', 'weather-details' ]; return networkTools.includes(toolId); } /** * 获取工具的 API URL 和测试参数 * @param {string} toolId - 工具ID * @returns {Object} { url, method, testParams, expectedStatus } */ getToolApiConfig(toolId) { const apiConfigs = { // 地震信息 'earthquake-info': { url: 'https://www.cunyuapi.top/earthquake', method: 'GET', testParams: null, expectedStatus: [200, 201, 400, 401, 403, 404, 500, 502, 503] // 任何响应都表示API可用 }, // 车辆信息 'car-info': { url: 'https://api.jkyai.top/API/clxxcx.php', method: 'GET', testParams: { car: 'test' }, expectedStatus: [200, 400, 404, 500] }, // 央视新闻 'cctv-news': { url: 'https://api.jkyai.top/API/ysxwrd.php', method: 'GET', testParams: { type: 'json' }, expectedStatus: [200, 400, 404, 500] }, // 油价查询 'oil-price': { url: 'https://api.jkyai.top/API/yjcx.php', method: 'GET', testParams: { city: '北京' }, expectedStatus: [200, 400, 404, 500] }, // 历史上的今天 'history-today': { url: 'https://api.jkyai.top/API/lssdjt.php', method: 'GET', testParams: { type: 'json' }, expectedStatus: [200, 400, 404, 500] }, // 域名价格 'domain-price': { url: 'https://api.jkyai.top/API/ymbjcx.php', method: 'GET', testParams: { domain: 'example.com' }, expectedStatus: [200, 400, 404, 500] }, // 科技资讯 'tech-news': { url: 'https://api.jkyai.top/API/kjzx.php', method: 'GET', testParams: { type: 'json' }, expectedStatus: [200, 400, 404, 500] }, // 金价查询 'gold-price': { url: 'https://api.pearktrue.cn/api/goldprice/', method: 'GET', testParams: null, expectedStatus: [200, 400, 404, 500] }, // 知乎热榜 'zhihu-hot': { url: 'http://shanhe.kim/api/za/zhihu.php', method: 'GET', testParams: null, expectedStatus: [200, 400, 404, 500] }, // 电影票房 'movie-box-office': { url: 'https://api.pearktrue.cn/api/maoyan/', method: 'GET', testParams: null, expectedStatus: [200, 400, 404, 500] }, // 足球新闻 'football-news': { url: 'https://api.jkyai.top/API/zqssrd.php', method: 'GET', testParams: { type: 'json' }, expectedStatus: [200, 400, 404, 500] }, // 火车查询 'train-query': { url: 'https://api.jkyai.top/API/hcpccx.php', method: 'GET', testParams: { from: '北京', to: '上海', date: new Date().toISOString().split('T')[0] }, expectedStatus: [200, 400, 404, 500] }, // 百度热榜 'baidu-hot': { url: 'https://api.suyanw.cn/api/bdrs.php', method: 'GET', testParams: { msg: '百度' }, expectedStatus: [200, 400, 404, 500] }, // B站热榜 'bili-hot-ranking': { url: 'https://api.suyanw.cn/api/bl.php', method: 'GET', testParams: { hh: '\n' }, expectedStatus: [200, 400, 404, 500] }, // IP查询 'ip-query': { url: 'https://api.ipify.org', method: 'GET', testParams: { format: 'json' }, expectedStatus: [200, 400, 404, 500] }, // IP/域名归属查询 'ip-info': { url: 'https://uapis.cn/api/v1/network/ipinfo', method: 'GET', testParams: { ip: '8.8.8.8' }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // DNS查询 'dns-query': { url: 'https://uapis.cn/api/v1/network/dns', method: 'GET', testParams: { domain: 'google.com', type: 'A' }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // 微信域名检测 'wx-domain-check': { url: 'https://uapis.cn/api/v1/network/wxdomain', method: 'GET', testParams: { domain: 'qq.com' }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // 智能搜索 'smart-search': { url: 'https://uapis.cn/api/v1/search/aggregate', method: 'POST', testParams: { query: 'test', timeout_ms: 5000, fetch_full: false }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // 多平台热榜 'hotboard': { url: 'https://uapis.cn/api/v1/misc/hotboard', method: 'GET', testParams: { type: 'zhihu' }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // AI翻译 'ai-translation': { url: 'https://uapis.cn/api/v1/ai/translate', method: 'POST', testParams: { text: 'test', from: 'zh', to: 'en' }, expectedStatus: [200, 400, 401, 403, 404, 500] }, // 天气详情 'weather-details': { url: 'https://uapis.cn/api/v1/misc/weather', method: 'GET', testParams: { city: '北京' }, expectedStatus: [200, 400, 401, 403, 404, 500] } }; return apiConfigs[toolId] || null; } /** * [重构] 执行 ping 测试(通过 HEAD 请求) * @param {string} url - 目标URL * @param {number} timeout - 超时时间(毫秒) * @returns {Promise} ping 是否成功 */ async pingUrl(url, timeout = 5000) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(url, { method: 'HEAD', signal: controller.signal, headers: { 'User-Agent': 'YMhut-Box/1.0' } }); clearTimeout(timeoutId); // 任何响应都表示服务器可达 return response.status >= 200 && response.status < 600; } catch (error) { // ping 失败 return false; } } /** * [重构] 检查单个工具的健康状态 * 1. 先执行 ping 测试 * 2. 然后执行 API 测试请求 * 3. 如果 ping 失败或 API 无响应,则工具不健康 * @param {string} toolId - 工具ID * @param {number} retries - 重试次数,默认2次 * @returns {Promise} 检查结果 */ async checkToolHealth(toolId, retries = 2) { const checkStartTime = Date.now(); try { // 更新检查状态 toolStatusManager.updateHealthStatus(toolId, 'checking', {}); this.currentCheckTool = toolId; } catch (error) { console.error(`[ToolHealthChecker] 更新检查状态失败 [${toolId}]:`, error); // 继续执行检查,不因状态更新失败而中断 } // 获取工具配置 const toolClass = toolRegistry[toolId]; let healthCheckConfig = null; let timeout = 10000; // 默认超时时间10秒 if (toolClass && toolClass.healthCheckConfig) { healthCheckConfig = toolClass.healthCheckConfig; if (healthCheckConfig.retries !== undefined) { retries = healthCheckConfig.retries; } if (healthCheckConfig.timeout !== undefined) { timeout = healthCheckConfig.timeout; } if (healthCheckConfig.enabled === false) { try { toolStatusManager.updateHealthStatus(toolId, 'healthy', { message: '健康检查已禁用', skipCheck: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } return { toolId, status: 'success', reason: 'health_check_disabled' }; } } // 获取 API 配置 const apiConfig = this.getToolApiConfig(toolId); if (!apiConfig) { // 如果没有 API 配置,说明是离线工具,直接返回成功 try { toolStatusManager.updateHealthStatus(toolId, 'healthy', { message: '离线工具,无需健康检查', skipCheck: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } return { toolId, status: 'success', reason: 'offline_tool' }; } // 提取域名用于 ping let domain = null; try { const urlObj = new URL(apiConfig.url); domain = urlObj.hostname; } catch (e) { // URL 解析失败,使用完整 URL domain = apiConfig.url; } // 执行健康检查 for (let attempt = 1; attempt <= retries; attempt++) { try { // 步骤1: Ping 测试 const pingSuccess = await this.pingUrl(apiConfig.url, 5000); if (!pingSuccess) { if (attempt === retries) { // 记录到数据库 try { await this._recordHealthCheck(toolId, 'failed', null, 'ping_failed', attempt); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: 'ping_failed', message: '服务器无法访问(ping 失败)', autoLock: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.add(toolId); return { toolId, status: 'failed', reason: 'ping_failed', attempt }; } await new Promise(resolve => setTimeout(resolve, 1000)); continue; } // 步骤2: API 测试请求 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); let requestUrl = apiConfig.url; let requestOptions = { method: apiConfig.method || 'GET', signal: controller.signal, headers: { 'User-Agent': 'YMhut-Box/1.0', 'Accept': '*/*' } }; // 添加测试参数 if (apiConfig.testParams) { if (apiConfig.method === 'POST') { requestOptions.headers['Content-Type'] = 'application/json'; requestOptions.body = JSON.stringify(apiConfig.testParams); } else { const params = new URLSearchParams(apiConfig.testParams); requestUrl = `${apiConfig.url}?${params.toString()}`; } } // 如果是智能搜索,添加 API Key(如果有) if (toolId === 'smart-search' && configManager.config?.api_keys?.uapipro) { requestOptions.headers['Authorization'] = `Bearer ${configManager.config.api_keys.uapipro}`; } const response = await fetch(requestUrl, requestOptions); clearTimeout(timeoutId); // 检查响应状态码 // 如果状态码在预期列表中,或者有响应(即使是错误),都认为 API 可用 const hasResponse = response.status >= 200 && response.status < 600; const isExpectedStatus = apiConfig.expectedStatus && apiConfig.expectedStatus.includes(response.status); if (hasResponse && (isExpectedStatus || response.status < 500)) { // API 可用,返回成功 const responseTime = Date.now() - checkStartTime; try { await this._recordHealthCheck(toolId, 'success', responseTime, null, attempt, response.status); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'healthy', { httpStatus: response.status, attempt, responseTime }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.delete(toolId); return { toolId, status: 'success', httpStatus: response.status, attempt }; } // 5xx 错误表示服务器问题 if (response.status >= 500) { if (attempt === retries) { try { await this._recordHealthCheck(toolId, 'failed', null, 'server_error', attempt, response.status); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: 'server_error', httpStatus: response.status, message: `服务器错误 (HTTP ${response.status})`, autoLock: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.add(toolId); return { toolId, status: 'failed', httpStatus: response.status, reason: 'server_error', attempt }; } await new Promise(resolve => setTimeout(resolve, 1000)); continue; } // 其他状态码(如 4xx)也认为 API 可用(至少 API 是响应的) const responseTime = Date.now() - checkStartTime; try { await this._recordHealthCheck(toolId, 'success', responseTime, null, attempt, response.status); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'healthy', { httpStatus: response.status, attempt, responseTime, message: 'API 可用(返回了响应)' }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.delete(toolId); return { toolId, status: 'success', httpStatus: response.status, reason: 'api_available', attempt }; } catch (error) { // 处理各种错误类型 const errorMessage = error.message || String(error); const isNetworkError = errorMessage.includes('network') || errorMessage.includes('fetch') || errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError') || errorMessage.includes('ERR_INTERNET_DISCONNECTED') || errorMessage.includes('ERR_NETWORK_CHANGED'); const isTimeoutError = errorMessage.includes('timeout') || errorMessage.includes('aborted') || error.name === 'AbortError'; if (attempt === retries) { let reason = 'network_error'; if (isTimeoutError) reason = 'timeout'; else if (isNetworkError) reason = 'network_error'; try { await this._recordHealthCheck(toolId, 'failed', null, reason, attempt); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: reason, error: errorMessage, message: this.getHealthCheckErrorMessage(reason), autoLock: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.add(toolId); return { toolId, status: 'failed', error: errorMessage, reason: reason, attempt }; } // 等待后重试 await new Promise(resolve => setTimeout(resolve, 1000)); } } // 所有重试都失败 try { await this._recordHealthCheck(toolId, 'failed', null, 'max_retries', retries); } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } try { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: 'max_retries', message: '重试次数超限', autoLock: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.add(toolId); return { toolId, status: 'failed', reason: 'max_retries' }; } /** * 记录健康检查结果到数据库 * @param {string} toolId - 工具ID * @param {string} status - 状态 ('success' | 'failed') * @param {number} responseTime - 响应时间(毫秒) * @param {string} errorMessage - 错误消息 * @param {number} attempt - 尝试次数 * @param {number} httpStatus - HTTP 状态码 */ async _recordHealthCheck(toolId, status, responseTime, errorMessage, attempt, httpStatus) { try { const checkDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD // [日志] 记录单个工具检查结果 if (configManager && configManager.logAction) { // 获取工具名称(从 toolRegistry 或使用 toolId) let toolName = toolId; try { const ToolClass = toolRegistry[toolId]; if (ToolClass && ToolClass.name) { toolName = ToolClass.name; } } catch (e) { // 忽略错误,使用 toolId } if (status === 'success') { configManager.logAction( `工具健康检查: ${toolName} 正常 (响应时间: ${responseTime || 'N/A'}ms)`, 'tool_health_check' ); } else { configManager.logAction( `工具健康检查: ${toolName} 异常 (${errorMessage || '未知错误'})`, 'tool_health_check' ); } } // 记录到工具健康检查结果表 if (window.electronAPI && window.electronAPI.recordToolHealthCheckResult) { await window.electronAPI.recordToolHealthCheckResult({ toolId: toolId, checkDate: checkDate, status: status, responseTime: responseTime || null, errorMessage: errorMessage || null, httpStatus: httpStatus || null, attempt: attempt || 1, reason: errorMessage || null }); } // 同时记录到接口健康度表(兼容旧系统) if (window.electronAPI && window.electronAPI.recordApiHealth) { await window.electronAPI.recordApiHealth({ apiId: `tool-${toolId}`, status: status === 'success' ? 'healthy' : 'unhealthy', responseTime: responseTime || null, errorMessage: errorMessage || null }); } // 同时记录接口调用 if (window.electronAPI && window.electronAPI.recordApiCall) { await window.electronAPI.recordApiCall({ apiId: `tool-${toolId}`, method: 'GET', url: this.getToolApiConfig(toolId)?.url || '', statusCode: httpStatus || null, responseTime: responseTime || null, success: status === 'success', errorMessage: errorMessage || null }); } } catch (error) { console.error(`[ToolHealthChecker] 记录健康检查结果失败 [${toolId}]:`, error); } } /** * 检查距离上次检查是否超过5天 * @returns {boolean} true 表示需要强制检查 */ shouldForceCheck() { if (!this.lastCheckDate) { return true; // 从未检查过,需要检查 } const lastCheck = new Date(this.lastCheckDate); const now = new Date(); const daysDiff = Math.floor((now - lastCheck) / (1000 * 60 * 60 * 24)); return daysDiff >= 5; // 超过5天需要强制检查 } /** * 检查今天是否已经检查过(不修改日期,只检查) * @param {boolean} updateDate - 是否更新检查日期,默认false * @returns {boolean} true表示今天已经检查过,false表示未检查 */ hasCheckedToday(updateDate = false) { const today = new Date().toDateString(); const savedDate = this.lastCheckDate || configManager.config?.tool_health_last_check || null; if (savedDate === today) { if (updateDate) { this.lastCheckDate = today; } return true; // 今天已经检查过 } if (updateDate) { this.lastCheckDate = today; try { if (!configManager.config) { configManager.config = {}; } configManager.config.tool_health_last_check = today; // 尝试保存到数据库(如果 electronAPI 可用) if (window.electronAPI && window.electronAPI.setConfig) { window.electronAPI.setConfig('tool_health_last_check', today); } } catch (error) { console.error('[ToolHealthChecker] 保存检查日期失败:', error); } } return false; // 今天未检查 } /** * 检查今天是否应该检查(用于shouldCheckToday的兼容性) * @returns {boolean} true表示应该检查,false表示今天已经检查过 */ shouldCheckToday() { return !this.hasCheckedToday(false); } /** * 获取上次检查日期 */ getLastCheckDate() { return this.lastCheckDate || configManager.config?.tool_health_last_check || null; } /** * 执行所有网络工具的健康检查 * @param {boolean} force - 是否强制检查(忽略每日限制) * @param {boolean} background - 是否在后台运行 * @returns {Promise} 检查结果列表 */ async checkAllTools(force = false, background = false) { // [日志] 记录健康检查开始 if (configManager && configManager.logAction) { configManager.logAction( `开始工具健康检查 (强制: ${force}, 后台: ${background})`, 'tool_health_check' ); } // [修复] 如果正在检查且不是后台模式,直接返回当前检查的 Promise if (this.isChecking && this.checkPromise) { if (!background) { // 如果已经有检查在进行,返回当前检查的 Promise console.log('[ToolHealthChecker] 检查已在进行中,返回现有 Promise'); if (configManager && configManager.logAction) { configManager.logAction('工具健康检查已在进行中,跳过重复检查', 'tool_health_check'); } return this.checkPromise; } // 后台模式允许继续(但通常不应该发生) } // [修复] 如果不是强制检查,检查今天是否已经检查过 if (!force) { const hasCheckedToday = this.hasCheckedToday(false); const shouldForce = this.shouldForceCheck(); // 如果今天已经检查过且不需要强制检查,直接返回已有结果 if (hasCheckedToday && !shouldForce) { // 返回已有的检查结果 if (this.checkResults && Object.keys(this.checkResults).length > 0) { return Object.values(this.checkResults); } // 如果没有结果,说明检查已完成但结果已清空,不需要重新检查 return []; } } this.isChecking = true; this.isBackgroundChecking = background; // [修复] 如果已有检查结果,保留它们(用于恢复显示) if (!this.checkResults || Object.keys(this.checkResults).length === 0) { this.checkResults = {}; } this.checkStartTime = Date.now(); const allTools = Object.keys(toolRegistry); const networkTools = allTools.filter(toolId => this.isNetworkTool(toolId)); // [修复] 重置进度(每次检查都重新开始,避免重复检查) this.checkProgress = { total: networkTools.length, checked: 0, success: 0, failed: 0 }; // [修复] 如果已有检查结果,先统计已有的结果 if (this.checkResults && Object.keys(this.checkResults).length > 0) { // 如果所有工具都已检查过,直接返回结果 const checkedTools = Object.keys(this.checkResults); if (checkedTools.length === networkTools.length) { console.log('[ToolHealthChecker] 所有工具已检查过,返回已有结果'); // 恢复进度统计 Object.values(this.checkResults).forEach(result => { if (result.status === 'success') { this.checkProgress.success++; } else { this.checkProgress.failed++; } }); this.checkProgress.checked = networkTools.length; this.isChecking = false; return Object.values(this.checkResults); } } // [修复] 创建检查 Promise const checkPromise = this._performCheck(networkTools); this.checkPromise = checkPromise; return checkPromise; } /** * [新增] 执行实际的检查逻辑 * @private */ async _performCheck(networkTools) { const results = []; // 逐个检查工具 for (let i = 0; i < networkTools.length; i++) { const toolId = networkTools[i]; // [修复] 如果这个工具已经检查过,跳过(避免重复检查) if (this.checkResults[toolId]) { console.log(`[ToolHealthChecker] 工具 ${toolId} 已检查过,跳过`); const existingResult = this.checkResults[toolId]; results.push(existingResult); // 更新统计 if (existingResult.status === 'success') { this.checkProgress.success++; } else { this.checkProgress.failed++; } // [优化] 更新进度(确保进度只增不减) const existingChecked = Math.max(this.checkProgress.checked, i + 1); this.checkProgress.checked = existingChecked; this._emitProgress({ ...this.checkProgress, currentTool: toolId, currentToolIndex: i + 1, lastResult: existingResult }); continue; } try { // [优化] 更新进度(确保进度只增不减) let newChecked = Math.max(this.checkProgress.checked, i); this.checkProgress.checked = newChecked; this._emitProgress({ ...this.checkProgress, currentTool: toolId, currentToolIndex: i + 1 }); // 执行健康检查,添加错误处理防止卡住 let result; try { result = await Promise.race([ this.checkToolHealth(toolId), new Promise((_, reject) => setTimeout(() => reject(new Error('健康检查超时')), 30000) ) ]); } catch (checkError) { console.error(`[ToolHealthChecker] 检查工具 ${toolId} 失败:`, checkError); // 检查失败,记录为失败状态 result = { toolId, status: 'failed', reason: checkError.message || '检查异常', error: checkError.message }; // 更新状态管理器 try { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: 'check_error', error: checkError.message, message: '健康检查异常', autoLock: true }); } catch (error) { console.error(`[ToolHealthChecker] 更新健康状态失败 [${toolId}]:`, error); } this.lockedTools.add(toolId); } results.push(result); this.checkResults[toolId] = result; // 更新统计 if (result.status === 'success') { this.checkProgress.success++; } else { this.checkProgress.failed++; } // [优化] 更新进度(确保进度只增不减) newChecked = Math.max(this.checkProgress.checked, i + 1); this.checkProgress.checked = newChecked; this._emitProgress({ ...this.checkProgress, currentTool: toolId, currentToolIndex: i + 1, lastResult: result }); // [重构] 如果是在后台运行,可以稍微延迟,避免阻塞UI if (this.isBackgroundChecking && i < networkTools.length - 1) { await new Promise(resolve => setTimeout(resolve, 100)); } } catch (error) { // 捕获循环中的任何错误,防止整个检查流程卡住 console.error(`[ToolHealthChecker] 处理工具 ${toolId} 时发生错误:`, error); const errorResult = { toolId, status: 'failed', reason: 'processing_error', error: error.message }; results.push(errorResult); this.checkResults[toolId] = errorResult; this.checkProgress.failed++; // [优化] 更新进度(确保进度只增不减) const errorChecked = Math.max(this.checkProgress.checked, i + 1); this.checkProgress.checked = errorChecked; // 继续检查下一个工具 this._emitProgress({ ...this.checkProgress, currentTool: toolId, currentToolIndex: i + 1, lastResult: errorResult }); } } // 保存锁定工具(添加错误处理) try { this.saveLockedTools(); } catch (error) { console.error('[ToolHealthChecker] 保存锁定工具失败:', error); } // 更新检查日期(添加错误处理) try { this.hasCheckedToday(true); } catch (error) { console.error('[ToolHealthChecker] 更新检查日期失败:', error); } // 保存检查结果到数据库(添加错误处理) try { await this._saveCheckResults(results); } catch (error) { console.error('[ToolHealthChecker] 保存检查结果失败:', error); // [日志] 记录保存失败 if (configManager && configManager.logAction) { configManager.logAction(`保存工具健康检查结果失败: ${error.message}`, 'error'); } } // [日志] 记录健康检查完成 if (configManager && configManager.logAction) { const successCount = this.checkProgress.success || 0; const failedCount = this.checkProgress.failed || 0; const totalCount = this.checkProgress.total || 0; configManager.logAction( `工具健康检查完成: 总计 ${totalCount},正常 ${successCount},异常 ${failedCount}`, 'tool_health_check' ); } // [修复] 确保状态正确重置 this.isChecking = false; this.isBackgroundChecking = false; this.currentCheckTool = null; // 触发完成事件 this._emitProgress({ ...this.checkProgress, completed: true }); // [修复] 延迟清除检查 Promise,确保所有等待的 Promise 都能获取结果 setTimeout(() => { this.checkPromise = null; }, 1000); return results; } /** * 保存检查结果到数据库 * @param {Array} results - 检查结果列表 */ async _saveCheckResults(results) { try { const checkDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const checkDateTime = new Date().toISOString(); // 完整的日期时间(用于显示) const checkDuration = this.checkStartTime ? Date.now() - this.checkStartTime : null; const summary = { total: results.length, success: results.filter(r => r.status === 'success').length, failed: results.filter(r => r.status === 'failed').length, checkDate: checkDate }; // [日志] 记录检查汇总信息 if (configManager && configManager.logAction) { const durationText = checkDuration ? `${Math.round(checkDuration / 1000)}秒` : '未知'; configManager.logAction( `工具健康检查汇总: 总计 ${summary.total},正常 ${summary.success},异常 ${summary.failed},耗时 ${durationText}`, 'tool_health_check' ); } // [修复] 更新上次检查日期(使用完整的日期时间字符串) this.lastCheckDate = checkDateTime; // 保存汇总到数据库 if (window.electronAPI && window.electronAPI.saveToolHealthCheckSummary) { await window.electronAPI.saveToolHealthCheckSummary({ checkDate: checkDate, totalTools: summary.total, successCount: summary.success, failedCount: summary.failed, checkDuration: checkDuration }); } // [修复] 保存检查日期到配置(兼容旧系统) try { if (!configManager.config) { configManager.config = {}; } // 保存检查日期(用于兼容旧系统) configManager.config.tool_health_last_check = checkDate; configManager.config.tool_health_check_results = { ...summary, results: results.map(r => ({ toolId: r.toolId, status: r.status, reason: r.reason, httpStatus: r.httpStatus })) }; // 尝试保存到数据库(如果 electronAPI 可用) if (window.electronAPI && window.electronAPI.setConfig) { window.electronAPI.setConfig('tool_health_check_results', configManager.config.tool_health_check_results); window.electronAPI.setConfig('tool_health_last_check', checkDate); } // 保存配置 configManager.saveConfig(); } catch (error) { console.error('[ToolHealthChecker] 保存检查结果到配置失败:', error); } // [修复] 通知主页面更新统计信息 if (window.mainPage && typeof window.mainPage.updateToolHealthStats === 'function') { // 延迟更新,确保数据库操作完成 setTimeout(() => { window.mainPage.updateToolHealthStats(); }, 500); } } catch (error) { console.error('[ToolHealthChecker] 保存检查结果失败:', error); } } /** * 检查工具是否被锁定 * @param {string} toolId - 工具ID * @returns {boolean} 工具是否被锁定 */ isToolLocked(toolId) { const toolStatus = toolStatusManager.getToolStatus(toolId); if (toolStatus.healthStatus === 'unhealthy' || toolStatus.lockReason) { return true; } return this.lockedTools.has(toolId); } /** * 获取健康检查错误消息 * @param {string} reason - 错误原因 * @returns {string} 错误消息 */ getHealthCheckErrorMessage(reason) { const errorMessages = { 'ping_failed': '服务器无法访问(ping 失败)', 'ssl_error': 'SSL证书验证失败', 'timeout': '请求超时', 'network_error': '网络连接失败', 'server_error': '服务器错误', 'http_error': 'HTTP请求失败', 'max_retries': '重试次数超限', 'health_check_failed': '工具健康检查失败' }; return errorMessages[reason] || '工具健康检查失败'; } /** * 解锁工具 * @param {string} toolId - 工具ID */ unlockTool(toolId) { toolStatusManager.updateHealthStatus(toolId, 'healthy', { message: '工具已解锁', autoUnlock: true }); this.lockedTools.delete(toolId); this.saveLockedTools(); } /** * 锁定工具 * @param {string} toolId - 工具ID * @param {string} reason - 锁定原因(可选) */ lockTool(toolId, reason = 'health_check_failed') { toolStatusManager.updateHealthStatus(toolId, 'unhealthy', { reason: reason, message: '工具已锁定', autoLock: true }); this.lockedTools.add(toolId); this.saveLockedTools(); } /** * 获取检查进度 * @returns {Object} 检查进度 */ getProgress() { return { ...this.checkProgress }; } /** * 获取当前检查的工具 * @returns {string|null} 当前检查的工具ID */ getCurrentCheckTool() { return this.currentCheckTool; } /** * [新增] 获取检查结果 * @returns {Object} 检查结果映射 */ getCheckResults() { return this.checkResults ? { ...this.checkResults } : {}; } /** * [新增] 清除检查结果(用于重新开始检查) */ clearCheckResults() { this.checkResults = {}; this.checkProgress = { total: 0, checked: 0, success: 0, failed: 0 }; } } export default new ToolHealthChecker();