update:1.优化 AI 流式生成和进度显示系统 2.新增写作风格系统提示词支持 3.灵感模式功能增强,支持灵感重写 4.设置页面功能扩展,新增Gemini适配器 5.提示词模板系统优化,调整灵感模式提示词

This commit is contained in:
xiamuceer
2025-12-28 19:35:23 +08:00
parent f32e51b594
commit 89848e2258
40 changed files with 2752 additions and 1824 deletions
+362 -126
View File
@@ -99,7 +99,7 @@ async def world_building_generator(
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=1,
max_tool_rounds=2,
tool_choice="auto",
provider=None,
model=None
@@ -139,51 +139,118 @@ async def world_building_generator(
final_prompt = base_prompt
yield await SSEResponse.send_progress("正在调用AI生成...", 30)
# 流式生成世界观
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=final_prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 定期更新进度
if chunk_count % 5 == 0:
progress = min(30 + (chunk_count // 5), 70)
yield await SSEResponse.send_progress(f"生成中... ({len(accumulated_text)}字符)", progress)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 解析结果 - 使用统一的JSON清洗方法
yield await SSEResponse.send_progress("解析AI返回结果...", 80)
# ===== 流式生成世界观(带重试机制) =====
MAX_WORLD_RETRIES = 3 # 最多重试3次
world_retry_count = 0
world_generation_success = False
world_data = {}
try:
# ✅ 使用 AIService 的统一清洗方法
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
world_data = json.loads(cleaned_text)
logger.info(f"世界观JSON解析成功")
while world_retry_count < MAX_WORLD_RETRIES and not world_generation_success:
try:
retry_suffix = f" (重试{world_retry_count}/{MAX_WORLD_RETRIES})" if world_retry_count > 0 else ""
yield await SSEResponse.send_progress(f"生成世界观{retry_suffix}...", 30 + world_retry_count * 5)
# 流式生成世界观
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=final_prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
except json.JSONDecodeError as e:
logger.error(f"❌ 世界构建JSON解析失败: {e}")
logger.error(f" 原始内容预览: {accumulated_text[:200]}")
world_data = {
"time_period": "AI返回格式错误,请重试",
"location": "AI返回格式错误,请重试",
"atmosphere": "AI返回格式错误,请重试",
"rules": "AI返回格式错误,请重试"
}
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 世界观生成独立进度:5-95%
if chunk_count % 5 == 0:
progress = min(5 + (chunk_count // 3), 95)
yield await SSEResponse.send_progress(f"世界观生成中... ({len(accumulated_text)}字符)", progress)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 检查是否返回空响应
if not accumulated_text or not accumulated_text.strip():
logger.warning(f"⚠️ AI返回空世界观(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ AI返回为空,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 达到最大重试次数,使用默认值
logger.error("❌ 世界观生成多次返回空响应")
world_data = {
"time_period": "AI多次返回为空,请稍后重试",
"location": "AI多次返回为空,请稍后重试",
"atmosphere": "AI多次返回为空,请稍后重试",
"rules": "AI多次返回为空,请稍后重试"
}
world_generation_success = True # 标记为成功以继续流程
break
# 解析结果 - 使用统一的JSON清洗方法
yield await SSEResponse.send_progress("解析世界观数据...", 96)
try:
logger.info(f"🔍 开始清洗JSON,原始长度: {len(accumulated_text)}")
logger.info(f" 原始内容预览: {accumulated_text[:300]}...")
# ✅ 使用 AIService 的统一清洗方法
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
logger.info(f"✅ JSON清洗完成,清洗后长度: {len(cleaned_text)}")
logger.info(f" 清洗后预览: {cleaned_text[:300]}...")
world_data = json.loads(cleaned_text)
logger.info(f"✅ 世界观JSON解析成功(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}")
world_generation_success = True # 解析成功,标记完成
except json.JSONDecodeError as e:
logger.error(f"❌ 世界构建JSON解析失败(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}: {e}")
logger.error(f" 原始内容长度: {len(accumulated_text)}")
logger.error(f" 原始内容预览: {accumulated_text[:200]}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ JSON解析失败,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 达到最大重试次数,使用默认值
world_data = {
"time_period": "AI返回格式错误,请重试",
"location": "AI返回格式错误,请重试",
"atmosphere": "AI返回格式错误,请重试",
"rules": "AI返回格式错误,请重试"
}
world_generation_success = True # 标记为成功以继续流程
except Exception as e:
logger.error(f"❌ 世界构建生成异常(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}: {type(e).__name__}: {e}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ 生成异常,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 最后一次重试仍失败,抛出异常
logger.error(f" accumulated_text 长度: {len(accumulated_text) if 'accumulated_text' in locals() else 'N/A'}")
raise
# 保存到数据库
yield await SSEResponse.send_progress("保存到数据库...", 90)
yield await SSEResponse.send_progress("保存世界观到数据库...", 99)
# 确保user_id存在
if not user_id:
@@ -240,41 +307,81 @@ async def world_building_generator(
project.wizard_step = 1
await db.commit()
# ===== 自动生成职业体系 =====
yield await SSEResponse.send_progress("🎯 开始生成职业体系框架...", 75)
# ===== 自动生成职业体系(带重试机制+流式) =====
yield await SSEResponse.send_progress("世界观完成!", 100, "success")
yield await SSEResponse.send_progress("🎯 开始生成职业体系框架...", 5)
logger.info(f"🎯 世界观已完成,开始为项目 {project.id} 自动生成职业体系")
try:
# 获取职业生成提示词模板(支持用户自定义)
template = await PromptService.get_template("CAREER_SYSTEM_GENERATION", user_id, db)
career_prompt = PromptService.format_prompt(
template,
title=project.title,
genre=genre or '未设定',
theme=theme or '未设定',
time_period=world_data.get('time_period', '未设定'),
location=world_data.get('location', '未设定'),
atmosphere=world_data.get('atmosphere', '未设定'),
rules=world_data.get('rules', '未设定')
)
yield await SSEResponse.send_progress("正在生成职业体系...", 78)
# 调用AI生成职业
result = await user_ai_service.generate_text(prompt=career_prompt)
career_response = result.get('content', '') if isinstance(result, dict) else result
if not career_response or not career_response.strip():
logger.warning("⚠️ AI返回空职业体系,跳过职业生成")
yield await SSEResponse.send_progress("职业体系生成跳过(AI返回为空)", 85)
else:
yield await SSEResponse.send_progress("解析职业体系数据...", 82)
MAX_CAREER_RETRIES = 3 # 最多重试3次
career_retry_count = 0
career_generation_success = False
while career_retry_count < MAX_CAREER_RETRIES and not career_generation_success:
try:
retry_suffix = f" (重试{career_retry_count}/{MAX_CAREER_RETRIES})" if career_retry_count > 0 else ""
yield await SSEResponse.send_progress(f"正在生成职业体系{retry_suffix}...", 10)
# 获取职业生成提示词模板(支持用户自定义)
template = await PromptService.get_template("CAREER_SYSTEM_GENERATION", user_id, db)
career_prompt = PromptService.format_prompt(
template,
title=project.title,
genre=genre or '未设定',
theme=theme or '未设定',
time_period=world_data.get('time_period', '未设定'),
location=world_data.get('location', '未设定'),
atmosphere=world_data.get('atmosphere', '未设定'),
rules=world_data.get('rules', '未设定')
)
# ✅ 使用流式生成职业体系
career_response = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=career_prompt,
provider=provider,
model=model
):
chunk_count += 1
career_response += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 职业体系生成独立进度:10-95%
if chunk_count % 5 == 0:
progress = min(10 + (chunk_count // 3), 95)
yield await SSEResponse.send_progress(
f"生成职业体系中... ({len(career_response)}字符)",
progress
)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
if not career_response or not career_response.strip():
logger.warning(f"⚠️ AI返回空职业体系(尝试{career_retry_count+1}/{MAX_CAREER_RETRIES}")
career_retry_count += 1
if career_retry_count < MAX_CAREER_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ AI返回为空,准备重试...",
10,
"warning"
)
continue
else:
yield await SSEResponse.send_progress("职业体系生成跳过(AI多次返回为空)", 99)
break
yield await SSEResponse.send_progress("解析职业体系数据...", 96)
# 清洗并解析JSON
try:
cleaned_response = user_ai_service._clean_json_response(career_response)
career_data = json.loads(cleaned_response)
logger.info(f"✅ 职业体系JSON解析成功")
logger.info(f"✅ 职业体系JSON解析成功(尝试{career_retry_count+1}/{MAX_CAREER_RETRIES}")
# 保存主职业
main_careers_created = []
@@ -338,22 +445,51 @@ async def world_building_generator(
await db.commit()
# 标记成功
career_generation_success = True
logger.info(f"🎉 职业体系生成完成:主职业{len(main_careers_created)}个,副职业{len(sub_careers_created)}")
yield await SSEResponse.send_progress(
f"✅ 职业体系生成完成(主{len(main_careers_created)}+副{len(sub_careers_created)}",
90
99
)
except json.JSONDecodeError as e:
logger.error(f"❌ 职业体系JSON解析失败: {e}")
yield await SSEResponse.send_progress("⚠️ 职业体系解析失败,已跳过", 85)
logger.error(f"❌ 职业体系JSON解析失败(尝试{career_retry_count+1}/{MAX_CAREER_RETRIES}: {e}")
career_retry_count += 1
if career_retry_count < MAX_CAREER_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ JSON解析失败,准备重试...",
10,
"warning"
)
continue
else:
yield await SSEResponse.send_progress("⚠️ 职业体系解析失败(已达最大重试次数),已跳过", 99)
except Exception as e:
logger.error(f"❌ 职业体系保存失败: {e}")
yield await SSEResponse.send_progress("⚠️ 职业体系保存失败,已跳过", 85)
except Exception as e:
logger.error(f"❌ 职业体系生成异常: {e}")
yield await SSEResponse.send_progress("⚠️ 职业体系生成失败,已跳过(不影响项目创建)", 85)
logger.error(f"❌ 职业体系保存失败(尝试{career_retry_count+1}/{MAX_CAREER_RETRIES}: {e}")
career_retry_count += 1
if career_retry_count < MAX_CAREER_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ 保存失败,准备重试...",
10,
"warning"
)
continue
else:
yield await SSEResponse.send_progress("⚠️ 职业体系保存失败(已达最大重试次数),已跳过", 99)
except Exception as e:
logger.error(f"❌ 职业体系生成异常(尝试{career_retry_count+1}/{MAX_CAREER_RETRIES}: {e}")
career_retry_count += 1
if career_retry_count < MAX_CAREER_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ 生成异常,准备重试...",
10,
"warning"
)
continue
else:
yield await SSEResponse.send_progress("⚠️ 职业体系生成失败(已达最大重试次数),已跳过(不影响项目创建)", 99)
db_committed = True
@@ -366,7 +502,8 @@ async def world_building_generator(
"rules": world_data.get("rules")
})
yield await SSEResponse.send_progress("完成!", 100, "success")
yield await SSEResponse.send_progress("职业体系完成", 100, "success")
yield await SSEResponse.send_progress("🎉 所有步骤已完成!", 100, "success")
yield await SSEResponse.send_done()
except GeneratorExit:
@@ -473,7 +610,7 @@ async def characters_generator(
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=1, # ✅ 优化: 从2轮减少到1轮
max_tool_rounds=2, # ✅ 优化: 从2轮减少到1轮
tool_choice="auto",
provider=None,
model=None
@@ -611,15 +748,32 @@ async def characters_generator(
else:
prompt = base_prompt
# 流式生成
# 流式生成(带字数统计)
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 定期更新进度和字数
if chunk_count % 5 == 0:
progress = min(batch_progress + 5 + (chunk_count // 10), batch_progress + 15)
yield await SSEResponse.send_progress(
f"生成角色中... ({len(accumulated_text)}字符)",
progress
)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 解析批次结果 - 使用统一的JSON清洗方法
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
@@ -1184,18 +1338,35 @@ async def outline_generator(
requirements=outline_requirements
)
# 流式生成大纲
# 流式生成大纲(带字数统计)
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=outline_prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 定期更新进度和字数(5-95%,AI生成占90%)
if chunk_count % 5 == 0:
progress = min(5 + (chunk_count // 3), 95)
yield await SSEResponse.send_progress(
f"生成大纲中... ({len(accumulated_text)}字符)",
progress
)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 解析大纲结果 - 使用统一的JSON清洗方法
yield await SSEResponse.send_progress("解析大纲...", 40)
yield await SSEResponse.send_progress("解析大纲...", 96)
try:
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
@@ -1208,7 +1379,7 @@ async def outline_generator(
return
# 保存大纲到数据库
yield await SSEResponse.send_progress("保存大纲到数据库...", 45)
yield await SSEResponse.send_progress("保存大纲到数据库...", 97)
created_outlines = []
for index, outline_item in enumerate(outline_data[:outline_count], 1):
outline = Outline(
@@ -1231,7 +1402,7 @@ async def outline_generator(
created_chapters = []
if project.outline_mode == 'one-to-one':
# 一对一模式:自动为每个大纲创建对应的章节
yield await SSEResponse.send_progress("一对一模式:自动创建章节...", 50)
yield await SSEResponse.send_progress("一对一模式:自动创建章节...", 98)
for outline in created_outlines:
chapter = Chapter(
@@ -1250,10 +1421,10 @@ async def outline_generator(
await db.refresh(chapter)
logger.info(f"✅ 一对一模式:自动创建了{len(created_chapters)}个章节")
yield await SSEResponse.send_progress(f"已自动创建{len(created_chapters)}个章节", 85)
yield await SSEResponse.send_progress(f"已自动创建{len(created_chapters)}个章节", 99)
else:
# 一对多模式:跳过自动创建,用户可手动展开
yield await SSEResponse.send_progress("细化模式:跳过自动创建章节", 85)
yield await SSEResponse.send_progress("细化模式:跳过自动创建章节", 99)
logger.info(f"📝 细化模式:跳过章节创建,用户可在大纲页面手动展开")
# 更新项目信息
@@ -1396,7 +1567,7 @@ async def world_building_regenerate_generator(
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=1,
max_tool_rounds=2,
tool_choice="auto",
provider=None,
model=None
@@ -1433,44 +1604,109 @@ async def world_building_regenerate_generator(
final_prompt = base_prompt
yield await SSEResponse.send_progress("正在调用AI生成...", 30)
# 流式生成世界观
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=final_prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
yield await SSEResponse.send_chunk(chunk)
if chunk_count % 5 == 0:
progress = min(30 + (chunk_count // 5), 70)
yield await SSEResponse.send_progress(f"生成中... ({len(accumulated_text)}字符)", progress)
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 解析结果 - 使用统一的JSON清洗方法
yield await SSEResponse.send_progress("解析AI返回结果...", 80)
# ===== 流式生成世界观(带重试机制) =====
MAX_WORLD_RETRIES = 3 # 最多重试3次
world_retry_count = 0
world_generation_success = False
world_data = {}
try:
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
world_data = json.loads(cleaned_text)
logger.info(f"✅ 世界观重新生成JSON解析成功")
while world_retry_count < MAX_WORLD_RETRIES and not world_generation_success:
try:
retry_suffix = f" (重试{world_retry_count}/{MAX_WORLD_RETRIES})" if world_retry_count > 0 else ""
yield await SSEResponse.send_progress(f"重新生成世界观{retry_suffix}...", 30 + world_retry_count * 5)
# 流式生成世界观
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=final_prompt,
provider=provider,
model=model
):
chunk_count += 1
accumulated_text += chunk
except json.JSONDecodeError as e:
logger.error(f"世界构建JSON解析失败: {e}")
world_data = {
"time_period": "AI返回格式错误,请重试",
"location": "AI返回格式错误,请重试",
"atmosphere": "AI返回格式错误,请重试",
"rules": "AI返回格式错误,请重试"
}
yield await SSEResponse.send_chunk(chunk)
if chunk_count % 5 == 0:
progress = min(30 + (chunk_count // 5), 85)
yield await SSEResponse.send_progress(f"生成中... ({len(accumulated_text)}字符)", progress)
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
# 检查是否返回空响应
if not accumulated_text or not accumulated_text.strip():
logger.warning(f"⚠️ AI返回空世界观(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ AI返回为空,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 达到最大重试次数,使用默认值
logger.error("❌ 世界观重新生成多次返回空响应")
world_data = {
"time_period": "AI多次返回为空,请稍后重试",
"location": "AI多次返回为空,请稍后重试",
"atmosphere": "AI多次返回为空,请稍后重试",
"rules": "AI多次返回为空,请稍后重试"
}
world_generation_success = True
break
# 解析结果 - 使用统一的JSON清洗方法
yield await SSEResponse.send_progress("解析AI返回结果...", 80)
try:
logger.info(f"🔍 开始清洗JSON,原始长度: {len(accumulated_text)}")
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
logger.info(f"✅ JSON清洗完成,清洗后长度: {len(cleaned_text)}")
world_data = json.loads(cleaned_text)
logger.info(f"✅ 世界观重新生成JSON解析成功(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}")
world_generation_success = True
except json.JSONDecodeError as e:
logger.error(f"❌ 世界构建JSON解析失败(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}: {e}")
logger.error(f" 原始内容长度: {len(accumulated_text)}")
logger.error(f" 原始内容预览: {accumulated_text[:200]}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ JSON解析失败,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 达到最大重试次数,使用默认值
world_data = {
"time_period": "AI返回格式错误,请重试",
"location": "AI返回格式错误,请重试",
"atmosphere": "AI返回格式错误,请重试",
"rules": "AI返回格式错误,请重试"
}
world_generation_success = True
except Exception as e:
logger.error(f"❌ 世界观重新生成异常(尝试{world_retry_count+1}/{MAX_WORLD_RETRIES}: {type(e).__name__}: {e}")
world_retry_count += 1
if world_retry_count < MAX_WORLD_RETRIES:
yield await SSEResponse.send_progress(
f"⚠️ 生成异常,准备重试...",
30 + world_retry_count * 5,
"warning"
)
continue
else:
# 最后一次重试仍失败,抛出异常
logger.error(f" accumulated_text 长度: {len(accumulated_text) if 'accumulated_text' in locals() else 'N/A'}")
raise
# 不保存到数据库,仅返回生成结果供用户预览
yield await SSEResponse.send_progress("生成完成,等待用户确认...", 90)