feat(api): 火山模型协议 - 接入方舟与即梦图片视频生成并支持 AK/SK 凭证

This commit is contained in:
ddCat
2026-07-29 18:08:01 +08:00
parent 9ad2fb1b99
commit c33e05ff63
29 changed files with 1739 additions and 310 deletions

View File

@@ -121,6 +121,7 @@ const IMAGE_MAX_PIXELS = 8294400;
const IMAGE_MAX_EDGE = 3840;
const IMAGE_MAX_RATIO = 3;
const IMAGE_OUTPUT_FORMAT = "png";
const VOLCENGINE_ARK_IMAGE_MAX_PIXELS = 4624220;
function normalizeQuality(quality: string) {
const value = quality.trim().toLowerCase();
@@ -190,6 +191,20 @@ function resolveRequestSize(quality: string | undefined, size: string) {
throw new Error("图像尺寸格式不支持,请使用 auto、9:16 或 1024x1024");
}
function normalizeVolcengineArkImageSize(size: string | undefined) {
if (!size) return undefined;
const dimensions = parseImageDimensions(size);
if (!dimensions || dimensions.width * dimensions.height <= VOLCENGINE_ARK_IMAGE_MAX_PIXELS) return size;
const scale = Math.sqrt(VOLCENGINE_ARK_IMAGE_MAX_PIXELS / (dimensions.width * dimensions.height));
let width = Math.floor((dimensions.width * scale) / 2) * 2;
let height = Math.floor((dimensions.height * scale) / 2) * 2;
while (width > 2 && height > 2 && width * height > VOLCENGINE_ARK_IMAGE_MAX_PIXELS) {
if (width >= height) width -= 2;
else height -= 2;
}
return `${width}x${height}`;
}
function resolveImageDataUrl(item: Record<string, unknown>) {
if (typeof item.b64_json === "string" && item.b64_json) {
return `data:image/png;base64,${item.b64_json}`;
@@ -250,6 +265,18 @@ function aiHeaders(config: AiConfig, contentType?: string) {
};
}
async function postVolcengineArkImage(config: ReturnType<typeof resolveModelRequestConfig>, payload: Record<string, unknown>, options?: RequestOptions) {
const upstreamUrl = aiApiUrl(config, "/images/generations");
const request = channelRequest(config, upstreamUrl, aiHeaders(config, "application/json"));
return (
await axios.post<ImageApiResponse>(request.url, payload, {
headers: request.headers,
withCredentials: request.credentials === "include",
signal: options?.signal,
})
).data;
}
function geminiBaseUrl(config: Pick<AiConfig, "baseUrl">) {
const normalizedBaseUrl = resolveBackendApiUrl(config.baseUrl).replace(/\/+$/, "");
const lowerBaseUrl = normalizedBaseUrl.toLowerCase();
@@ -768,25 +795,35 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
}
const quality = normalizeQuality(config.quality);
const requestSize = resolveRequestSize(quality, config.size);
const isVolcengineArk = requestConfig.interfaceType === "volcengine-ark-image";
const normalizedRequestSize = isVolcengineArk ? normalizeVolcengineArkImageSize(requestSize) : requestSize;
try {
const response = await axios.post<ImageApiResponse>(
aiApiUrl(requestConfig, "/images/generations"),
{
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, prompt),
n,
...(quality ? { quality } : {}),
...(requestSize ? { size: requestSize } : {}),
response_format: "b64_json",
output_format: IMAGE_OUTPUT_FORMAT,
...(config.transparentBackground === "true" ? { background: "transparent" } : {}),
},
{
headers: aiHeaders(requestConfig, "application/json"),
signal: options?.signal,
},
);
const images = parseImagePayload(response.data);
const payload = isVolcengineArk
? {
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, prompt),
n,
...(normalizedRequestSize ? { size: normalizedRequestSize } : {}),
}
: {
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, prompt),
n,
...(quality ? { quality } : {}),
...(requestSize ? { size: requestSize } : {}),
response_format: "b64_json",
output_format: IMAGE_OUTPUT_FORMAT,
...(config.transparentBackground === "true" ? { background: "transparent" } : {}),
};
const responseData = isVolcengineArk
? await postVolcengineArkImage(requestConfig, payload, options)
: (
await axios.post<ImageApiResponse>(aiApiUrl(requestConfig, "/images/generations"), payload, {
headers: aiHeaders(requestConfig, "application/json"),
signal: options?.signal,
})
).data;
const images = parseImagePayload(responseData);
return images;
} catch (error) {
throw new Error(readAxiosError(error, "请求失败"));
@@ -806,6 +843,28 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
throw new Error(readAxiosError(error, "请求失败"));
}
}
if (requestConfig.interfaceType === "volcengine-ark-image") {
if (mask) throw new Error("火山方舟图片协议不支持蒙版编辑,请移除蒙版后重试");
const quality = normalizeQuality(config.quality);
const requestSize = normalizeVolcengineArkImageSize(resolveRequestSize(quality, config.size));
try {
const images = await Promise.all(references.map((image) => imageToDataUrl(image)));
const response = await postVolcengineArkImage(
requestConfig,
{
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, requestPrompt),
n,
...(requestSize ? { size: requestSize } : {}),
...(images.length === 1 ? { image: images[0] } : images.length > 1 ? { image: images } : {}),
},
options,
);
return parseImagePayload(response);
} catch (error) {
throw new Error(readAxiosError(error, "火山方舟图片生成失败"));
}
}
const quality = normalizeQuality(config.quality);
const requestSize = resolveRequestSize(quality, config.size);
const formData = new FormData();