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
46 changed files with 5236 additions and 606 deletions
+145 -73
View File
@@ -13,9 +13,8 @@
---
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Built for AI Agents. Every command works as a structured tool call._
@@ -23,16 +22,29 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
## Showcase 1: A Cinematic Short Film from One Sentence
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -45,77 +57,136 @@ _Built for AI Agents. Every command works as a structured tool call._
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2分钟左右的视频,尺寸是16:9")_
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
### How it works
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
## Installation
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
**Manual install (npm)**
```bash
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> Requires Node.js >= 18.17.
> Binary install does not require Node.js. `npm install -g` remains fully supported.
## Quick Start
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
```bash
# Authenticate, recommended
bl auth login --console
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Fine-tune & deploy — a one-shot train-to-serve workflow
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
bl workspace list # List all workspaces
# Rate limit management (list / check / request / history)
bl quota list # View RPM/TPM limits (add --model to filter)
bl quota check # Current usage vs rate limits (add --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## Authentication
### API Key
### DashScope API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
```
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -123,20 +194,26 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). 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
```
### Alibaba Cloud OpenAPI AK/SK
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -145,31 +222,18 @@ bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# View current config
bl config show
# List all config profiles
bl config list
# Set defaults
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# Switch config profile
bl config use --name token-plan
# Self-update to latest or a specific version
bl update
bl update --to 0.1.14
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -181,3 +245,11 @@ Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usa
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+146 -73
View File
@@ -22,16 +22,29 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
## 功能特性
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
## 示例 1:一句话生成一部电影短片
> **注意:** 以下功能目前仅对中国站(aliyun.com)账号开放,国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT)、非阻塞探测任务状态(`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`)
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`)
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`)
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -41,80 +54,137 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _“帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9。”_
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
## 示例 2:一句话构建短片导演 Managed Agent
### 工作流程
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**。
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用,能够实现短片拍摄,导演专家生成视频,然后也能进行设计对应的分镜图。”_
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
## 安装
**Agent 安装(推荐)**
把下面这句话发给你的 Agent,它会自行判断环境并完成安装与校验:
```text
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
**手动安装(npm)**
```bash
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows(PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node 用户 / 开发者(需要 Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> 需要预先安装 Node.js >= 18.17。
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
## 快速开始
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
```bash
# 认证(推荐浏览器登录)
bl auth login --console
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
# 或使用 API key 认证
bl auth login --api-key sk-xxxxx
# 或使用 Token Plan(已内置 Base URL,登录时自动测试 Key)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 微调与部署 — 从训练到服务的一站式流程
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0;失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort)
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
bl workspace list # 列出所有业务空间
# 限流管理与提额(list / check / request / history)
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
bl quota check # 当前用量 vs 限流阈值(加 --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### API Key
### DashScope API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
```
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
### Token Plan API Key
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL;登录命令会先测试 Key,通过后才保存并激活 `token-plan` 配置。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -122,20 +192,26 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录(OAuth)
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(`model list`、`app list`、`usage summary/free/stats`、`workspace list`、`quota list/request/check/history`、`asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -144,31 +220,20 @@ bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 查看当前配置
bl config show
# 查看全部配置档
bl config list
# 设置默认值
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 切换配置档
bl config use --name token-plan
# 自更新到最新版本
bl update
# 安装指定版本
bl update --to 0.1.14
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群,获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -180,3 +245,11 @@ bl update
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+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. 技术栈
| 类别 | 选型 |
| --------- | -------------------------------------------------------------------------------------------- |
| 语言 | TypeScript(strict) |
| 运行时 | Node.js ≥ 22.12 |
| 包管理 | pnpm 10 + workspace catalog |
| 构建/测试 | [vite-plus](https://github.com/voidzero-dev/vite-plus)(`vp check` / `vp test` / `vp pack`) |
| HTTP | undici(经 core client 封装) |
| 模块 | ESM(`"type": "module"`) |
---
## 3. 核心架构:四层分层
项目按 **「纯逻辑 → 运行时框架 → 命令库 → 产品入口」** 严格分层,职责边界清晰:
```
┌─────────────────────────────────────────────────────────────────┐
│ 产品入口层 │
│ packages/cli (bl) packages/kscli (kscli) │
│ 决定命令路径 map、产品 identity、README、技能 reference │
└────────────────────────────┬────────────────────────────────────┘
│ createCli(commands, identity)
┌────────────────────────────▼────────────────────────────────────┐
│ 运行时框架层 packages/runtime (bailian-cli-runtime) │
│ 参数解析、命令树/registry、help、middleware、错误处理、输出 │
└────────────────────────────┬────────────────────────────────────┘
│ 调用 defineCommand 的 run()
┌────────────────────────────▼────────────────────────────────────┐
│ 命令库层 packages/commands (bailian-cli-commands) │
│ 96+ 命令实现;只导出 command,不决定产品路径 │
└────────────────────────────┬────────────────────────────────────┘
│ client / settings / auth
┌────────────────────────────▼────────────────────────────────────┐
│ 纯逻辑层 packages/core (bailian-cli-core) │
│ 鉴权、配置、HTTP client、错误、类型、文件工具、领域 API │
└─────────────────────────────────────────────────────────────────┘
```
### 分层边界(必须遵守)
| 层 | 可以做 | 不能做 |
| --------------- | --------------------------- | ------------------------------------------------------------- |
| **core** | 纯库逻辑、HTTP、鉴权解析 | 依赖 runtime/commands;硬编码 `bl`/`kscli`;调 `process.exit` |
| **runtime** | TTY、help、middleware、输出 | 写具体业务命令逻辑 |
| **commands** | 命令元数据 + `run` 实现 | 决定产品路径;在 usage 里写 bin 前缀 |
| **cli / kscli** | 命令路径 map、产品 identity | 不写命令业务逻辑 |
---
## 4. 包详解
### 4.1 `packages/core` — `bailian-cli-core`
纯逻辑层,被所有上层依赖。主要模块:
```
packages/core/src/
├── auth/ # API Key / Console token 解析与落盘
├── client/ # HTTP client、endpoints、MCP、流式解析
├── config/ # ~/.bailian/config.json、Settings、来源优先级
├── console/ # Console Gateway 调用
├── dataset/ # 数据集校验(ChatML/DPO/CPT schema)
├── finetune/ # 微调 API 与能力探测
├── deploy/ # 部署 API
├── advisor/ # 模型推荐(意图识别 + 召回)
├── errors/ # BailianError、UsageError、退出码
├── output/ # JSON/text 格式化(命令层也可用 runtime 的 emit)
├── files/ # 本地文件上传、URL 解析
├── telemetry/ # 命令执行遥测
└── types/ # Command、FlagsDef、defineCommand
```
**关键类型** — 每个命令通过 `defineCommand` 声明:
```typescript
defineCommand({
description: "…",
auth: "apiKey" | "console" | "none",
flags: {
/* camelCase key → kebab-case CLI flag */
},
usageArgs: "--prompt <text> [flags]", // 不含 bl/kscli 前缀
exampleArgs: ['--prompt "hello"'],
validate: (flags) => string | undefined, // 跨 flag 校验
run: async (ctx) => {
/* ctx.client / ctx.flags / ctx.settings */
},
});
```
**Client** 是命令的网络入口,凭证已注入,命令层不碰 token:
```typescript
ctx.client.requestJson({ path: "/…", method: "POST", body });
ctx.client.console({ product: "…", action: "…", params });
ctx.client.uploadFile(localPath);
ctx.client.mcp(…);
```
### 4.2 `packages/runtime` — `bailian-cli-runtime`
通用 CLI 框架,与具体业务无关。核心文件:
| 文件 | 职责 |
| ------------------ | ----------------------------------------------- |
| `create-cli.ts` | 入口工厂:`createCli(commands, identity).run()` |
| `registry.ts` | 从 `Record<string, AnyCommand>` 建树,动态 help |
| `args.ts` | 路径 + flag 解析 |
| `middleware.ts` | auth → telemetry → versionCheck → runCommand |
| `error-handler.ts` | 统一错误输出与退出码 |
| `urls.ts` | 用户面控制台 URL(非 API endpoint) |
| `output/` | 颜色、表格、进度条、banner |
| `pipeline/` | 多步 pipeline 编排(`bl pipeline run`) |
**Middleware 流水线**(洋葱模型):
```
argv 解析
→ authStage 按 command.auth 注入 apiKey / console 凭证到 ctx.client
→ telemetryStage 记录命令执行
→ versionCheckStage 检查 npm 更新
→ runCommandStage 调用 command.run(ctx)
```
### 4.3 `packages/commands` — `bailian-cli-commands`
命令实现库,按**能力域**组织目录(≠ 最终 CLI 路径):
```
packages/commands/src/commands/
├── text/ # 文本对话
├── omni/ # 全模态对话
├── image/ # 图像生成/编辑
├── video/ # 视频生成/编辑/下载
├── speech/ # 语音合成/识别
├── vision/ # 图像/视频理解
├── knowledge/ # 知识库检索/搜索/对话
├── memory/ # 记忆管理
├── app/ # 应用调用
├── mcp/ # MCP 服务
├── auth/ # 登录/登出/状态
├── config/ # 配置读写
├── console/ # 通用 Console Gateway 调用
├── dataset/ # 数据集上传/校验
├── finetune/ # 微调任务
├── deploy/ # 模型部署
├── quota/ # 限流与提额
├── workspace/ # 业务空间
├── usage/ # 用量统计
├── advisor/ # 模型推荐
├── asset-center/ # 资产中心(新)
├── pipeline/ # Pipeline 编排
├── search/ # 联网搜索
├── file/ # 文件上传
├── token-plan/ # Token 计划
└── update.ts # 自更新
```
每个命令文件 `export default defineCommand(…)`,并在 `packages/commands/src/index.ts` 具名 re-export。
### 4.4 `packages/cli` — `bailian-cli`(`bl`)
产品入口,极薄:
```typescript
// packages/cli/src/main.ts
createCli(commands, {
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
npmPackage: "bailian-cli",
}).run();
```
**命令路径由 `packages/cli/src/commands.ts` 决定**,例如:
```typescript
export const commands: Record<string, AnyCommand> = {
"text chat": textChat,
"asset-center list": assetList,
"finetune create": finetuneCreate,
update, // 单级命令 key 即路径
};
```
此文件还被 `tools/generate-reference.ts` 读取,生成 Agent Skill 参考文档。
### 4.5 `packages/kscli` — `knowledge-studio-cli`(`kscli`)
轻量 RAG 产品,**复用同一套 commands**,但路径拍平:
```typescript
const commands = {
retrieve: knowledgeRetrieve, // ↔ bl knowledge retrieve
search: knowledgeSearch, // ↔ bl knowledge search
chat: knowledgeChat, // ↔ bl knowledge chat
"config show": configShow,
update,
};
```
同一个 `knowledgeRetrieve` 实现,在 `bl` 显示 `bl knowledge retrieve`,在 `kscli` 显示 `kscli retrieve`——路径完全由产品入口 map 的 key 决定。
---
## 5. 一次命令执行的完整链路
以 `bl text chat --message "hi"` 为例:
```mermaid
sequenceDiagram
participant User
participant main as cli/main.ts
participant createCli as runtime/create-cli.ts
participant registry as runtime/registry.ts
participant mw as middleware
participant cmd as commands/text/chat.ts
participant client as core/client
User->>main: bl text chat --message "hi"
main->>createCli: createCli(commands, identity).run(argv)
createCli->>registry: 解析路径 ["text","chat"]
registry-->>createCli: 匹配 textChat command
createCli->>mw: authStage → 注入 apiKey 到 client
mw->>cmd: run(ctx)
cmd->>client: requestJson / parseSSE
client-->>User: stdout 输出
```
**配置与凭证解析优先级**(core 统一处理,命令不介入):
| 来源 | API Key | Console Token |
| ---- | ----------------------- | ------------------------------------- |
| 1 | `--api-key` flag | `~/.bailian/config.json` access_token |
| 2 | `DASHSCOPE_API_KEY` env | — |
| 3 | config.json `api_key` | — |
Console 命令额外有 `--console-region`、`--workspace-id` 等 flag(由 runtime 按 `auth: "console"` 自动展示)。
---
## 6. 鉴权域
每个命令声明 `auth` 字段,runtime 自动处理:
| auth 值 | 适用场景 | 凭证来源 | 网络方法 |
| ----------- | ------------------------------ | -------------------- | -------------------------------- |
| `"apiKey"` | DashScope API(模型推理等) | API Key | `client.request` / `requestJson` |
| `"console"` | Console Gateway(控制台能力) | Console access token | `client.console` |
| `"none"` | 纯本地(config、update、help) | 无 | 可选 credential-less client |
**规则**:调用 Console Gateway 的命令必须 `auth: "console"`,且**不要**重复声明 console 凭证域 flags。
---
## 7. 错误处理约定
CLI **只翻译自己能权威解释的错误**,服务端错误原样透传:
| 错误来源 | 处理 |
| ---------------------- | -------------------------------- |
| 缺参、flag 校验 | `UsageError` → 退出码 2 |
| 本地无凭证 | `BailianError(AUTH)` |
| 网络/DNS/TLS | `BailianError(NETWORK)` |
| HTTP 4xx/5xx、业务错码 | message **原样透传**,不二次包装 |
---
## 8. 开发工作流
### 8.1 环境准备
```bash
# 要求 Node >= 22.12, pnpm >= 10
pnpm install
# 格式化 + lint + 类型检查
pnpm run check # 或 vp check
# 本地跑 bl(tsx 直跑,无需 build)
pnpm run bl -- text chat --help
pnpm run kscli -- search --help
# 全量测试
pnpm test # 或 vp test
# 构建所有包
pnpm run ready # check + test + build
```
### 8.2 新增一个 `bl` 命令(最小路径)
假设新增 `bl widget do`:
**Step 1** — 实现命令(`packages/commands`)
```bash
# 新建
packages/commands/src/commands/widget/do.ts
```
```typescript
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const FLAGS = {
name: { type: "string", valueHint: "<name>", description: "Widget name", required: true },
} satisfies FlagsDef;
export default defineCommand({
description: "Do something with a widget",
auth: "apiKey", // 或 "console" / "none"
flags: FLAGS,
usageArgs: "--name <name>",
exampleArgs: ['--name "demo"'],
async run(ctx) {
const data = await ctx.client.requestJson({ path: "/…", method: "POST", body: { … } });
emitResult(ctx, data);
},
});
```
**Step 2** — 导出(`packages/commands/src/index.ts`)
```typescript
export { default as widgetDo } from "./commands/widget/do.ts";
```
**Step 3** — 注册产品路径(`packages/cli/src/commands.ts`)
```typescript
import { widgetDo } from "bailian-cli-commands";
// …
"widget do": widgetDo,
```
**Step 4** — E2E 测试(`packages/cli/tests/e2e/widget.e2e.test.ts`)
见 [docs/agents/cli-e2e-tests.md](agents/cli-e2e-tests.md):至少覆盖分组 help、`--help`、缺参用例。
**Step 5** — 验证
```bash
vp check
vp test
pnpm run bl -- widget do --help
```
> 若 `kscli` 也需要暴露:在 `packages/kscli/src/main.ts` 的 map 里加 key。
> 技能 reference 会在 pre-commit 时由 `generate-reference.ts` 自动从 `commands.ts` 生成。
详细清单 → [docs/agents/command-add-remove.md](agents/command-add-remove.md)
### 8.3 给已有命令加 flag
→ [docs/agents/command-flag-change.md](agents/command-flag-change.md)
---
## 9. 测试体系
```
packages/cli/tests/
├── e2e/ # 33 个 e2e 测试文件
│ ├── helpers.ts # runCli、环境变量 readiness 判断
│ ├── global-setup.ts
│ └── <topic>.e2e.test.ts
└── stress/ # 多能力并发压测
├── run.mjs
└── targets/
```
**E2E 双层结构**(固定模式):
```typescript
// 层 1:永远跑 — help / 分组,无需 API Key
describe("e2e: asset-center", () => {
test("asset-center 分组展示子命令帮助且成功退出", …);
test("asset-center list --help 正常退出", …);
});
// 层 2:skipIf 缺凭证 — dry-run / 真实集成
describe.skipIf(!isConsoleE2EReady())("e2e: asset-center(Console …)", () => {
test("缺少 --asset-id 时退出为用法错误 (2)", …);
test("真实 list 流程", …);
});
```
环境变量(常用):
| 变量 | 用途 |
| --------------------------------------------- | ------------------------- |
| `DASHSCOPE_API_KEY` | 模型 API 集成测试 |
| Console token(经 `bl auth login --console`) | 控制台命令测试 |
| `BAILIAN_E2E_*` | 各能力开关(视频/媒体等) |
压测:`pnpm run test:stress`
---
## 10. 命令能力地图(`bl` 全量)
当前 `packages/cli/src/commands.ts` 注册的命令组:
| 命令组 | 子命令示例 | auth 域 |
| -------------- | ------------------------------------------------------------------------------- | ---------------- |
| `auth` | login, status, logout | none / console |
| `text` | chat | apiKey |
| `omni` | (全模态对话) | apiKey |
| `image` | generate, edit | apiKey |
| `video` | generate, edit, ref, task get, download | apiKey |
| `vision` | describe | apiKey |
| `speech` | synthesize, recognize | apiKey |
| `knowledge` | retrieve, search, chat | apiKey |
| `memory` | add, search, list, update, delete, profile create/get | apiKey |
| `app` | call, list | apiKey / console |
| `mcp` | call, list, tools | apiKey |
| `search` | web | apiKey |
| `file` | upload | apiKey |
| `config` | show, set | none |
| `console` | call | console |
| `usage` | free, freetier, stats | console |
| `workspace` | list | console |
| `quota` | list, request, history, check | console |
| `dataset` | upload, list, get, delete, validate | console |
| `finetune` | create, list, get, cancel, delete, logs, checkpoints, export, watch, capability | console |
| `deploy` | create, list, get, models, scale, update, delete | console |
| `token-plan` | list-seats, create-key, assign-seats, add-member | console |
| `asset-center` | list, get, favorite, unfavorite, delete, download, stats, storage | console |
| `pipeline` | run, validate | apiKey |
| `advisor` | recommend | apiKey |
| `update` | (自更新) | none |
---
## 11. 非代码资产
```
tools/
├── generate-reference.ts # 从 cli/commands.ts → skills/bailian-cli/reference/
├── sync-skill-metadata.ts # 同步 SKILL.md 版本号
└── release/ # CI 发版自动化
skills/bailian-cli/ # Agent Skill(npx skills add modelstudioai/cli)
.github/workflows/ # CI/CD(publish.yml 等)
docs/agents/ # 各维护场景的 AI 清单
```
根脚本:
```bash
pnpm run sync:skill-assets # build + 生成 reference + 同步版本
pnpm run release:check # 发版前校验
```
---
## 12. 发布
- 版本号:`packages/core`、`runtime`、`commands`、`cli`、`kscli` **保持同步**
- 发布范围:`tools/release/lib/packages.mjs` 定义
- `bailian-cli` 走常规定义发布;`knowledge-studio-cli` 走 `--knowledge` 通道
- 详见 [docs/agents/publish.md](agents/publish.md)
---
## 13. 关键文件速查
| 我想… | 看这里 |
| ------------------- | --------------------------------------- |
| 了解项目契约 | `AGENTS.md` |
| 改 `bl` 命令路径 | `packages/cli/src/commands.ts` |
| 写/改命令逻辑 | `packages/commands/src/commands/<域>/` |
| 导出命令 | `packages/commands/src/index.ts` |
| 改 CLI 框架行为 | `packages/runtime/src/` |
| 改 HTTP/鉴权/配置 | `packages/core/src/` |
| 改 kscli 路径 | `packages/kscli/src/main.ts` |
| 加 E2E 测试 | `packages/cli/tests/e2e/` |
| 改控制台 URL | `packages/runtime/src/urls.ts` |
| 改 API endpoint | `packages/core/src/client/endpoints.ts` |
| 改配置 schema | `packages/core/src/config/schema.ts` |
| 生成 Agent 参考文档 | `tools/generate-reference.ts` |
---
## 14. 场景导航(维护清单)
| 场景 | 文档 |
| ----------- | ------------------------------------------------------- |
| 命令增删改 | [command-add-remove.md](agents/command-add-remove.md) |
| E2E 测试 | [cli-e2e-tests.md](agents/cli-e2e-tests.md) |
| 加/改 flag | [command-flag-change.md](agents/command-flag-change.md) |
| 模型上下架 | [model-add-remove.md](agents/model-add-remove.md) |
| 错误文案 | [error-hint-change.md](agents/error-hint-change.md) |
| 鉴权扩展 | [auth-change.md](agents/auth-change.md) |
| 配置项扩展 | [config-add.md](agents/config-add.md) |
| 发布 | [publish.md](agents/publish.md) |
| 工具链/lint | [lint-toolchain.md](agents/lint-toolchain.md) |
---
## 15. 架构设计要点(读懂代码的钥匙)
1. **命令实现 ≠ 产品路径** — 同一 `knowledgeRetrieve` 可以是 `bl knowledge retrieve` 或 `kscli retrieve`。
2. **defineCommand 是契约** — `auth` + `flags` + `run(ctx)` 是命令的全部接口;凭证和网络细节下沉到 core/runtime。
3. **registry 从 map 建树** — `"asset-center list"` 等 path 自动变成命令组,help 动态生成。
4. **flags 用 camelCase 定义** — runtime 渲染为 `--kebab-case`;`ParsedFlags<typeof FLAGS>` 提供类型安全。
5. **dry-run 是全局 flag** — `--dry-run` 在 auth stage 有例外处理,命令在 `run` 开头判断 `ctx.settings.dryRun`。
6. **本地路径即 URL** — 所有接受 URL 的参数同时支持本地文件路径,core `files/upload` 自动上传。
7. **Console Gateway 统一入口** — 控制台 API 走 `client.console({ product, action, params })`,不散落 raw fetch。
---
## 16. 本地配置速览
配置文件:`~/.bailian/config.json`
```json
{
"api_key": "sk-…",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"access_token": "…",
"console_region": "cn-beijing",
"console_site": "domestic"
}
```
常用环境变量:
| 变量 | 说明 |
| ---------------------------- | -------------------------- |
| `DASHSCOPE_API_KEY` | 模型 API Key |
| `DASHSCOPE_BASE_URL` | API Base URL |
| `BAILIAN_WORKSPACE_ID` | 业务空间 ID |
| `HTTP_PROXY` / `HTTPS_PROXY` | 代理(runtime 启动时读取) |
登录:
```bash
bl auth login # API Key
bl auth login --console # Console token(扫码)
bl auth status
```
---
_文档版本:基于仓库当前结构(含 `asset-center`、`kscli`);`packages/rag` 已演进为 `packages/kscli`。_
+3 -10
View File
@@ -25,7 +25,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
当前 command 鉴权域(`AuthRequirement`):
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent/workspace
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
@@ -35,7 +35,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
- `bl auth login --open-api ...` 只更新 `access_key_id` / `access_key_secret`
- `bl auth logout --console` 只清 `access_token`
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
- `bl auth logout` 清 `api_key` + `base_url` + `access_token` + `access_key_*`
@@ -78,9 +78,6 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- 如新增鉴权域,扩展 `AuthRequirement`
- 更新 `credentialFlagDefs()` 暴露该域可见的 flag
- 必要时新增 `*_AUTH_FLAGS`
- `workspace_id` 是作用域字段而非 credential,不要把它放进 `ConsoleCredential`;读取方式按命令 `auth` 域区分:
- `auth: "console"` 命令通过 `CONSOLE_AUTH_FLAGS` 自动获得 `--workspace-id`,由 `buildSettings()` 解析到 `settings.workspaceId`,命令统一从 `settings.workspaceId` 读取
- `auth: "apiKey"`/`"openapi"`/`"none"` 命令如需 `--workspace-id`,必须自声明 flag;因它不会进入 credential/global flags,命令从 `ctx.flags.workspaceId` 读取(可回退到 `settings.workspaceId`)
- [ ] `packages/core/src/auth/types.ts`:
- 新增 credential 类型 / source / scope 字段
- [ ] `packages/core/src/auth/resolver.ts`:
@@ -134,8 +131,6 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
## 完成后自查
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
```sh
# 各种凭证组合
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
@@ -155,11 +150,9 @@ Console 登录/网关相关改动:
```sh
pnpm -F bailian-cli exec tsx src/main.ts auth login --console
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json --workspace-id ws-xxx
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json
```
注意:`usage stats --dry-run` 仍会先校验 workspace,必须传入 `--workspace-id`(或 `BAILIAN_WORKSPACE_ID` / config `workspace_id`)。
## 常见漏点
- ✗ 加了新 token 来源但忘了改 resolver 优先级,实际不生效
+145 -73
View File
@@ -13,9 +13,8 @@
---
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Built for AI Agents. Every command works as a structured tool call._
@@ -23,16 +22,29 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
## Showcase 1: A Cinematic Short Film from One Sentence
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -45,77 +57,136 @@ _Built for AI Agents. Every command works as a structured tool call._
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2分钟左右的视频,尺寸是16:9")_
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
### How it works
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
## Installation
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
**Manual install (npm)**
```bash
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> Requires Node.js >= 18.17.
> Binary install does not require Node.js. `npm install -g` remains fully supported.
## Quick Start
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
```bash
# Authenticate, recommended
bl auth login --console
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Fine-tune & deploy — a one-shot train-to-serve workflow
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
bl workspace list # List all workspaces
# Rate limit management (list / check / request / history)
bl quota list # View RPM/TPM limits (add --model to filter)
bl quota check # Current usage vs rate limits (add --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## Authentication
### API Key
### DashScope API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
```
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -123,20 +194,26 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). 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
```
### Alibaba Cloud OpenAPI AK/SK
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -145,31 +222,18 @@ bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# View current config
bl config show
# List all config profiles
bl config list
# Set defaults
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# Switch config profile
bl config use --name token-plan
# Self-update to latest or a specific version
bl update
bl update --to 0.1.14
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -181,3 +245,11 @@ Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usa
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+146 -73
View File
@@ -22,16 +22,29 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
## 功能特性
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
## 示例 1:一句话生成一部电影短片
> **注意:** 以下功能目前仅对中国站(aliyun.com)账号开放,国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT)、非阻塞探测任务状态(`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`)
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`)
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`)
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -41,80 +54,137 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _“帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9。”_
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
## 示例 2:一句话构建短片导演 Managed Agent
### 工作流程
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**。
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用,能够实现短片拍摄,导演专家生成视频,然后也能进行设计对应的分镜图。”_
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
## 安装
**Agent 安装(推荐)**
把下面这句话发给你的 Agent,它会自行判断环境并完成安装与校验:
```text
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
**手动安装(npm)**
```bash
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows(PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node 用户 / 开发者(需要 Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> 需要预先安装 Node.js >= 18.17。
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
## 快速开始
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
```bash
# 认证(推荐浏览器登录)
bl auth login --console
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
# 或使用 API key 认证
bl auth login --api-key sk-xxxxx
# 或使用 Token Plan(已内置 Base URL,登录时自动测试 Key)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 微调与部署 — 从训练到服务的一站式流程
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0;失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort)
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
bl workspace list # 列出所有业务空间
# 限流管理与提额(list / check / request / history)
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
bl quota check # 当前用量 vs 限流阈值(加 --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### API Key
### DashScope API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
```
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
### Token Plan API Key
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL;登录命令会先测试 Key,通过后才保存并激活 `token-plan` 配置。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -122,20 +192,26 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录(OAuth)
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(`model list`、`app list`、`usage summary/free/stats`、`workspace list`、`quota list/request/check/history`、`asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK
### 阿里云 OpenAPI AK/SK(仅 Token Plan)
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
# 方式一:登录命令(持久化到 ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -144,31 +220,20 @@ bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 查看当前配置
bl config show
# 查看全部配置档
bl config list
# 设置默认值
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 切换配置档
bl config use --name token-plan
# 自更新到最新版本
bl update
# 安装指定版本
bl update --to 0.1.14
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群,获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -180,3 +245,11 @@ bl update
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.14.2",
"version": "1.14.1",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+16 -2
View File
@@ -84,6 +84,14 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
assetList,
assetGet,
assetFavorite,
assetUnfavorite,
assetDelete,
assetDownload,
assetStats,
assetStorage,
workspaceInit,
pluginInstall,
pluginLink,
@@ -93,7 +101,6 @@ import {
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentPlan,
@@ -203,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,
@@ -212,7 +227,6 @@ export const commands: Record<string, AnyCommand> = {
"skill update": skillUpdate,
"skill remove": skillRemove,
"skill list": skillList,
"skill init": skillInit,
"managed-agent init": managedAgentInit,
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
@@ -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-center(Console)", () => {
test("asset-center get 缺少 --asset-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "get", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--asset-id|Missing required argument/i);
});
test("asset-center favorite 缺少 --id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--id|Missing required argument/i);
});
test("asset-center download 缺少 --id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "download", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--id|Missing required argument/i);
});
test("asset-center list --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"list",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
api?: string;
data?: { deleteStatus?: string };
}>(stdout);
expect(data.api).toContain("listModelGeneratedAsset");
expect(data.data?.deleteStatus).toBe("NORMAL");
});
test("asset-center stats --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"stats",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ api?: string }>(stdout);
expect(data.api).toContain("countModelGeneratedAsset");
});
test("asset-center storage --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"storage",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ api?: string }>(stdout);
expect(data.api).toContain("getStorageQuota");
});
test("【console】asset-center list 真实调用或鉴权失败优雅退出", async () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID;
const args = ["asset-center", "list", "--output", "json", "--page-size", "1"];
if (workspaceId) args.push("--workspace-id", workspaceId);
const result = await runCli(args);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.14.2",
"version": "1.14.1",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
@@ -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>` | positional,primary |
| `--asset-id` | 与 positional 二选一(positional 优先) |
| `--include-download-url` | |
| `--include-thumbnail` | |
| `--thumbnail-width` / `--thumbnail-height` | |
### 5.3 `bl asset delete`
| Flag | 说明 |
| ------------- | --------------------------------------------------------- |
| `--id` | array, required, max 100 |
| `--permanent` | switch → `deleteType: PERMANENT_DELETE`;默认 SOFT_DELETE |
### 5.4 `bl asset download`
| Flag | 说明 |
| ------- | ----------------------------------------------------- |
| `--id` | array, required |
| `--out` | 仅当 `--id` 恰好 1 个时有效;调用 `downloadFile` 落盘 |
### 5.5 `bl asset stats`
| Flag | 说明 |
| ------------------------------------- | ---------------------------------------------- |
| (无 filter) | 默认 `deleteStatus: NORMAL` |
| `--recycle-bin` | `deleteStatus: SOFT_DELETED` |
| `--sync-failed` | 额外查询 `syncOssDataStatus: SYNC_FAILED` 计数 |
| `--type` / `--model` / `--keyword` 等 | 与 list 相同筛选维度(可选) |
---
## 6. 风险与待确认项
### 6.1 P0 — 编码前必须对齐
| # | 问题 | 影响 | 建议动作 |
| --- | ------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- |
| 1 | Console API 注册名(`zeldaHttp.{service}./zelda/api/v1/bailian/asset/*`) | 无法调用 | `bl console call` spike;与后端确认 service 名 |
| 2 | `tenantId` / `mainAccountUid` 由谁填充 | 所有接口必填 | 确认网关是否从 session 自动注入;否则扩展 config 或新增解析 API |
### 6.2 P1 — 产品设计
| # | 问题 | 建议默认 |
| --- | -------------------- | ----------------------------------------------- |
| 4 | 关键词搜索范围 | 仅 `assetName`;后续可加 `--search-description` |
| 5 | PRD 只提 image/video | CLI 暴露 IMAGE/VIDEO/AUDIO(与 API 一致) |
| 6 | 下载行为 | 默认输出 URL;单 ID + `--out` 落盘 |
| 7 | 永久删除 | 提供 `--permanent`,help 注明不可恢复 |
| 8 | 服务未开通 | 不预检查;失败时透传服务端 message |
| 9 | 收藏命令形态 | 两个命令 `favorite` / `unfavorite`(语义清晰) |
---
## 7. 错误处理
遵循 [AGENTS.md](../../../../../../AGENTS.md) 错误边界:
| 场景 | 处理 |
| --------------------------------- | --------------------------------------------- |
| 缺 `--workspace-id` | `BailianError(GENERAL)` + hint |
| 缺 console token | authStage 抛 `BailianError(AUTH)` |
| flag 校验失败 | `UsageError` (exit 2) |
| HTTP 4xx/5xx / 业务 success=false | `BailianError(GENERAL)`,message **原样透传** |
| batch ID > 100 | `UsageError` |
Console 未登录参考 `mcp/list.ts`:检测 `BailianGateway.Login.NotLogined` 时 hint 指向 `bl auth login --console`。
---
## 8. 测试策略
新建 `packages/cli/tests/e2e/asset.e2e.test.ts`,遵循 [cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)。
### 8.1 不 skip 层
- `bl asset` 分组 help
- 各子命令 `--help`
- 缺参 → exit 2
### 8.2 Console skip 层(`isConsoleE2EReady()`)
- 各命令 `--dry-run` 输出 api + data
- 真实 `asset list` / `asset storage` 集成(需已开通资产中心的工作空间)
环境:`BAILIAN_E2E=1` + console `access_token` + `BAILIAN_WORKSPACE_ID`。
---
## 9. 注册与文档变更清单
| 文件 | 变更 |
| -------------------------------------------------- | ------------------- |
| `packages/commands/src/commands/asset-center/*.ts` | 新建 |
| `packages/commands/src/index.ts` | export |
| `packages/cli/src/commands.ts` | 注册 map |
| `packages/cli/tests/e2e/asset.e2e.test.ts` | 新建 |
| `skills/bailian-cli/reference/` | pre-commit 自动生成 |
| `README.md` / `README.zh.md` | 发版前补充命令一览 |
---
## 10. 分期实施
### Phase 1 — 核心资产(P0)
```
asset list | get | favorite | unfavorite | delete | restore | download | stats | storage
```
**前置:** §6.1 #1 #2 确认。
### Phase 2+ — 可选扩展
```
asset models list | service status | service enable/disable
```
> OSS 转存相关命令(`asset-center oss *` / `transfer list`)已取消,不再排期。
## 11. 参考
- 命令注册:[docs/agents/command-add-remove.md](../../../../../../docs/agents/command-add-remove.md)
- E2E 规范:[docs/agents/cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)
- Console 命令样例:`packages/commands/src/commands/app/list.ts`
- 游标/表格:`packages/commands/src/commands/quota/list.ts`
- workspace 必填:`packages/commands/src/commands/usage/stats.ts`
- 文件落盘:`packages/commands/src/commands/video/download.ts`
@@ -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`;
}
+9 -17
View File
@@ -2,6 +2,7 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -27,31 +28,22 @@ const INSTALL_CONCURRENCY = 3;
export default defineCommand({
description: "Install skills from the Bailian skill registry into local agents",
auth: "none",
usageArgs: "--all | --name <name,...>",
usageArgs: "--name <all|name,...>",
flags: {
all: {
type: "switch",
description: "Install all skills from the registry",
},
name: {
type: "string",
valueHint: "<name,...>",
description: "Comma-separated skill names to install",
valueHint: "<all|name,...>",
description: "Skills to install: all or comma-separated skill names",
required: true,
},
},
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
if (!flags.all && !flags.name)
return "Specify --all to install everything or --name <name,...> for specific skills";
return undefined;
},
exampleArgs: ["--all", "--name spark-video,bailian-model-recommend"],
exampleArgs: ["--name all", "--name spark-video,bailian-model-recommend"],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, false);
const index = await fetchSkillsIndex();
const remoteNames = Object.keys(index.skills);
const parsed = ctx.flags.all ? "all" : parseSkillNames(ctx.flags.name, false);
const names = parsed === "all" ? remoteNames : parsed;
const names = requested === "all" ? remoteNames : requested;
const lock = readSkillLock();
const agents = detectInstalledAgents();
@@ -1,107 +0,0 @@
import {
BailianError,
ExitCode,
defineCommand,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
installSkillWithFanout,
readSkillLock,
runWithConcurrency,
writeSkillLock,
} from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
interface InitOutcome {
name: string;
status: "installed" | "failed";
publishedAt?: string;
agents?: string[];
reason?: string;
}
/** Prefix used to identify first-party Bailian skills in the registry. */
const BAILIAN_PREFIX = "bailian-";
/** Max number of skills downloading/installing at the same time. */
const INIT_CONCURRENCY = 3;
export default defineCommand({
description: "Install all bailian-* skills (one-shot bootstrap for new environments)",
auth: "none",
usageArgs: "",
exampleArgs: [""],
notes: [
"Fetches the registry index and installs every skill whose name starts with bailian-",
"Equivalent to: bl skill add --all (filtered to bailian-* skills)",
],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const index = await fetchSkillsIndex();
// Discover all bailian-* skills from the live registry index
const names = Object.keys(index.skills).filter((name) => name.startsWith(BAILIAN_PREFIX));
const lock = readSkillLock();
const agents = detectInstalledAgents();
const tasks = names.map((name) => async (): Promise<InitOutcome> => {
const entry = index.skills[name];
try {
const record = await installSkillWithFanout(
name,
entry,
agents,
lock.skills[name]?.links ?? [],
);
lock.skills[name] = record.lockEntry;
return {
name,
status: "installed",
publishedAt: entry.publishedAt,
agents: record.linkedAgents,
};
} catch (err) {
return {
name,
status: "failed",
reason: err instanceof Error ? err.message : String(err),
};
}
});
const results = await runWithConcurrency(tasks, INIT_CONCURRENCY);
writeSkillLock(lock);
if (format === "json") {
emitResult(
{
registry: getSkillRegistryBaseUrl(),
agents: agents.map((agent) => agent.id),
skills: results,
},
format,
);
} else if (results.length === 0) {
emitBare("No bailian-* skills found in the registry.");
} else {
const rows = results.map((result) => [
result.name,
result.status,
result.publishedAt ? result.publishedAt.slice(0, 10) : "-",
result.status === "installed" ? result.agents?.join(", ") || "-" : (result.reason ?? "-"),
]);
for (const line of formatTable(["NAME", "STATUS", "PUBLISHED", "AGENTS / REASON"], rows)) {
emitBare(line);
}
}
const failed = results.filter((result) => result.status === "failed");
if (failed.length > 0) {
throw new BailianError(
`${failed.length}/${results.length} skill(s) failed to install`,
ExitCode.GENERAL,
"Check the reason for failed skills in the output; network failures can be retried with bl skill init",
);
}
},
});
+2 -1
View File
@@ -1,5 +1,6 @@
import {
defineCommand,
detectOutputFormat,
computeSkillStatuses,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -23,7 +24,7 @@ export default defineCommand({
"STATUS: installed | outdated | not-installed | missing (lock has it, dir deleted) | untracked (dir exists, not managed)",
],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const format = detectOutputFormat(ctx.settings.output);
// Three-way reconciliation: live remote index × skill-lock.json (installation facts) × disk
const index = await fetchSkillsIndex();
const lock = readSkillLock();
@@ -2,6 +2,7 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
listSkillDirsOnDisk,
parseSkillNames,
readSkillLock,
@@ -33,7 +34,7 @@ export default defineCommand({
exampleArgs: ["--name spark-video", "--name all"],
async run(ctx) {
// Purely local operation: no remote access, works offline
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, false);
const lock = readSkillLock();
const names = requested === "all" ? Object.keys(lock.skills) : requested;
+8 -15
View File
@@ -2,6 +2,7 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fanOutSkillToAgents,
fetchSkillsIndex,
@@ -28,27 +29,19 @@ const UPDATE_CONCURRENCY = 3;
export default defineCommand({
description: "Update installed skills to the latest registry versions",
auth: "none",
usageArgs: "[--all] [--name <name,...>]",
usageArgs: "[--name <all|name,...>]",
flags: {
all: {
type: "switch",
description: "Update all installed skills (default when neither --all nor --name is given)",
},
name: {
type: "string",
valueHint: "<name,...>",
description: "Comma-separated skill names to update (must be already installed)",
valueHint: "<all|name,...>",
description:
"Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills)",
},
},
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
return undefined;
},
exampleArgs: ["", "--all", "--name spark-video"],
exampleArgs: ["", "--name spark-video"],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const updateAll = ctx.flags.all || !ctx.flags.name;
const requested = updateAll ? "all" : parseSkillNames(ctx.flags.name, false);
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, true);
const index = await fetchSkillsIndex();
const lock = readSkillLock();
const disk = new Set(listSkillDirsOnDisk());
+8 -1
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";
@@ -117,4 +125,3 @@ export { default as skillAdd } from "./commands/skill/add.ts";
export { default as skillUpdate } from "./commands/skill/update.ts";
export { default as skillRemove } from "./commands/skill/remove.ts";
export { default as skillList } from "./commands/skill/list.ts";
export { default as skillInit } from "./commands/skill/init.ts";
+15 -23
View File
@@ -17,14 +17,12 @@ describe("e2e: skill", () => {
test("skill add --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
test("skill update --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
@@ -39,37 +37,18 @@ describe("e2e: skill", () => {
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|registry/i);
});
test("skill init --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/bailian/i);
});
});
// Local-only cases: auth "none" + validation happens before any network access, no gating needed
describe("e2e: skill (local, no credentials)", () => {
test("skill add without --all or --name errors as usage error (2)", async () => {
test("skill add without --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|Usage:/i);
});
test("skill add with both --all and --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--all",
"--name",
"spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|either/i);
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
});
test("skill remove without --name errors as usage error (2)", async () => {
@@ -82,6 +61,19 @@ describe("e2e: skill (local, no credentials)", () => {
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
});
test("skill add rejects mixing all with specific names (2)", async () => {
// parseSkillNames throws UsageError before fetchSkillsIndex — offline-safe
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--name",
"all,spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/all/i);
});
test("skill remove of a not-installed skill fails with reason (1)", async () => {
const configDir = makeTempConfigDir();
const { stdout, exitCode } = await runCommandE2e(
@@ -165,7 +165,6 @@ export const SKILL_ROUTES: E2eRouteExports = {
"skill update": "skillUpdate",
"skill remove": "skillRemove",
"skill list": "skillList",
"skill init": "skillInit",
};
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.14.2",
"version": "1.14.1",
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.14.2",
"version": "1.14.1",
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
"keywords": [
"alibaba-cloud",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-runtime",
"version": "1.14.2",
"version": "1.14.1",
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.14.2"
version: "1.14.1"
requires:
bins: ["bl"]
description: >-
@@ -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 -81
View File
@@ -9,90 +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 init` | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
| `bl skill list` | List registry skills and diff against local installs | [skill.md](skill.md) |
| `bl skill remove` | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
| `bl skill update` | Update installed skills to the latest registry versions | [skill.md](skill.md) |
| `bl text chat` | Send a chat completion (OpenAI compatible, DashScope) | [text.md](text.md) |
| `bl token-plan add-member` | Add a member to a Token Plan organization | [token-plan.md](token-plan.md) |
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members | [token-plan.md](token-plan.md) |
| `bl token-plan create-key` | Create a Token Plan API key for a seat | [token-plan.md](token-plan.md) |
| `bl token-plan list-seats` | List Token Plan subscription seat details | [token-plan.md](token-plan.md) |
| `bl update` | Update the CLI to the latest or a specified version | [update.md](update.md) |
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
| `bl usage stats` | Query model usage statistics | [usage.md](usage.md) |
| `bl usage summary` | Show a unified usage summary: free-tier quota and recent usage overview | [usage.md](usage.md) |
| `bl workspace init` | Initialize Bailian workspace and activate postpaid services | [workspace.md](workspace.md) |
| `bl workspace list` | List all workspaces | [workspace.md](workspace.md) |
| Command | 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`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
| Group | Commands | Reference |
| -------------- | --------------------------------------------------------------------------------- | ---------------------------------- |
| `advisor` | `recommend` | [advisor.md](advisor.md) |
| `app` | `call`, `list` | [app.md](app.md) |
| `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
+15 -45
View File
@@ -7,13 +7,12 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ----------------- | ----------------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
| Command | Description |
| ----------------- | ---------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
## Command details
@@ -23,48 +22,24 @@ Index: [index.md](index.md)
| --------------- | ---------------------------------------------------------------- |
| **Name** | `skill add` |
| **Description** | Install skills from the Bailian skill registry into local agents |
| **Usage** | `bl skill add --all \| --name <name,...>` |
| **Usage** | `bl skill add --name <all\|name,...>` |
#### Flags
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------- |
| `--all` | switch | no | Install all skills from the registry |
| `--name <name,...>` | string | no | Comma-separated skill names to install |
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------- |
| `--name <all\|name,...>` | string | yes | Skills to install: all or comma-separated skill names |
#### Examples
```bash
bl skill add --all
bl skill add --name all
```
```bash
bl skill add --name spark-video,bailian-model-recommend
```
### `bl skill init`
| Field | Value |
| --------------- | ----------------------------------------------------------------------- |
| **Name** | `skill init` |
| **Description** | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| **Usage** | `bl skill init` |
#### Flags
_No command-specific flags._
#### Notes
- Fetches the registry index and installs every skill whose name starts with bailian-
- Equivalent to: bl skill add --all (filtered to bailian-\* skills)
#### Examples
```bash
bl skill init
```
### `bl skill list`
| Field | Value |
@@ -121,14 +96,13 @@ bl skill remove --name all
| --------------- | ------------------------------------------------------- |
| **Name** | `skill update` |
| **Description** | Update installed skills to the latest registry versions |
| **Usage** | `bl skill update [--all] [--name <name,...>]` |
| **Usage** | `bl skill update [--name <all\|name,...>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `--all` | switch | no | Update all installed skills (default when neither --all nor --name is given) |
| `--name <name,...>` | string | no | Comma-separated skill names to update (must be already installed) |
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
| `--name <all\|name,...>` | string | no | Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills) |
#### Examples
@@ -136,10 +110,6 @@ bl skill remove --name all
bl skill update
```
```bash
bl skill update --all
```
```bash
bl skill update --name spark-video
```
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "1.14.2"
version: "1.14.1"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "1.14.2"
version: "1.14.1"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-managed-agent
metadata:
version: "1.14.2"
version: "1.14.1"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "1.14.2"
version: "1.14.1"
requires:
bins: ["bl"]
description: >-
+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);