feat: 章节软删除、对话左滑删除,移除已读状态
- 章节:详情页增加删除按钮,软删除(is_active=False),AI 不再修改但保留供参考 - 章节:get_chapter 增加 is_active 校验,已删除章节返回 404 - 章节:AI 生成时参考同类别已删除章节摘要 - 对话:左滑显示删除,调用 hard delete API,删除前二次确认 - 对话:根布局包裹 GestureHandlerRootView 以支持 Swipeable - 对话:移除已读/未读状态展示及相关 i18n
This commit is contained in:
@@ -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}
|
||||
|
||||
## 第一步:提炼核心内容
|
||||
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user