133 lines
6.2 KiB
TypeScript
133 lines
6.2 KiB
TypeScript
import { videoUpscaleSchema } from "@/lib/video-canvas/video-upscale-contract";
|
|
import { videoEditRequestSchema } from "@/lib/video-canvas/video-edit-contract";
|
|
import { canvasLayerSplitSchema } from "@/lib/video-canvas/layer-split-contract";
|
|
import { smartEditRequestSchema } from "@/lib/video-canvas/smart-edit/contract";
|
|
import { audioRequestSchema } from "@/lib/video-canvas/audio-contract";
|
|
import { canResumeCanvasTask } from "@/lib/video-canvas/task-recovery";
|
|
import { imageOperationSchema } from "@/lib/video-canvas/image-operation";
|
|
import { after, NextResponse } from "next/server";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { z } from "zod";
|
|
import { canvasTaskRequestSchema } from "@/lib/video-canvas/task-contract";
|
|
import {
|
|
cancelCanvasGenerationTask,
|
|
createCanvasGenerationTask,
|
|
listCanvasGenerationTasks,
|
|
ownsTaskProject,
|
|
} from "@/lib/video-canvas/task-store";
|
|
import { runCanvasGenerationQueue } from "@/lib/video-canvas/task-worker";
|
|
export const runtime = "nodejs";
|
|
export const maxDuration = 900;
|
|
|
|
async function owner(projectId: unknown) {
|
|
const session = await auth();
|
|
if (!session?.user?.id) throw Object.assign(Error("未登录"), { status: 401 });
|
|
if (!z.string().uuid().safeParse(projectId).success)
|
|
throw Object.assign(Error("画布 ID 无效"), { status: 400 });
|
|
if (!(await ownsTaskProject(session.user.id, projectId as string)))
|
|
throw Object.assign(Error("画布不存在"), { status: 404 });
|
|
return session.user.id;
|
|
}
|
|
const fail = (error: unknown) =>
|
|
NextResponse.json(
|
|
{ success: false, error: error instanceof Error ? error.message : "任务操作失败" },
|
|
{ status: Number((error as any)?.status) || (error instanceof SyntaxError ? 400 : 500) },
|
|
);
|
|
const kick = (userId: string, projectId: string) =>
|
|
after(() =>
|
|
runCanvasGenerationQueue(userId, projectId).catch((error) =>
|
|
console.error("[Canvas tasks]", error),
|
|
),
|
|
);
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const text = await request.text();
|
|
if (new TextEncoder().encode(text).byteLength > 262144)
|
|
throw Object.assign(Error("任务数据过大,请使用已上传的素材链接"), { status: 413 });
|
|
const parsed = canvasTaskRequestSchema.safeParse(JSON.parse(text));
|
|
if (!parsed.success) throw Object.assign(Error("任务参数不合法"), { status: 400 });
|
|
if(parsed.data.workflow)throw Object.assign(Error("编组任务必须使用整组提交接口"),{status:400});
|
|
if (
|
|
parsed.data.kind === "image-operation" &&
|
|
!imageOperationSchema.safeParse(parsed.data.params).success
|
|
)
|
|
throw Object.assign(Error("图片处理参数不合法"), { status: 400 });
|
|
if (
|
|
parsed.data.attachment &&
|
|
((parsed.data.attachment.type === "story-asset" && parsed.data.kind !== "image") ||
|
|
(parsed.data.attachment.type === "story-prompts" && parsed.data.kind !== "analysis"))
|
|
)
|
|
throw Object.assign(Error("脚本结果类型不匹配"), { status: 400 });
|
|
if (parsed.data.kind === "audio" && !audioRequestSchema.safeParse(parsed.data.params).success)
|
|
throw Object.assign(Error("音频参数不合法"), { status: 400 });
|
|
if (
|
|
parsed.data.kind === "smart-edit" &&
|
|
!smartEditRequestSchema.safeParse(parsed.data.params).success
|
|
)
|
|
throw Object.assign(Error("剪辑参数无效"), { status: 400 });
|
|
if (parsed.data.kind === "video-upscale" && !videoUpscaleSchema.safeParse(parsed.data.params).success) throw Object.assign(Error("视频高清参数无效"), {status:400});
|
|
if (parsed.data.kind === "video-edit" && !videoEditRequestSchema.safeParse(parsed.data.params).success) throw Object.assign(Error("视频编辑参数无效"), {status:400});
|
|
if (parsed.data.kind === "image-layer-split" && !canvasLayerSplitSchema.safeParse(parsed.data.params).success) throw Object.assign(Error("图层分离参数无效"), {status:400});
|
|
const userId = await owner(parsed.data.projectId);
|
|
const task = await createCanvasGenerationTask(userId, parsed.data);
|
|
kick(userId, parsed.data.projectId);
|
|
return NextResponse.json({ success: true, task }, { status: 202 });
|
|
} catch (error) {
|
|
return fail(error);
|
|
}
|
|
}
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const projectId = new URL(request.url).searchParams.get("projectId") || "";
|
|
const userId = await owner(projectId);
|
|
const tasks = await listCanvasGenerationTasks(userId, projectId);
|
|
if (tasks.some((task) => task.status === "pending" || canResumeCanvasTask(task)))
|
|
kick(userId, projectId);
|
|
return NextResponse.json(
|
|
{ success: true, tasks },
|
|
{ headers: { "Cache-Control": "no-store" } },
|
|
);
|
|
} catch (error) {
|
|
return fail(error);
|
|
}
|
|
}
|
|
export async function DELETE(request: Request) {
|
|
try {
|
|
const { projectId, taskId } = await request.json();
|
|
const userId = await owner(projectId);
|
|
if (!z.string().uuid().safeParse(taskId).success)
|
|
throw Object.assign(Error("任务 ID 无效"), { status: 400 });
|
|
const result = await cancelCanvasGenerationTask(userId, projectId, taskId);
|
|
if (!result.task)
|
|
return NextResponse.json({ success: false, error: "任务不存在" }, { status: 404 });
|
|
if (!result.ok)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error:
|
|
result.task.status === "running"
|
|
? "任务已提交供应商,当前线路不支持中止。结果会继续保存,请勿重复生成。"
|
|
: "任务已结束或状态待核实,请先刷新任务状态。",
|
|
task: result.task,
|
|
},
|
|
{ status: 409 },
|
|
);
|
|
return NextResponse.json({ success: true, task: result.task });
|
|
} catch (error) {
|
|
return fail(error);
|
|
}
|
|
}
|
|
export async function PATCH(request: Request) {
|
|
try {
|
|
const { projectId, taskId, nodeId } = await request.json();
|
|
const userId = await owner(projectId);
|
|
if (!z.string().uuid().safeParse(taskId).success || typeof nodeId !== "string")
|
|
return NextResponse.json({ success: false }, { status: 400 });
|
|
const { hideCanvasGenerationNode } = await import("@/lib/video-canvas/task-store");
|
|
const success = await hideCanvasGenerationNode(userId, projectId, taskId, nodeId);
|
|
return NextResponse.json({ success }, { status: success ? 200 : 404 });
|
|
} catch (error) {
|
|
return fail(error);
|
|
}
|
|
}
|