mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d33cb71cc7 | |||
| 737f8db359 | |||
| a402026cce | |||
| da6e1313e5 | |||
| 2d2ec1c628 | |||
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| fd96fd664c | |||
| 719c0b68bb | |||
| 9e6f5ca9cb | |||
| 3f78de2dc1 | |||
| 67ae182ec3 | |||
| 676b6c2ece | |||
| 4f59ca5118 | |||
| 0872ff6a20 | |||
| afb547e0c8 | |||
| 8906108744 | |||
| 00bcee36a6 |
@@ -6,6 +6,26 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl quota delete`** — clears all custom QPM/TPM rate limits for a model.
|
||||
|
||||
### Changed
|
||||
|
||||
- **High-risk operation confirmation** — high-risk commands show risk details in `--help` and Skill command references. Without `--yes`, the high-risk operation is not executed; JSON output returns exit code `7` with `error.type: "requires_confirmation"`. After confirmation, re-run with `--yes`; `--dry-run` does not require confirmation.
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl quota delete`** — 清除指定模型的全部自定义 QPM/TPM 限流配置。
|
||||
|
||||
### 变更
|
||||
|
||||
- **高风险操作确认** — 高风险命令会在 `--help` 和 Skill 命令参考中展示风险说明。未传入 `--yes` 时,高风险操作不会执行;JSON 输出会返回退出码 `7` 和 `error.type: "requires_confirmation"`。确认后可添加 `--yes` 重新执行;`--dry-run` 无需确认。
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### 变更
|
||||
|
||||
- **删除与清除操作增加确认** —— `bl finetune delete`、`bl deploy delete`、`bl dataset delete` 和 `bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
@@ -85,6 +85,13 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
|
||||
高风险命令额外要求:
|
||||
|
||||
- `--help` 展示 runtime 注入的 `--yes`
|
||||
- 无 `--yes` 返回 exit code 7 和 JSON `type: "requires_confirmation"`
|
||||
- `--dry-run` 无需 `--yes`,且必须证明在任何远端请求或本地写入之前返回
|
||||
- runtime 的离线 high-risk fixture 必须覆盖带 `--yes` 确实进入 `run()`,并断言 `yes` 不进入 command 自有 flags
|
||||
|
||||
## Journey 层(用户旅程全链路)
|
||||
|
||||
- **定位**:命令 E2E 验单命令契约;journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
|
||||
|
||||
@@ -71,7 +71,9 @@ packages/commands/src/index.ts
|
||||
- `usageArgs`(不含 bin/path 前缀)
|
||||
- `exampleArgs`(不含 bin/path 前缀)
|
||||
- `validate`(跨 flag 校验)
|
||||
- 高风险命令必须声明 `risk: { level: "high", message: <双语文案> }`;`--yes` 由 runtime 注入,命令不得自行声明
|
||||
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
|
||||
- 声明 `risk` 的 `run(ctx)` 必须在任何远端请求或本地写入之前处理 `ctx.settings.dryRun` 并返回预览;runtime 只负责确认闸门,不替命令实现 dry-run
|
||||
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
|
||||
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
|
||||
- [ ] 用户可见 Help 文案在命令文件中就近提供 `en-US` / `zh-CN`:命令 `description`、flag `description`、`notes` 和包含自然语言的 `exampleArgs`;纯命令语法示例可保留为字符串,服务端错误不翻译
|
||||
|
||||
@@ -40,6 +40,8 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
|
||||
- [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
|
||||
- [ ] **高风险确认**:统一由 `bailian-protocol` 定义;reference / leaf help 以 `risk: high` 明示风险,业务 skill 不得引导 Agent 自动补 `--yes`。遇到 exit code 7 / `requires_confirmation` 时停止执行并请求确认;目标或范围变化后重新确认
|
||||
- [ ] **正常控制流**:`requires_confirmation` 不是 CLI bug,`assets/issue-reporting.md` 必须将 exit code 7 保持在 EXCLUDE 范围
|
||||
- [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read,未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提
|
||||
- [ ] **Hub vs 领域**:`bailian-cli` 的「When to use which command」只列 hub 拥有的意图;媒体 / 精调 / managed-agent 各留 hand-off 行,**不抄**领域默认模型与子命令明细
|
||||
- [ ] **渐进披露**:SKILL 写意图路由与领域硬规则;flags / usage / examples 以 `reference/` 或 `bl <command> --help` 为准,表后保留「勿猜 flag」指向句
|
||||
@@ -55,6 +57,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
|
||||
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
|
||||
- [ ] 跑 `pnpm run sync:skill-assets`(或 commit 走 pre-commit),提交生成的 `reference/` 与 version 同步结果
|
||||
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
|
||||
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
│ ├─ ~/.bailian/telemetry.jsonl
|
||||
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
|
||||
│
|
||||
└─ authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
└─ confirmationStage
|
||||
└─ versionCheckStage → authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
```
|
||||
|
||||
### 1. 三套鉴权与埋点标识
|
||||
@@ -77,7 +78,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
### 3. 全命令 AEM 客户端埋点
|
||||
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹 `authStage` 与命令执行,因此成功、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹确认闸门、`authStage` 与命令执行,因此成功、确认未通过、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。确认闸门仍位于版本检查、鉴权和业务执行之前,不会因为埋点而放行高风险操作。
|
||||
|
||||
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`:
|
||||
|
||||
@@ -92,7 +93,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
- `command`、`timestamp`、`durationMs`、`success`
|
||||
- `cliVersion`、`nodeVersion`、`os`
|
||||
- `authMethod`
|
||||
- 失败时的 `errorMessage`、`httpStatus`、`requestId`
|
||||
- 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId`
|
||||
- 安全 allowlist 过滤后的 `params`
|
||||
|
||||
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
|
||||
@@ -108,16 +109,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
AEM 映射:
|
||||
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段 |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ------------------------------------------------------------------ |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段,包含失败时的 `exitCode` |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
|
||||
远端发送是 best-effort,不得阻塞命令或改变退出码。正常退出最多等待 1 秒,SIGINT 最多等待 500 ms。
|
||||
|
||||
@@ -144,6 +145,7 @@ AEM 映射:
|
||||
- [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射
|
||||
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
|
||||
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
|
||||
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5`
|
||||
- [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口
|
||||
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ bl knowledge category delete --category-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------ |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -190,11 +190,11 @@ bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
|
||||
# 删除单个文档
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 批量删除,跳过确认
|
||||
# 用户明确确认后批量删除
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -120,10 +120,10 @@ bl knowledge file delete --file-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -254,10 +254,10 @@ bl knowledge delete --index-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ bl knowledge service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ bl knowledge service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ kscli category delete --category-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------ |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+6
-6
@@ -190,11 +190,11 @@ kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
|
||||
# 删除单个文档
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 批量删除,跳过确认
|
||||
# 用户明确确认后批量删除
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
|
||||
+4
-4
@@ -120,10 +120,10 @@ kscli file delete --file-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+5
-5
@@ -254,10 +254,10 @@ kscli kb delete --index-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ kscli --help
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ kscli service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ kscli service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
|
||||
**Flags**:`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`。
|
||||
|
||||
**实现方案**:`doc-delete.ts`;确认摘要含 index_id + doc_id 列表(≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
**实现方案**:`doc-delete.ts`;命令在 `risk` 对象中同时声明 `level: "high"` 和双语 `message`,由 runtime 在 `run()` 前统一确认;输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY 无 `--yes` exitCode 2 / live 配合 upload 清理链。
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 无 `--yes` 返回 exitCode 7 + `requires_confirmation` / live 配合 upload 清理链。
|
||||
|
||||
## doc tag — 批量更新文档标签
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
|
||||
// succeeded, so a backup deletion failure must not fail the pre-download
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed swap as failed */
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -214,7 +220,11 @@ async function main() {
|
||||
}
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* cleanup must not mask the original error */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaUpdate,
|
||||
quotaDelete,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
@@ -246,6 +247,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"workspace list": workspaceList,
|
||||
"quota list": quotaList,
|
||||
"quota update": quotaUpdate,
|
||||
"quota delete": quotaDelete,
|
||||
"quota history": quotaHistory,
|
||||
"quota check": quotaCheck,
|
||||
"permission list": permissionList,
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("e2e: Command Pack", () => {
|
||||
expect(linkedJson.linked.commands).toEqual([
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
@@ -170,6 +171,34 @@ describe("e2e: Command Pack", () => {
|
||||
expect(failed.stderr).toContain("Use agent fail only in tests.");
|
||||
});
|
||||
|
||||
test("high-risk 命令由 runtime 统一确认并支持安全 dry-run", async () => {
|
||||
const dangerousHelp = await runCli(["agent", "dangerous", "--help"], env());
|
||||
expect(dangerousHelp.exitCode, dangerousHelp.stderr).toBe(0);
|
||||
expect(dangerousHelp.stderr).toContain("--yes");
|
||||
|
||||
const unconfirmed = await runCli(["agent", "dangerous", "--output", "json"], env());
|
||||
expect(unconfirmed.exitCode).toBe(7);
|
||||
expect(JSON.parse(unconfirmed.stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
|
||||
const confirmed = await runCli(["agent", "dangerous", "--yes", "--output", "json"], env());
|
||||
expect(confirmed.exitCode, confirmed.stderr).toBe(0);
|
||||
expect(parseStdoutJson(confirmed.stdout)).toEqual({
|
||||
executed: true,
|
||||
dry_run: false,
|
||||
command_flags: [],
|
||||
});
|
||||
|
||||
const preview = await runCli(["agent", "dangerous", "--dry-run", "--output", "json"], env());
|
||||
expect(preview.exitCode, preview.stderr).toBe(0);
|
||||
expect(parseStdoutJson(preview.stdout)).toEqual({
|
||||
executed: false,
|
||||
dry_run: true,
|
||||
command_flags: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("plugin list 输出加载状态", async () => {
|
||||
const result = await runCli(["plugin", "list", "--output", "json"], env());
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
@@ -183,6 +212,7 @@ describe("e2e: Command Pack", () => {
|
||||
commands: [
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
|
||||
@@ -19,6 +19,26 @@ const ping = {
|
||||
},
|
||||
};
|
||||
|
||||
const dangerous = {
|
||||
description: "Exercise runtime confirmation for a high-risk Command Pack command",
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This fixture represents a high-risk operation.",
|
||||
"zh-CN": "该测试命令代表高风险操作。",
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
const dryRun = ctx.settings.dryRun;
|
||||
ctx.output.result({
|
||||
executed: !dryRun,
|
||||
dry_run: dryRun,
|
||||
command_flags: Object.keys(ctx.flags),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const credential = {
|
||||
description: "Read an API key through the Command Pack host adapter",
|
||||
auth: "apiKey",
|
||||
@@ -57,6 +77,7 @@ const fail = {
|
||||
export default {
|
||||
"agent credential": credential,
|
||||
"agent credential-denied": credentialDenied,
|
||||
"agent dangerous": dangerous,
|
||||
"agent fail": fail,
|
||||
"agent output": output,
|
||||
"agent ping": ping,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const skillsRoot = join(repositoryRoot, "skills");
|
||||
|
||||
test("every generated high-risk command reference requires user confirmation before --yes", () => {
|
||||
let highRiskCommandCount = 0;
|
||||
|
||||
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!skillDirectory.isDirectory()) continue;
|
||||
const referenceDirectory = join(skillsRoot, skillDirectory.name, "reference");
|
||||
|
||||
let referenceFiles: string[];
|
||||
try {
|
||||
referenceFiles = readdirSync(referenceDirectory).filter(
|
||||
(fileName) => fileName.endsWith(".md") && fileName !== "index.md",
|
||||
);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const referenceFile of referenceFiles) {
|
||||
const markdown = readFileSync(join(referenceDirectory, referenceFile), "utf8");
|
||||
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
|
||||
|
||||
for (const commandSection of commandSections) {
|
||||
if (!commandSection.includes("`--yes`")) continue;
|
||||
highRiskCommandCount += 1;
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/);
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
|
||||
expect(commandSection).toMatch(/type=.*requires_confirmation/);
|
||||
const agentSafetyLine = commandSection
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("> **Agent safety:**"));
|
||||
expect(agentSafetyLine).toBeDefined();
|
||||
expect(agentSafetyLine).toMatch(/never add `--yes` automatically/i);
|
||||
expect(agentSafetyLine).toMatch(/explicit user confirmation/i);
|
||||
expect(agentSafetyLine).not.toContain("`--dry-run`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(highRiskCommandCount).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"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,5 +1,5 @@
|
||||
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
fileId: {
|
||||
@@ -8,28 +8,25 @@ 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> [--yes]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified dataset file and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的数据集文件,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--file-id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
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;
|
||||
@@ -39,11 +36,6 @@ 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, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -25,10 +25,6 @@ const DELETE_FLAGS = {
|
||||
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
@@ -44,14 +40,15 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the deployment is permanently destroyed.",
|
||||
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified model deployment and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的模型部署,且无法撤销。",
|
||||
},
|
||||
],
|
||||
},
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
@@ -87,11 +84,6 @@ 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, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
jobId: {
|
||||
@@ -8,23 +8,22 @@ 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> [--yes]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified fine-tune job record and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的微调任务记录,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--job-id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
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.",
|
||||
@@ -40,11 +39,6 @@ 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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagConnectorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CATEGORY_DELETE_FLAGS = {
|
||||
@@ -16,16 +16,19 @@ const CATEGORY_DELETE_FLAGS = {
|
||||
description: { "en-US": "Category ID to delete", "zh-CN": "要删除的类目 ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a data-center category", "zh-CN": "删除数据中心类目" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This deletes the selected data-center category and cannot be undone.",
|
||||
"zh-CN": "该操作会删除所选数据中心类目,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--category-id <id> [flags]",
|
||||
flags: CATEGORY_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -49,11 +52,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete category ${flags.categoryId}\nThis cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await ctx.client.requestJson<
|
||||
RagConnectorResponse<Record<string, unknown> | undefined>
|
||||
>({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CHUNK_DELETE_FLAGS = {
|
||||
@@ -26,10 +26,6 @@ const CHUNK_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -48,6 +44,13 @@ export default defineCommand({
|
||||
"zh-CN": "从知识库中删除 Chunk(不可撤销)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the selected chunks and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除所选 Chunk,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
|
||||
flags: CHUNK_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -81,11 +84,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete ${flags.chunkId.length} chunk(s) from knowledge base ${flags.indexId} in ${batches.length} batch(es).\nChunks are permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
// Sequential batches; any batch failure aborts, listing already-deleted batches in the error
|
||||
let deletedCount = 0;
|
||||
for (const batchIds of batches) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagDeleteFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_DELETE_FLAGS = {
|
||||
@@ -25,28 +25,22 @@ const DOC_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary: list all doc_ids up to 5, otherwise show the first 5 + total count */
|
||||
function buildDeleteSummary(indexId: string, docIds: string[]): string {
|
||||
const listed =
|
||||
docIds.length <= 5
|
||||
? docIds.join("\n ")
|
||||
: `${docIds.slice(0, 5).join("\n ")}\n ... (${docIds.length} documents total)`;
|
||||
return `Delete ${docIds.length} document(s) from knowledge base ${indexId}:\n ${listed}\nDocuments and all their chunks are permanently removed from the index. This cannot be undone.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete documents and their chunks from a knowledge base",
|
||||
"zh-CN": "从知识库中删除文档及其 Chunk",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the selected documents and all of their chunks.",
|
||||
"zh-CN": "该操作会永久删除所选文档及其全部 Chunk,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> --doc-id <id> [flags]",
|
||||
flags: DOC_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -72,7 +66,7 @@ export default defineCommand({
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx --dry-run",
|
||||
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
@@ -89,11 +83,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
buildDeleteSummary(flags.indexId, flags.docId),
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await ctx.client.requestJson<RagDeleteFileResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagConnectorResponse,
|
||||
type RagDescribeFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const FILE_DELETE_FLAGS = {
|
||||
@@ -21,39 +19,23 @@ const FILE_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (file name/size); failure degrades to id-only */
|
||||
async function buildDeleteSummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
fileId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagDescribeFileResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.describeFile),
|
||||
method: "POST",
|
||||
body: { fileId },
|
||||
});
|
||||
if (detail.data?.fileName) infoPart = ` name: ${detail.data.fileName}`;
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Delete data-center file ${fileId}${infoPart}\nPERMANENT: if the file is referenced by knowledge bases, their document indexes break too. This differs from removing a document from one knowledge base.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Permanently delete a file from the data center",
|
||||
"zh-CN": "从数据中心永久删除文件",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This permanently deletes the data-center file. Knowledge-base document indexes that reference it may become invalid.",
|
||||
"zh-CN": "该操作会永久删除数据中心文件;引用该文件的知识库文档索引可能失效。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--file-id <id> [flags]",
|
||||
flags: FILE_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -82,11 +64,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeleteSummary(ctx.client, workspaceId, flags.fileId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<
|
||||
RagConnectorResponse<Record<string, unknown> | undefined>
|
||||
>({
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagIndexFilesResponse,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
import { fetchIndexDetail } from "./kb-info.ts";
|
||||
|
||||
const KB_DELETE_FLAGS = {
|
||||
indexId: {
|
||||
@@ -18,50 +16,23 @@ const KB_DELETE_FLAGS = {
|
||||
description: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup: name + document count; any lookup failure degrades to id-only (never blocks deletion) */
|
||||
async function buildDeleteSummary(
|
||||
ctx: { client: Parameters<typeof fetchIndexDetail>[0] },
|
||||
workspaceId: string,
|
||||
indexId: string,
|
||||
): Promise<string> {
|
||||
let namePart = "";
|
||||
let docCountPart = "";
|
||||
try {
|
||||
const detail = await fetchIndexDetail(ctx.client, workspaceId, indexId);
|
||||
namePart = ` name: ${detail.name}`;
|
||||
} catch {
|
||||
// Degrade gracefully: a missing name does not block confirmation
|
||||
}
|
||||
try {
|
||||
const filesUrl = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
|
||||
filesUrl.searchParams.set("index_id", indexId);
|
||||
filesUrl.searchParams.set("page_num", "1");
|
||||
filesUrl.searchParams.set("page_size", "1");
|
||||
const files = await ctx.client.requestJson<RagIndexFilesResponse>({
|
||||
path: filesUrl.toString(),
|
||||
method: "GET",
|
||||
});
|
||||
const totalCount = files.data?.total_count;
|
||||
if (typeof totalCount === "number") docCountPart = ` documents: ${totalCount}`;
|
||||
} catch {
|
||||
// Same graceful degradation as above
|
||||
}
|
||||
return `Delete knowledge base ${indexId}${namePart}${docCountPart}\nThis permanently removes the knowledge base with all documents and chunks. It cannot be undone.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete a knowledge base with all its documents and chunks",
|
||||
"zh-CN": "删除知识库及其所有文档和 Chunk",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This permanently deletes the knowledge base and all of its documents and chunks. Data-center files are not deleted.",
|
||||
"zh-CN": "该操作会永久删除知识库及其全部文档和 Chunk,但不会删除数据中心文件。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -90,11 +61,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? "" // --yes bypasses the prompt, so skip the summary lookups
|
||||
: await buildDeleteSummary(ctx, workspaceId, flags.indexId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DELETE_FLAGS = {
|
||||
@@ -18,48 +16,23 @@ const SERVICE_DELETE_FLAGS = {
|
||||
description: { "en-US": "Service (agent) ID", "zh-CN": "服务(Agent)ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); failure degrades to id-only */
|
||||
async function buildDeleteSummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let liveWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "deployed" || status === "edited") {
|
||||
liveWarning = "\nWARNING: this service is LIVE — deleting it breaks existing callers.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Delete service ${agentId}${infoPart}${liveWarning}\nDeletion cannot be undone; the agent_id can no longer be used for search or chat calls.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete a retrieval / Q&A service (soft delete, idempotent)",
|
||||
"zh-CN": "删除检索/问答服务(软删除,幂等)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This deletes the service and makes its agent ID unavailable for search and chat calls. The operation cannot be undone.",
|
||||
"zh-CN": "该操作会删除服务,使其 Agent ID 无法再用于搜索和对话调用,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -91,11 +64,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeleteSummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DEPLOY_FLAGS = {
|
||||
@@ -26,49 +24,23 @@ const SERVICE_DEPLOY_FLAGS = {
|
||||
"zh-CN": "新发布版本的描述",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); warns that deploying an edited draft overwrites live behavior; failure degrades to id-only */
|
||||
async function buildDeploySummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let editedWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "edited") {
|
||||
editedWarning =
|
||||
"\nWARNING: a published version is live — deploying replaces its behavior with the current draft.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Deploy service ${agentId}${infoPart}${editedWarning}\nPublishing changes what live callers get from this service.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Publish the beta draft of a service as a new version",
|
||||
"zh-CN": "将服务的 beta 草稿发布为新版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This publishes the current draft as a new version and changes the behavior seen by live callers.",
|
||||
"zh-CN": "该操作会将当前草稿发布为新版本,并改变线上调用方使用的服务行为。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DEPLOY_FLAGS,
|
||||
notes: [
|
||||
@@ -109,11 +81,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeploySummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -34,13 +34,6 @@ const APPLY_FLAGS = {
|
||||
"zh-CN": "目标 Provider(默认:全部已配置项)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm and apply without an interactive prompt (required to mutate)",
|
||||
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
|
||||
},
|
||||
},
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -64,7 +57,15 @@ export default defineCommand({
|
||||
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This applies the current plan and may create, update, or delete remote managed Agent resources.",
|
||||
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--concurrency <n>]",
|
||||
flags: APPLY_FLAGS,
|
||||
exampleArgs: ["--yes", "--provider bailian --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -124,23 +125,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const creates = actionable.filter((action) => action.action === "create").length;
|
||||
const updates = actionable.filter((action) => action.action === "update").length;
|
||||
const deletes = planned.destructiveActions;
|
||||
|
||||
for (const action of actionable) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
|
||||
if (!flags.yes) {
|
||||
throw new BailianError(
|
||||
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
|
||||
ExitCode.USAGE,
|
||||
"Review with `bl managed-agent plan`, then re-run with --yes to apply.",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executePlannedProject(planned, {
|
||||
|
||||
@@ -21,13 +21,6 @@ const DESTROY_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm and destroy without an interactive prompt (required)",
|
||||
"zh-CN": "无需交互提示直接确认并销毁(必填)",
|
||||
},
|
||||
},
|
||||
cascade: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -43,7 +36,15 @@ export default defineCommand({
|
||||
"zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--yes] [--cascade]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This deletes every managed Agent resource tracked in state; --cascade may also delete dependent resources.",
|
||||
"zh-CN": "该操作会删除 State 中跟踪的全部托管 Agent 资源;--cascade 还可能删除依赖资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--file <path>] [--cascade]",
|
||||
flags: DESTROY_FLAGS,
|
||||
exampleArgs: ["--yes", "--yes --cascade"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -86,14 +87,6 @@ export default defineCommand({
|
||||
else emitBare(line);
|
||||
}
|
||||
|
||||
if (!flags.yes) {
|
||||
throw new BailianError(
|
||||
`Refusing to destroy ${resources.length} resource(s) without confirmation.`,
|
||||
ExitCode.USAGE,
|
||||
"Re-run with --yes to destroy (add --cascade to remove dependents).",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
destroyPlannedProjectResources(planned, {
|
||||
|
||||
@@ -30,9 +30,16 @@ const SESSION_DELETE_FLAGS = {
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a session", "zh-CN": "删除 Session" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This deletes the specified remote managed Agent Session.",
|
||||
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
flags: SESSION_DELETE_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123"],
|
||||
exampleArgs: ["--session-id sess_abc123 --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
|
||||
@@ -37,9 +37,17 @@ export default defineCommand({
|
||||
"zh-CN": "从 State 中移除资源,但不销毁远端资源",
|
||||
},
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This removes the resource from local state without deleting it remotely, so this project will no longer track it.",
|
||||
"zh-CN": "该操作会从本地 State 中移除资源但不会删除远端资源,此项目将不再跟踪该资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--address <provider.type.name> [--file <path>]",
|
||||
flags: STATE_RM_FLAGS,
|
||||
exampleArgs: ["--address bailian.agent.assistant"],
|
||||
exampleArgs: ["--address bailian.agent.assistant --yes"],
|
||||
notes: OFFLINE_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineCommand, BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { defineCommand } from "bailian-cli-core";
|
||||
import { runPermissionChange, validatePermissionChange } from "./shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -7,7 +7,16 @@ export default defineCommand({
|
||||
"zh-CN": "撤销模型权限(推理 / 微调 / 部署)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <models> [--action <actions>] | --all --yes",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This revokes model permissions and may interrupt inference, fine-tuning, or deployment workloads. With --all, it also clears all historical inference grants.",
|
||||
"zh-CN":
|
||||
"该操作会撤销模型权限,可能导致推理、精调或部署任务中断;使用 --all 时会清除全部历史推理授权。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--model <models> [--action <actions>] | --all [flags]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
@@ -33,17 +42,10 @@ export default defineCommand({
|
||||
"zh-CN": "关闭一键授权并清除所有历史推理授权",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm --all without an interactive prompt (required)",
|
||||
"zh-CN": "无需交互提示确认执行 --all(必填)",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus",
|
||||
"--model qwen-plus,qwen3-max --action inference,finetune",
|
||||
"--model qwen-plus --yes",
|
||||
"--model qwen-plus,qwen3-max --action inference,finetune --yes",
|
||||
"--all --yes",
|
||||
"--model qwen-plus --dry-run --output json",
|
||||
],
|
||||
@@ -52,6 +54,11 @@ export default defineCommand({
|
||||
"en-US": "Grants apply to the business workspace your API key belongs to.",
|
||||
"zh-CN": "授权将应用于 API Key 所属的业务 Workspace。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"All revoke operations require --yes; use --dry-run to preview the request without confirmation.",
|
||||
"zh-CN": "所有撤权操作均需使用 --yes;可通过 --dry-run 免确认预览请求。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.",
|
||||
@@ -65,14 +72,6 @@ export default defineCommand({
|
||||
],
|
||||
validate: (flags) => validatePermissionChange(flags),
|
||||
async run(ctx) {
|
||||
const { flags, settings } = ctx;
|
||||
if (flags.all && !flags.yes && !settings.dryRun) {
|
||||
throw new BailianError(
|
||||
"Refusing to clear all historical inference grants without confirmation.",
|
||||
ExitCode.USAGE,
|
||||
"Re-run with --yes to close one-key authorization (or preview with --dry-run).",
|
||||
);
|
||||
}
|
||||
await runPermissionChange(ctx, flags, false);
|
||||
await runPermissionChange(ctx, ctx.flags, false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Clear all custom rate limits (QPM/TPM) for a model",
|
||||
"zh-CN": "清除模型的所有自定义限流配置(QPM/TPM)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This permanently clears all custom QPM/TPM rate limits for the specified model and cannot be undone.",
|
||||
"zh-CN": "该操作会永久清除指定模型的所有自定义 QPM/TPM 限流配置,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--model <model>",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: { "en-US": "Model name (required)", "zh-CN": "模型名称(必填)" },
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus",
|
||||
"--model qwen-plus --dry-run --output json",
|
||||
"--model qwen-plus --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const modelName = flags.model;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = { models: [{ model: modelName, operation_type: "DELETE" }] };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{ endpoint: ctx.client.url(modelsLimitsPath()), method: "POST", request: body },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.requestJson<{ request_id?: string }>({
|
||||
path: modelsLimitsPath(),
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ model: modelName, ...result }, format);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`Rate limits cleared for "${modelName}".\n`);
|
||||
},
|
||||
});
|
||||
@@ -1,16 +1,16 @@
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
|
||||
const MINUTE_SECONDS = 60;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Update model rate limits (QPM/TPM), or clear them with --delete",
|
||||
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
|
||||
"en-US": "Update model rate limits (QPM/TPM)",
|
||||
"zh-CN": "更新模型限流配置(QPM/TPM)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
@@ -34,38 +34,18 @@ export default defineCommand({
|
||||
"zh-CN": "每分钟最大 Token 数(TPM)",
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Clear all custom rate limits for the model",
|
||||
"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: [
|
||||
{
|
||||
"en-US":
|
||||
"Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.",
|
||||
'Fields you omit keep their current values (server-side OVERLAY merge). Clear all custom limits with the "quota delete" command instead.',
|
||||
"zh-CN":
|
||||
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
|
||||
},
|
||||
{
|
||||
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
|
||||
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
|
||||
"未指定的字段将保留当前值(服务端 OVERLAY 合并)。清除全部自定义限流配置请改用 “quota delete” 命令。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
@@ -74,10 +54,8 @@ export default defineCommand({
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (flags.delete && (flags.rpm !== undefined || flags.tpm !== undefined))
|
||||
return "--delete cannot be combined with --rpm/--tpm.";
|
||||
if (!flags.delete && flags.rpm === undefined && flags.tpm === undefined)
|
||||
return "one of --rpm / --tpm / --delete is required.";
|
||||
if (flags.rpm === undefined && flags.tpm === undefined)
|
||||
return "one of --rpm / --tpm is required.";
|
||||
if (flags.rpm !== undefined && flags.rpm < 0) return "--rpm must be a non-negative number.";
|
||||
if (flags.tpm !== undefined && flags.tpm < 0) return "--tpm must be a non-negative number.";
|
||||
return undefined;
|
||||
@@ -88,17 +66,13 @@ export default defineCommand({
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const entry: Record<string, unknown> = { model: modelName };
|
||||
if (flags.delete) {
|
||||
entry.operation_type = "DELETE";
|
||||
} else {
|
||||
if (flags.rpm !== undefined) {
|
||||
entry.request_limit = flags.rpm;
|
||||
entry.request_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
if (flags.tpm !== undefined) {
|
||||
entry.usage_limit = flags.tpm;
|
||||
entry.usage_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
if (flags.rpm !== undefined) {
|
||||
entry.request_limit = flags.rpm;
|
||||
entry.request_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
if (flags.tpm !== undefined) {
|
||||
entry.usage_limit = flags.tpm;
|
||||
entry.usage_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
const body = { models: [entry] };
|
||||
|
||||
@@ -110,13 +84,6 @@ 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",
|
||||
@@ -127,11 +94,6 @@ export default defineCommand({
|
||||
emitResult({ model: modelName, ...result }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.delete) {
|
||||
process.stdout.write(`Rate limits cleared for "${modelName}".\n`);
|
||||
return;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (flags.rpm !== undefined) parts.push(`QPM ${formatNumber(flags.rpm)}`);
|
||||
if (flags.tpm !== undefined) parts.push(`TPM ${formatNumber(flags.tpm)}`);
|
||||
|
||||
@@ -88,6 +88,7 @@ export { default as modelList } from "./commands/model/list.ts";
|
||||
export { default as workspaceList } from "./commands/workspace/list.ts";
|
||||
export { default as quotaList } from "./commands/quota/list.ts";
|
||||
export { default as quotaUpdate } from "./commands/quota/update.ts";
|
||||
export { default as quotaDelete } from "./commands/quota/delete.ts";
|
||||
export { default as quotaHistory } from "./commands/quota/history.ts";
|
||||
export { default as quotaCheck } from "./commands/quota/check.ts";
|
||||
export { default as permissionList } from "./commands/permission/list.ts";
|
||||
|
||||
@@ -293,16 +293,6 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
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",
|
||||
@@ -317,16 +307,34 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe("dataset.delete");
|
||||
});
|
||||
});
|
||||
|
||||
test("dataset delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
describe("e2e: dataset high-risk confirmation", () => {
|
||||
test("dataset delete --help 展示 runtime 注入的 --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 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file-id-xxx",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -201,24 +201,30 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe(`deploy.${sub}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("deploy delete --help 展示 --yes", async () => {
|
||||
describe("e2e: deploy high-risk confirmation", () => {
|
||||
test("deploy delete --help 展示 runtime 注入的 --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 保证确认门在发任何网络请求前触发
|
||||
test("deploy delete 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||||
"deploy",
|
||||
"delete",
|
||||
"--deployed-model",
|
||||
"dep-xxx",
|
||||
"--skip-precheck",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -294,27 +294,6 @@ 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",
|
||||
@@ -493,6 +472,35 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("e2e: finetune high-risk confirmation", () => {
|
||||
test("finetune delete --help 展示 runtime 注入的 --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 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"delete",
|
||||
"--job-id",
|
||||
"ft-xxx",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (DashScope)", () => {
|
||||
/**
|
||||
* 不同开发者的 key 状态不一:可能鉴权失败、可能账号下没有任何微调记录、
|
||||
|
||||
@@ -434,7 +434,7 @@ describe("e2e: knowledge chunk 组 (静态)", () => {
|
||||
expect(data.batches[1]!.request.chunkIds).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("chunk delete: 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("chunk delete: 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
@@ -447,9 +447,13 @@ describe("e2e: knowledge chunk 组 (静态)", () => {
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -550,7 +554,22 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", ()
|
||||
expect(data.request?.connectorId).toBe("conn_test");
|
||||
});
|
||||
|
||||
test("category delete: 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("category delete: dry-run 断言 categoryId", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"category",
|
||||
"delete",
|
||||
"--category-id",
|
||||
"cate_test",
|
||||
...COMMON,
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.endpoint).toMatch(/deleteCategory/);
|
||||
expect(data.request?.categoryId).toBe("cate_test");
|
||||
});
|
||||
|
||||
test("category delete: 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"category",
|
||||
@@ -561,9 +580,13 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", ()
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("file list: 缺 --category-id 报 USAGE (2)", async () => {
|
||||
@@ -619,7 +642,22 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", ()
|
||||
expect(data.request?.fileId).toBe("file_test");
|
||||
});
|
||||
|
||||
test("file delete: 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("file delete: dry-run 断言 fileId", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"file",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file_test",
|
||||
...COMMON,
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.endpoint).toMatch(/deleteFile/);
|
||||
expect(data.request?.fileId).toBe("file_test");
|
||||
});
|
||||
|
||||
test("file delete: 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"file",
|
||||
@@ -630,9 +668,13 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", ()
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("collection create: --name 21 字符 USAGE (2)", async () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("e2e: knowledge doc delete", () => {
|
||||
expect(data.request?.doc_ids).toEqual(["file_a", "file_b"]);
|
||||
});
|
||||
|
||||
test("非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
@@ -79,9 +79,18 @@ describe("e2e: knowledge doc delete", () => {
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: {
|
||||
code: 7,
|
||||
type: "requires_confirmation",
|
||||
hint: expect.stringContaining("--yes"),
|
||||
},
|
||||
});
|
||||
expect(stderr).not.toContain("sk-fake");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("e2e: knowledge kb delete", () => {
|
||||
expect(data.request?.index_id).toBe("idx_test");
|
||||
});
|
||||
|
||||
test("非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"delete",
|
||||
@@ -58,9 +58,17 @@ describe("e2e: knowledge kb delete", () => {
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: {
|
||||
code: 7,
|
||||
type: "requires_confirmation",
|
||||
hint: expect.stringContaining("--yes"),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -454,7 +454,7 @@ describe("e2e: knowledge service update", () => {
|
||||
});
|
||||
|
||||
describe("e2e: knowledge service deploy / delete (危险)", () => {
|
||||
test("deploy: 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("deploy: 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
@@ -465,9 +465,13 @@ describe("e2e: knowledge service deploy / delete (危险)", () => {
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("deploy: --dry-run 断言 body", async () => {
|
||||
@@ -491,7 +495,7 @@ describe("e2e: knowledge service deploy / delete (危险)", () => {
|
||||
expect(data.request?.agent_version_desc).toBe("v1 desc");
|
||||
});
|
||||
|
||||
test("delete: 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
test("delete: 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
@@ -502,9 +506,32 @@ describe("e2e: knowledge service deploy / delete (危险)", () => {
|
||||
"sk-fake",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("delete: --dry-run 断言 body", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
"delete",
|
||||
"--agent-id",
|
||||
"aid_test",
|
||||
"--workspace-id",
|
||||
"ws_test",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.endpoint).toMatch(/rag\/app\/delete/);
|
||||
expect(data.request?.agent_id).toBe("aid_test");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -132,6 +132,55 @@ describe("e2e: managed-agent", () => {
|
||||
expect(stderr).toMatch(/--file|--provider|--yes/i);
|
||||
});
|
||||
|
||||
test.each(["apply", "destroy"])("managed-agent %s 无 --yes 返回确认请求 (7)", async (command) => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
command,
|
||||
"--file",
|
||||
"agents.e2e-missing.yaml",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
["state rm", ["state", "rm"]],
|
||||
["session delete", ["session", "delete"]],
|
||||
])("managed-agent %s --help 展示 runtime 注入的 --yes", async (_commandName, commandPath) => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
...commandPath,
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["state rm", ["state", "rm", "--address", "bailian.agent.assistant"]],
|
||||
[
|
||||
"session delete",
|
||||
["session", "delete", "--session-id", "sess_e2e", "--api-key", "e2e-dummy-key"],
|
||||
],
|
||||
])("managed-agent %s 无 --yes 返回确认请求 (7)", async (_commandName, commandArgs) => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
...commandArgs,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
@@ -223,7 +272,6 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () =>
|
||||
"managed-agent",
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--yes",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
|
||||
@@ -98,18 +98,39 @@ describe("e2e: permission", () => {
|
||||
expect(stderr).toContain("at most 20");
|
||||
});
|
||||
|
||||
test("permission revoke --all 缺 --yes 拒绝执行", async () => {
|
||||
// --yes 护栏在 run() 开头、任何网络调用之前抛出;带 dummy key 让用例不依赖环境凭证(否则 auth stage 先报 AUTH(3))。
|
||||
test("permission revoke --all 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"revoke",
|
||||
"--all",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("Refusing");
|
||||
expect(stderr).toContain("--yes");
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("permission revoke --model 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"revoke",
|
||||
"--model",
|
||||
"qwen-plus",
|
||||
"--api-key",
|
||||
"e2e-dummy-key",
|
||||
"--base-url",
|
||||
"http://127.0.0.1:1",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
// --dry-run 跳过 auth stage(见 runtime middleware),无需凭证即可断言请求形状。
|
||||
|
||||
@@ -31,15 +31,20 @@ describe("e2e: quota", () => {
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--rpm");
|
||||
expect(stderr).toContain("--tpm");
|
||||
expect(stderr).toContain("--delete");
|
||||
});
|
||||
|
||||
test("quota delete --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "delete", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--yes");
|
||||
expect(stderr).toContain("bl quota delete --model qwen-plus");
|
||||
});
|
||||
|
||||
test("quota request 作为 quota update 的兼容别名可用", async () => {
|
||||
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 () => {
|
||||
@@ -68,7 +73,7 @@ describe("e2e: quota", () => {
|
||||
expect(stderr).toContain("at least 1 minute");
|
||||
});
|
||||
|
||||
test("quota update 缺少 --rpm/--tpm/--delete 报用法错误", async () => {
|
||||
test("quota update 缺少 --rpm/--tpm 报用法错误", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
"quota",
|
||||
"update",
|
||||
@@ -76,32 +81,38 @@ describe("e2e: quota", () => {
|
||||
"qwen-plus",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("one of --rpm / --tpm / --delete");
|
||||
expect(stderr).toContain("one of --rpm / --tpm");
|
||||
});
|
||||
|
||||
test("quota update --delete 与 --rpm 互斥", async () => {
|
||||
test("quota update 不再接受 --delete", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
"quota",
|
||||
"update",
|
||||
"--model",
|
||||
"qwen-plus",
|
||||
"--delete",
|
||||
"--rpm",
|
||||
"60",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("cannot be combined");
|
||||
expect(stderr).toContain("Unknown flag");
|
||||
});
|
||||
|
||||
test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
// 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发
|
||||
test("quota delete 缺少 --model 报用法错误", async () => {
|
||||
// 裸 `quota delete`(无任何 flag)会渲染 help 并正常退出,需带 flag 触发必填校验
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "delete", "--yes"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("Missing required flag: --model");
|
||||
});
|
||||
|
||||
test("quota delete 无 --yes 返回确认请求 (7)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(
|
||||
QUOTA_ROUTES,
|
||||
["quota", "update", "--model", "qwen-plus", "--delete"],
|
||||
["quota", "delete", "--model", "qwen-plus", "--output", "json"],
|
||||
{ DASHSCOPE_API_KEY: "sk-e2e-quota-delete" },
|
||||
);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
expect(exitCode).toBe(7);
|
||||
expect(JSON.parse(stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
});
|
||||
|
||||
test("quota update --rpm 负数报错", async () => {
|
||||
@@ -196,13 +207,12 @@ describe("e2e: quota", () => {
|
||||
expect(entry?.usage_limit_period).toBe(60);
|
||||
});
|
||||
|
||||
test("quota update --delete --dry-run 输出 DELETE 操作", async () => {
|
||||
test("quota delete --dry-run 输出 DELETE 操作", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
"quota",
|
||||
"update",
|
||||
"delete",
|
||||
"--model",
|
||||
"qwen-plus",
|
||||
"--delete",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
@@ -211,6 +221,7 @@ describe("e2e: quota", () => {
|
||||
const data = parseStdoutJson<{
|
||||
request?: { models?: { model?: string; operation_type?: string }[] };
|
||||
}>(stdout);
|
||||
expect(data.request?.models?.[0]?.model).toBe("qwen-plus");
|
||||
expect(data.request?.models?.[0]?.operation_type).toBe("DELETE");
|
||||
});
|
||||
|
||||
@@ -285,8 +296,8 @@ describe("e2e: quota", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 真实调用 GET /api/v1/models/limits。quota update 只测 --dry-run——live POST
|
||||
// 会真实改写账号限流,不做 e2e。
|
||||
// 真实调用 GET /api/v1/models/limits。quota update / quota delete 只测
|
||||
// --dry-run——live POST 会真实改写账号限流,不做 e2e。
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: quota(DashScope)", () => {
|
||||
test("quota list 文本输出正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
|
||||
@@ -103,6 +103,7 @@ export const ADVISOR_ROUTES: E2eRouteExports = {
|
||||
export const QUOTA_ROUTES: E2eRouteExports = {
|
||||
"quota list": "quotaList",
|
||||
"quota update": "quotaUpdate",
|
||||
"quota delete": "quotaDelete",
|
||||
// Backward-compatible alias of "quota update".
|
||||
"quota request": "quotaUpdate",
|
||||
"quota history": "quotaHistory",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -37,6 +37,8 @@ const MODELS_FILE = "models.jsonl";
|
||||
const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
/** Tighter than the interactive default: the silent channel must not stall `bl advisor recommend` */
|
||||
const INDEX_TIMEOUT_MS = 3000;
|
||||
/** No retries on the silent channel: a failed sync simply tries again on the next recommend */
|
||||
const FETCH_ATTEMPTS = 1;
|
||||
|
||||
interface SyncState {
|
||||
lastChecked: number;
|
||||
@@ -108,7 +110,7 @@ function wikiLockNeedsBackfill(contentHash: string): boolean {
|
||||
/** Fetch skills/index.json via the shared registry client and extract the wiki skill entry; returns null on any failure */
|
||||
async function fetchIndexEntry(): Promise<SkillIndexEntry | null> {
|
||||
try {
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS);
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS, FETCH_ATTEMPTS);
|
||||
return index.skills[WIKI_SKILL_NAME] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -160,6 +162,7 @@ export async function maybeSyncWikiData(): Promise<boolean> {
|
||||
entry,
|
||||
detectInstalledAgents(),
|
||||
previousLinks,
|
||||
FETCH_ATTEMPTS,
|
||||
);
|
||||
recordWikiInLock(record.lockEntry);
|
||||
} catch {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ExitCode = {
|
||||
QUOTA: 4,
|
||||
TIMEOUT: 5,
|
||||
NETWORK: 6,
|
||||
CONFIRMATION_REQUIRED: 7,
|
||||
CONTENT_FILTER: 10,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -100,5 +100,12 @@ export function atomicSwap(tmpDir: string, destDir: string): void {
|
||||
if (existsSync(backup) && !existsSync(destDir)) renameSync(backup, destDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup: the swap already succeeded, so a backup deletion failure
|
||||
// (permissions, host safe-delete guards on large dirs) must not fail the install;
|
||||
// leftover .old-* dirs are inert (skill status scans ignore them)
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed install as failed */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,22 @@ export async function installSkillFromBuffer(
|
||||
atomicSwap(tmpDir, dest);
|
||||
return { name, path: dest, meta };
|
||||
} finally {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
// Best-effort cleanup: on failure paths this must not mask the original error,
|
||||
// and on success the dir is already renamed away (existsSync → false)
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* leave the temp dir rather than hide the real error */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Install a single skill by index entry (download + validate + write to disk) */
|
||||
export async function installSkill(name: string, entry: SkillIndexEntry): Promise<InstalledSkill> {
|
||||
export async function installSkill(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
downloadAttempts?: number,
|
||||
): Promise<InstalledSkill> {
|
||||
if (entry.compression && entry.compression !== "tar.br") {
|
||||
throw new BailianError(
|
||||
`Skill ${name} uses unsupported compression format: ${entry.compression}`,
|
||||
@@ -74,7 +84,7 @@ export async function installSkill(name: string, entry: SkillIndexEntry): Promis
|
||||
"Upgrade bailian-cli to the latest version and retry",
|
||||
);
|
||||
}
|
||||
const buffer = await downloadSkillAsset(name, entry);
|
||||
const buffer = await downloadSkillAsset(name, entry, downloadAttempts);
|
||||
return installSkillFromBuffer(name, buffer, entry.contentHash);
|
||||
}
|
||||
|
||||
@@ -116,14 +126,17 @@ export interface SkillInstallRecord {
|
||||
* entry (batch writeSkillLock for commands, best-effort upsertSkillLockEntry for silent channels).
|
||||
* recordedLinks = the skill's previously recorded fan-out paths from the lock; lets the
|
||||
* fan-out replace copy-fallback artifacts and keeps unvisited paths reclaimable.
|
||||
* downloadAttempts = registry fetch attempts (undefined → interactive default; silent
|
||||
* background channels pass 1 to fail fast instead of stalling the host command).
|
||||
*/
|
||||
export async function installSkillWithFanout(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
agents: AgentTarget[] = detectInstalledAgents(),
|
||||
recordedLinks: string[] = [],
|
||||
downloadAttempts?: number,
|
||||
): Promise<SkillInstallRecord> {
|
||||
await installSkill(name, entry);
|
||||
await installSkill(name, entry, downloadAttempts);
|
||||
const fanout = fanOutSkillToAgents(name, agents, recordedLinks);
|
||||
return {
|
||||
lockEntry: buildSkillLockEntry(entry, fanout.links),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import { withRetry } from "../utils/retry.ts";
|
||||
import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
|
||||
/**
|
||||
@@ -9,8 +10,10 @@ import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
*/
|
||||
const DEFAULT_REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills";
|
||||
|
||||
const INDEX_TIMEOUT_MS = 10_000;
|
||||
const INDEX_TIMEOUT_MS = 30_000;
|
||||
const ASSET_TIMEOUT_MS = 120_000;
|
||||
/** Interactive channels retry transient failures; silent background channels pass 1 to fail fast */
|
||||
const DEFAULT_ATTEMPTS = 3;
|
||||
|
||||
export function getSkillRegistryBaseUrl(): string {
|
||||
const override = process.env.BAILIAN_SKILL_REGISTRY_URL?.trim();
|
||||
@@ -20,55 +23,64 @@ export function getSkillRegistryBaseUrl(): string {
|
||||
/**
|
||||
* Fetch the remote skill index. No local caching — the diff comparison is always
|
||||
* "live remote index vs local skill-lock.json".
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout than the interactive default.
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout and attempts=1
|
||||
* than the interactive defaults.
|
||||
*/
|
||||
export async function fetchSkillsIndex(timeoutMs: number = INDEX_TIMEOUT_MS): Promise<SkillsIndex> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
export async function fetchSkillsIndex(
|
||||
timeoutMs: number = INDEX_TIMEOUT_MS,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<SkillsIndex> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,29 +96,38 @@ export function resolveAssetFileName(entry?: SkillIndexEntry): string {
|
||||
}
|
||||
|
||||
/** Download the tar.br archive for a single skill (one skill = one GET) */
|
||||
export async function downloadSkillAsset(name: string, entry?: SkillIndexEntry): Promise<Buffer> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
export async function downloadSkillAsset(
|
||||
name: string,
|
||||
entry?: SkillIndexEntry,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<Buffer> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface TrackingEvent {
|
||||
durationMs: number;
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
exitCode?: number;
|
||||
httpStatus?: number;
|
||||
requestId?: string;
|
||||
cliVersion: string;
|
||||
@@ -19,7 +20,7 @@ export function createTrackingEvent(opts: {
|
||||
command: string;
|
||||
durationMs: number;
|
||||
success: boolean;
|
||||
error?: { message?: string; httpStatus?: number; requestId?: string };
|
||||
error?: { message?: string; exitCode?: number; httpStatus?: number; requestId?: string };
|
||||
cliVersion: string;
|
||||
authMethod?: AuthRequirement;
|
||||
params?: Record<string, unknown>;
|
||||
@@ -40,6 +41,7 @@ export function createTrackingEvent(opts: {
|
||||
|
||||
if (!opts.success && opts.error) {
|
||||
if (opts.error.message) event.errorMessage = opts.error.message;
|
||||
if (opts.error.exitCode !== undefined) event.exitCode = opts.error.exitCode;
|
||||
if (opts.error.httpStatus !== undefined) event.httpStatus = opts.error.httpStatus;
|
||||
if (opts.error.requestId) event.requestId = opts.error.requestId;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function trackCommandExecution(
|
||||
const start = performance.now();
|
||||
let success = true;
|
||||
let errorMessage: string | undefined;
|
||||
let exitCode: number | undefined;
|
||||
let httpStatus: number | undefined;
|
||||
let requestId: string | undefined;
|
||||
|
||||
@@ -112,6 +113,7 @@ export async function trackCommandExecution(
|
||||
success = false;
|
||||
if (err instanceof BailianError) {
|
||||
errorMessage = err.message;
|
||||
exitCode = err.exitCode;
|
||||
httpStatus = err.api?.httpStatus;
|
||||
requestId = err.api?.requestId;
|
||||
} else if (err instanceof Error) {
|
||||
@@ -125,7 +127,7 @@ export async function trackCommandExecution(
|
||||
command: commandPath.join(" "),
|
||||
durationMs,
|
||||
success,
|
||||
error: success ? undefined : { message: errorMessage, httpStatus, requestId },
|
||||
error: success ? undefined : { message: errorMessage, exitCode, httpStatus, requestId },
|
||||
cliVersion: deps.identity.version,
|
||||
authMethod: deps.authMethod,
|
||||
params: extractParams(flags),
|
||||
|
||||
@@ -258,10 +258,24 @@ export interface CommandContext<F extends FlagsDef = FlagsDef> {
|
||||
* typed flags (`ParsedFlags<F>` = 命令自有 flag). Stored heterogeneously as
|
||||
* {@link AnyCommand}; the precise typing lives at the `defineCommand` call site.
|
||||
*/
|
||||
export type CommandRiskLevel = "high";
|
||||
|
||||
export interface CommandRisk {
|
||||
level: CommandRiskLevel;
|
||||
message: LocalizedText;
|
||||
}
|
||||
|
||||
export interface Command<F extends FlagsDef = FlagsDef> {
|
||||
description: LocalizedText;
|
||||
/** Credential this command requires. See {@link AuthRequirement}. */
|
||||
auth: AuthRequirement;
|
||||
/**
|
||||
* Runtime-classified operation risk and its user-facing consequence message.
|
||||
* Omit for normal commands.
|
||||
* High-risk commands must return from `run` on `settings.dryRun` before any
|
||||
* remote request or local write; runtime only owns the confirmation gate.
|
||||
*/
|
||||
risk?: CommandRisk;
|
||||
/** Usage line arg portion, e.g. "--prompt <text> [flags]". Manually written. */
|
||||
usageArgs?: string;
|
||||
/** Example args (without the `<bin> <path>` prefix). */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type {
|
||||
Command,
|
||||
CommandRisk,
|
||||
CommandRiskLevel,
|
||||
AnyCommand,
|
||||
CommandContext,
|
||||
LocalizedText,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { defineCommand, type CommandPack, type CommandRiskLevel } from "../src/index.ts";
|
||||
|
||||
const noopRun = async () => {};
|
||||
|
||||
test("high-risk commands keep level and message in one typed object", () => {
|
||||
const command = defineCommand({
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
risk: { level: "high", message: "dangerous operation" },
|
||||
run: noopRun,
|
||||
});
|
||||
const pack = {
|
||||
"agent dangerous": {
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
risk: { level: "high", message: "dangerous operation" },
|
||||
run: noopRun,
|
||||
},
|
||||
} satisfies CommandPack;
|
||||
|
||||
expect(command.risk).toEqual({ level: "high", message: "dangerous operation" });
|
||||
expect(pack["agent dangerous"].risk.level).toBe("high");
|
||||
});
|
||||
|
||||
test("risk types reject flat or incomplete declarations", () => {
|
||||
const high = "high" satisfies CommandRiskLevel;
|
||||
// @ts-expect-error unsupported levels must be added to CommandRiskLevel first.
|
||||
const low = "low" satisfies CommandRiskLevel;
|
||||
|
||||
defineCommand({
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
// @ts-expect-error high-risk metadata requires a message.
|
||||
risk: { level: "high" },
|
||||
run: noopRun,
|
||||
});
|
||||
defineCommand({
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
// @ts-expect-error risk metadata is a single object, not a flat level.
|
||||
risk: "high",
|
||||
run: noopRun,
|
||||
});
|
||||
|
||||
expect(high).toBe("high");
|
||||
expect(low).toBe("low");
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "fs";
|
||||
import { createHash } from "crypto";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { brotliCompressSync } from "zlib";
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { brotliCompressSync } from "node:zlib";
|
||||
import tar from "tar-stream";
|
||||
import { afterEach, expect, test, vi } from "vite-plus/test";
|
||||
import { BailianError } from "../src/errors/base.ts";
|
||||
@@ -10,6 +10,18 @@ import type { AgentTarget } from "../src/skills/agents.ts";
|
||||
import { isSafeEntryName } from "../src/skills/extract.ts";
|
||||
import { installSkillFromBuffer, installSkillWithFanout } from "../src/skills/installer.ts";
|
||||
import { getSkillsDir } from "../src/skills/lock.ts";
|
||||
import { downloadSkillAsset, fetchSkillsIndex } from "../src/skills/registry.ts";
|
||||
|
||||
/** rmSync wrapped in a spy so tests can simulate host deletion guards (e.g. safe-delete) */
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
rmSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
fsMocks.rmSync.mockImplementation(actual.rmSync);
|
||||
return { ...actual, rmSync: fsMocks.rmSync };
|
||||
});
|
||||
|
||||
/** Run in an isolated temp config dir, restore env afterwards. */
|
||||
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
|
||||
@@ -225,15 +237,83 @@ test("fanout install: downloads, links agents, and builds lock entry with merged
|
||||
});
|
||||
});
|
||||
|
||||
test("fanout install: download failure surfaces as BailianError and leaves no canonical dir", async () => {
|
||||
test("fanout install: download failure exhausts retries and leaves no canonical dir", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: false, status: 404 })),
|
||||
);
|
||||
const fetchMock = vi.fn(async () => ({ ok: false, status: 404 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await expect(
|
||||
installSkillWithFanout("demo", { contentHash: "sha256:whatever" }, []),
|
||||
).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(existsSync(join(getSkillsDir(), "demo"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- host deletion guards (safe-delete): backup cleanup must not fail a completed install ----
|
||||
|
||||
const GUARD_ERROR =
|
||||
'[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":1583,"threshold":500}';
|
||||
|
||||
test("installer: guard blocking backup deletion does not fail the install", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": VALID_SKILL_MD }));
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const v2 = "---\nname: demo\ndescription: demo skill v2\n---\n";
|
||||
const installed = await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": v2 }));
|
||||
expect(installed.name).toBe("demo");
|
||||
expect(readFileSync(join(getSkillsDir(), "demo", "SKILL.md"), "utf-8")).toBe(v2);
|
||||
// The blocked backup stays on disk but is inert (status scans ignore .old-*)
|
||||
const leftovers = readdirSync(getSkillsDir()).filter((entry) => entry.startsWith("demo.old-"));
|
||||
expect(leftovers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("installer: temp cleanup failure does not mask the original error", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const buf = await buildTarBr({ "SKILL.md": VALID_SKILL_MD, "../evil.txt": "pwned\n" });
|
||||
await expect(installSkillFromBuffer("demo", buf)).rejects.toThrow(/unsafe tar entry/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- registry retry policy ----
|
||||
|
||||
test("registry: index fetch retries transient failures and succeeds", async () => {
|
||||
const indexPayload = { skills: { demo: { contentHash: "sha256:abc" } } };
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length < 3) throw new Error("network down");
|
||||
return { ok: true, status: 200, json: async () => indexPayload };
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const index = await fetchSkillsIndex(1000);
|
||||
expect(index.skills.demo.contentHash).toBe("sha256:abc");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("registry: attempts=1 keeps the silent channel fail-fast", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await expect(fetchSkillsIndex(1000, 1)).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("registry: asset download retries transient HTTP errors", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length === 1) return { ok: false, status: 503 };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
};
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const buffer = await downloadSkillAsset("demo");
|
||||
expect([...buffer]).toEqual([1, 2, 3]);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { expect, test, vi } from "vite-plus/test";
|
||||
import type { Identity, Settings } from "../src/config/schema.ts";
|
||||
import { BailianError } from "../src/errors/base.ts";
|
||||
import { ExitCode } from "../src/errors/codes.ts";
|
||||
import { buildRemoteAemOptions, type TrackingEvent } from "../src/telemetry/event.ts";
|
||||
|
||||
const sinkMocks = vi.hoisted(() => ({
|
||||
localSink: vi.fn<(event: TrackingEvent) => Promise<void>>(async () => {}),
|
||||
remoteSink: vi.fn<(event: TrackingEvent) => Promise<void>>(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../src/telemetry/sink.ts", () => sinkMocks);
|
||||
|
||||
import { trackCommandExecution } from "../src/telemetry/tracker.ts";
|
||||
|
||||
const identity: Identity = {
|
||||
binName: "bl",
|
||||
version: "0.0.0-test",
|
||||
clientName: "bailian-cli-test",
|
||||
npmPackage: "bailian-cli",
|
||||
};
|
||||
|
||||
test("records BailianError exitCode in local events and AEM ext", async () => {
|
||||
sinkMocks.localSink.mockClear();
|
||||
sinkMocks.remoteSink.mockClear();
|
||||
const error = new BailianError("该操作会永久删除文档。", ExitCode.CONFIRMATION_REQUIRED);
|
||||
|
||||
await expect(
|
||||
trackCommandExecution(
|
||||
{
|
||||
identity,
|
||||
settings: { telemetry: true } as Settings,
|
||||
authMethod: "apiKey",
|
||||
},
|
||||
["knowledge", "doc", "delete"],
|
||||
{},
|
||||
async () => {
|
||||
throw error;
|
||||
},
|
||||
),
|
||||
).rejects.toBe(error);
|
||||
|
||||
expect(sinkMocks.localSink).toHaveBeenCalledOnce();
|
||||
const event = sinkMocks.localSink.mock.calls[0]![0];
|
||||
expect(event).toMatchObject({
|
||||
command: "knowledge doc delete",
|
||||
success: false,
|
||||
exitCode: 7,
|
||||
errorMessage: "该操作会永久删除文档。",
|
||||
});
|
||||
expect(buildRemoteAemOptions(event)).toMatchObject({
|
||||
ext: expect.objectContaining({ exitCode: 7 }),
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"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": {
|
||||
|
||||
@@ -79,6 +79,13 @@ function isLocalizedText(value: unknown): value is LocalizedText {
|
||||
);
|
||||
}
|
||||
|
||||
function isCommandRisk(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
|
||||
const risk = value as Record<string, unknown>;
|
||||
return risk.level === "high" && isLocalizedText(risk.message);
|
||||
}
|
||||
|
||||
function assertCommand(path: string, value: unknown): asserts value is CommandPackCommand<any> {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new Error(`Command "${path}" must export an object.`);
|
||||
@@ -90,6 +97,9 @@ function assertCommand(path: string, value: unknown): asserts value is CommandPa
|
||||
if (!command.auth || !AUTH_REQUIREMENTS.has(command.auth)) {
|
||||
throw new Error(`Command "${path}" has an invalid auth requirement.`);
|
||||
}
|
||||
if (command.risk !== undefined && !isCommandRisk(command.risk)) {
|
||||
throw new Error(`Command "${path}" has invalid risk metadata.`);
|
||||
}
|
||||
if (typeof command.run !== "function") {
|
||||
throw new Error(`Command "${path}" is missing run(ctx).`);
|
||||
}
|
||||
@@ -106,6 +116,7 @@ function adaptCommandPack(
|
||||
{
|
||||
description: command.description,
|
||||
auth: command.auth,
|
||||
risk: command.risk,
|
||||
usageArgs: command.usageArgs,
|
||||
exampleArgs: command.exampleArgs,
|
||||
notes: command.notes,
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
// Confirmation guard for dangerous operations — used by irreversible or
|
||||
// production-affecting commands (kb/doc/chunk/category/file delete, service
|
||||
// delete/deploy, ...).
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type CommandRisk,
|
||||
type FlagsDef,
|
||||
type LocalizedText,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
/**
|
||||
* - `yes` (the command's own --yes switch) → pass through
|
||||
* - TTY: print the summary and wait for y/yes (case-insensitive); any other
|
||||
* input cancels with exit SUCCESS (cancellation is not an error)
|
||||
* - non-TTY without --yes: throw USAGE
|
||||
*/
|
||||
export async function confirmDangerousAction(summary: string, yes: boolean): Promise<void> {
|
||||
if (yes) return;
|
||||
if (!process.stdin.isTTY) {
|
||||
throw new BailianError(
|
||||
"Confirmation required for this destructive action.",
|
||||
ExitCode.USAGE,
|
||||
"Re-run with --yes to confirm in non-interactive mode",
|
||||
);
|
||||
/** Runtime-owned flag: commands declare risk, never their own confirmation flag. */
|
||||
export const CONFIRMATION_FLAGS = {
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm this high-risk operation",
|
||||
"zh-CN": "确认执行此高风险操作",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export function confirmationFlagDefs(command: { risk?: CommandRisk }): FlagsDef {
|
||||
return command.risk === undefined ? {} : CONFIRMATION_FLAGS;
|
||||
}
|
||||
|
||||
export function confirmationHint(): LocalizedText {
|
||||
return {
|
||||
"en-US":
|
||||
"This command performs a high-risk operation. To continue, add --yes to the original command and re-run it.",
|
||||
"zh-CN": "此命令将执行高风险操作。如确认继续,请在原命令中添加 --yes 后重新执行。",
|
||||
};
|
||||
}
|
||||
|
||||
interface ConfirmationRequiredErrorOptions {
|
||||
message: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
/** Semantic runtime error consumed by both humans and Agent callers. */
|
||||
export class ConfirmationRequiredError extends BailianError {
|
||||
constructor(options: ConfirmationRequiredErrorOptions) {
|
||||
super(options.message, ExitCode.CONFIRMATION_REQUIRED, options.hint);
|
||||
this.name = "ConfirmationRequiredError";
|
||||
}
|
||||
process.stderr.write(`${summary}\n`);
|
||||
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
||||
try {
|
||||
const answer = (await readline.question("Proceed? [y/N] ")).trim().toLowerCase();
|
||||
if (answer !== "y" && answer !== "yes") {
|
||||
process.stderr.write("Cancelled.\n");
|
||||
// Intentional: a user-initiated cancellation is not an error, and we want
|
||||
// to exit here rather than unwind through the middleware stack (which
|
||||
// would still print a success report for an action that did not happen).
|
||||
process.exit(ExitCode.SUCCESS);
|
||||
}
|
||||
} finally {
|
||||
readline.close();
|
||||
|
||||
override toJSON() {
|
||||
return {
|
||||
error: {
|
||||
code: this.exitCode,
|
||||
type: "requires_confirmation",
|
||||
message: this.message,
|
||||
hint: this.hint,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,18 @@ import {
|
||||
authStage,
|
||||
telemetryStage,
|
||||
versionCheckStage,
|
||||
confirmationStage,
|
||||
runCommandStage,
|
||||
type RunContext,
|
||||
} from "./middleware.ts";
|
||||
import type { AnyCommand, FlagsDef, Identity, ParsedFlags, SourceFlags } from "bailian-cli-core";
|
||||
import type {
|
||||
AnyCommand,
|
||||
FlagsDef,
|
||||
Identity,
|
||||
LocalizedText,
|
||||
ParsedFlags,
|
||||
SourceFlags,
|
||||
} from "bailian-cli-core";
|
||||
import {
|
||||
CONSOLE_AUTH_FLAGS,
|
||||
DEFAULT_LANGUAGE,
|
||||
@@ -33,6 +41,7 @@ import { loadCommandPacks } from "./command-packs/load.ts";
|
||||
import { createCommandPackManager } from "./command-packs/manager.ts";
|
||||
import type { CommandPackPolicy } from "./command-packs/types.ts";
|
||||
import { createTranslator } from "./i18n.ts";
|
||||
import { confirmationFlagDefs } from "./confirm.ts";
|
||||
|
||||
/** Per-product identity injected by each CLI entrypoint (bl / rag / …). */
|
||||
export interface CliOptions {
|
||||
@@ -113,7 +122,13 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
|
||||
installProcessHandlers(binName);
|
||||
|
||||
const runMiddleware = compose([versionCheckStage, telemetryStage, authStage, runCommandStage]);
|
||||
const runMiddleware = compose([
|
||||
telemetryStage,
|
||||
confirmationStage,
|
||||
versionCheckStage,
|
||||
authStage,
|
||||
runCommandStage,
|
||||
]);
|
||||
|
||||
function getLoadedCommandPacks(): ReturnType<typeof loadCommandPacks> {
|
||||
if (!loadedCommandPacksPromise) {
|
||||
@@ -122,11 +137,17 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
return loadedCommandPacksPromise;
|
||||
}
|
||||
|
||||
async function getRegistry(argv: string[]): Promise<CommandRegistry> {
|
||||
async function getRegistry(argv: string[]): Promise<{
|
||||
registry: CommandRegistry;
|
||||
localize: (text: LocalizedText) => string;
|
||||
}> {
|
||||
const localeSources = buildSources(pickConfigFlag(argv));
|
||||
const translator = createTranslator(localeSources.file.language ?? DEFAULT_LANGUAGE);
|
||||
const loaded = await getLoadedCommandPacks();
|
||||
return new CommandRegistry(loaded.commands, binName, translator);
|
||||
return {
|
||||
registry: new CommandRegistry(loaded.commands, binName, translator),
|
||||
localize: (text) => translator.localize(text),
|
||||
};
|
||||
}
|
||||
|
||||
/** Render help for `path`; root ([]) doubles as the onboarding / login guide. */
|
||||
@@ -157,7 +178,11 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatch(registry: CommandRegistry, argv: string[]): Promise<void> {
|
||||
async function dispatch(
|
||||
registry: CommandRegistry,
|
||||
argv: string[],
|
||||
localize: (text: LocalizedText) => string,
|
||||
): Promise<void> {
|
||||
const res = resolve(argv, registry);
|
||||
|
||||
switch (res.kind) {
|
||||
@@ -177,9 +202,11 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
try {
|
||||
// 全局与凭证域 flag 进 sources,命令自有 flag 进 ctx.flags。
|
||||
const credDefs = credentialFlagDefs(res.command);
|
||||
const confirmationDefs = confirmationFlagDefs(res.command);
|
||||
const parsedFlags = parseFlags(res.rest, {
|
||||
...GLOBAL_FLAGS,
|
||||
...credDefs,
|
||||
...confirmationDefs,
|
||||
...res.command.flags,
|
||||
}) as Record<string, unknown>;
|
||||
const globalFlags = pick(parsedFlags, [
|
||||
@@ -201,6 +228,8 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
path: res.path,
|
||||
command: res.command,
|
||||
flags: ownFlags,
|
||||
confirmed: parsedFlags.yes === true,
|
||||
localize,
|
||||
settings,
|
||||
sources,
|
||||
configStore: makeConfigStore(sources.configName),
|
||||
@@ -233,7 +262,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
run(argv: string[] = process.argv.slice(2)) {
|
||||
return Promise.resolve()
|
||||
.then(() => getRegistry(argv))
|
||||
.then((registry) => dispatch(registry, argv))
|
||||
.then(({ registry, localize }) => dispatch(registry, argv, localize))
|
||||
.catch(
|
||||
(err) => flushTelemetry(1000).finally(() => handleError(err, binName)) as unknown as void,
|
||||
);
|
||||
|
||||
@@ -63,7 +63,6 @@ export {
|
||||
|
||||
// Utility facilities consumed by commands
|
||||
export { poll } from "./utils/polling.ts";
|
||||
export { confirmDangerousAction } from "./confirm.ts";
|
||||
export { downloadFile, formatBytes } from "./utils/download.ts";
|
||||
export { runConcurrent, getConcurrency, downloadParallel } from "./utils/concurrent.ts";
|
||||
export { resolveImageSize } from "./utils/image-size.ts";
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ParsedFlags,
|
||||
ResolutionSources,
|
||||
Settings,
|
||||
LocalizedText,
|
||||
} from "bailian-cli-core";
|
||||
import {
|
||||
Client,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
performAutoUpdate,
|
||||
shouldAutoUpdate,
|
||||
} from "./utils/update-checker.ts";
|
||||
import { ConfirmationRequiredError, confirmationHint } from "./confirm.ts";
|
||||
|
||||
/**
|
||||
* What each middleware stage gets for the invocation in flight: the matched
|
||||
@@ -45,6 +47,10 @@ export interface RunContext {
|
||||
readonly command: AnyCommand;
|
||||
/** 只含本命令声明的 flag(分流后);全局 flag 在 sources/settings。 */
|
||||
flags: ParsedFlags<FlagsDef>;
|
||||
/** Whether the runtime-owned --yes flag was explicitly supplied. */
|
||||
readonly confirmed: boolean;
|
||||
/** Locale selector for runtime-owned command metadata and messages. */
|
||||
readonly localize: (text: LocalizedText) => string;
|
||||
/** 解析后的有效配置面(命令的新读取面;双轨迁移期与 config 并存)。 */
|
||||
settings: Settings;
|
||||
/** 解析源:provider/访问器用;业务命令不可见(窄视图类型不含此字段)。 */
|
||||
@@ -212,5 +218,21 @@ export const versionCheckStage: Middleware = async (ctx, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safety gate before update/auth/command stages. Telemetry may wrap this stage
|
||||
* so confirmation-required failures remain observable.
|
||||
*/
|
||||
export const confirmationStage: Middleware = async (ctx, next) => {
|
||||
if (ctx.command.risk === undefined || ctx.confirmed || ctx.settings.dryRun) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ConfirmationRequiredError({
|
||||
message: ctx.localize(ctx.command.risk.message),
|
||||
hint: ctx.localize(confirmationHint()),
|
||||
});
|
||||
};
|
||||
|
||||
/** Innermost stage: hand control to the command with its full context. */
|
||||
export const runCommandStage: Middleware = (ctx) => ctx.command.run(ctx);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AnyCommand,
|
||||
AuthRequirement,
|
||||
CommandRiskLevel,
|
||||
FlagDef,
|
||||
FlagsDef,
|
||||
LocalizedText,
|
||||
@@ -17,6 +18,7 @@ import { camelToKebab } from "./args.ts";
|
||||
import type { Translator } from "./i18n.ts";
|
||||
import { printQuickStart, printWelcomeBanner } from "./output/banner.ts";
|
||||
import { ansi } from "./output/color.ts";
|
||||
import { confirmationFlagDefs } from "./confirm.ts";
|
||||
|
||||
export type { Command, AnyCommand, FlagDef, FlagsDef } from "bailian-cli-core";
|
||||
|
||||
@@ -40,10 +42,16 @@ const AUTH_LABELS = {
|
||||
none: { "en-US": "No Auth", "zh-CN": "无需鉴权" },
|
||||
} satisfies Record<AuthRequirement, LocalizedText>;
|
||||
|
||||
const RISK_LEVEL_LABELS = {
|
||||
high: { "en-US": "high", "zh-CN": "高风险" },
|
||||
} satisfies Record<CommandRiskLevel, LocalizedText>;
|
||||
|
||||
const HELP_TEXT = {
|
||||
usage: { "en-US": "Usage:", "zh-CN": "用法:" },
|
||||
commands: { "en-US": "Commands:", "zh-CN": "命令:" },
|
||||
authentication: { "en-US": "Authentication:", "zh-CN": "鉴权方式:" },
|
||||
risk: { "en-US": "Risk:", "zh-CN": "风险等级:" },
|
||||
riskMessage: { "en-US": "Risk message:", "zh-CN": "风险说明:" },
|
||||
flags: { "en-US": "Flags:", "zh-CN": "选项:" },
|
||||
globalFlags: { "en-US": "Global Flags:", "zh-CN": "全局选项:" },
|
||||
modelAuthFlags: { "en-US": "Model Auth Flags:", "zh-CN": "模型鉴权选项:" },
|
||||
@@ -63,6 +71,10 @@ const HELP_TEXT = {
|
||||
},
|
||||
notes: { "en-US": "Notes:", "zh-CN": "说明:" },
|
||||
examples: { "en-US": "Examples:", "zh-CN": "示例:" },
|
||||
confirmedExample: {
|
||||
"en-US": "# Only after explicit confirmation:",
|
||||
"zh-CN": "# 仅在明确确认后执行:",
|
||||
},
|
||||
minimalWorkflow: { "en-US": "Minimal workflow.yaml:", "zh-CN": "最小 workflow.yaml:" },
|
||||
tryIt: { "en-US": "Try it:", "zh-CN": "试一试:" },
|
||||
} satisfies Record<string, LocalizedText>;
|
||||
@@ -101,7 +113,11 @@ export class CommandRegistry {
|
||||
|
||||
private register(path: string, command: AnyCommand): void {
|
||||
// 同名守卫:命令自有 flag 不得与全局或其可见凭证域 flag 同名。
|
||||
const reserved = { ...GLOBAL_FLAGS, ...credentialFlagDefs(command) };
|
||||
const reserved = {
|
||||
...GLOBAL_FLAGS,
|
||||
...confirmationFlagDefs(command),
|
||||
...credentialFlagDefs(command),
|
||||
};
|
||||
for (const key of Object.keys(command.flags ?? {})) {
|
||||
if (key in reserved) {
|
||||
throw new Error(`Command "${path}" redeclares reserved flag "${key}".`);
|
||||
@@ -427,8 +443,15 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b(this.localize(HELP_TEXT.
|
||||
out.write(
|
||||
`${b(this.localize(HELP_TEXT.authentication))} ${a(this.localize(AUTH_LABELS[cmd.auth]))}\n`,
|
||||
);
|
||||
if (cmd.risk !== undefined) {
|
||||
out.write(
|
||||
`${b(this.localize(HELP_TEXT.risk))} ${a(this.localize(RISK_LEVEL_LABELS[cmd.risk.level]))}\n`,
|
||||
);
|
||||
out.write(`${b(this.localize(HELP_TEXT.riskMessage))} ${this.localize(cmd.risk.message)}\n`);
|
||||
}
|
||||
const flagEntries = [
|
||||
...Object.entries(cmd.flags ?? {}),
|
||||
...Object.entries(confirmationFlagDefs(cmd)),
|
||||
...Object.entries(credentialFlagDefs(cmd)),
|
||||
] as [string, FlagDef][];
|
||||
if (flagEntries.length > 0) {
|
||||
@@ -454,6 +477,9 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b(this.localize(HELP_TEXT.
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.examples))}\n`);
|
||||
for (const example of cmd.exampleArgs) {
|
||||
const localizedExample = this.localize(example);
|
||||
if (cmd.risk !== undefined && /(?:^|\s)--yes(?:\s|$)/.test(localizedExample)) {
|
||||
out.write(` ${d(this.localize(HELP_TEXT.confirmedExample))}\n`);
|
||||
}
|
||||
const line = localizedExample.startsWith("#")
|
||||
? localizedExample
|
||||
: localizedExample
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resolveModelBaseUrl,
|
||||
writeConfigFile,
|
||||
type CommandPackManager,
|
||||
type LocalizedText,
|
||||
type SourceFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { authStage, type RunContext } from "../src/middleware.ts";
|
||||
@@ -56,6 +57,8 @@ function makeContext(
|
||||
path,
|
||||
command,
|
||||
flags: {},
|
||||
confirmed: false,
|
||||
localize: (text: LocalizedText) => (typeof text === "string" ? text : text["en-US"]),
|
||||
settings,
|
||||
sources,
|
||||
configStore: makeConfigStore(sources.configName),
|
||||
|
||||
@@ -141,6 +141,7 @@ test("loads an API 1 Command Pack and preserves its command contract", async ()
|
||||
expect(Object.keys(commands)).toEqual([
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
@@ -151,9 +152,53 @@ test("loads an API 1 Command Pack and preserves its command contract", async ()
|
||||
"en-US": "Ping the Command Pack fixture",
|
||||
"zh-CN": "调用 Command Pack 测试命令",
|
||||
});
|
||||
expect(commands["agent ping"]?.risk).toBeUndefined();
|
||||
expect(commands["agent dangerous"]?.risk).toEqual({
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This fixture represents a high-risk operation.",
|
||||
"zh-CN": "该测试命令代表高风险操作。",
|
||||
},
|
||||
});
|
||||
expect(commands["agent ping"]?.flags?.message).toMatchObject({ required: true, type: "string" });
|
||||
});
|
||||
|
||||
test.each([
|
||||
["null", null],
|
||||
["a non-object value", "high"],
|
||||
["a missing level", { message: "Dangerous operation." }],
|
||||
["an unsupported level", { level: "low", message: "Dangerous operation." }],
|
||||
["an invalid message", { level: "high", message: "" }],
|
||||
])("rejects Command Pack risk metadata with %s", async (_caseName, risk) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "command-pack-risk-test-"));
|
||||
|
||||
try {
|
||||
await writeFile(
|
||||
join(root, "commands.mjs"),
|
||||
`export default {
|
||||
"agent dangerous": {
|
||||
description: "Dangerous command",
|
||||
auth: "none",
|
||||
risk: ${JSON.stringify(risk)},
|
||||
async run() {},
|
||||
},
|
||||
};\n`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
loadAndValidateCommandPack(
|
||||
"@ali/bailian-plugin-agent",
|
||||
packageJson,
|
||||
policy.supported["@ali/bailian-plugin-agent"]!,
|
||||
identity,
|
||||
root,
|
||||
),
|
||||
).rejects.toThrow(/invalid risk/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects incompatible protocol versions and invalid command prefixes", async () => {
|
||||
await expect(
|
||||
loadAndValidateCommandPack(
|
||||
|
||||
@@ -1,25 +1,89 @@
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { ExitCode } from "bailian-cli-core";
|
||||
import { confirmDangerousAction } from "../src/confirm.ts";
|
||||
import { describe, expect, test, vi } from "vite-plus/test";
|
||||
import { defineCommand, ExitCode, type CommandRisk, type LocalizedText } from "bailian-cli-core";
|
||||
import { ConfirmationRequiredError, confirmationFlagDefs } from "../src/confirm.ts";
|
||||
import { confirmationStage, type RunContext } from "../src/middleware.ts";
|
||||
|
||||
const originalIsTTY = process.stdin.isTTY;
|
||||
afterEach(() => {
|
||||
process.stdin.isTTY = originalIsTTY;
|
||||
});
|
||||
const HIGH_RISK_MESSAGE = {
|
||||
"en-US": "This permanently deletes the document and its chunks.",
|
||||
"zh-CN": "该操作会永久删除文档及其 Chunk,且无法撤销。",
|
||||
} satisfies LocalizedText;
|
||||
|
||||
describe("confirmDangerousAction", () => {
|
||||
test("--yes 时直接通过,不触碰 stdin", async () => {
|
||||
await expect(confirmDangerousAction("Delete kb idx-1", true)).resolves.toBeUndefined();
|
||||
function makeContext(options: {
|
||||
risk?: CommandRisk;
|
||||
confirmed?: boolean;
|
||||
dryRun?: boolean;
|
||||
}): RunContext {
|
||||
const command = defineCommand({
|
||||
description: "Delete a document",
|
||||
auth: "none",
|
||||
risk: options.risk,
|
||||
async run() {},
|
||||
});
|
||||
return {
|
||||
identity: {
|
||||
binName: "bl",
|
||||
version: "0.0.0-test",
|
||||
clientName: "bailian-cli-test",
|
||||
npmPackage: "bailian-cli",
|
||||
},
|
||||
path: ["knowledge", "doc", "delete"],
|
||||
command,
|
||||
flags: {},
|
||||
confirmed: options.confirmed ?? false,
|
||||
localize: (text: LocalizedText) => (typeof text === "string" ? text : text["zh-CN"]),
|
||||
settings: { dryRun: options.dryRun ?? false } as RunContext["settings"],
|
||||
} as unknown as RunContext;
|
||||
}
|
||||
|
||||
describe("confirmation metadata", () => {
|
||||
test("injects --yes only for high-risk commands", () => {
|
||||
expect(
|
||||
confirmationFlagDefs({ risk: { level: "high", message: HIGH_RISK_MESSAGE } }),
|
||||
).toHaveProperty("yes");
|
||||
expect(confirmationFlagDefs({})).toEqual({});
|
||||
});
|
||||
|
||||
test("非 TTY 且无 --yes 时抛 USAGE 并引导 --yes", async () => {
|
||||
process.stdin.isTTY = false;
|
||||
try {
|
||||
await confirmDangerousAction("Delete kb idx-1", false);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect((error as { exitCode: number }).exitCode).toBe(ExitCode.USAGE);
|
||||
expect((error as { hint?: string }).hint).toMatch(/--yes/);
|
||||
}
|
||||
test("serializes the stable Agent-readable confirmation contract", () => {
|
||||
const error = new ConfirmationRequiredError({
|
||||
message: HIGH_RISK_MESSAGE["zh-CN"],
|
||||
hint: "此命令将执行高风险操作。如确认继续,请在原命令中添加 --yes 后重新执行。",
|
||||
});
|
||||
|
||||
expect(error.exitCode).toBe(ExitCode.CONFIRMATION_REQUIRED);
|
||||
expect(error.toJSON()).toEqual({
|
||||
error: {
|
||||
code: 7,
|
||||
type: "requires_confirmation",
|
||||
message: HIGH_RISK_MESSAGE["zh-CN"],
|
||||
hint: "此命令将执行高风险操作。如确认继续,请在原命令中添加 --yes 后重新执行。",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirmationStage", () => {
|
||||
test("blocks high-risk execution without echoing the original command", async () => {
|
||||
const next = vi.fn(async () => {});
|
||||
|
||||
const promise = confirmationStage(
|
||||
makeContext({ risk: { level: "high", message: HIGH_RISK_MESSAGE } }),
|
||||
next,
|
||||
);
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
exitCode: 7,
|
||||
message: HIGH_RISK_MESSAGE["zh-CN"],
|
||||
hint: "此命令将执行高风险操作。如确认继续,请在原命令中添加 --yes 后重新执行。",
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["explicit --yes", { risk: { level: "high", message: HIGH_RISK_MESSAGE }, confirmed: true }],
|
||||
["dry-run", { risk: { level: "high", message: HIGH_RISK_MESSAGE }, dryRun: true }],
|
||||
["normal command", {}],
|
||||
] as const)("passes %s through", async (_label, options) => {
|
||||
const next = vi.fn(async () => {});
|
||||
await confirmationStage(makeContext(options), next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ExitCode } from "bailian-cli-core";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { handleError } from "../src/error-handler.ts";
|
||||
import { ConfirmationRequiredError } from "../src/confirm.ts";
|
||||
|
||||
test("handleError: fetch failed JSON includes cause.code from errno", () => {
|
||||
const previousOutput = process.env.DASHSCOPE_OUTPUT;
|
||||
@@ -8,7 +9,7 @@ test("handleError: fetch failed JSON includes cause.code from errno", () => {
|
||||
|
||||
let stderr = "";
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const originalExit = process.exit;
|
||||
const originalExit = process.exit.bind(process);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
@@ -53,7 +54,7 @@ test("handleError: fetch failed without nested cause still maps to NETWORK", ()
|
||||
|
||||
let stderr = "";
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const originalExit = process.exit;
|
||||
const originalExit = process.exit.bind(process);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
@@ -84,3 +85,42 @@ test("handleError: fetch failed without nested cause still maps to NETWORK", ()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("handleError: confirmation text uses the standard message and hint layout", () => {
|
||||
const previousOutput = process.env.DASHSCOPE_OUTPUT;
|
||||
delete process.env.DASHSCOPE_OUTPUT;
|
||||
|
||||
let stderr = "";
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const originalExit = process.exit.bind(process);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
process.exit = ((code?: number) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as typeof process.exit;
|
||||
|
||||
const confirmation = new ConfirmationRequiredError({
|
||||
message: "This permanently deletes the selected documents and all of their chunks.",
|
||||
hint: "This command performs a high-risk operation. To continue, add --yes to the original command and re-run it.",
|
||||
});
|
||||
|
||||
try {
|
||||
expect(() => handleError(confirmation, "bl")).toThrow(
|
||||
new RegExp(`process\\.exit:${ExitCode.CONFIRMATION_REQUIRED}`),
|
||||
);
|
||||
expect(stderr).toContain(
|
||||
"This command performs a high-risk operation. To continue, add --yes to the original command and re-run it.",
|
||||
);
|
||||
expect(stderr).not.toContain("bl knowledge doc delete");
|
||||
expect(stderr).not.toContain("Risk:");
|
||||
expect(stderr).not.toContain("Action:");
|
||||
expect(stderr).not.toContain("Note:");
|
||||
} finally {
|
||||
process.stderr.write = originalWrite;
|
||||
process.exit = originalExit;
|
||||
if (previousOutput === undefined) delete process.env.DASHSCOPE_OUTPUT;
|
||||
else process.env.DASHSCOPE_OUTPUT = previousOutput;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,6 +47,13 @@ test("registry renders runtime help copy with the selected language", async () =
|
||||
},
|
||||
],
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This operation is permanent.",
|
||||
"zh-CN": "该操作无法撤销。",
|
||||
},
|
||||
},
|
||||
run: async () => {},
|
||||
});
|
||||
const registry = new CommandRegistry({ test: command }, "bl", translator);
|
||||
@@ -69,6 +76,8 @@ test("registry renders runtime help copy with the selected language", async () =
|
||||
|
||||
output = "";
|
||||
registry.printHelp(["test"], stream);
|
||||
expect(output).toContain("风险等级: 高风险");
|
||||
expect(output).toContain("风险说明: 该操作无法撤销。");
|
||||
expect(output).toContain("测试说明");
|
||||
expect(output).toContain('bl test --message "你好"');
|
||||
expect(output).toContain(" # 流式输出响应");
|
||||
|
||||
@@ -33,3 +33,55 @@ test("命令重声明其可见域的凭证 flag → 抛错;不可见域的同名
|
||||
});
|
||||
expect(() => new CommandRegistry({ "x y": modelCmd }, "bl")).not.toThrow();
|
||||
});
|
||||
|
||||
test("high risk 命令不能自行声明 runtime 保留的 yes", () => {
|
||||
const high = defineCommand({
|
||||
description: "test",
|
||||
auth: "none",
|
||||
risk: { level: "high", message: "dangerous operation" },
|
||||
flags: { yes: { type: "switch", description: "duplicate" } },
|
||||
run: noopRun,
|
||||
});
|
||||
const normal = defineCommand({
|
||||
description: "test",
|
||||
auth: "none",
|
||||
flags: { yes: { type: "switch", description: "command-owned" } },
|
||||
run: noopRun,
|
||||
});
|
||||
|
||||
expect(() => new CommandRegistry({ "x high": high }, "bl")).toThrow(/yes/);
|
||||
expect(() => new CommandRegistry({ "x normal": normal }, "bl")).not.toThrow();
|
||||
});
|
||||
|
||||
test("命令 help 只为 high risk 展示风险信息和 runtime 注入的 --yes", () => {
|
||||
const high = defineCommand({
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
risk: { level: "high", message: "dangerous operation" },
|
||||
exampleArgs: ["--dry-run", "--yes"],
|
||||
run: noopRun,
|
||||
});
|
||||
const normal = defineCommand({
|
||||
description: "safe",
|
||||
auth: "none",
|
||||
run: noopRun,
|
||||
});
|
||||
const registry = new CommandRegistry({ "asset delete": high, "asset list": normal }, "bl");
|
||||
|
||||
let highHelp = "";
|
||||
let normalHelp = "";
|
||||
registry.printHelp(["asset", "delete"], {
|
||||
write: (chunk: string) => (highHelp += chunk),
|
||||
} as unknown as NodeJS.WriteStream);
|
||||
registry.printHelp(["asset", "list"], {
|
||||
write: (chunk: string) => (normalHelp += chunk),
|
||||
} as unknown as NodeJS.WriteStream);
|
||||
|
||||
expect(highHelp).toContain("--yes");
|
||||
expect(highHelp).toContain("Risk: high");
|
||||
expect(highHelp).toContain("Risk message: dangerous operation");
|
||||
expect(highHelp).toMatch(/# Only after explicit confirmation:\n\s+bl asset delete --yes/);
|
||||
expect(normalHelp).not.toContain("--yes");
|
||||
expect(normalHelp).not.toContain("Risk:");
|
||||
expect(normalHelp).not.toContain("Risk message:");
|
||||
});
|
||||
|
||||
+27
-26
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -56,30 +56,30 @@ Do not guess flags — use the reference files or `--help`.
|
||||
|
||||
Use this table only after the decision table in [`bailian-protocol`](../bailian-protocol/SKILL.md#provider-selection-and-consent) has routed the request to `bl` (class 4, or class 2 after the user picks Bailian). Hub-owned intents only — for media / fine-tune / agents.yaml, soft hand-off to the domain skill.
|
||||
|
||||
| User intent | Command | Notes |
|
||||
| ------------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| Explicit Bailian model chat / text execution | `bl text chat` | Default `qwen3.8-max` |
|
||||
| Bailian agent / workflow | `bl app call` | Needs `--app-id` |
|
||||
| Find app by name | `bl app list` then `bl app call` | Console auth |
|
||||
| Bailian app memory CRUD (not host-agent memory) | `bl memory *` | [`reference/memory.md`](reference/memory.md) |
|
||||
| Bailian knowledge base RAG | `bl knowledge search` / `chat` | API key + agent/workspace IDs |
|
||||
| Upload a file as a step of a Bailian workflow | `bl file upload` | When you need `oss://` URL explicitly; not for generic hosting |
|
||||
| Bailian model selection / recommendation | `bl advisor recommend` | Intent → candidate recall → LLM ranking |
|
||||
| Bailian model catalog / pricing / params | `bl model list` | Console auth; `--model <family>` for detail, `--enrich` for input params |
|
||||
| Install / list / update / remove registry skills | `bl skill add` / `list` / `update` / `remove` | Bailian skill registry; see [`reference/skill.md`](reference/skill.md) |
|
||||
| Bailian MCP marketplace discovery / call | `bl mcp list` / `tools` / `call` | — |
|
||||
| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions |
|
||||
| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Coding Plan quota usage | `bl usage coding-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Console API (advanced) | `bl console call` | Console auth |
|
||||
| Bailian workspace listing | `bl workspace list` | Console auth |
|
||||
| Switch CLI Help / Quick Start language | `bl config set --key language --value zh-CN` | Use `en-US` to switch back; follows the active config profile |
|
||||
| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` |
|
||||
| Dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` |
|
||||
| agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` need `--yes` after `plan` |
|
||||
| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` |
|
||||
| User intent | Command | Notes |
|
||||
| ------------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| Explicit Bailian model chat / text execution | `bl text chat` | Default `qwen3.8-max` |
|
||||
| Bailian agent / workflow | `bl app call` | Needs `--app-id` |
|
||||
| Find app by name | `bl app list` then `bl app call` | Console auth |
|
||||
| Bailian app memory CRUD (not host-agent memory) | `bl memory *` | [`reference/memory.md`](reference/memory.md) |
|
||||
| Bailian knowledge base RAG | `bl knowledge search` / `chat` | API key + agent/workspace IDs |
|
||||
| Upload a file as a step of a Bailian workflow | `bl file upload` | When you need `oss://` URL explicitly; not for generic hosting |
|
||||
| Bailian model selection / recommendation | `bl advisor recommend` | Intent → candidate recall → LLM ranking |
|
||||
| Bailian model catalog / pricing / params | `bl model list` | Console auth; `--model <family>` for detail, `--enrich` for input params |
|
||||
| Install / list / update / remove registry skills | `bl skill add` / `list` / `update` / `remove` | Bailian skill registry; see [`reference/skill.md`](reference/skill.md) |
|
||||
| Bailian MCP marketplace discovery / call | `bl mcp list` / `tools` / `call` | — |
|
||||
| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions |
|
||||
| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Coding Plan quota usage | `bl usage coding-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Console API (advanced) | `bl console call` | Console auth |
|
||||
| Bailian workspace listing | `bl workspace list` | Console auth |
|
||||
| Switch CLI Help / Quick Start language | `bl config set --key language --value zh-CN` | Use `en-US` to switch back; follows the active config profile |
|
||||
| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` |
|
||||
| Dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` |
|
||||
| agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` also require `plan` |
|
||||
| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` |
|
||||
|
||||
Flags, usage, and examples: see hub [`reference/`](reference/index.md) or `bl <command> --help` — do not guess flags. Domain command details live in the owning skill's `reference/`.
|
||||
|
||||
@@ -123,6 +123,7 @@ schema-export commands.
|
||||
- Usage / quota / credits questions that do not name a product → ask which product (Bailian or another AI service) first; run `bl usage` / `bl quota` only after the user picks Bailian or Bailian context is already established.
|
||||
- "Remember this" and memory requests default to the host agent's own memory; `bl memory *` is only for Bailian app memory resources.
|
||||
- `bl file upload` and `bl pipeline run` are steps inside a Bailian workflow; do not use them to capture generic "upload this file" or "run a pipeline" requests.
|
||||
- `bl managed-agent apply` / `destroy` mutate remote resources and only execute with `--yes`; run `plan` first and show the diff before confirming a mutation.
|
||||
- For `risk: high` commands or `requires_confirmation`, follow the shared protocol; never add `--yes` automatically.
|
||||
- `bl managed-agent apply` / `destroy` have an additional domain rule: run `plan` first and show the diff before asking for confirmation.
|
||||
- When a matched `bl` command accepts a file URL, pass local paths directly; never require the user to host the file first.
|
||||
- Console login → always `--console-site domestic|international`; see [`../bailian-protocol/assets/setup.md`](../bailian-protocol/assets/setup.md#console-site-selection).
|
||||
|
||||
@@ -81,9 +81,10 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl plugin list` | No Auth | List installed Command Packs and their load status | [plugin.md](plugin.md) |
|
||||
| `bl plugin remove` | No Auth | Remove an installed Command Pack | [plugin.md](plugin.md) |
|
||||
| `bl quota check` | Console | Check current usage against rate limits | [quota.md](quota.md) |
|
||||
| `bl quota delete` | API Key | Clear all custom rate limits (QPM/TPM) for a model | [quota.md](quota.md) |
|
||||
| `bl quota history` | Console | View quota change history | [quota.md](quota.md) |
|
||||
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) | [quota.md](quota.md) |
|
||||
| `bl quota update` | API Key | Update model rate limits (QPM/TPM), or clear them with --delete | [quota.md](quota.md) |
|
||||
| `bl quota update` | API Key | Update model rate limits (QPM/TPM) | [quota.md](quota.md) |
|
||||
| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
|
||||
| `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
|
||||
| `bl skill init` | No Auth | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
|
||||
@@ -122,7 +123,7 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `permission` | `grant`, `list`, `revoke` | [permission.md](permission.md) |
|
||||
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
|
||||
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
|
||||
| `quota` | `check`, `history`, `list`, `update` | [quota.md](quota.md) |
|
||||
| `quota` | `check`, `delete`, `history`, `list`, `update` | [quota.md](quota.md) |
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
|
||||
@@ -82,20 +82,24 @@ bl knowledge category add --name sub --parent-id cate-xxx
|
||||
|
||||
### `bl knowledge category delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| **Name** | `knowledge category delete` |
|
||||
| **Description** | Delete a data-center category |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge category delete --category-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge category delete` |
|
||||
| **Description** | Delete a data-center category |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge category delete --category-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This deletes the selected data-center category and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--category-id <id>` | string | yes | Category ID to delete |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -110,6 +114,7 @@ bl knowledge category delete --category-id cate-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge category delete --category-id cate-xxx --yes
|
||||
```
|
||||
|
||||
@@ -248,6 +253,10 @@ bl knowledge chunk add --index-id idx-xxx --field columnA=v1 --field columnB=v2
|
||||
| **Description** | Delete chunks from a knowledge base (irreversible) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the selected chunks and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -255,8 +264,8 @@ bl knowledge chunk add --index-id idx-xxx --field columnA=v1 --field columnB=v2
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | yes | Knowledge base ID |
|
||||
| `--chunk-id <id>` | array | yes | Chunk ID to delete (repeatable; batches of 10 are sent automatically) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -271,6 +280,7 @@ bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --yes
|
||||
```
|
||||
|
||||
@@ -461,20 +471,24 @@ bl knowledge create --name demo --description 'product docs' --category-id cate-
|
||||
|
||||
### `bl knowledge delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| **Name** | `knowledge delete` |
|
||||
| **Description** | Delete a knowledge base with all its documents and chunks |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge delete --index-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge delete` |
|
||||
| **Description** | Delete a knowledge base with all its documents and chunks |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge delete --index-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the knowledge base and all of its documents and chunks. Data-center files are not deleted. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | yes | Knowledge base ID |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -490,17 +504,22 @@ bl knowledge delete --index-id idx-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge delete --index-id idx-xxx --yes
|
||||
```
|
||||
|
||||
### `bl knowledge doc delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------- |
|
||||
| **Name** | `knowledge doc delete` |
|
||||
| **Description** | Delete documents and their chunks from a knowledge base |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge doc delete --index-id <id> --doc-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------ |
|
||||
| **Name** | `knowledge doc delete` |
|
||||
| **Description** | Delete documents and their chunks from a knowledge base |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge doc delete --index-id <id> --doc-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the selected documents and all of their chunks. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -508,8 +527,8 @@ bl knowledge delete --index-id idx-xxx --yes
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | yes | Knowledge base ID |
|
||||
| `--doc-id <id>` | array | yes | Document ID to delete (repeatable) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -523,10 +542,11 @@ bl knowledge delete --index-id idx-xxx --yes
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id file-a --doc-id file-b --yes
|
||||
```
|
||||
|
||||
@@ -728,20 +748,24 @@ bl knowledge doc upload --file ./docs/ --dry-run --verbose
|
||||
|
||||
### `bl knowledge file delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| **Name** | `knowledge file delete` |
|
||||
| **Description** | Permanently delete a file from the data center |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge file delete --file-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge file delete` |
|
||||
| **Description** | Permanently delete a file from the data center |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge file delete --file-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the data-center file. Knowledge-base document indexes that reference it may become invalid. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--file-id <id>` | string | yes | Data-center file ID to delete |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -757,6 +781,7 @@ bl knowledge file delete --file-id file-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge file delete --file-id file-xxx --yes
|
||||
```
|
||||
|
||||
@@ -1032,20 +1057,24 @@ bl knowledge service create --name my-search --scene search --index-id idx-xxx
|
||||
|
||||
### `bl knowledge service delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `knowledge service delete` |
|
||||
| **Description** | Delete a retrieval / Q&A service (soft delete, idempotent) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge service delete --agent-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge service delete` |
|
||||
| **Description** | Delete a retrieval / Q&A service (soft delete, idempotent) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge service delete --agent-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This deletes the service and makes its agent ID unavailable for search and chat calls. The operation cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | yes | Service (agent) ID |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -1062,17 +1091,22 @@ bl knowledge service delete --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge service delete --agent-id aid-xxx --yes
|
||||
```
|
||||
|
||||
### `bl knowledge service deploy`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| **Name** | `knowledge service deploy` |
|
||||
| **Description** | Publish the beta draft of a service as a new version |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge service deploy --agent-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `knowledge service deploy` |
|
||||
| **Description** | Publish the beta draft of a service as a new version |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge service deploy --agent-id <id> [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This publishes the current draft as a new version and changes the behavior seen by live callers. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -1080,8 +1114,8 @@ bl knowledge service delete --agent-id aid-xxx --yes
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | yes | Service (agent) ID |
|
||||
| `--version-desc <text>` | string | no | Description for the newly published version |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -1098,6 +1132,7 @@ bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl knowledge service deploy --agent-id aid-xxx --version-desc 'tuned rerank params' --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -109,12 +109,16 @@ bl permission list --output text
|
||||
|
||||
### `bl permission revoke`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------- |
|
||||
| **Name** | `permission revoke` |
|
||||
| **Description** | Revoke model permissions (inference / finetune / deploy) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl permission revoke --model <models> [--action <actions>] \| --all --yes` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `permission revoke` |
|
||||
| **Description** | Revoke model permissions (inference / finetune / deploy) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl permission revoke --model <models> [--action <actions>] \| --all [flags]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This revokes model permissions and may interrupt inference, fine-tuning, or deployment workloads. With --all, it also clears all historical inference grants. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -123,27 +127,31 @@ bl permission list --output text
|
||||
| `--model <models>` | string | no | Model ID(s), comma-separated (max 20) |
|
||||
| `--action <actions>` | string | no | Permission action(s), comma-separated: inference, finetune, deploy (default: inference) |
|
||||
| `--all` | switch | no | Close one-key authorization and clear ALL historical inference grants |
|
||||
| `--yes` | switch | no | Confirm --all without an interactive prompt (required) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Grants apply to the business workspace your API key belongs to.
|
||||
- All revoke operations require --yes; use --dry-run to preview the request without confirmation.
|
||||
- --all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.
|
||||
- Actions you omit keep their current grants (server-side tri-state patch).
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl permission revoke --model qwen-plus
|
||||
# Only after explicit user confirmation:
|
||||
bl permission revoke --model qwen-plus --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
bl permission revoke --model qwen-plus,qwen3-max --action inference,finetune
|
||||
# Only after explicit user confirmation:
|
||||
bl permission revoke --model qwen-plus,qwen3-max --action inference,finetune --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl permission revoke --all --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Authentication | Description |
|
||||
| ------------------ | -------------- | --------------------------------------------------------------- |
|
||||
| `bl quota check` | Console | Check current usage against rate limits |
|
||||
| `bl quota history` | Console | View quota change history |
|
||||
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) |
|
||||
| `bl quota update` | API Key | Update model rate limits (QPM/TPM), or clear them with --delete |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------ | -------------- | ------------------------------------------------------------- |
|
||||
| `bl quota check` | Console | Check current usage against rate limits |
|
||||
| `bl quota delete` | API Key | Clear all custom rate limits (QPM/TPM) for a model |
|
||||
| `bl quota history` | Console | View quota change history |
|
||||
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) |
|
||||
| `bl quota update` | API Key | Update model rate limits (QPM/TPM) |
|
||||
|
||||
## Command details
|
||||
|
||||
@@ -58,6 +59,43 @@ bl quota check --model qwen3.6-plus,qwen-turbo
|
||||
bl quota check --output json
|
||||
```
|
||||
|
||||
### `bl quota delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `quota delete` |
|
||||
| **Description** | Clear all custom rate limits (QPM/TPM) for a model |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota delete --model <model>` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently clears all custom QPM/TPM rate limits for the specified model and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------------- |
|
||||
| `--model <model>` | string | yes | Model name (required) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl quota delete --model qwen-plus
|
||||
```
|
||||
|
||||
```bash
|
||||
bl quota delete --model qwen-plus --dry-run --output json
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl quota delete --model qwen-plus --yes
|
||||
```
|
||||
|
||||
### `bl quota history`
|
||||
|
||||
| Field | Value |
|
||||
@@ -149,29 +187,26 @@ 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] [--yes]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| **Name** | `quota update` |
|
||||
| **Description** | Update model rate limits (QPM/TPM) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ------------------------------------------ |
|
||||
| `--model <model>` | string | yes | Model name (required) |
|
||||
| `--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 |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ----------------------------- |
|
||||
| `--model <model>` | string | yes | Model name (required) |
|
||||
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
|
||||
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
|
||||
| `--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.
|
||||
- Fields you omit keep their current values (server-side OVERLAY merge). Clear all custom limits with the "quota delete" command instead.
|
||||
- Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.
|
||||
|
||||
#### Examples
|
||||
@@ -184,14 +219,6 @@ bl quota update --model qwen-plus --rpm 60 --tpm 100000
|
||||
bl quota update --model qwen3-max --tpm 500000
|
||||
```
|
||||
|
||||
```bash
|
||||
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,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -33,7 +33,7 @@ description: >-
|
||||
- Unsure which training methods a base model supports → `bl finetune capability --base-model <base>` or `--training-type sft|sft-lora|dpo|cpt`.
|
||||
- Text `--training-type` values: `sft` / `sft-lora` / `dpo` / `dpo-lora` / `cpt`. Audio bases include `cosyvoice-v3-flash`; image bases include `wan2.7-image-pro`.
|
||||
- Deployment plans: audio defaults to `--plan mu`; text/image default to `lora`.
|
||||
- Preview write operations (create / delete / cancel / scale) with `--dry-run` first, and confirm with the user before deleting a job or dataset.
|
||||
- For `risk: high` or `requires_confirmation`, follow `bailian-protocol`; never add `--yes` automatically.
|
||||
|
||||
## When to use which command
|
||||
|
||||
|
||||
@@ -19,25 +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> [--yes]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------- |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id>` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the specified dataset file and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the dataset file is permanently removed.
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -50,6 +50,7 @@ bl dataset delete --file-id file-id-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl dataset delete --file-id file-id-xxx --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -73,12 +73,16 @@ 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] [--yes]` |
|
||||
| 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]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the specified model deployment and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,14 +90,10 @@ 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 |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the deployment is permanently destroyed.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -105,6 +105,7 @@ bl deploy delete --deployed-model dep-... --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl deploy delete --deployed-model dep-... --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -181,25 +181,28 @@ 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> [--yes]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id>` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This permanently deletes the specified fine-tune job record and cannot be undone. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--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
|
||||
@@ -213,6 +216,7 @@ bl finetune delete --job-id ft-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl finetune delete --job-id ft-xxx --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
阿里云百炼托管 Agent 声明式基础设施入口:用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、
|
||||
创建/更新/销毁百炼托管 Agent 或 Deployment、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
|
||||
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、
|
||||
apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。
|
||||
apply / destroy 变更远端资源且受统一高风险确认闸门保护,务必先 plan 给用户看 diff 再让其确认,禁止自动添加 `--yes`。
|
||||
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
|
||||
子代理不走本 skill;生图生视频走 bailian-gen。
|
||||
官方安装:`bl skill init`(与共享协议 bailian-protocol 同装)。
|
||||
@@ -16,15 +16,17 @@ description: >-
|
||||
|
||||
# Bailian managed agent IaC (`bl managed-agent`)
|
||||
|
||||
**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Version & updates (pre-flight checklist) and CLI errors: report an issue. Command details are authoritative in [`reference/managed-agent.md`](reference/managed-agent.md) and `bl managed-agent --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.**
|
||||
**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): High-risk operation confirmation, Version & updates (pre-flight checklist), and CLI errors: report an issue. Command details are authoritative in [`reference/managed-agent.md`](reference/managed-agent.md) and `bl managed-agent --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.**
|
||||
|
||||
## Safety guardrail (the most important rule)
|
||||
|
||||
`apply` / `destroy` **mutate remote resources** and only execute when `--yes` is passed:
|
||||
`apply` / `destroy` **mutate remote resources** and add a domain-specific preview requirement on top of the shared high-risk confirmation protocol:
|
||||
|
||||
1. Always run `bl managed-agent plan` first and show the diff to the user.
|
||||
2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`.
|
||||
3. Never add `--yes` on your own initiative before the user has confirmed.
|
||||
2. Ask the user to confirm the exact action and scope shown in the plan.
|
||||
3. Only then run `apply` / `destroy` with `--yes`; a changed plan requires confirmation again.
|
||||
|
||||
`state rm`, `session delete`, and future `risk: high` commands follow the shared protocol.
|
||||
|
||||
## IaC lifecycle
|
||||
|
||||
@@ -32,8 +34,9 @@ description: >-
|
||||
1. Init bl managed-agent init # scaffold agents.yaml
|
||||
2. Validate bl managed-agent validate # offline, no network calls
|
||||
3. Preview bl managed-agent plan # show the pending change diff
|
||||
4. Apply bl managed-agent apply --yes # only after user confirmation
|
||||
5. Destroy bl managed-agent destroy --yes # only after user confirmation
|
||||
4. Confirm show the plan and ask the user # no automatic --yes
|
||||
5. Apply bl managed-agent apply --yes # only after explicit confirmation
|
||||
6. Destroy bl managed-agent destroy --yes # separate explicit confirmation
|
||||
```
|
||||
|
||||
## Deployment as IaC
|
||||
|
||||
@@ -31,24 +31,28 @@ Index: [index.md](index.md)
|
||||
|
||||
### `bl managed-agent apply`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent apply` |
|
||||
| **Description** | Apply planned changes to create/update/delete agent resources |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--concurrency <n>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent apply` |
|
||||
| **Description** | Apply planned changes to create/update/delete agent resources |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--concurrency <n>]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This applies the current plan and may create, update, or delete remote managed Agent resources. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------- | ------ | -------- | -------------------------------------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider (default: all configured) |
|
||||
| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) |
|
||||
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
|
||||
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------- | ------ | -------- | ------------------------------------------------------------------ |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider (default: all configured) |
|
||||
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
|
||||
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
@@ -59,29 +63,35 @@ Index: [index.md](index.md)
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent apply --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent apply --provider bailian --yes
|
||||
```
|
||||
|
||||
### `bl managed-agent destroy`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent destroy` |
|
||||
| **Description** | Destroy all managed agent resources tracked in state |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent destroy [--file <path>] [--yes] [--cascade]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent destroy` |
|
||||
| **Description** | Destroy all managed agent resources tracked in state |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent destroy [--file <path>] [--cascade]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This deletes every managed Agent resource tracked in state; --cascade may also delete dependent resources. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------------------------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--yes` | switch | no | Confirm and destroy without an interactive prompt (required) |
|
||||
| `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -94,10 +104,12 @@ bl managed-agent apply --provider bailian --yes
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent destroy --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent destroy --yes --cascade
|
||||
```
|
||||
|
||||
@@ -225,6 +237,10 @@ bl managed-agent session create --agent assistant --title 'debug run'
|
||||
| **Description** | Delete a session |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent session delete --session-id <id> [--provider <name>] [--file <path>]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This deletes the specified remote managed Agent Session. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -233,6 +249,7 @@ bl managed-agent session create --agent assistant --title 'debug run'
|
||||
| `--session-id <id>` | string | yes | Session ID (required) |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--provider <name>` | string | no | Target provider |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
@@ -245,7 +262,8 @@ bl managed-agent session create --agent assistant --title 'debug run'
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent session delete --session-id sess_abc123
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent session delete --session-id sess_abc123 --yes
|
||||
```
|
||||
|
||||
### `bl managed-agent session events`
|
||||
@@ -540,12 +558,16 @@ bl managed-agent state list --file agents.yaml
|
||||
|
||||
### `bl managed-agent state rm`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent state rm` |
|
||||
| **Description** | Remove a resource from state without destroying it remotely |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent state rm --address <provider.type.name> [--file <path>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent state rm` |
|
||||
| **Description** | Remove a resource from state without destroying it remotely |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent state rm --address <provider.type.name> [--file <path>]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This removes the resource from local state without deleting it remotely, so this project will no longer track it. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -553,6 +575,7 @@ bl managed-agent state list --file agents.yaml
|
||||
| -------------------------------- | ------ | -------- | --------------------------------------- |
|
||||
| `--address <provider.type.name>` | string | yes | Resource state address (required) |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
|
||||
#### Notes
|
||||
|
||||
@@ -561,7 +584,8 @@ bl managed-agent state list --file agents.yaml
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent state rm --address bailian.agent.assistant
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent state rm --address bailian.agent.assistant --yes
|
||||
```
|
||||
|
||||
### `bl managed-agent state show`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -41,6 +41,15 @@ Ask templates for classes 2 and 3 (match the user's language):
|
||||
|
||||
After approval, treat Bailian as selected for the current task. Do not ask again for intermediate commands, polling, downloads, retries, or related follow-ups. Ask again only if the scope changes materially, such as a substantially larger cost or a destructive operation.
|
||||
|
||||
## High-risk operation confirmation (mandatory)
|
||||
|
||||
`risk: high` in a command reference or leaf `--help` marks a high-risk operation. For older CLI output without this field, treat `--yes` as the conservative fallback. Exit code **7** with `error.type: "requires_confirmation"` is an expected stop signal, not a CLI bug.
|
||||
|
||||
- Never add `--yes` automatically.
|
||||
- Show the risk message and a safe summary of the action, target, and scope without exposing credentials, then ask for explicit confirmation.
|
||||
- Only after confirmation, re-run the same operation with `--yes`. Any material change to the scope requires confirmation again.
|
||||
- If the user declines or does not answer, stop.
|
||||
|
||||
## Family routing & hand-offs
|
||||
|
||||
业务路由(**软 hand-off**:按 skill **名**路由;已安装则 Read 其 `SKILL.md`,未安装则用 `bl <cmd> --help`,或提示整包安装
|
||||
|
||||
@@ -15,7 +15,7 @@ When `bl` fails, the agent first helps the user fix the problem. If the failure
|
||||
function shouldOfferIssueReport(exitCode, apiCode, message, hint):
|
||||
|
||||
# Step 1: Unambiguous EXCLUDE by exit code
|
||||
if exitCode in [2 (USAGE), 3 (AUTH), 4 (QUOTA), 10 (CONTENT_FILTER)]:
|
||||
if exitCode in [2 (USAGE), 3 (AUTH), 4 (QUOTA), 7 (CONFIRMATION_REQUIRED), 10 (CONTENT_FILTER)]:
|
||||
return EXCLUDE # help user fix; never offer reporting
|
||||
|
||||
# Step 2: NETWORK / TIMEOUT — exclude if hint is actionable
|
||||
@@ -69,18 +69,19 @@ function matchesIncludeCriteria(exitCode, apiCode, message):
|
||||
|
||||
These are **user**, **environment**, or **service business** errors. Give fix hints; do not ask to file an issue.
|
||||
|
||||
| Category | Signal | Examples |
|
||||
| -------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| **Usage / args** | Exit code **2** (USAGE) | Missing flag, invalid path, unknown subcommand, local file not found |
|
||||
| **Auth** | Exit code **3** (AUTH) | No API key, invalid key, expired console token |
|
||||
| **Quota** | Exit code **4** (QUOTA) | Free tier exhausted, rate limit / quota messages |
|
||||
| **Content filter** | Exit code **10** (CONTENT_FILTER) | Content moderation blocked the request |
|
||||
| **Model not found** | Message or `api_code` | `ModelNotFound`, `invalid_request_error` naming a bad model, HTTP 404 for model |
|
||||
| **Invalid API params** | USAGE or service validation | `InvalidParameter`, `invalid_request_error` for bad `--size`, `--format`, etc. |
|
||||
| **Free quota query** | `bl usage free` business result | Quota used up — not a CLI defect |
|
||||
| **Obvious local env** | Hint is sufficient | `ENOENT` / `EACCES`, wrong file path, disk full |
|
||||
| **Network (self-service)** | Exit code **6** (NETWORK) + clear hint | DNS, proxy, TLS — user fixes `DASHSCOPE_BASE_URL`, proxy, or network |
|
||||
| **Timeout (self-service)** | Exit code **5** (TIMEOUT) + hint works | Increase `--timeout`, check `base_url` with `bl auth status` |
|
||||
| Category | Signal | Examples |
|
||||
| -------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| **Usage / args** | Exit code **2** (USAGE) | Missing flag, invalid path, unknown subcommand, local file not found |
|
||||
| **Auth** | Exit code **3** (AUTH) | No API key, invalid key, expired console token |
|
||||
| **Quota** | Exit code **4** (QUOTA) | Free tier exhausted, rate limit / quota messages |
|
||||
| **Confirmation required** | Exit code **7** + `requires_confirmation` | Expected high-risk control flow; ask the user, never auto-retry with `--yes` |
|
||||
| **Content filter** | Exit code **10** (CONTENT_FILTER) | Content moderation blocked the request |
|
||||
| **Model not found** | Message or `api_code` | `ModelNotFound`, `invalid_request_error` naming a bad model, HTTP 404 for model |
|
||||
| **Invalid API params** | USAGE or service validation | `InvalidParameter`, `invalid_request_error` for bad `--size`, `--format`, etc. |
|
||||
| **Free quota query** | `bl usage free` business result | Quota used up — not a CLI defect |
|
||||
| **Obvious local env** | Hint is sufficient | `ENOENT` / `EACCES`, wrong file path, disk full |
|
||||
| **Network (self-service)** | Exit code **6** (NETWORK) + clear hint | DNS, proxy, TLS — user fixes `DASHSCOPE_BASE_URL`, proxy, or network |
|
||||
| **Timeout (self-service)** | Exit code **5** (TIMEOUT) + hint works | Increase `--timeout`, check `base_url` with `bl auth status` |
|
||||
|
||||
**Rule:** If the authoritative source of the error is the **service response** or **user input**, treat it as non-reportable (same boundary as the CLI repo’s error-handling docs).
|
||||
|
||||
@@ -337,15 +338,16 @@ Do **not** block on `gh` — always provide a manual path.
|
||||
|
||||
## Exit codes (reference)
|
||||
|
||||
| Code | Name | Usually reportable? |
|
||||
| ---- | -------------- | ----------------------------------------------- |
|
||||
| 0 | SUCCESS | — |
|
||||
| 1 | GENERAL | Sometimes (if CLI bug, not service passthrough) |
|
||||
| 2 | USAGE | No |
|
||||
| 3 | AUTH | No |
|
||||
| 4 | QUOTA | No |
|
||||
| 5 | TIMEOUT | Rarely (after user fixes env) |
|
||||
| 6 | NETWORK | Rarely (after user fixes env) |
|
||||
| 10 | CONTENT_FILTER | No |
|
||||
| Code | Name | Usually reportable? |
|
||||
| ---- | --------------------- | ----------------------------------------------- |
|
||||
| 0 | SUCCESS | — |
|
||||
| 1 | GENERAL | Sometimes (if CLI bug, not service passthrough) |
|
||||
| 2 | USAGE | No |
|
||||
| 3 | AUTH | No |
|
||||
| 4 | QUOTA | No |
|
||||
| 5 | TIMEOUT | Rarely (after user fixes env) |
|
||||
| 6 | NETWORK | Rarely (after user fixes env) |
|
||||
| 7 | CONFIRMATION_REQUIRED | No — expected high-risk control flow |
|
||||
| 10 | CONTENT_FILTER | No |
|
||||
|
||||
JSON errors use the same numeric `error.code` field when `--output json` is set.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-web-search
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.19.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type LocalizedText,
|
||||
} from "../packages/core/src/index.ts";
|
||||
import { commands } from "../packages/cli/src/commands.ts";
|
||||
import { confirmationFlagDefs } from "../packages/runtime/src/confirm.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SKILLS_DIR = join(__dirname, "../skills");
|
||||
@@ -127,7 +128,11 @@ function formatFlagsTable(flags: FlagsDef | undefined): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function formatExamples(path: string, exampleArgs: LocalizedText[] | undefined): string {
|
||||
function formatExamples(
|
||||
path: string,
|
||||
exampleArgs: LocalizedText[] | undefined,
|
||||
highRisk: boolean,
|
||||
): string {
|
||||
if (!exampleArgs?.length) return "_No examples._\n";
|
||||
// Commands store argument-only examples; prepend `bl <path>` for the reference.
|
||||
return (
|
||||
@@ -135,7 +140,11 @@ function formatExamples(path: string, exampleArgs: LocalizedText[] | undefined):
|
||||
.map((example) => {
|
||||
const text = referenceText(example);
|
||||
const line = text.startsWith("#") ? text : `bl ${path}${text ? ` ${text}` : ""}`;
|
||||
return ["```bash", line, "```"].join("\n");
|
||||
const confirmationComment =
|
||||
highRisk && /(?:^|\s)--yes(?:\s|$)/.test(text)
|
||||
? ["# Only after explicit user confirmation:"]
|
||||
: [];
|
||||
return ["```bash", ...confirmationComment, line, "```"].join("\n");
|
||||
})
|
||||
.join("\n\n") + "\n"
|
||||
);
|
||||
@@ -156,11 +165,29 @@ function commandSection(path: string, cmd: AnyCommand): string {
|
||||
// Commands store argument-only usage; the `bl <path>` prefix is added here.
|
||||
const usage = `bl ${path}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}`;
|
||||
lines.push(`| **Usage** | \`${escCell(usage)}\` |`);
|
||||
if (cmd.risk !== undefined) {
|
||||
lines.push(`| **Risk** | \`${cmd.risk.level}\` |`);
|
||||
lines.push(`| **Risk message** | ${escCell(referenceText(cmd.risk.message))} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
if (cmd.risk !== undefined) {
|
||||
lines.push(
|
||||
'> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, ' +
|
||||
"stop and ask for explicit user confirmation of the same action and scope.",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// 与命令 help 的 Flags 区一致:自有 + 该命令可见的凭证域 flag。
|
||||
lines.push("#### Flags", "");
|
||||
lines.push(formatFlagsTable({ ...cmd.flags, ...credentialFlagDefs(cmd) }));
|
||||
lines.push(
|
||||
formatFlagsTable({
|
||||
...cmd.flags,
|
||||
...confirmationFlagDefs(cmd),
|
||||
...credentialFlagDefs(cmd),
|
||||
}),
|
||||
);
|
||||
|
||||
if (cmd.notes?.length) {
|
||||
lines.push("#### Notes", "");
|
||||
@@ -168,7 +195,7 @@ function commandSection(path: string, cmd: AnyCommand): string {
|
||||
}
|
||||
|
||||
lines.push("#### Examples", "");
|
||||
lines.push(formatExamples(path, cmd.exampleArgs));
|
||||
lines.push(formatExamples(path, cmd.exampleArgs, cmd.risk !== undefined));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user