mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b55b29ac | |||
| f953f428c5 | |||
| e03103364e | |||
| 64b1e72238 | |||
| fa3f5de728 | |||
| 02d859ee18 | |||
| 2220f932b5 | |||
| 5c390fad74 | |||
| 6218637f7c | |||
| fa1a8c7b84 | |||
| 0c7ba721a7 | |||
| 0dbf071368 | |||
| 16e30686f0 | |||
| 7803d91e3f | |||
| c2d17707c0 | |||
| 3e8df956fe | |||
| 1dfb4400cc | |||
| fd2077b373 | |||
| ceef514a50 | |||
| 0dde03c783 | |||
| f4e479ce80 | |||
| 2090293f85 | |||
| a245359792 | |||
| 19bd5a5ea8 | |||
| e262f2e574 | |||
| 180aac9f28 | |||
| 3477a08dad | |||
| aa1484264e | |||
| e1eadad184 | |||
| 2767491ce6 | |||
| efb832815a | |||
| 902516bbad | |||
| f44fb2e95f | |||
| c0fca869c2 | |||
| 332efb05b8 | |||
| c6b9931d6f | |||
| a92c58420b | |||
| bcf68a31f3 | |||
| 8e5dd127a9 | |||
| 4f41eaabc2 | |||
| e1634c40ed | |||
| 4c5688ad4a | |||
| 176961dc56 | |||
| 888031562f | |||
| d017954ae8 | |||
| 9d19558645 | |||
| a9200a3f85 | |||
| 83d7982ecb | |||
| d33cb71cc7 | |||
| 737f8db359 | |||
| a402026cce | |||
| da6e1313e5 | |||
| 2d2ec1c628 | |||
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| fd96fd664c | |||
| 719c0b68bb | |||
| 9e6f5ca9cb | |||
| 3f78de2dc1 | |||
| 38926aeb22 | |||
| e53daf05eb | |||
| 67ae182ec3 | |||
| e3f2996291 | |||
| 59250c4f44 | |||
| 676b6c2ece | |||
| 4f59ca5118 | |||
| 0872ff6a20 | |||
| 98ebeb398c | |||
| 5c96077d9f | |||
| 8a7674e7c3 | |||
| afb547e0c8 | |||
| 4fdd8389f8 | |||
| bd8b08b617 | |||
| 449eede237 | |||
| 8906108744 | |||
| 2fbb6efe67 | |||
| 47e9bcc634 | |||
| 00bcee36a6 | |||
| e9479e4084 | |||
| 4c7ef0c7a5 | |||
| 17ffde4ac2 | |||
| 9600f06205 | |||
| fc3ac36d00 | |||
| fe11227789 | |||
| 9469556671 | |||
| 804a43f8ed | |||
| d343dc3265 | |||
| 0d24f46f73 | |||
| cc209ce0b1 | |||
| 8360a9f584 | |||
| 90d1ef9dc5 | |||
| 124c425133 | |||
| 670c15532b | |||
| b93e0d586d | |||
| 69c1fb9f08 | |||
| 18fd2ed8e5 | |||
| 4c51c95f31 | |||
| f29f5fe632 |
@@ -15,6 +15,8 @@ git add \
|
||||
skills/bailian-finetune/reference \
|
||||
skills/bailian-managed-agent/SKILL.md \
|
||||
skills/bailian-managed-agent/reference \
|
||||
skills/bailian-sandbox/SKILL.md \
|
||||
skills/bailian-sandbox/reference \
|
||||
skills/bailian-web-search/SKILL.md
|
||||
|
||||
vp staged
|
||||
|
||||
@@ -36,7 +36,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
|
||||
packages/core/src/client/ # HTTP client / endpoints / console gateway
|
||||
```
|
||||
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-sandbox` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
|
||||
约定:
|
||||
|
||||
|
||||
+126
@@ -6,6 +6,132 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.25.0] - 2026-09-14
|
||||
|
||||
### Added
|
||||
|
||||
- **Token Plan harness quota** — `bl token-plan harness-quota` shows Token Plan harness entitlement quota usage (Console auth), joining the harness list with issued entitlements to display used/total quota, usage ratio, and reset time; harnesses with a pending entitlement are listed as issuing.
|
||||
- Filter the harness list with `--type official_tool|infrastructure`; render as a quota box or `--output json`.
|
||||
|
||||
## [1.24.0] - 2026-09-11
|
||||
|
||||
### Added
|
||||
|
||||
- Sandbox instance and template lifecycle commands, with automatic build polling and `--async` submission.
|
||||
- `bl sandbox official-images` and template `--image` presets for Code Interpreter, Browser, and All-in-One.
|
||||
- `bl sandbox file upload` uploads template mount files with `source=sandbox_template`; use the returned File ID in `mntConfig`.
|
||||
- Sandbox supports shared `--base-url`, environment and Profile configuration, with workspace-based endpoint fallback.
|
||||
- Dedicated `bailian-sandbox` Skill with command references and instance connection guidance.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Preserve submitted `templateID` and `buildID` when template-build polling fails, so users can check the existing build before resubmitting.
|
||||
|
||||
### Security
|
||||
|
||||
- Sandbox REST calls use Bailian Bearer authentication without an E2B SDK or E2B API key. Connection credentials and dry-run environment values are redacted by default.
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Profile-level watermark control** — configure `watermark` with `bl config set --key watermark --value true|false` to control the default watermark behavior for image generation and editing, video generation and editing, and reference-to-video commands.
|
||||
- **ASR accuracy controls** — `bl speech recognize` now supports instant hot words with `--vocabulary`, contextual word enhancement with `--context`, and reusable pre-built vocabularies with `--vocabulary-id` for supported ASR models.
|
||||
- **Speech vocabulary management** — added `bl speech vocabulary create|list|get|update|delete` to manage reusable pre-built hot-word vocabularies.
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
|
||||
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
|
||||
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
|
||||
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
|
||||
|
||||
### Internal
|
||||
|
||||
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
|
||||
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
|
||||
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
|
||||
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent API commands** — added direct list, get, search, version, upload, download, run, pause, archive, event, and diagnostic operations for Agents, Environments, Skills, Vaults, Deployments, Sessions, and Files.
|
||||
- **Scoped YAML-backed resource creation** — `agent create`, `environment create`, `skill create`, `vault create`, `vault credential create`, and `deployment create` update `agents.yaml` and apply only the target resource without unrelated drift blocking the operation.
|
||||
- **Agent Skill attachment** — Agent creation supports existing custom or official Skill IDs as well as local Skill directories and ZIP archives.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bailian-only Managed Agent CLI** — `bl managed-agent` now targets the Bailian provider exclusively; provider-selection flags were removed and configurations containing other providers are rejected.
|
||||
- **Runtime mutation confirmation** — Deployment run/pause/unpause, Session archive/delete, and File delete operations require explicit high-risk confirmation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Managed Agent error reporting** — Apply and scoped-create failures preserve the underlying provider diagnostic instead of ending with only `Apply failed.`.
|
||||
|
||||
### Security
|
||||
|
||||
- Credentials are resolved in memory and removed from the process environment; Vault credential declarations reference environment variables without persisting plaintext secrets.
|
||||
|
||||
## [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
|
||||
|
||||
- Removed API Key validation from `bl auth login`.
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### Added
|
||||
|
||||
- **Automatic Profile API Key fallback** — unsupported commands use the API Key and endpoint from `default` without requiring a Profile switch; explicit credentials still take priority.
|
||||
- **Independent speech defaults** — Profiles support separate default models for TTS and ASR.
|
||||
- **Wan3.0 file-to-video** — `bl video generate --file` accepts local files or URLs and validates incompatible inputs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Wan3.0 is now the default video model** — regular API Key text, image, and reference video tasks default to `wan3.0-video`, with first/last-frame and reference-audio support. Token Plan defaults remain unchanged.
|
||||
- **Expanded Token Plan preset** — login adds speech defaults and missing capabilities while preserving existing user configuration.
|
||||
- **Improved speech and Skill installation experience** — streaming synthesis defaults to PCM; installation docs add a Node 18-compatible fallback and clarify the Node.js and Git prerequisites.
|
||||
|
||||
## [1.17.1] - 2026-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
+126
@@ -6,6 +6,132 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.25.0] - 2026-09-14
|
||||
|
||||
### 新增
|
||||
|
||||
- **Token Plan harness 权益额度** —— `bl token-plan harness-quota` 查看 Token Plan harness 权益额度用量(Console 认证),联合 harness 列表与已发放权益,展示已用/总额度、使用比例和重置时间;待发放权益的 harness 以「发放中」状态列出。
|
||||
- 通过 `--type official_tool|infrastructure` 筛选 harness 列表;支持额度框输出或 `--output json`。
|
||||
|
||||
## [1.24.0] - 2026-09-11
|
||||
|
||||
### 新增
|
||||
|
||||
- Sandbox 实例与模版生命周期命令,支持自动轮询构建状态和 `--async` 异步提交。
|
||||
- `bl sandbox official-images` 与模版 `--image` 参数,内置代码解释器、浏览器和全能型镜像预设。
|
||||
- `bl sandbox file upload` 使用 `source=sandbox_template` 上传模版挂载文件,返回的 File ID 可用于 `mntConfig`。
|
||||
- Sandbox 复用 `--base-url`、环境变量和 Profile 配置,未配置时通过工作空间拼接接入地址。
|
||||
- 独立的 `bailian-sandbox` Skill,提供命令参考和实例连接指南。
|
||||
|
||||
### 修复
|
||||
|
||||
- 模版构建轮询失败时保留已提交的 `templateID` 和 `buildID`,便于先查询已有构建,避免重复提交。
|
||||
|
||||
### 安全
|
||||
|
||||
- Sandbox REST 调用使用百炼 Bearer 鉴权,不依赖 E2B SDK 或 E2B API Key;连接凭据和 dry-run 环境变量默认脱敏。
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### 新增
|
||||
|
||||
- **Profile 级水印控制** —— 可通过 `bl config set --key watermark --value true|false` 设置图片生成与编辑、视频生成与编辑以及参考生视频命令的默认水印行为。
|
||||
- **ASR 准确率增强** —— `bl speech recognize` 现支持通过 `--vocabulary` 传入即时热词、通过 `--context` 增强上下文词表,以及通过 `--vocabulary-id` 使用适用于对应 ASR 模型的预编译热词表。
|
||||
- **语音热词表管理** —— 新增 `bl speech vocabulary create|list|get|update|delete`,用于管理可复用的预编译热词表。
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### 变更
|
||||
|
||||
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
|
||||
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。
|
||||
|
||||
### 修复
|
||||
|
||||
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
|
||||
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
|
||||
|
||||
### 内部
|
||||
|
||||
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
|
||||
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
|
||||
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent API 命令** —— 新增 Agent、Environment、Skill、Vault、Deployment、Session 和 File 的列表、详情、搜索、版本、上传、下载、运行、暂停、归档、事件及诊断等操作。
|
||||
- **基于 YAML 的单资源创建** —— `agent create`、`environment create`、`skill create`、`vault create`、`vault credential create` 和 `deployment create` 会更新 `agents.yaml`,并且只 Apply 目标资源,不受无关资源 Drift 阻塞。
|
||||
- **Agent Skill 挂载** —— 创建 Agent 时支持引用已有的自定义或官方 Skill ID,也支持本地 Skill 目录和 ZIP 文件。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Managed Agent CLI 限定为百炼 Provider** —— `bl managed-agent` 现在只面向百炼,移除 Provider 选择参数,并拒绝包含其他 Provider 的配置。
|
||||
- **运行时变更增加确认** —— Deployment 运行/暂停/恢复、Session 归档/删除以及 File 删除操作需要显式进行高风险确认。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Managed Agent 错误输出** —— Apply 和单资源创建失败时会保留底层 Provider 的具体诊断,不再只显示 `Apply failed.`。
|
||||
|
||||
### 安全
|
||||
|
||||
- 凭证仅在内存中解析并从进程环境清除;Vault Credential 声明通过环境变量引用 Secret,不会持久化明文。
|
||||
|
||||
## [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
|
||||
|
||||
### 已移除
|
||||
|
||||
- 移除 `bl auth login` 的 API Key 校验。
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### 新增
|
||||
|
||||
- **Profile API Key 自动回退** —— 当前 Profile 不支持某条命令时,自动使用 `default` 中的 API Key 和接入地址,无需手动切换 Profile;显式凭证仍然优先。
|
||||
- **语音合成与识别支持独立默认模型** —— Profile 支持分别配置 TTS 和 ASR 默认模型。
|
||||
- **Wan3.0 文件生视频** —— `bl video generate --file` 支持本地文件或 URL,并校验互斥输入。
|
||||
|
||||
### 变更
|
||||
|
||||
- **视频默认模型升级至 Wan3.0** —— 普通 API Key 的文生、图生和参考生视频默认使用 `wan3.0-video`,并支持首尾帧与参考音频;Token Plan 默认模型保持不变。
|
||||
- **扩展 Token Plan 预设** —— 登录后自动补充语音默认模型和缺失能力,同时保留用户已有配置。
|
||||
- **优化语音与 Skill 安装体验** —— 流式语音合成默认输出 PCM;Skill 安装文档补充兼容 Node 18 的降级方式及 Node.js、Git 前置条件。
|
||||
|
||||
## [1.17.1] - 2026-08-22
|
||||
|
||||
### 修复
|
||||
|
||||
+48
-16
@@ -13,22 +13,25 @@ npm install -g bailian-cli
|
||||
```
|
||||
|
||||
4. 校验:`bl --version`。
|
||||
5. 按下方「安装 skills」安装。
|
||||
|
||||
安装 skills(CLI 内置,无需 Git / npx skills):
|
||||
### 安装 skills
|
||||
|
||||
**Supported(优先):** 用 CLI 内置命令一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`):
|
||||
|
||||
```bash
|
||||
bl skill init
|
||||
```
|
||||
|
||||
**Supported:** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
|
||||
|
||||
**Advanced / 按需子集:**
|
||||
**降级:** 若 `bl skill init` 失败,先确认本机同时满足两个条件:`node -v` / `npm -v` 成功,**且** `git --version` 成功。两个条件都成立时,再回退到:
|
||||
|
||||
```bash
|
||||
bl skill add --name bailian-protocol,bailian-gen
|
||||
npx skills@1.5.18 add modelstudioai/cli --all -g
|
||||
```
|
||||
|
||||
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
|
||||
> 必须使用 `skills@1.5.18`:这是最后兼容 Node ≥18 的版本(与 Bailian CLI 的 Node ≥18.17.0 一致);
|
||||
|
||||
安装成功后,按第 5 节向用户说明语言切换与能力 tip(若也装了 skills,一并简要说明)。
|
||||
|
||||
## 2. 备选:二进制安装(无需 Node)
|
||||
|
||||
@@ -67,8 +70,6 @@ bl --version
|
||||
which bl # Windows: where.exe bl
|
||||
```
|
||||
|
||||
若自动 skill 安装失败,再手动执行:`bl skill init`。
|
||||
|
||||
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。
|
||||
|
||||
---
|
||||
@@ -99,12 +100,43 @@ which bl # Windows: where.exe bl
|
||||
bl auth status --output json
|
||||
```
|
||||
|
||||
## 5. 常见问题
|
||||
## 5. 安装成功后向用户说明(必做)
|
||||
|
||||
| 现象 | 可能原因 | 建议动作 |
|
||||
| ------------------------ | ---------------------------- | ------------------------------------------------ |
|
||||
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin` 或 `npm prefix -g` |
|
||||
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
|
||||
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
|
||||
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
|
||||
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
|
||||
安装与校验完成后,**主动**用中文向用户说明下列两项;不要等用户追问。可执行 `bl`(已登录时)对照终端 tip,但以下列表为准,勿臆造。
|
||||
|
||||
### 5.1 界面语言
|
||||
|
||||
Bailian CLI 支持中英文界面(help / 欢迎语 / Quick Start 主次文案)。默认 `en-US`,可切换为 `zh-CN`:
|
||||
|
||||
```bash
|
||||
bl config set --key language --value zh-CN
|
||||
bl config set --key language --value en-US
|
||||
```
|
||||
|
||||
向用户说明:可随时用上述命令切换语言。
|
||||
|
||||
### 5.2 能力 tip(Quick Start)
|
||||
|
||||
向用户展示「试试使用Bailian CLI完成这些任务」及下列 5 条(中英各一行,与 `bl` 根帮助 tip 一致):
|
||||
|
||||
1. 帮我创建一个能够生成短片分镜和视频的 Managed Agent。
|
||||
Help me create a Managed Agent that can generate short-film storyboards and videos.
|
||||
2. 生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。
|
||||
Generate an image of a cat in a spacesuit standing on Mars, then turn it into a video.
|
||||
3. 查看最近的模型用量、免费额度和限流情况。
|
||||
Check my recent model usage, free quota, and rate limits.
|
||||
4. 推荐一个适合图片理解和智能客服的模型。
|
||||
Recommend a model suitable for image understanding and intelligent customer service.
|
||||
5. 介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。
|
||||
Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
| 现象 | 可能原因 | 建议动作 |
|
||||
| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin` 或 `npm prefix -g` |
|
||||
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
|
||||
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
|
||||
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
|
||||
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
|
||||
| `bl skill init` 失败 | 网络 / registry 不可达等 | 有 Node 且有 Git 时降级 `npx skills@1.5.18 add modelstudioai/cli --all -g`;否则重试或告知用户 |
|
||||
|
||||
@@ -115,6 +115,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
|
||||
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
|
||||
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
|
||||
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
|
||||
| Model selection | "Recommend a model for image understanding and customer support." |
|
||||
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
|
||||
|
||||
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
|
||||
|
||||
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
|
||||
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
|
||||
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
|
||||
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
|
||||
- `bl auth logout --console` 只清 `access_token`
|
||||
@@ -44,8 +44,12 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
- `resolveApiKey()` — `auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
|
||||
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
|
||||
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
|
||||
- `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
|
||||
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
|
||||
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
|
||||
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
|
||||
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile;未传
|
||||
`--config` 时继续写当前激活项,失败和 dry-run 不切换
|
||||
- `resolveConsole()` — `auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
|
||||
- `resolveOpenApi()` — `auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
|
||||
@@ -58,17 +62,19 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
`bl managed-agent *` 按调用链分两层:
|
||||
|
||||
- **离线命令** — `init`、`validate`、`state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
|
||||
- **联网命令** — `plan`、`apply`、`destroy`、`state import`、`skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁,authStage 都经 `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run` 传 `credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
|
||||
- **联网命令** — `plan`、`apply`、`destroy`、`state import`、`skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁,authStage 经 `resolveApiKey(sources)` 解析 Bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层再断言 Bailian key 非空。例外:`plan --no-refresh` / `plan --dry-run` 传 `credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
|
||||
|
||||
`bl managed-agent` 是 Bailian-only 产品入口:命令不暴露 `--provider`,`init` 只生成 `providers.bailian`,所有远端调用固定传 `provider: "bailian"`。`resolveAgentProjectConfig` 在创建 SDK runtime 前通过 `assertBailianOnlyProviders` 拒绝包含非 Bailian Provider 的手写配置;共享 `@openagentpack/sdk` 仍可保留多 Provider 能力。
|
||||
|
||||
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道五步:
|
||||
|
||||
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
|
||||
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
|
||||
1. `prepareProviderEnv()` — 调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`,含兼容别名)中仍为 undefined 的项占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
|
||||
2. `resolveProjectConfig` — 完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`,避免空 key 在 SDK zod 层提前报 "received null"
|
||||
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
|
||||
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
|
||||
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
|
||||
5. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider;随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint;离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
|
||||
|
||||
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
|
||||
禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help;根 help、鉴权域等代表性子进程冒烟 |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version`、`search --help` 等代表性子进程冒烟 |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
|
||||
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
|
||||
@@ -37,10 +38,12 @@
|
||||
|
||||
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
|
||||
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
|
||||
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
|
||||
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
|
||||
|
||||
### 共享
|
||||
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
|
||||
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
@@ -48,9 +51,12 @@
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
test("<subcommand> --help 正常退出", async () => {
|
||||
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
@@ -74,11 +80,18 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 用例类型
|
||||
|
||||
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
|
||||
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
|
||||
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
|
||||
高风险命令额外要求:
|
||||
|
||||
- `--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 重复
|
||||
@@ -94,8 +107,8 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 安全与例外
|
||||
|
||||
- **禁止破坏真实用户配置**:`auth logout` 默认只用 `--dry-run`;需要验证实际落盘时,必须通过
|
||||
`BAILIAN_CONFIG_DIR` 指向隔离 fixture;`config set` 只用 `--dry-run`
|
||||
- **禁止破坏真实用户配置**:`auth logout` 和 `config set` 默认只用 `--dry-run`;只有验证持久化契约时,才允许通过
|
||||
`BAILIAN_CONFIG_DIR` 指向每个用例独占的临时目录实际落盘,并必须在 `finally` 中清理;禁止写入或复用真实 `~/.bailian`
|
||||
- **不加 dry-run**:`dryRun` 在 `resolveFileUrl` / `resolveCredential` / 上传**之后**的命令(如 `image edit`、`speech recognize` 带 `--url`)
|
||||
- **`--list-voices` 等旁路**:先于 `--text` 校验的 flag,缺参用例勿带该 flag
|
||||
- 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言
|
||||
@@ -105,7 +118,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
|
||||
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
|
||||
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
|
||||
|
||||
@@ -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`;纯命令语法示例可保留为字符串,服务端错误不翻译
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
|
||||
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
|
||||
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
|
||||
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。
|
||||
|
||||
### B. 鉴权 / 全局选项
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
|
||||
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
|
||||
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`。
|
||||
- [ ] API Key capability ID 由 host 按实际叶子命令路径生成(例如 `agent credential` → `agent.credential`);Command Pack 不声明额外元数据,Profile allowlist 对 pack 命令同样 fail closed。
|
||||
- [ ] `ctx.credentials.apiKey()` 必须返回 `authStage` 已注入 `ctx.client` 的实际凭证,不能重新读取原 Profile 绕过 capability fallback。
|
||||
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore` 或 `configStore`。
|
||||
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager,避免 pack 安装或删除其他 pack。
|
||||
- [ ] 单包失败必须 fail-open:保留内置命令和其他合法 pack。
|
||||
@@ -40,6 +42,7 @@
|
||||
## 测试与文档
|
||||
|
||||
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
|
||||
- [ ] Command Pack 测试覆盖 capability 格式/鉴权域、adapter 保留字段,以及 raw API Key 委托使用 fallback 后的实际 Base URL。
|
||||
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
|
||||
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
|
||||
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
|
||||
|
||||
@@ -29,6 +29,7 @@ config 文件 ─┘
|
||||
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
|
||||
- `parseConfigFile()` 解析新字段
|
||||
- 如果是 enum 字段,加校验
|
||||
- 如果是数组字段,明确“缺失 / 空数组 / 非法值”的不同语义;安全策略字段的非法值必须 fail closed
|
||||
|
||||
### B. 加载逻辑
|
||||
|
||||
@@ -51,6 +52,7 @@ config 文件 ─┘
|
||||
- [ ] 配置展示 / 修改命令同步:
|
||||
- `packages/commands/src/commands/config/show.ts` 显示新字段
|
||||
- `packages/commands/src/commands/config/set.ts` 的 `VALID_KEYS` / `KEY_ALIASES` / description 允许 set
|
||||
- `packages/commands/src/commands/config/ui.ts` / `ui-html.ts` 能按原类型往返数组字段,不能把 `[]` 保存成字段缺失
|
||||
|
||||
### E. 文档
|
||||
|
||||
@@ -61,6 +63,7 @@ config 文件 ─┘
|
||||
- [ ] 单测覆盖优先级:flag > env > file
|
||||
- [ ] 校验失败抛错(非法值)
|
||||
- [ ] 默认值正确
|
||||
- [ ] 数组配置覆盖 CLI 逗号/JSON 输入、Config UI 往返、去重和显式空数组
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -82,3 +85,4 @@ cat ~/.bailian/config.json
|
||||
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
|
||||
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
|
||||
- ✗ `config show` 不显示新字段,用户改了无法回查
|
||||
- ✗ UI 用 `String([])` 把显式空数组渲染为空串,保存后意外关闭安全策略
|
||||
|
||||
@@ -18,15 +18,19 @@
|
||||
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
|
||||
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
|
||||
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
|
||||
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
|
||||
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
|
||||
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
|
||||
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
|
||||
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
|
||||
- 对命中内置 preset 的 Profile,API Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
|
||||
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
|
||||
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
|
||||
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
|
||||
|
||||
## 3. 保持读写命令交互一致
|
||||
|
||||
- `auth login`、`config set` 等写命令未传 `--config` 时修改当前激活项。
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
|
||||
创建和激活;`config set --config <name>` 可创建但不自动激活。
|
||||
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
|
||||
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
|
||||
@@ -55,6 +59,8 @@
|
||||
|
||||
- 旧配置无 `active_config` -> `default`。
|
||||
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
|
||||
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url` 和 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
|
||||
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`)按文件值生效。
|
||||
- 显式命名 `--config` 和 `--config default` 均覆盖激活项且不修改磁盘状态。
|
||||
- 激活不存在的 Profile 失败且不写盘。
|
||||
- 悬空 `active_config` 明确失败。
|
||||
@@ -63,6 +69,9 @@
|
||||
- 显式 `auth login --config <name>` 成功后激活该 Profile,失败或 dry-run 不创建、不切换;
|
||||
`--config default` 成功后切回 `default`。
|
||||
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
|
||||
- Console/OpenAPI/none 命令不参与 API Key capability fallback;fallback 后的 Client 特殊端点行为必须跟随最终解析的 `base_url`,不能根据原 `settings.configName` 推断端点类型。
|
||||
- Fallback 反馈只描述 CLI 能权威确认的本地行为:当前 Profile 不支持空格分隔的用户可见叶子命令,本次将从 `default` 读取 API Key 配置;不得声称整个 Profile 已切换,也不得展示 capability ID 或 `<undeclared>` 等内部值。存在 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式覆盖时必须跳过 fallback 且不输出反馈。
|
||||
- Fallback 反馈写 stderr,`--quiet` 抑制;text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象。dry-run 和后续鉴权失败仍保留反馈;JSON 模式下多个 diagnostics 以空行分隔,任何模式都不得输出凭证值。
|
||||
- `config list/show/use/ui`、`auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
|
||||
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
|
||||
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。
|
||||
|
||||
@@ -46,6 +46,7 @@ runtime/error-handler.ts:
|
||||
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
|
||||
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
|
||||
- Node fs errno → 翻成 GENERAL + errno hint
|
||||
- `--output json` → stderr 输出两空格缩进的多行 error JSON;前面已有结构化 warning 时,两个 diagnostics 以空行分隔
|
||||
- 其它 Error → 默认走 cause 链
|
||||
↓
|
||||
process.exit(err.exitCode)
|
||||
|
||||
@@ -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」指向句
|
||||
@@ -54,7 +56,9 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
### C. 归属与生成
|
||||
|
||||
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
|
||||
- [ ] 新增领域目录时:补齐 `SKILL.md`(含 `metadata.version`),同步 `packages/cli/package.json` 的 reference 格式化路径、`.vite-hooks/pre-commit` 的生成物暂存清单、`tools/release/check.mjs` 的生成物校验清单;同步 hub 与共享协议的领域路由
|
||||
- [ ] 跑 `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. 三套鉴权与埋点标识
|
||||
@@ -29,7 +30,7 @@
|
||||
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
|
||||
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 无 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
|
||||
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 仅对阿里云 host 注入
|
||||
- DashScope 上传策略 `getPolicy` 只有 `x-dashscope-source-config`,没有显式 CLI `User-Agent`
|
||||
- Console Gateway 用户数据由后端直接区分,不依赖 `x-dashscope-source-config`;AEM 的 `authMethod=console` 是独立的命令侧统计
|
||||
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product`、`action`、`api` 是鉴权或路由字段,不计为埋点标识
|
||||
|
||||
### 2. 后端渠道参数
|
||||
@@ -77,7 +79,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 +94,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 +110,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 +146,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 | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
|
||||
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
|
||||
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
|
||||
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
|
||||
| Model selection | "Recommend a model for image understanding and customer support." |
|
||||
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
|
||||
|
||||
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
@@ -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.17.1",
|
||||
"version": "1.25.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
@@ -41,7 +41,7 @@
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference'",
|
||||
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference skills/bailian-sandbox/reference'",
|
||||
"sync:skill-version": "tsx ../../tools/sync-skill-metadata.ts",
|
||||
"build": "vp pack",
|
||||
"dev": "tsx src/main.ts",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@ import {
|
||||
searchWeb,
|
||||
speechSynthesize,
|
||||
speechRecognize,
|
||||
speechVocabularyCreate,
|
||||
speechVocabularyList,
|
||||
speechVocabularyGet,
|
||||
speechVocabularyUpdate,
|
||||
speechVocabularyDelete,
|
||||
fileUpload,
|
||||
consoleCall,
|
||||
usageFree,
|
||||
@@ -85,6 +90,7 @@ import {
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaUpdate,
|
||||
quotaDelete,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
@@ -124,6 +130,7 @@ import {
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
tokenPlanHarnessQuota,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -139,6 +146,18 @@ import {
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
managedAgentPlayground,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectWorkbench,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
managedAgentStateRm,
|
||||
@@ -151,6 +170,63 @@ import {
|
||||
managedAgentSessionSend,
|
||||
managedAgentSessionEvents,
|
||||
managedAgentSkillList,
|
||||
managedAgentAgentCreate,
|
||||
managedAgentEnvironmentCreate,
|
||||
managedAgentSkillCreate,
|
||||
managedAgentVaultCreate,
|
||||
managedAgentVaultCredentialCreate,
|
||||
managedAgentDeploymentCreate,
|
||||
managedAgentAgentList,
|
||||
managedAgentAgentGet,
|
||||
managedAgentAgentSearch,
|
||||
managedAgentAgentVersions,
|
||||
managedAgentEnvironmentList,
|
||||
managedAgentEnvironmentGet,
|
||||
managedAgentEnvironmentSearch,
|
||||
managedAgentSkillGet,
|
||||
managedAgentSkillSearch,
|
||||
managedAgentSkillVersions,
|
||||
managedAgentSkillDownload,
|
||||
managedAgentVaultList,
|
||||
managedAgentVaultGet,
|
||||
managedAgentVaultSearch,
|
||||
managedAgentDeploymentList,
|
||||
managedAgentDeploymentGet,
|
||||
managedAgentDeploymentSearch,
|
||||
managedAgentDeploymentRunsList,
|
||||
managedAgentDeploymentRunsGet,
|
||||
managedAgentDeploymentRun,
|
||||
managedAgentDeploymentPause,
|
||||
managedAgentDeploymentUnpause,
|
||||
managedAgentSessionSearch,
|
||||
managedAgentSessionUpdate,
|
||||
managedAgentSessionArchive,
|
||||
managedAgentSessionEventList,
|
||||
managedAgentSessionEventSend,
|
||||
managedAgentSessionEventStream,
|
||||
managedAgentSessionDebug,
|
||||
managedAgentSessionExport,
|
||||
managedAgentFileUpload,
|
||||
managedAgentFileList,
|
||||
managedAgentFileGet,
|
||||
managedAgentFileSearch,
|
||||
managedAgentFileDownload,
|
||||
managedAgentFileDelete,
|
||||
sandboxCreate,
|
||||
sandboxOfficialImages,
|
||||
sandboxFileUpload,
|
||||
sandboxList,
|
||||
sandboxGet,
|
||||
sandboxConnect,
|
||||
sandboxPause,
|
||||
sandboxResume,
|
||||
sandboxDelete,
|
||||
sandboxTemplateCreate,
|
||||
sandboxTemplateList,
|
||||
sandboxTemplateGet,
|
||||
sandboxTemplateUpdate,
|
||||
sandboxTemplateBuildStatus,
|
||||
sandboxTemplateDelete,
|
||||
} from "bailian-cli-commands";
|
||||
|
||||
// Full bailian-cli product: every command, exposed under the `bl` binary.
|
||||
@@ -231,6 +307,11 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"search web": searchWeb,
|
||||
"speech synthesize": speechSynthesize,
|
||||
"speech recognize": speechRecognize,
|
||||
"speech vocabulary create": speechVocabularyCreate,
|
||||
"speech vocabulary list": speechVocabularyList,
|
||||
"speech vocabulary get": speechVocabularyGet,
|
||||
"speech vocabulary update": speechVocabularyUpdate,
|
||||
"speech vocabulary delete": speechVocabularyDelete,
|
||||
"file upload": fileUpload,
|
||||
"console call": consoleCall,
|
||||
"usage free": usageFree,
|
||||
@@ -246,6 +327,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,
|
||||
@@ -285,6 +367,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
"token-plan add-member": tokenPlanAddMember,
|
||||
"token-plan harness-quota": tokenPlanHarnessQuota,
|
||||
"workspace init": workspaceInit,
|
||||
"plugin install": pluginInstall,
|
||||
"plugin link": pluginLink,
|
||||
@@ -300,6 +383,18 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent plan": managedAgentPlan,
|
||||
"managed-agent apply": managedAgentApply,
|
||||
"managed-agent destroy": managedAgentDestroy,
|
||||
"managed-agent playground": managedAgentPlayground,
|
||||
"managed-agent project init": managedAgentProjectInit,
|
||||
"managed-agent project validate": managedAgentProjectValidate,
|
||||
"managed-agent project build": managedAgentProjectBuild,
|
||||
"managed-agent project publish": managedAgentProjectPublish,
|
||||
"managed-agent project workbench": managedAgentProjectWorkbench,
|
||||
"managed-agent project version enable": managedAgentProjectVersionEnable,
|
||||
"managed-agent project version disable": managedAgentProjectVersionDisable,
|
||||
"managed-agent project version status": managedAgentProjectVersionStatus,
|
||||
"managed-agent project version list": managedAgentProjectVersionList,
|
||||
"managed-agent project version preview": managedAgentProjectVersionPreview,
|
||||
"managed-agent project version restore": managedAgentProjectVersionRestore,
|
||||
"managed-agent state list": managedAgentStateList,
|
||||
"managed-agent state show": managedAgentStateShow,
|
||||
"managed-agent state rm": managedAgentStateRm,
|
||||
@@ -312,6 +407,64 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent session send": managedAgentSessionSend,
|
||||
"managed-agent session events": managedAgentSessionEvents,
|
||||
"managed-agent skill-list": managedAgentSkillList,
|
||||
"managed-agent agent create": managedAgentAgentCreate,
|
||||
"managed-agent agent list": managedAgentAgentList,
|
||||
"managed-agent agent get": managedAgentAgentGet,
|
||||
"managed-agent agent search": managedAgentAgentSearch,
|
||||
"managed-agent agent versions": managedAgentAgentVersions,
|
||||
"managed-agent environment create": managedAgentEnvironmentCreate,
|
||||
"managed-agent environment list": managedAgentEnvironmentList,
|
||||
"managed-agent environment get": managedAgentEnvironmentGet,
|
||||
"managed-agent environment search": managedAgentEnvironmentSearch,
|
||||
"managed-agent skill create": managedAgentSkillCreate,
|
||||
"managed-agent skill list": managedAgentSkillList,
|
||||
"managed-agent skill get": managedAgentSkillGet,
|
||||
"managed-agent skill search": managedAgentSkillSearch,
|
||||
"managed-agent skill versions": managedAgentSkillVersions,
|
||||
"managed-agent skill download": managedAgentSkillDownload,
|
||||
"managed-agent vault create": managedAgentVaultCreate,
|
||||
"managed-agent vault credential create": managedAgentVaultCredentialCreate,
|
||||
"managed-agent vault list": managedAgentVaultList,
|
||||
"managed-agent vault get": managedAgentVaultGet,
|
||||
"managed-agent vault search": managedAgentVaultSearch,
|
||||
"managed-agent deployment create": managedAgentDeploymentCreate,
|
||||
"managed-agent deployment list": managedAgentDeploymentList,
|
||||
"managed-agent deployment get": managedAgentDeploymentGet,
|
||||
"managed-agent deployment search": managedAgentDeploymentSearch,
|
||||
"managed-agent deployment runs list": managedAgentDeploymentRunsList,
|
||||
"managed-agent deployment runs get": managedAgentDeploymentRunsGet,
|
||||
"managed-agent deployment run": managedAgentDeploymentRun,
|
||||
"managed-agent deployment pause": managedAgentDeploymentPause,
|
||||
"managed-agent deployment unpause": managedAgentDeploymentUnpause,
|
||||
"managed-agent session search": managedAgentSessionSearch,
|
||||
"managed-agent session update": managedAgentSessionUpdate,
|
||||
"managed-agent session archive": managedAgentSessionArchive,
|
||||
"managed-agent session event list": managedAgentSessionEventList,
|
||||
"managed-agent session event send": managedAgentSessionEventSend,
|
||||
"managed-agent session event stream": managedAgentSessionEventStream,
|
||||
"managed-agent session debug": managedAgentSessionDebug,
|
||||
"managed-agent session export": managedAgentSessionExport,
|
||||
"managed-agent file upload": managedAgentFileUpload,
|
||||
"managed-agent file list": managedAgentFileList,
|
||||
"managed-agent file get": managedAgentFileGet,
|
||||
"managed-agent file search": managedAgentFileSearch,
|
||||
"managed-agent file download": managedAgentFileDownload,
|
||||
"managed-agent file delete": managedAgentFileDelete,
|
||||
"sandbox create": sandboxCreate,
|
||||
"sandbox official-images": sandboxOfficialImages,
|
||||
"sandbox file upload": sandboxFileUpload,
|
||||
"sandbox list": sandboxList,
|
||||
"sandbox get": sandboxGet,
|
||||
"sandbox connect": sandboxConnect,
|
||||
"sandbox pause": sandboxPause,
|
||||
"sandbox resume": sandboxResume,
|
||||
"sandbox delete": sandboxDelete,
|
||||
"sandbox template create": sandboxTemplateCreate,
|
||||
"sandbox template list": sandboxTemplateList,
|
||||
"sandbox template get": sandboxTemplateGet,
|
||||
"sandbox template update": sandboxTemplateUpdate,
|
||||
"sandbox template build-status": sandboxTemplateBuildStatus,
|
||||
"sandbox template delete": sandboxTemplateDelete,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -9,7 +9,19 @@ const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures",
|
||||
let configDir: string;
|
||||
|
||||
function env(): NodeJS.ProcessEnv {
|
||||
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
|
||||
return {
|
||||
BAILIAN_CONFIG_DIR: configDir,
|
||||
DO_NOT_TRACK: "1",
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
};
|
||||
}
|
||||
|
||||
function parseStderrJsonDiagnostics(stderr: string): unknown[] {
|
||||
return stderr
|
||||
.trim()
|
||||
.split(/\n\s*\n/)
|
||||
.map((diagnostic) => JSON.parse(diagnostic) as unknown);
|
||||
}
|
||||
|
||||
describe("e2e: Command Pack", () => {
|
||||
@@ -53,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",
|
||||
@@ -71,6 +84,68 @@ describe("e2e: Command Pack", () => {
|
||||
expect(executed.exitCode, executed.stderr).toBe(0);
|
||||
expect(executed.stdout).toContain("command-pack:hello");
|
||||
|
||||
await writeFile(
|
||||
join(configDir, "config.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
base_url: "https://default.example.com",
|
||||
active_config: "company-plan",
|
||||
"company-plan": {
|
||||
api_key: "sk-plan",
|
||||
base_url: "https://plan.example.com",
|
||||
api_key_capabilities: [],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
const fallbackFailure = await runCli(["agent", "credential", "--output", "json"], env());
|
||||
expect(fallbackFailure.exitCode).toBe(3);
|
||||
expect(fallbackFailure.stderr).toMatch(/^\{\n {2}"warning": \{/);
|
||||
expect(fallbackFailure.stderr).toContain("\n\n{\n");
|
||||
expect(parseStderrJsonDiagnostics(fallbackFailure.stderr)).toEqual([
|
||||
{
|
||||
warning: {
|
||||
code: "PROFILE_API_KEY_FALLBACK",
|
||||
message:
|
||||
'Profile "company-plan" does not support command "agent credential"; API Key settings will be read from Profile "default" for this run.',
|
||||
profile: "company-plan",
|
||||
command_path: ["agent", "credential"],
|
||||
fallback_profile: "default",
|
||||
credential_fields: ["api_key", "base_url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
error: expect.objectContaining({
|
||||
code: 3,
|
||||
message: "No API key found.",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
await writeFile(
|
||||
join(configDir, "config.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
api_key: "sk-default",
|
||||
base_url: "https://default.example.com",
|
||||
active_config: "company-plan",
|
||||
"company-plan": {
|
||||
api_key: "sk-plan",
|
||||
base_url: "https://plan.example.com",
|
||||
api_key_capabilities: [],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
const fallbackCredential = await runCli(["agent", "credential"], env());
|
||||
expect(fallbackCredential.exitCode, fallbackCredential.stderr).toBe(0);
|
||||
expect(fallbackCredential.stderr).toContain('command "agent credential"');
|
||||
expect(fallbackCredential.stdout).toContain("credential-base-url:https://default.example.com");
|
||||
|
||||
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
|
||||
expect(credential.exitCode, credential.stderr).toBe(0);
|
||||
expect(credential.stdout).toContain("credential-source:flag");
|
||||
@@ -96,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);
|
||||
@@ -109,6 +212,7 @@ describe("e2e: Command Pack", () => {
|
||||
commands: [
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runCli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "bl");
|
||||
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
|
||||
|
||||
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
|
||||
|
||||
function runCliSmoke(args: string[]) {
|
||||
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
|
||||
}
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Global Flags:/);
|
||||
expect(stderr).toMatch(/--console-region <region>/);
|
||||
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: bl ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+24
-1
@@ -19,12 +19,34 @@ 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",
|
||||
async run(ctx) {
|
||||
const apiKey = ctx.credentials.apiKey();
|
||||
process.stdout.write(`credential-source:${apiKey.source}\n`);
|
||||
process.stdout.write(
|
||||
`credential-source:${apiKey.source} credential-base-url:${apiKey.baseUrl}\n`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,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,69 @@
|
||||
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");
|
||||
const scopedCreateCommands = new Set([
|
||||
"bl managed-agent agent create",
|
||||
"bl managed-agent deployment create",
|
||||
"bl managed-agent environment create",
|
||||
"bl managed-agent skill create",
|
||||
"bl managed-agent vault create",
|
||||
"bl managed-agent vault credential create",
|
||||
]);
|
||||
|
||||
test("generated references distinguish runtime high-risk confirmation from scoped create execution", () => {
|
||||
let highRiskCommandCount = 0;
|
||||
const seenScopedCreateCommands = new Set<string>();
|
||||
|
||||
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) {
|
||||
const commandName = commandSection.match(/^### `([^`]+)`/m)?.[1];
|
||||
const hasConfirmationFlag = commandSection.includes("`--yes`");
|
||||
const hasHighRiskMetadata = /\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/.test(commandSection);
|
||||
|
||||
if (!hasHighRiskMetadata) {
|
||||
if (!hasConfirmationFlag) continue;
|
||||
expect(commandName).toBeDefined();
|
||||
expect(scopedCreateCommands.has(commandName ?? "")).toBe(true);
|
||||
expect(commandSection).toMatch(/Without --yes, .*preview/i);
|
||||
seenScopedCreateCommands.add(commandName ?? "");
|
||||
continue;
|
||||
}
|
||||
|
||||
highRiskCommandCount += 1;
|
||||
expect(hasConfirmationFlag).toBe(true);
|
||||
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);
|
||||
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { validateSkillDir } from "bailian-cli-core";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { parse } from "yaml";
|
||||
import { commands } from "../src/commands.ts";
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const sandboxSkillDirectory = join(repositoryRoot, "skills/bailian-sandbox");
|
||||
|
||||
test("Sandbox is a valid standalone skill aligned with the CLI version", () => {
|
||||
const metadata = validateSkillDir(sandboxSkillDirectory, "bailian-sandbox");
|
||||
expect(metadata.name).toBe("bailian-sandbox");
|
||||
|
||||
const skillMarkdown = readFileSync(join(sandboxSkillDirectory, "SKILL.md"), "utf8");
|
||||
const frontmatter = parse(skillMarkdown.split(/^---\s*$/m)[1]) as {
|
||||
metadata: { version: string };
|
||||
};
|
||||
const cliPackage = JSON.parse(
|
||||
readFileSync(join(repositoryRoot, "packages/cli/package.json"), "utf8"),
|
||||
) as { version: string };
|
||||
expect(frontmatter.metadata.version).toBe(cliPackage.version);
|
||||
|
||||
for (const link of skillMarkdown.matchAll(/\]\(([^)]+)\)/g)) {
|
||||
expect(existsSync(join(sandboxSkillDirectory, link[1]))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Sandbox reference contains every registered Sandbox command only in its owning skill", () => {
|
||||
const expectedCommands = Object.keys(commands)
|
||||
.filter((commandPath) => commandPath.startsWith("sandbox "))
|
||||
.map((commandPath) => `bl ${commandPath}`)
|
||||
.sort();
|
||||
expect(expectedCommands.length).toBeGreaterThan(0);
|
||||
|
||||
const reference = readFileSync(join(sandboxSkillDirectory, "reference/sandbox.md"), "utf8");
|
||||
const documentedCommands = [...reference.matchAll(/^### `([^`]+)`/gm)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
expect(documentedCommands).toEqual(expectedCommands);
|
||||
|
||||
const index = readFileSync(join(sandboxSkillDirectory, "reference/index.md"), "utf8");
|
||||
for (const commandPath of expectedCommands) {
|
||||
expect(index).toContain(`\`${commandPath}\``);
|
||||
}
|
||||
expect(existsSync(join(repositoryRoot, "skills/bailian-cli/reference/sandbox.md"))).toBe(false);
|
||||
const hubIndex = readFileSync(
|
||||
join(repositoryRoot, "skills/bailian-cli/reference/index.md"),
|
||||
"utf8",
|
||||
);
|
||||
expect(hubIndex).not.toContain("`bl sandbox ");
|
||||
});
|
||||
@@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) {
|
||||
"video",
|
||||
"generate",
|
||||
"--model",
|
||||
"happyhorse-1.1-t2v",
|
||||
"wan3.0-video",
|
||||
"--prompt",
|
||||
"压测前置短视频:海浪与静态远景,无明显人物。",
|
||||
"--duration",
|
||||
|
||||
@@ -130,7 +130,7 @@ export async function generateCombinedFixtures({ suiteRoot, cliPackage }) {
|
||||
"video",
|
||||
"generate",
|
||||
"--model",
|
||||
"happyhorse-1.1-t2v",
|
||||
"wan3.0-video",
|
||||
"--prompt",
|
||||
"压测前置短视频:海浪与静态远景,无明显人物。",
|
||||
"--duration",
|
||||
|
||||
@@ -16,7 +16,7 @@ const motions = [
|
||||
|
||||
export const runStress = defineStressTarget({
|
||||
canonical: "video-i2v",
|
||||
defaultModel: "happyhorse-1.1-i2v",
|
||||
defaultModel: "wan3.0-video",
|
||||
batchDirPrefix: "video-i2v-batch",
|
||||
helpText: "pnpm run test:stress -- video-i2v [--reuse-fixtures] -- --count 5 -c 2",
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const prompts = [
|
||||
|
||||
export const runStress = defineStressTarget({
|
||||
canonical: "video-ref",
|
||||
defaultModel: "happyhorse-1.1-r2v",
|
||||
defaultModel: "wan3.0-video",
|
||||
batchDirPrefix: "video-ref-batch",
|
||||
helpText: "pnpm run test:stress -- video-ref [--reuse-fixtures] -- --count 5 -c 2",
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
export const runStress = defineStressTarget({
|
||||
canonical: "video-t2v",
|
||||
defaultModel: "happyhorse-1.1-t2v",
|
||||
defaultModel: "wan3.0-video",
|
||||
batchDirPrefix: "video-t2v-batch",
|
||||
helpText: `用法:pnpm run test:stress -- video-t2v -- --concurrency 1 --count 3
|
||||
详见 docs/agents/stress-batch-tests.md`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.17.1",
|
||||
"version": "1.25.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": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openagentpack/sdk": "0.3.2",
|
||||
"@openagentpack/sdk": "0.7.1",
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"bailian-cli-runtime": "workspace:*",
|
||||
"boxen": "catalog:",
|
||||
|
||||
@@ -1,91 +1,35 @@
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
chatPath,
|
||||
requestJson,
|
||||
normalizeModelBaseUrl,
|
||||
type AuthPersistPatch,
|
||||
type AuthStore,
|
||||
type Identity,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
|
||||
|
||||
interface ApiKeyLoginDeps {
|
||||
identity: Identity;
|
||||
settings: Settings;
|
||||
authStore: AuthStore;
|
||||
}
|
||||
|
||||
interface ApiKeyLoginProfile {
|
||||
baseUrl: string;
|
||||
persistBaseUrl?: string;
|
||||
defaultTextModel?: string;
|
||||
defaultVideoModel?: string;
|
||||
defaultImageToVideoModel?: string;
|
||||
defaultReferenceToVideoModel?: string;
|
||||
defaultImageModel?: string;
|
||||
defaultSpeechModel?: string;
|
||||
defaultSpeechRecognitionModel?: string;
|
||||
apiKeyCapabilities?: readonly string[];
|
||||
persistPatch?: AuthPersistPatch;
|
||||
}
|
||||
|
||||
const RETRY_DELAY_BASE_MS = 500;
|
||||
|
||||
function canRetry(error: unknown): boolean {
|
||||
if (error instanceof BailianError) {
|
||||
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
|
||||
const status = error.api?.httpStatus;
|
||||
return status === 401 || (status !== undefined && status >= 500);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return (
|
||||
error.name === "AbortError" ||
|
||||
error.name === "TimeoutError" ||
|
||||
error.message.includes("timed out") ||
|
||||
error.message === "fetch failed"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function validateAndPersistApiKey(
|
||||
/**
|
||||
* Persist an API key (and optional profile defaults) without a live model probe.
|
||||
* Login is credential storage; connectivity is verified on the first API command.
|
||||
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
|
||||
*/
|
||||
export async function persistApiKey(
|
||||
deps: ApiKeyLoginDeps,
|
||||
key: string,
|
||||
profile: ApiKeyLoginProfile,
|
||||
): Promise<void> {
|
||||
process.stderr.write("Testing key... ");
|
||||
const httpDeps = { identity: deps.identity, settings: deps.settings };
|
||||
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
|
||||
const persistBaseUrl = profile.persistBaseUrl
|
||||
? normalizeModelBaseUrl(profile.persistBaseUrl)
|
||||
: undefined;
|
||||
const validationModel = "qwen3.8-max";
|
||||
const requestOpts = {
|
||||
url: baseUrl + chatPath(),
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
timeout: Math.min(deps.settings.timeout, 30),
|
||||
body: {
|
||||
model: validationModel,
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
max_tokens: 1,
|
||||
stream: false,
|
||||
},
|
||||
};
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
await requestJson<unknown>(httpDeps, requestOpts);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt >= 3 || !canRetry(error)) {
|
||||
process.stderr.write("Failed\n");
|
||||
throw error;
|
||||
}
|
||||
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
process.stderr.write("Valid\n");
|
||||
await deps.authStore.login({
|
||||
...profile.persistPatch,
|
||||
api_key: key,
|
||||
@@ -95,5 +39,8 @@ export async function validateAndPersistApiKey(
|
||||
default_image_to_video_model: profile.defaultImageToVideoModel,
|
||||
default_reference_to_video_model: profile.defaultReferenceToVideoModel,
|
||||
default_image_model: profile.defaultImageModel,
|
||||
default_speech_model: profile.defaultSpeechModel,
|
||||
default_speech_recognition_model: profile.defaultSpeechRecognitionModel,
|
||||
api_key_capabilities: profile.apiKeyCapabilities ? [...profile.apiKeyCapabilities] : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
|
||||
import { validateAndPersistApiKey } from "./login-api-key.ts";
|
||||
import { persistApiKey } from "./login-api-key.ts";
|
||||
|
||||
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
|
||||
export interface LoginDeps {
|
||||
@@ -413,9 +413,7 @@ export async function runConsoleLogin(
|
||||
workspace_id: workspaceId || undefined,
|
||||
};
|
||||
if (apiKey) {
|
||||
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
|
||||
await validateAndPersistApiKey(deps, apiKey, {
|
||||
baseUrl: testBaseUrl,
|
||||
await persistApiKey(deps, apiKey, {
|
||||
persistBaseUrl: baseUrl || undefined,
|
||||
persistPatch: callbackPatch,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
normalizeModelBaseUrl,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
import { validateAndPersistApiKey } from "./login-api-key.ts";
|
||||
import { persistApiKey } from "./login-api-key.ts";
|
||||
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
|
||||
|
||||
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
|
||||
@@ -33,8 +33,8 @@ export default defineCommand({
|
||||
type: "string",
|
||||
valueHint: "<url>",
|
||||
description: {
|
||||
"en-US": "Model API base URL (used with --api-key for validation)",
|
||||
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
|
||||
"en-US": "Model API base URL to store with --api-key",
|
||||
"zh-CN": "与 --api-key 一并保存的模型 API Base URL",
|
||||
},
|
||||
},
|
||||
console: {
|
||||
@@ -164,21 +164,27 @@ export default defineCommand({
|
||||
if (!key) return;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitBare("Would validate and save API key.");
|
||||
emitBare("Would save API key.");
|
||||
return;
|
||||
}
|
||||
const profilePreset = getModelProfilePreset(settings.configName);
|
||||
const storedBaseUrl = store.stored().baseUrl;
|
||||
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
|
||||
const stored = store.stored();
|
||||
const storedBaseUrl = stored.baseUrl;
|
||||
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
|
||||
await validateAndPersistApiKey(deps, key, {
|
||||
baseUrl: resolvedBaseUrl,
|
||||
const apiKeyCapabilities = profilePreset
|
||||
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
|
||||
: stored.apiKeyCapabilities;
|
||||
await persistApiKey(deps, key, {
|
||||
persistBaseUrl,
|
||||
defaultTextModel: profilePreset?.defaultTextModel,
|
||||
defaultVideoModel: profilePreset?.defaultVideoModel,
|
||||
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,
|
||||
defaultReferenceToVideoModel: profilePreset?.defaultReferenceToVideoModel,
|
||||
defaultImageModel: profilePreset?.defaultImageModel,
|
||||
defaultSpeechModel: profilePreset?.defaultSpeechModel,
|
||||
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
|
||||
apiKeyCapabilities,
|
||||
});
|
||||
process.stderr.write(`API key saved to ${store.path}\n`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,9 +12,9 @@ export default defineCommand({
|
||||
valueHint: "<key>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Config key (language, base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
|
||||
"Config key (language, base_url, output, output_dir, timeout, watermark, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
|
||||
"zh-CN":
|
||||
"配置项名称(language、base_url、output、output_dir、timeout、api_key、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
|
||||
"配置项名称(language、base_url、output、output_dir、timeout、watermark、api_key、api_key_capabilities、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
@@ -29,7 +29,9 @@ export default defineCommand({
|
||||
"--key language --value zh-CN",
|
||||
"--key output --value json",
|
||||
"--key timeout --value 600",
|
||||
"--key watermark --value false",
|
||||
"--key base_url --value https://dashscope.aliyuncs.com",
|
||||
"--config company-plan --key api-key-capabilities --value text.chat,image.generate",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
isApiKeyCapability,
|
||||
normalizeModelBaseUrl,
|
||||
parseBooleanValue,
|
||||
SUPPORTED_LANGUAGES,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
@@ -12,6 +14,7 @@ export const VALID_KEYS = [
|
||||
"output",
|
||||
"output_dir",
|
||||
"timeout",
|
||||
"watermark",
|
||||
"api_key",
|
||||
"access_token",
|
||||
"access_key_id",
|
||||
@@ -23,7 +26,9 @@ export const VALID_KEYS = [
|
||||
"default_reference_to_video_model",
|
||||
"default_image_model",
|
||||
"default_speech_model",
|
||||
"default_speech_recognition_model",
|
||||
"default_omni_model",
|
||||
"api_key_capabilities",
|
||||
"workspace_id",
|
||||
] as const;
|
||||
|
||||
@@ -59,18 +64,22 @@ export const UI_ENUM_KEYS: Record<string, string[]> = {
|
||||
};
|
||||
|
||||
// Keys the UI renders as a true/false dropdown and stores as a boolean.
|
||||
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry"]);
|
||||
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry", "watermark"]);
|
||||
|
||||
// Default model each `default_*_model` key falls back to when left unset. These
|
||||
// mirror the inline `|| "<model>"` fallbacks in the generation commands
|
||||
// (text/chat, image/generate, video/generate, speech/synthesize, omni/chat) and
|
||||
// (text/chat, image/generate, video/generate, speech/synthesize,
|
||||
// speech/recognize, omni/chat) and
|
||||
// are surfaced as input placeholders so users can see the effective default
|
||||
// without persisting a value that would pin the model.
|
||||
export const UI_MODEL_DEFAULTS: Record<string, string> = {
|
||||
default_text_model: "qwen3.8-max",
|
||||
default_image_model: "qwen-image-3.0",
|
||||
default_video_model: "happyhorse-1.1-t2v",
|
||||
default_video_model: "wan3.0-video",
|
||||
default_image_to_video_model: "wan3.0-video",
|
||||
default_reference_to_video_model: "wan3.0-video",
|
||||
default_speech_model: "cosyvoice-v3-flash",
|
||||
default_speech_recognition_model: "fun-asr",
|
||||
default_omni_model: "qwen3.5-omni-plus",
|
||||
};
|
||||
|
||||
@@ -100,12 +109,26 @@ export const UI_MODEL_CATALOG: Record<string, ModelOption[]> = {
|
||||
{ id: "wanx2.x", role: "image/generate · async series" },
|
||||
],
|
||||
default_video_model: [
|
||||
{ id: "happyhorse-1.1-t2v", role: "video/generate default · text-to-video" },
|
||||
{ id: "wan3.0-video", role: "video/generate · t2v / i2v / r2v default" },
|
||||
{ id: "happyhorse-1.1-t2v", role: "video/generate · text-to-video" },
|
||||
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
|
||||
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
|
||||
],
|
||||
default_image_to_video_model: [
|
||||
{ id: "wan3.0-video", role: "video/generate default · image-to-video" },
|
||||
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
|
||||
],
|
||||
default_reference_to_video_model: [
|
||||
{ id: "wan3.0-video", role: "video/ref default · reference-to-video" },
|
||||
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
|
||||
],
|
||||
default_speech_model: [
|
||||
{ id: "cosyvoice-v3-flash", role: "speech/synthesize (TTS) default" },
|
||||
{ id: "fun-asr", role: "speech/recognize (ASR)" },
|
||||
{ id: "qwen-audio-3.0-tts-plus", role: "Token Plan speech/synthesize (TTS)" },
|
||||
],
|
||||
default_speech_recognition_model: [
|
||||
{ id: "fun-asr", role: "speech/recognize (ASR) default" },
|
||||
{ id: "qwen-audio-3.0-asr-flash", role: "Token Plan speech/recognize (ASR)" },
|
||||
],
|
||||
default_omni_model: [
|
||||
{ id: "qwen3.5-omni-plus", role: "omni/chat default" },
|
||||
@@ -128,7 +151,9 @@ export const KEY_ALIASES: Record<string, string> = {
|
||||
"default-reference-to-video-model": "default_reference_to_video_model",
|
||||
"default-image-model": "default_image_model",
|
||||
"default-speech-model": "default_speech_model",
|
||||
"default-speech-recognition-model": "default_speech_recognition_model",
|
||||
"default-omni-model": "default_omni_model",
|
||||
"api-key-capabilities": "api_key_capabilities",
|
||||
"workspace-id": "workspace_id",
|
||||
};
|
||||
|
||||
@@ -141,7 +166,10 @@ export function resolveKey(key: string): string {
|
||||
* Validate a single config entry and coerce its value to the stored type.
|
||||
* Throws BailianError(USAGE) for unknown keys or invalid values.
|
||||
*/
|
||||
export function validateAndCoerce(key: string, value: string): string | number {
|
||||
export function validateAndCoerce(
|
||||
key: string,
|
||||
value: string,
|
||||
): string | number | boolean | string[] {
|
||||
const resolvedKey = resolveKey(key);
|
||||
|
||||
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
|
||||
@@ -178,6 +206,41 @@ export function validateAndCoerce(key: string, value: string): string | number {
|
||||
|
||||
if (resolvedKey === "base_url") return normalizeModelBaseUrl(value);
|
||||
|
||||
if (resolvedKey === "watermark") return parseBooleanValue(value, "watermark");
|
||||
|
||||
if (resolvedKey === "api_key_capabilities") {
|
||||
let rawCapabilities: unknown;
|
||||
if (value.trim().startsWith("[")) {
|
||||
try {
|
||||
rawCapabilities = JSON.parse(value) as unknown;
|
||||
} catch {
|
||||
throw new BailianError(
|
||||
`Invalid API Key capability list "${value}". Use comma-separated values or a JSON array.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
rawCapabilities = value.split(",").map((capability) => capability.trim());
|
||||
}
|
||||
|
||||
if (!Array.isArray(rawCapabilities)) {
|
||||
throw new BailianError("Invalid API Key capability list. Expected an array.", ExitCode.USAGE);
|
||||
}
|
||||
const capabilities: string[] = [];
|
||||
for (const rawCapability of rawCapabilities) {
|
||||
if (typeof rawCapability !== "string" || !isApiKeyCapability(rawCapability.trim())) {
|
||||
throw new BailianError(
|
||||
`Invalid API Key capability "${String(rawCapability)}".`,
|
||||
ExitCode.USAGE,
|
||||
"Use lowercase alphanumeric segments separated by '.' or '-'.",
|
||||
);
|
||||
}
|
||||
const capability = rawCapability.trim();
|
||||
if (!capabilities.includes(capability)) capabilities.push(capability);
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -187,7 +250,10 @@ export function validateAndCoerce(key: string, value: string): string | number {
|
||||
* extras (console_*, telemetry) are validated here. Booleans are returned as
|
||||
* real booleans so they persist correctly in config.json.
|
||||
*/
|
||||
export function validateAndCoerceUi(key: string, value: string): string | number | boolean {
|
||||
export function validateAndCoerceUi(
|
||||
key: string,
|
||||
value: string,
|
||||
): string | number | boolean | string[] {
|
||||
const resolvedKey = resolveKey(key);
|
||||
|
||||
if ((VALID_KEYS as readonly string[]).includes(resolvedKey)) {
|
||||
|
||||
@@ -18,6 +18,7 @@ export default defineCommand({
|
||||
base_url: client.baseUrl,
|
||||
output: settings.output,
|
||||
timeout: settings.timeout,
|
||||
watermark: settings.watermark,
|
||||
config: settings.configName ?? "default",
|
||||
config_file: store.path,
|
||||
};
|
||||
|
||||
@@ -1215,7 +1215,9 @@ const PAGE_HTML = `<!doctype html>
|
||||
var row = el('div', 'row');
|
||||
var label = el('label', '', key); label.htmlFor = 'f_' + key;
|
||||
var val = data[key];
|
||||
var strVal = (val === undefined || val === null) ? '' : String(val);
|
||||
var strVal = Array.isArray(val)
|
||||
? (val.length ? val.join(', ') : '[]')
|
||||
: ((val === undefined || val === null) ? '' : String(val));
|
||||
if (SECRETS.indexOf(key) >= 0) {
|
||||
var input = el('input'); input.id = 'f_' + key; input.name = key; input.value = strVal;
|
||||
input.type = 'password';
|
||||
|
||||
@@ -139,15 +139,17 @@ function tokenMatches(provided: string | null, expected: string): boolean {
|
||||
/** Build the request cleaned/validated config block from a posted `data` map. */
|
||||
function buildProfilePatch(
|
||||
data: Record<string, unknown>,
|
||||
): Record<string, string | number | boolean> {
|
||||
const cleaned: Record<string, string | number | boolean> = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
): Record<string, string | number | boolean | string[]> {
|
||||
const cleaned: Record<string, string | number | boolean | string[]> = {};
|
||||
for (const [key, rawValue] of Object.entries(data)) {
|
||||
let value = "";
|
||||
if (typeof v === "string") value = v;
|
||||
else if (typeof v === "number" || typeof v === "boolean") value = String(v);
|
||||
// null/undefined/objects fall through as "" and clear the key
|
||||
if (typeof rawValue === "string") value = rawValue;
|
||||
else if (typeof rawValue === "number" || typeof rawValue === "boolean") {
|
||||
value = String(rawValue);
|
||||
} else if (Array.isArray(rawValue)) value = JSON.stringify(rawValue);
|
||||
// null/undefined/non-array objects fall through as "" and clear the key
|
||||
if (value === "") continue;
|
||||
cleaned[resolveKey(k)] = validateAndCoerceUi(k, value);
|
||||
cleaned[resolveKey(key)] = validateAndCoerceUi(key, value);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
@@ -155,7 +157,7 @@ function buildProfilePatch(
|
||||
/** Preserve valid Config fields that the UI does not expose or manage. */
|
||||
function mergeUnmanagedProfileFields(
|
||||
existing: Record<string, unknown>,
|
||||
managedPatch: Record<string, string | number | boolean>,
|
||||
managedPatch: Record<string, string | number | boolean | string[]>,
|
||||
): Record<string, unknown> {
|
||||
const managedKeys = new Set<string>(UI_VALID_KEYS);
|
||||
const merged: Record<string, unknown> = {};
|
||||
@@ -635,7 +637,7 @@ export function createConfigUiServer(
|
||||
return;
|
||||
}
|
||||
let normalized: string | undefined;
|
||||
let cleaned: Record<string, string | number | boolean>;
|
||||
let cleaned: Record<string, string | number | boolean | string[]>;
|
||||
try {
|
||||
normalized = normalizeConfigName(body.name);
|
||||
cleaned = buildProfilePatch(body.data as Record<string, unknown>);
|
||||
|
||||
@@ -13,9 +13,20 @@ const DELETE_FLAGS = {
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: [
|
||||
"--file-id file-id-xxx",
|
||||
"--file-id file-id-xxx --dry-run",
|
||||
"--file-id file-id-xxx --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
|
||||
@@ -40,9 +40,20 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
"--deployed-model dep-... --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
|
||||
@@ -13,9 +13,16 @@ const DELETE_FLAGS = {
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
|
||||
@@ -207,7 +207,7 @@ export default defineCommand({
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
const watermark = resolveWatermark(flags.watermark, settings.watermark);
|
||||
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size: resolveImageSize(flags.size, route.sizeProfile),
|
||||
|
||||
@@ -185,7 +185,7 @@ export default defineCommand({
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
const watermark = resolveWatermark(flags.watermark, settings.watermark);
|
||||
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import type { PagedResult } from "./pagination.ts";
|
||||
|
||||
export const API_TARGET_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const CURSOR_FLAGS = {
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Page size (1-100)",
|
||||
"zh-CN": "单页数量(1-100)",
|
||||
},
|
||||
},
|
||||
page: {
|
||||
type: "string",
|
||||
valueHint: "<cursor>",
|
||||
description: {
|
||||
"en-US": "Opaque page cursor returned by a previous request",
|
||||
"zh-CN": "上一次请求返回的不透明分页 Cursor",
|
||||
},
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Fetch all pages by following opaque cursors",
|
||||
"zh-CN": "跟随不透明 Cursor 获取全部分页",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const SEARCH_FLAGS = {
|
||||
query: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Case-insensitive text to find in IDs, names, and descriptions",
|
||||
"zh-CN": "在 ID、名称和描述中进行不区分大小写的文本搜索",
|
||||
},
|
||||
},
|
||||
pageLimit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Maximum pages to scan for client-side search (default: 10)",
|
||||
"zh-CN": "客户端搜索最多扫描的页数(默认:10)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const INCLUDE_ARCHIVED_FLAG = {
|
||||
includeArchived: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Include archived resources",
|
||||
"zh-CN": "包含已归档资源",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export function validateLimitAndPageLimit(flags: {
|
||||
limit?: number;
|
||||
pageLimit?: number;
|
||||
}): string | undefined {
|
||||
if (
|
||||
flags.limit !== undefined &&
|
||||
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100)
|
||||
) {
|
||||
return "--limit must be an integer between 1 and 100.";
|
||||
}
|
||||
if (
|
||||
flags.pageLimit !== undefined &&
|
||||
(!Number.isInteger(flags.pageLimit) || flags.pageLimit < 1 || flags.pageLimit > 100)
|
||||
) {
|
||||
return "--page-limit must be an integer between 1 and 100.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function splitCommaSeparated(value?: string): string[] | undefined {
|
||||
if (!value) return undefined;
|
||||
const values = value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
export function matchesQuery(query: string, ...values: unknown[]): boolean {
|
||||
const normalized = query.trim().toLocaleLowerCase();
|
||||
if (!normalized) return true;
|
||||
return values.some((value) => {
|
||||
if (value === undefined || value === null) return false;
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return text.toLocaleLowerCase().includes(normalized);
|
||||
});
|
||||
}
|
||||
|
||||
export interface SearchResult<T> extends PagedResult<T> {
|
||||
scannedPages: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export async function searchCursorPages<T>(
|
||||
fetchPage: (page?: string) => Promise<PagedResult<T>>,
|
||||
matches: (item: T) => boolean,
|
||||
pageLimit = 10,
|
||||
): Promise<SearchResult<T>> {
|
||||
const items: T[] = [];
|
||||
let page: string | undefined;
|
||||
let hasMore = false;
|
||||
let nextPage: string | undefined;
|
||||
let scannedPages = 0;
|
||||
|
||||
do {
|
||||
const result = await fetchPage(page);
|
||||
scannedPages += 1;
|
||||
items.push(...result.items.filter(matches));
|
||||
hasMore = result.hasMore;
|
||||
nextPage = result.nextPage;
|
||||
page = result.nextPage;
|
||||
} while (hasMore && page && scannedPages < pageLimit);
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
nextPage,
|
||||
scannedPages,
|
||||
truncated: Boolean(hasMore && nextPage),
|
||||
};
|
||||
}
|
||||
|
||||
export function emitCollection<T>(options: {
|
||||
format: "json" | "text";
|
||||
key: string;
|
||||
items: T[];
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
hasMore?: boolean;
|
||||
nextPage?: string;
|
||||
truncated?: boolean;
|
||||
scannedPages?: number;
|
||||
emptyMessage?: string;
|
||||
}): void {
|
||||
const {
|
||||
format,
|
||||
key,
|
||||
items,
|
||||
headers,
|
||||
rows,
|
||||
hasMore = false,
|
||||
nextPage,
|
||||
truncated,
|
||||
scannedPages,
|
||||
emptyMessage = "No resources found.",
|
||||
} = options;
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
[key]: items,
|
||||
has_more: hasMore,
|
||||
next_page: nextPage,
|
||||
...(truncated === undefined ? {} : { truncated }),
|
||||
...(scannedPages === undefined ? {} : { scanned_pages: scannedPages }),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
emitBare(emptyMessage);
|
||||
return;
|
||||
}
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
emitBare(`\nTotal: ${items.length}`);
|
||||
if (truncated) emitBare("Search stopped at --page-limit; more pages remain.");
|
||||
else if (hasMore)
|
||||
emitBare(`More results are available.${nextPage ? ` Next page: ${nextPage}` : ""}`);
|
||||
}
|
||||
|
||||
export function displayValue(value: unknown, maxLength = 40): string {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
type LoadedProjectConfig,
|
||||
type ProjectRuntimeContext,
|
||||
resolveProjectConfig,
|
||||
UserError,
|
||||
} from "@openagentpack/sdk";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import {
|
||||
assertProviderCredentials,
|
||||
type CredentialHost,
|
||||
@@ -20,17 +20,21 @@ export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
|
||||
|
||||
/**
|
||||
* Whether this run requires provider keys:
|
||||
* - "all" (default) — online command: every provider declared in agents.yaml
|
||||
* must have a non-empty key after injection
|
||||
* - "all" (default) — online command: the Bailian provider must have a
|
||||
* non-empty key after injection
|
||||
* - "none" — offline command (local config/state only), skip the check
|
||||
*/
|
||||
export type CredentialScope = "all" | "none";
|
||||
|
||||
export const BAILIAN_PROVIDER = "bailian";
|
||||
|
||||
interface AgentConfigOptions {
|
||||
resolveEnv?: boolean;
|
||||
environment?: Record<string, string>;
|
||||
projectName?: string;
|
||||
statePath?: string;
|
||||
credentials?: CredentialScope;
|
||||
overrideBailianBaseUrl?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +46,8 @@ interface AgentConfigOptions {
|
||||
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
|
||||
* 4. scrub all credential vars from process.env (real values now live only in
|
||||
* the config object → provider adapters, never the environment)
|
||||
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
|
||||
* 5. enforce Bailian-only config and fail with a CLI-authoritative AUTH error
|
||||
* if its key is empty
|
||||
* (offline commands pass `credentials: "none"` to skip the check)
|
||||
*/
|
||||
export async function resolveAgentProjectConfig(
|
||||
@@ -53,8 +58,11 @@ export async function resolveAgentProjectConfig(
|
||||
prepareProviderEnv();
|
||||
const resolved = await resolveProjectConfig(filePath, options);
|
||||
normalizeInterpolatedProviderBlocks(resolved.config.providers);
|
||||
injectProviderCredentials(resolved.config.providers, host);
|
||||
injectProviderCredentials(resolved.config.providers, host, {
|
||||
overrideBaseUrl: options.overrideBailianBaseUrl,
|
||||
});
|
||||
scrubCredentialEnv();
|
||||
assertBailianOnlyProviders(resolved.config.providers);
|
||||
if ((options.credentials ?? "all") !== "none") {
|
||||
assertProviderCredentials(resolved.config.providers);
|
||||
}
|
||||
@@ -91,15 +99,23 @@ export async function buildAgentRuntime(
|
||||
return { ...ctx, configPath };
|
||||
}
|
||||
|
||||
/** Ensure a user-supplied --provider value is actually configured in agents.yaml. */
|
||||
export function assertProviderConfigured(
|
||||
ctx: ProjectRuntimeContext,
|
||||
provider: string | undefined,
|
||||
): void {
|
||||
if (!provider || provider === "all") return;
|
||||
if (ctx.providers.has(provider)) return;
|
||||
const available = Array.from(ctx.providers.keys()).join(", ") || "none";
|
||||
throw new UserError(
|
||||
`Provider '${provider}' is not configured. Available providers: ${available}.`,
|
||||
/** Enforce the bl product boundary while the shared SDK remains multi-provider capable. */
|
||||
export function assertBailianOnlyProviders(providers: Record<string, unknown>): void {
|
||||
const configuredProviders = Object.keys(providers);
|
||||
const unsupportedProviders = configuredProviders.filter(
|
||||
(provider) => provider !== BAILIAN_PROVIDER,
|
||||
);
|
||||
if (unsupportedProviders.length > 0) {
|
||||
const names = unsupportedProviders.join(", ");
|
||||
throw new BailianError(
|
||||
`bl managed-agent only supports provider '${BAILIAN_PROVIDER}'; remove unsupported providers: ${names}. / bl managed-agent 仅支持 Provider '${BAILIAN_PROVIDER}';请移除不支持的 Provider:${names}。`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (!(BAILIAN_PROVIDER in providers)) {
|
||||
throw new BailianError(
|
||||
`bl managed-agent requires a '${BAILIAN_PROVIDER}' provider configuration. / bl managed-agent 需要配置 Provider '${BAILIAN_PROVIDER}'。`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ const CREDENTIAL_HINTS: Record<string, string> = {
|
||||
qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.",
|
||||
};
|
||||
|
||||
const BAILIAN_ONLY_NOTE = {
|
||||
"en-US":
|
||||
"bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.",
|
||||
"zh-CN": "bl managed-agent 仅支持百炼 Provider;包含其他 Provider 的配置会被拒绝。",
|
||||
} as const;
|
||||
|
||||
/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */
|
||||
export interface CredentialHost {
|
||||
client: Client;
|
||||
@@ -43,24 +49,18 @@ export interface CredentialHost {
|
||||
/**
|
||||
* Shared `--help` note documenting where agent commands get provider
|
||||
* credentials. Bailian goes through bl's own auth chain (commands declare
|
||||
* `auth: "apiKey"`); other providers come from env. Either way the resolved
|
||||
* credential is injected into the SDK in-memory and scrubbed from the
|
||||
* environment. Attach to every command that loads agents.yaml. `bl` prefix is
|
||||
* safe: agent commands ship on `bl` only.
|
||||
* `auth: "apiKey"`). The resolved credential is injected into the SDK in-memory
|
||||
* and scrubbed from the environment. Attach to every command that loads
|
||||
* agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
|
||||
*/
|
||||
export const CREDENTIALS_NOTE = [
|
||||
BAILIAN_ONLY_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
|
||||
"zh-CN":
|
||||
"百炼凭证来自 bl 鉴权链:--api-key > DASHSCOPE_API_KEY > `bl auth login`(当前激活的配置 Profile)。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
|
||||
"zh-CN":
|
||||
"其他 Provider 读取 agents.yaml 中引用的环境变量(例如 ${ANTHROPIC_API_KEY}),包括 .env 和 ~/.agents/config.json。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
|
||||
@@ -73,6 +73,7 @@ export const CREDENTIALS_NOTE = [
|
||||
* agents.yaml / local state only, so no login or provider key is required.
|
||||
*/
|
||||
export const OFFLINE_NOTE = [
|
||||
BAILIAN_ONLY_NOTE,
|
||||
{
|
||||
"en-US": "Runs fully offline against local files: no login or provider credentials required.",
|
||||
"zh-CN": "完全离线处理本地文件:无需登录或提供 Provider 凭证。",
|
||||
@@ -98,9 +99,10 @@ export function prepareProviderEnv(): void {
|
||||
* Override the bailian provider block with bl's authStage-resolved credential, so
|
||||
* the bailian API key is authoritatively the CLI auth chain's — never a config
|
||||
* file bare-read or a stale env value. `api_key` is replaced unconditionally
|
||||
* when a credential resolved; `base_url` / `workspace_id` are filled only when
|
||||
* the block references them and the interpolated value is empty (a literal in
|
||||
* agents.yaml is respected).
|
||||
* when a credential resolved. `base_url` is normally filled only when empty so
|
||||
* a literal in agents.yaml remains supported; directory projects pass
|
||||
* `overrideBaseUrl` so their connection always follows the Bailian CLI auth
|
||||
* chain. `workspace_id` is filled only when empty.
|
||||
*
|
||||
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
|
||||
* paths onto it verbatim; a value already ending in the suffix is left as-is.
|
||||
@@ -113,6 +115,7 @@ export function prepareProviderEnv(): void {
|
||||
export function injectProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
host: CredentialHost,
|
||||
options: { overrideBaseUrl?: boolean } = {},
|
||||
): void {
|
||||
const bailian = providers.bailian;
|
||||
if (!bailian || typeof bailian !== "object") return;
|
||||
@@ -120,7 +123,7 @@ export function injectProviderCredentials(
|
||||
|
||||
const cred = host.client.exportApiCredential();
|
||||
if (cred) block.api_key = cred.token;
|
||||
if ("base_url" in block && !block.base_url) {
|
||||
if ("base_url" in block && (options.overrideBaseUrl || !block.base_url)) {
|
||||
// Defensive normalization: the auth chain already normalizes base_url to
|
||||
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
|
||||
const origin = host.client.baseUrl.replace(/\/+$/, "");
|
||||
@@ -166,15 +169,16 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
|
||||
}
|
||||
|
||||
/**
|
||||
* After injection, fail with a CLI-authoritative AUTH error if any configured
|
||||
* provider's `api_key` resolved empty (missing env var, or no bl login for
|
||||
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
|
||||
* zod config error with a clean message plus a provider-specific hint. Validates
|
||||
* every declared provider, so a project is only runnable once all its providers'
|
||||
* keys are available; offline commands skip the check entirely.
|
||||
* After injection, fail with a CLI-authoritative AUTH error if Bailian's
|
||||
* `api_key` resolved empty. Replaces the SDK's raw config error with a clean
|
||||
* message and hint. Offline commands skip the check entirely.
|
||||
*/
|
||||
export function assertProviderCredentials(providers: Record<string, unknown>): void {
|
||||
export function assertProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
targetProviders?: readonly string[],
|
||||
): void {
|
||||
for (const [name, raw] of Object.entries(providers)) {
|
||||
if (targetProviders && !targetProviders.includes(name)) continue;
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
const block = raw as Record<string, unknown>;
|
||||
if (!("api_key" in block)) continue;
|
||||
|
||||
@@ -12,6 +12,12 @@ interface SdkApiErrorLike extends Error {
|
||||
responseBody: string;
|
||||
}
|
||||
|
||||
interface AgentDiagnosticLike {
|
||||
severity: string;
|
||||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function isSdkApiError(error: Error): error is SdkApiErrorLike {
|
||||
const candidate = error as Partial<SdkApiErrorLike>;
|
||||
return typeof candidate.statusCode === "number" && typeof candidate.responseBody === "string";
|
||||
@@ -45,6 +51,42 @@ function isSdkPollingTimeout(error: UserError): boolean {
|
||||
return /did not complete within the timeout/i.test(error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the final CLI error useful for hosts that retain stderr's terminal
|
||||
* BailianError but discard the diagnostics emitted before it. The complete
|
||||
* diagnostic collection remains on stdout/stderr; the terminal error carries
|
||||
* the first actionable reason and signals when more errors are available.
|
||||
*/
|
||||
export function formatAgentDiagnosticFailure(
|
||||
diagnostics: readonly AgentDiagnosticLike[],
|
||||
fallback: string,
|
||||
): string {
|
||||
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
||||
const firstError = errors[0];
|
||||
if (!firstError) return fallback;
|
||||
|
||||
const codePrefix = firstError.code?.trim() ? `[${firstError.code}] ` : "";
|
||||
const remaining = errors.length - 1;
|
||||
const remainingSuffix = remaining > 0 ? ` (+${remaining} more errors)` : "";
|
||||
return `${codePrefix}${firstError.message}${remainingSuffix}`;
|
||||
}
|
||||
|
||||
/** Add a recovery hint without replacing the original error or its metadata. */
|
||||
export function retainAgentError(error: unknown, hint: string, fallback: string): BailianError {
|
||||
if (error instanceof BailianError) {
|
||||
return new BailianError(error.message, error.exitCode, hint, {
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
cause: error.cause,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return new BailianError(error.message, ExitCode.GENERAL, hint, { cause: error });
|
||||
}
|
||||
const message = typeof error === "string" && error.trim() ? error : fallback;
|
||||
return new BailianError(message, ExitCode.GENERAL, hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an SDK-backed operation, translating SDK error types into BailianError so
|
||||
* bl's error handler produces the right exit code and hint formatting.
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type StateScope,
|
||||
} from "@openagentpack/sdk";
|
||||
|
||||
function createStateScope(configPath: string, projectName?: string): StateScope {
|
||||
export function createFileStateScope(configPath: string, projectName?: string): StateScope {
|
||||
const resolved = resolve(configPath);
|
||||
return { projectId: projectName ?? basename(dirname(resolved)) };
|
||||
}
|
||||
@@ -19,6 +19,6 @@ export async function loadFileState(
|
||||
): Promise<IStateManager> {
|
||||
const resolved = resolve(configPath);
|
||||
const backend = new LocalFileStateBackend({ configPath: resolved, statePath });
|
||||
const path = backend.getStatePath(createStateScope(resolved, projectName));
|
||||
const path = backend.getStatePath(createFileStateScope(resolved, projectName));
|
||||
return StateManager.load(path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { link, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
|
||||
export async function readInputFile(path: string): Promise<Uint8Array> {
|
||||
return new Uint8Array(await readFile(resolve(path)));
|
||||
}
|
||||
|
||||
export async function readJsonInput(argument: string): Promise<unknown> {
|
||||
const source = argument.startsWith("@")
|
||||
? await readFile(resolve(argument.slice(1)), "utf8")
|
||||
: argument;
|
||||
try {
|
||||
return JSON.parse(source) as unknown;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new BailianError(`Invalid JSON input: ${message}`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeOutputFile(
|
||||
outputPath: string,
|
||||
content: Uint8Array,
|
||||
force = false,
|
||||
): Promise<string> {
|
||||
const destination = resolve(outputPath);
|
||||
const parent = dirname(destination);
|
||||
await mkdir(parent, { recursive: true });
|
||||
const temporary = `${destination}.${randomUUID()}.tmp`;
|
||||
await writeFile(temporary, content, { flag: "wx" });
|
||||
try {
|
||||
if (force) {
|
||||
await rename(temporary, destination);
|
||||
} else {
|
||||
try {
|
||||
await link(temporary, destination);
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === "EEXIST") {
|
||||
throw new BailianError(
|
||||
`Output file already exists: ${destination}`,
|
||||
ExitCode.USAGE,
|
||||
"Choose another --output-file path or re-run with --force.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await unlink(temporary);
|
||||
}
|
||||
return destination;
|
||||
} catch (error) {
|
||||
await unlink(temporary).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMimeType(path: string): string {
|
||||
const filename = basename(path).toLocaleLowerCase();
|
||||
if (filename.endsWith(".json")) return "application/json";
|
||||
if (filename.endsWith(".md")) return "text/markdown";
|
||||
if (filename.endsWith(".txt")) return "text/plain";
|
||||
if (filename.endsWith(".pdf")) return "application/pdf";
|
||||
if (filename.endsWith(".png")) return "image/png";
|
||||
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (filename.endsWith(".zip")) return "application/zip";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
@@ -8,8 +8,9 @@ export interface PagedResult<T> {
|
||||
export async function fetchAllPages<T>(
|
||||
fetchPage: (page?: string) => Promise<PagedResult<T>>,
|
||||
all?: boolean,
|
||||
initialPage?: string,
|
||||
): Promise<PagedResult<T>> {
|
||||
const first = await fetchPage();
|
||||
const first = await fetchPage(initialPage);
|
||||
const items = [...first.items];
|
||||
let hasMore = first.hasMore;
|
||||
let nextPage = first.nextPage;
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const PLAYGROUND_PACKAGE = "@openagentpack/playground";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DEFAULT_PORT = 4848;
|
||||
const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i;
|
||||
|
||||
export interface PlaygroundLaunchOptions {
|
||||
port?: number;
|
||||
open: boolean;
|
||||
file?: string;
|
||||
project?: string;
|
||||
agent?: string;
|
||||
surface: "preview" | "workbench";
|
||||
client: Client;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface Launcher {
|
||||
command: string;
|
||||
args: string[];
|
||||
version?: string;
|
||||
fetched: boolean;
|
||||
}
|
||||
|
||||
interface ExistingPlayground {
|
||||
version: string;
|
||||
pid: number;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface PlaygroundProjectSummary {
|
||||
status?: string;
|
||||
agents?: Array<{ agent?: { id?: string } }>;
|
||||
}
|
||||
|
||||
export interface PlaygroundBrowserTarget {
|
||||
url: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
export async function launchManagedAgentPlayground(
|
||||
options: PlaygroundLaunchOptions,
|
||||
): Promise<void> {
|
||||
assertSupportedNodeVersion();
|
||||
const port = options.port ?? DEFAULT_PORT;
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
||||
throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE);
|
||||
}
|
||||
const sourcePath = resolve(
|
||||
options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"),
|
||||
);
|
||||
const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
||||
const launcher = await resolveLauncher();
|
||||
const existing = await probeExistingPlayground(port);
|
||||
if (existing) {
|
||||
const reusable =
|
||||
existing.projectId === projectId &&
|
||||
(launcher.version === undefined || existing.version === launcher.version);
|
||||
if (reusable) {
|
||||
emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`);
|
||||
await openPlaygroundSurface(port, options.surface, options.agent, options.open);
|
||||
return;
|
||||
}
|
||||
const released = await replaceExistingPlayground(existing, port);
|
||||
if (!released) {
|
||||
throw new BailianError(
|
||||
`Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
"Stop it manually or choose another --port.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const environment = buildPlaygroundEnvironment(options, port, sourcePath);
|
||||
if (launcher.fetched) {
|
||||
emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`);
|
||||
}
|
||||
const child = spawn(launcher.command, launcher.args, {
|
||||
env: environment,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
const removeSignalForwarding = forwardSignals(child);
|
||||
try {
|
||||
const readyPort = await waitForPlaygroundReady(child, port, 30_000, projectId);
|
||||
if (readyPort === null) {
|
||||
throw new BailianError(
|
||||
`Workbench did not become ready in time. Check the logs above, then open http://localhost:${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
emitBare(`Workbench ready at http://localhost:${readyPort}`);
|
||||
await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open);
|
||||
const exitCode = await waitForChildExit(child);
|
||||
if (exitCode !== 0) {
|
||||
throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL);
|
||||
}
|
||||
} finally {
|
||||
removeSignalForwarding();
|
||||
}
|
||||
}
|
||||
|
||||
export function playgroundBrowserTargetFromSummary(
|
||||
baseUrl: string,
|
||||
summary: PlaygroundProjectSummary,
|
||||
requestedAgent?: string,
|
||||
): PlaygroundBrowserTarget {
|
||||
if (summary.status !== "valid") return { url: baseUrl };
|
||||
const agentIds = (summary.agents ?? [])
|
||||
.map((entry) => entry.agent?.id?.trim())
|
||||
.filter((agentId): agentId is string => Boolean(agentId));
|
||||
const requested = requestedAgent?.trim();
|
||||
if (requested) {
|
||||
if (agentIds.includes(requested)) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(requested)}/preview` };
|
||||
}
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning: `Agent '${requested}' was not found. Opening the project Workbench instead.`,
|
||||
};
|
||||
}
|
||||
if (agentIds.length === 1) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(agentIds[0]!)}/preview` };
|
||||
}
|
||||
if (agentIds.length > 1) {
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning:
|
||||
"This project declares multiple Agents. Opening the Workbench; rerun with --agent <id> for Preview.",
|
||||
};
|
||||
}
|
||||
return { url: baseUrl };
|
||||
}
|
||||
|
||||
function assertSupportedNodeVersion(): void {
|
||||
const majorVersion = Number(process.versions.node.split(".")[0]);
|
||||
if (Number.isFinite(majorVersion) && majorVersion >= 22) return;
|
||||
throw new BailianError(
|
||||
"Managed Agent Workbench requires Node.js 22 or later.",
|
||||
ExitCode.USAGE,
|
||||
"Upgrade Node.js for Workbench; other Bailian CLI commands continue to support Node.js 18.17+.",
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveLauncher(): Promise<Launcher> {
|
||||
const explicit =
|
||||
process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() ||
|
||||
process.env.AGENTS_PLAYGROUND_BIN?.trim();
|
||||
if (explicit) {
|
||||
if (!existsSync(explicit)) {
|
||||
throw new BailianError(
|
||||
`Configured Workbench binary does not exist: ${explicit}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return { command: process.execPath, args: [explicit], fetched: false };
|
||||
}
|
||||
|
||||
const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest";
|
||||
const { stdout } = await execFileAsync(
|
||||
"npm",
|
||||
[
|
||||
"view",
|
||||
`${PLAYGROUND_PACKAGE}@${requestedVersion}`,
|
||||
"version",
|
||||
"--json",
|
||||
"--prefer-online",
|
||||
"--fetch-retries=0",
|
||||
"--fetch-timeout=10000",
|
||||
],
|
||||
{ timeout: 15_000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
let resolvedVersion: unknown;
|
||||
try {
|
||||
resolvedVersion = JSON.parse(stdout);
|
||||
} catch {
|
||||
resolvedVersion = undefined;
|
||||
}
|
||||
if (
|
||||
typeof resolvedVersion !== "string" ||
|
||||
!/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(resolvedVersion)
|
||||
) {
|
||||
throw new BailianError(
|
||||
"npm did not return a single valid Playground version. Specify an exact version or dist-tag. / npm 未返回唯一有效的 Playground 版本号。请指定精确版本或 dist-tag。",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const installed = resolveInstalledPlayground();
|
||||
if (installed?.version === resolvedVersion) return installed;
|
||||
|
||||
const monorepoBinary = findLocalPlaygroundBin(process.cwd());
|
||||
if (
|
||||
monorepoBinary &&
|
||||
readPlaygroundVersion(resolve(dirname(monorepoBinary), "../../package.json")) ===
|
||||
resolvedVersion
|
||||
) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [monorepoBinary],
|
||||
version: resolvedVersion,
|
||||
fetched: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npx",
|
||||
args: ["-y", `${PLAYGROUND_PACKAGE}@${resolvedVersion}`],
|
||||
version: resolvedVersion,
|
||||
fetched: true,
|
||||
};
|
||||
}
|
||||
|
||||
function readPlaygroundVersion(manifestPath: string): string | undefined {
|
||||
try {
|
||||
return (JSON.parse(readFileSync(manifestPath, "utf8")) as { version?: string }).version;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstalledPlayground(): Launcher | undefined {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJsonPath = require.resolve(`${PLAYGROUND_PACKAGE}/package.json`);
|
||||
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
version?: string;
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
const relativeBinary =
|
||||
typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["agents-playground"];
|
||||
if (!relativeBinary) return undefined;
|
||||
const binaryPath = resolve(dirname(packageJsonPath), relativeBinary);
|
||||
if (!existsSync(binaryPath)) return undefined;
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [binaryPath],
|
||||
version: manifest.version,
|
||||
fetched: false,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function findLocalPlaygroundBin(startDirectory: string): string | undefined {
|
||||
let directory = startDirectory;
|
||||
for (let depth = 0; depth < 10; depth += 1) {
|
||||
const candidate = resolve(directory, "packages/playground/dist/bin/playground.js");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = dirname(directory);
|
||||
if (parent === directory) break;
|
||||
directory = parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildPlaygroundEnvironment(
|
||||
options: PlaygroundLaunchOptions,
|
||||
port: number,
|
||||
sourcePath: string,
|
||||
): NodeJS.ProcessEnv {
|
||||
const credential = options.client.exportApiCredential();
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"),
|
||||
};
|
||||
if (options.surface === "workbench") {
|
||||
delete environment.AGENTS_CONFIG_PATH;
|
||||
environment.AGENTS_PROJECT_ROOT = sourcePath;
|
||||
} else {
|
||||
delete environment.AGENTS_PROJECT_ROOT;
|
||||
environment.AGENTS_CONFIG_PATH = sourcePath;
|
||||
}
|
||||
if (credential) environment.DASHSCOPE_API_KEY = credential.token;
|
||||
const baseUrl = options.client.baseUrl.replace(/\/+$/, "");
|
||||
environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio")
|
||||
? baseUrl
|
||||
: `${baseUrl}/api/v1/agentstudio`;
|
||||
if (options.settings.workspaceId) {
|
||||
environment.BAILIAN_WORKSPACE_ID = options.settings.workspaceId;
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
async function waitForPlaygroundReady(
|
||||
child: ChildProcess,
|
||||
fallbackPort: number,
|
||||
timeoutMs: number,
|
||||
expectedProjectId: string,
|
||||
): Promise<number | null> {
|
||||
let port = fallbackPort;
|
||||
let outputBuffer = "";
|
||||
child.stdout?.on("data", (chunk: Buffer | string) => {
|
||||
process.stdout.write(chunk);
|
||||
outputBuffer += chunk.toString();
|
||||
const match = outputBuffer.match(PLAYGROUND_URL_PATTERN);
|
||||
if (match?.[1]) port = Number(match[1]);
|
||||
});
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() <= deadline) {
|
||||
if (child.exitCode !== null) return null;
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(1_000),
|
||||
});
|
||||
const body = response.ok
|
||||
? ((await response.json()) as { playground?: { project_id?: string } })
|
||||
: undefined;
|
||||
if (body?.playground?.project_id === expectedProjectId) return port;
|
||||
} catch {
|
||||
// Not ready yet.
|
||||
}
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 300));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function probeExistingPlayground(port: number): Promise<ExistingPlayground | null> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const body = (await response.json()) as {
|
||||
playground?: { version?: string; pid?: number; project_id?: string };
|
||||
};
|
||||
if (!body.playground?.pid) return null;
|
||||
return {
|
||||
version: body.playground.version ?? "unknown",
|
||||
pid: body.playground.pid,
|
||||
projectId: body.playground.project_id,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceExistingPlayground(
|
||||
existing: ExistingPlayground,
|
||||
port: number,
|
||||
): Promise<boolean> {
|
||||
emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`);
|
||||
try {
|
||||
process.kill(existing.pid, "SIGTERM");
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 100));
|
||||
if (!(await probeExistingPlayground(port))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openPlaygroundSurface(
|
||||
port: number,
|
||||
surface: "preview" | "workbench",
|
||||
requestedAgent: string | undefined,
|
||||
shouldOpen: boolean,
|
||||
): Promise<void> {
|
||||
if (!shouldOpen) return;
|
||||
const target =
|
||||
surface === "workbench"
|
||||
? { url: `http://localhost:${port}` }
|
||||
: await resolvePlaygroundBrowserTarget(port, requestedAgent);
|
||||
if (target.warning) emitBare(`Warning: ${target.warning}`);
|
||||
openBrowser(target.url);
|
||||
}
|
||||
|
||||
async function resolvePlaygroundBrowserTarget(
|
||||
port: number,
|
||||
requestedAgent?: string,
|
||||
): Promise<PlaygroundBrowserTarget> {
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/project`, {
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
});
|
||||
if (!response.ok) return { url: baseUrl };
|
||||
return playgroundBrowserTargetFromSummary(
|
||||
baseUrl,
|
||||
(await response.json()) as PlaygroundProjectSummary,
|
||||
requestedAgent,
|
||||
);
|
||||
} catch {
|
||||
return { url: baseUrl };
|
||||
}
|
||||
}
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
const command =
|
||||
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
||||
const args = process.platform === "win32" ? ["", url] : [url];
|
||||
try {
|
||||
spawn(command, args, {
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
shell: process.platform === "win32",
|
||||
}).unref();
|
||||
} catch {
|
||||
emitBare(`Could not open a browser automatically. Visit ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function forwardSignals(child: ChildProcess): () => void {
|
||||
const forwardInterrupt = () => child.kill("SIGINT");
|
||||
const forwardTerminate = () => child.kill("SIGTERM");
|
||||
process.on("SIGINT", forwardInterrupt);
|
||||
process.on("SIGTERM", forwardTerminate);
|
||||
return () => {
|
||||
process.off("SIGINT", forwardInterrupt);
|
||||
process.off("SIGTERM", forwardTerminate);
|
||||
};
|
||||
}
|
||||
|
||||
function waitForChildExit(child: ChildProcess): Promise<number> {
|
||||
if (child.exitCode !== null) return Promise.resolve(child.exitCode);
|
||||
return new Promise((resolveExit, rejectExit) => {
|
||||
child.once("exit", (exitCode) => resolveExit(exitCode ?? 0));
|
||||
child.once("error", rejectExit);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import {
|
||||
type BackendRuntimeInput,
|
||||
type IStateManager,
|
||||
LocalFileStateBackend,
|
||||
planProjectWithStateBackend,
|
||||
type ResolvedProjectConfig,
|
||||
type ResourceAddress,
|
||||
type ResourceSyncRun,
|
||||
resolveProjectConfigFromObject,
|
||||
syncProjectResourcesWithStateBackend,
|
||||
} from "@openagentpack/sdk";
|
||||
import { BailianError, type CommandContext, detectOutputFormat, ExitCode } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseDocument } from "yaml";
|
||||
import { formatResourceLabel } from "./address-utils.ts";
|
||||
import { resolveAgentProjectConfig } from "./config-loader.ts";
|
||||
import { assertProviderCredentials } from "./credentials.ts";
|
||||
import { withStdoutProtected } from "./console-capture.ts";
|
||||
import { retainAgentError, withAgentErrors } from "./errors.ts";
|
||||
import { createFileStateScope } from "./file-state-manager.ts";
|
||||
import { installSdkTransport } from "./transport.ts";
|
||||
|
||||
export type ScopedCreateHost = Pick<CommandContext, "client" | "identity" | "settings">;
|
||||
export type ScopedCreateGroup = "agents" | "environments" | "skills" | "vaults" | "deployments";
|
||||
|
||||
export interface LoadedScopedCreateProject {
|
||||
configPath: string;
|
||||
projectName: string;
|
||||
config: ResolvedProjectConfig;
|
||||
source: string;
|
||||
provider: string;
|
||||
stateBackend: LocalFileStateBackend;
|
||||
stateScope: ReturnType<typeof createFileStateScope>;
|
||||
}
|
||||
|
||||
export interface ResourceKeySelection {
|
||||
key: string;
|
||||
reusedPending: boolean;
|
||||
}
|
||||
|
||||
export const SCOPED_CREATE_NOTE = [
|
||||
{
|
||||
"en-US":
|
||||
"Without --yes, this command only previews. --dry-run is fully offline. The scoped flow checks only the target resource and its transitive dependencies; unrelated resources are not refreshed or drift-checked.",
|
||||
"zh-CN":
|
||||
"不带 --yes 时仅预览;--dry-run 完全离线。定向流程只检查目标资源及其传递依赖,不刷新或检测无关资源的 Drift。",
|
||||
},
|
||||
];
|
||||
|
||||
export interface ScopedTopLevelCreateInput {
|
||||
host: ScopedCreateHost;
|
||||
project: LoadedScopedCreateProject;
|
||||
group: Exclude<ScopedCreateGroup, "agents">;
|
||||
resourceType: "environment" | "skill" | "vault" | "deployment";
|
||||
displayName: string;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
resolvedDeclaration: Record<string, unknown>;
|
||||
existingDeclarations: Record<string, Record<string, unknown>>;
|
||||
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
|
||||
fallbackKey: string;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
const normalize = (candidate: unknown): unknown => {
|
||||
if (Array.isArray(candidate)) return candidate.map(normalize);
|
||||
if (!candidate || typeof candidate !== "object") return candidate;
|
||||
return Object.fromEntries(
|
||||
Object.entries(candidate as Record<string, unknown>)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
||||
.map(([key, entry]) => [key, normalize(entry)]),
|
||||
);
|
||||
};
|
||||
return JSON.stringify(normalize(value));
|
||||
}
|
||||
|
||||
export function normalizeResourceKey(displayName: string, fallback = "resource"): string {
|
||||
const normalized = displayName
|
||||
.normalize("NFKC")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
export function selectResourceKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
resourceTypes: ResourceAddress["type"][];
|
||||
declarations: Record<string, Record<string, unknown>>;
|
||||
candidate: Record<string, unknown>;
|
||||
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
|
||||
fallbackKey?: string;
|
||||
state: IStateManager;
|
||||
}): ResourceKeySelection {
|
||||
const tracked = new Set(
|
||||
options.state
|
||||
.listResources()
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.address.provider === options.provider &&
|
||||
options.resourceTypes.includes(resource.address.type),
|
||||
)
|
||||
.map((resource) => resource.address.name),
|
||||
);
|
||||
const candidateJson = canonicalJson(options.candidate);
|
||||
for (const [key, declaration] of Object.entries(options.declarations)) {
|
||||
if (options.effectiveName(key, declaration) !== options.displayName || tracked.has(key))
|
||||
continue;
|
||||
if (canonicalJson(declaration) === candidateJson) return { key, reusedPending: true };
|
||||
}
|
||||
|
||||
const baseKey = normalizeResourceKey(options.displayName, options.fallbackKey);
|
||||
if (!(baseKey in options.declarations)) return { key: baseKey, reusedPending: false };
|
||||
let suffix = 2;
|
||||
while (`${baseKey}-${suffix}` in options.declarations) suffix += 1;
|
||||
return { key: `${baseKey}-${suffix}`, reusedPending: false };
|
||||
}
|
||||
|
||||
export async function loadScopedCreateProject(
|
||||
host: ScopedCreateHost,
|
||||
file: string,
|
||||
): Promise<LoadedScopedCreateProject> {
|
||||
const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
throw new BailianError(
|
||||
`Config file not found: ${file}`,
|
||||
ExitCode.USAGE,
|
||||
"Run `bl managed-agent init` first.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
const loaded = await withAgentErrors(() =>
|
||||
resolveAgentProjectConfig(host, file, { credentials: "none" }),
|
||||
);
|
||||
const source = await readFile(loaded.configPath, "utf8");
|
||||
if (source !== sourceBeforeLoad) {
|
||||
throw new BailianError(
|
||||
`${file} changed while it was being loaded.`,
|
||||
ExitCode.GENERAL,
|
||||
"Re-run the command against the latest file.",
|
||||
);
|
||||
}
|
||||
const provider = "bailian";
|
||||
if (!host.settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]);
|
||||
installSdkTransport(host);
|
||||
const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath });
|
||||
return {
|
||||
...loaded,
|
||||
source,
|
||||
provider,
|
||||
stateBackend,
|
||||
stateScope: createFileStateScope(loaded.configPath, loaded.projectName),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveCandidateDeclaration(options: {
|
||||
project: LoadedScopedCreateProject;
|
||||
group: ScopedCreateGroup;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const temporaryKey = "__bailian_cli_create_candidate__";
|
||||
const rawConfig = structuredClone(options.project.config) as unknown as Record<string, unknown>;
|
||||
const declarations = (rawConfig[options.group] ?? {}) as Record<string, unknown>;
|
||||
rawConfig[options.group] = { ...declarations, [temporaryKey]: options.rawDeclaration };
|
||||
const resolved = await withAgentErrors(() =>
|
||||
resolveProjectConfigFromObject(rawConfig, {
|
||||
projectName: options.project.projectName,
|
||||
basePath: dirname(options.project.configPath),
|
||||
}),
|
||||
);
|
||||
const resolvedGroup = resolved.config[options.group] as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined;
|
||||
return resolvedGroup?.[temporaryKey] ?? options.rawDeclaration;
|
||||
}
|
||||
|
||||
export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): Promise<void> {
|
||||
const { project, host } = input;
|
||||
const format = detectOutputFormat(host.settings.output);
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectResourceKey({
|
||||
displayName: input.displayName,
|
||||
provider: project.provider,
|
||||
resourceTypes: [input.resourceType],
|
||||
declarations: input.existingDeclarations,
|
||||
candidate: input.resolvedDeclaration,
|
||||
effectiveName: input.effectiveName,
|
||||
fallbackKey: input.fallbackKey,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const resourceKey = keySelection.key;
|
||||
const candidateConfig = structuredClone(project.config) as unknown as Record<string, unknown>;
|
||||
const existingGroup = (candidateConfig[input.group] ?? {}) as Record<string, unknown>;
|
||||
candidateConfig[input.group] = { ...existingGroup, [resourceKey]: input.resolvedDeclaration };
|
||||
candidateConfig._resolved = true;
|
||||
|
||||
const document = parseDocument(project.source);
|
||||
if (document.errors.length > 0) {
|
||||
throw new BailianError(
|
||||
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
document.setIn([input.group, resourceKey], input.rawDeclaration);
|
||||
const nextSource = document.toString();
|
||||
const root: ResourceAddress = {
|
||||
type: input.resourceType,
|
||||
name: resourceKey,
|
||||
provider: project.provider,
|
||||
};
|
||||
const backendInput: BackendRuntimeInput = {
|
||||
projectName: project.projectName,
|
||||
config: candidateConfig as unknown as ResolvedProjectConfig,
|
||||
configPath: project.configPath,
|
||||
providers: { [project.provider]: project.config.providers[project.provider] },
|
||||
stateBackend: project.stateBackend,
|
||||
stateScope: project.stateScope,
|
||||
};
|
||||
|
||||
if (host.settings.dryRun || !input.yes) {
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectWithStateBackend(backendInput, {
|
||||
provider: project.provider,
|
||||
scope: { roots: [root] },
|
||||
mode: "create-only",
|
||||
refresh: !host.settings.dryRun,
|
||||
quiet: format === "json",
|
||||
}),
|
||||
),
|
||||
);
|
||||
const readyToCreate = !planned.plan.diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
const result = {
|
||||
resource: {
|
||||
type: input.resourceType,
|
||||
key: resourceKey,
|
||||
name: input.displayName,
|
||||
provider: project.provider,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: false,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
requires_confirmation: !host.settings.dryRun,
|
||||
ready_to_create: readyToCreate,
|
||||
actions: planned.plan.actions,
|
||||
diagnostics: planned.plan.diagnostics,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Generated YAML key: ${resourceKey}`);
|
||||
renderPlan(planned.plan.actions, planned.plan.diagnostics);
|
||||
emitBare(
|
||||
host.settings.dryRun
|
||||
? "Dry run: YAML, State, and remote resources were not changed."
|
||||
: "Preview only: re-run with --yes to write YAML and create this resource.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceConfigAtomically(project.configPath, project.source, nextSource);
|
||||
let run: ResourceSyncRun;
|
||||
try {
|
||||
run = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
syncProjectResourcesWithStateBackend(backendInput, {
|
||||
provider: project.provider,
|
||||
scope: { roots: [root] },
|
||||
mode: "create-only",
|
||||
refresh: true,
|
||||
quiet: format === "json",
|
||||
policy: "block",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw retainAgentError(
|
||||
error,
|
||||
"The YAML declaration was kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
`Scoped ${input.resourceType} create failed.`,
|
||||
);
|
||||
}
|
||||
const remoteId = await project.stateBackend.read(
|
||||
project.stateScope,
|
||||
(state) => state.getResource(root)?.remote_id,
|
||||
);
|
||||
const results = run.execution?.results ?? [];
|
||||
const failed = results.find((result) => result.status === "failed");
|
||||
const result = {
|
||||
resource: {
|
||||
type: input.resourceType,
|
||||
key: resourceKey,
|
||||
name: input.displayName,
|
||||
provider: project.provider,
|
||||
remote_id: remoteId,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: true,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
actions: run.planned.plan.actions,
|
||||
diagnostics: run.planned.plan.diagnostics,
|
||||
results,
|
||||
error: failed?.error,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Wrote ${project.configPath} with ${input.resourceType} key '${resourceKey}'.`);
|
||||
if (!failed) emitBare(`Created ${input.resourceType} '${input.displayName}'.`);
|
||||
}
|
||||
if (failed) {
|
||||
throw new BailianError(
|
||||
failed.error ?? `Scoped ${input.resourceType} create failed.`,
|
||||
ExitCode.GENERAL,
|
||||
"The YAML declaration was kept. Fix the provider error, then re-run the same create command.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function replaceConfigAtomically(
|
||||
configPath: string,
|
||||
expectedSource: string,
|
||||
nextSource: string,
|
||||
): Promise<void> {
|
||||
const destination = resolve(configPath);
|
||||
const currentSource = await readFile(destination, "utf8");
|
||||
if (currentSource !== expectedSource) {
|
||||
throw new BailianError(
|
||||
`${configPath} changed while resource create was being prepared.`,
|
||||
ExitCode.GENERAL,
|
||||
"Review the latest YAML and re-run the command; no file was overwritten.",
|
||||
);
|
||||
}
|
||||
const currentStat = await stat(destination);
|
||||
const temporary = resolve(dirname(destination), `.${basename(destination)}.${randomUUID()}.tmp`);
|
||||
await writeFile(temporary, nextSource, { flag: "wx", mode: currentStat.mode });
|
||||
try {
|
||||
await rename(temporary, destination);
|
||||
} catch (error) {
|
||||
await unlink(temporary).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMetadata(values: string[] | undefined): Record<string, string> | undefined {
|
||||
if (!values?.length) return undefined;
|
||||
const metadata: Record<string, string> = {};
|
||||
for (const entry of values) {
|
||||
const separator = entry.indexOf("=");
|
||||
if (separator <= 0) {
|
||||
throw new BailianError(
|
||||
`Invalid metadata '${entry}'.`,
|
||||
ExitCode.USAGE,
|
||||
"Use --metadata key=value.",
|
||||
);
|
||||
}
|
||||
const key = entry.slice(0, separator).trim();
|
||||
if (!key) throw new BailianError("Metadata key must not be empty.", ExitCode.USAGE);
|
||||
metadata[key] = entry.slice(separator + 1);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function parseJsonInputs(
|
||||
values: string[] | undefined,
|
||||
label: string,
|
||||
): Promise<unknown[]> {
|
||||
const parsed: unknown[] = [];
|
||||
for (const value of values ?? []) {
|
||||
const raw = value.startsWith("@") ? await readFile(resolve(value.slice(1)), "utf8") : value;
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new BailianError(
|
||||
`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(decoded)) parsed.push(...decoded);
|
||||
else parsed.push(decoded);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderPlan(
|
||||
actions: Array<{ action: string; address: ResourceAddress }>,
|
||||
diagnostics: Array<{ severity: string; code: string; message: string }>,
|
||||
): void {
|
||||
for (const diagnostic of diagnostics) {
|
||||
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
for (const action of actions.filter((entry) => entry.action !== "no-op")) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { CloudAgent } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const AGENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const AGENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const AGENT_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Agent ID", "zh-CN": "Agent ID" },
|
||||
},
|
||||
agentVersion: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Specific agent version", "zh-CN": "指定 Agent 版本" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const AGENT_VERSIONS_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
agentId: AGENT_GET_FLAGS.agentId,
|
||||
};
|
||||
|
||||
export function agentRows(agents: CloudAgent[]): string[][] {
|
||||
return agents.map((agent) => [
|
||||
agent.id,
|
||||
displayValue(agent.name),
|
||||
displayValue(agent.version),
|
||||
displayValue(agent.type),
|
||||
displayValue(agent.updated_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { dirname, extname, relative, sep } from "node:path";
|
||||
import {
|
||||
type BackendRuntimeInput,
|
||||
buildAgentDecl,
|
||||
type IStateManager,
|
||||
inspectSkillSource,
|
||||
planProjectWithStateBackend,
|
||||
type ResolvedProjectConfig,
|
||||
type ResourceAddress,
|
||||
type ResourceSyncRun,
|
||||
type SkillDecl,
|
||||
syncProjectResourcesWithStateBackend,
|
||||
} from "@openagentpack/sdk";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseDocument } from "yaml";
|
||||
import { formatResourceLabel } from "../_engine/address-utils.ts";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { retainAgentError, withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
type LoadedScopedCreateProject,
|
||||
loadScopedCreateProject,
|
||||
normalizeResourceKey,
|
||||
replaceConfigAtomically,
|
||||
resolveCandidateDeclaration,
|
||||
selectResourceKey,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
export { replaceConfigAtomically } from "../_engine/scoped-create.ts";
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Agent display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Agent 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
required: true,
|
||||
description: { "en-US": "Model ID", "zh-CN": "模型 ID" },
|
||||
},
|
||||
instructions: {
|
||||
type: "string",
|
||||
valueHint: "<text|path>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Inline instructions or a ./, ../, or absolute file path",
|
||||
"zh-CN": "内联指令,或以 ./、../、/ 开头的文件路径",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Agent description", "zh-CN": "Agent 描述" },
|
||||
},
|
||||
skill: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Existing remote Skill ID (repeatable)",
|
||||
"zh-CN": "已存在的远端 Skill ID(可重复)",
|
||||
},
|
||||
},
|
||||
skillDir: {
|
||||
type: "array",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Local Skill directory or ZIP to declare, upload, and attach through the same scoped create (repeatable)",
|
||||
"zh-CN": "通过同一次定向创建声明、上传并绑定的本地 Skill 目录或 ZIP(可重复)",
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
valueHint: "<custom|official>",
|
||||
choices: ["custom", "official"],
|
||||
description: {
|
||||
"en-US": "Type applied to every --skill value (default: custom)",
|
||||
"zh-CN": "应用于所有 --skill 的类型(默认:custom)",
|
||||
},
|
||||
},
|
||||
tool: {
|
||||
type: "array",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Builtin tool name (repeatable)",
|
||||
"zh-CN": "内置工具名称(可重复)",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type BuiltAgentDecl = ReturnType<typeof buildAgentDecl>["agent"];
|
||||
type AgentSkillType = "custom" | "official";
|
||||
type AgentSkillDecl = NonNullable<BuiltAgentDecl["skills"]>[number];
|
||||
|
||||
interface AgentKeySelection {
|
||||
key: string;
|
||||
reusedPending: boolean;
|
||||
}
|
||||
|
||||
interface LocalSkillKeySelection {
|
||||
key: string;
|
||||
needsCreate: boolean;
|
||||
reusedPending: boolean;
|
||||
reusedTracked: boolean;
|
||||
}
|
||||
|
||||
interface PreparedLocalSkill extends LocalSkillKeySelection {
|
||||
name: string;
|
||||
source: string;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
resolvedDeclaration: SkillDecl;
|
||||
}
|
||||
|
||||
export function normalizeAgentKey(displayName: string): string {
|
||||
return normalizeResourceKey(displayName, "agent");
|
||||
}
|
||||
|
||||
export function selectAgentKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
agents: Record<string, BuiltAgentDecl>;
|
||||
candidate: BuiltAgentDecl;
|
||||
state: IStateManager;
|
||||
}): AgentKeySelection {
|
||||
return selectResourceKey({
|
||||
displayName: options.displayName,
|
||||
provider: options.provider,
|
||||
resourceTypes: ["agent", "template"],
|
||||
declarations: options.agents as unknown as Record<string, Record<string, unknown>>,
|
||||
candidate: options.candidate as unknown as Record<string, unknown>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "agent",
|
||||
state: options.state,
|
||||
});
|
||||
}
|
||||
|
||||
function agentSkillType(value: string | undefined): AgentSkillType {
|
||||
if (value === undefined || value === "custom") return "custom";
|
||||
if (value === "official") return "official";
|
||||
throw new BailianError("--type must be either custom or official.", ExitCode.USAGE);
|
||||
}
|
||||
|
||||
export function buildAgentSkillRefs(
|
||||
skillIds: string[] | undefined,
|
||||
type: AgentSkillType = "custom",
|
||||
): NonNullable<BuiltAgentDecl["skills"]> {
|
||||
const seen = new Set<string>();
|
||||
const refs: NonNullable<BuiltAgentDecl["skills"]> = [];
|
||||
for (const rawId of skillIds ?? []) {
|
||||
const skillId = rawId.trim();
|
||||
if (!skillId) {
|
||||
throw new BailianError("--skill values must not be empty.", ExitCode.USAGE);
|
||||
}
|
||||
if (seen.has(skillId)) continue;
|
||||
seen.add(skillId);
|
||||
refs.push({ type, skill_id: skillId });
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
const normalize = (candidate: unknown): unknown => {
|
||||
if (Array.isArray(candidate)) return candidate.map(normalize);
|
||||
if (!candidate || typeof candidate !== "object") return candidate;
|
||||
return Object.fromEntries(
|
||||
Object.entries(candidate as Record<string, unknown>)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
||||
.map(([key, entry]) => [key, normalize(entry)]),
|
||||
);
|
||||
};
|
||||
return JSON.stringify(normalize(value));
|
||||
}
|
||||
|
||||
export function selectLocalSkillKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
declarations: Record<string, Record<string, unknown>>;
|
||||
candidate: Record<string, unknown>;
|
||||
state: IStateManager;
|
||||
}): LocalSkillKeySelection {
|
||||
const trackedKeys = new Set(
|
||||
options.state
|
||||
.listResources()
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.address.provider === options.provider && resource.address.type === "skill",
|
||||
)
|
||||
.map((resource) => resource.address.name),
|
||||
);
|
||||
const sameName = Object.entries(options.declarations).filter(([key, declaration]) => {
|
||||
const effectiveName = typeof declaration.name === "string" ? declaration.name : key;
|
||||
return effectiveName === options.displayName;
|
||||
});
|
||||
const matchingTracked = sameName.find(
|
||||
([key, declaration]) =>
|
||||
trackedKeys.has(key) && canonicalJson(declaration) === canonicalJson(options.candidate),
|
||||
);
|
||||
if (matchingTracked) {
|
||||
return {
|
||||
key: matchingTracked[0],
|
||||
needsCreate: false,
|
||||
reusedPending: false,
|
||||
reusedTracked: true,
|
||||
};
|
||||
}
|
||||
const differentTracked = sameName.find(([key]) => trackedKeys.has(key));
|
||||
if (differentTracked) {
|
||||
throw new BailianError(
|
||||
`Skill '${options.displayName}' is already tracked with a different declaration.`,
|
||||
ExitCode.USAGE,
|
||||
"Modify its existing YAML declaration and run full `bl managed-agent apply`, or bind its remote ID with --skill.",
|
||||
);
|
||||
}
|
||||
const selected = selectResourceKey({
|
||||
displayName: options.displayName,
|
||||
provider: options.provider,
|
||||
resourceTypes: ["skill"],
|
||||
declarations: options.declarations,
|
||||
candidate: options.candidate,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "skill",
|
||||
state: options.state,
|
||||
});
|
||||
return {
|
||||
...selected,
|
||||
needsCreate: true,
|
||||
reusedTracked: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareLocalSkillSources(
|
||||
project: LoadedScopedCreateProject,
|
||||
sources: string[] | undefined,
|
||||
): Promise<PreparedLocalSkill[]> {
|
||||
const declarations = {
|
||||
...((project.config.skills ?? {}) as unknown as Record<string, Record<string, unknown>>),
|
||||
};
|
||||
const preparedByPath = new Map<string, PreparedLocalSkill>();
|
||||
const preparedByKey = new Map<string, PreparedLocalSkill>();
|
||||
for (const rawSource of sources ?? []) {
|
||||
const requestedSource = rawSource.trim();
|
||||
if (!requestedSource) {
|
||||
throw new BailianError("--skill-dir values must not be empty.", ExitCode.USAGE);
|
||||
}
|
||||
const inspected = await withAgentErrors(() =>
|
||||
inspectSkillSource(requestedSource, { basePath: process.cwd() }),
|
||||
);
|
||||
const sourceStat = await stat(inspected.sourcePath);
|
||||
const supportedSource =
|
||||
sourceStat.isDirectory() ||
|
||||
(sourceStat.isFile() && extname(inspected.sourcePath).toLowerCase() === ".zip");
|
||||
if (!supportedSource) {
|
||||
throw new BailianError(
|
||||
`--skill-dir requires a Skill directory or .zip file: ${requestedSource}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (preparedByPath.has(inspected.sourcePath)) continue;
|
||||
const source =
|
||||
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
|
||||
const rawDeclaration: Record<string, unknown> = {
|
||||
name: inspected.name,
|
||||
source,
|
||||
origin: "custom",
|
||||
provider: project.provider,
|
||||
};
|
||||
const resolvedDeclaration = (await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "skills",
|
||||
rawDeclaration,
|
||||
})) as unknown as SkillDecl;
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectLocalSkillKey({
|
||||
displayName: inspected.name,
|
||||
provider: project.provider,
|
||||
declarations,
|
||||
candidate: resolvedDeclaration as unknown as Record<string, unknown>,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const existingPrepared = preparedByKey.get(keySelection.key);
|
||||
if (existingPrepared) {
|
||||
preparedByPath.set(inspected.sourcePath, existingPrepared);
|
||||
continue;
|
||||
}
|
||||
const prepared: PreparedLocalSkill = {
|
||||
...keySelection,
|
||||
name: inspected.name,
|
||||
source,
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
};
|
||||
declarations[keySelection.key] = resolvedDeclaration as unknown as Record<string, unknown>;
|
||||
preparedByPath.set(inspected.sourcePath, prepared);
|
||||
preparedByKey.set(keySelection.key, prepared);
|
||||
}
|
||||
return [...preparedByKey.values()];
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent through an isolated YAML apply",
|
||||
"zh-CN": "通过隔离的 YAML Apply 声明并创建一个托管 Agent",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> --model <model> --instructions <text|path> [--description <text>] [--skill <id>...] [--type custom|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]",
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
'--name assistant --model qwen3.8-max --instructions "You are helpful."',
|
||||
"--name assistant --model qwen3.8-max --instructions ./prompts/assistant.md --skill skill_abc --yes",
|
||||
"--name slides --model qwen3.8-max --instructions ./prompts/slides.md --skill skill_pptx --type official --yes",
|
||||
"--name reviewer --model qwen3.8-max --instructions ./prompts/reviewer.md --skill-dir ./skills/code-review --yes",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Without --yes, previews the generated YAML key and scoped plan. --dry-run stays offline. Unrelated resources are not refreshed or drift-checked.",
|
||||
"zh-CN":
|
||||
"不带 --yes 时预览自动生成的 YAML key 和定向计划;--dry-run 完全离线。无关资源不会刷新或检测 Drift。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--skill writes an external Skill reference directly into the Agent declaration. --type defaults to custom; use --type official for platform Skills. These Skills are not managed through the top-level skills map.",
|
||||
"zh-CN":
|
||||
"--skill 会把外部 Skill 引用直接写入 Agent 声明。--type 默认为 custom;平台 Skill 使用 --type official。这些 Skill 不通过顶层 skills 资源管理。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--skill-dir accepts a local Skill directory or ZIP, writes it as a top-level custom Skill declaration, and writes its generated YAML key into the Agent skills list. Skill and Agent are created together in dependency order. --type applies only to --skill IDs.",
|
||||
"zh-CN":
|
||||
"--skill-dir 接收本地 Skill 目录或 ZIP,将其写为顶层 custom Skill 声明,并把生成的 YAML key 写入 Agent 的 skills 列表;Skill 与 Agent 按依赖顺序一并创建。--type 只作用于 --skill ID。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (!flags.name.trim()) return "--name must not be empty.";
|
||||
if (!flags.model.trim()) return "--model must not be empty.";
|
||||
if (!flags.instructions.trim()) return "--instructions must not be empty.";
|
||||
if (flags.type && !flags.skill?.length) return "--type requires at least one --skill.";
|
||||
if (flags.skill?.some((skillId) => !skillId.trim())) {
|
||||
return "--skill values must not be empty.";
|
||||
}
|
||||
if (flags.skillDir?.some((source) => !source.trim())) {
|
||||
return "--skill-dir values must not be empty.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { flags, settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
const project = await loadScopedCreateProject(ctx, file);
|
||||
const provider = project.provider;
|
||||
const externalSkillRefs = buildAgentSkillRefs(flags.skill, agentSkillType(flags.type));
|
||||
const localSkills = await prepareLocalSkillSources(project, flags.skillDir);
|
||||
const skillRefs: AgentSkillDecl[] = [
|
||||
...externalSkillRefs,
|
||||
...localSkills.map((skill) => skill.key),
|
||||
];
|
||||
|
||||
const rawAgent = buildAgentDecl(undefined, {
|
||||
name: flags.name.trim(),
|
||||
description: flags.description,
|
||||
model: flags.model,
|
||||
instructions: flags.instructions,
|
||||
provider,
|
||||
builtinTools: flags.tool,
|
||||
}).agent;
|
||||
rawAgent.skills = skillRefs;
|
||||
const candidateConfig = structuredClone(project.config);
|
||||
candidateConfig.skills = { ...candidateConfig.skills };
|
||||
for (const skill of localSkills) {
|
||||
candidateConfig.skills[skill.key] = skill.resolvedDeclaration;
|
||||
}
|
||||
candidateConfig._resolved = true;
|
||||
const candidateAgent = (await resolveCandidateDeclaration({
|
||||
project: { ...project, config: candidateConfig },
|
||||
group: "agents",
|
||||
rawDeclaration: rawAgent as unknown as Record<string, unknown>,
|
||||
})) as unknown as BuiltAgentDecl;
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectAgentKey({
|
||||
displayName: flags.name.trim(),
|
||||
provider,
|
||||
agents: (project.config.agents ?? {}) as Record<string, BuiltAgentDecl>,
|
||||
candidate: candidateAgent,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const agentKey = keySelection.key;
|
||||
candidateConfig.agents = { ...candidateConfig.agents, [agentKey]: candidateAgent };
|
||||
candidateConfig._resolved = true;
|
||||
|
||||
const document = parseDocument(project.source);
|
||||
if (document.errors.length > 0) {
|
||||
throw new BailianError(
|
||||
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
for (const skill of localSkills) {
|
||||
document.setIn(["skills", skill.key], skill.rawDeclaration);
|
||||
}
|
||||
document.setIn(["agents", agentKey], rawAgent);
|
||||
const nextSource = document.toString();
|
||||
const agentRoot: ResourceAddress = { type: "agent", name: agentKey, provider };
|
||||
const roots: ResourceAddress[] = [
|
||||
...localSkills
|
||||
.filter((skill) => skill.needsCreate)
|
||||
.map((skill) => ({ type: "skill" as const, name: skill.key, provider })),
|
||||
agentRoot,
|
||||
];
|
||||
const backendInput: BackendRuntimeInput = {
|
||||
projectName: project.projectName,
|
||||
config: candidateConfig as ResolvedProjectConfig,
|
||||
configPath: project.configPath,
|
||||
providers: { [provider]: candidateConfig.providers[provider] },
|
||||
stateBackend: project.stateBackend,
|
||||
stateScope: project.stateScope,
|
||||
};
|
||||
|
||||
if (settings.dryRun || !flags.yes) {
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectWithStateBackend(backendInput, {
|
||||
provider,
|
||||
scope: { roots },
|
||||
refresh: !settings.dryRun,
|
||||
quiet: format === "json",
|
||||
mode: "create-only",
|
||||
}),
|
||||
),
|
||||
);
|
||||
const readyToCreate = !planned.plan.diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
const result = {
|
||||
agent: { key: agentKey, name: flags.name.trim(), provider, skills: skillRefs },
|
||||
local_skills: localSkills.map((skill) => ({
|
||||
key: skill.key,
|
||||
name: skill.name,
|
||||
source: skill.source,
|
||||
reused_pending: skill.reusedPending,
|
||||
reused_tracked: skill.reusedTracked,
|
||||
})),
|
||||
config_file: project.configPath,
|
||||
yaml_written: false,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
requires_confirmation: !settings.dryRun,
|
||||
ready_to_create: readyToCreate,
|
||||
actions: planned.plan.actions,
|
||||
diagnostics: planned.plan.diagnostics,
|
||||
};
|
||||
if (format === "json") {
|
||||
emitResult(result, format);
|
||||
} else {
|
||||
for (const skill of localSkills) {
|
||||
emitBare(`Generated Skill YAML key: ${skill.key}`);
|
||||
}
|
||||
emitBare(`Generated Agent YAML key: ${agentKey}`);
|
||||
for (const diagnostic of planned.plan.diagnostics) {
|
||||
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
for (const action of planned.plan.actions.filter((entry) => entry.action !== "no-op")) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
emitBare(
|
||||
settings.dryRun
|
||||
? "Dry run: YAML, State, and remote resources were not changed."
|
||||
: "Preview only: re-run with --yes to write YAML and create this Agent.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceConfigAtomically(project.configPath, project.source, nextSource);
|
||||
let run: ResourceSyncRun;
|
||||
try {
|
||||
run = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
syncProjectResourcesWithStateBackend(backendInput, {
|
||||
provider,
|
||||
scope: { roots },
|
||||
refresh: true,
|
||||
quiet: format === "json",
|
||||
mode: "create-only",
|
||||
policy: "block",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw retainAgentError(
|
||||
error,
|
||||
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
"Scoped Agent and Skill create failed.",
|
||||
);
|
||||
}
|
||||
const remoteResources = await project.stateBackend.read(project.stateScope, (state) => ({
|
||||
agentId: state.getResource(agentRoot)?.remote_id,
|
||||
skillIds: Object.fromEntries(
|
||||
localSkills.map((skill) => [
|
||||
skill.key,
|
||||
state.getResource({ type: "skill", name: skill.key, provider })?.remote_id,
|
||||
]),
|
||||
),
|
||||
}));
|
||||
const results = run.execution?.results ?? [];
|
||||
const failed = results.find((entry) => entry.status === "failed");
|
||||
const status = failed ? "failed" : "completed";
|
||||
const result = {
|
||||
agent: {
|
||||
key: agentKey,
|
||||
name: flags.name.trim(),
|
||||
provider,
|
||||
remote_id: remoteResources.agentId,
|
||||
skills: skillRefs,
|
||||
},
|
||||
local_skills: localSkills.map((skill) => ({
|
||||
key: skill.key,
|
||||
name: skill.name,
|
||||
source: skill.source,
|
||||
remote_id: remoteResources.skillIds[skill.key],
|
||||
reused_pending: skill.reusedPending,
|
||||
reused_tracked: skill.reusedTracked,
|
||||
})),
|
||||
config_file: project.configPath,
|
||||
yaml_written: true,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
status,
|
||||
actions: run.planned.plan.actions,
|
||||
diagnostics: run.planned.plan.diagnostics,
|
||||
results,
|
||||
error: failed?.error,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(
|
||||
`Wrote ${project.configPath} with Agent key '${agentKey}'${localSkills.length ? ` and ${localSkills.length} local Skill declaration(s)` : ""}.`,
|
||||
);
|
||||
if (status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`);
|
||||
else emitBare(`Scoped create ${status}: ${failed?.error ?? "unknown error"}`);
|
||||
}
|
||||
if (failed) {
|
||||
throw new BailianError(
|
||||
failed.error ?? "Scoped Agent create failed.",
|
||||
ExitCode.GENERAL,
|
||||
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getRemoteAgent } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { AGENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Get a Managed Agent", "zh-CN": "获取托管 Agent 详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [--agent-version <n>] [--file <path>]",
|
||||
flags: AGENT_GET_FLAGS,
|
||||
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --agent-version 3 --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: (flags) =>
|
||||
flags.agentVersion !== undefined &&
|
||||
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
|
||||
? "--agent-version must be a positive integer."
|
||||
: undefined,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const agent = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteAgent(runtime, ctx.flags.agentId, {
|
||||
provider: "bailian",
|
||||
version: ctx.flags.agentVersion,
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(agent, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${agent.id}`);
|
||||
emitBare(`Name: ${displayValue(agent.name)}`);
|
||||
emitBare(`Description: ${displayValue(agent.description, 120)}`);
|
||||
emitBare(`Version: ${displayValue(agent.version)}`);
|
||||
emitBare(`Type: ${displayValue(agent.type)}`);
|
||||
emitBare(`Created: ${displayValue(agent.created_at)}`);
|
||||
emitBare(`Updated: ${displayValue(agent.updated_at)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { listRemoteAgents } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { AGENT_LIST_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agents", "zh-CN": "列出托管 Agent" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived] [--file <path>]",
|
||||
flags: AGENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--limit 50", "--all --include-archived --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgents(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "agents",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No agents found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { listRemoteAgents } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import {
|
||||
emitCollection,
|
||||
matchesQuery,
|
||||
searchCursorPages,
|
||||
validateLimitAndPageLimit,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { AGENT_SEARCH_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Search Managed Agents", "zh-CN": "搜索托管 Agent" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
|
||||
flags: AGENT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query assistant", "--query code --page-limit 20 --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return searchCursorPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgents(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit ?? 100,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
(agent) => matchesQuery(ctx.flags.query, agent.id, agent.name, agent.description),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "agents",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching agents found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { listRemoteAgentVersions } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { AGENT_VERSIONS_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent versions", "zh-CN": "列出托管 Agent 版本" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: AGENT_VERSIONS_FLAGS,
|
||||
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgentVersions(runtime, ctx.flags.agentId, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "versions",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No agent versions found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import {
|
||||
assertProviderConfigured,
|
||||
buildAgentRuntime,
|
||||
CREDENTIALS_NOTE,
|
||||
} from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
|
||||
const APPLY_FLAGS = {
|
||||
@@ -26,21 +22,6 @@ const APPLY_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Target provider (default: all configured)",
|
||||
"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,9 +45,17 @@ 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>] [--concurrency <n>]",
|
||||
flags: APPLY_FLAGS,
|
||||
exampleArgs: ["--yes", "--provider bailian --yes"],
|
||||
exampleArgs: ["--yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
@@ -77,7 +66,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_apply: {
|
||||
provider: flags.provider ?? "all",
|
||||
provider: "bailian",
|
||||
refresh: !flags.noRefresh,
|
||||
concurrency: flags.concurrency,
|
||||
},
|
||||
@@ -92,9 +81,8 @@ export default defineCommand({
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
assertProviderConfigured(runtime, flags.provider);
|
||||
return planProjectContext(runtime, {
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
refresh: !flags.noRefresh,
|
||||
quiet: true,
|
||||
onFeedback: renderAgentFeedback,
|
||||
@@ -109,11 +97,20 @@ export default defineCommand({
|
||||
if (format === "json") process.stderr.write(`${line}\n`);
|
||||
else emitBare(line);
|
||||
};
|
||||
if (plan.diagnostics.some((diag) => diag.severity === "error")) {
|
||||
for (const diag of plan.diagnostics) {
|
||||
if (diag.severity === "error") emitProgress(`[error] ${diag.code}: ${diag.message}`);
|
||||
const errorDiagnostics = plan.diagnostics.filter(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
if (errorDiagnostics.length > 0) {
|
||||
for (const diagnostic of errorDiagnostics) {
|
||||
emitProgress(`[error] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
throw new BailianError("Cannot apply: resolve the errors above first.", ExitCode.GENERAL);
|
||||
throw new BailianError(
|
||||
formatAgentDiagnosticFailure(
|
||||
errorDiagnostics,
|
||||
"Cannot apply: resolve the errors above first.",
|
||||
),
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const actionable = plan.actions.filter((action) => action.action !== "no-op");
|
||||
@@ -124,23 +121,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, {
|
||||
@@ -152,7 +137,8 @@ export default defineCommand({
|
||||
);
|
||||
|
||||
const succeeded = result.results.filter((entry) => entry.status === "success").length;
|
||||
const failed = result.results.filter((entry) => entry.status === "failed").length;
|
||||
const failedResults = result.results.filter((entry) => entry.status === "failed");
|
||||
const failed = failedResults.length;
|
||||
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
|
||||
|
||||
if (format === "json") {
|
||||
@@ -161,6 +147,9 @@ export default defineCommand({
|
||||
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
if (failed > 0) throw new BailianError("Apply failed.", ExitCode.GENERAL);
|
||||
if (failed > 0) {
|
||||
const firstFailure = failedResults.find((entry) => entry.error);
|
||||
throw new BailianError(firstFailure?.error ?? "Apply failed.", ExitCode.GENERAL);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { setRemoteDeploymentPaused } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
resolveDeploymentTarget,
|
||||
validateDeploymentActionTarget,
|
||||
} from "./_shared.ts";
|
||||
|
||||
export function createSetDeploymentPausedCommand(paused: boolean) {
|
||||
return defineCommand({
|
||||
description: paused
|
||||
? { "en-US": "Pause a Managed Agent deployment", "zh-CN": "暂停托管 Agent Deployment" }
|
||||
: { "en-US": "Unpause a Managed Agent deployment", "zh-CN": "恢复托管 Agent Deployment" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: paused
|
||||
? {
|
||||
"en-US":
|
||||
"This pauses the specified Managed Agent deployment and stops its scheduled executions until resumed.",
|
||||
"zh-CN": "该操作会暂停指定的托管 Agent Deployment,并停止其定时执行直至恢复。",
|
||||
}
|
||||
: {
|
||||
"en-US":
|
||||
"This resumes the specified Managed Agent deployment and may restart scheduled executions and related usage.",
|
||||
"zh-CN":
|
||||
"该操作会恢复指定的托管 Agent Deployment,可能重新开始定时执行并产生相关用量。",
|
||||
},
|
||||
},
|
||||
usageArgs: "(--deployment <name> | --deployment-id <id>)",
|
||||
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc --yes`],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateDeploymentActionTarget,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
[paused ? "would_pause_deployment" : "would_unpause_deployment"]:
|
||||
ctx.flags.deploymentId ?? ctx.flags.deployment,
|
||||
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
|
||||
return {
|
||||
deploymentId,
|
||||
deployment: await setRemoteDeploymentPaused(runtime, deploymentId, paused, {
|
||||
provider: "bailian",
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format);
|
||||
} else {
|
||||
emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { DeploymentInfo, DeploymentRunInfo, ProjectRuntimeContext } from "@openagentpack/sdk";
|
||||
import { UserError } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const DEPLOYMENT_ID_FLAG = {
|
||||
deploymentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Deployment ID", "zh-CN": "Deployment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const DEPLOYMENT_LIST_FILTER_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Filter by agent ID", "zh-CN": "按 Agent ID 筛选" },
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
valueHint: "<status>",
|
||||
choices: ["active", "paused"] as const,
|
||||
description: { "en-US": "Filter by deployment status", "zh-CN": "按 Deployment 状态筛选" },
|
||||
},
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
createdAtGte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or after this timestamp",
|
||||
"zh-CN": "创建时间不早于该时间戳",
|
||||
},
|
||||
},
|
||||
createdAtLte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or before this timestamp",
|
||||
"zh-CN": "创建时间不晚于该时间戳",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEPLOYMENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...DEPLOYMENT_LIST_FILTER_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_GET_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG };
|
||||
|
||||
export const DEPLOYMENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
query: SEARCH_FLAGS.query,
|
||||
...DEPLOYMENT_LIST_FILTER_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_RUNS_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...DEPLOYMENT_ID_FLAG,
|
||||
...CURSOR_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_RUNS_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
runId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Deployment run ID", "zh-CN": "Deployment Run ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEPLOYMENT_ACTION_TARGET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
deployment: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Logical deployment name in agents.yaml/state",
|
||||
"zh-CN": "agents.yaml/State 中的逻辑 Deployment 名称",
|
||||
},
|
||||
},
|
||||
deploymentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Direct deployment ID", "zh-CN": "直接指定 Deployment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function validateDeploymentActionTarget(flags: {
|
||||
deployment?: string;
|
||||
deploymentId?: string;
|
||||
}): string | undefined {
|
||||
if (Boolean(flags.deployment) === Boolean(flags.deploymentId)) {
|
||||
return "Provide exactly one of --deployment or --deployment-id.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function deploymentRows(deployments: DeploymentInfo[]): string[][] {
|
||||
return deployments.map((deployment) => [
|
||||
displayValue(deployment.id),
|
||||
deployment.status,
|
||||
displayValue(deployment.schedule?.expression),
|
||||
displayValue(deployment.paused_reason?.type),
|
||||
]);
|
||||
}
|
||||
|
||||
export function deploymentRunRows(runs: DeploymentRunInfo[]): string[][] {
|
||||
return runs.map((run) => [
|
||||
run.id,
|
||||
displayValue(run.deployment_id),
|
||||
displayValue(run.session_id),
|
||||
displayValue(run.status),
|
||||
displayValue(run.created_at),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resolveDeploymentTarget(
|
||||
runtime: ProjectRuntimeContext,
|
||||
options: { deployment?: string; deploymentId?: string },
|
||||
): Promise<string> {
|
||||
if (options.deploymentId) return options.deploymentId;
|
||||
const state = runtime.state.getResource({
|
||||
provider: "bailian",
|
||||
type: "deployment",
|
||||
name: options.deployment!,
|
||||
});
|
||||
if (!state?.remote_id) {
|
||||
throw new UserError(
|
||||
`Deployment '${options.deployment}' is not tracked in state. Use --deployment-id or run managed-agent apply/import first.`,
|
||||
);
|
||||
}
|
||||
return state.remote_id;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { DeploymentDecl } from "@openagentpack/sdk";
|
||||
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
parseJsonInputs,
|
||||
parseMetadata,
|
||||
resolveCandidateDeclaration,
|
||||
runScopedTopLevelCreate,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Deployment display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Deployment 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<yaml-key>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Existing Agent key from agents.yaml",
|
||||
"zh-CN": "agents.yaml 中已有的 Agent key",
|
||||
},
|
||||
},
|
||||
agentVersion: {
|
||||
type: "number",
|
||||
valueHint: "<number>",
|
||||
description: { "en-US": "Agent version", "zh-CN": "Agent 版本" },
|
||||
},
|
||||
environment: {
|
||||
type: "string",
|
||||
valueHint: "<yaml-key>",
|
||||
description: {
|
||||
"en-US": "Existing Environment key from agents.yaml",
|
||||
"zh-CN": "agents.yaml 中已有的 Environment key",
|
||||
},
|
||||
},
|
||||
vault: {
|
||||
type: "array",
|
||||
valueHint: "<yaml-key>",
|
||||
description: {
|
||||
"en-US": "Existing Vault key from agents.yaml (repeatable)",
|
||||
"zh-CN": "agents.yaml 中已有的 Vault key(可重复)",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: "array",
|
||||
valueHint: "<text>",
|
||||
description: {
|
||||
"en-US": "Initial user message (repeatable)",
|
||||
"zh-CN": "初始用户消息(可重复)",
|
||||
},
|
||||
},
|
||||
event: {
|
||||
type: "array",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "Initial user.message or system.message JSON (repeatable)",
|
||||
"zh-CN": "初始 user.message 或 system.message JSON(可重复)",
|
||||
},
|
||||
},
|
||||
resource: {
|
||||
type: "array",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "File Resource JSON with source or file_id (repeatable)",
|
||||
"zh-CN": "包含 source 或 file_id 的 File Resource JSON(可重复)",
|
||||
},
|
||||
},
|
||||
schedule: {
|
||||
type: "string",
|
||||
valueHint: "<cron>",
|
||||
description: { "en-US": "Five-field cron expression", "zh-CN": "五段式 Cron 表达式" },
|
||||
},
|
||||
timezone: {
|
||||
type: "string",
|
||||
valueHint: "<timezone>",
|
||||
description: { "en-US": "IANA schedule timezone", "zh-CN": "Schedule 的 IANA 时区" },
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Deployment description", "zh-CN": "Deployment 描述" },
|
||||
},
|
||||
metadata: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Metadata entry (repeatable)",
|
||||
"zh-CN": "Metadata 条目(可重复)",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent Deployment through a scoped YAML apply",
|
||||
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> --agent <yaml-key> (--message <text>... | --event <json|@path>...) [--agent-version <number>] [--environment <yaml-key>] [--vault <yaml-key>...] [--resource <json|@path>...] [--schedule <cron> --timezone <timezone>] [--description <text>] [--metadata <key=value>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: [
|
||||
{
|
||||
"en-US": '--name Daily --agent assistant --message "Generate the report"',
|
||||
"zh-CN": '--name Daily --agent assistant --message "生成报告"',
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
'--name Daily --agent assistant --event \'{"type":"system.message","content":"Be concise"}\' --yes',
|
||||
"zh-CN":
|
||||
'--name Daily --agent assistant --event \'{"type":"system.message","content":"保持简洁"}\' --yes',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release.",
|
||||
"zh-CN":
|
||||
"Initial Events 必须包含 1–50 条 user.message/system.message;本期 --resource 仅接受 File Resource。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Use either repeatable --message values or repeatable --event values; the two input forms cannot be mixed.",
|
||||
"zh-CN": "重复使用 --message 或重复使用 --event;两种输入形式不能混用。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (!flags.name.trim()) return "--name must not be empty.";
|
||||
if (!flags.agent.trim()) return "--agent must not be empty.";
|
||||
if (flags.message?.length && flags.event?.length) {
|
||||
return "--message and --event cannot be used together.";
|
||||
}
|
||||
if (!flags.message?.length && !flags.event?.length)
|
||||
return "Pass at least one --message or --event.";
|
||||
if (Boolean(flags.schedule) !== Boolean(flags.timezone)) {
|
||||
return "--schedule and --timezone must be provided together.";
|
||||
}
|
||||
if (
|
||||
flags.agentVersion !== undefined &&
|
||||
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
|
||||
) {
|
||||
return "--agent-version must be a positive integer.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const initialEvents = [
|
||||
...(ctx.flags.message ?? []).map((content) => ({ type: "user.message" as const, content })),
|
||||
...validateEvents(await parseJsonInputs(ctx.flags.event, "event")),
|
||||
];
|
||||
if (initialEvents.length < 1 || initialEvents.length > 50) {
|
||||
throw new BailianError("Initial Events must contain 1-50 entries.", ExitCode.USAGE);
|
||||
}
|
||||
const resources = validateResources(await parseJsonInputs(ctx.flags.resource, "resource"));
|
||||
if (ctx.flags.schedule && ctx.flags.schedule.trim().split(/\s+/).length !== 5) {
|
||||
throw new BailianError("--schedule must be a five-field cron expression.", ExitCode.USAGE);
|
||||
}
|
||||
if (ctx.flags.timezone) validateTimezone(ctx.flags.timezone);
|
||||
|
||||
const rawDeclaration: DeploymentDecl = {
|
||||
name: ctx.flags.name.trim(),
|
||||
agent: ctx.flags.agent,
|
||||
agent_version: ctx.flags.agentVersion,
|
||||
environment: ctx.flags.environment,
|
||||
vaults: ctx.flags.vault,
|
||||
resources,
|
||||
initial_events: initialEvents,
|
||||
schedule:
|
||||
ctx.flags.schedule && ctx.flags.timezone
|
||||
? { expression: ctx.flags.schedule, timezone: ctx.flags.timezone }
|
||||
: undefined,
|
||||
description: ctx.flags.description,
|
||||
provider: project.provider,
|
||||
metadata: parseMetadata(ctx.flags.metadata),
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "deployments",
|
||||
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "deployments",
|
||||
resourceType: "deployment",
|
||||
displayName: ctx.flags.name.trim(),
|
||||
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: (project.config.deployments ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "deployment",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function validateEvents(
|
||||
values: unknown[],
|
||||
): Array<{ type: "user.message" | "system.message"; content: string }> {
|
||||
return values.map((value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new BailianError("Each --event must be a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
const event = value as Record<string, unknown>;
|
||||
if (event.type !== "user.message" && event.type !== "system.message") {
|
||||
throw new BailianError(
|
||||
"--event type must be user.message or system.message.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (typeof event.content !== "string" || !event.content.trim()) {
|
||||
throw new BailianError("--event content must be a non-empty string.", ExitCode.USAGE);
|
||||
}
|
||||
return { type: event.type, content: event.content };
|
||||
});
|
||||
}
|
||||
|
||||
function validateResources(values: unknown[]): NonNullable<DeploymentDecl["resources"]> {
|
||||
return values.map((value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new BailianError("Each --resource must be a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
const resource = value as Record<string, unknown>;
|
||||
if (resource.type !== "file") {
|
||||
throw new BailianError("--resource currently accepts only type=file.", ExitCode.USAGE);
|
||||
}
|
||||
const source = typeof resource.source === "string" ? resource.source : undefined;
|
||||
const fileId = typeof resource.file_id === "string" ? resource.file_id : undefined;
|
||||
if (Boolean(source) === Boolean(fileId)) {
|
||||
throw new BailianError(
|
||||
"A File Resource must provide exactly one of source or file_id.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (resource.mount_path !== undefined && typeof resource.mount_path !== "string") {
|
||||
throw new BailianError("File Resource mount_path must be a string.", ExitCode.USAGE);
|
||||
}
|
||||
return {
|
||||
type: "file" as const,
|
||||
source,
|
||||
file_id: fileId,
|
||||
mount_path: resource.mount_path as string | undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function validateTimezone(timezone: string): void {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
||||
} catch {
|
||||
throw new BailianError(`Invalid IANA timezone: ${timezone}`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRemoteDeployment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { DEPLOYMENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent deployment",
|
||||
"zh-CN": "获取托管 Agent Deployment 详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployment-id <id>",
|
||||
flags: DEPLOYMENT_GET_FLAGS,
|
||||
exampleArgs: ["--deployment-id dep_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const deployment = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteDeployment(runtime, ctx.flags.deploymentId, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(deployment, format);
|
||||
else {
|
||||
emitBare(`ID: ${displayValue(deployment.id)}`);
|
||||
emitBare(`Status: ${deployment.status}`);
|
||||
emitBare(`Schedule:${displayValue(deployment.schedule?.expression)}`);
|
||||
emitBare(`Paused: ${displayValue(deployment.paused_reason)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { listRemoteDeployments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_LIST_FLAGS, deploymentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent deployments", "zh-CN": "列出托管 Agent Deployment" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--agent-id <id>] [--status active|paused] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--status active --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeployments(runtime, {
|
||||
provider: "bailian",
|
||||
agent_id: ctx.flags.agentId,
|
||||
status: ctx.flags.status,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.deployments,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "deployments",
|
||||
items: result.items,
|
||||
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
|
||||
rows: deploymentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No deployments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
|
||||
|
||||
export default createSetDeploymentPausedCommand(true);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { runRemoteDeployment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
resolveDeploymentTarget,
|
||||
validateDeploymentActionTarget,
|
||||
} from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Run a Managed Agent deployment now",
|
||||
"zh-CN": "立即运行托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This immediately starts a run for the specified Managed Agent deployment and may incur usage or trigger configured actions.",
|
||||
"zh-CN": "该操作会立即运行指定的托管 Agent Deployment,可能产生用量或触发已配置的动作。",
|
||||
},
|
||||
},
|
||||
usageArgs: "(--deployment <name> | --deployment-id <id>)",
|
||||
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateDeploymentActionTarget,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_run_deployment: ctx.flags.deploymentId ?? ctx.flags.deployment,
|
||||
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
|
||||
return {
|
||||
deploymentId,
|
||||
run: await runRemoteDeployment(runtime, deploymentId, { provider: "bailian" }),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (format === "json")
|
||||
emitResult({ deployment_id: result.deploymentId, ...result.run }, format);
|
||||
else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRemoteDeploymentRun } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../../_engine/errors.ts";
|
||||
import { DEPLOYMENT_RUNS_GET_FLAGS } from "../_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent deployment run",
|
||||
"zh-CN": "获取托管 Agent Deployment Run 详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--run-id <id>",
|
||||
flags: DEPLOYMENT_RUNS_GET_FLAGS,
|
||||
exampleArgs: ["--run-id run_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const run = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteDeploymentRun(runtime, ctx.flags.runId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(run, format);
|
||||
else {
|
||||
emitBare(`ID: ${run.id}`);
|
||||
emitBare(`Deployment: ${displayValue(run.deployment_id)}`);
|
||||
emitBare(`Session: ${displayValue(run.session_id)}`);
|
||||
emitBare(`Status: ${displayValue(run.status)}`);
|
||||
emitBare(`Created: ${displayValue(run.created_at)}`);
|
||||
emitBare(`Error: ${displayValue(run.error)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { listRemoteDeploymentRuns } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_RUNS_LIST_FLAGS, deploymentRunRows } from "../_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "List runs for a Managed Agent deployment",
|
||||
"zh-CN": "列出托管 Agent Deployment Run",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployment-id <id> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_RUNS_LIST_FLAGS,
|
||||
exampleArgs: ["--deployment-id dep_abc", "--deployment-id dep_abc --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeploymentRuns(runtime, ctx.flags.deploymentId, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "runs",
|
||||
items: result.items,
|
||||
headers: ["ID", "DEPLOYMENT", "SESSION", "STATUS", "CREATED"],
|
||||
rows: deploymentRunRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No deployment runs found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { listRemoteDeployments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_SEARCH_FLAGS, deploymentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Search Managed Agent deployments",
|
||||
"zh-CN": "搜索托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query report", "--query nightly --all --output json"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US": "Deployment search maps --query to the provider's server-side keyword parameter.",
|
||||
"zh-CN": "Deployment 搜索会把 --query 映射为 Provider 服务端 keyword 参数。",
|
||||
},
|
||||
],
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeployments(runtime, {
|
||||
provider: "bailian",
|
||||
keyword: ctx.flags.query,
|
||||
agent_id: ctx.flags.agentId,
|
||||
status: ctx.flags.status,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.deployments,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "deployments",
|
||||
items: result.items,
|
||||
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
|
||||
rows: deploymentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No matching deployments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
|
||||
|
||||
export default createSetDeploymentPausedCommand(false);
|
||||
@@ -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,
|
||||
@@ -67,7 +68,16 @@ export default defineCommand({
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return planDestroyProjectContext(runtime);
|
||||
const planned = planDestroyProjectContext(runtime);
|
||||
return {
|
||||
...planned,
|
||||
resources: planned.resources.filter(
|
||||
(resource) => resource.address.provider === "bailian",
|
||||
),
|
||||
defaultMemoryStores: (planned.defaultMemoryStores ?? []).filter(
|
||||
(memoryStore) => memoryStore.provider === "bailian",
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -86,14 +96,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, {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { CloudEnvironment } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const ENVIRONMENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
environmentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Environment ID", "zh-CN": "Environment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function environmentRows(environments: CloudEnvironment[]): string[][] {
|
||||
return environments.map((environment) => [
|
||||
environment.id,
|
||||
displayValue(environment.name),
|
||||
displayValue(environment.scope),
|
||||
displayValue(environment.version),
|
||||
displayValue(environment.updated_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { defineCommand, type FlagsDef } from "bailian-cli-core";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
parseMetadata,
|
||||
resolveCandidateDeclaration,
|
||||
runScopedTopLevelCreate,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Environment display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Environment 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Environment description", "zh-CN": "Environment 描述" },
|
||||
},
|
||||
metadata: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Metadata entry (repeatable)",
|
||||
"zh-CN": "Metadata 条目(可重复)",
|
||||
},
|
||||
},
|
||||
apt: packageFlag("APT"),
|
||||
pip: packageFlag("pip"),
|
||||
npm: packageFlag("npm"),
|
||||
cargo: packageFlag("Cargo"),
|
||||
gem: packageFlag("Ruby gem"),
|
||||
go: packageFlag("Go"),
|
||||
file: fileFlag(),
|
||||
yes: yesFlag(),
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent Environment through a scoped YAML apply",
|
||||
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Environment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> [--description <text>] [--metadata <key=value>...] [--apt <package>...] [--pip <package>...] [--npm <package>...] [--cargo <package>...] [--gem <package>...] [--go <package>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: [
|
||||
"--name Development",
|
||||
"--name Development --pip pandas --npm typescript --metadata owner=platform --yes",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Creates a cloud Environment with unrestricted networking. Without --yes, only previews the generated YAML key and scoped plan.",
|
||||
"zh-CN":
|
||||
"创建使用 unrestricted 网络的 cloud Environment。不带 --yes 时仅预览自动生成的 YAML key 和定向计划。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
|
||||
async run(ctx) {
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const packages = Object.fromEntries(
|
||||
["apt", "pip", "npm", "cargo", "gem", "go"]
|
||||
.map((manager) => [manager, ctx.flags[manager as keyof typeof ctx.flags]])
|
||||
.filter((entry) => Array.isArray(entry[1]) && entry[1].length > 0),
|
||||
);
|
||||
const rawDeclaration: Record<string, unknown> = {
|
||||
name: ctx.flags.name.trim(),
|
||||
description: ctx.flags.description,
|
||||
provider: project.provider,
|
||||
config: {
|
||||
type: "cloud",
|
||||
networking: { type: "unrestricted" },
|
||||
...(Object.keys(packages).length > 0 ? { packages } : {}),
|
||||
},
|
||||
metadata: parseMetadata(ctx.flags.metadata),
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "environments",
|
||||
rawDeclaration,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "environments",
|
||||
resourceType: "environment",
|
||||
displayName: ctx.flags.name.trim(),
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: (project.config.environments ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "environment",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function packageFlag(manager: string) {
|
||||
return {
|
||||
type: "array" as const,
|
||||
valueHint: "<package>",
|
||||
description: {
|
||||
"en-US": `${manager} package (repeatable)`,
|
||||
"zh-CN": `${manager} 软件包(可重复)`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fileFlag() {
|
||||
return {
|
||||
type: "string" as const,
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function yesFlag() {
|
||||
return {
|
||||
type: "switch" as const,
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getRemoteEnvironment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { ENVIRONMENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent environment",
|
||||
"zh-CN": "获取托管 Agent 环境详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--environment-id <id>",
|
||||
flags: ENVIRONMENT_GET_FLAGS,
|
||||
exampleArgs: ["--environment-id env_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const environment = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteEnvironment(runtime, ctx.flags.environmentId, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(environment, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${environment.id}`);
|
||||
emitBare(`Name: ${displayValue(environment.name)}`);
|
||||
emitBare(`Description: ${displayValue(environment.description, 120)}`);
|
||||
emitBare(`Scope: ${displayValue(environment.scope)}`);
|
||||
emitBare(`Version: ${displayValue(environment.version)}`);
|
||||
emitBare(`Updated: ${displayValue(environment.updated_at)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { listRemoteEnvironments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { ENVIRONMENT_LIST_FLAGS, environmentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
|
||||
flags: ENVIRONMENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteEnvironments(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "environments",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
|
||||
rows: environmentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No environments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user