feat(drawnix): 添加页面刷新后恢复视频任务轮询的功能

添加对页面刷新中断的视频生成任务的恢复机制。当用户刷新页面时,系统会通过 requestIdleCallback 或 setTimeout 自动恢复所有处于 PROCESSING 状态且拥有 remoteId 的视频任务,继续轮询其生成状态直至完成或失败,避免用户手动重新触发任务。
This commit is contained in:
liquid committed 2026-03-08 08:49:06 +08:00
1 parent d6f37f0bb0
commit d434c1655d
2 files changed
+94

No files matched your search

+15
View File
@@ -326,6 +326,21 @@ export const Drawnix: React.FC<DrawnixProps> = ({
}
}, [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) => {
@@ -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<void> {
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<void> {
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
*/