perf(*): 创作工作台 - 优化首页按需加载与画布交互并修复历史迁移冲突
This commit is contained in:
@@ -42,6 +42,8 @@ type BackendGenerationTaskOptions = {
|
||||
metadata?: Record<string, unknown>;
|
||||
onTaskUpdate?: (task: GenerationTask) => void;
|
||||
onTextDelta?: (text: string) => void;
|
||||
streamText?: boolean;
|
||||
enableThinking?: boolean;
|
||||
localIdempotencyKey?: string;
|
||||
localResumeOnly?: boolean;
|
||||
clientOperationId?: string;
|
||||
@@ -90,6 +92,9 @@ export async function runBackendGenerationTask(
|
||||
signal,
|
||||
metadata,
|
||||
onTaskUpdate,
|
||||
onTextDelta,
|
||||
streamText,
|
||||
enableThinking,
|
||||
localIdempotencyKey,
|
||||
localResumeOnly,
|
||||
clientOperationId,
|
||||
@@ -104,14 +109,14 @@ export async function runBackendGenerationTask(
|
||||
await dependencies.ensureLocalDreaminaReady?.(signal);
|
||||
throwIfAborted(signal);
|
||||
return await runLocalDreaminaGeneration(
|
||||
{ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, mask, signal, metadata, onTaskUpdate, localIdempotencyKey, localResumeOnly, clientOperationId, retryOf, attemptGroupId },
|
||||
{ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, mask, signal, metadata, onTaskUpdate, onTextDelta, streamText, enableThinking, localIdempotencyKey, localResumeOnly, clientOperationId, retryOf, attemptGroupId },
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
assertBackendRuntimeConfigured(config, mode);
|
||||
const prepared = await prepareGenerationReferences({ referenceImages, referenceVideos, referenceAudios, mask });
|
||||
throwIfAborted(signal);
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate }, prepared, dependencies);
|
||||
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate, onTextDelta, streamText, enableThinking }, prepared, dependencies);
|
||||
}
|
||||
|
||||
// 分镜等后台生产流程只需要可靠提交任务;任务状态与产物由项目工作区轮询和
|
||||
@@ -439,6 +444,7 @@ async function createBackendGenerationTask(options: BackendGenerationTaskOptions
|
||||
config: backendProviderConfig(config, mode),
|
||||
capabilityOptions: logicalModelId ? logicalCapabilityOptions(config, mode) : undefined,
|
||||
textHistory: options.textHistory,
|
||||
...(mode === "text" ? { textOptions: { stream: options.streamText !== false, thinking: options.enableThinking === true } } : {}),
|
||||
referenceImages: prepared.referenceImages,
|
||||
referenceVideos: prepared.referenceVideos,
|
||||
referenceAudios: prepared.referenceAudios,
|
||||
|
||||
@@ -12,14 +12,16 @@ export class ApiError extends Error {
|
||||
readonly status?: number;
|
||||
readonly code?: number;
|
||||
readonly retryable: boolean;
|
||||
readonly retryAfterMs?: number;
|
||||
readonly cause?: unknown;
|
||||
|
||||
constructor(message: string, options: { status?: number; code?: number; retryable?: boolean; cause?: unknown } = {}) {
|
||||
constructor(message: string, options: { status?: number; code?: number; retryable?: boolean; retryAfterMs?: number; cause?: unknown } = {}) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = options.status;
|
||||
this.code = options.code;
|
||||
this.retryable = options.retryable ?? isRetryableStatus(options.status ?? options.code);
|
||||
this.retryAfterMs = options.retryAfterMs;
|
||||
this.cause = options.cause;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +30,7 @@ export class ApiError extends Error {
|
||||
export const apiBaseURL = import.meta.env.VITE_CANVAS_BACKEND_URL || "/api";
|
||||
export const apiClient = axios.create({ baseURL: apiBaseURL, withCredentials: true });
|
||||
|
||||
export async function request<T>(promise: Promise<{ data: BackendEnvelope<T>; status?: number }>) {
|
||||
export async function request<T>(promise: Promise<{ data: BackendEnvelope<T>; status?: number; headers?: unknown }>) {
|
||||
try {
|
||||
const response = await promise;
|
||||
if (response.data.code !== 0) {
|
||||
@@ -36,6 +38,7 @@ export async function request<T>(promise: Promise<{ data: BackendEnvelope<T>; st
|
||||
status: response.status,
|
||||
code: response.data.code,
|
||||
retryable: isRetryableStatus(response.status) || isRetryableStatus(response.data.code),
|
||||
retryAfterMs: retryAfterMilliseconds(response.headers),
|
||||
});
|
||||
}
|
||||
return response.data.data;
|
||||
@@ -50,6 +53,7 @@ export async function request<T>(promise: Promise<{ data: BackendEnvelope<T>; st
|
||||
status,
|
||||
code,
|
||||
retryable: isRetryableStatus(status) || isRetryableStatus(code),
|
||||
retryAfterMs: retryAfterMilliseconds(error.response?.headers),
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -61,6 +65,21 @@ function isRetryableStatus(status?: number) {
|
||||
return status === 408 || status === 425 || status === 429 || (status !== undefined && status >= 500 && status <= 599);
|
||||
}
|
||||
|
||||
function retryAfterMilliseconds(headers: unknown) {
|
||||
if (!headers || typeof headers !== "object") return undefined;
|
||||
const headerBag = headers as { get?: (name: string) => unknown; [key: string]: unknown };
|
||||
const rawValue = headerBag.get?.("retry-after") ?? headerBag["retry-after"] ?? headerBag["Retry-After"];
|
||||
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
||||
if (value === undefined || value === null) return undefined;
|
||||
const text = String(value).trim();
|
||||
if (!text) return undefined;
|
||||
const seconds = Number(text);
|
||||
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
|
||||
const retryAt = Date.parse(text);
|
||||
if (!Number.isFinite(retryAt)) return undefined;
|
||||
return Math.max(0, retryAt - Date.now());
|
||||
}
|
||||
|
||||
export function compactApiParams(params: ApiParams) {
|
||||
return Object.fromEntries(Object.entries(params).filter(([, value]) => value !== "" && value !== undefined && (!Array.isArray(value) || value.length > 0))) as ApiParams;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export type GenerationTask = {
|
||||
receiptRecorded?: boolean;
|
||||
previewUrl?: string;
|
||||
previewKind?: "image" | "video";
|
||||
previewPosterUrl?: string;
|
||||
inputJson?: string;
|
||||
resultJson?: string;
|
||||
resultState?: GenerationTaskResultState;
|
||||
|
||||
@@ -37,6 +37,18 @@ export type RemoteUserDataSnapshot = {
|
||||
projects: CanvasProject[];
|
||||
};
|
||||
|
||||
export type CanvasLibrarySummary = Pick<CanvasProject, "id" | "projectId" | "title" | "createdAt" | "updatedAt"> & {
|
||||
nodeCount: number;
|
||||
previewNodes: CanvasProject["nodes"];
|
||||
};
|
||||
|
||||
export function listRemoteCanvasProjectsPage(options: { page: number; pageSize: number; projectId?: string; query?: string; sort?: string; signal?: AbortSignal }) {
|
||||
return request<{ projects: CanvasLibrarySummary[]; page: number; pageSize: number; total: number; hasMore: boolean }>(api.get("/canvas-projects", {
|
||||
signal: options.signal,
|
||||
params: compactApiParams({ page: options.page, page_size: options.pageSize, project_id: options.projectId, q: options.query, sort: options.sort }),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getRemoteUserDataSnapshot() {
|
||||
return request<RemoteUserDataSnapshot>(api.get("/user-data/snapshot"));
|
||||
}
|
||||
@@ -85,6 +97,10 @@ export function getRemoteAsset(id: string) {
|
||||
return request<{ asset: Asset }>(api.get(`/assets/${encodeURIComponent(id)}`));
|
||||
}
|
||||
|
||||
export function getRemoteAssetsByIds(ids: string[]) {
|
||||
return request<{ assets: Asset[] }>(api.post("/assets/batch", { ids }));
|
||||
}
|
||||
|
||||
export function upsertRemoteAsset(asset: Asset) {
|
||||
return request<{ asset: RemoteUserDataSummary }>(api.put(`/assets/${encodeURIComponent(asset.id)}`, { asset }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user