mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a755388734 | |||
| 2220f932b5 | |||
| 5c390fad74 | |||
| 6218637f7c | |||
| 0c7ba721a7 | |||
| 0dbf071368 | |||
| 16e30686f0 | |||
| 7803d91e3f | |||
| c2d17707c0 | |||
| 1dfb4400cc | |||
| fd2077b373 | |||
| 2090293f85 | |||
| a245359792 | |||
| 19bd5a5ea8 | |||
| e262f2e574 | |||
| 180aac9f28 | |||
| 3477a08dad | |||
| aa1484264e | |||
| 2767491ce6 | |||
| efb832815a | |||
| 902516bbad | |||
| f44fb2e95f | |||
| c6b9931d6f | |||
| a92c58420b | |||
| bcf68a31f3 | |||
| 8e5dd127a9 | |||
| 4f41eaabc2 | |||
| e1634c40ed | |||
| 4c5688ad4a | |||
| 176961dc56 | |||
| 888031562f | |||
| d017954ae8 | |||
| 9d19558645 | |||
| a9200a3f85 | |||
| 83d7982ecb | |||
| d33cb71cc7 | |||
| 737f8db359 | |||
| a402026cce | |||
| da6e1313e5 | |||
| 2d2ec1c628 | |||
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| 719c0b68bb | |||
| 9e6f5ca9cb | |||
| e53daf05eb | |||
| 67ae182ec3 | |||
| 676b6c2ece | |||
| 4f59ca5118 | |||
| 0872ff6a20 | |||
| afb547e0c8 | |||
| 449eede237 | |||
| 8906108744 | |||
| 2fbb6efe67 | |||
| 47e9bcc634 | |||
| 00bcee36a6 | |||
| 4c7ef0c7a5 | |||
| 9469556671 | |||
| b93e0d586d | |||
| 69c1fb9f08 |
@@ -6,6 +6,87 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Profile-level watermark control** — configure `watermark` with `bl config set --key watermark --value true|false` to control the default watermark behavior for image generation and editing, video generation and editing, and reference-to-video commands.
|
||||
- **ASR accuracy controls** — `bl speech recognize` now supports instant hot words with `--vocabulary`, contextual word enhancement with `--context`, and reusable pre-built vocabularies with `--vocabulary-id` for supported ASR models.
|
||||
- **Speech vocabulary management** — added `bl speech vocabulary create|list|get|update|delete` to manage reusable pre-built hot-word vocabularies.
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
|
||||
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
|
||||
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
|
||||
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
|
||||
|
||||
### Internal
|
||||
|
||||
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
|
||||
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
|
||||
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
|
||||
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent API commands** — added direct list, get, search, version, upload, download, run, pause, archive, event, and diagnostic operations for Agents, Environments, Skills, Vaults, Deployments, Sessions, and Files.
|
||||
- **Scoped YAML-backed resource creation** — `agent create`, `environment create`, `skill create`, `vault create`, `vault credential create`, and `deployment create` update `agents.yaml` and apply only the target resource without unrelated drift blocking the operation.
|
||||
- **Agent Skill attachment** — Agent creation supports existing custom or official Skill IDs as well as local Skill directories and ZIP archives.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bailian-only Managed Agent CLI** — `bl managed-agent` now targets the Bailian provider exclusively; provider-selection flags were removed and configurations containing other providers are rejected.
|
||||
- **Runtime mutation confirmation** — Deployment run/pause/unpause, Session archive/delete, and File delete operations require explicit high-risk confirmation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Managed Agent error reporting** — Apply and scoped-create failures preserve the underlying provider diagnostic instead of ending with only `Apply failed.`.
|
||||
|
||||
### Security
|
||||
|
||||
- Credentials are resolved in memory and removed from the process environment; Vault credential declarations reference environment variables without persisting plaintext secrets.
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl quota delete`** — clears all custom QPM/TPM rate limits for a model.
|
||||
|
||||
### Changed
|
||||
|
||||
- **High-risk operation confirmation** — high-risk commands show risk details in `--help` and Skill command references. Without `--yes`, the high-risk operation is not executed; JSON output returns exit code `7` with `error.type: "requires_confirmation"`. After confirmation, re-run with `--yes`; `--dry-run` does not require confirmation.
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -6,6 +6,87 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### 新增
|
||||
|
||||
- **Profile 级水印控制** —— 可通过 `bl config set --key watermark --value true|false` 设置图片生成与编辑、视频生成与编辑以及参考生视频命令的默认水印行为。
|
||||
- **ASR 准确率增强** —— `bl speech recognize` 现支持通过 `--vocabulary` 传入即时热词、通过 `--context` 增强上下文词表,以及通过 `--vocabulary-id` 使用适用于对应 ASR 模型的预编译热词表。
|
||||
- **语音热词表管理** —— 新增 `bl speech vocabulary create|list|get|update|delete`,用于管理可复用的预编译热词表。
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### 变更
|
||||
|
||||
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
|
||||
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。
|
||||
|
||||
### 修复
|
||||
|
||||
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
|
||||
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
|
||||
|
||||
### 内部
|
||||
|
||||
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
|
||||
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
|
||||
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent API 命令** —— 新增 Agent、Environment、Skill、Vault、Deployment、Session 和 File 的列表、详情、搜索、版本、上传、下载、运行、暂停、归档、事件及诊断等操作。
|
||||
- **基于 YAML 的单资源创建** —— `agent create`、`environment create`、`skill create`、`vault create`、`vault credential create` 和 `deployment create` 会更新 `agents.yaml`,并且只 Apply 目标资源,不受无关资源 Drift 阻塞。
|
||||
- **Agent Skill 挂载** —— 创建 Agent 时支持引用已有的自定义或官方 Skill ID,也支持本地 Skill 目录和 ZIP 文件。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Managed Agent CLI 限定为百炼 Provider** —— `bl managed-agent` 现在只面向百炼,移除 Provider 选择参数,并拒绝包含其他 Provider 的配置。
|
||||
- **运行时变更增加确认** —— Deployment 运行/暂停/恢复、Session 归档/删除以及 File 删除操作需要显式进行高风险确认。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Managed Agent 错误输出** —— Apply 和单资源创建失败时会保留底层 Provider 的具体诊断,不再只显示 `Apply failed.`。
|
||||
|
||||
### 安全
|
||||
|
||||
- 凭证仅在内存中解析并从进程环境清除;Vault Credential 声明通过环境变量引用 Secret,不会持久化明文。
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl quota delete`** — 清除指定模型的全部自定义 QPM/TPM 限流配置。
|
||||
|
||||
### 变更
|
||||
|
||||
- **高风险操作确认** — 高风险命令会在 `--help` 和 Skill 命令参考中展示风险说明。未传入 `--yes` 时,高风险操作不会执行;JSON 输出会返回退出码 `7` 和 `error.type: "requires_confirmation"`。确认后可添加 `--yes` 重新执行;`--dry-run` 无需确认。
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### 变更
|
||||
|
||||
- **删除与清除操作增加确认** —— `bl finetune delete`、`bl deploy delete`、`bl dataset delete` 和 `bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
@@ -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 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
|
||||
@@ -85,6 +85,13 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
|
||||
高风险命令额外要求:
|
||||
|
||||
- `--help` 展示 runtime 注入的 `--yes`
|
||||
- 无 `--yes` 返回 exit code 7 和 JSON `type: "requires_confirmation"`
|
||||
- `--dry-run` 无需 `--yes`,且必须证明在任何远端请求或本地写入之前返回
|
||||
- runtime 的离线 high-risk fixture 必须覆盖带 `--yes` 确实进入 `run()`,并断言 `yes` 不进入 command 自有 flags
|
||||
|
||||
## Journey 层(用户旅程全链路)
|
||||
|
||||
- **定位**:命令 E2E 验单命令契约;journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
|
||||
|
||||
@@ -71,7 +71,9 @@ packages/commands/src/index.ts
|
||||
- `usageArgs`(不含 bin/path 前缀)
|
||||
- `exampleArgs`(不含 bin/path 前缀)
|
||||
- `validate`(跨 flag 校验)
|
||||
- 高风险命令必须声明 `risk: { level: "high", message: <双语文案> }`;`--yes` 由 runtime 注入,命令不得自行声明
|
||||
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
|
||||
- 声明 `risk` 的 `run(ctx)` 必须在任何远端请求或本地写入之前处理 `ctx.settings.dryRun` 并返回预览;runtime 只负责确认闸门,不替命令实现 dry-run
|
||||
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
|
||||
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
|
||||
- [ ] 用户可见 Help 文案在命令文件中就近提供 `en-US` / `zh-CN`:命令 `description`、flag `description`、`notes` 和包含自然语言的 `exampleArgs`;纯命令语法示例可保留为字符串,服务端错误不翻译
|
||||
|
||||
@@ -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. 鉴权 / 全局选项
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
|
||||
- [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
|
||||
- [ ] **高风险确认**:统一由 `bailian-protocol` 定义;reference / leaf help 以 `risk: high` 明示风险,业务 skill 不得引导 Agent 自动补 `--yes`。遇到 exit code 7 / `requires_confirmation` 时停止执行并请求确认;目标或范围变化后重新确认
|
||||
- [ ] **正常控制流**:`requires_confirmation` 不是 CLI bug,`assets/issue-reporting.md` 必须将 exit code 7 保持在 EXCLUDE 范围
|
||||
- [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read,未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提
|
||||
- [ ] **Hub vs 领域**:`bailian-cli` 的「When to use which command」只列 hub 拥有的意图;媒体 / 精调 / managed-agent 各留 hand-off 行,**不抄**领域默认模型与子命令明细
|
||||
- [ ] **渐进披露**:SKILL 写意图路由与领域硬规则;flags / usage / examples 以 `reference/` 或 `bl <command> --help` 为准,表后保留「勿猜 flag」指向句
|
||||
@@ -55,6 +57,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
|
||||
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
|
||||
- [ ] 跑 `pnpm run sync:skill-assets`(或 commit 走 pre-commit),提交生成的 `reference/` 与 version 同步结果
|
||||
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
|
||||
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
│ ├─ ~/.bailian/telemetry.jsonl
|
||||
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
|
||||
│
|
||||
└─ authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
└─ confirmationStage
|
||||
└─ versionCheckStage → authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
```
|
||||
|
||||
### 1. 三套鉴权与埋点标识
|
||||
@@ -29,7 +30,7 @@
|
||||
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
|
||||
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 无 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
|
||||
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 仅对阿里云 host 注入
|
||||
- DashScope 上传策略 `getPolicy` 只有 `x-dashscope-source-config`,没有显式 CLI `User-Agent`
|
||||
- Console Gateway 用户数据由后端直接区分,不依赖 `x-dashscope-source-config`;AEM 的 `authMethod=console` 是独立的命令侧统计
|
||||
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product`、`action`、`api` 是鉴权或路由字段,不计为埋点标识
|
||||
|
||||
### 2. 后端渠道参数
|
||||
@@ -77,7 +79,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
### 3. 全命令 AEM 客户端埋点
|
||||
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹 `authStage` 与命令执行,因此成功、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹确认闸门、`authStage` 与命令执行,因此成功、确认未通过、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。确认闸门仍位于版本检查、鉴权和业务执行之前,不会因为埋点而放行高风险操作。
|
||||
|
||||
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`:
|
||||
|
||||
@@ -92,7 +94,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
- `command`、`timestamp`、`durationMs`、`success`
|
||||
- `cliVersion`、`nodeVersion`、`os`
|
||||
- `authMethod`
|
||||
- 失败时的 `errorMessage`、`httpStatus`、`requestId`
|
||||
- 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId`
|
||||
- 安全 allowlist 过滤后的 `params`
|
||||
|
||||
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
|
||||
@@ -108,16 +110,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
AEM 映射:
|
||||
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段 |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ------------------------------------------------------------------ |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段,包含失败时的 `exitCode` |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
|
||||
远端发送是 best-effort,不得阻塞命令或改变退出码。正常退出最多等待 1 秒,SIGINT 最多等待 500 ms。
|
||||
|
||||
@@ -144,6 +146,7 @@ AEM 映射:
|
||||
- [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射
|
||||
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
|
||||
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
|
||||
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5`
|
||||
- [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口
|
||||
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ bl knowledge category delete --category-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------ |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -190,11 +190,11 @@ bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
|
||||
# 删除单个文档
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 批量删除,跳过确认
|
||||
# 用户明确确认后批量删除
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
|
||||
@@ -120,10 +120,10 @@ bl knowledge file delete --file-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -254,10 +254,10 @@ bl knowledge delete --index-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ bl knowledge service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ bl knowledge service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ kscli category delete --category-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------ |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+6
-6
@@ -190,11 +190,11 @@ kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
|
||||
# 删除单个文档
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 批量删除,跳过确认
|
||||
# 用户明确确认后批量删除
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
|
||||
+4
-4
@@ -120,10 +120,10 @@ kscli file delete --file-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+5
-5
@@ -254,10 +254,10 @@ kscli kb delete --index-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ kscli --help
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ kscli service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ kscli service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
|
||||
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
|
||||
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
|
||||
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
|
||||
| Model selection | "Recommend a model for image understanding and customer support." |
|
||||
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
|
||||
|
||||
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
|
||||
**Flags**:`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`。
|
||||
|
||||
**实现方案**:`doc-delete.ts`;确认摘要含 index_id + doc_id 列表(≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
**实现方案**:`doc-delete.ts`;命令在 `risk` 对象中同时声明 `level: "high"` 和双语 `message`,由 runtime 在 `run()` 前统一确认;输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY 无 `--yes` exitCode 2 / live 配合 upload 清理链。
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 无 `--yes` 返回 exitCode 7 + `requires_confirmation` / live 配合 upload 清理链。
|
||||
|
||||
## doc tag — 批量更新文档标签
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.1",
|
||||
"version": "1.23.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
|
||||
// succeeded, so a backup deletion failure must not fail the pre-download
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed swap as failed */
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -214,7 +220,11 @@ async function main() {
|
||||
}
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* cleanup must not mask the original error */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@ import {
|
||||
searchWeb,
|
||||
speechSynthesize,
|
||||
speechRecognize,
|
||||
speechVocabularyCreate,
|
||||
speechVocabularyList,
|
||||
speechVocabularyGet,
|
||||
speechVocabularyUpdate,
|
||||
speechVocabularyDelete,
|
||||
fileUpload,
|
||||
consoleCall,
|
||||
usageFree,
|
||||
@@ -140,6 +145,18 @@ import {
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
managedAgentPlayground,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectWorkbench,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
managedAgentStateRm,
|
||||
@@ -152,6 +169,48 @@ 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,
|
||||
} from "bailian-cli-commands";
|
||||
|
||||
// Full bailian-cli product: every command, exposed under the `bl` binary.
|
||||
@@ -232,6 +291,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,
|
||||
@@ -302,6 +366,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 +390,49 @@ 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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("e2e: Command Pack", () => {
|
||||
expect(linkedJson.linked.commands).toEqual([
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
@@ -170,6 +171,34 @@ describe("e2e: Command Pack", () => {
|
||||
expect(failed.stderr).toContain("Use agent fail only in tests.");
|
||||
});
|
||||
|
||||
test("high-risk 命令由 runtime 统一确认并支持安全 dry-run", async () => {
|
||||
const dangerousHelp = await runCli(["agent", "dangerous", "--help"], env());
|
||||
expect(dangerousHelp.exitCode, dangerousHelp.stderr).toBe(0);
|
||||
expect(dangerousHelp.stderr).toContain("--yes");
|
||||
|
||||
const unconfirmed = await runCli(["agent", "dangerous", "--output", "json"], env());
|
||||
expect(unconfirmed.exitCode).toBe(7);
|
||||
expect(JSON.parse(unconfirmed.stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
|
||||
const confirmed = await runCli(["agent", "dangerous", "--yes", "--output", "json"], env());
|
||||
expect(confirmed.exitCode, confirmed.stderr).toBe(0);
|
||||
expect(parseStdoutJson(confirmed.stdout)).toEqual({
|
||||
executed: true,
|
||||
dry_run: false,
|
||||
command_flags: [],
|
||||
});
|
||||
|
||||
const preview = await runCli(["agent", "dangerous", "--dry-run", "--output", "json"], env());
|
||||
expect(preview.exitCode, preview.stderr).toBe(0);
|
||||
expect(parseStdoutJson(preview.stdout)).toEqual({
|
||||
executed: false,
|
||||
dry_run: true,
|
||||
command_flags: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("plugin list 输出加载状态", async () => {
|
||||
const result = await runCli(["plugin", "list", "--output", "json"], env());
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
@@ -183,6 +212,7 @@ describe("e2e: Command Pack", () => {
|
||||
commands: [
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
|
||||
@@ -19,6 +19,26 @@ const ping = {
|
||||
},
|
||||
};
|
||||
|
||||
const dangerous = {
|
||||
description: "Exercise runtime confirmation for a high-risk Command Pack command",
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This fixture represents a high-risk operation.",
|
||||
"zh-CN": "该测试命令代表高风险操作。",
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
const dryRun = ctx.settings.dryRun;
|
||||
ctx.output.result({
|
||||
executed: !dryRun,
|
||||
dry_run: dryRun,
|
||||
command_flags: Object.keys(ctx.flags),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const credential = {
|
||||
description: "Read an API key through the Command Pack host adapter",
|
||||
auth: "apiKey",
|
||||
@@ -57,6 +77,7 @@ const fail = {
|
||||
export default {
|
||||
"agent credential": credential,
|
||||
"agent credential-denied": credentialDenied,
|
||||
"agent dangerous": dangerous,
|
||||
"agent fail": fail,
|
||||
"agent output": output,
|
||||
"agent ping": ping,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const skillsRoot = join(repositoryRoot, "skills");
|
||||
const scopedCreateCommands = new Set([
|
||||
"bl managed-agent agent create",
|
||||
"bl managed-agent deployment create",
|
||||
"bl managed-agent environment create",
|
||||
"bl managed-agent skill create",
|
||||
"bl managed-agent vault create",
|
||||
"bl managed-agent vault credential create",
|
||||
]);
|
||||
|
||||
test("generated references distinguish runtime high-risk confirmation from scoped create execution", () => {
|
||||
let highRiskCommandCount = 0;
|
||||
const seenScopedCreateCommands = new Set<string>();
|
||||
|
||||
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!skillDirectory.isDirectory()) continue;
|
||||
const referenceDirectory = join(skillsRoot, skillDirectory.name, "reference");
|
||||
|
||||
let referenceFiles: string[];
|
||||
try {
|
||||
referenceFiles = readdirSync(referenceDirectory).filter(
|
||||
(fileName) => fileName.endsWith(".md") && fileName !== "index.md",
|
||||
);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const referenceFile of referenceFiles) {
|
||||
const markdown = readFileSync(join(referenceDirectory, referenceFile), "utf8");
|
||||
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
|
||||
|
||||
for (const commandSection of commandSections) {
|
||||
const commandName = commandSection.match(/^### `([^`]+)`/m)?.[1];
|
||||
const hasConfirmationFlag = commandSection.includes("`--yes`");
|
||||
const hasHighRiskMetadata = /\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/.test(commandSection);
|
||||
|
||||
if (!hasHighRiskMetadata) {
|
||||
if (!hasConfirmationFlag) continue;
|
||||
expect(commandName).toBeDefined();
|
||||
expect(scopedCreateCommands.has(commandName ?? "")).toBe(true);
|
||||
expect(commandSection).toMatch(/Without --yes, .*preview/i);
|
||||
seenScopedCreateCommands.add(commandName ?? "");
|
||||
continue;
|
||||
}
|
||||
|
||||
highRiskCommandCount += 1;
|
||||
expect(hasConfirmationFlag).toBe(true);
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
|
||||
expect(commandSection).toMatch(/type=.*requires_confirmation/);
|
||||
const agentSafetyLine = commandSection
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("> **Agent safety:**"));
|
||||
expect(agentSafetyLine).toBeDefined();
|
||||
expect(agentSafetyLine).toMatch(/never add `--yes` automatically/i);
|
||||
expect(agentSafetyLine).toMatch(/explicit user confirmation/i);
|
||||
expect(agentSafetyLine).not.toContain("`--dry-run`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(highRiskCommandCount).toBeGreaterThan(0);
|
||||
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.1",
|
||||
"version": "1.23.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,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
fileId: {
|
||||
@@ -8,28 +8,25 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id> [--yes]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified dataset file and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的数据集文件,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--file-id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--file-id file-id-xxx",
|
||||
"--file-id file-id-xxx --dry-run",
|
||||
"--file-id file-id-xxx --yes",
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the dataset file is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
@@ -39,11 +36,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDataset(ctx.client, fileId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -25,10 +25,6 @@ const DELETE_FLAGS = {
|
||||
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
@@ -44,14 +40,15 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the deployment is permanently destroyed.",
|
||||
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified model deployment and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的模型部署,且无法撤销。",
|
||||
},
|
||||
],
|
||||
},
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
@@ -87,11 +84,6 @@ export default defineCommand({
|
||||
}
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDeployment(ctx.client, deployedModel);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
jobId: {
|
||||
@@ -8,23 +8,22 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--job-id <id> [--yes]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the specified fine-tune job record and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除指定的微调任务记录,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--job-id <id>",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the job record is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
|
||||
@@ -40,11 +39,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteFineTune(ctx.client, jobId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -207,7 +207,7 @@ export default defineCommand({
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
const watermark = resolveWatermark(flags.watermark, settings.watermark);
|
||||
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size: resolveImageSize(flags.size, route.sizeProfile),
|
||||
|
||||
@@ -185,7 +185,7 @@ export default defineCommand({
|
||||
"prompt-extend",
|
||||
);
|
||||
|
||||
const watermark = resolveWatermark(flags.watermark);
|
||||
const watermark = resolveWatermark(flags.watermark, settings.watermark);
|
||||
|
||||
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
|
||||
size,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagConnectorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CATEGORY_DELETE_FLAGS = {
|
||||
@@ -16,16 +16,19 @@ const CATEGORY_DELETE_FLAGS = {
|
||||
description: { "en-US": "Category ID to delete", "zh-CN": "要删除的类目 ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a data-center category", "zh-CN": "删除数据中心类目" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This deletes the selected data-center category and cannot be undone.",
|
||||
"zh-CN": "该操作会删除所选数据中心类目,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--category-id <id> [flags]",
|
||||
flags: CATEGORY_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -49,11 +52,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete category ${flags.categoryId}\nThis cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await ctx.client.requestJson<
|
||||
RagConnectorResponse<Record<string, unknown> | undefined>
|
||||
>({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CHUNK_DELETE_FLAGS = {
|
||||
@@ -26,10 +26,6 @@ const CHUNK_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -48,6 +44,13 @@ export default defineCommand({
|
||||
"zh-CN": "从知识库中删除 Chunk(不可撤销)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the selected chunks and cannot be undone.",
|
||||
"zh-CN": "该操作会永久删除所选 Chunk,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
|
||||
flags: CHUNK_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -81,11 +84,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete ${flags.chunkId.length} chunk(s) from knowledge base ${flags.indexId} in ${batches.length} batch(es).\nChunks are permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
// Sequential batches; any batch failure aborts, listing already-deleted batches in the error
|
||||
let deletedCount = 0;
|
||||
for (const batchIds of batches) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type FlagsDef,
|
||||
type RagDeleteFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_DELETE_FLAGS = {
|
||||
@@ -25,28 +25,22 @@ const DOC_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary: list all doc_ids up to 5, otherwise show the first 5 + total count */
|
||||
function buildDeleteSummary(indexId: string, docIds: string[]): string {
|
||||
const listed =
|
||||
docIds.length <= 5
|
||||
? docIds.join("\n ")
|
||||
: `${docIds.slice(0, 5).join("\n ")}\n ... (${docIds.length} documents total)`;
|
||||
return `Delete ${docIds.length} document(s) from knowledge base ${indexId}:\n ${listed}\nDocuments and all their chunks are permanently removed from the index. This cannot be undone.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete documents and their chunks from a knowledge base",
|
||||
"zh-CN": "从知识库中删除文档及其 Chunk",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This permanently deletes the selected documents and all of their chunks.",
|
||||
"zh-CN": "该操作会永久删除所选文档及其全部 Chunk,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> --doc-id <id> [flags]",
|
||||
flags: DOC_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -72,7 +66,7 @@ export default defineCommand({
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx --dry-run",
|
||||
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
@@ -89,11 +83,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
buildDeleteSummary(flags.indexId, flags.docId),
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await ctx.client.requestJson<RagDeleteFileResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagConnectorResponse,
|
||||
type RagDescribeFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const FILE_DELETE_FLAGS = {
|
||||
@@ -21,39 +19,23 @@ const FILE_DELETE_FLAGS = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (file name/size); failure degrades to id-only */
|
||||
async function buildDeleteSummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
fileId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagDescribeFileResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.describeFile),
|
||||
method: "POST",
|
||||
body: { fileId },
|
||||
});
|
||||
if (detail.data?.fileName) infoPart = ` name: ${detail.data.fileName}`;
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Delete data-center file ${fileId}${infoPart}\nPERMANENT: if the file is referenced by knowledge bases, their document indexes break too. This differs from removing a document from one knowledge base.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Permanently delete a file from the data center",
|
||||
"zh-CN": "从数据中心永久删除文件",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This permanently deletes the data-center file. Knowledge-base document indexes that reference it may become invalid.",
|
||||
"zh-CN": "该操作会永久删除数据中心文件;引用该文件的知识库文档索引可能失效。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--file-id <id> [flags]",
|
||||
flags: FILE_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -82,11 +64,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeleteSummary(ctx.client, workspaceId, flags.fileId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<
|
||||
RagConnectorResponse<Record<string, unknown> | undefined>
|
||||
>({
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagIndexFilesResponse,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
import { fetchIndexDetail } from "./kb-info.ts";
|
||||
|
||||
const KB_DELETE_FLAGS = {
|
||||
indexId: {
|
||||
@@ -18,50 +16,23 @@ const KB_DELETE_FLAGS = {
|
||||
description: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup: name + document count; any lookup failure degrades to id-only (never blocks deletion) */
|
||||
async function buildDeleteSummary(
|
||||
ctx: { client: Parameters<typeof fetchIndexDetail>[0] },
|
||||
workspaceId: string,
|
||||
indexId: string,
|
||||
): Promise<string> {
|
||||
let namePart = "";
|
||||
let docCountPart = "";
|
||||
try {
|
||||
const detail = await fetchIndexDetail(ctx.client, workspaceId, indexId);
|
||||
namePart = ` name: ${detail.name}`;
|
||||
} catch {
|
||||
// Degrade gracefully: a missing name does not block confirmation
|
||||
}
|
||||
try {
|
||||
const filesUrl = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
|
||||
filesUrl.searchParams.set("index_id", indexId);
|
||||
filesUrl.searchParams.set("page_num", "1");
|
||||
filesUrl.searchParams.set("page_size", "1");
|
||||
const files = await ctx.client.requestJson<RagIndexFilesResponse>({
|
||||
path: filesUrl.toString(),
|
||||
method: "GET",
|
||||
});
|
||||
const totalCount = files.data?.total_count;
|
||||
if (typeof totalCount === "number") docCountPart = ` documents: ${totalCount}`;
|
||||
} catch {
|
||||
// Same graceful degradation as above
|
||||
}
|
||||
return `Delete knowledge base ${indexId}${namePart}${docCountPart}\nThis permanently removes the knowledge base with all documents and chunks. It cannot be undone.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete a knowledge base with all its documents and chunks",
|
||||
"zh-CN": "删除知识库及其所有文档和 Chunk",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This permanently deletes the knowledge base and all of its documents and chunks. Data-center files are not deleted.",
|
||||
"zh-CN": "该操作会永久删除知识库及其全部文档和 Chunk,但不会删除数据中心文件。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -90,11 +61,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? "" // --yes bypasses the prompt, so skip the summary lookups
|
||||
: await buildDeleteSummary(ctx, workspaceId, flags.indexId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DELETE_FLAGS = {
|
||||
@@ -18,48 +16,23 @@ const SERVICE_DELETE_FLAGS = {
|
||||
description: { "en-US": "Service (agent) ID", "zh-CN": "服务(Agent)ID" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); failure degrades to id-only */
|
||||
async function buildDeleteSummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let liveWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "deployed" || status === "edited") {
|
||||
liveWarning = "\nWARNING: this service is LIVE — deleting it breaks existing callers.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Delete service ${agentId}${infoPart}${liveWarning}\nDeletion cannot be undone; the agent_id can no longer be used for search or chat calls.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Delete a retrieval / Q&A service (soft delete, idempotent)",
|
||||
"zh-CN": "删除检索/问答服务(软删除,幂等)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This deletes the service and makes its agent ID unavailable for search and chat calls. The operation cannot be undone.",
|
||||
"zh-CN": "该操作会删除服务,使其 Agent ID 无法再用于搜索和对话调用,且无法撤销。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DELETE_FLAGS,
|
||||
notes: [
|
||||
@@ -91,11 +64,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeleteSummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DEPLOY_FLAGS = {
|
||||
@@ -26,49 +24,23 @@ const SERVICE_DEPLOY_FLAGS = {
|
||||
"zh-CN": "新发布版本的描述",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); warns that deploying an edited draft overwrites live behavior; failure degrades to id-only */
|
||||
async function buildDeploySummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let editedWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "edited") {
|
||||
editedWarning =
|
||||
"\nWARNING: a published version is live — deploying replaces its behavior with the current draft.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Deploy service ${agentId}${infoPart}${editedWarning}\nPublishing changes what live callers get from this service.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Publish the beta draft of a service as a new version",
|
||||
"zh-CN": "将服务的 beta 草稿发布为新版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This publishes the current draft as a new version and changes the behavior seen by live callers.",
|
||||
"zh-CN": "该操作会将当前草稿发布为新版本,并改变线上调用方使用的服务行为。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DEPLOY_FLAGS,
|
||||
notes: [
|
||||
@@ -109,11 +81,6 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeploySummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { FlagsDef } from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
import type { PagedResult } from "./pagination.ts";
|
||||
|
||||
export const API_TARGET_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const CURSOR_FLAGS = {
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Page size (1-100)",
|
||||
"zh-CN": "单页数量(1-100)",
|
||||
},
|
||||
},
|
||||
page: {
|
||||
type: "string",
|
||||
valueHint: "<cursor>",
|
||||
description: {
|
||||
"en-US": "Opaque page cursor returned by a previous request",
|
||||
"zh-CN": "上一次请求返回的不透明分页 Cursor",
|
||||
},
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Fetch all pages by following opaque cursors",
|
||||
"zh-CN": "跟随不透明 Cursor 获取全部分页",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const SEARCH_FLAGS = {
|
||||
query: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Case-insensitive text to find in IDs, names, and descriptions",
|
||||
"zh-CN": "在 ID、名称和描述中进行不区分大小写的文本搜索",
|
||||
},
|
||||
},
|
||||
pageLimit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Maximum pages to scan for client-side search (default: 10)",
|
||||
"zh-CN": "客户端搜索最多扫描的页数(默认:10)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const INCLUDE_ARCHIVED_FLAG = {
|
||||
includeArchived: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Include archived resources",
|
||||
"zh-CN": "包含已归档资源",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export function validateLimitAndPageLimit(flags: {
|
||||
limit?: number;
|
||||
pageLimit?: number;
|
||||
}): string | undefined {
|
||||
if (
|
||||
flags.limit !== undefined &&
|
||||
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100)
|
||||
) {
|
||||
return "--limit must be an integer between 1 and 100.";
|
||||
}
|
||||
if (
|
||||
flags.pageLimit !== undefined &&
|
||||
(!Number.isInteger(flags.pageLimit) || flags.pageLimit < 1 || flags.pageLimit > 100)
|
||||
) {
|
||||
return "--page-limit must be an integer between 1 and 100.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function splitCommaSeparated(value?: string): string[] | undefined {
|
||||
if (!value) return undefined;
|
||||
const values = value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
export function matchesQuery(query: string, ...values: unknown[]): boolean {
|
||||
const normalized = query.trim().toLocaleLowerCase();
|
||||
if (!normalized) return true;
|
||||
return values.some((value) => {
|
||||
if (value === undefined || value === null) return false;
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return text.toLocaleLowerCase().includes(normalized);
|
||||
});
|
||||
}
|
||||
|
||||
export interface SearchResult<T> extends PagedResult<T> {
|
||||
scannedPages: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export async function searchCursorPages<T>(
|
||||
fetchPage: (page?: string) => Promise<PagedResult<T>>,
|
||||
matches: (item: T) => boolean,
|
||||
pageLimit = 10,
|
||||
): Promise<SearchResult<T>> {
|
||||
const items: T[] = [];
|
||||
let page: string | undefined;
|
||||
let hasMore = false;
|
||||
let nextPage: string | undefined;
|
||||
let scannedPages = 0;
|
||||
|
||||
do {
|
||||
const result = await fetchPage(page);
|
||||
scannedPages += 1;
|
||||
items.push(...result.items.filter(matches));
|
||||
hasMore = result.hasMore;
|
||||
nextPage = result.nextPage;
|
||||
page = result.nextPage;
|
||||
} while (hasMore && page && scannedPages < pageLimit);
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
nextPage,
|
||||
scannedPages,
|
||||
truncated: Boolean(hasMore && nextPage),
|
||||
};
|
||||
}
|
||||
|
||||
export function emitCollection<T>(options: {
|
||||
format: "json" | "text";
|
||||
key: string;
|
||||
items: T[];
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
hasMore?: boolean;
|
||||
nextPage?: string;
|
||||
truncated?: boolean;
|
||||
scannedPages?: number;
|
||||
emptyMessage?: string;
|
||||
}): void {
|
||||
const {
|
||||
format,
|
||||
key,
|
||||
items,
|
||||
headers,
|
||||
rows,
|
||||
hasMore = false,
|
||||
nextPage,
|
||||
truncated,
|
||||
scannedPages,
|
||||
emptyMessage = "No resources found.",
|
||||
} = options;
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
[key]: items,
|
||||
has_more: hasMore,
|
||||
next_page: nextPage,
|
||||
...(truncated === undefined ? {} : { truncated }),
|
||||
...(scannedPages === undefined ? {} : { scanned_pages: scannedPages }),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
emitBare(emptyMessage);
|
||||
return;
|
||||
}
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
emitBare(`\nTotal: ${items.length}`);
|
||||
if (truncated) emitBare("Search stopped at --page-limit; more pages remain.");
|
||||
else if (hasMore)
|
||||
emitBare(`More results are available.${nextPage ? ` Next page: ${nextPage}` : ""}`);
|
||||
}
|
||||
|
||||
export function displayValue(value: unknown, maxLength = 40): string {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
type LoadedProjectConfig,
|
||||
type ProjectRuntimeContext,
|
||||
resolveProjectConfig,
|
||||
UserError,
|
||||
} from "@openagentpack/sdk";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import {
|
||||
assertProviderCredentials,
|
||||
type CredentialHost,
|
||||
@@ -20,17 +20,21 @@ export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
|
||||
|
||||
/**
|
||||
* Whether this run requires provider keys:
|
||||
* - "all" (default) — online command: every provider declared in agents.yaml
|
||||
* must have a non-empty key after injection
|
||||
* - "all" (default) — online command: the Bailian provider must have a
|
||||
* non-empty key after injection
|
||||
* - "none" — offline command (local config/state only), skip the check
|
||||
*/
|
||||
export type CredentialScope = "all" | "none";
|
||||
|
||||
export const BAILIAN_PROVIDER = "bailian";
|
||||
|
||||
interface AgentConfigOptions {
|
||||
resolveEnv?: boolean;
|
||||
environment?: Record<string, string>;
|
||||
projectName?: string;
|
||||
statePath?: string;
|
||||
credentials?: CredentialScope;
|
||||
overrideBailianBaseUrl?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +46,8 @@ interface AgentConfigOptions {
|
||||
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
|
||||
* 4. scrub all credential vars from process.env (real values now live only in
|
||||
* the config object → provider adapters, never the environment)
|
||||
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
|
||||
* 5. enforce Bailian-only config and fail with a CLI-authoritative AUTH error
|
||||
* if its key is empty
|
||||
* (offline commands pass `credentials: "none"` to skip the check)
|
||||
*/
|
||||
export async function resolveAgentProjectConfig(
|
||||
@@ -53,8 +58,11 @@ export async function resolveAgentProjectConfig(
|
||||
prepareProviderEnv();
|
||||
const resolved = await resolveProjectConfig(filePath, options);
|
||||
normalizeInterpolatedProviderBlocks(resolved.config.providers);
|
||||
injectProviderCredentials(resolved.config.providers, host);
|
||||
injectProviderCredentials(resolved.config.providers, host, {
|
||||
overrideBaseUrl: options.overrideBailianBaseUrl,
|
||||
});
|
||||
scrubCredentialEnv();
|
||||
assertBailianOnlyProviders(resolved.config.providers);
|
||||
if ((options.credentials ?? "all") !== "none") {
|
||||
assertProviderCredentials(resolved.config.providers);
|
||||
}
|
||||
@@ -91,15 +99,23 @@ export async function buildAgentRuntime(
|
||||
return { ...ctx, configPath };
|
||||
}
|
||||
|
||||
/** Ensure a user-supplied --provider value is actually configured in agents.yaml. */
|
||||
export function assertProviderConfigured(
|
||||
ctx: ProjectRuntimeContext,
|
||||
provider: string | undefined,
|
||||
): void {
|
||||
if (!provider || provider === "all") return;
|
||||
if (ctx.providers.has(provider)) return;
|
||||
const available = Array.from(ctx.providers.keys()).join(", ") || "none";
|
||||
throw new UserError(
|
||||
`Provider '${provider}' is not configured. Available providers: ${available}.`,
|
||||
/** Enforce the bl product boundary while the shared SDK remains multi-provider capable. */
|
||||
export function assertBailianOnlyProviders(providers: Record<string, unknown>): void {
|
||||
const configuredProviders = Object.keys(providers);
|
||||
const unsupportedProviders = configuredProviders.filter(
|
||||
(provider) => provider !== BAILIAN_PROVIDER,
|
||||
);
|
||||
if (unsupportedProviders.length > 0) {
|
||||
const names = unsupportedProviders.join(", ");
|
||||
throw new BailianError(
|
||||
`bl managed-agent only supports provider '${BAILIAN_PROVIDER}'; remove unsupported providers: ${names}. / bl managed-agent 仅支持 Provider '${BAILIAN_PROVIDER}';请移除不支持的 Provider:${names}。`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (!(BAILIAN_PROVIDER in providers)) {
|
||||
throw new BailianError(
|
||||
`bl managed-agent requires a '${BAILIAN_PROVIDER}' provider configuration. / bl managed-agent 需要配置 Provider '${BAILIAN_PROVIDER}'。`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ const CREDENTIAL_HINTS: Record<string, string> = {
|
||||
qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.",
|
||||
};
|
||||
|
||||
const BAILIAN_ONLY_NOTE = {
|
||||
"en-US":
|
||||
"bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.",
|
||||
"zh-CN": "bl managed-agent 仅支持百炼 Provider;包含其他 Provider 的配置会被拒绝。",
|
||||
} as const;
|
||||
|
||||
/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */
|
||||
export interface CredentialHost {
|
||||
client: Client;
|
||||
@@ -43,24 +49,18 @@ export interface CredentialHost {
|
||||
/**
|
||||
* Shared `--help` note documenting where agent commands get provider
|
||||
* credentials. Bailian goes through bl's own auth chain (commands declare
|
||||
* `auth: "apiKey"`); other providers come from env. Either way the resolved
|
||||
* credential is injected into the SDK in-memory and scrubbed from the
|
||||
* environment. Attach to every command that loads agents.yaml. `bl` prefix is
|
||||
* safe: agent commands ship on `bl` only.
|
||||
* `auth: "apiKey"`). The resolved credential is injected into the SDK in-memory
|
||||
* and scrubbed from the environment. Attach to every command that loads
|
||||
* agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
|
||||
*/
|
||||
export const CREDENTIALS_NOTE = [
|
||||
BAILIAN_ONLY_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
|
||||
"zh-CN":
|
||||
"百炼凭证来自 bl 鉴权链:--api-key > DASHSCOPE_API_KEY > `bl auth login`(当前激活的配置 Profile)。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
|
||||
"zh-CN":
|
||||
"其他 Provider 读取 agents.yaml 中引用的环境变量(例如 ${ANTHROPIC_API_KEY}),包括 .env 和 ~/.agents/config.json。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
|
||||
@@ -73,6 +73,7 @@ export const CREDENTIALS_NOTE = [
|
||||
* agents.yaml / local state only, so no login or provider key is required.
|
||||
*/
|
||||
export const OFFLINE_NOTE = [
|
||||
BAILIAN_ONLY_NOTE,
|
||||
{
|
||||
"en-US": "Runs fully offline against local files: no login or provider credentials required.",
|
||||
"zh-CN": "完全离线处理本地文件:无需登录或提供 Provider 凭证。",
|
||||
@@ -98,9 +99,10 @@ export function prepareProviderEnv(): void {
|
||||
* Override the bailian provider block with bl's authStage-resolved credential, so
|
||||
* the bailian API key is authoritatively the CLI auth chain's — never a config
|
||||
* file bare-read or a stale env value. `api_key` is replaced unconditionally
|
||||
* when a credential resolved; `base_url` / `workspace_id` are filled only when
|
||||
* the block references them and the interpolated value is empty (a literal in
|
||||
* agents.yaml is respected).
|
||||
* when a credential resolved. `base_url` is normally filled only when empty so
|
||||
* a literal in agents.yaml remains supported; directory projects pass
|
||||
* `overrideBaseUrl` so their connection always follows the Bailian CLI auth
|
||||
* chain. `workspace_id` is filled only when empty.
|
||||
*
|
||||
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
|
||||
* paths onto it verbatim; a value already ending in the suffix is left as-is.
|
||||
@@ -113,6 +115,7 @@ export function prepareProviderEnv(): void {
|
||||
export function injectProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
host: CredentialHost,
|
||||
options: { overrideBaseUrl?: boolean } = {},
|
||||
): void {
|
||||
const bailian = providers.bailian;
|
||||
if (!bailian || typeof bailian !== "object") return;
|
||||
@@ -120,7 +123,7 @@ export function injectProviderCredentials(
|
||||
|
||||
const cred = host.client.exportApiCredential();
|
||||
if (cred) block.api_key = cred.token;
|
||||
if ("base_url" in block && !block.base_url) {
|
||||
if ("base_url" in block && (options.overrideBaseUrl || !block.base_url)) {
|
||||
// Defensive normalization: the auth chain already normalizes base_url to
|
||||
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
|
||||
const origin = host.client.baseUrl.replace(/\/+$/, "");
|
||||
@@ -166,15 +169,16 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
|
||||
}
|
||||
|
||||
/**
|
||||
* After injection, fail with a CLI-authoritative AUTH error if any configured
|
||||
* provider's `api_key` resolved empty (missing env var, or no bl login for
|
||||
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
|
||||
* zod config error with a clean message plus a provider-specific hint. Validates
|
||||
* every declared provider, so a project is only runnable once all its providers'
|
||||
* keys are available; offline commands skip the check entirely.
|
||||
* After injection, fail with a CLI-authoritative AUTH error if Bailian's
|
||||
* `api_key` resolved empty. Replaces the SDK's raw config error with a clean
|
||||
* message and hint. Offline commands skip the check entirely.
|
||||
*/
|
||||
export function assertProviderCredentials(providers: Record<string, unknown>): void {
|
||||
export function assertProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
targetProviders?: readonly string[],
|
||||
): void {
|
||||
for (const [name, raw] of Object.entries(providers)) {
|
||||
if (targetProviders && !targetProviders.includes(name)) continue;
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
const block = raw as Record<string, unknown>;
|
||||
if (!("api_key" in block)) continue;
|
||||
|
||||
@@ -12,6 +12,12 @@ interface SdkApiErrorLike extends Error {
|
||||
responseBody: string;
|
||||
}
|
||||
|
||||
interface AgentDiagnosticLike {
|
||||
severity: string;
|
||||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function isSdkApiError(error: Error): error is SdkApiErrorLike {
|
||||
const candidate = error as Partial<SdkApiErrorLike>;
|
||||
return typeof candidate.statusCode === "number" && typeof candidate.responseBody === "string";
|
||||
@@ -45,6 +51,42 @@ function isSdkPollingTimeout(error: UserError): boolean {
|
||||
return /did not complete within the timeout/i.test(error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the final CLI error useful for hosts that retain stderr's terminal
|
||||
* BailianError but discard the diagnostics emitted before it. The complete
|
||||
* diagnostic collection remains on stdout/stderr; the terminal error carries
|
||||
* the first actionable reason and signals when more errors are available.
|
||||
*/
|
||||
export function formatAgentDiagnosticFailure(
|
||||
diagnostics: readonly AgentDiagnosticLike[],
|
||||
fallback: string,
|
||||
): string {
|
||||
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
||||
const firstError = errors[0];
|
||||
if (!firstError) return fallback;
|
||||
|
||||
const codePrefix = firstError.code?.trim() ? `[${firstError.code}] ` : "";
|
||||
const remaining = errors.length - 1;
|
||||
const remainingSuffix = remaining > 0 ? ` (+${remaining} more errors)` : "";
|
||||
return `${codePrefix}${firstError.message}${remainingSuffix}`;
|
||||
}
|
||||
|
||||
/** Add a recovery hint without replacing the original error or its metadata. */
|
||||
export function retainAgentError(error: unknown, hint: string, fallback: string): BailianError {
|
||||
if (error instanceof BailianError) {
|
||||
return new BailianError(error.message, error.exitCode, hint, {
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
cause: error.cause,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return new BailianError(error.message, ExitCode.GENERAL, hint, { cause: error });
|
||||
}
|
||||
const message = typeof error === "string" && error.trim() ? error : fallback;
|
||||
return new BailianError(message, ExitCode.GENERAL, hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an SDK-backed operation, translating SDK error types into BailianError so
|
||||
* bl's error handler produces the right exit code and hint formatting.
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type StateScope,
|
||||
} from "@openagentpack/sdk";
|
||||
|
||||
function createStateScope(configPath: string, projectName?: string): StateScope {
|
||||
export function createFileStateScope(configPath: string, projectName?: string): StateScope {
|
||||
const resolved = resolve(configPath);
|
||||
return { projectId: projectName ?? basename(dirname(resolved)) };
|
||||
}
|
||||
@@ -19,6 +19,6 @@ export async function loadFileState(
|
||||
): Promise<IStateManager> {
|
||||
const resolved = resolve(configPath);
|
||||
const backend = new LocalFileStateBackend({ configPath: resolved, statePath });
|
||||
const path = backend.getStatePath(createStateScope(resolved, projectName));
|
||||
const path = backend.getStatePath(createFileStateScope(resolved, projectName));
|
||||
return StateManager.load(path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { link, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
|
||||
export async function readInputFile(path: string): Promise<Uint8Array> {
|
||||
return new Uint8Array(await readFile(resolve(path)));
|
||||
}
|
||||
|
||||
export async function readJsonInput(argument: string): Promise<unknown> {
|
||||
const source = argument.startsWith("@")
|
||||
? await readFile(resolve(argument.slice(1)), "utf8")
|
||||
: argument;
|
||||
try {
|
||||
return JSON.parse(source) as unknown;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new BailianError(`Invalid JSON input: ${message}`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeOutputFile(
|
||||
outputPath: string,
|
||||
content: Uint8Array,
|
||||
force = false,
|
||||
): Promise<string> {
|
||||
const destination = resolve(outputPath);
|
||||
const parent = dirname(destination);
|
||||
await mkdir(parent, { recursive: true });
|
||||
const temporary = `${destination}.${randomUUID()}.tmp`;
|
||||
await writeFile(temporary, content, { flag: "wx" });
|
||||
try {
|
||||
if (force) {
|
||||
await rename(temporary, destination);
|
||||
} else {
|
||||
try {
|
||||
await link(temporary, destination);
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === "EEXIST") {
|
||||
throw new BailianError(
|
||||
`Output file already exists: ${destination}`,
|
||||
ExitCode.USAGE,
|
||||
"Choose another --output-file path or re-run with --force.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await unlink(temporary);
|
||||
}
|
||||
return destination;
|
||||
} catch (error) {
|
||||
await unlink(temporary).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMimeType(path: string): string {
|
||||
const filename = basename(path).toLocaleLowerCase();
|
||||
if (filename.endsWith(".json")) return "application/json";
|
||||
if (filename.endsWith(".md")) return "text/markdown";
|
||||
if (filename.endsWith(".txt")) return "text/plain";
|
||||
if (filename.endsWith(".pdf")) return "application/pdf";
|
||||
if (filename.endsWith(".png")) return "image/png";
|
||||
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (filename.endsWith(".zip")) return "application/zip";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
@@ -8,8 +8,9 @@ export interface PagedResult<T> {
|
||||
export async function fetchAllPages<T>(
|
||||
fetchPage: (page?: string) => Promise<PagedResult<T>>,
|
||||
all?: boolean,
|
||||
initialPage?: string,
|
||||
): Promise<PagedResult<T>> {
|
||||
const first = await fetchPage();
|
||||
const first = await fetchPage(initialPage);
|
||||
const items = [...first.items];
|
||||
let hasMore = first.hasMore;
|
||||
let nextPage = first.nextPage;
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const PLAYGROUND_PACKAGE = "@openagentpack/playground";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DEFAULT_PORT = 4848;
|
||||
const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i;
|
||||
|
||||
export interface PlaygroundLaunchOptions {
|
||||
port?: number;
|
||||
open: boolean;
|
||||
file?: string;
|
||||
project?: string;
|
||||
agent?: string;
|
||||
surface: "preview" | "workbench";
|
||||
client: Client;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface Launcher {
|
||||
command: string;
|
||||
args: string[];
|
||||
version?: string;
|
||||
fetched: boolean;
|
||||
}
|
||||
|
||||
interface ExistingPlayground {
|
||||
version: string;
|
||||
pid: number;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface PlaygroundProjectSummary {
|
||||
status?: string;
|
||||
agents?: Array<{ agent?: { id?: string } }>;
|
||||
}
|
||||
|
||||
export interface PlaygroundBrowserTarget {
|
||||
url: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
export async function launchManagedAgentPlayground(
|
||||
options: PlaygroundLaunchOptions,
|
||||
): Promise<void> {
|
||||
assertSupportedNodeVersion();
|
||||
const port = options.port ?? DEFAULT_PORT;
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
||||
throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE);
|
||||
}
|
||||
const sourcePath = resolve(
|
||||
options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"),
|
||||
);
|
||||
const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
||||
const launcher = await resolveLauncher();
|
||||
const existing = await probeExistingPlayground(port);
|
||||
if (existing) {
|
||||
const reusable =
|
||||
existing.projectId === projectId &&
|
||||
(launcher.version === undefined || existing.version === launcher.version);
|
||||
if (reusable) {
|
||||
emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`);
|
||||
await openPlaygroundSurface(port, options.surface, options.agent, options.open);
|
||||
return;
|
||||
}
|
||||
const released = await replaceExistingPlayground(existing, port);
|
||||
if (!released) {
|
||||
throw new BailianError(
|
||||
`Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
"Stop it manually or choose another --port.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const environment = buildPlaygroundEnvironment(options, port, sourcePath);
|
||||
if (launcher.fetched) {
|
||||
emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`);
|
||||
}
|
||||
const child = spawn(launcher.command, launcher.args, {
|
||||
env: environment,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
const removeSignalForwarding = forwardSignals(child);
|
||||
try {
|
||||
const readyPort = await waitForPlaygroundReady(child, port, 30_000, projectId);
|
||||
if (readyPort === null) {
|
||||
throw new BailianError(
|
||||
`Workbench did not become ready in time. Check the logs above, then open http://localhost:${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
emitBare(`Workbench ready at http://localhost:${readyPort}`);
|
||||
await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open);
|
||||
const exitCode = await waitForChildExit(child);
|
||||
if (exitCode !== 0) {
|
||||
throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL);
|
||||
}
|
||||
} finally {
|
||||
removeSignalForwarding();
|
||||
}
|
||||
}
|
||||
|
||||
export function playgroundBrowserTargetFromSummary(
|
||||
baseUrl: string,
|
||||
summary: PlaygroundProjectSummary,
|
||||
requestedAgent?: string,
|
||||
): PlaygroundBrowserTarget {
|
||||
if (summary.status !== "valid") return { url: baseUrl };
|
||||
const agentIds = (summary.agents ?? [])
|
||||
.map((entry) => entry.agent?.id?.trim())
|
||||
.filter((agentId): agentId is string => Boolean(agentId));
|
||||
const requested = requestedAgent?.trim();
|
||||
if (requested) {
|
||||
if (agentIds.includes(requested)) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(requested)}/preview` };
|
||||
}
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning: `Agent '${requested}' was not found. Opening the project Workbench instead.`,
|
||||
};
|
||||
}
|
||||
if (agentIds.length === 1) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(agentIds[0]!)}/preview` };
|
||||
}
|
||||
if (agentIds.length > 1) {
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning:
|
||||
"This project declares multiple Agents. Opening the Workbench; rerun with --agent <id> for Preview.",
|
||||
};
|
||||
}
|
||||
return { url: baseUrl };
|
||||
}
|
||||
|
||||
function assertSupportedNodeVersion(): void {
|
||||
const majorVersion = Number(process.versions.node.split(".")[0]);
|
||||
if (Number.isFinite(majorVersion) && majorVersion >= 22) return;
|
||||
throw new BailianError(
|
||||
"Managed Agent Workbench requires Node.js 22 or later.",
|
||||
ExitCode.USAGE,
|
||||
"Upgrade Node.js for Workbench; other Bailian CLI commands continue to support Node.js 18.17+.",
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveLauncher(): Promise<Launcher> {
|
||||
const explicit =
|
||||
process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() ||
|
||||
process.env.AGENTS_PLAYGROUND_BIN?.trim();
|
||||
if (explicit) {
|
||||
if (!existsSync(explicit)) {
|
||||
throw new BailianError(
|
||||
`Configured Workbench binary does not exist: ${explicit}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return { command: process.execPath, args: [explicit], fetched: false };
|
||||
}
|
||||
|
||||
const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest";
|
||||
const { stdout } = await execFileAsync(
|
||||
"npm",
|
||||
[
|
||||
"view",
|
||||
`${PLAYGROUND_PACKAGE}@${requestedVersion}`,
|
||||
"version",
|
||||
"--json",
|
||||
"--prefer-online",
|
||||
"--fetch-retries=0",
|
||||
"--fetch-timeout=10000",
|
||||
],
|
||||
{ timeout: 15_000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
let resolvedVersion: unknown;
|
||||
try {
|
||||
resolvedVersion = JSON.parse(stdout);
|
||||
} catch {
|
||||
resolvedVersion = undefined;
|
||||
}
|
||||
if (
|
||||
typeof resolvedVersion !== "string" ||
|
||||
!/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(resolvedVersion)
|
||||
) {
|
||||
throw new BailianError(
|
||||
"npm did not return a single valid Playground version. Specify an exact version or dist-tag. / npm 未返回唯一有效的 Playground 版本号。请指定精确版本或 dist-tag。",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const installed = resolveInstalledPlayground();
|
||||
if (installed?.version === resolvedVersion) return installed;
|
||||
|
||||
const monorepoBinary = findLocalPlaygroundBin(process.cwd());
|
||||
if (
|
||||
monorepoBinary &&
|
||||
readPlaygroundVersion(resolve(dirname(monorepoBinary), "../../package.json")) ===
|
||||
resolvedVersion
|
||||
) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [monorepoBinary],
|
||||
version: resolvedVersion,
|
||||
fetched: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npx",
|
||||
args: ["-y", `${PLAYGROUND_PACKAGE}@${resolvedVersion}`],
|
||||
version: resolvedVersion,
|
||||
fetched: true,
|
||||
};
|
||||
}
|
||||
|
||||
function readPlaygroundVersion(manifestPath: string): string | undefined {
|
||||
try {
|
||||
return (JSON.parse(readFileSync(manifestPath, "utf8")) as { version?: string }).version;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstalledPlayground(): Launcher | undefined {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJsonPath = require.resolve(`${PLAYGROUND_PACKAGE}/package.json`);
|
||||
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
version?: string;
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
const relativeBinary =
|
||||
typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["agents-playground"];
|
||||
if (!relativeBinary) return undefined;
|
||||
const binaryPath = resolve(dirname(packageJsonPath), relativeBinary);
|
||||
if (!existsSync(binaryPath)) return undefined;
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [binaryPath],
|
||||
version: manifest.version,
|
||||
fetched: false,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function findLocalPlaygroundBin(startDirectory: string): string | undefined {
|
||||
let directory = startDirectory;
|
||||
for (let depth = 0; depth < 10; depth += 1) {
|
||||
const candidate = resolve(directory, "packages/playground/dist/bin/playground.js");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = dirname(directory);
|
||||
if (parent === directory) break;
|
||||
directory = parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildPlaygroundEnvironment(
|
||||
options: PlaygroundLaunchOptions,
|
||||
port: number,
|
||||
sourcePath: string,
|
||||
): NodeJS.ProcessEnv {
|
||||
const credential = options.client.exportApiCredential();
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"),
|
||||
};
|
||||
if (options.surface === "workbench") {
|
||||
delete environment.AGENTS_CONFIG_PATH;
|
||||
environment.AGENTS_PROJECT_ROOT = sourcePath;
|
||||
} else {
|
||||
delete environment.AGENTS_PROJECT_ROOT;
|
||||
environment.AGENTS_CONFIG_PATH = sourcePath;
|
||||
}
|
||||
if (credential) environment.DASHSCOPE_API_KEY = credential.token;
|
||||
const baseUrl = options.client.baseUrl.replace(/\/+$/, "");
|
||||
environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio")
|
||||
? baseUrl
|
||||
: `${baseUrl}/api/v1/agentstudio`;
|
||||
if (options.settings.workspaceId) {
|
||||
environment.BAILIAN_WORKSPACE_ID = options.settings.workspaceId;
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
async function waitForPlaygroundReady(
|
||||
child: ChildProcess,
|
||||
fallbackPort: number,
|
||||
timeoutMs: number,
|
||||
expectedProjectId: string,
|
||||
): Promise<number | null> {
|
||||
let port = fallbackPort;
|
||||
let outputBuffer = "";
|
||||
child.stdout?.on("data", (chunk: Buffer | string) => {
|
||||
process.stdout.write(chunk);
|
||||
outputBuffer += chunk.toString();
|
||||
const match = outputBuffer.match(PLAYGROUND_URL_PATTERN);
|
||||
if (match?.[1]) port = Number(match[1]);
|
||||
});
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() <= deadline) {
|
||||
if (child.exitCode !== null) return null;
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(1_000),
|
||||
});
|
||||
const body = response.ok
|
||||
? ((await response.json()) as { playground?: { project_id?: string } })
|
||||
: undefined;
|
||||
if (body?.playground?.project_id === expectedProjectId) return port;
|
||||
} catch {
|
||||
// Not ready yet.
|
||||
}
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 300));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function probeExistingPlayground(port: number): Promise<ExistingPlayground | null> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const body = (await response.json()) as {
|
||||
playground?: { version?: string; pid?: number; project_id?: string };
|
||||
};
|
||||
if (!body.playground?.pid) return null;
|
||||
return {
|
||||
version: body.playground.version ?? "unknown",
|
||||
pid: body.playground.pid,
|
||||
projectId: body.playground.project_id,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceExistingPlayground(
|
||||
existing: ExistingPlayground,
|
||||
port: number,
|
||||
): Promise<boolean> {
|
||||
emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`);
|
||||
try {
|
||||
process.kill(existing.pid, "SIGTERM");
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 100));
|
||||
if (!(await probeExistingPlayground(port))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openPlaygroundSurface(
|
||||
port: number,
|
||||
surface: "preview" | "workbench",
|
||||
requestedAgent: string | undefined,
|
||||
shouldOpen: boolean,
|
||||
): Promise<void> {
|
||||
if (!shouldOpen) return;
|
||||
const target =
|
||||
surface === "workbench"
|
||||
? { url: `http://localhost:${port}` }
|
||||
: await resolvePlaygroundBrowserTarget(port, requestedAgent);
|
||||
if (target.warning) emitBare(`Warning: ${target.warning}`);
|
||||
openBrowser(target.url);
|
||||
}
|
||||
|
||||
async function resolvePlaygroundBrowserTarget(
|
||||
port: number,
|
||||
requestedAgent?: string,
|
||||
): Promise<PlaygroundBrowserTarget> {
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/project`, {
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
});
|
||||
if (!response.ok) return { url: baseUrl };
|
||||
return playgroundBrowserTargetFromSummary(
|
||||
baseUrl,
|
||||
(await response.json()) as PlaygroundProjectSummary,
|
||||
requestedAgent,
|
||||
);
|
||||
} catch {
|
||||
return { url: baseUrl };
|
||||
}
|
||||
}
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
const command =
|
||||
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
||||
const args = process.platform === "win32" ? ["", url] : [url];
|
||||
try {
|
||||
spawn(command, args, {
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
shell: process.platform === "win32",
|
||||
}).unref();
|
||||
} catch {
|
||||
emitBare(`Could not open a browser automatically. Visit ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function forwardSignals(child: ChildProcess): () => void {
|
||||
const forwardInterrupt = () => child.kill("SIGINT");
|
||||
const forwardTerminate = () => child.kill("SIGTERM");
|
||||
process.on("SIGINT", forwardInterrupt);
|
||||
process.on("SIGTERM", forwardTerminate);
|
||||
return () => {
|
||||
process.off("SIGINT", forwardInterrupt);
|
||||
process.off("SIGTERM", forwardTerminate);
|
||||
};
|
||||
}
|
||||
|
||||
function waitForChildExit(child: ChildProcess): Promise<number> {
|
||||
if (child.exitCode !== null) return Promise.resolve(child.exitCode);
|
||||
return new Promise((resolveExit, rejectExit) => {
|
||||
child.once("exit", (exitCode) => resolveExit(exitCode ?? 0));
|
||||
child.once("error", rejectExit);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import {
|
||||
type BackendRuntimeInput,
|
||||
type IStateManager,
|
||||
LocalFileStateBackend,
|
||||
planProjectWithStateBackend,
|
||||
type ResolvedProjectConfig,
|
||||
type ResourceAddress,
|
||||
type ResourceSyncRun,
|
||||
resolveProjectConfigFromObject,
|
||||
syncProjectResourcesWithStateBackend,
|
||||
} from "@openagentpack/sdk";
|
||||
import { BailianError, type CommandContext, detectOutputFormat, ExitCode } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseDocument } from "yaml";
|
||||
import { formatResourceLabel } from "./address-utils.ts";
|
||||
import { resolveAgentProjectConfig } from "./config-loader.ts";
|
||||
import { assertProviderCredentials } from "./credentials.ts";
|
||||
import { withStdoutProtected } from "./console-capture.ts";
|
||||
import { retainAgentError, withAgentErrors } from "./errors.ts";
|
||||
import { createFileStateScope } from "./file-state-manager.ts";
|
||||
import { installSdkTransport } from "./transport.ts";
|
||||
|
||||
export type ScopedCreateHost = Pick<CommandContext, "client" | "identity" | "settings">;
|
||||
export type ScopedCreateGroup = "agents" | "environments" | "skills" | "vaults" | "deployments";
|
||||
|
||||
export interface LoadedScopedCreateProject {
|
||||
configPath: string;
|
||||
projectName: string;
|
||||
config: ResolvedProjectConfig;
|
||||
source: string;
|
||||
provider: string;
|
||||
stateBackend: LocalFileStateBackend;
|
||||
stateScope: ReturnType<typeof createFileStateScope>;
|
||||
}
|
||||
|
||||
export interface ResourceKeySelection {
|
||||
key: string;
|
||||
reusedPending: boolean;
|
||||
}
|
||||
|
||||
export const SCOPED_CREATE_NOTE = [
|
||||
{
|
||||
"en-US":
|
||||
"Without --yes, this command only previews. --dry-run is fully offline. The scoped flow checks only the target resource and its transitive dependencies; unrelated resources are not refreshed or drift-checked.",
|
||||
"zh-CN":
|
||||
"不带 --yes 时仅预览;--dry-run 完全离线。定向流程只检查目标资源及其传递依赖,不刷新或检测无关资源的 Drift。",
|
||||
},
|
||||
];
|
||||
|
||||
export interface ScopedTopLevelCreateInput {
|
||||
host: ScopedCreateHost;
|
||||
project: LoadedScopedCreateProject;
|
||||
group: Exclude<ScopedCreateGroup, "agents">;
|
||||
resourceType: "environment" | "skill" | "vault" | "deployment";
|
||||
displayName: string;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
resolvedDeclaration: Record<string, unknown>;
|
||||
existingDeclarations: Record<string, Record<string, unknown>>;
|
||||
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
|
||||
fallbackKey: string;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
const normalize = (candidate: unknown): unknown => {
|
||||
if (Array.isArray(candidate)) return candidate.map(normalize);
|
||||
if (!candidate || typeof candidate !== "object") return candidate;
|
||||
return Object.fromEntries(
|
||||
Object.entries(candidate as Record<string, unknown>)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
||||
.map(([key, entry]) => [key, normalize(entry)]),
|
||||
);
|
||||
};
|
||||
return JSON.stringify(normalize(value));
|
||||
}
|
||||
|
||||
export function normalizeResourceKey(displayName: string, fallback = "resource"): string {
|
||||
const normalized = displayName
|
||||
.normalize("NFKC")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
export function selectResourceKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
resourceTypes: ResourceAddress["type"][];
|
||||
declarations: Record<string, Record<string, unknown>>;
|
||||
candidate: Record<string, unknown>;
|
||||
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
|
||||
fallbackKey?: string;
|
||||
state: IStateManager;
|
||||
}): ResourceKeySelection {
|
||||
const tracked = new Set(
|
||||
options.state
|
||||
.listResources()
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.address.provider === options.provider &&
|
||||
options.resourceTypes.includes(resource.address.type),
|
||||
)
|
||||
.map((resource) => resource.address.name),
|
||||
);
|
||||
const candidateJson = canonicalJson(options.candidate);
|
||||
for (const [key, declaration] of Object.entries(options.declarations)) {
|
||||
if (options.effectiveName(key, declaration) !== options.displayName || tracked.has(key))
|
||||
continue;
|
||||
if (canonicalJson(declaration) === candidateJson) return { key, reusedPending: true };
|
||||
}
|
||||
|
||||
const baseKey = normalizeResourceKey(options.displayName, options.fallbackKey);
|
||||
if (!(baseKey in options.declarations)) return { key: baseKey, reusedPending: false };
|
||||
let suffix = 2;
|
||||
while (`${baseKey}-${suffix}` in options.declarations) suffix += 1;
|
||||
return { key: `${baseKey}-${suffix}`, reusedPending: false };
|
||||
}
|
||||
|
||||
export async function loadScopedCreateProject(
|
||||
host: ScopedCreateHost,
|
||||
file: string,
|
||||
): Promise<LoadedScopedCreateProject> {
|
||||
const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
throw new BailianError(
|
||||
`Config file not found: ${file}`,
|
||||
ExitCode.USAGE,
|
||||
"Run `bl managed-agent init` first.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
const loaded = await withAgentErrors(() =>
|
||||
resolveAgentProjectConfig(host, file, { credentials: "none" }),
|
||||
);
|
||||
const source = await readFile(loaded.configPath, "utf8");
|
||||
if (source !== sourceBeforeLoad) {
|
||||
throw new BailianError(
|
||||
`${file} changed while it was being loaded.`,
|
||||
ExitCode.GENERAL,
|
||||
"Re-run the command against the latest file.",
|
||||
);
|
||||
}
|
||||
const provider = "bailian";
|
||||
if (!host.settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]);
|
||||
installSdkTransport(host);
|
||||
const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath });
|
||||
return {
|
||||
...loaded,
|
||||
source,
|
||||
provider,
|
||||
stateBackend,
|
||||
stateScope: createFileStateScope(loaded.configPath, loaded.projectName),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveCandidateDeclaration(options: {
|
||||
project: LoadedScopedCreateProject;
|
||||
group: ScopedCreateGroup;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const temporaryKey = "__bailian_cli_create_candidate__";
|
||||
const rawConfig = structuredClone(options.project.config) as unknown as Record<string, unknown>;
|
||||
const declarations = (rawConfig[options.group] ?? {}) as Record<string, unknown>;
|
||||
rawConfig[options.group] = { ...declarations, [temporaryKey]: options.rawDeclaration };
|
||||
const resolved = await withAgentErrors(() =>
|
||||
resolveProjectConfigFromObject(rawConfig, {
|
||||
projectName: options.project.projectName,
|
||||
basePath: dirname(options.project.configPath),
|
||||
}),
|
||||
);
|
||||
const resolvedGroup = resolved.config[options.group] as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined;
|
||||
return resolvedGroup?.[temporaryKey] ?? options.rawDeclaration;
|
||||
}
|
||||
|
||||
export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): Promise<void> {
|
||||
const { project, host } = input;
|
||||
const format = detectOutputFormat(host.settings.output);
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectResourceKey({
|
||||
displayName: input.displayName,
|
||||
provider: project.provider,
|
||||
resourceTypes: [input.resourceType],
|
||||
declarations: input.existingDeclarations,
|
||||
candidate: input.resolvedDeclaration,
|
||||
effectiveName: input.effectiveName,
|
||||
fallbackKey: input.fallbackKey,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const resourceKey = keySelection.key;
|
||||
const candidateConfig = structuredClone(project.config) as unknown as Record<string, unknown>;
|
||||
const existingGroup = (candidateConfig[input.group] ?? {}) as Record<string, unknown>;
|
||||
candidateConfig[input.group] = { ...existingGroup, [resourceKey]: input.resolvedDeclaration };
|
||||
candidateConfig._resolved = true;
|
||||
|
||||
const document = parseDocument(project.source);
|
||||
if (document.errors.length > 0) {
|
||||
throw new BailianError(
|
||||
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
document.setIn([input.group, resourceKey], input.rawDeclaration);
|
||||
const nextSource = document.toString();
|
||||
const root: ResourceAddress = {
|
||||
type: input.resourceType,
|
||||
name: resourceKey,
|
||||
provider: project.provider,
|
||||
};
|
||||
const backendInput: BackendRuntimeInput = {
|
||||
projectName: project.projectName,
|
||||
config: candidateConfig as unknown as ResolvedProjectConfig,
|
||||
configPath: project.configPath,
|
||||
providers: { [project.provider]: project.config.providers[project.provider] },
|
||||
stateBackend: project.stateBackend,
|
||||
stateScope: project.stateScope,
|
||||
};
|
||||
|
||||
if (host.settings.dryRun || !input.yes) {
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectWithStateBackend(backendInput, {
|
||||
provider: project.provider,
|
||||
scope: { roots: [root] },
|
||||
mode: "create-only",
|
||||
refresh: !host.settings.dryRun,
|
||||
quiet: format === "json",
|
||||
}),
|
||||
),
|
||||
);
|
||||
const readyToCreate = !planned.plan.diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
const result = {
|
||||
resource: {
|
||||
type: input.resourceType,
|
||||
key: resourceKey,
|
||||
name: input.displayName,
|
||||
provider: project.provider,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: false,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
requires_confirmation: !host.settings.dryRun,
|
||||
ready_to_create: readyToCreate,
|
||||
actions: planned.plan.actions,
|
||||
diagnostics: planned.plan.diagnostics,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Generated YAML key: ${resourceKey}`);
|
||||
renderPlan(planned.plan.actions, planned.plan.diagnostics);
|
||||
emitBare(
|
||||
host.settings.dryRun
|
||||
? "Dry run: YAML, State, and remote resources were not changed."
|
||||
: "Preview only: re-run with --yes to write YAML and create this resource.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceConfigAtomically(project.configPath, project.source, nextSource);
|
||||
let run: ResourceSyncRun;
|
||||
try {
|
||||
run = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
syncProjectResourcesWithStateBackend(backendInput, {
|
||||
provider: project.provider,
|
||||
scope: { roots: [root] },
|
||||
mode: "create-only",
|
||||
refresh: true,
|
||||
quiet: format === "json",
|
||||
policy: "block",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw retainAgentError(
|
||||
error,
|
||||
"The YAML declaration was kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
`Scoped ${input.resourceType} create failed.`,
|
||||
);
|
||||
}
|
||||
const remoteId = await project.stateBackend.read(
|
||||
project.stateScope,
|
||||
(state) => state.getResource(root)?.remote_id,
|
||||
);
|
||||
const results = run.execution?.results ?? [];
|
||||
const failed = results.find((result) => result.status === "failed");
|
||||
const result = {
|
||||
resource: {
|
||||
type: input.resourceType,
|
||||
key: resourceKey,
|
||||
name: input.displayName,
|
||||
provider: project.provider,
|
||||
remote_id: remoteId,
|
||||
},
|
||||
config_file: project.configPath,
|
||||
yaml_written: true,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
actions: run.planned.plan.actions,
|
||||
diagnostics: run.planned.plan.diagnostics,
|
||||
results,
|
||||
error: failed?.error,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(`Wrote ${project.configPath} with ${input.resourceType} key '${resourceKey}'.`);
|
||||
if (!failed) emitBare(`Created ${input.resourceType} '${input.displayName}'.`);
|
||||
}
|
||||
if (failed) {
|
||||
throw new BailianError(
|
||||
failed.error ?? `Scoped ${input.resourceType} create failed.`,
|
||||
ExitCode.GENERAL,
|
||||
"The YAML declaration was kept. Fix the provider error, then re-run the same create command.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function replaceConfigAtomically(
|
||||
configPath: string,
|
||||
expectedSource: string,
|
||||
nextSource: string,
|
||||
): Promise<void> {
|
||||
const destination = resolve(configPath);
|
||||
const currentSource = await readFile(destination, "utf8");
|
||||
if (currentSource !== expectedSource) {
|
||||
throw new BailianError(
|
||||
`${configPath} changed while resource create was being prepared.`,
|
||||
ExitCode.GENERAL,
|
||||
"Review the latest YAML and re-run the command; no file was overwritten.",
|
||||
);
|
||||
}
|
||||
const currentStat = await stat(destination);
|
||||
const temporary = resolve(dirname(destination), `.${basename(destination)}.${randomUUID()}.tmp`);
|
||||
await writeFile(temporary, nextSource, { flag: "wx", mode: currentStat.mode });
|
||||
try {
|
||||
await rename(temporary, destination);
|
||||
} catch (error) {
|
||||
await unlink(temporary).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMetadata(values: string[] | undefined): Record<string, string> | undefined {
|
||||
if (!values?.length) return undefined;
|
||||
const metadata: Record<string, string> = {};
|
||||
for (const entry of values) {
|
||||
const separator = entry.indexOf("=");
|
||||
if (separator <= 0) {
|
||||
throw new BailianError(
|
||||
`Invalid metadata '${entry}'.`,
|
||||
ExitCode.USAGE,
|
||||
"Use --metadata key=value.",
|
||||
);
|
||||
}
|
||||
const key = entry.slice(0, separator).trim();
|
||||
if (!key) throw new BailianError("Metadata key must not be empty.", ExitCode.USAGE);
|
||||
metadata[key] = entry.slice(separator + 1);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function parseJsonInputs(
|
||||
values: string[] | undefined,
|
||||
label: string,
|
||||
): Promise<unknown[]> {
|
||||
const parsed: unknown[] = [];
|
||||
for (const value of values ?? []) {
|
||||
const raw = value.startsWith("@") ? await readFile(resolve(value.slice(1)), "utf8") : value;
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new BailianError(
|
||||
`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(decoded)) parsed.push(...decoded);
|
||||
else parsed.push(decoded);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderPlan(
|
||||
actions: Array<{ action: string; address: ResourceAddress }>,
|
||||
diagnostics: Array<{ severity: string; code: string; message: string }>,
|
||||
): void {
|
||||
for (const diagnostic of diagnostics) {
|
||||
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
for (const action of actions.filter((entry) => entry.action !== "no-op")) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { CloudAgent } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const AGENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const AGENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const AGENT_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Agent ID", "zh-CN": "Agent ID" },
|
||||
},
|
||||
agentVersion: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Specific agent version", "zh-CN": "指定 Agent 版本" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const AGENT_VERSIONS_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
agentId: AGENT_GET_FLAGS.agentId,
|
||||
};
|
||||
|
||||
export function agentRows(agents: CloudAgent[]): string[][] {
|
||||
return agents.map((agent) => [
|
||||
agent.id,
|
||||
displayValue(agent.name),
|
||||
displayValue(agent.version),
|
||||
displayValue(agent.type),
|
||||
displayValue(agent.updated_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { dirname, extname, relative, sep } from "node:path";
|
||||
import {
|
||||
type BackendRuntimeInput,
|
||||
buildAgentDecl,
|
||||
type IStateManager,
|
||||
inspectSkillSource,
|
||||
planProjectWithStateBackend,
|
||||
type ResolvedProjectConfig,
|
||||
type ResourceAddress,
|
||||
type ResourceSyncRun,
|
||||
type SkillDecl,
|
||||
syncProjectResourcesWithStateBackend,
|
||||
} from "@openagentpack/sdk";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { parseDocument } from "yaml";
|
||||
import { formatResourceLabel } from "../_engine/address-utils.ts";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { retainAgentError, withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
type LoadedScopedCreateProject,
|
||||
loadScopedCreateProject,
|
||||
normalizeResourceKey,
|
||||
replaceConfigAtomically,
|
||||
resolveCandidateDeclaration,
|
||||
selectResourceKey,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
export { replaceConfigAtomically } from "../_engine/scoped-create.ts";
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Agent display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Agent 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
required: true,
|
||||
description: { "en-US": "Model ID", "zh-CN": "模型 ID" },
|
||||
},
|
||||
instructions: {
|
||||
type: "string",
|
||||
valueHint: "<text|path>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Inline instructions or a ./, ../, or absolute file path",
|
||||
"zh-CN": "内联指令,或以 ./、../、/ 开头的文件路径",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Agent description", "zh-CN": "Agent 描述" },
|
||||
},
|
||||
skill: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Existing remote Skill ID (repeatable)",
|
||||
"zh-CN": "已存在的远端 Skill ID(可重复)",
|
||||
},
|
||||
},
|
||||
skillDir: {
|
||||
type: "array",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US":
|
||||
"Local Skill directory or ZIP to declare, upload, and attach through the same scoped create (repeatable)",
|
||||
"zh-CN": "通过同一次定向创建声明、上传并绑定的本地 Skill 目录或 ZIP(可重复)",
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
valueHint: "<custom|official>",
|
||||
choices: ["custom", "official"],
|
||||
description: {
|
||||
"en-US": "Type applied to every --skill value (default: custom)",
|
||||
"zh-CN": "应用于所有 --skill 的类型(默认:custom)",
|
||||
},
|
||||
},
|
||||
tool: {
|
||||
type: "array",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Builtin tool name (repeatable)",
|
||||
"zh-CN": "内置工具名称(可重复)",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type BuiltAgentDecl = ReturnType<typeof buildAgentDecl>["agent"];
|
||||
type AgentSkillType = "custom" | "official";
|
||||
type AgentSkillDecl = NonNullable<BuiltAgentDecl["skills"]>[number];
|
||||
|
||||
interface AgentKeySelection {
|
||||
key: string;
|
||||
reusedPending: boolean;
|
||||
}
|
||||
|
||||
interface LocalSkillKeySelection {
|
||||
key: string;
|
||||
needsCreate: boolean;
|
||||
reusedPending: boolean;
|
||||
reusedTracked: boolean;
|
||||
}
|
||||
|
||||
interface PreparedLocalSkill extends LocalSkillKeySelection {
|
||||
name: string;
|
||||
source: string;
|
||||
rawDeclaration: Record<string, unknown>;
|
||||
resolvedDeclaration: SkillDecl;
|
||||
}
|
||||
|
||||
export function normalizeAgentKey(displayName: string): string {
|
||||
return normalizeResourceKey(displayName, "agent");
|
||||
}
|
||||
|
||||
export function selectAgentKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
agents: Record<string, BuiltAgentDecl>;
|
||||
candidate: BuiltAgentDecl;
|
||||
state: IStateManager;
|
||||
}): AgentKeySelection {
|
||||
return selectResourceKey({
|
||||
displayName: options.displayName,
|
||||
provider: options.provider,
|
||||
resourceTypes: ["agent", "template"],
|
||||
declarations: options.agents as unknown as Record<string, Record<string, unknown>>,
|
||||
candidate: options.candidate as unknown as Record<string, unknown>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "agent",
|
||||
state: options.state,
|
||||
});
|
||||
}
|
||||
|
||||
function agentSkillType(value: string | undefined): AgentSkillType {
|
||||
if (value === undefined || value === "custom") return "custom";
|
||||
if (value === "official") return "official";
|
||||
throw new BailianError("--type must be either custom or official.", ExitCode.USAGE);
|
||||
}
|
||||
|
||||
export function buildAgentSkillRefs(
|
||||
skillIds: string[] | undefined,
|
||||
type: AgentSkillType = "custom",
|
||||
): NonNullable<BuiltAgentDecl["skills"]> {
|
||||
const seen = new Set<string>();
|
||||
const refs: NonNullable<BuiltAgentDecl["skills"]> = [];
|
||||
for (const rawId of skillIds ?? []) {
|
||||
const skillId = rawId.trim();
|
||||
if (!skillId) {
|
||||
throw new BailianError("--skill values must not be empty.", ExitCode.USAGE);
|
||||
}
|
||||
if (seen.has(skillId)) continue;
|
||||
seen.add(skillId);
|
||||
refs.push({ type, skill_id: skillId });
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
const normalize = (candidate: unknown): unknown => {
|
||||
if (Array.isArray(candidate)) return candidate.map(normalize);
|
||||
if (!candidate || typeof candidate !== "object") return candidate;
|
||||
return Object.fromEntries(
|
||||
Object.entries(candidate as Record<string, unknown>)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
||||
.map(([key, entry]) => [key, normalize(entry)]),
|
||||
);
|
||||
};
|
||||
return JSON.stringify(normalize(value));
|
||||
}
|
||||
|
||||
export function selectLocalSkillKey(options: {
|
||||
displayName: string;
|
||||
provider: string;
|
||||
declarations: Record<string, Record<string, unknown>>;
|
||||
candidate: Record<string, unknown>;
|
||||
state: IStateManager;
|
||||
}): LocalSkillKeySelection {
|
||||
const trackedKeys = new Set(
|
||||
options.state
|
||||
.listResources()
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.address.provider === options.provider && resource.address.type === "skill",
|
||||
)
|
||||
.map((resource) => resource.address.name),
|
||||
);
|
||||
const sameName = Object.entries(options.declarations).filter(([key, declaration]) => {
|
||||
const effectiveName = typeof declaration.name === "string" ? declaration.name : key;
|
||||
return effectiveName === options.displayName;
|
||||
});
|
||||
const matchingTracked = sameName.find(
|
||||
([key, declaration]) =>
|
||||
trackedKeys.has(key) && canonicalJson(declaration) === canonicalJson(options.candidate),
|
||||
);
|
||||
if (matchingTracked) {
|
||||
return {
|
||||
key: matchingTracked[0],
|
||||
needsCreate: false,
|
||||
reusedPending: false,
|
||||
reusedTracked: true,
|
||||
};
|
||||
}
|
||||
const differentTracked = sameName.find(([key]) => trackedKeys.has(key));
|
||||
if (differentTracked) {
|
||||
throw new BailianError(
|
||||
`Skill '${options.displayName}' is already tracked with a different declaration.`,
|
||||
ExitCode.USAGE,
|
||||
"Modify its existing YAML declaration and run full `bl managed-agent apply`, or bind its remote ID with --skill.",
|
||||
);
|
||||
}
|
||||
const selected = selectResourceKey({
|
||||
displayName: options.displayName,
|
||||
provider: options.provider,
|
||||
resourceTypes: ["skill"],
|
||||
declarations: options.declarations,
|
||||
candidate: options.candidate,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "skill",
|
||||
state: options.state,
|
||||
});
|
||||
return {
|
||||
...selected,
|
||||
needsCreate: true,
|
||||
reusedTracked: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareLocalSkillSources(
|
||||
project: LoadedScopedCreateProject,
|
||||
sources: string[] | undefined,
|
||||
): Promise<PreparedLocalSkill[]> {
|
||||
const declarations = {
|
||||
...((project.config.skills ?? {}) as unknown as Record<string, Record<string, unknown>>),
|
||||
};
|
||||
const preparedByPath = new Map<string, PreparedLocalSkill>();
|
||||
const preparedByKey = new Map<string, PreparedLocalSkill>();
|
||||
for (const rawSource of sources ?? []) {
|
||||
const requestedSource = rawSource.trim();
|
||||
if (!requestedSource) {
|
||||
throw new BailianError("--skill-dir values must not be empty.", ExitCode.USAGE);
|
||||
}
|
||||
const inspected = await withAgentErrors(() =>
|
||||
inspectSkillSource(requestedSource, { basePath: process.cwd() }),
|
||||
);
|
||||
const sourceStat = await stat(inspected.sourcePath);
|
||||
const supportedSource =
|
||||
sourceStat.isDirectory() ||
|
||||
(sourceStat.isFile() && extname(inspected.sourcePath).toLowerCase() === ".zip");
|
||||
if (!supportedSource) {
|
||||
throw new BailianError(
|
||||
`--skill-dir requires a Skill directory or .zip file: ${requestedSource}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (preparedByPath.has(inspected.sourcePath)) continue;
|
||||
const source =
|
||||
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
|
||||
const rawDeclaration: Record<string, unknown> = {
|
||||
name: inspected.name,
|
||||
source,
|
||||
origin: "custom",
|
||||
provider: project.provider,
|
||||
};
|
||||
const resolvedDeclaration = (await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "skills",
|
||||
rawDeclaration,
|
||||
})) as unknown as SkillDecl;
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectLocalSkillKey({
|
||||
displayName: inspected.name,
|
||||
provider: project.provider,
|
||||
declarations,
|
||||
candidate: resolvedDeclaration as unknown as Record<string, unknown>,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const existingPrepared = preparedByKey.get(keySelection.key);
|
||||
if (existingPrepared) {
|
||||
preparedByPath.set(inspected.sourcePath, existingPrepared);
|
||||
continue;
|
||||
}
|
||||
const prepared: PreparedLocalSkill = {
|
||||
...keySelection,
|
||||
name: inspected.name,
|
||||
source,
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
};
|
||||
declarations[keySelection.key] = resolvedDeclaration as unknown as Record<string, unknown>;
|
||||
preparedByPath.set(inspected.sourcePath, prepared);
|
||||
preparedByKey.set(keySelection.key, prepared);
|
||||
}
|
||||
return [...preparedByKey.values()];
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent through an isolated YAML apply",
|
||||
"zh-CN": "通过隔离的 YAML Apply 声明并创建一个托管 Agent",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> --model <model> --instructions <text|path> [--description <text>] [--skill <id>...] [--type custom|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]",
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
'--name assistant --model qwen3.8-max --instructions "You are helpful."',
|
||||
"--name assistant --model qwen3.8-max --instructions ./prompts/assistant.md --skill skill_abc --yes",
|
||||
"--name slides --model qwen3.8-max --instructions ./prompts/slides.md --skill skill_pptx --type official --yes",
|
||||
"--name reviewer --model qwen3.8-max --instructions ./prompts/reviewer.md --skill-dir ./skills/code-review --yes",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Without --yes, previews the generated YAML key and scoped plan. --dry-run stays offline. Unrelated resources are not refreshed or drift-checked.",
|
||||
"zh-CN":
|
||||
"不带 --yes 时预览自动生成的 YAML key 和定向计划;--dry-run 完全离线。无关资源不会刷新或检测 Drift。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--skill writes an external Skill reference directly into the Agent declaration. --type defaults to custom; use --type official for platform Skills. These Skills are not managed through the top-level skills map.",
|
||||
"zh-CN":
|
||||
"--skill 会把外部 Skill 引用直接写入 Agent 声明。--type 默认为 custom;平台 Skill 使用 --type official。这些 Skill 不通过顶层 skills 资源管理。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--skill-dir accepts a local Skill directory or ZIP, writes it as a top-level custom Skill declaration, and writes its generated YAML key into the Agent skills list. Skill and Agent are created together in dependency order. --type applies only to --skill IDs.",
|
||||
"zh-CN":
|
||||
"--skill-dir 接收本地 Skill 目录或 ZIP,将其写为顶层 custom Skill 声明,并把生成的 YAML key 写入 Agent 的 skills 列表;Skill 与 Agent 按依赖顺序一并创建。--type 只作用于 --skill ID。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (!flags.name.trim()) return "--name must not be empty.";
|
||||
if (!flags.model.trim()) return "--model must not be empty.";
|
||||
if (!flags.instructions.trim()) return "--instructions must not be empty.";
|
||||
if (flags.type && !flags.skill?.length) return "--type requires at least one --skill.";
|
||||
if (flags.skill?.some((skillId) => !skillId.trim())) {
|
||||
return "--skill values must not be empty.";
|
||||
}
|
||||
if (flags.skillDir?.some((source) => !source.trim())) {
|
||||
return "--skill-dir values must not be empty.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { flags, settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const file = flags.file ?? "agents.yaml";
|
||||
const project = await loadScopedCreateProject(ctx, file);
|
||||
const provider = project.provider;
|
||||
const externalSkillRefs = buildAgentSkillRefs(flags.skill, agentSkillType(flags.type));
|
||||
const localSkills = await prepareLocalSkillSources(project, flags.skillDir);
|
||||
const skillRefs: AgentSkillDecl[] = [
|
||||
...externalSkillRefs,
|
||||
...localSkills.map((skill) => skill.key),
|
||||
];
|
||||
|
||||
const rawAgent = buildAgentDecl(undefined, {
|
||||
name: flags.name.trim(),
|
||||
description: flags.description,
|
||||
model: flags.model,
|
||||
instructions: flags.instructions,
|
||||
provider,
|
||||
builtinTools: flags.tool,
|
||||
}).agent;
|
||||
rawAgent.skills = skillRefs;
|
||||
const candidateConfig = structuredClone(project.config);
|
||||
candidateConfig.skills = { ...candidateConfig.skills };
|
||||
for (const skill of localSkills) {
|
||||
candidateConfig.skills[skill.key] = skill.resolvedDeclaration;
|
||||
}
|
||||
candidateConfig._resolved = true;
|
||||
const candidateAgent = (await resolveCandidateDeclaration({
|
||||
project: { ...project, config: candidateConfig },
|
||||
group: "agents",
|
||||
rawDeclaration: rawAgent as unknown as Record<string, unknown>,
|
||||
})) as unknown as BuiltAgentDecl;
|
||||
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
|
||||
selectAgentKey({
|
||||
displayName: flags.name.trim(),
|
||||
provider,
|
||||
agents: (project.config.agents ?? {}) as Record<string, BuiltAgentDecl>,
|
||||
candidate: candidateAgent,
|
||||
state,
|
||||
}),
|
||||
);
|
||||
const agentKey = keySelection.key;
|
||||
candidateConfig.agents = { ...candidateConfig.agents, [agentKey]: candidateAgent };
|
||||
candidateConfig._resolved = true;
|
||||
|
||||
const document = parseDocument(project.source);
|
||||
if (document.errors.length > 0) {
|
||||
throw new BailianError(
|
||||
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
for (const skill of localSkills) {
|
||||
document.setIn(["skills", skill.key], skill.rawDeclaration);
|
||||
}
|
||||
document.setIn(["agents", agentKey], rawAgent);
|
||||
const nextSource = document.toString();
|
||||
const agentRoot: ResourceAddress = { type: "agent", name: agentKey, provider };
|
||||
const roots: ResourceAddress[] = [
|
||||
...localSkills
|
||||
.filter((skill) => skill.needsCreate)
|
||||
.map((skill) => ({ type: "skill" as const, name: skill.key, provider })),
|
||||
agentRoot,
|
||||
];
|
||||
const backendInput: BackendRuntimeInput = {
|
||||
projectName: project.projectName,
|
||||
config: candidateConfig as ResolvedProjectConfig,
|
||||
configPath: project.configPath,
|
||||
providers: { [provider]: candidateConfig.providers[provider] },
|
||||
stateBackend: project.stateBackend,
|
||||
stateScope: project.stateScope,
|
||||
};
|
||||
|
||||
if (settings.dryRun || !flags.yes) {
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectWithStateBackend(backendInput, {
|
||||
provider,
|
||||
scope: { roots },
|
||||
refresh: !settings.dryRun,
|
||||
quiet: format === "json",
|
||||
mode: "create-only",
|
||||
}),
|
||||
),
|
||||
);
|
||||
const readyToCreate = !planned.plan.diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
const result = {
|
||||
agent: { key: agentKey, name: flags.name.trim(), provider, skills: skillRefs },
|
||||
local_skills: localSkills.map((skill) => ({
|
||||
key: skill.key,
|
||||
name: skill.name,
|
||||
source: skill.source,
|
||||
reused_pending: skill.reusedPending,
|
||||
reused_tracked: skill.reusedTracked,
|
||||
})),
|
||||
config_file: project.configPath,
|
||||
yaml_written: false,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
requires_confirmation: !settings.dryRun,
|
||||
ready_to_create: readyToCreate,
|
||||
actions: planned.plan.actions,
|
||||
diagnostics: planned.plan.diagnostics,
|
||||
};
|
||||
if (format === "json") {
|
||||
emitResult(result, format);
|
||||
} else {
|
||||
for (const skill of localSkills) {
|
||||
emitBare(`Generated Skill YAML key: ${skill.key}`);
|
||||
}
|
||||
emitBare(`Generated Agent YAML key: ${agentKey}`);
|
||||
for (const diagnostic of planned.plan.diagnostics) {
|
||||
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
for (const action of planned.plan.actions.filter((entry) => entry.action !== "no-op")) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
emitBare(
|
||||
settings.dryRun
|
||||
? "Dry run: YAML, State, and remote resources were not changed."
|
||||
: "Preview only: re-run with --yes to write YAML and create this Agent.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await replaceConfigAtomically(project.configPath, project.source, nextSource);
|
||||
let run: ResourceSyncRun;
|
||||
try {
|
||||
run = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
syncProjectResourcesWithStateBackend(backendInput, {
|
||||
provider,
|
||||
scope: { roots },
|
||||
refresh: true,
|
||||
quiet: format === "json",
|
||||
mode: "create-only",
|
||||
policy: "block",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw retainAgentError(
|
||||
error,
|
||||
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
"Scoped Agent and Skill create failed.",
|
||||
);
|
||||
}
|
||||
const remoteResources = await project.stateBackend.read(project.stateScope, (state) => ({
|
||||
agentId: state.getResource(agentRoot)?.remote_id,
|
||||
skillIds: Object.fromEntries(
|
||||
localSkills.map((skill) => [
|
||||
skill.key,
|
||||
state.getResource({ type: "skill", name: skill.key, provider })?.remote_id,
|
||||
]),
|
||||
),
|
||||
}));
|
||||
const results = run.execution?.results ?? [];
|
||||
const failed = results.find((entry) => entry.status === "failed");
|
||||
const status = failed ? "failed" : "completed";
|
||||
const result = {
|
||||
agent: {
|
||||
key: agentKey,
|
||||
name: flags.name.trim(),
|
||||
provider,
|
||||
remote_id: remoteResources.agentId,
|
||||
skills: skillRefs,
|
||||
},
|
||||
local_skills: localSkills.map((skill) => ({
|
||||
key: skill.key,
|
||||
name: skill.name,
|
||||
source: skill.source,
|
||||
remote_id: remoteResources.skillIds[skill.key],
|
||||
reused_pending: skill.reusedPending,
|
||||
reused_tracked: skill.reusedTracked,
|
||||
})),
|
||||
config_file: project.configPath,
|
||||
yaml_written: true,
|
||||
reused_pending: keySelection.reusedPending,
|
||||
status,
|
||||
actions: run.planned.plan.actions,
|
||||
diagnostics: run.planned.plan.diagnostics,
|
||||
results,
|
||||
error: failed?.error,
|
||||
};
|
||||
if (format === "json") emitResult(result, format);
|
||||
else {
|
||||
emitBare(
|
||||
`Wrote ${project.configPath} with Agent key '${agentKey}'${localSkills.length ? ` and ${localSkills.length} local Skill declaration(s)` : ""}.`,
|
||||
);
|
||||
if (status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`);
|
||||
else emitBare(`Scoped create ${status}: ${failed?.error ?? "unknown error"}`);
|
||||
}
|
||||
if (failed) {
|
||||
throw new BailianError(
|
||||
failed.error ?? "Scoped Agent create failed.",
|
||||
ExitCode.GENERAL,
|
||||
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getRemoteAgent } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { AGENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Get a Managed Agent", "zh-CN": "获取托管 Agent 详情" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [--agent-version <n>] [--file <path>]",
|
||||
flags: AGENT_GET_FLAGS,
|
||||
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --agent-version 3 --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: (flags) =>
|
||||
flags.agentVersion !== undefined &&
|
||||
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
|
||||
? "--agent-version must be a positive integer."
|
||||
: undefined,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const agent = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteAgent(runtime, ctx.flags.agentId, {
|
||||
provider: "bailian",
|
||||
version: ctx.flags.agentVersion,
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(agent, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${agent.id}`);
|
||||
emitBare(`Name: ${displayValue(agent.name)}`);
|
||||
emitBare(`Description: ${displayValue(agent.description, 120)}`);
|
||||
emitBare(`Version: ${displayValue(agent.version)}`);
|
||||
emitBare(`Type: ${displayValue(agent.type)}`);
|
||||
emitBare(`Created: ${displayValue(agent.created_at)}`);
|
||||
emitBare(`Updated: ${displayValue(agent.updated_at)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { listRemoteAgents } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { AGENT_LIST_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agents", "zh-CN": "列出托管 Agent" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived] [--file <path>]",
|
||||
flags: AGENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--limit 50", "--all --include-archived --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgents(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "agents",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No agents found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { listRemoteAgents } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import {
|
||||
emitCollection,
|
||||
matchesQuery,
|
||||
searchCursorPages,
|
||||
validateLimitAndPageLimit,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { AGENT_SEARCH_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Search Managed Agents", "zh-CN": "搜索托管 Agent" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
|
||||
flags: AGENT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query assistant", "--query code --page-limit 20 --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return searchCursorPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgents(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit ?? 100,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
(agent) => matchesQuery(ctx.flags.query, agent.id, agent.name, agent.description),
|
||||
ctx.flags.pageLimit,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "agents",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
truncated: result.truncated,
|
||||
scannedPages: result.scannedPages,
|
||||
emptyMessage: "No matching agents found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { listRemoteAgentVersions } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { AGENT_VERSIONS_FLAGS, agentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent versions", "zh-CN": "列出托管 Agent 版本" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: AGENT_VERSIONS_FLAGS,
|
||||
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteAgentVersions(runtime, ctx.flags.agentId, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "versions",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
|
||||
rows: agentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No agent versions found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
import {
|
||||
assertProviderConfigured,
|
||||
buildAgentRuntime,
|
||||
CREDENTIALS_NOTE,
|
||||
} from "./_engine/config-loader.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
|
||||
const APPLY_FLAGS = {
|
||||
@@ -26,21 +22,6 @@ const APPLY_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Target provider (default: all configured)",
|
||||
"zh-CN": "目标 Provider(默认:全部已配置项)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm and apply without an interactive prompt (required to mutate)",
|
||||
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
|
||||
},
|
||||
},
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -64,9 +45,17 @@ export default defineCommand({
|
||||
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This applies the current plan and may create, update, or delete remote managed Agent resources.",
|
||||
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--file <path>] [--concurrency <n>]",
|
||||
flags: APPLY_FLAGS,
|
||||
exampleArgs: ["--yes", "--provider bailian --yes"],
|
||||
exampleArgs: ["--yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
@@ -77,7 +66,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_apply: {
|
||||
provider: flags.provider ?? "all",
|
||||
provider: "bailian",
|
||||
refresh: !flags.noRefresh,
|
||||
concurrency: flags.concurrency,
|
||||
},
|
||||
@@ -92,9 +81,8 @@ export default defineCommand({
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
assertProviderConfigured(runtime, flags.provider);
|
||||
return planProjectContext(runtime, {
|
||||
provider: flags.provider,
|
||||
provider: "bailian",
|
||||
refresh: !flags.noRefresh,
|
||||
quiet: true,
|
||||
onFeedback: renderAgentFeedback,
|
||||
@@ -109,11 +97,20 @@ export default defineCommand({
|
||||
if (format === "json") process.stderr.write(`${line}\n`);
|
||||
else emitBare(line);
|
||||
};
|
||||
if (plan.diagnostics.some((diag) => diag.severity === "error")) {
|
||||
for (const diag of plan.diagnostics) {
|
||||
if (diag.severity === "error") emitProgress(`[error] ${diag.code}: ${diag.message}`);
|
||||
const errorDiagnostics = plan.diagnostics.filter(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
if (errorDiagnostics.length > 0) {
|
||||
for (const diagnostic of errorDiagnostics) {
|
||||
emitProgress(`[error] ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
throw new BailianError("Cannot apply: resolve the errors above first.", ExitCode.GENERAL);
|
||||
throw new BailianError(
|
||||
formatAgentDiagnosticFailure(
|
||||
errorDiagnostics,
|
||||
"Cannot apply: resolve the errors above first.",
|
||||
),
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const actionable = plan.actions.filter((action) => action.action !== "no-op");
|
||||
@@ -124,23 +121,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const creates = actionable.filter((action) => action.action === "create").length;
|
||||
const updates = actionable.filter((action) => action.action === "update").length;
|
||||
const deletes = planned.destructiveActions;
|
||||
|
||||
for (const action of actionable) {
|
||||
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
|
||||
if (!flags.yes) {
|
||||
throw new BailianError(
|
||||
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
|
||||
ExitCode.USAGE,
|
||||
"Review with `bl managed-agent plan`, then re-run with --yes to apply.",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executePlannedProject(planned, {
|
||||
@@ -152,7 +137,8 @@ export default defineCommand({
|
||||
);
|
||||
|
||||
const succeeded = result.results.filter((entry) => entry.status === "success").length;
|
||||
const failed = result.results.filter((entry) => entry.status === "failed").length;
|
||||
const failedResults = result.results.filter((entry) => entry.status === "failed");
|
||||
const failed = failedResults.length;
|
||||
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
|
||||
|
||||
if (format === "json") {
|
||||
@@ -161,6 +147,9 @@ export default defineCommand({
|
||||
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
if (failed > 0) throw new BailianError("Apply failed.", ExitCode.GENERAL);
|
||||
if (failed > 0) {
|
||||
const firstFailure = failedResults.find((entry) => entry.error);
|
||||
throw new BailianError(firstFailure?.error ?? "Apply failed.", ExitCode.GENERAL);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { setRemoteDeploymentPaused } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
resolveDeploymentTarget,
|
||||
validateDeploymentActionTarget,
|
||||
} from "./_shared.ts";
|
||||
|
||||
export function createSetDeploymentPausedCommand(paused: boolean) {
|
||||
return defineCommand({
|
||||
description: paused
|
||||
? { "en-US": "Pause a Managed Agent deployment", "zh-CN": "暂停托管 Agent Deployment" }
|
||||
: { "en-US": "Unpause a Managed Agent deployment", "zh-CN": "恢复托管 Agent Deployment" },
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: paused
|
||||
? {
|
||||
"en-US":
|
||||
"This pauses the specified Managed Agent deployment and stops its scheduled executions until resumed.",
|
||||
"zh-CN": "该操作会暂停指定的托管 Agent Deployment,并停止其定时执行直至恢复。",
|
||||
}
|
||||
: {
|
||||
"en-US":
|
||||
"This resumes the specified Managed Agent deployment and may restart scheduled executions and related usage.",
|
||||
"zh-CN":
|
||||
"该操作会恢复指定的托管 Agent Deployment,可能重新开始定时执行并产生相关用量。",
|
||||
},
|
||||
},
|
||||
usageArgs: "(--deployment <name> | --deployment-id <id>)",
|
||||
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc --yes`],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateDeploymentActionTarget,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
[paused ? "would_pause_deployment" : "would_unpause_deployment"]:
|
||||
ctx.flags.deploymentId ?? ctx.flags.deployment,
|
||||
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
|
||||
return {
|
||||
deploymentId,
|
||||
deployment: await setRemoteDeploymentPaused(runtime, deploymentId, paused, {
|
||||
provider: "bailian",
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format);
|
||||
} else {
|
||||
emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { DeploymentInfo, DeploymentRunInfo, ProjectRuntimeContext } from "@openagentpack/sdk";
|
||||
import { UserError } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const DEPLOYMENT_ID_FLAG = {
|
||||
deploymentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Deployment ID", "zh-CN": "Deployment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const DEPLOYMENT_LIST_FILTER_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Filter by agent ID", "zh-CN": "按 Agent ID 筛选" },
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
valueHint: "<status>",
|
||||
choices: ["active", "paused"] as const,
|
||||
description: { "en-US": "Filter by deployment status", "zh-CN": "按 Deployment 状态筛选" },
|
||||
},
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
createdAtGte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or after this timestamp",
|
||||
"zh-CN": "创建时间不早于该时间戳",
|
||||
},
|
||||
},
|
||||
createdAtLte: {
|
||||
type: "string",
|
||||
valueHint: "<timestamp>",
|
||||
description: {
|
||||
"en-US": "Created at or before this timestamp",
|
||||
"zh-CN": "创建时间不晚于该时间戳",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEPLOYMENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...DEPLOYMENT_LIST_FILTER_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_GET_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG };
|
||||
|
||||
export const DEPLOYMENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
query: SEARCH_FLAGS.query,
|
||||
...DEPLOYMENT_LIST_FILTER_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_RUNS_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...DEPLOYMENT_ID_FLAG,
|
||||
...CURSOR_FLAGS,
|
||||
};
|
||||
|
||||
export const DEPLOYMENT_RUNS_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
runId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Deployment run ID", "zh-CN": "Deployment Run ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEPLOYMENT_ACTION_TARGET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
deployment: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: {
|
||||
"en-US": "Logical deployment name in agents.yaml/state",
|
||||
"zh-CN": "agents.yaml/State 中的逻辑 Deployment 名称",
|
||||
},
|
||||
},
|
||||
deploymentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: { "en-US": "Direct deployment ID", "zh-CN": "直接指定 Deployment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function validateDeploymentActionTarget(flags: {
|
||||
deployment?: string;
|
||||
deploymentId?: string;
|
||||
}): string | undefined {
|
||||
if (Boolean(flags.deployment) === Boolean(flags.deploymentId)) {
|
||||
return "Provide exactly one of --deployment or --deployment-id.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function deploymentRows(deployments: DeploymentInfo[]): string[][] {
|
||||
return deployments.map((deployment) => [
|
||||
displayValue(deployment.id),
|
||||
deployment.status,
|
||||
displayValue(deployment.schedule?.expression),
|
||||
displayValue(deployment.paused_reason?.type),
|
||||
]);
|
||||
}
|
||||
|
||||
export function deploymentRunRows(runs: DeploymentRunInfo[]): string[][] {
|
||||
return runs.map((run) => [
|
||||
run.id,
|
||||
displayValue(run.deployment_id),
|
||||
displayValue(run.session_id),
|
||||
displayValue(run.status),
|
||||
displayValue(run.created_at),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resolveDeploymentTarget(
|
||||
runtime: ProjectRuntimeContext,
|
||||
options: { deployment?: string; deploymentId?: string },
|
||||
): Promise<string> {
|
||||
if (options.deploymentId) return options.deploymentId;
|
||||
const state = runtime.state.getResource({
|
||||
provider: "bailian",
|
||||
type: "deployment",
|
||||
name: options.deployment!,
|
||||
});
|
||||
if (!state?.remote_id) {
|
||||
throw new UserError(
|
||||
`Deployment '${options.deployment}' is not tracked in state. Use --deployment-id or run managed-agent apply/import first.`,
|
||||
);
|
||||
}
|
||||
return state.remote_id;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { DeploymentDecl } from "@openagentpack/sdk";
|
||||
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
parseJsonInputs,
|
||||
parseMetadata,
|
||||
resolveCandidateDeclaration,
|
||||
runScopedTopLevelCreate,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Deployment display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Deployment 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<yaml-key>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Existing Agent key from agents.yaml",
|
||||
"zh-CN": "agents.yaml 中已有的 Agent key",
|
||||
},
|
||||
},
|
||||
agentVersion: {
|
||||
type: "number",
|
||||
valueHint: "<number>",
|
||||
description: { "en-US": "Agent version", "zh-CN": "Agent 版本" },
|
||||
},
|
||||
environment: {
|
||||
type: "string",
|
||||
valueHint: "<yaml-key>",
|
||||
description: {
|
||||
"en-US": "Existing Environment key from agents.yaml",
|
||||
"zh-CN": "agents.yaml 中已有的 Environment key",
|
||||
},
|
||||
},
|
||||
vault: {
|
||||
type: "array",
|
||||
valueHint: "<yaml-key>",
|
||||
description: {
|
||||
"en-US": "Existing Vault key from agents.yaml (repeatable)",
|
||||
"zh-CN": "agents.yaml 中已有的 Vault key(可重复)",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: "array",
|
||||
valueHint: "<text>",
|
||||
description: {
|
||||
"en-US": "Initial user message (repeatable)",
|
||||
"zh-CN": "初始用户消息(可重复)",
|
||||
},
|
||||
},
|
||||
event: {
|
||||
type: "array",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "Initial user.message or system.message JSON (repeatable)",
|
||||
"zh-CN": "初始 user.message 或 system.message JSON(可重复)",
|
||||
},
|
||||
},
|
||||
resource: {
|
||||
type: "array",
|
||||
valueHint: "<json|@path>",
|
||||
description: {
|
||||
"en-US": "File Resource JSON with source or file_id (repeatable)",
|
||||
"zh-CN": "包含 source 或 file_id 的 File Resource JSON(可重复)",
|
||||
},
|
||||
},
|
||||
schedule: {
|
||||
type: "string",
|
||||
valueHint: "<cron>",
|
||||
description: { "en-US": "Five-field cron expression", "zh-CN": "五段式 Cron 表达式" },
|
||||
},
|
||||
timezone: {
|
||||
type: "string",
|
||||
valueHint: "<timezone>",
|
||||
description: { "en-US": "IANA schedule timezone", "zh-CN": "Schedule 的 IANA 时区" },
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Deployment description", "zh-CN": "Deployment 描述" },
|
||||
},
|
||||
metadata: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Metadata entry (repeatable)",
|
||||
"zh-CN": "Metadata 条目(可重复)",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent Deployment through a scoped YAML apply",
|
||||
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> --agent <yaml-key> (--message <text>... | --event <json|@path>...) [--agent-version <number>] [--environment <yaml-key>] [--vault <yaml-key>...] [--resource <json|@path>...] [--schedule <cron> --timezone <timezone>] [--description <text>] [--metadata <key=value>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: [
|
||||
{
|
||||
"en-US": '--name Daily --agent assistant --message "Generate the report"',
|
||||
"zh-CN": '--name Daily --agent assistant --message "生成报告"',
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
'--name Daily --agent assistant --event \'{"type":"system.message","content":"Be concise"}\' --yes',
|
||||
"zh-CN":
|
||||
'--name Daily --agent assistant --event \'{"type":"system.message","content":"保持简洁"}\' --yes',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release.",
|
||||
"zh-CN":
|
||||
"Initial Events 必须包含 1–50 条 user.message/system.message;本期 --resource 仅接受 File Resource。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Use either repeatable --message values or repeatable --event values; the two input forms cannot be mixed.",
|
||||
"zh-CN": "重复使用 --message 或重复使用 --event;两种输入形式不能混用。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (!flags.name.trim()) return "--name must not be empty.";
|
||||
if (!flags.agent.trim()) return "--agent must not be empty.";
|
||||
if (flags.message?.length && flags.event?.length) {
|
||||
return "--message and --event cannot be used together.";
|
||||
}
|
||||
if (!flags.message?.length && !flags.event?.length)
|
||||
return "Pass at least one --message or --event.";
|
||||
if (Boolean(flags.schedule) !== Boolean(flags.timezone)) {
|
||||
return "--schedule and --timezone must be provided together.";
|
||||
}
|
||||
if (
|
||||
flags.agentVersion !== undefined &&
|
||||
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
|
||||
) {
|
||||
return "--agent-version must be a positive integer.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const initialEvents = [
|
||||
...(ctx.flags.message ?? []).map((content) => ({ type: "user.message" as const, content })),
|
||||
...validateEvents(await parseJsonInputs(ctx.flags.event, "event")),
|
||||
];
|
||||
if (initialEvents.length < 1 || initialEvents.length > 50) {
|
||||
throw new BailianError("Initial Events must contain 1-50 entries.", ExitCode.USAGE);
|
||||
}
|
||||
const resources = validateResources(await parseJsonInputs(ctx.flags.resource, "resource"));
|
||||
if (ctx.flags.schedule && ctx.flags.schedule.trim().split(/\s+/).length !== 5) {
|
||||
throw new BailianError("--schedule must be a five-field cron expression.", ExitCode.USAGE);
|
||||
}
|
||||
if (ctx.flags.timezone) validateTimezone(ctx.flags.timezone);
|
||||
|
||||
const rawDeclaration: DeploymentDecl = {
|
||||
name: ctx.flags.name.trim(),
|
||||
agent: ctx.flags.agent,
|
||||
agent_version: ctx.flags.agentVersion,
|
||||
environment: ctx.flags.environment,
|
||||
vaults: ctx.flags.vault,
|
||||
resources,
|
||||
initial_events: initialEvents,
|
||||
schedule:
|
||||
ctx.flags.schedule && ctx.flags.timezone
|
||||
? { expression: ctx.flags.schedule, timezone: ctx.flags.timezone }
|
||||
: undefined,
|
||||
description: ctx.flags.description,
|
||||
provider: project.provider,
|
||||
metadata: parseMetadata(ctx.flags.metadata),
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "deployments",
|
||||
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "deployments",
|
||||
resourceType: "deployment",
|
||||
displayName: ctx.flags.name.trim(),
|
||||
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: (project.config.deployments ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "deployment",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function validateEvents(
|
||||
values: unknown[],
|
||||
): Array<{ type: "user.message" | "system.message"; content: string }> {
|
||||
return values.map((value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new BailianError("Each --event must be a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
const event = value as Record<string, unknown>;
|
||||
if (event.type !== "user.message" && event.type !== "system.message") {
|
||||
throw new BailianError(
|
||||
"--event type must be user.message or system.message.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (typeof event.content !== "string" || !event.content.trim()) {
|
||||
throw new BailianError("--event content must be a non-empty string.", ExitCode.USAGE);
|
||||
}
|
||||
return { type: event.type, content: event.content };
|
||||
});
|
||||
}
|
||||
|
||||
function validateResources(values: unknown[]): NonNullable<DeploymentDecl["resources"]> {
|
||||
return values.map((value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new BailianError("Each --resource must be a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
const resource = value as Record<string, unknown>;
|
||||
if (resource.type !== "file") {
|
||||
throw new BailianError("--resource currently accepts only type=file.", ExitCode.USAGE);
|
||||
}
|
||||
const source = typeof resource.source === "string" ? resource.source : undefined;
|
||||
const fileId = typeof resource.file_id === "string" ? resource.file_id : undefined;
|
||||
if (Boolean(source) === Boolean(fileId)) {
|
||||
throw new BailianError(
|
||||
"A File Resource must provide exactly one of source or file_id.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (resource.mount_path !== undefined && typeof resource.mount_path !== "string") {
|
||||
throw new BailianError("File Resource mount_path must be a string.", ExitCode.USAGE);
|
||||
}
|
||||
return {
|
||||
type: "file" as const,
|
||||
source,
|
||||
file_id: fileId,
|
||||
mount_path: resource.mount_path as string | undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function validateTimezone(timezone: string): void {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
||||
} catch {
|
||||
throw new BailianError(`Invalid IANA timezone: ${timezone}`, ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRemoteDeployment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { DEPLOYMENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent deployment",
|
||||
"zh-CN": "获取托管 Agent Deployment 详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployment-id <id>",
|
||||
flags: DEPLOYMENT_GET_FLAGS,
|
||||
exampleArgs: ["--deployment-id dep_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const deployment = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteDeployment(runtime, ctx.flags.deploymentId, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(deployment, format);
|
||||
else {
|
||||
emitBare(`ID: ${displayValue(deployment.id)}`);
|
||||
emitBare(`Status: ${deployment.status}`);
|
||||
emitBare(`Schedule:${displayValue(deployment.schedule?.expression)}`);
|
||||
emitBare(`Paused: ${displayValue(deployment.paused_reason)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { listRemoteDeployments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_LIST_FLAGS, deploymentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent deployments", "zh-CN": "列出托管 Agent Deployment" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--agent-id <id>] [--status active|paused] [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--status active --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeployments(runtime, {
|
||||
provider: "bailian",
|
||||
agent_id: ctx.flags.agentId,
|
||||
status: ctx.flags.status,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.deployments,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "deployments",
|
||||
items: result.items,
|
||||
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
|
||||
rows: deploymentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No deployments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
|
||||
|
||||
export default createSetDeploymentPausedCommand(true);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { runRemoteDeployment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import {
|
||||
DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
resolveDeploymentTarget,
|
||||
validateDeploymentActionTarget,
|
||||
} from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Run a Managed Agent deployment now",
|
||||
"zh-CN": "立即运行托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This immediately starts a run for the specified Managed Agent deployment and may incur usage or trigger configured actions.",
|
||||
"zh-CN": "该操作会立即运行指定的托管 Agent Deployment,可能产生用量或触发已配置的动作。",
|
||||
},
|
||||
},
|
||||
usageArgs: "(--deployment <name> | --deployment-id <id>)",
|
||||
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
|
||||
exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateDeploymentActionTarget,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_run_deployment: ctx.flags.deploymentId ?? ctx.flags.deployment,
|
||||
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
|
||||
return {
|
||||
deploymentId,
|
||||
run: await runRemoteDeployment(runtime, deploymentId, { provider: "bailian" }),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (format === "json")
|
||||
emitResult({ deployment_id: result.deploymentId, ...result.run }, format);
|
||||
else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRemoteDeploymentRun } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../../_engine/errors.ts";
|
||||
import { DEPLOYMENT_RUNS_GET_FLAGS } from "../_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent deployment run",
|
||||
"zh-CN": "获取托管 Agent Deployment Run 详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--run-id <id>",
|
||||
flags: DEPLOYMENT_RUNS_GET_FLAGS,
|
||||
exampleArgs: ["--run-id run_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const run = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteDeploymentRun(runtime, ctx.flags.runId, { provider: "bailian" });
|
||||
}),
|
||||
);
|
||||
if (format === "json") emitResult(run, format);
|
||||
else {
|
||||
emitBare(`ID: ${run.id}`);
|
||||
emitBare(`Deployment: ${displayValue(run.deployment_id)}`);
|
||||
emitBare(`Session: ${displayValue(run.session_id)}`);
|
||||
emitBare(`Status: ${displayValue(run.status)}`);
|
||||
emitBare(`Created: ${displayValue(run.created_at)}`);
|
||||
emitBare(`Error: ${displayValue(run.error)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { listRemoteDeploymentRuns } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_RUNS_LIST_FLAGS, deploymentRunRows } from "../_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "List runs for a Managed Agent deployment",
|
||||
"zh-CN": "列出托管 Agent Deployment Run",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployment-id <id> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_RUNS_LIST_FLAGS,
|
||||
exampleArgs: ["--deployment-id dep_abc", "--deployment-id dep_abc --all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeploymentRuns(runtime, ctx.flags.deploymentId, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "runs",
|
||||
items: result.items,
|
||||
headers: ["ID", "DEPLOYMENT", "SESSION", "STATUS", "CREATED"],
|
||||
rows: deploymentRunRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No deployment runs found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { listRemoteDeployments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { DEPLOYMENT_SEARCH_FLAGS, deploymentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Search Managed Agent deployments",
|
||||
"zh-CN": "搜索托管 Agent Deployment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [--limit <n>] [--page <cursor>] [--all]",
|
||||
flags: DEPLOYMENT_SEARCH_FLAGS,
|
||||
exampleArgs: ["--query report", "--query nightly --all --output json"],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US": "Deployment search maps --query to the provider's server-side keyword parameter.",
|
||||
"zh-CN": "Deployment 搜索会把 --query 映射为 Provider 服务端 keyword 参数。",
|
||||
},
|
||||
],
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteDeployments(runtime, {
|
||||
provider: "bailian",
|
||||
keyword: ctx.flags.query,
|
||||
agent_id: ctx.flags.agentId,
|
||||
status: ctx.flags.status,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
created_at_gte: ctx.flags.createdAtGte,
|
||||
created_at_lte: ctx.flags.createdAtLte,
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
});
|
||||
return {
|
||||
items: response.deployments,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "deployments",
|
||||
items: result.items,
|
||||
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
|
||||
rows: deploymentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No matching deployments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
|
||||
|
||||
export default createSetDeploymentPausedCommand(false);
|
||||
@@ -21,13 +21,6 @@ const DESTROY_FLAGS = {
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Confirm and destroy without an interactive prompt (required)",
|
||||
"zh-CN": "无需交互提示直接确认并销毁(必填)",
|
||||
},
|
||||
},
|
||||
cascade: {
|
||||
type: "switch",
|
||||
description: {
|
||||
@@ -43,7 +36,15 @@ export default defineCommand({
|
||||
"zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--yes] [--cascade]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This deletes every managed Agent resource tracked in state; --cascade may also delete dependent resources.",
|
||||
"zh-CN": "该操作会删除 State 中跟踪的全部托管 Agent 资源;--cascade 还可能删除依赖资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--file <path>] [--cascade]",
|
||||
flags: DESTROY_FLAGS,
|
||||
exampleArgs: ["--yes", "--yes --cascade"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
@@ -67,7 +68,16 @@ export default defineCommand({
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, file);
|
||||
return planDestroyProjectContext(runtime);
|
||||
const planned = planDestroyProjectContext(runtime);
|
||||
return {
|
||||
...planned,
|
||||
resources: planned.resources.filter(
|
||||
(resource) => resource.address.provider === "bailian",
|
||||
),
|
||||
defaultMemoryStores: (planned.defaultMemoryStores ?? []).filter(
|
||||
(memoryStore) => memoryStore.provider === "bailian",
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -86,14 +96,6 @@ export default defineCommand({
|
||||
else emitBare(line);
|
||||
}
|
||||
|
||||
if (!flags.yes) {
|
||||
throw new BailianError(
|
||||
`Refusing to destroy ${resources.length} resource(s) without confirmation.`,
|
||||
ExitCode.USAGE,
|
||||
"Re-run with --yes to destroy (add --cascade to remove dependents).",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
destroyPlannedProjectResources(planned, {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { CloudEnvironment } from "@openagentpack/sdk";
|
||||
import {
|
||||
API_TARGET_FLAGS,
|
||||
CURSOR_FLAGS,
|
||||
displayValue,
|
||||
INCLUDE_ARCHIVED_FLAG,
|
||||
SEARCH_FLAGS,
|
||||
} from "../_engine/api-helpers.ts";
|
||||
|
||||
export const ENVIRONMENT_LIST_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
...CURSOR_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_SEARCH_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
limit: CURSOR_FLAGS.limit,
|
||||
...SEARCH_FLAGS,
|
||||
...INCLUDE_ARCHIVED_FLAG,
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_GET_FLAGS = {
|
||||
...API_TARGET_FLAGS,
|
||||
environmentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
required: true,
|
||||
description: { "en-US": "Environment ID", "zh-CN": "Environment ID" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function environmentRows(environments: CloudEnvironment[]): string[][] {
|
||||
return environments.map((environment) => [
|
||||
environment.id,
|
||||
displayValue(environment.name),
|
||||
displayValue(environment.scope),
|
||||
displayValue(environment.version),
|
||||
displayValue(environment.updated_at),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { defineCommand, type FlagsDef } from "bailian-cli-core";
|
||||
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import {
|
||||
loadScopedCreateProject,
|
||||
parseMetadata,
|
||||
resolveCandidateDeclaration,
|
||||
runScopedTopLevelCreate,
|
||||
SCOPED_CREATE_NOTE,
|
||||
} from "../_engine/scoped-create.ts";
|
||||
|
||||
const FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
required: true,
|
||||
description: {
|
||||
"en-US": "Remote Environment display name; the YAML key is generated automatically",
|
||||
"zh-CN": "远端 Environment 显示名称;YAML key 将自动生成",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: { "en-US": "Environment description", "zh-CN": "Environment 描述" },
|
||||
},
|
||||
metadata: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description: {
|
||||
"en-US": "Metadata entry (repeatable)",
|
||||
"zh-CN": "Metadata 条目(可重复)",
|
||||
},
|
||||
},
|
||||
apt: packageFlag("APT"),
|
||||
pip: packageFlag("pip"),
|
||||
npm: packageFlag("npm"),
|
||||
cargo: packageFlag("Cargo"),
|
||||
gem: packageFlag("Ruby gem"),
|
||||
go: packageFlag("Go"),
|
||||
file: fileFlag(),
|
||||
yes: yesFlag(),
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Declare and create one Managed Agent Environment through a scoped YAML apply",
|
||||
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Environment",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--name <name> [--description <text>] [--metadata <key=value>...] [--apt <package>...] [--pip <package>...] [--npm <package>...] [--cargo <package>...] [--gem <package>...] [--go <package>...] [--file <path>] [--yes]",
|
||||
flags: FLAGS,
|
||||
exampleArgs: [
|
||||
"--name Development",
|
||||
"--name Development --pip pandas --npm typescript --metadata owner=platform --yes",
|
||||
],
|
||||
notes: [
|
||||
...CREDENTIALS_NOTE,
|
||||
...SCOPED_CREATE_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Creates a cloud Environment with unrestricted networking. Without --yes, only previews the generated YAML key and scoped plan.",
|
||||
"zh-CN":
|
||||
"创建使用 unrestricted 网络的 cloud Environment。不带 --yes 时仅预览自动生成的 YAML key 和定向计划。",
|
||||
},
|
||||
],
|
||||
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
|
||||
async run(ctx) {
|
||||
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
const packages = Object.fromEntries(
|
||||
["apt", "pip", "npm", "cargo", "gem", "go"]
|
||||
.map((manager) => [manager, ctx.flags[manager as keyof typeof ctx.flags]])
|
||||
.filter((entry) => Array.isArray(entry[1]) && entry[1].length > 0),
|
||||
);
|
||||
const rawDeclaration: Record<string, unknown> = {
|
||||
name: ctx.flags.name.trim(),
|
||||
description: ctx.flags.description,
|
||||
provider: project.provider,
|
||||
config: {
|
||||
type: "cloud",
|
||||
networking: { type: "unrestricted" },
|
||||
...(Object.keys(packages).length > 0 ? { packages } : {}),
|
||||
},
|
||||
metadata: parseMetadata(ctx.flags.metadata),
|
||||
};
|
||||
const resolvedDeclaration = await resolveCandidateDeclaration({
|
||||
project,
|
||||
group: "environments",
|
||||
rawDeclaration,
|
||||
});
|
||||
await runScopedTopLevelCreate({
|
||||
host: ctx,
|
||||
project,
|
||||
group: "environments",
|
||||
resourceType: "environment",
|
||||
displayName: ctx.flags.name.trim(),
|
||||
rawDeclaration,
|
||||
resolvedDeclaration,
|
||||
existingDeclarations: (project.config.environments ?? {}) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>,
|
||||
effectiveName: (key, declaration) =>
|
||||
typeof declaration.name === "string" ? declaration.name : key,
|
||||
fallbackKey: "environment",
|
||||
yes: ctx.flags.yes,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function packageFlag(manager: string) {
|
||||
return {
|
||||
type: "array" as const,
|
||||
valueHint: "<package>",
|
||||
description: {
|
||||
"en-US": `${manager} package (repeatable)`,
|
||||
"zh-CN": `${manager} 软件包(可重复)`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fileFlag() {
|
||||
return {
|
||||
type: "string" as const,
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function yesFlag() {
|
||||
return {
|
||||
type: "switch" as const,
|
||||
description: {
|
||||
"en-US": "Write YAML and run the scoped remote create",
|
||||
"zh-CN": "写入 YAML 并执行定向远端创建",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getRemoteEnvironment } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { displayValue } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { ENVIRONMENT_GET_FLAGS } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Get a Managed Agent environment",
|
||||
"zh-CN": "获取托管 Agent 环境详情",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--environment-id <id>",
|
||||
flags: ENVIRONMENT_GET_FLAGS,
|
||||
exampleArgs: ["--environment-id env_abc"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const environment = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return getRemoteEnvironment(runtime, ctx.flags.environmentId, {
|
||||
provider: "bailian",
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (format === "json") {
|
||||
emitResult(environment, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`ID: ${environment.id}`);
|
||||
emitBare(`Name: ${displayValue(environment.name)}`);
|
||||
emitBare(`Description: ${displayValue(environment.description, 120)}`);
|
||||
emitBare(`Scope: ${displayValue(environment.scope)}`);
|
||||
emitBare(`Version: ${displayValue(environment.version)}`);
|
||||
emitBare(`Updated: ${displayValue(environment.updated_at)}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { listRemoteEnvironments } from "@openagentpack/sdk";
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
|
||||
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "../_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "../_engine/errors.ts";
|
||||
import { fetchAllPages } from "../_engine/pagination.ts";
|
||||
import { ENVIRONMENT_LIST_FLAGS, environmentRows } from "./_shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
|
||||
flags: ENVIRONMENT_LIST_FLAGS,
|
||||
exampleArgs: ["", "--all --output json"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
validate: validateLimitAndPageLimit,
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(async () => {
|
||||
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
|
||||
return fetchAllPages(
|
||||
async (page) => {
|
||||
const response = await listRemoteEnvironments(runtime, {
|
||||
provider: "bailian",
|
||||
limit: ctx.flags.limit,
|
||||
page,
|
||||
include_archived: ctx.flags.includeArchived,
|
||||
});
|
||||
return {
|
||||
items: response.data,
|
||||
hasMore: response.has_more,
|
||||
nextPage: response.next_page,
|
||||
};
|
||||
},
|
||||
ctx.flags.all,
|
||||
ctx.flags.page,
|
||||
);
|
||||
}),
|
||||
);
|
||||
emitCollection({
|
||||
format,
|
||||
key: "environments",
|
||||
items: result.items,
|
||||
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
|
||||
rows: environmentRows(result.items),
|
||||
hasMore: result.hasMore,
|
||||
nextPage: result.nextPage,
|
||||
emptyMessage: "No environments found.",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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,19 +20,21 @@ 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({
|
||||
description: { "en-US": "Delete a session", "zh-CN": "删除 Session" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This deletes the specified remote managed Agent Session.",
|
||||
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--session-id <id> [--file <path>]",
|
||||
flags: SESSION_DELETE_FLAGS,
|
||||
exampleArgs: ["--session-id sess_abc123"],
|
||||
exampleArgs: ["--session-id sess_abc123 --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
@@ -43,7 +45,7 @@ export default defineCommand({
|
||||
emitResult(
|
||||
{
|
||||
would_delete_session: flags.sessionId,
|
||||
provider: flags.provider ?? "auto",
|
||||
provider: "bailian",
|
||||
config_file: file,
|
||||
},
|
||||
format,
|
||||
@@ -54,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}`);
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user