{ "name": "brief-vorbereiten", "nodes": [ { "parameters": {}, "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ -460, 40 ], "id": "e753db60-1139-4873-bc3f-33ce61502a0c", "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": "99319fb1-8e86-4e82-b192-3cb1dfc387cd", "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": "657d9657-3a90-4a83-b9db-adcc26ca3276", "name": "Webhook Start", "webhookId": "84a8da24-b2b3-43b6-b0ae-f61e0b09b798" }, { "parameters": { "mode": "manual", "assignments": { "assignments": [ { "id": "4cd0bc00-7d77-4297-92f2-2615b8e3ee30", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { "id": "7652c6df-df16-48d9-bc02-970407a0056c", "name": "nw_project", "value": "3da4d745e0e223cb", "type": "string" }, { "id": "2df2405a-8a94-412d-99f0-c03734764ff9", "name": "nw_engine", "value": "google.de", "type": "string" }, { "id": "29a4fac5-c86c-452a-9534-388fee8f0a00", "name": "nw_language", "value": "German", "type": "string" }, { "id": "d62d3a56-9f07-49f0-9dee-4016c0d0c59c", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { "id": "c8918f84-6424-48bb-8153-daec31e5d3fc", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { "id": "f316e4e7-757b-4163-b061-339796429248", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { "id": "74912d70-7d7c-41e2-9ac2-84865e460cf6", "name": "branch", "value": "main", "type": "string" }, { "id": "66c2f30e-0458-4251-974b-a8695c7e8b04", "name": "index_pfad", "value": "artikel-index.json", "type": "string" }, { "id": "1ce974ed-b971-497d-a246-eec21bfd6d11", "name": "brief_ordner", "value": "briefe", "type": "string" }, { "id": "e1dafef4-0eb6-45c0-acf2-8c7b543b7a02", "name": "poll_max", "value": 30, "type": "number" }, { "id": "d010e1c9-54a7-4ef9-b0db-517fe2d74b9b", "name": "poll_sekunden", "value": 20, "type": "number" }, { "id": "8525830a-5432-455c-98a2-25c158c685a6", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { "id": "8353a3fc-a8d3-421a-8ae7-f400ab6e0fba", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ -240, 200 ], "id": "8aa9183c-7dfe-4bbb-8000-40c04b4edd53", "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": "00c9281c-69fe-4f0e-87fc-c5588d4c5349", "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": "df2817d6-08d6-4c2f-9760-beabec0b1b6f", "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": "b8e741f7-f470-4047-8e46-a08641aa2ba6", "name": "Tabelle auswerten" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "37d71caa-2ff5-4431-b4cb-6a0cbfbdf78f", "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": "f2611812-b7b0-4b3d-aef1-797baffaa2de", "name": "Offene Artikel vorhanden?" }, { "parameters": {}, "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [ 640, 60 ], "id": "cfaa62aa-bb54-4b44-9739-436b1cffad88", "name": "Nichts zu tun" }, { "parameters": { "batchSize": 1, "options": { "reset": false } }, "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ 640, 320 ], "id": "fd897525-a406-457a-ae4c-778194fd1f0a", "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": "eba9ec08-14d7-4c79-9383-64ed222e135a", "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": "3668b98c-65db-405c-9d21-537ad4df4d3f", "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": "d2430eff-230f-4be5-ba0d-e4d1a4ab4ba9", "name": "Poll vorbereiten" }, { "parameters": { "amount": "={{ $('Konfiguration').first().json.poll_sekunden }}", "unit": "seconds" }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 1340, 420 ], "id": "becfbd1d-291f-4294-b1dd-a0a224b22de8", "name": "Warten", "webhookId": "83211d11-5e8d-4c8e-91ec-b5112d663862" }, { "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": "4d66a76e-5b27-4880-9b95-6ac38203e371", "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": "689dd568-3c1d-4af4-b5d3-50e537fdf119", "name": "Fehler Analyse abfragen", "onError": "continueRegularOutput" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c6958335-389e-4fde-8d82-9da8a837fea6", "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": "beb96c7d-17a0-459a-9083-b3ffb1da2b35", "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": "7d393c99-27f8-4a7c-9066-d2ccf81d5cbf", "name": "Versuch zaehlen" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "8ee3ea55-c784-4a43-8042-15869c607ad8", "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": "431e705f-2b52-4525-ba34-fb47b8011498", "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": "f6e65544-0e98-44e2-b698-d7d688829d61", "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 praxis: Array.isArray(a.praxis) ? a.praxis : [] },\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 schreibregeln: {\n preise: 'Preise niemals ausschreiben. Immer den Shortcode einsetzen: '\n + '[skrift_preis key=\"SCHLUESSEL\" ab=\"0|1\" brutto=\"0|1\"]. '\n + 'key ist der Produktschluessel (z. B. briefe, postkarten, porto_inland). '\n + 'ab=1 liefert den guenstigsten Staffelpreis, ab=0 den Standardpreis. '\n + 'brutto=1 inkl. MwSt., brutto=0 netto. Das Wort \"ab\" selbst gehoert in den Fliesstext, '\n + 'der Shortcode liefert nur die Zahl. Beispiel: '\n + 'Handschriftliche Briefe ab [skrift_preis key=\"briefe\" ab=\"1\"] pro Stueck. '\n + 'Ein unbekannter key ergibt eine leere Ausgabe, keinen Fehler.',\n faq: 'FAQ nicht in content_html schreiben, sondern als Feld faq mit Frage-Antwort-Paaren. '\n + 'Die Ausgabe im Beitrag uebernimmt der Shortcode [skrift_faq].',\n kurzantwort: 'Ein Absatz, der die Hauptfrage direkt beantwortet, als eigenes Feld kurzantwort.',\n praxis: 'Der Artikel baut auf den Stichpunkten in artikel.praxis auf. Jeder Stichpunkt kommt '\n + 'erkennbar im Text vor, als Beispiel, Erfahrung oder klare Empfehlung. Ist die Liste leer, '\n + 'wird der Artikel nicht geschrieben, sondern zuerst Praxiswissen bei Lucas erfragt.',\n begriffe: 'begriffe_pflicht sind verbindlich. begriffe_erweitert nur verwenden, wenn der Satz '\n + 'dadurch nicht schlechter wird. Keine Saetze, die nur fuer den NeuronWriter-Score da sind. '\n + 'Keine Wettbewerbernamen. Zielwert im Import ist 65.',\n links: 'Interne Links primaer auf Business-Leistungsseiten: handgeschriebene-businessbriefe, '\n + 'handgeschriebene-postkarten, unterschriftenservice, automatisierte-follow-ups. '\n + 'artikel.cta_ziel mindestens einmal im Fliesstext verlinken, mindestens zwei Business-Links insgesamt. '\n + 'papier-schriftmuster nur ergaenzend. Keine Links auf Privatkunden-Seiten, review, agb, datenschutz, '\n + 'impressum, zahlungsmethoden, lieferung-versand.',\n bausteine: 'Kein Block \"Auch fuer Privatpersonen.\" und kein Abschnitt \"Kostenloses Handschriftmuster '\n + 'anfordern.\" im Artikeltext, beides liefert das Seitenlayout. Der Artikel endet mit dem letzten '\n + 'inhaltlichen Abschnitt, nie mit einer Zusammenfassung.',\n faq_ueberschrift: 'Eigene FAQ-Ueberschrift als Feld faq_ueberschrift, konkret zum Thema, z. B. '\n + '\"Fragen zu den Kosten\". Satzfall, ohne Punkt am Ende.',\n stil: 'Kurze und lange Saetze mischen. Absaetze unterschiedlich lang. Keine Formeln wie '\n + '\"Genau das\", \"Der Grund ist\", \"nicht nur, sondern auch\", \"entscheidend ist\", '\n + '\"der groesste Hebel\", \"macht den Unterschied\". Keine Aufzaehlung mit erstens, zweitens. '\n + 'Keine rhetorische Frage mit Sofortantwort. Keine Behauptung ohne Beleg, also kein '\n + '\"erfahrungsgemaess\" oder \"in aller Regel\". Wo es passt, auch abraten: wann sich '\n + 'Handschrift nicht lohnt.',\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": "6081e30a-4127-4af7-8af7-2b2446711a71", "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": "4cefe611-3c98-4c4f-bf72-db5b8a05a5bd", "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;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kp = K(p);\nconst vorhanden = p.statusCode === 200 && kp && kp.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: kp.sha } : {}),\n }),\n}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2660, 300 ], "id": "ed3e6ae2-94a0-462c-98a2-e00c5a1c77ab", "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": "2331dd6c-8595-482e-abd4-e7ec9f155dd2", "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": "19ea007b-0521-423a-bbf0-c1318f1b1cd0", "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": "c9e0a373-8df8-4b4a-9dea-7b204d84b97a", "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": "22e97356-a573-465a-8847-7c4967923fa0", "name": "Pause Rate Limit", "webhookId": "10b98341-b2a3-4bee-9158-670abd71eefe" }, { "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": "abf1aba9-cac5-4556-8230-798373a16c30", "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": "a378732e-63fb-4137-aad3-e15a80768b1b", "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": "4160f022-aba5-4057-a179-8e632da542e2", "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": "c77421d1-349f-4cb6-8b8a-a1ead23bfb47", "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": "09faecc1-3eb8-4d31-a616-8286c1ef5b46", "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": "9e6b8af6-6231-4838-9a04-ba8bde15decc", "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" } }