feat(seo): aviso de colisión de tema en /generate blog en
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Al publicar el draft EN en Ghost, el título propuesto se compara contra los posts published+scheduled del sitio (corpus vía Admin API — incluye la cola programada, justo el caso del doble Kecksburg del 2026-07-10) con el topic_collision vendorizado. Si colisiona, el notice de Telegram lleva un bloque "🚨 Posible colisión de tema" en las tres rutas (live, dryrun, bare). Nunca bloquea: el draft se crea igual, el humano decide (fusionar, retitular o enlazar a propósito). Solo lang=en (stopwords inglesas). Títulos ajenos saneados de entidades Markdown (regla de _safe_send). Aislamiento: cualquier fallo del check → notice sin bloque y warning en logs, jamás rompe la publicación. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0e9a2e5b36
commit
dadc030d16
@@ -612,6 +612,7 @@ class OutputGenerator:
|
||||
return ""
|
||||
title = _extract_title(full_output) or topic
|
||||
mode = _resolve_seo_mode(seo_override)
|
||||
collision_note = await self._collision_note(lang, title)
|
||||
|
||||
if mode in ("on", "dryrun"):
|
||||
try:
|
||||
@@ -636,13 +637,17 @@ class OutputGenerator:
|
||||
# surface the proposal so Jose can inspect before trusting writes.
|
||||
result = await ghost.publish_draft(title, full_output)
|
||||
post = result["posts"][0]
|
||||
self.last_publish_notice = _seo_dryrun_message(ghost, post, seo, inserted_pairs)
|
||||
self.last_publish_notice = (
|
||||
_seo_dryrun_message(ghost, post, seo, inserted_pairs)
|
||||
+ collision_note)
|
||||
else:
|
||||
result = await ghost.publish_draft(
|
||||
title, full_output, tags=seo["tags"], seo=seo,
|
||||
body_html=linked_html)
|
||||
post = result["posts"][0]
|
||||
self.last_publish_notice = _seo_live_message(ghost, post, seo, inserted_pairs)
|
||||
self.last_publish_notice = (
|
||||
_seo_live_message(ghost, post, seo, inserted_pairs)
|
||||
+ collision_note)
|
||||
logger.info("Auto-published blog to Ghost",
|
||||
mode=mode, post_id=post["id"], links=len(inserted_pairs))
|
||||
return ""
|
||||
@@ -661,11 +666,27 @@ class OutputGenerator:
|
||||
result = await ghost.publish_draft(title, full_output)
|
||||
post = result["posts"][0]
|
||||
logger.info("Auto-published blog to Ghost (bare)", post_id=post["id"])
|
||||
return _bare_ghost_notice(ghost, post)
|
||||
return _bare_ghost_notice(ghost, post) + collision_note
|
||||
except Exception as e:
|
||||
logger.warning("Auto-publish to Ghost failed", error=str(e))
|
||||
return ""
|
||||
|
||||
async def _collision_note(self, lang: str, title: str) -> str:
|
||||
"""Aviso de canibalización del título propuesto contra los posts
|
||||
published+scheduled del sitio. Solo EN (las stopwords del motor son
|
||||
inglesas). Nunca lanza y nunca bloquea: el draft se publica igual y
|
||||
el aviso viaja en el notice de Telegram.
|
||||
"""
|
||||
if lang != "en":
|
||||
return ""
|
||||
try:
|
||||
from src.seo.autofill import collision_notice, fetch_collision_corpus
|
||||
corpus = await fetch_collision_corpus(lang)
|
||||
return collision_notice(title, corpus) or ""
|
||||
except Exception as e:
|
||||
logger.warning("Topic collision check failed — skipped", error=str(e))
|
||||
return ""
|
||||
|
||||
async def generate(self, session_id: int, output_type: OutputType,
|
||||
progress_callback=None, lang: str = "es",
|
||||
seo_override: str | None = None) -> str:
|
||||
|
||||
Reference in New Issue
Block a user