""" 回忆录序列化与图片归一化辅助(供 MemoirService 使用)。 """ from app.core.logging import get_logger from app.core.config import settings from app.features.memoir.models import Chapter, ChapterSection from app.features.memoir.memoir_images.schema import ( IMAGE_STATUS_COMPLETED, IMAGE_STATUS_FAILED, completed_image_assets, normalize_image_assets, ) from app.features.memoir.memoir_images.serializers import memoir_image_to_dict from app.features.memoir.memoir_images.settings import MemoirImageSettings from app.features.memoir.memoir_images.storage import ( CosDownloadUrlError, TencentCosStorageService, mark_image_delivery_unavailable, normalize_cos_url, resolve_image_storage_key, ) logger = get_logger(__name__) def normalize_image_assets_for_api(images: list[dict] | None) -> list[dict]: bucket = settings.tencent_cos_bucket or "" region = settings.tencent_cos_region or "" base_url = settings.tencent_cos_base_url or "" storage = TencentCosStorageService.from_settings(settings) img_settings = MemoirImageSettings.from_settings(settings) source_assets = normalize_image_assets(images) if not img_settings.enabled: source_assets = completed_image_assets(source_assets) normalized_assets: list[dict] = [] for item in source_assets: asset = dict(item) normalized_url = normalize_cos_url( asset.get("url"), bucket=bucket, region=region, base_url=base_url ) storage_key = resolve_image_storage_key(asset) if asset.get("status") == IMAGE_STATUS_COMPLETED and storage_key: try: asset["url"] = storage.get_download_url(storage_key) except CosDownloadUrlError as exc: logger.warning( "章节图片签名失败: key=%s, retryable=%s, error=%s", storage_key, exc.retryable, exc, ) asset = mark_image_delivery_unavailable(asset) except Exception as exc: logger.warning("章节图片签名失败: key=%s, error=%s", storage_key, exc) asset = mark_image_delivery_unavailable(asset) else: asset["url"] = normalized_url asset.pop("storage_key", None) normalized_assets.append(asset) return normalized_assets def is_image_permanently_unavailable(rec) -> bool: if not rec: return False status = getattr(rec, "status", None) or "" retryable = getattr(rec, "retryable", None) url = getattr(rec, "url", None) storage_key = getattr(rec, "storage_key", None) if status == IMAGE_STATUS_FAILED and retryable is False: return True if status == IMAGE_STATUS_COMPLETED and not url and not storage_key: return True return False def section_image_to_dict(section) -> dict | None: if getattr(section, "image_record", None): return memoir_image_to_dict(section.image_record) return None def chapter_cover_to_dict(ch: Chapter) -> dict | None: images = getattr(ch, "images", None) or [] for m in images: if getattr(m, "section_id", None) is None: return memoir_image_to_dict(m) if getattr(ch, "cover_image", None) and isinstance(ch.cover_image, dict): return ch.cover_image return None def sections_to_content_and_images(ch: Chapter) -> tuple[str, list[dict]]: sections = getattr(ch, "sections", None) or [] ordered = sorted(sections, key=lambda s: getattr(s, "order_index", 0)) parts = [] images = [] for s in ordered: text = (getattr(s, "content", None) or "").strip() if text: parts.append(text) img = section_image_to_dict(s) if img: images.append(img) placeholder = (img.get("placeholder") or "").strip() if placeholder: parts.append(placeholder) content = "\n\n".join(parts) if parts else "" return content, images def chapter_to_dict(ch: Chapter) -> dict: content, images_list = sections_to_content_and_images(ch) normalized_images = normalize_image_assets_for_api(images_list) cover = chapter_cover_to_dict(ch) cover_normalized = ( normalize_image_assets_for_api([cover])[0] if cover else None ) sections_data = [] if getattr(ch, "sections", None): for s in sorted(ch.sections, key=lambda x: getattr(x, "order_index", 0)): sec_img = section_image_to_dict(s) sec_img = ( normalize_image_assets_for_api([sec_img])[0] if sec_img else None ) sections_data.append({ "content": (getattr(s, "content", None) or "").strip(), "image": sec_img, }) return { "id": ch.id, "title": ch.title, "content": content, "order_index": ch.order_index, "status": ch.status, "category": ch.category, "images": normalized_images, "cover_image": cover_normalized, "sections": sections_data, "updated_at": ch.updated_at.isoformat() if ch.updated_at else None, "is_new": ch.is_new, "source_segments": ch.source_segments or [], }