- POST /api/alerts : token Bearer (comparaison temps constant, 503 si non configuré), schéma d'alerte PRD §4.4 validé Zod, exempté du SSO dans les hooks - Persistance alertes.yaml + commits git (réception, « traité ») - Inbox /alertes : filtres par profil impacté et statut, actions « Marquer traité » et « Créer/MAJ fiche » avec pré-remplissage du formulaire registre depuis l'alerte (aucun champ licence/capacité inventé) - nodered-flows/ : 4 flows importables (rss-ingest, license-watch, classifier avec le prompt figé du PRD §6 embarqué tel quel, notify) — nœuds core uniquement, configuration par variables d'env, README de câblage - Vérifié : check/lint/test (27) /build verts, autofixer propre, JSON des flows et fonctions embarquées validés, test fumée HTTP (201/400/401 sur l'endpoint, inbox, traité + commit, pré-remplissage MAJ fiche)
127 lines
4.3 KiB
JSON
127 lines
4.3 KiB
JSON
[
|
|
{
|
|
"id": "tab_rss_ingest",
|
|
"type": "tab",
|
|
"label": "veille-ia : rss-ingest",
|
|
"disabled": false,
|
|
"info": "PRD §5.1 — Poll flux RSS (HF blog, releases GitHub ComfyUI) → dédup → POST classifier.\n\nVariables d'environnement :\n- CLASSIFIER_URL (défaut http://127.0.0.1:1880/veille-ia/classifier)\n\nPour FreshRSS (API Google Reader) : remplacer le noeud « Sources RSS » par un http request authentifié vers l'instance FreshRSS_ynh.\nSources complémentaires à surveiller via changedetection + license-watch : BFL, MiniMax, Krea, Artificial Analysis, Midjourney, Runway, Suno."
|
|
},
|
|
{
|
|
"id": "rss_inject",
|
|
"type": "inject",
|
|
"z": "tab_rss_ingest",
|
|
"name": "toutes les heures",
|
|
"props": [{ "p": "payload" }],
|
|
"repeat": "3600",
|
|
"crontab": "",
|
|
"once": true,
|
|
"onceDelay": 30,
|
|
"topic": "",
|
|
"payload": "",
|
|
"payloadType": "date",
|
|
"x": 140,
|
|
"y": 100,
|
|
"wires": [["rss_sources"]]
|
|
},
|
|
{
|
|
"id": "rss_sources",
|
|
"type": "function",
|
|
"z": "tab_rss_ingest",
|
|
"name": "Sources RSS",
|
|
"func": "// Un message par flux. URLs vérifiées au 2026-08 :\n// - blog Hugging Face (feed.xml)\n// - releases GitHub (.atom, fonctionne pour tout dépôt)\nconst FEEDS = [\n 'https://huggingface.co/blog/feed.xml',\n 'https://github.com/comfyanonymous/ComfyUI/releases.atom'\n];\nreturn [FEEDS.map((url) => ({ url, method: 'GET' }))];",
|
|
"outputs": 1,
|
|
"timeout": "",
|
|
"noerr": 0,
|
|
"initialize": "",
|
|
"finalize": "",
|
|
"libs": [],
|
|
"x": 330,
|
|
"y": 100,
|
|
"wires": [["rss_requete"]]
|
|
},
|
|
{
|
|
"id": "rss_requete",
|
|
"type": "http request",
|
|
"z": "tab_rss_ingest",
|
|
"name": "GET flux",
|
|
"method": "use",
|
|
"ret": "txt",
|
|
"paytoqs": "ignore",
|
|
"url": "",
|
|
"tls": "",
|
|
"persist": false,
|
|
"proxy": "",
|
|
"insecureHTTPParser": false,
|
|
"authType": "",
|
|
"senderr": false,
|
|
"headers": [],
|
|
"x": 510,
|
|
"y": 100,
|
|
"wires": [["rss_xml"]]
|
|
},
|
|
{
|
|
"id": "rss_xml",
|
|
"type": "xml",
|
|
"z": "tab_rss_ingest",
|
|
"name": "XML → objet",
|
|
"property": "payload",
|
|
"attr": "$",
|
|
"chr": "",
|
|
"x": 670,
|
|
"y": 100,
|
|
"wires": [["rss_normaliser"]]
|
|
},
|
|
{
|
|
"id": "rss_normaliser",
|
|
"type": "function",
|
|
"z": "tab_rss_ingest",
|
|
"name": "Normaliser + dédup",
|
|
"func": "// Normalise RSS 2.0 et Atom, déduplique par guid (contexte flow).\nfunction texteDe(v) {\n if (v == null) return '';\n if (typeof v === 'string') return v;\n if (Array.isArray(v)) return texteDe(v[0]);\n if (typeof v === 'object') return v._ || v.$t || '';\n return String(v);\n}\nconst canal = msg.payload?.rss?.channel?.[0];\nconst items = canal?.item || msg.payload?.feed?.entry || [];\nconst vus = flow.get('vus') || {};\nconst nouveaux = [];\nfor (const it of items) {\n const titre = texteDe(it.title);\n const lien = texteDe(it.link) || it.link?.[0]?.$?.href || '';\n const guid = texteDe(it.guid) || texteDe(it.id) || lien;\n const texte = texteDe(it.description) || texteDe(it.summary) || texteDe(it.content);\n if (!guid || vus[guid]) continue;\n vus[guid] = Date.now();\n nouveaux.push({\n titreOriginal: titre || '(sans titre)',\n texte: `${titre}\\n\\n${texte}`,\n lien,\n typeSuggere: 'nouvelle_sortie'\n });\n}\nflow.set('vus', vus);\nif (nouveaux.length === 0) return null;\nconst classifier = env.get('CLASSIFIER_URL') || 'http://127.0.0.1:1880/veille-ia/classifier';\nreturn [nouveaux.map((n) => ({\n ...n,\n method: 'POST',\n url: classifier,\n headers: { 'content-type': 'application/json' },\n payload: n.texte\n}))];",
|
|
"outputs": 1,
|
|
"timeout": "",
|
|
"noerr": 0,
|
|
"initialize": "",
|
|
"finalize": "",
|
|
"libs": [],
|
|
"x": 860,
|
|
"y": 100,
|
|
"wires": [["rss_vers_classifier"]]
|
|
},
|
|
{
|
|
"id": "rss_vers_classifier",
|
|
"type": "http request",
|
|
"z": "tab_rss_ingest",
|
|
"name": "POST classifier",
|
|
"method": "use",
|
|
"ret": "txt",
|
|
"paytoqs": "ignore",
|
|
"url": "",
|
|
"tls": "",
|
|
"persist": false,
|
|
"proxy": "",
|
|
"insecureHTTPParser": false,
|
|
"authType": "",
|
|
"senderr": false,
|
|
"headers": [],
|
|
"x": 1070,
|
|
"y": 100,
|
|
"wires": [["rss_debug"]]
|
|
},
|
|
{
|
|
"id": "rss_debug",
|
|
"type": "debug",
|
|
"z": "tab_rss_ingest",
|
|
"name": "items envoyés",
|
|
"active": false,
|
|
"tosidebar": true,
|
|
"console": false,
|
|
"tostatus": true,
|
|
"complete": "titreOriginal",
|
|
"statusVal": "titreOriginal",
|
|
"statusType": "msg",
|
|
"x": 1060,
|
|
"y": 160,
|
|
"wires": []
|
|
}
|
|
]
|