diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index 1dc4e57..99b698f 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -2451,6 +2451,284 @@ async def expand_outline_generator( yield await tracker.error(f"展开失败: {str(e)}") +async def _save_background_task_result(db: AsyncSession, task_id: str, result_data: Dict[str, Any]) -> None: + """保存后台任务结果到 background_tasks.task_result。""" + from app.models.background_task import BackgroundTask + + task_result = await db.execute(select(BackgroundTask).where(BackgroundTask.id == task_id)) + task = task_result.scalar_one_or_none() + if task: + task.task_result = result_data + await db.commit() + + +async def _run_outline_expansion_background( + task_id: str, + user_id: str, + outline_id: str, + data: Dict[str, Any] +): + """后台执行单个大纲展开并可直接创建章节。""" + from app.database import get_engine + from app.api.settings import get_user_ai_service_from_db + from app.services.background_task_service import TaskProgressTracker + from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession as BgAsyncSession + + engine = await get_engine(user_id) + AsyncSessionLocal = async_sessionmaker(engine, class_=BgAsyncSession, expire_on_commit=False) + + async with AsyncSessionLocal() as bg_db: + tracker = TaskProgressTracker(task_id, user_id, "大纲展开") + try: + await tracker.start("开始大纲展开任务...") + + target_chapter_count = int(data.get("target_chapter_count", 3)) + expansion_strategy = data.get("expansion_strategy", "balanced") + enable_scene_analysis = data.get("enable_scene_analysis", True) + auto_create_chapters = data.get("auto_create_chapters", True) + batch_size = int(data.get("batch_size", 5)) + + await tracker.loading("加载大纲信息...", 0.3) + outline_result = await bg_db.execute(select(Outline).where(Outline.id == outline_id)) + outline = outline_result.scalar_one_or_none() + if not outline: + raise ValueError("大纲不存在") + + await tracker.loading("加载项目信息...", 0.7) + project_result = await bg_db.execute(select(Project).where(Project.id == outline.project_id)) + project = project_result.scalar_one_or_none() + if not project: + raise ValueError("项目不存在") + + if await tracker.check_cancelled(): + return + + await tracker.preparing(f"准备展开《{outline.title}》为 {target_chapter_count} 章...") + + bg_ai_service = await get_user_ai_service_from_db(user_id, bg_db) + expansion_service = PlotExpansionService(bg_ai_service) + + await tracker.generating( + current_chars=0, + estimated_total=target_chapter_count * 500, + message=f"AI分析大纲《{outline.title}》,生成章节规划..." + ) + + chapter_plans = await expansion_service.analyze_outline_for_chapters( + outline=outline, + project=project, + db=bg_db, + target_chapter_count=target_chapter_count, + expansion_strategy=expansion_strategy, + enable_scene_analysis=enable_scene_analysis, + provider=data.get("provider"), + model=data.get("model"), + batch_size=batch_size, + progress_callback=None + ) + + if await tracker.check_cancelled(): + return + if not chapter_plans: + raise ValueError("AI分析失败,未能生成章节规划") + + await tracker.parsing(f"规划生成完成,共 {len(chapter_plans)} 个章节") + + created_chapters = None + if auto_create_chapters: + await tracker.saving("创建章节记录...", 0.3) + created_chapters = await expansion_service.create_chapters_from_plans( + outline_id=outline_id, + chapter_plans=chapter_plans, + project_id=outline.project_id, + db=bg_db, + start_chapter_number=None + ) + await tracker.saving(f"成功创建 {len(created_chapters)} 个章节记录", 0.8) + + result_data = { + "outline_id": outline_id, + "outline_title": outline.title, + "target_chapter_count": target_chapter_count, + "actual_chapter_count": len(chapter_plans), + "expansion_strategy": expansion_strategy, + "chapter_plans": chapter_plans, + "created_chapters": [ + { + "id": ch.id, + "chapter_number": ch.chapter_number, + "title": ch.title, + "summary": ch.summary, + "outline_id": ch.outline_id, + "sub_index": ch.sub_index, + "status": ch.status + } + for ch in created_chapters + ] if created_chapters else None + } + await _save_background_task_result(bg_db, task_id, result_data) + await tracker.complete(f"《{outline.title}》展开完成") + except Exception as e: + logger.error(f"后台大纲展开失败: {str(e)}", exc_info=True) + try: + if bg_db.in_transaction(): + await bg_db.rollback() + except Exception: + pass + await tracker.error(str(e)) + + +async def _run_batch_outline_expansion_background( + task_id: str, + user_id: str, + data: Dict[str, Any] +): + """后台执行批量大纲展开并可直接创建章节。""" + from app.database import get_engine + from app.api.settings import get_user_ai_service_from_db + from app.services.background_task_service import TaskProgressTracker + from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession as BgAsyncSession + + engine = await get_engine(user_id) + AsyncSessionLocal = async_sessionmaker(engine, class_=BgAsyncSession, expire_on_commit=False) + + async with AsyncSessionLocal() as bg_db: + tracker = TaskProgressTracker(task_id, user_id, "批量大纲展开") + try: + await tracker.start("开始批量大纲展开任务...") + + project_id = data.get("project_id") + chapters_per_outline = int(data.get("chapters_per_outline", 3)) + expansion_strategy = data.get("expansion_strategy", "balanced") + auto_create_chapters = data.get("auto_create_chapters", True) + outline_ids = data.get("outline_ids") + + await tracker.loading("加载项目信息...", 0.4) + project_result = await bg_db.execute(select(Project).where(Project.id == project_id)) + project = project_result.scalar_one_or_none() + if not project: + raise ValueError("项目不存在") + + await tracker.loading("获取大纲列表...", 0.8) + if outline_ids: + outlines_result = await bg_db.execute( + select(Outline) + .where(Outline.project_id == project_id, Outline.id.in_(outline_ids)) + .order_by(Outline.order_index) + ) + else: + outlines_result = await bg_db.execute( + select(Outline) + .where(Outline.project_id == project_id) + .order_by(Outline.order_index) + ) + outlines = outlines_result.scalars().all() + if not outlines: + raise ValueError("没有找到要展开的大纲") + + total_outlines = len(outlines) + await tracker.preparing(f"共找到 {total_outlines} 个大纲,准备批量展开...") + + bg_ai_service = await get_user_ai_service_from_db(user_id, bg_db) + expansion_service = PlotExpansionService(bg_ai_service) + + expansion_results = [] + skipped_outlines = [] + total_chapters_created = 0 + + for idx, outline in enumerate(outlines): + if await tracker.check_cancelled(): + return + + await tracker.generating( + current_chars=idx * chapters_per_outline * 500, + estimated_total=total_outlines * chapters_per_outline * 500, + message=f"处理第 {idx + 1}/{total_outlines} 个大纲:《{outline.title}》" + ) + + existing_chapters_result = await bg_db.execute( + select(Chapter).where(Chapter.outline_id == outline.id).limit(1) + ) + existing_chapter = existing_chapters_result.scalar_one_or_none() + if existing_chapter: + skipped_outlines.append({ + "outline_id": outline.id, + "outline_title": outline.title, + "reason": "已展开" + }) + await tracker.warning(f"《{outline.title}》已展开过,已跳过") + continue + + chapter_plans = await expansion_service.analyze_outline_for_chapters( + outline=outline, + project=project, + db=bg_db, + target_chapter_count=chapters_per_outline, + expansion_strategy=expansion_strategy, + enable_scene_analysis=data.get("enable_scene_analysis", True), + provider=data.get("provider"), + model=data.get("model") + ) + + created_chapters = None + if auto_create_chapters: + created_chapters = await expansion_service.create_chapters_from_plans( + outline_id=outline.id, + chapter_plans=chapter_plans, + project_id=outline.project_id, + db=bg_db, + start_chapter_number=None + ) + total_chapters_created += len(created_chapters) + + expansion_results.append({ + "outline_id": outline.id, + "outline_title": outline.title, + "target_chapter_count": chapters_per_outline, + "actual_chapter_count": len(chapter_plans), + "expansion_strategy": expansion_strategy, + "chapter_plans": chapter_plans, + "created_chapters": [ + { + "id": ch.id, + "chapter_number": ch.chapter_number, + "title": ch.title, + "summary": ch.summary, + "outline_id": ch.outline_id, + "sub_index": ch.sub_index, + "status": ch.status + } + for ch in created_chapters + ] if created_chapters else None + }) + + await tracker.generating( + current_chars=(idx + 1) * chapters_per_outline * 500, + estimated_total=total_outlines * chapters_per_outline * 500, + message=f"《{outline.title}》展开完成 ({len(chapter_plans)} 章)" + ) + + await tracker.parsing("整理批量展开结果...") + result_data = { + "project_id": project_id, + "total_outlines_expanded": len(expansion_results), + "total_chapters_created": total_chapters_created, + "skipped_count": len(skipped_outlines), + "skipped_outlines": skipped_outlines, + "expansion_results": expansion_results + } + await _save_background_task_result(bg_db, task_id, result_data) + await tracker.complete(f"批量展开完成,共创建 {total_chapters_created} 个章节") + except Exception as e: + logger.error(f"后台批量大纲展开失败: {str(e)}", exc_info=True) + try: + if bg_db.in_transaction(): + await bg_db.rollback() + except Exception: + pass + await tracker.error(str(e)) + + @router.post("/{outline_id}/create-single-chapter", summary="一对一创建章节(传统模式)") async def create_single_chapter_from_outline( outline_id: str, @@ -2549,6 +2827,48 @@ async def create_single_chapter_from_outline( raise HTTPException(status_code=500, detail=f"创建章节失败: {str(e)}") +@router.post("/{outline_id}/expand-background", summary="后台展开单个大纲为多章") +async def expand_outline_to_chapters_background( + outline_id: str, + data: Dict[str, Any], + request: Request, + db: AsyncSession = Depends(get_db) +): + """创建后台任务展开单个大纲,任务完成后可在右下角后台任务面板查看结果。""" + result = await db.execute(select(Outline).where(Outline.id == outline_id)) + outline = result.scalar_one_or_none() + if not outline: + raise HTTPException(status_code=404, detail="大纲不存在") + + user_id = getattr(request.state, 'user_id', None) + await verify_project_access(outline.project_id, user_id, db) + + from app.services.background_task_service import background_task_service + + task_input = dict(data or {}) + task_input["outline_id"] = outline_id + task_input.setdefault("auto_create_chapters", True) + + task = await background_task_service.create_task( + user_id=user_id, + project_id=outline.project_id, + task_type="outline_expand", + task_input=task_input, + db=db + ) + + await background_task_service.spawn_background_task( + task.id, user_id, _run_outline_expansion_background, outline_id, task_input + ) + + return { + "task_id": task.id, + "task_type": "outline_expand", + "status": "pending", + "message": "大纲展开任务已创建,请通过后台任务面板查看进度" + } + + @router.post("/{outline_id}/expand-stream", summary="展开单个大纲为多章(SSE流式)") async def expand_outline_to_chapters_stream( outline_id: str, @@ -2901,6 +3221,41 @@ async def batch_expand_outlines_generator( yield await SSEResponse.send_error(f"批量展开失败: {str(e)}") +@router.post("/batch-expand-background", summary="后台批量展开大纲为多章") +async def batch_expand_outlines_background( + data: Dict[str, Any], + request: Request, + db: AsyncSession = Depends(get_db) +): + """创建后台任务批量展开大纲,任务完成后可在右下角后台任务面板查看结果。""" + user_id = getattr(request.state, 'user_id', None) + project = await verify_project_access(data.get("project_id"), user_id, db) + + from app.services.background_task_service import background_task_service + + task_input = dict(data or {}) + task_input.setdefault("auto_create_chapters", True) + + task = await background_task_service.create_task( + user_id=user_id, + project_id=project.id, + task_type="outline_batch_expand", + task_input=task_input, + db=db + ) + + await background_task_service.spawn_background_task( + task.id, user_id, _run_batch_outline_expansion_background, task_input + ) + + return { + "task_id": task.id, + "task_type": "outline_batch_expand", + "status": "pending", + "message": "批量大纲展开任务已创建,请通过后台任务面板查看进度" + } + + @router.post("/batch-expand-stream", summary="批量展开大纲为多章(SSE流式)") async def batch_expand_outlines_stream( data: Dict[str, Any], diff --git a/frontend/src/components/FloatingTaskPanel.tsx b/frontend/src/components/FloatingTaskPanel.tsx index ad8bcbf..3019ff4 100644 --- a/frontend/src/components/FloatingTaskPanel.tsx +++ b/frontend/src/components/FloatingTaskPanel.tsx @@ -151,6 +151,8 @@ export const FloatingTaskPanel: React.FC = ({ return '大纲续写'; case 'outline_expand': return '大纲展开'; + case 'outline_batch_expand': + return '批量大纲展开'; case 'chapter_generate': return '章节生成'; case 'chapter_batch': diff --git a/frontend/src/pages/Outline.tsx b/frontend/src/pages/Outline.tsx index d7286e3..eb815a6 100644 --- a/frontend/src/pages/Outline.tsx +++ b/frontend/src/pages/Outline.tsx @@ -5,11 +5,9 @@ import { useStore } from '../store'; import { eventBus } from '../store/eventBus'; import { getProjectTasks, type TaskStatus } from '../services/backgroundTaskService'; import { useOutlineSync } from '../store/hooks'; -import { SSEPostClient } from '../utils/sseClient'; -import { SSEProgressModal } from '../components/SSEProgressModal'; import { generateOutlineBackground } from '../services/backgroundTaskService'; import { outlineApi, chapterApi, projectApi, characterApi } from '../services/api'; -import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError, Character } from '../types'; +import type { ApiError, Character } from '../types'; // 大纲生成请求数据类型 interface OutlineGenerateRequestData { @@ -27,20 +25,6 @@ interface OutlineGenerateRequestData { provider?: string; } -// 跳过的大纲信息类型 -interface SkippedOutlineInfo { - outline_id: string; - outline_title: string; - reason: string; -} - -// 场景类型 -interface SceneInfo { - location: string; - characters: string[]; - purpose: string; -} - // 角色/组织条目类型(新格式) interface CharacterEntry { name: string; @@ -143,22 +127,6 @@ export default function Outline() { // ✅ 新增:记录场景区域的展开/折叠状态 const [scenesExpandStatus, setScenesExpandStatus] = useState>({}); - // 缓存批量展开的规划数据,避免重复AI调用 - const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState(null); - - // 批量展开预览的状态 - const [batchPreviewVisible, setBatchPreviewVisible] = useState(false); - const [batchPreviewData, setBatchPreviewData] = useState(null); - const [selectedOutlineIdx, setSelectedOutlineIdx] = useState(0); - const [selectedChapterIdx, setSelectedChapterIdx] = useState(0); - - // SSE进度状态 - const [sseProgress, setSSEProgress] = useState(0); - const [sseMessage, setSSEMessage] = useState(''); - const [sseModalVisible, setSSEModalVisible] = useState(false); - - - useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth <= 768); @@ -923,7 +891,7 @@ export default function Outline() { }); }; - // 展开单个大纲为多章 - 使用SSE显示进度 + // 展开单个大纲为多章 - 提交后台任务并在悬浮任务面板显示进度 const handleExpandOutline = async (outlineId: string, outlineTitle: string) => { try { setIsExpanding(true); @@ -1044,60 +1012,39 @@ export default function Outline() { ), - okText: '生成规划预览', + okText: '提交后台任务', cancelText: '取消', onOk: async () => { try { const values = await expansionForm.validateFields(); - // 关闭配置表单 Modal.destroyAll(); - - // 显示SSE进度Modal - setSSEProgress(0); - setSSEMessage('正在准备展开大纲...'); - setSSEModalVisible(true); setIsExpanding(true); - // 准备请求数据 const requestData = { ...values, - auto_create_chapters: false, // 第一步:仅生成规划 + auto_create_chapters: true, enable_scene_analysis: true }; - // 使用SSE客户端调用新的流式端点 - const apiUrl = `/api/outlines/${outlineId}/expand-stream`; - const client = new SSEPostClient(apiUrl, requestData, { - onProgress: (msg: string, progress: number) => { - setSSEMessage(msg); - setSSEProgress(progress); - }, - onResult: (data: OutlineExpansionResponse) => { - console.log('展开完成,结果:', data); - // 关闭SSE进度Modal - setSSEModalVisible(false); - // 显示规划预览 - showExpansionPreview(outlineId, data); - }, - onError: (error: string) => { - message.error(`展开失败: ${error}`); - setSSEModalVisible(false); - setIsExpanding(false); - }, - onComplete: () => { - setSSEModalVisible(false); - setIsExpanding(false); - } + const response = await fetch(`/api/outlines/${outlineId}/expand-background`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData), }); - // 开始连接 - client.connect(); + if (!response.ok) { + const err = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(err.detail || '创建大纲展开任务失败'); + } + + message.success('大纲展开任务已提交,可在右下角任务面板查看进度'); + eventBus.emit('background-task-created'); + setIsExpanding(false); } catch (error) { console.error('展开失败:', error); - message.error('展开失败'); - setSSEModalVisible(false); + message.error(error instanceof Error ? error.message : '展开失败'); setIsExpanding(false); } }, @@ -1392,134 +1339,7 @@ export default function Outline() { }); }; - // 显示展开规划预览,并提供确认创建章节的选项 - const showExpansionPreview = (outlineId: string, response: OutlineExpansionResponse) => { - // 缓存AI生成的规划数据 - const cachedPlans = response.chapter_plans; - - modalApi.confirm({ - title: ( - - - 展开规划预览 - - ), - width: 900, - centered: true, - okText: '确认并创建章节', - cancelText: '暂不创建', - content: ( -
-
- 策略: {response.expansion_strategy} - 章节数: {response.actual_chapter_count} - 预览模式(未创建章节) -
- ({ - key: idx.toString(), - label: ( - - {idx + 1}. {plan.title} - - ), - children: ( -
- - - - {plan.emotional_tone} - {plan.conflict_type} - 约{plan.estimated_words}字 - - - - - {plan.plot_summary} - - - - {plan.narrative_goal} - - - - - {plan.key_events.map((event, eventIdx) => ( -
• {event}
- ))} -
-
- - - - {plan.character_focus.map((char, charIdx) => ( - {char} - ))} - - - - {plan.scenes && plan.scenes.length > 0 && ( - - - {plan.scenes.map((scene, sceneIdx) => ( - -
地点:{scene.location}
-
角色:{scene.characters.join('、')}
-
目的:{scene.purpose}
-
- ))} -
-
- )} -
-
- ) - }))} - /> -
- ), - onOk: async () => { - // 第二步:用户确认后,直接使用缓存的规划创建章节(避免重复调用AI) - await handleConfirmCreateChapters(outlineId, cachedPlans); - }, - onCancel: () => { - message.info('已取消创建章节'); - } - }); - }; - - // 确认创建章节 - 使用缓存的规划数据,避免重复AI调用 - const handleConfirmCreateChapters = async ( - outlineId: string, - cachedPlans: ChapterPlanItem[] - ) => { - try { - setIsExpanding(true); - - // 使用新的API端点,直接传递缓存的规划数据 - const response = await outlineApi.createChaptersFromPlans(outlineId, cachedPlans); - - message.success( - `成功创建${response.chapters_created}个章节!`, - 3 - ); - - console.log('✅ 使用缓存的规划创建章节,避免了重复的AI调用'); - - // 刷新大纲和章节列表 - refreshOutlines(); - - } catch (error) { - console.error('创建章节失败:', error); - message.error('创建章节失败'); - } finally { - setIsExpanding(false); - } - }; - - // 批量展开所有大纲 - 使用SSE流式显示进度 + // 批量展开所有大纲 - 提交后台任务并在悬浮任务面板显示进度 const handleBatchExpandOutlines = () => { if (!currentProject?.id || outlines.length === 0) { message.warning('没有可展开的大纲'); @@ -1585,360 +1405,50 @@ export default function Outline() { ), - okText: '开始展开', + okText: '提交后台任务', cancelText: '取消', okButtonProps: { type: 'primary' }, onOk: async () => { try { const values = await batchExpansionForm.validateFields(); - // 关闭配置表单 Modal.destroyAll(); - - // 显示SSE进度Modal - setSSEProgress(0); - setSSEMessage('正在准备批量展开...'); - setSSEModalVisible(true); setIsExpanding(true); - // 准备请求数据 const requestData = { project_id: currentProject.id, ...values, - auto_create_chapters: false // 第一步:仅生成规划 + auto_create_chapters: true, + enable_scene_analysis: true }; - // 使用SSE客户端 - const apiUrl = `/api/outlines/batch-expand-stream`; - const client = new SSEPostClient(apiUrl, requestData, { - onProgress: (msg: string, progress: number) => { - setSSEMessage(msg); - setSSEProgress(progress); - }, - onResult: (data: BatchOutlineExpansionResponse) => { - console.log('批量展开完成,结果:', data); - // 缓存AI生成的规划数据 - setCachedBatchExpansionResponse(data); - setBatchPreviewData(data); - // 关闭SSE进度Modal - setSSEModalVisible(false); - // 重置选择状态 - setSelectedOutlineIdx(0); - setSelectedChapterIdx(0); - // 显示批量预览Modal - setBatchPreviewVisible(true); - }, - onError: (error: string) => { - message.error(`批量展开失败: ${error}`); - setSSEModalVisible(false); - setIsExpanding(false); - }, - onComplete: () => { - setSSEModalVisible(false); - setIsExpanding(false); - } + const response = await fetch('/api/outlines/batch-expand-background', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData), }); - // 开始连接 - client.connect(); + if (!response.ok) { + const err = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(err.detail || '创建批量展开任务失败'); + } + + message.success('批量展开任务已提交,可在右下角任务面板查看进度'); + eventBus.emit('background-task-created'); + setIsExpanding(false); } catch (error) { console.error('批量展开失败:', error); - message.error('批量展开失败'); - setSSEModalVisible(false); + message.error(error instanceof Error ? error.message : '批量展开失败'); setIsExpanding(false); } }, }); }; - // 渲染批量展开预览 Modal 内容 - const renderBatchPreviewContent = () => { - if (!batchPreviewData) return null; - - return ( -
- {/* 顶部统计信息 */} -
- 已处理: {batchPreviewData.total_outlines_expanded} 个大纲 - 总章节数: {batchPreviewData.expansion_results.reduce((sum: number, r: OutlineExpansionResponse) => sum + r.actual_chapter_count, 0)} - 预览模式(未创建章节) - {batchPreviewData.skipped_outlines && batchPreviewData.skipped_outlines.length > 0 && ( - 跳过: {batchPreviewData.skipped_outlines.length} 个大纲 - )} -
- - {/* 显示跳过的大纲信息 */} - {batchPreviewData.skipped_outlines && batchPreviewData.skipped_outlines.length > 0 && ( -
-
- ⚠️ 以下大纲已展开过,已自动跳过: -
- - {batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => ( -
- • {skipped.outline_title} {skipped.reason} -
- ))} -
-
- )} - - {/* 水平三栏布局 */} -
- {/* 左栏:大纲列表 */} -
-
大纲列表
- ( - { - setSelectedOutlineIdx(idx); - setSelectedChapterIdx(0); - }} - style={{ - cursor: 'pointer', - padding: '8px 12px', - background: selectedOutlineIdx === idx ? token.colorPrimaryBg : 'transparent', - borderRadius: token.borderRadius, - marginBottom: 4, - border: selectedOutlineIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent' - }} - > -
-
- {idx + 1}. {result.outline_title} -
- - {result.expansion_strategy} - {result.actual_chapter_count} 章 - -
-
- )} - /> -
- - {/* 中栏:章节列表 */} -
-
- 章节列表 ({batchPreviewData.expansion_results[selectedOutlineIdx]?.actual_chapter_count || 0} 章) -
- {batchPreviewData.expansion_results[selectedOutlineIdx] && ( - ( - setSelectedChapterIdx(idx)} - style={{ - cursor: 'pointer', - padding: '8px 12px', - background: selectedChapterIdx === idx ? token.colorPrimaryBg : 'transparent', - borderRadius: token.borderRadius, - marginBottom: 4, - border: selectedChapterIdx === idx ? `1px solid ${token.colorPrimary}` : '1px solid transparent' - }} - > -
-
- {idx + 1}. {plan.title} -
- - {plan.emotional_tone} - {plan.conflict_type} - 约{plan.estimated_words}字 - -
-
- )} - /> - )} -
- - {/* 右栏:章节详情 */} -
-
章节详情
- {batchPreviewData.expansion_results[selectedOutlineIdx]?.chapter_plans[selectedChapterIdx] ? ( - - - {batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].plot_summary} - - - - {batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].narrative_goal} - - - - - {(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].key_events as string[]).map((event: string, eventIdx: number) => ( -
• {event}
- ))} -
-
- - - - {(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].character_focus as string[]).map((char: string, charIdx: number) => ( - {char} - ))} - - - - {batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes && batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.length > 0 && ( - - - {batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => ( - -
地点:{scene.location}
-
角色:{scene.characters.join('、')}
-
目的:{scene.purpose}
-
- ))} -
-
- )} -
- ) : ( - - )} -
-
-
- ); - }; - - // 处理批量预览确认 - const handleBatchPreviewOk = async () => { - setBatchPreviewVisible(false); - await handleConfirmBatchCreateChapters(); - }; - - // 处理批量预览取消 - const handleBatchPreviewCancel = () => { - setBatchPreviewVisible(false); - message.info('已取消创建章节,规划已保存'); - }; - - - // 确认批量创建章节 - 使用缓存的规划数据 - const handleConfirmBatchCreateChapters = async () => { - try { - setIsExpanding(true); - - // 使用缓存的规划数据,避免重复调用AI - if (!cachedBatchExpansionResponse) { - message.error('规划数据丢失,请重新展开'); - return; - } - - console.log('✅ 使用缓存的批量规划数据创建章节,避免重复AI调用'); - - // 逐个大纲创建章节 - let totalCreated = 0; - const errors: string[] = []; - - for (const result of cachedBatchExpansionResponse.expansion_results) { - try { - // 使用create-chapters-from-plans接口,直接传递缓存的规划 - const response = await outlineApi.createChaptersFromPlans( - result.outline_id, - result.chapter_plans - ); - totalCreated += response.chapters_created; - } catch (error: unknown) { - const apiError = error as ApiError; - const err = error as Error; - const errorMsg = apiError.response?.data?.detail || err.message || '未知错误'; - errors.push(`${result.outline_title}: ${errorMsg}`); - console.error(`创建大纲 ${result.outline_title} 的章节失败:`, error); - } - } - - // 显示结果 - if (errors.length === 0) { - message.success( - `批量创建完成!共创建 ${totalCreated} 个章节`, - 3 - ); - } else { - message.warning( - `部分完成:成功创建 ${totalCreated} 个章节,${errors.length} 个失败`, - 5 - ); - console.error('失败详情:', errors); - } - - // 清除缓存 - setCachedBatchExpansionResponse(null); - - // 刷新列表 - refreshOutlines(); - - } catch (error) { - console.error('批量创建章节失败:', error); - message.error('批量创建章节失败'); - } finally { - setIsExpanding(false); - } - }; - - return ( <> - {/* 批量展开预览 Modal */} - - - 批量展开规划预览 - - } - open={batchPreviewVisible} - onOk={handleBatchPreviewOk} - onCancel={handleBatchPreviewCancel} - width={1200} - centered - okText="确认并批量创建章节" - cancelText="暂不创建" - okButtonProps={{ danger: true }} - > - {renderBatchPreviewContent()} - - {contextHolder} - {/* SSE进度Modal - 使用统一组件 */} - { - setSSEModalVisible(false); - setIsExpanding(false); - message.info('已取消操作'); - }} - />
{/* 固定头部 */} diff --git a/frontend/src/services/backgroundTaskService.ts b/frontend/src/services/backgroundTaskService.ts index c62680d..5a8674e 100644 --- a/frontend/src/services/backgroundTaskService.ts +++ b/frontend/src/services/backgroundTaskService.ts @@ -13,9 +13,10 @@ export interface TaskStatus { status_message: string | null; progress_details: { stage: string; - message: string; + message?: string; current_chars?: number; retry_count?: number; + queue_size?: number; } | null; error_message: string | null; task_result: Record | null;