Checkpoint: Ajout de server/articleFetcher.ts (fetch HTTP + extraction 500 mots, timeout 5s, fallback silencieux). Intégration dans rssEngine.ts : si le résumé RSS est court (<150 car.), le 1er paragraphe de l'article est récupéré et transmis à classifyArticle. Signature de classifyArticle mise à jour avec paramètre contenuPage optionnel. 0 erreur TypeScript, 21 tests passés.
This commit is contained in:
94
server/articleFetcher.ts
Normal file
94
server/articleFetcher.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 <p>, <h1>–<h3> 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(/<script[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<nav[\s\S]*?<\/nav>/gi, " ")
|
||||
.replace(/<header[\s\S]*?<\/header>/gi, " ")
|
||||
.replace(/<footer[\s\S]*?<\/footer>/gi, " ")
|
||||
.replace(/<aside[\s\S]*?<\/aside>/gi, " ")
|
||||
.replace(/<noscript[\s\S]*?<\/noscript>/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<string | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user