feat: 章节软删除、对话左滑删除,移除已读状态

- 章节:详情页增加删除按钮,软删除(is_active=False),AI 不再修改但保留供参考
- 章节:get_chapter 增加 is_active 校验,已删除章节返回 404
- 章节:AI 生成时参考同类别已删除章节摘要
- 对话:左滑显示删除,调用 hard delete API,删除前二次确认
- 对话:根布局包裹 GestureHandlerRootView 以支持 Swipeable
- 对话:移除已读/未读状态展示及相关 i18n
This commit is contained in:
Kevin
2026-03-19 10:44:35 +08:00
parent 1aa3d8593c
commit 9a1d31c71f
12 changed files with 223 additions and 80 deletions

View File

@@ -302,6 +302,7 @@ def get_narrative_prompt(
existing_content: str = "",
user_profile: str = "",
birth_year: Optional[int] = None,
archived_summaries: str = "",
) -> str:
"""将新对话改写为叙述(只输出新内容的改写,不重复已有内容)"""
context_tail = ""
@@ -309,6 +310,7 @@ def get_narrative_prompt(
context_tail = existing_content[-300:] if len(existing_content) > 300 else existing_content
context_section = f"\n\n【衔接上下文(已有内容的末尾,仅供参考衔接,不要重复)】:\n{context_tail}" if context_tail else ""
archived_section = f"\n\n【已删除的该类别历史章节(仅供参考,请勿直接使用或重复)】:\n{archived_summaries}" if archived_summaries else ""
profile_section = f"\n\n用户基本信息:\n{user_profile}" if user_profile else ""
age_hint = _build_age_hint(stage, birth_year)
@@ -323,6 +325,7 @@ def get_narrative_prompt(
新的对话内容:
{new_content}
{context_section}
{archived_section}
## 第一步:提炼核心内容
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:

View File

@@ -2,7 +2,7 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session, joinedload
from app.features.memoir.models import Book, Chapter, ChapterSection, MemoirState
@@ -56,3 +56,33 @@ async def get_memoir_state(user_id: str, db: AsyncSession) -> MemoirState | None
stmt = select(MemoirState).where(MemoirState.user_id == user_id)
result = await db.execute(stmt)
return result.scalar_one_or_none()
def get_archived_chapter_summaries_sync(
session: Session, user_id: str, category: str
) -> list[tuple[str, str]]:
"""获取已删除is_active=False的同类别章节的标题与内容摘要供 AI 参考。"""
stmt = (
select(Chapter)
.where(
Chapter.user_id == user_id,
Chapter.category == category,
Chapter.is_active == False, # noqa: E712
)
.options(joinedload(Chapter.sections))
.order_by(Chapter.updated_at.desc())
)
result = session.execute(stmt)
chapters = list(result.unique().scalars().all())
summaries: list[tuple[str, str]] = []
for ch in chapters:
sections = getattr(ch, "sections", None) or []
parts = [
(s.content or "").strip()
for s in sorted(sections, key=lambda x: getattr(x, "order_index", 0))
]
combined = "".join(parts)
preview = (combined[:200] + "...") if len(combined) > 200 else combined
if preview.strip():
summaries.append((ch.title or "", preview))
return summaries

View File

@@ -170,6 +170,8 @@ class MemoirService:
raise HTTPException(status_code=404, detail="Chapter not found")
if chapter.user_id != user_id:
raise HTTPException(status_code=403, detail="无权访问此章节")
if not chapter.is_active:
raise HTTPException(status_code=404, detail="Chapter not found")
await self._cleanup_unavailable_images(chapter)
return chapter_to_dict(chapter)

View File

@@ -23,6 +23,7 @@ from app.features.memoir.models import (
MemoirImage,
MemoirState,
)
from app.features.memoir import repo as memoir_repo
from app.features.user.models import User
from app.core.dependencies import get_llm_provider
from app.agents.state_schema import MemoirStateSchema, SlotData, default_state
@@ -713,6 +714,12 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
)
narrative = combined_text
# 已删除章节摘要供 AI 参考
archived = memoir_repo.get_archived_chapter_summaries_sync(db, user_id, chapter_category)
archived_summaries = "\n".join(
f"- 《{title_text}》:{preview}" for title_text, preview in archived
) if archived else ""
if llm:
try:
if not chapter:
@@ -733,6 +740,7 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
existing_content=existing_content,
user_profile=user_profile,
birth_year=user_birth_year,
archived_summaries=archived_summaries,
)
narrative_response = llm.invoke(narrative_prompt)
new_narrative = narrative_response.content.strip()
@@ -861,12 +869,18 @@ def generate_chapter_content(self, user_id: str, stage: str, new_content: str):
s.content for s in sorted(chapter.sections, key=lambda x: x.order_index) if (s.content or "").strip()
)
archived = memoir_repo.get_archived_chapter_summaries_sync(db, user_id, stage)
archived_summaries = "\n".join(
f"- 《{title_text}》:{preview}" for title_text, preview in archived
) if archived else ""
if llm:
prompt = get_narrative_prompt(
stage=stage,
slots={},
new_content=new_content,
existing_content=existing_content,
archived_summaries=archived_summaries,
)
response = llm.invoke(prompt)
new_narrative = response.content.strip()