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

This commit is contained in:
ddCat
2026-08-22 09:52:09 +08:00
parent 7418ddfa9c
commit 633560a5d8
91 changed files with 6380 additions and 4247 deletions

View 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 || "");
}