diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 9a0288e..f12db07 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -114,6 +114,8 @@ export default defineCommand({ } } + const storedValue: string | number = resolvedKey === "timeout" ? Number(value) : value; + const format = detectOutputFormat(config.output); if (config.dryRun) { @@ -122,7 +124,7 @@ export default defineCommand({ } const existing = readConfigFile() as Record; - existing[resolvedKey] = resolvedKey === "timeout" ? Number(value) : value; + existing[resolvedKey] = storedValue; await writeConfigFile(existing); if (!config.quiet) { diff --git a/packages/cli/src/commands/image/edit.ts b/packages/cli/src/commands/image/edit.ts index 8257fe3..a8bf39b 100644 --- a/packages/cli/src/commands/image/edit.ts +++ b/packages/cli/src/commands/image/edit.ts @@ -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"; @@ -47,9 +49,14 @@ export default defineCommand({ flag: "--negative-prompt ", 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 ", + description: "Enable prompt extend (true/false). Default: true.", + }, + { + flag: "--watermark ", + description: "Enable watermark (true/false). Default: true.", + }, { flag: "--out-dir ", description: "Download images to directory" }, { flag: "--out-prefix ", description: "Filename prefix (default: edited)" }, ], @@ -58,6 +65,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 +104,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 +112,8 @@ export default defineCommand({ ); contentItems.push({ text: prompt! }); + const watermark = resolveWatermark(flags.watermark); + const body: DashScopeImageRequest = { model, input: { @@ -127,7 +129,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, }, }; diff --git a/packages/cli/src/commands/image/generate.ts b/packages/cli/src/commands/image/generate.ts index 1ab4b9c..9189ba2 100644 --- a/packages/cli/src/commands/image/generate.ts +++ b/packages/cli/src/commands/image/generate.ts @@ -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"; @@ -57,11 +59,14 @@ 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 ", + description: + "Enable prompt extend (true/false). Default: true for qwen-image sync models; omit for async.", + }, + { + flag: "--watermark ", + description: "Enable watermark (true/false). Default: true.", }, - { 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 +83,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 +118,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 +136,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, }, }; diff --git a/packages/cli/src/commands/video/edit.ts b/packages/cli/src/commands/video/edit.ts index 926411b..9c69533 100644 --- a/packages/cli/src/commands/video/edit.ts +++ b/packages/cli/src/commands/video/edit.ts @@ -15,6 +15,8 @@ import { resolveCredential, BailianError, ExitCode, + resolveBooleanFlag, + resolveWatermark, } from "bailian-cli-core"; import { poll } from "../../utils/polling.ts"; import { downloadFile, formatBytes } from "../../utils/download.ts"; @@ -51,9 +53,14 @@ export default defineCommand({ flag: "--audio-setting ", 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 ", + description: "Enable prompt extend (true/false). Omit to use API default.", + }, + { + flag: "--watermark ", + description: "Enable watermark (true/false). Default: true.", + }, { flag: "--seed ", description: "Random seed for reproducible generation", type: "number" }, { flag: "--download ", description: "Save video to file on completion" }, { flag: "--no-wait", description: "Return task ID immediately without waiting" }, @@ -71,6 +78,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 +135,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 +151,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, }, }; diff --git a/packages/cli/src/commands/video/generate.ts b/packages/cli/src/commands/video/generate.ts index 1ab3ec9..cfd2bc3 100644 --- a/packages/cli/src/commands/video/generate.ts +++ b/packages/cli/src/commands/video/generate.ts @@ -15,6 +15,8 @@ import { resolveCredential, BailianError, ExitCode, + resolveBooleanFlag, + resolveWatermark, } from "bailian-cli-core"; import { poll } from "../../utils/polling.ts"; import { downloadFile, formatBytes } from "../../utils/download.ts"; @@ -58,8 +60,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 ", + description: "Enable prompt extend (true/false). Omit to use API default.", + }, + { + flag: "--watermark ", + description: "Enable watermark (true/false). Default: true.", + }, { flag: "--seed ", description: "Random seed for reproducible generation", type: "number" }, { flag: "--download ", description: "Save video to file on completion" }, { flag: "--no-wait", description: "Return task ID immediately without waiting" }, @@ -78,6 +86,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 +119,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 +136,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, }, }; diff --git a/packages/cli/src/commands/video/ref.ts b/packages/cli/src/commands/video/ref.ts index a5681a9..458e0df 100644 --- a/packages/cli/src/commands/video/ref.ts +++ b/packages/cli/src/commands/video/ref.ts @@ -15,6 +15,8 @@ import { resolveCredential, BailianError, ExitCode, + resolveBooleanFlag, + resolveWatermark, } from "bailian-cli-core"; import { poll } from "../../utils/polling.ts"; import { downloadFile, formatBytes } from "../../utils/download.ts"; @@ -61,9 +63,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 ", + description: "Enable prompt extend (true/false). Omit to use API default.", + }, + { + flag: "--watermark ", + description: "Enable watermark (true/false). Default: true.", + }, { flag: "--seed ", description: "Random seed for reproducible generation", type: "number" }, { flag: "--download ", description: "Save video to file on completion" }, { flag: "--no-wait", description: "Return task ID immediately without waiting" }, @@ -82,6 +89,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 +165,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 +179,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, }, }; diff --git a/packages/cli/src/pipeline/steps/bl-api.ts b/packages/cli/src/pipeline/steps/bl-api.ts index b6f7e03..8dec70d 100644 --- a/packages/cli/src/pipeline/steps/bl-api.ts +++ b/packages/cli/src/pipeline/steps/bl-api.ts @@ -14,6 +14,8 @@ import { resolveFileUrl, resolveCredential, stripUndefined, + resolveBooleanFlag, + resolveWatermark, type Config, type ChatRequest, type ChatResponse, @@ -163,8 +165,7 @@ export interface ImageGenerateInput { n?: number; seed?: number; "negative-prompt"?: string; - "prompt-extend"?: boolean; - "no-prompt-extend"?: boolean; + "prompt-extend"?: boolean | string; watermark?: boolean; "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,8 +250,7 @@ export interface ImageEditInput { n?: number; seed?: number; "negative-prompt"?: string; - "prompt-extend"?: boolean; - "no-prompt-extend"?: boolean; + "prompt-extend"?: boolean | string; watermark?: boolean; "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,7 +374,7 @@ export interface VideoGenerateInput { resolution?: string; ratio?: string; duration?: number; - "prompt-extend"?: boolean; + "prompt-extend"?: boolean | string; watermark?: boolean; 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, }, }; diff --git a/packages/core/src/telemetry/tracker.ts b/packages/core/src/telemetry/tracker.ts index 8d5b16e..a8d4f1c 100644 --- a/packages/core/src/telemetry/tracker.ts +++ b/packages/core/src/telemetry/tracker.ts @@ -67,7 +67,6 @@ const PARAM_ALLOWLIST = new Set([ "noWait", "textOnly", "promptExtend", - "noPromptExtend", "enableSsml", "watermark", "hasThoughts", diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index e8d098f..320dd0b 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -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 "./watermark.ts"; diff --git a/packages/core/src/utils/watermark.ts b/packages/core/src/utils/watermark.ts new file mode 100644 index 0000000..9443733 --- /dev/null +++ b/packages/core/src/utils/watermark.ts @@ -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`). */ +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 `). + * 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 resolveBooleanFlag(flagValue, true, "watermark") ?? true; +} diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 613a529..3a68f7a 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -2,6 +2,7 @@ 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/watermark.ts"; function testConfig(overrides: Partial = {}): Config { return { @@ -175,6 +176,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",