refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消
This commit is contained in:
73
web/src/services/api/video-provider-newapi.ts
Normal file
73
web/src/services/api/video-provider-newapi.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { modelCapabilityConfigFor, videoResolutionRequest } from "@/lib/model-capabilities";
|
||||
import { boolConfig } from "@/lib/seedance-video";
|
||||
import { getResourceOSSUrl } from "@/services/api/resources";
|
||||
import { modelOptionName } from "@/stores/use-config-store";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
|
||||
import type { ApiEnvelope, ApiVideoResponse, RequestOptions, ResolvedAiConfig, VideoGenerationTask, VideoGenerationTaskState } from "./video-contracts";
|
||||
import type { VideoProviderDeps } from "./video-provider-deps";
|
||||
import { normalizeVideoSeconds, normalizeVideoSize } from "./video-validation";
|
||||
|
||||
export async function createVideoGenerationsTask(deps: VideoProviderDeps, config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], videoReferences: ReferenceVideo[], audioReferences: ReferenceAudio[], options?: RequestOptions): Promise<VideoGenerationTask> {
|
||||
if (references.length > 9 || videoReferences.length > 3 || audioReferences.length > 3) throw new Error("NewAPI Video Generations 最多支持 9 张参考图、3 个参考视频和 3 个参考音频");
|
||||
if (audioReferences.length > 0 && videoReferences.length === 0) throw new Error("NewAPI Video Generations 的参考音频必须同时提供至少 1 个参考视频;纯音频生视频请切换到支持该模式的渠道");
|
||||
const [imageUrls, videoUrls, audioUrls] = await Promise.all([
|
||||
Promise.all(references.map((item) => resolveVideoGenerationsUrl(item.url || item.dataUrl, item.storageKey))),
|
||||
Promise.all(videoReferences.map((item) => resolveVideoGenerationsUrl(item.url, item.storageKey))),
|
||||
Promise.all(audioReferences.map((item) => resolveVideoGenerationsUrl(item.url, item.storageKey))),
|
||||
]);
|
||||
const profile = modelCapabilityConfigFor(config, model).video!;
|
||||
const resolution = newAPIVideoResolutionRequest(profile, config.vquality, modelOptionName(model));
|
||||
const payload = {
|
||||
model: modelOptionName(model),
|
||||
prompt: prompt.trim(),
|
||||
seconds: normalizeVideoSeconds(config.videoSeconds),
|
||||
aspect_ratio: normalizeVideoSize(config.size) || "16:9",
|
||||
...(resolution ? { resolution } : {}),
|
||||
...(profile.generateAudio.supported ? { generate_audio: boolConfig(config.videoGenerateAudio, profile.generateAudio.default) } : {}),
|
||||
...(imageUrls.length ? { image_urls: imageUrls } : {}),
|
||||
...(videoUrls.length ? { video_urls: videoUrls } : {}),
|
||||
...(audioUrls.length ? { audio_urls: audioUrls } : {}),
|
||||
};
|
||||
try {
|
||||
const created = deps.response.unwrapVideoResponse(await deps.transport.post<ApiVideoResponse>(deps.transport.apiUrl("/video/generations"), payload, options));
|
||||
const id = deps.response.videoTaskId(created);
|
||||
if (!id) throw new Error("NewAPI Video Generations 没有返回任务 ID");
|
||||
return { id, provider: "video-generations", model };
|
||||
} catch (error) {
|
||||
throw new Error(deps.response.readAxiosError(error, "NewAPI Video Generations 任务创建失败"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function pollVideoGenerationsTask(deps: VideoProviderDeps, task: VideoGenerationTask, options?: RequestOptions): Promise<VideoGenerationTaskState> {
|
||||
try {
|
||||
const raw = await deps.transport.get<ApiEnvelope<Record<string, unknown>>>(deps.transport.apiUrl(`/video/generations/${encodeURIComponent(task.id)}`), options);
|
||||
const state = deps.response.unwrapEnvelopeRecord(raw);
|
||||
const status = String(state.status || "").toUpperCase();
|
||||
if (status === "SUCCESS" || status === "SUCCEEDED" || status === "COMPLETED") {
|
||||
const url = String(state.result_url || state.video_url || state.url || "");
|
||||
if (!url) return { status: "failed", error: "视频任务已完成但没有返回视频地址" };
|
||||
return { status: "completed", result: await deps.response.videoResultFromUrl(url, options) };
|
||||
}
|
||||
if (status === "FAILURE" || status === "FAILED" || status === "CANCELLED") return { status: "failed", error: String(state.fail_reason || state.error || "视频生成失败") };
|
||||
return { status: "pending" };
|
||||
} catch (error) {
|
||||
throw new Error(deps.response.readAxiosError(error, "NewAPI Video Generations 任务查询失败"));
|
||||
}
|
||||
}
|
||||
|
||||
function newAPIVideoResolutionRequest(profile: NonNullable<ReturnType<typeof modelCapabilityConfigFor>["video"]>, value: string, model: string) {
|
||||
if (model.trim().toLowerCase() === "grok-video-1.5-1080p") return "1080p";
|
||||
return videoResolutionRequest(profile, value);
|
||||
}
|
||||
|
||||
async function resolveVideoGenerationsUrl(value: string | undefined, storageKey?: string) {
|
||||
if (storageKey?.startsWith("resource:")) return getResourceOSSUrl(storageKey);
|
||||
if (isPublicMediaUrl(value || "")) return String(value);
|
||||
throw new Error("NewAPI Video Generations 的参考素材需要公网 URL;请先把素材保存到对象存储");
|
||||
}
|
||||
|
||||
function isPublicMediaUrl(value: string) {
|
||||
return /^https?:\/\//i.test(value || "");
|
||||
}
|
||||
Reference in New Issue
Block a user