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') {