diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index 85b9434..19d2917 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -1541,11 +1541,19 @@ async def generate_chapter_content_stream( 确保在整个章节创作过程中始终保持风格的一致性。""" logger.info(f"✅ 已将写作风格注入系统提示词({len(style_content)}字符)") + # 🔢 计算 max_tokens 限制 + # 中文字符约 1.5-2 个 token,使用 2.5 倍系数确保有足够空间完成段落 + # 同时设置上限防止过长,下限确保基本可用 + calculated_max_tokens = int(target_word_count * 3) + calculated_max_tokens = max(2000, min(calculated_max_tokens, 16000)) # 限制在 2000-16000 之间 + logger.info(f"📊 目标字数: {target_word_count}, 计算 max_tokens: {calculated_max_tokens}") + # 准备生成参数 generate_kwargs = { "prompt": prompt, - "system_prompt": system_prompt_with_style, - "tool_choice": "required" + "system_prompt": system_prompt_with_style, + "tool_choice": "required", + "max_tokens": calculated_max_tokens # 添加 max_tokens 限制 } if custom_model: logger.info(f" 使用自定义模型: {custom_model}") @@ -1789,11 +1797,18 @@ async def get_analysis_task_status( current_time = datetime.now() # 自动恢复卡住的任务 + # 注意:后端分析有3次重试机制,每次重试会重置 started_at + # 所以超时时间需要足够长以支持完整的重试周期(约5分钟) if task.status == 'running': - # 如果任务在running状态超过1分钟,标记为失败 - if task.started_at and (current_time - task.started_at) > timedelta(minutes=1): + # 检查是否正在重试(error_message 包含"重试"信息) + is_retrying = task.error_message and '重试' in task.error_message + # 如果正在重试,给予更长的超时时间(5分钟),否则3分钟 + timeout_minutes = 5 if is_retrying else 3 + + # 如果任务在running状态超过超时时间,标记为失败 + if task.started_at and (current_time - task.started_at) > timedelta(minutes=timeout_minutes): task.status = 'failed' - task.error_message = '任务超时(超过1分钟未完成,已自动恢复)' + task.error_message = f'任务超时(超过{timeout_minutes}分钟未完成,已自动恢复)' task.completed_at = current_time task.progress = 0 auto_recovered = True @@ -1802,10 +1817,10 @@ async def get_analysis_task_status( logger.warning(f"🔄 自动恢复卡住的任务: {task.id}, 章节: {chapter_id}") elif task.status == 'pending': - # 如果任务在pending状态超过2分钟仍未开始,标记为失败 - if task.created_at and (current_time - task.created_at) > timedelta(minutes=2): + # 如果任务在pending状态超过3分钟仍未开始,标记为失败 + if task.created_at and (current_time - task.created_at) > timedelta(minutes=3): task.status = 'failed' - task.error_message = '任务启动超时(超过2分钟未启动,已自动恢复)' + task.error_message = '任务启动超时(超过3分钟未启动,已自动恢复)' task.completed_at = current_time task.progress = 0 auto_recovered = True @@ -2862,13 +2877,21 @@ async def generate_single_chapter_for_batch( 确保在整个章节创作过程中始终保持风格的一致性。""" logger.info(f"✅ 批量生成 - 已将写作风格注入系统提示词({len(style_content)}字符)") + # 🔢 计算 max_tokens 限制(批量生成) + # 中文字符约 1.5-2 个 token,使用 2.5 倍系数确保有足够空间完成段落 + # 同时设置上限防止过长,下限确保基本可用 + calculated_max_tokens = int(target_word_count * 3) + calculated_max_tokens = max(2000, min(calculated_max_tokens, 16000)) # 限制在 2000-16000 之间 + logger.info(f"📊 批量生成 - 目标字数: {target_word_count}, 计算 max_tokens: {calculated_max_tokens}") + # 非流式生成内容 full_content = "" # 准备生成参数 generate_kwargs = { "prompt": prompt, "system_prompt": system_prompt_with_style, - "tool_choice": "required" + "tool_choice": "required", + "max_tokens": calculated_max_tokens # 添加 max_tokens 限制 } # 如果传入了自定义模型,使用指定的模型 if custom_model: diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 077a412..0f9e7e1 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -547,7 +547,7 @@ class PromptService: 撰写第{chapter_number}章《{chapter_title}》的完整正文。 【基本要求】 -- 目标字数:{target_word_count}字(允许±500字浮动) +- 目标字数:{target_word_count}字(允许±200字浮动) - 叙事视角:{narrative_perspective} diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 3e702eb..45c1a41 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -1193,12 +1193,19 @@ export default function Chapters() { 等待分析 ); - case 'running': + case 'running': { + // 检查是否正在重试(后端会在error_message中包含"重试"信息) + const isRetrying = task.error_message && task.error_message.includes('重试'); return ( - } color="processing"> - 分析中 {task.progress}% + } + color={isRetrying ? "warning" : "processing"} + title={task.error_message || undefined} + > + {isRetrying ? `重试中 ${task.progress}%` : `分析中 ${task.progress}%`} ); + } case 'completed': return ( } color="success"> diff --git a/frontend/src/store/hooks.ts b/frontend/src/store/hooks.ts index 519a9a4..63a5001 100644 --- a/frontend/src/store/hooks.ts +++ b/frontend/src/store/hooks.ts @@ -363,12 +363,15 @@ export function useChapterSync() { } } else if (message.type === 'error') { throw new Error(message.error || '生成失败'); - } else if (message.type === 'done') { - // 生成完成,保存分析任务ID - analysisTaskId = message.analysis_task_id; + } else if (message.type === 'result') { + // 结果消息,包含分析任务ID + if (message.data?.analysis_task_id) { + analysisTaskId = message.data.analysis_task_id; + } if (onProgressUpdate) { onProgressUpdate('生成完成', 100); } + } else if (message.type === 'done') { // 生成完成,刷新章节数据 await refreshChapters(); } else if (message.type === 'analysis_started') {