feat: add durable CMS-free image video subsystem and UI components

This commit is contained in:
Wenxin Duan
2026-09-15 01:04:09 +08:00
parent 9349317eb8
commit 40e03e393c
14 changed files with 795 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import { createReadStream } from "node:fs";
import { HarnessError, ImageVideoDraftSchema, taskIdSchema } from "@agent-studio/harness";
import type { FastifyInstance, FastifyRequest } from "fastify";
import { z } from "zod";
import type { AppDependencies } from "../app.js";
import { requireSession } from "./sessions.js";
export function registerImageVideoRoutes(app: FastifyInstance, dependencies: AppDependencies) {
const context = (request: FastifyRequest) => {
const host = request.headers.host ?? "";
const ports = dependencies.imageVideoPorts ?? [80, 3001, 5173, 4173];
let url: URL;
try { url = new URL(`http://${host}`); } catch { throw new HarnessError("I2V_ORIGIN_DENIED", "Local origin required"); }
if (!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || !ports.includes(Number(url.port || 80)) || request.headers["sec-fetch-site"] === "cross-site" || (request.headers.origin !== undefined && request.headers.origin !== url.origin)) throw new HarnessError("I2V_ORIGIN_DENIED", "Image video tasks are available only from the local page");
if (!dependencies.imageVideo) throw new HarnessError("I2V_TASK_NOT_FOUND", "Image video service unavailable");
return dependencies.imageVideo;
};
app.get("/api/image-video/tasks", async (request, reply) => {
const service = context(request); void reply.header("Cache-Control", "no-store");
const history = await service.list();
return { tasks: await Promise.all(history.map(task => service.refresh(task.id))) };
});
app.get("/api/image-video/tasks/:taskId", async (request, reply) => {
const service = context(request); const { taskId } = z.object({ taskId: taskIdSchema }).parse(request.params);
void reply.header("Cache-Control", "no-store"); return { task: await service.refresh(taskId) };
});
app.post("/api/sessions/:sessionId/image-video/tasks", async (request, reply) => {
const service = context(request); const { sessionId } = z.object({ sessionId: z.string().regex(/^ses_[\w-]+$/) }).parse(request.params);
requireSession(dependencies, sessionId);
if (!request.headers["content-type"]?.startsWith("application/json")) throw new HarnessError("I2V_JSON_REQUIRED", "Draft creation requires JSON");
void reply.header("Cache-Control", "no-store"); return { task: await service.create(sessionId, ImageVideoDraftSchema.parse(request.body)) };
});
for (const kind of ["reference", "video"] as const) app.get(`/api/image-video/tasks/:taskId/${kind}`, async (request, reply) => {
const service = context(request); const { taskId } = z.object({ taskId: taskIdSchema }).parse(request.params);
const query = z.object({ download: z.literal("1").optional() }).strict().parse(request.query);
const media = await service.media(taskId, kind);
void reply.header("Cache-Control", "no-store").header("X-Content-Type-Options", "nosniff").header("Cross-Origin-Resource-Policy", "same-origin").header("Accept-Ranges", "bytes").type(media.contentType);
if (query.download === "1") void reply.header("Content-Disposition", `attachment; filename="${taskId}${kind === "video" ? ".mp4" : ".image"}"`);
const range = request.headers.range;
if (range !== undefined) {
const match = /^bytes=(\d*)-(\d*)$/.exec(range); let start = 0; let end = media.size - 1;
if (match?.[1]) { start = Number(match[1]); if (match[2]) end = Math.min(Number(match[2]), end); }
else if (match?.[2]) start = Math.max(0, media.size - Number(match[2]));
if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= media.size || end < 0) return reply.code(416).header("Content-Range", `bytes */${String(media.size)}`).send();
return reply.code(206).header("Content-Range", `bytes ${String(start)}-${String(end)}/${String(media.size)}`).header("Content-Length", end - start + 1).send(createReadStream(media.path, { start, end }));
}
return reply.header("Content-Length", media.size).send(createReadStream(media.path));
});
}