Files
YMhut-box-C-/box-old/src/js/toolHealthChecker.js
T
QWQLwToo 46a3674381
build-winui / winui (push) Has been cancelled
Add legacy Electron app
2026-06-26 13:29:02 +08:00

1230 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<boolean>} 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<Object>} 检查结果
*/
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<Array>} 检查结果列表
*/
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();