/** # 脚本说明 * SERP 24h 监控采样器(在 admin 服务器本机运行,零额外负载:只读管理接口): * nohup bun serp-monitor.ts > data/monitor.log 2>&1 & * 每 60s 采样 /api/serp/stats(hours24)+ /api/serp/pool(浏览器池)+ /api/serp/cache(缓存池), * 追加 JSONL 到 data/serp-monitor.jsonl;每 10 分钟输出一行摘要到 stdout(进 monitor.log)。 * 用途:跑一天边测边修的长期证据链(吞吐/错误率/门壳/池水位)。 */ import fs from "node:fs"; /** # 成员变量 基址与令牌(本机) **/ const BASE = "http://127.0.0.1:53100"; const TOKEN = process.env.SERP_TOKEN ?? ""; if (!TOKEN) { console.error("缺少 SERP_TOKEN"); process.exit(1); } /** # 成员变量 输出目录(data/,相对脚本) **/ const DATA = new URL("./data/", import.meta.url).pathname; fs.mkdirSync(DATA, { recursive: true }); const JSONL = `${DATA}serp-monitor.jsonl`; /** # 成员变量 上一次快照(用于计算每分钟增量) **/ let prev: { total: number; ok: number; failed: number; captcha: number; gate: number; at: number } | null = null; /** # 成员变量 每 10 分钟摘要计数器 **/ let sinceSummary = 0; /** # 函数 * 带超时 GET JSON。 */ async function getJson(path: string): Promise { const res = await fetch(`${BASE}${path}`, { headers: { authorization: `Bearer ${TOKEN}` }, signal: AbortSignal.timeout(15000) }); if (!res.ok) throw new Error(`HTTP ${res.status} ${path}`); const j = await res.json() as any; return j?.data ?? j; } /** # 函数 * 单轮采样:统计/池/缓存 → 增量计算 → JSONL 追加 → 10 分钟摘要打印。 */ async function sample(): Promise { const now = Date.now(); const [stats, pool, cache] = await Promise.all([ getJson("/api/serp/stats").catch(() => null), getJson("/api/serp/pool").catch(() => null), getJson("/api/serp/cache").catch(() => null), ]); if (!stats) return; const h = stats.hours24 ?? {}; const row = { at: new Date(now).toISOString(), h24: { total: h.total ?? 0, ok: h.ok ?? 0, failed: h.failed ?? 0, captcha: h.captcha ?? 0, gate: h.gate ?? 0, avg_ms: h.avg_ms ?? 0, p95_ms: h.p95_ms ?? 0 }, delta: null as any, browserPool: null as any, cachePool: null as any, }; if (prev && now - prev.at > 30000) { row.delta = { total: (h.total ?? 0) - prev.total, failed: (h.failed ?? 0) - prev.failed, gate: (h.gate ?? 0) - prev.gate, captcha: (h.captcha ?? 0) - prev.captcha, }; } prev = { total: h.total ?? 0, ok: h.ok ?? 0, failed: h.failed ?? 0, captcha: h.captcha ?? 0, gate: h.gate ?? 0, at: now }; if (pool?.totals) row.browserPool = { ...pool.totals, targets: pool.nodes.map((n: any) => `${n.node}:${n.total}/${n.target}`).join(",") }; if (cache) row.cachePool = { count: cache.count, maxCache: cache.maxCache }; fs.appendFileSync(JSONL, JSON.stringify(row) + "\n"); sinceSummary++; if (sinceSummary >= 10) { sinceSummary = 0; const d = row.delta; const errRate = d && d.total > 0 ? +(((d.failed) / d.total) * 100).toFixed(1) : null; console.log(`${row.at} Δ10min total=${d?.total} failed=${d?.failed} gate=${d?.gate} captcha=${d?.captcha} err=${errRate}% avg=${row.h24.avg_ms}ms browserPool=${row.browserPool ? `${row.browserPool.total}(ready${row.browserPool.ready})` : "?"} cache=${row.cachePool?.count}/${row.cachePool?.maxCache}`); } } /** # 主流程:60s 一轮,永久运行(systemd/nohup 托管) **/ console.log(`serp-monitor started ${new Date().toISOString()}`); for (;;) { await sample().catch((e) => console.error(`${new Date().toISOString()} sample error: ${String(e).slice(0, 160)}`)); await Bun.sleep(60000); }