From 38926aeb226708e55c596b8948ad3a8dcab3e467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=95=85=E7=92=83?= Date: Mon, 31 Aug 2026 19:26:31 +0800 Subject: [PATCH] feat: require --yes confirmation for destructive operations - finetune/deploy/dataset delete: add --yes flag + confirmDangerousAction - quota update --delete: confirm before clearing rate limits - non-TTY runs without --yes fail with a USAGE error - add e2e coverage and regenerate skill references --- .../commands/src/commands/dataset/delete.ts | 25 +++++++++++-- .../commands/src/commands/deploy/delete.ts | 25 +++++++++++-- .../commands/src/commands/finetune/delete.ts | 19 ++++++++-- .../commands/src/commands/quota/update.ts | 23 ++++++++++-- .../commands/tests/e2e/dataset.e2e.test.ts | 36 +++++++++++++++++++ .../commands/tests/e2e/deploy.e2e.test.ts | 19 ++++++++++ .../commands/tests/e2e/finetune.e2e.test.ts | 21 +++++++++++ packages/commands/tests/e2e/quota.e2e.test.ts | 12 +++++++ skills/bailian-cli/reference/quota.md | 18 ++++++---- skills/bailian-finetune/reference/dataset.md | 31 ++++++++++------ skills/bailian-finetune/reference/deploy.md | 21 +++++++---- skills/bailian-finetune/reference/finetune.md | 28 +++++++++------ 12 files changed, 233 insertions(+), 45 deletions(-) diff --git a/packages/commands/src/commands/dataset/delete.ts b/packages/commands/src/commands/dataset/delete.ts index 520cfaf4e..cc8115c89 100644 --- a/packages/commands/src/commands/dataset/delete.ts +++ b/packages/commands/src/commands/dataset/delete.ts @@ -1,5 +1,5 @@ import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime"; const DELETE_FLAGS = { fileId: { @@ -8,14 +8,28 @@ const DELETE_FLAGS = { description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" }, required: true, }, + yes: { + type: "switch", + description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" }, + }, } satisfies FlagsDef; export default defineCommand({ description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" }, auth: "apiKey", - usageArgs: "--file-id ", + usageArgs: "--file-id [--yes]", flags: DELETE_FLAGS, - exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"], + exampleArgs: [ + "--file-id file-id-xxx", + "--file-id file-id-xxx --dry-run", + "--file-id file-id-xxx --yes", + ], + notes: [ + { + "en-US": "Irreversible — the dataset file is permanently removed.", + "zh-CN": "该操作不可撤销——数据集文件将被永久删除。", + }, + ], async run(ctx) { const { settings, flags } = ctx; const fileId = flags.fileId; @@ -25,6 +39,11 @@ export default defineCommand({ return; } + await confirmDangerousAction( + `Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`, + flags.yes ?? false, + ); + const response = await deleteDataset(ctx.client, fileId); if (settings.quiet) { diff --git a/packages/commands/src/commands/deploy/delete.ts b/packages/commands/src/commands/deploy/delete.ts index bb31298ae..60cb809be 100644 --- a/packages/commands/src/commands/deploy/delete.ts +++ b/packages/commands/src/commands/deploy/delete.ts @@ -6,7 +6,7 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime"; const DELETE_FLAGS = { deployedModel: { @@ -25,6 +25,10 @@ const DELETE_FLAGS = { "zh-CN": "跳过本地 STOPPED/FAILED 状态预检查", }, }, + yes: { + type: "switch", + description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" }, + }, } satisfies FlagsDef; /** @@ -40,9 +44,19 @@ export default defineCommand({ "zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)", }, auth: "apiKey", - usageArgs: "--deployed-model [--skip-precheck]", + usageArgs: "--deployed-model [--skip-precheck] [--yes]", flags: DELETE_FLAGS, - exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"], + notes: [ + { + "en-US": "Irreversible — the deployment is permanently destroyed.", + "zh-CN": "该操作不可撤销——部署将被永久销毁。", + }, + ], + exampleArgs: [ + "--deployed-model dep-...", + "--deployed-model dep-... --dry-run", + "--deployed-model dep-... --yes", + ], async run(ctx) { const { settings, flags } = ctx; const deployedModel = flags.deployedModel; @@ -73,6 +87,11 @@ export default defineCommand({ } } + await confirmDangerousAction( + `Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`, + flags.yes ?? false, + ); + const response = await deleteDeployment(ctx.client, deployedModel); if (settings.quiet) { diff --git a/packages/commands/src/commands/finetune/delete.ts b/packages/commands/src/commands/finetune/delete.ts index c5d7c8f90..19c1caae3 100644 --- a/packages/commands/src/commands/finetune/delete.ts +++ b/packages/commands/src/commands/finetune/delete.ts @@ -1,5 +1,5 @@ import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime"; const DELETE_FLAGS = { jobId: { @@ -8,15 +8,23 @@ const DELETE_FLAGS = { description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" }, required: true, }, + yes: { + type: "switch", + description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" }, + }, } satisfies FlagsDef; export default defineCommand({ description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" }, auth: "apiKey", - usageArgs: "--job-id ", + usageArgs: "--job-id [--yes]", flags: DELETE_FLAGS, - exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"], + exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"], notes: [ + { + "en-US": "Irreversible — the job record is permanently removed.", + "zh-CN": "该操作不可撤销——任务记录将被永久删除。", + }, { "en-US": "Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.", @@ -32,6 +40,11 @@ export default defineCommand({ return; } + await confirmDangerousAction( + `Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`, + flags.yes ?? false, + ); + const response = await deleteFineTune(ctx.client, jobId); if (settings.quiet) { diff --git a/packages/commands/src/commands/quota/update.ts b/packages/commands/src/commands/quota/update.ts index c90030494..8b69688bc 100644 --- a/packages/commands/src/commands/quota/update.ts +++ b/packages/commands/src/commands/quota/update.ts @@ -1,5 +1,5 @@ import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { emitResult, confirmDangerousAction } from "bailian-cli-runtime"; import { formatNumber } from "../shared/format.ts"; const MINUTE_SECONDS = 60; @@ -10,7 +10,7 @@ export default defineCommand({ "zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置", }, auth: "apiKey", - usageArgs: "--model [--rpm ] [--tpm ] [--delete]", + usageArgs: "--model [--rpm ] [--tpm ] [--delete] [--yes]", flags: { model: { type: "string", @@ -41,11 +41,19 @@ export default defineCommand({ "zh-CN": "清除该模型的所有自定义限流配置", }, }, + yes: { + type: "switch", + description: { + "en-US": "Skip the confirmation prompt for --delete", + "zh-CN": "使用 --delete 时跳过确认提示", + }, + }, }, exampleArgs: [ "--model qwen-plus --rpm 60 --tpm 100000", "--model qwen3-max --tpm 500000", "--model qwen-plus --delete", + "--model qwen-plus --delete --yes", "--model qwen-plus --rpm 60 --output json", ], notes: [ @@ -55,6 +63,10 @@ export default defineCommand({ "zh-CN": "未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。", }, + { + "en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.", + "zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。", + }, { "en-US": "Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.", @@ -98,6 +110,13 @@ export default defineCommand({ return; } + if (flags.delete) { + await confirmDangerousAction( + `Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`, + flags.yes ?? false, + ); + } + const result = await ctx.client.requestJson<{ request_id?: string }>({ path: modelsLimitsPath(), method: "POST", diff --git a/packages/commands/tests/e2e/dataset.e2e.test.ts b/packages/commands/tests/e2e/dataset.e2e.test.ts index 0da6bb497..eb67b1926 100644 --- a/packages/commands/tests/e2e/dataset.e2e.test.ts +++ b/packages/commands/tests/e2e/dataset.e2e.test.ts @@ -292,6 +292,42 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { expect(exitCode, stdout + stderr).not.toBe(0); expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/); }); + + test("dataset delete --help 展示 --yes", async () => { + const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ + "dataset", + "delete", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--yes/i); + }); + + test("dataset delete --dry-run 发出结构化动作", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ + "dataset", + "delete", + "--file-id", + "file-id-xxx", + "--dry-run", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ action: string }>(stdout); + expect(data.action).toBe("dataset.delete"); + }); + + test("dataset delete 非 TTY 无 --yes 报 USAGE (2)", async () => { + const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ + "dataset", + "delete", + "--file-id", + "file-id-xxx", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--yes/); + }); }); describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => { diff --git a/packages/commands/tests/e2e/deploy.e2e.test.ts b/packages/commands/tests/e2e/deploy.e2e.test.ts index 0510c3c32..f9f1d49f0 100644 --- a/packages/commands/tests/e2e/deploy.e2e.test.ts +++ b/packages/commands/tests/e2e/deploy.e2e.test.ts @@ -201,6 +201,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { const data = parseStdoutJson<{ action: string }>(stdout); expect(data.action).toBe(`deploy.${sub}`); }); + + test("deploy delete --help 展示 --yes", async () => { + const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "delete", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--yes/i); + }); + + test("deploy delete 非 TTY 无 --yes 报 USAGE (2)", async () => { + // --skip-precheck 保证确认门在发任何网络请求前触发 + const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ + "deploy", + "delete", + "--deployed-model", + "dep-xxx", + "--skip-precheck", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--yes/); + }); }); describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => { diff --git a/packages/commands/tests/e2e/finetune.e2e.test.ts b/packages/commands/tests/e2e/finetune.e2e.test.ts index 8e0ea7aa8..1a6b87b6e 100644 --- a/packages/commands/tests/e2e/finetune.e2e.test.ts +++ b/packages/commands/tests/e2e/finetune.e2e.test.ts @@ -294,6 +294,27 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { expect(data.action).toBe(`finetune.${sub}`); }); + test("finetune delete --help 展示 --yes", async () => { + const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + "finetune", + "delete", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--yes/i); + }); + + test("finetune delete 非 TTY 无 --yes 报 USAGE (2)", async () => { + const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + "finetune", + "delete", + "--job-id", + "ft-xxx", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--yes/); + }); + test("finetune create --dry-run 解析多 datasets 中的空白", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", diff --git a/packages/commands/tests/e2e/quota.e2e.test.ts b/packages/commands/tests/e2e/quota.e2e.test.ts index 9dbf4932d..c6e2d3859 100644 --- a/packages/commands/tests/e2e/quota.e2e.test.ts +++ b/packages/commands/tests/e2e/quota.e2e.test.ts @@ -32,6 +32,7 @@ describe("e2e: quota", () => { expect(stderr).toContain("--rpm"); expect(stderr).toContain("--tpm"); expect(stderr).toContain("--delete"); + expect(stderr).toContain("--yes"); }); test("quota request 作为 quota update 的兼容别名可用", async () => { @@ -92,6 +93,17 @@ describe("e2e: quota", () => { expect(stderr).toContain("cannot be combined"); }); + test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => { + // 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发 + const { stderr, exitCode } = await runCommandE2e( + QUOTA_ROUTES, + ["quota", "update", "--model", "qwen-plus", "--delete"], + { DASHSCOPE_API_KEY: "sk-e2e-quota-delete" }, + ); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--yes/); + }); + test("quota update --rpm 负数报错", async () => { const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", diff --git a/skills/bailian-cli/reference/quota.md b/skills/bailian-cli/reference/quota.md index 755b98ac1..97b51231c 100644 --- a/skills/bailian-cli/reference/quota.md +++ b/skills/bailian-cli/reference/quota.md @@ -149,12 +149,12 @@ bl quota list --output json ### `bl quota update` -| Field | Value | -| ------------------ | -------------------------------------------------------------------- | -| **Name** | `quota update` | -| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete | -| **Authentication** | API Key | -| **Usage** | `bl quota update --model [--rpm ] [--tpm ] [--delete]` | +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------- | +| **Name** | `quota update` | +| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete | +| **Authentication** | API Key | +| **Usage** | `bl quota update --model [--rpm ] [--tpm ] [--delete] [--yes]` | #### Flags @@ -164,12 +164,14 @@ bl quota list --output json | `--rpm ` | number | no | Max requests per minute (QPM) | | `--tpm ` | number | no | Max tokens per minute (TPM) | | `--delete` | switch | no | Clear all custom rate limits for the model | +| `--yes` | switch | no | Skip the confirmation prompt for --delete | | `--api-key ` | string | no | API key | | `--base-url ` | string | no | API base URL | #### Notes - Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits. +- --delete requires confirmation; pass --yes to skip the prompt in scripts. - Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together. #### Examples @@ -186,6 +188,10 @@ bl quota update --model qwen3-max --tpm 500000 bl quota update --model qwen-plus --delete ``` +```bash +bl quota update --model qwen-plus --delete --yes +``` + ```bash bl quota update --model qwen-plus --rpm 60 --output json ``` diff --git a/skills/bailian-finetune/reference/dataset.md b/skills/bailian-finetune/reference/dataset.md index 59057b17f..f1df16b84 100644 --- a/skills/bailian-finetune/reference/dataset.md +++ b/skills/bailian-finetune/reference/dataset.md @@ -19,20 +19,25 @@ Index: [index.md](index.md) ### `bl dataset delete` -| Field | Value | -| ------------------ | ---------------------------------- | -| **Name** | `dataset delete` | -| **Description** | Delete a dataset file by ID | -| **Authentication** | API Key | -| **Usage** | `bl dataset delete --file-id ` | +| Field | Value | +| ------------------ | ------------------------------------------ | +| **Name** | `dataset delete` | +| **Description** | Delete a dataset file by ID | +| **Authentication** | API Key | +| **Usage** | `bl dataset delete --file-id [--yes]` | #### Flags -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | -------------------------- | -| `--file-id ` | string | yes | Dataset file ID (required) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | ---------------------------- | +| `--file-id ` | string | yes | Dataset file ID (required) | +| `--yes` | switch | no | Skip the confirmation prompt | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- Irreversible — the dataset file is permanently removed. #### Examples @@ -44,6 +49,10 @@ bl dataset delete --file-id file-id-xxx bl dataset delete --file-id file-id-xxx --dry-run ``` +```bash +bl dataset delete --file-id file-id-xxx --yes +``` + ### `bl dataset get` | Field | Value | diff --git a/skills/bailian-finetune/reference/deploy.md b/skills/bailian-finetune/reference/deploy.md index c73a30cfe..ea6299a3e 100644 --- a/skills/bailian-finetune/reference/deploy.md +++ b/skills/bailian-finetune/reference/deploy.md @@ -73,12 +73,12 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry- ### `bl deploy delete` -| Field | Value | -| ------------------ | ---------------------------------------------------------- | -| **Name** | `deploy delete` | -| **Description** | Delete a model deployment (must be STOPPED or FAILED) | -| **Authentication** | API Key | -| **Usage** | `bl deploy delete --deployed-model [--skip-precheck]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------ | +| **Name** | `deploy delete` | +| **Description** | Delete a model deployment (must be STOPPED or FAILED) | +| **Authentication** | API Key | +| **Usage** | `bl deploy delete --deployed-model [--skip-precheck] [--yes]` | #### Flags @@ -86,9 +86,14 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry- | ----------------------- | ------ | -------- | --------------------------------------------- | | `--deployed-model ` | string | yes | Deployed model identifier (required) | | `--skip-precheck` | switch | no | Skip the local STOPPED/FAILED status precheck | +| `--yes` | switch | no | Skip the confirmation prompt | | `--api-key ` | string | no | API key | | `--base-url ` | string | no | API base URL | +#### Notes + +- Irreversible — the deployment is permanently destroyed. + #### Examples ```bash @@ -99,6 +104,10 @@ bl deploy delete --deployed-model dep-... bl deploy delete --deployed-model dep-... --dry-run ``` +```bash +bl deploy delete --deployed-model dep-... --yes +``` + ### `bl deploy get` | Field | Value | diff --git a/skills/bailian-finetune/reference/finetune.md b/skills/bailian-finetune/reference/finetune.md index b0a8f4a14..4ec0887fc 100644 --- a/skills/bailian-finetune/reference/finetune.md +++ b/skills/bailian-finetune/reference/finetune.md @@ -181,23 +181,25 @@ bl finetune checkpoints --job-id ft-xxx --output json ### `bl finetune delete` -| Field | Value | -| ------------------ | ---------------------------------- | -| **Name** | `finetune delete` | -| **Description** | Delete a fine-tune job record | -| **Authentication** | API Key | -| **Usage** | `bl finetune delete --job-id ` | +| Field | Value | +| ------------------ | ------------------------------------------ | +| **Name** | `finetune delete` | +| **Description** | Delete a fine-tune job record | +| **Authentication** | API Key | +| **Usage** | `bl finetune delete --job-id [--yes]` | #### Flags -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | --------------------------- | -| `--job-id ` | string | yes | Fine-tune job ID (required) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | ---------------------------- | +| `--job-id ` | string | yes | Fine-tune job ID (required) | +| `--yes` | switch | no | Skip the confirmation prompt | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Notes +- Irreversible — the job record is permanently removed. - Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight. #### Examples @@ -210,6 +212,10 @@ bl finetune delete --job-id ft-xxx bl finetune delete --job-id ft-xxx --dry-run ``` +```bash +bl finetune delete --job-id ft-xxx --yes +``` + ### `bl finetune export` | Field | Value |