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));
});
}
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { buildApp, type AppDependencies } from "../src/app.js";
import { ImageVideoService, FileAttachmentStore } from "@agent-studio/harness";
import type { ImageVideoPublicTask } from "@agent-studio/harness";
import { mkdtemp, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Readable } from "node:stream";
async function fixture() {
const root = await mkdtemp(join(tmpdir(), "i2v-api-")); await mkdir(join(root, "ses_1"));
const attachments = new FileAttachmentStore({ sessionsRoot: root });
const image = await attachments.add("ses_1", { name: "pixel.png", mediaType: "image/png", stream: Readable.from(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAE0lEQVQImWP4z8DwnwGM/zMwAAAf7gP9qS/A4gAAAABJRU5ErkJggg==", "base64")) });
const service = new ImageVideoService({ root: join(root, "tasks"), attachments, remote: {} as never, collect: () => Promise.reject(new Error("No external operations permitted")) });
const dependencies = { imageVideo: service, sessionRegistry: { get: (id: string) => id === "ses_1" ? { sessionId: id } : undefined, disposeAll: async () => {} }, skillCatalog: { list: () => [] }, modelStore: {}, attachmentStore: attachments, approvalBroker: {} } as unknown as AppDependencies;
return { app: buildApp(dependencies), service, draft: { attachmentId: image.attachmentId, title: "Reference", prompt: "Natural motion and ambient sound", durationSeconds: 10 } };
}
describe("image video local API", () => {
it("creates locally, persists history independently from Sessions and serves registered reference ranges", async () => {
const { app, service, draft } = await fixture();
try {
const response = await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: draft });
expect(response.statusCode).toBe(200); const task = response.json<{ task: ImageVideoPublicTask }>().task;
expect(task.state).toBe("draft");
expect((await app.inject({ method: "GET", url: "/api/image-video/tasks" })).json<{ tasks: ImageVideoPublicTask[] }>().tasks).toHaveLength(1);
const ranged = await app.inject({ method: "GET", url: task.referenceUrl, headers: { range: "bytes=0-3" } });
expect(ranged.statusCode).toBe(206); expect(ranged.rawPayload.length).toBe(4); expect(ranged.headers["cache-control"]).toBe("no-store");
expect((await app.inject({ method: "GET", url: task.referenceUrl, headers: { range: "bytes=999999-" } })).statusCode).toBe(416);
expect((await app.inject({ method: "GET", url: `/api/image-video/tasks/${task.id}/video` })).statusCode).toBe(404);
expect((await service.list())[0]?.id).toBe(task.id);
} finally { await app.close(); }
});
it("rejects foreign origins, rebinding hosts, unknown sessions, arbitrary media paths and mutation types", async () => {
const { app, draft } = await fixture();
try {
for (const headers of [{ host: "evil.example", origin: "http://evil.example" }, { origin: "https://evil.example" }, { "sec-fetch-site": "cross-site" }]) expect((await app.inject({ method: "GET", url: "/api/image-video/tasks", headers })).statusCode).toBeGreaterThanOrEqual(400);
expect((await app.inject({ method: "POST", url: "/api/sessions/ses_other/image-video/tasks", payload: draft })).statusCode).toBe(404);
expect((await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: "text", headers: { "content-type": "text/plain" } })).statusCode).toBe(400);
expect((await app.inject({ method: "GET", url: "/api/image-video/tasks/secret.env/reference" })).statusCode).toBe(400);
expect((await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: { ...draft, path: "/etc/passwd" } })).statusCode).toBe(400);
} finally { await app.close(); }
});
});