2026-03-20 15:15:35 +08:00
|
|
|
|
"""
|
|
|
|
|
|
按 chapter_story_links 顺序将各 story 正文物化为单一 markdown(无 LLM)。
|
|
|
|
|
|
保留 story 内 asset:// 引用不变。
|
2026-03-20 16:36:42 +08:00
|
|
|
|
章节级 canonical:仅正文拼接,故事间用 ---;故事标题仅存 stories.title。
|
|
|
|
|
|
PDF 导出可单独物化「## 标题 + 正文」版本。
|
2026-03-20 15:15:35 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-03-20 16:36:42 +08:00
|
|
|
|
from app.features.memoir.markdown_sanitize import sanitize_story_for_chapter_compose
|
2026-03-20 15:15:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 16:36:42 +08:00
|
|
|
|
def _gather_title_body_pairs(chapter: Any) -> list[tuple[str, str]]:
|
2026-03-20 15:15:35 +08:00
|
|
|
|
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))
|
2026-03-20 16:36:42 +08:00
|
|
|
|
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))
|