fix:优化章节分析并发问题
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Progress, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button } from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
BulbOutlined,
|
||||
FireOutlined,
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
BulbOutlined,
|
||||
FireOutlined,
|
||||
HeartOutlined,
|
||||
TeamOutlined,
|
||||
TrophyOutlined,
|
||||
@@ -30,6 +30,11 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
if (visible && chapterId) {
|
||||
fetchAnalysisStatus();
|
||||
}
|
||||
|
||||
// 清理函数:组件卸载或关闭时清除轮询
|
||||
return () => {
|
||||
// 清除可能存在的轮询
|
||||
};
|
||||
}, [visible, chapterId]);
|
||||
|
||||
const fetchAnalysisStatus = async () => {
|
||||
@@ -117,16 +122,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
throw new Error(errorData.detail || '触发分析失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setTask({
|
||||
task_id: result.task_id,
|
||||
chapter_id: chapterId,
|
||||
status: 'pending',
|
||||
progress: 0
|
||||
});
|
||||
|
||||
// 开始轮询
|
||||
startPolling();
|
||||
// 触发成功后立即关闭Modal,让父组件的状态管理接管
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
@@ -134,6 +131,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const renderStatusIcon = () => {
|
||||
if (!task) return null;
|
||||
|
||||
@@ -480,7 +478,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
<Button key="close" onClick={onClose}>
|
||||
关闭
|
||||
</Button>,
|
||||
!task && (
|
||||
!task && !loading && (
|
||||
<Button
|
||||
key="analyze"
|
||||
type="primary"
|
||||
@@ -491,19 +489,30 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
开始分析
|
||||
</Button>
|
||||
),
|
||||
task && (task.status === 'failed' || task.status === 'completed') && (
|
||||
task && (task.status === 'failed') && (
|
||||
<Button
|
||||
key="reanalyze"
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={triggerAnalysis}
|
||||
loading={loading}
|
||||
danger={task.status === 'failed'}
|
||||
danger
|
||||
>
|
||||
重新分析
|
||||
</Button>
|
||||
),
|
||||
task && task.status === 'completed' && (
|
||||
<Button
|
||||
key="reanalyze"
|
||||
type="default"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={triggerAnalysis}
|
||||
loading={loading}
|
||||
>
|
||||
重新分析
|
||||
</Button>
|
||||
)
|
||||
]}
|
||||
].filter(Boolean)}
|
||||
>
|
||||
{loading && !task && (
|
||||
<div style={{ textAlign: 'center', padding: '48px' }}>
|
||||
@@ -518,11 +527,6 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
description={error}
|
||||
type="error"
|
||||
showIcon
|
||||
action={
|
||||
<Button size="small" danger onClick={triggerAnalysis}>
|
||||
开始分析
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message, Progress } from 'antd';
|
||||
import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message } from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
MenuOutlined,
|
||||
ReloadOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -72,8 +71,6 @@ const ChapterAnalysis: React.FC = () => {
|
||||
const [showAnnotations, setShowAnnotations] = useState(true);
|
||||
const [activeAnnotationId, setActiveAnnotationId] = useState<string | undefined>();
|
||||
const [sidebarVisible, setSidebarVisible] = useState(false);
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
const [analysisProgress, setAnalysisProgress] = useState(0);
|
||||
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
|
||||
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
|
||||
|
||||
@@ -165,105 +162,6 @@ const ChapterAnalysis: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReanalyze = async () => {
|
||||
if (!selectedChapter) return;
|
||||
|
||||
let pollInterval: number | null = null;
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
try {
|
||||
setAnalyzing(true);
|
||||
setAnalysisProgress(0);
|
||||
message.loading({ content: '开始分析章节...', key: 'analyze', duration: 0 });
|
||||
|
||||
// 触发分析任务
|
||||
const triggerRes = await api.post(`/chapters/${selectedChapter.id}/analyze`);
|
||||
const triggerData = triggerRes.data || triggerRes;
|
||||
const taskId = triggerData.task_id;
|
||||
|
||||
console.log('分析任务已创建:', taskId);
|
||||
|
||||
// 开始轮询状态
|
||||
let pollCount = 0;
|
||||
const maxPolls = 60; // 最多轮询60次(2分钟)
|
||||
|
||||
pollInterval = setInterval(async () => {
|
||||
pollCount++;
|
||||
|
||||
if (pollCount > maxPolls) {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
setAnalyzing(false);
|
||||
message.warning({ content: '分析超时,请稍后刷新页面查看结果', key: 'analyze' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusRes = await api.get(`/chapters/${selectedChapter.id}/analysis/status`);
|
||||
const responseData = statusRes.data || statusRes;
|
||||
|
||||
if (!responseData) {
|
||||
console.warn(`第${pollCount}次轮询:响应数据为空`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { status, progress, error_message } = responseData;
|
||||
console.log(`第${pollCount}次轮询:status=${status}, progress=${progress}`);
|
||||
|
||||
setAnalysisProgress(progress || 0);
|
||||
|
||||
if (status === 'completed') {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
setAnalyzing(false);
|
||||
message.success({ content: '分析完成!', key: 'analyze' });
|
||||
|
||||
// 重新加载标注数据
|
||||
try {
|
||||
const annotationsRes = await api.get(`/chapters/${selectedChapter.id}/annotations`);
|
||||
setAnnotationsData(annotationsRes.data || annotationsRes);
|
||||
} catch (annotErr) {
|
||||
console.error('加载标注数据失败:', annotErr);
|
||||
message.warning('分析完成,但加载标注数据失败,请刷新页面');
|
||||
}
|
||||
} else if (status === 'failed') {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
setAnalyzing(false);
|
||||
message.error({
|
||||
content: `分析失败:${error_message || '未知错误'}`,
|
||||
key: 'analyze'
|
||||
});
|
||||
}
|
||||
// pending 或 running 状态继续轮询
|
||||
} catch (pollErr) {
|
||||
console.error(`第${pollCount}次轮询失败:`, pollErr);
|
||||
// 轮询错误不中断,继续下一次轮询
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// 设置总超时(2分钟)
|
||||
timeoutId = setTimeout(() => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
setAnalyzing(false);
|
||||
message.warning({ content: '分析超时,请稍后刷新页面查看结果', key: 'analyze' });
|
||||
}, 120000);
|
||||
|
||||
} catch (err: any) {
|
||||
// 清理定时器
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
setAnalyzing(false);
|
||||
const errorMsg = err.response?.data?.detail || err.message || '触发分析失败';
|
||||
console.error('触发分析失败:', errorMsg, err);
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: 'analyze'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasAnnotations = annotationsData && annotationsData.annotations.length > 0;
|
||||
|
||||
if (loading) {
|
||||
@@ -352,15 +250,6 @@ const ChapterAnalysis: React.FC = () => {
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReanalyze}
|
||||
loading={analyzing}
|
||||
disabled={analyzing || !selectedChapter?.content || selectedChapter.content.trim() === ''}
|
||||
title={!selectedChapter?.content || selectedChapter.content.trim() === '' ? '章节内容为空,无法分析' : ''}
|
||||
>
|
||||
{analyzing ? '分析中...' : '重新分析'}
|
||||
</Button>
|
||||
{hasAnnotations && (
|
||||
<>
|
||||
<Switch
|
||||
@@ -382,16 +271,7 @@ const ChapterAnalysis: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{analyzing && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Progress percent={analysisProgress} size="small" status="active" />
|
||||
<span style={{ fontSize: 12, color: '#666', marginLeft: 8 }}>
|
||||
正在分析章节...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!analyzing && hasAnnotations && annotationsData && (
|
||||
{hasAnnotations && annotationsData && (
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
|
||||
共有 {annotationsData.summary.total_annotations} 个标注:
|
||||
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
|
||||
|
||||
+246
-22
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -331,6 +331,7 @@ export function useChapterSync() {
|
||||
|
||||
let buffer = '';
|
||||
let fullContent = '';
|
||||
let analysisTaskId: string | undefined;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
@@ -363,9 +364,13 @@ export function useChapterSync() {
|
||||
} else if (message.type === 'error') {
|
||||
throw new Error(message.error || '生成失败');
|
||||
} else if (message.type === 'done') {
|
||||
// 生成完成,保存分析任务ID
|
||||
analysisTaskId = message.analysis_task_id;
|
||||
// 生成完成,刷新章节数据
|
||||
await refreshChapters();
|
||||
return { content: fullContent, word_count: message.word_count };
|
||||
} else if (message.type === 'analysis_queued') {
|
||||
// 分析任务已加入队列
|
||||
analysisTaskId = message.task_id;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -374,7 +379,10 @@ export function useChapterSync() {
|
||||
}
|
||||
}
|
||||
|
||||
return { content: fullContent };
|
||||
return {
|
||||
content: fullContent,
|
||||
analysis_task_id: analysisTaskId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('AI流式生成章节内容失败:', error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user