mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b7993e854 | |||
| be6ddb6126 | |||
| 25ac5c9c84 | |||
| 8ef91fe395 | |||
| 5d9e22de8f | |||
| 20e3555b84 | |||
| 81fa5b567c | |||
| df987ad536 | |||
| 4c4e7afb83 | |||
| 17c52fb86f | |||
| 7ad14a79b9 | |||
| 8211268bd8 | |||
| 58252911a8 | |||
| 36ebd63716 | |||
| dac254af86 | |||
| c4f5bb09c6 | |||
| 9a13700390 |
@@ -6,6 +6,26 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.11.2] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- MCP tools and WebSearch now provide activation guidance and direct marketplace links when Bailian reports that the corresponding service is not activated. WebSearch also guides users with legacy SSE connections to reactivate the service using Streamable HTTP.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed text chat and API Key validation compatibility failures caused by sending unsupported `enable_thinking` values. Text chat now sends the parameter only when thinking is explicitly enabled, while validation uses a compatible model without sending it.
|
||||
|
||||
## [1.11.1] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- `bl image edit` now supports `--function` for specifying edit operations with Wanx image-edit models such as `wanx2.1-imageedit`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed image generation and editing failures and incorrect size parameters for some image models, improving compatibility with Qwen-Image, Wan/Wanx, Z-Image, and dated `wanx-v1` variants.
|
||||
|
||||
## [1.11.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.11.2] - 2026-07-28
|
||||
|
||||
### 变更
|
||||
|
||||
- MCP 工具或 WebSearch 因对应服务未开通而不可用时,CLI 现在会提供开通指引和市场直达链接;对于使用旧版 SSE 连接的 WebSearch,还会提示重新开通以切换至 Streamable HTTP。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复文本对话与 API Key 登录校验因传递不受支持的 `enable_thinking` 参数值而产生的兼容性错误。文本对话仅在用户明确开启思考模式时传递该参数,登录校验则改用兼容模型且不再传递该参数。
|
||||
|
||||
## [1.11.1] - 2026-07-28
|
||||
|
||||
### 新增
|
||||
|
||||
- `bl image edit` 新增 `--function` 参数,支持为万相图片编辑模型(如 `wanx2.1-imageedit`)指定编辑功能。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复部分图片模型在图片生成与编辑时的调用失败和尺寸参数错误,并完善 Qwen-Image、Wan/Wanx、Z-Image 系列及 `wanx-v1` 日期版本的兼容性。
|
||||
|
||||
## [1.11.0] - 2026-07-28
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -20,6 +20,8 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
|
||||
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
|
||||
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
|
||||
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
|
||||
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
|
||||
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
|
||||
|
||||
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function validateAndPersistApiKey(
|
||||
const persistBaseUrl = profile.persistBaseUrl
|
||||
? normalizeModelBaseUrl(profile.persistBaseUrl)
|
||||
: undefined;
|
||||
const validationModel = profile.defaultTextModel || "qwen3.7-max";
|
||||
const validationModel = "qwen3.7-max";
|
||||
const requestOpts = {
|
||||
url: baseUrl + chatPath(),
|
||||
method: "POST",
|
||||
@@ -68,7 +68,6 @@ export async function validateAndPersistApiKey(
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
max_tokens: 1,
|
||||
stream: false,
|
||||
enable_thinking: validationModel === "qwen3.8-max-preview",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
imagePath,
|
||||
imageSyncPath,
|
||||
taskPath,
|
||||
detectOutputFormat,
|
||||
resolveOutputDir,
|
||||
@@ -20,6 +18,7 @@ import {
|
||||
BailianError,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
resolveImageEditApi,
|
||||
ASYNC_FLAG,
|
||||
CONCURRENT_FLAG,
|
||||
redactDataUri,
|
||||
@@ -32,17 +31,6 @@ import { resolveImageSize } from "bailian-cli-runtime";
|
||||
import { join } from "path";
|
||||
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
|
||||
|
||||
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
|
||||
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
|
||||
|
||||
function isSyncModel(model: string): boolean {
|
||||
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
|
||||
}
|
||||
|
||||
function enablesPromptExtendByDefault(model: string): boolean {
|
||||
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
|
||||
}
|
||||
|
||||
const EDIT_FLAGS = {
|
||||
image: {
|
||||
type: "array",
|
||||
@@ -77,6 +65,12 @@ const EDIT_FLAGS = {
|
||||
valueHint: "<text>",
|
||||
description: "Negative prompt to exclude unwanted content",
|
||||
},
|
||||
function: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description:
|
||||
"wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit",
|
||||
},
|
||||
promptExtend: {
|
||||
type: "boolean",
|
||||
valueHint: "<bool>",
|
||||
@@ -114,6 +108,8 @@ export default defineCommand({
|
||||
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
|
||||
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
|
||||
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
|
||||
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
|
||||
'--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all',
|
||||
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
|
||||
],
|
||||
async run(ctx) {
|
||||
@@ -128,7 +124,7 @@ export default defineCommand({
|
||||
const prompt = flags.prompt;
|
||||
|
||||
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
|
||||
const useSync = isSyncModel(model);
|
||||
const route = resolveImageEditApi(model);
|
||||
|
||||
// Auto-upload local files (resolve all images in parallel)
|
||||
const resolvedImages = await Promise.all(
|
||||
@@ -138,71 +134,128 @@ export default defineCommand({
|
||||
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
flags.promptExtend,
|
||||
enablesPromptExtendByDefault(model) ? true : undefined,
|
||||
route.promptExtendDefault,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
// Build content: all images first, then text prompt
|
||||
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
|
||||
(u: string) => ({ image: u }),
|
||||
);
|
||||
contentItems.push({ text: prompt });
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: contentItems,
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
size: resolveImageSize(flags.size, useSync),
|
||||
n,
|
||||
seed: flags.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size: resolveImageSize(flags.size, route.sizeProfile),
|
||||
n,
|
||||
seed: flags.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark,
|
||||
};
|
||||
|
||||
let body: DashScopeImageRequest;
|
||||
if (route.inputStyle === "function-base-image") {
|
||||
const baseImageUrl = resolvedImages[0];
|
||||
if (!baseImageUrl) {
|
||||
throw new BailianError(
|
||||
"wanx*-imageedit requires at least one --image as base_image_url.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
function: flags.function || "description_edit",
|
||||
prompt,
|
||||
base_image_url: baseImageUrl,
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
} else if (route.inputStyle === "prompt-images") {
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
prompt,
|
||||
images: resolvedImages,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
} else {
|
||||
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
|
||||
(imageUrl: string) => ({ image: imageUrl }),
|
||||
);
|
||||
contentItems.push({ text: prompt });
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: contentItems,
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
...parameters,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Remove undefined parameters
|
||||
stripUndefined(body.parameters as Record<string, unknown>);
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
const previewBody = {
|
||||
...body,
|
||||
input: {
|
||||
messages: body.input.messages.map((message) => ({
|
||||
...message,
|
||||
content: message.content.map((item) =>
|
||||
item.image ? { ...item, image: redactDataUri(item.image) } : item,
|
||||
),
|
||||
})),
|
||||
},
|
||||
};
|
||||
emitResult({ request: previewBody, mode: useSync ? "sync" : "async" }, format);
|
||||
let previewBody: DashScopeImageRequest = body;
|
||||
if ("messages" in body.input) {
|
||||
previewBody = {
|
||||
...body,
|
||||
input: {
|
||||
messages: body.input.messages.map((message) => ({
|
||||
...message,
|
||||
content: message.content.map((item) =>
|
||||
item.image ? { ...item, image: redactDataUri(item.image) } : item,
|
||||
),
|
||||
})),
|
||||
},
|
||||
};
|
||||
} else if ("images" in body.input) {
|
||||
previewBody = {
|
||||
...body,
|
||||
input: {
|
||||
...body.input,
|
||||
images: body.input.images?.map((imageUrl) => redactDataUri(imageUrl)),
|
||||
},
|
||||
};
|
||||
} else if ("base_image_url" in body.input) {
|
||||
previewBody = {
|
||||
...body,
|
||||
input: {
|
||||
...body.input,
|
||||
base_image_url: redactDataUri(body.input.base_image_url),
|
||||
mask_image_url: body.input.mask_image_url
|
||||
? redactDataUri(body.input.mask_image_url)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
emitResult(
|
||||
{ request: previewBody, mode: route.useSync ? "sync" : "async", path: route.path },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.quiet) {
|
||||
process.stderr.write(
|
||||
`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
|
||||
`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const concurrent = getConcurrency(flags);
|
||||
|
||||
if (useSync) {
|
||||
await handleSyncMode(ctx.client, settings, body, flags, format, concurrent);
|
||||
if (route.useSync) {
|
||||
await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
|
||||
} else {
|
||||
await handleAsyncMode(ctx.client, settings, body, flags, format, concurrent);
|
||||
await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -210,6 +263,7 @@ export default defineCommand({
|
||||
async function handleSyncMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
path: string,
|
||||
body: DashScopeImageRequest,
|
||||
flags: EditFlags,
|
||||
format: OutputFormat,
|
||||
@@ -217,15 +271,15 @@ async function handleSyncMode(
|
||||
): Promise<void> {
|
||||
const results = await runConcurrent(concurrent, settings, () =>
|
||||
client.requestJson<DashScopeImageSyncResponse>({
|
||||
path: imageSyncPath(),
|
||||
path,
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
);
|
||||
|
||||
const imageUrls = results
|
||||
.flatMap((r) => r.output.choices || [])
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((result) => result.output.choices || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -239,6 +293,7 @@ async function handleSyncMode(
|
||||
async function handleAsyncMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
path: string,
|
||||
body: DashScopeImageRequest,
|
||||
flags: EditFlags,
|
||||
format: OutputFormat,
|
||||
@@ -249,14 +304,14 @@ async function handleAsyncMode(
|
||||
settings,
|
||||
() =>
|
||||
client.requestJson<DashScopeAsyncResponse>({
|
||||
path: imagePath(),
|
||||
path,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
}),
|
||||
"tasks",
|
||||
);
|
||||
const taskIds = responses.map((r) => r.output.task_id);
|
||||
const taskIds = responses.map((response) => response.output.task_id);
|
||||
|
||||
if (flags.async) {
|
||||
emitResult({ task_ids: taskIds }, format);
|
||||
@@ -269,12 +324,12 @@ async function handleAsyncMode(
|
||||
url: client.url(taskPath(taskId)),
|
||||
intervalSec: pollInterval,
|
||||
timeoutSec: settings.timeout,
|
||||
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
|
||||
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
|
||||
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
|
||||
getErrorMessage: (d) => {
|
||||
const o = (d as DashScopeTaskResponse).output;
|
||||
return o.message || o.code || undefined;
|
||||
isComplete: (data) => (data as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
|
||||
isFailed: (data) => (data as DashScopeTaskResponse).output.task_status === "FAILED",
|
||||
getStatus: (data) => (data as DashScopeTaskResponse).output.task_status,
|
||||
getErrorMessage: (data) => {
|
||||
const output = (data as DashScopeTaskResponse).output;
|
||||
return output.message || output.code || undefined;
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -285,13 +340,13 @@ async function handleAsyncMode(
|
||||
for (const result of results) {
|
||||
if (result.output.choices) {
|
||||
const urls = result.output.choices
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
imageUrls.push(...urls);
|
||||
}
|
||||
if (result.output.results) {
|
||||
const urls = result.output.results.map((r) => r.url).filter(Boolean);
|
||||
const urls = result.output.results.map((item) => item.url).filter(Boolean);
|
||||
if (urls.length > 0 && imageUrls.length === 0) {
|
||||
imageUrls.push(...urls);
|
||||
}
|
||||
@@ -321,8 +376,8 @@ async function saveImages(
|
||||
// Parallel download all images
|
||||
const items =
|
||||
imageUrls.length > 1
|
||||
? imageUrls.map((url, i) => {
|
||||
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
|
||||
? imageUrls.map((url, index) => {
|
||||
const filename = `${prefix}_${String(index + 1).padStart(3, "0")}.png`;
|
||||
return { url, destPath: join(outDir, filename) };
|
||||
})
|
||||
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
imagePath,
|
||||
imageSyncPath,
|
||||
taskPath,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
@@ -19,6 +17,7 @@ import {
|
||||
generateFilename,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
resolveImageGenerateApi,
|
||||
ASYNC_FLAG,
|
||||
CONCURRENT_FLAG,
|
||||
} from "bailian-cli-core";
|
||||
@@ -31,18 +30,6 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai
|
||||
|
||||
import { join } from "path";
|
||||
|
||||
// Qwen-Image 2.0 and Wan 2.7 use the sync multimodal-generation endpoint.
|
||||
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
|
||||
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
|
||||
|
||||
function isSyncModel(model: string): boolean {
|
||||
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
|
||||
}
|
||||
|
||||
function enablesPromptExtendByDefault(model: string): boolean {
|
||||
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
|
||||
}
|
||||
|
||||
const GENERATE_FLAGS = {
|
||||
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
|
||||
model: {
|
||||
@@ -109,6 +96,8 @@ export default defineCommand({
|
||||
'--prompt "Logo" --watermark false',
|
||||
'--prompt "An alien in the space" --watermark false',
|
||||
'--prompt "sunset" --model wan2.6-t2i --async --quiet',
|
||||
'--prompt "plush doll" --model z-image-turbo --size 1024*1024',
|
||||
'--prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024',
|
||||
'--prompt "Pro quality" --model qwen-image-2.0-pro',
|
||||
'--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
|
||||
],
|
||||
@@ -117,73 +106,90 @@ export default defineCommand({
|
||||
const prompt = flags.prompt;
|
||||
|
||||
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
|
||||
const useSync = isSyncModel(model);
|
||||
const defaultSize = useSync ? "1:1" : "1:1";
|
||||
const route = resolveImageGenerateApi(model);
|
||||
const defaultSize = "1:1";
|
||||
const sizeInput = flags.size || defaultSize;
|
||||
const size = resolveImageSize(sizeInput, useSync);
|
||||
const size = resolveImageSize(sizeInput, route.sizeProfile);
|
||||
const n = flags.n ?? 1;
|
||||
const concurrent = getConcurrency(flags);
|
||||
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
flags.promptExtend,
|
||||
enablesPromptExtendByDefault(model) ? true : undefined,
|
||||
route.promptExtendDefault,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content: [{ text: prompt }] }],
|
||||
},
|
||||
parameters: {
|
||||
size,
|
||||
n,
|
||||
seed: flags.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size,
|
||||
n,
|
||||
seed: flags.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark,
|
||||
};
|
||||
|
||||
const body: DashScopeImageRequest =
|
||||
route.inputStyle === "prompt"
|
||||
? {
|
||||
model,
|
||||
input: {
|
||||
prompt,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
parameters,
|
||||
}
|
||||
: {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content: [{ text: prompt }] }],
|
||||
},
|
||||
parameters: {
|
||||
...parameters,
|
||||
negative_prompt: flags.negativePrompt || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ request: body, mode: useSync ? "sync" : "async" }, format);
|
||||
emitResult(
|
||||
{ request: body, mode: route.useSync ? "sync" : "async", path: route.path },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.quiet) {
|
||||
process.stderr.write(`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}]\n`);
|
||||
process.stderr.write(`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}]\n`);
|
||||
}
|
||||
|
||||
if (useSync) {
|
||||
await handleSyncMode(ctx.client, settings, model, body, flags, format, concurrent);
|
||||
if (route.useSync) {
|
||||
await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
|
||||
} else {
|
||||
await handleAsyncMode(ctx.client, settings, model, body, flags, format, concurrent);
|
||||
await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ---- Sync mode: qwen-image-2.0 series ----
|
||||
// ---- Sync mode: qwen-image / wan2.7-image / z-image ----
|
||||
|
||||
async function handleSyncMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
_model: string,
|
||||
path: string,
|
||||
body: DashScopeImageRequest,
|
||||
flags: GenerateFlags,
|
||||
format: string,
|
||||
concurrent: number,
|
||||
): Promise<void> {
|
||||
const results = await runConcurrent(concurrent, settings, () =>
|
||||
client.requestJson<DashScopeImageSyncResponse>({ path: imageSyncPath(), method: "POST", body }),
|
||||
client.requestJson<DashScopeImageSyncResponse>({ path, method: "POST", body }),
|
||||
);
|
||||
|
||||
const imageUrls = results
|
||||
.flatMap((r) => r.output.choices || [])
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((result) => result.output.choices || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -194,12 +200,12 @@ async function handleSyncMode(
|
||||
await saveImages(imageUrls, flags, settings, format);
|
||||
}
|
||||
|
||||
// ---- Async mode: wan2.x / qwen-image-plus ----
|
||||
// ---- Async mode: wan2.6-t2i / wan2.6-image / legacy text2image ----
|
||||
|
||||
async function handleAsyncMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
_model: string,
|
||||
path: string,
|
||||
body: DashScopeImageRequest,
|
||||
flags: GenerateFlags,
|
||||
format: string,
|
||||
@@ -210,14 +216,14 @@ async function handleAsyncMode(
|
||||
settings,
|
||||
() =>
|
||||
client.requestJson<DashScopeAsyncResponse>({
|
||||
path: imagePath(),
|
||||
path,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
}),
|
||||
"tasks",
|
||||
);
|
||||
const taskIds = responses.map((r) => r.output.task_id);
|
||||
const taskIds = responses.map((response) => response.output.task_id);
|
||||
|
||||
// --async: return all task IDs immediately
|
||||
if (flags.async) {
|
||||
@@ -234,12 +240,12 @@ async function handleAsyncMode(
|
||||
url: pollUrl,
|
||||
intervalSec: pollInterval,
|
||||
timeoutSec: settings.timeout,
|
||||
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
|
||||
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
|
||||
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
|
||||
getErrorMessage: (d) => {
|
||||
const o = (d as DashScopeTaskResponse).output;
|
||||
return o.message || o.code || undefined;
|
||||
isComplete: (data) => (data as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
|
||||
isFailed: (data) => (data as DashScopeTaskResponse).output.task_status === "FAILED",
|
||||
getStatus: (data) => (data as DashScopeTaskResponse).output.task_status,
|
||||
getErrorMessage: (data) => {
|
||||
const output = (data as DashScopeTaskResponse).output;
|
||||
return output.message || output.code || undefined;
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -250,13 +256,13 @@ async function handleAsyncMode(
|
||||
for (const result of results) {
|
||||
if (result.output.choices) {
|
||||
const urls = result.output.choices
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
imageUrls.push(...urls);
|
||||
}
|
||||
if (result.output.results) {
|
||||
const urls = result.output.results.map((r) => r.url).filter(Boolean);
|
||||
const urls = result.output.results.map((item) => item.url).filter(Boolean);
|
||||
if (urls.length > 0 && imageUrls.length === 0) {
|
||||
imageUrls.push(...urls);
|
||||
}
|
||||
@@ -298,8 +304,8 @@ async function saveImages(
|
||||
// Parallel download all images
|
||||
const items =
|
||||
imageUrls.length > 1
|
||||
? imageUrls.map((url, i) => {
|
||||
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
|
||||
? imageUrls.map((url, index) => {
|
||||
const filename = `${prefix}_${String(index + 1).padStart(3, "0")}.png`;
|
||||
return { url, destPath: join(outDir, filename) };
|
||||
})
|
||||
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BailianError } from "bailian-cli-core";
|
||||
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
|
||||
|
||||
/** Detect MCP-not-activated / invalid 404 errors (CLI-wrapped server message). */
|
||||
export function isMcpNotActivated(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
const message = error.message;
|
||||
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
|
||||
return /未开通|MCP不存在|MCP_IS_INVALID/i.test(message);
|
||||
}
|
||||
|
||||
/** Activation hint; URL from runtime/urls.ts. */
|
||||
export function mcpActivateHint(serverCode: string): string {
|
||||
const lines = [
|
||||
`Activate (or re-activate) the ${serverCode} MCP in the Bailian MCP marketplace, then retry.`,
|
||||
];
|
||||
if (serverCode === "WebSearch") {
|
||||
lines.push(
|
||||
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
|
||||
);
|
||||
}
|
||||
lines.push(`Open: ${mcpMarketplaceDetailPage(serverCode)}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* For not-activated errors, keep the original message / exitCode and append a hint only.
|
||||
* Do not replace the server error message.
|
||||
*/
|
||||
export function rethrowWithMcpActivateHint(error: unknown, serverCode: string): never {
|
||||
if (isMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
|
||||
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||
|
||||
const CALL_FLAGS = {
|
||||
target: {
|
||||
@@ -130,14 +131,21 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
await client.initialize();
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
try {
|
||||
await client.initialize();
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
|
||||
if (result.isError) {
|
||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||
throw new BailianError(`Tool error: ${errText}`);
|
||||
if (result.isError) {
|
||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||
throw new BailianError(`Tool error: ${errText}`);
|
||||
}
|
||||
|
||||
emitResult(result, format);
|
||||
} catch (error) {
|
||||
if (!flags.url) {
|
||||
rethrowWithMcpActivateHint(error, serverCode);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
emitResult(result, format);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: "List tools exposed by an MCP server (tools/list)",
|
||||
@@ -36,8 +37,15 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ server: code, url, tools }, format);
|
||||
try {
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ server: code, url, tools }, format);
|
||||
} catch (error) {
|
||||
if (!flags.url) {
|
||||
rethrowWithMcpActivateHint(error, code);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
isMcpNotActivated,
|
||||
mcpActivateHint,
|
||||
rethrowWithMcpActivateHint,
|
||||
} from "../mcp/activate-hint.ts";
|
||||
|
||||
/** Detect WebSearch MCP not-activated / invalid 404 errors. */
|
||||
export const isWebSearchMcpNotActivated = isMcpNotActivated;
|
||||
|
||||
/** WebSearch activation hint. */
|
||||
export function webSearchActivateHint(): string {
|
||||
return mcpActivateHint("WebSearch");
|
||||
}
|
||||
|
||||
/** Keep the original message; append a hint for WebSearch not-activated errors. */
|
||||
export function rethrowWithWebSearchActivateHint(error: unknown): never {
|
||||
rethrowWithMcpActivateHint(error, "WebSearch");
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
mcpWebSearchPath,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { createSpinner } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { createSpinner, emitResult } from "bailian-cli-runtime";
|
||||
import { rethrowWithWebSearchActivateHint } from "./web-activate-hint.ts";
|
||||
|
||||
const WEB_SEARCH_FLAGS = {
|
||||
query: { type: "string", valueHint: "<text>", description: "Search query text" },
|
||||
@@ -41,11 +41,14 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(mcpWebSearchPath());
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
|
||||
emitResult({ tools }, format);
|
||||
try {
|
||||
const client = ctx.client.mcp(mcpWebSearchPath());
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ tools }, format);
|
||||
} catch (error) {
|
||||
rethrowWithWebSearchActivateHint(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ export default defineCommand({
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop("Failed.");
|
||||
throw error;
|
||||
rethrowWithWebSearchActivateHint(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,11 +149,6 @@ export default defineCommand({
|
||||
if (flags.thinkingBudget !== undefined) {
|
||||
body.thinking_budget = flags.thinkingBudget;
|
||||
}
|
||||
} else if (!shouldStream) {
|
||||
// DashScope qwen3 models default to enable_thinking=true server-side, but
|
||||
// non-streaming calls require it to be explicitly false. Stream calls
|
||||
// support thinking, so leave the field unset there (server handles it).
|
||||
body.enable_thinking = false;
|
||||
}
|
||||
|
||||
if (flags.tool) {
|
||||
|
||||
@@ -219,7 +219,6 @@ describe("e2e: auth", () => {
|
||||
body: {
|
||||
model: "qwen3.7-max",
|
||||
stream: false,
|
||||
enable_thinking: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -315,9 +314,8 @@ describe("e2e: auth", () => {
|
||||
authorization: "Bearer sk-sp-e2e-placeholder",
|
||||
sourceConfig: expect.any(String),
|
||||
body: {
|
||||
model: "qwen3.8-max-preview",
|
||||
model: "qwen3.7-max",
|
||||
stream: false,
|
||||
enable_thinking: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -96,6 +96,78 @@ describe("e2e: image edit", () => {
|
||||
"data:image/png;base64,<omitted>",
|
||||
);
|
||||
});
|
||||
|
||||
test("wan2.5-i2i-preview dry-run 走 prompt+images", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"edit",
|
||||
"--model",
|
||||
"wan2.5-i2i-preview",
|
||||
"--image",
|
||||
"https://example.com/source.png",
|
||||
"--prompt",
|
||||
"Place on a table",
|
||||
"--size",
|
||||
"1:1",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
mode?: string;
|
||||
path?: string;
|
||||
request?: {
|
||||
input?: { prompt?: string; images?: string[]; messages?: unknown };
|
||||
parameters?: { size?: string };
|
||||
};
|
||||
}>(stdout);
|
||||
expect(data.mode).toBe("async");
|
||||
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
|
||||
expect(data.request?.input?.prompt).toBe("Place on a table");
|
||||
expect(data.request?.input?.images).toEqual(["https://example.com/source.png"]);
|
||||
expect(data.request?.input?.messages).toBeUndefined();
|
||||
expect(data.request?.parameters?.size).toBe("1280*1280");
|
||||
});
|
||||
|
||||
test("wanx2.1-imageedit dry-run 走 function + base_image_url", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"edit",
|
||||
"--model",
|
||||
"wanx2.1-imageedit",
|
||||
"--image",
|
||||
"https://example.com/source.png",
|
||||
"--prompt",
|
||||
"转换成绘本风格",
|
||||
"--function",
|
||||
"stylization_all",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
mode?: string;
|
||||
path?: string;
|
||||
request?: {
|
||||
input?: {
|
||||
function?: string;
|
||||
prompt?: string;
|
||||
base_image_url?: string;
|
||||
images?: unknown;
|
||||
messages?: unknown;
|
||||
};
|
||||
};
|
||||
}>(stdout);
|
||||
expect(data.mode).toBe("async");
|
||||
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
|
||||
expect(data.request?.input?.function).toBe("stylization_all");
|
||||
expect(data.request?.input?.prompt).toBe("转换成绘本风格");
|
||||
expect(data.request?.input?.base_image_url).toBe("https://example.com/source.png");
|
||||
expect(data.request?.input?.images).toBeUndefined();
|
||||
expect(data.request?.input?.messages).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: image edit", () => {
|
||||
|
||||
@@ -61,6 +61,137 @@ describe("e2e: image generate", () => {
|
||||
expect(data.mode).toBe("sync");
|
||||
expect(data.request?.model).toBe("wan2.7-image");
|
||||
});
|
||||
|
||||
test("z-image-turbo dry-run 走 sync multimodal", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"z-image-turbo",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--size",
|
||||
"1024*1024",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ mode?: string; request?: { model?: string } }>(stdout);
|
||||
expect(data.mode).toBe("sync");
|
||||
expect(data.request?.model).toBe("z-image-turbo");
|
||||
});
|
||||
|
||||
test("qwen-image-plus dry-run 走 sync multimodal", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-plus",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--size",
|
||||
"1328*1328",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
mode?: string;
|
||||
path?: string;
|
||||
request?: { model?: string };
|
||||
}>(stdout);
|
||||
expect(data.mode).toBe("sync");
|
||||
expect(data.path).toBe("/api/v1/services/aigc/multimodal-generation/generation");
|
||||
expect(data.request?.model).toBe("qwen-image-plus");
|
||||
});
|
||||
|
||||
test("qwen-image-plus 默认 1:1 映射为 1328*1328", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-plus",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
|
||||
}>(stdout);
|
||||
expect(data.request?.parameters?.size).toBe("1328*1328");
|
||||
});
|
||||
|
||||
test("z-image-turbo 默认 prompt_extend 为 false", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"z-image-turbo",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
|
||||
}>(stdout);
|
||||
expect(data.request?.parameters?.prompt_extend).toBe(false);
|
||||
expect(data.request?.parameters?.size).toBe("1024*1024");
|
||||
});
|
||||
|
||||
test("wanx-v1 默认 1:1 映射为 1024*1024", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"wanx-v1",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
request?: { parameters?: { size?: string }; input?: { prompt?: string } };
|
||||
}>(stdout);
|
||||
expect(data.request?.parameters?.size).toBe("1024*1024");
|
||||
expect(data.request?.input?.prompt).toBe("一只猫");
|
||||
});
|
||||
|
||||
test("wanx2.0-t2i-turbo dry-run 走 text2image prompt 路径", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"wanx2.0-t2i-turbo",
|
||||
"--prompt",
|
||||
"一只猫",
|
||||
"--size",
|
||||
"1024*1024",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
mode?: string;
|
||||
path?: string;
|
||||
request?: { model?: string; input?: { prompt?: string; messages?: unknown } };
|
||||
}>(stdout);
|
||||
expect(data.mode).toBe("async");
|
||||
expect(data.path).toBe("/api/v1/services/aigc/text2image/image-synthesis");
|
||||
expect(data.request?.model).toBe("wanx2.0-t2i-turbo");
|
||||
expect(data.request?.input?.prompt).toBe("一只猫");
|
||||
expect(data.request?.input?.messages).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
|
||||
import {
|
||||
isMcpNotActivated,
|
||||
mcpActivateHint,
|
||||
rethrowWithMcpActivateHint,
|
||||
} from "../src/commands/mcp/activate-hint.ts";
|
||||
|
||||
describe("mcp-activate-hint", () => {
|
||||
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("裸 404 或非 MCP 错误不加开通判定", () => {
|
||||
expect(isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(false);
|
||||
expect(isMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
|
||||
});
|
||||
|
||||
test("hint 含对应 server 的 MCP 广场深链", () => {
|
||||
const serverCode = "market-cmapi00073529";
|
||||
expect(mcpActivateHint(serverCode)).toContain(mcpMarketplaceDetailPage(serverCode));
|
||||
expect(mcpActivateHint(serverCode)).toMatch(/Activate|re-activate/i);
|
||||
});
|
||||
|
||||
test("WebSearch hint 含 SSE 升级说明", () => {
|
||||
expect(mcpActivateHint("WebSearch")).toMatch(/SSE|Streamable HTTP/i);
|
||||
});
|
||||
|
||||
test("rethrow 保留原 message,补 hint", () => {
|
||||
const serverCode = "market-cmapi00073529";
|
||||
const original = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(original, serverCode);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(BailianError);
|
||||
const wrapped = error as BailianError;
|
||||
expect(wrapped.message).toBe(original.message);
|
||||
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(wrapped.hint).toContain(mcpMarketplaceDetailPage(serverCode));
|
||||
expect(wrapped.cause).toBe(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("已有 hint 或非未开通错误原样抛出", () => {
|
||||
const withHint = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
"already hinted",
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(withHint, "WebSearch");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(withHint);
|
||||
}
|
||||
|
||||
const other = new BailianError("MCP request failed: 401 Unauthorized");
|
||||
try {
|
||||
rethrowWithMcpActivateHint(other, "WebSearch");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(other);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { MCP_WEBSEARCH_PAGE } from "bailian-cli-runtime";
|
||||
import {
|
||||
isWebSearchMcpNotActivated,
|
||||
rethrowWithWebSearchActivateHint,
|
||||
webSearchActivateHint,
|
||||
} from "../src/commands/search/web-activate-hint.ts";
|
||||
|
||||
describe("web-activate-hint", () => {
|
||||
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
|
||||
expect(
|
||||
isWebSearchMcpNotActivated(
|
||||
new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebSearchMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebSearchMcpNotActivated(
|
||||
new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("裸 404 或非 MCP 错误不加开通判定", () => {
|
||||
expect(isWebSearchMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isWebSearchMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed")),
|
||||
).toBe(false);
|
||||
expect(isWebSearchMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
|
||||
});
|
||||
|
||||
test("hint 含 MCP 广场 WebSearch 深链", () => {
|
||||
expect(webSearchActivateHint()).toContain(MCP_WEBSEARCH_PAGE);
|
||||
expect(webSearchActivateHint()).toMatch(/Activate|re-activate/i);
|
||||
});
|
||||
|
||||
test("rethrow 保留原 message,补 Hint", () => {
|
||||
const original = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
try {
|
||||
rethrowWithWebSearchActivateHint(original);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(BailianError);
|
||||
const wrapped = error as BailianError;
|
||||
expect(wrapped.message).toBe(original.message);
|
||||
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(wrapped.hint).toContain(MCP_WEBSEARCH_PAGE);
|
||||
expect(wrapped.cause).toBe(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("已有 hint 或非未开通错误原样抛出", () => {
|
||||
const withHint = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
"already hinted",
|
||||
);
|
||||
try {
|
||||
rethrowWithWebSearchActivateHint(withHint);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(withHint);
|
||||
}
|
||||
|
||||
const other = new BailianError("MCP request failed: 401 Unauthorized");
|
||||
try {
|
||||
rethrowWithWebSearchActivateHint(other);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(other);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -7,15 +7,26 @@ export function chatPath(): string {
|
||||
}
|
||||
|
||||
// ---- Image Generation (DashScope) ----
|
||||
/** Async image API used by wan2.6-t2i / wan2.6-image (T2I) and similar message-format models. */
|
||||
export function imagePath(): string {
|
||||
return "/api/v1/services/aigc/image-generation/generation";
|
||||
}
|
||||
|
||||
// Synchronous image generation (qwen-image-2.0 / qwen-image-max series)
|
||||
/** Sync multimodal API (qwen-image / wan2.7-image / z-image generate; also wan2.6-image edit). */
|
||||
export function imageSyncPath(): string {
|
||||
return "/api/v1/services/aigc/multimodal-generation/generation";
|
||||
}
|
||||
|
||||
/** Legacy async text-to-image API (wan2.5/2.2/2.1-t2i, wanx-*-t2i). */
|
||||
export function imageText2ImagePath(): string {
|
||||
return "/api/v1/services/aigc/text2image/image-synthesis";
|
||||
}
|
||||
|
||||
/** Legacy async image-to-image / edit API (wan2.5-i2i, *imageedit*). */
|
||||
export function image2ImagePath(): string {
|
||||
return "/api/v1/services/aigc/image2image/image-synthesis";
|
||||
}
|
||||
|
||||
// ---- Video Generation (DashScope) ----
|
||||
export function videoGeneratePath(): string {
|
||||
return "/api/v1/services/aigc/video-generation/video-synthesis";
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { image2ImagePath, imagePath, imageSyncPath, imageText2ImagePath } from "./endpoints.ts";
|
||||
|
||||
/**
|
||||
* DashScope image APIs differ by model family:
|
||||
*
|
||||
* Generate (T2I):
|
||||
* - sync multimodal + messages: qwen-image*, wan2.7-image*, z-image*
|
||||
* - async image-generation + messages: wan2.6-t2i*, wan2.6-image*
|
||||
* (wan2.6-image sync multimodal requires 1–4 images, so pure T2I must be async)
|
||||
* - async text2image + prompt: wan2.5/2.2/2.1-t2i*, wanx*-t2i*
|
||||
*
|
||||
* Edit (I2I):
|
||||
* - sync multimodal + messages(+images): qwen-image-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
|
||||
* (pure T2I models such as z-image / qwen-image-plus / qwen-image-max are NOT edit models)
|
||||
* - async image2image + prompt/images: wan2.5-i2i*
|
||||
* - async image2image + function/base_image_url: *imageedit* (e.g. wanx2.1-imageedit)
|
||||
* - async image-generation + messages(+images): other async fallbacks
|
||||
*/
|
||||
|
||||
export type ImageApiKind =
|
||||
| "sync-multimodal"
|
||||
| "async-image-generation"
|
||||
| "async-text2image"
|
||||
| "async-image2image";
|
||||
|
||||
/** Model-family size presets — not inferred from sync/async. */
|
||||
export type ImageSizeProfile =
|
||||
| "qwen-image-2.0"
|
||||
| "qwen-image-fixed"
|
||||
| "wan27"
|
||||
| "z-image"
|
||||
| "wan26"
|
||||
| "wan-legacy"
|
||||
| "wanx-v1"
|
||||
| "wan25-i2i";
|
||||
|
||||
export type ImageInputStyle = "messages" | "prompt" | "prompt-images" | "function-base-image";
|
||||
|
||||
export interface ImageApiRoute {
|
||||
kind: ImageApiKind;
|
||||
path: string;
|
||||
/** True when the call is synchronous (no X-DashScope-Async / task poll). */
|
||||
useSync: boolean;
|
||||
/** How to shape `input` in the request body. */
|
||||
inputStyle: ImageInputStyle;
|
||||
/** Ratio → pixel map family for `--size`. */
|
||||
sizeProfile: ImageSizeProfile;
|
||||
/**
|
||||
* CLI default when `--prompt-extend` is omitted.
|
||||
* `undefined` means omit the parameter (leave to DashScope default).
|
||||
*/
|
||||
promptExtendDefault?: boolean;
|
||||
}
|
||||
|
||||
/** Models that accept text-only sync multimodal for generate. */
|
||||
const SYNC_GENERATE_PREFIXES = ["qwen-image", "wan2.7-image", "z-image"] as const;
|
||||
|
||||
/**
|
||||
* Models that support sync multimodal edit (messages must include images).
|
||||
* Pure T2I models (z-image / qwen-image-plus / qwen-image-max) are excluded.
|
||||
*/
|
||||
const SYNC_EDIT_PREFIXES = [
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-edit",
|
||||
"wan2.7-image",
|
||||
"wan2.6-image",
|
||||
] as const;
|
||||
|
||||
function startsWithAny(model: string, prefixes: readonly string[]): boolean {
|
||||
return prefixes.some((prefix) => model.startsWith(prefix));
|
||||
}
|
||||
|
||||
/** True when the model family can use sync multimodal for generate. */
|
||||
export function isSyncMultimodalImageModel(model: string): boolean {
|
||||
return startsWithAny(model, SYNC_GENERATE_PREFIXES) || model.startsWith("wan2.6-image");
|
||||
}
|
||||
|
||||
function isSyncGenerateModel(model: string): boolean {
|
||||
return startsWithAny(model, SYNC_GENERATE_PREFIXES);
|
||||
}
|
||||
|
||||
function isSyncEditModel(model: string): boolean {
|
||||
return startsWithAny(model, SYNC_EDIT_PREFIXES);
|
||||
}
|
||||
|
||||
/** wanx-v1 and dated aliases (e.g. wanx-v1-0521) use the legacy text2image API. */
|
||||
function isWanxV1Model(model: string): boolean {
|
||||
return /^wanx-v1(?:-|$)/i.test(model);
|
||||
}
|
||||
|
||||
/** wan2.5 / wan2.2 / wan2.1 / wanx text-to-image models use the legacy prompt API. */
|
||||
export function isLegacyText2ImageModel(model: string): boolean {
|
||||
if (model.startsWith("wan2.6-t2i") || model.startsWith("wan2.6-image")) return false;
|
||||
if (isSyncGenerateModel(model)) return false;
|
||||
if (/^wan2\.[0-5][^-]*-t2i/i.test(model)) return true;
|
||||
if (isWanxV1Model(model)) return true;
|
||||
if (/^wanx/i.test(model) && /t2i|text2image/i.test(model)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** wan2.5-i2i uses the legacy image2image prompt+images API. */
|
||||
export function isLegacyImage2ImageModel(model: string): boolean {
|
||||
return /wan2\.5-i2i/i.test(model);
|
||||
}
|
||||
|
||||
/** wanx*-imageedit uses function + base_image_url (not prompt+images). */
|
||||
export function isWanxFunctionImageEditModel(model: string): boolean {
|
||||
return /imageedit/i.test(model);
|
||||
}
|
||||
|
||||
export function resolveImageSizeProfile(model: string): ImageSizeProfile {
|
||||
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-edit")) {
|
||||
return "qwen-image-2.0";
|
||||
}
|
||||
// Remaining qwen-image* (plus / max / bare qwen-image) share the fixed table.
|
||||
if (model.startsWith("qwen-image")) {
|
||||
return "qwen-image-fixed";
|
||||
}
|
||||
if (model.startsWith("wan2.7-image")) return "wan27";
|
||||
if (model.startsWith("z-image")) return "z-image";
|
||||
if (
|
||||
model.startsWith("wan2.6-t2i") ||
|
||||
model.startsWith("wan2.6-image") ||
|
||||
model.startsWith("wan2.5-t2i")
|
||||
) {
|
||||
return "wan26";
|
||||
}
|
||||
if (isWanxV1Model(model)) return "wanx-v1";
|
||||
if (/wan2\.5-i2i/i.test(model)) return "wan25-i2i";
|
||||
if (isLegacyText2ImageModel(model)) return "wan-legacy";
|
||||
return "wan26";
|
||||
}
|
||||
|
||||
/** Official / CLI defaults for prompt_extend when the flag is omitted. */
|
||||
export function resolvePromptExtendDefault(model: string): boolean | undefined {
|
||||
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-max")) return true;
|
||||
// Z-Image docs default prompt_extend to false.
|
||||
if (model.startsWith("z-image")) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildRoute(
|
||||
partial: Omit<ImageApiRoute, "sizeProfile" | "promptExtendDefault">,
|
||||
model: string,
|
||||
): ImageApiRoute {
|
||||
return {
|
||||
...partial,
|
||||
sizeProfile: resolveImageSizeProfile(model),
|
||||
promptExtendDefault: resolvePromptExtendDefault(model),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveImageGenerateApi(model: string): ImageApiRoute {
|
||||
if (isSyncGenerateModel(model)) {
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "sync-multimodal",
|
||||
path: imageSyncPath(),
|
||||
useSync: true,
|
||||
inputStyle: "messages",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
if (isLegacyText2ImageModel(model)) {
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "async-text2image",
|
||||
path: imageText2ImagePath(),
|
||||
useSync: false,
|
||||
inputStyle: "prompt",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
// Includes wan2.6-t2i* and wan2.6-image* (text-only generate).
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "async-image-generation",
|
||||
path: imagePath(),
|
||||
useSync: false,
|
||||
inputStyle: "messages",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveImageEditApi(model: string): ImageApiRoute {
|
||||
if (isSyncEditModel(model)) {
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "sync-multimodal",
|
||||
path: imageSyncPath(),
|
||||
useSync: true,
|
||||
inputStyle: "messages",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
if (isWanxFunctionImageEditModel(model)) {
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "async-image2image",
|
||||
path: image2ImagePath(),
|
||||
useSync: false,
|
||||
inputStyle: "function-base-image",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
if (isLegacyImage2ImageModel(model)) {
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "async-image2image",
|
||||
path: image2ImagePath(),
|
||||
useSync: false,
|
||||
inputStyle: "prompt-images",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
return buildRoute(
|
||||
{
|
||||
kind: "async-image-generation",
|
||||
path: imagePath(),
|
||||
useSync: false,
|
||||
inputStyle: "messages",
|
||||
},
|
||||
model,
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ export {
|
||||
chatPath,
|
||||
imagePath,
|
||||
imageSyncPath,
|
||||
imageText2ImagePath,
|
||||
image2ImagePath,
|
||||
knowledgeChatEndpoint,
|
||||
knowledgeRetrievePath,
|
||||
knowledgeSearchEndpoint,
|
||||
@@ -18,6 +20,20 @@ export {
|
||||
userProfilePath,
|
||||
videoGeneratePath,
|
||||
} from "./endpoints.ts";
|
||||
export {
|
||||
isLegacyImage2ImageModel,
|
||||
isLegacyText2ImageModel,
|
||||
isSyncMultimodalImageModel,
|
||||
isWanxFunctionImageEditModel,
|
||||
resolveImageEditApi,
|
||||
resolveImageGenerateApi,
|
||||
resolveImageSizeProfile,
|
||||
resolvePromptExtendDefault,
|
||||
type ImageApiKind,
|
||||
type ImageApiRoute,
|
||||
type ImageInputStyle,
|
||||
type ImageSizeProfile,
|
||||
} from "./image-routes.ts";
|
||||
export { CHANNEL, SOURCE_CONFIG, TAGS, trackingHeaders } from "./headers.ts";
|
||||
export type { HttpDeps, RequestOpts } from "./http.ts";
|
||||
export { request, requestJson } from "./http.ts";
|
||||
|
||||
@@ -112,12 +112,26 @@ export interface StreamChunk {
|
||||
|
||||
export interface DashScopeImageRequest {
|
||||
model: string;
|
||||
input: {
|
||||
messages: Array<{
|
||||
role: "user";
|
||||
content: Array<{ text?: string; image?: string }>;
|
||||
}>;
|
||||
};
|
||||
input:
|
||||
| {
|
||||
messages: Array<{
|
||||
role: "user";
|
||||
content: Array<{ text?: string; image?: string }>;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
prompt: string;
|
||||
/** Required by image2image models such as wan2.5-i2i-preview. */
|
||||
images?: string[];
|
||||
negative_prompt?: string;
|
||||
}
|
||||
| {
|
||||
/** Required by wanx*-imageedit models. */
|
||||
function: string;
|
||||
prompt: string;
|
||||
base_image_url: string;
|
||||
mask_image_url?: string;
|
||||
};
|
||||
parameters?: {
|
||||
size?: string;
|
||||
n?: number;
|
||||
@@ -125,6 +139,7 @@ export interface DashScopeImageRequest {
|
||||
prompt_extend?: boolean;
|
||||
watermark?: boolean;
|
||||
negative_prompt?: string;
|
||||
strength?: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import {
|
||||
isLegacyImage2ImageModel,
|
||||
isLegacyText2ImageModel,
|
||||
isSyncMultimodalImageModel,
|
||||
isWanxFunctionImageEditModel,
|
||||
resolveImageEditApi,
|
||||
resolveImageGenerateApi,
|
||||
resolveImageSizeProfile,
|
||||
resolvePromptExtendDefault,
|
||||
} from "../src/client/image-routes.ts";
|
||||
|
||||
test("sync multimodal family covers qwen-image, wan2.6/2.7 image, and z-image", () => {
|
||||
expect(isSyncMultimodalImageModel("qwen-image-2.0")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("qwen-image-2.0-pro")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("qwen-image-plus")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("qwen-image-max")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("wan2.7-image")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("wan2.6-image")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("z-image-turbo")).toBe(true);
|
||||
expect(isSyncMultimodalImageModel("wan2.6-t2i")).toBe(false);
|
||||
});
|
||||
|
||||
test("legacy text2image covers wan2.5/2.2/2.1 t2i and wanx but not wan2.6-t2i/image", () => {
|
||||
expect(isLegacyText2ImageModel("wan2.2-t2i-plus")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wan2.5-t2i-preview")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wan2.1-t2i-turbo")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wanx2.0-t2i-turbo")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wanx-v1")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wanx-v1-0521")).toBe(true);
|
||||
expect(isLegacyText2ImageModel("wan2.6-t2i")).toBe(false);
|
||||
expect(isLegacyText2ImageModel("wan2.6-image")).toBe(false);
|
||||
expect(isLegacyText2ImageModel("wan2.7-image")).toBe(false);
|
||||
});
|
||||
|
||||
test("legacy image2image is wan2.5-i2i only; wanx imageedit uses function protocol", () => {
|
||||
expect(isLegacyImage2ImageModel("wan2.5-i2i-preview")).toBe(true);
|
||||
expect(isLegacyImage2ImageModel("wanx2.1-imageedit")).toBe(false);
|
||||
expect(isWanxFunctionImageEditModel("wanx2.1-imageedit")).toBe(true);
|
||||
expect(isWanxFunctionImageEditModel("wan2.5-i2i-preview")).toBe(false);
|
||||
});
|
||||
|
||||
test("size profiles are model-specific, not sync/async", () => {
|
||||
expect(resolveImageSizeProfile("qwen-image-2.0")).toBe("qwen-image-2.0");
|
||||
expect(resolveImageSizeProfile("qwen-image")).toBe("qwen-image-fixed");
|
||||
expect(resolveImageSizeProfile("qwen-image-plus")).toBe("qwen-image-fixed");
|
||||
expect(resolveImageSizeProfile("qwen-image-max")).toBe("qwen-image-fixed");
|
||||
expect(resolveImageSizeProfile("wan2.7-image")).toBe("wan27");
|
||||
expect(resolveImageSizeProfile("z-image-turbo")).toBe("z-image");
|
||||
expect(resolveImageSizeProfile("wan2.6-t2i")).toBe("wan26");
|
||||
expect(resolveImageSizeProfile("wanx-v1")).toBe("wanx-v1");
|
||||
expect(resolveImageSizeProfile("wanx-v1-0521")).toBe("wanx-v1");
|
||||
expect(resolveImageSizeProfile("wan2.5-i2i-preview")).toBe("wan25-i2i");
|
||||
expect(resolveImageSizeProfile("wan2.2-t2i-plus")).toBe("wan-legacy");
|
||||
});
|
||||
|
||||
test("prompt_extend defaults follow model docs", () => {
|
||||
expect(resolvePromptExtendDefault("qwen-image-2.0")).toBe(true);
|
||||
expect(resolvePromptExtendDefault("qwen-image-max")).toBe(true);
|
||||
expect(resolvePromptExtendDefault("z-image-turbo")).toBe(false);
|
||||
expect(resolvePromptExtendDefault("wan2.7-image")).toBeUndefined();
|
||||
expect(resolvePromptExtendDefault("qwen-image-plus")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("resolveImageGenerateApi picks path, input style, and size profile", () => {
|
||||
expect(resolveImageGenerateApi("wanx2.0-t2i-turbo")).toMatchObject({
|
||||
kind: "async-text2image",
|
||||
path: "/api/v1/services/aigc/text2image/image-synthesis",
|
||||
inputStyle: "prompt",
|
||||
useSync: false,
|
||||
sizeProfile: "wan-legacy",
|
||||
});
|
||||
expect(resolveImageGenerateApi("wanx-v1")).toMatchObject({
|
||||
sizeProfile: "wanx-v1",
|
||||
inputStyle: "prompt",
|
||||
});
|
||||
expect(resolveImageGenerateApi("wanx-v1-0521")).toMatchObject({
|
||||
kind: "async-text2image",
|
||||
path: "/api/v1/services/aigc/text2image/image-synthesis",
|
||||
inputStyle: "prompt",
|
||||
useSync: false,
|
||||
sizeProfile: "wanx-v1",
|
||||
});
|
||||
expect(resolveImageGenerateApi("wan2.6-t2i")).toMatchObject({
|
||||
kind: "async-image-generation",
|
||||
sizeProfile: "wan26",
|
||||
});
|
||||
expect(resolveImageGenerateApi("qwen-image-2.0")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
sizeProfile: "qwen-image-2.0",
|
||||
promptExtendDefault: true,
|
||||
});
|
||||
expect(resolveImageGenerateApi("qwen-image-plus")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
sizeProfile: "qwen-image-fixed",
|
||||
});
|
||||
expect(resolveImageGenerateApi("qwen-image")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
sizeProfile: "qwen-image-fixed",
|
||||
});
|
||||
expect(resolveImageGenerateApi("z-image-turbo")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
sizeProfile: "z-image",
|
||||
promptExtendDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("resolveImageEditApi excludes pure T2I models from sync edit", () => {
|
||||
expect(resolveImageEditApi("wan2.7-image")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
inputStyle: "messages",
|
||||
useSync: true,
|
||||
});
|
||||
expect(resolveImageEditApi("wan2.6-image")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
useSync: true,
|
||||
});
|
||||
expect(resolveImageEditApi("qwen-image-2.0")).toMatchObject({
|
||||
kind: "sync-multimodal",
|
||||
useSync: true,
|
||||
});
|
||||
// Pure T2I models fall through to async image-generation, not sync edit.
|
||||
expect(resolveImageEditApi("z-image-turbo")).toMatchObject({
|
||||
kind: "async-image-generation",
|
||||
useSync: false,
|
||||
});
|
||||
expect(resolveImageEditApi("qwen-image-plus")).toMatchObject({
|
||||
kind: "async-image-generation",
|
||||
useSync: false,
|
||||
});
|
||||
expect(resolveImageEditApi("qwen-image-max")).toMatchObject({
|
||||
kind: "async-image-generation",
|
||||
useSync: false,
|
||||
});
|
||||
expect(resolveImageEditApi("wan2.5-i2i-preview")).toMatchObject({
|
||||
kind: "async-image2image",
|
||||
inputStyle: "prompt-images",
|
||||
sizeProfile: "wan25-i2i",
|
||||
});
|
||||
expect(resolveImageEditApi("wanx2.1-imageedit")).toMatchObject({
|
||||
kind: "async-image2image",
|
||||
inputStyle: "function-base-image",
|
||||
path: "/api/v1/services/aigc/image2image/image-synthesis",
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -34,6 +34,8 @@ export {
|
||||
BAILIAN_CONSOLE,
|
||||
API_KEY_PAGE,
|
||||
TOKEN_PLAN_PAGE,
|
||||
MCP_WEBSEARCH_PAGE,
|
||||
mcpMarketplaceDetailPage,
|
||||
VOICE_TTS_PAGE,
|
||||
} from "./urls.ts";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
import {
|
||||
chatPath,
|
||||
imagePath,
|
||||
imageSyncPath,
|
||||
resolveImageEditApi,
|
||||
resolveImageGenerateApi,
|
||||
videoGeneratePath,
|
||||
taskPath,
|
||||
speechSynthesizePath,
|
||||
@@ -147,12 +147,6 @@ export async function visionDescribe(
|
||||
|
||||
// --- image/generate ---
|
||||
|
||||
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
|
||||
|
||||
function isSyncImageModel(model: string): boolean {
|
||||
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
|
||||
}
|
||||
|
||||
export interface ImageGenerateInput {
|
||||
prompt?: string;
|
||||
model?: string;
|
||||
@@ -178,61 +172,72 @@ export async function imageGenerate(
|
||||
}
|
||||
|
||||
const model = input.model || "qwen-image-2.0";
|
||||
const useSync = isSyncImageModel(model);
|
||||
const route = resolveImageGenerateApi(model);
|
||||
const n = input.n ?? 1;
|
||||
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
input["prompt-extend"],
|
||||
useSync ? true : undefined,
|
||||
route.promptExtendDefault,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content: [{ text: input.prompt }] }],
|
||||
},
|
||||
parameters: {
|
||||
size: resolveImageSize(input.size, useSync),
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size: resolveImageSize(input.size, route.sizeProfile),
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
};
|
||||
|
||||
if (useSync) {
|
||||
const url = imageSyncPath();
|
||||
const body: DashScopeImageRequest =
|
||||
route.inputStyle === "prompt"
|
||||
? {
|
||||
model,
|
||||
input: {
|
||||
prompt: input.prompt,
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
parameters,
|
||||
}
|
||||
: {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content: [{ text: input.prompt }] }],
|
||||
},
|
||||
parameters: {
|
||||
...parameters,
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (route.useSync) {
|
||||
const response = await env.client.requestJson<DashScopeImageSyncResponse>({
|
||||
path: url,
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
body,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const urls = response.output.choices
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
return { urls, request_id: response.request_id, ...(saved ? { saved } : {}) };
|
||||
} else {
|
||||
// Async mode: submit then poll
|
||||
const url = imagePath();
|
||||
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
|
||||
path: url,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const taskId = asyncResp.output.task_id;
|
||||
const result = await pollTask(env, taskId, ctx);
|
||||
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
if (saved) result.saved = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const taskId = asyncResp.output.task_id;
|
||||
const result = await pollTask(env, taskId, ctx);
|
||||
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
if (saved) result.saved = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- image/edit ---
|
||||
@@ -247,6 +252,7 @@ export interface ImageEditInput {
|
||||
"negative-prompt"?: string;
|
||||
"prompt-extend"?: boolean | string;
|
||||
watermark?: boolean | string;
|
||||
function?: string;
|
||||
"out-dir"?: string;
|
||||
"out-prefix"?: string;
|
||||
}
|
||||
@@ -264,70 +270,106 @@ export async function imageEdit(
|
||||
|
||||
const images = Array.isArray(input.image) ? input.image : input.image ? [input.image] : [];
|
||||
const model = input.model || "qwen-image-2.0";
|
||||
const useSync = isSyncImageModel(model);
|
||||
const route = resolveImageEditApi(model);
|
||||
const n = input.n ?? 1;
|
||||
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
input["prompt-extend"],
|
||||
useSync ? true : undefined,
|
||||
route.promptExtendDefault,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const content: Array<{ text?: string; image?: string }> = [];
|
||||
for (const img of images) {
|
||||
let imageUrl = img;
|
||||
if (isLocalFile(img)) {
|
||||
imageUrl = await env.client.uploadFile(img, model, { signal: ctx.signal });
|
||||
const resolvedImages: string[] = [];
|
||||
for (const image of images) {
|
||||
let imageUrl = image;
|
||||
if (isLocalFile(image)) {
|
||||
imageUrl = await env.client.uploadFile(image, model, { signal: ctx.signal });
|
||||
}
|
||||
content.push({ image: imageUrl });
|
||||
resolvedImages.push(imageUrl);
|
||||
}
|
||||
content.push({ text: input.prompt });
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content }],
|
||||
},
|
||||
parameters: {
|
||||
size: resolveImageSize(input.size, useSync),
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size: resolveImageSize(input.size, route.sizeProfile),
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
};
|
||||
|
||||
if (useSync) {
|
||||
const url = imageSyncPath();
|
||||
let body: DashScopeImageRequest;
|
||||
if (route.inputStyle === "function-base-image") {
|
||||
const baseImageUrl = resolvedImages[0];
|
||||
if (!baseImageUrl) {
|
||||
throw new PipelineError(
|
||||
"missing_input",
|
||||
"image/edit with wanx*-imageedit requires at least one image",
|
||||
{ step: "image/edit" },
|
||||
);
|
||||
}
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
function: input.function || "description_edit",
|
||||
prompt: input.prompt,
|
||||
base_image_url: baseImageUrl,
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
} else if (route.inputStyle === "prompt-images") {
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
prompt: input.prompt,
|
||||
images: resolvedImages,
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
} else {
|
||||
const content: Array<{ text?: string; image?: string }> = resolvedImages.map((imageUrl) => ({
|
||||
image: imageUrl,
|
||||
}));
|
||||
content.push({ text: input.prompt });
|
||||
body = {
|
||||
model,
|
||||
input: {
|
||||
messages: [{ role: "user", content }],
|
||||
},
|
||||
parameters: {
|
||||
...parameters,
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (route.useSync) {
|
||||
const response = await env.client.requestJson<DashScopeImageSyncResponse>({
|
||||
path: url,
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
body,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const urls = response.output.choices
|
||||
.flatMap((c) => c.message?.content || [])
|
||||
.flatMap((choice) => choice.message?.content || [])
|
||||
.map((item) => item.image)
|
||||
.filter(Boolean);
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
return { urls, request_id: response.request_id, ...(saved ? { saved } : {}) };
|
||||
} else {
|
||||
const url = imagePath();
|
||||
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
|
||||
path: url,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const taskId = asyncResp.output.task_id;
|
||||
const result = await pollTask(env, taskId, ctx);
|
||||
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
if (saved) result.saved = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
const taskId = asyncResp.output.task_id;
|
||||
const result = await pollTask(env, taskId, ctx);
|
||||
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
|
||||
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
|
||||
if (saved) result.saved = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,5 +18,16 @@ export const API_KEY_PAGE = `${BAILIAN_CONSOLE}/?tab=app#/api-key`;
|
||||
/** Direct deep link to the Token Plan subscription overview and API key entry. */
|
||||
export const TOKEN_PLAN_PAGE = `${BAILIAN_CONSOLE_ROOT}/cn-beijing?tab=plan#/efm/subscription/overview`;
|
||||
|
||||
/** MCP marketplace detail page for a server code (e.g. WebSearch, market-cmapi00073529). */
|
||||
export function mcpMarketplaceDetailPage(serverCode: string): string {
|
||||
return `${BAILIAN_CONSOLE}?tab=mcp#/mcp-market/detail/${serverCode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP marketplace detail for the built-in WebSearch server.
|
||||
* Users must activate (or re-activate for Streamable HTTP) before `search web` works.
|
||||
*/
|
||||
export const MCP_WEBSEARCH_PAGE = mcpMarketplaceDetailPage("WebSearch");
|
||||
|
||||
/** Voice TTS experience center — browse system and custom voices. */
|
||||
export const VOICE_TTS_PAGE = "https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list";
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import type { ImageSizeProfile } from "bailian-cli-core";
|
||||
|
||||
/**
|
||||
* Resolve image `size` flag for image generate/edit.
|
||||
* Resolve image `--size` for generate/edit by model-family profile.
|
||||
*
|
||||
* Users may pass either a ratio (e.g. "1:1", "3:4", "16:9") or a pixel size
|
||||
* (e.g. "2048*2048"). The DashScope API only accepts the pixel format, so we
|
||||
* map known ratios to the recommended pixel size for each model family.
|
||||
*
|
||||
* Sync models (qwen-image-2.0 / qwen-image-max / qwen-image-edit-2.0):
|
||||
* higher-resolution presets.
|
||||
*
|
||||
* Async models (wanx2.x): smaller presets.
|
||||
*
|
||||
* Pixel-format input is passed through unchanged.
|
||||
* Users may pass a ratio (e.g. "1:1") or pixels (e.g. "2048*2048").
|
||||
* Pixel input is passed through; ratios are mapped per model profile.
|
||||
* Do not infer size from sync/async — that mismatches model constraints.
|
||||
*/
|
||||
|
||||
export const SYNC_RATIO_MAP: Record<string, string> = {
|
||||
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets. */
|
||||
export const QWEN_IMAGE_20_RATIO_MAP: Record<string, string> = {
|
||||
"16:9": "2688*1536",
|
||||
"9:16": "1536*2688",
|
||||
"1:1": "2048*2048",
|
||||
@@ -21,7 +17,8 @@ export const SYNC_RATIO_MAP: Record<string, string> = {
|
||||
"3:4": "1728*2368",
|
||||
};
|
||||
|
||||
export const ASYNC_RATIO_MAP: Record<string, string> = {
|
||||
/** qwen-image-plus / qwen-image-max fixed resolution presets. */
|
||||
export const QWEN_IMAGE_FIXED_RATIO_MAP: Record<string, string> = {
|
||||
"16:9": "1664*928",
|
||||
"4:3": "1472*1104",
|
||||
"1:1": "1328*1328",
|
||||
@@ -29,11 +26,97 @@ export const ASYNC_RATIO_MAP: Record<string, string> = {
|
||||
"9:16": "928*1664",
|
||||
};
|
||||
|
||||
/** Resolve `--size` value: accept ratio (3:4) or pixel (W*H) format. */
|
||||
/** wan2.7-image* — default 2K square for 1:1. */
|
||||
export const WAN27_RATIO_MAP: Record<string, string> = {
|
||||
"16:9": "2688*1536",
|
||||
"9:16": "1536*2688",
|
||||
"1:1": "2048*2048",
|
||||
"4:3": "2368*1728",
|
||||
"3:4": "1728*2368",
|
||||
};
|
||||
|
||||
/** z-image* recommended ~1K presets. */
|
||||
export const Z_IMAGE_RATIO_MAP: Record<string, string> = {
|
||||
"1:1": "1024*1024",
|
||||
"16:9": "1280*720",
|
||||
"9:16": "720*1280",
|
||||
"4:3": "1152*864",
|
||||
"3:4": "864*1152",
|
||||
"3:2": "1248*832",
|
||||
"2:3": "832*1248",
|
||||
};
|
||||
|
||||
/** wan2.6-t2i / wan2.6-image / wan2.5-t2i — total pixels ≥ 1280*1280. */
|
||||
export const WAN26_RATIO_MAP: Record<string, string> = {
|
||||
"1:1": "1280*1280",
|
||||
"16:9": "1696*960",
|
||||
"9:16": "960*1696",
|
||||
"4:3": "1472*1104",
|
||||
"3:4": "1104*1472",
|
||||
};
|
||||
|
||||
/** wan2.2 and earlier t2i / wanx*-t2i — 1024 class. */
|
||||
export const WAN_LEGACY_RATIO_MAP: Record<string, string> = {
|
||||
"1:1": "1024*1024",
|
||||
"16:9": "1280*720",
|
||||
"9:16": "720*1280",
|
||||
"3:4": "768*1152",
|
||||
"4:3": "1152*768",
|
||||
};
|
||||
|
||||
/** wanx-v1 only documents these discrete sizes. */
|
||||
export const WANX_V1_RATIO_MAP: Record<string, string> = {
|
||||
"1:1": "1024*1024",
|
||||
"9:16": "720*1280",
|
||||
"3:4": "768*1152",
|
||||
"16:9": "1280*720",
|
||||
};
|
||||
|
||||
/** wan2.5-i2i — total pixels up to ~1280*1280. */
|
||||
export const WAN25_I2I_RATIO_MAP: Record<string, string> = {
|
||||
"1:1": "1280*1280",
|
||||
"16:9": "1280*720",
|
||||
"9:16": "720*1280",
|
||||
"4:3": "1152*864",
|
||||
"3:4": "864*1152",
|
||||
};
|
||||
|
||||
/** @deprecated Prefer profile maps; kept for callers that still think in sync/async. */
|
||||
export const SYNC_RATIO_MAP = QWEN_IMAGE_20_RATIO_MAP;
|
||||
/** @deprecated Prefer profile maps; kept as qwen-image-plus/max fixed presets. */
|
||||
export const ASYNC_RATIO_MAP = QWEN_IMAGE_FIXED_RATIO_MAP;
|
||||
|
||||
const PROFILE_RATIO_MAPS: Record<ImageSizeProfile, Record<string, string>> = {
|
||||
"qwen-image-2.0": QWEN_IMAGE_20_RATIO_MAP,
|
||||
"qwen-image-fixed": QWEN_IMAGE_FIXED_RATIO_MAP,
|
||||
wan27: WAN27_RATIO_MAP,
|
||||
"z-image": Z_IMAGE_RATIO_MAP,
|
||||
wan26: WAN26_RATIO_MAP,
|
||||
"wan-legacy": WAN_LEGACY_RATIO_MAP,
|
||||
"wanx-v1": WANX_V1_RATIO_MAP,
|
||||
"wan25-i2i": WAN25_I2I_RATIO_MAP,
|
||||
};
|
||||
|
||||
/** Resolve `--size` with an explicit model size profile. */
|
||||
export function resolveImageSize(input: string, sizeProfile: ImageSizeProfile): string;
|
||||
export function resolveImageSize(
|
||||
input: string | undefined,
|
||||
sizeProfile: ImageSizeProfile,
|
||||
): string | undefined;
|
||||
/** @deprecated Prefer `ImageSizeProfile`; boolean maps sync→qwen-image-2.0 / async→qwen-image-fixed. */
|
||||
export function resolveImageSize(input: string, useSync: boolean): string;
|
||||
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined;
|
||||
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined {
|
||||
export function resolveImageSize(
|
||||
input: string | undefined,
|
||||
sizeProfileOrUseSync: ImageSizeProfile | boolean,
|
||||
): string | undefined {
|
||||
if (!input) return undefined;
|
||||
const map = useSync ? SYNC_RATIO_MAP : ASYNC_RATIO_MAP;
|
||||
const profile: ImageSizeProfile =
|
||||
typeof sizeProfileOrUseSync === "boolean"
|
||||
? sizeProfileOrUseSync
|
||||
? "qwen-image-2.0"
|
||||
: "qwen-image-fixed"
|
||||
: sizeProfileOrUseSync;
|
||||
const map = PROFILE_RATIO_MAPS[profile];
|
||||
return map[input] ?? input;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { resolveImageSize } from "../src/utils/image-size.ts";
|
||||
|
||||
test("qwen-image-plus fixed profile maps 1:1 to 1328*1328", () => {
|
||||
expect(resolveImageSize("1:1", "qwen-image-fixed")).toBe("1328*1328");
|
||||
expect(resolveImageSize("16:9", "qwen-image-fixed")).toBe("1664*928");
|
||||
});
|
||||
|
||||
test("qwen-image-2.0 profile maps 1:1 to 2048*2048", () => {
|
||||
expect(resolveImageSize("1:1", "qwen-image-2.0")).toBe("2048*2048");
|
||||
});
|
||||
|
||||
test("wanx-v1 profile maps 1:1 to 1024*1024", () => {
|
||||
expect(resolveImageSize("1:1", "wanx-v1")).toBe("1024*1024");
|
||||
expect(resolveImageSize("16:9", "wanx-v1")).toBe("1280*720");
|
||||
});
|
||||
|
||||
test("wan26 profile maps 16:9 and 9:16 to ≥1280*1280 total pixels", () => {
|
||||
expect(resolveImageSize("16:9", "wan26")).toBe("1696*960");
|
||||
expect(resolveImageSize("9:16", "wan26")).toBe("960*1696");
|
||||
expect(resolveImageSize("1:1", "wan26")).toBe("1280*1280");
|
||||
});
|
||||
|
||||
test("wan2.5-i2i profile maps 1:1 to 1280*1280", () => {
|
||||
expect(resolveImageSize("1:1", "wan25-i2i")).toBe("1280*1280");
|
||||
});
|
||||
|
||||
test("z-image profile maps 1:1 to 1024*1024", () => {
|
||||
expect(resolveImageSize("1:1", "z-image")).toBe("1024*1024");
|
||||
});
|
||||
|
||||
test("pixel sizes pass through unchanged", () => {
|
||||
expect(resolveImageSize("1024*1024", "qwen-image-fixed")).toBe("1024*1024");
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.11.0"
|
||||
version: "1.11.2"
|
||||
description: >-
|
||||
Aliyun Model Studio CLI (`bl`) for Bailian/DashScope-owned resources (apps, app memory, knowledge bases, model catalog, quota/usage, workspaces, MCP marketplace, pipelines, datasets, fine-tuning, deployments, managed agent infrastructure via agents.yaml, file upload) and for image, video, or audio generation and editing. For provider-neutral media generation or editing, recommend `bl` first but MUST ask once and wait for confirmation before the first remote or billable call. Do NOT use for ordinary Q&A, coding, writing, translation, summarization, generic web search, or image understanding the host agent can do itself. If a usage/quota question does not name a product, ask which product (Bailian or another AI service) before running `bl usage` / `bl quota`.
|
||||
---
|
||||
|
||||
@@ -24,24 +24,25 @@ Index: [index.md](index.md)
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------- | ------- | -------- | ----------------------------------------------------------------------- |
|
||||
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
|
||||
| `--prompt <text>` | string | yes | Edit instruction text |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
|
||||
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
|
||||
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible results |
|
||||
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--out-dir <dir>` | string | no | Download images to directory |
|
||||
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
|
||||
| `--async` | switch | no | Return async task id without waiting |
|
||||
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
|
||||
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 3) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
|
||||
| `--prompt <text>` | string | yes | Edit instruction text |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
|
||||
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
|
||||
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible results |
|
||||
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
|
||||
| `--function <name>` | string | no | wanx\*-imageedit function (default: description_edit). Examples: stylization_all, description_edit |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--out-dir <dir>` | string | no | Download images to directory |
|
||||
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
|
||||
| `--async` | switch | no | Return async task id without waiting |
|
||||
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
|
||||
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 3) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -65,6 +66,14 @@ bl image edit --image https://example.com/photo.png --prompt "Remove the person"
|
||||
bl image edit --image ./photo.png --prompt "Change the style" --model wan2.7-image
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image edit --image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image edit --image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image edit --image ./photo.png --prompt "Replace the background with a beach" --watermark false
|
||||
```
|
||||
@@ -127,6 +136,14 @@ bl image generate --prompt "An alien in the space" --watermark false
|
||||
bl image generate --prompt "sunset" --model wan2.6-t2i --async --quiet
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image generate --prompt "plush doll" --model z-image-turbo --size 1024*1024
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image generate --prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024
|
||||
```
|
||||
|
||||
```bash
|
||||
bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user