Compare commits

..

8 Commits

Author SHA1 Message Date
故璃 38926aeb22 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
2026-08-31 19:26:31 +08:00
Gong Shiqi e3f2996291 Merge pull request #181 from modelstudioai/perf/registry-smoke-inprocess
perf(test): reduce CLI E2E startup overhead
2026-08-31 10:59:51 +08:00
若麒 59250c4f44 perf(test): streamline command e2e execution 2026-08-28 19:14:06 +08:00
若麒 98ebeb398c perf(test): run registry smoke help checks in process 2026-08-28 16:54:55 +08:00
Gong Shiqi 5c96077d9f Merge pull request #179 from modelstudioai/feat/cancel-key-auth
chore(release): prepare 1.18.1
2026-08-28 15:50:42 +08:00
若麒 8a7674e7c3 chore(release): prepare 1.18.1 2026-08-28 15:37:15 +08:00
Gong Shiqi 4fdd8389f8 Merge pull request #178 from modelstudioai/feat/cancel-key-auth
feat: cancel validate API key
2026-08-28 14:29:45 +08:00
rendianmeng bd8b08b617 feat: cancel validate API key 2026-08-28 13:10:56 +08:00
88 changed files with 902 additions and 598 deletions
+6
View File
@@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.18.1] - 2026-08-28
### Removed
- Removed API Key validation from `bl auth login`.
## [1.18.0] - 2026-08-27
### Added
+6
View File
@@ -6,6 +6,12 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.18.1] - 2026-08-28
### 已移除
- 移除 `bl auth login` 的 API Key 校验。
## [1.18.0] - 2026-08-27
### 新增
+3 -3
View File
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
`~/.bailian/config.json` 可同时保存 `api_key``access_token``access_key_*`。登录任一种方式不得删除另一种:
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址并把该预设的默认模型物化写入。API Key 验证成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi``console` 域凭证
- `bl auth logout --console` 只清 `access_token`
@@ -47,9 +47,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `--config` 只选择 config 文件 block不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
- `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profilepreset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profilepreset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败warning 与多行 `error` 对象以空行分隔stdout 仍只保留命令结果
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile未传
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile未传
`--config` 时继续写当前激活项,失败和 dry-run 不切换
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
- `resolveOpenApi()``auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
+21 -15
View File
@@ -2,16 +2,16 @@
## 架构分层
| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup`private`,不发布) |
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参dry-runlivegated每用例最小路由 |
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated`journeys/README.md` |
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help根 help |
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | `kscli/src/commands.ts` 推导 path/分组identity`--version``search --help` path |
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup`private`,不发布) |
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/livegated每用例最小路由 |
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated`journeys/README.md` |
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help根 help、鉴权域等代表性子进程冒烟 |
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help`--version``search --help` 等代表性子进程冒烟 |
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
**依赖边界**`e2e``core``commands/tests``e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
**依赖边界**`e2e``core``commands/tests``e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
## 触发条件
@@ -29,7 +29,8 @@
### commands E2E
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
- 子进程`runCommandE2e(routes, args)` from `./helpers.ts`spawn `harness/main.ts``routes` 为本 topic 最小 path → export 映射
- help`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程
- 子进程缺参、dry-run、live 使用 `runCommandE2e(routes, args)`spawn `harness/main.ts``routes` 为本 topic 最小 path → export 映射)
- fixtures`packages/commands/tests/e2e/fixtures/`
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map
@@ -37,10 +38,12 @@
- bl`runCli` from `packages/cli/tests/e2e/helpers.ts`
- kscli`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
### 共享
- gating / output / runner`e2e/gating``e2e/output``e2e/runner`
- gating / output / runner`e2e/gating``e2e/output``e2e/runner`runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
- globalSetup`vite.config.ts``packages/e2e/src/global-setup.ts`
- 解析 JSON stdout`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
@@ -48,9 +51,12 @@
## 双层 describe固定结构
```ts
// 1) 不 skip--help无密钥、无真实 API分组 help 由 bl registry.smoke 覆盖)
// 1) 不 skip进程内 --help无密钥、无真实 API分组 help 由 bl registry.smoke 覆盖)
describe("e2e: <topic>", () => {
test("<subcommand> --help 正常退出", ...);
test("<subcommand> --help 正常退出", async () => {
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
expect(result.exitCode, result.stderr).toBe(0);
});
});
// 2) skipIf缺参 / dry-run / 真实集成
@@ -74,7 +80,7 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
## 用例类型
1. **--help**`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
1. **--help**`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
2. **缺参**:带无害全局 flag`--quiet`)且不传 required flag → `exitCode === 2`
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
4. **真实集成**:放在 skip 块**末尾**
@@ -105,7 +111,7 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
- [ ] 至少一条真实集成(或说明为何仅 smoke不破坏已有集成用例顺序
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
+3 -3
View File
@@ -18,11 +18,11 @@
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
- 激活状态只选择配置 block不改变字段优先级字段仍为 flag > env > selected config > 默认值。
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset升级内置 Plan Profile 的 preset 需要重新登录。
- 对命中内置 preset 的 ProfileAPI Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘同时保留已有项且不做删除Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
- 对命中内置 preset 的 ProfileAPI Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘同时保留已有项且不做删除Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get``video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
@@ -30,7 +30,7 @@
## 3. 保持读写命令交互一致
- `auth login``config set` 等写命令未传 `--config` 时修改当前激活项。
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
创建和激活;`config set --config <name>` 可创建但不自动激活。
- `config show``auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.18.0",
"version": "1.18.1",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
@@ -1,14 +1,26 @@
import { describe, expect, test } from "vite-plus/test";
import { deriveGroupPaths } from "e2e/registry-smoke";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterAll, describe, expect, test } from "vite-plus/test";
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
import { CommandRegistry, resolve } from "bailian-cli-runtime";
import { commands } from "../../src/commands.ts";
import { runCli } from "./helpers.ts";
const commandPaths = Object.keys(commands).sort();
const groupPaths = deriveGroupPaths(commandPaths);
const registry = new CommandRegistry(commands, "bl");
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
function runCliSmoke(args: string[]) {
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
}
describe("e2e: bl registry smoke", () => {
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
const { stderr, exitCode } = await runCli(["--help"]);
const { stderr, exitCode } = await runCliSmoke(["--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/\bbl\b/i);
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
});
test("分组帮助按叶子命令展示不同鉴权域", async () => {
const { stderr, exitCode } = await runCli(["app", "--help"]);
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
[["token-plan", "list-seats"], "AK/SK"],
[["config", "show"], "No Auth"],
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain(`Authentication: ${authLabel}`);
});
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/Global Flags:/);
expect(stderr).toMatch(/--console-region <region>/);
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
});
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
expect(exitCode, stderr).toBe(0);
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
const commandPath = path.split(" ");
expect(resolve([...commandPath, "--help"], registry)).toEqual({
kind: "help",
path: commandPath,
});
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
});
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
expect(exitCode, stderr).toBe(0);
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
const commandPath = path.split(" ");
expect(resolve([...commandPath, "--help"], registry)).toEqual({
kind: "help",
path: commandPath,
});
expect(captureRegistryHelp(registry, commandPath)).toContain(
`Usage: bl ${path} <command> [flags]`,
);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.18.0",
"version": "1.18.1",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
@@ -1,23 +1,10 @@
import {
BailianError,
ExitCode,
chatPath,
requestJson,
normalizeModelBaseUrl,
type AuthPersistPatch,
type AuthStore,
type Identity,
type Settings,
} from "bailian-cli-core";
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
interface ApiKeyLoginDeps {
identity: Identity;
settings: Settings;
authStore: AuthStore;
}
interface ApiKeyLoginProfile {
baseUrl: string;
persistBaseUrl?: string;
defaultTextModel?: string;
defaultVideoModel?: string;
@@ -30,65 +17,19 @@ interface ApiKeyLoginProfile {
persistPatch?: AuthPersistPatch;
}
const RETRY_DELAY_BASE_MS = 500;
function canRetry(error: unknown): boolean {
if (error instanceof BailianError) {
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
const status = error.api?.httpStatus;
return status === 401 || (status !== undefined && status >= 500);
}
if (error instanceof Error) {
return (
error.name === "AbortError" ||
error.name === "TimeoutError" ||
error.message.includes("timed out") ||
error.message === "fetch failed"
);
}
return false;
}
export async function validateAndPersistApiKey(
/**
* Persist an API key (and optional profile defaults) without a live model probe.
* Login is credential storage; connectivity is verified on the first API command.
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
*/
export async function persistApiKey(
deps: ApiKeyLoginDeps,
key: string,
profile: ApiKeyLoginProfile,
): Promise<void> {
process.stderr.write("Testing key... ");
const httpDeps = { identity: deps.identity, settings: deps.settings };
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
const persistBaseUrl = profile.persistBaseUrl
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = "qwen3.8-max";
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body: {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
},
};
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
break;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) {
process.stderr.write("Failed\n");
throw error;
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
process.stderr.write("Valid\n");
await deps.authStore.login({
...profile.persistPatch,
api_key: key,
@@ -11,7 +11,7 @@ import {
type Settings,
} from "bailian-cli-core";
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
import { validateAndPersistApiKey } from "./login-api-key.ts";
import { persistApiKey } from "./login-api-key.ts";
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
export interface LoginDeps {
@@ -413,9 +413,7 @@ export async function runConsoleLogin(
workspace_id: workspaceId || undefined,
};
if (apiKey) {
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
await validateAndPersistApiKey(deps, apiKey, {
baseUrl: testBaseUrl,
await persistApiKey(deps, apiKey, {
persistBaseUrl: baseUrl || undefined,
persistPatch: callbackPatch,
});
+6 -7
View File
@@ -5,7 +5,7 @@ import {
normalizeModelBaseUrl,
} from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
import { validateAndPersistApiKey } from "./login-api-key.ts";
import { persistApiKey } from "./login-api-key.ts";
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
@@ -33,8 +33,8 @@ export default defineCommand({
type: "string",
valueHint: "<url>",
description: {
"en-US": "Model API base URL (used with --api-key for validation)",
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
"en-US": "Model API base URL to store with --api-key",
"zh-CN": "与 --api-key 一并保存的模型 API Base URL",
},
},
console: {
@@ -164,19 +164,17 @@ export default defineCommand({
if (!key) return;
if (settings.dryRun) {
emitBare("Would validate and save API key.");
emitBare("Would save API key.");
return;
}
const profilePreset = getModelProfilePreset(settings.configName);
const stored = store.stored();
const storedBaseUrl = stored.baseUrl;
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
const apiKeyCapabilities = profilePreset
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
: stored.apiKeyCapabilities;
await validateAndPersistApiKey(deps, key, {
baseUrl: resolvedBaseUrl,
await persistApiKey(deps, key, {
persistBaseUrl,
defaultTextModel: profilePreset?.defaultTextModel,
defaultVideoModel: profilePreset?.defaultVideoModel,
@@ -187,5 +185,6 @@ export default defineCommand({
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
apiKeyCapabilities,
});
process.stderr.write(`API key saved to ${store.path}\n`);
},
});
@@ -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) {
+21 -2
View File
@@ -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",
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { ADVISOR_ROUTES } from "./topic-routes.ts";
describe("e2e: advisor recommend", () => {
test("advisor recommend --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(ADVISOR_ROUTES, [
"advisor",
"recommend",
"--help",
+188 -288
View File
@@ -1,6 +1,4 @@
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import http from "node:http";
import type { AddressInfo } from "node:net";
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, test } from "vite-plus/test";
@@ -9,60 +7,16 @@ import {
isOpenApiE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { AUTH_ROUTES } from "./topic-routes.ts";
interface ValidationServer {
baseUrl: string;
requests: Array<{
path: string;
body: Record<string, unknown>;
authorization?: string;
sourceConfig?: string;
openApiSource?: string;
}>;
close(): Promise<void>;
}
async function startValidationServer(statusCode = 200): Promise<ValidationServer> {
const requests: ValidationServer["requests"] = [];
const server = http.createServer((request, response) => {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
const rawBody = Buffer.concat(chunks).toString("utf8");
requests.push({
path: request.url ?? "",
body: rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : {},
authorization: request.headers.authorization,
sourceConfig: request.headers["x-dashscope-source-config"] as string | undefined,
openApiSource: request.headers["x-dashscope-openapisource"] as string | undefined,
});
response.writeHead(statusCode, { "Content-Type": "application/json" });
if (statusCode >= 400) {
response.end(JSON.stringify({ code: "InvalidApiKey", message: "invalid key" }));
return;
}
response.end(
JSON.stringify({ choices: [{ message: { role: "assistant", content: "ok" } }] }),
);
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address() as AddressInfo;
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
close: () => new Promise<void>((resolve) => server.close(() => resolve())),
};
}
/** Auth E2E本地参数/持久化契约默认执行;真实鉴权请求按对应 readiness gate 执行。 */
describe("e2e: auth", () => {
test("auth login --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--help"]);
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "login", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/login|api-key/i);
expect(stderr).toMatch(/--console-site/);
@@ -155,13 +109,13 @@ describe("e2e: auth", () => {
});
test("auth logout --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--help"]);
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "logout", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/logout|dry-run|yes/i);
});
test("auth status --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--help"]);
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "status", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/status|output/i);
});
@@ -172,7 +126,7 @@ describe("e2e: auth", () => {
expect(stderr).toMatch(/Choose exactly one login mode/);
});
test("auth login --dry-run --api-key 不发起校验与落盘", async () => {
test("auth login --dry-run --api-key 不发起落盘", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
@@ -181,7 +135,7 @@ describe("e2e: auth", () => {
"sk-e2e-dry-run-placeholder",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would validate and save API key.");
expect(stdout).toContain("Would save API key.");
});
test("auth login --dry-run 仍校验显式 Base URL", async () => {
@@ -198,104 +152,83 @@ describe("e2e: auth", () => {
expect(stderr).toMatch(/Invalid model base URL/);
});
test("auth login --api-key 验证后原子保存凭证和 Base URL", async () => {
const validationServer = await startValidationServer();
test("auth login --api-key 原子保存凭证和 Base URL(无联网探测)", async () => {
const configDir = makeE2eOutputDir("auth-api-key-login");
const sdkBaseUrl = `${validationServer.baseUrl}/compatible-mode/v1/?source=login#fragment`;
try {
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-e2e-placeholder", "--base-url", sdkBaseUrl],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(validationServer.requests).toHaveLength(1);
expect(validationServer.requests[0]).toMatchObject({
path: "/compatible-mode/v1/chat/completions",
authorization: "Bearer sk-e2e-placeholder",
sourceConfig: expect.any(String),
openApiSource: "BailianCLI",
body: {
model: "qwen3.8-max",
stream: false,
},
});
const origin = "https://dashscope.example.test";
const sdkBaseUrl = `${origin}/compatible-mode/v1/?source=login#fragment`;
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-e2e-placeholder", "--base-url", sdkBaseUrl],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(login.stderr).toMatch(/API key saved to/);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBe("sk-e2e-placeholder");
expect(config.base_url).toBe(validationServer.baseUrl);
} finally {
await validationServer.close();
}
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBe("sk-e2e-placeholder");
expect(config.base_url).toBe(origin);
});
test("auth login --config token-plan 接受 Anthropic SDK Base URL", async () => {
const validationServer = await startValidationServer();
const configDir = makeE2eOutputDir("auth-token-plan-anthropic-base-url");
try {
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"token-plan",
"--api-key",
"sk-sp-e2e-placeholder",
"--base-url",
`${validationServer.baseUrl}/apps/anthropic?source=sdk#fragment`,
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(validationServer.requests).toHaveLength(1);
expect(validationServer.requests[0].path).toBe("/compatible-mode/v1/chat/completions");
const origin = "https://token-plan.example.test";
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"token-plan",
"--api-key",
"sk-sp-e2e-placeholder",
"--base-url",
`${origin}/apps/anthropic?source=sdk#fragment`,
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: validationServer.baseUrl,
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
} finally {
await validationServer.close();
}
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: origin,
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
});
test("auth login --config token-plan 追加新版 capability preset 且不删除已有能力", async () => {
const validationServer = await startValidationServer();
const configDir = makeE2eOutputDir("auth-token-plan-preset-login");
writeFileSync(
join(configDir, "config.json"),
@@ -315,72 +248,56 @@ describe("e2e: auth", () => {
) + "\n",
);
try {
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "sk-env-must-not-be-persisted",
DASHSCOPE_BASE_URL: validationServer.baseUrl,
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(validationServer.requests).toHaveLength(1);
expect(validationServer.requests[0]).toMatchObject({
path: "/compatible-mode/v1/chat/completions",
authorization: "Bearer sk-sp-e2e-placeholder",
sourceConfig: expect.any(String),
openApiSource: "BailianCLI",
body: {
model: "qwen3.8-max",
stream: false,
},
});
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "sk-env-must-not-be-persisted",
DASHSCOPE_BASE_URL: "https://env-must-not-override-preset.example.test",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("token-plan");
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"custom.command",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
expect((config["token-plan"] as Record<string, unknown>).base_url).not.toBe(
validationServer.baseUrl,
);
expect((config["token-plan"] as Record<string, unknown>).api_key).not.toBe(
"sk-env-must-not-be-persisted",
);
} finally {
await validationServer.close();
}
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("token-plan");
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"custom.command",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
expect((config["token-plan"] as Record<string, unknown>).base_url).not.toBe(
"https://env-must-not-override-preset.example.test",
);
expect((config["token-plan"] as Record<string, unknown>).api_key).not.toBe(
"sk-env-must-not-be-persisted",
);
});
test("auth login --config token-plan 为显式空白名单追加 capability preset", async () => {
const validationServer = await startValidationServer();
const configDir = makeE2eOutputDir("auth-token-plan-empty-capabilities-login");
writeFileSync(
join(configDir, "config.json"),
@@ -395,51 +312,46 @@ describe("e2e: auth", () => {
) + "\n",
);
try {
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: validationServer.baseUrl,
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(validationServer.requests).toHaveLength(1);
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "https://env-ignored-for-persist.example.test",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
} finally {
await validationServer.close();
}
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
});
test("auth login 未传 --config 时写当前激活 Config", async () => {
const validationServer = await startValidationServer();
const configDir = makeE2eOutputDir("auth-active-profile-login");
const storedBaseUrl = "https://dev-profile.example.test";
writeFileSync(
join(configDir, "config.json"),
JSON.stringify(
{
active_config: "dev",
dev: { base_url: validationServer.baseUrl },
dev: { base_url: storedBaseUrl },
},
null,
2,
@@ -451,60 +363,48 @@ describe("e2e: auth", () => {
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
};
try {
const activeLogin = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-active-placeholder"],
env,
);
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
const activeLogin = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-active-placeholder"],
env,
);
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
expect(validationServer.requests).toHaveLength(1);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("dev");
expect(config.dev).toMatchObject({
api_key: "sk-active-placeholder",
base_url: validationServer.baseUrl,
});
} finally {
await validationServer.close();
}
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("dev");
expect(config.dev).toMatchObject({
api_key: "sk-active-placeholder",
base_url: storedBaseUrl,
});
});
test("auth login --api-key 验证失败不留下半配置", async () => {
const validationServer = await startValidationServer(400);
test("auth login --api-key 非法 Base URL 失败不留下半配置", async () => {
const configDir = makeE2eOutputDir("auth-api-key-login-failure");
try {
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"failed-profile",
"--api-key",
"sk-invalid",
"--base-url",
validationServer.baseUrl,
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode).not.toBe(0);
expect(login.stderr).toMatch(/invalid key/);
expect(login.stderr).not.toMatch(/API key validation failed|Invalid API key/);
expect(existsSync(join(configDir, "config.json"))).toBe(false);
} finally {
await validationServer.close();
}
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"failed-profile",
"--api-key",
"sk-invalid",
"--base-url",
"ftp://example.com/models",
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode).not.toBe(0);
expect(login.stderr).toMatch(/Invalid model base URL/);
expect(existsSync(join(configDir, "config.json"))).toBe(false);
});
test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => {
@@ -520,7 +420,7 @@ describe("e2e: auth", () => {
"120",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would validate and save API key.");
expect(stdout).toContain("Would save API key.");
});
test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => {
@@ -0,0 +1,20 @@
import { expect, test } from "vite-plus/test";
import { captureCommandHelp, runCommandHelp } from "./helpers.ts";
import { QUOTA_ROUTES } from "./topic-routes.ts";
test("captureCommandHelp 用 topic 最小路由渲染真实命令帮助", async () => {
const helpOutput = await captureCommandHelp(QUOTA_ROUTES, ["quota", "list"]);
expect(helpOutput).toContain("Usage: bl quota list");
expect(helpOutput).toContain("--model <model>");
expect(helpOutput).toContain("--name <name>");
expect(helpOutput).toContain("--output <format>");
});
test("runCommandHelp 保持 commands E2E 的 stderr 与 exitCode 断言接口", async () => {
const result = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("Usage: bl quota list");
});
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { CONFIG_ROUTES } from "./topic-routes.ts";
/**
@@ -11,29 +11,29 @@ import { CONFIG_ROUTES } from "./topic-routes.ts";
describe("e2e: config", () => {
test("config show --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "show", "--help"]);
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "show", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/show|config/i);
});
test("config set --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--help"]);
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "set", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/set|--key|--value/i);
});
test("config list/use --help 正常退出", async () => {
const listResult = await runCommandE2e(CONFIG_ROUTES, ["config", "list", "--help"]);
const listResult = await runCommandHelp(CONFIG_ROUTES, ["config", "list", "--help"]);
expect(listResult.exitCode, listResult.stderr).toBe(0);
expect(listResult.stderr).toMatch(/list|active|profile/i);
const useResult = await runCommandE2e(CONFIG_ROUTES, ["config", "use", "--help"]);
const useResult = await runCommandHelp(CONFIG_ROUTES, ["config", "use", "--help"]);
expect(useResult.exitCode, useResult.stderr).toBe(0);
expect(useResult.stderr).toMatch(/use|--name|active/i);
});
test("config ui --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "ui", "--help"]);
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "ui", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/ui|--port|--no-open|web/i);
});
@@ -464,7 +464,7 @@ describe("e2e: config", () => {
});
test("config agent --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "agent", "--help"]);
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "agent", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/agent|--base-url|--model/i);
});
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { CONSOLE_FLAGS_DRY_RUN_ROUTES } from "./topic-routes.ts";
type ConsoleDryRunMeta = {
@@ -10,7 +10,7 @@ type ConsoleDryRunMeta = {
describe("e2e: console global flags (dry-run)", () => {
test("console call --help 不暴露命令级 region/site示例使用 --console-region", async () => {
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
"console",
"call",
"--help",
@@ -24,7 +24,7 @@ describe("e2e: console global flags (dry-run)", () => {
});
test("auth login --help:自有 flag 含 --console-site,不含其余 console 域 flag", async () => {
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
"auth",
"login",
"--help",
@@ -1,6 +1,12 @@
import { describe, expect, test } from "vite-plus/test";
import { join } from "path";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
import {
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
e2eFixturesDir,
} from "./helpers.ts";
import { DATASET_ROUTES } from "./topic-routes.ts";
/**
@@ -18,7 +24,7 @@ import { DATASET_ROUTES } from "./topic-routes.ts";
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
test("dataset upload --help 正常退出并展示 --file", async () => {
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(DATASET_ROUTES, [
"dataset",
"upload",
"--help",
@@ -286,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)", () => {
+23 -5
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { DEPLOY_ROUTES } from "./topic-routes.ts";
/**
@@ -16,14 +16,13 @@ import { DEPLOY_ROUTES } from "./topic-routes.ts";
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
test("deploy 列出子命令", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy"]);
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, ["deploy", "--help"]);
expect(exitCode, stderr).toBe(0);
const out = `${stdout}\n${stderr}`;
expect(out).toMatch(/create|list|get|delete|update|scale|models/);
expect(stderr).toMatch(/create|list|get|delete|update|scale|models/);
});
test("deploy create --help 正常退出并展示必填项", async () => {
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, [
"deploy",
"text",
"create",
@@ -202,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)", () => {
@@ -1,6 +1,12 @@
import { describe, expect, test } from "vite-plus/test";
import { join } from "path";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
import {
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
e2eFixturesDir,
} from "./helpers.ts";
import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
/**
@@ -9,7 +15,7 @@ import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
describe("e2e: file upload", () => {
test("file upload --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(FILE_UPLOAD_ROUTES, [
"file",
"upload",
"--help",
@@ -1,6 +1,12 @@
import { describe, expect, test } from "vite-plus/test";
import { join } from "path";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
import {
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
e2eFixturesDir,
} from "./helpers.ts";
import { FINETUNE_ROUTES } from "./topic-routes.ts";
/**
@@ -17,14 +23,15 @@ import { FINETUNE_ROUTES } from "./topic-routes.ts";
describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
test("finetune 列出子命令", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, ["finetune"]);
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, ["finetune", "--help"]);
expect(exitCode, stderr).toBe(0);
const out = `${stdout}\n${stderr}`;
expect(out).toMatch(/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/);
expect(stderr).toMatch(
/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/,
);
});
test("finetune create --help 正常退出并展示必填项", async () => {
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
"finetune",
"text",
"create",
@@ -287,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",
@@ -336,7 +364,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
test("finetune audio create --help 不暴露文本超参 flag", async () => {
// Audio models don't consume --training-type / --n-epochs / --batch-size /
// --learning-rate / --max-length, so those flags are not offered.
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
"finetune",
"audio",
"create",
@@ -369,7 +397,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
test("finetune video create --help 暴露视频超参且不含文本超参", async () => {
// Video exposes --n-epochs / --batch-size / --learning-rate; the text-only
// --training-type / --max-length surface is not offered.
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
"finetune",
"video",
"create",
+49
View File
@@ -8,7 +8,10 @@ import {
makeE2eOutputDir,
parseStdoutJson,
} from "e2e/output";
import { captureRegistryHelp } from "e2e/registry-smoke";
import { runNodeMain, type RunCliResult } from "e2e/runner";
import type { AnyCommand } from "bailian-cli-core";
import { CommandRegistry, resolve } from "bailian-cli-runtime";
import type { E2eRouteExports } from "./topic-routes.ts";
export {
@@ -42,6 +45,7 @@ export {
const e2eDir = dirname(fileURLToPath(import.meta.url));
const harnessMainTs = join(e2eDir, "harness", "main.ts");
const registryCache = new WeakMap<E2eRouteExports, Promise<CommandRegistry>>();
/** `packages/commands` 根目录 */
export const commandsPackageRoot = join(e2eDir, "..", "..");
@@ -55,6 +59,51 @@ function serializeRoutes(routes: E2eRouteExports): string {
);
}
function getCommandRegistry(routes: E2eRouteExports): Promise<CommandRegistry> {
const cachedRegistry = registryCache.get(routes);
if (cachedRegistry) return cachedRegistry;
const registryPromise = import("bailian-cli-commands").then((commandExports) => {
const commandLibrary = commandExports as Record<string, unknown>;
const commands: Record<string, AnyCommand> = {};
for (const [path, exportName] of Object.entries(routes)) {
const command = commandLibrary[exportName];
if (typeof command !== "object" || command === null || !("run" in command)) {
throw new Error(`Unknown bailian-cli-commands export: ${exportName}`);
}
commands[path] = command as AnyCommand;
}
return new CommandRegistry(commands, "bl");
});
registryCache.set(routes, registryPromise);
return registryPromise;
}
/** 使用 topic 最小路由在当前 Vitest worker 中渲染命令 help不启动 CLI 子进程。 */
export async function captureCommandHelp(
routes: E2eRouteExports,
commandPath: string[],
): Promise<string> {
const registry = await getCommandRegistry(routes);
const resolution = resolve([...commandPath, "--help"], registry);
if (resolution.kind !== "help" || resolution.path.join(" ") !== commandPath.join(" ")) {
throw new Error(`Command help did not resolve: ${commandPath.join(" ")}`);
}
return captureRegistryHelp(registry, commandPath);
}
/** 与 runCommandE2e 的结果结构兼容,但 help 在当前 Vitest worker 中渲染。 */
export async function runCommandHelp(
routes: E2eRouteExports,
args: string[],
): Promise<RunCliResult> {
if (args.at(-1) !== "--help") {
throw new Error("runCommandHelp requires --help as the final argument");
}
const stderr = await captureCommandHelp(routes, args.slice(0, -1));
return { stdout: "", stderr, exitCode: 0 };
}
/**
* harness E2E
* `routes` path export map
@@ -8,6 +8,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { IMAGE_ROUTES } from "./topic-routes.ts";
@@ -18,7 +19,7 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
describe("e2e: image edit", () => {
test("image edit --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "edit", "--help"]);
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, ["image", "edit", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/edit|--image|--prompt|--async|--concurrent/i);
});
@@ -7,6 +7,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { IMAGE_ROUTES } from "./topic-routes.ts";
@@ -19,7 +20,11 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
describe("e2e: image generate", () => {
test("image generate --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "generate", "--help"]);
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, [
"image",
"generate",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/generate|--prompt|--model/i);
});
@@ -3,6 +3,7 @@ import {
isChatE2EReady,
isMultimodalChatE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "../helpers.ts";
import { KNOWLEDGE_CHAT_ROUTES } from "../topic-routes.ts";
@@ -31,7 +32,7 @@ interface DryRunBody {
describe("e2e: knowledge chat", () => {
test("knowledge chat --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHAT_ROUTES, [
"knowledge",
"chat",
"--help",
@@ -11,6 +11,7 @@ import {
isOssImportE2EReady,
isTableKbE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "../helpers.ts";
import {
@@ -47,7 +48,7 @@ const HELP_SMOKE_CASES: Array<[string[], RegExp]> = [
describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
test("12 条命令 help 退出 0 且展示代表性 flagchunk add 单独测)", async () => {
for (const [commandPath, flagPattern] of HELP_SMOKE_CASES) {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
...commandPath,
"--help",
]);
@@ -59,7 +60,7 @@ describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
describe("e2e: knowledge chunk 组 (静态)", () => {
test("chunk add: --help 展示双通道 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"chunk",
"add",
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_DELETE_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge doc delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_DELETE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_DELETE_ROUTES, [
"knowledge",
"doc",
"delete",
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_LIST_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge doc list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_LIST_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_LIST_ROUTES, [
"knowledge",
"doc",
"list",
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
// Live coverage depends on a real job_id produced by doc upload; it is exercised
@@ -10,7 +10,7 @@ import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge doc status", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"doc",
"status",
@@ -2,12 +2,12 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_TAG_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge doc tag", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_TAG_ROUTES, [
"knowledge",
"doc",
"tag",
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_UPLOAD_ROUTES } from "../topic-routes.ts";
const fixtureDir = mkdtempSync(join(tmpdir(), "doc-upload-e2e-"));
@@ -17,7 +17,7 @@ interface DryRunStep {
describe("e2e: knowledge doc upload", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
"knowledge",
"doc",
"upload",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_KB_CREATE_ROUTES } from "../topic-routes.ts";
interface DryRunBody {
@@ -17,7 +17,7 @@ interface DryRunBody {
describe("e2e: knowledge kb create", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_CREATE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_CREATE_ROUTES, [
"knowledge",
"create",
"--help",
@@ -4,13 +4,13 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { deleteKbWithRetry } from "./journeys/journey-helpers.ts";
import { KNOWLEDGE_KB_DELETE_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge kb delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_DELETE_ROUTES, [
"knowledge",
"delete",
"--help",
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_KB_INFO_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge kb info", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_INFO_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_INFO_ROUTES, [
"knowledge",
"info",
"--help",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_KB_LIST_ROUTES } from "../topic-routes.ts";
interface DryRunBody {
@@ -9,7 +9,7 @@ interface DryRunBody {
describe("e2e: knowledge kb list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_LIST_ROUTES, [
"knowledge",
"list",
"--help",
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_KB_UPDATE_ROUTES } from "../topic-routes.ts";
describe("e2e: knowledge kb update", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_UPDATE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_UPDATE_ROUTES, [
"knowledge",
"update",
"--help",
@@ -4,6 +4,7 @@ import {
isSearchE2EReady,
isTableSearchE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "../helpers.ts";
import { KNOWLEDGE_SEARCH_ROUTES } from "../topic-routes.ts";
@@ -20,7 +21,7 @@ interface DryRunBody {
describe("e2e: knowledge search", () => {
test("knowledge search --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SEARCH_ROUTES, [
"knowledge",
"search",
"--help",
@@ -5,7 +5,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_SERVICE_ROUTES } from "../topic-routes.ts";
import { pickDifferentAgentModel } from "./verified-models.ts";
@@ -16,7 +16,7 @@ interface DryRunBody {
describe("e2e: knowledge service list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
@@ -3,6 +3,7 @@ import {
isDashScopeE2EReady,
isKbAdminE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "../helpers.ts";
import { KNOWLEDGE_ROUTES } from "../topic-routes.ts";
@@ -28,7 +29,7 @@ interface DryRunBody {
describe("e2e: knowledge retrieve", () => {
test("knowledge retrieve --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_ROUTES, [
"knowledge",
"retrieve",
"--help",
@@ -1,6 +1,6 @@
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { e2eFixturesDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
const AGENTS_DEPLOYMENT_YAML = join(e2eFixturesDir, "managed-agent", "agents-deployment.yaml");
@@ -123,7 +123,7 @@ describe("e2e: managed-agent", () => {
});
test("managed-agent apply --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
"managed-agent",
"apply",
"--help",
@@ -157,7 +157,7 @@ describe("e2e: managed-agent", () => {
});
test("managed-agent skill-list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill-list",
"--help",
+5 -5
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { MCP_ROUTES } from "./topic-routes.ts";
/**
@@ -18,25 +18,25 @@ import { MCP_ROUTES } from "./topic-routes.ts";
describe("e2e: mcp", () => {
test("mcp list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|--name|--type|--page/i);
});
test("mcp tools --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "tools", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "tools", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/tools|--server|--url/i);
});
test("mcp call --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "call", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "call", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/call|--target|--arg|--json/i);
});
test("mcp list --help 不暴露 --all 入口(市场全量已下线)", async () => {
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).not.toMatch(/--all/);
});
@@ -3,6 +3,7 @@ import {
isBailianE2EEnabled,
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { MEMORY_ROUTES } from "./topic-routes.ts";
@@ -38,25 +39,29 @@ function memoryLibraryCliArgs(): string[] {
describe("e2e: memory", () => {
test("memory add --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "add", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/add|--user-id|--content|messages/i);
});
test("memory list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|--user-id|memory-library/i);
});
test("memory search --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "search", "--help"]);
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"search",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/search|--query|user-id/i);
});
test("memory profile create --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"profile",
"create",
+2 -1
View File
@@ -6,13 +6,14 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { OMNI_ROUTES } from "./topic-routes.ts";
describe("e2e: omni", () => {
test("omni --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, ["omni", "--help"]);
const { stderr, exitCode } = await runCommandHelp(OMNI_ROUTES, ["omni", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/omni|--message|--audio|text-only/i);
});
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { PERMISSION_ROUTES } from "./topic-routes.ts";
describe("e2e: permission", () => {
test("permission list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
"permission",
"list",
"--help",
@@ -17,7 +17,7 @@ describe("e2e: permission", () => {
});
test("permission grant --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
"permission",
"grant",
"--help",
@@ -29,7 +29,7 @@ describe("e2e: permission", () => {
});
test("permission revoke --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
"permission",
"revoke",
"--help",
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { PIPELINE_ROUTES } from "./topic-routes.ts";
describe("e2e: pipeline", () => {
@@ -65,7 +65,7 @@ describe("e2e: pipeline", () => {
});
test("pipeline run --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
"pipeline",
"run",
"--help",
@@ -76,7 +76,7 @@ describe("e2e: pipeline", () => {
});
test("pipeline validate --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
"pipeline",
"validate",
"--help",
+19 -6
View File
@@ -4,20 +4,21 @@ import {
isConsoleAuthFailure,
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { QUOTA_ROUTES } from "./topic-routes.ts";
describe("e2e: quota", () => {
test("quota list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--model");
expect(stderr).toContain("--name");
});
test("quota list --help 包含所有示例", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("bl quota list");
expect(stderr).toContain("bl quota list --model qwen3-max");
@@ -25,30 +26,31 @@ describe("e2e: quota", () => {
});
test("quota update --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "update", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--model");
expect(stderr).toContain("--rpm");
expect(stderr).toContain("--tpm");
expect(stderr).toContain("--delete");
expect(stderr).toContain("--yes");
});
test("quota request 作为 quota update 的兼容别名可用", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "request", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "request", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--rpm");
expect(stderr).toContain("--delete");
});
test("quota history --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "history", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "history", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--page");
expect(stderr).toContain("--model");
});
test("quota check --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "check", "--help"]);
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "check", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--model");
expect(stderr).toContain("--period");
@@ -91,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",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { SEARCH_WEB_ROUTES } from "./topic-routes.ts";
function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> {
@@ -17,7 +17,7 @@ function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?:
describe("e2e: search web", () => {
test("search web --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(SEARCH_WEB_ROUTES, [
"search",
"web",
"--help",
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isBailianE2EEnabled, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isBailianE2EEnabled, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { SKILL_ROUTES } from "./topic-routes.ts";
/** Canonical always-published skill; also the backbone of advisor wiki sync */
@@ -15,33 +15,33 @@ function makeTempConfigDir(): string {
describe("e2e: skill", () => {
test("skill add --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
test("skill update --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
test("skill remove --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "remove", "--help"]);
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "remove", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--name/);
});
test("skill list --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "list", "--help"]);
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|registry/i);
});
test("skill init --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "init", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/bailian/i);
});
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { SPEECH_ROUTES } from "./topic-routes.ts";
/**
@@ -8,7 +8,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
describe("e2e: speech list-voices", () => {
test("speech synthesize --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
"speech",
"synthesize",
"--help",
@@ -18,7 +18,7 @@ describe("e2e: speech list-voices", () => {
});
test("speech recognize --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
"speech",
"recognize",
"--help",
@@ -9,6 +9,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { SPEECH_ROUTES } from "./topic-routes.ts";
@@ -50,7 +51,7 @@ describe("e2e: speech recognize", () => {
}
test("speech recognize --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
"speech",
"recognize",
"--help",
@@ -7,6 +7,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { SPEECH_ROUTES } from "./topic-routes.ts";
@@ -17,7 +18,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
describe("e2e: speech synthesize", () => {
test("speech synthesize --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
"speech",
"synthesize",
"--help",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
/**
@@ -8,7 +8,7 @@ import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
describe("e2e: text chat", () => {
test("text chat --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
const { stderr, exitCode } = await runCommandHelp(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--api\s+<chat\|responses>/i);
expect(stderr).toMatch(/chat|--message|model|stream/i);
@@ -1,10 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { makeE2eOutputDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { makeE2eOutputDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import { TOKEN_PLAN_ROUTES } from "./topic-routes.ts";
describe("e2e: token-plan", () => {
test("token-plan help shows centralized OpenAPI auth flags", async () => {
const { stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(TOKEN_PLAN_ROUTES, [
"token-plan",
"list-seats",
"--help",
@@ -1,17 +1,17 @@
import { describe, expect, test } from "vite-plus/test";
import { runCommandE2e } from "./helpers.ts";
import { runCommandHelp, runCommandE2e } from "./helpers.ts";
import { UPDATE_ROUTES } from "./topic-routes.ts";
describe("e2e: update", () => {
test("update --help 正常退出并展示 --to", async () => {
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--to/);
expect(stderr).toMatch(/<version>/);
});
test("update --help 包含 --to 示例", async () => {
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--to 0.1.14");
});
@@ -3,13 +3,14 @@ import {
isConsoleAuthFailure,
isConsoleE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { USAGE_ROUTES } from "./topic-routes.ts";
describe("e2e: usage coding-plan", () => {
test("usage coding-plan --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
"usage",
"coding-plan",
"--help",
@@ -19,7 +20,7 @@ describe("e2e: usage coding-plan", () => {
});
test("usage coding-plan --help 包含 --output json 示例", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
"usage",
"coding-plan",
"--help",
@@ -3,19 +3,20 @@ import {
isConsoleE2EReady,
isConsoleAuthFailure,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { USAGE_ROUTES } from "./topic-routes.ts";
describe("e2e: usage free", () => {
test("usage free --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--model|quota|free-tier/i);
});
test("usage free --help 包含所有示例", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("bl usage free");
expect(stderr).toContain("bl usage free --model qwen3-max");
@@ -3,6 +3,7 @@ import {
isConsoleE2EReady,
isConsoleAuthFailure,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { USAGE_ROUTES } from "./topic-routes.ts";
@@ -42,13 +43,13 @@ async function fetchDefaultWorkspaceId(): Promise<string> {
describe("e2e: usage stats", () => {
test("usage stats --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--model|--days|stats/i);
});
test("usage stats --help 包含所有示例", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("bl usage stats");
expect(stderr).toContain("bl usage stats --model qwen-turbo");
@@ -56,7 +57,7 @@ describe("e2e: usage stats", () => {
});
test("usage stats --help 包含 --workspace-id 选项", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--workspace-id");
});
@@ -3,13 +3,14 @@ import {
isConsoleAuthFailure,
isConsoleE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { USAGE_ROUTES } from "./topic-routes.ts";
describe("e2e: usage token-plan", () => {
test("usage token-plan --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
"usage",
"token-plan",
"--help",
@@ -19,7 +20,7 @@ describe("e2e: usage token-plan", () => {
});
test("usage token-plan --help 包含 --output json 示例", async () => {
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
"usage",
"token-plan",
"--help",
@@ -8,6 +8,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -22,7 +23,11 @@ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001";
describe("e2e: video download", () => {
test("video download --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "download", "--help"]);
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
"video",
"download",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/download|--task-id|--out/i);
});
@@ -7,6 +7,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -17,7 +18,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
describe("e2e: video edit", () => {
test("video edit --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "edit", "--help"]);
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "edit", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/edit|--video|--prompt|model|--async|--concurrent/i);
});
@@ -8,6 +8,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -18,7 +19,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
describe("e2e: video generate (i2v)", () => {
test("video generate --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
"video",
"generate",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/generate|--prompt|--image|model/i);
});
@@ -7,6 +7,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -17,7 +18,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
describe("e2e: video generate (t2v)", () => {
test("video generate --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
"video",
"generate",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/generate|--prompt|--model|download|image/i);
});
@@ -8,6 +8,7 @@ import {
isDashScopeE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -18,7 +19,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
describe("e2e: video ref (r2v)", () => {
test("video ref --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "ref", "--help"]);
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "ref", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/ref|--prompt|--image|model|--async|--concurrent/i);
});
@@ -3,6 +3,7 @@ import {
isBailianE2EEnabled,
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { VIDEO_ROUTES } from "./topic-routes.ts";
@@ -15,7 +16,7 @@ const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim();
describe("e2e: video task get", () => {
test("video task get --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
"video",
"task",
"get",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.18.0",
"version": "1.18.1",
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+20 -2
View File
@@ -6,8 +6,8 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
const groups = new Set<string>();
for (const path of commandPaths) {
const parts = path.split(" ");
for (let i = 1; i < parts.length; i++) {
const prefix = parts.slice(0, i).join(" ");
for (let partIndex = 1; partIndex < parts.length; partIndex++) {
const prefix = parts.slice(0, partIndex).join(" ");
const hasChildren = commandPaths.some(
(candidate) => candidate.startsWith(`${prefix} `) && candidate !== prefix,
);
@@ -16,3 +16,21 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
}
return [...groups].sort();
}
interface RegistryHelpPrinter {
printHelp(commandPath: string[], output: NodeJS.WriteStream): void;
}
export function captureRegistryHelp(printer: RegistryHelpPrinter, commandPath: string[]): string {
let helpOutput = "";
const output = {
isTTY: false,
write(chunk: string): boolean {
helpOutput += chunk;
return true;
},
} as NodeJS.WriteStream;
printer.printHelp(commandPath, output);
return helpOutput;
}
+16 -11
View File
@@ -4,6 +4,7 @@ import { promisify } from "util";
import { monorepoRoot } from "./monorepo-root.ts";
const execFileAsync = promisify(execFile);
const tsxLoader = import.meta.resolve("tsx");
export interface RunCliResult {
stdout: string;
@@ -18,24 +19,28 @@ export function resolveTsxBin(): string {
return join(root, "node_modules", ".bin", name);
}
/** 子进程通过 tsx 执行指定 main.ts 入口 */
/** 子进程通过 Node 的 tsx loader 执行指定 main.ts 入口,不启动 tsx CLI 的 IPC server。 */
export async function runNodeMain(
mainTs: string,
args: string[],
options: { cwd: string; env?: NodeJS.ProcessEnv } = { cwd: process.cwd() },
): Promise<RunCliResult> {
try {
const { stdout, stderr } = await execFileAsync(resolveTsxBin(), [mainTs, ...args], {
cwd: options.cwd,
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
NODE_NO_WARNINGS: "1",
DO_NOT_TRACK: "1",
...options.env,
const { stdout, stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, mainTs, ...args],
{
cwd: options.cwd,
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
NODE_NO_WARNINGS: "1",
DO_NOT_TRACK: "1",
...options.env,
},
},
});
);
return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
} catch (err: unknown) {
const e = err as {
+17
View File
@@ -0,0 +1,17 @@
interface FixtureOutput {
args: string[];
execArgv: string[];
marker: string | null;
}
if (process.argv[2] === "--fail") {
process.stderr.write("fixture failure\n");
process.exitCode = 7;
} else {
const output: FixtureOutput = {
args: process.argv.slice(2),
execArgv: process.execArgv,
marker: process.env.RUNNER_FIXTURE_MARKER ?? null,
};
process.stdout.write(`${JSON.stringify(output)}\n`);
}
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "vite-plus/test";
import { captureRegistryHelp } from "../src/registry-smoke.ts";
test("captureRegistryHelp 捕获指定命令路径的非 TTY 输出", () => {
const requestedPaths: string[][] = [];
const renderer = {
printHelp(commandPath: string[], output: NodeJS.WriteStream): void {
requestedPaths.push(commandPath);
output.write(`Usage: bl ${commandPath.join(" ")}`);
},
};
const output = captureRegistryHelp(renderer, ["text", "chat"]);
expect(output).toBe("Usage: bl text chat");
expect(requestedPaths).toEqual([["text", "chat"]]);
});
+37
View File
@@ -0,0 +1,37 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test } from "vite-plus/test";
import { runNodeMain } from "../src/run-subprocess.ts";
const testsDir = dirname(fileURLToPath(import.meta.url));
const fixtureMainTs = join(testsDir, "fixtures", "echo-argv.ts");
test("runNodeMain 通过 Node 的 tsx loader 执行 TypeScript 入口", async () => {
const result = await runNodeMain(fixtureMainTs, ["alpha", "beta"], {
cwd: testsDir,
env: { RUNNER_FIXTURE_MARKER: "forwarded" },
});
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stderr).toBe("");
const output = JSON.parse(result.stdout) as {
args: string[];
execArgv: string[];
marker: string | null;
};
expect(output.args).toEqual(["alpha", "beta"]);
expect(output.marker).toBe("forwarded");
const importFlagIndex = output.execArgv.indexOf("--import");
expect(importFlagIndex).toBeGreaterThanOrEqual(0);
expect(output.execArgv[importFlagIndex + 1]).toMatch(/tsx/);
});
test("runNodeMain 返回 TypeScript 入口的 stderr 与非零退出码", async () => {
const result = await runNodeMain(fixtureMainTs, ["--fail"], { cwd: testsDir });
expect(result).toEqual({
stdout: "",
stderr: "fixture failure\n",
exitCode: 7,
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.18.0",
"version": "1.18.1",
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
"keywords": [
"alibaba-cloud",
@@ -1,11 +1,13 @@
import { describe, expect, test } from "vite-plus/test";
import { deriveGroupPaths } from "e2e/registry-smoke";
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
import { CommandRegistry, resolve } from "bailian-cli-runtime";
import pkg from "../../package.json" with { type: "json" };
import { commands } from "../../src/commands.ts";
import { runKscli } from "./helpers.ts";
const commandPaths = Object.keys(commands).sort();
const groupPaths = deriveGroupPaths(commandPaths);
const registry = new CommandRegistry(commands, "kscli");
describe("e2e: kscli registry smoke", () => {
test("根帮助展示 kscli 与全局 flag", async () => {
@@ -38,13 +40,25 @@ describe("e2e: kscli registry smoke", () => {
expect(stderr).toMatch(/--query|Missing required/i);
});
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
expect(exitCode, stderr).toBe(0);
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
const commandPath = path.split(" ");
expect(resolve([...commandPath, "--help"], registry)).toEqual({
kind: "help",
path: commandPath,
});
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: kscli ${path}`);
});
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
expect(exitCode, stderr).toBe(0);
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
const commandPath = path.split(" ");
expect(resolve([...commandPath, "--help"], registry)).toEqual({
kind: "help",
path: commandPath,
});
expect(captureRegistryHelp(registry, commandPath)).toContain(
`Usage: kscli ${path} <command> [flags]`,
);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-runtime",
"version": "1.18.0",
"version": "1.18.1",
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -53,7 +53,7 @@ bl auth generate-access-token --access-key-id LTAIxxxxx --access-key-secret xxxx
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------------------------- |
| `--api-key <key>` | string | no | Model API key to store |
| `--base-url <url>` | string | no | Model API base URL (used with --api-key for validation) |
| `--base-url <url>` | string | no | Model API base URL to store with --api-key |
| `--console` | switch | no | Sign in via browser; use --console-site to choose domestic (default) or international |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--open-api` | switch | no | Store Alibaba Cloud OpenAPI AK/SK credentials |
+12 -6
View File
@@ -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 <model> [--rpm <n>] [--tpm <n>] [--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 <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
```
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-
+20 -11
View File
@@ -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 <id>` |
| Field | Value |
| ------------------ | ------------------------------------------ |
| **Name** | `dataset delete` |
| **Description** | Delete a dataset file by ID |
| **Authentication** | API Key |
| **Usage** | `bl dataset delete --file-id <id> [--yes]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------- |
| `--file-id <id>` | string | yes | Dataset file ID (required) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| 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
@@ -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 |
+15 -6
View File
@@ -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 <id> [--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 <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 |
+17 -11
View File
@@ -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 <id>` |
| Field | Value |
| ------------------ | ------------------------------------------ |
| **Name** | `finetune delete` |
| **Description** | Delete a fine-tune job record |
| **Authentication** | API Key |
| **Usage** | `bl finetune delete --job-id <id> [--yes]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | --------------------------- |
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| 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 |
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-managed-agent
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-
+2 -2
View File
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-
@@ -68,7 +68,7 @@ After approval, treat Bailian as selected for the current task. Do not ask again
Install, API key / console login, endpoint override, and config keys:
[`assets/setup.md`](assets/setup.md).
**Token Plan:** Get the API key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview), then run `bl auth login --config token-plan --api-key <key>`. The built-in Profile supplies the Base URL and its supported leaf API Key capabilities, and login validates the key before saving it. While that Profile is active, unsupported API Key commands automatically use `default` credentials. A per-command `--api-key` / `--base-url` or `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` bypasses this fallback entirely.
**Token Plan:** Get the API key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview), then run `bl auth login --config token-plan --api-key <key>`. The built-in Profile supplies the Base URL and its supported leaf API Key capabilities, and login saves the key without a live model probe. While that Profile is active, unsupported API Key commands automatically use `default` credentials. A per-command `--api-key` / `--base-url` or `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` bypasses this fallback entirely.
**Console login:** never run bare `bl auth login --console` — always pass `--console-site domestic` or `--console-site international`. Before login, run `bl config show --output json` and follow the site-selection rules in [`assets/setup.md` → Console site selection](assets/setup.md#console-site-selection).
+4 -4
View File
@@ -82,9 +82,9 @@ bl image generate --prompt "A cat"
bl video generate --prompt "A horse running through a field"
```
The built-in Profile supplies the Token Plan Base URL. `auth login` tests the key first, then saves
and activates the Profile only when validation succeeds; do not ask the user to configure the Base
URL or run a duplicate smoke test.
The built-in Profile supplies the Token Plan Base URL. `auth login` saves the key and activates
the Profile without a live model probe; do not ask the user to configure the Base URL or run a
duplicate smoke test.
### API Key capability fallback
@@ -120,7 +120,7 @@ bl config use --name default
```
`auth login --config token-plan` creates or updates that Profile and activates it only after the
credential is validated and saved. Failed login and `--dry-run` do not switch Profiles. Use
credential is saved. Failed login and `--dry-run` do not switch Profiles. Use
`--config default` for a one-command override. Config selection follows explicit `--config` >
persisted `active_config` > `default`; credential and endpoint fields inside the selected Profile
still follow flag > environment > config.
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-web-search
metadata:
version: "1.18.0"
version: "1.18.1"
requires:
bins: ["bl"]
description: >-