Files
SerpMax/stress/create-profiles.ts

85 lines
3.3 KiB
TypeScript

/** # 脚本说明
* 压测准备:批量在 admin(127.0.0.1:5310)创建 N 个 Profile。
* - 归属当前 MCP 用户(users.id=2,phptits),Token 由脚本直取 DB 不落盘不回显
* - 浏览器内核 CloakB(cloak)、代理 ipmart.io-US、标签 SERP、节点 OVH-FR
* - 输出 data/pool.json:[{ id, fingerprint, name }]
*
* 用法:bun create-profiles.ts [N=10000]
*/
import { $ } from "bun";
/** # 成员变量 admin 基址 **/
const ADMIN = "http://127.0.0.1:5310";
/** # 成员变量 目标代理(pressure 测试固定 ipmart.io-US,{sid} 每指纹独立出口) **/
const PROXY_ID = "19e3ab5c-c61e-4936-8a7f-d78f6635f57c";
/** # 成员变量 归属节点 **/
const NODE = "OVH-FR";
/** # 成员变量 标签 **/
const TAGS = ["SERP"];
const N = Number(process.argv[2] ?? 10000);
/** # 函数
* 通过 docker exec 查询 admin DB,取单值文本。
* 参数 sql:完整 SQL 语句
* 返回值:第一行第一列,去尾部换行
*/
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 失败: ${p.stderr.toString().slice(0, 300)}`);
return out;
}
/** # 成员变量 admin API Token(phptits,id=2;仅内存) **/
const TOKEN = sqlOne("SELECT api_token FROM users WHERE id=2");
const results: { id: string; fingerprint: string; name: string }[] = [];
let okCount = 0;
let errCount = 0;
const t0 = Date.now();
/** # 函数
* 创建单个 Profile,成功返回 {id, fingerprint, name},失败返回 null。
*/
async function createOne(i: number): Promise<{ id: string; fingerprint: string; name: string } | null> {
const name = `sz-serp-${String(i + 1).padStart(5, "0")}`;
try {
const res = await fetch(`${ADMIN}/api/profiles`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
// 浏览器外观随机浅色/深色(用户 2026-10-20 要求 SERP 号不再全浅色)
body: JSON.stringify({ name, browserKind: "cloak", browserTheme: Math.random() < 0.5 ? "light" : "dark", proxyId: PROXY_ID, tags: TAGS, node: NODE }),
signal: AbortSignal.timeout(20000),
});
if (res.status === 409) return null; // 指纹冲突(极小概率),跳过
if (!res.ok) throw new Error(`HTTP ${res.status} ${(await res.text()).slice(0, 120)}`);
const j = (await res.json()) as any;
const d = (j.data ?? j) as { id?: string; fingerprint?: string };
okCount++;
return { id: d.id ?? "", fingerprint: d.fingerprint ?? d.id ?? "", name };
} catch (e) {
errCount++;
if (errCount < 20) console.error(`create fail ${i}: ${String(e).slice(0, 140)}`);
return null;
}
}
/** 主循环:16 并发创建 N 个 */
const workers = 16;
let cursor = 0;
async function worker() {
while (cursor < N) {
const i = cursor++;
const r = await createOne(i);
if (r) results.push(r);
}
}
await Promise.all(Array.from({ length: workers }, () => worker()));
await Bun.write(new URL(import.meta.url).pathname.replace(/[^/]+$/, "") + `data/pool.json`, JSON.stringify(results));
console.log(`done: created=${okCount} failed=${errCount} elapsed=${((Date.now() - t0) / 1000).toFixed(1)}s -> data/pool.json`);