'use strict'; /* index-textbooks-headless.js — полный RAG-индекс: рендерит каждый учебник * настоящим браузером (puppeteer-core + системный Chrome/Edge) через локальный * сервер и забирает РЕНДЕРНЫЙ текст параграфов. Покрывает и JS-рендеримые * учебники (математика/физика-движки), которых нет в статическом HTML. * * Требует запущенный сервер (localhost:3000). Долгая операция (минуты). * Запуск: node backend/scripts/index-textbooks-headless.js * Дополняет/замещает чанки только для успешно отрендеренных учебников. */ require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); const fs = require('fs'); const path = require('path'); const jwt = require('jsonwebtoken'); const db = require('../src/db/db'); /* Текстовые страницы учебника требуют логина — выпускаем служебный JWT. */ function authToken() { const u = db.prepare("SELECT id, role, token_version FROM users WHERE is_banned = 0 AND role IN ('admin','teacher') ORDER BY id LIMIT 1").get() || db.prepare('SELECT id, role, token_version FROM users WHERE is_banned = 0 ORDER BY id LIMIT 1').get(); if (!u || !process.env.JWT_SECRET) return null; return jwt.sign({ id: u.id, role: u.role, tv: u.token_version }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '4h' }); } const BASE = process.env.ASSISTANT_INDEX_BASE || ('http://localhost:' + (process.env.PORT || 3000)); const BROWSERS = [ 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe', ]; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); async function run() { const puppeteer = require('puppeteer-core'); const exe = BROWSERS.find(p => { try { return fs.existsSync(p); } catch (e) { return false; } }); if (!exe) { console.error('Браузер не найден (Chrome/Edge)'); process.exit(1); } const books = db.prepare('SELECT slug, title FROM textbooks WHERE is_active = 1 ORDER BY slug').all(); const del = db.prepare('DELETE FROM textbook_chunks WHERE slug = ?'); const ins = db.prepare('INSERT INTO textbook_chunks (slug, textbook_title, section_title, text, section_ref) VALUES (?, ?, ?, ?, ?)'); const token = authToken(); if (!token) { console.error('Не удалось выпустить токен (нет пользователя или JWT_SECRET)'); process.exit(1); } const browser = await puppeteer.launch({ executablePath: exe, headless: true, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] }); const page = await browser.newPage(); await page.setViewport({ width: 1100, height: 900 }); await page.evaluateOnNewDocument((t) => { try { localStorage.setItem('ls_token', t); } catch (e) {} }, token); let totalChunks = 0, okBooks = 0; for (const b of books) { let chunks = []; try { await page.goto(`${BASE}/textbook/${b.slug}`, { waitUntil: 'networkidle2', timeout: 25000 }); await page.waitForSelector('.psel-card, .sec', { timeout: 12000 }).catch(() => {}); await sleep(400); const ids = await page.$$eval('.psel-card[data-id]', els => els.map(e => ({ id: e.dataset.id, name: ((e.querySelector('.psel-name') || {}).textContent || '').trim() }))); if (ids.length) { for (const s of ids) { try { await page.evaluate(id => { const c = document.querySelector('.psel-card[data-id="' + id + '"]'); if (c) c.click(); }, s.id); await sleep(550); const text = await page.evaluate(() => { const a = document.querySelector('.sec.active'); return a ? a.innerText.replace(/\s+/g, ' ').trim() : ''; }); if (text && text.length >= 80) chunks.push({ section: s.name.slice(0, 160), text: text.slice(0, 2000), ref: s.id }); } catch (e) {} } } else { const secs = await page.$$eval('.sec', els => els.map(e => e.innerText.replace(/\s+/g, ' ').trim())); secs.forEach(t => { if (t && t.length >= 80) chunks.push({ section: '', text: t.slice(0, 2000) }); }); } } catch (e) { /* книга не отрендерилась — оставляем как было */ } if (chunks.length) { del.run(b.slug); for (const c of chunks) ins.run(b.slug, b.title || b.slug, c.section, c.text, c.ref || null); okBooks++; totalChunks += chunks.length; console.log(` ${b.slug}: ${chunks.length}`); } else { console.log(` ${b.slug}: — (нет рендера, оставлено как есть)`); } } await browser.close(); console.log(`[headless] готово: ${okBooks}/${books.length} учебников, ${totalChunks} чанков (перезаписаны).`); process.exit(0); } run().catch(e => { console.error('[headless] ошибка:', e.message); process.exit(1); });