diff --git a/workflows/brief-vorbereiten.json b/workflows/brief-vorbereiten.json new file mode 100644 index 0000000..003ddbe --- /dev/null +++ b/workflows/brief-vorbereiten.json @@ -0,0 +1,1167 @@ +{ + "name": "brief-vorbereiten", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -460, + 40 + ], + "id": "e1787a43-3202-4cb7-8ae3-95c68df72c06", + "name": "Manuell starten" + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtDay": [ + 1 + ], + "triggerAtHour": 7, + "triggerAtMinute": 0 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + -460, + 200 + ], + "id": "905bc58d-522a-492c-bb33-1e67646ea176", + "name": "Zeitplan Montag 07:00", + "disabled": true + }, + { + "parameters": { + "httpMethod": "POST", + "path": "brief-vorbereiten", + "responseMode": "onReceived", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -460, + 360 + ], + "id": "9397421d-bbda-4219-8083-ea9d9b60d2ae", + "name": "Webhook Start", + "webhookId": "e0c767c4-135c-43ff-85b1-3208ccac9064" + }, + { + "parameters": { + "mode": "manual", + "assignments": { + "assignments": [ + { + "id": "fc40dace-efed-412d-b48d-be620f1219d2", + "name": "nw_base", + "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", + "type": "string" + }, + { + "id": "1533065c-b69b-445b-b900-3fcfda2dea32", + "name": "nw_project", + "value": "3da4d745e0e223cb", + "type": "string" + }, + { + "id": "d44820e2-6e02-4b3e-ad1d-5325ad9c6955", + "name": "nw_engine", + "value": "google.de", + "type": "string" + }, + { + "id": "f8e3b6a7-5b9b-4cee-bb32-182d91e490cf", + "name": "nw_language", + "value": "German", + "type": "string" + }, + { + "id": "d2220a20-09dd-4f0c-b369-659eeed0282d", + "name": "gitea_base", + "value": "https://gitea.lucas-orth.de", + "type": "string" + }, + { + "id": "8dcc7273-ce87-467a-a962-a18ca003a539", + "name": "gitea_owner", + "value": "lucas.orth", + "type": "string" + }, + { + "id": "6a5c4de2-2f89-49e1-b55d-788b2321ab1b", + "name": "gitea_repo", + "value": "Skrift-Auto-Article", + "type": "string" + }, + { + "id": "eef11081-81d9-491e-9fef-75a32eda1cc2", + "name": "branch", + "value": "main", + "type": "string" + }, + { + "id": "fe210a4a-89ca-4b77-8d7f-f21bd18db295", + "name": "index_pfad", + "value": "artikel-index.json", + "type": "string" + }, + { + "id": "a46ba44a-eb52-46de-a124-079ab068bc5c", + "name": "brief_ordner", + "value": "briefe", + "type": "string" + }, + { + "id": "d21f91b9-1959-4ded-896f-1758e98b6959", + "name": "poll_max", + "value": 30, + "type": "number" + }, + { + "id": "ad1bab66-9d3b-4a1a-82e6-d1cf5219eec6", + "name": "poll_sekunden", + "value": 20, + "type": "number" + }, + { + "id": "46000330-8284-4ecd-af14-46c0752cda76", + "name": "pause_zwischen_artikeln", + "value": 5, + "type": "number" + }, + { + "id": "209e8874-8b8c-4873-a9ba-f13ad65ad05b", + "name": "benachrichtigung_an", + "value": "privat@lucas-orth.de", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + -240, + 200 + ], + "id": "43da0e65-2f7e-4617-aca1-f256f246f2cb", + "name": "Konfiguration" + }, + { + "parameters": { + "method": "GET", + "url": "={{ $json.gitea_base }}/api/v1/repos/{{ $json.gitea_owner }}/{{ $json.gitea_repo }}/contents/{{ $json.index_pfad }}?ref={{ $json.branch }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": false + } + }, + "timeout": 60000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + -20, + 200 + ], + "id": "4d42aa4f-6a82-403f-b4d5-fd75282e5e3c", + "name": "Artikeltabelle lesen", + "credentials": { + "httpHeaderAuth": { + "id": "VOD4JM5zwpSC8wAr", + "name": "Gitea Skrift Auto Article" + } + }, + "onError": "continueErrorOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000 + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst e = $input.first().json;\nthrow new Error(\n 'Artikeltabelle konnte nicht gelesen werden. Pruefen: Gitea-Token-Scope (write:repository noetig), '\n + 'Pfad artikel-index.json im Branch main, Repo-Name. Antwort: '\n + JSON.stringify(e).slice(0, 600)\n);\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -20, + 420 + ], + "id": "d47a2d36-c2c3-40dc-b0d4-bb9bd12727e8", + "name": "Fehler Artikeltabelle" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\n// Gitea liefert den Dateiinhalt base64-kodiert.\nconst antwort = $input.first().json;\nif (!antwort.content) {\n throw new Error('Gitea-Antwort enthaelt kein Feld \"content\". Erhalten: ' + JSON.stringify(antwort).slice(0,400));\n}\nconst roh = Buffer.from(antwort.content, 'base64').toString('utf8');\nlet tabelle;\ntry { tabelle = JSON.parse(roh); }\ncatch (e) { throw new Error('artikel-index.json ist kein gueltiges JSON: ' + e.message); }\nif (!Array.isArray(tabelle)) {\n throw new Error('artikel-index.json muss ein Array sein, ist aber: ' + typeof tabelle);\n}\n\nconst konf = $('Konfiguration').first().json;\nconst pflicht = ['slug','titel','keyword','kategorie','cta_ziel','status'];\n\nconst offen = [];\ntabelle.forEach((zeile, i) => {\n if (zeile.status !== 'offen') return;\n const fehlt = pflicht.filter(f => !zeile[f]);\n if (fehlt.length) {\n throw new Error(`Zeile ${i} (slug: ${zeile.slug || '?'}) fehlen Pflichtfelder: ${fehlt.join(', ')}`);\n }\n offen.push({ json: { ...zeile, zeilen_index: i } });\n});\n\n// Kontext fuer spaetere Nodes einmal ablegen\nconst kontext = { index_sha: antwort.sha, tabelle_gesamt: tabelle, anzahl_offen: offen.length };\n$getWorkflowStaticData('global').lauf = kontext;\n\nif (offen.length === 0) return [{ json: { ...kontext, leer: true } }];\nreturn offen.map(o => ({ json: { ...o.json, index_sha: antwort.sha } }));\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 200, + 200 + ], + "id": "516ce26d-42a5-4a31-a1bf-6368cd2b3fb3", + "name": "Tabelle auswerten" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "fe2fd068-eb1c-4ac5-a379-40a18fdf1599", + "leftValue": "={{ $json.leer === true }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "looseTypeValidation": true, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 420, + 200 + ], + "id": "e608a1ef-ae5f-4eeb-bf16-545c77ec1156", + "name": "Offene Artikel vorhanden?" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 640, + 60 + ], + "id": "b9e02838-18d0-43b7-a1e3-7c9ea3e5d4ca", + "name": "Nichts zu tun" + }, + { + "parameters": { + "batchSize": 1, + "options": { + "reset": false + } + }, + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 640, + 320 + ], + "id": "638db643-ff14-4875-80af-8c717b045a48", + "name": "Artikel nacheinander" + }, + { + "parameters": { + "method": "POST", + "url": "={{ $('Konfiguration').first().json.nw_base }}/new-query", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": false + } + }, + "timeout": 60000 + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ project: $('Konfiguration').first().json.nw_project, keyword: $json.keyword, engine: $('Konfiguration').first().json.nw_engine, language: $('Konfiguration').first().json.nw_language }) }}", + "sendHeaders": true, + "specifyHeaders": "json", + "jsonHeaders": "={\"Content-Type\":\"application/json\"}" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 900, + 420 + ], + "id": "6979623d-d28f-4278-880d-2a91bcfe9e11", + "name": "Analyse starten", + "credentials": { + "httpHeaderAuth": { + "id": "U23n2O980GKWocWq", + "name": "NeuronWriter API" + } + }, + "onError": "continueErrorOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000 + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst artikel = $('Artikel nacheinander').first().json;\nconst e = $input.first().json;\nreturn [{ json: {\n slug: artikel.slug, keyword: artikel.keyword, ergebnis: 'fehler',\n stufe: 'new-query',\n meldung: 'NeuronWriter /new-query fehlgeschlagen fuer Keyword \"' + artikel.keyword\n + '\". Haeufige Ursachen: Query-Kontingent erschoepft, falsche Projekt-ID, API-Key ungueltig. Antwort: '\n + JSON.stringify(e).slice(0, 500)\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 900, + 640 + ], + "id": "1e07b3e2-ab81-4273-bbe5-9b4d5c9c52c5", + "name": "Fehler Analyse starten", + "onError": "continueRegularOutput" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst artikel = $('Artikel nacheinander').first().json;\nconst neu = $input.first().json;\nif (!neu.query) {\n throw new Error('NeuronWriter lieferte kein Feld \"query\". Antwort: ' + JSON.stringify(neu).slice(0,400));\n}\nreturn [{ json: { ...artikel, query: neu.query, query_url: neu.query_url,\n share_url: neu.share_url, versuch: 0 } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 420 + ], + "id": "4e8658a3-d8cc-4859-b9a7-c1064b46a6bd", + "name": "Poll vorbereiten" + }, + { + "parameters": { + "amount": "={{ $('Konfiguration').first().json.poll_sekunden }}", + "unit": "seconds" + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 1340, + 420 + ], + "id": "a3c33dc6-3c93-4f0b-953a-a8262cb87daf", + "name": "Warten", + "webhookId": "120963a7-ba5e-43a6-b9bb-83bc95f7acf9" + }, + { + "parameters": { + "method": "POST", + "url": "={{ $('Konfiguration').first().json.nw_base }}/get-query", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": false + } + }, + "timeout": 60000 + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ query: $json.query }) }}", + "sendHeaders": true, + "specifyHeaders": "json", + "jsonHeaders": "={\"Content-Type\":\"application/json\"}" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1560, + 420 + ], + "id": "8a17fa19-a403-4713-ac2f-39ab9a3b49b7", + "name": "Analyse abfragen", + "credentials": { + "httpHeaderAuth": { + "id": "U23n2O980GKWocWq", + "name": "NeuronWriter API" + } + }, + "onError": "continueErrorOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000 + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst a = $('Poll vorbereiten').first().json;\nreturn [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',\n stufe: 'get-query',\n meldung: 'NeuronWriter /get-query fehlgeschlagen fuer query ' + a.query + '. Antwort: '\n + JSON.stringify($input.first().json).slice(0, 500) }}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1560, + 640 + ], + "id": "3fb2a872-fecb-4b3e-a9b5-c9538111eb84", + "name": "Fehler Analyse abfragen", + "onError": "continueRegularOutput" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "68b33733-066e-40c4-a697-226bf1d37a0f", + "leftValue": "={{ $json.status }}", + "rightValue": "ready", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "looseTypeValidation": true, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1780, + 420 + ], + "id": "a92a355d-59c7-4022-8e39-a52a81bc6ff6", + "name": "Analyse fertig?" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst poll = $('Poll vorbereiten').first().json;\nconst antwort = $input.first().json;\nconst versuch = (poll.versuch || 0) + $runIndex + 1;\nreturn [{ json: { ...poll, versuch, letzter_status: antwort.status || 'unbekannt' } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 660 + ], + "id": "59f92f39-c2ac-4af2-863e-7d94fdaf83f4", + "name": "Versuch zaehlen" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "73ff3b19-579a-4908-8cf5-2f95df88cf0a", + "leftValue": "={{ $json.versuch }}", + "rightValue": "={{ $('Konfiguration').first().json.poll_max }}", + "operator": { + "type": "number", + "operation": "gte" + } + } + ], + "combinator": "and" + }, + "looseTypeValidation": true, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 2000, + 660 + ], + "id": "729a9708-2f4b-4e4b-8fc6-d5dc226773ca", + "name": "Poll-Limit erreicht?" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst a = $input.first().json;\nreturn [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',\n stufe: 'poll-timeout',\n meldung: 'NeuronWriter-Analyse fuer \"' + a.keyword + '\" war nach ' + a.versuch\n + ' Versuchen noch nicht fertig (letzter Status: ' + a.letzter_status\n + '). Analyse laeuft ggf. weiter, query merken: ' + a.query }}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + 780 + ], + "id": "c34d32e0-ef60-4e5c-8bf0-fb7274fa3c48", + "name": "Timeout melden" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\n// Feldnamen stammen aus einer echten /get-query-Antwort, siehe docs/feldnachweis.md\nconst a = $('Poll vorbereiten').first().json;\nconst n = $input.first().json;\n\nconst liste = (arr) => (arr || []).map(x => ({\n begriff: x.t,\n min: Array.isArray(x.sugg_usage) ? x.sugg_usage[0] : null,\n max: Array.isArray(x.sugg_usage) ? x.sugg_usage[1] : null,\n bei_wettbewerbern_pc: x.usage_pc ?? null,\n}));\n\nconst fragen = [];\nconst gesehen = new Set();\nconst addFrage = (q, quelle, gewicht) => {\n if (!q) return;\n const k = q.trim().toLowerCase();\n if (gesehen.has(k)) return;\n gesehen.add(k);\n fragen.push({ frage: q.trim(), quelle, gewicht });\n};\nObject.entries((n.ideas && n.ideas.topic_matrix) || {})\n .sort((x, y) => (y[1].importance || 0) - (x[1].importance || 0))\n .forEach(([q, v]) => addFrage(q, 'topic_matrix', v.importance || null));\n((n.ideas || {}).people_also_ask || []).forEach(x => addFrage(x.q, 'people_also_ask', null));\n((n.ideas || {}).suggest_questions || []).forEach(x => addFrage(x.q, 'suggest_questions', null));\n((n.ideas || {}).content_questions || []).forEach(x => addFrage(x.q, 'content_questions', null));\n\nconst brief = {\n // --- die vom Auftrag geforderten Felder ---\n query_id: n.query,\n keyword: n.keyword,\n begriffe_pflicht: liste((n.terms || {}).content_basic),\n begriffe_erweitert: liste((n.terms || {}).content_extended),\n fragen,\n wortzahl_ziel: ((n.metrics || {}).word_count || {}).target ?? null,\n\n // --- Artikelkontext aus der Tabelle ---\n artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel },\n\n // --- Zusatz fuer die Schreibvorlage ---\n zusatz: {\n wortzahl_median: ((n.metrics || {}).word_count || {}).median ?? null,\n lesbarkeit_ziel: ((n.metrics || {}).readability || {}).target ?? null,\n begriffe_titel: ((n.terms || {}).title || []).map(x => x.t),\n begriffe_description: ((n.terms || {}).desc || []).map(x => x.t),\n begriffe_h1: ((n.terms || {}).h1 || []).map(x => x.t),\n begriffe_h2: ((n.terms || {}).h2 || []).map(x => x.t),\n entitaeten: ((n.terms || {}).entities || []).slice(0, 15).map(x => ({ name: x.t, wichtigkeit: x.importance })),\n suchintention: (n.serp_summary || {}).top_intent ?? null,\n haeufigster_inhaltstyp: (n.serp_summary || {}).top_content_type ?? null,\n gliederungen_top5: (n.competitors || []).slice(0, 5).map(c => ({\n rang: c.rank, url: c.url, score: c.content_score, wortzahl: c.word_count,\n ueberschriften: (c.headers || []).map(h => `${h[0]}: ${h[1]}`).slice(0, 25),\n })),\n terms_txt_content_basic: ((n.terms_txt || {}).content_basic_w_ranges) ?? null,\n },\n\n meta: {\n erzeugt_am: new Date().toISOString(),\n analyse_url: n.query_url || a.query_url || null,\n freigabe_url: n.share_url || a.share_url || null,\n sprache: n.language, suchmaschine: n.engine, projekt: n.project,\n },\n};\n\nreturn [{ json: { slug: a.slug, zeilen_index: a.zeilen_index, query: n.query,\n keyword: n.keyword, brief } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + 300 + ], + "id": "b1468413-2e7b-4f94-96c4-b0a7179b1b67", + "name": "Brief bauen" + }, + { + "parameters": { + "method": "GET", + "url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.brief_ordner }}/brief-{{ $json.slug }}.json?ref={{ $('Konfiguration').first().json.branch }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": true, + "fullResponse": true + } + }, + "timeout": 60000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 2440, + 300 + ], + "id": "8975c0b7-b424-40e6-8dd4-590669bca105", + "name": "Brief vorhanden?", + "credentials": { + "httpHeaderAuth": { + "id": "VOD4JM5zwpSC8wAr", + "name": "Gitea Skrift Auto Article" + } + } + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst b = $('Brief bauen').first().json;\nconst p = $input.first().json;\nconst vorhanden = p.statusCode === 200 && p.body && p.body.sha;\n\nconst inhalt = Buffer.from(JSON.stringify(b.brief, null, 2), 'utf8').toString('base64');\nconst konf = $('Konfiguration').first().json;\n\nreturn [{ json: {\n ...b,\n methode: vorhanden ? 'PUT' : 'POST',\n pfad: `${konf.brief_ordner}/brief-${b.slug}.json`,\n commit_body: JSON.stringify({\n content: inhalt,\n message: `Brief ${vorhanden ? 'aktualisiert' : 'erzeugt'}: ${b.slug} (${b.keyword})`,\n branch: konf.branch,\n ...(vorhanden ? { sha: p.body.sha } : {}),\n }),\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2660, + 300 + ], + "id": "afb04526-f6e8-4e1a-a509-dda65ca2ff73", + "name": "Commit vorbereiten" + }, + { + "parameters": { + "method": "={{ $json.methode }}", + "url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $json.pfad }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": false + } + }, + "timeout": 60000 + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.commit_body }}", + "sendHeaders": true, + "specifyHeaders": "json", + "jsonHeaders": "={\"Content-Type\":\"application/json\"}" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 2880, + 300 + ], + "id": "52b0dd0b-8fd7-4ea9-992a-6cd6441ef163", + "name": "Brief committen", + "credentials": { + "httpHeaderAuth": { + "id": "VOD4JM5zwpSC8wAr", + "name": "Gitea Skrift Auto Article" + } + }, + "onError": "continueErrorOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000 + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst b = $('Brief bauen').first().json;\nreturn [{ json: { slug: b.slug, keyword: b.keyword, query: b.query, ergebnis: 'fehler',\n stufe: 'gitea-commit',\n meldung: 'Brief konnte nicht ins Repo geschrieben werden. Haeufigste Ursache: Gitea-Token ohne '\n + 'Scope write:repository. Antwort: ' + JSON.stringify($input.first().json).slice(0, 500) }}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2880, + 520 + ], + "id": "206a3db5-ccca-463c-be77-93715a215352", + "name": "Fehler Brief committen", + "onError": "continueRegularOutput" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst b = $('Brief bauen').first().json;\nreturn [{ json: {\n slug: b.slug, keyword: b.keyword, query: b.query, zeilen_index: b.zeilen_index,\n ergebnis: 'ok', neuer_status: 'brief_bereit',\n pfad: $('Commit vorbereiten').first().json.pfad,\n wortzahl_ziel: b.brief.wortzahl_ziel,\n anzahl_pflichtbegriffe: b.brief.begriffe_pflicht.length,\n anzahl_fragen: b.brief.fragen.length,\n analyse_url: b.brief.meta.analyse_url,\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3100, + 300 + ], + "id": "153c2001-4e5c-4ab2-a018-58d139a41920", + "name": "Erfolg merken" + }, + { + "parameters": { + "amount": "={{ $('Konfiguration').first().json.pause_zwischen_artikeln }}", + "unit": "seconds" + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 3320, + 400 + ], + "id": "bd5fe14c-6097-418b-a91a-97d9679e9067", + "name": "Pause Rate Limit", + "webhookId": "4504a64d-5777-4cfe-bcc6-66a9f8b1c7c9" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst alle = $input.all().map(i => i.json);\nconst tabelle = $('Tabelle auswerten').first().json.tabelle_gesamt\n || $getWorkflowStaticData('global').lauf.tabelle_gesamt;\nconst index_sha = $('Artikeltabelle lesen').first().json.sha;\n\nconst kopie = JSON.parse(JSON.stringify(tabelle));\nlet geaendert = 0;\nalle.filter(r => r.ergebnis === 'ok').forEach(r => {\n const i = kopie.findIndex(z => z.slug === r.slug);\n if (i >= 0) { kopie[i].status = 'brief_bereit'; kopie[i].query_id = r.query; geaendert++; }\n});\n\nconst ok = alle.filter(r => r.ergebnis === 'ok');\nconst fehler = alle.filter(r => r.ergebnis === 'fehler');\n\nreturn [{ json: {\n index_sha, geaendert,\n neue_tabelle: JSON.stringify(kopie, null, 2),\n ok, fehler,\n anzahl_ok: ok.length, anzahl_fehler: fehler.length,\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 900, + 140 + ], + "id": "e9189aa1-03a8-4463-8682-64fc8f0e52e2", + "name": "Ergebnisse sammeln" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst s = $input.first().json;\nconst konf = $('Konfiguration').first().json;\nreturn [{ json: { ...s, commit_body: JSON.stringify({\n content: Buffer.from(s.neue_tabelle, 'utf8').toString('base64'),\n message: `Status auf brief_bereit gesetzt (${s.geaendert} Artikel)`,\n branch: konf.branch,\n sha: s.index_sha,\n})}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 140 + ], + "id": "52907f9e-bf97-48b1-bcf9-a796c7ff3438", + "name": "Statusupdate vorbereiten" + }, + { + "parameters": { + "method": "PUT", + "url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.index_pfad }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": false + } + }, + "timeout": 60000 + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.commit_body }}", + "sendHeaders": true, + "specifyHeaders": "json", + "jsonHeaders": "={\"Content-Type\":\"application/json\"}" + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1340, + 140 + ], + "id": "f08430c6-a045-44e3-86c2-5d071c49249f", + "name": "Status zurueckschreiben", + "credentials": { + "httpHeaderAuth": { + "id": "VOD4JM5zwpSC8wAr", + "name": "Gitea Skrift Auto Article" + } + }, + "onError": "continueErrorOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000 + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nthrow new Error('Status konnte nicht zurueckgeschrieben werden. Die Briefe sind erzeugt, aber die '\n + 'Artikeltabelle steht noch auf \"offen\". Vor dem naechsten Lauf pruefen, sonst werden Analysen '\n + 'doppelt erzeugt. Antwort: ' + JSON.stringify($input.first().json).slice(0, 500));\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1340, + -60 + ], + "id": "1235e563-1812-48c1-bf17-c661c3f06ff4", + "name": "Fehler Statusupdate" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst s = $('Ergebnisse sammeln').first().json;\nconst z = [];\nz.push(`Briefe vorbereitet: ${s.anzahl_ok}, Fehler: ${s.anzahl_fehler}`);\nz.push('');\nif (s.ok.length) {\n z.push('Fertig:');\n s.ok.forEach(r => z.push(\n ` ${r.slug} | Zielwortzahl ${r.wortzahl_ziel} | ${r.anzahl_pflichtbegriffe} Pflichtbegriffe, `\n + `${r.anzahl_fragen} Fragen\\n Datei: ${r.pfad}\\n Analyse: ${r.analyse_url}`));\n z.push('');\n}\nif (s.fehler.length) {\n z.push('Fehler:');\n s.fehler.forEach(r => z.push(` ${r.slug || '?'} [${r.stufe}]: ${r.meldung}`));\n z.push('');\n}\nz.push('Naechster Schritt: git pull, dann die Briefe in Cowork als Schreibvorlage nutzen.');\nreturn [{ json: {\n betreff: `Skrift Ratgeber: ${s.anzahl_ok} Brief(e) bereit` + (s.anzahl_fehler ? `, ${s.anzahl_fehler} Fehler` : ''),\n text: z.join('\\n'),\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1560, + 140 + ], + "id": "65282b64-53b8-4ffc-8f28-4822a9b7c7e4", + "name": "Meldung formulieren" + }, + { + "parameters": { + "fromEmail": "", + "toEmail": "={{ $('Konfiguration').first().json.benachrichtigung_an }}", + "subject": "={{ $json.betreff }}", + "emailFormat": "text", + "text": "={{ $json.text }}", + "options": {} + }, + "type": "n8n-nodes-base.emailSend", + "typeVersion": 2.1, + "position": [ + 1780, + 140 + ], + "id": "510b0e37-8db3-4b5a-8abd-a5fe4403fb9d", + "name": "Benachrichtigung", + "disabled": true, + "notes": "SMTP-Credential zuweisen, Absender eintragen, dann Node aktivieren." + } + ], + "connections": { + "Manuell starten": { + "main": [ + [ + { + "node": "Konfiguration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Zeitplan Montag 07:00": { + "main": [ + [ + { + "node": "Konfiguration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook Start": { + "main": [ + [ + { + "node": "Konfiguration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Konfiguration": { + "main": [ + [ + { + "node": "Artikeltabelle lesen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Artikeltabelle lesen": { + "main": [ + [ + { + "node": "Tabelle auswerten", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fehler Artikeltabelle", + "type": "main", + "index": 0 + } + ] + ] + }, + "Tabelle auswerten": { + "main": [ + [ + { + "node": "Offene Artikel vorhanden?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Offene Artikel vorhanden?": { + "main": [ + [ + { + "node": "Nichts zu tun", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Artikel nacheinander", + "type": "main", + "index": 0 + } + ] + ] + }, + "Artikel nacheinander": { + "main": [ + [ + { + "node": "Ergebnisse sammeln", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Analyse starten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyse starten": { + "main": [ + [ + { + "node": "Poll vorbereiten", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fehler Analyse starten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Poll vorbereiten": { + "main": [ + [ + { + "node": "Warten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Warten": { + "main": [ + [ + { + "node": "Analyse abfragen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyse abfragen": { + "main": [ + [ + { + "node": "Analyse fertig?", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fehler Analyse abfragen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyse fertig?": { + "main": [ + [ + { + "node": "Brief bauen", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Versuch zaehlen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Versuch zaehlen": { + "main": [ + [ + { + "node": "Poll-Limit erreicht?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Poll-Limit erreicht?": { + "main": [ + [ + { + "node": "Timeout melden", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Warten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Brief bauen": { + "main": [ + [ + { + "node": "Brief vorhanden?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Brief vorhanden?": { + "main": [ + [ + { + "node": "Commit vorbereiten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Commit vorbereiten": { + "main": [ + [ + { + "node": "Brief committen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Brief committen": { + "main": [ + [ + { + "node": "Erfolg merken", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fehler Brief committen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Erfolg merken": { + "main": [ + [ + { + "node": "Pause Rate Limit", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fehler Brief committen": { + "main": [ + [ + { + "node": "Pause Rate Limit", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fehler Analyse starten": { + "main": [ + [ + { + "node": "Pause Rate Limit", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fehler Analyse abfragen": { + "main": [ + [ + { + "node": "Pause Rate Limit", + "type": "main", + "index": 0 + } + ] + ] + }, + "Timeout melden": { + "main": [ + [ + { + "node": "Pause Rate Limit", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pause Rate Limit": { + "main": [ + [ + { + "node": "Artikel nacheinander", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ergebnisse sammeln": { + "main": [ + [ + { + "node": "Statusupdate vorbereiten", + "type": "main", + "index": 0 + } + ] + ] + }, + "Statusupdate vorbereiten": { + "main": [ + [ + { + "node": "Status zurueckschreiben", + "type": "main", + "index": 0 + } + ] + ] + }, + "Status zurueckschreiben": { + "main": [ + [ + { + "node": "Meldung formulieren", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Fehler Statusupdate", + "type": "main", + "index": 0 + } + ] + ] + }, + "Meldung formulieren": { + "main": [ + [ + { + "node": "Benachrichtigung", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveManualExecutions": true, + "callerPolicy": "workflowsFromSameOwner" + } +} \ No newline at end of file