1 line
10 KiB
JSON
1 line
10 KiB
JSON
{"repo": "bads10/cybrief", "files": {"backend/services/deepseek_service.js": {"9f2f66a9ec0eba622075e7d80109ac9d4c9ef14e": {"t": 1785746706.4232395, "n": 4787, "h": "d4e0db63141a7bde", "c": "/**\n * Service IA — DeepSeek (API compatible OpenAI)\n *\n * Remplace Gemini pour la génération de résumés bilingues + classification.\n * Pour les sources X/Twitter (opts.isTweet), DeepSeek juge aussi la pertinence :\n * un tweet n'est conservé que si `relevant: true` (filtre anti-bruit).\n *\n * Sortie : même schéma que le service Gemini, avec en plus le champ `relevant`.\n */\n\nconst axios = require('axios');\nrequire('dotenv').config();\n\nconst DEEPSEEK_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/chat/completions';\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\n\nfunction buildPrompt(item, { isTweet }) {\n const sourceLine = isTweet\n ? `\\nSource : tweet/X (texte court, contient souvent un lien vers l'article complet).`\n : '';\n\n const relevanceRule = isTweet\n ? `\n- \"relevant\": true SI ce tweet rapporte une vraie information de cybersécurité\n (vulnérabilité, attaque, fuite de données, alerte, recherche, CVE, malware, APT...).\n false si c'est une promo, un avis personnel sans info, une réponse hors-sujet,\n un retweet sans valeur, une offre d'emploi, une annonce produit marketing, ou du bruit.`\n : '\\n- \"relevant\": true (toujours, ce n\\'est pas un tweet).';\n\n return `\nTu es un analyste cybersécurité expert. Traite cette alerte et génère du contenu BILINGUE (français ET anglais).${sourceLine}\n\nRéponds UNIQUEMENT en JSON valide (un seul objet), sans bloc markdown :\n\n{\n \"relevant\": true | false,\n \"title\": \"Titre en français (concis, percutant, max 100 caractères)\",\n \"titleEn\": \"Title in English (concise, impactful, max 100 characters)\",\n \"summary\": \"Résumé en 3-4 phrases en français, clair et actionnable pour un analyste SOC\",\n \"summaryEn\": \"3-4 sentence summary in English, clear and actionable for a SOC analyst\",\n \"severity\": \"Faible\" | \"Moyen\" | \"Élevé\" | \"Critique\",\n \"tags\": \"tag1, tag2, tag3 (en français, ex: Ransomware, Vulnérabilité, Zero-Day)\",\n \"cve\": \"CVE-XXXX-YYYY, ... (CVE mentionnés séparés par virgule, ou chaîne vide)\",\n \"attackType\": \"Type d'attaque principal en français (ex: Ransomware, Phishing, Supply Chain, DDoS, Zero-Day, APT) ou chaîne vide\",\n \"affectedSystems\": \"Systèmes/logiciels/versions affectés séparés par virgule ou chaîne vide\",\n \"iocs\": \"Indicateurs séparés par | (ex: Hash:abc123|IP:1.2.3.4|Domaine:evil.com) ou chaîne vide\"\n}\n\nRègles :${relevanceRule}\n- Si relevant=false, remplis quand même title/summary brièvement mais l'article sera ignoré.\n\nTitre original: ${item.title || ''}\nLien: ${item.link || ''}\nDate: ${item.isoDate || item.pubDate || ''}\nContenu: ${(item.contentSnippet || item.content || '').slice(0, 2000)}\n`.trim();\n}\n\n/**\n * Résume un item via DeepSeek.\n * @param {object} item - item RSS ({ title, link, contentSnippet, content, ... })\n * @param {object} opts - { isTweet?: boolean }\n * @returns {Promise<object|null>} objet structuré (avec `relevant`) ou null si échec dur\n */\nasync function summarizeWithDeepSeek(item, opts = {}) {\n const apiKey = process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n console.error('[Cybrief][DeepSeek] DEEPSEEK_API_KEY manquante');\n return null;\n }\n\n const prompt = buildPrompt(item, { isTweet: !!opts.isTweet });\n\n try {\n const res = await axios.post(\n DEEPSEEK_URL,\n {\n model: DEEPSEEK_MODEL,\n messages: [\n { role: 'system', content: 'Tu réponds uniquement par un objet JSON valide.' },\n { role: 'user', content: prompt },\n ],\n response_format: { type: 'json_object' },\n temperature: 0.2,\n },\n {\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\n timeout: 60000,\n }\n );\n\n const raw = res.data.choices?.[0]?.message?.content || '{}';\n const clean = raw.replace(/```json\\s*/gi, '').replace(/```/g, '').trim();\n\n let parsed;\n try {\n parsed = JSON.parse(clean);\n } catch {\n console.warn('[Cybrief][DeepSeek] JSON invalide, fallback brut');\n return {\n relevant: true,\n summary: (item.contentSnippet || item.title || '').slice(0, 300),\n severity: 'Moyen',\n tags: 'Veille Cyber',\n cve: '', attackType: '', affectedSystems: '', iocs: '',\n };\n }\n\n if (typeof parsed.relevant !== 'boolean') parsed.relevant = true;\n return parsed;\n } catch (err) {\n const status = err.response?.status;\n if (status === 429) {\n console.warn('[Cybrief][DeepSeek] ⚠️ Rate limit (429)');\n } else if (status === 401 || status === 402) {\n console.error(`[Cybrief][DeepSeek] Auth/crédit (${status}) — vérifier la clé et le solde`);\n } else {\n console.error(`[Cybrief][DeepSeek] Erreur ${status || err.message}`);\n }\n return null;\n }\n}\n\nmodule.exports = { summarizeWithDeepSeek };\n"}, "7cf244ad147c478ae12c36cf61614c9e081442fb": {"t": 1785767117.4842904, "n": 4787, "h": "d4e0db63141a7bde", "c": "/**\n * Service IA — DeepSeek (API compatible OpenAI)\n *\n * Remplace Gemini pour la génération de résumés bilingues + classification.\n * Pour les sources X/Twitter (opts.isTweet), DeepSeek juge aussi la pertinence :\n * un tweet n'est conservé que si `relevant: true` (filtre anti-bruit).\n *\n * Sortie : même schéma que le service Gemini, avec en plus le champ `relevant`.\n */\n\nconst axios = require('axios');\nrequire('dotenv').config();\n\nconst DEEPSEEK_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/chat/completions';\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\n\nfunction buildPrompt(item, { isTweet }) {\n const sourceLine = isTweet\n ? `\\nSource : tweet/X (texte court, contient souvent un lien vers l'article complet).`\n : '';\n\n const relevanceRule = isTweet\n ? `\n- \"relevant\": true SI ce tweet rapporte une vraie information de cybersécurité\n (vulnérabilité, attaque, fuite de données, alerte, recherche, CVE, malware, APT...).\n false si c'est une promo, un avis personnel sans info, une réponse hors-sujet,\n un retweet sans valeur, une offre d'emploi, une annonce produit marketing, ou du bruit.`\n : '\\n- \"relevant\": true (toujours, ce n\\'est pas un tweet).';\n\n return `\nTu es un analyste cybersécurité expert. Traite cette alerte et génère du contenu BILINGUE (français ET anglais).${sourceLine}\n\nRéponds UNIQUEMENT en JSON valide (un seul objet), sans bloc markdown :\n\n{\n \"relevant\": true | false,\n \"title\": \"Titre en français (concis, percutant, max 100 caractères)\",\n \"titleEn\": \"Title in English (concise, impactful, max 100 characters)\",\n \"summary\": \"Résumé en 3-4 phrases en français, clair et actionnable pour un analyste SOC\",\n \"summaryEn\": \"3-4 sentence summary in English, clear and actionable for a SOC analyst\",\n \"severity\": \"Faible\" | \"Moyen\" | \"Élevé\" | \"Critique\",\n \"tags\": \"tag1, tag2, tag3 (en français, ex: Ransomware, Vulnérabilité, Zero-Day)\",\n \"cve\": \"CVE-XXXX-YYYY, ... (CVE mentionnés séparés par virgule, ou chaîne vide)\",\n \"attackType\": \"Type d'attaque principal en français (ex: Ransomware, Phishing, Supply Chain, DDoS, Zero-Day, APT) ou chaîne vide\",\n \"affectedSystems\": \"Systèmes/logiciels/versions affectés séparés par virgule ou chaîne vide\",\n \"iocs\": \"Indicateurs séparés par | (ex: Hash:abc123|IP:1.2.3.4|Domaine:evil.com) ou chaîne vide\"\n}\n\nRègles :${relevanceRule}\n- Si relevant=false, remplis quand même title/summary brièvement mais l'article sera ignoré.\n\nTitre original: ${item.title || ''}\nLien: ${item.link || ''}\nDate: ${item.isoDate || item.pubDate || ''}\nContenu: ${(item.contentSnippet || item.content || '').slice(0, 2000)}\n`.trim();\n}\n\n/**\n * Résume un item via DeepSeek.\n * @param {object} item - item RSS ({ title, link, contentSnippet, content, ... })\n * @param {object} opts - { isTweet?: boolean }\n * @returns {Promise<object|null>} objet structuré (avec `relevant`) ou null si échec dur\n */\nasync function summarizeWithDeepSeek(item, opts = {}) {\n const apiKey = process.env.DEEPSEEK_API_KEY;\n if (!apiKey) {\n console.error('[Cybrief][DeepSeek] DEEPSEEK_API_KEY manquante');\n return null;\n }\n\n const prompt = buildPrompt(item, { isTweet: !!opts.isTweet });\n\n try {\n const res = await axios.post(\n DEEPSEEK_URL,\n {\n model: DEEPSEEK_MODEL,\n messages: [\n { role: 'system', content: 'Tu réponds uniquement par un objet JSON valide.' },\n { role: 'user', content: prompt },\n ],\n response_format: { type: 'json_object' },\n temperature: 0.2,\n },\n {\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\n timeout: 60000,\n }\n );\n\n const raw = res.data.choices?.[0]?.message?.content || '{}';\n const clean = raw.replace(/```json\\s*/gi, '').replace(/```/g, '').trim();\n\n let parsed;\n try {\n parsed = JSON.parse(clean);\n } catch {\n console.warn('[Cybrief][DeepSeek] JSON invalide, fallback brut');\n return {\n relevant: true,\n summary: (item.contentSnippet || item.title || '').slice(0, 300),\n severity: 'Moyen',\n tags: 'Veille Cyber',\n cve: '', attackType: '', affectedSystems: '', iocs: '',\n };\n }\n\n if (typeof parsed.relevant !== 'boolean') parsed.relevant = true;\n return parsed;\n } catch (err) {\n const status = err.response?.status;\n if (status === 429) {\n console.warn('[Cybrief][DeepSeek] ⚠️ Rate limit (429)');\n } else if (status === 401 || status === 402) {\n console.error(`[Cybrief][DeepSeek] Auth/crédit (${status}) — vérifier la clé et le solde`);\n } else {\n console.error(`[Cybrief][DeepSeek] Erreur ${status || err.message}`);\n }\n return null;\n }\n}\n\nmodule.exports = { summarizeWithDeepSeek };\n"}}}} |