mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad5c44d746 | |||
| 9450895a06 | |||
| e681263049 | |||
| ffc460156c | |||
| 70b50060cf | |||
| 3909a17da1 | |||
| 5ed15d3a16 | |||
| 640dd02bc5 | |||
| 6eeb8fe0cb | |||
| d0610a61dc | |||
| 57c2d98308 | |||
| 78e6993475 | |||
| 79a0d2db9a | |||
| 8e6af6c669 | |||
| f7d32504ab | |||
| cdf94a8c89 | |||
| 7461189007 | |||
| 4ccda5f929 | |||
| 4086da572f | |||
| ce4d66b736 | |||
| 196a0aa506 | |||
| a9b0a752a8 | |||
| 2b7a0c742a | |||
| e5818e103c | |||
| 7797940626 | |||
| 5f1c97940d | |||
| 94ccab0898 | |||
| b895f88abb | |||
| 7eedc05b99 | |||
| f3c7b6fb10 | |||
| eb196cb4a6 | |||
| f1b6cacd7f | |||
| 9133b6bdd1 | |||
| 98ba3279fa | |||
| 3b7c4cfabc | |||
| d5d9fcb50f | |||
| d5c4bd3572 | |||
| eb4f9af3e7 | |||
| 3ea2931152 | |||
| b402f3eacd | |||
| b7a4efe619 | |||
| bedd59df27 | |||
| 39a488181e | |||
| 4ec0f6828b | |||
| 4dcec7d075 | |||
| 30f7525d50 | |||
| daefc094ec | |||
| aa38d5c670 | |||
| cc51164c2f | |||
| ae0c2c1213 | |||
| 01a62eb85b | |||
| 798ce596f6 | |||
| bd91e9d1c2 | |||
| e244771ee9 | |||
| 94f9dbbe9e | |||
| 8a0dd70206 | |||
| 9379da7a4c | |||
| ddcd564e61 | |||
| 0e4dd4b824 | |||
| 241de61866 | |||
| 61d9a74166 | |||
| 69eb759490 | |||
| 313966d7a9 | |||
| d74d4efcd0 | |||
| e7422bd2e5 | |||
| 2d5c49b02e | |||
| 9bd8b60c22 | |||
| 0369bd36b0 | |||
| 92a978af3c | |||
| 9749a11d76 | |||
| 2965080cb7 | |||
| 81959145d7 | |||
| 4343fc87af | |||
| 1c76749ee5 | |||
| 1b568e8d37 | |||
| 5d1b7aac3a | |||
| e292b20d4b | |||
| 12e7a22195 | |||
| 219d8be80a | |||
| ab766d44d3 | |||
| 1d9852805f | |||
| 99a3dbae2d | |||
| 4d84af614b | |||
| 2389681ad6 | |||
| 9ae5dc924d | |||
| 946b7029c6 | |||
| d6cb075629 | |||
| b9ecd5c43b | |||
| 978f332fea | |||
| 4502424200 | |||
| 03839766bc | |||
| 5007b9b574 | |||
| 1f8b9ace7e | |||
| 8286a74fb6 | |||
| 9eb2acbb65 | |||
| 1d35326c86 | |||
| eb6c2b8e2a | |||
| ebd6226a9f | |||
| e25d3b0b8e | |||
| 0e33c70e65 | |||
| 3c64461cca | |||
| 24092b423c | |||
| f30fff9065 | |||
| a7245c0f62 | |||
| 752a79e442 | |||
| 80bdcb83f6 | |||
| d9e8601a50 | |||
| e3bb5a7fa0 | |||
| 54da9aa29a | |||
| ef463e8d5d | |||
| 8ee2c378f5 | |||
| 9fc6434a26 | |||
| 43abf0aca5 |
@@ -0,0 +1,27 @@
|
||||
# Poke the FC publish-skills flow after skills/ changes land.
|
||||
# The FC side reconciles this repo's skills/ directory against OSS
|
||||
# (bailian-wiki/skills/) using the repo HEAD snapshot as the only
|
||||
# source of truth — the request itself carries no content. Both the
|
||||
# repo and branch params are validated against FC-side whitelists
|
||||
# (PUBLISH_REPOS / PUBLISH_BRANCHES).
|
||||
#
|
||||
# feat/cli-skill-sync is temporary for end-to-end testing; remove it
|
||||
# (here and from the FC PUBLISH_BRANCHES whitelist) once the sync
|
||||
# link is verified on main.
|
||||
name: Publish skills to OSS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- feat/cli-skill-sync
|
||||
paths:
|
||||
- "skills/**"
|
||||
|
||||
jobs:
|
||||
poke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger FC publish-skills
|
||||
run: |
|
||||
curl -sf -X POST "${{ vars.FC_TRIGGER_URL }}/publish-skills?repo=modelstudioai/cli&branch=${{ github.ref_name }}"
|
||||
@@ -35,7 +35,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
|
||||
packages/core/src/client/ # HTTP client / endpoints / console gateway
|
||||
```
|
||||
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `npx skills add modelstudioai/cli --all -g` 安装(整包装齐,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
|
||||
约定:
|
||||
|
||||
|
||||
@@ -6,6 +6,79 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.16.0] - 2026-08-17
|
||||
|
||||
> Full knowledge-base lifecycle management arrives in the CLI: create and configure knowledge bases, upload documents, tune chunks, and deploy retrieval/Q&A services — all from `bl knowledge` and `kscli`.
|
||||
|
||||
### Added
|
||||
|
||||
- **Knowledge base management** — `bl knowledge create` / `list` / `info` / `update` / `delete` manage knowledge bases end to end; `bl knowledge stats` reports document counts and usage over a past time range.
|
||||
- **Document management** — `bl knowledge doc upload` uploads local files or whole directories (recursive scan, skips unsupported formats and tool directories like `node_modules`); `doc list` / `status` / `tag` / `delete` cover the rest of the document lifecycle, and `doc import-oss` imports documents from OSS.
|
||||
- **Retrieval / Q&A service management** — `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` manage retrieval and Q&A service configurations, including deploying a draft to a published version.
|
||||
- **Chunk management** — `bl knowledge chunk add` / `list` / `update` / `delete` inspect and fine-tune document chunks.
|
||||
- **Data-center management** — `bl knowledge category list` / `add` / `delete`, `bl knowledge file list` / `get` / `delete`, and `bl knowledge collection create` / `get` manage categories, raw files, and data collections.
|
||||
- **Service version selection for retrieval and chat** — `bl knowledge search` and `bl knowledge chat` accept `--agent-version` to call the beta (draft) config for debugging or a specific published version.
|
||||
- **`kscli` parity** — all new knowledge commands are also available in Knowledge Studio CLI under shorter paths, e.g. `kscli kb list`, `kscli doc upload`, `kscli service deploy`.
|
||||
|
||||
### Removed
|
||||
|
||||
- **`bl knowledge search --query-history` removed** — the parameter never took effect; use `bl knowledge chat` with `--message` history for multi-turn scenarios.
|
||||
|
||||
### Internal
|
||||
|
||||
- Requests now carry a static OpenAPI source identification header for backend channel attribution.
|
||||
- Added knowledge-base E2E suites, including five user-journey scenarios covering cold start, content ops, chunk tuning, service tuning, and the data plane.
|
||||
|
||||
## [1.15.1] - 2026-08-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Model permission management** — `bl permission list` shows per-model inference / fine-tune / deploy grants; `bl permission grant` and `bl permission revoke` manage them, with `--all` to one-key grant inference for every model in the workspace (including future ones).
|
||||
|
||||
### Changed
|
||||
|
||||
- **`bl quota request` renamed to `bl quota update`** — set per-model QPM/TPM via `--rpm`/`--tpm` and clear custom limits with the new `--delete`; omitted fields keep their current values, and the old `quota request` path keeps working as an alias.
|
||||
- **`bl quota list` reworked** — now reads the model-limits API and shows per-model and workspace-level request/usage limits plus async queue/concurrency limits in a single table.
|
||||
- **`bl model list` no longer requires Console login** — the model catalog and `--enrich` parameter-schema endpoints are public.
|
||||
- **`bl skill init` output simplified** — per-skill status is now `success`/`failed` (previously `installed`) with an aggregate `success`/`partial`/`failed` result; the `publishedAt` and `agents` fields were removed.
|
||||
|
||||
## [1.15.0] - 2026-08-14
|
||||
|
||||
### Added
|
||||
|
||||
- **Responses API for `bl text chat`** — Use `--api responses` to call the DashScope Responses API with streaming, tool definitions, and structured JSON output; Chat Completions remains the default.
|
||||
- **Subscription plan usage views** — `bl usage token-plan` displays 5-hour and weekly quota usage, while `bl usage coding-plan` displays 5-hour, weekly, and monthly usage; both support text and JSON output.
|
||||
- **Authentication requirements in command help** — Help output now states whether a command requires an API Key, Console login, or Alibaba Cloud OpenAPI credentials.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Broader speech-recognition model support** — `bl speech recognize` now routes asynchronous file-transcription and synchronous Flash ASR models to the appropriate DashScope APIs, with clear guidance for unsupported realtime models.
|
||||
- **MCP transport compatibility** — MCP commands now fall back from Streamable HTTP to classic SSE for compatible Bailian and custom endpoints.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Binary updates now refresh installed Agent Skills after a successful CLI upgrade.
|
||||
- Fixed unavailable Token Plan quota values and missing reset times.
|
||||
- Fixed Qwen3 file-transcription result handling so waiting mode and `--out` work correctly.
|
||||
- Fixed MCP SSE chunk parsing, header timeouts, abort cleanup, and fallback status matching.
|
||||
- Network failures in JSON output now preserve the errno value in `cause.code`.
|
||||
|
||||
## [1.14.3] - 2026-08-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Free-tier quota compatibility** — `bl usage free` and `bl usage freetier` now use the current Bailian Commerce console APIs for quota queries, activation, and deactivation, with consistent asynchronous-task polling.
|
||||
|
||||
## [1.14.2] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl skill init`** — Install all first-party `bailian-*` skills into detected local AI Agents in one step.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Skill command interface** — Skill management commands now default to JSON output for Agent workflows; `bl skill add` and `bl skill update` use explicit `--all` and `--name` selectors.
|
||||
|
||||
## [1.14.1] - 2026-08-05
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,6 +6,79 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.16.0] - 2026-08-17
|
||||
|
||||
> CLI 迎来知识库全生命周期管理:从创建配置知识库、上传文档、调优切片,到部署检索/问答服务,均可通过 `bl knowledge` 与 `kscli` 完成。
|
||||
|
||||
### 新增
|
||||
|
||||
- **知识库管理** —— `bl knowledge create` / `list` / `info` / `update` / `delete` 覆盖知识库的完整生命周期;`bl knowledge stats` 查询指定过去时间段内的文档数量与用量统计。
|
||||
- **文档管理** —— `bl knowledge doc upload` 支持上传本地文件或整个目录(递归扫描,自动跳过不支持的格式及 `node_modules` 等工具目录);`doc list` / `status` / `tag` / `delete` 覆盖文档生命周期其余环节,`doc import-oss` 支持从 OSS 导入文档。
|
||||
- **检索 / 问答服务管理** —— `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` 管理检索与问答服务配置,支持将草稿部署为正式版本。
|
||||
- **切片管理** —— `bl knowledge chunk add` / `list` / `update` / `delete` 查看并精调文档切片。
|
||||
- **数据中心管理** —— `bl knowledge category list` / `add` / `delete`、`bl knowledge file list` / `get` / `delete`、`bl knowledge collection create` / `get` 管理类目、原始文件与数据集。
|
||||
- **检索与问答支持指定服务版本** —— `bl knowledge search` 和 `bl knowledge chat` 新增 `--agent-version`,可调用 beta(草稿)配置进行调试,或指定已发布的版本号。
|
||||
- **`kscli` 同步支持** —— 全部新知识库命令在 Knowledge Studio CLI 中以更短路径提供,如 `kscli kb list`、`kscli doc upload`、`kscli service deploy`。
|
||||
|
||||
### 移除
|
||||
|
||||
- **移除 `bl knowledge search --query-history`** —— 该参数此前并未实际生效;多轮场景请改用 `bl knowledge chat` 并通过 `--message` 传入对话历史。
|
||||
|
||||
### 内部
|
||||
|
||||
- 请求现在携带静态的 OpenAPI 来源标识请求头,用于后端渠道归因。
|
||||
- 新增知识库 E2E 测试套件,含冷启动、内容运营、切片调优、服务调优、数据面五条用户旅程场景。
|
||||
|
||||
## [1.15.1] - 2026-08-17
|
||||
|
||||
### 新增
|
||||
|
||||
- **模型权限管理** —— `bl permission list` 查看各模型的推理 / 微调 / 部署授权;`bl permission grant` 与 `bl permission revoke` 负责授予和回收,支持 `--all` 一键为工作区全部模型(含后续新增模型)开启推理授权。
|
||||
|
||||
### 变更
|
||||
|
||||
- **`bl quota request` 更名为 `bl quota update`** —— 通过 `--rpm`/`--tpm` 设置单模型 QPM/TPM,新增 `--delete` 一键清除自定义限制;未指定的字段保持当前值,旧命令 `quota request` 仍作为别名可用。
|
||||
- **`bl quota list` 重构** —— 改从模型限制接口读取数据,单表展示模型级与工作区级的请求/用量限制及异步队列/并发限制。
|
||||
- **`bl model list` 不再需要控制台登录** —— 模型目录与 `--enrich` 参数结构端点均为公开接口。
|
||||
- **`bl skill init` 输出精简** —— 单技能状态改为 `success`/`failed`(原为 `installed`),新增 `success`/`partial`/`failed` 汇总结果;移除 `publishedAt` 与 `agents` 字段。
|
||||
|
||||
## [1.15.0] - 2026-08-14
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl text chat` 支持 Responses API** —— 可通过 `--api responses` 调用 DashScope Responses API,支持流式输出、工具定义和结构化 JSON 输出;默认仍使用 Chat Completions。
|
||||
- **订阅套餐用量视图** —— `bl usage token-plan` 支持查看 5 小时和每周额度,`bl usage coding-plan` 支持查看 5 小时、每周和每月额度;两者均提供文本与 JSON 输出。
|
||||
- **命令帮助展示鉴权要求** —— Help 输出现在会明确标注命令需要 API Key、控制台登录还是阿里云 OpenAPI 凭证。
|
||||
|
||||
### 变更
|
||||
|
||||
- **扩展语音识别模型支持** —— `bl speech recognize` 现在会将异步文件转写和同步 Flash ASR 模型路由至对应的 DashScope API,并为暂不支持的实时模型提供明确提示。
|
||||
- **增强 MCP 传输兼容性** —— MCP 命令现在可为兼容的百炼及自定义端点从 Streamable HTTP 自动回退至经典 SSE。
|
||||
|
||||
### 修复
|
||||
|
||||
- 二进制方式升级 CLI 成功后,现在会同步刷新已安装的 Agent Skills。
|
||||
- 修复 Token Plan 额度不可用或缺少重置时间时的展示问题。
|
||||
- 修复 Qwen3 文件转写结果处理,使等待模式和 `--out` 能够正常工作。
|
||||
- 修复 MCP SSE 分块解析、响应头超时、中止清理和回退状态匹配问题。
|
||||
- JSON 输出中的网络错误现在会在 `cause.code` 中保留 errno。
|
||||
|
||||
## [1.14.3] - 2026-08-12
|
||||
|
||||
### 修复
|
||||
|
||||
- **免费额度兼容性** —— `bl usage free` 和 `bl usage freetier` 现在使用最新的 Bailian Commerce 控制台 API 查询、开通和关闭免费额度,并统一处理异步任务轮询。
|
||||
|
||||
## [1.14.2] - 2026-08-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl skill init`** —— 一次性将全部官方 `bailian-*` Skill 安装到本机检测到的 AI Agent。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Skill 命令接口** —— Skill 管理命令现在默认输出适合 Agent 工作流的 JSON;`bl skill add` 和 `bl skill update` 使用明确的 `--all` 与 `--name` 选择参数。
|
||||
|
||||
## [1.14.1] - 2026-08-05
|
||||
|
||||
### 新增
|
||||
|
||||
+34
-31
@@ -1,8 +1,38 @@
|
||||
# 阿里云百炼CLI 安装说明(供 AI Agent 阅读)
|
||||
|
||||
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**使用二进制一键安装(无需 Node);若环境已有 Node / 需要 Command Pack,再用 npm。不要臆造版本号或路径;以用户环境为准。
|
||||
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**在已有 Node.js(**≥ 18.17.0**)的环境用 npm 安装;若没有可用 Node / npm,再走二进制一键安装。不要臆造版本号或路径;以用户环境为准。
|
||||
|
||||
## 1. 推荐:二进制安装(无需 Node)
|
||||
## 1. 推荐:npm 安装(要求 **≥ 18.17.0**)
|
||||
|
||||
1. `node -v` 确认版本 ≥ 18.17.0。
|
||||
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn)。
|
||||
3. 执行:
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
4. 校验:`bl --version`。
|
||||
|
||||
安装 skills(CLI 内置,无需 Git / npx skills):
|
||||
|
||||
```bash
|
||||
bl skill init
|
||||
```
|
||||
|
||||
**Supported:** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
|
||||
|
||||
**Advanced / 按需子集:**
|
||||
|
||||
```bash
|
||||
bl skill add --name bailian-protocol,bailian-gen
|
||||
```
|
||||
|
||||
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
|
||||
|
||||
## 2. 备选:二进制安装(无需 Node)
|
||||
|
||||
当环境没有 Node / npm,或 Node 版本过低无法走 npm 时,使用二进制安装脚本。脚本安装 CLI 成功后会自动执行 `bl skill init`。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
@@ -37,36 +67,9 @@ bl --version
|
||||
which bl # Windows: where.exe bl
|
||||
```
|
||||
|
||||
> CDN / GitHub Release 未就绪或下载失败时,回退到下方 npm 安装。
|
||||
若自动 skill 安装失败,再手动执行:`bl skill init`。
|
||||
|
||||
## 2. 备选:npm 安装(要求 **≥ 18.17.0**)
|
||||
|
||||
1. `node -v` 确认版本。
|
||||
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn)。
|
||||
3. 执行:
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
4. 校验:`bl --version`。
|
||||
|
||||
可选 skills(与 CLI 本体无关,按需):
|
||||
|
||||
```bash
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```
|
||||
|
||||
**Supported:** 始终使用 `--all -g`,一次装齐整套 `bailian-*`(含共享协议 `bailian-protocol`)。Agent Skills / `npx skills` **不会**按 metadata 自动拉依赖。
|
||||
|
||||
**Advanced / 不推荐:** 子集 `-s` 时 skills CLI 不会自动带上 `bailian-protocol`;若坚持子集,必须手动同时指定,例如:
|
||||
|
||||
```bash
|
||||
# Advanced: you MUST include bailian-protocol yourself — installer does not pull it
|
||||
npx skills add modelstudioai/cli -g -s bailian-protocol -s bailian-gen
|
||||
```
|
||||
|
||||
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
|
||||
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
|
||||
---
|
||||
|
||||
_Chat with Qwen, generate images & videos, understand images, call agents,_
|
||||
_manage memory, search the web — all from your terminal._
|
||||
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
|
||||
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
|
||||
_every AI capability, one command away._
|
||||
|
||||
_Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
@@ -22,29 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
## Features
|
||||
|
||||
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
|
||||
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
|
||||
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
|
||||
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
|
||||
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
|
||||
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
|
||||
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
|
||||
|
||||
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
|
||||
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
|
||||
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
|
||||
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
|
||||
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
|
||||
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
|
||||
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
|
||||
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
|
||||
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
|
||||
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
|
||||
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
|
||||
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
|
||||
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
|
||||
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
|
||||
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
|
||||
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
|
||||
|
||||
## Showcase: One-Sentence Cinematic Video
|
||||
## Showcase 1: A Cinematic Short Film from One Sentence
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -57,136 +45,93 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
|
||||
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
|
||||
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
|
||||
>
|
||||
> _(Original: "帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2分钟左右的视频,尺寸是16:9")_
|
||||
|
||||
### How it works
|
||||
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
|
||||
|
||||
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
|
||||
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
|
||||
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
|
||||
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
|
||||
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
|
||||
|
||||
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Recommended — no Node required
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
**Agent install (recommended)**
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
|
||||
|
||||
# Node users / developers (Node.js >= 18.17)
|
||||
npm install -g bailian-cli
|
||||
|
||||
# Agent skills
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```text
|
||||
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
|
||||
```
|
||||
|
||||
> Binary install does not require Node.js. `npm install -g` remains fully supported.
|
||||
**Install with NPM**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> Requires Node.js >= 18.17.
|
||||
|
||||
**Install on macOS/Linux**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
**Install on Windows**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate, recommended
|
||||
bl auth login --console
|
||||
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
|
||||
|
||||
# Or authenticate with an API key
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# Or use Token Plan (Base URL built in; the key is tested during login)
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
# Configure a coding agent to use DashScope
|
||||
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
|
||||
|
||||
# Chat with Qwen
|
||||
bl text chat --message "What is DashScope?"
|
||||
|
||||
# Multimodal chat (text + image + audio + video)
|
||||
bl omni --message "Describe this image" --image ./photo.jpg
|
||||
|
||||
# Generate an image
|
||||
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
|
||||
|
||||
# Generate a video from local image
|
||||
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
|
||||
|
||||
# Model recommendation — find the best model for your use case
|
||||
bl advisor recommend --message "I need a visual-understanding chatbot"
|
||||
|
||||
# Compare specific models
|
||||
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
|
||||
|
||||
# Browser login (required for console capability commands)
|
||||
bl auth login --console
|
||||
|
||||
# Fine-tune & deploy — a one-shot train-to-serve workflow
|
||||
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
|
||||
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
|
||||
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
|
||||
bl finetune capability --model qwen3-8b # Which training types a model supports
|
||||
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
|
||||
|
||||
# Browse models / apps / free-tier quota / usage statistics / workspaces
|
||||
bl model list # Browse model families and pricing
|
||||
bl app list
|
||||
bl usage summary # Unified view: free-tier quota + recent usage overview
|
||||
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
|
||||
bl workspace list # List all workspaces
|
||||
|
||||
# Rate limit management (list / check / request / history)
|
||||
bl quota list # View RPM/TPM limits (add --model to filter)
|
||||
bl quota check # Current usage vs rate limits (add --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
|
||||
bl quota history # View quota-change history
|
||||
|
||||
# Asset center — browse, download, and manage model-generated assets (requires console login)
|
||||
bl asset-center list --type IMAGE
|
||||
bl asset-center get <asset-id> --include-download-url
|
||||
bl asset-center download --id <asset-id>
|
||||
bl asset-center stats
|
||||
bl asset-center storage
|
||||
|
||||
# Token Plan team management (requires AK/SK, see auth below)
|
||||
bl token-plan list-seats # View subscription seat details
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| Scenario | What to say to your Agent |
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| 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." |
|
||||
| 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." |
|
||||
|
||||
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## Authentication
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# Option 2: Login command (persisted to ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# Option 3: Per-command flag
|
||||
bl text chat --api-key sk-xxxxx --message "Hello"
|
||||
```
|
||||
|
||||
### Token Plan API Key
|
||||
|
||||
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
|
||||
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
|
||||
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
@@ -194,26 +139,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### Console Login (OAuth)
|
||||
|
||||
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). Opens the Bailian console in your browser to sign in.
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
|
||||
### Alibaba Cloud OpenAPI AK/SK
|
||||
|
||||
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
|
||||
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
|
||||
|
||||
```bash
|
||||
# Option 1: Login command (persisted to ~/.bailian/config.json)
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
|
||||
# Option 2: Environment variables
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -222,18 +161,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# View current config
|
||||
bl config show
|
||||
|
||||
# Set defaults
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# List all config profiles
|
||||
bl config list
|
||||
|
||||
# Self-update to latest or a specific version
|
||||
bl update
|
||||
bl update --to 0.1.14
|
||||
# Switch config profile
|
||||
bl config use --name token-plan
|
||||
```
|
||||
|
||||
Config file location: `~/.bailian/config.json`
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
|
||||
|
||||
## Links
|
||||
|
||||
| Resource | URL |
|
||||
@@ -245,11 +197,3 @@ Config file location: `~/.bailian/config.json`
|
||||
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## Changelog
|
||||
|
||||
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
+90
-147
@@ -22,29 +22,16 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
## 功能特性
|
||||
|
||||
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
|
||||
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
|
||||
- **素材理解** — 图像、文档、音频、长视频的解析与问答
|
||||
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
|
||||
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
|
||||
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
|
||||
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
|
||||
|
||||
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
|
||||
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
|
||||
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
|
||||
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
|
||||
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
|
||||
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
|
||||
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
|
||||
|
||||
> **注意:** 以下功能目前仅对中国站(aliyun.com)账号开放,国际站 / 全球站账号暂不支持。
|
||||
|
||||
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
|
||||
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
|
||||
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
|
||||
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
|
||||
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
|
||||
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT)、非阻塞探测任务状态(`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`)
|
||||
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`)
|
||||
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`)
|
||||
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
|
||||
|
||||
## 示例:一句话生成一部电影短片
|
||||
## 示例 1:一句话生成一部电影短片
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -54,137 +41,96 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
|
||||
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
|
||||
> _“帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9。”_
|
||||
|
||||
### 工作流程
|
||||
## 示例 2:一句话构建短片导演 Managed Agent
|
||||
|
||||
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
|
||||
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
|
||||
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**。
|
||||
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
|
||||
<p align="center"><i>👆 点击封面播放完整演示</i></p>
|
||||
|
||||
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _“帮我构建一个 managedagent 应用,能够实现短片拍摄,导演专家生成视频,然后也能进行设计对应的分镜图。”_
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 推荐 — 无需本机 Node.js
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
**Agent 安装(推荐)**
|
||||
|
||||
# Windows(PowerShell)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
把下面这句话发给你的 Agent,它会自行判断环境并完成安装与校验:
|
||||
|
||||
# Node 用户 / 开发者(需要 Node.js >= 18.17)
|
||||
npm install -g bailian-cli
|
||||
|
||||
# Agent skills
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```text
|
||||
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
|
||||
```
|
||||
|
||||
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
|
||||
**NPM 安装**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> 需要预先安装 Node.js >= 18.17。
|
||||
|
||||
**macOS/Linux 安装**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
**Windows 安装**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 认证(推荐浏览器登录)
|
||||
bl auth login --console
|
||||
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
|
||||
|
||||
# 或使用 API key 认证
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# 或使用 Token Plan(已内置 Base URL,登录时自动测试 Key)
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
# 配置 Coding Agent 使用 DashScope
|
||||
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
|
||||
|
||||
# 和通义千问对话
|
||||
bl text chat --message "你好,介绍一下阿里云百炼平台"
|
||||
|
||||
# 多模态对话(文本 + 图片 + 音频 + 视频)
|
||||
bl omni --message "描述这张图片" --image ./photo.jpg
|
||||
|
||||
# 生成图片
|
||||
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
|
||||
|
||||
# 图生视频(本地文件自动上传)
|
||||
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
|
||||
|
||||
# 模型推荐 — 根据场景推荐最适合的模型
|
||||
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
|
||||
|
||||
# 对比特定模型
|
||||
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
|
||||
|
||||
# 浏览器登录(控制台能力相关命令需要)
|
||||
bl auth login --console
|
||||
|
||||
# 微调与部署 — 从训练到服务的一站式流程
|
||||
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
|
||||
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
|
||||
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0;失败/取消报错)
|
||||
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
|
||||
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
|
||||
|
||||
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
|
||||
bl model list # 浏览模型系列与价格信息
|
||||
bl app list
|
||||
bl usage summary # 统一视图:免费额度 + 近期用量概览
|
||||
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
|
||||
bl workspace list # 列出所有业务空间
|
||||
|
||||
# 限流管理与提额(list / check / request / history)
|
||||
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
|
||||
bl quota check # 当前用量 vs 限流阈值(加 --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
|
||||
bl quota history # 查看提额历史记录
|
||||
|
||||
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
|
||||
bl asset-center list --type IMAGE
|
||||
bl asset-center get <asset-id> --include-download-url
|
||||
bl asset-center download --id <asset-id>
|
||||
bl asset-center stats
|
||||
bl asset-center storage
|
||||
|
||||
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
|
||||
bl token-plan list-seats # 查看订阅席位明细
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| 场景 | 可以这样对 Agent 说 |
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## 认证方式
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
|
||||
|
||||
```bash
|
||||
# 方式一:环境变量
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# 方式二:登录命令(持久化到 ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# 方式三:命令行参数
|
||||
bl text chat --api-key sk-xxxxx --message "你好"
|
||||
```
|
||||
|
||||
### Token Plan API Key
|
||||
|
||||
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
|
||||
CLI 已内置 Token Plan 的默认 Base URL;登录命令会先测试 Key,通过后才保存并激活 `token-plan` 配置。
|
||||
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
@@ -192,26 +138,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### 控制台登录(OAuth)
|
||||
|
||||
控制台能力命令(`model list`、`app list`、`usage summary/free/stats`、`workspace list`、`quota list/request/check/history`、`asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
|
||||
### 阿里云 OpenAPI AK/SK
|
||||
|
||||
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
|
||||
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
|
||||
|
||||
```bash
|
||||
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
|
||||
# 方式二:环境变量
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
```
|
||||
|
||||
## 配置
|
||||
@@ -220,20 +160,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# 查看当前配置
|
||||
bl config show
|
||||
|
||||
# 设置默认值
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# 查看全部配置档
|
||||
bl config list
|
||||
|
||||
# 自更新到最新版本
|
||||
bl update
|
||||
|
||||
# 安装指定版本
|
||||
bl update --to 0.1.14
|
||||
# 切换配置档
|
||||
bl config use --name token-plan
|
||||
```
|
||||
|
||||
配置文件位置:`~/.bailian/config.json`
|
||||
|
||||
## 更新
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群,获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
|
||||
|
||||
## 相关链接
|
||||
|
||||
| 资源 | 地址 |
|
||||
@@ -245,11 +196,3 @@ bl update --to 0.1.14
|
||||
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## 更新日志
|
||||
|
||||
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
@@ -1,574 +0,0 @@
|
||||
# bailian-cli 快速上手指南
|
||||
|
||||
> 本文档面向新加入项目的开发者,帮助你理解 monorepo 的整体架构、代码组织方式和日常开发流程。
|
||||
> AI Agent 维护契约见根目录 [`AGENTS.md`](../AGENTS.md);各场景的详细清单见 [`docs/agents/`](agents/)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目是什么
|
||||
|
||||
**bailian-cli** 是阿里云百炼(DashScope / Model Studio)平台的命令行工具,让用户和 AI Agent 通过终端调用平台的全部 AI 能力:
|
||||
|
||||
- 文本/全模态对话、图像/视频生成与编辑、语音合成与识别
|
||||
- 知识库检索、记忆管理、应用调用、MCP 集成
|
||||
- 微调与部署、数据集管理、配额与业务空间
|
||||
- 控制台能力(用量统计、限流提额、资产中心等)
|
||||
|
||||
仓库以 **pnpm monorepo** 组织,产出两个 npm 产品:
|
||||
|
||||
| 产品 | 包名 | 二进制 | 定位 |
|
||||
| -------------- | ---------------------- | ---------------- | ------------------------------------ |
|
||||
| 百炼全量 CLI | `bailian-cli` | `bl` / `bailian` | 暴露全部命令 |
|
||||
| 知识库轻量 CLI | `knowledge-studio-cli` | `kscli` | 仅 config + knowledge 命令,路径拍平 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 类别 | 选型 |
|
||||
| --------- | -------------------------------------------------------------------------------------------- |
|
||||
| 语言 | TypeScript(strict) |
|
||||
| 运行时 | Node.js ≥ 22.12 |
|
||||
| 包管理 | pnpm 10 + workspace catalog |
|
||||
| 构建/测试 | [vite-plus](https://github.com/voidzero-dev/vite-plus)(`vp check` / `vp test` / `vp pack`) |
|
||||
| HTTP | undici(经 core client 封装) |
|
||||
| 模块 | ESM(`"type": "module"`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心架构:四层分层
|
||||
|
||||
项目按 **「纯逻辑 → 运行时框架 → 命令库 → 产品入口」** 严格分层,职责边界清晰:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 产品入口层 │
|
||||
│ packages/cli (bl) packages/kscli (kscli) │
|
||||
│ 决定命令路径 map、产品 identity、README、技能 reference │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ createCli(commands, identity)
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 运行时框架层 packages/runtime (bailian-cli-runtime) │
|
||||
│ 参数解析、命令树/registry、help、middleware、错误处理、输出 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ 调用 defineCommand 的 run()
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 命令库层 packages/commands (bailian-cli-commands) │
|
||||
│ 96+ 命令实现;只导出 command,不决定产品路径 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ client / settings / auth
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 纯逻辑层 packages/core (bailian-cli-core) │
|
||||
│ 鉴权、配置、HTTP client、错误、类型、文件工具、领域 API │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 分层边界(必须遵守)
|
||||
|
||||
| 层 | 可以做 | 不能做 |
|
||||
| --------------- | --------------------------- | ------------------------------------------------------------- |
|
||||
| **core** | 纯库逻辑、HTTP、鉴权解析 | 依赖 runtime/commands;硬编码 `bl`/`kscli`;调 `process.exit` |
|
||||
| **runtime** | TTY、help、middleware、输出 | 写具体业务命令逻辑 |
|
||||
| **commands** | 命令元数据 + `run` 实现 | 决定产品路径;在 usage 里写 bin 前缀 |
|
||||
| **cli / kscli** | 命令路径 map、产品 identity | 不写命令业务逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 包详解
|
||||
|
||||
### 4.1 `packages/core` — `bailian-cli-core`
|
||||
|
||||
纯逻辑层,被所有上层依赖。主要模块:
|
||||
|
||||
```
|
||||
packages/core/src/
|
||||
├── auth/ # API Key / Console token 解析与落盘
|
||||
├── client/ # HTTP client、endpoints、MCP、流式解析
|
||||
├── config/ # ~/.bailian/config.json、Settings、来源优先级
|
||||
├── console/ # Console Gateway 调用
|
||||
├── dataset/ # 数据集校验(ChatML/DPO/CPT schema)
|
||||
├── finetune/ # 微调 API 与能力探测
|
||||
├── deploy/ # 部署 API
|
||||
├── advisor/ # 模型推荐(意图识别 + 召回)
|
||||
├── errors/ # BailianError、UsageError、退出码
|
||||
├── output/ # JSON/text 格式化(命令层也可用 runtime 的 emit)
|
||||
├── files/ # 本地文件上传、URL 解析
|
||||
├── telemetry/ # 命令执行遥测
|
||||
└── types/ # Command、FlagsDef、defineCommand
|
||||
```
|
||||
|
||||
**关键类型** — 每个命令通过 `defineCommand` 声明:
|
||||
|
||||
```typescript
|
||||
defineCommand({
|
||||
description: "…",
|
||||
auth: "apiKey" | "console" | "none",
|
||||
flags: {
|
||||
/* camelCase key → kebab-case CLI flag */
|
||||
},
|
||||
usageArgs: "--prompt <text> [flags]", // 不含 bl/kscli 前缀
|
||||
exampleArgs: ['--prompt "hello"'],
|
||||
validate: (flags) => string | undefined, // 跨 flag 校验
|
||||
run: async (ctx) => {
|
||||
/* ctx.client / ctx.flags / ctx.settings */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Client** 是命令的网络入口,凭证已注入,命令层不碰 token:
|
||||
|
||||
```typescript
|
||||
ctx.client.requestJson({ path: "/…", method: "POST", body });
|
||||
ctx.client.console({ product: "…", action: "…", params });
|
||||
ctx.client.uploadFile(localPath);
|
||||
ctx.client.mcp(…);
|
||||
```
|
||||
|
||||
### 4.2 `packages/runtime` — `bailian-cli-runtime`
|
||||
|
||||
通用 CLI 框架,与具体业务无关。核心文件:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ------------------ | ----------------------------------------------- |
|
||||
| `create-cli.ts` | 入口工厂:`createCli(commands, identity).run()` |
|
||||
| `registry.ts` | 从 `Record<string, AnyCommand>` 建树,动态 help |
|
||||
| `args.ts` | 路径 + flag 解析 |
|
||||
| `middleware.ts` | auth → telemetry → versionCheck → runCommand |
|
||||
| `error-handler.ts` | 统一错误输出与退出码 |
|
||||
| `urls.ts` | 用户面控制台 URL(非 API endpoint) |
|
||||
| `output/` | 颜色、表格、进度条、banner |
|
||||
| `pipeline/` | 多步 pipeline 编排(`bl pipeline run`) |
|
||||
|
||||
**Middleware 流水线**(洋葱模型):
|
||||
|
||||
```
|
||||
argv 解析
|
||||
→ authStage 按 command.auth 注入 apiKey / console 凭证到 ctx.client
|
||||
→ telemetryStage 记录命令执行
|
||||
→ versionCheckStage 检查 npm 更新
|
||||
→ runCommandStage 调用 command.run(ctx)
|
||||
```
|
||||
|
||||
### 4.3 `packages/commands` — `bailian-cli-commands`
|
||||
|
||||
命令实现库,按**能力域**组织目录(≠ 最终 CLI 路径):
|
||||
|
||||
```
|
||||
packages/commands/src/commands/
|
||||
├── text/ # 文本对话
|
||||
├── omni/ # 全模态对话
|
||||
├── image/ # 图像生成/编辑
|
||||
├── video/ # 视频生成/编辑/下载
|
||||
├── speech/ # 语音合成/识别
|
||||
├── vision/ # 图像/视频理解
|
||||
├── knowledge/ # 知识库检索/搜索/对话
|
||||
├── memory/ # 记忆管理
|
||||
├── app/ # 应用调用
|
||||
├── mcp/ # MCP 服务
|
||||
├── auth/ # 登录/登出/状态
|
||||
├── config/ # 配置读写
|
||||
├── console/ # 通用 Console Gateway 调用
|
||||
├── dataset/ # 数据集上传/校验
|
||||
├── finetune/ # 微调任务
|
||||
├── deploy/ # 模型部署
|
||||
├── quota/ # 限流与提额
|
||||
├── workspace/ # 业务空间
|
||||
├── usage/ # 用量统计
|
||||
├── advisor/ # 模型推荐
|
||||
├── asset-center/ # 资产中心(新)
|
||||
├── pipeline/ # Pipeline 编排
|
||||
├── search/ # 联网搜索
|
||||
├── file/ # 文件上传
|
||||
├── token-plan/ # Token 计划
|
||||
└── update.ts # 自更新
|
||||
```
|
||||
|
||||
每个命令文件 `export default defineCommand(…)`,并在 `packages/commands/src/index.ts` 具名 re-export。
|
||||
|
||||
### 4.4 `packages/cli` — `bailian-cli`(`bl`)
|
||||
|
||||
产品入口,极薄:
|
||||
|
||||
```typescript
|
||||
// packages/cli/src/main.ts
|
||||
createCli(commands, {
|
||||
binName: "bl",
|
||||
version: pkg.version,
|
||||
clientName: "bailian-cli",
|
||||
npmPackage: "bailian-cli",
|
||||
}).run();
|
||||
```
|
||||
|
||||
**命令路径由 `packages/cli/src/commands.ts` 决定**,例如:
|
||||
|
||||
```typescript
|
||||
export const commands: Record<string, AnyCommand> = {
|
||||
"text chat": textChat,
|
||||
"asset-center list": assetList,
|
||||
"finetune create": finetuneCreate,
|
||||
update, // 单级命令 key 即路径
|
||||
};
|
||||
```
|
||||
|
||||
此文件还被 `tools/generate-reference.ts` 读取,生成 Agent Skill 参考文档。
|
||||
|
||||
### 4.5 `packages/kscli` — `knowledge-studio-cli`(`kscli`)
|
||||
|
||||
轻量 RAG 产品,**复用同一套 commands**,但路径拍平:
|
||||
|
||||
```typescript
|
||||
const commands = {
|
||||
retrieve: knowledgeRetrieve, // ↔ bl knowledge retrieve
|
||||
search: knowledgeSearch, // ↔ bl knowledge search
|
||||
chat: knowledgeChat, // ↔ bl knowledge chat
|
||||
"config show": configShow,
|
||||
update,
|
||||
};
|
||||
```
|
||||
|
||||
同一个 `knowledgeRetrieve` 实现,在 `bl` 显示 `bl knowledge retrieve`,在 `kscli` 显示 `kscli retrieve`——路径完全由产品入口 map 的 key 决定。
|
||||
|
||||
---
|
||||
|
||||
## 5. 一次命令执行的完整链路
|
||||
|
||||
以 `bl text chat --message "hi"` 为例:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant main as cli/main.ts
|
||||
participant createCli as runtime/create-cli.ts
|
||||
participant registry as runtime/registry.ts
|
||||
participant mw as middleware
|
||||
participant cmd as commands/text/chat.ts
|
||||
participant client as core/client
|
||||
|
||||
User->>main: bl text chat --message "hi"
|
||||
main->>createCli: createCli(commands, identity).run(argv)
|
||||
createCli->>registry: 解析路径 ["text","chat"]
|
||||
registry-->>createCli: 匹配 textChat command
|
||||
createCli->>mw: authStage → 注入 apiKey 到 client
|
||||
mw->>cmd: run(ctx)
|
||||
cmd->>client: requestJson / parseSSE
|
||||
client-->>User: stdout 输出
|
||||
```
|
||||
|
||||
**配置与凭证解析优先级**(core 统一处理,命令不介入):
|
||||
|
||||
| 来源 | API Key | Console Token |
|
||||
| ---- | ----------------------- | ------------------------------------- |
|
||||
| 1 | `--api-key` flag | `~/.bailian/config.json` access_token |
|
||||
| 2 | `DASHSCOPE_API_KEY` env | — |
|
||||
| 3 | config.json `api_key` | — |
|
||||
|
||||
Console 命令额外有 `--console-region`、`--workspace-id` 等 flag(由 runtime 按 `auth: "console"` 自动展示)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 鉴权域
|
||||
|
||||
每个命令声明 `auth` 字段,runtime 自动处理:
|
||||
|
||||
| auth 值 | 适用场景 | 凭证来源 | 网络方法 |
|
||||
| ----------- | ------------------------------ | -------------------- | -------------------------------- |
|
||||
| `"apiKey"` | DashScope API(模型推理等) | API Key | `client.request` / `requestJson` |
|
||||
| `"console"` | Console Gateway(控制台能力) | Console access token | `client.console` |
|
||||
| `"none"` | 纯本地(config、update、help) | 无 | 可选 credential-less client |
|
||||
|
||||
**规则**:调用 Console Gateway 的命令必须 `auth: "console"`,且**不要**重复声明 console 凭证域 flags。
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理约定
|
||||
|
||||
CLI **只翻译自己能权威解释的错误**,服务端错误原样透传:
|
||||
|
||||
| 错误来源 | 处理 |
|
||||
| ---------------------- | -------------------------------- |
|
||||
| 缺参、flag 校验 | `UsageError` → 退出码 2 |
|
||||
| 本地无凭证 | `BailianError(AUTH)` |
|
||||
| 网络/DNS/TLS | `BailianError(NETWORK)` |
|
||||
| HTTP 4xx/5xx、业务错码 | message **原样透传**,不二次包装 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 开发工作流
|
||||
|
||||
### 8.1 环境准备
|
||||
|
||||
```bash
|
||||
# 要求 Node >= 22.12, pnpm >= 10
|
||||
pnpm install
|
||||
|
||||
# 格式化 + lint + 类型检查
|
||||
pnpm run check # 或 vp check
|
||||
|
||||
# 本地跑 bl(tsx 直跑,无需 build)
|
||||
pnpm run bl -- text chat --help
|
||||
pnpm run kscli -- search --help
|
||||
|
||||
# 全量测试
|
||||
pnpm test # 或 vp test
|
||||
|
||||
# 构建所有包
|
||||
pnpm run ready # check + test + build
|
||||
```
|
||||
|
||||
### 8.2 新增一个 `bl` 命令(最小路径)
|
||||
|
||||
假设新增 `bl widget do`:
|
||||
|
||||
**Step 1** — 实现命令(`packages/commands`)
|
||||
|
||||
```bash
|
||||
# 新建
|
||||
packages/commands/src/commands/widget/do.ts
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { defineCommand, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const FLAGS = {
|
||||
name: { type: "string", valueHint: "<name>", description: "Widget name", required: true },
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Do something with a widget",
|
||||
auth: "apiKey", // 或 "console" / "none"
|
||||
flags: FLAGS,
|
||||
usageArgs: "--name <name>",
|
||||
exampleArgs: ['--name "demo"'],
|
||||
async run(ctx) {
|
||||
const data = await ctx.client.requestJson({ path: "/…", method: "POST", body: { … } });
|
||||
emitResult(ctx, data);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2** — 导出(`packages/commands/src/index.ts`)
|
||||
|
||||
```typescript
|
||||
export { default as widgetDo } from "./commands/widget/do.ts";
|
||||
```
|
||||
|
||||
**Step 3** — 注册产品路径(`packages/cli/src/commands.ts`)
|
||||
|
||||
```typescript
|
||||
import { widgetDo } from "bailian-cli-commands";
|
||||
// …
|
||||
"widget do": widgetDo,
|
||||
```
|
||||
|
||||
**Step 4** — E2E 测试(`packages/cli/tests/e2e/widget.e2e.test.ts`)
|
||||
|
||||
见 [docs/agents/cli-e2e-tests.md](agents/cli-e2e-tests.md):至少覆盖分组 help、`--help`、缺参用例。
|
||||
|
||||
**Step 5** — 验证
|
||||
|
||||
```bash
|
||||
vp check
|
||||
vp test
|
||||
pnpm run bl -- widget do --help
|
||||
```
|
||||
|
||||
> 若 `kscli` 也需要暴露:在 `packages/kscli/src/main.ts` 的 map 里加 key。
|
||||
> 技能 reference 会在 pre-commit 时由 `generate-reference.ts` 自动从 `commands.ts` 生成。
|
||||
|
||||
详细清单 → [docs/agents/command-add-remove.md](agents/command-add-remove.md)
|
||||
|
||||
### 8.3 给已有命令加 flag
|
||||
|
||||
→ [docs/agents/command-flag-change.md](agents/command-flag-change.md)
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试体系
|
||||
|
||||
```
|
||||
packages/cli/tests/
|
||||
├── e2e/ # 33 个 e2e 测试文件
|
||||
│ ├── helpers.ts # runCli、环境变量 readiness 判断
|
||||
│ ├── global-setup.ts
|
||||
│ └── <topic>.e2e.test.ts
|
||||
└── stress/ # 多能力并发压测
|
||||
├── run.mjs
|
||||
└── targets/
|
||||
```
|
||||
|
||||
**E2E 双层结构**(固定模式):
|
||||
|
||||
```typescript
|
||||
// 层 1:永远跑 — help / 分组,无需 API Key
|
||||
describe("e2e: asset-center", () => {
|
||||
test("asset-center 分组展示子命令帮助且成功退出", …);
|
||||
test("asset-center list --help 正常退出", …);
|
||||
});
|
||||
|
||||
// 层 2:skipIf 缺凭证 — dry-run / 真实集成
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: asset-center(Console …)", () => {
|
||||
test("缺少 --asset-id 时退出为用法错误 (2)", …);
|
||||
test("真实 list 流程", …);
|
||||
});
|
||||
```
|
||||
|
||||
环境变量(常用):
|
||||
|
||||
| 变量 | 用途 |
|
||||
| --------------------------------------------- | ------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | 模型 API 集成测试 |
|
||||
| Console token(经 `bl auth login --console`) | 控制台命令测试 |
|
||||
| `BAILIAN_E2E_*` | 各能力开关(视频/媒体等) |
|
||||
|
||||
压测:`pnpm run test:stress`
|
||||
|
||||
---
|
||||
|
||||
## 10. 命令能力地图(`bl` 全量)
|
||||
|
||||
当前 `packages/cli/src/commands.ts` 注册的命令组:
|
||||
|
||||
| 命令组 | 子命令示例 | auth 域 |
|
||||
| -------------- | ------------------------------------------------------------------------------- | ---------------- |
|
||||
| `auth` | login, status, logout | none / console |
|
||||
| `text` | chat | apiKey |
|
||||
| `omni` | (全模态对话) | apiKey |
|
||||
| `image` | generate, edit | apiKey |
|
||||
| `video` | generate, edit, ref, task get, download | apiKey |
|
||||
| `vision` | describe | apiKey |
|
||||
| `speech` | synthesize, recognize | apiKey |
|
||||
| `knowledge` | retrieve, search, chat | apiKey |
|
||||
| `memory` | add, search, list, update, delete, profile create/get | apiKey |
|
||||
| `app` | call, list | apiKey / console |
|
||||
| `mcp` | call, list, tools | apiKey |
|
||||
| `search` | web | apiKey |
|
||||
| `file` | upload | apiKey |
|
||||
| `config` | show, set | none |
|
||||
| `console` | call | console |
|
||||
| `usage` | free, freetier, stats | console |
|
||||
| `workspace` | list | console |
|
||||
| `quota` | list, request, history, check | console |
|
||||
| `dataset` | upload, list, get, delete, validate | console |
|
||||
| `finetune` | create, list, get, cancel, delete, logs, checkpoints, export, watch, capability | console |
|
||||
| `deploy` | create, list, get, models, scale, update, delete | console |
|
||||
| `token-plan` | list-seats, create-key, assign-seats, add-member | console |
|
||||
| `asset-center` | list, get, favorite, unfavorite, delete, download, stats, storage | console |
|
||||
| `pipeline` | run, validate | apiKey |
|
||||
| `advisor` | recommend | apiKey |
|
||||
| `update` | (自更新) | none |
|
||||
|
||||
---
|
||||
|
||||
## 11. 非代码资产
|
||||
|
||||
```
|
||||
tools/
|
||||
├── generate-reference.ts # 从 cli/commands.ts → skills/bailian-cli/reference/
|
||||
├── sync-skill-metadata.ts # 同步 SKILL.md 版本号
|
||||
└── release/ # CI 发版自动化
|
||||
|
||||
skills/bailian-cli/ # Agent Skill(npx skills add modelstudioai/cli)
|
||||
.github/workflows/ # CI/CD(publish.yml 等)
|
||||
docs/agents/ # 各维护场景的 AI 清单
|
||||
```
|
||||
|
||||
根脚本:
|
||||
|
||||
```bash
|
||||
pnpm run sync:skill-assets # build + 生成 reference + 同步版本
|
||||
pnpm run release:check # 发版前校验
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 发布
|
||||
|
||||
- 版本号:`packages/core`、`runtime`、`commands`、`cli`、`kscli` **保持同步**
|
||||
- 发布范围:`tools/release/lib/packages.mjs` 定义
|
||||
- `bailian-cli` 走常规定义发布;`knowledge-studio-cli` 走 `--knowledge` 通道
|
||||
- 详见 [docs/agents/publish.md](agents/publish.md)
|
||||
|
||||
---
|
||||
|
||||
## 13. 关键文件速查
|
||||
|
||||
| 我想… | 看这里 |
|
||||
| ------------------- | --------------------------------------- |
|
||||
| 了解项目契约 | `AGENTS.md` |
|
||||
| 改 `bl` 命令路径 | `packages/cli/src/commands.ts` |
|
||||
| 写/改命令逻辑 | `packages/commands/src/commands/<域>/` |
|
||||
| 导出命令 | `packages/commands/src/index.ts` |
|
||||
| 改 CLI 框架行为 | `packages/runtime/src/` |
|
||||
| 改 HTTP/鉴权/配置 | `packages/core/src/` |
|
||||
| 改 kscli 路径 | `packages/kscli/src/main.ts` |
|
||||
| 加 E2E 测试 | `packages/cli/tests/e2e/` |
|
||||
| 改控制台 URL | `packages/runtime/src/urls.ts` |
|
||||
| 改 API endpoint | `packages/core/src/client/endpoints.ts` |
|
||||
| 改配置 schema | `packages/core/src/config/schema.ts` |
|
||||
| 生成 Agent 参考文档 | `tools/generate-reference.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 14. 场景导航(维护清单)
|
||||
|
||||
| 场景 | 文档 |
|
||||
| ----------- | ------------------------------------------------------- |
|
||||
| 命令增删改 | [command-add-remove.md](agents/command-add-remove.md) |
|
||||
| E2E 测试 | [cli-e2e-tests.md](agents/cli-e2e-tests.md) |
|
||||
| 加/改 flag | [command-flag-change.md](agents/command-flag-change.md) |
|
||||
| 模型上下架 | [model-add-remove.md](agents/model-add-remove.md) |
|
||||
| 错误文案 | [error-hint-change.md](agents/error-hint-change.md) |
|
||||
| 鉴权扩展 | [auth-change.md](agents/auth-change.md) |
|
||||
| 配置项扩展 | [config-add.md](agents/config-add.md) |
|
||||
| 发布 | [publish.md](agents/publish.md) |
|
||||
| 工具链/lint | [lint-toolchain.md](agents/lint-toolchain.md) |
|
||||
|
||||
---
|
||||
|
||||
## 15. 架构设计要点(读懂代码的钥匙)
|
||||
|
||||
1. **命令实现 ≠ 产品路径** — 同一 `knowledgeRetrieve` 可以是 `bl knowledge retrieve` 或 `kscli retrieve`。
|
||||
2. **defineCommand 是契约** — `auth` + `flags` + `run(ctx)` 是命令的全部接口;凭证和网络细节下沉到 core/runtime。
|
||||
3. **registry 从 map 建树** — `"asset-center list"` 等 path 自动变成命令组,help 动态生成。
|
||||
4. **flags 用 camelCase 定义** — runtime 渲染为 `--kebab-case`;`ParsedFlags<typeof FLAGS>` 提供类型安全。
|
||||
5. **dry-run 是全局 flag** — `--dry-run` 在 auth stage 有例外处理,命令在 `run` 开头判断 `ctx.settings.dryRun`。
|
||||
6. **本地路径即 URL** — 所有接受 URL 的参数同时支持本地文件路径,core `files/upload` 自动上传。
|
||||
7. **Console Gateway 统一入口** — 控制台 API 走 `client.console({ product, action, params })`,不散落 raw fetch。
|
||||
|
||||
---
|
||||
|
||||
## 16. 本地配置速览
|
||||
|
||||
配置文件:`~/.bailian/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"api_key": "sk-…",
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"access_token": "…",
|
||||
"console_region": "cn-beijing",
|
||||
"console_site": "domestic"
|
||||
}
|
||||
```
|
||||
|
||||
常用环境变量:
|
||||
|
||||
| 变量 | 说明 |
|
||||
| ---------------------------- | -------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | 模型 API Key |
|
||||
| `DASHSCOPE_BASE_URL` | API Base URL |
|
||||
| `BAILIAN_WORKSPACE_ID` | 业务空间 ID |
|
||||
| `HTTP_PROXY` / `HTTPS_PROXY` | 代理(runtime 启动时读取) |
|
||||
|
||||
登录:
|
||||
|
||||
```bash
|
||||
bl auth login # API Key
|
||||
bl auth login --console # Console token(扫码)
|
||||
bl auth status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_文档版本:基于仓库当前结构(含 `asset-center`、`kscli`);`packages/rag` 已演进为 `packages/kscli`。_
|
||||
@@ -25,7 +25,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
当前 command 鉴权域(`AuthRequirement`):
|
||||
|
||||
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
|
||||
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent/workspace
|
||||
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
|
||||
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
|
||||
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
|
||||
|
||||
@@ -35,7 +35,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
|
||||
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
|
||||
- `bl auth login --open-api ...` 只更新 `access_key_id` / `access_key_secret`
|
||||
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
|
||||
- `bl auth logout --console` 只清 `access_token`
|
||||
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
|
||||
- `bl auth logout` 清 `api_key` + `base_url` + `access_token` + `access_key_*`
|
||||
@@ -78,6 +78,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
- 如新增鉴权域,扩展 `AuthRequirement`
|
||||
- 更新 `credentialFlagDefs()` 暴露该域可见的 flag
|
||||
- 必要时新增 `*_AUTH_FLAGS`
|
||||
- `workspace_id` 是作用域字段而非 credential,不要把它放进 `ConsoleCredential`;读取方式按命令 `auth` 域区分:
|
||||
- `auth: "console"` 命令通过 `CONSOLE_AUTH_FLAGS` 自动获得 `--workspace-id`,由 `buildSettings()` 解析到 `settings.workspaceId`,命令统一从 `settings.workspaceId` 读取
|
||||
- `auth: "apiKey"`/`"openapi"`/`"none"` 命令如需 `--workspace-id`,必须自声明 flag;因它不会进入 credential/global flags,命令从 `ctx.flags.workspaceId` 读取(可回退到 `settings.workspaceId`)
|
||||
- [ ] `packages/core/src/auth/types.ts`:
|
||||
- 新增 credential 类型 / source / scope 字段
|
||||
- [ ] `packages/core/src/auth/resolver.ts`:
|
||||
@@ -131,6 +134,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
## 完成后自查
|
||||
|
||||
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
|
||||
|
||||
```sh
|
||||
# 各种凭证组合
|
||||
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
||||
@@ -150,9 +155,11 @@ Console 登录/网关相关改动:
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli exec tsx src/main.ts auth login --console
|
||||
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json
|
||||
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
注意:`usage stats --dry-run` 仍会先校验 workspace,必须传入 `--workspace-id`(或 `BAILIAN_WORKSPACE_ID` / config `workspace_id`)。
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 加了新 token 来源但忘了改 resolver 优先级,实际不生效
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
@@ -27,7 +28,7 @@
|
||||
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
@@ -78,6 +79,14 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
|
||||
## Journey 层(用户旅程全链路)
|
||||
|
||||
- **定位**:命令 E2E 验单命令契约;journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
|
||||
- **闭环断言**:fixture 埋独特标记词,以“标记词能否被召回”判定回路闭合;硬断言 fail,软断言 `recordSoft` 落报告人工复核
|
||||
- **日志产物**:`createJourneyReporter` 在 `test/output/<session>/` 落盘 `journey-report.md`、分步 stdout/stderr、`resources.json`(未清理资源警示)
|
||||
- **入口**:`pnpm run test:journey`;旅程清单与约定见 [journeys/README.md](../../packages/commands/tests/e2e/knowledge/journeys/README.md)
|
||||
- **新增命令时**:评估是否属于某条旅程的环节,是则纳入对应 journey 并更新 README 映射表
|
||||
|
||||
## 增删命令同步
|
||||
|
||||
- **commands export** + **topic 路由**(`topic-routes.ts` 或测试文件内 `ROUTES`)+ **产品 map**(`cli/commands.ts` / `kscli/commands.ts`)
|
||||
|
||||
+12
-11
@@ -11,16 +11,17 @@
|
||||
|
||||
## 统一口径(安装)
|
||||
|
||||
1. **Supported install:** `npx skills add modelstudioai/cli --all -g`(整包装齐,含 `bailian-protocol`)
|
||||
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它;Agent Skills / `npx skills` **不会**按 frontmatter 自动拉依赖
|
||||
1. **Supported install:** `bl skill init`(装齐 registry 中全部 `bailian-*`,含 `bailian-protocol`)
|
||||
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它
|
||||
3. **不要**在 frontmatter 写 `companions`,也不要对外说「companions = 安装器硬依赖」
|
||||
4. 子集安装(`-s`)为 **advanced / 不推荐**:skills CLI 不会自动带上 protocol;漏装会导致相对路径 Read 失败
|
||||
4. 子集安装:`bl skill add --name bailian-protocol,<skill>`;漏装 protocol 会导致相对路径 Read 失败
|
||||
5. **`bl skill add --all`:** 安装 registry 全量(含 `spark-video` 等非 bailian 技能);一键安装 / `bl update` 用 `skill init`,不要用 `--all`
|
||||
|
||||
## 概念图
|
||||
|
||||
```text
|
||||
bailian-protocol ← 共享协议(consent / 鉴权 / 版本 / 错误上报)
|
||||
▲ 靠 --all -g 与业务 skill 同装;非安装器强制 companions
|
||||
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
|
||||
│
|
||||
┌───────┴────────┬────────────────┬──────────────────┐
|
||||
bailian-gen bailian-finetune bailian-managed-agent
|
||||
@@ -37,8 +38,8 @@ bailian-gen bailian-finetune bailian-managed-agent
|
||||
|
||||
### A. 分层边界
|
||||
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `--all -g`;业务 skill **不**声明 `companions`
|
||||
- [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `npx skills add modelstudioai/cli --all -g`
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
|
||||
- [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
|
||||
- [ ] **软 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」指向句
|
||||
@@ -46,9 +47,9 @@ bailian-gen bailian-finetune bailian-managed-agent
|
||||
### B. 文案与落款一致性
|
||||
|
||||
- [ ] 领域 skill(gen / finetune / managed-agent)路由或命令表后有指向 `reference/` 的句;文末 `## references`(protocol + reference)与家族对齐
|
||||
- [ ] description 含 WHAT + WHEN + 反触发;安装说明指向 `--all -g`,不写 companions 必装
|
||||
- [ ] description 含 WHAT + WHEN + 反触发;安装说明指向 `bl skill init`,不写 companions 必装
|
||||
- [ ] Quick examples 只演示本 skill 职责(hub 不示范 `bl image` / `bl video` 等)
|
||||
- [ ] 若改了安装方式:同步 `README.md` / `README.zh.md` / `INSTALL.md` / `skills/*/README*` / `skills/bailian-protocol/assets/setup.md` 中的 `npx skills add …` 示例(改 `INSTALL.md` 时按 [install-doc-change.md](install-doc-change.md) 同步静态页)
|
||||
- [ ] 若改了安装方式:同步 `README.md` / `README.zh.md` / `INSTALL.md` / `skills/*/README*` / `skills/bailian-protocol/assets/setup.md` 中的 `bl skill init` / `bl skill add …` 示例(改 `INSTALL.md` 时按 [install-doc-change.md](install-doc-change.md) 同步静态页)
|
||||
|
||||
### C. 归属与生成
|
||||
|
||||
@@ -60,8 +61,8 @@ bailian-gen bailian-finetune bailian-managed-agent
|
||||
|
||||
```sh
|
||||
pnpm run sync:skill-assets
|
||||
# 本地试装(测本仓库改动,勿只拉远端)
|
||||
npx skills add "$(pwd)" --all -g -y
|
||||
# 已发布版本试装
|
||||
bl skill init
|
||||
```
|
||||
|
||||
抽查:打开 `skills/bailian-cli/SKILL.md` 确认无领域子命令明细表、无 `companions`;打开对应领域 skill 确认有「勿猜 flag」与 hand-off。
|
||||
@@ -69,7 +70,7 @@ npx skills add "$(pwd)" --all -g -y
|
||||
## 常见漏点
|
||||
|
||||
- ✗ hub 路由表再次抄回 image / video / finetune / managed-agent 明细 → token 膨胀且与领域 skill 双份漂移
|
||||
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 Agent Skills / `npx skills` 合同不符
|
||||
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 `bl skill add` 合同不符
|
||||
- ✗ 软 hand-off 写成硬路径 `../bailian-*/SKILL.md` 当执行前提 → 子集安装断链
|
||||
- ✗ 只改 SKILL、忘改 `GROUP_OWNER_SKILL` → reference 落错 skill
|
||||
- ✗ 手改 `skills/*/reference/*.md` → 下次 generate 被覆盖
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# Chunk 管理命令手册
|
||||
|
||||
Chunk 是知识库中最小的检索单元。文档导入后自动切分为 chunk,也可以手动添加。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否² | 所属文档 ID;表格/图片知识库必填,文档型可选 |
|
||||
| `--content <text>` | string | 否¹ | Chunk 正文,最多 6000 字符(文档型);与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取正文(`.md`/`.txt` 等);与 `--content` 互斥 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,最多 50 字符(文档型) |
|
||||
| `--image-url <url>` | array | 否 | Chunk 图片 URL(可重复,最多 10 个;文档型) |
|
||||
| `--field <key=value>` | array | 否¹ | 任意字段键值对(可重复),用于表格/图片知识库,键为 Excel 列名;与 content/title/image 互斥 |
|
||||
|
||||
> ¹ `--content`/`--content-file`/`--title`/`--image-url` 与 `--field` 互斥,必须提供其一。
|
||||
> ² 表格/图片知识库必须提供 `--doc-id`。文档型知识库可选。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--field` 与 `--content`/`--content-file`/`--title`/`--image-url` 互斥
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--content` 最多 6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
- `--image-url` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
chunk created (pipeline: idx-xxx)
|
||||
List chunks to find the new chunk id.
|
||||
```
|
||||
|
||||
quiet 模式:无输出(成功退出码 0)。
|
||||
|
||||
json 模式:返回 API 原始响应(不含 chunk ID)。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 支持文档/表格/图片知识库;音视频知识库不支持。
|
||||
- API 响应不含 chunk ID,需用 `chunk list` 查找新 chunk。
|
||||
- API 幂等但限流 10 次/秒,批量脚本需自行节流。
|
||||
- 表格/图片知识库用 `--field`,键为 Excel 列名,值为字符串。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 添加文本 chunk
|
||||
bl knowledge chunk add --index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx
|
||||
|
||||
# 添加表格行(字段方式)
|
||||
bl knowledge chunk add --index-id idx-xxx --field 列A=v1 --field 列B=v2
|
||||
|
||||
# 从文件读取内容
|
||||
bl knowledge chunk add --index-id idx-xxx --content-file ./chunk.md --doc-id doc-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否 | 只显示属于此文档的 chunk |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
[chunk] chunk-xxx (doc: intro.md, doc_id: file-xxx) status: COMPLETED
|
||||
chunk content preview (truncated at 200 chars)…
|
||||
total: 1
|
||||
```
|
||||
|
||||
> 如果 chunk 被排除检索,行尾会显示 `[excluded from retrieval]`。
|
||||
|
||||
quiet 模式:每行一个 `metadata._id`(chunk ID),用于管道传给 update/delete。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 含完整 chunk 数据。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用 `metadata._id` 作为 chunk ID,`metadata.doc_id` 作为文档 ID,在 chunk update/delete 中使用。
|
||||
- 页大小默认 20,最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有 chunk
|
||||
bl knowledge chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看某文档的 chunk
|
||||
bl knowledge chunk list --index-id idx-xxx --doc-id file-xxx --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | string | 是 | Chunk ID(`metadata._id`,来自 chunk list 输出) |
|
||||
| `--doc-id <id>` | string | 是 | 所属文档 ID(`metadata.doc_id`,来自 chunk list 输出) |
|
||||
| `--content <text>` | string | 否¹ | 新内容,10-6000 字符;与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取新内容 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,0-50 字符(空字符串清除标题;不传则不变) |
|
||||
| `--exclude` | switch | 否² | 将此 chunk 排除出检索 |
|
||||
| `--include` | switch | 否² | 将此 chunk 恢复检索(默认行为) |
|
||||
|
||||
> ¹ `--content` 与 `--content-file` 互斥。
|
||||
> ² `--exclude` 与 `--include` 互斥。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--exclude` 与 `--include` 互斥
|
||||
- 至少提供一个更新项(`--content`/`--content-file`/`--title`/`--exclude`/`--include`)
|
||||
- `--content` 长度 10-6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: chunk-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 内容必须 10-6000 字符,且不超过知识库的 max chunk size。
|
||||
- `--content-file` 期望 UTF-8 纯文本文件,不解析 `.docx`/`.pdf` 等文档格式。
|
||||
- 仅切换 `--exclude`/`--include` 而不提供新内容时,CLI 自动读回当前内容并重新提交(API 要求 content 字段必填,CLI 隐藏了此限制)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 修改内容
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text" --workspace-id ws-xxx
|
||||
|
||||
# 排除 chunk 不参与检索
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude
|
||||
|
||||
# 恢复检索
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --include
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
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 | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 chunk(s) in 1 batch(es)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 `{ deleted_count, batches }`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端每次最多接受 10 个 chunk ID,CLI 自动分批。
|
||||
- 如果某批失败,操作停止,已删除的批次会在错误 hint 中列出。
|
||||
- Chunk 被永久移除,不可恢复。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除多个 chunk
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,268 @@
|
||||
# 数据中心集合与分类命令手册
|
||||
|
||||
集合(collection)是数据中心的顶层容器,对应服务端的 connector。分类(category)用于组织集合内的文件,支持多级嵌套。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 集合名称(1-20 字符) |
|
||||
| `--description <text>` | string | 是 | 集合描述 |
|
||||
| `--store-type <type>` | string | 否 | 存储类型:`platform`(托管,默认)或 `custom`(自有 OSS bucket) |
|
||||
| `--oss-region <id>` | string | 否 | OSS region ID(`--store-type custom` 时必填) |
|
||||
| `--oss-bucket <name>` | string | 否 | OSS bucket 名称(`--store-type custom` 时必填) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--store-type` 只能是 `platform` 或 `custom`
|
||||
- `--store-type custom` 时 `--oss-region` 和 `--oss-bucket` 必填
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: conn-xxx (my-collection, PLATFORM)
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `platform` 使用平台托管存储;`custom` 使用已授权的 OSS bucket。
|
||||
- 自定义 bucket 必须携带标签 `bailian-connector-access=ReadAndWrite`(百炼的标签访问控制),否则服务端报 `setBucketCORS failed` 误导性错误。
|
||||
- **无集合删除 API**,创建需谨慎。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建平台托管的集合
|
||||
bl knowledge collection create --name my-collection --description "team docs" --workspace-id ws-xxx
|
||||
|
||||
# 创建使用自有 OSS bucket 的集合
|
||||
bl knowledge collection create --name oss-coll --description "own bucket" --store-type custom --oss-region cn-beijing --oss-bucket my-bucket
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------- |
|
||||
| `--collection-id <id>` | string | 否¹ | 集合 ID |
|
||||
| `--name <text>` | string | 否¹ | 集合名称 |
|
||||
|
||||
> ¹ `--collection-id` 和 `--name` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--collection-id` 和 `--name` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: conn-xxx
|
||||
name: my-collection
|
||||
description: team docs
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- getConnector 不返回 `fileConnectorConfig`(`storeType`/`regionId`/`bucketName`),这些字段仅在创建时通过请求体传入,查询时不可读回。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 按 ID 查询
|
||||
bl knowledge collection get --collection-id conn-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称查询
|
||||
bl knowledge collection get --name my-collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--collection-id <id>` | string | 否 | 按集合 ID 过滤 |
|
||||
| `--parent-id <id>` | string | 否 | 列出此分类的子分类 |
|
||||
| `--name <text>` | string | 否 | 按分类名称过滤(精确匹配,与知识库列表的模糊匹配不同) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌 |
|
||||
| `--max-result <n>` | number | 否 | 每页条数(默认:20) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
cate-xxx product-docs
|
||||
cate-yyy system-docs [default]
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
> 标记 `[default]` 的是文件未指定分类时的默认归属。
|
||||
|
||||
quiet 模式:每行一个 `categoryId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有分类
|
||||
bl knowledge category list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
bl knowledge category list --name my-category
|
||||
|
||||
# 翻页
|
||||
bl knowledge category list --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category add --name <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------- |
|
||||
| `--name <text>` | string | 是 | 分类名称(1-20 字符) |
|
||||
| `--parent-id <id>` | string | 否 | 创建为指定分类的子分类 |
|
||||
| `--collection-id <id>` | string | 否 | 创建在此集合下(默认:平台集合) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: cate-xxx (product-docs)
|
||||
```
|
||||
|
||||
quiet 模式:输出分类 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用分类按业务域组织数据中心文件。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建分类
|
||||
bl knowledge category add --name product-docs --workspace-id ws-xxx
|
||||
|
||||
# 创建子分类
|
||||
bl knowledge category add --name sub --parent-id cate-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------ |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: cate-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 含文件或子分类的分类的删除行为由服务端定义——服务端错误原样透传。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除分类(交互确认)
|
||||
bl knowledge category delete --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge category delete --category-id cate-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,344 @@
|
||||
# 文档管理命令手册
|
||||
|
||||
文档管理覆盖文件上传、OSS 导入、解析状态跟踪、文档删除和标签管理。文档导入知识库后自动解析为 chunk。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个文档,`FAILED` 状态的文档红色高亮。
|
||||
|
||||
```
|
||||
doc-xxx COMPLETED intro.md md 1024
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `doc_id` 与 `file_id` 的关系:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;通过 `knowledge doc upload --index-id` 导入的,`doc_id` 可能包含 workspace 后缀。
|
||||
- 页大小默认 10(服务端默认),最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出文档
|
||||
bl knowledge doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 每页 100 条
|
||||
bl knowledge doc list --index-id idx-xxx --page-size 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--job-id <id>` | string | 是 | 导入任务 ID(`ingestionId`,由 create/upload 返回) |
|
||||
| `--page-number <n>` | number | 否 | 页码 |
|
||||
| `--page-size <n>` | number | 否 | 每页条数 |
|
||||
| `--wait` | switch | 否 | 轮询直到任务到达终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
status: COMPLETED
|
||||
doc-xxx COMPLETED intro.md
|
||||
```
|
||||
|
||||
quiet 模式:输出任务状态(`PENDING`/`RUNNING`/`COMPLETED`)。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含每个文档的状态。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--index-id` 和 `--job-id` 服务端均要求必传,只传一个会返回 `SystemError`。
|
||||
- 整体任务状态为 `PENDING` / `RUNNING` / `COMPLETED`(无 `FAILED` 值)。
|
||||
- 单个文档可能解析失败(如 `PARSE_FAILED`),此时 CLI 以非零退出码报错,服务端消息原样透传。
|
||||
- 如果服务端对空闲知识库返回 `SystemError`,说明该 job 可能不存在。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看任务状态
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
|
||||
# 轮询等待完成,10 秒间隔
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --wait --poll-interval 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--file <path>` | array | 是 | 本地文件或目录路径(可重复)。目录递归扫描,不支持的格式自动跳过 |
|
||||
| `--index-id <id>` | string | 否 | 上传后导入到此知识库(所有文件合并为一个导入任务) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:工作区默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复),应用到每个上传的文件 |
|
||||
| `--wait` | switch | 否 | 轮询导入任务直到终态(需要 `--index-id`) |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--wait` 要求同时指定 `--index-id`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
intro.md file-xxx registered
|
||||
job: job-xxx
|
||||
status: COMPLETED
|
||||
|
||||
Uploaded 1 file.
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回自定义结构,包含 `files`(路径和 fileId)、`skipped`、`index_id`、`ingestion_id`、`final_status`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 上传管道:申请 lease → PUT 到 OSS → 注册文件 →(可选)创建导入任务。
|
||||
- 目录递归扫描,`node_modules`、`.git` 等自动跳过。
|
||||
- 多文件按顺序处理(无并发),避免 OSS 限流。
|
||||
- 支持的文件格式:`.pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`
|
||||
- 部分文件上传失败时,已注册的 fileId 会在错误 hint 中列出。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 上传单个文件
|
||||
bl knowledge doc upload --file ./a.md --workspace-id ws-xxx
|
||||
|
||||
# 上传多个文件并导入到知识库,等待完成
|
||||
bl knowledge doc upload --file ./a.md --file ./b.pdf --index-id idx-xxx --wait
|
||||
|
||||
# 上传整个目录
|
||||
bl knowledge doc upload --file ./docs/ --workspace-id ws-xxx
|
||||
|
||||
# 干跑预览(查看将上传和跳过的文件)
|
||||
bl knowledge doc upload --file ./docs/ --dry-run --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 document(s)
|
||||
doc-a
|
||||
doc-b
|
||||
```
|
||||
|
||||
quiet 模式:每行一个已删除的 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID 列表。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 只从知识库索引中移除文档,数据中心源文件不受影响(用 `file delete` 删除源文件)。
|
||||
- `doc_id` 应从 `knowledge doc list --quiet` 获取,而非 `doc upload` 返回的 `fileId`。
|
||||
- 删除是异步的:服务端立即返回 Success,但 `doc list` 中可能仍显示该文档(约 30 秒后传播完成)。
|
||||
- 输出的是服务端实际删除的 ID 列表,可能与请求的数量不一致(会在 stderr 警告)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除单个文档
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--doc-id <id>` | array | 是 | 数据中心文件 ID(可重复,最多 20 个/次) |
|
||||
| `--tag <text>` | array | 是 | 标签(可重复),应用到每个 `--doc-id` |
|
||||
| `--mode <mode>` | string | 否 | 更新模式:`append`(默认,追加)或 `overwrite`(覆盖) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--doc-id` 最多 20 个/次
|
||||
- `--tag` 最多 100 个
|
||||
- 每个标签最多 32 字符
|
||||
- 标签总长度最多 700 字符
|
||||
- `--mode` 只能是 `append` 或 `overwrite`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
tagged: 2 file(s) with [project-a, draft]
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 同一组标签应用到所有 `--doc-id`;不同标签集需多次执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 追加标签
|
||||
bl knowledge doc tag --doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx
|
||||
|
||||
# 覆盖标签
|
||||
bl knowledge doc tag --doc-id file-a --doc-id file-b --tag final --mode overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------------------------------- |
|
||||
| `--bucket <name>` | string | 是 | 已授权的 OSS bucket 名称 |
|
||||
| `--region <id>` | string | 是 | OSS region ID(如 `cn-beijing`) |
|
||||
| `--oss-key <key>` | array | 是 | OSS 对象 key(可重复,最多 10 个/次) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复,最多 10 个) |
|
||||
| `--overwrite` | switch | 否 | 覆盖之前从相同 OSS key 导入的文件 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--oss-key` 最多 10 个/次
|
||||
- `--tag` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
imported: 2 file(s)
|
||||
file-a SUCCESS docs/a.pdf
|
||||
file-b SUCCESS docs/b.docx
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.addFileResultList[]` 包含每个文件的 fileId、status 和 ossKey。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- bucket 必须事先授权给平台服务角色(RAM 中的 `AliyunServiceRoleForBailian`)。
|
||||
- 文件名取自 OSS key 的 basename。
|
||||
- `--overwrite` 会替换之前导入的文件并生成**新的 fileId**(旧 fileId 失效)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 导入单个文件
|
||||
bl knowledge doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx
|
||||
|
||||
# 导入多个文件并覆盖
|
||||
bl knowledge doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,157 @@
|
||||
# 数据中心文件管理命令手册
|
||||
|
||||
数据中心是知识库文件的存储层。文件通过 `doc upload` 或 `doc import-oss` 进入数据中心,再导入到知识库。数据中心文件可被多个知识库引用。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID(通过 `category list` 或 `file get` 获取) |
|
||||
| `--name <text>` | string | 否 | 按文件名过滤 |
|
||||
| `--file-id <id>` | array | 否 | 按文件 ID 过滤(可重复) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌(从上次输出获取) |
|
||||
| `--max-result <n>` | number | 否 | 每页条数 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
file-xxx SUCCESS intro.md 1024
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--category-id` 必须是真实的分类 ID。与上传 API 不同,字面量 `default` 在此不被解析,传入会返回空列表。通过 `file get` 的 category 字段或 `category list` 获取真实 ID。
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出分类下文件
|
||||
bl knowledge file list --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
bl knowledge file list --category-id cate-xxx --name report
|
||||
|
||||
# 翻页
|
||||
bl knowledge file list --category-id cate-xxx --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: file-xxx
|
||||
name: intro.md
|
||||
type: md
|
||||
size: 1024
|
||||
status: SUCCESS
|
||||
parser: AUTO_SELECT
|
||||
category: cate-xxx
|
||||
uploaded: 2026-01-01T00:00:00Z
|
||||
tags: project-a, draft
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 无特殊注意事项。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看文件详情
|
||||
bl knowledge file get --file-id file-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: file-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:如果知识库引用了此文件,相关文档索引会失效。
|
||||
- 与 `doc delete` 的区别:`doc delete` 只从单个知识库索引中移除文档,数据中心源文件保留;`file delete` 删除源文件本身,影响所有引用它的知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除文件(交互确认)
|
||||
bl knowledge file delete --file-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge file delete --file-id file-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,340 @@
|
||||
# 知识库管理命令手册
|
||||
|
||||
知识库(Knowledge Base / pipeline / index)是 RAG 的核心载体,存储文档解析后的向量索引。本组命令覆盖知识库的创建、查看、更新、删除和监控。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | --------------------------------- |
|
||||
| `--name <text>` | string | 否 | 按知识库名称模糊过滤(1-20 字符) |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个知识库,字段以双空格分隔,末尾显示总数。
|
||||
|
||||
```
|
||||
idx-xxx my-kb text-embedding-v4 600 product docs
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 返回的 `id` 字段作为后续命令的 `--index-id` 使用。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有知识库
|
||||
bl knowledge list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤,第二页
|
||||
bl knowledge list --name demo --page-number 2 --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:按诊断维度分组展示。
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: idx-xxx
|
||||
name: my-kb
|
||||
description: product docs
|
||||
dataType: ...
|
||||
Indexing: [immutable — recreate required to change]
|
||||
embeddingModelName: text-embedding-v4
|
||||
embeddingDimension: 1024
|
||||
chunkSize: 600
|
||||
overlapSize: ...
|
||||
chunkMode: ...
|
||||
separator: ...
|
||||
Retrieval:
|
||||
rerankModelName: ...
|
||||
rerankMinScore: ...
|
||||
rerankTopN: ...
|
||||
rerankMode: ...
|
||||
enableRewrite: ...
|
||||
denseSimilarityTopK: ...
|
||||
sparseSimilarityTopK: ...
|
||||
Data:
|
||||
sourceType: ...
|
||||
connectorId: ...
|
||||
```
|
||||
|
||||
quiet 模式:输出知识库 ID。
|
||||
|
||||
json 模式:返回知识库完整配置 JSON。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看知识库详情
|
||||
bl knowledge info --index-id idx-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge create --name <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 知识库名称(1-20 字符,工作区内唯一) |
|
||||
| `--doc-id <id>` | array | 否¹ | 数据中心文件 ID(可重复);与 `--category-id` 互斥 |
|
||||
| `--category-id <id>` | array | 否¹ | 按分类导入该分类下所有文件(可重复);与 `--doc-id` 互斥 |
|
||||
| `--embedding-model <name>` | string | 否 | 向量模型名称(默认:`text-embedding-v4`) |
|
||||
| `--chunk-size <n>` | number | 否 | 切片大小,字符数(默认:600,建议 300-800) |
|
||||
| `--wait` | switch | 否 | 轮询初始导入任务直到终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
> ¹ `--doc-id` 和 `--category-id` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--doc-id` 和 `--category-id` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
index_id: idx-xxx
|
||||
ingestion_id: job-xxx
|
||||
status: COMPLETED
|
||||
Next: check the import job status, then search against this knowledge base.
|
||||
```
|
||||
|
||||
quiet 模式:只输出知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `pipelineId`(知识库 ID)和 `ingestionId`(导入任务 ID)。`--wait` 时追加 `final_status` 字段。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 结构/存储类型固定为默认文档知识库(非结构化,BUILT_IN 存储)。
|
||||
- 返回知识库 ID(`pipelineId`)和初始导入任务 ID(`ingestionId`)。
|
||||
- 使用 `doc status` 或 `--wait` 跟踪导入进度。
|
||||
- 如果 `--wait` 后部分文档解析失败,CLI 以非零退出码报错,知识库已创建成功的事实会在 hint 中提示。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 从指定文件创建知识库
|
||||
bl knowledge create --name demo --doc-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 从分类导入并等待导入完成
|
||||
bl knowledge create --name demo --category-id cate-xxx --wait
|
||||
|
||||
# 指定向量模型和切片大小
|
||||
bl knowledge create --name my-kb --doc-id file-a --doc-id file-b --embedding-model text-embedding-v4 --chunk-size 400 --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--name <text>` | string | 否 | 新名称(1-20 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述 |
|
||||
| `--rerank-min-score <score>` | number | 否 | rerank 最低分数阈值,范围 0-1(低于此分的 chunk 被过滤) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供 `--name`、`--description`、`--rerank-min-score` 之一,否则报错 "Nothing to update"
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--rerank-min-score` 范围 0-1
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 更新描述
|
||||
bl knowledge update --index-id idx-xxx --description "product docs v2" --workspace-id ws-xxx
|
||||
|
||||
# 调整 rerank 阈值
|
||||
bl knowledge update --index-id idx-xxx --rerank-min-score 0.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
bl knowledge delete --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge delete --index-id idx-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--start <time>` | string | 否 | 范围起始:Unix 秒或 ISO 日期(默认:24 小时前) |
|
||||
| `--end <time>` | string | 否 | 范围结束:Unix 秒或 ISO 日期(默认:当前时间) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
plan: ...
|
||||
storage: 100 / 1000
|
||||
peak qps: 5
|
||||
qps windows: 24 data point(s)
|
||||
```
|
||||
|
||||
quiet 模式:输出 json 格式。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `storageMonitorData` 和 `qpsMonitorData`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 默认查询最近 24 小时数据。
|
||||
- 时间戳自动转换为 epoch 秒(API 要求秒级字符串)。13 位毫秒时间戳会自动降为秒。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看最近 24 小时监控
|
||||
bl knowledge stats --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 指定日期范围
|
||||
bl knowledge stats --index-id idx-xxx --start 2026-07-30 --end 2026-07-31
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,822 @@
|
||||
# `bl knowledge` 命令完整用法指南
|
||||
|
||||
> `bl knowledge` / `kscli` 知识库 CLI 命令总览,覆盖全部 34 个子命令。完整参数与示例请参阅各子域手册。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [核心概念与实体关系](#核心概念与实体关系)
|
||||
3. [通用约定](#通用约定)
|
||||
4. [典型工作流](#典型工作流)
|
||||
5. [命令手册](#命令手册)
|
||||
- [知识库管理](#知识库管理) → [完整手册](knowledge/kb.md)
|
||||
- [文档管理](#文档管理) → [完整手册](knowledge/doc.md)
|
||||
- [检索服务管理](#检索服务管理) → [完整手册](knowledge/service.md)
|
||||
- [Chunk 管理](#chunk-管理) → [完整手册](knowledge/chunk.md)
|
||||
- [数据中心文件管理](#数据中心文件管理) → [完整手册](knowledge/file.md)
|
||||
- [数据中心集合与分类](#数据中心集合与分类) → [完整手册](knowledge/collection-category.md)
|
||||
- [检索与对话](#检索与对话) → [完整手册](knowledge/search-chat.md)
|
||||
6. [常见错误与排查](#常见错误与排查)
|
||||
7. [附录:命令速查表](#附录命令速查表)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
`bl knowledge` 是阿里云百炼 CLI 的知识库命令组,覆盖 RAG(检索增强生成)全链路能力:
|
||||
|
||||
- **知识库全生命周期管理**:创建、查看、更新、删除、监控
|
||||
- **文档管理**:上传本地文件、从 OSS 批量导入、查看解析状态、删除、打标签
|
||||
- **Chunk 级运维**:直接增删改查知识库中的内容切片
|
||||
- **检索服务管理**:创建/部署/复制/删除 Q&A 和检索服务(agent),管理 draft 与发布版本
|
||||
- **数据中心管理**:文件、集合(connector)、分类的增删查
|
||||
- **检索与对话**:语义检索(search)、多轮对话(chat)、兼容旧检索(retrieve)
|
||||
|
||||
共 34 个子命令,按功能域分为 7 组。所有命令均使用 DashScope API Key 鉴权。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念与实体关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据中心 (Data Center) │
|
||||
│ │
|
||||
│ 集合 (Collection) ──┬── 分类 (Category) ── 文件 (File) │
|
||||
│ │ "connector" 可多级嵌套 │
|
||||
│ └── 默认分类 │
|
||||
│ │
|
||||
│ 文件来源:doc upload(本地上传) / doc import-oss(OSS导入) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 导入 (import job)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 知识库 (Knowledge Base) │
|
||||
│ │
|
||||
│ 知识库 (KB / pipeline / index) │
|
||||
│ ├── 文档 (Doc) ── 解析状态: PENDING/RUNNING/COMPLETED │
|
||||
│ │ └── Chunk ── 内容切片,可增删改查、排除/恢复检索 │
|
||||
│ └── 索引设置 (immutable): 向量模型、切片大小等 │
|
||||
│ │
|
||||
│ 知识库管理命令: create / list / info / update / delete / stats │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 绑定 (agent_config.kb_search_configs)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 检索服务 (Service / Agent) │
|
||||
│ │
|
||||
│ Service (agent) │
|
||||
│ ├── scene: chat (Q&A) 或 search (检索) │
|
||||
│ ├── 版本: beta (草稿) → 1, 2, 3... (已发布) │
|
||||
│ ├── 状态: draft → deployed → edited → deleted │
|
||||
│ └── 配置: 模型、温度、策略、rerank 等 │
|
||||
│ │
|
||||
│ 消费方式: search (语义检索) / chat (多轮对话) │
|
||||
│ 管理命令: create / update / deploy / copy / delete / list / get │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键关系**:
|
||||
|
||||
- **数据中心文件 → 知识库**:通过 `knowledge create --doc-id` 或 `knowledge doc upload --index-id` 导入,文件解析后自动生成 chunk
|
||||
- **知识库 → 检索服务**:一个服务可绑定多个知识库,服务配置中 `kb_search_configs` 指定关联的知识库 ID
|
||||
- **检索服务 → 检索/对话**:`search` 和 `chat` 命令通过 `--agent-id` 指定服务来执行检索或对话
|
||||
|
||||
---
|
||||
|
||||
## 通用约定
|
||||
|
||||
### 鉴权
|
||||
|
||||
所有 `bl knowledge` 命令均使用 **DashScope API Key**(Bearer token)鉴权。获取方式:百炼控制台 API Key 页面。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--api-key <key>` 命令行参数
|
||||
2. `DASHSCOPE_API_KEY` 环境变量
|
||||
3. 配置文件中的 `api_key`(`bl config set api_key <key>`)
|
||||
|
||||
### Workspace ID
|
||||
|
||||
知识库 API 使用 workspace 级域名(`{workspaceId}.cn-beijing.maas.aliyuncs.com`),因此 **几乎所有 knowledge 命令都需要 workspace ID**。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--workspace-id <id>` 命令行参数
|
||||
2. `BAILIAN_WORKSPACE_ID` 环境变量
|
||||
3. 配置文件中的 `workspace_id`(`bl config set workspace_id <id>`)
|
||||
|
||||
缺失时报错:`Workspace ID is required.`
|
||||
|
||||
### 全局通用参数
|
||||
|
||||
以下参数在所有 `bl knowledge` 子命令中通用,后续命令手册中不再逐条列出:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------------------- | ------ | ----------------------------------------------------------- |
|
||||
| `--output <format>` | string | 输出格式:`text`(默认,人类友好)或 `json`(API 原始响应) |
|
||||
| `--api-key <key>` | string | DashScope API Key |
|
||||
| `--base-url <url>` | string | API 基地址(一般不需要指定) |
|
||||
| `--timeout <seconds>` | number | 请求超时秒数 |
|
||||
| `--quiet` | switch | 静默模式,只输出关键结果(如 ID 列表) |
|
||||
| `--verbose` | switch | 详细模式,打印 HTTP 请求/响应详情到 stderr |
|
||||
| `--dry-run` | switch | 干跑模式,预览将发送的请求结构,不实际调用 API |
|
||||
| `--config <name>` | string | 使用指定配置 profile 执行命令 |
|
||||
|
||||
> **注意**:命令手册中每个命令的参数表只列出该命令**特有**的参数。上述全局参数对所有命令有效。
|
||||
|
||||
### 输出格式约定
|
||||
|
||||
- **text 模式**(默认):人类友好的表格/结构化文本,适合终端查看。不同命令的输出格式见各命令的「输出」部分。
|
||||
- **json 模式**(`--output json`):返回 API 原始 JSON 响应,适合程序化处理和 agent 解析。
|
||||
- **quiet 模式**(`--quiet`):只输出最精简的结果(通常只有 ID),适合管道串联。
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
`--dry-run` 模式下,命令会输出将发送的 endpoint 和 request body,但**不实际发起网络请求**。部分命令在 dry-run 下仍会执行本地校验(如文件扩展名检查、参数约束检查)。
|
||||
|
||||
---
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 场景 A:从零搭建知识库并检索
|
||||
|
||||
```bash
|
||||
# 1. 上传本地文件到数据中心,同时导入到新知识库
|
||||
bl knowledge doc upload --file ./docs/intro.md --workspace-id ws-xxx
|
||||
# → 返回 file-id
|
||||
|
||||
# 2. 用文件创建知识库
|
||||
bl knowledge create --name my-kb --doc-id file-xxx --workspace-id ws-xxx --wait
|
||||
# → 返回 index-id (pipelineId) 和导入任务状态
|
||||
|
||||
# 3. 创建检索服务(search 场景)
|
||||
bl knowledge service create --name my-search --scene search --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 agent-id
|
||||
|
||||
# 4. 部署服务
|
||||
bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx --yes
|
||||
|
||||
# 5. 执行检索
|
||||
bl knowledge search --query "什么是RAG" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 B:上传目录并导入到已有知识库
|
||||
|
||||
```bash
|
||||
# 1. 上传整个目录到数据中心并直接导入到知识库(一步到位)
|
||||
bl knowledge doc upload --file ./docs/ --index-id idx-xxx --workspace-id ws-xxx --wait
|
||||
# → 文件逐个上传到 OSS → 注册到数据中心 → 创建合并导入任务 → 轮询到完成
|
||||
|
||||
# 2. 检查文档状态
|
||||
bl knowledge doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 查看 doc_id 和解析状态
|
||||
|
||||
# 3. 如果有文档解析失败,查看导入任务详情
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 C:创建并部署 Q&A 服务
|
||||
|
||||
```bash
|
||||
# 1. 创建 chat 场景的检索服务
|
||||
bl knowledge service create --name my-qa --scene chat --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 初始状态: draft, 版本: beta
|
||||
|
||||
# 2. 调整配置(如修改模型、温度)
|
||||
bl knowledge service update --agent-id aid-xxx --model qwen-max --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 3. 用 beta 版本测试
|
||||
bl knowledge chat --message "什么是RAG?" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
|
||||
# 4. 测试通过后发布
|
||||
bl knowledge service deploy --agent-id aid-xxx --version-desc "首版" --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 D:知识库内容运维
|
||||
|
||||
```bash
|
||||
# 1. 查看 chunk 列表
|
||||
bl knowledge chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 metadata._id (chunk id) 和 metadata.doc_id (document id)
|
||||
|
||||
# 2. 修改 chunk 内容
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --content "修正后的内容" --workspace-id ws-xxx
|
||||
|
||||
# 3. 排除某个 chunk 不参与检索(不删除内容)
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --exclude --workspace-id ws-xxx
|
||||
|
||||
# 4. 手动添加新 chunk
|
||||
bl knowledge chunk add --index-id idx-xxx --content "新增的知识片段" --title "补充说明" --workspace-id ws-xxx
|
||||
|
||||
# 5. 删除 chunk(批量,自动分批每 10 个一组)
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --yes --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 E:服务迁移/复用
|
||||
|
||||
```bash
|
||||
# 1. 复制现有服务为新草稿
|
||||
bl knowledge service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
# → 返回新的 agent-id,名称加 copy_ 前缀
|
||||
|
||||
# 2. 修改新服务配置
|
||||
bl knowledge service update --agent-id aid-new --name "改进版" --temperature 0.5 --workspace-id ws-xxx
|
||||
|
||||
# 3. 测试并发布
|
||||
bl knowledge chat --message "测试" --agent-id aid-new --agent-version beta --workspace-id ws-xxx
|
||||
bl knowledge service deploy --agent-id aid-new --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 F:从 OSS 批量导入文件
|
||||
|
||||
```bash
|
||||
# 1. 从已授权的 OSS bucket 批量导入文件到数据中心
|
||||
bl knowledge doc import-oss \
|
||||
--bucket my-bucket --region cn-beijing \
|
||||
--oss-key docs/a.pdf --oss-key docs/b.docx \
|
||||
--workspace-id ws-xxx
|
||||
# → 返回各文件的 fileId
|
||||
|
||||
# 2. 创建知识库并导入这些文件
|
||||
bl knowledge create --name oss-kb --doc-id file-a --doc-id file-b --workspace-id ws-xxx --wait
|
||||
|
||||
# 3. 检索
|
||||
bl knowledge search --query "相关内容" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 命令手册
|
||||
|
||||
以下按功能域分组,覆盖全部 34 个子命令。每个条目包含功能说明、用法签名(kscli 前缀)和详细手册链接。
|
||||
|
||||
> 完整参数表、参数约束、输出说明、注意事项与示例请参阅各子域手册。子域手册中的用法签名使用 `bl knowledge` 前缀。
|
||||
|
||||
---
|
||||
|
||||
### 知识库管理
|
||||
|
||||
> 📖 [完整手册](knowledge/kb.md) — 6 个命令
|
||||
|
||||
#### `kscli kb list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
```bash
|
||||
kscli kb list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
```bash
|
||||
kscli kb info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-info)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
```bash
|
||||
kscli kb create --name <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
```bash
|
||||
kscli kb update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
```bash
|
||||
kscli kb delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
```bash
|
||||
kscli kb stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-stats)
|
||||
|
||||
---
|
||||
|
||||
### 文档管理
|
||||
|
||||
> 📖 [完整手册](knowledge/doc.md) — 6 个命令
|
||||
|
||||
#### `kscli doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
```bash
|
||||
kscli doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
```bash
|
||||
kscli doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-status)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
```bash
|
||||
kscli doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-upload)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
```bash
|
||||
kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
```bash
|
||||
kscli doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-tag)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
```bash
|
||||
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-import-oss)
|
||||
|
||||
---
|
||||
|
||||
### 检索服务管理
|
||||
|
||||
> 📖 [完整手册](knowledge/service.md) — 7 个命令
|
||||
|
||||
#### `kscli service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
```bash
|
||||
kscli service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
```bash
|
||||
kscli service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
```bash
|
||||
kscli service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
```bash
|
||||
kscli service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
```bash
|
||||
kscli service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-deploy)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
```bash
|
||||
kscli service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
```bash
|
||||
kscli service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-copy)
|
||||
|
||||
---
|
||||
|
||||
### Chunk 管理
|
||||
|
||||
> 📖 [完整手册](knowledge/chunk.md) — 4 个命令
|
||||
|
||||
#### `kscli chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
```bash
|
||||
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
```bash
|
||||
kscli chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
```bash
|
||||
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
```bash
|
||||
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心文件管理
|
||||
|
||||
> 📖 [完整手册](knowledge/file.md) — 3 个命令
|
||||
|
||||
#### `kscli file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
```bash
|
||||
kscli file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
```bash
|
||||
kscli file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
```bash
|
||||
kscli file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心集合与分类
|
||||
|
||||
> 📖 [完整手册](knowledge/collection-category.md) — 5 个命令
|
||||
|
||||
#### `kscli collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
```bash
|
||||
kscli collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
```bash
|
||||
kscli collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category add --name <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-delete)
|
||||
|
||||
---
|
||||
|
||||
### 检索与对话
|
||||
|
||||
> 📖 [完整手册](knowledge/search-chat.md) — 3 个命令
|
||||
|
||||
#### `kscli retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
```bash
|
||||
kscli retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-retrieve)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
```bash
|
||||
kscli search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-search)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
```bash
|
||||
kscli chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-chat)
|
||||
|
||||
---
|
||||
|
||||
## 常见错误与排查
|
||||
|
||||
### Workspace ID 缺失
|
||||
|
||||
**报错**:`Workspace ID is required.`
|
||||
|
||||
**原因**:所有 knowledge 管理命令都需要 workspace ID 来构造 API 端点(`{workspaceId}.cn-beijing.maas.aliyuncs.com`)。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 方式1:命令行参数
|
||||
bl knowledge list --workspace-id ws-xxx
|
||||
|
||||
# 方式2:环境变量
|
||||
export BAILIAN_WORKSPACE_ID=ws-xxx
|
||||
|
||||
# 方式3:配置文件
|
||||
bl config set workspace_id ws-xxx
|
||||
```
|
||||
|
||||
### 知识库 ID 不存在
|
||||
|
||||
**报错**:`Knowledge base not found: idx-xxx`
|
||||
|
||||
**原因**:`--index-id` 指定的知识库在当前 workspace 中不存在。
|
||||
|
||||
**解决**:先 `bl knowledge list` 确认知识库 ID。
|
||||
|
||||
### 导入任务 SystemError
|
||||
|
||||
**报错**:服务端返回 `SystemError`
|
||||
|
||||
**原因**:`doc status` 传入了不存在的 job ID,或知识库空闲无任务。
|
||||
|
||||
**解决**:检查 `doc list` 输出中的 `ingestionId`,或从 `doc upload`/`knowledge create` 的返回值获取。
|
||||
|
||||
### doc_id 与 fileId 混淆
|
||||
|
||||
**问题**:`doc delete` 时用了 `doc upload` 返回的 `fileId` 而非 `doc list` 返回的 `doc_id`。
|
||||
|
||||
**原因**:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。
|
||||
|
||||
**解决**:始终用 `doc list --quiet` 获取 `doc_id`。
|
||||
|
||||
### retrieve 已废弃
|
||||
|
||||
**问题**:`retrieve` 命令输出废弃警告。
|
||||
|
||||
**解决**:改用 `search` 命令。`search` 通过 `--agent-id` 驱动检索策略,支持多知识库、路由、rerank 等高级特性。`retrieve` 直接操作 `--index-id`,功能受限且不再迭代。
|
||||
|
||||
### OSS 导入权限错误
|
||||
|
||||
**报错**:服务端返回权限相关错误。
|
||||
|
||||
**原因**:OSS bucket 未授权给平台服务角色。
|
||||
|
||||
**解决**:检查 RAM 控制台中的 `AliyunServiceRoleForBailian` 角色是否已正确授权。
|
||||
|
||||
### Chat SSE error
|
||||
|
||||
**报错**:`Chat API error` + API error code。
|
||||
|
||||
**原因**:流式对话过程中服务端返回 error 事件。
|
||||
|
||||
**解决**:检查 `--agent-id` 是否存在、服务是否已部署、API Key 是否有效。错误消息和 code 原样透传,不二次包装。
|
||||
|
||||
### file list 返回空
|
||||
|
||||
**问题**:`file list --category-id default` 返回空列表。
|
||||
|
||||
**原因**:与上传 API 不同,`file list` 不解析字面量 `default`,需要真实分类 ID。
|
||||
|
||||
**解决**:通过 `file get` 的 category 字段或 `category list` 获取真实分类 ID。
|
||||
|
||||
### 集合无法删除
|
||||
|
||||
**问题**:没有 `collection delete` 命令。
|
||||
|
||||
**原因**:暂不支持通过 CLI 删除。
|
||||
|
||||
**解决**:创建集合需谨慎。如需隔离,创建新集合并迁移文件。
|
||||
|
||||
---
|
||||
|
||||
## 附录:命令速查表
|
||||
|
||||
| 命令 | 功能 | 关键参数 |
|
||||
| ------------------------- | ------------ | ----------------------------------------------------------- |
|
||||
| `kscli kb list` | 列出知识库 | `--name` |
|
||||
| `kscli kb info` | 知识库详情 | `--index-id` |
|
||||
| `kscli kb create` | 创建知识库 | `--name`, `--doc-id`/`--category-id` |
|
||||
| `kscli kb update` | 更新知识库 | `--index-id`, `--name`/`--description`/`--rerank-min-score` |
|
||||
| `kscli kb delete` | 删除知识库 | `--index-id`, `--yes` |
|
||||
| `kscli kb stats` | 监控数据 | `--index-id`, `--start`/`--end` |
|
||||
| `kscli doc list` | 文档列表 | `--index-id` |
|
||||
| `kscli doc status` | 导入任务状态 | `--index-id`, `--job-id`, `--wait` |
|
||||
| `kscli doc upload` | 上传文件 | `--file`, `--index-id`, `--wait` |
|
||||
| `kscli doc delete` | 删除文档 | `--index-id`, `--doc-id` |
|
||||
| `kscli doc tag` | 文件打标签 | `--doc-id`, `--tag`, `--mode` |
|
||||
| `kscli doc import-oss` | OSS 导入 | `--bucket`, `--region`, `--oss-key` |
|
||||
| `kscli service list` | 服务列表 | `--scene` |
|
||||
| `kscli service get` | 服务详情 | `--agent-id` |
|
||||
| `kscli service create` | 创建服务 | `--name`, `--scene`, `--index-id` |
|
||||
| `kscli service update` | 更新服务 | `--agent-id`, 配置参数 |
|
||||
| `kscli service deploy` | 发布服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service delete` | 删除服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service copy` | 复制服务 | `--agent-id` |
|
||||
| `kscli chunk add` | 添加 chunk | `--index-id`, `--content`/`--field` |
|
||||
| `kscli chunk list` | chunk 列表 | `--index-id`, `--doc-id` |
|
||||
| `kscli chunk update` | 更新 chunk | `--index-id`, `--chunk-id`, `--doc-id` |
|
||||
| `kscli chunk delete` | 删除 chunk | `--index-id`, `--chunk-id`, `--yes` |
|
||||
| `kscli file list` | 文件列表 | `--category-id` |
|
||||
| `kscli file get` | 文件详情 | `--file-id` |
|
||||
| `kscli file delete` | 删除文件 | `--file-id`, `--yes` |
|
||||
| `kscli collection create` | 创建集合 | `--name`, `--description` |
|
||||
| `kscli collection get` | 集合详情 | `--collection-id`/`--name` |
|
||||
| `kscli category list` | 分类列表 | `--collection-id`, `--parent-id` |
|
||||
| `kscli category add` | 创建分类 | `--name`, `--parent-id` |
|
||||
| `kscli category delete` | 删除分类 | `--category-id`, `--yes` |
|
||||
| `kscli retrieve` | 检索(废弃) | `--index-id`, `--query` |
|
||||
| `kscli search` | 语义检索 | `--query`, `--agent-id` |
|
||||
| `kscli chat` | RAG 对话 | `--message`, `--agent-id` |
|
||||
@@ -0,0 +1,218 @@
|
||||
# 检索与对话命令手册
|
||||
|
||||
以下命令通过检索服务(agent)消费知识库。`search` 用于语义检索,`chat` 用于多轮对话。`retrieve` 已废弃。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--query <text>` | string | 是 | 检索查询文本 |
|
||||
| `--dense-similarity-top-k <n>` | number | 否 | 稠密检索 top K |
|
||||
| `--sparse-similarity-top-k <n>` | number | 否 | 稀疏检索 top K |
|
||||
| `--rerank` | switch | 否 | 启用 rerank |
|
||||
| `--rerank-top-n <n>` | number | 否 | rerank 返回 top N 结果 |
|
||||
| `--rerank-model <name>` | string | 否 | rerank 模型名,如 `qwen3-rerank-hybrid` |
|
||||
| `--rerank-mode <mode>` | string | 否 | rerank 模式:`qa`、`similar` 或 `custom` |
|
||||
| `--rerank-instruct <text>` | string | 否 | 自定义 rerank 指令(`--rerank-mode custom` 时使用) |
|
||||
| `--top-k <n>` | number | 否 | 返回结果数(已废弃,用 `--rerank-top-n` 替代) |
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **已废弃**,推荐使用 `search` 命令。`search` 通过 agent_id 驱动检索策略,支持更多高级特性。
|
||||
- `--top-k` 已废弃,使用 `--rerank-top-n` 替代,传入 `--top-k` 会输出 stderr 警告。
|
||||
- 此命令直接用 `--index-id` 检索,不需要创建检索服务。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
bl knowledge retrieve --index-id idx-xxx --query "How to use Alibaba Cloud Bailian" --workspace-id ws-xxx
|
||||
|
||||
# 启用 rerank
|
||||
bl knowledge retrieve --index-id idx-xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------- |
|
||||
| `--query <text>` | string | 是 | 检索查询文本(不可为空) |
|
||||
| `--agent-id <id>` | string | 是 | 检索服务 ID(在控制台知识检索页面获取,或通过 `service list` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复),用于多模态检索 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--query` 不可为空(API 要求 `minLength: 1`)
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 包含检索结果。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 检索范围和策略(多知识库加权、路由、rerank 等)由 `--agent-id` 对应的服务配置驱动。只需 `--query` 和 `--agent-id` 即可调用。
|
||||
- `--agent-version beta` 调试草稿配置进行调试,部署前验证效果。
|
||||
- 与 `retrieve` 的区别:`search` 通过 agent_id 间接驱动检索策略(支持多知识库、路由、rerank 等),`retrieve` 直接操作 index_id 且功能较少。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
bl knowledge search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态检索(带图片)
|
||||
bl knowledge search --query "describe this image" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg
|
||||
|
||||
# 调试草稿版本
|
||||
bl knowledge search --query "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--message <text>` | array | 是¹ | 消息文本(可重复)。支持 `role:content` 前缀设置角色(如 `user:hello`),默认角色为 `user`。也支持完整 JSON 对象传递结构化消息 |
|
||||
| `--agent-id <id>` | string | 是 | Q&A 服务 ID(在控制台知识问答页面获取,或通过 `service list --scene chat` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复)。附加到最后一条 user 消息作为多模态内容 |
|
||||
|
||||
> ¹ `--message` 或 `--image` 至少提供其一。纯图片查询可以只传 `--image`(CLI 会自动创建空 user 消息承载图片)。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--message` 或 `--image` 至少提供一个
|
||||
- `--image` 不能与已包含 `image_url` 内容部分的消息同时使用
|
||||
|
||||
**输出**
|
||||
|
||||
**TTY text 模式**(实时流式):
|
||||
|
||||
```
|
||||
🔍 Retrieving...
|
||||
✍️ Generating...
|
||||
这是AI生成的回答内容,逐字流式输出...
|
||||
```
|
||||
|
||||
> 进度标签由 SSE `step_change` 事件驱动:`tool_calling`(检索中)→ `plan_start`(规划中)→ `generation_start`(生成中)。
|
||||
|
||||
**非 TTY text 模式**(缓冲输出):
|
||||
|
||||
```
|
||||
完整的回答文本...
|
||||
```
|
||||
|
||||
**json 模式**(`--output json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "完整的回答文本...",
|
||||
"request_id": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
quiet 模式:输出完整的回答文本。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- API 仅支持 SSE 流式响应。TTY 环境下实时打印 token;非 TTY 环境缓冲后输出完整文本。
|
||||
- SSE 事件生命周期:`tool_calling` → `tool_return` → `plan_start` → `planning` → `plan_end` → `generation_start` → `generating` → `generation_end`。`tool_calling` → `tool_return` 可能循环多次。
|
||||
- 多轮对话:用 `--message "user:..."` 和 `--message "assistant:..."` 传递对话历史。
|
||||
- `--agent-version beta` 调用草稿配置进行调试。
|
||||
- `--image` 附加到最后一条 user 消息上。如果消息中已包含 `image_url` 内容部分,则不能再用 `--image`。
|
||||
- `--verbose` 模式下,所有 SSE 事件详情会输出到 stderr。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 单轮对话
|
||||
bl knowledge chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多轮对话(带历史)
|
||||
bl knowledge chat \
|
||||
--message "user:What is RAG?" \
|
||||
--message "assistant:RAG is retrieval-augmented generation..." \
|
||||
--message "How does it work?" \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态对话(带图片)
|
||||
bl knowledge chat \
|
||||
--message "Describe these images" \
|
||||
--image https://example.com/a.png \
|
||||
--image https://example.com/b.png \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 调试草稿版本
|
||||
bl knowledge chat --message "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,401 @@
|
||||
# 检索服务管理命令手册
|
||||
|
||||
检索服务(也称 agent)是知识库的检索入口。通过 `--agent-id` 在 search/chat 命令中使用。服务有 `chat`(问答)和 `search`(检索)两种场景。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------------- |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--status <status>` | string | 否 | 按状态过滤:`draft`、`deployed`(含 edited)、`deleted` |
|
||||
| `--name <text>` | string | 否 | 按服务名称模糊过滤 |
|
||||
| `--agent-id <id>` | string | 否 | 按精确 agent ID 过滤 |
|
||||
| `--index-id <id>` | string | 否 | 按关联知识库 ID 过滤 |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--status` 只能是 `draft`、`deployed`、`deleted`
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
aid-xxx deployed 2 my-qa (kb: my-kb)
|
||||
total: 1
|
||||
Use an agent_id above with the knowledge chat command.
|
||||
```
|
||||
|
||||
> 最后一行根据 scene 自动提示用 `search` 还是 `chat` 命令消费。
|
||||
|
||||
quiet 模式:每行一个 `agent_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端要求 `--scene` 必填,要查看两种场景的服务需分别执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出 chat 服务
|
||||
bl knowledge service list --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 只看已部署的检索服务
|
||||
bl knowledge service list --scene search --status deployed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--agent-version <version>` | string | 否 | 指定版本查看(`beta` 或已发布版本号);不传则返回所有版本 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: aid-xxx
|
||||
name: my-qa
|
||||
desc: product Q&A
|
||||
scene: chat
|
||||
status: deployed
|
||||
Version beta:
|
||||
desc: draft
|
||||
policy: turbo
|
||||
model: qwen-max
|
||||
temperature: 0.7
|
||||
kb: idx-xxx (my-kb)
|
||||
Version 1:
|
||||
published: 2026-01-01
|
||||
...
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不传 `--agent-version` 时返回所有版本(beta 草稿 + 已发布版本号)。
|
||||
- 版本值原样传递,有效值集合由服务端维护。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看服务完整详情
|
||||
bl knowledge service get --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看 beta 草稿配置
|
||||
bl knowledge service get --agent-id aid-xxx --agent-version beta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 服务名称(最多 200 字符,同一场景下工作区内唯一) |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--description <text>` | string | 否 | 服务描述(最多 1000 字符) |
|
||||
| `--index-id <id>` | string | 否 | 绑定此知识库;其他配置使用服务端默认值 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 最多 200 字符
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--description` 最多 1000 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: aid-xxx (status: draft, version: beta)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不指定 `--index-id` 时,服务端使用默认 agent 配置。
|
||||
- beta 草稿可通过 search/chat 的 `--agent-version beta` 测试,部署后才生效。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建 Q&A 服务
|
||||
bl knowledge service create --name my-qa --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 创建检索服务并绑定知识库
|
||||
bl knowledge service create --name my-search --scene search --index-id idx-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------ | ------ | ---- | ---------------------------------------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--name <text>` | string | 否 | 新名称(最多 200 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述(最多 1000 字符) |
|
||||
| `--agent-version <version>` | string | 否 | 目标版本(默认:beta 草稿。已发布版本只接受 `--version-desc`) |
|
||||
| `--version-desc <text>` | string | 否 | 版本描述 |
|
||||
| `--policy <policy>` | string | 否 | Agent 策略:`turbo`(快速)或 `agentic`(多轮) |
|
||||
| `--model <name>` | string | 否 | 生成模型代码(须在平台白名单中) |
|
||||
| `--temperature <n>` | number | 否 | 采样温度,范围 0-2 |
|
||||
| `--max-llm-calls <n>` | number | 否 | 单次请求最大 LLM 调用次数,范围 1-30 |
|
||||
| `--enable-session-file <bool>` | string | 否 | 启用会话文件:`true` 或 `false` |
|
||||
| `--enable-refusal <bool>` | string | 否 | 启用拒答:`true` 或 `false` |
|
||||
| `--enable-anti-leak <bool>` | string | 否 | 启用防泄漏:`true` 或 `false` |
|
||||
| `--enable-rich-text <bool>` | string | 否 | 启用富文本输出:`true` 或 `false` |
|
||||
| `--enable-citation <bool>` | string | 否 | 启用引用标注:`true` 或 `false` |
|
||||
| `--config-file <path>` | string | 否 | JSON 文件替换整个 `agent_config`(含嵌套设置如 `kb_search_configs`);与标量配置参数互斥 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供一个更新项(`--name`/`--description`/`--version-desc`/`--config-file`/标量配置参数),否则报错 "Nothing to update"
|
||||
- `--config-file` 与标量配置参数(`--policy`/`--model`/`--temperature` 等)互斥
|
||||
- 已发布版本 + 配置变更 → 报错(已发布版本只接受 `--version-desc`)
|
||||
- `--name` 最多 200 字符;`--description` 最多 1000 字符
|
||||
- `--policy` 只能是 `turbo` 或 `agentic`
|
||||
- `--temperature` 范围 0-2
|
||||
- `--max-llm-calls` 范围 1-30
|
||||
- 布尔参数(`--enable-*`)只能是 `true` 或 `false`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: aid-xxx
|
||||
Draft config changed — verify with --agent-version beta, then deploy.
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 配置变更只作用于 beta 草稿;已发布版本只接受 `--version-desc`。
|
||||
- 标量配置参数采用 read-merge-write:CLI 先读取当前 beta 配置,再合并变更后整体提交(API 是整替换语义)。
|
||||
- `--config-file` 替换整个配置,适合设置嵌套字段(如 `kb_search_configs`)。
|
||||
- 修改草稿后用 `--agent-version beta` 在 search/chat 上测试,通过后 `service deploy` 发布。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 调整温度
|
||||
bl knowledge service update --agent-id aid-xxx --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 用 JSON 文件替换整个配置
|
||||
bl knowledge service update --agent-id aid-xxx --config-file ./agent-config.json
|
||||
|
||||
# 给已发布版本 1 加描述
|
||||
bl knowledge service update --agent-id aid-xxx --agent-version 1 --version-desc "first stable release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deployed: aid-xxx version 2
|
||||
```
|
||||
|
||||
quiet 模式:输出新版本号。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 版本号自动递增,状态变为 `deployed`。
|
||||
- 发布影响线上调用方,确认提示会警告。
|
||||
- 如果当前状态为 `edited`(已发布后又改了草稿),确认提示会额外警告「发布会覆盖线上行为」。
|
||||
- 需要工作区的知识库修改权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 发布(交互确认)
|
||||
bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 带描述并跳过确认
|
||||
bl knowledge service deploy --agent-id aid-xxx --version-desc "tuned rerank params" --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: aid-xxx (status: deleted)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 删除不可撤销,`agent_id` 不再可用于 search/chat 调用。
|
||||
- API 是幂等的:删除已删除的服务不会报错。
|
||||
- 如果服务状态为 `deployed` 或 `edited`,确认提示会额外警告「此服务正在线上运行」。
|
||||
- 需要工作区的知识库删除权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
bl knowledge service delete --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge service delete --agent-id aid-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--agent-id <id>` | string | 是 | 源服务(agent)ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
new agent_id: aid-new (name: copy_my-qa, status: draft)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出新 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 副本初始为 beta 草稿,测试后需 deploy 发布。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 复制服务
|
||||
bl knowledge service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -21,6 +21,7 @@
|
||||
"bl": "pnpm -F bailian-cli dev",
|
||||
"kscli": "pnpm -F knowledge-studio-cli dev",
|
||||
"test": "vp test",
|
||||
"test:journey": "vp test packages/commands/tests/e2e/knowledge/journeys",
|
||||
"release:check": "node tools/release/check.mjs",
|
||||
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
|
||||
"test:stress": "node packages/cli/tests/stress/run.mjs"
|
||||
|
||||
+90
-146
@@ -13,8 +13,9 @@
|
||||
|
||||
---
|
||||
|
||||
_Chat with Qwen, generate images & videos, understand images, call agents,_
|
||||
_manage memory, search the web — all from your terminal._
|
||||
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
|
||||
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
|
||||
_every AI capability, one command away._
|
||||
|
||||
_Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
@@ -22,29 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
## Features
|
||||
|
||||
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
|
||||
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
|
||||
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
|
||||
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
|
||||
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
|
||||
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
|
||||
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
|
||||
|
||||
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
|
||||
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
|
||||
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
|
||||
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
|
||||
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
|
||||
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
|
||||
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
|
||||
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
|
||||
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
|
||||
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
|
||||
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
|
||||
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
|
||||
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
|
||||
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
|
||||
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
|
||||
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
|
||||
|
||||
## Showcase: One-Sentence Cinematic Video
|
||||
## Showcase 1: A Cinematic Short Film from One Sentence
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -57,136 +45,93 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
|
||||
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
|
||||
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
|
||||
>
|
||||
> _(Original: "帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2分钟左右的视频,尺寸是16:9")_
|
||||
|
||||
### How it works
|
||||
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
|
||||
|
||||
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
|
||||
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
|
||||
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
|
||||
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
|
||||
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
|
||||
|
||||
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Recommended — no Node required
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
**Agent install (recommended)**
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
|
||||
|
||||
# Node users / developers (Node.js >= 18.17)
|
||||
npm install -g bailian-cli
|
||||
|
||||
# Agent skills
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```text
|
||||
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
|
||||
```
|
||||
|
||||
> Binary install does not require Node.js. `npm install -g` remains fully supported.
|
||||
**Install with NPM**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> Requires Node.js >= 18.17.
|
||||
|
||||
**Install on macOS/Linux**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
**Install on Windows**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate, recommended
|
||||
bl auth login --console
|
||||
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
|
||||
|
||||
# Or authenticate with an API key
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# Or use Token Plan (Base URL built in; the key is tested during login)
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
# Configure a coding agent to use DashScope
|
||||
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
|
||||
|
||||
# Chat with Qwen
|
||||
bl text chat --message "What is DashScope?"
|
||||
|
||||
# Multimodal chat (text + image + audio + video)
|
||||
bl omni --message "Describe this image" --image ./photo.jpg
|
||||
|
||||
# Generate an image
|
||||
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
|
||||
|
||||
# Generate a video from local image
|
||||
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
|
||||
|
||||
# Model recommendation — find the best model for your use case
|
||||
bl advisor recommend --message "I need a visual-understanding chatbot"
|
||||
|
||||
# Compare specific models
|
||||
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
|
||||
|
||||
# Browser login (required for console capability commands)
|
||||
bl auth login --console
|
||||
|
||||
# Fine-tune & deploy — a one-shot train-to-serve workflow
|
||||
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
|
||||
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
|
||||
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
|
||||
bl finetune capability --model qwen3-8b # Which training types a model supports
|
||||
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
|
||||
|
||||
# Browse models / apps / free-tier quota / usage statistics / workspaces
|
||||
bl model list # Browse model families and pricing
|
||||
bl app list
|
||||
bl usage summary # Unified view: free-tier quota + recent usage overview
|
||||
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
|
||||
bl workspace list # List all workspaces
|
||||
|
||||
# Rate limit management (list / check / request / history)
|
||||
bl quota list # View RPM/TPM limits (add --model to filter)
|
||||
bl quota check # Current usage vs rate limits (add --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
|
||||
bl quota history # View quota-change history
|
||||
|
||||
# Asset center — browse, download, and manage model-generated assets (requires console login)
|
||||
bl asset-center list --type IMAGE
|
||||
bl asset-center get <asset-id> --include-download-url
|
||||
bl asset-center download --id <asset-id>
|
||||
bl asset-center stats
|
||||
bl asset-center storage
|
||||
|
||||
# Token Plan team management (requires AK/SK, see auth below)
|
||||
bl token-plan list-seats # View subscription seat details
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| Scenario | What to say to your Agent |
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| 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." |
|
||||
| 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." |
|
||||
|
||||
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## Authentication
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# Option 2: Login command (persisted to ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# Option 3: Per-command flag
|
||||
bl text chat --api-key sk-xxxxx --message "Hello"
|
||||
```
|
||||
|
||||
### Token Plan API Key
|
||||
|
||||
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
|
||||
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
|
||||
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
@@ -194,26 +139,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### Console Login (OAuth)
|
||||
|
||||
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). Opens the Bailian console in your browser to sign in.
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
|
||||
### Alibaba Cloud OpenAPI AK/SK
|
||||
|
||||
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
|
||||
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
|
||||
|
||||
```bash
|
||||
# Option 1: Login command (persisted to ~/.bailian/config.json)
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
|
||||
# Option 2: Environment variables
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -222,18 +161,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# View current config
|
||||
bl config show
|
||||
|
||||
# Set defaults
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# List all config profiles
|
||||
bl config list
|
||||
|
||||
# Self-update to latest or a specific version
|
||||
bl update
|
||||
bl update --to 0.1.14
|
||||
# Switch config profile
|
||||
bl config use --name token-plan
|
||||
```
|
||||
|
||||
Config file location: `~/.bailian/config.json`
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
|
||||
|
||||
## Links
|
||||
|
||||
| Resource | URL |
|
||||
@@ -245,11 +197,3 @@ Config file location: `~/.bailian/config.json`
|
||||
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## Changelog
|
||||
|
||||
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
+90
-147
@@ -22,29 +22,16 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
## 功能特性
|
||||
|
||||
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
|
||||
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
|
||||
- **素材理解** — 图像、文档、音频、长视频的解析与问答
|
||||
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
|
||||
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
|
||||
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
|
||||
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
|
||||
|
||||
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
|
||||
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
|
||||
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
|
||||
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
|
||||
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
|
||||
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
|
||||
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
|
||||
|
||||
> **注意:** 以下功能目前仅对中国站(aliyun.com)账号开放,国际站 / 全球站账号暂不支持。
|
||||
|
||||
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
|
||||
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
|
||||
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
|
||||
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
|
||||
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
|
||||
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT)、非阻塞探测任务状态(`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`)
|
||||
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`)
|
||||
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`)
|
||||
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
|
||||
|
||||
## 示例:一句话生成一部电影短片
|
||||
## 示例 1:一句话生成一部电影短片
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -54,137 +41,96 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
|
||||
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
|
||||
> _“帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9。”_
|
||||
|
||||
### 工作流程
|
||||
## 示例 2:一句话构建短片导演 Managed Agent
|
||||
|
||||
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
|
||||
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
|
||||
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**。
|
||||
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
|
||||
<p align="center"><i>👆 点击封面播放完整演示</i></p>
|
||||
|
||||
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _“帮我构建一个 managedagent 应用,能够实现短片拍摄,导演专家生成视频,然后也能进行设计对应的分镜图。”_
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 推荐 — 无需本机 Node.js
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
**Agent 安装(推荐)**
|
||||
|
||||
# Windows(PowerShell)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
把下面这句话发给你的 Agent,它会自行判断环境并完成安装与校验:
|
||||
|
||||
# Node 用户 / 开发者(需要 Node.js >= 18.17)
|
||||
npm install -g bailian-cli
|
||||
|
||||
# Agent skills
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```text
|
||||
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
|
||||
```
|
||||
|
||||
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
|
||||
**NPM 安装**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> 需要预先安装 Node.js >= 18.17。
|
||||
|
||||
**macOS/Linux 安装**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
**Windows 安装**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 认证(推荐浏览器登录)
|
||||
bl auth login --console
|
||||
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
|
||||
|
||||
# 或使用 API key 认证
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# 或使用 Token Plan(已内置 Base URL,登录时自动测试 Key)
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
# 配置 Coding Agent 使用 DashScope
|
||||
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
|
||||
|
||||
# 和通义千问对话
|
||||
bl text chat --message "你好,介绍一下阿里云百炼平台"
|
||||
|
||||
# 多模态对话(文本 + 图片 + 音频 + 视频)
|
||||
bl omni --message "描述这张图片" --image ./photo.jpg
|
||||
|
||||
# 生成图片
|
||||
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
|
||||
|
||||
# 图生视频(本地文件自动上传)
|
||||
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
|
||||
|
||||
# 模型推荐 — 根据场景推荐最适合的模型
|
||||
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
|
||||
|
||||
# 对比特定模型
|
||||
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
|
||||
|
||||
# 浏览器登录(控制台能力相关命令需要)
|
||||
bl auth login --console
|
||||
|
||||
# 微调与部署 — 从训练到服务的一站式流程
|
||||
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
|
||||
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
|
||||
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0;失败/取消报错)
|
||||
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
|
||||
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
|
||||
|
||||
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
|
||||
bl model list # 浏览模型系列与价格信息
|
||||
bl app list
|
||||
bl usage summary # 统一视图:免费额度 + 近期用量概览
|
||||
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
|
||||
bl workspace list # 列出所有业务空间
|
||||
|
||||
# 限流管理与提额(list / check / request / history)
|
||||
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
|
||||
bl quota check # 当前用量 vs 限流阈值(加 --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
|
||||
bl quota history # 查看提额历史记录
|
||||
|
||||
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
|
||||
bl asset-center list --type IMAGE
|
||||
bl asset-center get <asset-id> --include-download-url
|
||||
bl asset-center download --id <asset-id>
|
||||
bl asset-center stats
|
||||
bl asset-center storage
|
||||
|
||||
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
|
||||
bl token-plan list-seats # 查看订阅席位明细
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| 场景 | 可以这样对 Agent 说 |
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## 认证方式
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
|
||||
|
||||
```bash
|
||||
# 方式一:环境变量
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# 方式二:登录命令(持久化到 ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# 方式三:命令行参数
|
||||
bl text chat --api-key sk-xxxxx --message "你好"
|
||||
```
|
||||
|
||||
### Token Plan API Key
|
||||
|
||||
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
|
||||
CLI 已内置 Token Plan 的默认 Base URL;登录命令会先测试 Key,通过后才保存并激活 `token-plan` 配置。
|
||||
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
@@ -192,26 +138,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### 控制台登录(OAuth)
|
||||
|
||||
控制台能力命令(`model list`、`app list`、`usage summary/free/stats`、`workspace list`、`quota list/request/check/history`、`asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
|
||||
### 阿里云 OpenAPI AK/SK
|
||||
|
||||
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
|
||||
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
|
||||
|
||||
```bash
|
||||
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
|
||||
# 方式二:环境变量
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
```
|
||||
|
||||
## 配置
|
||||
@@ -220,20 +160,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# 查看当前配置
|
||||
bl config show
|
||||
|
||||
# 设置默认值
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# 查看全部配置档
|
||||
bl config list
|
||||
|
||||
# 自更新到最新版本
|
||||
bl update
|
||||
|
||||
# 安装指定版本
|
||||
bl update --to 0.1.14
|
||||
# 切换配置档
|
||||
bl config use --name token-plan
|
||||
```
|
||||
|
||||
配置文件位置:`~/.bailian/config.json`
|
||||
|
||||
## 更新
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群,获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
|
||||
|
||||
## 相关链接
|
||||
|
||||
| 资源 | 地址 |
|
||||
@@ -245,11 +196,3 @@ bl update --to 0.1.14
|
||||
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## 更新日志
|
||||
|
||||
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# 迭代一设计 · doc 组命令
|
||||
|
||||
> 命令:`doc upload` / `doc list` / `doc status` / `doc delete` / `doc tag` / `doc import-oss`
|
||||
> 公共约定见 [README.md](README.md)。
|
||||
|
||||
## doc upload — 上传本地文件入库(编排命令)
|
||||
|
||||
**说明**:本迭代最复杂命令。把"本地文件 → 数据中心 →(可选)导入知识库"封装为一条命令,替代构建期最高频的控制台操作(S2.2 痛点:高)。对标竞品 add-file。
|
||||
|
||||
**编排四步**:
|
||||
|
||||
| 步 | API | 输入 | 输出 |
|
||||
| ---------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| 1 申请租约 | `POST /api/v1/connector/dash/applyFileUploadLease` | `category`(类目ID) + `fileName` + `sizeBytes`(字符串!) + `contentMd5`(Base64) | `leaseId` + `param.url/method/headers` |
|
||||
| 2 OSS 上传 | `PUT {param.url}` | 文件二进制 + `param.headers`(含 `x-bailian-extra`、`Content-Type`) | HTTP 200 |
|
||||
| 3 注册文件 | `POST /api/v1/connector/dash/addFile` | `leaseId` + `category` + `parser: "AUTO_SELECT"` + `tags?` | `fileId` |
|
||||
| 4 导入(可选,传 `--index-id` 时) | `POST /api/v1/indices/rag/index/job/create` | `indexId` + `dataSource: { sourceType: "DATA_CENTER_FILE", fileIds }` | `ingestionId` |
|
||||
|
||||
坑位(实现注释必须标注):
|
||||
|
||||
- `sizeBytes` 必须字符串;`contentMd5` = `crypto.createHash("md5").update(buf).digest("base64")`
|
||||
- 租约/注册的类目参数名是 `category`,不是 `categoryId`
|
||||
- 第 4 步 body 是嵌套 `dataSource: { sourceType, fileIds }`(实测;公开文档的平铺 `documentIds` 会报 `Index.InvalidParameter`)
|
||||
- **第 4 步必须显式传 `sourceType`,不传会导入整个数据中心(API 文档明示的默认行为)**
|
||||
- 步骤 2 走 OSS 域名不走 DashScope 网关,用原生 fetch 而非 ctx.client(无 Bearer 头);失败归类 NETWORK
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 类型 | 必填 | 说明 |
|
||||
| -------------------------------------------------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--file <path>` | array | 是 | 本地文件路径,可重复;扩展名与大小按产品支持范围预校验(见下方格式白名单) |
|
||||
| `--index-id <id>` | string | 否 | 注册后立即导入该知识库(触发第 4 步,多文件合并为一个 job) |
|
||||
| `--category-id <id>` | string | 否 | 目标类目;缺省自动解析默认类目(listCategory 取 `isDefault: true`),解析失败报 GENERAL + hint 显式传 `--category-id` |
|
||||
| `--tag <text>` | array | 否 | addFile tags,可重复 |
|
||||
| `--wait` / `--poll-interval <s>` / `--timeout <s>` | — | 否 | 与 `--index-id` 联用,轮询 job status 至终态 |
|
||||
|
||||
**validate**:`--wait` 无 `--index-id` → USAGE;文件不存在/不可读 → GENERAL + errno hint(沿用错误边界规范)。
|
||||
|
||||
**格式白名单与大小预校验**(依据 data/documents.md「支持的格式」,读文件前拦截,避免白传 OSS):
|
||||
|
||||
| 类型 | 扩展名 | 硬限(超限 USAGE) |
|
||||
| ------ | -------------------------- | ----------------------------------------------------- |
|
||||
| 文档 | .doc .docx .ppt .pptx .pdf | 150 MB |
|
||||
| 表格 | .xls .xlsx | 10 MB(产品为“建议值”,超限降级为 stderr 警告不拦截) |
|
||||
| 图片 | .png .jpg .jpeg .bmp .gif | 20 MB(尺寸约束不做客户端校验,留服务端) |
|
||||
| 纯文本 | .md .txt .html | 10 MB(同表格,警告不拦截) |
|
||||
|
||||
- 扩展名不在白名单 → USAGE,错误信息列出支持格式;白名单常量独立导出便于后续随产品更新
|
||||
- 开放问题:create-kb.md 提及 .csv 但 documents.md 格式表未列——文档口径不一致,实现前向产品确认;确认前 .csv 暂入白名单(服务端拒绝会透传)
|
||||
|
||||
**输出**:
|
||||
|
||||
- text:每文件一行 `<fileName> <fileId> registered`;有导入时追加 `job: <ingestionId>`;--wait 结束追加终态
|
||||
- json:`{ files: [{path, fileId}], index_id?, ingestion_id?, final_status? }`(编排命令无单一响应可透传,输出自定义稳定结构)
|
||||
- quiet:仅 fileId 每行一个
|
||||
|
||||
**实现方案**:
|
||||
|
||||
- 文件 `doc-upload.ts`;多文件串行执行 1-3 步(首版不并发,避免 OSS 限流复杂化),全部注册成功后合并执行第 4 步
|
||||
- 部分失败语义:任一文件步骤 1-3 失败即中止并报错,已成功的 fileId 列入错误 hint(幂等重传代价低)
|
||||
- 默认类目解析结果进程内缓存(多文件只查一次)
|
||||
- dry-run:不读文件内容(size/md5 以占位符表示),输出四步编排计划 `{ steps: [{step, endpoint, request}] }`
|
||||
|
||||
**测试方案**:
|
||||
|
||||
- help / 缺 `--file` exitCode 2 / `--wait` 无 `--index-id` exitCode 2
|
||||
- 文件不存在 → 非零退出 + ENOENT hint;`.zip` 扩展名 → USAGE 列出支持格式
|
||||
- dry-run:断言 steps 长度(带/不带 --index-id 为 4/3)、lease 请求 `sizeBytes` 为字符串类型、job 请求含 `sourceType: "DATA_CENTER_FILE"`
|
||||
- live:上传 1KB 临时 md 文件 → 断言 fileId 前缀 `file_` → afterAll doc delete + 数据中心 deleteFile 清理
|
||||
|
||||
## doc list — 查询知识库文档列表
|
||||
|
||||
**说明**:列出库内文档及解析/索引状态,含 FAILED 发现(S2.3 / S5.2)。
|
||||
|
||||
**API**:`GET /api/v1/indices/rag/index/files`,query string:`index_id` + `page_num`(注意本接口是 page_num)+ `page_size`(默认 10,最大 100)。
|
||||
|
||||
**Flags**:`--index-id` 必填;`--page-number` / `--page-size`。
|
||||
|
||||
**输出**:
|
||||
|
||||
- text:每行 `doc_id status doc_name doc_type size`;status=FAILED 行红色高亮(TTY);尾行 `total: N`
|
||||
- json 透传;quiet 仅 doc_id
|
||||
|
||||
**实现/测试**:单 API 直映射(`doc-list.ts`);dry-run 断言 query 参数名为 `page_num`;live 断言 rows 结构与 doc_id 前缀。
|
||||
|
||||
## doc status — 查询导入任务状态
|
||||
|
||||
**说明**:查导入任务进度,`--wait` 阻塞至终态供脚本串行(S2.3 痛点:高,L3 验收:FAILED 时非零 exit code)。
|
||||
|
||||
**API**:`GET /api/v1/indices/rag/index_job/status`,query string:`index_id` + `job_id`(**双必填,仅传其一服务端返回 SystemError,客户端前置双校验拦截**)+ 分页参数。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| -------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | 是 | 知识库 ID |
|
||||
| `--job-id <id>` | 是 | 导入任务 ID(kb create / doc upload 返回的 ingestionId;也见 doc list 的 ingestion_id) |
|
||||
| `--page-number` / `--page-size` | 否 | 任务含大量文档时分页 |
|
||||
| `--wait` / `--poll-interval <s>`(默认 5) / `--timeout <s>`(默认 600) | 否 | 轮询至终态 |
|
||||
|
||||
**行为**:
|
||||
|
||||
- 终态 FINISH → exit 0;FAILED → `BailianError(GENERAL)` 透传服务端 message(含文档级失败明细摘要),exit 1
|
||||
- `--wait` 超时 → TIMEOUT(5)
|
||||
- 已知行为:库无进行中任务时接口可能返回 SystemError——hint 引导 "check ingestion_id via doc list"
|
||||
|
||||
**输出**:text 顶部任务总状态 + 文档级状态列表(FAILED 高亮);json 透传。
|
||||
|
||||
**测试方案**:help / 缺任一必填(两条用例)/ dry-run 断言 query 含两个 id / live:配合 upload 用例拿真实 job 轮询到 FINISH;`--wait --timeout 1` 对慢任务断言 exitCode 5(若不稳定则仅静态覆盖超时路径,live 标记 skip 原因)。
|
||||
|
||||
## doc delete — 删除文档【危险操作】
|
||||
|
||||
**说明**:从知识库删除文档及其全部切片(S5.1 内容更新循环)。
|
||||
|
||||
**API**:`POST /api/v1/indices/rag/index/delete_file`,body `{ index_id, doc_ids }`(snake_case)。响应 `data.deleted[]` 为实际删除列表。
|
||||
|
||||
**Flags**:`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`。
|
||||
|
||||
**实现方案**:`doc-delete.ts`;确认摘要含 index_id + doc_id 列表(≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY 无 `--yes` exitCode 2 / live 配合 upload 清理链。
|
||||
|
||||
## doc tag — 批量更新文档标签
|
||||
|
||||
**说明**:批量打标,支撑标签过滤检索(S2.4)。
|
||||
|
||||
**API**:`POST /api/v1/connector/dash/batchUpdateFileTag`。`fileInfos`(1-20 项,每项 `fileId` + `tags`,单标签 ≤32 字符、单文件 ≤100 个、总长 ≤700)+ `updateMode`(OVERWRITE/APPEND)。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| --------------- | ---- | ------------------------------------------------------------------------- |
|
||||
| `--doc-id <id>` | 是 | 可重复,1-20 个(客户端预校验),映射 fileInfos[].fileId |
|
||||
| `--tag <text>` | 是 | 可重复,应用到所有 `--doc-id`(首版同一组标签批量打;异构标签用多次调用) |
|
||||
| `--mode <m>` | 否 | choices: `overwrite`/`append`,默认 `append`(追加比覆盖安全,作为缺省) |
|
||||
|
||||
**实现/测试**:`doc-tag.ts` 单 API 直映射;客户端预校验标签长度约束(USAGE 前置拦截);dry-run 断言 `updateMode: "APPEND"` 大写映射与 fileInfos 结构;live 打标后 listFile/describeFile 验证回读。
|
||||
|
||||
## doc import-oss — 从授权 OSS 批量导入
|
||||
|
||||
**说明**:从已 SLR 授权的 OSS Bucket 批量导入数据中心(大客户批量场景)。
|
||||
|
||||
**API**:`POST /api/v1/connector/dash/addFilesFromAuthorizedOss`。必填 `categoryId/categoryType/ossBucket/ossRegionId/fileDetails`(1-10 项,每项 `fileName+ossKey`)。返回 `data.fileIds`。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| -------------------- | ---- | -------------------------------------------- |
|
||||
| `--bucket <name>` | 是 | 映射 ossBucket |
|
||||
| `--region <id>` | 是 | 映射 ossRegionId(如 cn-beijing) |
|
||||
| `--oss-key <key>` | 是 | 可重复,1-10 个;fileName 取 key 的 basename |
|
||||
| `--category-id <id>` | 否 | 缺省走默认类目解析(复用 upload 的解析函数) |
|
||||
| `--tag <text>` | 否 | 可重复,≤10 |
|
||||
| `--overwrite` | 否 | switch,映射 overWriteFileByOssKey |
|
||||
|
||||
固定值:`categoryType: "UNSTRUCTURED"`;`parser` 不暴露(默认 AUTO_SELECT,审慎原则——DASH_QWEN_VL_PARSER 等需配 parserConfig,使用方式未验证)。
|
||||
|
||||
**错误边界**:SLR 未授权的服务端权限错误原样透传,hint 附 RAM 控制台确认 `AliyunServiceRoleForBailian` 的指引(该指引来自 API 文档 Note,属可权威解释范围)。
|
||||
|
||||
**实现/测试**:`doc-import-oss.ts` 单 API 直映射;dry-run 断言 fileDetails 结构与 fileName 派生逻辑;live 依赖 OSS 授权环境,gating 追加 `BAILIAN_E2E_OSS_BUCKET` 环境变量,无则 skip。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.14.1",
|
||||
"version": "1.16.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -33,6 +33,37 @@ import {
|
||||
knowledgeRetrieve,
|
||||
knowledgeSearch,
|
||||
knowledgeChat,
|
||||
knowledgeKbList,
|
||||
knowledgeKbInfo,
|
||||
knowledgeDocList,
|
||||
knowledgeDocStatus,
|
||||
knowledgeDocUpload,
|
||||
knowledgeKbCreate,
|
||||
knowledgeKbUpdate,
|
||||
knowledgeKbDelete,
|
||||
knowledgeDocDelete,
|
||||
knowledgeDocTag,
|
||||
knowledgeServiceList,
|
||||
knowledgeServiceGet,
|
||||
knowledgeServiceCreate,
|
||||
knowledgeServiceUpdate,
|
||||
knowledgeServiceDeploy,
|
||||
knowledgeServiceDelete,
|
||||
knowledgeServiceCopy,
|
||||
knowledgeChunkAdd,
|
||||
knowledgeChunkList,
|
||||
knowledgeChunkUpdate,
|
||||
knowledgeChunkDelete,
|
||||
knowledgeKbStats,
|
||||
knowledgeCategoryList,
|
||||
knowledgeCategoryAdd,
|
||||
knowledgeCategoryDelete,
|
||||
knowledgeFileList,
|
||||
knowledgeFileGet,
|
||||
knowledgeFileDelete,
|
||||
knowledgeCollectionCreate,
|
||||
knowledgeCollectionGet,
|
||||
knowledgeDocImportOss,
|
||||
mcpCall,
|
||||
mcpList,
|
||||
mcpTools,
|
||||
@@ -45,15 +76,20 @@ import {
|
||||
usageFreetier,
|
||||
usageStats,
|
||||
usageSummary,
|
||||
usageTokenPlan,
|
||||
usageCodingPlan,
|
||||
pipelineRun,
|
||||
pipelineValidate,
|
||||
advisorRecommend,
|
||||
modelList,
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaRequest,
|
||||
quotaUpdate,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
permissionGrant,
|
||||
permissionRevoke,
|
||||
datasetUpload,
|
||||
datasetList,
|
||||
datasetGet,
|
||||
@@ -62,6 +98,7 @@ import {
|
||||
finetuneTextCreate,
|
||||
finetuneAudioCreate,
|
||||
finetuneImageCreate,
|
||||
finetuneVideoCreate,
|
||||
finetuneList,
|
||||
finetuneGet,
|
||||
finetuneCancel,
|
||||
@@ -71,6 +108,7 @@ import {
|
||||
finetuneExport,
|
||||
finetuneWatch,
|
||||
finetuneCapability,
|
||||
finetunePrice,
|
||||
deployTextCreate,
|
||||
deployAudioCreate,
|
||||
deployImageCreate,
|
||||
@@ -80,18 +118,12 @@ import {
|
||||
deployScale,
|
||||
deployUpdate,
|
||||
deployDelete,
|
||||
deployPause,
|
||||
deployResume,
|
||||
tokenPlanListSeats,
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
assetList,
|
||||
assetGet,
|
||||
assetFavorite,
|
||||
assetUnfavorite,
|
||||
assetDelete,
|
||||
assetDownload,
|
||||
assetStats,
|
||||
assetStorage,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -101,6 +133,7 @@ import {
|
||||
skillUpdate,
|
||||
skillRemove,
|
||||
skillList,
|
||||
skillInit,
|
||||
managedAgentInit,
|
||||
managedAgentValidate,
|
||||
managedAgentPlan,
|
||||
@@ -159,6 +192,39 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"knowledge retrieve": knowledgeRetrieve,
|
||||
"knowledge search": knowledgeSearch,
|
||||
"knowledge chat": knowledgeChat,
|
||||
"knowledge list": knowledgeKbList,
|
||||
"knowledge info": knowledgeKbInfo,
|
||||
"knowledge create": knowledgeKbCreate,
|
||||
"knowledge update": knowledgeKbUpdate,
|
||||
"knowledge delete": knowledgeKbDelete,
|
||||
"knowledge doc list": knowledgeDocList,
|
||||
"knowledge doc status": knowledgeDocStatus,
|
||||
"knowledge doc upload": knowledgeDocUpload,
|
||||
"knowledge doc delete": knowledgeDocDelete,
|
||||
"knowledge doc tag": knowledgeDocTag,
|
||||
"knowledge service list": knowledgeServiceList,
|
||||
"knowledge service get": knowledgeServiceGet,
|
||||
"knowledge service create": knowledgeServiceCreate,
|
||||
"knowledge service update": knowledgeServiceUpdate,
|
||||
"knowledge service deploy": knowledgeServiceDeploy,
|
||||
"knowledge service delete": knowledgeServiceDelete,
|
||||
"knowledge service copy": knowledgeServiceCopy,
|
||||
"knowledge chunk add": knowledgeChunkAdd,
|
||||
"knowledge chunk list": knowledgeChunkList,
|
||||
"knowledge chunk update": knowledgeChunkUpdate,
|
||||
"knowledge chunk delete": knowledgeChunkDelete,
|
||||
"knowledge stats": knowledgeKbStats,
|
||||
"knowledge doc import-oss": knowledgeDocImportOss,
|
||||
// Data-center commands live under knowledge (no separate connector namespace);
|
||||
// the user-facing term for connector is "collection".
|
||||
"knowledge collection create": knowledgeCollectionCreate,
|
||||
"knowledge collection get": knowledgeCollectionGet,
|
||||
"knowledge category list": knowledgeCategoryList,
|
||||
"knowledge category add": knowledgeCategoryAdd,
|
||||
"knowledge category delete": knowledgeCategoryDelete,
|
||||
"knowledge file list": knowledgeFileList,
|
||||
"knowledge file get": knowledgeFileGet,
|
||||
"knowledge file delete": knowledgeFileDelete,
|
||||
"mcp call": mcpCall,
|
||||
"mcp list": mcpList,
|
||||
"mcp tools": mcpTools,
|
||||
@@ -171,15 +237,20 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"usage freetier": usageFreetier,
|
||||
"usage stats": usageStats,
|
||||
"usage summary": usageSummary,
|
||||
"usage token-plan": usageTokenPlan,
|
||||
"usage coding-plan": usageCodingPlan,
|
||||
"pipeline run": pipelineRun,
|
||||
"pipeline validate": pipelineValidate,
|
||||
"advisor recommend": advisorRecommend,
|
||||
"model list": modelList,
|
||||
"workspace list": workspaceList,
|
||||
"quota list": quotaList,
|
||||
"quota request": quotaRequest,
|
||||
"quota update": quotaUpdate,
|
||||
"quota history": quotaHistory,
|
||||
"quota check": quotaCheck,
|
||||
"permission list": permissionList,
|
||||
"permission grant": permissionGrant,
|
||||
"permission revoke": permissionRevoke,
|
||||
"dataset upload": datasetUpload,
|
||||
"dataset list": datasetList,
|
||||
"dataset get": datasetGet,
|
||||
@@ -188,6 +259,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"finetune text create": finetuneTextCreate,
|
||||
"finetune audio create": finetuneAudioCreate,
|
||||
"finetune image create": finetuneImageCreate,
|
||||
"finetune video create": finetuneVideoCreate,
|
||||
"finetune list": finetuneList,
|
||||
"finetune get": finetuneGet,
|
||||
"finetune cancel": finetuneCancel,
|
||||
@@ -197,6 +269,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"finetune export": finetuneExport,
|
||||
"finetune watch": finetuneWatch,
|
||||
"finetune capability": finetuneCapability,
|
||||
"finetune price": finetunePrice,
|
||||
"deploy text create": deployTextCreate,
|
||||
"deploy audio create": deployAudioCreate,
|
||||
"deploy image create": deployImageCreate,
|
||||
@@ -206,18 +279,12 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"deploy scale": deployScale,
|
||||
"deploy update": deployUpdate,
|
||||
"deploy delete": deployDelete,
|
||||
"deploy pause": deployPause,
|
||||
"deploy resume": deployResume,
|
||||
"token-plan list-seats": tokenPlanListSeats,
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
"token-plan add-member": tokenPlanAddMember,
|
||||
"asset-center list": assetList,
|
||||
"asset-center get": assetGet,
|
||||
"asset-center favorite": assetFavorite,
|
||||
"asset-center unfavorite": assetUnfavorite,
|
||||
"asset-center delete": assetDelete,
|
||||
"asset-center download": assetDownload,
|
||||
"asset-center stats": assetStats,
|
||||
"asset-center storage": assetStorage,
|
||||
"workspace init": workspaceInit,
|
||||
"plugin install": pluginInstall,
|
||||
"plugin link": pluginLink,
|
||||
@@ -227,6 +294,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"skill update": skillUpdate,
|
||||
"skill remove": skillRemove,
|
||||
"skill list": skillList,
|
||||
"skill init": skillInit,
|
||||
"managed-agent init": managedAgentInit,
|
||||
"managed-agent validate": managedAgentValidate,
|
||||
"managed-agent plan": managedAgentPlan,
|
||||
@@ -245,3 +313,13 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent session events": managedAgentSessionEvents,
|
||||
"managed-agent skill-list": managedAgentSkillList,
|
||||
};
|
||||
|
||||
/**
|
||||
* Runtime-only aliases for renamed commands: dispatched by the CLI (merged in
|
||||
* main.ts) but kept out of the canonical map so generate-reference.ts only
|
||||
* documents the canonical path.
|
||||
*/
|
||||
export const commandAliases: Record<string, AnyCommand> = {
|
||||
// Pre-migration name of "quota update".
|
||||
"quota request": quotaUpdate,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createCli } from "bailian-cli-runtime";
|
||||
import { commands } from "./commands.ts";
|
||||
import { commandAliases, commands } from "./commands.ts";
|
||||
import { commandPackPolicy } from "./command-pack-policy.ts";
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
@@ -10,11 +10,14 @@ const quickStartTasks = [
|
||||
"Help me analyze this video and write a Xiaohongshu-style post",
|
||||
] as const;
|
||||
|
||||
void createCli(commands, {
|
||||
binName: "bl",
|
||||
version: pkg.version,
|
||||
clientName: "bailian-cli",
|
||||
npmPackage: "bailian-cli",
|
||||
quickStartTasks,
|
||||
commandPacks: commandPackPolicy,
|
||||
}).run();
|
||||
void createCli(
|
||||
{ ...commands, ...commandAliases },
|
||||
{
|
||||
binName: "bl",
|
||||
version: pkg.version,
|
||||
clientName: "bailian-cli",
|
||||
npmPackage: "bailian-cli",
|
||||
quickStartTasks,
|
||||
commandPacks: commandPackPolicy,
|
||||
},
|
||||
).run();
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
|
||||
describe("e2e: asset-center", () => {
|
||||
test("asset-center 分组展示子命令帮助且成功退出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["asset-center"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const output = `${stdout}\n${stderr}`;
|
||||
expect(output).toContain("list");
|
||||
expect(output).toContain("storage");
|
||||
expect(output).not.toContain("oss");
|
||||
});
|
||||
|
||||
test("asset-center list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--type");
|
||||
expect(stderr).toContain("--recycle-bin");
|
||||
expect(stderr).toContain("bl asset-center list");
|
||||
});
|
||||
|
||||
test("asset-center get --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "get", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--asset-id");
|
||||
});
|
||||
|
||||
test("asset-center favorite --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--id");
|
||||
});
|
||||
|
||||
test("asset-center delete --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "delete", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--permanent");
|
||||
});
|
||||
|
||||
test("asset-center download --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "download", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--id");
|
||||
expect(stderr).not.toMatch(/(^|\s)--out(\s|$)/);
|
||||
});
|
||||
|
||||
test("asset-center stats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--sync-failed");
|
||||
});
|
||||
|
||||
test("asset-center storage --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "storage", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl asset-center storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: asset-center(Console)", () => {
|
||||
test("asset-center get 缺少 --asset-id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "get", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--asset-id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center favorite 缺少 --id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center download 缺少 --id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "download", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center list --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"list",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { deleteStatus?: string };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("listModelGeneratedAsset");
|
||||
expect(data.data?.deleteStatus).toBe("NORMAL");
|
||||
});
|
||||
|
||||
test("asset-center stats --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"stats",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string }>(stdout);
|
||||
expect(data.api).toContain("countModelGeneratedAsset");
|
||||
});
|
||||
|
||||
test("asset-center storage --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"storage",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string }>(stdout);
|
||||
expect(data.api).toContain("getStorageQuota");
|
||||
});
|
||||
|
||||
test("【console】asset-center list 真实调用或鉴权失败优雅退出", async () => {
|
||||
const workspaceId = process.env.BAILIAN_WORKSPACE_ID;
|
||||
const args = ["asset-center", "list", "--output", "json", "--page-size", "1"];
|
||||
if (workspaceId) args.push("--workspace-id", workspaceId);
|
||||
|
||||
const result = await runCli(args);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,15 @@ const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl 与全局 flag", async () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
expect(stderr).toMatch(/token-plan create-key\s+\[AK\/SK\]\s+Create a Token Plan API key/);
|
||||
expect(stderr).toMatch(/config show\s+\[No Auth\]\s+Display current configuration/);
|
||||
expect(stderr).toMatch(/--base-url/);
|
||||
expect(stderr).toMatch(/--console-region/);
|
||||
expect(stderr).toMatch(/--console-site/);
|
||||
@@ -18,6 +23,24 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/^\s*--region\s/m);
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[["text", "chat"], "API Key"],
|
||||
[["app", "list"], "Console"],
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.14.1",
|
||||
"version": "1.16.0",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
# 资产中心 CLI 命令树设计
|
||||
|
||||
> 本文档定义 `bl asset` 命令族的路径结构、help 层级、flags 概览与示例。
|
||||
> 技术实现细节见 [DESIGN.md](./DESIGN.md);API 字段见 [api-doc.md](./api-doc.md)。
|
||||
|
||||
## 1. 命名原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
| ------------ | ------------------------------------------------------------------------- |
|
||||
| 产品路径前缀 | `asset`(不用 `asset-center`,与 `deploy` / `dataset` 等产品域一致) |
|
||||
| 层级深度 | 最多三级:`asset <group> <action>` |
|
||||
| 子组条件 | 仅当子组下 ≥ 2 个 action 时使用子组(见 AGENTS.md) |
|
||||
| bin 前缀 | `usageArgs` / `exampleArgs` 不写 `bl`;help 由 runtime 按路径补全 |
|
||||
| 鉴权 | 全部 `auth: "console"`;自动可见 `--console-region` 等 CONSOLE_AUTH_FLAGS |
|
||||
|
||||
---
|
||||
|
||||
## 2. 命令树总览
|
||||
|
||||
```
|
||||
bl asset
|
||||
│
|
||||
├── list # 分页查询资产列表
|
||||
├── get <asset-id> # 查询单个资产详情
|
||||
├── favorite # 收藏资产
|
||||
├── unfavorite # 取消收藏
|
||||
├── delete # 删除资产(默认软删到回收站)
|
||||
├── restore # 从回收站恢复
|
||||
├── download # 获取下载链接 / 可选落盘
|
||||
├── stats # 资产数量统计
|
||||
├── storage # 存储容量与配额
|
||||
│
|
||||
├── models # [P1] 模型列表(辅助筛选)
|
||||
│ └── list
|
||||
│
|
||||
├── service # [P1/P2] 服务开通状态
|
||||
│ ├── status
|
||||
│ ├── enable # [P2]
|
||||
│ └── disable # [P2]
|
||||
│
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品入口注册 Map
|
||||
|
||||
`packages/cli/src/commands.ts` 中预期注册(camelCase export → kebab path):
|
||||
|
||||
| Map Key | Export 名(建议) | Phase |
|
||||
| ------------------------- | --------------------- | ----- |
|
||||
| `"asset list"` | `assetList` | 1 |
|
||||
| `"asset get"` | `assetGet` | 1 |
|
||||
| `"asset favorite"` | `assetFavorite` | 1 |
|
||||
| `"asset unfavorite"` | `assetUnfavorite` | 1 |
|
||||
| `"asset delete"` | `assetDelete` | 1 |
|
||||
| `"asset restore"` | `assetRestore` | 1 |
|
||||
| `"asset download"` | `assetDownload` | 1 |
|
||||
| `"asset stats"` | `assetStats` | 1 |
|
||||
| `"asset storage"` | `assetStorage` | 1 |
|
||||
| `"asset models list"` | `assetModelsList` | 2 |
|
||||
| `"asset service status"` | `assetServiceStatus` | 2 |
|
||||
| `"asset service enable"` | `assetServiceEnable` | 3 |
|
||||
| `"asset service disable"` | `assetServiceDisable` | 3 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Help 层级预览
|
||||
|
||||
### 4.1 顶层分组
|
||||
|
||||
```
|
||||
$ bl asset
|
||||
Asset management commands for Bailian Asset Center.
|
||||
|
||||
Commands:
|
||||
list List model-generated assets
|
||||
get Get asset details by ID
|
||||
favorite Mark assets as favorites
|
||||
unfavorite Remove assets from favorites
|
||||
delete Delete assets (soft delete by default)
|
||||
restore Restore soft-deleted assets
|
||||
download Get asset download URLs
|
||||
stats Count assets by type
|
||||
storage View storage quota and usage
|
||||
models Model configuration helpers
|
||||
service Asset center service subscription
|
||||
|
||||
Run `bl asset <command> --help` for details.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 各命令规格
|
||||
|
||||
以下 `usageArgs` 为命令 metadata 中的值(不含 global flags)。Global flags(`--output`、`--dry-run`、`--quiet` 等)与 console flags(`--workspace-id` 等)由 runtime 自动追加到 help。
|
||||
|
||||
---
|
||||
|
||||
### 5.1 Phase 1 命令
|
||||
|
||||
#### `bl asset list`
|
||||
|
||||
```
|
||||
Description: List model-generated assets with filters and cursor pagination
|
||||
|
||||
Usage: bl asset list [flags]
|
||||
|
||||
Flags:
|
||||
--type <type> Asset type: IMAGE, VIDEO, AUDIO
|
||||
--model <name> Filter by model name
|
||||
--keyword <text> Filter by asset name (substring)
|
||||
--favorited Show only favorited assets
|
||||
--recycle-bin Show soft-deleted assets (recycle bin)
|
||||
--sync-status <status> OSS sync status filter
|
||||
--begin-time <datetime> Filter by generate time start (ISO_LOCAL_DATE_TIME)
|
||||
--end-time <datetime> Filter by generate time end
|
||||
--include-download-url Include signed download URLs
|
||||
--include-thumbnail Include thumbnail URLs
|
||||
--thumbnail-width <px> Thumbnail width
|
||||
--thumbnail-height <px> Thumbnail height
|
||||
--page-size <n> Page size (default: 10, max: 100)
|
||||
--next-token <token> Cursor for next page
|
||||
--pre-token <token> Cursor for previous page
|
||||
|
||||
Examples:
|
||||
bl asset list
|
||||
bl asset list --type IMAGE --model qwen-image-3.0
|
||||
bl asset list --favorited --page-size 20
|
||||
bl asset list --recycle-bin
|
||||
bl asset list --keyword landscape --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #1 查看资产列表
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset get <asset-id>`
|
||||
|
||||
```
|
||||
Description: Get full details of a model-generated asset
|
||||
|
||||
Usage: bl asset get <asset-id> [flags]
|
||||
|
||||
Arguments:
|
||||
<asset-id> Asset ID to query
|
||||
|
||||
Flags:
|
||||
--asset-id <id> Asset ID (alternative to positional)
|
||||
--include-download-url Include signed download URL
|
||||
--include-thumbnail Include thumbnail URL
|
||||
--thumbnail-width <px> Thumbnail width
|
||||
--thumbnail-height <px> Thumbnail height
|
||||
|
||||
Examples:
|
||||
bl asset get asset-001
|
||||
bl asset get asset-001 --include-download-url --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #2 查看资产详情
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset favorite`
|
||||
|
||||
```
|
||||
Description: Add assets to favorites
|
||||
|
||||
Usage: bl asset favorite --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset favorite --id asset-001
|
||||
bl asset favorite --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** #3 收藏
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset unfavorite`
|
||||
|
||||
```
|
||||
Description: Remove assets from favorites
|
||||
|
||||
Usage: bl asset unfavorite --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset unfavorite --id asset-001
|
||||
bl asset unfavorite --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** #3 取消收藏
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset delete`
|
||||
|
||||
```
|
||||
Description: Delete assets (soft delete to recycle bin by default)
|
||||
|
||||
Usage: bl asset delete --id <asset-id> [--id <asset-id>...] [flags]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
--permanent Permanently delete (cannot be restored)
|
||||
|
||||
Examples:
|
||||
bl asset delete --id asset-001
|
||||
bl asset delete --id asset-001 --id asset-002
|
||||
bl asset delete --id asset-001 --permanent
|
||||
```
|
||||
|
||||
**PRD 映射:** #4 删除资产、#5 批量删除
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset restore`
|
||||
|
||||
```
|
||||
Description: Restore soft-deleted assets from recycle bin
|
||||
|
||||
Usage: bl asset restore --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset restore --id asset-001
|
||||
bl asset restore --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** 补充能力(配合回收站)
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset download`
|
||||
|
||||
```
|
||||
Description: Get signed download URLs for assets
|
||||
|
||||
Usage: bl asset download --id <asset-id> [--id <asset-id>...] [--out <path>]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
--out <path> Save file to path (only when exactly one --id)
|
||||
|
||||
Examples:
|
||||
bl asset download --id asset-001
|
||||
bl asset download --id asset-001 --out ./image.png
|
||||
bl asset download --id asset-001 --id asset-002 --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #6 下载资产
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset stats`
|
||||
|
||||
```
|
||||
Description: Count model-generated assets by type
|
||||
|
||||
Usage: bl asset stats [flags]
|
||||
|
||||
Flags:
|
||||
--type <type> Filter by asset type
|
||||
--model <name> Filter by model name
|
||||
--keyword <text> Filter by asset name
|
||||
--favorited Count only favorited assets
|
||||
--recycle-bin Count soft-deleted assets
|
||||
--sync-failed Also count assets with failed OSS sync
|
||||
--begin-time <datetime> Filter by generate time start
|
||||
--end-time <datetime> Filter by generate time end
|
||||
|
||||
Examples:
|
||||
bl asset stats
|
||||
bl asset stats --sync-failed
|
||||
bl asset stats --type IMAGE --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #7 查看资产统计
|
||||
|
||||
**text 输出示例:**
|
||||
|
||||
```
|
||||
Total: 200
|
||||
Image: 150
|
||||
Video: 30
|
||||
Audio: 20
|
||||
Sync failed: 5 # 仅 --sync-failed 时出现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset storage`
|
||||
|
||||
```
|
||||
Description: View storage quota, usage, and overage pricing
|
||||
|
||||
Usage: bl asset storage [flags]
|
||||
|
||||
Examples:
|
||||
bl asset storage
|
||||
bl asset storage --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #14 查看容量信息
|
||||
|
||||
**text 输出示例:**
|
||||
|
||||
```
|
||||
Used: 1.2 GB
|
||||
Free quota: 5.0 GB
|
||||
Overage: ¥0.12/GB/month
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Phase 2/3 可选命令
|
||||
|
||||
#### `bl asset models list`
|
||||
|
||||
```
|
||||
Description: List managed models grouped by asset type
|
||||
|
||||
Usage: bl asset models list
|
||||
|
||||
Examples:
|
||||
bl asset models list --output json
|
||||
```
|
||||
|
||||
用途:配合 `bl asset list --model` 时查阅可用 modelId。
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset service status`
|
||||
|
||||
```
|
||||
Description: Check whether asset center service is enabled
|
||||
|
||||
Usage: bl asset service status
|
||||
|
||||
Examples:
|
||||
bl asset service status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 6. PRD 覆盖矩阵
|
||||
|
||||
| PRD # | 功能 | CLI 命令 | Phase | 状态 |
|
||||
| ----- | ------------- | ------------------------------- | ----- | ---------------------------------- |
|
||||
| 1 | 查看资产列表 | `asset list` | 1 | ✅ 可开发 |
|
||||
| 2 | 查看资产详情 | `asset get` | 1 | ✅ 可开发 |
|
||||
| 3 | 收藏/取消收藏 | `asset favorite` / `unfavorite` | 1 | ✅ 可开发 |
|
||||
| 4 | 删除资产 | `asset delete` | 1 | ✅ 可开发 |
|
||||
| 5 | 批量删除 | `asset delete`(多 `--id`) | 1 | ✅ 可开发 |
|
||||
| 6 | 下载资产 | `asset download` | 1 | ✅ 可开发 |
|
||||
| 7 | 查看资产统计 | `asset stats` | 1 | ✅ 可开发(转存失败用 workaround) |
|
||||
| 14 | 查看容量信息 | `asset storage` | 1 | ✅ 可开发 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 典型工作流
|
||||
|
||||
### 7.1 首次使用
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
bl config set workspace_id ws-xxxxx
|
||||
bl asset service status # 可选:确认已开通
|
||||
bl asset storage # 查看容量
|
||||
```
|
||||
|
||||
### 7.2 浏览与筛选
|
||||
|
||||
```bash
|
||||
bl asset list
|
||||
bl asset list --type IMAGE --model qwen-image-3.0 --keyword landscape
|
||||
bl asset list --favorited
|
||||
bl asset list --recycle-bin
|
||||
bl asset get asset-001 --include-download-url
|
||||
bl asset stats
|
||||
bl asset stats --sync-failed
|
||||
```
|
||||
|
||||
### 7.3 资产管理
|
||||
|
||||
```bash
|
||||
bl asset favorite --id asset-001
|
||||
bl asset unfavorite --id asset-001
|
||||
bl asset delete --id asset-001
|
||||
bl asset delete --id asset-001 --id asset-002
|
||||
bl asset restore --id asset-001
|
||||
bl asset download --id asset-001 --out ./image.png
|
||||
```
|
||||
|
||||
### 7.5 脚本翻页(JSON)
|
||||
|
||||
```bash
|
||||
# 第一页
|
||||
bl asset list --page-size 50 --output json
|
||||
# 后续页(使用响应中的 nextToken)
|
||||
bl asset list --page-size 50 --next-token 1000 --output json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 与现有命令的风格对齐
|
||||
|
||||
| 参考命令 | 对齐点 |
|
||||
| ------------------------------ | -------------------------------------------------- |
|
||||
| `bl app list` | console gateway 调用、dry-run 输出 `{ api, data }` |
|
||||
| `bl dataset list` | text 表格 + json items 结构 |
|
||||
| `bl deploy list/get/create` | 产品域子命令命名、多级 path |
|
||||
| `bl memory profile get/create` | 三级 path 子组 |
|
||||
| `bl quota list` | `zeldaHttp.*` API 名、响应 extract |
|
||||
| `bl video download` | `--out` 落盘 |
|
||||
| `bl usage stats` | `requireWorkspaceId`、console E2E 模式 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 版本 | 说明 |
|
||||
| ---------- | ---- | ---------------------------------------------- |
|
||||
| 2026-07-09 | 0.1 | 初稿:命令树、PRD 映射、分 Phase 规格 |
|
||||
| 2026-08-07 | 0.2 | 取消 OSS 转存命令(`oss *` / `transfer list`) |
|
||||
@@ -1,408 +0,0 @@
|
||||
# 资产中心 CLI 设计文档
|
||||
|
||||
> 本文档描述 `bl asset` 命令族的技术设计方案,供开发、评审与联调使用。
|
||||
> 接口字段细节见同目录 [api-doc.md](./api-doc.md);命令路径与 help 结构见 [COMMAND-TREE.md](./COMMAND-TREE.md)。
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
百炼资产中心(Asset Center)提供模型生成资产的存储、检索、收藏、删除与容量管理能力。产品 PRD 要求 CLI 覆盖以下模块:
|
||||
|
||||
| 模块 | PRD 能力 |
|
||||
| -------- | ----------------------------------------------- |
|
||||
| 资产管理 | 列表、详情、收藏/取消收藏、删除、批量删除、下载 |
|
||||
| 资产统计 | 总量、按类型统计 |
|
||||
| 容量 | 已用容量、免费额度、超额单价 |
|
||||
|
||||
后端接口通过 **Zelda HTTP 网关** 暴露,Base Path 为 `/zelda/api/v1/bailian/asset`,详见 [api-doc.md](./api-doc.md)。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
- 在 `packages/commands` 实现可复用命令库,由 `packages/cli/src/commands.ts` 注册为 `bl asset ...` 产品路径
|
||||
- 遵循 monorepo 分层约定:`commands` 不写产品 bin 前缀;Console Gateway 命令统一 `auth: "console"`
|
||||
- 服务端错误原样透传;CLI 仅对参数校验、缺凭证、网络失败等内部错误发出语义化 `BailianError`
|
||||
- 支持 `--dry-run`、`--output json`、text 表格输出等现有 CLI 惯例
|
||||
|
||||
### 1.3 非目标
|
||||
|
||||
- 不在 `rag` 入口暴露(首期与 `deploy` / `finetune` 一致,仅 `bl`)
|
||||
- 不暴露 `sendMqMessage` 等内部 MQ 接口
|
||||
- 不在 `core` / `runtime` 层硬编码 `bl` 命令名或控制台 URL
|
||||
|
||||
---
|
||||
|
||||
## 2. PRD → API → CLI 映射
|
||||
|
||||
### 2.1 资产管理
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
|
||||
| ----- | ------------- | ------------------------------------------- | --------------------------------------------- | ------------------------------------------ |
|
||||
| 1 | 查看资产列表 | `bl asset list` | `listModelGeneratedAsset` | 游标分页;支持类型/模型/关键词/收藏/回收站 |
|
||||
| 2 | 查看资产详情 | `bl asset get <asset-id>` | `getModelGeneratedAsset` | positional 或 `--asset-id` |
|
||||
| 3 | 收藏/取消收藏 | `bl asset favorite` / `bl asset unfavorite` | `batchFavoriteAsset` / `batchUnfavoriteAsset` | 单 ID 也走 batch(长度 1) |
|
||||
| 4 | 删除资产 | `bl asset delete` | `batchDeleteAsset` | 默认 `SOFT_DELETE`(移入回收站) |
|
||||
| 5 | 批量删除 | `bl asset delete` | `batchDeleteAsset` | `--id` 可重复,最多 100 |
|
||||
| 6 | 下载资产 | `bl asset download` | `batchGetAssetDownloadUrl` | 默认输出 URL;单资产可选 `--out` 落盘 |
|
||||
|
||||
**建议补充(API 已有、PRD 未写):**
|
||||
|
||||
| 能力 | CLI 命令 | API Action |
|
||||
| ------------ | ------------------ | ------------------- |
|
||||
| 从回收站恢复 | `bl asset restore` | `batchRestoreAsset` |
|
||||
|
||||
### 2.2 资产统计
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
|
||||
| ----- | ------------ | ---------------- | -------------------------- | --------------------------------------- |
|
||||
| 7 | 查看资产统计 | `bl asset stats` | `countModelGeneratedAsset` | 输出 total / image / video / audio 计数 |
|
||||
|
||||
**转存失败数(PRD 子项):**
|
||||
|
||||
- API 支持 `syncOssDataStatus=SYNC_FAILED` 筛选,但无独立 `failureCount` 字段
|
||||
- **Phase 1 方案**:`bl asset stats --sync-failed` 额外发起一次 count 查询,输出 `sync_failed_count`
|
||||
- **Phase 3 备选**:等后端在 stats 响应中增加专用字段后收敛
|
||||
|
||||
### 2.4 容量
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action |
|
||||
| ----- | ------------ | ------------------ | ----------------- |
|
||||
| 14 | 查看容量信息 | `bl asset storage` | `getStorageQuota` |
|
||||
|
||||
### 2.5 可选扩展(API 有、PRD 未列)
|
||||
|
||||
| CLI 命令 | API Action | 优先级 |
|
||||
| ------------------------------------- | ------------------------------- | -------------------- |
|
||||
| `bl asset service status` | `checkAssetServiceSubscription` | P1 |
|
||||
| `bl asset service enable` / `disable` | `subscribeAssetService` | P2 |
|
||||
| `bl asset models list` | `listModels` | P1(配合 list 筛选) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构与分层
|
||||
|
||||
### 3.1 在 monorepo 中的位置
|
||||
|
||||
```
|
||||
packages/commands/src/commands/asset-center/*.ts ← 命令实现(本目录)
|
||||
↓ export
|
||||
packages/commands/src/index.ts
|
||||
↓ import + map key
|
||||
packages/cli/src/commands.ts ← "asset list": assetList, ...
|
||||
↓
|
||||
packages/runtime (createCli / authStage / registry)
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
- 实现文件按能力组织在本目录
|
||||
- `usageArgs` / `exampleArgs` 不含 `bl` 前缀
|
||||
- 所有 asset 命令 `auth: "console"`;不重复声明 `CONSOLE_AUTH_FLAGS`(runtime 自动注入)
|
||||
|
||||
### 3.2 目录结构
|
||||
|
||||
```
|
||||
asset-center/
|
||||
├── api-doc.md # 后端 API 文档(已有)
|
||||
├── DESIGN.md # 本文档
|
||||
├── COMMAND-TREE.md # 命令树与 help 结构
|
||||
├── types.ts # TypeScript 类型(ModelGeneratedAssetItem 等)
|
||||
├── utils.ts # 公共请求构建、API 调用、响应解析
|
||||
├── list.ts
|
||||
├── get.ts
|
||||
├── favorite.ts
|
||||
├── unfavorite.ts
|
||||
├── delete.ts
|
||||
├── download.ts
|
||||
├── stats.ts
|
||||
└── storage.ts
|
||||
```
|
||||
|
||||
### 3.3 共享层 `utils.ts`
|
||||
|
||||
参考 `token-plan/utils.ts`、`usage/stats.ts` 的 `requireWorkspaceId` 模式。
|
||||
|
||||
#### 3.3.1 API 名称约定
|
||||
|
||||
与 `quota/list.ts` 中 `zeldaHttp.dashscopeModel./zelda/api/v1/...` 类似,资产中心预期为:
|
||||
|
||||
```typescript
|
||||
const ASSET_SERVICE = "bailianAsset"; // ⚠️ 编码前需 spike 确认
|
||||
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
|
||||
|
||||
function assetApi(action: string): string {
|
||||
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
|
||||
}
|
||||
```
|
||||
|
||||
编码第一步用 `bl console call --api <name> --data '{...}'` 验证实际注册名。
|
||||
|
||||
#### 3.3.2 公共请求体
|
||||
|
||||
所有接口继承 `AssetHttpBaseRequest`(见 api-doc §公共请求参数):
|
||||
|
||||
| 字段 | CLI 来源 | 状态 |
|
||||
| ---------------- | --------------------------------------------------------- | ---------- |
|
||||
| `workspace` | `settings.workspaceId`(`--workspace-id` / env / config) | ✅ 已有 |
|
||||
| `tenantId` | 待定 | ⚠️ 需确认 |
|
||||
| `mainAccountUid` | 待定 | ⚠️ 需确认 |
|
||||
| `apiSource` | 固定 `"CLI"` | 实现时写入 |
|
||||
| `aliYunUid` 等 | 网关 session 注入或省略 | 待确认 |
|
||||
|
||||
`requireWorkspaceId(settings, binName)` 在缺少 workspace 时抛出 `BailianError(GENERAL)`,hint 指向 `bl workspace list`。
|
||||
|
||||
#### 3.3.3 调用封装
|
||||
|
||||
```typescript
|
||||
async function callAssetApi<T>(
|
||||
ctx: CommandRunContext,
|
||||
action: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const payload = { ...buildBaseRequest(ctx), ...body };
|
||||
const raw = await ctx.client.console(assetApi(action), payload);
|
||||
return extractAssetResponse<T>(raw);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.4 响应解析
|
||||
|
||||
Console Gateway 响应可能存在多层嵌套(参考 `quota/list.ts` 的 `extractResponseData`):
|
||||
|
||||
1. 剥 gateway 外层:`data` → `DataV2` → `data` → ...
|
||||
2. 到达业务 `Result<T>`:`{ success, code, message, data }`
|
||||
3. 若 `success === false`:抛 `BailianError(GENERAL, message)`,**不翻译、不替换** message
|
||||
4. 成功时返回 `data` 字段
|
||||
|
||||
---
|
||||
|
||||
## 4. 命令实现规范
|
||||
|
||||
### 4.1 通用模式
|
||||
|
||||
每个命令文件遵循:
|
||||
|
||||
```typescript
|
||||
export default defineCommand({
|
||||
description: "...",
|
||||
auth: "console",
|
||||
usageArgs: "...",
|
||||
flags: { ... },
|
||||
exampleArgs: ["...", "--output json"],
|
||||
validate(ctx) { /* 跨 flag 条件校验 */ },
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ api: assetApi("..."), data: { ... } }, format);
|
||||
return;
|
||||
}
|
||||
const data = await callAssetApi(ctx, "actionName", { ... });
|
||||
// text 表格 或 emitResult(json)
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
参考实现:`app/list.ts`(console + dry-run)、`dataset/list.ts`(表格输出)、`video/download.ts`(落盘)。
|
||||
|
||||
### 4.2 分页模型(`asset list`)
|
||||
|
||||
**与 `app list` 不同**:资产列表使用 **id 游标分页**,不是 page/pageSize 页码模式。
|
||||
|
||||
| Flag | API 字段 | 说明 |
|
||||
| -------------- | ----------- | -------------------------- |
|
||||
| `--page-size` | `pageSize` | 默认 10,最大 100 |
|
||||
| `--next-token` | `nextToken` | 下一页游标(来自上次响应) |
|
||||
| `--pre-token` | `preToken` | 上一页游标 |
|
||||
|
||||
JSON 输出保留 `nextToken` / `preToken` / `hasNext` / `hasPre`,便于脚本翻页。
|
||||
|
||||
### 4.3 批量 ID 传参
|
||||
|
||||
批量操作(favorite / unfavorite / delete / restore / download)统一:
|
||||
|
||||
```typescript
|
||||
id: {
|
||||
type: "array",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID(s) to operate on (repeatable, max 100)",
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
CLI 用法:`--id asset-001 --id asset-002` 或多次重复。实现时在 `validate` 中校验 `ids.length <= 100`。
|
||||
|
||||
### 4.4 输出格式
|
||||
|
||||
| 命令 | text 默认 | json |
|
||||
| ---------- | -------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `list` | 表格:assetId / type / name / model / favorited / generateTime | items + pagination |
|
||||
| `get` | 关键字段摘要 | 完整 item |
|
||||
| `stats` | 数字摘要 | `{ total_count, image_count, ... }` |
|
||||
| `storage` | 人类可读字节 + 单价 | 原始 quota 字段 |
|
||||
| 写操作 | 一行确认(affectedCount) | `{ success, affected_count }` |
|
||||
| `download` | URL 列表或 saved 路径 | `{ items: [{ asset_id, download_url }] }` |
|
||||
|
||||
使用 `formatTable`(`dataset/list.ts`)、`formatBytes`(`video/download.ts`)、`emitResult` / `emitBare`。
|
||||
|
||||
### 4.5 条件校验(`validate`)
|
||||
|
||||
| 命令 | 规则 |
|
||||
| ---------- | --------------------------------------------------------- |
|
||||
| `delete` | `--permanent` 映射 `PERMANENT_DELETE`;默认 `SOFT_DELETE` |
|
||||
| 所有 batch | `assetIdList.length <= 100` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键命令 Flag 详设
|
||||
|
||||
### 5.1 `bl asset list`
|
||||
|
||||
| Flag | 类型 | API 映射 | 说明 |
|
||||
| ------------------------ | ---------------- | ---------------------------- | ------------------------------------------------------------ |
|
||||
| `--type` | string (choices) | `assetType` | `IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| `--model` | string | `modelName` | PRD「按模型筛选」 |
|
||||
| `--keyword` | string | `assetName` | PRD「关键词」;是否同时搜 description 待产品确认 |
|
||||
| `--favorited` | switch | `favorited: true` | 仅看收藏 |
|
||||
| `--recycle-bin` | switch | `deleteStatus: SOFT_DELETED` | 仅看回收站 |
|
||||
| `--sync-status` | string (choices) | `syncOssDataStatus` | `NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| `--begin-time` | string | `beginTime` | ISO_LOCAL_DATE_TIME |
|
||||
| `--end-time` | string | `endTime` | ISO_LOCAL_DATE_TIME |
|
||||
| `--include-download-url` | switch | `includeDownloadUrl` | |
|
||||
| `--include-thumbnail` | switch | `includeThumbnail` | |
|
||||
| `--thumbnail-width` | number | `thumbnailWidth` | 配合 thumbnail |
|
||||
| `--thumbnail-height` | number | `thumbnailHeight` | 配合 thumbnail |
|
||||
| `--page-size` | number | `pageSize` | |
|
||||
| `--next-token` | number | `nextToken` | |
|
||||
| `--pre-token` | number | `preToken` | |
|
||||
|
||||
### 5.2 `bl asset get`
|
||||
|
||||
| 参数/Flag | 说明 |
|
||||
| ------------------------------------------ | --------------------------------------- |
|
||||
| `<asset-id>` | positional,primary |
|
||||
| `--asset-id` | 与 positional 二选一(positional 优先) |
|
||||
| `--include-download-url` | |
|
||||
| `--include-thumbnail` | |
|
||||
| `--thumbnail-width` / `--thumbnail-height` | |
|
||||
|
||||
### 5.3 `bl asset delete`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------------- | --------------------------------------------------------- |
|
||||
| `--id` | array, required, max 100 |
|
||||
| `--permanent` | switch → `deleteType: PERMANENT_DELETE`;默认 SOFT_DELETE |
|
||||
|
||||
### 5.4 `bl asset download`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------- | ----------------------------------------------------- |
|
||||
| `--id` | array, required |
|
||||
| `--out` | 仅当 `--id` 恰好 1 个时有效;调用 `downloadFile` 落盘 |
|
||||
|
||||
### 5.5 `bl asset stats`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------------------------------------- | ---------------------------------------------- |
|
||||
| (无 filter) | 默认 `deleteStatus: NORMAL` |
|
||||
| `--recycle-bin` | `deleteStatus: SOFT_DELETED` |
|
||||
| `--sync-failed` | 额外查询 `syncOssDataStatus: SYNC_FAILED` 计数 |
|
||||
| `--type` / `--model` / `--keyword` 等 | 与 list 相同筛选维度(可选) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与待确认项
|
||||
|
||||
### 6.1 P0 — 编码前必须对齐
|
||||
|
||||
| # | 问题 | 影响 | 建议动作 |
|
||||
| --- | ------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- |
|
||||
| 1 | Console API 注册名(`zeldaHttp.{service}./zelda/api/v1/bailian/asset/*`) | 无法调用 | `bl console call` spike;与后端确认 service 名 |
|
||||
| 2 | `tenantId` / `mainAccountUid` 由谁填充 | 所有接口必填 | 确认网关是否从 session 自动注入;否则扩展 config 或新增解析 API |
|
||||
|
||||
### 6.2 P1 — 产品设计
|
||||
|
||||
| # | 问题 | 建议默认 |
|
||||
| --- | -------------------- | ----------------------------------------------- |
|
||||
| 4 | 关键词搜索范围 | 仅 `assetName`;后续可加 `--search-description` |
|
||||
| 5 | PRD 只提 image/video | CLI 暴露 IMAGE/VIDEO/AUDIO(与 API 一致) |
|
||||
| 6 | 下载行为 | 默认输出 URL;单 ID + `--out` 落盘 |
|
||||
| 7 | 永久删除 | 提供 `--permanent`,help 注明不可恢复 |
|
||||
| 8 | 服务未开通 | 不预检查;失败时透传服务端 message |
|
||||
| 9 | 收藏命令形态 | 两个命令 `favorite` / `unfavorite`(语义清晰) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
遵循 [AGENTS.md](../../../../../../AGENTS.md) 错误边界:
|
||||
|
||||
| 场景 | 处理 |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| 缺 `--workspace-id` | `BailianError(GENERAL)` + hint |
|
||||
| 缺 console token | authStage 抛 `BailianError(AUTH)` |
|
||||
| flag 校验失败 | `UsageError` (exit 2) |
|
||||
| HTTP 4xx/5xx / 业务 success=false | `BailianError(GENERAL)`,message **原样透传** |
|
||||
| batch ID > 100 | `UsageError` |
|
||||
|
||||
Console 未登录参考 `mcp/list.ts`:检测 `BailianGateway.Login.NotLogined` 时 hint 指向 `bl auth login --console`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
新建 `packages/cli/tests/e2e/asset.e2e.test.ts`,遵循 [cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)。
|
||||
|
||||
### 8.1 不 skip 层
|
||||
|
||||
- `bl asset` 分组 help
|
||||
- 各子命令 `--help`
|
||||
- 缺参 → exit 2
|
||||
|
||||
### 8.2 Console skip 层(`isConsoleE2EReady()`)
|
||||
|
||||
- 各命令 `--dry-run` 输出 api + data
|
||||
- 真实 `asset list` / `asset storage` 集成(需已开通资产中心的工作空间)
|
||||
|
||||
环境:`BAILIAN_E2E=1` + console `access_token` + `BAILIAN_WORKSPACE_ID`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 注册与文档变更清单
|
||||
|
||||
| 文件 | 变更 |
|
||||
| -------------------------------------------------- | ------------------- |
|
||||
| `packages/commands/src/commands/asset-center/*.ts` | 新建 |
|
||||
| `packages/commands/src/index.ts` | export |
|
||||
| `packages/cli/src/commands.ts` | 注册 map |
|
||||
| `packages/cli/tests/e2e/asset.e2e.test.ts` | 新建 |
|
||||
| `skills/bailian-cli/reference/` | pre-commit 自动生成 |
|
||||
| `README.md` / `README.zh.md` | 发版前补充命令一览 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 分期实施
|
||||
|
||||
### Phase 1 — 核心资产(P0)
|
||||
|
||||
```
|
||||
asset list | get | favorite | unfavorite | delete | restore | download | stats | storage
|
||||
```
|
||||
|
||||
**前置:** §6.1 #1 #2 确认。
|
||||
|
||||
### Phase 2+ — 可选扩展
|
||||
|
||||
```
|
||||
asset models list | service status | service enable/disable
|
||||
```
|
||||
|
||||
> OSS 转存相关命令(`asset-center oss *` / `transfer list`)已取消,不再排期。
|
||||
|
||||
## 11. 参考
|
||||
|
||||
- 命令注册:[docs/agents/command-add-remove.md](../../../../../../docs/agents/command-add-remove.md)
|
||||
- E2E 规范:[docs/agents/cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)
|
||||
- Console 命令样例:`packages/commands/src/commands/app/list.ts`
|
||||
- 游标/表格:`packages/commands/src/commands/quota/list.ts`
|
||||
- workspace 必填:`packages/commands/src/commands/usage/stats.ts`
|
||||
- 文件落盘:`packages/commands/src/commands/video/download.ts`
|
||||
@@ -1,35 +0,0 @@
|
||||
# Asset Center 命令测试报告 — Phase 2
|
||||
|
||||
- **测试时间**: 2026-07-10 09:07:09 (UTC)
|
||||
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
|
||||
- **测试 IMAGE**: `asset_98175cbf83294f7b8ada86657623dcf3`
|
||||
- **测试 VIDEO**: `asset_df026105d2274ff9b8c824058fa23d60`
|
||||
- **策略**: 可逆写操作(favorite/unfavorite 往返);download 到 /tmp 后删除;其余只读
|
||||
- **汇总**: 16 通过 / 0 失败 / 16 总计
|
||||
|
||||
> Phase 1 报告见同目录 [TEST-REPORT.md](./TEST-REPORT.md)(24 项 dry-run + 只读基础验证)
|
||||
|
||||
## Phase 2 测试结果
|
||||
|
||||
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
|
||||
| --- | ---- | ------------------------------------------ | -------- | ------- | ---- | ------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | 下载 | `asset-center download (IMAGE)` | 真实调用 | ✅ PASS | 0 | 20045ms | saved /tmp/asset-center-test-asset_98175cbf83294f7b8ada86657623dcf3.png (1449847 bytes, reported 1.4 MB) |
|
||||
| 2 | 查询 | `asset-center get --include-download-url` | 真实调用 | ✅ PASS | 0 | 21226ms | download_url present |
|
||||
| 3 | 查询 | `asset-center list --include-download-url` | 真实调用 | ✅ PASS | 0 | 19057ms | items contain download_url |
|
||||
| 4 | 查询 | `asset-center list --next-token` | 真实调用 | ✅ PASS | 0 | 19584ms | page2=3 items, overlap=0, has_pre=true |
|
||||
| 5 | 统计 | `asset-center stats --type IMAGE` | 真实调用 | ✅ PASS | 0 | 18830ms | image=7, total=7 |
|
||||
| 6 | 统计 | `asset-center stats --sync-failed` | 真实调用 | ✅ PASS | 0 | 19084ms | total=27, sync_failed=0 |
|
||||
| 7 | 查询 | `asset-center list --recycle-bin` | 真实调用 | ✅ PASS | 0 | 19314ms | 0 soft-deleted item(s) |
|
||||
| 8 | 输出 | `asset-center list (text)` | 真实调用 | ✅ PASS | 0 | 17840ms | 4 lines table output |
|
||||
| 9 | 收藏 | `asset-center favorite (真实)` | 真实调用 | ✅ PASS | 0 | 22082ms | affected=1 |
|
||||
| 10 | 收藏 | `get 验证 favorited=true` | 真实调用 | ✅ PASS | 0 | 18865ms | favorited=true ✓ |
|
||||
| 11 | 查询 | `list --favorited 含测试资产` | 真实调用 | ✅ PASS | 0 | 22120ms | found in favorited list |
|
||||
| 12 | 收藏 | `asset-center unfavorite (真实)` | 真实调用 | ✅ PASS | 0 | 22133ms | affected=1 |
|
||||
| 13 | 收藏 | `get 验证 favorited=false (恢复)` | 真实调用 | ✅ PASS | 0 | 27797ms | favorited=false ✓ |
|
||||
| 14 | 收藏 | `favorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 22947ms | affected=2 |
|
||||
| 15 | 收藏 | `unfavorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 15385ms | affected=2 |
|
||||
| 16 | 边界 | `get 不存在的 asset-id` | 真实调用 | ✅ PASS | 1 | 12207ms | exit 1, 服务端错误原样透传: "资产不存在" |
|
||||
|
||||
## 边界行为说明
|
||||
|
||||
查询不存在的 `asset-id` 时,服务端返回业务错误 **「资产不存在」**,CLI 按约定 **原样透传**(exit code 1),不会替换为本地文案。这与 AGENTS.md 错误处理边界一致。
|
||||
@@ -1,36 +0,0 @@
|
||||
# Asset Center 命令测试报告
|
||||
|
||||
- **测试时间**: 2026-07-10 08:41:06 (UTC)
|
||||
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
|
||||
- **样本 Asset ID**: `asset_df026105d2274ff9b8c824058fa23d60`
|
||||
- **策略**: 只读命令真实调用;写操作/下载一律 `--dry-run`
|
||||
- **汇总**: 24 通过 / 0 失败 / 24 总计
|
||||
|
||||
## 测试结果
|
||||
|
||||
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
|
||||
| --- | ---- | ---------------------------------- | -------- | ------- | ---- | ------- | -------------------------------------------------------------- |
|
||||
| 1 | 查询 | `asset-center list` | 真实调用 | ✅ PASS | 0 | 18962ms | 3 item(s), next=94 |
|
||||
| 2 | 查询 | `asset-center list --type IMAGE` | 真实调用 | ✅ PASS | 0 | 16411ms | 2 item(s), next=90 |
|
||||
| 3 | 查询 | `asset-center get` | 真实调用 | ✅ PASS | 0 | 16541ms | {gmtModified, aliyunUid, generateTime, aliyunMainId} |
|
||||
| 4 | 统计 | `asset-center stats` | 真实调用 | ✅ PASS | 0 | 14226ms | total=27 |
|
||||
| 5 | 统计 | `asset-center storage` | 真实调用 | ✅ PASS | 0 | 18622ms | {used_storage_size, free_storage_quota, extra_storage_price} |
|
||||
| 9 | 查询 | `asset-center list --dry-run` | dry-run | ✅ PASS | 0 | 23855ms | dry-run → /zelda/api/v1/bailian/asset/listModelGeneratedAsset |
|
||||
| 10 | 查询 | `asset-center get --dry-run` | dry-run | ✅ PASS | 0 | 17398ms | dry-run → /zelda/api/v1/bailian/asset/getModelGeneratedAsset |
|
||||
| 11 | 收藏 | `asset-center favorite` | dry-run | ✅ PASS | 0 | 16471ms | dry-run → /zelda/api/v1/bailian/asset/batchFavoriteAsset |
|
||||
| 12 | 收藏 | `asset-center unfavorite` | dry-run | ✅ PASS | 0 | 19724ms | dry-run → /zelda/api/v1/bailian/asset/batchUnfavoriteAsset |
|
||||
| 13 | 删除 | `asset-center delete` | dry-run | ✅ PASS | 0 | 20612ms | dry-run → /zelda/api/v1/bailian/asset/batchDeleteAsset |
|
||||
| 14 | 下载 | `asset-center download` | dry-run | ✅ PASS | 0 | 20348ms | dry-run → /zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl |
|
||||
| 15 | 统计 | `asset-center stats --dry-run` | dry-run | ✅ PASS | 0 | 14898ms | dry-run → /zelda/api/v1/bailian/asset/countModelGeneratedAsset |
|
||||
| 16 | 统计 | `asset-center storage --dry-run` | dry-run | ✅ PASS | 0 | 14305ms | dry-run → /zelda/api/v1/bailian/asset/getStorageQuota |
|
||||
| 21 | 校验 | `asset-center get (缺 asset-id)` | 参数校验 | ✅ PASS | 2 | 15202ms | Error: Missing required flag: --asset-id |
|
||||
| 22 | 校验 | `asset-center favorite (缺 --id)` | 参数校验 | ✅ PASS | 2 | 16697ms | Error: Missing required flag: --id |
|
||||
| 23 | 校验 | `asset-center download (缺 --out)` | 参数校验 | ✅ PASS | 2 | 18803ms | Error: Missing required flag: --out |
|
||||
|
||||
## 模式说明
|
||||
|
||||
| 模式 | 说明 |
|
||||
| -------- | -------------------------------------------------- |
|
||||
| 真实调用 | 只读 API,不修改数据 |
|
||||
| dry-run | 输出 `{ api, data, gateway }` 请求体,不发起写操作 |
|
||||
| 参数校验 | 预期 exit code 2(用法错误) |
|
||||
@@ -1,624 +0,0 @@
|
||||
# BailianAssetZeldaHttpService API 文档
|
||||
|
||||
通过 Zelda 网关调用 bailian-asset HTTP 接口文档。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base Path**: `/zelda/api/v1/bailian/asset`
|
||||
- **Method**: POST
|
||||
- **Content-Type**: `application/json`
|
||||
- **Accept**: `application/json`
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
所有接口返回 `Result<T>` 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "string",
|
||||
"success": true,
|
||||
"code": "string",
|
||||
"message": "string",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | ------- | ---------------------- |
|
||||
| requestId | String | 请求唯一ID |
|
||||
| success | Boolean | 是否成功 |
|
||||
| code | String | 错误码(失败时返回) |
|
||||
| message | String | 错误信息(失败时返回) |
|
||||
| data | Object | 业务数据(成功时返回) |
|
||||
|
||||
## 公共请求参数(基类字段)
|
||||
|
||||
所有接口请求体均继承自 `AssetHttpBaseRequest`,包含以下公共字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------ | ---- | ---------------------------------------------- |
|
||||
| requestId | String | 否 | 请求唯一ID |
|
||||
| apiSource | String | 否 | 调用入口渠道,如 OpenAPI、CloudSDK |
|
||||
| tenantId | String | 是 | 内部租户ID |
|
||||
| workspace | String | 是 | 业务空间ID |
|
||||
| aliYunUid | String | 否 | 阿里云子账号ID |
|
||||
| mainAccountUid | String | 是 | 阿里云主账号ID |
|
||||
| callerType | String | 否 | 账号类型:partner/customer/sub/AssumedRoleUser |
|
||||
| callerParentId | Long | 否 | 调用者所属主账号ID |
|
||||
| accessKeyId | String | 否 | STS认证:用户AccessKeyId |
|
||||
| securityToken | String | 否 | STS认证:扮演者的STS Token |
|
||||
|
||||
---
|
||||
|
||||
## 1. 开通/关闭资产中心服务
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/subscribeAssetService`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------ | ------------ | ---- | --------------------------------------- |
|
||||
| action | String(Enum) | 是 | 操作类型:`ENABLE`-开通,`DISABLE`-关闭 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------ | ------------ | ------------ |
|
||||
| status | String(Enum) | 当前服务状态 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"action": "ENABLE"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 批量收藏资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchFavoriteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ---------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待收藏的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否收藏成功 |
|
||||
| affectedCount | Integer | 实际被收藏的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002", "asset-003"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 批量取消收藏资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchUnfavoriteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | -------------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待取消收藏的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | ------------------------ |
|
||||
| success | Boolean | 是否取消收藏成功 |
|
||||
| affectedCount | Integer | 实际被取消收藏的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 批量删除资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchDeleteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ----------------------------------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待删除的资产ID列表,长度不超过 100 |
|
||||
| deleteType | String(Enum) | 是 | 删除类型:`SOFT_DELETE`-软删除,`PERMANENT_DELETE`-彻底删除 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否删除成功 |
|
||||
| affectedCount | Integer | 实际被删除的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"],
|
||||
"deleteType": "SOFT_DELETE"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 批量恢复软删除资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchRestoreAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ---------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待恢复的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否恢复成功 |
|
||||
| affectedCount | Integer | 实际被恢复的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 分页查询模型生成资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/listModelGeneratedAsset`
|
||||
|
||||
采用 id 游标分页,默认 pageSize=10,最大 100。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------------ | ---- | ---------------------------------------------------------------------------------- |
|
||||
| preToken | Long | 否 | 向前翻页游标 |
|
||||
| nextToken | Long | 否 | 向后翻页游标(查询下一页时传入上一次响应的 nextToken) |
|
||||
| pageSize | Integer | 否 | 每页大小,默认 10,最大 100 |
|
||||
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
|
||||
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL,默认 false |
|
||||
| thumbnailWidth | Integer | 否 | 缩放图宽度(像素),includeThumbnail=true 时生效 |
|
||||
| thumbnailHeight | Integer | 否 | 缩放图高度(像素),includeThumbnail=true 时生效 |
|
||||
| softDeleteTimeOrder | String(Enum) | 否 | 软删除时间排序方式:`ASC`-正序,`DESC`-倒序;仅在 deleteStatus=SOFT_DELETED 时有效 |
|
||||
| assetType | String(Enum) | 否 | 资产类型:`IMAGE`-图片,`VIDEO`-视频,`AUDIO`-音频 |
|
||||
| favorited | Boolean | 否 | 是否被收藏 |
|
||||
| assetName | String | 否 | 资产名称(子串模糊匹配) |
|
||||
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
|
||||
| trusted | Boolean | 否 | 是否可信 |
|
||||
| modelType | String | 否 | 生成资产的模型类型 |
|
||||
| modelName | String | 否 | 生成资产的模型型号 |
|
||||
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态:`NOT_SYNCED` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态:`NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| deleteStatus | String(Enum) | 否 | 删除状态:`NORMAL` / `SOFT_DELETED` / `PERMANENTLY_DELETED` |
|
||||
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME,如 `2023-10-25T14:30:00` |
|
||||
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME,如 `2023-10-25T14:30:00` |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | ----------------------------- | ------------ |
|
||||
| dataList | List<ModelGeneratedAssetItem> | 资产列表 |
|
||||
| preToken | Long | 前一页游标 |
|
||||
| nextToken | Long | 下一页游标 |
|
||||
| hasNext | Boolean | 是否有下一页 |
|
||||
| hasPre | Boolean | 是否有前一页 |
|
||||
|
||||
**ModelGeneratedAssetItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| id | Long | 主键 ID(分页游标 token) |
|
||||
| gmtCreate | Date | 创建时间 |
|
||||
| gmtModified | Date | 修改时间 |
|
||||
| workspaceId | String | 工作空间ID |
|
||||
| tenantId | String | 租户ID |
|
||||
| aliyunUid | String | 阿里云子账号ID |
|
||||
| aliyunMainId | String | 阿里云主账号ID |
|
||||
| assetId | String | 资产 ID |
|
||||
| assetType | String | 资产类型:IMAGE/VIDEO/AUDIO |
|
||||
| assetSource | String | 资产来源:MODEL_GENERATED/OFFICIAL/USER_UPLOADED |
|
||||
| favorited | Boolean | 是否被收藏 |
|
||||
| assetName | String | 资产名称 |
|
||||
| assetDescription | String | 资产描述 |
|
||||
| assetSize | Long | 资产大小(字节) |
|
||||
| md5 | String | 资产 MD5 |
|
||||
| ossBucket | String | 资产所在 OSS Bucket |
|
||||
| ossKey | String | 资产在 OSS bucket 中的 key |
|
||||
| region | String | 工作空间地域 |
|
||||
| ossRegion | String | 资产所在 OSS bucket 的地域 |
|
||||
| trusted | Boolean | 是否可信 |
|
||||
| modelType | String | 模型类型 |
|
||||
| modelName | String | 模型型号 |
|
||||
| syncWhiteListStatus | String | 同步白名单状态 |
|
||||
| syncOssDataStatus | String | 同步 OSS 数据状态 |
|
||||
| deleteStatus | String | 删除状态:NORMAL/SOFT_DELETED/PERMANENTLY_DELETED |
|
||||
| generateTime | Long | 资产生成时间戳(毫秒) |
|
||||
| softDeleteDays | Integer | 已被软删除的天数(仅当 deleteStatus=SOFT_DELETED 且请求 softDeleteTimeOrder 时返回) |
|
||||
| originalOssUrl | String | 原始 OSS URL |
|
||||
| downloadUrl | String | 文件下载链接(仅当请求 includeDownloadUrl=true 时返回) |
|
||||
| thumbnailUrl | String | 资产缩放图 URL(仅当请求 includeThumbnail=true 时返回;视频返回首帧缩放图,图片返回缩放图,音频返回 null) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"pageSize": 20,
|
||||
"includeDownloadUrl": true,
|
||||
"includeThumbnail": true,
|
||||
"thumbnailWidth": 200,
|
||||
"thumbnailHeight": 200,
|
||||
"assetType": "IMAGE",
|
||||
"favorited": true,
|
||||
"beginTime": "2024-01-01T00:00:00",
|
||||
"endTime": "2024-12-31T23:59:59"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"dataList": [
|
||||
{
|
||||
"id": 1001,
|
||||
"assetId": "asset-001",
|
||||
"assetType": "IMAGE",
|
||||
"assetName": "generated_image_01.png",
|
||||
"assetDescription": "A landscape painting",
|
||||
"favorited": true,
|
||||
"generateTime": 1700000000000
|
||||
}
|
||||
],
|
||||
"nextToken": 1000,
|
||||
"hasNext": true,
|
||||
"hasPre": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 统计模型生成资产数量
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/countModelGeneratedAsset`
|
||||
|
||||
查询条件与 `listModelGeneratedAsset` 一致(不需要分页参数),按资产类型分组返回数量。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------------ | ---- | ------------------------------------------------ |
|
||||
| assetType | String(Enum) | 否 | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| favorited | Boolean | 否 | 是否被收藏 |
|
||||
| assetName | String | 否 | 资产名称(子串模糊匹配) |
|
||||
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
|
||||
| trusted | Boolean | 否 | 是否可信 |
|
||||
| modelType | String | 否 | 模型类型 |
|
||||
| modelName | String | 否 | 模型型号 |
|
||||
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态 |
|
||||
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态 |
|
||||
| deleteStatus | String(Enum) | 否 | 删除状态 |
|
||||
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME |
|
||||
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------- | ---- | ---------------- |
|
||||
| imageCount | Long | 图片类型资产数量 |
|
||||
| videoCount | Long | 视频类型资产数量 |
|
||||
| audioCount | Long | 音频类型资产数量 |
|
||||
| totalCount | Long | 总资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"deleteStatus": "NORMAL"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"imageCount": 150,
|
||||
"videoCount": 30,
|
||||
"audioCount": 20,
|
||||
"totalCount": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 批量获取资产下载链接
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl`
|
||||
|
||||
一次最多获取 100 个资产的下载链接。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ------------------------------------------ |
|
||||
| assetIdList | List<String> | 是 | 待获取下载链接的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----- | -------------------------- | -------------------------------- |
|
||||
| items | List<AssetDownloadUrlItem> | 资产下载链接列表,按请求顺序返回 |
|
||||
|
||||
**AssetDownloadUrlItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ------ | ---------------------------------------------------------- |
|
||||
| assetId | String | 资产 ID |
|
||||
| downloadUrl | String | 资产下载链接(带签名);资产不存在或缺少 OSS 信息时为 null |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002", "asset-003"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 查询模型生成资产详情
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/getModelGeneratedAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------ | ------- | ---- | ------------------------------------------------ |
|
||||
| assetId | String | 是 | 待查询的资产 ID |
|
||||
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
|
||||
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL,默认 false |
|
||||
| thumbnailWidth | Integer | 否 | 缩放图宽度(像素),includeThumbnail=true 时生效 |
|
||||
| thumbnailHeight | Integer | 否 | 缩放图高度(像素),includeThumbnail=true 时生效 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ----------------------- | ------------------------ |
|
||||
| item | ModelGeneratedAssetItem | 资产详情(结构同第12节) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetId": "asset-001",
|
||||
"includeDownloadUrl": true,
|
||||
"includeThumbnail": true,
|
||||
"thumbnailWidth": 200,
|
||||
"thumbnailHeight": 200
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. 获取存储额度与用量
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/getStorageQuota`
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数(`workspace`、`tenantId` 必填)。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------------- | ------ | --------------------------------------------- |
|
||||
| freeStorageQuota | Long | 平台免费存储额度(单位:字节) |
|
||||
| usedStorageSize | Long | 当前用户已使用的存储量(单位:字节) |
|
||||
| extraStoragePrice | String | 超出免费额度的费用说明(如 "¥0.12元/GB/月") |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. 通用 MQ 消息发送
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/sendMqMessage`
|
||||
|
||||
向指定的 RocketMQ Producer 发送 JSON 格式的消息。producerType 对应 `EnumRocketMqProducerType` 枚举的 code 值,mainAccountUid 作为消息路由 key。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------ | ---- | --------------------------------------------------------------------------------------------------- |
|
||||
| producerType | String | 是 | 生产者类型:`WHITE_LIST_ASSET_PRODUCER` / `OSS_DATA_HANDEL_PRODUCER` / `ORIGIN_ASSET_INFO_PRODUCER` |
|
||||
| messageBody | String | 是 | JSON 格式的消息体字符串 |
|
||||
| messageKey | String | 否 | 消息 key(可选,为空时默认使用 mainAccountUid) |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | ------------ |
|
||||
| success | Boolean | 是否发送成功 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"producerType": "ORIGIN_ASSET_INFO_PRODUCER",
|
||||
"messageBody": "{\"time\":1700000000000,\"modelId\":\"model-abc\",\"type\":\"IMAGE\",\"workspace\":\"ws-xxxxx\",\"ossUrl\":\"oss://my-bucket/path/to/asset.png\"}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. 查询模型列表
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/listModels`
|
||||
|
||||
返回当前服务管理的模型配置列表,按资产类型分组,包含每个模型的ID及是否可信标识。
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ---------------- | ---------------------------- |
|
||||
| modelGroups | List<ModelGroup> | 按资产类型分组的模型配置列表 |
|
||||
|
||||
**ModelGroup 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | --------------- | ------------------------------------- |
|
||||
| assetType | String(Enum) | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| models | List<ModelItem> | 该类型下管理的模型列表 |
|
||||
|
||||
**ModelItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | -------------- |
|
||||
| modelId | String | 模型ID |
|
||||
| trusted | Boolean | 该模型是否可信 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"modelGroups": [
|
||||
{
|
||||
"assetType": "IMAGE",
|
||||
"models": [
|
||||
{ "modelId": "qwen-image-3.0", "trusted": true },
|
||||
{ "modelId": "qwen-image-3.0-pro", "trusted": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"models": [
|
||||
{ "modelId": "wan2.7-t2v", "trusted": true },
|
||||
{ "modelId": "wan2.7-i2v", "trusted": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. 查询用户是否已开通资产中心服务
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/checkAssetServiceSubscription`
|
||||
|
||||
查询当前用户是否已开通资产中心服务。
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数(`mainAccountUid` 必填)。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | ------------------------------------------------- |
|
||||
| enabled | Boolean | 是否已开通资产中心服务:true-已开通,false-未开通 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,67 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
...ASSET_ID_FLAG,
|
||||
permanent: {
|
||||
type: "switch",
|
||||
description: "Permanently delete assets (cannot be restored)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center delete` — 删除资产(默认软删到回收站)。
|
||||
*
|
||||
* 软删可恢复;--permanent 为永久删除。支持重复 --id(单次最多 100 个)。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Delete assets (soft delete to recycle bin by default)",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...] [flags]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002", "--id asset-001 --permanent"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const deleteType = flags.permanent ? "PERMANENT_DELETE" : "SOFT_DELETE";
|
||||
const body = { assetIdList, deleteType };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchDeleteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchDeleteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const verb = flags.permanent ? "Permanently deleted" : "Deleted";
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`${verb} ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult(
|
||||
{ affected_count: data.affectedCount ?? assetIdList.length, delete_type: deleteType },
|
||||
format,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetDownloadResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload } from "./utils.ts";
|
||||
|
||||
const DOWNLOAD_FLAGS = {
|
||||
id: {
|
||||
type: "string",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID to get download URL for",
|
||||
required: true,
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center download` — 通过资产 ID 获取签名下载链接。
|
||||
*
|
||||
* 调用 batchGetAssetDownloadUrl,输出 download URL,不落盘。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Get a signed download URL for an asset by ID",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id>",
|
||||
flags: DOWNLOAD_FLAGS,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --output json", "--id asset-001 --quiet"],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetId = flags.id;
|
||||
const body = { assetIdList: [assetId] };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
asset_id: assetId,
|
||||
action: "download",
|
||||
...dryRunPayload(settings, identity.binName, ASSET_API.batchGetAssetDownloadUrl, body),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetDownloadResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchGetAssetDownloadUrl,
|
||||
body,
|
||||
);
|
||||
|
||||
const url = data.items?.[0]?.downloadUrl;
|
||||
if (!url) {
|
||||
throw new BailianError(`No download URL available for ${assetId}.`, ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ asset_id: assetId, download_url: url }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("AssetId", 16)} ${assetId}`);
|
||||
emitBare(`${padEnd("DownloadUrl", 16)} ${url}`);
|
||||
},
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center favorite` — 收藏一个或多个资产。
|
||||
*
|
||||
* 支持重复 --id(单次最多 100 个),调用 batchFavoriteAsset。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Add assets to favorites",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...]",
|
||||
flags: ASSET_ID_FLAG,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const body = { assetIdList };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchFavoriteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchFavoriteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`Favorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetGetResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload, formatGenerateTime } from "./utils.ts";
|
||||
|
||||
const GET_FLAGS = {
|
||||
assetId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Asset ID to query",
|
||||
required: true,
|
||||
},
|
||||
includeDownloadUrl: {
|
||||
type: "switch",
|
||||
description: "Include signed download URL",
|
||||
},
|
||||
includeThumbnail: {
|
||||
type: "switch",
|
||||
description: "Include thumbnail URL",
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail width in pixels",
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail height in pixels",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center get` — 按 ID 查询单个资产详情。
|
||||
*
|
||||
* 可选 --include-download-url / --include-thumbnail 获取签名 URL。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Get full details of a model-generated asset",
|
||||
auth: "console",
|
||||
usageArgs: "--asset-id <id> [flags]",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: [
|
||||
"--asset-id asset-001",
|
||||
"--asset-id asset-001 --include-download-url --output json",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetId = flags.assetId;
|
||||
|
||||
const body: Record<string, unknown> = { assetId };
|
||||
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
|
||||
if (flags.includeThumbnail) body.includeThumbnail = true;
|
||||
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
|
||||
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.getModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetGetResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.getModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const item = data.item;
|
||||
if (!item) {
|
||||
emitBare("Asset not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(item, format);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("AssetId", 16)} ${item.assetId ?? "-"}`);
|
||||
emitBare(`${padEnd("Type", 16)} ${item.assetType ?? "-"}`);
|
||||
emitBare(`${padEnd("Name", 16)} ${item.assetName ?? "-"}`);
|
||||
emitBare(`${padEnd("Description", 16)} ${item.assetDescription ?? "-"}`);
|
||||
emitBare(`${padEnd("Model", 16)} ${item.modelName ?? "-"}`);
|
||||
emitBare(`${padEnd("Favorited", 16)} ${item.favorited ? "yes" : "no"}`);
|
||||
emitBare(`${padEnd("Generated", 16)} ${formatGenerateTime(item.generateTime)}`);
|
||||
if (item.downloadUrl) emitBare(`${padEnd("DownloadUrl", 16)} ${item.downloadUrl}`);
|
||||
if (item.thumbnailUrl) emitBare(`${padEnd("ThumbnailUrl", 16)} ${item.thumbnailUrl}`);
|
||||
},
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, formatTable } from "bailian-cli-runtime";
|
||||
import type { AssetListResponse, ModelGeneratedAssetItem } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_LIST_FILTER_FLAGS,
|
||||
buildListFilterBody,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
formatGenerateTime,
|
||||
} from "./utils.ts";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
...ASSET_LIST_FILTER_FLAGS,
|
||||
includeDownloadUrl: {
|
||||
type: "switch",
|
||||
description: "Include signed download URLs in the response",
|
||||
},
|
||||
includeThumbnail: {
|
||||
type: "switch",
|
||||
description: "Include thumbnail URLs in the response",
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail width in pixels",
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail height in pixels",
|
||||
},
|
||||
pageSize: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Results per page (default: 10, max: 100)",
|
||||
},
|
||||
nextToken: {
|
||||
type: "number",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the next page",
|
||||
},
|
||||
preToken: {
|
||||
type: "number",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the previous page",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function normalizeItem(item: ModelGeneratedAssetItem) {
|
||||
return {
|
||||
asset_id: item.assetId ?? "",
|
||||
asset_type: item.assetType ?? "",
|
||||
asset_name: item.assetName ?? "",
|
||||
model_name: item.modelName ?? "",
|
||||
favorited: item.favorited ?? false,
|
||||
generate_time: item.generateTime,
|
||||
download_url: item.downloadUrl,
|
||||
thumbnail_url: item.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `bl asset-center list` — 分页查询模型生成资产列表。
|
||||
*
|
||||
* 支持类型/模型/关键词/收藏/回收站/OSS 同步状态/时间范围筛选,以及
|
||||
* --next-token / --pre-token 游标翻页;可选返回签名下载链接与缩略图 URL。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "List model-generated assets with filters and cursor pagination",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: [
|
||||
"",
|
||||
"--type IMAGE --model qwen-image-3.0",
|
||||
"--favorited --page-size 20",
|
||||
"--recycle-bin",
|
||||
"--keyword landscape --output json",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const pageSize = flags.pageSize ?? 10;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
...buildListFilterBody(flags),
|
||||
pageSize,
|
||||
};
|
||||
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
|
||||
if (flags.includeThumbnail) body.includeThumbnail = true;
|
||||
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
|
||||
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
|
||||
if (flags.nextToken !== undefined) body.nextToken = flags.nextToken;
|
||||
if (flags.preToken !== undefined) body.preToken = flags.preToken;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.listModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetListResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.listModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const items = (data.dataList ?? []).map(normalizeItem);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
items,
|
||||
pre_token: data.preToken,
|
||||
next_token: data.nextToken,
|
||||
has_next: data.hasNext,
|
||||
has_pre: data.hasPre,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
emitBare("No assets found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = ["ASSET_ID", "TYPE", "NAME", "MODEL", "FAVORITED", "GENERATED"];
|
||||
const rows = items.map((item) => [
|
||||
item.asset_id,
|
||||
item.asset_type,
|
||||
item.asset_name,
|
||||
item.model_name,
|
||||
item.favorited ? "yes" : "-",
|
||||
formatGenerateTime(item.generate_time),
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (data.hasPre) parts.push("has previous page");
|
||||
if (data.hasNext) parts.push(`next token: ${data.nextToken}`);
|
||||
if (parts.length > 0) emitBare(`\n${parts.join("; ")}`);
|
||||
},
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetCountResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_LIST_FILTER_FLAGS,
|
||||
buildListFilterBody,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
} from "./utils.ts";
|
||||
|
||||
const STATS_FLAGS = {
|
||||
...ASSET_LIST_FILTER_FLAGS,
|
||||
syncFailed: {
|
||||
type: "switch",
|
||||
description: "Also count assets with failed OSS sync",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center stats` — 按类型统计模型生成资产数量。
|
||||
*
|
||||
* 复用 list 的筛选条件;--sync-failed 额外统计 OSS 同步失败的资产数。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Count model-generated assets by type",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: STATS_FLAGS,
|
||||
exampleArgs: ["", "--sync-failed", "--type IMAGE --output json"],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const body = buildListFilterBody(flags);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.countModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetCountResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.countModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
let syncFailedCount: number | undefined;
|
||||
if (flags.syncFailed) {
|
||||
const failed = await callAssetApi<AssetCountResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.countModelGeneratedAsset,
|
||||
{ ...body, syncOssDataStatus: "SYNC_FAILED" },
|
||||
);
|
||||
syncFailedCount = failed.totalCount ?? 0;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
total_count: data.totalCount ?? 0,
|
||||
image_count: data.imageCount ?? 0,
|
||||
video_count: data.videoCount ?? 0,
|
||||
audio_count: data.audioCount ?? 0,
|
||||
...(syncFailedCount !== undefined ? { sync_failed_count: syncFailedCount } : {}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("Total", 14)} ${data.totalCount ?? 0}`);
|
||||
emitBare(`${padEnd("Image", 14)} ${data.imageCount ?? 0}`);
|
||||
emitBare(`${padEnd("Video", 14)} ${data.videoCount ?? 0}`);
|
||||
emitBare(`${padEnd("Audio", 14)} ${data.audioCount ?? 0}`);
|
||||
if (syncFailedCount !== undefined) {
|
||||
emitBare(`${padEnd("Sync failed", 14)} ${syncFailedCount}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetStorageQuotaResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload, formatStorageBytes } from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center storage` — 查看存储配额、已用容量与超额计费说明。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "View storage quota, usage, and overage pricing",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
exampleArgs: ["", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings, identity } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(dryRunPayload(settings, identity.binName, ASSET_API.getStorageQuota, {}), format);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetStorageQuotaResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.getStorageQuota,
|
||||
{},
|
||||
);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
used_storage_size: data.usedStorageSize,
|
||||
free_storage_quota: data.freeStorageQuota,
|
||||
extra_storage_price: data.extraStoragePrice,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("Used", 14)} ${formatStorageBytes(data.usedStorageSize)}`);
|
||||
emitBare(`${padEnd("Free quota", 14)} ${formatStorageBytes(data.freeStorageQuota)}`);
|
||||
emitBare(`${padEnd("Overage", 14)} ${data.extraStoragePrice ?? "-"}`);
|
||||
},
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
export type AssetType = "IMAGE" | "VIDEO" | "AUDIO";
|
||||
|
||||
export type AssetDeleteStatus = "NORMAL" | "SOFT_DELETED" | "PERMANENTLY_DELETED";
|
||||
|
||||
export type AssetSyncOssStatus = "NOT_SYNCED" | "IN_SYNCING" | "SYNC_SUCCESS" | "SYNC_FAILED";
|
||||
|
||||
export type AssetDeleteType = "SOFT_DELETE" | "PERMANENT_DELETE";
|
||||
|
||||
export interface AssetHttpBaseRequest {
|
||||
workspace: string;
|
||||
tenantId?: string;
|
||||
mainAccountUid?: string;
|
||||
apiSource?: string;
|
||||
}
|
||||
|
||||
export interface ModelGeneratedAssetItem {
|
||||
id?: number;
|
||||
assetId?: string;
|
||||
assetType?: string;
|
||||
assetName?: string;
|
||||
assetDescription?: string;
|
||||
favorited?: boolean;
|
||||
assetSize?: number;
|
||||
modelType?: string;
|
||||
modelName?: string;
|
||||
deleteStatus?: string;
|
||||
syncOssDataStatus?: string;
|
||||
generateTime?: number;
|
||||
downloadUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
gmtCreate?: string;
|
||||
gmtModified?: string;
|
||||
}
|
||||
|
||||
export interface AssetListResponse {
|
||||
dataList?: ModelGeneratedAssetItem[];
|
||||
preToken?: number;
|
||||
nextToken?: number;
|
||||
hasNext?: boolean;
|
||||
hasPre?: boolean;
|
||||
}
|
||||
|
||||
export interface AssetGetResponse {
|
||||
item?: ModelGeneratedAssetItem;
|
||||
}
|
||||
|
||||
export interface AssetBatchResponse {
|
||||
success?: boolean;
|
||||
affectedCount?: number;
|
||||
}
|
||||
|
||||
export interface AssetDownloadUrlItem {
|
||||
assetId?: string;
|
||||
downloadUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface AssetDownloadResponse {
|
||||
items?: AssetDownloadUrlItem[];
|
||||
}
|
||||
|
||||
export interface AssetCountResponse {
|
||||
imageCount?: number;
|
||||
videoCount?: number;
|
||||
audioCount?: number;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export interface AssetStorageQuotaResponse {
|
||||
freeStorageQuota?: number;
|
||||
usedStorageSize?: number;
|
||||
extraStoragePrice?: string;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center unfavorite` — 取消收藏一个或多个资产。
|
||||
*
|
||||
* 支持重复 --id(单次最多 100 个),调用 batchUnfavoriteAsset。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Remove assets from favorites",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...]",
|
||||
flags: ASSET_ID_FLAG,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const body = { assetIdList };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchUnfavoriteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchUnfavoriteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`Unfavorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
effectiveConsoleGatewayConfig,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import type { AssetHttpBaseRequest, AssetSyncOssStatus, AssetType } from "./types.ts";
|
||||
|
||||
const ASSET_SERVICE = "dashscopeModel";
|
||||
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
|
||||
export const MAX_ASSET_BATCH_SIZE = 100;
|
||||
|
||||
export const ASSET_API = {
|
||||
listModelGeneratedAsset: assetApi("listModelGeneratedAsset"),
|
||||
getModelGeneratedAsset: assetApi("getModelGeneratedAsset"),
|
||||
batchFavoriteAsset: assetApi("batchFavoriteAsset"),
|
||||
batchUnfavoriteAsset: assetApi("batchUnfavoriteAsset"),
|
||||
batchDeleteAsset: assetApi("batchDeleteAsset"),
|
||||
batchGetAssetDownloadUrl: assetApi("batchGetAssetDownloadUrl"),
|
||||
countModelGeneratedAsset: assetApi("countModelGeneratedAsset"),
|
||||
getStorageQuota: assetApi("getStorageQuota"),
|
||||
} as const;
|
||||
|
||||
export const ASSET_ID_FLAG = {
|
||||
id: {
|
||||
type: "array",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID(s) to operate on (repeatable, max 100)",
|
||||
required: true,
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const ASSET_LIST_FILTER_FLAGS = {
|
||||
type: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: "Asset type: IMAGE, VIDEO, or AUDIO",
|
||||
choices: ["IMAGE", "VIDEO", "AUDIO"] as const,
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Filter by model name",
|
||||
},
|
||||
keyword: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by asset name (substring match)",
|
||||
},
|
||||
favorited: {
|
||||
type: "switch",
|
||||
description: "Show or count only favorited assets",
|
||||
},
|
||||
recycleBin: {
|
||||
type: "switch",
|
||||
description: "Show or count soft-deleted assets (recycle bin)",
|
||||
},
|
||||
syncStatus: {
|
||||
type: "string",
|
||||
valueHint: "<status>",
|
||||
description: "OSS sync status filter",
|
||||
choices: ["NOT_SYNCED", "IN_SYNCING", "SYNC_SUCCESS", "SYNC_FAILED"] as const,
|
||||
},
|
||||
beginTime: {
|
||||
type: "string",
|
||||
valueHint: "<datetime>",
|
||||
description: "Filter by generate time start (ISO_LOCAL_DATE_TIME)",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
valueHint: "<datetime>",
|
||||
description: "Filter by generate time end (ISO_LOCAL_DATE_TIME)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type AssetListFilterFlags = ParsedFlags<typeof ASSET_LIST_FILTER_FLAGS>;
|
||||
|
||||
function assetApi(action: string): string {
|
||||
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
|
||||
}
|
||||
|
||||
function getNestedRecord(
|
||||
obj: Record<string, unknown>,
|
||||
key: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const val = obj[key];
|
||||
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractAssetResponse<T>(result: unknown): T {
|
||||
const raw = result as Record<string, unknown>;
|
||||
const data = getNestedRecord(raw, "data");
|
||||
if (!data) {
|
||||
throw new BailianError("Unexpected empty response from asset API.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const dataV2 = getNestedRecord(data, "DataV2");
|
||||
const payload = dataV2
|
||||
? (getNestedRecord(getNestedRecord(dataV2, "data") ?? dataV2, "data") ??
|
||||
getNestedRecord(dataV2, "data") ??
|
||||
dataV2)
|
||||
: (getNestedRecord(data, "data") ?? data);
|
||||
|
||||
if (payload.success === false) {
|
||||
const message =
|
||||
typeof payload.message === "string" && payload.message.length > 0
|
||||
? payload.message
|
||||
: typeof payload.code === "string"
|
||||
? payload.code
|
||||
: "Asset API request failed.";
|
||||
throw new BailianError(message, ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
if (payload.data !== undefined) {
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export function requireWorkspaceId(settings: Settings, binName: string): string {
|
||||
if (settings.workspaceId) return settings.workspaceId;
|
||||
|
||||
throw new BailianError(
|
||||
`workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or \`${binName} config set workspace_id <id>\`.`,
|
||||
ExitCode.GENERAL,
|
||||
`Run \`${binName} workspace list\` to view available workspaces.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildBaseRequest(settings: Settings, binName: string): AssetHttpBaseRequest {
|
||||
// workspace 由 CLI 注入;tenantId / mainAccountUid 由 Console 网关从登录 session 自动填充,
|
||||
// CLI 侧无需也不应手动解析阿里云账号 ID。
|
||||
return {
|
||||
workspace: requireWorkspaceId(settings, binName),
|
||||
apiSource: "CLI",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildListFilterBody(flags: AssetListFilterFlags): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {};
|
||||
|
||||
if (flags.type) body.assetType = flags.type as AssetType;
|
||||
if (flags.model) body.modelName = flags.model;
|
||||
if (flags.keyword) body.assetName = flags.keyword;
|
||||
if (flags.favorited) body.favorited = true;
|
||||
if (flags.recycleBin) {
|
||||
body.deleteStatus = "SOFT_DELETED";
|
||||
} else {
|
||||
body.deleteStatus = "NORMAL";
|
||||
}
|
||||
if (flags.syncStatus) body.syncOssDataStatus = flags.syncStatus as AssetSyncOssStatus;
|
||||
if (flags.beginTime) body.beginTime = flags.beginTime;
|
||||
if (flags.endTime) body.endTime = flags.endTime;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function validateAssetIds(ids: string[] | undefined): string | undefined {
|
||||
if (!ids || ids.length === 0) {
|
||||
return "At least one --id is required.";
|
||||
}
|
||||
if (ids.length > MAX_ASSET_BATCH_SIZE) {
|
||||
return `At most ${MAX_ASSET_BATCH_SIZE} asset IDs are allowed per request.`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function callAssetApi<T>(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
binName: string,
|
||||
api: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const payload = { ...buildBaseRequest(settings, binName), ...body };
|
||||
const raw = await client.console(api, payload);
|
||||
return extractAssetResponse<T>(raw);
|
||||
}
|
||||
|
||||
export function dryRunPayload(
|
||||
settings: Settings,
|
||||
binName: string,
|
||||
api: string,
|
||||
body: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
api,
|
||||
data: { ...buildBaseRequest(settings, binName), ...body },
|
||||
...effectiveConsoleGatewayConfig(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGenerateTime(ts?: number): string {
|
||||
if (ts == null) return "-";
|
||||
return new Date(ts).toISOString().replace("T", " ").slice(0, 19);
|
||||
}
|
||||
|
||||
export function formatStorageBytes(bytes?: number): string {
|
||||
if (bytes == null) return "-";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Read-only discovery of locally installed AI tooling, surfaced by `config ui`:
|
||||
// - Agent skills installed under ~/.agents/skills (via `npx skills add`).
|
||||
// - Agent skills installed under ~/.agents/skills (via `bl skill add`).
|
||||
// - MCP servers declared in each coding agent's local config file.
|
||||
// - Coding agent frameworks and whether the bailian-cli provider is wired in.
|
||||
//
|
||||
@@ -128,8 +128,8 @@ function countFiles(dir: string, budget = 500): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill directories to scan, keyed by the module that owns them. `npx skills
|
||||
* add --all` fans skills out into each installed agent, so the same skill can
|
||||
* Skill directories to scan, keyed by the module that owns them. `bl skill init` /
|
||||
* `bl skill add` fans skills out into each installed agent, so the same skill can
|
||||
* live in several of these roots at once.
|
||||
*/
|
||||
function skillRoots(home: string): Array<{ source: string; dir: string }> {
|
||||
|
||||
@@ -548,7 +548,7 @@ export const PAGE_HTML = `<!doctype html>
|
||||
<section id="view-skills" class="view">
|
||||
<div class="view-head">
|
||||
<h2 class="view-title">Installed <span class="grad">Skills</span></h2>
|
||||
<p class="view-sub">Agent skills discovered across every local agent module (~/.agents/skills plus each agent's skills folder). Installed via <code style="font-family:var(--mono)">npx skills add</code>.</p>
|
||||
<p class="view-sub">Agent skills discovered across every local agent module (~/.agents/skills plus each agent's skills folder). Installed via <code style="font-family:var(--mono)">bl skill add</code>.</p>
|
||||
</div>
|
||||
<div class="toolbar"><input id="skillSearch" class="search" type="search" placeholder="Search skills…" autocomplete="off"><button id="addSkillBtn" class="btn-dark" type="button">+ Add skill</button></div>
|
||||
<div id="skillsBody"><div class="loading">Loading…</div></div>
|
||||
@@ -1444,7 +1444,7 @@ export const PAGE_HTML = `<!doctype html>
|
||||
function renderSkills() {
|
||||
var body = document.getElementById('skillsBody');
|
||||
var pager = document.getElementById('skillsPager');
|
||||
if (!SKILLS.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills installed.', 'Install with <code>npx skills add modelstudioai/cli --all -g</code>'); return; }
|
||||
if (!SKILLS.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills installed.', 'Install with <code>bl skill init</code>'); return; }
|
||||
var list = SKILLS.filter(function (s) { return skillMatches(s, SKILL_Q); });
|
||||
if (!list.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills match "' + SKILL_Q + '".', ''); return; }
|
||||
var info = pageSlice(list, SKILL_PAGE, getPageSize('skills')); SKILL_PAGE = info.page;
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineCommand({
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
`--api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
|
||||
`--api zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
|
||||
`--api some.api.name --data '{"key":"value"}' --console-region cn-beijing`,
|
||||
],
|
||||
async run(ctx) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
fileId: {
|
||||
@@ -19,20 +19,18 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "dataset.delete", file_id: fileId }, format);
|
||||
emitResult({ action: "dataset.delete", file_id: fileId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteDataset(ctx.client, fileId);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`Deleted ${fileId}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
if (settings.quiet) {
|
||||
emitBare(fileId);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, getDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, getDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const GET_FLAGS = {
|
||||
fileId: {
|
||||
@@ -19,10 +19,9 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "dataset.get", file_id: fileId }, format);
|
||||
emitResult({ action: "dataset.get", file_id: fileId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,19 +44,10 @@ export default defineCommand({
|
||||
description: file.description ?? "",
|
||||
};
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ ...item, request_id: response.request_id }, format);
|
||||
return;
|
||||
if (settings.quiet) {
|
||||
emitBare(item.file_id);
|
||||
} else {
|
||||
emitResult({ ...item, request_id: response.request_id }, "json");
|
||||
}
|
||||
|
||||
// text / quiet
|
||||
emitBare(`file_id: ${item.file_id}`);
|
||||
emitBare(`name: ${item.name}`);
|
||||
emitBare(`size: ${item.size}`);
|
||||
if (item.md5) emitBare(`md5: ${item.md5}`);
|
||||
if (item.purpose) emitBare(`purpose: ${item.purpose}`);
|
||||
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
|
||||
if (item.description) emitBare(`description: ${item.description}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, listDatasets, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
|
||||
import { defineCommand, listDatasets, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
@@ -23,7 +23,6 @@ export default defineCommand({
|
||||
exampleArgs: ["", "--purpose fine-tune", "--purpose evaluation --page-size 20", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
@@ -33,7 +32,7 @@ export default defineCommand({
|
||||
page_size: flags.pageSize,
|
||||
purpose: flags.purpose,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -46,7 +45,6 @@ export default defineCommand({
|
||||
const files = response.data?.files ?? [];
|
||||
const total = response.data?.total;
|
||||
|
||||
// Normalize to consistent structure for both text/json output.
|
||||
const items = files.map((item) => ({
|
||||
file_id: item.file_id ?? "",
|
||||
name: item.name ?? "",
|
||||
@@ -54,20 +52,10 @@ export default defineCommand({
|
||||
purpose: item.purpose ?? "",
|
||||
}));
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ items, total, request_id: response.request_id }, format);
|
||||
return;
|
||||
if (settings.quiet) {
|
||||
for (const item of items) emitBare(item.file_id);
|
||||
} else {
|
||||
emitResult({ items, total, request_id: response.request_id }, "json");
|
||||
}
|
||||
|
||||
// text / quiet
|
||||
if (items.length === 0) {
|
||||
emitBare("No dataset files found.");
|
||||
return;
|
||||
}
|
||||
const headers = ["FILE_ID", "NAME", "SIZE", "PURPOSE"];
|
||||
const rows = items.map((i) => [i.file_id, i.name, i.size, i.purpose]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
if (total !== undefined) emitBare(`\nTotal: ${total}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
uploadDataset,
|
||||
validateDataset,
|
||||
parseDatasetSchemaFlag,
|
||||
formatIssue,
|
||||
MAX_DATASET_BYTES,
|
||||
MAX_CPT_BYTES,
|
||||
MAX_MEDIA_ZIP_BYTES,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const UPLOAD_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: "Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image)",
|
||||
description: "Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT, ≤2GB media zip)",
|
||||
required: true,
|
||||
},
|
||||
purpose: {
|
||||
@@ -29,7 +29,7 @@ const UPLOAD_FLAGS = {
|
||||
type: "string",
|
||||
valueHint: "<s>",
|
||||
description:
|
||||
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
|
||||
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
|
||||
},
|
||||
noValidate: {
|
||||
type: "switch",
|
||||
@@ -45,7 +45,7 @@ export default defineCommand({
|
||||
description: "Upload a dataset file (.jsonl or .zip) to Bailian",
|
||||
auth: "apiKey",
|
||||
usageArgs:
|
||||
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image>] [--no-validate] [--full-validate]",
|
||||
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image|video>] [--no-validate] [--full-validate]",
|
||||
flags: UPLOAD_FLAGS,
|
||||
exampleArgs: [
|
||||
"--file train.jsonl",
|
||||
@@ -58,13 +58,14 @@ export default defineCommand({
|
||||
],
|
||||
notes: [
|
||||
"Supports .jsonl (text) and .zip (audio/image archives with a data.jsonl",
|
||||
"manifest). Five record schemas are recognized: chatml = {messages:[...]}",
|
||||
"manifest). Six record schemas are recognized: chatml = {messages:[...]}",
|
||||
'(SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."}',
|
||||
'(continual pre-training, raw text); tts = {wav_fn:"train/xxx.wav",',
|
||||
'text:"..."} (audio fine-tuning); image = {img_path:"..."} (image',
|
||||
"generation). With no --schema, a record carrying wav_fn is validated as",
|
||||
"TTS, img_path as image, chosen/rejected as DPO, text (no messages) as CPT,",
|
||||
"otherwise ChatML. Upload cap: 300MB text, 1GB image. Upload uses the",
|
||||
"generation); video = {first_frame_path:...} (video generation). With no",
|
||||
"--schema, a record carrying wav_fn is validated as TTS, img_path as image,",
|
||||
"chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML.",
|
||||
"Upload cap: 200MB SFT/DPO text, 300MB CPT, 2GB media zip. Upload uses the",
|
||||
"OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is",
|
||||
"persisted (the DashScope-native /api/v1/files drops it).",
|
||||
],
|
||||
@@ -73,19 +74,15 @@ export default defineCommand({
|
||||
const filePath = flags.file;
|
||||
const purpose = flags.purpose || "fine-tune";
|
||||
const schema = parseDatasetSchemaFlag(flags.schema);
|
||||
if (schema === "video") {
|
||||
throw new BailianError(
|
||||
`--schema video is not supported.`,
|
||||
ExitCode.USAGE,
|
||||
`Supported schemas: chatml, dpo, cpt, tts, image.`,
|
||||
);
|
||||
}
|
||||
const format = detectOutputFormat(settings.output);
|
||||
// Image schema allows larger ZIPs (1 GB vs 300 MB for text).
|
||||
const isMediaSchema = schema === "image";
|
||||
// Size caps differ per training type: SFT/DPO 200MB, CPT 300MB, media ZIP 2GB.
|
||||
const isMediaSchema = schema === "image" || schema === "video";
|
||||
const maxBytes = isMediaSchema
|
||||
? MAX_MEDIA_ZIP_BYTES
|
||||
: schema === "cpt"
|
||||
? MAX_CPT_BYTES
|
||||
: MAX_DATASET_BYTES;
|
||||
|
||||
if (!flags.noValidate) {
|
||||
const maxBytes = isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES;
|
||||
const result = await validateDataset(filePath, {
|
||||
fullValidate: flags.fullValidate,
|
||||
schema,
|
||||
@@ -125,11 +122,11 @@ export default defineCommand({
|
||||
action: "dataset.upload",
|
||||
file: filePath,
|
||||
purpose,
|
||||
max_bytes: isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES,
|
||||
max_bytes: maxBytes,
|
||||
validate: !flags.noValidate,
|
||||
schema: schema ?? "auto",
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -142,11 +139,8 @@ export default defineCommand({
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(file.file_id);
|
||||
} else if (format === "text") {
|
||||
emitBare(`Uploaded ${file.name} → file_id=${file.file_id}`);
|
||||
emitRequestId(request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult({ ...file, request_id }, format);
|
||||
emitResult({ ...file, request_id }, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
validateDataset,
|
||||
parseDatasetSchemaFlag,
|
||||
formatIssue,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type ValidationResult,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
function formatStats(result: ValidationResult): string[] {
|
||||
const out: string[] = [];
|
||||
if (result.stats.totalRecords !== undefined) out.push(`records: ${result.stats.totalRecords}`);
|
||||
if (result.stats.sampledRecords !== undefined)
|
||||
out.push(`sampled: ${result.stats.sampledRecords}`);
|
||||
if (result.stats.bytes !== undefined) out.push(`bytes: ${result.stats.bytes}`);
|
||||
if (result.stats.durationMs !== undefined) out.push(`took: ${result.stats.durationMs}ms`);
|
||||
return out;
|
||||
}
|
||||
|
||||
const VALIDATE_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
@@ -36,7 +23,7 @@ const VALIDATE_FLAGS = {
|
||||
type: "string",
|
||||
valueHint: "<s>",
|
||||
description:
|
||||
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
|
||||
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -44,13 +31,14 @@ export default defineCommand({
|
||||
description: "Locally validate a dataset file (.jsonl or .zip) without uploading",
|
||||
// 纯本地校验,不触网、不需 API key(与 `pipeline validate` 一致)。
|
||||
auth: "none",
|
||||
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image>]",
|
||||
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image|video>]",
|
||||
flags: VALIDATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--file train.jsonl",
|
||||
"--file dpo.jsonl --schema dpo",
|
||||
"--file cpt.jsonl --schema cpt",
|
||||
"--file audio.zip --schema tts",
|
||||
"--file wan-i2v-training-dataset.zip --schema video",
|
||||
"--file eval.jsonl --full-validate",
|
||||
"--file train.jsonl --output json",
|
||||
],
|
||||
@@ -60,27 +48,20 @@ export default defineCommand({
|
||||
"Schemas: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen,",
|
||||
'rejected}; cpt = {text:"..."} (continual pre-training, raw text);',
|
||||
'tts = {wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning);',
|
||||
'image = {img_path:"..."} (image generation). With no --schema, a record',
|
||||
"carrying wav_fn is validated as TTS, img_path as image, chosen/rejected",
|
||||
"as DPO, text (no messages) as CPT, otherwise ChatML. Pass --schema to",
|
||||
"require a specific shape on every record. ZIP archives (.zip) are",
|
||||
"validated structurally (data.jsonl present, media references resolve) in",
|
||||
"addition to per-record content checks. Use --full-validate to JSON.parse",
|
||||
"every line.",
|
||||
'image = {img_path:"..."} (image generation);',
|
||||
'video = {first_frame_path:"...", video_path:"..."} (video generation,',
|
||||
"i2v first-frame or kf2v first+last-frame with last_frame_path). With no",
|
||||
"--schema, a record carrying wav_fn is validated as TTS, img_path as image,",
|
||||
"first_frame_path/video_path as video, chosen/rejected as DPO, text (no",
|
||||
"messages) as CPT, otherwise ChatML. Pass --schema to require a specific",
|
||||
"shape on every record. ZIP archives (.zip) are validated structurally",
|
||||
"(data.jsonl present, media references resolve) in addition to per-record",
|
||||
"content checks. Use --full-validate to JSON.parse every line.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const filePath = flags.file;
|
||||
const schema = parseDatasetSchemaFlag(flags.schema);
|
||||
if (schema === "video") {
|
||||
throw new BailianError(
|
||||
`--schema video is not supported.`,
|
||||
ExitCode.USAGE,
|
||||
`Supported schemas: chatml, dpo, cpt, tts, image.`,
|
||||
);
|
||||
}
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
@@ -89,38 +70,17 @@ export default defineCommand({
|
||||
full: flags.fullValidate,
|
||||
schema: schema ?? "auto",
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await validateDataset(filePath, { fullValidate: flags.fullValidate, schema });
|
||||
|
||||
if (format === "json") {
|
||||
// For json output we always emit the structured result, exit code conveys validity.
|
||||
emitResult(result, format);
|
||||
} else if (settings.quiet) {
|
||||
if (settings.quiet) {
|
||||
emitBare(result.valid ? "ok" : "fail");
|
||||
} else {
|
||||
const status = result.valid ? "PASSED" : "FAILED";
|
||||
emitBare(`Dataset validation ${status} for ${result.filePath}`);
|
||||
const stats = formatStats(result);
|
||||
if (stats.length) emitBare(` ${stats.join(" · ")}`);
|
||||
|
||||
if (result.errors.length) {
|
||||
emitBare(`Errors (${result.errors.length}):`);
|
||||
for (const error of result.errors.slice(0, 20)) emitBare(formatIssue(error));
|
||||
if (result.errors.length > 20) {
|
||||
emitBare(` … and ${result.errors.length - 20} more.`);
|
||||
}
|
||||
}
|
||||
if (result.warnings.length) {
|
||||
emitBare(`Warnings (${result.warnings.length}):`);
|
||||
for (const warning of result.warnings.slice(0, 10)) emitBare(formatIssue(warning));
|
||||
if (result.warnings.length > 10) {
|
||||
emitBare(` … and ${result.warnings.length - 10} more.`);
|
||||
}
|
||||
}
|
||||
emitResult(result, "json");
|
||||
}
|
||||
|
||||
if (!result.valid) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
createDeployment,
|
||||
pickPlanStrategy,
|
||||
STRATEGIES,
|
||||
@@ -11,16 +10,16 @@ import {
|
||||
type CommandContext,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const CREATE_FLAGS = {
|
||||
model: {
|
||||
modelName: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Model name (catalog model or fine-tuned output) (required)",
|
||||
valueHint: "<model_name>",
|
||||
description: "Model to deploy — fine-tuned output name or catalog model (required)",
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
displayName: {
|
||||
type: "string",
|
||||
valueHint: "<display_name>",
|
||||
description: "Console display name for the deployment (required)",
|
||||
@@ -64,7 +63,7 @@ const CREATE_FLAGS = {
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const CREATE_USAGE =
|
||||
"--model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
|
||||
"--model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
|
||||
|
||||
const CREATE_NOTES = [
|
||||
"Plan defaults to `lora` (Token-billed) for text/image and `mu` (model-unit-",
|
||||
@@ -78,14 +77,11 @@ const CREATE_NOTES = [
|
||||
"Use `bl deploy models --source base` to inspect available templates.",
|
||||
"After creation, status starts at PENDING and transitions to RUNNING.",
|
||||
"Invoke the deployed model with: bl text chat --model <deployed_model>",
|
||||
"WARNING: --model is overloaded across commands and refers to DIFFERENT",
|
||||
"values. `bl deploy <modality> create --model` takes the exported model_name",
|
||||
"(e.g. `qwen3-8b-ft-...`), but the create response also returns a",
|
||||
"`deployed_model` field (the deployment instance id, e.g.",
|
||||
"`qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use",
|
||||
"the `deployed_model` from the create response — NOT the `model_name` you",
|
||||
"passed to `deploy <modality> create`. Do not reuse the value across the two",
|
||||
"commands.",
|
||||
"NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).",
|
||||
"The create response also returns a `deployed_model` field — the deployment",
|
||||
"instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference",
|
||||
"(`bl text chat --model <deployed_model>`) and lifecycle commands",
|
||||
"(`deploy get/scale/pause/resume/delete --deployed-model <id>`).",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -119,10 +115,9 @@ async function runCreate(
|
||||
ctx: CommandContext<typeof CREATE_FLAGS>,
|
||||
): Promise<void> {
|
||||
const { identity, settings, flags } = ctx;
|
||||
const model = flags.model as string;
|
||||
const name = flags.name as string;
|
||||
const model = flags.modelName as string;
|
||||
const name = flags.displayName as string;
|
||||
const plan = (flags.plan as string | undefined) || defaultDeployPlan(modality);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Plan-specific behaviour is owned by core `plans.ts`. The strategy resolves
|
||||
// the plan-specific body fragment (mu may auto-pick a template from the
|
||||
@@ -146,7 +141,7 @@ async function runCreate(
|
||||
};
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.create", body }, format);
|
||||
emitResult({ action: "deploy.create", body }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,17 +150,8 @@ async function runCreate(
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployment?.deployed_model ?? "");
|
||||
} else if (format === "text") {
|
||||
emitBare(`Created deployment.`);
|
||||
if (deployment?.deployed_model) emitBare(` deployed_model: ${deployment.deployed_model}`);
|
||||
if (deployment?.status) emitBare(` status: ${deployment.status}`);
|
||||
if (deployment?.plan) emitBare(` plan: ${deployment.plan}`);
|
||||
emitBare(
|
||||
`\nNext: track readiness with: ${identity.binName} deploy get --deployed-model ${deployment?.deployed_model ?? "<id>"}`,
|
||||
);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +162,10 @@ export const deployTextCreate = defineCommand({
|
||||
usageArgs: CREATE_USAGE,
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model my-qwen-sft --name my-sft-test",
|
||||
"--model qwen3.6-flash-2026-04-16 --name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
|
||||
"--model qwen3-8b --name my-qwen3-mu --plan mu",
|
||||
"--model qwen3-8b --name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
|
||||
"--model-name my-qwen-sft --display-name my-sft-test",
|
||||
"--model-name qwen3.6-flash-2026-04-16 --display-name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
|
||||
"--model-name qwen3-8b --display-name my-qwen3-mu --plan mu",
|
||||
"--model-name qwen3-8b --display-name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
|
||||
],
|
||||
notes: CREATE_NOTES,
|
||||
validate: (flags) => validateCreate("text", flags),
|
||||
@@ -193,9 +179,9 @@ export const deployAudioCreate = defineCommand({
|
||||
usageArgs: CREATE_USAGE,
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model my-cosyvoice-ft --name my-tts",
|
||||
"--model my-cosyvoice-ft --name my-tts --deploy-spec dps-xxxx --capacity 1",
|
||||
"--model my-cosyvoice-ft --name my-tts --dry-run",
|
||||
"--model-name my-cosyvoice-ft --display-name my-tts",
|
||||
"--model-name my-cosyvoice-ft --display-name my-tts --deploy-spec dps-xxxx --capacity 1",
|
||||
"--model-name my-cosyvoice-ft --display-name my-tts --dry-run",
|
||||
],
|
||||
notes: CREATE_NOTES,
|
||||
validate: (flags) => validateCreate("audio", flags),
|
||||
@@ -209,9 +195,9 @@ export const deployImageCreate = defineCommand({
|
||||
usageArgs: CREATE_USAGE,
|
||||
flags: CREATE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model my-wan-ft --name my-wan",
|
||||
"--model my-wan-ft --name my-wan-mu --plan mu",
|
||||
"--model my-wan-ft --name my-wan --dry-run",
|
||||
"--model-name my-wan-ft --display-name my-wan",
|
||||
"--model-name my-wan-ft --display-name my-wan-mu --plan mu",
|
||||
"--model-name my-wan-ft --display-name my-wan --dry-run",
|
||||
],
|
||||
notes: CREATE_NOTES,
|
||||
validate: (flags) => validateCreate("image", flags),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
deleteDeployment,
|
||||
getDeployment,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -38,10 +37,9 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, format);
|
||||
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +53,8 @@ export default defineCommand({
|
||||
if (status && status !== "STOPPED" && status !== "FAILED") {
|
||||
throw new BailianError(
|
||||
`Deployment ${deployedModel} is ${status}. Only STOPPED / FAILED deployments can be deleted. ` +
|
||||
`Stop it first via the platform console, or pass --skip-precheck to attempt deletion anyway.`,
|
||||
`Run \`bl deploy pause --deployed-model ${deployedModel}\` to pause it first, ` +
|
||||
`or pass --skip-precheck to attempt deletion anyway.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
@@ -69,11 +68,8 @@ export default defineCommand({
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployedModel);
|
||||
} else if (format === "text") {
|
||||
emitBare(`Deleted ${deployedModel}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, getDeployment, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, getDeployment, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const GET_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -22,10 +22,9 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.get", deployed_model: deployedModel }, format);
|
||||
emitResult({ action: "deploy.get", deployed_model: deployedModel }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ export default defineCommand({
|
||||
const deployment = response.output ?? response.data;
|
||||
|
||||
if (!deployment) {
|
||||
emitBare(`No data returned for ${deployedModel}`);
|
||||
emitResult({ deployed_model: deployedModel, request_id: response.request_id }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,18 +56,6 @@ export default defineCommand({
|
||||
if (deployment.gmt_create) item.created_at = deployment.gmt_create;
|
||||
if (deployment.gmt_modified) item.updated_at = deployment.gmt_modified;
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ ...item, request_id: response.request_id }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// text / quiet — fixed-width label column for alignment
|
||||
const label = (key: string) => `${key}:`.padEnd(18);
|
||||
for (const [key, value] of Object.entries(item)) {
|
||||
if (value === "" || value === undefined) continue;
|
||||
const display = typeof value === "string" ? value : JSON.stringify(value);
|
||||
emitBare(`${label(key)}${display}`);
|
||||
}
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
emitResult({ ...item, request_id: response.request_id }, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
listDeployments,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
|
||||
import { defineCommand, listDeployments, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
@@ -28,13 +23,12 @@ export default defineCommand({
|
||||
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const status = flags.status || undefined;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{ action: "deploy.list", page: flags.page, page_size: flags.pageSize, status },
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -57,27 +51,6 @@ export default defineCommand({
|
||||
created_at: item.gmt_create ?? "",
|
||||
}));
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ items, total, request_id: response.request_id }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// text / quiet
|
||||
if (items.length === 0) {
|
||||
emitBare("No deployments found.");
|
||||
return;
|
||||
}
|
||||
const headers = ["DEPLOYED_MODEL", "MODEL_NAME", "STATUS", "PLAN", "CAPACITY", "CREATED_AT"];
|
||||
const rows = items.map((item) => [
|
||||
item.deployed_model,
|
||||
item.model_name,
|
||||
item.status,
|
||||
item.plan,
|
||||
item.capacity,
|
||||
item.created_at,
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
if (total !== undefined) emitBare(`\nTotal: ${total}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
emitResult({ items, total, request_id: response.request_id }, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
listDeployableModels,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
|
||||
import { defineCommand, listDeployableModels, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const MODELS_FLAGS = {
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
@@ -39,7 +34,6 @@ export default defineCommand({
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
// Default version to v1.0 — without it, the API returns the legacy catalog
|
||||
// (only old fine-tune outputs). Pass --catalog-version "" to opt out.
|
||||
const version = flags.catalogVersion === "" ? undefined : (flags.catalogVersion ?? "v1.0");
|
||||
@@ -54,7 +48,7 @@ export default defineCommand({
|
||||
version,
|
||||
model_source: modelSource,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -72,102 +66,55 @@ export default defineCommand({
|
||||
// Two response shapes:
|
||||
// - custom (fine-tuned): top-level supported_plans: string[]
|
||||
// - base (catalog): plans: [{plan, templates?, cu_specs?}]
|
||||
// For json: surface the deployment-relevant fields preserved as a tree, so
|
||||
// Surface the deployment-relevant fields preserved as a tree, so
|
||||
// downstream tooling can drive `bl deploy <modality> create --deploy-spec <…>`
|
||||
// without a second round-trip. For text: keep the compact one-line summary.
|
||||
if (format === "json") {
|
||||
const items = models.map((model) => {
|
||||
const out: Record<string, unknown> = {
|
||||
model_name: model.model_name ?? "",
|
||||
};
|
||||
if (model.base_model) out.base_model = model.base_model;
|
||||
if (model.model_source) out.model_source = model.model_source;
|
||||
if (model.supported_plans && model.supported_plans.length > 0) {
|
||||
out.supported_plans = model.supported_plans;
|
||||
}
|
||||
if (model.plans && model.plans.length > 0) {
|
||||
out.plans = model.plans.map((plan) => {
|
||||
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
|
||||
if (plan.cu_specs && plan.cu_specs.length > 0) {
|
||||
planEntry.cu_specs = plan.cu_specs;
|
||||
}
|
||||
if (plan.templates && plan.templates.length > 0) {
|
||||
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
|
||||
// Drop noisy/redundant: template_source, template_type,
|
||||
// template_version, deploy_spec (typically == template_id).
|
||||
planEntry.templates = plan.templates.map((template) => {
|
||||
const tpl: Record<string, unknown> = {};
|
||||
if (template.template_id) tpl.template_id = template.template_id;
|
||||
if (template.template_name) tpl.template_name = template.template_name;
|
||||
if (template.charge_type) tpl.charge_type = template.charge_type;
|
||||
// Flatten roles.unified for the common COUPLED case.
|
||||
const unified = template.roles?.unified;
|
||||
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
|
||||
if (unified?.capacity_unit_per_instance !== undefined)
|
||||
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
|
||||
// Preserve split-role configs (SEPERATED) as-is so callers
|
||||
// can still drive prefill/decode sizing.
|
||||
if (template.roles?.prefill || template.roles?.decode) {
|
||||
tpl.roles = {
|
||||
prefill: template.roles?.prefill,
|
||||
decode: template.roles?.decode,
|
||||
};
|
||||
}
|
||||
if (template.template_desc) tpl.template_desc = template.template_desc;
|
||||
return tpl;
|
||||
});
|
||||
}
|
||||
return planEntry;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
emitResult({ items, total, request_id: response.request_id }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// text / quiet — keep the compact single-line summary table.
|
||||
const textItems = models.map((model) => {
|
||||
let plansSummary = "";
|
||||
if (model.supported_plans && model.supported_plans.length > 0) {
|
||||
plansSummary = model.supported_plans.join(",");
|
||||
} else if (model.plans && model.plans.length > 0) {
|
||||
plansSummary = model.plans
|
||||
.map((plan) => {
|
||||
const planName = plan.plan ?? "?";
|
||||
if (plan.templates && plan.templates.length > 0) {
|
||||
return `${planName}(${plan.templates.length}t)`;
|
||||
}
|
||||
if (plan.cu_specs && plan.cu_specs.length > 0) {
|
||||
return `${planName}(${plan.cu_specs.join("/")})`;
|
||||
}
|
||||
return planName;
|
||||
})
|
||||
.join(",");
|
||||
} else {
|
||||
plansSummary = "-";
|
||||
}
|
||||
return {
|
||||
// without a second round-trip.
|
||||
const items = models.map((model) => {
|
||||
const out: Record<string, unknown> = {
|
||||
model_name: model.model_name ?? "",
|
||||
base_model: model.base_model ?? "",
|
||||
source: model.model_source ?? "",
|
||||
plans: plansSummary,
|
||||
};
|
||||
if (model.base_model) out.base_model = model.base_model;
|
||||
if (model.model_source) out.model_source = model.model_source;
|
||||
if (model.supported_plans && model.supported_plans.length > 0) {
|
||||
out.supported_plans = model.supported_plans;
|
||||
}
|
||||
if (model.plans && model.plans.length > 0) {
|
||||
out.plans = model.plans.map((plan) => {
|
||||
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
|
||||
if (plan.cu_specs && plan.cu_specs.length > 0) {
|
||||
planEntry.cu_specs = plan.cu_specs;
|
||||
}
|
||||
if (plan.templates && plan.templates.length > 0) {
|
||||
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
|
||||
// Drop noisy/redundant: template_source, template_type,
|
||||
// template_version, deploy_spec (typically == template_id).
|
||||
planEntry.templates = plan.templates.map((template) => {
|
||||
const tpl: Record<string, unknown> = {};
|
||||
if (template.template_id) tpl.template_id = template.template_id;
|
||||
if (template.template_name) tpl.template_name = template.template_name;
|
||||
if (template.charge_type) tpl.charge_type = template.charge_type;
|
||||
// Flatten roles.unified for the common COUPLED case.
|
||||
const unified = template.roles?.unified;
|
||||
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
|
||||
if (unified?.capacity_unit_per_instance !== undefined)
|
||||
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
|
||||
// Preserve split-role configs (SEPERATED) as-is so callers
|
||||
// can still drive prefill/decode sizing.
|
||||
if (template.roles?.prefill || template.roles?.decode) {
|
||||
tpl.roles = {
|
||||
prefill: template.roles?.prefill,
|
||||
decode: template.roles?.decode,
|
||||
};
|
||||
}
|
||||
if (template.template_desc) tpl.template_desc = template.template_desc;
|
||||
return tpl;
|
||||
});
|
||||
}
|
||||
return planEntry;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
if (textItems.length === 0) {
|
||||
emitBare("No deployable models found.");
|
||||
return;
|
||||
}
|
||||
const headers = ["MODEL_NAME", "BASE_MODEL", "SOURCE", "PLANS"];
|
||||
const rows = textItems.map((item) => [
|
||||
item.model_name,
|
||||
item.base_model,
|
||||
item.source,
|
||||
item.plans,
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
if (total !== undefined) emitBare(`\nTotal: ${total}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
emitResult({ items, total, request_id: response.request_id }, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
defineCommand,
|
||||
stopModelService,
|
||||
listIndependentDeployedModels,
|
||||
findDeploymentEntry,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const PAUSE_FLAGS = {
|
||||
deployedModel: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Deployed model identifier (required)",
|
||||
required: true,
|
||||
},
|
||||
skipPrecheck: {
|
||||
type: "switch",
|
||||
description: "Skip the local RUNNING/PENDING status precheck",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl deploy pause` — pause a running deployment.
|
||||
*
|
||||
* Takes the model service offline so it no longer serves inference requests.
|
||||
* For mu/ptu plans, billing stops while paused.
|
||||
* Precheck: status must be RUNNING or PENDING.
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Pause a running model deployment (stops billing for mu/ptu)",
|
||||
auth: "console",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
flags: PAUSE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --skip-precheck",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
],
|
||||
notes: [
|
||||
"While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.",
|
||||
"Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.pause", deployed_model: deployedModel }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
// Precheck: verify the deployment is in a pausable state.
|
||||
if (!flags.skipPrecheck) {
|
||||
try {
|
||||
const entries = await listIndependentDeployedModels(ctx.client);
|
||||
const entry = findDeploymentEntry(entries, deployedModel);
|
||||
if (entry) {
|
||||
const status = (entry.status ?? "").toUpperCase();
|
||||
if (status && status !== "RUNNING" && status !== "PENDING") {
|
||||
throw new BailianError(
|
||||
`Deployment ${deployedModel} is ${status}. Only RUNNING / PENDING deployments can be paused. ` +
|
||||
`Pass --skip-precheck to attempt the pause anyway.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
// If entry not found in list, proceed — the server will surface the real error.
|
||||
} catch (error) {
|
||||
if (error instanceof BailianError) throw error;
|
||||
// If the list call itself failed, proceed and let the API call surface the error.
|
||||
}
|
||||
}
|
||||
|
||||
const response = await stopModelService(ctx.client, deployedModel);
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployedModel);
|
||||
} else {
|
||||
emitResult({ deployed_model: deployedModel, action: "pause", ...response }, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
defineCommand,
|
||||
startModelService,
|
||||
listIndependentDeployedModels,
|
||||
findDeploymentEntry,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const RESUME_FLAGS = {
|
||||
deployedModel: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Deployed model identifier (required)",
|
||||
required: true,
|
||||
},
|
||||
skipPrecheck: {
|
||||
type: "switch",
|
||||
description: "Skip the local STOPPED status precheck",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl deploy resume` — resume a paused deployment.
|
||||
*
|
||||
* Brings the model service back online so it can serve inference requests.
|
||||
* Precheck: status must be STOPPED.
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Resume a paused model deployment (brings service back online)",
|
||||
auth: "console",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
flags: RESUME_FLAGS,
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --skip-precheck",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
],
|
||||
notes: [
|
||||
"Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.",
|
||||
"For mu/ptu plans, billing resumes once the service is back online.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.resume", deployed_model: deployedModel }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
// Precheck: verify the deployment is in a resumable state.
|
||||
if (!flags.skipPrecheck) {
|
||||
try {
|
||||
const entries = await listIndependentDeployedModels(ctx.client);
|
||||
const entry = findDeploymentEntry(entries, deployedModel);
|
||||
if (entry) {
|
||||
const status = (entry.status ?? "").toUpperCase();
|
||||
if (status && status !== "STOPPED") {
|
||||
throw new BailianError(
|
||||
`Deployment ${deployedModel} is ${status}. Only STOPPED deployments can be resumed. ` +
|
||||
`Pass --skip-precheck to attempt the resume anyway.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
// If entry not found in list, proceed — the server will surface the real error.
|
||||
} catch (error) {
|
||||
if (error instanceof BailianError) throw error;
|
||||
// If the list call itself failed, proceed and let the API call surface the error.
|
||||
}
|
||||
}
|
||||
|
||||
const response = await startModelService(ctx.client, deployedModel);
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployedModel);
|
||||
} else {
|
||||
emitResult({ deployed_model: deployedModel, action: "resume", ...response }, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
scaleDeployment,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, scaleDeployment, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const SCALE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -52,7 +47,6 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body: Record<string, unknown> = {};
|
||||
if (flags.capacity !== undefined) body.capacity = flags.capacity;
|
||||
@@ -60,21 +54,16 @@ export default defineCommand({
|
||||
if (flags.outputTpm !== undefined) body.output_tpm = flags.outputTpm;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, format);
|
||||
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await scaleDeployment(ctx.client, deployedModel, body);
|
||||
const deployment = response.output ?? response.data;
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployedModel);
|
||||
} else if (format === "text") {
|
||||
const cap = deployment?.capacity !== undefined ? ` (capacity=${deployment.capacity})` : "";
|
||||
emitBare(`Scaled ${deployedModel}${cap}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
updateDeployment,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, updateDeployment, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const UPDATE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -48,31 +43,22 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body: Record<string, unknown> = {};
|
||||
if (flags.rpmLimit !== undefined) body.rpm_limit = flags.rpmLimit;
|
||||
if (flags.tpmLimit !== undefined) body.tpm_limit = flags.tpmLimit;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, format);
|
||||
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await updateDeployment(ctx.client, deployedModel, body);
|
||||
const deployment = response.output ?? response.data;
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(deployedModel);
|
||||
} else if (format === "text") {
|
||||
const parts: string[] = [];
|
||||
if (deployment?.rpm_limit !== undefined) parts.push(`rpm_limit=${deployment.rpm_limit}`);
|
||||
if (deployment?.tpm_limit !== undefined) parts.push(`tpm_limit=${deployment.tpm_limit}`);
|
||||
const summary = parts.length ? ` (${parts.join(", ")})` : "";
|
||||
emitBare(`Updated ${deployedModel}${summary}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, cancelFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, cancelFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const CANCEL_FLAGS = {
|
||||
jobId: {
|
||||
@@ -23,24 +23,18 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const jobId = flags.jobId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "finetune.cancel", job_id: jobId }, format);
|
||||
emitResult({ action: "finetune.cancel", job_id: jobId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await cancelFineTune(ctx.client, jobId);
|
||||
const job = response.output ?? response.data;
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(jobId);
|
||||
} else if (format === "text") {
|
||||
const status = job?.status ? ` (status=${job.status})` : "";
|
||||
emitBare(`Cancelled ${jobId}${status}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
fetchModelList,
|
||||
fetchModelListAll,
|
||||
fetchModelCapability,
|
||||
listSupportedTrainingTypes,
|
||||
modelSupportsTrainingType,
|
||||
isTrainingTypeCli,
|
||||
trainingTypeMethodVariant,
|
||||
TRAINING_TYPES_CLI,
|
||||
callConsoleGateway,
|
||||
effectiveConsoleGatewayConfig,
|
||||
anonymousConsoleCall,
|
||||
UsageError,
|
||||
type Settings,
|
||||
type ModelCapability,
|
||||
@@ -17,8 +15,6 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* Page through every foundation-model page (listFoundationModels, public — no
|
||||
* console login needed, so the gateway is called anonymously). Returns raw
|
||||
@@ -26,36 +22,12 @@ const PAGE_SIZE = 50;
|
||||
* for filtering.
|
||||
*/
|
||||
async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapability[]> {
|
||||
const eff = effectiveConsoleGatewayConfig(settings);
|
||||
const call = (api: string, data: Record<string, unknown>) =>
|
||||
callConsoleGateway(
|
||||
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
|
||||
settings.timeout,
|
||||
{ api, data },
|
||||
);
|
||||
const first = await fetchModelList(call, { pageNo: 1, pageSize: PAGE_SIZE });
|
||||
const all = [...first.models];
|
||||
const totalPages = Math.ceil(first.total / PAGE_SIZE);
|
||||
for (let pageNo = 2; pageNo <= totalPages; pageNo++) {
|
||||
const result = await fetchModelList(call, { pageNo, pageSize: PAGE_SIZE });
|
||||
all.push(...result.models);
|
||||
}
|
||||
const all = await fetchModelListAll(anonymousConsoleCall(settings));
|
||||
return all as ModelCapability[];
|
||||
}
|
||||
|
||||
const VARIANT_LABEL: Record<string, string> = {
|
||||
full: "full-parameter",
|
||||
lora: "LoRA",
|
||||
};
|
||||
|
||||
function describeTrainingType(value: string): string {
|
||||
if (!isTrainingTypeCli(value)) return value;
|
||||
const { method, variant } = trainingTypeMethodVariant(value);
|
||||
return `${VARIANT_LABEL[variant] ?? variant} ${method.toUpperCase()}`;
|
||||
}
|
||||
|
||||
const CAPABILITY_FLAGS = {
|
||||
model: {
|
||||
baseModel: {
|
||||
type: "string",
|
||||
valueHint: "<m>",
|
||||
description: "List training types supported by this base model.",
|
||||
@@ -71,31 +43,31 @@ export default defineCommand({
|
||||
description:
|
||||
"Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)",
|
||||
auth: "none",
|
||||
usageArgs: "--model <m> | --training-type <t>",
|
||||
usageArgs: "--base-model <m> | --training-type <t>",
|
||||
flags: CAPABILITY_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model qwen3-8b",
|
||||
"--base-model qwen3-8b",
|
||||
"--training-type sft-lora",
|
||||
"--training-type cpt --output json",
|
||||
"--training-type sft --quiet",
|
||||
],
|
||||
notes: [
|
||||
"Exactly one of --model / --training-type is required.",
|
||||
"Exactly one of --base-model / --training-type is required.",
|
||||
"Training-type values use the `<method>` / `<method>-lora` convention:",
|
||||
"sft | sft-lora | dpo | dpo-lora | cpt. (cpt has no -lora variant server-side.)",
|
||||
"Queries listFoundationModels, a public API — no console login needed.",
|
||||
],
|
||||
validate: (f) => {
|
||||
if (f.model && f.trainingType)
|
||||
return "--model and --training-type are mutually exclusive; pass one.";
|
||||
if (!f.model && !f.trainingType) return "one of --model / --training-type is required.";
|
||||
if (f.baseModel && f.trainingType)
|
||||
return "--base-model and --training-type are mutually exclusive; pass one.";
|
||||
if (!f.baseModel && !f.trainingType)
|
||||
return "one of --base-model / --training-type is required.";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const model = flags.model || undefined;
|
||||
const model = flags.baseModel || undefined;
|
||||
const trainingType = flags.trainingType || undefined;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
@@ -104,7 +76,7 @@ export default defineCommand({
|
||||
model,
|
||||
training_type: trainingType,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -113,7 +85,7 @@ export default defineCommand({
|
||||
if (model) {
|
||||
const capability = await fetchModelCapability(settings, model);
|
||||
if (!capability) {
|
||||
emitBare(`No foundation model found matching "${model}".`);
|
||||
emitResult({ model, error: `No foundation model found matching "${model}".` }, "json");
|
||||
return;
|
||||
}
|
||||
const supported = listSupportedTrainingTypes(capability);
|
||||
@@ -121,23 +93,15 @@ export default defineCommand({
|
||||
for (const value of supported) emitBare(value);
|
||||
return;
|
||||
}
|
||||
if (format !== "text") {
|
||||
emitResult(
|
||||
{
|
||||
model: capability.model ?? model,
|
||||
supported,
|
||||
supports: capability.supports,
|
||||
trainingTypes: capability.trainingTypes,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitBare(`${capability.model ?? model}`);
|
||||
emitBare(supported.length ? "Supported training types:" : "No supported training types.");
|
||||
for (const value of supported) {
|
||||
emitBare(` ${value.padEnd(10)} ${describeTrainingType(value)}`);
|
||||
}
|
||||
emitResult(
|
||||
{
|
||||
model: capability.model ?? model,
|
||||
supported,
|
||||
supports: capability.supports,
|
||||
trainingTypes: capability.trainingTypes,
|
||||
},
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,20 +126,15 @@ export default defineCommand({
|
||||
for (const entry of matched) emitBare(entry.model);
|
||||
return;
|
||||
}
|
||||
if (format !== "text") {
|
||||
emitResult(
|
||||
{
|
||||
training_type: trainingType,
|
||||
method,
|
||||
variant,
|
||||
count: matched.length,
|
||||
models: matched,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitBare(`Models supporting ${trainingType} (${method} / ${variant}): ${matched.length}`);
|
||||
for (const entry of matched) emitBare(` ${entry.model}`);
|
||||
emitResult(
|
||||
{
|
||||
training_type: trainingType,
|
||||
method,
|
||||
variant,
|
||||
count: matched.length,
|
||||
models: matched,
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
listCheckpoints,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
|
||||
import { defineCommand, listCheckpoints, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const CHECKPOINTS_FLAGS = {
|
||||
jobId: {
|
||||
@@ -15,6 +10,8 @@ const CHECKPOINTS_FLAGS = {
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const EXPIRY_WARN_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours
|
||||
|
||||
export default defineCommand({
|
||||
description: "List checkpoints produced by a fine-tune job",
|
||||
auth: "apiKey",
|
||||
@@ -22,16 +19,15 @@ export default defineCommand({
|
||||
flags: CHECKPOINTS_FLAGS,
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
|
||||
notes: [
|
||||
"Use the returned `checkpoint` value with `finetune export` to publish",
|
||||
"a deployable model.",
|
||||
"`model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model-name`.",
|
||||
"Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const jobId = flags.jobId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "finetune.checkpoints", job_id: jobId }, format);
|
||||
emitResult({ action: "finetune.checkpoints", job_id: jobId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,22 +40,26 @@ export default defineCommand({
|
||||
checkpoint: item.checkpoint ?? item.checkpoint_id ?? "",
|
||||
step: item.step !== undefined ? String(item.step) : "",
|
||||
status: item.status ?? "",
|
||||
model_name: item.model_name ?? "",
|
||||
expire_time: item.expire_time ?? "",
|
||||
}));
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ items, total, request_id: response.request_id }, format);
|
||||
return;
|
||||
}
|
||||
emitResult({ items, total, request_id: response.request_id }, "json");
|
||||
|
||||
// text / quiet
|
||||
if (items.length === 0) {
|
||||
emitBare("No checkpoints found.");
|
||||
return;
|
||||
// Near-expiry warning: check if any non-expired checkpoint is within 72h of expiry.
|
||||
const now = Date.now();
|
||||
const expiringSoon = items.filter((item) => {
|
||||
if (!item.expire_time) return false;
|
||||
const deadline = new Date(item.expire_time).getTime();
|
||||
if (Number.isNaN(deadline)) return false;
|
||||
const remaining = deadline - now;
|
||||
return remaining > 0 && remaining < EXPIRY_WARN_THRESHOLD_MS;
|
||||
});
|
||||
if (expiringSoon.length > 0) {
|
||||
process.stderr.write(
|
||||
`\n[warning] ${expiringSoon.length} checkpoint(s) will expire within 72 hours. ` +
|
||||
"Export or deploy before expiry to avoid losing the model artifact.\n",
|
||||
);
|
||||
}
|
||||
const headers = ["CHECKPOINT", "STEP", "STATUS"];
|
||||
const rows = items.map((i) => [i.checkpoint, i.step, i.status]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
emitBare(`\nTotal: ${total}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
createFineTune,
|
||||
getDataset,
|
||||
uploadDataset,
|
||||
@@ -27,7 +26,7 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { existsSync, statSync } from "fs";
|
||||
import { basename } from "path";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
/**
|
||||
* A `--datasets` / `--validations` token is treated as a local file to upload
|
||||
@@ -208,7 +207,7 @@ async function uploadResolvedLocal(
|
||||
}
|
||||
|
||||
/** The modality a `finetune <modality> create` subcommand is bound to. */
|
||||
type CommandModality = "text" | "audio" | "image";
|
||||
type CommandModality = "text" | "audio" | "image" | "video";
|
||||
|
||||
/**
|
||||
* Flags shared by every `finetune <modality> create` subcommand: what to train
|
||||
@@ -216,10 +215,10 @@ type CommandModality = "text" | "audio" | "image";
|
||||
* output. Every modality's model consumes these.
|
||||
*/
|
||||
const COMMON_FLAGS = {
|
||||
model: {
|
||||
baseModel: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Base model to fine-tune",
|
||||
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
|
||||
required: true,
|
||||
},
|
||||
datasets: {
|
||||
@@ -317,13 +316,41 @@ const IMAGE_FLAGS = {
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const TEXT_USAGE =
|
||||
"--model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
|
||||
"--base-model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
|
||||
|
||||
const AUDIO_USAGE =
|
||||
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
|
||||
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
|
||||
|
||||
const IMAGE_USAGE =
|
||||
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
|
||||
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
|
||||
|
||||
/**
|
||||
* Video (Wan i2v/kf2v) flags: exposes the three hyper-parameters that the
|
||||
* video API supports and users may want to override. Defaults are model-specific
|
||||
* (resolved by the sft-lora profile: wan2.7 → batch_size 1 / max_pixels 102400,
|
||||
* wan2.5 → 4 / 36864, wan2.2 → 4 / 262144).
|
||||
*/
|
||||
const VIDEO_FLAGS = {
|
||||
...COMMON_FLAGS,
|
||||
nEpochs: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Training epochs (default: 50)",
|
||||
},
|
||||
batchSize: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Batch size (default: model-specific, 1 for wan2.7, 4 for wan2.5/2.2)",
|
||||
},
|
||||
learningRate: {
|
||||
type: "string",
|
||||
valueHint: "<str>",
|
||||
description: 'Learning rate as a string to preserve precision (default: "2e-5")',
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const VIDEO_USAGE =
|
||||
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>]";
|
||||
|
||||
const COMMON_NOTES = [
|
||||
"Creating a job uploads any local datasets and consumes training quota.",
|
||||
@@ -383,7 +410,7 @@ async function runCreate<F extends FlagsDef>(
|
||||
): Promise<void> {
|
||||
const { identity, settings } = ctx;
|
||||
const flags = ctx.flags as Record<string, unknown>;
|
||||
const model = flags.model as string;
|
||||
const model = flags.baseModel as string;
|
||||
const datasetsRaw = flags.datasets as string;
|
||||
|
||||
// CosyVoice audio fine-tuning accepts exactly one training file
|
||||
@@ -441,6 +468,10 @@ async function runCreate<F extends FlagsDef>(
|
||||
if (detected === "image-i2i") modality = "image-i2i";
|
||||
}
|
||||
}
|
||||
if (commandModality === "video" && firstLocalPath && !settings.dryRun) {
|
||||
const detected = await detectModality(firstLocalPath);
|
||||
if (detected === "video-kf2v") modality = "video-kf2v";
|
||||
}
|
||||
|
||||
const training = await analyzeDatasetTokens(
|
||||
settings,
|
||||
@@ -606,8 +637,6 @@ async function runCreate<F extends FlagsDef>(
|
||||
if (modelName) body.model_name = modelName;
|
||||
if (suffix) body.finetuned_output_suffix = suffix;
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
const pending = [
|
||||
...training.localPaths.map((path) => ({ field: "datasets", path })),
|
||||
@@ -617,7 +646,7 @@ async function runCreate<F extends FlagsDef>(
|
||||
pending.length > 0
|
||||
? { action: "finetune.create", body, pending_uploads: pending }
|
||||
: { action: "finetune.create", body },
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -627,16 +656,8 @@ async function runCreate<F extends FlagsDef>(
|
||||
|
||||
if (settings.quiet) {
|
||||
if (job?.job_id) emitBare(job.job_id);
|
||||
} else if (format === "text") {
|
||||
if (job?.job_id) {
|
||||
emitBare(`Created fine-tune job: ${job.job_id}`);
|
||||
if (job.status) emitBare(`Status: ${job.status}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,14 +668,14 @@ export const finetuneTextCreate = defineCommand({
|
||||
usageArgs: TEXT_USAGE,
|
||||
flags: TEXT_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model qwen3-8b --datasets file-xxx",
|
||||
"--model qwen3-8b --datasets ./train.jsonl",
|
||||
"--model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
|
||||
"--model qwen3-8b --datasets file-aaa,./extra.jsonl",
|
||||
"--model qwen3-8b --datasets ./train.jsonl --training-type sft",
|
||||
'--model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
|
||||
"--model qwen3-8b --datasets file-xxx --output json",
|
||||
"--model qwen3-8b --datasets file-xxx --dry-run",
|
||||
"--base-model qwen3-8b --datasets file-xxx",
|
||||
"--base-model qwen3-8b --datasets ./train.jsonl",
|
||||
"--base-model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
|
||||
"--base-model qwen3-8b --datasets file-aaa,./extra.jsonl",
|
||||
"--base-model qwen3-8b --datasets ./train.jsonl --training-type sft",
|
||||
'--base-model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
|
||||
"--base-model qwen3-8b --datasets file-xxx --output json",
|
||||
"--base-model qwen3-8b --datasets file-xxx --dry-run",
|
||||
],
|
||||
notes: TEXT_NOTES,
|
||||
run: (ctx) => runCreate("text", ctx),
|
||||
@@ -667,11 +688,11 @@ export const finetuneAudioCreate = defineCommand({
|
||||
usageArgs: AUDIO_USAGE,
|
||||
flags: AUDIO_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model cosyvoice-v3-flash --datasets ./audio.zip",
|
||||
"--model cosyvoice-v3-flash --datasets file-xxx",
|
||||
"--model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
|
||||
"--model cosyvoice-v3-flash --datasets file-xxx --output json",
|
||||
"--model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
|
||||
"--base-model cosyvoice-v3-flash --datasets ./audio.zip",
|
||||
"--base-model cosyvoice-v3-flash --datasets file-xxx",
|
||||
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
|
||||
"--base-model cosyvoice-v3-flash --datasets file-xxx --output json",
|
||||
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
|
||||
],
|
||||
notes: AUDIO_NOTES,
|
||||
run: (ctx) => runCreate("audio", ctx),
|
||||
@@ -684,13 +705,38 @@ export const finetuneImageCreate = defineCommand({
|
||||
usageArgs: IMAGE_USAGE,
|
||||
flags: IMAGE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--model wan2.7-image-pro --datasets ./images.zip",
|
||||
"--model wan2.7-image-pro --datasets file-xxx",
|
||||
"--model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
|
||||
"--model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
|
||||
"--model wan2.7-image-pro --datasets file-xxx --output json",
|
||||
"--model wan2.7-image-pro --datasets ./images.zip --dry-run",
|
||||
"--base-model wan2.7-image-pro --datasets ./images.zip",
|
||||
"--base-model wan2.7-image-pro --datasets file-xxx",
|
||||
"--base-model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
|
||||
"--base-model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
|
||||
"--base-model wan2.7-image-pro --datasets file-xxx --output json",
|
||||
"--base-model wan2.7-image-pro --datasets ./images.zip --dry-run",
|
||||
],
|
||||
notes: IMAGE_NOTES,
|
||||
run: (ctx) => runCreate("image", ctx),
|
||||
});
|
||||
|
||||
const VIDEO_NOTES = [
|
||||
...COMMON_NOTES,
|
||||
"Video generation training (Wan i2v/kf2v) runs efficient_sft with model-",
|
||||
"specific defaults: wan2.7 (batch_size=1, max_pixels=102400), wan2.5/2.2",
|
||||
"(batch_size=4, max_pixels per model). Override with --batch-size/--n-epochs.",
|
||||
"Datasets are .zip archives with data.jsonl + frame images + videos.",
|
||||
"Recommended: ≥10 training samples, 20-100 for stable results.",
|
||||
];
|
||||
|
||||
/** `bl finetune video create` — fine-tune a video generation model. Datasets are `.zip`. */
|
||||
export const finetuneVideoCreate = defineCommand({
|
||||
description: "Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft)",
|
||||
auth: "apiKey",
|
||||
usageArgs: VIDEO_USAGE,
|
||||
flags: VIDEO_FLAGS,
|
||||
exampleArgs: [
|
||||
"--base-model wan2.7-i2v --datasets file-xxx",
|
||||
"--base-model wan2.7-i2v --datasets ./i2v-data.zip",
|
||||
"--base-model wan2.2-kf2v-flash --datasets file-xxx --n-epochs 100",
|
||||
"--base-model wan2.7-i2v --datasets file-xxx --dry-run",
|
||||
],
|
||||
notes: VIDEO_NOTES,
|
||||
run: (ctx) => runCreate("video", ctx),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, deleteFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
jobId: {
|
||||
@@ -23,10 +23,9 @@ export default defineCommand({
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const jobId = flags.jobId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "finetune.delete", job_id: jobId }, format);
|
||||
emitResult({ action: "finetune.delete", job_id: jobId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,11 +33,8 @@ export default defineCommand({
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(jobId);
|
||||
} else if (format === "text") {
|
||||
emitBare(`Deleted ${jobId}.`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
exportCheckpoint,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, exportCheckpoint, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const EXPORT_FLAGS = {
|
||||
jobId: {
|
||||
@@ -39,11 +34,10 @@ export default defineCommand({
|
||||
"explicit export is the canonical path for non-best checkpoints.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { identity, settings, flags } = ctx;
|
||||
const { settings, flags } = ctx;
|
||||
const jobId = flags.jobId;
|
||||
const checkpoint = flags.checkpoint;
|
||||
const modelName = flags.modelName;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
@@ -53,7 +47,7 @@ export default defineCommand({
|
||||
checkpoint,
|
||||
model_name: modelName,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -64,14 +58,8 @@ export default defineCommand({
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(exported);
|
||||
} else if (format === "text") {
|
||||
emitBare(`Exported ${jobId} / ${checkpoint} → model_name=${exported}`);
|
||||
emitBare(
|
||||
`Next: ${identity.binName} deploy text create --model ${exported} --name <display-name>`,
|
||||
);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
emitResult(response, "json");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Best-effort actual training fee calculation using the model catalog's
|
||||
* "ft" (fine-tune) price entry. Pure API-key domain — no console auth needed.
|
||||
*
|
||||
* The model catalog (`listFoundationModels` via public gateway) returns a
|
||||
* `prices[]` array **only when `queryPrice: true` is passed** (the same flag
|
||||
* `fetchModelDetail` uses). Combined with the job's `output.usage` (actual
|
||||
* consumed tokens, present on SUCCEEDED / CANCELED), this gives the exact
|
||||
* training cost without any console-domain login.
|
||||
*/
|
||||
import {
|
||||
callConsoleGateway,
|
||||
effectiveConsoleGatewayConfig,
|
||||
unwrapResponse,
|
||||
MODEL_LIST_API,
|
||||
type Settings,
|
||||
type ModelPriceInfo,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
export interface ActualFee {
|
||||
cost: number;
|
||||
unitPrice: number;
|
||||
priceUnit: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the model's training price from the public catalog gateway.
|
||||
* Uses the same anonymous gateway path as `fetchModelCapability` (no console
|
||||
* token required), but adds `queryPrice: true` to include the prices array.
|
||||
*/
|
||||
async function fetchTrainingPrice(
|
||||
settings: Settings,
|
||||
model: string,
|
||||
): Promise<ModelPriceInfo | null> {
|
||||
const eff = effectiveConsoleGatewayConfig(settings);
|
||||
const result = await callConsoleGateway(
|
||||
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
|
||||
settings.timeout,
|
||||
{
|
||||
api: MODEL_LIST_API,
|
||||
data: {
|
||||
input: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
group: true,
|
||||
model,
|
||||
queryPrice: true,
|
||||
querySampleCode: false,
|
||||
queryGroupByModel: true,
|
||||
queryQuota: false,
|
||||
queryQpmInfo: false,
|
||||
queryApplyStatus: false,
|
||||
queryPermissions: false,
|
||||
queryActivationStatus: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const responseData = unwrapResponse(result as Record<string, unknown>);
|
||||
const list = (responseData.list as Record<string, unknown>[]) ?? [];
|
||||
// The response is grouped; find the exact model in items.
|
||||
for (const group of list) {
|
||||
const items = (group.items as Record<string, unknown>[]) ?? [];
|
||||
for (const item of items) {
|
||||
if (item.model === model) {
|
||||
const prices = (item.prices as ModelPriceInfo[]) ?? [];
|
||||
return prices.find((entry) => entry.type === "ft") ?? null;
|
||||
}
|
||||
}
|
||||
// Flat response fallback (no items nesting).
|
||||
if (group.model === model) {
|
||||
const prices = (group.prices as ModelPriceInfo[]) ?? [];
|
||||
return prices.find((entry) => entry.type === "ft") ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the actual training fee from the model catalog's "ft" price entry.
|
||||
* Returns null when the price is unavailable (network error, model not in
|
||||
* catalog, or no "ft" entry). Never throws.
|
||||
*
|
||||
* Only uses the public model catalog (model metadata) — does NOT call
|
||||
* console-domain pricing APIs (modelCenter.getModelPrice). Models whose
|
||||
* catalog entry lacks a "ft" price (e.g. CosyVoice) will simply omit the
|
||||
* training_cost field until the platform adds it to the catalog.
|
||||
*/
|
||||
export async function computeActualFee(
|
||||
settings: Settings,
|
||||
model: string,
|
||||
usageTokens: number,
|
||||
): Promise<ActualFee | null> {
|
||||
try {
|
||||
const ftEntry = await fetchTrainingPrice(settings, model);
|
||||
const unitPrice = Number(ftEntry?.price);
|
||||
if (!Number.isFinite(unitPrice) || unitPrice <= 0) return null;
|
||||
const priceUnit = ftEntry?.priceUnit ?? "每百万tokens";
|
||||
// Catalog price is yuan per million tokens.
|
||||
const cost = (usageTokens / 1_000_000) * unitPrice;
|
||||
return { cost: Number(cost.toFixed(4)), unitPrice, priceUnit };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineCommand, detectOutputFormat, getFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { computeActualFee } from "./fee.ts";
|
||||
|
||||
const GET_FLAGS = {
|
||||
jobId: {
|
||||
@@ -17,12 +18,11 @@ export default defineCommand({
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
|
||||
async run(ctx) {
|
||||
const { identity, settings, flags } = ctx;
|
||||
const { settings, flags } = ctx;
|
||||
const jobId = flags.jobId;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "finetune.get", job_id: jobId }, format);
|
||||
emitResult({ action: "finetune.get", job_id: jobId }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,18 +30,24 @@ export default defineCommand({
|
||||
const job = response.output ?? response.data;
|
||||
|
||||
if (!job) {
|
||||
emitBare(`No data returned for ${jobId}`);
|
||||
emitResult({ job_id: jobId, error: "No data returned" }, "json");
|
||||
return;
|
||||
}
|
||||
|
||||
const hp = job.hyper_parameters;
|
||||
const hyperParameters = job.hyper_parameters;
|
||||
const hyperParts: string[] = [];
|
||||
if (hp?.n_epochs !== undefined) hyperParts.push(`n_epochs=${hp.n_epochs}`);
|
||||
if (hp?.batch_size !== undefined) hyperParts.push(`batch_size=${hp.batch_size}`);
|
||||
if (hp?.learning_rate !== undefined) hyperParts.push(`learning_rate=${hp.learning_rate}`);
|
||||
if (hp?.max_length !== undefined) hyperParts.push(`max_length=${hp.max_length}`);
|
||||
if (hyperParameters?.n_epochs !== undefined)
|
||||
hyperParts.push(`n_epochs=${hyperParameters.n_epochs}`);
|
||||
if (hyperParameters?.batch_size !== undefined)
|
||||
hyperParts.push(`batch_size=${hyperParameters.batch_size}`);
|
||||
if (hyperParameters?.learning_rate !== undefined)
|
||||
hyperParts.push(`learning_rate=${hyperParameters.learning_rate}`);
|
||||
if (hyperParameters?.max_length !== undefined)
|
||||
hyperParts.push(`max_length=${hyperParameters.max_length}`);
|
||||
|
||||
const item = {
|
||||
const usageTokens = typeof job.usage === "number" ? job.usage : undefined;
|
||||
|
||||
const item: Record<string, unknown> = {
|
||||
job_id: job.job_id ?? jobId,
|
||||
base_model: job.model ?? "",
|
||||
status: job.status ?? "",
|
||||
@@ -53,29 +59,20 @@ export default defineCommand({
|
||||
model_name: job.model_name ?? "",
|
||||
created_at: job.create_time ?? job.gmt_create ?? "",
|
||||
updated_at: job.end_time ?? job.gmt_modified ?? "",
|
||||
usage_tokens: usageTokens ?? "",
|
||||
charge_type: typeof job.charge_type === "string" ? job.charge_type : "",
|
||||
};
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ ...item, request_id: response.request_id }, format);
|
||||
return;
|
||||
// Actual fee: only when the platform reports a concrete token count
|
||||
// (SUCCEEDED / CANCELED). Best-effort — silently omitted on lookup failure.
|
||||
if (usageTokens !== undefined && usageTokens > 0 && job.model) {
|
||||
const fee = await computeActualFee(settings, job.model, usageTokens);
|
||||
if (fee) {
|
||||
item.training_cost = fee.cost;
|
||||
item.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
|
||||
}
|
||||
}
|
||||
|
||||
// text / quiet
|
||||
emitBare(`job_id: ${item.job_id}`);
|
||||
if (item.base_model) emitBare(`base_model: ${item.base_model}`);
|
||||
if (item.status) emitBare(`status: ${item.status}`);
|
||||
if (item.training_type) emitBare(`training_type: ${item.training_type}`);
|
||||
if (item.training_files.length) emitBare(`training_files: ${item.training_files.join(", ")}`);
|
||||
if (item.validation_files.length)
|
||||
emitBare(`validation_files: ${item.validation_files.join(", ")}`);
|
||||
if (item.hyper_params) emitBare(`hyper_params: ${item.hyper_params}`);
|
||||
if (item.output_model)
|
||||
emitBare(
|
||||
`output_model: ${item.output_model} (→ ${identity.binName} deploy text create --model)`,
|
||||
);
|
||||
if (item.model_name) emitBare(`model_name: ${item.model_name}`);
|
||||
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
|
||||
if (item.updated_at) emitBare(`updated_at: ${item.updated_at}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
emitResult({ ...item, request_id: response.request_id }, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, listFineTunes, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
|
||||
import { defineCommand, listFineTunes, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
@@ -13,71 +13,48 @@ const LIST_FLAGS = {
|
||||
valueHint: "<s>",
|
||||
description: "Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED)",
|
||||
},
|
||||
baseModel: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Filter by base model ID (server-side)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List fine-tune jobs",
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>]",
|
||||
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>] [--base-model <model>]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
|
||||
exampleArgs: ["", "--status RUNNING", "--base-model qwen3-8b", "--page-size 20"],
|
||||
async run(ctx) {
|
||||
const { identity, settings, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const { settings, flags } = ctx;
|
||||
const pageNo = flags.page;
|
||||
const pageSize = flags.pageSize;
|
||||
const status = flags.status || undefined;
|
||||
const model = flags.baseModel || undefined;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ action: "finetune.list", page: pageNo, page_size: pageSize, status }, format);
|
||||
emitResult(
|
||||
{ action: "finetune.list", page: pageNo, page_size: pageSize, status, model },
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status });
|
||||
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status, model });
|
||||
const payload = response.output ?? response.data;
|
||||
const jobs = payload?.jobs ?? [];
|
||||
const total = payload?.total;
|
||||
|
||||
const items = jobs.map((item) => ({
|
||||
job_id: item.job_id ?? "",
|
||||
base_model: item.model ?? "",
|
||||
status: item.status ?? "",
|
||||
training_type: item.training_type ?? "",
|
||||
output_model: item.finetuned_output ?? "",
|
||||
created_at: item.create_time ?? item.gmt_create ?? "",
|
||||
const items = jobs.map((job) => ({
|
||||
job_id: job.job_id ?? "",
|
||||
base_model: job.model ?? "",
|
||||
status: job.status ?? "",
|
||||
training_type: job.training_type ?? "",
|
||||
output_model: job.finetuned_output ?? "",
|
||||
created_at: job.create_time ?? job.gmt_create ?? "",
|
||||
}));
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ items, total, request_id: response.request_id }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// text / quiet
|
||||
if (items.length === 0) {
|
||||
emitBare("No fine-tune jobs found.");
|
||||
return;
|
||||
}
|
||||
const headers = [
|
||||
"JOB_ID",
|
||||
"BASE_MODEL",
|
||||
"STATUS",
|
||||
"TRAINING_TYPE",
|
||||
"OUTPUT_MODEL",
|
||||
"CREATED_AT",
|
||||
];
|
||||
const rows = items.map((i) => [
|
||||
i.job_id,
|
||||
i.base_model,
|
||||
i.status,
|
||||
i.training_type,
|
||||
i.output_model,
|
||||
i.created_at,
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
if (total !== undefined) emitBare(`\nTotal: ${total}`);
|
||||
emitBare(
|
||||
`Tip: OUTPUT_MODEL is the input for \`${identity.binName} deploy text create --model\``,
|
||||
);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
emitResult({ items, total, request_id: response.request_id }, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
getFineTuneLogs,
|
||||
type Client,
|
||||
type FineTuneLogEntry,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
/**
|
||||
* Render a single log entry as a single line (mirrors the flatten logic used
|
||||
* for non-search text output: prefer common fields, fall back to JSON).
|
||||
* Render a single log entry as a single line (used for search matching:
|
||||
* prefer common fields, fall back to JSON).
|
||||
*/
|
||||
function renderEntry(entry: FineTuneLogEntry | string): string {
|
||||
if (typeof entry === "string") return entry;
|
||||
const record = entry as Record<string, unknown>;
|
||||
const ts = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
|
||||
const timestamp = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
|
||||
const level = (record.level ?? "") as string;
|
||||
const msg = (record.message ?? record.msg ?? record.log ?? "") as string;
|
||||
if (msg || ts || level) {
|
||||
return [ts, level, msg].filter(Boolean).join("\t");
|
||||
const message = (record.message ?? record.msg ?? record.log ?? "") as string;
|
||||
if (message || timestamp || level) {
|
||||
return [timestamp, level, message].filter(Boolean).join("\t");
|
||||
}
|
||||
return JSON.stringify(entry);
|
||||
}
|
||||
@@ -48,16 +47,16 @@ async function fetchAllLogs(
|
||||
let total = 0;
|
||||
// Hard cap to avoid an unbounded loop if the server misreports `total`.
|
||||
const maxPages = 200;
|
||||
for (let i = 0; i < maxPages; i++) {
|
||||
for (let page = 0; page < maxPages; page++) {
|
||||
const response = await getFineTuneLogs(client, jobId, { pageNo, pageSize });
|
||||
const payload = response.output ?? response.data;
|
||||
const page = payload?.logs ?? [];
|
||||
const logs = payload?.logs ?? [];
|
||||
total = payload?.total ?? total;
|
||||
if (page.length === 0) break;
|
||||
entries.push(...page);
|
||||
if (logs.length === 0) break;
|
||||
entries.push(...logs);
|
||||
// Stop once we've collected everything the server claims exists.
|
||||
if (total && entries.length >= total) break;
|
||||
if (page.length < pageSize) break;
|
||||
if (logs.length < pageSize) break;
|
||||
pageNo++;
|
||||
}
|
||||
return { entries, total };
|
||||
@@ -110,7 +109,6 @@ export default defineCommand({
|
||||
const pageSize = flags.pageSize;
|
||||
const search = flags.search || undefined;
|
||||
const tail = flags.tail;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
@@ -122,7 +120,7 @@ export default defineCommand({
|
||||
search,
|
||||
tail,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -147,18 +145,6 @@ export default defineCommand({
|
||||
const result =
|
||||
tailApplied !== undefined ? scanned.slice(scanned.length - tailApplied) : scanned;
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
if (result.length === 0) {
|
||||
emitBare(search ? `No logs matched "${search}".` : "No logs returned.");
|
||||
return;
|
||||
}
|
||||
for (const entry of result) emitBare(renderEntry(entry));
|
||||
const parts: string[] = [`${result.length} shown`];
|
||||
if (matched !== undefined) parts.push(`matched ${matched}`);
|
||||
parts.push(`of ${entries.length}` + (total ? ` (total ${total})` : ""));
|
||||
emitBare(`\n${parts.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
emitResult(
|
||||
{
|
||||
...(matched !== undefined ? { matched } : {}),
|
||||
@@ -168,28 +154,13 @@ export default defineCommand({
|
||||
...(tailApplied !== undefined ? { tail: tailApplied } : {}),
|
||||
logs: result,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: single page, verbatim response.
|
||||
const response = await getFineTuneLogs(ctx.client, jobId, { pageNo, pageSize });
|
||||
const payload = response.output ?? response.data;
|
||||
const logs = payload?.logs ?? [];
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
if (logs.length === 0) {
|
||||
emitBare("No logs returned.");
|
||||
return;
|
||||
}
|
||||
for (const entry of logs) {
|
||||
emitBare(renderEntry(entry));
|
||||
}
|
||||
if (payload?.total !== undefined) emitBare(`\nTotal: ${payload.total}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
emitResult(response, "json");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
defineCommand,
|
||||
fetchTrainingModelPrice,
|
||||
estimateSftDpoTokens,
|
||||
estimateCptTokens,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const PRICE_FLAGS = {
|
||||
baseModel: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
|
||||
required: true,
|
||||
},
|
||||
datasets: {
|
||||
type: "string",
|
||||
valueHint: "<ids>",
|
||||
description: "Training dataset file IDs, comma-separated (required)",
|
||||
required: true,
|
||||
},
|
||||
trainingType: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: "Training type: sft | dpo | cpt (default: sft)",
|
||||
},
|
||||
nEpochs: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Number of training epochs (default: 3)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const SUPPORTED_TRAINING_TYPES = ["sft", "dpo", "cpt"];
|
||||
|
||||
// Fixed hyper-parameters used for estimation. Only n_epochs materially affects
|
||||
// the estimate; the rest are held at representative defaults (not exposed as
|
||||
// flags to keep the command surface minimal).
|
||||
const ESTIMATE_BATCH_SIZE = 16;
|
||||
const ESTIMATE_MAX_LENGTH = 8192;
|
||||
const DEFAULT_N_EPOCHS = 3;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Estimate the training cost for a fine-tune job (token billing)",
|
||||
auth: "console",
|
||||
usageArgs: "--base-model <model> --datasets <ids> [--training-type <type>] [--n-epochs <n>]",
|
||||
flags: PRICE_FLAGS,
|
||||
exampleArgs: [
|
||||
"--base-model qwen3-8b --datasets file-ft-xxx",
|
||||
"--base-model qwen3-8b --datasets file-ft-xxx,file-ft-yyy --n-epochs 2",
|
||||
"--base-model qwen3-8b --datasets file-ft-xxx --training-type cpt",
|
||||
],
|
||||
notes: [
|
||||
"Estimate only — the server computes token usage from the datasets; final cost is subject to the bill.",
|
||||
"Covers token billing for sft / dpo / cpt. Training-unit (MTU) billing is not supported by this command.",
|
||||
"Hyper-parameters other than --n-epochs are fixed at representative defaults for estimation.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const model = flags.baseModel;
|
||||
const datasetIds = flags.datasets
|
||||
.split(",")
|
||||
.map((datasetId) => datasetId.trim())
|
||||
.filter(Boolean);
|
||||
const trainingType = (flags.trainingType ?? "sft").toLowerCase();
|
||||
const nEpochs = flags.nEpochs ?? DEFAULT_N_EPOCHS;
|
||||
|
||||
if (!SUPPORTED_TRAINING_TYPES.includes(trainingType)) {
|
||||
throw new BailianError(
|
||||
`Unsupported training type "${trainingType}". Supported: ${SUPPORTED_TRAINING_TYPES.join(", ")}.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (datasetIds.length === 0) {
|
||||
throw new BailianError("--datasets must contain at least one file ID.", ExitCode.USAGE);
|
||||
}
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{ action: "finetune.price", model, datasets: datasetIds, trainingType, nEpochs },
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unit price (yuan per 千Token).
|
||||
const priceInfo = await fetchTrainingModelPrice(ctx.client, model);
|
||||
const unitPrice = Number(priceInfo.price);
|
||||
if (!Number.isFinite(unitPrice)) {
|
||||
throw new BailianError(
|
||||
`No training price found for model "${model}".`,
|
||||
ExitCode.GENERAL,
|
||||
undefined,
|
||||
{ rawResponse: JSON.stringify(priceInfo) },
|
||||
);
|
||||
}
|
||||
|
||||
// Per-epoch token estimate (min/max range).
|
||||
const estimate =
|
||||
trainingType === "cpt"
|
||||
? await estimateCptTokens(ctx.client, model, datasetIds.join(","), nEpochs)
|
||||
: await estimateSftDpoTokens(ctx.client, datasetIds, {
|
||||
nEpochs,
|
||||
batchSize: ESTIMATE_BATCH_SIZE,
|
||||
maxLength: ESTIMATE_MAX_LENGTH,
|
||||
});
|
||||
|
||||
const minPerEpoch = estimate.estimatedDatasetConsumedTokensMinPerEpoch ?? 0;
|
||||
const maxPerEpoch = estimate.estimatedDatasetConsumedTokensMaxPerEpoch ?? 0;
|
||||
const mixedMinPerEpoch = estimate.estimatedMixedConsumedTokensMinPerEpoch ?? 0;
|
||||
const mixedMaxPerEpoch = estimate.estimatedMixedConsumedTokensMaxPerEpoch ?? 0;
|
||||
|
||||
const minTokens = (minPerEpoch + mixedMinPerEpoch) * nEpochs;
|
||||
const maxTokens = (maxPerEpoch + mixedMaxPerEpoch) * nEpochs;
|
||||
// price is yuan per 1000 tokens.
|
||||
const minFee = (minTokens / 1000) * unitPrice;
|
||||
const maxFee = (maxTokens / 1000) * unitPrice;
|
||||
|
||||
emitResult(
|
||||
{
|
||||
model,
|
||||
training_type: trainingType,
|
||||
n_epochs: nEpochs,
|
||||
unit_price: unitPrice,
|
||||
price_unit: priceInfo.priceUnit ?? "千Token",
|
||||
estimated_tokens: { min: minTokens, max: maxTokens },
|
||||
estimated_fee_yuan: {
|
||||
min: Number(minFee.toFixed(4)),
|
||||
max: Number(maxFee.toFixed(4)),
|
||||
},
|
||||
disclaimer: "Server-side estimate; final cost is subject to the bill.",
|
||||
},
|
||||
"json",
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
getFineTune,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { computeActualFee } from "./fee.ts";
|
||||
|
||||
const DEFAULT_INTERVAL_SEC = 10;
|
||||
const MIN_INTERVAL_SEC = 1;
|
||||
@@ -103,7 +103,6 @@ export default defineCommand({
|
||||
const follow = flags.follow;
|
||||
const intervalSec = Math.max(MIN_INTERVAL_SEC, flags.interval ?? DEFAULT_INTERVAL_SEC);
|
||||
const pollTimeoutSec = flags.pollTimeout;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
@@ -114,7 +113,7 @@ export default defineCommand({
|
||||
interval: intervalSec,
|
||||
timeout: pollTimeoutSec,
|
||||
},
|
||||
format,
|
||||
"json",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -132,16 +131,24 @@ export default defineCommand({
|
||||
if (settings.quiet) {
|
||||
// Just the status word — ideal for `status=$(... finetune watch ... --quiet)`.
|
||||
emitBare(status || "UNKNOWN");
|
||||
} else if (format === "text") {
|
||||
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
|
||||
if (status === "SUCCEEDED") emitBare(`✓ ${jobId} ${status}`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
} else {
|
||||
// json: a compact, purpose-built status probe.
|
||||
emitResult(
|
||||
{ job_id: jobId, status: status || "UNKNOWN", terminal, request_id: response.request_id },
|
||||
format,
|
||||
);
|
||||
const output: Record<string, unknown> = {
|
||||
job_id: jobId,
|
||||
status: status || "UNKNOWN",
|
||||
terminal,
|
||||
request_id: response.request_id,
|
||||
};
|
||||
// Enrich terminal output with actual fee when usage is reported.
|
||||
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
|
||||
if (terminal && usageTokens && usageTokens > 0 && job?.model) {
|
||||
output.usage_tokens = usageTokens;
|
||||
const fee = await computeActualFee(settings, job.model as string, usageTokens);
|
||||
if (fee) {
|
||||
output.training_cost = fee.cost;
|
||||
output.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
|
||||
}
|
||||
}
|
||||
emitResult(output, "json");
|
||||
}
|
||||
|
||||
if (terminal && status !== "SUCCEEDED") {
|
||||
@@ -168,18 +175,28 @@ export default defineCommand({
|
||||
const job = response.output ?? response.data;
|
||||
const status = String(job?.status ?? "").toUpperCase();
|
||||
|
||||
if (format === "text" && !settings.quiet && status !== lastStatus) {
|
||||
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
|
||||
if (!settings.quiet && status !== lastStatus) {
|
||||
process.stderr.write(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}\n`);
|
||||
lastStatus = status;
|
||||
}
|
||||
|
||||
if (TERMINAL_STATUSES.has(status)) {
|
||||
const elapsed = Date.now() - startedAt;
|
||||
if (format !== "text" || settings.quiet) {
|
||||
emitResult(response, format);
|
||||
} else if (status === "SUCCEEDED") {
|
||||
emitBare(`\n✓ ${jobId} ${status} (elapsed ${formatElapsed(elapsed)})`);
|
||||
emitRequestId(response.request_id, settings.quiet);
|
||||
if (settings.quiet) {
|
||||
emitBare(status || "UNKNOWN");
|
||||
} else {
|
||||
// Enrich the raw response with actual fee when usage is available.
|
||||
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
|
||||
const enriched: Record<string, unknown> = { ...response };
|
||||
if (usageTokens && usageTokens > 0 && job?.model) {
|
||||
const fee = await computeActualFee(settings, job.model as string, usageTokens);
|
||||
if (fee) {
|
||||
enriched.training_cost = fee.cost;
|
||||
enriched.usage_tokens = usageTokens;
|
||||
enriched.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
|
||||
}
|
||||
}
|
||||
emitResult(enriched, "json");
|
||||
}
|
||||
if (status !== "SUCCEEDED") {
|
||||
throw new BailianError(
|
||||
@@ -205,7 +222,7 @@ export default defineCommand({
|
||||
// Any other error (including the BailianError thrown above) propagates to
|
||||
// the central handler.
|
||||
if (controller.signal.aborted) {
|
||||
emitBare("\nInterrupted.");
|
||||
process.stderr.write("\nInterrupted.\n");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagAddCategoryResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CATEGORY_ADD_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Category name (1-20 chars)",
|
||||
required: true,
|
||||
},
|
||||
parentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Create as a sub-category of this category",
|
||||
},
|
||||
collectionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Create under this collection (defaults to the platform collection)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Create a data-center category",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--name <text> [flags]",
|
||||
flags: CATEGORY_ADD_FLAGS,
|
||||
notes: ["Use categories to organize data-center files by business domain."],
|
||||
exampleArgs: ["--name product-docs --workspace-id ws-xxx", "--name sub --parent-id cate-xxx"],
|
||||
validate(flags) {
|
||||
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// categoryType fixed to UNSTRUCTURED (the only valid value for knowledge-base creation today)
|
||||
const body = {
|
||||
categoryName: flags.name,
|
||||
categoryType: "UNSTRUCTURED",
|
||||
...(flags.parentId ? { parentCategoryId: flags.parentId } : {}),
|
||||
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addCategory);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAddCategoryResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const categoryId = response.data?.categoryId;
|
||||
if (settings.quiet) {
|
||||
emitBare(categoryId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`created: ${categoryId ?? "-"} (${flags.name})`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagConnectorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CATEGORY_DELETE_FLAGS = {
|
||||
categoryId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Category ID to delete",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Delete a data-center category",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--category-id <id> [flags]",
|
||||
flags: CATEGORY_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Behavior for categories containing files or sub-categories is server-defined — the server error is passed through as-is.",
|
||||
],
|
||||
exampleArgs: ["--category-id cate-xxx --workspace-id ws-xxx", "--category-id cate-xxx --yes"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = { categoryId: flags.categoryId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteCategory);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
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>
|
||||
>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`deleted: ${flags.categoryId}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagListCategoryResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CATEGORY_LIST_FLAGS = {
|
||||
collectionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Filter by exact collection ID",
|
||||
},
|
||||
parentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "List sub-categories of this exact parent category",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by category name (exact match, unlike the knowledge base list)",
|
||||
},
|
||||
nextToken: {
|
||||
type: "string",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the next page (from previous output)",
|
||||
},
|
||||
maxResult: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Items per page (default: 20)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List data-center categories",
|
||||
auth: "apiKey",
|
||||
usageArgs: "[flags]",
|
||||
flags: CATEGORY_LIST_FLAGS,
|
||||
notes: [
|
||||
"Categories marked [default] are where files land when no category is specified.",
|
||||
"Pagination is cursor-based: reuse the printed next token to continue.",
|
||||
],
|
||||
exampleArgs: ["--workspace-id ws-xxx", "--name my-category", "--next-token <token>"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// type fixed to UNSTRUCTURED, not exposed as a flag (the only valid value today); note: maxResult is singular
|
||||
const body = {
|
||||
type: "UNSTRUCTURED",
|
||||
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
|
||||
...(flags.parentId ? { parentId: flags.parentId } : {}),
|
||||
...(flags.name ? { categoryName: flags.name } : {}),
|
||||
...(flags.nextToken ? { nextToken: flags.nextToken } : {}),
|
||||
...(flags.maxResult !== undefined ? { maxResult: flags.maxResult } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listCategory);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagListCategoryResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const categories = response.data?.categoryList ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const category of categories) emitBare(category.categoryId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
if (categories.length === 0) {
|
||||
emitBare("No categories found.");
|
||||
} else {
|
||||
for (const category of categories) {
|
||||
const defaultMark = category.isDefault ? " [default]" : "";
|
||||
emitBare(truncateLine(`${category.categoryId} ${category.categoryName}${defaultMark}`));
|
||||
}
|
||||
}
|
||||
const nextToken = response.data?.nextToken;
|
||||
if (nextToken) emitBare(`next: --next-token ${nextToken}`);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type KnowledgeChatStreamChunk,
|
||||
} from "bailian-cli-core";
|
||||
import { ansi, emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CHAT_FLAGS = {
|
||||
message: {
|
||||
@@ -27,11 +28,15 @@ const CHAT_FLAGS = {
|
||||
description: "Q&A service ID (find in console knowledge Q&A page)",
|
||||
required: true,
|
||||
},
|
||||
// 知识库走 workspace 专属域名,--workspace-id 属命令自有 flag(console 凭证域不适用)。
|
||||
workspaceId: {
|
||||
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
|
||||
// flag here (the console credential scope does not apply).
|
||||
...WORKSPACE_FLAG,
|
||||
// Named to avoid the runtime-reserved global --version flag
|
||||
agentVersion: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
|
||||
valueHint: "<version>",
|
||||
description:
|
||||
"Service version to call: beta (draft for debugging) or a published number; default is the latest published version",
|
||||
},
|
||||
image: {
|
||||
type: "array",
|
||||
@@ -146,6 +151,7 @@ export default defineCommand({
|
||||
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
|
||||
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
|
||||
'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.',
|
||||
"`--agent-version beta` calls the draft config for debugging before it is deployed.",
|
||||
],
|
||||
exampleArgs: [
|
||||
'--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
|
||||
@@ -168,14 +174,7 @@ export default defineCommand({
|
||||
messages = [{ role: "user", content: "" }];
|
||||
}
|
||||
|
||||
const workspaceId = flags.workspaceId || settings.workspaceId;
|
||||
if (!workspaceId) {
|
||||
throw new BailianError(
|
||||
"Workspace ID is required.",
|
||||
ExitCode.USAGE,
|
||||
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
|
||||
);
|
||||
}
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
// API only supports SSE; streamOutput controls whether to print tokens in real-time
|
||||
@@ -199,6 +198,9 @@ export default defineCommand({
|
||||
parameters: {
|
||||
agent_options: {
|
||||
agent_id: flags.agentId,
|
||||
// Omitted flag → field not sent (default behavior unchanged); the value is
|
||||
// not validated — the set of versions is server-side state
|
||||
...(flags.agentVersion ? { agent_version: flags.agentVersion } : {}),
|
||||
},
|
||||
},
|
||||
stream: true,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
import { readUtf8TextFile } from "./upload-support.ts";
|
||||
|
||||
const CHUNK_ADD_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
docId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description:
|
||||
"Owning document ID from the doc list command; required in practice for all knowledge base types",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Chunk body text, up to 6000 chars (document-type); alternative to --content-file",
|
||||
},
|
||||
contentFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: "Read chunk body from a UTF-8 plain text file (.md/.txt etc.)",
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Chunk title, up to 50 chars (document-type)",
|
||||
},
|
||||
imageUrl: {
|
||||
type: "array",
|
||||
valueHint: "<url>",
|
||||
description: "Chunk image URL (repeatable, up to 10; document-type)",
|
||||
},
|
||||
field: {
|
||||
type: "array",
|
||||
valueHint: "<key=value>",
|
||||
description:
|
||||
"Arbitrary field entry (repeatable) for table/image knowledge bases where keys are Excel column headers; mutually exclusive with content/title/image flags",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Parse --field key=value: split on the first =, value may contain = */
|
||||
export function parseFieldEntries(entries: string[]): Record<string, string> {
|
||||
const field: Record<string, string> = {};
|
||||
for (const entry of entries) {
|
||||
const separatorIndex = entry.indexOf("=");
|
||||
if (separatorIndex <= 0) {
|
||||
throw new BailianError(`--field must be key=value, got: ${entry}`, ExitCode.USAGE);
|
||||
}
|
||||
field[entry.slice(0, separatorIndex)] = entry.slice(separatorIndex + 1);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Add a chunk directly to a knowledge base",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> (--content <text> | --field <k=v>) [flags]",
|
||||
flags: CHUNK_ADD_FLAGS,
|
||||
notes: [
|
||||
"Document / table / image knowledge bases are supported; audio-video ones are not.",
|
||||
"--doc-id is required in practice for all knowledge base types. Use the document-level id from the doc list command; the per-row doc_id in chunk list output is not accepted.",
|
||||
"Image-type documents do not support text chunks. Target a text-type document (docx/pdf/txt) instead.",
|
||||
"The API is idempotent but rate-limited to 10 calls per second — throttle batch scripts.",
|
||||
"The response carries no chunk id; list chunks afterwards to find the new one.",
|
||||
"For table/image knowledge bases use --field with Excel column headers as keys; values are passed through as strings.",
|
||||
],
|
||||
exampleArgs: [
|
||||
'--index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx',
|
||||
"--index-id idx-xxx --field 列A=v1 --field 列B=v2",
|
||||
],
|
||||
validate(flags) {
|
||||
const hasConvenience =
|
||||
flags.content !== undefined ||
|
||||
flags.contentFile !== undefined ||
|
||||
flags.title !== undefined ||
|
||||
!!flags.imageUrl?.length;
|
||||
const hasField = !!flags.field?.length;
|
||||
if (hasConvenience && hasField) {
|
||||
return "--field is mutually exclusive with --content/--content-file/--title/--image-url";
|
||||
}
|
||||
if (!hasConvenience && !hasField) {
|
||||
return "Provide chunk content via --content/--content-file or --field entries";
|
||||
}
|
||||
if (flags.content !== undefined && flags.contentFile !== undefined) {
|
||||
return "Use either --content or --content-file, not both";
|
||||
}
|
||||
if (flags.content !== undefined && flags.content.length > 6000) {
|
||||
return "--content must be at most 6000 characters";
|
||||
}
|
||||
if (flags.title !== undefined && flags.title.length > 50) {
|
||||
return "--title must be at most 50 characters";
|
||||
}
|
||||
if (flags.imageUrl !== undefined && flags.imageUrl.length > 10) {
|
||||
return "--image-url accepts at most 10 entries";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// dry-run also reads the file and parses --field (rehearsal semantics)
|
||||
let field: Record<string, unknown>;
|
||||
if (flags.field?.length) {
|
||||
field = parseFieldEntries(flags.field);
|
||||
} else {
|
||||
const content =
|
||||
flags.contentFile !== undefined
|
||||
? readUtf8TextFile(flags.contentFile, "--content")
|
||||
: flags.content;
|
||||
if (typeof content === "string" && content.length > 6000) {
|
||||
throw new BailianError("Chunk content must be at most 6000 characters", ExitCode.USAGE);
|
||||
}
|
||||
field = {
|
||||
...(content !== undefined ? { content } : {}),
|
||||
...(flags.title !== undefined ? { title: flags.title } : {}),
|
||||
...(flags.imageUrl?.length ? { image_urls: flags.imageUrl } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const body = {
|
||||
pipelineId: flags.indexId,
|
||||
...(flags.docId ? { dataId: flags.docId } : {}),
|
||||
field,
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkCreate);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
// The response carries no chunk_id — quiet mode exits 0 silently on success
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`chunk created (pipeline: ${flags.indexId})`);
|
||||
emitBare("List chunks to find the new chunk id.");
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
type FlagsDef,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CHUNK_DELETE_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
chunkId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: "Chunk ID to delete (repeatable; batches of 10 are sent automatically)",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** The server caps each request at 10 chunk ids — the client batches automatically (bulk delete is where the CLI beats the console) */
|
||||
export function splitIntoBatches(chunkIds: string[], batchSize = 10): string[][] {
|
||||
const batches: string[][] = [];
|
||||
for (let batchStart = 0; batchStart < chunkIds.length; batchStart += batchSize) {
|
||||
batches.push(chunkIds.slice(batchStart, batchStart + batchSize));
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Delete chunks from a knowledge base (irreversible)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
|
||||
flags: CHUNK_DELETE_FLAGS,
|
||||
notes: ["Accepts at most 10 chunk ids per call; larger sets are batched automatically."],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --chunk-id chunk-a --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const batches = splitIntoBatches(flags.chunkId);
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkDelete);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint,
|
||||
batches: batches.map((batchIds) => ({
|
||||
request: { pipelineId: flags.indexId, chunkIds: batchIds },
|
||||
})),
|
||||
},
|
||||
format,
|
||||
);
|
||||
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) {
|
||||
try {
|
||||
await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body: { pipelineId: flags.indexId, chunkIds: batchIds },
|
||||
});
|
||||
deletedCount += batchIds.length;
|
||||
} catch (error) {
|
||||
if (deletedCount > 0 && error instanceof BailianError && !error.hint) {
|
||||
throw new BailianError(
|
||||
error.message,
|
||||
error.exitCode,
|
||||
`${deletedCount} chunk(s) in earlier batches were already deleted.`,
|
||||
{ cause: error, api: error.api, rawResponse: error.rawResponse },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`deleted: ${deletedCount} chunk(s) in ${batches.length} batch(es)`);
|
||||
return;
|
||||
}
|
||||
emitResult({ deleted_count: deletedCount, batches: batches.length }, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagChunkListResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const CHUNK_LIST_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
docId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Only show chunks belonging to this document",
|
||||
},
|
||||
...PAGE_FLAGS,
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List chunks in a knowledge base with content and status",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: CHUNK_LIST_FLAGS,
|
||||
notes: [
|
||||
"Use metadata._id as the chunk id and metadata.doc_id as the document id in chunk update/delete commands.",
|
||||
"Page size defaults to 20 (server default), max 100.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --doc-id file-xxx --page-size 50",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
|
||||
return "--page-size must be between 1 and 100";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Gotcha: this endpoint's pagination keys are pageNum/pageSize (camelCase, in the body)
|
||||
const body = {
|
||||
indexId: flags.indexId,
|
||||
pageNum: flags.pageNumber ?? 1,
|
||||
pageSize: flags.pageSize ?? 20,
|
||||
...(flags.docId ? { docId: flags.docId } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkList);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagChunkListResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const nodes = response.data?.nodes ?? [];
|
||||
if (settings.quiet) {
|
||||
// chunk ids only, for piping into chunk update/delete
|
||||
for (const node of nodes) emitBare(node.metadata?._id ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
if (nodes.length === 0) {
|
||||
emitBare("No chunks found.");
|
||||
} else {
|
||||
for (const node of nodes) {
|
||||
const metadata = node.metadata ?? {};
|
||||
const statusPart = metadata._chunk_status_message
|
||||
? ` status: ${metadata._chunk_status_message}`
|
||||
: "";
|
||||
const excludedPart =
|
||||
metadata.is_displayed_chunk_content === false ? " [excluded from retrieval]" : "";
|
||||
emitBare(
|
||||
`[chunk] ${metadata._id ?? "?"} (doc: ${metadata.doc_name ?? "?"}, doc_id: ${metadata.doc_id ?? "?"})${statusPart}${excludedPart}`,
|
||||
);
|
||||
const contentText = metadata.content ?? node.text ?? "";
|
||||
emitBare(` ${contentText.length > 200 ? `${contentText.slice(0, 200)}…` : contentText}`);
|
||||
}
|
||||
}
|
||||
emitBare(`total: ${response.data?.total ?? nodes.length}`);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagChunkListResponse,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
import { readUtf8TextFile } from "./upload-support.ts";
|
||||
|
||||
const CHUNK_UPDATE_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
chunkId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Chunk ID (metadata._id from the chunk list output)",
|
||||
required: true,
|
||||
},
|
||||
docId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Document ID owning the chunk (metadata.doc_id from the chunk list output)",
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "New chunk content, 10-6000 chars; alternative to --content-file",
|
||||
},
|
||||
contentFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: "Read new content from a UTF-8 plain text file (.md/.txt etc.)",
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Chunk title, 0-50 chars (empty string clears it; omit to keep unchanged)",
|
||||
},
|
||||
exclude: { type: "switch", description: "Exclude this chunk from retrieval" },
|
||||
include: { type: "switch", description: "Include this chunk in retrieval (default)" },
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** When only toggling include/exclude, read back the current content first (the API requires content — hide that quirk from users) */
|
||||
async function fetchChunkContent(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
indexId: string,
|
||||
chunkId: string,
|
||||
docId: string,
|
||||
): Promise<string> {
|
||||
const maxPages = 10;
|
||||
for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
|
||||
const response = await client.requestJson<RagChunkListResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.chunkList),
|
||||
method: "POST",
|
||||
body: { indexId, docId, pageNum, pageSize: 100 },
|
||||
});
|
||||
const nodes = response.data?.nodes ?? [];
|
||||
const match = nodes.find((node) => node.metadata?._id === chunkId);
|
||||
const matchContent = match?.metadata?.content ?? match?.text;
|
||||
if (typeof matchContent === "string") return matchContent;
|
||||
if (nodes.length < 100) break;
|
||||
}
|
||||
throw new BailianError(
|
||||
`Chunk not found: ${chunkId}`,
|
||||
ExitCode.GENERAL,
|
||||
"Check the chunk id via the chunk list command.",
|
||||
);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Update chunk content or toggle its retrieval visibility",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> --chunk-id <id> --doc-id <id> [flags]",
|
||||
flags: CHUNK_UPDATE_FLAGS,
|
||||
notes: [
|
||||
"Content must be 10-6000 characters and within the knowledge base's max chunk size.",
|
||||
"--content-file expects a UTF-8 plain text file; document formats (.docx/.pdf) are not parsed here.",
|
||||
"Toggling --exclude/--include without new content re-submits the existing content automatically.",
|
||||
],
|
||||
exampleArgs: [
|
||||
'--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text"',
|
||||
"--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.content !== undefined && flags.contentFile !== undefined) {
|
||||
return "Use either --content or --content-file, not both";
|
||||
}
|
||||
if (flags.exclude && flags.include) return "--exclude and --include are mutually exclusive";
|
||||
const hasContent = flags.content !== undefined || flags.contentFile !== undefined;
|
||||
if (!hasContent && !flags.exclude && !flags.include && flags.title === undefined) {
|
||||
return "Nothing to update — pass --content/--content-file, --title, --exclude or --include";
|
||||
}
|
||||
// Content lower-bound is enforced here (not deferred to run) so dry-run and
|
||||
// missing-flag diagnostics surface the same error as the live request.
|
||||
if (flags.content !== undefined && (flags.content.length < 10 || flags.content.length > 6000)) {
|
||||
return "--content must be 10-6000 characters";
|
||||
}
|
||||
if (flags.title !== undefined && flags.title.length > 50) {
|
||||
return "--title must be at most 50 characters";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// dry-run also reads the file and validates (rehearsal semantics); the read-back
|
||||
// request is only made outside dry-run and when no new content is given
|
||||
let content =
|
||||
flags.contentFile !== undefined
|
||||
? readUtf8TextFile(flags.contentFile, "--content")
|
||||
: flags.content;
|
||||
if (content !== undefined && (content.length < 10 || content.length > 6000)) {
|
||||
throw new BailianError("Chunk content must be 10-6000 characters", ExitCode.USAGE);
|
||||
}
|
||||
|
||||
if (content === undefined) {
|
||||
if (settings.dryRun) {
|
||||
content = "<current-content (fetched at run time)>";
|
||||
} else {
|
||||
content = await fetchChunkContent(
|
||||
ctx.client,
|
||||
workspaceId,
|
||||
flags.indexId,
|
||||
flags.chunkId,
|
||||
flags.docId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
pipelineId: flags.indexId,
|
||||
chunkId: flags.chunkId,
|
||||
dataId: flags.docId,
|
||||
content,
|
||||
// Without exclude/include the chunk stays retrievable (safe default)
|
||||
isDisplayedChunkContent: !flags.exclude,
|
||||
...(flags.title !== undefined ? { title: flags.title } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkUpdate);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`updated: ${flags.chunkId}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagAddConnectorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const COLLECTION_CREATE_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Collection name",
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Collection description (required by the server)",
|
||||
required: true,
|
||||
},
|
||||
storeType: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: "Storage: platform (managed) or custom (your own OSS bucket)",
|
||||
},
|
||||
ossRegion: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "OSS region id (required with --store-type custom)",
|
||||
},
|
||||
ossBucket: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "OSS bucket name (required with --store-type custom)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Create a FILE data collection",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--name <text> --description <text> [flags]",
|
||||
flags: COLLECTION_CREATE_FLAGS,
|
||||
notes: [
|
||||
"Store type defaults to platform (managed storage); custom uses your authorized OSS bucket.",
|
||||
"Custom buckets must carry the bucket tag bailian-connector-access=ReadAndWrite (Bailian's tag-based access control); without it the server rejects creation with a misleading 'setBucketCORS failed' error.",
|
||||
"There is no collection delete API — create collections deliberately.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--name my-collection --description 'team docs' --workspace-id ws-xxx",
|
||||
"--name oss-coll --description 'own bucket' --store-type custom --oss-region cn-beijing --oss-bucket my-bucket",
|
||||
],
|
||||
validate(flags) {
|
||||
// Server rejects names longer than 20 characters ("Connector name is longer than 20")
|
||||
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
|
||||
const storeType = flags.storeType ?? "platform";
|
||||
if (storeType !== "platform" && storeType !== "custom") {
|
||||
return "--store-type must be platform or custom";
|
||||
}
|
||||
if (storeType === "custom" && (!flags.ossRegion || !flags.ossBucket)) {
|
||||
return "--store-type custom requires --oss-region and --oss-bucket";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const storeType = (flags.storeType ?? "platform").toUpperCase();
|
||||
// The server contract still uses connector* fields; only the CLI-facing term is collection.
|
||||
// CUSTOM fields are regionId/bucketName per api/connector/add-connector.md (live-verified;
|
||||
// the earlier ossRegionId/ossBucket naming was an implementation error, rejected with InvalidParameter).
|
||||
const body = {
|
||||
connectorType: "FILE",
|
||||
connectorName: flags.name,
|
||||
description: flags.description,
|
||||
fileConnectorConfig: {
|
||||
storeType,
|
||||
...(storeType === "CUSTOM"
|
||||
? { regionId: flags.ossRegion, bucketName: flags.ossBucket }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addConnector);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAddConnectorResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const collectionId = response.data?.connectorId;
|
||||
if (settings.quiet) {
|
||||
emitBare(collectionId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`created: ${collectionId ?? "-"} (${flags.name}, ${storeType})`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagGetConnectorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const COLLECTION_GET_FLAGS = {
|
||||
collectionId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Collection ID; alternative to --name",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Collection name; alternative to --collection-id",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show data collection details",
|
||||
auth: "apiKey",
|
||||
usageArgs: "(--collection-id <id> | --name <text>) [flags]",
|
||||
flags: COLLECTION_GET_FLAGS,
|
||||
exampleArgs: ["--collection-id conn-xxx --workspace-id ws-xxx", "--name my-collection"],
|
||||
validate(flags) {
|
||||
if (!flags.collectionId && !flags.name) return "Pass --collection-id or --name";
|
||||
if (flags.collectionId && flags.name) return "Use either --collection-id or --name, not both";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// The server contract still uses connector* fields; only the CLI-facing term is collection
|
||||
const body = {
|
||||
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
|
||||
...(flags.name ? { connectorName: flags.name } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.getConnector);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagGetConnectorResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const collection = response.data;
|
||||
if (settings.quiet) {
|
||||
emitBare(collection?.connectorId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`id: ${collection?.connectorId ?? "-"}`);
|
||||
emitBare(`name: ${collection?.connectorName ?? "-"}`);
|
||||
emitBare(`description: ${collection?.description ?? "-"}`);
|
||||
// getConnector does not return fileConnectorConfig (storeType/regionId/bucketName);
|
||||
// these fields are only available on the create request body.
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagDeleteFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_DELETE_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
docId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: "Document ID to delete (repeatable)",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...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: "Delete documents and their chunks from a knowledge base",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> --doc-id <id> [flags]",
|
||||
flags: DOC_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Removes documents from the knowledge base index only; the source files remain in the data center.",
|
||||
"Use the doc_id from `knowledge doc list --quiet`, not the fileId from `knowledge doc upload`. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.",
|
||||
"Deletion may take up to ~30s to propagate — the document may still appear in the doc list briefly.",
|
||||
"The output lists the ids actually deleted.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// snake_case: body { index_id, doc_ids }
|
||||
const body = { index_id: flags.indexId, doc_ids: flags.docId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDeleteFile);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
buildDeleteSummary(flags.indexId, flags.docId),
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await ctx.client.requestJson<RagDeleteFileResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
// Output follows the server's data.deleted list
|
||||
const deleted = response.data?.deleted ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const docId of deleted) emitBare(docId);
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`deleted: ${deleted.length} document(s)`);
|
||||
for (const docId of deleted) emitBare(` ${docId}`);
|
||||
if (deleted.length !== flags.docId.length) {
|
||||
process.stderr.write(
|
||||
`Warning: requested ${flags.docId.length} deletion(s) but the server reported ${deleted.length}.\n`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { basename } from "node:path";
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagOssImportResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_IMPORT_OSS_FLAGS = {
|
||||
bucket: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Authorized OSS bucket name",
|
||||
required: true,
|
||||
},
|
||||
region: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "OSS region id (e.g. cn-beijing)",
|
||||
required: true,
|
||||
},
|
||||
ossKey: {
|
||||
type: "array",
|
||||
valueHint: "<key>",
|
||||
description: "OSS object key to import (repeatable, 1-10 per call)",
|
||||
required: true,
|
||||
},
|
||||
categoryId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Target data-center category (default: the default category)",
|
||||
},
|
||||
tag: {
|
||||
type: "array",
|
||||
valueHint: "<text>",
|
||||
description: "File tag applied to every imported file (repeatable, up to 10)",
|
||||
},
|
||||
overwrite: {
|
||||
type: "switch",
|
||||
description: "Overwrite files previously imported from the same OSS keys",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Batch import files from an authorized OSS bucket into the data center",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--bucket <name> --region <id> --oss-key <key> [flags]",
|
||||
flags: DOC_IMPORT_OSS_FLAGS,
|
||||
notes: [
|
||||
"The bucket must be authorized to the platform service role beforehand; permission errors from the server are passed through with a pointer to check AliyunServiceRoleForBailian in the RAM console.",
|
||||
"File names are derived from the OSS key basename.",
|
||||
"--overwrite replaces the previously imported file and issues a NEW fileId (the old one becomes invalid) — verified live.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx",
|
||||
"--bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.ossKey.length > 10) return "--oss-key accepts at most 10 entries per call";
|
||||
if (flags.tag !== undefined && flags.tag.length > 10) {
|
||||
return "--tag accepts at most 10 entries";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// categoryType fixed to UNSTRUCTURED; parser not exposed as a flag (defaults to AUTO_SELECT)
|
||||
const body = {
|
||||
categoryId: flags.categoryId ?? "default",
|
||||
categoryType: "UNSTRUCTURED",
|
||||
ossBucket: flags.bucket,
|
||||
ossRegionId: flags.region,
|
||||
fileDetails: flags.ossKey.map((ossKey) => ({ fileName: basename(ossKey), ossKey })),
|
||||
...(flags.tag?.length ? { tags: flags.tag } : {}),
|
||||
...(flags.overwrite ? { overWriteFileByOssKey: true } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addFilesFromAuthorizedOss);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagOssImportResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
// Live-verified shape: results come back as addFileResultList (the docs' flat
|
||||
// fileIds field is not returned); per-file status is SUCCESS on success
|
||||
const results = response.data?.addFileResultList ?? [];
|
||||
const fileIds = results
|
||||
.map((result) => result.fileId)
|
||||
.filter((fileId): fileId is string => !!fileId);
|
||||
if (settings.quiet) {
|
||||
for (const fileId of fileIds) emitBare(fileId);
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`imported: ${fileIds.length} file(s)`);
|
||||
for (const result of results) {
|
||||
emitBare(` ${result.fileId ?? "-"} ${result.status ?? "-"} ${result.ossKey ?? ""}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagIndexFilesResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, ansi } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_LIST_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
...PAGE_FLAGS,
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List documents in a knowledge base with parse/index status",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: DOC_LIST_FLAGS,
|
||||
notes: [
|
||||
"Documents with status FAILED are highlighted in text mode — use the import job status command to inspect failures.",
|
||||
"Page size defaults to 10 (server default), max 100.",
|
||||
],
|
||||
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx", "--index-id idx-xxx --page-size 100"],
|
||||
validate(flags) {
|
||||
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
|
||||
return "--page-size must be between 1 and 100";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Gotcha: this endpoint's page parameter is page_num (not page_number)
|
||||
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
|
||||
url.searchParams.set("index_id", flags.indexId);
|
||||
url.searchParams.set("page_num", String(flags.pageNumber ?? 1));
|
||||
url.searchParams.set("page_size", String(flags.pageSize ?? 10));
|
||||
const endpoint = url.toString();
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagIndexFilesResponse>({
|
||||
path: endpoint,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const rows = response.data?.rows ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const row of rows) emitBare(row.doc_id ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
const styles = ansi(process.stdout);
|
||||
if (rows.length === 0) {
|
||||
emitBare("No documents found.");
|
||||
} else {
|
||||
for (const row of rows) {
|
||||
const line = truncateLine(
|
||||
[row.doc_id, row.status, row.doc_name, row.doc_type ?? "-", row.size ?? "-"].join(" "),
|
||||
);
|
||||
emitBare(row.status === "FAILED" ? styles.red(line) : line);
|
||||
}
|
||||
}
|
||||
emitBare(`total: ${response.data?.total_count ?? rows.length}`);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type RagIndexJobStatusResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, ansi } from "bailian-cli-runtime";
|
||||
import {
|
||||
resolveWorkspaceId,
|
||||
PAGE_FLAGS,
|
||||
WORKSPACE_FLAG,
|
||||
failedImportDocs,
|
||||
importJobFailureMessage,
|
||||
pollImportJob,
|
||||
} from "./shared.ts";
|
||||
|
||||
const DOC_STATUS_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
jobId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Import job ID (ingestionId returned by import commands)",
|
||||
required: true,
|
||||
},
|
||||
...PAGE_FLAGS,
|
||||
wait: { type: "switch", description: "Poll until the job reaches a terminal state" },
|
||||
pollInterval: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: "Polling interval when waiting (default: 5)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function printStatus(response: RagIndexJobStatusResponse): void {
|
||||
const styles = ansi(process.stdout);
|
||||
emitBare(`status: ${response.data?.ingestion_status ?? "UNKNOWN"}`);
|
||||
for (const doc of response.data?.rows ?? []) {
|
||||
const docState = doc.code ?? doc.status ?? "?";
|
||||
const line = ` ${doc.doc_id ?? "?"} ${docState} ${doc.doc_name ?? ""}`;
|
||||
emitBare(docState.includes("FAILED") ? styles.red(line) : line);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Check knowledge base import job status",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> --job-id <id> [flags]",
|
||||
flags: DOC_STATUS_FLAGS,
|
||||
notes: [
|
||||
"Both --index-id and --job-id are required (passing only one returns SystemError).",
|
||||
"If you see a SystemError, the job may not exist — check the ingestion id in the document list output.",
|
||||
"Overall job states are PENDING / RUNNING / COMPLETED; per-document failures (for example PARSE_FAILED) exit non-zero with the error message passed through.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --job-id job-xxx --wait --poll-interval 10",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Both required flags are enforced by the parser up front; parameters go in
|
||||
// the query string (they are ignored in the body)
|
||||
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexJobStatus));
|
||||
url.searchParams.set("index_id", flags.indexId);
|
||||
url.searchParams.set("job_id", flags.jobId);
|
||||
if (flags.pageNumber !== undefined) {
|
||||
url.searchParams.set("page_number", String(flags.pageNumber));
|
||||
}
|
||||
if (flags.pageSize !== undefined) {
|
||||
url.searchParams.set("page_size", String(flags.pageSize));
|
||||
}
|
||||
const endpoint = url.toString();
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
let response: RagIndexJobStatusResponse;
|
||||
if (flags.wait) {
|
||||
// Reuse the shared polling (timeout → TIMEOUT(5)); failure detection happens
|
||||
// uniformly after return, based on per-document status
|
||||
response = await pollImportJob(ctx.client, settings, {
|
||||
statusUrl: endpoint,
|
||||
intervalSec: flags.pollInterval ?? 5,
|
||||
});
|
||||
} else {
|
||||
response = await ctx.client.requestJson<RagIndexJobStatusResponse>({
|
||||
path: endpoint,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// Any per-document failure means a non-zero exit; the server message is passed through verbatim
|
||||
if (failedImportDocs(response).length > 0) {
|
||||
throw new BailianError(
|
||||
importJobFailureMessage(response, "Import job reported document failures."),
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(response.data?.ingestion_status ?? "UNKNOWN");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
printStatus(response);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagBatchUpdateTagResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const DOC_TAG_FLAGS = {
|
||||
docId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: "Data-center file ID to tag (repeatable, 1-20 per call)",
|
||||
required: true,
|
||||
},
|
||||
tag: {
|
||||
type: "array",
|
||||
valueHint: "<text>",
|
||||
description: "Tag applied to every --doc-id (repeatable, each up to 32 chars)",
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
valueHint: "<mode>",
|
||||
description: "Update mode: append (default) or overwrite",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Batch update tags on data-center files",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--doc-id <id> --tag <text> [flags]",
|
||||
flags: DOC_TAG_FLAGS,
|
||||
notes: [
|
||||
"The same tag set is applied to every --doc-id; run the command multiple times for different tag sets.",
|
||||
"Server limits: up to 100 tags per file, total tag length up to 700 chars, tag up to 32 chars.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx",
|
||||
"--doc-id file-a --doc-id file-b --tag final --mode overwrite",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.docId.length > 20) return "--doc-id accepts at most 20 ids per call";
|
||||
if (flags.mode !== undefined && flags.mode !== "append" && flags.mode !== "overwrite") {
|
||||
return "--mode must be append or overwrite";
|
||||
}
|
||||
// Hard limits stated by the API contract: each tag ≤32 chars; ≤100 tags per file; total length ≤700
|
||||
if (flags.tag.length > 100) return "At most 100 tags per file";
|
||||
const overlongTag = flags.tag.find((tag) => tag.length > 32);
|
||||
if (overlongTag) return `Tag exceeds 32 characters: ${overlongTag}`;
|
||||
const totalLength = flags.tag.reduce((sum, tag) => sum + tag.length, 0);
|
||||
if (totalLength > 700) return "Total tag length exceeds 700 characters";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = {
|
||||
fileInfos: flags.docId.map((fileId) => ({ fileId, tags: flags.tag })),
|
||||
updateMode: (flags.mode ?? "append").toUpperCase(),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.batchUpdateFileTag);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagBatchUpdateTagResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`tagged: ${flags.docId.length} file(s) with [${flags.tag.join(", ")}]`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,322 @@
|
||||
// Orchestration command: local file → data center → (optional) import into a knowledge base.
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type RagUploadLeaseResponse,
|
||||
type RagAddFileResponse,
|
||||
type RagJobCreateResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import {
|
||||
resolveWorkspaceId,
|
||||
WORKSPACE_FLAG,
|
||||
failedImportDocs,
|
||||
importJobFailureMessage,
|
||||
importJobStatus,
|
||||
importJobStatusUrl,
|
||||
pollImportJob,
|
||||
withPartialSuccessHint,
|
||||
} from "./shared.ts";
|
||||
import { checkUploadFile, expandUploadPaths } from "./upload-support.ts";
|
||||
|
||||
const DOC_UPLOAD_FLAGS = {
|
||||
file: {
|
||||
type: "array",
|
||||
valueHint: "<path>",
|
||||
description:
|
||||
"Local file or directory path (repeatable). Directories are scanned recursively; unsupported formats are skipped",
|
||||
required: true,
|
||||
},
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Import into this knowledge base after registration (one job for all files)",
|
||||
},
|
||||
categoryId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Target data-center category; defaults to the workspace default category",
|
||||
},
|
||||
tag: {
|
||||
type: "array",
|
||||
valueHint: "<text>",
|
||||
description: "File tag (repeatable), applied to every uploaded file",
|
||||
},
|
||||
wait: {
|
||||
type: "switch",
|
||||
description: "Poll the import job to a terminal state (needs --index-id)",
|
||||
},
|
||||
pollInterval: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: "Polling interval when waiting (default: 5)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
interface UploadedFile {
|
||||
path: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description:
|
||||
"Upload local files or directories to the data center and optionally import into a knowledge base",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file <path> [flags]",
|
||||
flags: DOC_UPLOAD_FLAGS,
|
||||
notes: [
|
||||
"Pipeline: apply upload lease → PUT to OSS → register file → (with --index-id) create import job.",
|
||||
"Without --category-id the workspace default category is resolved automatically.",
|
||||
"Directories are scanned recursively; node_modules, .git, and similar are skipped automatically.",
|
||||
"Multiple files are processed sequentially; on failure, already-registered file ids are listed in the error hint.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--file ./a.md --workspace-id ws-xxx",
|
||||
"--file ./a.md --file ./b.pdf --index-id idx-xxx --wait",
|
||||
"--file ./docs/ --workspace-id ws-xxx",
|
||||
"--file ./docs/ --dry-run --verbose",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.wait && !flags.indexId) return "--wait requires --index-id";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Expand directories into individual file paths; unsupported extensions are
|
||||
// collected into `skipped` rather than throwing (directory-scan semantics)
|
||||
const { files: expandedFiles, skipped } = expandUploadPaths(flags.file);
|
||||
if (expandedFiles.length === 0) {
|
||||
throw new BailianError(
|
||||
"No supported files found",
|
||||
ExitCode.USAGE,
|
||||
`Supported formats: .pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`,
|
||||
);
|
||||
}
|
||||
|
||||
// Local pre-flight validation also runs in dry-run (rehearsal semantics: surface
|
||||
// file problems early); exceeding a soft limit only warns
|
||||
const checkedFiles = expandedFiles.map((filePath) => {
|
||||
const checked = checkUploadFile(filePath);
|
||||
if (checked.warning) process.stderr.write(`Warning: ${checked.warning}\n`);
|
||||
return { filePath, sizeBytes: checked.sizeBytes };
|
||||
});
|
||||
|
||||
if (settings.dryRun) {
|
||||
// dry-run does not read file contents (md5 shown as a placeholder)
|
||||
const categoryPlaceholder = flags.categoryId ?? "default";
|
||||
const steps = checkedFiles.flatMap((checkedFile) => [
|
||||
{
|
||||
step: "applyFileUploadLease",
|
||||
endpoint: ragEndpoint(workspaceId, RAG_PATHS.applyFileUploadLease),
|
||||
request: {
|
||||
category: categoryPlaceholder,
|
||||
fileName: basename(checkedFile.filePath),
|
||||
sizeBytes: String(checkedFile.sizeBytes), // gotcha: must be a string
|
||||
contentMd5: "<md5-base64>",
|
||||
} as unknown,
|
||||
},
|
||||
{
|
||||
step: "ossPut",
|
||||
endpoint: "<lease.param.url>",
|
||||
request: { method: "PUT", headers: "<lease.param.headers>" } as unknown,
|
||||
},
|
||||
{
|
||||
step: "addFile",
|
||||
endpoint: ragEndpoint(workspaceId, RAG_PATHS.addFile),
|
||||
request: {
|
||||
leaseId: "<leaseId>",
|
||||
category: categoryPlaceholder,
|
||||
parser: "AUTO_SELECT",
|
||||
...(flags.tag?.length ? { tags: flags.tag } : {}),
|
||||
} as unknown,
|
||||
},
|
||||
]);
|
||||
if (flags.indexId) {
|
||||
steps.push({
|
||||
step: "createImportJob",
|
||||
endpoint: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate),
|
||||
request: {
|
||||
indexId: flags.indexId,
|
||||
// Live-verified: the field name is docIds (not documentIds as in the
|
||||
// public docs); omitting sourceType would import the entire data center.
|
||||
sourceType: "DATA_CENTER_FILE",
|
||||
docIds: ["<fileId>"],
|
||||
} as unknown,
|
||||
});
|
||||
}
|
||||
emitResult({ steps, skipped }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default category: the literal "default" is accepted by lease/addFile
|
||||
// (verified against the live API), so no listCategory resolution is needed
|
||||
const categoryId = flags.categoryId ?? "default";
|
||||
|
||||
// Multiple files run steps 1-3 sequentially (no concurrency in this version,
|
||||
// to avoid OSS rate-limit complexity)
|
||||
const uploaded: UploadedFile[] = [];
|
||||
for (const checkedFile of checkedFiles) {
|
||||
try {
|
||||
const fileBuffer = readFileSync(checkedFile.filePath);
|
||||
const contentMd5 = createHash("md5").update(fileBuffer).digest("base64");
|
||||
|
||||
// 1) Apply for an upload lease (gotcha: the category parameter is named
|
||||
// category, not categoryId; sizeBytes must be a string)
|
||||
const lease = await ctx.client.requestJson<RagUploadLeaseResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.applyFileUploadLease),
|
||||
method: "POST",
|
||||
body: {
|
||||
category: categoryId,
|
||||
fileName: basename(checkedFile.filePath),
|
||||
sizeBytes: String(checkedFile.sizeBytes),
|
||||
contentMd5,
|
||||
},
|
||||
});
|
||||
const leaseId = lease.data?.leaseId;
|
||||
const leaseParam = lease.data?.param;
|
||||
if (!leaseId || !leaseParam?.url) {
|
||||
throw new BailianError(
|
||||
`Upload lease response missing leaseId/url for ${checkedFile.filePath}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
// 2) OSS upload: goes to the OSS host, not the DashScope gateway — native fetch without a Bearer header
|
||||
let ossResponse: Response;
|
||||
try {
|
||||
ossResponse = await fetch(leaseParam.url, {
|
||||
method: leaseParam.method ?? "PUT",
|
||||
headers: leaseParam.headers,
|
||||
body: fileBuffer,
|
||||
});
|
||||
} catch (error) {
|
||||
const causeCode = (error as { cause?: { code?: string } }).cause?.code;
|
||||
throw new BailianError(
|
||||
`OSS upload failed for ${basename(checkedFile.filePath)}`,
|
||||
ExitCode.NETWORK,
|
||||
causeCode ? `Network error (${causeCode}).` : undefined,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
if (!ossResponse.ok) {
|
||||
const ossBody = await ossResponse.text().catch(() => "");
|
||||
throw new BailianError(
|
||||
`OSS upload rejected (HTTP ${ossResponse.status}) for ${basename(checkedFile.filePath)}${ossBody ? `: ${ossBody.slice(0, 300)}` : ""}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
// 3) Register the file
|
||||
const added = await ctx.client.requestJson<RagAddFileResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.addFile),
|
||||
method: "POST",
|
||||
body: {
|
||||
leaseId,
|
||||
category: categoryId,
|
||||
parser: "AUTO_SELECT",
|
||||
...(flags.tag?.length ? { tags: flags.tag } : {}),
|
||||
},
|
||||
});
|
||||
const fileId = added.data?.fileId;
|
||||
if (!fileId) {
|
||||
throw new BailianError(
|
||||
`addFile response missing fileId for ${checkedFile.filePath}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
uploaded.push({ path: checkedFile.filePath, fileId });
|
||||
} catch (error) {
|
||||
// Partial-failure semantics: abort with an error, listing already-registered
|
||||
// fileIds in the hint (re-uploading is cheap and idempotent)
|
||||
if (uploaded.length > 0) {
|
||||
throw withPartialSuccessHint(
|
||||
error,
|
||||
`Already registered: ${uploaded.map((item) => item.fileId).join(", ")}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Optional import (merged into a single job after all files are registered)
|
||||
let ingestionId: string | undefined;
|
||||
let finalStatus: string | undefined;
|
||||
if (flags.indexId) {
|
||||
const job = await ctx.client.requestJson<RagJobCreateResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate),
|
||||
method: "POST",
|
||||
body: {
|
||||
indexId: flags.indexId,
|
||||
// Live-verified: the field name is docIds (not documentIds as in the
|
||||
// public docs); omitting sourceType would import the entire data center.
|
||||
sourceType: "DATA_CENTER_FILE",
|
||||
docIds: uploaded.map((item) => item.fileId),
|
||||
},
|
||||
});
|
||||
ingestionId = job.data?.ingestionId;
|
||||
if (flags.wait && ingestionId) {
|
||||
const statusResponse = await pollImportJob(ctx.client, settings, {
|
||||
statusUrl: importJobStatusUrl(workspaceId, flags.indexId, ingestionId).toString(),
|
||||
intervalSec: flags.pollInterval ?? 5,
|
||||
});
|
||||
finalStatus = importJobStatus(statusResponse);
|
||||
// Job finished but some documents failed to parse → non-zero exit, server message passed through verbatim
|
||||
if (failedImportDocs(statusResponse).length > 0) {
|
||||
throw new BailianError(
|
||||
importJobFailureMessage(statusResponse, "Import job reported document failures."),
|
||||
ExitCode.GENERAL,
|
||||
`Registered file ids: ${uploaded.map((item) => item.fileId).join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.quiet) {
|
||||
for (const item of uploaded) emitBare(item.fileId);
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
for (const item of uploaded) {
|
||||
emitBare(`${basename(item.path)} ${item.fileId} registered`);
|
||||
}
|
||||
if (ingestionId) emitBare(`job: ${ingestionId}`);
|
||||
if (finalStatus) emitBare(`status: ${finalStatus}`);
|
||||
// Summary line: always show counts; list skipped files only with --verbose
|
||||
const summaryParts = [`Uploaded ${uploaded.length} file${uploaded.length !== 1 ? "s" : ""}`];
|
||||
if (skipped.length > 0) {
|
||||
summaryParts.push(`skipped ${skipped.length} unsupported`);
|
||||
}
|
||||
emitBare(`\n${summaryParts.join(", ")}.`);
|
||||
if (settings.verbose && skipped.length > 0) {
|
||||
emitBare("Skipped files:");
|
||||
for (const skippedPath of skipped) {
|
||||
emitBare(` ${basename(skippedPath)}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// An orchestration command has no single response to pass through — emit a custom stable shape
|
||||
emitResult(
|
||||
{
|
||||
files: uploaded.map((item) => ({ path: item.path, fileId: item.fileId })),
|
||||
skipped,
|
||||
...(flags.indexId ? { index_id: flags.indexId } : {}),
|
||||
...(ingestionId ? { ingestion_id: ingestionId } : {}),
|
||||
...(finalStatus ? { final_status: finalStatus } : {}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
defineCommand,
|
||||
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 { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const FILE_DELETE_FLAGS = {
|
||||
fileId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Data-center file ID to delete",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...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: "Permanently delete a file from the data center",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id> [flags]",
|
||||
flags: FILE_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Irreversible. If knowledge bases reference this file, their related document indexes become invalid.",
|
||||
"To remove a document from a single knowledge base only, use the document delete command instead.",
|
||||
],
|
||||
exampleArgs: ["--file-id file-xxx --workspace-id ws-xxx", "--file-id file-xxx --yes"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = { fileId: flags.fileId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteFile);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
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>
|
||||
>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`deleted: ${flags.fileId}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagDescribeFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const FILE_GET_FLAGS = {
|
||||
fileId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Data-center file ID",
|
||||
required: true,
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show data-center file details (size, MD5, tags, timestamps)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id> [flags]",
|
||||
flags: FILE_GET_FLAGS,
|
||||
exampleArgs: ["--file-id file-xxx --workspace-id ws-xxx"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = { fileId: flags.fileId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.describeFile);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagDescribeFileResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const file = response.data;
|
||||
if (settings.quiet) {
|
||||
emitBare(file?.fileId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format !== "text") {
|
||||
emitResult(response, format);
|
||||
return;
|
||||
}
|
||||
emitBare(`id: ${file?.fileId ?? "-"}`);
|
||||
emitBare(`name: ${file?.fileName ?? "-"}`);
|
||||
emitBare(`type: ${file?.fileType ?? "-"}`);
|
||||
emitBare(`size: ${file?.sizeBytes ?? "-"}`);
|
||||
emitBare(`status: ${file?.status ?? "-"}`);
|
||||
emitBare(`parser: ${file?.parser ?? "-"}`);
|
||||
emitBare(`category: ${file?.category ?? "-"}`);
|
||||
emitBare(`uploaded: ${file?.uploadTime ?? "-"}`);
|
||||
const tags = Array.isArray(file?.tags) ? file.tags.join(", ") : (file?.tags ?? "-");
|
||||
emitBare(`tags: ${tags || "-"}`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagListFileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const FILE_LIST_FLAGS = {
|
||||
categoryId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Category to list (find ids via the category list command); exact match",
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by exact file name without its extension (a.md → pass a)",
|
||||
},
|
||||
fileId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description: "Filter by exact file ID (repeatable)",
|
||||
},
|
||||
nextToken: {
|
||||
type: "string",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the next page (from previous output)",
|
||||
},
|
||||
maxResult: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Items per page",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List files in a data-center category",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--category-id <id> [flags]",
|
||||
flags: FILE_LIST_FLAGS,
|
||||
notes: [
|
||||
"A real category id is required — the default value is not resolved here. Find the id via the category list command.",
|
||||
"--name matches the exact file name without its extension (for a.md pass a); partial keywords return no results.",
|
||||
"Pagination is cursor-based: reuse the printed next token to continue.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--category-id cate-xxx --workspace-id ws-xxx",
|
||||
"--category-id cate-xxx --name report",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = {
|
||||
categoryId: flags.categoryId,
|
||||
...(flags.name ? { fileName: flags.name } : {}),
|
||||
...(flags.fileId?.length ? { fileIds: flags.fileId } : {}),
|
||||
...(flags.nextToken ? { nextToken: flags.nextToken } : {}),
|
||||
...(flags.maxResult !== undefined ? { maxResult: flags.maxResult } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listFile);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagListFileResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const files = response.data?.fileList ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const file of files) emitBare(file.fileId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
if (files.length === 0) {
|
||||
emitBare("No files found.");
|
||||
} else {
|
||||
for (const file of files) {
|
||||
emitBare(
|
||||
truncateLine(
|
||||
[file.fileId, file.status ?? "-", file.fileName, file.sizeBytes ?? "-"].join(" "),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
const nextToken = response.data?.nextToken;
|
||||
if (nextToken) emitBare(`next: --next-token ${nextToken}`);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type RagCreateIndexV2Response,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import {
|
||||
resolveWorkspaceId,
|
||||
WORKSPACE_FLAG,
|
||||
failedImportDocs,
|
||||
importJobFailureMessage,
|
||||
importJobStatus,
|
||||
importJobStatusUrl,
|
||||
pollImportJob,
|
||||
} from "./shared.ts";
|
||||
|
||||
const KB_CREATE_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Knowledge base name (1-20 chars, unique in workspace)",
|
||||
required: true,
|
||||
},
|
||||
docId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description:
|
||||
"Data-center file id to import (repeatable); mutually exclusive with --category-id",
|
||||
},
|
||||
categoryId: {
|
||||
type: "array",
|
||||
valueHint: "<id>",
|
||||
description:
|
||||
"Import every file under this category (repeatable); mutually exclusive with --doc-id",
|
||||
},
|
||||
embeddingModel: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Embedding model name (default: text-embedding-v4)",
|
||||
},
|
||||
chunkSize: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Chunk size in characters (default: 600, recommended 300-800)",
|
||||
},
|
||||
wait: { type: "switch", description: "Poll the initial import job to a terminal state" },
|
||||
pollInterval: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: "Polling interval when waiting (default: 5)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** sourceType/docIds/categoryIds derivation, centralized for unit testing (gotcha: the parameter is docIds, not fileIds) */
|
||||
export function buildDataSourceFields(flags: { docId?: string[]; categoryId?: string[] }): {
|
||||
sourceType: string;
|
||||
docIds?: string[];
|
||||
categoryIds?: string[];
|
||||
dataSources: Array<{ sourceType: string }>;
|
||||
} {
|
||||
if (flags.docId?.length) {
|
||||
return {
|
||||
sourceType: "DATA_CENTER_FILE",
|
||||
docIds: flags.docId,
|
||||
dataSources: [{ sourceType: "DATA_CENTER_FILE" }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
sourceType: "DATA_CENTER_CATEGORY",
|
||||
categoryIds: flags.categoryId,
|
||||
dataSources: [{ sourceType: "DATA_CENTER_CATEGORY" }],
|
||||
};
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Create a knowledge base and import data-center files or categories",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--name <text> (--doc-id <id> | --category-id <id>) [flags]",
|
||||
flags: KB_CREATE_FLAGS,
|
||||
notes: [
|
||||
"Structure/sink types are fixed to the default document knowledge base (unstructured, BUILT_IN storage).",
|
||||
"Returns the knowledge base id (pipelineId) and the initial import job id (ingestionId).",
|
||||
"Use the import job status command (or --wait) to track the initial import.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--name demo --doc-id file-xxx --workspace-id ws-xxx",
|
||||
"--name demo --category-id cate-xxx --wait",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
|
||||
const hasDocIds = !!flags.docId?.length;
|
||||
const hasCategoryIds = !!flags.categoryId?.length;
|
||||
if (hasDocIds && hasCategoryIds) return "Use either --doc-id or --category-id, not both";
|
||||
if (!hasDocIds && !hasCategoryIds)
|
||||
return "Provide --doc-id or --category-id as the data source";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Fixed values, not exposed as flags in this version: structureType unstructured, sinkType BUILT_IN.
|
||||
// Note: the public docs' example uses sinkType DEFAULT, but BUILT_IN is what works against the live API.
|
||||
const body = {
|
||||
name: flags.name,
|
||||
structureType: "unstructured",
|
||||
sinkType: "BUILT_IN",
|
||||
embeddingModelName: flags.embeddingModel ?? "text-embedding-v4",
|
||||
chunkSize: flags.chunkSize ?? 600,
|
||||
...buildDataSourceFields(flags),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexCreateV2);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagCreateIndexV2Response>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
const pipelineId = response.data?.pipelineId;
|
||||
const ingestionId = response.data?.ingestionId;
|
||||
|
||||
let finalStatus: string | undefined;
|
||||
if (flags.wait && pipelineId && ingestionId) {
|
||||
const statusResponse = await pollImportJob(ctx.client, settings, {
|
||||
statusUrl: importJobStatusUrl(workspaceId, pipelineId, ingestionId).toString(),
|
||||
intervalSec: flags.pollInterval ?? 5,
|
||||
});
|
||||
finalStatus = importJobStatus(statusResponse);
|
||||
// Job finished but some documents failed to parse → non-zero exit, server message
|
||||
// passed through verbatim (the knowledge base was created; its id goes in the hint)
|
||||
if (failedImportDocs(statusResponse).length > 0) {
|
||||
throw new BailianError(
|
||||
importJobFailureMessage(statusResponse, "Initial import reported document failures."),
|
||||
ExitCode.GENERAL,
|
||||
`Knowledge base created: ${pipelineId}`,
|
||||
{ api: { requestId: statusResponse.request_id } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(pipelineId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`index_id: ${pipelineId ?? "-"}`);
|
||||
if (ingestionId) emitBare(`ingestion_id: ${ingestionId}`);
|
||||
if (finalStatus) emitBare(`status: ${finalStatus}`);
|
||||
emitBare("Next: check the import job status, then search against this knowledge base.");
|
||||
return;
|
||||
}
|
||||
emitResult(finalStatus ? { ...response, final_status: finalStatus } : response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagIndexFilesResponse,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
import { fetchIndexDetail } from "./kb-info.ts";
|
||||
|
||||
const KB_DELETE_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...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: "Delete a knowledge base with all its documents and chunks",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Irreversible — the knowledge base and all indexed content are permanently removed.",
|
||||
"Files in the data center are not affected; only the knowledge base index is deleted.",
|
||||
],
|
||||
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx", "--index-id idx-xxx --yes"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// This endpoint is back to snake_case: body { index_id }
|
||||
const body = { index_id: flags.indexId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDelete);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
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",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`deleted: ${flags.indexId}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagIndexListResponse,
|
||||
type RagIndexRow,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const KB_INFO_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function indexDetailUrl(workspaceId: string, indexId: string): string {
|
||||
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexList));
|
||||
url.searchParams.set("pipeline_id", indexId);
|
||||
url.searchParams.set("page_number", "1");
|
||||
url.searchParams.set("page_size", "1");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The index/list API now supports pipeline_id filtering, so a single
|
||||
* request suffices instead of paginating. Reused by kb delete for its
|
||||
* confirmation summary.
|
||||
*/
|
||||
export async function fetchIndexDetail(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
indexId: string,
|
||||
): Promise<RagIndexRow> {
|
||||
const response = await client.requestJson<RagIndexListResponse>({
|
||||
path: indexDetailUrl(workspaceId, indexId),
|
||||
method: "GET",
|
||||
});
|
||||
const row = response.data?.rows?.[0];
|
||||
if (!row) {
|
||||
throw new BailianError(
|
||||
`Knowledge base not found: ${indexId}`,
|
||||
ExitCode.GENERAL,
|
||||
"Check the id — list knowledge bases in this workspace to verify it.",
|
||||
);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function formatField(label: string, value: string | number | boolean | null | undefined): string {
|
||||
return ` ${label}: ${value ?? "-"}`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show knowledge base configuration details",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_INFO_FLAGS,
|
||||
notes: ["Indexing settings are immutable; changing them requires recreating the knowledge base."],
|
||||
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: indexDetailUrl(workspaceId, flags.indexId),
|
||||
request: null,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const row = await fetchIndexDetail(ctx.client, workspaceId, flags.indexId);
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(row.id ?? "");
|
||||
return;
|
||||
}
|
||||
if (format !== "text") {
|
||||
emitResult(row, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// Grouped by diagnostic concern; the immutable annotation on Indexing tells
|
||||
// users which settings require recreating the knowledge base
|
||||
emitBare("Basic:");
|
||||
emitBare(formatField("id", row.id));
|
||||
emitBare(formatField("name", row.name));
|
||||
emitBare(formatField("description", row.description));
|
||||
emitBare(formatField("dataType", row.dataType));
|
||||
emitBare("Indexing: [immutable — recreate required to change]");
|
||||
emitBare(formatField("embeddingModelName", row.embeddingModelName));
|
||||
emitBare(formatField("embeddingDimension", row.embeddingDimension));
|
||||
emitBare(formatField("chunkSize", row.chunkSize));
|
||||
emitBare(formatField("overlapSize", row.overlapSize));
|
||||
emitBare(formatField("chunkMode", row.chunkMode));
|
||||
emitBare(formatField("separator", row.separator));
|
||||
emitBare("Retrieval:");
|
||||
emitBare(formatField("rerankModelName", row.rerankModelName));
|
||||
emitBare(formatField("rerankMinScore", row.rerankMinScore));
|
||||
emitBare(formatField("rerankTopN", row.rerankTopN));
|
||||
emitBare(formatField("rerankMode", row.rerankMode));
|
||||
emitBare(formatField("enableRewrite", row.enableRewrite));
|
||||
emitBare(formatField("denseSimilarityTopK", row.denseSimilarityTopK));
|
||||
emitBare(formatField("sparseSimilarityTopK", row.sparseSimilarityTopK));
|
||||
emitBare("Data:");
|
||||
emitBare(formatField("sourceType", row.sourceType));
|
||||
emitBare(formatField("connectorId", row.connectorId));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagIndexListResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const KB_LIST_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by knowledge base name (fuzzy match, 1-20 chars)",
|
||||
},
|
||||
...PAGE_FLAGS,
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "List knowledge bases in the workspace",
|
||||
auth: "apiKey",
|
||||
usageArgs: "[flags]",
|
||||
flags: KB_LIST_FLAGS,
|
||||
notes: [
|
||||
"Auth: uses DashScope API Key (Bearer token).",
|
||||
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.",
|
||||
"Use the returned id as --index-id in knowledge base / document management commands.",
|
||||
],
|
||||
exampleArgs: ["--workspace-id ws-xxx", "--name demo --page-number 2 --page-size 50"],
|
||||
validate(flags) {
|
||||
if (flags.name !== undefined && (flags.name.length < 1 || flags.name.length > 20)) {
|
||||
return "--name must be 1-20 characters";
|
||||
}
|
||||
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
|
||||
return "--page-size must be between 1 and 100";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Pagination and filter parameters must go in the query string — the server ignores them in the body
|
||||
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexList));
|
||||
if (flags.name) url.searchParams.set("pipeline_name", flags.name);
|
||||
url.searchParams.set("page_number", String(flags.pageNumber ?? 1));
|
||||
url.searchParams.set("page_size", String(flags.pageSize ?? 20));
|
||||
const endpoint = url.toString();
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: null }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagIndexListResponse>({
|
||||
path: endpoint,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const rows = response.data?.rows ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const row of rows) emitBare(row.id);
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
if (rows.length === 0) {
|
||||
emitBare("No knowledge bases found.");
|
||||
} else {
|
||||
for (const row of rows) {
|
||||
emitBare(
|
||||
truncateLine(
|
||||
[
|
||||
row.id,
|
||||
row.name,
|
||||
row.embeddingModelName ?? "-",
|
||||
row.chunkSize ?? "-",
|
||||
row.description ?? "",
|
||||
].join(" "),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
emitBare(`total: ${response.data?.total ?? rows.length}`);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type RagMonitorResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const KB_STATS_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
start: {
|
||||
type: "string",
|
||||
valueHint: "<time>",
|
||||
description:
|
||||
"Range start: Unix seconds or ISO date, must be in the past (default: 24 hours ago)",
|
||||
},
|
||||
end: {
|
||||
type: "string",
|
||||
valueHint: "<time>",
|
||||
description: "Range end: Unix seconds or ISO date, must be in the past (default: now)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Normalize time input to a second-precision string (the API requires seconds as a string). Accepts Unix seconds or an ISO date. */
|
||||
export function toEpochSecondsString(input: string): string {
|
||||
if (/^\d+$/.test(input)) {
|
||||
// Digits-only input is treated as Unix seconds; 13-digit millisecond timestamps are reduced to seconds
|
||||
return input.length >= 13 ? String(Math.floor(Number(input) / 1000)) : input;
|
||||
}
|
||||
// Non-numeric input must be a full ISO date (YYYY-MM-DD, optionally with a time
|
||||
// part). Date.parse alone is too lenient — V8 silently reads truncated input
|
||||
// like "2026-" as Jan 1st, which would query a misleading range.
|
||||
const parsedMs = /^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(input) ? Date.parse(input) : Number.NaN;
|
||||
if (Number.isNaN(parsedMs)) {
|
||||
throw new BailianError(
|
||||
`Invalid time value: ${input}`,
|
||||
ExitCode.USAGE,
|
||||
"Pass Unix seconds (e.g. 1780900000) or an ISO date (e.g. 2026-07-30T00:00:00Z).",
|
||||
);
|
||||
}
|
||||
return String(Math.floor(parsedMs / 1000));
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show knowledge base storage and QPS monitoring data",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_STATS_FLAGS,
|
||||
notes: [
|
||||
"Defaults to the last 24 hours when --start/--end are omitted.",
|
||||
"Timestamps are normalized to epoch seconds as required by the server.",
|
||||
"Future timestamps are rejected for --start and clamped to now for --end, since the monitor API only returns past data.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --start 2026-07-30 --end 2026-07-31",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const startTimestamp = flags.start
|
||||
? toEpochSecondsString(flags.start)
|
||||
: String(nowSeconds - 24 * 3600);
|
||||
let endTimestamp = flags.end ? toEpochSecondsString(flags.end) : String(nowSeconds);
|
||||
|
||||
// The monitor API rejects future timestamps with a misleading
|
||||
// "missing or invalid" error — validate here with a clear message.
|
||||
if (Number(startTimestamp) > nowSeconds) {
|
||||
throw new BailianError(
|
||||
`Start time is in the future; the monitor API only accepts past or current timestamps.`,
|
||||
ExitCode.USAGE,
|
||||
"Use a start date/time at or before now, or omit --start to default to 24 hours ago.",
|
||||
);
|
||||
}
|
||||
const clampedEnd = Number(endTimestamp) > nowSeconds;
|
||||
if (clampedEnd) {
|
||||
endTimestamp = String(nowSeconds);
|
||||
}
|
||||
|
||||
const body = { indexId: flags.indexId, startTimestamp, endTimestamp };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexMonitor);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagMonitorResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (format !== "text") {
|
||||
emitResult(response, format);
|
||||
return;
|
||||
}
|
||||
if (clampedEnd) {
|
||||
emitBare("note: end time was in the future, clamped to now.");
|
||||
}
|
||||
// Shape verified against the live API: the monitor fields are objects, not arrays
|
||||
const storage = response.data?.storageMonitorData;
|
||||
const qps = response.data?.qpsMonitorData;
|
||||
emitBare(`plan: ${response.data?.pipelineCommercialType ?? "-"}`);
|
||||
emitBare(
|
||||
`storage: ${storage?.indexStorageUsage ?? "-"} / ${storage?.indexStorageLimit ?? "-"}`,
|
||||
);
|
||||
emitBare(`peak qps: ${qps?.peakQps ?? "-"}`);
|
||||
emitBare(`qps windows: ${qps?.monitorData?.length ?? 0} data point(s)`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const KB_UPDATE_FLAGS = {
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Knowledge base ID",
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "New knowledge base name (1-20 chars)",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "New knowledge base description",
|
||||
},
|
||||
rerankMinScore: {
|
||||
type: "number",
|
||||
valueHint: "<score>",
|
||||
description: "Rerank minimum score threshold, range 0-1 (chunks below are filtered)",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Update knowledge base name, description or rerank threshold",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--index-id <id> [flags]",
|
||||
flags: KB_UPDATE_FLAGS,
|
||||
notes: [
|
||||
"Indexing settings (embedding model, chunk size, etc.) are immutable — recreate the knowledge base to change them.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--index-id idx-xxx --description 'product docs v2' --workspace-id ws-xxx",
|
||||
"--index-id idx-xxx --rerank-min-score 0.3",
|
||||
],
|
||||
validate(flags) {
|
||||
if (
|
||||
flags.name === undefined &&
|
||||
flags.description === undefined &&
|
||||
flags.rerankMinScore === undefined
|
||||
) {
|
||||
return "Nothing to update — pass --name, --description or --rerank-min-score";
|
||||
}
|
||||
if (flags.name !== undefined && (flags.name.length < 1 || flags.name.length > 20)) {
|
||||
return "--name must be 1-20 characters";
|
||||
}
|
||||
if (
|
||||
flags.rerankMinScore !== undefined &&
|
||||
(flags.rerankMinScore < 0 || flags.rerankMinScore > 1)
|
||||
) {
|
||||
return "--rerank-min-score must be between 0 and 1";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Gotcha: this endpoint names the knowledge base ID parameter `id` (not index_id/indexId)
|
||||
const body = {
|
||||
id: flags.indexId,
|
||||
...(flags.name !== undefined ? { name: flags.name } : {}),
|
||||
...(flags.description !== undefined ? { description: flags.description } : {}),
|
||||
...(flags.rerankMinScore !== undefined ? { rerankMinScore: flags.rerankMinScore } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexUpdate);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`updated: ${flags.indexId}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -2,13 +2,12 @@ import {
|
||||
defineCommand,
|
||||
knowledgeSearchEndpoint,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
type KnowledgeSearchRequest,
|
||||
type KnowledgeSearchResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SEARCH_FLAGS = {
|
||||
query: {
|
||||
@@ -23,23 +22,21 @@ const SEARCH_FLAGS = {
|
||||
description: "Retrieval service ID (find in console knowledge retrieval page)",
|
||||
required: true,
|
||||
},
|
||||
// 知识库走 workspace 专属域名,--workspace-id 属命令自有 flag(console 凭证域不适用)。
|
||||
workspaceId: {
|
||||
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
|
||||
// flag here (the console credential scope does not apply).
|
||||
...WORKSPACE_FLAG,
|
||||
// Named to avoid the runtime-reserved global --version flag
|
||||
agentVersion: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
|
||||
valueHint: "<version>",
|
||||
description:
|
||||
"Service version to call: beta (draft for debugging) or a published number; default is the latest published version",
|
||||
},
|
||||
image: {
|
||||
type: "array",
|
||||
valueHint: "<url>",
|
||||
description: "Image URL for multimodal retrieval (repeatable)",
|
||||
},
|
||||
queryHistory: {
|
||||
type: "string",
|
||||
valueHint: "<json>",
|
||||
description:
|
||||
'User conversation history JSON for context understanding and query rewriting. Format: \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]\'',
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
@@ -51,24 +48,16 @@ export default defineCommand({
|
||||
"Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.",
|
||||
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
|
||||
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
|
||||
"`--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.",
|
||||
"`--agent-version beta` calls the draft config for debugging before it is deployed.",
|
||||
],
|
||||
exampleArgs: [
|
||||
'--query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
|
||||
'--api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg',
|
||||
'--query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]\'',
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
|
||||
const workspaceId = flags.workspaceId || settings.workspaceId;
|
||||
if (!workspaceId) {
|
||||
throw new BailianError(
|
||||
"Workspace ID is required.",
|
||||
ExitCode.USAGE,
|
||||
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
|
||||
);
|
||||
}
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
@@ -77,23 +66,14 @@ export default defineCommand({
|
||||
agent_id: flags.agentId,
|
||||
};
|
||||
|
||||
if (flags.image && flags.image.length > 0) {
|
||||
body.images = flags.image;
|
||||
// Omitted flag → field not sent (default behavior unchanged: latest published
|
||||
// version); the value is not validated — the set of versions is server-side state
|
||||
if (flags.agentVersion) {
|
||||
body.agent_version = flags.agentVersion;
|
||||
}
|
||||
|
||||
// Parse query_history JSON for multi-turn context
|
||||
if (flags.queryHistory) {
|
||||
try {
|
||||
body.query_history = JSON.parse(flags.queryHistory) as Array<{
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}>;
|
||||
} catch {
|
||||
throw new BailianError(
|
||||
'--query-history must be valid JSON. Example: --query-history \'[{"role":"user","content":"What is RAG"}]\'',
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
if (flags.image && flags.image.length > 0) {
|
||||
body.images = flags.image;
|
||||
}
|
||||
|
||||
const url = knowledgeSearchEndpoint(workspaceId);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_COPY_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Source service (agent) ID to copy",
|
||||
required: true,
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Copy a service into a new draft (name gets a copy_ prefix)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_COPY_FLAGS,
|
||||
notes: [
|
||||
"The copy starts as a beta draft; test it with --agent-version beta, then deploy to publish.",
|
||||
"Requires the knowledge-base create permission in the workspace.",
|
||||
],
|
||||
exampleArgs: ["--agent-id aid-xxx --workspace-id ws-xxx"],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = { agent_id: flags.agentId };
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentCopy);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const newAgentId = agentMutationField(response, "agent_id");
|
||||
if (settings.quiet) {
|
||||
emitBare(newAgentId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(
|
||||
`new agent_id: ${newAgentId ?? "-"} (name: ${agentMutationField(response, "agent_name") ?? "-"}, status: ${agentMutationField(response, "agent_status") ?? "draft"})`,
|
||||
);
|
||||
emitBare(
|
||||
"Test the draft with --agent-version beta on search/chat, then deploy it to publish.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user