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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
"""伏笔管理API路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional, List
|
||||
|
||||
from app.database import get_db
|
||||
from app.api.common import verify_project_access
|
||||
from app.services.foreshadow_service import foreshadow_service
|
||||
from app.schemas.foreshadow import (
|
||||
ForeshadowCreate,
|
||||
ForeshadowUpdate,
|
||||
ForeshadowResponse,
|
||||
ForeshadowListResponse,
|
||||
ForeshadowStatsResponse,
|
||||
PlantForeshadowRequest,
|
||||
ResolveForeshadowRequest,
|
||||
SyncFromAnalysisRequest,
|
||||
SyncFromAnalysisResponse,
|
||||
ForeshadowContextResponse
|
||||
)
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/foreshadows", tags=["foreshadows"])
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}", response_model=ForeshadowListResponse)
|
||||
async def get_project_foreshadows(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
status: Optional[str] = Query(None, description="状态筛选: pending/planted/resolved/abandoned"),
|
||||
category: Optional[str] = Query(None, description="分类筛选"),
|
||||
source_type: Optional[str] = Query(None, description="来源筛选: analysis/manual"),
|
||||
is_long_term: Optional[bool] = Query(None, description="是否长线伏笔"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
limit: int = Query(50, ge=1, le=100, description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取项目所有伏笔
|
||||
|
||||
支持按状态、分类、来源筛选,支持分页
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
|
||||
result = await foreshadow_service.get_project_foreshadows(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
status=status,
|
||||
category=category,
|
||||
source_type=source_type,
|
||||
is_long_term=is_long_term,
|
||||
page=page,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取伏笔列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取伏笔列表失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/stats", response_model=ForeshadowStatsResponse)
|
||||
async def get_foreshadow_stats(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
current_chapter: Optional[int] = Query(None, ge=1, description="当前章节号(用于计算超期)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取项目伏笔统计"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
|
||||
stats = await foreshadow_service.get_stats(db, project_id, current_chapter)
|
||||
return stats
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取伏笔统计失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取伏笔统计失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/context/{chapter_number}", response_model=ForeshadowContextResponse)
|
||||
async def get_chapter_foreshadow_context(
|
||||
project_id: str,
|
||||
chapter_number: int,
|
||||
request: Request,
|
||||
include_pending: bool = Query(True, description="包含待埋入伏笔"),
|
||||
include_overdue: bool = Query(True, description="包含超期伏笔"),
|
||||
lookahead: int = Query(5, ge=1, le=20, description="向前看几章"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取章节生成的伏笔上下文
|
||||
|
||||
用于在章节生成时提供伏笔提醒
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
|
||||
context = await foreshadow_service.build_chapter_context(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
chapter_number=chapter_number,
|
||||
include_pending=include_pending,
|
||||
include_overdue=include_overdue,
|
||||
lookahead=lookahead
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取伏笔上下文失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取伏笔上下文失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/pending-resolve")
|
||||
async def get_pending_resolve_foreshadows(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
current_chapter: int = Query(..., ge=1, description="当前章节号"),
|
||||
lookahead: int = Query(5, ge=1, le=20, description="向前看几章"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取待回收伏笔列表(用于章节生成提醒)"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
|
||||
foreshadows = await foreshadow_service.get_pending_resolve_foreshadows(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
current_chapter=current_chapter,
|
||||
lookahead=lookahead
|
||||
)
|
||||
|
||||
return {
|
||||
"total": len(foreshadows),
|
||||
"items": [f.to_dict() for f in foreshadows]
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取待回收伏笔失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取待回收伏笔失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{foreshadow_id}", response_model=ForeshadowResponse)
|
||||
async def get_foreshadow(
|
||||
foreshadow_id: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取单个伏笔详情"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
# 验证权限
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
return foreshadow.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取伏笔详情失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取伏笔详情失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("", response_model=ForeshadowResponse)
|
||||
async def create_foreshadow(
|
||||
data: ForeshadowCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建伏笔(手动添加)
|
||||
|
||||
创建一个新的自定义伏笔
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(data.project_id, user_id, db)
|
||||
|
||||
foreshadow = await foreshadow_service.create_foreshadow(db, data)
|
||||
return foreshadow.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 创建伏笔失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"创建伏笔失败: {str(e)}")
|
||||
|
||||
|
||||
@router.put("/{foreshadow_id}", response_model=ForeshadowResponse)
|
||||
async def update_foreshadow(
|
||||
foreshadow_id: str,
|
||||
data: ForeshadowUpdate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新伏笔"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
updated = await foreshadow_service.update_foreshadow(db, foreshadow_id, data)
|
||||
return updated.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 更新伏笔失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"更新伏笔失败: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/{foreshadow_id}")
|
||||
async def delete_foreshadow(
|
||||
foreshadow_id: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除伏笔"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
await foreshadow_service.delete_foreshadow(db, foreshadow_id)
|
||||
|
||||
return {"message": "伏笔删除成功", "id": foreshadow_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 删除伏笔失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"删除伏笔失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{foreshadow_id}/plant", response_model=ForeshadowResponse)
|
||||
async def plant_foreshadow(
|
||||
foreshadow_id: str,
|
||||
data: PlantForeshadowRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
标记伏笔为已埋入
|
||||
|
||||
将伏笔状态从pending改为planted,记录埋入章节
|
||||
"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
updated = await foreshadow_service.mark_as_planted(db, foreshadow_id, data)
|
||||
return updated.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 标记伏笔埋入失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"标记伏笔埋入失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{foreshadow_id}/resolve", response_model=ForeshadowResponse)
|
||||
async def resolve_foreshadow(
|
||||
foreshadow_id: str,
|
||||
data: ResolveForeshadowRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
标记伏笔为已回收
|
||||
|
||||
将伏笔状态改为resolved或partially_resolved
|
||||
"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
updated = await foreshadow_service.mark_as_resolved(db, foreshadow_id, data)
|
||||
return updated.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 标记伏笔回收失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"标记伏笔回收失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{foreshadow_id}/abandon", response_model=ForeshadowResponse)
|
||||
async def abandon_foreshadow(
|
||||
foreshadow_id: str,
|
||||
request: Request,
|
||||
reason: Optional[str] = Query(None, description="废弃原因"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
标记伏笔为已废弃
|
||||
|
||||
决定不再使用此伏笔
|
||||
"""
|
||||
try:
|
||||
foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id)
|
||||
|
||||
if not foreshadow:
|
||||
raise HTTPException(status_code=404, detail="伏笔不存在")
|
||||
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(foreshadow.project_id, user_id, db)
|
||||
|
||||
updated = await foreshadow_service.mark_as_abandoned(db, foreshadow_id, reason)
|
||||
return updated.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 标记伏笔废弃失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"标记伏笔废弃失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/sync-from-analysis", response_model=SyncFromAnalysisResponse)
|
||||
async def sync_foreshadows_from_analysis(
|
||||
project_id: str,
|
||||
data: SyncFromAnalysisRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
从分析结果同步伏笔
|
||||
|
||||
从章节分析结果中提取伏笔信息,同步到伏笔管理表
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
|
||||
result = await foreshadow_service.sync_from_analysis(db, project_id, data)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 同步伏笔失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"同步伏笔失败: {str(e)}")
|
||||
+39
-12
@@ -1,7 +1,7 @@
|
||||
"""记忆管理API - 提供记忆的查询、分析等接口"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, desc
|
||||
from sqlalchemy import select, and_, desc, delete
|
||||
from typing import List, Optional
|
||||
from app.database import get_db
|
||||
from app.models.memory import StoryMemory, PlotAnalysis
|
||||
@@ -9,6 +9,7 @@ from app.models.chapter import Chapter
|
||||
from app.models.project import Project
|
||||
from app.services.memory_service import memory_service
|
||||
from app.services.plot_analyzer import get_plot_analyzer
|
||||
from app.services.foreshadow_service import foreshadow_service
|
||||
from app.services.ai_service import create_user_ai_service
|
||||
from app.models.settings import Settings
|
||||
from app.logger import get_logger
|
||||
@@ -71,13 +72,23 @@ async def analyze_chapter(
|
||||
max_tokens=settings.max_tokens
|
||||
)
|
||||
|
||||
# 执行剧情分析
|
||||
# 获取已埋入的伏笔列表(用于回收匹配)
|
||||
existing_foreshadows = await foreshadow_service.get_planted_foreshadows_for_analysis(
|
||||
db=db,
|
||||
project_id=project_id
|
||||
)
|
||||
logger.info(f"📋 已获取{len(existing_foreshadows)}个已埋入伏笔用于分析匹配")
|
||||
|
||||
# 执行剧情分析(传入已有伏笔列表)
|
||||
analyzer = get_plot_analyzer(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),
|
||||
user_id=user_id,
|
||||
db=db,
|
||||
existing_foreshadows=existing_foreshadows
|
||||
)
|
||||
|
||||
if not analysis_result:
|
||||
@@ -116,16 +127,14 @@ async def analyze_chapter(
|
||||
word_count=chapter.word_count
|
||||
)
|
||||
|
||||
# 检查是否已存在分析记录
|
||||
existing = await db.execute(
|
||||
# 检查是否已存在分析记录,如有则删除
|
||||
existing_result = await db.execute(
|
||||
select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
# 删除旧记录
|
||||
await db.execute(
|
||||
select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id)
|
||||
)
|
||||
await db.delete(existing.scalar_one())
|
||||
existing_analysis = existing_result.scalar_one_or_none()
|
||||
if existing_analysis:
|
||||
await db.delete(existing_analysis)
|
||||
await db.flush()
|
||||
|
||||
db.add(plot_analysis)
|
||||
await db.commit()
|
||||
@@ -169,13 +178,31 @@ async def analyze_chapter(
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 【新增】自动更新伏笔状态
|
||||
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(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter_id,
|
||||
chapter_number=chapter.chapter_number,
|
||||
analysis_foreshadows=analysis_foreshadows
|
||||
)
|
||||
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
|
||||
"memories_count": saved_count,
|
||||
"foreshadow_stats": foreshadow_stats
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -35,6 +35,8 @@ from app.services.ai_service import AIService
|
||||
from app.services.prompt_service import prompt_service, PromptService
|
||||
from app.services.memory_service import memory_service
|
||||
from app.services.plot_expansion_service import PlotExpansionService
|
||||
from app.services.foreshadow_service import foreshadow_service
|
||||
from app.services.memory_service import memory_service
|
||||
from app.logger import get_logger
|
||||
from app.api.settings import get_user_ai_service
|
||||
from app.utils.sse_response import SSEResponse, create_sse_response, WizardProgressTracker
|
||||
@@ -261,7 +263,7 @@ async def delete_outline(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除大纲,同时删除该大纲对应的所有章节"""
|
||||
"""删除大纲,同时删除该大纲对应的所有章节和相关的伏笔数据"""
|
||||
result = await db.execute(
|
||||
select(Outline).where(Outline.id == outline_id)
|
||||
)
|
||||
@@ -279,6 +281,7 @@ async def delete_outline(
|
||||
|
||||
# 获取要删除的章节并计算总字数
|
||||
deleted_word_count = 0
|
||||
deleted_foreshadow_count = 0
|
||||
if project.outline_mode == 'one-to-one':
|
||||
# one-to-one模式:通过chapter_number获取对应章节
|
||||
chapters_result = await db.execute(
|
||||
@@ -290,6 +293,33 @@ async def delete_outline(
|
||||
chapters_to_delete = chapters_result.scalars().all()
|
||||
deleted_word_count = sum(ch.word_count or 0 for ch in chapters_to_delete)
|
||||
|
||||
# 🔮 清理章节相关的伏笔数据和向量记忆
|
||||
for chapter in chapters_to_delete:
|
||||
try:
|
||||
# 清理向量数据库中的记忆数据
|
||||
await memory_service.delete_chapter_memories(
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter.id
|
||||
)
|
||||
logger.info(f"✅ 已清理章节 {chapter.id[:8]} 的向量记忆数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理章节 {chapter.id[:8]} 向量记忆失败: {str(e)}")
|
||||
|
||||
try:
|
||||
# 清理伏笔数据(分析来源的伏笔)
|
||||
foreshadow_result = await foreshadow_service.delete_chapter_foreshadows(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter.id,
|
||||
only_analysis_source=True
|
||||
)
|
||||
deleted_foreshadow_count += foreshadow_result.get('deleted_count', 0)
|
||||
if foreshadow_result.get('deleted_count', 0) > 0:
|
||||
logger.info(f"🔮 已清理章节 {chapter.id[:8]} 的 {foreshadow_result['deleted_count']} 个伏笔数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理章节 {chapter.id[:8]} 伏笔数据失败: {str(e)}")
|
||||
|
||||
# 删除章节
|
||||
delete_result = await db.execute(
|
||||
delete(Chapter).where(
|
||||
@@ -298,7 +328,7 @@ async def delete_outline(
|
||||
)
|
||||
)
|
||||
deleted_chapters_count = delete_result.rowcount
|
||||
logger.info(f"一对一模式:删除大纲 {outline_id}(序号{outline.order_index}),同时删除了第{outline.order_index}章({deleted_chapters_count}个章节,{deleted_word_count}字)")
|
||||
logger.info(f"一对一模式:删除大纲 {outline_id}(序号{outline.order_index}),同时删除了第{outline.order_index}章({deleted_chapters_count}个章节,{deleted_word_count}字,{deleted_foreshadow_count}个伏笔)")
|
||||
else:
|
||||
# one-to-many模式:通过outline_id获取关联章节
|
||||
chapters_result = await db.execute(
|
||||
@@ -307,12 +337,39 @@ async def delete_outline(
|
||||
chapters_to_delete = chapters_result.scalars().all()
|
||||
deleted_word_count = sum(ch.word_count or 0 for ch in chapters_to_delete)
|
||||
|
||||
# 🔮 清理章节相关的伏笔数据和向量记忆
|
||||
for chapter in chapters_to_delete:
|
||||
try:
|
||||
# 清理向量数据库中的记忆数据
|
||||
await memory_service.delete_chapter_memories(
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter.id
|
||||
)
|
||||
logger.info(f"✅ 已清理章节 {chapter.id[:8]} 的向量记忆数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理章节 {chapter.id[:8]} 向量记忆失败: {str(e)}")
|
||||
|
||||
try:
|
||||
# 清理伏笔数据(分析来源的伏笔)
|
||||
foreshadow_result = await foreshadow_service.delete_chapter_foreshadows(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
chapter_id=chapter.id,
|
||||
only_analysis_source=True
|
||||
)
|
||||
deleted_foreshadow_count += foreshadow_result.get('deleted_count', 0)
|
||||
if foreshadow_result.get('deleted_count', 0) > 0:
|
||||
logger.info(f"🔮 已清理章节 {chapter.id[:8]} 的 {foreshadow_result['deleted_count']} 个伏笔数据")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 清理章节 {chapter.id[:8]} 伏笔数据失败: {str(e)}")
|
||||
|
||||
# 删除章节
|
||||
delete_result = await db.execute(
|
||||
delete(Chapter).where(Chapter.outline_id == outline_id)
|
||||
)
|
||||
deleted_chapters_count = delete_result.rowcount
|
||||
logger.info(f"一对多模式:删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节({deleted_word_count}字)")
|
||||
logger.info(f"一对多模式:删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节({deleted_word_count}字,{deleted_foreshadow_count}个伏笔)")
|
||||
|
||||
# 更新项目字数
|
||||
if deleted_word_count > 0:
|
||||
@@ -353,7 +410,8 @@ async def delete_outline(
|
||||
|
||||
return {
|
||||
"message": "大纲删除成功",
|
||||
"deleted_chapters": deleted_chapters_count
|
||||
"deleted_chapters": deleted_chapters_count,
|
||||
"deleted_foreshadows": deleted_foreshadow_count
|
||||
}
|
||||
|
||||
|
||||
@@ -614,6 +672,12 @@ async def _generate_new_outline(
|
||||
# 全新生成模式:删除旧大纲和关联的所有章节
|
||||
logger.info(f"全新生成:删除项目 {project.id} 的旧大纲和章节(outline_mode: {project.outline_mode})")
|
||||
|
||||
# 清理伏笔数据
|
||||
try:
|
||||
await foreshadow_service.clear_project_foreshadows_for_reset(db, project.id)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理伏笔数据失败(不影响主流程): {str(e)}")
|
||||
|
||||
from sqlalchemy import delete as sql_delete
|
||||
|
||||
# 先获取所有旧章节并计算总字数
|
||||
@@ -1601,9 +1665,15 @@ async def new_outline_generator(
|
||||
logger.info(f"🔄 重试生成完成,累计{len(ai_content)}字符")
|
||||
|
||||
# 全新生成模式:删除旧大纲和关联的所有章节
|
||||
yield await tracker.saving("清理旧大纲和章节...", 0.2)
|
||||
yield await tracker.saving("清理旧大纲、章节和伏笔...", 0.2)
|
||||
logger.info(f"全新生成:删除项目 {project_id} 的旧大纲和章节(outline_mode: {project.outline_mode})")
|
||||
|
||||
# 清理伏笔数据
|
||||
try:
|
||||
await foreshadow_service.clear_project_foreshadows_for_reset(db, project_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理伏笔数据失败(不影响主流程): {str(e)}")
|
||||
|
||||
from sqlalchemy import delete as sql_delete
|
||||
|
||||
# 先获取所有旧章节并计算总字数
|
||||
|
||||
Reference in New Issue
Block a user