469 lines
20 KiB
Python
469 lines
20 KiB
Python
# -*- 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']))
|