1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/** # 脚本说明
|
||||
* 存量 SERP Profile 浏览器外观随机化:标签含 SERP 的 Profile 主题随机浅色/深色(50/50)。
|
||||
* 两步:① admin DB 批量 UPDATE browser_theme;② 逐个 PATCH 节点
|
||||
* /profiles/:指纹/browser-env {theme}(下一次浏览器启动生效;节点侧 ID 统一为指纹)。
|
||||
* 认证:节点侧 x-node-key = nodes.key;节点寻址 nodes.base_url。
|
||||
* 幂等可重跑(每次重新随机)。
|
||||
* 用法:在 admin 机上执行 bun randomize-themes.ts [并发=24]
|
||||
* (bun 自动加载 admin/.env 的 DATABASE_URL;本地默认 serpmax_admin@127.0.0.1:5432)
|
||||
*/
|
||||
import { SQL } from "bun";
|
||||
|
||||
/** # 成员变量 admin Postgres 连接(DATABASE_URL 覆盖;默认本机 serpmax_admin) **/
|
||||
const sql = new SQL(process.env.DATABASE_URL ?? "postgres://postgres:postgres@127.0.0.1:5432/serpmax_admin");
|
||||
|
||||
/** # 成员变量 节点 PATCH 并发数 **/
|
||||
const CONCURRENCY = Math.max(1, Number(process.argv[2] ?? 24));
|
||||
|
||||
/** # 函数
|
||||
* 主流程:拉取 SERP Profile 与节点信息 → 每号随机主题 → 批量落 admin DB → 并发推节点。
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
// 节点寻址与密钥(name → base_url / key)
|
||||
const nodes = await sql`SELECT name, base_url, key FROM nodes`;
|
||||
const nodeById = new Map<string, { baseUrl: string; key: string }>();
|
||||
for (const n of nodes) {
|
||||
nodeById.set(String(n.name), { baseUrl: String(n.base_url ?? "").replace(/\/$/, ""), key: String(n.key ?? "") });
|
||||
}
|
||||
|
||||
// SERP Profile 池(admin 侧 ID 为 UUID;指纹是节点侧 ID)
|
||||
const rows = await sql`SELECT id::text AS id, fingerprint, node, browser_theme FROM profiles WHERE tags @> ARRAY['SERP']::text[]`;
|
||||
console.log(`[randomize] SERP Profile 共 ${rows.length} 个`);
|
||||
if (!rows.length) return;
|
||||
|
||||
// 逐号随机主题并按目标节点分组
|
||||
type Item = { id: string; fingerprint: string; theme: string };
|
||||
const byNode = new Map<string, Item[]>();
|
||||
for (const r of rows) {
|
||||
const item: Item = {
|
||||
id: String(r.id),
|
||||
fingerprint: String(r.fingerprint ?? r.id),
|
||||
theme: Math.random() < 0.5 ? "light" : "dark",
|
||||
};
|
||||
const list = byNode.get(String(r.node ?? "")) ?? [];
|
||||
list.push(item);
|
||||
byNode.set(String(r.node ?? ""), list);
|
||||
}
|
||||
|
||||
// ① admin DB 批量落库(2000/批,UPDATE...FROM VALUES)
|
||||
let dbUpdated = 0;
|
||||
for (const items of byNode.values()) {
|
||||
for (let i = 0; i < items.length; i += 2000) {
|
||||
const chunk = items.slice(i, i + 2000);
|
||||
const params = chunk.flatMap((it) => [it.id, it.theme]);
|
||||
const ph = chunk.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(",");
|
||||
await sql.unsafe(`UPDATE profiles AS p SET browser_theme = v.theme FROM (VALUES ${ph}) AS v(id, theme) WHERE p.id::text = v.id`, params);
|
||||
dbUpdated += chunk.length;
|
||||
}
|
||||
}
|
||||
console.log(`[randomize] admin DB 已更新 ${dbUpdated} 个`);
|
||||
|
||||
// ② 并发推节点(失败不阻塞,最后汇总)
|
||||
let okCount = 0;
|
||||
let failCount = 0;
|
||||
const cursor = { n: 0 };
|
||||
const all = [...byNode.entries()].flatMap(([node, items]) => items.map((it) => ({ node, ...it })));
|
||||
async function worker(): Promise<void> {
|
||||
while (cursor.n < all.length) {
|
||||
const it = all[cursor.n++];
|
||||
const target = nodeById.get(it.node);
|
||||
if (!target?.baseUrl) { failCount++; continue; }
|
||||
try {
|
||||
const res = await fetch(`${target.baseUrl}/profiles/${encodeURIComponent(it.fingerprint)}/browser-env`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", "x-node-key": target.key },
|
||||
body: JSON.stringify({ theme: it.theme }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (res.ok) okCount++; else failCount++;
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
if ((okCount + failCount) % 1000 === 0) console.log(`[randomize] 节点推送 ${okCount + failCount}/${all.length}`);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
|
||||
console.log(`[randomize] 完成:节点推送成功 ${okCount},失败 ${failCount}`);
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user