price-bot / src /core /utils.ts
3v324v23's picture
feat: 初始化电商价格采集工具 CLI 与 Web 演示界面
e068192
import crypto from "crypto";
export function nowIso() {
return new Date().toISOString();
}
export async function sleep(ms: number) {
if (ms <= 0) return;
await new Promise<void>((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<T, R>(items: T[], concurrency: number, fn: (item: T) => Promise<R>) {
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<T>(fn: () => Promise<T>, 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);
};
}