mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Merge pull request #13 from modelstudioai/feat/watermark-and-paired-flags
fix: Fix the issue of the watermark being always on and address the i…
This commit is contained in:
@@ -25,6 +25,15 @@ interface FlagSchema {
|
||||
arrays: Set<string>;
|
||||
}
|
||||
|
||||
function buildAllowedFlagKeys(options: OptionDef[]): Set<string> {
|
||||
const keys = new Set<string>();
|
||||
for (const opt of options) {
|
||||
const key = flagKey(opt);
|
||||
if (key) keys.add(key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function buildSchema(options: OptionDef[]): FlagSchema {
|
||||
const booleans = new Set<string>();
|
||||
const numbers = new Set<string>();
|
||||
@@ -91,6 +100,7 @@ export function scanCommandPath(argv: string[], globalOptions: OptionDef[] = [])
|
||||
* - default: string
|
||||
*/
|
||||
export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
|
||||
const allowedKeys = buildAllowedFlagKeys(options);
|
||||
const schema = buildSchema(options);
|
||||
const flags: GlobalFlags = {
|
||||
quiet: false,
|
||||
@@ -130,20 +140,30 @@ export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
|
||||
|
||||
const camelKey = kebabToCamel(key);
|
||||
|
||||
if (!allowedKeys.has(camelKey)) {
|
||||
throw new BailianError(
|
||||
`Unknown flag "--${key}". Run with --help to see available options.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
// Switch-style flags (--quiet, --dry-run): no value. Value flags need a non-flag next token.
|
||||
if (schema.booleans.has(camelKey)) {
|
||||
(flags as Record<string, unknown>)[camelKey] = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// --prompt <text>, --watermark <bool>, …
|
||||
if (value === undefined) {
|
||||
i++;
|
||||
value = argv[i];
|
||||
const next = argv[i];
|
||||
if (next === undefined || next.startsWith("-")) {
|
||||
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
|
||||
}
|
||||
value = next;
|
||||
}
|
||||
|
||||
if (value === undefined)
|
||||
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
|
||||
|
||||
if (schema.arrays.has(camelKey)) {
|
||||
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
|
||||
if (arr) arr.push(value);
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
type DashScopeImageSyncResponse,
|
||||
ExitCode,
|
||||
BailianError,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "bailian-cli-core";
|
||||
import { downloadFile } from "../../utils/download.ts";
|
||||
import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts";
|
||||
@@ -22,6 +24,10 @@ import { promptText, failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import { resolveImageSize } from "../../utils/image-size.ts";
|
||||
import { join } from "path";
|
||||
import {
|
||||
BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
|
||||
BOOL_FLAG_WATERMARK,
|
||||
} from "../../utils/flag-descriptions.ts";
|
||||
|
||||
export default defineCommand({
|
||||
name: "image edit",
|
||||
@@ -47,9 +53,14 @@ export default defineCommand({
|
||||
flag: "--negative-prompt <text>",
|
||||
description: "Negative prompt to exclude unwanted content",
|
||||
},
|
||||
{ flag: "--prompt-extend", description: "Enable prompt smart rewrite (default: true)" },
|
||||
{ flag: "--no-prompt-extend", description: "Disable prompt extend" },
|
||||
{ flag: "--watermark", description: "Add watermark to output images" },
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
},
|
||||
{ flag: "--out-dir <dir>", description: "Download images to directory" },
|
||||
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: edited)" },
|
||||
],
|
||||
@@ -58,6 +69,7 @@ export default defineCommand({
|
||||
'bl image edit --image https://example.com/logo.png --prompt "Change color to blue" --n 3',
|
||||
'bl image edit --image ./a.png --image ./b.png --prompt "把两张图合并成一张拼图"',
|
||||
'bl image edit --image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
|
||||
'bl image edit --image ./photo.png --prompt "把背景换成海滩" --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// Normalize --image to string array (supports both single and repeated flags)
|
||||
@@ -96,15 +108,7 @@ export default defineCommand({
|
||||
);
|
||||
const n = (flags.n as number) ?? 1;
|
||||
|
||||
// Determine prompt_extend
|
||||
let promptExtend: boolean | undefined;
|
||||
if (flags.noPromptExtend === true) {
|
||||
promptExtend = false;
|
||||
} else if (flags.promptExtend === true) {
|
||||
promptExtend = true;
|
||||
} else {
|
||||
promptExtend = true; // default on for qwen-image
|
||||
}
|
||||
const promptExtend = resolveBooleanFlag(flags.promptExtend, true, "prompt-extend");
|
||||
|
||||
// Build content: all images first, then text prompt
|
||||
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
|
||||
@@ -112,6 +116,8 @@ export default defineCommand({
|
||||
);
|
||||
contentItems.push({ text: prompt! });
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
input: {
|
||||
@@ -127,7 +133,7 @@ export default defineCommand({
|
||||
n,
|
||||
seed: flags.seed as number | undefined,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: flags.watermark === true ? true : undefined,
|
||||
watermark,
|
||||
negative_prompt: (flags.negativePrompt as string) || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
type OutputFormat,
|
||||
type DashScopeTaskResponse,
|
||||
generateFilename,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "../../utils/polling.ts";
|
||||
import { downloadFile } from "../../utils/download.ts";
|
||||
@@ -24,6 +26,10 @@ import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/con
|
||||
import { promptText, failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import { resolveImageSize } from "../../utils/image-size.ts";
|
||||
import {
|
||||
BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
|
||||
BOOL_FLAG_WATERMARK,
|
||||
} from "../../utils/flag-descriptions.ts";
|
||||
|
||||
import { join } from "path";
|
||||
|
||||
@@ -57,11 +63,13 @@ export default defineCommand({
|
||||
description: "Negative prompt to exclude unwanted content",
|
||||
},
|
||||
{
|
||||
flag: "--prompt-extend",
|
||||
description: "Automatically extend prompt for better results (default: true for qwen-image)",
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
},
|
||||
{ flag: "--no-prompt-extend", description: "Disable prompt extend" },
|
||||
{ flag: "--watermark", description: "Add watermark to generated images" },
|
||||
{
|
||||
flag: "--no-wait",
|
||||
description: "Return task ID immediately without waiting (async models only)",
|
||||
@@ -78,7 +86,9 @@ export default defineCommand({
|
||||
'bl image generate --prompt "一只穿太空服的猫在火星上"',
|
||||
'bl image generate --prompt "Logo design" --n 3 --out-dir ./generated/',
|
||||
'bl image generate --prompt "Mountain landscape" --size 2688*1536',
|
||||
'bl image generate --prompt "A castle" --seed 42 --no-prompt-extend',
|
||||
'bl image generate --prompt "A castle" --seed 42 --prompt-extend false',
|
||||
'bl image generate --prompt "Logo" --watermark false',
|
||||
'bl image generate --prompt "An alien in the space" --watermark false',
|
||||
'bl image generate --prompt "sunset" --model wan2.6-t2i --no-wait --quiet',
|
||||
'bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro',
|
||||
'bl image generate --prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
|
||||
@@ -111,15 +121,13 @@ export default defineCommand({
|
||||
const n = (flags.n as number) ?? 1;
|
||||
const concurrent = getConcurrency(flags);
|
||||
|
||||
// Determine prompt_extend: default true for qwen-image, undefined for others
|
||||
let promptExtend: boolean | undefined;
|
||||
if (flags.noPromptExtend === true) {
|
||||
promptExtend = false;
|
||||
} else if (flags.promptExtend === true) {
|
||||
promptExtend = true;
|
||||
} else if (useSync) {
|
||||
promptExtend = true; // qwen-image default
|
||||
}
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
flags.promptExtend,
|
||||
useSync ? true : undefined,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
@@ -131,7 +139,7 @@ export default defineCommand({
|
||||
n,
|
||||
seed: flags.seed as number | undefined,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: flags.watermark === true ? true : undefined,
|
||||
watermark,
|
||||
negative_prompt: (flags.negativePrompt as string) || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,11 +15,17 @@ import {
|
||||
resolveCredential,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "../../utils/polling.ts";
|
||||
import { downloadFile, formatBytes } from "../../utils/download.ts";
|
||||
import { promptText, failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import {
|
||||
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
BOOL_FLAG_WATERMARK,
|
||||
} from "../../utils/flag-descriptions.ts";
|
||||
|
||||
export default defineCommand({
|
||||
name: "video edit",
|
||||
@@ -51,9 +57,14 @@ export default defineCommand({
|
||||
flag: "--audio-setting <mode>",
|
||||
description: "Audio: auto (default) or origin (keep original)",
|
||||
},
|
||||
{ flag: "--prompt-extend", description: "Enable prompt intelligent rewriting (default: true)" },
|
||||
{ flag: "--no-prompt-extend", description: "Disable prompt intelligent rewriting" },
|
||||
{ flag: "--watermark", description: 'Add "AI生成" watermark' },
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
|
||||
@@ -71,6 +82,7 @@ export default defineCommand({
|
||||
'bl video edit --video https://example.com/input.mp4 --prompt "将整个画面转换为黏土风格"',
|
||||
'bl video edit --video https://example.com/input.mp4 --prompt "替换衣服为图片中的款式" --ref-image https://example.com/clothes.png',
|
||||
'bl video edit --video https://example.com/input.mp4 --prompt "Convert to anime style" --resolution 720P --download output.mp4',
|
||||
'bl video edit --video https://example.com/input.mp4 --prompt "给视频里的小猫穿上衣服" --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// --- Validate video URL ---
|
||||
@@ -127,8 +139,8 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
// --- Build request body ---
|
||||
const promptExtend =
|
||||
flags.noPromptExtend === true ? false : flags.promptExtend === true ? true : undefined;
|
||||
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeVideoEditRequest = {
|
||||
model,
|
||||
@@ -143,7 +155,7 @@ export default defineCommand({
|
||||
duration: (flags.duration as number) || undefined,
|
||||
audio_setting: (flags.audioSetting as "auto" | "origin") || undefined,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: flags.watermark === true ? true : undefined,
|
||||
watermark,
|
||||
seed: flags.seed as number | undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,12 +15,18 @@ import {
|
||||
resolveCredential,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "../../utils/polling.ts";
|
||||
import { downloadFile, formatBytes } from "../../utils/download.ts";
|
||||
import { runConcurrent, getConcurrency } from "../../utils/concurrent.ts";
|
||||
import { promptText, failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import {
|
||||
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
BOOL_FLAG_WATERMARK,
|
||||
} from "../../utils/flag-descriptions.ts";
|
||||
|
||||
// Normalize shorthand resolution (720P, 1080P) to pixel format for video generation models
|
||||
const RESOLUTION_SHORTCUTS: Record<string, string> = {
|
||||
@@ -58,8 +64,14 @@ export default defineCommand({
|
||||
description: "Video duration in seconds (default: 5)",
|
||||
type: "number",
|
||||
},
|
||||
{ flag: "--prompt-extend", description: "Automatically extend prompt for better results" },
|
||||
{ flag: "--watermark", description: "Add watermark to generated video" },
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
|
||||
@@ -78,6 +90,7 @@ export default defineCommand({
|
||||
'bl video generate --prompt "Ocean waves at sunset." --download sunset.mp4',
|
||||
'bl video generate --image https://example.com/cat.png --prompt "让画面中的猫动起来"',
|
||||
'bl video generate --prompt "Mountain landscape" --resolution 1280*720 --duration 5',
|
||||
'bl video generate --prompt "A cat playing with a ball" --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
let prompt = flags.prompt as string | undefined;
|
||||
@@ -110,6 +123,9 @@ export default defineCommand({
|
||||
resolvedImageUrl = await resolveFileUrl(imageUrl, credential.token, model);
|
||||
}
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
|
||||
|
||||
const body: DashScopeVideoRequest = {
|
||||
model,
|
||||
input: {
|
||||
@@ -124,8 +140,8 @@ export default defineCommand({
|
||||
resolution: normalizeResolution(flags.resolution as string) || undefined,
|
||||
ratio: (flags.ratio as string) || undefined,
|
||||
duration: (flags.duration as number) || undefined,
|
||||
prompt_extend: flags.promptExtend === true ? true : undefined,
|
||||
watermark: flags.watermark === true ? true : undefined,
|
||||
prompt_extend: promptExtend,
|
||||
watermark,
|
||||
seed: flags.seed as number | undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,11 +15,17 @@ import {
|
||||
resolveCredential,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "../../utils/polling.ts";
|
||||
import { downloadFile, formatBytes } from "../../utils/download.ts";
|
||||
import { promptText, failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import {
|
||||
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
BOOL_FLAG_WATERMARK,
|
||||
} from "../../utils/flag-descriptions.ts";
|
||||
|
||||
export default defineCommand({
|
||||
name: "video ref",
|
||||
@@ -61,9 +67,14 @@ export default defineCommand({
|
||||
description: "Video duration in seconds (2-10, default: 5)",
|
||||
type: "number",
|
||||
},
|
||||
{ flag: "--prompt-extend", description: "Enable prompt intelligent rewriting" },
|
||||
{ flag: "--no-prompt-extend", description: "Disable prompt intelligent rewriting" },
|
||||
{ flag: "--watermark", description: "Add watermark to generated video" },
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
|
||||
@@ -82,6 +93,7 @@ export default defineCommand({
|
||||
'bl video ref --prompt "视频1在弹吉他,图1走过来" --ref-video scene.mp4 --image person.jpg',
|
||||
'bl video ref --prompt "图1说话" --image person.jpg --image-voice voice.mp3 --resolution 1080P',
|
||||
'bl video ref --prompt "图1和图2在对话" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3',
|
||||
'bl video ref --prompt "图1在喝水" --image person.jpg --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// --- Validate prompt ---
|
||||
@@ -157,8 +169,8 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
// --- Build request body ---
|
||||
const promptExtend =
|
||||
flags.noPromptExtend === true ? false : flags.promptExtend === true ? true : undefined;
|
||||
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
|
||||
const body: DashScopeVideoRefRequest = {
|
||||
model,
|
||||
@@ -171,7 +183,7 @@ export default defineCommand({
|
||||
ratio: (flags.ratio as string) || undefined,
|
||||
duration: (flags.duration as number) || undefined,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: flags.watermark === true ? true : undefined,
|
||||
watermark,
|
||||
seed: flags.seed as number | undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
resolveFileUrl,
|
||||
resolveCredential,
|
||||
stripUndefined,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
type Config,
|
||||
type ChatRequest,
|
||||
type ChatResponse,
|
||||
@@ -163,9 +165,8 @@ export interface ImageGenerateInput {
|
||||
n?: number;
|
||||
seed?: number;
|
||||
"negative-prompt"?: string;
|
||||
"prompt-extend"?: boolean;
|
||||
"no-prompt-extend"?: boolean;
|
||||
watermark?: boolean;
|
||||
"prompt-extend"?: boolean | string;
|
||||
watermark?: boolean | string;
|
||||
"out-dir"?: string;
|
||||
"out-prefix"?: string;
|
||||
}
|
||||
@@ -185,14 +186,11 @@ export async function imageGenerate(
|
||||
const useSync = isSyncImageModel(model);
|
||||
const n = input.n ?? 1;
|
||||
|
||||
let promptExtend: boolean | undefined;
|
||||
if (input["no-prompt-extend"]) {
|
||||
promptExtend = false;
|
||||
} else if (input["prompt-extend"]) {
|
||||
promptExtend = true;
|
||||
} else if (useSync) {
|
||||
promptExtend = true;
|
||||
}
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
input["prompt-extend"],
|
||||
useSync ? true : undefined,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const body: DashScopeImageRequest = {
|
||||
model,
|
||||
@@ -204,7 +202,7 @@ export async function imageGenerate(
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: input.watermark === true ? true : undefined,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
};
|
||||
@@ -252,9 +250,8 @@ export interface ImageEditInput {
|
||||
n?: number;
|
||||
seed?: number;
|
||||
"negative-prompt"?: string;
|
||||
"prompt-extend"?: boolean;
|
||||
"no-prompt-extend"?: boolean;
|
||||
watermark?: boolean;
|
||||
"prompt-extend"?: boolean | string;
|
||||
watermark?: boolean | string;
|
||||
"out-dir"?: string;
|
||||
"out-prefix"?: string;
|
||||
}
|
||||
@@ -275,14 +272,11 @@ export async function imageEdit(
|
||||
const useSync = isSyncImageModel(model);
|
||||
const n = input.n ?? 1;
|
||||
|
||||
let promptExtend: boolean | undefined;
|
||||
if (input["no-prompt-extend"]) {
|
||||
promptExtend = false;
|
||||
} else if (input["prompt-extend"]) {
|
||||
promptExtend = true;
|
||||
} else if (useSync) {
|
||||
promptExtend = true;
|
||||
}
|
||||
const promptExtend = resolveBooleanFlag(
|
||||
input["prompt-extend"],
|
||||
useSync ? true : undefined,
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const content: Array<{ text?: string; image?: string }> = [];
|
||||
for (const img of images) {
|
||||
@@ -305,7 +299,7 @@ export async function imageEdit(
|
||||
n,
|
||||
seed: input.seed,
|
||||
prompt_extend: promptExtend,
|
||||
watermark: input.watermark === true ? true : undefined,
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
negative_prompt: input["negative-prompt"] || undefined,
|
||||
},
|
||||
};
|
||||
@@ -380,8 +374,8 @@ export interface VideoGenerateInput {
|
||||
resolution?: string;
|
||||
ratio?: string;
|
||||
duration?: number;
|
||||
"prompt-extend"?: boolean;
|
||||
watermark?: boolean;
|
||||
"prompt-extend"?: boolean | string;
|
||||
watermark?: boolean | string;
|
||||
seed?: number;
|
||||
"poll-interval"?: number;
|
||||
}
|
||||
@@ -424,8 +418,8 @@ export async function videoGenerate(
|
||||
resolution: input.resolution || undefined,
|
||||
ratio: input.ratio || undefined,
|
||||
duration: input.duration,
|
||||
prompt_extend: input["prompt-extend"],
|
||||
watermark: input.watermark,
|
||||
prompt_extend: resolveBooleanFlag(input["prompt-extend"], undefined, "prompt-extend"),
|
||||
watermark: resolveWatermark(input.watermark),
|
||||
seed: input.seed,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/** Shared --foo <bool> help text; keep wording consistent with actual CLI/request behavior. */
|
||||
|
||||
export const BOOL_FLAG_WATERMARK =
|
||||
"Enable watermark (true/false). Omit flag to use CLI default (true).";
|
||||
|
||||
/** CLI sends prompt_extend=true when flag omitted (qwen-image edit, etc.). */
|
||||
export const BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE =
|
||||
"Enable prompt extend (true/false). Omit flag to use CLI default (true).";
|
||||
|
||||
/** Sync qwen-image defaults on; async models omit the field unless flag is set. */
|
||||
export const BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE =
|
||||
"Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default).";
|
||||
|
||||
/** CLI omits prompt_extend in the request when flag is unset (video commands). */
|
||||
export const BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT =
|
||||
"Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default).";
|
||||
@@ -0,0 +1,95 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core";
|
||||
import { parseFlags } from "../src/args.ts";
|
||||
import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts";
|
||||
|
||||
const IMAGE_GENERATE_OPTIONS = [
|
||||
{ flag: "--prompt <text>", description: "Image description", required: true },
|
||||
{ flag: "--model <model>", description: "Model ID" },
|
||||
{ flag: "--watermark <bool>", description: BOOL_FLAG_WATERMARK },
|
||||
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
|
||||
];
|
||||
|
||||
test("parseFlags rejects unknown long flags", () => {
|
||||
expect(() =>
|
||||
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "BailianError",
|
||||
exitCode: ExitCode.USAGE,
|
||||
message: expect.stringContaining('Unknown flag "--xxxx"'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags rejects unknown flags with = syntax", () => {
|
||||
expect(() =>
|
||||
parseFlags(
|
||||
["--prompt=cat", "--unknown-flag=yes"],
|
||||
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
|
||||
),
|
||||
).toThrow(/Unknown flag "--unknown-flag"/);
|
||||
});
|
||||
|
||||
test("parseFlags accepts defined command and global flags", () => {
|
||||
const flags = parseFlags(
|
||||
["--quiet", "--prompt", "cat", "--watermark", "false"],
|
||||
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
|
||||
);
|
||||
expect(flags.quiet).toBe(true);
|
||||
expect(flags.prompt).toBe("cat");
|
||||
expect(flags.watermark).toBe("false");
|
||||
});
|
||||
|
||||
test("parseFlags rejects value flag when next token is another flag", () => {
|
||||
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
|
||||
for (const argv of [
|
||||
["--watermark", "--prompt", "cat"],
|
||||
["--watermark", "-h"],
|
||||
["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"],
|
||||
]) {
|
||||
expect(() => parseFlags(argv, opts)).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "BailianError",
|
||||
exitCode: ExitCode.USAGE,
|
||||
message: expect.stringContaining("Flag --watermark requires a value"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseFlags rejects trailing value flag without value", () => {
|
||||
expect(() =>
|
||||
parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Flag --watermark requires a value"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags allows boolean flags without values adjacent to other flags", () => {
|
||||
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
|
||||
const flags = parseFlags(
|
||||
["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"],
|
||||
opts,
|
||||
);
|
||||
expect(flags.quiet).toBe(true);
|
||||
expect(flags.dryRun).toBe(true);
|
||||
expect(flags.noWait).toBe(true);
|
||||
expect(flags.prompt).toBe("cat");
|
||||
expect(flags.watermark).toBe("false");
|
||||
});
|
||||
|
||||
test("parseFlags does not treat the next flag as a boolean flag value", () => {
|
||||
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
|
||||
expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Flag --prompt requires a value"),
|
||||
}),
|
||||
);
|
||||
// --dry-run is boolean: no value check; parsing continues to --prompt.
|
||||
const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts);
|
||||
expect(flags.dryRun).toBe(true);
|
||||
expect(flags.prompt).toBe("cat");
|
||||
});
|
||||
@@ -67,7 +67,6 @@ const PARAM_ALLOWLIST = new Set([
|
||||
"noWait",
|
||||
"textOnly",
|
||||
"promptExtend",
|
||||
"noPromptExtend",
|
||||
"enableSsml",
|
||||
"watermark",
|
||||
"hasThoughts",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
|
||||
/** Parse true/false from CLI flags (e.g. `--watermark <bool>`). */
|
||||
export function parseBooleanValue(value: unknown, label = "boolean"): boolean {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string") {
|
||||
const v = value.trim().toLowerCase();
|
||||
if (v === "true") return true;
|
||||
if (v === "false") return false;
|
||||
}
|
||||
throw new BailianError(
|
||||
`Invalid ${label} value "${String(value)}". Use true or false.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
export function parseOptionalBooleanValue(value: unknown, label = "boolean"): boolean | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
return parseBooleanValue(value, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tri-state boolean CLI flag (`--name <bool>`).
|
||||
* Returns `defaultWhenUnset` when the flag is omitted.
|
||||
*/
|
||||
export function resolveBooleanFlag(
|
||||
flagValue: unknown,
|
||||
defaultWhenUnset: boolean | undefined,
|
||||
label = "boolean",
|
||||
): boolean | undefined {
|
||||
const fromFlag = parseOptionalBooleanValue(flagValue, label);
|
||||
if (fromFlag !== undefined) return fromFlag;
|
||||
return defaultWhenUnset;
|
||||
}
|
||||
|
||||
/** Resolve `--watermark` flag; default true when unset. */
|
||||
export function resolveWatermark(flagValue: unknown): boolean {
|
||||
return parseOptionalBooleanValue(flagValue, "watermark") ?? true;
|
||||
}
|
||||
@@ -5,3 +5,9 @@ export { maskToken } from "./token.ts";
|
||||
export { isInteractive } from "./env.ts";
|
||||
export { isCI } from "./env.ts";
|
||||
export { stripUndefined } from "./object.ts";
|
||||
export {
|
||||
parseBooleanValue,
|
||||
parseOptionalBooleanValue,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "./boolean-flag.ts";
|
||||
|
||||
@@ -2,6 +2,11 @@ import { expect, test } from "vite-plus/test";
|
||||
import type { Config } from "../src/index.ts";
|
||||
import { BailianError, ExitCode, McpClient, mapApiError, request } from "../src/index.ts";
|
||||
import { parseConfigFile } from "../src/config/schema.ts";
|
||||
import {
|
||||
parseBooleanValue,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
} from "../src/utils/boolean-flag.ts";
|
||||
|
||||
function testConfig(overrides: Partial<Config> = {}): Config {
|
||||
return {
|
||||
@@ -175,6 +180,26 @@ test("McpClient uses injected client identity for initialize and User-Agent", as
|
||||
});
|
||||
});
|
||||
|
||||
test("resolveWatermark uses flag or defaults to true", () => {
|
||||
expect(resolveWatermark("false")).toBe(false);
|
||||
expect(resolveWatermark("true")).toBe(true);
|
||||
expect(resolveWatermark(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
test("resolveBooleanFlag uses flag or defaultWhenUnset", () => {
|
||||
expect(resolveBooleanFlag("false", true, "prompt-extend")).toBe(false);
|
||||
expect(resolveBooleanFlag(undefined, true, "prompt-extend")).toBe(true);
|
||||
expect(resolveBooleanFlag(undefined, undefined, "prompt-extend")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("parseBooleanValue accepts only true and false strings (case-insensitive)", () => {
|
||||
expect(parseBooleanValue("true")).toBe(true);
|
||||
expect(parseBooleanValue("FALSE")).toBe(false);
|
||||
expect(() => parseBooleanValue("1")).toThrow(BailianError);
|
||||
expect(() => parseBooleanValue("yes")).toThrow(BailianError);
|
||||
expect(() => parseBooleanValue("maybe")).toThrow(BailianError);
|
||||
});
|
||||
|
||||
test("parseConfigFile accepts only well-formed http(s) base_url / console_gateway_url", () => {
|
||||
expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe(
|
||||
"https://dashscope.aliyuncs.com",
|
||||
|
||||
Reference in New Issue
Block a user