fix/various fixes
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"""Memoir service — 回忆录编排(章节生成、状态流转);通过 MemoryService 获取 evidence。"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -14,6 +13,8 @@ from app.agents.memoir.prompts import (
|
||||
CHAPTER_ORDER,
|
||||
STAGE_TO_ORDER,
|
||||
)
|
||||
from app.core.logging import get_logger
|
||||
from app.core.storage_purge import delete_object_storage_keys_best_effort
|
||||
from app.features.memoir import repo
|
||||
from app.features.memoir.asset_resolver import (
|
||||
collect_asset_ids_for_chapter,
|
||||
@@ -21,14 +22,19 @@ from app.features.memoir.asset_resolver import (
|
||||
strip_legacy_image_placeholders,
|
||||
)
|
||||
from app.features.memoir.asset_urls import signed_urls_for_asset_ids
|
||||
from app.features.memoir.chapter_markdown_compose import (
|
||||
materialize_chapter_markdown_from_loaded_chapter,
|
||||
)
|
||||
from app.features.memoir.cover_eligibility import primary_chapter_memoir_image
|
||||
from app.features.memoir.helpers import (
|
||||
chapter_to_dict,
|
||||
chapter_to_list_dict,
|
||||
is_image_permanently_unavailable,
|
||||
)
|
||||
from app.features.memoir.models import Book, Chapter, ChapterSection
|
||||
from app.features.memoir.memoir_images.settings import MemoirImageSettings
|
||||
from app.features.memoir.models import Book, Chapter
|
||||
from app.features.memory.service import MemoryService
|
||||
from app.ports.storage import ObjectStorage
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -43,9 +49,12 @@ class MemoirService:
|
||||
self,
|
||||
db: AsyncSession,
|
||||
memory_service: Optional[MemoryService] = None,
|
||||
*,
|
||||
object_storage: ObjectStorage | None = None,
|
||||
):
|
||||
self._db = db
|
||||
self._memory = memory_service
|
||||
self._object_storage = object_storage
|
||||
|
||||
async def get_evidence(self, user_id: str, query: str, *, top_k: int = 10) -> dict:
|
||||
"""通过 MemoryService 获取检索证据(章节生成时优先使用)。"""
|
||||
@@ -59,13 +68,10 @@ class MemoirService:
|
||||
return await self._memory.retrieve(user_id, query, top_k=top_k)
|
||||
|
||||
async def _cleanup_unavailable_images(self, ch: Chapter) -> None:
|
||||
sections = getattr(ch, "sections", None) or []
|
||||
cleaned = False
|
||||
for s in sections:
|
||||
rec = getattr(s, "image_record", None)
|
||||
for rec in getattr(ch, "images", None) or []:
|
||||
if rec and is_image_permanently_unavailable(rec):
|
||||
logger.info("清理不可用配图: chapter=%s, section=%s", ch.id, s.id)
|
||||
s.image_id = None
|
||||
logger.info("清理不可用配图: chapter=%s, image=%s", ch.id, rec.id)
|
||||
await self._db.delete(rec)
|
||||
cleaned = True
|
||||
if cleaned:
|
||||
@@ -125,8 +131,8 @@ class MemoirService:
|
||||
select(Chapter)
|
||||
.where(Chapter.user_id == user_id, Chapter.is_active == True)
|
||||
.options(
|
||||
joinedload(Chapter.sections).joinedload(ChapterSection.image_record),
|
||||
joinedload(Chapter.images),
|
||||
joinedload(Chapter.story_links),
|
||||
)
|
||||
.order_by(Chapter.order_index)
|
||||
)
|
||||
@@ -205,13 +211,19 @@ class MemoirService:
|
||||
return chapter_to_dict(chapter, asset_url_map=asset_map)
|
||||
|
||||
async def disable_chapter(self, chapter_id: str, user_id: str) -> dict:
|
||||
chapter = await self._db.get(Chapter, chapter_id)
|
||||
chapter = await repo.get_chapter_by_id(chapter_id, self._db)
|
||||
if not chapter:
|
||||
raise HTTPException(status_code=404, detail="Chapter not found")
|
||||
if chapter.user_id != user_id:
|
||||
raise HTTPException(status_code=403, detail="无权操作此章节")
|
||||
cos_keys = await repo.collect_cos_storage_keys_for_chapter(self._db, chapter)
|
||||
chapter.is_active = False
|
||||
await self._db.commit()
|
||||
delete_object_storage_keys_best_effort(
|
||||
self._object_storage,
|
||||
cos_keys,
|
||||
log_prefix=f"chapter_soft_delete id={chapter_id}",
|
||||
)
|
||||
return {"status": "ok", "message": "章节已清除"}
|
||||
|
||||
async def regenerate_chapter(self, chapter_id: str, user_id: str) -> dict:
|
||||
@@ -220,9 +232,46 @@ class MemoirService:
|
||||
raise HTTPException(status_code=404, detail="Chapter not found")
|
||||
if chapter.user_id != user_id:
|
||||
raise HTTPException(status_code=403, detail="无权操作此章节")
|
||||
# TODO: 实现重新整理逻辑
|
||||
n = await repo.count_chapter_story_links(self._db, chapter_id)
|
||||
if n > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="该章节由故事编排驱动,请更新故事正文或调整故事顺序,不支持在此处整章再生。",
|
||||
)
|
||||
# TODO: 非 story-backed 章节的 LLM 重新整理
|
||||
return {"status": "ok", "message": "Chapter regeneration triggered"}
|
||||
|
||||
async def set_chapter_story_order(
|
||||
self, chapter_id: str, user_id: str, story_ids: list[str]
|
||||
) -> dict:
|
||||
chapter = await self._db.get(Chapter, chapter_id)
|
||||
if not chapter:
|
||||
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")
|
||||
try:
|
||||
await repo.replace_chapter_story_links_async(
|
||||
self._db,
|
||||
chapter_id=chapter_id,
|
||||
user_id=user_id,
|
||||
story_ids=story_ids,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
ch = await repo.get_chapter_with_story_links_for_compose(chapter_id, self._db)
|
||||
if not ch:
|
||||
raise HTTPException(status_code=404, detail="Chapter not found")
|
||||
if not ch.story_links:
|
||||
md = ""
|
||||
else:
|
||||
md = materialize_chapter_markdown_from_loaded_chapter(ch)
|
||||
await repo.append_chapter_compose_version_async(self._db, ch, md)
|
||||
await self._db.commit()
|
||||
return {"status": "ok", "chapter_id": chapter_id, "story_count": len(story_ids)}
|
||||
|
||||
async def get_memoir_state(self, user_id: str) -> dict:
|
||||
from app.features.memoir.state_service import get_or_create_state
|
||||
|
||||
@@ -244,7 +293,7 @@ class MemoirService:
|
||||
有正文、尚无 cover_asset、且 legacy 封面 MemoirImage 未 completed 时,
|
||||
派发 generate_chapter_cover(由 intent/asset 闭环完成)。
|
||||
"""
|
||||
from app.tasks.chapter_cover_tasks import generate_chapter_cover
|
||||
from app.tasks.chapter_cover_enqueue import try_enqueue_generate_chapter_cover
|
||||
|
||||
img_settings = MemoirImageSettings.from_env()
|
||||
if not img_settings.enabled:
|
||||
@@ -260,30 +309,18 @@ class MemoirService:
|
||||
if getattr(ch, "cover_asset_id", None):
|
||||
continue
|
||||
md = (ch.canonical_markdown or "").strip()
|
||||
section_blob = "\n\n".join(
|
||||
(s.content or "").strip()
|
||||
for s in getattr(ch, "sections", None) or []
|
||||
if (s.content or "").strip()
|
||||
)
|
||||
body = md or strip_legacy_image_placeholders(section_blob).strip()
|
||||
body = strip_legacy_image_placeholders(md).strip() if md else ""
|
||||
if not body:
|
||||
continue
|
||||
images = getattr(ch, "images", None) or []
|
||||
cover_rec = next(
|
||||
(m for m in images if getattr(m, "section_id", None) is None),
|
||||
None,
|
||||
)
|
||||
if (
|
||||
cover_rec
|
||||
and (getattr(cover_rec, "status") or "").strip() == "completed"
|
||||
):
|
||||
cover_rec = primary_chapter_memoir_image(ch)
|
||||
if cover_rec and (cover_rec.status or "").strip() == "completed":
|
||||
continue
|
||||
try:
|
||||
generate_chapter_cover.delay(ch.id)
|
||||
enqueued = await asyncio.to_thread(
|
||||
try_enqueue_generate_chapter_cover, ch.id, "http"
|
||||
)
|
||||
if enqueued:
|
||||
triggered.append(ch.id)
|
||||
logger.info("触发生成章节封面(asset): chapter=%s", ch.id)
|
||||
except Exception as exc:
|
||||
logger.warning("封面任务派发失败: chapter=%s, error=%s", ch.id, exc)
|
||||
return {"triggered": triggered}
|
||||
|
||||
async def mark_memoir_read(self, user_id: str) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user