1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/** # 脚本说明
|
||||
* 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<any> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user