From 22398c14830e5cb9eaf048cdf6b906ed654fd66b Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 27 May 2026 14:47:45 +0800 Subject: [PATCH] auto-save 2026-05-27 14:47 (~2) --- .memory/worklog.json | 14 ++--- web/canvas-app/src/hooks/useApi.js | 82 ++++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 91efd23..dae9ae3 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,12 +1,5 @@ { "entries": [ - { - "files_changed": 3, - "hash": "1d0a77b", - "message": "fix: prefer width-first workbench scaling", - "ts": "2026-05-20T18:58:31+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "4a22ca0", @@ -3192,6 +3185,13 @@ "message": "auto-save 2026-05-27 14:36 (~3)", "hash": "5046e23", "files_changed": 3 + }, + { + "ts": "2026-05-27T14:42:16+08:00", + "type": "commit", + "message": "auto-save 2026-05-27 14:42 (~2)", + "hash": "a699899", + "files_changed": 2 } ] } diff --git a/web/canvas-app/src/hooks/useApi.js b/web/canvas-app/src/hooks/useApi.js index 52c3942..e9480f0 100644 --- a/web/canvas-app/src/hooks/useApi.js +++ b/web/canvas-app/src/hooks/useApi.js @@ -79,6 +79,36 @@ const newestGeneratedVideo = (job) => ( [...(job.generated_videos || [])].sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] ) +const parseVideoTaskId = (pollTaskId) => { + const match = /^skg:([^:]+):([^:]+)$/.exec(String(pollTaskId || '')) + if (!match) { + const err = new Error('未知视频任务类型') + err.terminal = true + throw err + } + return { jobId: match[1], videoId: match[2] } +} + +export const readVideoTask = async (pollTaskId) => { + const { jobId, videoId } = parseVideoTaskId(pollTaskId) + const job = await requestJson(`/jobs/${jobId}`, { method: 'GET' }) + const item = (job.generated_videos || []).find(v => v.id === videoId) + if (!item) { + const err = new Error('视频任务不存在') + err.terminal = true + throw err + } + return { + jobId, + videoId, + job, + video: item, + status: item.status, + progress: item.progress || 0, + url: item.status === 'completed' ? toAssetUrl(item.url || `/jobs/${jobId}/storyboard-videos/${videoId}.mp4`) : '' + } +} + const normalizeVideoSize = (value) => { const raw = String(value || '').trim().toLowerCase() const map = { @@ -256,32 +286,44 @@ export const useVideoGeneration = () => { } const pollVideoTask = async (pollTaskId, onProgress = () => {}) => { - const match = /^skg:([^:]+):([^:]+)$/.exec(String(pollTaskId || '')) - if (!match) throw new Error('未知视频任务类型') - const [, jobId, videoId] = match - const maxAttempts = 180 + const maxAttempts = 720 const interval = 5000 + let transientFailures = 0 for (let i = 0; i < maxAttempts; i += 1) { - const job = await requestJson(`/jobs/${jobId}`, { method: 'GET' }) - const item = (job.generated_videos || []).find(v => v.id === videoId) - if (!item) throw new Error('视频任务不存在') - const percentage = item.progress || Math.min(Math.round((i / maxAttempts) * 100), 98) - onProgress(i + 1, percentage) - progress.attempt = i + 1 - progress.percentage = percentage - if (item.status === 'completed') { - const result = { ...item, url: toAssetUrl(item.url || `/jobs/${jobId}/storyboard-videos/${videoId}.mp4`) } - video.value = result - setSuccess() - return result - } - if (item.status === 'failed') { - throw new Error(item.error || '视频生成失败') + try { + const snapshot = await readVideoTask(pollTaskId) + const item = snapshot.video + transientFailures = 0 + const percentage = item.progress || Math.min(Math.round((i / maxAttempts) * 100), 98) + onProgress(i + 1, percentage) + progress.attempt = i + 1 + progress.percentage = percentage + if (item.status === 'completed') { + const result = { ...item, url: snapshot.url } + video.value = result + setSuccess() + return result + } + if (item.status === 'failed') { + const err = new Error(item.error || '视频生成失败') + err.terminal = true + throw err + } + } catch (err) { + if (err.terminal) throw err + transientFailures += 1 + if (transientFailures >= 24) { + const retryable = new Error(err.message || '视频状态同步暂时中断') + retryable.retryable = true + throw retryable + } } await new Promise(resolve => setTimeout(resolve, interval)) } - throw new Error('视频生成超时') + const retryable = new Error('视频仍在生成,已交给后台同步') + retryable.retryable = true + throw retryable } const generate = async (params) => {