fix:优化章节分析并发问题

This commit is contained in:
xiamuceer
2025-11-05 00:11:27 +08:00
parent e62286eab1
commit 7e9781477b
5 changed files with 478 additions and 340 deletions
+246 -22
View File
@@ -1,10 +1,10 @@
import { useState, useEffect, useRef } from 'react';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined } from '@ant-design/icons';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { useChapterSync } from '../store/hooks';
import { projectApi, writingStyleApi } from '../services/api';
import type { Chapter, ChapterUpdate, ApiError, WritingStyle } from '../types';
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask } from '../types';
import { cardStyles } from '../components/CardStyles';
import ChapterAnalysis from '../components/ChapterAnalysis';
@@ -26,6 +26,9 @@ export default function Chapters() {
const [targetWordCount, setTargetWordCount] = useState<number>(3000);
const [analysisVisible, setAnalysisVisible] = useState(false);
const [analysisChapterId, setAnalysisChapterId] = useState<string | null>(null);
// 分析任务状态管理
const [analysisTasksMap, setAnalysisTasksMap] = useState<Record<string, AnalysisTask>>({});
const pollingIntervalsRef = useRef<Record<string, number>>({});
useEffect(() => {
const handleResize = () => {
@@ -46,10 +49,96 @@ export default function Chapters() {
if (currentProject?.id) {
refreshChapters();
loadWritingStyles();
loadAnalysisTasks();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]);
// 清理轮询定时器
useEffect(() => {
return () => {
Object.values(pollingIntervalsRef.current).forEach(interval => {
clearInterval(interval);
});
};
}, []);
// 加载所有章节的分析任务状态
const loadAnalysisTasks = async () => {
if (!chapters || chapters.length === 0) return;
const tasksMap: Record<string, AnalysisTask> = {};
for (const chapter of chapters) {
// 只查询有内容的章节
if (chapter.content && chapter.content.trim() !== '') {
try {
const response = await fetch(`/api/chapters/${chapter.id}/analysis/status`);
if (response.ok) {
const task: AnalysisTask = await response.json();
tasksMap[chapter.id] = task;
// 如果任务正在运行,启动轮询
if (task.status === 'pending' || task.status === 'running') {
startPollingTask(chapter.id);
}
}
} catch (error) {
// 404或其他错误表示没有分析任务,忽略
console.debug(`章节 ${chapter.id} 暂无分析任务`);
}
}
}
setAnalysisTasksMap(tasksMap);
};
// 启动单个章节的任务轮询
const startPollingTask = (chapterId: string) => {
// 如果已经在轮询,先清除
if (pollingIntervalsRef.current[chapterId]) {
clearInterval(pollingIntervalsRef.current[chapterId]);
}
const interval = window.setInterval(async () => {
try {
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
if (!response.ok) return;
const task: AnalysisTask = await response.json();
setAnalysisTasksMap(prev => ({
...prev,
[chapterId]: task
}));
// 任务完成或失败,停止轮询
if (task.status === 'completed' || task.status === 'failed') {
clearInterval(pollingIntervalsRef.current[chapterId]);
delete pollingIntervalsRef.current[chapterId];
if (task.status === 'completed') {
message.success(`章节分析完成`);
} else if (task.status === 'failed') {
message.error(`章节分析失败: ${task.error_message || '未知错误'}`);
}
}
} catch (error) {
console.error('轮询分析任务失败:', error);
}
}, 2000);
pollingIntervalsRef.current[chapterId] = interval;
// 5分钟超时
setTimeout(() => {
if (pollingIntervalsRef.current[chapterId]) {
clearInterval(pollingIntervalsRef.current[chapterId]);
delete pollingIntervalsRef.current[chapterId];
}
}, 300000);
};
const loadWritingStyles = async () => {
if (!currentProject?.id) return;
@@ -162,7 +251,7 @@ export default function Chapters() {
setIsContinuing(true);
setIsGenerating(true);
await generateChapterContentStream(editingId, (content) => {
const result = await generateChapterContentStream(editingId, (content) => {
editorForm.setFieldsValue({ content });
if (contentTextAreaRef.current) {
@@ -173,7 +262,24 @@ export default function Chapters() {
}
}, selectedStyleId, targetWordCount);
message.success('AI创作成功');
message.success('AI创作成功,正在分析章节内容...');
// 如果返回了分析任务ID,启动轮询
if (result?.analysis_task_id) {
const taskId = result.analysis_task_id;
setAnalysisTasksMap(prev => ({
...prev,
[editingId]: {
task_id: taskId,
chapter_id: editingId,
status: 'pending',
progress: 0
}
}));
// 启动轮询
startPollingTask(editingId);
}
} catch (error) {
const apiError = error as ApiError;
message.error('AI创作失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误'));
@@ -330,6 +436,46 @@ export default function Chapters() {
setAnalysisVisible(true);
};
// 渲染分析状态标签
const renderAnalysisStatus = (chapterId: string) => {
const task = analysisTasksMap[chapterId];
if (!task) {
return null;
}
switch (task.status) {
case 'pending':
return (
<Tag icon={<SyncOutlined spin />} color="processing">
</Tag>
);
case 'running':
return (
<Tag icon={<SyncOutlined spin />} color="processing">
{task.progress}%
</Tag>
);
case 'completed':
return (
<Tag icon={<CheckCircleOutlined />} color="success">
</Tag>
);
case 'failed':
return (
<Tooltip title={task.error_message}>
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
</Tooltip>
);
default:
return null;
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
@@ -385,15 +531,30 @@ export default function Chapters() {
>
</Button>,
<Tooltip title={!item.content || item.content.trim() === '' ? '请先生成章节内容' : ''}>
<Button
icon={<FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
disabled={!item.content || item.content.trim() === ''}
>
</Button>
</Tooltip>,
(() => {
const task = analysisTasksMap[item.id];
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' :
''
}
>
<Button
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
>
{isAnalyzing ? '分析中' : '查看分析'}
</Button>
</Tooltip>
);
})(),
<Button
type="text"
icon={<SettingOutlined />}
@@ -411,6 +572,7 @@ export default function Chapters() {
<span>{item.chapter_number}{item.title}</span>
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: '#52c41a' }} />
{renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
<Tag icon={<LockOutlined />} color="warning">
@@ -441,15 +603,30 @@ export default function Chapters() {
size="small"
title="编辑内容"
/>
<Tooltip title={!item.content || item.content.trim() === '' ? '请先生成章节内容' : '查看分析'}>
<Button
type="text"
icon={<FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
size="small"
disabled={!item.content || item.content.trim() === ''}
/>
</Tooltip>
{(() => {
const task = analysisTasksMap[item.id];
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
size="small"
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
/>
</Tooltip>
);
})()}
<Button
type="text"
icon={<SettingOutlined />}
@@ -686,6 +863,53 @@ export default function Chapters() {
visible={analysisVisible}
onClose={() => {
setAnalysisVisible(false);
// 延迟500ms后刷新该章节的分析状态,给后端足够时间完成数据库写入
if (analysisChapterId) {
const chapterIdToRefresh = analysisChapterId;
setTimeout(() => {
fetch(`/api/chapters/${chapterIdToRefresh}/analysis/status`)
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('获取状态失败');
})
.then((task: AnalysisTask) => {
setAnalysisTasksMap(prev => ({
...prev,
[chapterIdToRefresh]: task
}));
// 如果任务正在运行,启动轮询
if (task.status === 'pending' || task.status === 'running') {
startPollingTask(chapterIdToRefresh);
}
})
.catch(error => {
console.error('刷新分析状态失败:', error);
// 如果查询失败,再延迟尝试一次
setTimeout(() => {
fetch(`/api/chapters/${chapterIdToRefresh}/analysis/status`)
.then(response => response.ok ? response.json() : null)
.then((task: AnalysisTask | null) => {
if (task) {
setAnalysisTasksMap(prev => ({
...prev,
[chapterIdToRefresh]: task
}));
if (task.status === 'pending' || task.status === 'running') {
startPollingTask(chapterIdToRefresh);
}
}
})
.catch(err => console.error('第二次刷新失败:', err));
}, 1000);
});
}, 500);
}
setAnalysisChapterId(null);
}}
/>