Merge pull request #194 from modelstudioai/feat/global-watermark-config

Feat/global watermark config
This commit is contained in:
Gong Shiqi
2026-09-10 16:49:33 +08:00
committed by GitHub
29 changed files with 447 additions and 52 deletions
+3 -2
View File
@@ -12,9 +12,9 @@ export default defineCommand({
valueHint: "<key>",
description: {
"en-US":
"Config key (language, base_url, output, output_dir, timeout, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
"Config key (language, base_url, output, output_dir, timeout, watermark, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
"zh-CN":
"配置项名称(language、base_url、output、output_dir、timeout、api_key、api_key_capabilities、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
"配置项名称(language、base_url、output、output_dir、timeout、watermark、api_key、api_key_capabilities、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
},
required: true,
},
@@ -29,6 +29,7 @@ export default defineCommand({
"--key language --value zh-CN",
"--key output --value json",
"--key timeout --value 600",
"--key watermark --value false",
"--key base_url --value https://dashscope.aliyuncs.com",
"--config company-plan --key api-key-capabilities --value text.chat,image.generate",
],
@@ -3,6 +3,7 @@ import {
ExitCode,
isApiKeyCapability,
normalizeModelBaseUrl,
parseBooleanValue,
SUPPORTED_LANGUAGES,
} from "bailian-cli-core";
@@ -13,6 +14,7 @@ export const VALID_KEYS = [
"output",
"output_dir",
"timeout",
"watermark",
"api_key",
"access_token",
"access_key_id",
@@ -62,7 +64,7 @@ export const UI_ENUM_KEYS: Record<string, string[]> = {
};
// Keys the UI renders as a true/false dropdown and stores as a boolean.
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry"]);
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry", "watermark"]);
// Default model each `default_*_model` key falls back to when left unset. These
// mirror the inline `|| "<model>"` fallbacks in the generation commands
@@ -164,7 +166,10 @@ export function resolveKey(key: string): string {
* Validate a single config entry and coerce its value to the stored type.
* Throws BailianError(USAGE) for unknown keys or invalid values.
*/
export function validateAndCoerce(key: string, value: string): string | number | string[] {
export function validateAndCoerce(
key: string,
value: string,
): string | number | boolean | string[] {
const resolvedKey = resolveKey(key);
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
@@ -201,6 +206,8 @@ export function validateAndCoerce(key: string, value: string): string | number |
if (resolvedKey === "base_url") return normalizeModelBaseUrl(value);
if (resolvedKey === "watermark") return parseBooleanValue(value, "watermark");
if (resolvedKey === "api_key_capabilities") {
let rawCapabilities: unknown;
if (value.trim().startsWith("[")) {
@@ -18,6 +18,7 @@ export default defineCommand({
base_url: client.baseUrl,
output: settings.output,
timeout: settings.timeout,
watermark: settings.watermark,
config: settings.configName ?? "default",
config_file: store.path,
};
+1 -1
View File
@@ -207,7 +207,7 @@ export default defineCommand({
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(flags.size, route.sizeProfile),
@@ -185,7 +185,7 @@ export default defineCommand({
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size,
+1 -1
View File
@@ -197,7 +197,7 @@ export default defineCommand({
// --- Build request body ---
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const body: DashScopeVideoEditRequest = {
model,
@@ -208,7 +208,7 @@ export default defineCommand({
resolvedFileUrl = await ctx.client.uploadFile(fileUrl, model);
}
const watermark = resolveWatermark(flags.watermark);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const body: DashScopeVideoRequest = {
+1 -1
View File
@@ -249,7 +249,7 @@ export default defineCommand({
// --- Build request body ---
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const body: DashScopeVideoRefRequest = {
model,
@@ -33,3 +33,9 @@ test("default-speech-recognition-model alias accepts an ASR model ID", () => {
"qwen-audio-3.0-asr-flash",
);
});
test("watermark config accepts only boolean text and stores a boolean", () => {
expect(validateAndCoerce("watermark", "false")).toBe(false);
expect(validateAndCoerce("watermark", "TRUE")).toBe(true);
expect(() => validateAndCoerce("watermark", "yes")).toThrow(/true or false/i);
});
@@ -200,8 +200,10 @@ test("GET /api/config 返回全部 profile、明文密钥与持久化激活项",
expect(res.json.keys).toContain("console_site");
expect(res.json.keys).toContain("telemetry");
expect(res.json.keys).toContain("default_speech_recognition_model");
expect(res.json.keys).toContain("watermark");
expect(res.json.enums.console_site).toEqual(["domestic", "international"]);
expect(res.json.booleanKeys).toContain("telemetry");
expect(res.json.booleanKeys).toContain("watermark");
// Default field hints are surfaced as prefilled values in the UI.
expect(res.json.fieldDefaults.default_image_model).toBe("qwen-image-3.0");
expect(res.json.fieldDefaults.default_text_model).toBe("qwen3.8-max");
@@ -64,10 +64,53 @@ describe("e2e: config", () => {
config_file?: string;
base_url?: string;
timeout?: number;
watermark?: boolean;
}>(stdout);
expect(data.config_file).toBeDefined();
expect(data.base_url).toBeDefined();
expect(data.timeout).toBeDefined();
expect(data.watermark).toBe(true);
});
test("config set 将 watermark 作为 boolean 写入并由 config show 读回", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-watermark-"));
try {
const env = { BAILIAN_CONFIG_DIR: configDir };
const setResult = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"set",
"--config",
"media",
"--key",
"watermark",
"--value",
"false",
"--output",
"json",
],
env,
);
expect(setResult.exitCode, setResult.stderr).toBe(0);
expect(parseStdoutJson<{ watermark?: boolean }>(setResult.stdout).watermark).toBe(false);
const persisted = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
Record<string, unknown>
>;
expect(persisted.media?.watermark).toBe(false);
const showResult = await runCommandE2e(
CONFIG_ROUTES,
["config", "show", "--config", "media", "--output", "json"],
env,
);
expect(showResult.exitCode, showResult.stderr).toBe(0);
expect(parseStdoutJson<{ watermark?: boolean }>(showResult.stdout).watermark).toBe(false);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config show --output text", async () => {
@@ -240,4 +240,75 @@ describe("e2e: pipeline", () => {
expect(stdout).toBe("");
expect(stderr).toMatch(/--events must be one of: jsonl/i);
});
test("pipeline image/generate dry-run 继承 Profile watermark=false", async () => {
const configDir = await mkdtemp(join(tmpdir(), "bl-pipeline-wm-"));
const workflowPath = join(configDir, "image-generate.json");
try {
await writeFile(
join(configDir, "config.json"),
JSON.stringify({ api_key: "sk-test-placeholder", watermark: false }, null, 2) + "\n",
);
await writeFile(
workflowPath,
JSON.stringify({
version: "workflow/v1",
steps: [{ id: "gen", type: "image/generate", input: { prompt: "A cat" } }],
}),
);
const { stdout, stderr, exitCode } = await runCommandE2e(
PIPELINE_ROUTES,
["pipeline", "run", "--file", workflowPath, "--dry-run", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir, DASHSCOPE_API_KEY: "", DASHSCOPE_BASE_URL: "" },
);
expect(exitCode, stderr).toBe(0);
const report = parseStdoutJson<{
status?: string;
steps?: Array<{ type?: string; input?: { watermark?: boolean; prompt?: string } }>;
}>(stdout);
expect(report.status).toBe("planned");
expect(report.steps?.[0]).toMatchObject({
type: "image/generate",
input: { prompt: "A cat", watermark: false },
});
} finally {
await rm(configDir, { recursive: true, force: true });
}
});
test("pipeline 步骤显式 watermark=true 覆盖 Profile false", async () => {
const configDir = await mkdtemp(join(tmpdir(), "bl-pipeline-wm-ov-"));
const workflowPath = join(configDir, "image-generate.json");
try {
await writeFile(
join(configDir, "config.json"),
JSON.stringify({ api_key: "sk-test-placeholder", watermark: false }, null, 2) + "\n",
);
await writeFile(
workflowPath,
JSON.stringify({
version: "workflow/v1",
steps: [
{
id: "gen",
type: "image/generate",
input: { prompt: "A cat", watermark: true },
},
],
}),
);
const { stdout, stderr, exitCode } = await runCommandE2e(
PIPELINE_ROUTES,
["pipeline", "run", "--file", workflowPath, "--dry-run", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir, DASHSCOPE_API_KEY: "", DASHSCOPE_BASE_URL: "" },
);
expect(exitCode, stderr).toBe(0);
const report = parseStdoutJson<{
steps?: Array<{ input?: { watermark?: boolean } }>;
}>(stdout);
expect(report.steps?.[0]?.input?.watermark).toBe(true);
} finally {
await rm(configDir, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,80 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { IMAGE_ROUTES, VIDEO_ROUTES, type E2eRouteExports } from "./topic-routes.ts";
interface WatermarkScenario {
name: string;
routes: E2eRouteExports;
args: string[];
}
const scenarios: WatermarkScenario[] = [
{
name: "image generate",
routes: IMAGE_ROUTES,
args: ["image", "generate", "--prompt", "A cat"],
},
{
name: "image edit",
routes: IMAGE_ROUTES,
args: [
"image",
"edit",
"--image",
"https://example.com/input.png",
"--prompt",
"Blue background",
],
},
{
name: "video generate",
routes: VIDEO_ROUTES,
args: ["video", "generate", "--prompt", "A cat waves"],
},
{
name: "video edit",
routes: VIDEO_ROUTES,
args: ["video", "edit", "--video", "https://example.com/input.mp4", "--prompt", "Warm colors"],
},
{
name: "video ref",
routes: VIDEO_ROUTES,
args: [
"video",
"ref",
"--image",
"https://example.com/person.png",
"--prompt",
"Image 1 waves",
],
},
];
describe("e2e: global watermark config", () => {
for (const scenario of scenarios) {
test(`${scenario.name} uses watermark=false from the selected Profile`, async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-watermark-profile-"));
try {
writeFileSync(
join(configDir, "config.json"),
JSON.stringify({ media: { watermark: false } }, null, 2) + "\n",
);
const { stdout, stderr, exitCode } = await runCommandE2e(
scenario.routes,
[...scenario.args, "--config", "media", "--dry-run", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { watermark?: boolean } };
}>(stdout);
expect(data.request?.parameters?.watermark).toBe(false);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
}
});
+1
View File
@@ -272,6 +272,7 @@ export function buildSettings(s: ResolutionSources): Settings {
outputExplicit: Boolean(flags.output || env.DASHSCOPE_OUTPUT || file.output),
outputDir: file.output_dir || undefined,
timeout,
watermark: file.watermark ?? true,
defaultTextModel: file.default_text_model,
defaultVideoModel: file.default_video_model,
defaultImageToVideoModel: file.default_image_to_video_model,
+4
View File
@@ -35,6 +35,7 @@ export interface ConfigFile {
output?: "text" | "json";
output_dir?: string;
timeout?: number;
watermark?: boolean;
default_text_model?: string;
default_video_model?: string;
default_image_to_video_model?: string;
@@ -63,6 +64,7 @@ export const CONFIG_FILE_KEYS = [
"output",
"output_dir",
"timeout",
"watermark",
"default_text_model",
"default_video_model",
"default_image_to_video_model",
@@ -156,6 +158,7 @@ export function parseConfigFile(raw: unknown): ConfigFile {
if (typeof obj.output_dir === "string" && obj.output_dir.length > 0)
out.output_dir = obj.output_dir;
if (typeof obj.timeout === "number" && obj.timeout > 0) out.timeout = obj.timeout;
if (typeof obj.watermark === "boolean") out.watermark = obj.watermark;
if (typeof obj.default_text_model === "string" && obj.default_text_model.length > 0)
out.default_text_model = obj.default_text_model;
if (typeof obj.default_video_model === "string" && obj.default_video_model.length > 0)
@@ -224,6 +227,7 @@ export interface Settings {
outputExplicit: boolean;
outputDir?: string;
timeout: number;
watermark: boolean;
defaultTextModel?: string;
defaultVideoModel?: string;
defaultImageToVideoModel?: string;
+3 -3
View File
@@ -34,7 +34,7 @@ export function resolveBooleanFlag(
return defaultWhenUnset;
}
/** Resolve `--watermark` flag; default true when unset. */
export function resolveWatermark(flagValue: unknown): boolean {
return parseOptionalBooleanValue(flagValue, "watermark") ?? true;
/** Resolve `--watermark`; command flag overrides config, then defaults to true. */
export function resolveWatermark(flagValue: unknown, configuredValue?: boolean): boolean {
return parseOptionalBooleanValue(flagValue, "watermark") ?? configuredValue ?? true;
}
@@ -79,6 +79,14 @@ test("default_speech_recognition_model 从配置文件进入运行时 Settings",
expect(resolve({ file }).defaultSpeechRecognitionModel).toBe("qwen-audio-3.0-asr-flash");
});
test("watermark 从配置文件进入 Settings,缺省时保持合规默认值 true", () => {
expect(parseConfigFile({ watermark: false }).watermark).toBe(false);
expect(parseConfigFile({ watermark: true }).watermark).toBe(true);
expect(parseConfigFile({ watermark: "false" }).watermark).toBeUndefined();
expect(resolve({ file: { watermark: false } }).watermark).toBe(false);
expect(resolve({}).watermark).toBe(true);
});
test("baseUrl:flag > env > file > 默认,所有来源统一归一化", () => {
const flags = { baseUrl: "https://flag.example.com/compatible-mode/v1?source=flag" };
const env = { DASHSCOPE_BASE_URL: "https://env.example.com/apps/anthropic#env" };
+1
View File
@@ -28,6 +28,7 @@ function makeSettings(configName?: string): Settings {
output: "json",
outputExplicit: false,
timeout: 30,
watermark: true,
verbose: false,
quiet: true,
dryRun: false,
+4
View File
@@ -31,6 +31,7 @@ function testDeps(identity: Partial<Identity> = {}): {
output: "json",
outputExplicit: true,
timeout: 30,
watermark: true,
verbose: false,
quiet: true,
dryRun: false,
@@ -290,6 +291,9 @@ test("resolveWatermark uses flag or defaults to true", () => {
expect(resolveWatermark("false")).toBe(false);
expect(resolveWatermark("true")).toBe(true);
expect(resolveWatermark(undefined)).toBe(true);
expect(resolveWatermark(undefined, false)).toBe(false);
expect(resolveWatermark("true", false)).toBe(true);
expect(resolveWatermark("false", true)).toBe(false);
});
test("resolveBooleanFlag uses flag or defaultWhenUnset", () => {
+1
View File
@@ -23,6 +23,7 @@ function testDeps(overrides?: Partial<Settings>): { identity: Identity; settings
output: "json",
outputExplicit: true,
timeout: 5,
watermark: true,
verbose: false,
quiet: true,
dryRun: false,
@@ -16,6 +16,22 @@ export interface PipelineEnv {
settings: Settings;
}
/** Media steps that inherit Profile `watermark` when the YAML omits the field. */
export const PIPELINE_WATERMARK_STEPS = new Set(["image/generate", "image/edit", "video/generate"]);
/**
* Fill Profile watermark into a planned/executed step input.
* Explicit step `watermark` wins; otherwise use Settings (file → default true).
*/
export function applyProfileWatermarkToStepInput(
stepType: string,
input: Record<string, unknown>,
settings: Settings,
): Record<string, unknown> {
if (!PIPELINE_WATERMARK_STEPS.has(stepType) || input.watermark !== undefined) return input;
return { ...input, watermark: settings.watermark };
}
/**
* Build the in-process env for pipeline steps. Uses the same source resolution
* as the CLI itself (env vars, config file; no CLI flags), but forces JSON
+9 -4
View File
@@ -1,5 +1,5 @@
import { PipelineError, toPipelineError } from "./errors.ts";
import { buildPipelineEnv } from "./bl-config.ts";
import { applyProfileWatermarkToStepInput, buildPipelineEnv } from "./bl-config.ts";
import { getDefaultStepDispatcher, type StepDispatcher } from "./dispatcher.ts";
import {
evaluateCondition,
@@ -156,12 +156,17 @@ async function executePipelineInternal(
if (options.dryRun) {
for (const planStep of topologicalOrder(plan)) {
const resolved = resolvePlannedStepInput(planStep.step, pipeline, normalizedRuntimeInput);
const plannedInput = applyProfileWatermarkToStepInput(
planStep.step.type,
resolved.redacted,
blEnv.settings,
);
const report: PipelineStepReport = {
id: planStep.step.id,
type: planStep.step.type,
status: "planned",
dependencies: planStep.dependencies,
input: resolved.redacted,
input: plannedInput,
...(planStep.step.when !== undefined ? { condition: "pending" } : {}),
};
reports.push(report);
@@ -170,14 +175,14 @@ async function executePipelineInternal(
timestamp: now(),
status: "planned",
step: stepEvent(planStep),
input: inputSummary(resolved.redacted, resolved.sensitiveKeys),
input: inputSummary(plannedInput, resolved.sensitiveKeys),
});
await emit({
type: "step.planned",
timestamp: now(),
status: "planned",
step: stepEvent(planStep),
input: inputSummary(resolved.redacted, resolved.sensitiveKeys),
input: inputSummary(plannedInput, resolved.sensitiveKeys),
...(planStep.step.when !== undefined ? { condition: "pending" as const } : {}),
});
}
@@ -193,7 +193,8 @@ export async function imageGenerate(
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
// Step input overrides Profile; omit → use Profile / CLI default (true).
watermark: resolveWatermark(input.watermark, env.settings.watermark),
};
const body: DashScopeImageRequest =
@@ -300,7 +301,8 @@ export async function imageEdit(
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
// Step input overrides Profile; omit → use Profile / CLI default (true).
watermark: resolveWatermark(input.watermark, env.settings.watermark),
};
let body: DashScopeImageRequest;
@@ -461,7 +463,8 @@ export async function videoGenerate(
ratio: input.ratio || undefined,
duration: input.duration,
prompt_extend: resolveBooleanFlag(input["prompt-extend"], undefined, "prompt-extend"),
watermark: resolveWatermark(input.watermark),
// Step input overrides Profile; omit → use Profile / CLI default (true).
watermark: resolveWatermark(input.watermark, env.settings.watermark),
seed: input.seed,
},
};
@@ -1,5 +1,9 @@
import { registerStep } from "../dispatcher.ts";
import { buildPipelineEnv, type PipelineEnv } from "../bl-config.ts";
import {
applyProfileWatermarkToStepInput,
buildPipelineEnv,
type PipelineEnv,
} from "../bl-config.ts";
import { isRecord } from "../utils.ts";
import {
textChat,
@@ -153,9 +157,13 @@ async function executeDirectBlStep(
input: Record<string, unknown>,
ctx: StepContext,
): Promise<StepResult> {
const env = (ctx.blEnv as PipelineEnv | undefined) ?? buildPipelineEnv();
if (ctx.dryRun) {
// Surface the Profile-effective watermark in dry-run so plans match runtime.
const plannedInput = applyProfileWatermarkToStepInput(id, input, env.settings);
return {
metadata: { dryRun: true, step: id, plannedInput: input },
metadata: { dryRun: true, step: id, plannedInput },
warnings: [
{ code: "dry_run_skipped", message: `Step ${id} was not executed in dry-run mode` },
],
@@ -167,7 +175,6 @@ async function executeDirectBlStep(
throw new Error(`No direct API handler registered for step: ${id}`);
}
const env = (ctx.blEnv as PipelineEnv | undefined) ?? buildPipelineEnv();
const data = await handler(env, input, ctx);
const builder = RESULT_BUILDERS[id];
if (builder) {
@@ -1,8 +1,9 @@
/** Shared --foo <bool> help text; keep wording consistent with actual CLI/request behavior. */
export const BOOL_FLAG_WATERMARK = {
"en-US": "Enable watermark (true/false). Omit flag to use CLI default (true).",
"zh-CN": "是否启用水印(true/false)。不传时使用 CLI 默认值 true。",
"en-US":
"Enable watermark (true/false). Omit flag to use the Profile setting or CLI default (true).",
"zh-CN": "是否启用水印(true/false)。不传时使用 Profile 配置或 CLI 默认值 true。",
};
/** CLI sends prompt_extend=true when flag omitted (qwen-image edit, etc.). */
@@ -0,0 +1,129 @@
import { expect, test } from "vite-plus/test";
import type { Client } from "bailian-cli-core";
import { applyProfileWatermarkToStepInput, type PipelineEnv } from "../src/pipeline/bl-config.ts";
import { imageEdit, imageGenerate, videoGenerate } from "../src/pipeline/steps/bl-api.ts";
import type { StepContext } from "../src/pipeline/types.ts";
type CapturedRequest = {
path?: string;
method?: string;
body?: {
parameters?: { watermark?: boolean };
};
async?: boolean;
};
function makeEnv(watermark: boolean): {
env: PipelineEnv;
captured: CapturedRequest[];
} {
const captured: CapturedRequest[] = [];
const client = {
uploadFile: async (source: string) => source,
requestJson: async (opts: CapturedRequest) => {
captured.push(opts);
if (opts.async) {
return { output: { task_id: "task-wm", task_status: "PENDING" } };
}
// Sync image response shape (qwen-image / wan2.7-image).
return {
request_id: "req-wm",
output: {
choices: [{ message: { content: [{ image: "https://example.com/out.png" }] } }],
},
};
},
} as unknown as Client;
return {
env: {
client,
settings: {
quiet: true,
output: "json",
outputExplicit: true,
timeout: 300,
watermark,
verbose: false,
dryRun: false,
telemetry: false,
} as PipelineEnv["settings"],
},
captured,
};
}
function makeCtx(): StepContext {
return { dryRun: false, signal: new AbortController().signal, timeoutSeconds: 1 };
}
test("pipeline imageGenerate inherits Profile watermark=false when step omits it", async () => {
const { env, captured } = makeEnv(false);
await imageGenerate(env, { prompt: "A cat" }, makeCtx());
expect(captured[0]?.body?.parameters?.watermark).toBe(false);
});
test("pipeline imageGenerate keeps step watermark over Profile", async () => {
const { env, captured } = makeEnv(false);
await imageGenerate(env, { prompt: "A cat", watermark: true }, makeCtx());
expect(captured[0]?.body?.parameters?.watermark).toBe(true);
});
test("pipeline imageEdit inherits Profile watermark=false when step omits it", async () => {
const { env, captured } = makeEnv(false);
await imageEdit(env, { prompt: "Blue sky", image: "https://example.com/in.png" }, makeCtx());
expect(captured[0]?.body?.parameters?.watermark).toBe(false);
});
test("pipeline videoGenerate inherits Profile watermark=false when step omits it", async () => {
const { env, captured } = makeEnv(false);
// First call submits async task; pollTaskWithOptions will keep polling — mock SUCCEEDED quickly.
const client = env.client as unknown as {
requestJson: (opts: CapturedRequest) => Promise<unknown>;
};
let calls = 0;
client.requestJson = async (opts: CapturedRequest) => {
captured.push(opts);
calls += 1;
if (calls === 1) {
return { output: { task_id: "task-wm", task_status: "PENDING" } };
}
return {
output: {
task_id: "task-wm",
task_status: "SUCCEEDED",
video_url: "https://example.com/out.mp4",
},
};
};
await videoGenerate(
env,
{ prompt: "A cat walks", "poll-interval": 0 },
{ ...makeCtx(), timeoutSeconds: 5 },
);
expect(captured[0]?.body?.parameters?.watermark).toBe(false);
});
test("pipeline defaults watermark to true when Profile leaves the compliance default", async () => {
const { env, captured } = makeEnv(true);
await imageGenerate(env, { prompt: "A cat" }, makeCtx());
expect(captured[0]?.body?.parameters?.watermark).toBe(true);
});
test("applyProfileWatermarkToStepInput fills omitted watermark from Settings", () => {
const settings = { watermark: false } as PipelineEnv["settings"];
expect(
applyProfileWatermarkToStepInput("image/generate", { prompt: "A cat" }, settings).watermark,
).toBe(false);
expect(
applyProfileWatermarkToStepInput(
"image/generate",
{ prompt: "A cat", watermark: true },
settings,
).watermark,
).toBe(true);
expect(
applyProfileWatermarkToStepInput("text/chat", { message: "hi" }, settings).watermark,
).toBeUndefined();
});
+8 -4
View File
@@ -88,10 +88,10 @@ bl config list --output json
#### Flags
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--key <key>` | string | yes | Config key (language, base*url, output, output_dir, timeout, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default*\*\_model, workspace_id) |
| `--value <value>` | string | yes | Value to set |
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--key <key>` | string | yes | Config key (language, base*url, output, output_dir, timeout, watermark, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default*\*\_model, workspace_id) |
| `--value <value>` | string | yes | Value to set |
#### Examples
@@ -107,6 +107,10 @@ bl config set --key output --value json
bl config set --key timeout --value 600
```
```bash
bl config set --key watermark --value false
```
```bash
bl config set --key base_url --value https://dashscope.aliyuncs.com
```
+2 -2
View File
@@ -36,7 +36,7 @@ Index: [index.md](index.md)
| `--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). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use the Profile setting or 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 |
@@ -99,7 +99,7 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use the Profile setting or CLI default (true). |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--out-dir <dir>` | string | no | Download images to directory |
+22 -22
View File
@@ -56,26 +56,26 @@ bl video download --task-id 3b256896-xxxx --out video.mp4 --quiet
#### Flags
| Flag | Type | Required | Description |
| -------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.0-video-edit) |
| `--video <url>` | string | yes | Input video URL or local file (mp4/mov, 2-10s) |
| `--prompt <text>` | string | no | Edit instruction (e.g. "Convert the scene to a claymation style") |
| `--ref-image <url>` | string | no | Reference image URL (up to 4, comma-separated) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4) |
| `--duration <seconds>` | number | no | Output video duration in seconds (2-10) |
| `--audio-setting <auto\|origin>` | string | no | Audio: auto (default) or origin (keep original) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--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: 15) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------ |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.0-video-edit) |
| `--video <url>` | string | yes | Input video URL or local file (mp4/mov, 2-10s) |
| `--prompt <text>` | string | no | Edit instruction (e.g. "Convert the scene to a claymation style") |
| `--ref-image <url>` | string | no | Reference image URL (up to 4, comma-separated) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4) |
| `--duration <seconds>` | number | no | Output video duration in seconds (2-10) |
| `--audio-setting <auto\|origin>` | string | no | Audio: auto (default) or origin (keep original) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use the Profile setting or CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--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: 15) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -117,7 +117,7 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
| `--ratio <ratio>` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use the Profile setting or CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--file <url-or-path>` | string | no | Reference file URL or local path for file-to-video (wan3.0-video only; mutually exclusive with --image/--last-frame) |
@@ -172,7 +172,7 @@ bl video generate --prompt "A cat playing with a ball" --watermark false
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use the Profile setting or CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--async` | switch | no | Return async task id without waiting |