重构回忆录为 story-first / markdown-first 架构并整合图片意图与前端 UI 修复
本次 squash merge 将 codex-story-first-image-intent 的整体改动合入 development,核心内容包括: 1. 后端数据与迁移:新增 stories、story_versions、story_image_intents、chapter_cover_intents、assets 等模型与 Alembic 迁移,建立 story-first、markdown-first、asset-first 的主数据链路。 2. 生成与任务链:引入 StoryBuilderOrchestrator、ChapterComposerOrchestrator、story_image_tasks、chapter_cover_tasks,图片生成从正文占位符改为结构化 intent -> asset -> markdown 回填。 3. 并发与一致性:为 story/chapter intent 增加 claim_token、claimed_at、attempt_count,采用数据库原子 claim 为主、Redis 锁为辅,避免重复生成、锁误删和 processing 卡死。 4. Memoir 读写路径:章节 canonical_markdown 成为正文真源,列表/详情接口补齐 markdown、cover_asset、word_count 等字段,PDF 与 asset 解析链路同步升级。 5. Memory / Retrieval:扩展 transcript ingest、chunking、evidence 检索与 story 聚合基础设施,为后续 story-first RAG 与多 agent 编排提供底座。 6. App 端体验:章节页继续走 MarkdownRenderer 阅读链,同时吸收 fix3-19 的跨平台 UI glitch 修复;更新对话页、首页、文案资源与章节列表映射逻辑。 7. 测试与文档:补充 asset resolver、story image task、章节封面派发、markdown 映射等回归测试,并加入图片占位符退役设计文档。
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
"""Memoir repository — Book, Chapter, MemoirState data access."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.features.memoir.models import Book, Chapter, ChapterSection, MemoirState
|
||||
from app.features.memoir.models import (
|
||||
Book,
|
||||
Chapter,
|
||||
ChapterSection,
|
||||
ChapterVersion,
|
||||
MemoirState,
|
||||
)
|
||||
|
||||
|
||||
async def get_current_book(user_id: str, db: AsyncSession) -> Book | None:
|
||||
@@ -91,3 +100,101 @@ def get_archived_chapter_summaries_sync(
|
||||
if preview.strip():
|
||||
summaries.append((ch.title or "", preview))
|
||||
return summaries
|
||||
|
||||
|
||||
def ensure_chapter_markdown_and_version_sync(
|
||||
session: Session,
|
||||
chapter: Chapter,
|
||||
markdown: str,
|
||||
) -> None:
|
||||
"""
|
||||
为已有 chapter 设置 canonical_markdown 并创建 chapter_version。
|
||||
由 _save_narrative_to_sections 调用,确保 markdown 真源与版本链。
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
count_stmt = select(func.count(ChapterVersion.id)).where(
|
||||
ChapterVersion.chapter_id == chapter.id
|
||||
)
|
||||
version_no = (session.execute(count_stmt).scalar() or 0) + 1
|
||||
|
||||
version = ChapterVersion(
|
||||
id=str(uuid.uuid4()),
|
||||
chapter_id=chapter.id,
|
||||
version_no=version_no,
|
||||
markdown_snapshot=markdown,
|
||||
actor_type="ai",
|
||||
source_type="generate",
|
||||
)
|
||||
session.add(version)
|
||||
session.flush()
|
||||
chapter.canonical_markdown = markdown
|
||||
chapter.current_version_id = version.id
|
||||
|
||||
|
||||
def save_chapter_markdown_sync(
|
||||
session: Session,
|
||||
*,
|
||||
user_id: str,
|
||||
chapter_id: str | None,
|
||||
title: str,
|
||||
category: str,
|
||||
order_index: int,
|
||||
markdown: str,
|
||||
source_segments: list[str] | None = None,
|
||||
) -> Chapter:
|
||||
"""
|
||||
将 markdown 写入 chapter.canonical_markdown 和 chapter_versions。
|
||||
Agent 不直接调用,由 service/task 调用。
|
||||
若 chapter_id 为 None 则新建章节。
|
||||
"""
|
||||
if chapter_id:
|
||||
chapter = session.get(Chapter, chapter_id)
|
||||
if not chapter or chapter.user_id != user_id:
|
||||
raise ValueError(f"Chapter {chapter_id} not found or access denied")
|
||||
else:
|
||||
chapter = Chapter(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
category=category,
|
||||
order_index=order_index,
|
||||
status="completed",
|
||||
is_new=True,
|
||||
is_active=True,
|
||||
source_segments=source_segments or [],
|
||||
)
|
||||
session.add(chapter)
|
||||
session.flush()
|
||||
|
||||
# 创建 chapter_version
|
||||
from sqlalchemy import func
|
||||
|
||||
count_stmt = select(func.count(ChapterVersion.id)).where(
|
||||
ChapterVersion.chapter_id == chapter.id
|
||||
)
|
||||
version_no = (session.execute(count_stmt).scalar() or 0) + 1
|
||||
|
||||
version = ChapterVersion(
|
||||
id=str(uuid.uuid4()),
|
||||
chapter_id=chapter.id,
|
||||
version_no=version_no,
|
||||
markdown_snapshot=markdown,
|
||||
actor_type="ai",
|
||||
source_type="generate",
|
||||
)
|
||||
session.add(version)
|
||||
session.flush()
|
||||
|
||||
chapter.canonical_markdown = markdown
|
||||
chapter.current_version_id = version.id
|
||||
chapter.title = title
|
||||
chapter.is_new = True
|
||||
if source_segments:
|
||||
chapter.source_segments = list(
|
||||
set((chapter.source_segments or []) + source_segments)
|
||||
)
|
||||
|
||||
session.flush()
|
||||
session.refresh(chapter)
|
||||
return chapter
|
||||
|
||||
Reference in New Issue
Block a user