mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 241345d7f1 | |||
| 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 | |||
| e53daf05eb | |||
| 449eede237 | |||
| 2fbb6efe67 | |||
| 47e9bcc634 | |||
| 4c7ef0c7a5 | |||
| 9469556671 | |||
| b93e0d586d | |||
| 69c1fb9f08 |
@@ -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)。
|
||||
|
||||
约定:
|
||||
|
||||
|
||||
@@ -6,6 +6,92 @@ 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
|
||||
|
||||
@@ -6,6 +6,92 @@
|
||||
|
||||
[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
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -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 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
@@ -62,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 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
|
||||
@@ -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. 鉴权 / 全局选项
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ 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) 一并核对
|
||||
|
||||
@@ -30,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` |
|
||||
|
||||
@@ -41,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. 后端渠道参数
|
||||
|
||||
@@ -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 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.19.0",
|
||||
"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",
|
||||
|
||||
@@ -70,6 +70,11 @@ import {
|
||||
searchWeb,
|
||||
speechSynthesize,
|
||||
speechRecognize,
|
||||
speechVocabularyCreate,
|
||||
speechVocabularyList,
|
||||
speechVocabularyGet,
|
||||
speechVocabularyUpdate,
|
||||
speechVocabularyDelete,
|
||||
fileUpload,
|
||||
consoleCall,
|
||||
usageFree,
|
||||
@@ -125,6 +130,7 @@ import {
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
tokenPlanHarnessQuota,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -140,6 +146,18 @@ import {
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
managedAgentPlayground,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectWorkbench,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
managedAgentStateRm,
|
||||
@@ -152,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.
|
||||
@@ -232,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,
|
||||
@@ -287,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,
|
||||
@@ -302,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,
|
||||
@@ -314,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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,18 @@ 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("every generated high-risk command reference requires user confirmation before --yes", () => {
|
||||
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;
|
||||
@@ -27,9 +36,21 @@ test("every generated high-risk command reference requires user confirmation bef
|
||||
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
|
||||
|
||||
for (const commandSection of commandSections) {
|
||||
if (!commandSection.includes("`--yes`")) continue;
|
||||
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(commandSection).toMatch(/\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/);
|
||||
expect(hasConfirmationFlag).toBe(true);
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
|
||||
expect(commandSection).toMatch(/type=.*requires_confirmation/);
|
||||
const agentSafetyLine = commandSection
|
||||
@@ -44,4 +65,5 @@ test("every generated high-risk command reference requires user confirmation bef
|
||||
}
|
||||
|
||||
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 ");
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.19.0",
|
||||
"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:",
|
||||
|
||||
@@ -12,9 +12,9 @@ export default defineCommand({
|
||||
valueHint: "<key>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Config key (language, base_url, output, output_dir, timeout, api_key, api_key_capabilities, 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、api_key_capabilities、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,6 +29,7 @@ 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",
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ExitCode,
|
||||
isApiKeyCapability,
|
||||
normalizeModelBaseUrl,
|
||||
parseBooleanValue,
|
||||
SUPPORTED_LANGUAGES,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
@@ -13,6 +14,7 @@ export const VALID_KEYS = [
|
||||
"output",
|
||||
"output_dir",
|
||||
"timeout",
|
||||
"watermark",
|
||||
"api_key",
|
||||
"access_token",
|
||||
"access_key_id",
|
||||
@@ -62,7 +64,7 @@ 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
|
||||
@@ -164,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 | string[] {
|
||||
export function validateAndCoerce(
|
||||
key: string,
|
||||
value: string,
|
||||
): string | number | boolean | string[] {
|
||||
const resolvedKey = resolveKey(key);
|
||||
|
||||
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
|
||||
@@ -201,6 +206,8 @@ 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("[")) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,14 +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(默认:全部已配置项)",
|
||||
},
|
||||
},
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -65,9 +53,9 @@ export default defineCommand({
|
||||
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--concurrency <n>]",
|
||||
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;
|
||||
@@ -78,7 +66,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_apply: {
|
||||
provider: flags.provider ?? "all",
|
||||
provider: "bailian",
|
||||
refresh: !flags.noRefresh,
|
||||
concurrency: flags.concurrency,
|
||||
},
|
||||
@@ -93,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,
|
||||
@@ -110,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");
|
||||
@@ -141,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") {
|
||||
@@ -150,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);
|
||||
@@ -68,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",
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { listRemoteEnvironments } 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 { ENVIRONMENT_SEARCH_FLAGS, environmentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Search Managed Agent environments",
|
||||
"zh-CN": "搜索托管 Agent 环境",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
|
||||
flags: ENVIRONMENT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query sandbox", "--query production --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 listRemoteEnvironments(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,
|
||||
};
|
||||
},
|
||||
(environment) =>
|
||||
matchesQuery(
|
||||
ctx.flags.query,
|
||||
environment.id,
|
||||
environment.name,
|
||||
environment.description,
|
||||
),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "environments",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
|
||||
rows: environmentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching environments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
import { basename } from "node:path";
|
||||
import type { ProviderFileInfo } from "@openagentpack/sdk";
|
||||
import {
|
||||
deleteFile,
|
||||
downloadRemoteFile,
|
||||
getFileInfo,
|
||||
listRemoteFiles,
|
||||
uploadFile,
|
||||
} from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
emitCollection,
|
||||
matchesQuery,
|
||||
SEARCH_FLAGS,
|
||||
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 { inferMimeType, readInputFile, writeOutputFile } from "./_engine/output-file.ts";
|
||||
import { fetchAllPages } from "./_engine/pagination.ts";
|
||||
|
||||
const FILE_ID_FLAG = {
|
||||
fileId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Remote file ID", "zh-CN": "远端 File ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const SCOPE_ID_FLAG = {
|
||||
scopeId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Filter by scope ID", "zh-CN": "按 Scope ID 筛选" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const UPLOAD_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
path: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
|
||||
},
|
||||
filename: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
|
||||
},
|
||||
mimeType: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: { "en-US": "MIME type override", "zh-CN": "覆盖 MIME 类型" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SCOPE_ID_FLAG };
|
||||
const SEARCH_RESOURCE_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...SCOPE_ID_FLAG,
|
||||
};
|
||||
const GET_FLAGS = { ...API_TARGET_FLAGS, ...FILE_ID_FLAG };
|
||||
const DOWNLOAD_FLAGS = {
|
||||
...GET_FLAGS,
|
||||
outputFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Destination path", "zh-CN": "目标路径" },
|
||||
},
|
||||
force: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
|
||||
},
|
||||
} as const;
|
||||
const DELETE_FLAGS = {
|
||||
...GET_FLAGS,
|
||||
} as const;
|
||||
|
||||
function fileRows(files: ProviderFileInfo[]): string[][] {
|
||||
return files.map((file) => [
|
||||
file.id,
|
||||
displayValue(file.filename),
|
||||
displayValue(file.status),
|
||||
String(file.size_bytes),
|
||||
displayValue(file.scope?.id),
|
||||
displayValue(file.created_at),
|
||||
]);
|
||||
}
|
||||
|
||||
export const managedAgentFileUpload = defineCommand({
|
||||
description: { "en-US": "Upload a Managed Agent file", "zh-CN": "上传托管 Agent 文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
|
||||
flags: UPLOAD_FLAGS,
|
||||
exampleArgs: ["--path ./report.pdf"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_upload_file: ctx.flags.path,
|
||||
filename: ctx.flags.filename ?? basename(ctx.flags.path),
|
||||
mime_type: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const content = await readInputFile(ctx.flags.path);
|
||||
const file = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return uploadFile(runtime, content, ctx.flags.filename ?? basename(ctx.flags.path), {
|
||||
provider: "bailian",
|
||||
mimeType: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(file, format);
|
||||
else emitBare(`File uploaded: ${file.id} (${file.filename})`);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentFileList = defineCommand({
|
||||
description: { "en-US": "List Managed Agent files", "zh-CN": "列出托管 Agent 文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--scope-id <id>] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: ["", "--scope-id sess_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 listRemoteFiles(runtime, {
|
||||
provider: "bailian",
|
||||
scope_id: ctx.flags.scopeId,
|
||||
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: "files",
|
||||
items: result.items,
|
||||
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
|
||||
rows: fileRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No files found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentFileGet = defineCommand({
|
||||
description: { "en-US": "Get Managed Agent file metadata", "zh-CN": "获取托管 Agent 文件元数据" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id>",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: ["--file-id file_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const file = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getFileInfo(runtime, ctx.flags.fileId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(file, format);
|
||||
else {
|
||||
emitBare(`ID: ${file.id}`);
|
||||
emitBare(`Filename: ${file.filename}`);
|
||||
emitBare(`MIME: ${file.mime_type}`);
|
||||
emitBare(`Bytes: ${file.size_bytes}`);
|
||||
emitBare(`Status: ${displayValue(file.status)}`);
|
||||
emitBare(`Scope: ${displayValue(file.scope?.id)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentFileSearch = defineCommand({
|
||||
description: { "en-US": "Search Managed Agent files", "zh-CN": "搜索托管 Agent 文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--scope-id <id>] [--limit <n>] [--page-limit <n>]",
|
||||
flags: SEARCH_RESOURCE_FLAGS,
|
||||
exampleArgs: ["--query report", "--query pdf --scope-id sess_abc --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 listRemoteFiles(runtime, {
|
||||
provider: "bailian",
|
||||
scope_id: ctx.flags.scopeId,
|
||||
limit: ctx.flags.limit ?? 100,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
(file) => matchesQuery(ctx.flags.query, file.id, file.filename, file.mime_type),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "files",
|
||||
items: result.items,
|
||||
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
|
||||
rows: fileRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching files found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentFileDownload = defineCommand({
|
||||
description: {
|
||||
"en-US": "Download Managed Agent file content",
|
||||
"zh-CN": "下载托管 Agent 文件内容",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id> --output-file <path> [--force]",
|
||||
flags: DOWNLOAD_FLAGS,
|
||||
exampleArgs: ["--file-id file_abc --output-file ./artifact.pdf"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_download_file: ctx.flags.fileId, output_file: ctx.flags.outputFile },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const content = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return downloadRemoteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
|
||||
if (format === "json")
|
||||
emitResult({ downloaded: ctx.flags.fileId, output_file: outputFile }, format);
|
||||
else emitBare(`File downloaded to ${outputFile}`);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentFileDelete = defineCommand({
|
||||
description: { "en-US": "Delete a Managed Agent file", "zh-CN": "删除托管 Agent 文件" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified remote Managed Agent file.",
|
||||
"zh-CN": "该操作会永久删除指定的远端托管 Agent 文件。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--file-id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--file-id file_abc --dry-run", "--file-id file_abc --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_delete_file: ctx.flags.fileId }, format);
|
||||
return;
|
||||
}
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
await deleteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult({ deleted: ctx.flags.fileId }, format);
|
||||
else emitBare(`File ${ctx.flags.fileId} deleted.`);
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
@@ -12,48 +13,21 @@ import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
const GITIGNORE_ADDITIONS = `
|
||||
# agents
|
||||
agents.state.json
|
||||
.openagentpack/versions/
|
||||
.env
|
||||
`;
|
||||
|
||||
const PROVIDERS = ["bailian", "claude", "qoder", "ark", "all"] as const;
|
||||
|
||||
const PROVIDER_BLOCKS: Record<string, string> = {
|
||||
bailian: ` bailian:\n # bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL\n api_key: \${DASHSCOPE_API_KEY}\n base_url: \${BAILIAN_BASE_URL}`,
|
||||
claude: ` claude:\n api_key: \${ANTHROPIC_API_KEY}`,
|
||||
qoder: ` qoder:\n api_key: \${QODER_PAT}\n gateway: "https://api.qoder.com/api/v1/cloud"`,
|
||||
ark: ` ark:\n api_key: \${ARK_API_KEY}`,
|
||||
};
|
||||
|
||||
const SINGLE_MODEL: Record<string, string> = {
|
||||
bailian: ` model: qwen3.8-max`,
|
||||
claude: ` model: claude-sonnet-4-6`,
|
||||
qoder: ` model: ultimate`,
|
||||
ark: ` model: doubao-seed-2-1-pro-260628`,
|
||||
};
|
||||
|
||||
function buildTemplate(options: { provider: string; agentName: string }): string {
|
||||
const providerBlock =
|
||||
options.provider === "all"
|
||||
? `${PROVIDER_BLOCKS.bailian}\n${PROVIDER_BLOCKS.claude}\n${PROVIDER_BLOCKS.qoder}\n${PROVIDER_BLOCKS.ark}`
|
||||
: PROVIDER_BLOCKS[options.provider]!;
|
||||
|
||||
const modelBlock =
|
||||
options.provider === "all"
|
||||
? ` model:\n bailian: qwen3.8-max\n claude: claude-sonnet-4-6\n qoder: ultimate\n ark: doubao-seed-2-1-pro-260628`
|
||||
: SINGLE_MODEL[options.provider]!;
|
||||
|
||||
const toolBlock =
|
||||
options.provider === "bailian"
|
||||
? "[bash, read, glob, grep]"
|
||||
: "[read, glob, grep, web_search, web_fetch]";
|
||||
|
||||
function buildTemplate(options: { agentName: string }): string {
|
||||
return `version: "1"
|
||||
|
||||
providers:
|
||||
${providerBlock}
|
||||
bailian:
|
||||
# bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL
|
||||
api_key: \${DASHSCOPE_API_KEY}
|
||||
base_url: \${BAILIAN_BASE_URL}
|
||||
|
||||
defaults:
|
||||
provider: ${options.provider === "all" ? "all" : options.provider}
|
||||
provider: bailian
|
||||
|
||||
environments:
|
||||
dev:
|
||||
@@ -65,25 +39,16 @@ environments:
|
||||
agents:
|
||||
${options.agentName}:
|
||||
description: "General-purpose assistant"
|
||||
${modelBlock}
|
||||
model: qwen3.8-max
|
||||
instructions: |
|
||||
You are a helpful assistant.
|
||||
environment: dev
|
||||
tools:
|
||||
builtin: ${toolBlock}
|
||||
builtin: [bash, read, glob, grep]
|
||||
`;
|
||||
}
|
||||
|
||||
const INIT_FLAGS = {
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Provider: bailian, claude, qoder, ark, all (default: bailian)",
|
||||
"zh-CN": "Provider:bailian、claude、qoder、ark、all(默认:bailian)",
|
||||
},
|
||||
choices: PROVIDERS,
|
||||
},
|
||||
agentName: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
@@ -108,23 +73,22 @@ const INIT_FLAGS = {
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Create a new agents.yaml template",
|
||||
"zh-CN": "创建新的 agents.yaml 模板",
|
||||
"en-US": "Create an agents.yaml template",
|
||||
"zh-CN": "创建 agents.yaml 模板",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
|
||||
usageArgs: "[--agent-name <name>] [--file <path>] [--force]",
|
||||
flags: INIT_FLAGS,
|
||||
exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"],
|
||||
exampleArgs: ["", "--agent-name assistant"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const provider = flags.provider ?? "bailian";
|
||||
const agentName = flags.agentName ?? "assistant";
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
|
||||
if (existsSync(file) && !flags.force) {
|
||||
throw new BailianError(
|
||||
`${file} already exists.`,
|
||||
`${resolve(file)} already exists.`,
|
||||
ExitCode.USAGE,
|
||||
"Pass --force to overwrite.",
|
||||
);
|
||||
@@ -141,7 +105,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_create: file,
|
||||
provider,
|
||||
provider: "bailian",
|
||||
agent: agentName,
|
||||
would_update_gitignore: wouldUpdateGitignore,
|
||||
},
|
||||
@@ -150,7 +114,7 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const template = buildTemplate({ provider, agentName });
|
||||
const template = buildTemplate({ agentName });
|
||||
await writeFile(file, template, "utf8");
|
||||
|
||||
if (existsSync(gitignorePath)) {
|
||||
@@ -163,14 +127,12 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ created: file, provider, agent: agentName }, format);
|
||||
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
|
||||
} else {
|
||||
emitBare(`Created ${file}`);
|
||||
if (provider === "bailian" || provider === "all") {
|
||||
emitBare(
|
||||
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
|
||||
);
|
||||
}
|
||||
emitBare(`Created ${resolve(file)}`);
|
||||
emitBare(
|
||||
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
|
||||
);
|
||||
emitBare("Next: edit agents.yaml, then run `bl managed-agent plan`.");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { 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 PLAN_FLAGS = {
|
||||
@@ -26,14 +22,6 @@ const PLAN_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Target provider (default: all configured)",
|
||||
"zh-CN": "目标 Provider(默认:全部已配置项)",
|
||||
},
|
||||
},
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -56,9 +44,9 @@ export default defineCommand({
|
||||
"zh-CN": "显示将应用到 Agent 基础设施的变更",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
|
||||
usageArgs: "[--file <path>] [--no-refresh] [--refresh-only]",
|
||||
flags: PLAN_FLAGS,
|
||||
exampleArgs: ["", "--provider bailian", "--no-refresh"],
|
||||
exampleArgs: ["", "--no-refresh"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
@@ -83,9 +71,8 @@ export default defineCommand({
|
||||
const runtime = await buildAgentRuntime(ctx, file, {
|
||||
credentials: offline ? "none" : "all",
|
||||
});
|
||||
assertProviderConfigured(runtime, flags.provider);
|
||||
return planProjectContext(runtime, {
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
refresh: !offline,
|
||||
quiet: format === "json",
|
||||
onFeedback: format === "json" ? undefined : renderAgentFeedback,
|
||||
@@ -95,17 +82,18 @@ export default defineCommand({
|
||||
|
||||
const plan = planned.plan;
|
||||
const hasErrors = plan.diagnostics.some((diag) => diag.severity === "error");
|
||||
const failureMessage = formatAgentDiagnosticFailure(plan.diagnostics, "Plan contains errors.");
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(plan, format);
|
||||
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
|
||||
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const diag of plan.diagnostics) {
|
||||
emitBare(`[${diag.severity}] ${diag.code}: ${diag.message}`);
|
||||
}
|
||||
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
|
||||
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
|
||||
|
||||
const creates = plan.actions.filter((action) => action.action === "create");
|
||||
const updates = plan.actions.filter((action) => action.action === "update");
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
commitProjectBuild,
|
||||
createDirectoryWorkspaceVersionService,
|
||||
executeProjectPublish,
|
||||
initializeDirectoryProject,
|
||||
planProjectPublish,
|
||||
previewProjectBuild,
|
||||
type ProjectBuildResolver,
|
||||
resolveDirectoryProjectRoot,
|
||||
validateDirectoryProject,
|
||||
} from "@openagentpack/sdk/project-workspace";
|
||||
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
|
||||
import { installSdkTransport } from "./_engine/transport.ts";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
|
||||
const PROJECT_FLAG = {
|
||||
project: {
|
||||
type: "string",
|
||||
valueHint: "<directory>",
|
||||
description: {
|
||||
"en-US": "Directory project root (default: current directory)",
|
||||
"zh-CN": "目录项目根路径(默认:当前目录)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const JSON_FORMAT = "json" as const;
|
||||
|
||||
export const managedAgentProjectInit = defineCommand({
|
||||
description: {
|
||||
"en-US": "Create a directory project or convert the local agents.yaml",
|
||||
"zh-CN": "创建目录项目,或转换当前 agents.yaml",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: {
|
||||
project: {
|
||||
...PROJECT_FLAG.project,
|
||||
description: {
|
||||
"en-US": "Directory project root (default: ./managed-agent under the current directory)",
|
||||
"zh-CN": "目录项目根路径(默认:当前目录下的 ./managed-agent)",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--project ./my-agent", "--project ."],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.",
|
||||
"zh-CN":
|
||||
"不传 --project 时创建 managed-agent/ 子目录;后续项目操作请先进入该目录。使用 --project . 可在当前目录初始化或转换 agents.yaml;不会覆盖已有项目文件。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.",
|
||||
"zh-CN":
|
||||
"新项目在四类资源目录的 _examples/ 下生成配置示例,不写入 agent.json 引用,也不参与 Build/Publish。需要使用时,将示例复制到 _examples/ 外,再配置 Agent 引用。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const projectRoot = ctx.flags.project ?? "./managed-agent";
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_initialize_project: projectRoot,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await initializeDirectoryProject({ projectRoot });
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectValidate = defineCommand({
|
||||
description: { "en-US": "Validate a directory Agent project", "zh-CN": "校验目录式 Agent 项目" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent"],
|
||||
async run(ctx) {
|
||||
const result = await validateDirectoryProject(ctx.flags.project ?? ".");
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === JSON_FORMAT) emitResult(safeInspection(result), format);
|
||||
else {
|
||||
for (const diagnostic of [...result.diagnostics, ...result.warnings]) {
|
||||
emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
||||
emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`);
|
||||
}
|
||||
}
|
||||
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
||||
throw new BailianError("Directory project validation failed.", ExitCode.GENERAL);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectBuild = defineCommand({
|
||||
description: {
|
||||
"en-US": "Organize directory source and generate the immutable Publish Build",
|
||||
"zh-CN": "整理目录源文件并生成不可变的发布 Build",
|
||||
},
|
||||
auth: "none",
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.",
|
||||
"zh-CN":
|
||||
"Build 无需确认即可写入本地项目文件,包括推断的资源关联及将 Vault 明文密钥移入 .env。使用 --dry-run 可只预览不写入。Publish 变更远端资源前仍需显式确认。",
|
||||
},
|
||||
],
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--dry-run", "--project ./my-agent"],
|
||||
async validate(flags) {
|
||||
await withAgentErrors(async () => {
|
||||
const root = await resolveDirectoryProjectRoot(flags.project ?? ".");
|
||||
const metadata = await stat(join(root, "project.json")).catch((error: unknown) => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!metadata?.isFile()) {
|
||||
throw new BailianError(
|
||||
`Not a project root: ${root} (project.json is missing).`,
|
||||
ExitCode.USAGE,
|
||||
"Run from the directory containing project.json, or pass --project <directory>.",
|
||||
);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const root = ctx.flags.project ?? ".";
|
||||
const preview = await previewProjectBuild(root);
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(safeInspection(preview), format);
|
||||
return;
|
||||
}
|
||||
if (!preview.can_build)
|
||||
throw new BailianError(
|
||||
preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ??
|
||||
"Directory project is invalid and cannot be built.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
const built = await commitProjectBuild({
|
||||
projectRoot: root,
|
||||
baseRevision: preview.project_revision,
|
||||
});
|
||||
emitResult(
|
||||
{ manifest: built.manifest, organization_moves: preview.organization_moves },
|
||||
format,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectPublish = defineCommand({
|
||||
description: {
|
||||
"en-US": "Publish the current directory-project Build and record a version",
|
||||
"zh-CN": "发布当前目录项目 Build 并记录版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This publishes the current directory-project Build and may create, update, or delete remote managed Agent resources.",
|
||||
"zh-CN": "该操作会发布当前目录项目 Build,可能创建、更新或删除远端托管 Agent 资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--project <directory>] [--no-refresh] [--concurrency <n>]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Skip remote refresh before planning",
|
||||
"zh-CN": "规划前跳过远端刷新",
|
||||
},
|
||||
},
|
||||
concurrency: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Maximum parallel resource operations",
|
||||
"zh-CN": "最大并行资源操作数",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: ["--yes", "--project ./my-agent --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
installSdkTransport(ctx);
|
||||
const root = ctx.flags.project ?? ".";
|
||||
const resolveBuild: ProjectBuildResolver = async (buildPath, options) =>
|
||||
(await resolveAgentProjectConfig(ctx, buildPath, {
|
||||
environment: options?.environment,
|
||||
overrideBailianBaseUrl: true,
|
||||
})) as unknown as Awaited<ReturnType<ProjectBuildResolver>>;
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectPublish(root, {
|
||||
provider: "bailian",
|
||||
refresh: !ctx.flags.noRefresh,
|
||||
quiet: true,
|
||||
onFeedback: renderAgentFeedback,
|
||||
resolveBuild,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op");
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
project_revision: planned.project_revision,
|
||||
build_manifest: planned.build_manifest,
|
||||
plan: planned.planned.plan,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const action of actions) {
|
||||
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(`${marker} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executeProjectPublish({
|
||||
projectRoot: planned.project_root,
|
||||
expectedProjectRevision: planned.project_revision,
|
||||
expectedYamlHash: planned.build_manifest.yaml_hash,
|
||||
expectedPlanFingerprint: planned.plan_fingerprint,
|
||||
provider: "bailian",
|
||||
refresh: !ctx.flags.noRefresh,
|
||||
concurrency: ctx.flags.concurrency,
|
||||
policy: "force",
|
||||
onFeedback: renderAgentFeedback,
|
||||
resolveBuild,
|
||||
}),
|
||||
),
|
||||
);
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectWorkbench = defineCommand({
|
||||
description: {
|
||||
"en-US": "Launch the directory project Workbench",
|
||||
"zh-CN": "启动目录项目 Workbench",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--project <directory>] [--port <n>] [--no-open]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
port: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Local port (default: 4848)", "zh-CN": "本地端口(默认:4848)" },
|
||||
},
|
||||
noOpen: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Do not open a browser", "zh-CN": "不自动打开浏览器" },
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--project ./my-agent --no-open"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const root = ctx.flags.project ?? ".";
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchManagedAgentPlayground({
|
||||
project: root,
|
||||
port: ctx.flags.port ?? 4848,
|
||||
open: !ctx.flags.noOpen,
|
||||
surface: "workbench",
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionEnable = versionToggleCommand(true);
|
||||
export const managedAgentProjectVersionDisable = versionToggleCommand(false);
|
||||
|
||||
function versionToggleCommand(enabled: boolean) {
|
||||
return defineCommand({
|
||||
description: enabled
|
||||
? { "en-US": "Enable directory project versions", "zh-CN": "启用目录项目版本管理" }
|
||||
: { "en-US": "Disable directory project versions", "zh-CN": "关闭目录项目版本管理" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent"],
|
||||
async run(ctx) {
|
||||
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_set_enabled: enabled, status: await service.status() },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = enabled
|
||||
? await service.enable("Enable project versions")
|
||||
: await service.disable();
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const managedAgentProjectVersionStatus = defineCommand({
|
||||
description: {
|
||||
"en-US": "Show directory project version status",
|
||||
"zh-CN": "显示目录项目版本状态",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent --output json"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionList = defineCommand({
|
||||
description: { "en-US": "List directory project versions", "zh-CN": "列出目录项目版本" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>] [--limit <n>] [--cursor <cursor>]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Maximum versions to return", "zh-CN": "最多返回的版本数" },
|
||||
},
|
||||
cursor: {
|
||||
type: "string",
|
||||
valueHint: "<cursor>",
|
||||
description: { "en-US": "Pagination cursor", "zh-CN": "分页游标" },
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--limit 20 --output json"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({
|
||||
limit: ctx.flags.limit,
|
||||
cursor: ctx.flags.cursor,
|
||||
}),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const VERSION_ID_FLAG = {
|
||||
versionId: {
|
||||
type: "string",
|
||||
valueHint: "<full-version>",
|
||||
required: true,
|
||||
description: { "en-US": "Full project version ID", "zh-CN": "完整的项目版本 ID" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const managedAgentProjectVersionPreview = defineCommand({
|
||||
description: { "en-US": "Preview a directory project version", "zh-CN": "预览目录项目历史版本" },
|
||||
auth: "none",
|
||||
usageArgs: "--version-id <full-version> [--project <directory>]",
|
||||
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
|
||||
exampleArgs: ["--version-id <full-version>"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion(
|
||||
ctx.flags.versionId,
|
||||
),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionRestore = defineCommand({
|
||||
description: {
|
||||
"en-US": "Restore a version to the project working directory",
|
||||
"zh-CN": "将历史版本恢复到项目工作目录",
|
||||
},
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This restores the full directory source to the working tree. Version history and remote State will not move.",
|
||||
"zh-CN": "该操作会将完整目录源文件恢复到工作区;版本历史和远端 State 不会移动。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--version-id <full-version> [--project <directory>]",
|
||||
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
|
||||
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes"],
|
||||
async run(ctx) {
|
||||
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
|
||||
const preview = await service.previewVersion(ctx.flags.versionId);
|
||||
if (!preview.can_restore)
|
||||
throw new BailianError(
|
||||
preview.blockers[0] ?? preview.diagnostics[0]?.message ?? "Version cannot be restored.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_restore: ctx.flags.versionId, preview },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const restored = await service.restoreVersion(ctx.flags.versionId, {
|
||||
headVersion: preview.base_head_version,
|
||||
projectRevision: preview.base_project_revision,
|
||||
});
|
||||
emitResult(restored, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
function safeInspection(result: {
|
||||
project_root: string;
|
||||
project_revision: string;
|
||||
source_manifest_hash: string;
|
||||
yaml_hash: string;
|
||||
diagnostics: unknown[];
|
||||
warnings: unknown[];
|
||||
organization_moves: unknown[];
|
||||
canonical_yaml: string;
|
||||
before_yaml?: string;
|
||||
can_build?: boolean;
|
||||
}) {
|
||||
return {
|
||||
project_root: result.project_root,
|
||||
project_revision: result.project_revision,
|
||||
source_manifest_hash: result.source_manifest_hash,
|
||||
yaml_hash: result.yaml_hash,
|
||||
diagnostics: result.diagnostics,
|
||||
warnings: result.warnings,
|
||||
organization_moves: result.organization_moves,
|
||||
before_yaml: "before_yaml" in result ? result.before_yaml : undefined,
|
||||
after_yaml: result.canonical_yaml,
|
||||
can_build: "can_build" in result ? result.can_build : undefined,
|
||||
};
|
||||
}
|
||||
@@ -49,14 +49,6 @@ const SESSION_CREATE_FLAGS = {
|
||||
valueHint: "<title>",
|
||||
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Target provider (multi-provider agents)",
|
||||
"zh-CN": "目标 Provider(多 Provider Agent)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -83,7 +75,7 @@ export default defineCommand({
|
||||
{
|
||||
would_create_session: {
|
||||
agent: flags.agent ?? "auto",
|
||||
provider: flags.provider ?? "auto",
|
||||
provider: "bailian",
|
||||
environment: flags.environment,
|
||||
vault: flags.vault,
|
||||
memory_stores: parseMemoryStores(flags.memoryStores),
|
||||
@@ -101,7 +93,7 @@ export default defineCommand({
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return createSessionForAgent(runtime, {
|
||||
agent: flags.agent,
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
environment: flags.environment,
|
||||
vault: flags.vault,
|
||||
memoryStores: parseMemoryStores(flags.memoryStores),
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
getManagedAgentProviderCapabilities,
|
||||
getSession,
|
||||
listRemoteFiles,
|
||||
listSessionEvents,
|
||||
type ProjectRuntimeContext,
|
||||
type ProviderFileInfo,
|
||||
type ProviderSessionEvent,
|
||||
type ProviderSessionInfo,
|
||||
} from "@openagentpack/sdk";
|
||||
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { API_TARGET_FLAGS } 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 { writeOutputFile } from "./_engine/output-file.ts";
|
||||
import { fetchAllPages } from "./_engine/pagination.ts";
|
||||
|
||||
const SESSION_ID_FLAG = {
|
||||
sessionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const DEBUG_FLAGS = { ...API_TARGET_FLAGS, ...SESSION_ID_FLAG };
|
||||
const EXPORT_FLAGS = {
|
||||
...DEBUG_FLAGS,
|
||||
outputFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
|
||||
},
|
||||
force: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface SessionDiagnosticBundle {
|
||||
session: ProviderSessionInfo;
|
||||
events: unknown[];
|
||||
files: ProviderFileInfo[];
|
||||
capabilities: ReturnType<typeof getManagedAgentProviderCapabilities>;
|
||||
errors: Array<{ component: "events" | "files"; message: string }>;
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function collectSessionDiagnostics(
|
||||
runtime: ProjectRuntimeContext,
|
||||
sessionId: string,
|
||||
provider?: string,
|
||||
): Promise<SessionDiagnosticBundle> {
|
||||
// Session lookup is the identity anchor. If it fails, the aggregate is not meaningful.
|
||||
const session = await getSession(runtime, sessionId, provider);
|
||||
const providerName =
|
||||
provider ??
|
||||
(runtime.providers.size === 1 ? Array.from(runtime.providers.keys())[0]! : "bailian");
|
||||
const errors: SessionDiagnosticBundle["errors"] = [];
|
||||
let events: ProviderSessionEvent[] = [];
|
||||
let files: ProviderFileInfo[] = [];
|
||||
|
||||
try {
|
||||
const result = await fetchAllPages(async (page) => {
|
||||
const response = await listSessionEvents(runtime, sessionId, {
|
||||
provider,
|
||||
limit: 100,
|
||||
page_token: page,
|
||||
order: "asc",
|
||||
});
|
||||
return { items: response.events, hasMore: response.has_more, nextPage: response.next_page };
|
||||
}, true);
|
||||
events = result.items;
|
||||
} catch (error) {
|
||||
errors.push({ component: "events", message: errorMessage(error) });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetchAllPages(async (page) => {
|
||||
const response = await listRemoteFiles(runtime, {
|
||||
provider,
|
||||
scope_id: sessionId,
|
||||
limit: 100,
|
||||
page,
|
||||
});
|
||||
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
|
||||
}, true);
|
||||
files = result.items;
|
||||
} catch (error) {
|
||||
errors.push({ component: "files", message: errorMessage(error) });
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
events: sanitizeSessionEvents(events),
|
||||
files,
|
||||
capabilities: getManagedAgentProviderCapabilities(providerName),
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
const SENSITIVE_MARKER = String.raw`(?:api[_-]?key|access[_-]?key|private[_-]?key|secret|token|authorization|credential|password|signature|signed[_-]?url)`;
|
||||
const SENSITIVE_KEY = new RegExp(`${SENSITIVE_MARKER}|tool[_-]?input`, "i");
|
||||
const SENSITIVE_URL_PARAMETER = new RegExp(
|
||||
`([?&][^=&#\\s]*(?:${SENSITIVE_MARKER})[^=&#\\s]*=)[^&#\\s"'<>),}\\]]*`,
|
||||
"gi",
|
||||
);
|
||||
const SENSITIVE_DOUBLE_QUOTED_VALUE = new RegExp(
|
||||
`("[^"]*(?:${SENSITIVE_MARKER})[^"]*"\\s*:\\s*")[^"]*(")`,
|
||||
"gi",
|
||||
);
|
||||
const SENSITIVE_SINGLE_QUOTED_VALUE = new RegExp(
|
||||
`('[^']*(?:${SENSITIVE_MARKER})[^']*'\\s*:\\s*')[^']*(')`,
|
||||
"gi",
|
||||
);
|
||||
const SENSITIVE_ASSIGNMENT = new RegExp(
|
||||
`(\\b[^\\s=:,;]*(?:${SENSITIVE_MARKER})[^\\s=:,;]*\\s*=\\s*)(?!\\[REDACTED\\])(?:"[^"]*"|'[^']*'|[^\\s,;&]+)`,
|
||||
"gi",
|
||||
);
|
||||
const AUTHORIZATION_VALUE = /\b(authorization\s*:\s*)(?:bearer\s+)?[^\s,;]+/gi;
|
||||
const BEARER_TOKEN = /\b(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
|
||||
const API_KEY_LITERAL = /\bsk-[A-Za-z0-9_-]{8,}\b/g;
|
||||
|
||||
function redactSensitiveText(value: string): string {
|
||||
return value
|
||||
.replace(SENSITIVE_URL_PARAMETER, "$1[REDACTED]")
|
||||
.replace(SENSITIVE_DOUBLE_QUOTED_VALUE, "$1[REDACTED]$2")
|
||||
.replace(SENSITIVE_SINGLE_QUOTED_VALUE, "$1[REDACTED]$2")
|
||||
.replace(SENSITIVE_ASSIGNMENT, "$1[REDACTED]")
|
||||
.replace(AUTHORIZATION_VALUE, "$1[REDACTED]")
|
||||
.replace(BEARER_TOKEN, "$1[REDACTED]")
|
||||
.replace(API_KEY_LITERAL, "[REDACTED]");
|
||||
}
|
||||
|
||||
export function redactSensitiveValues(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(redactSensitiveValues);
|
||||
if (typeof value === "string") return redactSensitiveText(value);
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entry]) => [
|
||||
key,
|
||||
SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactSensitiveValues(entry),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function jsonBytes(value: unknown): Uint8Array {
|
||||
return new TextEncoder().encode(`${JSON.stringify(redactSensitiveValues(value), null, 2)}\n`);
|
||||
}
|
||||
|
||||
// Minimal ZIP writer using stored (uncompressed) entries. This avoids shelling out
|
||||
// and keeps export deterministic across supported Node runtimes.
|
||||
function crc32(bytes: Uint8Array): number {
|
||||
let checksum = 0xffffffff;
|
||||
for (const byte of bytes) {
|
||||
checksum ^= byte;
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
checksum = (checksum >>> 1) ^ (checksum & 1 ? 0xedb88320 : 0);
|
||||
}
|
||||
}
|
||||
return (checksum ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
||||
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
||||
const output = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const part of parts) {
|
||||
output.set(part, offset);
|
||||
offset += part.length;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function littleEndian(values: Array<[number, number]>): Uint8Array {
|
||||
const size = values.reduce((sum, [, bytes]) => sum + bytes, 0);
|
||||
const buffer = new ArrayBuffer(size);
|
||||
const view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
for (const [value, bytes] of values) {
|
||||
if (bytes === 2) view.setUint16(offset, value, true);
|
||||
else view.setUint32(offset, value, true);
|
||||
offset += bytes;
|
||||
}
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
export function createZip(entries: Array<{ name: string; content: Uint8Array }>): Uint8Array {
|
||||
const encoder = new TextEncoder();
|
||||
const localParts: Uint8Array[] = [];
|
||||
const centralParts: Uint8Array[] = [];
|
||||
let localOffset = 0;
|
||||
for (const entry of entries) {
|
||||
const name = encoder.encode(entry.name);
|
||||
const checksum = crc32(entry.content);
|
||||
const localHeader = littleEndian([
|
||||
[0x04034b50, 4],
|
||||
[20, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[checksum, 4],
|
||||
[entry.content.length, 4],
|
||||
[entry.content.length, 4],
|
||||
[name.length, 2],
|
||||
[0, 2],
|
||||
]);
|
||||
const local = concatBytes([localHeader, name, entry.content]);
|
||||
localParts.push(local);
|
||||
const centralHeader = littleEndian([
|
||||
[0x02014b50, 4],
|
||||
[20, 2],
|
||||
[20, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[checksum, 4],
|
||||
[entry.content.length, 4],
|
||||
[entry.content.length, 4],
|
||||
[name.length, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[0, 4],
|
||||
[localOffset, 4],
|
||||
]);
|
||||
centralParts.push(concatBytes([centralHeader, name]));
|
||||
localOffset += local.length;
|
||||
}
|
||||
const central = concatBytes(centralParts);
|
||||
const end = littleEndian([
|
||||
[0x06054b50, 4],
|
||||
[0, 2],
|
||||
[0, 2],
|
||||
[entries.length, 2],
|
||||
[entries.length, 2],
|
||||
[central.length, 4],
|
||||
[localOffset, 4],
|
||||
[0, 2],
|
||||
]);
|
||||
return concatBytes([...localParts, central, end]);
|
||||
}
|
||||
|
||||
export const managedAgentSessionDebug = defineCommand({
|
||||
description: { "en-US": "Aggregate session diagnostics", "zh-CN": "聚合 Session 诊断信息" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id>",
|
||||
flags: DEBUG_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const bundle = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
|
||||
}),
|
||||
);
|
||||
const redacted = redactSensitiveValues(bundle);
|
||||
if (format === "json") {
|
||||
emitResult(redacted, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`Session: ${bundle.session.id} (${bundle.session.status})`);
|
||||
emitBare(`Events: ${bundle.events.length}`);
|
||||
emitBare(`Scoped files: ${bundle.files.length}`);
|
||||
emitBare(`Partial errors: ${bundle.errors.length}`);
|
||||
for (const error of bundle.errors) emitBare(` ${error.component}: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentSessionExport = defineCommand({
|
||||
description: {
|
||||
"en-US": "Export session diagnostics as a ZIP",
|
||||
"zh-CN": "将 Session 诊断信息导出为 ZIP",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> --output-file <path> [--force]",
|
||||
flags: EXPORT_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc --output-file ./session-debug.zip"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"The ZIP contains metadata only; file bodies and credential-like values are excluded/redacted.",
|
||||
"zh-CN": "ZIP 仅包含元数据;不会包含文件正文,凭证类字段会被移除或脱敏。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_export_session: ctx.flags.sessionId, output_file: ctx.flags.outputFile },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const bundle = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
|
||||
}),
|
||||
);
|
||||
const manifest = {
|
||||
schema_version: 1,
|
||||
generated_at: new Date().toISOString(),
|
||||
session_id: bundle.session.id,
|
||||
partial: bundle.errors.length > 0,
|
||||
counts: {
|
||||
events: bundle.events.length,
|
||||
files: bundle.files.length,
|
||||
errors: bundle.errors.length,
|
||||
},
|
||||
contents: ["session.json", "events.json", "files.json", "capabilities.json", "errors.json"],
|
||||
note: "File bodies are not included. Credential-like fields are redacted.",
|
||||
};
|
||||
const zip = createZip([
|
||||
{ name: "manifest.json", content: jsonBytes(manifest) },
|
||||
{ name: "session.json", content: jsonBytes(bundle.session) },
|
||||
{ name: "events.json", content: jsonBytes(bundle.events) },
|
||||
{ name: "files.json", content: jsonBytes(bundle.files) },
|
||||
{ name: "capabilities.json", content: jsonBytes(bundle.capabilities) },
|
||||
{ name: "errors.json", content: jsonBytes(bundle.errors) },
|
||||
]);
|
||||
const outputFile = await writeOutputFile(ctx.flags.outputFile, zip, ctx.flags.force);
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{ exported: bundle.session.id, output_file: outputFile, partial: manifest.partial },
|
||||
format,
|
||||
);
|
||||
} else {
|
||||
emitBare(
|
||||
`Session diagnostics exported to ${outputFile}${manifest.partial ? " (partial)" : ""}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -20,11 +20,6 @@ const SESSION_DELETE_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -37,7 +32,7 @@ export default defineCommand({
|
||||
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
usageArgs: "--session-id <id> [--file <path>]",
|
||||
flags: SESSION_DELETE_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123 --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -50,7 +45,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_delete_session: flags.sessionId,
|
||||
provider: flags.provider ?? "auto",
|
||||
provider: "bailian",
|
||||
config_file: file,
|
||||
},
|
||||
format,
|
||||
@@ -61,7 +56,7 @@ export default defineCommand({
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
await deleteSession(runtime, flags.sessionId, flags.provider);
|
||||
await deleteSession(runtime, flags.sessionId, "bailian");
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import type { ProviderSessionEvent, SessionEventInput } from "@openagentpack/sdk";
|
||||
import {
|
||||
isTerminalSessionStatus,
|
||||
listSessionEvents,
|
||||
sendRemoteSessionEvents,
|
||||
SessionEventSchema,
|
||||
streamSessionEvents,
|
||||
} from "@openagentpack/sdk";
|
||||
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
|
||||
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
emitCollection,
|
||||
splitCommaSeparated,
|
||||
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 { readJsonInput } from "./_engine/output-file.ts";
|
||||
import { fetchAllPages } from "./_engine/pagination.ts";
|
||||
|
||||
const SESSION_ID_FLAG = {
|
||||
sessionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const EVENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...SESSION_ID_FLAG,
|
||||
...CURSOR_FLAGS,
|
||||
order: {
|
||||
type: "string",
|
||||
valueHint: "<order>",
|
||||
choices: ["asc", "desc"] as const,
|
||||
description: { "en-US": "Event order: asc or desc", "zh-CN": "事件顺序:asc 或 desc" },
|
||||
},
|
||||
types: {
|
||||
type: "string",
|
||||
valueHint: "<types>",
|
||||
description: {
|
||||
"en-US": "Comma-separated raw event types",
|
||||
"zh-CN": "原始事件类型,多个以逗号分隔",
|
||||
},
|
||||
},
|
||||
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;
|
||||
|
||||
const EVENT_SEND_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...SESSION_ID_FLAG,
|
||||
event: {
|
||||
type: "string",
|
||||
valueHint: "<json|@path>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Raw event object/array as JSON or @event.json",
|
||||
"zh-CN": "原始事件对象/数组 JSON,或 @event.json",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const EVENT_STREAM_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...SESSION_ID_FLAG,
|
||||
afterId: {
|
||||
type: "string",
|
||||
valueHint: "<event-id>",
|
||||
description: { "en-US": "Resume after this event ID", "zh-CN": "从该 Event ID 之后继续" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
function eventRows(events: ProviderSessionEvent[]): string[][] {
|
||||
return events.map((event) => [
|
||||
displayValue(event.id),
|
||||
event.raw_type,
|
||||
displayValue(event.session_thread_id),
|
||||
displayValue(event.content ?? event.status ?? event.tool_name, 70),
|
||||
]);
|
||||
}
|
||||
|
||||
function normalizeEventInput(value: unknown): SessionEventInput[] {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
if (
|
||||
values.length === 0 ||
|
||||
values.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Event input must be a JSON object or a non-empty array of objects.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
for (const [index, entry] of values.entries()) {
|
||||
const validation = SessionEventSchema.safeParse(entry);
|
||||
if (validation.success) continue;
|
||||
const issue = validation.error.issues[0];
|
||||
const path = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
|
||||
throw new BailianError(
|
||||
`Invalid event at index ${index}${path}: ${issue?.message ?? "does not match the Session event schema"}.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return values as SessionEventInput[];
|
||||
}
|
||||
|
||||
export const managedAgentSessionEventList = defineCommand({
|
||||
description: {
|
||||
"en-US": "List events for a Managed Agent session",
|
||||
"zh-CN": "列出托管 Agent Session 事件",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--session-id <id> [--types <types>] [--order asc|desc] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: EVENT_LIST_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --all --output json"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US": "--types is applied client-side to each page returned by the provider.",
|
||||
"zh-CN": "--types 会在客户端对 Provider 返回的每一页结果进行过滤。",
|
||||
},
|
||||
],
|
||||
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 listSessionEvents(runtime, ctx.flags.sessionId, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page_token: page,
|
||||
order: ctx.flags.order,
|
||||
types: splitCommaSeparated(ctx.flags.types),
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
});
|
||||
return {
|
||||
items: response.events,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "events",
|
||||
items: sanitizeSessionEvents(result.items),
|
||||
headers: ["ID", "TYPE", "THREAD", "CONTENT"],
|
||||
rows: eventRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No events found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentSessionEventSend = defineCommand({
|
||||
description: {
|
||||
"en-US": "Send raw events to a Managed Agent session",
|
||||
"zh-CN": "向托管 Agent Session 发送原始事件",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> --event <json|@path>",
|
||||
flags: EVENT_SEND_FLAGS,
|
||||
exampleArgs: [
|
||||
'--session-id sess_abc --event \'{"type":"message","role":"user","content":[{"type":"text","text":"hello"}]}\'',
|
||||
"--session-id sess_abc --event @event.json",
|
||||
],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const events = normalizeEventInput(await readJsonInput(ctx.flags.event));
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_send_events: events, session_id: ctx.flags.sessionId }, format);
|
||||
return;
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return sendRemoteSessionEvents(runtime, ctx.flags.sessionId, events, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult({ session_id: ctx.flags.sessionId, ...result }, format);
|
||||
else emitBare(`Sent ${events.length} event(s): ${result.event_ids.join(", ")}`);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentSessionEventStream = defineCommand({
|
||||
description: {
|
||||
"en-US": "Stream events from a Managed Agent session",
|
||||
"zh-CN": "流式读取托管 Agent Session 事件",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> [--after-id <event-id>]",
|
||||
flags: EVENT_STREAM_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --after-id evt_123 --output json"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"When the provider has no native event cursor, --after-id resumes through paginated history polling and event ID de-duplication.",
|
||||
"zh-CN":
|
||||
"当 Provider 不支持原生事件 Cursor 时,--after-id 会通过分页历史轮询和 Event ID 去重实现续传。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const events: ProviderSessionEvent[] = [];
|
||||
for await (const event of streamSessionEvents(runtime, ctx.flags.sessionId, {
|
||||
provider: "bailian",
|
||||
after_id: ctx.flags.afterId,
|
||||
})) {
|
||||
events.push(event);
|
||||
if (format !== "json") emitBare(JSON.stringify(sanitizeSessionEvents([event])[0]));
|
||||
if (event.type === "status" && isTerminalSessionStatus(event.status)) break;
|
||||
}
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{ session_id: ctx.flags.sessionId, events: sanitizeSessionEvents(events) },
|
||||
format,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,98 +1,2 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { listSessionEvents } from "@openagentpack/sdk";
|
||||
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
|
||||
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";
|
||||
|
||||
const SESSION_EVENTS_FLAGS = {
|
||||
sessionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Session ID (required)", "zh-CN": "Session ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Maximum number of events to fetch", "zh-CN": "要获取的最大事件数" },
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Fetch all pages by following the cursor",
|
||||
"zh-CN": "跟随 Cursor 获取全部分页",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List event history for a session", "zh-CN": "列出 Session 的事件历史" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
|
||||
flags: SESSION_EVENTS_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
|
||||
const { items: events, hasMore } = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return fetchAllPages(async (page) => {
|
||||
const result = await listSessionEvents(runtime, flags.sessionId, {
|
||||
provider: flags.provider,
|
||||
limit: flags.limit,
|
||||
page_token: page,
|
||||
});
|
||||
return {
|
||||
items: result.events,
|
||||
hasMore: result.has_more,
|
||||
nextPage: result.next_page,
|
||||
};
|
||||
}, flags.all);
|
||||
}),
|
||||
);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ events: sanitizeSessionEvents(events), has_more: hasMore }, format);
|
||||
return;
|
||||
}
|
||||
if (events.length === 0) {
|
||||
emitBare("No events found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = ["#", "TYPE", "CONTENT"];
|
||||
const rows = events.map((event, index) => {
|
||||
let preview = "";
|
||||
if (event.type === "message") preview = (event.content ?? "").slice(0, 60);
|
||||
else if (event.type === "tool_use") preview = event.tool_name ?? "";
|
||||
else if (event.type === "tool_result") preview = (event.content ?? "").slice(0, 60);
|
||||
else if (event.type === "status") preview = event.status ?? "";
|
||||
else if (event.type === "error") preview = (event.content ?? "").slice(0, 60);
|
||||
else preview = event.raw_type;
|
||||
return [String(index + 1), event.type, preview];
|
||||
});
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
emitBare(`\nTotal: ${events.length}`);
|
||||
if (hasMore) emitBare("More events available. Use --all to fetch all.");
|
||||
},
|
||||
});
|
||||
// Backward-compatible alias for `managed-agent session event list`.
|
||||
export { managedAgentSessionEventList as default } from "./session-event.ts";
|
||||
|
||||
@@ -20,17 +20,12 @@ const SESSION_GET_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Get details of a session", "zh-CN": "获取 Session 详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
usageArgs: "--session-id <id> [--file <path>]",
|
||||
flags: SESSION_GET_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -42,7 +37,7 @@ export default defineCommand({
|
||||
const session = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return getSession(runtime, flags.sessionId, flags.provider);
|
||||
return getSession(runtime, flags.sessionId, "bailian");
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ 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 {
|
||||
CURSOR_FLAGS,
|
||||
splitCommaSeparated,
|
||||
validateLimitAndPageLimit,
|
||||
} from "./_engine/api-helpers.ts";
|
||||
|
||||
const SESSION_LIST_FLAGS = {
|
||||
file: {
|
||||
@@ -27,10 +32,31 @@ const SESSION_LIST_FLAGS = {
|
||||
"zh-CN": "跟随 Cursor 获取全部分页",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
page: CURSOR_FLAGS.page,
|
||||
statuses: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
valueHint: "<statuses>",
|
||||
description: {
|
||||
"en-US": "Comma-separated session statuses",
|
||||
"zh-CN": "Session 状态,多个以逗号分隔",
|
||||
},
|
||||
},
|
||||
createdAtGte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or after this RFC 3339 timestamp",
|
||||
"zh-CN": "创建时间不早于该 RFC 3339 时间戳",
|
||||
},
|
||||
},
|
||||
createdAtLte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or before this RFC 3339 timestamp",
|
||||
"zh-CN": "创建时间不晚于该 RFC 3339 时间戳",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -40,36 +66,51 @@ export default defineCommand({
|
||||
"zh-CN": "列出 Provider 中的 Session",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--agent <name>] [--all] [--provider <name>] [--file <path>]",
|
||||
usageArgs: "[--agent <name>] [--statuses <statuses>] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: SESSION_LIST_FLAGS,
|
||||
exampleArgs: ["", "--agent assistant", "--all"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
|
||||
const { items: summaries, hasMore } = await withAgentErrors(() =>
|
||||
const {
|
||||
items: summaries,
|
||||
hasMore,
|
||||
nextPage,
|
||||
} = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return fetchAllPages(async (page) => {
|
||||
const result = await listSessionSummaries(runtime, {
|
||||
agent: flags.agent,
|
||||
provider: flags.provider,
|
||||
filter: page ? { page } : undefined,
|
||||
});
|
||||
return {
|
||||
items: result.summaries,
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
};
|
||||
}, flags.all);
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const result = await listSessionSummaries(runtime, {
|
||||
agent: flags.agent,
|
||||
provider: "bailian",
|
||||
filter: {
|
||||
page,
|
||||
limit: flags.limit,
|
||||
statuses: splitCommaSeparated(flags.statuses),
|
||||
created_at_gte: flags.createdAtGte,
|
||||
created_at_lte: flags.createdAtLte,
|
||||
},
|
||||
});
|
||||
return {
|
||||
items: result.summaries,
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
};
|
||||
},
|
||||
flags.all,
|
||||
flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const sessions = summaries.map((summary) => summary.session);
|
||||
if (format === "json") {
|
||||
emitResult({ sessions, has_more: hasMore }, format);
|
||||
emitResult({ sessions, has_more: hasMore, next_page: nextPage }, format);
|
||||
return;
|
||||
}
|
||||
if (sessions.length === 0) {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { ProviderSessionInfo } from "@openagentpack/sdk";
|
||||
import {
|
||||
archiveRemoteSession,
|
||||
listSessionSummaries,
|
||||
updateRemoteSession,
|
||||
} from "@openagentpack/sdk";
|
||||
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
emitCollection,
|
||||
matchesQuery,
|
||||
SEARCH_FLAGS,
|
||||
searchCursorPages,
|
||||
splitCommaSeparated,
|
||||
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 { readJsonInput } from "./_engine/output-file.ts";
|
||||
|
||||
const SESSION_ID_FLAG = {
|
||||
sessionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const SEARCH_RESOURCE_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Filter by configured agent name",
|
||||
"zh-CN": "按配置中的 Agent 名称筛选",
|
||||
},
|
||||
},
|
||||
statuses: {
|
||||
type: "string",
|
||||
valueHint: "<statuses>",
|
||||
description: {
|
||||
"en-US": "Comma-separated session statuses",
|
||||
"zh-CN": "Session 状态,多个以逗号分隔",
|
||||
},
|
||||
},
|
||||
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;
|
||||
|
||||
const UPDATE_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...SESSION_ID_FLAG,
|
||||
title: {
|
||||
type: "string",
|
||||
valueHint: "<title>",
|
||||
description: { "en-US": "New session title", "zh-CN": "新的 Session 标题" },
|
||||
},
|
||||
metadata: {
|
||||
type: "string",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "String-valued metadata JSON or @file",
|
||||
"zh-CN": "值为字符串的 Metadata JSON 或 @file",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const ARCHIVE_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...SESSION_ID_FLAG,
|
||||
} as const;
|
||||
|
||||
function sessionRows(sessions: ProviderSessionInfo[]): string[][] {
|
||||
return sessions.map((session) => [
|
||||
session.id,
|
||||
displayValue(session.title),
|
||||
displayValue(session.agent_id),
|
||||
session.status,
|
||||
displayValue(session.updated_at),
|
||||
]);
|
||||
}
|
||||
|
||||
async function parseMetadata(value?: string): Promise<Record<string, string> | undefined> {
|
||||
if (!value) return undefined;
|
||||
const parsed = await readJsonInput(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new BailianError("--metadata must be a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
for (const [key, entry] of Object.entries(parsed)) {
|
||||
if (typeof entry !== "string") {
|
||||
throw new BailianError(`Metadata value for '${key}' must be a string.`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
return parsed as Record<string, string>;
|
||||
}
|
||||
|
||||
export const managedAgentSessionSearch = defineCommand({
|
||||
description: { "en-US": "Search Managed Agent sessions", "zh-CN": "搜索托管 Agent Session" },
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--query <text> [--agent <name>] [--statuses <statuses>] [--limit <n>] [--page-limit <n>]",
|
||||
flags: SEARCH_RESOURCE_FLAGS,
|
||||
exampleArgs: ["--query debug", "--query failed --statuses failed --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 listSessionSummaries(runtime, {
|
||||
agent: ctx.flags.agent,
|
||||
provider: "bailian",
|
||||
filter: {
|
||||
page,
|
||||
limit: ctx.flags.limit ?? 100,
|
||||
statuses: splitCommaSeparated(ctx.flags.statuses),
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
},
|
||||
});
|
||||
return {
|
||||
items: response.summaries,
|
||||
hasMore: response.hasMore,
|
||||
nextPage: response.nextPage,
|
||||
};
|
||||
},
|
||||
(summary) =>
|
||||
matchesQuery(
|
||||
ctx.flags.query,
|
||||
summary.session.id,
|
||||
summary.session.title,
|
||||
summary.session.status,
|
||||
summary.session.agent_id,
|
||||
summary.agentName,
|
||||
),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const sessions = result.items.map((summary) => summary.session);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "sessions",
|
||||
items: sessions,
|
||||
headers: ["ID", "TITLE", "AGENT", "STATUS", "UPDATED"],
|
||||
rows: sessionRows(sessions),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching sessions found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentSessionUpdate = defineCommand({
|
||||
description: { "en-US": "Update a Managed Agent session", "zh-CN": "更新托管 Agent Session" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> [--title <title>] [--metadata <json|@path>]",
|
||||
flags: UPDATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--session-id sess_abc --title 'investigation'",
|
||||
"--session-id sess_abc --metadata @metadata.json",
|
||||
],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: (flags) =>
|
||||
!flags.title && !flags.metadata ? "Provide --title or --metadata." : undefined,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const input = { title: ctx.flags.title, metadata: await parseMetadata(ctx.flags.metadata) };
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_update_session: ctx.flags.sessionId, input }, format);
|
||||
return;
|
||||
}
|
||||
const session = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return updateRemoteSession(runtime, ctx.flags.sessionId, input, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(session, format);
|
||||
else emitBare(`Session ${session.id} updated.`);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentSessionArchive = defineCommand({
|
||||
description: { "en-US": "Archive a Managed Agent session", "zh-CN": "归档托管 Agent Session" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This archives the specified remote Managed Agent Session.",
|
||||
"zh-CN": "该操作会归档指定的远端托管 Agent Session。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--session-id <id>",
|
||||
flags: ARCHIVE_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc --dry-run", "--session-id sess_abc --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ would_archive_session: ctx.flags.sessionId }, format);
|
||||
return;
|
||||
}
|
||||
const session = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return archiveRemoteSession(runtime, ctx.flags.sessionId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult({ archived: ctx.flags.sessionId, session }, format);
|
||||
else emitBare(`Session ${ctx.flags.sessionId} archived.`);
|
||||
},
|
||||
});
|
||||
@@ -59,11 +59,6 @@ const SESSION_RUN_FLAGS = {
|
||||
valueHint: "<title>",
|
||||
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
noStream: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -108,7 +103,7 @@ export default defineCommand({
|
||||
|
||||
const runOptions = {
|
||||
agent: flags.agent,
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
environment: flags.environment,
|
||||
vault: flags.vault,
|
||||
memoryStores: parseMemoryStores(flags.memoryStores),
|
||||
@@ -121,7 +116,7 @@ export default defineCommand({
|
||||
would_run: {
|
||||
prompt: flags.prompt,
|
||||
agent: flags.agent ?? "auto",
|
||||
provider: flags.provider ?? "auto",
|
||||
provider: "bailian",
|
||||
environment: flags.environment,
|
||||
vault: flags.vault,
|
||||
memory_stores: runOptions.memoryStores,
|
||||
|
||||
@@ -27,11 +27,6 @@ const SESSION_SEND_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
noStream: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -68,7 +63,7 @@ export default defineCommand({
|
||||
would_send: {
|
||||
session_id: flags.sessionId,
|
||||
message: flags.message,
|
||||
provider: flags.provider ?? "auto",
|
||||
provider: "bailian",
|
||||
mode: flags.noStream ? "polling" : "streaming",
|
||||
},
|
||||
config_file: file,
|
||||
@@ -83,7 +78,7 @@ export default defineCommand({
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
if (flags.noStream) {
|
||||
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
});
|
||||
renderCollectedEvents(result, asJson, {
|
||||
session_id: flags.sessionId,
|
||||
@@ -94,7 +89,7 @@ export default defineCommand({
|
||||
flags.sessionId,
|
||||
flags.message,
|
||||
{
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
},
|
||||
);
|
||||
await streamAndRenderEvents(events, asJson, {
|
||||
|
||||
@@ -1,115 +1,2 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { listSkills } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
const SKILL_SOURCES = ["custom", "official", "all"] as const;
|
||||
type SkillSource = (typeof SKILL_SOURCES)[number];
|
||||
|
||||
const SKILL_LIST_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
source: {
|
||||
type: "string",
|
||||
valueHint: "<source>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Skill catalog: custom (workspace-uploaded, default), official (built-in), or all (both catalogs in one call)",
|
||||
"zh-CN":
|
||||
"Skill Catalog:custom(Workspace 上传,默认)、official(内置)或 all(一次调用获取两类 Catalog)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "List skills from the provider's skill catalog",
|
||||
"zh-CN": "列出 Provider Skill Catalog 中的 Skill",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--source custom|official|all] [--provider <name>] [--file <path>]",
|
||||
flags: SKILL_LIST_FLAGS,
|
||||
exampleArgs: [
|
||||
"",
|
||||
"--source official",
|
||||
"--source all --output json",
|
||||
"--source custom --provider bailian",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US": "Providers without a skill listing API (e.g. ark) return an empty list.",
|
||||
"zh-CN": "没有 Skill 列表 API 的 Provider(例如 ark)会返回空列表。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"For agent-driven skill selection, use `--source all --output json`: one call returns both catalogs with per-skill `source` and `description` fields to pick from.",
|
||||
"zh-CN":
|
||||
"由 Agent 选择 Skill 时,请使用 `--source all --output json`:一次调用返回两类 Catalog,并为每个 Skill 提供 `source` 和 `description` 字段用于选择。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"When generating a task that needs a suitable skill, call this command to match official or custom skills before wiring them into the task.",
|
||||
"zh-CN": "生成需要合适 Skill 的任务时,先调用此命令匹配官方或自定义 Skill,再将其接入任务。",
|
||||
},
|
||||
],
|
||||
validate: (f) =>
|
||||
f.source && !SKILL_SOURCES.includes(f.source as SkillSource)
|
||||
? "--source must be one of: custom, official, all."
|
||||
: undefined,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
const source = (flags.source as SkillSource | undefined) ?? "custom";
|
||||
|
||||
const skills = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
if (source !== "all") {
|
||||
return listSkills(runtime, { provider: flags.provider, source });
|
||||
}
|
||||
// Both catalogs in one call; each entry carries its own `source` field.
|
||||
const [customSkills, officialSkills] = await Promise.all([
|
||||
listSkills(runtime, { provider: flags.provider, source: "custom" }),
|
||||
listSkills(runtime, { provider: flags.provider, source: "official" }),
|
||||
]);
|
||||
return [...customSkills, ...officialSkills];
|
||||
}),
|
||||
);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ source, skills }, format);
|
||||
return;
|
||||
}
|
||||
if (skills.length === 0) {
|
||||
emitBare(source === "all" ? "No skills found." : `No ${source} skills found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "CREATED"];
|
||||
const rows = skills.map((skill) => [
|
||||
skill.id,
|
||||
skill.name.slice(0, 32),
|
||||
skill.source,
|
||||
skill.status,
|
||||
skill.latest_version ?? "-",
|
||||
skill.created_at ?? "-",
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
emitBare(`\nTotal: ${skills.length} (${source})`);
|
||||
},
|
||||
});
|
||||
// Backward-compatible alias for the API-oriented `managed-agent skill list` command.
|
||||
export { default } from "./skill/list.ts";
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const SKILL_SOURCES = ["custom", "official", "all"] as const;
|
||||
export type SkillSource = (typeof SKILL_SOURCES)[number];
|
||||
|
||||
const SOURCE_FLAG = {
|
||||
source: {
|
||||
type: "string",
|
||||
valueHint: "<source>",
|
||||
choices: SKILL_SOURCES,
|
||||
description: {
|
||||
"en-US": "Skill catalog: custom (default), official, or all",
|
||||
"zh-CN": "Skill Catalog:custom(默认)、official 或 all",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const SKILL_LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG };
|
||||
|
||||
export const SKILL_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...SOURCE_FLAG,
|
||||
};
|
||||
|
||||
export const SKILL_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
skillId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Skill ID", "zh-CN": "Skill ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const SKILL_VERSIONS_FLAGS = {
|
||||
...SKILL_GET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
};
|
||||
|
||||
export const SKILL_DOWNLOAD_FLAGS = {
|
||||
...SKILL_GET_FLAGS,
|
||||
skillVersion: {
|
||||
type: "string",
|
||||
valueHint: "<version>",
|
||||
required: true,
|
||||
description: { "en-US": "Skill version", "zh-CN": "Skill 版本" },
|
||||
},
|
||||
outputFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
|
||||
},
|
||||
force: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function skillRows(skills: ProviderSkillInfo[]): string[][] {
|
||||
return skills.map((skill) => [
|
||||
skill.id,
|
||||
displayValue(skill.name),
|
||||
skill.source,
|
||||
skill.status,
|
||||
displayValue(skill.latest_version),
|
||||
displayValue(skill.updated_at ?? skill.created_at),
|
||||
]);
|
||||
}
|
||||
|
||||
export function versionRows(versions: SkillVersionInfo[]): string[][] {
|
||||
return versions.map((version) => [
|
||||
displayValue(version.version),
|
||||
displayValue(version.name),
|
||||
displayValue(version.type),
|
||||
displayValue(version.status),
|
||||
displayValue(version.updated_at ?? version.created_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { dirname, relative, sep } from "node:path";
|
||||
import { inspectSkillSource } from "@openagentpack/sdk";
|
||||
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
resolveCandidateDeclaration,
|
||||
runScopedTopLevelCreate,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
source: {
|
||||
type: "string",
|
||||
valueHint: "<directory|zip|SKILL.md>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Local Skill directory, ZIP archive, or single SKILL.md",
|
||||
"zh-CN": "本地 Skill 目录、ZIP 压缩包或单个 SKILL.md",
|
||||
},
|
||||
},
|
||||
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 upload the Skill through the scoped create",
|
||||
"zh-CN": "写入 YAML 并通过定向创建上传 Skill",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one custom Managed Agent Skill from a local source",
|
||||
"zh-CN": "从本地来源声明并创建一个自定义托管 Agent Skill",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--source <directory|zip|SKILL.md> [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: ["--source ./skills/code-review", "--source ./skill.zip --yes"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"The YAML key is derived from SKILL.md frontmatter name. Remote URLs remain available through handwritten YAML plus full apply.",
|
||||
"zh-CN":
|
||||
"YAML key 从 SKILL.md frontmatter 的 name 生成。远程 URL 仍可手工写入 YAML 后执行全量 Apply。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => (!flags.source.trim() ? "--source must not be empty." : undefined),
|
||||
async run(ctx) {
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const inspected = await inspectSkillSource(ctx.flags.source, { basePath: process.cwd() });
|
||||
const source =
|
||||
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
|
||||
const existingSkills = (project.config.skills ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const trackedSameName = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
state.listResources().some((resource) => {
|
||||
if (resource.address.provider !== project.provider || resource.address.type !== "skill") {
|
||||
return false;
|
||||
}
|
||||
const declaration = existingSkills[resource.address.name];
|
||||
const canonicalName =
|
||||
typeof declaration?.name === "string" ? declaration.name : resource.address.name;
|
||||
return canonicalName === inspected.name;
|
||||
}),
|
||||
);
|
||||
if (trackedSameName) {
|
||||
throw new BailianError(
|
||||
`Skill '${inspected.name}' is already tracked.`,
|
||||
ExitCode.USAGE,
|
||||
"Modify its existing YAML declaration and run full `bl managed-agent apply` to create a new version.",
|
||||
);
|
||||
}
|
||||
const rawDeclaration: Record<string, unknown> = {
|
||||
name: inspected.name,
|
||||
source,
|
||||
origin: "custom",
|
||||
provider: project.provider,
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "skills",
|
||||
rawDeclaration,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "skills",
|
||||
resourceType: "skill",
|
||||
displayName: inspected.name,
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: existingSkills,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "skill",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { downloadRemoteSkill } 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 { writeOutputFile } from "../_engine/output-file.ts";
|
||||
import { SKILL_DOWNLOAD_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Download a Managed Agent skill version",
|
||||
"zh-CN": "下载托管 Agent Skill 版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--skill-id <id> --skill-version <version> --output-file <path> [--force]",
|
||||
flags: SKILL_DOWNLOAD_FLAGS,
|
||||
exampleArgs: ["--skill-id skill_abc --skill-version 3 --output-file ./skill.zip"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_download_skill: ctx.flags.skillId,
|
||||
version: ctx.flags.skillVersion,
|
||||
output_file: ctx.flags.outputFile,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const content = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return downloadRemoteSkill(runtime, ctx.flags.skillId, ctx.flags.skillVersion, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
|
||||
if (format === "json")
|
||||
emitResult(
|
||||
{ downloaded: ctx.flags.skillId, version: ctx.flags.skillVersion, output_file: outputFile },
|
||||
format,
|
||||
);
|
||||
else emitBare(`Skill downloaded to ${outputFile}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getRemoteSkill } 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 { SKILL_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Get a Managed Agent skill", "zh-CN": "获取托管 Agent Skill 详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--skill-id <id>",
|
||||
flags: SKILL_GET_FLAGS,
|
||||
exampleArgs: ["--skill-id skill_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const skill = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteSkill(runtime, ctx.flags.skillId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(skill, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${skill.id}`);
|
||||
emitBare(`Name: ${skill.name}`);
|
||||
emitBare(`Description: ${displayValue(skill.description, 120)}`);
|
||||
emitBare(`Source: ${skill.source}`);
|
||||
emitBare(`Status: ${skill.status}`);
|
||||
emitBare(`Version: ${displayValue(skill.latest_version)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { listRemoteSkills } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
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 { SKILL_LIST_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
|
||||
|
||||
export function buildSkillListJsonResult<T>(
|
||||
source: SkillSource,
|
||||
result: { items: T[]; hasMore: boolean; nextPage?: string },
|
||||
) {
|
||||
return {
|
||||
source,
|
||||
skills: result.items,
|
||||
has_more: result.hasMore,
|
||||
next_page: result.nextPage,
|
||||
};
|
||||
}
|
||||
|
||||
async function listOneCatalog(
|
||||
runtime: Parameters<typeof listRemoteSkills>[0],
|
||||
source: Exclude<SkillSource, "all">,
|
||||
options: { limit?: number; page?: string; all?: boolean },
|
||||
) {
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteSkills(runtime, {
|
||||
provider: "bailian",
|
||||
source,
|
||||
limit: options.limit,
|
||||
page,
|
||||
});
|
||||
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
|
||||
},
|
||||
options.all,
|
||||
options.page,
|
||||
);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent skills", "zh-CN": "列出托管 Agent Skill" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--source custom|official|all] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: SKILL_LIST_FLAGS,
|
||||
exampleArgs: ["", "--source official", "--source all --all --output json"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"--source all combines one page from each catalog, or every page with --all; it does not accept --page.",
|
||||
"zh-CN":
|
||||
"--source all 默认合并两个 Catalog 的各一页,传入 --all 时获取全部分页;该模式不接受 --page。",
|
||||
},
|
||||
],
|
||||
validate: (flags) =>
|
||||
validateLimitAndPageLimit(flags) ??
|
||||
(flags.source === "all" && flags.page
|
||||
? "--source all cannot be combined with --page."
|
||||
: undefined),
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
if (source !== "all") {
|
||||
return listOneCatalog(runtime, source, {
|
||||
limit: ctx.flags.limit,
|
||||
page: ctx.flags.page,
|
||||
all: ctx.flags.all,
|
||||
});
|
||||
}
|
||||
const [custom, official] = await Promise.all([
|
||||
listOneCatalog(runtime, "custom", {
|
||||
limit: ctx.flags.limit,
|
||||
all: ctx.flags.all,
|
||||
}),
|
||||
listOneCatalog(runtime, "official", {
|
||||
limit: ctx.flags.limit,
|
||||
all: ctx.flags.all,
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
items: [...custom.items, ...official.items],
|
||||
hasMore: custom.hasMore || official.hasMore,
|
||||
nextPage: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(buildSkillListJsonResult(source, result), format);
|
||||
return;
|
||||
}
|
||||
emitCollection({
|
||||
format,
|
||||
key: "skills",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
|
||||
rows: skillRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No skills found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { listRemoteSkills } 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 { SKILL_SEARCH_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Search Managed Agent skills", "zh-CN": "搜索托管 Agent Skill" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--source custom|official|all] [--limit <n>] [--page-limit <n>]",
|
||||
flags: SKILL_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query browser --source official", "--query report --source all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const searchCatalog = (catalog: Exclude<SkillSource, "all">) =>
|
||||
searchCursorPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteSkills(runtime, {
|
||||
provider: "bailian",
|
||||
source: catalog,
|
||||
limit: ctx.flags.limit ?? 100,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
(skill) => matchesQuery(ctx.flags.query, skill.id, skill.name, skill.description),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
if (source !== "all") return searchCatalog(source);
|
||||
const [custom, official] = await Promise.all([
|
||||
searchCatalog("custom"),
|
||||
searchCatalog("official"),
|
||||
]);
|
||||
return {
|
||||
items: [...custom.items, ...official.items],
|
||||
hasMore: custom.hasMore || official.hasMore,
|
||||
nextPage: undefined,
|
||||
scannedPages: custom.scannedPages + official.scannedPages,
|
||||
truncated: custom.truncated || official.truncated,
|
||||
};
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "skills",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
|
||||
rows: skillRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching skills found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { listRemoteSkillVersions } 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 { SKILL_VERSIONS_FLAGS, versionRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "List Managed Agent skill versions",
|
||||
"zh-CN": "列出托管 Agent Skill 版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--skill-id <id> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: SKILL_VERSIONS_FLAGS,
|
||||
exampleArgs: ["--skill-id skill_abc", "--skill-id skill_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 listRemoteSkillVersions(runtime, ctx.flags.skillId, {
|
||||
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: ["VERSION", "NAME", "TYPE", "STATUS", "UPDATED"],
|
||||
rows: versionRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No skill versions found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { importResource, parseStateAddress } from "@openagentpack/sdk";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
@@ -8,7 +14,7 @@ import { withAgentErrors } from "./_engine/errors.ts";
|
||||
const STATE_IMPORT_FLAGS = {
|
||||
address: {
|
||||
type: "string",
|
||||
valueHint: "<provider.type.name>",
|
||||
valueHint: "<bailian.type.name>",
|
||||
description: {
|
||||
"en-US": "Resource state address (required)",
|
||||
"zh-CN": "资源 State 地址(必填)",
|
||||
@@ -42,6 +48,17 @@ const STATE_IMPORT_FLAGS = {
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function parseBailianStateAddress(address: string) {
|
||||
const parsed = parseStateAddress(address, { requireProvider: true });
|
||||
if (parsed.provider !== "bailian") {
|
||||
throw new BailianError(
|
||||
`bl managed-agent can import only Bailian resources; address provider was '${parsed.provider}'. / bl managed-agent 只能导入百炼资源;地址中的 Provider 为 '${parsed.provider}'。`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Import an existing remote resource into agents state",
|
||||
@@ -49,7 +66,7 @@ export default defineCommand({
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--address <provider.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
|
||||
"--address <bailian.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
|
||||
flags: STATE_IMPORT_FLAGS,
|
||||
exampleArgs: ["--address bailian.agent.assistant --remote-id agent-abc123"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -61,7 +78,7 @@ export default defineCommand({
|
||||
if (settings.dryRun) {
|
||||
// Validate the address shape locally so dry-run still catches usage errors.
|
||||
await withAgentErrors(async () => {
|
||||
parseStateAddress(flags.address, { requireProvider: true });
|
||||
parseBailianStateAddress(flags.address);
|
||||
});
|
||||
emitResult(
|
||||
{
|
||||
@@ -78,9 +95,7 @@ export default defineCommand({
|
||||
await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
// Parse first so a malformed address fails fast, before any config I/O.
|
||||
const parsed = parseStateAddress(flags.address, {
|
||||
requireProvider: true,
|
||||
});
|
||||
const parsed = parseBailianStateAddress(flags.address);
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
await importResource(runtime, parsed, flags.remoteId, {
|
||||
resourceVersion: flags.resourceVersion,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { validateProjectConfig } from "@openagentpack/sdk";
|
||||
import { OFFLINE_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
|
||||
|
||||
const VALIDATE_FLAGS = {
|
||||
file: {
|
||||
@@ -56,7 +56,10 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
throw new BailianError(`Validation failed with ${errorCount} error(s).`, ExitCode.GENERAL);
|
||||
throw new BailianError(
|
||||
formatAgentDiagnosticFailure(diagnostics, `Validation failed with ${errorCount} error(s).`),
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { CloudVault } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const VAULT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const VAULT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const VAULT_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
vaultId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Vault ID", "zh-CN": "Vault ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function vaultRows(vaults: CloudVault[]): string[][] {
|
||||
return vaults.map((vault) => [
|
||||
vault.id,
|
||||
displayValue(vault.display_name),
|
||||
displayValue(vault.type),
|
||||
displayValue(vault.created_at),
|
||||
displayValue(vault.updated_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 Vault display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Vault 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
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 empty Managed Agent Vault through a scoped YAML apply",
|
||||
"zh-CN": "通过定向 YAML Apply 声明并创建一个空的托管 Agent Vault",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--name <name> [--metadata <key=value>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: ["--name Production", "--name Production --metadata owner=platform --yes"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US": "Creates an empty Vault. Add secrets later with `vault credential create`.",
|
||||
"zh-CN": "创建空 Vault;随后使用 `vault credential create` 添加 Secret。",
|
||||
},
|
||||
],
|
||||
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 rawDeclaration: Record<string, unknown> = {
|
||||
display_name: ctx.flags.name.trim(),
|
||||
provider: project.provider,
|
||||
credentials: [],
|
||||
metadata: parseMetadata(ctx.flags.metadata),
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "vaults",
|
||||
rawDeclaration,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "vaults",
|
||||
resourceType: "vault",
|
||||
displayName: ctx.flags.name.trim(),
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: (project.config.vaults ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.display_name === "string" ? declaration.display_name : key,
|
||||
fallbackKey: "vault",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
import { dirname } from "node:path";
|
||||
import {
|
||||
bootstrapRuntimeCredentialsSync,
|
||||
createVaultCredentialWithStateBackend,
|
||||
planVaultCredentialCreateWithStateBackend,
|
||||
type ResolvedProjectConfig,
|
||||
resolveProjectConfigFromObject,
|
||||
} 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 { CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../../_engine/console-capture.ts";
|
||||
import { retainAgentError, withAgentErrors } from "../../_engine/errors.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
parseMetadata,
|
||||
replaceConfigAtomically,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
vault: {
|
||||
type: "string",
|
||||
valueHint: "<yaml-key>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Existing tracked Vault key from agents.yaml",
|
||||
"zh-CN": "agents.yaml 中已跟踪的 Vault key",
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Credential display name",
|
||||
"zh-CN": "Credential 显示名称",
|
||||
},
|
||||
},
|
||||
secretName: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Environment variable name exposed to the Agent",
|
||||
"zh-CN": "提供给 Agent 的环境变量名",
|
||||
},
|
||||
},
|
||||
secretEnv: {
|
||||
type: "string",
|
||||
valueHint: "<env-name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Local environment variable containing the secret value",
|
||||
"zh-CN": "保存真实 Secret 的本地环境变量名",
|
||||
},
|
||||
},
|
||||
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 create the remote Credential",
|
||||
"zh-CN": "写入 YAML 并创建远端 Credential",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Append and create one environment-variable Credential in a tracked Vault",
|
||||
"zh-CN": "在已跟踪 Vault 中追加并创建一个环境变量 Credential",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--vault <yaml-key> --name <name> --secret-name <name> --secret-env <env-name> [--metadata <key=value>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: [
|
||||
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN",
|
||||
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN --yes",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"--secret-env is an environment variable name, not the secret itself. The CLI auto-loads the nearest .env from the current directory upward; shell exports and CI secret injection also work.",
|
||||
"zh-CN":
|
||||
"--secret-env 接收环境变量名,不接收 Secret 本身。CLI 会从当前目录向上自动加载最近的 .env,也支持 Shell export 和 CI Secret 注入。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"YAML stores only ${ENV_NAME}. Never commit .env; subsequent full apply runs must provide the same environment variable.",
|
||||
"zh-CN":
|
||||
"YAML 只保存 ${ENV_NAME}。不要提交 .env;后续执行全量 Apply 时仍需提供同名环境变量。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (!flags.vault.trim()) return "--vault must not be empty.";
|
||||
if (!flags.name.trim()) return "--name must not be empty.";
|
||||
if (!flags.secretName.trim()) return "--secret-name must not be empty.";
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(flags.secretEnv)) {
|
||||
return "--secret-env must be a valid environment variable name.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
bootstrapRuntimeCredentialsSync();
|
||||
const secretValue = process.env[ctx.flags.secretEnv];
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml").finally(
|
||||
() => {
|
||||
delete process.env[ctx.flags.secretEnv];
|
||||
},
|
||||
);
|
||||
const vault = project.config.vaults?.[ctx.flags.vault];
|
||||
if (!vault) {
|
||||
throw new BailianError(
|
||||
`Vault '${ctx.flags.vault}' is not declared in agents.yaml.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (ctx.flags.yes && !ctx.settings.dryRun && !secretValue) {
|
||||
throw new BailianError(
|
||||
`Environment variable '${ctx.flags.secretEnv}' is not set or is empty.`,
|
||||
ExitCode.USAGE,
|
||||
`Set it in your shell, CI secret store, or a local .env file, then re-run with --secret-env ${ctx.flags.secretEnv}.`,
|
||||
);
|
||||
}
|
||||
const metadata = parseMetadata(ctx.flags.metadata);
|
||||
const rawCredential = {
|
||||
name: ctx.flags.name.trim(),
|
||||
type: "environment_variable" as const,
|
||||
secret_name: ctx.flags.secretName.trim(),
|
||||
secret_value: `\${${ctx.flags.secretEnv}}`,
|
||||
networking: { type: "unrestricted" as const },
|
||||
...(metadata ? { metadata } : {}),
|
||||
};
|
||||
const runtimeCredential = {
|
||||
...rawCredential,
|
||||
secret_value: secretValue ?? "__dry_run_secret__",
|
||||
};
|
||||
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,
|
||||
);
|
||||
}
|
||||
const rawConfig = document.toJS() as {
|
||||
vaults?: Record<string, { credentials?: unknown[] }>;
|
||||
};
|
||||
const rawCredentials = rawConfig.vaults?.[ctx.flags.vault]?.credentials ?? [];
|
||||
const sameNameCredentials = rawCredentials.filter(
|
||||
(credential): credential is Record<string, unknown> =>
|
||||
Boolean(
|
||||
credential &&
|
||||
typeof credential === "object" &&
|
||||
!Array.isArray(credential) &&
|
||||
(credential as Record<string, unknown>).name === rawCredential.name,
|
||||
),
|
||||
);
|
||||
let reusedPending = false;
|
||||
if (sameNameCredentials.length > 0) {
|
||||
const lastCredential = rawCredentials.at(-1);
|
||||
const existing = sameNameCredentials[0]!;
|
||||
if (
|
||||
sameNameCredentials.length !== 1 ||
|
||||
existing !== lastCredential ||
|
||||
!sameCredentialDeclaration(existing, rawCredential)
|
||||
) {
|
||||
throw new BailianError(
|
||||
`Vault '${ctx.flags.vault}' already declares a different credential named '${rawCredential.name}'.`,
|
||||
ExitCode.USAGE,
|
||||
"Modify the existing YAML declaration and run full `bl managed-agent apply`; create never overwrites it.",
|
||||
);
|
||||
}
|
||||
reusedPending = true;
|
||||
}
|
||||
const rawCandidate = structuredClone(project.config) as unknown as Record<string, unknown>;
|
||||
const rawVaults = rawCandidate.vaults as Record<string, Record<string, unknown>>;
|
||||
const rawVault = rawVaults[ctx.flags.vault]!;
|
||||
if (!reusedPending) {
|
||||
rawVaults[ctx.flags.vault] = {
|
||||
...rawVault,
|
||||
credentials: [
|
||||
...((rawVault.credentials as unknown[] | undefined) ?? []),
|
||||
runtimeCredential,
|
||||
],
|
||||
};
|
||||
}
|
||||
const resolvedCandidate = await withAgentErrors(() =>
|
||||
resolveProjectConfigFromObject(rawCandidate, {
|
||||
projectName: project.projectName,
|
||||
basePath: dirname(project.configPath),
|
||||
}),
|
||||
);
|
||||
const candidateConfig = resolvedCandidate.config;
|
||||
if (!reusedPending) {
|
||||
document.addIn(["vaults", ctx.flags.vault, "credentials"], rawCredential);
|
||||
}
|
||||
const nextSource = reusedPending ? project.source : document.toString();
|
||||
const backendInput = {
|
||||
projectName: project.projectName,
|
||||
config: candidateConfig as ResolvedProjectConfig,
|
||||
configPath: project.configPath,
|
||||
providers: { [project.provider]: candidateConfig.providers[project.provider] },
|
||||
stateBackend: project.stateBackend,
|
||||
stateScope: project.stateScope,
|
||||
};
|
||||
|
||||
if (ctx.settings.dryRun || !ctx.flags.yes) {
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planVaultCredentialCreateWithStateBackend(
|
||||
backendInput,
|
||||
ctx.flags.vault,
|
||||
ctx.flags.name.trim(),
|
||||
{
|
||||
provider: project.provider,
|
||||
refresh: !ctx.settings.dryRun,
|
||||
quiet: format === "json",
|
||||
checkRemote: !ctx.settings.dryRun,
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
const result = {
|
||||
vault: { key: ctx.flags.vault, remote_id: planned.vaultRemoteId },
|
||||
credential: {
|
||||
name: ctx.flags.name.trim(),
|
||||
secret_name: ctx.flags.secretName.trim(),
|
||||
secret_env: ctx.flags.secretEnv,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: false,
|
||||
reused_pending: reusedPending,
|
||||
requires_confirmation: !ctx.settings.dryRun,
|
||||
ready_to_create: true,
|
||||
remote_checked: planned.remoteChecked,
|
||||
reuse_remote: planned.reuseRemote,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Vault: ${ctx.flags.vault}`);
|
||||
emitBare(`Credential: ${ctx.flags.name.trim()} (secret from ${ctx.flags.secretEnv})`);
|
||||
emitBare(
|
||||
ctx.settings.dryRun
|
||||
? "Dry run: YAML, State, and remote resources were not changed."
|
||||
: "Preview only: re-run with --yes to append YAML and create this Credential.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceConfigAtomically(project.configPath, project.source, nextSource);
|
||||
let created;
|
||||
try {
|
||||
created = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
createVaultCredentialWithStateBackend(
|
||||
backendInput,
|
||||
ctx.flags.vault,
|
||||
ctx.flags.name.trim(),
|
||||
{ provider: project.provider, refresh: true, quiet: format === "json" },
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw retainAgentError(
|
||||
error,
|
||||
"The YAML declaration was kept. Restore the same secret environment variable and re-run this command; an already-created matching Credential will be adopted.",
|
||||
"Vault Credential create failed.",
|
||||
);
|
||||
}
|
||||
const result = {
|
||||
vault: { key: ctx.flags.vault, remote_id: created.vaultRemoteId },
|
||||
credential: {
|
||||
id: created.credentialId,
|
||||
name: ctx.flags.name.trim(),
|
||||
secret_name: ctx.flags.secretName.trim(),
|
||||
secret_env: ctx.flags.secretEnv,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: true,
|
||||
reused_pending: reusedPending,
|
||||
adopted: created.adopted,
|
||||
status: "completed",
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Wrote ${project.configPath} and created Credential '${ctx.flags.name.trim()}'.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function sameCredentialDeclaration(
|
||||
existing: Record<string, unknown>,
|
||||
candidate: Record<string, unknown>,
|
||||
): boolean {
|
||||
return canonicalJson(existing) === canonicalJson(candidate);
|
||||
}
|
||||
|
||||
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(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, entry]) => [key, normalize(entry)]),
|
||||
);
|
||||
};
|
||||
return JSON.stringify(normalize(value));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getRemoteVault } 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 { VAULT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Get a Managed Agent vault", "zh-CN": "获取托管 Agent Vault 详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--vault-id <id>",
|
||||
flags: VAULT_GET_FLAGS,
|
||||
exampleArgs: ["--vault-id vault_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const vault = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteVault(runtime, ctx.flags.vaultId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(vault, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${vault.id}`);
|
||||
emitBare(`Name: ${displayValue(vault.display_name)}`);
|
||||
emitBare(`Type: ${displayValue(vault.type)}`);
|
||||
emitBare(`Created: ${displayValue(vault.created_at)}`);
|
||||
emitBare(`Updated: ${displayValue(vault.updated_at)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { listRemoteVaults } 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 { VAULT_LIST_FLAGS, vaultRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent vaults", "zh-CN": "列出托管 Agent Vault" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
|
||||
flags: VAULT_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 listRemoteVaults(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: "vaults",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"],
|
||||
rows: vaultRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No vaults found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { listRemoteVaults } 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 { VAULT_SEARCH_FLAGS, vaultRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Search Managed Agent vaults", "zh-CN": "搜索托管 Agent Vault" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
|
||||
flags: VAULT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query github", "--query production --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 listRemoteVaults(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,
|
||||
};
|
||||
},
|
||||
(vault) => matchesQuery(ctx.flags.query, vault.id, vault.display_name, vault.metadata),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "vaults",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"],
|
||||
rows: vaultRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching vaults found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
|
||||
|
||||
const PLAYGROUND_BASE_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Local port (default: 4848)",
|
||||
"zh-CN": "本地端口(默认:4848)",
|
||||
},
|
||||
},
|
||||
noOpen: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Do not open a browser automatically",
|
||||
"zh-CN": "不自动打开浏览器",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const PLAYGROUND_FLAGS = {
|
||||
...PLAYGROUND_BASE_FLAGS,
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Agent to preview (required when the project declares multiple Agents)",
|
||||
"zh-CN": "要预览的 Agent(项目包含多个 Agent 时需要指定)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const PLAYGROUND_NOTES = [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.",
|
||||
"zh-CN":
|
||||
"会话预览需要 Node.js 22+,并继续使用 agents.yaml;目录 Workbench 位于 managed-agent project workbench。",
|
||||
},
|
||||
];
|
||||
|
||||
export const managedAgentPlayground = defineCommand({
|
||||
description: {
|
||||
"en-US": "Launch a Session Preview for an agents.yaml Agent",
|
||||
"zh-CN": "为 agents.yaml 中的 Agent 启动会话预览",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--agent <id>] [--port <n>] [--no-open]",
|
||||
flags: PLAYGROUND_FLAGS,
|
||||
exampleArgs: ["", "--agent assistant", "--file agents.yaml --no-open"],
|
||||
notes: PLAYGROUND_NOTES,
|
||||
async run(ctx) {
|
||||
const file = ctx.flags.file ?? "agents.yaml";
|
||||
const port = ctx.flags.port ?? 4848;
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_launch: "playground",
|
||||
config_file: file,
|
||||
agent: ctx.flags.agent,
|
||||
port,
|
||||
open_browser: !ctx.flags.noOpen,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchManagedAgentPlayground({
|
||||
file,
|
||||
agent: ctx.flags.agent,
|
||||
port,
|
||||
open: !ctx.flags.noOpen,
|
||||
surface: "preview",
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
import {
|
||||
agentStudioFilesPath,
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
sandboxBaseUrl,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { redactConnectionCredentials, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const UPLOAD_SOURCE = "sandbox_template";
|
||||
|
||||
export const sandboxFileUpload = defineCommand({
|
||||
description: {
|
||||
"en-US": "Upload a workspace file for Sandbox template mounts",
|
||||
"zh-CN": "上传工作空间文件,供 Sandbox 模版挂载使用",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
|
||||
flags: {
|
||||
...WORKSPACE_FLAG,
|
||||
path: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
|
||||
},
|
||||
filename: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
|
||||
},
|
||||
mimeType: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: {
|
||||
"en-US": "Multipart file MIME type (default: application/octet-stream)",
|
||||
"zh-CN": "Multipart 文件部分的 MIME 类型(默认:application/octet-stream)",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--path ./config.json --output json",
|
||||
"--path ./config.json --quiet",
|
||||
"--path ./notes.txt --filename notes.txt --mime-type text/plain --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"POST /api/v1/agentstudio/files with multipart fields file and source=sandbox_template. Uses a Bailian Bearer API Key, not Console authentication or an E2B key; no agents.yaml is needed.",
|
||||
"zh-CN":
|
||||
"向 /api/v1/agentstudio/files 发送 multipart 字段 file 和 source=sandbox_template。使用百炼 Bearer API Key,不使用 Console 鉴权或 E2B Key;无需 agents.yaml。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Base URL follows Sandbox: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. Without one, --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id selects the cn-beijing origin. The upload path has no /sandbox prefix.",
|
||||
"zh-CN":
|
||||
"Base URL 沿用 Sandbox:--base-url > DASHSCOPE_BASE_URL > 登录/Profile 的 base_url。未配置时,按 --workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id 选择 cn-beijing 地址。上传路径不带 /sandbox 前缀。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Returns the upload response immediately; --quiet prints only its id. Upload does not wait for security review: status=checking is not ready to mount. Use an available file's id as mntConfig[].originFileId in template create/update --body, together with mountPath and optional originFileName, in the same workspace. This does not transfer files into a running instance.",
|
||||
"zh-CN":
|
||||
"上传响应返回后立即输出,--quiet 仅输出 id。不会等待安全审核:status=checking 不代表已可挂载。在同一工作空间的 template create/update --body 中,将可用文件的 id 填入 mntConfig[].originFileId,同时传入 mountPath 和可选的 originFileName。此命令不向运行中的实例传文件。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--dry-run previews the endpoint, source, and local path without reading or uploading the file. The service detects the MIME type and enforces upload limits.",
|
||||
"zh-CN":
|
||||
"--dry-run 仅预览 Endpoint、source 和本地路径,不读取或上传文件。MIME 类型检测和上传限制由服务端执行。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const endpoint = ctx.client.url(agentStudioFilesPath(), () =>
|
||||
sandboxBaseUrl(resolveWorkspaceId(ctx)),
|
||||
);
|
||||
const filename = ctx.flags.filename ?? basename(ctx.flags.path);
|
||||
const mimeType = ctx.flags.mimeType ?? "application/octet-stream";
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
method: "POST",
|
||||
endpoint,
|
||||
request: { source: UPLOAD_SOURCE, file: { path: ctx.flags.path, filename, mimeType } },
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = new Uint8Array(await readFile(ctx.flags.path));
|
||||
const form = new FormData();
|
||||
form.append("source", UPLOAD_SOURCE);
|
||||
form.append("file", new Blob([content], { type: mimeType }), filename);
|
||||
const file = await ctx.client.requestJson<Record<string, unknown>>({
|
||||
method: "POST",
|
||||
path: endpoint,
|
||||
body: form,
|
||||
});
|
||||
if (!file || typeof file.id !== "string" || !file.id.trim()) {
|
||||
throw new BailianError(
|
||||
"Upload response did not contain a File ID. / 上传响应未包含 File ID。",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
if (ctx.settings.quiet) emitBare(file.id);
|
||||
else emitResult(redactConnectionCredentials(file), format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { defineCommand, detectOutputFormat, UsageError } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
|
||||
/** Pinned CLI presets, not a live catalog or a guarantee of regional availability. */
|
||||
export const SANDBOX_IMAGES = [
|
||||
{
|
||||
id: "code-interpreter",
|
||||
imageName: "代码解释器",
|
||||
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/code-interpreter-v1:v0.0.44",
|
||||
icon: "https://img.alicdn.com/imgextra/i2/O1CN01iX8RH9ckFvC093x2_!!6000000000905-2-tps-72-72.png",
|
||||
description: {
|
||||
"en-US": "Python / Node.js runtimes with common data-processing libraries",
|
||||
"zh-CN": "Python / Node.js 运行时+常用数据处理库",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "browser",
|
||||
imageName: "浏览器",
|
||||
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
|
||||
icon: "https://img.alicdn.com/imgextra/i4/O1CN01ShCbPEunrRI093x2_!!6000000000155-2-tps-72-72.png",
|
||||
description: {
|
||||
"en-US": "Chromium and a visual desktop for clicking, filling forms, and screenshots",
|
||||
"zh-CN": "Chromium +可视化桌面,支持点击/ 填表/截图",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "all-in-one",
|
||||
imageName: "全能型",
|
||||
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/all-in-one:v0.0.44",
|
||||
icon: "https://img.alicdn.com/imgextra/i1/O1CN01smhklUahsyE093x2_!!6000000006657-2-tps-72-72.png",
|
||||
description: {
|
||||
"en-US": "Code execution and browser capabilities together",
|
||||
"zh-CN": "代码执行+浏览器双能力",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const SANDBOX_IMAGE_CHOICES = SANDBOX_IMAGES.flatMap((image) => [image.id, image.imageName]);
|
||||
|
||||
export const SANDBOX_IMAGE_NOTES = SANDBOX_IMAGES.map((image) => ({
|
||||
"en-US": `${image.id} (${image.imageName}): ${image.description["en-US"]}. ${image.imageUrl}`,
|
||||
"zh-CN": `${image.id}(${image.imageName}):${image.description["zh-CN"]}。${image.imageUrl}`,
|
||||
}));
|
||||
|
||||
export function resolveSandboxImage(selector: string) {
|
||||
const image = SANDBOX_IMAGES.find(
|
||||
(candidate) => candidate.id === selector || candidate.imageName === selector,
|
||||
);
|
||||
if (!image) {
|
||||
throw new UsageError(
|
||||
`Unknown built-in image / 未知内置镜像: ${selector}. ${SANDBOX_IMAGE_CHOICES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
export const sandboxOfficialImages = defineCommand({
|
||||
description: {
|
||||
"en-US": "List the built-in Sandbox base images (offline)",
|
||||
"zh-CN": "列出 CLI 内置的 Sandbox 基础镜像(离线)",
|
||||
},
|
||||
auth: "none",
|
||||
exampleArgs: ["", "--output json", "--quiet"],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Use a preset ID or Chinese name with template create/update --image. These are pinned cn-beijing images; other environments may differ. Use --from-image for a custom image.",
|
||||
"zh-CN":
|
||||
"在 template create/update 中通过 --image 传入预设 ID 或中文名。这些是固定版本的 cn-beijing 镜像,其他环境可能不同;自定义镜像使用 --from-image。",
|
||||
},
|
||||
...SANDBOX_IMAGE_NOTES,
|
||||
],
|
||||
async run(ctx) {
|
||||
if (ctx.settings.quiet) {
|
||||
for (const image of SANDBOX_IMAGES) emitBare(image.id);
|
||||
return;
|
||||
}
|
||||
if (detectOutputFormat(ctx.settings.output) === "json") {
|
||||
emitResult(SANDBOX_IMAGES, "json");
|
||||
return;
|
||||
}
|
||||
for (const image of SANDBOX_IMAGES) {
|
||||
emitBare(`${image.id} (${image.imageName})`);
|
||||
emitBare(` ${image.imageUrl}`);
|
||||
emitBare(` ${image.description["en-US"]} / ${image.description["zh-CN"]}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
import {
|
||||
BailianError,
|
||||
detectOutputFormat,
|
||||
defineCommand,
|
||||
ExitCode,
|
||||
SANDBOX_PATHS,
|
||||
sandboxInstanceActionPath,
|
||||
sandboxInstancePath,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import {
|
||||
BODY_FLAG,
|
||||
displayValue,
|
||||
INSTANCE_TIMEOUT_FLAG,
|
||||
mergeObjectField,
|
||||
parseKeyValueEntries,
|
||||
readRequestBody,
|
||||
redactConnectionCredentials,
|
||||
redactRequestSecrets,
|
||||
resolveSandboxEndpoint,
|
||||
SANDBOX_NOTES,
|
||||
setDefined,
|
||||
SHOW_CREDENTIALS_FLAG,
|
||||
validateIntegerRange,
|
||||
WORKSPACE_FLAG,
|
||||
type JsonObject,
|
||||
} from "./shared.ts";
|
||||
|
||||
interface SandboxInfo extends JsonObject {
|
||||
sandboxID?: string;
|
||||
templateID?: string;
|
||||
state?: string;
|
||||
cpuCount?: number;
|
||||
memoryMB?: number;
|
||||
startedAt?: string;
|
||||
endAt?: string;
|
||||
}
|
||||
|
||||
const SANDBOX_ID_FLAG = {
|
||||
sandboxId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Sandbox instance ID", "zh-CN": "Sandbox 实例 ID" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...BODY_FLAG,
|
||||
templateId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Ready template ID; may alternatively be supplied as templateID in --body",
|
||||
"zh-CN": "已就绪的模版 ID;也可通过 --body 中的 templateID 提供",
|
||||
},
|
||||
},
|
||||
...INSTANCE_TIMEOUT_FLAG,
|
||||
allowInternetAccess: {
|
||||
type: "boolean",
|
||||
valueHint: "<bool>",
|
||||
description: { "en-US": "Allow public internet access", "zh-CN": "允许访问公网" },
|
||||
},
|
||||
metadata: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Instance metadata entry; repeat for multiple values",
|
||||
"zh-CN": "实例 Metadata 键值;可重复传入",
|
||||
},
|
||||
},
|
||||
env: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Instance environment variable; repeat for multiple values",
|
||||
"zh-CN": "实例环境变量;可重复传入",
|
||||
},
|
||||
},
|
||||
autoPause: {
|
||||
type: "boolean",
|
||||
valueHint: "<bool>",
|
||||
description: {
|
||||
"en-US": "Pause the instance when its timeout expires",
|
||||
"zh-CN": "实例超时后自动暂停",
|
||||
},
|
||||
},
|
||||
autoResume: {
|
||||
type: "boolean",
|
||||
valueHint: "<bool>",
|
||||
description: {
|
||||
"en-US": "Automatically resume a paused instance when connecting",
|
||||
"zh-CN": "连接已暂停实例时自动恢复",
|
||||
},
|
||||
},
|
||||
allowOut: {
|
||||
type: "array",
|
||||
valueHint: "<address>",
|
||||
description: {
|
||||
"en-US": "Outbound allow-list entry; repeat for multiple values",
|
||||
"zh-CN": "出站白名单条目;可重复传入",
|
||||
},
|
||||
},
|
||||
denyOut: {
|
||||
type: "array",
|
||||
valueHint: "<address>",
|
||||
description: {
|
||||
"en-US": "Outbound deny-list entry; repeat for multiple values",
|
||||
"zh-CN": "出站黑名单条目;可重复传入",
|
||||
},
|
||||
},
|
||||
maskRequestHost: {
|
||||
type: "string",
|
||||
valueHint: "<host>",
|
||||
description: {
|
||||
"en-US": "Override the outbound request Host header",
|
||||
"zh-CN": "覆盖出站请求的 Host Header",
|
||||
},
|
||||
},
|
||||
...SHOW_CREDENTIALS_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
|
||||
|
||||
const LIST_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
templateId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Filter by template ID", "zh-CN": "按模版 ID 筛选" },
|
||||
},
|
||||
sandboxId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Filter by sandbox ID", "zh-CN": "按 Sandbox ID 筛选" },
|
||||
},
|
||||
state: {
|
||||
type: "string",
|
||||
valueHint: "<state>",
|
||||
description: {
|
||||
"en-US": "Filter by state, for example running or paused",
|
||||
"zh-CN": "按状态筛选,例如 running 或 paused",
|
||||
},
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Maximum results (1-50)", "zh-CN": "最大返回数量(1–50)" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const GET_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...SANDBOX_ID_FLAG,
|
||||
...SHOW_CREDENTIALS_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
const ACTION_FLAGS = { ...WORKSPACE_FLAG, ...SANDBOX_ID_FLAG } satisfies FlagsDef;
|
||||
const CONNECTION_ACTION_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...SANDBOX_ID_FLAG,
|
||||
...BODY_FLAG,
|
||||
...INSTANCE_TIMEOUT_FLAG,
|
||||
...SHOW_CREDENTIALS_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export async function buildSandboxCreateBody(flags: CreateFlags): Promise<JsonObject> {
|
||||
const body = await readRequestBody(flags.body);
|
||||
setDefined(body, "templateID", flags.templateId);
|
||||
setDefined(body, "timeout", flags.instanceTimeout);
|
||||
setDefined(body, "allow_internet_access", flags.allowInternetAccess);
|
||||
setDefined(body, "autoPause", flags.autoPause);
|
||||
setDefined(body, "autoResume", flags.autoResume);
|
||||
mergeObjectField(body, "metadata", parseKeyValueEntries(flags.metadata, "--metadata"));
|
||||
mergeObjectField(body, "envVars", parseKeyValueEntries(flags.env, "--env"));
|
||||
|
||||
const networkOverrides: JsonObject = {};
|
||||
setDefined(networkOverrides, "allowOut", flags.allowOut);
|
||||
setDefined(networkOverrides, "denyOut", flags.denyOut);
|
||||
setDefined(networkOverrides, "maskRequestHost", flags.maskRequestHost);
|
||||
mergeObjectField(body, "network", networkOverrides);
|
||||
|
||||
if (typeof body.templateID !== "string" || body.templateID.trim().length === 0) {
|
||||
throw new BailianError(
|
||||
"Sandbox create requires --template-id or templateID in --body.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
validateIntegerRange(body.timeout, "Sandbox timeout", 300, 604800);
|
||||
return body;
|
||||
}
|
||||
|
||||
async function buildConnectionBody(flags: {
|
||||
body?: string;
|
||||
instanceTimeout?: number;
|
||||
}): Promise<JsonObject> {
|
||||
const body = await readRequestBody(flags.body);
|
||||
setDefined(body, "timeout", flags.instanceTimeout);
|
||||
validateIntegerRange(body.timeout, "Sandbox timeout", 300, 604800);
|
||||
return body;
|
||||
}
|
||||
|
||||
function emitSandboxObject(
|
||||
sandbox: SandboxInfo,
|
||||
options: { format: "text" | "json"; quiet: boolean; showCredentials: boolean },
|
||||
): void {
|
||||
const output = options.showCredentials ? sandbox : redactConnectionCredentials(sandbox);
|
||||
if (options.quiet) {
|
||||
emitBare(displayValue(sandbox.sandboxID));
|
||||
return;
|
||||
}
|
||||
emitResult(output, options.format);
|
||||
}
|
||||
|
||||
export const sandboxCreate = defineCommand({
|
||||
description: { "en-US": "Create a Sandbox instance", "zh-CN": "创建 Sandbox 实例" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "(--template-id <id> | --body <json|@path>) [flags]",
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--template-id tpl-xxx --instance-timeout 3600",
|
||||
"--template-id tpl-xxx --base-url https://workspace.cn-beijing.maas.aliyuncs.com",
|
||||
"--body @sandbox.json --dry-run --output json",
|
||||
"--template-id tpl-xxx --show-credentials --output json",
|
||||
],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(ctx, SANDBOX_PATHS.sandboxes);
|
||||
const body = await buildSandboxCreateBody(ctx.flags);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
|
||||
return;
|
||||
}
|
||||
const response = await ctx.client.requestJson<SandboxInfo>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
emitSandboxObject(response, {
|
||||
format,
|
||||
quiet: ctx.settings.quiet,
|
||||
showCredentials: ctx.flags.showCredentials,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxList = defineCommand({
|
||||
description: { "en-US": "List Sandbox instances", "zh-CN": "列出 Sandbox 实例" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--template-id <id>] [--sandbox-id <id>] [--state <state>] [--limit <n>]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: ["", "--state running --limit 20", "--template-id tpl-xxx --output json"],
|
||||
notes: SANDBOX_NOTES,
|
||||
validate(flags) {
|
||||
if (
|
||||
flags.limit !== undefined &&
|
||||
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 50)
|
||||
) {
|
||||
return "--limit must be an integer between 1 and 50.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const url = new URL(resolveSandboxEndpoint(ctx, SANDBOX_PATHS.sandboxList));
|
||||
if (ctx.flags.templateId) url.searchParams.set("templateID", ctx.flags.templateId);
|
||||
if (ctx.flags.sandboxId) url.searchParams.set("sandboxID", ctx.flags.sandboxId);
|
||||
if (ctx.flags.state) url.searchParams.set("state", ctx.flags.state);
|
||||
if (ctx.flags.limit !== undefined) url.searchParams.set("limit", String(ctx.flags.limit));
|
||||
const response = await ctx.client.requestJson<SandboxInfo[]>({
|
||||
path: url.toString(),
|
||||
method: "GET",
|
||||
});
|
||||
if (ctx.settings.quiet) {
|
||||
for (const sandbox of response) emitBare(displayValue(sandbox.sandboxID));
|
||||
return;
|
||||
}
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === "json") {
|
||||
emitResult(response, format);
|
||||
return;
|
||||
}
|
||||
if (response.length === 0) {
|
||||
emitBare("No Sandbox instances found.");
|
||||
return;
|
||||
}
|
||||
const rows = response.map((sandbox) => [
|
||||
displayValue(sandbox.sandboxID),
|
||||
displayValue(sandbox.templateID),
|
||||
displayValue(sandbox.state),
|
||||
displayValue(sandbox.cpuCount),
|
||||
displayValue(sandbox.memoryMB),
|
||||
displayValue(sandbox.startedAt),
|
||||
displayValue(sandbox.endAt),
|
||||
]);
|
||||
for (const line of formatTable(
|
||||
["ID", "TEMPLATE", "STATE", "CPU", "MEMORY_MB", "STARTED_AT", "END_AT"],
|
||||
rows,
|
||||
)) {
|
||||
emitBare(line);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxGet = defineCommand({
|
||||
description: { "en-US": "Get Sandbox instance details", "zh-CN": "获取 Sandbox 实例详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--sandbox-id <id> [--show-credentials]",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: ["--sandbox-id sbx-xxx", "--sandbox-id sbx-xxx --show-credentials --output json"],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const endpoint = resolveSandboxEndpoint(ctx, sandboxInstancePath(ctx.flags.sandboxId));
|
||||
const response = await ctx.client.requestJson<SandboxInfo>({ path: endpoint, method: "GET" });
|
||||
emitSandboxObject(response, {
|
||||
format: detectOutputFormat(ctx.settings.output),
|
||||
quiet: ctx.settings.quiet,
|
||||
showCredentials: ctx.flags.showCredentials,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function connectionCommand(action: "connect" | "resume") {
|
||||
const actionText = action === "connect" ? "Connect to" : "Resume";
|
||||
const actionTextZh = action === "connect" ? "连接" : "恢复";
|
||||
return defineCommand({
|
||||
description: {
|
||||
"en-US": `${actionText} a Sandbox instance and return connection information`,
|
||||
"zh-CN": `${actionTextZh} Sandbox 实例并返回连接信息`,
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--sandbox-id <id> [--instance-timeout <seconds>] [--show-credentials]",
|
||||
flags: CONNECTION_ACTION_FLAGS,
|
||||
exampleArgs: [
|
||||
`--sandbox-id sbx-xxx --instance-timeout 3600`,
|
||||
`--sandbox-id sbx-xxx --show-credentials --output json`,
|
||||
],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(
|
||||
ctx,
|
||||
sandboxInstanceActionPath(ctx.flags.sandboxId, action),
|
||||
);
|
||||
const body = await buildConnectionBody(ctx.flags);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
|
||||
return;
|
||||
}
|
||||
const response = await ctx.client.requestJson<SandboxInfo>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body: Object.keys(body).length > 0 ? body : undefined,
|
||||
});
|
||||
emitSandboxObject(response, {
|
||||
format,
|
||||
quiet: ctx.settings.quiet,
|
||||
showCredentials: ctx.flags.showCredentials,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const sandboxConnect = connectionCommand("connect");
|
||||
export const sandboxResume = connectionCommand("resume");
|
||||
|
||||
export const sandboxPause = defineCommand({
|
||||
description: { "en-US": "Pause a Sandbox instance", "zh-CN": "暂停 Sandbox 实例" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--sandbox-id <id>",
|
||||
flags: ACTION_FLAGS,
|
||||
exampleArgs: ["--sandbox-id sbx-xxx", "--sandbox-id sbx-xxx --dry-run --output json"],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(
|
||||
ctx,
|
||||
sandboxInstanceActionPath(ctx.flags.sandboxId, "pause"),
|
||||
);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "POST", endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
await ctx.client.request({ path: endpoint, method: "POST" });
|
||||
if (ctx.settings.quiet) emitBare(ctx.flags.sandboxId);
|
||||
else emitResult({ sandboxID: ctx.flags.sandboxId, paused: true }, format);
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxDelete = defineCommand({
|
||||
description: { "en-US": "Release a Sandbox instance", "zh-CN": "释放 Sandbox 实例" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently releases the Sandbox instance and cannot be undone.",
|
||||
"zh-CN": "该操作会永久释放 Sandbox 实例,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--sandbox-id <id>",
|
||||
flags: ACTION_FLAGS,
|
||||
exampleArgs: ["--sandbox-id sbx-xxx --dry-run", "--sandbox-id sbx-xxx --yes"],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(ctx, sandboxInstancePath(ctx.flags.sandboxId));
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "DELETE", endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
await ctx.client.request({ path: endpoint, method: "DELETE" });
|
||||
if (ctx.settings.quiet) emitBare(ctx.flags.sandboxId);
|
||||
else emitResult({ sandboxID: ctx.flags.sandboxId, deleted: true }, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
sandboxApiPath,
|
||||
sandboxBaseUrl,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type LocalizedText,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
export const WORKSPACE_FLAG = {
|
||||
workspaceId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Workspace ID for the default Sandbox endpoint; optional with a configured base URL",
|
||||
"zh-CN": "默认 Sandbox Endpoint 的 Workspace ID;已配置 Base URL 时可省略",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const BODY_FLAG = {
|
||||
body: {
|
||||
type: "string",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "JSON request body, inline or loaded from an @file path; explicit flags override it",
|
||||
"zh-CN": "JSON 请求体,可内联或从 @文件路径读取;显式 Flag 优先",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const INSTANCE_TIMEOUT_FLAG = {
|
||||
instanceTimeout: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: {
|
||||
"en-US": "Sandbox lifetime after this operation (300-604800 seconds)",
|
||||
"zh-CN": "本次操作后的 Sandbox 存活时间(300–604800 秒)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const SHOW_CREDENTIALS_FLAG = {
|
||||
showCredentials: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Print envd and traffic access tokens instead of redacting them",
|
||||
"zh-CN": "输出 envd 与流量访问 Token 原值,不进行脱敏",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const POLL_INTERVAL_FLAG = {
|
||||
pollInterval: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: {
|
||||
"en-US": "Template build polling interval (default: 5 seconds)",
|
||||
"zh-CN": "模版构建轮询间隔(默认:5 秒)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const SANDBOX_NOTES: LocalizedText[] = [
|
||||
{
|
||||
"en-US": "Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.",
|
||||
"zh-CN": "鉴权:使用百炼 API Key 作为 Authorization Bearer Token;不会发送 E2B Key。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.",
|
||||
"zh-CN":
|
||||
"Base URL 优先级:--base-url > DASHSCOPE_BASE_URL > 登录/Profile 的 base_url。CLI 取其 origin 并追加 /api/v1/agentstudio/sandbox;未配置时使用工作空间的 cn-beijing Endpoint。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id.",
|
||||
"zh-CN":
|
||||
"未配置 Base URL 时必须提供 Workspace:--workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Sandbox is currently available in cn-beijing only and requires prior SLR authorization.",
|
||||
"zh-CN": "Sandbox 当前仅支持 cn-beijing,首次使用前需完成 SLR 授权。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.",
|
||||
"zh-CN":
|
||||
"全局 --timeout 限制 HTTP 请求和模版构建总轮询时间;--instance-timeout 映射到 Sandbox API 的实例存活时间字段。",
|
||||
},
|
||||
];
|
||||
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
|
||||
interface SandboxEndpointContext {
|
||||
flags: { workspaceId?: string };
|
||||
settings: { workspaceId?: string };
|
||||
client: Pick<Client, "url">;
|
||||
}
|
||||
|
||||
export function resolveSandboxEndpoint(ctx: SandboxEndpointContext, path: string): string {
|
||||
return ctx.client.url(sandboxApiPath(path), () => sandboxBaseUrl(resolveWorkspaceId(ctx)));
|
||||
}
|
||||
|
||||
export function resolveWorkspaceId(ctx: Omit<SandboxEndpointContext, "client">): string {
|
||||
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
|
||||
if (!workspaceId) {
|
||||
throw new BailianError(
|
||||
"Workspace ID is required when no base URL is configured. / 未配置 Base URL 时必须提供 Workspace ID。",
|
||||
ExitCode.USAGE,
|
||||
"Pass --workspace-id, set BAILIAN_WORKSPACE_ID, configure workspace_id, or set --base-url. / 请传入 --workspace-id、设置 BAILIAN_WORKSPACE_ID 或 workspace_id 配置,或通过 --base-url 指定地址。",
|
||||
);
|
||||
}
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
function asJsonObject(value: unknown, source: string): JsonObject {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new BailianError(`${source} must contain a JSON object.`, ExitCode.USAGE);
|
||||
}
|
||||
return value as JsonObject;
|
||||
}
|
||||
|
||||
export async function readRequestBody(argument?: string): Promise<JsonObject> {
|
||||
if (!argument) return {};
|
||||
const source = argument.startsWith("@") ? argument.slice(1) : undefined;
|
||||
if (source !== undefined && source.length === 0) {
|
||||
throw new BailianError("--body @path must include a file path.", ExitCode.USAGE);
|
||||
}
|
||||
const raw = source === undefined ? argument : await readFile(source, "utf8");
|
||||
try {
|
||||
return asJsonObject(JSON.parse(raw) as unknown, source ? `Body file ${source}` : "--body");
|
||||
} catch (error) {
|
||||
if (error instanceof BailianError) throw error;
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new BailianError(
|
||||
source ? `Body file ${source} contains invalid JSON.` : "--body contains invalid JSON.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function setDefined(body: JsonObject, key: string, value: unknown): void {
|
||||
if (value !== undefined) body[key] = value;
|
||||
}
|
||||
|
||||
export function parseKeyValueEntries(entries: string[] | undefined, flag: string): JsonObject {
|
||||
const values: JsonObject = {};
|
||||
for (const entry of entries ?? []) {
|
||||
const separator = entry.indexOf("=");
|
||||
if (separator <= 0) {
|
||||
throw new BailianError(`${flag} values must use KEY=VALUE format.`, ExitCode.USAGE);
|
||||
}
|
||||
const key = entry.slice(0, separator).trim();
|
||||
if (!key) {
|
||||
throw new BailianError(`${flag} values must include a non-empty key.`, ExitCode.USAGE);
|
||||
}
|
||||
values[key] = entry.slice(separator + 1);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function mergeObjectField(body: JsonObject, key: string, overrides: JsonObject): void {
|
||||
if (Object.keys(overrides).length === 0) return;
|
||||
const current = body[key];
|
||||
const base =
|
||||
current !== null && typeof current === "object" && !Array.isArray(current)
|
||||
? (current as JsonObject)
|
||||
: {};
|
||||
body[key] = { ...base, ...overrides };
|
||||
}
|
||||
|
||||
export function validateIntegerRange(
|
||||
value: unknown,
|
||||
label: string,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): void {
|
||||
if (
|
||||
value !== undefined &&
|
||||
(typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum)
|
||||
) {
|
||||
throw new BailianError(
|
||||
`${label} must be an integer between ${minimum} and ${maximum}.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePositiveInteger(value: unknown, label: string): void {
|
||||
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
||||
throw new BailianError(`${label} must be a positive integer.`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
const CONNECTION_CREDENTIAL_KEYS = new Set(["envdAccessToken", "trafficAccessToken"]);
|
||||
const SENSITIVE_KEY = /(?:token|secret|password|credential|api[_-]?key)/i;
|
||||
|
||||
function redactValue(value: unknown, redactEnvironmentMaps: boolean): unknown {
|
||||
if (Array.isArray(value)) return value.map((entry) => redactValue(entry, redactEnvironmentMaps));
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
const output: JsonObject = {};
|
||||
for (const [key, entry] of Object.entries(value as JsonObject)) {
|
||||
if (
|
||||
CONNECTION_CREDENTIAL_KEYS.has(key) ||
|
||||
SENSITIVE_KEY.test(key) ||
|
||||
(redactEnvironmentMaps && (key === "envVars" || key === "envConfig"))
|
||||
) {
|
||||
if (entry !== undefined && entry !== null) {
|
||||
output[key] =
|
||||
(key === "envVars" || key === "envConfig") &&
|
||||
typeof entry === "object" &&
|
||||
!Array.isArray(entry)
|
||||
? Object.fromEntries(
|
||||
Object.keys(entry as JsonObject).map((environmentKey) => [
|
||||
environmentKey,
|
||||
"[REDACTED]",
|
||||
]),
|
||||
)
|
||||
: "[REDACTED]";
|
||||
} else {
|
||||
output[key] = entry;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
output[key] = redactValue(entry, redactEnvironmentMaps);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function redactConnectionCredentials<T>(value: T): T {
|
||||
return redactValue(value, false) as T;
|
||||
}
|
||||
|
||||
export function redactRequestSecrets<T>(value: T): T {
|
||||
return redactValue(value, true) as T;
|
||||
}
|
||||
|
||||
export function displayValue(value: unknown): string {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
if (Array.isArray(value)) return value.join(",");
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return `${value}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "-";
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
import {
|
||||
BailianError,
|
||||
detectOutputFormat,
|
||||
defineCommand,
|
||||
ExitCode,
|
||||
sandboxApiPath,
|
||||
SANDBOX_PATHS,
|
||||
sandboxTemplateBuildStatusPath,
|
||||
sandboxTemplatePath,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { createSpinner, emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import { resolveSandboxImage, SANDBOX_IMAGE_CHOICES, SANDBOX_IMAGE_NOTES } from "./images.ts";
|
||||
import {
|
||||
BODY_FLAG,
|
||||
displayValue,
|
||||
mergeObjectField,
|
||||
parseKeyValueEntries,
|
||||
POLL_INTERVAL_FLAG,
|
||||
readRequestBody,
|
||||
redactRequestSecrets,
|
||||
resolveSandboxEndpoint,
|
||||
SANDBOX_NOTES,
|
||||
setDefined,
|
||||
validateIntegerRange,
|
||||
validatePositiveInteger,
|
||||
WORKSPACE_FLAG,
|
||||
type JsonObject,
|
||||
} from "./shared.ts";
|
||||
|
||||
const TEMPLATE_ASYNC_FLAG = {
|
||||
async: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Return the submitted templateID/buildID immediately without polling",
|
||||
"zh-CN": "提交后立即返回 templateID/buildID,不轮询构建状态",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
interface TemplateInfo extends JsonObject {
|
||||
templateID?: string;
|
||||
templateName?: string;
|
||||
cpuCount?: number;
|
||||
memoryMB?: number;
|
||||
spawnCount?: number;
|
||||
buildCount?: number;
|
||||
updatedAt?: string;
|
||||
buildID?: string;
|
||||
buildStatus?: string;
|
||||
}
|
||||
|
||||
interface TemplateBuildStatus extends JsonObject {
|
||||
templateID?: string;
|
||||
buildID?: string;
|
||||
status?: "building" | "ready" | "error";
|
||||
reason?: { code?: number | string; message?: string };
|
||||
}
|
||||
|
||||
const TEMPLATE_ID_FLAG = {
|
||||
templateId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Sandbox template ID", "zh-CN": "Sandbox 模版 ID" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const TEMPLATE_MUTATION_FIELDS = {
|
||||
image: {
|
||||
type: "string",
|
||||
valueHint: "<preset>",
|
||||
choices: SANDBOX_IMAGE_CHOICES,
|
||||
description: {
|
||||
"en-US": "Built-in image ID or Chinese name; fills fromImage and imageName",
|
||||
"zh-CN": "内置镜像 ID 或中文名;自动填写 fromImage 和 imageName",
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Template name", "zh-CN": "模版名称" },
|
||||
},
|
||||
cpuCount: {
|
||||
type: "number",
|
||||
valueHint: "<cores>",
|
||||
description: { "en-US": "vCPU count", "zh-CN": "vCPU 核数" },
|
||||
},
|
||||
memoryMb: {
|
||||
type: "number",
|
||||
valueHint: "<mb>",
|
||||
description: { "en-US": "Memory in MB", "zh-CN": "内存大小(MB)" },
|
||||
},
|
||||
fromImage: {
|
||||
type: "string",
|
||||
valueHint: "<image>",
|
||||
description: { "en-US": "Base image identifier", "zh-CN": "基础镜像标识" },
|
||||
},
|
||||
imageName: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Base image display name", "zh-CN": "基础镜像展示名称" },
|
||||
},
|
||||
env: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Template environment variable; repeat for multiple values",
|
||||
"zh-CN": "模版环境变量;可重复传入",
|
||||
},
|
||||
},
|
||||
allowOut: {
|
||||
type: "array",
|
||||
valueHint: "<address>",
|
||||
description: {
|
||||
"en-US": "Outbound allow-list entry; repeat for multiple values",
|
||||
"zh-CN": "出站白名单条目;可重复传入",
|
||||
},
|
||||
},
|
||||
denyOut: {
|
||||
type: "array",
|
||||
valueHint: "<address>",
|
||||
description: {
|
||||
"en-US": "Outbound deny-list entry; repeat for multiple values",
|
||||
"zh-CN": "出站黑名单条目;可重复传入",
|
||||
},
|
||||
},
|
||||
autoPauseTime: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: {
|
||||
"en-US": "Automatically pause after 300-604800 seconds",
|
||||
"zh-CN": "在 300–604800 秒后自动暂停",
|
||||
},
|
||||
},
|
||||
maxRunningTime: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: {
|
||||
"en-US": "Maximum running lifetime in seconds (300-604800)",
|
||||
"zh-CN": "最大运行时间(秒,300–604800)",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Template description", "zh-CN": "模版描述" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...BODY_FLAG,
|
||||
...TEMPLATE_MUTATION_FIELDS,
|
||||
tag: {
|
||||
type: "array",
|
||||
valueHint: "<tag>",
|
||||
description: { "en-US": "E2B tag; repeat for multiple values", "zh-CN": "E2B Tag;可重复传入" },
|
||||
},
|
||||
alias: {
|
||||
type: "string",
|
||||
valueHint: "<alias>",
|
||||
description: { "en-US": "E2B template alias", "zh-CN": "E2B 模版别名" },
|
||||
},
|
||||
...TEMPLATE_ASYNC_FLAG,
|
||||
...POLL_INTERVAL_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const UPDATE_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...TEMPLATE_ID_FLAG,
|
||||
...BODY_FLAG,
|
||||
...TEMPLATE_MUTATION_FIELDS,
|
||||
...TEMPLATE_ASYNC_FLAG,
|
||||
...POLL_INTERVAL_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
|
||||
type UpdateFlags = ParsedFlags<typeof UPDATE_FLAGS>;
|
||||
|
||||
const LIST_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Maximum results (1-100)", "zh-CN": "最大返回数量(1–100)" },
|
||||
},
|
||||
cursor: {
|
||||
type: "string",
|
||||
valueHint: "<cursor>",
|
||||
description: { "en-US": "Server-side pagination cursor", "zh-CN": "服务端分页 Cursor" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const GET_FLAGS = { ...WORKSPACE_FLAG, ...TEMPLATE_ID_FLAG } satisfies FlagsDef;
|
||||
const BUILD_STATUS_FLAGS = {
|
||||
...WORKSPACE_FLAG,
|
||||
...TEMPLATE_ID_FLAG,
|
||||
buildId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Template build ID", "zh-CN": "模版构建 ID" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function applyTemplateMutationFlags(body: JsonObject, flags: CreateFlags | UpdateFlags): void {
|
||||
if (flags.image !== undefined) {
|
||||
const image = resolveSandboxImage(flags.image);
|
||||
body.fromImage = image.imageUrl;
|
||||
body.imageName = image.imageName;
|
||||
}
|
||||
setDefined(body, "name", flags.name);
|
||||
setDefined(body, "cpuCount", flags.cpuCount);
|
||||
setDefined(body, "memoryMB", flags.memoryMb);
|
||||
setDefined(body, "fromImage", flags.fromImage);
|
||||
setDefined(body, "imageName", flags.imageName);
|
||||
setDefined(body, "autoPauseTime", flags.autoPauseTime);
|
||||
setDefined(body, "maxRunningTimeout", flags.maxRunningTime);
|
||||
setDefined(body, "description", flags.description);
|
||||
mergeObjectField(body, "envConfig", parseKeyValueEntries(flags.env, "--env"));
|
||||
|
||||
const networkOverrides: JsonObject = {};
|
||||
setDefined(networkOverrides, "allowOut", flags.allowOut);
|
||||
setDefined(networkOverrides, "denyOut", flags.denyOut);
|
||||
mergeObjectField(body, "networkConfig", networkOverrides);
|
||||
}
|
||||
|
||||
function validateTemplateMutationBody(body: JsonObject, create: boolean): void {
|
||||
if (create && (typeof body.name !== "string" || body.name.trim().length === 0)) {
|
||||
throw new BailianError("Template create requires --name or name in --body.", ExitCode.USAGE);
|
||||
}
|
||||
|
||||
const hasCpu = body.cpuCount !== undefined;
|
||||
const hasMemory = body.memoryMB !== undefined;
|
||||
if (create && (!hasCpu || !hasMemory)) {
|
||||
throw new BailianError(
|
||||
"Template create requires both --cpu-count and --memory-mb, or their --body fields.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (hasCpu !== hasMemory) {
|
||||
throw new BailianError("cpuCount and memoryMB must be provided together.", ExitCode.USAGE);
|
||||
}
|
||||
if (hasCpu) validatePositiveInteger(body.cpuCount, "cpuCount");
|
||||
if (hasMemory) validatePositiveInteger(body.memoryMB, "memoryMB");
|
||||
validateIntegerRange(body.autoPauseTime, "autoPauseTime", 300, 604800);
|
||||
validateIntegerRange(body.maxRunningTimeout, "maxRunningTimeout", 300, 604800);
|
||||
if (!create && Object.keys(body).length === 0) {
|
||||
throw new BailianError("Template update requires at least one field.", ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildTemplateCreateBody(flags: CreateFlags): Promise<JsonObject> {
|
||||
const body = await readRequestBody(flags.body);
|
||||
applyTemplateMutationFlags(body, flags);
|
||||
setDefined(body, "tags", flags.tag);
|
||||
setDefined(body, "alias", flags.alias);
|
||||
validateTemplateMutationBody(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function buildTemplateUpdateBody(flags: UpdateFlags): Promise<JsonObject> {
|
||||
const body = await readRequestBody(flags.body);
|
||||
applyTemplateMutationFlags(body, flags);
|
||||
validateTemplateMutationBody(body, false);
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function waitForTemplateBuild(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
endpoint: string,
|
||||
intervalSec: number,
|
||||
): Promise<TemplateBuildStatus> {
|
||||
const deadline = Date.now() + settings.timeout * 1000;
|
||||
const spinner = createSpinner("Waiting for template build...");
|
||||
if (!settings.quiet) spinner.start();
|
||||
try {
|
||||
while (true) {
|
||||
const remainingMs = deadline - Date.now();
|
||||
if (remainingMs <= 0) break;
|
||||
const status = await client.requestJson<TemplateBuildStatus>({
|
||||
path: endpoint,
|
||||
method: "GET",
|
||||
timeout: remainingMs / 1000,
|
||||
});
|
||||
if (!settings.quiet) spinner.update(`Build status: ${status.status ?? "unknown"}`);
|
||||
if (status.status === "ready") {
|
||||
spinner.stop("Template build ready.");
|
||||
return status;
|
||||
}
|
||||
if (status.status === "error") {
|
||||
spinner.stop("Template build failed.");
|
||||
const reason =
|
||||
status.reason?.message ??
|
||||
(status.reason?.code !== undefined ? `${status.reason.code}` : "Template build failed.");
|
||||
throw new BailianError(reason, ExitCode.GENERAL);
|
||||
}
|
||||
const sleepMs = Math.min(intervalSec * 1000, deadline - Date.now());
|
||||
if (sleepMs > 0) {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, sleepMs));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
spinner.stop();
|
||||
}
|
||||
throw new BailianError(
|
||||
"Template build polling timed out.",
|
||||
ExitCode.TIMEOUT,
|
||||
"Increase --timeout or query the build status command with the returned IDs.",
|
||||
);
|
||||
}
|
||||
|
||||
function validatePollInterval(flags: { pollInterval?: number }): string | undefined {
|
||||
if (
|
||||
flags.pollInterval !== undefined &&
|
||||
(!Number.isInteger(flags.pollInterval) || flags.pollInterval <= 0)
|
||||
) {
|
||||
return "--poll-interval must be a positive integer.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function emitTemplateMutationResult(options: {
|
||||
response: TemplateInfo;
|
||||
endpoint: string;
|
||||
client: Client;
|
||||
settings: Settings;
|
||||
async: boolean;
|
||||
pollInterval?: number;
|
||||
}): Promise<void> {
|
||||
const format = detectOutputFormat(options.settings.output);
|
||||
if (!options.response.templateID || !options.response.buildID) {
|
||||
throw new BailianError(
|
||||
"Template mutation response is missing templateID or buildID.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
if (options.async) {
|
||||
if (options.settings.quiet) {
|
||||
emitBare(`${options.response.templateID}\t${options.response.buildID}`);
|
||||
} else {
|
||||
emitResult(options.response, format);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Poll the same origin that accepted the build, including custom gateways.
|
||||
const buildEndpoint = new URL(
|
||||
sandboxApiPath(
|
||||
sandboxTemplateBuildStatusPath(options.response.templateID, options.response.buildID),
|
||||
),
|
||||
options.endpoint,
|
||||
).toString();
|
||||
let build: TemplateBuildStatus;
|
||||
try {
|
||||
build = await waitForTemplateBuild(
|
||||
options.client,
|
||||
options.settings,
|
||||
buildEndpoint,
|
||||
options.pollInterval ?? 5,
|
||||
);
|
||||
} catch (error) {
|
||||
const hint =
|
||||
`Submitted build / 已提交的构建: templateID=${options.response.templateID}, buildID=${options.response.buildID}.\n` +
|
||||
"Check this build with sandbox template build-status before submitting another build. / 请先通过 sandbox template build-status 查询本次构建,再决定是否重新提交。";
|
||||
if (error instanceof BailianError) {
|
||||
throw new BailianError(
|
||||
error.message,
|
||||
error.exitCode,
|
||||
[error.hint, hint].filter(Boolean).join("\n"),
|
||||
{
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
cause: error.cause,
|
||||
},
|
||||
);
|
||||
}
|
||||
// Leave transport errors intact so runtime retains timeout/network classification.
|
||||
const recovery = {
|
||||
templateID: options.response.templateID,
|
||||
buildID: options.response.buildID,
|
||||
hint,
|
||||
};
|
||||
process.stderr.write(
|
||||
format === "json" ? `${JSON.stringify(recovery, null, 2)}\n\n` : `${hint}\n`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (options.settings.quiet) emitBare(displayValue(options.response.templateID));
|
||||
else emitResult({ template: options.response, build }, format);
|
||||
}
|
||||
|
||||
export const sandboxTemplateCreate = defineCommand({
|
||||
description: { "en-US": "Create a Sandbox template", "zh-CN": "创建 Sandbox 模版" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "(--name <name> --cpu-count <cores> --memory-mb <mb> | --body <json|@path>) [flags]",
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--name browser --image browser --cpu-count 1 --memory-mb 2048",
|
||||
"--name python --cpu-count 1 --memory-mb 2048",
|
||||
"--body @template.json --async --output json",
|
||||
"--name browser --cpu-count 4 --memory-mb 8192 --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
...SANDBOX_NOTES,
|
||||
...SANDBOX_IMAGE_NOTES,
|
||||
{
|
||||
"en-US":
|
||||
"--image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.",
|
||||
"zh-CN":
|
||||
"--image 覆盖 body 中的 fromImage/imageName;显式 --from-image/--image-name 再覆盖对应预设字段。不传 --image 时保持原有镜像行为。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"For local file mounts, use sandbox file upload first; put the available file's id into --body mntConfig[].originFileId with mountPath and optional originFileName. Upload and template must use the same workspace. Other complete nested structures can also be supplied through --body.",
|
||||
"zh-CN":
|
||||
"挂载本地文件前先调用 sandbox file upload;将可用文件的 id 填入 --body 的 mntConfig[].originFileId,并传入 mountPath 和可选的 originFileName。上传和模版须位于同一工作空间。其他完整嵌套结构也可通过 --body 提供。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"By default the command waits for build status ready; --async returns the submitted build immediately.",
|
||||
"zh-CN": "默认等待构建状态变为 ready;--async 会立即返回已提交的构建信息。",
|
||||
},
|
||||
],
|
||||
validate: validatePollInterval,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(ctx, SANDBOX_PATHS.templateCreate);
|
||||
const body = await buildTemplateCreateBody(ctx.flags);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
|
||||
return;
|
||||
}
|
||||
const response = await ctx.client.requestJson<TemplateInfo>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
await emitTemplateMutationResult({
|
||||
response,
|
||||
endpoint,
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
async: ctx.flags.async,
|
||||
pollInterval: ctx.flags.pollInterval,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxTemplateList = defineCommand({
|
||||
description: { "en-US": "List Sandbox templates", "zh-CN": "列出 Sandbox 模版" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--cursor <cursor>]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: ["", "--limit 100 --output json"],
|
||||
notes: [
|
||||
...SANDBOX_NOTES,
|
||||
{
|
||||
"en-US":
|
||||
"The API response does not expose a next cursor, so automatic --all pagination is unavailable.",
|
||||
"zh-CN": "API 响应未提供下一页 Cursor,因此不支持自动 --all 翻页。",
|
||||
},
|
||||
],
|
||||
validate(flags) {
|
||||
if (
|
||||
flags.limit !== undefined &&
|
||||
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100)
|
||||
) {
|
||||
return "--limit must be an integer between 1 and 100.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const url = new URL(resolveSandboxEndpoint(ctx, SANDBOX_PATHS.templateList));
|
||||
if (ctx.flags.limit !== undefined) url.searchParams.set("limit", String(ctx.flags.limit));
|
||||
if (ctx.flags.cursor) url.searchParams.set("cursor", ctx.flags.cursor);
|
||||
const response = await ctx.client.requestJson<TemplateInfo[]>({
|
||||
path: url.toString(),
|
||||
method: "GET",
|
||||
});
|
||||
if (ctx.settings.quiet) {
|
||||
for (const template of response) emitBare(displayValue(template.templateID));
|
||||
return;
|
||||
}
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === "json") {
|
||||
emitResult(response, format);
|
||||
return;
|
||||
}
|
||||
if (response.length === 0) {
|
||||
emitBare("No Sandbox templates found.");
|
||||
return;
|
||||
}
|
||||
const rows = response.map((template) => [
|
||||
displayValue(template.templateID),
|
||||
displayValue(template.templateName),
|
||||
displayValue(template.cpuCount),
|
||||
displayValue(template.memoryMB),
|
||||
displayValue(template.spawnCount),
|
||||
displayValue(template.buildCount),
|
||||
displayValue(template.updatedAt),
|
||||
]);
|
||||
for (const line of formatTable(
|
||||
["ID", "NAME", "CPU", "MEMORY_MB", "INSTANCES", "BUILDS", "UPDATED_AT"],
|
||||
rows,
|
||||
)) {
|
||||
emitBare(line);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxTemplateGet = defineCommand({
|
||||
description: { "en-US": "Get Sandbox template details", "zh-CN": "获取 Sandbox 模版详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--template-id <id>",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: ["--template-id tpl-xxx", "--template-id tpl-xxx --output json"],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const endpoint = resolveSandboxEndpoint(ctx, sandboxTemplatePath(ctx.flags.templateId));
|
||||
const response = await ctx.client.requestJson<TemplateInfo>({ path: endpoint, method: "GET" });
|
||||
if (ctx.settings.quiet) emitBare(displayValue(response.templateID));
|
||||
else emitResult(response, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxTemplateUpdate = defineCommand({
|
||||
description: { "en-US": "Update a Sandbox template", "zh-CN": "更新 Sandbox 模版" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--template-id <id> (--body <json|@path> | [fields])",
|
||||
flags: UPDATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--template-id tpl-xxx --image all-in-one",
|
||||
"--template-id tpl-xxx --cpu-count 4 --memory-mb 8192",
|
||||
"--template-id tpl-xxx --body @template-update.json --async --output json",
|
||||
"--template-id tpl-xxx --description updated --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
...SANDBOX_NOTES,
|
||||
...SANDBOX_IMAGE_NOTES,
|
||||
{
|
||||
"en-US":
|
||||
"--image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.",
|
||||
"zh-CN":
|
||||
"--image 覆盖 body 中的 fromImage/imageName;显式 --from-image/--image-name 再覆盖对应预设字段。不传 --image 时保持原有镜像行为。",
|
||||
},
|
||||
{
|
||||
"en-US": "Supplying envConfig or --env replaces the template's complete environment map.",
|
||||
"zh-CN": "传入 envConfig 或 --env 会整体替换模版的环境变量 Map。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Use sandbox file upload for local mount files. In --body mntConfig[], set originFileId to the available file's id and supply mountPath; upload and template must use the same workspace.",
|
||||
"zh-CN":
|
||||
"本地挂载文件先通过 sandbox file upload 上传。在 --body 的 mntConfig[] 中,将 originFileId 设为可用文件的 id 并传入 mountPath;上传和模版须位于同一工作空间。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"By default the command waits for build status ready; --async returns the submitted build immediately.",
|
||||
"zh-CN": "默认等待构建状态变为 ready;--async 会立即返回已提交的构建信息。",
|
||||
},
|
||||
],
|
||||
validate: validatePollInterval,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(ctx, sandboxTemplatePath(ctx.flags.templateId));
|
||||
const body = await buildTemplateUpdateBody(ctx.flags);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "PUT", endpoint, request: redactRequestSecrets(body) }, format);
|
||||
return;
|
||||
}
|
||||
const response = await ctx.client.requestJson<TemplateInfo>({
|
||||
path: endpoint,
|
||||
method: "PUT",
|
||||
body,
|
||||
});
|
||||
await emitTemplateMutationResult({
|
||||
response,
|
||||
endpoint,
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
async: ctx.flags.async,
|
||||
pollInterval: ctx.flags.pollInterval,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxTemplateBuildStatus = defineCommand({
|
||||
description: {
|
||||
"en-US": "Get Sandbox template build status",
|
||||
"zh-CN": "获取 Sandbox 模版构建状态",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--template-id <id> --build-id <id>",
|
||||
flags: BUILD_STATUS_FLAGS,
|
||||
exampleArgs: [
|
||||
"--template-id tpl-xxx --build-id build-xxx",
|
||||
"--template-id tpl-xxx --build-id build-xxx --output json",
|
||||
],
|
||||
notes: SANDBOX_NOTES,
|
||||
async run(ctx) {
|
||||
const endpoint = resolveSandboxEndpoint(
|
||||
ctx,
|
||||
sandboxTemplateBuildStatusPath(ctx.flags.templateId, ctx.flags.buildId),
|
||||
);
|
||||
const response = await ctx.client.requestJson<TemplateBuildStatus>({
|
||||
path: endpoint,
|
||||
method: "GET",
|
||||
});
|
||||
if (ctx.settings.quiet) emitBare(displayValue(response.status));
|
||||
else emitResult(response, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const sandboxTemplateDelete = defineCommand({
|
||||
description: { "en-US": "Delete a Sandbox template", "zh-CN": "删除 Sandbox 模版" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the Sandbox template and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除 Sandbox 模版,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--template-id <id>",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: ["--template-id tpl-xxx --dry-run", "--template-id tpl-xxx --yes"],
|
||||
notes: [
|
||||
...SANDBOX_NOTES,
|
||||
{
|
||||
"en-US":
|
||||
"The server rejects deletion while running or paused instances still use the template.",
|
||||
"zh-CN": "仍有运行中或已暂停实例使用该模版时,服务端会拒绝删除。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const endpoint = resolveSandboxEndpoint(ctx, sandboxTemplatePath(ctx.flags.templateId));
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ method: "DELETE", endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
await ctx.client.request({ path: endpoint, method: "DELETE" });
|
||||
if (ctx.settings.quiet) emitBare(ctx.flags.templateId);
|
||||
else emitResult({ templateID: ctx.flags.templateId, deleted: true }, format);
|
||||
},
|
||||
});
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
speechRecognizePath,
|
||||
resolveAsrApi,
|
||||
buildAsrFlashRequest,
|
||||
buildAsrContextMessages,
|
||||
buildAsyncAsrLanguageFields,
|
||||
collectAsrTranscriptionItems,
|
||||
extractAsrFlashText,
|
||||
parseInstantVocabulary,
|
||||
type AsrApiRoute,
|
||||
type AsrFlashFamily,
|
||||
type OutputFormat,
|
||||
@@ -73,8 +75,30 @@ const RECOGNIZE_FLAGS = {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Hot-word vocabulary ID for improved accuracy",
|
||||
"zh-CN": "用于提升识别准确率的热词表 ID",
|
||||
"en-US":
|
||||
"Pre-built hot-word vocabulary ID (create it via `speech vocabulary create`). Its target_model must exactly match --model, otherwise it is silently ignored. Wider model support than --vocabulary, including Fun-ASR and Paraformer",
|
||||
"zh-CN":
|
||||
"预编译热词列表 ID(可用 `speech vocabulary create` 创建)。其 target_model 必须与 --model 完全一致,否则静默失效且无报错。支持模型比 --vocabulary 更广,含 Fun-ASR 与 Paraformer 系列",
|
||||
},
|
||||
},
|
||||
vocabulary: {
|
||||
type: "string",
|
||||
valueHint: "<json>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Instant hot words as JSON object of word→weight, e.g. '{\"Fendouzhe\":4}'. Weight 1-5 (4 recommended; higher values can hurt other words), 50 for super hot word. No pre-built vocabulary needed. Takes effect only on Qwen-Audio-3.0-ASR-Flash models",
|
||||
"zh-CN":
|
||||
"即时热词,JSON 对象「热词→权重」,例如 '{\"奋斗者\":4}'。权重 1-5(推荐 4,过高会拖累其他词),50 表示超级热词。无需预先创建热词表。仅 Qwen-Audio-3.0-ASR-Flash 系列模型生效",
|
||||
},
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Context enhancement word list to improve accuracy on proper nouns; must contain the target words themselves (a topic description alone has little effect); max 400 chars. Takes effect only on Qwen-Audio-3.0-ASR-Flash and Fun-ASR-Flash models",
|
||||
"zh-CN":
|
||||
"上下文增强词表,提升专有名词准确率;须包含待识别的原词本身(只写主题描述效果有限),最长 400 字符。仅 Qwen-Audio-3.0-ASR-Flash 系列与 Fun-ASR-Flash 模型生效",
|
||||
},
|
||||
},
|
||||
channelId: {
|
||||
@@ -110,9 +134,11 @@ function assertSyncFlashFlagsAllowed(
|
||||
const unsupported: string[] = [];
|
||||
if (flags.diarization === true) unsupported.push("--diarization");
|
||||
if (flags.speakerCount !== undefined) unsupported.push("--speaker-count");
|
||||
// qwen3 sync Flash does not use vocabulary_id; input-audio Flash (fun-asr-flash* / qwen-audio-*-asr-flash) does
|
||||
if (flashFamily === "qwen3" && flags.vocabularyId !== undefined) {
|
||||
unsupported.push("--vocabulary-id");
|
||||
// qwen3 sync Flash has no place for vocabulary_id / vocabulary / context in its body shape
|
||||
if (flashFamily === "qwen3") {
|
||||
if (flags.vocabularyId !== undefined) unsupported.push("--vocabulary-id");
|
||||
if (flags.vocabulary !== undefined) unsupported.push("--vocabulary");
|
||||
if (flags.context !== undefined) unsupported.push("--context");
|
||||
}
|
||||
if (flags.channelId !== undefined) unsupported.push("--channel-id");
|
||||
if (flags.async === true) unsupported.push("--async");
|
||||
@@ -121,12 +147,34 @@ function assertSyncFlashFlagsAllowed(
|
||||
if (unsupported.length > 0) {
|
||||
throw new BailianError(
|
||||
`Model "${model}" uses sync Flash ASR and does not support: ${unsupported.join(", ")}.\n` +
|
||||
`Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.`,
|
||||
syncFlashUnsupportedHint(unsupported),
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Pick a hint that matches the rejected flags (vocab/context vs diarization/async/…). */
|
||||
function syncFlashUnsupportedHint(unsupported: string[]): string {
|
||||
const vocabularyRelated = new Set(["--vocabulary", "--vocabulary-id", "--context"]);
|
||||
const hasVocabularyRelated = unsupported.some((flag) => vocabularyRelated.has(flag));
|
||||
const hasOtherFlags = unsupported.some((flag) => !vocabularyRelated.has(flag));
|
||||
|
||||
if (hasVocabularyRelated && !hasOtherFlags) {
|
||||
return (
|
||||
"Hint: Use qwen-audio-3.0-asr-flash (or an async filetrans model such as " +
|
||||
"qwen-audio-3.0-asr-flash-filetrans) for vocabulary/context flags."
|
||||
);
|
||||
}
|
||||
if (hasVocabularyRelated && hasOtherFlags) {
|
||||
return (
|
||||
"Hint: For vocabulary/context flags use qwen-audio-3.0-asr-flash or " +
|
||||
"qwen-audio-3.0-asr-flash-filetrans; for the other flags use an async filetrans model " +
|
||||
"(e.g. fun-asr)."
|
||||
);
|
||||
}
|
||||
return "Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.";
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash)",
|
||||
@@ -141,6 +189,8 @@ export default defineCommand({
|
||||
"--url https://example.com/meeting.wav --diarization --speaker-count 3",
|
||||
"--url https://example.com/audio.mp3 --language zh",
|
||||
"--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123",
|
||||
'--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash-filetrans --vocabulary \'{"奋斗者":4,"鲸落":4}\'',
|
||||
'--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash-filetrans --context "奋斗者号 鲸落 深海勇士"',
|
||||
"--url https://example.com/audio.mp3 --out result.json",
|
||||
"--url https://example.com/audio.mp3 --async --quiet",
|
||||
"--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en",
|
||||
@@ -198,6 +248,9 @@ export default defineCommand({
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const vocabulary =
|
||||
flags.vocabulary !== undefined ? parseInstantVocabulary(flags.vocabulary) : undefined;
|
||||
|
||||
// Auto-upload local files in parallel
|
||||
const resolvedUrls = await Promise.all(rawUrls.map((url) => ctx.client.uploadFile(url, model)));
|
||||
|
||||
@@ -210,6 +263,7 @@ export default defineCommand({
|
||||
model,
|
||||
route,
|
||||
resolvedUrls[0]!,
|
||||
vocabulary,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -223,16 +277,19 @@ export default defineCommand({
|
||||
|
||||
const body: DashScopeASRRequest = {
|
||||
model,
|
||||
input:
|
||||
route.asyncInputStyle === "file_url"
|
||||
input: {
|
||||
...(route.asyncInputStyle === "file_url"
|
||||
? { file_url: resolvedUrls[0]! }
|
||||
: { file_urls: resolvedUrls },
|
||||
: { file_urls: resolvedUrls }),
|
||||
...(flags.context !== undefined ? { context: buildAsrContextMessages(flags.context) } : {}),
|
||||
},
|
||||
parameters: {
|
||||
channel_id: channelId !== undefined ? [channelId] : [0],
|
||||
...languageFields,
|
||||
diarization_enabled: diarization ? true : undefined,
|
||||
speaker_count: speakerCount,
|
||||
vocabulary_id: vocabularyId,
|
||||
vocabulary,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -260,6 +317,7 @@ async function handleSyncFlashMode(
|
||||
model: string,
|
||||
route: AsrApiRoute,
|
||||
audioUrl: string,
|
||||
vocabulary: Record<string, number> | undefined,
|
||||
): Promise<void> {
|
||||
const flashFamily = route.flashFamily as AsrFlashFamily;
|
||||
const body = buildAsrFlashRequest({
|
||||
@@ -267,6 +325,8 @@ async function handleSyncFlashMode(
|
||||
audioUrl,
|
||||
language: flags.language,
|
||||
vocabularyId: flags.vocabularyId,
|
||||
vocabulary,
|
||||
context: flags.context,
|
||||
flashFamily,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
speechVocabularyPath,
|
||||
buildVocabularyRequest,
|
||||
createVocabulary,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import {
|
||||
VOCABULARY_BODY_FLAGS,
|
||||
VOCABULARY_LIMIT_NOTES,
|
||||
validateVocabularySource,
|
||||
readVocabularyEntries,
|
||||
} from "./shared.ts";
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: {
|
||||
"en-US":
|
||||
"ASR model this vocabulary is built for (required). Must exactly match the --model passed to `speech recognize` later, otherwise the vocabulary is silently ignored",
|
||||
"zh-CN":
|
||||
"该热词表服务的 ASR 模型(必填)。必须与后续 `speech recognize` 的 --model 完全一致,否则热词表静默失效",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
prefix: {
|
||||
type: "string",
|
||||
valueHint: "<prefix>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Custom vocabulary prefix (required). Digits and lowercase letters only, max 10 chars",
|
||||
"zh-CN": "热词表自定义前缀(必填)。仅允许数字和小写字母,最长 10 个字符",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
...VOCABULARY_BODY_FLAGS,
|
||||
} satisfies FlagsDef;
|
||||
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Create a precompiled hot-word vocabulary for ASR",
|
||||
"zh-CN": "创建用于语音识别的预编译热词表",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <model> --prefix <prefix> (--words <json> | --words-file <path>) [flags]",
|
||||
flags: CREATE_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"The --model must exactly match the --model used later with `speech recognize --vocabulary-id`; a mismatch causes silent failure with no error.",
|
||||
"zh-CN":
|
||||
"--model 必须与后续 `speech recognize --vocabulary-id` 使用的 --model 完全一致;不一致时热词表会静默失效且无报错。",
|
||||
},
|
||||
...VOCABULARY_LIMIT_NOTES,
|
||||
],
|
||||
exampleArgs: [
|
||||
{
|
||||
"en-US": '--model fun-asr --prefix demo --words \'{"Fendouzhe":4,"Jingluo":4}\'',
|
||||
"zh-CN": '--model fun-asr --prefix demo --words \'{"奋斗者":4,"鲸落":4}\'',
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
'--model paraformer-v2 --prefix demo --words \'[{"text":"Fendouzhe","weight":4,"lang":"zh"}]\'',
|
||||
"zh-CN":
|
||||
'--model paraformer-v2 --prefix demo --words \'[{"text":"奋斗者","weight":4,"lang":"zh"}]\'',
|
||||
},
|
||||
{
|
||||
"en-US": "--model fun-asr --prefix demo --words '{\"Fendouzhe\":4}' --lang zh",
|
||||
"zh-CN": "--model fun-asr --prefix demo --words '{\"奋斗者\":4}' --lang zh",
|
||||
},
|
||||
"--model fun-asr --prefix demo --words-file ./hotwords.json",
|
||||
{
|
||||
"en-US": "--model fun-asr --prefix demo --words '{\"Fendouzhe\":4}' --quiet",
|
||||
"zh-CN": "--model fun-asr --prefix demo --words '{\"奋斗者\":4}' --quiet",
|
||||
},
|
||||
],
|
||||
validate: (flags: CreateFlags) => validateVocabularySource(flags),
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const vocabulary = readVocabularyEntries(flags);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const request = buildVocabularyRequest("create_vocabulary", {
|
||||
target_model: flags.model,
|
||||
prefix: flags.prefix,
|
||||
vocabulary,
|
||||
});
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: ctx.client.url(speechVocabularyPath()),
|
||||
request,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await createVocabulary(ctx.client, {
|
||||
targetModel: flags.model,
|
||||
prefix: flags.prefix,
|
||||
vocabulary,
|
||||
});
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(response.output?.vocabulary_id ?? "");
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
speechVocabularyPath,
|
||||
buildVocabularyRequest,
|
||||
deleteVocabulary,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { VOCABULARY_ID_FLAG } from "./shared.ts";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
...VOCABULARY_ID_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
type DeleteFlags = ParsedFlags<typeof DELETE_FLAGS>;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete a precompiled hot-word vocabulary",
|
||||
"zh-CN": "删除预编译热词表",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified hot-word vocabulary and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的热词表,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--id vocab-demo-xxx --dry-run", "--id vocab-demo-xxx --yes"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const vocabularyId = (flags as DeleteFlags).id;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: ctx.client.url(speechVocabularyPath()),
|
||||
request: buildVocabularyRequest("delete_vocabulary", {
|
||||
vocabulary_id: vocabularyId,
|
||||
}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteVocabulary(ctx.client, vocabularyId);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(vocabularyId);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
speechVocabularyPath,
|
||||
buildVocabularyRequest,
|
||||
queryVocabulary,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { VOCABULARY_ID_FLAG } from "./shared.ts";
|
||||
|
||||
const GET_FLAGS = {
|
||||
...VOCABULARY_ID_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
type GetFlags = ParsedFlags<typeof GET_FLAGS>;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get details of a precompiled hot-word vocabulary",
|
||||
"zh-CN": "查看预编译热词表详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--id <id>",
|
||||
flags: GET_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Use this command to confirm target_model before calling `speech recognize --vocabulary-id`; a model mismatch causes silent failure.",
|
||||
"zh-CN":
|
||||
"调用 `speech recognize --vocabulary-id` 前请用本命令确认 target_model;模型不一致会导致静默失效。",
|
||||
},
|
||||
],
|
||||
exampleArgs: ["--id vocab-demo-xxx", "--id vocab-demo-xxx --quiet"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const vocabularyId = (flags as GetFlags).id;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: ctx.client.url(speechVocabularyPath()),
|
||||
request: buildVocabularyRequest("query_vocabulary", {
|
||||
vocabulary_id: vocabularyId,
|
||||
}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await queryVocabulary(ctx.client, vocabularyId);
|
||||
const output = response.output;
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(output?.target_model ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "text") {
|
||||
emitBare(`vocabulary_id: ${vocabularyId}`);
|
||||
emitBare(`status: ${output?.status ?? ""}`);
|
||||
emitBare(`target_model: ${output?.target_model ?? ""}`);
|
||||
emitBare(`gmt_create: ${output?.gmt_create ?? ""}`);
|
||||
emitBare(`gmt_modified: ${output?.gmt_modified ?? ""}`);
|
||||
const entries = output?.vocabulary ?? [];
|
||||
if (entries.length > 0) {
|
||||
emitBare("vocabulary:");
|
||||
for (const entry of entries) {
|
||||
const langPart = entry.lang ? ` lang=${entry.lang}` : "";
|
||||
emitBare(` ${entry.text} weight=${entry.weight}${langPart}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
speechVocabularyPath,
|
||||
buildVocabularyRequest,
|
||||
listVocabularies,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
prefix: {
|
||||
type: "string",
|
||||
valueHint: "<prefix>",
|
||||
description: {
|
||||
"en-US": "Filter by vocabulary prefix",
|
||||
"zh-CN": "按热词表前缀过滤",
|
||||
},
|
||||
},
|
||||
page: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Page number, 1-based (default: 1). Mapped to API page_index (0-based) as page - 1",
|
||||
"zh-CN": "页码,从 1 开始(默认:1)。映射为 API 的 page_index(从 0 开始):page - 1",
|
||||
},
|
||||
},
|
||||
pageSize: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Results per page (default: 10)",
|
||||
"zh-CN": "每页结果数(默认:10)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
type ListFlags = ParsedFlags<typeof LIST_FLAGS>;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "List precompiled hot-word vocabularies",
|
||||
"zh-CN": "列出预编译热词表",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--prefix <prefix>] [--page <n>] [--page-size <n>]",
|
||||
flags: LIST_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"List responses do not include target_model; use `speech vocabulary get` to inspect the model a vocabulary was built for.",
|
||||
"zh-CN": "list 响应不含 target_model;要对齐模型请使用 `speech vocabulary get`。",
|
||||
},
|
||||
{
|
||||
"en-US": "Vocabularies with status UNDEPLOYED are silently ignored by ASR.",
|
||||
"zh-CN": "status 为 UNDEPLOYED 的热词表会被 ASR 静默忽略。",
|
||||
},
|
||||
],
|
||||
exampleArgs: ["", "--prefix demo", "--page 2 --page-size 20"],
|
||||
validate: (flags: ListFlags) => {
|
||||
if (flags.page !== undefined && flags.page < 1) {
|
||||
return "--page must be >= 1.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const pageIndex = flags.page !== undefined ? flags.page - 1 : undefined;
|
||||
const input: Record<string, unknown> = {};
|
||||
if (flags.prefix !== undefined) input.prefix = flags.prefix;
|
||||
if (pageIndex !== undefined) input.page_index = pageIndex;
|
||||
if (flags.pageSize !== undefined) input.page_size = flags.pageSize;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: ctx.client.url(speechVocabularyPath()),
|
||||
request: buildVocabularyRequest("list_vocabulary", input),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await listVocabularies(ctx.client, {
|
||||
prefix: flags.prefix,
|
||||
pageIndex,
|
||||
pageSize: flags.pageSize,
|
||||
});
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
const items = response.output?.vocabulary_list ?? [];
|
||||
if (items.length === 0) {
|
||||
emitBare("No vocabularies found.");
|
||||
} else {
|
||||
let hasUndeployed = false;
|
||||
for (const item of items) {
|
||||
const id = item.vocabulary_id ?? "";
|
||||
const status = item.status ?? "";
|
||||
const modified = item.gmt_modified ?? "";
|
||||
if (status === "UNDEPLOYED") hasUndeployed = true;
|
||||
emitBare(`[${id}] ${status} ${modified}`.trimEnd());
|
||||
}
|
||||
if (hasUndeployed) {
|
||||
emitBare(
|
||||
"Note: UNDEPLOYED vocabularies are silently ignored by ASR. Use `speech vocabulary get` to inspect them.",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
readTextFromPathOrStdin,
|
||||
parseVocabularyEntries,
|
||||
UsageError,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
type VocabularyEntry,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
/** Shared --id flag for get / update / delete. */
|
||||
export const VOCABULARY_ID_FLAG = {
|
||||
id: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Hot-word vocabulary ID (required)",
|
||||
"zh-CN": "热词表 ID(必填)",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Shared hot-word body flags for create / update. */
|
||||
export const VOCABULARY_BODY_FLAGS = {
|
||||
words: {
|
||||
type: "string",
|
||||
valueHint: "<json>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Hot words as JSON object of word→weight, e.g. '{\"Fendouzhe\":4}'; or the API entry array for per-entry lang. Weight 1-5 (4 recommended); when the vocabulary target_model is a Qwen-Audio-3.0-ASR-Flash series model, 50 is also allowed as super hot word. Or use --words-file",
|
||||
"zh-CN":
|
||||
"热词,JSON 对象「热词→权重」,例如 '{\"奋斗者\":4}';需要逐条指定语言时可传 API 的条目数组。权重 1-5(推荐 4);当热词表的 target_model 为 Qwen-Audio-3.0-ASR-Flash 系列时还可使用 50(超级热词)。也可使用 --words-file",
|
||||
},
|
||||
},
|
||||
wordsFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "JSON file with the hot words (use - for stdin)",
|
||||
"zh-CN": "包含热词的 JSON 文件(使用 - 从 stdin 读取)",
|
||||
},
|
||||
},
|
||||
lang: {
|
||||
type: "string",
|
||||
valueHint: "<code>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Language code applied to every hot word when using object form (optional; ignored for array form). Paraformer: zh/en/ja/yue/ko/de/fr/ru; Fun-ASR: zh/en/ja",
|
||||
"zh-CN":
|
||||
"对象形态时应用到所有热词的语言代码(选填;数组形态忽略)。Paraformer 支持 zh/en/ja/yue/ko/de/fr/ru;Fun-ASR 支持 zh/en/ja",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type VocabularySourceFlags = ParsedFlags<typeof VOCABULARY_BODY_FLAGS>;
|
||||
|
||||
/** Cross-flag validation for --words / --words-file. */
|
||||
export function validateVocabularySource(flags: VocabularySourceFlags): string | undefined {
|
||||
if (!flags.words && !flags.wordsFile) {
|
||||
return "Provide --words or --words-file.";
|
||||
}
|
||||
if (flags.words && flags.wordsFile) {
|
||||
return "Use either --words or --words-file, not both.";
|
||||
}
|
||||
if (!flags.words) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
parseVocabularyEntries(flags.words, flags.lang);
|
||||
} catch (error) {
|
||||
if (error instanceof UsageError) {
|
||||
return error.message;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Read and parse vocabulary entries from flag or file. */
|
||||
export function readVocabularyEntries(flags: VocabularySourceFlags): VocabularyEntry[] {
|
||||
const raw = flags.wordsFile ? readTextFromPathOrStdin(flags.wordsFile) : (flags.words as string);
|
||||
return parseVocabularyEntries(raw, flags.lang);
|
||||
}
|
||||
|
||||
/** Shared notes covering account limits and silent-failure pitfalls. */
|
||||
export const VOCABULARY_LIMIT_NOTES = [
|
||||
{
|
||||
"en-US":
|
||||
"Each account may have at most 10 vocabularies; updates must be at least 5 minutes apart. See improve-asr-accuracy for full limits.",
|
||||
"zh-CN": "每个账号最多 10 个热词表;两次更新间隔至少 5 分钟。完整限制见 improve-asr-accuracy。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Hot-word vocabularies are not supported in Singapore sub-workspaces; the server error is passed through as-is.",
|
||||
"zh-CN": "新加坡子业务空间不支持热词表;服务端错误会原样透传。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Weight 1-5 (4 recommended); when the vocabulary target_model is a Qwen-Audio-3.0-ASR-Flash series model, 50 is also allowed as super hot word.",
|
||||
"zh-CN":
|
||||
"权重 1-5(推荐 4);当热词表的 target_model 为 Qwen-Audio-3.0-ASR-Flash 系列时还可使用 50(超级热词)。",
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
speechVocabularyPath,
|
||||
buildVocabularyRequest,
|
||||
updateVocabulary,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import {
|
||||
VOCABULARY_ID_FLAG,
|
||||
VOCABULARY_BODY_FLAGS,
|
||||
VOCABULARY_LIMIT_NOTES,
|
||||
validateVocabularySource,
|
||||
readVocabularyEntries,
|
||||
} from "./shared.ts";
|
||||
|
||||
const UPDATE_FLAGS = {
|
||||
...VOCABULARY_ID_FLAG,
|
||||
...VOCABULARY_BODY_FLAGS,
|
||||
} satisfies FlagsDef;
|
||||
type UpdateFlags = ParsedFlags<typeof UPDATE_FLAGS>;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Replace the contents of a precompiled hot-word vocabulary",
|
||||
"zh-CN": "完全替换预编译热词表的内容",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This fully replaces all hot words in the vocabulary. Entries not listed will be discarded and cannot be undone.",
|
||||
"zh-CN": "该操作会完全替换热词表中的全部词条。未列出的词将被丢弃,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--id <id> (--words <json> | --words-file <path>) [flags]",
|
||||
flags: UPDATE_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"update is a full replace, not an append. Prefer --dry-run first to preview the complete vocabulary that will be written.",
|
||||
"zh-CN": "update 是完全替换,不是增量追加。建议先用 --dry-run 预览将要写入的完整词表。",
|
||||
},
|
||||
...VOCABULARY_LIMIT_NOTES,
|
||||
],
|
||||
exampleArgs: [
|
||||
{
|
||||
"en-US": "--id vocab-demo-xxx --words '{\"Fendouzhe\":4}' --dry-run",
|
||||
"zh-CN": "--id vocab-demo-xxx --words '{\"奋斗者\":4}' --dry-run",
|
||||
},
|
||||
{
|
||||
"en-US": '--id vocab-demo-xxx --words \'{"Fendouzhe":4,"Jingluo":4}\' --yes',
|
||||
"zh-CN": '--id vocab-demo-xxx --words \'{"奋斗者":4,"鲸落":4}\' --yes',
|
||||
},
|
||||
],
|
||||
validate: (flags: UpdateFlags) => validateVocabularySource(flags),
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const vocabularyId = flags.id;
|
||||
const vocabulary = readVocabularyEntries(flags);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const request = buildVocabularyRequest("update_vocabulary", {
|
||||
vocabulary_id: vocabularyId,
|
||||
vocabulary,
|
||||
});
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: ctx.client.url(speechVocabularyPath()),
|
||||
request,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await updateVocabulary(ctx.client, vocabularyId, vocabulary);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(vocabularyId);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { printQuotaBox, readNumber, type QuotaSection } from "../usage/quota-box.ts";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
import type { HarnessBenefitItem, TokenPlanEquityInfo } from "./types.ts";
|
||||
|
||||
const HARNESS_LIST_API = "zeldaEasy.broadscope-bailian.token-plan.detail";
|
||||
const EQUITY_INFO_API = "zeldaEasy.bailian-commerce.tokenPlan.queryTokenPlanEquityInfo";
|
||||
|
||||
/** One harness that carries an entitlement quota, issued or still pending. */
|
||||
interface HarnessQuotaRow {
|
||||
planCode: string;
|
||||
title: string;
|
||||
type: string;
|
||||
/** `issuing` means the harness carries a quota but no entitlement is issued yet. */
|
||||
status: "issued" | "issuing";
|
||||
quotaUnit: string;
|
||||
instanceId?: string;
|
||||
totalQuota?: number;
|
||||
availableQuota?: number;
|
||||
usedQuota?: number;
|
||||
/** Used ratio in percent with 0.1 precision, matching the console display. */
|
||||
usedPercent?: number;
|
||||
instanceStartTime?: number;
|
||||
instanceEndTime?: number;
|
||||
}
|
||||
|
||||
function readArray<T>(result: unknown, field: string): T[] {
|
||||
const response = unwrapResponse(result as Record<string, unknown>);
|
||||
const value = response[field];
|
||||
return Array.isArray(value) ? (value as T[]) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the harness list with the issued entitlements, keeping the server order.
|
||||
* A harness carrying a resource pack but no matching entitlement is still
|
||||
* listed as `issuing` — issuance lags the purchase by a few minutes.
|
||||
*/
|
||||
function buildRows(
|
||||
items: HarnessBenefitItem[],
|
||||
equityInfos: TokenPlanEquityInfo[],
|
||||
): HarnessQuotaRow[] {
|
||||
const rows: HarnessQuotaRow[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
// Only harnesses that carry a resource pack have an entitlement quota at all.
|
||||
if (item.hasResourcePack !== true) continue;
|
||||
|
||||
const planCodes = item.planCodes ?? [];
|
||||
const equity = equityInfos.find(
|
||||
(equityInfo) => equityInfo.equityType && planCodes.includes(equityInfo.equityType),
|
||||
);
|
||||
const planCode = equity?.equityType ?? planCodes[0] ?? "";
|
||||
const row: HarnessQuotaRow = {
|
||||
planCode,
|
||||
title: item.title ?? planCode,
|
||||
type: item.type ?? "",
|
||||
status: equity ? "issued" : "issuing",
|
||||
quotaUnit: item.quotaUnit ?? item.priceInfo?.unit ?? "",
|
||||
};
|
||||
|
||||
if (!equity) {
|
||||
rows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (equity.instanceId) row.instanceId = equity.instanceId;
|
||||
const totalQuota = readNumber(equity.totalQuota);
|
||||
if (totalQuota !== undefined) row.totalQuota = totalQuota;
|
||||
const availableQuota = readNumber(equity.availableQuota);
|
||||
if (availableQuota !== undefined) row.availableQuota = availableQuota;
|
||||
if (totalQuota !== undefined && availableQuota !== undefined) {
|
||||
row.usedQuota = Math.max(totalQuota - availableQuota, 0);
|
||||
if (totalQuota > 0) {
|
||||
row.usedPercent = Math.round((row.usedQuota / totalQuota) * 1000) / 10;
|
||||
}
|
||||
}
|
||||
const instanceStartTime = readNumber(equity.instanceStartTime);
|
||||
if (instanceStartTime !== undefined) row.instanceStartTime = instanceStartTime;
|
||||
const instanceEndTime = readNumber(equity.instanceEndTime);
|
||||
if (instanceEndTime !== undefined) row.instanceEndTime = instanceEndTime;
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function toSection(row: HarnessQuotaRow): QuotaSection {
|
||||
const section: QuotaSection = {
|
||||
label: `${row.title} (${row.planCode})`,
|
||||
emptyMessage:
|
||||
row.status === "issuing"
|
||||
? "Quota is being issued; issuance can take up to 5 minutes."
|
||||
: "No positive quota total reported; check the Bailian Token Plan console.",
|
||||
};
|
||||
if (row.status === "issuing") return section;
|
||||
|
||||
if (row.totalQuota !== undefined && row.usedQuota !== undefined) {
|
||||
const unitSuffix = row.quotaUnit ? ` ${row.quotaUnit}` : "";
|
||||
section.detail = `Used: ${formatNumber(row.usedQuota)} / ${formatNumber(row.totalQuota)}${unitSuffix}`;
|
||||
if (row.totalQuota > 0) section.percentage = row.usedQuota / row.totalQuota;
|
||||
}
|
||||
if (row.instanceEndTime !== undefined) section.resetTime = row.instanceEndTime;
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Show Token Plan harness entitlement quota usage",
|
||||
"zh-CN": "查看 Token Plan harness 权益额度用量",
|
||||
},
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: {
|
||||
type: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
choices: ["official_tool", "infrastructure"] as const,
|
||||
description: {
|
||||
"en-US": "Filter harness list by type: official_tool, infrastructure",
|
||||
"zh-CN": "按类型筛选 harness 列表:official_tool、infrastructure",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--type official_tool", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const benefitsData = flags.type ? { type: flags.type } : {};
|
||||
const equityData = { queryTokenPlanEquityInfoRequest: { requestId: randomUUID() } };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
requests: [
|
||||
{ api: HARNESS_LIST_API, data: benefitsData },
|
||||
{ api: EQUITY_INFO_API, data: equityData },
|
||||
],
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [benefitsResult, equityResult] = await Promise.all([
|
||||
ctx.client.console(HARNESS_LIST_API, benefitsData),
|
||||
ctx.client.console(EQUITY_INFO_API, equityData),
|
||||
]);
|
||||
const rows = buildRows(
|
||||
readArray<HarnessBenefitItem>(benefitsResult, "items"),
|
||||
readArray<TokenPlanEquityInfo>(equityResult, "tokenPlanEquityInfos"),
|
||||
);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ generatedAt: Date.now(), items: rows }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
process.stdout.write("No harness with entitlement quota found.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printQuotaBox("Token Plan Harness Quota", rows.map(toSection), Date.now());
|
||||
},
|
||||
});
|
||||
@@ -65,3 +65,25 @@ export interface AddOrganizationMemberResponse {
|
||||
SeatAssigned?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Console gateway payloads keep the server's camelCase keys, unlike the
|
||||
// PascalCase OpenAPI shapes above.
|
||||
|
||||
export interface HarnessBenefitItem {
|
||||
type?: string;
|
||||
hasResourcePack?: boolean;
|
||||
planCodes?: string[];
|
||||
title?: string;
|
||||
quotaUnit?: string;
|
||||
priceInfo?: { unit?: string } | null;
|
||||
}
|
||||
|
||||
export interface TokenPlanEquityInfo {
|
||||
instanceId?: string;
|
||||
templateCode?: string;
|
||||
equityType?: string;
|
||||
totalQuota?: number;
|
||||
availableQuota?: number;
|
||||
instanceStartTime?: number;
|
||||
instanceEndTime?: number;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user