/** * articleFetcher.ts * * Récupère le contenu textuel des 500 premiers mots d'un article web * pour enrichir la classification IA quand le résumé RSS est trop court. * * Stratégie : * - Fetch HTTP avec timeout 5 secondes * - Extraction des balises

,

du HTML brut (sans dépendance lourde) * - Limite à 500 mots pour rester dans le contexte LLM * - Retourne null en cas d'erreur (timeout, 403, paywall, JS-only…) — silencieux */ const FETCH_TIMEOUT_MS = 5_000; const MAX_WORDS = 500; /** * Extrait le texte brut des balises de contenu d'un HTML. * Approche légère sans parser DOM complet. */ function extractTextFromHtml(html: string): string { // Supprimer les balises script, style, nav, header, footer, aside let cleaned = html .replace(//gi, " ") .replace(//gi, " ") .replace(//gi, " ") .replace(//gi, " ") .replace(//gi, " ") .replace(//gi, " ") .replace(//gi, " "); // Extraire le contenu des balises de texte pertinentes const contentTags = cleaned.match(/<(p|h[1-3]|li|blockquote)[^>]*>([\s\S]*?)<\/\1>/gi) ?? []; const texts = contentTags.map((tag) => tag .replace(/<[^>]+>/g, " ") // supprimer les balises HTML restantes .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/ /g, " ") .replace(/"/g, '"') .replace(/&#\d+;/g, " ") .replace(/\s+/g, " ") .trim() ); return texts.filter((t) => t.length > 20).join(" "); } /** * Récupère les premiers ~500 mots du contenu d'un article web. * Retourne null si l'article est inaccessible ou si le contenu est trop pauvre. * * @param url URL de l'article * @returns Texte extrait (500 mots max) ou null */ export async function fetchArticleFirstParagraph(url: string): Promise { try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); const response = await fetch(url, { signal: controller.signal, headers: { "User-Agent": "Mozilla/5.0 (compatible; VeilleBot/1.0; +https://itinova.fr)", Accept: "text/html,application/xhtml+xml", "Accept-Language": "fr-FR,fr;q=0.9", }, }); clearTimeout(timer); if (!response.ok) return null; const contentType = response.headers.get("content-type") ?? ""; if (!contentType.includes("text/html")) return null; const html = await response.text(); const text = extractTextFromHtml(html); if (!text || text.length < 50) return null; // Limiter à MAX_WORDS mots const words = text.split(/\s+/); const truncated = words.slice(0, MAX_WORDS).join(" "); return truncated.length > 50 ? truncated : null; } catch { // Timeout, réseau, CORS, paywall, etc. — silencieux return null; } }