mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
76 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 | |||
| 94ccab0898 | |||
| b895f88abb | |||
| 7eedc05b99 | |||
| f3c7b6fb10 | |||
| eb196cb4a6 | |||
| d5c4bd3572 | |||
| eb4f9af3e7 | |||
| b7a4efe619 | |||
| 4ec0f6828b | |||
| 30f7525d50 | |||
| aa38d5c670 | |||
| cc51164c2f | |||
| d74d4efcd0 | |||
| e7422bd2e5 | |||
| 2d5c49b02e | |||
| 9bd8b60c22 | |||
| 0369bd36b0 | |||
| 92a978af3c | |||
| 9749a11d76 | |||
| 2965080cb7 | |||
| 81959145d7 | |||
| 4343fc87af | |||
| 1c76749ee5 | |||
| 1b568e8d37 | |||
| 5d1b7aac3a | |||
| e292b20d4b | |||
| 12e7a22195 | |||
| 219d8be80a | |||
| ab766d44d3 | |||
| 1d9852805f | |||
| 99a3dbae2d | |||
| 9ae5dc924d | |||
| d6cb075629 | |||
| 5007b9b574 | |||
| 8286a74fb6 | |||
| 9eb2acbb65 | |||
| 1d35326c86 | |||
| eb6c2b8e2a | |||
| ebd6226a9f | |||
| e25d3b0b8e | |||
| 0e33c70e65 | |||
| 3c64461cca | |||
| f30fff9065 | |||
| a7245c0f62 | |||
| 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 安装。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -82,15 +82,31 @@ Send the following to your Agent — it will detect your environment, then insta
|
||||
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
|
||||
```
|
||||
|
||||
**Manual install (npm)**
|
||||
**Install with NPM**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
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
|
||||
|
||||
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
|
||||
|
||||
+18
-2
@@ -81,15 +81,31 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
|
||||
```
|
||||
|
||||
**手动安装(npm)**
|
||||
**NPM 安装**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
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。
|
||||
|
||||
## 快速开始
|
||||
|
||||
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
|
||||
|
||||
@@ -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"
|
||||
|
||||
+18
-2
@@ -82,15 +82,31 @@ Send the following to your Agent — it will detect your environment, then insta
|
||||
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
|
||||
```
|
||||
|
||||
**Manual install (npm)**
|
||||
**Install with NPM**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
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
|
||||
|
||||
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
|
||||
|
||||
@@ -81,15 +81,31 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
|
||||
```
|
||||
|
||||
**手动安装(npm)**
|
||||
**NPM 安装**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
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。
|
||||
|
||||
## 快速开始
|
||||
|
||||
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
|
||||
|
||||
@@ -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.3",
|
||||
"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,
|
||||
@@ -53,9 +84,12 @@ import {
|
||||
modelList,
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaRequest,
|
||||
quotaUpdate,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
permissionGrant,
|
||||
permissionRevoke,
|
||||
datasetUpload,
|
||||
datasetList,
|
||||
datasetGet,
|
||||
@@ -64,6 +98,7 @@ import {
|
||||
finetuneTextCreate,
|
||||
finetuneAudioCreate,
|
||||
finetuneImageCreate,
|
||||
finetuneVideoCreate,
|
||||
finetuneList,
|
||||
finetuneGet,
|
||||
finetuneCancel,
|
||||
@@ -73,6 +108,7 @@ import {
|
||||
finetuneExport,
|
||||
finetuneWatch,
|
||||
finetuneCapability,
|
||||
finetunePrice,
|
||||
deployTextCreate,
|
||||
deployAudioCreate,
|
||||
deployImageCreate,
|
||||
@@ -82,6 +118,8 @@ import {
|
||||
deployScale,
|
||||
deployUpdate,
|
||||
deployDelete,
|
||||
deployPause,
|
||||
deployResume,
|
||||
tokenPlanListSeats,
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
@@ -154,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,
|
||||
@@ -174,9 +245,12 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"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,
|
||||
@@ -185,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,
|
||||
@@ -194,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,
|
||||
@@ -203,6 +279,8 @@ 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,
|
||||
@@ -235,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,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.14.3",
|
||||
"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,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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
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_CREATE_FLAGS = {
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Service name (up to 200 chars, unique per scene in the workspace)",
|
||||
required: true,
|
||||
},
|
||||
scene: {
|
||||
type: "string",
|
||||
valueHint: "<scene>",
|
||||
description: "Service scene: chat (Q&A) or search (retrieval)",
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Service description (up to 1000 chars)",
|
||||
},
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Bind this knowledge base; other settings use server defaults",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Create a retrieval / Q&A service (initial status: draft, version: beta)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--name <text> --scene <chat|search> [flags]",
|
||||
flags: SERVICE_CREATE_FLAGS,
|
||||
notes: [
|
||||
"Without an explicit configuration the server applies its default agent settings.",
|
||||
"The draft (beta) version can be tested via --agent-version beta on search/chat before deploying.",
|
||||
"Requires the knowledge-base create permission in the workspace.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--name my-qa --scene chat --workspace-id ws-xxx",
|
||||
"--name my-search --scene search --index-id idx-xxx",
|
||||
],
|
||||
validate(flags) {
|
||||
if (flags.name.length > 200) return "--name must be at most 200 characters";
|
||||
if (flags.scene !== "chat" && flags.scene !== "search") {
|
||||
return "--scene must be chat or search";
|
||||
}
|
||||
if (flags.description !== undefined && flags.description.length > 1000) {
|
||||
return "--description must be at most 1000 characters";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// agent_config is optional — omit to use server defaults; with --index-id build
|
||||
// the minimal kb_search_configs (name/desc etc. are backfilled by the system from
|
||||
// the knowledge base, so they are not sent)
|
||||
const body = {
|
||||
agent_name: flags.name,
|
||||
agent_scene: flags.scene,
|
||||
...(flags.description ? { agent_desc: flags.description } : {}),
|
||||
...(flags.indexId ? { agent_config: { kb_search_configs: [{ id: flags.indexId }] } } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentCreate);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const agentId = agentMutationField(response, "agent_id");
|
||||
if (settings.quiet) {
|
||||
emitBare(agentId ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(
|
||||
`created: ${agentId ?? "-"} (status: ${agentMutationField(response, "agent_status") ?? "draft"}, version: ${agentMutationField(response, "agent_version") ?? "beta"})`,
|
||||
);
|
||||
emitBare(
|
||||
"Test the draft with --agent-version beta on search/chat, then deploy it to publish.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DELETE_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Service (agent) ID",
|
||||
required: true,
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); failure degrades to id-only */
|
||||
async function buildDeleteSummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let liveWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "deployed" || status === "edited") {
|
||||
liveWarning = "\nWARNING: this service is LIVE — deleting it breaks existing callers.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Delete service ${agentId}${infoPart}${liveWarning}\nDeletion cannot be undone; the agent_id can no longer be used for search or chat calls.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Delete a retrieval / Q&A service (soft delete, idempotent)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Deletion cannot be undone; the agent_id becomes unusable for search and chat calls.",
|
||||
"Idempotent — deleting an already-deleted service does not fail.",
|
||||
"Requires the knowledge-base delete permission in the workspace.",
|
||||
],
|
||||
exampleArgs: ["--agent-id aid-xxx --workspace-id ws-xxx", "--agent-id aid-xxx --yes"],
|
||||
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.agentDelete);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeleteSummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(
|
||||
`deleted: ${flags.agentId} (status: ${agentMutationField(response, "agent_status") ?? "deleted"})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_DEPLOY_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Service (agent) ID",
|
||||
required: true,
|
||||
},
|
||||
versionDesc: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Description for the newly published version",
|
||||
},
|
||||
yes: { type: "switch", description: "Skip the confirmation prompt" },
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Confirmation summary lookup (name/status); warns that deploying an edited draft overwrites live behavior; failure degrades to id-only */
|
||||
async function buildDeploySummary(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
let infoPart = "";
|
||||
let editedWarning = "";
|
||||
try {
|
||||
const detail = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId },
|
||||
});
|
||||
const name = detail.data?.agent_name;
|
||||
const status = detail.data?.agent_status;
|
||||
if (name) infoPart += ` name: ${name}`;
|
||||
if (status) {
|
||||
infoPart += ` status: ${status}`;
|
||||
if (status === "edited") {
|
||||
editedWarning =
|
||||
"\nWARNING: a published version is live — deploying replaces its behavior with the current draft.";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Degrade gracefully: a failed lookup does not block confirmation
|
||||
}
|
||||
return `Deploy service ${agentId}${infoPart}${editedWarning}\nPublishing changes what live callers get from this service.`;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Publish the beta draft of a service as a new version",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_DEPLOY_FLAGS,
|
||||
notes: [
|
||||
"The version number auto-increments; status becomes deployed.",
|
||||
"Publishing affects live callers — the confirmation prompt guards against accidents.",
|
||||
"Requires the knowledge-base modify permission in the workspace.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--agent-id aid-xxx --workspace-id ws-xxx",
|
||||
"--agent-id aid-xxx --version-desc 'tuned rerank params' --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = {
|
||||
agent_id: flags.agentId,
|
||||
...(flags.versionDesc ? { agent_version_desc: flags.versionDesc } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentDeploy);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = flags.yes
|
||||
? ""
|
||||
: await buildDeploySummary(ctx.client, workspaceId, flags.agentId);
|
||||
await confirmDangerousAction(summary, flags.yes ?? false);
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const newVersion = agentMutationField(response, "agent_version");
|
||||
if (settings.quiet) {
|
||||
emitBare(newVersion ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
emitBare(`deployed: ${flags.agentId} version ${newVersion ?? "-"}`);
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagAgentDetail,
|
||||
type RagAgentGetResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_GET_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Service (agent) ID",
|
||||
required: true,
|
||||
},
|
||||
agentVersion: {
|
||||
type: "string",
|
||||
valueHint: "<version>",
|
||||
description: "Specific version to inspect (beta or a published number); omit for all versions",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function printDetail(detail: RagAgentDetail): void {
|
||||
emitBare(`Version ${detail.agent_version ?? "?"}:`);
|
||||
if (detail.agent_version_desc) emitBare(` desc: ${detail.agent_version_desc}`);
|
||||
if (detail.publish_time !== undefined) emitBare(` published: ${detail.publish_time}`);
|
||||
const config = detail.agent_config;
|
||||
if (!config) return;
|
||||
if (config.agent_policy) emitBare(` policy: ${config.agent_policy}`);
|
||||
if (config.agent_model) emitBare(` model: ${config.agent_model}`);
|
||||
if (config.temperature !== undefined) emitBare(` temperature: ${config.temperature}`);
|
||||
for (const kbConfig of config.kb_search_configs ?? []) {
|
||||
const kbId = typeof kbConfig.id === "string" ? kbConfig.id : "?";
|
||||
const kbName = typeof kbConfig.name === "string" ? ` (${kbConfig.name})` : "";
|
||||
emitBare(` kb: ${kbId}${kbName}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show service (agent) details including per-version configuration",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_GET_FLAGS,
|
||||
notes: [
|
||||
"Without --agent-version all versions are returned (beta draft plus published numbers).",
|
||||
"The version value is passed through as-is; the valid set is server-side state.",
|
||||
],
|
||||
exampleArgs: [
|
||||
"--agent-id aid-xxx --workspace-id ws-xxx",
|
||||
"--agent-id aid-xxx --agent-version beta",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const body = {
|
||||
agent_id: flags.agentId,
|
||||
...(flags.agentVersion ? { agent_version: flags.agentVersion } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentGet);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentGetResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
if (settings.quiet) {
|
||||
emitBare(data?.agent_id ?? "");
|
||||
return;
|
||||
}
|
||||
if (format !== "text") {
|
||||
emitResult(response, format);
|
||||
return;
|
||||
}
|
||||
emitBare("Basic:");
|
||||
emitBare(` id: ${data?.agent_id ?? "-"}`);
|
||||
emitBare(` name: ${data?.agent_name ?? "-"}`);
|
||||
emitBare(` desc: ${data?.agent_desc ?? "-"}`);
|
||||
emitBare(` scene: ${data?.agent_scene ?? "-"}`);
|
||||
emitBare(` status: ${data?.agent_status ?? "-"}`);
|
||||
for (const detail of data?.agent_details ?? []) {
|
||||
printDetail(detail);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
type FlagsDef,
|
||||
type RagAgentListResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const SERVICE_LIST_FLAGS = {
|
||||
scene: {
|
||||
type: "string",
|
||||
valueHint: "<scene>",
|
||||
description: "Service scene: chat (Q&A) or search (retrieval). Required by the server",
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
valueHint: "<status>",
|
||||
description: "Filter by status: draft, deployed (includes edited) or deleted",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by service name (fuzzy match)",
|
||||
},
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Filter by exact agent ID",
|
||||
},
|
||||
indexId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Filter by exact linked knowledge base (pipeline) ID",
|
||||
},
|
||||
...PAGE_FLAGS,
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const SCENES = ["chat", "search"];
|
||||
const STATUSES = ["draft", "deployed", "deleted"];
|
||||
|
||||
export default defineCommand({
|
||||
description: "List retrieval / Q&A services (agents) in the workspace",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--scene <chat|search> [flags]",
|
||||
flags: SERVICE_LIST_FLAGS,
|
||||
notes: [
|
||||
"A scene (chat or search) is required — run once per scene to see both.",
|
||||
"Use the returned agent_id with the search or chat commands, or with service management commands.",
|
||||
],
|
||||
exampleArgs: ["--scene chat --workspace-id ws-xxx", "--scene search --status deployed"],
|
||||
validate(flags) {
|
||||
if (!SCENES.includes(flags.scene)) return "--scene must be chat or search";
|
||||
if (flags.status !== undefined && !STATUSES.includes(flags.status)) {
|
||||
return "--status must be draft, deployed or deleted";
|
||||
}
|
||||
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);
|
||||
|
||||
// agent/list pagination goes in the body
|
||||
const body = {
|
||||
agent_scene: flags.scene,
|
||||
...(flags.status ? { agent_status: flags.status } : {}),
|
||||
...(flags.name ? { agent_name: flags.name } : {}),
|
||||
...(flags.agentId ? { agent_id: flags.agentId } : {}),
|
||||
...(flags.indexId ? { pipeline_id: flags.indexId } : {}),
|
||||
page_number: flags.pageNumber ?? 1,
|
||||
page_size: flags.pageSize ?? 10,
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentList);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ endpoint, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentListResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const rows = response.data?.rows ?? [];
|
||||
if (settings.quiet) {
|
||||
for (const row of rows) emitBare(row.agent_id ?? "");
|
||||
return;
|
||||
}
|
||||
if (format === "text") {
|
||||
if (rows.length === 0) {
|
||||
emitBare("No services found.");
|
||||
} else {
|
||||
for (const row of rows) {
|
||||
const kbNames = (row.pipeline_list ?? [])
|
||||
.map((pipeline) => pipeline.pipeline_name)
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
emitBare(
|
||||
truncateLine(
|
||||
[
|
||||
row.agent_id,
|
||||
row.agent_status,
|
||||
row.agent_version,
|
||||
row.agent_name,
|
||||
kbNames ? `(kb: ${kbNames})` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
emitBare(`total: ${response.data?.total_count ?? rows.length}`);
|
||||
// Connect finding an ID with using it (capability wording, no hardcoded product path)
|
||||
emitBare(
|
||||
flags.scene === "search"
|
||||
? "Use an agent_id above with the knowledge search command."
|
||||
: "Use an agent_id above with the knowledge chat command.",
|
||||
);
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
defineCommand,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagAgentConfig,
|
||||
type RagAgentGetResponse,
|
||||
type RagAgentMutationResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const BOOL_CHOICES = ["true", "false"];
|
||||
|
||||
const SERVICE_UPDATE_FLAGS = {
|
||||
agentId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Service (agent) ID",
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "New service name (up to 200 chars)",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "New service description (up to 1000 chars)",
|
||||
},
|
||||
agentVersion: {
|
||||
type: "string",
|
||||
valueHint: "<version>",
|
||||
description:
|
||||
"Target version (default: beta draft). Published versions only accept --version-desc",
|
||||
},
|
||||
versionDesc: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Version description",
|
||||
},
|
||||
policy: {
|
||||
type: "string",
|
||||
valueHint: "<policy>",
|
||||
description: "Agent policy: turbo (fast) or agentic (multi-turn)",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Generation model code (must be in the platform allowlist)",
|
||||
},
|
||||
temperature: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Sampling temperature, range 0-2",
|
||||
},
|
||||
maxLlmCalls: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Max LLM calls per request, range 1-30",
|
||||
},
|
||||
enableSessionFile: {
|
||||
type: "string",
|
||||
valueHint: "<bool>",
|
||||
description: "Enable session files: true or false",
|
||||
},
|
||||
enableRefusal: {
|
||||
type: "string",
|
||||
valueHint: "<bool>",
|
||||
description: "Enable refusal answers: true or false",
|
||||
},
|
||||
enableAntiLeak: {
|
||||
type: "string",
|
||||
valueHint: "<bool>",
|
||||
description: "Enable anti prompt-leak: true or false",
|
||||
},
|
||||
enableRichText: {
|
||||
type: "string",
|
||||
valueHint: "<bool>",
|
||||
description: "Enable rich text output: true or false",
|
||||
},
|
||||
enableCitation: {
|
||||
type: "string",
|
||||
valueHint: "<bool>",
|
||||
description: "Enable citations: true or false",
|
||||
},
|
||||
configFile: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description:
|
||||
"JSON file replacing the whole agent_config (for nested settings like kb_search_configs); mutually exclusive with scalar config flags",
|
||||
},
|
||||
...WORKSPACE_FLAG,
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type UpdateFlags = {
|
||||
policy?: string;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
maxLlmCalls?: number;
|
||||
enableSessionFile?: string;
|
||||
enableRefusal?: string;
|
||||
enableAntiLeak?: string;
|
||||
enableRichText?: string;
|
||||
enableCitation?: string;
|
||||
};
|
||||
|
||||
/** Scalar config flag → agent_config field mapping (centralized for validation and merge) */
|
||||
function collectScalarConfig(flags: UpdateFlags): Partial<RagAgentConfig> {
|
||||
const scalar: Partial<RagAgentConfig> = {};
|
||||
if (flags.policy !== undefined) scalar.agent_policy = flags.policy;
|
||||
if (flags.model !== undefined) scalar.agent_model = flags.model;
|
||||
if (flags.temperature !== undefined) scalar.temperature = flags.temperature;
|
||||
if (flags.maxLlmCalls !== undefined) scalar.max_num_llm_calls = flags.maxLlmCalls;
|
||||
if (flags.enableSessionFile !== undefined) scalar.enable_session_file = flags.enableSessionFile;
|
||||
if (flags.enableRefusal !== undefined) scalar.enable_refusal = flags.enableRefusal;
|
||||
if (flags.enableAntiLeak !== undefined) scalar.enable_anti_leak = flags.enableAntiLeak;
|
||||
if (flags.enableRichText !== undefined) scalar.enable_rich_text = flags.enableRichText;
|
||||
if (flags.enableCitation !== undefined) scalar.enable_citation = flags.enableCitation;
|
||||
return scalar;
|
||||
}
|
||||
|
||||
/** --config-file: read JSON + structural/enum/range validation; unknown fields warn but pass through */
|
||||
export function parseConfigFile(filePath: string): RagAgentConfig {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = readFileSync(filePath, "utf-8");
|
||||
} catch (error) {
|
||||
const errno = (error as { code?: string }).code ?? "unknown";
|
||||
throw new BailianError(
|
||||
`Cannot read config file: ${filePath}`,
|
||||
ExitCode.GENERAL,
|
||||
`File system error (${errno}) — check the path and permissions.`,
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new BailianError("--config-file must contain valid JSON.", ExitCode.USAGE);
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
throw new BailianError("--config-file must contain a JSON object.", ExitCode.USAGE);
|
||||
}
|
||||
const config = parsed as RagAgentConfig;
|
||||
validateConfigValues(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
const KNOWN_CONFIG_KEYS = new Set([
|
||||
"agent_policy",
|
||||
"agent_model",
|
||||
"enable_session_file",
|
||||
"enable_refusal",
|
||||
"enable_anti_leak",
|
||||
"enable_rich_text",
|
||||
"enable_citation",
|
||||
"temperature",
|
||||
"max_num_llm_calls",
|
||||
"max_completion_tokens",
|
||||
"session_file_max_parse_length",
|
||||
"enable_kb_router",
|
||||
"kb_router_model",
|
||||
"rerank_top_n",
|
||||
"hybrid_rerank",
|
||||
"kb_search_configs",
|
||||
]);
|
||||
|
||||
/** Enum/range validation + behavioral warnings, per the agent_config contract */
|
||||
export function validateConfigValues(config: RagAgentConfig): void {
|
||||
if (config.agent_policy !== undefined && !["turbo", "agentic"].includes(config.agent_policy)) {
|
||||
throw new BailianError("agent_policy must be turbo or agentic", ExitCode.USAGE);
|
||||
}
|
||||
if (config.temperature !== undefined && (config.temperature < 0 || config.temperature > 2)) {
|
||||
throw new BailianError("temperature must be between 0 and 2", ExitCode.USAGE);
|
||||
}
|
||||
if (
|
||||
config.max_num_llm_calls !== undefined &&
|
||||
(config.max_num_llm_calls < 1 || config.max_num_llm_calls > 30)
|
||||
) {
|
||||
throw new BailianError("max_num_llm_calls must be between 1 and 30", ExitCode.USAGE);
|
||||
}
|
||||
if (config.rerank_top_n !== undefined && (config.rerank_top_n < 1 || config.rerank_top_n > 20)) {
|
||||
throw new BailianError("rerank_top_n must be between 1 and 20", ExitCode.USAGE);
|
||||
}
|
||||
// Behavioral warning: the server clears rerank_instruct unless rerank_mode is custom
|
||||
for (const kbConfig of config.kb_search_configs ?? []) {
|
||||
const rerank = kbConfig.rerank as
|
||||
| { rerank_mode?: string; rerank_instruct?: string }
|
||||
| undefined;
|
||||
if (rerank?.rerank_instruct && rerank.rerank_mode !== "custom") {
|
||||
process.stderr.write(
|
||||
"Warning: rerank_instruct is only effective with rerank_mode=custom; the server clears it otherwise.\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Unknown fields: warn but pass through (the server is the source of truth)
|
||||
for (const key of Object.keys(config)) {
|
||||
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
||||
process.stderr.write(`Warning: unknown agent_config field passed through: ${key}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** read-merge-write: fetch the full beta-draft config and merge scalar changes (the API has whole-replace semantics) */
|
||||
async function fetchBetaConfig(
|
||||
client: Client,
|
||||
workspaceId: string,
|
||||
agentId: string,
|
||||
): Promise<RagAgentConfig> {
|
||||
const response = await client.requestJson<RagAgentGetResponse>({
|
||||
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
|
||||
method: "POST",
|
||||
body: { agent_id: agentId, agent_version: "beta" },
|
||||
});
|
||||
const betaDetail = (response.data?.agent_details ?? []).find(
|
||||
(detail) => detail.agent_version === "beta",
|
||||
);
|
||||
return betaDetail?.agent_config ?? {};
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Update service name, description or draft configuration",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--agent-id <id> [flags]",
|
||||
flags: SERVICE_UPDATE_FLAGS,
|
||||
notes: [
|
||||
"Configuration changes only apply to the beta draft; published versions accept --version-desc only.",
|
||||
"To change the configuration of a published version, first update the beta draft (this command without --agent-version or with --agent-version beta), then run service deploy to publish a new version.",
|
||||
"Scalar config flags merge into the current draft config (read-merge-write); --config-file replaces the whole config and is mutually exclusive with them.",
|
||||
"After updating the draft, verify with --agent-version beta on search/chat, then deploy.",
|
||||
"Requires the knowledge-base modify permission in the workspace.",
|
||||
],
|
||||
// Note on `agent_version` in the request body: this is the agent-management
|
||||
// domain's "target version" parameter (beta draft or a published version
|
||||
// number). It is NOT the search/chat debug-draft field that the project-wide
|
||||
// agent_version cleanup removes — the two share a name but live in different
|
||||
// API surfaces and have different semantics.
|
||||
exampleArgs: [
|
||||
"--agent-id aid-xxx --temperature 0.7 --workspace-id ws-xxx",
|
||||
"--agent-id aid-xxx --config-file ./agent-config.json",
|
||||
"--agent-id aid-xxx --agent-version 1 --version-desc 'first stable release'",
|
||||
],
|
||||
validate(flags) {
|
||||
const scalarTouched = Object.keys(collectScalarConfig(flags)).length > 0;
|
||||
const anyChange =
|
||||
flags.name !== undefined ||
|
||||
flags.description !== undefined ||
|
||||
flags.versionDesc !== undefined ||
|
||||
flags.configFile !== undefined ||
|
||||
scalarTouched;
|
||||
if (!anyChange)
|
||||
return "Nothing to update — pass a name/description/version-desc or config flags";
|
||||
if (flags.configFile !== undefined && scalarTouched) {
|
||||
return "--config-file is mutually exclusive with scalar config flags";
|
||||
}
|
||||
// Version check up front: published version + config change → USAGE (the server
|
||||
// would also reject it, but failing early is faster)
|
||||
const publishedVersion = flags.agentVersion !== undefined && flags.agentVersion !== "beta";
|
||||
if (publishedVersion && (scalarTouched || flags.configFile !== undefined)) {
|
||||
return "Published versions only accept --version-desc; update the beta draft to change config";
|
||||
}
|
||||
if (flags.name !== undefined && flags.name.length > 200) {
|
||||
return "--name must be at most 200 characters";
|
||||
}
|
||||
if (flags.description !== undefined && flags.description.length > 1000) {
|
||||
return "--description must be at most 1000 characters";
|
||||
}
|
||||
if (flags.policy !== undefined && !["turbo", "agentic"].includes(flags.policy)) {
|
||||
return "--policy must be turbo or agentic";
|
||||
}
|
||||
if (flags.temperature !== undefined && (flags.temperature < 0 || flags.temperature > 2)) {
|
||||
return "--temperature must be between 0 and 2";
|
||||
}
|
||||
if (flags.maxLlmCalls !== undefined && (flags.maxLlmCalls < 1 || flags.maxLlmCalls > 30)) {
|
||||
return "--max-llm-calls must be between 1 and 30";
|
||||
}
|
||||
for (const [flagName, value] of [
|
||||
["--enable-session-file", flags.enableSessionFile],
|
||||
["--enable-refusal", flags.enableRefusal],
|
||||
["--enable-anti-leak", flags.enableAntiLeak],
|
||||
["--enable-rich-text", flags.enableRichText],
|
||||
["--enable-citation", flags.enableCitation],
|
||||
] as const) {
|
||||
if (value !== undefined && !BOOL_CHOICES.includes(value)) {
|
||||
return `${flagName} must be true or false`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const workspaceId = resolveWorkspaceId(ctx);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const scalarConfig = collectScalarConfig(flags);
|
||||
const hasConfigChange = flags.configFile !== undefined || Object.keys(scalarConfig).length > 0;
|
||||
|
||||
let agentConfig: RagAgentConfig | undefined;
|
||||
if (flags.configFile !== undefined) {
|
||||
// dry-run also reads the file and validates (rehearsal semantics)
|
||||
agentConfig = parseConfigFile(flags.configFile);
|
||||
} else if (Object.keys(scalarConfig).length > 0) {
|
||||
if (settings.dryRun) {
|
||||
// dry-run does not issue the read-merge request; show the scalar delta with a placeholder note
|
||||
agentConfig = { ...scalarConfig };
|
||||
} else {
|
||||
// The API has whole-replace semantics — read the full beta config first,
|
||||
// then merge (hidden from the user)
|
||||
const currentConfig = await fetchBetaConfig(ctx.client, workspaceId, flags.agentId);
|
||||
agentConfig = { ...currentConfig, ...scalarConfig };
|
||||
validateConfigValues(agentConfig);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
agent_id: flags.agentId,
|
||||
...(flags.name !== undefined ? { agent_name: flags.name } : {}),
|
||||
...(flags.description !== undefined ? { agent_desc: flags.description } : {}),
|
||||
...(flags.agentVersion !== undefined ? { agent_version: flags.agentVersion } : {}),
|
||||
...(flags.versionDesc !== undefined ? { agent_version_desc: flags.versionDesc } : {}),
|
||||
...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
|
||||
};
|
||||
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentUpdate);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint,
|
||||
request: body,
|
||||
...(hasConfigChange && flags.configFile === undefined
|
||||
? { note: "scalar config flags are merged into the current beta config at run time" }
|
||||
: {}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
|
||||
path: endpoint,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (settings.quiet) return;
|
||||
if (format === "text") {
|
||||
emitBare(`updated: ${flags.agentId}`);
|
||||
if (hasConfigChange) {
|
||||
emitBare("Draft config changed — verify with --agent-version beta, then deploy.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
emitResult(response, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
// Shared building blocks for the knowledge admin commands.
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type RagIndexJobDoc,
|
||||
type RagIndexJobStatusResponse,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
|
||||
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
|
||||
// flag here (the console credential scope does not apply).
|
||||
export const WORKSPACE_FLAG = {
|
||||
workspaceId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
// Unified pagination flags for admin list commands. The server-side page/size
|
||||
// parameter names differ per endpoint (page_number/page_num/pageNum/pageNumber…)
|
||||
// and the body-vs-query-string placement also varies — each command maps these
|
||||
// flags to its own API contract. Never pass the flag names through verbatim;
|
||||
// the CLI-facing flag vocabulary is stable even when the backend is not.
|
||||
export const PAGE_FLAGS = {
|
||||
pageNumber: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
pageSize: { type: "number", valueHint: "<n>", description: "Page size per request" },
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Three-level fallback: flag > BAILIAN_WORKSPACE_ID env > config (env/config are merged into settings); missing → USAGE. */
|
||||
export function resolveWorkspaceId(ctx: {
|
||||
flags: { workspaceId?: string };
|
||||
settings: { workspaceId?: string };
|
||||
identity: { binName: string };
|
||||
}): string {
|
||||
const workspaceId = ctx.flags.workspaceId || ctx.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>`,
|
||||
);
|
||||
}
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
/** Truncate text-mode table rows to the terminal width; no truncation when not a TTY (pipe/redirect). */
|
||||
export function truncateLine(line: string): string {
|
||||
if (!process.stdout.isTTY) return line;
|
||||
const width = process.stdout.columns ?? 120;
|
||||
return line.length > width ? `${line.slice(0, Math.max(0, width - 1))}…` : line;
|
||||
}
|
||||
|
||||
// ---- Shared import-job (index_job/status) logic ----
|
||||
// Verified against the live API: the overall job state lives in `ingestion_status`
|
||||
// (PENDING/RUNNING/COMPLETED, no FAILED value); the per-document list is `rows[]`
|
||||
// and failures surface via `rows[].code` (e.g. PARSE_FAILED).
|
||||
// Shared by doc status / doc upload / kb create — contract changes only touch this file.
|
||||
|
||||
/** Overall job state (ingestion_status) */
|
||||
export function importJobStatus(response: unknown): string {
|
||||
return (response as RagIndexJobStatusResponse).data?.ingestion_status ?? "UNKNOWN";
|
||||
}
|
||||
|
||||
/** Per-document failures: rows[].code / status containing FAILED */
|
||||
export function failedImportDocs(response: RagIndexJobStatusResponse): RagIndexJobDoc[] {
|
||||
return (response.data?.rows ?? []).filter((doc) =>
|
||||
[doc.code, doc.status].some((value) => typeof value === "string" && value.includes("FAILED")),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether every document has reached a terminal state (FINISH or *FAILED).
|
||||
* Used to stop polling before ingestion_status becomes COMPLETED — the server
|
||||
* may leave the job on RUNNING indefinitely even after all documents finish.
|
||||
*/
|
||||
function allDocsTerminal(response: RagIndexJobStatusResponse): boolean {
|
||||
const rows = response.data?.rows ?? [];
|
||||
if (rows.length === 0) return false;
|
||||
const totalCount = response.data?.total_count;
|
||||
if (typeof totalCount === "number" && rows.length < totalCount) return false;
|
||||
return rows.every((doc) => {
|
||||
const code = doc.code ?? doc.status ?? "";
|
||||
return code === "FINISH" || code.includes("FAILED");
|
||||
});
|
||||
}
|
||||
|
||||
/** Per-document failure summary: lists both failed and succeeded documents so the user knows the full picture. */
|
||||
export function importJobFailureMessage(
|
||||
response: RagIndexJobStatusResponse,
|
||||
fallbackMessage: string,
|
||||
): string {
|
||||
const rows = response.data?.rows ?? [];
|
||||
const failed = failedImportDocs(response);
|
||||
const failedIds = new Set(failed.map((doc) => doc.doc_id));
|
||||
const succeeded = rows.filter((doc) => !failedIds.has(doc.doc_id));
|
||||
|
||||
const failedDetail = failed
|
||||
.map((doc) => `${doc.doc_name ?? doc.doc_id ?? "?"}: ${doc.message ?? doc.code ?? "unknown"}`)
|
||||
.join("; ");
|
||||
const succeededDetail = succeeded.map((doc) => doc.doc_name ?? doc.doc_id ?? "?").join(", ");
|
||||
|
||||
const parts: string[] = [];
|
||||
if (failedDetail) parts.push(`failed: ${failedDetail}`);
|
||||
if (succeededDetail) parts.push(`succeeded: ${succeededDetail}`);
|
||||
|
||||
return parts.length > 0 ? `${fallbackMessage} (${parts.join("; ")})` : fallbackMessage;
|
||||
}
|
||||
|
||||
/** Build the index_job/status query string (both index_id and job_id are required) */
|
||||
export function importJobStatusUrl(workspaceId: string, indexId: string, jobId: string): URL {
|
||||
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexJobStatus));
|
||||
url.searchParams.set("index_id", indexId);
|
||||
url.searchParams.set("job_id", jobId);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the import job until the overall state is COMPLETED or every document
|
||||
* has reached a terminal state (FINISH or FAILED). The overall state has no
|
||||
* FAILED value, so isFailed is always false — failures are determined by the
|
||||
* caller after return via `failedImportDocs`. The server may leave
|
||||
* ingestion_status on RUNNING indefinitely after documents finish, so checking
|
||||
* rows[] mid-poll avoids a misleading timeout; callers re-check failedImportDocs
|
||||
* after return, keeping the error path uniform.
|
||||
*/
|
||||
export async function pollImportJob(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
options: { statusUrl: string; intervalSec: number },
|
||||
): Promise<RagIndexJobStatusResponse> {
|
||||
return poll<RagIndexJobStatusResponse>(client, settings, {
|
||||
url: options.statusUrl,
|
||||
intervalSec: options.intervalSec,
|
||||
timeoutSec: settings.timeout,
|
||||
isComplete: (data) => {
|
||||
const response = data as RagIndexJobStatusResponse;
|
||||
return importJobStatus(response) === "COMPLETED" || allDocsTerminal(response);
|
||||
},
|
||||
isFailed: () => false,
|
||||
getStatus: (data) => {
|
||||
const response = data as RagIndexJobStatusResponse;
|
||||
const status = importJobStatus(response);
|
||||
const failedCount = failedImportDocs(response).length;
|
||||
const total = response.data?.total_count;
|
||||
if (typeof total === "number" && total > 0) {
|
||||
return `${status} (${failedCount}/${total} failed)`;
|
||||
}
|
||||
return failedCount > 0 ? `${status} (${failedCount} failed)` : status;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Attach a hint for partial-success cases while preserving server context (api/rawResponse kept) */
|
||||
export function withPartialSuccessHint(error: unknown, hint: string): unknown {
|
||||
if (!(error instanceof BailianError) || error.hint) return error;
|
||||
return new BailianError(error.message, error.exitCode, hint, {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response fields from agent-domain mutation endpoints. Gotcha verified
|
||||
* against the live API: create/copy return agent_id etc. inside data, but
|
||||
* deploy returns agent_version/agent_status at the top level next to code —
|
||||
* the server envelope is inconsistent, so read both locations defensively.
|
||||
*/
|
||||
export function agentMutationField(
|
||||
response: { data?: Record<string, unknown>; [key: string]: unknown },
|
||||
field: "agent_id" | "agent_name" | "agent_version" | "agent_status",
|
||||
): string | undefined {
|
||||
const nested = response.data?.[field];
|
||||
if (typeof nested === "string") return nested;
|
||||
const topLevel = response[field];
|
||||
return typeof topLevel === "string" ? topLevel : undefined;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Local pre-flight validation for file uploads (doc upload).
|
||||
// Default category: the lease/addFile `category` parameter accepts the literal
|
||||
// "default" (verified against the live API), so no listCategory resolution is needed.
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { basename, extname, join } from "node:path";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
export interface UploadFormatRule {
|
||||
maxBytes: number;
|
||||
/** block: exceeding the limit throws USAGE; warn: only a stderr warning (documented as a recommended limit) */
|
||||
enforce: "block" | "warn";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format allowlist based on the officially supported document formats.
|
||||
* .csv is inconsistent across the public docs — kept in the allowlist for now;
|
||||
* a server-side rejection would be passed through verbatim.
|
||||
*/
|
||||
export const UPLOAD_FORMAT_RULES: Record<string, UploadFormatRule> = {
|
||||
".doc": { maxBytes: 150 * MB, enforce: "block" },
|
||||
".docx": { maxBytes: 150 * MB, enforce: "block" },
|
||||
".ppt": { maxBytes: 150 * MB, enforce: "block" },
|
||||
".pptx": { maxBytes: 150 * MB, enforce: "block" },
|
||||
".pdf": { maxBytes: 150 * MB, enforce: "block" },
|
||||
".png": { maxBytes: 20 * MB, enforce: "block" },
|
||||
".jpg": { maxBytes: 20 * MB, enforce: "block" },
|
||||
".jpeg": { maxBytes: 20 * MB, enforce: "block" },
|
||||
".bmp": { maxBytes: 20 * MB, enforce: "block" },
|
||||
".gif": { maxBytes: 20 * MB, enforce: "block" },
|
||||
".xls": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
".xlsx": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
".csv": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
".md": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
".txt": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
".html": { maxBytes: 10 * MB, enforce: "warn" },
|
||||
};
|
||||
|
||||
/** Returns true when the extension is in the upload format allowlist. */
|
||||
export function isSupportedExtension(filePath: string): boolean {
|
||||
const extension = extname(filePath).toLowerCase();
|
||||
return extension in UPLOAD_FORMAT_RULES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory names skipped when expanding a directory path (recursive scan).
|
||||
* Covers common tooling artifacts that should never contain user documents.
|
||||
*/
|
||||
const IGNORED_DIRECTORIES = new Set([
|
||||
"node_modules",
|
||||
".git",
|
||||
".svn",
|
||||
".hg",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
".env",
|
||||
".tox",
|
||||
"dist",
|
||||
"build",
|
||||
".cache",
|
||||
".next",
|
||||
".nuxt",
|
||||
]);
|
||||
|
||||
export interface ExpandResult {
|
||||
files: string[];
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand an array of paths into individual file paths.
|
||||
* - Regular files are included as-is.
|
||||
* - Directories are recursively scanned; files with unsupported extensions are
|
||||
* collected into `skipped` instead of throwing.
|
||||
* - Common tooling directories (node_modules, .git, …) are silently skipped.
|
||||
* - A non-existent path throws USAGE so the user gets a clear error.
|
||||
*/
|
||||
export function expandUploadPaths(paths: string[]): ExpandResult {
|
||||
const files: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
function walkDirectory(directoryPath: string): void {
|
||||
let entries: import("node:fs").Dirent[];
|
||||
try {
|
||||
entries = readdirSync(directoryPath, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
const errno = (error as { code?: string }).code ?? "unknown";
|
||||
throw new BailianError(
|
||||
`Cannot read directory: ${directoryPath}`,
|
||||
ExitCode.GENERAL,
|
||||
`File system error (${errno}) — check the path and permissions.`,
|
||||
);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const entryFullPath = join(directoryPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
||||
walkDirectory(entryFullPath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile()) {
|
||||
if (isSupportedExtension(entry.name)) {
|
||||
files.push(entryFullPath);
|
||||
} else {
|
||||
skipped.push(entryFullPath);
|
||||
}
|
||||
}
|
||||
// Symlinks: withFileTypes follows symlinks for isFile/isDirectory,
|
||||
// so they are handled by the branches above.
|
||||
}
|
||||
}
|
||||
|
||||
for (const inputPath of paths) {
|
||||
let pathStat: import("node:fs").Stats;
|
||||
try {
|
||||
pathStat = statSync(inputPath);
|
||||
} catch (error) {
|
||||
const errno = (error as { code?: string }).code ?? "unknown";
|
||||
throw new BailianError(
|
||||
`Cannot read path: ${inputPath}`,
|
||||
ExitCode.GENERAL,
|
||||
`File system error (${errno}) — check the path and permissions.`,
|
||||
);
|
||||
}
|
||||
if (pathStat.isDirectory()) {
|
||||
walkDirectory(inputPath);
|
||||
} else if (pathStat.isFile()) {
|
||||
files.push(inputPath);
|
||||
}
|
||||
// Other types (socket, block device, etc.) are silently ignored.
|
||||
}
|
||||
|
||||
return { files, skipped };
|
||||
}
|
||||
|
||||
/** Local pre-flight check before reading the file: extension allowlist + hard/soft size limits. File I/O failure → GENERAL + errno hint. */
|
||||
export function checkUploadFile(filePath: string): { sizeBytes: number; warning?: string } {
|
||||
const extension = extname(filePath).toLowerCase();
|
||||
const rule = UPLOAD_FORMAT_RULES[extension];
|
||||
if (!rule) {
|
||||
throw new BailianError(
|
||||
`Unsupported file type: ${extension || basename(filePath)}`,
|
||||
ExitCode.USAGE,
|
||||
`Supported formats: ${Object.keys(UPLOAD_FORMAT_RULES).join(" ")}`,
|
||||
);
|
||||
}
|
||||
let sizeBytes: number;
|
||||
try {
|
||||
sizeBytes = statSync(filePath).size;
|
||||
} catch (error) {
|
||||
const errno = (error as { code?: string }).code ?? "unknown";
|
||||
throw new BailianError(
|
||||
`Cannot read file: ${filePath}`,
|
||||
ExitCode.GENERAL,
|
||||
`File system error (${errno}) — check the path and permissions.`,
|
||||
);
|
||||
}
|
||||
if (sizeBytes > rule.maxBytes) {
|
||||
const limitMb = rule.maxBytes / MB;
|
||||
if (rule.enforce === "block") {
|
||||
throw new BailianError(
|
||||
`File exceeds the ${limitMb} MB limit for ${extension}: ${basename(filePath)}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return {
|
||||
sizeBytes,
|
||||
warning: `${basename(filePath)} exceeds the recommended 10 MB for ${extension}; the server may reject or truncate it.`,
|
||||
};
|
||||
}
|
||||
return { sizeBytes };
|
||||
}
|
||||
|
||||
// Known document-format extensions — used to tailor the hint on decode failure
|
||||
// (pointing users to the document upload flow instead)
|
||||
const DOCUMENT_EXTENSIONS = new Set([
|
||||
".doc",
|
||||
".docx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".pdf",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".bmp",
|
||||
".gif",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Read a UTF-8 plain-text file.
|
||||
* Support is defined by content, not extension: any UTF-8 text is valid.
|
||||
* Strict decode failure → USAGE, with a hint pointing to the document upload
|
||||
* flow when the extension is a document format; file I/O failure → GENERAL + errno.
|
||||
*
|
||||
* @param inlineAlternativeFlag When provided, an ENOENT on a value that looks
|
||||
* like inline text (no path separator / no extension) appends a hint pointing
|
||||
* the user to this flag instead. Pass the inline-text flag name (e.g.
|
||||
* `"--content"`) only from commands that have a file-vs-inline choice.
|
||||
*/
|
||||
export function readUtf8TextFile(filePath: string, inlineAlternativeFlag?: string): string {
|
||||
let fileBuffer: Buffer;
|
||||
try {
|
||||
fileBuffer = readFileSync(filePath);
|
||||
} catch (error) {
|
||||
const errno = (error as { code?: string }).code ?? "unknown";
|
||||
let hint = `File system error (${errno}) — check the path and permissions.`;
|
||||
if (
|
||||
inlineAlternativeFlag &&
|
||||
errno === "ENOENT" &&
|
||||
!extname(filePath) &&
|
||||
!filePath.includes("/") &&
|
||||
!filePath.includes("\\")
|
||||
) {
|
||||
hint += ` If you meant to pass text content directly, use ${inlineAlternativeFlag} instead of --content-file.`;
|
||||
}
|
||||
throw new BailianError(`Cannot read file: ${filePath}`, ExitCode.GENERAL, hint);
|
||||
}
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(fileBuffer);
|
||||
} catch {
|
||||
const extension = extname(filePath).toLowerCase();
|
||||
const hint = DOCUMENT_EXTENSIONS.has(extension)
|
||||
? "Document formats cannot be used as chunk content. Upload documents via the document upload command; to edit a chunk, save the text as .md/.txt first."
|
||||
: "Convert the file to UTF-8 encoding and retry.";
|
||||
throw new BailianError(
|
||||
`File is not valid UTF-8 plain text: ${basename(filePath)}`,
|
||||
ExitCode.USAGE,
|
||||
hint,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
anonymousConsoleCall,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
fetchModelDetail,
|
||||
@@ -290,7 +291,7 @@ function printPredictConfigTable(entries: PredictConfigEntry[]): void {
|
||||
|
||||
export default defineCommand({
|
||||
description: "Browse model families or show detailed model info in the Bailian model marketplace",
|
||||
auth: "console",
|
||||
auth: "none",
|
||||
usageArgs:
|
||||
"[--model <model>] [--page <n>] [--page-size <n>] [--provider <p>] [--capability <c>] [--feature <f>] [--enrich]",
|
||||
flags: LIST_FLAGS,
|
||||
@@ -302,10 +303,14 @@ export default defineCommand({
|
||||
"--model qwen-max --enrich --output json",
|
||||
"--feature function-calling --output json",
|
||||
],
|
||||
notes: [
|
||||
"Both the catalog and --enrich parameter-schema endpoints are public — no console login needed.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
|
||||
const modelKey = flags.model;
|
||||
const call = anonymousConsoleCall(settings);
|
||||
|
||||
// ── Detail mode ──
|
||||
if (modelKey) {
|
||||
@@ -316,7 +321,7 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = await fetchModelDetail(ctx.client.console.bind(ctx.client), modelKey);
|
||||
const detail = await fetchModelDetail(call, modelKey);
|
||||
|
||||
if (!detail) {
|
||||
emitBare(`Model "${modelKey}" not found.`);
|
||||
@@ -328,10 +333,7 @@ export default defineCommand({
|
||||
await Promise.all(
|
||||
trunkItems.map(async (item) => {
|
||||
if (!item.model) return;
|
||||
const config = await fetchPredictConfig(
|
||||
ctx.client.console.bind(ctx.client),
|
||||
item.model,
|
||||
);
|
||||
const config = await fetchPredictConfig(call, item.model);
|
||||
if (config) item.predictConfig = config;
|
||||
}),
|
||||
);
|
||||
@@ -361,7 +363,7 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const { total, groups } = await fetchModelGroups(ctx.client.console.bind(ctx.client), params);
|
||||
const { total, groups } = await fetchModelGroups(call, params);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(formatBrowseJson(groups, total), format);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineCommand } from "bailian-cli-core";
|
||||
import { runPermissionChange, validatePermissionChange } from "./shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: "Grant model permissions (inference / finetune / deploy)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <models> [--action <actions>] | --all",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<models>",
|
||||
description: "Model ID(s), comma-separated (max 20)",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
valueHint: "<actions>",
|
||||
description:
|
||||
"Permission action(s), comma-separated: inference, finetune, deploy (default: inference)",
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description:
|
||||
"One-key grant inference for all models in the workspace (including future ones)",
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus",
|
||||
"--model qwen-plus,qwen3-max --action inference,finetune",
|
||||
"--all",
|
||||
"--model qwen-plus --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
"Grants apply to the business workspace your API key belongs to.",
|
||||
"--all maps to the server one-key switch (access_all_entities: OPEN) and only covers inference.",
|
||||
"Actions you omit keep their current grants (server-side tri-state patch).",
|
||||
],
|
||||
validate: (flags) => validatePermissionChange(flags),
|
||||
async run(ctx) {
|
||||
await runPermissionChange(ctx, ctx.flags, true);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { defineCommand, detectOutputFormat, modelsPermissionsPath } from "bailian-cli-core";
|
||||
import { emitResult, renderBoxTable } from "bailian-cli-runtime";
|
||||
import { buildQuery } from "../shared/params.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types — mirror GET /api/v1/models/permissions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface PermissionDetail {
|
||||
inference?: boolean | null;
|
||||
fine_tune?: boolean | null;
|
||||
deploy?: boolean | null;
|
||||
}
|
||||
|
||||
interface ModelPermission {
|
||||
model: string;
|
||||
name?: string;
|
||||
permissions?: PermissionDetail;
|
||||
}
|
||||
|
||||
interface PermissionsResponse {
|
||||
output?: {
|
||||
total?: number;
|
||||
page_no?: number;
|
||||
page_size?: number;
|
||||
permissions?: ModelPermission[];
|
||||
};
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Formatters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Tri-state permission cell: true → yes, false → no, null/undefined → "-". */
|
||||
function formatGrant(granted: boolean | null | undefined): string {
|
||||
if (granted == null) return "-";
|
||||
return granted ? "yes" : "no";
|
||||
}
|
||||
|
||||
function printTable(permissions: ModelPermission[], total: number, emptyHint: string): void {
|
||||
if (permissions.length === 0) {
|
||||
process.stdout.write(`No model permissions found.\n${emptyHint}\n`);
|
||||
return;
|
||||
}
|
||||
const headers = ["Model", "Name", "Inference", "Fine-tune", "Deploy"];
|
||||
const rows = permissions.map((entry) => [
|
||||
entry.model,
|
||||
entry.name ?? "-",
|
||||
formatGrant(entry.permissions?.inference),
|
||||
formatGrant(entry.permissions?.fine_tune),
|
||||
formatGrant(entry.permissions?.deploy),
|
||||
]);
|
||||
const lines = renderBoxTable({
|
||||
headers,
|
||||
rows,
|
||||
align: ["left", "left", "right", "right", "right"],
|
||||
});
|
||||
for (const line of lines) process.stdout.write(line + "\n");
|
||||
process.stdout.write(`\nTotal: ${total}\n`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default defineCommand({
|
||||
description: "List model permissions (inference / fine-tune / deploy) in the workspace",
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--scope <scope>] [--model <model>] [--name <name>] [--page <n>] [--page-size <n>]",
|
||||
flags: {
|
||||
scope: {
|
||||
type: "string",
|
||||
valueHint: "<scope>",
|
||||
choices: ["authorized", "authorizable"] as const,
|
||||
description: "Authorization scope: authorizable (default, full catalog), authorized",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Model ID (exact match)",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Fuzzy search by model name or ID",
|
||||
},
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 20)" },
|
||||
},
|
||||
exampleArgs: [
|
||||
"",
|
||||
"--model qwen-plus",
|
||||
"--scope authorized",
|
||||
"--name qwen --page-size 50",
|
||||
"--output text",
|
||||
],
|
||||
notes: [
|
||||
"Default scope is `authorizable` (the full grantable catalog); use `--scope authorized` to see only models already granted.",
|
||||
"Output defaults to JSON; pass `--output text` for a table. Permission values are tri-state: true / false / null (never set).",
|
||||
"Values mirror the server's grant records as-is for the workspace bound to your API key. A model reporting false/null can still be callable (access may come from other channels); see the Model Studio authorization docs for the exact semantics.",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
|
||||
const scope = flags.scope ?? "authorizable";
|
||||
|
||||
const query = {
|
||||
authorization_scope: scope.toUpperCase(),
|
||||
model: flags.model || undefined,
|
||||
name: flags.name || undefined,
|
||||
page_no: flags.page || 1,
|
||||
page_size: flags.pageSize || 20,
|
||||
};
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{ endpoint: ctx.client.url(modelsPermissionsPath()), method: "GET", query },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await ctx.client.requestJson<PermissionsResponse>({
|
||||
path: modelsPermissionsPath() + buildQuery(query),
|
||||
});
|
||||
const permissions = resp.output?.permissions ?? [];
|
||||
const total = resp.output?.total ?? permissions.length;
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ items: permissions, total }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// The default authorized view is empty until something is granted — point
|
||||
// at the authorizable catalog instead of ending with a bare "nothing".
|
||||
const binName = ctx.identity.binName;
|
||||
const emptyHint =
|
||||
scope === "authorized"
|
||||
? `Nothing granted yet in this workspace. Browse grantable models with \`${binName} permission list --scope authorizable\`, then grant with \`${binName} permission grant --model <model>\`.`
|
||||
: `Adjust --name/--model filters, or check pagination with --page/--page-size.`;
|
||||
|
||||
printTable(permissions, total, emptyHint);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { defineCommand, BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { runPermissionChange, validatePermissionChange } from "./shared.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: "Revoke model permissions (inference / finetune / deploy)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <models> [--action <actions>] | --all --yes",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<models>",
|
||||
description: "Model ID(s), comma-separated (max 20)",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
valueHint: "<actions>",
|
||||
description:
|
||||
"Permission action(s), comma-separated: inference, finetune, deploy (default: inference)",
|
||||
},
|
||||
all: {
|
||||
type: "switch",
|
||||
description: "Close one-key authorization and clear ALL historical inference grants",
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: "Confirm --all without an interactive prompt (required)",
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus",
|
||||
"--model qwen-plus,qwen3-max --action inference,finetune",
|
||||
"--all --yes",
|
||||
"--model qwen-plus --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
"Grants apply to the business workspace your API key belongs to.",
|
||||
"--all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.",
|
||||
"Actions you omit keep their current grants (server-side tri-state patch).",
|
||||
],
|
||||
validate: (flags) => validatePermissionChange(flags),
|
||||
async run(ctx) {
|
||||
const { flags, settings } = ctx;
|
||||
if (flags.all && !flags.yes && !settings.dryRun) {
|
||||
throw new BailianError(
|
||||
"Refusing to clear all historical inference grants without confirmation.",
|
||||
ExitCode.USAGE,
|
||||
"Re-run with --yes to close one-key authorization (or preview with --dry-run).",
|
||||
);
|
||||
}
|
||||
await runPermissionChange(ctx, flags, false);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
detectOutputFormat,
|
||||
modelsPermissionsPath,
|
||||
type Client,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { parseCommaList } from "../shared/params.ts";
|
||||
|
||||
// POST /api/v1/models/permissions accepts at most 20 models per call.
|
||||
export const MAX_MODELS_PER_REQUEST = 20;
|
||||
|
||||
// POST body field names (server ignores unknown keys silently — the docs' curl
|
||||
// example spells `fine_tune`, but only `finetune` actually takes effect).
|
||||
export const PERMISSION_ACTIONS = ["inference", "finetune", "deploy"] as const;
|
||||
export type PermissionAction = (typeof PERMISSION_ACTIONS)[number];
|
||||
|
||||
/** Parse --action into deduped actions (default: inference); returns an error message on bad values. */
|
||||
export function parsePermissionActions(
|
||||
actionFlag: string | undefined,
|
||||
): PermissionAction[] | { error: string } {
|
||||
if (!actionFlag) return ["inference"];
|
||||
const actions = parseCommaList(actionFlag);
|
||||
if (actions.length === 0) return { error: "--action must not be empty." };
|
||||
for (const action of actions) {
|
||||
if (!(PERMISSION_ACTIONS as readonly string[]).includes(action)) {
|
||||
return { error: `--action "${action}" is invalid; use ${PERMISSION_ACTIONS.join(", ")}.` };
|
||||
}
|
||||
}
|
||||
return actions as PermissionAction[];
|
||||
}
|
||||
|
||||
/** Cross-flag validation shared by grant and revoke. */
|
||||
export function validatePermissionChange(flags: {
|
||||
model?: string;
|
||||
action?: string;
|
||||
all: boolean;
|
||||
}): string | undefined {
|
||||
if (flags.all && flags.model) return "--all cannot be combined with --model.";
|
||||
if (!flags.all && !flags.model) return "one of --model / --all is required.";
|
||||
const actions = parsePermissionActions(flags.action);
|
||||
if ("error" in actions) return actions.error;
|
||||
if (flags.all && (actions.length !== 1 || actions[0] !== "inference"))
|
||||
return "--all only supports the inference action.";
|
||||
if (flags.model) {
|
||||
const models = parseCommaList(flags.model);
|
||||
if (models.length === 0) return "--model must not be empty.";
|
||||
if (models.length > MAX_MODELS_PER_REQUEST)
|
||||
return `--model accepts at most ${MAX_MODELS_PER_REQUEST} models per call.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared grant/revoke execution: build the POST body (per-model tri-state
|
||||
* patch, or the access_all_entities one-key switch) and send it. Validation
|
||||
* (mutual exclusion, action values, model count) has already run.
|
||||
*/
|
||||
export async function runPermissionChange(
|
||||
ctx: { settings: Settings; client: Client },
|
||||
flags: { model?: string; action?: string; all: boolean },
|
||||
grant: boolean,
|
||||
): Promise<void> {
|
||||
const format = ctx.settings.outputExplicit ? detectOutputFormat(ctx.settings.output) : "json";
|
||||
const actions = parsePermissionActions(flags.action) as PermissionAction[];
|
||||
const models = flags.model ? parseCommaList(flags.model) : [];
|
||||
|
||||
const body: Record<string, unknown> = flags.all
|
||||
? { access_all_entities: grant ? "OPEN" : "CLOSE" }
|
||||
: {
|
||||
models: models.map((model) => {
|
||||
const entry: Record<string, unknown> = { model };
|
||||
for (const action of actions) entry[action] = grant;
|
||||
return entry;
|
||||
}),
|
||||
};
|
||||
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ endpoint: ctx.client.url(modelsPermissionsPath()), method: "POST", request: body },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.requestJson<{ request_id?: string }>({
|
||||
path: modelsPermissionsPath(),
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const verb = grant ? "granted" : "revoked";
|
||||
if (format === "json") {
|
||||
const summary: Record<string, unknown> = flags.all
|
||||
? { all: true, action: "inference" }
|
||||
: { models, actions };
|
||||
emitResult({ ...summary, [verb]: true, request_id: result.request_id }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.all) {
|
||||
process.stdout.write(
|
||||
grant
|
||||
? "Inference permission granted for all models in the workspace (including future ones).\n"
|
||||
: "One-key authorization closed; historical inference grants cleared.\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`Permissions ${verb} (${actions.join(", ")}): ${models.join(", ")}\n`);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineCommand, detectOutputFormat, BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { ansi, emitResult } from "bailian-cli-runtime";
|
||||
import { displayWidth, padEnd } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
|
||||
const HISTORY_API = "zeldaEasy.broadscope-platform.modelInstance.listModelLimitApplications";
|
||||
|
||||
@@ -49,10 +50,6 @@ function formatDateTime(ts: string | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function printTable(records: LimitApplicationItem[], total: number): void {
|
||||
const color = ansi(process.stdout);
|
||||
|
||||
|
||||
@@ -1,297 +1,195 @@
|
||||
import {
|
||||
defineCommand,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
detectOutputFormat,
|
||||
unwrapResponse,
|
||||
MODEL_LIST_API,
|
||||
type Client,
|
||||
} from "bailian-cli-core";
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult, renderBoxTable } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
import { buildQuery, parseCommaList } from "../shared/params.ts";
|
||||
|
||||
const MONITOR_API = "zeldaEasy.bailian-telemetry.monitor.getMonitorData";
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types — mirror GET /api/v1/models/limits
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface QpmInfoItem {
|
||||
count_limit: number;
|
||||
count_limit_period: number;
|
||||
usage_limit: number;
|
||||
usage_limit_period: number;
|
||||
usage_limit_field: string;
|
||||
type: string;
|
||||
interface LimitSpec {
|
||||
request_limit: number | null;
|
||||
request_limit_period: number | null;
|
||||
usage_limit: number | null;
|
||||
usage_limit_field: string | null;
|
||||
usage_limit_period: number | null;
|
||||
async_user_queue_limit: number | null;
|
||||
async_user_concurrency_limit: number | null;
|
||||
}
|
||||
|
||||
interface ModelWithQpm {
|
||||
interface ModelQuota {
|
||||
model: string;
|
||||
qpmInfo?: Record<string, QpmInfoItem>;
|
||||
workspace_id?: string;
|
||||
model_limit?: LimitSpec | null;
|
||||
workspace_limit?: LimitSpec | null;
|
||||
}
|
||||
|
||||
interface MonitorPoint {
|
||||
value: number;
|
||||
timestamp: number;
|
||||
interface LimitsResponse {
|
||||
output?: {
|
||||
total?: number;
|
||||
page_no?: number;
|
||||
page_size?: number;
|
||||
quotas?: ModelQuota[];
|
||||
};
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
interface MonitorMetric {
|
||||
aggMethod: string;
|
||||
metricName: string;
|
||||
points: MonitorPoint[];
|
||||
// ---------------------------------------------------------------------------
|
||||
// Formatters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Compact rate display: `500/s`, `60/min`, `83,333/6s`; "-" when unlimited. */
|
||||
function formatLimit(limit: number | null | undefined, period: number | null | undefined): string {
|
||||
if (limit == null) return "-";
|
||||
const seconds = period ?? 60;
|
||||
if (seconds === 1) return `${formatNumber(limit)}/s`;
|
||||
if (seconds === 60) return `${formatNumber(limit)}/min`;
|
||||
return `${formatNumber(limit)}/${seconds}s`;
|
||||
}
|
||||
|
||||
function calculateRPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
|
||||
if (!item) return 0;
|
||||
const period = item.count_limit_period || fallbackPeriod;
|
||||
if (!period) return 0;
|
||||
return Math.floor((item.count_limit * 60) / period);
|
||||
function formatRequestLimit(spec: LimitSpec | null | undefined): string {
|
||||
if (!spec) return "-";
|
||||
return formatLimit(spec.request_limit, spec.request_limit_period);
|
||||
}
|
||||
|
||||
function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
|
||||
if (!item) return 0;
|
||||
const period = item.usage_limit_period || fallbackPeriod;
|
||||
if (!period) return 0;
|
||||
return Math.floor((item.usage_limit * 60) / period);
|
||||
function formatUsageLimit(spec: LimitSpec | null | undefined): string {
|
||||
if (!spec) return "-";
|
||||
return formatLimit(spec.usage_limit, spec.usage_limit_period);
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
async function fetchMonitorData(
|
||||
client: Client,
|
||||
modelName: string,
|
||||
windowMinutes: number,
|
||||
): Promise<{ rpm: number; tpm: number }> {
|
||||
const now = Date.now();
|
||||
const startTime = now - windowMinutes * 60 * 1000;
|
||||
|
||||
try {
|
||||
const raw = await client.console(MONITOR_API, {
|
||||
reqDTO: {
|
||||
monitorType: "Advanced",
|
||||
metricFilters: [
|
||||
{ aggMethod: "sum_pm", metricName: "model_total_amount" },
|
||||
{ aggMethod: "sum_pm", metricName: "model_call_count" },
|
||||
],
|
||||
labelFilters: {
|
||||
resourceId: modelName,
|
||||
resourceType: "model",
|
||||
},
|
||||
startTime,
|
||||
endTime: now,
|
||||
},
|
||||
});
|
||||
|
||||
const resp = unwrapResponse(raw as Record<string, unknown>);
|
||||
const metrics = (resp.data ?? resp) as MonitorMetric[] | Record<string, unknown>;
|
||||
if (!Array.isArray(metrics)) {
|
||||
return { rpm: 0, tpm: 0 };
|
||||
}
|
||||
|
||||
let rpm = 0;
|
||||
let tpm = 0;
|
||||
|
||||
for (const metric of metrics) {
|
||||
if (metric.aggMethod !== "sum_pm" || !metric.points?.length) continue;
|
||||
const lastValue = metric.points[metric.points.length - 1].value ?? 0;
|
||||
if (metric.metricName === "model_call_count") rpm = Math.round(lastValue);
|
||||
if (metric.metricName === "model_total_amount") tpm = Math.round(lastValue);
|
||||
}
|
||||
|
||||
return { rpm, tpm };
|
||||
} catch (error) {
|
||||
// Re-throw authentication errors (BailianError with ExitCode.AUTH);
|
||||
// other errors are treated as "no data" and show "-" in the table.
|
||||
if (error instanceof BailianError && error.exitCode === ExitCode.AUTH) {
|
||||
throw error;
|
||||
}
|
||||
return { rpm: -1, tpm: -1 };
|
||||
/** Async task headroom as `queue/concurrency`; "-" when the model has no async limits. */
|
||||
function formatAsync(spec: LimitSpec | null | undefined): string {
|
||||
if (!spec || (spec.async_user_queue_limit == null && spec.async_user_concurrency_limit == null)) {
|
||||
return "-";
|
||||
}
|
||||
const queue =
|
||||
spec.async_user_queue_limit != null ? formatNumber(spec.async_user_queue_limit) : "-";
|
||||
const concurrency =
|
||||
spec.async_user_concurrency_limit != null
|
||||
? formatNumber(spec.async_user_concurrency_limit)
|
||||
: "-";
|
||||
return `${queue}/${concurrency}`;
|
||||
}
|
||||
|
||||
async function fetchAllModelsWithQpm(client: Client): Promise<ModelWithQpm[]> {
|
||||
const allModels: ModelWithQpm[] = [];
|
||||
let pageNo = 1;
|
||||
|
||||
while (true) {
|
||||
const input: Record<string, unknown> = {
|
||||
pageNo,
|
||||
pageSize: 50,
|
||||
group: false,
|
||||
queryQpmInfo: true,
|
||||
ignoreWorkspaceServiceSite: true,
|
||||
supports: { selfServiceLimitIncrease: true },
|
||||
};
|
||||
|
||||
const raw = await client.console(MODEL_LIST_API, { input });
|
||||
|
||||
const resp = unwrapResponse(raw as Record<string, unknown>);
|
||||
const list = (resp.list as ModelWithQpm[]) ?? [];
|
||||
const total = (resp.total as number) ?? 0;
|
||||
|
||||
allModels.push(...list);
|
||||
if (allModels.length >= total || list.length === 0) break;
|
||||
pageNo++;
|
||||
function printTable(quotas: ModelQuota[], total: number): void {
|
||||
if (quotas.length === 0) {
|
||||
process.stdout.write("No rate limits found.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
return allModels;
|
||||
}
|
||||
|
||||
interface ListRow {
|
||||
model: string;
|
||||
rpm: string;
|
||||
tpm: string;
|
||||
rpmQuotaLeft: number | null;
|
||||
tpmQuotaLeft: number | null;
|
||||
rpmQuotaLabel: string | null;
|
||||
tpmQuotaLabel: string | null;
|
||||
}
|
||||
|
||||
function printTable(rows: ListRow[]): void {
|
||||
const headers = ["Model", "Req/min", "Token/min", "RPM Left", "TPM Left"];
|
||||
|
||||
const rpmPercents = rows.map((r) => r.rpmQuotaLeft);
|
||||
const rpmLabels = rows.map((r) => r.rpmQuotaLabel);
|
||||
const tpmPercents = rows.map((r) => r.tpmQuotaLeft);
|
||||
const tpmLabels = rows.map((r) => r.tpmQuotaLabel);
|
||||
|
||||
const tableRows = rows.map((r) => [r.model, r.rpm, r.tpm, "", ""]);
|
||||
|
||||
const headers = ["Model", "Req Limit", "Usage Limit", "WS Req", "WS Usage", "Async Q/C"];
|
||||
const rows = quotas.map((quota) => [
|
||||
quota.model,
|
||||
formatRequestLimit(quota.model_limit),
|
||||
formatUsageLimit(quota.model_limit),
|
||||
formatRequestLimit(quota.workspace_limit),
|
||||
formatUsageLimit(quota.workspace_limit),
|
||||
formatAsync(quota.model_limit),
|
||||
]);
|
||||
const lines = renderBoxTable({
|
||||
headers,
|
||||
rows: tableRows,
|
||||
align: ["left", "right", "right", "left", "left"],
|
||||
barColumns: [
|
||||
{ index: 3, percents: rpmPercents, labels: rpmLabels, width: 15 },
|
||||
{ index: 4, percents: tpmPercents, labels: tpmLabels, width: 15 },
|
||||
],
|
||||
rows,
|
||||
align: ["left", "right", "right", "right", "right", "right"],
|
||||
});
|
||||
|
||||
for (const line of lines) process.stdout.write(line + "\n");
|
||||
process.stdout.write(`\nTotal: ${total}\n`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default defineCommand({
|
||||
description: "View model RPM/TPM rate limits",
|
||||
auth: "console",
|
||||
usageArgs: "[--model <model>] [flags]",
|
||||
description: "View model rate limits (QPM/TPM, account and workspace level)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--model <model>] [--name <name>] [--page <n>] [--page-size <n>]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Model name(s), comma-separated",
|
||||
description: "Model name(s), comma-separated (exact match)",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Fuzzy search by model name",
|
||||
},
|
||||
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
|
||||
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 20)" },
|
||||
},
|
||||
exampleArgs: ["", "--model qwen3.6-plus", "--model qwen3.6-plus,qwen-turbo", "--output json"],
|
||||
exampleArgs: [
|
||||
"",
|
||||
"--model qwen3-max",
|
||||
"--model qwen3-max,qwen-plus",
|
||||
"--name qwen --page-size 50",
|
||||
"--output json",
|
||||
],
|
||||
notes: ["Usage-vs-limit pressure checks live in `quota check` (console auth)."],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const modelFlag = flags.model || undefined;
|
||||
const nameFlag = flags.name || undefined;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const endpoint = ctx.client.url(modelsLimitsPath());
|
||||
|
||||
if (settings.dryRun) {
|
||||
const input: Record<string, unknown> = {
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
group: false,
|
||||
queryQpmInfo: true,
|
||||
ignoreWorkspaceServiceSite: true,
|
||||
supports: { selfServiceLimitIncrease: true },
|
||||
};
|
||||
emitResult(
|
||||
{
|
||||
apis: [
|
||||
MODEL_LIST_API,
|
||||
{ api: MONITOR_API, note: "called per-model for text output with gauges" },
|
||||
],
|
||||
modelListInput: { input },
|
||||
},
|
||||
format,
|
||||
);
|
||||
if (modelFlag) {
|
||||
// One exact-match GET per model; dry-run lists them all.
|
||||
const requests = parseCommaList(modelFlag).map((model) => ({
|
||||
endpoint,
|
||||
method: "GET",
|
||||
query: { model, page_size: 100 },
|
||||
}));
|
||||
emitResult({ requests }, format);
|
||||
} else {
|
||||
emitResult(
|
||||
{
|
||||
endpoint,
|
||||
method: "GET",
|
||||
query: {
|
||||
name: nameFlag,
|
||||
page_no: flags.page || 1,
|
||||
page_size: flags.pageSize || 20,
|
||||
},
|
||||
},
|
||||
format,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let models = await fetchAllModelsWithQpm(ctx.client);
|
||||
let quotas: ModelQuota[];
|
||||
let total: number;
|
||||
|
||||
if (modelFlag) {
|
||||
const names = new Set(
|
||||
modelFlag
|
||||
.split(",")
|
||||
.map((n) => n.trim())
|
||||
.filter(Boolean),
|
||||
// Exact lookup per model, then merge.
|
||||
const responses = await Promise.all(
|
||||
parseCommaList(modelFlag).map((model) =>
|
||||
ctx.client.requestJson<LimitsResponse>({
|
||||
path: modelsLimitsPath() + buildQuery({ model, page_size: 100 }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
models = models.filter((m) => names.has(m.model));
|
||||
if (models.length === 0) {
|
||||
throw new BailianError(`no matching models found for "${modelFlag}".`);
|
||||
}
|
||||
quotas = responses.flatMap((resp) => resp.output?.quotas ?? []);
|
||||
total = quotas.length;
|
||||
} else {
|
||||
const resp = await ctx.client.requestJson<LimitsResponse>({
|
||||
path:
|
||||
modelsLimitsPath() +
|
||||
buildQuery({
|
||||
name: nameFlag,
|
||||
page_no: flags.page || 1,
|
||||
page_size: flags.pageSize || 20,
|
||||
}),
|
||||
});
|
||||
quotas = resp.output?.quotas ?? [];
|
||||
total = resp.output?.total ?? quotas.length;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
const items = models.map((m) => {
|
||||
const qpm = m.qpmInfo;
|
||||
const modelDefault = qpm?.["model-default"];
|
||||
const userSpec = qpm?.["user-spec"];
|
||||
|
||||
const defaultRPM = calculateRPM(modelDefault);
|
||||
const defaultTPM = calculateTPM(modelDefault);
|
||||
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
|
||||
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
|
||||
|
||||
return {
|
||||
model: m.model,
|
||||
rpm: currentRPM > 0 ? currentRPM : null,
|
||||
tpm: currentTPM > 0 ? currentTPM : null,
|
||||
};
|
||||
});
|
||||
emitResult(items, format);
|
||||
emitResult({ items: quotas, total }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
// For text output with gauges, we need monitor data
|
||||
const monitorResults = await Promise.all(
|
||||
models.map((m) => fetchMonitorData(ctx.client, m.model, 2)),
|
||||
);
|
||||
|
||||
const rows: ListRow[] = models.map((m, idx) => {
|
||||
const qpm = m.qpmInfo;
|
||||
const modelDefault = qpm?.["model-default"];
|
||||
const userSpec = qpm?.["user-spec"];
|
||||
|
||||
const defaultRPM = calculateRPM(modelDefault);
|
||||
const defaultTPM = calculateTPM(modelDefault);
|
||||
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
|
||||
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
|
||||
|
||||
const rpmUsage = monitorResults[idx].rpm;
|
||||
const tpmUsage = monitorResults[idx].tpm;
|
||||
|
||||
// RPM Quota Left = 1 - (rpmUsage / currentRPM) in percentage
|
||||
let rpmQuotaPercent: number | null = null;
|
||||
let rpmQuotaLabel: string | null = null;
|
||||
if (rpmUsage >= 0 && currentRPM > 0) {
|
||||
rpmQuotaPercent = Math.max(0, 100 - (rpmUsage / currentRPM) * 100);
|
||||
rpmQuotaLabel = rpmQuotaPercent.toFixed(1) + "%";
|
||||
}
|
||||
|
||||
// TPM Quota Left = 1 - (tpmUsage / currentTPM) in percentage
|
||||
let tpmQuotaPercent: number | null = null;
|
||||
let tpmQuotaLabel: string | null = null;
|
||||
if (tpmUsage >= 0 && currentTPM > 0) {
|
||||
tpmQuotaPercent = Math.max(0, 100 - (tpmUsage / currentTPM) * 100);
|
||||
tpmQuotaLabel = tpmQuotaPercent.toFixed(1) + "%";
|
||||
}
|
||||
|
||||
return {
|
||||
model: m.model,
|
||||
rpm: currentRPM > 0 ? formatNumber(currentRPM) : "-",
|
||||
tpm: currentTPM > 0 ? formatNumber(currentTPM) : "-",
|
||||
rpmQuotaLeft: rpmQuotaPercent,
|
||||
tpmQuotaLeft: tpmQuotaPercent,
|
||||
rpmQuotaLabel,
|
||||
tpmQuotaLabel,
|
||||
};
|
||||
});
|
||||
|
||||
if (rows.length === 0) {
|
||||
process.stdout.write("No models found.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printTable(rows);
|
||||
printTable(quotas, total);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import {
|
||||
defineCommand,
|
||||
UsageError,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
detectOutputFormat,
|
||||
type Client,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
|
||||
const UPDATE_LIMITS_API = "zeldaEasy.broadscope-platform.modelInstance.updateFoundationModelLimits";
|
||||
|
||||
interface QpmInfoItem {
|
||||
count_limit: number;
|
||||
count_limit_period: number;
|
||||
usage_limit: number;
|
||||
usage_limit_period: number;
|
||||
usage_limit_field: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
|
||||
if (!item) return 0;
|
||||
const period = item.usage_limit_period || fallbackPeriod;
|
||||
if (!period) return 0;
|
||||
return Math.floor((item.usage_limit * 60) / period);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const data = getNestedRecord(result, "data");
|
||||
if (!data) return result;
|
||||
const dataV2 = getNestedRecord(data, "DataV2");
|
||||
if (dataV2) {
|
||||
const inner = getNestedRecord(dataV2, "data");
|
||||
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
|
||||
return innerData ?? inner ?? dataV2;
|
||||
}
|
||||
const direct = getNestedRecord(data, "data");
|
||||
return direct ?? data;
|
||||
}
|
||||
|
||||
async function fetchModelQpmInfo(
|
||||
client: Client,
|
||||
modelName: string,
|
||||
): Promise<{ model: string; qpmInfo: Record<string, QpmInfoItem> } | undefined> {
|
||||
const raw = await client.console(MODEL_LIST_API, {
|
||||
input: {
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
name: modelName,
|
||||
group: false,
|
||||
queryQpmInfo: true,
|
||||
ignoreWorkspaceServiceSite: true,
|
||||
supports: { selfServiceLimitIncrease: true },
|
||||
},
|
||||
});
|
||||
|
||||
const resp = extractResponseData(raw as Record<string, unknown>);
|
||||
const list = (resp.list as Array<{ model: string; qpmInfo?: Record<string, QpmInfoItem> }>) ?? [];
|
||||
return list.find((m) => m.model === modelName && m.qpmInfo) as
|
||||
| { model: string; qpmInfo: Record<string, QpmInfoItem> }
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Request a temporary quota increase",
|
||||
auth: "console",
|
||||
usageArgs: "--model <model> --tpm <value> [flags]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Model name (required)",
|
||||
required: true,
|
||||
},
|
||||
tpm: {
|
||||
type: "string",
|
||||
valueHint: "<value>",
|
||||
description: "Target TPM value (required)",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-turbo --tpm 100000",
|
||||
"--model qwen3.6-plus --tpm 8000000",
|
||||
"--model qwen-turbo --tpm 100000 --output json",
|
||||
],
|
||||
validate: (f) => (Number(f.tpm) > 0 ? undefined : "--tpm must be a positive number."),
|
||||
async run(ctx) {
|
||||
const { identity, settings, flags } = ctx;
|
||||
const modelName = flags.model;
|
||||
const tpmValue = Number(flags.tpm);
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
const requestData = {
|
||||
input: {
|
||||
model: modelName,
|
||||
limit: { usage_limit: tpmValue },
|
||||
},
|
||||
};
|
||||
emitResult({ api: UPDATE_LIMITS_API, data: requestData }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelInfo = await fetchModelQpmInfo(ctx.client, modelName);
|
||||
if (!modelInfo) {
|
||||
throw new BailianError(
|
||||
`model "${modelName}" not found or does not support self-service quota increase.`,
|
||||
ExitCode.GENERAL,
|
||||
`Run \`${identity.binName} quota list\` to view available models.`,
|
||||
);
|
||||
}
|
||||
|
||||
const modelDefault = modelInfo.qpmInfo["model-default"];
|
||||
const userSpec = modelInfo.qpmInfo["user-spec"];
|
||||
const minLimit = calculateTPM(modelDefault);
|
||||
const currentLimit = calculateTPM(userSpec, modelDefault?.usage_limit_period) || minLimit;
|
||||
const maxLimit = minLimit * 2;
|
||||
|
||||
if (tpmValue < minLimit || tpmValue > maxLimit) {
|
||||
throw new UsageError(
|
||||
`TPM value ${tpmValue.toLocaleString()} is out of range. ` +
|
||||
`Current: ${currentLimit.toLocaleString()}, Range: ${minLimit.toLocaleString()} ~ ${maxLimit.toLocaleString()}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
input: {
|
||||
model: modelName,
|
||||
limit: { usage_limit: tpmValue },
|
||||
originalQpmInfo: modelInfo.qpmInfo,
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
|
||||
const submitRequest = async (confirmedDowngrade?: boolean): Promise<unknown> => {
|
||||
if (confirmedDowngrade) {
|
||||
requestData.input.confirmedDowngrade = true;
|
||||
}
|
||||
try {
|
||||
return await ctx.client.console(UPDATE_LIMITS_API, requestData);
|
||||
} catch (err) {
|
||||
if (err instanceof BailianError && err.message.includes("NotLogined")) {
|
||||
throw new BailianError(
|
||||
"session expired.",
|
||||
ExitCode.AUTH,
|
||||
`Run \`${identity.binName} auth login --console\` to re-authenticate.`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
let result = await submitRequest();
|
||||
const resp = extractResponseData(result as Record<string, unknown>);
|
||||
|
||||
if (resp.needConfirm) {
|
||||
const confirmCode = resp.confirmCode as string;
|
||||
|
||||
if (confirmCode === "Refresh_Required") {
|
||||
throw new BailianError("rate limit has been updated externally. Please retry.");
|
||||
}
|
||||
|
||||
if (confirmCode === "Downgrade") {
|
||||
result = await submitRequest(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(result, format);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`Quota updated for "${modelName}": TPM ${currentLimit.toLocaleString()} → ${tpmValue.toLocaleString()}\n`,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
|
||||
const MINUTE_SECONDS = 60;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Update model rate limits (QPM/TPM), or clear them with --delete",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Model name (required)",
|
||||
required: true,
|
||||
},
|
||||
rpm: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Max requests per minute (QPM)",
|
||||
},
|
||||
tpm: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Max tokens per minute (TPM)",
|
||||
},
|
||||
delete: {
|
||||
type: "switch",
|
||||
description: "Clear all custom rate limits for the model",
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus --rpm 60 --tpm 100000",
|
||||
"--model qwen3-max --tpm 500000",
|
||||
"--model qwen-plus --delete",
|
||||
"--model qwen-plus --rpm 60 --output json",
|
||||
],
|
||||
notes: [
|
||||
"Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.",
|
||||
"Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.",
|
||||
],
|
||||
validate: (flags) => {
|
||||
if (flags.delete && (flags.rpm !== undefined || flags.tpm !== undefined))
|
||||
return "--delete cannot be combined with --rpm/--tpm.";
|
||||
if (!flags.delete && flags.rpm === undefined && flags.tpm === undefined)
|
||||
return "one of --rpm / --tpm / --delete is required.";
|
||||
if (flags.rpm !== undefined && flags.rpm < 0) return "--rpm must be a non-negative number.";
|
||||
if (flags.tpm !== undefined && flags.tpm < 0) return "--tpm must be a non-negative number.";
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const modelName = flags.model;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
const entry: Record<string, unknown> = { model: modelName };
|
||||
if (flags.delete) {
|
||||
entry.operation_type = "DELETE";
|
||||
} else {
|
||||
if (flags.rpm !== undefined) {
|
||||
entry.request_limit = flags.rpm;
|
||||
entry.request_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
if (flags.tpm !== undefined) {
|
||||
entry.usage_limit = flags.tpm;
|
||||
entry.usage_limit_period = MINUTE_SECONDS;
|
||||
}
|
||||
}
|
||||
const body = { models: [entry] };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{ endpoint: ctx.client.url(modelsLimitsPath()), method: "POST", request: body },
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.requestJson<{ request_id?: string }>({
|
||||
path: modelsLimitsPath(),
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ model: modelName, ...result }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.delete) {
|
||||
process.stdout.write(`Rate limits cleared for "${modelName}".\n`);
|
||||
return;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (flags.rpm !== undefined) parts.push(`QPM ${formatNumber(flags.rpm)}`);
|
||||
if (flags.tpm !== undefined) parts.push(`TPM ${formatNumber(flags.tpm)}`);
|
||||
process.stdout.write(`Rate limits updated for "${modelName}": ${parts.join(", ")}\n`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Format an integer with en-US thousands separators for table / text output. */
|
||||
export function formatNumber(num: number): string {
|
||||
return num.toLocaleString("en-US");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Split a comma-separated flag value into trimmed, deduped, non-empty entries. */
|
||||
export function parseCommaList(value: string): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** Serialize defined, non-empty params into a `?key=value` query string ("" when empty). */
|
||||
export function buildQuery(params: Record<string, string | number | undefined>): string {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== "") search.set(key, String(value));
|
||||
}
|
||||
const queryString = search.toString();
|
||||
return queryString ? `?${queryString}` : "";
|
||||
}
|
||||
@@ -4,21 +4,12 @@ import {
|
||||
defineCommand,
|
||||
detectInstalledAgents,
|
||||
fetchSkillsIndex,
|
||||
getSkillRegistryBaseUrl,
|
||||
installSkillWithFanout,
|
||||
readSkillLock,
|
||||
runWithConcurrency,
|
||||
writeSkillLock,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
|
||||
|
||||
interface InitOutcome {
|
||||
name: string;
|
||||
status: "installed" | "failed";
|
||||
publishedAt?: string;
|
||||
agents?: string[];
|
||||
reason?: string;
|
||||
}
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
|
||||
/** Prefix used to identify first-party Bailian skills in the registry. */
|
||||
const BAILIAN_PREFIX = "bailian-";
|
||||
@@ -26,6 +17,22 @@ const BAILIAN_PREFIX = "bailian-";
|
||||
/** Max number of skills downloading/installing at the same time. */
|
||||
const INIT_CONCURRENCY = 3;
|
||||
|
||||
/** Default output format when user does not pass --output explicitly. */
|
||||
const DEFAULT_FORMAT = "json";
|
||||
|
||||
/** All status values used by skill init (per-skill outcome + aggregate result). */
|
||||
const STATUS = {
|
||||
success: "success",
|
||||
partial: "partial",
|
||||
failed: "failed",
|
||||
} as const;
|
||||
|
||||
interface InitOutcome {
|
||||
name: string;
|
||||
status: typeof STATUS.success | typeof STATUS.failed;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Install all bailian-* skills (one-shot bootstrap for new environments)",
|
||||
auth: "none",
|
||||
@@ -36,7 +43,7 @@ export default defineCommand({
|
||||
"Equivalent to: bl skill add --all (filtered to bailian-* skills)",
|
||||
],
|
||||
async run(ctx) {
|
||||
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
|
||||
const format = ctx.settings.outputExplicit ? ctx.settings.output : DEFAULT_FORMAT;
|
||||
const index = await fetchSkillsIndex();
|
||||
|
||||
// Discover all bailian-* skills from the live registry index
|
||||
@@ -55,16 +62,11 @@ export default defineCommand({
|
||||
lock.skills[name]?.links ?? [],
|
||||
);
|
||||
lock.skills[name] = record.lockEntry;
|
||||
return {
|
||||
name,
|
||||
status: "installed",
|
||||
publishedAt: entry.publishedAt,
|
||||
agents: record.linkedAgents,
|
||||
};
|
||||
return { name, status: STATUS.success };
|
||||
} catch (err) {
|
||||
return {
|
||||
name,
|
||||
status: "failed",
|
||||
status: STATUS.failed,
|
||||
reason: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
@@ -72,30 +74,46 @@ export default defineCommand({
|
||||
const results = await runWithConcurrency(tasks, INIT_CONCURRENCY);
|
||||
writeSkillLock(lock);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
registry: getSkillRegistryBaseUrl(),
|
||||
agents: agents.map((agent) => agent.id),
|
||||
skills: results,
|
||||
},
|
||||
format,
|
||||
);
|
||||
const installed = results.filter((result) => result.status === STATUS.success);
|
||||
const failed = results.filter((result) => result.status === STATUS.failed);
|
||||
|
||||
const status =
|
||||
failed.length === 0
|
||||
? STATUS.success
|
||||
: installed.length === 0
|
||||
? STATUS.failed
|
||||
: STATUS.partial;
|
||||
|
||||
if (format === DEFAULT_FORMAT) {
|
||||
const agentIds = agents.map((agent) => agent.id);
|
||||
const payload: Record<string, unknown> = {
|
||||
status,
|
||||
skills: installed.map((result) => result.name),
|
||||
};
|
||||
if (failed.length > 0) {
|
||||
payload.failed = failed.map((result) => ({
|
||||
name: result.name,
|
||||
reason: result.reason,
|
||||
agents: agentIds,
|
||||
}));
|
||||
}
|
||||
emitResult(payload, format);
|
||||
} else if (results.length === 0) {
|
||||
emitBare("No bailian-* skills found in the registry.");
|
||||
} else {
|
||||
const rows = results.map((result) => [
|
||||
result.name,
|
||||
result.status,
|
||||
result.publishedAt ? result.publishedAt.slice(0, 10) : "-",
|
||||
result.status === "installed" ? result.agents?.join(", ") || "-" : (result.reason ?? "-"),
|
||||
]);
|
||||
for (const line of formatTable(["NAME", "STATUS", "PUBLISHED", "AGENTS / REASON"], rows)) {
|
||||
emitBare(line);
|
||||
emitBare(
|
||||
status === STATUS.success
|
||||
? `Installed ${installed.length} bailian-* skills.`
|
||||
: `Installed ${installed.length}/${results.length} bailian-* skills.`,
|
||||
);
|
||||
if (failed.length > 0) {
|
||||
emitBare("Failed:");
|
||||
for (const item of failed) {
|
||||
emitBare(` ${item.name}: ${item.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const failed = results.filter((result) => result.status === "failed");
|
||||
if (failed.length > 0) {
|
||||
throw new BailianError(
|
||||
`${failed.length}/${results.length} skill(s) failed to install`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user