mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
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
This commit is contained in:
@@ -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 <id>",
|
||||
usageArgs: "--file-id <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) {
|
||||
|
||||
@@ -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 <id> [--skip-precheck]",
|
||||
usageArgs: "--deployed-model <id> [--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) {
|
||||
|
||||
@@ -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 <id>",
|
||||
usageArgs: "--job-id <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) {
|
||||
|
||||
@@ -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 <model> [--rpm <n>] [--tpm <n>] [--delete]",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--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",
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -150,11 +150,11 @@ 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 <model> [--rpm <n>] [--tpm <n>] [--delete]` |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -164,12 +164,14 @@ bl quota list --output json
|
||||
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
|
||||
| `--tpm <n>` | 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 <key>` | string | no | API key |
|
||||
| `--base-url <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
|
||||
```
|
||||
|
||||
@@ -20,20 +20,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 <id>` |
|
||||
| **Usage** | `bl dataset delete --file-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------- |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the dataset file is permanently removed.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -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 |
|
||||
|
||||
@@ -74,11 +74,11 @@ 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 <id> [--skip-precheck]` |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,9 +86,14 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------- |
|
||||
| `--deployed-model <id>` | 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 <key>` | string | no | API key |
|
||||
| `--base-url <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 |
|
||||
|
||||
@@ -182,22 +182,24 @@ 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 <id>` |
|
||||
| **Usage** | `bl finetune delete --job-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | --------------------------- |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <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 |
|
||||
|
||||
Reference in New Issue
Block a user