7512ec2bad
Закрыт хвост §5.3: node-скриптовые точки Яндекса уведены на рендер-сервис, чтобы
под self_render ВЕСЬ Яндекс шёл через виртуалку+прокси (не крутил браузер на портале).
- YandexListSource (интерфейс) + LocalYandexListSource (xf4, локальный Playwright) +
RenderServiceYandexListSource (self-render, POST type=yandex-list на виртуалку).
- PlaywrightYandexDirectory (листинг шаг 1) и PlaywrightYandexPhoneSearch (разворот)
расшиты на YandexListSource — заодно стали тестируемы офлайн (6 новых тестов).
- PlaywrightYandexCardReader под self_render получает SelfRenderClient (был PageFetcher).
- Рендер-сервис: режим type=yandex-list (скролл ленты + сбор организаций → {orgs}),
проброс scrolls.
- Провайдер: yandexListSource() по флагу; разворот-Яндекс на виртуалку+прокси.
Итог: при self_render ВКЛ на xf4 и на локальном браузере портала не остаётся НИ ОДНОЙ
точки — 2ГИС и весь Яндекс через виртуалку+прокси. 459/459 тестов, Pint+Larastan чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
3.0 KiB
JavaScript
69 lines
3.0 KiB
JavaScript
// HTTP рендер-сервис на виртуалке (топология Б, §5.6). POST /render {url,type} → {data}.
|
|
// Портал (SelfRenderClient) зовёт как xf4. Ни базы, ни Redis, ни денег — только рисует.
|
|
// Секрет портал↔виртуалка — заголовок X-Render-Key (env RENDER_KEY). Параллельность контекстов —
|
|
// env MAX_CONCURRENCY (по памяти виртуалки, §9). Прокси — env PROXIES. Всё из окружения, не из гита.
|
|
import http from 'node:http';
|
|
import { render, closeBrowser } from './browser.mjs';
|
|
import { createProxyPool } from './proxy-pool.mjs';
|
|
|
|
const PORT = Number(process.env.PORT || 8790);
|
|
const KEY = String(process.env.RENDER_KEY || '');
|
|
const MAX = Math.max(1, Number(process.env.MAX_CONCURRENCY || 4));
|
|
const proxies = createProxyPool(process.env.PROXIES);
|
|
|
|
// Семафор: не больше MAX браузер-контекстов одновременно (память). Лишние ждут очереди.
|
|
let active = 0;
|
|
const waiters = [];
|
|
function acquire() {
|
|
if (active < MAX) { active++; return Promise.resolve(); }
|
|
return new Promise((res) => waiters.push(res));
|
|
}
|
|
function release() {
|
|
active--;
|
|
const next = waiters.shift();
|
|
if (next) { active++; next(); }
|
|
}
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve) => {
|
|
let buf = '';
|
|
req.on('data', (c) => { buf += c; if (buf.length > 1_000_000) req.destroy(); });
|
|
req.on('end', () => resolve(buf));
|
|
});
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const json = (code, obj) => {
|
|
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(obj));
|
|
};
|
|
|
|
if (req.method === 'GET' && req.url === '/health') return json(200, { ok: true, active, max: MAX, proxies: proxies.size });
|
|
if (req.method !== 'POST' || req.url !== '/render') return json(404, { error: 'not found' });
|
|
if (KEY === '' || req.headers['x-render-key'] !== KEY) return json(401, { error: 'unauthorized' });
|
|
|
|
let payload;
|
|
try { payload = JSON.parse(await readBody(req)); } catch { return json(400, { error: 'bad json' }); }
|
|
const url = String(payload?.url || '');
|
|
const allowed = ['initialState', 'html', 'yandex-list'];
|
|
const type = allowed.includes(payload?.type) ? payload.type : 'html';
|
|
const scrolls = Math.max(1, Math.min(30, Number(payload?.scrolls) || 6));
|
|
if (!/^https?:\/\//i.test(url)) return json(400, { error: 'bad url' });
|
|
|
|
await acquire();
|
|
try {
|
|
const data = await render(url, type, proxies.next(), scrolls);
|
|
json(200, { data });
|
|
} catch (e) {
|
|
json(200, { data: '', error: String(e).slice(0, 200) }); // не роняем поток портала — пустые данные
|
|
} finally {
|
|
release();
|
|
}
|
|
});
|
|
|
|
server.listen(PORT, () => console.log(`[render-service] :${PORT} max=${MAX} proxies=${proxies.size}`));
|
|
|
|
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
process.on(sig, async () => { await closeBrowser(); server.close(() => process.exit(0)); });
|
|
}
|