// +---------------------------------------------------------------------- /** * 插件健康检查命令(合并版) * * 融合原 addon:health(人工排查 + --fix 自动修复)与 addon:health-check * (系统环境 / 缓存系统检查 + JSON/HTML 输出 + 告警 + 报告存档)的能力。 * * php think addon:health # 系统级 + 全部插件健康检查 * php think addon:health # 单独检查某个插件(含详情) * php think addon:health --detail # 显示详细信息 * php think addon:health --fix # 尝试自动修复问题(清缓存/修权限) * php think addon:health --output=json # 输出 JSON(机器消费) * php think addon:health --output=html # 输出 HTML 报告 * php think addon:health --send-alert # 异常时发送告警 * php think addon:health --webhook=URL # 告警 Webhook 地址 * php think addon:health --email=ADDR # 告警邮箱 * php think addon:health --save-report # 存档检查报告(runtime 保留 30 天) */ declare(strict_types=1); namespace ywxapp\command; use think\console\Command; use think\console\Input; use think\console\input\Argument; use think\console\input\Option; use think\console\Output; use think\facade\Log; use think\facade\Config; use think\facade\Cache; use ywxapp\service\AddonPerformanceMonitor; /** * AddonHealth 类 * * @author ywxapp */ class AddonHealth extends Command { /** * 配置命令 */ protected function configure() { $this->setName('addon:health') ->addArgument('addon', Argument::OPTIONAL, '插件名称(留空则检查全部插件 + 系统环境)') ->addOption('detail', 'd', Option::VALUE_NONE, '显示详细信息') ->addOption('fix', 'f', Option::VALUE_NONE, '尝试自动修复问题(清缓存 / 修权限)') ->addOption('output', 'o', Option::VALUE_OPTIONAL, '输出格式 (text, json, html)', 'text') ->addOption('send-alert', 'a', Option::VALUE_NONE, '异常时发送告警通知') ->addOption('webhook', 'w', Option::VALUE_OPTIONAL, '告警 Webhook URL') ->addOption('email', 'e', Option::VALUE_OPTIONAL, '告警邮箱') ->addOption('save-report', 's', Option::VALUE_NONE, '保存检查报告') ->setDescription('插件健康检查(含系统环境 / 缓存 / 文件 / 性能 / 依赖 / 告警)'); } /** * 执行命令 * * @param Input $input 输入 * @param Output $output 输出 * @return int 退出码(0 健康 / 1 异常) */ protected function execute(Input $input, Output $output): int { $addon = $input->getArgument('addon'); $detail = $input->getOption('detail'); $fix = $input->getOption('fix'); $outputFormat = $input->getOption('output') ?: 'text'; $sendAlert = $input->getOption('send-alert'); $webhookUrl = $input->getOption('webhook'); $email = $input->getOption('email'); $saveReport = $input->getOption('save-report'); // 仅文本模式打印标题横幅,避免污染 JSON/HTML 输出(JSON/HTML 模式下保持纯输出) if ($outputFormat === 'text') { $output->writeln('=== 插件健康检查 ==='); $output->writeln(''); } // 构建检查报告(始终含系统环境 + 缓存系统检查) $report = $this->buildReport($addon, $detail); // 按格式输出 switch ($outputFormat) { case 'json': $this->outputJson($report, $output); break; case 'html': $this->outputHtml($report, $output); break; default: $this->outputText($report, $output); // 单插件 + --detail 时,补充版本 / 作者 / 依赖等详细信息 if ($addon && $detail) { $this->outputSingleDetail($addon, $report['addon'][$addon] ?? [], $output); } break; } // 自动修复 if ($fix) { $this->runAutoFix($report, $output); } // 发送告警 if ($sendAlert && !$report['healthy']) { $this->sendAlert($report, $webhookUrl, $email, $output); } // 保存报告 if ($saveReport) { $this->saveReport($report, $output); } return $report['healthy'] ? 0 : 1; } /** * 构建健康检查报告 */ private function buildReport(?string $addon, bool $detail): array { $report = [ 'healthy' => true, 'timestamp' => time(), 'checks' => [], 'issues' => [], 'warnings' => [], 'addon' => [] ]; // 1/2. 系统环境 + 缓存系统(始终检查,属全局健康) $report['checks']['system'] = $this->checkSystemEnvironment(); $report['checks']['cache'] = $this->checkCacheSystem(); if ($addon) { // 单插件深检 $report['addon'][$addon] = $this->getAddonHealth($addon); // --detail 时把性能采样并入报告,使 JSON/HTML 输出也包含详情 // (version/author/dependencies 等已在 getAddonHealth 返回中,此处补性能) if ($detail && !empty($report['addon'][$addon]['enabled'])) { $stats = AddonPerformanceMonitor::getPerformanceStats($addon); if ($stats['total_calls'] > 0) { $report['addon'][$addon]['performance'] = $stats; } } } else { // 全量:补充文件 / 性能 / 依赖 / 状态聚合检查 $report['checks']['files'] = $this->checkAddonFiles(); $report['checks']['performance'] = $this->checkAddonPerformance(); $report['checks']['dependencies'] = $this->checkDependencies(); $report['checks']['status'] = $this->checkaddontatus(); $addon = Config::get('addon', []); foreach ($addon as $a) { if (!isset($a['name'])) { continue; } $report['addon'][$a['name']] = $this->getAddonHealth($a['name']); } } // 聚合 checks 中的问题 / 警告 foreach ($report['checks'] as $checkResult) { if (!$checkResult['healthy']) { $report['healthy'] = false; $report['issues'] = array_merge($report['issues'], $checkResult['issues']); } if (!empty($checkResult['warnings'])) { $report['warnings'] = array_merge($report['warnings'], $checkResult['warnings']); } } // 插件个体健康状态影响整体 foreach ($report['addon'] as $health) { if (!$health['healthy']) { $report['healthy'] = false; } } return $report; } /** * 获取单个插件健康状态(丰富信息:目录 / 文件 / info / 依赖 / 扩展 / 主类 / 性能) */ private function getAddonHealth(string $addon): array { $health = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'enabled' => false, 'install_time' => null ]; $addonPath = ADDON_PATH . $addon; // 检查插件目录 if (!is_dir($addonPath)) { $health['healthy'] = false; $health['issues'][] = "插件目录不存在: {$addonPath}"; return $health; } // 检查必需文件 $requiredFiles = ['info.php', 'Addon.php']; foreach ($requiredFiles as $file) { if (!is_file($addonPath . DIRECTORY_SEPARATOR . $file)) { $health['healthy'] = false; $health['issues'][] = "缺少必需文件: {$file}"; } } // 检查 info.php $infoFile = $addonPath . DIRECTORY_SEPARATOR . 'info.php'; if (is_file($infoFile)) { $info = include $infoFile; $health['enabled'] = ($info['state'] ?? false) == 1; $health['install_time'] = $info['install_time'] ?? null; $health['version'] = $info['version'] ?? null; $health['author'] = $info['author'] ?? null; $health['description'] = $info['intro'] ?? null; // 检查依赖插件 if (isset($info['dependencies']) && !empty($info['dependencies'])) { $health['dependencies'] = $info['dependencies']; foreach ($info['dependencies'] as $dep) { if (!$this->isAddonInstalled($dep)) { $health['healthy'] = false; $health['issues'][] = "依赖插件 {$dep} 未安装"; } elseif (!$this->isAddonEnabled($dep)) { $health['healthy'] = false; $health['issues'][] = "依赖插件 {$dep} 未启用"; } } } // 检查 PHP 扩展 if (isset($info['extensions']) && !empty($info['extensions'])) { $health['extensions'] = $info['extensions']; foreach ($info['extensions'] as $ext) { if (!extension_loaded($ext)) { $health['healthy'] = false; $health['issues'][] = "缺少PHP扩展: {$ext}"; } } } } // 检查插件主类与必需方法 $addonClass = '\\addon\\' . $addon . '\\Addon'; if (class_exists($addonClass)) { try { $instance = app()->make($addonClass); foreach (['install', 'uninstall'] as $method) { if (!method_exists($instance, $method)) { $health['healthy'] = false; $health['issues'][] = "缺少必需方法: {$method}()"; } } } catch (\Exception $e) { $health['healthy'] = false; $health['issues'][] = "插件类初始化失败: " . $e->getMessage(); } } // 性能检查 if ($health['enabled']) { $stats = AddonPerformanceMonitor::getPerformanceStats($addon); if ($stats['total_calls'] > 10) { if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) { $health['warnings'][] = "执行时间过长: " . number_format($stats['avg_execution_time'], 2) . "秒"; } if ($stats['avg_memory_usage'] > AddonPerformanceMonitor::MEMORY_THRESHOLD) { $health['warnings'][] = "内存使用过多: " . number_format($stats['avg_memory_usage'] / 1024 / 1024, 2) . "MB"; } if ($stats['success_rate'] < 90) { $health['healthy'] = false; $health['issues'][] = "成功率过低: " . number_format($stats['success_rate'], 2) . "%"; } } } return $health; } /** * 检查系统环境(PHP 版本 / 必需扩展 / runtime 可写 / 磁盘空间) */ private function checkSystemEnvironment(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; // PHP 版本 $phpVersion = PHP_VERSION; $result['details']['php_version'] = $phpVersion; if (version_compare($phpVersion, '8.0.0', '<')) { $result['healthy'] = false; $result['issues'][] = "PHP版本过低 ({$phpVersion}),要求 >= 8.0.0"; } // 必需扩展 foreach (['json', 'mbstring', 'curl', 'zip'] as $ext) { $loaded = extension_loaded($ext); $result['details'][$ext] = $loaded; if (!$loaded) { $result['healthy'] = false; $result['issues'][] = "缺少PHP扩展: {$ext}"; } } // runtime 目录可写 $runtimePath = root_path() . 'runtime'; if (!is_writable($runtimePath)) { $result['healthy'] = false; $result['issues'][] = "runtime目录不可写: {$runtimePath}"; } // 磁盘空间 $freeSpace = disk_free_space(root_path()); $result['details']['disk_free_space'] = $this->formatBytes((int) $freeSpace); if ($freeSpace < 104857600) { // 小于 100MB $result['warnings'][] = "磁盘空间不足,剩余: " . $this->formatBytes((int) $freeSpace); } return $result; } /** * 检查缓存系统 */ private function checkCacheSystem(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; try { $testKey = 'health_check_test_' . time(); $testValue = ['test' => true, 'timestamp' => time()]; Cache::set($testKey, $testValue, 60); $retrieved = Cache::get($testKey); if ($retrieved === false) { $result['healthy'] = false; $result['issues'][] = "缓存写入/读取失败"; } else { $result['details']['cache_test'] = 'passed'; } Cache::delete($testKey); // 插件缓存命中情况(仅警告,不影响健康) $addon = Config::get('addon', []); foreach ($addon as $addon) { if (!isset($addon['name']) || !($addon['state'] ?? false)) { continue; } if (Cache::get('addon_config_' . $addon['name']) === false) { $result['warnings'][] = "插件 {$addon['name']} 缓存未命中,可能影响性能"; } } } catch (\Exception $e) { $result['healthy'] = false; $result['issues'][] = "缓存系统异常: " . $e->getMessage(); } return $result; } /** * 检查插件文件完整性(全量) */ private function checkAddonFiles(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; $addon = Config::get('addon', []); foreach ($addon as $addon) { if (!isset($addon['name'])) { continue; } $addonPath = ADDON_PATH . $addon['name']; $result['details'][$addon['name']] = []; if (!is_dir($addonPath)) { $result['healthy'] = false; $result['issues'][] = "插件 {$addon['name']} 目录不存在"; continue; } foreach (['info.php', 'Addon.php'] as $file) { $filePath = $addonPath . DIRECTORY_SEPARATOR . $file; $exists = is_file($filePath); $result['details'][$addon['name']][$file] = $exists; if (!$exists) { $result['healthy'] = false; $result['issues'][] = "插件 {$addon['name']} 缺少必需文件: {$file}"; } } // 文件权限(不可读即警告) $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($addonPath), \RecursiveIteratorIterator::SELF_FIRST ); $permissionIssues = []; foreach ($iterator as $item) { if ($item->isFile() && ($item->getPerms() & 0x0004) === 0) { $permissionIssues[] = $item->getPathname(); } } if (!empty($permissionIssues)) { $result['warnings'][] = "插件 {$addon['name']} 有文件权限问题"; $result['details'][$addon['name']]['permission_issues'] = count($permissionIssues); } } return $result; } /** * 检查插件性能(全量) */ private function checkAddonPerformance(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; $problematicaddon = AddonPerformanceMonitor::getProblematicaddon(); if (!empty($problematicaddon)) { $result['healthy'] = false; foreach ($problematicaddon as $addon => $info) { $result['issues'][] = "插件 {$addon} 性能异常: " . implode(', ', $info['issues']); $result['details'][$addon] = $info['issues']; } } $overview = AddonPerformanceMonitor::getAllPerformanceOverview(); $totalCalls = $totalTime = $totalSuccess = 0; foreach ($overview as $stats) { $totalCalls += $stats['total_calls']; $totalTime += $stats['avg_execution_time'] * $stats['total_calls']; $totalSuccess += ($stats['success_rate'] / 100) * $stats['total_calls']; } if ($totalCalls > 0) { $avgTime = $totalTime / $totalCalls; $successRate = ($totalSuccess / $totalCalls) * 100; $result['details']['total_calls'] = $totalCalls; $result['details']['avg_execution_time'] = $avgTime; $result['details']['overall_success_rate'] = $successRate; if ($avgTime > 1.0) { $result['warnings'][] = "总体平均执行时间过长: " . number_format($avgTime * 1000, 2) . " ms"; } if ($successRate < 95) { $result['healthy'] = false; $result['issues'][] = "总体成功率过低: " . number_format($successRate, 2) . "%"; } } return $result; } /** * 检查依赖关系(全量) */ private function checkDependencies(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; $addon = Config::get('addon', []); $installedaddon = []; foreach ($addon as $addon) { if (isset($addon['name'])) { $installedaddon[$addon['name']] = $addon['state'] ?? false; } } foreach ($addon as $addon) { if (!isset($addon['name'])) { continue; } $addonName = $addon['name']; $result['details'][$addonName] = []; $addonClass = '\\addon\\' . $addonName . '\\Addon'; if (!class_exists($addonClass)) { continue; } try { $instance = app()->make($addonClass); if (method_exists($instance, 'getDependencies')) { $dependencies = $instance->getDependencies(); $result['details'][$addonName]['dependencies'] = $dependencies; foreach ($dependencies as $dep) { if (!isset($installedaddon[$dep])) { $result['healthy'] = false; $result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未安装"; } elseif (!$installedaddon[$dep]) { $result['healthy'] = false; $result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未启用"; } } } if (method_exists($instance, 'getExtensions')) { $extensions = $instance->getExtensions(); $result['details'][$addonName]['extensions'] = $extensions; foreach ($extensions as $ext) { if (!extension_loaded($ext)) { $result['healthy'] = false; $result['issues'][] = "插件 {$addonName} 需要PHP扩展: {$ext}"; } } } } catch (\Exception $e) { $result['warnings'][] = "无法检查插件 {$addonName} 的依赖: " . $e->getMessage(); } } return $result; } /** * 检查插件状态(全量) */ private function checkaddontatus(): array { $result = [ 'healthy' => true, 'issues' => [], 'warnings' => [], 'details' => [] ]; $addon = Config::get('addon', []); $enabledCount = $disabledCount = $errorCount = 0; foreach ($addon as $addon) { if (!isset($addon['name'])) { continue; } $addonName = $addon['name']; $isEnabled = ($addon['state'] ?? false) == 1; if ($isEnabled) { $enabledCount++; $addonClass = '\\addon\\' . $addonName . '\\Addon'; if (class_exists($addonClass)) { try { app()->make($addonClass); $result['details'][$addonName] = 'healthy'; } catch (\Exception $e) { $errorCount++; $result['healthy'] = false; $result['issues'][] = "插件 {$addonName} 加载失败: " . $e->getMessage(); $result['details'][$addonName] = 'error'; } } else { $errorCount++; $result['healthy'] = false; $result['issues'][] = "插件 {$addonName} 主类不存在"; $result['details'][$addonName] = 'missing'; } } else { $disabledCount++; $result['details'][$addonName] = 'disabled'; } } $result['details']['enabled_count'] = $enabledCount; $result['details']['disabled_count'] = $disabledCount; $result['details']['error_count'] = $errorCount; return $result; } /** * 输出文本格式 */ private function outputText(array $report, Output $output): void { $output->writeln('检查时间: ' . date('Y-m-d H:i:s', $report['timestamp'])); $output->writeln('整体状态: ' . ($report['healthy'] ? '健康' : '异常')); $output->writeln(''); // 检查项 foreach ($report['checks'] as $checkName => $checkResult) { $status = $checkResult['healthy'] ? '' : ''; $output->writeln("{$status} {$this->getCheckName($checkName)}"); foreach ($checkResult['issues'] ?? [] as $issue) { $output->writeln(" {$issue}"); } foreach ($checkResult['warnings'] ?? [] as $warning) { $output->writeln(" {$warning}"); } } // 插件状态 if (!empty($report['addon'])) { $output->writeln(''); $output->writeln('插件状态:'); foreach ($report['addon'] as $addon => $health) { $status = $health['healthy'] ? '' : ''; $output->writeln("{$status} {$addon}" . ($health['enabled'] ? ' (已启用)' : ' (已禁用)')); foreach ($health['issues'] ?? [] as $issue) { $output->writeln(" {$issue}"); } foreach ($health['warnings'] ?? [] as $warning) { $output->writeln(" {$warning}"); } } } // 汇总 $output->writeln(''); $output->writeln('=== 检查汇总 ==='); $output->writeln('总问题数: ' . count($report['issues'])); $output->writeln('总警告数: ' . count($report['warnings'])); $output->writeln('插件总数: ' . count($report['addon'])); $output->writeln('健康插件: ' . count(array_filter($report['addon'], fn($a) => $a['healthy']))); } /** * 单插件详细信息(--detail 时补充) */ private function outputSingleDetail(string $addon, array $health, Output $output): void { if (empty($health)) { return; } $output->writeln(''); $output->writeln('详细信息:'); $output->writeln("版本: " . ($health['version'] ?? '未知')); $output->writeln("作者: " . ($health['author'] ?? '未知')); $output->writeln("描述: " . ($health['description'] ?? '无')); $output->writeln("启用状态: " . ($health['enabled'] ? '已启用' : '已禁用')); $output->writeln("安装时间: " . (isset($health['install_time']) ? date('Y-m-d H:i:s', $health['install_time']) : '未知')); if (isset($health['dependencies'])) { $output->writeln("依赖插件: " . implode(', ', $health['dependencies'])); } if (isset($health['extensions'])) { $output->writeln("PHP扩展: " . implode(', ', $health['extensions'])); } if ($health['enabled']) { $stats = $health['performance'] ?? AddonPerformanceMonitor::getPerformanceStats($addon); if (!empty($stats) && $stats['total_calls'] > 0) { $output->writeln(''); $output->writeln('性能状态:'); $output->writeln(" 总调用次数: {$stats['total_calls']}"); $output->writeln(" 平均执行时间: " . number_format($stats['avg_execution_time'] * 1000, 2) . " ms"); $output->writeln(" 成功率: " . number_format($stats['success_rate'], 2) . "%"); if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) { $output->writeln(' ⚠ 执行时间过长'); } if ($stats['success_rate'] < 90) { $output->writeln(' ⚠ 成功率过低'); } } } } /** * 输出 JSON 格式 */ private function outputJson(array $report, Output $output): void { echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } /** * 输出 HTML 格式 */ private function outputHtml(array $report, Output $output): void { $html = '插件健康检查报告'; $html .= ''; $html .= '

插件健康检查报告

'; $html .= '

检查时间: ' . date('Y-m-d H:i:s', $report['timestamp']) . '

'; $html .= '

整体状态: ' . ($report['healthy'] ? '健康' : '异常') . '

'; foreach ($report['checks'] as $checkName => $checkResult) { $html .= '
'; $html .= '

' . $this->getCheckName($checkName) . '

'; foreach ($checkResult['issues'] ?? [] as $issue) { $html .= '
✗ ' . htmlspecialchars($issue) . '
'; } foreach ($checkResult['warnings'] ?? [] as $warning) { $html .= '
⚠ ' . htmlspecialchars($warning) . '
'; } $html .= '
'; } $html .= ''; echo $html; } /** * 获取检查项中文名 */ private function getCheckName(string $checkKey): string { $names = [ 'system' => '系统环境检查', 'cache' => '缓存系统检查', 'files' => '插件文件检查', 'performance' => '插件性能检查', 'dependencies' => '依赖关系检查', 'status' => '插件状态检查' ]; return $names[$checkKey] ?? $checkKey; } /** * 尝试自动修复所有问题(清缓存 / 修权限) */ private function runAutoFix(array $report, Output $output): void { // 遍历插件问题(缓存测试键已由 checkCacheSystem 自身清理,无需此处重复) foreach ($report['addon'] as $addon => $health) { $this->tryAutoFix($addon, $health['issues'] ?? [], $output); } } /** * 尝试自动修复单个插件问题 */ private function tryAutoFix(string $addon, array $issues, Output $output): void { $fixedCount = 0; foreach ($issues as $issue) { // 缓存类问题 if (strpos($issue, '缓存') !== false) { try { Cache::delete('addon_config_' . $addon); Cache::delete('addon_config_version_' . $addon); $output->writeln(" ✓ 已清除插件 {$addon} 缓存"); $fixedCount++; } catch (\Exception $e) { $output->writeln(" ⚠ 清除缓存失败: {$e->getMessage()}"); } } // 权限类问题(含 runtime 不可写) if (strpos($issue, '权限') !== false || strpos($issue, 'runtime') !== false) { try { if (strpos($issue, 'runtime') !== false) { $this->fixPermissions(root_path() . 'runtime'); } $this->fixPermissions(ADDON_PATH . $addon); $output->writeln(" ✓ 已修复文件权限 ({$addon})"); $fixedCount++; } catch (\Exception $e) { $output->writeln(" ⚠ 修复权限失败: {$e->getMessage()}"); } } } if ($fixedCount > 0) { $output->writeln("已自动修复 {$fixedCount} 个问题({$addon})"); } else { $output->writeln("插件 {$addon} 无可通过 --fix 自动修复的问题"); } } /** * 修复文件权限(递归 chmod) */ private function fixPermissions(string $path): void { if (!is_dir($path)) { return; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { chmod($item->getPathname(), $item->isDir() ? 0755 : 0644); } chmod($path, 0755); } /** * 发送告警 */ private function sendAlert(array $report, ?string $webhookUrl, ?string $email, Output $output): void { $message = "插件系统健康检查发现异常!\n\n"; $message .= "检查时间: " . date('Y-m-d H:i:s', $report['timestamp']) . "\n"; $message .= "总问题数: " . count($report['issues']) . "\n"; $message .= "总警告数: " . count($report['warnings']) . "\n\n"; if (!empty($report['issues'])) { $message .= "问题列表:\n"; foreach ($report['issues'] as $issue) { $message .= "- {$issue}\n"; } $message .= "\n"; } Log::error('插件健康检查异常', $report); if ($webhookUrl) { try { $this->sendWebhook($webhookUrl, $report); $output->writeln('已发送Webhook告警'); } catch (\Exception $e) { $output->writeln('Webhook发送失败: ' . $e->getMessage() . ''); } } if ($email) { try { $this->sendEmail($email, '插件系统健康检查告警', $message); $output->writeln('已发送邮件告警'); } catch (\Exception $e) { $output->writeln('邮件发送失败: ' . $e->getMessage() . ''); } } $output->writeln('告警消息: ' . str_replace("\n", " ", $message) . ''); } /** * 发送 Webhook */ private function sendWebhook(string $url, array $data): void { $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode >= 400) { throw new \Exception("Webhook返回错误: HTTP {$httpCode}"); } } /** * 发送邮件 */ private function sendEmail(string $to, string $subject, string $message): void { $headers = "From: noreply@yourdomain.com\r\n"; $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; if (!mail($to, $subject, $message, $headers)) { throw new \Exception("邮件发送失败"); } } /** * 保存检查报告(runtime/addon_health_reports,保留 30 天) */ private function saveReport(array $report, Output $output): void { $reportDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_health_reports'; if (!is_dir($reportDir)) { mkdir($reportDir, 0755, true); } $reportFile = $reportDir . DIRECTORY_SEPARATOR . 'health_report_' . date('YmdHis') . '.json'; file_put_contents($reportFile, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $output->writeln("检查报告已保存: {$reportFile}"); $this->cleanupOldReports($reportDir, 30); } /** * 清理旧报告 */ private function cleanupOldReports(string $dir, int $days): void { $files = glob($dir . DIRECTORY_SEPARATOR . 'health_report_*.json'); $cutoffTime = time() - ($days * 86400); foreach ($files as $file) { if (filemtime($file) < $cutoffTime) { unlink($file); } } } /** * 格式化字节 */ private function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, 2) . ' ' . $units[$pow]; } /** * 检查插件是否已安装 */ private function isAddonInstalled(string $addon): bool { return is_dir(ADDON_PATH . $addon) && is_file(ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php'); } /** * 检查插件是否已启用 */ private function isAddonEnabled(string $addon): bool { $infoFile = ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php'; if (!is_file($infoFile)) { return false; } $info = include $infoFile; return isset($info['state']) && $info['state'] == 1; } }