90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
/** # 脚本说明
|
|
* 存量 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();
|