mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1c97940d | |||
| f1b6cacd7f | |||
| 9133b6bdd1 | |||
| 98ba3279fa | |||
| 3b7c4cfabc | |||
| d5d9fcb50f | |||
| 3ea2931152 | |||
| b402f3eacd | |||
| bedd59df27 | |||
| 39a488181e | |||
| 4dcec7d075 | |||
| daefc094ec | |||
| ae0c2c1213 | |||
| 01a62eb85b | |||
| 798ce596f6 | |||
| bd91e9d1c2 | |||
| e244771ee9 | |||
| 94f9dbbe9e | |||
| 8a0dd70206 | |||
| 9379da7a4c | |||
| ddcd564e61 | |||
| 0e4dd4b824 | |||
| 241de61866 | |||
| 61d9a74166 | |||
| 69eb759490 | |||
| 313966d7a9 | |||
| 4d84af614b | |||
| 24092b423c | |||
| 752a79e442 | |||
| 80bdcb83f6 |
@@ -123,7 +123,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### Console Login (OAuth)
|
||||
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, asset center, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### 控制台登录(OAuth)
|
||||
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、资产中心、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
|
||||
@@ -1,574 +0,0 @@
|
||||
# bailian-cli 快速上手指南
|
||||
|
||||
> 本文档面向新加入项目的开发者,帮助你理解 monorepo 的整体架构、代码组织方式和日常开发流程。
|
||||
> AI Agent 维护契约见根目录 [`AGENTS.md`](../AGENTS.md);各场景的详细清单见 [`docs/agents/`](agents/)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目是什么
|
||||
|
||||
**bailian-cli** 是阿里云百炼(DashScope / Model Studio)平台的命令行工具,让用户和 AI Agent 通过终端调用平台的全部 AI 能力:
|
||||
|
||||
- 文本/全模态对话、图像/视频生成与编辑、语音合成与识别
|
||||
- 知识库检索、记忆管理、应用调用、MCP 集成
|
||||
- 微调与部署、数据集管理、配额与业务空间
|
||||
- 控制台能力(用量统计、限流提额、资产中心等)
|
||||
|
||||
仓库以 **pnpm monorepo** 组织,产出两个 npm 产品:
|
||||
|
||||
| 产品 | 包名 | 二进制 | 定位 |
|
||||
| -------------- | ---------------------- | ---------------- | ------------------------------------ |
|
||||
| 百炼全量 CLI | `bailian-cli` | `bl` / `bailian` | 暴露全部命令 |
|
||||
| 知识库轻量 CLI | `knowledge-studio-cli` | `kscli` | 仅 config + knowledge 命令,路径拍平 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 类别 | 选型 |
|
||||
| --------- | -------------------------------------------------------------------------------------------- |
|
||||
| 语言 | TypeScript(strict) |
|
||||
| 运行时 | Node.js ≥ 22.12 |
|
||||
| 包管理 | pnpm 10 + workspace catalog |
|
||||
| 构建/测试 | [vite-plus](https://github.com/voidzero-dev/vite-plus)(`vp check` / `vp test` / `vp pack`) |
|
||||
| HTTP | undici(经 core client 封装) |
|
||||
| 模块 | ESM(`"type": "module"`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心架构:四层分层
|
||||
|
||||
项目按 **「纯逻辑 → 运行时框架 → 命令库 → 产品入口」** 严格分层,职责边界清晰:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 产品入口层 │
|
||||
│ packages/cli (bl) packages/kscli (kscli) │
|
||||
│ 决定命令路径 map、产品 identity、README、技能 reference │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ createCli(commands, identity)
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 运行时框架层 packages/runtime (bailian-cli-runtime) │
|
||||
│ 参数解析、命令树/registry、help、middleware、错误处理、输出 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ 调用 defineCommand 的 run()
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 命令库层 packages/commands (bailian-cli-commands) │
|
||||
│ 96+ 命令实现;只导出 command,不决定产品路径 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ client / settings / auth
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ 纯逻辑层 packages/core (bailian-cli-core) │
|
||||
│ 鉴权、配置、HTTP client、错误、类型、文件工具、领域 API │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 分层边界(必须遵守)
|
||||
|
||||
| 层 | 可以做 | 不能做 |
|
||||
| --------------- | --------------------------- | ------------------------------------------------------------- |
|
||||
| **core** | 纯库逻辑、HTTP、鉴权解析 | 依赖 runtime/commands;硬编码 `bl`/`kscli`;调 `process.exit` |
|
||||
| **runtime** | TTY、help、middleware、输出 | 写具体业务命令逻辑 |
|
||||
| **commands** | 命令元数据 + `run` 实现 | 决定产品路径;在 usage 里写 bin 前缀 |
|
||||
| **cli / kscli** | 命令路径 map、产品 identity | 不写命令业务逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 包详解
|
||||
|
||||
### 4.1 `packages/core` — `bailian-cli-core`
|
||||
|
||||
纯逻辑层,被所有上层依赖。主要模块:
|
||||
|
||||
```
|
||||
packages/core/src/
|
||||
├── auth/ # API Key / Console token 解析与落盘
|
||||
├── client/ # HTTP client、endpoints、MCP、流式解析
|
||||
├── config/ # ~/.bailian/config.json、Settings、来源优先级
|
||||
├── console/ # Console Gateway 调用
|
||||
├── dataset/ # 数据集校验(ChatML/DPO/CPT schema)
|
||||
├── finetune/ # 微调 API 与能力探测
|
||||
├── deploy/ # 部署 API
|
||||
├── advisor/ # 模型推荐(意图识别 + 召回)
|
||||
├── errors/ # BailianError、UsageError、退出码
|
||||
├── output/ # JSON/text 格式化(命令层也可用 runtime 的 emit)
|
||||
├── files/ # 本地文件上传、URL 解析
|
||||
├── telemetry/ # 命令执行遥测
|
||||
└── types/ # Command、FlagsDef、defineCommand
|
||||
```
|
||||
|
||||
**关键类型** — 每个命令通过 `defineCommand` 声明:
|
||||
|
||||
```typescript
|
||||
defineCommand({
|
||||
description: "…",
|
||||
auth: "apiKey" | "console" | "none",
|
||||
flags: {
|
||||
/* camelCase key → kebab-case CLI flag */
|
||||
},
|
||||
usageArgs: "--prompt <text> [flags]", // 不含 bl/kscli 前缀
|
||||
exampleArgs: ['--prompt "hello"'],
|
||||
validate: (flags) => string | undefined, // 跨 flag 校验
|
||||
run: async (ctx) => {
|
||||
/* ctx.client / ctx.flags / ctx.settings */
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Client** 是命令的网络入口,凭证已注入,命令层不碰 token:
|
||||
|
||||
```typescript
|
||||
ctx.client.requestJson({ path: "/…", method: "POST", body });
|
||||
ctx.client.console({ product: "…", action: "…", params });
|
||||
ctx.client.uploadFile(localPath);
|
||||
ctx.client.mcp(…);
|
||||
```
|
||||
|
||||
### 4.2 `packages/runtime` — `bailian-cli-runtime`
|
||||
|
||||
通用 CLI 框架,与具体业务无关。核心文件:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ------------------ | ----------------------------------------------- |
|
||||
| `create-cli.ts` | 入口工厂:`createCli(commands, identity).run()` |
|
||||
| `registry.ts` | 从 `Record<string, AnyCommand>` 建树,动态 help |
|
||||
| `args.ts` | 路径 + flag 解析 |
|
||||
| `middleware.ts` | auth → telemetry → versionCheck → runCommand |
|
||||
| `error-handler.ts` | 统一错误输出与退出码 |
|
||||
| `urls.ts` | 用户面控制台 URL(非 API endpoint) |
|
||||
| `output/` | 颜色、表格、进度条、banner |
|
||||
| `pipeline/` | 多步 pipeline 编排(`bl pipeline run`) |
|
||||
|
||||
**Middleware 流水线**(洋葱模型):
|
||||
|
||||
```
|
||||
argv 解析
|
||||
→ authStage 按 command.auth 注入 apiKey / console 凭证到 ctx.client
|
||||
→ telemetryStage 记录命令执行
|
||||
→ versionCheckStage 检查 npm 更新
|
||||
→ runCommandStage 调用 command.run(ctx)
|
||||
```
|
||||
|
||||
### 4.3 `packages/commands` — `bailian-cli-commands`
|
||||
|
||||
命令实现库,按**能力域**组织目录(≠ 最终 CLI 路径):
|
||||
|
||||
```
|
||||
packages/commands/src/commands/
|
||||
├── text/ # 文本对话
|
||||
├── omni/ # 全模态对话
|
||||
├── image/ # 图像生成/编辑
|
||||
├── video/ # 视频生成/编辑/下载
|
||||
├── speech/ # 语音合成/识别
|
||||
├── vision/ # 图像/视频理解
|
||||
├── knowledge/ # 知识库检索/搜索/对话
|
||||
├── memory/ # 记忆管理
|
||||
├── app/ # 应用调用
|
||||
├── mcp/ # MCP 服务
|
||||
├── auth/ # 登录/登出/状态
|
||||
├── config/ # 配置读写
|
||||
├── console/ # 通用 Console Gateway 调用
|
||||
├── dataset/ # 数据集上传/校验
|
||||
├── finetune/ # 微调任务
|
||||
├── deploy/ # 模型部署
|
||||
├── quota/ # 限流与提额
|
||||
├── workspace/ # 业务空间
|
||||
├── usage/ # 用量统计
|
||||
├── advisor/ # 模型推荐
|
||||
├── asset-center/ # 资产中心(新)
|
||||
├── pipeline/ # Pipeline 编排
|
||||
├── search/ # 联网搜索
|
||||
├── file/ # 文件上传
|
||||
├── token-plan/ # Token 计划
|
||||
└── update.ts # 自更新
|
||||
```
|
||||
|
||||
每个命令文件 `export default defineCommand(…)`,并在 `packages/commands/src/index.ts` 具名 re-export。
|
||||
|
||||
### 4.4 `packages/cli` — `bailian-cli`(`bl`)
|
||||
|
||||
产品入口,极薄:
|
||||
|
||||
```typescript
|
||||
// packages/cli/src/main.ts
|
||||
createCli(commands, {
|
||||
binName: "bl",
|
||||
version: pkg.version,
|
||||
clientName: "bailian-cli",
|
||||
npmPackage: "bailian-cli",
|
||||
}).run();
|
||||
```
|
||||
|
||||
**命令路径由 `packages/cli/src/commands.ts` 决定**,例如:
|
||||
|
||||
```typescript
|
||||
export const commands: Record<string, AnyCommand> = {
|
||||
"text chat": textChat,
|
||||
"asset-center list": assetList,
|
||||
"finetune create": finetuneCreate,
|
||||
update, // 单级命令 key 即路径
|
||||
};
|
||||
```
|
||||
|
||||
此文件还被 `tools/generate-reference.ts` 读取,生成 Agent Skill 参考文档。
|
||||
|
||||
### 4.5 `packages/kscli` — `knowledge-studio-cli`(`kscli`)
|
||||
|
||||
轻量 RAG 产品,**复用同一套 commands**,但路径拍平:
|
||||
|
||||
```typescript
|
||||
const commands = {
|
||||
retrieve: knowledgeRetrieve, // ↔ bl knowledge retrieve
|
||||
search: knowledgeSearch, // ↔ bl knowledge search
|
||||
chat: knowledgeChat, // ↔ bl knowledge chat
|
||||
"config show": configShow,
|
||||
update,
|
||||
};
|
||||
```
|
||||
|
||||
同一个 `knowledgeRetrieve` 实现,在 `bl` 显示 `bl knowledge retrieve`,在 `kscli` 显示 `kscli retrieve`——路径完全由产品入口 map 的 key 决定。
|
||||
|
||||
---
|
||||
|
||||
## 5. 一次命令执行的完整链路
|
||||
|
||||
以 `bl text chat --message "hi"` 为例:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant main as cli/main.ts
|
||||
participant createCli as runtime/create-cli.ts
|
||||
participant registry as runtime/registry.ts
|
||||
participant mw as middleware
|
||||
participant cmd as commands/text/chat.ts
|
||||
participant client as core/client
|
||||
|
||||
User->>main: bl text chat --message "hi"
|
||||
main->>createCli: createCli(commands, identity).run(argv)
|
||||
createCli->>registry: 解析路径 ["text","chat"]
|
||||
registry-->>createCli: 匹配 textChat command
|
||||
createCli->>mw: authStage → 注入 apiKey 到 client
|
||||
mw->>cmd: run(ctx)
|
||||
cmd->>client: requestJson / parseSSE
|
||||
client-->>User: stdout 输出
|
||||
```
|
||||
|
||||
**配置与凭证解析优先级**(core 统一处理,命令不介入):
|
||||
|
||||
| 来源 | API Key | Console Token |
|
||||
| ---- | ----------------------- | ------------------------------------- |
|
||||
| 1 | `--api-key` flag | `~/.bailian/config.json` access_token |
|
||||
| 2 | `DASHSCOPE_API_KEY` env | — |
|
||||
| 3 | config.json `api_key` | — |
|
||||
|
||||
Console 命令额外有 `--console-region`、`--workspace-id` 等 flag(由 runtime 按 `auth: "console"` 自动展示)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 鉴权域
|
||||
|
||||
每个命令声明 `auth` 字段,runtime 自动处理:
|
||||
|
||||
| auth 值 | 适用场景 | 凭证来源 | 网络方法 |
|
||||
| ----------- | ------------------------------ | -------------------- | -------------------------------- |
|
||||
| `"apiKey"` | DashScope API(模型推理等) | API Key | `client.request` / `requestJson` |
|
||||
| `"console"` | Console Gateway(控制台能力) | Console access token | `client.console` |
|
||||
| `"none"` | 纯本地(config、update、help) | 无 | 可选 credential-less client |
|
||||
|
||||
**规则**:调用 Console Gateway 的命令必须 `auth: "console"`,且**不要**重复声明 console 凭证域 flags。
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理约定
|
||||
|
||||
CLI **只翻译自己能权威解释的错误**,服务端错误原样透传:
|
||||
|
||||
| 错误来源 | 处理 |
|
||||
| ---------------------- | -------------------------------- |
|
||||
| 缺参、flag 校验 | `UsageError` → 退出码 2 |
|
||||
| 本地无凭证 | `BailianError(AUTH)` |
|
||||
| 网络/DNS/TLS | `BailianError(NETWORK)` |
|
||||
| HTTP 4xx/5xx、业务错码 | message **原样透传**,不二次包装 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 开发工作流
|
||||
|
||||
### 8.1 环境准备
|
||||
|
||||
```bash
|
||||
# 要求 Node >= 22.12, pnpm >= 10
|
||||
pnpm install
|
||||
|
||||
# 格式化 + lint + 类型检查
|
||||
pnpm run check # 或 vp check
|
||||
|
||||
# 本地跑 bl(tsx 直跑,无需 build)
|
||||
pnpm run bl -- text chat --help
|
||||
pnpm run kscli -- search --help
|
||||
|
||||
# 全量测试
|
||||
pnpm test # 或 vp test
|
||||
|
||||
# 构建所有包
|
||||
pnpm run ready # check + test + build
|
||||
```
|
||||
|
||||
### 8.2 新增一个 `bl` 命令(最小路径)
|
||||
|
||||
假设新增 `bl widget do`:
|
||||
|
||||
**Step 1** — 实现命令(`packages/commands`)
|
||||
|
||||
```bash
|
||||
# 新建
|
||||
packages/commands/src/commands/widget/do.ts
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { defineCommand, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const FLAGS = {
|
||||
name: { type: "string", valueHint: "<name>", description: "Widget name", required: true },
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: "Do something with a widget",
|
||||
auth: "apiKey", // 或 "console" / "none"
|
||||
flags: FLAGS,
|
||||
usageArgs: "--name <name>",
|
||||
exampleArgs: ['--name "demo"'],
|
||||
async run(ctx) {
|
||||
const data = await ctx.client.requestJson({ path: "/…", method: "POST", body: { … } });
|
||||
emitResult(ctx, data);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2** — 导出(`packages/commands/src/index.ts`)
|
||||
|
||||
```typescript
|
||||
export { default as widgetDo } from "./commands/widget/do.ts";
|
||||
```
|
||||
|
||||
**Step 3** — 注册产品路径(`packages/cli/src/commands.ts`)
|
||||
|
||||
```typescript
|
||||
import { widgetDo } from "bailian-cli-commands";
|
||||
// …
|
||||
"widget do": widgetDo,
|
||||
```
|
||||
|
||||
**Step 4** — E2E 测试(`packages/cli/tests/e2e/widget.e2e.test.ts`)
|
||||
|
||||
见 [docs/agents/cli-e2e-tests.md](agents/cli-e2e-tests.md):至少覆盖分组 help、`--help`、缺参用例。
|
||||
|
||||
**Step 5** — 验证
|
||||
|
||||
```bash
|
||||
vp check
|
||||
vp test
|
||||
pnpm run bl -- widget do --help
|
||||
```
|
||||
|
||||
> 若 `kscli` 也需要暴露:在 `packages/kscli/src/main.ts` 的 map 里加 key。
|
||||
> 技能 reference 会在 pre-commit 时由 `generate-reference.ts` 自动从 `commands.ts` 生成。
|
||||
|
||||
详细清单 → [docs/agents/command-add-remove.md](agents/command-add-remove.md)
|
||||
|
||||
### 8.3 给已有命令加 flag
|
||||
|
||||
→ [docs/agents/command-flag-change.md](agents/command-flag-change.md)
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试体系
|
||||
|
||||
```
|
||||
packages/cli/tests/
|
||||
├── e2e/ # 33 个 e2e 测试文件
|
||||
│ ├── helpers.ts # runCli、环境变量 readiness 判断
|
||||
│ ├── global-setup.ts
|
||||
│ └── <topic>.e2e.test.ts
|
||||
└── stress/ # 多能力并发压测
|
||||
├── run.mjs
|
||||
└── targets/
|
||||
```
|
||||
|
||||
**E2E 双层结构**(固定模式):
|
||||
|
||||
```typescript
|
||||
// 层 1:永远跑 — help / 分组,无需 API Key
|
||||
describe("e2e: asset-center", () => {
|
||||
test("asset-center 分组展示子命令帮助且成功退出", …);
|
||||
test("asset-center list --help 正常退出", …);
|
||||
});
|
||||
|
||||
// 层 2:skipIf 缺凭证 — dry-run / 真实集成
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: asset-center(Console …)", () => {
|
||||
test("缺少 --asset-id 时退出为用法错误 (2)", …);
|
||||
test("真实 list 流程", …);
|
||||
});
|
||||
```
|
||||
|
||||
环境变量(常用):
|
||||
|
||||
| 变量 | 用途 |
|
||||
| --------------------------------------------- | ------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | 模型 API 集成测试 |
|
||||
| Console token(经 `bl auth login --console`) | 控制台命令测试 |
|
||||
| `BAILIAN_E2E_*` | 各能力开关(视频/媒体等) |
|
||||
|
||||
压测:`pnpm run test:stress`
|
||||
|
||||
---
|
||||
|
||||
## 10. 命令能力地图(`bl` 全量)
|
||||
|
||||
当前 `packages/cli/src/commands.ts` 注册的命令组:
|
||||
|
||||
| 命令组 | 子命令示例 | auth 域 |
|
||||
| -------------- | ------------------------------------------------------------------------------- | ---------------- |
|
||||
| `auth` | login, status, logout | none / console |
|
||||
| `text` | chat | apiKey |
|
||||
| `omni` | (全模态对话) | apiKey |
|
||||
| `image` | generate, edit | apiKey |
|
||||
| `video` | generate, edit, ref, task get, download | apiKey |
|
||||
| `vision` | describe | apiKey |
|
||||
| `speech` | synthesize, recognize | apiKey |
|
||||
| `knowledge` | retrieve, search, chat | apiKey |
|
||||
| `memory` | add, search, list, update, delete, profile create/get | apiKey |
|
||||
| `app` | call, list | apiKey / console |
|
||||
| `mcp` | call, list, tools | apiKey |
|
||||
| `search` | web | apiKey |
|
||||
| `file` | upload | apiKey |
|
||||
| `config` | show, set | none |
|
||||
| `console` | call | console |
|
||||
| `usage` | free, freetier, stats | console |
|
||||
| `workspace` | list | console |
|
||||
| `quota` | list, request, history, check | console |
|
||||
| `dataset` | upload, list, get, delete, validate | console |
|
||||
| `finetune` | create, list, get, cancel, delete, logs, checkpoints, export, watch, capability | console |
|
||||
| `deploy` | create, list, get, models, scale, update, delete | console |
|
||||
| `token-plan` | list-seats, create-key, assign-seats, add-member | console |
|
||||
| `asset-center` | list, get, favorite, unfavorite, delete, download, stats, storage | console |
|
||||
| `pipeline` | run, validate | apiKey |
|
||||
| `advisor` | recommend | apiKey |
|
||||
| `update` | (自更新) | none |
|
||||
|
||||
---
|
||||
|
||||
## 11. 非代码资产
|
||||
|
||||
```
|
||||
tools/
|
||||
├── generate-reference.ts # 从 cli/commands.ts → skills/bailian-cli/reference/
|
||||
├── sync-skill-metadata.ts # 同步 SKILL.md 版本号
|
||||
└── release/ # CI 发版自动化
|
||||
|
||||
skills/bailian-cli/ # Agent Skill(npx skills add modelstudioai/cli)
|
||||
.github/workflows/ # CI/CD(publish.yml 等)
|
||||
docs/agents/ # 各维护场景的 AI 清单
|
||||
```
|
||||
|
||||
根脚本:
|
||||
|
||||
```bash
|
||||
pnpm run sync:skill-assets # build + 生成 reference + 同步版本
|
||||
pnpm run release:check # 发版前校验
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 发布
|
||||
|
||||
- 版本号:`packages/core`、`runtime`、`commands`、`cli`、`kscli` **保持同步**
|
||||
- 发布范围:`tools/release/lib/packages.mjs` 定义
|
||||
- `bailian-cli` 走常规定义发布;`knowledge-studio-cli` 走 `--knowledge` 通道
|
||||
- 详见 [docs/agents/publish.md](agents/publish.md)
|
||||
|
||||
---
|
||||
|
||||
## 13. 关键文件速查
|
||||
|
||||
| 我想… | 看这里 |
|
||||
| ------------------- | --------------------------------------- |
|
||||
| 了解项目契约 | `AGENTS.md` |
|
||||
| 改 `bl` 命令路径 | `packages/cli/src/commands.ts` |
|
||||
| 写/改命令逻辑 | `packages/commands/src/commands/<域>/` |
|
||||
| 导出命令 | `packages/commands/src/index.ts` |
|
||||
| 改 CLI 框架行为 | `packages/runtime/src/` |
|
||||
| 改 HTTP/鉴权/配置 | `packages/core/src/` |
|
||||
| 改 kscli 路径 | `packages/kscli/src/main.ts` |
|
||||
| 加 E2E 测试 | `packages/cli/tests/e2e/` |
|
||||
| 改控制台 URL | `packages/runtime/src/urls.ts` |
|
||||
| 改 API endpoint | `packages/core/src/client/endpoints.ts` |
|
||||
| 改配置 schema | `packages/core/src/config/schema.ts` |
|
||||
| 生成 Agent 参考文档 | `tools/generate-reference.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 14. 场景导航(维护清单)
|
||||
|
||||
| 场景 | 文档 |
|
||||
| ----------- | ------------------------------------------------------- |
|
||||
| 命令增删改 | [command-add-remove.md](agents/command-add-remove.md) |
|
||||
| E2E 测试 | [cli-e2e-tests.md](agents/cli-e2e-tests.md) |
|
||||
| 加/改 flag | [command-flag-change.md](agents/command-flag-change.md) |
|
||||
| 模型上下架 | [model-add-remove.md](agents/model-add-remove.md) |
|
||||
| 错误文案 | [error-hint-change.md](agents/error-hint-change.md) |
|
||||
| 鉴权扩展 | [auth-change.md](agents/auth-change.md) |
|
||||
| 配置项扩展 | [config-add.md](agents/config-add.md) |
|
||||
| 发布 | [publish.md](agents/publish.md) |
|
||||
| 工具链/lint | [lint-toolchain.md](agents/lint-toolchain.md) |
|
||||
|
||||
---
|
||||
|
||||
## 15. 架构设计要点(读懂代码的钥匙)
|
||||
|
||||
1. **命令实现 ≠ 产品路径** — 同一 `knowledgeRetrieve` 可以是 `bl knowledge retrieve` 或 `kscli retrieve`。
|
||||
2. **defineCommand 是契约** — `auth` + `flags` + `run(ctx)` 是命令的全部接口;凭证和网络细节下沉到 core/runtime。
|
||||
3. **registry 从 map 建树** — `"asset-center list"` 等 path 自动变成命令组,help 动态生成。
|
||||
4. **flags 用 camelCase 定义** — runtime 渲染为 `--kebab-case`;`ParsedFlags<typeof FLAGS>` 提供类型安全。
|
||||
5. **dry-run 是全局 flag** — `--dry-run` 在 auth stage 有例外处理,命令在 `run` 开头判断 `ctx.settings.dryRun`。
|
||||
6. **本地路径即 URL** — 所有接受 URL 的参数同时支持本地文件路径,core `files/upload` 自动上传。
|
||||
7. **Console Gateway 统一入口** — 控制台 API 走 `client.console({ product, action, params })`,不散落 raw fetch。
|
||||
|
||||
---
|
||||
|
||||
## 16. 本地配置速览
|
||||
|
||||
配置文件:`~/.bailian/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"api_key": "sk-…",
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"access_token": "…",
|
||||
"console_region": "cn-beijing",
|
||||
"console_site": "domestic"
|
||||
}
|
||||
```
|
||||
|
||||
常用环境变量:
|
||||
|
||||
| 变量 | 说明 |
|
||||
| ---------------------------- | -------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | 模型 API Key |
|
||||
| `DASHSCOPE_BASE_URL` | API Base URL |
|
||||
| `BAILIAN_WORKSPACE_ID` | 业务空间 ID |
|
||||
| `HTTP_PROXY` / `HTTPS_PROXY` | 代理(runtime 启动时读取) |
|
||||
|
||||
登录:
|
||||
|
||||
```bash
|
||||
bl auth login # API Key
|
||||
bl auth login --console # Console token(扫码)
|
||||
bl auth status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_文档版本:基于仓库当前结构(含 `asset-center`、`kscli`);`packages/rag` 已演进为 `packages/kscli`。_
|
||||
@@ -123,7 +123,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### Console Login (OAuth)
|
||||
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, asset center, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
|
||||
@@ -122,7 +122,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
|
||||
### 控制台登录(OAuth)
|
||||
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、资产中心、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
usageFreetier,
|
||||
usageStats,
|
||||
usageSummary,
|
||||
usageTokenPlan,
|
||||
usageCodingPlan,
|
||||
pipelineRun,
|
||||
pipelineValidate,
|
||||
advisorRecommend,
|
||||
@@ -84,14 +86,6 @@ import {
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
assetList,
|
||||
assetGet,
|
||||
assetFavorite,
|
||||
assetUnfavorite,
|
||||
assetDelete,
|
||||
assetDownload,
|
||||
assetStats,
|
||||
assetStorage,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -172,6 +166,8 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"usage freetier": usageFreetier,
|
||||
"usage stats": usageStats,
|
||||
"usage summary": usageSummary,
|
||||
"usage token-plan": usageTokenPlan,
|
||||
"usage coding-plan": usageCodingPlan,
|
||||
"pipeline run": pipelineRun,
|
||||
"pipeline validate": pipelineValidate,
|
||||
"advisor recommend": advisorRecommend,
|
||||
@@ -211,14 +207,6 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
"token-plan add-member": tokenPlanAddMember,
|
||||
"asset-center list": assetList,
|
||||
"asset-center get": assetGet,
|
||||
"asset-center favorite": assetFavorite,
|
||||
"asset-center unfavorite": assetUnfavorite,
|
||||
"asset-center delete": assetDelete,
|
||||
"asset-center download": assetDownload,
|
||||
"asset-center stats": assetStats,
|
||||
"asset-center storage": assetStorage,
|
||||
"workspace init": workspaceInit,
|
||||
"plugin install": pluginInstall,
|
||||
"plugin link": pluginLink,
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
|
||||
describe("e2e: asset-center", () => {
|
||||
test("asset-center 分组展示子命令帮助且成功退出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["asset-center"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const output = `${stdout}\n${stderr}`;
|
||||
expect(output).toContain("list");
|
||||
expect(output).toContain("storage");
|
||||
expect(output).not.toContain("oss");
|
||||
});
|
||||
|
||||
test("asset-center list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--type");
|
||||
expect(stderr).toContain("--recycle-bin");
|
||||
expect(stderr).toContain("bl asset-center list");
|
||||
});
|
||||
|
||||
test("asset-center get --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "get", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--asset-id");
|
||||
});
|
||||
|
||||
test("asset-center favorite --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--id");
|
||||
});
|
||||
|
||||
test("asset-center delete --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "delete", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--permanent");
|
||||
});
|
||||
|
||||
test("asset-center download --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "download", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--id");
|
||||
expect(stderr).not.toMatch(/(^|\s)--out(\s|$)/);
|
||||
});
|
||||
|
||||
test("asset-center stats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--sync-failed");
|
||||
});
|
||||
|
||||
test("asset-center storage --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "storage", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl asset-center storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: asset-center(Console)", () => {
|
||||
test("asset-center get 缺少 --asset-id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "get", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--asset-id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center favorite 缺少 --id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center download 缺少 --id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["asset-center", "download", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--id|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("asset-center list --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"list",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { deleteStatus?: string };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("listModelGeneratedAsset");
|
||||
expect(data.data?.deleteStatus).toBe("NORMAL");
|
||||
});
|
||||
|
||||
test("asset-center stats --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"stats",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string }>(stdout);
|
||||
expect(data.api).toContain("countModelGeneratedAsset");
|
||||
});
|
||||
|
||||
test("asset-center storage --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"asset-center",
|
||||
"storage",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string }>(stdout);
|
||||
expect(data.api).toContain("getStorageQuota");
|
||||
});
|
||||
|
||||
test("【console】asset-center list 真实调用或鉴权失败优雅退出", async () => {
|
||||
const workspaceId = process.env.BAILIAN_WORKSPACE_ID;
|
||||
const args = ["asset-center", "list", "--output", "json", "--page-size", "1"];
|
||||
if (workspaceId) args.push("--workspace-id", workspaceId);
|
||||
|
||||
const result = await runCli(args);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,15 @@ const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl 与全局 flag", async () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
expect(stderr).toMatch(/token-plan create-key\s+\[AK\/SK\]\s+Create a Token Plan API key/);
|
||||
expect(stderr).toMatch(/config show\s+\[No Auth\]\s+Display current configuration/);
|
||||
expect(stderr).toMatch(/--base-url/);
|
||||
expect(stderr).toMatch(/--console-region/);
|
||||
expect(stderr).toMatch(/--console-site/);
|
||||
@@ -18,6 +23,24 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/^\s*--region\s/m);
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[["text", "chat"], "API Key"],
|
||||
[["app", "list"], "Console"],
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
# 资产中心 CLI 命令树设计
|
||||
|
||||
> 本文档定义 `bl asset` 命令族的路径结构、help 层级、flags 概览与示例。
|
||||
> 技术实现细节见 [DESIGN.md](./DESIGN.md);API 字段见 [api-doc.md](./api-doc.md)。
|
||||
|
||||
## 1. 命名原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
| ------------ | ------------------------------------------------------------------------- |
|
||||
| 产品路径前缀 | `asset`(不用 `asset-center`,与 `deploy` / `dataset` 等产品域一致) |
|
||||
| 层级深度 | 最多三级:`asset <group> <action>` |
|
||||
| 子组条件 | 仅当子组下 ≥ 2 个 action 时使用子组(见 AGENTS.md) |
|
||||
| bin 前缀 | `usageArgs` / `exampleArgs` 不写 `bl`;help 由 runtime 按路径补全 |
|
||||
| 鉴权 | 全部 `auth: "console"`;自动可见 `--console-region` 等 CONSOLE_AUTH_FLAGS |
|
||||
|
||||
---
|
||||
|
||||
## 2. 命令树总览
|
||||
|
||||
```
|
||||
bl asset
|
||||
│
|
||||
├── list # 分页查询资产列表
|
||||
├── get <asset-id> # 查询单个资产详情
|
||||
├── favorite # 收藏资产
|
||||
├── unfavorite # 取消收藏
|
||||
├── delete # 删除资产(默认软删到回收站)
|
||||
├── restore # 从回收站恢复
|
||||
├── download # 获取下载链接 / 可选落盘
|
||||
├── stats # 资产数量统计
|
||||
├── storage # 存储容量与配额
|
||||
│
|
||||
├── models # [P1] 模型列表(辅助筛选)
|
||||
│ └── list
|
||||
│
|
||||
├── service # [P1/P2] 服务开通状态
|
||||
│ ├── status
|
||||
│ ├── enable # [P2]
|
||||
│ └── disable # [P2]
|
||||
│
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品入口注册 Map
|
||||
|
||||
`packages/cli/src/commands.ts` 中预期注册(camelCase export → kebab path):
|
||||
|
||||
| Map Key | Export 名(建议) | Phase |
|
||||
| ------------------------- | --------------------- | ----- |
|
||||
| `"asset list"` | `assetList` | 1 |
|
||||
| `"asset get"` | `assetGet` | 1 |
|
||||
| `"asset favorite"` | `assetFavorite` | 1 |
|
||||
| `"asset unfavorite"` | `assetUnfavorite` | 1 |
|
||||
| `"asset delete"` | `assetDelete` | 1 |
|
||||
| `"asset restore"` | `assetRestore` | 1 |
|
||||
| `"asset download"` | `assetDownload` | 1 |
|
||||
| `"asset stats"` | `assetStats` | 1 |
|
||||
| `"asset storage"` | `assetStorage` | 1 |
|
||||
| `"asset models list"` | `assetModelsList` | 2 |
|
||||
| `"asset service status"` | `assetServiceStatus` | 2 |
|
||||
| `"asset service enable"` | `assetServiceEnable` | 3 |
|
||||
| `"asset service disable"` | `assetServiceDisable` | 3 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Help 层级预览
|
||||
|
||||
### 4.1 顶层分组
|
||||
|
||||
```
|
||||
$ bl asset
|
||||
Asset management commands for Bailian Asset Center.
|
||||
|
||||
Commands:
|
||||
list List model-generated assets
|
||||
get Get asset details by ID
|
||||
favorite Mark assets as favorites
|
||||
unfavorite Remove assets from favorites
|
||||
delete Delete assets (soft delete by default)
|
||||
restore Restore soft-deleted assets
|
||||
download Get asset download URLs
|
||||
stats Count assets by type
|
||||
storage View storage quota and usage
|
||||
models Model configuration helpers
|
||||
service Asset center service subscription
|
||||
|
||||
Run `bl asset <command> --help` for details.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 各命令规格
|
||||
|
||||
以下 `usageArgs` 为命令 metadata 中的值(不含 global flags)。Global flags(`--output`、`--dry-run`、`--quiet` 等)与 console flags(`--workspace-id` 等)由 runtime 自动追加到 help。
|
||||
|
||||
---
|
||||
|
||||
### 5.1 Phase 1 命令
|
||||
|
||||
#### `bl asset list`
|
||||
|
||||
```
|
||||
Description: List model-generated assets with filters and cursor pagination
|
||||
|
||||
Usage: bl asset list [flags]
|
||||
|
||||
Flags:
|
||||
--type <type> Asset type: IMAGE, VIDEO, AUDIO
|
||||
--model <name> Filter by model name
|
||||
--keyword <text> Filter by asset name (substring)
|
||||
--favorited Show only favorited assets
|
||||
--recycle-bin Show soft-deleted assets (recycle bin)
|
||||
--sync-status <status> OSS sync status filter
|
||||
--begin-time <datetime> Filter by generate time start (ISO_LOCAL_DATE_TIME)
|
||||
--end-time <datetime> Filter by generate time end
|
||||
--include-download-url Include signed download URLs
|
||||
--include-thumbnail Include thumbnail URLs
|
||||
--thumbnail-width <px> Thumbnail width
|
||||
--thumbnail-height <px> Thumbnail height
|
||||
--page-size <n> Page size (default: 10, max: 100)
|
||||
--next-token <token> Cursor for next page
|
||||
--pre-token <token> Cursor for previous page
|
||||
|
||||
Examples:
|
||||
bl asset list
|
||||
bl asset list --type IMAGE --model qwen-image-3.0
|
||||
bl asset list --favorited --page-size 20
|
||||
bl asset list --recycle-bin
|
||||
bl asset list --keyword landscape --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #1 查看资产列表
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset get <asset-id>`
|
||||
|
||||
```
|
||||
Description: Get full details of a model-generated asset
|
||||
|
||||
Usage: bl asset get <asset-id> [flags]
|
||||
|
||||
Arguments:
|
||||
<asset-id> Asset ID to query
|
||||
|
||||
Flags:
|
||||
--asset-id <id> Asset ID (alternative to positional)
|
||||
--include-download-url Include signed download URL
|
||||
--include-thumbnail Include thumbnail URL
|
||||
--thumbnail-width <px> Thumbnail width
|
||||
--thumbnail-height <px> Thumbnail height
|
||||
|
||||
Examples:
|
||||
bl asset get asset-001
|
||||
bl asset get asset-001 --include-download-url --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #2 查看资产详情
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset favorite`
|
||||
|
||||
```
|
||||
Description: Add assets to favorites
|
||||
|
||||
Usage: bl asset favorite --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset favorite --id asset-001
|
||||
bl asset favorite --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** #3 收藏
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset unfavorite`
|
||||
|
||||
```
|
||||
Description: Remove assets from favorites
|
||||
|
||||
Usage: bl asset unfavorite --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset unfavorite --id asset-001
|
||||
bl asset unfavorite --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** #3 取消收藏
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset delete`
|
||||
|
||||
```
|
||||
Description: Delete assets (soft delete to recycle bin by default)
|
||||
|
||||
Usage: bl asset delete --id <asset-id> [--id <asset-id>...] [flags]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
--permanent Permanently delete (cannot be restored)
|
||||
|
||||
Examples:
|
||||
bl asset delete --id asset-001
|
||||
bl asset delete --id asset-001 --id asset-002
|
||||
bl asset delete --id asset-001 --permanent
|
||||
```
|
||||
|
||||
**PRD 映射:** #4 删除资产、#5 批量删除
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset restore`
|
||||
|
||||
```
|
||||
Description: Restore soft-deleted assets from recycle bin
|
||||
|
||||
Usage: bl asset restore --id <asset-id> [--id <asset-id>...]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
|
||||
Examples:
|
||||
bl asset restore --id asset-001
|
||||
bl asset restore --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
**PRD 映射:** 补充能力(配合回收站)
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset download`
|
||||
|
||||
```
|
||||
Description: Get signed download URLs for assets
|
||||
|
||||
Usage: bl asset download --id <asset-id> [--id <asset-id>...] [--out <path>]
|
||||
|
||||
Flags:
|
||||
--id <asset-id> Asset ID (repeatable, max 100, required)
|
||||
--out <path> Save file to path (only when exactly one --id)
|
||||
|
||||
Examples:
|
||||
bl asset download --id asset-001
|
||||
bl asset download --id asset-001 --out ./image.png
|
||||
bl asset download --id asset-001 --id asset-002 --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #6 下载资产
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset stats`
|
||||
|
||||
```
|
||||
Description: Count model-generated assets by type
|
||||
|
||||
Usage: bl asset stats [flags]
|
||||
|
||||
Flags:
|
||||
--type <type> Filter by asset type
|
||||
--model <name> Filter by model name
|
||||
--keyword <text> Filter by asset name
|
||||
--favorited Count only favorited assets
|
||||
--recycle-bin Count soft-deleted assets
|
||||
--sync-failed Also count assets with failed OSS sync
|
||||
--begin-time <datetime> Filter by generate time start
|
||||
--end-time <datetime> Filter by generate time end
|
||||
|
||||
Examples:
|
||||
bl asset stats
|
||||
bl asset stats --sync-failed
|
||||
bl asset stats --type IMAGE --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #7 查看资产统计
|
||||
|
||||
**text 输出示例:**
|
||||
|
||||
```
|
||||
Total: 200
|
||||
Image: 150
|
||||
Video: 30
|
||||
Audio: 20
|
||||
Sync failed: 5 # 仅 --sync-failed 时出现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset storage`
|
||||
|
||||
```
|
||||
Description: View storage quota, usage, and overage pricing
|
||||
|
||||
Usage: bl asset storage [flags]
|
||||
|
||||
Examples:
|
||||
bl asset storage
|
||||
bl asset storage --output json
|
||||
```
|
||||
|
||||
**PRD 映射:** #14 查看容量信息
|
||||
|
||||
**text 输出示例:**
|
||||
|
||||
```
|
||||
Used: 1.2 GB
|
||||
Free quota: 5.0 GB
|
||||
Overage: ¥0.12/GB/month
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Phase 2/3 可选命令
|
||||
|
||||
#### `bl asset models list`
|
||||
|
||||
```
|
||||
Description: List managed models grouped by asset type
|
||||
|
||||
Usage: bl asset models list
|
||||
|
||||
Examples:
|
||||
bl asset models list --output json
|
||||
```
|
||||
|
||||
用途:配合 `bl asset list --model` 时查阅可用 modelId。
|
||||
|
||||
---
|
||||
|
||||
#### `bl asset service status`
|
||||
|
||||
```
|
||||
Description: Check whether asset center service is enabled
|
||||
|
||||
Usage: bl asset service status
|
||||
|
||||
Examples:
|
||||
bl asset service status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 6. PRD 覆盖矩阵
|
||||
|
||||
| PRD # | 功能 | CLI 命令 | Phase | 状态 |
|
||||
| ----- | ------------- | ------------------------------- | ----- | ---------------------------------- |
|
||||
| 1 | 查看资产列表 | `asset list` | 1 | ✅ 可开发 |
|
||||
| 2 | 查看资产详情 | `asset get` | 1 | ✅ 可开发 |
|
||||
| 3 | 收藏/取消收藏 | `asset favorite` / `unfavorite` | 1 | ✅ 可开发 |
|
||||
| 4 | 删除资产 | `asset delete` | 1 | ✅ 可开发 |
|
||||
| 5 | 批量删除 | `asset delete`(多 `--id`) | 1 | ✅ 可开发 |
|
||||
| 6 | 下载资产 | `asset download` | 1 | ✅ 可开发 |
|
||||
| 7 | 查看资产统计 | `asset stats` | 1 | ✅ 可开发(转存失败用 workaround) |
|
||||
| 14 | 查看容量信息 | `asset storage` | 1 | ✅ 可开发 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 典型工作流
|
||||
|
||||
### 7.1 首次使用
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
bl config set workspace_id ws-xxxxx
|
||||
bl asset service status # 可选:确认已开通
|
||||
bl asset storage # 查看容量
|
||||
```
|
||||
|
||||
### 7.2 浏览与筛选
|
||||
|
||||
```bash
|
||||
bl asset list
|
||||
bl asset list --type IMAGE --model qwen-image-3.0 --keyword landscape
|
||||
bl asset list --favorited
|
||||
bl asset list --recycle-bin
|
||||
bl asset get asset-001 --include-download-url
|
||||
bl asset stats
|
||||
bl asset stats --sync-failed
|
||||
```
|
||||
|
||||
### 7.3 资产管理
|
||||
|
||||
```bash
|
||||
bl asset favorite --id asset-001
|
||||
bl asset unfavorite --id asset-001
|
||||
bl asset delete --id asset-001
|
||||
bl asset delete --id asset-001 --id asset-002
|
||||
bl asset restore --id asset-001
|
||||
bl asset download --id asset-001 --out ./image.png
|
||||
```
|
||||
|
||||
### 7.5 脚本翻页(JSON)
|
||||
|
||||
```bash
|
||||
# 第一页
|
||||
bl asset list --page-size 50 --output json
|
||||
# 后续页(使用响应中的 nextToken)
|
||||
bl asset list --page-size 50 --next-token 1000 --output json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 与现有命令的风格对齐
|
||||
|
||||
| 参考命令 | 对齐点 |
|
||||
| ------------------------------ | -------------------------------------------------- |
|
||||
| `bl app list` | console gateway 调用、dry-run 输出 `{ api, data }` |
|
||||
| `bl dataset list` | text 表格 + json items 结构 |
|
||||
| `bl deploy list/get/create` | 产品域子命令命名、多级 path |
|
||||
| `bl memory profile get/create` | 三级 path 子组 |
|
||||
| `bl quota list` | `zeldaHttp.*` API 名、响应 extract |
|
||||
| `bl video download` | `--out` 落盘 |
|
||||
| `bl usage stats` | `requireWorkspaceId`、console E2E 模式 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 版本 | 说明 |
|
||||
| ---------- | ---- | ---------------------------------------------- |
|
||||
| 2026-07-09 | 0.1 | 初稿:命令树、PRD 映射、分 Phase 规格 |
|
||||
| 2026-08-07 | 0.2 | 取消 OSS 转存命令(`oss *` / `transfer list`) |
|
||||
@@ -1,408 +0,0 @@
|
||||
# 资产中心 CLI 设计文档
|
||||
|
||||
> 本文档描述 `bl asset` 命令族的技术设计方案,供开发、评审与联调使用。
|
||||
> 接口字段细节见同目录 [api-doc.md](./api-doc.md);命令路径与 help 结构见 [COMMAND-TREE.md](./COMMAND-TREE.md)。
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
百炼资产中心(Asset Center)提供模型生成资产的存储、检索、收藏、删除与容量管理能力。产品 PRD 要求 CLI 覆盖以下模块:
|
||||
|
||||
| 模块 | PRD 能力 |
|
||||
| -------- | ----------------------------------------------- |
|
||||
| 资产管理 | 列表、详情、收藏/取消收藏、删除、批量删除、下载 |
|
||||
| 资产统计 | 总量、按类型统计 |
|
||||
| 容量 | 已用容量、免费额度、超额单价 |
|
||||
|
||||
后端接口通过 **Zelda HTTP 网关** 暴露,Base Path 为 `/zelda/api/v1/bailian/asset`,详见 [api-doc.md](./api-doc.md)。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
- 在 `packages/commands` 实现可复用命令库,由 `packages/cli/src/commands.ts` 注册为 `bl asset ...` 产品路径
|
||||
- 遵循 monorepo 分层约定:`commands` 不写产品 bin 前缀;Console Gateway 命令统一 `auth: "console"`
|
||||
- 服务端错误原样透传;CLI 仅对参数校验、缺凭证、网络失败等内部错误发出语义化 `BailianError`
|
||||
- 支持 `--dry-run`、`--output json`、text 表格输出等现有 CLI 惯例
|
||||
|
||||
### 1.3 非目标
|
||||
|
||||
- 不在 `rag` 入口暴露(首期与 `deploy` / `finetune` 一致,仅 `bl`)
|
||||
- 不暴露 `sendMqMessage` 等内部 MQ 接口
|
||||
- 不在 `core` / `runtime` 层硬编码 `bl` 命令名或控制台 URL
|
||||
|
||||
---
|
||||
|
||||
## 2. PRD → API → CLI 映射
|
||||
|
||||
### 2.1 资产管理
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
|
||||
| ----- | ------------- | ------------------------------------------- | --------------------------------------------- | ------------------------------------------ |
|
||||
| 1 | 查看资产列表 | `bl asset list` | `listModelGeneratedAsset` | 游标分页;支持类型/模型/关键词/收藏/回收站 |
|
||||
| 2 | 查看资产详情 | `bl asset get <asset-id>` | `getModelGeneratedAsset` | positional 或 `--asset-id` |
|
||||
| 3 | 收藏/取消收藏 | `bl asset favorite` / `bl asset unfavorite` | `batchFavoriteAsset` / `batchUnfavoriteAsset` | 单 ID 也走 batch(长度 1) |
|
||||
| 4 | 删除资产 | `bl asset delete` | `batchDeleteAsset` | 默认 `SOFT_DELETE`(移入回收站) |
|
||||
| 5 | 批量删除 | `bl asset delete` | `batchDeleteAsset` | `--id` 可重复,最多 100 |
|
||||
| 6 | 下载资产 | `bl asset download` | `batchGetAssetDownloadUrl` | 默认输出 URL;单资产可选 `--out` 落盘 |
|
||||
|
||||
**建议补充(API 已有、PRD 未写):**
|
||||
|
||||
| 能力 | CLI 命令 | API Action |
|
||||
| ------------ | ------------------ | ------------------- |
|
||||
| 从回收站恢复 | `bl asset restore` | `batchRestoreAsset` |
|
||||
|
||||
### 2.2 资产统计
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
|
||||
| ----- | ------------ | ---------------- | -------------------------- | --------------------------------------- |
|
||||
| 7 | 查看资产统计 | `bl asset stats` | `countModelGeneratedAsset` | 输出 total / image / video / audio 计数 |
|
||||
|
||||
**转存失败数(PRD 子项):**
|
||||
|
||||
- API 支持 `syncOssDataStatus=SYNC_FAILED` 筛选,但无独立 `failureCount` 字段
|
||||
- **Phase 1 方案**:`bl asset stats --sync-failed` 额外发起一次 count 查询,输出 `sync_failed_count`
|
||||
- **Phase 3 备选**:等后端在 stats 响应中增加专用字段后收敛
|
||||
|
||||
### 2.4 容量
|
||||
|
||||
| PRD # | 能力 | CLI 命令 | API Action |
|
||||
| ----- | ------------ | ------------------ | ----------------- |
|
||||
| 14 | 查看容量信息 | `bl asset storage` | `getStorageQuota` |
|
||||
|
||||
### 2.5 可选扩展(API 有、PRD 未列)
|
||||
|
||||
| CLI 命令 | API Action | 优先级 |
|
||||
| ------------------------------------- | ------------------------------- | -------------------- |
|
||||
| `bl asset service status` | `checkAssetServiceSubscription` | P1 |
|
||||
| `bl asset service enable` / `disable` | `subscribeAssetService` | P2 |
|
||||
| `bl asset models list` | `listModels` | P1(配合 list 筛选) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构与分层
|
||||
|
||||
### 3.1 在 monorepo 中的位置
|
||||
|
||||
```
|
||||
packages/commands/src/commands/asset-center/*.ts ← 命令实现(本目录)
|
||||
↓ export
|
||||
packages/commands/src/index.ts
|
||||
↓ import + map key
|
||||
packages/cli/src/commands.ts ← "asset list": assetList, ...
|
||||
↓
|
||||
packages/runtime (createCli / authStage / registry)
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
- 实现文件按能力组织在本目录
|
||||
- `usageArgs` / `exampleArgs` 不含 `bl` 前缀
|
||||
- 所有 asset 命令 `auth: "console"`;不重复声明 `CONSOLE_AUTH_FLAGS`(runtime 自动注入)
|
||||
|
||||
### 3.2 目录结构
|
||||
|
||||
```
|
||||
asset-center/
|
||||
├── api-doc.md # 后端 API 文档(已有)
|
||||
├── DESIGN.md # 本文档
|
||||
├── COMMAND-TREE.md # 命令树与 help 结构
|
||||
├── types.ts # TypeScript 类型(ModelGeneratedAssetItem 等)
|
||||
├── utils.ts # 公共请求构建、API 调用、响应解析
|
||||
├── list.ts
|
||||
├── get.ts
|
||||
├── favorite.ts
|
||||
├── unfavorite.ts
|
||||
├── delete.ts
|
||||
├── download.ts
|
||||
├── stats.ts
|
||||
└── storage.ts
|
||||
```
|
||||
|
||||
### 3.3 共享层 `utils.ts`
|
||||
|
||||
参考 `token-plan/utils.ts`、`usage/stats.ts` 的 `requireWorkspaceId` 模式。
|
||||
|
||||
#### 3.3.1 API 名称约定
|
||||
|
||||
与 `quota/list.ts` 中 `zeldaHttp.dashscopeModel./zelda/api/v1/...` 类似,资产中心预期为:
|
||||
|
||||
```typescript
|
||||
const ASSET_SERVICE = "bailianAsset"; // ⚠️ 编码前需 spike 确认
|
||||
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
|
||||
|
||||
function assetApi(action: string): string {
|
||||
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
|
||||
}
|
||||
```
|
||||
|
||||
编码第一步用 `bl console call --api <name> --data '{...}'` 验证实际注册名。
|
||||
|
||||
#### 3.3.2 公共请求体
|
||||
|
||||
所有接口继承 `AssetHttpBaseRequest`(见 api-doc §公共请求参数):
|
||||
|
||||
| 字段 | CLI 来源 | 状态 |
|
||||
| ---------------- | --------------------------------------------------------- | ---------- |
|
||||
| `workspace` | `settings.workspaceId`(`--workspace-id` / env / config) | ✅ 已有 |
|
||||
| `tenantId` | 待定 | ⚠️ 需确认 |
|
||||
| `mainAccountUid` | 待定 | ⚠️ 需确认 |
|
||||
| `apiSource` | 固定 `"CLI"` | 实现时写入 |
|
||||
| `aliYunUid` 等 | 网关 session 注入或省略 | 待确认 |
|
||||
|
||||
`requireWorkspaceId(settings, binName)` 在缺少 workspace 时抛出 `BailianError(GENERAL)`,hint 指向 `bl workspace list`。
|
||||
|
||||
#### 3.3.3 调用封装
|
||||
|
||||
```typescript
|
||||
async function callAssetApi<T>(
|
||||
ctx: CommandRunContext,
|
||||
action: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const payload = { ...buildBaseRequest(ctx), ...body };
|
||||
const raw = await ctx.client.console(assetApi(action), payload);
|
||||
return extractAssetResponse<T>(raw);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.4 响应解析
|
||||
|
||||
Console Gateway 响应可能存在多层嵌套(参考 `quota/list.ts` 的 `extractResponseData`):
|
||||
|
||||
1. 剥 gateway 外层:`data` → `DataV2` → `data` → ...
|
||||
2. 到达业务 `Result<T>`:`{ success, code, message, data }`
|
||||
3. 若 `success === false`:抛 `BailianError(GENERAL, message)`,**不翻译、不替换** message
|
||||
4. 成功时返回 `data` 字段
|
||||
|
||||
---
|
||||
|
||||
## 4. 命令实现规范
|
||||
|
||||
### 4.1 通用模式
|
||||
|
||||
每个命令文件遵循:
|
||||
|
||||
```typescript
|
||||
export default defineCommand({
|
||||
description: "...",
|
||||
auth: "console",
|
||||
usageArgs: "...",
|
||||
flags: { ... },
|
||||
exampleArgs: ["...", "--output json"],
|
||||
validate(ctx) { /* 跨 flag 条件校验 */ },
|
||||
async run(ctx) {
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult({ api: assetApi("..."), data: { ... } }, format);
|
||||
return;
|
||||
}
|
||||
const data = await callAssetApi(ctx, "actionName", { ... });
|
||||
// text 表格 或 emitResult(json)
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
参考实现:`app/list.ts`(console + dry-run)、`dataset/list.ts`(表格输出)、`video/download.ts`(落盘)。
|
||||
|
||||
### 4.2 分页模型(`asset list`)
|
||||
|
||||
**与 `app list` 不同**:资产列表使用 **id 游标分页**,不是 page/pageSize 页码模式。
|
||||
|
||||
| Flag | API 字段 | 说明 |
|
||||
| -------------- | ----------- | -------------------------- |
|
||||
| `--page-size` | `pageSize` | 默认 10,最大 100 |
|
||||
| `--next-token` | `nextToken` | 下一页游标(来自上次响应) |
|
||||
| `--pre-token` | `preToken` | 上一页游标 |
|
||||
|
||||
JSON 输出保留 `nextToken` / `preToken` / `hasNext` / `hasPre`,便于脚本翻页。
|
||||
|
||||
### 4.3 批量 ID 传参
|
||||
|
||||
批量操作(favorite / unfavorite / delete / restore / download)统一:
|
||||
|
||||
```typescript
|
||||
id: {
|
||||
type: "array",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID(s) to operate on (repeatable, max 100)",
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
CLI 用法:`--id asset-001 --id asset-002` 或多次重复。实现时在 `validate` 中校验 `ids.length <= 100`。
|
||||
|
||||
### 4.4 输出格式
|
||||
|
||||
| 命令 | text 默认 | json |
|
||||
| ---------- | -------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `list` | 表格:assetId / type / name / model / favorited / generateTime | items + pagination |
|
||||
| `get` | 关键字段摘要 | 完整 item |
|
||||
| `stats` | 数字摘要 | `{ total_count, image_count, ... }` |
|
||||
| `storage` | 人类可读字节 + 单价 | 原始 quota 字段 |
|
||||
| 写操作 | 一行确认(affectedCount) | `{ success, affected_count }` |
|
||||
| `download` | URL 列表或 saved 路径 | `{ items: [{ asset_id, download_url }] }` |
|
||||
|
||||
使用 `formatTable`(`dataset/list.ts`)、`formatBytes`(`video/download.ts`)、`emitResult` / `emitBare`。
|
||||
|
||||
### 4.5 条件校验(`validate`)
|
||||
|
||||
| 命令 | 规则 |
|
||||
| ---------- | --------------------------------------------------------- |
|
||||
| `delete` | `--permanent` 映射 `PERMANENT_DELETE`;默认 `SOFT_DELETE` |
|
||||
| 所有 batch | `assetIdList.length <= 100` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键命令 Flag 详设
|
||||
|
||||
### 5.1 `bl asset list`
|
||||
|
||||
| Flag | 类型 | API 映射 | 说明 |
|
||||
| ------------------------ | ---------------- | ---------------------------- | ------------------------------------------------------------ |
|
||||
| `--type` | string (choices) | `assetType` | `IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| `--model` | string | `modelName` | PRD「按模型筛选」 |
|
||||
| `--keyword` | string | `assetName` | PRD「关键词」;是否同时搜 description 待产品确认 |
|
||||
| `--favorited` | switch | `favorited: true` | 仅看收藏 |
|
||||
| `--recycle-bin` | switch | `deleteStatus: SOFT_DELETED` | 仅看回收站 |
|
||||
| `--sync-status` | string (choices) | `syncOssDataStatus` | `NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| `--begin-time` | string | `beginTime` | ISO_LOCAL_DATE_TIME |
|
||||
| `--end-time` | string | `endTime` | ISO_LOCAL_DATE_TIME |
|
||||
| `--include-download-url` | switch | `includeDownloadUrl` | |
|
||||
| `--include-thumbnail` | switch | `includeThumbnail` | |
|
||||
| `--thumbnail-width` | number | `thumbnailWidth` | 配合 thumbnail |
|
||||
| `--thumbnail-height` | number | `thumbnailHeight` | 配合 thumbnail |
|
||||
| `--page-size` | number | `pageSize` | |
|
||||
| `--next-token` | number | `nextToken` | |
|
||||
| `--pre-token` | number | `preToken` | |
|
||||
|
||||
### 5.2 `bl asset get`
|
||||
|
||||
| 参数/Flag | 说明 |
|
||||
| ------------------------------------------ | --------------------------------------- |
|
||||
| `<asset-id>` | positional,primary |
|
||||
| `--asset-id` | 与 positional 二选一(positional 优先) |
|
||||
| `--include-download-url` | |
|
||||
| `--include-thumbnail` | |
|
||||
| `--thumbnail-width` / `--thumbnail-height` | |
|
||||
|
||||
### 5.3 `bl asset delete`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------------- | --------------------------------------------------------- |
|
||||
| `--id` | array, required, max 100 |
|
||||
| `--permanent` | switch → `deleteType: PERMANENT_DELETE`;默认 SOFT_DELETE |
|
||||
|
||||
### 5.4 `bl asset download`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------- | ----------------------------------------------------- |
|
||||
| `--id` | array, required |
|
||||
| `--out` | 仅当 `--id` 恰好 1 个时有效;调用 `downloadFile` 落盘 |
|
||||
|
||||
### 5.5 `bl asset stats`
|
||||
|
||||
| Flag | 说明 |
|
||||
| ------------------------------------- | ---------------------------------------------- |
|
||||
| (无 filter) | 默认 `deleteStatus: NORMAL` |
|
||||
| `--recycle-bin` | `deleteStatus: SOFT_DELETED` |
|
||||
| `--sync-failed` | 额外查询 `syncOssDataStatus: SYNC_FAILED` 计数 |
|
||||
| `--type` / `--model` / `--keyword` 等 | 与 list 相同筛选维度(可选) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与待确认项
|
||||
|
||||
### 6.1 P0 — 编码前必须对齐
|
||||
|
||||
| # | 问题 | 影响 | 建议动作 |
|
||||
| --- | ------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- |
|
||||
| 1 | Console API 注册名(`zeldaHttp.{service}./zelda/api/v1/bailian/asset/*`) | 无法调用 | `bl console call` spike;与后端确认 service 名 |
|
||||
| 2 | `tenantId` / `mainAccountUid` 由谁填充 | 所有接口必填 | 确认网关是否从 session 自动注入;否则扩展 config 或新增解析 API |
|
||||
|
||||
### 6.2 P1 — 产品设计
|
||||
|
||||
| # | 问题 | 建议默认 |
|
||||
| --- | -------------------- | ----------------------------------------------- |
|
||||
| 4 | 关键词搜索范围 | 仅 `assetName`;后续可加 `--search-description` |
|
||||
| 5 | PRD 只提 image/video | CLI 暴露 IMAGE/VIDEO/AUDIO(与 API 一致) |
|
||||
| 6 | 下载行为 | 默认输出 URL;单 ID + `--out` 落盘 |
|
||||
| 7 | 永久删除 | 提供 `--permanent`,help 注明不可恢复 |
|
||||
| 8 | 服务未开通 | 不预检查;失败时透传服务端 message |
|
||||
| 9 | 收藏命令形态 | 两个命令 `favorite` / `unfavorite`(语义清晰) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
遵循 [AGENTS.md](../../../../../../AGENTS.md) 错误边界:
|
||||
|
||||
| 场景 | 处理 |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| 缺 `--workspace-id` | `BailianError(GENERAL)` + hint |
|
||||
| 缺 console token | authStage 抛 `BailianError(AUTH)` |
|
||||
| flag 校验失败 | `UsageError` (exit 2) |
|
||||
| HTTP 4xx/5xx / 业务 success=false | `BailianError(GENERAL)`,message **原样透传** |
|
||||
| batch ID > 100 | `UsageError` |
|
||||
|
||||
Console 未登录参考 `mcp/list.ts`:检测 `BailianGateway.Login.NotLogined` 时 hint 指向 `bl auth login --console`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
新建 `packages/cli/tests/e2e/asset.e2e.test.ts`,遵循 [cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)。
|
||||
|
||||
### 8.1 不 skip 层
|
||||
|
||||
- `bl asset` 分组 help
|
||||
- 各子命令 `--help`
|
||||
- 缺参 → exit 2
|
||||
|
||||
### 8.2 Console skip 层(`isConsoleE2EReady()`)
|
||||
|
||||
- 各命令 `--dry-run` 输出 api + data
|
||||
- 真实 `asset list` / `asset storage` 集成(需已开通资产中心的工作空间)
|
||||
|
||||
环境:`BAILIAN_E2E=1` + console `access_token` + `BAILIAN_WORKSPACE_ID`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 注册与文档变更清单
|
||||
|
||||
| 文件 | 变更 |
|
||||
| -------------------------------------------------- | ------------------- |
|
||||
| `packages/commands/src/commands/asset-center/*.ts` | 新建 |
|
||||
| `packages/commands/src/index.ts` | export |
|
||||
| `packages/cli/src/commands.ts` | 注册 map |
|
||||
| `packages/cli/tests/e2e/asset.e2e.test.ts` | 新建 |
|
||||
| `skills/bailian-cli/reference/` | pre-commit 自动生成 |
|
||||
| `README.md` / `README.zh.md` | 发版前补充命令一览 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 分期实施
|
||||
|
||||
### Phase 1 — 核心资产(P0)
|
||||
|
||||
```
|
||||
asset list | get | favorite | unfavorite | delete | restore | download | stats | storage
|
||||
```
|
||||
|
||||
**前置:** §6.1 #1 #2 确认。
|
||||
|
||||
### Phase 2+ — 可选扩展
|
||||
|
||||
```
|
||||
asset models list | service status | service enable/disable
|
||||
```
|
||||
|
||||
> OSS 转存相关命令(`asset-center oss *` / `transfer list`)已取消,不再排期。
|
||||
|
||||
## 11. 参考
|
||||
|
||||
- 命令注册:[docs/agents/command-add-remove.md](../../../../../../docs/agents/command-add-remove.md)
|
||||
- E2E 规范:[docs/agents/cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)
|
||||
- Console 命令样例:`packages/commands/src/commands/app/list.ts`
|
||||
- 游标/表格:`packages/commands/src/commands/quota/list.ts`
|
||||
- workspace 必填:`packages/commands/src/commands/usage/stats.ts`
|
||||
- 文件落盘:`packages/commands/src/commands/video/download.ts`
|
||||
@@ -1,35 +0,0 @@
|
||||
# Asset Center 命令测试报告 — Phase 2
|
||||
|
||||
- **测试时间**: 2026-07-10 09:07:09 (UTC)
|
||||
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
|
||||
- **测试 IMAGE**: `asset_98175cbf83294f7b8ada86657623dcf3`
|
||||
- **测试 VIDEO**: `asset_df026105d2274ff9b8c824058fa23d60`
|
||||
- **策略**: 可逆写操作(favorite/unfavorite 往返);download 到 /tmp 后删除;其余只读
|
||||
- **汇总**: 16 通过 / 0 失败 / 16 总计
|
||||
|
||||
> Phase 1 报告见同目录 [TEST-REPORT.md](./TEST-REPORT.md)(24 项 dry-run + 只读基础验证)
|
||||
|
||||
## Phase 2 测试结果
|
||||
|
||||
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
|
||||
| --- | ---- | ------------------------------------------ | -------- | ------- | ---- | ------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | 下载 | `asset-center download (IMAGE)` | 真实调用 | ✅ PASS | 0 | 20045ms | saved /tmp/asset-center-test-asset_98175cbf83294f7b8ada86657623dcf3.png (1449847 bytes, reported 1.4 MB) |
|
||||
| 2 | 查询 | `asset-center get --include-download-url` | 真实调用 | ✅ PASS | 0 | 21226ms | download_url present |
|
||||
| 3 | 查询 | `asset-center list --include-download-url` | 真实调用 | ✅ PASS | 0 | 19057ms | items contain download_url |
|
||||
| 4 | 查询 | `asset-center list --next-token` | 真实调用 | ✅ PASS | 0 | 19584ms | page2=3 items, overlap=0, has_pre=true |
|
||||
| 5 | 统计 | `asset-center stats --type IMAGE` | 真实调用 | ✅ PASS | 0 | 18830ms | image=7, total=7 |
|
||||
| 6 | 统计 | `asset-center stats --sync-failed` | 真实调用 | ✅ PASS | 0 | 19084ms | total=27, sync_failed=0 |
|
||||
| 7 | 查询 | `asset-center list --recycle-bin` | 真实调用 | ✅ PASS | 0 | 19314ms | 0 soft-deleted item(s) |
|
||||
| 8 | 输出 | `asset-center list (text)` | 真实调用 | ✅ PASS | 0 | 17840ms | 4 lines table output |
|
||||
| 9 | 收藏 | `asset-center favorite (真实)` | 真实调用 | ✅ PASS | 0 | 22082ms | affected=1 |
|
||||
| 10 | 收藏 | `get 验证 favorited=true` | 真实调用 | ✅ PASS | 0 | 18865ms | favorited=true ✓ |
|
||||
| 11 | 查询 | `list --favorited 含测试资产` | 真实调用 | ✅ PASS | 0 | 22120ms | found in favorited list |
|
||||
| 12 | 收藏 | `asset-center unfavorite (真实)` | 真实调用 | ✅ PASS | 0 | 22133ms | affected=1 |
|
||||
| 13 | 收藏 | `get 验证 favorited=false (恢复)` | 真实调用 | ✅ PASS | 0 | 27797ms | favorited=false ✓ |
|
||||
| 14 | 收藏 | `favorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 22947ms | affected=2 |
|
||||
| 15 | 收藏 | `unfavorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 15385ms | affected=2 |
|
||||
| 16 | 边界 | `get 不存在的 asset-id` | 真实调用 | ✅ PASS | 1 | 12207ms | exit 1, 服务端错误原样透传: "资产不存在" |
|
||||
|
||||
## 边界行为说明
|
||||
|
||||
查询不存在的 `asset-id` 时,服务端返回业务错误 **「资产不存在」**,CLI 按约定 **原样透传**(exit code 1),不会替换为本地文案。这与 AGENTS.md 错误处理边界一致。
|
||||
@@ -1,36 +0,0 @@
|
||||
# Asset Center 命令测试报告
|
||||
|
||||
- **测试时间**: 2026-07-10 08:41:06 (UTC)
|
||||
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
|
||||
- **样本 Asset ID**: `asset_df026105d2274ff9b8c824058fa23d60`
|
||||
- **策略**: 只读命令真实调用;写操作/下载一律 `--dry-run`
|
||||
- **汇总**: 24 通过 / 0 失败 / 24 总计
|
||||
|
||||
## 测试结果
|
||||
|
||||
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
|
||||
| --- | ---- | ---------------------------------- | -------- | ------- | ---- | ------- | -------------------------------------------------------------- |
|
||||
| 1 | 查询 | `asset-center list` | 真实调用 | ✅ PASS | 0 | 18962ms | 3 item(s), next=94 |
|
||||
| 2 | 查询 | `asset-center list --type IMAGE` | 真实调用 | ✅ PASS | 0 | 16411ms | 2 item(s), next=90 |
|
||||
| 3 | 查询 | `asset-center get` | 真实调用 | ✅ PASS | 0 | 16541ms | {gmtModified, aliyunUid, generateTime, aliyunMainId} |
|
||||
| 4 | 统计 | `asset-center stats` | 真实调用 | ✅ PASS | 0 | 14226ms | total=27 |
|
||||
| 5 | 统计 | `asset-center storage` | 真实调用 | ✅ PASS | 0 | 18622ms | {used_storage_size, free_storage_quota, extra_storage_price} |
|
||||
| 9 | 查询 | `asset-center list --dry-run` | dry-run | ✅ PASS | 0 | 23855ms | dry-run → /zelda/api/v1/bailian/asset/listModelGeneratedAsset |
|
||||
| 10 | 查询 | `asset-center get --dry-run` | dry-run | ✅ PASS | 0 | 17398ms | dry-run → /zelda/api/v1/bailian/asset/getModelGeneratedAsset |
|
||||
| 11 | 收藏 | `asset-center favorite` | dry-run | ✅ PASS | 0 | 16471ms | dry-run → /zelda/api/v1/bailian/asset/batchFavoriteAsset |
|
||||
| 12 | 收藏 | `asset-center unfavorite` | dry-run | ✅ PASS | 0 | 19724ms | dry-run → /zelda/api/v1/bailian/asset/batchUnfavoriteAsset |
|
||||
| 13 | 删除 | `asset-center delete` | dry-run | ✅ PASS | 0 | 20612ms | dry-run → /zelda/api/v1/bailian/asset/batchDeleteAsset |
|
||||
| 14 | 下载 | `asset-center download` | dry-run | ✅ PASS | 0 | 20348ms | dry-run → /zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl |
|
||||
| 15 | 统计 | `asset-center stats --dry-run` | dry-run | ✅ PASS | 0 | 14898ms | dry-run → /zelda/api/v1/bailian/asset/countModelGeneratedAsset |
|
||||
| 16 | 统计 | `asset-center storage --dry-run` | dry-run | ✅ PASS | 0 | 14305ms | dry-run → /zelda/api/v1/bailian/asset/getStorageQuota |
|
||||
| 21 | 校验 | `asset-center get (缺 asset-id)` | 参数校验 | ✅ PASS | 2 | 15202ms | Error: Missing required flag: --asset-id |
|
||||
| 22 | 校验 | `asset-center favorite (缺 --id)` | 参数校验 | ✅ PASS | 2 | 16697ms | Error: Missing required flag: --id |
|
||||
| 23 | 校验 | `asset-center download (缺 --out)` | 参数校验 | ✅ PASS | 2 | 18803ms | Error: Missing required flag: --out |
|
||||
|
||||
## 模式说明
|
||||
|
||||
| 模式 | 说明 |
|
||||
| -------- | -------------------------------------------------- |
|
||||
| 真实调用 | 只读 API,不修改数据 |
|
||||
| dry-run | 输出 `{ api, data, gateway }` 请求体,不发起写操作 |
|
||||
| 参数校验 | 预期 exit code 2(用法错误) |
|
||||
@@ -1,624 +0,0 @@
|
||||
# BailianAssetZeldaHttpService API 文档
|
||||
|
||||
通过 Zelda 网关调用 bailian-asset HTTP 接口文档。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base Path**: `/zelda/api/v1/bailian/asset`
|
||||
- **Method**: POST
|
||||
- **Content-Type**: `application/json`
|
||||
- **Accept**: `application/json`
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
所有接口返回 `Result<T>` 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "string",
|
||||
"success": true,
|
||||
"code": "string",
|
||||
"message": "string",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | ------- | ---------------------- |
|
||||
| requestId | String | 请求唯一ID |
|
||||
| success | Boolean | 是否成功 |
|
||||
| code | String | 错误码(失败时返回) |
|
||||
| message | String | 错误信息(失败时返回) |
|
||||
| data | Object | 业务数据(成功时返回) |
|
||||
|
||||
## 公共请求参数(基类字段)
|
||||
|
||||
所有接口请求体均继承自 `AssetHttpBaseRequest`,包含以下公共字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------ | ---- | ---------------------------------------------- |
|
||||
| requestId | String | 否 | 请求唯一ID |
|
||||
| apiSource | String | 否 | 调用入口渠道,如 OpenAPI、CloudSDK |
|
||||
| tenantId | String | 是 | 内部租户ID |
|
||||
| workspace | String | 是 | 业务空间ID |
|
||||
| aliYunUid | String | 否 | 阿里云子账号ID |
|
||||
| mainAccountUid | String | 是 | 阿里云主账号ID |
|
||||
| callerType | String | 否 | 账号类型:partner/customer/sub/AssumedRoleUser |
|
||||
| callerParentId | Long | 否 | 调用者所属主账号ID |
|
||||
| accessKeyId | String | 否 | STS认证:用户AccessKeyId |
|
||||
| securityToken | String | 否 | STS认证:扮演者的STS Token |
|
||||
|
||||
---
|
||||
|
||||
## 1. 开通/关闭资产中心服务
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/subscribeAssetService`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------ | ------------ | ---- | --------------------------------------- |
|
||||
| action | String(Enum) | 是 | 操作类型:`ENABLE`-开通,`DISABLE`-关闭 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------ | ------------ | ------------ |
|
||||
| status | String(Enum) | 当前服务状态 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"action": "ENABLE"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 批量收藏资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchFavoriteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ---------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待收藏的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否收藏成功 |
|
||||
| affectedCount | Integer | 实际被收藏的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002", "asset-003"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 批量取消收藏资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchUnfavoriteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | -------------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待取消收藏的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | ------------------------ |
|
||||
| success | Boolean | 是否取消收藏成功 |
|
||||
| affectedCount | Integer | 实际被取消收藏的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 批量删除资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchDeleteAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ----------------------------------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待删除的资产ID列表,长度不超过 100 |
|
||||
| deleteType | String(Enum) | 是 | 删除类型:`SOFT_DELETE`-软删除,`PERMANENT_DELETE`-彻底删除 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否删除成功 |
|
||||
| affectedCount | Integer | 实际被删除的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"],
|
||||
"deleteType": "SOFT_DELETE"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 批量恢复软删除资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchRestoreAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ---------------------------------- |
|
||||
| assetIdList | List<String> | 是 | 待恢复的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------- | -------------------- |
|
||||
| success | Boolean | 是否恢复成功 |
|
||||
| affectedCount | Integer | 实际被恢复的资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 分页查询模型生成资产
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/listModelGeneratedAsset`
|
||||
|
||||
采用 id 游标分页,默认 pageSize=10,最大 100。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------------ | ---- | ---------------------------------------------------------------------------------- |
|
||||
| preToken | Long | 否 | 向前翻页游标 |
|
||||
| nextToken | Long | 否 | 向后翻页游标(查询下一页时传入上一次响应的 nextToken) |
|
||||
| pageSize | Integer | 否 | 每页大小,默认 10,最大 100 |
|
||||
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
|
||||
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL,默认 false |
|
||||
| thumbnailWidth | Integer | 否 | 缩放图宽度(像素),includeThumbnail=true 时生效 |
|
||||
| thumbnailHeight | Integer | 否 | 缩放图高度(像素),includeThumbnail=true 时生效 |
|
||||
| softDeleteTimeOrder | String(Enum) | 否 | 软删除时间排序方式:`ASC`-正序,`DESC`-倒序;仅在 deleteStatus=SOFT_DELETED 时有效 |
|
||||
| assetType | String(Enum) | 否 | 资产类型:`IMAGE`-图片,`VIDEO`-视频,`AUDIO`-音频 |
|
||||
| favorited | Boolean | 否 | 是否被收藏 |
|
||||
| assetName | String | 否 | 资产名称(子串模糊匹配) |
|
||||
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
|
||||
| trusted | Boolean | 否 | 是否可信 |
|
||||
| modelType | String | 否 | 生成资产的模型类型 |
|
||||
| modelName | String | 否 | 生成资产的模型型号 |
|
||||
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态:`NOT_SYNCED` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态:`NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
|
||||
| deleteStatus | String(Enum) | 否 | 删除状态:`NORMAL` / `SOFT_DELETED` / `PERMANENTLY_DELETED` |
|
||||
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME,如 `2023-10-25T14:30:00` |
|
||||
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME,如 `2023-10-25T14:30:00` |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | ----------------------------- | ------------ |
|
||||
| dataList | List<ModelGeneratedAssetItem> | 资产列表 |
|
||||
| preToken | Long | 前一页游标 |
|
||||
| nextToken | Long | 下一页游标 |
|
||||
| hasNext | Boolean | 是否有下一页 |
|
||||
| hasPre | Boolean | 是否有前一页 |
|
||||
|
||||
**ModelGeneratedAssetItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| id | Long | 主键 ID(分页游标 token) |
|
||||
| gmtCreate | Date | 创建时间 |
|
||||
| gmtModified | Date | 修改时间 |
|
||||
| workspaceId | String | 工作空间ID |
|
||||
| tenantId | String | 租户ID |
|
||||
| aliyunUid | String | 阿里云子账号ID |
|
||||
| aliyunMainId | String | 阿里云主账号ID |
|
||||
| assetId | String | 资产 ID |
|
||||
| assetType | String | 资产类型:IMAGE/VIDEO/AUDIO |
|
||||
| assetSource | String | 资产来源:MODEL_GENERATED/OFFICIAL/USER_UPLOADED |
|
||||
| favorited | Boolean | 是否被收藏 |
|
||||
| assetName | String | 资产名称 |
|
||||
| assetDescription | String | 资产描述 |
|
||||
| assetSize | Long | 资产大小(字节) |
|
||||
| md5 | String | 资产 MD5 |
|
||||
| ossBucket | String | 资产所在 OSS Bucket |
|
||||
| ossKey | String | 资产在 OSS bucket 中的 key |
|
||||
| region | String | 工作空间地域 |
|
||||
| ossRegion | String | 资产所在 OSS bucket 的地域 |
|
||||
| trusted | Boolean | 是否可信 |
|
||||
| modelType | String | 模型类型 |
|
||||
| modelName | String | 模型型号 |
|
||||
| syncWhiteListStatus | String | 同步白名单状态 |
|
||||
| syncOssDataStatus | String | 同步 OSS 数据状态 |
|
||||
| deleteStatus | String | 删除状态:NORMAL/SOFT_DELETED/PERMANENTLY_DELETED |
|
||||
| generateTime | Long | 资产生成时间戳(毫秒) |
|
||||
| softDeleteDays | Integer | 已被软删除的天数(仅当 deleteStatus=SOFT_DELETED 且请求 softDeleteTimeOrder 时返回) |
|
||||
| originalOssUrl | String | 原始 OSS URL |
|
||||
| downloadUrl | String | 文件下载链接(仅当请求 includeDownloadUrl=true 时返回) |
|
||||
| thumbnailUrl | String | 资产缩放图 URL(仅当请求 includeThumbnail=true 时返回;视频返回首帧缩放图,图片返回缩放图,音频返回 null) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"pageSize": 20,
|
||||
"includeDownloadUrl": true,
|
||||
"includeThumbnail": true,
|
||||
"thumbnailWidth": 200,
|
||||
"thumbnailHeight": 200,
|
||||
"assetType": "IMAGE",
|
||||
"favorited": true,
|
||||
"beginTime": "2024-01-01T00:00:00",
|
||||
"endTime": "2024-12-31T23:59:59"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"dataList": [
|
||||
{
|
||||
"id": 1001,
|
||||
"assetId": "asset-001",
|
||||
"assetType": "IMAGE",
|
||||
"assetName": "generated_image_01.png",
|
||||
"assetDescription": "A landscape painting",
|
||||
"favorited": true,
|
||||
"generateTime": 1700000000000
|
||||
}
|
||||
],
|
||||
"nextToken": 1000,
|
||||
"hasNext": true,
|
||||
"hasPre": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 统计模型生成资产数量
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/countModelGeneratedAsset`
|
||||
|
||||
查询条件与 `listModelGeneratedAsset` 一致(不需要分页参数),按资产类型分组返回数量。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------------ | ---- | ------------------------------------------------ |
|
||||
| assetType | String(Enum) | 否 | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| favorited | Boolean | 否 | 是否被收藏 |
|
||||
| assetName | String | 否 | 资产名称(子串模糊匹配) |
|
||||
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
|
||||
| trusted | Boolean | 否 | 是否可信 |
|
||||
| modelType | String | 否 | 模型类型 |
|
||||
| modelName | String | 否 | 模型型号 |
|
||||
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态 |
|
||||
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态 |
|
||||
| deleteStatus | String(Enum) | 否 | 删除状态 |
|
||||
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME |
|
||||
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------- | ---- | ---------------- |
|
||||
| imageCount | Long | 图片类型资产数量 |
|
||||
| videoCount | Long | 视频类型资产数量 |
|
||||
| audioCount | Long | 音频类型资产数量 |
|
||||
| totalCount | Long | 总资产数量 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"deleteStatus": "NORMAL"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"imageCount": 150,
|
||||
"videoCount": 30,
|
||||
"audioCount": 20,
|
||||
"totalCount": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 批量获取资产下载链接
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl`
|
||||
|
||||
一次最多获取 100 个资产的下载链接。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ------------------------------------------ |
|
||||
| assetIdList | List<String> | 是 | 待获取下载链接的资产ID列表,长度不超过 100 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----- | -------------------------- | -------------------------------- |
|
||||
| items | List<AssetDownloadUrlItem> | 资产下载链接列表,按请求顺序返回 |
|
||||
|
||||
**AssetDownloadUrlItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ------ | ---------------------------------------------------------- |
|
||||
| assetId | String | 资产 ID |
|
||||
| downloadUrl | String | 资产下载链接(带签名);资产不存在或缺少 OSS 信息时为 null |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetIdList": ["asset-001", "asset-002", "asset-003"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 查询模型生成资产详情
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/getModelGeneratedAsset`
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------ | ------- | ---- | ------------------------------------------------ |
|
||||
| assetId | String | 是 | 待查询的资产 ID |
|
||||
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
|
||||
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL,默认 false |
|
||||
| thumbnailWidth | Integer | 否 | 缩放图宽度(像素),includeThumbnail=true 时生效 |
|
||||
| thumbnailHeight | Integer | 否 | 缩放图高度(像素),includeThumbnail=true 时生效 |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ----------------------- | ------------------------ |
|
||||
| item | ModelGeneratedAssetItem | 资产详情(结构同第12节) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"assetId": "asset-001",
|
||||
"includeDownloadUrl": true,
|
||||
"includeThumbnail": true,
|
||||
"thumbnailWidth": 200,
|
||||
"thumbnailHeight": 200
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. 获取存储额度与用量
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/getStorageQuota`
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数(`workspace`、`tenantId` 必填)。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------------- | ------ | --------------------------------------------- |
|
||||
| freeStorageQuota | Long | 平台免费存储额度(单位:字节) |
|
||||
| usedStorageSize | Long | 当前用户已使用的存储量(单位:字节) |
|
||||
| extraStoragePrice | String | 超出免费额度的费用说明(如 "¥0.12元/GB/月") |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. 通用 MQ 消息发送
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/sendMqMessage`
|
||||
|
||||
向指定的 RocketMQ Producer 发送 JSON 格式的消息。producerType 对应 `EnumRocketMqProducerType` 枚举的 code 值,mainAccountUid 作为消息路由 key。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------ | ---- | --------------------------------------------------------------------------------------------------- |
|
||||
| producerType | String | 是 | 生产者类型:`WHITE_LIST_ASSET_PRODUCER` / `OSS_DATA_HANDEL_PRODUCER` / `ORIGIN_ASSET_INFO_PRODUCER` |
|
||||
| messageBody | String | 是 | JSON 格式的消息体字符串 |
|
||||
| messageKey | String | 否 | 消息 key(可选,为空时默认使用 mainAccountUid) |
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | ------------ |
|
||||
| success | Boolean | 是否发送成功 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890",
|
||||
"producerType": "ORIGIN_ASSET_INFO_PRODUCER",
|
||||
"messageBody": "{\"time\":1700000000000,\"modelId\":\"model-abc\",\"type\":\"IMAGE\",\"workspace\":\"ws-xxxxx\",\"ossUrl\":\"oss://my-bucket/path/to/asset.png\"}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. 查询模型列表
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/listModels`
|
||||
|
||||
返回当前服务管理的模型配置列表,按资产类型分组,包含每个模型的ID及是否可信标识。
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ---------------- | ---------------------------- |
|
||||
| modelGroups | List<ModelGroup> | 按资产类型分组的模型配置列表 |
|
||||
|
||||
**ModelGroup 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | --------------- | ------------------------------------- |
|
||||
| assetType | String(Enum) | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
|
||||
| models | List<ModelItem> | 该类型下管理的模型列表 |
|
||||
|
||||
**ModelItem 结构:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | -------------- |
|
||||
| modelId | String | 模型ID |
|
||||
| trusted | Boolean | 该模型是否可信 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"modelGroups": [
|
||||
{
|
||||
"assetType": "IMAGE",
|
||||
"models": [
|
||||
{ "modelId": "qwen-image-3.0", "trusted": true },
|
||||
{ "modelId": "qwen-image-3.0-pro", "trusted": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"models": [
|
||||
{ "modelId": "wan2.7-t2v", "trusted": true },
|
||||
{ "modelId": "wan2.7-i2v", "trusted": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. 查询用户是否已开通资产中心服务
|
||||
|
||||
**POST** `/zelda/api/v1/bailian/asset/checkAssetServiceSubscription`
|
||||
|
||||
查询当前用户是否已开通资产中心服务。
|
||||
|
||||
### 请求参数
|
||||
|
||||
仅需公共参数(`mainAccountUid` 必填)。
|
||||
|
||||
### 响应 data
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------- | ------------------------------------------------- |
|
||||
| enabled | Boolean | 是否已开通资产中心服务:true-已开通,false-未开通 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "ws-xxxxx",
|
||||
"tenantId": "123456",
|
||||
"mainAccountUid": "1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,67 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
...ASSET_ID_FLAG,
|
||||
permanent: {
|
||||
type: "switch",
|
||||
description: "Permanently delete assets (cannot be restored)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center delete` — 删除资产(默认软删到回收站)。
|
||||
*
|
||||
* 软删可恢复;--permanent 为永久删除。支持重复 --id(单次最多 100 个)。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Delete assets (soft delete to recycle bin by default)",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...] [flags]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002", "--id asset-001 --permanent"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const deleteType = flags.permanent ? "PERMANENT_DELETE" : "SOFT_DELETE";
|
||||
const body = { assetIdList, deleteType };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchDeleteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchDeleteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const verb = flags.permanent ? "Permanently deleted" : "Deleted";
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`${verb} ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult(
|
||||
{ affected_count: data.affectedCount ?? assetIdList.length, delete_type: deleteType },
|
||||
format,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetDownloadResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload } from "./utils.ts";
|
||||
|
||||
const DOWNLOAD_FLAGS = {
|
||||
id: {
|
||||
type: "string",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID to get download URL for",
|
||||
required: true,
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center download` — 通过资产 ID 获取签名下载链接。
|
||||
*
|
||||
* 调用 batchGetAssetDownloadUrl,输出 download URL,不落盘。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Get a signed download URL for an asset by ID",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id>",
|
||||
flags: DOWNLOAD_FLAGS,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --output json", "--id asset-001 --quiet"],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetId = flags.id;
|
||||
const body = { assetIdList: [assetId] };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
asset_id: assetId,
|
||||
action: "download",
|
||||
...dryRunPayload(settings, identity.binName, ASSET_API.batchGetAssetDownloadUrl, body),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetDownloadResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchGetAssetDownloadUrl,
|
||||
body,
|
||||
);
|
||||
|
||||
const url = data.items?.[0]?.downloadUrl;
|
||||
if (!url) {
|
||||
throw new BailianError(`No download URL available for ${assetId}.`, ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
if (settings.quiet) {
|
||||
emitBare(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult({ asset_id: assetId, download_url: url }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("AssetId", 16)} ${assetId}`);
|
||||
emitBare(`${padEnd("DownloadUrl", 16)} ${url}`);
|
||||
},
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center favorite` — 收藏一个或多个资产。
|
||||
*
|
||||
* 支持重复 --id(单次最多 100 个),调用 batchFavoriteAsset。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Add assets to favorites",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...]",
|
||||
flags: ASSET_ID_FLAG,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const body = { assetIdList };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchFavoriteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchFavoriteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`Favorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetGetResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload, formatGenerateTime } from "./utils.ts";
|
||||
|
||||
const GET_FLAGS = {
|
||||
assetId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Asset ID to query",
|
||||
required: true,
|
||||
},
|
||||
includeDownloadUrl: {
|
||||
type: "switch",
|
||||
description: "Include signed download URL",
|
||||
},
|
||||
includeThumbnail: {
|
||||
type: "switch",
|
||||
description: "Include thumbnail URL",
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail width in pixels",
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail height in pixels",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center get` — 按 ID 查询单个资产详情。
|
||||
*
|
||||
* 可选 --include-download-url / --include-thumbnail 获取签名 URL。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Get full details of a model-generated asset",
|
||||
auth: "console",
|
||||
usageArgs: "--asset-id <id> [flags]",
|
||||
flags: GET_FLAGS,
|
||||
exampleArgs: [
|
||||
"--asset-id asset-001",
|
||||
"--asset-id asset-001 --include-download-url --output json",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetId = flags.assetId;
|
||||
|
||||
const body: Record<string, unknown> = { assetId };
|
||||
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
|
||||
if (flags.includeThumbnail) body.includeThumbnail = true;
|
||||
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
|
||||
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.getModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetGetResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.getModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const item = data.item;
|
||||
if (!item) {
|
||||
emitBare("Asset not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(item, format);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("AssetId", 16)} ${item.assetId ?? "-"}`);
|
||||
emitBare(`${padEnd("Type", 16)} ${item.assetType ?? "-"}`);
|
||||
emitBare(`${padEnd("Name", 16)} ${item.assetName ?? "-"}`);
|
||||
emitBare(`${padEnd("Description", 16)} ${item.assetDescription ?? "-"}`);
|
||||
emitBare(`${padEnd("Model", 16)} ${item.modelName ?? "-"}`);
|
||||
emitBare(`${padEnd("Favorited", 16)} ${item.favorited ? "yes" : "no"}`);
|
||||
emitBare(`${padEnd("Generated", 16)} ${formatGenerateTime(item.generateTime)}`);
|
||||
if (item.downloadUrl) emitBare(`${padEnd("DownloadUrl", 16)} ${item.downloadUrl}`);
|
||||
if (item.thumbnailUrl) emitBare(`${padEnd("ThumbnailUrl", 16)} ${item.thumbnailUrl}`);
|
||||
},
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, formatTable } from "bailian-cli-runtime";
|
||||
import type { AssetListResponse, ModelGeneratedAssetItem } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_LIST_FILTER_FLAGS,
|
||||
buildListFilterBody,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
formatGenerateTime,
|
||||
} from "./utils.ts";
|
||||
|
||||
const LIST_FLAGS = {
|
||||
...ASSET_LIST_FILTER_FLAGS,
|
||||
includeDownloadUrl: {
|
||||
type: "switch",
|
||||
description: "Include signed download URLs in the response",
|
||||
},
|
||||
includeThumbnail: {
|
||||
type: "switch",
|
||||
description: "Include thumbnail URLs in the response",
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail width in pixels",
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: "number",
|
||||
valueHint: "<px>",
|
||||
description: "Thumbnail height in pixels",
|
||||
},
|
||||
pageSize: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: "Results per page (default: 10, max: 100)",
|
||||
},
|
||||
nextToken: {
|
||||
type: "number",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the next page",
|
||||
},
|
||||
preToken: {
|
||||
type: "number",
|
||||
valueHint: "<token>",
|
||||
description: "Cursor for the previous page",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
function normalizeItem(item: ModelGeneratedAssetItem) {
|
||||
return {
|
||||
asset_id: item.assetId ?? "",
|
||||
asset_type: item.assetType ?? "",
|
||||
asset_name: item.assetName ?? "",
|
||||
model_name: item.modelName ?? "",
|
||||
favorited: item.favorited ?? false,
|
||||
generate_time: item.generateTime,
|
||||
download_url: item.downloadUrl,
|
||||
thumbnail_url: item.thumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `bl asset-center list` — 分页查询模型生成资产列表。
|
||||
*
|
||||
* 支持类型/模型/关键词/收藏/回收站/OSS 同步状态/时间范围筛选,以及
|
||||
* --next-token / --pre-token 游标翻页;可选返回签名下载链接与缩略图 URL。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "List model-generated assets with filters and cursor pagination",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: LIST_FLAGS,
|
||||
exampleArgs: [
|
||||
"",
|
||||
"--type IMAGE --model qwen-image-3.0",
|
||||
"--favorited --page-size 20",
|
||||
"--recycle-bin",
|
||||
"--keyword landscape --output json",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const pageSize = flags.pageSize ?? 10;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
...buildListFilterBody(flags),
|
||||
pageSize,
|
||||
};
|
||||
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
|
||||
if (flags.includeThumbnail) body.includeThumbnail = true;
|
||||
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
|
||||
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
|
||||
if (flags.nextToken !== undefined) body.nextToken = flags.nextToken;
|
||||
if (flags.preToken !== undefined) body.preToken = flags.preToken;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.listModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetListResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.listModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
const items = (data.dataList ?? []).map(normalizeItem);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
items,
|
||||
pre_token: data.preToken,
|
||||
next_token: data.nextToken,
|
||||
has_next: data.hasNext,
|
||||
has_pre: data.hasPre,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
emitBare("No assets found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = ["ASSET_ID", "TYPE", "NAME", "MODEL", "FAVORITED", "GENERATED"];
|
||||
const rows = items.map((item) => [
|
||||
item.asset_id,
|
||||
item.asset_type,
|
||||
item.asset_name,
|
||||
item.model_name,
|
||||
item.favorited ? "yes" : "-",
|
||||
formatGenerateTime(item.generate_time),
|
||||
]);
|
||||
for (const line of formatTable(headers, rows)) emitBare(line);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (data.hasPre) parts.push("has previous page");
|
||||
if (data.hasNext) parts.push(`next token: ${data.nextToken}`);
|
||||
if (parts.length > 0) emitBare(`\n${parts.join("; ")}`);
|
||||
},
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetCountResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_LIST_FILTER_FLAGS,
|
||||
buildListFilterBody,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
} from "./utils.ts";
|
||||
|
||||
const STATS_FLAGS = {
|
||||
...ASSET_LIST_FILTER_FLAGS,
|
||||
syncFailed: {
|
||||
type: "switch",
|
||||
description: "Also count assets with failed OSS sync",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
* `bl asset-center stats` — 按类型统计模型生成资产数量。
|
||||
*
|
||||
* 复用 list 的筛选条件;--sync-failed 额外统计 OSS 同步失败的资产数。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Count model-generated assets by type",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: STATS_FLAGS,
|
||||
exampleArgs: ["", "--sync-failed", "--type IMAGE --output json"],
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const body = buildListFilterBody(flags);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.countModelGeneratedAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetCountResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.countModelGeneratedAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
let syncFailedCount: number | undefined;
|
||||
if (flags.syncFailed) {
|
||||
const failed = await callAssetApi<AssetCountResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.countModelGeneratedAsset,
|
||||
{ ...body, syncOssDataStatus: "SYNC_FAILED" },
|
||||
);
|
||||
syncFailedCount = failed.totalCount ?? 0;
|
||||
}
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
total_count: data.totalCount ?? 0,
|
||||
image_count: data.imageCount ?? 0,
|
||||
video_count: data.videoCount ?? 0,
|
||||
audio_count: data.audioCount ?? 0,
|
||||
...(syncFailedCount !== undefined ? { sync_failed_count: syncFailedCount } : {}),
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("Total", 14)} ${data.totalCount ?? 0}`);
|
||||
emitBare(`${padEnd("Image", 14)} ${data.imageCount ?? 0}`);
|
||||
emitBare(`${padEnd("Video", 14)} ${data.videoCount ?? 0}`);
|
||||
emitBare(`${padEnd("Audio", 14)} ${data.audioCount ?? 0}`);
|
||||
if (syncFailedCount !== undefined) {
|
||||
emitBare(`${padEnd("Sync failed", 14)} ${syncFailedCount}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
|
||||
import type { AssetStorageQuotaResponse } from "./types.ts";
|
||||
import { ASSET_API, callAssetApi, dryRunPayload, formatStorageBytes } from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center storage` — 查看存储配额、已用容量与超额计费说明。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "View storage quota, usage, and overage pricing",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
exampleArgs: ["", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings, identity } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(dryRunPayload(settings, identity.binName, ASSET_API.getStorageQuota, {}), format);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetStorageQuotaResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.getStorageQuota,
|
||||
{},
|
||||
);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(
|
||||
{
|
||||
used_storage_size: data.usedStorageSize,
|
||||
free_storage_quota: data.freeStorageQuota,
|
||||
extra_storage_price: data.extraStoragePrice,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emitBare(`${padEnd("Used", 14)} ${formatStorageBytes(data.usedStorageSize)}`);
|
||||
emitBare(`${padEnd("Free quota", 14)} ${formatStorageBytes(data.freeStorageQuota)}`);
|
||||
emitBare(`${padEnd("Overage", 14)} ${data.extraStoragePrice ?? "-"}`);
|
||||
},
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
export type AssetType = "IMAGE" | "VIDEO" | "AUDIO";
|
||||
|
||||
export type AssetDeleteStatus = "NORMAL" | "SOFT_DELETED" | "PERMANENTLY_DELETED";
|
||||
|
||||
export type AssetSyncOssStatus = "NOT_SYNCED" | "IN_SYNCING" | "SYNC_SUCCESS" | "SYNC_FAILED";
|
||||
|
||||
export type AssetDeleteType = "SOFT_DELETE" | "PERMANENT_DELETE";
|
||||
|
||||
export interface AssetHttpBaseRequest {
|
||||
workspace: string;
|
||||
tenantId?: string;
|
||||
mainAccountUid?: string;
|
||||
apiSource?: string;
|
||||
}
|
||||
|
||||
export interface ModelGeneratedAssetItem {
|
||||
id?: number;
|
||||
assetId?: string;
|
||||
assetType?: string;
|
||||
assetName?: string;
|
||||
assetDescription?: string;
|
||||
favorited?: boolean;
|
||||
assetSize?: number;
|
||||
modelType?: string;
|
||||
modelName?: string;
|
||||
deleteStatus?: string;
|
||||
syncOssDataStatus?: string;
|
||||
generateTime?: number;
|
||||
downloadUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
gmtCreate?: string;
|
||||
gmtModified?: string;
|
||||
}
|
||||
|
||||
export interface AssetListResponse {
|
||||
dataList?: ModelGeneratedAssetItem[];
|
||||
preToken?: number;
|
||||
nextToken?: number;
|
||||
hasNext?: boolean;
|
||||
hasPre?: boolean;
|
||||
}
|
||||
|
||||
export interface AssetGetResponse {
|
||||
item?: ModelGeneratedAssetItem;
|
||||
}
|
||||
|
||||
export interface AssetBatchResponse {
|
||||
success?: boolean;
|
||||
affectedCount?: number;
|
||||
}
|
||||
|
||||
export interface AssetDownloadUrlItem {
|
||||
assetId?: string;
|
||||
downloadUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface AssetDownloadResponse {
|
||||
items?: AssetDownloadUrlItem[];
|
||||
}
|
||||
|
||||
export interface AssetCountResponse {
|
||||
imageCount?: number;
|
||||
videoCount?: number;
|
||||
audioCount?: number;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export interface AssetStorageQuotaResponse {
|
||||
freeStorageQuota?: number;
|
||||
usedStorageSize?: number;
|
||||
extraStoragePrice?: string;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import type { AssetBatchResponse } from "./types.ts";
|
||||
import {
|
||||
ASSET_API,
|
||||
ASSET_ID_FLAG,
|
||||
callAssetApi,
|
||||
dryRunPayload,
|
||||
validateAssetIds,
|
||||
} from "./utils.ts";
|
||||
|
||||
/**
|
||||
* `bl asset-center unfavorite` — 取消收藏一个或多个资产。
|
||||
*
|
||||
* 支持重复 --id(单次最多 100 个),调用 batchUnfavoriteAsset。
|
||||
*/
|
||||
export default defineCommand({
|
||||
description: "Remove assets from favorites",
|
||||
auth: "console",
|
||||
usageArgs: "--id <asset-id> [--id <asset-id>...]",
|
||||
flags: ASSET_ID_FLAG,
|
||||
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
|
||||
validate(flags) {
|
||||
return validateAssetIds(flags.id);
|
||||
},
|
||||
async run(ctx) {
|
||||
const { settings, identity, flags } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const assetIdList = flags.id;
|
||||
const body = { assetIdList };
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
dryRunPayload(settings, identity.binName, ASSET_API.batchUnfavoriteAsset, body),
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await callAssetApi<AssetBatchResponse>(
|
||||
ctx.client,
|
||||
settings,
|
||||
identity.binName,
|
||||
ASSET_API.batchUnfavoriteAsset,
|
||||
body,
|
||||
);
|
||||
|
||||
if (settings.quiet || format === "text") {
|
||||
emitBare(`Unfavorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
|
||||
} else {
|
||||
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
effectiveConsoleGatewayConfig,
|
||||
type Client,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import type { AssetHttpBaseRequest, AssetSyncOssStatus, AssetType } from "./types.ts";
|
||||
|
||||
const ASSET_SERVICE = "dashscopeModel";
|
||||
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
|
||||
export const MAX_ASSET_BATCH_SIZE = 100;
|
||||
|
||||
export const ASSET_API = {
|
||||
listModelGeneratedAsset: assetApi("listModelGeneratedAsset"),
|
||||
getModelGeneratedAsset: assetApi("getModelGeneratedAsset"),
|
||||
batchFavoriteAsset: assetApi("batchFavoriteAsset"),
|
||||
batchUnfavoriteAsset: assetApi("batchUnfavoriteAsset"),
|
||||
batchDeleteAsset: assetApi("batchDeleteAsset"),
|
||||
batchGetAssetDownloadUrl: assetApi("batchGetAssetDownloadUrl"),
|
||||
countModelGeneratedAsset: assetApi("countModelGeneratedAsset"),
|
||||
getStorageQuota: assetApi("getStorageQuota"),
|
||||
} as const;
|
||||
|
||||
export const ASSET_ID_FLAG = {
|
||||
id: {
|
||||
type: "array",
|
||||
valueHint: "<asset-id>",
|
||||
description: "Asset ID(s) to operate on (repeatable, max 100)",
|
||||
required: true,
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const ASSET_LIST_FILTER_FLAGS = {
|
||||
type: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: "Asset type: IMAGE, VIDEO, or AUDIO",
|
||||
choices: ["IMAGE", "VIDEO", "AUDIO"] as const,
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Filter by model name",
|
||||
},
|
||||
keyword: {
|
||||
type: "string",
|
||||
valueHint: "<text>",
|
||||
description: "Filter by asset name (substring match)",
|
||||
},
|
||||
favorited: {
|
||||
type: "switch",
|
||||
description: "Show or count only favorited assets",
|
||||
},
|
||||
recycleBin: {
|
||||
type: "switch",
|
||||
description: "Show or count soft-deleted assets (recycle bin)",
|
||||
},
|
||||
syncStatus: {
|
||||
type: "string",
|
||||
valueHint: "<status>",
|
||||
description: "OSS sync status filter",
|
||||
choices: ["NOT_SYNCED", "IN_SYNCING", "SYNC_SUCCESS", "SYNC_FAILED"] as const,
|
||||
},
|
||||
beginTime: {
|
||||
type: "string",
|
||||
valueHint: "<datetime>",
|
||||
description: "Filter by generate time start (ISO_LOCAL_DATE_TIME)",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
valueHint: "<datetime>",
|
||||
description: "Filter by generate time end (ISO_LOCAL_DATE_TIME)",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
type AssetListFilterFlags = ParsedFlags<typeof ASSET_LIST_FILTER_FLAGS>;
|
||||
|
||||
function assetApi(action: string): string {
|
||||
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
|
||||
}
|
||||
|
||||
function getNestedRecord(
|
||||
obj: Record<string, unknown>,
|
||||
key: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const val = obj[key];
|
||||
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractAssetResponse<T>(result: unknown): T {
|
||||
const raw = result as Record<string, unknown>;
|
||||
const data = getNestedRecord(raw, "data");
|
||||
if (!data) {
|
||||
throw new BailianError("Unexpected empty response from asset API.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const dataV2 = getNestedRecord(data, "DataV2");
|
||||
const payload = dataV2
|
||||
? (getNestedRecord(getNestedRecord(dataV2, "data") ?? dataV2, "data") ??
|
||||
getNestedRecord(dataV2, "data") ??
|
||||
dataV2)
|
||||
: (getNestedRecord(data, "data") ?? data);
|
||||
|
||||
if (payload.success === false) {
|
||||
const message =
|
||||
typeof payload.message === "string" && payload.message.length > 0
|
||||
? payload.message
|
||||
: typeof payload.code === "string"
|
||||
? payload.code
|
||||
: "Asset API request failed.";
|
||||
throw new BailianError(message, ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
if (payload.data !== undefined) {
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export function requireWorkspaceId(settings: Settings, binName: string): string {
|
||||
if (settings.workspaceId) return settings.workspaceId;
|
||||
|
||||
throw new BailianError(
|
||||
`workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or \`${binName} config set workspace_id <id>\`.`,
|
||||
ExitCode.GENERAL,
|
||||
`Run \`${binName} workspace list\` to view available workspaces.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildBaseRequest(settings: Settings, binName: string): AssetHttpBaseRequest {
|
||||
// workspace 由 CLI 注入;tenantId / mainAccountUid 由 Console 网关从登录 session 自动填充,
|
||||
// CLI 侧无需也不应手动解析阿里云账号 ID。
|
||||
return {
|
||||
workspace: requireWorkspaceId(settings, binName),
|
||||
apiSource: "CLI",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildListFilterBody(flags: AssetListFilterFlags): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {};
|
||||
|
||||
if (flags.type) body.assetType = flags.type as AssetType;
|
||||
if (flags.model) body.modelName = flags.model;
|
||||
if (flags.keyword) body.assetName = flags.keyword;
|
||||
if (flags.favorited) body.favorited = true;
|
||||
if (flags.recycleBin) {
|
||||
body.deleteStatus = "SOFT_DELETED";
|
||||
} else {
|
||||
body.deleteStatus = "NORMAL";
|
||||
}
|
||||
if (flags.syncStatus) body.syncOssDataStatus = flags.syncStatus as AssetSyncOssStatus;
|
||||
if (flags.beginTime) body.beginTime = flags.beginTime;
|
||||
if (flags.endTime) body.endTime = flags.endTime;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function validateAssetIds(ids: string[] | undefined): string | undefined {
|
||||
if (!ids || ids.length === 0) {
|
||||
return "At least one --id is required.";
|
||||
}
|
||||
if (ids.length > MAX_ASSET_BATCH_SIZE) {
|
||||
return `At most ${MAX_ASSET_BATCH_SIZE} asset IDs are allowed per request.`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function callAssetApi<T>(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
binName: string,
|
||||
api: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const payload = { ...buildBaseRequest(settings, binName), ...body };
|
||||
const raw = await client.console(api, payload);
|
||||
return extractAssetResponse<T>(raw);
|
||||
}
|
||||
|
||||
export function dryRunPayload(
|
||||
settings: Settings,
|
||||
binName: string,
|
||||
api: string,
|
||||
body: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
api,
|
||||
data: { ...buildBaseRequest(settings, binName), ...body },
|
||||
...effectiveConsoleGatewayConfig(settings),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGenerateTime(ts?: number): string {
|
||||
if (ts == null) return "-";
|
||||
return new Date(ts).toISOString().replace("T", " ").slice(0, 19);
|
||||
}
|
||||
|
||||
export function formatStorageBytes(bytes?: number): string {
|
||||
if (bytes == null) return "-";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default defineCommand({
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
`--api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
|
||||
`--api zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
|
||||
`--api some.api.name --data '{"key":"value"}' --console-region cn-beijing`,
|
||||
],
|
||||
async run(ctx) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BailianError } from "bailian-cli-core";
|
||||
import { BailianError, isStreamableHttpUnsupported } from "bailian-cli-core";
|
||||
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
|
||||
|
||||
/** Detect MCP-not-activated / invalid 404 errors (CLI-wrapped server message). */
|
||||
export function isMcpNotActivated(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
const message = error.message;
|
||||
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
|
||||
if (!/^MCP request failed:\s*404\b/i.test(message)) return false;
|
||||
return /未开通|MCP不存在|MCP_IS_INVALID/i.test(message);
|
||||
}
|
||||
|
||||
@@ -26,14 +26,28 @@ export function mcpActivateHint(serverCode: string): string {
|
||||
/**
|
||||
* For not-activated errors, keep the original message / exitCode and append a hint only.
|
||||
* Do not replace the server error message.
|
||||
* WebSearch + 405 streamableHttp: do not fall back; attach a re-activate / upgrade hint.
|
||||
*/
|
||||
export function rethrowWithMcpActivateHint(error: unknown, serverCode: string): never {
|
||||
if (isMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
|
||||
if (!(error instanceof BailianError) || error.hint) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (isMcpNotActivated(error)) {
|
||||
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
|
||||
if (serverCode === "WebSearch" && isStreamableHttpUnsupported(error)) {
|
||||
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ const CALL_FLAGS = {
|
||||
url: {
|
||||
type: "string",
|
||||
valueHint: "<url>",
|
||||
description: "Override the MCP endpoint URL (for non-Bailian servers)",
|
||||
description:
|
||||
"Override the MCP endpoint URL (non-Bailian). Tries Streamable HTTP first, then classic SSE on the same URL.",
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
type CallFlags = ParsedFlags<typeof CALL_FLAGS>;
|
||||
@@ -114,14 +115,14 @@ export default defineCommand({
|
||||
const { serverCode, toolName } = parseTarget(flags.target);
|
||||
const toolArgs = buildToolArgs(flags);
|
||||
|
||||
const url = flags.url || ctx.client.url(bailianMcpPath(serverCode));
|
||||
const previewUrl = flags.url || ctx.client.url(bailianMcpPath(serverCode));
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
server: serverCode,
|
||||
url,
|
||||
url: previewUrl,
|
||||
tool: toolName,
|
||||
arguments: toolArgs,
|
||||
},
|
||||
@@ -130,13 +131,14 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
let client: { close?(): void } | undefined;
|
||||
try {
|
||||
await client.initialize();
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
const connected = await ctx.client.connectBailianMcp(serverCode, flags.url);
|
||||
client = connected.client;
|
||||
const result = await connected.client.callTool(toolName, toolArgs);
|
||||
|
||||
if (result.isError) {
|
||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||
const errText = result.content.map((contentItem) => contentItem.text || "").join("\n");
|
||||
throw new BailianError(`Tool error: ${errText}`);
|
||||
}
|
||||
|
||||
@@ -146,6 +148,8 @@ export default defineCommand({
|
||||
rethrowWithMcpActivateHint(error, serverCode);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
client?.close?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ export default defineCommand({
|
||||
url: {
|
||||
type: "string",
|
||||
valueHint: "<url>",
|
||||
description: "Override the MCP endpoint URL (for non-Bailian servers)",
|
||||
description:
|
||||
"Override the MCP endpoint URL (non-Bailian). Tries Streamable HTTP first, then classic SSE on the same URL.",
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
@@ -28,24 +29,27 @@ export default defineCommand({
|
||||
const { settings, flags } = ctx;
|
||||
const code = flags.server;
|
||||
|
||||
const url = flags.url || ctx.client.url(bailianMcpPath(code));
|
||||
const previewUrl = flags.url || ctx.client.url(bailianMcpPath(code));
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ server: code, url, action: "tools/list" }, format);
|
||||
emitResult({ server: code, url: previewUrl, action: "tools/list" }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
let client: { close?(): void } | undefined;
|
||||
try {
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ server: code, url, tools }, format);
|
||||
const connected = await ctx.client.connectBailianMcp(code, flags.url);
|
||||
client = connected.client;
|
||||
const tools = await connected.client.listTools();
|
||||
emitResult({ server: code, url: connected.url, tools }, format);
|
||||
} catch (error) {
|
||||
if (!flags.url) {
|
||||
rethrowWithMcpActivateHint(error, code);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
client?.close?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,13 @@ import {
|
||||
stripUndefined,
|
||||
taskPath,
|
||||
speechRecognizePath,
|
||||
resolveAsrApi,
|
||||
buildAsrFlashRequest,
|
||||
buildAsyncAsrLanguageFields,
|
||||
collectAsrTranscriptionItems,
|
||||
extractAsrFlashText,
|
||||
type AsrApiRoute,
|
||||
type AsrFlashFamily,
|
||||
type OutputFormat,
|
||||
type FlagsDef,
|
||||
type ParsedFlags,
|
||||
@@ -27,8 +34,18 @@ const RECOGNIZE_FLAGS = {
|
||||
description: "Audio file URL or local file path (repeatable, max 100)",
|
||||
required: true,
|
||||
},
|
||||
model: { type: "string", valueHint: "<model>", description: "Model ID (default: fun-asr)" },
|
||||
language: { type: "string", valueHint: "<lang>", description: "Language hint (e.g. zh, en, ja)" },
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description:
|
||||
"Model ID (default: fun-asr). Async: fun-asr / *-filetrans / paraformer-*; sync: qwen3-asr-flash* / fun-asr-flash* / qwen-audio-*-asr-flash",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
valueHint: "<lang>",
|
||||
description:
|
||||
"Language hint (e.g. zh, en, ja). Classic async/input-audio: language_hints; qwen3-filetrans: language; qwen3 sync: asr_options.language",
|
||||
},
|
||||
diarization: { type: "switch", description: "Enable automatic speaker diarization" },
|
||||
speakerCount: {
|
||||
type: "number",
|
||||
@@ -55,8 +72,33 @@ const RECOGNIZE_FLAGS = {
|
||||
} satisfies FlagsDef;
|
||||
type RecognizeFlags = ParsedFlags<typeof RECOGNIZE_FLAGS>;
|
||||
|
||||
function assertSyncFlashFlagsAllowed(
|
||||
flags: RecognizeFlags,
|
||||
model: string,
|
||||
flashFamily: AsrFlashFamily,
|
||||
): void {
|
||||
const unsupported: string[] = [];
|
||||
if (flags.diarization === true) unsupported.push("--diarization");
|
||||
if (flags.speakerCount !== undefined) unsupported.push("--speaker-count");
|
||||
// qwen3 sync Flash does not use vocabulary_id; input-audio Flash (fun-asr-flash* / qwen-audio-*-asr-flash) does
|
||||
if (flashFamily === "qwen3" && flags.vocabularyId !== undefined) {
|
||||
unsupported.push("--vocabulary-id");
|
||||
}
|
||||
if (flags.channelId !== undefined) unsupported.push("--channel-id");
|
||||
if (flags.async === true) unsupported.push("--async");
|
||||
if (flags.pollInterval !== undefined) unsupported.push("--poll-interval");
|
||||
|
||||
if (unsupported.length > 0) {
|
||||
throw new BailianError(
|
||||
`Model "${model}" uses sync Flash ASR and does not support: ${unsupported.join(", ")}.\n` +
|
||||
`Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Recognize speech from audio files (FunAudio-ASR)",
|
||||
description: "Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--url <audio-url> [flags]",
|
||||
flags: RECOGNIZE_FLAGS,
|
||||
@@ -68,6 +110,7 @@ export default defineCommand({
|
||||
"--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123",
|
||||
"--url https://example.com/audio.mp3 --out result.json",
|
||||
"--url https://example.com/audio.mp3 --async --quiet",
|
||||
"--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
@@ -90,22 +133,70 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const model = flags.model || "fun-asr";
|
||||
const route = resolveAsrApi(model);
|
||||
if (route.kind === "unsupported") {
|
||||
throw new BailianError(
|
||||
route.unsupportedReason ?? `Unsupported ASR model: ${model}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
if (route.kind === "sync-flash") {
|
||||
assertSyncFlashFlagsAllowed(flags, model, route.flashFamily!);
|
||||
if (rawUrls.length !== 1) {
|
||||
throw new BailianError(
|
||||
`Model "${model}" is a sync Flash ASR model and accepts exactly one --url (got ${rawUrls.length}).\n` +
|
||||
`Hint: Pass a single audio URL, or use an async filetrans model for batch files.`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
route.kind === "async-filetrans" &&
|
||||
route.asyncInputStyle === "file_url" &&
|
||||
rawUrls.length !== 1
|
||||
) {
|
||||
throw new BailianError(
|
||||
`Model "${model}" accepts exactly one --url (got ${rawUrls.length}).\n` +
|
||||
"Hint: qwen3-asr-flash-filetrans* requires a single file_url.",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
// Auto-upload local files in parallel
|
||||
const resolvedUrls = await Promise.all(rawUrls.map((u) => ctx.client.uploadFile(u, model)));
|
||||
const resolvedUrls = await Promise.all(rawUrls.map((url) => ctx.client.uploadFile(url, model)));
|
||||
|
||||
if (route.kind === "sync-flash") {
|
||||
await handleSyncFlashMode(
|
||||
ctx.client,
|
||||
settings,
|
||||
flags,
|
||||
format,
|
||||
model,
|
||||
route,
|
||||
resolvedUrls[0]!,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const channelId = flags.channelId;
|
||||
const language = flags.language;
|
||||
const vocabularyId = flags.vocabularyId;
|
||||
const languageFields = buildAsyncAsrLanguageFields(
|
||||
route.asyncLanguageStyle ?? "language_hints",
|
||||
flags.language,
|
||||
);
|
||||
|
||||
const body: DashScopeASRRequest = {
|
||||
model,
|
||||
input: {
|
||||
file_urls: resolvedUrls,
|
||||
},
|
||||
input:
|
||||
route.asyncInputStyle === "file_url"
|
||||
? { file_url: resolvedUrls[0]! }
|
||||
: { file_urls: resolvedUrls },
|
||||
parameters: {
|
||||
channel_id: channelId !== undefined ? [channelId] : [0],
|
||||
language_hints: language ? [language] : undefined,
|
||||
...languageFields,
|
||||
diarization_enabled: diarization ? true : undefined,
|
||||
speaker_count: speakerCount,
|
||||
vocabulary_id: vocabularyId,
|
||||
@@ -116,7 +207,7 @@ export default defineCommand({
|
||||
stripUndefined(body.parameters as Record<string, unknown>);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ request: body, mode: "async" }, format);
|
||||
emitResult({ request: body, mode: "async", path: speechRecognizePath() }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,6 +219,55 @@ export default defineCommand({
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSyncFlashMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
flags: RecognizeFlags,
|
||||
format: OutputFormat,
|
||||
model: string,
|
||||
route: AsrApiRoute,
|
||||
audioUrl: string,
|
||||
): Promise<void> {
|
||||
const flashFamily = route.flashFamily as AsrFlashFamily;
|
||||
const body = buildAsrFlashRequest({
|
||||
model,
|
||||
audioUrl,
|
||||
language: flags.language,
|
||||
vocabularyId: flags.vocabularyId,
|
||||
flashFamily,
|
||||
});
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ request: body, mode: "sync", path: route.path }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.quiet) {
|
||||
process.stderr.write(`[Model: ${model}] [Mode: sync] [Files: 1]\n`);
|
||||
}
|
||||
|
||||
const response = await client.requestJson<Record<string, unknown>>({
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
headers: { "X-DashScope-SSE": "disable" },
|
||||
body,
|
||||
});
|
||||
|
||||
const text = extractAsrFlashText(response, flashFamily);
|
||||
if (text) {
|
||||
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
||||
} else {
|
||||
emitBare(JSON.stringify(response));
|
||||
}
|
||||
|
||||
if (flags.out) {
|
||||
writeFileSync(flags.out, JSON.stringify(response, null, 2) + "\n");
|
||||
if (!settings.quiet) {
|
||||
process.stderr.write(`Full result saved to: ${flags.out}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAsyncMode(
|
||||
client: Client,
|
||||
settings: Settings,
|
||||
@@ -160,16 +300,16 @@ async function handleAsyncMode(
|
||||
url: pollUrl,
|
||||
intervalSec: pollInterval,
|
||||
timeoutSec: settings.timeout,
|
||||
isComplete: (d) => (d as DashScopeASRTaskResult).output.task_status === "SUCCEEDED",
|
||||
isFailed: (d) => (d as DashScopeASRTaskResult).output.task_status === "FAILED",
|
||||
getStatus: (d) => (d as DashScopeASRTaskResult).output.task_status,
|
||||
getErrorMessage: (d) => {
|
||||
const o = (d as DashScopeASRTaskResult).output;
|
||||
return (o as unknown as Record<string, unknown>).message as string | undefined;
|
||||
isComplete: (data) => (data as DashScopeASRTaskResult).output.task_status === "SUCCEEDED",
|
||||
isFailed: (data) => (data as DashScopeASRTaskResult).output.task_status === "FAILED",
|
||||
getStatus: (data) => (data as DashScopeASRTaskResult).output.task_status,
|
||||
getErrorMessage: (data) => {
|
||||
const output = (data as DashScopeASRTaskResult).output;
|
||||
return (output as unknown as Record<string, unknown>).message as string | undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const results = result.output.results ?? [];
|
||||
const results = collectAsrTranscriptionItems(result.output);
|
||||
|
||||
if (results.length === 0) {
|
||||
emitResult({ task_id: taskId, status: result.output.task_status }, format);
|
||||
@@ -179,12 +319,14 @@ async function handleAsyncMode(
|
||||
// Collect all transcription data for --out
|
||||
const allTransData: Record<string, unknown>[] = [];
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const subResult = results[i]!;
|
||||
for (let index = 0; index < results.length; index++) {
|
||||
const subResult = results[index]!;
|
||||
const isMulti = fileCount > 1;
|
||||
|
||||
if (isMulti) {
|
||||
process.stdout.write(`=== [${i + 1}/${results.length}] ${subResult.file_url ?? ""} ===\n`);
|
||||
process.stdout.write(
|
||||
`=== [${index + 1}/${results.length}] ${subResult.file_url ?? ""} ===\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (subResult.subtask_status === "FAILED") {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { printQuotaBox, readNumber, type QuotaSection } from "./quota-box.ts";
|
||||
import { formatNumber } from "./shared.ts";
|
||||
|
||||
const CODING_PLAN_USAGE_API =
|
||||
"zeldaEasy.broadscope-bailian.codingPlan.queryCodingPlanInstanceInfoV2";
|
||||
|
||||
const COMMODITY_CODES: Record<string, string> = {
|
||||
domestic: "sfm_codingplan_public_cn",
|
||||
international: "sfm_codingplan_public_intl",
|
||||
};
|
||||
|
||||
interface CodingPlanWindow {
|
||||
usedQuota?: number;
|
||||
totalQuota?: number;
|
||||
/** Usage ratio in [0, 1]; absent when the window has no positive total or no used value. */
|
||||
percentage?: number;
|
||||
resetTime?: number;
|
||||
}
|
||||
|
||||
interface CodingPlanUsage {
|
||||
instanceType?: string;
|
||||
per5Hour: CodingPlanWindow;
|
||||
perWeek: CodingPlanWindow;
|
||||
perBillMonth: CodingPlanWindow;
|
||||
}
|
||||
|
||||
function readWindow(
|
||||
quotaInfo: Record<string, unknown> | undefined,
|
||||
fieldPrefix: string,
|
||||
): CodingPlanWindow {
|
||||
const window: CodingPlanWindow = {};
|
||||
if (!quotaInfo) return window;
|
||||
|
||||
const usedQuota = readNumber(quotaInfo[`${fieldPrefix}UsedQuota`]);
|
||||
if (usedQuota !== undefined) window.usedQuota = usedQuota;
|
||||
const totalQuota = readNumber(quotaInfo[`${fieldPrefix}TotalQuota`]);
|
||||
if (totalQuota !== undefined) window.totalQuota = totalQuota;
|
||||
const resetTime = readNumber(quotaInfo[`${fieldPrefix}QuotaNextRefreshTime`]);
|
||||
if (resetTime !== undefined) window.resetTime = resetTime;
|
||||
|
||||
// Console rule: the usage rate only exists with a positive total and a used value.
|
||||
if (usedQuota !== undefined && totalQuota !== undefined && totalQuota > 0) {
|
||||
window.percentage = usedQuota / totalQuota;
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
/** Pick the first VALID instance's quota info, mirroring the Coding Plan console. */
|
||||
function readUsage(result: unknown): CodingPlanUsage | undefined {
|
||||
const response = unwrapResponse(result as Record<string, unknown>);
|
||||
const instances = Array.isArray(response.codingPlanInstanceInfos)
|
||||
? (response.codingPlanInstanceInfos as Record<string, unknown>[])
|
||||
: [];
|
||||
const validInstance = instances.find((instance) => instance.status === "VALID");
|
||||
if (!validInstance) return undefined;
|
||||
|
||||
const quotaInfo = validInstance.codingPlanQuotaInfo as Record<string, unknown> | undefined;
|
||||
const usage: CodingPlanUsage = {
|
||||
per5Hour: readWindow(quotaInfo, "per5Hour"),
|
||||
perWeek: readWindow(quotaInfo, "perWeek"),
|
||||
perBillMonth: readWindow(quotaInfo, "perBillMonth"),
|
||||
};
|
||||
if (typeof validInstance.instanceType === "string" && validInstance.instanceType) {
|
||||
usage.instanceType = validInstance.instanceType;
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
function toSection(label: string, window: CodingPlanWindow): QuotaSection {
|
||||
const section: QuotaSection = {
|
||||
label,
|
||||
emptyMessage: "No quota data for this window; verify in the Bailian Coding Plan console.",
|
||||
percentage: window.percentage,
|
||||
resetTime: window.resetTime,
|
||||
};
|
||||
if (window.usedQuota !== undefined && window.totalQuota !== undefined) {
|
||||
section.detail = `Used: ${formatNumber(window.usedQuota)} / ${formatNumber(window.totalQuota)}`;
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
function printView(usage: CodingPlanUsage, generatedAt: number): void {
|
||||
const planSuffix = usage.instanceType ? ` (${usage.instanceType})` : "";
|
||||
printQuotaBox(
|
||||
`Coding Plan Usage${planSuffix}`,
|
||||
[
|
||||
toSection("5-hour quota", usage.per5Hour),
|
||||
toSection("1-week quota", usage.perWeek),
|
||||
toSection("Monthly quota", usage.perBillMonth),
|
||||
],
|
||||
generatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show Coding Plan quota usage",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
exampleArgs: ["", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const requestData = {
|
||||
queryCodingPlanInstanceInfoRequest: {
|
||||
commodityCode: COMMODITY_CODES[settings.consoleSite ?? "domestic"],
|
||||
onlyLatestOne: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ api: CODING_PLAN_USAGE_API, data: requestData }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.console(CODING_PLAN_USAGE_API, requestData);
|
||||
const usage = readUsage(result);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(usage ?? {}, format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!usage) {
|
||||
process.stdout.write("No active Coding Plan subscription found.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printView(usage, Date.now());
|
||||
},
|
||||
});
|
||||
@@ -1,95 +1,22 @@
|
||||
import { defineCommand, detectOutputFormat, fetchModelList, type Client } from "bailian-cli-core";
|
||||
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
FREE_TIER_API,
|
||||
FREE_TIER_ONLY_STATUS_API,
|
||||
extractFreeTierOnlyStatuses,
|
||||
extractQuotas,
|
||||
fetchAllModels,
|
||||
pollFreeTierBatch,
|
||||
} from "./shared.ts";
|
||||
|
||||
const ACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchActivateFreeTierOnly";
|
||||
const DEACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchDeactivateFreeTierOnly";
|
||||
const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota";
|
||||
const FREE_TIER_ONLY_STATUS_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierOnlyStatus";
|
||||
|
||||
interface FreeTierQuota {
|
||||
model: string;
|
||||
quotaTotal: number;
|
||||
quotaInitTotal: number;
|
||||
}
|
||||
|
||||
interface FreeTierOnlyStatus {
|
||||
model: string;
|
||||
freeTierOnly: boolean;
|
||||
}
|
||||
const ACTIVATE_API = "zeldaEasy.bailian-commerce.freeTrial.batchActivateFreeTierOnly";
|
||||
const DEACTIVATE_API = "zeldaEasy.bailian-commerce.freeTrial.batchDeactivateFreeTierOnly";
|
||||
|
||||
interface BatchResultFailure {
|
||||
failureModelId: string;
|
||||
errorCode: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 500;
|
||||
const MAX_POLLS = 20;
|
||||
|
||||
async function pollUntilDone(
|
||||
client: Client,
|
||||
api: string,
|
||||
requestKey: string,
|
||||
models: string[],
|
||||
): Promise<unknown> {
|
||||
let nextTaskId: string | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < MAX_POLLS; attempt++) {
|
||||
const requestData = {
|
||||
[requestKey]: nextTaskId ? { taskId: nextTaskId } : { models },
|
||||
};
|
||||
|
||||
const raw = await client.console(api, requestData);
|
||||
|
||||
const resp = extractResponseData(raw as Record<string, unknown>);
|
||||
if (resp.taskId && Object.keys(resp).length === 1) {
|
||||
nextTaskId = resp.taskId as string;
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
continue;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchAllModelNames(client: Client): Promise<string[]> {
|
||||
const allModels: Record<string, unknown>[] = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const result = await fetchModelList((api, data) => client.console(api, data), {
|
||||
pageNo: page,
|
||||
pageSize: 50,
|
||||
});
|
||||
allModels.push(...result.models);
|
||||
if (allModels.length >= result.total) break;
|
||||
page++;
|
||||
}
|
||||
return allModels.map((item) => item.model as string).filter(Boolean);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description:
|
||||
"Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable",
|
||||
@@ -161,7 +88,7 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
if (!modelFlag) {
|
||||
models = await fetchAllModelNames(ctx.client);
|
||||
models = (await fetchAllModels(ctx.client)).map((model) => model.name);
|
||||
}
|
||||
|
||||
if (off) {
|
||||
@@ -172,12 +99,10 @@ export default defineCommand({
|
||||
}),
|
||||
]);
|
||||
|
||||
const quotaData = extractResponseData(quotaResult as Record<string, unknown>);
|
||||
const quotas = (quotaData.freeTierQuotas ?? []) as FreeTierQuota[];
|
||||
const quotas = extractQuotas(quotaResult);
|
||||
const quotaMap = new Map(quotas.map((quota) => [quota.model, quota]));
|
||||
|
||||
const stopData = extractResponseData(stopResult as Record<string, unknown>);
|
||||
const stopStatuses = (stopData.freeTierOnlyStatuses ?? []) as FreeTierOnlyStatus[];
|
||||
const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
|
||||
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));
|
||||
|
||||
for (const name of models) {
|
||||
@@ -192,7 +117,7 @@ export default defineCommand({
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await pollUntilDone(ctx.client, api, requestKey, [name]);
|
||||
await pollFreeTierBatch(ctx.client, api, requestKey, [name]);
|
||||
process.stdout.write(`Disabled auto-stop for "${name}".\n`);
|
||||
}
|
||||
return;
|
||||
@@ -200,13 +125,13 @@ export default defineCommand({
|
||||
|
||||
const jsonResults: unknown[] = [];
|
||||
for (const name of models) {
|
||||
const result = await pollUntilDone(ctx.client, api, requestKey, [name]);
|
||||
const result = await pollFreeTierBatch(ctx.client, api, requestKey, [name]);
|
||||
if (format === "json") {
|
||||
jsonResults.push(result);
|
||||
continue;
|
||||
}
|
||||
if (result) {
|
||||
const resultData = extractResponseData(result as Record<string, unknown>);
|
||||
const resultData = unwrapResponse(result as Record<string, unknown>);
|
||||
const failureModels = (resultData.failureModels as BatchResultFailure[]) ?? [];
|
||||
if (failureModels.length > 0) {
|
||||
process.stderr.write(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
ansi,
|
||||
displayWidth,
|
||||
renderGauge,
|
||||
type GaugeCell,
|
||||
type TextStyle,
|
||||
} from "bailian-cli-runtime";
|
||||
import { formatDateTime } from "./shared.ts";
|
||||
|
||||
const BOX_WIDTH = 76;
|
||||
|
||||
/** One quota window rendered inside the box: a label + usage ratio + reset time. */
|
||||
export interface QuotaSection {
|
||||
label: string;
|
||||
/** Shown instead of the gauge when the usage ratio is absent. */
|
||||
emptyMessage: string;
|
||||
/** Usage ratio in [0, 1]; absent means no data (possibly unlimited). */
|
||||
percentage?: number;
|
||||
resetTime?: number;
|
||||
/** Optional dim line under the gauge, e.g. "Used: 38 / 100". */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/** Accept only finite numbers; anything else counts as absent (possibly unlimited). */
|
||||
export function readNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
/** Match the `usage free` gauge label style: 0.1% precision, no trailing zeros. */
|
||||
function formatPercentage(ratio: number): string {
|
||||
const percent = Math.round(ratio * 1000) / 10;
|
||||
return `${Number.isInteger(percent) ? percent : percent.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatRemainingTime(resetTime: number, now: number): string {
|
||||
const remainingMs = Math.max(0, resetTime - now);
|
||||
const totalMinutes = Math.floor(remainingMs / 60_000);
|
||||
if (totalMinutes === 0) return "now";
|
||||
|
||||
const days = Math.floor(totalMinutes / (24 * 60));
|
||||
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const parts: string[] = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}m`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/** Print a bordered quota box with a title line and one gauge per section. */
|
||||
export function printQuotaBox(title: string, sections: QuotaSection[], generatedAt: number): void {
|
||||
const color = ansi(process.stdout);
|
||||
const writeLine = (text = "", style?: TextStyle) => {
|
||||
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${text}`));
|
||||
process.stdout.write(`│ ${style ? style(text) : text}${" ".repeat(padding)}│\n`);
|
||||
};
|
||||
// Pre-colored gauge cell: pad from the plain variant so ANSI escapes never shift the border.
|
||||
const writeGaugeLine = (cell: GaugeCell) => {
|
||||
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${cell.plain}`));
|
||||
process.stdout.write(`│ ${cell.colored}${" ".repeat(padding)}│\n`);
|
||||
};
|
||||
const writeQuota = (section: QuotaSection) => {
|
||||
writeLine(section.label, color.bold);
|
||||
if (section.percentage === undefined) {
|
||||
writeLine(section.emptyMessage, color.dim);
|
||||
return;
|
||||
}
|
||||
|
||||
const gaugeLabel = `${formatPercentage(section.percentage)} used`;
|
||||
writeGaugeLine(renderGauge(section.percentage * 100, gaugeLabel));
|
||||
if (section.detail) {
|
||||
writeLine(section.detail, color.dim);
|
||||
}
|
||||
if (section.resetTime === undefined) {
|
||||
writeLine("Resets: not applicable (no usage yet)", color.dim);
|
||||
return;
|
||||
}
|
||||
|
||||
const resetText = `Resets: ${formatDateTime(section.resetTime)} (in ${formatRemainingTime(section.resetTime, generatedAt)})`;
|
||||
writeLine(resetText, color.dim);
|
||||
};
|
||||
|
||||
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);
|
||||
writeLine(title, color.cyan);
|
||||
writeLine(`Generated at: ${formatDateTime(generatedAt)} (local time)`, color.dim);
|
||||
for (const section of sections) {
|
||||
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
|
||||
writeQuota(section);
|
||||
}
|
||||
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
|
||||
}
|
||||
@@ -24,6 +24,14 @@ export function formatDate(ts: number): string {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(ts: number): string {
|
||||
const date = new Date(ts);
|
||||
const hour = String(date.getHours()).padStart(2, "0");
|
||||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||||
const second = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${formatDate(ts)} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
export function requireWorkspaceId(settings: Settings, binName: string): string {
|
||||
if (settings.workspaceId) return settings.workspaceId;
|
||||
|
||||
@@ -102,9 +110,9 @@ export async function fetchAllModels(client: Client): Promise<ModelInfo[]> {
|
||||
// Free-tier quota
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota";
|
||||
export const FREE_TIER_API = "zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota";
|
||||
export const FREE_TIER_ONLY_STATUS_API =
|
||||
"zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierOnlyStatus";
|
||||
"zeldaEasy.bailian-commerce.freeTrial.queryFreeTierOnlyStatus";
|
||||
|
||||
export interface FreeTierQuota {
|
||||
model: string;
|
||||
@@ -257,22 +265,27 @@ export interface ListStatisticResponse {
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 500;
|
||||
const MAX_POLLS = 30;
|
||||
const DEFAULT_MAX_POLLS = 30;
|
||||
|
||||
export async function pollTelemetryApi(
|
||||
/**
|
||||
* Poll a console API until it returns a terminal (non task-id) response.
|
||||
* The gateway answers an async request with a bare `{taskId}` envelope; the
|
||||
* caller re-issues with that id until real data arrives or the budget runs out.
|
||||
* `buildRequest` shapes each attempt (initial call vs. taskId follow-up) so the
|
||||
* same loop serves every request-wrapper convention (telemetry `reqDTO`,
|
||||
* free-tier batch `…Request`).
|
||||
*/
|
||||
export async function pollConsoleUntilDone(
|
||||
client: Client,
|
||||
api: string,
|
||||
reqDTO: Record<string, unknown>,
|
||||
buildRequest: (taskId: string | undefined) => Record<string, unknown>,
|
||||
maxPolls = DEFAULT_MAX_POLLS,
|
||||
): Promise<unknown> {
|
||||
let nextTaskId: string | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < MAX_POLLS; attempt++) {
|
||||
const requestData = nextTaskId
|
||||
? { reqDTO: { ...reqDTO, asyncTaskId: nextTaskId } }
|
||||
: { reqDTO };
|
||||
|
||||
const raw = await client.console(api, requestData);
|
||||
const resp = extractResponseData(raw as Record<string, unknown>);
|
||||
for (let attempt = 0; attempt < maxPolls; attempt++) {
|
||||
const raw = await client.console(api, buildRequest(nextTaskId));
|
||||
const resp = unwrapResponse(raw as Record<string, unknown>);
|
||||
|
||||
if (resp.taskId && Object.keys(resp).length === 1) {
|
||||
nextTaskId = resp.taskId as string;
|
||||
@@ -284,6 +297,32 @@ export async function pollTelemetryApi(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Telemetry APIs wrap the payload in `reqDTO` and echo the task id as `asyncTaskId`. */
|
||||
export async function pollTelemetryApi(
|
||||
client: Client,
|
||||
api: string,
|
||||
reqDTO: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
return pollConsoleUntilDone(client, api, (taskId) =>
|
||||
taskId ? { reqDTO: { ...reqDTO, asyncTaskId: taskId } } : { reqDTO },
|
||||
);
|
||||
}
|
||||
|
||||
/** Free-tier batch activate/deactivate wrap the payload in `requestKey` and echo `taskId`. */
|
||||
export async function pollFreeTierBatch(
|
||||
client: Client,
|
||||
api: string,
|
||||
requestKey: string,
|
||||
models: string[],
|
||||
): Promise<unknown> {
|
||||
return pollConsoleUntilDone(
|
||||
client,
|
||||
api,
|
||||
(taskId) => ({ [requestKey]: taskId ? { taskId } : { models } }),
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
export function extractOverviewData(result: unknown): OverviewStatistic | undefined {
|
||||
const resp = extractResponseData(result as Record<string, unknown>);
|
||||
if (resp.callSuccessCount !== undefined || resp.usages !== undefined) {
|
||||
|
||||
@@ -1,176 +1,26 @@
|
||||
import {
|
||||
defineCommand,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
detectOutputFormat,
|
||||
type Settings,
|
||||
type Client,
|
||||
} from "bailian-cli-core";
|
||||
import { defineCommand, BailianError, ExitCode, detectOutputFormat } from "bailian-cli-core";
|
||||
import { ansi, emitResult } from "bailian-cli-runtime";
|
||||
import { displayWidth, padEnd } from "bailian-cli-runtime";
|
||||
|
||||
const OVERVIEW_API = "zeldaEasy.bailian-telemetry.model.getModelUsageStatistic";
|
||||
const LIST_API = "zeldaEasy.bailian-telemetry.model.listModelUsageStatisticData";
|
||||
|
||||
interface UsageItem {
|
||||
key: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface OverviewStatistic {
|
||||
callCount: number;
|
||||
modelCount: number;
|
||||
callSuccessCount: number;
|
||||
usages: UsageItem[];
|
||||
}
|
||||
|
||||
interface ModelStatisticItem {
|
||||
model: string;
|
||||
callSuccessCount: number;
|
||||
usages?: UsageItem[];
|
||||
usage?: Record<string, number | undefined>;
|
||||
}
|
||||
|
||||
interface ListStatisticResponse {
|
||||
list: ModelStatisticItem[];
|
||||
totalCount: number;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 500;
|
||||
const MAX_POLLS = 30;
|
||||
|
||||
async function pollTelemetryApi(
|
||||
client: Client,
|
||||
api: string,
|
||||
reqDTO: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
let nextTaskId: string | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < MAX_POLLS; attempt++) {
|
||||
const requestData = nextTaskId
|
||||
? { reqDTO: { ...reqDTO, asyncTaskId: nextTaskId } }
|
||||
: { reqDTO };
|
||||
|
||||
const raw = await client.console(api, requestData);
|
||||
|
||||
const resp = extractResponseData(raw as Record<string, unknown>);
|
||||
|
||||
if (resp.taskId && Object.keys(resp).length === 1) {
|
||||
nextTaskId = resp.taskId as string;
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requireWorkspaceId(settings: Settings, binName: string): string {
|
||||
if (settings.workspaceId) return settings.workspaceId;
|
||||
|
||||
throw new BailianError(
|
||||
`workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or \`${binName} config set workspace_id <id>\`.`,
|
||||
ExitCode.GENERAL,
|
||||
`Run \`${binName} workspace list\` to view available workspaces.`,
|
||||
);
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function formatDate(ts: number): string {
|
||||
const date = new Date(ts);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function extractOverviewData(result: unknown): OverviewStatistic | undefined {
|
||||
const resp = extractResponseData(result as Record<string, unknown>);
|
||||
if (resp.callSuccessCount !== undefined || resp.usages !== undefined) {
|
||||
return resp as unknown as OverviewStatistic;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractListData(result: unknown): ListStatisticResponse {
|
||||
const resp = extractResponseData(result as Record<string, unknown>);
|
||||
const list = (resp.list as ModelStatisticItem[]) ?? [];
|
||||
const totalCount = (resp.totalCount as number) ?? 0;
|
||||
const maxResults = (resp.maxResults as number) ?? 0;
|
||||
return { list, totalCount, maxResults };
|
||||
}
|
||||
|
||||
function resolveUsageMap(item: ModelStatisticItem): Record<string, number> {
|
||||
const out: Record<string, number> = {};
|
||||
if (item.usages && Array.isArray(item.usages)) {
|
||||
for (const entry of item.usages) {
|
||||
if (entry.key && entry.value != null) {
|
||||
out[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.usage && typeof item.usage === "object") {
|
||||
for (const [key, val] of Object.entries(item.usage)) {
|
||||
if (val != null) out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
import {
|
||||
LIST_API,
|
||||
OVERVIEW_API,
|
||||
USAGE_KEY_LABELS,
|
||||
extractListData,
|
||||
extractOverviewData,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
pollTelemetryApi,
|
||||
requireWorkspaceId,
|
||||
resolveUsageMap,
|
||||
type ModelStatisticItem,
|
||||
type OverviewStatistic,
|
||||
} from "./shared.ts";
|
||||
|
||||
interface UsageLabel {
|
||||
en: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const USAGE_KEY_LABELS: Record<string, UsageLabel> = {
|
||||
total_token: { en: "Total Tokens", unit: "tokens" },
|
||||
input_token: { en: "Input Tokens", unit: "tokens" },
|
||||
output_token: { en: "Output Tokens", unit: "tokens" },
|
||||
input_token_cache: { en: "Cached Tokens", unit: "tokens" },
|
||||
input_token_cache_read: { en: "Cache Read", unit: "tokens" },
|
||||
input_token_cache_creation: { en: "Cache Creation", unit: "tokens" },
|
||||
thinking_input_token: { en: "Thinking Input", unit: "tokens" },
|
||||
thinking_output_token: { en: "Thinking Output", unit: "tokens" },
|
||||
text_input_token: { en: "Text Input", unit: "tokens" },
|
||||
purein_text_output_token: { en: "Text Output", unit: "tokens" },
|
||||
embedding_token: { en: "Embedding", unit: "tokens" },
|
||||
image_number: { en: "Images", unit: "images" },
|
||||
video_duration: { en: "Video Duration", unit: "sec" },
|
||||
content_duration: { en: "Audio Duration", unit: "sec" },
|
||||
tts_text_number: { en: "TTS Chars", unit: "chars" },
|
||||
total_token_avg: { en: "Avg Tokens/Req" },
|
||||
};
|
||||
|
||||
function formatLabel(label: UsageLabel): string {
|
||||
const unitSuffix = label.unit ? ` [${label.unit}]` : "";
|
||||
return `${label.en}${unitSuffix}`;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { printQuotaBox, readNumber } from "./quota-box.ts";
|
||||
|
||||
const TOKEN_PLAN_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
|
||||
interface TokenPlanUsage {
|
||||
per5HourPercentage?: number;
|
||||
per5HourResetTime?: number;
|
||||
per1WeekPercentage?: number;
|
||||
per1WeekResetTime?: number;
|
||||
}
|
||||
|
||||
function readUsage(result: unknown): TokenPlanUsage {
|
||||
const response = unwrapResponse(result as Record<string, unknown>);
|
||||
const usage: TokenPlanUsage = {};
|
||||
|
||||
const per5HourPercentage = readNumber(response.per5HourPercentage);
|
||||
if (per5HourPercentage !== undefined) usage.per5HourPercentage = per5HourPercentage;
|
||||
const per5HourResetTime = readNumber(response.per5HourResetTime);
|
||||
if (per5HourResetTime !== undefined) usage.per5HourResetTime = per5HourResetTime;
|
||||
const per1WeekPercentage = readNumber(response.per1WeekPercentage);
|
||||
if (per1WeekPercentage !== undefined) usage.per1WeekPercentage = per1WeekPercentage;
|
||||
const per1WeekResetTime = readNumber(response.per1WeekResetTime);
|
||||
if (per1WeekResetTime !== undefined) usage.per1WeekResetTime = per1WeekResetTime;
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
function printView(usage: TokenPlanUsage, generatedAt: number): void {
|
||||
printQuotaBox(
|
||||
"Token Plan Usage",
|
||||
[
|
||||
{
|
||||
label: "5-hour quota",
|
||||
emptyMessage:
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
percentage: usage.per5HourPercentage,
|
||||
resetTime: usage.per5HourResetTime,
|
||||
},
|
||||
{
|
||||
label: "1-week quota",
|
||||
emptyMessage:
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
percentage: usage.per1WeekPercentage,
|
||||
resetTime: usage.per1WeekResetTime,
|
||||
},
|
||||
],
|
||||
generatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show Token Plan quota usage",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
exampleArgs: ["", "--output json"],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ api: TOKEN_PLAN_USAGE_API, data: {} }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.console(TOKEN_PLAN_USAGE_API, {});
|
||||
const usage = readUsage(result);
|
||||
|
||||
if (format === "json") {
|
||||
emitResult(usage, format);
|
||||
return;
|
||||
}
|
||||
|
||||
printView(usage, Date.now());
|
||||
},
|
||||
});
|
||||
@@ -48,6 +48,8 @@ export { default as usageFree } from "./commands/usage/free.ts";
|
||||
export { default as usageFreetier } from "./commands/usage/freetier.ts";
|
||||
export { default as usageStats } from "./commands/usage/stats.ts";
|
||||
export { default as usageSummary } from "./commands/usage/summary.ts";
|
||||
export { default as usageTokenPlan } from "./commands/usage/token-plan.ts";
|
||||
export { default as usageCodingPlan } from "./commands/usage/coding-plan.ts";
|
||||
export { default as pipelineRun } from "./commands/pipeline/run.ts";
|
||||
export { default as pipelineValidate } from "./commands/pipeline/validate.ts";
|
||||
export { default as advisorRecommend } from "./commands/advisor/recommend.ts";
|
||||
@@ -91,14 +93,6 @@ export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.
|
||||
export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts";
|
||||
export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts";
|
||||
export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.ts";
|
||||
export { default as assetList } from "./commands/asset-center/list.ts";
|
||||
export { default as assetGet } from "./commands/asset-center/get.ts";
|
||||
export { default as assetFavorite } from "./commands/asset-center/favorite.ts";
|
||||
export { default as assetUnfavorite } from "./commands/asset-center/unfavorite.ts";
|
||||
export { default as assetDelete } from "./commands/asset-center/delete.ts";
|
||||
export { default as assetDownload } from "./commands/asset-center/download.ts";
|
||||
export { default as assetStats } from "./commands/asset-center/stats.ts";
|
||||
export { default as assetStorage } from "./commands/asset-center/storage.ts";
|
||||
export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
|
||||
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
|
||||
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vite-plus/test";
|
||||
import codingPlanUsage from "../src/commands/usage/coding-plan.ts";
|
||||
|
||||
const originalNoColor = process.env.NO_COLOR;
|
||||
const originalForceColor = process.env.FORCE_COLOR;
|
||||
const originalIsTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
||||
else process.env.NO_COLOR = originalNoColor;
|
||||
if (originalForceColor === undefined) delete process.env.FORCE_COLOR;
|
||||
else process.env.FORCE_COLOR = originalForceColor;
|
||||
if (originalIsTty) Object.defineProperty(process.stdout, "isTTY", originalIsTty);
|
||||
else delete (process.stdout as { isTTY?: boolean }).isTTY;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function captureStdout(): string[] {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
async function runCodingPlan(response: Record<string, unknown>, output?: string): Promise<void> {
|
||||
await codingPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(response) },
|
||||
flags: {},
|
||||
settings: { dryRun: false, output },
|
||||
} as never);
|
||||
}
|
||||
|
||||
function wrapResponse(data: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeInstanceResponse(
|
||||
quotaInfo: Record<string, unknown>,
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Record<string, unknown> {
|
||||
return wrapResponse({
|
||||
codingPlanInstanceInfos: [
|
||||
{ status: "VALID", instanceType: "pro", codingPlanQuotaInfo: quotaInfo, ...overrides },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const FULL_QUOTA_INFO = {
|
||||
per5HourUsedQuota: 38,
|
||||
per5HourTotalQuota: 100,
|
||||
per5HourQuotaNextRefreshTime: 1_786_000_000_000,
|
||||
perWeekUsedQuota: 500,
|
||||
perWeekTotalQuota: 1000,
|
||||
perWeekQuotaNextRefreshTime: 1_786_100_000_000,
|
||||
perBillMonthUsedQuota: 950,
|
||||
perBillMonthTotalQuota: 1000,
|
||||
perBillMonthQuotaNextRefreshTime: 1_786_200_000_000,
|
||||
};
|
||||
|
||||
describe("usage coding-plan view", () => {
|
||||
test("renders the three quota windows with usage rates and used/total details", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(makeInstanceResponse(FULL_QUOTA_INFO));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("Coding Plan Usage (pro)");
|
||||
expect(renderedOutput).toContain("5-hour quota");
|
||||
expect(renderedOutput).toContain("1-week quota");
|
||||
expect(renderedOutput).toContain("Monthly quota");
|
||||
expect(renderedOutput).toContain("38% used");
|
||||
expect(renderedOutput).toContain("50% used");
|
||||
expect(renderedOutput).toContain("95% used");
|
||||
expect(renderedOutput).toContain("Used: 38 / 100");
|
||||
expect(renderedOutput).toContain("Used: 950 / 1,000");
|
||||
});
|
||||
|
||||
test("skips non-VALID instances when picking quota info", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(
|
||||
wrapResponse({
|
||||
codingPlanInstanceInfos: [
|
||||
{
|
||||
status: "EXPIRED",
|
||||
codingPlanQuotaInfo: { per5HourUsedQuota: 1, per5HourTotalQuota: 2 },
|
||||
},
|
||||
{ status: "VALID", codingPlanQuotaInfo: FULL_QUOTA_INFO },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output.join("")).toContain("38% used");
|
||||
});
|
||||
|
||||
test("renders windows without a positive total as missing quota data", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(
|
||||
makeInstanceResponse({
|
||||
per5HourUsedQuota: 38,
|
||||
per5HourTotalQuota: 0,
|
||||
perWeekUsedQuota: 500,
|
||||
perBillMonthUsedQuota: "not-a-number",
|
||||
perBillMonthTotalQuota: 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
const emptyMessageCount = renderedOutput.split(
|
||||
"No quota data for this window; verify in the Bailian Coding Plan console.",
|
||||
).length;
|
||||
expect(emptyMessageCount - 1).toBe(3);
|
||||
});
|
||||
|
||||
test("reports when there is no active subscription", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(wrapResponse({ codingPlanInstanceInfos: [] }));
|
||||
|
||||
expect(output.join("")).toContain("No active Coding Plan subscription found.");
|
||||
});
|
||||
|
||||
test("renders the gauge in the usage-free style: brand fill and proportional cells", async () => {
|
||||
delete process.env.NO_COLOR;
|
||||
process.env.FORCE_COLOR = "3";
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(makeInstanceResponse(FULL_QUOTA_INFO));
|
||||
|
||||
// Brand-cyan fill cell, same as the `usage free` gauge column
|
||||
expect(output.join("")).toContain("\u001B[38;2;0;150;160m\u2588");
|
||||
});
|
||||
|
||||
test("fills gauge cells proportionally to the usage rate", async () => {
|
||||
process.env.NO_COLOR = "1";
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(makeInstanceResponse(FULL_QUOTA_INFO));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
// 95% of the default 20-cell gauge → 19 filled cells + 1 track space
|
||||
expect(renderedOutput).toContain(`${"\u2588".repeat(19)} `);
|
||||
expect(renderedOutput).not.toContain("\u2588".repeat(20));
|
||||
});
|
||||
});
|
||||
|
||||
describe("usage coding-plan json", () => {
|
||||
test("outputs the three windows with used/total/percentage/resetTime", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(makeInstanceResponse(FULL_QUOTA_INFO), "json");
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({
|
||||
instanceType: "pro",
|
||||
per5Hour: {
|
||||
usedQuota: 38,
|
||||
totalQuota: 100,
|
||||
percentage: 0.38,
|
||||
resetTime: 1_786_000_000_000,
|
||||
},
|
||||
perWeek: {
|
||||
usedQuota: 500,
|
||||
totalQuota: 1000,
|
||||
percentage: 0.5,
|
||||
resetTime: 1_786_100_000_000,
|
||||
},
|
||||
perBillMonth: {
|
||||
usedQuota: 950,
|
||||
totalQuota: 1000,
|
||||
percentage: 0.95,
|
||||
resetTime: 1_786_200_000_000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("returns an empty JSON object when no VALID instance exists", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(wrapResponse({}), "json");
|
||||
|
||||
expect(output.join("").trim()).toBe("{}");
|
||||
});
|
||||
|
||||
test("omits non-numeric quota fields from the JSON output", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runCodingPlan(
|
||||
makeInstanceResponse(
|
||||
{ per5HourUsedQuota: "not-a-number", per5HourTotalQuota: 100 },
|
||||
{ instanceType: undefined },
|
||||
),
|
||||
"json",
|
||||
);
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({
|
||||
per5Hour: { totalQuota: 100 },
|
||||
perWeek: {},
|
||||
perBillMonth: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import {
|
||||
@@ -16,6 +18,37 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
*/
|
||||
|
||||
describe("e2e: speech recognize", () => {
|
||||
async function runRecognizeDryRun(args: string[]) {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
...args,
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
"--quiet",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
return parseStdoutJson<{
|
||||
mode?: string;
|
||||
path?: string;
|
||||
request?: {
|
||||
model?: string;
|
||||
parameters?: {
|
||||
format?: string;
|
||||
language_hints?: string[];
|
||||
language?: string;
|
||||
vocabulary_id?: string;
|
||||
};
|
||||
input?: {
|
||||
file_url?: string;
|
||||
file_urls?: string[];
|
||||
messages?: Array<{ content?: Array<{ type?: string }> }>;
|
||||
};
|
||||
};
|
||||
}>(stdout);
|
||||
}
|
||||
|
||||
test("speech recognize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
@@ -25,6 +58,217 @@ describe("e2e: speech recognize", () => {
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/recognize|--url|model|audio/i);
|
||||
});
|
||||
|
||||
test("speech recognize sync-flash dry-run 走 multimodal-generation", async () => {
|
||||
const body = await runRecognizeDryRun([
|
||||
"--model",
|
||||
"qwen-audio-3.0-asr-flash",
|
||||
"--url",
|
||||
"https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav",
|
||||
"--language",
|
||||
"en",
|
||||
"--vocabulary-id",
|
||||
"vocab-e2e",
|
||||
]);
|
||||
expect(body.mode).toBe("sync");
|
||||
expect(body.path).toBe("/api/v1/services/aigc/multimodal-generation/generation");
|
||||
expect(body.request?.model).toBe("qwen-audio-3.0-asr-flash");
|
||||
expect(body.request?.parameters?.format).toBe("wav");
|
||||
expect(body.request?.parameters?.language_hints).toEqual(["en"]);
|
||||
expect(body.request?.parameters?.vocabulary_id).toBe("vocab-e2e");
|
||||
expect(body.request?.input?.messages?.[0]?.content?.[0]?.type).toBe("input_audio");
|
||||
});
|
||||
|
||||
test("speech recognize qwen3 filetrans dry-run 使用 file_url 与 language", async () => {
|
||||
const body = await runRecognizeDryRun([
|
||||
"--model",
|
||||
"qwen3-asr-flash-filetrans",
|
||||
"--url",
|
||||
"https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav",
|
||||
"--language",
|
||||
"zh",
|
||||
]);
|
||||
expect(body.mode).toBe("async");
|
||||
expect(body.path).toBe("/api/v1/services/audio/asr/transcription");
|
||||
expect(body.request?.input?.file_url?.startsWith("https://")).toBe(true);
|
||||
expect(body.request?.input?.file_urls).toBeUndefined();
|
||||
expect(body.request?.parameters?.language).toBe("zh");
|
||||
expect(body.request?.parameters?.language_hints).toBeUndefined();
|
||||
});
|
||||
|
||||
test("speech recognize realtime 模型报用法错误", async () => {
|
||||
// Use --dry-run to skip auth so CI without API keys still hits USAGE(2)
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--model",
|
||||
"qwen3-asr-flash-realtime",
|
||||
"--url",
|
||||
"https://example.com/a.wav",
|
||||
"--dry-run",
|
||||
"--quiet",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/realtime|WebSocket|unsupported/i);
|
||||
});
|
||||
|
||||
test("speech recognize flash 真实请求走 sync endpoint 并落盘 --out", async () => {
|
||||
let requestPath = "";
|
||||
let requestBody: Record<string, unknown> = {};
|
||||
let sseHeader: string | undefined;
|
||||
const server = http.createServer((request, response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
requestPath = request.url ?? "";
|
||||
requestBody = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>;
|
||||
sseHeader = request.headers["x-dashscope-sse"] as string | undefined;
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
output: { text: "flash recognition works" },
|
||||
request_id: "request-146",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address() as AddressInfo;
|
||||
const outDir = makeE2eOutputDir("speech-recognize-flash-sync");
|
||||
const outPath = join(outDir, "result.json");
|
||||
|
||||
try {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--model",
|
||||
"fun-asr-flash-2026-06-15",
|
||||
"--url",
|
||||
"https://example.com/sample.wav",
|
||||
"--api-key",
|
||||
"sk-e2e-placeholder",
|
||||
"--base-url",
|
||||
`http://127.0.0.1:${address.port}`,
|
||||
"--out",
|
||||
outPath,
|
||||
"--quiet",
|
||||
]);
|
||||
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("flash recognition works");
|
||||
expect(requestPath).toBe("/api/v1/services/aigc/multimodal-generation/generation");
|
||||
expect(sseHeader).toBe("disable");
|
||||
expect(requestBody).toMatchObject({
|
||||
model: "fun-asr-flash-2026-06-15",
|
||||
parameters: { format: "wav" },
|
||||
});
|
||||
expect(JSON.parse(readFileSync(outPath, "utf8"))).toMatchObject({
|
||||
output: { text: "flash recognition works" },
|
||||
request_id: "request-146",
|
||||
});
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test("speech recognize flash 多 --url 在发请求前报用法错误", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--model",
|
||||
"qwen-audio-3.0-asr-flash",
|
||||
"--url",
|
||||
"https://example.com/a.wav",
|
||||
"--url",
|
||||
"https://example.com/b.wav",
|
||||
"--dry-run",
|
||||
"--quiet",
|
||||
]);
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/exactly one --url|sync Flash/i);
|
||||
});
|
||||
|
||||
test("speech recognize qwen3-filetrans 轮询成功后下载 result.transcription_url", async () => {
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = request.url ?? "";
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
if (url.startsWith("/api/v1/services/audio/asr/transcription")) {
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
output: { task_id: "task-qwen3", task_status: "PENDING" },
|
||||
request_id: "req-submit",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (url.startsWith("/api/v1/tasks/")) {
|
||||
const address = server.address() as AddressInfo;
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
task_id: "task-qwen3",
|
||||
task_status: "SUCCEEDED",
|
||||
result: {
|
||||
transcription_url: `http://127.0.0.1:${address.port}/transcription.json`,
|
||||
},
|
||||
},
|
||||
request_id: "req-poll",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (url.startsWith("/transcription.json")) {
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
file_url: "https://example.com/a.wav",
|
||||
transcripts: [{ text: "你好世界", sentences: [{ text: "你好世界" }] }],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404);
|
||||
response.end(JSON.stringify({ message: `unexpected path: ${url}` }));
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address() as AddressInfo;
|
||||
const outDir = makeE2eOutputDir("speech-recognize-qwen3-filetrans");
|
||||
const outPath = join(outDir, "result.json");
|
||||
|
||||
try {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--model",
|
||||
"qwen3-asr-flash-filetrans",
|
||||
"--url",
|
||||
"https://example.com/a.wav",
|
||||
"--language",
|
||||
"zh",
|
||||
"--api-key",
|
||||
"sk-e2e-placeholder",
|
||||
"--base-url",
|
||||
`http://127.0.0.1:${address.port}`,
|
||||
"--poll-interval",
|
||||
"1",
|
||||
"--out",
|
||||
outPath,
|
||||
"--quiet",
|
||||
]);
|
||||
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("你好世界");
|
||||
expect(JSON.parse(readFileSync(outPath, "utf8"))).toMatchObject({
|
||||
transcripts: [{ text: "你好世界" }],
|
||||
});
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
@@ -109,6 +109,8 @@ export const USAGE_ROUTES: E2eRouteExports = {
|
||||
"usage free": "usageFree",
|
||||
"usage freetier": "usageFreetier",
|
||||
"usage stats": "usageStats",
|
||||
"usage token-plan": "usageTokenPlan",
|
||||
"usage coding-plan": "usageCodingPlan",
|
||||
};
|
||||
|
||||
export const DEPLOY_ROUTES: E2eRouteExports = {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage coding-plan", () => {
|
||||
test("usage coding-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Coding Plan|quota/i);
|
||||
});
|
||||
|
||||
test("usage coding-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage coding-plan --output json");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: usage coding-plan(Console)", () => {
|
||||
test("usage coding-plan --dry-run 输出网关请求计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { queryCodingPlanInstanceInfoRequest?: Record<string, unknown> };
|
||||
}>(stdout);
|
||||
expect(data.api).toBe("zeldaEasy.broadscope-bailian.codingPlan.queryCodingPlanInstanceInfoV2");
|
||||
expect(data.data?.queryCodingPlanInstanceInfoRequest).toEqual({
|
||||
commodityCode: "sfm_codingplan_public_cn",
|
||||
onlyLatestOne: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("usage coding-plan --output json 返回窗口结构", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "coding-plan", "--output", "json"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
per5Hour?: { percentage?: number };
|
||||
perWeek?: { percentage?: number };
|
||||
perBillMonth?: { percentage?: number };
|
||||
}>(result.stdout);
|
||||
// 无有效订阅返回 {};有订阅时三个窗口必须存在
|
||||
if (Object.keys(data).length > 0) {
|
||||
expect(data.per5Hour).toBeTypeOf("object");
|
||||
expect(data.perWeek).toBeTypeOf("object");
|
||||
expect(data.perBillMonth).toBeTypeOf("object");
|
||||
}
|
||||
});
|
||||
|
||||
test("usage coding-plan 默认渲染生成时间与额度窗口或无订阅提示", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "coding-plan"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
if (result.stdout.includes("No active Coding Plan subscription found.")) return;
|
||||
expect(result.stdout).toContain("Generated at:");
|
||||
expect(result.stdout).toContain("5-hour quota");
|
||||
expect(result.stdout).toContain("1-week quota");
|
||||
expect(result.stdout).toContain("Monthly quota");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage token-plan", () => {
|
||||
test("usage token-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Token Plan|quota/i);
|
||||
});
|
||||
|
||||
test("usage token-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage token-plan --output json");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () => {
|
||||
test("usage token-plan --dry-run 输出网关请求计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string; data?: Record<string, unknown> }>(stdout);
|
||||
expect(data.api).toBe("zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage");
|
||||
expect(data.data).toEqual({});
|
||||
});
|
||||
|
||||
test("usage token-plan --output json 返回可用的额度字段", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--output", "json"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
per5HourPercentage?: number;
|
||||
per5HourResetTime?: number;
|
||||
per1WeekPercentage?: number;
|
||||
per1WeekResetTime?: number;
|
||||
}>(result.stdout);
|
||||
const fields = [
|
||||
data.per5HourPercentage,
|
||||
data.per5HourResetTime,
|
||||
data.per1WeekPercentage,
|
||||
data.per1WeekResetTime,
|
||||
];
|
||||
for (const field of fields) {
|
||||
if (field !== undefined) expect(field).toBeTypeOf("number");
|
||||
}
|
||||
});
|
||||
|
||||
test("usage token-plan 默认渲染生成时间与两个额度窗口", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("Generated at:");
|
||||
expect(result.stdout).toContain("5-hour quota");
|
||||
expect(result.stdout).toContain("1-week quota");
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,12 @@ describe("mcp-activate-hint", () => {
|
||||
false,
|
||||
);
|
||||
expect(isMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
|
||||
// Nested wrapper phrase must not match (anchored at start).
|
||||
expect(
|
||||
isMcpNotActivated(
|
||||
new BailianError("MCP error (-32000): MCP request failed: 404 Not Found - 未开通"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("hint 含对应 server 的 MCP 广场深链", () => {
|
||||
@@ -38,6 +44,36 @@ describe("mcp-activate-hint", () => {
|
||||
expect(mcpActivateHint("WebSearch")).toMatch(/SSE|Streamable HTTP/i);
|
||||
});
|
||||
|
||||
test("WebSearch + 405 streamableHttp 补重开通 hint", () => {
|
||||
const original = new BailianError(
|
||||
"MCP request failed: 405 Method Not Allowed - current mcp not support streamableHttp",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(original, "WebSearch");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(BailianError);
|
||||
const wrapped = error as BailianError;
|
||||
expect(wrapped.message).toBe(original.message);
|
||||
expect(wrapped.hint).toMatch(/SSE|Streamable HTTP|Activate|re-activate/i);
|
||||
expect(wrapped.hint).toContain(mcpMarketplaceDetailPage("WebSearch"));
|
||||
}
|
||||
});
|
||||
|
||||
test("非 WebSearch 的 405 streamableHttp 不补 hint(由 fallback 处理)", () => {
|
||||
const original = new BailianError(
|
||||
"MCP request failed: 405 Method Not Allowed - current mcp not support streamableHttp",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(original, "WebParser");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("rethrow 保留原 message,补 hint", () => {
|
||||
const serverCode = "market-cmapi00073529";
|
||||
const original = new BailianError(
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vite-plus/test";
|
||||
import tokenPlanUsage from "../src/commands/usage/token-plan.ts";
|
||||
|
||||
const originalNoColor = process.env.NO_COLOR;
|
||||
const originalForceColor = process.env.FORCE_COLOR;
|
||||
const originalIsTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
||||
else process.env.NO_COLOR = originalNoColor;
|
||||
if (originalForceColor === undefined) delete process.env.FORCE_COLOR;
|
||||
else process.env.FORCE_COLOR = originalForceColor;
|
||||
if (originalIsTty) Object.defineProperty(process.stdout, "isTTY", originalIsTty);
|
||||
else delete (process.stdout as { isTTY?: boolean }).isTTY;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function captureStdout(): string[] {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
async function runTokenPlan(response: Record<string, unknown>, output?: string): Promise<void> {
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(response) },
|
||||
flags: {},
|
||||
settings: { dryRun: false, output },
|
||||
} as never);
|
||||
}
|
||||
|
||||
function makeUsageResponse(
|
||||
per5HourPercentage?: number,
|
||||
per1WeekPercentage = per5HourPercentage,
|
||||
): Record<string, unknown> {
|
||||
const usage: Record<string, number> = {};
|
||||
if (per5HourPercentage !== undefined) {
|
||||
usage.per5HourPercentage = per5HourPercentage;
|
||||
if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000;
|
||||
}
|
||||
if (per1WeekPercentage !== undefined) {
|
||||
usage.per1WeekPercentage = per1WeekPercentage;
|
||||
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
|
||||
}
|
||||
|
||||
return wrapResponse(usage);
|
||||
}
|
||||
|
||||
function wrapResponse(usage: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
data: {
|
||||
DataV2: {
|
||||
data: {
|
||||
data: usage,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("usage token-plan view", () => {
|
||||
test("renders the gauge with the usage-free brand fill color", async () => {
|
||||
delete process.env.NO_COLOR;
|
||||
process.env.FORCE_COLOR = "3";
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0.5));
|
||||
|
||||
// Brand-cyan fill cell, same as the `usage free` gauge column
|
||||
expect(output.join("")).toContain("\u001B[38;2;0;150;160m\u2588");
|
||||
});
|
||||
|
||||
test("renders proportional gauge cells with a transparent track", async () => {
|
||||
process.env.NO_COLOR = "1";
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0.5));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
// 50% of the default 20-cell gauge → 10 filled cells + 10 track spaces
|
||||
expect(renderedOutput).toContain(`${"\u2588".repeat(10)}${" ".repeat(10)}`);
|
||||
expect(renderedOutput).not.toContain("\u2588".repeat(11));
|
||||
});
|
||||
|
||||
test("accepts missing reset times when the quota usage is zero", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0));
|
||||
|
||||
expect(output.join("")).toContain("Resets: not applicable (no usage yet)");
|
||||
});
|
||||
|
||||
test("allows one unused quota window without masking another reset time", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0, 0.5));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("Resets: not applicable (no usage yet)");
|
||||
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test("renders missing quota windows as possibly unlimited", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse());
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders only the missing quota window as possibly unlimited", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(undefined, 0.5));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).not.toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test("renders a window with a missing percentage as possibly unlimited even when its reset time is present", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(wrapResponse({ per5HourResetTime: 1_786_000_000_000 }));
|
||||
|
||||
expect(output.join("")).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
|
||||
test("treats non-numeric quota fields as absent instead of failing", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(
|
||||
wrapResponse({ per5HourPercentage: "not-a-number", per1WeekPercentage: Number.NaN }),
|
||||
);
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("usage token-plan json", () => {
|
||||
test("outputs the four core usage fields with --output json", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0.5, 0.25), "json");
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({
|
||||
per5HourPercentage: 0.5,
|
||||
per5HourResetTime: 1_786_000_000_000,
|
||||
per1WeekPercentage: 0.25,
|
||||
per1WeekResetTime: 1_786_100_000_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns an empty JSON object when no quota fields are available", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(), "json");
|
||||
|
||||
expect(output.join("").trim()).toBe("{}");
|
||||
});
|
||||
|
||||
test("omits non-numeric quota fields from the JSON output", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(
|
||||
wrapResponse({ per5HourPercentage: "not-a-number", per1WeekPercentage: 0 }),
|
||||
"json",
|
||||
);
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({ per1WeekPercentage: 0 });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import { imageSyncPath, speechRecognizePath } from "./endpoints.ts";
|
||||
|
||||
/**
|
||||
* DashScope ASR APIs differ by model family:
|
||||
*
|
||||
* - async file transcription (`.../audio/asr/transcription`):
|
||||
* fun-asr*, paraformer* (non-realtime), *-filetrans, sensevoice*
|
||||
* language via `parameters.language_hints`
|
||||
* - sync multimodal (`.../aigc/multimodal-generation/generation`):
|
||||
* - qwen3: `{ content: [{ audio }] }` + optional `asr_options.language`
|
||||
* (qwen3-asr-flash*)
|
||||
* - input-audio: `{ type: input_audio, input_audio.data }` +
|
||||
* `format`/`sample_rate` + optional `language_hints`
|
||||
* (fun-asr-flash*, qwen-audio-*-asr-flash*)
|
||||
* - realtime / streaming: WebSocket — not supported by `speech recognize`
|
||||
*/
|
||||
|
||||
export type AsrApiKind = "async-filetrans" | "sync-flash" | "unsupported";
|
||||
|
||||
/** Sync-flash request body shape differs by Flash protocol family. */
|
||||
export type AsrFlashFamily = "qwen3" | "input-audio";
|
||||
|
||||
export interface AsrApiRoute {
|
||||
kind: AsrApiKind;
|
||||
path: string;
|
||||
/** True when the call is synchronous (no X-DashScope-Async / task poll). */
|
||||
useSync: boolean;
|
||||
/**
|
||||
* Async transcription request input style.
|
||||
* - `file_urls`: classic async models (fun-asr / paraformer / qwen-audio filetrans...)
|
||||
* - `file_url`: qwen3-asr-flash-filetrans family
|
||||
*/
|
||||
asyncInputStyle?: "file_urls" | "file_url";
|
||||
/**
|
||||
* Async transcription language field style.
|
||||
* - `language_hints`: fun-asr / paraformer / qwen-audio filetrans...
|
||||
* - `language`: qwen3-asr-flash-filetrans*
|
||||
*/
|
||||
asyncLanguageStyle?: "language_hints" | "language";
|
||||
flashFamily?: AsrFlashFamily;
|
||||
/** Human-readable reason when kind is unsupported. */
|
||||
unsupportedReason?: string;
|
||||
}
|
||||
|
||||
function isRealtimeOrStreaming(model: string): boolean {
|
||||
return /realtime|streaming/i.test(model);
|
||||
}
|
||||
|
||||
function isFiletransModel(model: string): boolean {
|
||||
return /filetrans/i.test(model);
|
||||
}
|
||||
|
||||
function isQwen3FiletransModel(model: string): boolean {
|
||||
return /^qwen3-asr-flash-filetrans(?:-|$)/i.test(model);
|
||||
}
|
||||
|
||||
const INPUT_AUDIO_FLASH_PREFIXES = ["fun-asr-flash", "qwen-audio"] as const;
|
||||
|
||||
/**
|
||||
* Fun-ASR-Flash / Qwen-Audio-*-ASR-Flash share the input_audio + format protocol.
|
||||
* Examples: fun-asr-flash-2026-06-15, qwen-audio-3.0-asr-flash
|
||||
*/
|
||||
function isInputAudioFlashModel(model: string): boolean {
|
||||
if (isRealtimeOrStreaming(model) || isFiletransModel(model)) return false;
|
||||
if (model.startsWith(INPUT_AUDIO_FLASH_PREFIXES[0])) return true;
|
||||
if (model.startsWith(INPUT_AUDIO_FLASH_PREFIXES[1]) && /asr-flash/i.test(model)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qwen3-ASR-Flash sync models use content.audio + asr_options.
|
||||
* Examples: qwen3-asr-flash, qwen3-asr-flash-2025-09-08, qwen3-asr-flash-us
|
||||
*/
|
||||
function isQwen3AsrFlashModel(model: string): boolean {
|
||||
if (!/^qwen3-asr-flash(?:-|$)/i.test(model)) return false;
|
||||
if (isFiletransModel(model) || isRealtimeOrStreaming(model)) return false;
|
||||
if (isInputAudioFlashModel(model)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which DashScope ASR API a model should use for file recognition.
|
||||
* Unknown models default to async-filetrans (preserves existing CLI behavior).
|
||||
*/
|
||||
export function resolveAsrApi(model: string): AsrApiRoute {
|
||||
if (isRealtimeOrStreaming(model)) {
|
||||
return {
|
||||
kind: "unsupported",
|
||||
path: "",
|
||||
useSync: false,
|
||||
unsupportedReason:
|
||||
`Model "${model}" is a realtime/streaming ASR model and requires a WebSocket API. ` +
|
||||
`Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) or a sync flash model ` +
|
||||
`(e.g. qwen3-asr-flash, qwen-audio-3.0-asr-flash) with this command.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (isFiletransModel(model)) {
|
||||
const isQwen3Filetrans = isQwen3FiletransModel(model);
|
||||
return {
|
||||
kind: "async-filetrans",
|
||||
path: speechRecognizePath(),
|
||||
useSync: false,
|
||||
asyncInputStyle: isQwen3Filetrans ? "file_url" : "file_urls",
|
||||
asyncLanguageStyle: isQwen3Filetrans ? "language" : "language_hints",
|
||||
};
|
||||
}
|
||||
|
||||
if (isInputAudioFlashModel(model)) {
|
||||
return {
|
||||
kind: "sync-flash",
|
||||
path: imageSyncPath(),
|
||||
useSync: true,
|
||||
flashFamily: "input-audio",
|
||||
};
|
||||
}
|
||||
|
||||
if (isQwen3AsrFlashModel(model)) {
|
||||
return {
|
||||
kind: "sync-flash",
|
||||
path: imageSyncPath(),
|
||||
useSync: true,
|
||||
flashFamily: "qwen3",
|
||||
};
|
||||
}
|
||||
|
||||
// fun-asr / paraformer / sensevoice / unknown → keep legacy async path
|
||||
return {
|
||||
kind: "async-filetrans",
|
||||
path: speechRecognizePath(),
|
||||
useSync: false,
|
||||
asyncInputStyle: "file_urls",
|
||||
asyncLanguageStyle: "language_hints",
|
||||
};
|
||||
}
|
||||
|
||||
/** Infer audio container hint for input-audio Flash `parameters.format`. */
|
||||
export function inferAudioFormatHint(audioUrl: string): string {
|
||||
// data URI: data:audio/mpeg;base64,... → mp3; data:audio/x-wav;... → wav
|
||||
const dataType = /^data:audio\/([^;,]+)/i.exec(audioUrl)?.[1]?.toLowerCase();
|
||||
if (dataType) {
|
||||
if (dataType === "mpeg") return "mp3";
|
||||
if (dataType === "x-wav" || dataType === "wave") return "wav";
|
||||
return dataType;
|
||||
}
|
||||
|
||||
const pathPart = audioUrl.split(/[?#]/, 1)[0] ?? audioUrl;
|
||||
const match = pathPart.match(/\.([a-zA-Z0-9]+)$/);
|
||||
const extension = match?.[1]?.toLowerCase();
|
||||
if (!extension) return "wav";
|
||||
if (extension === "mpeg") return "mp3";
|
||||
return extension;
|
||||
}
|
||||
|
||||
export interface BuildAsrFlashRequestOpts {
|
||||
model: string;
|
||||
audioUrl: string;
|
||||
language?: string;
|
||||
/** Precompiled hotword vocabulary ID; supported for input-audio Flash (fun-asr-flash* / qwen-audio-*-asr-flash). */
|
||||
vocabularyId?: string;
|
||||
flashFamily: AsrFlashFamily;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build language fields for async ASR routes.
|
||||
* qwen3-asr-flash-filetrans* → `language`; other async models → `language_hints`.
|
||||
*/
|
||||
export function buildAsyncAsrLanguageFields(
|
||||
languageStyle: "language_hints" | "language",
|
||||
language?: string,
|
||||
): { language_hints?: string[]; language?: string } {
|
||||
if (!language) return {};
|
||||
if (languageStyle === "language") {
|
||||
return { language };
|
||||
}
|
||||
return { language_hints: [language] };
|
||||
}
|
||||
|
||||
/** Build a sync multimodal ASR request body for Flash models. */
|
||||
export function buildAsrFlashRequest(opts: BuildAsrFlashRequestOpts): Record<string, unknown> {
|
||||
const { model, audioUrl, language, vocabularyId, flashFamily } = opts;
|
||||
|
||||
if (flashFamily === "input-audio") {
|
||||
// Match official Qwen-Audio / Fun-ASR-Flash docs: language_hints + vocabulary_id
|
||||
const parameters: Record<string, unknown> = {
|
||||
format: inferAudioFormatHint(audioUrl),
|
||||
sample_rate: "16000",
|
||||
};
|
||||
if (language) {
|
||||
parameters.language_hints = [language];
|
||||
}
|
||||
if (vocabularyId) {
|
||||
parameters.vocabulary_id = vocabularyId;
|
||||
}
|
||||
return {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "input_audio",
|
||||
input_audio: { data: audioUrl },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
const asrOptions: Record<string, unknown> = {};
|
||||
if (language) {
|
||||
asrOptions.language = language;
|
||||
}
|
||||
|
||||
const parameters: Record<string, unknown> = {};
|
||||
if (Object.keys(asrOptions).length > 0) {
|
||||
parameters.asr_options = asrOptions;
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ audio: audioUrl }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (Object.keys(parameters).length > 0) {
|
||||
body.parameters = parameters;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract recognition text from a sync Flash ASR response.
|
||||
* Qwen3 uses choices[].message.content; input-audio Flash uses output.text /
|
||||
* output.sentence.text / output.output.sentence.text.
|
||||
*/
|
||||
export function extractAsrFlashText(
|
||||
response: Record<string, unknown>,
|
||||
flashFamily: AsrFlashFamily,
|
||||
): string {
|
||||
const output = response.output as Record<string, unknown> | undefined;
|
||||
if (!output) return "";
|
||||
|
||||
if (flashFamily === "input-audio") {
|
||||
if (typeof output.text === "string" && output.text.length > 0) {
|
||||
return output.text;
|
||||
}
|
||||
const topSentence = output.sentence as Record<string, unknown> | undefined;
|
||||
if (typeof topSentence?.text === "string" && topSentence.text.length > 0) {
|
||||
return topSentence.text;
|
||||
}
|
||||
const nested = output.output as Record<string, unknown> | undefined;
|
||||
const nestedSentence = nested?.sentence as Record<string, unknown> | undefined;
|
||||
if (typeof nestedSentence?.text === "string") {
|
||||
return nestedSentence.text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const choices = output.choices as Array<Record<string, unknown>> | undefined;
|
||||
if (!choices?.length) return "";
|
||||
|
||||
const texts: string[] = [];
|
||||
for (const choice of choices) {
|
||||
const message = choice.message as Record<string, unknown> | undefined;
|
||||
if (!message) continue;
|
||||
const content = message.content;
|
||||
if (typeof content === "string") {
|
||||
texts.push(content);
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (const item of content) {
|
||||
if (typeof item === "string") {
|
||||
texts.push(item);
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === "object") {
|
||||
const record = item as Record<string, unknown>;
|
||||
if (typeof record.text === "string") {
|
||||
texts.push(record.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize async ASR task transcription items:
|
||||
* - classic models: `output.results[]`
|
||||
* - qwen3-asr-flash-filetrans*: `output.result.transcription_url`
|
||||
*/
|
||||
export function collectAsrTranscriptionItems(output: {
|
||||
results?: Array<{
|
||||
file_url?: string;
|
||||
transcription_url?: string;
|
||||
subtask_status?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}>;
|
||||
result?: { transcription_url?: string };
|
||||
}): Array<{
|
||||
file_url?: string;
|
||||
transcription_url?: string;
|
||||
subtask_status?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}> {
|
||||
if (output.results && output.results.length > 0) {
|
||||
return output.results;
|
||||
}
|
||||
const transcriptionUrl = output.result?.transcription_url;
|
||||
if (typeof transcriptionUrl === "string" && transcriptionUrl.length > 0) {
|
||||
return [{ transcription_url: transcriptionUrl, subtask_status: "SUCCEEDED" }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -5,7 +5,13 @@ import { ExitCode } from "../errors/codes.ts";
|
||||
import { request, requestJson, type HttpDeps, type RequestOpts } from "./http.ts";
|
||||
import { buildAcsCanonicalQuery, signAcsRequest, type AcsQueryParams } from "./acs.ts";
|
||||
import { imageFileToDataUri, isLocalFile, resolveFileUrl } from "../files/upload.ts";
|
||||
import { McpClient } from "./mcp.ts";
|
||||
import {
|
||||
bailianMcpPath,
|
||||
bailianMcpSsePath,
|
||||
connectBailianMcpWithFallback,
|
||||
McpClient,
|
||||
type McpConnectedClient,
|
||||
} from "./mcp.ts";
|
||||
import { callConsoleGateway } from "../console/gateway.ts";
|
||||
import { refreshAccessToken } from "../auth/refresh-token.ts";
|
||||
import { maskToken } from "../utils/token.ts";
|
||||
@@ -164,6 +170,25 @@ export class Client {
|
||||
return new McpClient(this.http, url, this.deps.apiCred?.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a Bailian MCP: try Streamable HTTP, then SSE on 405 (except WebSearch).
|
||||
* `urlOverride` maps to `--url`: Streamable first, then classic SSE on the same URL (405/404).
|
||||
*/
|
||||
connectBailianMcp(
|
||||
serverCode: string,
|
||||
urlOverride?: string,
|
||||
): Promise<{ client: McpConnectedClient; url: string }> {
|
||||
this.requireApi();
|
||||
return connectBailianMcpWithFallback({
|
||||
deps: this.http,
|
||||
authToken: this.deps.apiCred?.token,
|
||||
httpUrl: this.url(bailianMcpPath(serverCode)),
|
||||
sseUrl: this.url(bailianMcpSsePath(serverCode)),
|
||||
serverCode,
|
||||
urlOverride,
|
||||
});
|
||||
}
|
||||
|
||||
async console<T>(api: string, data: Record<string, unknown>): Promise<T> {
|
||||
if (!this.deps.consoleCred) {
|
||||
throw new BailianError("This command needs a console access token.", ExitCode.AUTH);
|
||||
|
||||
@@ -34,6 +34,18 @@ export {
|
||||
type ImageInputStyle,
|
||||
type ImageSizeProfile,
|
||||
} from "./image-routes.ts";
|
||||
export {
|
||||
buildAsrFlashRequest,
|
||||
buildAsyncAsrLanguageFields,
|
||||
collectAsrTranscriptionItems,
|
||||
extractAsrFlashText,
|
||||
inferAudioFormatHint,
|
||||
resolveAsrApi,
|
||||
type AsrApiKind,
|
||||
type AsrApiRoute,
|
||||
type AsrFlashFamily,
|
||||
type BuildAsrFlashRequestOpts,
|
||||
} from "./asr-routes.ts";
|
||||
export { CHANNEL, sourceConfig, trackingHeaders, type TrackingIdentity } from "./headers.ts";
|
||||
export type { HttpDeps, RequestOpts } from "./http.ts";
|
||||
export { request, requestJson } from "./http.ts";
|
||||
@@ -57,7 +69,19 @@ export {
|
||||
type AcsQueryParams,
|
||||
type AcsSignConfig,
|
||||
} from "./acs.ts";
|
||||
export type { McpTool, McpToolResult } from "./mcp.ts";
|
||||
export { McpClient, bailianMcpPath } from "./mcp.ts";
|
||||
export type {
|
||||
McpTool,
|
||||
McpToolResult,
|
||||
McpConnectedClient,
|
||||
ConnectBailianMcpOptions,
|
||||
} from "./mcp.ts";
|
||||
export {
|
||||
McpClient,
|
||||
bailianMcpPath,
|
||||
bailianMcpSsePath,
|
||||
isStreamableHttpUnsupported,
|
||||
isUrlOverrideSseFallbackCandidate,
|
||||
connectBailianMcpWithFallback,
|
||||
} from "./mcp.ts";
|
||||
export type { ServerSentEvent } from "./stream.ts";
|
||||
export { parseSSE } from "./stream.ts";
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* MCP classic HTTP+SSE client (protocol 2024-11-05 transport).
|
||||
*
|
||||
* Flow: GET /sse → endpoint event → POST JSON-RPC to message URL;
|
||||
* responses arrive as SSE `message` events matched by JSON-RPC id.
|
||||
*/
|
||||
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import type { HttpDeps } from "./http.ts";
|
||||
import { trackingHeaders } from "./headers.ts";
|
||||
import type { McpTool, McpToolResult } from "./mcp.ts";
|
||||
import { parseSSE } from "./stream.ts";
|
||||
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: "2.0";
|
||||
id?: number | string | null;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string; data?: unknown };
|
||||
}
|
||||
|
||||
type PendingResolver = {
|
||||
resolve: (value: JsonRpcResponse) => void;
|
||||
reject: (reason: unknown) => void;
|
||||
};
|
||||
|
||||
/** Match JSON-RPC ids with string keys (number or string echo from server). */
|
||||
function pendingKey(id: number | string): string {
|
||||
return String(id);
|
||||
}
|
||||
|
||||
export class McpSseClient {
|
||||
private sseUrl: string;
|
||||
private messageUrl: string | undefined;
|
||||
private nextId = 1;
|
||||
private deps: HttpDeps;
|
||||
private authToken: string | undefined;
|
||||
private abortController: AbortController | undefined;
|
||||
private pending = new Map<string, PendingResolver>();
|
||||
private endpointReady: Promise<void>;
|
||||
private resolveEndpoint: (() => void) | undefined;
|
||||
private rejectEndpoint: ((reason: unknown) => void) | undefined;
|
||||
private closed = false;
|
||||
/** Set when the SSE GET ends without an intentional close(); later RPCs fail fast. */
|
||||
private streamEnded = false;
|
||||
|
||||
constructor(deps: HttpDeps, sseUrl: string, authToken?: string) {
|
||||
this.deps = deps;
|
||||
this.sseUrl = sseUrl;
|
||||
this.authToken = authToken;
|
||||
this.endpointReady = new Promise<void>((resolve, reject) => {
|
||||
this.resolveEndpoint = resolve;
|
||||
this.rejectEndpoint = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/** Open the SSE session and run initialize / notifications/initialized. */
|
||||
async initialize(): Promise<void> {
|
||||
if (!this.authToken) {
|
||||
throw new BailianError("This command needs a model-domain API key.", ExitCode.AUTH);
|
||||
}
|
||||
|
||||
await this.openSse();
|
||||
|
||||
const result = await this.rpc("initialize", {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: this.deps.identity.clientName,
|
||||
version: this.deps.identity.version,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.deps.settings.verbose) {
|
||||
console.error(`[MCP SSE] Session initialized`);
|
||||
console.error(`[MCP SSE] Server: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
await this.notify("notifications/initialized");
|
||||
}
|
||||
|
||||
async listTools(): Promise<McpTool[]> {
|
||||
const result = (await this.rpc("tools/list")) as { tools: McpTool[] };
|
||||
return result.tools || [];
|
||||
}
|
||||
|
||||
async callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
|
||||
const result = (await this.rpc("tools/call", { name, arguments: args })) as McpToolResult;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Abort the hanging GET /sse so the CLI process can exit. */
|
||||
close(): void {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
this.abortController?.abort();
|
||||
this.failPending(new BailianError("MCP SSE session closed.", ExitCode.GENERAL));
|
||||
this.messageUrl = undefined;
|
||||
}
|
||||
|
||||
private failPending(reason: unknown): void {
|
||||
for (const [, waiter] of this.pending) {
|
||||
waiter.reject(reason);
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
|
||||
private markStreamEnded(reason: BailianError): void {
|
||||
this.streamEnded = true;
|
||||
this.messageUrl = undefined;
|
||||
this.failPending(reason);
|
||||
}
|
||||
|
||||
private async openSse(): Promise<void> {
|
||||
if (this.abortController) return;
|
||||
|
||||
// One abortController for header/error-body wait; clear timer before the long-lived stream.
|
||||
this.abortController = new AbortController();
|
||||
const timeoutMs = this.deps.settings.timeout * 1000;
|
||||
let headerTimedOut = false;
|
||||
const headerTimer = setTimeout(() => {
|
||||
headerTimedOut = true;
|
||||
this.abortController?.abort();
|
||||
}, timeoutMs);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "text/event-stream",
|
||||
"User-Agent": `${this.deps.identity.clientName}/${this.deps.identity.version}`,
|
||||
...trackingHeaders(this.deps.identity),
|
||||
};
|
||||
if (this.authToken) {
|
||||
headers["Authorization"] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
if (this.deps.settings.verbose) {
|
||||
console.error(`> GET ${this.sseUrl}`);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(this.sseUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
clearTimeout(headerTimer);
|
||||
// Allow a later initialize() to openSse again on this instance.
|
||||
this.abortController = undefined;
|
||||
if (this.closed) {
|
||||
throw new BailianError("MCP SSE session closed.", ExitCode.GENERAL);
|
||||
}
|
||||
if (headerTimedOut) {
|
||||
throw new BailianError("MCP SSE timed out waiting for response headers.", ExitCode.TIMEOUT);
|
||||
}
|
||||
// Rethrow fetch failures so runtime can surface errno (e.g. ENOTFOUND) in JSON/text.
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.deps.settings.verbose) {
|
||||
console.error(`< ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// Keep headerTimer until error body is read (or times out).
|
||||
let errMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const errBody = await response.text();
|
||||
if (errBody) errMsg += ` - ${errBody.slice(0, 500)}`;
|
||||
} catch (error) {
|
||||
clearTimeout(headerTimer);
|
||||
this.abortController = undefined;
|
||||
if (this.closed) {
|
||||
throw new BailianError("MCP SSE session closed.", ExitCode.GENERAL);
|
||||
}
|
||||
if (headerTimedOut) {
|
||||
throw new BailianError(
|
||||
"MCP SSE timed out reading error response body.",
|
||||
ExitCode.TIMEOUT,
|
||||
);
|
||||
}
|
||||
throw new BailianError(errMsg, ExitCode.GENERAL, undefined, { cause: error });
|
||||
}
|
||||
clearTimeout(headerTimer);
|
||||
this.abortController = undefined;
|
||||
// Do not rejectEndpoint — openSse never awaits endpointReady on this path.
|
||||
throw new BailianError(errMsg, ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
clearTimeout(headerTimer);
|
||||
|
||||
void this.consumeSse(response).catch((error) => {
|
||||
if (this.closed) return;
|
||||
const reason =
|
||||
error instanceof BailianError
|
||||
? error
|
||||
: new BailianError(
|
||||
`MCP SSE stream failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
this.rejectEndpoint?.(reason);
|
||||
// consumeSse already markStreamEnded on a clean end; cover parse/read failures here.
|
||||
if (!this.streamEnded) {
|
||||
this.markStreamEnded(reason);
|
||||
}
|
||||
});
|
||||
|
||||
const endpointTimeout = cancellableTimeoutReject(
|
||||
timeoutMs,
|
||||
"MCP SSE timed out waiting for endpoint event.",
|
||||
);
|
||||
try {
|
||||
await Promise.race([this.endpointReady, endpointTimeout.promise]);
|
||||
} finally {
|
||||
endpointTimeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private async consumeSse(response: Response): Promise<void> {
|
||||
for await (const event of parseSSE(response)) {
|
||||
if (this.closed) break;
|
||||
|
||||
// Spec requires event: endpoint; ignore unnamed events so JSON is not treated as a URL.
|
||||
if (event.event === "endpoint") {
|
||||
const raw = event.data.trim();
|
||||
if (!raw) continue;
|
||||
// Only accept same-origin message URLs so we never forward the Bearer token cross-origin.
|
||||
this.messageUrl = resolveSameOriginMessageUrl(this.sseUrl, raw);
|
||||
this.resolveEndpoint?.();
|
||||
this.resolveEndpoint = undefined;
|
||||
this.rejectEndpoint = undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Omitted SSE event type defaults to "message".
|
||||
if (event.event === "message" || event.event === undefined) {
|
||||
let payload: JsonRpcResponse;
|
||||
try {
|
||||
payload = JSON.parse(event.data) as JsonRpcResponse;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (typeof payload.id !== "number" && typeof payload.id !== "string") continue;
|
||||
const key = pendingKey(payload.id);
|
||||
const waiter = this.pending.get(key);
|
||||
if (!waiter) continue;
|
||||
this.pending.delete(key);
|
||||
waiter.resolve(payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.closed) return;
|
||||
|
||||
if (!this.messageUrl) {
|
||||
const error = new BailianError(
|
||||
"MCP SSE stream ended before endpoint event.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
this.rejectEndpoint?.(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// After endpoint: mark dead and wake pending; don't throw (avoid unhandledRejection).
|
||||
this.markStreamEnded(new BailianError("MCP SSE stream ended unexpectedly.", ExitCode.GENERAL));
|
||||
}
|
||||
|
||||
private async rpc(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
||||
if (this.closed || this.streamEnded) {
|
||||
throw new BailianError("MCP SSE stream ended unexpectedly.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const id = this.nextId++;
|
||||
const key = pendingKey(id);
|
||||
const body = {
|
||||
jsonrpc: "2.0" as const,
|
||||
id,
|
||||
method,
|
||||
...(params ? { params } : {}),
|
||||
};
|
||||
|
||||
const timeoutMs = this.deps.settings.timeout * 1000;
|
||||
const responsePromise = new Promise<JsonRpcResponse>((resolve, reject) => {
|
||||
this.pending.set(key, { resolve, reject });
|
||||
});
|
||||
// Stream may end and reject pending before Promise.race; attach catch to avoid unhandledRejection.
|
||||
void responsePromise.catch(() => undefined);
|
||||
const responseTimeout = cancellableTimeoutReject(
|
||||
timeoutMs,
|
||||
`MCP SSE timed out waiting for response to ${method}.`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.postMessage(body);
|
||||
if (this.closed || this.streamEnded) {
|
||||
throw new BailianError("MCP SSE stream ended unexpectedly.", ExitCode.GENERAL);
|
||||
}
|
||||
const data = await Promise.race([responsePromise, responseTimeout.promise]);
|
||||
if (data.error) {
|
||||
throw new BailianError(
|
||||
`MCP error (${data.error.code}): ${data.error.message}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
return data.result;
|
||||
} catch (error) {
|
||||
this.pending.delete(key);
|
||||
throw error;
|
||||
} finally {
|
||||
responseTimeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
||||
const body = {
|
||||
jsonrpc: "2.0" as const,
|
||||
method,
|
||||
...(params ? { params } : {}),
|
||||
};
|
||||
await this.postMessage(body);
|
||||
}
|
||||
|
||||
private async postMessage(body: unknown): Promise<void> {
|
||||
if (this.closed || this.streamEnded) {
|
||||
throw new BailianError("MCP SSE stream ended unexpectedly.", ExitCode.GENERAL);
|
||||
}
|
||||
if (!this.messageUrl) {
|
||||
throw new BailianError("MCP SSE message endpoint is not ready.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json, text/event-stream",
|
||||
"User-Agent": `${this.deps.identity.clientName}/${this.deps.identity.version}`,
|
||||
...trackingHeaders(this.deps.identity),
|
||||
};
|
||||
// Bearer is only sent to a messageUrl that already passed the same-origin check.
|
||||
if (this.authToken) {
|
||||
headers["Authorization"] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
if (this.deps.settings.verbose) {
|
||||
console.error(`> POST ${this.messageUrl}`);
|
||||
console.error(`> Method: ${(body as { method?: string }).method}`);
|
||||
}
|
||||
|
||||
const timeoutMs = this.deps.settings.timeout * 1000;
|
||||
// Combine per-RPC timeout with session abort so close() cancels in-flight POSTs.
|
||||
const requestSignal = createLinkedAbortSignal(timeoutMs, this.abortController?.signal);
|
||||
let res: Response;
|
||||
try {
|
||||
try {
|
||||
res = await fetch(this.messageUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: requestSignal.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.closed) {
|
||||
throw new BailianError("MCP SSE session closed.", ExitCode.GENERAL);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.deps.settings.verbose) {
|
||||
console.error(`< ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
// Keep signal until error body is read (same class of bug as GET openSse).
|
||||
let errMsg = `MCP request failed: ${res.status} ${res.statusText}`;
|
||||
try {
|
||||
const errBody = await res.text();
|
||||
if (errBody) errMsg += ` - ${errBody.slice(0, 500)}`;
|
||||
} catch (error) {
|
||||
if (this.closed) {
|
||||
throw new BailianError("MCP SSE session closed.", ExitCode.GENERAL);
|
||||
}
|
||||
if (requestSignal.timedOut) {
|
||||
throw new BailianError(
|
||||
"MCP SSE timed out reading error response body.",
|
||||
ExitCode.TIMEOUT,
|
||||
);
|
||||
}
|
||||
throw new BailianError(errMsg, ExitCode.GENERAL, undefined, { cause: error });
|
||||
}
|
||||
throw new BailianError(errMsg, ExitCode.GENERAL);
|
||||
}
|
||||
} finally {
|
||||
requestSignal.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the SSE endpoint data to an absolute URL and require same origin as sseUrl. */
|
||||
export function resolveSameOriginMessageUrl(sseUrl: string, endpointData: string): string {
|
||||
let resolved: URL;
|
||||
let base: URL;
|
||||
try {
|
||||
base = new URL(sseUrl);
|
||||
resolved = new URL(endpointData, sseUrl);
|
||||
} catch {
|
||||
throw new BailianError(
|
||||
`MCP SSE endpoint is not a valid URL: ${endpointData}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
if (resolved.origin !== base.origin) {
|
||||
throw new BailianError(
|
||||
`MCP SSE endpoint origin mismatch: expected ${base.origin}, got ${resolved.origin}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
return resolved.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancellable timeout rejection: after Promise.race settles, call cancel()
|
||||
* to clear the timer and avoid unhandledRejection.
|
||||
*/
|
||||
function cancellableTimeoutReject(
|
||||
timeoutMs: number,
|
||||
message: string,
|
||||
): { promise: Promise<never>; cancel: () => void } {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const promise = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
reject(new BailianError(message, ExitCode.TIMEOUT));
|
||||
}, timeoutMs);
|
||||
});
|
||||
// Swallow late rejects after cancel to avoid unhandledRejection.
|
||||
void promise.catch(() => undefined);
|
||||
|
||||
return {
|
||||
promise,
|
||||
cancel: () => {
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Timeout + optional parent abort without AbortSignal.any (Node 18). */
|
||||
function createLinkedAbortSignal(
|
||||
timeoutMs: number,
|
||||
parentSignal?: AbortSignal,
|
||||
): { signal: AbortSignal; cleanup: () => void; timedOut: boolean } {
|
||||
const controller = new AbortController();
|
||||
const state = { timedOut: false };
|
||||
const timeout = setTimeout(() => {
|
||||
state.timedOut = true;
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
parentSignal?.removeEventListener("abort", abortFromParent);
|
||||
};
|
||||
|
||||
if (parentSignal?.aborted) abortFromParent();
|
||||
else parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
||||
controller.signal.addEventListener("abort", cleanup, { once: true });
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cleanup,
|
||||
get timedOut() {
|
||||
return state.timedOut;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import type { HttpDeps } from "./http.ts";
|
||||
import { trackingHeaders } from "./headers.ts";
|
||||
import { McpSseClient } from "./mcp-sse.ts";
|
||||
import { parseSSE } from "./stream.ts";
|
||||
|
||||
// ---- JSON-RPC 2.0 Types ----
|
||||
|
||||
@@ -27,7 +29,7 @@ interface JsonRpcRequest {
|
||||
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: "2.0";
|
||||
id: number;
|
||||
id?: number | string | null;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string; data?: unknown };
|
||||
}
|
||||
@@ -61,6 +63,101 @@ export function bailianMcpPath(serverCode: string): string {
|
||||
return `/api/v1/mcps/${serverCode}/mcp`;
|
||||
}
|
||||
|
||||
/** Classic SSE path: `/api/v1/mcps/<serverCode>/sse`. */
|
||||
export function bailianMcpSsePath(serverCode: string): string {
|
||||
return `/api/v1/mcps/${serverCode}/sse`;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when Streamable HTTP is unsupported and classic SSE fallback should be tried.
|
||||
* Anchored to HTTP wrapper text only (not JSON-RPC / nested copies). Bailian 404 excluded.
|
||||
*/
|
||||
export function isStreamableHttpUnsupported(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
return /^MCP request failed:\s*405\b/i.test(error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE fallback for `--url` (official backwards-compat: same URL, HTTP 405/404 then GET SSE).
|
||||
*/
|
||||
export function isUrlOverrideSseFallbackCandidate(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
return /^MCP request failed:\s*(405|404)\b/i.test(error.message);
|
||||
}
|
||||
|
||||
export type McpConnectedClient = {
|
||||
initialize(): Promise<void>;
|
||||
listTools(): Promise<McpTool[]>;
|
||||
callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
|
||||
close?(): void;
|
||||
};
|
||||
|
||||
export type ConnectBailianMcpOptions = {
|
||||
deps: HttpDeps;
|
||||
authToken: string | undefined;
|
||||
/** Full Streamable HTTP URL (/mcp). */
|
||||
httpUrl: string;
|
||||
/** Full classic SSE URL (/sse). */
|
||||
sseUrl: string;
|
||||
serverCode: string;
|
||||
/**
|
||||
* Explicit `--url` override: try Streamable on that URL first;
|
||||
* on 405/404 fall back to classic SSE on the same URL.
|
||||
*/
|
||||
urlOverride?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect via Streamable HTTP first; on 405 (except WebSearch), fall back to SSE.
|
||||
* `--url` uses the same URL for Streamable then classic SSE (official backwards-compat).
|
||||
* For WebSearch, rethrow the original error so commands can attach a re-activate hint.
|
||||
*/
|
||||
export async function connectBailianMcpWithFallback(
|
||||
options: ConnectBailianMcpOptions,
|
||||
): Promise<{ client: McpConnectedClient; url: string }> {
|
||||
const { deps, authToken, httpUrl, sseUrl, serverCode, urlOverride } = options;
|
||||
|
||||
if (urlOverride) {
|
||||
const httpClient = new McpClient(deps, urlOverride, authToken);
|
||||
try {
|
||||
await httpClient.initialize();
|
||||
return { client: httpClient, url: urlOverride };
|
||||
} catch (error) {
|
||||
if (!isUrlOverrideSseFallbackCandidate(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const sseClient = new McpSseClient(deps, urlOverride, authToken);
|
||||
try {
|
||||
await sseClient.initialize();
|
||||
return { client: sseClient, url: urlOverride };
|
||||
} catch (error) {
|
||||
sseClient.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const httpClient = new McpClient(deps, httpUrl, authToken);
|
||||
try {
|
||||
await httpClient.initialize();
|
||||
return { client: httpClient, url: httpUrl };
|
||||
} catch (error) {
|
||||
if (!isStreamableHttpUnsupported(error) || serverCode === "WebSearch") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const sseClient = new McpSseClient(deps, sseUrl, authToken);
|
||||
try {
|
||||
await sseClient.initialize();
|
||||
return { client: sseClient, url: sseUrl };
|
||||
} catch (error) {
|
||||
sseClient.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- MCP Client ----
|
||||
|
||||
export class McpClient {
|
||||
@@ -121,7 +218,7 @@ export class McpClient {
|
||||
};
|
||||
|
||||
const response = await this.send(body);
|
||||
const data = (await response.json()) as JsonRpcResponse;
|
||||
const data = await this.readJsonRpcResponse(response, id);
|
||||
|
||||
if (data.error) {
|
||||
throw new BailianError(
|
||||
@@ -143,6 +240,44 @@ export class McpClient {
|
||||
await this.send(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSON-RPC response by Content-Type: application/json or text/event-stream.
|
||||
*/
|
||||
private async readJsonRpcResponse(
|
||||
response: Response,
|
||||
expectedId: number,
|
||||
): Promise<JsonRpcResponse> {
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (contentType.includes("text/event-stream")) {
|
||||
return await this.readJsonRpcFromSse(response, expectedId);
|
||||
}
|
||||
|
||||
return (await response.json()) as JsonRpcResponse;
|
||||
}
|
||||
|
||||
private async readJsonRpcFromSse(
|
||||
response: Response,
|
||||
expectedId: number,
|
||||
): Promise<JsonRpcResponse> {
|
||||
const expectedKey = String(expectedId);
|
||||
for await (const event of parseSSE(response)) {
|
||||
if (event.event && event.event !== "message") continue;
|
||||
let payload: JsonRpcResponse;
|
||||
try {
|
||||
payload = JSON.parse(event.data) as JsonRpcResponse;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (payload.id == null) continue;
|
||||
if (String(payload.id) !== expectedKey) continue;
|
||||
return payload;
|
||||
}
|
||||
throw new BailianError(
|
||||
"MCP SSE response stream ended without a matching JSON-RPC response.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
private async send(body: unknown): Promise<Response> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -7,6 +7,71 @@ export interface ServerSentEvent {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/** Normalize CRLF/CR to LF; hold a trailing `\r` so a split CRLF is not double-broken. */
|
||||
function takeNormalizedSseLines(buffer: string): { lines: string[]; rest: string } {
|
||||
let text = buffer;
|
||||
let holdTrailingCr = false;
|
||||
if (text.endsWith("\r")) {
|
||||
holdTrailingCr = true;
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
|
||||
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
const parts = text.split("\n");
|
||||
const incomplete = parts.pop() ?? "";
|
||||
return {
|
||||
lines: parts,
|
||||
rest: holdTrailingCr ? `${incomplete}\r` : incomplete,
|
||||
};
|
||||
}
|
||||
|
||||
function applySseLine(
|
||||
line: string,
|
||||
event: Partial<ServerSentEvent>,
|
||||
maxBuffer: number,
|
||||
): { event: Partial<ServerSentEvent>; completed?: ServerSentEvent } {
|
||||
if (line === "") {
|
||||
if (event.data === undefined) {
|
||||
return { event: {} };
|
||||
}
|
||||
return {
|
||||
event: {},
|
||||
completed: { data: event.data, event: event.event, id: event.id },
|
||||
};
|
||||
}
|
||||
|
||||
if (line.startsWith(":")) {
|
||||
return { event };
|
||||
}
|
||||
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) {
|
||||
return { event };
|
||||
}
|
||||
|
||||
const field = line.slice(0, colonIndex);
|
||||
const fieldValue = line.slice(colonIndex + 1).trimStart();
|
||||
const nextEvent: Partial<ServerSentEvent> = { ...event };
|
||||
|
||||
switch (field) {
|
||||
case "data":
|
||||
nextEvent.data =
|
||||
nextEvent.data !== undefined ? `${nextEvent.data}\n${fieldValue}` : fieldValue;
|
||||
if (nextEvent.data.length > maxBuffer) {
|
||||
throw new BailianError("SSE event exceeded the maximum buffer size.", ExitCode.GENERAL);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
nextEvent.event = fieldValue;
|
||||
break;
|
||||
case "id":
|
||||
nextEvent.id = fieldValue;
|
||||
break;
|
||||
}
|
||||
|
||||
return { event: nextEvent };
|
||||
}
|
||||
|
||||
export async function* parseSSE(response: Response): AsyncGenerator<ServerSentEvent> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) return;
|
||||
@@ -14,69 +79,55 @@ export async function* parseSSE(response: Response): AsyncGenerator<ServerSentEv
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
// Guard against a hostile or malfunctioning stream that never emits a newline
|
||||
// (or builds a single absurdly large event): bound the in-memory buffer so the
|
||||
// parser cannot be driven to exhaust process memory.
|
||||
const MAX_SSE_BUFFER = 16 * 1024 * 1024; // 16 MiB
|
||||
|
||||
try {
|
||||
// Keep partial event fields across chunks.
|
||||
let event: Partial<ServerSentEvent> = {};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (done) {
|
||||
// EOF: treat any held `\r` as a line ending.
|
||||
if (buffer.length > 0) {
|
||||
const finalText = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
const parts = finalText.split("\n");
|
||||
buffer = parts.pop() ?? "";
|
||||
for (const line of parts) {
|
||||
const applied = applySseLine(line, event, MAX_SSE_BUFFER);
|
||||
event = applied.event;
|
||||
if (applied.completed) {
|
||||
yield applied.completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
if (buffer.length > MAX_SSE_BUFFER) {
|
||||
throw new BailianError("SSE stream exceeded the maximum buffer size.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
let event: Partial<ServerSentEvent> = {};
|
||||
const { lines, rest } = takeNormalizedSseLines(buffer);
|
||||
buffer = rest;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === "") {
|
||||
if (event.data !== undefined) {
|
||||
yield { data: event.data, event: event.event, id: event.id };
|
||||
}
|
||||
event = {};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith(":")) continue; // comment
|
||||
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const field = line.slice(0, colonIndex);
|
||||
const value = line.slice(colonIndex + 1).trimStart();
|
||||
|
||||
switch (field) {
|
||||
case "data":
|
||||
event.data = event.data !== undefined ? `${event.data}\n${value}` : value;
|
||||
if (event.data.length > MAX_SSE_BUFFER) {
|
||||
throw new BailianError(
|
||||
"SSE event exceeded the maximum buffer size.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
event.event = value;
|
||||
break;
|
||||
case "id":
|
||||
event.id = value;
|
||||
break;
|
||||
const applied = applySseLine(line, event, MAX_SSE_BUFFER);
|
||||
event = applied.event;
|
||||
if (applied.completed) {
|
||||
yield applied.completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
if (buffer.trim() && buffer.includes("data:")) {
|
||||
const colonIndex = buffer.indexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
yield { data: buffer.slice(colonIndex + 1).trimStart() };
|
||||
}
|
||||
// Legacy EOF flush: apply trailing field line and dispatch with event/id intact.
|
||||
if (buffer.length > 0) {
|
||||
const applied = applySseLine(buffer, event, MAX_SSE_BUFFER);
|
||||
event = applied.event;
|
||||
}
|
||||
if (event.data !== undefined) {
|
||||
yield { data: event.data, event: event.event, id: event.id };
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
|
||||
@@ -58,7 +58,7 @@ export function effectiveConsoleGatewayConfig(
|
||||
}
|
||||
|
||||
export interface ConsoleGatewayRequest {
|
||||
/** Console API name, e.g. zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota */
|
||||
/** Console API name, e.g. zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota */
|
||||
api: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -533,33 +533,46 @@ export interface DashScopeTTSStreamChunk {
|
||||
export interface DashScopeASRRequest {
|
||||
model: string;
|
||||
input: {
|
||||
file_urls: string[];
|
||||
file_urls?: string[];
|
||||
file_url?: string;
|
||||
};
|
||||
parameters?: {
|
||||
channel_id?: number[];
|
||||
/** Classic async models (fun-asr / paraformer / qwen-audio filetrans, etc.) */
|
||||
language_hints?: string[];
|
||||
/** qwen3-asr-flash-filetrans* uses singular `language` */
|
||||
language?: string;
|
||||
diarization_enabled?: boolean;
|
||||
speaker_count?: number;
|
||||
vocabulary_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DashScopeASRTranscriptionItem {
|
||||
file_url?: string;
|
||||
transcription_url?: string;
|
||||
subtask_status?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DashScopeASRTaskResult {
|
||||
output: {
|
||||
task_id: string;
|
||||
task_status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "UNKNOWN";
|
||||
results?: Array<{
|
||||
file_url?: string;
|
||||
/** Multi-file async results (fun-asr / paraformer / qwen-audio filetrans, etc.) */
|
||||
results?: DashScopeASRTranscriptionItem[];
|
||||
/** Singular result returned by qwen3-asr-flash-filetrans* on success */
|
||||
result?: {
|
||||
transcription_url?: string;
|
||||
subtask_status?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}>;
|
||||
};
|
||||
task_metrics?: {
|
||||
TOTAL: number;
|
||||
SUCCEEDED: number;
|
||||
FAILED: number;
|
||||
};
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
usage?: Record<string, unknown>;
|
||||
request_id: string;
|
||||
|
||||
@@ -48,6 +48,7 @@ export type {
|
||||
ChatTool,
|
||||
DashScopeASRRequest,
|
||||
DashScopeASRTaskResult,
|
||||
DashScopeASRTranscriptionItem,
|
||||
DashScopeAsyncResponse,
|
||||
DashScopeImageRequest,
|
||||
DashScopeImageSyncResponse,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import {
|
||||
buildAsrFlashRequest,
|
||||
buildAsyncAsrLanguageFields,
|
||||
collectAsrTranscriptionItems,
|
||||
extractAsrFlashText,
|
||||
inferAudioFormatHint,
|
||||
resolveAsrApi,
|
||||
} from "../src/client/asr-routes.ts";
|
||||
|
||||
test("resolveAsrApi routes model families correctly", () => {
|
||||
const cases = [
|
||||
{
|
||||
model: "fun-asr",
|
||||
expected: {
|
||||
kind: "async-filetrans",
|
||||
useSync: false,
|
||||
path: "/api/v1/services/audio/asr/transcription",
|
||||
asyncInputStyle: "file_urls",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "qwen3-asr-flash-filetrans-2025-11-17",
|
||||
expected: {
|
||||
kind: "async-filetrans",
|
||||
useSync: false,
|
||||
path: "/api/v1/services/audio/asr/transcription",
|
||||
asyncInputStyle: "file_url",
|
||||
asyncLanguageStyle: "language",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "qwen-audio-3.0-asr-flash-filetrans",
|
||||
expected: {
|
||||
kind: "async-filetrans",
|
||||
useSync: false,
|
||||
asyncInputStyle: "file_urls",
|
||||
asyncLanguageStyle: "language_hints",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "qwen3-asr-flash-us",
|
||||
expected: {
|
||||
kind: "sync-flash",
|
||||
useSync: true,
|
||||
flashFamily: "qwen3",
|
||||
path: "/api/v1/services/aigc/multimodal-generation/generation",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "qwen-audio-3.0-asr-flash",
|
||||
expected: {
|
||||
kind: "sync-flash",
|
||||
useSync: true,
|
||||
flashFamily: "input-audio",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "qwen3-asr-flash-realtime",
|
||||
expected: {
|
||||
kind: "unsupported",
|
||||
},
|
||||
},
|
||||
{
|
||||
model: "foo-asr-flash",
|
||||
expected: {
|
||||
kind: "async-filetrans",
|
||||
useSync: false,
|
||||
path: "/api/v1/services/audio/asr/transcription",
|
||||
asyncInputStyle: "file_urls",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const { model, expected } of cases) {
|
||||
const route = resolveAsrApi(model);
|
||||
expect(route, model).toMatchObject(expected);
|
||||
if (expected.kind === "unsupported") {
|
||||
expect(route.unsupportedReason, model).toMatch(/realtime|streaming|WebSocket/i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("unknown models default to async-filetrans for backward compatibility", () => {
|
||||
expect(resolveAsrApi("custom-asr-model")).toMatchObject({
|
||||
kind: "async-filetrans",
|
||||
useSync: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("inferAudioFormatHint reads extension from url", () => {
|
||||
expect(inferAudioFormatHint("https://example.com/a.mp3")).toBe("mp3");
|
||||
expect(inferAudioFormatHint("oss://bucket/path/file.WAV")).toBe("wav");
|
||||
expect(inferAudioFormatHint("https://example.com/a.mpeg?x=1")).toBe("mp3");
|
||||
expect(inferAudioFormatHint("https://example.com/noext")).toBe("wav");
|
||||
expect(inferAudioFormatHint("data:audio/mpeg;base64,AAA")).toBe("mp3");
|
||||
expect(inferAudioFormatHint("data:audio/x-wav;base64,AAA")).toBe("wav");
|
||||
expect(inferAudioFormatHint("data:audio/ogg;codecs=opus;base64,AAA")).toBe("ogg");
|
||||
});
|
||||
|
||||
test("buildAsrFlashRequest shapes qwen3 and input-audio bodies", () => {
|
||||
expect(
|
||||
buildAsrFlashRequest({
|
||||
model: "qwen3-asr-flash",
|
||||
audioUrl: "https://example.com/a.mp3",
|
||||
language: "en",
|
||||
flashFamily: "qwen3",
|
||||
}),
|
||||
).toEqual({
|
||||
model: "qwen3-asr-flash",
|
||||
input: {
|
||||
messages: [{ role: "user", content: [{ audio: "https://example.com/a.mp3" }] }],
|
||||
},
|
||||
parameters: { asr_options: { language: "en" } },
|
||||
});
|
||||
|
||||
expect(
|
||||
buildAsrFlashRequest({
|
||||
model: "qwen-audio-3.0-asr-flash",
|
||||
audioUrl: "https://example.com/a.wav",
|
||||
language: "en",
|
||||
vocabularyId: "vocab-abc",
|
||||
flashFamily: "input-audio",
|
||||
}),
|
||||
).toEqual({
|
||||
model: "qwen-audio-3.0-asr-flash",
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_audio", input_audio: { data: "https://example.com/a.wav" } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
format: "wav",
|
||||
sample_rate: "16000",
|
||||
language_hints: ["en"],
|
||||
vocabulary_id: "vocab-abc",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("buildAsyncAsrLanguageFields maps language by async style", () => {
|
||||
expect(buildAsyncAsrLanguageFields("language_hints", "zh")).toEqual({
|
||||
language_hints: ["zh"],
|
||||
});
|
||||
expect(buildAsyncAsrLanguageFields("language", "zh")).toEqual({ language: "zh" });
|
||||
expect(buildAsyncAsrLanguageFields("language", undefined)).toEqual({});
|
||||
});
|
||||
|
||||
test("extractAsrFlashText reads qwen3 choices and input-audio text fields", () => {
|
||||
expect(
|
||||
extractAsrFlashText(
|
||||
{
|
||||
output: {
|
||||
choices: [{ message: { content: [{ text: "你好" }] } }],
|
||||
},
|
||||
},
|
||||
"qwen3",
|
||||
),
|
||||
).toBe("你好");
|
||||
|
||||
expect(
|
||||
extractAsrFlashText(
|
||||
{
|
||||
output: {
|
||||
text: "Hello World",
|
||||
output: { sentence: { text: "ignored when text present" } },
|
||||
},
|
||||
},
|
||||
"input-audio",
|
||||
),
|
||||
).toBe("Hello World");
|
||||
|
||||
expect(
|
||||
extractAsrFlashText(
|
||||
{
|
||||
output: {
|
||||
sentence: { text: "top-level sentence" },
|
||||
},
|
||||
},
|
||||
"input-audio",
|
||||
),
|
||||
).toBe("top-level sentence");
|
||||
|
||||
expect(
|
||||
extractAsrFlashText(
|
||||
{
|
||||
output: {
|
||||
output: { sentence: { text: "nested sentence" } },
|
||||
},
|
||||
},
|
||||
"input-audio",
|
||||
),
|
||||
).toBe("nested sentence");
|
||||
});
|
||||
|
||||
test("collectAsrTranscriptionItems prefers results[] then singular result", () => {
|
||||
expect(
|
||||
collectAsrTranscriptionItems({
|
||||
results: [{ transcription_url: "https://example.com/a.json", file_url: "https://a.wav" }],
|
||||
}),
|
||||
).toEqual([{ transcription_url: "https://example.com/a.json", file_url: "https://a.wav" }]);
|
||||
|
||||
expect(
|
||||
collectAsrTranscriptionItems({
|
||||
result: { transcription_url: "https://example.com/qwen3.json" },
|
||||
}),
|
||||
).toEqual([{ transcription_url: "https://example.com/qwen3.json", subtask_status: "SUCCEEDED" }]);
|
||||
|
||||
expect(collectAsrTranscriptionItems({})).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,736 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import type { Identity, Settings } from "../src/index.ts";
|
||||
import {
|
||||
BailianError,
|
||||
bailianMcpPath,
|
||||
bailianMcpSsePath,
|
||||
connectBailianMcpWithFallback,
|
||||
isStreamableHttpUnsupported,
|
||||
isUrlOverrideSseFallbackCandidate,
|
||||
McpClient,
|
||||
} from "../src/index.ts";
|
||||
import { McpSseClient, resolveSameOriginMessageUrl } from "../src/client/mcp-sse.ts";
|
||||
|
||||
function testDeps(overrides?: Partial<Settings>): { identity: Identity; settings: Settings } {
|
||||
return {
|
||||
identity: {
|
||||
binName: "bl",
|
||||
version: "0.0.0-test",
|
||||
npmPackage: "bailian-cli",
|
||||
clientName: "bailian-cli",
|
||||
},
|
||||
settings: {
|
||||
output: "json",
|
||||
outputExplicit: true,
|
||||
timeout: 5,
|
||||
verbose: false,
|
||||
quiet: true,
|
||||
dryRun: false,
|
||||
telemetry: true,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function jsonRpcResult(id: number | string, result: unknown): string {
|
||||
return `event:message\ndata:${JSON.stringify({ jsonrpc: "2.0", id, result })}\n\n`;
|
||||
}
|
||||
|
||||
function requestUrl(input: string | URL | Request): string {
|
||||
if (typeof input === "string") return input;
|
||||
if (input instanceof URL) return input.href;
|
||||
return input.url;
|
||||
}
|
||||
|
||||
test("bailianMcp 路径与 isStreamableHttpUnsupported", () => {
|
||||
expect(bailianMcpPath("WebParser")).toBe("/api/v1/mcps/WebParser/mcp");
|
||||
expect(bailianMcpSsePath("WebParser")).toBe("/api/v1/mcps/WebParser/sse");
|
||||
|
||||
expect(
|
||||
isStreamableHttpUnsupported(
|
||||
new BailianError(
|
||||
"MCP request failed: 405 Method Not Allowed - current mcp not support streamableHttp",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isStreamableHttpUnsupported(new BailianError("MCP request failed: 405 Method Not Allowed")),
|
||||
).toBe(true);
|
||||
expect(isStreamableHttpUnsupported(new BailianError("MCP request failed: 404 Not Found"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isStreamableHttpUnsupported(new Error("405 streamableHttp"))).toBe(false);
|
||||
// JSON-RPC business 405 must not trigger HTTP transport fallback
|
||||
expect(isStreamableHttpUnsupported(new BailianError("MCP error (405): Method Not Allowed"))).toBe(
|
||||
false,
|
||||
);
|
||||
// Nested wrapper phrase in a JSON-RPC message must not trigger fallback.
|
||||
expect(
|
||||
isStreamableHttpUnsupported(
|
||||
new BailianError("MCP error (-32000): MCP request failed: 405 Method Not Allowed"),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isUrlOverrideSseFallbackCandidate(new BailianError("MCP request failed: 404 Not Found")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isUrlOverrideSseFallbackCandidate(
|
||||
new BailianError("MCP request failed: 405 Method Not Allowed"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isUrlOverrideSseFallbackCandidate(new BailianError("MCP error (404): not found"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isUrlOverrideSseFallbackCandidate(
|
||||
new BailianError("MCP error (-32000): MCP request failed: 404 Not Found"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("resolveSameOriginMessageUrl:同源通过、跨域拒绝", () => {
|
||||
expect(
|
||||
resolveSameOriginMessageUrl(
|
||||
"https://example.test/api/v1/mcps/WebParser/sse",
|
||||
"/api/v1/mcps/WebParser/message?sessionId=x",
|
||||
),
|
||||
).toBe("https://example.test/api/v1/mcps/WebParser/message?sessionId=x");
|
||||
|
||||
expect(() =>
|
||||
resolveSameOriginMessageUrl(
|
||||
"https://example.test/api/v1/mcps/WebParser/sse",
|
||||
"https://evil.example/steal",
|
||||
),
|
||||
).toThrow(/origin mismatch/i);
|
||||
});
|
||||
|
||||
test("connectBailianMcpWithFallback:成功走 Streamable;405 降级 SSE", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
// Streamable success path
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
if (requestUrl(input).includes("/sse")) {
|
||||
return new Response("should not hit sse", { status: 500 });
|
||||
}
|
||||
if (body.method === "notifications/initialized") {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result: {} }), {
|
||||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const connected = await connectBailianMcpWithFallback({
|
||||
deps: testDeps(),
|
||||
authToken: "sk-test",
|
||||
httpUrl: "https://example.test/api/v1/mcps/WebParser/mcp",
|
||||
sseUrl: "https://example.test/api/v1/mcps/WebParser/sse",
|
||||
serverCode: "WebParser",
|
||||
});
|
||||
expect(connected.url).toContain("/mcp");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
// Bare HTTP 405 (no streamableHttp body text) → SSE
|
||||
let sseController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
const encoder = new TextEncoder();
|
||||
const urls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
urls.push(`${init?.method ?? "GET"} ${url}`);
|
||||
|
||||
if (url.endsWith("/mcp")) {
|
||||
return new Response("Method Not Allowed", {
|
||||
status: 405,
|
||||
statusText: "Method Not Allowed",
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith("/sse") && (init?.method ?? "GET") === "GET") {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
sseController = controller;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
"event:endpoint\ndata:/api/v1/mcps/WebParser/message?sessionId=test-session\n\n",
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/message")) {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
queueMicrotask(() => {
|
||||
if (body.id != null && sseController) {
|
||||
sseController.enqueue(encoder.encode(jsonRpcResult(body.id, {})));
|
||||
}
|
||||
});
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
return new Response("unexpected", { status: 500 });
|
||||
};
|
||||
|
||||
try {
|
||||
const connected = await connectBailianMcpWithFallback({
|
||||
deps: testDeps(),
|
||||
authToken: "sk-test",
|
||||
httpUrl: "https://example.test/api/v1/mcps/WebParser/mcp",
|
||||
sseUrl: "https://example.test/api/v1/mcps/WebParser/sse",
|
||||
serverCode: "WebParser",
|
||||
});
|
||||
expect(connected.url).toContain("/sse");
|
||||
expect(urls.some((entry) => entry.includes("GET ") && entry.includes("/sse"))).toBe(true);
|
||||
connected.client.close?.();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("connectBailianMcpWithFallback:WebSearch 不降级;urlOverride 同 URL 降级 SSE;404 不降级 Bailian 路径", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const urls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (input) => {
|
||||
urls.push(requestUrl(input));
|
||||
return new Response("current mcp not support streamableHttp", {
|
||||
status: 405,
|
||||
statusText: "Method Not Allowed",
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await expect(
|
||||
connectBailianMcpWithFallback({
|
||||
deps: testDeps(),
|
||||
authToken: "sk-test",
|
||||
httpUrl: "https://example.test/api/v1/mcps/WebSearch/mcp",
|
||||
sseUrl: "https://example.test/api/v1/mcps/WebSearch/sse",
|
||||
serverCode: "WebSearch",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BailianError);
|
||||
expect(urls.some((url) => url.includes("/sse"))).toBe(false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
// urlOverride: after POST 405, fall back with GET SSE on the same URL
|
||||
urls.length = 0;
|
||||
let sseController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
const encoder = new TextEncoder();
|
||||
const overrideUrl = "https://custom.example/mcp";
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
const method = init?.method ?? "GET";
|
||||
urls.push(`${method} ${url}`);
|
||||
|
||||
if (method === "POST" && url === overrideUrl) {
|
||||
return new Response("Method Not Allowed", {
|
||||
status: 405,
|
||||
statusText: "Method Not Allowed",
|
||||
});
|
||||
}
|
||||
|
||||
if (method === "GET" && url === overrideUrl) {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
sseController = controller;
|
||||
controller.enqueue(encoder.encode("event:endpoint\ndata:/message?sessionId=x\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/message")) {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
queueMicrotask(() => {
|
||||
if (body.id != null && sseController) {
|
||||
sseController.enqueue(encoder.encode(jsonRpcResult(body.id, {})));
|
||||
}
|
||||
});
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
return new Response("unexpected", { status: 500 });
|
||||
};
|
||||
|
||||
try {
|
||||
const connected = await connectBailianMcpWithFallback({
|
||||
deps: testDeps(),
|
||||
authToken: "sk-test",
|
||||
httpUrl: "https://example.test/api/v1/mcps/WebParser/mcp",
|
||||
sseUrl: "https://example.test/api/v1/mcps/WebParser/sse",
|
||||
serverCode: "WebParser",
|
||||
urlOverride: overrideUrl,
|
||||
});
|
||||
expect(connected.url).toBe(overrideUrl);
|
||||
expect(urls.some((entry) => entry.startsWith(`GET ${overrideUrl}`))).toBe(true);
|
||||
connected.client.close?.();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response("MCP不存在或未开通", { status: 404, statusText: "Not Found" });
|
||||
|
||||
try {
|
||||
await expect(
|
||||
connectBailianMcpWithFallback({
|
||||
deps: testDeps(),
|
||||
authToken: "sk-test",
|
||||
httpUrl: "https://example.test/api/v1/mcps/WebParser/mcp",
|
||||
sseUrl: "https://example.test/api/v1/mcps/WebParser/sse",
|
||||
serverCode: "WebParser",
|
||||
}),
|
||||
).rejects.toMatchObject({ message: expect.stringContaining("404") });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:流结束后立刻失败 pending(不干等到 timeout)", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
if ((init?.method ?? "GET") === "GET" || url.endsWith("/sse")) {
|
||||
// Close the stream immediately after the endpoint event
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode("event:endpoint\ndata:/api/v1/mcps/WebParser/message?sessionId=x\n\n"),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response(null, { status: 200 });
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(
|
||||
testDeps({ timeout: 5 }),
|
||||
"https://example.test/sse",
|
||||
"sk-test",
|
||||
);
|
||||
const started = Date.now();
|
||||
await expect(client.initialize()).rejects.toThrow(/stream ended unexpectedly/i);
|
||||
expect(Date.now() - started).toBeLessThan(2000);
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:string JSON-RPC id 可匹配;仅认 event:endpoint", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let sseController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
if ((init?.method ?? "GET") === "GET" || url.endsWith("/sse")) {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
sseController = controller;
|
||||
// Untyped events must not be treated as endpoint
|
||||
controller.enqueue(
|
||||
encoder.encode(`data:${JSON.stringify({ jsonrpc: "2.0", id: 99, result: {} })}\n\n`),
|
||||
);
|
||||
controller.enqueue(
|
||||
encoder.encode("event:endpoint\ndata:/api/v1/mcps/WebParser/message?sessionId=x\n\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/message")) {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
queueMicrotask(() => {
|
||||
if (body.id != null && sseController) {
|
||||
// Echo id as a string
|
||||
sseController.enqueue(encoder.encode(jsonRpcResult(String(body.id), {})));
|
||||
}
|
||||
});
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
return new Response("unexpected", { status: 500 });
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(testDeps(), "https://example.test/sse", "sk-test");
|
||||
await client.initialize();
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpClient:支持 text/event-stream 响应体", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (_input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
if (body.method === "notifications/initialized") {
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
const sse = `event: message\ndata: ${JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: body.id,
|
||||
result: {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
serverInfo: { name: "x", version: "0" },
|
||||
},
|
||||
})}\n\n`;
|
||||
return new Response(sse, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpClient(testDeps(), "https://example.test/mcp", "sk-test");
|
||||
await client.initialize();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient.close 可中止挂起 GET", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let aborted = false;
|
||||
|
||||
globalThis.fetch = async (_input, init) => {
|
||||
const signal = init?.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
aborted = true;
|
||||
});
|
||||
}
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
"event:endpoint\ndata:/api/v1/mcps/WebParser/message?sessionId=x\n\n",
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(testDeps(), "https://example.test/sse", "sk-test");
|
||||
const initPromise = client.initialize().catch(() => undefined);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
client.close();
|
||||
await initPromise;
|
||||
expect(aborted).toBe(true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:等待响应头受 --timeout 约束", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (_input, init) => {
|
||||
const signal = init?.signal;
|
||||
return new Promise((_resolve, reject) => {
|
||||
if (!signal) {
|
||||
reject(new Error("missing signal"));
|
||||
return;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException("This operation was aborted.", "AbortError"));
|
||||
return;
|
||||
}
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => reject(new DOMException("This operation was aborted.", "AbortError")),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(
|
||||
testDeps({ timeout: 1 }),
|
||||
"https://example.test/sse",
|
||||
"sk-test",
|
||||
);
|
||||
const started = Date.now();
|
||||
await expect(client.initialize()).rejects.toThrow(/timed out waiting for response headers/i);
|
||||
expect(Date.now() - started).toBeLessThan(2500);
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:非 2xx 不产生 unhandledRejection", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const unhandled: unknown[] = [];
|
||||
const onUnhandled = (reason: unknown) => {
|
||||
unhandled.push(reason);
|
||||
};
|
||||
process.on("unhandledRejection", onUnhandled);
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response("boom", { status: 500, statusText: "Internal Server Error" });
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(testDeps(), "https://example.test/sse", "sk-test");
|
||||
await expect(client.initialize()).rejects.toThrow(/MCP request failed:\s*500/i);
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
expect(unhandled).toEqual([]);
|
||||
client.close();
|
||||
} finally {
|
||||
process.off("unhandledRejection", onUnhandled);
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:非 2xx 读 body 仍受 --timeout 约束", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (_input, init) => {
|
||||
const signal = init?.signal;
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
async text() {
|
||||
return new Promise<string>((_resolve, reject) => {
|
||||
if (!signal) {
|
||||
reject(new Error("missing signal"));
|
||||
return;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException("This operation was aborted.", "AbortError"));
|
||||
return;
|
||||
}
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => reject(new DOMException("This operation was aborted.", "AbortError")),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
} as Response;
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(
|
||||
testDeps({ timeout: 1 }),
|
||||
"https://example.test/sse",
|
||||
"sk-test",
|
||||
);
|
||||
const started = Date.now();
|
||||
await expect(client.initialize()).rejects.toThrow(/timed out reading error response body/i);
|
||||
expect(Date.now() - started).toBeLessThan(2500);
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:fetch 失败抛出原始 TypeError(保留 ENOTFOUND)", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.test"), {
|
||||
code: "ENOTFOUND",
|
||||
});
|
||||
const fetchFailed = new TypeError("fetch failed", { cause: root });
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw fetchFailed;
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(testDeps(), "https://example.test/sse", "sk-test");
|
||||
const error = await client.initialize().catch((reason: unknown) => reason);
|
||||
expect(error).toBe(fetchFailed);
|
||||
expect((error as TypeError & { cause?: NodeJS.ErrnoException }).cause?.code).toBe("ENOTFOUND");
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:fetch 失败后同实例可重新 openSse", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let attempt = 0;
|
||||
let sseController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
const method = init?.method ?? "GET";
|
||||
if (method === "GET" || url.endsWith("/sse")) {
|
||||
attempt += 1;
|
||||
if (attempt === 1) {
|
||||
throw new TypeError("fetch failed");
|
||||
}
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
sseController = controller;
|
||||
controller.enqueue(encoder.encode("event: endpoint\ndata: /message\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : {};
|
||||
queueMicrotask(() => {
|
||||
if (body.id != null && sseController) {
|
||||
sseController.enqueue(encoder.encode(jsonRpcResult(body.id, {})));
|
||||
}
|
||||
});
|
||||
return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(testDeps(), "https://example.test/sse", "sk-test");
|
||||
await expect(client.initialize()).rejects.toThrow(/fetch failed/i);
|
||||
await client.initialize();
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:close 可中止进行中的 POST", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let postAborted = false;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
const method = init?.method ?? "GET";
|
||||
if (method === "GET" || url.endsWith("/sse")) {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode("event: endpoint\ndata: /message\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
const signal = init?.signal;
|
||||
return new Promise((_resolve, reject) => {
|
||||
if (!signal) {
|
||||
reject(new Error("missing signal"));
|
||||
return;
|
||||
}
|
||||
const onAbort = () => {
|
||||
postAborted = true;
|
||||
reject(new DOMException("This operation was aborted.", "AbortError"));
|
||||
};
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(
|
||||
testDeps({ timeout: 5 }),
|
||||
"https://example.test/sse",
|
||||
"sk-test",
|
||||
);
|
||||
const initPromise = client.initialize();
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
client.close();
|
||||
await expect(initPromise).rejects.toThrow(/session closed|aborted/i);
|
||||
expect(postAborted).toBe(true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("McpSseClient:POST 非 2xx 读 body 仍受 --timeout 约束", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = requestUrl(input);
|
||||
const method = init?.method ?? "GET";
|
||||
if (method === "GET" || url.endsWith("/sse")) {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode("event: endpoint\ndata: /message\n\n"));
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
const signal = init?.signal;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
if (!signal) return;
|
||||
const onAbort = () => {
|
||||
try {
|
||||
controller.error(new DOMException("This operation was aborted.", "AbortError"));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
if (signal.aborted) onAbort();
|
||||
else signal.addEventListener("abort", onAbort, { once: true });
|
||||
},
|
||||
});
|
||||
return new Response(body, { status: 500, statusText: "Internal Server Error" });
|
||||
};
|
||||
|
||||
try {
|
||||
const client = new McpSseClient(
|
||||
testDeps({ timeout: 1 }),
|
||||
"https://example.test/sse",
|
||||
"sk-test",
|
||||
);
|
||||
const started = Date.now();
|
||||
await expect(client.initialize()).rejects.toThrow(/timed out reading error response body/i);
|
||||
expect(Date.now() - started).toBeLessThan(2500);
|
||||
client.close();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { parseSSE } from "../src/client/stream.ts";
|
||||
|
||||
async function collectEvents(
|
||||
chunks: string[],
|
||||
): Promise<Array<{ data: string; event?: string; id?: string }>> {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const response = new Response(stream, {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
const events: Array<{ data: string; event?: string; id?: string }> = [];
|
||||
for await (const event of parseSSE(response)) {
|
||||
events.push(event);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
test("parseSSE:单 chunk 完整事件保持原行为", async () => {
|
||||
const events = await collectEvents([
|
||||
'event: message\ndata: {"ok":true}\nid: 1\n\ndata: plain\n\n',
|
||||
]);
|
||||
expect(events).toEqual([{ data: '{"ok":true}', event: "message", id: "1" }, { data: "plain" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:多行 data 与注释保持原行为", async () => {
|
||||
const events = await collectEvents([": keep-alive\ndata: line1\ndata: line2\n\n"]);
|
||||
expect(events).toEqual([{ data: "line1\nline2" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:跨 chunk 保留 event 类型", async () => {
|
||||
const events = await collectEvents(["event: endpoint\n", "data: /message?sessionId=abc\n\n"]);
|
||||
expect(events).toEqual([{ data: "/message?sessionId=abc", event: "endpoint" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:跨 chunk 保留 id,且多事件连续正确", async () => {
|
||||
const events = await collectEvents([
|
||||
"id: a\nevent: message\n",
|
||||
'data: {"n":1}\n\n',
|
||||
"event: message\ndata: ",
|
||||
'{"n":2}\n\n',
|
||||
]);
|
||||
expect(events).toEqual([
|
||||
{ data: '{"n":1}', event: "message", id: "a" },
|
||||
{ data: '{"n":2}', event: "message" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("parseSSE:CRLF 行尾可解析 endpoint", async () => {
|
||||
const events = await collectEvents(["event: endpoint\r\ndata: /message\r\n\r\n"]);
|
||||
expect(events).toEqual([{ data: "/message", event: "endpoint" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:纯 CR 行尾可解析 endpoint", async () => {
|
||||
const events = await collectEvents(["event: endpoint\rdata: /message\r\r"]);
|
||||
expect(events).toEqual([{ data: "/message", event: "endpoint" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:跨 chunk 的 CRLF(\\r|\\n)不丢事件", async () => {
|
||||
const events = await collectEvents(["event: endpoint\r", "\ndata: /message\r\n\r\n"]);
|
||||
expect(events).toEqual([{ data: "/message", event: "endpoint" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:EOF without blank line keeps event type", async () => {
|
||||
const events = await collectEvents(["event: endpoint\ndata: /message"]);
|
||||
expect(events).toEqual([{ data: "/message", event: "endpoint" }]);
|
||||
});
|
||||
|
||||
test("parseSSE:EOF data-only flush keeps prior behavior", async () => {
|
||||
const events = await collectEvents(["data: plain"]);
|
||||
expect(events).toEqual([{ data: "plain" }]);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -78,11 +78,12 @@ function fromFetchFailed(err: TypeError): BailianError {
|
||||
if (causeMsg && causeMsg !== code) detailParts.push(causeMsg);
|
||||
const detail = detailParts.length > 0 ? detailParts.join(": ") : "unknown cause";
|
||||
|
||||
// Prefer the errno (ENOTFOUND, …) so JSON toJSON() exposes cause.code for agents.
|
||||
return new BailianError(
|
||||
`Network request failed: ${detail}`,
|
||||
ExitCode.NETWORK,
|
||||
pickNetworkHint(code),
|
||||
{ cause: err },
|
||||
{ cause: cause ?? err },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,13 @@ export {
|
||||
// Output facilities consumed by commands
|
||||
export { emitResult, emitBare, emitRequestId } from "./output/output.ts";
|
||||
export { formatTable } from "./output/table.ts";
|
||||
export { renderBoxTable, type BoxTableOptions, type BarColumn } from "./output/box-table.ts";
|
||||
export {
|
||||
renderBoxTable,
|
||||
renderGauge,
|
||||
type BoxTableOptions,
|
||||
type BarColumn,
|
||||
type GaugeCell,
|
||||
} from "./output/box-table.ts";
|
||||
export { createSpinner, createProgressBar } from "./output/progress.ts";
|
||||
export { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
|
||||
export { maybeShowStatusBar } from "./output/status-bar.ts";
|
||||
|
||||
@@ -136,6 +136,26 @@ interface RenderedCell {
|
||||
colored: string;
|
||||
}
|
||||
|
||||
/** A standalone gauge cell (bar + label) for non-table layouts, e.g. the usage quota box. */
|
||||
export interface GaugeCell {
|
||||
plain: string;
|
||||
colored: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single gauge cell in the `usage free` table style: brand-cyan fill,
|
||||
* transparent track, and a light-blue label after the bar. `percent` is 0-100;
|
||||
* null renders an empty gauge with a neutral label.
|
||||
*/
|
||||
export function renderGauge(
|
||||
percent: number | null,
|
||||
label: string,
|
||||
width: number = DEFAULT_BAR_WIDTH,
|
||||
out: NodeJS.WriteStream = process.stdout,
|
||||
): GaugeCell {
|
||||
return buildBarCell(percent, label, width, colorLevel(out));
|
||||
}
|
||||
|
||||
function buildBarCell(
|
||||
percent: number | null,
|
||||
label: string,
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
taskPath,
|
||||
speechSynthesizePath,
|
||||
speechRecognizePath,
|
||||
resolveAsrApi,
|
||||
buildAsrFlashRequest,
|
||||
buildAsyncAsrLanguageFields,
|
||||
collectAsrTranscriptionItems,
|
||||
extractAsrFlashText,
|
||||
stripUndefined,
|
||||
resolveBooleanFlag,
|
||||
resolveWatermark,
|
||||
@@ -23,6 +28,7 @@ import {
|
||||
type DashScopeTTSRequest,
|
||||
type DashScopeTTSResponse,
|
||||
type DashScopeASRRequest,
|
||||
type DashScopeASRTaskResult,
|
||||
type ChatMessageContent,
|
||||
isLocalFile,
|
||||
} from "bailian-cli-core";
|
||||
@@ -573,27 +579,103 @@ export async function speechRecognize(
|
||||
});
|
||||
}
|
||||
|
||||
const model = input.model || "fun-asr";
|
||||
const route = resolveAsrApi(model);
|
||||
if (route.kind === "unsupported") {
|
||||
throw new PipelineError(
|
||||
"invalid_input",
|
||||
route.unsupportedReason ?? `Unsupported ASR model: ${model}`,
|
||||
{
|
||||
step: "speech/recognize",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (route.kind === "sync-flash") {
|
||||
if (rawUrls.length !== 1) {
|
||||
throw new PipelineError(
|
||||
"invalid_input",
|
||||
`Model "${model}" is a sync Flash ASR model and accepts exactly one url (got ${rawUrls.length})`,
|
||||
{ step: "speech/recognize" },
|
||||
);
|
||||
}
|
||||
const unsupportedFlags: string[] = [];
|
||||
if (input.diarization) unsupportedFlags.push("diarization");
|
||||
if (input["speaker-count"] !== undefined) unsupportedFlags.push("speaker-count");
|
||||
// input-audio Flash supports vocabulary_id; qwen3 sync Flash does not
|
||||
if (route.flashFamily === "qwen3" && input["vocabulary-id"] !== undefined) {
|
||||
unsupportedFlags.push("vocabulary-id");
|
||||
}
|
||||
if (input["channel-id"] !== undefined) unsupportedFlags.push("channel-id");
|
||||
if (unsupportedFlags.length > 0) {
|
||||
throw new PipelineError(
|
||||
"invalid_input",
|
||||
`Model "${model}" uses sync Flash ASR and does not support: ${unsupportedFlags.join(", ")}`,
|
||||
{ step: "speech/recognize" },
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
route.kind === "async-filetrans" &&
|
||||
route.asyncInputStyle === "file_url" &&
|
||||
rawUrls.length !== 1
|
||||
) {
|
||||
throw new PipelineError(
|
||||
"invalid_input",
|
||||
`Model "${model}" accepts exactly one url (got ${rawUrls.length})`,
|
||||
{ step: "speech/recognize" },
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve local files to upload URLs
|
||||
const fileUrls: string[] = [];
|
||||
for (const u of rawUrls) {
|
||||
if (isLocalFile(u)) {
|
||||
for (const audioUrl of rawUrls) {
|
||||
if (isLocalFile(audioUrl)) {
|
||||
fileUrls.push(
|
||||
await env.client.uploadFile(u, input.model || "fun-asr", {
|
||||
await env.client.uploadFile(audioUrl, model, {
|
||||
signal: ctx.signal,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
fileUrls.push(u);
|
||||
fileUrls.push(audioUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const model = input.model || "fun-asr";
|
||||
if (route.kind === "sync-flash") {
|
||||
const flashFamily = route.flashFamily!;
|
||||
const body = buildAsrFlashRequest({
|
||||
model,
|
||||
audioUrl: fileUrls[0]!,
|
||||
language: input.language,
|
||||
vocabularyId: input["vocabulary-id"],
|
||||
flashFamily,
|
||||
});
|
||||
const response = await env.client.requestJson<Record<string, unknown>>({
|
||||
path: route.path,
|
||||
method: "POST",
|
||||
headers: { "X-DashScope-SSE": "disable" },
|
||||
body,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
return {
|
||||
text: extractAsrFlashText(response, flashFamily),
|
||||
model,
|
||||
mode: "sync",
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
const languageFields = buildAsyncAsrLanguageFields(
|
||||
route.asyncLanguageStyle ?? "language_hints",
|
||||
input.language,
|
||||
);
|
||||
const body: DashScopeASRRequest = {
|
||||
model,
|
||||
input: { file_urls: fileUrls },
|
||||
input:
|
||||
route.asyncInputStyle === "file_url" ? { file_url: fileUrls[0]! } : { file_urls: fileUrls },
|
||||
parameters: {
|
||||
channel_id: input["channel-id"] !== undefined ? [input["channel-id"]] : undefined,
|
||||
language_hints: input.language ? [input.language] : undefined,
|
||||
...languageFields,
|
||||
diarization_enabled: input.diarization,
|
||||
speaker_count: input["speaker-count"],
|
||||
vocabulary_id: input["vocabulary-id"],
|
||||
@@ -601,9 +683,8 @@ export async function speechRecognize(
|
||||
};
|
||||
stripUndefined(body.parameters as Record<string, unknown>);
|
||||
|
||||
const url = speechRecognizePath();
|
||||
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
|
||||
path: url,
|
||||
path: speechRecognizePath(),
|
||||
method: "POST",
|
||||
body,
|
||||
async: true,
|
||||
@@ -614,7 +695,65 @@ export async function speechRecognize(
|
||||
const pollIntervalMs = (input["poll-interval"] ?? 2) * 1000;
|
||||
const timeoutMs = (ctx.timeoutSeconds ?? 300) * 1000;
|
||||
|
||||
return await pollTaskWithOptions(env, taskId, pollIntervalMs, timeoutMs, ctx);
|
||||
// ASR polling reads original output, avoids generic flatten (avoids transcription_url polluting media urls)
|
||||
const asrTask = await pollAsrTaskWithOptions(env, taskId, pollIntervalMs, timeoutMs, ctx);
|
||||
const transcriptionItems = collectAsrTranscriptionItems(asrTask.output);
|
||||
|
||||
const base: Record<string, unknown> = {
|
||||
task_id: asrTask.output.task_id,
|
||||
task_status: asrTask.output.task_status,
|
||||
request_id: asrTask.request_id,
|
||||
mode: "async",
|
||||
model,
|
||||
};
|
||||
if (asrTask.output.results) base.results = asrTask.output.results;
|
||||
if (asrTask.output.result) {
|
||||
base.result = asrTask.output.result;
|
||||
if (typeof asrTask.output.result.transcription_url === "string") {
|
||||
base.transcription_url = asrTask.output.result.transcription_url;
|
||||
}
|
||||
}
|
||||
if (asrTask.output.task_metrics) base.task_metrics = asrTask.output.task_metrics;
|
||||
if (asrTask.usage) base.usage = asrTask.usage;
|
||||
|
||||
if (transcriptionItems.length === 0) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const texts: string[] = [];
|
||||
const transcripts: Record<string, unknown>[] = [];
|
||||
for (const item of transcriptionItems) {
|
||||
if (!item.transcription_url) continue;
|
||||
const transRes = await fetch(item.transcription_url, { signal: ctx.signal });
|
||||
if (!transRes.ok) {
|
||||
throw new PipelineError(
|
||||
"async_task_failed",
|
||||
`Failed to download transcription: HTTP ${transRes.status}`,
|
||||
{ step: "speech/recognize", details: { taskId, url: item.transcription_url } },
|
||||
);
|
||||
}
|
||||
const transData = (await transRes.json()) as Record<string, unknown>;
|
||||
transcripts.push(transData);
|
||||
const transcriptList = transData.transcripts as
|
||||
| Array<{ text?: string; sentences?: Array<{ text?: string }> }>
|
||||
| undefined;
|
||||
if (!transcriptList?.length) continue;
|
||||
for (const transcript of transcriptList) {
|
||||
if (transcript.sentences?.length) {
|
||||
for (const sentence of transcript.sentences) {
|
||||
if (sentence.text) texts.push(sentence.text);
|
||||
}
|
||||
} else if (transcript.text) {
|
||||
texts.push(transcript.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
text: texts.join("\n"),
|
||||
transcripts,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Shared: task polling ---
|
||||
@@ -640,7 +779,7 @@ function flattenTaskResponse(resp: DashScopeTaskResponse): Record<string, unknow
|
||||
if (urls.length > 0) flat.urls = urls;
|
||||
}
|
||||
if (output.results) {
|
||||
const urls = output.results.map((r) => r.url).filter(Boolean);
|
||||
const urls = output.results.map((item) => item.url).filter(Boolean);
|
||||
if (urls.length > 0 && !flat.urls) flat.urls = urls;
|
||||
}
|
||||
if (output.task_metrics) flat.task_metrics = output.task_metrics;
|
||||
@@ -658,13 +797,13 @@ async function pollTask(
|
||||
return await pollTaskWithOptions(env, taskId, pollIntervalMs, timeoutMs, ctx);
|
||||
}
|
||||
|
||||
async function pollTaskWithOptions(
|
||||
async function pollUntilSucceeded(
|
||||
env: PipelineEnv,
|
||||
taskId: string,
|
||||
pollIntervalMs: number,
|
||||
timeoutMs: number,
|
||||
ctx?: StepContext,
|
||||
): Promise<Record<string, unknown>> {
|
||||
): Promise<DashScopeTaskResponse> {
|
||||
const started = Date.now();
|
||||
let attempt = 0;
|
||||
|
||||
@@ -687,7 +826,7 @@ async function pollTaskWithOptions(
|
||||
const status = result.output.task_status;
|
||||
|
||||
if (status === "SUCCEEDED") {
|
||||
return flattenTaskResponse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (status === "FAILED") {
|
||||
@@ -718,6 +857,33 @@ async function pollTaskWithOptions(
|
||||
}
|
||||
}
|
||||
|
||||
async function pollTaskWithOptions(
|
||||
env: PipelineEnv,
|
||||
taskId: string,
|
||||
pollIntervalMs: number,
|
||||
timeoutMs: number,
|
||||
ctx?: StepContext,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return flattenTaskResponse(await pollUntilSucceeded(env, taskId, pollIntervalMs, timeoutMs, ctx));
|
||||
}
|
||||
|
||||
/** ASR task polling: preserve original output (includes results[] / result.transcription_url). */
|
||||
async function pollAsrTaskWithOptions(
|
||||
env: PipelineEnv,
|
||||
taskId: string,
|
||||
pollIntervalMs: number,
|
||||
timeoutMs: number,
|
||||
ctx?: StepContext,
|
||||
): Promise<DashScopeASRTaskResult> {
|
||||
return (await pollUntilSucceeded(
|
||||
env,
|
||||
taskId,
|
||||
pollIntervalMs,
|
||||
timeoutMs,
|
||||
ctx,
|
||||
)) as DashScopeASRTaskResult;
|
||||
}
|
||||
|
||||
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -25,6 +25,13 @@ interface CommandNode {
|
||||
children: Map<string, CommandNode>;
|
||||
}
|
||||
|
||||
const AUTH_LABELS = {
|
||||
apiKey: "API Key",
|
||||
console: "Console",
|
||||
openapi: "AK/SK",
|
||||
none: "No Auth",
|
||||
} satisfies Record<AuthRequirement, string>;
|
||||
|
||||
/**
|
||||
* What a command path resolves to in the registry. The single judgement that
|
||||
* feeds `resolve()` — no scattered `isGroupPath` + throwing `resolve`.
|
||||
@@ -157,14 +164,37 @@ export class CommandRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
private buildResourceLines(a: (s: string) => string, d: (s: string) => string): string {
|
||||
const entries: Array<{ path: string; desc: string }> = [];
|
||||
private buildCommandLines(
|
||||
entries: Array<{ path: string; auth: AuthRequirement; desc: string }>,
|
||||
accent: (text: string) => string,
|
||||
dim: (text: string) => string,
|
||||
): string {
|
||||
const maxPathLength = Math.max(...entries.map((entry) => entry.path.length));
|
||||
const maxAuthLength = Math.max(
|
||||
...entries.map((entry) => `[${AUTH_LABELS[entry.auth]}]`.length),
|
||||
);
|
||||
const rows = entries.map((entry) => {
|
||||
const authLabel = `[${AUTH_LABELS[entry.auth]}]`;
|
||||
return ` ${accent(entry.path.padEnd(maxPathLength + 2))} ${accent(authLabel.padEnd(maxAuthLength + 2))} ${dim(entry.desc)}`;
|
||||
});
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
private buildResourceLines(
|
||||
accent: (text: string) => string,
|
||||
dim: (text: string) => string,
|
||||
): string {
|
||||
const entries: Array<{ path: string; auth: AuthRequirement; desc: string }> = [];
|
||||
|
||||
const collect = (node: CommandNode, prefix: string) => {
|
||||
for (const [name, child] of node.children) {
|
||||
const fullPath = prefix ? `${prefix} ${name}` : name;
|
||||
if (child.command) {
|
||||
entries.push({ path: fullPath, desc: child.command.description });
|
||||
entries.push({
|
||||
path: fullPath,
|
||||
auth: child.command.auth,
|
||||
desc: child.command.description,
|
||||
});
|
||||
}
|
||||
if (child.children.size > 0) {
|
||||
collect(child, fullPath);
|
||||
@@ -173,8 +203,7 @@ export class CommandRegistry {
|
||||
};
|
||||
collect(this.root, "");
|
||||
|
||||
const maxLen = Math.max(...entries.map((e) => e.path.length));
|
||||
return entries.map((e) => ` ${a(e.path.padEnd(maxLen + 2))} ${d(e.desc)}`).join("\n");
|
||||
return this.buildCommandLines(entries, accent, dim);
|
||||
}
|
||||
|
||||
private buildFlagLines(
|
||||
@@ -341,6 +370,7 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b("Getting Help:")}
|
||||
|
||||
out.write(`\n${cmd.description}\n`);
|
||||
out.write(`${b("Usage:")} ${prefix}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}\n`);
|
||||
out.write(`${b("Authentication:")} ${a(AUTH_LABELS[cmd.auth])}\n`);
|
||||
const flagEntries = [
|
||||
...Object.entries(cmd.flags ?? {}),
|
||||
...Object.entries(credentialFlagDefs(cmd)),
|
||||
@@ -373,18 +403,25 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b("Getting Help:")}
|
||||
}
|
||||
|
||||
private printChildren(node: CommandNode, prefix: string, out: NodeJS.WriteStream): void {
|
||||
const entries: Array<{ fullName: string; description: string }> = [];
|
||||
const collect = (n: CommandNode, p: string) => {
|
||||
for (const [name, child] of n.children) {
|
||||
const entries: Array<{ path: string; auth: AuthRequirement; desc: string }> = [];
|
||||
const collect = (currentNode: CommandNode, currentPath: string) => {
|
||||
for (const [name, child] of currentNode.children) {
|
||||
if (child.command)
|
||||
entries.push({ fullName: `${p} ${name}`, description: child.command.description });
|
||||
if (child.children.size > 0) collect(child, `${p} ${name}`);
|
||||
entries.push({
|
||||
path: `${currentPath} ${name}`,
|
||||
auth: child.command.auth,
|
||||
desc: child.command.description,
|
||||
});
|
||||
if (child.children.size > 0) collect(child, `${currentPath} ${name}`);
|
||||
}
|
||||
};
|
||||
collect(node, prefix);
|
||||
const maxLen = Math.max(...entries.map((e) => e.fullName.length));
|
||||
for (const { fullName, description } of entries) {
|
||||
out.write(` ${this.accent(fullName.padEnd(maxLen), out)} ${this.dim(description, out)}\n`);
|
||||
}
|
||||
out.write(
|
||||
this.buildCommandLines(
|
||||
entries,
|
||||
(text) => this.accent(text, out),
|
||||
(text) => this.dim(text, out),
|
||||
) + "\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ExitCode } from "bailian-cli-core";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { handleError } from "../src/error-handler.ts";
|
||||
|
||||
test("handleError: fetch failed JSON includes cause.code from errno", () => {
|
||||
const previousOutput = process.env.DASHSCOPE_OUTPUT;
|
||||
process.env.DASHSCOPE_OUTPUT = "json";
|
||||
|
||||
let stderr = "";
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const originalExit = process.exit;
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
process.exit = ((code?: number) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as typeof process.exit;
|
||||
|
||||
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.invalid"), {
|
||||
code: "ENOTFOUND",
|
||||
});
|
||||
const fetchFailed = new TypeError("fetch failed", { cause: root });
|
||||
|
||||
try {
|
||||
expect(() => handleError(fetchFailed, "bl")).toThrow(
|
||||
new RegExp(`process\\.exit:${ExitCode.NETWORK}`),
|
||||
);
|
||||
const payload = JSON.parse(stderr.trim()) as {
|
||||
error: { code: number; message: string; cause?: { message: string; code?: string } };
|
||||
};
|
||||
expect(payload.error.code).toBe(ExitCode.NETWORK);
|
||||
expect(payload.error.message).toMatch(/ENOTFOUND/);
|
||||
expect(payload.error.cause).toEqual({
|
||||
message: root.message,
|
||||
code: "ENOTFOUND",
|
||||
});
|
||||
} finally {
|
||||
process.stderr.write = originalWrite;
|
||||
process.exit = originalExit;
|
||||
if (previousOutput === undefined) {
|
||||
delete process.env.DASHSCOPE_OUTPUT;
|
||||
} else {
|
||||
process.env.DASHSCOPE_OUTPUT = previousOutput;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("handleError: fetch failed without nested cause still maps to NETWORK", () => {
|
||||
const previousOutput = process.env.DASHSCOPE_OUTPUT;
|
||||
process.env.DASHSCOPE_OUTPUT = "json";
|
||||
|
||||
let stderr = "";
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const originalExit = process.exit;
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
process.exit = ((code?: number) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as typeof process.exit;
|
||||
|
||||
const fetchFailed = new TypeError("fetch failed");
|
||||
|
||||
try {
|
||||
expect(() => handleError(fetchFailed, "bl")).toThrow(
|
||||
new RegExp(`process\\.exit:${ExitCode.NETWORK}`),
|
||||
);
|
||||
const payload = JSON.parse(stderr.trim()) as {
|
||||
error: { code: number; message: string; cause?: { message: string; code?: string } };
|
||||
};
|
||||
expect(payload.error.code).toBe(ExitCode.NETWORK);
|
||||
expect(payload.error.message).toMatch(/unknown cause/);
|
||||
expect(payload.error.cause).toEqual({ message: "fetch failed" });
|
||||
} finally {
|
||||
process.stderr.write = originalWrite;
|
||||
process.exit = originalExit;
|
||||
if (previousOutput === undefined) {
|
||||
delete process.env.DASHSCOPE_OUTPUT;
|
||||
} else {
|
||||
process.env.DASHSCOPE_OUTPUT = previousOutput;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import type { Client } from "bailian-cli-core";
|
||||
import { PipelineError } from "../src/pipeline/errors.ts";
|
||||
import type { PipelineEnv } from "../src/pipeline/bl-config.ts";
|
||||
import { speechRecognize } from "../src/pipeline/steps/bl-api.ts";
|
||||
import type { StepContext } from "../src/pipeline/types.ts";
|
||||
|
||||
type CapturedRequest = {
|
||||
path?: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
async?: boolean;
|
||||
};
|
||||
|
||||
function makeEnv(requestJsonImpl?: (opts: CapturedRequest) => Promise<unknown>): {
|
||||
env: PipelineEnv;
|
||||
captured: CapturedRequest[];
|
||||
} {
|
||||
const captured: CapturedRequest[] = [];
|
||||
const client = {
|
||||
uploadFile: async (source: string) => source,
|
||||
requestJson: async (opts: CapturedRequest) => {
|
||||
captured.push(opts);
|
||||
if (requestJsonImpl) return requestJsonImpl(opts);
|
||||
return { output: { text: "ok" } };
|
||||
},
|
||||
} as unknown as Client;
|
||||
|
||||
return {
|
||||
env: {
|
||||
client,
|
||||
settings: { quiet: true, output: "json" } as PipelineEnv["settings"],
|
||||
},
|
||||
captured,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(): StepContext {
|
||||
return { dryRun: false, signal: new AbortController().signal };
|
||||
}
|
||||
|
||||
test("pipeline speechRecognize routes input-audio flash to sync multimodal endpoint", async () => {
|
||||
const { env, captured } = makeEnv();
|
||||
const result = (await speechRecognize(
|
||||
env,
|
||||
{
|
||||
url: "https://example.com/a.wav",
|
||||
model: "qwen-audio-3.0-asr-flash",
|
||||
language: "en",
|
||||
"vocabulary-id": "vocab-1",
|
||||
},
|
||||
makeCtx(),
|
||||
)) as { mode?: string; text?: string };
|
||||
|
||||
expect(result.mode).toBe("sync");
|
||||
expect(result.text).toBe("ok");
|
||||
expect(captured).toHaveLength(1);
|
||||
expect(captured[0]?.path).toBe("/api/v1/services/aigc/multimodal-generation/generation");
|
||||
expect(captured[0]?.headers?.["X-DashScope-SSE"]).toBe("disable");
|
||||
expect(captured[0]?.body).toMatchObject({
|
||||
model: "qwen-audio-3.0-asr-flash",
|
||||
parameters: {
|
||||
format: "wav",
|
||||
language_hints: ["en"],
|
||||
vocabulary_id: "vocab-1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("pipeline speechRecognize maps qwen3-filetrans language to parameters.language", async () => {
|
||||
const { env, captured } = makeEnv(async (opts) => {
|
||||
if (opts.async || opts.method === "POST") {
|
||||
return { output: { task_id: "task-1", task_status: "PENDING" } };
|
||||
}
|
||||
return {
|
||||
output: { task_id: "task-1", task_status: "SUCCEEDED", results: [] },
|
||||
request_id: "r1",
|
||||
};
|
||||
});
|
||||
|
||||
await speechRecognize(
|
||||
env,
|
||||
{
|
||||
url: "https://example.com/a.wav",
|
||||
model: "qwen3-asr-flash-filetrans",
|
||||
language: "zh",
|
||||
"poll-interval": 0,
|
||||
},
|
||||
makeCtx(),
|
||||
);
|
||||
|
||||
expect(captured[0]?.path).toBe("/api/v1/services/audio/asr/transcription");
|
||||
expect(captured[0]?.async).toBe(true);
|
||||
expect(captured[0]?.body).toMatchObject({
|
||||
model: "qwen3-asr-flash-filetrans",
|
||||
input: { file_url: "https://example.com/a.wav" },
|
||||
parameters: { language: "zh" },
|
||||
});
|
||||
expect(
|
||||
(captured[0]?.body?.parameters as Record<string, unknown> | undefined)?.language_hints,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("pipeline speechRecognize rejects realtime models before requesting", async () => {
|
||||
const { env, captured } = makeEnv();
|
||||
await expect(
|
||||
speechRecognize(
|
||||
env,
|
||||
{ url: "https://example.com/a.wav", model: "qwen3-asr-flash-realtime" },
|
||||
makeCtx(),
|
||||
),
|
||||
).rejects.toBeInstanceOf(PipelineError);
|
||||
expect(captured).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("pipeline speechRecognize rejects multiple urls for sync flash", async () => {
|
||||
const { env, captured } = makeEnv();
|
||||
await expect(
|
||||
speechRecognize(
|
||||
env,
|
||||
{
|
||||
url: ["https://example.com/a.wav", "https://example.com/b.wav"],
|
||||
model: "fun-asr-flash-2026-06-15",
|
||||
},
|
||||
makeCtx(),
|
||||
),
|
||||
).rejects.toBeInstanceOf(PipelineError);
|
||||
expect(captured).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("pipeline speechRecognize downloads qwen3 singular result.transcription_url", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const transcriptionUrl = "https://example.com/transcription.json";
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
expect(url).toBe(transcriptionUrl);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
transcripts: [{ text: "pipeline hello", sentences: [{ text: "pipeline hello" }] }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { env, captured } = makeEnv(async (opts) => {
|
||||
if (opts.async || opts.method === "POST") {
|
||||
return { output: { task_id: "task-1", task_status: "PENDING" } };
|
||||
}
|
||||
return {
|
||||
output: {
|
||||
task_id: "task-1",
|
||||
task_status: "SUCCEEDED",
|
||||
result: { transcription_url: transcriptionUrl },
|
||||
},
|
||||
request_id: "r1",
|
||||
};
|
||||
});
|
||||
|
||||
const result = (await speechRecognize(
|
||||
env,
|
||||
{
|
||||
url: "https://example.com/a.wav",
|
||||
model: "qwen3-asr-flash-filetrans",
|
||||
"poll-interval": 0,
|
||||
},
|
||||
makeCtx(),
|
||||
)) as {
|
||||
mode?: string;
|
||||
text?: string;
|
||||
transcription_url?: string;
|
||||
result?: { transcription_url?: string };
|
||||
};
|
||||
|
||||
expect(captured[0]?.async).toBe(true);
|
||||
expect(result.mode).toBe("async");
|
||||
expect(result.text).toBe("pipeline hello");
|
||||
expect(result.transcription_url).toBe(transcriptionUrl);
|
||||
expect(result.result?.transcription_url).toBe(transcriptionUrl);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Generated
+3
-3
@@ -24,12 +24,12 @@ catalogs:
|
||||
chalk:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
tar-stream:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
smol-toml:
|
||||
specifier: ^1.4.2
|
||||
version: 1.7.0
|
||||
tar-stream:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
tsx:
|
||||
specifier: ^4.23.0
|
||||
version: 4.23.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.14.2"
|
||||
version: "1.14.3"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -71,6 +71,8 @@ Use this table only after the decision table in [`bailian-protocol`](../bailian-
|
||||
| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions |
|
||||
| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Coding Plan quota usage | `bl usage coding-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Console API (advanced) | `bl console call` | Console auth |
|
||||
| Bailian workspace listing | `bl workspace list` | Console auth |
|
||||
| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` |
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `bl advisor recommend` | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------------- | -------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `bl advisor recommend` | API Key | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl advisor recommend`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `advisor recommend` |
|
||||
| **Description** | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
|
||||
| **Usage** | `bl advisor recommend --message <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `advisor recommend` |
|
||||
| **Description** | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl advisor recommend --message <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,20 +7,21 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------- | ---------------------------------------------- |
|
||||
| `bl app call` | Call a Bailian application (agent or workflow) |
|
||||
| `bl app list` | List Bailian applications |
|
||||
| Command | Authentication | Description |
|
||||
| ------------- | -------------- | ---------------------------------------------- |
|
||||
| `bl app call` | API Key | Call a Bailian application (agent or workflow) |
|
||||
| `bl app list` | Console | List Bailian applications |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl app call`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| **Name** | `app call` |
|
||||
| **Description** | Call a Bailian application (agent or workflow) |
|
||||
| **Usage** | `bl app call --app-id <id> --prompt <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| **Name** | `app call` |
|
||||
| **Description** | Call a Bailian application (agent or workflow) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl app call --app-id <id> --prompt <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -67,11 +68,12 @@ bl app call --app-id abc123 --prompt "Start" --biz-params '{"key":"value"}'
|
||||
|
||||
### `bl app list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------- |
|
||||
| **Name** | `app list` |
|
||||
| **Description** | List Bailian applications |
|
||||
| **Usage** | `bl app list [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------- |
|
||||
| **Name** | `app list` |
|
||||
| **Description** | List Bailian applications |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl app list [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
# `bl asset-center` commands
|
||||
|
||||
> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand.
|
||||
> Regenerate: `pnpm --filter bailian-cli run generate:reference`.
|
||||
|
||||
Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | -------------------------------------------------------------- |
|
||||
| `bl asset-center delete` | Delete assets (soft delete to recycle bin by default) |
|
||||
| `bl asset-center download` | Get a signed download URL for an asset by ID |
|
||||
| `bl asset-center favorite` | Add assets to favorites |
|
||||
| `bl asset-center get` | Get full details of a model-generated asset |
|
||||
| `bl asset-center list` | List model-generated assets with filters and cursor pagination |
|
||||
| `bl asset-center stats` | Count model-generated assets by type |
|
||||
| `bl asset-center storage` | View storage quota, usage, and overage pricing |
|
||||
| `bl asset-center unfavorite` | Remove assets from favorites |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl asset-center delete`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------------- |
|
||||
| **Name** | `asset-center delete` |
|
||||
| **Description** | Delete assets (soft delete to recycle bin by default) |
|
||||
| **Usage** | `bl asset-center delete --id <asset-id> [--id <asset-id>...] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--id <asset-id>` | array | yes | Asset ID(s) to operate on (repeatable, max 100) |
|
||||
| `--permanent` | switch | no | Permanently delete assets (cannot be restored) |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center delete --id asset-001
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center delete --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center delete --id asset-001 --permanent
|
||||
```
|
||||
|
||||
### `bl asset-center download`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| **Name** | `asset-center download` |
|
||||
| **Description** | Get a signed download URL for an asset by ID |
|
||||
| **Usage** | `bl asset-center download --id <asset-id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--id <asset-id>` | string | yes | Asset ID to get download URL for |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center download --id asset-001
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center download --id asset-001 --output json
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center download --id asset-001 --quiet
|
||||
```
|
||||
|
||||
### `bl asset-center favorite`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------- |
|
||||
| **Name** | `asset-center favorite` |
|
||||
| **Description** | Add assets to favorites |
|
||||
| **Usage** | `bl asset-center favorite --id <asset-id> [--id <asset-id>...]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--id <asset-id>` | array | yes | Asset ID(s) to operate on (repeatable, max 100) |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center favorite --id asset-001
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center favorite --id asset-001 --id asset-002
|
||||
```
|
||||
|
||||
### `bl asset-center get`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------- |
|
||||
| **Name** | `asset-center get` |
|
||||
| **Description** | Get full details of a model-generated asset |
|
||||
| **Usage** | `bl asset-center get --asset-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--asset-id <id>` | string | yes | Asset ID to query |
|
||||
| `--include-download-url` | switch | no | Include signed download URL |
|
||||
| `--include-thumbnail` | switch | no | Include thumbnail URL |
|
||||
| `--thumbnail-width <px>` | number | no | Thumbnail width in pixels |
|
||||
| `--thumbnail-height <px>` | number | no | Thumbnail height in pixels |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center get --asset-id asset-001
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center get --asset-id asset-001 --include-download-url --output json
|
||||
```
|
||||
|
||||
### `bl asset-center list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------- |
|
||||
| **Name** | `asset-center list` |
|
||||
| **Description** | List model-generated assets with filters and cursor pagination |
|
||||
| **Usage** | `bl asset-center list [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------------------------------------------- | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--type <IMAGE\|VIDEO\|AUDIO>` | string | no | Asset type: IMAGE, VIDEO, or AUDIO |
|
||||
| `--model <name>` | string | no | Filter by model name |
|
||||
| `--keyword <text>` | string | no | Filter by asset name (substring match) |
|
||||
| `--favorited` | switch | no | Show or count only favorited assets |
|
||||
| `--recycle-bin` | switch | no | Show or count soft-deleted assets (recycle bin) |
|
||||
| `--sync-status <NOT_SYNCED\|IN_SYNCING\|SYNC_SUCCESS\|SYNC_FAILED>` | string | no | OSS sync status filter |
|
||||
| `--begin-time <datetime>` | string | no | Filter by generate time start (ISO_LOCAL_DATE_TIME) |
|
||||
| `--end-time <datetime>` | string | no | Filter by generate time end (ISO_LOCAL_DATE_TIME) |
|
||||
| `--include-download-url` | switch | no | Include signed download URLs in the response |
|
||||
| `--include-thumbnail` | switch | no | Include thumbnail URLs in the response |
|
||||
| `--thumbnail-width <px>` | number | no | Thumbnail width in pixels |
|
||||
| `--thumbnail-height <px>` | number | no | Thumbnail height in pixels |
|
||||
| `--page-size <n>` | number | no | Results per page (default: 10, max: 100) |
|
||||
| `--next-token <token>` | number | no | Cursor for the next page |
|
||||
| `--pre-token <token>` | number | no | Cursor for the previous page |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center list
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center list --type IMAGE --model qwen-image-3.0
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center list --favorited --page-size 20
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center list --recycle-bin
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center list --keyword landscape --output json
|
||||
```
|
||||
|
||||
### `bl asset-center stats`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------ |
|
||||
| **Name** | `asset-center stats` |
|
||||
| **Description** | Count model-generated assets by type |
|
||||
| **Usage** | `bl asset-center stats [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------------------------------------------- | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--type <IMAGE\|VIDEO\|AUDIO>` | string | no | Asset type: IMAGE, VIDEO, or AUDIO |
|
||||
| `--model <name>` | string | no | Filter by model name |
|
||||
| `--keyword <text>` | string | no | Filter by asset name (substring match) |
|
||||
| `--favorited` | switch | no | Show or count only favorited assets |
|
||||
| `--recycle-bin` | switch | no | Show or count soft-deleted assets (recycle bin) |
|
||||
| `--sync-status <NOT_SYNCED\|IN_SYNCING\|SYNC_SUCCESS\|SYNC_FAILED>` | string | no | OSS sync status filter |
|
||||
| `--begin-time <datetime>` | string | no | Filter by generate time start (ISO_LOCAL_DATE_TIME) |
|
||||
| `--end-time <datetime>` | string | no | Filter by generate time end (ISO_LOCAL_DATE_TIME) |
|
||||
| `--sync-failed` | switch | no | Also count assets with failed OSS sync |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center stats
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center stats --sync-failed
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center stats --type IMAGE --output json
|
||||
```
|
||||
|
||||
### `bl asset-center storage`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------- |
|
||||
| **Name** | `asset-center storage` |
|
||||
| **Description** | View storage quota, usage, and overage pricing |
|
||||
| **Usage** | `bl asset-center storage [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center storage
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center storage --output json
|
||||
```
|
||||
|
||||
### `bl asset-center unfavorite`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------- |
|
||||
| **Name** | `asset-center unfavorite` |
|
||||
| **Description** | Remove assets from favorites |
|
||||
| **Usage** | `bl asset-center unfavorite --id <asset-id> [--id <asset-id>...]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--id <asset-id>` | array | yes | Asset ID(s) to operate on (repeatable, max 100) |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl asset-center unfavorite --id asset-001
|
||||
```
|
||||
|
||||
```bash
|
||||
bl asset-center unfavorite --id asset-001 --id asset-002
|
||||
```
|
||||
@@ -7,22 +7,23 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `bl auth generate-access-token` | Generate a CLI access token using OpenAPI AK/SK |
|
||||
| `bl auth login` | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) |
|
||||
| `bl auth logout` | Clear stored credentials; full logout also clears the model Base URL |
|
||||
| `bl auth status` | Show current authentication state |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------------------- | -------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `bl auth generate-access-token` | No Auth | Generate a CLI access token using OpenAPI AK/SK |
|
||||
| `bl auth login` | No Auth | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) |
|
||||
| `bl auth logout` | No Auth | Clear stored credentials; full logout also clears the model Base URL |
|
||||
| `bl auth status` | No Auth | Show current authentication state |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl auth generate-access-token`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `auth generate-access-token` |
|
||||
| **Description** | Generate a CLI access token using OpenAPI AK/SK |
|
||||
| **Usage** | `bl auth generate-access-token --access-key-id <id> --access-key-secret <secret> --security-token <token>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `auth generate-access-token` |
|
||||
| **Description** | Generate a CLI access token using OpenAPI AK/SK |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl auth generate-access-token --access-key-id <id> --access-key-secret <secret> --security-token <token>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -40,11 +41,12 @@ bl auth generate-access-token --access-key-id LTAIxxxxx --access-key-secret xxxx
|
||||
|
||||
### `bl auth login`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `auth login` |
|
||||
| **Description** | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) |
|
||||
| **Usage** | `bl auth login --api-key <key> \| --console \| --open-api --access-key-id <id> --access-key-secret <secret>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `auth login` |
|
||||
| **Description** | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl auth login --api-key <key> \| --console \| --open-api --access-key-id <id> --access-key-secret <secret>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -78,11 +80,12 @@ bl auth login --open-api --access-key-id LTAIxxxxx --access-key-secret xxxxx
|
||||
|
||||
### `bl auth logout`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------- |
|
||||
| **Name** | `auth logout` |
|
||||
| **Description** | Clear stored credentials; full logout also clears the model Base URL |
|
||||
| **Usage** | `bl auth logout [--console \| --open-api] [--dry-run]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `auth logout` |
|
||||
| **Description** | Clear stored credentials; full logout also clears the model Base URL |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl auth logout [--console \| --open-api] [--dry-run]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -111,11 +114,12 @@ bl auth logout --dry-run
|
||||
|
||||
### `bl auth status`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------- |
|
||||
| **Name** | `auth status` |
|
||||
| **Description** | Show current authentication state |
|
||||
| **Usage** | `bl auth status` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------- |
|
||||
| **Name** | `auth status` |
|
||||
| **Description** | Show current authentication state |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl auth status` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,24 +7,25 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ----------------- | ------------------------------------------------ |
|
||||
| `bl config agent` | Configure a coding agent to use DashScope API |
|
||||
| `bl config list` | List config profiles and show the active profile |
|
||||
| `bl config set` | Set a config value |
|
||||
| `bl config show` | Display current configuration |
|
||||
| `bl config ui` | Open a local web UI to manage config profiles |
|
||||
| `bl config use` | Set the active config profile |
|
||||
| Command | Authentication | Description |
|
||||
| ----------------- | -------------- | ------------------------------------------------ |
|
||||
| `bl config agent` | No Auth | Configure a coding agent to use DashScope API |
|
||||
| `bl config list` | No Auth | List config profiles and show the active profile |
|
||||
| `bl config set` | No Auth | Set a config value |
|
||||
| `bl config show` | No Auth | Display current configuration |
|
||||
| `bl config ui` | No Auth | Open a local web UI to manage config profiles |
|
||||
| `bl config use` | No Auth | Set the active config profile |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl config agent`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `config agent` |
|
||||
| **Description** | Configure a coding agent to use DashScope API |
|
||||
| **Usage** | `bl config agent --agent <name> (--base-url <url> \| --region <region>) (--api-key <key> \| --key <encoded>) --model <model>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `config agent` |
|
||||
| **Description** | Configure a coding agent to use DashScope API |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config agent --agent <name> (--base-url <url> \| --region <region>) (--api-key <key> \| --key <encoded>) --model <model>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -55,11 +56,12 @@ bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatib
|
||||
|
||||
### `bl config list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `config list` |
|
||||
| **Description** | List config profiles and show the active profile |
|
||||
| **Usage** | `bl config list` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| **Name** | `config list` |
|
||||
| **Description** | List config profiles and show the active profile |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config list` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -77,11 +79,12 @@ bl config list --output json
|
||||
|
||||
### `bl config set`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------- |
|
||||
| **Name** | `config set` |
|
||||
| **Description** | Set a config value |
|
||||
| **Usage** | `bl config set --key <key> --value <value>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| **Name** | `config set` |
|
||||
| **Description** | Set a config value |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config set --key <key> --value <value>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -106,11 +109,12 @@ bl config set --key base_url --value https://dashscope.aliyuncs.com
|
||||
|
||||
### `bl config show`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------- |
|
||||
| **Name** | `config show` |
|
||||
| **Description** | Display current configuration |
|
||||
| **Usage** | `bl config show` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------- |
|
||||
| **Name** | `config show` |
|
||||
| **Description** | Display current configuration |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config show` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -128,11 +132,12 @@ bl config show --output json
|
||||
|
||||
### `bl config ui`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------- |
|
||||
| **Name** | `config ui` |
|
||||
| **Description** | Open a local web UI to manage config profiles |
|
||||
| **Usage** | `bl config ui [--port <port>] [--no-open]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------- |
|
||||
| **Name** | `config ui` |
|
||||
| **Description** | Open a local web UI to manage config profiles |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config ui [--port <port>] [--no-open]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -157,11 +162,12 @@ bl config ui --no-open
|
||||
|
||||
### `bl config use`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------- |
|
||||
| **Name** | `config use` |
|
||||
| **Description** | Set the active config profile |
|
||||
| **Usage** | `bl config use --name <name>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------- |
|
||||
| **Name** | `config use` |
|
||||
| **Description** | Set the active config profile |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl config use --name <name>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ----------------- | ---------------------------------------------- |
|
||||
| `bl console call` | Call a Bailian console API via the CLI gateway |
|
||||
| Command | Authentication | Description |
|
||||
| ----------------- | -------------- | ---------------------------------------------- |
|
||||
| `bl console call` | Console | Call a Bailian console API via the CLI gateway |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl console call`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| **Name** | `console call` |
|
||||
| **Description** | Call a Bailian console API via the CLI gateway |
|
||||
| **Usage** | `bl console call --api <api> --data <json> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| **Name** | `console call` |
|
||||
| **Description** | Call a Bailian console API via the CLI gateway |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl console call --api <api> --data <json> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -35,7 +36,7 @@ Index: [index.md](index.md)
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl console call --api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'
|
||||
bl console call --api zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------- | -------------------------------------------------------- |
|
||||
| `bl file upload` | Upload a local file to DashScope temporary storage (48h) |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------- | -------------- | -------------------------------------------------------- |
|
||||
| `bl file upload` | API Key | Upload a local file to DashScope temporary storage (48h) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl file upload`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| **Name** | `file upload` |
|
||||
| **Description** | Upload a local file to DashScope temporary storage (48h) |
|
||||
| **Usage** | `bl file upload --file <path> --model <model>` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------- |
|
||||
| **Name** | `file upload` |
|
||||
| **Description** | Upload a local file to DashScope temporary storage (48h) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl file upload --file <path> --model <model>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -9,99 +9,92 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## Quick index
|
||||
|
||||
| Command | Description | Detail |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `bl advisor recommend` | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) | [advisor.md](advisor.md) |
|
||||
| `bl app call` | Call a Bailian application (agent or workflow) | [app.md](app.md) |
|
||||
| `bl app list` | List Bailian applications | [app.md](app.md) |
|
||||
| `bl asset-center delete` | Delete assets (soft delete to recycle bin by default) | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center download` | Get a signed download URL for an asset by ID | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center favorite` | Add assets to favorites | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center get` | Get full details of a model-generated asset | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center list` | List model-generated assets with filters and cursor pagination | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center stats` | Count model-generated assets by type | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center storage` | View storage quota, usage, and overage pricing | [asset-center.md](asset-center.md) |
|
||||
| `bl asset-center unfavorite` | Remove assets from favorites | [asset-center.md](asset-center.md) |
|
||||
| `bl auth generate-access-token` | Generate a CLI access token using OpenAPI AK/SK | [auth.md](auth.md) |
|
||||
| `bl auth login` | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) | [auth.md](auth.md) |
|
||||
| `bl auth logout` | Clear stored credentials; full logout also clears the model Base URL | [auth.md](auth.md) |
|
||||
| `bl auth status` | Show current authentication state | [auth.md](auth.md) |
|
||||
| `bl config agent` | Configure a coding agent to use DashScope API | [config.md](config.md) |
|
||||
| `bl config list` | List config profiles and show the active profile | [config.md](config.md) |
|
||||
| `bl config set` | Set a config value | [config.md](config.md) |
|
||||
| `bl config show` | Display current configuration | [config.md](config.md) |
|
||||
| `bl config ui` | Open a local web UI to manage config profiles | [config.md](config.md) |
|
||||
| `bl config use` | Set the active config profile | [config.md](config.md) |
|
||||
| `bl console call` | Call a Bailian console API via the CLI gateway | [console.md](console.md) |
|
||||
| `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) |
|
||||
| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | [knowledge.md](knowledge.md) |
|
||||
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | [knowledge.md](knowledge.md) |
|
||||
| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | [knowledge.md](knowledge.md) |
|
||||
| `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) |
|
||||
| `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) |
|
||||
| `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) |
|
||||
| `bl memory add` | Add memory from messages or custom content | [memory.md](memory.md) |
|
||||
| `bl memory delete` | Delete a memory node | [memory.md](memory.md) |
|
||||
| `bl memory list` | List memory nodes for a user | [memory.md](memory.md) |
|
||||
| `bl memory profile create` | Create a user profile schema for memory profiling | [memory.md](memory.md) |
|
||||
| `bl memory profile get` | Get user profile by schema ID and user ID | [memory.md](memory.md) |
|
||||
| `bl memory search` | Search memory nodes by query or messages | [memory.md](memory.md) |
|
||||
| `bl memory update` | Update a memory node content | [memory.md](memory.md) |
|
||||
| `bl model list` | Browse model families or show detailed model info in the Bailian model marketplace | [model.md](model.md) |
|
||||
| `bl pipeline run` | Run a pipeline workflow definition | [pipeline.md](pipeline.md) |
|
||||
| `bl pipeline validate` | Validate a pipeline definition without executing | [pipeline.md](pipeline.md) |
|
||||
| `bl plugin install` | Install or upgrade an allowlisted Command Pack | [plugin.md](plugin.md) |
|
||||
| `bl plugin link` | Link an allowlisted local Command Pack for development | [plugin.md](plugin.md) |
|
||||
| `bl plugin list` | List installed Command Packs and their load status | [plugin.md](plugin.md) |
|
||||
| `bl plugin remove` | Remove an installed Command Pack | [plugin.md](plugin.md) |
|
||||
| `bl quota check` | Check current usage against rate limits | [quota.md](quota.md) |
|
||||
| `bl quota history` | View quota change history | [quota.md](quota.md) |
|
||||
| `bl quota list` | View model RPM/TPM rate limits | [quota.md](quota.md) |
|
||||
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
|
||||
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
|
||||
| `bl skill add` | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
|
||||
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
|
||||
| `bl skill list` | List registry skills and diff against local installs | [skill.md](skill.md) |
|
||||
| `bl skill remove` | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
|
||||
| `bl skill update` | Update installed skills to the latest registry versions | [skill.md](skill.md) |
|
||||
| `bl text chat` | Send a chat completion (OpenAI compatible, DashScope) | [text.md](text.md) |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details | [token-plan.md](token-plan.md) |
|
||||
| `bl update` | Update the CLI to the latest or a specified version | [update.md](update.md) |
|
||||
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
| `bl usage stats` | Query model usage statistics | [usage.md](usage.md) |
|
||||
| `bl usage summary` | Show a unified usage summary: free-tier quota and recent usage overview | [usage.md](usage.md) |
|
||||
| `bl workspace init` | Initialize Bailian workspace and activate postpaid services | [workspace.md](workspace.md) |
|
||||
| `bl workspace list` | List all workspaces | [workspace.md](workspace.md) |
|
||||
| Command | Authentication | Description | Detail |
|
||||
| ------------------------------- | -------------- | ---------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `bl advisor recommend` | API Key | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) | [advisor.md](advisor.md) |
|
||||
| `bl app call` | API Key | Call a Bailian application (agent or workflow) | [app.md](app.md) |
|
||||
| `bl app list` | Console | List Bailian applications | [app.md](app.md) |
|
||||
| `bl auth generate-access-token` | No Auth | Generate a CLI access token using OpenAPI AK/SK | [auth.md](auth.md) |
|
||||
| `bl auth login` | No Auth | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) | [auth.md](auth.md) |
|
||||
| `bl auth logout` | No Auth | Clear stored credentials; full logout also clears the model Base URL | [auth.md](auth.md) |
|
||||
| `bl auth status` | No Auth | Show current authentication state | [auth.md](auth.md) |
|
||||
| `bl config agent` | No Auth | Configure a coding agent to use DashScope API | [config.md](config.md) |
|
||||
| `bl config list` | No Auth | List config profiles and show the active profile | [config.md](config.md) |
|
||||
| `bl config set` | No Auth | Set a config value | [config.md](config.md) |
|
||||
| `bl config show` | No Auth | Display current configuration | [config.md](config.md) |
|
||||
| `bl config ui` | No Auth | Open a local web UI to manage config profiles | [config.md](config.md) |
|
||||
| `bl config use` | No Auth | Set the active config profile | [config.md](config.md) |
|
||||
| `bl console call` | Console | Call a Bailian console API via the CLI gateway | [console.md](console.md) |
|
||||
| `bl file upload` | API Key | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) |
|
||||
| `bl knowledge chat` | API Key | Chat with a Bailian knowledge base (RAG Q&A with streaming) | [knowledge.md](knowledge.md) |
|
||||
| `bl knowledge retrieve` | API Key | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | [knowledge.md](knowledge.md) |
|
||||
| `bl knowledge search` | API Key | Search a Bailian knowledge base (RAG semantic retrieval) | [knowledge.md](knowledge.md) |
|
||||
| `bl mcp call` | API Key | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) |
|
||||
| `bl mcp list` | Console | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) |
|
||||
| `bl mcp tools` | API Key | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) |
|
||||
| `bl memory add` | API Key | Add memory from messages or custom content | [memory.md](memory.md) |
|
||||
| `bl memory delete` | API Key | Delete a memory node | [memory.md](memory.md) |
|
||||
| `bl memory list` | API Key | List memory nodes for a user | [memory.md](memory.md) |
|
||||
| `bl memory profile create` | API Key | Create a user profile schema for memory profiling | [memory.md](memory.md) |
|
||||
| `bl memory profile get` | API Key | Get user profile by schema ID and user ID | [memory.md](memory.md) |
|
||||
| `bl memory search` | API Key | Search memory nodes by query or messages | [memory.md](memory.md) |
|
||||
| `bl memory update` | API Key | Update a memory node content | [memory.md](memory.md) |
|
||||
| `bl model list` | Console | Browse model families or show detailed model info in the Bailian model marketplace | [model.md](model.md) |
|
||||
| `bl pipeline run` | No Auth | Run a pipeline workflow definition | [pipeline.md](pipeline.md) |
|
||||
| `bl pipeline validate` | No Auth | Validate a pipeline definition without executing | [pipeline.md](pipeline.md) |
|
||||
| `bl plugin install` | No Auth | Install or upgrade an allowlisted Command Pack | [plugin.md](plugin.md) |
|
||||
| `bl plugin link` | No Auth | Link an allowlisted local Command Pack for development | [plugin.md](plugin.md) |
|
||||
| `bl plugin list` | No Auth | List installed Command Packs and their load status | [plugin.md](plugin.md) |
|
||||
| `bl plugin remove` | No Auth | Remove an installed Command Pack | [plugin.md](plugin.md) |
|
||||
| `bl quota check` | Console | Check current usage against rate limits | [quota.md](quota.md) |
|
||||
| `bl quota history` | Console | View quota change history | [quota.md](quota.md) |
|
||||
| `bl quota list` | Console | View model RPM/TPM rate limits | [quota.md](quota.md) |
|
||||
| `bl quota request` | Console | Request a temporary quota increase | [quota.md](quota.md) |
|
||||
| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
|
||||
| `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
|
||||
| `bl skill init` | No Auth | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
|
||||
| `bl skill list` | No Auth | List registry skills and diff against local installs | [skill.md](skill.md) |
|
||||
| `bl skill remove` | No Auth | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
|
||||
| `bl skill update` | No Auth | Update installed skills to the latest registry versions | [skill.md](skill.md) |
|
||||
| `bl text chat` | API Key | Send a chat completion (OpenAI compatible, DashScope) | [text.md](text.md) |
|
||||
| `bl token-plan add-member` | AK/SK | Add a member to a Token Plan organization | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan assign-seats` | AK/SK | Batch assign Token Plan seats to members | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan create-key` | AK/SK | Create a Token Plan API key for a seat | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan list-seats` | AK/SK | List Token Plan subscription seat details | [token-plan.md](token-plan.md) |
|
||||
| `bl update` | No Auth | Update the CLI to the latest or a specified version | [update.md](update.md) |
|
||||
| `bl usage coding-plan` | Console | Show Coding Plan quota usage | [usage.md](usage.md) |
|
||||
| `bl usage free` | Console | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
|
||||
| `bl usage freetier` | Console | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
| `bl usage stats` | Console | Query model usage statistics | [usage.md](usage.md) |
|
||||
| `bl usage summary` | Console | Show a unified usage summary: free-tier quota and recent usage overview | [usage.md](usage.md) |
|
||||
| `bl usage token-plan` | Console | Show Token Plan quota usage | [usage.md](usage.md) |
|
||||
| `bl workspace init` | No Auth | Initialize Bailian workspace and activate postpaid services | [workspace.md](workspace.md) |
|
||||
| `bl workspace list` | Console | List all workspaces | [workspace.md](workspace.md) |
|
||||
|
||||
## By group
|
||||
|
||||
| Group | Commands | Reference |
|
||||
| -------------- | --------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `advisor` | `recommend` | [advisor.md](advisor.md) |
|
||||
| `app` | `call`, `list` | [app.md](app.md) |
|
||||
| `asset-center` | `delete`, `download`, `favorite`, `get`, `list`, `stats`, `storage`, `unfavorite` | [asset-center.md](asset-center.md) |
|
||||
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
|
||||
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
|
||||
| `console` | `call` | [console.md](console.md) |
|
||||
| `file` | `upload` | [file.md](file.md) |
|
||||
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
|
||||
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
|
||||
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
|
||||
| `model` | `list` | [model.md](model.md) |
|
||||
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
|
||||
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
|
||||
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
|
||||
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
|
||||
| Group | Commands | Reference |
|
||||
| ------------ | ---------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `advisor` | `recommend` | [advisor.md](advisor.md) |
|
||||
| `app` | `call`, `list` | [app.md](app.md) |
|
||||
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
|
||||
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
|
||||
| `console` | `call` | [console.md](console.md) |
|
||||
| `file` | `upload` | [file.md](file.md) |
|
||||
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
|
||||
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
|
||||
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
|
||||
| `model` | `list` | [model.md](model.md) |
|
||||
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
|
||||
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
|
||||
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `coding-plan`, `free`, `freetier`, `stats`, `summary`, `token-plan` | [usage.md](usage.md) |
|
||||
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
|
||||
|
||||
## Global flags
|
||||
|
||||
|
||||
@@ -7,21 +7,22 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------- |
|
||||
| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
|
||||
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
|
||||
| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) |
|
||||
| Command | Authentication | Description |
|
||||
| ----------------------- | -------------- | ------------------------------------------------------------------------- |
|
||||
| `bl knowledge chat` | API Key | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
|
||||
| `bl knowledge retrieve` | API Key | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
|
||||
| `bl knowledge search` | API Key | Search a Bailian knowledge base (RAG semantic retrieval) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl knowledge chat`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| **Name** | `knowledge chat` |
|
||||
| **Description** | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
|
||||
| **Usage** | `bl knowledge chat --message <text> --agent-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| **Name** | `knowledge chat` |
|
||||
| **Description** | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge chat --message <text> --agent-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -57,11 +58,12 @@ bl knowledge chat --message "Describe these images" --image https://example.com/
|
||||
|
||||
### `bl knowledge retrieve`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge retrieve` |
|
||||
| **Description** | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
|
||||
| **Usage** | `bl knowledge retrieve --index-id <id> --query <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------- |
|
||||
| **Name** | `knowledge retrieve` |
|
||||
| **Description** | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge retrieve --index-id <id> --query <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -92,11 +94,12 @@ bl knowledge retrieve --index-id idx_xxx --query "RAG retrieval" --rerank --rera
|
||||
|
||||
### `bl knowledge search`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| **Name** | `knowledge search` |
|
||||
| **Description** | Search a Bailian knowledge base (RAG semantic retrieval) |
|
||||
| **Usage** | `bl knowledge search --query <text> --agent-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| **Name** | `knowledge search` |
|
||||
| **Description** | Search a Bailian knowledge base (RAG semantic retrieval) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl knowledge search --query <text> --agent-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,33 +7,34 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| `bl mcp call` | Call a tool on an MCP server (tools/call) |
|
||||
| `bl mcp list` | List MCP servers activated under your Bailian account |
|
||||
| `bl mcp tools` | List tools exposed by an MCP server (tools/list) |
|
||||
| Command | Authentication | Description |
|
||||
| -------------- | -------------- | ----------------------------------------------------- |
|
||||
| `bl mcp call` | API Key | Call a tool on an MCP server (tools/call) |
|
||||
| `bl mcp list` | Console | List MCP servers activated under your Bailian account |
|
||||
| `bl mcp tools` | API Key | List tools exposed by an MCP server (tools/list) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl mcp call`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------- |
|
||||
| **Name** | `mcp call` |
|
||||
| **Description** | Call a tool on an MCP server (tools/call) |
|
||||
| **Usage** | `bl mcp call --target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------- |
|
||||
| **Name** | `mcp call` |
|
||||
| **Description** | Call a tool on an MCP server (tools/call) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl mcp call --target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------- |
|
||||
| `--target <server.tool>` | string | yes | Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection |
|
||||
| `--arg <kv>` | array | no | Tool argument (repeatable). Values parsed as JSON if possible, else string. |
|
||||
| `--json <obj>` | string | no | Full arguments object as JSON; merged with --arg (arg wins). |
|
||||
| `--query <text>` | string | no | Shortcut for --arg query=<text> (mirrors many DashScope MCP tools). |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `--target <server.tool>` | string | yes | Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection |
|
||||
| `--arg <kv>` | array | no | Tool argument (repeatable). Values parsed as JSON if possible, else string. |
|
||||
| `--json <obj>` | string | no | Full arguments object as JSON; merged with --arg (arg wins). |
|
||||
| `--query <text>` | string | no | Shortcut for --arg query=<text> (mirrors many DashScope MCP tools). |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (non-Bailian). Tries Streamable HTTP first, then classic SSE on the same URL. |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -51,11 +52,12 @@ bl mcp call --target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3
|
||||
|
||||
### `bl mcp list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| **Name** | `mcp list` |
|
||||
| **Description** | List MCP servers activated under your Bailian account |
|
||||
| **Usage** | `bl mcp list [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| **Name** | `mcp list` |
|
||||
| **Description** | List MCP servers activated under your Bailian account |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl mcp list [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,20 +88,21 @@ bl mcp list --output json
|
||||
|
||||
### `bl mcp tools`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `mcp tools` |
|
||||
| **Description** | List tools exposed by an MCP server (tools/list) |
|
||||
| **Usage** | `bl mcp tools --server <code> [--url <url>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| **Name** | `mcp tools` |
|
||||
| **Description** | List tools exposed by an MCP server (tools/list) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl mcp tools --server <code> [--url <url>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ------------------------------------------------------- |
|
||||
| `--server <code>` | string | yes | Server code from `mcp list` (e.g. market-cmapi00073529) |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `--server <code>` | string | yes | Server code from `mcp list` (e.g. market-cmapi00073529) |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (non-Bailian). Tries Streamable HTTP first, then classic SSE on the same URL. |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
@@ -7,25 +7,26 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| -------------------------- | ------------------------------------------------- |
|
||||
| `bl memory add` | Add memory from messages or custom content |
|
||||
| `bl memory delete` | Delete a memory node |
|
||||
| `bl memory list` | List memory nodes for a user |
|
||||
| `bl memory profile create` | Create a user profile schema for memory profiling |
|
||||
| `bl memory profile get` | Get user profile by schema ID and user ID |
|
||||
| `bl memory search` | Search memory nodes by query or messages |
|
||||
| `bl memory update` | Update a memory node content |
|
||||
| Command | Authentication | Description |
|
||||
| -------------------------- | -------------- | ------------------------------------------------- |
|
||||
| `bl memory add` | API Key | Add memory from messages or custom content |
|
||||
| `bl memory delete` | API Key | Delete a memory node |
|
||||
| `bl memory list` | API Key | List memory nodes for a user |
|
||||
| `bl memory profile create` | API Key | Create a user profile schema for memory profiling |
|
||||
| `bl memory profile get` | API Key | Get user profile by schema ID and user ID |
|
||||
| `bl memory search` | API Key | Search memory nodes by query or messages |
|
||||
| `bl memory update` | API Key | Update a memory node content |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl memory add`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------- |
|
||||
| **Name** | `memory add` |
|
||||
| **Description** | Add memory from messages or custom content |
|
||||
| **Usage** | `bl memory add --user-id <id> [--messages <json>] [--content <text>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------- |
|
||||
| **Name** | `memory add` |
|
||||
| **Description** | Add memory from messages or custom content |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory add --user-id <id> [--messages <json>] [--content <text>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -55,11 +56,12 @@ bl memory add --user-id user1 --content "Lives in Beijing" --profile-schema sche
|
||||
|
||||
### `bl memory delete`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `memory delete` |
|
||||
| **Description** | Delete a memory node |
|
||||
| **Usage** | `bl memory delete --node-id <id> --user-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| **Name** | `memory delete` |
|
||||
| **Description** | Delete a memory node |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory delete --node-id <id> --user-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -79,11 +81,12 @@ bl memory delete --node-id node_xxx --user-id user1
|
||||
|
||||
### `bl memory list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------- |
|
||||
| **Name** | `memory list` |
|
||||
| **Description** | List memory nodes for a user |
|
||||
| **Usage** | `bl memory list --user-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------- |
|
||||
| **Name** | `memory list` |
|
||||
| **Description** | List memory nodes for a user |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory list --user-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -108,11 +111,12 @@ bl memory list --user-id user1 --page-size 20 --page 2
|
||||
|
||||
### `bl memory profile create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------- |
|
||||
| **Name** | `memory profile create` |
|
||||
| **Description** | Create a user profile schema for memory profiling |
|
||||
| **Usage** | `bl memory profile create --name <name> --attributes <json> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `memory profile create` |
|
||||
| **Description** | Create a user profile schema for memory profiling |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory profile create --name <name> --attributes <json> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -132,11 +136,12 @@ bl memory profile create --name "user_basic" --attributes '[{"name":"age","descr
|
||||
|
||||
### `bl memory profile get`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------- |
|
||||
| **Name** | `memory profile get` |
|
||||
| **Description** | Get user profile by schema ID and user ID |
|
||||
| **Usage** | `bl memory profile get --schema-id <id> --user-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| **Name** | `memory profile get` |
|
||||
| **Description** | Get user profile by schema ID and user ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory profile get --schema-id <id> --user-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -155,11 +160,12 @@ bl memory profile get --schema-id schema_xxx --user-id user1
|
||||
|
||||
### `bl memory search`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------- |
|
||||
| **Name** | `memory search` |
|
||||
| **Description** | Search memory nodes by query or messages |
|
||||
| **Usage** | `bl memory search --user-id <id> [--query <text>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `memory search` |
|
||||
| **Description** | Search memory nodes by query or messages |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory search --user-id <id> [--query <text>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -185,11 +191,12 @@ bl memory search --user-id user1 --messages '[{"role":"user","content":"recommen
|
||||
|
||||
### `bl memory update`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------- |
|
||||
| **Name** | `memory update` |
|
||||
| **Description** | Update a memory node content |
|
||||
| **Usage** | `bl memory update --node-id <id> --user-id <id> --content <text>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| **Name** | `memory update` |
|
||||
| **Description** | Update a memory node content |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl memory update --node-id <id> --user-id <id> --content <text>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| --------------- | ---------------------------------------------------------------------------------- |
|
||||
| `bl model list` | Browse model families or show detailed model info in the Bailian model marketplace |
|
||||
| Command | Authentication | Description |
|
||||
| --------------- | -------------- | ---------------------------------------------------------------------------------- |
|
||||
| `bl model list` | Console | Browse model families or show detailed model info in the Bailian model marketplace |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl model list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `model list` |
|
||||
| **Description** | Browse model families or show detailed model info in the Bailian model marketplace |
|
||||
| **Usage** | `bl model list [--model <model>] [--page <n>] [--page-size <n>] [--provider <p>] [--capability <c>] [--feature <f>] [--enrich]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `model list` |
|
||||
| **Description** | Browse model families or show detailed model info in the Bailian model marketplace |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl model list [--model <model>] [--page <n>] [--page-size <n>] [--provider <p>] [--capability <c>] [--feature <f>] [--enrich]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,20 +7,21 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ------------------------------------------------ |
|
||||
| `bl pipeline run` | Run a pipeline workflow definition |
|
||||
| `bl pipeline validate` | Validate a pipeline definition without executing |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------------- | -------------- | ------------------------------------------------ |
|
||||
| `bl pipeline run` | No Auth | Run a pipeline workflow definition |
|
||||
| `bl pipeline validate` | No Auth | Validate a pipeline definition without executing |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl pipeline run`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------- |
|
||||
| **Name** | `pipeline run` |
|
||||
| **Description** | Run a pipeline workflow definition |
|
||||
| **Usage** | `bl pipeline run --file <path> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------- |
|
||||
| **Name** | `pipeline run` |
|
||||
| **Description** | Run a pipeline workflow definition |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl pipeline run --file <path> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -57,11 +58,12 @@ bl pipeline run --file workflow.yaml --output json
|
||||
|
||||
### `bl pipeline validate`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `pipeline validate` |
|
||||
| **Description** | Validate a pipeline definition without executing |
|
||||
| **Usage** | `bl pipeline validate --file <path>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| **Name** | `pipeline validate` |
|
||||
| **Description** | Validate a pipeline definition without executing |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl pipeline validate --file <path>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,22 +7,23 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | ------------------------------------------------------ |
|
||||
| `bl plugin install` | Install or upgrade an allowlisted Command Pack |
|
||||
| `bl plugin link` | Link an allowlisted local Command Pack for development |
|
||||
| `bl plugin list` | List installed Command Packs and their load status |
|
||||
| `bl plugin remove` | Remove an installed Command Pack |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------- | -------------- | ------------------------------------------------------ |
|
||||
| `bl plugin install` | No Auth | Install or upgrade an allowlisted Command Pack |
|
||||
| `bl plugin link` | No Auth | Link an allowlisted local Command Pack for development |
|
||||
| `bl plugin list` | No Auth | List installed Command Packs and their load status |
|
||||
| `bl plugin remove` | No Auth | Remove an installed Command Pack |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl plugin install`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------- |
|
||||
| **Name** | `plugin install` |
|
||||
| **Description** | Install or upgrade an allowlisted Command Pack |
|
||||
| **Usage** | `bl plugin install --package <name[@version]>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| **Name** | `plugin install` |
|
||||
| **Description** | Install or upgrade an allowlisted Command Pack |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl plugin install --package <name[@version]>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -42,11 +43,12 @@ bl plugin install --package @ali/bailian-plugin-agent@beta
|
||||
|
||||
### `bl plugin link`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------ |
|
||||
| **Name** | `plugin link` |
|
||||
| **Description** | Link an allowlisted local Command Pack for development |
|
||||
| **Usage** | `bl plugin link --path <directory>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------ |
|
||||
| **Name** | `plugin link` |
|
||||
| **Description** | Link an allowlisted local Command Pack for development |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl plugin link --path <directory>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -62,11 +64,12 @@ bl plugin link --path ../bailian-plugin-agent
|
||||
|
||||
### `bl plugin list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------- |
|
||||
| **Name** | `plugin list` |
|
||||
| **Description** | List installed Command Packs and their load status |
|
||||
| **Usage** | `bl plugin list` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------- |
|
||||
| **Name** | `plugin list` |
|
||||
| **Description** | List installed Command Packs and their load status |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl plugin list` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -84,11 +87,12 @@ bl plugin list --output json
|
||||
|
||||
### `bl plugin remove`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------- |
|
||||
| **Name** | `plugin remove` |
|
||||
| **Description** | Remove an installed Command Pack |
|
||||
| **Usage** | `bl plugin remove --name <package>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------- |
|
||||
| **Name** | `plugin remove` |
|
||||
| **Description** | Remove an installed Command Pack |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl plugin remove --name <package>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,22 +7,23 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------------- |
|
||||
| `bl quota check` | Check current usage against rate limits |
|
||||
| `bl quota history` | View quota change history |
|
||||
| `bl quota list` | View model RPM/TPM rate limits |
|
||||
| `bl quota request` | Request a temporary quota increase |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------ | -------------- | --------------------------------------- |
|
||||
| `bl quota check` | Console | Check current usage against rate limits |
|
||||
| `bl quota history` | Console | View quota change history |
|
||||
| `bl quota list` | Console | View model RPM/TPM rate limits |
|
||||
| `bl quota request` | Console | Request a temporary quota increase |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl quota check`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| **Name** | `quota check` |
|
||||
| **Description** | Check current usage against rate limits |
|
||||
| **Usage** | `bl quota check [--model <model>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| **Name** | `quota check` |
|
||||
| **Description** | Check current usage against rate limits |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl quota check [--model <model>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -59,11 +60,12 @@ bl quota check --output json
|
||||
|
||||
### `bl quota history`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------- |
|
||||
| **Name** | `quota history` |
|
||||
| **Description** | View quota change history |
|
||||
| **Usage** | `bl quota history [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------- |
|
||||
| **Name** | `quota history` |
|
||||
| **Description** | View quota change history |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl quota history [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -101,11 +103,12 @@ bl quota history --output json
|
||||
|
||||
### `bl quota list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------- |
|
||||
| **Name** | `quota list` |
|
||||
| **Description** | View model RPM/TPM rate limits |
|
||||
| **Usage** | `bl quota list [--model <model>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------- |
|
||||
| **Name** | `quota list` |
|
||||
| **Description** | View model RPM/TPM rate limits |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl quota list [--model <model>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -137,11 +140,12 @@ bl quota list --output json
|
||||
|
||||
### `bl quota request`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| **Name** | `quota request` |
|
||||
| **Description** | Request a temporary quota increase |
|
||||
| **Usage** | `bl quota request --model <model> --tpm <value> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------- |
|
||||
| **Name** | `quota request` |
|
||||
| **Description** | Request a temporary quota increase |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl quota request --model <model> --tpm <value> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| `bl search web` | Search the web using DashScope MCP WebSearch service |
|
||||
| Command | Authentication | Description |
|
||||
| --------------- | -------------- | ---------------------------------------------------- |
|
||||
| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl search web`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| **Name** | `search web` |
|
||||
| **Description** | Search the web using DashScope MCP WebSearch service |
|
||||
| **Usage** | `bl search web --query <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| **Name** | `search web` |
|
||||
| **Description** | Search the web using DashScope MCP WebSearch service |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl search web --query <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,23 +7,24 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ----------------- | ----------------------------------------------------------------------- |
|
||||
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
|
||||
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) |
|
||||
| `bl skill list` | List registry skills and diff against local installs |
|
||||
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
|
||||
| `bl skill update` | Update installed skills to the latest registry versions |
|
||||
| Command | Authentication | Description |
|
||||
| ----------------- | -------------- | ----------------------------------------------------------------------- |
|
||||
| `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents |
|
||||
| `bl skill init` | No Auth | Install all bailian-\* skills (one-shot bootstrap for new environments) |
|
||||
| `bl skill list` | No Auth | List registry skills and diff against local installs |
|
||||
| `bl skill remove` | No Auth | Remove locally installed skills (registry is untouched) |
|
||||
| `bl skill update` | No Auth | Update installed skills to the latest registry versions |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl skill add`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------- |
|
||||
| **Name** | `skill add` |
|
||||
| **Description** | Install skills from the Bailian skill registry into local agents |
|
||||
| **Usage** | `bl skill add --all \| --name <name,...>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------- |
|
||||
| **Name** | `skill add` |
|
||||
| **Description** | Install skills from the Bailian skill registry into local agents |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl skill add --all \| --name <name,...>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -44,11 +45,12 @@ bl skill add --name spark-video,bailian-model-recommend
|
||||
|
||||
### `bl skill init`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------- |
|
||||
| **Name** | `skill init` |
|
||||
| **Description** | Install all bailian-\* skills (one-shot bootstrap for new environments) |
|
||||
| **Usage** | `bl skill init` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------- |
|
||||
| **Name** | `skill init` |
|
||||
| **Description** | Install all bailian-\* skills (one-shot bootstrap for new environments) |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl skill init` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -67,11 +69,12 @@ bl skill init
|
||||
|
||||
### `bl skill list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| **Name** | `skill list` |
|
||||
| **Description** | List registry skills and diff against local installs |
|
||||
| **Usage** | `bl skill list` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| **Name** | `skill list` |
|
||||
| **Description** | List registry skills and diff against local installs |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl skill list` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -93,11 +96,12 @@ bl skill list --output json
|
||||
|
||||
### `bl skill remove`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------- |
|
||||
| **Name** | `skill remove` |
|
||||
| **Description** | Remove locally installed skills (registry is untouched) |
|
||||
| **Usage** | `bl skill remove --name <all\|name,...>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| **Name** | `skill remove` |
|
||||
| **Description** | Remove locally installed skills (registry is untouched) |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl skill remove --name <all\|name,...>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -117,11 +121,12 @@ bl skill remove --name all
|
||||
|
||||
### `bl skill update`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------- |
|
||||
| **Name** | `skill update` |
|
||||
| **Description** | Update installed skills to the latest registry versions |
|
||||
| **Usage** | `bl skill update [--all] [--name <name,...>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| **Name** | `skill update` |
|
||||
| **Description** | Update installed skills to the latest registry versions |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl skill update [--all] [--name <name,...>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| `bl text chat` | Send a chat completion (OpenAI compatible, DashScope) |
|
||||
| Command | Authentication | Description |
|
||||
| -------------- | -------------- | ----------------------------------------------------- |
|
||||
| `bl text chat` | API Key | Send a chat completion (OpenAI compatible, DashScope) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl text chat`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| **Name** | `text chat` |
|
||||
| **Description** | Send a chat completion (OpenAI compatible, DashScope) |
|
||||
| **Usage** | `bl text chat --message <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| **Name** | `text chat` |
|
||||
| **Description** | Send a chat completion (OpenAI compatible, DashScope) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl text chat --message <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,22 +7,23 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ----------------------------------------- |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------------------- | -------------- | ----------------------------------------- |
|
||||
| `bl token-plan add-member` | AK/SK | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | AK/SK | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | AK/SK | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | AK/SK | List Token Plan subscription seat details |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl token-plan add-member`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan add-member` |
|
||||
| **Description** | Add a member to a Token Plan organization |
|
||||
| **Usage** | `bl token-plan add-member --account-name <name> --org-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan add-member` |
|
||||
| **Description** | Add a member to a Token Plan organization |
|
||||
| **Authentication** | AK/SK |
|
||||
| **Usage** | `bl token-plan add-member --account-name <name> --org-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -54,11 +55,12 @@ bl token-plan add-member --account-name member1 --org-id org_123 --spec-type sta
|
||||
|
||||
### `bl token-plan assign-seats`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan assign-seats` |
|
||||
| **Description** | Batch assign Token Plan seats to members |
|
||||
| **Usage** | `bl token-plan assign-seats --workspace-id <id> --seat-type <type> --account-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan assign-seats` |
|
||||
| **Description** | Batch assign Token Plan seats to members |
|
||||
| **Authentication** | AK/SK |
|
||||
| **Usage** | `bl token-plan assign-seats --workspace-id <id> --seat-type <type> --account-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,11 +88,12 @@ bl token-plan assign-seats --workspace-id ws_456 --seat-type pro --account-id ac
|
||||
|
||||
### `bl token-plan create-key`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------ |
|
||||
| **Name** | `token-plan create-key` |
|
||||
| **Description** | Create a Token Plan API key for a seat |
|
||||
| **Usage** | `bl token-plan create-key --account-id <id> --workspace-id <id> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------ |
|
||||
| **Name** | `token-plan create-key` |
|
||||
| **Description** | Create a Token Plan API key for a seat |
|
||||
| **Authentication** | AK/SK |
|
||||
| **Usage** | `bl token-plan create-key --account-id <id> --workspace-id <id> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -117,11 +120,12 @@ bl token-plan create-key --account-id acc_123 --workspace-id ws_456 --descriptio
|
||||
|
||||
### `bl token-plan list-seats`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------- |
|
||||
| **Name** | `token-plan list-seats` |
|
||||
| **Description** | List Token Plan subscription seat details |
|
||||
| **Usage** | `bl token-plan list-seats [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------- |
|
||||
| **Name** | `token-plan list-seats` |
|
||||
| **Description** | List Token Plan subscription seat details |
|
||||
| **Authentication** | AK/SK |
|
||||
| **Usage** | `bl token-plan list-seats [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ----------- | --------------------------------------------------- |
|
||||
| `bl update` | Update the CLI to the latest or a specified version |
|
||||
| Command | Authentication | Description |
|
||||
| ----------- | -------------- | --------------------------------------------------- |
|
||||
| `bl update` | No Auth | Update the CLI to the latest or a specified version |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl update`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| **Name** | `update` |
|
||||
| **Description** | Update the CLI to the latest or a specified version |
|
||||
| **Usage** | `bl update [--to <version>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| **Name** | `update` |
|
||||
| **Description** | Update the CLI to the latest or a specified version |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl update [--to <version>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,22 +7,53 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) |
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
|
||||
| `bl usage stats` | Query model usage statistics |
|
||||
| `bl usage summary` | Show a unified usage summary: free-tier quota and recent usage overview |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `bl usage coding-plan` | Console | Show Coding Plan quota usage |
|
||||
| `bl usage free` | Console | Query free-tier quota for models (all models if --model is omitted) |
|
||||
| `bl usage freetier` | Console | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
|
||||
| `bl usage stats` | Console | Query model usage statistics |
|
||||
| `bl usage summary` | Console | Show a unified usage summary: free-tier quota and recent usage overview |
|
||||
| `bl usage token-plan` | Console | Show Token Plan quota usage |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl usage coding-plan`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------ |
|
||||
| **Name** | `usage coding-plan` |
|
||||
| **Description** | Show Coding Plan quota usage |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage coding-plan [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl usage coding-plan
|
||||
```
|
||||
|
||||
```bash
|
||||
bl usage coding-plan --output json
|
||||
```
|
||||
|
||||
### `bl usage free`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------- |
|
||||
| **Name** | `usage free` |
|
||||
| **Description** | Query free-tier quota for models (all models if --model is omitted) |
|
||||
| **Usage** | `bl usage free [--model <model>[,model2,...]] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------- |
|
||||
| **Name** | `usage free` |
|
||||
| **Description** | Query free-tier quota for models (all models if --model is omitted) |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage free [--model <model>[,model2,...]] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -73,11 +104,12 @@ bl usage free --model qwen3-max --console-region cn-beijing
|
||||
|
||||
### `bl usage freetier`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `usage freetier` |
|
||||
| **Description** | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
|
||||
| **Usage** | `bl usage freetier <--model <model>[,model2,...] \| --all> [--off] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `usage freetier` |
|
||||
| **Description** | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage freetier <--model <model>[,model2,...] \| --all> [--off] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -120,11 +152,12 @@ bl usage freetier --off --all
|
||||
|
||||
### `bl usage stats`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------- |
|
||||
| **Name** | `usage stats` |
|
||||
| **Description** | Query model usage statistics |
|
||||
| **Usage** | `bl usage stats [--model <model>] [--days <days>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `usage stats` |
|
||||
| **Description** | Query model usage statistics |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage stats [--model <model>] [--days <days>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -170,11 +203,12 @@ bl usage stats --output json
|
||||
|
||||
### `bl usage summary`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------- |
|
||||
| **Name** | `usage summary` |
|
||||
| **Description** | Show a unified usage summary: free-tier quota and recent usage overview |
|
||||
| **Usage** | `bl usage summary [--days <days>] [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------- |
|
||||
| **Name** | `usage summary` |
|
||||
| **Description** | Show a unified usage summary: free-tier quota and recent usage overview |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage summary [--days <days>] [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -199,3 +233,31 @@ bl usage summary --days 30
|
||||
```bash
|
||||
bl usage summary --output json
|
||||
```
|
||||
|
||||
### `bl usage token-plan`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------- |
|
||||
| **Name** | `usage token-plan` |
|
||||
| **Description** | Show Token Plan quota usage |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl usage token-plan [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl usage token-plan
|
||||
```
|
||||
|
||||
```bash
|
||||
bl usage token-plan --output json
|
||||
```
|
||||
|
||||
@@ -7,20 +7,21 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | ----------------------------------------------------------- |
|
||||
| `bl workspace init` | Initialize Bailian workspace and activate postpaid services |
|
||||
| `bl workspace list` | List all workspaces |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------- | -------------- | ----------------------------------------------------------- |
|
||||
| `bl workspace init` | No Auth | Initialize Bailian workspace and activate postpaid services |
|
||||
| `bl workspace list` | Console | List all workspaces |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl workspace init`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `workspace init` |
|
||||
| **Description** | Initialize Bailian workspace and activate postpaid services |
|
||||
| **Usage** | `bl workspace init --access-key-id <id> --access-key-secret <secret> [--security-token <token>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `workspace init` |
|
||||
| **Description** | Initialize Bailian workspace and activate postpaid services |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl workspace init --access-key-id <id> --access-key-secret <secret> [--security-token <token>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -38,11 +39,12 @@ bl workspace init --access-key-id LTAIxxxxx --access-key-secret xxxxx
|
||||
|
||||
### `bl workspace list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------- |
|
||||
| **Name** | `workspace list` |
|
||||
| **Description** | List all workspaces |
|
||||
| **Usage** | `bl workspace list [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | --------------------------- |
|
||||
| **Name** | `workspace list` |
|
||||
| **Description** | List all workspaces |
|
||||
| **Authentication** | Console |
|
||||
| **Usage** | `bl workspace list [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.14.2"
|
||||
version: "1.14.3"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -7,23 +7,24 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| --------------------- | ------------------------------------------------------------------ |
|
||||
| `bl dataset delete` | Delete a dataset file by ID |
|
||||
| `bl dataset get` | Get details of a single dataset file |
|
||||
| `bl dataset list` | List uploaded dataset files |
|
||||
| `bl dataset upload` | Upload a dataset file (.jsonl or .zip) to Bailian |
|
||||
| `bl dataset validate` | Locally validate a dataset file (.jsonl or .zip) without uploading |
|
||||
| Command | Authentication | Description |
|
||||
| --------------------- | -------------- | ------------------------------------------------------------------ |
|
||||
| `bl dataset delete` | API Key | Delete a dataset file by ID |
|
||||
| `bl dataset get` | API Key | Get details of a single dataset file |
|
||||
| `bl dataset list` | API Key | List uploaded dataset files |
|
||||
| `bl dataset upload` | API Key | Upload a dataset file (.jsonl or .zip) to Bailian |
|
||||
| `bl dataset validate` | No Auth | Locally validate a dataset file (.jsonl or .zip) without uploading |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl dataset delete`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------- |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Usage** | `bl dataset delete --file-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -45,11 +46,12 @@ bl dataset delete --file-id file-id-xxx --dry-run
|
||||
|
||||
### `bl dataset get`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------ |
|
||||
| **Name** | `dataset get` |
|
||||
| **Description** | Get details of a single dataset file |
|
||||
| **Usage** | `bl dataset get --file-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------ |
|
||||
| **Name** | `dataset get` |
|
||||
| **Description** | Get details of a single dataset file |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset get --file-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -71,11 +73,12 @@ bl dataset get --file-id file-xxx --output json
|
||||
|
||||
### `bl dataset list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------- |
|
||||
| **Name** | `dataset list` |
|
||||
| **Description** | List uploaded dataset files |
|
||||
| **Usage** | `bl dataset list [--page <n>] [--page-size <n>] [--purpose <name>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------- |
|
||||
| **Name** | `dataset list` |
|
||||
| **Description** | List uploaded dataset files |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset list [--page <n>] [--page-size <n>] [--purpose <name>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -107,11 +110,12 @@ bl dataset list --output json
|
||||
|
||||
### `bl dataset upload`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `dataset upload` |
|
||||
| **Description** | Upload a dataset file (.jsonl or .zip) to Bailian |
|
||||
| **Usage** | `bl dataset upload --file <path> [--purpose <name>] [--schema <chatml\|dpo\|cpt\|tts\|image>] [--no-validate] [--full-validate]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `dataset upload` |
|
||||
| **Description** | Upload a dataset file (.jsonl or .zip) to Bailian |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset upload --file <path> [--purpose <name>] [--schema <chatml\|dpo\|cpt\|tts\|image>] [--no-validate] [--full-validate]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -170,11 +174,12 @@ bl dataset upload --file train.jsonl --no-validate
|
||||
|
||||
### `bl dataset validate`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `dataset validate` |
|
||||
| **Description** | Locally validate a dataset file (.jsonl or .zip) without uploading |
|
||||
| **Usage** | `bl dataset validate --file <path> [--full-validate] [--schema <chatml\|dpo\|cpt\|tts\|image>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `dataset validate` |
|
||||
| **Description** | Locally validate a dataset file (.jsonl or .zip) without uploading |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl dataset validate --file <path> [--full-validate] [--schema <chatml\|dpo\|cpt\|tts\|image>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,27 +7,28 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------ | --------------------------------------------------------- |
|
||||
| `bl deploy audio create` | Create an audio (TTS) model deployment |
|
||||
| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| `bl deploy get` | Get details of a single model deployment |
|
||||
| `bl deploy image create` | Create an image generation model deployment |
|
||||
| `bl deploy list` | List model deployments |
|
||||
| `bl deploy models` | List models available for deployment |
|
||||
| `bl deploy scale` | Scale a deployment's capacity |
|
||||
| `bl deploy text create` | Create a text model deployment |
|
||||
| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------------ | -------------- | --------------------------------------------------------- |
|
||||
| `bl deploy audio create` | API Key | Create an audio (TTS) model deployment |
|
||||
| `bl deploy delete` | API Key | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| `bl deploy get` | API Key | Get details of a single model deployment |
|
||||
| `bl deploy image create` | API Key | Create an image generation model deployment |
|
||||
| `bl deploy list` | API Key | List model deployments |
|
||||
| `bl deploy models` | API Key | List models available for deployment |
|
||||
| `bl deploy scale` | API Key | Scale a deployment's capacity |
|
||||
| `bl deploy text create` | API Key | Create a text model deployment |
|
||||
| `bl deploy update` | API Key | Update a deployment's rate limits (rpm_limit / tpm_limit) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl deploy audio create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy audio create` |
|
||||
| **Description** | Create an audio (TTS) model deployment |
|
||||
| **Usage** | `bl deploy audio create --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>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy audio create` |
|
||||
| **Description** | Create an audio (TTS) model deployment |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy audio create --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>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -83,11 +84,12 @@ bl deploy audio create --model my-cosyvoice-ft --name my-tts --dry-run
|
||||
|
||||
### `bl deploy delete`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------- |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -110,11 +112,12 @@ bl deploy delete --deployed-model dep-... --dry-run
|
||||
|
||||
### `bl deploy get`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------- |
|
||||
| **Name** | `deploy get` |
|
||||
| **Description** | Get details of a single model deployment |
|
||||
| **Usage** | `bl deploy get --deployed-model <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------- |
|
||||
| **Name** | `deploy get` |
|
||||
| **Description** | Get details of a single model deployment |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy get --deployed-model <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -136,11 +139,12 @@ bl deploy get --deployed-model qwen-plus-2025-12-01-b6d61c71 --output json
|
||||
|
||||
### `bl deploy image create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy image create` |
|
||||
| **Description** | Create an image generation model deployment |
|
||||
| **Usage** | `bl deploy image create --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>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy image create` |
|
||||
| **Description** | Create an image generation model deployment |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy image create --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>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -196,11 +200,12 @@ bl deploy image create --model my-wan-ft --name my-wan --dry-run
|
||||
|
||||
### `bl deploy list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------- |
|
||||
| **Name** | `deploy list` |
|
||||
| **Description** | List model deployments |
|
||||
| **Usage** | `bl deploy list [--page <n>] [--page-size <n>] [--status <s>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------- |
|
||||
| **Name** | `deploy list` |
|
||||
| **Description** | List model deployments |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy list [--page <n>] [--page-size <n>] [--status <s>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -228,11 +233,12 @@ bl deploy list --page-size 20 --output json
|
||||
|
||||
### `bl deploy models`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy models` |
|
||||
| **Description** | List models available for deployment |
|
||||
| **Usage** | `bl deploy models [--page <n>] [--page-size <n>] [--catalog-version <v>] [--source <custom\|public>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy models` |
|
||||
| **Description** | List models available for deployment |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy models [--page <n>] [--page-size <n>] [--catalog-version <v>] [--source <custom\|public>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -265,11 +271,12 @@ bl deploy models --catalog-version v1.0 --output json
|
||||
|
||||
### `bl deploy scale`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy scale` |
|
||||
| **Description** | Scale a deployment's capacity |
|
||||
| **Usage** | `bl deploy scale --deployed-model <id> --capacity <n> [--input-tpm <n>] [--output-tpm <n>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy scale` |
|
||||
| **Description** | Scale a deployment's capacity |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy scale --deployed-model <id> --capacity <n> [--input-tpm <n>] [--output-tpm <n>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -294,11 +301,12 @@ bl deploy scale --deployed-model dep-... --capacity 2
|
||||
|
||||
### `bl deploy text create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy text create` |
|
||||
| **Description** | Create a text model deployment |
|
||||
| **Usage** | `bl deploy text create --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>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy text create` |
|
||||
| **Description** | Create a text model deployment |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy text create --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>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -358,11 +366,12 @@ bl deploy text create --model qwen3-8b --name my-qwen3 --plan mu --deploy-spec M
|
||||
|
||||
### `bl deploy update`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy update` |
|
||||
| **Description** | Update a deployment's rate limits (rpm_limit / tpm_limit) |
|
||||
| **Usage** | `bl deploy update --deployed-model <id> [--rpm-limit <n>] [--tpm-limit <n>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------- |
|
||||
| **Name** | `deploy update` |
|
||||
| **Description** | Update a deployment's rate limits (rpm_limit / tpm_limit) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy update --deployed-model <id> [--rpm-limit <n>] [--tpm-limit <n>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,30 +7,31 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bl finetune audio create` | Create an audio TTS model fine-tune job (sft-lora) |
|
||||
| `bl finetune cancel` | Cancel a running fine-tune job |
|
||||
| `bl finetune capability` | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) |
|
||||
| `bl finetune checkpoints` | List checkpoints produced by a fine-tune job |
|
||||
| `bl finetune delete` | Delete a fine-tune job record |
|
||||
| `bl finetune export` | Publish a checkpoint as a deployable model |
|
||||
| `bl finetune get` | Get details of a single fine-tune job |
|
||||
| `bl finetune image create` | Create an image generation model fine-tune job (sft-lora) |
|
||||
| `bl finetune list` | List fine-tune jobs |
|
||||
| `bl finetune logs` | Fetch training logs for a fine-tune job |
|
||||
| `bl finetune text create` | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
|
||||
| `bl finetune watch` | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. |
|
||||
| Command | Authentication | Description |
|
||||
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bl finetune audio create` | API Key | Create an audio TTS model fine-tune job (sft-lora) |
|
||||
| `bl finetune cancel` | API Key | Cancel a running fine-tune job |
|
||||
| `bl finetune capability` | No Auth | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) |
|
||||
| `bl finetune checkpoints` | API Key | List checkpoints produced by a fine-tune job |
|
||||
| `bl finetune delete` | API Key | Delete a fine-tune job record |
|
||||
| `bl finetune export` | API Key | Publish a checkpoint as a deployable model |
|
||||
| `bl finetune get` | API Key | Get details of a single fine-tune job |
|
||||
| `bl finetune image create` | API Key | Create an image generation model fine-tune job (sft-lora) |
|
||||
| `bl finetune list` | API Key | List fine-tune jobs |
|
||||
| `bl finetune logs` | API Key | Fetch training logs for a fine-tune job |
|
||||
| `bl finetune text create` | API Key | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
|
||||
| `bl finetune watch` | API Key | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl finetune audio create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune audio create` |
|
||||
| **Description** | Create an audio TTS model fine-tune job (sft-lora) |
|
||||
| **Usage** | `bl finetune audio create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune audio create` |
|
||||
| **Description** | Create an audio TTS model fine-tune job (sft-lora) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune audio create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -79,11 +80,12 @@ bl finetune audio create --model cosyvoice-v3-flash --datasets ./audio.zip --dry
|
||||
|
||||
### `bl finetune cancel`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------- |
|
||||
| **Name** | `finetune cancel` |
|
||||
| **Description** | Cancel a running fine-tune job |
|
||||
| **Usage** | `bl finetune cancel --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `finetune cancel` |
|
||||
| **Description** | Cancel a running fine-tune job |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune cancel --job-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -110,11 +112,12 @@ bl finetune cancel --job-id ft-xxx --dry-run
|
||||
|
||||
### `bl finetune capability`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune capability` |
|
||||
| **Description** | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) |
|
||||
| **Usage** | `bl finetune capability --model <m> \| --training-type <t>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune capability` |
|
||||
| **Description** | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl finetune capability --model <m> \| --training-type <t>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -150,11 +153,12 @@ bl finetune capability --training-type sft --quiet
|
||||
|
||||
### `bl finetune checkpoints`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| **Name** | `finetune checkpoints` |
|
||||
| **Description** | List checkpoints produced by a fine-tune job |
|
||||
| **Usage** | `bl finetune checkpoints --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------- |
|
||||
| **Name** | `finetune checkpoints` |
|
||||
| **Description** | List checkpoints produced by a fine-tune job |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune checkpoints --job-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -181,11 +185,12 @@ bl finetune checkpoints --job-id ft-xxx --output json
|
||||
|
||||
### `bl finetune delete`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------- |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Usage** | `bl finetune delete --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -212,11 +217,12 @@ bl finetune delete --job-id ft-xxx --dry-run
|
||||
|
||||
### `bl finetune export`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune export` |
|
||||
| **Description** | Publish a checkpoint as a deployable model |
|
||||
| **Usage** | `bl finetune export --job-id <id> --checkpoint <name> --model-name <name>` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune export` |
|
||||
| **Description** | Publish a checkpoint as a deployable model |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune export --job-id <id> --checkpoint <name> --model-name <name>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -242,11 +248,12 @@ bl finetune export --job-id ft-xxx --checkpoint ckpt-3 --model-name my-qwen-sft
|
||||
|
||||
### `bl finetune get`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------- |
|
||||
| **Name** | `finetune get` |
|
||||
| **Description** | Get details of a single fine-tune job |
|
||||
| **Usage** | `bl finetune get --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------- |
|
||||
| **Name** | `finetune get` |
|
||||
| **Description** | Get details of a single fine-tune job |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune get --job-id <id>` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -268,11 +275,12 @@ bl finetune get --job-id ft-xxx --output json
|
||||
|
||||
### `bl finetune image create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `finetune image create` |
|
||||
| **Description** | Create an image generation model fine-tune job (sft-lora) |
|
||||
| **Usage** | `bl finetune image create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i\|i2i>] [--learning-rate <str>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `finetune image create` |
|
||||
| **Description** | Create an image generation model fine-tune job (sft-lora) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune image create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i\|i2i>] [--learning-rate <str>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -329,11 +337,12 @@ bl finetune image create --model wan2.7-image-pro --datasets ./images.zip --dry-
|
||||
|
||||
### `bl finetune list`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------- |
|
||||
| **Name** | `finetune list` |
|
||||
| **Description** | List fine-tune jobs |
|
||||
| **Usage** | `bl finetune list [--page <n>] [--page-size <n>] [--status <s>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------- |
|
||||
| **Name** | `finetune list` |
|
||||
| **Description** | List fine-tune jobs |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune list [--page <n>] [--page-size <n>] [--status <s>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -361,11 +370,12 @@ bl finetune list --page-size 20 --output json
|
||||
|
||||
### `bl finetune logs`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune logs` |
|
||||
| **Description** | Fetch training logs for a fine-tune job |
|
||||
| **Usage** | `bl finetune logs --job-id <id> [--page <n>] [--page-size <n>] [--search <keyword>] [--tail <n>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune logs` |
|
||||
| **Description** | Fetch training logs for a fine-tune job |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune logs --job-id <id> [--page <n>] [--page-size <n>] [--search <keyword>] [--tail <n>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -407,11 +417,12 @@ bl finetune logs --job-id ft-xxx --search checkpoint --tail 5
|
||||
|
||||
### `bl finetune text create`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune text create` |
|
||||
| **Description** | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
|
||||
| **Usage** | `bl finetune text create --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>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune text create` |
|
||||
| **Description** | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune text create --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>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -486,11 +497,12 @@ bl finetune text create --model qwen3-8b --datasets file-xxx --dry-run
|
||||
|
||||
### `bl finetune watch`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune watch` |
|
||||
| **Description** | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. |
|
||||
| **Usage** | `bl finetune watch --job-id <id> [--follow] [--interval <sec>] [--poll-timeout <sec>]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `finetune watch` |
|
||||
| **Description** | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune watch --job-id <id> [--follow] [--interval <sec>] [--poll-timeout <sec>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -9,34 +9,34 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## Quick index
|
||||
|
||||
| Command | Description | Detail |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| `bl dataset delete` | Delete a dataset file by ID | [dataset.md](dataset.md) |
|
||||
| `bl dataset get` | Get details of a single dataset file | [dataset.md](dataset.md) |
|
||||
| `bl dataset list` | List uploaded dataset files | [dataset.md](dataset.md) |
|
||||
| `bl dataset upload` | Upload a dataset file (.jsonl or .zip) to Bailian | [dataset.md](dataset.md) |
|
||||
| `bl dataset validate` | Locally validate a dataset file (.jsonl or .zip) without uploading | [dataset.md](dataset.md) |
|
||||
| `bl deploy audio create` | Create an audio (TTS) model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) | [deploy.md](deploy.md) |
|
||||
| `bl deploy get` | Get details of a single model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy image create` | Create an image generation model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy list` | List model deployments | [deploy.md](deploy.md) |
|
||||
| `bl deploy models` | List models available for deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy scale` | Scale a deployment's capacity | [deploy.md](deploy.md) |
|
||||
| `bl deploy text create` | Create a text model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) | [deploy.md](deploy.md) |
|
||||
| `bl finetune audio create` | Create an audio TTS model fine-tune job (sft-lora) | [finetune.md](finetune.md) |
|
||||
| `bl finetune cancel` | Cancel a running fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune capability` | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) | [finetune.md](finetune.md) |
|
||||
| `bl finetune checkpoints` | List checkpoints produced by a fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune delete` | Delete a fine-tune job record | [finetune.md](finetune.md) |
|
||||
| `bl finetune export` | Publish a checkpoint as a deployable model | [finetune.md](finetune.md) |
|
||||
| `bl finetune get` | Get details of a single fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune image create` | Create an image generation model fine-tune job (sft-lora) | [finetune.md](finetune.md) |
|
||||
| `bl finetune list` | List fine-tune jobs | [finetune.md](finetune.md) |
|
||||
| `bl finetune logs` | Fetch training logs for a fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune text create` | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) | [finetune.md](finetune.md) |
|
||||
| `bl finetune watch` | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. | [finetune.md](finetune.md) |
|
||||
| Command | Authentication | Description | Detail |
|
||||
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| `bl dataset delete` | API Key | Delete a dataset file by ID | [dataset.md](dataset.md) |
|
||||
| `bl dataset get` | API Key | Get details of a single dataset file | [dataset.md](dataset.md) |
|
||||
| `bl dataset list` | API Key | List uploaded dataset files | [dataset.md](dataset.md) |
|
||||
| `bl dataset upload` | API Key | Upload a dataset file (.jsonl or .zip) to Bailian | [dataset.md](dataset.md) |
|
||||
| `bl dataset validate` | No Auth | Locally validate a dataset file (.jsonl or .zip) without uploading | [dataset.md](dataset.md) |
|
||||
| `bl deploy audio create` | API Key | Create an audio (TTS) model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy delete` | API Key | Delete a model deployment (must be STOPPED or FAILED) | [deploy.md](deploy.md) |
|
||||
| `bl deploy get` | API Key | Get details of a single model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy image create` | API Key | Create an image generation model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy list` | API Key | List model deployments | [deploy.md](deploy.md) |
|
||||
| `bl deploy models` | API Key | List models available for deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy scale` | API Key | Scale a deployment's capacity | [deploy.md](deploy.md) |
|
||||
| `bl deploy text create` | API Key | Create a text model deployment | [deploy.md](deploy.md) |
|
||||
| `bl deploy update` | API Key | Update a deployment's rate limits (rpm_limit / tpm_limit) | [deploy.md](deploy.md) |
|
||||
| `bl finetune audio create` | API Key | Create an audio TTS model fine-tune job (sft-lora) | [finetune.md](finetune.md) |
|
||||
| `bl finetune cancel` | API Key | Cancel a running fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune capability` | No Auth | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) | [finetune.md](finetune.md) |
|
||||
| `bl finetune checkpoints` | API Key | List checkpoints produced by a fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune delete` | API Key | Delete a fine-tune job record | [finetune.md](finetune.md) |
|
||||
| `bl finetune export` | API Key | Publish a checkpoint as a deployable model | [finetune.md](finetune.md) |
|
||||
| `bl finetune get` | API Key | Get details of a single fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune image create` | API Key | Create an image generation model fine-tune job (sft-lora) | [finetune.md](finetune.md) |
|
||||
| `bl finetune list` | API Key | List fine-tune jobs | [finetune.md](finetune.md) |
|
||||
| `bl finetune logs` | API Key | Fetch training logs for a fine-tune job | [finetune.md](finetune.md) |
|
||||
| `bl finetune text create` | API Key | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) | [finetune.md](finetune.md) |
|
||||
| `bl finetune watch` | API Key | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. | [finetune.md](finetune.md) |
|
||||
|
||||
## By group
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.14.2"
|
||||
version: "1.14.3"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -39,6 +39,8 @@ description: >-
|
||||
| A/V understanding (files the host can't play) | `bl omni --video` / `--audio` | `qwen3.5-omni-plus` |
|
||||
| Image/video describe (user names Bailian) | `bl vision describe` | `qwen-vl-max`; host-first for plain image Q&A |
|
||||
|
||||
For ASR model selection, keep `fun-asr` (or other `*-filetrans`) for long recordings, repeated files, speaker diarization, or asynchronous task IDs. For one local or remote audio file up to about five minutes when the user asks for low-latency Flash models, use `--model fun-asr-flash-2026-06-15`, `--model qwen-audio-3.0-asr-flash`, or `--model qwen3-asr-flash`. Flash recognition is synchronous and accepts exactly one file per call.
|
||||
|
||||
Flags, usage, and examples: see [`reference/`](reference/index.md) or `bl <command> --help` — do not guess flags.
|
||||
|
||||
## Local files (mandatory)
|
||||
|
||||
@@ -7,20 +7,21 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| `bl image edit` | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) |
|
||||
| `bl image generate` | Generate images (Qwen-Image / wan2.x) |
|
||||
| Command | Authentication | Description |
|
||||
| ------------------- | -------------- | -------------------------------------------------------------------- |
|
||||
| `bl image edit` | API Key | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) |
|
||||
| `bl image generate` | API Key | Generate images (Qwen-Image / wan2.x) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl image edit`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------- |
|
||||
| **Name** | `image edit` |
|
||||
| **Description** | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) |
|
||||
| **Usage** | `bl image edit --image <url> --prompt <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `image edit` |
|
||||
| **Description** | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl image edit --image <url> --prompt <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -80,11 +81,12 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
|
||||
|
||||
### `bl image generate`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------- |
|
||||
| **Name** | `image generate` |
|
||||
| **Description** | Generate images (Qwen-Image / wan2.x) |
|
||||
| **Usage** | `bl image generate --prompt <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| **Name** | `image generate` |
|
||||
| **Description** | Generate images (Qwen-Image / wan2.x) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl image generate --prompt <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -9,19 +9,19 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## Quick index
|
||||
|
||||
| Command | Description | Detail |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------- |
|
||||
| `bl image edit` | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) | [image.md](image.md) |
|
||||
| `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
|
||||
| `bl omni` | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
|
||||
| `bl speech recognize` | Recognize speech from audio files (FunAudio-ASR) | [speech.md](speech.md) |
|
||||
| `bl speech synthesize` | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) |
|
||||
| `bl video download` | Download a completed video by task ID | [video.md](video.md) |
|
||||
| `bl video edit` | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
|
||||
| `bl video generate` | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) | [video.md](video.md) |
|
||||
| `bl video ref` | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
|
||||
| `bl video task get` | Query async task status | [video.md](video.md) |
|
||||
| `bl vision describe` | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
|
||||
| Command | Authentication | Description | Detail |
|
||||
| ---------------------- | -------------- | ----------------------------------------------------------------------------------------------------- | ---------------------- |
|
||||
| `bl image edit` | API Key | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) | [image.md](image.md) |
|
||||
| `bl image generate` | API Key | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
|
||||
| `bl omni` | API Key | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
|
||||
| `bl speech recognize` | API Key | Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash) | [speech.md](speech.md) |
|
||||
| `bl speech synthesize` | API Key | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) |
|
||||
| `bl video download` | API Key | Download a completed video by task ID | [video.md](video.md) |
|
||||
| `bl video edit` | API Key | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
|
||||
| `bl video generate` | API Key | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) | [video.md](video.md) |
|
||||
| `bl video ref` | API Key | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
|
||||
| `bl video task get` | API Key | Query async task status | [video.md](video.md) |
|
||||
| `bl vision describe` | API Key | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
|
||||
|
||||
## By group
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| --------- | ---------------------------------------------------- |
|
||||
| `bl omni` | Multimodal chat with text + audio output (Qwen-Omni) |
|
||||
| Command | Authentication | Description |
|
||||
| --------- | -------------- | ---------------------------------------------------- |
|
||||
| `bl omni` | API Key | Multimodal chat with text + audio output (Qwen-Omni) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl omni`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| **Name** | `omni` |
|
||||
| **Description** | Multimodal chat with text + audio output (Qwen-Omni) |
|
||||
| **Usage** | `bl omni --message <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| **Name** | `omni` |
|
||||
| **Description** | Multimodal chat with text + audio output (Qwen-Omni) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl omni --message <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
@@ -7,37 +7,38 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ------------------------------------------------ |
|
||||
| `bl speech recognize` | Recognize speech from audio files (FunAudio-ASR) |
|
||||
| `bl speech synthesize` | Synthesize speech from text (CosyVoice TTS) |
|
||||
| Command | Authentication | Description |
|
||||
| ---------------------- | -------------- | ----------------------------------------------------------------- |
|
||||
| `bl speech recognize` | API Key | Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash) |
|
||||
| `bl speech synthesize` | API Key | Synthesize speech from text (CosyVoice TTS) |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl speech recognize`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `speech recognize` |
|
||||
| **Description** | Recognize speech from audio files (FunAudio-ASR) |
|
||||
| **Usage** | `bl speech recognize --url <audio-url> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| **Name** | `speech recognize` |
|
||||
| **Description** | Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl speech recognize --url <audio-url> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------- | ------ | -------- | ------------------------------------------------------- |
|
||||
| `--url <url>` | array | yes | Audio file URL or local file path (repeatable, max 100) |
|
||||
| `--model <model>` | string | no | Model ID (default: fun-asr) |
|
||||
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja) |
|
||||
| `--diarization` | switch | no | Enable automatic speaker diarization |
|
||||
| `--speaker-count <n>` | number | no | Expected number of speakers (requires --diarization) |
|
||||
| `--vocabulary-id <id>` | string | no | Hot-word vocabulary ID for improved accuracy |
|
||||
| `--channel-id <n>` | number | no | Audio channel ID (default: 0) |
|
||||
| `--out <path>` | string | no | Save full transcription result to JSON file |
|
||||
| `--async` | switch | no | Return async task id without waiting |
|
||||
| `--poll-interval <seconds>` | number | no | Polling interval in seconds (default: 2) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--url <url>` | array | yes | Audio file URL or local file path (repeatable, max 100) |
|
||||
| `--model <model>` | string | no | Model ID (default: fun-asr). Async: fun-asr / _-filetrans / paraformer-_; sync: qwen3-asr-flash* / fun-asr-flash* / qwen-audio-\*-asr-flash |
|
||||
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja). Classic async/input-audio: language_hints; qwen3-filetrans: language; qwen3 sync: asr_options.language |
|
||||
| `--diarization` | switch | no | Enable automatic speaker diarization |
|
||||
| `--speaker-count <n>` | number | no | Expected number of speakers (requires --diarization) |
|
||||
| `--vocabulary-id <id>` | string | no | Hot-word vocabulary ID for improved accuracy |
|
||||
| `--channel-id <n>` | number | no | Audio channel ID (default: 0) |
|
||||
| `--out <path>` | string | no | Save full transcription result to JSON file |
|
||||
| `--async` | switch | no | Return async task id without waiting |
|
||||
| `--poll-interval <seconds>` | number | no | Polling interval in seconds (default: 2) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -69,13 +70,18 @@ bl speech recognize --url https://example.com/audio.mp3 --out result.json
|
||||
bl speech recognize --url https://example.com/audio.mp3 --async --quiet
|
||||
```
|
||||
|
||||
```bash
|
||||
bl speech recognize --url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en
|
||||
```
|
||||
|
||||
### `bl speech synthesize`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------- |
|
||||
| **Name** | `speech synthesize` |
|
||||
| **Description** | Synthesize speech from text (CosyVoice TTS) |
|
||||
| **Usage** | `bl speech synthesize --text <text> [flags]` |
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------- |
|
||||
| **Name** | `speech synthesize` |
|
||||
| **Description** | Synthesize speech from text (CosyVoice TTS) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl speech synthesize --text <text> [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user