Merge pull request '修复阿里云参考图拉取失败及异步错误原因丢失' (#29) from codex/alibaba-reference-error-20261104 into main
This commit is contained in:
commit
8fd0dba59f
@ -5,7 +5,7 @@
|
||||
* - qwen-image-edit-plus 系列:支持 1-6 张输出,自定义分辨率,提示词优化
|
||||
* - qwen-image-edit:仅支持 1 张输出
|
||||
*
|
||||
* API 文档:https://help.aliyun.com/zh/model-studio/developer-reference/qwen-vl-plus
|
||||
* API 文档:https://help.aliyun.com/en/model-studio/qwen-image-generation-and-editing-api-reference
|
||||
*/
|
||||
|
||||
import sharp from "sharp";
|
||||
@ -46,6 +46,8 @@ interface AlibabaImageResponse {
|
||||
output?: {
|
||||
task_id?: string;
|
||||
task_status?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
choices?: Array<{
|
||||
finish_reason: string;
|
||||
message: {
|
||||
@ -122,6 +124,11 @@ async function fetchImageBufferWithRetry(
|
||||
timeoutMs = ALIBABA_IMAGE_FETCH_TIMEOUT_MS,
|
||||
attempts = 2
|
||||
): Promise<{ buffer: Buffer; contentType: string | null }> {
|
||||
if (imageUrl.startsWith("data:image/")) {
|
||||
const match = imageUrl.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i);
|
||||
if (!match) throw new Error("输入图片数据格式无效");
|
||||
return { buffer: Buffer.from(match[2], "base64"), contentType: match[1] };
|
||||
}
|
||||
let lastError: unknown = null;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
const ctx = createAbortSignal(timeoutMs);
|
||||
@ -168,7 +175,7 @@ function calculateAlibabaSizeByAspect(aspectValue: number, baseSize: number): st
|
||||
}
|
||||
|
||||
async function inferAspectRatioFromImageUrl(imageUrl: string): Promise<number | null> {
|
||||
if (!/^https?:\/\//i.test(imageUrl)) return null;
|
||||
if (!/^(?:https?:\/\/|data:image\/)/i.test(imageUrl)) return null;
|
||||
|
||||
try {
|
||||
const { buffer } = await fetchImageBufferWithRetry(imageUrl);
|
||||
@ -186,7 +193,7 @@ async function inferAspectRatioFromImageUrl(imageUrl: string): Promise<number |
|
||||
async function inferImageDimensionsFromImageUrl(
|
||||
imageUrl: string,
|
||||
): Promise<{ width: number; height: number; aspectRatio: number } | null> {
|
||||
if (!/^https?:\/\//i.test(imageUrl)) return null;
|
||||
if (!/^(?:https?:\/\/|data:image\/)/i.test(imageUrl)) return null;
|
||||
|
||||
try {
|
||||
const { buffer } = await fetchImageBufferWithRetry(imageUrl);
|
||||
@ -302,6 +309,14 @@ async function prepareAlibabaInputImageUrl(
|
||||
maxSide: ALIBABA_MAX_INPUT_SIDE,
|
||||
modelId,
|
||||
});
|
||||
if (isAlibabaQwenImage3ModelId(modelId)) {
|
||||
if (prepared.buffer.length > maxInputBytes) {
|
||||
throw new Error("输入图片超过模型大小限制,请先压缩图片后重试");
|
||||
}
|
||||
const format = prepared.transformed ? prepared.extension : metadata.format;
|
||||
const mime = format === "jpg" || format === "jpeg" ? "image/jpeg" : `image/${format}`;
|
||||
return `data:${mime};base64,${prepared.buffer.toString("base64")}`;
|
||||
}
|
||||
if (!prepared.transformed) {
|
||||
return normalizedUrl;
|
||||
}
|
||||
@ -565,8 +580,10 @@ async function sendAlibabaImageAsyncRequest(params: {
|
||||
return task;
|
||||
}
|
||||
if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
|
||||
const reason = task.output?.message || task.message || `阿里图片任务${taskStatus || "失败"}`;
|
||||
const code = task.output?.code || task.code;
|
||||
throw new AlibabaImageTerminalTaskError(
|
||||
task.message || `阿里图片任务${taskStatus || "失败"}(task_id: ${taskId})`,
|
||||
`${reason}${code ? `(${code})` : ""}(task_id: ${taskId})`,
|
||||
);
|
||||
}
|
||||
lastPollError = null;
|
||||
|
||||
@ -246,6 +246,9 @@ export function getRequestFailureDetails(
|
||||
if (/上传到存储桶失败|存储.*失败|归档.*失败|archive.*failed|storage.*failed/i.test(text)) {
|
||||
return detail("RESULT_STORAGE_FAILED", "处理结果保存失败,生成结果可能已经存在。", "请保留当前页面并联系管理员提供请求编号,先不要重复生成。", "unknown");
|
||||
}
|
||||
if (/failed to download (?:the )?image url|failed to download.*reference.*image/i.test(text)) {
|
||||
return detail("PROVIDER_REFERENCE_FETCH_FAILED", "模型服务未能读取参考图。", "请重新上传参考图或切换模型后再提交;若原图仍可打开,请联系管理员检查模型取图线路,并提供请求编号。");
|
||||
}
|
||||
if (/参考图.*(?:下载失败|不可|失效)|无法获取图片|图片(?:源文件|链接).*不可|获取图片.*失败|get image url failed/i.test(text)) {
|
||||
return detail("SOURCE_IMAGE_UNAVAILABLE", "请求中的图片无法读取,链接可能已失效或没有访问权限。", "请重新上传原图后再提交;若原图仍可打开,请联系管理员检查取图服务。");
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ export default defineConfig({
|
||||
/routes\/replacement-product-identity\.test\.ts/,
|
||||
/routes\/detail-replacement-batch\.test\.ts/,
|
||||
/routes\/alibaba-image-input\.test\.ts/,
|
||||
/routes\/alibaba-task-errors\.test\.ts/,
|
||||
/routes\/model3d-style-pro-regressions\.test\.ts/,
|
||||
/routes\/clawd-request-lifecycle\.test\.ts/,
|
||||
/routes\/canvas-skill-tag-order\.test\.ts/,
|
||||
|
||||
59
tests/routes/alibaba-task-errors.test.ts
Normal file
59
tests/routes/alibaba-task-errors.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||||
import sharp from "sharp";
|
||||
|
||||
test("Alibaba terminal task errors preserve the nested provider reason and do not resubmit", async () => {
|
||||
const methods: string[] = [];
|
||||
const loader = serverLoader({
|
||||
"@/lib/storage/image-archive": {}, "@/lib/storage/service": {},
|
||||
setTimeout: (callback: () => void, ms: number) => setTimeout(callback, ms === 5000 ? 0 : ms),
|
||||
fetch: async (_url: string, init: RequestInit) => {
|
||||
methods.push(init.method || "GET");
|
||||
return Response.json(methods.length === 1
|
||||
? { output: { task_id: "original-task", task_status: "PENDING" } }
|
||||
: { output: { task_id: "original-task", task_status: "FAILED", code: "InvalidParameter", message: "Failed to download image URL after retries." } });
|
||||
},
|
||||
});
|
||||
const provider = loader("lib/ai/providers/alibaba-image.ts").createAlibabaImage({ apiKey: "fixture-key", baseURL: "https://dashscope.example/api/v1/services/aigc/multimodal-generation/generation" });
|
||||
let error: unknown;
|
||||
try { await provider.chat("qwen-image-3.0").doGenerate({ messages: [{ role: "user", content: "fixture" }], imageParams: { aspectRatio: "1:1" } }); } catch (caught) { error = caught; }
|
||||
expect(String(error)).toContain("Failed to download image URL after retries.");
|
||||
expect(String(error)).toContain("InvalidParameter");
|
||||
expect(String(error)).toContain("original-task");
|
||||
expect(methods).toEqual(["POST", "GET"]);
|
||||
const failure = loader("lib/request-error.ts").getRequestFailureDetails(error);
|
||||
expect(failure).toMatchObject({ code: "PROVIDER_REFERENCE_FETCH_FAILED", message: "模型服务未能读取参考图。" });
|
||||
expect(failure.recommendation).toContain("模型取图线路");
|
||||
expect(failure.recommendation).not.toContain("稍后重试");
|
||||
});
|
||||
|
||||
test("Qwen 3 sends reference bytes directly with the right MIME and keeps auto resolution", async () => {
|
||||
const png = await sharp({ create: { width: 32, height: 64, channels: 3, background: "blue" } }).png().toBuffer();
|
||||
const jpeg = await sharp(png).jpeg().toBuffer();
|
||||
const webp = await sharp(png).webp().toBuffer();
|
||||
const originals = [png, jpeg, webp];
|
||||
let submitted: any;
|
||||
let posts = 0;
|
||||
const loader = serverLoader({
|
||||
"@/lib/storage/image-archive": {}, "@/lib/storage/service": { uploadFile() { throw new Error("Reference transport must not upload an intermediate file"); } },
|
||||
setTimeout: (callback: () => void, ms: number) => setTimeout(callback, ms === 5000 ? 0 : ms),
|
||||
fetch: async (url: string, init: RequestInit = {}) => {
|
||||
if (init.method === "POST") { posts++; submitted = JSON.parse(String(init.body)); return Response.json({ output: { task_id: "inline-task", task_status: "PENDING" } }); }
|
||||
if (url.includes("/tasks/")) return Response.json({ output: { task_status: "FAILED", message: "transport-inspection-complete" } });
|
||||
const index = Number(new URL(url).pathname.slice(1));
|
||||
return new Response(originals[index], { headers: { "content-type": ["image/png", "image/jpeg", "image/webp"][index] } });
|
||||
},
|
||||
});
|
||||
const provider = loader("lib/ai/providers/alibaba-image.ts").createAlibabaImage({ apiKey: "fixture-key", baseURL: "https://dashscope.example" });
|
||||
await expect(provider.chat("qwen-image-3.0").doGenerate({ messages: [{ role: "user", content: [
|
||||
{ type: "text", text: "fixture" }, ...originals.map((_bytes, index) => ({ type: "image", image: `https://source.example/${index}` })),
|
||||
] }], imageParams: { aspectRatio: "auto", imageSize: "2K" } })).rejects.toThrow("transport-inspection-complete");
|
||||
expect(posts).toBe(1);
|
||||
expect(submitted.parameters.size).toBe("1024*2048");
|
||||
const images = submitted.input.messages[0].content.filter((part: any) => part.image).map((part: any) => part.image);
|
||||
expect(images).toHaveLength(3);
|
||||
images.forEach((value: string, index: number) => {
|
||||
expect(value.split(";", 1)[0]).toBe(["data:image/png", "data:image/jpeg", "data:image/webp"][index]);
|
||||
expect(Buffer.from(value.split(",")[1], "base64").equals(originals[index])).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user