refactor:重构伏笔回收逻辑,支持章节重新分析时回退已回收伏笔状态

This commit is contained in:
xiamuceer-j
2026-02-12 12:39:51 +08:00
parent e3b2a2bee4
commit fe8a0168e4
2 changed files with 175 additions and 255 deletions
+3 -2
View File
@@ -174,8 +174,9 @@ class SyncFromAnalysisResponse(BaseModel):
"""从分析同步伏笔响应""" """从分析同步伏笔响应"""
synced_count: int synced_count: int
skipped_count: int skipped_count: int
new_foreshadows: List[ForeshadowResponse] resolved_count: int = 0
skipped_reasons: List[dict] new_foreshadows: List[ForeshadowResponse] = []
skipped_reasons: List[dict] = []
class ForeshadowContextRequest(BaseModel): class ForeshadowContextRequest(BaseModel):
+172 -253
View File
@@ -4,12 +4,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, or_, desc, func, delete, update from sqlalchemy import select, and_, or_, desc, func, delete, update
from datetime import datetime from datetime import datetime
import uuid import uuid
import hashlib
from app.models.foreshadow import Foreshadow from app.models.foreshadow import Foreshadow
from app.models.chapter import Chapter from app.models.chapter import Chapter
from app.models.memory import PlotAnalysis, StoryMemory from app.models.memory import PlotAnalysis, StoryMemory
from app.schemas.foreshadow import ( from app.schemas.foreshadow import (
ForeshadowCreate, ForeshadowUpdate, ForeshadowCreate, ForeshadowUpdate,
PlantForeshadowRequest, ResolveForeshadowRequest, PlantForeshadowRequest, ResolveForeshadowRequest,
SyncFromAnalysisRequest SyncFromAnalysisRequest
) )
@@ -18,6 +19,33 @@ from app.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
def generate_stable_foreshadow_id(chapter_id: str, content: str, foreshadow_type: str = "planted") -> str:
"""
生成稳定的伏笔唯一标识符
使用 chapter_id + content_hash 的方式,确保:
1. 同一章节、相同内容的伏笔只有一个唯一ID
2. 重新分析同一章节不会产生新ID
3. 标识符足够短且可读
Args:
chapter_id: 章节ID
content: 伏笔内容
foreshadow_type: 伏笔类型(planted/resolved
Returns:
稳定的唯一标识符,格式:{type}_{chapter_id_hash}_{content_hash}
"""
# 生成内容哈希(取前12位,足够区分)
content_normalized = content.strip().lower()
content_hash = hashlib.md5(content_normalized.encode('utf-8')).hexdigest()[:12]
# 生成章节ID哈希(取前8位)
chapter_hash = hashlib.md5(chapter_id.encode('utf-8')).hexdigest()[:8]
return f"{foreshadow_type}_{chapter_hash}_{content_hash}"
class ForeshadowService: class ForeshadowService:
"""伏笔管理服务""" """伏笔管理服务"""
@@ -341,7 +369,10 @@ class ForeshadowService:
data: SyncFromAnalysisRequest data: SyncFromAnalysisRequest
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
从章节分析结果同步伏笔 从章节分析结果同步伏笔(重构版)
统一复用 auto_update_from_analysis 的核心逻辑,避免重复代码。
本方法仅负责从 PlotAnalysis 表读取数据,然后委托处理。
Args: Args:
db: 数据库会话 db: 数据库会话
@@ -352,10 +383,13 @@ class ForeshadowService:
同步结果 同步结果
""" """
try: try:
synced_count = 0 total_stats = {
skipped_count = 0 "synced_count": 0,
new_foreshadows = [] "skipped_count": 0,
skipped_reasons = [] "resolved_count": 0,
"new_foreshadows": [],
"skipped_reasons": []
}
# 获取分析结果 # 获取分析结果
query = select(PlotAnalysis).where(PlotAnalysis.project_id == project_id) query = select(PlotAnalysis).where(PlotAnalysis.project_id == project_id)
@@ -377,125 +411,23 @@ class ForeshadowService:
if not chapter: if not chapter:
continue continue
for idx, fs_data in enumerate(analysis.foreshadows): # 委托给统一的处理方法
# 生成唯一标识符 chapter_stats = await self.auto_update_from_analysis(
source_memory_id = f"analysis_{analysis.id}_{idx}" db=db,
project_id=project_id,
# 检查是否已存在 chapter_id=chapter.id,
existing = await db.execute( chapter_number=chapter.chapter_number,
select(Foreshadow).where( analysis_foreshadows=analysis.foreshadows
Foreshadow.source_memory_id == source_memory_id )
)
) # 汇总统计
existing_foreshadow = existing.scalar_one_or_none() total_stats["synced_count"] += chapter_stats.get("planted_count", 0) + chapter_stats.get("resolved_count", 0)
total_stats["resolved_count"] += chapter_stats.get("resolved_count", 0)
if existing_foreshadow and not data.overwrite_existing: total_stats["skipped_count"] += chapter_stats.get("skipped_resolve_count", 0)
skipped_count += 1
skipped_reasons.append({
"source_memory_id": source_memory_id,
"reason": "已存在同步记录"
})
continue
# 创建或更新伏笔
fs_content = fs_data.get("content", "")
fs_type = fs_data.get("type", "planted")
fs_strength = fs_data.get("strength", 5)
fs_subtlety = fs_data.get("subtlety", 5)
# 新增字段解析
fs_title = fs_data.get("title", "")
if not fs_title:
# 回退:从content截取标题
fs_title = fs_content[:50] + ("..." if len(fs_content) > 50 else "")
fs_category = fs_data.get("category")
fs_is_long_term = fs_data.get("is_long_term", False)
fs_related_characters = fs_data.get("related_characters", [])
fs_estimated_resolve = fs_data.get("estimated_resolve_chapter")
fs_keyword = fs_data.get("keyword", "")
# 🔧 修复Bug#7:如果AI没有填写estimated_resolve_chapter,提供合理的默认值
if fs_estimated_resolve is None and fs_type == "planted":
# 根据伏笔类型和长线属性计算默认回收章节
if fs_is_long_term:
# 长线伏笔:当前章节 + 15章
fs_estimated_resolve = chapter.chapter_number + 15
else:
# 短线伏笔:当前章节 + 5章
fs_estimated_resolve = chapter.chapter_number + 5
logger.info(f"⚠️ AI未填写estimated_resolve_chapter,使用默认值: 第{fs_estimated_resolve}")
# 确定状态
status = "planted" if (fs_type == "planted" and data.auto_set_planted) else "pending"
if fs_type == "resolved":
status = "resolved"
if existing_foreshadow:
# 更新现有记录
existing_foreshadow.title = fs_title
existing_foreshadow.content = fs_content
existing_foreshadow.strength = fs_strength
existing_foreshadow.subtlety = fs_subtlety
existing_foreshadow.status = status
existing_foreshadow.category = fs_category
existing_foreshadow.is_long_term = fs_is_long_term
existing_foreshadow.related_characters = fs_related_characters if fs_related_characters else None
existing_foreshadow.hint_text = fs_keyword if fs_keyword else None
if fs_estimated_resolve:
existing_foreshadow.target_resolve_chapter_number = fs_estimated_resolve
await db.flush()
new_foreshadows.append(existing_foreshadow.to_dict())
else:
# 创建新记录
foreshadow = Foreshadow(
id=str(uuid.uuid4()),
project_id=project_id,
title=fs_title,
content=fs_content,
hint_text=fs_keyword if fs_keyword else None,
source_type="analysis",
source_memory_id=source_memory_id,
source_analysis_id=analysis.id,
plant_chapter_id=chapter.id if status == "planted" else None,
plant_chapter_number=chapter.chapter_number if status == "planted" else None,
planted_at=datetime.now() if status == "planted" else None,
target_resolve_chapter_number=fs_estimated_resolve if fs_estimated_resolve else None,
status=status,
is_long_term=fs_is_long_term,
importance=min(fs_strength / 10.0, 1.0),
strength=fs_strength,
subtlety=fs_subtlety,
category=fs_category,
related_characters=fs_related_characters if fs_related_characters else None,
auto_remind=True,
remind_before_chapters=5,
include_in_context=True
)
# 如果是回收的伏笔
if fs_type == "resolved":
foreshadow.actual_resolve_chapter_id = chapter.id
foreshadow.actual_resolve_chapter_number = chapter.chapter_number
foreshadow.resolved_at = datetime.now()
if fs_data.get("reference_chapter"):
foreshadow.plant_chapter_number = fs_data.get("reference_chapter")
db.add(foreshadow)
await db.flush()
new_foreshadows.append(foreshadow.to_dict())
synced_count += 1
await db.commit() logger.info(f"✅ 伏笔同步完成: 同步{total_stats['synced_count']}个(其中回收{total_stats['resolved_count']}个), 跳过{total_stats['skipped_count']}")
logger.info(f"✅ 伏笔同步完成: 同步{synced_count}个, 跳过{skipped_count}") return total_stats
return {
"synced_count": synced_count,
"skipped_count": skipped_count,
"new_foreshadows": new_foreshadows,
"skipped_reasons": skipped_reasons
}
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
@@ -891,6 +823,7 @@ class ForeshadowService:
"id": f.id, "id": f.id,
"title": f.title, "title": f.title,
"content": f.content, "content": f.content,
"hint_text": f.hint_text,
"plant_chapter_number": f.plant_chapter_number, "plant_chapter_number": f.plant_chapter_number,
"target_resolve_chapter_number": f.target_resolve_chapter_number, "target_resolve_chapter_number": f.target_resolve_chapter_number,
"category": f.category, "category": f.category,
@@ -953,15 +886,13 @@ class ForeshadowService:
logger.debug(f"🔍 找到章节 {chapter_id[:8]} 的分析ID: {len(analysis_ids)}") logger.debug(f"🔍 找到章节 {chapter_id[:8]} 的分析ID: {len(analysis_ids)}")
# 2. 构建查询条件:查找与该章节相关的伏笔 # 2. 构建查询条件:查找与该章节相关的伏笔
# 相关性包括 # 匹配方式
# 1. 埋入章节是该章节 (plant_chapter_id) # 1. 埋入章节是该章节 (plant_chapter_id)
# 2. 回收章节是该章节 (actual_resolve_chapter_id) # 2. 回收章节是该章节 (actual_resolve_chapter_id)
# 3. 来源分析ID对应该章节的分析 (source_analysis_id) # 3. 来源分析ID对应该章节的分析 (source_analysis_id)
# 4. source_memory_id 包含章节ID (auto_update_from_analysis 创建的)
or_conditions = [ or_conditions = [
Foreshadow.plant_chapter_id == chapter_id, Foreshadow.plant_chapter_id == chapter_id,
Foreshadow.actual_resolve_chapter_id == chapter_id, Foreshadow.actual_resolve_chapter_id == chapter_id,
Foreshadow.source_memory_id.like(f"auto_analysis_{chapter_id}%")
] ]
# 如果找到了分析ID,添加 source_analysis_id 匹配条件 # 如果找到了分析ID,添加 source_analysis_id 匹配条件
@@ -1019,7 +950,10 @@ class ForeshadowService:
""" """
清理章节分析产生的伏笔(用于重新分析前的清理) 清理章节分析产生的伏笔(用于重新分析前的清理)
只清理 source_type='analysis' 且 source_memory_id 包含该章节ID 的伏笔 两步操作:
1. 删除 source_type='analysis' 且 plant_chapter_id == chapter_id 的伏笔
2. 回退在本章被回收的伏笔(将其从 resolved 恢复为 planted
保留手动创建的伏笔 保留手动创建的伏笔
Args: Args:
@@ -1031,18 +965,12 @@ class ForeshadowService:
清理统计信息 清理统计信息
""" """
try: try:
# 步骤1: 删除在本章埋入的分析伏笔
query = select(Foreshadow).where( query = select(Foreshadow).where(
and_( and_(
Foreshadow.project_id == project_id, Foreshadow.project_id == project_id,
Foreshadow.source_type == "analysis", Foreshadow.source_type == "analysis",
or_( Foreshadow.plant_chapter_id == chapter_id
Foreshadow.source_memory_id.like(f"analysis_%_{chapter_id}%"),
Foreshadow.source_memory_id.like(f"auto_analysis_{chapter_id}%"),
and_(
Foreshadow.plant_chapter_id == chapter_id,
Foreshadow.status.in_(["pending", "planted"])
)
)
) )
) )
@@ -1052,24 +980,77 @@ class ForeshadowService:
cleaned_count = len(foreshadows_to_clean) cleaned_count = len(foreshadows_to_clean)
cleaned_ids = [f.id for f in foreshadows_to_clean] cleaned_ids = [f.id for f in foreshadows_to_clean]
# 执行删除
for foreshadow in foreshadows_to_clean: for foreshadow in foreshadows_to_clean:
await db.delete(foreshadow) await db.delete(foreshadow)
# 步骤2: 回退在本章被回收的伏笔(恢复为 planted 状态)
reverted_count = await self._revert_chapter_resolutions(db, project_id, chapter_id)
await db.commit() await db.commit()
if cleaned_count > 0: if cleaned_count > 0 or reverted_count > 0:
logger.info(f"🧹 已清理章节 {chapter_id[:8]}{cleaned_count} 个分析伏笔(准备重新分析)") logger.info(f"🧹 已清理章节 {chapter_id[:8]}: 删除{cleaned_count}个分析伏笔, 回退{reverted_count}个回收状态")
return { return {
"cleaned_count": cleaned_count, "cleaned_count": cleaned_count,
"cleaned_ids": cleaned_ids "cleaned_ids": cleaned_ids,
"reverted_count": reverted_count
} }
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
logger.error(f"❌ 清理章节分析伏笔失败: {str(e)}") logger.error(f"❌ 清理章节分析伏笔失败: {str(e)}")
raise raise
async def _revert_chapter_resolutions(
self,
db: AsyncSession,
project_id: str,
chapter_id: str
) -> int:
"""
回退在指定章节中被回收的伏笔
将 actual_resolve_chapter_id == chapter_id 且 status 为 resolved/partially_resolved 的伏笔
恢复为 planted 状态,以便重新分析时可以重新匹配回收
Args:
db: 数据库会话
project_id: 项目ID
chapter_id: 章节ID
Returns:
回退的伏笔数量
"""
try:
update_query = (
update(Foreshadow)
.where(
and_(
Foreshadow.project_id == project_id,
Foreshadow.actual_resolve_chapter_id == chapter_id,
Foreshadow.status.in_(["resolved", "partially_resolved"])
)
)
.values(
status="planted",
actual_resolve_chapter_id=None,
actual_resolve_chapter_number=None,
resolved_at=None,
resolution_text=None
)
)
result = await db.execute(update_query)
reverted_count = result.rowcount
if reverted_count > 0:
logger.info(f"↩️ 回退了 {reverted_count} 个在章节 {chapter_id[:8]} 中被回收的伏笔")
return reverted_count
except Exception as e:
logger.error(f"❌ 回退章节回收失败: {str(e)}")
return 0
async def clear_project_foreshadows_for_reset( async def clear_project_foreshadows_for_reset(
self, self,
@@ -1202,14 +1183,14 @@ class ForeshadowService:
# 策略2: 内容匹配备用机制(当没有reference_id或ID匹配失败时) # 策略2: 内容匹配备用机制(当没有reference_id或ID匹配失败时)
if not existing and planted_foreshadows: if not existing and planted_foreshadows:
existing = await self._match_foreshadow_by_content( matched = self._match_foreshadow_by_content(
fs_data, planted_foreshadows fs_data, planted_foreshadows
) )
if existing: if matched:
matched_by_content = True matched_by_content = True
logger.info(f"🔍 通过内容匹配找到伏笔: {existing.get('title')}") logger.info(f"🔍 通过内容匹配找到伏笔: {matched.get('title')}")
# 重新获取完整的伏笔对象 # 重新获取完整的伏笔对象
existing = await self.get_foreshadow(db, existing.get('id')) existing = await self.get_foreshadow(db, matched.get('id'))
# 检查伏笔是否已被回收(防止重复回收) # 检查伏笔是否已被回收(防止重复回收)
if existing: if existing:
@@ -1246,93 +1227,44 @@ class ForeshadowService:
elif existing: elif existing:
logger.warning(f"⚠️ 伏笔状态不是planted,跳过回收: {existing.title} (status: {existing.status})") logger.warning(f"⚠️ 伏笔状态不是planted,跳过回收: {existing.title} (status: {existing.status})")
else: else:
# 创建新回收记录(未能匹配已埋入伏笔) # 找不到匹配已埋入伏笔,跳过(不创建新记录!
# 核心原则:只有"埋入"操作会创建伏笔记录,"回收"只是更新已有记录
# 如果没有埋入的伏笔,就不可能存在回收
fs_title = fs_data.get("title", fs_data.get("content", "")[:30]) fs_title = fs_data.get("title", fs_data.get("content", "")[:30])
reference_chapter = fs_data.get("reference_chapter") logger.warning(f"⚠️ 未找到匹配的已埋入伏笔,跳过回收(不创建新记录): {fs_title}")
logger.warning(f" 提示:AI可能误识别了回收伏笔,或者 reference_foreshadow_id 未正确填写")
# 检查是否已存在相同的回收记录(防止重复创建) stats["skipped_resolve_count"] = stats.get("skipped_resolve_count", 0) + 1
duplicate_check = await db.execute( continue
select(Foreshadow).where(
and_(
Foreshadow.project_id == project_id,
Foreshadow.title == fs_title,
Foreshadow.actual_resolve_chapter_number == chapter_number,
Foreshadow.source_type == "analysis",
Foreshadow.status == "resolved"
)
)
)
duplicate_fs = duplicate_check.scalar_one_or_none()
if duplicate_fs:
logger.info(f"️ 已存在相同的回收记录,跳过: {fs_title}")
continue
logger.warning(f"⚠️ 未能匹配到已埋入伏笔,创建新的回收记录: {fs_title}")
new_resolved_foreshadow = Foreshadow(
id=str(uuid.uuid4()),
project_id=project_id,
title=fs_title,
content=fs_data.get("content", ""),
resolution_text=fs_data.get("content", ""),
source_type="analysis",
source_memory_id=f"auto_analysis_{chapter_id}_{fs_title[:30]}",
plant_chapter_number=reference_chapter if reference_chapter else None,
actual_resolve_chapter_id=chapter_id,
actual_resolve_chapter_number=chapter_number,
resolved_at=datetime.now(),
status="resolved",
is_long_term=fs_data.get("is_long_term", False),
importance=min(fs_data.get("strength", 5) / 10.0, 1.0),
strength=fs_data.get("strength", 5),
subtlety=fs_data.get("subtlety", 5),
category=fs_data.get("category"),
related_characters=fs_data.get("related_characters"),
auto_remind=False,
include_in_context=True
)
db.add(new_resolved_foreshadow)
await db.flush()
stats["resolved_count"] += 1
stats["created_count"] += 1
stats["created_ids"].append(new_resolved_foreshadow.id)
logger.info(f"✅ 创建新的回收伏笔记录: {fs_title} (ID: {new_resolved_foreshadow.id})")
elif fs_type == "planted": elif fs_type == "planted":
fs_title = fs_data.get("title", "") fs_content = fs_data.get("content", "")
if not fs_title: if not fs_content:
fs_title = fs_data.get("content", "")[:50] + "..." logger.warning(f"⚠️ 伏笔内容为空,跳过")
analysis_query = select(PlotAnalysis.id).where(
PlotAnalysis.chapter_id == chapter_id
).order_by(PlotAnalysis.created_at.desc()).limit(1)
analysis_result = await db.execute(analysis_query)
analysis_id = analysis_result.scalar_one_or_none()
if not analysis_id:
logger.warning(f"⚠️ 未找到章节 {chapter_id} 的分析记录,跳过伏笔创建")
continue continue
fs_index = analysis_foreshadows.index(fs_data) fs_title = fs_data.get("title", "")
source_memory_id = f"analysis_{analysis_id}_{fs_index}" if not fs_title:
fs_title = fs_content[:50] + ("..." if len(fs_content) > 50 else "")
# 检查是否已存在(防止重复分析创建重复记录 # 使用稳定的唯一标识符(基于 chapter_id + content_hash
source_memory_id = generate_stable_foreshadow_id(
chapter_id, fs_content, fs_type
)
# 检查是否已存在(使用稳定ID去重,防止重复分析创建重复记录)
existing_check = await db.execute( existing_check = await db.execute(
select(Foreshadow).where( select(Foreshadow).where(
or_( and_(
# 方式1:通过source_memory_id精确匹配 Foreshadow.project_id == project_id,
and_( or_(
Foreshadow.project_id == project_id, # 方式1:通过稳定source_memory_id精确匹配
Foreshadow.source_memory_id == source_memory_id Foreshadow.source_memory_id == source_memory_id,
), # 方式2:通过标题+章节号匹配(兼容旧数据)
# 方式2:通过标题+章节号匹配 and_(
and_( Foreshadow.title == fs_title,
Foreshadow.project_id == project_id, Foreshadow.plant_chapter_id == chapter_id,
Foreshadow.title == fs_title, Foreshadow.source_type == "analysis"
Foreshadow.plant_chapter_number == chapter_number, )
Foreshadow.source_type == "analysis",
Foreshadow.status == "planted"
) )
) )
) )
@@ -1342,36 +1274,35 @@ class ForeshadowService:
if existing_fs: if existing_fs:
# 更新已存在的伏笔,避免重复创建 # 更新已存在的伏笔,避免重复创建
existing_fs.title = fs_title existing_fs.title = fs_title
existing_fs.content = fs_data.get("content", existing_fs.content) existing_fs.content = fs_content
existing_fs.strength = fs_data.get("strength", existing_fs.strength) existing_fs.strength = fs_data.get("strength", existing_fs.strength)
existing_fs.subtlety = fs_data.get("subtlety", existing_fs.subtlety) existing_fs.subtlety = fs_data.get("subtlety", existing_fs.subtlety)
existing_fs.hint_text = fs_data.get("keyword", existing_fs.hint_text) existing_fs.hint_text = fs_data.get("keyword", existing_fs.hint_text)
existing_fs.target_resolve_chapter_number = fs_data.get("estimated_resolve_chapter", existing_fs.target_resolve_chapter_number) existing_fs.category = fs_data.get("category", existing_fs.category)
# 确保source_memory_id是最新的 existing_fs.is_long_term = fs_data.get("is_long_term", existing_fs.is_long_term)
existing_fs.related_characters = fs_data.get("related_characters", existing_fs.related_characters)
if fs_data.get("estimated_resolve_chapter"):
existing_fs.target_resolve_chapter_number = fs_data.get("estimated_resolve_chapter")
# 更新为稳定的source_memory_id
existing_fs.source_memory_id = source_memory_id existing_fs.source_memory_id = source_memory_id
existing_fs.source_analysis_id = analysis_id
await db.flush() await db.flush()
stats["updated_ids"].append(existing_fs.id)
logger.info(f"📝 更新已存在伏笔(避免重复): {fs_title} (ID: {existing_fs.id})") logger.info(f"📝 更新已存在伏笔(避免重复): {fs_title} (ID: {existing_fs.id})")
else: else:
# 创建新伏笔 # 创建新伏笔
# 不再为 estimated_resolve_chapter 设置默认值,避免误报"超期"
estimated_resolve = fs_data.get("estimated_resolve_chapter") estimated_resolve = fs_data.get("estimated_resolve_chapter")
if estimated_resolve is None: if estimated_resolve is None:
# 根据伏笔类型计算默认回收章节 logger.info(f"️ AI未填写estimated_resolve_chapter,不设默认值,标记为无明确回收计划")
if fs_data.get("is_long_term", False):
estimated_resolve = chapter_number + 15
else:
estimated_resolve = chapter_number + 5
logger.info(f"⚠️ AI未填写estimated_resolve_chapter,使用默认值: 第{estimated_resolve}")
new_foreshadow = Foreshadow( new_foreshadow = Foreshadow(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
project_id=project_id, project_id=project_id,
title=fs_title, title=fs_title,
content=fs_data.get("content", ""), content=fs_content,
hint_text=fs_data.get("keyword"), hint_text=fs_data.get("keyword"),
source_type="analysis", source_type="analysis",
source_memory_id=source_memory_id, # 使用统一格式 source_memory_id=source_memory_id, # 使用稳定的唯一标识
source_analysis_id=analysis_id, # 关联分析ID
plant_chapter_id=chapter_id, plant_chapter_id=chapter_id,
plant_chapter_number=chapter_number, plant_chapter_number=chapter_number,
planted_at=datetime.now(), planted_at=datetime.now(),
@@ -1450,22 +1381,10 @@ class ForeshadowService:
stats["checked_count"] = len(pending_foreshadows) stats["checked_count"] = len(pending_foreshadows)
for fs in pending_foreshadows: for fs in pending_foreshadows:
# 简单检查:如果伏笔标题或内容的关键词出现在章节中 # 用户明确指定了本章埋入的伏笔,自动标记为已埋入
# 或者用户已明确指定本章埋入,则自动标记 # 注:只有 pending 状态且 plant_chapter_number == chapter_number 的伏笔
should_plant = False # 才会被 get_foreshadows_to_plant 查出,所以这里直接标记即可
should_plant = True
# 检查标题关键词
if fs.title and len(fs.title) >= 4:
# 提取标题中的关键词(取前4-10个字符)
keywords = [fs.title[:min(10, len(fs.title))]]
for kw in keywords:
if kw in chapter_content:
should_plant = True
break
# 如果明确指定了本章埋入,直接标记
if fs.plant_chapter_number == chapter_number:
should_plant = True
if should_plant: if should_plant:
fs.status = "planted" fs.status = "planted"
@@ -1490,11 +1409,11 @@ class ForeshadowService:
return {"checked_count": 0, "planted_count": 0, "planted_ids": [], "error": str(e)} return {"checked_count": 0, "planted_count": 0, "planted_ids": [], "error": str(e)}
async def _match_foreshadow_by_content( def _match_foreshadow_by_content(
self, self,
resolved_fs_data: Dict[str, Any], resolved_fs_data: Dict[str, Any],
planted_foreshadows: List[Dict[str, Any]], planted_foreshadows: List[Dict[str, Any]],
min_similarity: float = 0.3 min_similarity: float = 0.5
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
""" """
通过内容相似度匹配伏笔(备用机制) 通过内容相似度匹配伏笔(备用机制)