fix: improve generation task reliability
This commit is contained in:
+80
-101
@@ -3,6 +3,7 @@ import {
|
||||
buildAuthHeaders,
|
||||
isRecord,
|
||||
readJsonResponse,
|
||||
serverRequest,
|
||||
throwResponseError,
|
||||
} from "./serverConnection";
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
@@ -243,6 +244,10 @@ function emitImageRouteDebug(label: string, payload: Record<string, unknown>): v
|
||||
|
||||
let taskHistoryRouteMissing = false;
|
||||
|
||||
const TASK_SUBMIT_TIMEOUT_MS = 90_000;
|
||||
const TASK_STATUS_TIMEOUT_MS = 20_000;
|
||||
const NON_RETRYING_REQUEST = { maxRetries: 0 };
|
||||
|
||||
export const aiGenerationClient = {
|
||||
async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
|
||||
const requestUrl = buildApiUrl("ai/image");
|
||||
@@ -256,15 +261,13 @@ export const aiGenerationClient = {
|
||||
projectId: input.projectId,
|
||||
conversationId: input.conversationId,
|
||||
});
|
||||
const res = await fetch(requestUrl, {
|
||||
const payload = await serverRequest<ImageTaskCreateResponse>("ai/image", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Image generation request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Image generation request failed");
|
||||
}
|
||||
const payload = await readJsonResponse<ImageTaskCreateResponse>(res, "Image generation response failed");
|
||||
if (payload.providerDebug) {
|
||||
emitImageRouteDebug("[ai/image-provider-debug]", payload.providerDebug as Record<string, unknown>);
|
||||
}
|
||||
@@ -272,96 +275,83 @@ export const aiGenerationClient = {
|
||||
},
|
||||
|
||||
async createVideoTask(input: VideoGenInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/video"), {
|
||||
return serverRequest<{ taskId: string }>("ai/video", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Video generation request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Video generation request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Video generation response failed");
|
||||
},
|
||||
|
||||
async createVideoSuperResolveTask(input: VideoSuperResolveInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/video/super-resolve"), {
|
||||
return serverRequest<{ taskId: string }>("ai/video/super-resolve", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Video super-resolution request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Video super-resolution request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Video super-resolution response failed");
|
||||
},
|
||||
|
||||
async createEraseSubtitlesTask(input: EraseSubtitlesInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/video/erase-subtitles"), {
|
||||
return serverRequest<{ taskId: string }>("ai/video/erase-subtitles", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Subtitle removal request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Subtitle removal request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Subtitle removal response failed");
|
||||
},
|
||||
|
||||
async createVideoEditTask(input: VideoEditInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/video/edit"), {
|
||||
return serverRequest<{ taskId: string }>("ai/video/edit", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify({ ...input, model: input.model || "happyhorse-1.0-video-edit" }),
|
||||
body: { ...input, model: input.model || "happyhorse-1.0-video-edit" },
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Video edit request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Video edit request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Video edit response failed");
|
||||
},
|
||||
|
||||
async createImageSuperResolveTask(input: ImageSuperResolveInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/image/super-resolve"), {
|
||||
return serverRequest<{ taskId: string }>("ai/image/super-resolve", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Image super-resolution request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Image super-resolution request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Image super-resolution response failed");
|
||||
},
|
||||
|
||||
async createImageEditTask(input: ImageEditInput): Promise<{ taskId: string }> {
|
||||
const res = await fetch(buildApiUrl("ai/image/edit"), {
|
||||
return serverRequest<{ taskId: string }>("ai/image/edit", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Image edit request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Image edit request failed");
|
||||
}
|
||||
return readJsonResponse<{ taskId: string }>(res, "Image edit response failed");
|
||||
},
|
||||
|
||||
async cancelTask(taskId: string): Promise<void> {
|
||||
const res = await fetch(buildApiUrl(`ai/tasks/${taskId}/cancel`), {
|
||||
method: "PATCH",
|
||||
headers: buildAuthHeaders(),
|
||||
});
|
||||
if (!res.ok && res.status !== 404) {
|
||||
await throwResponseError(res, "Task cancel failed");
|
||||
try {
|
||||
await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
|
||||
method: "PATCH",
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Task cancel failed",
|
||||
});
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) return;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
|
||||
const res = await fetch(buildApiUrl(`ai/tasks/${taskId}`), {
|
||||
method: "GET",
|
||||
headers: buildAuthHeaders(),
|
||||
return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
|
||||
timeoutMs: TASK_STATUS_TIMEOUT_MS,
|
||||
fallbackMessage: "Task status request failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Task status request failed");
|
||||
}
|
||||
return readJsonResponse<AiTaskStatus>(res, "Task status response failed");
|
||||
},
|
||||
|
||||
async downloadTaskResult(taskId: string): Promise<{ blob: Blob; filename?: string; contentType?: string }> {
|
||||
@@ -387,49 +377,41 @@ export const aiGenerationClient = {
|
||||
if (params?.status) search.set("status", params.status);
|
||||
if (params?.type) search.set("type", params.type);
|
||||
if (params?.projectId) search.set("projectId", params.projectId);
|
||||
const res = await fetch(buildApiUrl(`ai/tasks${search.toString() ? `?${search}` : ""}`), {
|
||||
method: "GET",
|
||||
headers: buildAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
try {
|
||||
await throwResponseError(res, "Task history request failed");
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
taskHistoryRouteMissing = true;
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
try {
|
||||
const payload = await serverRequest<unknown>(`ai/tasks${search.toString() ? `?${search}` : ""}`, {
|
||||
fallbackMessage: "Task history request failed",
|
||||
});
|
||||
return extractTaskList(payload).map(toPreviewTask);
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
taskHistoryRouteMissing = true;
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const payload = await readJsonResponse<unknown>(res, "Task history response failed");
|
||||
return extractTaskList(payload).map(toPreviewTask);
|
||||
},
|
||||
|
||||
async bindTaskToConversation(taskId: string, conversationId: number): Promise<void> {
|
||||
const res = await fetch(buildApiUrl(`ai/tasks/${taskId}/conversation`), {
|
||||
method: "PATCH",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify({ conversationId }),
|
||||
});
|
||||
if (res.status === 404) {
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Task conversation binding failed");
|
||||
try {
|
||||
await serverRequest<void>(`ai/tasks/${taskId}/conversation`, {
|
||||
method: "PATCH",
|
||||
body: { conversationId },
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Task conversation binding failed",
|
||||
});
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) return;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async uploadAsset(input: UploadAssetInput): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
|
||||
const res = await fetch(buildApiUrl("oss/upload"), {
|
||||
return serverRequest<{ url: string; signedUrl?: string; ossKey?: string }>("oss/upload", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Asset upload failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Asset upload failed");
|
||||
}
|
||||
return readJsonResponse<{ url: string; ossKey?: string }>(res, "Asset upload response failed");
|
||||
},
|
||||
|
||||
async uploadAssetBinary(blob: Blob, options?: { name?: string; mimeType?: string; scope?: string }): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
|
||||
@@ -451,15 +433,12 @@ export const aiGenerationClient = {
|
||||
},
|
||||
|
||||
async uploadAssetByUrl(input: UploadAssetByUrlInput): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
|
||||
const res = await fetch(buildApiUrl("oss/upload-by-url"), {
|
||||
return serverRequest<{ url: string; signedUrl?: string; ossKey?: string }>("oss/upload-by-url", {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify(input),
|
||||
body: input,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Asset upload by URL failed",
|
||||
});
|
||||
if (!res.ok) {
|
||||
await throwResponseError(res, "Asset upload by URL failed");
|
||||
}
|
||||
return readJsonResponse<{ url: string; ossKey?: string }>(res, "Asset upload by URL response failed");
|
||||
},
|
||||
|
||||
subscribeTaskStatus(
|
||||
|
||||
Reference in New Issue
Block a user