refactor(api,expo): 多智能体与会话收敛、回忆录兼容层移除、后端测试集大幅删减

- 对齐「多智能体收敛」与「回忆录 stories-first / markdown-first」方向:收紧运行时契约、
  删除过渡兼容路径与双轨逻辑,并同步更新客户端与文档。

- Chat:以 ChatOrchestrator 为实时编排入口;删除独立 conversation_agent,精简 prompts。
- Memoir:删除 memory_agent;MemoirOrchestrator、classification / story_route 与 prompts 收敛到
  prepare_batches + run_story_pipeline_for_category_batch 主链路。
- 将 agents 侧 processor 迁入 feature 层为 background_runner,并移除 features 下重复/过时
  processor 封装。

- 新增 history_store,强化「conversation_messages 为 DB 真源、Redis 为缓存」模型。
- 调整 models、repo、service、session_history;精简 WS message_types,重构 pipeline 与 router。

- 移除章节占位、整章再生等旧路径;章节列表与封面逻辑要求 story 关联;收紧 cover 资格与
  enqueue。
- helpers、repo、service、router、reading_segment_materialize、story_pipeline_sync、pdf_service
  等按 canonical markdown / cover_asset_id 收缩;删除 memoir_images/provider 等冗余。
- tasks:memoir_tasks、chapter_cover_tasks 等大幅瘦身;story_image_tasks 等与当前图片任务对齐。

- core:config、logging、redis、task_tracker 小幅调整。
- auth / user / payment / quota:路由或服务侧删减过时接口或逻辑(如 payment router 行数减少)。

- pyproject.toml、development.sh、.env.example / .env.production、README 等同步说明或变量。

- Alembic 0001_initial_schema 微调(与当前 schema 叙事一致的小改动)。

- 回忆录:types / mappers / api、章节页与 memoir 页与后端契约对齐;markdown-renderer 调整。
- 语音:删除 voice/player,voice-segment-store 相应精简。

- api/tests:删除 conftest 及绝大部分既有测试文件(websocket_baseline、conversation、memoir
  图片、PDF、SMS 等),属有意收缩/待按 backend-test-system 重建的信号。
- docs:新增多智能体收敛与移除兼容层计划摘要;更新 story-first 设计、backend-test-system、
  multi-agent-refactor-plan、实施总结等。

BREAKING CHANGE: 后端对外契约、回忆录章节字段与若干路由/任务行为已变更;大量 API 测试被移除,
  CI 若依赖这些用例需按新策略补测或调整流水线。
This commit is contained in:
Kevin
2026-03-22 16:45:57 +08:00
parent 70070216c4
commit 786ebf8ae6
122 changed files with 2802 additions and 7941 deletions

View File

@@ -4,13 +4,12 @@ Celery 任务模块
from .celery_app import celery_app
from .chapter_cover_tasks import generate_chapter_cover
from .memoir_tasks import process_memoir_segments, generate_chapter_images
from .memoir_tasks import process_memoir_segments
from .story_image_tasks import generate_story_image
__all__ = [
"celery_app",
"process_memoir_segments",
"generate_chapter_images",
"generate_chapter_cover",
"generate_story_image",
]

View File

@@ -13,13 +13,14 @@ 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_legacy_image_placeholders
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
from app.features.memoir.models import Chapter, ChapterStoryLink
logger = get_logger(__name__)
@@ -35,7 +36,9 @@ 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 or chapter.status == "empty":
if not chapter.category:
return False
if not chapter_has_story_links(chapter):
return False
if getattr(chapter, "cover_asset_id", None):
return False
@@ -43,7 +46,7 @@ def _chapter_eligible_for_http_enqueue(chapter: Chapter | None) -> bool:
body = md or ""
if not body.strip():
return False
body = strip_legacy_image_placeholders(body).strip()
body = strip_image_placeholders(body).strip()
if not body:
return False
if not chapter_eligible_for_cover_by_inline_body_image_count(chapter):
@@ -66,6 +69,7 @@ def _load_chapter_for_enqueue_sync(chapter_id: str) -> Chapter | None:
.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()

View File

@@ -16,21 +16,20 @@ from sqlalchemy.orm import joinedload
from app.core.db import get_sync_db
from app.core.dependencies import get_image_generator
from app.core.logging import get_logger
from app.core.redis_lock import acquire_redis_lock, release_redis_lock
from app.features.asset.models import Asset
from app.features.memoir.chapter_cover import (
aggregate_cover_prompt_from_chapter,
aggregate_cover_prompt_from_stories,
)
from app.features.memoir.cover_eligibility import (
chapter_eligible_for_cover_by_inline_body_image_count,
chapter_has_story_links,
)
from app.features.memoir.memoir_images.storage import TencentCosStorageService
from app.features.memoir.models import Chapter, ChapterCoverIntent, ChapterStoryLink
from app.ports.image_gen import TaskStatus
from app.core.logging import get_logger
logger = get_logger(__name__)
CHAPTER_COVER_LOCK_TTL_SECONDS = 1800
@@ -82,28 +81,17 @@ def _chapter_cover_claimable_clause(now: datetime):
def _build_chapter_cover_brief(chapter: Chapter) -> str:
prompt_brief = ""
stories = []
if getattr(chapter, "story_links", None):
for link in sorted(
chapter.story_links, key=lambda l: getattr(l, "order_index", 0)
):
story = getattr(link, "story", None)
if story:
stories.append(story)
prompt_brief = aggregate_cover_prompt_from_stories(
stories,
chapter_title=chapter.title or "",
chapter_category=chapter.category or "",
)
if prompt_brief:
return prompt_brief
md = (chapter.canonical_markdown or "").strip()
excerpt = md[:200] if md else ""
return aggregate_cover_prompt_from_chapter(
for link in sorted(
chapter.story_links, key=lambda link_row: link_row.order_index or 0
):
story = getattr(link, "story", None)
if story:
stories.append(story)
return aggregate_cover_prompt_from_stories(
stories,
chapter_title=chapter.title or "",
chapter_category=chapter.category or "",
markdown_excerpt=excerpt,
)
@@ -179,7 +167,7 @@ def generate_chapter_cover(self, chapter_id: str):
lock_key, ttl_seconds=CHAPTER_COVER_LOCK_TTL_SECONDS
)
if lock_handle is None:
logger.info("generate_chapter_cover: chapter=%s, reason=locked", chapter_id)
logger.debug("generate_chapter_cover: chapter=%s, reason=locked", chapter_id)
return {"status": "locked"}
claim_token = uuid.uuid4().hex
@@ -195,20 +183,26 @@ def generate_chapter_cover(self, chapter_id: str):
)
chapter = db.execute(stmt).unique().scalar_one_or_none()
if not chapter:
logger.info(
logger.debug(
"generate_chapter_cover: chapter=%s, reason=not_found", chapter_id
)
return {"status": "no_chapter"}
if not chapter_eligible_for_cover_by_inline_body_image_count(chapter):
logger.info(
logger.debug(
"generate_chapter_cover: chapter=%s, reason=insufficient_inline_body_images",
chapter_id,
)
return {"status": "insufficient_inline_body_images"}
if not chapter_has_story_links(chapter):
logger.debug(
"generate_chapter_cover: chapter=%s, reason=no_story_links",
chapter_id,
)
return {"status": "no_story_links"}
if getattr(chapter, "cover_asset_id", None):
logger.info(
logger.debug(
"generate_chapter_cover: chapter=%s, reason=has_cover_asset",
chapter_id,
)
@@ -216,7 +210,7 @@ def generate_chapter_cover(self, chapter_id: str):
intent = _claim_chapter_cover_intent_sync(db, chapter, claim_token)
if not intent:
logger.info(
logger.debug(
"generate_chapter_cover: chapter=%s, reason=no_claimable_intent",
chapter_id,
)
@@ -252,7 +246,7 @@ def generate_chapter_cover(self, chapter_id: str):
or (intent_db.status or "").strip() != "processing"
or (intent_db.claim_token or "").strip() != claim_token
):
logger.info(
logger.debug(
"generate_chapter_cover: skip persist intent=%s status=%s claim=%s",
intent.id,
getattr(intent_db, "status", None),
@@ -286,10 +280,17 @@ def generate_chapter_cover(self, chapter_id: str):
db.commit()
logger.info(
"generate_chapter_cover: chapter=%s, asset=%s, url=%s",
"generate_chapter_cover: chapter=%s, asset=%s",
chapter_id,
asset_id,
)
logger.debug(
"generate_chapter_cover: chapter=%s asset=%s url=%s cos_key=%s prompt_final=%s",
chapter_id,
asset_id,
url,
cos_key,
prompt_final,
)
return {"status": "success", "asset_id": asset_id}
except Exception as exc:
@@ -309,6 +310,6 @@ def generate_chapter_cover(self, chapter_id: str):
logger.warning(
"generate_chapter_cover failed: chapter=%s, error=%s", chapter_id, exc
)
raise self.retry(exc=exc)
raise self.retry(exc=exc) from exc
finally:
release_redis_lock(lock_handle)

View File

@@ -3,64 +3,47 @@
"""
import json
from app.core.logging import get_logger
import uuid
from io import BytesIO
from typing import Dict, List, Set
from datetime import datetime, timezone
from typing import Dict, List, Set
import redis
from celery import shared_task
from PIL import Image
from sqlalchemy import delete, select
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.db import get_sync_db
from app.features.conversation.models import Segment
from app.features.memoir.models import (
Book,
Chapter,
MemoirImage,
MemoirState,
)
from app.features.user.models import User
from app.core.dependencies import get_llm_provider
from app.agents.state_schema import MemoirStateSchema, SlotData, default_state
from app.agents.memoir import MemoirOrchestrator
from app.agents.chat.prompts_profile import format_user_profile_context
from app.agents.memoir import MemoirOrchestrator
from app.agents.state_schema import MemoirStateSchema, SlotData, default_state
from app.core.db import get_sync_db
from app.core.dependencies import get_llm_provider
from app.core.logging import get_logger
from app.features.conversation.models import Segment
from app.features.memoir.cover_eligibility import (
chapter_needs_cover_enqueue,
)
from app.features.memoir.memoir_images.parser import (
build_initial_image_assets,
parse_image_placeholders,
)
import hashlib
from app.core.dependencies import get_image_generator
from app.agents.image_prompt import ImagePromptOrchestrator
from app.features.memoir.memoir_images.schema import (
completed_image_assets,
IMAGE_STATUS_COMPLETED,
IMAGE_STATUS_FAILED,
IMAGE_STATUS_PENDING,
IMAGE_STATUS_PROCESSING,
normalize_image_assets,
)
from app.features.memoir.memoir_images.serializers import (
image_dict_to_row_kwargs,
memoir_image_to_dict,
)
from app.features.memoir.memoir_images.settings import MemoirImageSettings
from app.ports.image_gen import TaskStatus
from app.features.memoir.memoir_images.storage import (
TencentCosStorageService,
CosUploadError,
)
from app.features.memoir.cover_eligibility import (
chapter_needs_cover_enqueue,
cover_memoir_image_pending_or_failed,
from app.features.memoir.models import (
Book,
MemoirImage,
MemoirState,
)
from app.features.memoir.story_pipeline_sync import (
run_story_pipeline_for_category_batch,
)
from app.features.user.models import User
logger = get_logger(__name__)
_REDIS_CLIENTS: dict[bool, redis.Redis] = {}
@@ -101,20 +84,6 @@ def _release_chapter_lock(user_id: str, stage: str):
r.delete(lock_key)
def _acquire_chapter_image_lock(chapter_id: str, timeout: int = 600) -> bool:
"""获取章节补图分布式锁,避免同一章节重复补图。"""
r = _get_redis_client()
lock_key = f"lock:chapter-images:{chapter_id}"
return r.set(lock_key, "1", nx=True, ex=timeout)
def _release_chapter_image_lock(chapter_id: str):
"""释放章节补图分布式锁。"""
r = _get_redis_client()
lock_key = f"lock:chapter-images:{chapter_id}"
r.delete(lock_key)
def _update_task_status_sync(
user_id: str, task_id: str, status: str, result: Dict = None
):
@@ -139,7 +108,7 @@ def _update_task_status_sync(
r.hset(key, task_id, json.dumps(task_info))
r.expire(key, 3600) # 1小时过期
logger.info(f"任务状态已更新: task_id={task_id}, status={status}")
logger.debug("任务状态已更新: task_id=%s status=%s", task_id, status)
except Exception as e:
logger.error(f"更新任务状态失败: {e}")
@@ -240,25 +209,6 @@ def _select_placeholders_for_effective_max(
return [{**item, "index": index} for index, item in enumerate(selected)]
def initialize_chapter_images(_chapter):
"""兼容旧调用:封面由 generate_chapter_cover 处理。"""
logger.info("initialize_chapter_images: 封面由 generate_chapter_cover 处理,跳过")
return []
def _normalize_image_bytes_for_storage(image_bytes: bytes) -> bytes:
with Image.open(BytesIO(image_bytes)) as image:
output = BytesIO()
if image.mode in {"RGBA", "LA"}:
normalized = image
elif image.mode == "P":
normalized = image.convert("RGBA")
else:
normalized = image.convert("RGB")
normalized.save(output, format="PNG")
return output.getvalue()
def _coerce_state(model: MemoirState) -> MemoirStateSchema:
"""将数据库模型转换为 Schema"""
return MemoirStateSchema.model_validate(
@@ -372,8 +322,6 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
except Exception as e:
logger.warning("Memory ingest 跳过: %s", e)
# 获取用户状态和资料
state = _get_or_create_state_sync(user_id, db)
llm = _get_llm()
image_settings = MemoirImageSettings.from_env()
@@ -391,75 +339,71 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
story_dispatch_ids: Set[str] = set()
def _process_category(
chapter_category: str,
category_segments: List,
state: MemoirStateSchema,
profile: str,
birth_year,
llm,
):
"""stories-first路由 + 写 story物化 chapter。"""
nonlocal story_dispatch_ids
chapter, needs_cover, disp = run_story_pipeline_for_category_batch(
db,
user_id=user_id,
chapter_category=chapter_category,
category_segments=category_segments,
state=state,
user_profile=profile,
user_birth_year=birth_year,
llm=llm,
)
story_dispatch_ids |= disp
db.flush()
db.refresh(chapter)
needs_cover_enqueue = (
image_settings.enabled and chapter_needs_cover_enqueue(chapter)
)
stmt_book = (
select(Book)
.where(Book.user_id == user_id)
.order_by(Book.updated_at.desc())
)
result_book = db.execute(stmt_book)
book = result_book.scalar_one_or_none()
if not book:
book = Book(
id=str(uuid.uuid4()),
user_id=user_id,
title="我的回忆录",
total_pages=0,
total_words=0,
cover_image_url=None,
)
db.add(book)
book.has_update = True
book.last_update_chapter_id = chapter.id
return chapter, needs_cover_enqueue
def _raise_retry():
raise self.retry(countdown=10)
memoir_orchestrator = MemoirOrchestrator()
chapters_to_enqueue, _ = memoir_orchestrator.run(
segments=segments,
prepared = memoir_orchestrator.prepare_batches(
segments=list(segments),
llm=llm,
user_profile=user_profile,
user_birth_year=user_birth_year,
get_or_create_state=lambda: _get_or_create_state_sync(user_id, db),
update_slot=lambda stage, slot_name, snippet, seg_ids: (
_update_slot_sync(user_id, stage, slot_name, snippet, seg_ids, db)
),
acquire_lock=lambda stage: _acquire_chapter_lock(user_id, stage),
release_lock=lambda stage: _release_chapter_lock(user_id, stage),
process_category=_process_category,
raise_retry=_raise_retry,
)
chapters_to_enqueue: Set[str] = set()
for (
chapter_category,
category_segments,
) in prepared.category_to_segments.items():
if not _acquire_chapter_lock(user_id, chapter_category):
logger.warning(
"章节锁竞争: category=%s, 延迟重试",
chapter_category,
)
raise self.retry(countdown=10)
try:
chapter, needs_cover, disp = run_story_pipeline_for_category_batch(
db,
user_id=user_id,
chapter_category=chapter_category,
category_segments=category_segments,
state=prepared.state,
user_profile=user_profile,
user_birth_year=user_birth_year,
llm=llm,
)
story_dispatch_ids |= disp
db.flush()
db.refresh(chapter)
needs_cover_enqueue = (
image_settings.enabled and chapter_needs_cover_enqueue(chapter)
)
stmt_book = (
select(Book)
.where(Book.user_id == user_id)
.order_by(Book.updated_at.desc())
)
result_book = db.execute(stmt_book)
book = result_book.scalar_one_or_none()
if not book:
book = Book(
id=str(uuid.uuid4()),
user_id=user_id,
title="我的回忆录",
total_pages=0,
total_words=0,
cover_image_url=None,
)
db.add(book)
book.has_update = True
book.last_update_chapter_id = chapter.id
if chapter and needs_cover_enqueue:
chapters_to_enqueue.add(chapter.id)
finally:
_release_chapter_lock(user_id, chapter_category)
# 标记段落为已处理
for seg in segments:
seg.processed = True
@@ -503,7 +447,7 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
_update_task_status_sync(user_id, task_id, "failure", {"error": str(e)})
# 重试
raise self.retry(exc=e)
raise self.retry(exc=e) from e
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
@@ -580,161 +524,4 @@ def generate_chapter_content(self, user_id: str, stage: str, new_content: str):
except Exception as e:
logger.error(f"章节生成失败: {e}")
raise self.retry(exc=e)
def build_cos_key(user_id: str, chapter_id: str, index: int | str, prompt: str) -> str:
short_hash = hashlib.sha1(prompt.encode("utf-8")).hexdigest()[:10]
index_part = "cover" if index in (-1, "cover") else str(index)
return f"memoirs/{user_id}/{chapter_id}/{index_part}-{short_hash}.png"
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def generate_chapter_images(self, chapter_id: str):
"""异步补图:仅处理章节级 MemoirImagepending/failed。正文配图走 story_image_tasks。"""
lock_acquired = False
provider = None
with get_sync_db() as db:
try:
stmt = (
select(Chapter)
.where(Chapter.id == chapter_id)
.options(joinedload(Chapter.images))
)
chapter = db.execute(stmt).unique().scalar_one_or_none()
if not chapter:
logger.info("章节补图跳过: chapter=%s, reason=not_found", chapter_id)
return {"status": "no_chapter"}
cover_to_generate = cover_memoir_image_pending_or_failed(chapter)
if not cover_to_generate:
logger.info(
"章节补图跳过: chapter=%s, reason=no_pending_cover", chapter_id
)
return {"status": "no_images"}
settings = MemoirImageSettings.from_env()
if not settings.enabled:
logger.info("章节补图跳过: chapter=%s, reason=disabled", chapter_id)
return {"status": "disabled"}
lock_acquired = _acquire_chapter_image_lock(chapter_id)
if not lock_acquired:
logger.info("章节补图跳过: chapter=%s, reason=locked", chapter_id)
return {"status": "locked"}
prompt_orchestrator = ImagePromptOrchestrator(_get_llm(), settings)
image_generator = get_image_generator()
storage = TencentCosStorageService.from_env()
logger.info(
"章节封面补图开始: chapter=%s, cover=%s",
chapter_id,
bool(cover_to_generate),
)
retryable_failures: list[str] = []
permanent_failures: list[str] = []
def _apply_item_to_memoir_image(rec: MemoirImage, d: dict):
rec.placeholder = d.get("placeholder")
rec.description = d.get("description")
rec.status = (d.get("status") or "pending").strip() or "pending"
rec.prompt = d.get("prompt")
rec.url = d.get("url")
rec.storage_key = d.get("storage_key")
rec.provider = d.get("provider")
rec.style = d.get("style")
rec.size = d.get("size")
rec.error = d.get("error")
rec.retryable = d.get("retryable")
rec.updated_at = datetime.now(timezone.utc)
# 封面图(正文来自 canonical_markdown
if cover_to_generate:
current_item = memoir_image_to_dict(cover_to_generate) or {}
current_item.setdefault("placeholder", "")
current_item.setdefault("description", "")
current_item["status"] = IMAGE_STATUS_PROCESSING
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
_apply_item_to_memoir_image(cover_to_generate, current_item)
db.commit()
try:
raw_md = (
getattr(chapter, "canonical_markdown", None) or ""
).strip()
context_excerpt = " ".join(raw_md.split("\n")[:5])[:200]
prompt_data = prompt_orchestrator.build_cover_prompt(
chapter_title=chapter.title,
chapter_category=chapter.category or "",
context_excerpt=context_excerpt,
)
result = image_generator.generate(
prompt_data["prompt"],
prompt_data["size"],
prompt_data["style"],
)
if result.status != TaskStatus.COMPLETED or not result.image_url:
raise RuntimeError(result.error or "Image generation failed")
image_bytes = _normalize_image_bytes_for_storage(
image_generator.download_image(result.image_url)
)
key = build_cos_key(
chapter.user_id, chapter.id, "cover", prompt_data["prompt"]
)
current_item["storage_key"] = key
current_item["url"] = storage.upload_bytes(
image_bytes, key, "image/png"
)
current_item["prompt"] = prompt_data["prompt"]
current_item["style"] = prompt_data["style"]
current_item["size"] = prompt_data["size"]
current_item["status"] = IMAGE_STATUS_COMPLETED
current_item["error"] = None
current_item["retryable"] = None
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
_apply_item_to_memoir_image(cover_to_generate, current_item)
db.commit()
logger.info(
"章节封面图生成成功: chapter=%s, url=%s",
chapter_id,
current_item["url"],
)
except Exception as exc:
failure_msg = f"cover, error={exc}"
if isinstance(exc, CosUploadError) and not exc.retryable:
permanent_failures.append(failure_msg)
logger.error(
"封面图上传不可重试,清理: chapter=%s, %s",
chapter_id,
failure_msg,
)
db.delete(cover_to_generate)
db.commit()
else:
current_item = memoir_image_to_dict(cover_to_generate) or {}
current_item["status"] = IMAGE_STATUS_FAILED
current_item["error"] = str(exc)
current_item["retryable"] = True
current_item["updated_at"] = datetime.now(
timezone.utc
).isoformat()
retryable_failures.append(failure_msg)
logger.warning(
"封面图生成失败(可重试): chapter=%s, %s",
chapter_id,
failure_msg,
)
_apply_item_to_memoir_image(cover_to_generate, current_item)
db.commit()
if retryable_failures:
raise RuntimeError(
f"章节补图存在可重试失败项: chapter={chapter_id}, failures={'; '.join(retryable_failures)}"
)
return {"status": "success"}
except Exception as exc:
logger.error("章节补图任务失败: chapter=%s, error=%s", chapter_id, exc)
raise self.retry(exc=exc)
finally:
if provider:
provider.close()
if lock_acquired:
_release_chapter_image_lock(chapter_id)
raise self.retry(exc=e) from e

View File

@@ -160,7 +160,7 @@ def generate_story_image(self, story_id: str):
lock_key = f"lock:story-image:{story_id}"
lock_handle = acquire_redis_lock(lock_key, ttl_seconds=STORY_IMAGE_LOCK_TTL_SECONDS)
if lock_handle is None:
logger.info("generate_story_image: story=%s, reason=locked", story_id)
logger.debug("generate_story_image: story=%s, reason=locked", story_id)
return {"status": "locked"}
claim_token = uuid.uuid4().hex
@@ -170,7 +170,7 @@ def generate_story_image(self, story_id: str):
with get_sync_db() as db:
row = _claim_story_image_intent_sync(db, story_id, claim_token)
if not row:
logger.info(
logger.debug(
"generate_story_image: story=%s, reason=no_claimable_intent",
story_id,
)
@@ -213,7 +213,7 @@ def generate_story_image(self, story_id: str):
or (intent_db.status or "").strip() != "processing"
or (intent_db.claim_token or "").strip() != claim_token
):
logger.info(
logger.debug(
"generate_story_image: skip persist intent=%s status=%s claim=%s",
intent.id,
getattr(intent_db, "status", None),
@@ -249,12 +249,14 @@ def generate_story_image(self, story_id: str):
# 仅当 intent 仍指向当前版本时回填正文,避免慢任务/重试把图插到新版本上
if not target_vid or target_vid != current_vid:
db.commit()
logger.info(
logger.debug(
"generate_story_image: stale intent skip backfill story=%s "
"intent_ver=%s current=%s",
"intent_ver=%s current=%s url=%s asset=%s",
story_id,
target_vid,
current_vid,
url,
asset_id,
)
return {"status": "success_stale", "asset_id": asset_id}
@@ -297,10 +299,17 @@ def generate_story_image(self, story_id: str):
_enqueue_chapter_recompose_for_story(story_id)
logger.info(
"generate_story_image: story=%s, asset=%s, url=%s",
"generate_story_image: story=%s, asset=%s",
story_id,
asset_id,
)
logger.debug(
"generate_story_image: story=%s asset=%s url=%s cos_key=%s prompt_final=%s",
story_id,
asset_id,
url,
cos_key,
prompt_final,
)
return {"status": "success", "asset_id": asset_id}
except Exception as exc: