From af0b09daee109f1b2268915c2f8e8ca21b902bdc Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Apr 2026 16:14:20 +0800 Subject: [PATCH] debug --- code/app/admin/controller/Site.php | 12 +- code/app/admin/middleware/AdminCors.php | 5 +- .../DomainSpiderCrawlWorkbenchHelper.php | 26 +- ...-admin2-spider-workbench-production-fix.md | 286 ++++++++++++++++++ 4 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 docs/2026-04-16-admin2-spider-workbench-production-fix.md diff --git a/code/app/admin/controller/Site.php b/code/app/admin/controller/Site.php index 278cdec..935aaa8 100644 --- a/code/app/admin/controller/Site.php +++ b/code/app/admin/controller/Site.php @@ -1519,9 +1519,17 @@ class Site extends BaseController $intWindowHours, $strBotScope, $mixSelectedBots - )); + ))->header([ + 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', + 'Pragma' => 'no-cache', + 'Expires' => '0', + ]); } catch (\Throwable $Throwable) { - return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage()); + return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage())->header([ + 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', + 'Pragma' => 'no-cache', + 'Expires' => '0', + ]); } } diff --git a/code/app/admin/middleware/AdminCors.php b/code/app/admin/middleware/AdminCors.php index 4ee0ecf..2534b5d 100644 --- a/code/app/admin/middleware/AdminCors.php +++ b/code/app/admin/middleware/AdminCors.php @@ -16,7 +16,8 @@ class AdminCors extends AllowCrossDomain protected $header = [ 'Access-Control-Allow-Credentials' => 'true', 'Access-Control-Max-Age' => 3600, - 'Access-Control-Allow-Methods' => '*', - 'Access-Control-Allow-Headers' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS', + 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With, admin-token', + 'Vary' => 'Origin', ]; } diff --git a/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php b/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php index fc353cd..1bce3f6 100644 --- a/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php +++ b/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php @@ -6,6 +6,8 @@ namespace app\common\helper; class DomainSpiderCrawlWorkbenchHelper { + protected const MAX_AGGREGATE_RUNS = 12; + protected static function codeRootFromStorageRoot(string $storageRoot): string { return rtrim(dirname(rtrim($storageRoot, '/')), '/'); @@ -308,7 +310,7 @@ class DomainSpiderCrawlWorkbenchHelper continue; } $items[] = [ - 'summary' => $summary, + 'summary_path' => (string)$summaryPath, 'normalized' => self::normalizeRunSummary($publicRoot, $summary, (string)$summaryPath), 'generated_at_ts' => $generatedTimestamp, ]; @@ -336,7 +338,14 @@ class DomainSpiderCrawlWorkbenchHelper $anomalyRecords = []; $latestGeneratedAt = ''; foreach ($runEntries as $entry) { - $summary = is_array($entry['summary'] ?? null) ? (array)$entry['summary'] : []; + $summaryPath = trim((string)($entry['summary_path'] ?? '')); + if ($summaryPath === '') { + continue; + } + $summary = self::readJsonFile($summaryPath); + if (empty($summary)) { + continue; + } $generatedAt = trim((string)($summary['generated_at'] ?? '')); if ($generatedAt !== '' && ($latestGeneratedAt === '' || strcmp($generatedAt, $latestGeneratedAt) > 0)) { $latestGeneratedAt = $generatedAt; @@ -363,6 +372,15 @@ class DomainSpiderCrawlWorkbenchHelper return $aggregated; } + protected static function limitAggregateRunEntries(array $runEntries): array + { + if (count($runEntries) <= self::MAX_AGGREGATE_RUNS) { + return $runEntries; + } + + return array_slice($runEntries, 0, self::MAX_AGGREGATE_RUNS); + } + protected static function buildRecentRuns(array $runEntries, int $limit): array { $items = []; @@ -993,7 +1011,7 @@ class DomainSpiderCrawlWorkbenchHelper $rows = []; foreach ($windows as $hours) { $runEntries = self::collectRunEntries($publicRoot, $runsRoot, $hours); - $summary = self::aggregateRunSummaries($runEntries); + $summary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries)); $summary = self::filterSummaryByBotScope($summary, $botScope); $summary = self::filterSummaryBySelectedBots($summary, $selectedBots); $metrics = self::summarizeWindow($summary, $host); @@ -1241,7 +1259,7 @@ class DomainSpiderCrawlWorkbenchHelper $recentRuns = self::buildRecentRuns($runEntries, $runLimit); if ($windowHours > 0 && !empty($runEntries)) { - $latestSummary = self::aggregateRunSummaries($runEntries); + $latestSummary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries)); } if (empty($latestSummary) && !empty($recentRuns)) { diff --git a/docs/2026-04-16-admin2-spider-workbench-production-fix.md b/docs/2026-04-16-admin2-spider-workbench-production-fix.md new file mode 100644 index 0000000..54f7677 --- /dev/null +++ b/docs/2026-04-16-admin2-spider-workbench-production-fix.md @@ -0,0 +1,286 @@ +# 2026-04-16 Admin2 蜘蛛预览正式服排障记录 + +## 背景 + +本次排障发生在正式服管理端 `https://admin2.jpjdxs.info/`。 + +用户反馈的问题分为三个阶段: + +1. 管理端请求蜘蛛工作台接口时报 CORS 错误。 +2. 前端在处理临时产物和缓存后,首页出现运行时异常,页面加载不稳定。 +3. 页面恢复后,点击“蜘蛛预览”接口返回 `500`。 + +本次工作定位为正式服辅助分析、止血、优化与修复,开发主线仍由主 Codex 继续维护。 + +## 最终结论 + +这次问题不是单点故障,而是三层问题叠加: + +1. 前端生产环境接口地址指向了跨域域名,导致浏览器先出现 CORS 报错。 +2. 排障过程中过的脏 `dist` 与浏览器 `Service Worker` 缓存叠加,导致首页运行时异常。 +3. 蜘蛛工作台接口在 24 小时窗口下同步聚合过多抓取摘要文件,触发 PHP 128MB 内存上限,最终返回 `500`。 + +## 线上现象 + +### 1. CORS 阶段 + +浏览器最初报错: + +- 管理端来源:`https://admin2.jpjdxs.info` +- 被请求接口:`https://adapi.ra12.shop/site/domain/spider-crawl/workbench/summary?...` +- 浏览器错误:`No 'Access-Control-Allow-Origin' header is present on the requested resource` + +### 2. 前端运行时阶段 + +首页曾出现运行时异常: + +- `TypeError: Cannot read properties of null (reading 'nextSibling')` + +这个阶段和手工改动过的前端产物以及旧缓存有关,不是蜘蛛工作台业务逻辑本身的问题。 + +### 3. 蜘蛛预览 500 阶段 + +页面恢复后,点击蜘蛛预览再次报错: + +- 请求地址: + - `/api/site/domain/spider-crawl/workbench/summary?run_limit=10&anomaly_limit=80&window_hours=24&bot_scope=all&selected_bots=baiduspider,sogou` +- 浏览器错误: + - `AxiosError: Request failed with status code 500` + +## 根因定位 + +### 根因一:前端生产环境接口配置为跨域域名 + +正式服前端原先在生产环境里直接请求外部域名,浏览器因此走了跨域模式。 + +关联文件: + +- `VideoAdminV2/.env.production` + +修复思路: + +- 将前端生产环境接口基址改为同源 `/api` +- 由管理端 nginx 反向代理到后端服务 + +### 根因二:旧 `Service Worker` 与临时修改产物污染 + +在排障期间,浏览器端同时受到以下因素影响: + +- 旧 `Service Worker` 缓存 +- 旧静态资源哈希 +- 临时手工调整过的 `dist` + +这会导致首页有机会出现和真实源码不一致的运行时行为。 + +修复思路: + +- 用 `nvm` 环境重新构建干净前端产物 +- 关闭 PWA,避免继续注册新的 `Service Worker` +- 首页增加旧缓存清理逻辑 +- 首页增加 `localStorage` 解析容错 + +### 根因三:蜘蛛工作台同步聚合过多摘要文件导致 PHP 内存耗尽 + +这是点击“蜘蛛预览”时 `500` 的真实根因。 + +日志证据来自: + +- `/www/wwwlogs/source.video2.com.error.log` + +关键错误: + +- `Allowed memory size of 134217728 bytes exhausted` +- 涉及文件: + - `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php` + - `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlLogHelper.php` + +当时请求示例: + +- `GET /site/domain/spider-crawl/workbench/summary?run_limit=10&anomaly_limit=80&window_hours=24&bot_scope=all&selected_bots=baiduspider,sogou` + +触发原因: + +- `window_hours=24` 时会扫描大量 `crawl-logs.summary.json` +- 旧实现会把多份 summary 读入内存后进行聚合 +- 正式服数据量明显大于开发环境 + +排障时观察到的数据规模: + +- `storage/domain-spider-crawl/runs` 目录约 `47M` +- `crawl-logs.summary.json` 数量约 `154` 份 +- 单个摘要文件最大接近 `958507` bytes + +## 已执行的修复 + +### 一、管理端同源代理与 CORS 调整 + +#### 1. 管理端 nginx 增加 `/api/` 反向代理 + +文件: + +- `/www/server/panel/vhost/nginx/admin2.jpjdxs.info.conf` + +处理效果: + +- 管理端改为访问同源 `/api/...` +- 管理端不再直接跨域请求 `adapi.ra12.shop` + +#### 2. 后端 CORS 头调整 + +文件: + +- `/www/wwwroot/VideoSource2/code/app/admin/middleware/AdminCors.php` + +处理效果: + +- 补齐允许方法 +- 补齐允许头 +- 增加 `Vary: Origin` + +## 二、前端正式服止血 + +### 1. 前端生产环境改为同源接口 + +文件: + +- `/www/wwwroot/VideoAdminV2/.env.production` + +关键配置: + +- `VITE_API_URL=/api` +- `VITE_PWA=false` + +### 2. 首页增加容错和旧缓存清理 + +文件: + +- `/www/wwwroot/VideoAdminV2/index.html` + +处理内容: + +- `IOMS-global` 的 `localStorage` 读取加 `try/catch` +- 启动时注销旧 `serviceWorker` +- 清理旧缓存 + +### 3. 清理不再需要的启动期缓存逻辑 + +文件: + +- `/www/wwwroot/VideoAdminV2/src/main.ts` + +处理内容: + +- 去掉启动时全量清理缓存的临时逻辑,避免继续引入不确定性 + +### 4. 使用 `nvm` 环境重新构建前端 + +确认到正式服 `nvm` 环境为: + +- Node: `v20.19.6` +- npm: `10.8.2` + +实际使用命令: + +```bash +export HOME=/home/www +export NVM_DIR="$HOME/.nvm" +. "$NVM_DIR/nvm.sh" +cd /www/wwwroot/VideoAdminV2 +nvm use 20 +npm run build:pro +``` + +构建后线上主包为: + +- `assets/js/index-23f333c2.js` + +## 三、蜘蛛工作台 500 修复 + +核心修复文件: + +- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php` + +处理内容: + +1. 增加聚合 run 上限: + - `MAX_AGGREGATE_RUNS = 12` +2. `collectRunEntries()` 不再保存整份 summary 内容,只保存: + - `summary_path` + - `normalized` + - `generated_at_ts` +3. `aggregateRunSummaries()` 改为按需读取文件,而不是提前把所有 summary 常驻内存 +4. `buildSummary()` 在窗口聚合前先裁剪最近 run +5. `buildWindowComparison()` 也在聚合前先裁剪最近 run + +这次修复的目标是正式服止血,不是最终态架构优化。 + +## 验证结果 + +### 1. 前端接口基址 + +已确认前端主包中接口基址为: + +- `baseURL:"/api"` + +### 2. CORS 头 + +已确认响应头包含: + +- `Access-Control-Allow-Origin: https://admin2.jpjdxs.info` +- `Vary: Origin` + +### 3. helper 在 128MB 内存下可跑通 + +排障时在本机使用命令验证: + +```bash +php -d memory_limit=128M -r '... DomainSpiderCrawlWorkbenchHelper::buildSummary(...) ...' +``` + +修复后返回正常,说明核心内存问题已被压住。 + +### 4. PHP-FPM 已重载 + +执行: + +```bash +service php-fpm-83 reload +``` + +用于确保线上 PHP 进程加载到修复后的 helper 代码。 + +### 5. 用户回归验证 + +用户最终反馈: + +- “好了终于跑通了” + +说明这次正式服问题已经完成闭环。 + +## 对主 Codex 的交接建议 + +这次修复属于正式服止血方案,主 Codex 后续建议继续做结构化优化: + +1. 不要在在线接口里同步扫描过多 run 文件。 +2. 将蜘蛛工作台改造成预聚合模型,而不是请求时全量聚合。 +3. 将 `1h / 6h / 24h / 72h` 窗口统计结果提前固化为摘要文件或缓存。 +4. 为大窗口查询增加更清晰的降级策略与上限说明。 +5. 如果后续还要放大统计维度,应考虑异步任务或离线聚合,不要继续依赖同步 `json_decode(file_get_contents(...))` 全量加载。 + +## 本次涉及文件 + +前端相关: + +- `/www/wwwroot/VideoAdminV2/.env.production` +- `/www/wwwroot/VideoAdminV2/index.html` +- `/www/wwwroot/VideoAdminV2/src/main.ts` +- `/www/server/panel/vhost/nginx/admin2.jpjdxs.info.conf` + +后端相关: + +- `/www/wwwroot/VideoSource2/code/app/admin/middleware/AdminCors.php` +- `/www/wwwroot/VideoSource2/code/app/common/helper/DomainSpiderCrawlWorkbenchHelper.php` + +文档落点: + +- `/www/wwwroot/VideoSource2/docs/2026-04-16-admin2-spider-workbench-production-fix.md`