299 lines
9.8 KiB
TypeScript
299 lines
9.8 KiB
TypeScript
import { generationErrorMessage } from "@/lib/generation-error";
|
|
import { apiClient, request, type BackendEnvelope } from "@/services/api/request";
|
|
|
|
export type { BackendEnvelope } from "@/services/api/request";
|
|
|
|
export type TaskStatus = "queued" | "running" | "succeeded" | "failed" | "cancelled";
|
|
export type TaskBillingStatus = "reserved" | "running" | "settled" | "refunded" | "uncertain";
|
|
export type ProviderCancelStatus = "requested" | "confirmed" | "uncertain";
|
|
export type AgentSessionStatus = "active" | "completed" | "failed";
|
|
|
|
export type GenerationTask = {
|
|
id: string;
|
|
sessionId?: string;
|
|
projectId?: string;
|
|
type: string;
|
|
status: TaskStatus;
|
|
progress?: number;
|
|
stage?: string;
|
|
prompt: string;
|
|
operation?: string;
|
|
provider?: string;
|
|
model?: string;
|
|
providerRequestId?: string;
|
|
providerCancelStatus?: ProviderCancelStatus;
|
|
providerCancelError?: string;
|
|
providerCancelAttempts?: number;
|
|
providerCancelRequestedAt?: string;
|
|
providerCancelledAt?: string;
|
|
errorCode?: string;
|
|
previewUrl?: string;
|
|
previewKind?: "image" | "video";
|
|
inputJson?: string;
|
|
resultJson?: string;
|
|
textDraft?: string;
|
|
error?: string;
|
|
attempts: number;
|
|
startedAt?: string;
|
|
completedAt?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
billing?: {
|
|
amountMicrocredits: number;
|
|
status: TaskBillingStatus;
|
|
};
|
|
clientContext?: {
|
|
conversationId: string;
|
|
messageId: string;
|
|
batchIndex?: number;
|
|
batchCount?: number;
|
|
};
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
};
|
|
|
|
export type ProviderTaskQueryResult = {
|
|
task: GenerationTask;
|
|
providerStatus: string;
|
|
recovered: boolean;
|
|
billingSettled: boolean;
|
|
};
|
|
|
|
export type TaskTextDelta = {
|
|
id: string;
|
|
taskId: string;
|
|
sequence: number;
|
|
content: string;
|
|
byteCount: number;
|
|
createdAt: string;
|
|
expiresAt: string;
|
|
};
|
|
|
|
export type TaskTextReplay = {
|
|
deltas: TaskTextDelta[];
|
|
textDraft?: string;
|
|
finalText?: string;
|
|
complete: boolean;
|
|
};
|
|
|
|
export type AgentSession = {
|
|
id: string;
|
|
projectId?: string;
|
|
status: AgentSessionStatus;
|
|
prompt: string;
|
|
canvasSnapshotJson?: string;
|
|
canvasOpsJson?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type AgentMessage = {
|
|
id: string;
|
|
sessionId: string;
|
|
role: "user" | "assistant" | "system" | "tool" | string;
|
|
content: string;
|
|
payload?: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type TaskResult = {
|
|
id: string;
|
|
taskId: string;
|
|
sessionId?: string;
|
|
kind: string;
|
|
url?: string;
|
|
payload?: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type SessionFile = {
|
|
id: string;
|
|
sessionId: string;
|
|
fileName: string;
|
|
mimeType: string;
|
|
size: number;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type TaskLog = {
|
|
id: string;
|
|
taskId: string;
|
|
level: "info" | "warn" | "error" | string;
|
|
message: string;
|
|
payload?: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type AgentSessionDetail = {
|
|
session: AgentSession;
|
|
messages: AgentMessage[];
|
|
tasks: GenerationTask[];
|
|
results: TaskResult[];
|
|
};
|
|
|
|
export type CreateSessionInput = {
|
|
projectId?: string;
|
|
prompt: string;
|
|
canvasSnapshot?: Record<string, unknown>;
|
|
references?: string[];
|
|
projectStyle?: { presetId: string; title: string; prompt: string };
|
|
characters?: Array<{ assetId: string; versionId: string; name: string; definition: Record<string, unknown> }>;
|
|
config?: Record<string, unknown>;
|
|
};
|
|
|
|
export type CreateTaskInput = {
|
|
sessionId?: string;
|
|
projectId?: string;
|
|
type?: string;
|
|
operation?: string;
|
|
prompt: string;
|
|
provider?: string;
|
|
model?: string;
|
|
input?: Record<string, unknown>;
|
|
};
|
|
|
|
const api = apiClient;
|
|
|
|
export function createAgentSession(input: CreateSessionInput) {
|
|
return request<AgentSessionDetail>(api.post("/sessions", input)).then((detail) => {
|
|
detail.tasks.forEach((task) => notifyCanvasTaskCreated(task));
|
|
return detail;
|
|
});
|
|
}
|
|
|
|
export function queryAgentSession(id: string) {
|
|
return request<AgentSessionDetail>(api.get(`/sessions/${encodeURIComponent(id)}`));
|
|
}
|
|
|
|
export function agentSessionFailureMessage(detail: AgentSessionDetail, fallback = "后端影视 Agent 会话失败") {
|
|
for (let index = detail.tasks.length - 1; index >= 0; index -= 1) {
|
|
const task = detail.tasks[index];
|
|
if ((task.status === "failed" || task.status === "cancelled") && task.error?.trim()) return generationErrorMessage(task.error.trim());
|
|
}
|
|
for (let index = detail.messages.length - 1; index >= 0; index -= 1) {
|
|
const message = detail.messages[index];
|
|
if (message.role === "assistant" && message.content.trim()) return generationErrorMessage(message.content.trim());
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export function downloadSessionResults(id: string) {
|
|
return request<TaskResult[]>(api.get(`/sessions/${encodeURIComponent(id)}/results`));
|
|
}
|
|
|
|
export function uploadAgentFile(sessionId: string, file: File) {
|
|
const formData = new FormData();
|
|
formData.append("sessionId", sessionId);
|
|
formData.append("file", file);
|
|
return request<SessionFile>(api.post("/files", formData));
|
|
}
|
|
|
|
export function createGenerationTask(input: CreateTaskInput) {
|
|
return request<GenerationTask>(api.post("/tasks", input)).then((task) => {
|
|
notifyCanvasTaskCreated(task);
|
|
// 创建任务时积分已被预占,不能等任务结束后才刷新可用余额。
|
|
window.dispatchEvent(new CustomEvent("wallet:updated"));
|
|
return task;
|
|
});
|
|
}
|
|
|
|
export function listGenerationTasks(limit = 30, options?: { projectId?: string; activeOnly?: boolean }) {
|
|
return request<GenerationTask[]>(api.get("/tasks", { params: { limit, projectId: options?.projectId, activeOnly: options?.activeOnly || undefined } }));
|
|
}
|
|
|
|
export function queryGenerationTask(id: string, options?: { signal?: AbortSignal }) {
|
|
return request<GenerationTask>(api.get(`/tasks/${encodeURIComponent(id)}`, { signal: options?.signal }));
|
|
}
|
|
|
|
export function appendTaskTextDelta(id: string, content: string) {
|
|
return request<TaskTextDelta>(api.post(`/tasks/${encodeURIComponent(id)}/text-deltas`, { content }));
|
|
}
|
|
|
|
export function queryTaskTextReplay(id: string, after = 0) {
|
|
return request<TaskTextReplay>(api.get(`/tasks/${encodeURIComponent(id)}/text-deltas`, { params: { after } }));
|
|
}
|
|
|
|
export function retryGenerationTask(id: string) {
|
|
return request<GenerationTask>(api.post(`/tasks/${encodeURIComponent(id)}/retry`));
|
|
}
|
|
|
|
export function queryFailedVideoProviderTask(id: string) {
|
|
return request<ProviderTaskQueryResult>(api.post(`/tasks/${encodeURIComponent(id)}/query-provider`));
|
|
}
|
|
|
|
export function cancelGenerationTask(id: string) {
|
|
return request<GenerationTask>(api.post(`/tasks/${encodeURIComponent(id)}/cancel`));
|
|
}
|
|
|
|
export function listTaskLogs(id: string) {
|
|
return request<TaskLog[]>(api.get(`/tasks/${encodeURIComponent(id)}/logs`));
|
|
}
|
|
|
|
export async function waitForGenerationTask(id: string, options?: { signal?: AbortSignal; intervalMs?: number; timeoutMs?: number; initialTask?: GenerationTask; onTaskUpdate?: (task: GenerationTask) => void }) {
|
|
const startedAt = Date.now();
|
|
const intervalMs = options?.intervalMs || 2000;
|
|
let lastTask = options?.initialTask;
|
|
let lastQueryError: unknown;
|
|
try {
|
|
while (Date.now() - startedAt < (options?.timeoutMs || taskWaitTimeoutMs(lastTask))) {
|
|
if (options?.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
let task: GenerationTask;
|
|
try {
|
|
task = await queryGenerationTask(id, { signal: options?.signal });
|
|
lastTask = task;
|
|
lastQueryError = undefined;
|
|
options?.onTaskUpdate?.(task);
|
|
} catch (error) {
|
|
lastQueryError = error;
|
|
await delay(intervalMs, options?.signal);
|
|
continue;
|
|
}
|
|
if (task.status === "succeeded") {
|
|
window.dispatchEvent(new CustomEvent("wallet:updated"));
|
|
return task;
|
|
}
|
|
if (task.status === "failed" || task.status === "cancelled") {
|
|
window.dispatchEvent(new CustomEvent("wallet:updated"));
|
|
throw new Error(task.error ? generationErrorMessage(task.error) : `任务${task.status === "cancelled" ? "已取消" : "失败"}`);
|
|
}
|
|
await delay(intervalMs, options?.signal);
|
|
}
|
|
} catch (error) {
|
|
if (options?.signal?.aborted) {
|
|
await cancelGenerationTask(id).catch(() => undefined);
|
|
window.dispatchEvent(new CustomEvent("wallet:updated"));
|
|
throw new DOMException("Aborted", "AbortError");
|
|
}
|
|
throw error;
|
|
}
|
|
throw new Error(lastQueryError instanceof Error ? `任务状态同步失败:${lastQueryError.message}` : "任务执行超时,请稍后重试");
|
|
}
|
|
|
|
function taskWaitTimeoutMs(task?: GenerationTask) {
|
|
const type = task?.type || "";
|
|
if (type.includes("storyboard")) return 13 * 60 * 1000;
|
|
if (type.includes("video")) return 32 * 60 * 1000;
|
|
if (type.includes("image")) return 10 * 60 * 1000;
|
|
if (type.includes("text") || type.includes("audio")) return 12 * 60 * 1000;
|
|
return 10 * 60 * 1000;
|
|
}
|
|
|
|
function delay(ms: number, signal?: AbortSignal) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const timer = window.setTimeout(resolve, ms);
|
|
signal?.addEventListener(
|
|
"abort",
|
|
() => {
|
|
window.clearTimeout(timer);
|
|
reject(new DOMException("Aborted", "AbortError"));
|
|
},
|
|
{ once: true },
|
|
);
|
|
});
|
|
}
|
|
|
|
function notifyCanvasTaskCreated(task: GenerationTask) {
|
|
if (typeof window === "undefined" || !task.projectId) return;
|
|
window.dispatchEvent(new CustomEvent("canvas:task-created", { detail: { task } }));
|
|
}
|