fix:1.修复向导模式生成大纲并细纲逻辑
This commit is contained in:
+131
-168
@@ -17,6 +17,7 @@ from app.models.project_default_style import ProjectDefaultStyle
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.mcp_tool_service import MCPToolService
|
||||
from app.services.prompt_service import prompt_service
|
||||
from app.services.plot_expansion_service import PlotExpansionService
|
||||
from app.logger import get_logger
|
||||
from app.utils.sse_response import SSEResponse, create_sse_response
|
||||
from app.api.settings import get_user_ai_service
|
||||
@@ -875,24 +876,22 @@ async def outline_generator(
|
||||
db: AsyncSession,
|
||||
user_ai_service: AIService
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""大纲生成流式生成器 - 向导固定生成前5章作为开局"""
|
||||
"""大纲生成流式生成器 - 向导生成3个大纲节点,每个展开为3章,共9章"""
|
||||
db_committed = False
|
||||
try:
|
||||
yield await SSEResponse.send_progress("开始生成大纲...", 5)
|
||||
|
||||
project_id = data.get("project_id")
|
||||
# 向导固定生成5章,忽略传入的chapter_count
|
||||
chapter_count = 5
|
||||
# 向导固定生成3个大纲节点
|
||||
outline_count = 3
|
||||
# 每个大纲展开为3章
|
||||
chapters_per_outline = 3
|
||||
narrative_perspective = data.get("narrative_perspective")
|
||||
target_words = data.get("target_words", 100000)
|
||||
requirements = data.get("requirements", "")
|
||||
provider = data.get("provider")
|
||||
model = data.get("model")
|
||||
|
||||
# 5章一次性生成,不需要分批
|
||||
BATCH_SIZE = 5
|
||||
MAX_RETRIES = 3
|
||||
|
||||
# 获取项目信息
|
||||
yield await SSEResponse.send_progress("加载项目信息...", 10)
|
||||
result = await db.execute(
|
||||
@@ -915,192 +914,156 @@ async def outline_generator(
|
||||
for char in characters
|
||||
])
|
||||
|
||||
# 分批生成大纲
|
||||
yield await SSEResponse.send_progress("准备分批生成大纲...", 20)
|
||||
# 第一阶段:生成3个粗粒度大纲节点
|
||||
yield await SSEResponse.send_progress(f"生成{outline_count}个大纲节点...", 20)
|
||||
|
||||
all_outlines = []
|
||||
total_batches = (chapter_count + BATCH_SIZE - 1) // BATCH_SIZE
|
||||
outline_requirements = f"{requirements}\n\n【重要说明】这是小说的开局部分,请生成{outline_count}个大纲节点,重点关注:\n"
|
||||
outline_requirements += "1. 引入主要角色和世界观设定\n"
|
||||
outline_requirements += "2. 建立主线冲突和故事钩子\n"
|
||||
outline_requirements += "3. 展开初期情节,为后续发展埋下伏笔\n"
|
||||
outline_requirements += "4. 不要试图完结故事,这只是开始部分\n"
|
||||
outline_requirements += "5. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记\n"
|
||||
|
||||
for batch_idx in range(total_batches):
|
||||
start_chapter = batch_idx * BATCH_SIZE + 1
|
||||
end_chapter = min((batch_idx + 1) * BATCH_SIZE, chapter_count)
|
||||
current_batch_size = end_chapter - start_chapter + 1
|
||||
|
||||
batch_progress = 20 + (batch_idx * 55 // total_batches)
|
||||
|
||||
# 重试逻辑
|
||||
retry_count = 0
|
||||
batch_success = False
|
||||
|
||||
while retry_count < MAX_RETRIES and not batch_success:
|
||||
try:
|
||||
retry_suffix = f" (重试{retry_count}/{MAX_RETRIES})" if retry_count > 0 else ""
|
||||
yield await SSEResponse.send_progress(
|
||||
f"生成第{start_chapter}-{end_chapter}章大纲{retry_suffix}...",
|
||||
batch_progress
|
||||
)
|
||||
|
||||
# 构建批次提示词 - 包含前文摘要保持故事连贯
|
||||
previous_context = ""
|
||||
if all_outlines:
|
||||
previous_context = "\n\n【前文情节摘要】:\n"
|
||||
for outline in all_outlines[-3:]: # 只包含最近3章,避免过长
|
||||
ch_num = outline.get("chapter_number", "?")
|
||||
ch_title = outline.get("title", "未命名")
|
||||
ch_summary = outline.get("summary", "")[:100]
|
||||
previous_context += f"第{ch_num}章《{ch_title}》: {ch_summary}...\n"
|
||||
previous_context += f"\n请确保第{start_chapter}-{end_chapter}章与前文情节自然衔接,保持故事连贯性。\n"
|
||||
|
||||
# 向导专用的开局大纲要求
|
||||
batch_requirements = f"{requirements}\n\n【重要说明】这是小说的开局部分,请生成前5章大纲,重点关注:\n"
|
||||
batch_requirements += "1. 引入主要角色和世界观设定\n"
|
||||
batch_requirements += "2. 建立主线冲突和故事钩子\n"
|
||||
batch_requirements += "3. 展开初期情节,为后续发展埋下伏笔\n"
|
||||
batch_requirements += "4. 不要试图完结故事,这只是开始部分\n"
|
||||
batch_requirements += "5. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记\n"
|
||||
|
||||
batch_prompt = prompt_service.get_complete_outline_prompt(
|
||||
title=project.title,
|
||||
theme=project.theme or "未设定",
|
||||
genre=project.genre or "通用",
|
||||
chapter_count=5, # 固定5章
|
||||
narrative_perspective=narrative_perspective,
|
||||
target_words=target_words // 20, # 开局约占总字数的1/20
|
||||
time_period=project.world_time_period or "未设定",
|
||||
location=project.world_location or "未设定",
|
||||
atmosphere=project.world_atmosphere or "未设定",
|
||||
rules=project.world_rules or "未设定",
|
||||
characters_info=characters_info or "暂无角色信息",
|
||||
requirements=batch_requirements
|
||||
)
|
||||
|
||||
# 流式生成
|
||||
accumulated_text = ""
|
||||
async for chunk in user_ai_service.generate_text_stream(
|
||||
prompt=batch_prompt,
|
||||
provider=provider,
|
||||
model=model
|
||||
):
|
||||
accumulated_text += chunk
|
||||
yield await SSEResponse.send_chunk(chunk)
|
||||
|
||||
# 解析结果
|
||||
cleaned_text = accumulated_text.strip()
|
||||
|
||||
# 移除markdown代码块标记
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
batch_outline_data = json.loads(cleaned_text)
|
||||
if not isinstance(batch_outline_data, list):
|
||||
batch_outline_data = [batch_outline_data]
|
||||
|
||||
# 验证生成数量
|
||||
if len(batch_outline_data) < current_batch_size:
|
||||
logger.warning(f"批次{batch_idx+1}生成数量不足: 期望{current_batch_size}, 实际{len(batch_outline_data)}")
|
||||
if retry_count < MAX_RETRIES - 1:
|
||||
retry_count += 1
|
||||
yield await SSEResponse.send_progress(
|
||||
f"生成数量不足,准备重试...",
|
||||
batch_progress,
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
|
||||
# 修正章节编号
|
||||
for i, chapter_data in enumerate(batch_outline_data):
|
||||
chapter_data["chapter_number"] = start_chapter + i
|
||||
|
||||
all_outlines.extend(batch_outline_data)
|
||||
batch_success = True
|
||||
logger.info(f"批次{batch_idx+1}成功生成{len(batch_outline_data)}章大纲")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"大纲生成批次{batch_idx+1} JSON解析失败(尝试{retry_count+1}/{MAX_RETRIES}): {e}")
|
||||
retry_count += 1
|
||||
if retry_count < MAX_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"解析失败,准备重试...",
|
||||
batch_progress,
|
||||
"warning"
|
||||
)
|
||||
else:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"批次{batch_idx+1}多次重试失败,跳过",
|
||||
batch_progress,
|
||||
"warning"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"批次{batch_idx+1}生成异常(尝试{retry_count+1}/{MAX_RETRIES}): {e}")
|
||||
retry_count += 1
|
||||
if retry_count < MAX_RETRIES:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"生成异常,准备重试...",
|
||||
batch_progress,
|
||||
"warning"
|
||||
)
|
||||
else:
|
||||
yield await SSEResponse.send_progress(
|
||||
f"批次{batch_idx+1}多次重试失败,跳过",
|
||||
batch_progress,
|
||||
"warning"
|
||||
)
|
||||
outline_prompt = prompt_service.get_complete_outline_prompt(
|
||||
title=project.title,
|
||||
theme=project.theme or "未设定",
|
||||
genre=project.genre or "通用",
|
||||
chapter_count=outline_count,
|
||||
narrative_perspective=narrative_perspective,
|
||||
target_words=target_words // 10, # 开局约占总字数的1/10
|
||||
time_period=project.world_time_period or "未设定",
|
||||
location=project.world_location or "未设定",
|
||||
atmosphere=project.world_atmosphere or "未设定",
|
||||
rules=project.world_rules or "未设定",
|
||||
characters_info=characters_info or "暂无角色信息",
|
||||
requirements=outline_requirements
|
||||
)
|
||||
|
||||
if not all_outlines:
|
||||
yield await SSEResponse.send_error("所有批次都生成失败,请重试")
|
||||
# 流式生成大纲
|
||||
accumulated_text = ""
|
||||
async for chunk in user_ai_service.generate_text_stream(
|
||||
prompt=outline_prompt,
|
||||
provider=provider,
|
||||
model=model
|
||||
):
|
||||
accumulated_text += chunk
|
||||
yield await SSEResponse.send_chunk(chunk)
|
||||
|
||||
# 解析大纲结果
|
||||
yield await SSEResponse.send_progress("解析大纲...", 40)
|
||||
cleaned_text = accumulated_text.strip()
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
try:
|
||||
outline_data = json.loads(cleaned_text)
|
||||
if not isinstance(outline_data, list):
|
||||
outline_data = [outline_data]
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"大纲JSON解析失败: {e}")
|
||||
yield await SSEResponse.send_error("大纲生成失败,请重试")
|
||||
return
|
||||
|
||||
outline_data = all_outlines
|
||||
|
||||
# 保存到数据库
|
||||
yield await SSEResponse.send_progress("保存大纲到数据库...", 90)
|
||||
|
||||
# 保存大纲到数据库
|
||||
yield await SSEResponse.send_progress("保存大纲到数据库...", 45)
|
||||
created_outlines = []
|
||||
for index, chapter_data in enumerate(outline_data[:chapter_count], 1):
|
||||
chapter_num = chapter_data.get("chapter_number", index)
|
||||
|
||||
for index, outline_item in enumerate(outline_data[:outline_count], 1):
|
||||
outline = Outline(
|
||||
project_id=project_id,
|
||||
title=chapter_data.get("title", f"第{chapter_num}章"),
|
||||
content=chapter_data.get("summary", chapter_data.get("content", "")),
|
||||
structure=json.dumps(chapter_data, ensure_ascii=False),
|
||||
order_index=chapter_num
|
||||
title=outline_item.get("title", f"第{index}节"),
|
||||
content=outline_item.get("summary", outline_item.get("content", "")),
|
||||
structure=json.dumps(outline_item, ensure_ascii=False),
|
||||
order_index=index
|
||||
)
|
||||
db.add(outline)
|
||||
created_outlines.append(outline)
|
||||
|
||||
chapter = Chapter(
|
||||
project_id=project_id,
|
||||
chapter_number=chapter_num,
|
||||
title=chapter_data.get("title", f"第{chapter_num}章"),
|
||||
summary=chapter_data.get("summary", chapter_data.get("content", ""))[:500] if chapter_data.get("summary") or chapter_data.get("content") else "",
|
||||
status="draft"
|
||||
)
|
||||
db.add(chapter)
|
||||
|
||||
# 更新项目(向导固定生成5章作为开局)
|
||||
project.chapter_count = 5
|
||||
await db.flush() # 获取大纲ID
|
||||
for outline in created_outlines:
|
||||
await db.refresh(outline)
|
||||
|
||||
logger.info(f"✅ 成功创建{len(created_outlines)}个大纲节点")
|
||||
|
||||
# 第二阶段:使用PlotExpansionService将每个大纲展开为详细章节
|
||||
yield await SSEResponse.send_progress(f"开始将大纲展开为详细章节...", 50)
|
||||
|
||||
expansion_service = PlotExpansionService(user_ai_service)
|
||||
total_chapters_created = 0
|
||||
start_chapter_number = 1
|
||||
|
||||
for outline_idx, outline in enumerate(created_outlines, 1):
|
||||
yield await SSEResponse.send_progress(
|
||||
f"展开第{outline_idx}/{len(created_outlines)}个大纲节点...",
|
||||
50 + (outline_idx - 1) * 35 // len(created_outlines)
|
||||
)
|
||||
|
||||
try:
|
||||
# 分析大纲并生成章节规划
|
||||
chapter_plans = await expansion_service.analyze_outline_for_chapters(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=chapters_per_outline,
|
||||
expansion_strategy="balanced",
|
||||
enable_scene_analysis=False,
|
||||
provider=provider,
|
||||
model=model
|
||||
)
|
||||
|
||||
logger.info(f"大纲 {outline.title} 生成了 {len(chapter_plans)} 个章节规划")
|
||||
|
||||
# 创建章节记录
|
||||
chapters = await expansion_service.create_chapters_from_plans(
|
||||
outline_id=outline.id,
|
||||
chapter_plans=chapter_plans,
|
||||
project_id=project_id,
|
||||
db=db,
|
||||
start_chapter_number=start_chapter_number
|
||||
)
|
||||
|
||||
total_chapters_created += len(chapters)
|
||||
start_chapter_number += len(chapters)
|
||||
|
||||
logger.info(f"✅ 大纲 {outline.title} 创建了 {len(chapters)} 个章节记录")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 展开大纲 {outline.title} 失败: {e}")
|
||||
yield await SSEResponse.send_progress(
|
||||
f"⚠️ 展开大纲{outline_idx}失败,跳过",
|
||||
50 + outline_idx * 35 // len(created_outlines),
|
||||
"warning"
|
||||
)
|
||||
continue
|
||||
|
||||
# 更新项目信息
|
||||
project.chapter_count = total_chapters_created
|
||||
project.narrative_perspective = narrative_perspective
|
||||
project.target_words = target_words
|
||||
project.status = "writing"
|
||||
project.wizard_status = "completed"
|
||||
|
||||
project.wizard_step = 4
|
||||
|
||||
await db.commit()
|
||||
db_committed = True
|
||||
|
||||
logger.info(f"📊 向导大纲生成完成:")
|
||||
logger.info(f" - 创建大纲节点:{len(created_outlines)} 个")
|
||||
logger.info(f" - 创建详细章节:{total_chapters_created} 个")
|
||||
logger.info(f" - 平均每个大纲:{total_chapters_created / len(created_outlines):.1f} 章")
|
||||
|
||||
# 发送结果
|
||||
yield await SSEResponse.send_result({
|
||||
"message": f"成功生成{len(created_outlines)}章大纲",
|
||||
"count": len(created_outlines),
|
||||
"message": f"成功生成{len(created_outlines)}个大纲节点,展开为{total_chapters_created}个详细章节",
|
||||
"outline_count": len(created_outlines),
|
||||
"chapter_count": total_chapters_created,
|
||||
"outlines": [
|
||||
{
|
||||
"id": outline.id,
|
||||
"order_index": outline.order_index,
|
||||
"title": outline.title,
|
||||
"content": outline.content[:100] + "..." if len(outline.content) > 100 else outline.content
|
||||
|
||||
Reference in New Issue
Block a user