- DB: segments 用户输入文本(Alembic 0002) - Chat: 阶段检测/阶段提示/回复限制,编排与访谈/画像 prompts 调整 - Memoir: 忠实度检查 agent,叙事与分类等链路更新 - Core: agent 日志、Alembic 启动、LangChain/日志/配置等 - Story: time_hints;Memory 检索与相关测试 - Expo: 助手头像、会话页与消息拆分、实时会话与文案/i18n - Docs/scripts/tests: 迁移脚本、LLM JSON/记忆检索文档、新增单测
138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
"""
|
||
章节封面 Celery 任务入队闸门:DB 二次校验 + Redis 短时去重,避免重复 delay 同一 chapter。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Literal
|
||
|
||
import redis
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import joinedload
|
||
|
||
from app.core.config import settings
|
||
from app.core.db import get_sync_db
|
||
from app.core.logging import get_logger
|
||
from app.features.memoir.asset_resolver import strip_image_placeholders
|
||
from app.features.memoir.cover_eligibility import (
|
||
chapter_eligible_for_cover_by_inline_body_image_count,
|
||
chapter_has_story_links,
|
||
chapter_needs_cover_enqueue,
|
||
primary_chapter_memoir_image,
|
||
)
|
||
from app.features.memoir.models import Chapter, ChapterStoryLink
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
CHAPTER_COVER_ENQUEUE_DEDUP_TTL_SECONDS = 450
|
||
_ENQUEUE_KEY_PREFIX = "enqueue:chapter-cover:"
|
||
|
||
|
||
def _enqueue_dedup_key(chapter_id: str) -> str:
|
||
return f"{_ENQUEUE_KEY_PREFIX}{chapter_id}"
|
||
|
||
|
||
def _chapter_eligible_for_http_enqueue(chapter: Chapter | None) -> bool:
|
||
"""与 MemoirService.check_and_trigger_cover_generation 循环条件一致。"""
|
||
if not chapter:
|
||
return False
|
||
if not chapter.category:
|
||
return False
|
||
if not chapter_has_story_links(chapter):
|
||
return False
|
||
if getattr(chapter, "cover_asset_id", None):
|
||
return False
|
||
md = (chapter.canonical_markdown or "").strip()
|
||
body = md or ""
|
||
if not body.strip():
|
||
return False
|
||
body = strip_image_placeholders(body).strip()
|
||
if not body:
|
||
return False
|
||
if not chapter_eligible_for_cover_by_inline_body_image_count(chapter):
|
||
return False
|
||
cover_rec = primary_chapter_memoir_image(chapter)
|
||
if cover_rec and (cover_rec.status or "").strip() == "completed":
|
||
return False
|
||
return True
|
||
|
||
|
||
def _chapter_eligible_for_pipeline_enqueue(chapter: Chapter | None) -> bool:
|
||
"""尚无 cover_asset、正文插图数 > 3(与 HTTP 闸门共用 chapter_needs_cover_enqueue 核心)。"""
|
||
return bool(chapter_needs_cover_enqueue(chapter))
|
||
|
||
|
||
def _load_chapter_for_enqueue_sync(chapter_id: str) -> Chapter | None:
|
||
with get_sync_db() as db:
|
||
stmt = (
|
||
select(Chapter)
|
||
.where(Chapter.id == chapter_id)
|
||
.options(
|
||
joinedload(Chapter.images),
|
||
joinedload(Chapter.story_links).joinedload(ChapterStoryLink.story),
|
||
)
|
||
)
|
||
return db.execute(stmt).unique().scalar_one_or_none()
|
||
|
||
|
||
def try_enqueue_generate_chapter_cover(
|
||
chapter_id: str,
|
||
source: Literal["http", "pipeline"] = "pipeline",
|
||
) -> bool:
|
||
"""
|
||
若章节仍需要生成封面,则 SET NX 去重后派发 generate_chapter_cover。
|
||
|
||
Returns:
|
||
True 当且仅当成功调用 delay(通过闸门)。
|
||
"""
|
||
chapter = _load_chapter_for_enqueue_sync(chapter_id)
|
||
if source == "http":
|
||
eligible = _chapter_eligible_for_http_enqueue(chapter)
|
||
else:
|
||
eligible = _chapter_eligible_for_pipeline_enqueue(chapter)
|
||
if not eligible:
|
||
return False
|
||
|
||
key = _enqueue_dedup_key(chapter_id)
|
||
try:
|
||
client = redis.from_url(settings.redis_url, decode_responses=True)
|
||
if not client.set(
|
||
key, "1", nx=True, ex=CHAPTER_COVER_ENQUEUE_DEDUP_TTL_SECONDS
|
||
):
|
||
logger.debug(
|
||
"chapter_cover enqueue skipped (dedup): chapter={} source={}",
|
||
chapter_id,
|
||
source,
|
||
)
|
||
return False
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"chapter_cover enqueue dedup redis failed, allowing enqueue: chapter={} error={}",
|
||
chapter_id,
|
||
exc,
|
||
)
|
||
|
||
from app.tasks.chapter_cover_tasks import generate_chapter_cover
|
||
|
||
try:
|
||
generate_chapter_cover.delay(chapter_id)
|
||
except Exception as exc:
|
||
logger.warning(
|
||
"chapter_cover delay failed: chapter={} error={}",
|
||
chapter_id,
|
||
exc,
|
||
)
|
||
try:
|
||
client = redis.from_url(settings.redis_url, decode_responses=True)
|
||
client.delete(key)
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
logger.info(
|
||
"chapter_cover enqueued: chapter={} source={}",
|
||
chapter_id,
|
||
source,
|
||
)
|
||
return True
|