update:1.开放系统内置提示词,支持用户自定义模板
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
# 应用配置
|
# 应用配置
|
||||||
# ==========================================
|
# ==========================================
|
||||||
APP_NAME=MuMuAINovel
|
APP_NAME=MuMuAINovel
|
||||||
APP_VERSION=1.0.0
|
APP_VERSION=1.0.8
|
||||||
APP_HOST=0.0.0.0
|
APP_HOST=0.0.0.0
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|||||||
+58
-21
@@ -37,7 +37,7 @@ from app.schemas.regeneration import (
|
|||||||
RegenerationTaskStatus
|
RegenerationTaskStatus
|
||||||
)
|
)
|
||||||
from app.services.ai_service import AIService
|
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.plot_analyzer import PlotAnalyzer
|
||||||
from app.services.memory_service import memory_service
|
from app.services.memory_service import memory_service
|
||||||
from app.services.chapter_regenerator import ChapterRegenerator
|
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 '暂无大纲'
|
chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲'
|
||||||
logger.warning(f"⚠️ 一对多模式但无expansion_plan,使用大纲内容")
|
logger.warning(f"⚠️ 一对多模式但无expansion_plan,使用大纲内容")
|
||||||
|
|
||||||
# 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料
|
# 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料(支持自定义)
|
||||||
if previous_content:
|
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,
|
title=project.title,
|
||||||
theme=project.theme or '',
|
theme=project.theme or '',
|
||||||
genre=project.genre or '',
|
genre=project.genre or '',
|
||||||
@@ -1253,14 +1255,25 @@ async def generate_chapter_content_stream(
|
|||||||
chapter_number=current_chapter.chapter_number,
|
chapter_number=current_chapter.chapter_number,
|
||||||
chapter_title=current_chapter.title,
|
chapter_title=current_chapter.title,
|
||||||
chapter_outline=chapter_outline_content,
|
chapter_outline=chapter_outline_content,
|
||||||
style_content=style_content,
|
|
||||||
target_word_count=target_word_count,
|
target_word_count=target_word_count,
|
||||||
memory_context=memory_context,
|
max_word_count=target_word_count + 1000,
|
||||||
mcp_references=mcp_reference_materials,
|
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 "暂无相关记忆"
|
||||||
outline_mode=outline_mode
|
|
||||||
)
|
)
|
||||||
|
# 插入模式说明和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:
|
else:
|
||||||
prompt = prompt_service.get_chapter_generation_prompt(
|
prompt = base_prompt
|
||||||
|
else:
|
||||||
|
template = await PromptService.get_template("CHAPTER_GENERATION", current_user_id, db_session)
|
||||||
|
base_prompt = PromptService.format_prompt(
|
||||||
|
template,
|
||||||
title=project.title,
|
title=project.title,
|
||||||
theme=project.theme or '',
|
theme=project.theme or '',
|
||||||
genre=project.genre or '',
|
genre=project.genre or '',
|
||||||
@@ -1274,12 +1287,23 @@ async def generate_chapter_content_stream(
|
|||||||
chapter_number=current_chapter.chapter_number,
|
chapter_number=current_chapter.chapter_number,
|
||||||
chapter_title=current_chapter.title,
|
chapter_title=current_chapter.title,
|
||||||
chapter_outline=chapter_outline_content,
|
chapter_outline=chapter_outline_content,
|
||||||
style_content=style_content,
|
|
||||||
target_word_count=target_word_count,
|
target_word_count=target_word_count,
|
||||||
memory_context=memory_context,
|
max_word_count=target_word_count + 1000
|
||||||
mcp_references=mcp_reference_materials,
|
|
||||||
outline_mode=outline_mode
|
|
||||||
)
|
)
|
||||||
|
# 插入模式说明和记忆、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:
|
if mcp_reference_materials:
|
||||||
logger.info(f"📖 已整合MCP参考资料({len(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 '暂无大纲'
|
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
|
||||||
logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容")
|
logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容")
|
||||||
|
|
||||||
# 生成提示词
|
# 生成提示词(支持自定义)
|
||||||
if previous_content:
|
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,
|
title=project.title,
|
||||||
theme=project.theme or '',
|
theme=project.theme or '',
|
||||||
genre=project.genre or '',
|
genre=project.genre or '',
|
||||||
@@ -2429,13 +2456,20 @@ async def generate_single_chapter_for_batch(
|
|||||||
chapter_number=chapter.chapter_number,
|
chapter_number=chapter.chapter_number,
|
||||||
chapter_title=chapter.title,
|
chapter_title=chapter.title,
|
||||||
chapter_outline=chapter_outline_content,
|
chapter_outline=chapter_outline_content,
|
||||||
style_content=style_content,
|
|
||||||
target_word_count=target_word_count,
|
target_word_count=target_word_count,
|
||||||
memory_context=memory_context,
|
max_word_count=target_word_count + 1000,
|
||||||
outline_mode=outline_mode
|
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:
|
else:
|
||||||
prompt = prompt_service.get_chapter_generation_prompt(
|
prompt = base_prompt
|
||||||
|
else:
|
||||||
|
# 获取自定义提示词模板
|
||||||
|
template = await PromptService.get_template("CHAPTER_GENERATION", user_id, db_session)
|
||||||
|
base_prompt = PromptService.format_prompt(
|
||||||
|
template,
|
||||||
title=project.title,
|
title=project.title,
|
||||||
theme=project.theme or '',
|
theme=project.theme or '',
|
||||||
genre=project.genre or '',
|
genre=project.genre or '',
|
||||||
@@ -2449,11 +2483,14 @@ async def generate_single_chapter_for_batch(
|
|||||||
chapter_number=chapter.chapter_number,
|
chapter_number=chapter.chapter_number,
|
||||||
chapter_title=chapter.title,
|
chapter_title=chapter.title,
|
||||||
chapter_outline=chapter_outline_content,
|
chapter_outline=chapter_outline_content,
|
||||||
style_content=style_content,
|
|
||||||
target_word_count=target_word_count,
|
target_word_count=target_word_count,
|
||||||
memory_context=memory_context,
|
max_word_count=target_word_count + 1000
|
||||||
outline_mode=outline_mode
|
|
||||||
)
|
)
|
||||||
|
# 应用写作风格
|
||||||
|
if style_content:
|
||||||
|
prompt = WritingStyleManager.apply_style_to_prompt(base_prompt, style_content)
|
||||||
|
else:
|
||||||
|
prompt = base_prompt
|
||||||
|
|
||||||
# 非流式生成内容
|
# 非流式生成内容
|
||||||
full_content = ""
|
full_content = ""
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from app.schemas.character import (
|
|||||||
CharacterGenerateRequest
|
CharacterGenerateRequest
|
||||||
)
|
)
|
||||||
from app.services.ai_service import AIService
|
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.logger import get_logger
|
||||||
from app.api.settings import get_user_ai_service
|
from app.api.settings import get_user_ai_service
|
||||||
|
|
||||||
@@ -419,8 +419,11 @@ async def generate_character(
|
|||||||
- 其他要求:{request.requirements or '无'}
|
- 其他要求:{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,
|
project_context=project_context,
|
||||||
user_input=user_input
|
user_input=user_input
|
||||||
)
|
)
|
||||||
@@ -825,7 +828,11 @@ async def generate_character_stream(
|
|||||||
|
|
||||||
yield await SSEResponse.send_progress("构建AI提示词...", 20)
|
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,
|
project_context=project_context,
|
||||||
user_input=user_input
|
user_input=user_input
|
||||||
)
|
)
|
||||||
|
|||||||
+50
-118
@@ -1,5 +1,5 @@
|
|||||||
"""灵感模式API - 通过对话引导创建项目"""
|
"""灵感模式API - 通过对话引导创建项目"""
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import json
|
import json
|
||||||
@@ -7,97 +7,13 @@ import json
|
|||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.services.ai_service import AIService
|
from app.services.ai_service import AIService
|
||||||
from app.api.settings import get_user_ai_service
|
from app.api.settings import get_user_ai_service
|
||||||
|
from app.services.prompt_service import prompt_service, PromptService
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
|
|
||||||
router = APIRouter(prefix="/inspiration", tags=["灵感模式"])
|
router = APIRouter(prefix="/inspiration", tags=["灵感模式"])
|
||||||
logger = get_logger(__name__)
|
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设置(递减以保持一致性)
|
||||||
TEMPERATURE_SETTINGS = {
|
TEMPERATURE_SETTINGS = {
|
||||||
"title": 0.8, # 书名阶段可以更有创意
|
"title": 0.8, # 书名阶段可以更有创意
|
||||||
@@ -153,6 +69,8 @@ def validate_options_response(result: Dict[str, Any], step: str, max_retries: in
|
|||||||
@router.post("/generate-options")
|
@router.post("/generate-options")
|
||||||
async def generate_options(
|
async def generate_options(
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
|
http_request: Request,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
ai_service: AIService = Depends(get_user_ai_service)
|
ai_service: AIService = Depends(get_user_ai_service)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -183,26 +101,47 @@ async def generate_options(
|
|||||||
|
|
||||||
logger.info(f"灵感模式:生成{step}阶段的选项(第{attempt + 1}次尝试)")
|
logger.info(f"灵感模式:生成{step}阶段的选项(第{attempt + 1}次尝试)")
|
||||||
|
|
||||||
# 获取对应的提示词模板
|
# 获取用户ID
|
||||||
if step not in INSPIRATION_PROMPTS:
|
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 {
|
return {
|
||||||
"error": f"不支持的步骤: {step}",
|
"error": f"不支持的步骤: {step}",
|
||||||
"prompt": "",
|
"prompt": "",
|
||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_template = INSPIRATION_PROMPTS[step]
|
# 获取自定义提示词模板
|
||||||
|
prompt_template_str = await PromptService.get_template(template_key, user_id, db)
|
||||||
|
|
||||||
# 准备格式化参数(提供默认值避免KeyError)
|
# 准备格式化参数
|
||||||
# 关键改进:保持initial_idea在所有阶段传递,确保内容关联性
|
|
||||||
format_params = {
|
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", ""),
|
"title": context.get("title", ""),
|
||||||
"description": context.get("description", ""),
|
"description": context.get("description", ""),
|
||||||
"theme": context.get("theme", "")
|
"theme": context.get("theme", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
# 格式化系统提示词
|
# 格式化提示词(灵感模式的模板是特殊格式,包含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)
|
system_prompt = prompt_template["system"].format(**format_params)
|
||||||
user_prompt = prompt_template["user"].format(**format_params)
|
user_prompt = prompt_template["user"].format(**format_params)
|
||||||
|
|
||||||
@@ -302,6 +241,8 @@ async def generate_options(
|
|||||||
@router.post("/quick-generate")
|
@router.post("/quick-generate")
|
||||||
async def quick_generate(
|
async def quick_generate(
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
|
http_request: Request,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
ai_service: AIService = Depends(get_user_ai_service)
|
ai_service: AIService = Depends(get_user_ai_service)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -326,6 +267,9 @@ async def quick_generate(
|
|||||||
try:
|
try:
|
||||||
logger.info("灵感模式:智能补全")
|
logger.info("灵感模式:智能补全")
|
||||||
|
|
||||||
|
# 获取用户ID
|
||||||
|
user_id = getattr(http_request.state, 'user_id', None)
|
||||||
|
|
||||||
# 构建补全提示词
|
# 构建补全提示词
|
||||||
existing_info = []
|
existing_info = []
|
||||||
if data.get("title"):
|
if data.get("title"):
|
||||||
@@ -339,35 +283,23 @@ async def quick_generate(
|
|||||||
|
|
||||||
existing_text = "\n".join(existing_info) if existing_info else "暂无信息"
|
existing_text = "\n".join(existing_info) if existing_info else "暂无信息"
|
||||||
|
|
||||||
system_prompt = """你是一位专业的小说创作顾问。用户提供了部分小说信息,请补全缺失的字段。
|
# 获取自定义提示词模板
|
||||||
|
prompt_template_str = await PromptService.get_template("INSPIRATION_QUICK_COMPLETE", user_id, db)
|
||||||
|
|
||||||
用户已提供的信息:
|
# 格式化提示词
|
||||||
{existing}
|
try:
|
||||||
|
prompts = json.loads(prompt_template_str)
|
||||||
请生成完整的小说方案,包含:
|
# 格式化参数
|
||||||
1. title: 书名(3-6字,如果用户已提供则保持原样)
|
prompts["system"] = prompts["system"].replace("{existing}", existing_text)
|
||||||
2. description: 简介(50-100字,必须基于用户提供的信息,不要偏离原意)
|
prompts["user"] = prompts["user"].replace("{existing}", existing_text)
|
||||||
3. theme: 核心主题(30-50字,必须与用户提供的信息保持一致)
|
except (json.JSONDecodeError, KeyError):
|
||||||
4. genre: 类型标签数组(2-3个)
|
# 降级使用原有方法
|
||||||
|
prompts = prompt_service.get_inspiration_quick_complete_prompt(existing=existing_text)
|
||||||
重要:所有补全的内容都必须与用户提供的信息保持高度关联,确保前后一致性。
|
|
||||||
|
|
||||||
返回JSON格式:
|
|
||||||
{{
|
|
||||||
"title": "书名",
|
|
||||||
"description": "简介内容...",
|
|
||||||
"theme": "主题内容...",
|
|
||||||
"genre": ["类型1", "类型2"]
|
|
||||||
}}
|
|
||||||
|
|
||||||
只返回纯JSON,不要有其他文字。"""
|
|
||||||
|
|
||||||
user_prompt = "请补全小说信息"
|
|
||||||
|
|
||||||
# 调用AI
|
# 调用AI
|
||||||
response = await ai_service.generate_text(
|
response = await ai_service.generate_text(
|
||||||
prompt=user_prompt,
|
prompt=prompts["user"],
|
||||||
system_prompt=system_prompt.format(existing=existing_text),
|
system_prompt=prompts["system"],
|
||||||
temperature=0.7
|
temperature=0.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from app.schemas.relationship import (
|
|||||||
)
|
)
|
||||||
from app.schemas.character import CharacterResponse
|
from app.schemas.character import CharacterResponse
|
||||||
from app.services.ai_service import AIService
|
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.logger import get_logger
|
||||||
from app.api.settings import get_user_ai_service
|
from app.api.settings import get_user_ai_service
|
||||||
|
|
||||||
@@ -496,8 +496,11 @@ async def generate_organization(
|
|||||||
- 其他要求:{gen_request.requirements or '无'}
|
- 其他要求:{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,
|
project_context=project_context,
|
||||||
user_input=user_input
|
user_input=user_input
|
||||||
)
|
)
|
||||||
@@ -689,7 +692,11 @@ async def generate_organization_stream(
|
|||||||
|
|
||||||
yield await SSEResponse.send_progress("构建AI提示词...", 20)
|
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,
|
project_context=project_context,
|
||||||
user_input=user_input
|
user_input=user_input
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from app.schemas.outline import (
|
|||||||
CreateChaptersFromPlansResponse
|
CreateChaptersFromPlansResponse
|
||||||
)
|
)
|
||||||
from app.services.ai_service import AIService
|
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.memory_service import memory_service
|
||||||
from app.services.plot_expansion_service import PlotExpansionService
|
from app.services.plot_expansion_service import PlotExpansionService
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
@@ -477,8 +477,10 @@ async def _generate_new_outline(
|
|||||||
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
|
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
|
||||||
mcp_reference_materials = ""
|
mcp_reference_materials = ""
|
||||||
|
|
||||||
# 使用完整提示词(插入MCP参考资料)
|
# 使用完整提示词(插入MCP参考资料,支持自定义)
|
||||||
prompt = prompt_service.get_complete_outline_prompt(
|
template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id, db)
|
||||||
|
prompt = PromptService.format_prompt(
|
||||||
|
template,
|
||||||
title=project.title,
|
title=project.title,
|
||||||
theme=request.theme or project.theme or "未设定",
|
theme=request.theme or project.theme or "未设定",
|
||||||
genre=request.genre or project.genre 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)}")
|
logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,降级为基础模式: {str(e)}")
|
||||||
mcp_reference_materials = ""
|
mcp_reference_materials = ""
|
||||||
|
|
||||||
# 使用标准续写提示词模板(支持记忆+MCP增强)
|
# 使用标准续写提示词模板(支持记忆+MCP增强+自定义)
|
||||||
prompt = prompt_service.get_outline_continue_prompt(
|
template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", user_id, db)
|
||||||
|
prompt = PromptService.format_prompt(
|
||||||
|
template,
|
||||||
title=project.title,
|
title=project.title,
|
||||||
theme=request.theme or project.theme or "未设定",
|
theme=request.theme or project.theme or "未设定",
|
||||||
genre=request.genre or project.genre or "通用",
|
genre=request.genre or project.genre or "通用",
|
||||||
@@ -814,6 +818,7 @@ async def _continue_outline(
|
|||||||
recent_plot=recent_plot,
|
recent_plot=recent_plot,
|
||||||
plot_stage_instruction=stage_instruction,
|
plot_stage_instruction=stage_instruction,
|
||||||
start_chapter=current_start_chapter,
|
start_chapter=current_start_chapter,
|
||||||
|
end_chapter=current_start_chapter + current_batch_size - 1,
|
||||||
story_direction=request.story_direction or "自然延续",
|
story_direction=request.story_direction or "自然延续",
|
||||||
requirements=request.requirements or "",
|
requirements=request.requirements or "",
|
||||||
memory_context=memory_context,
|
memory_context=memory_context,
|
||||||
@@ -1084,9 +1089,11 @@ async def new_outline_generator(
|
|||||||
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
|
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
|
||||||
mcp_reference_materials = ""
|
mcp_reference_materials = ""
|
||||||
|
|
||||||
# 使用完整提示词(插入MCP参考资料)
|
# 使用完整提示词(插入MCP参考资料,支持自定义)
|
||||||
yield await SSEResponse.send_progress("准备AI提示词...", 20)
|
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,
|
title=project.title,
|
||||||
theme=data.get("theme") or project.theme or "未设定",
|
theme=data.get("theme") or project.theme or "未设定",
|
||||||
genre=data.get("genre") or project.genre or "通用",
|
genre=data.get("genre") or project.genre or "通用",
|
||||||
@@ -1412,8 +1419,10 @@ async def continue_outline_generator(
|
|||||||
batch_progress + 5
|
batch_progress + 5
|
||||||
)
|
)
|
||||||
|
|
||||||
# 使用标准续写提示词模板(支持记忆+MCP增强)
|
# 使用标准续写提示词模板(支持记忆+MCP增强+自定义)
|
||||||
prompt = prompt_service.get_outline_continue_prompt(
|
template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", user_id, db)
|
||||||
|
prompt = PromptService.format_prompt(
|
||||||
|
template,
|
||||||
title=project.title,
|
title=project.title,
|
||||||
theme=data.get("theme") or project.theme or "未设定",
|
theme=data.get("theme") or project.theme or "未设定",
|
||||||
genre=data.get("genre") or project.genre or "通用",
|
genre=data.get("genre") or project.genre or "通用",
|
||||||
@@ -1429,6 +1438,7 @@ async def continue_outline_generator(
|
|||||||
recent_plot=recent_plot,
|
recent_plot=recent_plot,
|
||||||
plot_stage_instruction=stage_instruction,
|
plot_stage_instruction=stage_instruction,
|
||||||
start_chapter=current_start_chapter,
|
start_chapter=current_start_chapter,
|
||||||
|
end_chapter=current_start_chapter + current_batch_size - 1,
|
||||||
story_direction=data.get("story_direction", "自然延续"),
|
story_direction=data.get("story_direction", "自然延续"),
|
||||||
requirements=data.get("requirements", ""),
|
requirements=data.get("requirements", ""),
|
||||||
memory_context=memory_context,
|
memory_context=memory_context,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""AI去味API - 核心特色功能"""
|
"""AI去味API - 核心特色功能"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.generation_history import GenerationHistory
|
from app.models.generation_history import GenerationHistory
|
||||||
from app.schemas.polish import PolishRequest, PolishResponse
|
from app.schemas.polish import PolishRequest, PolishResponse
|
||||||
from app.services.ai_service import AIService
|
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.logger import get_logger
|
||||||
from app.api.settings import get_user_ai_service
|
from app.api.settings import get_user_ai_service
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ logger = get_logger(__name__)
|
|||||||
@router.post("", response_model=PolishResponse, summary="AI去味")
|
@router.post("", response_model=PolishResponse, summary="AI去味")
|
||||||
async def polish_text(
|
async def polish_text(
|
||||||
request: PolishRequest,
|
request: PolishRequest,
|
||||||
|
http_request: Request,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||||
):
|
):
|
||||||
@@ -32,8 +33,14 @@ async def polish_text(
|
|||||||
这是本项目的核心特色功能!
|
这是本项目的核心特色功能!
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 构建AI去味提示词
|
# 获取用户ID
|
||||||
prompt = prompt_service.get_denoising_prompt(
|
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
|
original_text=request.original_text
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,6 +92,7 @@ async def polish_batch(
|
|||||||
project_id: int = None,
|
project_id: int = None,
|
||||||
provider: str = None,
|
provider: str = None,
|
||||||
model: str = None,
|
model: str = None,
|
||||||
|
http_request: Request = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||||
):
|
):
|
||||||
@@ -94,12 +102,18 @@ async def polish_batch(
|
|||||||
适用于一次性处理多个章节或段落
|
适用于一次性处理多个章节或段落
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 获取用户ID
|
||||||
|
user_id = getattr(http_request.state, 'user_id', None) if http_request else None
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for idx, text in enumerate(texts):
|
for idx, text in enumerate(texts):
|
||||||
logger.info(f"处理第 {idx+1}/{len(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(
|
polished_text = await user_ai_service.generate_text(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ from app.models.writing_style import WritingStyle
|
|||||||
from app.models.project_default_style import ProjectDefaultStyle
|
from app.models.project_default_style import ProjectDefaultStyle
|
||||||
from app.services.ai_service import AIService
|
from app.services.ai_service import AIService
|
||||||
from app.services.mcp_tool_service import MCPToolService
|
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.services.plot_expansion_service import PlotExpansionService
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
from app.utils.sse_response import SSEResponse, create_sse_response
|
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)
|
yield await SSEResponse.send_error("title、description、theme 和 genre 是必需的参数", 400)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 获取基础提示词
|
# 获取基础提示词(支持自定义)
|
||||||
yield await SSEResponse.send_progress("准备AI提示词...", 15)
|
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,
|
title=title,
|
||||||
theme=theme,
|
theme=theme,
|
||||||
genre=genre
|
genre=genre or "通用类型"
|
||||||
)
|
)
|
||||||
|
|
||||||
# MCP工具增强:收集参考资料
|
# MCP工具增强:收集参考资料
|
||||||
@@ -455,8 +457,11 @@ async def characters_generator(
|
|||||||
else:
|
else:
|
||||||
batch_requirements += "\n主要是配角(supporting)和反派(antagonist)"
|
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, # 传递精确数量
|
count=current_batch_size, # 传递精确数量
|
||||||
time_period=world_context.get("time_period", ""),
|
time_period=world_context.get("time_period", ""),
|
||||||
location=world_context.get("location", ""),
|
location=world_context.get("location", ""),
|
||||||
@@ -954,7 +959,10 @@ async def outline_generator(
|
|||||||
outline_requirements += "4. 不要试图完结故事,这只是开始部分\n"
|
outline_requirements += "4. 不要试图完结故事,这只是开始部分\n"
|
||||||
outline_requirements += "5. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记\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,
|
title=project.title,
|
||||||
theme=project.theme or "未设定",
|
theme=project.theme or "未设定",
|
||||||
genre=project.genre or "通用",
|
genre=project.genre or "通用",
|
||||||
@@ -966,6 +974,7 @@ async def outline_generator(
|
|||||||
atmosphere=project.world_atmosphere or "未设定",
|
atmosphere=project.world_atmosphere or "未设定",
|
||||||
rules=project.world_rules or "未设定",
|
rules=project.world_rules or "未设定",
|
||||||
characters_info=characters_info or "暂无角色信息",
|
characters_info=characters_info or "暂无角色信息",
|
||||||
|
mcp_references="",
|
||||||
requirements=outline_requirements
|
requirements=outline_requirements
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1150,9 +1159,11 @@ async def world_building_regenerate_generator(
|
|||||||
enable_mcp = data.get("enable_mcp", True)
|
enable_mcp = data.get("enable_mcp", True)
|
||||||
user_id = data.get("user_id")
|
user_id = data.get("user_id")
|
||||||
|
|
||||||
# 获取基础提示词
|
# 获取基础提示词(支持自定义)
|
||||||
yield await SSEResponse.send_progress("准备AI提示词...", 15)
|
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,
|
title=project.title,
|
||||||
theme=project.theme or "未设定",
|
theme=project.theme or "未设定",
|
||||||
genre=project.genre or "通用"
|
genre=project.genre or "通用"
|
||||||
|
|||||||
+2
-1
@@ -142,7 +142,7 @@ from app.api import (
|
|||||||
projects, outlines, characters, chapters,
|
projects, outlines, characters, chapters,
|
||||||
wizard_stream, relationships, organizations,
|
wizard_stream, relationships, organizations,
|
||||||
auth, users, settings, writing_styles, memories,
|
auth, users, settings, writing_styles, memories,
|
||||||
mcp_plugins, admin, inspiration
|
mcp_plugins, admin, inspiration, prompt_templates
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(auth.router, prefix="/api")
|
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(writing_styles.router, prefix="/api")
|
||||||
app.include_router(memories.router) # 记忆管理API (已包含/api前缀)
|
app.include_router(memories.router) # 记忆管理API (已包含/api前缀)
|
||||||
app.include_router(mcp_plugins.router, prefix="/api") # MCP插件管理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"
|
static_dir = Path(__file__).parent.parent / "static"
|
||||||
if static_dir.exists():
|
if static_dir.exists():
|
||||||
|
|||||||
@@ -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"<PromptTemplate(id={self.id}, user_id={self.user_id}, template_key={self.template_key})>"
|
||||||
@@ -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="参数字典")
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"""章节重新生成服务"""
|
"""章节重新生成服务"""
|
||||||
from typing import Dict, Any, AsyncGenerator, Optional, List
|
from typing import Dict, Any, AsyncGenerator, Optional, List
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.services.ai_service import AIService
|
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.chapter import Chapter
|
||||||
from app.models.memory import PlotAnalysis
|
from app.models.memory import PlotAnalysis
|
||||||
from app.schemas.regeneration import ChapterRegenerateRequest, PreserveElementsConfig
|
from app.schemas.regeneration import ChapterRegenerateRequest, PreserveElementsConfig
|
||||||
@@ -24,7 +25,9 @@ class ChapterRegenerator:
|
|||||||
analysis: Optional[PlotAnalysis],
|
analysis: Optional[PlotAnalysis],
|
||||||
regenerate_request: ChapterRegenerateRequest,
|
regenerate_request: ChapterRegenerateRequest,
|
||||||
project_context: Dict[str, Any],
|
project_context: Dict[str, Any],
|
||||||
style_content: str = ""
|
style_content: str = "",
|
||||||
|
user_id: str = None,
|
||||||
|
db: AsyncSession = None
|
||||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
"""
|
"""
|
||||||
根据反馈重新生成章节(流式)
|
根据反馈重新生成章节(流式)
|
||||||
@@ -34,6 +37,9 @@ class ChapterRegenerator:
|
|||||||
analysis: 分析结果(可选)
|
analysis: 分析结果(可选)
|
||||||
regenerate_request: 重新生成请求参数
|
regenerate_request: 重新生成请求参数
|
||||||
project_context: 项目上下文(项目信息、角色、大纲等)
|
project_context: 项目上下文(项目信息、角色、大纲等)
|
||||||
|
style_content: 写作风格
|
||||||
|
user_id: 用户ID(用于获取自定义提示词)
|
||||||
|
db: 数据库会话(用于查询自定义提示词)
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
包含类型和数据的字典: {'type': 'progress'/'chunk', 'data': ...}
|
包含类型和数据的字典: {'type': 'progress'/'chunk', 'data': ...}
|
||||||
@@ -52,12 +58,14 @@ class ChapterRegenerator:
|
|||||||
|
|
||||||
# 2. 构建完整提示词
|
# 2. 构建完整提示词
|
||||||
yield {'type': 'progress', 'progress': 10, 'message': '正在构建生成提示词...'}
|
yield {'type': 'progress', 'progress': 10, 'message': '正在构建生成提示词...'}
|
||||||
full_prompt = self._build_regeneration_prompt(
|
full_prompt = await self._build_regeneration_prompt(
|
||||||
chapter=chapter,
|
chapter=chapter,
|
||||||
modification_instructions=modification_instructions,
|
modification_instructions=modification_instructions,
|
||||||
project_context=project_context,
|
project_context=project_context,
|
||||||
regenerate_request=regenerate_request,
|
regenerate_request=regenerate_request,
|
||||||
style_content=style_content
|
style_content=style_content,
|
||||||
|
user_id=user_id,
|
||||||
|
db=db
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"🎯 提示词构建完成,开始AI生成")
|
logger.info(f"🎯 提示词构建完成,开始AI生成")
|
||||||
@@ -158,126 +166,31 @@ class ChapterRegenerator:
|
|||||||
|
|
||||||
return "\n".join(instructions)
|
return "\n".join(instructions)
|
||||||
|
|
||||||
def _build_regeneration_prompt(
|
async def _build_regeneration_prompt(
|
||||||
self,
|
self,
|
||||||
chapter: Chapter,
|
chapter: Chapter,
|
||||||
modification_instructions: str,
|
modification_instructions: str,
|
||||||
project_context: Dict[str, Any],
|
project_context: Dict[str, Any],
|
||||||
regenerate_request: ChapterRegenerateRequest,
|
regenerate_request: ChapterRegenerateRequest,
|
||||||
style_content: str = ""
|
style_content: str = "",
|
||||||
|
user_id: str = None,
|
||||||
|
db: AsyncSession = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""构建完整的重新生成提示词"""
|
"""构建完整的重新生成提示词"""
|
||||||
|
# 获取自定义提示词模板
|
||||||
prompt_parts = []
|
template = await PromptService.get_template("CHAPTER_REGENERATION", user_id, db)
|
||||||
|
# 格式化提示词
|
||||||
# 系统角色
|
return PromptService.format_prompt(
|
||||||
prompt_parts.append("""你是一位经验丰富的专业小说编辑和作家。现在需要根据反馈意见重新创作一个章节。
|
template,
|
||||||
|
chapter_number=chapter.chapter_number,
|
||||||
你的任务是:
|
title=chapter.title,
|
||||||
1. 仔细理解原章节的内容和意图
|
word_count=chapter.word_count,
|
||||||
2. 认真分析所有的修改要求
|
content=chapter.content,
|
||||||
3. 在保持故事连贯性的前提下,创作一个改进后的新版本
|
modification_instructions=modification_instructions,
|
||||||
4. 确保新版本在艺术性和可读性上都有明显提升
|
project_context=project_context,
|
||||||
|
style_content=style_content,
|
||||||
---
|
target_word_count=regenerate_request.target_word_count
|
||||||
""")
|
)
|
||||||
|
|
||||||
# 原始章节信息
|
|
||||||
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)
|
|
||||||
|
|
||||||
def calculate_content_diff(
|
def calculate_content_diff(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from app.models.settings import Settings as UserSettings
|
|||||||
from app.mcp.registry import mcp_registry
|
from app.mcp.registry import mcp_registry
|
||||||
from app.services.ai_service import create_user_ai_service
|
from app.services.ai_service import create_user_ai_service
|
||||||
from app.schemas.mcp_plugin import MCPTestResult
|
from app.schemas.mcp_plugin import MCPTestResult
|
||||||
|
from app.services.prompt_service import prompt_service
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
from app.user_manager import User
|
from app.user_manager import User
|
||||||
|
|
||||||
@@ -168,26 +169,11 @@ class MCPTestService:
|
|||||||
logger.debug(f"📋 OpenAI工具列表: {[t['function']['name'] for t in openai_tools]}")
|
logger.debug(f"📋 OpenAI工具列表: {[t['function']['name'] for t in openai_tools]}")
|
||||||
|
|
||||||
# 调用AI选择工具
|
# 调用AI选择工具
|
||||||
prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。
|
prompts = prompt_service.get_mcp_tool_test_prompts(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 中定义的完全一致,包括大小写和命名风格"""
|
|
||||||
|
|
||||||
ai_response = await ai_service.generate_text(
|
ai_response = await ai_service.generate_text(
|
||||||
prompt=prompt,
|
prompt=prompts["user"],
|
||||||
system_prompt=system_prompt,
|
system_prompt=prompts["system"],
|
||||||
tools=openai_tools,
|
tools=openai_tools,
|
||||||
tool_choice="required"
|
tool_choice="required"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""剧情分析服务 - 自动分析章节的钩子、伏笔、冲突等元素"""
|
"""剧情分析服务 - 自动分析章节的钩子、伏笔、冲突等元素"""
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.services.ai_service import AIService
|
from app.services.ai_service import AIService
|
||||||
|
from app.services.prompt_service import prompt_service, PromptService
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@@ -11,169 +13,6 @@ logger = get_logger(__name__)
|
|||||||
class PlotAnalyzer:
|
class PlotAnalyzer:
|
||||||
"""剧情分析器 - 使用AI分析章节内容"""
|
"""剧情分析器 - 使用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):
|
def __init__(self, ai_service: AIService):
|
||||||
"""
|
"""
|
||||||
初始化剧情分析器
|
初始化剧情分析器
|
||||||
@@ -189,7 +28,9 @@ class PlotAnalyzer:
|
|||||||
chapter_number: int,
|
chapter_number: int,
|
||||||
title: str,
|
title: str,
|
||||||
content: str,
|
content: str,
|
||||||
word_count: int
|
word_count: int,
|
||||||
|
user_id: str = None,
|
||||||
|
db: AsyncSession = None
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
分析单章内容
|
分析单章内容
|
||||||
@@ -199,6 +40,8 @@ class PlotAnalyzer:
|
|||||||
title: 章节标题
|
title: 章节标题
|
||||||
content: 章节内容
|
content: 章节内容
|
||||||
word_count: 字数
|
word_count: 字数
|
||||||
|
user_id: 用户ID(用于获取自定义提示词)
|
||||||
|
db: 数据库会话(用于查询自定义提示词)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
分析结果字典,失败返回None
|
分析结果字典,失败返回None
|
||||||
@@ -209,8 +52,11 @@ class PlotAnalyzer:
|
|||||||
# 如果内容过长,截取前8000字(避免超token)
|
# 如果内容过长,截取前8000字(避免超token)
|
||||||
analysis_content = content[:8000] if len(content) > 8000 else content
|
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,
|
chapter_number=chapter_number,
|
||||||
title=title,
|
title=title,
|
||||||
word_count=word_count,
|
word_count=word_count,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from app.models.project import Project
|
|||||||
from app.models.character import Character
|
from app.models.character import Character
|
||||||
from app.models.chapter import Chapter
|
from app.models.chapter import Chapter
|
||||||
from app.services.ai_service import AIService
|
from app.services.ai_service import AIService
|
||||||
|
from app.services.prompt_service import prompt_service, PromptService
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -107,15 +108,27 @@ class PlotExpansionService:
|
|||||||
# 获取大纲上下文(前后大纲)
|
# 获取大纲上下文(前后大纲)
|
||||||
context_info = await self._get_outline_context(outline, project.id, db)
|
context_info = await self._get_outline_context(outline, project.id, db)
|
||||||
|
|
||||||
# 构建分析提示词
|
# 获取自定义提示词模板
|
||||||
prompt = self._build_expansion_prompt(
|
template = await PromptService.get_template("PLOT_EXPANSION_SINGLE_BATCH", project.user_id, db)
|
||||||
outline=outline,
|
# 格式化提示词
|
||||||
project=project,
|
prompt = PromptService.format_prompt(
|
||||||
characters_info=characters_info,
|
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,
|
context_info=context_info,
|
||||||
|
strategy_instruction=expansion_strategy,
|
||||||
target_chapter_count=target_chapter_count,
|
target_chapter_count=target_chapter_count,
|
||||||
expansion_strategy=expansion_strategy,
|
scene_instruction="", # 暂时为空
|
||||||
enable_scene_analysis=enable_scene_analysis
|
scene_field="" # 暂时为空
|
||||||
)
|
)
|
||||||
|
|
||||||
# 调用AI生成章节规划
|
# 调用AI生成章节规划
|
||||||
@@ -182,17 +195,43 @@ class PlotExpansionService:
|
|||||||
await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size)
|
await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size)
|
||||||
|
|
||||||
# 构建当前批次的提示词(包含已生成章节的上下文)
|
# 构建当前批次的提示词(包含已生成章节的上下文)
|
||||||
prompt = self._build_batch_expansion_prompt(
|
previous_context = ""
|
||||||
outline=outline,
|
if all_chapter_plans:
|
||||||
project=project,
|
previous_summaries = []
|
||||||
characters_info=characters_info,
|
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,
|
context_info=context_info,
|
||||||
target_chapter_count=current_batch_size,
|
previous_context=previous_context,
|
||||||
expansion_strategy=expansion_strategy,
|
strategy_instruction=expansion_strategy,
|
||||||
enable_scene_analysis=enable_scene_analysis,
|
|
||||||
start_index=current_start_index,
|
start_index=current_start_index,
|
||||||
previous_chapters=all_chapter_plans,
|
end_index=current_start_index + current_batch_size - 1,
|
||||||
total_chapters=target_chapter_count
|
target_chapter_count=current_batch_size,
|
||||||
|
scene_instruction="", # 暂时为空
|
||||||
|
scene_field="" # 暂时为空
|
||||||
)
|
)
|
||||||
|
|
||||||
# 调用AI生成当前批次
|
# 调用AI生成当前批次
|
||||||
@@ -452,258 +491,6 @@ class PlotExpansionService:
|
|||||||
|
|
||||||
return context if context else "(无前后文)"
|
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(
|
def _parse_expansion_response(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -852,6 +852,509 @@ class PromptService:
|
|||||||
2. 所有内容描述中严禁使用任何特殊符号
|
2. 所有内容描述中严禁使用任何特殊符号
|
||||||
3. 不要有任何额外的文字说明"""
|
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} 个章节的详细规划。
|
OUTLINE_EXPANSION = """你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapters} 个章节的详细规划。
|
||||||
|
|
||||||
@@ -1340,6 +1843,416 @@ class PromptService:
|
|||||||
scene_field=scene_field
|
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()
|
prompt_service = PromptService()
|
||||||
@@ -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 '是否为系统默认模板';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import WritingStyles from './pages/WritingStyles';
|
|||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
import MCPPlugins from './pages/MCPPlugins';
|
import MCPPlugins from './pages/MCPPlugins';
|
||||||
import UserManagement from './pages/UserManagement';
|
import UserManagement from './pages/UserManagement';
|
||||||
|
import PromptTemplates from './pages/PromptTemplates';
|
||||||
// import Polish from './pages/Polish';
|
// import Polish from './pages/Polish';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import AuthCallback from './pages/AuthCallback';
|
import AuthCallback from './pages/AuthCallback';
|
||||||
@@ -42,6 +43,7 @@ function App() {
|
|||||||
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
|
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
|
||||||
<Route path="/inspiration" element={<ProtectedRoute><Inspiration /></ProtectedRoute>} />
|
<Route path="/inspiration" element={<ProtectedRoute><Inspiration /></ProtectedRoute>} />
|
||||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||||
|
<Route path="/prompt-templates" element={<ProtectedRoute><><PromptTemplates /><AppFooter /></></ProtectedRoute>} />
|
||||||
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
|
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
|
||||||
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
|
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
|
||||||
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
|
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
|
||||||
|
|||||||
@@ -351,9 +351,9 @@ const Inspiration: React.FC = () => {
|
|||||||
type: 'ai',
|
type: 'ai',
|
||||||
content: `很好!现在请选择你想要的大纲模式:
|
content: `很好!现在请选择你想要的大纲模式:
|
||||||
|
|
||||||
📋 **一对一模式**:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
||||||
|
|
||||||
📚 **一对多模式**:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
||||||
|
|
||||||
请选择:`,
|
请选择:`,
|
||||||
options: ['📋 一对一模式', '📚 一对多模式']
|
options: ['📋 一对一模式', '📚 一对多模式']
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { 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 { projectApi } from '../services/api';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { useProjectSync } from '../store/hooks';
|
import { useProjectSync } from '../store/hooks';
|
||||||
@@ -438,6 +438,12 @@ export default function ProjectList() {
|
|||||||
{
|
{
|
||||||
type: 'divider'
|
type: 'divider'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'prompt-templates',
|
||||||
|
label: '提示词管理',
|
||||||
|
icon: <FileSearchOutlined />,
|
||||||
|
onClick: () => navigate('/prompt-templates')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'mcp',
|
key: 'mcp',
|
||||||
label: 'MCP插件',
|
label: 'MCP插件',
|
||||||
@@ -546,6 +552,12 @@ export default function ProjectList() {
|
|||||||
{
|
{
|
||||||
type: 'divider'
|
type: 'divider'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'prompt-templates',
|
||||||
|
label: '提示词管理',
|
||||||
|
icon: <FileSearchOutlined />,
|
||||||
|
onClick: () => navigate('/prompt-templates')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'mcp',
|
key: 'mcp',
|
||||||
label: 'MCP插件',
|
label: 'MCP插件',
|
||||||
|
|||||||
@@ -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<CategoryGroup[]>([]);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('0');
|
||||||
|
const [editingTemplate, setEditingTemplate] = useState<PromptTemplate | null>(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<CategoryGroup[]>('/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 (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
padding: isMobile ? '20px 16px' : '40px 24px'
|
||||||
|
}}>
|
||||||
|
{/* 头部卡片 */}
|
||||||
|
<div style={{
|
||||||
|
maxWidth: 1400,
|
||||||
|
margin: '0 auto',
|
||||||
|
marginBottom: isMobile ? 20 : 40
|
||||||
|
}}>
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
borderRadius: isMobile ? 12 : 16,
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row align="middle" justify="space-between" gutter={[16, 16]}>
|
||||||
|
<Col xs={24} sm={12} md={14}>
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Space align="center">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/projects')}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
/>
|
||||||
|
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
|
||||||
|
<FileSearchOutlined style={{ color: '#667eea', marginRight: 8 }} />
|
||||||
|
提示词模板管理
|
||||||
|
</Title>
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14, marginLeft: isMobile ? 40 : 48 }}>
|
||||||
|
自定义AI生成提示词,打造个性化创作体验
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={10}>
|
||||||
|
<Space wrap style={{ justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={handleExport}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
导出配置
|
||||||
|
</Button>
|
||||||
|
<Upload
|
||||||
|
accept=".json"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={handleImport}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
导入配置
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 使用提示 */}
|
||||||
|
<Alert
|
||||||
|
message={
|
||||||
|
<Space align="center">
|
||||||
|
<InfoCircleOutlined style={{ fontSize: 16, color: '#1890ff' }} />
|
||||||
|
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使用说明</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', marginBottom: 8 }}>
|
||||||
|
• <strong>系统默认模板</strong>(灰色头部):始终启用,无需手动开关。点击"编辑"后将创建您的自定义副本。
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block' }}>
|
||||||
|
• <strong>已自定义模板</strong>(紫色头部):可通过开关控制启用/禁用,使用 <Text code>{'{variable_name}'}</Text> 格式表示变量占位符。点击"重置"可恢复为系统默认。
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
type="info"
|
||||||
|
showIcon={false}
|
||||||
|
style={{
|
||||||
|
marginTop: isMobile ? 16 : 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)',
|
||||||
|
border: '1px solid #91d5ff'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{/* 分类标签 */}
|
||||||
|
{categories.length > 0 && (
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
borderRadius: isMobile ? 12 : 16,
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
marginBottom: isMobile ? 16 : 24
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
activeKey={selectedCategory}
|
||||||
|
onChange={setSelectedCategory}
|
||||||
|
items={[
|
||||||
|
{ key: '0', label: `全部 (${categories.reduce((sum, cat) => sum + cat.count, 0)})` },
|
||||||
|
...categories.map((cat, index) => ({
|
||||||
|
key: (index + 1).toString(),
|
||||||
|
label: `${cat.category} (${cat.count})`
|
||||||
|
}))
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 模板列表 */}
|
||||||
|
{currentTemplates.length === 0 ? (
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Empty
|
||||||
|
description="暂无模板数据"
|
||||||
|
style={{ padding: '80px 0' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{currentTemplates.map(template => (
|
||||||
|
<Col {...gridConfig} key={template.id}>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
variant="borderless"
|
||||||
|
style={cardStyles.project}
|
||||||
|
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
||||||
|
{...cardHoverHandlers}
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<div style={{
|
||||||
|
background: template.is_system_default
|
||||||
|
? 'linear-gradient(135deg, #a8a8a8 0%, #636363 100%)'
|
||||||
|
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
padding: isMobile ? '16px' : '20px',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: '#fff', flex: 1 }} ellipsis>
|
||||||
|
{template.template_name}
|
||||||
|
</Title>
|
||||||
|
{!template.is_system_default && (
|
||||||
|
<Switch
|
||||||
|
checked={template.is_active}
|
||||||
|
onChange={(checked) => handleToggleActive(template, checked)}
|
||||||
|
size={isMobile ? 'small' : 'default'}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Space wrap>
|
||||||
|
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
|
||||||
|
{template.category}
|
||||||
|
</Tag>
|
||||||
|
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
|
||||||
|
{template.is_system_default ? '系统默认' : '已自定义'}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div style={{ padding: isMobile ? '16px' : '20px' }}>
|
||||||
|
<Paragraph
|
||||||
|
type="secondary"
|
||||||
|
ellipsis={{ rows: 3 }}
|
||||||
|
style={{ minHeight: 66, marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
{template.description || '暂无描述'}
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<Space wrap style={{ marginBottom: 16 }}>
|
||||||
|
<Tag
|
||||||
|
icon={<CheckCircleOutlined />}
|
||||||
|
color={template.is_system_default || template.is_active ? 'success' : 'default'}
|
||||||
|
>
|
||||||
|
{template.is_system_default ? '始终启用' : (template.is_active ? '已启用' : '已禁用')}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 16 }}>
|
||||||
|
模板键: {template.template_key}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(template)}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
style={{ borderRadius: 6 }}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => handleReset(template.template_key)}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
style={{ borderRadius: 6 }}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 编辑对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={`编辑模板: ${editingTemplate?.template_name}`}
|
||||||
|
open={editorVisible}
|
||||||
|
onCancel={() => 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}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>模板名称</label>
|
||||||
|
<Input
|
||||||
|
value={editingTemplate?.template_name || ''}
|
||||||
|
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, template_name: e.target.value } : null)}
|
||||||
|
placeholder="输入模板名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>描述</label>
|
||||||
|
<TextArea
|
||||||
|
value={editingTemplate?.description || ''}
|
||||||
|
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, description: e.target.value } : null)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="简要描述模板用途"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>模板内容</label>
|
||||||
|
<TextArea
|
||||||
|
value={editingTemplate?.template_content || ''}
|
||||||
|
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, template_content: e.target.value } : null)}
|
||||||
|
rows={isMobile ? 15 : 20}
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '13px' }}
|
||||||
|
placeholder="输入提示词模板内容..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
message="提示:使用 {variable_name} 格式表示变量占位符"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user