Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,21 @@ export default function Sidebar() {
}).catch(() => {});
};
fetchUnread();
const timer = setInterval(fetchUnread, 60000);
return () => clearInterval(timer);
let timer: ReturnType<typeof setInterval> | null = setInterval(fetchUnread, 60000);
// visibility 暂停:后台标签不再每 60s 拉文件夹统计
const onVisibility = () => {
if (document.hidden) {
if (timer) { clearInterval(timer); timer = null; }
} else {
fetchUnread();
timer = setInterval(fetchUnread, 60000);
}
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
if (timer) clearInterval(timer);
document.removeEventListener("visibilitychange", onVisibility);
};
}, []);
const {
metas,
Expand Down
74 changes: 53 additions & 21 deletions frontend/src/components/ToolPanel/TranslationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

import { paperApi, translateApi, tasksApi, type BilingualSegment } from '@/services/api';
import { useToast } from '@/contexts/ToastContext';

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
Expand Down Expand Up @@ -66,6 +67,8 @@ export function TranslationPanel({ selectedText, paperId, paperArxivId, paperPdf

const leftScrollRef = useRef<HTMLDivElement>(null);
const rightScrollRef = useRef<HTMLDivElement>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null); // 翻译轮询定时器(卸载时清理)
const { toast } = useToast();

const [containerWidth, setContainerWidth] = useState(400);
const scale = useMemo(() => {
Expand Down Expand Up @@ -121,30 +124,52 @@ export function TranslationPanel({ selectedText, paperId, paperArxivId, paperPdf
setTranslating(true);
try {
const { task_id } = await translateApi.startBilingualPdf(paperId, mode);
const poll = setInterval(async () => {
// 后端 /tasks/{id} 返回 global_tracker.to_dict():含 finished/success
const status = (await tasksApi.getStatus(task_id)) as unknown as {
finished: boolean;
success: boolean;
error: string | null;
};
if (!status.finished) return;
clearInterval(poll);
if (status.success) {
const result = (await tasksApi.getResult(task_id)) as {
segments?: BilingualSegment[];
pdf_url?: string;
const pollStart = Date.now();
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟超时兜底
const MAX_ERRORS = 10; // 连续查询失败上限
let consecutiveErrors = 0;
pollRef.current = setInterval(async () => {
// 超时兜底:任务挂起时不再无限轮询(此前 1800 次/小时泄漏)
if (Date.now() - pollStart > POLL_TIMEOUT_MS) {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
setTranslating(false);
toast('error', '翻译超时,请稍后重试');
return;
}
try {
// 后端 /tasks/{id} 返回 global_tracker.to_dict():含 finished/success
const status = (await tasksApi.getStatus(task_id)) as unknown as {
finished: boolean;
success: boolean;
error: string | null;
};
if (mode === 'fast' && result.segments) {
setSegments(result.segments);
} else if (mode === 'layout' && result.pdf_url) {
setLayoutPdfUrl(result.pdf_url);
consecutiveErrors = 0;
if (!status.finished) return;
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
if (status.success) {
const result = (await tasksApi.getResult(task_id)) as {
segments?: BilingualSegment[];
pdf_url?: string;
};
if (mode === 'fast' && result.segments) {
setSegments(result.segments);
} else if (mode === 'layout' && result.pdf_url) {
setLayoutPdfUrl(result.pdf_url);
}
setViewMode('bilingual');
} else {
toast('error', status.error || '翻译失败,请稍后重试');
}
setTranslating(false);
} catch {
// 连续错误上限:网络持续异常时中断,不再静默空转
consecutiveErrors += 1;
if (consecutiveErrors >= MAX_ERRORS) {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
setTranslating(false);
toast('error', '翻译状态查询持续失败,请稍后重试');
}
setViewMode('bilingual');
} else {
alert(status.error || '翻译失败,请稍后重试');
}
setTranslating(false);
}, 2000);
} catch (err) {
console.error('Failed to start translation:', err);
Expand All @@ -170,6 +195,13 @@ export function TranslationPanel({ selectedText, paperId, paperArxivId, paperPdf
return () => observer.disconnect();
}, []);

// 卸载时清理翻译轮询定时器,防泄漏
useEffect(() => {
return () => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
};
}, []);

const pdfUrl = useMemo(() => {
const token = localStorage.getItem('auth_token') || '';
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '';
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/graph/CitationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,16 @@ function RichCitationListView({ data }: { data: CitationDetail }) {
source_paper_title: data.paper_title,
entries,
});
const pollStart = Date.now();
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟超时兜底
pollRef.current = setInterval(async () => {
if (Date.now() - pollStart > POLL_TIMEOUT_MS) {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = undefined; }
setImportTask(null);
setShowModal(false);
toast("warning", "导入超时,请稍后在论文列表确认结果");
return;
}
try {
const status = await ingestApi.importStatus(task_id);
setImportTask(status);
Expand All @@ -501,7 +510,7 @@ function RichCitationListView({ data }: { data: CitationDetail }) {
setShowModal(false);
toast("error", "导入进度查询失败,请稍后在论文列表确认结果");
}
}, 1000);
}, 2000);
} catch (err) {
toast("error", `导入启动失败: ${String(err)}`);
setShowModal(false);
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/contexts/GlobalTaskContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,23 @@ export function GlobalTaskProvider({ children }: { children: React.ReactNode })
useEffect(() => {
fetchTasks();
intervalRef.current = setInterval(fetchTasks, POLL_IDLE);

// visibility 暂停:标签页不可见时停止轮询(此前后台标签 360-1800 次/小时冗余请求)
const onVisibility = () => {
if (document.hidden) {
if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
} else {
// 恢复可见:立即拉一次 + 按当前状态重启轮询
fetchTasks();
const interval = hasRunningRef.current ? POLL_FAST : POLL_IDLE;
intervalRef.current = setInterval(fetchTasks, interval);
}
};
document.addEventListener("visibilitychange", onVisibility);

return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
document.removeEventListener("visibilitychange", onVisibility);
};
}, [fetchTasks]);

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* PaperMind - API 服务层
* @author Color2333
*/
import { retryAsync } from "@/lib/errorHandler";
import type {
SystemStatus,
Topic,
Expand Down Expand Up @@ -192,8 +193,10 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
return resp.json();
}

// GET 幂等,套 retryAsync(3 次指数退避,仅 NETWORK/SERVER/timeout 重试);
// POST/PATCH/PUT/DELETE 不套,避免重复写入
function get<T>(path: string, opts?: { signal?: AbortSignal }) {
return request<T>(path, { signal: opts?.signal });
return retryAsync(() => request<T>(path, { signal: opts?.signal }));
}

function post<T>(path: string, body?: unknown, opts?: { signal?: AbortSignal }) {
Expand Down