Workflows, Mu-Plugin, Feldnachweis und Doku
This commit is contained in:
468
build/gen_wf1.py
Normal file
468
build/gen_wf1.py
Normal file
@@ -0,0 +1,468 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Erzeugt workflows/brief-vorbereiten.json fuer n8n 1.120.x"""
|
||||
import json, os, uuid
|
||||
|
||||
CRED_NW = {"httpHeaderAuth": {"id": "U23n2O980GKWocWq", "name": "NeuronWriter API"}}
|
||||
CRED_GITEA = {"httpHeaderAuth": {"id": "VOD4JM5zwpSC8wAr", "name": "Gitea Skrift Auto Article"}}
|
||||
|
||||
nodes, conns = [], {}
|
||||
def nid(): return str(uuid.uuid4())
|
||||
|
||||
def node(name, typ, tv, pos, params, **kw):
|
||||
n = {"parameters": params, "type": typ, "typeVersion": tv,
|
||||
"position": pos, "id": nid(), "name": name}
|
||||
n.update(kw)
|
||||
nodes.append(n)
|
||||
return name
|
||||
|
||||
def link(a, b, out=0, inp=0):
|
||||
conns.setdefault(a, {"main": []})
|
||||
while len(conns[a]["main"]) <= out:
|
||||
conns[a]["main"].append([])
|
||||
conns[a]["main"][out].append({"node": b, "type": "main", "index": inp})
|
||||
|
||||
def http(name, pos, method, url, cred, body=None, qs=None, error_out=True, retry=True, extra=None):
|
||||
p = {
|
||||
"method": method, "url": url,
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"options": {"response": {"response": {"neverError": False}}, "timeout": 60000},
|
||||
}
|
||||
if qs:
|
||||
p["sendQuery"] = True
|
||||
p["specifyQuery"] = "json"
|
||||
p["jsonQuery"] = qs
|
||||
if body is not None:
|
||||
p["sendBody"] = True
|
||||
p["specifyBody"] = "json"
|
||||
p["jsonBody"] = body
|
||||
p["sendHeaders"] = True
|
||||
p["specifyHeaders"] = "json"
|
||||
p["jsonHeaders"] = '={"Content-Type":"application/json"}'
|
||||
if extra: p.update(extra)
|
||||
kw = {"credentials": cred}
|
||||
if error_out: kw["onError"] = "continueErrorOutput"
|
||||
if retry:
|
||||
kw["retryOnFail"] = True
|
||||
kw["maxTries"] = 3
|
||||
kw["waitBetweenTries"] = 3000
|
||||
return node(name, "n8n-nodes-base.httpRequest", 4.3, pos, p, **kw)
|
||||
|
||||
def code(name, pos, js, mode="runOnceForAllItems", **kw):
|
||||
return node(name, "n8n-nodes-base.code", 2, pos, {"mode": mode, "jsCode": js}, **kw)
|
||||
|
||||
def iff(name, pos, left, op_type, op, right=None, **kw):
|
||||
c = {"id": nid(), "leftValue": left, "rightValue": right if right is not None else "",
|
||||
"operator": {"type": op_type, "operation": op}}
|
||||
if right is None: c["operator"]["singleValue"] = True
|
||||
return node(name, "n8n-nodes-base.if", 2.2, pos, {
|
||||
"conditions": {"options": {"caseSensitive": True, "leftValue": "",
|
||||
"typeValidation": "loose", "version": 2},
|
||||
"conditions": [c], "combinator": "and"},
|
||||
"looseTypeValidation": True, "options": {}}, **kw)
|
||||
|
||||
# ---------------------------------------------------------------- Trigger
|
||||
node("Manuell starten", "n8n-nodes-base.manualTrigger", 1, [-460, 40], {})
|
||||
node("Zeitplan Montag 07:00", "n8n-nodes-base.scheduleTrigger", 1.2, [-460, 200],
|
||||
{"rule": {"interval": [{"field": "weeks", "triggerAtDay": [1],
|
||||
"triggerAtHour": 7, "triggerAtMinute": 0}]}},
|
||||
disabled=True)
|
||||
node("Webhook Start", "n8n-nodes-base.webhook", 2.1, [-460, 360],
|
||||
{"httpMethod": "POST", "path": "brief-vorbereiten",
|
||||
"responseMode": "onReceived", "options": {}},
|
||||
webhookId=str(uuid.uuid4()))
|
||||
|
||||
# ---------------------------------------------------------------- Konfiguration
|
||||
KONF = node("Konfiguration", "n8n-nodes-base.set", 3.4, [-240, 200], {
|
||||
"mode": "manual",
|
||||
"assignments": {"assignments": [
|
||||
{"id": nid(), "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string"},
|
||||
{"id": nid(), "name": "nw_project", "value": "3da4d745e0e223cb", "type": "string"},
|
||||
{"id": nid(), "name": "nw_engine", "value": "google.de", "type": "string"},
|
||||
{"id": nid(), "name": "nw_language", "value": "German", "type": "string"},
|
||||
{"id": nid(), "name": "gitea_base", "value": "https://gitea.lucas-orth.de", "type": "string"},
|
||||
{"id": nid(), "name": "gitea_owner", "value": "lucas.orth", "type": "string"},
|
||||
{"id": nid(), "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string"},
|
||||
{"id": nid(), "name": "branch", "value": "main", "type": "string"},
|
||||
{"id": nid(), "name": "index_pfad", "value": "artikel-index.json", "type": "string"},
|
||||
{"id": nid(), "name": "brief_ordner","value": "briefe", "type": "string"},
|
||||
{"id": nid(), "name": "poll_max", "value": 30, "type": "number"},
|
||||
{"id": nid(), "name": "poll_sekunden","value": 20, "type": "number"},
|
||||
{"id": nid(), "name": "pause_zwischen_artikeln", "value": 5, "type": "number"},
|
||||
{"id": nid(), "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string"},
|
||||
]}, "options": {}})
|
||||
for t in ["Manuell starten", "Zeitplan Montag 07:00", "Webhook Start"]:
|
||||
link(t, KONF)
|
||||
|
||||
# ---------------------------------------------------------------- Tabelle lesen
|
||||
LESEN = http("Artikeltabelle lesen", [-20, 200], "GET",
|
||||
"={{ $json.gitea_base }}/api/v1/repos/{{ $json.gitea_owner }}/{{ $json.gitea_repo }}/contents/{{ $json.index_pfad }}?ref={{ $json.branch }}",
|
||||
CRED_GITEA)
|
||||
link(KONF, LESEN)
|
||||
|
||||
FEHLER_LESEN = code("Fehler Artikeltabelle", [-20, 420], """
|
||||
const e = $input.first().json;
|
||||
throw new Error(
|
||||
'Artikeltabelle konnte nicht gelesen werden. Pruefen: Gitea-Token-Scope (write:repository noetig), '
|
||||
+ 'Pfad artikel-index.json im Branch main, Repo-Name. Antwort: '
|
||||
+ JSON.stringify(e).slice(0, 600)
|
||||
);
|
||||
""")
|
||||
link(LESEN, FEHLER_LESEN, out=1)
|
||||
|
||||
AUSWERTEN = code("Tabelle auswerten", [200, 200], """
|
||||
// Gitea liefert den Dateiinhalt base64-kodiert.
|
||||
const antwort = $input.first().json;
|
||||
if (!antwort.content) {
|
||||
throw new Error('Gitea-Antwort enthaelt kein Feld "content". Erhalten: ' + JSON.stringify(antwort).slice(0,400));
|
||||
}
|
||||
const roh = Buffer.from(antwort.content, 'base64').toString('utf8');
|
||||
let tabelle;
|
||||
try { tabelle = JSON.parse(roh); }
|
||||
catch (e) { throw new Error('artikel-index.json ist kein gueltiges JSON: ' + e.message); }
|
||||
if (!Array.isArray(tabelle)) {
|
||||
throw new Error('artikel-index.json muss ein Array sein, ist aber: ' + typeof tabelle);
|
||||
}
|
||||
|
||||
const konf = $('Konfiguration').first().json;
|
||||
const pflicht = ['slug','titel','keyword','kategorie','cta_ziel','status'];
|
||||
|
||||
const offen = [];
|
||||
tabelle.forEach((zeile, i) => {
|
||||
if (zeile.status !== 'offen') return;
|
||||
const fehlt = pflicht.filter(f => !zeile[f]);
|
||||
if (fehlt.length) {
|
||||
throw new Error(`Zeile ${i} (slug: ${zeile.slug || '?'}) fehlen Pflichtfelder: ${fehlt.join(', ')}`);
|
||||
}
|
||||
offen.push({ json: { ...zeile, zeilen_index: i } });
|
||||
});
|
||||
|
||||
// Kontext fuer spaetere Nodes einmal ablegen
|
||||
const kontext = { index_sha: antwort.sha, tabelle_gesamt: tabelle, anzahl_offen: offen.length };
|
||||
$getWorkflowStaticData('global').lauf = kontext;
|
||||
|
||||
if (offen.length === 0) return [{ json: { ...kontext, leer: true } }];
|
||||
return offen.map(o => ({ json: { ...o.json, index_sha: antwort.sha } }));
|
||||
""")
|
||||
link(LESEN, AUSWERTEN, out=0)
|
||||
|
||||
GIBTS = iff("Offene Artikel vorhanden?", [420, 200], "={{ $json.leer === true }}", "boolean", "true")
|
||||
link(AUSWERTEN, GIBTS)
|
||||
|
||||
NICHTS = node("Nichts zu tun", "n8n-nodes-base.noOp", 1, [640, 60], {})
|
||||
link(GIBTS, NICHTS, out=0)
|
||||
|
||||
# ---------------------------------------------------------------- Loop
|
||||
LOOP = node("Artikel nacheinander", "n8n-nodes-base.splitInBatches", 3, [640, 320],
|
||||
{"batchSize": 1, "options": {"reset": False}})
|
||||
link(GIBTS, LOOP, out=1)
|
||||
|
||||
# ---------------------------------------------------------------- Analyse starten
|
||||
NEU = http("Analyse starten", [900, 420], "POST",
|
||||
"={{ $('Konfiguration').first().json.nw_base }}/new-query", CRED_NW,
|
||||
body="={{ JSON.stringify({ project: $('Konfiguration').first().json.nw_project, keyword: $json.keyword, engine: $('Konfiguration').first().json.nw_engine, language: $('Konfiguration').first().json.nw_language }) }}")
|
||||
link(LOOP, NEU, out=1)
|
||||
|
||||
FEHLER_NEU = code("Fehler Analyse starten", [900, 640], """
|
||||
const artikel = $('Artikel nacheinander').first().json;
|
||||
const e = $input.first().json;
|
||||
return [{ json: {
|
||||
slug: artikel.slug, keyword: artikel.keyword, ergebnis: 'fehler',
|
||||
stufe: 'new-query',
|
||||
meldung: 'NeuronWriter /new-query fehlgeschlagen fuer Keyword "' + artikel.keyword
|
||||
+ '". Haeufige Ursachen: Query-Kontingent erschoepft, falsche Projekt-ID, API-Key ungueltig. Antwort: '
|
||||
+ JSON.stringify(e).slice(0, 500)
|
||||
}}];
|
||||
""", onError="continueRegularOutput")
|
||||
link(NEU, FEHLER_NEU, out=1)
|
||||
|
||||
POLLSTART = code("Poll vorbereiten", [1120, 420], """
|
||||
const artikel = $('Artikel nacheinander').first().json;
|
||||
const neu = $input.first().json;
|
||||
if (!neu.query) {
|
||||
throw new Error('NeuronWriter lieferte kein Feld "query". Antwort: ' + JSON.stringify(neu).slice(0,400));
|
||||
}
|
||||
return [{ json: { ...artikel, query: neu.query, query_url: neu.query_url,
|
||||
share_url: neu.share_url, versuch: 0 } }];
|
||||
""")
|
||||
link(NEU, POLLSTART, out=0)
|
||||
|
||||
WARTEN = node("Warten", "n8n-nodes-base.wait", 1.1, [1340, 420],
|
||||
{"amount": "={{ $('Konfiguration').first().json.poll_sekunden }}", "unit": "seconds"},
|
||||
webhookId=str(uuid.uuid4()))
|
||||
link(POLLSTART, WARTEN)
|
||||
|
||||
ABFRAGE = http("Analyse abfragen", [1560, 420], "POST",
|
||||
"={{ $('Konfiguration').first().json.nw_base }}/get-query", CRED_NW,
|
||||
body="={{ JSON.stringify({ query: $json.query }) }}")
|
||||
link(WARTEN, ABFRAGE)
|
||||
|
||||
FEHLER_ABFRAGE = code("Fehler Analyse abfragen", [1560, 640], """
|
||||
const a = $('Poll vorbereiten').first().json;
|
||||
return [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',
|
||||
stufe: 'get-query',
|
||||
meldung: 'NeuronWriter /get-query fehlgeschlagen fuer query ' + a.query + '. Antwort: '
|
||||
+ JSON.stringify($input.first().json).slice(0, 500) }}];
|
||||
""", onError="continueRegularOutput")
|
||||
link(ABFRAGE, FEHLER_ABFRAGE, out=1)
|
||||
|
||||
FERTIG = iff("Analyse fertig?", [1780, 420],
|
||||
"={{ $json.status }}", "string", "equals", "ready")
|
||||
link(ABFRAGE, FERTIG, out=0)
|
||||
|
||||
ZAEHLEN = code("Versuch zaehlen", [1780, 660], """
|
||||
const poll = $('Poll vorbereiten').first().json;
|
||||
const antwort = $input.first().json;
|
||||
const versuch = (poll.versuch || 0) + $runIndex + 1;
|
||||
return [{ json: { ...poll, versuch, letzter_status: antwort.status || 'unbekannt' } }];
|
||||
""")
|
||||
link(FERTIG, ZAEHLEN, out=1)
|
||||
|
||||
LIMIT = iff("Poll-Limit erreicht?", [2000, 660],
|
||||
"={{ $json.versuch }}", "number", "gte",
|
||||
"={{ $('Konfiguration').first().json.poll_max }}")
|
||||
link(ZAEHLEN, LIMIT)
|
||||
|
||||
TIMEOUT = code("Timeout melden", [2220, 780], """
|
||||
const a = $input.first().json;
|
||||
return [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',
|
||||
stufe: 'poll-timeout',
|
||||
meldung: 'NeuronWriter-Analyse fuer "' + a.keyword + '" war nach ' + a.versuch
|
||||
+ ' Versuchen noch nicht fertig (letzter Status: ' + a.letzter_status
|
||||
+ '). Analyse laeuft ggf. weiter, query merken: ' + a.query }}];
|
||||
""")
|
||||
link(LIMIT, TIMEOUT, out=0)
|
||||
link(LIMIT, WARTEN, out=1) # noch Versuche uebrig -> weiter pollen
|
||||
|
||||
json.dump({"nodes": nodes, "connections": conns}, open('/tmp/wf1_teil1.json','w'), indent=1)
|
||||
print("Teil 1 fertig:", len(nodes), "Nodes")
|
||||
|
||||
# ---------------------------------------------------------------- Brief bauen
|
||||
BRIEF = code("Brief bauen", [2220, 300], r"""
|
||||
// Feldnamen stammen aus einer echten /get-query-Antwort, siehe docs/feldnachweis.md
|
||||
const a = $('Poll vorbereiten').first().json;
|
||||
const n = $input.first().json;
|
||||
|
||||
const liste = (arr) => (arr || []).map(x => ({
|
||||
begriff: x.t,
|
||||
min: Array.isArray(x.sugg_usage) ? x.sugg_usage[0] : null,
|
||||
max: Array.isArray(x.sugg_usage) ? x.sugg_usage[1] : null,
|
||||
bei_wettbewerbern_pc: x.usage_pc ?? null,
|
||||
}));
|
||||
|
||||
const fragen = [];
|
||||
const gesehen = new Set();
|
||||
const addFrage = (q, quelle, gewicht) => {
|
||||
if (!q) return;
|
||||
const k = q.trim().toLowerCase();
|
||||
if (gesehen.has(k)) return;
|
||||
gesehen.add(k);
|
||||
fragen.push({ frage: q.trim(), quelle, gewicht });
|
||||
};
|
||||
Object.entries((n.ideas && n.ideas.topic_matrix) || {})
|
||||
.sort((x, y) => (y[1].importance || 0) - (x[1].importance || 0))
|
||||
.forEach(([q, v]) => addFrage(q, 'topic_matrix', v.importance || null));
|
||||
((n.ideas || {}).people_also_ask || []).forEach(x => addFrage(x.q, 'people_also_ask', null));
|
||||
((n.ideas || {}).suggest_questions || []).forEach(x => addFrage(x.q, 'suggest_questions', null));
|
||||
((n.ideas || {}).content_questions || []).forEach(x => addFrage(x.q, 'content_questions', null));
|
||||
|
||||
const brief = {
|
||||
// --- die vom Auftrag geforderten Felder ---
|
||||
query_id: n.query,
|
||||
keyword: n.keyword,
|
||||
begriffe_pflicht: liste((n.terms || {}).content_basic),
|
||||
begriffe_erweitert: liste((n.terms || {}).content_extended),
|
||||
fragen,
|
||||
wortzahl_ziel: ((n.metrics || {}).word_count || {}).target ?? null,
|
||||
|
||||
// --- Artikelkontext aus der Tabelle ---
|
||||
artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel },
|
||||
|
||||
// --- Zusatz fuer die Schreibvorlage ---
|
||||
zusatz: {
|
||||
wortzahl_median: ((n.metrics || {}).word_count || {}).median ?? null,
|
||||
lesbarkeit_ziel: ((n.metrics || {}).readability || {}).target ?? null,
|
||||
begriffe_titel: ((n.terms || {}).title || []).map(x => x.t),
|
||||
begriffe_description: ((n.terms || {}).desc || []).map(x => x.t),
|
||||
begriffe_h1: ((n.terms || {}).h1 || []).map(x => x.t),
|
||||
begriffe_h2: ((n.terms || {}).h2 || []).map(x => x.t),
|
||||
entitaeten: ((n.terms || {}).entities || []).slice(0, 15).map(x => ({ name: x.t, wichtigkeit: x.importance })),
|
||||
suchintention: (n.serp_summary || {}).top_intent ?? null,
|
||||
haeufigster_inhaltstyp: (n.serp_summary || {}).top_content_type ?? null,
|
||||
gliederungen_top5: (n.competitors || []).slice(0, 5).map(c => ({
|
||||
rang: c.rank, url: c.url, score: c.content_score, wortzahl: c.word_count,
|
||||
ueberschriften: (c.headers || []).map(h => `${h[0]}: ${h[1]}`).slice(0, 25),
|
||||
})),
|
||||
terms_txt_content_basic: ((n.terms_txt || {}).content_basic_w_ranges) ?? null,
|
||||
},
|
||||
|
||||
meta: {
|
||||
erzeugt_am: new Date().toISOString(),
|
||||
analyse_url: n.query_url || a.query_url || null,
|
||||
freigabe_url: n.share_url || a.share_url || null,
|
||||
sprache: n.language, suchmaschine: n.engine, projekt: n.project,
|
||||
},
|
||||
};
|
||||
|
||||
return [{ json: { slug: a.slug, zeilen_index: a.zeilen_index, query: n.query,
|
||||
keyword: n.keyword, brief } }];
|
||||
""")
|
||||
link(FERTIG, BRIEF, out=0)
|
||||
|
||||
PRUEF = http("Brief vorhanden?", [2440, 300], "GET",
|
||||
"={{ $('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 }}",
|
||||
CRED_GITEA, error_out=False, retry=False,
|
||||
extra={"options": {"response": {"response": {"neverError": True, "fullResponse": True}}, "timeout": 60000}})
|
||||
link(BRIEF, PRUEF)
|
||||
|
||||
COMMIT_VOR = code("Commit vorbereiten", [2660, 300], """
|
||||
const b = $('Brief bauen').first().json;
|
||||
const p = $input.first().json;
|
||||
const vorhanden = p.statusCode === 200 && p.body && p.body.sha;
|
||||
|
||||
const inhalt = Buffer.from(JSON.stringify(b.brief, null, 2), 'utf8').toString('base64');
|
||||
const konf = $('Konfiguration').first().json;
|
||||
|
||||
return [{ json: {
|
||||
...b,
|
||||
methode: vorhanden ? 'PUT' : 'POST',
|
||||
pfad: `${konf.brief_ordner}/brief-${b.slug}.json`,
|
||||
commit_body: JSON.stringify({
|
||||
content: inhalt,
|
||||
message: `Brief ${vorhanden ? 'aktualisiert' : 'erzeugt'}: ${b.slug} (${b.keyword})`,
|
||||
branch: konf.branch,
|
||||
...(vorhanden ? { sha: p.body.sha } : {}),
|
||||
}),
|
||||
}}];
|
||||
""")
|
||||
link(PRUEF, COMMIT_VOR)
|
||||
|
||||
COMMIT = http("Brief committen", [2880, 300], "={{ $json.methode }}",
|
||||
"={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $json.pfad }}",
|
||||
CRED_GITEA, body="={{ $json.commit_body }}")
|
||||
link(COMMIT_VOR, COMMIT)
|
||||
|
||||
FEHLER_COMMIT = code("Fehler Brief committen", [2880, 520], """
|
||||
const b = $('Brief bauen').first().json;
|
||||
return [{ json: { slug: b.slug, keyword: b.keyword, query: b.query, ergebnis: 'fehler',
|
||||
stufe: 'gitea-commit',
|
||||
meldung: 'Brief konnte nicht ins Repo geschrieben werden. Haeufigste Ursache: Gitea-Token ohne '
|
||||
+ 'Scope write:repository. Antwort: ' + JSON.stringify($input.first().json).slice(0, 500) }}];
|
||||
""", onError="continueRegularOutput")
|
||||
link(COMMIT, FEHLER_COMMIT, out=1)
|
||||
|
||||
ERFOLG = code("Erfolg merken", [3100, 300], """
|
||||
const b = $('Brief bauen').first().json;
|
||||
return [{ json: {
|
||||
slug: b.slug, keyword: b.keyword, query: b.query, zeilen_index: b.zeilen_index,
|
||||
ergebnis: 'ok', neuer_status: 'brief_bereit',
|
||||
pfad: $('Commit vorbereiten').first().json.pfad,
|
||||
wortzahl_ziel: b.brief.wortzahl_ziel,
|
||||
anzahl_pflichtbegriffe: b.brief.begriffe_pflicht.length,
|
||||
anzahl_fragen: b.brief.fragen.length,
|
||||
analyse_url: b.brief.meta.analyse_url,
|
||||
}}];
|
||||
""")
|
||||
link(COMMIT, ERFOLG, out=0)
|
||||
|
||||
PAUSE = node("Pause Rate Limit", "n8n-nodes-base.wait", 1.1, [3320, 400],
|
||||
{"amount": "={{ $('Konfiguration').first().json.pause_zwischen_artikeln }}", "unit": "seconds"},
|
||||
webhookId=str(uuid.uuid4()))
|
||||
for q in [ERFOLG, FEHLER_COMMIT, FEHLER_NEU, FEHLER_ABFRAGE, TIMEOUT]:
|
||||
link(q, PAUSE)
|
||||
link(PAUSE, LOOP)
|
||||
|
||||
# ---------------------------------------------------------------- Nach dem Loop
|
||||
SAMMELN = code("Ergebnisse sammeln", [900, 140], """
|
||||
const alle = $input.all().map(i => i.json);
|
||||
const tabelle = $('Tabelle auswerten').first().json.tabelle_gesamt
|
||||
|| $getWorkflowStaticData('global').lauf.tabelle_gesamt;
|
||||
const index_sha = $('Artikeltabelle lesen').first().json.sha;
|
||||
|
||||
const kopie = JSON.parse(JSON.stringify(tabelle));
|
||||
let geaendert = 0;
|
||||
alle.filter(r => r.ergebnis === 'ok').forEach(r => {
|
||||
const i = kopie.findIndex(z => z.slug === r.slug);
|
||||
if (i >= 0) { kopie[i].status = 'brief_bereit'; kopie[i].query_id = r.query; geaendert++; }
|
||||
});
|
||||
|
||||
const ok = alle.filter(r => r.ergebnis === 'ok');
|
||||
const fehler = alle.filter(r => r.ergebnis === 'fehler');
|
||||
|
||||
return [{ json: {
|
||||
index_sha, geaendert,
|
||||
neue_tabelle: JSON.stringify(kopie, null, 2),
|
||||
ok, fehler,
|
||||
anzahl_ok: ok.length, anzahl_fehler: fehler.length,
|
||||
}}];
|
||||
""")
|
||||
link(LOOP, SAMMELN, out=0)
|
||||
|
||||
STATUS_VOR = code("Statusupdate vorbereiten", [1120, 140], """
|
||||
const s = $input.first().json;
|
||||
const konf = $('Konfiguration').first().json;
|
||||
return [{ json: { ...s, commit_body: JSON.stringify({
|
||||
content: Buffer.from(s.neue_tabelle, 'utf8').toString('base64'),
|
||||
message: `Status auf brief_bereit gesetzt (${s.geaendert} Artikel)`,
|
||||
branch: konf.branch,
|
||||
sha: s.index_sha,
|
||||
})}}];
|
||||
""")
|
||||
link(SAMMELN, STATUS_VOR)
|
||||
|
||||
STATUS = http("Status zurueckschreiben", [1340, 140], "PUT",
|
||||
"={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.index_pfad }}",
|
||||
CRED_GITEA, body="={{ $json.commit_body }}")
|
||||
link(STATUS_VOR, STATUS)
|
||||
|
||||
FEHLER_STATUS = code("Fehler Statusupdate", [1340, -60], """
|
||||
throw new Error('Status konnte nicht zurueckgeschrieben werden. Die Briefe sind erzeugt, aber die '
|
||||
+ 'Artikeltabelle steht noch auf "offen". Vor dem naechsten Lauf pruefen, sonst werden Analysen '
|
||||
+ 'doppelt erzeugt. Antwort: ' + JSON.stringify($input.first().json).slice(0, 500));
|
||||
""")
|
||||
link(STATUS, FEHLER_STATUS, out=1)
|
||||
|
||||
MAIL_TEXT = code("Meldung formulieren", [1560, 140], r"""
|
||||
const s = $('Ergebnisse sammeln').first().json;
|
||||
const z = [];
|
||||
z.push(`Briefe vorbereitet: ${s.anzahl_ok}, Fehler: ${s.anzahl_fehler}`);
|
||||
z.push('');
|
||||
if (s.ok.length) {
|
||||
z.push('Fertig:');
|
||||
s.ok.forEach(r => z.push(
|
||||
` ${r.slug} | Zielwortzahl ${r.wortzahl_ziel} | ${r.anzahl_pflichtbegriffe} Pflichtbegriffe, `
|
||||
+ `${r.anzahl_fragen} Fragen\n Datei: ${r.pfad}\n Analyse: ${r.analyse_url}`));
|
||||
z.push('');
|
||||
}
|
||||
if (s.fehler.length) {
|
||||
z.push('Fehler:');
|
||||
s.fehler.forEach(r => z.push(` ${r.slug || '?'} [${r.stufe}]: ${r.meldung}`));
|
||||
z.push('');
|
||||
}
|
||||
z.push('Naechster Schritt: git pull, dann die Briefe in Cowork als Schreibvorlage nutzen.');
|
||||
return [{ json: {
|
||||
betreff: `Skrift Ratgeber: ${s.anzahl_ok} Brief(e) bereit` + (s.anzahl_fehler ? `, ${s.anzahl_fehler} Fehler` : ''),
|
||||
text: z.join('\n'),
|
||||
}}];
|
||||
""")
|
||||
link(STATUS, MAIL_TEXT, out=0)
|
||||
|
||||
node("Benachrichtigung", "n8n-nodes-base.emailSend", 2.1, [1780, 140], {
|
||||
"fromEmail": "", "toEmail": "={{ $('Konfiguration').first().json.benachrichtigung_an }}",
|
||||
"subject": "={{ $json.betreff }}", "emailFormat": "text",
|
||||
"text": "={{ $json.text }}", "options": {}},
|
||||
disabled=True,
|
||||
notes="SMTP-Credential zuweisen, Absender eintragen, dann Node aktivieren.")
|
||||
link(MAIL_TEXT, "Benachrichtigung")
|
||||
|
||||
wf = {
|
||||
"name": "brief-vorbereiten",
|
||||
"nodes": nodes,
|
||||
"connections": conns,
|
||||
"settings": {"executionOrder": "v1", "saveManualExecutions": True,
|
||||
"callerPolicy": "workflowsFromSameOwner"},
|
||||
}
|
||||
ziel = os.path.join(os.path.dirname(__file__), '..', 'workflows', 'brief-vorbereiten.json')
|
||||
json.dump(wf, open(ziel, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
||||
print("geschrieben:", os.path.normpath(ziel))
|
||||
print("Nodes:", len(nodes), "| Verbindungen:", sum(len(v) for m in conns.values() for v in m['main']))
|
||||
Reference in New Issue
Block a user