fix: 修复 Liblib provider 认证和多个图片生成关键缺陷

- 重写 LiblibImageProvider:Bearer token 改为 HMAC-SHA1 签名认证,
  适配 Liblib 真实 API(Star-3 Alpha 文生图端点)
- 修复 chapter.images JSON 列原地修改不持久化(深拷贝+整列重赋值)
- 修复 generate_chapter_images 在事务提交前派发(改为 commit 后统一 delay)
- 修复 initialize_chapter_images 覆盖已完成图片(新增 merge 去重逻辑)
- 修复 Android failed 图片渲染为错误卡片(改为隐藏,保持正文连续)
- 模型模板 UUID 改为环境变量配置(LIBLIB_TEMPLATE_UUID)
- 更新 .env 凭证格式为 ACCESS_KEY/SECRET_KEY
- 补充 test_memoir_image_bootstrap 缺失的 unittest.mock 导入

Made-with: Cursor
This commit is contained in:
Kevin
2026-03-10 17:02:50 +08:00
parent 830b6efc39
commit 0970cb7408
11 changed files with 484 additions and 111 deletions

View File

@@ -78,6 +78,54 @@ def _update_task_status_sync(user_id: str, task_id: str, status: str, result: Di
except Exception as e:
logger.error(f"更新任务状态失败: {e}")
def _merge_chapter_image_assets(
existing_images: list[dict] | None,
placeholders: list[dict],
provider: str,
style: str,
size: str,
now_iso: str,
) -> list[dict]:
existing_by_placeholder = {
item.get("placeholder"): dict(item)
for item in (existing_images or [])
if item.get("placeholder")
}
merged_assets: list[dict] = []
for item in placeholders:
existing = existing_by_placeholder.get(item["placeholder"])
if existing:
merged_item = dict(existing)
merged_item["index"] = item["index"]
merged_item["placeholder"] = item["placeholder"]
merged_item["description"] = item["description"]
merged_item["provider"] = merged_item.get("provider") or provider
merged_item["style"] = merged_item.get("style") or style
merged_item["size"] = merged_item.get("size") or size
merged_item["created_at"] = merged_item.get("created_at") or now_iso
merged_item["updated_at"] = merged_item.get("updated_at") or now_iso
if merged_item.get("status") == "completed" and not merged_item.get("url"):
merged_item["status"] = "failed"
merged_item["error"] = merged_item.get("error") or "missing image url"
else:
merged_item = build_initial_image_assets(
placeholders=[item],
provider=provider,
style=style,
size=size,
now_iso=now_iso,
)[0]
merged_assets.append(merged_item)
return merged_assets
def chapter_has_images_to_generate(images: list[dict] | None) -> bool:
return any(item.get("status") in {"pending", "failed"} for item in (images or []))
def initialize_chapter_images(chapter) -> list[dict]:
"""Parse IMAGE placeholders from chapter content and build pending image assets."""
settings = MemoirImageSettings.from_env()
@@ -88,15 +136,14 @@ def initialize_chapter_images(chapter) -> list[dict]:
prompt_service = MemoirImagePromptService(llm=None, settings=settings)
placeholders = parse_image_placeholders(chapter.content, settings.max_per_chapter)
style = prompt_service.CATEGORY_STYLE_MAP.get(chapter.category, settings.default_style)
chapter.images = build_initial_image_assets(
chapter.images = _merge_chapter_image_assets(
existing_images=chapter.images,
placeholders=placeholders,
provider=settings.provider,
style=style,
size=settings.default_size,
now_iso=datetime.now(timezone.utc).isoformat(),
)
if chapter.images:
generate_chapter_images.delay(chapter.id)
return chapter.images
@@ -234,6 +281,7 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
try:
db = SessionLocal()
try:
chapters_to_enqueue: set[str] = set()
# 获取段落
stmt = select(Segment).where(Segment.id.in_(segment_ids))
result = db.execute(stmt)
@@ -401,6 +449,8 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
db.flush()
initialize_chapter_images(chapter)
if chapter_has_images_to_generate(chapter.images):
chapters_to_enqueue.add(chapter.id)
# 更新 Book
stmt_book = select(Book).where(Book.user_id == user_id).order_by(Book.updated_at.desc())
@@ -426,6 +476,13 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
seg.processed = True
db.commit()
for chapter_id in sorted(chapters_to_enqueue):
try:
generate_chapter_images.delay(chapter_id)
except Exception as exc:
logger.warning(f"补图任务派发失败: chapter={chapter_id}, error={exc}")
logger.info(f"回忆录处理完成: user_id={user_id}, task_id={task_id}")
# 更新任务状态为成功
@@ -546,16 +603,21 @@ def generate_chapter_images(self, chapter_id: str):
settings = MemoirImageSettings.from_env()
prompt_service = MemoirImagePromptService(llm_service.get_llm(), settings)
provider = LiblibImageProvider()
provider = LiblibImageProvider(template_uuid=settings.liblib_template_uuid)
storage = TencentCosStorageService.from_env()
images = [dict(item) for item in (chapter.images or [])]
for item in chapter.images:
for index, item in enumerate(images):
if item.get("status") == "completed" and item.get("url"):
continue
if item.get("status") not in {"pending", "failed"}:
continue
item["status"] = "processing"
current_item = dict(item)
current_item["status"] = "processing"
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
images[index] = current_item
chapter.images = images
db.commit()
try:
@@ -580,21 +642,23 @@ def generate_chapter_images(self, chapter_id: str):
max_attempts=settings.max_attempts,
)
image_bytes = provider.download_image(job)
key = build_cos_key(chapter.user_id, chapter.id, item["index"], prompt_data["prompt"])
item["url"] = storage.upload_bytes(image_bytes, key, "image/png")
item["prompt"] = prompt_data["prompt"]
item["style"] = prompt_data["style"]
item["size"] = prompt_data["size"]
item["status"] = "completed"
item["error"] = None
key = build_cos_key(chapter.user_id, chapter.id, current_item["index"], prompt_data["prompt"])
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"] = "completed"
current_item["error"] = None
except Exception as exc:
item["status"] = "failed"
item["error"] = str(exc)
logger.warning(f"图片生成失败: chapter={chapter_id}, index={item.get('index')}, error={exc}")
current_item["status"] = "failed"
current_item["error"] = str(exc)
logger.warning(f"图片生成失败: chapter={chapter_id}, index={current_item.get('index')}, error={exc}")
item["updated_at"] = datetime.now(timezone.utc).isoformat()
current_item["updated_at"] = datetime.now(timezone.utc).isoformat()
images[index] = current_item
chapter.images = images
db.commit()
db.commit()
return {"status": "success"}
finally:
db.close()