* 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>
85 lines
3.9 KiB
TypeScript
85 lines
3.9 KiB
TypeScript
import { localForageStorageForScope } from "@/lib/localforage-storage";
|
|
import { getActiveUserScope } from "@/lib/user-scope";
|
|
|
|
export const CREATION_CONVERSATIONS_KEY = "creation-conversations-v1";
|
|
|
|
type PendingCreationMessage = {
|
|
id: string;
|
|
role: "user" | "assistant";
|
|
mode?: string;
|
|
status?: string;
|
|
taskIds?: string[];
|
|
};
|
|
|
|
export type StoredCreationConversation = {
|
|
id: string;
|
|
messages: PendingCreationMessage[];
|
|
};
|
|
|
|
export function updateCreationConversationSnapshot<T extends { id: string }>(conversations: T[], conversationId: string, updater: (conversation: T) => T) {
|
|
return conversations.map((conversation) => (conversation.id === conversationId ? updater(conversation) : conversation));
|
|
}
|
|
|
|
// 对话、生成任务与素材是独立持久状态;删除历史记录不能在这里级联清理任务或资源。
|
|
export function removeCreationConversationSnapshot<T extends { id: string }>(conversations: T[], conversationId: string) {
|
|
if (!conversationId) throw new Error("缺少要删除的创作对话 ID");
|
|
const next = conversations.filter((conversation) => conversation.id !== conversationId);
|
|
if (next.length === conversations.length) throw new Error("要删除的创作对话不存在");
|
|
return next;
|
|
}
|
|
|
|
function isRecoverableCreationMessage(message: PendingCreationMessage) {
|
|
if (message.role !== "assistant" || !message.taskIds?.length) return false;
|
|
if (message.mode === "text") return message.status === "streaming" || message.status === "pending";
|
|
// 媒体消息的前端等待可能先于后端结束(例如长视频),消息被判失败但任务其实已经成功。
|
|
// 失败态一并纳入恢复:任务确实失败时收敛结果不变,任务成功时把结果补回消息。
|
|
return message.status === "pending" || message.status === "error";
|
|
}
|
|
|
|
export function pendingCreationTaskKey(conversations: StoredCreationConversation[]) {
|
|
return conversations
|
|
.flatMap((conversation) => conversation.messages.flatMap((message) => (isRecoverableCreationMessage(message) ? [`${conversation.id}:${message.id}:${(message.taskIds || []).join(",")}`] : [])))
|
|
.join("|");
|
|
}
|
|
|
|
export function pendingCreationTaskIds(conversations: StoredCreationConversation[]) {
|
|
const taskIds = conversations.flatMap((conversation) =>
|
|
conversation.messages.flatMap((message) => {
|
|
if (!isRecoverableCreationMessage(message)) return [];
|
|
return message.taskIds || [];
|
|
}),
|
|
);
|
|
return Array.from(new Set(taskIds));
|
|
}
|
|
|
|
export async function loadCreationConversations<T extends StoredCreationConversation>() {
|
|
const storage = localForageStorageForScope(getActiveUserScope());
|
|
const value = await storage.getItem(CREATION_CONVERSATIONS_KEY);
|
|
if (!value) return null;
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(value);
|
|
} catch {
|
|
throw new Error("创作对话持久状态无效");
|
|
}
|
|
if (!Array.isArray(parsed)) throw new Error("创作对话持久状态无效");
|
|
return parsed as T[];
|
|
}
|
|
|
|
function persistableCreationConversations<T extends StoredCreationConversation>(conversations: T[]) {
|
|
return conversations.map((conversation) => ({
|
|
...conversation,
|
|
messages: conversation.messages.map((message) => {
|
|
const candidate = message as PendingCreationMessage & { resultUrls?: unknown; resultStorageKeys?: unknown };
|
|
if (!Array.isArray(candidate.resultStorageKeys) || candidate.resultStorageKeys.length === 0) return message;
|
|
const { resultUrls: _transientResultUrls, ...persistedMessage } = candidate;
|
|
return persistedMessage as typeof message;
|
|
}),
|
|
})) as T[];
|
|
}
|
|
|
|
export async function saveCreationConversations<T extends StoredCreationConversation>(conversations: T[]) {
|
|
const storage = localForageStorageForScope(getActiveUserScope());
|
|
await storage.setItem(CREATION_CONVERSATIONS_KEY, JSON.stringify(persistableCreationConversations(conversations)));
|
|
}
|