/** # 脚本说明 * 压测收尾:删除全部测试 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 { 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()}`);