diff --git a/README.md b/README.md
index 37b2ece..52a637b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+



@@ -22,7 +22,7 @@
如果这个项目对你有帮助,欢迎通过以下方式支持开发:
-**[☕ 请我喝杯咖啡](https://zanzhupage.vercel.app/)**
+**[☕ 请我喝杯咖啡](https://mumuverse.space:1588/)**
您的支持是我持续开发的动力!🙏
diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py
index 87cde3b..2fbb3c6 100644
--- a/backend/app/api/chapters.py
+++ b/backend/app/api/chapters.py
@@ -1124,14 +1124,23 @@ async def generate_chapter_content_stream(
# 发送开始事件
yield f"data: {json.dumps({'type': 'start', 'message': '开始AI创作...'}, ensure_ascii=False)}\n\n"
- # 🔧 MCP工具增强:收集章节参考资料
+ # 🔧 MCP工具增强:收集章节参考资料(优化版)
mcp_reference_materials = ""
if enable_mcp and current_user_id:
try:
- yield f"data: {json.dumps({'type': 'progress', 'message': '🔍 尝试使用MCP工具收集参考资料...', 'progress': 28}, ensure_ascii=False)}\n\n"
+ # 1️⃣ 静默检查工具可用性
+ from app.services.mcp_tool_service import mcp_tool_service
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=current_user_id,
+ db_session=db_session
+ )
- # 构建资料收集提示词
- planning_prompt = f"""你正在为小说《{project.title}》创作第{current_chapter.chapter_number}章《{current_chapter.title}》。
+ # 2️⃣ 只在有工具时才显示消息和调用
+ if available_tools:
+ yield f"data: {json.dumps({'type': 'progress', 'message': '🔍 使用MCP工具收集参考资料...', 'progress': 28}, ensure_ascii=False)}\n\n"
+
+ # 构建资料收集提示词
+ planning_prompt = f"""你正在为小说《{project.title}》创作第{current_chapter.chapter_number}章《{current_chapter.title}》。
【章节大纲】
{outline.content if outline else current_chapter.summary or '暂无大纲'}
@@ -1151,30 +1160,32 @@ async def generate_chapter_content_stream(
4. 文化习俗和生活细节
请根据章节内容,有针对性地查询1-2个最关键的问题。"""
-
- # 调用MCP增强的AI(非流式,最多2轮工具调用)
- planning_result = await user_ai_service.generate_text_with_mcp(
- prompt=planning_prompt,
- user_id=current_user_id,
- db_session=db_session,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- # 提取参考资料
- if planning_result.get("tool_calls_made", 0) > 0:
- tool_count = planning_result["tool_calls_made"]
- yield f"data: {json.dumps({'type': 'progress', 'message': f'✅ MCP工具调用成功({tool_count}次)', 'progress': 32}, ensure_ascii=False)}\n\n"
- mcp_reference_materials = planning_result.get("content", "")
- logger.info(f"📚 MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+
+ # 调用MCP增强的AI(非流式,限制1轮避免超时)
+ planning_result = await user_ai_service.generate_text_with_mcp(
+ prompt=planning_prompt,
+ user_id=current_user_id,
+ db_session=db_session,
+ enable_mcp=True,
+ max_tool_rounds=1, # ✅ 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ # 3️⃣ 提取参考资料并显示结果
+ if planning_result.get("tool_calls_made", 0) > 0:
+ tool_count = planning_result["tool_calls_made"]
+ yield f"data: {json.dumps({'type': 'progress', 'message': f'✅ MCP工具调用成功({tool_count}次)', 'progress': 32}, ensure_ascii=False)}\n\n"
+ mcp_reference_materials = planning_result.get("content", "")
+ logger.info(f"📚 MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+ else:
+ yield f"data: {json.dumps({'type': 'progress', 'message': 'ℹ️ MCP未使用工具,继续', 'progress': 32}, ensure_ascii=False)}\n\n"
else:
- yield f"data: {json.dumps({'type': 'progress', 'message': 'ℹ️ 未使用MCP工具(无可用工具或不需要)', 'progress': 32}, ensure_ascii=False)}\n\n"
+ logger.debug(f"用户 {current_user_id} 未启用MCP工具,跳过MCP增强")
except Exception as e:
- logger.warning(f"MCP工具调用失败(降级处理): {e}")
+ logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
yield f"data: {json.dumps({'type': 'progress', 'message': '⚠️ MCP工具暂时不可用,使用基础模式', 'progress': 32}, ensure_ascii=False)}\n\n"
# 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料
diff --git a/backend/app/api/characters.py b/backend/app/api/characters.py
index 2253c14..8f64496 100644
--- a/backend/app/api/characters.py
+++ b/backend/app/api/characters.py
@@ -436,27 +436,54 @@ async def generate_character(
logger.info(f" - 用户ID:{user_id}")
try:
- # 使用支持MCP的生成方法
- result = await user_ai_service.generate_text_with_mcp(
- prompt=prompt,
- user_id=user_id,
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None, # 使用AIService初始化时的配置
- model=None # 使用AIService初始化时的配置
- )
-
- # 提取内容
- if isinstance(result, dict):
- ai_response = result.get('content', '')
- logger.info(f"✅ AI响应接收完成(MCP增强),长度:{len(ai_response)} 字符")
- if result.get('tool_calls'):
- logger.info(f" - 工具调用:{len(result['tool_calls'])} 次")
+ # 🔧 MCP工具增强:静默检查并收集参考资料
+ mcp_enhanced_prompt = prompt
+ if user_id:
+ try:
+ from app.services.mcp_tool_service import mcp_tool_service
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id,
+ db_session=db
+ )
+
+ # 只在有工具时才调用
+ if available_tools:
+ logger.info(f"🔍 检测到可用MCP工具,尝试收集参考资料...")
+ result = await user_ai_service.generate_text_with_mcp(
+ prompt=prompt,
+ user_id=user_id,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ # 提取内容
+ if isinstance(result, dict):
+ ai_response = result.get('content', '')
+ logger.info(f"✅ AI响应接收完成(MCP增强),长度:{len(ai_response)} 字符")
+ if result.get('tool_calls_made', 0) > 0:
+ logger.info(f" - MCP工具调用:{result['tool_calls_made']} 次")
+ else:
+ ai_response = result
+ logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符")
+ else:
+ logger.debug(f"用户 {user_id} 未启用MCP工具,使用基础模式")
+ # 不使用MCP,直接生成
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
+
+ except Exception as mcp_error:
+ logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(mcp_error)}")
+ # 降级:不使用MCP
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
else:
- ai_response = result
- logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符")
+ # 无用户ID,直接使用基础模式
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
except Exception as ai_error:
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
@@ -807,21 +834,47 @@ async def generate_character_stream(
logger.info(f"🎯 开始为项目 {request.project_id} 生成角色(SSE流式)")
try:
- result = await user_ai_service.generate_text_with_mcp(
- prompt=prompt,
- user_id=user_id,
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- if isinstance(result, dict):
- ai_response = result.get('content', '')
+ # 🔧 MCP工具增强:静默检查并收集参考资料
+ if user_id:
+ try:
+ from app.services.mcp_tool_service import mcp_tool_service
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id,
+ db_session=db
+ )
+
+ # 只在有工具时才调用
+ if available_tools:
+ logger.info(f"🔍 检测到可用MCP工具,尝试收集参考资料...")
+ result = await user_ai_service.generate_text_with_mcp(
+ prompt=prompt,
+ user_id=user_id,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ if isinstance(result, dict):
+ ai_response = result.get('content', '')
+ if result.get('tool_calls_made', 0) > 0:
+ logger.info(f"✅ MCP工具调用成功({result['tool_calls_made']}次)")
+ else:
+ ai_response = result
+ else:
+ logger.debug(f"用户 {user_id} 未启用MCP工具,使用基础模式")
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
+
+ except Exception as mcp_error:
+ logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(mcp_error)}")
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
else:
- ai_response = result
+ result = await user_ai_service.generate_text(prompt=prompt)
+ ai_response = result.get('content', '') if isinstance(result, dict) else result
except Exception as ai_error:
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py
index b79fb3f..b1cb953 100644
--- a/backend/app/api/outlines.py
+++ b/backend/app/api/outlines.py
@@ -310,7 +310,7 @@ async def generate_outline(
# 模式:全新生成
if actual_mode == "new":
return await _generate_new_outline(
- request, project, db, user_ai_service
+ request, project, db, user_ai_service, user_id
)
# 模式:续写
@@ -344,7 +344,8 @@ async def _generate_new_outline(
request: OutlineGenerateRequest,
project: Project,
db: AsyncSession,
- user_ai_service: AIService
+ user_ai_service: AIService,
+ user_id: str = None
) -> OutlineListResponse:
"""全新生成大纲(MCP增强版)"""
logger.info(f"全新生成大纲 - 项目: {project.id}, enable_mcp: {request.enable_mcp}")
@@ -360,14 +361,26 @@ async def _generate_new_outline(
for char in characters
])
- # 🔍 MCP工具增强:收集情节设计参考资料
+ # 🔍 MCP工具增强:收集情节设计参考资料(优化版)
mcp_reference_materials = ""
if request.enable_mcp:
try:
- logger.info(f"🔍 尝试使用MCP工具收集大纲设计参考资料...")
+ # 1️⃣ 静默检查工具可用性(注意:新建大纲时user_id可能不可用)
+ from app.services.mcp_tool_service import mcp_tool_service
+ # 使用传入的user_id参数
- # 构建资料收集查询
- planning_query = f"""你正在为小说《{project.title}》设计完整大纲。
+ if user_id:
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id,
+ db_session=db
+ )
+
+ # 2️⃣ 只在有工具时才调用
+ if available_tools:
+ logger.info(f"🔍 检测到可用MCP工具,收集大纲设计参考资料...")
+
+ # 构建资料收集查询
+ planning_query = f"""你正在为小说《{project.title}》设计完整大纲。
项目信息:
- 主题:{request.theme or project.theme}
- 类型:{request.genre or project.genre}
@@ -389,27 +402,31 @@ async def _generate_new_outline(
3. 符合世界观的情节元素和场景设计灵感
请有针对性地查询1-2个最关键的问题。"""
-
- # 调用MCP增强的AI(非流式,最多2轮工具调用)
- planning_result = await user_ai_service.generate_text_with_mcp(
- prompt=planning_query,
- user_id="system", # 全新生成时可能没有用户上下文
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- # 提取参考资料
- if planning_result.get("tool_calls_made", 0) > 0:
- mcp_reference_materials = planning_result.get("content", "")
- logger.info(f"📚 MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+
+ # 调用MCP增强的AI(非流式,限制1轮避免超时)
+ planning_result = await user_ai_service.generate_text_with_mcp(
+ prompt=planning_query,
+ user_id=user_id,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # ✅ 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ # 提取参考资料
+ if planning_result.get("tool_calls_made", 0) > 0:
+ mcp_reference_materials = planning_result.get("content", "")
+ logger.info(f"✅ MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+ else:
+ logger.info(f"ℹ️ MCP未使用工具,继续")
+ else:
+ logger.debug(f"用户 {user_id} 未启用MCP工具,跳过MCP增强")
else:
- logger.info(f"ℹ️ MCP工具未进行调用,继续正常生成")
+ logger.debug("无用户上下文,跳过MCP增强")
except Exception as e:
- logger.warning(f"⚠️ MCP工具调用失败,继续使用常规模式: {str(e)}")
+ logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
mcp_reference_materials = ""
# 使用完整提示词(插入MCP参考资料)
@@ -659,15 +676,24 @@ async def _continue_outline(
logger.warning(f"⚠️ 记忆上下文构建失败,继续不使用记忆: {str(e)}")
memory_context = None
- # 🔍 MCP工具增强:收集续写参考资料
+ # 🔍 MCP工具增强:收集续写参考资料(优化版)
mcp_reference_materials = ""
if request.enable_mcp:
try:
- logger.info(f"🔍 第{batch_num + 1}批:尝试使用MCP工具收集续写参考资料...")
+ # 1️⃣ 静默检查工具可用性
+ from app.services.mcp_tool_service import mcp_tool_service
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id,
+ db_session=db
+ )
- # 构建资料收集查询
- latest_summary = latest_outlines[-1].content if latest_outlines else ""
- planning_query = f"""你正在为小说《{project.title}》续写大纲。
+ # 2️⃣ 只在有工具时才调用
+ if available_tools:
+ logger.info(f"🔍 第{batch_num + 1}批:检测到可用MCP工具,收集续写参考资料...")
+
+ # 构建资料收集查询
+ latest_summary = latest_outlines[-1].content if latest_outlines else ""
+ planning_query = f"""你正在为小说《{project.title}》续写大纲。
当前进度:已有{len(latest_outlines)}章,即将续写第{current_start_chapter}-{current_start_chapter + current_batch_size - 1}章
项目信息:
@@ -686,27 +712,29 @@ async def _continue_outline(
3. 符合类型特点的场景设计和剧情元素
请有针对性地查询1-2个最关键的问题。"""
-
- # 调用MCP增强的AI(非流式,最多2轮工具调用)
- planning_result = await user_ai_service.generate_text_with_mcp(
- prompt=planning_query,
- user_id=user_id,
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- # 提取参考资料
- if planning_result.get("tool_calls_made", 0) > 0:
- mcp_reference_materials = planning_result.get("content", "")
- logger.info(f"📚 第{batch_num + 1}批MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+
+ # 调用MCP增强的AI(非流式,限制1轮避免超时)
+ planning_result = await user_ai_service.generate_text_with_mcp(
+ prompt=planning_query,
+ user_id=user_id,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # ✅ 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ # 提取参考资料
+ if planning_result.get("tool_calls_made", 0) > 0:
+ mcp_reference_materials = planning_result.get("content", "")
+ logger.info(f"✅ 第{batch_num + 1}批MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+ else:
+ logger.info(f"ℹ️ 第{batch_num + 1}批MCP未使用工具,继续")
else:
- logger.info(f"ℹ️ 第{batch_num + 1}批MCP工具未进行调用,继续正常生成")
+ logger.debug(f"用户 {user_id} 未启用MCP工具,跳过第{batch_num + 1}批MCP增强")
except Exception as e:
- logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,继续使用常规模式: {str(e)}")
+ logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,降级为基础模式: {str(e)}")
mcp_reference_materials = ""
# 使用标准续写提示词模板(支持记忆+MCP增强)
@@ -892,15 +920,29 @@ async def new_outline_generator(
for char in characters
])
- # 🔍 MCP工具增强:收集情节设计参考资料
+ # 🔍 MCP工具增强:收集情节设计参考资料(优化版)
mcp_reference_materials = ""
if enable_mcp:
try:
- yield await SSEResponse.send_progress("🔍 使用MCP工具收集参考资料...", 18)
- logger.info(f"🔍 尝试使用MCP工具收集大纲设计参考资料...")
+ # 1️⃣ 静默检查工具可用性
+ from app.services.mcp_tool_service import mcp_tool_service
+ # 尝试从环境获取user_id(SSE流式场景下可能没有)
+ # 这里可以考虑让前端传递user_id
+ user_id_for_mcp = data.get("user_id") # 需要前端传递
- # 构建资料收集查询
- planning_query = f"""你正在为小说《{project.title}》设计完整大纲。
+ if user_id_for_mcp:
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id_for_mcp,
+ db_session=db
+ )
+
+ # 2️⃣ 只在有工具时才显示消息和调用
+ if available_tools:
+ yield await SSEResponse.send_progress("🔍 使用MCP工具收集参考资料...", 18)
+ logger.info(f"🔍 检测到可用MCP工具,收集大纲设计参考资料...")
+
+ # 构建资料收集查询
+ planning_query = f"""你正在为小说《{project.title}》设计完整大纲。
项目信息:
- 主题:{data.get('theme') or project.theme}
- 类型:{data.get('genre') or project.genre}
@@ -922,28 +964,32 @@ async def new_outline_generator(
3. 符合世界观的情节元素和场景设计灵感
请有针对性地查询1-2个最关键的问题。"""
-
- # 调用MCP增强的AI(非流式,最多2轮工具调用)
- planning_result = await user_ai_service.generate_text_with_mcp(
- prompt=planning_query,
- user_id="system",
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- # 提取参考资料
- if planning_result.get("tool_calls_made", 0) > 0:
- mcp_reference_materials = planning_result.get("content", "")
- logger.info(f"📚 MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
- yield await SSEResponse.send_progress(f"📚 MCP收集到参考资料 ({len(mcp_reference_materials)}字符)", 19)
+
+ # 调用MCP增强的AI(非流式,限制1轮避免超时)
+ planning_result = await user_ai_service.generate_text_with_mcp(
+ prompt=planning_query,
+ user_id=user_id_for_mcp,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # ✅ 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
+ )
+
+ # 提取参考资料
+ if planning_result.get("tool_calls_made", 0) > 0:
+ mcp_reference_materials = planning_result.get("content", "")
+ logger.info(f"✅ MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+ yield await SSEResponse.send_progress(f"✅ MCP收集到参考资料 ({len(mcp_reference_materials)}字符)", 19)
+ else:
+ logger.info(f"ℹ️ MCP未使用工具,继续")
+ else:
+ logger.debug(f"用户 {user_id_for_mcp} 未启用MCP工具,跳过MCP增强")
else:
- logger.info(f"ℹ️ MCP工具未进行调用,继续正常生成")
+ logger.debug("无用户上下文,跳过MCP增强")
except Exception as e:
- logger.warning(f"⚠️ MCP工具调用失败,继续使用常规模式: {str(e)}")
+ logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
mcp_reference_materials = ""
# 使用完整提示词(插入MCP参考资料)
@@ -1185,20 +1231,29 @@ async def continue_outline_generator(
except Exception as e:
logger.warning(f"⚠️ 记忆上下文构建失败: {str(e)}")
memory_context = None
- # 🔍 MCP工具增强:收集续写参考资料
+ # 🔍 MCP工具增强:收集续写参考资料(优化版)
mcp_reference_materials = ""
enable_mcp = data.get("enable_mcp", True)
if enable_mcp:
try:
- yield await SSEResponse.send_progress(
- f"🔍 第{str(batch_num + 1)}批:使用MCP工具收集参考资料...",
- batch_progress + 4
+ # 1️⃣ 静默检查工具可用性
+ from app.services.mcp_tool_service import mcp_tool_service
+ available_tools = await mcp_tool_service.get_user_enabled_tools(
+ user_id=user_id,
+ db_session=db
)
- logger.info(f"🔍 第{batch_num + 1}批:尝试使用MCP工具收集续写参考资料...")
- # 构建资料收集查询
- latest_summary = latest_outlines[-1].content if latest_outlines else ""
- planning_query = f"""你正在为小说《{project.title}》续写大纲。
+ # 2️⃣ 只在有工具时才显示消息和调用
+ if available_tools:
+ yield await SSEResponse.send_progress(
+ f"🔍 第{str(batch_num + 1)}批:使用MCP工具收集参考资料...",
+ batch_progress + 4
+ )
+ logger.info(f"🔍 第{batch_num + 1}批:检测到可用MCP工具,收集续写参考资料...")
+
+ # 构建资料收集查询
+ latest_summary = latest_outlines[-1].content if latest_outlines else ""
+ planning_query = f"""你正在为小说《{project.title}》续写大纲。
当前进度:已有{len(latest_outlines)}章,即将续写第{current_start_chapter}-{current_start_chapter + current_batch_size - 1}章
项目信息:
@@ -1217,31 +1272,33 @@ async def continue_outline_generator(
3. 符合类型特点的场景设计和剧情元素
请有针对性地查询1-2个最关键的问题。"""
-
- # 调用MCP增强的AI(非流式,最多2轮工具调用)
- planning_result = await user_ai_service.generate_text_with_mcp(
- prompt=planning_query,
- user_id=user_id,
- db_session=db,
- enable_mcp=True,
- max_tool_rounds=2,
- tool_choice="auto",
- provider=None,
- model=None
- )
-
- # 提取参考资料
- if planning_result.get("tool_calls_made", 0) > 0:
- mcp_reference_materials = planning_result.get("content", "")
- logger.info(f"📚 第{batch_num + 1}批MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
- yield await SSEResponse.send_progress(
- f"📚 第{str(batch_num + 1)}批收集到参考资料 ({len(mcp_reference_materials)}字符)",
- batch_progress + 4.5
+
+ # 调用MCP增强的AI(非流式,限制1轮避免超时)
+ planning_result = await user_ai_service.generate_text_with_mcp(
+ prompt=planning_query,
+ user_id=user_id,
+ db_session=db,
+ enable_mcp=True,
+ max_tool_rounds=1, # ✅ 减少为1轮,避免超时
+ tool_choice="auto",
+ provider=None,
+ model=None
)
+
+ # 提取参考资料
+ if planning_result.get("tool_calls_made", 0) > 0:
+ mcp_reference_materials = planning_result.get("content", "")
+ logger.info(f"✅ 第{batch_num + 1}批MCP工具收集参考资料:{len(mcp_reference_materials)} 字符")
+ yield await SSEResponse.send_progress(
+ f"✅ 第{str(batch_num + 1)}批收集到参考资料 ({len(mcp_reference_materials)}字符)",
+ batch_progress + 4.5
+ )
+ else:
+ logger.info(f"ℹ️ 第{batch_num + 1}批MCP未使用工具,继续")
else:
- logger.info(f"ℹ️ 第{batch_num + 1}批MCP工具未进行调用,继续正常生成")
+ logger.debug(f"用户 {user_id} 未启用MCP工具,跳过第{batch_num + 1}批MCP增强")
except Exception as e:
- logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,继续使用常规模式: {str(e)}")
+ logger.warning(f"⚠️ 第{batch_num + 1}批MCP工具调用失败,降级为基础模式: {str(e)}")
mcp_reference_materials = ""
diff --git a/backend/app/api/wizard_stream.py b/backend/app/api/wizard_stream.py
index c83946c..b3a0ab3 100644
--- a/backend/app/api/wizard_stream.py
+++ b/backend/app/api/wizard_stream.py
@@ -246,6 +246,10 @@ async def world_building_generator(
except Exception as e:
logger.warning(f"设置默认写作风格失败: {e},不影响项目创建")
+ # 更新向导步骤状态为1(世界观已完成)
+ project.wizard_step = 1
+ await db.commit()
+
db_committed = True
# 发送最终结果
@@ -824,8 +828,9 @@ async def characters_generator(
logger.info(f" - 创建角色关系:{relationships_created} 条")
logger.info(f" - 创建组织成员:{members_created} 条")
- # 更新项目的角色数量
+ # 更新项目的角色数量和向导步骤状态为2(角色已完成)
project.character_count = len(created_characters)
+ project.wizard_step = 2
logger.info(f"✅ 更新项目角色数量: {project.character_count}")
await db.commit()
@@ -1022,7 +1027,7 @@ async def outline_generator(
project.target_words = target_words
project.status = "writing"
project.wizard_status = "completed"
- project.wizard_step = 4
+ project.wizard_step = 3
await db.commit()
db_committed = True
@@ -1269,7 +1274,6 @@ async def regenerate_world_building_stream(
# 从中间件注入user_id到data中
if hasattr(request.state, 'user_id'):
data['user_id'] = request.state.user_id
-
return create_sse_response(world_building_regenerate_generator(project_id, data, db, user_ai_service))
diff --git a/backend/app/database.py b/backend/app/database.py
index ede79d8..fd06871 100644
--- a/backend/app/database.py
+++ b/backend/app/database.py
@@ -86,6 +86,7 @@ async def get_engine(user_id: str):
"server_settings": {
"application_name": settings.app_name,
"jit": "off",
+ "search_path": "public",
},
"command_timeout": 60,
"statement_cache_size": 500,
diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py
index e9a98cc..31187e7 100644
--- a/backend/app/services/prompt_service.py
+++ b/backend/app/services/prompt_service.py
@@ -112,61 +112,109 @@ class PromptService:
"""提示词模板管理"""
# 世界构建提示词
- WORLD_BUILDING = """你是一位资深的世界观设计师(World-Building Architect)。你的任务是基于输入信息,构建一个高度原创、深度自洽、且充满戏剧冲突的小说世界观。
+ WORLD_BUILDING = """你是一位资深的世界观设计师。基于以下输入信息,构建一个高度原创、深度自洽、充满戏剧冲突的小说世界观。
-# 1. 输入信息
+# 输入信息
书名:{title}
主题:{theme}
类型:{genre}
-# 2. 核心指令(CRITICAL)
-* **去标签化**:严禁使用通用的“XX纪元”、“XX时代”、“XX年”作为时间背景的开头或核心描述。请直接描述世界所处的状态、技术水平或生存现状。
-* **动态演绎**:所有设定必须直接由输入的主题衍生而来。例如,如果是赛博朋克,不要只写“高科技”,要写“义体技术如何导致了贫民窟的特定生活方式”。
-* **拒绝陈词滥调**:避免使用宏大的空洞词汇,专注于具体的、可感知的细节。
+# 核心要求
+* **类型适配性**:世界观必须符合小说类型的特征,不要生成不匹配的设定
+* **主题贴合性**:时代背景要能有效支撑和体现小说主题
+* **原创性**:在类型框架内发挥创意,创造独特但合理的世界设定
+* **具象化**:避免空洞概念,用具体可感的细节描述世界
+* **逻辑自洽**:确保所有设定相互支撑,形成完整体系
+* **戏剧张力**:设定要能为故事冲突提供支撑
-# 3. 世界构建框架
-请生成包含以下四个核心板块的JSON。请确保所有板块互为因果,逻辑严密。
+# 类型指导原则
+根据小说类型选择适当的设定风格:
-**重要说明:每个字段的value必须是一个完整的文本字符串,将以下所有要点整合成连贯的段落描述,不要使用嵌套的JSON对象或数组。**
+**现代都市/言情/青春**:
+- 时间设定:当代现实社会(2020年代)或近未来(2030-2050年)
+- 避免使用:大崩解、纪元、末日、重生等宏大概念
+- 重点描述:具体的城市环境、社会现状、文化氛围
+- 例如:一线城市的竞争压力、职场文化、代际冲突、社交媒体影响等
-1. **time_period (时间线与文明阶段)**:
- 请将以下内容整合为一段完整的文字描述(300-500字):
- * 描述当前世界处于什么**发展阶段**(是毁灭边缘、新生萌芽、还是停滞不前?),**不要给这个阶段起名字**,而是描述其**特征**。
- * **历史转折点**:具体的事件(战争、发明、灾难),它如何直接导致了现在的局面?
- * **当下的核心矛盾**:时间流逝带来的具体焦虑是什么?(例如:资源枯竭的倒计时、某种信仰的崩塌)。
-2. **location (空间与生态环境)**:
- 请将以下内容整合为一段完整的文字描述(300-500字):
- * **舞台特征**:描述主要故事发生的地理或空间环境(如:悬浮的破碎岛屿、被真菌覆盖的地铁网络)。
- * **环境与生存**:地理环境如何强迫居民改变了生活方式?(例如:因为引力失衡,建筑都是倒挂的)。
- * **标志性奇观**:一个能代表这个世界独特性的具体场景或建筑。
-3. **atmosphere (感官体验与基调)**:
- 请将以下内容整合为一段完整的文字描述(300-500字):
- * **感官细节**:如果站在这个世界的街头,会**闻**到什么?**听**到什么?(不要只写"压抑",要写"空气中弥漫着铁锈和合成营养膏的酸味")。
- * **视觉美学**:描述具体的色彩倾向和光影质感。
- * **居民心态**:普通人普遍的心理状态(是麻木、狂热、还是某种特定的恐惧)。
-4. **rules (运作逻辑与禁忌)**:
- 请将以下内容整合为一段完整的文字描述(300-500字):
- * **核心法则**:这个世界运行的底层逻辑(物理、魔法或科技)。**重点描述代价**(使用力量需要支付什么?)。
- * **权力架构**:谁掌握资源?他们通过什么手段维持控制(暴力、技术垄断、宗教洗脑)?
- * **红线禁忌**:这个社会绝对不能触碰的具体底线,以及违反后的直接后果。
+**历史/古代**:
+- 时间设定:明确的历史朝代或虚构但有历史感的古代社会
+- 避免使用:科技元素、未来概念
+- 重点描述:时代特征、礼教制度、阶级分化
-# 4. 严格格式要求
-1. **绝对纯净JSON**:你的[唯一]输出必须是一个完整的JSON对象。输出必须以左花括号开始,并以右花括号结束。
-2. **禁止额外字符**:不要在JSON对象之前或之后包含任何说明文字、Markdown标记(如三个反引号加json)、注释或任何其他非JSON字符。
-3. **JSON内部文本规则**:在JSON的value字符串内部:
- * 严禁使用任何中文引号(""'')或英文引号来表示强调或引用。
- * 所有【专有名词】(如地点、人物、组织)应使用【】包裹。
- * 所有《作品》或《特殊概念》的标题应使用《》包裹。
-4. **JSON结构**:严格遵守`"key": "value"`的英文双引号结构,并使用下面指定的key。
-5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节,整合为连贯的段落文本。
-6. **禁止嵌套结构**:value必须是纯文本字符串,绝对不能是JSON对象或数组,所有信息都要整合在一个字符串中。
+**玄幻/仙侠/修真**:
+- 时间设定:修炼文明的特定时期,可以有门派兴衰、修炼体系变革
+- 可以使用宏大设定,但要与修炼体系紧密结合
+- 重点描述:修炼规则、灵气环境、门派势力
-{{
- "time_period": "(此处填写一段完整的文字描述,包含发展阶段特征、历史转折点、核心矛盾等内容,300-500字)",
- "location": "(此处填写一段完整的文字描述,包含舞台特征、环境与生存、标志性奇观等内容,300-500字)",
- "atmosphere": "(此处填写一段完整的文字描述,包含感官细节、视觉美学、居民心态等内容,300-500字)",
- "rules": "(此处填写一段完整的文字描述,包含核心法则、权力架构、红线禁忌等内容,300-500字)"
-}}"""
+**科幻**:
+- 时间设定:未来某个明确时期(如2150年、星际时代初期等)
+- 可以有文明转折,但要具体说明科技水平和社会形态
+- 避免空泛的纪元名称,多用具体的科技特征描述
+
+**奇幻/魔法**:
+- 时间设定:魔法文明的特定阶段
+- 重点描述:魔法体系、种族关系、大陆格局
+
+**悬疑/推理/惊悚**:
+- 时间设定:当代或历史某个时期
+- 重点描述:案件背景、社会环境、人际关系网
+
+**军事/战争**:
+- 时间设定:战争时期的具体年代
+- 重点描述:战争形势、阵营对立、军事科技水平
+
+# 设定尺度控制
+**切记:不要为所有类型都生成宏大的世界观!**
+
+- 如果是现代都市题材,就写现实社会的某个城市、某个行业、某个阶层
+- 如果是校园青春,就写学校环境、学生生活、成长困境
+- 如果是职场言情,就写公司文化、行业特点、职业压力
+- 只有史诗级题材(玄幻、科幻、奇幻等)才需要宏大的世界观架构
+
+# 输出要求
+生成包含以下四个字段的JSON对象,每个字段用300-500字的连贯段落描述:
+
+1. **time_period**(时间背景与社会状态)
+ - **重要**:根据类型和主题,设定合适规模的时间背景
+ - 现代题材:描述当前社会的具体特征(如:2024年的北京,互联网行业高速发展...)
+ - 历史题材:明确朝代和历史阶段(如:明朝嘉靖年间,海禁政策下的沿海地区...)
+ - 幻想题材:描述文明发展阶段,但要具体而非空泛(如:大陆诸国林立的战国时代,而非"XX纪元")
+ - 阐明时代核心矛盾和社会焦虑(要贴合主题)
+
+2. **location**(空间环境与地理特征)
+ - 描绘故事主要发生的空间环境(具体的城市、地区、场所)
+ - 现代题材:具体城市名或城市类型(一线城市、沿海城市、内陆小城等)
+ - 说明环境如何影响居民的生存方式
+ - 刻画能代表世界独特性的标志性场景
+
+3. **atmosphere**(感官体验与情感基调)
+ - 描述身临其境的感官细节(视觉、听觉、嗅觉等)
+ - 阐述世界的美学风格和色彩基调
+ - 刻画居民普遍的心理状态和情绪氛围
+ - **要与主题情感呼应**(如竞争焦虑、成长迷茫、爱情憧憬等)
+
+4. **rules**(世界规则与社会结构)
+ - 阐明世界运行的核心法则和底层逻辑
+ - 现代题材:社会规则、行业潜规则、人际交往法则
+ - 幻想题材:力量体系、社会等级、资源分配
+ - 描述权力结构和利益格局
+ - 揭示社会禁忌及违反后的后果
+
+# 格式规范
+1. **纯JSON输出**:只输出JSON对象,以左花括号开始、右花括号结束
+2. **无额外标记**:不要包含markdown标记、代码块符号或任何说明文字
+3. **纯文本值**:每个字段值必须是完整的段落文本,不使用嵌套结构
+4. **无特殊符号**:文本中不使用引号、方括号等特殊符号包裹内容
+5. **丰富细节**:每个字段提供充实的原创内容,避免模板化表达
+
+# 反面示例(避免这样的设定)
+❌ 不好的设定:故事设定在大崩解后的XX纪元、新世界秩序、文明重启...
+✅ 好的设定:故事设定在2024年的深圳,互联网创业浪潮下的年轻人...
+
+❌ 不好的设定:升华纪元、共鸣指数、灵光纯度...(现代都市题材不要用这些)
+✅ 好的设定:通过高考分数、学历背景、家庭条件来衡量个人价值...(符合现实)
+
+请根据输入的类型和主题,生成**规模适当、风格匹配**的世界观设定。"""
# 批量角色生成提示词
CHARACTERS_BATCH_GENERATION = """你是一位专业的角色设定师。请根据以下世界观和要求,生成{count}个立体丰满的角色和组织:
@@ -199,8 +247,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),请使用英文引号或【】《》标记
-3. 专有名词和强调内容使用【】或《》,不要用引号
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词、地点、人物、组织名称等直接书写,不使用任何符号包裹
请严格按照以下JSON数组格式返回(每个角色为数组中的一个对象):
[
@@ -279,7 +327,7 @@ class PromptService:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中必须精确包含{count}个对象
3. 不要引用任何本批次中不存在的角色或组织名称
-4. 文本描述中不要使用中文引号(""),改用【】或《》"""
+4. 所有内容描述中严禁使用任何特殊符号,包括但不限于中文引号、英文引号、方括号、书名号等"""
# 向导大纲生成提示词
COMPLETE_OUTLINE_GENERATION = """你是一位经验丰富的小说作家和编剧。请根据以下信息生成完整的{chapter_count}章小说大纲:
@@ -315,8 +363,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记
-3. 专有名词、书名、事件名使用【】或《》
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词、事件名等直接书写,不使用任何符号包裹
请严格按照以下JSON数组格式返回(共{chapter_count}个章节对象):
[
@@ -345,7 +393,7 @@ class PromptService:
再次强调:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中要包含{chapter_count}个章节对象
-3. 文本中不要使用中文引号(""),改用【】或《》"""
+3. 所有内容描述中严禁使用任何特殊符号"""
# 大纲续写提示词(记忆增强版)
OUTLINE_CONTINUE_GENERATION = """你是一位经验丰富的小说作家和编剧。请基于以下信息续写小说大纲:
@@ -396,8 +444,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》
-3. 文本描述中的专有名词使用【】标记
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词直接书写,不使用任何符号包裹
请严格按照以下JSON数组格式返回(共{chapter_count}个章节对象):
[
@@ -428,7 +476,7 @@ class PromptService:
2. 数组中要包含{chapter_count}个章节对象
3. 每个summary必须是100-200字的详细描述
4. 确保字段结构与已有章节完全一致
-5. 文本中不要使用中文引号(""),改用【】或《》"""
+5. 所有内容描述中严禁使用任何特殊符号"""
# AI去味提示词(核心特色功能)
AI_DENOISING = """你是一位追求自然写作风格的编辑。你的任务是将AI生成的文本改写得更像人类作家的手笔。
@@ -593,8 +641,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),改用【】或《》
-3. 专有名词和强调内容使用【】标记
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词直接书写,不使用任何符号包裹
请严格按照以下JSON格式返回:
{{
@@ -609,7 +657,7 @@ class PromptService:
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
-2. 文本中不要使用中文引号(""),改用【】或《》
+2. 所有内容描述中严禁使用任何特殊符号
3. 不要有任何额外的文字说明"""
# 单个角色生成提示词
@@ -655,8 +703,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),改用【】或《》
-3. 文本描述中的专有名词使用【】标记
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词直接书写,不使用任何符号包裹
请严格按照以下JSON格式返回:
{{
@@ -714,7 +762,7 @@ class PromptService:
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
-2. 文本中不要使用中文引号(""),改用【】或《》
+2. 所有内容描述中严禁使用任何特殊符号
3. 不要有任何额外的文字说明"""
# 单个组织生成提示词
@@ -765,8 +813,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),改用【】或《》
-3. 文本描述中的专有名词使用【】标记
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词直接书写,不使用任何符号包裹
请严格按照以下JSON格式返回:
{{
@@ -799,7 +847,7 @@ class PromptService:
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
-2. 文本中不要使用中文引号(""),改用【】或《》
+2. 所有内容描述中严禁使用任何特殊符号
3. 不要有任何额外的文字说明"""
# 大纲展开为多章节的提示词
@@ -853,8 +901,8 @@ class PromptService:
**重要格式要求:**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
-2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》
-3. 文本描述中的专有名词使用【】标记
+2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
+3. 所有专有名词直接书写,不使用任何符号包裹
请严格按照以下JSON数组格式输出:
[
@@ -875,7 +923,7 @@ class PromptService:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中要包含{target_chapters}个章节对象
3. 每个plot_summary必须是200-300字的详细描述
-4. 文本中不要使用中文引号(""),改用【】或《》"""
+4. 所有内容描述中严禁使用任何特殊符号"""
@staticmethod
def format_prompt(template: str, **kwargs) -> str:
diff --git a/frontend/package.json b/frontend/package.json
index cfc1b2f..eb63205 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "1.0.4",
+ "version": "1.0.5",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/AIProjectGenerator.tsx b/frontend/src/components/AIProjectGenerator.tsx
new file mode 100644
index 0000000..5798863
--- /dev/null
+++ b/frontend/src/components/AIProjectGenerator.tsx
@@ -0,0 +1,922 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Card, Button, Space, Typography, message, Progress } from 'antd';
+import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
+import { wizardStreamApi } from '../services/api';
+import type { ApiError } from '../types';
+
+const { Title, Paragraph, Text } = Typography;
+
+export interface GenerationConfig {
+ title: string;
+ description: string;
+ theme: string;
+ genre: string | string[];
+ narrative_perspective: string;
+ target_words: number;
+ chapter_count: number;
+ character_count: number;
+}
+
+interface AIProjectGeneratorProps {
+ config: GenerationConfig;
+ storagePrefix: 'wizard' | 'inspiration';
+ onComplete: (projectId: string) => void;
+ onBack?: () => void;
+ isMobile?: boolean;
+ resumeProjectId?: string;
+}
+
+type GenerationStep = 'pending' | 'processing' | 'completed' | 'error';
+
+interface GenerationSteps {
+ worldBuilding: GenerationStep;
+ characters: GenerationStep;
+ outline: GenerationStep;
+}
+
+export const AIProjectGenerator: React.FC
= ({
+ config,
+ storagePrefix,
+ onComplete,
+ isMobile = false,
+ resumeProjectId
+}) => {
+ const navigate = useNavigate();
+
+ // 状态管理
+ const [loading, setLoading] = useState(false);
+ const [projectId, setProjectId] = useState('');
+
+ // SSE流式进度状态
+ const [progress, setProgress] = useState(0);
+ const [progressMessage, setProgressMessage] = useState('');
+ const [errorDetails, setErrorDetails] = useState('');
+ const [generationSteps, setGenerationSteps] = useState({
+ worldBuilding: 'pending',
+ characters: 'pending',
+ outline: 'pending'
+ });
+
+ // 保存生成数据,用于重试
+ const [generationData, setGenerationData] = useState(null);
+ // 保存世界观生成结果,用于后续步骤
+ const [worldBuildingResult, setWorldBuildingResult] = useState(null);
+
+ // LocalStorage 键名
+ const storageKeys = {
+ projectId: `${storagePrefix}_project_id`,
+ generationData: `${storagePrefix}_generation_data`,
+ currentStep: `${storagePrefix}_current_step`
+ };
+
+ // 保存进度到localStorage
+ const saveProgress = (projectId: string, data: GenerationConfig, step: string) => {
+ try {
+ localStorage.setItem(storageKeys.projectId, projectId);
+ localStorage.setItem(storageKeys.generationData, JSON.stringify(data));
+ localStorage.setItem(storageKeys.currentStep, step);
+ } catch (error) {
+ console.error('保存进度失败:', error);
+ }
+ };
+
+ // 清理localStorage
+ const clearStorage = () => {
+ localStorage.removeItem(storageKeys.projectId);
+ localStorage.removeItem(storageKeys.generationData);
+ localStorage.removeItem(storageKeys.currentStep);
+ };
+
+ // 开始自动化生成流程
+ useEffect(() => {
+ if (config) {
+ if (resumeProjectId) {
+ // 恢复生成模式
+ handleResumeGenerate(config, resumeProjectId);
+ } else {
+ // 新建项目模式
+ handleAutoGenerate(config);
+ }
+ }
+ }, [config, resumeProjectId]);
+
+ // 恢复未完成项目的生成
+ const handleResumeGenerate = async (data: GenerationConfig, projectIdParam: string) => {
+ try {
+ setLoading(true);
+ setProgress(0);
+ setProgressMessage('检查项目状态...');
+ setErrorDetails('');
+ setGenerationData(data);
+ setProjectId(projectIdParam);
+
+ // 获取项目信息,判断当前完成到哪一步
+ const response = await fetch(`/api/projects/${projectIdParam}`, {
+ credentials: 'include'
+ });
+ if (!response.ok) {
+ throw new Error('获取项目信息失败');
+ }
+ const project = await response.json();
+ const wizardStep = project.wizard_step || 0;
+
+ // 根据wizard_step判断从哪里继续
+ if (wizardStep === 0) {
+ // 从世界观开始
+ message.info('从世界观步骤开始生成...');
+ setGenerationSteps({ worldBuilding: 'processing', characters: 'pending', outline: 'pending' });
+ await resumeFromWorldBuilding(data);
+ } else if (wizardStep === 1) {
+ // 世界观已完成,从角色开始
+ message.info('世界观已完成,从角色步骤继续...');
+ setGenerationSteps({ worldBuilding: 'completed', characters: 'processing', outline: 'pending' });
+
+ // 获取世界观数据
+ const worldResult = {
+ project_id: projectIdParam,
+ time_period: project.world_time_period || '',
+ location: project.world_location || '',
+ atmosphere: project.world_atmosphere || '',
+ rules: project.world_rules || ''
+ };
+ setWorldBuildingResult(worldResult);
+ setProgress(33);
+
+ await resumeFromCharacters(data, worldResult);
+ } else if (wizardStep === 2) {
+ // 世界观和角色已完成,从大纲开始
+ message.info('世界观和角色已完成,从大纲步骤继续...');
+ setGenerationSteps({ worldBuilding: 'completed', characters: 'completed', outline: 'processing' });
+ setProgress(66);
+ await resumeFromOutline(data, projectIdParam);
+ } else {
+ // 已全部完成
+ message.success('项目已完成,正在跳转...');
+ setProgress(100);
+ onComplete(projectIdParam);
+ setTimeout(() => {
+ navigate(`/project/${projectIdParam}`);
+ }, 1000);
+ }
+ } catch (error) {
+ const apiError = error as ApiError;
+ const errorMsg = apiError.response?.data?.detail || apiError.message || '未知错误';
+ console.error('恢复生成失败:', errorMsg);
+ setErrorDetails(errorMsg);
+ message.error('恢复生成失败:' + errorMsg);
+ setLoading(false);
+ }
+ };
+
+ // 恢复:从世界观步骤开始
+ const resumeFromWorldBuilding = async (data: GenerationConfig) => {
+ const genreString = Array.isArray(data.genre) ? data.genre.join('、') : data.genre;
+
+ const worldResult = await wizardStreamApi.generateWorldBuildingStream(
+ {
+ title: data.title,
+ description: data.description,
+ theme: data.theme,
+ genre: genreString,
+ narrative_perspective: data.narrative_perspective,
+ target_words: data.target_words,
+ chapter_count: data.chapter_count,
+ character_count: data.character_count,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ setWorldBuildingResult(result);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('世界观生成失败:', error);
+ setErrorDetails(`世界观生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('世界观生成完成');
+ }
+ }
+ );
+
+ await resumeFromCharacters(data, worldResult);
+ };
+
+ // 恢复:从角色步骤继续
+ const resumeFromCharacters = async (data: GenerationConfig, worldResult: any) => {
+ const genreString = Array.isArray(data.genre) ? data.genre.join('、') : data.genre;
+ const pid = projectId || worldResult.project_id;
+
+ setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
+ setProgressMessage('正在生成角色...');
+
+ await wizardStreamApi.generateCharactersStream(
+ {
+ project_id: pid,
+ count: data.character_count,
+ world_context: {
+ time_period: worldResult.time_period || '',
+ location: worldResult.location || '',
+ atmosphere: worldResult.atmosphere || '',
+ rules: worldResult.rules || '',
+ },
+ theme: data.theme,
+ genre: genreString,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(33 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ console.log(`成功生成${result.characters?.length || 0}个角色`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('角色生成失败:', error);
+ setErrorDetails(`角色生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('角色生成完成');
+ }
+ }
+ );
+
+ await resumeFromOutline(data, pid);
+ };
+
+ // 恢复:从大纲步骤继续
+ const resumeFromOutline = async (data: GenerationConfig, pid: string) => {
+ setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
+ setProgressMessage('正在生成大纲...');
+
+ await wizardStreamApi.generateCompleteOutlineStream(
+ {
+ project_id: pid,
+ chapter_count: data.chapter_count,
+ narrative_perspective: data.narrative_perspective,
+ target_words: data.target_words,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(66 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: () => {
+ console.log('大纲生成完成');
+ setGenerationSteps(prev => ({ ...prev, outline: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('大纲生成失败:', error);
+ setErrorDetails(`大纲生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, outline: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('大纲生成完成');
+ }
+ }
+ );
+
+ // 全部完成
+ setProgress(100);
+ setProgressMessage('项目创建完成!正在跳转...');
+ message.success('项目创建成功!正在进入项目...');
+ clearStorage();
+ setLoading(false);
+
+ onComplete(pid);
+ setTimeout(() => {
+ navigate(`/project/${pid}`);
+ }, 1000);
+ };
+
+ // 自动化生成流程
+ const handleAutoGenerate = async (data: GenerationConfig) => {
+ try {
+ setLoading(true);
+ setProgress(0);
+ setProgressMessage('开始创建项目...');
+ setErrorDetails('');
+ setGenerationData(data);
+ saveProgress('', data, 'generating');
+
+ const genreString = Array.isArray(data.genre) ? data.genre.join('、') : data.genre;
+
+ // 步骤1: 生成世界观并创建项目
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'processing' }));
+ setProgressMessage('正在生成世界观...');
+
+ const worldResult = await wizardStreamApi.generateWorldBuildingStream(
+ {
+ title: data.title,
+ description: data.description,
+ theme: data.theme,
+ genre: genreString,
+ narrative_perspective: data.narrative_perspective,
+ target_words: data.target_words,
+ chapter_count: data.chapter_count,
+ character_count: data.character_count,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ setProjectId(result.project_id);
+ setWorldBuildingResult(result);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('世界观生成失败:', error);
+ setErrorDetails(`世界观生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('世界观生成完成');
+ }
+ }
+ );
+
+ if (!worldResult?.project_id) {
+ throw new Error('项目创建失败:未获取到项目ID');
+ }
+
+ const createdProjectId = worldResult.project_id;
+ setProjectId(createdProjectId);
+ setWorldBuildingResult(worldResult);
+ saveProgress(createdProjectId, data, 'generating');
+
+ // 步骤2: 生成角色
+ setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
+ setProgressMessage('正在生成角色...');
+
+ await wizardStreamApi.generateCharactersStream(
+ {
+ project_id: createdProjectId,
+ count: data.character_count,
+ world_context: {
+ time_period: worldResult.time_period || '',
+ location: worldResult.location || '',
+ atmosphere: worldResult.atmosphere || '',
+ rules: worldResult.rules || '',
+ },
+ theme: data.theme,
+ genre: genreString,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(33 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ console.log(`成功生成${result.characters?.length || 0}个角色`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('角色生成失败:', error);
+ setErrorDetails(`角色生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('角色生成完成');
+ }
+ }
+ );
+
+ // 步骤3: 生成大纲
+ setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
+ setProgressMessage('正在生成大纲...');
+
+ await wizardStreamApi.generateCompleteOutlineStream(
+ {
+ project_id: createdProjectId,
+ chapter_count: data.chapter_count,
+ narrative_perspective: data.narrative_perspective,
+ target_words: data.target_words,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(66 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: () => {
+ console.log('大纲生成完成');
+ setGenerationSteps(prev => ({ ...prev, outline: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('大纲生成失败:', error);
+ setErrorDetails(`大纲生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, outline: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('大纲生成完成');
+ }
+ }
+ );
+
+ // 全部完成 - 自动跳转到项目详情页
+ setProgress(100);
+ setProgressMessage('项目创建完成!正在跳转...');
+ message.success('项目创建成功!正在进入项目...');
+ clearStorage();
+
+ // 调用完成回调
+ onComplete(createdProjectId);
+
+ // 延迟1秒后自动跳转到项目详情页
+ setTimeout(() => {
+ navigate(`/project/${createdProjectId}`);
+ }, 1000);
+
+ } catch (error) {
+ const apiError = error as ApiError;
+ const errorMsg = apiError.response?.data?.detail || apiError.message || '未知错误';
+ console.error('创建项目失败:', errorMsg);
+ setErrorDetails(errorMsg);
+ message.error('创建项目失败:' + errorMsg);
+ setLoading(false);
+ }
+ };
+
+ // 智能重试:从失败的步骤继续生成
+ const handleSmartRetry = async () => {
+ if (!generationData) {
+ message.warning('缺少生成数据');
+ return;
+ }
+
+ setLoading(true);
+ setErrorDetails('');
+
+ try {
+ if (generationSteps.worldBuilding === 'error') {
+ message.info('从世界观步骤开始重新生成...');
+ await retryFromWorldBuilding();
+ } else if (generationSteps.characters === 'error') {
+ message.info('从角色步骤继续生成...');
+ await retryFromCharacters();
+ } else if (generationSteps.outline === 'error') {
+ message.info('从大纲步骤继续生成...');
+ await retryFromOutline();
+ }
+ } catch (error: any) {
+ console.error('智能重试失败:', error);
+ message.error('重试失败:' + (error.message || '未知错误'));
+ setLoading(false);
+ }
+ };
+
+ // 从世界观步骤重新开始
+ const retryFromWorldBuilding = async () => {
+ if (!generationData) return;
+
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'processing' }));
+ setProgressMessage('重新生成世界观...');
+
+ const genreString = Array.isArray(generationData.genre) ? generationData.genre.join('、') : generationData.genre;
+
+ const worldResult = await wizardStreamApi.generateWorldBuildingStream(
+ {
+ title: generationData.title,
+ description: generationData.description,
+ theme: generationData.theme,
+ genre: genreString,
+ narrative_perspective: generationData.narrative_perspective,
+ target_words: generationData.target_words,
+ chapter_count: generationData.chapter_count,
+ character_count: generationData.character_count,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ setProjectId(result.project_id);
+ setWorldBuildingResult(result);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('世界观生成失败:', error);
+ setErrorDetails(`世界观生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, worldBuilding: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('世界观重新生成完成');
+ }
+ }
+ );
+
+ if (!worldResult?.project_id) {
+ throw new Error('项目创建失败:未获取到项目ID');
+ }
+
+ await continueFromCharacters(worldResult);
+ };
+
+ // 从角色步骤继续
+ const retryFromCharacters = async () => {
+ if (!generationData || !projectId || !worldBuildingResult) {
+ message.warning('缺少必要数据,无法从角色步骤继续');
+ setLoading(false);
+ return;
+ }
+
+ setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
+ setProgressMessage('重新生成角色...');
+
+ const genreString = Array.isArray(generationData.genre) ? generationData.genre.join('、') : generationData.genre;
+
+ await wizardStreamApi.generateCharactersStream(
+ {
+ project_id: projectId,
+ count: generationData.character_count,
+ world_context: {
+ time_period: worldBuildingResult.time_period || '',
+ location: worldBuildingResult.location || '',
+ atmosphere: worldBuildingResult.atmosphere || '',
+ rules: worldBuildingResult.rules || '',
+ },
+ theme: generationData.theme,
+ genre: genreString,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(33 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ console.log(`成功生成${result.characters?.length || 0}个角色`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('角色生成失败:', error);
+ setErrorDetails(`角色生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('角色重新生成完成');
+ }
+ }
+ );
+
+ await continueFromOutline();
+ };
+
+ // 从大纲步骤继续
+ const retryFromOutline = async () => {
+ if (!generationData || !projectId) {
+ message.warning('缺少必要数据,无法从大纲步骤继续');
+ setLoading(false);
+ return;
+ }
+
+ setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
+ setProgressMessage('重新生成大纲...');
+
+ await wizardStreamApi.generateCompleteOutlineStream(
+ {
+ project_id: projectId,
+ chapter_count: generationData.chapter_count,
+ narrative_perspective: generationData.narrative_perspective,
+ target_words: generationData.target_words,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(66 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: () => {
+ console.log('大纲生成完成');
+ setGenerationSteps(prev => ({ ...prev, outline: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('大纲生成失败:', error);
+ setErrorDetails(`大纲生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, outline: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('大纲重新生成完成');
+ }
+ }
+ );
+
+ setProgress(100);
+ setProgressMessage('项目创建完成!正在跳转...');
+ message.success('项目创建成功!正在进入项目...');
+ setLoading(false);
+
+ // 调用完成回调
+ if (projectId) {
+ onComplete(projectId);
+
+ // 延迟1秒后自动跳转到项目详情页
+ setTimeout(() => {
+ navigate(`/project/${projectId}`);
+ }, 1000);
+ }
+ };
+
+ // 从角色步骤开始的完整流程
+ const continueFromCharacters = async (worldResult: any) => {
+ if (!generationData || !worldResult?.project_id) return;
+
+ const genreString = Array.isArray(generationData.genre) ? generationData.genre.join('、') : generationData.genre;
+
+ setGenerationSteps(prev => ({ ...prev, characters: 'processing' }));
+ setProgressMessage('正在生成角色...');
+
+ await wizardStreamApi.generateCharactersStream(
+ {
+ project_id: worldResult.project_id,
+ count: generationData.character_count,
+ world_context: {
+ time_period: worldResult.time_period || '',
+ location: worldResult.location || '',
+ atmosphere: worldResult.atmosphere || '',
+ rules: worldResult.rules || '',
+ },
+ theme: generationData.theme,
+ genre: genreString,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(33 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: (result) => {
+ console.log(`成功生成${result.characters?.length || 0}个角色`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('角色生成失败:', error);
+ setErrorDetails(`角色生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, characters: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('角色生成完成');
+ }
+ }
+ );
+
+ await continueFromOutline();
+ };
+
+ // 从大纲步骤开始的完整流程
+ const continueFromOutline = async () => {
+ if (!generationData || !projectId) return;
+
+ setGenerationSteps(prev => ({ ...prev, outline: 'processing' }));
+ setProgressMessage('正在生成大纲...');
+
+ await wizardStreamApi.generateCompleteOutlineStream(
+ {
+ project_id: projectId,
+ chapter_count: generationData.chapter_count,
+ narrative_perspective: generationData.narrative_perspective,
+ target_words: generationData.target_words,
+ },
+ {
+ onProgress: (msg, prog) => {
+ setProgress(66 + Math.floor(prog / 3));
+ setProgressMessage(msg);
+ },
+ onResult: () => {
+ console.log('大纲生成完成');
+ setGenerationSteps(prev => ({ ...prev, outline: 'completed' }));
+ },
+ onError: (error) => {
+ console.error('大纲生成失败:', error);
+ setErrorDetails(`大纲生成失败: ${error}`);
+ setGenerationSteps(prev => ({ ...prev, outline: 'error' }));
+ setLoading(false);
+ throw new Error(error);
+ },
+ onComplete: () => {
+ console.log('大纲生成完成');
+ }
+ }
+ );
+
+ setProgress(100);
+ setProgressMessage('项目创建完成!正在跳转...');
+ message.success('项目创建成功!正在进入项目...');
+ setLoading(false);
+
+ // 调用完成回调
+ if (projectId) {
+ onComplete(projectId);
+
+ // 延迟1秒后自动跳转到项目详情页
+ setTimeout(() => {
+ navigate(`/project/${projectId}`);
+ }, 1000);
+ }
+ };
+
+
+ // 获取步骤状态图标和颜色
+ const getStepStatus = (step: GenerationStep) => {
+ if (step === 'completed') return { icon: , color: '#52c41a' };
+ if (step === 'processing') return { icon: , color: '#1890ff' };
+ if (step === 'error') return { icon: '✗', color: '#ff4d4f' };
+ return { icon: '○', color: '#d9d9d9' };
+ };
+
+ const hasError = generationSteps.worldBuilding === 'error' ||
+ generationSteps.characters === 'error' ||
+ generationSteps.outline === 'error';
+
+ // 渲染生成进度页面
+ const renderGenerating = () => (
+
+
+ 正在为《{config.title}》生成内容
+
+
+
+
+
+
+ {progressMessage}
+
+
+ {errorDetails && (
+
+ 错误详情:
+
+
+ {errorDetails}
+
+
+ )}
+
+
+ {[
+ { key: 'worldBuilding', label: '生成世界观', step: generationSteps.worldBuilding },
+ { key: 'characters', label: '生成角色', step: generationSteps.characters },
+ { key: 'outline', label: '生成大纲', step: generationSteps.outline },
+ ].map(({ key, label, step }) => {
+ const status = getStepStatus(step);
+ return (
+
+
+ {label}
+
+
+ {status.icon}
+
+
+ );
+ })}
+
+
+
+
+ {hasError ? '生成过程中出现错误,请点击重试按钮重新生成' : '请耐心等待,AI正在为您精心创作...'}
+
+
+ {hasError && (
+
+
+
+ )}
+
+
+ );
+
+ return renderGenerating();
+};
\ No newline at end of file
diff --git a/frontend/src/components/ChapterRegenerationModal.tsx b/frontend/src/components/ChapterRegenerationModal.tsx
index 08d5ef6..560906f 100644
--- a/frontend/src/components/ChapterRegenerationModal.tsx
+++ b/frontend/src/components/ChapterRegenerationModal.tsx
@@ -9,7 +9,6 @@ import {
Space,
Alert,
Divider,
- Progress,
Tag,
message,
Collapse,
@@ -22,6 +21,7 @@ import {
CloseCircleOutlined
} from '@ant-design/icons';
import { ssePost } from '../utils/sseClient';
+import { SSEProgressModal } from './SSEProgressModal';
const { TextArea } = Input;
const { Panel } = Collapse;
@@ -242,22 +242,6 @@ const ChapterRegenerationModal: React.FC = ({
)
}
>
- {status === 'generating' && (
-
-
-
- 已生成 {wordCount} 字
-
-
- }
- type="info"
- showIcon
- style={{ marginBottom: 16 }}
- />
- )}
{status === 'success' && (