diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index 667543c..d905983 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -11,7 +11,10 @@ from asyncio import Queue, Lock from app.database import get_db from app.api.common import verify_project_access -from app.services.chapter_context_service import ChapterContextBuilder, FocusedMemoryRetriever +from app.services.chapter_context_service import ( + OneToManyContextBuilder, + OneToOneContextBuilder +) from app.models.chapter import Chapter from app.models.project import Project from app.models.outline import Outline @@ -1325,55 +1328,6 @@ async def generate_chapter_content_stream( ) outline = outline_result.scalar_one_or_none() - # 获取所有大纲用于上下文 - all_outlines_result = await db_session.execute( - select(Outline) - .where(Outline.project_id == current_chapter.project_id) - .order_by(Outline.order_index) - .execution_options(populate_existing=True) - ) - all_outlines = all_outlines_result.scalars().all() - outlines_context = "\n".join([ - f"第{o.order_index}章 {o.title}: {o.content[:100]}..." - for o in all_outlines - ]) - - # 获取角色信息(包含职业信息) - characters_result = await db_session.execute( - select(Character).where(Character.project_id == current_chapter.project_id) - ) - characters = characters_result.scalars().all() - - # 📝 根据大纲模式智能筛选相关角色 - filter_character_names = None - if outline_mode == 'one-to-one': - # 1-1模式:从outline.structure中提取characters字段 - if outline and outline.structure: - try: - structure = json.loads(outline.structure) - filter_character_names = structure.get('characters', []) - if filter_character_names: - logger.info(f"📋 1-1模式:从structure提取角色列表 {filter_character_names}") - except json.JSONDecodeError: - logger.warning(f"⚠️ outline.structure解析失败,使用全部角色") - else: - # 1-n模式:从chapter.expansion_plan中提取character_focus字段 - if current_chapter.expansion_plan: - try: - plan = json.loads(current_chapter.expansion_plan) - filter_character_names = plan.get('character_focus', []) - if filter_character_names: - logger.info(f"📋 1-n模式:从expansion_plan提取角色焦点 {filter_character_names}") - except json.JSONDecodeError: - logger.warning(f"⚠️ expansion_plan解析失败,使用全部角色") - - characters_info = await build_characters_info_with_careers( - db=db_session, - project_id=current_chapter.project_id, - characters=characters, - filter_character_names=filter_character_names - ) - # 获取写作风格 style_content = "" if style_id: @@ -1395,23 +1349,59 @@ async def generate_chapter_content_stream( else: logger.info("未指定写作风格,使用原始提示词") - # 🚀 使用新的优化上下文构建器(含伏笔服务) - logger.info(f"🔧 使用优化的章节上下文构建器(V2 + 伏笔提醒)") - context_builder = ChapterContextBuilder(foreshadow_service=foreshadow_service) - chapter_context = await context_builder.build( - chapter=current_chapter, - project=project, - outline=outline, - user_id=current_user_id, - db=db_session - ) - - # 日志输出统计信息 - logger.info(f"📊 优化上下文统计:") - logger.info(f" - 章节序号: {current_chapter.chapter_number}") - logger.info(f" - 衔接锚点长度: {len(chapter_context.continuation_point or '')} 字符") - logger.info(f" - 相关记忆: {chapter_context.context_stats.get('memory_count', 0)} 条") - logger.info(f" - 总上下文长度: {chapter_context.context_stats.get('total_length', 0)} 字符") + # 🚀 根据大纲模式选择独立的上下文构建器 + if outline_mode == 'one-to-one': + # ========== 1-1模式:使用独立的简化构建器 ========== + logger.info(f"🔧 [1-1模式] 使用 OneToOneContextBuilder") + context_builder = OneToOneContextBuilder( + memory_service=memory_service, + foreshadow_service=foreshadow_service + ) + chapter_context = await context_builder.build( + chapter=current_chapter, + project=project, + outline=outline, + user_id=current_user_id, + db=db_session, + target_word_count=target_word_count + ) + + # 日志输出统计信息 + logger.info(f"📊 [1-1模式] 上下文统计:") + logger.info(f" - 章节序号: {current_chapter.chapter_number}") + logger.info(f" - 大纲长度: {chapter_context.context_stats.get('outline_length', 0)} 字符") + logger.info(f" - 上一章内容: {chapter_context.context_stats.get('previous_content_length', 0)} 字符") + logger.info(f" - 角色信息: {chapter_context.context_stats.get('characters_length', 0)} 字符") + logger.info(f" - 伏笔提醒: {chapter_context.context_stats.get('foreshadow_length', 0)} 字符") + logger.info(f" - 相关记忆: {chapter_context.context_stats.get('memories_length', 0)} 字符") + logger.info(f" - 总长度: {chapter_context.context_stats.get('total_length', 0)} 字符") + else: + # ========== 1-N模式:使用独立的完整构建器 ========== + logger.info(f"🔧 [1-N模式] 使用 OneToManyContextBuilder") + context_builder = OneToManyContextBuilder( + memory_service=memory_service, + foreshadow_service=foreshadow_service + ) + chapter_context = await context_builder.build( + chapter=current_chapter, + project=project, + outline=outline, + user_id=current_user_id, + db=db_session, + style_content=style_content, + target_word_count=target_word_count, + temp_narrative_perspective=temp_narrative_perspective + ) + + # 日志输出统计信息 + logger.info(f"📊 [1-N模式] 上下文统计:") + logger.info(f" - 章节序号: {current_chapter.chapter_number}") + logger.info(f" - 衔接锚点: {chapter_context.context_stats.get('continuation_length', 0)} 字符") + logger.info(f" - 角色信息: {chapter_context.context_stats.get('characters_length', 0)} 字符") + logger.info(f" - 相关记忆: {chapter_context.context_stats.get('memories_length', 0)} 字符") + logger.info(f" - 故事骨架: {chapter_context.context_stats.get('skeleton_length', 0)} 字符") + logger.info(f" - 伏笔提醒: {chapter_context.context_stats.get('foreshadow_length', 0)} 字符") + logger.info(f" - 总长度: {chapter_context.context_stats.get('total_length', 0)} 字符") yield await tracker.loading("上下文构建完成", 0.8) @@ -1423,102 +1413,91 @@ async def generate_chapter_content_stream( ) logger.info(f"📝 使用叙事人称: {chapter_perspective}") - # 📋 根据大纲模式构建差异化的章节大纲上下文 - chapter_outline_content = "" + # 🚀 根据大纲模式选择提示词模板和参数 if outline_mode == 'one-to-one': - # 一对一模式:使用大纲的 content - chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲' - logger.info(f"✏️ 一对一模式:使用大纲内容作为章节指导") - else: - # 一对多模式:优先使用 expansion_plan 的详细规划 - if current_chapter.expansion_plan: - try: - plan = json.loads(current_chapter.expansion_plan) - chapter_outline_content = f"""【本章详细规划】 -剧情摘要:{plan.get('plot_summary', '无')} - -关键事件: -{chr(10).join(f'- {event}' for event in plan.get('key_events', []))} - -角色焦点:{', '.join(plan.get('character_focus', []))} - -情感基调:{plan.get('emotional_tone', '未设定')} - -叙事目标:{plan.get('narrative_goal', '未设定')} - -冲突类型:{plan.get('conflict_type', '未设定')}""" - - # 可选:附加章节 summary - if current_chapter.summary and current_chapter.summary.strip(): - chapter_outline_content += f"\n\n【章节补充说明】\n{current_chapter.summary}" - - # 可选:附加大纲的背景信息(限制长度,避免喧宾夺主) - if outline: - 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: - logger.warning(f"⚠️ expansion_plan解析失败: {e},回退到大纲内容") - chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲' + # 1-1模式 + if chapter_context.continuation_point: + # 有上一章内容 + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_ONE_NEXT", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=current_chapter.chapter_number, + chapter_title=current_chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=chapter_perspective, + previous_chapter_content=chapter_context.continuation_point, + characters_info=chapter_context.chapter_characters or '暂无角色信息', + chapter_careers=chapter_context.chapter_careers or '暂无职业信息', + foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', + relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' + ) + logger.debug(f"创建第{current_chapter.chapter_number}章提示词: {base_prompt}") else: - # 没有expansion_plan,使用大纲内容 - chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲' - logger.warning(f"⚠️ 一对多模式但无expansion_plan,使用大纲内容") - - # 🚀 使用 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, - # P0 核心参数 - project_title=project.title, - chapter_number=current_chapter.chapter_number, - chapter_title=current_chapter.title, - chapter_outline=chapter_outline_content, - target_word_count=target_word_count, - continuation_point=chapter_context.continuation_point, - # P1 重要参数 - 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 '' - ) + # 第一章 + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_ONE", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=current_chapter.chapter_number, + chapter_title=current_chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=chapter_perspective, + characters_info=chapter_context.chapter_characters or '暂无角色信息', + chapter_careers=chapter_context.chapter_careers or '暂无职业信息', + foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', + relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' + ) + logger.debug(f"创建第一章提示词: {base_prompt}") else: - # 第一章,使用无前置内容模板 - template = await PromptService.get_template("CHAPTER_GENERATION_V2", current_user_id, db_session) - base_prompt = PromptService.format_prompt( - template, - # P0 核心参数 - project_title=project.title, - chapter_number=current_chapter.chapter_number, - chapter_title=current_chapter.title, - chapter_outline=chapter_outline_content, - target_word_count=target_word_count, - # P1 重要参数 - genre=project.genre or '未设定', - narrative_perspective=chapter_perspective, - characters_info=characters_info or '暂无角色信息' - ) + # ========== 1-n模式:使用完整模板 ========== + if chapter_context.continuation_point: + # 有前置内容,使用 WITH_CONTEXT 模板 + logger.info(f"📝 [1-n模式] 使用带上下文的模板(第{current_chapter.chapter_number}章)") + + # 提取上一章摘要 + previous_summary = "(无上一章摘要,请根据锚点续写)" + if chapter_context.previous_chapter_summary: + previous_summary = chapter_context.previous_chapter_summary + + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_MANY_NEXT", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=current_chapter.chapter_number, + chapter_title=current_chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + continuation_point=chapter_context.continuation_point, + genre=project.genre or '未设定', + narrative_perspective=chapter_perspective, + characters_info=chapter_context.chapter_characters or '暂无角色信息', + foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', + previous_chapter_summary=previous_summary, + story_skeleton=chapter_context.story_skeleton or '', + relevant_memories=chapter_context.relevant_memories or '' + ) + logger.debug(f"创建第{current_chapter.chapter_number}章提示词: {base_prompt}") + else: + # 第1章,使用无前置内容模板 + logger.info(f"📝 [1-n模式] 使用第一章模板") + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_MANY", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=current_chapter.chapter_number, + chapter_title=current_chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=chapter_perspective, + characters_info=chapter_context.chapter_characters or '暂无角色信息' + ) + logger.debug(f"创建第一章提示词: {base_prompt}") # 应用写作风格 if style_content: @@ -2687,18 +2666,6 @@ async def generate_single_chapter_for_batch( ) outline = outline_result.scalar_one_or_none() - # 获取所有大纲用于上下文 - all_outlines_result = await db_session.execute( - select(Outline) - .where(Outline.project_id == chapter.project_id) - .order_by(Outline.order_index) - ) - all_outlines = all_outlines_result.scalars().all() - outlines_context = "\n".join([ - f"第{o.order_index}章 {o.title}: {o.content[:100]}..." - for o in all_outlines - ]) - # 获取角色信息(包含职业信息) characters_result = await db_session.execute( select(Character).where(Character.project_id == chapter.project_id) @@ -2746,16 +2713,38 @@ 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(foreshadow_service=foreshadow_service) - chapter_context = await context_builder.build( - chapter=chapter, - project=project, - outline=outline, - user_id=user_id, - db=db_session - ) + # 🚀 根据大纲模式选择独立的上下文构建器(批量生成) + if outline_mode == 'one-to-one': + # 1-1模式 + logger.info(f"🔧 批量生成 - [1-1模式] 使用 OneToOneContextBuilder") + context_builder = OneToOneContextBuilder( + memory_service=memory_service, + foreshadow_service=foreshadow_service + ) + chapter_context = await context_builder.build( + chapter=chapter, + project=project, + outline=outline, + user_id=user_id, + db=db_session, + target_word_count=target_word_count + ) + else: + # 1-N模式:使用独立的完整构建器 + logger.info(f"🔧 批量生成 - [1-N模式] 使用 OneToManyContextBuilder") + context_builder = OneToManyContextBuilder( + memory_service=memory_service, + foreshadow_service=foreshadow_service + ) + chapter_context = await context_builder.build( + chapter=chapter, + project=project, + outline=outline, + user_id=user_id, + db=db_session, + style_content=style_content, + target_word_count=target_word_count + ) # 日志输出统计信息 logger.info(f"📊 批量生成 - 优化上下文统计:") @@ -2809,57 +2798,88 @@ async def generate_single_chapter_for_batch( chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲' logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容") - # 🚀 使用 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, - # P0 核心参数 - project_title=project.title, - chapter_number=chapter.chapter_number, - chapter_title=chapter.title, - chapter_outline=chapter_outline_content, - target_word_count=target_word_count, - continuation_point=chapter_context.continuation_point, - # P1 重要参数 - 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 '' - ) + # 🚀 根据大纲模式选择提示词模板(批量生成) + if outline_mode == 'one-to-one': + # 1-1模式 + if chapter_context.continuation_point: + # 有上一章内容 + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_ONE_NEXT", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=chapter.chapter_number, + chapter_title=chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=project.narrative_perspective or '第三人称', + previous_chapter_content=chapter_context.continuation_point, + characters_info=chapter_context.chapter_characters or '暂无角色信息', + chapter_careers=chapter_context.chapter_careers or '暂无职业信息', + foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', + relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' + ) + else: + # 第一章 + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_ONE", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=chapter.chapter_number, + chapter_title=chapter.title, + chapter_outline=chapter_context.chapter_outline, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=project.narrative_perspective or '第三人称', + characters_info=chapter_context.chapter_characters or '暂无角色信息', + chapter_careers=chapter_context.chapter_careers or '暂无职业信息', + foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', + relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' + ) else: - # 第一章,使用无前置内容模板 - template = await PromptService.get_template("CHAPTER_GENERATION_V2", user_id, db_session) - base_prompt = PromptService.format_prompt( - template, - # P0 核心参数 - project_title=project.title, - chapter_number=chapter.chapter_number, - chapter_title=chapter.title, - chapter_outline=chapter_outline_content, - target_word_count=target_word_count, - # P1 重要参数 - genre=project.genre or '未设定', - narrative_perspective=project.narrative_perspective or '第三人称', - characters_info=characters_info or '暂无角色信息' - ) + # 1-n模式:使用原有的完整模板 + if chapter_context.continuation_point: + # 有前置内容,使用 WITH_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_ONE_TO_MANY_NEXT", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=chapter.chapter_number, + chapter_title=chapter.title, + chapter_outline=chapter_outline_content, + target_word_count=target_word_count, + continuation_point=chapter_context.continuation_point, + 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, + story_skeleton=chapter_context.story_skeleton or '', + relevant_memories=chapter_context.relevant_memories or '' + ) + else: + # 第一章,使用无前置内容模板 + template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_MANY", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, + project_title=project.title, + chapter_number=chapter.chapter_number, + chapter_title=chapter.title, + chapter_outline=chapter_outline_content, + target_word_count=target_word_count, + genre=project.genre or '未设定', + narrative_perspective=project.narrative_perspective or '第三人称', + characters_info=characters_info or '暂无角色信息' + ) # 应用写作风格 if style_content: diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index 11d5c15..af75702 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -129,7 +129,7 @@ async def get_outlines( request: Request, db: AsyncSession = Depends(get_db) ): - """获取指定项目的所有大纲""" + """获取指定项目的所有大纲(优化版:后端完全解析structure,构建标准JSON返回)""" # 验证用户权限 user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) @@ -148,6 +148,27 @@ async def get_outlines( ) outlines = result.scalars().all() + # 🔧 优化:后端完全解析structure,提取所有字段填充到outline对象 + for outline in outlines: + if outline.structure: + try: + structure_data = json.loads(outline.structure) + + # 从structure中提取所有字段填充到outline对象 + outline.title = structure_data.get("title", f"第{outline.order_index}章") + outline.content = structure_data.get("summary") or structure_data.get("content", "") + + # structure字段保持不变,供前端使用其他字段(如characters、scenes等) + + except json.JSONDecodeError: + logger.warning(f"解析大纲 {outline.id} 的structure失败") + outline.title = f"第{outline.order_index}章" + outline.content = "解析失败" + else: + # 没有structure的异常情况 + outline.title = f"第{outline.order_index}章" + outline.content = "暂无内容" + return OutlineListResponse(total=total, items=outlines) @@ -205,11 +226,24 @@ async def update_outline( # 更新字段 update_data = outline_update.model_dump(exclude_unset=True) + + # 🔧 特殊处理:如果直接传递了structure字段,优先使用它 + if 'structure' in update_data: + # 直接使用前端传递的structure(前端已经处理好了完整的JSON) + outline.structure = update_data['structure'] + logger.info(f"直接更新大纲 {outline_id} 的structure字段") + # 从update_data中移除structure,避免后续重复处理 + structure_updated = True + del update_data['structure'] + else: + structure_updated = False + + # 更新其他字段 for field, value in update_data.items(): setattr(outline, field, value) - # 如果修改了content或title,同步更新structure字段 - if 'content' in update_data or 'title' in update_data: + # 如果没有直接更新structure,但修改了content或title,则同步更新structure字段 + if not structure_updated and ('content' in update_data or 'title' in update_data): try: # 尝试解析现有的structure if outline.structure: @@ -605,754 +639,217 @@ async def predict_organizations( raise HTTPException(status_code=500, detail=f"组织预测失败: {str(e)}") - -async def _generate_new_outline( - request: OutlineGenerateRequest, +async def _build_outline_continue_context( project: Project, - db: AsyncSession, - user_ai_service: AIService, - user_id: str -) -> OutlineListResponse: - """全新生成大纲(MCP增强版)""" - logger.info(f"全新生成大纲 - 项目: {project.id}, enable_mcp: {request.enable_mcp}") - - # 获取角色信息 - characters_result = await db.execute( - select(Character).where(Character.project_id == project.id) - ) - characters = characters_result.scalars().all() - characters_info = _build_characters_info(characters) - - # 设置用户信息以启用MCP - if user_id: - user_ai_service.user_id = user_id - user_ai_service.db_session = db - - # 使用提示词模板 - template = await PromptService.get_template("OUTLINE_CREATE", user_id, db) - prompt = PromptService.format_prompt( - template, - title=project.title, - theme=request.theme or project.theme or "未设定", - genre=request.genre or project.genre or "通用", - chapter_count=request.chapter_count, - narrative_perspective=request.narrative_perspective, - target_words=request.target_words, - time_period=project.world_time_period or "未设定", - location=project.world_location or "未设定", - atmosphere=project.world_atmosphere or "未设定", - rules=project.world_rules or "未设定", - characters_info=characters_info or "暂无角色信息", - requirements=request.requirements or "", - mcp_references="" - ) - - # 调用AI流式生成大纲(带字数统计) - accumulated_text = "" - chunk_count = 0 - - async for chunk in user_ai_service.generate_text_stream( - prompt=prompt, - provider=request.provider, - model=request.model, - auto_mcp=request.enable_mcp - ): - chunk_count += 1 - accumulated_text += chunk - - # 这里是非SSE接口,不需要发送chunk - # 如果未来需要转SSE,可以在这里yield - - ai_content = accumulated_text - ai_response = {"content": ai_content} - - # 解析响应 - outline_data = _parse_ai_response(ai_content) - - # 全新生成模式:删除旧大纲和关联的所有章节 - 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 - - # 先获取所有旧章节并计算总字数 - old_chapters_result = await db.execute( - select(Chapter).where(Chapter.project_id == project.id) - ) - old_chapters = old_chapters_result.scalars().all() - deleted_word_count = sum(ch.word_count or 0 for ch in old_chapters) - - # 删除所有旧章节(无论是一对一还是一对多模式) - delete_result = await db.execute( - sql_delete(Chapter).where(Chapter.project_id == project.id) - ) - deleted_chapters_count = delete_result.rowcount - logger.info(f"✅ 全新生成:删除了 {deleted_chapters_count} 个旧章节({deleted_word_count}字)") - - # 更新项目字数 - if deleted_word_count > 0: - project.current_words = max(0, project.current_words - deleted_word_count) - logger.info(f"更新项目字数:减少 {deleted_word_count} 字") - - # 再删除所有旧大纲 - delete_outline_result = await db.execute( - sql_delete(Outline).where(Outline.project_id == project.id) - ) - deleted_outlines_count = delete_outline_result.rowcount - logger.info(f"✅ 全新生成:删除了 {deleted_outlines_count} 个旧大纲") - - # 保存新大纲 - outlines = await _save_outlines( - project.id, outline_data, db, start_index=1 - ) - - # 记录历史 - history = GenerationHistory( - project_id=project.id, - prompt=prompt, - generated_content=json.dumps(ai_response, ensure_ascii=False) if isinstance(ai_response, dict) else ai_response, - model=request.model or "default" - ) - db.add(history) - - await db.commit() - - for outline in outlines: - await db.refresh(outline) - - logger.info(f"全新生成完成 - {len(outlines)} 章") - return OutlineListResponse(total=len(outlines), items=outlines) - - -async def _build_smart_outline_context( latest_outlines: List[Outline], - user_id: str, - project_id: str + characters: List[Character], + chapter_count: int, + plot_stage: str, + story_direction: str, + requirements: str, + db: AsyncSession ) -> dict: """ - 智能构建大纲续写上下文(支持海量大纲场景) + 构建大纲续写上下文(简化版) - 策略: - 1. 故事骨架:每50章采样1章(仅标题) - 2. 近期概要:最近20章(标题+简要) - 3. 最近详细:最近2章(完整内容) + 包含内容: + 1. 项目基础信息:title, theme, genre, world_time_period, world_location, + world_atmosphere, world_rules, narrative_perspective + 2. 最近10章的完整大纲structure(解析JSON转化为文本) + 3. 所有角色的全部信息 + 4. 用户输入:chapter_count, plot_stage, story_direction, requirements Args: + project: 项目对象 latest_outlines: 所有已有大纲列表 - user_id: 用户ID - project_id: 项目ID + characters: 所有角色列表 + chapter_count: 要生成的章节数 + plot_stage: 情节阶段 + story_direction: 故事发展方向 + requirements: 其他要求 Returns: - 包含压缩后上下文的字典 + 包含上下文信息的字典 """ - total_count = len(latest_outlines) - context = { - 'story_skeleton': '', # 故事骨架(标题列表) - 'recent_summary': '', # 近期概要(标题+内容前50字) - 'recent_detail': '', # 最近详细(完整内容) + 'project_info': '', + 'recent_outlines': '', + 'characters_info': '', + 'user_input': '', 'stats': { - 'total': total_count, - 'skeleton_samples': 0, - 'recent_summaries': 0, - 'recent_details': 0 + 'total_outlines': len(latest_outlines), + 'recent_outlines_count': 0, + 'characters_count': len(characters) } } try: - # 1. 故事骨架(每50章采样,仅标题) - if total_count > 50: - sample_interval = 50 - skeleton_indices = list(range(0, total_count, sample_interval)) - skeleton_titles = [ - f"第{latest_outlines[idx].order_index}章: {latest_outlines[idx].title}" - for idx in skeleton_indices - ] - context['story_skeleton'] = "【故事骨架】\n" + "\n".join(skeleton_titles) - context['stats']['skeleton_samples'] = len(skeleton_titles) - logger.info(f" ✅ 故事骨架:采样{len(skeleton_titles)}章标题") - - # 2. 近期概要(最近20章,标题+内容前50字) - recent_summary_count = min(20, total_count) - if recent_summary_count > 2: # 排除最后2章(它们会完整展示) - recent_for_summary = latest_outlines[-recent_summary_count:-2] - recent_summaries = [ - f"第{o.order_index}章《{o.title}》: {o.content[:50]}..." - for o in recent_for_summary - ] - context['recent_summary'] = "【近期大纲概要】\n" + "\n".join(recent_summaries) - context['stats']['recent_summaries'] = len(recent_summaries) - logger.info(f" ✅ 近期概要:{len(recent_summaries)}章") - - # 3. 最近详细(最近2章,完整内容) - recent_detail_count = min(2, total_count) - recent_details = latest_outlines[-recent_detail_count:] - detail_texts = [ - f"第{o.order_index}章《{o.title}》: {o.content}" - for o in recent_details + # 1. 项目基础信息 + project_info_parts = [ + f"【项目基础信息】", + f"标题:{project.title}", + f"主题:{project.theme or '未设定'}", + f"类型:{project.genre or '未设定'}", + f"时代背景:{project.world_time_period or '未设定'}", + f"地点设定:{project.world_location or '未设定'}", + f"氛围基调:{project.world_atmosphere or '未设定'}", + f"世界规则:{project.world_rules or '未设定'}", + f"叙事视角:{project.narrative_perspective or '第三人称'}" ] - context['recent_detail'] = "【最近大纲详情】\n" + "\n".join(detail_texts) - context['stats']['recent_details'] = len(detail_texts) - logger.info(f" ✅ 最近详细:{len(detail_texts)}章") + context['project_info'] = "\n".join(project_info_parts) + + # 2. 最近10章的完整大纲structure(解析JSON转化为文本) + recent_count = min(10, len(latest_outlines)) + if recent_count > 0: + recent_outlines = latest_outlines[-recent_count:] + context['stats']['recent_outlines_count'] = recent_count + + outline_texts = [] + outline_texts.append(f"【最近{recent_count}章大纲详情】") + + for outline in recent_outlines: + outline_text = f"\n第{outline.order_index}章《{outline.title}》" + + # 尝试解析structure字段 + if outline.structure: + try: + structure_data = json.loads(outline.structure) + + # 提取各个字段(使用实际存储的字段名) + if structure_data.get('summary'): + outline_text += f"\n 概要:{structure_data['summary']}" + + # key_points 对应 关键事件 + if structure_data.get('key_points'): + events = structure_data['key_points'] + if isinstance(events, list): + outline_text += f"\n 关键事件:{', '.join(events)}" + else: + outline_text += f"\n 关键事件:{events}" + + # characters 对应 重点角色 + if structure_data.get('characters'): + chars = structure_data['characters'] + if isinstance(chars, list): + outline_text += f"\n 重点角色:{', '.join(chars)}" + else: + outline_text += f"\n 重点角色:{chars}" + + # emotion 对应 情感基调 + if structure_data.get('emotion'): + outline_text += f"\n 情感基调:{structure_data['emotion']}" + + # goal 对应 叙事目标 + if structure_data.get('goal'): + outline_text += f"\n 叙事目标:{structure_data['goal']}" + + # scenes 场景信息(可选显示) + if structure_data.get('scenes'): + scenes = structure_data['scenes'] + if isinstance(scenes, list) and scenes: + outline_text += f"\n 场景:{', '.join(scenes)}" + + except json.JSONDecodeError: + # 如果解析失败,使用content字段 + outline_text += f"\n 内容:{outline.content}" + else: + # 没有structure,使用content + outline_text += f"\n 内容:{outline.content}" + + outline_texts.append(outline_text) + + context['recent_outlines'] = "\n".join(outline_texts) + logger.info(f" ✅ 最近大纲:{recent_count}章") + + # 3. 所有角色的全部信息(包括职业信息) + if characters: + from app.models.career import Career, CharacterCareer + + char_texts = [] + char_texts.append("【角色信息】") + + for char in characters: + char_text = f"\n{char.name}({'组织' if char.is_organization else '角色'},{char.role_type})" + + if char.personality: + char_text += f"\n 性格特点:{char.personality}" + + if char.background: + char_text += f"\n 背景故事:{char.background}" + + if char.appearance: + char_text += f"\n 外貌描述:{char.appearance}" + + if char.traits: + char_text += f"\n 特征标签:{char.traits}" + + if char.relationships: + char_text += f"\n 关系网络:{char.relationships}" + + # 组织特有字段 + if char.is_organization: + if char.organization_type: + char_text += f"\n 组织类型:{char.organization_type}" + if char.organization_purpose: + char_text += f"\n 组织宗旨:{char.organization_purpose}" + if char.organization_members: + char_text += f"\n 组织成员:{char.organization_members}" + + # 查询角色的职业信息 + if not char.is_organization: + try: + career_result = await db.execute( + select(Career, CharacterCareer) + .join(CharacterCareer, Career.id == CharacterCareer.career_id) + .where(CharacterCareer.character_id == char.id) + ) + career_data = career_result.first() + + if career_data: + career, char_career = career_data + char_text += f"\n 职业:{career.name}" + if char_career.current_stage: + char_text += f"({char_career.current_stage}阶段)" + if char_career.career_type: + char_text += f"\n 职业类型:{char_career.career_type}" + except Exception as e: + logger.warning(f"查询角色 {char.name} 的职业信息失败: {str(e)}") + + char_texts.append(char_text) + + context['characters_info'] = "\n".join(char_texts) + logger.info(f" ✅ 角色信息:{len(characters)}个角色") + else: + context['characters_info'] = "【角色信息】\n暂无角色信息" + + # 4. 用户输入 + user_input_parts = [ + "【用户输入】", + f"要生成章节数:{chapter_count}章", + f"情节阶段:{plot_stage}", + f"故事发展方向:{story_direction}", + ] + if requirements: + user_input_parts.append(f"其他要求:{requirements}") + + context['user_input'] = "\n".join(user_input_parts) # 计算总长度 total_length = sum([ - len(context['story_skeleton']), - len(context['recent_summary']), - len(context['recent_detail']) + len(context['project_info']), + len(context['recent_outlines']), + len(context['characters_info']), + len(context['user_input']) ]) context['stats']['total_length'] = total_length - logger.info(f"📊 大纲上下文总长度: {total_length} 字符") + logger.info(f"📊 大纲续写上下文总长度: {total_length} 字符") except Exception as e: - logger.error(f"❌ 构建智能大纲上下文失败: {str(e)}", exc_info=True) + logger.error(f"❌ 构建大纲续写上下文失败: {str(e)}", exc_info=True) return context -async def _continue_outline( - request: OutlineGenerateRequest, - project: Project, - existing_outlines: List[Outline], - db: AsyncSession, - user_ai_service: AIService, - user_id: str -) -> OutlineListResponse: - """续写大纲 - 分批生成,每批5章(记忆+MCP+自动角色引入增强版)""" - logger.info(f"续写大纲 - 项目: {project.id}, 已有: {len(existing_outlines)} 章, enable_mcp: {request.enable_mcp}, enable_auto_characters: {request.enable_auto_characters}") - - # 分析已有大纲 - current_chapter_count = len(existing_outlines) - last_chapter_number = existing_outlines[-1].order_index - - # 计算需要生成的总章数和批次 - total_chapters_to_generate = request.chapter_count - batch_size = 5 # 每批生成5章 - total_batches = (total_chapters_to_generate + batch_size - 1) // batch_size - - logger.info(f"分批生成计划: 总共{total_chapters_to_generate}章,分{total_batches}批,每批{batch_size}章") - - # 获取角色信息(所有批次共用) - characters_result = await db.execute( - select(Character).where(Character.project_id == project.id) - ) - characters = characters_result.scalars().all() - characters_info = _build_characters_info(characters) - - # 情节阶段指导 - stage_instructions = { - "development": "继续展开情节,深化角色关系,推进主线冲突", - "climax": "进入故事高潮,矛盾激化,关键冲突爆发", - "ending": "解决主要冲突,收束伏笔,给出结局" - } - stage_instruction = stage_instructions.get(request.plot_stage, "") - - # 🎭 【方案A】先角色后大纲:在生成大纲前预测并创建角色 - # 🔧 判断:如果confirmed_organizations存在,说明已经是组织确认阶段,跳过角色处理 - if request.enable_auto_characters and not request.confirmed_organizations: - # 检查是否有用户确认的角色列表 - if request.confirmed_characters: - # 直接使用用户确认的角色列表创建角色 - try: - from app.services.auto_character_service import get_auto_character_service - - logger.info(f"🎭 【确认模式】用户提供了 {len(request.confirmed_characters)} 个确认的角色,直接创建") - - auto_char_service = get_auto_character_service(user_ai_service) - - # 🔧 去重检查:获取现有角色名称列表,避免重复创建 - existing_character_names = {char.name for char in characters} - actually_created_count = 0 - - for char_data in request.confirmed_characters: - try: - # 检查角色是否已存在 - char_name = char_data.get("name") or char_data.get("character_name") - if char_name in existing_character_names: - logger.warning(f"⚠️ 角色 '{char_name}' 已存在,跳过创建") - continue - - # 生成角色详细信息 - character_data = await auto_char_service._generate_character_details( - spec=char_data, - project=project, - existing_characters=list(characters), - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp - ) - - # 创建角色记录 - character = await auto_char_service._create_character_record( - project_id=project.id, - character_data=character_data, - db=db - ) - - # 建立关系 - relationships_data = character_data.get("relationships") or character_data.get("relationships_array", []) - if relationships_data: - await auto_char_service._create_relationships( - new_character=character, - relationship_specs=relationships_data, - existing_characters=list(characters), - project_id=project.id, - db=db - ) - - characters.append(character) - existing_character_names.add(character.name) # 更新已存在的角色名称集合 - actually_created_count += 1 - logger.info(f"✅ 创建确认的角色: {character.name}") - - except Exception as e: - logger.error(f"创建确认的角色失败: {e}", exc_info=True) - continue - - # 提交角色到数据库 - if actually_created_count > 0: - await db.commit() - logger.info(f"✅ 【确认模式】实际创建了 {actually_created_count} 个新角色(跳过了 {len(request.confirmed_characters) - actually_created_count} 个已存在的角色)") - else: - logger.info(f"ℹ️ 【确认模式】所有角色均已存在,无需创建") - - # 更新角色信息(供后续大纲生成使用) - characters_info = _build_characters_info(characters) - - except Exception as e: - logger.error(f"⚠️ 【确认模式】创建确认角色失败: {e}", exc_info=True) - else: - # 根据 require_character_confirmation 决定处理方式 - try: - from app.services.auto_character_service import get_auto_character_service - - # 构建已有章节概览 - all_chapters_brief_for_analysis = _build_chapters_brief(existing_outlines) - - auto_char_service = get_auto_character_service(user_ai_service) - - if request.require_character_confirmation: - # 🔮 预测模式:仅预测角色,不自动创建,需要用户确认 - logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色(需用户确认)") - - auto_result = await auto_char_service.analyze_and_create_characters( - project_id=project.id, - outline_content="", # 预测模式不需要大纲内容 - existing_characters=list(characters), - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp, - all_chapters_brief=all_chapters_brief_for_analysis, - start_chapter=last_chapter_number + 1, - chapter_count=total_chapters_to_generate, - plot_stage=request.plot_stage, - story_direction=request.story_direction or "自然延续", - preview_only=True # ✅ 仅预测不创建 - ) - - # 检查是否需要新角色 - if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"): - predicted_count = len(auto_result["predicted_characters"]) - logger.warning( - f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!" - ) - - # 🚨 抛出特殊异常,包含预测的角色信息 - raise HTTPException( - status_code=449, # 449 Retry With - detail={ - "code": "CHARACTER_CONFIRMATION_REQUIRED", - "message": "续写需要引入新角色,请先确认角色信息", - "predicted_characters": auto_result["predicted_characters"], - "reason": auto_result.get("reason", "剧情发展需要新角色"), - "chapter_range": f"第{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}章" - } - ) - else: - logger.info(f"✅ 【预测模式】AI判断无需引入新角色,继续生成大纲") - else: - # 🚀 直接创建模式:预测后自动创建,无需用户确认 - logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新角色(无需确认)") - - auto_result = await auto_char_service.analyze_and_create_characters( - project_id=project.id, - outline_content="", - existing_characters=list(characters), - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp, - all_chapters_brief=all_chapters_brief_for_analysis, - start_chapter=last_chapter_number + 1, - chapter_count=total_chapters_to_generate, - plot_stage=request.plot_stage, - story_direction=request.story_direction or "自然延续", - preview_only=False # ✅ 直接创建角色 - ) - - # 如果创建了新角色,更新角色列表 - if auto_result.get("new_characters"): - new_count = len(auto_result["new_characters"]) - logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新角色") - - # 提交角色到数据库 - await db.commit() - - # 更新角色信息(供后续大纲生成使用) - characters.extend(auto_result["new_characters"]) - characters_info = _build_characters_info(characters) - else: - logger.info(f"✅ 【直接创建模式】AI判断无需引入新角色,继续生成大纲") - - except HTTPException: - raise - except Exception as e: - logger.error(f"⚠️ 【方案A】预测性角色引入失败: {e}", exc_info=True) - # 不阻断大纲生成流程 - - # 🏛️ 【组织引入】在生成大纲前预测并创建组织 - if request.enable_auto_organizations: - # 获取现有组织 - existing_organizations = await _get_existing_organizations(project.id, db) - - # 检查是否有用户确认的组织列表 - if request.confirmed_organizations: - # 直接使用用户确认的组织列表创建组织 - try: - from app.services.auto_organization_service import get_auto_organization_service - - logger.info(f"🏛️ 【确认模式】用户提供了 {len(request.confirmed_organizations)} 个确认的组织,直接创建") - - auto_org_service = get_auto_organization_service(user_ai_service) - - for org_data in request.confirmed_organizations: - try: - # 生成组织详细信息 - organization_data = await auto_org_service._generate_organization_details( - spec=org_data, - project=project, - existing_characters=list(characters), - existing_organizations=existing_organizations, - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp - ) - - # 创建组织记录 - org_character, organization = await auto_org_service._create_organization_record( - project_id=project.id, - organization_data=organization_data, - db=db - ) - - # 建立成员关系 - members_data = organization_data.get("initial_members", []) - if members_data: - await auto_org_service._create_member_relationships( - organization=organization, - member_specs=members_data, - existing_characters=list(characters), - project_id=project.id, - db=db - ) - - # 更新角色列表(组织也是Character) - characters.append(org_character) - existing_organizations.append({ - "id": organization.id, - "name": org_character.name, - "organization_type": org_character.organization_type, - "organization_purpose": org_character.organization_purpose, - "power_level": organization.power_level, - "location": organization.location, - "motto": organization.motto - }) - logger.info(f"✅ 创建确认的组织: {org_character.name}") - - except Exception as e: - logger.error(f"创建确认的组织失败: {e}", exc_info=True) - continue - - # 提交组织到数据库 - await db.commit() - - # 更新角色信息(供后续大纲生成使用) - characters_info = _build_characters_info(characters) - - logger.info(f"✅ 【确认模式】成功创建 {len(request.confirmed_organizations)} 个用户确认的组织") - - except Exception as e: - logger.error(f"⚠️ 【确认模式】创建确认组织失败: {e}", exc_info=True) - else: - # 根据 require_organization_confirmation 决定处理方式 - try: - from app.services.auto_organization_service import get_auto_organization_service - - # 构建已有章节概览 - all_chapters_brief_for_org_analysis = _build_chapters_brief(existing_outlines) - - auto_org_service = get_auto_organization_service(user_ai_service) - - if request.require_organization_confirmation: - # 🔮 预测模式:仅预测组织,不自动创建,需要用户确认 - logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新组织(需用户确认)") - - auto_result = await auto_org_service.analyze_and_create_organizations( - project_id=project.id, - outline_content="", # 预测模式不需要大纲内容 - existing_characters=list(characters), - existing_organizations=existing_organizations, - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp, - all_chapters_brief=all_chapters_brief_for_org_analysis, - start_chapter=last_chapter_number + 1, - chapter_count=total_chapters_to_generate, - plot_stage=request.plot_stage, - story_direction=request.story_direction or "自然延续", - preview_only=True # ✅ 仅预测不创建 - ) - - # 检查是否需要新组织 - if auto_result.get("needs_new_organizations") and auto_result.get("predicted_organizations"): - predicted_count = len(auto_result["predicted_organizations"]) - logger.warning( - f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新组织,需要用户确认!" - ) - - # 🚨 抛出特殊异常,包含预测的组织信息 - raise HTTPException( - status_code=449, # 449 Retry With - detail={ - "code": "ORGANIZATION_CONFIRMATION_REQUIRED", - "message": "续写需要引入新组织,请先确认组织信息", - "predicted_organizations": auto_result["predicted_organizations"], - "reason": auto_result.get("reason", "剧情发展需要新组织"), - "chapter_range": f"第{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}章" - } - ) - else: - logger.info(f"✅ 【预测模式】AI判断无需引入新组织,继续生成大纲") - else: - # 🚀 直接创建模式:预测后自动创建,无需用户确认 - logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新组织(无需确认)") - - auto_result = await auto_org_service.analyze_and_create_organizations( - project_id=project.id, - outline_content="", - existing_characters=list(characters), - existing_organizations=existing_organizations, - db=db, - user_id=user_id, - enable_mcp=request.enable_mcp, - all_chapters_brief=all_chapters_brief_for_org_analysis, - start_chapter=last_chapter_number + 1, - chapter_count=total_chapters_to_generate, - plot_stage=request.plot_stage, - story_direction=request.story_direction or "自然延续", - preview_only=False # ✅ 直接创建组织 - ) - - # 如果创建了新组织,更新角色列表 - if auto_result.get("new_organizations"): - new_count = len(auto_result["new_organizations"]) - logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新组织") - - # 提交组织到数据库 - await db.commit() - - # 更新角色信息(供后续大纲生成使用) - for org_item in auto_result["new_organizations"]: - org_char = org_item.get("character") - if org_char: - characters.append(org_char) - characters_info = _build_characters_info(characters) - else: - logger.info(f"✅ 【直接创建模式】AI判断无需引入新组织,继续生成大纲") - - except HTTPException: - raise - except Exception as e: - logger.error(f"⚠️ 【组织引入】预测性组织引入失败: {e}", exc_info=True) - # 不阻断大纲生成流程 - - # 批量生成 - all_new_outlines = [] - current_start_chapter = last_chapter_number + 1 - - for batch_num in range(total_batches): - # 计算当前批次的章节数 - remaining_chapters = total_chapters_to_generate - len(all_new_outlines) - current_batch_size = min(batch_size, remaining_chapters) - - logger.info(f"开始生成第{batch_num + 1}/{total_batches}批,章节范围: {current_start_chapter}-{current_start_chapter + current_batch_size - 1}") - - # 获取最新的大纲列表(包括之前批次生成的) - latest_result = await db.execute( - select(Outline) - .where(Outline.project_id == project.id) - .order_by(Outline.order_index) - ) - latest_outlines = latest_result.scalars().all() - - # 🚀 使用智能上下文构建(支持海量大纲) - smart_context = await _build_smart_outline_context( - latest_outlines=latest_outlines, - user_id=user_id, - project_id=project.id - ) - - # 组装上下文字符串 - all_chapters_brief = "" - if smart_context['story_skeleton']: - all_chapters_brief += smart_context['story_skeleton'] + "\n\n" - if smart_context['recent_summary']: - all_chapters_brief += smart_context['recent_summary'] + "\n\n" - - # 最近详细内容作为 recent_plot - recent_plot = smart_context['recent_detail'] - - # 日志统计 - stats = smart_context['stats'] - logger.info(f"📊 大纲上下文统计: 总数{stats['total']}, 骨架{stats['skeleton_samples']}, " - f"概要{stats['recent_summaries']}, 详细{stats['recent_details']}, " - f"长度{stats['total_length']}字符") - - # 🧠 构建记忆增强上下文(仅续写模式需要) - memory_context = None - try: - logger.info(f"🧠 为第{batch_num + 1}批构建记忆上下文...") - # 使用最近一章的大纲作为查询 - query_outline = latest_outlines[-1].content if latest_outlines else "" - memory_context = await memory_service.build_context_for_generation( - user_id=user_id, - project_id=project.id, - current_chapter=current_start_chapter, - chapter_outline=query_outline, - character_names=[c.name for c in characters] if characters else None - ) - logger.info(f"✅ 记忆上下文构建完成: {memory_context['stats']}") - except Exception as e: - logger.warning(f"⚠️ 记忆上下文构建失败,继续不使用记忆: {str(e)}") - memory_context = None - - # 设置用户信息以启用MCP - if user_id: - user_ai_service.user_id = user_id - user_ai_service.db_session = db - - # 使用标准续写提示词模板(支持记忆+MCP增强+自定义) - template = await PromptService.get_template("OUTLINE_CONTINUE", user_id, db) - prompt = PromptService.format_prompt( - template, - title=project.title, - theme=request.theme or project.theme or "未设定", - genre=request.genre or project.genre or "通用", - narrative_perspective=request.narrative_perspective, - chapter_count=current_batch_size, # 当前批次的章节数 - time_period=project.world_time_period or "未设定", - location=project.world_location or "未设定", - atmosphere=project.world_atmosphere or "未设定", - rules=project.world_rules or "未设定", - characters_info=characters_info or "暂无角色信息", - current_chapter_count=len(latest_outlines), - all_chapters_brief=all_chapters_brief, - recent_plot=recent_plot, - plot_stage_instruction=stage_instruction, - start_chapter=current_start_chapter, - end_chapter=current_start_chapter + current_batch_size - 1, - story_direction=request.story_direction or "自然延续", - requirements=request.requirements or "", - memory_context=memory_context, - mcp_references="" - ) - - # 调用AI生成当前批次(带重试机制) - logger.info(f"正在调用AI流式生成第{batch_num + 1}批...") - - max_retries = 2 - retry_count = 0 - outline_data = None - - while retry_count <= max_retries: - accumulated_text = "" - chunk_count = 0 - - # 第一次使用原始prompt,重试时添加格式强调 - current_prompt = prompt if retry_count == 0 else ( - prompt + "\n\n【重要提醒】请确保返回完整的JSON数组,不要截断。每个章节对象必须包含完整的title、summary等字段。" - ) - - async for chunk in user_ai_service.generate_text_stream( - prompt=current_prompt, - provider=request.provider, - model=request.model - ): - chunk_count += 1 - accumulated_text += chunk - - # 这里是非SSE接口,不需要发送chunk - - ai_content = accumulated_text - ai_response = {"content": ai_content} - - # 解析响应 - try: - outline_data = _parse_ai_response(ai_content, raise_on_error=True) - break # 解析成功,跳出循环 - - except JSONParseError as e: - retry_count += 1 - if retry_count > max_retries: - # 超过最大重试次数,使用fallback数据 - logger.error(f"❌ 第{batch_num + 1}批解析失败,已达最大重试次数({max_retries}),使用fallback数据") - outline_data = _parse_ai_response(ai_content, raise_on_error=False) - break - - logger.warning(f"⚠️ 第{batch_num + 1}批JSON解析失败(第{retry_count}次),正在重试...") - - # 保存当前批次的大纲 - batch_outlines = await _save_outlines( - project.id, outline_data, db, start_index=current_start_chapter - ) - - # 记录历史 - history = GenerationHistory( - project_id=project.id, - prompt=f"[批次{batch_num + 1}/{total_batches}] {str(prompt)[:500]}", - generated_content=json.dumps(ai_response, ensure_ascii=False) if isinstance(ai_response, dict) else ai_response, - model=request.model or "default" - ) - db.add(history) - - # 提交当前批次 - await db.commit() - - for outline in batch_outlines: - await db.refresh(outline) - - all_new_outlines.extend(batch_outlines) - current_start_chapter += current_batch_size - - logger.info(f"第{batch_num + 1}批生成完成,本批生成{len(batch_outlines)}章") - - - # 返回所有大纲(包括旧的和新的) - final_result = await db.execute( - select(Outline) - .where(Outline.project_id == project.id) - .order_by(Outline.order_index) - ) - all_outlines = final_result.scalars().all() - - logger.info(f"续写完成 - 共{total_batches}批,新增 {len(all_new_outlines)} 章,总计 {len(all_outlines)} 章") - return OutlineListResponse(total=len(all_outlines), items=all_outlines) - - class JSONParseError(Exception): """JSON解析失败异常,用于触发重试""" def __init__(self, message: str, original_content: str = ""): @@ -1447,7 +944,7 @@ async def _save_outlines( start_index: int = 1 ) -> List[Outline]: """ - 保存大纲到数据库 + 保存大纲到数据库(修复版:从structure中提取title和content保存到数据库) 如果项目为one-to-one模式,同时自动创建对应的章节 """ @@ -1461,22 +958,15 @@ async def _save_outlines( for idx, chapter_data in enumerate(outline_data): order_idx = chapter_data.get("chapter_number", start_index + idx) - title = chapter_data.get("title", f"第{order_idx}章") - # 优先使用summary,其次content - content = chapter_data.get("summary") or chapter_data.get("content", "") + # 🔧 修复:从structure中提取title和summary/content保存到数据库 + chapter_title = chapter_data.get("title", f"第{order_idx}章") + chapter_content = chapter_data.get("summary") or chapter_data.get("content", "") - # 如果有额外信息,添加到内容中 - if "key_events" in chapter_data: - content += f"\n\n关键事件:" + "、".join(chapter_data["key_events"]) - if "characters_involved" in chapter_data: - content += f"\n涉及角色:" + "、".join(chapter_data["characters_involved"]) - - # 创建大纲 outline = Outline( project_id=project_id, - title=title, - content=content, + title=chapter_title, # 从JSON中提取title + content=chapter_content, # 从JSON中提取summary或content structure=json.dumps(chapter_data, ensure_ascii=False), order_index=order_idx ) @@ -1490,11 +980,21 @@ async def _save_outlines( for outline in outlines: await db.refresh(outline) + # 🔧 从structure中提取title和summary用于创建章节 + try: + structure_data = json.loads(outline.structure) if outline.structure else {} + chapter_title = structure_data.get("title", f"第{outline.order_index}章") + chapter_summary = structure_data.get("summary") or structure_data.get("content", "") + except json.JSONDecodeError: + logger.warning(f"解析大纲 {outline.id} 的structure失败,使用默认值") + chapter_title = f"第{outline.order_index}章" + chapter_summary = "" + # 为每个大纲创建对应的章节 chapter = Chapter( project_id=project_id, - title=outline.title, - summary=outline.content, + title=chapter_title, + summary=chapter_summary, chapter_number=outline.order_index, sub_index=1, outline_id=None, # one-to-one模式不关联outline_id @@ -1561,7 +1061,6 @@ async def new_outline_generator( genre=data.get("genre") or project.genre or "通用", chapter_count=chapter_count, narrative_perspective=data.get("narrative_perspective") or "第三人称", - target_words=data.get("target_words") or project.target_words or 100000, time_period=project.world_time_period or "未设定", location=project.world_location or "未设定", atmosphere=project.world_atmosphere or "未设定", @@ -1570,7 +1069,7 @@ async def new_outline_generator( requirements=data.get("requirements") or "", mcp_references="" ) - + logger.debug(f"NEW提示词: {prompt}") # 添加调试日志 model_param = data.get("model") provider_param = data.get("provider") @@ -2329,49 +1828,25 @@ async def continue_outline_generator( ) latest_outlines = latest_result.scalars().all() - # 🚀 使用智能上下文构建(支持海量大纲) - smart_context = await _build_smart_outline_context( + # 🚀 使用新的简化上下文构建 + context = await _build_outline_continue_context( + project=project, latest_outlines=latest_outlines, - user_id=user_id, - project_id=project_id + characters=characters, + chapter_count=current_batch_size, + plot_stage=data.get("plot_stage", "development"), + story_direction=data.get("story_direction", "自然延续"), + requirements=data.get("requirements", ""), + db=db ) - # 组装上下文字符串 - all_chapters_brief = "" - if smart_context['story_skeleton']: - all_chapters_brief += smart_context['story_skeleton'] + "\n\n" - if smart_context['recent_summary']: - all_chapters_brief += smart_context['recent_summary'] + "\n\n" - - # 最近详细内容作为 recent_plot - recent_plot = smart_context['recent_detail'] - # 日志统计 - stats = smart_context['stats'] - logger.info(f"📊 批次{batch_num + 1}大纲上下文: 总数{stats['total']}, " - f"骨架{stats['skeleton_samples']}, 概要{stats['recent_summaries']}, " - f"详细{stats['recent_details']}, 长度{stats['total_length']}字符") + stats = context['stats'] + logger.info(f"📊 批次{batch_num + 1}大纲上下文: 总大纲{stats['total_outlines']}, " + f"最近{stats['recent_outlines_count']}章, " + f"角色{stats['characters_count']}个, " + f"长度{stats['total_length']}字符") - # 🧠 构建记忆增强上下文 - memory_context = None - try: - yield await tracker.generating( - current_chars=0, - estimated_total=estimated_chars_per_batch, - message="🧠 构建记忆上下文..." - ) - query_outline = latest_outlines[-1].content if latest_outlines else "" - memory_context = await memory_service.build_context_for_generation( - user_id=user_id, - project_id=project_id, - current_chapter=current_start_chapter, - chapter_outline=query_outline, - character_names=[c.name for c in characters] if characters else None - ) - logger.info(f"✅ 记忆上下文: {memory_context['stats']}") - except Exception as e: - logger.warning(f"⚠️ 记忆上下文构建失败: {str(e)}") - memory_context = None # 设置用户信息以启用MCP if user_id: user_ai_service.user_id = user_id @@ -2383,32 +1858,33 @@ async def continue_outline_generator( message=f"🤖 调用AI生成第{str(batch_num + 1)}批..." ) - # 使用标准续写提示词模板(支持记忆+MCP增强+自定义) + # 使用标准续写提示词模板(简化版) template = await PromptService.get_template("OUTLINE_CONTINUE", user_id, db) prompt = PromptService.format_prompt( template, + # 基础信息 title=project.title, - theme=data.get("theme") or project.theme or "未设定", - genre=data.get("genre") or project.genre or "通用", - narrative_perspective=data.get("narrative_perspective") or project.narrative_perspective or "第三人称", - chapter_count=current_batch_size, + theme=project.theme or "未设定", + genre=project.genre or "通用", + narrative_perspective=project.narrative_perspective or "第三人称", time_period=project.world_time_period or "未设定", location=project.world_location or "未设定", atmosphere=project.world_atmosphere or "未设定", rules=project.world_rules or "未设定", - characters_info=characters_info or "暂无角色信息", - current_chapter_count=len(latest_outlines), - all_chapters_brief=all_chapters_brief, - recent_plot=recent_plot, - plot_stage_instruction=stage_instruction, + # 上下文信息 + recent_outlines=context['recent_outlines'], + characters_info=context['characters_info'], + # 续写参数 + chapter_count=current_batch_size, start_chapter=current_start_chapter, end_chapter=current_start_chapter + current_batch_size - 1, + current_chapter_count=len(latest_outlines), + plot_stage_instruction=stage_instruction, story_direction=data.get("story_direction", "自然延续"), requirements=data.get("requirements", ""), - memory_context=memory_context, mcp_references="" ) - + logger.debug(f" 续写提示词: {prompt}") # 调用AI生成当前批次 model_param = data.get("model") provider_param = data.get("provider") diff --git a/backend/app/api/wizard_stream.py b/backend/app/api/wizard_stream.py index cbdeb4e..2abad27 100644 --- a/backend/app/api/wizard_stream.py +++ b/backend/app/api/wizard_stream.py @@ -355,6 +355,7 @@ async def career_system_generator( title=project.title, genre=project.genre or '未设定', theme=project.theme or '未设定', + description=project.description or '暂无简介', time_period=world_data.get('time_period', '未设定'), location=world_data.get('location', '未设定'), atmosphere=world_data.get('atmosphere', '未设定'), diff --git a/backend/app/services/chapter_context_service.py b/backend/app/services/chapter_context_service.py index 2966d1a..8f3e20b 100644 --- a/backend/app/services/chapter_context_service.py +++ b/backend/app/services/chapter_context_service.py @@ -19,48 +19,47 @@ logger = get_logger(__name__) @dataclass -class ChapterContext: +class OneToManyContext: """ - 章节上下文数据结构 + 1-N模式章节上下文数据结构 采用RTCO框架的分层设计: - - P0-核心(必须):大纲、衔接点、字数要求 - - P1-重要(按需):角色、情感基调、风格 - - P2-参考(条件触发):记忆、故事骨架、MCP资料、伏笔提醒 + - P0-核心:大纲、衔接锚点、字数要求 + - P1-重要:角色、情感基调、风格 + - P2-参考:记忆、故事骨架、伏笔提醒 """ - # === P0-核心信息(必须包含)=== - chapter_outline: str = "" # 本章大纲 - continuation_point: Optional[str] = None # 衔接锚点(增强版:含上一章摘要和结尾) - previous_chapter_summary: Optional[str] = None # 🔧 新增:上一章剧情摘要 - previous_chapter_events: Optional[List[str]] = None # 🔧 新增:上一章关键事件 - target_word_count: int = 3000 # 目标字数 - min_word_count: int = 2500 # 最小字数 - max_word_count: int = 4000 # 最大字数 - narrative_perspective: str = "第三人称" # 叙事视角 + # === P0-核心信息 === + chapter_outline: str = "" # 本章大纲(从expansion_plan构建) + continuation_point: Optional[str] = None # 衔接锚点 + previous_chapter_summary: Optional[str] = None # 上一章剧情摘要 + previous_chapter_events: Optional[List[str]] = None # 上一章关键事件 + target_word_count: int = 3000 + min_word_count: int = 2500 + max_word_count: int = 4000 + narrative_perspective: str = "第三人称" # === 本章基本信息 === - chapter_number: int = 1 # 章节序号 - chapter_title: str = "" # 章节标题 + chapter_number: int = 1 + chapter_title: str = "" # === 项目基本信息 === - title: str = "" # 书名 - genre: str = "" # 类型 - theme: str = "" # 主题 + title: str = "" + genre: str = "" + theme: str = "" - # === P1-重要信息(按需包含)=== - chapter_characters: str = "" # 本章涉及角色(精简) - emotional_tone: str = "" # 情感基调 - style_instruction: str = "" # 写作风格指令(摘要化) + # === P1-重要信息 === + chapter_characters: str = "" # 从character_focus筛选的角色 + emotional_tone: str = "" + style_instruction: str = "" - # === P2-参考信息(条件触发)=== - relevant_memories: Optional[str] = None # 相关记忆(精简版) - story_skeleton: Optional[str] = None # 故事骨架(50章+启用) - mcp_references: Optional[str] = None # MCP参考资料 - foreshadow_reminders: Optional[str] = None # 伏笔提醒(新增) + # === P2-参考信息 === + relevant_memories: Optional[str] = None + story_skeleton: Optional[str] = None # 50章+启用 + foreshadow_reminders: Optional[str] = None # === 元信息 === - context_stats: Dict[str, Any] = field(default_factory=dict) # 统计信息 + context_stats: Dict[str, Any] = field(default_factory=dict) def get_total_context_length(self) -> int: """计算总上下文长度""" @@ -74,9 +73,61 @@ class ChapterContext: return total -class ChapterContextBuilder: +@dataclass +class OneToOneContext: """ - 章节上下文构建器 + 1-1模式章节上下文数据结构 + + 采用RTCO框架的分层设计: + - P0-核心:从outline.structure提取的大纲、字数要求 + - P1-重要:上一章最后500字、从structure.characters获取的角色、本章职业体系 + - P2-参考:伏笔提醒、相关记忆(相关度>0.6) + """ + + # === P0-核心信息 === + chapter_outline: str = "" # 从outline.structure提取 + target_word_count: int = 3000 + min_word_count: int = 2500 + max_word_count: int = 4000 + narrative_perspective: str = "第三人称" + + # === 本章基本信息 === + chapter_number: int = 1 + chapter_title: str = "" + + # === 项目基本信息 === + title: str = "" + genre: str = "" + theme: str = "" + + # === P1-重要信息 === + continuation_point: Optional[str] = None # 上一章最后500字 + chapter_characters: str = "" # 从structure.characters获取 + chapter_careers: Optional[str] = None # 本章涉及的职业完整信息 + + # === P2-参考信息 === + foreshadow_reminders: Optional[str] = None + relevant_memories: Optional[str] = None # 相关度>0.6 + + # === 元信息 === + context_stats: Dict[str, Any] = field(default_factory=dict) + + def get_total_context_length(self) -> int: + """计算总上下文长度""" + total = 0 + for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters', + 'chapter_careers', 'foreshadow_reminders', 'relevant_memories']: + value = getattr(self, field_name, None) + if value: + total += len(value) + return total + + +# ==================== 1-N模式上下文构建器 ==================== + +class OneToManyContextBuilder: + """ + 1-N模式上下文构建器 实现动态裁剪逻辑,根据章节序号自动调整上下文复杂度: - 第1章:无前置上下文,仅提供大纲和角色 @@ -117,9 +168,9 @@ class ChapterContextBuilder: style_content: Optional[str] = None, target_word_count: int = 3000, temp_narrative_perspective: Optional[str] = None - ) -> ChapterContext: + ) -> OneToManyContext: """ - 构建章节生成所需的上下文 + 构建章节生成所需的上下文(1-N模式) Args: chapter: 章节对象 @@ -132,10 +183,10 @@ class ChapterContextBuilder: temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认) Returns: - ChapterContext: 结构化的上下文对象 + OneToManyContext: 结构化的上下文对象 """ chapter_number = chapter.chapter_number - logger.info(f"📝 开始构建章节上下文: 第{chapter_number}章") + logger.info(f"📝 [1-N模式] 开始构建章节上下文: 第{chapter_number}章") # 确定叙事视角 narrative_perspective = ( @@ -145,7 +196,7 @@ class ChapterContextBuilder: ) # 初始化上下文 - context = ChapterContext( + context = OneToManyContext( chapter_number=chapter_number, chapter_title=chapter.title or "", title=project.title or "", @@ -158,9 +209,7 @@ class ChapterContextBuilder: ) # === P0-核心信息(始终构建)=== - context.chapter_outline = await self._build_chapter_outline( - chapter, outline, project.outline_mode - ) + context.chapter_outline = self._build_chapter_outline_1n(chapter, outline) # === 衔接锚点(根据章节调整长度,增强版含摘要和事件)=== if chapter_number == 1: @@ -175,7 +224,7 @@ class ChapterContextBuilder: context.continuation_point = ending_info.get('ending_text') context.previous_chapter_summary = ending_info.get('summary') context.previous_chapter_events = ending_info.get('key_events') - logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符, 摘要: {len(context.previous_chapter_summary or '')}字符") + logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符") else: ending_info = await self._get_last_ending_enhanced( chapter, db, self.ENDING_LENGTH_NORMAL @@ -183,10 +232,10 @@ class ChapterContextBuilder: context.continuation_point = ending_info.get('ending_text') context.previous_chapter_summary = ending_info.get('summary') context.previous_chapter_events = ending_info.get('key_events') - logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符, 摘要: {len(context.previous_chapter_summary or '')}字符") + logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符") # === P1-重要信息 === - context.chapter_characters = await self._build_chapter_characters( + context.chapter_characters = await self._build_chapter_characters_1n( chapter, project, outline, db ) context.emotional_tone = self._extract_emotional_tone(chapter, outline) @@ -215,7 +264,7 @@ class ChapterContextBuilder: ) logger.info(f" ✅ 故事骨架: {len(context.story_skeleton or '')}字符") - # === P2-伏笔提醒(新增)=== + # === P2-伏笔提醒=== if self.foreshadow_service: context.foreshadow_reminders = await self._get_foreshadow_reminders( project.id, chapter_number, db @@ -225,6 +274,7 @@ class ChapterContextBuilder: # === 统计信息 === context.context_stats = { + "mode": "one-to-many", "chapter_number": chapter_number, "has_continuation": context.continuation_point is not None, "continuation_length": len(context.continuation_point or ""), @@ -235,36 +285,21 @@ class ChapterContextBuilder: "total_length": context.get_total_context_length() } - logger.info(f"📊 上下文构建完成: 总长度 {context.context_stats['total_length']} 字符") + logger.info(f"📊 [1-N模式] 上下文构建完成: 总长度 {context.context_stats['total_length']} 字符") return context - async def _build_chapter_outline( + def _build_chapter_outline_1n( self, chapter: Chapter, - outline: Optional[Outline], - outline_mode: str + outline: Optional[Outline] ) -> str: - """ - 构建本章大纲内容 - - Args: - chapter: 章节对象 - outline: 大纲对象 - outline_mode: 大纲模式(one-to-one/one-to-many) - - Returns: - 本章大纲文本 - """ - if outline_mode == 'one-to-one': - # 一对一模式:使用大纲的 content - return outline.content if outline else chapter.summary or '暂无大纲' - else: - # 一对多模式:优先使用 expansion_plan 的详细规划 - if chapter.expansion_plan: - try: - plan = json.loads(chapter.expansion_plan) - outline_content = f"""剧情摘要:{plan.get('plot_summary', '无')} + """构建1-N模式的章节大纲""" + # 优先使用 expansion_plan 的详细规划 + if chapter.expansion_plan: + try: + plan = json.loads(chapter.expansion_plan) + outline_content = f"""剧情摘要:{plan.get('plot_summary', '无')} 关键事件: {chr(10).join(f'- {event}' for event in plan.get('key_events', []))} @@ -273,50 +308,60 @@ class ChapterContextBuilder: 情感基调:{plan.get('emotional_tone', '未设定')} 叙事目标:{plan.get('narrative_goal', '未设定')} 冲突类型:{plan.get('conflict_type', '未设定')}""" - return outline_content - except json.JSONDecodeError: - pass - - # 回退到大纲内容 - return outline.content if outline else chapter.summary or '暂无大纲' + return outline_content + except json.JSONDecodeError: + pass + + # 回退到大纲内容 + return outline.content if outline else chapter.summary or '暂无大纲' - async def _get_last_ending( + async def _build_chapter_characters_1n( self, chapter: Chapter, - db: AsyncSession, - max_length: int - ) -> Optional[str]: - """ - 获取上一章结尾内容作为衔接锚点(旧版本,保留兼容性) - - Args: - chapter: 当前章节 - db: 数据库会话 - max_length: 最大长度 - - Returns: - 上一章结尾内容 - """ - if chapter.chapter_number <= 1: - return None - - # 查询上一章 - result = await db.execute( - select(Chapter) - .where(Chapter.project_id == chapter.project_id) - .where(Chapter.chapter_number == chapter.chapter_number - 1) + project: Project, + outline: Optional[Outline], + db: AsyncSession + ) -> str: + """构建1-N模式的角色信息(从expansion_plan提取character_focus)""" + # 获取所有角色 + characters_result = await db.execute( + select(Character).where(Character.project_id == project.id) ) - prev_chapter = result.scalar_one_or_none() + characters = characters_result.scalars().all() - if not prev_chapter or not prev_chapter.content: - return None + if not characters: + return "暂无角色信息" - # 提取结尾内容 - content = prev_chapter.content.strip() - if len(content) <= max_length: - return content + # 从expansion_plan中提取角色焦点 + filter_character_names = None + if chapter.expansion_plan: + try: + plan = json.loads(chapter.expansion_plan) + filter_character_names = plan.get('character_focus', []) + except json.JSONDecodeError: + pass - return content[-max_length:] + # 筛选角色 + if filter_character_names: + characters = [c for c in characters if c.name in filter_character_names] + + if not characters: + return "暂无相关角色" + + # 构建精简的角色信息 + char_lines = [] + for c in characters[:10]: + role_type = "主角" if c.role_type == "protagonist" else ( + "反派" if c.role_type == "antagonist" else "配角" + ) + personality_brief = "" + if c.personality: + personality_brief = c.personality[:50] + if len(c.personality) > 50: + personality_brief += "..." + char_lines.append(f"- {c.name}({role_type}): {personality_brief}") + + return "\n".join(char_lines) async def _get_last_ending_enhanced( self, @@ -324,22 +369,7 @@ class ChapterContextBuilder: db: AsyncSession, max_length: int ) -> Dict[str, Any]: - """ - 获取增强版衔接锚点(含上一章摘要和关键事件) - - 🔧 新增功能: - 1. 提取上一章结尾文本 - 2. 获取上一章剧情摘要(从记忆或expansion_plan) - 3. 提取上一章关键事件 - - Args: - chapter: 当前章节 - db: 数据库会话 - max_length: 最大长度 - - Returns: - 包含 ending_text, summary, key_events 的字典 - """ + """获取增强版衔接锚点(含上一章摘要和关键事件)""" result_info = { 'ending_text': None, 'summary': None, @@ -369,7 +399,6 @@ class ChapterContextBuilder: result_info['ending_text'] = content[-max_length:] # 2. 获取上一章摘要 - # 优先从记忆中获取 chapter_summary summary_result = await db.execute( select(StoryMemory.content) .where(StoryMemory.project_id == chapter.project_id) @@ -380,12 +409,10 @@ class ChapterContextBuilder: summary_mem = summary_result.scalar_one_or_none() if summary_mem: - result_info['summary'] = summary_mem[:300] # 限制长度 + result_info['summary'] = summary_mem[:300] elif prev_chapter.summary: - # 回退到章节的summary字段 result_info['summary'] = prev_chapter.summary[:300] elif prev_chapter.expansion_plan: - # 再回退到expansion_plan中的plot_summary try: plan = json.loads(prev_chapter.expansion_plan) result_info['summary'] = plan.get('plot_summary', '')[:300] @@ -398,115 +425,18 @@ class ChapterContextBuilder: plan = json.loads(prev_chapter.expansion_plan) key_events = plan.get('key_events', []) if key_events: - result_info['key_events'] = key_events[:5] # 最多5个事件 + result_info['key_events'] = key_events[:5] except json.JSONDecodeError: pass - # 如果没有从expansion_plan获取到,尝试从记忆中获取 - if not result_info['key_events']: - events_result = await db.execute( - select(StoryMemory.content) - .where(StoryMemory.project_id == chapter.project_id) - .where(StoryMemory.chapter_id == prev_chapter.id) - .where(StoryMemory.memory_type == 'plot_point') - .limit(5) - ) - event_mems = events_result.scalars().all() - if event_mems: - result_info['key_events'] = [e[:100] for e in event_mems] - return result_info - async def _build_chapter_characters( - self, - chapter: Chapter, - project: Project, - outline: Optional[Outline], - db: AsyncSession - ) -> str: - """ - 构建本章涉及的角色信息(精简版) - - 只返回本章相关的角色,而非全部角色 - - Args: - chapter: 章节对象 - project: 项目对象 - outline: 大纲对象 - db: 数据库会话 - - Returns: - 本章角色信息文本 - """ - # 获取所有角色 - characters_result = await db.execute( - select(Character).where(Character.project_id == project.id) - ) - characters = characters_result.scalars().all() - - if not characters: - return "暂无角色信息" - - # 提取本章相关角色名单 - filter_character_names = None - - # 从大纲或扩展计划中提取角色 - if project.outline_mode == 'one-to-one': - if outline and outline.structure: - try: - structure = json.loads(outline.structure) - filter_character_names = structure.get('characters', []) - except json.JSONDecodeError: - pass - else: - if chapter.expansion_plan: - try: - plan = json.loads(chapter.expansion_plan) - filter_character_names = plan.get('character_focus', []) - except json.JSONDecodeError: - pass - - # 筛选角色 - if filter_character_names: - characters = [c for c in characters if c.name in filter_character_names] - - if not characters: - return "暂无相关角色" - - # 构建精简的角色信息(每个角色最多100字符) - char_lines = [] - for c in characters[:10]: # 最多10个角色 - role_type = "主角" if c.role_type == "protagonist" else ( - "反派" if c.role_type == "antagonist" else "配角" - ) - - # 性格摘要(最多50字符) - personality_brief = "" - if c.personality: - personality_brief = c.personality[:50] - if len(c.personality) > 50: - personality_brief += "..." - - char_lines.append(f"- {c.name}({role_type}): {personality_brief}") - - return "\n".join(char_lines) - def _extract_emotional_tone( self, chapter: Chapter, outline: Optional[Outline] ) -> str: - """ - 提取本章情感基调 - - Args: - chapter: 章节对象 - outline: 大纲对象 - - Returns: - 情感基调描述 - """ - # 尝试从扩展计划中提取 + """提取本章情感基调""" if chapter.expansion_plan: try: plan = json.loads(chapter.expansion_plan) @@ -516,7 +446,6 @@ class ChapterContextBuilder: except json.JSONDecodeError: pass - # 尝试从大纲结构中提取 if outline and outline.structure: try: structure = json.loads(outline.structure) @@ -529,22 +458,13 @@ class ChapterContextBuilder: return "未设定" def _summarize_style(self, style_content: str) -> str: - """ - 将风格描述压缩为关键要点 - - Args: - style_content: 完整风格描述 - - Returns: - 摘要化的风格描述 - """ + """将风格描述压缩为关键要点""" if not style_content: return "" if len(style_content) <= self.STYLE_MAX_LENGTH: return style_content - # 简单截断(后续可以用AI提取关键词) return style_content[:self.STYLE_MAX_LENGTH] + "..." async def _get_relevant_memories( @@ -555,29 +475,11 @@ class ChapterContextBuilder: chapter_outline: str, limit: int = 3 ) -> Optional[str]: - """ - 获取与本章最相关的记忆(精简版) - - 策略: - 1. 仅检索与大纲语义最相关的记忆 - 2. 提高重要性阈值,过滤低质量记忆 - 3. 优先返回未回收的伏笔 - - Args: - user_id: 用户ID - project_id: 项目ID - chapter_number: 当前章节号 - chapter_outline: 本章大纲 - limit: 返回数量限制 - - Returns: - 格式化的记忆文本 - """ + """获取与本章最相关的记忆""" if not self.memory_service: return None try: - # 1. 语义检索相关记忆(提高阈值) relevant = await self.memory_service.search_memories( user_id=user_id, project_id=project_id, @@ -586,13 +488,11 @@ class ChapterContextBuilder: min_importance=self.MEMORY_IMPORTANCE_THRESHOLD ) - # 2. 检查即将到期的伏笔 foreshadows = await self._get_due_foreshadows( user_id, project_id, chapter_number, - lookahead=5 # 仅看5章内需要回收的 + lookahead=5 ) - # 3. 合并并格式化 return self._format_memories(relevant, foreshadows, max_length=500) except Exception as e: @@ -606,18 +506,7 @@ class ChapterContextBuilder: chapter_number: int, lookahead: int = 5 ) -> List[Dict[str, Any]]: - """ - 获取即将需要回收的伏笔 - - Args: - user_id: 用户ID - project_id: 项目ID - chapter_number: 当前章节号 - lookahead: 往前看的章节数 - - Returns: - 待回收伏笔列表 - """ + """获取即将需要回收的伏笔""" if not self.memory_service: return [] @@ -626,7 +515,6 @@ class ChapterContextBuilder: user_id, project_id, chapter_number ) - # 过滤:只保留埋下时间较长(超过lookahead章)的伏笔 due_foreshadows = [] for fs in foreshadows: meta = fs.get('metadata', {}) @@ -638,7 +526,7 @@ class ChapterContextBuilder: 'importance': meta.get('importance', 0.5) }) - return due_foreshadows[:2] # 最多2条 + return due_foreshadows[:2] except Exception as e: logger.error(f"❌ 获取待回收伏笔失败: {str(e)}") @@ -650,21 +538,10 @@ class ChapterContextBuilder: foreshadows: List[Dict[str, Any]], max_length: int = 500 ) -> str: - """ - 格式化记忆为简洁文本,严格限制长度 - - Args: - relevant: 相关记忆列表 - foreshadows: 待回收伏笔列表 - max_length: 最大长度 - - Returns: - 格式化的记忆文本 - """ + """格式化记忆为简洁文本""" lines = [] current_length = 0 - # 优先添加待回收伏笔 if foreshadows: lines.append("【待回收伏笔】") for fs in foreshadows[:2]: @@ -674,7 +551,6 @@ class ChapterContextBuilder: lines.append(text) current_length += len(text) - # 添加相关记忆 if relevant and current_length < max_length: lines.append("【相关记忆】") for mem in relevant: @@ -694,36 +570,72 @@ class ChapterContextBuilder: db: AsyncSession ) -> Optional[str]: """ - 获取伏笔提醒信息用于章节生成 + 获取伏笔提醒信息(增强版) 策略: - 1. 获取计划在本章或之前回收但未回收的伏笔(超期提醒) - 2. 获取已埋入且接近需要回收的伏笔(提前提醒) - 3. 获取本章计划埋入的伏笔(埋入提醒) - - Args: - project_id: 项目ID - chapter_number: 当前章节号 - db: 数据库会话 - - Returns: - 格式化的伏笔提醒文本 + 1. 本章必须回收的伏笔(target_resolve_chapter_number == chapter_number) + 2. 超期未回收的伏笔(target_resolve_chapter_number < chapter_number) + 3. 即将到期的伏笔(target_resolve_chapter_number 在未来3章内) """ if not self.foreshadow_service: return None try: - context_result = await self.foreshadow_service.build_chapter_context( + lines = [] + + # 1. 本章必须回收的伏笔 + must_resolve = await self.foreshadow_service.get_must_resolve_foreshadows( db=db, project_id=project_id, - chapter_number=chapter_number, - include_pending=True, - include_overdue=True, - lookahead=5 + chapter_number=chapter_number ) - context_text = context_result.get("context_text", "") - return context_text if context_text else None + if must_resolve: + lines.append("【🎯 本章必须回收的伏笔】") + for f in must_resolve: + lines.append(f"- {f.title}") + lines.append(f" 埋入章节:第{f.plant_chapter_number}章") + lines.append(f" 伏笔内容:{f.content[:100]}{'...' if len(f.content) > 100 else ''}") + if f.resolution_notes: + lines.append(f" 回收提示:{f.resolution_notes}") + lines.append("") + + # 2. 超期未回收的伏笔 + overdue = await self.foreshadow_service.get_overdue_foreshadows( + db=db, + project_id=project_id, + current_chapter=chapter_number + ) + + if overdue: + lines.append("【⚠️ 超期待回收伏笔】") + for f in overdue[:3]: # 最多显示3个 + overdue_chapters = chapter_number - (f.target_resolve_chapter_number or 0) + lines.append(f"- {f.title} [已超期{overdue_chapters}章]") + lines.append(f" 埋入章节:第{f.plant_chapter_number}章,原计划第{f.target_resolve_chapter_number}章回收") + lines.append(f" 伏笔内容:{f.content[:80]}...") + lines.append("") + + # 3. 即将到期的伏笔(未来3章内) + upcoming = await self.foreshadow_service.get_pending_resolve_foreshadows( + db=db, + project_id=project_id, + current_chapter=chapter_number, + lookahead=3 + ) + + # 过滤:只保留未来章节的,排除本章和超期的 + upcoming_filtered = [f for f in upcoming + if (f.target_resolve_chapter_number or 0) > chapter_number] + + if upcoming_filtered: + lines.append("【📋 即将到期的伏笔(仅供参考)】") + for f in upcoming_filtered[:3]: # 最多显示3个 + remaining = (f.target_resolve_chapter_number or 0) - chapter_number + lines.append(f"- {f.title}(计划第{f.target_resolve_chapter_number}章回收,还有{remaining}章)") + lines.append("") + + return "\n".join(lines) if lines else None except Exception as e: logger.error(f"❌ 获取伏笔提醒失败: {str(e)}") @@ -735,21 +647,10 @@ class ChapterContextBuilder: chapter_number: int, db: AsyncSession ) -> Optional[str]: - """ - 构建故事骨架(每N章采样) - - Args: - project_id: 项目ID - chapter_number: 当前章节号 - db: 数据库会话 - - Returns: - 故事骨架文本 - """ + """构建故事骨架(每N章采样)""" try: - # 获取所有已完成章节的摘要 result = await db.execute( - select(Chapter.chapter_number, Chapter.title) + select(Chapter.id, Chapter.chapter_number, Chapter.title) .where(Chapter.project_id == project_id) .where(Chapter.chapter_number < chapter_number) .where(Chapter.content != None) @@ -761,15 +662,13 @@ class ChapterContextBuilder: if not chapters: return None - # 采样:每N章取一个 skeleton_lines = ["【故事骨架】"] - for i, (ch_num, ch_title) in enumerate(chapters): + for i, (ch_id, ch_num, ch_title) in enumerate(chapters): if i % self.SKELETON_SAMPLE_INTERVAL == 0: - # 尝试获取章节摘要 summary_result = await db.execute( select(StoryMemory.content) .where(StoryMemory.project_id == project_id) - .where(StoryMemory.story_timeline == ch_num) + .where(StoryMemory.chapter_id == ch_id) .where(StoryMemory.memory_type == 'chapter_summary') .limit(1) ) @@ -790,121 +689,559 @@ class ChapterContextBuilder: return None -class FocusedMemoryRetriever: +# ==================== 1-1模式上下文构建器 ==================== + +class OneToOneContextBuilder: """ - 精简记忆检索器 + 1-1模式上下文构建器 - 相比原有的memory_service,提供更精准、更简洁的记忆检索 + 上下文构建策略: + P0核心信息: + 1. 从outline.structure的JSON中提取:summary, scenes, key_points, emotion, goal + 2. target_word_count + + P1重要信息: + 1. 上一章完整内容的最后500字作为参考 + 2. 根据structure中的characters获取角色信息(含职业) + + P2参考信息: + 1. 伏笔提醒 + 2. 根据角色名检索相关记忆(相关度>0.6) """ - def __init__(self, memory_service): + def __init__(self, memory_service=None, foreshadow_service=None): """ - 初始化检索器 + 初始化构建器 Args: - memory_service: 基础记忆服务实例 + memory_service: 记忆服务实例(可选) + foreshadow_service: 伏笔服务实例(可选) """ self.memory_service = memory_service + self.foreshadow_service = foreshadow_service - async def get_relevant_memories( + async def build( self, + chapter: Chapter, + project: Project, + outline: Optional[Outline], user_id: str, - project_id: str, - chapter_number: int, - chapter_outline: str, - limit: int = 3 - ) -> str: + db: AsyncSession, + target_word_count: int = 3000 + ) -> OneToOneContext: """ - 获取与本章最相关的记忆 - - 策略: - 1. 仅检索与大纲语义最相关的记忆 - 2. 提高重要性阈值,过滤低质量记忆 - 3. 优先返回未回收的伏笔 + 构建1-1模式上下文 Args: + chapter: 章节对象 + project: 项目对象 + outline: 大纲对象 user_id: 用户ID - project_id: 项目ID - chapter_number: 当前章节号 - chapter_outline: 本章大纲 - limit: 返回数量限制 - + db: 数据库会话 + target_word_count: 目标字数 + Returns: - 格式化的记忆文本 + OneToOneContext: 上下文对象 """ - # 1. 语义检索相关记忆(提高阈值) - relevant = await self.memory_service.search_memories( - user_id=user_id, - project_id=project_id, - query=chapter_outline, - limit=limit, - min_importance=0.7 # 从0.4提高到0.7 + chapter_number = chapter.chapter_number + logger.info(f"📝 [1-1模式] 开始构建上下文: 第{chapter_number}章") + + # 初始化上下文 + context = OneToOneContext( + chapter_number=chapter_number, + chapter_title=chapter.title or "", + title=project.title or "", + genre=project.genre or "", + theme=project.theme or "", + target_word_count=target_word_count, + min_word_count=max(500, target_word_count - 500), + max_word_count=target_word_count + 1000, + narrative_perspective=project.narrative_perspective or "第三人称" ) - # 2. 检查即将到期的伏笔 - due_foreshadows = await self._get_due_foreshadows( - user_id, project_id, chapter_number, - lookahead=5 # 仅看5章内需要回收的 - ) + # === P0-核心信息 === + context.chapter_outline = self._build_outline_from_structure(outline, chapter) + logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符") - # 3. 合并并格式化 - return self._format_memories(relevant, due_foreshadows, max_length=500) + # === P1-重要信息 === + # 1. 获取上一章内容的最后500字 + if chapter_number > 1: + prev_chapter_result = await db.execute( + select(Chapter) + .where(Chapter.project_id == chapter.project_id) + .where(Chapter.chapter_number == chapter_number - 1) + ) + prev_chapter = prev_chapter_result.scalar_one_or_none() + + if prev_chapter and prev_chapter.content: + content = prev_chapter.content.strip() + if len(content) <= 500: + context.continuation_point = content + else: + context.continuation_point = content[-500:] + logger.info(f" ✅ P1-上一章内容(最后500字): {len(context.continuation_point)}字符") + else: + context.continuation_point = None + logger.info(f" ⚠️ P1-上一章内容: 无") + else: + context.continuation_point = None + logger.info(f" ✅ P1-第1章无需上一章内容") + + # 2. 根据structure中的characters获取角色信息(含职业) + character_names = [] + if outline and outline.structure: + try: + structure = json.loads(outline.structure) + character_names = structure.get('characters', []) + logger.info(f" 📋 从structure提取角色: {character_names}") + except json.JSONDecodeError: + pass + + if character_names: + # 获取角色基本信息 + characters_result = await db.execute( + select(Character) + .where(Character.project_id == project.id) + .where(Character.name.in_(character_names)) + ) + characters = characters_result.scalars().all() + + if characters: + # 构建包含职业信息的角色上下文和职业详情 + characters_info, careers_info = await self._build_characters_and_careers( + db=db, + project_id=project.id, + characters=characters, + filter_character_names=character_names + ) + context.chapter_characters = characters_info + context.chapter_careers = careers_info + logger.info(f" ✅ P1-角色信息: {len(context.chapter_characters)}字符") + logger.info(f" ✅ P1-职业信息: {len(context.chapter_careers or '')}字符") + else: + context.chapter_characters = "暂无角色信息" + context.chapter_careers = None + logger.info(f" ⚠️ P1-角色信息: 筛选后无匹配角色") + else: + context.chapter_characters = "暂无角色信息" + context.chapter_careers = None + logger.info(f" ⚠️ P1-角色信息: 无") + + # === P2-参考信息 === + # 1. 伏笔提醒 + if self.foreshadow_service: + context.foreshadow_reminders = await self._get_foreshadow_reminders( + project.id, chapter_number, db + ) + if context.foreshadow_reminders: + logger.info(f" ✅ P2-伏笔提醒: {len(context.foreshadow_reminders)}字符") + else: + logger.info(f" ⚠️ P2-伏笔提醒: 无") + + # 2. 根据角色名检索相关记忆(相关度>0.6) + if character_names and self.memory_service: + try: + query_text = " ".join(character_names) + + relevant_memories = await self.memory_service.search_memories( + user_id=user_id, + project_id=project.id, + query=query_text, + limit=10, + min_importance=0.0 + ) + + filtered_memories = [ + mem for mem in relevant_memories + if mem.get('similarity', 0) > 0.6 + ] + + if filtered_memories: + memory_lines = ["【相关记忆】"] + for mem in filtered_memories: + similarity = mem.get('similarity', 0) + content = mem.get('content', '')[:100] + memory_lines.append(f"- (相关度:{similarity:.2f}) {content}") + + context.relevant_memories = "\n".join(memory_lines) + logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.6)") + else: + context.relevant_memories = None + logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆") + + except Exception as e: + logger.error(f" ❌ 检索相关记忆失败: {str(e)}") + context.relevant_memories = None + else: + context.relevant_memories = None + logger.info(f" ⚠️ P2-相关记忆: 无角色或记忆服务不可用") + + # === 统计信息 === + context.context_stats = { + "mode": "one-to-one", + "chapter_number": chapter_number, + "has_previous_content": context.continuation_point is not None, + "previous_content_length": len(context.continuation_point or ""), + "outline_length": len(context.chapter_outline), + "characters_length": len(context.chapter_characters), + "careers_length": len(context.chapter_careers or ""), + "foreshadow_length": len(context.foreshadow_reminders or ""), + "memories_length": len(context.relevant_memories or ""), + "total_length": context.get_total_context_length() + } + + logger.info(f"📊 [1-1模式] 上下文构建完成: 总长度 {context.context_stats['total_length']} 字符") + + return context - async def _get_due_foreshadows( + def _build_outline_from_structure( + self, + outline: Optional[Outline], + chapter: Chapter + ) -> str: + """从outline.structure提取大纲信息(1-1模式专用)""" + if outline and outline.structure: + try: + structure = json.loads(outline.structure) + + outline_parts = [] + + if structure.get('summary'): + outline_parts.append(f"【章节概要】\n{structure['summary']}") + + if structure.get('scenes'): + scenes_text = "\n".join([f"- {scene}" for scene in structure['scenes']]) + outline_parts.append(f"【场景设定】\n{scenes_text}") + + if structure.get('key_points'): + points_text = "\n".join([f"- {point}" for point in structure['key_points']]) + outline_parts.append(f"【情节要点】\n{points_text}") + + if structure.get('emotion'): + outline_parts.append(f"【情感基调】\n{structure['emotion']}") + + if structure.get('goal'): + outline_parts.append(f"【叙事目标】\n{structure['goal']}") + + return "\n\n".join(outline_parts) + + except json.JSONDecodeError as e: + logger.error(f" ❌ 解析outline.structure失败: {e}") + return outline.content if outline else "暂无大纲" + else: + return outline.content if outline else "暂无大纲" + + async def _build_characters_and_careers( + self, + db: AsyncSession, + project_id: str, + characters: list, + filter_character_names: Optional[list] = None + ) -> tuple[str, Optional[str]]: + """ + 构建角色信息和职业信息(1-1模式专用) + 获取角色的完整数据,并关联查询每个职业的完整数据 + 分别返回角色信息和职业信息 + + Args: + db: 数据库会话 + project_id: 项目ID + characters: 角色列表 + filter_character_names: 筛选的角色名称列表 + + Returns: + tuple: (角色信息字符串, 职业信息字符串) + """ + if not characters: + return '暂无角色信息', None + + # 如果提供了筛选名单,只保留匹配的角色 + if filter_character_names: + filtered_characters = [c for c in characters if c.name in filter_character_names] + if not filtered_characters: + logger.warning(f"筛选后无匹配角色,使用全部角色。筛选名单: {filter_character_names}") + filtered_characters = characters + else: + logger.info(f"根据筛选名单保留 {len(filtered_characters)}/{len(characters)} 个角色: {[c.name for c in filtered_characters]}") + characters = filtered_characters + + # 获取角色ID列表 + character_ids = [c.id for c in characters] + if not character_ids: + return '暂无角色信息', None + + # 重新查询角色的完整数据(确保获取所有字段) + full_characters_result = await db.execute( + select(Character).where(Character.id.in_(character_ids)) + ) + full_characters = {c.id: c for c in full_characters_result.scalars().all()} + + # 获取所有角色的职业关联数据 + character_careers_result = await db.execute( + select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids)) + ) + character_careers = character_careers_result.scalars().all() + + # 收集所有需要查询的职业ID + career_ids = set() + for cc in character_careers: + career_ids.add(cc.career_id) + + # 查询所有相关职业的完整数据 + careers_map = {} + if career_ids: + careers_result = await db.execute( + select(Career).where(Career.id.in_(list(career_ids))) + ) + careers_map = {c.id: c for c in careers_result.scalars().all()} + logger.info(f" 📋 查询到 {len(careers_map)} 个职业的完整数据") + + # 构建角色ID到职业关联数据的映射 + char_career_relations = {} + for cc in character_careers: + if cc.character_id not in char_career_relations: + char_career_relations[cc.character_id] = {'main': [], 'sub': []} + + # 保存完整的CharacterCareer对象 + if cc.career_type == 'main': + char_career_relations[cc.character_id]['main'].append(cc) + else: + char_career_relations[cc.character_id]['sub'].append(cc) + + # 构建角色信息字符串 + characters_info_parts = [] + for char_id in character_ids[:10]: # 限制最多10个角色 + c = full_characters.get(char_id) + if not c: + continue + + # === 角色基本信息 === + entity_type = '组织' if c.is_organization else '角色' + role_type_map = { + 'protagonist': '主角', + 'antagonist': '反派', + 'supporting': '配角' + } + role_type = role_type_map.get(c.role_type, c.role_type or '配角') + + # 构建基本信息行 + info_lines = [f"【{c.name}】({entity_type}, {role_type})"] + + # === 角色详细属性 === + if c.age: + info_lines.append(f" 年龄: {c.age}") + if c.gender: + info_lines.append(f" 性别: {c.gender}") + if c.appearance: + appearance_preview = c.appearance[:100] if len(c.appearance) > 100 else c.appearance + info_lines.append(f" 外貌: {appearance_preview}") + if c.personality: + personality_preview = c.personality[:100] if len(c.personality) > 100 else c.personality + info_lines.append(f" 性格: {personality_preview}") + if c.background: + background_preview = c.background[:150] if len(c.background) > 150 else c.background + info_lines.append(f" 背景: {background_preview}") + + # === 职业信息(完整数据)=== + if char_id in char_career_relations: + career_relations = char_career_relations[char_id] + + # 主职业 + if career_relations['main']: + for cc in career_relations['main']: + career = careers_map.get(cc.career_id) + if career: + # 解析职业的完整阶段信息 + try: + stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages + current_stage_info = None + for stage in stages: + if stage.get('level') == cc.current_stage: + current_stage_info = stage + break + + stage_name = current_stage_info.get('name', f'第{cc.current_stage}阶') if current_stage_info else f'第{cc.current_stage}阶' + except (json.JSONDecodeError, AttributeError, TypeError) as e: + logger.warning(f"解析职业阶段信息失败: {e}") + stage_name = f'第{cc.current_stage}阶' + stage_desc = '' + + # 构建主职业信息(只显示引用,详细信息在下面的"本章职业"部分) + info_lines.append(f" 主职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})") + + # 副职业 + if career_relations['sub']: + info_lines.append(f" 副职业:") + for cc in career_relations['sub']: + career = careers_map.get(cc.career_id) + if career: + # 解析副职业阶段信息 + try: + stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages + current_stage_info = None + for stage in stages: + if stage.get('level') == cc.current_stage: + current_stage_info = stage + break + stage_name = current_stage_info.get('name', f'第{cc.current_stage}阶') if current_stage_info else f'第{cc.current_stage}阶' + except (json.JSONDecodeError, AttributeError, TypeError): + stage_name = f'第{cc.current_stage}阶' + + # 副职业也只显示引用 + info_lines.append(f" - {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})") + + # === 组织特有信息 === + if c.is_organization: + if c.organization_type: + info_lines.append(f" 组织类型: {c.organization_type}") + if c.organization_purpose: + info_lines.append(f" 组织目的: {c.organization_purpose[:100]}") + if c.organization_members: + info_lines.append(f" 组织成员: {c.organization_members[:100]}") + + # 组合完整信息 + full_info = "\n".join(info_lines) + characters_info_parts.append(full_info) + + characters_result = "\n\n".join(characters_info_parts) + logger.info(f" ✅ 构建了 {len(characters_info_parts)} 个角色的完整信息,总长度: {len(characters_result)} 字符") + + # === 构建职业信息部分 === + careers_info_parts = [] + if careers_map: + for career_id, career in careers_map.items(): + career_lines = [f"{career.name} ({career.type}职业)"] + + # 职业描述 + if career.description: + career_lines.append(f" 描述: {career.description}") + + # 职业分类 + if career.category: + career_lines.append(f" 分类: {career.category}") + + # 阶段体系 + try: + stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages + if stages: + career_lines.append(f" 阶段体系: (共{career.max_stage}阶)") + for stage in stages: # 显示所有阶段 + level = stage.get('level', '?') + name = stage.get('name', '未命名') + desc = stage.get('description', '') + career_lines.append(f" {level}阶-{name}: {desc}") + except (json.JSONDecodeError, AttributeError, TypeError) as e: + logger.warning(f"解析职业阶段失败: {e}") + career_lines.append(f" 阶段体系: 共{career.max_stage}阶") + + # 职业要求 + if career.requirements: + career_lines.append(f" 职业要求: {career.requirements}") + + # 特殊能力 + if career.special_abilities: + career_lines.append(f" 特殊能力: {career.special_abilities}") + + # 世界观规则 + if career.worldview_rules: + career_lines.append(f" 世界观规则: {career.worldview_rules}") + + # 属性加成 + if career.attribute_bonuses: + try: + bonuses = json.loads(career.attribute_bonuses) if isinstance(career.attribute_bonuses, str) else career.attribute_bonuses + if bonuses: + bonus_str = ", ".join([f"{k}:{v}" for k, v in bonuses.items()]) + career_lines.append(f" 属性加成: {bonus_str}") + except (json.JSONDecodeError, AttributeError, TypeError): + pass + + careers_info_parts.append("\n".join(career_lines)) + + careers_result = None + if careers_info_parts: # 有职业数据就返回 + careers_result = "\n\n".join(careers_info_parts) + logger.info(f" ✅ 构建了 {len(careers_map)} 个职业的完整信息,总长度: {len(careers_result)} 字符") + else: + logger.info(f" ⚠️ 本章无涉及职业") + + return characters_result, careers_result + + async def _get_foreshadow_reminders( self, - user_id: str, project_id: str, chapter_number: int, - lookahead: int = 5 - ) -> List[Dict[str, Any]]: - """获取即将需要回收的伏笔""" - foreshadows = await self.memory_service.find_unresolved_foreshadows( - user_id, project_id, chapter_number - ) + db: AsyncSession + ) -> Optional[str]: + """ + 获取伏笔提醒信息(增强版) - # 过滤:只保留埋下时间较长的伏笔 - due_foreshadows = [] - for fs in foreshadows: - meta = fs.get('metadata', {}) - fs_chapter = meta.get('chapter_number', 0) - if chapter_number - fs_chapter >= lookahead: - due_foreshadows.append({ - 'chapter': fs_chapter, - 'content': fs.get('content', '')[:60], - 'importance': meta.get('importance', 0.5) - }) + 策略: + 1. 本章必须回收的伏笔(target_resolve_chapter_number == chapter_number) + 2. 超期未回收的伏笔(target_resolve_chapter_number < chapter_number) + 3. 即将到期的伏笔(target_resolve_chapter_number 在未来3章内) + """ + if not self.foreshadow_service: + return None - return due_foreshadows[:2] # 最多2条 - - def _format_memories( - self, - relevant: List[Dict[str, Any]], - foreshadows: List[Dict[str, Any]], - max_length: int = 500 - ) -> str: - """格式化为简洁文本,严格限制长度""" - lines = [] - current_length = 0 - - # 优先添加待回收伏笔 - if foreshadows: - lines.append("【待回收伏笔】") - for fs in foreshadows[:2]: - text = f"- 第{fs['chapter']}章埋下:{fs['content']}" - if current_length + len(text) > max_length: - break - lines.append(text) - current_length += len(text) - - # 添加相关记忆 - if relevant and current_length < max_length: - lines.append("【相关记忆】") - for mem in relevant: - content = mem.get('content', '')[:80] - text = f"- {content}" - if current_length + len(text) > max_length: - break - lines.append(text) - current_length += len(text) - - return "\n".join(lines) if lines else "" \ No newline at end of file + try: + lines = [] + + # 1. 本章必须回收的伏笔 + must_resolve = await self.foreshadow_service.get_must_resolve_foreshadows( + db=db, + project_id=project_id, + chapter_number=chapter_number + ) + + if must_resolve: + lines.append("【🎯 本章必须回收的伏笔】") + for f in must_resolve: + lines.append(f"- {f.title}") + lines.append(f" 埋入章节:第{f.plant_chapter_number}章") + lines.append(f" 伏笔内容:{f.content[:100]}{'...' if len(f.content) > 100 else ''}") + if f.resolution_notes: + lines.append(f" 回收提示:{f.resolution_notes}") + lines.append("") + + # 2. 超期未回收的伏笔 + overdue = await self.foreshadow_service.get_overdue_foreshadows( + db=db, + project_id=project_id, + current_chapter=chapter_number + ) + + if overdue: + lines.append("【⚠️ 超期待回收伏笔】") + for f in overdue[:3]: # 最多显示3个 + overdue_chapters = chapter_number - (f.target_resolve_chapter_number or 0) + lines.append(f"- {f.title} [已超期{overdue_chapters}章]") + lines.append(f" 埋入章节:第{f.plant_chapter_number}章,原计划第{f.target_resolve_chapter_number}章回收") + lines.append(f" 伏笔内容:{f.content[:80]}...") + lines.append("") + + # 3. 即将到期的伏笔(未来3章内) + upcoming = await self.foreshadow_service.get_pending_resolve_foreshadows( + db=db, + project_id=project_id, + current_chapter=chapter_number, + lookahead=3 + ) + + # 过滤:只保留未来章节的,排除本章和超期的 + upcoming_filtered = [f for f in upcoming + if (f.target_resolve_chapter_number or 0) > chapter_number] + + if upcoming_filtered: + lines.append("【📋 即将到期的伏笔(仅供参考)】") + for f in upcoming_filtered[:3]: # 最多显示3个 + remaining = (f.target_resolve_chapter_number or 0) - chapter_number + lines.append(f"- {f.title}(计划第{f.target_resolve_chapter_number}章回收,还有{remaining}章)") + lines.append("") + + return "\n".join(lines) if lines else None + + except Exception as e: + logger.error(f"❌ 获取伏笔提醒失败: {str(e)}") + return None + diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 4696278..b327dfb 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -294,7 +294,6 @@ class PromptService: 类型:{genre} 开篇章节数:{chapter_count} 叙事视角:{narrative_perspective} -全书目标字数:{target_words} @@ -327,9 +326,9 @@ class PromptService: {{ "chapter_number": 1, "title": "章节标题", - "summary": "章节概要(300-500字):主要情节、冲突、转折", + "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", "scenes": ["场景1描述", "场景2描述", "场景3描述"], - "characters": ["角色1", "角色2"], + "characters": ["涉及角色1", "涉及角色2"], "key_points": ["情节要点1", "情节要点2"], "emotion": "本章情感基调", "goal": "本章叙事目标" @@ -348,8 +347,9 @@ class PromptService: 【格式规范】 - 纯JSON数组输出,无markdown标记 -- 内容描述中严禁使用特殊符号(引号、方括号、书名号等) -- 专有名词、事件名直接书写 +- 内容描述中严禁使用特殊符号 +- 专有名词直接书写 +- 字段结构与已有章节完全一致 @@ -367,6 +367,7 @@ class PromptService: ✅ 符合类型:情节符合{genre}类型特征 ✅ 主题贴合:体现主题"{theme}" ✅ 开篇定位:是开局而非完整故事 +✅ 描述详细:每个summary 500-1000字 【禁止事项】 ❌ 输出markdown或代码块标记 @@ -375,7 +376,7 @@ class PromptService: ❌ 节奏过快,信息过载 """ - # 大纲续写提示词 V2(RTCO框架 + 记忆增强) + # 大纲续写提示词 V2(RTCO框架 - 简化版) OUTLINE_CONTINUE = """ 你是经验丰富的小说作家和编剧,擅长续写{genre}类型的小说大纲。 @@ -399,7 +400,7 @@ class PromptService: 叙事视角:{narrative_perspective} - + 【世界观】 时间背景:{time_period} 地理位置:{location} @@ -407,35 +408,27 @@ class PromptService: 世界规则:{rules} - -【角色信息】 + +{recent_outlines} + + + +【所有角色信息】 {characters_info} - -【已有章节概览】(共{current_chapter_count}章) -{all_chapters_brief} - -【最近剧情】 -{recent_plot} - - - -【🧠 智能记忆系统 - 续写参考】 -以下是从故事记忆库中检索到的相关信息: - -{memory_context} - + +【用户输入】 +续写章节数:{chapter_count}章 +情节阶段:{plot_stage_instruction} +故事方向:{story_direction} +其他要求:{requirements} + {mcp_references} - -【其他要求】 -{requirements} - - 【输出格式】 返回第{start_chapter}到第{end_chapter}章的JSON数组(共{chapter_count}个对象): @@ -444,7 +437,7 @@ class PromptService: {{ "chapter_number": {start_chapter}, "title": "章节标题", - "summary": "章节概要(300-500字):主要情节、角色互动、关键事件、冲突与转折", + "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", "scenes": ["场景1描述", "场景2描述", "场景3描述"], "characters": ["涉及角色1", "涉及角色2"], "key_points": ["情节要点1", "情节要点2"], @@ -473,16 +466,15 @@ class PromptService: 【续写要求】 ✅ 剧情连贯:与前文自然衔接,保持连贯性 -✅ 记忆参考:适当参考记忆中的伏笔、钩子、情节点 -✅ 伏笔回收:考虑回收未完结伏笔,制造呼应 -✅ 角色发展:遵循角色成长轨迹 +✅ 角色发展:遵循角色成长轨迹,充分利用角色信息 ✅ 情节阶段:遵循{plot_stage_instruction}的要求 ✅ 风格一致:保持与已有章节相同风格和详细程度 +✅ 大纲详细:充分解析最近10章大纲的structure字段信息 【必须遵守】 ✅ 数量精确:数组包含{chapter_count}个章节 ✅ 编号正确:从第{start_chapter}章开始 -✅ 描述详细:每个summary 100-200字 +✅ 描述详细:每个summary 500-1000字 ✅ 承上启下:自然衔接前文 【禁止事项】 @@ -490,10 +482,11 @@ class PromptService: ❌ 在描述中使用特殊符号 ❌ 与前文矛盾或脱节 ❌ 忽略已有角色发展 +❌ 忽略最近大纲中的情节线索 """ - # 章节生成V2 - 无前置章节版本(用于第1章) - CHAPTER_GENERATION_V2 = """ + # 章节生成 - 1-N模式(第1章) + CHAPTER_GENERATION_ONE_TO_MANY = """ 你是《{project_title}》的作者,一位专注于{genre}类型的网络小说家。 @@ -537,8 +530,127 @@ class PromptService: 现在开始创作: """ - # 章节生成V2 - 带前置章节版本(用于第2章及以后) - CHAPTER_GENERATION_V2_WITH_CONTEXT = """ + # 章节生成 - 1-1模式(第1章) + CHAPTER_GENERATION_ONE_TO_ONE = """ +你是《{project_title}》的作者,一位专注于{genre}类型的网络小说家。 + + + +【创作任务】 +撰写第{chapter_number}章《{chapter_title}》的完整正文。 + +【基本要求】 +- 目标字数:{target_word_count}字(允许±200字浮动) +- 叙事视角:{narrative_perspective} + + + +【本章大纲】 +{chapter_outline} + + + +【本章角色】 +{characters_info} + + + +【本章职业】 +{chapter_careers} + + + +【必须遵守】 +✅ 严格按照大纲推进情节 +✅ 保持角色性格、说话方式一致 +✅ 字数需要严格控制在目标字数内 + +【禁止事项】 +❌ 输出章节标题、序号等元信息 +❌ 使用"总之"、"综上所述"等AI常见总结语 +❌ 添加作者注释或创作说明 +❌ 生成字数禁止超过目标字数 + + + +【输出规范】 +直接输出小说正文内容,从故事场景或动作开始。 +无需任何前言、后记或解释性文字。 + +现在开始创作: +""" + + # 章节生成 - 1-1模式(第2章及以后) + CHAPTER_GENERATION_ONE_TO_ONE_NEXT = """ +你是《{project_title}》的作者,一位专注于{genre}类型的网络小说家。 + + + +【创作任务】 +撰写第{chapter_number}章《{chapter_title}》的完整正文。 + +【基本要求】 +- 目标字数:{target_word_count}字(允许±200字浮动) +- 叙事视角:{narrative_perspective} + + + +【本章大纲】 +{chapter_outline} + + + +【上一章末尾500字内容】 +{previous_chapter_content} + + + +【本章角色】 +{characters_info} + + + +【本章职业】 +{chapter_careers} + + + +【🎯 伏笔提醒】 +{foreshadow_reminders} + + + +【相关记忆】 +{relevant_memories} + + + +【必须遵守】 +✅ 严格按照大纲推进情节 +✅ 自然承接上一章末尾内容,保持连贯性 +✅ 保持角色性格、说话方式一致 +✅ 字数需要严格控制在目标字数内 +✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔 + +【禁止事项】 +❌ 输出章节标题、序号等元信息 +❌ 使用"总之"、"综上所述"等AI常见总结语 +❌ 在结尾处使用开放式反问 +❌ 添加作者注释或创作说明 +❌ 重复上一章已发生的事件 +❌ 生成字数禁止超过目标字数 + + + +【输出规范】 +直接输出小说正文内容,从故事场景或动作开始。 +无需任何前言、后记或解释性文字。 + +现在开始创作: +""" + + # 章节生成 - 1-N模式(第2章及以后) + CHAPTER_GENERATION_ONE_TO_MANY_NEXT = """ 你是《{project_title}》的作者,一位专注于{genre}类型的网络小说家。 @@ -870,7 +982,7 @@ class PromptService: - **category**:分类(identity=身世/mystery=悬念/item=物品/relationship=关系/event=事件/ability=能力/prophecy=预言) - **is_long_term**:是否长线伏笔(跨10章以上回收为true) - **related_characters**:涉及的角色名列表 -- **estimated_resolve_chapter**:预估回收章节号(埋下时预估,回收时为当前章节) +- **estimated_resolve_chapter**:【必填】预估回收章节号(埋下时必须预估,回收时为当前章节) **3. 冲突分析 (Conflict)** - 冲突类型:人与人/人与己/人与环境/人与社会 @@ -1997,7 +2109,12 @@ class PromptService: 【设计任务】 -根据世界观信息,设计一个完整且合理的职业体系,包括主职业和副职业。 +根据世界观信息和项目简介,设计一个完整且合理的职业体系。 +职业体系必须与项目简介中的故事背景和角色设定高度契合。 + +【数量要求】 +- 主职业:精确生成3个 +- 副职业:精确生成2个 @@ -2005,6 +2122,9 @@ class PromptService: 书名:{title} 类型:{genre} 主题:{theme} +简介:{description} + +【世界观设定】 时间背景:{time_period} 地理位置:{location} 氛围基调:{atmosphere} @@ -2014,23 +2134,32 @@ class PromptService: 【设计要求】 -**1. 主职业(main_careers)** -- 根据世界观特点,决定需要多少个主职业 +**1. 主职业(main_careers)- 必须精确生成3个** - 主职业是角色的核心发展方向 -- 必须严格符合世界观规则 +- 必须严格符合世界观规则和简介中的故事背景 +- 3个主职业应该覆盖不同的发展路线(如:战斗型、智慧型、特殊型) - 每个主职业的阶段数量可以不同(体现职业复杂度差异) +- 职业设计要能支撑简介中描述的故事情节 -**2. 副职业(sub_careers)** -- 根据世界需要,决定需要多少个副职业 +**2. 副职业(sub_careers)- 必须精确生成2个** - 副职业包含生产、辅助、特殊技能类 +- 2个副职业应该具有互补性,丰富角色的多样性 - 每个副职业的阶段数量可以不同 - 不要让所有副职业都是相同的阶段数 +- 副职业要能为主职业提供辅助或增益 **3. 阶段设计(stages)** - 每个职业的stages数组长度必须等于max_stage - 阶段名称要符合世界观文化背景 - 阶段描述要体现明确的能力提升路径 - 确保职业间的阶段数量有差异 +- 主职业阶段数建议:8-12个 +- 副职业阶段数建议:5-8个 + +**4. 简介契合度** +- 职业体系必须与项目简介中的故事设定相匹配 +- 如果简介中提到特定职业或能力,优先设计相关职业 +- 职业的能力和特点要能支撑简介中的情节发展 @@ -2072,17 +2201,22 @@ class PromptService: 【必须遵守】 -✅ 职业数量和类型根据世界观自行决定 +✅ 主职业数量:必须精确生成3个,不多不少 +✅ 副职业数量:必须精确生成2个,不多不少 ✅ 不同职业的max_stage必须不同 -✅ 主职业阶段数建议:5-15个 -✅ 副职业阶段数建议:3-10个 +✅ 主职业阶段数建议:8-12个 +✅ 副职业阶段数建议:5-8个 ✅ stages数组长度必须等于max_stage ✅ 确保职业体系与世界观高度契合 +✅ 职业设计必须支撑项目简介中的故事情节 【禁止事项】 +❌ 生成超过3个主职业或少于3个主职业 +❌ 生成超过2个副职业或少于2个副职业 ❌ 所有职业使用相同的阶段数 ❌ 输出markdown标记 -❌ 职业设计与世界观脱节 +❌ 职业设计与世界观或简介脱节 +❌ 忽略简介中提到的职业或能力设定 """ # 局部重写提示词(RTCO框架) @@ -2441,7 +2575,7 @@ class PromptService: "parameters": ["project_context", "user_input"] }, "OUTLINE_CREATE": { - "name": "初始大纲生成", + "name": "大纲生成", "category": "大纲生成", "description": "根据项目信息生成完整的章节大纲", "parameters": ["title", "theme", "genre", "chapter_count", "narrative_perspective", "target_words", @@ -2456,21 +2590,36 @@ class PromptService: "all_chapters_brief", "recent_plot", "memory_context", "mcp_references", "plot_stage_instruction", "start_chapter", "end_chapter", "story_direction", "requirements"] }, - "CHAPTER_GENERATION_V2": { - "name": "章节创作V2(首章)", + "CHAPTER_GENERATION_ONE_TO_MANY": { + "name": "章节创作-1-N模式(第1章)", "category": "章节创作", - "description": "根据大纲创作章节内容(用于第1章,无前置章节)", + "description": "1-N模式:根据大纲创作章节内容(用于第1章,无前置章节)", "parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline", "target_word_count", "narrative_perspective", "characters_info"] }, - "CHAPTER_GENERATION_V2_WITH_CONTEXT": { - "name": "章节创作V2(续章)", + "CHAPTER_GENERATION_ONE_TO_MANY_NEXT": { + "name": "章节创作-1-N模式(第2章及以后)", "category": "章节创作", - "description": "基于前置章节内容创作新章节(用于第2章及以后)", + "description": "1-N模式:基于前置章节内容创作新章节(用于第2章及以后)", "parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline", "target_word_count", "narrative_perspective", "characters_info", "continuation_point", "foreshadow_reminders", "relevant_memories", "story_skeleton", "previous_chapter_summary"] }, + "CHAPTER_GENERATION_ONE_TO_ONE": { + "name": "章节创作-1-1模式(第1章)", + "category": "章节创作", + "description": "1-1模式:章节创作(用于第1章,无前置章节)", + "parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline", + "target_word_count", "narrative_perspective", "characters_info", "chapter_careers"] + }, + "CHAPTER_GENERATION_ONE_TO_ONE_NEXT": { + "name": "章节创作-1-1模式(第2章及以后)", + "category": "章节创作", + "description": "1-1模式:基于上一章内容创作新章节(用于第2章及以后)", + "parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline", + "target_word_count", "narrative_perspective", "previous_chapter_content", + "characters_info", "chapter_careers", "foreshadow_reminders", "relevant_memories"] + }, "CHAPTER_REGENERATION_SYSTEM": { "name": "章节重写系统提示", "category": "章节重写", @@ -2565,8 +2714,8 @@ class PromptService: "CAREER_SYSTEM_GENERATION": { "name": "职业体系生成", "category": "世界构建", - "description": "根据世界观自动生成完整的职业体系,包括主职业和副职业", - "parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules"] + "description": "根据世界观和项目简介自动生成完整的职业体系,包括主职业和副职业", + "parameters": ["title", "genre", "theme", "description", "time_period", "location", "atmosphere", "rules"] }, "INSPIRATION_TITLE_SYSTEM": { "name": "灵感模式-书名生成(系统提示词)",