feat(ghost): guard anti-duplicados en el fallback de publish
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s

Si el autofill falla DESPUÉS de que Ghost aceptara el POST (p.ej. error
leyendo la respuesta), el draft ya existe: el fallback ahora comprueba
por título exacto entre los drafts recientes (find_draft_by_title) y
reutiliza el existente en vez de duplicar. Solo aplica al camino de
fallback-tras-excepción — el modo OFF y la re-generación deliberada
siguen creando draft nuevo como siempre. Best-effort: si el check
falla, se publica bare como antes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-04 20:26:23 +00:00
co-authored by Claude Fable 5
parent 76c927f0d5
commit 7d07375dd1
+45
View File
@@ -419,6 +419,37 @@ class GhostPublisher:
)
return f"{signing}.{sig}"
async def find_draft_by_title(self, title: str) -> dict | None:
"""Busca entre los drafts recientes uno con título exacto.
Guard anti-duplicados del fallback de _publish_blog_to_ghost: si el
POST de publish_draft fue aceptado por Ghost pero la lectura de la
respuesta falló (p.ej. decode roto), el draft existe aunque el caller
viera una excepción. Best-effort: None si no hay match o si falla."""
import aiohttp as _aio
token = self._make_token()
url = (
f"{self.url}/ghost/api/admin/posts/"
"?filter=status:draft&order=created_at%20desc&limit=15"
"&fields=id,title,slug"
)
async with _aio.ClientSession(timeout=_aio.ClientTimeout(total=15)) as sess:
async with sess.get(
url,
headers={
"Authorization": f"Ghost {token}",
"Accept-Version": "v5.0",
"Accept-Encoding": "gzip, deflate", # ver publish_draft
},
) as resp:
if resp.status != 200:
return None
data = await resp.json()
for p in data.get("posts", []):
if p.get("title") == title:
return p
return None
async def publish_draft(self, title: str, markdown_content: str,
tags: list[str] | None = None,
seo: dict | None = None,
@@ -559,6 +590,20 @@ class OutputGenerator:
except Exception as e:
logger.warning("SEO autofill failed; falling back to bare draft",
error=str(e))
# Guard anti-duplicados: si la excepción llegó DESPUÉS de que
# Ghost aceptara el POST (p.ej. fallo leyendo la respuesta),
# el draft ya existe — re-publicar lo duplicaría (visto
# 2026-07-04 con el decode br roto de aiohttp).
try:
existing = await ghost.find_draft_by_title(title)
except Exception as find_err:
logger.warning("Draft-exists check failed; continuing to bare publish",
error=str(find_err))
existing = None
if existing:
logger.info("Draft ya existente en Ghost tras fallo de autofill; no se re-publica",
post_id=existing["id"])
return _bare_ghost_notice(ghost, existing)
# fall through to the bare-draft publish below
# OFF mode, or autofill failed → today's exact bare-draft publish.