From d102328b75e5172e29c2aa2ac4229764d9e137f1 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Sat, 29 Nov 2025 22:01:02 +0800 Subject: [PATCH] =?UTF-8?q?update:1.=E5=BC=80=E6=94=BE=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E6=8F=90=E7=A4=BA=E8=AF=8D=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=94=A8=E6=88=B7=E8=87=AA=E5=AE=9A=E4=B9=89=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 2 +- backend/app/api/chapters.py | 79 +- backend/app/api/characters.py | 15 +- backend/app/api/inspiration.py | 172 +--- backend/app/api/organizations.py | 15 +- backend/app/api/outlines.py | 28 +- backend/app/api/polish.py | 24 +- backend/app/api/prompt_templates.py | 478 +++++++++ backend/app/api/wizard_stream.py | 27 +- backend/app/main.py | 3 +- backend/app/models/prompt_template.py | 30 + backend/app/schemas/prompt_template.py | 68 ++ backend/app/services/chapter_regenerator.py | 147 +-- backend/app/services/mcp_test_service.py | 22 +- backend/app/services/plot_analyzer.py | 178 +--- .../app/services/plot_expansion_service.py | 323 ++----- backend/app/services/prompt_service.py | 913 ++++++++++++++++++ .../migration_add_prompt_templates.sql | 34 + frontend/package.json | 2 +- frontend/src/App.tsx | 2 + frontend/src/pages/Inspiration.tsx | 4 +- frontend/src/pages/ProjectList.tsx | 14 +- frontend/src/pages/PromptTemplates.tsx | 491 ++++++++++ 23 files changed, 2325 insertions(+), 746 deletions(-) create mode 100644 backend/app/api/prompt_templates.py create mode 100644 backend/app/models/prompt_template.py create mode 100644 backend/app/schemas/prompt_template.py create mode 100644 backend/scripts/migration_add_prompt_templates.sql create mode 100644 frontend/src/pages/PromptTemplates.tsx diff --git a/backend/.env.example b/backend/.env.example index d51729d..907da4f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,7 +8,7 @@ # 应用配置 # ========================================== APP_NAME=MuMuAINovel -APP_VERSION=1.0.0 +APP_VERSION=1.0.8 APP_HOST=0.0.0.0 APP_PORT=8000 DEBUG=false diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index 7a48afe..48ae04a 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -37,7 +37,7 @@ from app.schemas.regeneration import ( RegenerationTaskStatus ) from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService, WritingStyleManager from app.services.plot_analyzer import PlotAnalyzer from app.services.memory_service import memory_service from app.services.chapter_regenerator import ChapterRegenerator @@ -1236,9 +1236,11 @@ async def generate_chapter_content_stream( chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲' logger.warning(f"⚠️ 一对多模式但无expansion_plan,使用大纲内容") - # 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料 + # 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料(支持自定义) if previous_content: - prompt = prompt_service.get_chapter_generation_with_context_prompt( + template = await PromptService.get_template("CHAPTER_GENERATION_WITH_CONTEXT", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or '', genre=project.genre or '', @@ -1253,14 +1255,25 @@ async def generate_chapter_content_stream( chapter_number=current_chapter.chapter_number, chapter_title=current_chapter.title, chapter_outline=chapter_outline_content, - style_content=style_content, target_word_count=target_word_count, - memory_context=memory_context, - mcp_references=mcp_reference_materials, - outline_mode=outline_mode + max_word_count=target_word_count + 1000, + memory_context=memory_context.get('recent_context', '') + "\n" + memory_context.get('relevant_memories', '') + "\n" + memory_context.get('foreshadows', '') + "\n" + memory_context.get('character_states', '') + "\n" + memory_context.get('plot_points', '') if memory_context else "暂无相关记忆" ) + # 插入模式说明和MCP参考 + mode_instruction = "\n\n【创作模式说明】\n本章采用细纲模式:本章是大纲节点的细化展开之一。请严格遵循上述详细规划(expansion_plan)中的剧情点、角色焦点、情感基调和叙事目标,确保与整体规划保持一致,同时自然衔接前文内容。\n" if outline_mode == 'one-to-many' else "\n\n【创作模式说明】\n本章采用一对一模式:一个大纲节点对应一个章节。请在承接前文的基础上,充分展开大纲中的情节,保持叙事的完整性。\n" + mcp_text = "" + if mcp_reference_materials: + mcp_text = "\n【📚 MCP工具搜索 - 参考资料】\n以下是通过MCP工具搜索到的相关参考资料,可用于丰富情节和细节:\n\n" + mcp_reference_materials + "\n" + base_prompt = base_prompt.replace("本章信息:", mcp_text + mode_instruction + "\n本章信息:") + # 应用写作风格 + if style_content: + prompt = WritingStyleManager.apply_style_to_prompt(base_prompt, style_content) + else: + prompt = base_prompt else: - prompt = prompt_service.get_chapter_generation_prompt( + template = await PromptService.get_template("CHAPTER_GENERATION", current_user_id, db_session) + base_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or '', genre=project.genre or '', @@ -1274,12 +1287,23 @@ async def generate_chapter_content_stream( chapter_number=current_chapter.chapter_number, chapter_title=current_chapter.title, chapter_outline=chapter_outline_content, - style_content=style_content, target_word_count=target_word_count, - memory_context=memory_context, - mcp_references=mcp_reference_materials, - outline_mode=outline_mode + max_word_count=target_word_count + 1000 ) + # 插入模式说明和记忆、MCP参考 + mode_instruction = "\n\n【创作模式说明】\n本章采用细纲模式:本章是大纲节点的细化展开之一。请严格遵循上述详细规划中的剧情点、角色焦点和情感基调,确保与整体规划保持一致。\n" if outline_mode == 'one-to-many' else "\n\n【创作模式说明】\n本章采用一对一模式:一个大纲节点对应一个章节。请充分展开大纲中的情节,注重叙事的完整性和丰满度。\n" + memory_text = "" + if memory_context: + memory_text = "\n【🧠 智能记忆系统 - 重要参考】\n" + memory_context.get('recent_context', '') + "\n" + memory_context.get('relevant_memories', '') + "\n" + memory_context.get('foreshadows', '') + "\n" + memory_context.get('character_states', '') + "\n" + memory_context.get('plot_points', '') + mcp_text = "" + if mcp_reference_materials: + mcp_text = "\n【📚 MCP工具搜索 - 参考资料】\n以下是通过MCP工具搜索到的相关参考资料,可用于丰富情节和细节:\n\n" + mcp_reference_materials + "\n" + base_prompt = base_prompt.replace("本章信息:", memory_text + mcp_text + mode_instruction + "\n\n本章信息:") + # 应用写作风格 + if style_content: + prompt = WritingStyleManager.apply_style_to_prompt(base_prompt, style_content) + else: + prompt = base_prompt if mcp_reference_materials: logger.info(f"📖 已整合MCP参考资料({len(mcp_reference_materials)}字符)到章节生成提示词") @@ -2412,9 +2436,12 @@ async def generate_single_chapter_for_batch( chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲' logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容") - # 生成提示词 + # 生成提示词(支持自定义) if previous_content: - prompt = prompt_service.get_chapter_generation_with_context_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("CHAPTER_GENERATION_WITH_CONTEXT", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or '', genre=project.genre or '', @@ -2429,13 +2456,20 @@ async def generate_single_chapter_for_batch( chapter_number=chapter.chapter_number, chapter_title=chapter.title, chapter_outline=chapter_outline_content, - style_content=style_content, target_word_count=target_word_count, - memory_context=memory_context, - outline_mode=outline_mode + max_word_count=target_word_count + 1000, + memory_context=memory_context.get('recent_context', '') + "\n" + memory_context.get('relevant_memories', '') + "\n" + memory_context.get('foreshadows', '') + "\n" + memory_context.get('character_states', '') + "\n" + memory_context.get('plot_points', '') if memory_context else "暂无相关记忆" ) + # 应用写作风格 + if style_content: + prompt = WritingStyleManager.apply_style_to_prompt(base_prompt, style_content) + else: + prompt = base_prompt else: - prompt = prompt_service.get_chapter_generation_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("CHAPTER_GENERATION", user_id, db_session) + base_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or '', genre=project.genre or '', @@ -2449,11 +2483,14 @@ async def generate_single_chapter_for_batch( chapter_number=chapter.chapter_number, chapter_title=chapter.title, chapter_outline=chapter_outline_content, - style_content=style_content, target_word_count=target_word_count, - memory_context=memory_context, - outline_mode=outline_mode + max_word_count=target_word_count + 1000 ) + # 应用写作风格 + if style_content: + prompt = WritingStyleManager.apply_style_to_prompt(base_prompt, style_content) + else: + prompt = base_prompt # 非流式生成内容 full_content = "" diff --git a/backend/app/api/characters.py b/backend/app/api/characters.py index 8f64496..26826d1 100644 --- a/backend/app/api/characters.py +++ b/backend/app/api/characters.py @@ -19,7 +19,7 @@ from app.schemas.character import ( CharacterGenerateRequest ) from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger from app.api.settings import get_user_ai_service @@ -419,8 +419,11 @@ async def generate_character( - 其他要求:{request.requirements or '无'} """ - # 使用统一的提示词服务 - prompt = prompt_service.get_single_character_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("SINGLE_CHARACTER", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, project_context=project_context, user_input=user_input ) @@ -825,7 +828,11 @@ async def generate_character_stream( yield await SSEResponse.send_progress("构建AI提示词...", 20) - prompt = prompt_service.get_single_character_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("SINGLE_CHARACTER", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, project_context=project_context, user_input=user_input ) diff --git a/backend/app/api/inspiration.py b/backend/app/api/inspiration.py index 3bad1e7..625917c 100644 --- a/backend/app/api/inspiration.py +++ b/backend/app/api/inspiration.py @@ -1,5 +1,5 @@ """灵感模式API - 通过对话引导创建项目""" -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from typing import Dict, Any import json @@ -7,97 +7,13 @@ import json from app.database import get_db from app.services.ai_service import AIService from app.api.settings import get_user_ai_service +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger router = APIRouter(prefix="/inspiration", tags=["灵感模式"]) logger = get_logger(__name__) -# 灵感模式提示词模板 -INSPIRATION_PROMPTS = { - "title": { - "system": """你是一位专业的小说创作顾问。 -用户的原始想法:{initial_idea} - -请根据用户的想法,生成6个吸引人的书名建议,要求: -1. 紧扣用户的原始想法和核心故事构思 -2. 富有创意和吸引力 -3. 涵盖不同的风格倾向 - -返回JSON格式: -{{ - "prompt": "根据你的想法,我为你准备了几个书名建议:", - "options": ["书名1", "书名2", "书名3", "书名4", "书名5", "书名6"] -}} - -只返回纯JSON,不要有其他文字。""", - "user": "用户的想法:{initial_idea}\n请生成6个书名建议" - }, - - "description": { - "system": """你是一位专业的小说创作顾问。 -用户的原始想法:{initial_idea} -已确定的书名:{title} - -请生成6个精彩的小说简介,要求: -1. 必须紧扣用户的原始想法,确保简介是原始想法的具体展开 -2. 符合已确定的书名风格 -3. 简洁有力,每个50-100字 -4. 包含核心冲突 -5. 涵盖不同的故事走向,但都基于用户的原始构思 - -返回JSON格式: -{{"prompt":"选择一个简介:","options":["简介1","简介2","简介3","简介4","简介5","简介6"]}} - -只返回纯JSON,不要有其他文字,不要换行。""", - "user": "原始想法:{initial_idea}\n书名:{title}\n请生成6个简介选项" - }, - - "theme": { - "system": """你是一位专业的小说创作顾问。 -用户的原始想法:{initial_idea} -小说信息: -- 书名:{title} -- 简介:{description} - -请生成6个深刻的主题选项,要求: -1. 必须与用户的原始想法保持高度一致 -2. 符合书名和简介的风格 -3. 有深度和思想性 -4. 每个50-150字 -5. 涵盖不同角度(如:成长、复仇、救赎、探索等),但都围绕用户的核心构思 - -返回JSON格式: -{{"prompt":"这本书的核心主题是什么?","options":["主题1","主题2","主题3","主题4","主题5","主题6"]}} - -只返回纯JSON,不要有其他文字,不要换行。""", - "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n请生成6个主题选项" - }, - - "genre": { - "system": """你是一位专业的小说创作顾问。 -用户的原始想法:{initial_idea} -小说信息: -- 书名:{title} -- 简介:{description} -- 主题:{theme} - -请生成6个合适的类型标签(每个2-4字),要求: -1. 必须符合用户原始想法中暗示的类型倾向 -2. 符合小说整体风格 -3. 可以多选组合 - -常见类型:玄幻、都市、科幻、武侠、仙侠、历史、言情、悬疑、奇幻、修仙等 - -返回JSON格式: -{{"prompt":"选择类型标签(可多选):","options":["类型1","类型2","类型3","类型4","类型5","类型6"]}} - -只返回紧凑的纯JSON,不要换行,不要有其他文字。""", - "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n主题:{theme}\n请生成6个类型标签" - } -} - - # 不同阶段的temperature设置(递减以保持一致性) TEMPERATURE_SETTINGS = { "title": 0.8, # 书名阶段可以更有创意 @@ -153,6 +69,8 @@ def validate_options_response(result: Dict[str, Any], step: str, max_retries: in @router.post("/generate-options") async def generate_options( data: Dict[str, Any], + http_request: Request, + db: AsyncSession = Depends(get_db), ai_service: AIService = Depends(get_user_ai_service) ) -> Dict[str, Any]: """ @@ -183,28 +101,49 @@ async def generate_options( logger.info(f"灵感模式:生成{step}阶段的选项(第{attempt + 1}次尝试)") - # 获取对应的提示词模板 - if step not in INSPIRATION_PROMPTS: + # 获取用户ID + user_id = getattr(http_request.state, 'user_id', None) + + # 获取对应的提示词模板(根据step确定模板key) + template_key_map = { + "title": "INSPIRATION_TITLE", + "description": "INSPIRATION_DESCRIPTION", + "theme": "INSPIRATION_THEME", + "genre": "INSPIRATION_GENRE" + } + template_key = template_key_map.get(step) + + if not template_key: return { "error": f"不支持的步骤: {step}", "prompt": "", "options": [] } - prompt_template = INSPIRATION_PROMPTS[step] + # 获取自定义提示词模板 + prompt_template_str = await PromptService.get_template(template_key, user_id, db) - # 准备格式化参数(提供默认值避免KeyError) - # 关键改进:保持initial_idea在所有阶段传递,确保内容关联性 + # 准备格式化参数 format_params = { - "initial_idea": context.get("initial_idea", context.get("description", "")), # 优先使用initial_idea,兼容旧数据 + "initial_idea": context.get("initial_idea", context.get("description", "")), "title": context.get("title", ""), "description": context.get("description", ""), "theme": context.get("theme", "") } - # 格式化系统提示词 - system_prompt = prompt_template["system"].format(**format_params) - user_prompt = prompt_template["user"].format(**format_params) + # 格式化提示词(灵感模式的模板是特殊格式,包含system和user两部分) + # 尝试解析为JSON格式的字典 + try: + prompt_template = json.loads(prompt_template_str) + system_prompt = prompt_template["system"].format(**format_params) + user_prompt = prompt_template["user"].format(**format_params) + except (json.JSONDecodeError, KeyError): + # 如果不是JSON格式,降级使用原有方法 + prompt_template = prompt_service.get_inspiration_prompt(step) + if not prompt_template: + return {"error": f"无法获取提示词模板: {step}", "prompt": "", "options": []} + system_prompt = prompt_template["system"].format(**format_params) + user_prompt = prompt_template["user"].format(**format_params) # 如果是重试,在提示词中强调格式要求 if attempt > 0: @@ -302,6 +241,8 @@ async def generate_options( @router.post("/quick-generate") async def quick_generate( data: Dict[str, Any], + http_request: Request, + db: AsyncSession = Depends(get_db), ai_service: AIService = Depends(get_user_ai_service) ) -> Dict[str, Any]: """ @@ -326,6 +267,9 @@ async def quick_generate( try: logger.info("灵感模式:智能补全") + # 获取用户ID + user_id = getattr(http_request.state, 'user_id', None) + # 构建补全提示词 existing_info = [] if data.get("title"): @@ -339,35 +283,23 @@ async def quick_generate( existing_text = "\n".join(existing_info) if existing_info else "暂无信息" - system_prompt = """你是一位专业的小说创作顾问。用户提供了部分小说信息,请补全缺失的字段。 - -用户已提供的信息: -{existing} - -请生成完整的小说方案,包含: -1. title: 书名(3-6字,如果用户已提供则保持原样) -2. description: 简介(50-100字,必须基于用户提供的信息,不要偏离原意) -3. theme: 核心主题(30-50字,必须与用户提供的信息保持一致) -4. genre: 类型标签数组(2-3个) - -重要:所有补全的内容都必须与用户提供的信息保持高度关联,确保前后一致性。 - -返回JSON格式: -{{ - "title": "书名", - "description": "简介内容...", - "theme": "主题内容...", - "genre": ["类型1", "类型2"] -}} - -只返回纯JSON,不要有其他文字。""" + # 获取自定义提示词模板 + prompt_template_str = await PromptService.get_template("INSPIRATION_QUICK_COMPLETE", user_id, db) - user_prompt = "请补全小说信息" + # 格式化提示词 + try: + prompts = json.loads(prompt_template_str) + # 格式化参数 + prompts["system"] = prompts["system"].replace("{existing}", existing_text) + prompts["user"] = prompts["user"].replace("{existing}", existing_text) + except (json.JSONDecodeError, KeyError): + # 降级使用原有方法 + prompts = prompt_service.get_inspiration_quick_complete_prompt(existing=existing_text) # 调用AI response = await ai_service.generate_text( - prompt=user_prompt, - system_prompt=system_prompt.format(existing=existing_text), + prompt=prompts["user"], + system_prompt=prompts["system"], temperature=0.7 ) diff --git a/backend/app/api/organizations.py b/backend/app/api/organizations.py index 4f86b59..05ed465 100644 --- a/backend/app/api/organizations.py +++ b/backend/app/api/organizations.py @@ -24,7 +24,7 @@ from app.schemas.relationship import ( ) from app.schemas.character import CharacterResponse from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger from app.api.settings import get_user_ai_service @@ -496,8 +496,11 @@ async def generate_organization( - 其他要求:{gen_request.requirements or '无'} """ - # 使用统一的提示词服务 - prompt = prompt_service.get_single_organization_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("SINGLE_ORGANIZATION", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, project_context=project_context, user_input=user_input ) @@ -689,7 +692,11 @@ async def generate_organization_stream( yield await SSEResponse.send_progress("构建AI提示词...", 20) - prompt = prompt_service.get_single_organization_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("SINGLE_ORGANIZATION", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, project_context=project_context, user_input=user_input ) diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index e68de66..a5eb214 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -25,7 +25,7 @@ from app.schemas.outline import ( CreateChaptersFromPlansResponse ) from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.services.memory_service import memory_service from app.services.plot_expansion_service import PlotExpansionService from app.logger import get_logger @@ -477,8 +477,10 @@ async def _generate_new_outline( logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}") mcp_reference_materials = "" - # 使用完整提示词(插入MCP参考资料) - prompt = prompt_service.get_complete_outline_prompt( + # 使用完整提示词(插入MCP参考资料,支持自定义) + template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", 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 "通用", @@ -797,8 +799,10 @@ async def _continue_outline( logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,降级为基础模式: {str(e)}") mcp_reference_materials = "" - # 使用标准续写提示词模板(支持记忆+MCP增强) - prompt = prompt_service.get_outline_continue_prompt( + # 使用标准续写提示词模板(支持记忆+MCP增强+自定义) + template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", 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 "通用", @@ -814,6 +818,7 @@ async def _continue_outline( 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, @@ -1084,9 +1089,11 @@ async def new_outline_generator( logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}") mcp_reference_materials = "" - # 使用完整提示词(插入MCP参考资料) + # 使用完整提示词(插入MCP参考资料,支持自定义) yield await SSEResponse.send_progress("准备AI提示词...", 20) - prompt = prompt_service.get_complete_outline_prompt( + template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id_for_mcp, 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 "通用", @@ -1412,8 +1419,10 @@ async def continue_outline_generator( batch_progress + 5 ) - # 使用标准续写提示词模板(支持记忆+MCP增强) - prompt = prompt_service.get_outline_continue_prompt( + # 使用标准续写提示词模板(支持记忆+MCP增强+自定义) + template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", 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 "通用", @@ -1429,6 +1438,7 @@ async def continue_outline_generator( 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=data.get("story_direction", "自然延续"), requirements=data.get("requirements", ""), memory_context=memory_context, diff --git a/backend/app/api/polish.py b/backend/app/api/polish.py index 35767ff..3f02067 100644 --- a/backend/app/api/polish.py +++ b/backend/app/api/polish.py @@ -1,12 +1,12 @@ """AI去味API - 核心特色功能""" -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.generation_history import GenerationHistory from app.schemas.polish import PolishRequest, PolishResponse from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger from app.api.settings import get_user_ai_service @@ -17,6 +17,7 @@ logger = get_logger(__name__) @router.post("", response_model=PolishResponse, summary="AI去味") async def polish_text( request: PolishRequest, + http_request: Request, db: AsyncSession = Depends(get_db), user_ai_service: AIService = Depends(get_user_ai_service) ): @@ -32,8 +33,14 @@ async def polish_text( 这是本项目的核心特色功能! """ try: - # 构建AI去味提示词 - prompt = prompt_service.get_denoising_prompt( + # 获取用户ID + user_id = getattr(http_request.state, 'user_id', None) + + # 获取自定义提示词模板 + template = await PromptService.get_template("DENOISING", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, original_text=request.original_text ) @@ -85,6 +92,7 @@ async def polish_batch( project_id: int = None, provider: str = None, model: str = None, + http_request: Request = None, db: AsyncSession = Depends(get_db), user_ai_service: AIService = Depends(get_user_ai_service) ): @@ -94,12 +102,18 @@ async def polish_batch( 适用于一次性处理多个章节或段落 """ try: + # 获取用户ID + user_id = getattr(http_request.state, 'user_id', None) if http_request else None + results = [] for idx, text in enumerate(texts): logger.info(f"处理第 {idx+1}/{len(texts)} 个文本") - prompt = prompt_service.get_denoising_prompt(original_text=text) + # 获取自定义提示词模板 + template = await PromptService.get_template("DENOISING", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt(template, original_text=text) polished_text = await user_ai_service.generate_text( prompt=prompt, diff --git a/backend/app/api/prompt_templates.py b/backend/app/api/prompt_templates.py new file mode 100644 index 0000000..28629a9 --- /dev/null +++ b/backend/app/api/prompt_templates.py @@ -0,0 +1,478 @@ +"""提示词模板管理 API""" +from fastapi import APIRouter, HTTPException, Depends, Query, Request +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, delete +from typing import List, Optional +from datetime import datetime +import json + +from app.database import get_db +from app.models.prompt_template import PromptTemplate +from app.schemas.prompt_template import ( + PromptTemplateCreate, + PromptTemplateUpdate, + PromptTemplateResponse, + PromptTemplateListResponse, + PromptTemplateCategoryResponse, + PromptTemplateExport, + PromptTemplatePreviewRequest +) +from app.services.prompt_service import PromptService +from app.logger import get_logger + +logger = get_logger(__name__) + +router = APIRouter(prefix="/prompt-templates", tags=["提示词模板管理"]) + + +@router.get("", response_model=PromptTemplateListResponse) +async def get_all_templates( + request: Request, + category: Optional[str] = Query(None, description="按分类筛选"), + is_active: Optional[bool] = Query(None, description="按启用状态筛选"), + db: AsyncSession = Depends(get_db) +): + """ + 获取用户所有提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + query = select(PromptTemplate).where(PromptTemplate.user_id == user_id) + + if category: + query = query.where(PromptTemplate.category == category) + if is_active is not None: + query = query.where(PromptTemplate.is_active == is_active) + + query = query.order_by(PromptTemplate.category, PromptTemplate.template_key) + + result = await db.execute(query) + templates = result.scalars().all() + + # 获取所有分类 + categories_result = await db.execute( + select(PromptTemplate.category) + .where(PromptTemplate.user_id == user_id) + .distinct() + ) + categories = [c for c in categories_result.scalars().all() if c] + + return PromptTemplateListResponse( + templates=templates, + total=len(templates), + categories=sorted(categories) + ) + + +@router.get("/categories", response_model=List[PromptTemplateCategoryResponse]) +async def get_templates_by_category( + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 按分类获取提示词模板(合并用户自定义和系统默认) + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + # 1. 查询用户自定义模板 + result = await db.execute( + select(PromptTemplate) + .where(PromptTemplate.user_id == user_id) + .order_by(PromptTemplate.category, PromptTemplate.template_key) + ) + user_templates = result.scalars().all() + + # 2. 获取所有系统默认模板 + system_templates = PromptService.get_all_system_templates() + + # 3. 构建用户自定义模板的键集合 + user_template_keys = {t.template_key for t in user_templates} + + # 4. 合并模板:用户自定义的 + 未自定义的系统默认 + all_templates = [] + current_time = datetime.now() + + # 添加用户自定义的模板 + for user_template in user_templates: + user_template.is_system_default = False # 标记为已自定义 + all_templates.append(user_template) + + # 添加未自定义的系统默认模板 + for sys_template in system_templates: + if sys_template['template_key'] not in user_template_keys: + # 这个系统模板用户还没有自定义,创建临时对象 + template_obj = PromptTemplate( + id=sys_template['template_key'], # 使用template_key作为临时ID + user_id=user_id, + template_key=sys_template['template_key'], + template_name=sys_template['template_name'], + template_content=sys_template['content'], + description=sys_template['description'], + category=sys_template['category'], + parameters=json.dumps(sys_template['parameters']), + is_active=True, + is_system_default=True, + created_at=current_time, + updated_at=current_time + ) + all_templates.append(template_obj) + + # 5. 按分类分组 + category_dict = {} + for template in all_templates: + cat = template.category or "未分类" + if cat not in category_dict: + category_dict[cat] = [] + category_dict[cat].append(template) + + # 6. 构建响应 + response = [] + for category, temps in sorted(category_dict.items()): + # 按template_key排序,确保顺序一致 + temps.sort(key=lambda t: t.template_key) + response.append(PromptTemplateCategoryResponse( + category=category, + count=len(temps), + templates=temps + )) + + return response + + +@router.get("/system-defaults") +async def get_system_defaults( + request: Request +): + """ + 获取所有系统默认提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + # 从PromptService获取所有系统默认模板 + system_templates = PromptService.get_all_system_templates() + + return { + "templates": system_templates, + "total": len(system_templates) + } + + +@router.get("/{template_key}", response_model=PromptTemplateResponse) +async def get_template( + template_key: str, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 获取指定的提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_key + ) + ) + template = result.scalar_one_or_none() + + if not template: + raise HTTPException(status_code=404, detail=f"模板 {template_key} 不存在") + + return template + + +@router.post("", response_model=PromptTemplateResponse) +async def create_or_update_template( + data: PromptTemplateCreate, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 创建或更新提示词模板(Upsert) + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + # 查找现有模板 + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == data.template_key + ) + ) + template = result.scalar_one_or_none() + + if template: + # 更新现有模板 + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(template, key, value) + logger.info(f"用户 {user_id} 更新模板 {data.template_key}") + else: + # 创建新模板 + template = PromptTemplate( + user_id=user_id, + **data.model_dump() + ) + db.add(template) + logger.info(f"用户 {user_id} 创建模板 {data.template_key}") + + await db.commit() + await db.refresh(template) + + return template + + +@router.put("/{template_key}", response_model=PromptTemplateResponse) +async def update_template( + template_key: str, + data: PromptTemplateUpdate, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 更新提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_key + ) + ) + template = result.scalar_one_or_none() + + if not template: + raise HTTPException(status_code=404, detail=f"模板 {template_key} 不存在") + + # 更新模板 + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(template, key, value) + + await db.commit() + await db.refresh(template) + logger.info(f"用户 {user_id} 更新模板 {template_key}") + + return template + + +@router.delete("/{template_key}") +async def delete_template( + template_key: str, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 删除自定义提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_key + ) + ) + template = result.scalar_one_or_none() + + if not template: + raise HTTPException(status_code=404, detail=f"模板 {template_key} 不存在") + + await db.delete(template) + await db.commit() + logger.info(f"用户 {user_id} 删除模板 {template_key}") + + return {"message": "模板已删除", "template_key": template_key} + + +@router.post("/{template_key}/reset") +async def reset_to_default( + template_key: str, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 重置为系统默认模板(删除用户自定义版本) + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + # 验证系统默认模板是否存在 + system_template = PromptService.get_system_template_info(template_key) + if not system_template: + raise HTTPException(status_code=404, detail=f"系统默认模板 {template_key} 不存在") + + # 查找并删除用户的自定义模板 + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_key + ) + ) + template = result.scalar_one_or_none() + + if template: + await db.delete(template) + await db.commit() + logger.info(f"用户 {user_id} 删除自定义模板 {template_key},恢复为系统默认") + return {"message": "已重置为系统默认", "template_key": template_key} + else: + # 用户本来就没有自定义,已经是系统默认状态 + logger.info(f"用户 {user_id} 的模板 {template_key} 本来就是系统默认") + return {"message": "已是系统默认状态", "template_key": template_key} + + +@router.post("/export", response_model=PromptTemplateExport) +async def export_templates( + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 导出用户所有自定义模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + result = await db.execute( + select(PromptTemplate).where(PromptTemplate.user_id == user_id) + ) + templates = result.scalars().all() + + # 转换为导出格式 + export_data = [ + { + "template_key": t.template_key, + "template_name": t.template_name, + "template_content": t.template_content, + "description": t.description, + "category": t.category, + "parameters": t.parameters, + "is_active": t.is_active + } + for t in templates + ] + + logger.info(f"用户 {user_id} 导出了 {len(export_data)} 个模板") + + return PromptTemplateExport( + templates=export_data, + export_time=datetime.now() + ) + + +@router.post("/import") +async def import_templates( + data: PromptTemplateExport, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 导入提示词模板 + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + imported_count = 0 + updated_count = 0 + + for template_data in data.templates: + # 查找是否已存在 + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_data.template_key + ) + ) + existing = result.scalar_one_or_none() + + if existing: + # 更新现有模板 + for key, value in template_data.model_dump().items(): + setattr(existing, key, value) + updated_count += 1 + else: + # 创建新模板 + new_template = PromptTemplate( + user_id=user_id, + **template_data.model_dump() + ) + db.add(new_template) + imported_count += 1 + + await db.commit() + logger.info(f"用户 {user_id} 导入了 {imported_count} 个新模板,更新了 {updated_count} 个模板") + + return { + "message": "导入成功", + "imported": imported_count, + "updated": updated_count, + "total": imported_count + updated_count + } + + +@router.post("/{template_key}/preview") +async def preview_template( + template_key: str, + data: PromptTemplatePreviewRequest, + request: Request +): + """ + 预览提示词模板(渲染变量) + """ + # 从认证中间件获取用户ID + user_id = getattr(request.state, 'user_id', None) + if not user_id: + raise HTTPException(status_code=401, detail="未登录") + + try: + # 使用PromptService的format_prompt方法 + rendered = PromptService.format_prompt( + data.template_content, + **data.parameters + ) + + return { + "success": True, + "rendered_content": rendered, + "parameters_used": list(data.parameters.keys()) + } + except KeyError as e: + return { + "success": False, + "error": f"缺少必需的参数: {str(e)}", + "rendered_content": None + } + except Exception as e: + return { + "success": False, + "error": f"渲染失败: {str(e)}", + "rendered_content": None + } \ No newline at end of file diff --git a/backend/app/api/wizard_stream.py b/backend/app/api/wizard_stream.py index 3ff28ed..bb165f5 100644 --- a/backend/app/api/wizard_stream.py +++ b/backend/app/api/wizard_stream.py @@ -16,7 +16,7 @@ from app.models.writing_style import WritingStyle from app.models.project_default_style import ProjectDefaultStyle from app.services.ai_service import AIService from app.services.mcp_tool_service import MCPToolService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.services.plot_expansion_service import PlotExpansionService from app.logger import get_logger from app.utils.sse_response import SSEResponse, create_sse_response @@ -57,12 +57,14 @@ async def world_building_generator( yield await SSEResponse.send_error("title、description、theme 和 genre 是必需的参数", 400) return - # 获取基础提示词 + # 获取基础提示词(支持自定义) yield await SSEResponse.send_progress("准备AI提示词...", 15) - base_prompt = prompt_service.get_world_building_prompt( + template = await PromptService.get_template("WORLD_BUILDING", user_id, db) + base_prompt = PromptService.format_prompt( + template, title=title, theme=theme, - genre=genre + genre=genre or "通用类型" ) # MCP工具增强:收集参考资料 @@ -455,8 +457,11 @@ async def characters_generator( else: batch_requirements += "\n主要是配角(supporting)和反派(antagonist)" + # 获取自定义提示词模板 + template = await PromptService.get_template("CHARACTERS_BATCH", user_id, db) # 构建基础提示词 - base_prompt = prompt_service.get_characters_batch_prompt( + base_prompt = PromptService.format_prompt( + template, count=current_batch_size, # 传递精确数量 time_period=world_context.get("time_period", ""), location=world_context.get("location", ""), @@ -954,7 +959,10 @@ async def outline_generator( outline_requirements += "4. 不要试图完结故事,这只是开始部分\n" outline_requirements += "5. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记\n" - outline_prompt = prompt_service.get_complete_outline_prompt( + # 获取自定义提示词模板 + template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id, db) + outline_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or "未设定", genre=project.genre or "通用", @@ -966,6 +974,7 @@ async def outline_generator( atmosphere=project.world_atmosphere or "未设定", rules=project.world_rules or "未设定", characters_info=characters_info or "暂无角色信息", + mcp_references="", requirements=outline_requirements ) @@ -1150,9 +1159,11 @@ async def world_building_regenerate_generator( enable_mcp = data.get("enable_mcp", True) user_id = data.get("user_id") - # 获取基础提示词 + # 获取基础提示词(支持自定义) yield await SSEResponse.send_progress("准备AI提示词...", 15) - base_prompt = prompt_service.get_world_building_prompt( + template = await PromptService.get_template("WORLD_BUILDING", user_id, db) + base_prompt = PromptService.format_prompt( + template, title=project.title, theme=project.theme or "未设定", genre=project.genre or "通用" diff --git a/backend/app/main.py b/backend/app/main.py index 7dc1410..eed4af5 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -142,7 +142,7 @@ from app.api import ( projects, outlines, characters, chapters, wizard_stream, relationships, organizations, auth, users, settings, writing_styles, memories, - mcp_plugins, admin, inspiration + mcp_plugins, admin, inspiration, prompt_templates ) app.include_router(auth.router, prefix="/api") @@ -161,6 +161,7 @@ app.include_router(organizations.router, prefix="/api") app.include_router(writing_styles.router, prefix="/api") app.include_router(memories.router) # 记忆管理API (已包含/api前缀) app.include_router(mcp_plugins.router, prefix="/api") # MCP插件管理API +app.include_router(prompt_templates.router, prefix="/api") # 提示词模板管理API static_dir = Path(__file__).parent.parent / "static" if static_dir.exists(): diff --git a/backend/app/models/prompt_template.py b/backend/app/models/prompt_template.py new file mode 100644 index 0000000..ec4237c --- /dev/null +++ b/backend/app/models/prompt_template.py @@ -0,0 +1,30 @@ +"""提示词模板数据模型""" +from sqlalchemy import Column, String, Text, Boolean, DateTime, Index +from sqlalchemy.sql import func +from app.database import Base +import uuid + + +class PromptTemplate(Base): + """提示词模板表""" + __tablename__ = "prompt_templates" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id = Column(String(50), nullable=False, index=True, comment="用户ID") + template_key = Column(String(100), nullable=False, comment="模板键名") + template_name = Column(String(200), nullable=False, comment="模板显示名称") + template_content = Column(Text, nullable=False, comment="模板内容") + description = Column(Text, comment="模板描述") + category = Column(String(50), comment="模板分类") + parameters = Column(Text, comment="模板参数定义(JSON)") + is_active = Column(Boolean, default=True, comment="是否启用") + is_system_default = Column(Boolean, default=False, comment="是否为系统默认模板") + created_at = Column(DateTime, server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间") + + __table_args__ = ( + Index('idx_user_template', 'user_id', 'template_key', unique=True), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/app/schemas/prompt_template.py b/backend/app/schemas/prompt_template.py new file mode 100644 index 0000000..3115c49 --- /dev/null +++ b/backend/app/schemas/prompt_template.py @@ -0,0 +1,68 @@ +"""提示词模板相关的Pydantic模型""" +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional, List +from datetime import datetime + + +class PromptTemplateBase(BaseModel): + """提示词模板基础模型""" + template_key: str = Field(..., description="模板键名") + template_name: str = Field(..., description="模板显示名称") + template_content: str = Field(..., description="模板内容") + description: Optional[str] = Field(None, description="模板描述") + category: Optional[str] = Field(None, description="模板分类") + parameters: Optional[str] = Field(None, description="模板参数定义(JSON)") + is_active: bool = Field(True, description="是否启用") + + +class PromptTemplateCreate(PromptTemplateBase): + """创建提示词模板请求模型""" + pass + + +class PromptTemplateUpdate(BaseModel): + """更新提示词模板请求模型""" + template_name: Optional[str] = Field(None, description="模板显示名称") + template_content: Optional[str] = Field(None, description="模板内容") + description: Optional[str] = Field(None, description="模板描述") + category: Optional[str] = Field(None, description="模板分类") + parameters: Optional[str] = Field(None, description="模板参数定义(JSON)") + is_active: Optional[bool] = Field(None, description="是否启用") + + +class PromptTemplateResponse(PromptTemplateBase): + """提示词模板响应模型""" + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + is_system_default: bool + created_at: datetime + updated_at: datetime + + +class PromptTemplateListResponse(BaseModel): + """提示词模板列表响应""" + templates: List[PromptTemplateResponse] + total: int + categories: List[str] + + +class PromptTemplateCategoryResponse(BaseModel): + """提示词模板分类响应""" + category: str + count: int + templates: List[PromptTemplateResponse] + + +class PromptTemplateExport(BaseModel): + """提示词模板导出模型""" + templates: List[PromptTemplateBase] + export_time: datetime + version: str = "1.0" + + +class PromptTemplatePreviewRequest(BaseModel): + """提示词模板预览请求""" + template_content: str = Field(..., description="模板内容") + parameters: dict = Field(..., description="参数字典") \ No newline at end of file diff --git a/backend/app/services/chapter_regenerator.py b/backend/app/services/chapter_regenerator.py index 657b97c..db6ca69 100644 --- a/backend/app/services/chapter_regenerator.py +++ b/backend/app/services/chapter_regenerator.py @@ -1,7 +1,8 @@ """章节重新生成服务""" from typing import Dict, Any, AsyncGenerator, Optional, List +from sqlalchemy.ext.asyncio import AsyncSession from app.services.ai_service import AIService -from app.services.prompt_service import prompt_service +from app.services.prompt_service import prompt_service, PromptService from app.models.chapter import Chapter from app.models.memory import PlotAnalysis from app.schemas.regeneration import ChapterRegenerateRequest, PreserveElementsConfig @@ -24,7 +25,9 @@ class ChapterRegenerator: analysis: Optional[PlotAnalysis], regenerate_request: ChapterRegenerateRequest, project_context: Dict[str, Any], - style_content: str = "" + style_content: str = "", + user_id: str = None, + db: AsyncSession = None ) -> AsyncGenerator[Dict[str, Any], None]: """ 根据反馈重新生成章节(流式) @@ -34,6 +37,9 @@ class ChapterRegenerator: analysis: 分析结果(可选) regenerate_request: 重新生成请求参数 project_context: 项目上下文(项目信息、角色、大纲等) + style_content: 写作风格 + user_id: 用户ID(用于获取自定义提示词) + db: 数据库会话(用于查询自定义提示词) Yields: 包含类型和数据的字典: {'type': 'progress'/'chunk', 'data': ...} @@ -52,12 +58,14 @@ class ChapterRegenerator: # 2. 构建完整提示词 yield {'type': 'progress', 'progress': 10, 'message': '正在构建生成提示词...'} - full_prompt = self._build_regeneration_prompt( + full_prompt = await self._build_regeneration_prompt( chapter=chapter, modification_instructions=modification_instructions, project_context=project_context, regenerate_request=regenerate_request, - style_content=style_content + style_content=style_content, + user_id=user_id, + db=db ) logger.info(f"🎯 提示词构建完成,开始AI生成") @@ -158,126 +166,31 @@ class ChapterRegenerator: return "\n".join(instructions) - def _build_regeneration_prompt( + async def _build_regeneration_prompt( self, chapter: Chapter, modification_instructions: str, project_context: Dict[str, Any], regenerate_request: ChapterRegenerateRequest, - style_content: str = "" + style_content: str = "", + user_id: str = None, + db: AsyncSession = None ) -> str: """构建完整的重新生成提示词""" - - prompt_parts = [] - - # 系统角色 - prompt_parts.append("""你是一位经验丰富的专业小说编辑和作家。现在需要根据反馈意见重新创作一个章节。 - -你的任务是: -1. 仔细理解原章节的内容和意图 -2. 认真分析所有的修改要求 -3. 在保持故事连贯性的前提下,创作一个改进后的新版本 -4. 确保新版本在艺术性和可读性上都有明显提升 - ---- -""") - - # 原始章节信息 - prompt_parts.append(f"""## 📖 原始章节信息 - -**章节**:第{chapter.chapter_number}章 -**标题**:{chapter.title} -**字数**:{chapter.word_count}字 - -**原始内容**: -{chapter.content} - ---- -""") - - # 修改指令 - prompt_parts.append(modification_instructions) - prompt_parts.append("\n---\n") - - # 项目背景信息 - prompt_parts.append(f"""## 🌍 项目背景信息 - -**小说标题**:{project_context.get('project_title', '未知')} -**题材**:{project_context.get('genre', '未设定')} -**主题**:{project_context.get('theme', '未设定')} -**叙事视角**:{project_context.get('narrative_perspective', '第三人称')} -**世界观设定**: -- 时代背景:{project_context.get('time_period', '未设定')} -- 地理位置:{project_context.get('location', '未设定')} -- 氛围基调:{project_context.get('atmosphere', '未设定')} - ---- -""") - - # 角色信息 - if project_context.get('characters_info'): - prompt_parts.append(f"""## 👥 角色信息 - -{project_context['characters_info']} - ---- -""") - - # 章节大纲 - if project_context.get('chapter_outline'): - prompt_parts.append(f"""## 📝 本章大纲 - -{project_context['chapter_outline']} - ---- -""") - - # 前置章节上下文 - if project_context.get('previous_context'): - prompt_parts.append(f"""## 📚 前置章节上下文 - -{project_context['previous_context']} - ---- -""") - - # 写作风格要求(如果提供) - if style_content: - prompt_parts.append(f"""## 🎨 写作风格要求 - -{style_content} - -请在重新创作时严格遵循上述写作风格。 - ---- -""") - - # 创作要求 - prompt_parts.append(f"""## ✨ 创作要求 - -1. **解决问题**:针对上述修改指令中提到的所有问题进行改进 -2. **保持连贯**:确保与前后章节的情节、人物、风格保持一致 -3. **提升质量**:在节奏、情感、描写等方面明显优于原版 -4. **保留精华**:保持原章节中优秀的部分和关键情节 -5. **字数控制**:目标字数约{regenerate_request.target_word_count}字(可适当浮动±20%) -{f'6. **风格一致**:严格按照上述写作风格进行创作' if style_content else ''} - ---- - -## 🎬 开始创作 - -请现在开始创作改进后的新版本章节内容。 - -**重要提示**: -- 直接输出章节正文内容,从故事内容开始写 -- **不要**输出章节标题(如"第X章"、"第X章:XXX"等) -- **不要**输出任何额外的说明、注释或元数据 -- 只需要纯粹的故事正文内容 - -现在开始: -""") - - return "\n".join(prompt_parts) + # 获取自定义提示词模板 + template = await PromptService.get_template("CHAPTER_REGENERATION", user_id, db) + # 格式化提示词 + return PromptService.format_prompt( + template, + chapter_number=chapter.chapter_number, + title=chapter.title, + word_count=chapter.word_count, + content=chapter.content, + modification_instructions=modification_instructions, + project_context=project_context, + style_content=style_content, + target_word_count=regenerate_request.target_word_count + ) def calculate_content_diff( self, diff --git a/backend/app/services/mcp_test_service.py b/backend/app/services/mcp_test_service.py index e0fcd38..e8235f2 100644 --- a/backend/app/services/mcp_test_service.py +++ b/backend/app/services/mcp_test_service.py @@ -13,6 +13,7 @@ from app.models.settings import Settings as UserSettings from app.mcp.registry import mcp_registry from app.services.ai_service import create_user_ai_service from app.schemas.mcp_plugin import MCPTestResult +from app.services.prompt_service import prompt_service from app.logger import get_logger from app.user_manager import User @@ -168,26 +169,11 @@ class MCPTestService: logger.debug(f"📋 OpenAI工具列表: {[t['function']['name'] for t in openai_tools]}") # 调用AI选择工具 - prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。 - -⚠️ 重要规则:生成参数时,必须严格使用工具 schema 中定义的原始参数名称,不要转换为 snake_case 或其他格式。 -例如:如果 schema 中是 'nextThoughtNeeded',就必须使用 'nextThoughtNeeded',不能改成 'next_thought_needed'。 - -请选择一个合适的工具进行测试,优先选择搜索、查询类工具。 -生成真实有效的测试参数(例如搜索"人工智能最新进展"而不是"test")。 - -现在开始测试这个插件。""" - - system_prompt = """你是专业的API测试工具。当给定工具列表时,选择一个工具并使用合适的参数调用它。 - -⚠️ 关键规则:调用工具时,必须严格使用 schema 中定义的原始参数名,不要自行转换命名风格。 -- 如果参数名是 camelCase(如 nextThoughtNeeded),就使用 camelCase -- 如果参数名是 snake_case(如 next_thought),就使用 snake_case -- 保持与 schema 中定义的完全一致,包括大小写和命名风格""" + prompts = prompt_service.get_mcp_tool_test_prompts(plugin.plugin_name) ai_response = await ai_service.generate_text( - prompt=prompt, - system_prompt=system_prompt, + prompt=prompts["user"], + system_prompt=prompts["system"], tools=openai_tools, tool_choice="required" ) diff --git a/backend/app/services/plot_analyzer.py b/backend/app/services/plot_analyzer.py index abfe5d2..7c2e437 100644 --- a/backend/app/services/plot_analyzer.py +++ b/backend/app/services/plot_analyzer.py @@ -1,6 +1,8 @@ """剧情分析服务 - 自动分析章节的钩子、伏笔、冲突等元素""" from typing import Dict, Any, List, Optional +from sqlalchemy.ext.asyncio import AsyncSession from app.services.ai_service import AIService +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger import json import re @@ -11,169 +13,6 @@ logger = get_logger(__name__) class PlotAnalyzer: """剧情分析器 - 使用AI分析章节内容""" - # AI分析提示词模板 - ANALYSIS_PROMPT = """你是一位专业的小说编辑和剧情分析师。请深度分析以下章节内容: - -**章节信息:** -- 章节: 第{chapter_number}章 -- 标题: {title} -- 字数: {word_count}字 - -**章节内容:** -{content} - ---- - -**分析任务:** -请从专业编辑的角度,全面分析这一章节: - -### 1. 剧情钩子 (Hooks) - 吸引读者的元素 -识别能够吸引读者继续阅读的关键元素: -- **悬念钩子**: 未解之谜、疑问、谜团 -- **情感钩子**: 引发共鸣的情感点、触动心弦的时刻 -- **冲突钩子**: 矛盾对抗、紧张局势 -- **认知钩子**: 颠覆认知的信息、惊人真相 - -每个钩子需要: -- 类型分类 -- 具体内容描述 -- 强度评分(1-10) -- 出现位置(开头/中段/结尾) -- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! - -### 2. 伏笔分析 (Foreshadowing) -- **埋下的新伏笔**: 描述内容、预期作用、隐藏程度(1-10) -- **回收的旧伏笔**: 呼应哪一章、回收效果评分 -- **伏笔质量**: 巧妙性和合理性评估 -- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! - -### 3. 冲突分析 (Conflict) -- 冲突类型: 人与人/人与己/人与环境/人与社会 -- 冲突各方及其立场 -- 冲突强度评分(1-10) -- 冲突解决进度(0-100%) - -### 4. 情感曲线 (Emotional Arc) -- 主导情绪: 紧张/温馨/悲伤/激昂/平静等 -- 情感强度(1-10) -- 情绪变化轨迹描述 - -### 5. 角色状态追踪 (Character Development) -对每个出场角色分析: -- 心理状态变化(前→后) -- 关系变化 -- 关键行动和决策 -- 成长或退步 - -### 6. 关键情节点 (Plot Points) -列出3-5个核心情节点: -- 情节内容 -- 类型(revelation/conflict/resolution/transition) -- 重要性(0.0-1.0) -- 对故事的影响 -- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! - -### 7. 场景与节奏 -- 主要场景 -- 叙事节奏(快/中/慢) -- 对话与描写的比例 - -### 8. 质量评分 -- 节奏把控: 1-10分 -- 吸引力: 1-10分 -- 连贯性: 1-10分 -- 整体质量: 1-10分 - -### 9. 改进建议 -提供3-5条具体的改进建议 - ---- - -**输出格式(纯JSON,不要markdown标记):** - -{{ - "hooks": [ - {{ - "type": "悬念", - "content": "具体描述", - "strength": 8, - "position": "中段", - "keyword": "必须从原文逐字复制的文本片段" - }} - ], - "foreshadows": [ - {{ - "content": "伏笔内容", - "type": "planted", - "strength": 7, - "subtlety": 8, - "reference_chapter": null, - "keyword": "必须从原文逐字复制的文本片段" - }} - ], - "conflict": {{ - "types": ["人与人", "人与己"], - "parties": ["主角-复仇", "反派-维护现状"], - "level": 8, - "description": "冲突描述", - "resolution_progress": 0.3 - }}, - "emotional_arc": {{ - "primary_emotion": "紧张", - "intensity": 8, - "curve": "平静→紧张→高潮→释放", - "secondary_emotions": ["期待", "焦虑"] - }}, - "character_states": [ - {{ - "character_name": "张三", - "state_before": "犹豫", - "state_after": "坚定", - "psychological_change": "心理变化描述", - "key_event": "触发事件", - "relationship_changes": {{"李四": "关系改善"}} - }} - ], - "plot_points": [ - {{ - "content": "情节点描述", - "type": "revelation", - "importance": 0.9, - "impact": "推动故事发展", - "keyword": "必须从原文逐字复制的文本片段" - }} - ], - "scenes": [ - {{ - "location": "地点", - "atmosphere": "氛围", - "duration": "时长估计" - }} - ], - "pacing": "varied", - "dialogue_ratio": 0.4, - "description_ratio": 0.3, - "scores": {{ - "pacing": 8, - "engagement": 9, - "coherence": 8, - "overall": 8.5 - }}, - "plot_stage": "发展", - "suggestions": [ - "具体建议1", - "具体建议2" - ] -}} - -**重要提示:** -1. 每个钩子、伏笔、情节点的keyword字段是必填的,不能为空 -2. keyword必须是从章节原文中逐字复制的文本,长度8-25字 -3. keyword用于在前端标注文本位置,所以必须能在原文中精确找到 -4. 不要使用概括性语句或改写后的文字作为keyword - -只返回JSON,不要其他说明。""" - def __init__(self, ai_service: AIService): """ 初始化剧情分析器 @@ -189,7 +28,9 @@ class PlotAnalyzer: chapter_number: int, title: str, content: str, - word_count: int + word_count: int, + user_id: str = None, + db: AsyncSession = None ) -> Optional[Dict[str, Any]]: """ 分析单章内容 @@ -199,6 +40,8 @@ class PlotAnalyzer: title: 章节标题 content: 章节内容 word_count: 字数 + user_id: 用户ID(用于获取自定义提示词) + db: 数据库会话(用于查询自定义提示词) Returns: 分析结果字典,失败返回None @@ -209,8 +52,11 @@ class PlotAnalyzer: # 如果内容过长,截取前8000字(避免超token) analysis_content = content[:8000] if len(content) > 8000 else content - # 构建提示词 - prompt = self.ANALYSIS_PROMPT.format( + # 获取自定义提示词模板 + template = await PromptService.get_template("PLOT_ANALYSIS", user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, chapter_number=chapter_number, title=title, word_count=word_count, diff --git a/backend/app/services/plot_expansion_service.py b/backend/app/services/plot_expansion_service.py index 2b816dd..8c31b17 100644 --- a/backend/app/services/plot_expansion_service.py +++ b/backend/app/services/plot_expansion_service.py @@ -9,6 +9,7 @@ from app.models.project import Project from app.models.character import Character from app.models.chapter import Chapter from app.services.ai_service import AIService +from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger logger = get_logger(__name__) @@ -107,15 +108,27 @@ class PlotExpansionService: # 获取大纲上下文(前后大纲) context_info = await self._get_outline_context(outline, project.id, db) - # 构建分析提示词 - prompt = self._build_expansion_prompt( - outline=outline, - project=project, - characters_info=characters_info, + # 获取自定义提示词模板 + template = await PromptService.get_template("PLOT_EXPANSION_SINGLE_BATCH", project.user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, + project_title=project.title, + project_genre=project.genre or '通用', + project_theme=project.theme or '未设定', + project_narrative_perspective=project.narrative_perspective or '第三人称', + project_world_time_period=project.world_time_period or '未设定', + project_world_location=project.world_location or '未设定', + project_world_atmosphere=project.world_atmosphere or '未设定', + characters_info=characters_info or '暂无角色', + outline_order_index=outline.order_index, + outline_title=outline.title, + outline_content=outline.content, context_info=context_info, + strategy_instruction=expansion_strategy, target_chapter_count=target_chapter_count, - expansion_strategy=expansion_strategy, - enable_scene_analysis=enable_scene_analysis + scene_instruction="", # 暂时为空 + scene_field="" # 暂时为空 ) # 调用AI生成章节规划 @@ -182,17 +195,43 @@ class PlotExpansionService: await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size) # 构建当前批次的提示词(包含已生成章节的上下文) - prompt = self._build_batch_expansion_prompt( - outline=outline, - project=project, - characters_info=characters_info, + previous_context = "" + if all_chapter_plans: + previous_summaries = [] + for ch in all_chapter_plans[-3:]: # 只显示最近3章 + previous_summaries.append( + f"第{ch['sub_index']}节《{ch['title']}》: {ch['plot_summary'][:100]}..." + ) + previous_context = f""" + 【已生成章节概要】(接续生成,注意衔接) + {chr(10).join(previous_summaries)} + + ⚠️ 当前是第{current_start_index}-{current_start_index + current_batch_size - 1}节(共{target_chapter_count}节中的一部分) + """ + # 获取自定义提示词模板 + template = await PromptService.get_template("PLOT_EXPANSION_MULTI_BATCH", project.user_id, db) + # 格式化提示词 + prompt = PromptService.format_prompt( + template, + project_title=project.title, + project_genre=project.genre or '通用', + project_theme=project.theme or '未设定', + project_narrative_perspective=project.narrative_perspective or '第三人称', + project_world_time_period=project.world_time_period or '未设定', + project_world_location=project.world_location or '未设定', + project_world_atmosphere=project.world_atmosphere or '未设定', + characters_info=characters_info or '暂无角色', + outline_order_index=outline.order_index, + outline_title=outline.title, + outline_content=outline.content, context_info=context_info, - target_chapter_count=current_batch_size, - expansion_strategy=expansion_strategy, - enable_scene_analysis=enable_scene_analysis, + previous_context=previous_context, + strategy_instruction=expansion_strategy, start_index=current_start_index, - previous_chapters=all_chapter_plans, - total_chapters=target_chapter_count + end_index=current_start_index + current_batch_size - 1, + target_chapter_count=current_batch_size, + scene_instruction="", # 暂时为空 + scene_field="" # 暂时为空 ) # 调用AI生成当前批次 @@ -452,258 +491,6 @@ class PlotExpansionService: return context if context else "(无前后文)" - def _build_expansion_prompt( - self, - outline: Outline, - project: Project, - characters_info: str, - context_info: str, - target_chapter_count: int, - expansion_strategy: str, - enable_scene_analysis: bool - ) -> str: - """构建大纲展开提示词""" - - strategy_desc = { - "balanced": "均衡展开:每章剧情量相当,节奏平稳", - "climax": "高潮重点:重点章节剧情丰富,其他章节简洁过渡", - "detail": "细节丰富:每章都深入描写,场景和情感细腻" - } - - strategy_instruction = strategy_desc.get(expansion_strategy, strategy_desc["balanced"]) - - # 场景字段(避免f-string中的反斜杠) - scene_field = ',\n "main_scenes": ["场景1", "场景2"]' if enable_scene_analysis else '' - - scene_instruction = "" - if enable_scene_analysis: - scene_instruction = """ -5. 场景分析(每章需包含): - - 主要场景地点 - - 场景氛围 - - 关键道具/环境元素 -""" - - prompt = f"""你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapter_count} 个章节的详细规划。 - -【项目信息】 -小说名称:{project.title} -类型:{project.genre or '通用'} -主题:{project.theme or '未设定'} -叙事视角:{project.narrative_perspective or '第三人称'} - -【世界观背景】 -时间背景:{project.world_time_period or '未设定'} -地理位置:{project.world_location or '未设定'} -氛围基调:{project.world_atmosphere or '未设定'} - -【角色信息】 -{characters_info or '暂无角色'} - -【当前大纲节点 - 展开对象】 -序号:第 {outline.order_index} 节 -标题:{outline.title} -内容:{outline.content} - -【上下文参考】 -{context_info} - -【展开策略】 -{strategy_instruction} - -【⚠️ 重要约束 - 必须严格遵守】 -1. **内容边界约束**: - - ✅ 只能展开【当前大纲节点】中明确描述的内容 - - ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息) - - ❌ 不要让剧情快速推进,要深化而非跨越 - -2. **展开原则**: - - 将当前大纲的单一事件拆解为多个细节丰富的章节 - - 深入挖掘情感、心理、环境、对话等细节 - - 放慢叙事节奏,让读者充分体验当前阶段的剧情 - - 每个章节都应该是当前大纲内容的不同侧面或阶段 - -3. **如何避免剧情越界**: - - 如果当前大纲描述"主角遇到困境",展开时应详写困境的发现、分析、情感冲击等 - - 不要直接写到"解决困境",除非原大纲明确包含解决过程 - - 如果看到【后一节】的内容,那些是禁区,绝不提前展开 - -【任务要求】 -1. 深度分析该大纲的剧情容量和叙事节奏 -2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内) -3. 将大纲拆解为 {target_chapter_count} 个章节,每章需包含: - - sub_index: 子章节序号(1, 2, 3...) - - title: 章节标题(体现该章核心冲突或情感) - - plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件,仅限当前大纲内容) - - key_events: 关键事件列表(3-5个关键剧情点,必须在当前大纲范围内) - - character_focus: 角色焦点(主要涉及的角色名称) - - emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等) - - narrative_goal: 叙事目标(该章要达成的叙事效果) - - conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等) - - estimated_words: 预计字数(建议2000-5000字) -{scene_instruction} -4. 确保章节间: - - 衔接自然流畅 - - 剧情递进合理(但不超出当前大纲边界) - - 节奏张弛有度 - - 每章都有明确的叙事价值 - - 最后一章结束时,剧情发展程度应恰好完成当前大纲描述的内容,不多不少 - -【输出格式】 -请严格按照以下JSON数组格式输出,不要添加任何其他文字: -[ - {{ - "sub_index": 1, - "title": "章节标题", - "plot_summary": "该章详细剧情摘要...", - "key_events": ["关键事件1", "关键事件2", "关键事件3"], - "character_focus": ["角色A", "角色B"], - "emotional_tone": "情感基调", - "narrative_goal": "叙事目标", - "conflict_type": "冲突类型", - "estimated_words": 3000{scene_field} - }} -] - -请开始分析并生成章节规划: -""" - return prompt - - def _build_batch_expansion_prompt( - self, - outline: Outline, - project: Project, - characters_info: str, - context_info: str, - target_chapter_count: int, - expansion_strategy: str, - enable_scene_analysis: bool, - start_index: int, - previous_chapters: List[Dict[str, Any]], - total_chapters: int - ) -> str: - """构建分批展开提示词""" - - strategy_desc = { - "balanced": "均衡展开:每章剧情量相当,节奏平稳", - "climax": "高潮重点:重点章节剧情丰富,其他章节简洁过渡", - "detail": "细节丰富:每章都深入描写,场景和情感细腻" - } - - strategy_instruction = strategy_desc.get(expansion_strategy, strategy_desc["balanced"]) - - # 场景字段 - scene_field = ',\n "main_scenes": ["场景1", "场景2"]' if enable_scene_analysis else '' - - scene_instruction = "" - if enable_scene_analysis: - scene_instruction = """ -5. 场景分析(每章需包含): - - 主要场景地点 - - 场景氛围 - - 关键道具/环境元素 -""" - - # 构建已生成章节的摘要 - previous_context = "" - if previous_chapters: - previous_summaries = [] - for ch in previous_chapters[-3:]: # 只显示最近3章 - previous_summaries.append( - f"第{ch['sub_index']}节《{ch['title']}》: {ch['plot_summary'][:100]}..." - ) - previous_context = f""" -【已生成章节概要】(接续生成,注意衔接) -{chr(10).join(previous_summaries)} - -⚠️ 当前是第{start_index}-{start_index + target_chapter_count - 1}节(共{total_chapters}节中的一部分) -""" - - prompt = f"""你是专业的小说情节架构师。请继续分析以下大纲节点,将其展开为第{start_index}-{start_index + target_chapter_count - 1}节(共{target_chapter_count}个章节)的详细规划。 - -【项目信息】 -小说名称:{project.title} -类型:{project.genre or '通用'} -主题:{project.theme or '未设定'} -叙事视角:{project.narrative_perspective or '第三人称'} - -【世界观背景】 -时间背景:{project.world_time_period or '未设定'} -地理位置:{project.world_location or '未设定'} -氛围基调:{project.world_atmosphere or '未设定'} - -【角色信息】 -{characters_info or '暂无角色'} - -【当前大纲节点 - 展开对象】 -序号:第 {outline.order_index} 节 -标题:{outline.title} -内容:{outline.content} - -【上下文参考】 -{context_info} -{previous_context} - -【展开策略】 -{strategy_instruction} - -【⚠️ 重要约束 - 必须严格遵守】 -1. **内容边界约束**: - - ✅ 只能展开【当前大纲节点】中明确描述的内容 - - ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息) - - ❌ 不要让剧情快速推进,要深化而非跨越 - -2. **分批连续性约束**: - - 这是第{start_index}-{start_index + target_chapter_count - 1}节,是整个展开的一部分 - - 必须与前面已生成的章节自然衔接 - - 从第{start_index}节开始编号(sub_index从{start_index}开始) - - 继续深化当前大纲的内容,保持叙事连贯性 - -3. **展开原则**: - - 将当前大纲的单一事件拆解为多个细节丰富的章节 - - 深入挖掘情感、心理、环境、对话等细节 - - 放慢叙事节奏,让读者充分体验当前阶段的剧情 - - 每个章节都应该是当前大纲内容的不同侧面或阶段 - -【任务要求】 -1. 深度分析该大纲的剧情容量和叙事节奏 -2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内) -3. 生成第{start_index}-{start_index + target_chapter_count - 1}节的章节规划,每章需包含: - - sub_index: 子章节序号(从{start_index}开始) - - title: 章节标题(体现该章核心冲突或情感) - - plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件) - - key_events: 关键事件列表(3-5个关键剧情点) - - character_focus: 角色焦点(主要涉及的角色名称) - - emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等) - - narrative_goal: 叙事目标(该章要达成的叙事效果) - - conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等) - - estimated_words: 预计字数(建议2000-5000字) -{scene_instruction} -4. 确保章节间: - - 与前面章节衔接自然流畅 - - 剧情递进合理(但不超出当前大纲边界) - - 节奏张弛有度 - - 每章都有明确的叙事价值 - -【输出格式】 -请严格按照以下JSON数组格式输出,不要添加任何其他文字: -[ - {{ - "sub_index": {start_index}, - "title": "章节标题", - "plot_summary": "该章详细剧情摘要...", - "key_events": ["关键事件1", "关键事件2", "关键事件3"], - "character_focus": ["角色A", "角色B"], - "emotional_tone": "情感基调", - "narrative_goal": "叙事目标", - "conflict_type": "冲突类型", - "estimated_words": 3000{scene_field} - }} -] - -请开始分析并生成第{start_index}-{start_index + target_chapter_count - 1}节的章节规划: -""" - return prompt def _parse_expansion_response( self, diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 8d8de78..a77041f 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -851,7 +851,510 @@ class PromptService: 1. 只返回纯JSON对象,不要有```json```这样的标记 2. 所有内容描述中严禁使用任何特殊符号 3. 不要有任何额外的文字说明""" + + # 情节分析提示词 + PLOT_ANALYSIS = """你是一位专业的小说编辑和剧情分析师。请深度分析以下章节内容: + +**章节信息:** +- 章节: 第{chapter_number}章 +- 标题: {title} +- 字数: {word_count}字 + +**章节内容:** +{content} + +--- + +**分析任务:** +请从专业编辑的角度,全面分析这一章节: + +### 1. 剧情钩子 (Hooks) - 吸引读者的元素 +识别能够吸引读者继续阅读的关键元素: +- **悬念钩子**: 未解之谜、疑问、谜团 +- **情感钩子**: 引发共鸣的情感点、触动心弦的时刻 +- **冲突钩子**: 矛盾对抗、紧张局势 +- **认知钩子**: 颠覆认知的信息、惊人真相 + +每个钩子需要: +- 类型分类 +- 具体内容描述 +- 强度评分(1-10) +- 出现位置(开头/中段/结尾) +- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! + +### 2. 伏笔分析 (Foreshadowing) +- **埋下的新伏笔**: 描述内容、预期作用、隐藏程度(1-10) +- **回收的旧伏笔**: 呼应哪一章、回收效果评分 +- **伏笔质量**: 巧妙性和合理性评估 +- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! + +### 3. 冲突分析 (Conflict) +- 冲突类型: 人与人/人与己/人与环境/人与社会 +- 冲突各方及其立场 +- 冲突强度评分(1-10) +- 冲突解决进度(0-100%) + +### 4. 情感曲线 (Emotional Arc) +- 主导情绪: 紧张/温馨/悲伤/激昂/平静等 +- 情感强度(1-10) +- 情绪变化轨迹描述 + +### 5. 角色状态追踪 (Character Development) +对每个出场角色分析: +- 心理状态变化(前→后) +- 关系变化 +- 关键行动和决策 +- 成长或退步 + +### 6. 关键情节点 (Plot Points) +列出3-5个核心情节点: +- 情节内容 +- 类型(revelation/conflict/resolution/transition) +- 重要性(0.0-1.0) +- 对故事的影响 +- **关键词**: 【必填】从章节原文中逐字复制一段关键文本(8-25字),必须是原文中真实存在的连续文字,用于在文本中精确定位。不要概括或改写,必须原样复制! + +### 7. 场景与节奏 +- 主要场景 +- 叙事节奏(快/中/慢) +- 对话与描写的比例 + +### 8. 质量评分 +- 节奏把控: 1-10分 +- 吸引力: 1-10分 +- 连贯性: 1-10分 +- 整体质量: 1-10分 + +### 9. 改进建议 +提供3-5条具体的改进建议 + +--- + +**输出格式(纯JSON,不要markdown标记):** + +{{ + "hooks": [ + {{ + "type": "悬念", + "content": "具体描述", + "strength": 8, + "position": "中段", + "keyword": "必须从原文逐字复制的文本片段" + }} + ], + "foreshadows": [ + {{ + "content": "伏笔内容", + "type": "planted", + "strength": 7, + "subtlety": 8, + "reference_chapter": null, + "keyword": "必须从原文逐字复制的文本片段" + }} + ], + "conflict": {{ + "types": ["人与人", "人与己"], + "parties": ["主角-复仇", "反派-维护现状"], + "level": 8, + "description": "冲突描述", + "resolution_progress": 0.3 + }}, + "emotional_arc": {{ + "primary_emotion": "紧张", + "intensity": 8, + "curve": "平静→紧张→高潮→释放", + "secondary_emotions": ["期待", "焦虑"] + }}, + "character_states": [ + {{ + "character_name": "张三", + "state_before": "犹豫", + "state_after": "坚定", + "psychological_change": "心理变化描述", + "key_event": "触发事件", + "relationship_changes": {{"李四": "关系改善"}} + }} + ], + "plot_points": [ + {{ + "content": "情节点描述", + "type": "revelation", + "importance": 0.9, + "impact": "推动故事发展", + "keyword": "必须从原文逐字复制的文本片段" + }} + ], + "scenes": [ + {{ + "location": "地点", + "atmosphere": "氛围", + "duration": "时长估计" + }} + ], + "pacing": "varied", + "dialogue_ratio": 0.4, + "description_ratio": 0.3, + "scores": {{ + "pacing": 8, + "engagement": 9, + "coherence": 8, + "overall": 8.5 + }}, + "plot_stage": "发展", + "suggestions": [ + "具体建议1", + "具体建议2" + ] +}} + +**重要提示:** +1. 每个钩子、伏笔、情节点的keyword字段是必填的,不能为空 +2. keyword必须是从章节原文中逐字复制的文本,长度8-25字 +3. keyword用于在前端标注文本位置,所以必须能在原文中精确找到 +4. 不要使用概括性语句或改写后的文字作为keyword + +只返回JSON,不要其他说明。""" + + # 大纲单批次展开提示词 + PLOT_EXPANSION_SINGLE_BATCH = """你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapter_count} 个章节的详细规划。 + +【项目信息】 +小说名称:{project_title} +类型:{project_genre} +主题:{project_theme} +叙事视角:{project_narrative_perspective} + +【世界观背景】 +时间背景:{project_world_time_period} +地理位置:{project_world_location} +氛围基调:{project_world_atmosphere} + +【角色信息】 +{characters_info} + +【当前大纲节点 - 展开对象】 +序号:第 {outline_order_index} 节 +标题:{outline_title} +内容:{outline_content} + +【上下文参考】 +{context_info} + +【展开策略】 +{strategy_instruction} + +【⚠️ 重要约束 - 必须严格遵守】 +1. **内容边界约束**: + - ✅ 只能展开【当前大纲节点】中明确描述的内容 + - ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息) + - ❌ 不要让剧情快速推进,要深化而非跨越 + +2. **展开原则**: + - 将当前大纲的单一事件拆解为多个细节丰富的章节 + - 深入挖掘情感、心理、环境、对话等细节 + - 放慢叙事节奏,让读者充分体验当前阶段的剧情 + - 每个章节都应该是当前大纲内容的不同侧面或阶段 + +3. **如何避免剧情越界**: + - 如果当前大纲描述"主角遇到困境",展开时应详写困境的发现、分析、情感冲击等 + - 不要直接写到"解决困境",除非原大纲明确包含解决过程 + - 如果看到【后一节】的内容,那些是禁区,绝不提前展开 + +【任务要求】 +1. 深度分析该大纲的剧情容量和叙事节奏 +2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内) +3. 将大纲拆解为 {target_chapter_count} 个章节,每章需包含: + - sub_index: 子章节序号(1, 2, 3...) + - title: 章节标题(体现该章核心冲突或情感) + - plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件,仅限当前大纲内容) + - key_events: 关键事件列表(3-5个关键剧情点,必须在当前大纲范围内) + - character_focus: 角色焦点(主要涉及的角色名称) + - emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等) + - narrative_goal: 叙事目标(该章要达成的叙事效果) + - conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等) + - estimated_words: 预计字数(建议2000-5000字) +{scene_instruction} +4. 确保章节间: + - 衔接自然流畅 + - 剧情递进合理(但不超出当前大纲边界) + - 节奏张弛有度 + - 每章都有明确的叙事价值 + - 最后一章结束时,剧情发展程度应恰好完成当前大纲描述的内容,不多不少 + +【输出格式】 +请严格按照以下JSON数组格式输出,不要添加任何其他文字: +[ + {{ + "sub_index": 1, + "title": "章节标题", + "plot_summary": "该章详细剧情摘要...", + "key_events": ["关键事件1", "关键事件2", "关键事件3"], + "character_focus": ["角色A", "角色B"], + "emotional_tone": "情感基调", + "narrative_goal": "叙事目标", + "conflict_type": "冲突类型", + "estimated_words": 3000{scene_field} + }} +] + +请开始分析并生成章节规划: +""" + + # 大纲分批展开提示词 + PLOT_EXPANSION_MULTI_BATCH = """你是专业的小说情节架构师。请继续分析以下大纲节点,将其展开为第{start_index}-{end_index}节(共{target_chapter_count}个章节)的详细规划。 + +【项目信息】 +小说名称:{project_title} +类型:{project_genre} +主题:{project_theme} +叙事视角:{project_narrative_perspective} + +【世界观背景】 +时间背景:{project_world_time_period} +地理位置:{project_world_location} +氛围基调:{project_world_atmosphere} + +【角色信息】 +{characters_info} + +【当前大纲节点 - 展开对象】 +序号:第 {outline_order_index} 节 +标题:{outline_title} +内容:{outline_content} + +【上下文参考】 +{context_info} +{previous_context} + +【展开策略】 +{strategy_instruction} + +【⚠️ 重要约束 - 必须严格遵守】 +1. **内容边界约束**: + - ✅ 只能展开【当前大纲节点】中明确描述的内容 + - ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息) + - ❌ 不要让剧情快速推进,要深化而非跨越 + +2. **分批连续性约束**: + - 这是第{start_index}-{end_index}节,是整个展开的一部分 + - 必须与前面已生成的章节自然衔接 + - 从第{start_index}节开始编号(sub_index从{start_index}开始) + - 继续深化当前大纲的内容,保持叙事连贯性 + +3. **展开原则**: + - 将当前大纲的单一事件拆解为多个细节丰富的章节 + - 深入挖掘情感、心理、环境、对话等细节 + - 放慢叙事节奏,让读者充分体验当前阶段的剧情 + - 每个章节都应该是当前大纲内容的不同侧面或阶段 + +【任务要求】 +1. 深度分析该大纲的剧情容量和叙事节奏 +2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内) +3. 生成第{start_index}-{end_index}节的章节规划,每章需包含: + - sub_index: 子章节序号(从{start_index}开始) + - title: 章节标题(体现该章核心冲突或情感) + - plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件) + - key_events: 关键事件列表(3-5个关键剧情点) + - character_focus: 角色焦点(主要涉及的角色名称) + - emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等) + - narrative_goal: 叙事目标(该章要达成的叙事效果) + - conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等) + - estimated_words: 预计字数(建议2000-5000字) +{scene_instruction} +4. 确保章节间: + - 与前面章节衔接自然流畅 + - 剧情递进合理(但不超出当前大纲边界) + - 节奏张弛有度 + - 每章都有明确的叙事价值 + +【输出格式】 +请严格按照以下JSON数组格式输出,不要添加任何其他文字: +[ + {{ + "sub_index": {start_index}, + "title": "章节标题", + "plot_summary": "该章详细剧情摘要...", + "key_events": ["关键事件1", "关键事件2", "关键事件3"], + "character_focus": ["角色A", "角色B"], + "emotional_tone": "情感基调", + "narrative_goal": "叙事目标", + "conflict_type": "冲突类型", + "estimated_words": 3000{scene_field} + }} +] + +请开始分析并生成第{start_index}-{end_index}节的章节规划: +""" + + # 章节重写系统提示词 + CHAPTER_REGENERATION_SYSTEM = """你是一位经验丰富的专业小说编辑和作家。现在需要根据反馈意见重新创作一个章节。 + +你的任务是: +1. 仔细理解原始章节的内容和意图 +2. 认真分析所有的修改要求 +3. 在保持故事连贯性的前提下,创作一个改进后的新版本 +4. 确保新版本在艺术性和可读性上都有明显提升 + +--- +""" + # MCP工具测试提示词 + MCP_TOOL_TEST = """你是MCP插件测试助手,需要测试插件 '{plugin_name}' 的功能。 + +⚠️ 重要规则:生成参数时,必须严格使用工具 schema 中定义的原始参数名称,不要转换为 snake_case 或其他格式。 +例如:如果 schema 中是 'nextThoughtNeeded',就必须使用 'nextThoughtNeeded',不能改成 'next_thought_needed'。 + +请选择一个合适的工具进行测试,优先选择搜索、查询类工具。 +生成真实有效的测试参数(例如搜索"人工智能最新进展"而不是"test")。 + +现在开始测试这个插件。""" + + MCP_TOOL_TEST_SYSTEM = """你是专业的API测试工具。当给定工具列表时,选择一个工具并使用合适的参数调用它。 + +⚠️ 关键规则:调用工具时,必须严格使用 schema 中定义的原始参数名,不要自行转换命名风格。 +- 如果参数名是 camelCase(如 nextThoughtNeeded),就使用 camelCase +- 如果参数名是 snake_case(如 next_thought),就使用 snake_case +- 保持与 schema 中定义的完全一致,包括大小写和命名风格""" + # 灵感模式提示词字典 + INSPIRATION_PROMPTS = { + "title": { + "system": """你是一位专业的小说创作顾问。 +用户的原始想法:{initial_idea} + +请根据用户的想法,生成6个吸引人的书名建议,要求: +1. 紧扣用户的原始想法和核心故事构思 +2. 富有创意和吸引力 +3. 涵盖不同的风格倾向 + +返回JSON格式: +{{ + "prompt": "根据你的想法,我为你准备了几个书名建议:", + "options": ["书名1", "书名2", "书名3", "书名4", "书名5", "书名6"] +}} + +只返回纯JSON,不要有其他文字。""", + "user": "用户的想法:{initial_idea}\n请生成6个书名建议" + }, + "description": { + "system": """你是一位专业的小说创作顾问。 +用户的原始想法:{initial_idea} +已确定的书名:{title} + +请生成6个精彩的小说简介,要求: +1. 必须紧扣用户的原始想法,确保简介是原始想法的具体展开 +2. 符合已确定的书名风格 +3. 简洁有力,每个50-100字 +4. 包含核心冲突 +5. 涵盖不同的故事走向,但都基于用户的原始构思 + +返回JSON格式: +{{"prompt":"选择一个简介:","options":["简介1","简介2","简介3","简介4","简介5","简介6"]}} + +只返回纯JSON,不要有其他文字,不要换行。""", + "user": "原始想法:{initial_idea}\n书名:{title}\n请生成6个简介选项" + }, + "theme": { + "system": """你是一位专业的小说创作顾问。 +用户的原始想法:{initial_idea} +小说信息: +- 书名:{title} +- 简介:{description} + +请生成6个深刻的主题选项,要求: +1. 必须与用户的原始想法保持高度一致 +2. 符合书名和简介的风格 +3. 有深度和思想性 +4. 每个50-150字 +5. 涵盖不同角度(如:成长、复仇、救赎、探索等),但都围绕用户的核心构思 + +返回JSON格式: +{{"prompt":"这本书的核心主题是什么?","options":["主题1","主题2","主题3","主题4","主题5","主题6"]}} + +只返回纯JSON,不要有其他文字,不要换行。""", + "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n请生成6个主题选项" + }, + "genre": { + "system": """你是一位专业的小说创作顾问。 +用户的原始想法:{initial_idea} +小说信息: +- 书名:{title} +- 简介:{description} +- 主题:{theme} + +请生成6个合适的类型标签(每个2-4字),要求: +1. 必须符合用户原始想法中暗示的类型倾向 +2. 符合小说整体风格 +3. 可以多选组合 + +常见类型:玄幻、都市、科幻、武侠、仙侠、历史、言情、悬疑、奇幻、修仙等 + +返回JSON格式: +{{"prompt":"选择类型标签(可多选):","options":["类型1","类型2","类型3","类型4","类型5","类型6"]}} + +只返回紧凑的纯JSON,不要换行,不要有其他文字。""", + "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n主题:{theme}\n请生成6个类型标签" + } + } + + # 灵感模式智能补全提示词 + INSPIRATION_QUICK_COMPLETE = """你是一位专业的小说创作顾问。用户提供了部分小说信息,请补全缺失的字段。 + +用户已提供的信息: +{existing} + +请生成完整的小说方案,包含: +1. title: 书名(3-6字,如果用户已提供则保持原样) +2. description: 简介(50-100字,必须基于用户提供的信息,不要偏离原意) +3. theme: 核心主题(30-50字,必须与用户提供的信息保持一致) +4. genre: 类型标签数组(2-3个) + +重要:所有补全的内容都必须与用户提供的信息保持高度关联,确保前后一致性。 + +返回JSON格式: +{{ + "title": "书名", + "description": "简介内容...", + "theme": "主题内容...", + "genre": ["类型1", "类型2"] +}} + +只返回纯JSON,不要有其他文字。""" + # 世界观资料收集提示词(MCP增强用) + MCP_WORLD_BUILDING_PLANNING = """你正在为小说《{title}》设计世界观。 + +【小说信息】 +- 题材:{genre} +- 主题:{theme} +- 简介:{description} + +【任务】 +请使用可用工具搜索相关背景资料,帮助构建更真实、更有深度的世界观设定。 +你可以查询: +1. 历史背景(如果是历史题材) +2. 地理环境和文化特征 +3. 相关领域的专业知识 +4. 类似作品的设定参考 + +请查询最关键的1个问题(不要超过1个)。""" + + # 角色资料收集提示词(MCP增强用) + MCP_CHARACTER_PLANNING = """你正在为小说《{title}》设计角色。 + +【小说信息】 +- 题材:{genre} +- 主题:{theme} +- 时代背景:{time_period} +- 地理位置:{location} + +【任务】 +请使用可用工具搜索相关参考资料,帮助设计更真实、更有深度的角色。 +你可以查询: +1. 该时代/地域的真实历史人物特征 +2. 文化背景和社会习俗 +3. 职业特点和生活方式 +4. 相关领域的人物原型 + +请查询最关键的1个问题(不要超过1个)。""" # 大纲展开为多章节的提示词 OUTLINE_EXPANSION = """你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapters} 个章节的详细规划。 @@ -1339,7 +1842,417 @@ class PromptService: scene_instruction=scene_instruction, scene_field=scene_field ) + + @classmethod + def get_plot_analysis_prompt(cls, chapter_number: int, title: str, + content: str, word_count: int) -> str: + """获取章节剧情分析提示词""" + return cls.format_prompt( + cls.PLOT_ANALYSIS, + chapter_number=chapter_number, + title=title, + content=content, + word_count=word_count + ) + @classmethod + def get_plot_expansion_single_batch_prompt(cls, project_title: str, project_genre: str, project_theme: str, + project_narrative_perspective: str, project_world_time_period: str, + project_world_location: str, project_world_atmosphere: str, + characters_info: str, outline_order_index: int, outline_title: str, + outline_content: str, context_info: str, strategy_instruction: str, + target_chapter_count: int, scene_instruction: str, scene_field: str) -> str: + """获取大纲单批次展开提示词""" + return cls.format_prompt( + cls.PLOT_EXPANSION_SINGLE_BATCH, + project_title=project_title, project_genre=project_genre, project_theme=project_theme, + project_narrative_perspective=project_narrative_perspective, project_world_time_period=project_world_time_period, + project_world_location=project_world_location, project_world_atmosphere=project_world_atmosphere, + characters_info=characters_info, outline_order_index=outline_order_index, outline_title=outline_title, + outline_content=outline_content, context_info=context_info, strategy_instruction=strategy_instruction, + target_chapter_count=target_chapter_count, scene_instruction=scene_instruction, scene_field=scene_field + ) + @classmethod + def get_plot_expansion_multi_batch_prompt(cls, project_title: str, project_genre: str, project_theme: str, + project_narrative_perspective: str, project_world_time_period: str, + project_world_location: str, project_world_atmosphere: str, + characters_info: str, outline_order_index: int, outline_title: str, + outline_content: str, context_info: str, previous_context: str, + strategy_instruction: str, start_index: int, end_index: int, + target_chapter_count: int, scene_instruction: str, scene_field: str) -> str: + """获取大纲分批展开提示词""" + return cls.format_prompt( + cls.PLOT_EXPANSION_MULTI_BATCH, + project_title=project_title, project_genre=project_genre, project_theme=project_theme, + project_narrative_perspective=project_narrative_perspective, project_world_time_period=project_world_time_period, + project_world_location=project_world_location, project_world_atmosphere=project_world_atmosphere, + characters_info=characters_info, outline_order_index=outline_order_index, outline_title=outline_title, + outline_content=outline_content, context_info=context_info, previous_context=previous_context, + strategy_instruction=strategy_instruction, start_index=start_index, end_index=end_index, + target_chapter_count=target_chapter_count, scene_instruction=scene_instruction, scene_field=scene_field + ) + @classmethod + def get_chapter_regeneration_prompt(cls, chapter_number: int, title: str, word_count: int, content: str, + modification_instructions: str, project_context: Dict[str, Any], + style_content: str, target_word_count: int) -> str: + """获取章节重写提示词""" + prompt_parts = [cls.CHAPTER_REGENERATION_SYSTEM] + + # 原始章节信息 + prompt_parts.append(f"""## 📖 原始章节信息 + +**章节**:第{chapter_number}章 +**标题**:{title} +**字数**:{word_count}字 + +**原始内容**: +{content} + +--- +""") + + # 修改指令 + prompt_parts.append(modification_instructions) + prompt_parts.append("\n---\n") + + # 项目背景信息 + prompt_parts.append(f"""## 🌍 项目背景信息 + +**小说标题**:{project_context.get('project_title', '未知')} +**题材**:{project_context.get('genre', '未设定')} +**主题**:{project_context.get('theme', '未设定')} +**叙事视角**:{project_context.get('narrative_perspective', '第三人称')} +**世界观设定**: +- 时代背景:{project_context.get('time_period', '未设定')} +- 地理位置:{project_context.get('location', '未设定')} +- 氛围基调:{project_context.get('atmosphere', '未设定')} + +--- +""") + + # 角色信息 + if project_context.get('characters_info'): + prompt_parts.append(f"""## 👥 角色信息 + +{project_context['characters_info']} + +--- +""") + + # 章节大纲 + if project_context.get('chapter_outline'): + prompt_parts.append(f"""## 📝 本章大纲 + +{project_context['chapter_outline']} + +--- +""") + + # 前置章节上下文 + if project_context.get('previous_context'): + prompt_parts.append(f"""## 📚 前置章节上下文 + +{project_context['previous_context']} + +--- +""") + + # 写作风格要求 + if style_content: + prompt_parts.append(f"""## 🎨 写作风格要求 + +{style_content} + +请在重新创作时严格遵循上述写作风格。 + +--- +""") + + # 创作要求 + prompt_parts.append(f"""## ✨ 创作要求 + +1. **解决问题**:针对上述修改指令中提到的所有问题进行改进 +2. **保持连贯**:确保与前后章节的情节、人物、风格保持一致 +3. **提升质量**:在节奏、情感、描写等方面明显优于原版 +4. **保留精华**:保持原章节中优秀的部分和关键情节 +5. **字数控制**:目标字数约{target_word_count}字(可适当浮动±20%) +{f'6. **风格一致**:严格按照上述写作风格进行创作' if style_content else ''} + +--- + +## 🎬 开始创作 + +请现在开始创作改进后的新版本章节内容。 + +**重要提示**: +- 直接输出章节正文内容,从故事内容开始写 +- **不要**输出章节标题(如"第X章"、"第X章:XXX"等) +- **不要**输出任何额外的说明、注释或元数据 +- 只需要纯粹的故事正文内容 + +现在开始: +""") + + return "\n".join(prompt_parts) + + @classmethod + def get_inspiration_prompt(cls, step: str) -> Optional[Dict[str, str]]: + """获取灵感模式指定步骤的提示词""" + return cls.INSPIRATION_PROMPTS.get(step) + + @classmethod + def get_inspiration_quick_complete_prompt(cls, existing: str) -> Dict[str, str]: + """获取灵感模式智能补全的提示词""" + return { + "system": cls.format_prompt(cls.INSPIRATION_QUICK_COMPLETE, existing=existing), + "user": "请补全小说信息" + } + + @classmethod + def get_mcp_tool_test_prompts(cls, plugin_name: str) -> Dict[str, str]: + """获取MCP工具测试的提示词""" + return { + "user": cls.format_prompt(cls.MCP_TOOL_TEST, plugin_name=plugin_name), + "system": cls.MCP_TOOL_TEST_SYSTEM + } # 创建全局提示词服务实例 + + # ========== 自定义提示词支持 ========== + + @classmethod + async def get_template_with_fallback(cls, + template_key: str, + user_id: str = None, + db = None) -> str: + """ + 获取提示词模板(优先用户自定义,支持降级) + + Args: + template_key: 模板键名 + user_id: 用户ID(可选,如果不提供则直接返回系统默认) + db: 数据库会话(可选) + + Returns: + 提示词模板内容 + """ + # 如果没有提供user_id或db,直接返回系统默认 + if not user_id or not db: + return getattr(cls, template_key, None) + + # 尝试获取用户自定义模板 + return await cls.get_template(template_key, user_id, db) + + @classmethod + async def get_template(cls, + template_key: str, + user_id: str, + db) -> str: + """ + 获取提示词模板(优先用户自定义) + + Args: + template_key: 模板键名 + user_id: 用户ID + db: 数据库会话 + + Returns: + 提示词模板内容 + """ + from sqlalchemy import select + from app.models.prompt_template import PromptTemplate + from app.logger import get_logger + + logger = get_logger(__name__) + + # 1. 尝试从数据库获取用户自定义模板 + result = await db.execute( + select(PromptTemplate).where( + PromptTemplate.user_id == user_id, + PromptTemplate.template_key == template_key, + PromptTemplate.is_active == True + ) + ) + custom_template = result.scalar_one_or_none() + + if custom_template: + logger.info(f"✅ 使用用户自定义提示词: user_id={user_id}, template_key={template_key}, template_name={custom_template.template_name}") + return custom_template.template_content + + # 2. 降级到系统默认模板 + logger.info(f"⚪ 使用系统默认提示词: user_id={user_id}, template_key={template_key} (未找到自定义模板)") + return getattr(cls, template_key, None) + + @classmethod + def get_all_system_templates(cls) -> list: + """ + 获取所有系统默认模板的信息 + + Returns: + 系统模板列表 + """ + templates = [] + + # 定义所有模板及其元信息 + template_definitions = { + "WORLD_BUILDING": { + "name": "世界构建", + "category": "世界构建", + "description": "用于生成小说世界观设定,包括时间背景、地理位置、氛围基调和世界规则", + "parameters": ["title", "theme", "genre"] + }, + "CHARACTERS_BATCH_GENERATION": { + "name": "批量角色生成", + "category": "角色生成", + "description": "批量生成多个角色和组织,建立角色关系网络", + "parameters": ["count", "time_period", "location", "atmosphere", "rules", "theme", "genre", "requirements"] + }, + "SINGLE_CHARACTER_GENERATION": { + "name": "单个角色生成", + "category": "角色生成", + "description": "生成单个角色的详细设定", + "parameters": ["project_context", "user_input"] + }, + "SINGLE_ORGANIZATION_GENERATION": { + "name": "组织生成", + "category": "角色生成", + "description": "生成组织/势力的详细设定", + "parameters": ["project_context", "user_input"] + }, + "COMPLETE_OUTLINE_GENERATION": { + "name": "完整大纲生成", + "category": "大纲生成", + "description": "根据项目信息生成完整的章节大纲", + "parameters": ["title", "theme", "genre", "chapter_count", "narrative_perspective", "target_words", + "time_period", "location", "atmosphere", "rules", "characters_info", "requirements", "mcp_references"] + }, + "OUTLINE_CONTINUE_GENERATION": { + "name": "大纲续写", + "category": "大纲生成", + "description": "基于已有章节续写大纲", + "parameters": ["title", "theme", "genre", "narrative_perspective", "chapter_count", "time_period", + "location", "atmosphere", "rules", "characters_info", "current_chapter_count", + "all_chapters_brief", "recent_plot", "memory_context", "mcp_references", + "plot_stage_instruction", "start_chapter", "end_chapter", "story_direction", "requirements"] + }, + "OUTLINE_GENERATION": { + "name": "基础大纲生成", + "category": "大纲生成", + "description": "生成基础章节大纲框架", + "parameters": ["genre", "theme", "target_words", "requirements"] + }, + "OUTLINE_EXPANSION": { + "name": "大纲展开", + "category": "大纲生成", + "description": "将单个大纲节点展开为多个章节", + "parameters": ["title", "genre", "theme", "narrative_perspective", "time_period", "location", + "atmosphere", "rules", "characters_info", "outline_order", "outline_title", + "outline_content", "context_info", "strategy_instruction", "target_chapters", + "scene_instruction", "scene_field"] + }, + "CHAPTER_GENERATION": { + "name": "章节创作", + "category": "章节创作", + "description": "根据大纲创作章节内容", + "parameters": ["title", "theme", "genre", "narrative_perspective", "time_period", "location", + "atmosphere", "rules", "characters_info", "outlines_context", "chapter_number", + "chapter_title", "chapter_outline", "target_word_count", "max_word_count"] + }, + "CHAPTER_GENERATION_WITH_CONTEXT": { + "name": "章节创作(带上下文)", + "category": "章节创作", + "description": "基于前置章节内容创作新章节", + "parameters": ["title", "theme", "genre", "narrative_perspective", "time_period", "location", + "atmosphere", "rules", "characters_info", "outlines_context", "previous_content", + "memory_context", "chapter_number", "chapter_title", "chapter_outline", + "target_word_count", "max_word_count"] + }, + "CHAPTER_REGENERATION_SYSTEM": { + "name": "章节重写系统提示", + "category": "章节重写", + "description": "用于章节重写的系统提示词", + "parameters": ["chapter_number", "title", "word_count", "content", "modification_instructions", + "project_context", "style_content", "target_word_count"] + }, + "AI_DENOISING": { + "name": "AI去味", + "category": "辅助功能", + "description": "将AI生成的文本改写得更自然", + "parameters": ["original_text"] + }, + "PLOT_ANALYSIS": { + "name": "情节分析", + "category": "情节分析", + "description": "深度分析章节的剧情、钩子、伏笔等", + "parameters": ["chapter_number", "title", "content", "word_count"] + }, + "PLOT_EXPANSION_SINGLE_BATCH": { + "name": "大纲单批次展开", + "category": "情节展开", + "description": "将大纲节点展开为详细章节规划(单批次)", + "parameters": ["project_title", "project_genre", "project_theme", "project_narrative_perspective", + "project_world_time_period", "project_world_location", "project_world_atmosphere", + "characters_info", "outline_order_index", "outline_title", "outline_content", + "context_info", "strategy_instruction", "target_chapter_count", "scene_instruction", "scene_field"] + }, + "PLOT_EXPANSION_MULTI_BATCH": { + "name": "大纲分批展开", + "category": "情节展开", + "description": "将大纲节点展开为详细章节规划(分批)", + "parameters": ["project_title", "project_genre", "project_theme", "project_narrative_perspective", + "project_world_time_period", "project_world_location", "project_world_atmosphere", + "characters_info", "outline_order_index", "outline_title", "outline_content", + "context_info", "previous_context", "strategy_instruction", "start_index", + "end_index", "target_chapter_count", "scene_instruction", "scene_field"] + }, + "MCP_TOOL_TEST": { + "name": "MCP工具测试", + "category": "MCP测试", + "description": "用于测试MCP插件功能", + "parameters": ["plugin_name"] + }, + "MCP_WORLD_BUILDING_PLANNING": { + "name": "MCP世界观规划", + "category": "MCP增强", + "description": "使用MCP工具搜索资料辅助世界观设计", + "parameters": ["title", "genre", "theme", "description"] + }, + "MCP_CHARACTER_PLANNING": { + "name": "MCP角色规划", + "category": "MCP增强", + "description": "使用MCP工具搜索资料辅助角色设计", + "parameters": ["title", "genre", "theme", "time_period", "location"] + } + } + + for key, info in template_definitions.items(): + template_content = getattr(cls, key, None) + if template_content: + templates.append({ + "template_key": key, + "template_name": info["name"], + "category": info["category"], + "description": info["description"], + "parameters": info["parameters"], + "content": template_content + }) + + return templates + + @classmethod + def get_system_template_info(cls, template_key: str) -> dict: + """ + 获取指定系统模板的信息 + + Args: + template_key: 模板键名 + + Returns: + 模板信息字典 + """ + all_templates = cls.get_all_system_templates() + for template in all_templates: + if template["template_key"] == template_key: + return template + return None prompt_service = PromptService() \ No newline at end of file diff --git a/backend/scripts/migration_add_prompt_templates.sql b/backend/scripts/migration_add_prompt_templates.sql new file mode 100644 index 0000000..28247e5 --- /dev/null +++ b/backend/scripts/migration_add_prompt_templates.sql @@ -0,0 +1,34 @@ +-- 创建提示词模板表 +CREATE TABLE IF NOT EXISTS prompt_templates ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(50) NOT NULL, + template_key VARCHAR(100) NOT NULL, + template_name VARCHAR(200) NOT NULL, + template_content TEXT NOT NULL, + description TEXT, + category VARCHAR(50), + parameters TEXT, + is_active BOOLEAN DEFAULT TRUE, + is_system_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT uk_user_template UNIQUE (user_id, template_key) +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_user_template ON prompt_templates(user_id, template_key); +CREATE INDEX IF NOT EXISTS idx_user_id ON prompt_templates(user_id); +CREATE INDEX IF NOT EXISTS idx_category ON prompt_templates(category); + +-- 添加注释 +COMMENT ON TABLE prompt_templates IS '提示词模板表'; +COMMENT ON COLUMN prompt_templates.user_id IS '用户ID'; +COMMENT ON COLUMN prompt_templates.template_key IS '模板键名'; +COMMENT ON COLUMN prompt_templates.template_name IS '模板显示名称'; +COMMENT ON COLUMN prompt_templates.template_content IS '模板内容'; +COMMENT ON COLUMN prompt_templates.description IS '模板描述'; +COMMENT ON COLUMN prompt_templates.category IS '模板分类'; +COMMENT ON COLUMN prompt_templates.parameters IS '模板参数定义(JSON)'; +COMMENT ON COLUMN prompt_templates.is_active IS '是否启用'; +COMMENT ON COLUMN prompt_templates.is_system_default IS '是否为系统默认模板'; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index b13acf2..8990ee7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.0.7", + "version": "1.0.8", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f79b8f8..59741ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import WritingStyles from './pages/WritingStyles'; import Settings from './pages/Settings'; import MCPPlugins from './pages/MCPPlugins'; import UserManagement from './pages/UserManagement'; +import PromptTemplates from './pages/PromptTemplates'; // import Polish from './pages/Polish'; import Login from './pages/Login'; import AuthCallback from './pages/AuthCallback'; @@ -42,6 +43,7 @@ function App() { } /> } /> } /> + <>} /> } /> } /> } /> diff --git a/frontend/src/pages/Inspiration.tsx b/frontend/src/pages/Inspiration.tsx index 1dcc4ee..1cabfe3 100644 --- a/frontend/src/pages/Inspiration.tsx +++ b/frontend/src/pages/Inspiration.tsx @@ -351,9 +351,9 @@ const Inspiration: React.FC = () => { type: 'ai', content: `很好!现在请选择你想要的大纲模式: -📋 **一对一模式**:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。 +📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。 -📚 **一对多模式**:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。 +📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。 请选择:`, options: ['📋 一对一模式', '📚 一对多模式'] diff --git a/frontend/src/pages/ProjectList.tsx b/frontend/src/pages/ProjectList.tsx index f3b487e..05d99ec 100644 --- a/frontend/src/pages/ProjectList.tsx +++ b/frontend/src/pages/ProjectList.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown, Form, Input, InputNumber } from 'antd'; -import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined } from '@ant-design/icons'; +import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined, FileSearchOutlined } from '@ant-design/icons'; import { projectApi } from '../services/api'; import { useStore } from '../store'; import { useProjectSync } from '../store/hooks'; @@ -438,6 +438,12 @@ export default function ProjectList() { { type: 'divider' }, + { + key: 'prompt-templates', + label: '提示词管理', + icon: , + onClick: () => navigate('/prompt-templates') + }, { key: 'mcp', label: 'MCP插件', @@ -546,6 +552,12 @@ export default function ProjectList() { { type: 'divider' }, + { + key: 'prompt-templates', + label: '提示词管理', + icon: , + onClick: () => navigate('/prompt-templates') + }, { key: 'mcp', label: 'MCP插件', diff --git a/frontend/src/pages/PromptTemplates.tsx b/frontend/src/pages/PromptTemplates.tsx new file mode 100644 index 0000000..48a41d2 --- /dev/null +++ b/frontend/src/pages/PromptTemplates.tsx @@ -0,0 +1,491 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Card, + Tabs, + Button, + Switch, + Modal, + Input, + Tag, + message, + Space, + Typography, + Row, + Col, + Alert, + Upload, + Spin, + Empty +} from 'antd'; +import { + EditOutlined, + ReloadOutlined, + DownloadOutlined, + UploadOutlined, + CheckCircleOutlined, + FileSearchOutlined, + ArrowLeftOutlined, + InfoCircleOutlined +} from '@ant-design/icons'; +import axios from 'axios'; +import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles'; + +const { TextArea } = Input; +const { Title, Text, Paragraph } = Typography; + +interface PromptTemplate { + id: string; + user_id: string; + template_key: string; + template_name: string; + template_content: string; + description: string; + category: string; + parameters: string; + is_active: boolean; + is_system_default: boolean; + created_at: string; + updated_at: string; +} + +interface CategoryGroup { + category: string; + count: number; + templates: PromptTemplate[]; +} + +export default function PromptTemplates() { + const navigate = useNavigate(); + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState('0'); + const [editingTemplate, setEditingTemplate] = useState(null); + const [editorVisible, setEditorVisible] = useState(false); + const [loading, setLoading] = useState(false); + + const isMobile = window.innerWidth <= 768; + + // 加载模板数据 + const loadTemplates = async () => { + try { + setLoading(true); + const response = await axios.get('/api/prompt-templates/categories'); + setCategories(response.data); + } catch (error: any) { + message.error(error.response?.data?.detail || '加载失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadTemplates(); + }, []); + + // 获取当前分类的模板 + const getCurrentTemplates = (): PromptTemplate[] => { + const index = parseInt(selectedCategory); + if (index === 0) { + return categories.flatMap(cat => cat.templates); + } + return categories[index - 1]?.templates || []; + }; + + // 编辑模板 + const handleEdit = (template: PromptTemplate) => { + setEditingTemplate({ ...template }); + setEditorVisible(true); + }; + + // 保存模板 + const handleSave = async () => { + if (!editingTemplate) return; + + try { + setLoading(true); + await axios.post('/api/prompt-templates', { + template_key: editingTemplate.template_key, + template_name: editingTemplate.template_name, + template_content: editingTemplate.template_content, + description: editingTemplate.description, + category: editingTemplate.category, + parameters: editingTemplate.parameters, + is_active: editingTemplate.is_active + }); + message.success('保存成功'); + setEditorVisible(false); + loadTemplates(); + } catch (error: any) { + message.error(error.response?.data?.detail || '保存失败'); + } finally { + setLoading(false); + } + }; + + // 重置为系统默认 + const handleReset = async (templateKey: string) => { + Modal.confirm({ + title: '确认重置', + content: '确定要重置为系统默认模板吗?这将覆盖您的自定义内容。', + okText: '确定', + cancelText: '取消', + centered: true, + onOk: async () => { + try { + setLoading(true); + await axios.post(`/api/prompt-templates/${templateKey}/reset`); + message.success('已重置为系统默认'); + loadTemplates(); + } catch (error: any) { + message.error(error.response?.data?.detail || '重置失败'); + } finally { + setLoading(false); + } + } + }); + }; + + // 切换启用状态 + const handleToggleActive = async (template: PromptTemplate, checked: boolean) => { + try { + await axios.put(`/api/prompt-templates/${template.template_key}`, { + is_active: checked + }); + loadTemplates(); + } catch (error: any) { + message.error(error.response?.data?.detail || '操作失败'); + } + }; + + // 导出所有模板 + const handleExport = async () => { + try { + const response = await axios.post('/api/prompt-templates/export'); + const blob = new Blob([JSON.stringify(response.data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `prompt-templates-${new Date().toISOString().split('T')[0]}.json`; + a.click(); + URL.revokeObjectURL(url); + message.success('导出成功'); + } catch (error: any) { + message.error(error.response?.data?.detail || '导出失败'); + } + }; + + // 导入模板 + const handleImport = async (file: File) => { + try { + const text = await file.text(); + const data = JSON.parse(text); + await axios.post('/api/prompt-templates/import', data); + message.success('导入成功'); + loadTemplates(); + } catch (error: any) { + message.error(error.response?.data?.detail || '导入失败'); + } + return false; // 阻止默认上传行为 + }; + + const currentTemplates = getCurrentTemplates(); + + return ( +
+ {/* 头部卡片 */} +
+ + + + + + + + + + + + + + {/* 使用提示 */} + + + 使用说明 + + } + description={ +
+ + • 系统默认模板(灰色头部):始终启用,无需手动开关。点击"编辑"后将创建您的自定义副本。 + + + • 已自定义模板(紫色头部):可通过开关控制启用/禁用,使用 {'{variable_name}'} 格式表示变量占位符。点击"重置"可恢复为系统默认。 + +
+ } + type="info" + showIcon={false} + style={{ + marginTop: isMobile ? 16 : 24, + borderRadius: 12, + background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)', + border: '1px solid #91d5ff' + }} + /> +
+
+ + {/* 主内容区 */} +
+ + {/* 分类标签 */} + {categories.length > 0 && ( + + sum + cat.count, 0)})` }, + ...categories.map((cat, index) => ({ + key: (index + 1).toString(), + label: `${cat.category} (${cat.count})` + })) + ]} + /> + + )} + + {/* 模板列表 */} + {currentTemplates.length === 0 ? ( + + + + ) : ( + + {currentTemplates.map(template => ( + + + {/* 头部 */} +
+ +
+ + {template.template_name} + + {!template.is_system_default && ( + handleToggleActive(template, checked)} + size={isMobile ? 'small' : 'default'} + style={{ marginLeft: 8 }} + /> + )} +
+ + + {template.category} + + + {template.is_system_default ? '系统默认' : '已自定义'} + + +
+
+ + {/* 内容 */} +
+ + {template.description || '暂无描述'} + + + + } + color={template.is_system_default || template.is_active ? 'success' : 'default'} + > + {template.is_system_default ? '始终启用' : (template.is_active ? '已启用' : '已禁用')} + + + + + 模板键: {template.template_key} + + + {/* 操作按钮 */} + + + + +
+
+ + ))} +
+ )} +
+
+ + {/* 编辑对话框 */} + setEditorVisible(false)} + onOk={handleSave} + width={isMobile ? '100%' : 900} + centered={!isMobile} + confirmLoading={loading} + okText="保存" + cancelText="取消" + style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined} + styles={isMobile ? { + body: { + maxHeight: 'calc(100vh - 110px)', + overflowY: 'auto', + padding: '16px' + } + } : undefined} + > + +
+ + setEditingTemplate(prev => prev ? { ...prev, template_name: e.target.value } : null)} + placeholder="输入模板名称" + /> +
+ +
+ +