/** * Flock RPA 本机互斥锁 — 防止 embed Worker 与 batch 探针同时开浏览器打同一出口 IP */ import fs from "node:fs"; import path from "node:path"; const LOCK_DIR = path.join(process.cwd(), ".rpa"); const LOCK_PATH = path.join(LOCK_DIR, "flock-rpa.lock"); export type FlockLockInfo = { owner: string; pid: number; at: string; }; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function isPidAlive(pid: number): boolean { if (!Number.isFinite(pid) || pid <= 0) { return false; } try { process.kill(pid, 0); return true; } catch { return false; } } export function readFlockExclusiveLock(): FlockLockInfo | null { try { if (!fs.existsSync(LOCK_PATH)) { return null; } const raw = JSON.parse(fs.readFileSync(LOCK_PATH, "utf8")) as FlockLockInfo; if (!raw?.owner || !raw?.pid) { return null; } return raw; } catch { return null; } } /** 锁是否被其他存活进程占用 */ export function isFlockExclusiveBusy(ignoreOwner?: string): boolean { const info = readFlockExclusiveLock(); if (!info) { return false; } if (!isPidAlive(info.pid)) { try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } return false; } if (ignoreOwner && info.owner === ignoreOwner && info.pid === process.pid) { return false; } return info.pid !== process.pid; } export function getFlockExclusiveBusyMessage(): string | null { if (!isFlockExclusiveBusy()) { return null; } const info = readFlockExclusiveLock(); const who = info?.owner === "batch" ? "本地批量探针" : "其他 Flock 查价任务"; return `Flock 官网访问正被${who}占用(串行防互踩),请待其结束后再试`; } /** 仅当本地 batch 探针占锁时用于网站入队拒绝(Worker 占锁时仍可入队排队) */ export function getFlockBatchExclusiveBusyMessage(): string | null { const info = readFlockExclusiveLock(); if (!info || info.owner !== "batch") { return null; } if (!isPidAlive(info.pid)) { try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } return null; } return "Flock 官网访问正被本地批量探针占用(串行防互踩),请待探针结束后再在网站查询"; } function tryAcquireOnce(owner: string): boolean { fs.mkdirSync(LOCK_DIR, { recursive: true }); const stale = readFlockExclusiveLock(); if (stale && !isPidAlive(stale.pid)) { try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } } try { const fd = fs.openSync(LOCK_PATH, "wx"); const payload: FlockLockInfo = { owner, pid: process.pid, at: new Date().toISOString(), }; fs.writeFileSync(fd, JSON.stringify(payload), "utf8"); fs.closeSync(fd); return true; } catch (error) { const code = error && typeof error === "object" && "code" in error ? String((error as { code: unknown }).code) : ""; if (code === "EEXIST") { const again = readFlockExclusiveLock(); if (again && again.pid === process.pid) { return true; } if (again && !isPidAlive(again.pid)) { try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } return tryAcquireOnce(owner); } return false; } throw error; } } export function releaseFlockExclusiveLock(owner: string): void { const info = readFlockExclusiveLock(); if (!info) { return; } if (info.pid !== process.pid && info.owner !== owner) { return; } try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ } } /** 等待获取本机唯一 Flock 浏览器锁 */ export async function withFlockExclusiveLock( owner: string, fn: () => Promise, options?: { waitMs?: number; pollMs?: number }, ): Promise { const waitMs = options?.waitMs ?? 180_000; const pollMs = options?.pollMs ?? 1_000; const deadline = Date.now() + waitMs; while (!tryAcquireOnce(owner)) { if (Date.now() >= deadline) { throw new Error( getFlockExclusiveBusyMessage() ?? "Flock 官网访问繁忙,请稍后重试", ); } const info = readFlockExclusiveLock(); console.warn( `[flock-lock] 等待互斥锁 owner=${info?.owner ?? "?"} pid=${info?.pid ?? "?"}…`, ); await sleep(pollMs); } console.log(`[flock-lock] 已获取互斥锁 owner=${owner} pid=${process.pid}`); try { return await fn(); } finally { releaseFlockExclusiveLock(owner); console.log(`[flock-lock] 已释放互斥锁 owner=${owner}`); } } /** 失败是否不应 BullMQ 重试(上限/拒号重试只会加重 IP 风控) */ export function isFlockNonRetryableFailure(message?: string): boolean { if (!message) { return false; } return ( /flock_quote_limit|thank you for your interest|报价.*上限|quote\s*limit/i.test( message, ) || /flock_account_rejected|issue creating your account/i.test(message) || /flock_account_exists|already (have|registered)/i.test(message) || /flock_login_failed|session_expired|未配置 FLOCK_LOGIN_/i.test(message) || /互斥锁|正被.*占用|防互踩/i.test(message) || /FLOCK_RATES_PENDING|still looking for the best rates|约需\s*15\s*分钟/i.test( message, ) ); }