Latest-Developments: Bullet-Format Name|URL statt nur Name

Problem: Pill-Link verwies auf falschen Post, weil sources_json fuer
Telegram-Kanaele viele Eintraege mit gleichem Namen aber unterschiedlichen
Post-URLs hat. Der Name-Match traf den ersten Eintrag (falschen Post).

Fix: Bullet-Format von {Name, Name} auf {Name|URL, Name|URL} erweitert.
Backend-Parser loest {M<ID>} nun zu Name|URL auf, URL kommt direkt vom
articles.source_url des belegenden Artikels. Kein sources_json-Lookup
noetig, keine Name-Kollision mehr moeglich.

Backend (analyzer.py):
- _parse_latest_developments: articles_by_id speichert (name, url) Tuple,
  Items werden als Name|URL gespeichert. Uebernommene Klammer-Items mit
  Pipe werden akzeptiert. Legacy-Items ohne Pipe bleiben als reiner Name.
- Prompt-Regel und Output-Beispiel auf {Name|URL, Name|URL} erweitert.

Frontend (components.js):
- buildPill-Aufruf vor Pipe-Split: Name und URL getrennt, wenn URL vorhanden
  wird Pseudo-src {name, url} uebergeben — eindeutiger Klicklink. Ohne URL
  Fallback auf lookupByName in sources_json (fuer Legacy-Bullets).
Dieser Commit ist enthalten in:
claude-dev
2026-04-18 23:19:02 +00:00
Ursprung 82e46792c7
Commit 34be98edaf
2 geänderte Dateien mit 64 neuen und 18 gelöschten Zeilen

Datei anzeigen

@@ -228,7 +228,7 @@ REGELN:
- Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]".
- Jedes Bullet ENDET mit einer Quellen-Klammer — ZWINGEND. Bullets ohne Klammer werden verworfen.
- NEUE Bullets (aus den NEUEN MELDUNGEN): {{M<ID1>, M<ID2>}} mit den ganzzahligen IDs aus der "ID:"-Zeile der belegenden Meldung(en). Beispiele: {{M42}} oder {{M42, M17}}.
- UEBERNOMMENE Bullets aus BISHERIGE ENTWICKLUNGEN: behalten ihre bestehende {{Name1, Name2}}-Klammer UNVERAENDERT. NICHT in M-IDs umwandeln.
- UEBERNOMMENE Bullets aus BISHERIGE ENTWICKLUNGEN: behalten ihre bestehende Klammer KOMPLETT UND UNVERAENDERT, inklusive des Pipe-Zeichens und der URL. Beispiel: {{Reuters|https://reuters.com/article, Rybar|https://t.me/rybar/123}}. NICHT in M-IDs umwandeln, NICHT die URL entfernen, NICHT umformatieren.
- Wenn mehrere Meldungen dasselbe Ereignis belegen: EIN Bullet, Zeitstempel = frühester Zeitpunkt, ALLE IDs in der Klammer.
- Bestehende Bullets aus BISHERIGE ENTWICKLUNGEN sinngemäß übernehmen, NICHT umformulieren. Nur entfernen, wenn sie durch neue Meldungen nachweislich überholt sind oder die 8-Bullet-Grenze überschritten wird (dann älteste fallen raus). Wenn einem uebernommenen Bullet die Quellen-Klammer fehlt (Altformat): Bullet VERWERFEN und nicht in die neue Liste uebernehmen.
- Wenn eine Quelle eine erkennbare politische Ausrichtung hat (z.B. pro-russisch, staatsnah, rechtsextrem), im Bullet-Text erwähnen ("laut pro-russischem Telegram-Kanal Rybar...").
@@ -241,7 +241,7 @@ REGELN:
OUTPUT-FORMAT (ausschliesslich, keine Anführungszeichen, kein Code-Fence, JEDE Zeile beginnt mit "- "):
- [DD.MM. HH:MM] Ereignistext neu. {{M<ID>}}
- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M<ID1>, M<ID2>}}
- [DD.MM. HH:MM] Ereignistext aus BISHERIGE ENTWICKLUNGEN. {{Quellenname1, Quellenname2}}
- [DD.MM. HH:MM] Ereignistext aus BISHERIGE ENTWICKLUNGEN. {{Quellenname1|URL1, Quellenname2|URL2}}
..."""
@@ -430,21 +430,28 @@ class AnalyzerAgent:
"""Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort.
Jeder Bullet MUSS mit einer Quellen-Klammer enden (geschweifte Klammern).
- Items im Format M<ID> werden gegen new_articles aufgeloest zu Quellen-Namen.
- Items ohne ID-Pattern werden als bereits-aufgeloeste Namen uebernommen.
- Bullets ohne Klammer oder mit leerer Klammer werden verworfen.
Items koennen drei Formen haben, werden alle zu 'Name|URL' normalisiert (URL optional):
- M<ID>: Aufloesung gegen new_articles, ergibt 'Name|URL'.
- 'Name|URL': wird uebernommen (Format aus previous_developments).
- 'Name' (ohne URL): bleibt unveraendert, wird als 'Name' gespeichert (Fallback).
Bullets ohne Klammer oder mit leerer Klammer werden verworfen.
Die URL wird direkt dem belegenden Artikel entnommen (article.source_url) — damit
ist der Klick im Frontend eindeutig auf den belegenden Post, ohne sources_json-Lookup.
"""
if not text:
return []
articles_by_id: dict[str, str] = {}
# Mapping id -> (name, url) aus new_articles
articles_by_id: dict[str, tuple[str, str]] = {}
if new_articles:
for a in new_articles:
aid = a.get("id")
if aid is not None:
name = (a.get("source") or "").strip()
url = (a.get("source_url") or "").strip()
if name:
articles_by_id[str(aid)] = name
articles_by_id[str(aid)] = (name, url)
bullets: list[str] = []
# Dash-Praefix + zweiter Datums-Punkt + optionales Jahr: Claude Haiku laesst diese gelegentlich weg.
@@ -455,6 +462,14 @@ class AnalyzerAgent:
id_item = re.compile(r"^[M#]\s*(\d+)$", re.IGNORECASE)
junk_item = re.compile(r"^(unbekannt|unknown|n/?a|keine|keine quelle|tba)$", re.IGNORECASE)
def _format_item(name: str, url: str) -> str:
"""Formatiert Name + URL zu 'Name|URL' (oder 'Name' wenn URL leer)."""
name = (name or "").strip()
url = (url or "").strip()
# Pipe im Namen ist extrem unwahrscheinlich, aber sicher ersetzen
name = name.replace("|", "/")
return f"{name}|{url}" if url else name
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
@@ -473,16 +488,37 @@ class AnalyzerAgent:
raw_items = [it.strip() for it in brace_match.group(1).split(",") if it.strip()]
resolved: list[str] = []
seen_keys: set[str] = set()
def _dedupe_key(name: str) -> str:
return name.strip().lower()
for it in raw_items:
if junk_item.match(it):
continue
mid = id_item.match(it)
if mid:
name = articles_by_id.get(mid.group(1))
if name and name not in resolved:
resolved.append(name)
pair = articles_by_id.get(mid.group(1))
if pair:
name, url = pair
key = _dedupe_key(name)
if key not in seen_keys:
seen_keys.add(key)
resolved.append(_format_item(name, url))
elif "|" in it:
# bereits im Name|URL-Format
parts = it.split("|", 1)
name_p = parts[0].strip()
url_p = (parts[1] if len(parts) > 1 else "").strip()
if name_p and not junk_item.match(name_p):
key = _dedupe_key(name_p)
if key not in seen_keys:
seen_keys.add(key)
resolved.append(_format_item(name_p, url_p))
else:
if it not in resolved:
key = _dedupe_key(it)
if key not in seen_keys:
seen_keys.add(key)
resolved.append(it)
if not resolved:

Datei anzeigen

@@ -812,19 +812,29 @@ const UI = {
let pillsHtml = '';
// Primär: {Name1, Name2} am Bullet-Ende
// Primär: {Name1|URL1, Name2|URL2} oder {Name1, Name2} am Bullet-Ende
const trailing = trailingNamesRe.exec(rawBody);
if (trailing) {
rawBody = rawBody.replace(trailingNamesRe, '').trim();
const names = trailing[1].split(',').map(s => s.trim()).filter(Boolean);
const items = trailing[1].split(',').map(s => s.trim()).filter(Boolean);
const seen = new Set();
pillsHtml = names.map(name => {
const key = normalize(name);
pillsHtml = items.map(item => {
// Split am ersten Pipe: "Name|URL" → Name + URL; ohne Pipe nur Name
const pipeIdx = item.indexOf('|');
const itemName = pipeIdx >= 0 ? item.slice(0, pipeIdx).trim() : item.trim();
const itemUrl = pipeIdx >= 0 ? item.slice(pipeIdx + 1).trim() : '';
if (!itemName) return '';
const key = normalize(itemName);
if (seen.has(key)) return '';
seen.add(key);
if (/^(unbekannt|unknown|n\/a|keine)$/i.test(name)) return '';
const src = lookupByName(name);
return buildPill(src, name);
if (/^(unbekannt|unknown|n\/a|keine)$/i.test(itemName)) return '';
// Wenn URL direkt mitgeliefert wurde: eindeutiger Link, keine Kollision mit sources_json moeglich
if (itemUrl) {
return buildPill({ name: itemName, url: itemUrl }, itemName);
}
// Fallback (Legacy-Bullets ohne URL): Name-Lookup in sources_json
const src = lookupByName(itemName);
return buildPill(src, itemName);
}).filter(Boolean).join('');
}