mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Merge pull request #194 from modelstudioai/feat/global-watermark-config
Feat/global watermark config
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -28,6 +28,7 @@ function makeSettings(configName?: string): Settings {
|
||||
output: "json",
|
||||
outputExplicit: false,
|
||||
timeout: 30,
|
||||
watermark: true,
|
||||
verbose: false,
|
||||
quiet: true,
|
||||
dryRun: false,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user