mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6338df36be | |||
| cb6740965f | |||
| b68ff45fb9 | |||
| 4990b27436 |
@@ -37,6 +37,7 @@ tools/generated
|
||||
.claude/settings.local.json
|
||||
.claude/scheduled_tasks.lock
|
||||
.cursor/
|
||||
.qoder/
|
||||
.qwen/
|
||||
.qoder
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -6,6 +6,17 @@ 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
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
|
||||
[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
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -26,7 +26,7 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
|
||||
|
||||
- **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
|
||||
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
|
||||
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
|
||||
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
|
||||
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
|
||||
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
|
||||
- **图像生成与编辑** — Qwen-Image 2.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
|
||||
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
|
||||
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
|
||||
|
||||
@@ -26,7 +26,7 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
|
||||
|
||||
- **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
|
||||
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
|
||||
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
|
||||
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
|
||||
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
|
||||
|
||||
@@ -26,7 +26,7 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
- **文本对话** — Qwen3.8-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
|
||||
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
|
||||
- **图像生成与编辑** — Qwen-Image 2.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
|
||||
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
|
||||
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -95,8 +95,6 @@ import {
|
||||
skillList,
|
||||
managedAgentInit,
|
||||
managedAgentValidate,
|
||||
managedAgentSync,
|
||||
managedAgentMigrate,
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
@@ -215,8 +213,6 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"skill list": skillList,
|
||||
"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 destroy": managedAgentDestroy,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -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-2.0",
|
||||
default_image_model: "qwen-image-3.0",
|
||||
default_video_model: "happyhorse-1.1-t2v",
|
||||
default_speech_model: "cosyvoice-v3-flash",
|
||||
default_omni_model: "qwen3.5-omni-plus",
|
||||
@@ -86,7 +86,8 @@ export const UI_MODEL_CATALOG: Record<string, ModelOption[]> = {
|
||||
{ id: "qwen3.6-flash", role: "fast · advisor intent" },
|
||||
],
|
||||
default_image_model: [
|
||||
{ id: "qwen-image-2.0", role: "image/generate default · sync" },
|
||||
{ id: "qwen-image-3.0", role: "image/generate default · sync" },
|
||||
{ id: "qwen-image-2.0", role: "image/generate · 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-2.0",
|
||||
"--file cat.png --model qwen-image-3.0",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
|
||||
@@ -47,7 +47,7 @@ const EDIT_FLAGS = {
|
||||
model: {
|
||||
type: "string",
|
||||
valueHint: "<model>",
|
||||
description: "Model ID (default: qwen-image-2.0)",
|
||||
description: "Model ID (default: qwen-image-3.0)",
|
||||
},
|
||||
size: {
|
||||
type: "string",
|
||||
@@ -123,7 +123,7 @@ export default defineCommand({
|
||||
}
|
||||
const prompt = flags.prompt;
|
||||
|
||||
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
|
||||
const model = flags.model || settings.defaultImageModel || "qwen-image-3.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-2.0)",
|
||||
description: "Model ID (default: qwen-image-3.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-2.0";
|
||||
const model = flags.model || settings.defaultImageModel || "qwen-image-3.0";
|
||||
const route = resolveImageGenerateApi(model);
|
||||
const defaultSize = "1:1";
|
||||
const sizeInput = flags.size || defaultSize;
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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,465 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -93,8 +93,6 @@ export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-se
|
||||
export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.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 managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
|
||||
|
||||
@@ -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-2.0");
|
||||
expect(res.json.fieldDefaults.default_image_model).toBe("qwen-image-3.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-2.0" });
|
||||
expect(res.json.modelCatalog.default_image_model[0]).toMatchObject({ id: "qwen-image-3.0" });
|
||||
expect(res.json.modelCatalog.default_video_model.map((m: { id: string }) => m.id)).toContain(
|
||||
"happyhorse-1.1-i2v",
|
||||
);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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.
|
||||
@@ -1,30 +0,0 @@
|
||||
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-2.0】图片编辑", async () => {
|
||||
test("【qwen-image-3.0】图片编辑", async () => {
|
||||
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
|
||||
const gen = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
"--prompt",
|
||||
"一只简笔画小猫,白底",
|
||||
"--out-dir",
|
||||
@@ -220,7 +220,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: ima
|
||||
"image",
|
||||
"edit",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
"--image",
|
||||
imagePath!,
|
||||
"--prompt",
|
||||
|
||||
@@ -202,19 +202,19 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())(
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--prompt|Usage:/i);
|
||||
});
|
||||
|
||||
test("【qwen-image-2.0】图片生成", async () => {
|
||||
test("【qwen-image-3.0】图片生成", async () => {
|
||||
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
"--prompt",
|
||||
"一只简笔画小猫,白底",
|
||||
"--out-dir",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -246,191 +245,3 @@ 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,8 +170,6 @@ export const SKILL_ROUTES: E2eRouteExports = {
|
||||
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 destroy": "managedAgentDestroy",
|
||||
|
||||
@@ -159,7 +159,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
"--prompt",
|
||||
"一只简笔画小猫,白底",
|
||||
"--out-dir",
|
||||
|
||||
@@ -169,7 +169,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
"image",
|
||||
"generate",
|
||||
"--model",
|
||||
"qwen-image-2.0",
|
||||
"qwen-image-3.0",
|
||||
"--prompt",
|
||||
"一片绿色的树叶,白底",
|
||||
"--out-dir",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -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-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
|
||||
* - sync multimodal + messages(+images): qwen-image-3.0*, 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,6 +60,7 @@ 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",
|
||||
@@ -109,7 +110,12 @@ export function isWanxFunctionImageEditModel(model: string): boolean {
|
||||
}
|
||||
|
||||
export function resolveImageSizeProfile(model: string): ImageSizeProfile {
|
||||
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-edit")) {
|
||||
// 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")
|
||||
) {
|
||||
return "qwen-image-2.0";
|
||||
}
|
||||
// Remaining qwen-image* (plus / max / bare qwen-image) share the fixed table.
|
||||
@@ -133,7 +139,13 @@ 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-2.0") || model.startsWith("qwen-image-max")) return true;
|
||||
if (
|
||||
model.startsWith("qwen-image-3.0") ||
|
||||
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;
|
||||
|
||||
@@ -11,6 +11,7 @@ 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);
|
||||
@@ -41,6 +42,7 @@ 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");
|
||||
@@ -55,6 +57,7 @@ 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);
|
||||
@@ -85,6 +88,11 @@ 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",
|
||||
@@ -115,6 +123,10 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -171,7 +171,7 @@ export async function imageGenerate(
|
||||
});
|
||||
}
|
||||
|
||||
const model = input.model || "qwen-image-2.0";
|
||||
const model = input.model || "qwen-image-3.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-2.0";
|
||||
const model = input.model || "qwen-image-3.0";
|
||||
const route = resolveImageEditApi(model);
|
||||
const n = input.n ?? 1;
|
||||
|
||||
|
||||
@@ -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. */
|
||||
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets(3.0 经 profile 复用此表). */
|
||||
export const QWEN_IMAGE_20_RATIO_MAP: Record<string, string> = {
|
||||
"16:9": "2688*1536",
|
||||
"9:16": "1536*2688",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.14.0"
|
||||
version: "1.14.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -45,5 +45,5 @@ bl file upload --file audio.wav --model qwen3-asr-flash
|
||||
```
|
||||
|
||||
```bash
|
||||
bl file upload --file cat.png --model qwen-image-2.0
|
||||
bl file upload --file cat.png --model qwen-image-3.0
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.14.0"
|
||||
version: "1.14.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.14.0"
|
||||
version: "1.14.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -29,8 +29,8 @@ description: >-
|
||||
|
||||
| User intent | Command | Default model |
|
||||
| --------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
|
||||
| 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-image | `bl image generate` | `qwen-image-3.0` |
|
||||
| Image edit / multi-image merge | `bl image edit` (repeat `--image`) | `qwen-image-3.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` |
|
||||
|
||||
@@ -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-2.0) |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-3.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-2.0) |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-3.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,7 +1,7 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.14.0"
|
||||
version: "1.14.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -14,7 +14,6 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `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) |
|
||||
@@ -28,14 +27,13 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `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`, `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) |
|
||||
| Group | Commands | Reference |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `managed-agent` | `apply`, `destroy`, `init`, `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`, `validate` | [managed-agent.md](managed-agent.md) |
|
||||
|
||||
## Global flags
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ Index: [index.md](index.md)
|
||||
| `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 |
|
||||
@@ -26,7 +25,6 @@ Index: [index.md](index.md)
|
||||
| `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
|
||||
@@ -132,40 +130,6 @@ 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 |
|
||||
@@ -610,67 +574,6 @@ 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,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.14.0"
|
||||
version: "1.14.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
Reference in New Issue
Block a user