"""migrate legacy placeholders and memoir_images to assets 1. 从 chapters.canonical_markdown 移除 {{IMAGE:...}} / {{{{IMAGE:...}}}} 占位符 2. 将已完成 memoir_images(含 storage_key)写入 assets;章节封面绑定 cover_asset_id Revision ID: 0008_legacy_assets Revises: 0007_assets Create Date: 2026-03-19 """ import uuid from datetime import datetime, timezone from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "0008_legacy_assets" down_revision: Union[str, Sequence[str], None] = "0007_assets" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: from app.features.memoir.asset_resolver import strip_legacy_image_placeholders conn = op.get_bind() rows_ch = conn.execute( sa.text( "SELECT id, canonical_markdown FROM chapters WHERE canonical_markdown IS NOT NULL" ) ).fetchall() for cid, md in rows_ch: if not md or not str(md).strip(): continue cleaned = strip_legacy_image_placeholders(str(md)) if cleaned != str(md).strip(): conn.execute( sa.text("UPDATE chapters SET canonical_markdown = :md WHERE id = :id"), {"md": cleaned, "id": cid}, ) rows_mi = conn.execute( sa.text( """ SELECT id, chapter_id, section_id, storage_key, url, provider, style, status FROM memoir_images WHERE status = 'completed' AND storage_key IS NOT NULL AND TRIM(storage_key) <> '' """ ) ).fetchall() existing = { r[0] for r in conn.execute(sa.text("SELECT storage_key FROM assets")).fetchall() if r[0] } for row in rows_mi: _mid, chapter_id, section_id, storage_key, url, provider, style, _status = row sk = (storage_key or "").strip() if not sk or sk in existing: continue aid = str(uuid.uuid4()) asset_type = "chapter_cover" if section_id is None else "story_image" now = datetime.now(timezone.utc) conn.execute( sa.text( """ INSERT INTO assets ( id, asset_type, storage_key, url, provider, style_profile, prompt_final, status, width, height, created_at ) VALUES ( :id, :atype, :sk, :url, :prov, :style, :prompt, 'completed', NULL, NULL, :created ) """ ), { "id": aid, "atype": asset_type, "sk": sk, "url": url, "prov": provider, "style": style, "prompt": None, "created": now, }, ) existing.add(sk) if section_id is None and chapter_id: conn.execute( sa.text( """ UPDATE chapters SET cover_asset_id = :aid WHERE id = :cid AND (cover_asset_id IS NULL OR cover_asset_id = '') """ ), {"aid": aid, "cid": chapter_id}, ) def downgrade() -> None: pass