// ==UserScript== // @name 脚本健康度看板 (Script Health Dashboard) // @namespace https://github.com/scriptcat/script-health-dashboard // @version 0.2.0 // @description 后台监控所有已安装脚本的运行状态,提供健康度评分、历史趋势、智能告警看板(第二阶段:评分引擎+告警+历史) // @author ScriptCat User // @background // @crontab 0 * * * * // @grant GM_info // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_log // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_openInTab // @license MIT // ==/UserScript== (async function () { 'use strict'; // ============================================================ // 常量定义区 // ============================================================ // 存储键命名规范: // SHD:stats:{scriptId} - 单个脚本的运行统计数据 // SHD:report:latest - 最新一次健康报告 // SHD:report:history - 健康报告历史记录(数组,最多100条) // SHD:alert:{scriptId} - 单个脚本的最后告警时间戳 // SHD:self:meta - 本看板脚本自身元数据 const STORAGE_PREFIX_STATS = 'SHD:stats:'; // 脚本统计数据前缀 const STORAGE_PREFIX_ALERT = 'SHD:alert:'; // 告警去重前缀 const STORAGE_KEY_REPORT = 'SHD:report:latest'; // 最新健康报告存储键 const STORAGE_KEY_HISTORY = 'SHD:report:history'; // 历史报告存储键 const STATS_PREFIX_LENGTH = STORAGE_PREFIX_STATS.length; // 本看板脚本自身的唯一标识(用于自身上报) const SELF_SCRIPT_ID = 'script-health-dashboard'; const SELF_SCRIPT_NAME = '脚本健康度看板'; // 历史记录最大保留条数 const MAX_HISTORY_SIZE = 100; // 告警去重时间窗口(毫秒):1 小时 const ALERT_DEDUP_WINDOW_MS = 60 * 60 * 1000; // 健康等级常量 const HEALTH_LEVEL = { HEALTHY: 'healthy', // 健康:评分 >= 80 WARNING: 'warning', // 警告:评分 50 ~ 80 CRITICAL: 'critical' // 严重:评分 < 50 }; // 健康判定阈值(基于评分) const THRESHOLD_SCORE_HEALTHY = 80; // 评分 >= 80 为健康 const THRESHOLD_SCORE_WARNING = 50; // 评分 >= 50 为警告 // 【第二阶段新增】健康度评分扣分阈值 const SCORE_RULES = { // 成功率扣分 SUCCESS_RATE_CRITICAL: 0.50, // 成功率低于 50% SUCCESS_RATE_WARNING: 0.80, // 成功率低于 80% DEDUCT_SUCCESS_RATE_CRITICAL: 35,// 低于 50% 共扣 35 分 DEDUCT_SUCCESS_RATE_WARNING: 20, // 低于 80% 扣 20 分 // 最后执行时间扣分 LAST_EXEC_SEVERE_DAYS: 30, // 超过 30 天未执行 LAST_EXEC_WARNING_DAYS: 7, // 超过 7 天未执行 DEDUCT_LAST_EXEC_SEVERE: 25, // 超过 30 天共扣 25 分 DEDUCT_LAST_EXEC_WARNING: 10, // 超过 7 天扣 10 分 // 平均耗时扣分 AVG_DURATION_SEVERE_MS: 10000, // 超过 10 秒 AVG_DURATION_WARNING_MS: 5000, // 超过 5 秒 DEDUCT_AVG_DURATION_SEVERE: 10, // 超过 10 秒共扣 10 分 DEDUCT_AVG_DURATION_WARNING: 5, // 超过 5 秒扣 5 分 // 版本状态扣分 DEDUCT_OUTDATED_VERSION: 10 // 有新版本可更新扣 10 分 }; // 一天的毫秒数(用于天数计算) const ONE_DAY_MS = 24 * 60 * 60 * 1000; // ============================================================ // 日志工具函数 // ============================================================ /** * 统一日志输出(通过 GM_log) * @param {string} message - 日志内容 * @param {'info'|'warn'|'error'} level - 日志级别 */ function log(message, level = 'info') { try { // ScriptCat 的 GM_log 支持 level 参数 GM_log(message, level); } catch (e) { // 降级:如果 GM_log 不可用则使用 console console.log(`[SHD][${level}] ${message}`); } } // ============================================================ // 存储工具函数 // ============================================================ /** * 读取存储值(带 JSON 解析与异常保护) * @param {string} key - 存储键 * @param {*} defaultValue - 默认值 * @returns {*} 解析后的值或默认值 */ function safeGetValue(key, defaultValue = null) { try { const raw = GM_getValue(key, null); if (raw === null || raw === undefined) { return defaultValue; } // ScriptCat 的 GM_getValue 会自动反序列化对象,此处做一层字符串保护 if (typeof raw === 'string') { try { return JSON.parse(raw); } catch (e) { return raw; } } return raw; } catch (e) { log(`读取存储失败 [${key}]: ${e.message}`, 'error'); return defaultValue; } } /** * 写入存储值(带日志记录) * @param {string} key - 存储键 * @param {*} value - 要存储的值 * @param {boolean} [silent=false] - 是否静默写入(不打印日志) */ function safeSetValue(key, value, silent = false) { try { GM_setValue(key, value); if (!silent) { log(`数据已写入存储 [${key}]`, 'info'); } } catch (e) { log(`写入存储失败 [${key}]: ${e.message}`, 'error'); } } /** * 删除存储值(带日志记录) * @param {string} key - 存储键 */ function safeDeleteValue(key) { try { GM_deleteValue(key); log(`数据已从存储删除 [${key}]`, 'info'); } catch (e) { log(`删除存储失败 [${key}]: ${e.message}`, 'error'); } } // ============================================================ // 默认统计数据生成 // ============================================================ /** * 生成某个脚本的默认统计数据结构(用于首次发现或数据迁移) * @param {string} scriptId - 脚本唯一标识 * @param {string} [name=''] - 脚本名称 * @param {string} [version=''] - 脚本版本 * @returns {Object} 默认统计数据对象 */ function createDefaultStats(scriptId, name = '', version = '') { return { scriptId: scriptId, // 脚本唯一标识 name: name || scriptId, // 脚本名称 version: version, // 脚本版本号 totalExecutions: 0, // 总执行次数 successCount: 0, // 成功执行次数 failureCount: 0, // 失败执行次数(= totalExecutions - successCount) totalDuration: 0, // 累计耗时(毫秒) lastExecTime: 0, // 最后执行时间戳(毫秒) firstSeenTime: Date.now(), // 首次发现时间戳 lastUpdated: Date.now(), // 数据最后更新时间戳 latestVersion: version, // 【第二阶段新增】已知最新版本(用于版本对比) hasUpdate: false // 【第二阶段新增】是否有新版本可更新 }; } // ============================================================ // 数据采集核心函数 // ============================================================ /** * 采集所有已注册脚本的统计数据 * * 采用"反向注册"策略:本看板通过 GM_listValues 遍历所有存储键, * 筛选出以 SHD:stats: 开头的键来识别所有已上报统计数据的脚本。 * 对尚无统计数据或字段缺失的脚本进行初始化 / 数据迁移。 * * @returns {Promise<{statsList: Array, totalCount: number, migratedCount: number}>} */ async function collectScriptsInfo() { log('====== 开始采集脚本数据 ======', 'info'); const statsList = []; let migratedCount = 0; try { // 第一步:获取所有存储键 const allKeys = GM_listValues(); log(`存储中共有 ${allKeys.length} 个键`, 'info'); // 第二步:筛选出脚本统计数据键 const statsKeys = allKeys.filter(key => key.startsWith(STORAGE_PREFIX_STATS)); log(`发现 ${statsKeys.length} 个脚本统计记录`, 'info'); // 第三步:逐个读取并做数据迁移 / 兼容处理 for (const key of statsKeys) { const scriptId = key.substring(STATS_PREFIX_LENGTH); let stats = safeGetValue(key, null); if (!stats) { // 场景A:数据不存在(可能被手动删除),初始化默认值 log(`脚本 [${scriptId}] 无统计数据,初始化默认值`, 'warn'); stats = createDefaultStats(scriptId); safeSetValue(key, stats, true); migratedCount++; } else { // 场景B:数据存在,做字段完整性检查(向前兼容旧版本数据) let needMigrate = false; const defaults = createDefaultStats(scriptId); // 必须存在的字段列表(含第二阶段新增字段) const requiredFields = [ 'scriptId', 'name', 'version', 'totalExecutions', 'successCount', 'failureCount', 'totalDuration', 'lastExecTime', 'firstSeenTime', 'lastUpdated', 'latestVersion', 'hasUpdate' ]; for (const field of requiredFields) { if (stats[field] === undefined || stats[field] === null) { log(`脚本 [${scriptId}] 缺失字段 [${field}],执行数据迁移`, 'warn'); stats[field] = defaults[field]; needMigrate = true; } } // 如果发生迁移,更新时间戳并写回存储 if (needMigrate) { stats.lastUpdated = Date.now(); safeSetValue(key, stats, true); migratedCount++; } } statsList.push(stats); } log(`====== 采集完成:共 ${statsList.length} 个脚本,迁移 ${migratedCount} 个 ======`, 'info'); } catch (e) { log(`采集脚本数据时发生异常: ${e.message}`, 'error'); } return { statsList: statsList, totalCount: statsList.length, migratedCount: migratedCount }; } // ============================================================ // 【第二阶段新增】健康度评分引擎 // ============================================================ /** * 计算单个脚本的健康度评分(满分 100 分) * * 评分规则(扣分制,从 100 分开始扣): * 1. 成功率扣分: * - 总执行次数 > 0 时: * · 成功率 < 80% → 扣 20 分 * · 成功率 < 50% → 额外再扣 15 分(共扣 35 分) * 2. 最后执行时间扣分: * - 超过 7 天未执行 → 扣 10 分 * - 超过 30 天未执行 → 额外再扣 15 分(共扣 25 分) * 3. 平均耗时扣分: * - 平均耗时 > 5 秒 → 扣 5 分 * - 平均耗时 > 10 秒 → 额外再扣 5 分(共扣 10 分) * 4. 版本状态扣分: * - 有新版本可更新(hasUpdate=true)→ 扣 10 分 * * 边界处理: * - 从未执行过的脚本(totalExecutions=0):成功率不扣分,但 lastExecTime 为 0 时 * 视为"从未执行",按超时最严重档扣分 * - 最终分数限制在 [0, 100] 区间 * * @param {Object} stats - 脚本统计数据 * @returns {{score: number, level: string, successRate: number, avgDuration: number, deductDetails: Object}} */ function calculateHealthScore(stats) { // 起始满分 let score = 100; // 扣分明细(用于调试与看板展示) const deductDetails = { successRate: 0, lastExec: 0, avgDuration: 0, version: 0 }; // ---- 1. 成功率扣分 ---- const total = stats.totalExecutions || 0; const success = stats.successCount || 0; // 无执行记录视为成功率 100%(不扣分),避免新脚本被误判 const successRate = total > 0 ? success / total : 1; const avgDuration = total > 0 ? Math.round(stats.totalDuration / total) : 0; if (total > 0) { if (successRate < SCORE_RULES.SUCCESS_RATE_CRITICAL) { // 低于 50%:共扣 35 分 deductDetails.successRate = SCORE_RULES.DEDUCT_SUCCESS_RATE_CRITICAL; } else if (successRate < SCORE_RULES.SUCCESS_RATE_WARNING) { // 低于 80%:扣 20 分 deductDetails.successRate = SCORE_RULES.DEDUCT_SUCCESS_RATE_WARNING; } } score -= deductDetails.successRate; // ---- 2. 最后执行时间扣分 ---- // lastExecTime = 0 表示从未执行过,按最严重档处理 const now = Date.now(); let daysSinceLastExec; if (stats.lastExecTime && stats.lastExecTime > 0) { daysSinceLastExec = (now - stats.lastExecTime) / ONE_DAY_MS; } else { // 从未执行:视为无限大天数(触发最严重档) daysSinceLastExec = Infinity; } if (daysSinceLastExec > SCORE_RULES.LAST_EXEC_SEVERE_DAYS) { // 超过 30 天:共扣 25 分 deductDetails.lastExec = SCORE_RULES.DEDUCT_LAST_EXEC_SEVERE; } else if (daysSinceLastExec > SCORE_RULES.LAST_EXEC_WARNING_DAYS) { // 超过 7 天:扣 10 分 deductDetails.lastExec = SCORE_RULES.DEDUCT_LAST_EXEC_WARNING; } score -= deductDetails.lastExec; // ---- 3. 平均耗时扣分 ---- if (avgDuration > SCORE_RULES.AVG_DURATION_SEVERE_MS) { // 超过 10 秒:共扣 10 分 deductDetails.avgDuration = SCORE_RULES.DEDUCT_AVG_DURATION_SEVERE; } else if (avgDuration > SCORE_RULES.AVG_DURATION_WARNING_MS) { // 超过 5 秒:扣 5 分 deductDetails.avgDuration = SCORE_RULES.DEDUCT_AVG_DURATION_WARNING; } score -= deductDetails.avgDuration; // ---- 4. 版本状态扣分 ---- if (stats.hasUpdate === true) { // 有新版本可更新:扣 10 分 deductDetails.version = SCORE_RULES.DEDUCT_OUTDATED_VERSION; } score -= deductDetails.version; // ---- 分数限制在 [0, 100] ---- score = Math.max(0, Math.min(100, score)); // ---- 根据分数划分状态等级 ---- let level; if (score >= THRESHOLD_SCORE_HEALTHY) { level = HEALTH_LEVEL.HEALTHY; } else if (score >= THRESHOLD_SCORE_WARNING) { level = HEALTH_LEVEL.WARNING; } else { level = HEALTH_LEVEL.CRITICAL; } return { score: score, level: level, successRate: successRate, avgDuration: avgDuration, deductDetails: deductDetails }; } // ============================================================ // 健康度计算函数(旧版兼容包装,第一阶段接口) // ============================================================ /** * 根据统计数据计算单个脚本的健康等级 * 第二阶段起,内部改为调用 calculateHealthScore * * @param {Object} stats - 脚本统计数据 * @returns {{level: string, successRate: number, avgDuration: number, score: number, deductDetails: Object}} */ function calculateHealth(stats) { const result = calculateHealthScore(stats); return { level: result.level, successRate: result.successRate, avgDuration: result.avgDuration, score: result.score, deductDetails: result.deductDetails }; } // ============================================================ // 健康报告生成函数 // ============================================================ /** * 生成健康报告并存储到 SHD:report:latest * @param {Array} statsList - 所有脚本统计数据列表 * @returns {Object} 生成的健康报告对象 */ function generateHealthReport(statsList) { let healthyCount = 0; let warningCount = 0; let criticalCount = 0; let totalScore = 0; // 构建各脚本详情列表 const details = statsList.map(stats => { const health = calculateHealth(stats); totalScore += health.score; // 统计各等级数量 if (health.level === HEALTH_LEVEL.HEALTHY) { healthyCount++; } else if (health.level === HEALTH_LEVEL.WARNING) { warningCount++; } else { criticalCount++; } return { scriptId: stats.scriptId, name: stats.name, version: stats.version, totalExecutions: stats.totalExecutions, successCount: stats.successCount, failureCount: stats.failureCount, successRate: parseFloat(health.successRate.toFixed(4)), avgDuration: health.avgDuration, totalDuration: stats.totalDuration, lastExecTime: stats.lastExecTime, healthLevel: health.level, healthScore: health.score, // 【第二阶段新增】健康度评分 deductDetails: health.deductDetails, // 【第二阶段新增】扣分明细 hasUpdate: stats.hasUpdate === true // 【第二阶段新增】是否有更新 }; }); // 计算平均健康度评分 const avgScore = statsList.length > 0 ? parseFloat((totalScore / statsList.length).toFixed(2)) : 100; // 构建完整报告对象 const report = { timestamp: Date.now(), // 报告生成时间戳 totalScripts: statsList.length, // 总脚本数 healthyCount: healthyCount, // 健康脚本数 warningCount: warningCount, // 警告脚本数 criticalCount: criticalCount, // 严重脚本数 avgScore: avgScore, // 【第二阶段新增】平均健康度评分 details: details // 各脚本详情列表 }; // 存储最新健康报告(静默写入,避免日志过多) safeSetValue(STORAGE_KEY_REPORT, report, true); log( `健康报告已生成:总 ${report.totalScripts} 个脚本,平均分 ${avgScore},` + `健康 ${healthyCount} / 警告 ${warningCount} / 严重 ${criticalCount}`, 'info' ); return report; } // ============================================================ // 【第二阶段新增】历史记录管理 // ============================================================ /** * 追加一条历史记录到 SHD:report:history * 保留最近 MAX_HISTORY_SIZE 条,超出时自动删除最旧记录 * * @param {Object} report - 最新生成的健康报告 */ function appendHistoryRecord(report) { try { // 构建精简的历史记录条目(不包含 details,避免存储膨胀) const historyEntry = { timestamp: report.timestamp, totalScripts: report.totalScripts, healthy: report.healthyCount, warning: report.warningCount, critical: report.criticalCount, avgScore: report.avgScore }; // 读取现有历史记录 const history = safeGetValue(STORAGE_KEY_HISTORY, []); if (!Array.isArray(history)) { log('历史记录格式异常,重置为空数组', 'warn'); history.length = 0; } // 追加新记录 history.push(historyEntry); // 超出最大保留条数时,删除最旧记录 while (history.length > MAX_HISTORY_SIZE) { history.shift(); } // 写回存储 safeSetValue(STORAGE_KEY_HISTORY, history, true); log(`历史记录已追加(当前共 ${history.length} 条,上限 ${MAX_HISTORY_SIZE} 条)`, 'info'); } catch (e) { log(`追加历史记录失败: ${e.message}`, 'error'); } } // ============================================================ // 【第二阶段新增】智能告警触发 // ============================================================ /** * 检查并发送严重告警 * 规则: * - 仅对 healthLevel === 'critical' 的脚本发送通知 * - 同一脚本在 1 小时内不重复告警(基于 SHD:alert:{scriptId} 记录) * - 通知内容包含脚本名称和当前健康度分数 * * @param {Array} details - 健康报告中的脚本详情列表 */ function checkAndAlert(details) { try { const now = Date.now(); let alertCount = 0; for (const detail of details) { if (detail.healthLevel !== HEALTH_LEVEL.CRITICAL) { continue; } // 检查去重窗口 const alertKey = STORAGE_PREFIX_ALERT + detail.scriptId; const lastAlertTime = safeGetValue(alertKey, 0); if (lastAlertTime && (now - lastAlertTime) < ALERT_DEDUP_WINDOW_MS) { // 在去重窗口内,跳过本次告警 const remainMin = Math.ceil((ALERT_DEDUP_WINDOW_MS - (now - lastAlertTime)) / 60000); log( `脚本 [${detail.name}] 处于告警去重窗口内(剩余 ${remainMin} 分钟),跳过告警`, 'info' ); continue; } // 发送严重告警通知 const title = `⚠️ 脚本健康告警:${detail.name}`; const text = `健康度评分:${detail.healthScore} / 100(严重)\n` + `总执行次数:${detail.totalExecutions}\n` + `成功率:${(detail.successRate * 100).toFixed(1)}%\n` + `平均耗时:${detail.avgDuration} ms\n` + `请检查该脚本是否正常运行。`; GM_notification({ title: title, text: text, onclick: () => { log(`用户点击了 [${detail.name}] 的告警通知`, 'info'); } }); // 更新该脚本的最后告警时间 safeSetValue(alertKey, now, true); log(`已发送严重告警:[${detail.name}] 评分=${detail.healthScore}`, 'warn'); alertCount++; } if (alertCount > 0) { log(`本次共发送 ${alertCount} 条严重告警通知`, 'warn'); } else { log('本次无需发送严重告警', 'info'); } } catch (e) { log(`智能告警检查失败: ${e.message}`, 'error'); } } // ============================================================ // 主健康检查流程 // ============================================================ /** * 执行完整的健康检查流程 * 流程:采集数据 → 计算健康度评分 → 生成报告 → 追加历史记录 → 触发告警 * * @returns {Promise<{totalScripts: number, healthy: number, warning: number, critical: number, avgScore: number}>} */ async function runHealthCheck() { log('>>>>>> 健康检查流程启动 <<<<<<', 'info'); // 第一步:采集脚本数据 const { statsList } = await collectScriptsInfo(); // 第二步:生成健康报告(内部会计算评分并存储) const report = generateHealthReport(statsList); // 第三步:【第二阶段新增】追加历史记录 appendHistoryRecord(report); // 第四步:【第二阶段新增】触发智能告警 checkAndAlert(report.details); log('>>>>>> 健康检查流程完成 <<<<<<', 'info'); return { totalScripts: report.totalScripts, healthy: report.healthyCount, warning: report.warningCount, critical: report.criticalCount, avgScore: report.avgScore }; } // ============================================================ // 通知工具函数 // ============================================================ /** * 发送桌面通知 * @param {string} title - 通知标题 * @param {string} text - 通知正文 * @param {function} [onclick] - 点击回调 */ function sendNotification(title, text, onclick) { try { GM_notification({ title: title, text: text, onclick: onclick || (() => {}) }); } catch (e) { log(`发送通知失败: ${e.message}`, 'error'); } } // ============================================================ // 菜单命令注册 // ============================================================ /** * 菜单命令①:查看健康度看板 * 第二阶段仍仅弹出提示,后续阶段将打开可视化看板页面 */ GM_registerMenuCommand('📊 查看健康度看板', () => { log('用户点击菜单:查看健康度看板', 'info'); // 读取最新报告,展示简要信息 const report = safeGetValue(STORAGE_KEY_REPORT, null); if (report) { sendNotification( '📊 脚本健康度看板', `可视化看板开发中,敬请期待 🚧\n\n` + `最近一次检查(${new Date(report.timestamp).toLocaleString()}):\n` + `总脚本数:${report.totalScripts}\n` + `平均评分:${report.avgScore} / 100\n` + `健康:${report.healthyCount} | 警告:${report.warningCount} | 严重:${report.criticalCount}` ); } else { sendNotification( '脚本健康度看板', '看板开发中,敬请期待 🚧\n暂无检查记录,请先执行一次健康检查。' ); } }); /** * 菜单命令②:立即执行健康检查 * 手动触发一次完整的健康检查流程,并发送结果通知 */ GM_registerMenuCommand('🔄 立即执行健康检查', async () => { log('用户点击菜单:立即执行健康检查', 'info'); sendNotification('健康检查', '⏳ 正在执行健康检查,请稍候...'); try { const result = await runHealthCheck(); // 根据结果选择通知图标 const icon = result.critical > 0 ? '⚠️' : (result.warning > 0 ? '🔔' : '✅'); sendNotification( `${icon} 健康检查完成`, `共监控 ${result.totalScripts} 个脚本\n` + `平均评分:${result.avgScore} / 100\n` + `健康: ${result.healthy} | 警告: ${result.warning} | 严重: ${result.critical}` ); } catch (e) { log(`健康检查执行失败: ${e.message}`, 'error'); sendNotification('❌ 健康检查失败', `执行过程中发生错误: ${e.message}`); } }); // ============================================================ // 【第二阶段新增】为其他脚本提供的上报接口 // ============================================================ /** * 脚本统计数据上报接口 * * 接收脚本ID和统计数据对象,自动合并更新到存储中。 * 其他脚本可通过本函数(或按相同逻辑直接调用 GM_setValue)上报运行数据。 * * @param {string} scriptId - 脚本唯一标识(建议使用脚本名称或命名空间哈希) * @param {Object} stats - 统计数据对象,支持字段: * - {string} [name] - 脚本名称 * - {number} [totalExecutions] - 总执行次数 * - {number} [successCount] - 成功次数 * - {number} [totalDuration] - 累计耗时(毫秒) * - {number} [lastExecTime] - 最后执行时间戳 * - {string} [version] - 当前版本 * - {string} [latestVersion] - 已知最新版本(用于版本对比) * - {boolean} [hasUpdate] - 是否有新版本可更新 * * 合并策略:传入字段覆盖原有字段,未传入字段保持不变。 */ function reportScriptStats(scriptId, stats) { if (!scriptId) { log('reportScriptStats: 缺少 scriptId 参数', 'error'); return; } if (!stats || typeof stats !== 'object') { log(`reportScriptStats: 脚本 [${scriptId}] 的 stats 参数无效`, 'error'); return; } const key = STORAGE_PREFIX_STATS + scriptId; let existing = safeGetValue(key, null); if (!existing) { // 首次上报,初始化默认结构 existing = createDefaultStats( scriptId, stats.name || '', stats.version || '' ); log(`脚本 [${scriptId}] 首次上报,已创建统计记录`, 'info'); } // 合并更新传入的字段(覆盖式更新) if (stats.name !== undefined) existing.name = stats.name; if (stats.version !== undefined) existing.version = stats.version; if (stats.totalExecutions !== undefined) existing.totalExecutions = stats.totalExecutions; if (stats.successCount !== undefined) existing.successCount = stats.successCount; if (stats.totalDuration !== undefined) existing.totalDuration = stats.totalDuration; if (stats.lastExecTime !== undefined) existing.lastExecTime = stats.lastExecTime; if (stats.latestVersion !== undefined) existing.latestVersion = stats.latestVersion; if (stats.hasUpdate !== undefined) existing.hasUpdate = stats.hasUpdate === true; // 派生字段:失败次数 = 总次数 - 成功次数 existing.failureCount = (existing.totalExecutions || 0) - (existing.successCount || 0); if (existing.failureCount < 0) existing.failureCount = 0; existing.lastUpdated = Date.now(); safeSetValue(key, existing, true); log(`脚本 [${scriptId}] 数据已上报更新`, 'info'); } /** * 【内部使用】本看板脚本自身上报运行数据(以身作则) * 每次启动 / 每小时定时执行时调用一次 * * @param {boolean} success - 本次执行是否成功 * @param {number} duration - 本次执行耗时(毫秒) */ function reportSelfStats(success, duration) { try { const key = STORAGE_PREFIX_STATS + SELF_SCRIPT_ID; let self = safeGetValue(key, null); if (!self) { self = createDefaultStats(SELF_SCRIPT_ID, SELF_SCRIPT_NAME, GM_info.script.version); } // 累计执行统计 self.totalExecutions = (self.totalExecutions || 0) + 1; if (success) { self.successCount = (self.successCount || 0) + 1; } self.failureCount = self.totalExecutions - self.successCount; self.totalDuration = (self.totalDuration || 0) + (duration || 0); self.lastExecTime = Date.now(); self.version = GM_info.script.version; self.latestVersion = GM_info.script.version; self.hasUpdate = false; // 自身版本信息暂无法自动检测 self.lastUpdated = Date.now(); safeSetValue(key, self, true); log(`本看板自身执行数据已上报:第 ${self.totalExecutions} 次执行`, 'info'); } catch (e) { log(`自身数据上报失败: ${e.message}`, 'error'); } } // ============================================================ // 调试接口暴露 // ============================================================ // 将核心函数暴露到全局,方便在浏览器控制台调试 // 注意:后台脚本环境可能无 window 对象,用 try-catch 保护 try { window.SC_HEALTH_DASHBOARD = { version: GM_info.script.version, collectScriptsInfo, runHealthCheck, reportScriptStats, // 【第二阶段】对外暴露上报接口 reportSelfStats, generateHealthReport, calculateHealth, calculateHealthScore, // 【第二阶段】暴露评分引擎 appendHistoryRecord, checkAndAlert, log }; } catch (e) { // 后台脚本环境无 window 对象,忽略此错误 } // ============================================================ // 脚本启动入口 / Crontab 定时任务执行体 // ============================================================ log('========================================', 'info'); log(`脚本健康度看板 v${GM_info.script.version} 已启动`, 'info'); log('运行模式:后台脚本 | 定时任务:每小时执行一次', 'info'); log('========================================', 'info'); // 记录本次执行起始时间(用于自身耗时统计) const execStartTime = Date.now(); // 执行完整的健康检查流程(启动时 + 每小时 crontab 触发时) try { const result = await runHealthCheck(); log(`健康检查完成:共 ${result.totalScripts} 个脚本,平均分 ${result.avgScore}`, 'info'); // 自身执行成功,上报本看板运行数据(以身作则) const execDuration = Date.now() - execStartTime; reportSelfStats(true, execDuration); } catch (e) { log(`健康检查执行异常: ${e.message}`, 'error'); // 即使失败也上报(标记为失败) const execDuration = Date.now() - execStartTime; reportSelfStats(false, execDuration); } // ============================================================ // Crontab 定时任务说明 // ============================================================ // 当 @crontab "0 * * * *" 触发时,ScriptCat 会重新执行本后台脚本。 // 因此上面的"启动入口"代码即为定时任务的执行体: // 1. 执行 runHealthCheck() 进行完整健康检查 // - 采集所有脚本数据 // - 逐一计算健康度评分 // - 生成汇总报告 // - 追加历史记录(保留最近 100 条) // - 触发严重告警通知(1 小时去重) // 2. 上报本看板自身的运行数据 // 每小时自动执行一次。 })();