update: 更新伏笔管理删除和同步逻辑,避免重复引入

This commit is contained in:
xiamuceer
2026-04-09 09:15:11 +08:00
parent 5968a3d29e
commit 425aab9eec
6 changed files with 357 additions and 19 deletions
+82 -9
View File
@@ -145,12 +145,31 @@ async def analyze_chapter(
chapter_id,
chapter.chapter_number
)
# 重新分析前,先清理该章节旧记忆(关系库 + 向量库)
old_memories_result = await db.execute(
select(StoryMemory).where(StoryMemory.chapter_id == chapter_id)
)
old_memories = old_memories_result.scalars().all()
for old_mem in old_memories:
await db.delete(old_mem)
await db.flush()
if user_id:
try:
await memory_service.delete_chapter_memories(
user_id=user_id,
project_id=project_id,
chapter_id=chapter_id
)
except Exception as vector_delete_error:
logger.warning(f"⚠️ 清理章节向量记忆失败(继续分析): {str(vector_delete_error)}")
# 保存记忆到数据库和向量库
saved_count = 0
for mem_data in memories_data:
memory_id = str(uuid.uuid4())
# 保存到关系数据库
memory = StoryMemory(
id=memory_id,
@@ -164,7 +183,7 @@ async def analyze_chapter(
**mem_data['metadata']
)
db.add(memory)
# 保存到向量库
await memory_service.add_memory(
user_id=user_id,
@@ -175,13 +194,66 @@ async def analyze_chapter(
metadata=mem_data['metadata']
)
saved_count += 1
await db.commit()
entity_changes = {
"careers": {"updated_count": 0, "changes": []},
"character_states": {
"state_updated_count": 0,
"relationship_created_count": 0,
"relationship_updated_count": 0,
"org_updated_count": 0,
"changes": []
},
"organization_states": {"updated_count": 0, "changes": []}
}
# 更新角色职业 / 角色状态关系 / 组织状态
if analysis_result.get('character_states'):
try:
from app.services.career_update_service import CareerUpdateService
career_update_result = await CareerUpdateService.update_careers_from_analysis(
db=db,
project_id=project_id,
character_states=analysis_result.get('character_states', []),
chapter_id=chapter_id,
chapter_number=chapter.chapter_number
)
entity_changes["careers"] = career_update_result
except Exception as career_error:
logger.error(f"⚠️ 更新角色职业失败(不影响分析结果): {str(career_error)}", exc_info=True)
try:
from app.services.character_state_update_service import CharacterStateUpdateService
state_update_result = await CharacterStateUpdateService.update_from_analysis(
db=db,
project_id=project_id,
character_states=analysis_result.get('character_states', []),
chapter_id=chapter_id,
chapter_number=chapter.chapter_number
)
entity_changes["character_states"] = state_update_result
except Exception as state_error:
logger.error(f"⚠️ 更新角色状态、关系和组织成员失败(不影响分析结果): {str(state_error)}", exc_info=True)
if analysis_result.get('organization_states'):
try:
from app.services.character_state_update_service import CharacterStateUpdateService
org_state_result = await CharacterStateUpdateService.update_organization_states(
db=db,
project_id=project_id,
organization_states=analysis_result.get('organization_states', []),
chapter_number=chapter.chapter_number
)
entity_changes["organization_states"] = org_state_result
except Exception as org_state_error:
logger.error(f"⚠️ 更新组织自身状态失败(不影响分析结果): {str(org_state_error)}", exc_info=True)
# 【新增】自动更新伏笔状态
foreshadow_stats = {"planted_count": 0, "resolved_count": 0, "created_count": 0}
analysis_foreshadows = analysis_result.get('foreshadows', [])
if analysis_foreshadows:
try:
foreshadow_stats = await foreshadow_service.auto_update_from_analysis(
@@ -194,15 +266,16 @@ async def analyze_chapter(
logger.info(f"📊 伏笔自动更新: 埋入{foreshadow_stats['planted_count']}个, 回收{foreshadow_stats['resolved_count']}")
except Exception as fs_error:
logger.error(f"⚠️ 伏笔自动更新失败(不影响分析结果): {str(fs_error)}")
logger.info(f"✅ 章节分析完成: 保存{saved_count}条记忆")
return {
"success": True,
"message": f"分析完成,提取了{saved_count}条记忆",
"analysis": plot_analysis.to_dict(),
"memories_count": saved_count,
"foreshadow_stats": foreshadow_stats
"foreshadow_stats": foreshadow_stats,
"entity_changes": entity_changes
}
except HTTPException:
+134 -8
View File
@@ -9,6 +9,8 @@ import hashlib
from app.models.foreshadow import Foreshadow
from app.models.chapter import Chapter
from app.models.memory import PlotAnalysis, StoryMemory
from app.models.project import Project
from app.services.memory_service import memory_service
from app.schemas.foreshadow import (
ForeshadowCreate, ForeshadowUpdate,
PlantForeshadowRequest, ResolveForeshadowRequest,
@@ -232,18 +234,136 @@ class ForeshadowService:
db: AsyncSession,
foreshadow_id: str
) -> bool:
"""删除伏笔"""
"""删除伏笔,并同步清理关联记忆数据和历史分析引用"""
try:
foreshadow = await self.get_foreshadow(db, foreshadow_id)
if not foreshadow:
return False
project_result = await db.execute(
select(Project).where(Project.id == foreshadow.project_id)
)
project = project_result.scalar_one_or_none()
deleted_memory_rows = 0
deleted_vector_memories = 0
cleaned_analysis_refs = 0
cleaned_analysis_rows = 0
foreshadow_keywords = []
content_snippet = (foreshadow.content or "")[:50].strip()
if foreshadow.title and foreshadow.title.strip():
foreshadow_keywords.append(foreshadow.title.strip())
if content_snippet:
foreshadow_keywords.append(content_snippet)
memory_conditions = [
StoryMemory.project_id == foreshadow.project_id,
StoryMemory.memory_type == "foreshadow"
]
keyword_conditions = []
for keyword in foreshadow_keywords:
keyword_conditions.append(StoryMemory.content.contains(keyword))
keyword_conditions.append(StoryMemory.title.contains(keyword))
if keyword_conditions:
delete_memory_query = delete(StoryMemory).where(
and_(*memory_conditions, or_(*keyword_conditions))
)
delete_memory_result = await db.execute(delete_memory_query)
deleted_memory_rows = delete_memory_result.rowcount or 0
if project and project.user_id and foreshadow_keywords:
deleted_vector_memories = await memory_service.delete_foreshadow_memories(
user_id=project.user_id,
project_id=foreshadow.project_id,
foreshadow_keywords=foreshadow_keywords
)
analysis_result = await db.execute(
select(PlotAnalysis).where(PlotAnalysis.project_id == foreshadow.project_id)
)
project_analyses = analysis_result.scalars().all()
for analysis in project_analyses:
analysis_foreshadows = analysis.foreshadows or []
if not analysis_foreshadows:
continue
original_count = len(analysis_foreshadows)
filtered_foreshadows = []
removed_count = 0
for item in analysis_foreshadows:
if not isinstance(item, dict):
filtered_foreshadows.append(item)
continue
should_remove = False
# 1. 清理历史回收引用
if item.get("reference_foreshadow_id") == foreshadow_id:
should_remove = True
# 2. 清理历史埋入记录,避免“手动同步分析伏笔”再次从 PlotAnalysis 重建已删除伏笔
if not should_remove and foreshadow.source_type == "analysis":
item_type = item.get("type")
item_content = (item.get("content") or "").strip()
item_title = (item.get("title") or "").strip()
item_source_memory_id = None
if item_type == "planted" and item_content and analysis.chapter_id == foreshadow.plant_chapter_id:
item_source_memory_id = generate_stable_foreshadow_id(
analysis.chapter_id,
item_content,
item_type
)
if foreshadow.source_memory_id and item_source_memory_id == foreshadow.source_memory_id:
should_remove = True
elif (
item_title
and foreshadow.title
and item_title == foreshadow.title.strip()
and content_snippet
and content_snippet in item_content
):
should_remove = True
if should_remove:
removed_count += 1
continue
filtered_foreshadows.append(item)
if removed_count > 0:
analysis.foreshadows = filtered_foreshadows
analysis.foreshadows_planted = sum(
1 for f in filtered_foreshadows
if isinstance(f, dict) and f.get('type') == 'planted'
)
analysis.foreshadows_resolved = sum(
1 for f in filtered_foreshadows
if isinstance(f, dict) and f.get('type') == 'resolved'
)
cleaned_analysis_refs += removed_count
cleaned_analysis_rows += 1
logger.info(
f"🧹 已清理章节分析 {analysis.chapter_id[:8]}{removed_count} 条已删除伏笔历史记录 "
f"(原数量: {original_count}, 现数量: {len(filtered_foreshadows)})"
)
await db.delete(foreshadow)
await db.commit()
logger.info(f"✅ 删除伏笔成功: {foreshadow.title}")
logger.info(
f"✅ 删除伏笔成功: {foreshadow.title} "
f"(关系记忆清理: {deleted_memory_rows}条, 向量记忆清理: {deleted_vector_memories}条, "
f"历史分析引用清理: {cleaned_analysis_refs}条/{cleaned_analysis_rows}个分析)"
)
return True
except Exception as e:
await db.rollback()
logger.error(f"❌ 删除伏笔失败: {str(e)}")
@@ -1173,16 +1293,22 @@ class ForeshadowService:
matched_by_content = False
# 策略1: 优先使用 reference_id 精确匹配
# 重要:如果分析结果里已经给出了 reference_foreshadow_id
# 但该伏笔已被用户删除或已不属于当前项目,则直接跳过,
# 不再退回内容匹配,避免把“已删除伏笔”的旧分析结果错误同步到其他同名/近似伏笔上。
if reference_id:
existing = await self.get_foreshadow(db, reference_id)
if existing and existing.project_id == project_id:
logger.info(f"🎯 通过ID精确匹配伏笔: {existing.title}")
else:
existing = None
logger.warning(f"⚠️ 伏笔ID不存在或不属于该项目: {reference_id}")
logger.warning(f"⚠️ 伏笔ID不存在或不属于该项目,跳过本次回收同步: {reference_id}")
stats["skipped_resolve_count"] = stats.get("skipped_resolve_count", 0) + 1
stats["errors"].append(f"reference_foreshadow_id 无效或已删除: {reference_id}")
continue
# 策略2: 内容匹配备用机制(当没有reference_id或ID匹配失败时
if not existing and planted_foreshadows:
# 策略2: 内容匹配备用机制(仅在 analysis 未提供 reference_id 时启用
if not reference_id and not existing and planted_foreshadows:
matched = self._match_foreshadow_by_content(
fs_data, planted_foreshadows
)
+53
View File
@@ -723,6 +723,59 @@ class MemoryService:
return "\n".join(lines) + "\n"
async def delete_foreshadow_memories(
self,
user_id: str,
project_id: str,
foreshadow_keywords: List[str]
) -> int:
"""
根据伏笔关键词删除向量库中的相关伏笔记忆
说明:当前记忆系统未持久化 [reference_foreshadow_id](backend/app/services/prompt_service.py:1109) /
[foreshadow_id](backend/app/services/foreshadow_service.py:230) 映射,因此这里采用内容关键词匹配作为清理策略,
仅删除 [memory_type='foreshadow'](backend/app/models/memory.py:23) 的向量记忆。
Args:
user_id: 用户ID
project_id: 项目ID
foreshadow_keywords: 伏笔关键词列表
Returns:
实际删除数量
"""
try:
keywords = [kw.strip() for kw in foreshadow_keywords if kw and kw.strip()]
if not keywords:
return 0
collection = self.get_collection(user_id, project_id)
results = collection.get(where={"memory_type": "foreshadow"})
ids_to_delete = []
documents = results.get('documents') or []
metadatas = results.get('metadatas') or []
result_ids = results.get('ids') or []
for index, memory_id in enumerate(result_ids):
document = documents[index] if index < len(documents) else ""
metadata = metadatas[index] if index < len(metadatas) else {}
title = str((metadata or {}).get('title', ''))
haystack = f"{title}\n{document}".lower()
if any(keyword.lower() in haystack for keyword in keywords):
ids_to_delete.append(memory_id)
if ids_to_delete:
collection.delete(ids=ids_to_delete)
logger.info(f"🗑️ 已删除项目{project_id[:8]}{len(ids_to_delete)}条伏笔相关向量记忆")
return len(ids_to_delete)
except Exception as e:
logger.error(f"❌ 删除伏笔相关向量记忆失败: {str(e)}")
return 0
async def delete_chapter_memories(
self,
user_id: str,