fix(seo): sanitize malformed link suggestions + clean link report
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7a995da88f
commit
6d39875fcf
+21
-11
@@ -328,16 +328,25 @@ def _bare_ghost_notice(ghost: "GhostPublisher", post: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _seo_link_summary(seo: dict, n_links: int) -> str:
|
||||
pairs = [(l["phrase"], l["slug"]) for l in seo.get("internal_links", [])]
|
||||
return f"{n_links} insertados" + (f" — {pairs}" if pairs else "")
|
||||
def _seo_link_summary(inserted_pairs: list[dict], n_suggestions: int) -> str:
|
||||
"""Report ACTUALLY-INSERTED links only ("phrase → /slug/"), with a separate
|
||||
count of well-formed suggestions that were skipped (no verbatim body match).
|
||||
Never surfaces raw rejected suggestions."""
|
||||
n = len(inserted_pairs)
|
||||
skipped = max(0, n_suggestions - n)
|
||||
head = f"{n} insertados" + (f", {skipped} omitidos" if skipped else "")
|
||||
if not inserted_pairs:
|
||||
return head
|
||||
body = "; ".join(f"{p['phrase']} → /{p['slug']}/" for p in inserted_pairs)
|
||||
return f"{head} — {body}"
|
||||
|
||||
|
||||
def _seo_live_message(ghost: "GhostPublisher", post: dict, seo: dict, n_links: int) -> str:
|
||||
def _seo_live_message(ghost: "GhostPublisher", post: dict, seo: dict, inserted_pairs: list[dict]) -> str:
|
||||
"""Separate Telegram message for the live autofill path (SEO written to draft)."""
|
||||
slug = post.get("slug", "")
|
||||
warns = seo.get("seo_warnings") or []
|
||||
warn_line = ("\n⚠️ revisar: " + "; ".join(warns)) if warns else ""
|
||||
n_sug = len(seo.get("internal_links", []))
|
||||
return (
|
||||
f"📤 *Borrador en Ghost — SEO autorrelleno*\n"
|
||||
f"Editar: {ghost.url}/ghost/#/editor/post/{post['id']}\n\n"
|
||||
@@ -346,7 +355,7 @@ def _seo_live_message(ghost: "GhostPublisher", post: dict, seo: dict, n_links: i
|
||||
f"• meta desc ({len(seo['meta_description'])}): {seo['meta_description']}\n"
|
||||
f"• excerpt: {len(seo['custom_excerpt'])} car.\n"
|
||||
f"• tags: {', '.join(seo['tags'])}\n"
|
||||
f"• internal links: {_seo_link_summary(seo, n_links)}"
|
||||
f"• internal links: {_seo_link_summary(inserted_pairs, n_sug)}"
|
||||
f"{warn_line}\n\n"
|
||||
f"🖼️ *Imagen sugerida:*\n"
|
||||
f"`imagefinder --query \"{seo['image_query']}\" --context \"{seo['image_context']}\" --lang {ghost.lang}`\n\n"
|
||||
@@ -354,11 +363,12 @@ def _seo_live_message(ghost: "GhostPublisher", post: dict, seo: dict, n_links: i
|
||||
)
|
||||
|
||||
|
||||
def _seo_dryrun_message(ghost: "GhostPublisher", post: dict, seo: dict, n_links: int) -> str:
|
||||
def _seo_dryrun_message(ghost: "GhostPublisher", post: dict, seo: dict, inserted_pairs: list[dict]) -> str:
|
||||
"""Separate Telegram message for DRY-RUN: bare draft created, SEO only proposed."""
|
||||
slug = post.get("slug", "")
|
||||
warns = seo.get("seo_warnings") or []
|
||||
warn_line = ("\n⚠️ revisar: " + "; ".join(warns)) if warns else ""
|
||||
n_sug = len(seo.get("internal_links", []))
|
||||
return (
|
||||
f"🧪 *DRY-RUN SEO* — draft creado SIN escribir SEO (solo revisión)\n"
|
||||
f"Editar: {ghost.url}/ghost/#/editor/post/{post['id']}\n\n"
|
||||
@@ -367,7 +377,7 @@ def _seo_dryrun_message(ghost: "GhostPublisher", post: dict, seo: dict, n_links:
|
||||
f"• meta desc ({len(seo['meta_description'])}): {seo['meta_description']}\n"
|
||||
f"• excerpt ({len(seo['custom_excerpt'])} car.): {seo['custom_excerpt']}\n"
|
||||
f"• tags: {', '.join(seo['tags'])}\n"
|
||||
f"• internal links que se insertarían: {_seo_link_summary(seo, n_links)}"
|
||||
f"• internal links que se insertarían: {_seo_link_summary(inserted_pairs, n_sug)}"
|
||||
f"{warn_line}\n\n"
|
||||
f"🖼️ *Imagen sugerida:*\n"
|
||||
f"`imagefinder --query \"{seo['image_query']}\" --context \"{seo['image_context']}\" --lang {ghost.lang}`\n\n"
|
||||
@@ -523,7 +533,7 @@ class OutputGenerator:
|
||||
raise RuntimeError("generate_seo_fields returned None")
|
||||
# Build the SAME html that will be posted, then link it deterministically.
|
||||
html = ghost._build_html(article_md)
|
||||
linked_html, n_links = insert_internal_links(
|
||||
linked_html, inserted_pairs = insert_internal_links(
|
||||
html, seo["internal_links"], menu, lang)
|
||||
|
||||
if mode == "dryrun":
|
||||
@@ -531,15 +541,15 @@ 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, n_links)
|
||||
self.last_publish_notice = _seo_dryrun_message(ghost, post, seo, inserted_pairs)
|
||||
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, n_links)
|
||||
self.last_publish_notice = _seo_live_message(ghost, post, seo, inserted_pairs)
|
||||
logger.info("Auto-published blog to Ghost",
|
||||
mode=mode, post_id=post["id"], links=n_links)
|
||||
mode=mode, post_id=post["id"], links=len(inserted_pairs))
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.warning("SEO autofill failed; falling back to bare draft",
|
||||
|
||||
Reference in New Issue
Block a user