From d434c1655d172ddec8ab000ac8f07d4efecda7ff Mon Sep 17 00:00:00 2001 From: liquid Date: Sun, 8 Mar 2026 08:49:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(drawnix):=20=E6=B7=BB=E5=8A=A0=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=88=B7=E6=96=B0=E5=90=8E=E6=81=A2=E5=A4=8D=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E4=BB=BB=E5=8A=A1=E8=BD=AE=E8=AF=A2=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加对页面刷新中断的视频生成任务的恢复机制。当用户刷新页面时,系统会通过 requestIdleCallback 或 setTimeout 自动恢复所有处于 PROCESSING 状态且拥有 remoteId 的视频任务,继续轮询其生成状态直至完成或失败,避免用户手动重新触发任务。 --- packages/drawnix/src/drawnix.tsx | 15 ++++ .../media-executor/fallback-executor.ts | 79 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/packages/drawnix/src/drawnix.tsx b/packages/drawnix/src/drawnix.tsx index a8288f57..aa47dc76 100644 --- a/packages/drawnix/src/drawnix.tsx +++ b/packages/drawnix/src/drawnix.tsx @@ -326,6 +326,21 @@ export const Drawnix: React.FC = ({ } }, [board]); + // Initialize fallback media executor to resume pending tasks + useEffect(() => { + const resumeTasks = () => { + import('./services/media-executor/fallback-executor').then(({ fallbackMediaExecutor }) => { + fallbackMediaExecutor.resumePendingTasks(); + }); + }; + + if ('requestIdleCallback' in window) { + (window as Window).requestIdleCallback(resumeTasks, { timeout: 5000 }); + } else { + setTimeout(resumeTasks, 1000); + } + }, []); + // 监听 API 认证错误事件,自动打开设置对话框 useEffect(() => { const handleApiAuthError = (event: Event) => { diff --git a/packages/drawnix/src/services/media-executor/fallback-executor.ts b/packages/drawnix/src/services/media-executor/fallback-executor.ts index e07cd402..dcef9feb 100644 --- a/packages/drawnix/src/services/media-executor/fallback-executor.ts +++ b/packages/drawnix/src/services/media-executor/fallback-executor.ts @@ -15,7 +15,9 @@ import type { GeminiConfig, VideoAPIConfig, } from './types'; +import { Task, TaskStatus } from '../../types/task.types'; import { taskStorageWriter } from './task-storage-writer'; +import { taskStorageReader } from '../task-storage-reader'; import { geminiSettings } from '../../utils/settings-manager'; import { startLLMApiLog, @@ -433,6 +435,9 @@ export class FallbackMediaExecutor implements IMediaExecutor { httpStatus: 200, }); + // 保存 remoteId,用于页面刷新后恢复轮询 + await taskStorageWriter.updateRemoteId(taskId, videoId); + options?.onProgress?.({ progress: 10, phase: 'polling' }); // 轮询等待视频完成 @@ -651,6 +656,80 @@ export class FallbackMediaExecutor implements IMediaExecutor { } } + /** + * 恢复未完成的任务(例如页面刷新导致中断的任务) + * 仅恢复有 remoteId 且状态为 processing 的任务 + */ + async resumePendingTasks(): Promise { + try { + const pendingTasks = await taskStorageReader.getAllTasks({ status: TaskStatus.PROCESSING }); + + // 筛选出有 remoteId 的视频任务 + const videoTasks = pendingTasks.filter(t => + t.type === 'video' && t.remoteId && t.status === TaskStatus.PROCESSING + ); + + if (videoTasks.length === 0) { + return; + } + + console.log(`[FallbackMediaExecutor] Resuming ${videoTasks.length} pending video tasks...`); + + // 并行恢复 + await Promise.all(videoTasks.map(task => this.resumeVideoTask(task))); + } catch (error) { + console.error('[FallbackMediaExecutor] Failed to resume pending tasks:', error); + } + } + + /** + * 恢复单个视频任务的轮询 + */ + private async resumeVideoTask(task: Task): Promise { + const config = this.getConfig(); + const videoId = task.remoteId!; + + try { + console.log(`[FallbackMediaExecutor] Resuming video task: ${task.id} (remoteId: ${videoId})`); + + // 重新开始轮询 + const result = await pollVideoStatus( + videoId, + config.videoConfig, + (progress) => { + // 这里的 progress 是 0-1 + // 视频生成中,polling 阶段通常对应 10%-90% + const mappedProgress = 10 + progress * 80; + // taskStorageWriter.updateStatus 会写入 storage,触发 observer + taskStorageWriter.updateStatus(task.id, TaskStatus.PROCESSING).catch(() => {}); + taskStorageWriter.updateProgress(task.id, mappedProgress).catch(() => {}); + } + ); + + // 缓存远程 URL + const cachedVidUrl = await cacheRemoteUrl(result.url, task.id, 'video', 'mp4'); + + const duration = task.params.duration as string | undefined; + + // 完成任务 + await taskStorageWriter.completeTask(task.id, { + url: cachedVidUrl, + format: 'mp4', + size: 0, + duration: duration ? parseInt(duration, 10) : undefined, + }); + + console.log(`[FallbackMediaExecutor] Resumed video task completed: ${task.id}`); + } catch (error: any) { + console.error(`[FallbackMediaExecutor] Failed to resume task ${task.id}:`, error); + + await taskStorageWriter.failTask(task.id, { + code: error.code || 'RESUME_FAILED', + message: error.message || 'Failed to resume task' + }); + } + } + /** * 规范化 baseUrl,移除尾部 / 或 /v1,便于拼接 /v1/videos */