feat(ghost): guard anti-duplicados en el fallback de publish
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
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:
co-authored by
Claude Fable 5
parent
76c927f0d5
commit
7d07375dd1
@@ -419,6 +419,37 @@ class GhostPublisher:
|
|||||||
)
|
)
|
||||||
return f"{signing}.{sig}"
|
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,
|
async def publish_draft(self, title: str, markdown_content: str,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
seo: dict | None = None,
|
seo: dict | None = None,
|
||||||
@@ -559,6 +590,20 @@ class OutputGenerator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("SEO autofill failed; falling back to bare draft",
|
logger.warning("SEO autofill failed; falling back to bare draft",
|
||||||
error=str(e))
|
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
|
# fall through to the bare-draft publish below
|
||||||
|
|
||||||
# OFF mode, or autofill failed → today's exact bare-draft publish.
|
# OFF mode, or autofill failed → today's exact bare-draft publish.
|
||||||
|
|||||||
Reference in New Issue
Block a user