Compare commits

...

9 Commits

Author SHA1 Message Date
rendianmeng 575110e62d chore: sync packages/cli README with root for publish
Publish check requires root and packages/cli README files to match.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:39:24 +08:00
rendianmeng 882bc6becb feat(asset-center): remove oss transfer commands 2026-08-07 14:50:56 +08:00
rendianmeng 78ca730100 feat: asset center oss api test 2026-08-06 15:46:31 +08:00
rendianmeng 539247b10d Merge branch 'main' into feat/asset-center
Keep asset-center and managed-agent command exports, regenerate skill-scoped references.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 10:59:22 +08:00
rendianmeng 10ddd0a948 Merge branch 'main' into feat/asset-center
Resolve conflicts by keeping asset-center commands alongside main's plugin/workspace/config updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 13:33:15 +08:00
rendianmeng 7b5af2c205 feat: api testing 2026-07-20 09:53:34 +08:00
rendianmeng 2a1c96fb43 feat: 接口调试中 2026-07-10 16:27:28 +08:00
rendianmeng 788d3faafc Merge branch main of github.com:modelstudioai/cli into feat/asset-center 2026-07-10 14:10:20 +08:00
rendianmeng f32bd2323f feat: asset-center init 2026-07-10 14:07:42 +08:00
27 changed files with 4626 additions and 84 deletions
+9 -1
View File
@@ -41,6 +41,7 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
@@ -149,6 +150,13 @@ bl quota check # Current usage vs rate li
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
@@ -186,7 +194,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
+9 -1
View File
@@ -41,6 +41,7 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
@@ -147,6 +148,13 @@ bl quota check # 当前用量 vs 限流
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan 团队版管理(需 AK/SK见下方认证说明
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
@@ -184,7 +192,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history``asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
+574
View File
@@ -0,0 +1,574 @@
# 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. 技术栈
| 类别 | 选型 |
| --------- | -------------------------------------------------------------------------------------------- |
| 语言 | TypeScriptstrict |
| 运行时 | 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
# 本地跑 bltsx 直跑,无需 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 正常退出", );
});
// 层 2skipIf 缺凭证 — dry-run / 真实集成
describe.skipIf(!isConsoleE2EReady())("e2e: asset-centerConsole …)", () => {
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 Skillnpx skills add modelstudioai/cli
.github/workflows/ # CI/CDpublish.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`。_
+9 -1
View File
@@ -41,6 +41,7 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
@@ -149,6 +150,13 @@ bl quota check # Current usage vs rate li
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
@@ -186,7 +194,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
+9 -1
View File
@@ -41,6 +41,7 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
@@ -147,6 +148,13 @@ bl quota check # 当前用量 vs 限流
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan 团队版管理(需 AK/SK见下方认证说明
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
@@ -184,7 +192,7 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history``asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
+16
View File
@@ -84,6 +84,14 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
assetList,
assetGet,
assetFavorite,
assetUnfavorite,
assetDelete,
assetDownload,
assetStats,
assetStorage,
workspaceInit,
pluginInstall,
pluginLink,
@@ -202,6 +210,14 @@ 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,
@@ -0,0 +1,131 @@
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-centerConsole", () => {
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);
});
});
@@ -0,0 +1,434 @@
# 资产中心 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` |
@@ -0,0 +1,408 @@
# 资产中心 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>` | positionalprimary |
| `--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`
@@ -0,0 +1,35 @@
# 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 错误处理边界一致。
@@ -0,0 +1,36 @@
# 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用法错误 |
@@ -0,0 +1,624 @@
# 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
}
}
```
@@ -0,0 +1,67 @@
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,
);
}
},
});
@@ -0,0 +1,76 @@
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}`);
},
});
@@ -0,0 +1,54 @@
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);
}
},
});
@@ -0,0 +1,95 @@
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}`);
},
});
@@ -0,0 +1,150 @@
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("; ")}`);
},
});
@@ -0,0 +1,86 @@
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}`);
}
},
});
@@ -0,0 +1,47 @@
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 ?? "-"}`);
},
});
@@ -0,0 +1,72 @@
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;
}
@@ -0,0 +1,54 @@
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);
}
},
});
@@ -0,0 +1,209 @@
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`;
}
+8
View File
@@ -91,6 +91,14 @@ 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,295 @@
# `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
```
+89 -80
View File
@@ -9,89 +9,98 @@ 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 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 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 | 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 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) |
## By group
| 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`, `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) |
| `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`, `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) |
## Global flags
+523
View File
@@ -0,0 +1,523 @@
#!/usr/bin/env node
/**
* Asset-center Phase 2: reversible write ops + advanced read scenarios.
*/
import { execFileSync } from "node:child_process";
import { existsSync, unlinkSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { writeFileSync } from "node:fs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const cliDir = join(root, "packages/cli");
const mainTs = join(cliDir, "src/main.ts");
const workspaceId = process.env.BAILIAN_WORKSPACE_ID ?? "llm-0xvms4kqhbqjlg8s";
function runBl(args, { timeout = 180_000 } = {}) {
const start = Date.now();
try {
const stdout = execFileSync("pnpm", ["exec", "tsx", mainTs, ...args], {
cwd: cliDir,
encoding: "utf8",
timeout,
env: { ...process.env, BAILIAN_WORKSPACE_ID: workspaceId },
stdio: ["pipe", "pipe", "pipe"],
});
return { exitCode: 0, stdout: stdout.trim(), stderr: "", durationMs: Date.now() - start };
} catch (error) {
return {
exitCode: error.status ?? 1,
stdout: (error.stdout ?? "").trim(),
stderr: (error.stderr ?? "").trim(),
durationMs: Date.now() - start,
};
}
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function status(exitCode, expected = 0) {
return exitCode === expected ? "✅ PASS" : "❌ FAIL";
}
function pickUnfavoritedImage(listJson) {
return listJson?.items?.find((item) => item.asset_type === "IMAGE" && item.favorited === false);
}
const results = [];
function record(category, command, mode, result, expect, summary) {
const pass = result.exitCode === expect;
results.push({ category, command, mode, ...result, expect, pass, summary });
console.log(` ${pass ? "✅" : "❌"} ${command}${summary}`);
return pass;
}
console.log(`\n🔍 Asset Center Phase 2 — workspace: ${workspaceId}\n`);
// --- Setup: find test assets ---
const imageList = runBl([
"asset-center",
"list",
"--type",
"IMAGE",
"--page-size",
"10",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const imageJson = parseJson(imageList.stdout);
const testImage = pickUnfavoritedImage(imageJson);
const testImageId = testImage?.asset_id ?? imageJson?.items?.[0]?.asset_id;
const videoList = runBl([
"asset-center",
"list",
"--type",
"VIDEO",
"--page-size",
"1",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const testVideoId = parseJson(videoList.stdout)?.items?.[0]?.asset_id;
console.log(`📦 测试 IMAGE: ${testImageId}`);
console.log(`📦 测试 VIDEO: ${testVideoId}\n`);
// 1. Download IMAGE to /tmp
const downloadPath = `/tmp/asset-center-test-${testImageId}.png`;
if (existsSync(downloadPath)) unlinkSync(downloadPath);
const dlResult = runBl(
[
"asset-center",
"download",
"--id",
testImageId,
"--out",
downloadPath,
"--output",
"json",
"--workspace-id",
workspaceId,
],
{ timeout: 300_000 },
);
let dlSummary = "download failed";
if (dlResult.exitCode === 0) {
const dlJson = parseJson(dlResult.stdout);
const fileExists = existsSync(downloadPath);
const fileSize = fileExists ? statSync(downloadPath).size : 0;
dlSummary = fileExists
? `saved ${downloadPath} (${fileSize} bytes, reported ${dlJson?.size ?? "?"})`
: `exit 0 but file missing`;
if (fileExists) unlinkSync(downloadPath);
}
record("下载", "asset-center download (IMAGE)", "真实调用", dlResult, 0, dlSummary);
// 2. Get with --include-download-url
const getUrlResult = runBl([
"asset-center",
"get",
"--asset-id",
testImageId,
"--include-download-url",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const getUrlJson = parseJson(getUrlResult.stdout);
const hasDownloadUrl = Boolean(getUrlJson?.download_url ?? getUrlJson?.downloadUrl);
record(
"查询",
"asset-center get --include-download-url",
"真实调用",
getUrlResult,
0,
hasDownloadUrl ? "download_url present" : `no download_url in response`,
);
// 3. List with --include-download-url
const listUrlResult = runBl([
"asset-center",
"list",
"--type",
"IMAGE",
"--page-size",
"2",
"--include-download-url",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const listUrlJson = parseJson(listUrlResult.stdout);
const listHasUrl = listUrlJson?.items?.some((item) => item.download_url);
record(
"查询",
"asset-center list --include-download-url",
"真实调用",
listUrlResult,
0,
listHasUrl ? "items contain download_url" : "no download_url in items",
);
// 4. Pagination: page 1 → next-token → page 2
const page1 = parseJson(
runBl([
"asset-center",
"list",
"--page-size",
"3",
"--output",
"json",
"--workspace-id",
workspaceId,
]).stdout,
);
const nextToken = page1?.next_token;
let pageSummary = "no next_token (single page)";
if (nextToken != null) {
const page2Result = runBl([
"asset-center",
"list",
"--page-size",
"3",
"--next-token",
String(nextToken),
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const page2 = parseJson(page2Result.stdout);
const ids1 = new Set(page1.items.map((item) => item.asset_id));
const overlap = page2?.items?.filter((item) => ids1.has(item.asset_id)).length ?? 0;
pageSummary =
page2Result.exitCode === 0
? `page2=${page2?.items?.length ?? 0} items, overlap=${overlap}, has_pre=${page2?.has_pre}`
: `page2 failed exit ${page2Result.exitCode}`;
record("查询", "asset-center list --next-token", "真实调用", page2Result, 0, pageSummary);
} else {
record(
"查询",
"asset-center list --next-token",
"跳过",
{ exitCode: 0, durationMs: 0, stdout: "", stderr: "" },
0,
pageSummary,
);
}
// 5. Stats filters
const statsImage = runBl([
"asset-center",
"stats",
"--type",
"IMAGE",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const statsImageJson = parseJson(statsImage.stdout);
record(
"统计",
"asset-center stats --type IMAGE",
"真实调用",
statsImage,
0,
`image=${statsImageJson?.image_count ?? "?"}, total=${statsImageJson?.total_count ?? "?"}`,
);
const statsSyncFailed = runBl([
"asset-center",
"stats",
"--sync-failed",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const statsSyncJson = parseJson(statsSyncFailed.stdout);
record(
"统计",
"asset-center stats --sync-failed",
"真实调用",
statsSyncFailed,
0,
`total=${statsSyncJson?.total_count ?? "?"}, sync_failed=${statsSyncJson?.sync_failed_count ?? "?"}`,
);
// 6. Recycle bin (read-only)
const recycleResult = runBl([
"asset-center",
"list",
"--recycle-bin",
"--page-size",
"5",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const recycleJson = parseJson(recycleResult.stdout);
record(
"查询",
"asset-center list --recycle-bin",
"真实调用",
recycleResult,
0,
`${recycleJson?.items?.length ?? 0} soft-deleted item(s)`,
);
// 7. Text output format
const textResult = runBl([
"asset-center",
"list",
"--page-size",
"2",
"--workspace-id",
workspaceId,
]);
const textHasTable = textResult.stdout.includes("ASSET_ID") || textResult.stdout.includes("asset_");
record(
"输出",
"asset-center list (text)",
"真实调用",
textResult,
0,
textHasTable
? `${textResult.stdout.split("\n").filter(Boolean).length} lines table output`
: "unexpected text format",
);
// 8. Favorite → verify → unfavorite → verify (round-trip)
if (testImageId) {
const favResult = runBl([
"asset-center",
"favorite",
"--id",
testImageId,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const favJson = parseJson(favResult.stdout);
record(
"收藏",
"asset-center favorite (真实)",
"真实调用",
favResult,
0,
`affected=${favJson?.affected_count ?? "?"}`,
);
const verifyFav = runBl([
"asset-center",
"get",
"--asset-id",
testImageId,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const verifyFavJson = parseJson(verifyFav.stdout);
const isFavorited = verifyFavJson?.favorited === true;
record(
"收藏",
"get 验证 favorited=true",
"真实调用",
verifyFav,
0,
isFavorited ? "favorited=true ✓" : `favorited=${verifyFavJson?.favorited}`,
);
const favListResult = runBl([
"asset-center",
"list",
"--favorited",
"--page-size",
"20",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const favListJson = parseJson(favListResult.stdout);
const inFavList = favListJson?.items?.some((item) => item.asset_id === testImageId);
record(
"查询",
"list --favorited 含测试资产",
"真实调用",
favListResult,
0,
inFavList ? "found in favorited list" : "NOT in favorited list",
);
const unfavResult = runBl([
"asset-center",
"unfavorite",
"--id",
testImageId,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const unfavJson = parseJson(unfavResult.stdout);
record(
"收藏",
"asset-center unfavorite (真实)",
"真实调用",
unfavResult,
0,
`affected=${unfavJson?.affected_count ?? "?"}`,
);
const verifyUnfav = runBl([
"asset-center",
"get",
"--asset-id",
testImageId,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const verifyUnfavJson = parseJson(verifyUnfav.stdout);
const isUnfavorited = verifyUnfavJson?.favorited === false;
record(
"收藏",
"get 验证 favorited=false (恢复)",
"真实调用",
verifyUnfav,
0,
isUnfavorited ? "favorited=false ✓" : `favorited=${verifyUnfavJson?.favorited}`,
);
}
// 9. Batch favorite (2 ids) round-trip
const twoImages = imageJson?.items?.filter((item) => !item.favorited).slice(0, 2) ?? [];
if (twoImages.length === 2) {
const [first, second] = twoImages;
const batchFav = runBl([
"asset-center",
"favorite",
"--id",
first.asset_id,
"--id",
second.asset_id,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
record("收藏", "favorite 批量 (--id x2)", "真实调用", batchFav, 0, summarizeBatch(batchFav));
const batchUnfav = runBl([
"asset-center",
"unfavorite",
"--id",
first.asset_id,
"--id",
second.asset_id,
"--output",
"json",
"--workspace-id",
workspaceId,
]);
record(
"收藏",
"unfavorite 批量 (--id x2)",
"真实调用",
batchUnfav,
0,
summarizeBatch(batchUnfav),
);
}
// 10. Get non-existent asset — server error passthrough (exit 1)
const badGet = runBl([
"asset-center",
"get",
"--asset-id",
"asset_nonexistent_00000000",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const badGetErr = parseJson(badGet.stderr);
record(
"边界",
"get 不存在的 asset-id",
"真实调用",
badGet,
1,
badGetErr?.error?.message === "资产不存在"
? 'exit 1, 服务端错误原样透传: "资产不存在"'
: `exit ${badGet.exitCode}, msg=${badGetErr?.error?.message ?? badGet.stderr.slice(0, 60)}`,
);
function summarizeBatch(result) {
const json = parseJson(result.stdout);
return json ? `affected=${json.affected_count ?? "?"}` : result.stdout.slice(0, 60);
}
// --- Report ---
const passed = results.filter((row) => row.pass).length;
const failed = results.filter((row) => !row.pass).length;
const lines = [
`# Asset Center 命令测试报告 — Phase 2`,
``,
`- **测试时间**: ${new Date().toISOString().replace("T", " ").slice(0, 19)} (UTC)`,
`- **Workspace**: \`${workspaceId}\``,
`- **测试 IMAGE**: \`${testImageId ?? "n/a"}\``,
`- **测试 VIDEO**: \`${testVideoId ?? "n/a"}\``,
`- **策略**: 可逆写操作favorite/unfavorite 往返download 到 /tmp 后删除;其余只读`,
`- **汇总**: ${passed} 通过 / ${failed} 失败 / ${results.length} 总计`,
``,
`> Phase 1 报告见同目录 [TEST-REPORT.md](./TEST-REPORT.md)24 项 dry-run + 只读基础验证)`,
``,
`## Phase 2 测试结果`,
``,
`| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |`,
`|---|------|------|------|------|------|------|----------|`,
];
results.forEach((row, index) => {
lines.push(
`| ${index + 1} | ${row.category} | \`${row.command}\` | ${row.mode} | ${status(row.exitCode, row.expect)} | ${row.exitCode} | ${row.durationMs}ms | ${(row.summary ?? "").replace(/\|/g, "\\|")} |`,
);
});
if (failed > 0) {
lines.push(``, `## 失败详情`, ``);
for (const row of results.filter((row) => !row.pass)) {
lines.push(`### ${row.command}`, ``);
lines.push(`**stderr:**`, `\`\`\``, row.stderr || "(empty)", `\`\`\``, ``);
lines.push(`**stdout:**`, `\`\`\``, row.stdout?.slice(0, 800) || "(empty)", `\`\`\``, ``);
}
}
const reportPath = join(root, "packages/commands/src/commands/asset-center/TEST-REPORT-PHASE2.md");
writeFileSync(reportPath, lines.join("\n"));
console.log(`\n📄 Report: ${reportPath}`);
console.log(`${passed} passed, ❌ ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);
+507
View File
@@ -0,0 +1,507 @@
#!/usr/bin/env node
/**
* Asset-center command integration test runner.
* Read-only commands: real API. Write commands: --dry-run only.
*/
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { readFileSync, writeFileSync } from "node:fs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const cliDir = join(root, "packages/cli");
const mainTs = join(cliDir, "src/main.ts");
const workspaceId = process.env.BAILIAN_WORKSPACE_ID ?? "llm-0xvms4kqhbqjlg8s";
function runBl(args, { timeout = 120_000 } = {}) {
const start = Date.now();
try {
const stdout = execFileSync("pnpm", ["exec", "tsx", mainTs, ...args], {
cwd: cliDir,
encoding: "utf8",
timeout,
env: { ...process.env, BAILIAN_WORKSPACE_ID: workspaceId },
stdio: ["pipe", "pipe", "pipe"],
});
return {
exitCode: 0,
stdout: stdout.trim(),
stderr: "",
durationMs: Date.now() - start,
};
} catch (error) {
return {
exitCode: error.status ?? 1,
stdout: (error.stdout ?? "").trim(),
stderr: (error.stderr ?? "").trim(),
durationMs: Date.now() - start,
};
}
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function summarizeOutput(result) {
const json = parseJson(result.stdout);
if (json) {
if (json.api) return `dry-run → ${json.api.split(".").pop()}`;
if (json.items?.length !== undefined)
return `${json.items.length} item(s)${json.next_token != null ? `, next=${json.next_token}` : ""}`;
if (json.item) return `asset=${json.item.asset_id ?? json.item.assetId ?? "?"}`;
if (json.total_count != null) return `total=${json.total_count}`;
if (json.used_bytes != null)
return `used=${json.used_bytes} / quota=${json.quota_bytes ?? "?"}`;
if (json.policy_id ?? json.policyId) return `policy=${json.policy_id ?? json.policyId}`;
if (json.authorized != null) return `authorized=${json.authorized}`;
if (json.saved) return `saved=${json.saved}`;
const keys = Object.keys(json).slice(0, 4).join(", ");
return keys ? `{${keys}}` : "json ok";
}
const lines = result.stdout.split("\n").filter(Boolean);
if (lines.length === 0 && result.stderr) {
const errLine = result.stderr
.split("\n")
.find((line) => line.includes("Error") || line.includes("error"));
return errLine?.slice(0, 80) ?? result.stderr.slice(0, 80);
}
if (lines.length <= 3) return lines.join(" | ").slice(0, 100);
return `${lines.length} lines, first: ${lines[0].slice(0, 60)}`;
}
function status(exitCode, expected = 0) {
if (exitCode === expected) return "✅ PASS";
return "❌ FAIL";
}
// Fetch sample asset ID
const listResult = runBl([
"asset-center",
"list",
"--page-size",
"1",
"--output",
"json",
"--workspace-id",
workspaceId,
]);
const listJson = parseJson(listResult.stdout);
const sampleAssetId = listJson?.items?.[0]?.asset_id ?? "asset_df026105d2274ff9b8c824058fa23d60";
const cases = [
// --- Read-only (real API) ---
{
category: "查询",
command: "asset-center list",
mode: "真实调用",
args: [
"asset-center",
"list",
"--page-size",
"3",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "查询",
command: "asset-center list --type IMAGE",
mode: "真实调用",
args: [
"asset-center",
"list",
"--type",
"IMAGE",
"--page-size",
"2",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "查询",
command: "asset-center get",
mode: "真实调用",
args: [
"asset-center",
"get",
"--asset-id",
sampleAssetId,
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "统计",
command: "asset-center stats",
mode: "真实调用",
args: ["asset-center", "stats", "--output", "json", "--workspace-id", workspaceId],
expect: 0,
},
{
category: "统计",
command: "asset-center storage",
mode: "真实调用",
args: ["asset-center", "storage", "--output", "json", "--workspace-id", workspaceId],
expect: 0,
},
{
category: "转存",
command: "asset-center transfer list",
mode: "真实调用",
args: [
"asset-center",
"transfer",
"list",
"--page-size",
"5",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "OSS",
command: "asset-center oss show",
mode: "真实调用",
args: ["asset-center", "oss", "show", "--output", "json", "--workspace-id", workspaceId],
expect: 0,
},
// --- Dry-run (write / side-effect) ---
{
category: "查询",
command: "asset-center list --dry-run",
mode: "dry-run",
args: ["asset-center", "list", "--dry-run", "--output", "json", "--workspace-id", workspaceId],
expect: 0,
},
{
category: "查询",
command: "asset-center get --dry-run",
mode: "dry-run",
args: [
"asset-center",
"get",
"--asset-id",
sampleAssetId,
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "收藏",
command: "asset-center favorite",
mode: "dry-run",
args: [
"asset-center",
"favorite",
"--id",
sampleAssetId,
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "收藏",
command: "asset-center unfavorite",
mode: "dry-run",
args: [
"asset-center",
"unfavorite",
"--id",
sampleAssetId,
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "删除",
command: "asset-center delete",
mode: "dry-run",
args: [
"asset-center",
"delete",
"--id",
sampleAssetId,
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "下载",
command: "asset-center download",
mode: "dry-run",
args: [
"asset-center",
"download",
"--id",
sampleAssetId,
"--out",
"/tmp/asset-test-download.mp4",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "统计",
command: "asset-center stats --dry-run",
mode: "dry-run",
args: ["asset-center", "stats", "--dry-run", "--output", "json", "--workspace-id", workspaceId],
expect: 0,
},
{
category: "统计",
command: "asset-center storage --dry-run",
mode: "dry-run",
args: [
"asset-center",
"storage",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "OSS",
command: "asset-center oss slr authorize",
mode: "dry-run",
args: [
"asset-center",
"oss",
"slr",
"authorize",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "OSS",
command: "asset-center oss bind",
mode: "dry-run",
args: [
"asset-center",
"oss",
"bind",
"--bucket",
"test-bucket",
"--region",
"cn-hangzhou",
"--path-prefix",
"assets/",
"--policy",
"ALL",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "OSS",
command: "asset-center oss update",
mode: "dry-run",
args: [
"asset-center",
"oss",
"update",
"--policy-id",
"policy-test-001",
"--bucket",
"test-bucket",
"--region",
"cn-hangzhou",
"--path-prefix",
"assets/",
"--policy",
"BEFORE_DAYS",
"--before-days",
"30",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
{
category: "OSS",
command: "asset-center oss unbind",
mode: "dry-run",
args: [
"asset-center",
"oss",
"unbind",
"--policy-id",
"policy-test-001",
"--dry-run",
"--output",
"json",
"--workspace-id",
workspaceId,
],
expect: 0,
},
// --- Validation (expect usage error) ---
{
category: "校验",
command: "asset-center get (缺 asset-id)",
mode: "参数校验",
args: ["asset-center", "get", "--quiet", "--workspace-id", workspaceId],
expect: 2,
},
{
category: "校验",
command: "asset-center favorite (缺 --id)",
mode: "参数校验",
args: ["asset-center", "favorite", "--quiet", "--workspace-id", workspaceId],
expect: 2,
},
{
category: "校验",
command: "asset-center download (缺 --out)",
mode: "参数校验",
args: [
"asset-center",
"download",
"--id",
sampleAssetId,
"--quiet",
"--workspace-id",
workspaceId,
],
expect: 2,
},
{
category: "校验",
command: "asset-center oss bind (BEFORE_DAYS 缺 before-days)",
mode: "参数校验",
args: [
"asset-center",
"oss",
"bind",
"--bucket",
"b",
"--region",
"cn-hangzhou",
"--path-prefix",
"p/",
"--policy",
"BEFORE_DAYS",
"--workspace-id",
workspaceId,
],
expect: 2,
},
];
console.log(`\n🔍 Asset Center 命令调试 — workspace: ${workspaceId}`);
console.log(`📦 样本 asset-id: ${sampleAssetId}\n`);
const results = [];
for (const testCase of cases) {
process.stdout.write(` running: ${testCase.command} ... `);
const result = runBl(testCase.args);
const pass = result.exitCode === testCase.expect;
console.log(pass ? "ok" : `FAIL (exit ${result.exitCode})`);
results.push({ ...testCase, ...result, pass });
}
const passed = results.filter((row) => row.pass).length;
const failed = results.filter((row) => !row.pass).length;
// Markdown report
const lines = [
`# Asset Center 命令测试报告`,
``,
`- **测试时间**: ${new Date().toISOString().replace("T", " ").slice(0, 19)} (UTC)`,
`- **Workspace**: \`${workspaceId}\``,
`- **样本 Asset ID**: \`${sampleAssetId}\``,
`- **策略**: 只读命令真实调用;写操作/下载/OSS 变更一律 \`--dry-run\``,
`- **汇总**: ${passed} 通过 / ${failed} 失败 / ${results.length} 总计`,
``,
`## 测试结果`,
``,
`| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |`,
`|---|------|------|------|------|------|------|----------|`,
];
results.forEach((row, index) => {
lines.push(
`| ${index + 1} | ${row.category} | \`${row.command}\` | ${row.mode} | ${status(row.exitCode, row.expect)} | ${row.exitCode} | ${row.durationMs}ms | ${summarizeOutput(row).replace(/\|/g, "\\|")} |`,
);
});
if (failed > 0) {
lines.push(``, `## 失败详情`, ``);
for (const row of results.filter((row) => !row.pass)) {
lines.push(`### ${row.command}`, ``);
lines.push(`**stderr:**`, `\`\`\``, row.stderr || "(empty)", `\`\`\``, ``);
lines.push(`**stdout:**`, `\`\`\``, row.stdout.slice(0, 500) || "(empty)", `\`\`\``, ``);
}
}
lines.push(
``,
`## 模式说明`,
``,
`| 模式 | 说明 |`,
`|------|------|`,
`| 真实调用 | 只读 API不修改数据 |`,
`| dry-run | 输出 \`{ api, data, gateway }\` 请求体,不发起写操作 |`,
`| 参数校验 | 预期 exit code 2用法错误 |`,
);
const reportPath = join(root, "packages/commands/src/commands/asset-center/TEST-REPORT.md");
writeFileSync(reportPath, lines.join("\n"));
console.log(`\n📄 Report written: ${reportPath}`);
console.log(`${passed} passed, ❌ ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);