85 lines
3.3 KiB
TypeScript
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`);
|