fix: improve generation task reliability
This commit is contained in:
@@ -32,6 +32,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { assetClient, type ServerAssetItem } from "../../api/assetClient";
|
||||
import { communityClient } from "../../api/communityClient";
|
||||
import { modelCapabilitiesClient } from "../../api/modelCapabilitiesClient";
|
||||
import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway";
|
||||
import WorkspacePageShell from "../../components/WorkspacePageShell";
|
||||
import type {
|
||||
@@ -118,7 +119,7 @@ import {
|
||||
defaultVideoModel,
|
||||
image4kCapableModels,
|
||||
imageFocusRatioOptions,
|
||||
imageModelOptions,
|
||||
imageModelOptions as fallbackCanvasImageModelOptions,
|
||||
imageRatioOptions,
|
||||
textModelOptions,
|
||||
videoDurationOptions,
|
||||
@@ -354,6 +355,8 @@ function CanvasPage({
|
||||
const [projectNameEditing, setProjectNameEditing] = useState(false);
|
||||
const [videoNodeMenu, setVideoNodeMenu] = useState<{ left: number; top: number; nodeId: string } | null>(null);
|
||||
const [videoNodes, setVideoNodes] = useState<CanvasVideoNode[]>([]);
|
||||
const [canvasImageModelOptions, setCanvasImageModelOptions] = useState<CanvasOption[]>(fallbackCanvasImageModelOptions);
|
||||
const [canvasVideoModelOptions, setCanvasVideoModelOptions] = useState<CanvasOption[]>(canvasEnterpriseVideoModelOptions);
|
||||
const [selectedNode, setSelectedNode] = useState<CanvasSelectedNode | null>(null);
|
||||
const [selectedNodes, setSelectedNodes] = useState<CanvasSelectedNode[]>([]);
|
||||
const [selectionContextMenu, setSelectionContextMenu] = useState<CanvasFloatingMenuPosition | null>(null);
|
||||
@@ -458,9 +461,39 @@ function CanvasPage({
|
||||
callbacksRef: dragCallbacksRef,
|
||||
suppressNextPaneClickRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
setCanvasImageModelOptions(fallbackCanvasImageModelOptions);
|
||||
setCanvasVideoModelOptions(canvasEnterpriseVideoModelOptions);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
modelCapabilitiesClient
|
||||
.get()
|
||||
.then((capabilities) => {
|
||||
if (cancelled) return;
|
||||
setCanvasImageModelOptions(capabilities.imageModels.length ? capabilities.imageModels : fallbackCanvasImageModelOptions);
|
||||
setCanvasVideoModelOptions(capabilities.videoModels.length ? capabilities.videoModels : canvasEnterpriseVideoModelOptions);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return;
|
||||
setCanvasImageModelOptions(fallbackCanvasImageModelOptions);
|
||||
setCanvasVideoModelOptions(canvasEnterpriseVideoModelOptions);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const visibleImageModelOptions = useMemo(
|
||||
() => filterImageModelOptionsForSession(imageModelOptions, session),
|
||||
[session],
|
||||
() => filterImageModelOptionsForSession(canvasImageModelOptions, session),
|
||||
[canvasImageModelOptions, session],
|
||||
);
|
||||
const fallbackVisibleImageModel = visibleImageModelOptions[0]?.value || defaultImageModel;
|
||||
const resolveVisibleImageModel = useCallback(
|
||||
@@ -5044,7 +5077,7 @@ function CanvasPage({
|
||||
ariaLabel="选择视频模型"
|
||||
className="canvas-select-chip--model studio-canvas-composer-chip"
|
||||
value={toHappyHorseDisplayModel(videoNode.model || defaultVideoModel)}
|
||||
options={canvasEnterpriseVideoModelOptions}
|
||||
options={canvasVideoModelOptions}
|
||||
open={canvasSelectMenu === `${videoNode.id}:video-model`}
|
||||
onToggle={() =>
|
||||
setCanvasSelectMenu((current) =>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type AdVideoUserConfig,
|
||||
} from "../../api/adVideoPlanClient";
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { serverRequest } from "../../api/serverConnection";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
|
||||
@@ -430,15 +431,6 @@ export interface VideoHistoryListResponse {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
import { getStoredToken } from "../../api/serverConnection";
|
||||
|
||||
const API_BASE = "/api/ai/ecommerce/video-history";
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = getStoredToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
export async function buildDurableVideoHistoryPayload(payload: SaveVideoHistoryPayload): Promise<SaveVideoHistoryPayload> {
|
||||
const uploadAssetByUrl = payload.uploadAssetByUrl;
|
||||
const scenes = await Promise.all(
|
||||
@@ -486,13 +478,12 @@ export async function buildDurableVideoHistoryPayload(payload: SaveVideoHistoryP
|
||||
|
||||
export async function saveVideoHistory(payload: SaveVideoHistoryPayload): Promise<{ id: number; createdAt: string }> {
|
||||
const { uploadAssetByUrl: _uploadAssetByUrl, ...historyPayload } = await buildDurableVideoHistoryPayload(payload);
|
||||
const res = await fetch(API_BASE, {
|
||||
return serverRequest<{ id: number; createdAt: string }>("ai/ecommerce/video-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
||||
body: JSON.stringify(historyPayload),
|
||||
body: historyPayload,
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "Failed to save video history",
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to save video history");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function removeTemporaryHistoryUrls(item: VideoHistoryItem): VideoHistoryItem {
|
||||
@@ -511,12 +502,10 @@ export async function fetchVideoHistory(
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
): Promise<VideoHistoryListResponse> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}?limit=${limit}&offset=${offset}`,
|
||||
{ headers: getAuthHeaders() },
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to fetch video history");
|
||||
const history = (await res.json()) as VideoHistoryListResponse;
|
||||
const search = new URLSearchParams({ limit: String(limit), offset: String(offset) });
|
||||
const history = await serverRequest<VideoHistoryListResponse>(`ai/ecommerce/video-history?${search}`, {
|
||||
fallbackMessage: "Failed to fetch video history",
|
||||
});
|
||||
return {
|
||||
...history,
|
||||
items: history.items.map(removeTemporaryHistoryUrls),
|
||||
@@ -524,9 +513,9 @@ export async function fetchVideoHistory(
|
||||
}
|
||||
|
||||
export async function deleteVideoHistory(id: number): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/${id}`, {
|
||||
await serverRequest<void>(`ai/ecommerce/video-history/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "Failed to delete video history",
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to delete video history");
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
||||
abortRef.current = false;
|
||||
taskIdRef.current = saved.taskId;
|
||||
void waitForTask(saved.taskId, {
|
||||
kind: "image",
|
||||
onProgress: (e) => {
|
||||
setStatus(`${e.status} / ${e.progress}%`);
|
||||
if (e.status === "completed" && e.resultUrl) {
|
||||
@@ -446,6 +447,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
||||
|
||||
const pollTaskUntilDone = useCallback(async (taskId: string): Promise<string | null> => {
|
||||
return waitForTask(taskId, {
|
||||
kind: "image",
|
||||
abortRef,
|
||||
onProgress: (e) => setGenerationProgress(e.progress || 0),
|
||||
});
|
||||
@@ -559,7 +561,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
||||
referenceUrls: refUrls,
|
||||
});
|
||||
taskIdRef.current = taskId;
|
||||
saveToolTaskState("imagewb", { taskId, status: "running", progress: 0 });
|
||||
saveToolTaskState("imagewb", { taskId, status: "running", progress: 0 });
|
||||
|
||||
const tempUrl = await pollTaskUntilDone(taskId);
|
||||
if (tempUrl) {
|
||||
|
||||
@@ -369,7 +369,7 @@ function WorkbenchPage({
|
||||
.get()
|
||||
.then((capabilities) => {
|
||||
if (cancelled) return;
|
||||
const nextVideoModels = VIDEO_MODEL_OPTIONS;
|
||||
const nextVideoModels = capabilities.videoModels.length ? capabilities.videoModels : VIDEO_MODEL_OPTIONS;
|
||||
|
||||
applyImageModels(capabilities.imageModels);
|
||||
setVideoModelOptions(nextVideoModels);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Persists task state to localStorage so in-progress tasks survive page switches.
|
||||
*/
|
||||
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
|
||||
const KEEPALIVE_PREFIX = "omniai:tool-task:";
|
||||
|
||||
interface ToolTaskKeepalive {
|
||||
@@ -59,38 +61,19 @@ export function clearToolTaskState(key: string): void {
|
||||
try { window.localStorage.removeItem(KEEPALIVE_PREFIX + key); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const TASK_POLL_INTERVAL = 3000;
|
||||
const TASK_POLL_TIMEOUT = 30 * 60 * 1000;
|
||||
|
||||
export async function pollTaskUntilDone(
|
||||
taskId: string,
|
||||
onProgress?: (progress: number) => void,
|
||||
abortRef?: { current: boolean },
|
||||
kind: "image" | "video" = "video",
|
||||
): Promise<string | null> {
|
||||
const startTime = Date.now();
|
||||
const { aiGenerationClient } = await import("../../api/aiGenerationClient");
|
||||
|
||||
while (true) {
|
||||
if (abortRef?.current) return null;
|
||||
if (Date.now() - startTime > TASK_POLL_TIMEOUT) return null;
|
||||
|
||||
try {
|
||||
const task = await aiGenerationClient.getTaskStatus(taskId);
|
||||
if (!task) return null;
|
||||
|
||||
const progress = Math.min(99, task.progress || 0);
|
||||
onProgress?.(progress);
|
||||
|
||||
if (task.status === "completed") {
|
||||
return task.resultUrl || null;
|
||||
}
|
||||
if (task.status === "failed" || task.status === "cancelled") {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// retry on next poll
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, TASK_POLL_INTERVAL));
|
||||
try {
|
||||
return await waitForTask(taskId, {
|
||||
kind,
|
||||
abortRef,
|
||||
onProgress: (event) => onProgress?.(Math.min(99, Number(event.progress || 0))),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user