Compare commits

..

1 Commits

Author SHA1 Message Date
chenanran555 ac48c3ec23 faet(agent): add sync and migrate commands 2026-08-10 20:55:02 +08:00
90 changed files with 2272 additions and 6808 deletions
-4
View File
@@ -37,7 +37,6 @@ tools/generated
.claude/settings.local.json
.claude/scheduled_tasks.lock
.cursor/
.qoder/
.qwen/
.qoder
.playwright-mcp/
@@ -52,6 +51,3 @@ packages/cli/scene/**/outputs/
# Local scratch / plan drafts (never commit)
.scratch/
# pnpm pack output
*.tgz
-11
View File
@@ -6,17 +6,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.14.1] - 2026-08-05
### Added
- **Focused Bailian Skills** — `npx skills add modelstudioai/cli --all -g` now installs dedicated skills for media generation, fine-tuning, Managed Agent, and shared execution rules, improving task routing while reducing irrelevant context.
### Changed
- **Default image model upgraded to Qwen-Image 3.0** — image generation, image editing, pipelines, the config UI, and related documentation now default to `qwen-image-3.0` for API Key users.
- **Broader coding-agent compatibility** — Skill installation and updates now detect more coding agents, preserve existing installation links, and automatically backfill skills into newly detected agents.
## [1.14.0] - 2026-08-04
### Added
-11
View File
@@ -6,17 +6,6 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.14.1] - 2026-08-05
### 新增
- **百炼 Skill 按领域拆分** —— 通过 `npx skills add modelstudioai/cli --all -g` 可统一安装图片与视频生成、模型微调、Managed Agent 和共享执行协议等专用 Skill,提升任务路由准确性并减少无关上下文。
### 变更
- **默认图片模型升级至 Qwen-Image 3.0** —— 普通 API Key 用户的图片生成、图片编辑、Pipeline、配置 UI 和相关文档现在默认使用 `qwen-image-3.0`。
- **扩展 Coding Agent 兼容范围** —— Skill 安装与更新现在能够识别更多 Coding Agent,保留已有安装链接,并自动将 Skill 补充到新识别的 Agent。
## [1.14.0] - 2026-08-04
### 新增
+137 -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,28 @@ _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 2.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`)
- **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 +56,129 @@ _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
# 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 +186,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`). 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 +214,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 +237,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.
+138 -73
View File
@@ -22,16 +22,28 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
## 功能特性
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 2.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`)
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -41,80 +53,130 @@ _专为 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 # 查看提额历史记录
# 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 +184,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`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```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 +212,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 +237,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)。
+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 优先级,实际不生效
+137 -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,28 @@ _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 2.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`)
- **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 +56,129 @@ _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
# 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 +186,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`). 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 +214,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 +237,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.
+138 -73
View File
@@ -22,16 +22,28 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
## 功能特性
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 2.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`)
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -41,80 +53,130 @@ _专为 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 # 查看提额历史记录
# 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 +184,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`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```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 +212,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 +237,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.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+4 -16
View File
@@ -30,10 +30,6 @@ import {
memoryDelete,
memoryProfileCreate,
memoryProfileGet,
memoryProfileList,
memoryProfileDetail,
memoryProfileUpdate,
memoryProfileDelete,
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
@@ -88,8 +84,6 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
tokenPlanPersonalUsage,
tokenPlanPersonalKey,
workspaceInit,
pluginInstall,
pluginLink,
@@ -99,12 +93,12 @@ import {
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentSync,
managedAgentMigrate,
managedAgentPlan,
managedAgentApply,
managedAgentRun,
managedAgentDestroy,
managedAgentStateList,
managedAgentStateShow,
@@ -156,10 +150,6 @@ export const commands: Record<string, AnyCommand> = {
"memory delete": memoryDelete,
"memory profile create": memoryProfileCreate,
"memory profile get": memoryProfileGet,
"memory profile list": memoryProfileList,
"memory profile detail": memoryProfileDetail,
"memory profile update": memoryProfileUpdate,
"memory profile delete": memoryProfileDelete,
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
"knowledge chat": knowledgeChat,
@@ -214,8 +204,6 @@ export const commands: Record<string, AnyCommand> = {
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"token-plan personal-usage": tokenPlanPersonalUsage,
"token-plan personal-key": tokenPlanPersonalKey,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
@@ -225,12 +213,12 @@ 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 sync": managedAgentSync,
"managed-agent migrate": managedAgentMigrate,
"managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply,
"managed-agent run": managedAgentRun,
"managed-agent destroy": managedAgentDestroy,
"managed-agent state list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.14.2",
"version": "1.14.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
@@ -61,7 +61,7 @@ export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry"]);
// without persisting a value that would pin the model.
export const UI_MODEL_DEFAULTS: Record<string, string> = {
default_text_model: "qwen3.8-max",
default_image_model: "qwen-image-3.0",
default_image_model: "qwen-image-2.0",
default_video_model: "happyhorse-1.1-t2v",
default_speech_model: "cosyvoice-v3-flash",
default_omni_model: "qwen3.5-omni-plus",
@@ -86,8 +86,7 @@ export const UI_MODEL_CATALOG: Record<string, ModelOption[]> = {
{ id: "qwen3.6-flash", role: "fast · advisor intent" },
],
default_image_model: [
{ id: "qwen-image-3.0", role: "image/generate default · sync" },
{ id: "qwen-image-2.0", role: "image/generate · sync" },
{ id: "qwen-image-2.0", role: "image/generate default · sync" },
{ id: "qwen-image-max", role: "image/generate · sync" },
{ id: "qwen-image-edit-2.0", role: "image/edit · sync" },
{ id: "wanx2.x", role: "image/generate · async series" },
@@ -23,7 +23,7 @@ export default defineCommand({
"--file photo.jpg --model qwen3-vl-plus",
"--file video.mp4 --model wan2.1-t2v-plus",
"--file audio.wav --model qwen3-asr-flash",
"--file cat.png --model qwen-image-3.0",
"--file cat.png --model qwen-image-2.0",
],
async run(ctx) {
const { settings, flags } = ctx;
+2 -2
View File
@@ -47,7 +47,7 @@ const EDIT_FLAGS = {
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-3.0)",
description: "Model ID (default: qwen-image-2.0)",
},
size: {
type: "string",
@@ -123,7 +123,7 @@ export default defineCommand({
}
const prompt = flags.prompt;
const model = flags.model || settings.defaultImageModel || "qwen-image-3.0";
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
const route = resolveImageEditApi(model);
// Auto-upload local files (resolve all images in parallel)
@@ -35,7 +35,7 @@ const GENERATE_FLAGS = {
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-3.0)",
description: "Model ID (default: qwen-image-2.0)",
},
size: {
type: "string",
@@ -105,7 +105,7 @@ export default defineCommand({
const { settings, flags } = ctx;
const prompt = flags.prompt;
const model = flags.model || settings.defaultImageModel || "qwen-image-3.0";
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
const route = resolveImageGenerateApi(model);
const defaultSize = "1:1";
const sizeInput = flags.size || defaultSize;
@@ -50,7 +50,6 @@ export interface CredentialHost {
*/
export const CREDENTIALS_NOTE = [
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
"The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.",
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
];
@@ -86,19 +85,13 @@ export function prepareProviderEnv(): void {
* the block references them and the interpolated value is empty (a literal in
* agents.yaml is respected).
*
* `base_url` is composed from the workspace when one is known — block
* `workspace_id` (agents.yaml literal or interpolated `${BAILIAN_WORKSPACE_ID}`)
* first, then bl's configured `workspace_id` — because agentstudio is served
* only on the workspace-scoped host; the bare model-domain origin 404s it
* (managed-agents API overview: `https://{workspace_id}.cn-beijing.maas.
* aliyuncs.com/api/v1/agentstudio`, region cn-beijing only). Only with no
* workspace at all does the model-domain origin get {@link AGENTSTUDIO_API_PATH}
* suffixed. A value already ending in the suffix is left as-is. base_url is
* filled even without a credential — `client.baseUrl` is readable
* credential-less — so offline commands (which skip the credential assert)
* still satisfy the SDK's "workspace_id or base_url" schema. With no
* credential the `api_key` is left untouched: online commands reject it via
* {@link assertProviderCredentials}.
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
* paths onto it verbatim; a value already ending in the suffix is left as-is.
* It is filled even without a credential — `client.baseUrl` is readable
* credential-less (defaults to the CLI's model-domain base URL) — so offline
* commands (which skip the credential assert) still satisfy the SDK's
* "workspace_id or base_url" schema. With no credential the `api_key` is left
* untouched: online commands reject it via {@link assertProviderCredentials}.
*/
export function injectProviderCredentials(
providers: Record<string, unknown>,
@@ -110,27 +103,16 @@ export function injectProviderCredentials(
const cred = host.client.exportApiCredential();
if (cred) block.api_key = cred.token;
if ("workspace_id" in block && !block.workspace_id) {
// agents.yaml interpolation already replaced `${BAILIAN_WORKSPACE_ID}` in
// file-based flows; the inline runtime passes an object config that never
// interpolates, so read the env var here too (prepareProviderEnv
// placeholders it to "" when unset). bl's configured workspace_id is the
// last resort.
block.workspace_id =
process.env.BAILIAN_WORKSPACE_ID?.trim() || host.settings.workspaceId || "";
}
if ("base_url" in block && !block.base_url) {
const workspaceId = typeof block.workspace_id === "string" ? block.workspace_id.trim() : "";
if (workspaceId) {
block.base_url = `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${AGENTSTUDIO_API_PATH}`;
} else {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
}
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
}
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
block.workspace_id = host.settings.workspaceId;
}
}
@@ -1,124 +0,0 @@
import { mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import {
type BackendRuntimeInput,
LocalFileStateBackend,
resolveProjectConfigFromObject,
} from "@openagentpack/sdk";
import { getConfigDir } from "bailian-cli-core";
import {
assertProviderCredentials,
type CredentialHost,
injectProviderCredentials,
normalizeInterpolatedProviderBlocks,
prepareProviderEnv,
scrubCredentialEnv,
} from "./credentials.ts";
import { type HostContext, installSdkTransport } from "./transport.ts";
/** Default agent identity `bl managed-agent run` materializes and reuses. */
export const DEFAULT_INLINE_AGENT = "dsh-remote-runner";
/** Default model for the materialized agent. */
export const DEFAULT_INLINE_MODEL = "qwen3.8-max";
/** Default role when the caller supplies no `--instructions`. */
export const DEFAULT_INLINE_INSTRUCTIONS = "You are a helpful assistant. Complete the task.";
/** Environment name declared in the inline config; one cloud env per agent. */
const INLINE_ENVIRONMENT = "cloud";
export interface InlineAgentOptions {
agentName: string;
instructions: string;
model: string;
/** Override the persisted state location (defaults under the bl config dir). */
statePath?: string;
}
/**
* Slugify an agent name into a filesystem- and project-id-safe token. The state
* for each distinct agent lives in its own directory so repeat runs reuse the
* same materialized remote agent.
*/
function slugify(agentName: string): string {
const slug = agentName
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.length > 0 ? slug : "agent";
}
/** Where a materialized agent's state is persisted (not the user's cwd). */
export function inlineStatePath(agentName: string): string {
return join(getConfigDir(), "managed-agent", slugify(agentName), "state.json");
}
/**
* The minimal in-memory project config that materializes into one cloud agent.
* `providers.bailian` carries empty `api_key`/`base_url`/`workspace_id`
* placeholders so {@link injectProviderCredentials} fills them from bl's auth
* chain and workspace sources (it only writes fields the block already
* declares). `workspace_id` lets injection compose the workspace-scoped
* agentstudio host instead of the model-domain origin.
*/
export function buildInlineConfig(opts: InlineAgentOptions): Record<string, unknown> {
return {
version: "1",
providers: {
bailian: { api_key: "", base_url: "", workspace_id: "" },
},
defaults: { provider: "bailian" },
environments: {
[INLINE_ENVIRONMENT]: {
description: "Bailian CLI cloud environment",
config: { type: "cloud", networking: { type: "unrestricted" } },
},
},
agents: {
[opts.agentName]: {
description: opts.agentName,
model: opts.model,
instructions: opts.instructions,
environment: INLINE_ENVIRONMENT,
provider: "bailian",
},
},
};
}
/**
* Build the `BackendRuntimeInput` shared by ensure (`syncAgentResourcesWith
* StateBackend`) and run (`readProjectRuntime` + `startSessionRun`). Mirrors the
* credential spine of {@link buildAgentRuntime} but sources config from an
* in-memory object instead of a file, so no `agents.yaml` or `apply` is required.
*/
export async function buildInlineBackendInput(
host: HostContext & CredentialHost,
opts: InlineAgentOptions,
): Promise<BackendRuntimeInput> {
installSdkTransport(host);
prepareProviderEnv();
const rawConfig = buildInlineConfig(opts);
const { config, projectName } = await resolveProjectConfigFromObject(rawConfig, {
projectName: slugify(opts.agentName),
});
normalizeInterpolatedProviderBlocks(config.providers);
injectProviderCredentials(config.providers, host);
scrubCredentialEnv();
assertProviderCredentials(config.providers);
const statePath = opts.statePath ?? inlineStatePath(opts.agentName);
mkdirSync(dirname(statePath), { recursive: true });
const stateBackend = new LocalFileStateBackend({ statePath });
return {
projectName,
config,
stateBackend,
stateScope: { projectId: slugify(opts.agentName) },
providers: config.providers,
};
}
@@ -0,0 +1,221 @@
import {
listCloudEnvironments,
listCloudVaults,
listFiles,
listSkills,
type ProjectRuntimeContext,
type SyncProjectResult,
} from "@openagentpack/sdk";
import { BailianError, ExitCode } from "bailian-cli-core";
/** Resource types the SDK can reverse-export (its syncable whitelist). */
export const SYNCABLE_TYPES = ["environment", "vault", "file", "skill", "agent"] as const;
export type SyncableType = (typeof SYNCABLE_TYPES)[number];
/**
* Types selectable by remote ID through the shared resolution below. Agents are
* excluded: their synced yaml keys ARE remote IDs, so the sync command narrows
* them with a direct key hit (plus referenced-skill handling) instead.
*/
export type SelectableResourceType = Exclude<SyncableType, "agent">;
/** Maps a syncable type to its top-level group key in the synced config. */
const GROUP_KEY: Record<SyncableType, string> = {
environment: "environments",
vault: "vaults",
file: "files",
skill: "skills",
agent: "agents",
};
/** Remote id lookup outcome: human label + optional `agents.resource` yaml key tag. */
interface LocatedRemote {
label?: string;
taggedKey?: string;
}
/**
* Narrow one resource group of a synced config to the single entry identified
* by its remote ID; returns the kept yaml key.
*
* Group keys are only guaranteed to be remote IDs for agents; the other types
* derive their key from the `agents.resource` metadata tag or a display-name
* slug. Resolution therefore tries, in order: direct key hit → remote lookup
* by ID (provider list API) → metadata-tag key → label match against the
* exported declarations. Misses and ambiguity fail loudly instead of guessing.
*/
export async function narrowGroupToRemoteId(
runtime: ProjectRuntimeContext,
provider: string,
result: SyncProjectResult,
type: SelectableResourceType,
remoteId: string,
): Promise<string> {
const groupKey = GROUP_KEY[type];
const group = (result.config[groupKey] ?? {}) as Record<string, Record<string, unknown>>;
let keptKey: string;
if (remoteId in group) {
keptKey = remoteId;
} else {
const located = await locateRemote(runtime, provider, type, remoteId);
const resolved =
located.taggedKey && located.taggedKey in group
? located.taggedKey
: matchByLabel(type, group, located.label);
if (!resolved) {
throw new BailianError(
`Remote ${type} '${remoteId}'${located.label ? ` (${located.label})` : ""} has no matching entry in the synced output.`,
ExitCode.GENERAL,
"The resource may be archived or renamed; run a full sync (without the id flag) to inspect the exported keys.",
);
}
keptKey = resolved;
}
result.config[groupKey] = { [keptKey]: group[keptKey]! };
if (type in result.counts) result.counts[type] = 1;
if (type === "skill" && result.skillFiles) {
for (const skillName of result.skillFiles.keys()) {
if (skillName !== keptKey) result.skillFiles.delete(skillName);
}
}
return keptKey;
}
/** Look a remote resource up by ID via the provider's list API. */
async function locateRemote(
runtime: ProjectRuntimeContext,
provider: string,
type: SelectableResourceType,
remoteId: string,
): Promise<LocatedRemote> {
if (type === "environment") {
const environments = await listCloudEnvironments(runtime, { provider });
const hit = environments.find((environment) => environment.id === remoteId);
if (!hit) {
throw notFound(
type,
remoteId,
environments.map((environment) => formatCandidate(environment.id, environment.name)),
);
}
return { label: hit.name, taggedKey: hit.metadata?.["agents.resource"] };
}
if (type === "vault") {
const vaults = await listCloudVaults(runtime, { provider });
const hit = vaults.find((vault) => vault.id === remoteId);
if (!hit) {
throw notFound(
type,
remoteId,
vaults.map((vault) => formatCandidate(vault.id, vault.display_name)),
);
}
return { label: hit.display_name, taggedKey: hit.metadata?.["agents.resource"] };
}
if (type === "file") {
const files = await listFiles(runtime, { provider });
const hit = files.find((fileInfo) => fileInfo.id === remoteId);
if (!hit) {
throw notFound(
type,
remoteId,
files.map((fileInfo) => formatCandidate(fileInfo.id, fileInfo.filename)),
);
}
return { label: hit.filename };
}
// Synced skills come from the workspace's custom catalog (the raw /skills listing).
const skills = await listSkills(runtime, { provider, source: "custom" });
const hit = skills.find((skill) => skill.id === remoteId);
if (!hit) {
throw notFound(
type,
remoteId,
skills.map((skill) => formatCandidate(skill.id, skill.name)),
);
}
return { label: hit.name };
}
/**
* Batch-resolve remote skill IDs to synced skills-group keys via the custom
* skill catalog: look each ID up for its display name, then match group keys
* on normalized label (skills keys are display-name slugs). IDs that cannot be
* resolved — or whose label matches more than one key — come back as
* unmatched for the caller to surface. One catalog call serves the whole batch.
*/
export async function resolveSkillKeysByIds(
runtime: ProjectRuntimeContext,
provider: string,
group: Record<string, Record<string, unknown>>,
remoteIds: string[],
): Promise<{ resolved: Map<string, string>; unmatched: string[] }> {
const resolved = new Map<string, string>();
const unmatched: string[] = [];
if (remoteIds.length === 0) return { resolved, unmatched };
const catalog = await listSkills(runtime, { provider, source: "custom" });
for (const remoteId of remoteIds) {
const hit = catalog.find((skill) => skill.id === remoteId);
const matches = hit
? Object.keys(group).filter((key) => normalizeLabel(key) === normalizeLabel(hit.name))
: [];
if (matches.length === 1) {
resolved.set(remoteId, matches[0]!);
} else {
unmatched.push(remoteId);
}
}
return { resolved, unmatched };
}
/**
* Match a group entry by the remote resource's human label. Vaults and files
* carry the label verbatim in their exported decl; environment and skill keys
* are display-name slugs, so those compare on normalized alphanumerics.
*/
function matchByLabel(
type: SelectableResourceType,
group: Record<string, Record<string, unknown>>,
label: string | undefined,
): string | undefined {
if (!label) return undefined;
const matches = Object.entries(group).filter(([key, decl]) => {
if (type === "vault") return decl.display_name === label;
if (type === "file") return decl.name === label || decl.source === label;
return normalizeLabel(key) === normalizeLabel(label);
});
if (matches.length > 1) {
throw new BailianError(
`Multiple synced ${type} entries match '${label}': ${matches.map(([key]) => key).join(", ")}.`,
ExitCode.GENERAL,
"Run a full sync (without the id flag) and narrow the output manually.",
);
}
return matches[0]?.[0];
}
/** Compare display labels and slug-derived keys on lowercase alphanumerics only. */
function normalizeLabel(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
}
function notFound(type: string, remoteId: string, candidates: string[]): BailianError {
const shown = candidates.slice(0, 20);
const suffix =
candidates.length > shown.length ? `, … ${candidates.length - shown.length} more` : "";
return new BailianError(
`${type} '${remoteId}' not found on the remote workspace.`,
ExitCode.USAGE,
candidates.length > 0
? `Available: ${shown.join(", ")}${suffix}.`
: `The workspace has no syncable ${type} resources.`,
);
}
function formatCandidate(id: string, label?: string): string {
return label && label !== id ? `${id} (${label})` : id;
}
@@ -0,0 +1,123 @@
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { migrateConfig } from "@openagentpack/sdk";
import { parse as parseYaml } from "yaml";
import { withAgentErrors } from "./_engine/errors.ts";
/** bl's migrate is bailian-only: the merge target must resolve to bailian. */
const MIGRATE_PROVIDER = "bailian";
const MIGRATE_FLAGS = {
from: {
type: "string",
valueHint: "<path>",
description: "Synced config to migrate from (default: agents.synced.yaml)",
},
to: {
type: "string",
valueHint: "<path>",
description: "Target agents.yaml to merge into (default: agents.yaml)",
},
} satisfies FlagsDef;
export default defineCommand({
description: "Merge a synced config into a bailian agents.yaml",
auth: "apiKey",
usageArgs: "[--from <path>] [--to <path>]",
flags: MIGRATE_FLAGS,
exampleArgs: ["", "--from agents.synced.yaml --to agents.yaml"],
notes: [
"The merge itself runs against local files; bl's unified apiKey gate still applies — login via `bl auth login`, pass --api-key, or set DASHSCOPE_API_KEY.",
"Only a bailian-target agents.yaml is supported: migrated resources are re-pointed to provider bailian, with models/tools/environments normalized to Bailian-supported values.",
"Resources whose YAML key already exists in the target are skipped, never overwritten.",
"Run `bl managed-agent plan` afterwards to review the merged config before apply.",
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const fromPath = flags.from ?? "agents.synced.yaml";
const toPath = flags.to ?? "agents.yaml";
// Aligned with the other managed-agent commands: dry-run short-circuits
// first and only echoes the planned action — no file I/O, no validation.
if (settings.dryRun) {
emitResult({ would_migrate: { from: fromPath, to: toPath } }, format);
return;
}
const result = await withAgentErrors(async () => {
await assertBailianTarget(toPath);
return migrateConfig({ fromPath, toPath });
});
await writeFile(toPath, result.yaml, "utf8");
if (format === "json") {
emitResult(
{ migrated: toPath, from: fromPath, added: result.added, skipped: result.skipped },
format,
);
return;
}
const addedParts = Object.entries(result.added).map(([group, count]) => `${count} ${group}`);
const skippedParts = Object.entries(result.skipped).map(
([group, count]) => `${count} ${group}`,
);
if (addedParts.length > 0) {
emitBare(`Migrated ${addedParts.join(", ")} into ${toPath}.`);
} else {
emitBare("No new resources to migrate (all already exist in target).");
}
if (skippedParts.length > 0) {
emitBare(`Skipped (already exist): ${skippedParts.join(", ")}.`);
}
if (addedParts.length > 0) {
// `bl` prefix is safe: agent commands ship on `bl` only.
emitBare("Next: run `bl managed-agent plan` to review the merged config.");
}
},
});
/**
* Enforce the bailian-only contract before merging: the target file must exist
* and its provider (defaults.provider, else the first providers key) must be
* bailian. An undeterminable provider is left to the SDK's own error.
*/
async function assertBailianTarget(toPath: string): Promise<void> {
if (!existsSync(toPath)) {
throw new BailianError(
`Target file '${toPath}' not found.`,
ExitCode.USAGE,
// `bl` prefix is safe: agent commands ship on `bl` only.
"Create it first with `bl managed-agent init`, then re-run migrate.",
);
}
const parsed: unknown = parseYaml(await readFile(toPath, "utf8"));
if (!parsed || typeof parsed !== "object") return;
const config = parsed as Record<string, unknown>;
const defaults = config.defaults as Record<string, unknown> | undefined;
const providers = config.providers as Record<string, unknown> | undefined;
const targetProvider =
typeof defaults?.provider === "string" && defaults.provider
? defaults.provider
: Object.keys(providers ?? {})[0];
if (targetProvider && targetProvider !== MIGRATE_PROVIDER) {
throw new BailianError(
`Target provider '${targetProvider}' is not supported: migrate only targets the ${MIGRATE_PROVIDER} provider.`,
ExitCode.USAGE,
`Set defaults.provider to ${MIGRATE_PROVIDER} (or make ${MIGRATE_PROVIDER} the providers block) in '${toPath}'.`,
);
}
}
@@ -1,137 +0,0 @@
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import {
readProjectRuntime,
startSessionRun,
startSessionRunPolling,
syncAgentResourcesWithStateBackend,
} from "@openagentpack/sdk";
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import {
buildInlineBackendInput,
DEFAULT_INLINE_AGENT,
DEFAULT_INLINE_INSTRUCTIONS,
DEFAULT_INLINE_MODEL,
} from "./_engine/inline-runtime.ts";
import { renderCollectedEvents, streamAndRenderEvents } from "./_engine/session-render.ts";
const RUN_FLAGS = {
prompt: {
type: "string",
valueHint: "<text>",
description: "Task to run (required)",
required: true,
},
instructions: {
type: "string",
valueHint: "<text>",
description: "Role/system instructions for the remote agent (default: generic assistant)",
},
model: {
type: "string",
valueHint: "<id>",
description: `Model for the remote agent (default: ${DEFAULT_INLINE_MODEL})`,
},
agent: {
type: "string",
valueHint: "<name>",
description: `Agent identity to create/reuse (default: ${DEFAULT_INLINE_AGENT})`,
},
noStream: {
type: "switch",
description: "Use polling instead of SSE streaming",
},
} satisfies FlagsDef;
export default defineCommand({
description: "Provision (if needed) a cloud agent and run a task in one step",
auth: "apiKey",
usageArgs: "--prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]",
flags: RUN_FLAGS,
exampleArgs: [
'--prompt "Summarize the latest AI news"',
'--prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max',
],
notes: [
...CREDENTIALS_NOTE,
"Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.",
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const asJson = format === "json";
const agentName = flags.agent ?? DEFAULT_INLINE_AGENT;
const model = flags.model ?? DEFAULT_INLINE_MODEL;
const instructions = flags.instructions ?? DEFAULT_INLINE_INSTRUCTIONS;
if (settings.dryRun) {
emitResult(
{
would_run: {
prompt: flags.prompt,
agent: agentName,
model,
instructions,
mode: flags.noStream ? "polling" : "streaming",
},
},
format,
);
return;
}
await withAgentErrors(() =>
withStdoutProtected(async () => {
const input = await buildInlineBackendInput(ctx, { agentName, instructions, model });
// Ensure the remote agent + its cloud environment exist. Idempotent:
// a repeat run with the same agent name reuses the materialized state.
if (!asJson) process.stderr.write(`Ensuring cloud agent "${agentName}"…\n`);
const sync = await syncAgentResourcesWithStateBackend(input, agentName, {
policy: "force",
quiet: true,
});
if (sync.status !== "completed") {
const detail =
sync.error ??
sync.diagnostics.find((diag) => diag.severity === "error")?.message ??
`provisioning ended with status "${sync.status}"`;
throw new BailianError(
`Failed to provision cloud agent "${agentName}": ${detail}`,
ExitCode.GENERAL,
);
}
// Run the task inside a runtime bound to the just-materialized state.
await readProjectRuntime(input, async (runtime) => {
if (flags.noStream) {
const run = await startSessionRunPolling(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
renderCollectedEvents(run, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
} else {
const run = await startSessionRun(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
await streamAndRenderEvents(run.events, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
}
});
}),
);
},
});
@@ -0,0 +1,465 @@
import { existsSync, readdirSync, statSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
type ProjectRuntimeContext,
type SyncProjectResult,
syncProviderResourcesFromContext,
} from "@openagentpack/sdk";
import { stringify as stringifyYaml } from "yaml";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import {
narrowGroupToRemoteId,
resolveSkillKeysByIds,
type SelectableResourceType,
SYNCABLE_TYPES,
type SyncableType,
} from "./_engine/sync-selection.ts";
/** bl's sync is bailian-only: the reverse-export source is always AgentStudio. */
const SYNC_PROVIDER = "bailian";
const DEFAULT_SYNC_OUTPUT = "agents.synced.yaml";
const SYNC_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: "Config file path (default: agents.yaml)",
},
out: {
type: "string",
valueHint: "<path>",
description: `Output path for the synced config (default: ${DEFAULT_SYNC_OUTPUT})`,
},
types: {
type: "string",
valueHint: "<list>",
description: `Comma-separated resource types to sync: ${SYNCABLE_TYPES.join(", ")} (default: all)`,
},
agentId: {
type: "string",
valueHint: "<id>",
description: "Sync a single agent by its remote ID (plus the skills it references)",
},
environmentId: {
type: "string",
valueHint: "<id>",
description: "Sync a single environment by its remote ID",
},
vaultId: {
type: "string",
valueHint: "<id>",
description: "Sync a single vault by its remote ID",
},
fileId: {
type: "string",
valueHint: "<id>",
description: "Sync a single file resource by its remote ID",
},
skillId: {
type: "string",
valueHint: "<id>",
description: "Sync a single skill by its remote ID (overrides --agent-id skill narrowing)",
},
force: {
type: "switch",
description: "Overwrite an existing output file",
},
skipMissingFiles: {
type: "switch",
description: "Drop file resources whose local source is missing instead of keeping them",
},
} satisfies FlagsDef;
export default defineCommand({
description: "Export remote bailian resources into a local synced config",
auth: "apiKey",
usageArgs:
"[--types <list>] [--agent-id|--environment-id|--vault-id|--file-id|--skill-id <id>] [--file <path>] [--out <path>] [--force] [--skip-missing-files]",
flags: SYNC_FLAGS,
exampleArgs: [
"",
"--types agent,skill",
"--agent-id agent-abc123",
"--skill-id skill-xyz --types skill",
"--force --skip-missing-files",
],
notes: [
...CREDENTIALS_NOTE,
"Syncs from the bailian provider only: remote AgentStudio resources (environments, vaults, files, skills, agents) are exported into a local synced config for review.",
"--types narrows the export to the listed resource types (plural spellings accepted); sessions are runtime instances, not syncable resources.",
"With --agent-id, the agents group keeps only that agent (yaml key = remote agent ID) and its referenced custom skills are synced along — the skill group is exported even when --types omits it; official skill references need no local entry.",
"Bailian binds environments/vaults/files at the session level, not on the agent, so --agent-id keeps those groups as shared infrastructure.",
"Each --environment-id/--vault-id/--file-id/--skill-id narrows its own resource group to the single remote resource (yaml key resolved from the remote listing); combine with --types for a minimal output.",
"Requires an agents.yaml with a bailian provider block — run `bl managed-agent init` first.",
"Secrets are never exported: vault credentials keep ${ENV} placeholders; set those env vars locally before apply.",
"Merge the synced config into agents.yaml with `bl managed-agent migrate`.",
],
validate: (flagValues) => {
if (!flagValues.types) return undefined;
const { types, invalid } = parseSyncTypes(flagValues.types);
if (invalid.length > 0) {
return `--types contains unsupported values: ${invalid.join(", ")}. Valid types: ${SYNCABLE_TYPES.join(", ")}.`;
}
if (types.length === 0) {
return `--types must list at least one of: ${SYNCABLE_TYPES.join(", ")}.`;
}
const idFlagByType: Record<SyncableType, string | undefined> = {
agent: flagValues.agentId,
environment: flagValues.environmentId,
vault: flagValues.vaultId,
file: flagValues.fileId,
skill: flagValues.skillId,
};
for (const [selectionType, value] of Object.entries(idFlagByType)) {
if (value && !types.includes(selectionType as SyncableType)) {
return `--${selectionType}-id requires --types to include ${selectionType}.`;
}
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const out = flags.out ?? DEFAULT_SYNC_OUTPUT;
// undefined → the SDK exports every syncable type.
let types = flags.types ? parseSyncTypes(flags.types).types : undefined;
// --agent-id pulls the agent's referenced skills along, so the skill group
// is exported even when --types omits it.
if (types && flags.agentId && !types.includes("skill")) types = [...types, "skill"];
// Per-group single-resource selections, resolved by remote ID after export.
const resourceSelections: Partial<Record<SelectableResourceType, string>> = {
environment: flags.environmentId,
vault: flags.vaultId,
file: flags.fileId,
skill: flags.skillId,
};
// Aligned with the other managed-agent commands: dry-run short-circuits
// first and only echoes the planned action — no guards, no file I/O.
if (settings.dryRun) {
emitResult(
{
would_sync: {
provider: SYNC_PROVIDER,
config_file: file,
out,
types,
agent_id: flags.agentId,
environment_id: flags.environmentId,
vault_id: flags.vaultId,
file_id: flags.fileId,
skill_id: flags.skillId,
},
},
format,
);
return;
}
if (existsSync(out) && !flags.force) {
throw new BailianError(
`${out} already exists.`,
ExitCode.USAGE,
"Pass --force to overwrite, or --out to write elsewhere.",
);
}
const { result, agentFilter, selectedKeys } = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
assertProviderConfigured(runtime, SYNC_PROVIDER);
const synced = await syncProviderResourcesFromContext(runtime, {
provider: SYNC_PROVIDER,
types,
});
// An explicit --skill-id wins over the agent's referenced-skill narrowing.
const filtered = flags.agentId
? await filterConfigToAgent(runtime, synced, flags.agentId, {
narrowSkills: !flags.skillId,
})
: undefined;
const keptKeys: Partial<Record<SelectableResourceType, string>> = {};
for (const [selectionType, remoteId] of Object.entries(resourceSelections)) {
if (!remoteId) continue;
keptKeys[selectionType as SelectableResourceType] = await narrowGroupToRemoteId(
runtime,
SYNC_PROVIDER,
synced,
selectionType as SelectableResourceType,
remoteId,
);
}
return { result: synced, agentFilter: filtered, selectedKeys: keptKeys };
}),
);
const narrowed = Boolean(agentFilter) || Object.keys(selectedKeys).length > 0;
const baseDir = dirname(out);
const removedFiles = flags.skipMissingFiles
? removeMissingFileSources(result.config, baseDir)
: [];
const yamlContent =
narrowed || removedFiles.length > 0
? stringifyYaml(result.config, { lineWidth: 0 })
: result.yaml;
await writeFile(out, yamlContent, "utf8");
const downloadedSkillFiles = await writeDownloadedSkillFiles(result, baseDir);
// Custom skills whose content could not be downloaded need local files
// before an apply would round-trip; surface them instead of prompting.
const missingSkillSources = collectMissingSkillSources(result.config, baseDir);
const secretEnvVars = (result.secretPlaceholders ?? []).map(
(placeholder) => placeholder.envVar,
);
if (format === "json") {
emitResult(
{
synced: out,
provider: SYNC_PROVIDER,
agent_id: flags.agentId,
selected_keys: Object.keys(selectedKeys).length > 0 ? selectedKeys : undefined,
counts: result.counts,
skill_files_downloaded: downloadedSkillFiles,
removed_files: removedFiles,
missing_skill_sources: missingSkillSources,
secret_env_vars: secretEnvVars,
unmatched_skill_refs: agentFilter?.unmatchedSkillIds,
},
format,
);
return;
}
const countParts = Object.entries(result.counts).map(([type, count]) => `${count} ${type}(s)`);
emitBare(
`Synced ${countParts.length > 0 ? countParts.join(", ") : "0 resources"} from ${SYNC_PROVIDER} into ${out}.`,
);
if (agentFilter) {
emitBare(
agentFilter.keptSkills
? `Narrowed to agent ${flags.agentId} (kept ${agentFilter.keptSkills.length} referenced skill(s)).`
: `Narrowed to agent ${flags.agentId}.`,
);
if (agentFilter.unmatchedSkillIds.length > 0) {
emitBare(
`Skill references without a matching skills entry (kept on the agent as-is): ${agentFilter.unmatchedSkillIds.join(", ")}.`,
);
}
}
for (const [selectionType, keptKey] of Object.entries(selectedKeys)) {
const requestedId = resourceSelections[selectionType as SelectableResourceType];
emitBare(
`Narrowed ${selectionType} to ${requestedId}${keptKey !== requestedId ? ` (key: ${keptKey})` : ""}.`,
);
}
if (downloadedSkillFiles > 0) {
emitBare(`Downloaded ${downloadedSkillFiles} skill file(s) into ./skills/.`);
}
if (removedFiles.length > 0) {
emitBare(
`Removed ${removedFiles.length} file resource(s) with missing local sources: ${removedFiles.join(", ")}.`,
);
}
if (missingSkillSources.length > 0) {
emitBare(
`Missing local skill sources (provide the files before apply): ${missingSkillSources.join(", ")}.`,
);
}
if (secretEnvVars.length > 0) {
emitBare(`Set these env vars locally before apply: ${secretEnvVars.join(", ")}.`);
}
// `bl` prefix is safe: agent commands ship on `bl` only.
emitBare(
`Next: review ${out}, then run \`bl managed-agent migrate\` to merge it into agents.yaml.`,
);
},
});
/**
* Parse the --types list. Accepts singular and plural spellings ("skills" →
* "skill") and dedupes; invalid tokens are returned verbatim so validate()
* can reject them with the original user input.
*/
function parseSyncTypes(raw: string): { types: SyncableType[]; invalid: string[] } {
const types: SyncableType[] = [];
const invalid: string[] = [];
const tokens = raw
.split(",")
.map((token) => token.trim())
.filter(Boolean);
for (const token of tokens) {
const singular = token.endsWith("s") ? token.slice(0, -1) : token;
const matched = SYNCABLE_TYPES.find(
(candidate) => candidate === token || candidate === singular,
);
if (!matched) {
invalid.push(token);
} else if (!types.includes(matched)) {
types.push(matched);
}
}
return { types, invalid };
}
/** Result of narrowing a synced config to a single agent. */
interface AgentFilterResult {
/** Undefined when skills narrowing was skipped (--skill-id takes over). */
keptSkills?: string[];
unmatchedSkillIds: string[];
}
/**
* Narrow a full synced config to a single agent: the agents group keeps only
* the entry whose yaml key equals the remote agent ID, and — unless an
* explicit --skill-id selection takes over (`narrowSkills: false`) — the
* skills group keeps only the custom skills that agent references (official
* skills live in the provider catalog and need no local entry). Skill
* references carry remote skill IDs while skills-group keys are
* metadata/display-name derived, so key misses are resolved through the
* custom skill catalog before being surfaced as unmatched (never silently
* dropped from the agent itself). Downloaded skill files and counts are
* re-scoped to what remains.
*/
async function filterConfigToAgent(
runtime: ProjectRuntimeContext,
result: SyncProjectResult,
agentId: string,
options: { narrowSkills: boolean },
): Promise<AgentFilterResult> {
const config = result.config;
const agents = (config.agents ?? {}) as Record<string, Record<string, unknown>>;
const selected = agents[agentId];
if (!selected) {
const available = Object.keys(agents);
throw new BailianError(
`Agent '${agentId}' not found on the remote workspace.`,
ExitCode.USAGE,
available.length > 0
? `Available agent IDs: ${available.join(", ")}.`
: "The workspace has no syncable (non-archived) agents.",
);
}
config.agents = { [agentId]: selected };
if ("agent" in result.counts) result.counts.agent = 1;
if (!options.narrowSkills) {
return { unmatchedSkillIds: [] };
}
const skillRefs = Array.isArray(selected.skills)
? (selected.skills as Array<Record<string, unknown>>)
: [];
// Official skills resolve against the provider catalog at apply time; only
// custom references map to synced skills-group declarations.
const referencedIds = new Set(
skillRefs
.filter((skillRef) => skillRef.type !== "official")
.map((skillRef) => skillRef.skill_id)
.filter((skillId): skillId is string => typeof skillId === "string"),
);
const skills = (config.skills ?? {}) as Record<string, Record<string, unknown>>;
const keptSkills = Object.keys(skills).filter((key) => referencedIds.has(key));
const pendingIds = Array.from(referencedIds).filter((skillId) => !(skillId in skills));
// Key misses: the ref carries a remote ID while the group key is a
// display-name slug — resolve through the custom catalog before declaring
// the reference unmatched.
const { resolved, unmatched: unmatchedSkillIds } = await resolveSkillKeysByIds(
runtime,
SYNC_PROVIDER,
skills,
pendingIds,
);
for (const resolvedKey of resolved.values()) {
if (!keptSkills.includes(resolvedKey)) keptSkills.push(resolvedKey);
}
if (keptSkills.length > 0) {
config.skills = Object.fromEntries(keptSkills.map((key) => [key, skills[key]!]));
} else {
delete config.skills;
}
if (result.skillFiles) {
const kept = new Set(keptSkills);
for (const skillName of result.skillFiles.keys()) {
if (!kept.has(skillName)) result.skillFiles.delete(skillName);
}
}
if ("skill" in result.counts) result.counts.skill = keptSkills.length;
return { keptSkills, unmatchedSkillIds };
}
/**
* Drop file resources whose `source` does not exist locally — the remote
* platform cannot hand file content back, so keeping them would make the
* synced config un-appliable. Returns the removed YAML keys.
*/
function removeMissingFileSources(config: Record<string, unknown>, baseDir: string): string[] {
const files = (config.files ?? {}) as Record<string, Record<string, unknown>>;
const removed = Object.entries(files)
.filter(
([, decl]) => typeof decl.source === "string" && !existsSync(join(baseDir, decl.source)),
)
.map(([key]) => key);
for (const key of removed) {
delete files[key];
}
if (Object.keys(files).length === 0) {
delete config.files;
}
return removed;
}
/** Persist provider-downloaded skill files under ./skills/<name>/; returns the file count. */
async function writeDownloadedSkillFiles(
result: SyncProjectResult,
baseDir: string,
): Promise<number> {
if (!result.skillFiles?.size) return 0;
let written = 0;
for (const [skillName, skillFileList] of result.skillFiles) {
for (const skillFile of skillFileList) {
const filePath = join(baseDir, "skills", skillName, skillFile.relativePath);
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, skillFile.content);
written++;
}
}
return written;
}
/** Custom skills whose local source dir/file is absent or empty after sync. */
function collectMissingSkillSources(config: Record<string, unknown>, baseDir: string): string[] {
const skills = (config.skills ?? {}) as Record<string, Record<string, unknown>>;
const missing: string[] = [];
for (const [key, decl] of Object.entries(skills)) {
if (decl.origin !== "custom" || typeof decl.source !== "string") continue;
const sourcePath = join(baseDir, decl.source);
if (!existsSync(sourcePath)) {
missing.push(key);
continue;
}
const sourceStat = statSync(sourcePath);
if (sourceStat.isDirectory() && readdirSync(sourcePath).length === 0) {
missing.push(key);
}
}
return missing;
}
+2 -28
View File
@@ -28,16 +28,6 @@ const ADD_FLAGS = {
valueHint: "<id>",
description: "Memory library ID (isolate memory space)",
},
projectId: {
type: "string",
valueHint: "<id>",
description: "Memory extraction rule ID (defaults to the library's default rule)",
},
metaData: {
type: "string",
valueHint: "<json>",
description: 'Custom metadata JSON object: {"location":"Beijing"}',
},
} satisfies FlagsDef;
type AddFlags = ParsedFlags<typeof ADD_FLAGS>;
@@ -50,7 +40,6 @@ export default defineCommand({
'--user-id user1 --content "The user likes Python programming"',
'--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'',
'--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx',
'--user-id user1 --content "Lives in Beijing" --meta-data \'{"source":"onboarding"}\'',
],
validate: (f: AddFlags) =>
!f.messages && !f.content ? "Provide --messages or --content." : undefined,
@@ -74,15 +63,6 @@ export default defineCommand({
if (flags.profileSchema) body.profile_schema = flags.profileSchema;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.projectId) body.project_id = flags.projectId;
if (flags.metaData) {
try {
body.meta_data = JSON.parse(flags.metaData);
} catch {
throw new UsageError("--meta-data must be valid JSON object");
}
}
const format = detectOutputFormat(settings.output);
@@ -98,14 +78,8 @@ export default defineCommand({
});
if (settings.quiet || format === "text") {
const nodes = response.memory_nodes ?? [];
if (nodes.length === 0) {
emitBare("No memory fragments were extracted.");
} else {
for (const node of nodes) {
emitBare(`[${node.event ?? "ADD"}] ${node.memory_node_id} ${node.content}`);
}
}
const ids = response.memory_ids?.join(", ") || "none";
emitBare(`Memory added. IDs: ${ids}`);
} else {
emitResult(response, format);
}
@@ -24,11 +24,6 @@ export default defineCommand({
},
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
projectId: {
type: "string",
valueHint: "<id>",
description: "Memory extraction rule ID (defaults to the library's default rule)",
},
},
exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"],
async run(ctx) {
@@ -41,7 +36,6 @@ export default defineCommand({
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize));
if (flags.page !== undefined) params.set("page_num", String(flags.page));
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
if (flags.projectId) params.set("project_id", flags.projectId);
const path = `${memoryListPath()}?${params.toString()}`;
@@ -1,44 +0,0 @@
import { defineCommand, profileSchemaItemPath, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Delete a profile schema",
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
},
exampleArgs: ["--schema-id schema_xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const query = params.toString();
const base = profileSchemaItemPath(flags.schemaId);
const path = query ? `${base}?${query}` : base;
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "DELETE" }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path,
method: "DELETE",
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} deleted.`);
} else {
emitResult(response, format);
}
},
});
@@ -1,52 +0,0 @@
import {
defineCommand,
profileSchemaItemPath,
detectOutputFormat,
type ProfileSchemaGetResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Show a profile schema and its attribute IDs",
auth: "apiKey",
usageArgs: "--schema-id <id> [flags]",
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
},
exampleArgs: ["--schema-id schema_xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const query = params.toString();
const base = profileSchemaItemPath(flags.schemaId);
const path = query ? `${base}?${query}` : base;
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaGetResponse>({
path,
method: "GET",
});
if (settings.quiet || format === "text") {
emitBare(`${response.name}${response.description ? ` — ${response.description}` : ""}`);
for (const attribute of response.attributes ?? []) {
emitBare(` [${attribute.attribute_id}] ${attribute.name}`);
}
} else {
emitResult(response, format);
}
},
});
@@ -1,55 +0,0 @@
import {
defineCommand,
profileSchemaPath,
detectOutputFormat,
type ProfileSchemaListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "List profile schemas",
auth: "apiKey",
usageArgs: "[flags]",
flags: {
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 10)" },
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
},
exampleArgs: ["", "--page-size 20 --page 2"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const params = new URLSearchParams();
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize));
if (flags.page !== undefined) params.set("page_num", String(flags.page));
const query = params.toString();
const path = query ? `${profileSchemaPath()}?${query}` : profileSchemaPath();
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "GET" }, format);
return;
}
const response = await ctx.client.requestJson<ProfileSchemaListResponse>({
path,
method: "GET",
});
if (settings.quiet || format === "text") {
const schemas = response.profile_schemas ?? [];
if (schemas.length === 0) {
emitBare("No profile schemas found.");
} else {
for (const schema of schemas) {
emitBare(`[${schema.profile_schema_id}] ${schema.name}`);
}
if (response.total !== undefined) emitBare(`\nTotal: ${response.total}`);
}
} else {
emitResult(response, format);
}
},
});
@@ -1,81 +0,0 @@
import {
defineCommand,
UsageError,
profileSchemaItemPath,
detectOutputFormat,
type ProfileSchemaUpdateRequest,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import type { FlagsDef, ParsedFlags } from "bailian-cli-core";
const UPDATE_FLAGS = {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
name: { type: "string", valueHint: "<name>", description: "New schema name" },
description: { type: "string", valueHint: "<text>", description: "New schema description" },
attributeOps: {
type: "string",
valueHint: "<json>",
description:
'Attribute operations JSON array: [{"op":"add","name":"plan"},{"op":"delete","attribute_id":"attr_1"}]',
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
} satisfies FlagsDef;
type UpdateFlags = ParsedFlags<typeof UPDATE_FLAGS>;
export default defineCommand({
description: "Update a profile schema's name, description, or attributes",
auth: "apiKey",
usageArgs: "--schema-id <id> [--name <name>] [--attribute-ops <json>] [flags]",
flags: UPDATE_FLAGS,
notes: ["Attribute IDs for update/delete operations come from `memory profile detail`."],
exampleArgs: [
'--schema-id schema_xxx --name "user_basic_v2"',
'--schema-id schema_xxx --attribute-ops \'[{"op":"add","name":"plan","description":"subscription plan"}]\'',
'--schema-id schema_xxx --attribute-ops \'[{"op":"delete","attribute_id":"attr_1"}]\'',
],
validate: (f: UpdateFlags) =>
!f.name && !f.description && !f.attributeOps
? "Provide --name, --description, or --attribute-ops."
: undefined,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const body: ProfileSchemaUpdateRequest = {};
if (flags.name) body.name = flags.name;
if (flags.description) body.description = flags.description;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.attributeOps) {
try {
body.attributes_operations = JSON.parse(flags.attributeOps);
} catch {
throw new UsageError("--attribute-ops must be valid JSON array");
}
}
const path = profileSchemaItemPath(flags.schemaId);
if (settings.dryRun) {
emitResult({ endpoint: ctx.client.url(path), method: "PATCH", request: body }, format);
return;
}
const response = await ctx.client.requestJson<{ request_id: string }>({
path,
method: "PATCH",
body,
});
if (settings.quiet || format === "text") {
emitBare(`Profile schema ${flags.schemaId} updated.`);
} else {
emitResult(response, format);
}
},
});
@@ -24,38 +24,6 @@ const SEARCH_FLAGS = {
description: "Number of results to return (default: 10)",
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
projectIds: {
type: "array",
valueHint: "<id>",
description: "Memory extraction rule ID for hybrid retrieval (repeatable)",
},
minScore: {
type: "number",
valueHint: "<n>",
description: "Minimum similarity score, 0-1 (default: 0.3)",
},
enableRerank: {
type: "boolean",
valueHint: "<bool>",
description:
"Rerank results. Also selects the billing tier: false bills lite, true bills pro (~50x). (default: true)",
},
planVersion: {
type: "string",
valueHint: "<lite|pro>",
description:
"Documented billing tier. The service currently honors --enable-rerank instead, so prefer that flag",
},
enableJudge: {
type: "boolean",
valueHint: "<bool>",
description: "Enable the intent-discrimination callback (default: false)",
},
enableRewrite: {
type: "boolean",
valueHint: "<bool>",
description: "Enable query rewriting (default: false)",
},
} satisfies FlagsDef;
type SearchFlags = ParsedFlags<typeof SEARCH_FLAGS>;
@@ -67,7 +35,6 @@ export default defineCommand({
exampleArgs: [
'--user-id user1 --query "programming preferences"',
'--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5',
'--user-id user1 --query "preferences" --enable-rerank false --min-score 0.5',
],
validate: (f: SearchFlags) =>
!f.query && !f.messages ? "Provide --query or --messages." : undefined,
@@ -94,21 +61,6 @@ export default defineCommand({
if (flags.topK !== undefined) body.top_k = flags.topK;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.projectIds && flags.projectIds.length > 0) body.project_ids = flags.projectIds;
if (flags.minScore !== undefined) body.min_score = flags.minScore;
if (flags.enableRerank !== undefined) body.enable_rerank = flags.enableRerank;
if (flags.enableJudge !== undefined) body.enable_judge = flags.enableJudge;
if (flags.enableRewrite !== undefined) body.enable_rewrite = flags.enableRewrite;
if (flags.planVersion) {
if (flags.planVersion !== "lite" && flags.planVersion !== "pro") {
throw new UsageError("--plan-version must be lite or pro");
}
body.plan_version = flags.planVersion;
// The service ignores plan_version on its own, so mirror the intent onto
// the flag it does honor unless the caller set that one explicitly.
if (flags.enableRerank === undefined) body.enable_rerank = flags.planVersion === "pro";
}
const format = detectOutputFormat(settings.output);
@@ -1,6 +1,5 @@
import {
defineCommand,
UsageError,
memoryNodePath,
detectOutputFormat,
type MemoryNodeUpdateRequest,
@@ -35,16 +34,6 @@ export default defineCommand({
valueHint: "<id>",
description: "Memory library ID (non-default library)",
},
timestamp: {
type: "number",
valueHint: "<unix-seconds>",
description: "When the remembered event happened (default: now)",
},
metaData: {
type: "string",
valueHint: "<json>",
description: 'Custom metadata JSON object, merged incrementally: {"source":"manual"}',
},
},
exampleArgs: ['--node-id node_xxx --user-id user1 --content "updated memory content"'],
async run(ctx) {
@@ -58,15 +47,6 @@ export default defineCommand({
custom_content: content,
};
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
if (flags.timestamp !== undefined) body.timestamp = flags.timestamp;
if (flags.metaData) {
try {
body.meta_data = JSON.parse(flags.metaData);
} catch {
throw new UsageError("--meta-data must be valid JSON object");
}
}
const format = detectOutputFormat(settings.output);
+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());
@@ -1,18 +0,0 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const GET_KEY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/api-keys/getKeyByUid";
export default defineCommand({
description: "Get the personal-edition TokenPlan API key (masked) for the current account",
auth: "console",
usageArgs: "[flags]",
flags: {},
exampleArgs: [""],
async run(ctx) {
const { settings } = ctx;
const format = detectOutputFormat(settings.output);
const result = await ctx.client.console(GET_KEY_API, {});
emitResult(result, format);
},
});
@@ -1,62 +0,0 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
const SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
const ADDON_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
const COMMODITY_CN = "sfm_tokenplansolo_public_cn";
const COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
const ADDON_CN = "sfm_tokenplansoloaddon_public_cn";
const ADDON_INTL = "sfm_tokenplansoloaddon_public_intl";
function nested(obj: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
const val = obj[key];
return val && typeof val === "object" && !Array.isArray(val)
? (val as Record<string, unknown>)
: undefined;
}
/** Unwrap the console gateway `data.DataV2.data.data` envelope to the business payload. */
function extract(result: Record<string, unknown>): Record<string, unknown> {
const data = nested(result, "data");
if (!data) return result;
const dataV2 = nested(data, "DataV2");
if (dataV2) {
const inner = nested(dataV2, "data");
const innerData = inner ? nested(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
return nested(data, "data") ?? data;
}
export default defineCommand({
description:
"Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits)",
auth: "console",
usageArgs: "[flags]",
flags: {},
exampleArgs: [""],
async run(ctx) {
const { settings } = ctx;
const format = detectOutputFormat(settings.output);
const intl = settings.consoleSite === "international";
const [usage, subscription, addon] = await Promise.all([
ctx.client.console(USAGE_API, {}),
ctx.client.console(SUBSCRIPTION_API, {
queryInstanceInfoRequest: { commodityCode: intl ? COMMODITY_INTL : COMMODITY_CN },
}),
ctx.client.console(ADDON_API, { commodityCode: intl ? ADDON_INTL : ADDON_CN }),
]);
emitResult(
{
usage: extract(usage as Record<string, unknown>),
subscription: extract(subscription as Record<string, unknown>),
addonSummary: extract(addon as Record<string, unknown>),
},
format,
);
},
});
+2 -8
View File
@@ -33,10 +33,6 @@ export { default as memoryUpdate } from "./commands/memory/update.ts";
export { default as memoryDelete } from "./commands/memory/delete.ts";
export { default as memoryProfileCreate } from "./commands/memory/profile-create.ts";
export { default as memoryProfileGet } from "./commands/memory/profile-get.ts";
export { default as memoryProfileList } from "./commands/memory/profile-list.ts";
export { default as memoryProfileDetail } from "./commands/memory/profile-detail.ts";
export { default as memoryProfileUpdate } from "./commands/memory/profile-update.ts";
export { default as memoryProfileDelete } from "./commands/memory/profile-delete.ts";
export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts";
export { default as knowledgeSearch } from "./commands/knowledge/search.ts";
export { default as knowledgeChat } from "./commands/knowledge/chat.ts";
@@ -95,13 +91,12 @@ 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 tokenPlanPersonalUsage } from "./commands/token-plan/personal-usage.ts";
export { default as tokenPlanPersonalKey } from "./commands/token-plan/personal-key.ts";
export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
export { default as managedAgentSync } from "./commands/managed-agent/sync.ts";
export { default as managedAgentMigrate } from "./commands/managed-agent/migrate.ts";
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
export { default as managedAgentRun } from "./commands/managed-agent/run.ts";
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts";
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
@@ -124,4 +119,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";
+2 -2
View File
@@ -89,13 +89,13 @@ test("GET /api/config 返回全部 profile、明文密钥与持久化激活项",
expect(res.json.enums.console_site).toEqual(["domestic", "international"]);
expect(res.json.booleanKeys).toContain("telemetry");
// Default field hints are surfaced as prefilled values in the UI.
expect(res.json.fieldDefaults.default_image_model).toBe("qwen-image-3.0");
expect(res.json.fieldDefaults.default_image_model).toBe("qwen-image-2.0");
expect(res.json.fieldDefaults.default_text_model).toBe("qwen3.8-max");
expect(res.json.fieldDefaults.output_dir).toContain("bailian-output");
expect(res.json.fieldDefaults.timeout).toBe("300");
expect(res.json.fieldDefaults.base_url).toBe("https://dashscope.aliyuncs.com");
// Per-category model catalog (click-to-fill suggestions) is exposed too.
expect(res.json.modelCatalog.default_image_model[0]).toMatchObject({ id: "qwen-image-3.0" });
expect(res.json.modelCatalog.default_image_model[0]).toMatchObject({ id: "qwen-image-2.0" });
expect(res.json.modelCatalog.default_video_model.map((m: { id: string }) => m.id)).toContain(
"happyhorse-1.1-i2v",
);
@@ -124,8 +124,7 @@ test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", ()
expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
});
test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面量则保留", () => {
delete process.env.BAILIAN_WORKSPACE_ID;
test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则保留", () => {
const empty = { bailian: { api_key: "", workspace_id: "" } };
injectProviderCredentials(
empty,
@@ -133,16 +132,6 @@ test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面
);
expect(empty.bailian.workspace_id).toBe("ws-settings");
// 内联运行时(对象配置)不做 ${} 插值,env 变量在此补读。
process.env.BAILIAN_WORKSPACE_ID = "ws-env";
const fromEnv = { bailian: { api_key: "", workspace_id: "" } };
injectProviderCredentials(
fromEnv,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(fromEnv.bailian.workspace_id).toBe("ws-env");
delete process.env.BAILIAN_WORKSPACE_ID;
const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } };
injectProviderCredentials(
literal,
@@ -151,37 +140,6 @@ test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面
expect(literal.bailian.workspace_id).toBe("ws-yaml");
});
test("inject:workspace 已知时 base_url 拼工作空间主机,而非模型域 origin", () => {
// agents.yaml 字面量 workspace_id + 空 base_url。
const literal = { bailian: { api_key: "", base_url: "", workspace_id: "ws-yaml" } };
injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() }));
expect(literal.bailian.base_url).toBe(
"https://ws-yaml.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
);
// 内联块:workspace_id 由 settings 填充后同样走工作空间主机。
const inline = { bailian: { api_key: "", base_url: "", workspace_id: "" } };
injectProviderCredentials(
inline,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(inline.bailian.workspace_id).toBe("ws-settings");
expect(inline.bailian.base_url).toBe(
"https://ws-settings.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
);
// 显式 base_url 字面量永远优先于拼装。
const explicit = {
bailian: {
api_key: "",
base_url: "https://custom.example.com/api/v1/agentstudio",
workspace_id: "ws-yaml",
},
};
injectProviderCredentials(explicit, makeHost({ apiCred: bailianCred() }));
expect(explicit.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
});
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(providers, makeHost({}));
@@ -0,0 +1,15 @@
version: "1"
providers:
claude:
api_key: ${ANTHROPIC_API_KEY}
defaults:
provider: claude
agents:
assistant:
description: "E2E claude-target fixture"
model: claude-sonnet-4-6
instructions: |
You are a helpful assistant.
@@ -0,0 +1,30 @@
version: "1"
providers:
bailian:
api_key: ${DASHSCOPE_API_KEY}
base_url: ${BAILIAN_BASE_URL}
environments:
dev:
config:
type: cloud
networking:
type: unrestricted
provider: bailian
agents:
assistant:
description: "E2E synced fixture (already exists in target)"
model: qwen3.7-max
instructions: |
You are a helpful assistant.
environment: dev
provider: bailian
reviewer:
description: "E2E synced fixture (new agent)"
model: qwen3.7-max
instructions: |
You review code.
environment: dev
provider: bailian
@@ -194,13 +194,13 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: ima
expect(stderr).toMatch(/--prompt|Usage:/i);
});
test("【qwen-image-3.0】图片编辑", async () => {
test("【qwen-image-2.0】图片编辑", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
const gen = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
"--prompt",
"一只简笔画小猫,白底",
"--out-dir",
@@ -220,7 +220,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: ima
"image",
"edit",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
"--image",
imagePath!,
"--prompt",
@@ -202,19 +202,19 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())(
"image",
"generate",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--prompt|Usage:/i);
});
test("【qwen-image-3.0】图片生成", async () => {
test("【qwen-image-2.0】图片生成", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
"--prompt",
"一只简笔画小猫,白底",
"--out-dir",
@@ -1,5 +1,6 @@
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
/**
@@ -245,3 +246,191 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () =>
expect(data.remote_id).toBe("agent-e2e");
});
});
describe("e2e: managed-agent sync / migrate(bailian-only)", () => {
const fixturesDir = join(e2eFixturesDir, "managed-agent");
const agentsYaml = join(fixturesDir, "agents.yaml");
const agentsSyncedYaml = join(fixturesDir, "agents-synced.yaml");
const agentsClaudeYaml = join(fixturesDir, "agents-claude.yaml");
test("managed-agent sync --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--out|--force|--skip-missing-files/i);
expect(stderr).toMatch(/--agent-id/i);
expect(stderr).toMatch(/--types/i);
expect(stderr).toMatch(/--environment-id/i);
expect(stderr).toMatch(/--vault-id/i);
expect(stderr).toMatch(/--file-id/i);
expect(stderr).toMatch(/--skill-id/i);
});
test("sync 非法 --types 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--types",
"agent,sessions",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--types contains unsupported values: sessions/i);
});
test("sync --agent-id 搭配不含 agent 的 --types 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--agent-id",
"agent-e2e",
"--types",
"skill",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--agent-id requires --types to include agent/i);
});
test("sync --vault-id 搭配不含 vault 的 --types 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--vault-id",
"vault-e2e",
"--types",
"agent",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--vault-id requires --types to include vault/i);
});
test("sync 输出文件已存在且未 --force 时退出为用法错误 (2)", async () => {
// auth: "apiKey" 的凭证解析先于 run() 执行;注入假 key 让用例不依赖环境凭证,
// 命令仍会在覆盖写守卫处短路(先于构建 SDK runtime),不产生任何网络请求。
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
["managed-agent", "sync", "--out", agentsYaml, "--quiet"],
{ DASHSCOPE_API_KEY: "sk-e2e-sync" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/already exists/i);
});
test("sync --dry-run 仅回显计划,即使 --out 已存在也不报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--dry-run",
"--agent-id",
"agent-e2e",
"--skill-id",
"skill-e2e",
"--types",
"agents,skills",
"--out",
agentsYaml,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_sync?: {
provider?: string;
out?: string;
agent_id?: string;
skill_id?: string;
types?: string[];
};
}>(stdout);
expect(data.would_sync?.provider).toBe("bailian");
expect(data.would_sync?.out).toBe(agentsYaml);
expect(data.would_sync?.agent_id).toBe("agent-e2e");
expect(data.would_sync?.skill_id).toBe("skill-e2e");
// 复数拼写归一化为 SDK 的单数资源类型
expect(data.would_sync?.types).toEqual(["agent", "skill"]);
});
test("sync --agent-id 时 --types 自动补充 skill(关联技能联动导出)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"sync",
"--dry-run",
"--agent-id",
"agent-e2e",
"--types",
"agent",
"--out",
"agents.synced.e2e-missing.yaml",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_sync?: { types?: string[] } }>(stdout);
expect(data.would_sync?.types).toEqual(["agent", "skill"]);
});
test("managed-agent migrate --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"migrate",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--from|--to/i);
});
test("migrate 目标文件缺失时退出为用法错误 (2)", async () => {
// auth: "apiKey" 的凭证解析先于 run() 执行;注入假 key 让用例不依赖环境凭证,
// 命令仍会在目标文件守卫处短路,不产生任何网络请求。
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"migrate",
"--from",
agentsSyncedYaml,
"--to",
"agents.e2e-missing.yaml",
"--quiet",
],
{ DASHSCOPE_API_KEY: "sk-e2e-migrate" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Target file .*agents\.e2e-missing\.yaml.*not found/i);
});
test("migrate 目标 provider 非 bailian 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
["managed-agent", "migrate", "--from", agentsSyncedYaml, "--to", agentsClaudeYaml, "--quiet"],
{ DASHSCOPE_API_KEY: "sk-e2e-migrate" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/only targets the bailian provider/i);
});
test("migrate --dry-run 仅回显计划,目标缺失也不报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"migrate",
"--dry-run",
"--from",
agentsSyncedYaml,
"--to",
"agents.e2e-missing.yaml",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_migrate?: { from?: string; to?: string };
}>(stdout);
expect(data.would_migrate?.from).toBe(agentsSyncedYaml);
expect(data.would_migrate?.to).toBe("agents.e2e-missing.yaml");
});
});
+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(
+2 -8
View File
@@ -33,10 +33,6 @@ export const MEMORY_ROUTES: E2eRouteExports = {
"memory delete": "memoryDelete",
"memory profile create": "memoryProfileCreate",
"memory profile get": "memoryProfileGet",
"memory profile list": "memoryProfileList",
"memory profile detail": "memoryProfileDetail",
"memory profile update": "memoryProfileUpdate",
"memory profile delete": "memoryProfileDelete",
};
export const KNOWLEDGE_ROUTES: E2eRouteExports = {
@@ -162,8 +158,6 @@ export const TOKEN_PLAN_ROUTES: E2eRouteExports = {
"token-plan create-key": "tokenPlanCreateKey",
"token-plan assign-seats": "tokenPlanAssignSeats",
"token-plan add-member": "tokenPlanAddMember",
"token-plan personal-usage": "tokenPlanPersonalUsage",
"token-plan personal-key": "tokenPlanPersonalKey",
};
export const SKILL_ROUTES: E2eRouteExports = {
@@ -171,15 +165,15 @@ export const SKILL_ROUTES: E2eRouteExports = {
"skill update": "skillUpdate",
"skill remove": "skillRemove",
"skill list": "skillList",
"skill init": "skillInit",
};
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent init": "managedAgentInit",
"managed-agent validate": "managedAgentValidate",
"managed-agent sync": "managedAgentSync",
"managed-agent migrate": "managedAgentMigrate",
"managed-agent plan": "managedAgentPlan",
"managed-agent apply": "managedAgentApply",
"managed-agent run": "managedAgentRun",
"managed-agent destroy": "managedAgentDestroy",
"managed-agent state list": "managedAgentStateList",
"managed-agent state rm": "managedAgentStateRm",
@@ -159,7 +159,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"image",
"generate",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
"--prompt",
"一只简笔画小猫,白底",
"--out-dir",
@@ -169,7 +169,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"image",
"generate",
"--model",
"qwen-image-3.0",
"qwen-image-2.0",
"--prompt",
"一片绿色的树叶,白底",
"--out-dir",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.14.2",
"version": "1.14.0",
"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 -5
View File
@@ -75,11 +75,7 @@ export function profileSchemaPath(): string {
}
export function userProfilePath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/user_profile`;
}
export function profileSchemaItemPath(schemaId: string): string {
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}`;
return `/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`;
}
// ---- Knowledge Base Retrieve (DashScope) ----
+3 -15
View File
@@ -10,7 +10,7 @@ import { image2ImagePath, imagePath, imageSyncPath, imageText2ImagePath } from "
* - async text2image + prompt: wan2.5/2.2/2.1-t2i*, wanx*-t2i*
*
* Edit (I2I):
* - sync multimodal + messages(+images): qwen-image-3.0*, qwen-image-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
* - sync multimodal + messages(+images): qwen-image-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
* (pure T2I models such as z-image / qwen-image-plus / qwen-image-max are NOT edit models)
* - async image2image + prompt/images: wan2.5-i2i*
* - async image2image + function/base_image_url: *imageedit* (e.g. wanx2.1-imageedit)
@@ -60,7 +60,6 @@ const SYNC_GENERATE_PREFIXES = ["qwen-image", "wan2.7-image", "z-image"] as cons
* Pure T2I models (z-image / qwen-image-plus / qwen-image-max) are excluded.
*/
const SYNC_EDIT_PREFIXES = [
"qwen-image-3.0",
"qwen-image-2.0",
"qwen-image-edit",
"wan2.7-image",
@@ -110,12 +109,7 @@ export function isWanxFunctionImageEditModel(model: string): boolean {
}
export function resolveImageSizeProfile(model: string): ImageSizeProfile {
// 3.0 暂复用 2.0 高分比例表(CLI ratio→像素便捷映射);不做独立 3.0 profile。
if (
model.startsWith("qwen-image-3.0") ||
model.startsWith("qwen-image-2.0") ||
model.startsWith("qwen-image-edit")
) {
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-edit")) {
return "qwen-image-2.0";
}
// Remaining qwen-image* (plus / max / bare qwen-image) share the fixed table.
@@ -139,13 +133,7 @@ export function resolveImageSizeProfile(model: string): ImageSizeProfile {
/** Official / CLI defaults for prompt_extend when the flag is omitted. */
export function resolvePromptExtendDefault(model: string): boolean | undefined {
if (
model.startsWith("qwen-image-3.0") ||
model.startsWith("qwen-image-2.0") ||
model.startsWith("qwen-image-max")
) {
return true;
}
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-max")) return true;
// Z-Image docs default prompt_extend to false.
if (model.startsWith("z-image")) return false;
return undefined;
-1
View File
@@ -13,7 +13,6 @@ export {
memoryNodePath,
memorySearchPath,
mcpWebSearchPath,
profileSchemaItemPath,
profileSchemaPath,
speechRecognizePath,
speechSynthesizePath,
+1 -9
View File
@@ -199,15 +199,7 @@ export function buildSources(flags: Partial<SourceFlags>): ResolutionSources {
const raw = readRawConfigObject();
const configExplicit = flags.config !== undefined;
const activeConfigName = readStoredActiveConfigName(raw, !configExplicit);
// Config selection: --config flag > BAILIAN_CONFIG env > persisted active_config.
// The env lets a host (e.g. dsh) pin a named profile for all child `bl`
// calls without rewriting --config or the user's active_config.
const envConfig = process.env.BAILIAN_CONFIG;
const configName = configExplicit
? normalizeConfigName(flags.config)
: envConfig
? normalizeConfigName(envConfig)
: activeConfigName;
const configName = configExplicit ? normalizeConfigName(flags.config) : activeConfigName;
return {
flags,
file: parseConfigFile(readRawConfigBlock(raw, configName)),
+7 -82
View File
@@ -305,22 +305,11 @@ export interface MemoryAddRequest {
custom_content?: string;
profile_schema?: string;
memory_library_id?: string;
project_id?: string;
meta_data?: Record<string, unknown>;
}
/** 变更的记忆片段;`event` 为 ADD / UPDATE / DELETE。 */
export interface MemoryAddNode {
memory_node_id: string;
content: string;
event?: string;
/** 仅 `event` 为 UPDATE 时有效。 */
old_content?: string;
}
export interface MemoryAddResponse {
request_id: string;
memory_nodes?: MemoryAddNode[];
memory_ids?: string[];
}
export interface MemorySearchRequest {
@@ -329,17 +318,6 @@ export interface MemorySearchRequest {
query?: string;
top_k?: number;
memory_library_id?: string;
project_ids?: string[];
min_score?: number;
/**
* 计费档位的**有效**开关。服务端当前忽略单独传入的 `plan_version`,
* 只有 `enable_rerank: false` 才会按 lite 计费(pro 约为 lite 的 50 倍)。
*/
enable_rerank?: boolean;
/** 文档所述的档位字段;当前服务端未按文档生效,与 `enable_rerank` 一起传。 */
plan_version?: "lite" | "pro";
enable_judge?: boolean;
enable_rewrite?: boolean;
}
export interface MemoryNode {
@@ -347,19 +325,13 @@ export interface MemoryNode {
content: string;
user_id?: string;
meta_data?: Record<string, unknown>;
project_id?: string;
/** 秒级 Unix 时间戳。 */
created_at?: number;
/** 秒级 Unix 时间戳。 */
updated_at?: number;
timestamp?: number;
created_at?: string;
updated_at?: string;
}
export interface MemorySearchResponse {
request_id: string;
memory_nodes: MemoryNode[];
/** 本次检索实际计费的档位。 */
billing_plan?: string;
}
export interface MemoryNodeListResponse {
@@ -375,18 +347,13 @@ export interface MemoryNodeUpdateRequest {
custom_content: string;
/** 非默认记忆库时必填(与控制台记忆库 ID 一致) */
memory_library_id?: string;
/** 记忆片段对应事件发生时的秒级 Unix 时间戳。 */
timestamp?: number;
/** 增量更新。 */
meta_data?: Record<string, unknown>;
}
// ---- Memory Profile (DashScope v2) ----
export interface ProfileAttribute {
name: string;
description?: string;
default_value?: string;
description: string;
value?: string;
}
@@ -394,8 +361,6 @@ export interface ProfileSchemaCreateRequest {
name: string;
description?: string;
attributes: ProfileAttribute[];
memory_library_id?: string;
plan_version?: "lite" | "pro";
}
export interface ProfileSchemaCreateResponse {
@@ -403,52 +368,12 @@ export interface ProfileSchemaCreateResponse {
profile_schema_id: string;
}
export interface ProfileSchemaSummary {
profile_schema_id: string;
name: string;
description?: string;
}
export interface ProfileSchemaListResponse {
request_id: string;
profile_schemas: ProfileSchemaSummary[];
total?: number;
}
/** 画像模板详情;`attributes[].attribute_id` 是更新/删除属性时的定位键。 */
export interface ProfileSchemaGetResponse {
request_id: string;
name: string;
description?: string;
attributes: Array<ProfileAttribute & { attribute_id: string }>;
}
export interface ProfileSchemaAttributeOperation {
op: "add" | "update" | "delete";
/** `update` / `delete` 必填。 */
attribute_id?: string;
/** `add` 必填。 */
name?: string;
description?: string;
default_value?: string | null;
}
export interface ProfileSchemaUpdateRequest {
name?: string;
description?: string;
memory_library_id?: string;
attributes_operations?: ProfileSchemaAttributeOperation[];
}
/**
* 用户画像。服务端返回的是模板名称/描述与属性值,不回传 schema_id / user_id。
*/
export interface UserProfileResponse {
request_id: string;
profile: {
schema_name?: string;
schema_description?: string;
attributes: Array<{ id: string; name: string; value?: string }>;
schema_id: string;
user_id: string;
attributes: ProfileAttribute[];
};
}
-12
View File
@@ -11,7 +11,6 @@ import {
} from "../src/client/image-routes.ts";
test("sync multimodal family covers qwen-image, wan2.6/2.7 image, and z-image", () => {
expect(isSyncMultimodalImageModel("qwen-image-3.0")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-2.0")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-2.0-pro")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-plus")).toBe(true);
@@ -42,7 +41,6 @@ test("legacy image2image is wan2.5-i2i only; wanx imageedit uses function protoc
});
test("size profiles are model-specific, not sync/async", () => {
expect(resolveImageSizeProfile("qwen-image-3.0")).toBe("qwen-image-2.0");
expect(resolveImageSizeProfile("qwen-image-2.0")).toBe("qwen-image-2.0");
expect(resolveImageSizeProfile("qwen-image")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("qwen-image-plus")).toBe("qwen-image-fixed");
@@ -57,7 +55,6 @@ test("size profiles are model-specific, not sync/async", () => {
});
test("prompt_extend defaults follow model docs", () => {
expect(resolvePromptExtendDefault("qwen-image-3.0")).toBe(true);
expect(resolvePromptExtendDefault("qwen-image-2.0")).toBe(true);
expect(resolvePromptExtendDefault("qwen-image-max")).toBe(true);
expect(resolvePromptExtendDefault("z-image-turbo")).toBe(false);
@@ -88,11 +85,6 @@ test("resolveImageGenerateApi picks path, input style, and size profile", () =>
kind: "async-image-generation",
sizeProfile: "wan26",
});
expect(resolveImageGenerateApi("qwen-image-3.0")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "qwen-image-2.0",
promptExtendDefault: true,
});
expect(resolveImageGenerateApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "qwen-image-2.0",
@@ -123,10 +115,6 @@ test("resolveImageEditApi excludes pure T2I models from sync edit", () => {
kind: "sync-multimodal",
useSync: true,
});
expect(resolveImageEditApi("qwen-image-3.0")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
});
expect(resolveImageEditApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
-4
View File
@@ -1,4 +0,0 @@
# build artifacts (regenerated by `pnpm build`)
client.bundle.js
dist/
*.tgz
-232
View File
@@ -1,232 +0,0 @@
# bailian-cli-dsh
把阿里云百炼(Model Studio)的能力接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的 profile bundle。
本包提供两项能力:
| 能力 | 说明 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
| **Bailian 设置页** | 通用的百炼凭证配置(AK/SK 存入 `dsh` bl profile + DashScope API Key)+ TokenPlan 用量展示 + 记忆库配置 + 新会话欢迎页 |
| **跨会话长期记忆** | 自动检索注入 + 自动落库,模型可主动 search/add/list。按量计费,默认停用 |
---
## 1. 前置条件
- Node ≥ 22.19(`dsh` 的要求)
- `bl`(用量展示通过子进程调用 `bl console call`)
```sh
npm install -g bailian-cli
```
- **阿里云 AK/SK**(AccessKey ID + AccessKey Secret)—— 用于控制台鉴权,查询用量信息。在 webui 设置页填入即可,无需环境变量。
- **DashScope API Key**(`sk-` 前缀,按量付费)—— 用于记忆库等 DashScope API 调用。在设置页「凭证配置」填入,与 AK/SK 并列为通用凭证。
获取方式:[阿里云控制台 → AccessKey 管理](https://ram.console.aliyun.com/manage/ak)
---
## 2. 安装到 `web` profile
`npx @deepseek-ai/dsh web` 是 `dsh --profile web` 的别名,配置目录是 `~/.dsh/profiles/web/`。
```sh
pnpm -F bailian-cli-dsh build # vp pack(host)+ esbuild(client.bundle.js)
cd packages/dsh && pnpm pack
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-<version>.tgz
```
确认 bailian 行都在:
```sh
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian'
```
启动:
```sh
npx @deepseek-ai/dsh web
```
Web UI 在 http://127.0.0.1:3080。
---
## 3. Bailian 设置页 + 欢迎页
安装并重启后:
- **Settings → Bailian**:通用设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
- **新会话欢迎页**:每个新会话(blank)在输入框上方显示「百炼 Agent」欢迎页(Tab + 功能卡片),发出第一条消息后自动隐藏。
### 凭证配置(通用)
1. 在「凭证配置」区填入 **AccessKey ID** 和 **AccessKey Secret**
2. 点击 **「保存凭证」**
Host 会执行 `bl auth login --open-api --config dsh`,将 AK/SK 和新生成的 access_token 存入 bl 的 `dsh` 专属 profile。**所有后续百炼插件共用此凭证**,无需重复配置。
### TokenPlan 用量
1. 选择区域和站点
2. 点击 **「查询用量」**
Host 执行 `bl console call --config dsh` 调用 3 个个人版控制台接口,返回:
- **用量百分比** —— 5 小时窗口 / 1 周窗口的用量百分比和重置时间
- **套餐信息** —— 套餐类型(基础版/标准版/高级版)、状态、剩余天数、到期时间、自动续费
- **额外用量包** —— Credits 总量、剩余量、生效中数量
### 凭证解析优先级
凭证保存到 bl 的 `dsh` profile 后,所有百炼插件通过 `--config dsh` 读取。行内 config 的 `accessKeyId`/`accessKeySecret` 作为兜底(未通过 UI 保存时自动使用)。
### 行内配置(可选)
如果不想在 UI 里每次输入,可以在 profile 的 `cordis.patch.yml` 里固化凭证:
```yaml
- id: bailian-tokenplan-usage
config:
# accessKeyId / accessKeySecret: 兜底凭证(未通过 UI 保存时使用)
# consoleRegion: cn-beijing
# consoleSite: domestic
# profile: dsh # 默认用 dsh 专属 profile
```
配置后 UI 表单会留空,但点击「查询用量」会使用行内凭证。
---
## 4. 跨会话长期记忆
默认停用(按量计费)。在 `cordis.patch.yml` 中设 `disabled: false` 启用,然后在设置页配置 API Key 和参数。
### 功能
- **自动检索注入**:新会话首轮,用用户消息搜索记忆,将结果注入上下文(`autoInject`,默认开启)
- **自动落库**:每轮结束,将该轮新消息发送到记忆库 add API(`autoPersist`,默认开启)
- **模型工具**:`bailian_memory_search`(检索)、`bailian_memory_add`(存储)、`bailian_memory_list`(浏览)
### 触发机制
| 时机 | 触发方式 |
| ---------- | --------------------------------------------------------- |
| 新会话首轮 | 自动检索记忆注入上下文(`agent/pre-step` 事件) |
| 对话中 | 模型主动调用 `bailian_memory_search`/`bailian_memory_add` |
| 轮次结束 | 自动落库新消息(`agent/turn-stopping` 事件) |
### 凭证与配置
- **API Key**:DashScope 按量付费 Key(`sk-`),在设置页「凭证配置」填入
- **Base URL**:默认 `https://dashscope.aliyuncs.com/api/v2/apps/memory/`
- **User ID**:记忆归属 ID,默认读系统用户名
- **Plan Version**:`lite`(便宜,关闭 rerank)或 `pro`(开启 rerank,约 50 倍成本)。注意:实际计费由 `enable_rerank` 控制
- **Top K**:检索返回数量(1-100,默认 10)
- **Memory Library ID**:记忆库 ID,留空用默认
### 计费
- Add:120 QPM
- Search:300 QPM(Lite ¥0.00002/次,Pro ¥0.001/次)
- 总计不超过 3000 QPM
### 启用
```yaml
- id: bailian-memory
disabled: false
config:
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
planVersion: "lite"
topK: 10
autoInject: true
autoPersist: true
```
启用后在设置页「记忆库」section 配置 API Key 和参数。
> 记忆库调用 DashScope memory v2 API(非 `bl memory`),因为 v2 API 暴露了 `min_score`、`enable_rerank`、`plan_version`、`memory_library_id` 等参数 `bl memory` 不支持。
## 5. 验证
```sh
# 配置合成
npx @deepseek-ai/dsh --profile web --dump-config | grep bailian
# bl 就绪
bl auth status
```
启动后验证:
- **欢迎页**:新开一个会话,输入框上方出现「百炼 Agent」欢迎页
- **凭证配置**:打开 Settings → Bailian → 填入 AK/SK → 保存凭证
- **用量展示**:同页面选择区域 → 查询用量
- **记忆库**:启用 `bailian-memory` 后,同页面配置 API Key
---
## 6. 常见问题
| 现象 | 原因 |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 用量查询报 `bl auth login failed` | AK/SK 无效或无权限;确认 AK 有百炼控制台访问权限 |
| 用量查询报 `NotLogined` 或 token 过期 | bl 的 access token 已过期;Host 会自动通过 AK/SK 刷新,确认 AK/SK 正确 |
| 用量查询报 `bl console call failed` | 控制台接口调用失败;检查 region/site 是否匹配你的账号 |
| 用量查询报 `Workspace.NotAuthorised` | bl 用了其他 profile 的旧 access_token;Host 默认用 `--config dsh` 专属 profile 隔离,首次 login 会生成新 token |
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
| 设置页/欢迎页看不到 Bailian | 需**重启 `dsh web`**(bundle 在启动时加载);确认 `dump-config` 有 `bailian-client` 行,且 `client.bundle.js` 为 ModuleLoader 格式 |
| 启动报 `invalid plugin ... apply` | 包根 `dist/index.mjs` 必须导出 `apply`(no-op 插件);重新 `pnpm build` 再装 |
---
## 7. 卸载
```sh
npx @deepseek-ai/dsh plugin --profile web remove bailian-cli-dsh
```
---
## 架构说明
### Host 半
- `src/tokenplan-usage/index.ts` —— 凭证 + TokenPlan 用量。`inject: ['subprocess']`,所有 bl 命令带 `--config dsh` 隔离凭证。两个 webServer 路由:
- `POST /bailian/credentials` — 保存 AK/SK(`bl auth login --open-api --config dsh`,生成新 token)
- `POST /bailian/tokenplan/usage` — 查询用量(`bl console call --config dsh`,3 个个人版接口)
- `src/memory/index.ts` —— 记忆库(默认停用)。直接调 DashScope memory v2 API,注册 tools + auto-inject/persist。路由 `/bailian/memory/config`、`/bailian/memory/status`。
- `src/index.ts` —— 包根 no-op 插件,供 `bailian-client` 行加载(该行只为了让 client-modules 服务浏览器 bundle)。
> 路由用 `/bailian/*` 而非 `/api/*`:`/api` 前缀被 dsh 的 RPC 网关(apiProxy)占用,自定义路由会被遮蔽。
调用链路:**AK/SK → `bl auth login --open-api --config dsh`(存入 dsh profile)→ `bl console call --config dsh`(读 dsh profile token → 控制台网关)→ 个人版 TokenPlan 接口**
### Client 半(`src/client.ts`)
- 唯一的浏览器源码,构建为 DSH ModuleLoader 格式(见下)。
- 注册 `settings.section`(id: `bailian`,label: `Bailian`),渲染通用百炼设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
- 注册 `conversation.input.dock`(id: `bailian-welcome`):当 `session.blank === true`(新会话)渲染「百炼 Agent」欢迎页(Tab + 功能卡片),开始对话后自动隐藏。
- 通过 `fetch('/bailian/*')` 调 Host 路由。
### Client 构建(ModuleLoader 格式)
DSH 浏览器只加载 `window.__ModuleLoader__.load({ id, factory })` 格式的 bundle(`require('react')` 由浏览器 ModuleLoader 提供)。vite-plus 产出裸 ES module,格式不对,所以 client 单独用 esbuild 构建:
- `scripts/build-client.mjs` —— 把 `src/client.ts` 构建为 CJS + browser + `react` external,包上 ModuleLoader banner/footer,输出 `client.bundle.js`。
- `package.json` 的 `build` = `vp pack && node scripts/build-client.mjs`。
- `package.json` 的 `exports["./client"]` 与 `dsh.client: { platform: "web" }` 指向 `client.bundle.js`,被 client-modules 扫描并服务。
- `cordis.patch.yml` 的 `bailian-client` 行 `name` 必须是**包根**(`bailian-cli-dsh`,无子路径),client-modules 才能 `require.resolve("<name>/package.json")` 识别 `dsh.client`。
改 client UI 只需编辑 `src/client.ts`,`pnpm build` 自动重新生成 `client.bundle.js`。
### 共享模块(`src/shared/`)
- `bl.ts` —— `bl` 子进程调用封装(env 转发、stdout/stderr 收集、JSON 解析)
- `credentials.ts` —— TokenPlan / 按量付费 Key 分类工具
- `http.ts` —— DashScope HTTP 客户端
这些模块来自早期版本(vision / image / managed-agent / RAG / memory 工具),已移除工具实现但保留共享逻辑作为参考。
-53
View File
@@ -1,53 +0,0 @@
# bailian-cli-dsh — Aliyun Model Studio (Bailian) as a dsh profile bundle.
#
# Inserts Bailian plugin rows: TokenPlan usage display + cross-session memory.
# Every inserted id is `bailian-`-prefixed so a user profile can address,
# reconfigure, or disable any single capability without touching the others.
# Remember that a later patch REPLACES a row's whole `config` rather than
# merging into it, so restate the complete config when overriding.
- insert:
# Client-only row: name is the package ROOT (no subpath) so client-modules
# can resolve "<name>/package.json" and detect the dsh.client declaration.
# Its node half (dist/index.mjs) is a no-op; the row exists to serve the
# browser bundle (client.bundle.js) that renders the Bailian settings page
# and the new-session welcome page.
- id: bailian-client
name: bailian-cli-dsh
# TokenPlan usage display (dual-face: Host provides two webServer routes,
# Client renders a general "Bailian" settings.section page). All bl commands
# use `--config dsh` to isolate credentials in a dedicated bl profile.
#
# Two routes:
# POST /api/bailian/credentials — saves AK/SK to dsh profile
# (bl auth login --open-api --config dsh). Generates fresh access_token.
# POST /api/bailian/tokenplan/usage — fetches personal-edition usage
# using the dsh profile (no AK/SK in body; credentials already saved).
#
# Users configure AK/SK once on the settings page; all future Bailian
# plugins reuse the same dsh profile credentials.
#
# Config fields:
# accessKeyId / accessKeySecret: fallback when not provided via UI.
# consoleRegion: default region (cn-beijing).
# consoleSite: domestic | international (default: domestic).
# profile: bl config profile name (default: dsh).
- id: bailian-tokenplan-usage
name: bailian-cli-dsh/tokenplan-usage
config: {}
# Disabled by default: memory add/search are billed per call. Enable in
# the profile patch and configure API Key + parameters on the Bailian
# settings page. Calls DashScope memory v2 API directly (not bl memory)
# for full parameter control (min_score, enable_rerank, plan_version,
# memory_library_id, enable_judge, enable_rewrite).
- id: bailian-memory
name: bailian-cli-dsh/memory
disabled: true
config:
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
planVersion: "lite"
topK: 10
autoInject: true
autoPersist: true
-102
View File
@@ -1,102 +0,0 @@
{
"name": "bailian-cli-dsh",
"version": "1.14.2",
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider and personal-edition TokenPlan usage display in the webui.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
"url": "https://github.com/modelstudioai/cli/issues"
},
"license": "Apache-2.0",
"author": "Aliyun Model Studio",
"repository": {
"type": "git",
"url": "git+https://github.com/modelstudioai/cli.git",
"directory": "packages/dsh"
},
"files": [
"README.md",
"dist",
"client.bundle.js",
"cordis.patch.yml"
],
"type": "module",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.mjs"
},
"./tokenplan-usage": {
"types": "./src/tokenplan-usage/index.ts",
"default": "./dist/tokenplan-usage/index.mjs"
},
"./memory": {
"types": "./src/memory/index.ts",
"default": "./dist/memory/index.mjs"
},
"./client": "./client.bundle.js",
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"exports": {
".": "./dist/index.mjs",
"./tokenplan-usage": "./dist/tokenplan-usage/index.mjs",
"./memory": "./dist/memory/index.mjs",
"./client": "./client.bundle.js",
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"build": "vp pack && node scripts/build-client.mjs",
"dev": "vp pack --watch",
"test": "vp test",
"check": "vp check"
},
"dependencies": {
"@deepseek-ai/schemastery": "^3.18.1"
},
"devDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"@deepseek-ai/dsh-web": "^0.1.0-rc.6",
"@types/node": "catalog:",
"typescript": "^6.0.2",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"@deepseek-ai/dsh-web": "^0.1.0-rc.6"
},
"engines": {
"node": ">=22.19.0"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"platform": "web"
}
}
}
-44
View File
@@ -1,44 +0,0 @@
/**
* Build the browser client bundle in the DSH ModuleLoader closure format.
*
* The DSH web shell only loads client plugins that call
* `window.__ModuleLoader__.load({ id, factory })`, resolving externals (react)
* through the injected `require`. vite-plus emits plain ESM (wrong format), so
* the client is built separately with esbuild: CJS + browser platform + react
* external, wrapped in the ModuleLoader banner/footer.
*
* Run after `vp pack` (see package.json "build").
*/
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
const esbuild = join(pkgDir, "node_modules", ".bin", "esbuild");
const banner =
'window.__ModuleLoader__.load({ id: "bailian-cli-dsh", factory: (require) => { ' +
"var module = { exports: {} }; var exports = module.exports;";
const footer = "return module.exports; } });";
const result = spawnSync(
esbuild,
[
"src/client.ts",
"--bundle",
"--format=cjs",
"--platform=browser",
"--external:react",
`--banner:js=${banner}`,
`--footer:js=${footer}`,
"--outfile=client.bundle.js",
],
{ cwd: pkgDir, stdio: "inherit" },
);
if (result.status !== 0) {
// Throw rather than process.exit: an uncaught top-level error still yields a
// non-zero exit (so `pnpm build` fails), and it carries esbuild's own status.
throw new Error(`build-client: esbuild failed with status ${result.status ?? "unknown"}`);
}
console.log("build-client: client.bundle.js (ModuleLoader format) written");
File diff suppressed because it is too large Load Diff
-132
View File
@@ -1,132 +0,0 @@
/**
* Bailian feature registry — single source of truth mapping a welcome-page
* card to a **bailian-cli command**. The console-API knowledge lives in
* bailian-cli (packages/commands); this bundle only shells out to `bl`, so a
* feature added there is reusable here for free.
*
* Each entry is exposed two ways by the Host:
* 1. a **model tool** `bailian_<id>` (natural-language entry: the LLM reads
* `intent` and calls the tool when the user asks in plain language);
* 2. the **generic route** `POST /bailian/console { featureId }` (card-click
* entry: the client renders `summarize`/`data`).
*
* Adding a feature = (a) add a `bl` command in bailian-cli, (b) add one record
* here. Tool + card come for free.
*
* Browser-safe (no node imports) so both the vite host build and the esbuild
* client bundle can import it.
*
* @module bailian-cli-dsh/features
*/
export interface BailianFeature {
/** Stable id; tool name is `bailian_<id>`. */
id: string;
/** Card title (matched against welcome cards). */
title: string;
/** Card description. */
desc: string;
/** Tool description: tells the LLM which user utterances should use it. */
intent: string;
/** Natural-language query sent into the conversation when the card is clicked. */
query: string;
/** `bl` command args (without `--output`); the Host appends `--output json`. */
argv: string[];
/** Args appended when the user supplies no params (e.g. ["--all"]). */
defaultArgs?: string[];
/** Optional params the LLM (or UI) may supply; mapped to bl flags. */
paramFlags?: FeatureParam[];
/** Human/LLM summary of the command's JSON output. */
summarize: (data: any) => string;
}
export interface FeatureParam {
/** Tool parameter name (LLM fills it). */
name: string;
/** bl flag it maps to (e.g. --model). */
flag: string;
type: "string" | "number" | "boolean";
description: string;
}
function pick(obj: any, ...keys: string[]): any {
for (const k of keys) if (obj && obj[k] !== undefined && obj[k] !== null) return obj[k];
return undefined;
}
function pct(v: any): string {
if (v === undefined || v === null) return "—";
const n = (typeof v === "number" ? v : Number(v)) * 100;
return (isNaN(n) ? 0 : n).toFixed(1) + "%";
}
export const FEATURES: BailianFeature[] = [
{
id: "free-tier",
title: "免费额度一键防护",
desc: "查询免费额度用量,一键开启「用完即停」,额度耗尽自动停止调用,不再产生意外扣费",
intent:
"查询百炼免费额度用量与『用完即停』防护状态。当用户提到免费额度、额度耗尽、意外扣费、用完即停、额度防护时使用。",
query: "帮我查看百炼免费额度用量,并告诉我怎么开启「用完即停」防护",
argv: ["usage", "freetier"],
defaultArgs: ["--all"],
paramFlags: [
{
name: "models",
flag: "--model",
type: "string",
description:
"逗号分隔的模型列表;不填则查询全部(--all)。若用户只关心特定模型且未说明,可先用 AskUserQuestion 询问。",
},
],
summarize: (d) => {
if (!d || typeof d !== "object") return "未获取到免费额度数据。";
const list = pick(d, "quotas", "quotaList", "models", "list");
if (Array.isArray(list)) {
const lines = list.slice(0, 8).map((m: any) => {
const model = pick(m, "model", "modelName", "modelId") ?? "?";
const total = pick(m, "quotaTotal", "totalQuota", "total");
const used = pick(m, "quotaUsed", "usedQuota", "used");
const on = pick(m, "freeTierOnly");
return `- ${model}: 已用 ${used ?? "?"} / 共 ${total ?? "?"}${on !== undefined ? `,用完即停 ${on ? "开" : "关"}` : ""}`;
});
return lines.length
? `免费额度:\n${lines.join("\n")}`
: "免费额度: " + JSON.stringify(d).slice(0, 300);
}
return "免费额度: " + JSON.stringify(d).slice(0, 300);
},
},
{
id: "usage",
title: "模型用量统计",
desc: "各模型/TokenPlan 的用量与百分比一次查清,自动生成用量分析",
intent:
"查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口百分比、重置时间、套餐、用量包)。当用户问用量、用了多少、额度百分比、TokenPlan 使用情况时使用。",
query: "帮我查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口、套餐与用量包)",
argv: ["token-plan", "personal-usage"],
summarize: (d) => {
if (!d || typeof d !== "object") return "未获取到用量数据。";
const u = d.usage ?? d;
const parts: string[] = [];
if (u.per5HourPercentage !== undefined)
parts.push(`5 小时窗口已用 ${pct(u.per5HourPercentage)}`);
if (u.per1WeekPercentage !== undefined)
parts.push(`1 周窗口已用 ${pct(u.per1WeekPercentage)}`);
const sub = d.subscription;
if (sub && sub.remainingDays !== undefined) parts.push(`套餐剩余 ${sub.remainingDays} 天`);
const add = d.addonSummary;
if (add && add.remainingCredits !== undefined)
parts.push(`用量包剩余 ${add.remainingCredits}/${add.totalCredits}`);
return parts.length
? `TokenPlan 用量: ${parts.join(";")}`
: "用量: " + JSON.stringify(d).slice(0, 300);
},
},
];
export function featureById(id: string): BailianFeature | undefined {
return FEATURES.find((f) => f.id === id);
}
export function featureByTitle(title: string): BailianFeature | undefined {
return FEATURES.find((f) => f.title === title);
}
-20
View File
@@ -1,20 +0,0 @@
/**
* bailian-cli-dsh — Aliyun Model Studio capabilities as a DeepSeek Harness
* profile bundle. The package's substance is `cordis.patch.yml`, declared by
* the `dsh.bundle.patch` manifest field and resolved by the profile composer.
*
* This root module is the no-op node half loaded by the `bailian-client` row
* (whose purpose is to make `client-modules` serve the browser bundle
* `client.bundle.js`). Cordis requires every row to resolve to a plugin with
* an `apply` method, so this exports a minimal one. The real Host logic lives
* in `./tokenplan-usage` and `./memory`; the browser UI lives in
* `client.bundle.js`.
*
* @module bailian-cli-dsh
*/
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-cli-dsh";
/** No-op: this row exists only to serve the client bundle. */
export function apply(): void {}
-621
View File
@@ -1,621 +0,0 @@
/**
* `bailian-cli-dsh/memory` (Host half): cross-session long-term memory backed
* by Bailian's hosted memory library (DashScope memory v2 API).
*
* Provides:
* - Two model tools: `bailian_memory_search` (recall) + `bailian_memory_add`
* (store), plus `bailian_memory_list` (browse).
* - Auto-inject: on the first turn of each session (or every turn if
* configured), search memory and inject relevant facts into context.
* - Auto-persist: when a turn closes, send new user/assistant messages to
* the add API so future sessions can recall them.
* - webServer routes for the Client settings page to configure memory
* parameters (apiKey, baseUrl, userId, planVersion, etc.).
*
* Calls go straight to DashScope rather than through `bl memory`, because
* the v2 API exposes retrieval controls (`min_score`, `plan_version`,
* `enable_rerank`, `memory_library_id`, `enable_judge`, `enable_rewrite`)
* the CLI does not surface.
*
* BILLING: add and search are charged per call. `pro` costs ~50x `lite` per
* search. The `enable_rerank` flag is what actually selects the billing tier
* (verified: sending `plan_version: lite` alone still bills `pro`).
*
* @module bailian-cli-dsh/memory
*/
import { userInfo } from "node:os";
import type { Context } from "@deepseek-ai/cordis";
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
import type {} from "@deepseek-ai/dsh-agent";
import type { ContentBlock, Message } from "@deepseek-ai/dsh-llm";
import { createUserMessage } from "@deepseek-ai/dsh-llm";
import { defineTool } from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import type { IncomingMessage, ServerResponse } from "node:http";
import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts";
import { dashScopeFetch } from "../shared/http.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-memory";
/** Seams this plugin registers into. */
export const inject = ["tools", "agents", "webServer"];
export interface Config {
/** DashScope API key (pay-as-you-go sk-ws-). Falls back to $DASHSCOPE_API_KEY. */
apiKey?: string;
/** Memory API base URL (default: https://dashscope.aliyuncs.com/api/v2/apps/memory/). */
baseUrl?: string;
/** Memory entity id. Falls back to $BAILIAN_MEMORY_USER_ID, then OS user. */
userId?: string;
/** Memory library id; defaults to the account default. */
memoryLibraryId?: string;
/** Memory extraction rule id. */
projectId?: string;
/** Profile template id; omitting skips profile extraction (and its cost). */
profileSchema?: string;
/** Search strategy; pro enables rerank at ~50x the cost. */
planVersion?: "lite" | "pro";
topK?: number;
minScore?: number;
/** Retrieve relevant memories and inject into the conversation. */
autoInject?: boolean;
/** Retrieve every turn instead of once per session. */
injectEveryTurn?: boolean;
/** Persist each turn's new messages when the turn closes. */
autoPersist?: boolean;
}
export const Config: z<Config> = z.object({
apiKey: z.string().role("secret").description("Pay-as-you-go DashScope API key (sk-)."),
baseUrl: z.string().description("Memory API base URL."),
userId: z.string().description("Memory entity id owning these memories."),
memoryLibraryId: z.string().description("Memory library id."),
projectId: z.string().description("Memory extraction rule id."),
profileSchema: z.string().description("Profile template id; enables profile extraction."),
planVersion: z.union(["lite", "pro"] as const).description("Search strategy; pro ~50x cost."),
topK: z.natural().description("Maximum memories to recall (1-100)."),
minScore: z.number().description("Minimum similarity score, 0-1."),
autoInject: z.boolean().description("Inject recalled memories automatically."),
injectEveryTurn: z.boolean().description("Retrieve every turn instead of once per session."),
autoPersist: z.boolean().description("Persist new messages when a turn closes."),
});
const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/api/v2/apps/memory/";
const DEFAULT_TOP_K = 10;
const DEFAULT_PLAN_VERSION = "lite";
const CONFIG_ROUTE = "/bailian/memory/config";
const STATUS_ROUTE = "/bailian/memory/status";
interface MemoryNode {
memory_node_id?: string;
content?: string;
event?: string;
old_content?: string;
created_at?: number;
updated_at?: number;
meta_data?: Record<string, unknown>;
}
interface MemoryResponse {
request_id?: string;
memory_nodes?: readonly MemoryNode[];
total?: number;
page_num?: number;
page_size?: number;
billing_plan?: string;
}
interface ChatTurn {
role: "user" | "assistant";
content: string;
}
/** Mutable runtime config — updated via webServer route, initialized from Cordis config. */
interface MemoryRuntimeConfig {
apiKey: string | undefined;
baseUrl: string;
userId: string;
memoryLibraryId: string | undefined;
projectId: string | undefined;
profileSchema: string | undefined;
planVersion: "lite" | "pro";
topK: number;
minScore: number | undefined;
autoInject: boolean;
injectEveryTurn: boolean;
autoPersist: boolean;
}
/** Resolution order: explicit config, then env, then OS user. */
function resolveUserId(ctx: Context, config: Config): string {
if (config.userId !== undefined && config.userId.length > 0) return config.userId;
const fromEnv = ctx.get("launchEnvironment")?.get("BAILIAN_MEMORY_USER_ID")?.value;
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
return userInfo().username;
}
function textOf(content: readonly ContentBlock[]): string {
return content
.filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
.map((block) => block.text)
.join("\n")
.trim();
}
/** Plain user/assistant exchanges; tool traffic and injected context are not memories. */
function conversationTurns(messages: readonly Message[]): ChatTurn[] {
const turns: ChatTurn[] = [];
for (const message of messages) {
if (message.role !== "user" && message.role !== "assistant") continue;
if (message.role === "user" && message.source.kind !== "user") continue;
const text = textOf(message.content);
if (text.length > 0) turns.push({ role: message.role, content: text });
}
return turns;
}
/** Read a UTF-8 POST body up to a size limit. */
function readJsonBody(req: IncomingMessage, maxBytes: number = 16384): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let total = 0;
req.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > maxBytes) {
req.destroy();
reject(new Error("body too large"));
return;
}
chunks.push(chunk);
});
req.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
if (text.length === 0) return resolve({});
try {
resolve(JSON.parse(text));
} catch {
reject(new Error("invalid JSON"));
}
});
req.on("error", reject);
});
}
function sendJson(res: ServerResponse, status: number, data: unknown): void {
res.statusCode = status;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(data));
}
class MemoryClient {
constructor(
private readonly apiKey: string,
private readonly baseUrl: string,
private readonly cfg: MemoryRuntimeConfig,
private readonly userId: string,
) {}
private shared(): Record<string, unknown> {
return {
user_id: this.userId,
...(this.cfg.memoryLibraryId !== undefined
? { memory_library_id: this.cfg.memoryLibraryId }
: {}),
};
}
async add(
messages: readonly ChatTurn[],
signal: AbortSignal | undefined,
overrides?: { customContent?: string; metaData?: Record<string, unknown> },
): Promise<MemoryResponse> {
return dashScopeFetch<MemoryResponse>({
url: `${this.baseUrl}add`,
method: "POST",
apiKey: this.apiKey,
signal,
body: {
...this.shared(),
...(overrides?.customContent !== undefined
? { custom_content: overrides.customContent }
: { messages }),
...(this.cfg.projectId !== undefined ? { project_id: this.cfg.projectId } : {}),
...(this.cfg.profileSchema !== undefined ? { profile_schema: this.cfg.profileSchema } : {}),
...(overrides?.metaData !== undefined ? { meta_data: overrides.metaData } : {}),
},
});
}
async search(
messages: readonly ChatTurn[],
signal: AbortSignal | undefined,
overrides?: { topK?: number; minScore?: number; planVersion?: "lite" | "pro" },
): Promise<MemoryResponse> {
const planVersion = overrides?.planVersion ?? this.cfg.planVersion ?? DEFAULT_PLAN_VERSION;
return dashScopeFetch<MemoryResponse>({
url: `${this.baseUrl}memory_nodes/search`,
method: "POST",
apiKey: this.apiKey,
signal,
body: {
...this.shared(),
messages,
top_k: overrides?.topK ?? this.cfg.topK ?? DEFAULT_TOP_K,
...((overrides?.minScore ?? this.cfg.minScore) !== undefined
? { min_score: overrides?.minScore ?? this.cfg.minScore }
: {}),
// enable_rerank is what actually selects the billing tier (verified:
// plan_version alone still bills pro). Send both for safety.
enable_rerank: planVersion === "pro",
plan_version: planVersion,
...(this.cfg.projectId !== undefined ? { project_ids: [this.cfg.projectId] } : {}),
},
});
}
async list(
signal: AbortSignal | undefined,
overrides?: { pageNum?: number; pageSize?: number },
): Promise<MemoryResponse> {
const params = new URLSearchParams({
user_id: this.userId,
page_num: String(overrides?.pageNum ?? 1),
page_size: String(overrides?.pageSize ?? 10),
...(this.cfg.memoryLibraryId !== undefined
? { memory_library_id: this.cfg.memoryLibraryId }
: {}),
});
return dashScopeFetch<MemoryResponse>({
url: `${this.baseUrl}memory_nodes?${params.toString()}`,
method: "GET",
apiKey: this.apiKey,
signal,
});
}
}
function formatMemories(nodes: readonly MemoryNode[]): string {
const items = nodes
.map((node) => node.content?.trim())
.filter((content): content is string => content !== undefined && content.length > 0);
if (items.length === 0) return "";
return `What you remember about this user from earlier sessions:\n${items.map((item) => `- ${item}`).join("\n")}`;
}
/** Register model tools for deliberate memory operations. */
function registerTools(ctx: Context, client: () => MemoryClient | undefined): void {
ctx.tools.register(
defineTool({
name: "bailian_memory_search",
description:
"Recall facts stored about this user in earlier sessions. Use when the user refers to prior context, preferences, or decisions you have no record of in this session.",
parameters: {
query: { type: "string", required: true, description: "What to recall." },
top_k: { type: "integer", description: "Maximum memories to return (1-100)." },
min_score: { type: "number", description: "Minimum similarity score, 0-1." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
memories: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
id: { type: "string", required: true },
content: { type: "string", required: true },
},
},
},
},
},
render: (_args, value) => [
{
type: "text",
text:
value.memories.length === 0
? "No relevant memories."
: value.memories.map((m: any) => `- ${m.content}`).join("\n"),
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const mem = client();
if (mem === undefined)
throw new Error(
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
);
const result = await mem.search([{ role: "user", content: args.query }], exec.signal, {
...(args.top_k !== undefined ? { topK: args.top_k } : {}),
...(args.min_score !== undefined ? { minScore: args.min_score } : {}),
});
return {
memories: (result.memory_nodes ?? []).map((node) => ({
id: node.memory_node_id ?? "",
content: node.content ?? "",
})),
};
},
}),
);
ctx.tools.register(
defineTool({
name: "bailian_memory_add",
description:
"Store a durable fact about this user so later sessions can recall it. Use for stable preferences, decisions, and context — not for transient task state.",
parameters: {
content: { type: "string", required: true, description: "The fact to remember." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: { stored: { type: "integer", required: true } },
},
render: (_args, value) => [
{ type: "text", text: `Stored ${value.stored} memory fragment(s).` },
],
},
async execute(args, exec) {
const mem = client();
if (mem === undefined)
throw new Error(
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
);
const result = await mem.add([], exec.signal, { customContent: args.content });
return { stored: (result.memory_nodes ?? []).length };
},
}),
);
ctx.tools.register(
defineTool({
name: "bailian_memory_list",
description:
"List all stored memory fragments for this user. Use to review what the system already knows.",
parameters: {
page_size: { type: "integer", description: "Results per page (default 10)." },
page_num: { type: "integer", description: "Page number, starting from 1." },
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
memories: {
type: "array",
required: true,
items: {
type: "object",
additionalProperties: false,
properties: {
id: { type: "string", required: true },
content: { type: "string", required: true },
},
},
},
total: { type: "integer", required: true },
},
},
render: (_args, value) => [
{
type: "text",
text: `${value.total} memory fragment(s):\n${value.memories.map((m: any) => `- ${m.content}`).join("\n")}`,
},
],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const mem = client();
if (mem === undefined) throw new Error("bailian-memory: not configured.");
const result = await mem.list(exec.signal, {
...(args.page_size !== undefined ? { pageSize: args.page_size } : {}),
...(args.page_num !== undefined ? { pageNum: args.page_num } : {}),
});
return {
memories: (result.memory_nodes ?? []).map((node) => ({
id: node.memory_node_id ?? "",
content: node.content ?? "",
})),
total: result.total ?? 0,
};
},
}),
);
}
/** Auto-inject (search on first turn) + auto-persist (add on turn end). */
function registerAutoBehavior(
ctx: Context,
client: () => MemoryClient | undefined,
cfg: () => MemoryRuntimeConfig,
): void {
const injectedSessions = new WeakSet<Agent>();
const persistedCursor = new WeakMap<Agent, number>();
const currentCfg = cfg();
if (currentCfg.autoInject !== false) {
ctx.on(
"agent/pre-step",
async (
{
agent,
messages,
signal,
}: { agent: Agent; messages: readonly Message[]; signal: AbortSignal },
next: () => Promise<PreStepDecision>,
) => {
const decision = await next();
if (decision.kind !== "enter") return decision;
if (injectedSessions.has(agent) && currentCfg.injectEveryTurn !== true) return decision;
const query = textOf(messages.flatMap((m) => m.content));
if (query.length === 0) return decision;
const mem = client();
if (mem === undefined) return decision;
let nodes: readonly MemoryNode[] = [];
try {
const result = await mem.search([{ role: "user", content: query }], signal);
nodes = result.memory_nodes ?? [];
} catch {
return decision;
}
injectedSessions.add(agent);
if (nodes.length === 0) return decision;
const text = formatMemories(nodes);
return {
...decision,
messages: [
...decision.messages,
createUserMessage({
content: [{ type: "text", text }],
source: {
kind: "plugin",
plugin: name,
form: "snapshot",
sections: [{ name, text }],
},
}),
],
};
},
{ prepend: true },
);
}
if (currentCfg.autoPersist !== false) {
ctx.on(
"agent/turn-stopping",
async ({ agent, signal }: { agent: Agent; signal: AbortSignal }) => {
const mem = client();
if (mem === undefined) return;
const turns = conversationTurns(agent.session.deriveMessages());
const cursor = persistedCursor.get(agent) ?? 0;
const newTurns = turns.slice(cursor);
if (newTurns.length === 0) return;
persistedCursor.set(agent, turns.length);
try {
await mem.add(newTurns, signal);
} catch {
persistedCursor.set(agent, cursor);
}
},
);
}
}
export function apply(ctx: Context, config: Config): void {
const webServer = ctx.get("webServer");
// Mutable runtime config — initialized from Cordis config, updatable via webServer route.
let runtime: MemoryRuntimeConfig = {
apiKey: config.apiKey,
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
userId: resolveUserId(ctx, config),
memoryLibraryId: config.memoryLibraryId,
projectId: config.projectId,
profileSchema: config.profileSchema,
planVersion: config.planVersion ?? DEFAULT_PLAN_VERSION,
topK: config.topK ?? DEFAULT_TOP_K,
minScore: config.minScore,
autoInject: config.autoInject ?? true,
injectEveryTurn: config.injectEveryTurn ?? false,
autoPersist: config.autoPersist ?? true,
};
/** Build a MemoryClient from the current runtime config, or undefined if no API key. */
function buildClient(): MemoryClient | undefined {
if (runtime.apiKey === undefined || runtime.apiKey.length === 0) return undefined;
if (isTokenPlanKey(runtime.apiKey)) {
throw new Error(tokenPlanKeyRejection(name, "the memory API"));
}
return new MemoryClient(runtime.apiKey, runtime.baseUrl, runtime, runtime.userId);
}
// Register tools + auto behavior.
registerTools(ctx, buildClient);
registerAutoBehavior(ctx, buildClient, () => runtime);
// webServer routes for the Client settings page.
if (webServer !== undefined) {
ctx.effect(() =>
webServer.register({
kind: "exact",
path: STATUS_ROUTE,
handler: async (_req: IncomingMessage, res: ServerResponse) => {
sendJson(res, 200, {
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
userId: runtime.userId,
baseUrl: runtime.baseUrl,
planVersion: runtime.planVersion,
topK: runtime.topK,
autoInject: runtime.autoInject,
injectEveryTurn: runtime.injectEveryTurn,
autoPersist: runtime.autoPersist,
memoryLibraryId: runtime.memoryLibraryId,
});
},
}),
);
ctx.effect(() =>
webServer.register({
kind: "exact",
path: CONFIG_ROUTE,
handler: async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "POST") {
sendJson(res, 405, { error: "use POST" });
return;
}
let body: Record<string, unknown>;
try {
body = (await readJsonBody(req)) as Record<string, unknown>;
} catch (error) {
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
return;
}
// Update mutable fields from the request body.
if (typeof body.apiKey === "string") runtime.apiKey = body.apiKey || undefined;
if (typeof body.baseUrl === "string" && body.baseUrl.length > 0)
runtime.baseUrl = body.baseUrl;
if (typeof body.userId === "string" && body.userId.length > 0)
runtime.userId = body.userId;
if (typeof body.memoryLibraryId === "string")
runtime.memoryLibraryId = body.memoryLibraryId || undefined;
if (typeof body.projectId === "string") runtime.projectId = body.projectId || undefined;
if (typeof body.profileSchema === "string")
runtime.profileSchema = body.profileSchema || undefined;
if (body.planVersion === "lite" || body.planVersion === "pro")
runtime.planVersion = body.planVersion;
if (typeof body.topK === "number") runtime.topK = body.topK;
if (typeof body.minScore === "number") runtime.minScore = body.minScore;
if (typeof body.autoInject === "boolean") runtime.autoInject = body.autoInject;
if (typeof body.injectEveryTurn === "boolean")
runtime.injectEveryTurn = body.injectEveryTurn;
if (typeof body.autoPersist === "boolean") runtime.autoPersist = body.autoPersist;
sendJson(res, 200, {
ok: true,
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
});
},
}),
);
}
}
-161
View File
@@ -1,161 +0,0 @@
/**
* Shared `bl` invocation for the plugins that delegate to the Bailian CLI
* rather than calling DashScope directly — the ones whose CLI implementation
* carries real substance (async task polling, artifact download, SSE session
* streaming, `agents.yaml` resolution) that a plugin should not restate.
* @module bailian-cli-dsh/shared/bl
*/
import type { Context } from "@deepseek-ai/cordis";
import type { SubprocessSpawnSpec } from "@deepseek-ai/dsh-subprocess";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
const DEFAULT_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
const DEFAULT_STDERR_MAX_BYTES = 64 * 1024;
const DEFAULT_GRACE_MS = 5_000;
/**
* Environment names `bl` reads for credentials, endpoint routing, and profile
* selection. `scrubbedParentEnv()` strips credential-shaped names from every
* harness child, so the key would never reach `bl` unless forwarded here.
*/
const FORWARDED_ENV_NAMES = [
"DASHSCOPE_API_KEY",
"DASHSCOPE_BASE_URL",
"DASHSCOPE_TIMEOUT",
"BAILIAN_WORKSPACE_ID",
"BAILIAN_CONFIG_DIR",
"ALIBABA_CLOUD_ACCESS_KEY_ID",
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
"ALIBABA_CLOUD_SECURITY_TOKEN",
] as const;
/** A `bl` invocation that exited non-zero or produced unreadable output. */
export class BlError extends Error {
constructor(
message: string,
readonly detail: { argv: readonly string[]; exitCode: number | null; stderr: string },
options?: { cause?: unknown },
) {
super(message, options);
this.name = "BlError";
}
}
export interface RunBlOptions {
/** Working directory for the child; callers pass the session cwd. */
cwd: string;
signal: AbortSignal;
/** Extra entries layered after the forwarded Bailian names. */
env?: NodeJS.ProcessEnv;
stdoutMaxBytes?: number;
graceMs?: number;
}
export interface BlOutcome {
stdout: string;
stderr: string;
exitCode: number | null;
terminatedBy: NodeJS.Signals | null;
}
function abortError(): DOMException {
return new DOMException("bl invocation aborted", "AbortError");
}
function forwardedEnv(ctx: Context, extra: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
const launchEnvironment = launchEnvironmentOf(ctx);
const env: NodeJS.ProcessEnv = {};
for (const name of FORWARDED_ENV_NAMES) {
const entry = launchEnvironment.get(name);
if (entry !== undefined) env[name] = entry.value;
}
return { ...env, ...extra };
}
/**
* Run `bl` to completion and collect its output.
* @throws {BlError} when the executable cannot be resolved.
* @throws {DOMException} `AbortError` when the caller's signal fires.
*/
export async function runBl(
ctx: Context,
argv: readonly string[],
options: RunBlOptions,
): Promise<BlOutcome> {
if (options.signal.aborted) throw abortError();
const env = forwardedEnv(ctx, options.env);
let executable: string;
try {
executable = await ctx.subprocess.resolveExecutable(
"bl",
env as Readonly<Record<string, string>>,
options.signal,
);
} catch (error) {
throw new BlError(
"the `bl` executable was not found on PATH; install it with `npm install -g bailian-cli`",
{ argv, exitCode: null, stderr: "" },
{ cause: error },
);
}
const spec: SubprocessSpawnSpec = {
argv: [executable, ...argv],
cwd: options.cwd,
stdio: {
stdin: "ignore",
stdout: { maxBytes: options.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES },
stderr: { maxBytes: DEFAULT_STDERR_MAX_BYTES },
},
graceMs: options.graceMs ?? DEFAULT_GRACE_MS,
signal: options.signal,
env,
};
const handle = ctx.subprocess.spawn(spec);
if (options.signal.aborted) throw abortError();
const outcome = await handle.done;
if (options.signal.aborted) throw abortError();
return {
stdout: handle.collected.stdout?.readFrom(0).text ?? "",
stderr: handle.collected.stderr?.readFrom(0).text ?? "",
exitCode: outcome.exitCode,
terminatedBy: outcome.signal,
};
}
/**
* Run `bl … --output json` and parse stdout.
* @throws {BlError} on non-zero exit or unparseable stdout.
*/
export async function runBlJson<T>(
ctx: Context,
argv: readonly string[],
options: RunBlOptions,
): Promise<T> {
const withJson = [...argv, "--output", "json"];
const outcome = await runBl(ctx, withJson, options);
if (outcome.exitCode !== 0) {
// bl passes service errors through verbatim; surface them unchanged.
const reason = outcome.stderr.trim() || outcome.stdout.trim() || "no diagnostics on stderr";
throw new BlError(`bl ${argv.join(" ")} failed: ${reason}`, {
argv: withJson,
exitCode: outcome.exitCode,
stderr: outcome.stderr,
});
}
try {
return JSON.parse(outcome.stdout) as T;
} catch (error) {
throw new BlError(
`bl ${argv.join(" ")} did not emit JSON on stdout`,
{ argv: withJson, exitCode: outcome.exitCode, stderr: outcome.stderr },
{ cause: error },
);
}
}
-93
View File
@@ -1,93 +0,0 @@
/**
* Pure credential classification and pairing shared by the plugins that call
* pay-as-you-go DashScope APIs directly (memory, knowledge base) or through
* `bl managed-agent` (agentstudio). No runtime imports — this module is safe
* to load from tests and its rules are locked by `tests/credentials.test.ts`.
*
* TokenPlan keys (`sk-sp-`) and pay-as-you-go keys (`sk-ws-`) are not
* interchangeable: the TokenPlan gateway 401s a pay-as-you-go key, and the
* service APIs this package calls 401 or 404 a TokenPlan key. The LLM
* provider row keeps its TokenPlan key under a dedicated env name
* (`BAILIAN_TOKENPLAN_API_KEY`); every other plugin needs a pay-as-you-go key
* and rejects a TokenPlan one up front instead of failing at request time.
*
* @module bailian-cli-dsh/shared/credentials
*/
/**
* Standard DashScope model-domain endpoint. It serves the model APIs plus the
* memory v2 and knowledge indices the plugins call directly — but NOT
* `/api/v1/agentstudio`, which lives on the workspace-scoped host.
*/
export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com";
/** Key prefix that marks a TokenPlan key (which service APIs reject). */
export const TOKEN_PLAN_KEY_PREFIX = "sk-sp-";
/** Whether a key is shaped like a TokenPlan key (which service APIs reject). */
export function isTokenPlanKey(apiKey: string): boolean {
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
}
/**
* Whether a base URL points at the TokenPlan gateway. That gateway serves the
* model-inference routes only — none of the service APIs this package calls,
* including `/api/v1/agentstudio`, so requests to it 404.
*/
export function isTokenPlanEndpoint(baseUrl: string): boolean {
try {
return new URL(baseUrl).hostname.startsWith("token-plan.");
} catch {
// An unparseable URL fails the request later with its own diagnostics;
// this check only classifies well-formed endpoints.
return false;
}
}
/**
* The standard error wording every plugin uses when it resolves a TokenPlan
* key, so all three surfaces fail with one recognizable, actionable message.
*/
export function tokenPlanKeyRejection(plugin: string, capability: string): string {
return (
`${plugin}: the resolved API key is a TokenPlan key (${TOKEN_PLAN_KEY_PREFIX}…), which ` +
`${capability} rejects. Use a pay-as-you-go key (sk-ws-): set \`apiKey\` in this row's ` +
"config or $DASHSCOPE_API_KEY. TokenPlan keys belong on $BAILIAN_TOKENPLAN_API_KEY, " +
"which only the `bailian-tokenplan` LLM provider reads."
);
}
/**
* Build the `--api-key` / `--base-url` flags handed to `bl managed-agent run`.
* Each resolved half ships independently:
*
* - A resolved key becomes `--api-key`, overriding bl's auth chain so an
* active TokenPlan profile cannot substitute its own key.
* - A resolved endpoint becomes `--base-url`, overriding the ACTIVE PROFILE's
* base_url — the half that fixes the classic `Bailian API 404`, where a
* TokenPlan (or bare model-domain) origin does not serve
* `/api/v1/agentstudio`.
*
* There is deliberately NO fallback endpoint: agentstudio is only served on
* the workspace-scoped host (see {@link workspaceEndpoint}), and an unknown
* workspace is a configuration gap, not a defaultable value. Unresolved halves
* emit nothing and bl's own auth chain decides them.
*/
export function credentialFlags(apiKey: string | undefined, baseUrl: string | undefined): string[] {
const flags: string[] = [];
if (baseUrl !== undefined && baseUrl.length > 0) flags.push("--base-url", baseUrl);
if (apiKey !== undefined && apiKey.length > 0) flags.push("--api-key", apiKey);
return flags;
}
/**
* Compose the workspace-scoped agentstudio host for a workspace id. The
* managed-agent API is served only from
* `https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`
* (bl/the SDK append the resource path onto this origin); the plain
* dashscope origin 404s it, and a key only unlocks its own workspace's host
* (a mismatched one 403s `Endpoint.AccessDenied`).
*/
export function workspaceEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`;
}
-101
View File
@@ -1,101 +0,0 @@
/**
* Direct DashScope HTTP for the plugins whose CLI counterpart does not expose
* the full parameter surface (long-term memory, knowledge-base retrieval).
* Service errors pass through verbatim — this layer classifies nothing.
* @module bailian-cli-dsh/shared/http
*/
import type { Context } from "@deepseek-ai/cordis";
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
import { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
export { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
/** A non-2xx DashScope response, carrying the server's own wording. */
export class DashScopeError extends Error {
constructor(
message: string,
readonly detail: { status: number; code?: string; requestId?: string },
options?: { cause?: unknown },
) {
super(message, options);
this.name = "DashScopeError";
}
}
/**
* Resolve the DashScope key: explicit row config first, then the launch
* environment (process env, project `.env`, harness-home `.env`). Callers
* that get `undefined` decide their own failure mode — opt-in plugins reject
* at boot, the managed-agent tool falls through to bl's own auth chain.
*/
export function resolveApiKey(ctx: Context, explicit?: string): string | undefined {
if (explicit !== undefined && explicit.length > 0) return explicit;
const entry = launchEnvironmentOf(ctx).get("DASHSCOPE_API_KEY");
return entry !== undefined && entry.value.length > 0 ? entry.value : undefined;
}
export function resolveBaseUrl(ctx: Context, explicit?: string): string {
if (explicit !== undefined && explicit.length > 0) return explicit;
const entry = launchEnvironmentOf(ctx).get("DASHSCOPE_BASE_URL");
return entry !== undefined && entry.value.length > 0 ? entry.value : DASHSCOPE_DEFAULT_BASE_URL;
}
export interface DashScopeRequest {
url: string;
method: "GET" | "POST" | "PATCH" | "DELETE";
apiKey: string;
body?: unknown;
signal?: AbortSignal | undefined;
}
interface DashScopeErrorBody {
code?: string;
message?: string;
request_id?: string;
error?: { code?: string; message?: string };
}
/**
* Issue one DashScope request and parse its JSON body.
* @throws {DashScopeError} on a non-2xx response or an unreadable body.
*/
export async function dashScopeFetch<T>(request: DashScopeRequest): Promise<T> {
const response = await fetch(request.url, {
method: request.method,
headers: {
Authorization: `Bearer ${request.apiKey}`,
"Content-Type": "application/json",
},
...(request.body !== undefined ? { body: JSON.stringify(request.body) } : {}),
...(request.signal !== undefined ? { signal: request.signal } : {}),
redirect: "error",
});
const text = await response.text();
if (!response.ok) {
let parsed: DashScopeErrorBody = {};
try {
parsed = JSON.parse(text) as DashScopeErrorBody;
} catch {
// A non-JSON error body is still worth surfacing as-is.
}
const code = parsed.code ?? parsed.error?.code;
const message = parsed.message ?? parsed.error?.message ?? text.trim();
throw new DashScopeError(message.length > 0 ? message : `HTTP ${response.status}`, {
status: response.status,
...(code !== undefined ? { code } : {}),
...(parsed.request_id !== undefined ? { requestId: parsed.request_id } : {}),
});
}
try {
return JSON.parse(text) as T;
} catch (error) {
throw new DashScopeError(
"DashScope returned a non-JSON success body",
{ status: response.status },
{ cause: error },
);
}
}
-423
View File
@@ -1,423 +0,0 @@
/**
* `bailian-cli-dsh/tokenplan-usage` (Host half): provides two webServer
* routes for the Client's "Bailian" settings page:
*
* 1. `POST /api/bailian/credentials` — saves AK/SK to the dedicated `dsh`
* bl profile via `bl auth login --open-api --config dsh`. This generates
* a fresh access_token and stores AK/SK + token in the profile. All
* subsequent console calls read this profile.
*
* 2. `POST /api/bailian/tokenplan/usage` — fetches personal-edition
* TokenPlan usage (3 console APIs) using the `dsh` profile credentials.
* Takes only `{ region, site }`; AK/SK are already saved in the profile.
*
* Configuration UX: users save AK/SK once on the settings page. All future
* Bailian plugins reuse the same `dsh` profile credentials.
*
* @module bailian-cli-dsh/tokenplan-usage
*/
import type { Context } from "@deepseek-ai/cordis";
import type { IncomingMessage, ServerResponse } from "node:http";
import { runBl } from "../shared/bl.ts";
import { defineTool } from "@deepseek-ai/dsh-tools";
import type { JsonValue } from "@deepseek-ai/dsh-session";
import { FEATURES, featureById, type FeatureParam } from "../features.ts";
import z from "@deepseek-ai/schemastery";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-tokenplan-usage";
/** Hard deps: bl via subprocess; routes need webServer; feature tools need tools. */
export const inject = ["subprocess", "webServer", "tools"];
export interface Config {
/** Alibaba Cloud Access Key ID. Fallback when not provided via UI. */
accessKeyId?: string;
/** Alibaba Cloud Access Key Secret. Fallback when not provided via UI. */
accessKeySecret?: string;
/** Console gateway region (default: cn-beijing). */
consoleRegion?: string;
/** Console site: domestic or international (default: domestic). */
consoleSite?: "domestic" | "international";
/** Dedicated bl config profile name (default: dsh). */
profile?: string;
}
export const Config = z.object({
accessKeyId: z.string().description("Alibaba Cloud Access Key ID (fallback)."),
accessKeySecret: z.string().description("Alibaba Cloud Access Key Secret (fallback)."),
consoleRegion: z.string().description("Console gateway region (default: cn-beijing)."),
consoleSite: z.string().description("Console site: domestic or international."),
profile: z.string().description("Dedicated bl config profile name (default: dsh)."),
});
/** Personal-edition console API names (from bailian-tokenplan frontend). */
const PERSONAL_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
const PERSONAL_SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
const PERSONAL_ADDON_SUMMARY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
const PERSONAL_SUB_COMMODITY_CN = "sfm_tokenplansolo_public_cn";
const PERSONAL_SUB_COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
const PERSONAL_ADDON_COMMODITY_CN = "sfm_tokenplansoloaddon_public_cn";
const PERSONAL_ADDON_COMMODITY_INTL = "sfm_tokenplansoloaddon_public_intl";
const CREDENTIALS_ROUTE = "/bailian/credentials";
const USAGE_ROUTE = "/bailian/tokenplan/usage";
const CONSOLE_ROUTE = "/bailian/console";
const BL_LOGIN_TIMEOUT_MS = 30_000;
const BL_CALL_TIMEOUT_MS = 90_000;
const BL_LOGIN_GRACE_MS = 20_000;
const BL_CALL_GRACE_MS = 60_000;
const DEFAULT_PROFILE = "dsh";
interface FetchResult {
usage: unknown;
subscription: unknown;
addonSummary: unknown;
errors: Array<{ api: string; message: string }>;
}
/** Extract the business payload from a console gateway response. */
function extractData(response: unknown): unknown {
if (response === null || typeof response !== "object") return response;
const outer = (response as Record<string, unknown>).data;
if (outer !== null && typeof outer === "object") {
const dataV2 = (outer as Record<string, unknown>).DataV2;
if (dataV2 !== null && typeof dataV2 === "object") {
const inner = (dataV2 as Record<string, unknown>).data;
if (inner !== null && typeof inner === "object") {
const payload = (inner as Record<string, unknown>).data;
if (payload !== undefined) return payload;
return inner;
}
}
const fallback = (outer as Record<string, unknown>).data;
if (fallback !== undefined) return fallback;
}
return response;
}
/** Read a UTF-8 POST body up to a size limit. */
function readJsonBody(req: IncomingMessage, maxBytes: number = 8192): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let total = 0;
req.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > maxBytes) {
req.destroy();
reject(new Error("request body too large"));
return;
}
chunks.push(chunk);
});
req.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
if (text.length === 0) return resolve({});
try {
resolve(JSON.parse(text));
} catch {
reject(new Error("invalid JSON body"));
}
});
req.on("error", reject);
});
}
/** Send a JSON response with a status code. */
function sendJson(res: ServerResponse, status: number, data: unknown): void {
res.statusCode = status;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(data));
}
export function apply(ctx: Context, config: Config): void {
const webServer = ctx.get("webServer");
if (webServer === undefined) return;
const profile = config.profile || DEFAULT_PROFILE;
/** Save AK/SK to the dsh profile (bl auth login --open-api --config dsh). */
async function saveCredentials(accessKeyId: string, accessKeySecret: string): Promise<void> {
const loginArgs = [
"auth",
"login",
"--open-api",
"--config",
profile,
"--access-key-id",
accessKeyId,
"--access-key-secret",
accessKeySecret,
];
const loginOutcome = await runBl(ctx, loginArgs, {
cwd: process.cwd(),
signal: AbortSignal.timeout(BL_LOGIN_TIMEOUT_MS),
graceMs: BL_LOGIN_GRACE_MS,
});
if (loginOutcome.exitCode !== 0) {
const reason =
loginOutcome.stderr.trim() || loginOutcome.stdout.trim() || `exit ${loginOutcome.exitCode}`;
throw new Error(`bl auth login failed: ${reason}`);
}
}
/** Call a console API using the dsh profile (credentials already saved). */
async function consoleCall(
region: string,
site: string,
api: string,
data: Record<string, unknown>,
): Promise<unknown> {
const callArgs = [
"console",
"call",
"--config",
profile,
"--api",
api,
"--data",
JSON.stringify(data),
"--console-region",
region,
"--console-site",
site,
"--output",
"json",
];
const callOutcome = await runBl(ctx, callArgs, {
cwd: process.cwd(),
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
graceMs: BL_CALL_GRACE_MS,
});
if (callOutcome.exitCode !== 0) {
const reason =
callOutcome.stderr.trim() || callOutcome.stdout.trim() || `exit ${callOutcome.exitCode}`;
throw new Error(`bl console call failed (${api}): ${reason}`);
}
try {
return JSON.parse(callOutcome.stdout);
} catch {
return { raw: callOutcome.stdout };
}
}
/** Fetch all personal-edition TokenPlan usage (3 console calls). */
async function fetchUsage(region: string, site: string): Promise<FetchResult> {
const isIntl = site === "international";
const subCommodity = isIntl ? PERSONAL_SUB_COMMODITY_INTL : PERSONAL_SUB_COMMODITY_CN;
const addonCommodity = isIntl ? PERSONAL_ADDON_COMMODITY_INTL : PERSONAL_ADDON_COMMODITY_CN;
const errors: Array<{ api: string; message: string }> = [];
let usage = null;
let subscription = null;
let addonSummary = null;
try {
usage = extractData(await consoleCall(region, site, PERSONAL_USAGE_API, {}));
} catch (error) {
errors.push({
api: "usage",
message: error instanceof Error ? error.message : String(error),
});
}
try {
subscription = extractData(
await consoleCall(region, site, PERSONAL_SUBSCRIPTION_API, {
queryInstanceInfoRequest: { commodityCode: subCommodity },
}),
);
} catch (error) {
errors.push({
api: "subscription",
message: error instanceof Error ? error.message : String(error),
});
}
try {
addonSummary = extractData(
await consoleCall(region, site, PERSONAL_ADDON_SUMMARY_API, {
commodityCode: addonCommodity,
}),
);
} catch (error) {
errors.push({
api: "addonSummary",
message: error instanceof Error ? error.message : String(error),
});
}
return { usage, subscription, addonSummary, errors };
}
// Route 1: Save credentials to the dsh bl profile.
ctx.effect(() =>
webServer.register({
kind: "exact",
path: CREDENTIALS_ROUTE,
handler: async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "POST") {
sendJson(res, 405, { error: "method not allowed, use POST" });
return;
}
let body: Record<string, unknown>;
try {
body = (await readJsonBody(req)) as Record<string, unknown>;
} catch (error) {
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
return;
}
const accessKeyId = (body.accessKeyId as string) || config.accessKeyId;
const accessKeySecret = (body.accessKeySecret as string) || config.accessKeySecret;
if (!accessKeyId || !accessKeySecret) {
sendJson(res, 400, { error: "accessKeyId and accessKeySecret are required." });
return;
}
try {
await saveCredentials(accessKeyId, accessKeySecret);
sendJson(res, 200, { ok: true, profile });
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
}
},
}),
);
// Route 2: Fetch TokenPlan usage using the dsh profile credentials.
ctx.effect(() =>
webServer.register({
kind: "exact",
path: USAGE_ROUTE,
handler: async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "POST") {
sendJson(res, 405, { error: "method not allowed, use POST" });
return;
}
let body: Record<string, unknown>;
try {
body = (await readJsonBody(req)) as Record<string, unknown>;
} catch (error) {
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
return;
}
// If AK/SK are provided in the body, save them first (auto-provision).
const bodyKeyId = (body.accessKeyId as string) || undefined;
const bodyKeySecret = (body.accessKeySecret as string) || undefined;
if (bodyKeyId && bodyKeySecret) {
try {
await saveCredentials(bodyKeyId, bodyKeySecret);
} catch (error) {
sendJson(res, 500, {
error: error instanceof Error ? error.message : "credential save failed",
});
return;
}
}
const region = (body.region as string) || config.consoleRegion || "cn-beijing";
const site = (body.site as string) || config.consoleSite || "domestic";
try {
const result = await fetchUsage(region, site);
sendJson(res, 200, result);
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
}
},
}),
);
// ── Feature layer: reuse bailian-cli commands as model tools + a generic route ──
/** Run a feature's `bl` command with the dsh profile; returns parsed JSON. */
async function invokeFeature(
feature: (typeof FEATURES)[number],
params?: Record<string, unknown>,
): Promise<JsonValue> {
const extra: string[] = [];
for (const pf of feature.paramFlags ?? []) {
const val = params?.[pf.name];
if (val !== undefined && val !== null && val !== "") extra.push(pf.flag, String(val));
}
if (extra.length === 0 && feature.defaultArgs) extra.push(...feature.defaultArgs);
const args = [...feature.argv, ...extra, "--config", profile, "--output", "json"];
const outcome = await runBl(ctx, args, {
cwd: process.cwd(),
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
graceMs: BL_CALL_GRACE_MS,
});
if (outcome.exitCode !== 0) {
const reason = outcome.stderr.trim() || outcome.stdout.trim() || `exit ${outcome.exitCode}`;
throw new Error(`bl ${feature.argv.join(" ")} failed: ${reason}`);
}
try {
return JSON.parse(outcome.stdout);
} catch {
return { raw: outcome.stdout };
}
}
// Natural-language entry: one model tool per feature.
const tools = ctx.get("tools");
if (tools !== undefined) {
for (const feature of FEATURES) {
// Keep FeatureParam's literal `type` union: widening it to `string`
// makes the map unassignable to ParameterSchemaSpec.
const parameters: Record<string, { type: FeatureParam["type"]; description: string }> = {};
for (const pf of feature.paramFlags ?? []) {
parameters[pf.name] = { type: pf.type, description: pf.description };
}
ctx.effect(() =>
tools.register(
defineTool({
name: `bailian_${feature.id}`,
description: `${feature.title}。${feature.intent}`,
parameters,
output: {
schema: { type: "object", additionalProperties: true },
render: (_a, value) => [
{ type: "text", text: String((value as any).summary ?? JSON.stringify(value)) },
],
},
async execute(args) {
const data = await invokeFeature(feature, args as Record<string, unknown>);
return { summary: feature.summarize(data), data };
},
}),
),
);
}
}
// Card-click entry: generic route dispatching to a feature by id.
ctx.effect(() =>
webServer.register({
kind: "exact",
path: CONSOLE_ROUTE,
handler: async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "POST") {
sendJson(res, 405, { error: "method not allowed, use POST" });
return;
}
let body: Record<string, unknown>;
try {
body = (await readJsonBody(req)) as Record<string, unknown>;
} catch (error) {
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
return;
}
const feature = featureById(String(body.featureId ?? ""));
if (feature === undefined) {
sendJson(res, 400, { error: `unknown featureId: ${String(body.featureId)}` });
return;
}
try {
const data = await invokeFeature(
feature,
body.params as Record<string, unknown> | undefined,
);
sendJson(res, 200, { summary: feature.summarize(data), data });
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
}
},
}),
);
}
-59
View File
@@ -1,59 +0,0 @@
import { expect, test } from "vite-plus/test";
import {
credentialFlags,
DASHSCOPE_DEFAULT_BASE_URL,
isTokenPlanEndpoint,
isTokenPlanKey,
workspaceEndpoint,
} from "../src/shared/credentials.ts";
// 行为锁定:两类 Key(sk-sp- TokenPlan / sk-ws- 按量付费)不可混用。
// TokenPlan 网关 401 按量付费 Key,TokenPlan 网关只提供模型推理,不提供
// 服务 API。managed-agent 的凭证两半独立下发:解析出 key 就显式
// --api-key(不让 bl 用活动 profile 的 key),解析出端点就显式 --base-url
// (不让 bl 用活动 profile 的端点)。agentstudio 只在工作空间前缀主机上提供,
// 因此绝不存在"默认端点"——工作空间未知就是配置缺口,该报错而不是猜。
// 这些共享函数来自早期版本(vision / image / managed-agent 等工具),
// 工具已移除但凭证分类逻辑保留作为参考。
test("isTokenPlanKey classifies by prefix", () => {
expect(isTokenPlanKey("sk-sp-abc123")).toBe(true);
expect(isTokenPlanKey("sk-ws-abc123")).toBe(false);
expect(isTokenPlanKey("")).toBe(false);
});
test("isTokenPlanEndpoint classifies the gateway host", () => {
expect(isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com")).toBe(true);
expect(
isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"),
).toBe(true);
expect(isTokenPlanEndpoint(DASHSCOPE_DEFAULT_BASE_URL)).toBe(false);
expect(isTokenPlanEndpoint(workspaceEndpoint("llm-x"))).toBe(false);
// 不可解析的 URL 交给后续请求自己报错,这里只做形状分类。
expect(isTokenPlanEndpoint("not a url")).toBe(false);
});
test("workspaceEndpoint composes the workspace-scoped agentstudio host", () => {
expect(workspaceEndpoint("llm-kpgesh4vqzf5gzv9")).toBe(
"https://llm-kpgesh4vqzf5gzv9.cn-beijing.maas.aliyuncs.com",
);
expect(workspaceEndpoint("ws_abc")).toBe("https://ws_abc.cn-beijing.maas.aliyuncs.com");
});
test("credentialFlags: each resolved half ships independently, no defaults", () => {
expect(credentialFlags(undefined, undefined)).toEqual([]);
expect(credentialFlags("", "")).toEqual([]);
// 只有 key:端点留给 bl 解析,绝不塞一个会 404 的默认主机。
expect(credentialFlags("sk-ws-abc", undefined)).toEqual(["--api-key", "sk-ws-abc"]);
// 只有端点:也下发,key 留给 bl 的 auth chain。
expect(credentialFlags(undefined, "https://ws.example.com")).toEqual([
"--base-url",
"https://ws.example.com",
]);
expect(credentialFlags("sk-ws-abc", "https://ws.example.com")).toEqual([
"--base-url",
"https://ws.example.com",
"--api-key",
"sk-ws-abc",
]);
});
-20
View File
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["es2023"],
"moduleDetection": "force",
"module": "nodenext",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"noUnusedLocals": true,
"declaration": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
}
}
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig } from "vite-plus";
export default defineConfig({
pack: {
entry: ["src/index.ts", "src/tokenplan-usage/index.ts", "src/memory/index.ts"],
minify: true,
dts: {
tsgo: true,
},
},
lint: {
options: {
typeAware: true,
typeCheck: true,
},
},
fmt: {},
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.14.2",
"version": "1.14.0",
"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.0",
"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": {
@@ -171,7 +171,7 @@ export async function imageGenerate(
});
}
const model = input.model || "qwen-image-3.0";
const model = input.model || "qwen-image-2.0";
const route = resolveImageGenerateApi(model);
const n = input.n ?? 1;
@@ -269,7 +269,7 @@ export async function imageEdit(
}
const images = Array.isArray(input.image) ? input.image : input.image ? [input.image] : [];
const model = input.model || "qwen-image-3.0";
const model = input.model || "qwen-image-2.0";
const route = resolveImageEditApi(model);
const n = input.n ?? 1;
+1 -1
View File
@@ -8,7 +8,7 @@ import type { ImageSizeProfile } from "bailian-cli-core";
* Do not infer size from sync/async — that mismatches model constraints.
*/
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets(3.0 经 profile 复用此表). */
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets. */
export const QWEN_IMAGE_20_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
+339 -1878
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -24,10 +24,6 @@ catalogMode: prefer
overrides:
vite: "catalog:"
vitest: "catalog:"
# The @deepseek-ai/dsh-* rc line (used only by bailian-cli-dsh) peers on
# packages that were never published: dsh-type-meta, dsh-environment,
# dsh-tasks. Auto-installing peers therefore 404s the whole workspace.
autoInstallPeers: false
peerDependencyRules:
allowAny:
- vite
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.14.2"
version: "1.14.0"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -45,5 +45,5 @@ bl file upload --file audio.wav --model qwen3-asr-flash
```
```bash
bl file upload --file cat.png --model qwen-image-3.0
bl file upload --file cat.png --model qwen-image-2.0
```
+22 -29
View File
@@ -36,11 +36,7 @@ Use this index for the skill-scoped quick index and global flags.
| `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 delete` | Delete a profile schema | [memory.md](memory.md) |
| `bl memory profile detail` | Show a profile schema and its attribute IDs | [memory.md](memory.md) |
| `bl memory profile get` | Get user profile by schema ID and user ID | [memory.md](memory.md) |
| `bl memory profile list` | List profile schemas | [memory.md](memory.md) |
| `bl memory profile update` | Update a profile schema's name, description, or attributes | [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) |
@@ -56,7 +52,6 @@ Use this index for the skill-scoped quick index and global flags.
| `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) |
@@ -65,8 +60,6 @@ Use this index for the skill-scoped quick index and global flags.
| `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 token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account | [token-plan.md](token-plan.md) |
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) | [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) |
@@ -77,28 +70,28 @@ Use this index for the skill-scoped quick index and global flags.
## 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 delete`, `profile detail`, `profile get`, `profile list`, `profile update`, `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`, `personal-key`, `personal-usage` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
| Group | Commands | Reference |
| ------------ | ---------------------------------------------------------------------------- | ------------------------------ |
| `advisor` | `recommend` | [advisor.md](advisor.md) |
| `app` | `call`, `list` | [app.md](app.md) |
| `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) |
| `config` | `agent`, `list`, `set`, `show`, `ui`, `use` | [config.md](config.md) |
| `console` | `call` | [console.md](console.md) |
| `file` | `upload` | [file.md](file.md) |
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
| `model` | `list` | [model.md](model.md) |
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `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
+43 -178
View File
@@ -7,19 +7,15 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| -------------------------- | ---------------------------------------------------------- |
| `bl memory add` | Add memory from messages or custom content |
| `bl memory delete` | Delete a memory node |
| `bl memory list` | List memory nodes for a user |
| `bl memory profile create` | Create a user profile schema for memory profiling |
| `bl memory profile delete` | Delete a profile schema |
| `bl memory profile detail` | Show a profile schema and its attribute IDs |
| `bl memory profile get` | Get user profile by schema ID and user ID |
| `bl memory profile list` | List profile schemas |
| `bl memory profile update` | Update a profile schema's name, description, or attributes |
| `bl memory search` | Search memory nodes by query or messages |
| `bl memory update` | Update a memory node content |
| Command | Description |
| -------------------------- | ------------------------------------------------- |
| `bl memory add` | Add memory from messages or custom content |
| `bl memory delete` | Delete a memory node |
| `bl memory list` | List memory nodes for a user |
| `bl memory profile create` | Create a user profile schema for memory profiling |
| `bl memory profile get` | Get user profile by schema ID and user ID |
| `bl memory search` | Search memory nodes by query or messages |
| `bl memory update` | Update a memory node content |
## Command details
@@ -33,17 +29,15 @@ Index: [index.md](index.md)
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--messages <json>` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] |
| `--content <text>` | string | no | Custom content text to memorize |
| `--profile-schema <id>` | string | no | Profile schema ID for user profiling |
| `--memory-library-id <id>` | string | no | Memory library ID (isolate memory space) |
| `--project-id <id>` | string | no | Memory extraction rule ID (defaults to the library's default rule) |
| `--meta-data <json>` | string | no | Custom metadata JSON object: {"location":"Beijing"} |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--messages <json>` | string | no | Messages JSON array: [{"role":"user","content":"..."},...] |
| `--content <text>` | string | no | Custom content text to memorize |
| `--profile-schema <id>` | string | no | Profile schema ID for user profiling |
| `--memory-library-id <id>` | string | no | Memory library ID (isolate memory space) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -59,10 +53,6 @@ bl memory add --user-id user1 --messages '[{"role":"user","content":"I like trav
bl memory add --user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx
```
```bash
bl memory add --user-id user1 --content "Lives in Beijing" --meta-data '{"source":"onboarding"}'
```
### `bl memory delete`
| Field | Value |
@@ -97,15 +87,14 @@ bl memory delete --node-id node_xxx --user-id user1
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--project-id <id>` | string | no | Memory extraction rule ID (defaults to the library's default rule) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------ |
| `--user-id <id>` | string | yes | User ID (required) |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -141,52 +130,6 @@ bl memory list --user-id user1 --page-size 20 --page 2
bl memory profile create --name "user_basic" --attributes '[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]'
```
### `bl memory profile delete`
| Field | Value |
| --------------- | --------------------------------------------------- |
| **Name** | `memory profile delete` |
| **Description** | Delete a profile schema |
| **Usage** | `bl memory profile delete --schema-id <id> [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile delete --schema-id schema_xxx
```
### `bl memory profile detail`
| Field | Value |
| --------------- | --------------------------------------------------- |
| **Name** | `memory profile detail` |
| **Description** | Show a profile schema and its attribute IDs |
| **Usage** | `bl memory profile detail --schema-id <id> [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile detail --schema-id schema_xxx
```
### `bl memory profile get`
| Field | Value |
@@ -210,72 +153,6 @@ bl memory profile detail --schema-id schema_xxx
bl memory profile get --schema-id schema_xxx --user-id user1
```
### `bl memory profile list`
| Field | Value |
| --------------- | -------------------------------- |
| **Name** | `memory profile list` |
| **Description** | List profile schemas |
| **Usage** | `bl memory profile list [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------ |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--page-size <n>` | number | no | Results per page (default: 10) |
| `--page <n>` | number | no | Page number (default: 1) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
```bash
bl memory profile list
```
```bash
bl memory profile list --page-size 20 --page 2
```
### `bl memory profile update`
| Field | Value |
| --------------- | -------------------------------------------------------------------------------------------- |
| **Name** | `memory profile update` |
| **Description** | Update a profile schema's name, description, or attributes |
| **Usage** | `bl memory profile update --schema-id <id> [--name <name>] [--attribute-ops <json>] [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `--schema-id <id>` | string | yes | Profile schema ID (required) |
| `--name <name>` | string | no | New schema name |
| `--description <text>` | string | no | New schema description |
| `--attribute-ops <json>` | string | no | Attribute operations JSON array: [{"op":"add","name":"plan"},{"op":"delete","attribute_id":"attr_1"}] |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Attribute IDs for update/delete operations come from `memory profile detail`.
#### Examples
```bash
bl memory profile update --schema-id schema_xxx --name "user_basic_v2"
```
```bash
bl memory profile update --schema-id schema_xxx --attribute-ops '[{"op":"add","name":"plan","description":"subscription plan"}]'
```
```bash
bl memory profile update --schema-id schema_xxx --attribute-ops '[{"op":"delete","attribute_id":"attr_1"}]'
```
### `bl memory search`
| Field | Value |
@@ -286,21 +163,15 @@ bl memory profile update --schema-id schema_xxx --attribute-ops '[{"op":"delete"
#### Flags
| Flag | Type | Required | Description |
| ---------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--query <text>` | string | no | Search query text |
| `--messages <json>` | string | no | Messages JSON array for context-based search |
| `--top-k <n>` | number | no | Number of results to return (default: 10) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--project-ids <id>` | array | no | Memory extraction rule ID for hybrid retrieval (repeatable) |
| `--min-score <n>` | number | no | Minimum similarity score, 0-1 (default: 0.3) |
| `--enable-rerank <bool>` | boolean | no | Rerank results. Also selects the billing tier: false bills lite, true bills pro (~50x). (default: true) |
| `--plan-version <lite\|pro>` | string | no | Documented billing tier. The service currently honors --enable-rerank instead, so prefer that flag |
| `--enable-judge <bool>` | boolean | no | Enable the intent-discrimination callback (default: false) |
| `--enable-rewrite <bool>` | boolean | no | Enable query rewriting (default: false) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | -------------------------------------------- |
| `--user-id <id>` | string | yes | User ID (required) |
| `--query <text>` | string | no | Search query text |
| `--messages <json>` | string | no | Messages JSON array for context-based search |
| `--top-k <n>` | number | no | Number of results to return (default: 10) |
| `--memory-library-id <id>` | string | no | Memory library ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -312,10 +183,6 @@ bl memory search --user-id user1 --query "programming preferences"
bl memory search --user-id user1 --messages '[{"role":"user","content":"recommend a book"}]' --top-k 5
```
```bash
bl memory search --user-id user1 --query "preferences" --enable-rerank false --min-score 0.5
```
### `bl memory update`
| Field | Value |
@@ -326,16 +193,14 @@ bl memory search --user-id user1 --query "preferences" --enable-rerank false --m
#### Flags
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `--node-id <id>` | string | yes | Memory node ID (required) |
| `--user-id <id>` | string | yes | User ID (required) |
| `--content <text>` | string | yes | New content for the memory node (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (non-default library) |
| `--timestamp <unix-seconds>` | number | no | When the remembered event happened (default: now) |
| `--meta-data <json>` | string | no | Custom metadata JSON object, merged incrementally: {"source":"manual"} |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------ |
| `--node-id <id>` | string | yes | Memory node ID (required) |
| `--user-id <id>` | string | yes | User ID (required) |
| `--content <text>` | string | yes | New content for the memory node (required) |
| `--memory-library-id <id>` | string | no | Memory library ID (non-default library) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
+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
```
+6 -54
View File
@@ -7,14 +7,12 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ------------------------------ | -------------------------------------------------------------------------------------- |
| `bl token-plan add-member` | Add a member to a Token Plan organization |
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
| `bl token-plan list-seats` | List Token Plan subscription seat details |
| `bl token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account |
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
| Command | Description |
| ---------------------------- | ----------------------------------------- |
| `bl token-plan add-member` | Add a member to a Token Plan organization |
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
| `bl token-plan list-seats` | List Token Plan subscription seat details |
## Command details
@@ -155,49 +153,3 @@ bl token-plan list-seats --page-size 20 --status NORMAL
```bash
bl token-plan list-seats --query-assigned true --seat-type standard
```
### `bl token-plan personal-key`
| Field | Value |
| --------------- | --------------------------------------------------------------------------- |
| **Name** | `token-plan personal-key` |
| **Description** | Get the personal-edition TokenPlan API key (masked) for the current account |
| **Usage** | `bl token-plan personal-key [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 token-plan personal-key
```
### `bl token-plan personal-usage`
| Field | Value |
| --------------- | -------------------------------------------------------------------------------------- |
| **Name** | `token-plan personal-usage` |
| **Description** | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
| **Usage** | `bl token-plan personal-usage [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 token-plan personal-usage
```
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "1.14.2"
version: "1.14.0"
requires:
bins: ["bl"]
description: >-
+3 -3
View File
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "1.14.2"
version: "1.14.0"
requires:
bins: ["bl"]
description: >-
@@ -29,8 +29,8 @@ description: >-
| User intent | Command | Default model |
| --------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
| Text-to-image | `bl image generate` | `qwen-image-3.0` |
| Image edit / multi-image merge | `bl image edit` (repeat `--image`) | `qwen-image-3.0` |
| Text-to-image | `bl image generate` | `qwen-image-2.0` |
| Image edit / multi-image merge | `bl image edit` (repeat `--image`) | `qwen-image-2.0` |
| Text-to-video / image-to-video | `bl video generate` | `happyhorse-1.1-t2v` / `-i2v` (with `--image`) |
| Video edit / style transfer | `bl video edit` | `happyhorse-1.0-video-edit` |
| Reference-to-video + voice | `bl video ref` | `happyhorse-1.1-r2v` |
+2 -2
View File
@@ -28,7 +28,7 @@ Index: [index.md](index.md)
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- |
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
| `--prompt <text>` | string | yes | Edit instruction text |
| `--model <model>` | string | no | Model ID (default: qwen-image-3.0) |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible results |
@@ -91,7 +91,7 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `--prompt <text>` | string | yes | Image description |
| `--model <model>` | string | no | Model ID (default: qwen-image-3.0) |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Image size: ratio (3:4, 16:9, 1:1) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images per request (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible generation |
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-managed-agent
metadata:
version: "1.14.2"
version: "1.14.0"
requires:
bins: ["bl"]
description: >-
+24 -23
View File
@@ -9,32 +9,33 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Description | Detail |
| --------------------------------- | -------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| Command | Description | Detail |
| --------------------------------- | ------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent migrate` | Merge a synced config into a bailian agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent sync` | Export remote bailian resources into a local synced config | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
## By group
| Group | Commands | Reference |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `run`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
| Group | Commands | Reference |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `migrate`, `plan`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `sync`, `validate` | [managed-agent.md](managed-agent.md) |
## Global flags
@@ -7,26 +7,27 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| --------------------------------- | -------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
| Command | Description |
| --------------------------------- | ------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent migrate` | Merge a synced config into a bailian agents.yaml |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent sync` | Export remote bailian resources into a local synced config |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
## Command details
@@ -53,7 +54,6 @@ Index: [index.md](index.md)
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -88,7 +88,6 @@ bl managed-agent apply --provider bailian --yes
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -133,6 +132,40 @@ bl managed-agent init --provider bailian --agent-name assistant
bl managed-agent init --provider all
```
### `bl managed-agent migrate`
| Field | Value |
| --------------- | -------------------------------------------------------- |
| **Name** | `managed-agent migrate` |
| **Description** | Merge a synced config into a bailian agents.yaml |
| **Usage** | `bl managed-agent migrate [--from <path>] [--to <path>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ----------------------------------------------------------- |
| `--from <path>` | string | no | Synced config to migrate from (default: agents.synced.yaml) |
| `--to <path>` | string | no | Target agents.yaml to merge into (default: agents.yaml) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- The merge itself runs against local files; bl's unified apiKey gate still applies — login via `bl auth login`, pass --api-key, or set DASHSCOPE_API_KEY.
- Only a bailian-target agents.yaml is supported: migrated resources are re-pointed to provider bailian, with models/tools/environments normalized to Bailian-supported values.
- Resources whose YAML key already exists in the target are skipped, never overwritten.
- Run `bl managed-agent plan` afterwards to review the merged config before apply.
#### Examples
```bash
bl managed-agent migrate
```
```bash
bl managed-agent migrate --from agents.synced.yaml --to agents.yaml
```
### `bl managed-agent plan`
| Field | Value |
@@ -155,7 +188,6 @@ bl managed-agent init --provider all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- --no-refresh and --dry-run plan offline from local config and state: no remote requests, no state writes, provider keys are not checked.
@@ -174,44 +206,6 @@ bl managed-agent plan --provider bailian
bl managed-agent plan --no-refresh
```
### `bl managed-agent run`
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent run` |
| **Description** | Provision (if needed) a cloud agent and run a task in one step |
| **Usage** | `bl managed-agent run --prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]` |
#### Flags
| Flag | Type | Required | Description |
| ----------------------- | ------ | -------- | -------------------------------------------------------------------------- |
| `--prompt <text>` | string | yes | Task to run (required) |
| `--instructions <text>` | string | no | Role/system instructions for the remote agent (default: generic assistant) |
| `--model <id>` | string | no | Model for the remote agent (default: qwen3.8-max) |
| `--agent <name>` | string | no | Agent identity to create/reuse (default: dsh-remote-runner) |
| `--no-stream` | switch | no | Use polling instead of SSE streaming |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.
#### Examples
```bash
bl managed-agent run --prompt "Summarize the latest AI news"
```
```bash
bl managed-agent run --prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max
```
### `bl managed-agent session create`
| Field | Value |
@@ -237,7 +231,6 @@ bl managed-agent run --prompt "Audit this dependency tree" --instructions "You a
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -276,7 +269,6 @@ bl managed-agent session create --agent assistant --title 'debug run'
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -309,7 +301,6 @@ bl managed-agent session delete --session-id sess_abc123
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -344,7 +335,6 @@ bl managed-agent session events --session-id sess_abc123 --all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -376,7 +366,6 @@ bl managed-agent session get --session-id sess_abc123
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -421,7 +410,6 @@ bl managed-agent session list --all
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.
@@ -459,7 +447,6 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo"
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -490,7 +477,6 @@ bl managed-agent session send --session-id sess_abc123 --message "continue"
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Providers without a skill listing API (e.g. ark) return an empty list.
@@ -537,7 +523,6 @@ bl managed-agent skill-list --source custom --provider bailian
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
@@ -625,6 +610,67 @@ bl managed-agent state rm --address bailian.agent.assistant
bl managed-agent state show --address bailian.agent.assistant
```
### `bl managed-agent sync`
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent sync` |
| **Description** | Export remote bailian resources into a local synced config |
| **Usage** | `bl managed-agent sync [--types <list>] [--agent-id\|--environment-id\|--vault-id\|--file-id\|--skill-id <id>] [--file <path>] [--out <path>] [--force] [--skip-missing-files]` |
#### Flags
| Flag | Type | Required | Description |
| ----------------------- | ------ | -------- | --------------------------------------------------------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--out <path>` | string | no | Output path for the synced config (default: agents.synced.yaml) |
| `--types <list>` | string | no | Comma-separated resource types to sync: environment, vault, file, skill, agent (default: all) |
| `--agent-id <id>` | string | no | Sync a single agent by its remote ID (plus the skills it references) |
| `--environment-id <id>` | string | no | Sync a single environment by its remote ID |
| `--vault-id <id>` | string | no | Sync a single vault by its remote ID |
| `--file-id <id>` | string | no | Sync a single file resource by its remote ID |
| `--skill-id <id>` | string | no | Sync a single skill by its remote ID (overrides --agent-id skill narrowing) |
| `--force` | switch | no | Overwrite an existing output file |
| `--skip-missing-files` | switch | no | Drop file resources whose local source is missing instead of keeping them |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Syncs from the bailian provider only: remote AgentStudio resources (environments, vaults, files, skills, agents) are exported into a local synced config for review.
- --types narrows the export to the listed resource types (plural spellings accepted); sessions are runtime instances, not syncable resources.
- With --agent-id, the agents group keeps only that agent (yaml key = remote agent ID) and its referenced custom skills are synced along — the skill group is exported even when --types omits it; official skill references need no local entry.
- Bailian binds environments/vaults/files at the session level, not on the agent, so --agent-id keeps those groups as shared infrastructure.
- Each --environment-id/--vault-id/--file-id/--skill-id narrows its own resource group to the single remote resource (yaml key resolved from the remote listing); combine with --types for a minimal output.
- Requires an agents.yaml with a bailian provider block — run `bl managed-agent init` first.
- Secrets are never exported: vault credentials keep ${ENV} placeholders; set those env vars locally before apply.
- Merge the synced config into agents.yaml with `bl managed-agent migrate`.
#### Examples
```bash
bl managed-agent sync
```
```bash
bl managed-agent sync --types agent,skill
```
```bash
bl managed-agent sync --agent-id agent-abc123
```
```bash
bl managed-agent sync --skill-id skill-xyz --types skill
```
```bash
bl managed-agent sync --force --skip-missing-files
```
### `bl managed-agent validate`
| Field | Value |
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "1.14.2"
version: "1.14.0"
requires:
bins: ["bl"]
description: >-