refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消

This commit is contained in:
ddCat
2026-08-22 09:52:09 +08:00
parent 7323a7b044
commit 8be75c1fbd
91 changed files with 6380 additions and 4247 deletions

View File

@@ -0,0 +1,122 @@
import axios from "axios";
import type { ApiEnvelope, ApiVideoResponse, RequestOptions, SeedanceTask, VideoGenerationResult } from "./video-contracts";
export function videoTaskId(payload: { id?: string; request_id?: string; task_id?: string }) {
return payload.id || payload.request_id || payload.task_id || "";
}
export function unwrapVideoResponse(payload: ApiVideoResponse) {
return unwrapEnvelope(payload, "接口没有返回视频任务");
}
export function unwrapSeedanceTask(payload: ApiEnvelope<SeedanceTask>) {
return unwrapEnvelope(payload, "Seedance 接口没有返回任务");
}
export function unwrapEnvelope<T>(payload: ApiEnvelope<T>, emptyMessage: string): T {
if (!payload) throw new Error(emptyMessage);
if (typeof payload === "object" && "code" in payload && typeof payload.code === "number") {
if (payload.code !== 0) throw new Error(payload.msg || "请求失败");
if (!payload.data) throw new Error(emptyMessage);
return payload.data;
}
return payload as T;
}
export function readAxiosError(error: unknown, fallback: string) {
if (axios.isCancel(error)) return "请求已取消";
if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
const responseData = error.response?.data;
return responseData?.msg || responseData?.error?.message || statusMessage(error.response?.status, fallback);
}
if (error instanceof DOMException && error.name === "AbortError") return "请求已取消";
return error instanceof Error ? error.message : fallback;
}
export function statusMessage(status: number | undefined, fallback: string) {
if (status === 401 || status === 403) return "鉴权失败,请检查 API Key、套餐权限或模型权限";
if (status === 429) return "请求被限流或额度不足,请稍后重试";
return status ? `${fallback}(${status})` : fallback;
}
export async function assertVideoBlob(blob: Blob) {
if (!blob.type.includes("json")) return;
let payload: { code?: number; msg?: string; error?: { message?: string } };
try {
payload = JSON.parse(await blob.text()) as { code?: number; msg?: string; error?: { message?: string } };
} catch {
return;
}
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "视频下载失败");
if (payload.error?.message) throw new Error(payload.error.message);
}
export function delay(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
},
{ once: true },
);
});
}
export function blobToDataUrl(blob: Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("读取本地素材失败"));
reader.readAsDataURL(blob);
});
}
export async function videoResultFromUrl(url: string, options?: RequestOptions): Promise<VideoGenerationResult> {
try {
const response = await axios.get<Blob>(url, { responseType: "blob", signal: options?.signal });
await assertVideoBlob(response.data);
return { blob: response.data };
} catch (error) {
if (axios.isCancel(error) || options?.signal?.aborted) throw error;
return { url, mimeType: "video/mp4" };
}
}
export type VideoResponseTools = {
assertVideoBlob: typeof assertVideoBlob;
blobToDataUrl: typeof blobToDataUrl;
delay: typeof delay;
readAxiosError: typeof readAxiosError;
unwrapEnvelope: typeof unwrapEnvelope;
unwrapEnvelopeRecord: typeof unwrapEnvelopeRecord;
unwrapSeedanceTask: typeof unwrapSeedanceTask;
unwrapVideoResponse: typeof unwrapVideoResponse;
videoResultFromUrl: typeof videoResultFromUrl;
videoTaskId: typeof videoTaskId;
};
function unwrapEnvelopeRecord(value: ApiEnvelope<Record<string, unknown>>): Record<string, unknown> {
if (value && typeof value === "object" && "data" in value && value.data && typeof value.data === "object") return value.data as Record<string, unknown>;
return value as Record<string, unknown>;
}
export const videoResponseTools: VideoResponseTools = {
assertVideoBlob,
blobToDataUrl,
delay,
readAxiosError,
unwrapEnvelope,
unwrapEnvelopeRecord,
unwrapSeedanceTask,
unwrapVideoResponse,
videoResultFromUrl,
videoTaskId,
};