import crypto from "crypto"; export function nowIso() { return new Date().toISOString(); } export async function sleep(ms: number) { if (ms <= 0) return; await new Promise((resolve) => setTimeout(resolve, ms)); } export function normalizeUrl(url: string) { const u = url.trim(); if (u.startsWith("//")) return `https:${u}`; if (u.startsWith("http://") || u.startsWith("https://")) return u; return `https://${u}`; } export function parsePrice(text: string | null) { if (!text) return null; const t = text.replace(/[¥¥,\s]/g, ""); const m = t.match(/(\d+(?:\.\d+)?)/); if (!m) return null; const n = Number(m[1]); return Number.isFinite(n) ? n : null; } export function parseSales(text: string | null) { if (!text) return null; const t = text.replace(/[,,\s]/g, ""); const m = t.match(/(\d+(?:\.\d+)?)(万|千)?/); if (!m) return null; const base = Number(m[1]); if (!Number.isFinite(base)) return null; const unit = m[2]; if (unit === "万") return Math.round(base * 10_000); if (unit === "千") return Math.round(base * 1000); return Math.round(base); } export function parseScore(text: string | null) { if (!text) return null; const t = text.replace(/[^\d.]/g, ""); const n = Number(t); if (!Number.isFinite(n)) return null; if (n > 10) return null; return n; } export function stableId(input: string) { return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16); } export function computeGroupKey(title: string) { const t = title .toLowerCase() .replace(/[【】\[\]()()]/g, " ") .replace(/[^\p{L}\p{N}]+/gu, " ") .trim(); const tokens = t.split(/\s+/).filter(Boolean); const core = tokens.slice(0, 8).join("-"); return core.slice(0, 64) || stableId(title); } export function clamp(n: number, min: number, max: number) { return Math.min(max, Math.max(min, n)); } export async function asyncPool(items: T[], concurrency: number, fn: (item: T) => Promise) { const limit = Math.max(1, Math.floor(concurrency)); const out: R[] = new Array(items.length); let i = 0; async function worker() { while (true) { const idx = i; i += 1; if (idx >= items.length) return; out[idx] = await fn(items[idx]!); } } const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker()); await Promise.all(workers); return out; } export async function withRetry(fn: () => Promise, opts: { retries: number; baseMs: number; maxMs: number; jitter: number }) { let attempt = 0; while (true) { try { return await fn(); } catch (e) { if (attempt >= opts.retries) throw e; const exp = opts.baseMs * 2 ** attempt; const ms = Math.min(opts.maxMs, exp); const j = Math.floor(ms * opts.jitter * Math.random()); await sleep(ms + j); attempt += 1; } } } export function createRateLimiter(qps: number) { const intervalMs = qps <= 0 ? 0 : Math.floor(1000 / qps); let last = 0; return async () => { if (intervalMs <= 0) return; const now = Date.now(); const wait = Math.max(0, last + intervalMs - now); last = now + wait; await sleep(wait); }; }