1230 lines
48 KiB
JavaScript
1230 lines
48 KiB
JavaScript
// 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();
|