feat(automation): autonomous pipeline with Telegram bot and N8N integration

- Add full auto pipeline: RSS ingest → GPT relevance score → AI rewrite → WP draft
- Add Telegram bot with inline buttons (rewrite/discard/override) and commands (/run, /rejected, /status)
- Add smart publish scheduler: max 2 drafts/day, spread over week (09:00 & 14:00 CET)
- Add N8N API endpoints (/api/n8n/pipeline, /api/n8n/ingest) with X-API-Key auth
- Add GPT-based relevance scoring (0-100) for VanLife/Camping/Outdoor topics
- Remove Ampel risk-level policy check from ingestion (all enabled feeds are used)
- Add Telegram webhook endpoint and setup endpoint
- Add delete_wp_post() for Telegram discard action
- Add DB migrations for relevance_score and scheduled_publish_at columns
- Update .env.example with all new configuration variables
- Add docs/AUTOMATION.md with full setup and usage documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OliverGiertz 2026-03-21 09:40:15 +00:00
parent 6332a9a399
commit 6192f8e527
11 changed files with 1361 additions and 25 deletions

View file

@ -146,6 +146,47 @@ def generate_article_tags(article: dict[str, Any], rewritten_text: str | None =
return []
def score_article_relevance(article: dict[Any, Any]) -> dict[str, Any]:
"""Score article relevance for VanLife/Camping/Outdoor blog (0-100).
Returns {"score": int, "reason": str, "topics": list[str]}.
Raises RuntimeError on OpenAI failure.
"""
title = (article.get("title") or "").strip()
text = _sanitize_source_text(article.get("content_raw") or "")
if not text:
text = (article.get("summary") or "").strip()
prompt = (
"Bewerte die Relevanz des folgenden Artikels für einen deutschen VanLife-, Camping- und Outdoor-Blog. "
"Relevante Themen: Campingplätze, Stellplätze, Wohnmobil, Camper, Van, Roadtrip, "
"Outdoor-Ausrüstung, Wandern, Naturreisen, Reise-Tipps für Campende. "
"Nicht relevant: allgemeine Nachrichten, Politik, Wirtschaft, Sport (außer Outdoor), Unterhaltung.\n\n"
"Antworte NUR mit einem JSON-Objekt:\n"
'{"score": <0-100>, "reason": "<kurze Begründung auf Deutsch>", "topics": ["<Thema1>", "<Thema2>"]}\n\n'
f"Titel: {title}\n\n"
f"Text (Auszug):\n{text[:2000]}"
)
raw = _openai_chat(
"Du bist ein Redakteur für einen VanLife- und Camping-Blog und bewertest Artikelrelevanz.",
prompt,
temperature=0.1,
)
try:
match = re.search(r"\{[\s\S]*\}", raw)
if match:
parsed = json.loads(match.group(0))
score = max(0, min(100, int(parsed.get("score", 0))))
return {
"score": score,
"reason": str(parsed.get("reason", "")),
"topics": [str(t) for t in (parsed.get("topics") or [])],
}
except Exception:
pass
return {"score": 0, "reason": "Parsing-Fehler bei Relevanz-Score", "topics": []}
def merge_generated_tags(meta_json: str | None, tags: list[str]) -> str:
meta: dict[str, Any] = {}
if meta_json: