Files
SerpMax/stress/cleanup-profiles.ts

64 lines
2.8 KiB
TypeScript

/** # 脚本说明
* 压测收尾:删除全部测试 Profile(SERP 标签)。
* 分页拉取(限 500)→ 并发 DELETE /api/profiles/:id(admin 级联:节点数据删除/扩展绑定清理)。
* 404 视为已删;最后复查 admin DB 与节点 profile 目录残留数。
* 用法:bun cleanup-profiles.ts
*/
const fs = require("fs") as typeof import("fs");
const ADMIN = "http://127.0.0.1:5310";
const DATA = `${new URL(".", import.meta.url).pathname}data`;
/** # 函数:docker exec 查 admin DB 单值 **/
function sqlOne(sql: string): string {
const p = Bun.spawnSync(["docker", "exec", "serpmax-pg", "psql", "-U", "postgres", "-d", "serpmax_admin", "-tAc", sql], { stdout: "pipe", stderr: "pipe" });
const out = p.stdout.toString().trim();
if (p.exitCode !== 0 || !out) throw new Error(`SQL 失败`);
return out;
}
const TOKEN = sqlOne("SELECT api_token FROM users WHERE id=2");
// 分页拉取 SERP 标签 Profile(标签过滤走服务端,兜底本地再筛)
const fps: { id: string; fp: string }[] = [];
let page = 1;
while (page <= 40) {
const res = await fetch(`${ADMIN}/api/profiles?limit=500&page=${page}`, {
headers: { authorization: `Bearer ${TOKEN}` },
signal: AbortSignal.timeout(30000),
});
if (!res.ok) throw new Error(`列表 ${res.status}`);
const j: any = await res.json();
const data = j.data ?? j;
const items: any[] = data.items ?? [];
for (const it of items) {
const tags: string[] = it.tags ?? [];
if (tags.includes("SERP") && (it.name ?? "").startsWith("sz-serp-")) {
fps.push({ id: it.id, fp: it.fingerprint ?? "" });
}
}
const total = data.total ?? 0;
if (items.length < 500 || fps.length >= total) break;
page++;
}
console.log(`[cleanup] 目标 ${fps.length} 个`);
let deleted = 0, skipped = 0;
let cursor = 0;
const workers = 16;
async function worker(): Promise<void> {
while (cursor < fps.length) {
const i = cursor++;
const t = fps[i];
if (!t.id) { skipped++; continue; }
try {
const res = await fetch(`${ADMIN}/api/profiles/${t.id}`, { method: "DELETE", headers: { authorization: `Bearer ${TOKEN}` }, signal: AbortSignal.timeout(60000) });
if (res.ok || res.status === 404) deleted++;
else { skipped++; if (skipped < 10) console.error(`[cleanup] 删 ${t.fp} 状态 ${res.status}`); }
} catch (e) { skipped++; }
if (deleted % 500 === 0 && deleted > 0) console.log(`[cleanup] deleted=${deleted} skipped=${skipped}`);
}
}
await Promise.all(Array.from({ length: workers }, () => worker()));
console.log(`[cleanup] 完成 deleted=${deleted} skipped=${skipped}`);
console.log(`[cleanup] admin 残留 SERP=${sqlOne("SELECT count(*) FROM profiles WHERE tags @> ARRAY['SERP']::text[]")}`);
console.log(`[cleanup] 节点目录残留=${Bun.spawnSync(["bash", "-c", "ls /opt/serpmax/node/data/profiles/ | wc -l"], { stdout: "pipe" }).stdout.toString().trim()}`);