Add Stage 2.8 recall, quality gate, retries, and publish idempotency

This commit is contained in:
Mimikko-zeus
2026-06-10 21:31:13 +08:00
parent 07786e3bc0
commit b46cef2c7b
16 changed files with 1253 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import hashlib
from dataclasses import dataclass
from datetime import date, datetime, timezone
from pathlib import Path
@@ -20,6 +21,9 @@ class PublishResult:
class BlogClient(Protocol):
def get_post_by_slug(self, slug: str) -> dict[str, Any] | None:
...
def create_post(self, payload: dict[str, Any]) -> dict[str, Any]:
...
@@ -153,6 +157,18 @@ def dry_run_publish(slug: str, base_url: str) -> PublishResult:
)
def _content_hash(value: str) -> str:
return hashlib.sha256((value or "").encode("utf-8")).hexdigest()
def _get_existing_post(client: BlogClient, slug: str) -> dict[str, Any] | None:
getter = getattr(client, "get_post_by_slug", None)
if getter is None:
return None
existing = getter(slug)
return existing if isinstance(existing, dict) else None
def publish_markdown(
*,
title: str,
@@ -163,6 +179,7 @@ def publish_markdown(
mode: str,
markdown_report: dict[str, Any],
client: BlogClient | None,
idempotency_config: dict[str, Any] | None = None,
) -> PublishResult:
blocking_errors = markdown_report.get("blocking_errors", []) or []
blog_url = f"{base_url.rstrip('/')}/posts/{slug}"
@@ -187,6 +204,39 @@ def publish_markdown(
error="missing_blog_client",
)
idempotency_config = idempotency_config or {}
if bool(idempotency_config.get("enabled", False)):
try:
existing_post = _get_existing_post(client, slug)
except Exception as exc:
return PublishResult(
mode=mode,
status="failed",
slug=slug,
blog_url=blog_url,
public_ok=False,
error=f"idempotency_check_failed:{type(exc).__name__}: {exc}",
)
if existing_post is not None:
existing_content = str(existing_post.get("content") or existing_post.get("markdown") or "")
if _content_hash(existing_content) == _content_hash(markdown):
return PublishResult(
mode=mode,
status="already_published",
slug=slug,
blog_url=blog_url,
public_ok=True,
)
if not bool(idempotency_config.get("allow_republish", False)):
return PublishResult(
mode=mode,
status="blocked",
slug=slug,
blog_url=blog_url,
public_ok=False,
error="slug_already_exists",
)
payload = {"title": title, "content": markdown, "tags": tags, "slug": slug}
try:
create_resp = client.create_post(payload)