From 26be04c32a4c2e4befa4b0ec76d5145d312f9042 Mon Sep 17 00:00:00 2001 From: xiamuceer-j Date: Thu, 12 Feb 2026 12:40:01 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E6=9B=B4=E6=96=B0=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E7=94=9F=E6=88=90/=E5=88=86=E6=9E=90=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E6=A8=A1=E6=9D=BF=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=85=B3=E7=B3=BB=E4=BA=92=E5=8A=A8=E7=BA=A6?= =?UTF-8?q?=E6=9D=9F=E5=92=8C=E7=BB=84=E7=BB=87=E7=8A=B6=E6=80=81=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E5=88=86=E6=9E=90=E7=BB=B4=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/plot_analyzer.py | 122 +++++--------- backend/app/services/prompt_service.py | 222 +++++++++++++++++++------ 2 files changed, 219 insertions(+), 125 deletions(-) diff --git a/backend/app/services/plot_analyzer.py b/backend/app/services/plot_analyzer.py index 350fedf..c13992a 100644 --- a/backend/app/services/plot_analyzer.py +++ b/backend/app/services/plot_analyzer.py @@ -38,7 +38,8 @@ class PlotAnalyzer: db: AsyncSession = None, max_retries: int = 3, existing_foreshadows: Optional[List[Dict[str, Any]]] = None, - on_retry: Optional[OnRetryCallback] = None + on_retry: Optional[OnRetryCallback] = None, + characters_info: str = "" ) -> Optional[Dict[str, Any]]: """ 分析单章内容(带重试机制) @@ -53,6 +54,7 @@ class PlotAnalyzer: max_retries: 最大重试次数,默认3次 existing_foreshadows: 已埋入的伏笔列表(用于回收匹配) on_retry: 重试时的回调函数,参数为 (当前重试次数, 最大重试次数, 等待秒数, 错误原因) + characters_info: 项目角色信息文本(用于角色名称匹配) Returns: 分析结果字典,失败返回None @@ -83,7 +85,8 @@ class PlotAnalyzer: title=title, word_count=word_count, content=analysis_content, - existing_foreshadows=foreshadows_text + existing_foreshadows=foreshadows_text, + characters_info=characters_info if characters_info else "(暂无角色信息)" ) last_error = None @@ -187,12 +190,13 @@ class PlotAnalyzer: def _format_existing_foreshadows(self, foreshadows: Optional[List[Dict[str, Any]]]) -> str: """ - 格式化已有伏笔列表,用于注入到分析提示词中(智能分类版) + 格式化已有伏笔列表,用于注入到分析提示词中 - 核心策略: - 1. 必须回收的伏笔 - 明确标注,要求AI识别回收 - 2. 超期的伏笔 - 提醒AI尽快回收 - 3. 未到期的伏笔 - 明确标注禁止提前回收 + 核心策略(重构版): + - 分层展示所有已埋入伏笔,让AI能识别"自然回收" + - 第1层:本章必须回收的伏笔(最详细) + - 第2层:超期伏笔(较详细) + - 第3层:其他已埋入伏笔(精简信息,供AI判断是否自然回收了) Args: foreshadows: 伏笔列表,每个包含 id, title, content, plant_chapter_number, resolve_status 等 @@ -203,98 +207,60 @@ class PlotAnalyzer: if not foreshadows: return "(暂无已埋入的伏笔)" - # 按回收状态分类 - must_resolve = [] # 本章必须回收 - overdue = [] # 已超期 - not_yet = [] # 尚未到期 - no_plan = [] # 无明确计划 - - for fs in foreshadows: - status = fs.get('resolve_status', 'no_plan') - if status == 'must_resolve_now': - must_resolve.append(fs) - elif status == 'overdue': - overdue.append(fs) - elif status == 'not_yet': - not_yet.append(fs) - else: - no_plan.append(fs) + # 分类伏笔 + must_resolve = [fs for fs in foreshadows if fs.get('resolve_status') == 'must_resolve_now'] + overdue = [fs for fs in foreshadows if fs.get('resolve_status') == 'overdue'] + others = [fs for fs in foreshadows if fs.get('resolve_status') not in ('must_resolve_now', 'overdue')] lines = [] - # 1. 本章必须回收的伏笔(最高优先级) + # === 第1层:本章必须回收的伏笔(最详细)=== if must_resolve: - lines.append("=" * 50) - lines.append("【🎯 本章必须回收的伏笔 - 请务必识别回收】") - lines.append("=" * 50) + lines.append("=" * 40) + lines.append("【🎯 本章必须回收的伏笔】") + lines.append("=" * 40) for i, fs in enumerate(must_resolve, 1): fs_id = fs.get('id', 'unknown') fs_title = fs.get('title', '未命名伏笔') - fs_content = fs.get('content', '')[:150] + fs_content = fs.get('content', '')[:200] plant_chapter = fs.get('plant_chapter_number', '?') + hint_text = fs.get('hint_text', '') lines.append(f"{i}. 【ID: {fs_id}】{fs_title}") - lines.append(f" ⚠️ 回收要求:必须在本章回收此伏笔") lines.append(f" 埋入章节:第{plant_chapter}章") - lines.append(f" 伏笔内容:{fs_content}{'...' if len(fs.get('content', '')) > 150 else ''}") - lines.append(f" 回收时请在 reference_foreshadow_id 中填写: {fs_id}") + lines.append(f" 伏笔内容:{fs_content}{'...' if len(fs.get('content', '')) > 200 else ''}") + if hint_text: + lines.append(f" 埋入暗示:{hint_text[:100]}") + lines.append(f" ⚠️ 回收时 reference_foreshadow_id 填写: {fs_id}") lines.append("") - # 2. 超期的伏笔(需要尽快处理) + # === 第2层:超期伏笔 === if overdue: - lines.append("-" * 50) - lines.append("【⚠️ 超期待回收伏笔 - 建议尽快回收】") - lines.append("-" * 50) - for i, fs in enumerate(overdue, 1): + lines.append("【⚠️ 超期未回收伏笔 - 如章节内容回收了请标记】") + for fs in overdue[:5]: fs_id = fs.get('id', 'unknown') - fs_title = fs.get('title', '未命名伏笔') - fs_content = fs.get('content', '')[:100] + fs_title = fs.get('title', '') plant_chapter = fs.get('plant_chapter_number', '?') - hint = fs.get('resolve_hint', '') - - lines.append(f"{i}. 【ID: {fs_id}】{fs_title}") - lines.append(f" 状态:{hint}") - lines.append(f" 埋入章节:第{plant_chapter}章") - lines.append(f" 内容:{fs_content}{'...' if len(fs.get('content', '')) > 100 else ''}") - lines.append("") - - # 3. 尚未到期的伏笔(禁止提前回收,仅作参考) - if not_yet: - lines.append("-" * 50) - lines.append("【📋 尚未到期的伏笔 - 仅供参考,请勿在本章回收】") - lines.append("-" * 50) - lines.append("⚠️ 以下伏笔尚未到计划回收时间,请勿提前回收!") + lines.append(f"- 【ID: {fs_id}】{fs_title}(第{plant_chapter}章埋入)") lines.append("") - for i, fs in enumerate(not_yet[:5], 1): # 最多显示5个 - fs_title = fs.get('title', '未命名伏笔') - target_chapter = fs.get('target_resolve_chapter_number', '?') - hint = fs.get('resolve_hint', '') - - lines.append(f"{i}. {fs_title}") - lines.append(f" 计划回收章节:第{target_chapter}章 | {hint}") - lines.append("") - - if len(not_yet) > 5: - lines.append(f" ... 还有 {len(not_yet) - 5} 个未到期伏笔") - lines.append("") - # 4. 无明确计划的伏笔(可根据剧情自然回收) - if no_plan: - lines.append("-" * 50) - lines.append("【📝 无明确计划的伏笔 - 可根据剧情自然回收】") - lines.append("-" * 50) - for i, fs in enumerate(no_plan[:3], 1): # 最多显示3个 + # === 第3层:其他已埋入伏笔(精简)=== + if others: + lines.append("【📋 其他已埋入伏笔 - 如章节内容自然回收了请标记】") + for fs in others[:10]: fs_id = fs.get('id', 'unknown') - fs_title = fs.get('title', '未命名伏笔') - fs_content = fs.get('content', '')[:80] + fs_title = fs.get('title', '') plant_chapter = fs.get('plant_chapter_number', '?') - - lines.append(f"{i}. 【ID: {fs_id}】{fs_title}") - lines.append(f" 埋入章节:第{plant_chapter}章") - lines.append(f" 内容:{fs_content}{'...' if len(fs.get('content', '')) > 80 else ''}") - lines.append("") + lines.append(f"- 【ID: {fs_id}】{fs_title}(第{plant_chapter}章埋入)") + if len(others) > 10: + lines.append(f" ... 还有{len(others) - 10}个伏笔未列出") + lines.append("") - return "\n".join(lines) if lines else "(暂无已埋入的伏笔)" + # 操作指引 + lines.append("提示:如果章节内容回收了上述任一伏笔,请在 foreshadows 数组中") + lines.append("添加 type='resolved' 的记录,并在 reference_foreshadow_id 填写对应ID。") + + return "\n".join(lines) def _parse_analysis_response(self, response: str) -> Optional[Dict[str, Any]]: """ diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 61dea2a..af89ff6 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -324,27 +324,37 @@ class PromptService: [ {{ - "chapter_number": 1, - "title": "章节标题", - "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", - "scenes": ["场景1描述", "场景2描述", "场景3描述"], - "characters": ["涉及角色1", "涉及角色2"], - "key_points": ["情节要点1", "情节要点2"], - "emotion": "本章情感基调", - "goal": "本章叙事目标" - }}, - {{ - "chapter_number": 2, - "title": "章节标题", - "summary": "章节概要...", - "scenes": ["场景1", "场景2"], - "characters": ["角色1", "角色2"], - "key_points": ["要点1", "要点2"], - "emotion": "情感基调", - "goal": "叙事目标" - }} + "chapter_number": 1, + "title": "章节标题", + "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", + "scenes": ["场景1描述", "场景2描述", "场景3描述"], + "characters": [ + {{"name": "角色名1", "type": "character"}}, + {{"name": "组织/势力名1", "type": "organization"}} + ], + "key_points": ["情节要点1", "情节要点2"], + "emotion": "本章情感基调", + "goal": "本章叙事目标" + }}, + {{ + "chapter_number": 2, + "title": "章节标题", + "summary": "章节概要...", + "scenes": ["场景1", "场景2"], + "characters": [ + {{"name": "角色名2", "type": "character"}}, + {{"name": "组织名2", "type": "organization"}} + ], + "key_points": ["要点1", "要点2"], + "emotion": "情感基调", + "goal": "叙事目标" + }} ] +【characters字段说明】 +- type为"character"表示个人角色,type为"organization"表示组织/势力/门派/帮派等 +- 必须区分角色和组织,不要把组织当作角色 + 【格式规范】 - 纯JSON数组输出,无markdown标记 - 内容描述中严禁使用特殊符号 @@ -435,27 +445,37 @@ class PromptService: [ {{ - "chapter_number": {start_chapter}, - "title": "章节标题", - "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", - "scenes": ["场景1描述", "场景2描述", "场景3描述"], - "characters": ["涉及角色1", "涉及角色2"], - "key_points": ["情节要点1", "情节要点2"], - "emotion": "本章情感基调", - "goal": "本章叙事目标" - }}, - {{ - "chapter_number": {start_chapter} + 1, - "title": "章节标题", - "summary": "章节概要...", - "scenes": ["场景1", "场景2"], - "characters": ["角色1", "角色2"], - "key_points": ["要点1", "要点2"], - "emotion": "情感基调", - "goal": "叙事目标" - }} + "chapter_number": {start_chapter}, + "title": "章节标题", + "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折", + "scenes": ["场景1描述", "场景2描述", "场景3描述"], + "characters": [ + {{"name": "角色名1", "type": "character"}}, + {{"name": "组织/势力名1", "type": "organization"}} + ], + "key_points": ["情节要点1", "情节要点2"], + "emotion": "本章情感基调", + "goal": "本章叙事目标" + }}, + {{ + "chapter_number": {start_chapter} + 1, + "title": "章节标题", + "summary": "章节概要...", + "scenes": ["场景1", "场景2"], + "characters": [ + {{"name": "角色名2", "type": "character"}}, + {{"name": "组织名2", "type": "organization"}} + ], + "key_points": ["要点1", "要点2"], + "emotion": "情感基调", + "goal": "叙事目标" + }} ] +【characters字段说明】 +- type为"character"表示个人角色,type为"organization"表示组织/势力/门派/帮派等 +- 必须区分角色和组织,不要把组织当作角色 + 【格式规范】 - 纯JSON数组输出,无markdown标记 - 内容描述中严禁使用特殊符号 @@ -505,21 +525,45 @@ class PromptService: -【本章角色】 +【本章角色 - 请严格遵循角色设定】 {characters_info} + +⚠️ 角色互动须知: +- 角色之间的对话和行为必须符合其关系设定(如师徒、敌对等) +- 涉及组织的情节须体现角色在组织中的身份和职位 +- 角色的能力表现须符合其职业和阶段设定 + +【本章职业】 +{chapter_careers} + + + +【🎯 伏笔提醒】 +{foreshadow_reminders} + + + +【相关记忆】 +{relevant_memories} + + 【必须遵守】 ✅ 严格按照大纲推进情节 ✅ 保持角色性格、说话方式一致 +✅ 角色互动须符合关系设定(师徒、朋友、敌对等) +✅ 组织相关情节须体现成员身份和职位层级 ✅ 字数控制在目标范围内 +✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔 【禁止事项】 ❌ 输出章节标题、序号等元信息 ❌ 使用"总之"、"综上所述"等AI常见总结语 ❌ 在结尾处使用开放式反问 ❌ 添加作者注释或创作说明 +❌ 角色行为超出其职业阶段的能力范围 @@ -559,11 +603,22 @@ class PromptService: {chapter_careers} + +【🎯 伏笔提醒】 +{foreshadow_reminders} + + + +【相关记忆】 +{relevant_memories} + + 【必须遵守】 ✅ 严格按照大纲推进情节 ✅ 保持角色性格、说话方式一致 ✅ 字数需要严格控制在目标字数内 +✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔 【禁止事项】 ❌ 输出章节标题、序号等元信息 @@ -599,6 +654,11 @@ class PromptService: {chapter_outline} + +【上一章剧情概要】 +{previous_chapter_summary} + + 【上一章末尾500字内容】 {previous_chapter_content} @@ -668,6 +728,11 @@ class PromptService: {chapter_outline} + +【最近章节规划 - 故事脉络参考】 +{recent_chapters_context} + + 【衔接锚点 - 必须承接】 上一章结尾: @@ -684,10 +749,20 @@ class PromptService: -【本章角色】 +【本章角色 - 请严格遵循角色设定】 {characters_info} + +⚠️ 角色互动须知: +- 角色之间的对话和行为必须符合其关系设定(如师徒、敌对等) +- 涉及组织的情节须体现角色在组织中的身份和职位 +- 角色的能力表现须符合其职业和阶段设定 + +【本章职业】 +{chapter_careers} + + 【🎯 伏笔提醒 - 需关注】 {foreshadow_reminders} @@ -698,16 +773,13 @@ class PromptService: {relevant_memories} - -【故事骨架 - 背景】 -{story_skeleton} - - 【必须遵守】 ✅ 严格按照大纲推进情节 ✅ 自然承接上一章结尾,不重复已发生事件 ✅ 保持角色性格、说话方式一致 +✅ 角色互动须符合关系设定(师徒、朋友、敌对等) +✅ 组织相关情节须体现成员身份和职位层级 ✅ 字数控制在目标范围内 ✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔 @@ -723,6 +795,7 @@ class PromptService: ❌ 添加作者注释或创作说明 ❌ 重复叙述上一章已发生的事件(包括环境描写、心理活动) ❌ 在开篇使用"接上回"、"书接上文"等套话 +❌ 角色行为超出其职业阶段的能力范围 @@ -947,6 +1020,13 @@ class PromptService: {existing_foreshadows} + +【项目角色信息 - 用于角色状态分析】 +以下是项目中已有的角色列表,分析 character_states 和 relationship_changes 时请使用这些角色的准确名称: + +{characters_info} + + 【分析维度】 @@ -1006,12 +1086,33 @@ class PromptService: - 关系变化 - 关键行动和决策 - 成长或退步 -- **💼 职业变化(可选)**: +- **💀 存活状态(重要)**: + - survival_status: 角色当前存活状态 + - 可选值:active(正常)/deceased(死亡)/missing(失踪)/retired(退场) + - 默认为null(表示无变化),仅当章节中角色明确死亡、失踪或永久退场时才填写 + - 死亡/失踪需要有明确的剧情依据,不可臆测 +- ** 职业变化(可选)**: - 仅当章节明确描述职业进展时填写 - main_career_stage_change: 整数(+1晋升/-1退步/0无变化) - sub_career_changes: 副职业变化数组 - new_careers: 新获得职业 - career_breakthrough: 突破过程描述 +- **🏛️ 组织变化(可选)**: + - 仅当章节明确描述角色与组织关系变化时填写 + - organization_changes: 组织变动数组 + - 每项包含:organization_name(组织名)、change_type(加入joined/离开left/晋升promoted/降级demoted/开除expelled/叛变betrayed)、new_position(新职位,可选)、loyalty_change(忠诚度变化描述,可选)、description(变化描述) + +**5b. 组织状态追踪 (Organization Status) - 可选** +仅当章节涉及组织势力变化时填写,分析出场组织的状态变化: +- 组织名称 +- 势力等级变化(power_change: 整数,+N增强/-N削弱/0无变化) +- 据点变化(new_location: 新据点,可选) +- 宗旨/目标变化(new_purpose: 新目标,可选) +- 组织状态描述(status_description: 当前状态概述) +- 关键事件(key_event: 触发变化的事件) +- **💀 组织存续状态(重要)**: + - is_destroyed: 组织是否被覆灭(true/false,默认false) + - 仅当章节明确描述组织被彻底消灭、瓦解、灭亡时设为true **6. 关键情节点 (Plot Points)** 列出3-5个核心情节点: @@ -1128,6 +1229,7 @@ class PromptService: "character_states": [ {{ "character_name": "张三", + "survival_status": null, "state_before": "犹豫", "state_after": "坚定", "psychological_change": "心理变化描述", @@ -1138,7 +1240,16 @@ class PromptService: "sub_career_changes": [{{"career_name": "炼丹", "stage_change": 1}}], "new_careers": [], "career_breakthrough": "突破描述" - }} + }}, + "organization_changes": [ + {{ + "organization_name": "某门派", + "change_type": "promoted", + "new_position": "长老", + "loyalty_change": "忠诚度提升", + "description": "因立下大功被提拔为长老" + }} + ] }} ], "plot_points": [ @@ -1157,6 +1268,17 @@ class PromptService: "duration": "时长估计" }} ], + "organization_states": [ + {{ + "organization_name": "某门派", + "power_change": -10, + "new_location": null, + "new_purpose": null, + "status_description": "因内乱势力受损,但核心力量未动摇", + "key_event": "长老叛变导致分支瓦解", + "is_destroyed": false + }} + ], "pacing": "varied", "dialogue_ratio": 0.4, "description_ratio": 0.3, @@ -1181,6 +1303,10 @@ class PromptService: ✅ 逐字复制:keyword必须从原文复制,长度8-25字 ✅ 精确定位:keyword能在原文中精确找到 ✅ 职业变化可选:仅当章节明确描述时填写 +✅ 组织变化可选:仅当章节明确描述角色与组织关系变动时填写(character_states中的organization_changes) +✅ 组织状态可选:仅当章节明确描述组织势力/据点/目标变化时填写(organization_states顶级字段) +✅ 存活状态谨慎:survival_status仅当章节有明确死亡/失踪/退场描写时填写,默认null +✅ 组织覆灭谨慎:is_destroyed仅当组织被彻底消灭时设true,组织受损不算覆灭 ✅ 【伏笔ID追踪】回收伏笔时,必须从【已埋入伏笔列表】中查找匹配的ID填入 reference_foreshadow_id 【评分约束 - 严格执行】 @@ -1199,6 +1325,8 @@ class PromptService: ❌ 输出markdown标记 ❌ 遗漏必填的keyword字段 ❌ 无根据地添加职业变化 +❌ 无根据地添加组织变化或组织状态变化 +❌ 无确切剧情依据地标记角色死亡或组织覆灭 ❌ 所有章节都打7-8分的"安全分" ❌ 高分章节给大量建议,或低分章节不给建议 """