feature: 新增伏笔管理系统,支持可视化追踪、AI智能关联回收及章节生成时的伏笔提醒
This commit is contained in:
+214
-55
@@ -43,6 +43,7 @@ from app.services.ai_service import AIService
|
||||
from app.services.prompt_service import prompt_service, PromptService, WritingStyleManager
|
||||
from app.services.plot_analyzer import PlotAnalyzer
|
||||
from app.services.memory_service import memory_service
|
||||
from app.services.foreshadow_service import foreshadow_service
|
||||
from app.services.chapter_regenerator import ChapterRegenerator
|
||||
from app.logger import get_logger
|
||||
from app.api.settings import get_user_ai_service
|
||||
@@ -284,45 +285,58 @@ async def update_chapter(
|
||||
project.current_words = project.current_words - old_word_count + new_word_count
|
||||
|
||||
# 如果内容被清空,清理相关数据
|
||||
if not chapter.content or chapter.content.strip() == "":
|
||||
chapter.status = "draft"
|
||||
|
||||
# 清理分析任务
|
||||
analysis_tasks_result = await db.execute(
|
||||
select(AnalysisTask).where(AnalysisTask.chapter_id == chapter_id)
|
||||
)
|
||||
analysis_tasks = analysis_tasks_result.scalars().all()
|
||||
for task in analysis_tasks:
|
||||
await db.delete(task)
|
||||
|
||||
# 清理分析结果
|
||||
plot_analysis_result = await db.execute(
|
||||
select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id)
|
||||
)
|
||||
plot_analyses = plot_analysis_result.scalars().all()
|
||||
for analysis in plot_analyses:
|
||||
await db.delete(analysis)
|
||||
|
||||
# 清理故事记忆(关系数据库)
|
||||
story_memories_result = await db.execute(
|
||||
select(StoryMemory).where(StoryMemory.chapter_id == chapter_id)
|
||||
)
|
||||
story_memories = story_memories_result.scalars().all()
|
||||
for memory in story_memories:
|
||||
await db.delete(memory)
|
||||
|
||||
# 清理向量数据库中的记忆数据
|
||||
try:
|
||||
await memory_service.delete_chapter_memories(
|
||||
user_id=user_id,
|
||||
project_id=chapter.project_id,
|
||||
chapter_id=chapter_id
|
||||
if not chapter.content or chapter.content.strip() == "":
|
||||
chapter.status = "draft"
|
||||
|
||||
# 清理分析任务
|
||||
analysis_tasks_result = await db.execute(
|
||||
select(AnalysisTask).where(AnalysisTask.chapter_id == chapter_id)
|
||||
)
|
||||
logger.info(f"✅ 已清理章节 {chapter_id[:8]} 的向量记忆数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理向量记忆数据失败: {str(e)}")
|
||||
|
||||
logger.info(f"🗑️ 章节 {chapter_id[:8]} 内容已清空,已清理分析和记忆数据")
|
||||
analysis_tasks = analysis_tasks_result.scalars().all()
|
||||
for task in analysis_tasks:
|
||||
await db.delete(task)
|
||||
|
||||
# 清理分析结果
|
||||
plot_analysis_result = await db.execute(
|
||||
select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id)
|
||||
)
|
||||
plot_analyses = plot_analysis_result.scalars().all()
|
||||
for analysis in plot_analyses:
|
||||
await db.delete(analysis)
|
||||
|
||||
# 清理故事记忆(关系数据库)
|
||||
story_memories_result = await db.execute(
|
||||
select(StoryMemory).where(StoryMemory.chapter_id == chapter_id)
|
||||
)
|
||||
story_memories = story_memories_result.scalars().all()
|
||||
for memory in story_memories:
|
||||
await db.delete(memory)
|
||||
|
||||
# 清理向量数据库中的记忆数据
|
||||
try:
|
||||
await memory_service.delete_chapter_memories(
|
||||
user_id=user_id,
|
||||
project_id=chapter.project_id,
|
||||
chapter_id=chapter_id
|
||||
)
|
||||
logger.info(f"✅ 已清理章节 {chapter_id[:8]} 的向量记忆数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理向量记忆数据失败: {str(e)}")
|
||||
|
||||
# 🔮 清理章节相关的分析伏笔数据
|
||||
try:
|
||||
foreshadow_result = await foreshadow_service.delete_chapter_foreshadows(
|
||||
db=db,
|
||||
project_id=chapter.project_id,
|
||||
chapter_id=chapter_id,
|
||||
only_analysis_source=True # 只删除分析来源的伏笔,保留手动创建的
|
||||
)
|
||||
if foreshadow_result['deleted_count'] > 0:
|
||||
logger.info(f"🔮 已清理章节 {chapter_id[:8]} 的 {foreshadow_result['deleted_count']} 个伏笔数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理伏笔数据失败: {str(e)}")
|
||||
|
||||
logger.info(f"🗑️ 章节 {chapter_id[:8]} 内容已清空,已清理分析、记忆和伏笔数据")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(chapter)
|
||||
@@ -397,6 +411,20 @@ async def delete_chapter(
|
||||
logger.warning(f"⚠️ 清理向量记忆数据失败: {str(e)}")
|
||||
# 不阻断删除流程,继续执行
|
||||
|
||||
# 🔮 清理与该章节相关的伏笔数据(仅分析来源的伏笔)
|
||||
try:
|
||||
foreshadow_result = await foreshadow_service.delete_chapter_foreshadows(
|
||||
db=db,
|
||||
project_id=chapter.project_id,
|
||||
chapter_id=chapter_id,
|
||||
only_analysis_source=True # 只删除分析来源的伏笔,保留手动创建的
|
||||
)
|
||||
if foreshadow_result['deleted_count'] > 0:
|
||||
logger.info(f"🔮 已清理章节 {chapter_id[:8]} 的 {foreshadow_result['deleted_count']} 个伏笔数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理伏笔数据失败: {str(e)}")
|
||||
# 不阻断删除流程,继续执行
|
||||
|
||||
# 删除章节(关系数据库中的记忆会被级联删除)
|
||||
await db.delete(chapter)
|
||||
await db.commit()
|
||||
@@ -861,13 +889,22 @@ async def analyze_chapter_background(
|
||||
task.progress = 20
|
||||
await db_session.commit()
|
||||
|
||||
# 3. 使用PlotAnalyzer分析章节
|
||||
# 获取已埋入的伏笔列表(用于回收匹配,传入当前章节号以启用智能标记)
|
||||
existing_foreshadows = await foreshadow_service.get_planted_foreshadows_for_analysis(
|
||||
db=db_session,
|
||||
project_id=project_id,
|
||||
current_chapter_number=chapter.chapter_number # 传入当前章节号以启用智能标记
|
||||
)
|
||||
logger.info(f"📋 后台分析 - 已获取{len(existing_foreshadows)}个已埋入伏笔用于匹配(含智能回收标记)")
|
||||
|
||||
# 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表)
|
||||
analyzer = PlotAnalyzer(ai_service)
|
||||
analysis_result = await analyzer.analyze_chapter(
|
||||
chapter_number=chapter.chapter_number,
|
||||
title=chapter.title,
|
||||
content=chapter.content,
|
||||
word_count=chapter.word_count or len(chapter.content)
|
||||
word_count=chapter.word_count or len(chapter.content),
|
||||
existing_foreshadows=existing_foreshadows
|
||||
)
|
||||
|
||||
if not analysis_result:
|
||||
@@ -953,7 +990,20 @@ async def analyze_chapter_background(
|
||||
task.progress = 80
|
||||
await db_session.commit()
|
||||
|
||||
# 5. 提取记忆并保存到向量数据库(传入章节内容用于计算位置)
|
||||
# 5. 清理旧的分析伏笔(重新分析时需要先清理)
|
||||
try:
|
||||
async with write_lock:
|
||||
clean_result = await foreshadow_service.clean_chapter_analysis_foreshadows(
|
||||
db=db_session,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter_id
|
||||
)
|
||||
if clean_result['cleaned_count'] > 0:
|
||||
logger.info(f"🧹 重新分析前清理了 {clean_result['cleaned_count']} 个旧伏笔")
|
||||
except Exception as clean_error:
|
||||
logger.warning(f"⚠️ 清理旧伏笔失败(继续分析): {str(clean_error)}")
|
||||
|
||||
# 6. 提取记忆并保存到向量数据库(传入章节内容用于计算位置)
|
||||
memories = analyzer.extract_memories_from_analysis(
|
||||
analysis=analysis_result,
|
||||
chapter_id=chapter_id,
|
||||
@@ -1053,6 +1103,33 @@ async def analyze_chapter_background(
|
||||
else:
|
||||
logger.debug("📋 分析结果中无角色状态信息,跳过职业更新")
|
||||
|
||||
# 🔮 自动更新伏笔状态(根据分析结果)
|
||||
if analysis_result.get('foreshadows'):
|
||||
try:
|
||||
logger.info(f"🔮 开始根据分析结果自动更新伏笔状态...")
|
||||
async with write_lock:
|
||||
foreshadow_stats = await foreshadow_service.auto_update_from_analysis(
|
||||
db=db_session,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter_id,
|
||||
chapter_number=chapter.chapter_number,
|
||||
analysis_foreshadows=analysis_result.get('foreshadows', [])
|
||||
)
|
||||
|
||||
if foreshadow_stats['planted_count'] > 0 or foreshadow_stats['resolved_count'] > 0:
|
||||
logger.info(
|
||||
f"✅ 伏笔自动更新: 埋入{foreshadow_stats['planted_count']}个, "
|
||||
f"回收{foreshadow_stats['resolved_count']}个"
|
||||
)
|
||||
else:
|
||||
logger.info("ℹ️ 本章节无新的伏笔状态变化")
|
||||
|
||||
except Exception as foreshadow_error:
|
||||
# 伏笔更新失败不应影响整个分析流程
|
||||
logger.error(f"⚠️ 自动更新伏笔失败: {str(foreshadow_error)}", exc_info=True)
|
||||
else:
|
||||
logger.debug("📋 分析结果中无伏笔信息,跳过伏笔自动更新")
|
||||
|
||||
# 最终更新任务状态(写操作,需要锁)- 增加重试机制
|
||||
update_success = False
|
||||
for retry in range(3):
|
||||
@@ -1293,9 +1370,9 @@ async def generate_chapter_content_stream(
|
||||
else:
|
||||
logger.info("未指定写作风格,使用原始提示词")
|
||||
|
||||
# 🚀 使用新的优化上下文构建器
|
||||
logger.info(f"🔧 使用优化的章节上下文构建器(V2)")
|
||||
context_builder = ChapterContextBuilder()
|
||||
# 🚀 使用新的优化上下文构建器(含伏笔服务)
|
||||
logger.info(f"🔧 使用优化的章节上下文构建器(V2 + 伏笔提醒)")
|
||||
context_builder = ChapterContextBuilder(foreshadow_service=foreshadow_service)
|
||||
chapter_context = await context_builder.build(
|
||||
chapter=current_chapter,
|
||||
project=project,
|
||||
@@ -1350,9 +1427,12 @@ async def generate_chapter_content_stream(
|
||||
if current_chapter.summary and current_chapter.summary.strip():
|
||||
chapter_outline_content += f"\n\n【章节补充说明】\n{current_chapter.summary}"
|
||||
|
||||
# 可选:附加大纲的背景信息
|
||||
# 可选:附加大纲的背景信息(限制长度,避免喧宾夺主)
|
||||
if outline:
|
||||
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline.content}"
|
||||
outline_bg = outline.content
|
||||
if len(outline_bg) > 200:
|
||||
outline_bg = outline_bg[:200] + "..."
|
||||
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline_bg}"
|
||||
|
||||
logger.info(f"✏️ 一对多模式:使用expansion_plan详细规划({len(chapter_outline_content)}字符)")
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -1366,6 +1446,18 @@ async def generate_chapter_content_stream(
|
||||
# 🚀 使用 V2 优化模板构建提示词
|
||||
if chapter_context.continuation_point:
|
||||
# 有前置内容,使用 WITH_CONTEXT 模板
|
||||
|
||||
# 尝试从context中提取上一章摘要
|
||||
previous_summary = "(无上一章摘要,请根据锚点续写)"
|
||||
if chapter_context.context_stats.get('recent_summaries', 0) > 0:
|
||||
# 简单的提取逻辑,实际可能需要更精确的解析
|
||||
# 但在这里,context_stats并没有直接存储内容。
|
||||
# 我们利用ChapterContext对象中可能存在的summary信息,或者直接从recent_summary文本中截取最后一段
|
||||
if hasattr(chapter_context, 'recent_summary') and chapter_context.recent_summary:
|
||||
lines = chapter_context.recent_summary.strip().split('\n')
|
||||
if lines:
|
||||
previous_summary = lines[-1]
|
||||
|
||||
template = await PromptService.get_template("CHAPTER_GENERATION_V2_WITH_CONTEXT", current_user_id, db_session)
|
||||
base_prompt = PromptService.format_prompt(
|
||||
template,
|
||||
@@ -1380,6 +1472,8 @@ async def generate_chapter_content_stream(
|
||||
genre=project.genre or '未设定',
|
||||
narrative_perspective=chapter_perspective,
|
||||
characters_info=characters_info or '暂无角色信息',
|
||||
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
|
||||
previous_chapter_summary=previous_summary,
|
||||
# P2 参考参数(动态裁剪后的)
|
||||
story_skeleton=chapter_context.story_skeleton or '',
|
||||
relevant_memories=chapter_context.relevant_memories or ''
|
||||
@@ -1494,6 +1588,20 @@ async def generate_chapter_content_stream(
|
||||
|
||||
logger.info(f"成功创作章节 {chapter_id},共 {new_word_count} 字")
|
||||
|
||||
# 🔮 章节生成后自动标记计划在本章埋入的伏笔
|
||||
try:
|
||||
plant_result = await foreshadow_service.auto_plant_pending_foreshadows(
|
||||
db=db_session,
|
||||
project_id=project.id,
|
||||
chapter_id=chapter_id,
|
||||
chapter_number=current_chapter.chapter_number,
|
||||
chapter_content=full_content
|
||||
)
|
||||
if plant_result.get('planted_count', 0) > 0:
|
||||
logger.info(f"🔮 自动标记伏笔已埋入: {plant_result['planted_count']}个")
|
||||
except Exception as plant_error:
|
||||
logger.warning(f"⚠️ 自动标记伏笔埋入失败: {str(plant_error)}")
|
||||
|
||||
# 创建分析任务
|
||||
analysis_task = AnalysisTask(
|
||||
chapter_id=chapter_id,
|
||||
@@ -2266,6 +2374,9 @@ async def execute_batch_generation_in_order(
|
||||
task.started_at = datetime.now()
|
||||
await db_session.commit()
|
||||
|
||||
# 维护上一章的摘要,用于传递给下一章(防重复上下文)
|
||||
last_generated_summary = None
|
||||
|
||||
# 按顺序生成每个章节
|
||||
for idx, chapter_id in enumerate(task.chapter_ids, 1):
|
||||
# 检查任务是否被取消
|
||||
@@ -2314,7 +2425,8 @@ async def execute_batch_generation_in_order(
|
||||
raise Exception(f"前置条件不满足: {error_msg}")
|
||||
|
||||
# 生成章节内容(复用现有流式生成逻辑的核心部分),传递model参数
|
||||
await generate_single_chapter_for_batch(
|
||||
# 并获取生成后的摘要(如果生成函数支持返回)
|
||||
generated_summary = await generate_single_chapter_for_batch(
|
||||
db_session=db_session,
|
||||
chapter=chapter,
|
||||
user_id=user_id,
|
||||
@@ -2322,9 +2434,15 @@ async def execute_batch_generation_in_order(
|
||||
target_word_count=task.target_word_count,
|
||||
ai_service=ai_service,
|
||||
write_lock=write_lock,
|
||||
custom_model=custom_model
|
||||
custom_model=custom_model,
|
||||
previous_summary_context=last_generated_summary
|
||||
)
|
||||
|
||||
# 更新上一章摘要,供下一章使用
|
||||
if generated_summary:
|
||||
last_generated_summary = f"第{chapter.chapter_number}章《{chapter.title}》:{generated_summary}"
|
||||
logger.info(f"📝 已更新上一章摘要上下文: {last_generated_summary[:50]}...")
|
||||
|
||||
logger.info(f"✅ 章节生成完成: 第{chapter.chapter_number}章")
|
||||
|
||||
# 如果启用同步分析
|
||||
@@ -2499,11 +2617,15 @@ async def generate_single_chapter_for_batch(
|
||||
target_word_count: int,
|
||||
ai_service: AIService,
|
||||
write_lock: Lock,
|
||||
custom_model: Optional[str] = None
|
||||
):
|
||||
custom_model: Optional[str] = None,
|
||||
previous_summary_context: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
为批量生成执行单个章节的生成(非流式)
|
||||
复用现有生成逻辑的核心部分
|
||||
|
||||
Returns:
|
||||
生成章节的摘要(前200字)
|
||||
"""
|
||||
# 获取项目信息
|
||||
project_result = await db_session.execute(
|
||||
@@ -2584,9 +2706,9 @@ async def generate_single_chapter_for_batch(
|
||||
if style.user_id is None or style.user_id == user_id:
|
||||
style_content = style.prompt_content or ""
|
||||
|
||||
# 🚀 使用新的优化上下文构建器
|
||||
logger.info(f"🔧 批量生成 - 使用优化的章节上下文构建器(V2)")
|
||||
context_builder = ChapterContextBuilder()
|
||||
# 🚀 使用新的优化上下文构建器(含伏笔服务)
|
||||
logger.info(f"🔧 批量生成 - 使用优化的章节上下文构建器(V2 + 伏笔提醒)")
|
||||
context_builder = ChapterContextBuilder(foreshadow_service=foreshadow_service)
|
||||
chapter_context = await context_builder.build(
|
||||
chapter=chapter,
|
||||
project=project,
|
||||
@@ -2631,9 +2753,12 @@ async def generate_single_chapter_for_batch(
|
||||
if chapter.summary and chapter.summary.strip():
|
||||
chapter_outline_content += f"\n\n【章节补充说明】\n{chapter.summary}"
|
||||
|
||||
# 可选:附加大纲的背景信息
|
||||
# 可选:附加大纲的背景信息(限制长度)
|
||||
if outline:
|
||||
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline.content}"
|
||||
outline_bg = outline.content
|
||||
if len(outline_bg) > 200:
|
||||
outline_bg = outline_bg[:200] + "..."
|
||||
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline_bg}"
|
||||
|
||||
logger.info(f"✏️ 批量生成 - 一对多模式:使用expansion_plan详细规划")
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -2647,6 +2772,18 @@ async def generate_single_chapter_for_batch(
|
||||
# 🚀 使用 V2 优化模板构建提示词(批量生成)
|
||||
if chapter_context.continuation_point:
|
||||
# 有前置内容,使用 WITH_CONTEXT 模板
|
||||
|
||||
# 确定上一章摘要:优先使用传入的 previous_summary_context(批量生成的上一章),
|
||||
# 否则尝试从 chapter_context 中获取
|
||||
final_prev_summary = "(无上一章摘要,请根据锚点续写)"
|
||||
|
||||
if previous_summary_context:
|
||||
final_prev_summary = previous_summary_context
|
||||
elif hasattr(chapter_context, 'recent_summary') and chapter_context.recent_summary:
|
||||
lines = chapter_context.recent_summary.strip().split('\n')
|
||||
if lines:
|
||||
final_prev_summary = lines[-1]
|
||||
|
||||
template = await PromptService.get_template("CHAPTER_GENERATION_V2_WITH_CONTEXT", user_id, db_session)
|
||||
base_prompt = PromptService.format_prompt(
|
||||
template,
|
||||
@@ -2661,6 +2798,8 @@ async def generate_single_chapter_for_batch(
|
||||
genre=project.genre or '未设定',
|
||||
narrative_perspective=project.narrative_perspective or '第三人称',
|
||||
characters_info=characters_info or '暂无角色信息',
|
||||
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
|
||||
previous_chapter_summary=final_prev_summary,
|
||||
# P2 参考参数(动态裁剪后的)
|
||||
story_skeleton=chapter_context.story_skeleton or '',
|
||||
relevant_memories=chapter_context.relevant_memories or ''
|
||||
@@ -2741,6 +2880,26 @@ async def generate_single_chapter_for_batch(
|
||||
await db_session.refresh(chapter)
|
||||
|
||||
logger.info(f"✅ 单章节生成完成: 第{chapter.chapter_number}章,共 {new_word_count} 字")
|
||||
|
||||
# 生成简短摘要返回
|
||||
summary_preview = full_content[:300].replace('\n', ' ') if full_content else ""
|
||||
|
||||
# 🔮 批量生成后自动标记计划在本章埋入的伏笔
|
||||
try:
|
||||
async with write_lock:
|
||||
plant_result = await foreshadow_service.auto_plant_pending_foreshadows(
|
||||
db=db_session,
|
||||
project_id=chapter.project_id,
|
||||
chapter_id=chapter.id,
|
||||
chapter_number=chapter.chapter_number,
|
||||
chapter_content=full_content
|
||||
)
|
||||
if plant_result.get('planted_count', 0) > 0:
|
||||
logger.info(f"🔮 批量生成 - 自动标记伏笔已埋入: {plant_result['planted_count']}个")
|
||||
except Exception as plant_error:
|
||||
logger.warning(f"⚠️ 批量生成 - 自动标记伏笔埋入失败: {str(plant_error)}")
|
||||
|
||||
return summary_preview
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user