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:
@@ -228,7 +228,7 @@ REGELN:
|
|||||||
- Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]".
|
- 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.
|
- 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}}.
|
- 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.
|
- 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.
|
- 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...").
|
- 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 "- "):
|
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. {{M<ID>}}
|
||||||
- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M<ID1>, M<ID2>}}
|
- [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.
|
"""Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort.
|
||||||
|
|
||||||
Jeder Bullet MUSS mit einer Quellen-Klammer enden (geschweifte Klammern).
|
Jeder Bullet MUSS mit einer Quellen-Klammer enden (geschweifte Klammern).
|
||||||
- Items im Format M<ID> werden gegen new_articles aufgeloest zu Quellen-Namen.
|
Items koennen drei Formen haben, werden alle zu 'Name|URL' normalisiert (URL optional):
|
||||||
- Items ohne ID-Pattern werden als bereits-aufgeloeste Namen uebernommen.
|
- M<ID>: Aufloesung gegen new_articles, ergibt 'Name|URL'.
|
||||||
- Bullets ohne Klammer oder mit leerer Klammer werden verworfen.
|
- '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:
|
if not text:
|
||||||
return []
|
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:
|
if new_articles:
|
||||||
for a in new_articles:
|
for a in new_articles:
|
||||||
aid = a.get("id")
|
aid = a.get("id")
|
||||||
if aid is not None:
|
if aid is not None:
|
||||||
name = (a.get("source") or "").strip()
|
name = (a.get("source") or "").strip()
|
||||||
|
url = (a.get("source_url") or "").strip()
|
||||||
if name:
|
if name:
|
||||||
articles_by_id[str(aid)] = name
|
articles_by_id[str(aid)] = (name, url)
|
||||||
|
|
||||||
bullets: list[str] = []
|
bullets: list[str] = []
|
||||||
# Dash-Praefix + zweiter Datums-Punkt + optionales Jahr: Claude Haiku laesst diese gelegentlich weg.
|
# 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)
|
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)
|
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():
|
for raw_line in text.splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
@@ -473,16 +488,37 @@ class AnalyzerAgent:
|
|||||||
|
|
||||||
raw_items = [it.strip() for it in brace_match.group(1).split(",") if it.strip()]
|
raw_items = [it.strip() for it in brace_match.group(1).split(",") if it.strip()]
|
||||||
resolved: list[str] = []
|
resolved: list[str] = []
|
||||||
|
seen_keys: set[str] = set()
|
||||||
|
|
||||||
|
def _dedupe_key(name: str) -> str:
|
||||||
|
return name.strip().lower()
|
||||||
|
|
||||||
for it in raw_items:
|
for it in raw_items:
|
||||||
if junk_item.match(it):
|
if junk_item.match(it):
|
||||||
continue
|
continue
|
||||||
mid = id_item.match(it)
|
mid = id_item.match(it)
|
||||||
if mid:
|
if mid:
|
||||||
name = articles_by_id.get(mid.group(1))
|
pair = articles_by_id.get(mid.group(1))
|
||||||
if name and name not in resolved:
|
if pair:
|
||||||
resolved.append(name)
|
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:
|
else:
|
||||||
if it not in resolved:
|
key = _dedupe_key(it)
|
||||||
|
if key not in seen_keys:
|
||||||
|
seen_keys.add(key)
|
||||||
resolved.append(it)
|
resolved.append(it)
|
||||||
|
|
||||||
if not resolved:
|
if not resolved:
|
||||||
|
|||||||
@@ -812,19 +812,29 @@ const UI = {
|
|||||||
|
|
||||||
let pillsHtml = '';
|
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);
|
const trailing = trailingNamesRe.exec(rawBody);
|
||||||
if (trailing) {
|
if (trailing) {
|
||||||
rawBody = rawBody.replace(trailingNamesRe, '').trim();
|
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();
|
const seen = new Set();
|
||||||
pillsHtml = names.map(name => {
|
pillsHtml = items.map(item => {
|
||||||
const key = normalize(name);
|
// 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 '';
|
if (seen.has(key)) return '';
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
if (/^(unbekannt|unknown|n\/a|keine)$/i.test(name)) return '';
|
if (/^(unbekannt|unknown|n\/a|keine)$/i.test(itemName)) return '';
|
||||||
const src = lookupByName(name);
|
// Wenn URL direkt mitgeliefert wurde: eindeutiger Link, keine Kollision mit sources_json moeglich
|
||||||
return buildPill(src, name);
|
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('');
|
}).filter(Boolean).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren