增加即梦的生图和生视频功能

This commit is contained in:
andy committed 2026-08-19 17:58:22 +08:00
1 parent a9a4cdf125
commit 542246e4ce
20 files changed
+1008 -53

No files matched your search

@@ -13,6 +13,16 @@ describe("create studio template interaction", () => {
expect(applyTemplate).not.toMatch(/\bsetMode\s*\(/);
expect(applyTemplate).toContain("[generateMode]: template.prompt");
});
it("keeps the effect preview display-only instead of adding it to generation materials", async () => {
const source = await readFile(createStudioUrl, "utf8");
const applyTemplate = functionSource(source, "applyTemplateToConsole", "resetTemplateForm");
const buildBody = functionSource(source, "buildGenerationBody", "submit");
expect(applyTemplate).not.toMatch(/\bsetMaterials\s*\(/);
expect(applyTemplate).not.toContain("previewImageUrl");
expect(buildBody).not.toContain("previewImageUrl");
});
});
function functionSource(source: string, name: string, nextName: string) {
+22 -1
View File
@@ -1,3 +1,24 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("Go provider adapter contract",()=>{it("freezes bounded adapters and safe errors",async()=>{const c=JSON.parse(await readFile(new URL("../contracts/providers/http-v1.json",import.meta.url),"utf8"));expect(Object.keys(c.providers)).toEqual(["volcengine-visual","evolink","bailian","seedance"]);expect(c.errors).toEqual({generic:true,secretSafe:true});expect(c.liveCallsInTests).toBe(false);});});
describe("Go provider adapter contract", () => {
it("freezes bounded adapters and safe errors", async () => {
const contract = JSON.parse(await readFile(new URL("../contracts/providers/http-v1.json", import.meta.url), "utf8"));
expect(Object.keys(contract.providers)).toEqual(["volcengine-visual", "evolink", "bailian", "seedance"]);
expect(contract.providers["volcengine-visual"]).toMatchObject({
model: "jimeng_seedream46_cvtob",
submitAction: "JimengSeedream46CVToBSubmitTask",
queryAction: "JimengSeedream46CVToBGetResult",
version: "2024-06-06"
});
expect(contract.providers.seedance).toMatchObject({
model: "doubao-seedance-2-0-260128",
submit: "/contents/generations/tasks",
query: "/contents/generations/tasks/{id}",
maxMaterials: 4,
resolutions: ["480p", "720p", "1080p"]
});
expect(contract.errors).toEqual({ generic: true, secretSafe: true });
expect(contract.liveCallsInTests).toBe(false);
});
});
+72 -2
View File
@@ -1,10 +1,80 @@
import { describe, expect, it } from "vitest";
import { extractSeedanceUsage } from "@/lib/seedance/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createSeedanceTask, extractSeedanceUsage, querySeedanceTask } from "@/lib/seedance/client";
describe("Seedance usage extraction", () => {
afterEach(() => {
vi.unstubAllGlobals();
delete process.env.SEEDANCE_API_KEY;
delete process.env.SEEDANCE_BASE_URL;
delete process.env.SEEDANCE_MODEL;
});
it("reads completion_tokens from top-level and nested provider responses", () => {
expect(extractSeedanceUsage({ usage: { completion_tokens: 12345 } })).toEqual({ completionTokens: 12345 });
expect(extractSeedanceUsage({ data: { usage: { completionTokens: 67890 } } })).toEqual({ completionTokens: 67890 });
expect(extractSeedanceUsage({ usage: { prompt_tokens: 100 } })).toBeUndefined();
});
it("sends the official Seedance 2.0 multimodal payload without UI labels", async () => {
process.env.SEEDANCE_API_KEY = "test-key";
process.env.SEEDANCE_BASE_URL = "https://ark.test/api/v3";
process.env.SEEDANCE_MODEL = "doubao-seedance-2-0-260128";
let body: Record<string, unknown> | undefined;
vi.stubGlobal("fetch", vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
body = JSON.parse(String(init?.body));
return new Response(JSON.stringify({ id: "task-1" }), { status: 200, headers: { "Content-Type": "application/json" } });
}));
await createSeedanceTask({
prompt: "combine",
settings: { ratio: "16:9", duration: 8, resolution: "1080p" },
materials: [
{ type: "image", url: "/image.png", label: "@图片1" },
{ type: "video", url: "/video.mp4", label: "@视频1" },
{ type: "audio", url: "/audio.mp3", label: "@音频1" }
],
origin: "https://app.test"
});
expect(body).toMatchObject({
model: "doubao-seedance-2-0-260128",
generate_audio: true,
ratio: "16:9",
duration: 8,
resolution: "1080p",
watermark: false
});
const content = body?.content as Array<Record<string, unknown>>;
expect(content).toHaveLength(4);
expect(content.slice(1).every((item) => !("label" in item))).toBe(true);
});
it("keeps the provider expired status distinct from a failed task", async () => {
process.env.SEEDANCE_API_KEY = "test-key";
process.env.SEEDANCE_BASE_URL = "https://ark.test/api/v3";
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: "task-1", status: "expired" }), {
status: 200,
headers: { "Content-Type": "application/json" }
})));
await expect(querySeedanceTask("task-1")).resolves.toMatchObject({ status: "expired" });
});
it("rejects more materials than the Seedance 2.0 content limit before calling the provider", async () => {
process.env.SEEDANCE_API_KEY = "test-key";
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
await expect(createSeedanceTask({
prompt: "combine",
settings: {},
materials: Array.from({ length: 5 }, (_, index) => ({
type: "image" as const,
url: `/image-${index + 1}.png`,
label: `@图片${index + 1}`
})),
origin: "https://app.test"
})).rejects.toThrow("最多支持 4 个素材");
expect(fetchMock).not.toHaveBeenCalled();
});
});
+2 -2
View File
@@ -54,10 +54,10 @@ describe("task management and public API helpers", () => {
delete process.env.ALI_OSS_PUBLIC_BASE_URL;
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
const target = String(input);
if (target.includes("CVSync2AsyncSubmitTask")) {
if (target.includes("JimengSeedream46CVToBSubmitTask")) {
return new Response(JSON.stringify({ code: 10000, data: { task_id: "visual-task-1" } }), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (target.includes("CVSync2AsyncGetResult")) {
if (target.includes("JimengSeedream46CVToBGetResult")) {
return new Response(JSON.stringify({ code: 10000, data: { status: "done", image_urls: ["https://cdn.test/result.png"] } }), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (target === "https://cdn.test/result.png") {
+1
View File
@@ -32,6 +32,7 @@ describe("video settings", () => {
expect(normalizeVideoRatio("21:9")).toBe("21:9");
expect(normalizeVideoRatio("bad-ratio")).toBe("9:16");
expect(normalizeVideoResolution("1080p", "doubao-seedance-2-0-260128")).toBe("1080p");
expect(normalizeVideoResolution("4k", "doubao-seedance-2-0-260128")).toBe("720p");
expect(normalizeVideoResolution("1080p", "doubao-seedance-2-0-fast-260128")).toBe("720p");
});
});
+35 -3
View File
@@ -1,7 +1,12 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { sha256Hex, signVolcengineRequest } from "@/lib/volcengine/signature";
import { queryVisualTask, submitVisualTask, type VisualClientConfig } from "@/lib/volcengine/visual-client";
describe("Volcengine Visual signing", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("creates canonical request and signed headers", () => {
const signed = signVolcengineRequest({
method: "POST",
@@ -10,7 +15,7 @@ describe("Volcengine Visual signing", () => {
Version: "2022-08-31",
Action: "CVSync2AsyncSubmitTask"
},
body: JSON.stringify({ req_key: "jimeng_seedream46_cvtob", prompt: "test" }),
body: JSON.stringify({ req_key: "legacy-model", prompt: "test" }),
accessKeyId: "ak",
secretAccessKey: "sk",
region: "cn-north-1",
@@ -19,7 +24,34 @@ describe("Volcengine Visual signing", () => {
});
expect(signed.url).toBe("https://visual.volcengineapi.com/?Action=CVSync2AsyncSubmitTask&Version=2022-08-31");
expect(signed.headers.Authorization).toContain("HMAC-SHA256 Credential=ak/20260528/cn-north-1/cv/request");
expect(signed.headers["X-Content-Sha256"]).toBe(sha256Hex(JSON.stringify({ req_key: "jimeng_seedream46_cvtob", prompt: "test" })));
expect(signed.headers["X-Content-Sha256"]).toBe(sha256Hex(JSON.stringify({ req_key: "legacy-model", prompt: "test" })));
expect(signed.canonicalRequest).toContain("content-type;host;x-content-sha256;x-date");
});
it("uses the dedicated Seedream 4.6 submit and query protocol", async () => {
const urls: string[] = [];
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
urls.push(url);
return new Response(JSON.stringify({ code: 10000, data: { task_id: "task-1", status: "queued" } }), {
status: 200,
headers: { "Content-Type": "application/json" }
});
}));
const config: VisualClientConfig = {
accessKeyId: "ak",
secretAccessKey: "sk",
region: "cn-north-1",
service: "cv",
endpoint: "https://visual.volcengineapi.com"
};
await submitVisualTask({ req_key: "jimeng_seedream46_cvtob", prompt: "draw", scale: 50 }, config);
await queryVisualTask({ req_key: "jimeng_seedream46_cvtob", task_id: "task-1" }, config);
expect(urls).toEqual([
"https://visual.volcengineapi.com/?Action=JimengSeedream46CVToBSubmitTask&Version=2024-06-06",
"https://visual.volcengineapi.com/?Action=JimengSeedream46CVToBGetResult&Version=2024-06-06"
]);
});
});