'use strict'; /* * token-server-ctx.js — тот же HTTP-контракт, что и token-server.js, но поверх * chrome-launcher-ctx.js (1 процесс Chrome × N browser contexts). * * Отдельный файл + отдельный порт, чтобы гонять A/B рядом со старым сервером, * не трогая боевой. Когда убедишься что лучше — в token-server.js меняется * ОДНА строка импорта на './chrome-launcher-ctx.js', этот файл больше не нужен. * * Запуск на VM: * $env:PORT="3003"; $env:INSTANCES="30"; $env:SHARDS="1" * pm2 start token-server-ctx.js --name tokens-ctx */ import 'dotenv/config'; import express from 'express'; import { getTokenForAccount, registerHlsSession, ensurePool, shutdownPool, rotateProxies, poolStats, probeExitIps, selfTest, } from './chrome-launcher-ctx.js'; const app = express(); const PORT = process.env.PORT || 3003; const API_KEY = process.env.API_KEY || 'GCHksldkkqg2254fobvmgjhtrk4o35igojvkfdothrypewohkj74365@'; async function startPool() { try { await ensurePool(); } catch (e) { console.error('[pool] Ошибка инициализации пула:', e.message, '— повтор через 10с'); setTimeout(startPool, 10000); } } startPool(); app.use(express.json()); app.use((req, res, next) => { if (req.headers['x-api-key'] !== API_KEY) { return res.status(401).json({ ok: false, error: 'unauthorized' }); } next(); }); app.get('/health', (_req, res) => res.json({ ok: true })); // Диагностика пула — сколько контекстов живо/занято app.get('/stats', (_req, res) => res.json({ ok: true, ...poolStats() })); // Реальные exit-IP по контекстам — проверка, что прокси развели IP. // Ходит в ipify из каждого контекста, на 30 слотах занимает несколько секунд. app.get('/probe-ips', async (_req, res) => { try { res.json({ ok: true, ...(await probeExitIps()) }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // POST /token body: { oauth, proxy? } → { ok, token, sessionId, deviceId, proxy, userAgent, headers } // proxy (host:port:user:pass или url) — минтить на КОНКРЕТНОМ прокси аккаунта: // форвардер слота переключается на него + reload. Без proxy — на прокси слота (как было). app.post('/token', async (req, res) => { const { oauth, proxy } = req.body || {}; if (!oauth) return res.status(400).json({ ok: false, error: 'oauth required' }); try { const result = await getTokenForAccount(oauth, { timeout: 60000, proxy: proxy || null }); res.json({ ok: true, ...result }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // POST /selftest body: { oauth, target? } // Минт + follow/unfollow внутри ОДНОГО контекста. integrityFailed=false → минт живой, // значит «failed integrity check» в панели даёт маршрутизация GQL, а не контексты. app.post('/selftest', async (req, res) => { const { oauth, target, mode } = req.body || {}; if (!oauth) return res.status(400).json({ ok: false, error: 'oauth required' }); try { const opts = {}; if (target) opts.target = target; if (mode) opts.mode = mode; // fetch | sniff | both (деф both) const result = await selfTest(oauth, opts); res.json({ ok: true, ...result }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // POST /hls-register body: { oauth, channel } → { ok, status, streams } // Дёргается панелью из дропсов (server.js _dropsRegisterHls). Ответ отдаём как есть — // панель читает data.ok и data.streams на верхнем уровне. app.post('/hls-register', async (req, res) => { const { oauth, channel } = req.body || {}; if (!oauth || !channel) return res.status(400).json({ ok: false, error: 'oauth and channel required' }); try { const result = await registerHlsSession(oauth, channel, { timeout: 45000 }); res.json(result); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // POST /rotate-pool — пересоздаёт контексты со свежими sticky-session (новые exit-IP) app.post('/rotate-pool', async (_req, res) => { console.log('[token-server-ctx] Запрос на ротацию контекстов...'); try { await rotateProxies(); res.json({ ok: true, message: 'Контексты обновлены', ...poolStats() }); } catch (e) { console.error('[token-server-ctx] Ошибка ротации:', e.message); res.status(500).json({ ok: false, error: e.message }); } }); const server = app.listen(PORT, () => { console.log(`Token server (ctx) running on port ${PORT}`); console.log(`API key: ${API_KEY}`); }); server.on('error', (e) => { if (e.code === 'EADDRINUSE') { console.error(`[server] Порт ${PORT} занят, повтор через 3с...`); setTimeout(() => server.listen(PORT), 3000); } else { console.error('[server] Ошибка:', e.message); } }); // Watchdog — если сервер не отвечает за 10с, выходим, pm2 поднимет заново setInterval(async () => { try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 10000); await fetch(`http://127.0.0.1:${PORT}/health`, { headers: { 'x-api-key': API_KEY }, signal: ctrl.signal }); clearTimeout(t); } catch (e) { console.error('[watchdog] Сервер не отвечает, перезапуск:', e.message); process.exit(1); } }, 2 * 60 * 1000); function gracefulShutdown() { console.log('[server] Завершение...'); shutdownPool(); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 5000).unref(); } process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown);