fix(ark-assets): 方舟素材库 - 阻止图片 Agent Plan 误用 asset:// 可信素材

Agent Plan Seedream 与 Seedance 共用 /api/plan/v3,BaseURL 判断会把图片生成误判为视频可信素材路径。仅视频 mode/协议才同步或提交 asset://。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
VicoHu
2026-09-17 17:43:01 +08:00
parent b6bae4f957
commit 77f2c9d52c
7 changed files with 172 additions and 10 deletions

View File

@@ -59,7 +59,12 @@ const seedancePixels = {
export function isSeedanceVideoConfig(config: AiConfig | Pick<AiConfig, "model" | "videoModel" | "baseUrl">) {
const requestConfig = "channels" in config ? resolveModelRequestConfig(config, config.model || config.videoModel) : config;
return isSeedanceVideoModel(modelOptionName(requestConfig.model || requestConfig.videoModel)) || isArkPlanBaseUrl(requestConfig.baseUrl);
// Agent Plan 图片与视频共用 /api/plan/v3;按协议排除图片,避免 Seedream 误判为 Seedance。
if ("interfaceType" in requestConfig) {
const interfaceType = requestConfig.interfaceType;
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-agent-plan-image") return false;
}
return isSeedanceVideoModel(modelOptionName(requestConfig.model || requestConfig.videoModel)) || isArkPlanBaseUrl(requestConfig.baseUrl || "");
}
export function isSeedanceVideoModel(model: string) {

View File

@@ -95,7 +95,7 @@ export async function runBackendGenerationTask(
throwIfAborted(signal);
assertClientPromptLimit(mode, prompt, config, metadata);
assertBackendRuntimeConfigured(config, mode);
const prepared = await prepareGenerationReferences({ config, referenceImages, referenceVideos, referenceAudios, mask });
const prepared = await prepareGenerationReferences({ config, mode, referenceImages, referenceVideos, referenceAudios, mask });
throwIfAborted(signal);
return createAndWaitGenerationTask({ projectId, mode, prompt, config, referenceImages, referenceVideos, referenceAudios, textHistory, signal, metadata, onTaskUpdate, onTextDelta, streamText, enableThinking, clientOperationId, retryOf, attemptGroupId }, prepared, dependencies);
}
@@ -233,12 +233,14 @@ function assertClientPromptLimit(mode: BackendGenerationMode, prompt: string, co
async function prepareGenerationReferences({
config,
mode,
referenceImages = [],
referenceVideos = [],
referenceAudios = [],
mask,
}: Pick<BackendGenerationTaskOptions, "config" | "referenceImages" | "referenceVideos" | "referenceAudios" | "mask">): Promise<PreparedGenerationReferences> {
const preferArkAssetUrl = usesArkVideoAssetReference(config);
}: Pick<BackendGenerationTaskOptions, "config" | "mode" | "referenceImages" | "referenceVideos" | "referenceAudios" | "mask">): Promise<PreparedGenerationReferences> {
// asset:// 仅视频生成可用;Agent Plan Seedream 与 Seedance 共用 /api/plan/v3,不能按 BaseURL 误判。
const preferArkAssetUrl = mode === "video" && usesArkVideoAssetReference(config);
const preparedImages = await Promise.all(referenceImages.map((image) => prepareBackendImageReference(image, preferArkAssetUrl)));
const preparedVideos = await Promise.all(referenceVideos.map(prepareBackendMediaReference));
const preparedAudios = await Promise.all(referenceAudios.map(prepareBackendMediaReference));
@@ -249,7 +251,10 @@ async function prepareGenerationReferences({
// 与后端 isArkPrivateAssetVideoConfig 对齐:方舟视频渠道允许参考图直接 asset:// 引用,
// 跳过可信素材上传同步(适合已录入方舟素材 ID 的素材,例如被授权的真人像素材)。
function usesArkVideoAssetReference(config: AiConfig) {
return resolveModelRequestConfig(config, config.model).interfaceType === "volcengine-ark-video" || isArkPlanBaseUrl(config.baseUrl || "");
const interfaceType = resolveModelRequestConfig(config, config.model).interfaceType;
if (interfaceType === "volcengine-ark-video" || interfaceType === "volcengine-ark-agent-plan-video") return true;
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-agent-plan-image") return false;
return isArkPlanBaseUrl(config.baseUrl || "");
}
async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {

View File

@@ -0,0 +1,86 @@
import { describe, expect, test } from "bun:test";
import { prepareBackendGenerationTask } from "../src/services/api/generation-task";
import { createModelChannel, defaultConfig, encodeChannelModel } from "../src/stores/use-config-store";
function agentPlanImageConfig() {
const channel = createModelChannel({
id: "ark-plan-image",
name: "Ark Plan Seedream",
baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
apiKey: "test-key",
interfaceType: "volcengine-ark-agent-plan-image",
models: ["doubao-seedream-4-0-250828"],
modelCosts: [{
model: "doubao-seedream-4-0-250828",
capability: "image",
protocol: "volcengine-ark-agent-plan-image",
billingMode: "fixed_request",
unitPriceMicrocredits: 1,
}],
});
const model = encodeChannelModel(channel.id, "doubao-seedream-4-0-250828");
return { ...defaultConfig, channels: [channel], model, baseUrl: channel.baseUrl, interfaceType: channel.interfaceType };
}
function agentPlanVideoConfig() {
const channel = createModelChannel({
id: "ark-plan-video",
name: "Ark Plan Seedance",
baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
apiKey: "test-key",
interfaceType: "volcengine-ark-agent-plan-video",
models: ["doubao-seedance-1-5-pro-251215"],
modelCosts: [{
model: "doubao-seedance-1-5-pro-251215",
capability: "video",
protocol: "volcengine-ark-agent-plan-video",
billingMode: "token",
inputPricePerMillion: 1,
outputPricePerMillion: 1,
}],
});
const model = encodeChannelModel(channel.id, "doubao-seedance-1-5-pro-251215");
return { ...defaultConfig, channels: [channel], model, videoModel: model, baseUrl: channel.baseUrl, interfaceType: channel.interfaceType };
}
describe("generation-task ark asset references", () => {
test("agent plan Seedream image mode does not rewrite references to asset://", async () => {
const input = await prepareBackendGenerationTask({
mode: "image",
prompt: "edit this",
config: agentPlanImageConfig(),
referenceImages: [{
id: "ref-1",
name: "ref.png",
type: "image/png",
url: "https://cdn.example.com/ref.png",
storageKey: "resource:res-1",
arkAssetId: "asset-already-synced",
}],
});
const images = (input.input as { referenceImages?: Array<{ url?: string; storageKey?: string }> }).referenceImages || [];
expect(images).toHaveLength(1);
expect(images[0]?.url).not.toStartWith("asset://");
expect(images[0]?.storageKey).toBe("resource:res-1");
});
test("agent plan Seedance video mode still prefers asset:// when arkAssetId exists", async () => {
const input = await prepareBackendGenerationTask({
mode: "video",
prompt: "animate this",
config: agentPlanVideoConfig(),
referenceImages: [{
id: "ref-1",
name: "ref.png",
type: "image/png",
url: "https://cdn.example.com/ref.png",
storageKey: "resource:res-1",
arkAssetId: "asset-already-synced",
}],
});
const images = (input.input as { referenceImages?: Array<{ url?: string }> }).referenceImages || [];
expect(images).toHaveLength(1);
expect(images[0]?.url).toBe("asset://asset-already-synced");
});
});