fix: kill the "已开始重试处理项目" success-toast loop

The previous fix stopped the failure-toast spam but exposed a success-toast
loop. handleRetryProject calls loadProjects, which sets loading=true and swaps
the project list for HomePage's "正在加载项目列表" spinner — unmounting every
ProjectCard. That reset the useRef auto-start guard, so on remount auto-start
fired again → onRetry → handleRetryProject → loadProjects → unmount → ...
an infinite loop, one "已开始重试处理项目" toast per cycle (and the flickering
spinner in the report).

Three cuts so the loop can't form:
1. Auto-start guard moved from useRef to a module-level Set<projectId>, which
   survives the remount. Project ids are unique per import → once per session.
2. Silent auto-start no longer calls onRetry — it must never drive the parent's
   toast/reload path. Only user-clicked retries notify the parent.
3. handleRetryProject no longer re-issues retryProcessing (the card already
   sent the request); it just toasts once and refreshes.

Frontend typecheck clean; production build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
周小舟
2026-05-30 23:48:10 +08:00
parent 678ed430c8
commit 50be1de3ae
2 changed files with 27 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect } from 'react'
import { Card, Tag, Button, Space, Typography, Popconfirm, message, Tooltip } from 'antd'
import { PlayCircleOutlined, DeleteOutlined, DownloadOutlined, ReloadOutlined, LoadingOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
@@ -49,6 +49,13 @@ if (typeof document !== 'undefined') {
const { Text } = Typography
// Tracks which project ids have already had a best-effort auto-start, surviving
// component remounts (the list briefly unmounts while HomePage shows its
// loading spinner). A useRef would reset on every remount and let auto-start
// fire again, which created an infinite onRetry→loadProjects→remount loop.
// Project ids are unique per import, so once-per-session is exactly right.
const autoStartedProjectIds = new Set<string>()
interface ProjectCardProps {
project: Project
onDelete: (id: string) => void
@@ -244,20 +251,18 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
// 导致 effect 反复触发 → 对一个还没下载完的 B站项目疯狂 POST /process返回
// 400 "Video file not found")→ 满屏「重试失败」。下载完成后后端会自动启动
// 流水线,所以这里只需做一次「尽力而为」的启动即可。
const autoStartAttempted = useRef(false)
useEffect(() => {
autoStartAttempted.current = false
}, [project.id])
useEffect(() => {
if (
project.status === 'pending' &&
!isDownloading &&
!autoStartAttempted.current
!autoStartedProjectIds.has(project.id)
) {
autoStartAttempted.current = true
// Best-effort, one-shot. Uploads (file already present) start processing;
// B站 imports whose download isn't done yet return 400 here — that's fine,
// the backend auto-starts the pipeline when the download completes.
autoStartedProjectIds.add(project.id)
// Best-effort, one-shot per project. Uploads (file already present) start
// processing; B站 imports whose download isn't done yet return 400 here —
// that's fine, the backend auto-starts the pipeline when the download
// completes. Silent + no onRetry so this never drives the parent's
// toast/reload path.
handleRetry({ silent: true })
}
}, [project.status, project.id, isDownloading])
@@ -282,8 +287,10 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
} else {
await projectApi.retryProcessing(project.id)
}
// 移除重复的toast显示让父组件统一处理
if (onRetry) {
// 让父组件统一处理 toast / 刷新。但「静默自动启动」绝不能触发父组件,
// 否则会走 handleRetryProject → loadProjects → 列表重挂载 → 再次自动启动
// 的死循环。只有用户手动点重试才通知父组件。
if (onRetry && !opts?.silent) {
onRetry(project.id)
}
} catch (error) {

View File

@@ -89,23 +89,16 @@ const HomePage: React.FC = () => {
}
}
const handleRetryProject = async (projectId: string) => {
// 由 ProjectCard 在「用户手动点重试」且重试请求已成功后调用。
// ProjectCard.handleRetry 已经发过 start/retryProcessing 请求,这里只负责
// 提示 + 刷新列表,绝不能再发一次重试请求(会和卡片自身的请求叠加,并制造
// loadProjects→重挂载→自动启动 的循环)。
const handleRetryProject = async () => {
message.success('已开始重试处理项目')
try {
// 查找项目状态
const project = projects.find(p => p.id === projectId)
if (!project) {
message.error('项目不存在')
return
}
// 统一使用retryProcessing API它会自动处理视频文件不存在的情况
await projectApi.retryProcessing(projectId)
message.success('已开始重试处理项目')
await loadProjects()
} catch (error) {
message.error('重试失败,请稍后再试')
console.error('Retry project error:', error)
console.error('Refresh after retry error:', error)
}
}
@@ -365,7 +358,7 @@ const HomePage: React.FC = () => {
<ProjectCard
project={project}
onDelete={handleDeleteProject}
onRetry={() => handleRetryProject(project.id)}
onRetry={() => handleRetryProject()}
onClick={() => handleProjectCardClick(project)}
/>
</div>