Skip to content

Commit cf3ae02

Browse files
fix(image): 按模型族分流文生图/图编 API 路径与入参
修复 wanx/wan2.x-t2i、wan2.5-i2i、z-image、qwen-image-plus 等误走 endpoint 导致的 url error,并补齐路由单测与 dry-run 覆盖。 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 56d9a96 commit cf3ae02

10 files changed

Lines changed: 600 additions & 202 deletions

File tree

packages/commands/src/commands/image/edit.ts

Lines changed: 87 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {
22
defineCommand,
3-
imagePath,
4-
imageSyncPath,
53
taskPath,
64
detectOutputFormat,
75
resolveOutputDir,
@@ -20,6 +18,7 @@ import {
2018
BailianError,
2119
resolveBooleanFlag,
2220
resolveWatermark,
21+
resolveImageEditApi,
2322
ASYNC_FLAG,
2423
CONCURRENT_FLAG,
2524
redactDataUri,
@@ -32,13 +31,8 @@ import { resolveImageSize } from "bailian-cli-runtime";
3231
import { join } from "path";
3332
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
3433

35-
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
3634
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
3735

38-
function isSyncModel(model: string): boolean {
39-
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
40-
}
41-
4236
function enablesPromptExtendByDefault(model: string): boolean {
4337
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
4438
}
@@ -114,6 +108,7 @@ export default defineCommand({
114108
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
115109
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
116110
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
111+
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
117112
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
118113
],
119114
async run(ctx) {
@@ -128,7 +123,7 @@ export default defineCommand({
128123
const prompt = flags.prompt;
129124

130125
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
131-
const useSync = isSyncModel(model);
126+
const route = resolveImageEditApi(model);
132127

133128
// Auto-upload local files (resolve all images in parallel)
134129
const resolvedImages = await Promise.all(
@@ -142,90 +137,118 @@ export default defineCommand({
142137
"prompt-extend",
143138
);
144139

145-
// Build content: all images first, then text prompt
146-
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
147-
(u: string) => ({ image: u }),
148-
);
149-
contentItems.push({ text: prompt });
150-
151140
const watermark = resolveWatermark(flags.watermark);
152141

153-
const body: DashScopeImageRequest = {
154-
model,
155-
input: {
156-
messages: [
157-
{
158-
role: "user",
159-
content: contentItems,
160-
},
161-
],
162-
},
163-
parameters: {
164-
size: resolveImageSize(flags.size, useSync),
165-
n,
166-
seed: flags.seed,
167-
prompt_extend: promptExtend,
168-
watermark,
169-
negative_prompt: flags.negativePrompt || undefined,
170-
},
142+
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
143+
size: resolveImageSize(flags.size, route.useSync),
144+
n,
145+
seed: flags.seed,
146+
prompt_extend: promptExtend,
147+
watermark,
171148
};
172149

150+
let body: DashScopeImageRequest;
151+
if (route.inputStyle === "prompt-images") {
152+
body = {
153+
model,
154+
input: {
155+
prompt,
156+
images: resolvedImages,
157+
negative_prompt: flags.negativePrompt || undefined,
158+
},
159+
parameters,
160+
};
161+
} else {
162+
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
163+
(imageUrl: string) => ({ image: imageUrl }),
164+
);
165+
contentItems.push({ text: prompt });
166+
body = {
167+
model,
168+
input: {
169+
messages: [
170+
{
171+
role: "user",
172+
content: contentItems,
173+
},
174+
],
175+
},
176+
parameters: {
177+
...parameters,
178+
negative_prompt: flags.negativePrompt || undefined,
179+
},
180+
};
181+
}
182+
173183
// Remove undefined parameters
174184
stripUndefined(body.parameters as Record<string, unknown>);
175185

176186
const format = detectOutputFormat(settings.output);
177187

178188
if (settings.dryRun) {
179-
const previewBody = {
180-
...body,
181-
input: {
182-
messages: body.input.messages.map((message) => ({
183-
...message,
184-
content: message.content.map((item) =>
185-
item.image ? { ...item, image: redactDataUri(item.image) } : item,
186-
),
187-
})),
188-
},
189-
};
190-
emitResult({ request: previewBody, mode: useSync ? "sync" : "async" }, format);
189+
const previewBody =
190+
"messages" in body.input
191+
? {
192+
...body,
193+
input: {
194+
messages: body.input.messages.map((message) => ({
195+
...message,
196+
content: message.content.map((item) =>
197+
item.image ? { ...item, image: redactDataUri(item.image) } : item,
198+
),
199+
})),
200+
},
201+
}
202+
: {
203+
...body,
204+
input: {
205+
...body.input,
206+
images: body.input.images?.map((imageUrl) => redactDataUri(imageUrl)),
207+
},
208+
};
209+
emitResult(
210+
{ request: previewBody, mode: route.useSync ? "sync" : "async", path: route.path },
211+
format,
212+
);
191213
return;
192214
}
193215

194216
if (!settings.quiet) {
195217
process.stderr.write(
196-
`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
218+
`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
197219
);
198220
}
199221

200222
const concurrent = getConcurrency(flags);
201223

202-
if (useSync) {
203-
await handleSyncMode(ctx.client, settings, body, flags, format, concurrent);
224+
if (route.useSync) {
225+
await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
204226
} else {
205-
await handleAsyncMode(ctx.client, settings, body, flags, format, concurrent);
227+
await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
206228
}
207229
},
208230
});
209231

210232
async function handleSyncMode(
211233
client: Client,
212234
settings: Settings,
235+
path: string,
213236
body: DashScopeImageRequest,
214237
flags: EditFlags,
215238
format: OutputFormat,
216239
concurrent: number,
217240
): Promise<void> {
218241
const results = await runConcurrent(concurrent, settings, () =>
219242
client.requestJson<DashScopeImageSyncResponse>({
220-
path: imageSyncPath(),
243+
path,
221244
method: "POST",
222245
body,
223246
}),
224247
);
225248

226249
const imageUrls = results
227-
.flatMap((r) => r.output.choices || [])
228-
.flatMap((c) => c.message?.content || [])
250+
.flatMap((result) => result.output.choices || [])
251+
.flatMap((choice) => choice.message?.content || [])
229252
.map((item) => item.image)
230253
.filter(Boolean);
231254

@@ -239,6 +262,7 @@ async function handleSyncMode(
239262
async function handleAsyncMode(
240263
client: Client,
241264
settings: Settings,
265+
path: string,
242266
body: DashScopeImageRequest,
243267
flags: EditFlags,
244268
format: OutputFormat,
@@ -249,14 +273,14 @@ async function handleAsyncMode(
249273
settings,
250274
() =>
251275
client.requestJson<DashScopeAsyncResponse>({
252-
path: imagePath(),
276+
path,
253277
method: "POST",
254278
body,
255279
async: true,
256280
}),
257281
"tasks",
258282
);
259-
const taskIds = responses.map((r) => r.output.task_id);
283+
const taskIds = responses.map((response) => response.output.task_id);
260284

261285
if (flags.async) {
262286
emitResult({ task_ids: taskIds }, format);
@@ -269,12 +293,12 @@ async function handleAsyncMode(
269293
url: client.url(taskPath(taskId)),
270294
intervalSec: pollInterval,
271295
timeoutSec: settings.timeout,
272-
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
273-
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
274-
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
275-
getErrorMessage: (d) => {
276-
const o = (d as DashScopeTaskResponse).output;
277-
return o.message || o.code || undefined;
296+
isComplete: (data) => (data as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
297+
isFailed: (data) => (data as DashScopeTaskResponse).output.task_status === "FAILED",
298+
getStatus: (data) => (data as DashScopeTaskResponse).output.task_status,
299+
getErrorMessage: (data) => {
300+
const output = (data as DashScopeTaskResponse).output;
301+
return output.message || output.code || undefined;
278302
},
279303
}),
280304
);
@@ -285,13 +309,13 @@ async function handleAsyncMode(
285309
for (const result of results) {
286310
if (result.output.choices) {
287311
const urls = result.output.choices
288-
.flatMap((c) => c.message?.content || [])
312+
.flatMap((choice) => choice.message?.content || [])
289313
.map((item) => item.image)
290314
.filter(Boolean);
291315
imageUrls.push(...urls);
292316
}
293317
if (result.output.results) {
294-
const urls = result.output.results.map((r) => r.url).filter(Boolean);
318+
const urls = result.output.results.map((item) => item.url).filter(Boolean);
295319
if (urls.length > 0 && imageUrls.length === 0) {
296320
imageUrls.push(...urls);
297321
}
@@ -321,8 +345,8 @@ async function saveImages(
321345
// Parallel download all images
322346
const items =
323347
imageUrls.length > 1
324-
? imageUrls.map((url, i) => {
325-
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
348+
? imageUrls.map((url, index) => {
349+
const filename = `${prefix}_${String(index + 1).padStart(3, "0")}.png`;
326350
return { url, destPath: join(outDir, filename) };
327351
})
328352
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];

0 commit comments

Comments
 (0)