de544c25b0
Рисовалка топологии Б (§5.6): портал зовёт по HTTP как xf4, она рисует и
отдаёт данные. Ни базы, ни Redis, ни денег. Смоук на живом 2ГИС: листинг 12
фирм, карточка 1 — чистый initialState.
- browser.mjs — постоянный браузер + маскировка + контекст-на-страницу +
museum double-tap для поиска 2ГИС + отсечка картинок/шрифтов/медиа.
- proxy-pool.mjs — round-robin пул прокси из env PROXIES (резидентные/мобильные RU).
- server.mjs — POST /render {url,type}→{data}, auth X-Render-Key, семафор
MAX_CONCURRENCY, /health.
- package.json, README.md, smoke.mjs, .gitignore (node_modules не в гит).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
35 lines
1.3 KiB
JavaScript
35 lines
1.3 KiB
JavaScript
// Пул прокси round-robin. Источник — env PROXIES (через запятую). Провайдер-агностично: резидентные/
|
|
// мобильные RU. Формат каждого: «http://user:pass@host:port» ИЛИ «host:port» (тогда схема http).
|
|
// Ключи/пароли — ТОЛЬКО в .env виртуалки, в гит не идут. Пул пуст → next() отдаёт null (без прокси).
|
|
|
|
function parseOne(raw) {
|
|
const s = String(raw).trim();
|
|
if (s === '') return null;
|
|
const withScheme = /^[a-z]+:\/\//i.test(s) ? s : 'http://' + s;
|
|
let u;
|
|
try { u = new URL(withScheme); } catch { return null; }
|
|
const proxy = { server: `${u.protocol}//${u.host}` };
|
|
if (u.username) proxy.username = decodeURIComponent(u.username);
|
|
if (u.password) proxy.password = decodeURIComponent(u.password);
|
|
return proxy;
|
|
}
|
|
|
|
export function createProxyPool(envValue) {
|
|
const list = String(envValue || '')
|
|
.split(',')
|
|
.map(parseOne)
|
|
.filter(Boolean);
|
|
|
|
let cursor = 0;
|
|
return {
|
|
size: list.length,
|
|
// Следующий прокси по кругу (playwright-объект) или null, если пул пуст.
|
|
next() {
|
|
if (list.length === 0) return null;
|
|
const p = list[cursor % list.length];
|
|
cursor++;
|
|
return p;
|
|
},
|
|
};
|
|
}
|