fix(media): 创作结果恢复与等待上限 - 长视频不再误判失败 (#665)

* fix(media): 创作结果恢复 - 结果定位符缺失时回退到任务结果

首页创作生成的图片/视频在本地素材索引尚未同步时(物化结果幂等命中、换标签页、
素材同步延迟)会被收敛成“任务已结束,但生成结果暂时无法读取”:结果定位符只查
本地素材 store,查不到就返回空,于是结果既不挂载也不显示,而服务端其实已经成功。

- generationTaskMaterializedStorageKeys 在本地查不到素材时回退到结果自带的
  providerArtifactRef(resource:<id>),展示地址仍由渲染层按需续签;
- 携带过期签名的历史地址不再直接交给 <img>/<video>,避免首帧 403 空白。

* fix(media): 前端等待上限对齐后端超时 - 长视频不再被前端误判失败

视频任务的前端等待上限写死 32 分钟,而后端视频超时默认 60 分钟(现网配到 120 分钟)、
上游 30 秒档常态 35-45 分钟。结果:任务在后端正常跑并最终成功,前端先报
“任务执行超时,请稍后重试”,消息被标成失败;而失败态消息不会被恢复流程接管
(isRecoverableCreationMessage 只认 pending),用户看到的就是“失败”且视频永远不显示。

- taskWaitTimeoutMs:视频 32 → 125 分钟、分镜 13 → 25 分钟,都不小于后端同类超时;
- isRecoverableCreationMessage:媒体消息的失败态也纳入恢复,任务确实失败时收敛结果不变,
  任务其实成功时把结果补回消息。

---------

Co-authored-by: xiaomuclaw <282836183+xiaomuclaw@users.noreply.github.com>
This commit is contained in:
xiaomuclaw
2026-10-06 09:13:43 +08:00
committed by GitHub
parent 8bc93a415b
commit 3daf9db53a
4 changed files with 31 additions and 9 deletions

View File

@@ -196,6 +196,19 @@ export function CreationUserMessage({ item, shotNumber, onEditUserMessage }: { i
);
}
/**
* 历史 URL 只是短期 hint:签名已过期的地址不再直接交给 <img>/<video>,
* 否则首帧就会出现 403(画面空白)。带 storageKey 的结果随后由续签流程补上新的地址。
*/
function usableResultUrls(urls: string[]) {
const now = Date.now();
return urls.filter((url) => {
if (!url) return false;
const expires = /[?&]expires=(\d+)/.exec(url);
return !expires || Number(expires[1]) * 1000 > now;
});
}
export function MediaResult({
item,
onRetryFailure,
@@ -214,7 +227,7 @@ export function MediaResult({
const assets = useAssetStore((state) => state.assets);
const storedResultUrls = item.resultUrls || [];
const resultStorageKeys = item.resultStorageKeys?.length ? item.resultStorageKeys : creationResultStorageKeys(assets, { messageId: item.id, taskIds: item.taskIds || [], resultUrls: storedResultUrls });
const [resolvedResultUrls, setResolvedResultUrls] = useState(storedResultUrls);
const [resolvedResultUrls, setResolvedResultUrls] = useState(() => usableResultUrls(storedResultUrls));
useEffect(() => {
let active = true;
@@ -242,13 +255,13 @@ export function MediaResult({
}
};
if (!resultStorageKeys.length) {
setResolvedResultUrls(storedResultUrls);
setResolvedResultUrls(usableResultUrls(storedResultUrls));
return () => {
active = false;
};
}
// 历史记录只把 URL 当作短期 hint;真正恢复依赖稳定 storageKey,展示 URL 由访问缓存按需续签。
setResolvedResultUrls(storedResultUrls);
setResolvedResultUrls(usableResultUrls(storedResultUrls));
const resolver = item.mode === "video" ? resolveMediaUrl : resolveImageUrl;
void Promise.all(
resultStorageKeys.map(async (storageKey, index) => {
@@ -268,7 +281,7 @@ export function MediaResult({
};
}, [item.id, item.mode, resultStorageKeys.join("|"), storedResultUrls.join("|")]);
const alignedResultUrls = resultStorageKeys.length ? resolvedResultUrls : storedResultUrls;
const alignedResultUrls = resultStorageKeys.length ? resolvedResultUrls : usableResultUrls(storedResultUrls);
const displayResultUrls = alignedResultUrls.filter(Boolean);
const resultAssetIds = alignedResultUrls.length || resultStorageKeys.length ? creationResultAssetIds(assets, { messageId: item.id, taskIds: item.taskIds || [], resultUrls: alignedResultUrls, resultStorageKeys }) : [];
const expectedResultCount = resultStorageKeys.length || alignedResultUrls.length;