From ae37d8386ed113fbb3c6cb28fd438074ccab1acd Mon Sep 17 00:00:00 2001 From: xiamuceer-j Date: Wed, 21 Jan 2026 14:51:17 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E5=86=85=E5=AE=B9=E5=88=86=E6=9E=90=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E5=90=8E=E5=89=8D=E7=AB=AF=E4=B8=8D=E5=88=B7=E6=96=B0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/chapters.py | 30 +++++++++++++++++++++++++++--- frontend/src/pages/Outline.tsx | 22 +++++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index 3c290c5..85b9434 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -397,7 +397,9 @@ async def delete_chapter( ) project = result.scalar_one_or_none() if project: - project.current_words = max(0, project.current_words - chapter.word_count) + # 处理 word_count 和 current_words 可能为 None 的情况 + chapter_word_count = chapter.word_count or 0 + project.current_words = max(0, (project.current_words or 0) - chapter_word_count) # 🗑️ 清理向量数据库中的记忆数据 try: @@ -897,14 +899,36 @@ async def analyze_chapter_background( ) logger.info(f"📋 后台分析 - 已获取{len(existing_foreshadows)}个已埋入伏笔用于匹配(含智能回收标记)") - # 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表) + # 定义重试回调函数,用于在重试时更新任务状态 + async def on_retry_callback(attempt: int, max_retries: int, wait_time: int, error_reason: str): + """重试时更新任务状态,让前端能感知到重试进度""" + try: + async with write_lock: + # 重新获取任务(确保获取最新状态) + task_result_retry = await db_session.execute( + select(AnalysisTask).where(AnalysisTask.id == task_id) + ) + task_retry = task_result_retry.scalar_one_or_none() + if task_retry: + # 更新任务状态,保持 running 但更新 started_at 以重置超时计时器 + task_retry.status = 'running' + task_retry.started_at = datetime.now() # 重置开始时间,防止超时检测误判 + task_retry.progress = 25 + attempt * 5 # 根据重试次数更新进度 + task_retry.error_message = f"正在重试({attempt}/{max_retries}):{error_reason[:100]}" + await db_session.commit() + logger.info(f"🔄 分析任务重试状态已更新: 尝试 {attempt}/{max_retries}, 等待 {wait_time}s, 原因: {error_reason[:50]}...") + except Exception as callback_error: + logger.warning(f"⚠️ 更新重试状态失败: {callback_error}") + + # 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表和重试回调) analyzer = PlotAnalyzer(ai_service) analysis_result = await analyzer.analyze_chapter( chapter_number=chapter.chapter_number, title=chapter.title, content=chapter.content, word_count=chapter.word_count or len(chapter.content), - existing_foreshadows=existing_foreshadows + existing_foreshadows=existing_foreshadows, + on_retry=on_retry_callback ) if not analysis_result: diff --git a/frontend/src/pages/Outline.tsx b/frontend/src/pages/Outline.tsx index 02aa119..eadad87 100644 --- a/frontend/src/pages/Outline.tsx +++ b/frontend/src/pages/Outline.tsx @@ -994,11 +994,11 @@ export default function Outline() { // 删除展开的章节内容(保留大纲) const handleDeleteExpandedChapters = async (outlineTitle: string, chapters: Array<{ id: string }>) => { try { - // 批量删除所有章节 - const deletePromises = chapters.map(chapter => - chapterApi.deleteChapter(chapter.id) - ); - await Promise.all(deletePromises); + // 使用顺序删除避免并发导致的字数计算竞态条件 + // 并发删除会导致多个请求同时读取项目字数并各自减去章节字数,造成计算错误 + for (const chapter of chapters) { + await chapterApi.deleteChapter(chapter.id); + } message.success(`已删除《${outlineTitle}》展开的所有 ${chapters.length} 个章节`); await refreshOutlines(); @@ -1007,6 +1007,18 @@ export default function Outline() { const updatedProject = await projectApi.getProject(currentProject.id); setCurrentProject(updatedProject); } + // 更新展开状态 + setOutlineExpandStatus(prev => { + const newStatus = { ...prev }; + // 找到被删除章节对应的大纲ID并更新其状态 + const outlineId = Object.keys(newStatus).find(id => + outlines.find(o => o.id === id && o.title === outlineTitle) + ); + if (outlineId) { + newStatus[outlineId] = false; + } + return newStatus; + }); } catch (error: unknown) { const apiError = error as ApiError; message.error(apiError.response?.data?.detail || '删除章节失败');