""" 按 chapter_story_links 顺序将各 story 正文物化为单一 markdown(无 LLM)。 保留 story 内 asset:// 引用不变。 章节级 canonical:仅正文拼接,故事间用 ---;故事标题仅存 stories.title。 PDF 导出可单独物化「## 标题 + 正文」版本。 """ from typing import Any from app.features.memoir.markdown_sanitize import sanitize_story_for_chapter_compose def _gather_title_body_pairs(chapter: Any) -> list[tuple[str, str]]: links = sorted( list(getattr(chapter, "story_links", None) or []), key=lambda x: getattr(x, "order_index", 0), ) pairs: list[tuple[str, str]] = [] for link in links: st = getattr(link, "story", None) if st is None: continue title = (getattr(st, "title", None) or "").strip() body = (getattr(st, "canonical_markdown", None) or "").strip() pairs.append((title, body)) return pairs def compose_ordered_stories_to_markdown(ordered: list[tuple[str, str]]) -> str: """ :param ordered: (story_title, canonical_markdown) 已按阅读顺序排好(title 仅用于清洗去重) :return: 章节级 markdown;仅各故事正文,非空块之间用 \\n\\n---\\n\\n 分隔 """ bodies: list[str] = [] for title, md in ordered: raw = (md or "").strip() if not raw: continue cleaned = sanitize_story_for_chapter_compose(raw, title) if cleaned: bodies.append(cleaned) return "\n\n---\n\n".join(bodies) def compose_ordered_stories_to_pdf_markdown(ordered: list[tuple[str, str]]) -> str: """PDF:每故事 ## 标题 + 正文,块间 ---(标题来自元数据,不写回章节 canonical)。""" parts: list[str] = [] for title, md in ordered: t = (title or "").strip() or "故事" raw = (md or "").strip() if not raw: continue body = sanitize_story_for_chapter_compose(raw, title) if not body: continue parts.append(f"## {t}\n\n{body}") return "\n\n---\n\n".join(parts) def materialize_chapter_markdown_from_loaded_chapter(chapter: Any) -> str: """要求 chapter.story_links 已 eager-load,且各 link.story 可用。""" return compose_ordered_stories_to_markdown(_gather_title_body_pairs(chapter)) def materialize_chapter_pdf_markdown_from_loaded_chapter(chapter: Any) -> str: """PDF 专用:含每段 ## 故事名。""" return compose_ordered_stories_to_pdf_markdown(_gather_title_body_pairs(chapter))