Files
life-echo/api/app/features/story/models.py

162 lines
5.6 KiB
Python
Raw Normal View History

重构回忆录为 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 映射等回归测试,并加入图片占位符退役设计文档。
2026-03-20 10:30:07 +08:00
"""
Story Layer 数据模型
- stories: 可独立讲述的人生故事canonical_markdown 为正文真源
- story_versions: 版本链记录每次生成/编辑
- story_evidence_links: story evidencechunk/fact/timeline_event/summary的关联
"""
from sqlalchemy import (
Column,
DateTime,
Float,
ForeignKey,
Integer,
JSON,
String,
Text,
)
from sqlalchemy.orm import relationship
from app.core.db import Base, utc_now
class Story(Base):
"""可独立讲述的一段人生故事,正文真源。"""
__tablename__ = "stories"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
title = Column(String, nullable=False)
stage = Column(
String, nullable=True
) # childhood / education / career / family / belief / summary
story_type = Column(
String, nullable=True
) # event / person / relationship / reflection / turning_point
summary = Column(Text, nullable=True)
canonical_markdown = Column(Text, nullable=True) # 当前生效正文
time_start = Column(String, nullable=True) # 起始时间,粗粒度 year/month/date
time_end = Column(String, nullable=True)
people_refs = Column(JSON, nullable=True)
place_refs = Column(JSON, nullable=True)
tag_refs = Column(JSON, nullable=True)
status = Column(String, default="active") # active / archived / merged / draft
confidence = Column(Float, nullable=True)
current_version_id = Column(String, nullable=True) # FK 在 migration 中分步添加
created_at = Column(DateTime(timezone=True), default=utc_now)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
versions = relationship(
"StoryVersion",
back_populates="story",
foreign_keys="StoryVersion.story_id",
cascade="all, delete-orphan",
)
current_version = relationship(
"StoryVersion",
primaryjoin="Story.current_version_id == StoryVersion.id",
foreign_keys="StoryVersion.id",
)
user = relationship("User", back_populates="stories")
evidence_links = relationship(
"StoryEvidenceLink",
back_populates="story",
cascade="all, delete-orphan",
)
chapter_links = relationship(
"ChapterStoryLink",
back_populates="story",
cascade="all, delete-orphan",
)
image_intents = relationship(
"StoryImageIntent",
back_populates="story",
cascade="all, delete-orphan",
)
class StoryVersion(Base):
"""Story 版本快照,记录正文变更与来源。"""
__tablename__ = "story_versions"
id = Column(String, primary_key=True)
story_id = Column(
String, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False, index=True
)
version_no = Column(Integer, nullable=False)
markdown_snapshot = Column(Text, nullable=False)
change_summary = Column(Text, nullable=True)
actor_type = Column(String, nullable=True) # ai / user / editor / system
source_type = Column(
String, nullable=True
) # generate / rewrite / merge / manual / migration
parent_version_id = Column(
String, ForeignKey("story_versions.id", ondelete="SET NULL"), nullable=True
)
prompt_meta = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=utc_now)
story = relationship("Story", back_populates="versions", foreign_keys=[story_id])
parent_version = relationship(
"StoryVersion",
remote_side="StoryVersion.id",
foreign_keys=[parent_version_id],
)
class StoryEvidenceLink(Base):
"""Story 与 evidence 的关联。"""
__tablename__ = "story_evidence_links"
id = Column(String, primary_key=True)
story_id = Column(
String, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False, index=True
)
evidence_type = Column(
String, nullable=False
) # chunk / fact / timeline_event / summary
evidence_id = Column(String, nullable=False)
role = Column(String, nullable=True) # primary / supporting / background
weight = Column(Float, nullable=True)
created_at = Column(DateTime(timezone=True), default=utc_now)
story = relationship("Story", back_populates="evidence_links")
class StoryImageIntent(Base):
"""Story 主插图意图,每个 active story 仅允许 1 条 intent_role=primary。"""
__tablename__ = "story_image_intents"
id = Column(String, primary_key=True)
story_id = Column(
String, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False, index=True
)
story_version_id = Column(
String,
ForeignKey("story_versions.id", ondelete="SET NULL"),
nullable=True,
)
intent_role = Column(String, nullable=False) # primary
caption = Column(String, nullable=True)
prompt_brief = Column(Text, nullable=True)
style_profile = Column(String, nullable=True)
status = Column(String, nullable=False) # pending / processing / completed / failed
claim_token = Column(String, nullable=True)
claimed_at = Column(DateTime(timezone=True), nullable=True)
attempt_count = Column(Integer, nullable=False, default=0)
asset_id = Column(String, nullable=True)
error = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=utc_now)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
story = relationship("Story", back_populates="image_intents")
story_version = relationship(
"StoryVersion",
foreign_keys=[story_version_id],
)