diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b78d442..0756fdee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [2.9.0] - 2026-11-12 + +### Added + +- **App control-plane commands**: Manage Bailian applications from the CLI via the console gateway. + - `bl app create` / `get` / `update` / `publish` / `delete` / `copy` / `versions` for agents (app-control 2.0) and workflows; `delete` is high-risk and requires `--yes`. + - `bl app workflow get` / `save` / `publish` / `export` for workflow canvases; `save` replaces the whole draft with optimistic-lock versioning, `publish` normalizes triggers like the console editor. + - Agent config updates read the current draft and merge `--model` / `--instructions` / `--config-json` / `--config-file`. +- **App list filtering**: `bl app list --type workflow` lists workflows using the console exclusion filter. + +### Changed + +- **Omni default model**: `bl omni` now defaults to `qwen3.8-omni-flash` (was `qwen3.5-omni-plus`). + +### Fixed + +- **Free-tier model operations**: Tighten confirmation gating and error handling; recovery stays behind the confirmation gate. +- **Recovery hints**: Preserve the explicit `--config` profile after API Key fallback, and append `--config` for named profiles. + +### Internal + +- **Telemetry identity**: Replace the anonymous device_id with a persisted anonymous uid, published atomically (temp+rename) to avoid short-writes and stale-lock races. + ## [2.8.6] - 2026-11-12 ### Added diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index a3c049f9..08c6629f 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,29 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [2.9.0] - 2026-11-12 + +### 新增 + +- **应用控制面命令**:通过控制台网关在命令行管理百炼应用。 + - `bl app create` / `get` / `update` / `publish` / `delete` / `copy` / `versions`:管理智能体(app-control 2.0)与工作流;`delete` 为高风险操作,需 `--yes`。 + - `bl app workflow get` / `save` / `publish` / `export`:管理工作流画布;`save` 整幅替换草稿并带乐观锁版本,`publish` 按控制台编辑器逻辑规范化触发器。 + - 智能体配置更新先读取当前草稿,再合并 `--model` / `--instructions` / `--config-json` / `--config-file`。 +- **应用列表筛选**:`bl app list --type workflow` 按控制台排除法列出工作流。 + +### 变更 + +- **Omni 默认模型**:`bl omni` 默认模型改为 `qwen3.8-omni-flash`(原 `qwen3.5-omni-plus`)。 + +### 修复 + +- **免费额度模型操作**:收紧确认闸门与错误处理;恢复操作保持在确认闸门之后。 +- **恢复提示**:API Key 回退后保留显式 `--config` 配置 profile,并为命名 profile 追加 `--config` 提示。 + +### 内部 + +- **遥测身份**:以持久化的匿名 uid 替代匿名 device_id,采用原子发布(临时文件+重命名),避免短写与陈旧锁竞争。 + ## [2.8.6] - 2026-11-12 ### 新增 diff --git a/README.md b/README.md index 5723b130..74935b78 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ _Built for AI Agents. Every command works as a structured tool call._ - **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 +- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools; create, update, publish, copy, and delete agents/workflows, and edit workflow canvases from the CLI - **Native MCP setup** — Register Bailian MCP servers in Codex, Claude Code, Cursor, Qoder, Qoder Work, QwenWork (千问办公), Qwen Code, Gemini CLI, or Pi - **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints - **Batch inference** — Prepare CSV/JSONL, create and follow asynchronous Chat/Embedding tasks, download original results/errors with optional CSV, or send one synchronous Batch Chat request diff --git a/README.zh.md b/README.zh.md index 74ecd333..252d0b51 100644 --- a/README.zh.md +++ b/README.zh.md @@ -24,7 +24,7 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_ - **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成 - **素材理解** — 图像、文档、音频、长视频的解析与问答 -- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具 +- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具;并可在命令行创建/更新/发布/复制/删除智能体与工作流、编辑工作流画布 - **原生 MCP 接入** — 将百炼 MCP 服务注册到 Codex、Claude Code、Cursor、Qoder、Qoder Work、千问办公(QwenWork)、Qwen Code、Gemini CLI 或 Pi - **模型训推** — 数据集校验上传、模型精调、专属模型部署上线 - **批量推理** — CSV/JSONL 构造、异步 Chat/Embedding 任务创建与跟踪、原始结果/错误下载及可选 CSV,或同步单请求 Batch Chat diff --git a/packages/bailian-kb-dsh/package.json b/packages/bailian-kb-dsh/package.json index 5121bd2d..c18e0fb9 100644 --- a/packages/bailian-kb-dsh/package.json +++ b/packages/bailian-kb-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-kb-dsh", - "version": "2.8.6", + "version": "2.9.0", "description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the bl CLI management skill.", "keywords": [ "alibaba-cloud", diff --git a/packages/bailian-memo-dsh/package.json b/packages/bailian-memo-dsh/package.json index 9351280f..8366c86d 100644 --- a/packages/bailian-memo-dsh/package.json +++ b/packages/bailian-memo-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-memo-dsh", - "version": "2.8.6", + "version": "2.9.0", "description": "Bailian personal memory for DeepSeek Harness: automatic recall, silent curation, and user-controlled remember/forget.", "keywords": [ "alibaba-cloud", diff --git a/packages/cli/package.json b/packages/cli/package.json index a92119f2..df95718f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "2.8.6", + "version": "2.9.0", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 9c1c6924..4873802f 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -34,6 +34,17 @@ import { update, appCall, appList, + appCreate, + appGet, + appUpdate, + appPublish, + appDelete, + appCopy, + appVersions, + appWorkflowGet, + appWorkflowSave, + appWorkflowPublish, + appWorkflowExport, memoryAdd, memorySearch, memoryList, @@ -378,6 +389,17 @@ export const commands: Record = { update, "app call": appCall, "app list": appList, + "app create": appCreate, + "app get": appGet, + "app update": appUpdate, + "app publish": appPublish, + "app delete": appDelete, + "app copy": appCopy, + "app versions": appVersions, + "app workflow get": appWorkflowGet, + "app workflow save": appWorkflowSave, + "app workflow publish": appWorkflowPublish, + "app workflow export": appWorkflowExport, "memory add": memoryAdd, "memory search": memorySearch, "memory list": memoryList, diff --git a/packages/commands/package.json b/packages/commands/package.json index e3f98408..1e33ab3d 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "2.8.6", + "version": "2.9.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": { diff --git a/packages/commands/src/commands/app/copy.ts b/packages/commands/src/commands/app/copy.ts new file mode 100644 index 00000000..7dc61b5c --- /dev/null +++ b/packages/commands/src/commands/app/copy.ts @@ -0,0 +1,45 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_COPY_API, + APP_ID_FLAG, + consoleCall, + consoleDryRunPreview, + extractAppCode, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Copy a Bailian application (agent or workflow)", + "zh-CN": "复制百炼应用(智能体或工作流)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + }, + exampleArgs: ["--app-id abc123"], + async run(ctx) { + const { settings, flags } = ctx; + const code = flags.appId; + const data = { reqDTO: { code } }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, APP_CONTROL_COPY_API, data)); + return; + } + + const payload = await consoleCall(ctx.client, APP_CONTROL_COPY_API, data); + const newAppCode = extractAppCode(payload); + ctx.output.result( + { app_code: newAppCode ?? null, source_app_code: code, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Application copied: ${newAppCode ?? "(see result)"}`, + "zh-CN": `应用已复制:${newAppCode ?? "(见 result)"}`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/create.ts b/packages/commands/src/commands/app/create.ts new file mode 100644 index 00000000..fd01a48a --- /dev/null +++ b/packages/commands/src/commands/app/create.ts @@ -0,0 +1,170 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_CREATE_API, + APP_DATA_CREATE_DRAFT_API, + APP_TYPE_AGENT, + CONFIG_FLAGS, + APP_TYPE_FLAG, + consoleCall, + consoleDryRunPreview, + extractAppCode, + readConfigJson, + resolveAppType, + validateConfigFlags, +} from "./shared.ts"; + +/** Default agent config aligned with the console create modal (new-agent). */ +function defaultAgentConfig(model?: string): Record { + return { + model: model || "auto", + model_config: { + temperature: 0.7, + enable_thinking: true, + thinking_budget: 4000, + }, + dialog_round: 3, + }; +} + +export default defineCommand({ + description: { + "en-US": "Create a Bailian application (agent or workflow)", + "zh-CN": "创建百炼应用(智能体或工作流)", + }, + auth: "console", + usageArgs: "--name [flags]", + flags: { + name: { + type: "string", + valueHint: "", + description: { "en-US": "Application name (required)", "zh-CN": "应用名称(必填)" }, + required: true, + }, + ...APP_TYPE_FLAG, + description: { + type: "string", + valueHint: "", + description: { "en-US": "Application description", "zh-CN": "应用描述" }, + }, + icon: { + type: "string", + valueHint: "", + description: { "en-US": "Application icon key", "zh-CN": "应用图标 key" }, + }, + model: { + type: "string", + valueHint: "", + description: { + "en-US": "Agent model ID (default: auto; agent only)", + "zh-CN": "智能体模型 ID(默认:auto;仅 agent 有效)", + }, + }, + ...CONFIG_FLAGS, + }, + notes: [ + { + "en-US": + "Agent creation uses the app-control 2.0 API with a default config (model auto, dialog_round 3). --config-json/--config-file shallow-merges over the default config; --model overrides config.model.", + "zh-CN": + "智能体创建走 app-control 2.0 接口,携带默认配置(model auto、dialog_round 3)。--config-json/--config-file 浅合并覆盖默认配置;--model 覆盖 config.model。", + }, + { + "en-US": + "Workflow creation creates an empty draft canvas; edit it with `app workflow save` and publish with `app workflow publish`.", + "zh-CN": + "工作流创建为空草稿画布;用 `app workflow save` 编辑画布,用 `app workflow publish` 发布。", + }, + ], + exampleArgs: [ + { + "en-US": '--name "Customer service"', + "zh-CN": '--name "客户服务"', + }, + { + "en-US": '--name "Customer service" --model qwen3-max --description "After-sales bot"', + "zh-CN": '--name "客户服务" --model qwen3-max --description "售后机器人"', + }, + { + "en-US": '--name "Order flow" --type workflow', + "zh-CN": '--name "订单流程" --type workflow', + }, + { + "en-US": '--name "My agent" --config-file ./agent-config.json', + "zh-CN": '--name "我的智能体" --config-file ./agent-config.json', + }, + ], + validate: (flags) => { + const conflict = validateConfigFlags(flags); + if (conflict) return conflict; + if ( + resolveAppType(flags) === "workflow" && + (flags.model !== undefined || + flags.configJson !== undefined || + flags.configFile !== undefined) + ) { + return { + "en-US": + "--model/--config-json/--config-file only apply to agent creation; edit a workflow canvas with `app workflow save`", + "zh-CN": + "--model/--config-json/--config-file 仅适用于智能体创建;工作流画布请用 `app workflow save` 编辑", + }; + } + return undefined; + }, + async run(ctx) { + const { settings, flags } = ctx; + const appType = resolveAppType(flags); + const userConfig = await readConfigJson(flags); + + let api: string; + let data: Record; + if (appType === "workflow") { + api = APP_DATA_CREATE_DRAFT_API; + data = { + reqDTO: { + name: flags.name, + appDescription: flags.description, + applicationConfigDTO: {}, + createType: "Workflow", + modelId: "", + icon: flags.icon, + subType: "chat", + }, + }; + } else { + const config = { ...defaultAgentConfig(flags.model), ...userConfig }; + if (flags.model) config.model = flags.model; + api = APP_CONTROL_CREATE_API; + data = { + reqDTO: { + name: flags.name, + description: flags.description, + icon: flags.icon, + source: "console", + type: APP_TYPE_AGENT, + status: 1, + config, + }, + }; + } + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, api, data)); + return; + } + + const payload = await consoleCall(ctx.client, api, data); + const appCode = extractAppCode(payload); + ctx.output.result( + { app_code: appCode ?? null, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Application created: ${appCode ?? "(see result)"}`, + "zh-CN": `应用已创建:${appCode ?? "(见 result)"}`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/delete.ts b/packages/commands/src/commands/app/delete.ts new file mode 100644 index 00000000..f45c09b4 --- /dev/null +++ b/packages/commands/src/commands/app/delete.ts @@ -0,0 +1,57 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_DELETE_API, + APP_ID_FLAG, + consoleCall, + consoleDryRunPreview, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Delete a Bailian application (agent or workflow)", + "zh-CN": "删除百炼应用(智能体或工作流)", + }, + auth: "console", + risk: { + level: "high", + message: { + "en-US": + "This permanently deletes the application and its configuration. Confirm the app ID before proceeding.", + "zh-CN": "该操作会永久删除应用及其配置,请确认应用 ID。", + }, + }, + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + }, + notes: [ + { + "en-US": "Deletion is irreversible. Run `app list` or `app get` first to confirm the app ID.", + "zh-CN": "删除不可撤销。建议先用 `app list` 或 `app get` 确认应用 ID。", + }, + ], + exampleArgs: ["--app-id abc123", "--app-id abc123 --yes"], + async run(ctx) { + const { settings, flags } = ctx; + const code = flags.appId; + const data = { reqDTO: { code } }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, APP_CONTROL_DELETE_API, data)); + return; + } + + const payload = await consoleCall(ctx.client, APP_CONTROL_DELETE_API, data); + ctx.output.result( + { app_code: code, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Application ${code} deleted.`, + "zh-CN": `应用 ${code} 已删除。`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/get.ts b/packages/commands/src/commands/app/get.ts new file mode 100644 index 00000000..bce42343 --- /dev/null +++ b/packages/commands/src/commands/app/get.ts @@ -0,0 +1,84 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_DETAIL_API, + APP_CONTROL_VERSION_GET_API, + APP_DATA_GET_API, + APP_ID_FLAG, + APP_TYPE_FLAG, + SCOPE_FLAG, + consoleCall, + consoleDryRunPreview, + resolveAppType, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Get Bailian application details (agent or workflow)", + "zh-CN": "查询百炼应用详情(智能体或工作流)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + ...APP_TYPE_FLAG, + ...SCOPE_FLAG, + agentVersion: { + type: "number", + valueHint: "", + description: { + "en-US": "Agent version number (agent only; implies published scope by default)", + "zh-CN": "智能体版本号(仅 agent;默认读取发布态)", + }, + }, + }, + notes: [ + { + "en-US": + "Agent details come from app-control 2.0; workflow details from app-data. For the workflow canvas config use `app workflow get`.", + "zh-CN": + "智能体详情走 app-control 2.0;工作流详情走 app-data。工作流画布配置请用 `app workflow get`。", + }, + ], + exampleArgs: [ + "--app-id abc123", + "--app-id abc123 --scope publish", + "--app-id abc123 --agent-version 3", + "--app-id abc123 --type workflow", + ], + validate: (flags) => { + if (resolveAppType(flags) === "workflow" && flags.agentVersion !== undefined) { + return { + "en-US": + "--agent-version only applies to agent; use `app workflow get --flow-version` for workflow", + "zh-CN": "--agent-version 仅适用于智能体;工作流请用 `app workflow get --flow-version`", + }; + } + return undefined; + }, + async run(ctx) { + const { settings, flags } = ctx; + const appType = resolveAppType(flags); + const code = flags.appId; + + let api: string; + let data: Record; + if (appType === "workflow") { + api = APP_DATA_GET_API; + data = { reqDTO: { code, scope: flags.scope ?? "draft" } }; + } else if (flags.agentVersion !== undefined) { + api = APP_CONTROL_VERSION_GET_API; + data = { reqDTO: { code, version: flags.agentVersion, scope: flags.scope ?? "publish" } }; + } else { + api = APP_CONTROL_DETAIL_API; + data = { reqDTO: { code, scope: flags.scope ?? "draft" } }; + } + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, api, data)); + return; + } + + const payload = await consoleCall(ctx.client, api, data); + ctx.output.result(payload); + }, +}); diff --git a/packages/commands/src/commands/app/list.ts b/packages/commands/src/commands/app/list.ts index 85189f1e..daeed961 100644 --- a/packages/commands/src/commands/app/list.ts +++ b/packages/commands/src/commands/app/list.ts @@ -1,6 +1,12 @@ import { defineCommand } from "bailian-cli-core"; - -const APP_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list"; +import { + APP_CONTROL_LIST_API, + APP_TYPE_AGENT, + APP_TYPE_CCAI, + APP_TYPE_FLAG, + APP_TYPE_HIGH_CODE, + resolveAppType, +} from "./shared.ts"; export default defineCommand({ description: { "en-US": "List Bailian applications", "zh-CN": "列出百炼应用" }, @@ -25,6 +31,7 @@ export default defineCommand({ valueHint: "", description: { "en-US": "Results per page (default: 30)", "zh-CN": "每页结果数(默认:30)" }, }, + ...APP_TYPE_FLAG, }, exampleArgs: [ "", @@ -33,6 +40,7 @@ export default defineCommand({ "zh-CN": "--name 客户服务", }, "--page 2 --page-size 10", + "--type workflow", "--output json", ], async run(ctx) { @@ -40,24 +48,29 @@ export default defineCommand({ const name = flags.name || ""; const pageNo = flags.page || 1; const pageSize = flags.pageSize || 30; + const appType = resolveAppType(flags); - const data = { - reqDTO: { - name, - notInTypes: [10], - type: 5, - statuses: [1, 4], - page_no: pageNo, - page_size: pageSize, - }, + const reqDTO: Record = { + name, + statuses: [1, 4], + page_no: pageNo, + page_size: pageSize, }; + if (appType === "workflow") { + // Workflow page uses exclusion: not agent, not high-code, not CCAI. + reqDTO.notInTypes = [APP_TYPE_AGENT, APP_TYPE_HIGH_CODE, APP_TYPE_CCAI]; + } else { + reqDTO.type = APP_TYPE_AGENT; + reqDTO.notInTypes = [APP_TYPE_CCAI]; + } + const data = { reqDTO }; if (settings.dryRun) { - ctx.output.result({ api: APP_LIST_API, data }); + ctx.output.result({ api: APP_CONTROL_LIST_API, data }); return; } - const result = await ctx.client.console(APP_LIST_API, data); + const result = await ctx.client.console(APP_CONTROL_LIST_API, data); const list: unknown[] = result?.data?.DataV2?.data?.data?.list ?? []; const total: number = result?.data?.DataV2?.data?.data?.total ?? 0; diff --git a/packages/commands/src/commands/app/publish.ts b/packages/commands/src/commands/app/publish.ts new file mode 100644 index 00000000..762a9be5 --- /dev/null +++ b/packages/commands/src/commands/app/publish.ts @@ -0,0 +1,57 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_PUBLISH_API, + APP_ID_FLAG, + consoleCall, + consoleDryRunPreview, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Publish a Bailian agent application", + "zh-CN": "发布百炼智能体应用", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + description: { + type: "string", + valueHint: "", + description: { "en-US": "Version description", "zh-CN": "版本描述" }, + }, + }, + notes: [ + { + "en-US": + "Publishes the current agent draft via app-control 2.0. To publish a workflow, use `app workflow publish`.", + "zh-CN": "通过 app-control 2.0 发布智能体当前草稿。发布工作流请用 `app workflow publish`。", + }, + ], + exampleArgs: ["--app-id abc123", '--app-id abc123 --description "Release v2"'], + async run(ctx) { + const { settings, flags } = ctx; + const code = flags.appId; + const reqDTO: Record = { code }; + if (flags.description !== undefined) reqDTO.version_description = flags.description; + const data = { reqDTO }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, APP_CONTROL_PUBLISH_API, data)); + return; + } + + const payload = await consoleCall(ctx.client, APP_CONTROL_PUBLISH_API, data); + ctx.output.result( + { app_code: code, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Application ${code} published.`, + "zh-CN": `应用 ${code} 已发布。`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/shared.ts b/packages/commands/src/commands/app/shared.ts new file mode 100644 index 00000000..52a67b85 --- /dev/null +++ b/packages/commands/src/commands/app/shared.ts @@ -0,0 +1,302 @@ +import { readFile } from "node:fs/promises"; +import { + BailianError, + ExitCode, + UsageError, + effectiveConsoleGatewayConfig, + unwrapConsoleResponse, + type Client, + type FlagsDef, + type Settings, +} from "bailian-cli-core"; + +// --------------------------------------------------------------------------- +// Console control-plane API names (mirrors the app-center assistant/workflow +// pages in bailian-agent-infra/apps/app-flow-agents). +// app-control.* / app-data.* wrap the payload in { reqDTO }; app-orchestra-flow.* +// takes the payload as-is. +// --------------------------------------------------------------------------- + +export const APP_CONTROL_CREATE_API = "zeldaEasy.broadscope-bailian.app-control.create"; +export const APP_CONTROL_UPDATE_API = "zeldaEasy.broadscope-bailian.app-control.update"; +export const APP_CONTROL_PUBLISH_API = "zeldaEasy.broadscope-bailian.app-control.publish"; +export const APP_CONTROL_DELETE_API = "zeldaEasy.broadscope-bailian.app-control.delete"; +export const APP_CONTROL_COPY_API = "zeldaEasy.broadscope-bailian.app-control.copy"; +export const APP_CONTROL_DETAIL_API = "zeldaEasy.broadscope-bailian.app-control.detail"; +export const APP_CONTROL_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list"; +export const APP_CONTROL_VERSION_LIST_API = + "zeldaEasy.broadscope-bailian.app-control.getAgentVersionList"; +export const APP_CONTROL_VERSION_GET_API = + "zeldaEasy.broadscope-bailian.app-control.getAgentVersion"; + +export const APP_DATA_CREATE_DRAFT_API = "zeldaEasy.broadscope-bailian.app-data.createAppDraft"; +export const APP_DATA_UPDATE_DRAFT_API = "zeldaEasy.broadscope-bailian.app-data.updateAppDraft"; +export const APP_DATA_GET_API = "zeldaEasy.broadscope-bailian.app-data.get"; + +export const FLOW_GET_CONFIG_API = "zeldaEasy.broadscope-bailian.app-orchestra-flow.getConfig"; +export const FLOW_SAVE_API = "zeldaEasy.broadscope-bailian.app-orchestra-flow.save"; +export const FLOW_PUBLISH_API = "zeldaEasy.broadscope-bailian.app-orchestra-flow.publish"; +export const FLOW_VERSION_LIST_API = + "zeldaEasy.broadscope-bailian.app-orchestra-flow.getFlowVersionConfig"; +export const FLOW_VERSION_GET_API = + "zeldaEasy.broadscope-bailian.app-orchestra-flow.getFlowVersion"; +export const FLOW_EXPORT_DSL_API = + "zeldaEasy.broadscope-bailian.app-orchestra-flow.generateDSLSchema"; + +/** Application type enum on the wire (ApplicationTypeEnum). */ +export const APP_TYPE_AGENT = 5; +export const APP_TYPE_WORKFLOW = 7; +/** CCAI app type, excluded from both list pages. */ +export const APP_TYPE_CCAI = 10; +/** High-code app type, excluded from the workflow list page. */ +export const APP_TYPE_HIGH_CODE = 8; + +// --------------------------------------------------------------------------- +// Shared flags +// --------------------------------------------------------------------------- + +export const APP_ID_FLAG = { + appId: { + type: "string", + valueHint: "", + description: { + "en-US": "Application ID (app code, required)", + "zh-CN": "应用 ID(应用 code,必填)", + }, + required: true, + }, +} satisfies FlagsDef; + +export const APP_TYPE_FLAG = { + type: { + type: "string", + valueHint: "", + choices: ["agent", "workflow"] as const, + description: { + "en-US": "Application type: agent (default), workflow", + "zh-CN": "应用类型:agent(默认)、workflow", + }, + }, +} satisfies FlagsDef; + +export const SCOPE_FLAG = { + scope: { + type: "string", + valueHint: "", + choices: ["draft", "publish"] as const, + description: { + "en-US": "Data scope: draft (default), publish", + "zh-CN": "数据范围:draft(默认)、publish", + }, + }, +} satisfies FlagsDef; + +export const CONFIG_FLAGS = { + configJson: { + type: "string", + valueHint: "", + description: { + "en-US": "Config JSON string (mutually exclusive with --config-file)", + "zh-CN": "配置 JSON 字符串(与 --config-file 互斥)", + }, + }, + configFile: { + type: "string", + valueHint: "", + description: { + "en-US": "Path to a config JSON file (mutually exclusive with --config-json)", + "zh-CN": "配置 JSON 文件路径(与 --config-json 互斥)", + }, + }, +} satisfies FlagsDef; + +export type AppType = "agent" | "workflow"; + +export function resolveAppType(flags: { type?: AppType }): AppType { + return flags.type ?? "agent"; +} + +// --------------------------------------------------------------------------- +// Config JSON input (--config / --config-file) +// --------------------------------------------------------------------------- + +/** Cross-flag validation for the mutually exclusive config inputs. */ +export function validateConfigFlags(flags: { + configJson?: string; + configFile?: string; +}): { "en-US": string; "zh-CN": string } | undefined { + if (flags.configJson !== undefined && flags.configFile !== undefined) { + return { + "en-US": "--config-json and --config-file are mutually exclusive", + "zh-CN": "--config-json 与 --config-file 互斥,只能提供其中一个", + }; + } + return undefined; +} + +function fileReadHint(error: NodeJS.ErrnoException, filePath: string): string { + switch (error.code) { + case "ENOENT": + return `Config file not found: ${filePath}`; + case "EACCES": + return `Permission denied reading config file: ${filePath}`; + case "EISDIR": + return `Config file path is a directory: ${filePath}`; + default: + return `Cannot read config file: ${filePath} (${error.code ?? error.message})`; + } +} + +/** + * Read the config JSON object from --config-json or --config-file. + * Returns undefined when neither is provided. Invalid JSON or a non-object + * payload is a usage error; file I/O failures keep their errno-specific hint. + */ +export async function readConfigJson(flags: { + configJson?: string; + configFile?: string; +}): Promise | undefined> { + const conflict = validateConfigFlags(flags); + if (conflict) throw new UsageError(conflict["en-US"]); + + let raw: string | undefined; + if (flags.configJson !== undefined) { + raw = flags.configJson; + } else if (flags.configFile !== undefined) { + const filePath = flags.configFile; + raw = await readFile(filePath, "utf-8").catch((error: NodeJS.ErrnoException) => { + throw new BailianError( + `Cannot read config file: ${filePath}`, + ExitCode.GENERAL, + fileReadHint(error, filePath), + ); + }); + } + if (raw === undefined) return undefined; + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new UsageError("--config-json/--config-file must contain valid JSON"); + } + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new UsageError("--config-json/--config-file JSON must be an object"); + } + return parsed as Record; +} + +// --------------------------------------------------------------------------- +// Console call helpers +// --------------------------------------------------------------------------- + +/** Call a console API and unwrap the known protocol envelope. */ +export async function consoleCall( + client: Client, + api: string, + data: Record, +): Promise { + const raw = await client.console(api, data); + return unwrapConsoleResponse(raw); +} + +/** Uniform dry-run preview for console write commands (matches `console call`). */ +export function consoleDryRunPreview( + settings: Settings, + api: string, + data: Record, +): Record { + return { api, data, ...effectiveConsoleGatewayConfig(settings) }; +} + +/** Create/copy responses carry the new app code as a string or { result }. */ +export function extractAppCode(payload: unknown): string | undefined { + if (typeof payload === "string" && payload.trim()) return payload; + if (typeof payload === "object" && payload !== null) { + const result = (payload as Record).result; + if (typeof result === "string" && result.trim()) return result; + } + return undefined; +} + +export function asRecord(value: unknown): Record { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : {}; +} + +// --------------------------------------------------------------------------- +// Workflow canvas helpers +// --------------------------------------------------------------------------- + +export interface WorkflowCanvas { + config: string; + version: string; + applicationDTO?: Record; +} + +/** Read the current draft canvas (config JSON string + optimistic-lock version). */ +export async function fetchWorkflowCanvas( + client: Client, + appCode: string, +): Promise { + const payload = asRecord(await consoleCall(client, FLOW_GET_CONFIG_API, { appCode })); + const config = payload.config; + const version = payload.version; + if (typeof config !== "string" || typeof version !== "string" || !version) { + throw new BailianError( + "Console returned no workflow canvas config/version.", + ExitCode.GENERAL, + "Check the app ID with `app get --type workflow`, or pass --version explicitly.", + ); + } + return { + config, + version, + applicationDTO: asRecord(payload.applicationDTO), + }; +} + +/** + * Normalize a trigger list the way the workflow editor does before publish: + * drop "none" triggers (uiConfig.type or the legacy __triggerType parameter) + * and strip the UI-only uiConfig field. + */ +export function normalizeTriggerList(raw: unknown): Record[] { + if (!Array.isArray(raw)) return []; + const triggers: Record[] = []; + for (const entry of raw) { + const trigger = asRecord(entry); + let uiConfig: Record = {}; + if (typeof trigger.uiConfig === "string") { + try { + uiConfig = asRecord(JSON.parse(trigger.uiConfig)); + } catch { + uiConfig = {}; + } + } + if (uiConfig.type === "none") continue; + const parameters = Array.isArray(trigger.parameters) ? trigger.parameters : []; + const triggerTypeParam = parameters.find( + (param) => asRecord(param).parameterKey === "__triggerType", + ); + if (asRecord(triggerTypeParam).parameterValue === "none") continue; + const { uiConfig: _uiConfig, ...rest } = trigger; + triggers.push(rest); + } + return triggers; +} + +/** Fallback: read triggers from the canvas Start node (mirrors the editor). */ +export function triggersFromCanvas(config: string): unknown[] { + try { + const canvas = asRecord(JSON.parse(config)); + const nodes = Array.isArray(canvas.nodes) ? canvas.nodes : []; + const startNode = nodes.find((node) => asRecord(node).type === "Start"); + const nodeParam = asRecord(asRecord(startNode).config).nodeParam; + const triggerList = asRecord(nodeParam).triggerList; + return Array.isArray(triggerList) ? triggerList : []; + } catch { + return []; + } +} diff --git a/packages/commands/src/commands/app/update.ts b/packages/commands/src/commands/app/update.ts new file mode 100644 index 00000000..c32ecd9d --- /dev/null +++ b/packages/commands/src/commands/app/update.ts @@ -0,0 +1,172 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_DETAIL_API, + APP_CONTROL_UPDATE_API, + APP_DATA_GET_API, + APP_DATA_UPDATE_DRAFT_API, + APP_ID_FLAG, + APP_TYPE_FLAG, + CONFIG_FLAGS, + asRecord, + consoleCall, + consoleDryRunPreview, + readConfigJson, + resolveAppType, + validateConfigFlags, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Update a Bailian application (agent config or workflow metadata)", + "zh-CN": "更新百炼应用(智能体配置或工作流元信息)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + ...APP_TYPE_FLAG, + name: { + type: "string", + valueHint: "", + description: { "en-US": "New application name", "zh-CN": "新的应用名称" }, + }, + description: { + type: "string", + valueHint: "", + description: { "en-US": "New application description", "zh-CN": "新的应用描述" }, + }, + icon: { + type: "string", + valueHint: "", + description: { "en-US": "New application icon key", "zh-CN": "新的应用图标 key" }, + }, + model: { + type: "string", + valueHint: "", + description: { + "en-US": "Agent model ID (agent only; sets config.model)", + "zh-CN": "智能体模型 ID(仅 agent;写入 config.model)", + }, + }, + instructions: { + type: "string", + valueHint: "", + description: { + "en-US": "Agent instructions (agent only; sets config.instruction)", + "zh-CN": "智能体提示词(仅 agent;写入 config.instruction)", + }, + }, + ...CONFIG_FLAGS, + }, + notes: [ + { + "en-US": + "Reads the current draft first, then merges: current config -> --config-json/--config-file (shallow) -> --model/--instructions. Only changed fields are sent. Requires a console token even for --dry-run (read-only planning).", + "zh-CN": + "先读取当前草稿再合并:当前 config -> --config-json/--config-file(浅合并)-> --model/--instructions。仅提交变更字段。--dry-run 也需要 console 凭证(只读规划)。", + }, + { + "en-US": + "Agent updates use app-control 2.0. Workflow updates only change metadata (name/description/icon); edit the canvas with `app workflow save`.", + "zh-CN": + "智能体更新走 app-control 2.0。工作流更新仅改元信息(名称/描述/图标);画布编辑请用 `app workflow save`。", + }, + ], + exampleArgs: [ + '--app-id abc123 --name "New name"', + "--app-id abc123 --model qwen3-max", + '--app-id abc123 --instructions "You are a helpful assistant"', + "--app-id abc123 --config-file ./agent-config.json", + '--app-id abc123 --type workflow --name "New flow name"', + ], + validate: (flags) => { + const conflict = validateConfigFlags(flags); + if (conflict) return conflict; + const hasChange = + flags.name !== undefined || + flags.description !== undefined || + flags.icon !== undefined || + flags.model !== undefined || + flags.instructions !== undefined || + flags.configJson !== undefined || + flags.configFile !== undefined; + if (!hasChange) { + return { + "en-US": + "Provide at least one field to update (--name/--description/--icon/--model/--instructions/--config-json)", + "zh-CN": + "请至少提供一个待更新字段(--name/--description/--icon/--model/--instructions/--config-json)", + }; + } + if ( + resolveAppType(flags) === "workflow" && + (flags.model !== undefined || + flags.instructions !== undefined || + flags.configJson !== undefined || + flags.configFile !== undefined) + ) { + return { + "en-US": + "--model/--instructions/--config-json/--config-file only apply to agent; edit a workflow canvas with `app workflow save`", + "zh-CN": + "--model/--instructions/--config-json/--config-file 仅适用于智能体;工作流画布请用 `app workflow save` 编辑", + }; + } + return undefined; + }, + async run(ctx) { + const { settings, flags } = ctx; + const appType = resolveAppType(flags); + const code = flags.appId; + const userConfig = await readConfigJson(flags); + + let api: string; + let data: Record; + if (appType === "workflow") { + // Read-modify-write the whole applicationDTO, mirroring the workflow editor. + const detail = asRecord( + await consoleCall(ctx.client, APP_DATA_GET_API, { reqDTO: { code, scope: "draft" } }), + ); + const merged: Record = { ...detail }; + if (flags.name !== undefined) merged.name = flags.name; + if (flags.description !== undefined) merged.appDescription = flags.description; + if (flags.icon !== undefined) merged.icon = flags.icon; + api = APP_DATA_UPDATE_DRAFT_API; + data = { reqDTO: merged }; + } else { + const detail = asRecord( + await consoleCall(ctx.client, APP_CONTROL_DETAIL_API, { reqDTO: { code, scope: "draft" } }), + ); + const reqDTO: Record = { code }; + if (flags.name !== undefined) reqDTO.name = flags.name; + if (flags.description !== undefined) reqDTO.description = flags.description; + if (flags.icon !== undefined) reqDTO.icon = flags.icon; + if (userConfig || flags.model !== undefined || flags.instructions !== undefined) { + const config: Record = { ...asRecord(detail.config), ...userConfig }; + if (flags.model !== undefined) config.model = flags.model; + if (flags.instructions !== undefined) config.instruction = flags.instructions; + reqDTO.config = config; + } + api = APP_CONTROL_UPDATE_API; + data = { reqDTO }; + } + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, api, data)); + return; + } + + const payload = await consoleCall(ctx.client, api, data); + ctx.output.result( + { app_code: code, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Application ${code} updated.`, + "zh-CN": `应用 ${code} 已更新。`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/versions.ts b/packages/commands/src/commands/app/versions.ts new file mode 100644 index 00000000..d10c08db --- /dev/null +++ b/packages/commands/src/commands/app/versions.ts @@ -0,0 +1,40 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_CONTROL_VERSION_LIST_API, + APP_ID_FLAG, + APP_TYPE_FLAG, + FLOW_VERSION_LIST_API, + consoleCall, + consoleDryRunPreview, + resolveAppType, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "List published versions of a Bailian application", + "zh-CN": "列出百炼应用的已发布版本", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + ...APP_TYPE_FLAG, + }, + exampleArgs: ["--app-id abc123", "--app-id abc123 --type workflow"], + async run(ctx) { + const { settings, flags } = ctx; + const appType = resolveAppType(flags); + const code = flags.appId; + + const api = appType === "workflow" ? FLOW_VERSION_LIST_API : APP_CONTROL_VERSION_LIST_API; + const data = appType === "workflow" ? { appCode: code } : { reqDTO: { code } }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, api, data)); + return; + } + + const payload = await consoleCall(ctx.client, api, data); + ctx.output.result(payload); + }, +}); diff --git a/packages/commands/src/commands/app/workflow/export.ts b/packages/commands/src/commands/app/workflow/export.ts new file mode 100644 index 00000000..01ed693d --- /dev/null +++ b/packages/commands/src/commands/app/workflow/export.ts @@ -0,0 +1,83 @@ +import { BailianError, ExitCode, defineCommand } from "bailian-cli-core"; +import { downloadFile, formatBytes } from "bailian-cli-runtime"; +import { + APP_ID_FLAG, + FLOW_EXPORT_DSL_API, + asRecord, + consoleCall, + consoleDryRunPreview, +} from "../shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Export a workflow as a DSL schema (download URL)", + "zh-CN": "导出工作流 DSL(返回下载地址)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + flowVersion: { + type: "string", + valueHint: "", + description: { + "en-US": "Published version to export (default: current draft)", + "zh-CN": "要导出的已发布版本(默认:当前草稿)", + }, + }, + outputFile: { + type: "string", + valueHint: "", + description: { + "en-US": "Download the DSL to this file instead of printing the URL", + "zh-CN": "将 DSL 下载到该文件,而非仅输出下载地址", + }, + }, + }, + exampleArgs: [ + "--app-id abc123", + "--app-id abc123 --flow-version 3", + "--app-id abc123 --output-file ./flow.dsl", + ], + async run(ctx) { + const { settings, flags } = ctx; + const appCode = flags.appId; + const data: Record = { appCode }; + if (flags.flowVersion !== undefined) data.version = flags.flowVersion; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, FLOW_EXPORT_DSL_API, data)); + return; + } + + const payload = asRecord(await consoleCall(ctx.client, FLOW_EXPORT_DSL_API, data)); + const url = payload.result; + if (typeof url !== "string" || !url) { + throw new BailianError( + "Console returned no DSL download URL.", + ExitCode.GENERAL, + "Check the app ID and version.", + ); + } + + if (flags.outputFile !== undefined) { + const { size } = await downloadFile(url, flags.outputFile, { + quiet: settings.quiet || settings.output === "json", + }); + ctx.output.result( + { app_code: appCode, saved: flags.outputFile, size: formatBytes(size) }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `DSL downloaded to ${flags.outputFile}`, + "zh-CN": `DSL 已下载到 ${flags.outputFile}`, + }), + } + : undefined, + ); + return; + } + + ctx.output.result({ app_code: appCode, url }); + }, +}); diff --git a/packages/commands/src/commands/app/workflow/get.ts b/packages/commands/src/commands/app/workflow/get.ts new file mode 100644 index 00000000..092d4bd6 --- /dev/null +++ b/packages/commands/src/commands/app/workflow/get.ts @@ -0,0 +1,109 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { BailianError, ExitCode, defineCommand } from "bailian-cli-core"; +import { + APP_ID_FLAG, + FLOW_GET_CONFIG_API, + FLOW_VERSION_GET_API, + SCOPE_FLAG, + asRecord, + consoleCall, + consoleDryRunPreview, +} from "../shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Get a workflow canvas (draft or a published version)", + "zh-CN": "读取工作流画布(草稿或已发布版本)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + flowVersion: { + type: "string", + valueHint: "", + description: { + "en-US": "Published version to read (default: current draft)", + "zh-CN": "要读取的已发布版本(默认:当前草稿)", + }, + }, + ...SCOPE_FLAG, + outputFile: { + type: "string", + valueHint: "", + description: { + "en-US": "Write the canvas config JSON to this file (overwrites)", + "zh-CN": "将画布配置 JSON 写入该文件(覆盖写入)", + }, + }, + }, + notes: [ + { + "en-US": + "Without --flow-version, reads the current draft canvas (config + version). With --flow-version, reads a published version (scope defaults to publish). The canvas config is a JSON string; --output-file writes it parsed and pretty-printed.", + "zh-CN": + "不带 --flow-version 读取当前草稿画布(config + version);带 --flow-version 读取已发布版本(scope 默认 publish)。画布 config 是 JSON 字符串;--output-file 会解析并美化后写入。", + }, + ], + exampleArgs: [ + "--app-id abc123", + "--app-id abc123 --flow-version 3", + "--app-id abc123 --output-file ./canvas.json", + ], + async run(ctx) { + const { settings, flags } = ctx; + const appCode = flags.appId; + + const api = flags.flowVersion !== undefined ? FLOW_VERSION_GET_API : FLOW_GET_CONFIG_API; + const data = + flags.flowVersion !== undefined + ? { appCode, version: flags.flowVersion, scope: flags.scope ?? "publish" } + : { appCode }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, api, data)); + return; + } + + const payload = asRecord(await consoleCall(ctx.client, api, data)); + + if (flags.outputFile !== undefined) { + const config = payload.config; + if (typeof config !== "string") { + throw new BailianError( + "Console returned no workflow canvas config.", + ExitCode.GENERAL, + "Check the app ID and version.", + ); + } + let pretty: string; + try { + pretty = JSON.stringify(JSON.parse(config), null, 2); + } catch { + pretty = config; + } + const destination = resolve(flags.outputFile); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, pretty, "utf-8"); + ctx.output.result( + { + app_code: appCode, + version: payload.version ?? flags.flowVersion ?? null, + output_file: destination, + }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Canvas written to ${destination}`, + "zh-CN": `画布已写入 ${destination}`, + }), + } + : undefined, + ); + return; + } + + ctx.output.result(payload); + }, +}); diff --git a/packages/commands/src/commands/app/workflow/publish.ts b/packages/commands/src/commands/app/workflow/publish.ts new file mode 100644 index 00000000..769f619e --- /dev/null +++ b/packages/commands/src/commands/app/workflow/publish.ts @@ -0,0 +1,99 @@ +import { UsageError, defineCommand } from "bailian-cli-core"; +import { + APP_ID_FLAG, + FLOW_PUBLISH_API, + asRecord, + consoleCall, + consoleDryRunPreview, + fetchWorkflowCanvas, + normalizeTriggerList, + triggersFromCanvas, +} from "../shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Publish a workflow (publishes the current draft canvas)", + "zh-CN": "发布工作流(发布当前草稿画布)", + }, + auth: "console", + usageArgs: "--app-id [flags]", + flags: { + ...APP_ID_FLAG, + description: { + type: "string", + valueHint: "", + description: { "en-US": "Version description", "zh-CN": "版本描述" }, + }, + triggerList: { + type: "string", + valueHint: "", + description: { + "en-US": "Trigger list JSON array (default: read from the current draft)", + "zh-CN": "触发器列表 JSON 数组(默认:从当前草稿读取)", + }, + }, + }, + notes: [ + { + "en-US": + "Reads the current draft canvas and version, then publishes. Triggers are taken from the draft (applicationConfigDTO.triggerList, falling back to the Start node) unless --trigger-list is given; 'none' triggers are dropped. Requires a console token even for --dry-run (read-only planning).", + "zh-CN": + "先读取当前草稿画布与版本再发布。触发器默认取自草稿(applicationConfigDTO.triggerList,兜底 Start 节点),除非显式传 --trigger-list;'none' 触发器会被过滤。--dry-run 也需要 console 凭证(只读规划)。", + }, + ], + exampleArgs: [ + "--app-id abc123", + '--app-id abc123 --description "Release v2"', + "--app-id abc123 --trigger-list '[]'", + ], + async run(ctx) { + const { settings, flags } = ctx; + const appCode = flags.appId; + + let explicitTriggers: unknown[] | undefined; + if (flags.triggerList !== undefined) { + let parsed: unknown; + try { + parsed = JSON.parse(flags.triggerList); + } catch { + throw new UsageError("--trigger-list must be valid JSON"); + } + if (!Array.isArray(parsed)) { + throw new UsageError("--trigger-list must be a JSON array"); + } + explicitTriggers = parsed; + } + + const canvas = await fetchWorkflowCanvas(ctx.client, appCode); + const applicationConfig = asRecord(canvas.applicationDTO?.applicationConfigDTO); + const rawTriggers = + explicitTriggers ?? applicationConfig.triggerList ?? triggersFromCanvas(canvas.config); + const triggerList = normalizeTriggerList(rawTriggers); + + const data: Record = { + appCode, + config: canvas.config, + version: canvas.version, + triggerList, + }; + if (flags.description !== undefined) data.description = flags.description; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, FLOW_PUBLISH_API, data)); + return; + } + + const payload = await consoleCall(ctx.client, FLOW_PUBLISH_API, data); + ctx.output.result( + { app_code: appCode, version: canvas.version, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Workflow ${appCode} published.`, + "zh-CN": `工作流 ${appCode} 已发布。`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/commands/app/workflow/save.ts b/packages/commands/src/commands/app/workflow/save.ts new file mode 100644 index 00000000..de9a8960 --- /dev/null +++ b/packages/commands/src/commands/app/workflow/save.ts @@ -0,0 +1,86 @@ +import { defineCommand } from "bailian-cli-core"; +import { + APP_ID_FLAG, + CONFIG_FLAGS, + FLOW_SAVE_API, + consoleCall, + consoleDryRunPreview, + fetchWorkflowCanvas, + readConfigJson, + validateConfigFlags, +} from "../shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Save a workflow canvas (replaces the current draft)", + "zh-CN": "保存工作流画布(整幅替换当前草稿)", + }, + auth: "console", + usageArgs: "--app-id (--config-json | --config-file ) [flags]", + flags: { + ...APP_ID_FLAG, + ...CONFIG_FLAGS, + flowVersion: { + type: "string", + valueHint: "", + description: { + "en-US": "Expected current version for optimistic locking (default: read from server)", + "zh-CN": "乐观锁期望的当前版本(默认:从服务端读取)", + }, + }, + }, + notes: [ + { + "en-US": + "The canvas config is a whole-document replacement of the draft. Export it first with `app workflow get --output-file`, edit, then save. Without --flow-version the current version is read from the server (requires a console token, also for --dry-run).", + "zh-CN": + "画布 config 为整幅替换草稿。建议先用 `app workflow get --output-file` 导出、编辑后再保存。不带 --flow-version 时从服务端读取当前版本(需要 console 凭证,--dry-run 同样需要)。", + }, + ], + exampleArgs: [ + "--app-id abc123 --config-file ./canvas.json", + '--app-id abc123 --config-json \'{"nodes":[],"edges":[]}\'', + "--app-id abc123 --config-file ./canvas.json --flow-version 5", + ], + validate: (flags) => { + const conflict = validateConfigFlags(flags); + if (conflict) return conflict; + if (flags.configJson === undefined && flags.configFile === undefined) { + return { + "en-US": "One of --config-json or --config-file is required", + "zh-CN": "必须提供 --config-json 或 --config-file 之一", + }; + } + return undefined; + }, + async run(ctx) { + const { settings, flags } = ctx; + const appCode = flags.appId; + const canvas = await readConfigJson(flags); + + let version = flags.flowVersion; + if (version === undefined) { + const current = await fetchWorkflowCanvas(ctx.client, appCode); + version = current.version; + } + const data = { appCode, config: JSON.stringify(canvas), version }; + + if (settings.dryRun) { + ctx.output.result(consoleDryRunPreview(settings, FLOW_SAVE_API, data)); + return; + } + + const payload = await consoleCall(ctx.client, FLOW_SAVE_API, data); + ctx.output.result( + { app_code: appCode, version, result: payload }, + settings.output === "text" + ? { + text: ctx.localize({ + "en-US": `Workflow ${appCode} canvas saved (version ${version}).`, + "zh-CN": `工作流 ${appCode} 画布已保存(版本 ${version})。`, + }), + } + : undefined, + ); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 441b1ca4..732502bf 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -37,6 +37,17 @@ export { default as configAgent } from "./commands/config/agent/index.ts"; export { default as update } from "./commands/update.ts"; export { default as appCall } from "./commands/app/call.ts"; export { default as appList } from "./commands/app/list.ts"; +export { default as appCreate } from "./commands/app/create.ts"; +export { default as appGet } from "./commands/app/get.ts"; +export { default as appUpdate } from "./commands/app/update.ts"; +export { default as appPublish } from "./commands/app/publish.ts"; +export { default as appDelete } from "./commands/app/delete.ts"; +export { default as appCopy } from "./commands/app/copy.ts"; +export { default as appVersions } from "./commands/app/versions.ts"; +export { default as appWorkflowGet } from "./commands/app/workflow/get.ts"; +export { default as appWorkflowSave } from "./commands/app/workflow/save.ts"; +export { default as appWorkflowPublish } from "./commands/app/workflow/publish.ts"; +export { default as appWorkflowExport } from "./commands/app/workflow/export.ts"; export { default as memoryAdd } from "./commands/memory/add.ts"; export { default as memorySearch } from "./commands/memory/search.ts"; export { default as memoryList } from "./commands/memory/list.ts"; diff --git a/packages/commands/tests/e2e/app-control.e2e.test.ts b/packages/commands/tests/e2e/app-control.e2e.test.ts new file mode 100644 index 00000000..f3f16180 --- /dev/null +++ b/packages/commands/tests/e2e/app-control.e2e.test.ts @@ -0,0 +1,638 @@ +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vite-plus/test"; +import { httpTransport } from "bailian-cli-core"; +import { buildIsolatedCliEnv } from "e2e/isolation"; +import { + isConsoleE2EReady, + parseStdoutJson, + runCommandE2e, + runCommandHelp, + runCommandInProcess, + runConsoleCommandE2e, +} from "./helpers.ts"; +import { APP_CONTROL_ROUTES } from "./topic-routes.ts"; +import { + APP_CONTROL_CREATE_API, + APP_CONTROL_DELETE_API, + APP_CONTROL_DETAIL_API, + APP_CONTROL_LIST_API, + APP_CONTROL_UPDATE_API, + APP_DATA_CREATE_DRAFT_API, + APP_DATA_GET_API, + APP_DATA_UPDATE_DRAFT_API, + FLOW_GET_CONFIG_API, + FLOW_PUBLISH_API, + FLOW_SAVE_API, +} from "../../src/commands/app/shared.ts"; + +// Offline only: real Console credentials and online acceptance are deliberately not used. +const offlineEnvironment: NodeJS.ProcessEnv = { + DASHSCOPE_API_KEY: undefined, + DASHSCOPE_ACCESS_TOKEN: undefined, + DASHSCOPE_BASE_URL: undefined, + BAILIAN_ACCESS_KEY_ID: undefined, + BAILIAN_ACCESS_KEY_SECRET: undefined, + BAILIAN_SECURITY_TOKEN: undefined, + ALIBABA_CLOUD_ACCESS_KEY_ID: undefined, + ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined, + ALIBABA_CLOUD_SECURITY_TOKEN: undefined, + BAILIAN_WORKSPACE_ID: undefined, + DASHSCOPE_OUTPUT: undefined, + DASHSCOPE_VERBOSE: "0", + DASHSCOPE_QUIET: "0", + HTTP_PROXY: undefined, + HTTPS_PROXY: undefined, + ALL_PROXY: undefined, + NO_PROXY: undefined, + http_proxy: undefined, + https_proxy: undefined, + all_proxy: undefined, + no_proxy: undefined, + NODE_USE_ENV_PROXY: "0", + NODE_OPTIONS: undefined, + NO_UPDATE_NOTIFIER: "1", + DO_NOT_TRACK: "1", + NO_COLOR: "1", + FORCE_COLOR: undefined, +}; + +interface GatewayRequest { + Api: string; + Data: Record; +} + +/** Mock the console gateway; record requests and reply with a DataV2 envelope. */ +function mockConsole(responses: Record): GatewayRequest[] { + const requests: GatewayRequest[] = []; + vi.spyOn(httpTransport, "fetch").mockImplementation(async (_url, init) => { + if (typeof init?.body !== "string") throw new Error("Expected a console gateway form body."); + const body = new URLSearchParams(init.body); + const request = JSON.parse(body.get("params") ?? "{}") as GatewayRequest; + requests.push(request); + if (!(request.Api in responses)) throw new Error(`Unexpected console API: ${request.Api}`); + const payload = responses[request.Api]; + return new Response(JSON.stringify({ data: { DataV2: { data: { data: payload } } } })); + }); + return requests; +} + +function writeConfig(configDir: string, body: Record): void { + writeFileSync(join(configDir, "config.json"), JSON.stringify(body)); +} + +// --------------------------------------------------------------------------- +// Offline contracts: help, validation, static dry-run (zero requests), risk gate +// --------------------------------------------------------------------------- + +describe("e2e: app control offline contracts", () => { + let isolated: ReturnType; + + beforeEach(() => { + isolated = buildIsolatedCliEnv({ overrides: offlineEnvironment }); + writeConfig(isolated.env.BAILIAN_CONFIG_DIR!, { language: "en-US", telemetry: false }); + vi.spyOn(httpTransport, "fetch").mockRejectedValue(new Error("Unexpected Console HTTP")); + vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Unexpected offline HTTP")); + }); + afterEach(() => { + try { + expect(httpTransport.fetch).not.toHaveBeenCalled(); + expect(globalThis.fetch).not.toHaveBeenCalled(); + } finally { + vi.restoreAllMocks(); + isolated.dispose(); + } + }); + + function offline(args: string[]) { + return runCommandInProcess(APP_CONTROL_ROUTES, args, { + ...isolated.env, + ...offlineEnvironment, + }); + } + + const leaves = [ + { path: "app create", flags: ["--name", "--type", "--config-json", "--config-file"] }, + { path: "app get", flags: ["--app-id", "--type", "--scope", "--agent-version"] }, + { path: "app update", flags: ["--app-id", "--name", "--model", "--instructions"] }, + { path: "app publish", flags: ["--app-id", "--description"] }, + { path: "app delete", flags: ["--app-id", "--yes"] }, + { path: "app copy", flags: ["--app-id"] }, + { path: "app versions", flags: ["--app-id", "--type"] }, + { path: "app workflow get", flags: ["--app-id", "--flow-version", "--output-file"] }, + { + path: "app workflow save", + flags: ["--app-id", "--config-json", "--config-file", "--flow-version"], + }, + { path: "app workflow publish", flags: ["--app-id", "--description", "--trigger-list"] }, + { path: "app workflow export", flags: ["--app-id", "--flow-version", "--output-file"] }, + ]; + + test.each(leaves)("$path help exposes Console scope flags", async (leaf) => { + const result = await runCommandHelp(APP_CONTROL_ROUTES, [...leaf.path.split(" "), "--help"]); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stderr).toContain(`bl ${leaf.path}`); + for (const flag of [ + ...leaf.flags, + "--console-region", + "--console-site", + "--console-switch-agent", + "--workspace-id", + ]) { + expect(result.stderr).toContain(flag); + } + }); + + const missingRequired = [ + { path: "app create", args: [] as string[] }, + { path: "app get", args: [] }, + { path: "app update", args: [] }, + { path: "app publish", args: [] }, + { path: "app delete", args: [] }, + { path: "app copy", args: [] }, + { path: "app versions", args: [] }, + { path: "app workflow get", args: [] }, + { path: "app workflow save", args: [] }, + { path: "app workflow publish", args: [] }, + { path: "app workflow export", args: [] }, + ]; + + test.each(missingRequired)("$path without required flags exits 2", async (scenario) => { + const result = await offline([ + ...scenario.path.split(" "), + ...scenario.args, + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + }); + + test("app create rejects invalid --config-json JSON", async () => { + const result = await offline([ + "app", + "create", + "--name", + "x", + "--config-json", + "{not json", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + expect(JSON.parse(result.stderr).error.message).toMatch(/valid JSON/i); + }); + + test("app create rejects --config-json together with --config-file", async () => { + const result = await offline([ + "app", + "create", + "--name", + "x", + "--config-json", + "{}", + "--config-file", + "c.json", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + expect(JSON.parse(result.stderr).error.message).toMatch(/mutually exclusive/i); + }); + + test("app create workflow rejects --model", async () => { + const result = await offline([ + "app", + "create", + "--name", + "x", + "--type", + "workflow", + "--model", + "qwen3-max", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + expect(JSON.parse(result.stderr).error.message).toMatch(/workflow save/i); + }); + + test("app update requires at least one changed field", async () => { + const result = await offline([ + "app", + "update", + "--app-id", + "abc", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + expect(JSON.parse(result.stderr).error.message).toMatch(/at least one/i); + }); + + test("app get workflow rejects --agent-version", async () => { + const result = await offline([ + "app", + "get", + "--app-id", + "abc", + "--type", + "workflow", + "--agent-version", + "3", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + }); + + test("app workflow save requires --config-json or --config-file", async () => { + const result = await offline([ + "app", + "workflow", + "save", + "--app-id", + "abc", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(2); + expect(JSON.parse(result.stderr).error.message).toMatch(/--config/); + }); + + const staticDryRuns = [ + { + name: "create agent", + args: ["app", "create", "--name", "Demo"], + api: APP_CONTROL_CREATE_API, + }, + { + name: "create workflow", + args: ["app", "create", "--name", "Flow", "--type", "workflow"], + api: APP_DATA_CREATE_DRAFT_API, + }, + { + name: "get agent", + args: ["app", "get", "--app-id", "abc"], + api: APP_CONTROL_DETAIL_API, + }, + { + name: "publish agent", + args: ["app", "publish", "--app-id", "abc"], + api: "zeldaEasy.broadscope-bailian.app-control.publish", + }, + { + name: "delete agent", + args: ["app", "delete", "--app-id", "abc"], + api: APP_CONTROL_DELETE_API, + }, + { + name: "copy agent", + args: ["app", "copy", "--app-id", "abc"], + api: "zeldaEasy.broadscope-bailian.app-control.copy", + }, + { + name: "versions agent", + args: ["app", "versions", "--app-id", "abc"], + api: "zeldaEasy.broadscope-bailian.app-control.getAgentVersionList", + }, + { + name: "workflow get", + args: ["app", "workflow", "get", "--app-id", "abc"], + api: FLOW_GET_CONFIG_API, + }, + { + name: "workflow export", + args: ["app", "workflow", "export", "--app-id", "abc"], + api: "zeldaEasy.broadscope-bailian.app-orchestra-flow.generateDSLSchema", + }, + ]; + + test.each(staticDryRuns)( + "$name --dry-run previews the API with zero requests", + async (scenario) => { + const result = await offline([...scenario.args, "--dry-run", "--output", "json"]); + expect(result.exitCode, result.stderr).toBe(0); + const data = parseStdoutJson<{ api: string; data: Record }>(result.stdout); + expect(data.api).toBe(scenario.api); + expect(data.data).toBeDefined(); + }, + ); + + test("app list --type workflow uses exclusion filter", async () => { + const result = await offline([ + "app", + "list", + "--type", + "workflow", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + const data = parseStdoutJson<{ api: string; data: { reqDTO: Record } }>( + result.stdout, + ); + expect(data.api).toBe(APP_CONTROL_LIST_API); + expect(data.data.reqDTO.notInTypes).toEqual([5, 8, 10]); + expect(data.data.reqDTO.type).toBeUndefined(); + }); + + test("app list default keeps agent filter", async () => { + const result = await offline(["app", "list", "--dry-run", "--output", "json"]); + expect(result.exitCode, result.stderr).toBe(0); + const data = parseStdoutJson<{ data: { reqDTO: Record } }>(result.stdout); + expect(data.data.reqDTO.type).toBe(5); + expect(data.data.reqDTO.notInTypes).toEqual([10]); + }); + + test("app delete without --yes requires confirmation (exit 7)", async () => { + const result = await offline(["app", "delete", "--app-id", "abc", "--output", "json"]); + expect(result.exitCode, result.stderr).toBe(7); + const error = JSON.parse(result.stderr).error; + expect(error.type).toBe("requires_confirmation"); + }); +}); + +// --------------------------------------------------------------------------- +// Mocked console: request payloads, read-modify-write planning, response unwrap +// --------------------------------------------------------------------------- + +describe("e2e: app control mocked console", () => { + let isolated: ReturnType; + + beforeEach(() => { + isolated = buildIsolatedCliEnv({ overrides: offlineEnvironment }); + writeConfig(isolated.env.BAILIAN_CONFIG_DIR!, { + language: "en-US", + telemetry: false, + access_token: "test-console-token", + }); + }); + afterEach(() => { + vi.restoreAllMocks(); + isolated.dispose(); + }); + + function run(args: string[]) { + return runCommandInProcess(APP_CONTROL_ROUTES, args, { + ...isolated.env, + ...offlineEnvironment, + }); + } + + test("app create agent sends default config and extracts the app code", async () => { + const requests = mockConsole({ [APP_CONTROL_CREATE_API]: { result: "app-new-123" } }); + const result = await run([ + "app", + "create", + "--name", + "Demo", + "--model", + "qwen3-max", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests).toHaveLength(1); + const reqDTO = requests[0]!.Data.reqDTO as Record; + expect(reqDTO.name).toBe("Demo"); + expect(reqDTO.type).toBe(5); + expect(reqDTO.source).toBe("console"); + const config = reqDTO.config as Record; + expect(config.model).toBe("qwen3-max"); + expect(config.dialog_round).toBe(3); + const data = parseStdoutJson<{ app_code: string }>(result.stdout); + expect(data.app_code).toBe("app-new-123"); + }); + + test("app update agent dry-run reads the draft and previews merged config (no write)", async () => { + const requests = mockConsole({ + [APP_CONTROL_DETAIL_API]: { + code: "abc", + name: "Old", + config: { model: "auto", dialog_round: 3 }, + }, + }); + const result = await run([ + "app", + "update", + "--app-id", + "abc", + "--model", + "qwen3-max", + "--instructions", + "Be helpful", + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + // Only the read-only detail call; the update write must not happen. + expect(requests.map((request) => request.Api)).toEqual([APP_CONTROL_DETAIL_API]); + const data = parseStdoutJson<{ api: string; data: { reqDTO: Record } }>( + result.stdout, + ); + expect(data.api).toBe(APP_CONTROL_UPDATE_API); + const config = data.data.reqDTO.config as Record; + expect(config.model).toBe("qwen3-max"); + expect(config.instruction).toBe("Be helpful"); + expect(config.dialog_round).toBe(3); + }); + + test("app update agent merges current config and writes once", async () => { + const requests = mockConsole({ + [APP_CONTROL_DETAIL_API]: { code: "abc", config: { model: "auto", dialog_round: 5 } }, + [APP_CONTROL_UPDATE_API]: { result: true }, + }); + const result = await run([ + "app", + "update", + "--app-id", + "abc", + "--config-json", + '{"max_loop":10}', + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([ + APP_CONTROL_DETAIL_API, + APP_CONTROL_UPDATE_API, + ]); + const reqDTO = requests[1]!.Data.reqDTO as Record; + const config = reqDTO.config as Record; + expect(config.dialog_round).toBe(5); + expect(config.max_loop).toBe(10); + expect(config.model).toBe("auto"); + }); + + test("app update workflow reads the DTO and writes metadata back", async () => { + const requests = mockConsole({ + [APP_DATA_GET_API]: { code: "abc", name: "Old", appDescription: "old desc", type: 7 }, + [APP_DATA_UPDATE_DRAFT_API]: { result: true }, + }); + const result = await run([ + "app", + "update", + "--app-id", + "abc", + "--type", + "workflow", + "--name", + "New", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([ + APP_DATA_GET_API, + APP_DATA_UPDATE_DRAFT_API, + ]); + const reqDTO = requests[1]!.Data.reqDTO as Record; + expect(reqDTO.name).toBe("New"); + expect(reqDTO.appDescription).toBe("old desc"); + expect(reqDTO.type).toBe(7); + }); + + test("app workflow save dry-run resolves version with a read only", async () => { + const requests = mockConsole({ + [FLOW_GET_CONFIG_API]: { config: '{"nodes":[]}', version: "7" }, + }); + const result = await run([ + "app", + "workflow", + "save", + "--app-id", + "abc", + "--config-json", + '{"nodes":[1]}', + "--dry-run", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([FLOW_GET_CONFIG_API]); + const data = parseStdoutJson<{ api: string; data: Record }>(result.stdout); + expect(data.api).toBe(FLOW_SAVE_API); + expect(data.data.version).toBe("7"); + expect(data.data.config).toBe('{"nodes":[1]}'); + }); + + test("app workflow save writes the canvas with the resolved version", async () => { + const requests = mockConsole({ + [FLOW_GET_CONFIG_API]: { config: '{"nodes":[]}', version: "7" }, + [FLOW_SAVE_API]: { result: true }, + }); + const result = await run([ + "app", + "workflow", + "save", + "--app-id", + "abc", + "--config-json", + '{"nodes":[1]}', + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([FLOW_GET_CONFIG_API, FLOW_SAVE_API]); + expect(requests[1]!.Data.version).toBe("7"); + expect(requests[1]!.Data.appCode).toBe("abc"); + }); + + test("app workflow publish normalizes triggers from the draft", async () => { + const draftTriggers = [ + { flowName: "keep", uiConfig: '{"type":"webhook"}', parameters: [] }, + { flowName: "drop", uiConfig: '{"type":"none"}', parameters: [] }, + ]; + const requests = mockConsole({ + [FLOW_GET_CONFIG_API]: { + config: '{"nodes":[]}', + version: "9", + applicationDTO: { applicationConfigDTO: { triggerList: draftTriggers } }, + }, + [FLOW_PUBLISH_API]: { result: true }, + }); + const result = await run([ + "app", + "workflow", + "publish", + "--app-id", + "abc", + "--description", + "v2", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([FLOW_GET_CONFIG_API, FLOW_PUBLISH_API]); + const publishData = requests[1]!.Data; + expect(publishData.version).toBe("9"); + expect(publishData.description).toBe("v2"); + const triggerList = publishData.triggerList as Array>; + expect(triggerList).toHaveLength(1); + expect(triggerList[0]!.flowName).toBe("keep"); + expect(triggerList[0]!.uiConfig).toBeUndefined(); + }); + + test("app delete --yes enters run and calls the delete API", async () => { + const requests = mockConsole({ [APP_CONTROL_DELETE_API]: { result: true } }); + const result = await run(["app", "delete", "--app-id", "abc", "--yes", "--output", "json"]); + expect(result.exitCode, result.stderr).toBe(0); + expect(requests.map((request) => request.Api)).toEqual([APP_CONTROL_DELETE_API]); + expect((requests[0]!.Data.reqDTO as Record).code).toBe("abc"); + }); +}); + +// --------------------------------------------------------------------------- +// Representative subprocess coverage (shell / stdio / exit-code contract) +// --------------------------------------------------------------------------- + +describe("e2e: app control subprocess", () => { + test("app create --dry-run previews via a real subprocess", async () => { + const result = await runCommandE2e( + APP_CONTROL_ROUTES, + ["app", "create", "--name", "Demo", "--dry-run", "--output", "json"], + { ...offlineEnvironment, BAILIAN_E2E: "0" }, + ); + expect(result.exitCode, result.stderr).toBe(0); + const data = parseStdoutJson<{ api: string }>(result.stdout); + expect(data.api).toBe(APP_CONTROL_CREATE_API); + }); + + test("app delete confirmation gate via a real subprocess (exit 7)", async () => { + const result = await runCommandE2e( + APP_CONTROL_ROUTES, + ["app", "delete", "--app-id", "abc", "--output", "json"], + { ...offlineEnvironment, BAILIAN_E2E: "0" }, + ); + expect(result.exitCode, result.stderr).toBe(7); + expect(JSON.parse(result.stderr).error.type).toBe("requires_confirmation"); + }); +}); + +// --------------------------------------------------------------------------- +// Live integration (gated; requires a real console session) +// --------------------------------------------------------------------------- + +describe.skipIf(!isConsoleE2EReady())("e2e: app control live (Console)", () => { + test("app list returns a structured page", async () => { + const result = await runConsoleCommandE2e(APP_CONTROL_ROUTES, [ + "app", + "list", + "--page-size", + "5", + "--output", + "json", + ]); + expect(result.exitCode, result.stderr).toBe(0); + const data = parseStdoutJson<{ total: number; apps: unknown[] }>(result.stdout); + expect(Array.isArray(data.apps)).toBe(true); + }, 60_000); +}); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index bb46dbaa..9adce6f3 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -179,6 +179,21 @@ export const CONSOLE_READ_ROUTES: E2eRouteExports = { "dataset groups": "datasetGroups", }; +export const APP_CONTROL_ROUTES: E2eRouteExports = { + "app list": "appList", + "app create": "appCreate", + "app get": "appGet", + "app update": "appUpdate", + "app publish": "appPublish", + "app delete": "appDelete", + "app copy": "appCopy", + "app versions": "appVersions", + "app workflow get": "appWorkflowGet", + "app workflow save": "appWorkflowSave", + "app workflow publish": "appWorkflowPublish", + "app workflow export": "appWorkflowExport", +}; + export const ADVISOR_ROUTES: E2eRouteExports = { "advisor recommend": "advisorRecommend", }; diff --git a/packages/core/package.json b/packages/core/package.json index a1d094d3..6ff4b57e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "2.8.6", + "version": "2.9.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": { diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 067c5db9..46acf87a 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "2.8.6", + "version": "2.9.0", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index af4549fe..2ffea374 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "2.8.6", + "version": "2.9.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": { diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index a97999d5..d640aad3 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,12 +1,12 @@ --- name: bailian-cli metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- 阿里云百炼 / Aliyun Bailian / DashScope 资源管理与 `bl` CLI hub: - 应用调用(bl app)、决策模型推理(bl decision)、模型目录/模型列表、用量/额度/配额、免费额度、 + 应用调用与管理(bl app:调用/创建/更新/发布/删除/复制/版本、工作流画布)、决策模型推理(bl decision)、模型目录/模型列表、用量/额度/配额、免费额度、 工作空间、MCP 市场、pipeline、文件上传、ParseX 文档/音视频解析与字段抽取、Batch 批量推理与 CSV 转换、同步 Batch Chat、console API、登录鉴权与配置、 Agent skill 安装/列表/更新/卸载(bl skill add|list|update|remove,百炼 skill registry)。 用户点名百炼 / DashScope / `bl`,或继续既有 `bl` 工作流时直接使用。 @@ -74,7 +74,9 @@ Use this table only after the decision table in [`bailian-protocol`](../bailian- | Typed decision model inference | `bl decision` | Workspace endpoint; returns `choice` / `noul` / `score` values and probabilities, not generated text | | Explicit ParseX parsing / structured field extraction | `bl parsex parse submit` / `result`; `bl parsex extract submit` / `result` | Workspace + API key; submit returns biz_id, result --follow waits. HTTP(S) source URLs; extraction can reuse a parsed document. See `reference/parsex.md`. | | Bailian agent / workflow | `bl app call` | Needs `--app-id` | -| Find app by name | `bl app list` then `bl app call` | Console auth | +| Find app by name | `bl app list` then `bl app call` | Console auth; `--type workflow` lists workflows | +| Create / update / publish / delete / copy an app | `bl app create` / `update` / `publish` / `delete` / `copy` / `get` / `versions` | Console auth; agent config via `--model`/`--instructions`/`--config-json`; `delete` needs `--yes`; see [`reference/app.md`](reference/app.md) | +| Workflow canvas read / save / publish / export | `bl app workflow get` / `save` / `publish` / `export` | Console auth; `save` replaces the whole draft canvas; see [`reference/app.md`](reference/app.md) | | Personal memory / Bailian memory resources | → skill `bailian-memory` | User-enabled personal memory; explicit resource operations use `bl memory --help` as fallback. | | Personal documents / Bailian knowledge workflows | Read skill `bailian-knowledge` | If missing: `bl knowledge --help` or `bl skill init` | | Resolve a Console file key / error-report URL | `bl file url` | Console auth; use the returned business file key, not a file ID or upload URI. Resolves only; no download or fixed expiry. Existing URLs need no resolution. | @@ -115,6 +117,7 @@ bl text chat --message "Write a poem about spring in Chinese" bl decision --workspace-id --state "Is this requesting a refund?" --questions '{"refund":{"type":"noul"}}' bl app list --output json bl app call --app-id --prompt "Hello" +bl app create --name "My agent" --model qwen3-max bl usage stats bl model list --model qwen ``` @@ -131,6 +134,13 @@ More examples per command: see `reference/.md` (e.g. [`reference/text.md` 2. Pick `code` (app ID); handle `user_prompt_params` via `--biz-params '{"key":"value"}'` 3. `bl app call --app-id --prompt "..."` +### Create and configure an app + +1. `bl app create --name [--type workflow] --output json` → returns `app_code` +2. Agent: `bl app update --app-id --model --instructions "..."` (reads the draft, merges, writes) +3. `bl app publish --app-id ` to release; `bl app get --app-id ` to inspect +4. Workflow canvas: `bl app workflow get --app-id --output-file canvas.json` → edit → `bl app workflow save --app-id --config-file canvas.json` → `bl app workflow publish --app-id ` + ### Batch 文件工作流 1. 文件推理统一使用异步任务。读取 `reference/batch.md` 的数据样例;CSV 用 `bl batch convert` 构造,已有标准 Batch JSONL 可直接使用。 diff --git a/skills/bailian-cli/reference/app.md b/skills/bailian-cli/reference/app.md index d0a6dc84..063f1414 100644 --- a/skills/bailian-cli/reference/app.md +++ b/skills/bailian-cli/reference/app.md @@ -7,10 +7,21 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| ------------- | -------------- | ---------------------------------------------- | -| `bl app call` | API Key | Call a Bailian application (agent or workflow) | -| `bl app list` | Console | List Bailian applications | +| Command | Authentication | Description | +| ------------------------- | -------------- | ---------------------------------------------------------------- | +| `bl app call` | API Key | Call a Bailian application (agent or workflow) | +| `bl app copy` | Console | Copy a Bailian application (agent or workflow) | +| `bl app create` | Console | Create a Bailian application (agent or workflow) | +| `bl app delete` | Console | Delete a Bailian application (agent or workflow) | +| `bl app get` | Console | Get Bailian application details (agent or workflow) | +| `bl app list` | Console | List Bailian applications | +| `bl app publish` | Console | Publish a Bailian agent application | +| `bl app update` | Console | Update a Bailian application (agent config or workflow metadata) | +| `bl app versions` | Console | List published versions of a Bailian application | +| `bl app workflow export` | Console | Export a workflow as a DSL schema (download URL) | +| `bl app workflow get` | Console | Get a workflow canvas (draft or a published version) | +| `bl app workflow publish` | Console | Publish a workflow (publishes the current draft canvas) | +| `bl app workflow save` | Console | Save a workflow canvas (replaces the current draft) | ## Command details @@ -66,6 +77,162 @@ bl app call --app-id abc123 --prompt "Search for materials" --pipeline-ids pipe1 bl app call --app-id abc123 --prompt "Start" --biz-params '{"key":"value"}' ``` +### `bl app copy` + +| Field | Value | +| ------------------ | ---------------------------------------------- | +| **Name** | `app copy` | +| **Description** | Copy a Bailian application (agent or workflow) | +| **Authentication** | Console | +| **Usage** | `bl app copy --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Examples + +```bash +bl app copy --app-id abc123 +``` + +### `bl app create` + +| Field | Value | +| ------------------ | ------------------------------------------------ | +| **Name** | `app create` | +| **Description** | Create a Bailian application (agent or workflow) | +| **Authentication** | Console | +| **Usage** | `bl app create --name [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------------------------ | +| `--name ` | string | yes | Application name (required) | +| `--type ` | string | no | Application type: agent (default), workflow | +| `--description ` | string | no | Application description | +| `--icon ` | string | no | Application icon key | +| `--model ` | string | no | Agent model ID (default: auto; agent only) | +| `--config-json ` | string | no | Config JSON string (mutually exclusive with --config-file) | +| `--config-file ` | string | no | Path to a config JSON file (mutually exclusive with --config-json) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Agent creation uses the app-control 2.0 API with a default config (model auto, dialog_round 3). --config-json/--config-file shallow-merges over the default config; --model overrides config.model. +- Workflow creation creates an empty draft canvas; edit it with `app workflow save` and publish with `app workflow publish`. + +#### Examples + +```bash +bl app create --name "Customer service" +``` + +```bash +bl app create --name "Customer service" --model qwen3-max --description "After-sales bot" +``` + +```bash +bl app create --name "Order flow" --type workflow +``` + +```bash +bl app create --name "My agent" --config-file ./agent-config.json +``` + +### `bl app delete` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------- | +| **Name** | `app delete` | +| **Description** | Delete a Bailian application (agent or workflow) | +| **Authentication** | Console | +| **Usage** | `bl app delete --app-id [flags]` | +| **Risk** | `high` | +| **Risk message** | This permanently deletes the application and its configuration. Confirm the app ID before proceeding. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Deletion is irreversible. Run `app list` or `app get` first to confirm the app ID. + +#### Examples + +```bash +bl app delete --app-id abc123 +``` + +```bash +# Only after explicit user confirmation: +bl app delete --app-id abc123 --yes +``` + +### `bl app get` + +| Field | Value | +| ------------------ | --------------------------------------------------- | +| **Name** | `app get` | +| **Description** | Get Bailian application details (agent or workflow) | +| **Authentication** | Console | +| **Usage** | `bl app get --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | --------------------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--type ` | string | no | Application type: agent (default), workflow | +| `--scope ` | string | no | Data scope: draft (default), publish | +| `--agent-version ` | number | no | Agent version number (agent only; implies published scope by default) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Agent details come from app-control 2.0; workflow details from app-data. For the workflow canvas config use `app workflow get`. + +#### Examples + +```bash +bl app get --app-id abc123 +``` + +```bash +bl app get --app-id abc123 --scope publish +``` + +```bash +bl app get --app-id abc123 --agent-version 3 +``` + +```bash +bl app get --app-id abc123 --type workflow +``` + ### `bl app list` | Field | Value | @@ -82,6 +249,7 @@ bl app call --app-id abc123 --prompt "Start" --biz-params '{"key":"value"}' | `--name ` | string | no | Filter by app name (keyword search) | | `--page ` | number | no | Page number (default: 1) | | `--page-size ` | number | no | Results per page (default: 30) | +| `--type ` | string | no | Application type: agent (default), workflow | | `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | | `--console-site ` | string | no | Console site: domestic, international | | `--console-switch-agent ` | number | no | Switch agent UID for delegated access | @@ -101,6 +269,282 @@ bl app list --name customer service bl app list --page 2 --page-size 10 ``` +```bash +bl app list --type workflow +``` + ```bash bl app list --output json ``` + +### `bl app publish` + +| Field | Value | +| ------------------ | -------------------------------------- | +| **Name** | `app publish` | +| **Description** | Publish a Bailian agent application | +| **Authentication** | Console | +| **Usage** | `bl app publish --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--description ` | string | no | Version description | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Publishes the current agent draft via app-control 2.0. To publish a workflow, use `app workflow publish`. + +#### Examples + +```bash +bl app publish --app-id abc123 +``` + +```bash +bl app publish --app-id abc123 --description "Release v2" +``` + +### `bl app update` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------- | +| **Name** | `app update` | +| **Description** | Update a Bailian application (agent config or workflow metadata) | +| **Authentication** | Console | +| **Usage** | `bl app update --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------------------------ | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--type ` | string | no | Application type: agent (default), workflow | +| `--name ` | string | no | New application name | +| `--description ` | string | no | New application description | +| `--icon ` | string | no | New application icon key | +| `--model ` | string | no | Agent model ID (agent only; sets config.model) | +| `--instructions ` | string | no | Agent instructions (agent only; sets config.instruction) | +| `--config-json ` | string | no | Config JSON string (mutually exclusive with --config-file) | +| `--config-file ` | string | no | Path to a config JSON file (mutually exclusive with --config-json) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Reads the current draft first, then merges: current config -> --config-json/--config-file (shallow) -> --model/--instructions. Only changed fields are sent. Requires a console token even for --dry-run (read-only planning). +- Agent updates use app-control 2.0. Workflow updates only change metadata (name/description/icon); edit the canvas with `app workflow save`. + +#### Examples + +```bash +bl app update --app-id abc123 --name "New name" +``` + +```bash +bl app update --app-id abc123 --model qwen3-max +``` + +```bash +bl app update --app-id abc123 --instructions "You are a helpful assistant" +``` + +```bash +bl app update --app-id abc123 --config-file ./agent-config.json +``` + +```bash +bl app update --app-id abc123 --type workflow --name "New flow name" +``` + +### `bl app versions` + +| Field | Value | +| ------------------ | ------------------------------------------------ | +| **Name** | `app versions` | +| **Description** | List published versions of a Bailian application | +| **Authentication** | Console | +| **Usage** | `bl app versions --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--type ` | string | no | Application type: agent (default), workflow | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Examples + +```bash +bl app versions --app-id abc123 +``` + +```bash +bl app versions --app-id abc123 --type workflow +``` + +### `bl app workflow export` + +| Field | Value | +| ------------------ | ------------------------------------------------ | +| **Name** | `app workflow export` | +| **Description** | Export a workflow as a DSL schema (download URL) | +| **Authentication** | Console | +| **Usage** | `bl app workflow export --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | --------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--flow-version ` | string | no | Published version to export (default: current draft) | +| `--output-file ` | string | no | Download the DSL to this file instead of printing the URL | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Examples + +```bash +bl app workflow export --app-id abc123 +``` + +```bash +bl app workflow export --app-id abc123 --flow-version 3 +``` + +```bash +bl app workflow export --app-id abc123 --output-file ./flow.dsl +``` + +### `bl app workflow get` + +| Field | Value | +| ------------------ | ---------------------------------------------------- | +| **Name** | `app workflow get` | +| **Description** | Get a workflow canvas (draft or a published version) | +| **Authentication** | Console | +| **Usage** | `bl app workflow get --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--flow-version ` | string | no | Published version to read (default: current draft) | +| `--scope ` | string | no | Data scope: draft (default), publish | +| `--output-file ` | string | no | Write the canvas config JSON to this file (overwrites) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Without --flow-version, reads the current draft canvas (config + version). With --flow-version, reads a published version (scope defaults to publish). The canvas config is a JSON string; --output-file writes it parsed and pretty-printed. + +#### Examples + +```bash +bl app workflow get --app-id abc123 +``` + +```bash +bl app workflow get --app-id abc123 --flow-version 3 +``` + +```bash +bl app workflow get --app-id abc123 --output-file ./canvas.json +``` + +### `bl app workflow publish` + +| Field | Value | +| ------------------ | ------------------------------------------------------- | +| **Name** | `app workflow publish` | +| **Description** | Publish a workflow (publishes the current draft canvas) | +| **Authentication** | Console | +| **Usage** | `bl app workflow publish --app-id [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--description ` | string | no | Version description | +| `--trigger-list ` | string | no | Trigger list JSON array (default: read from the current draft) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Reads the current draft canvas and version, then publishes. Triggers are taken from the draft (applicationConfigDTO.triggerList, falling back to the Start node) unless --trigger-list is given; 'none' triggers are dropped. Requires a console token even for --dry-run (read-only planning). + +#### Examples + +```bash +bl app workflow publish --app-id abc123 +``` + +```bash +bl app workflow publish --app-id abc123 --description "Release v2" +``` + +```bash +bl app workflow publish --app-id abc123 --trigger-list '[]' +``` + +### `bl app workflow save` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------- | +| **Name** | `app workflow save` | +| **Description** | Save a workflow canvas (replaces the current draft) | +| **Authentication** | Console | +| **Usage** | `bl app workflow save --app-id (--config-json \| --config-file ) [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | --------------------------------------------------------------------------- | +| `--app-id ` | string | yes | Application ID (app code, required) | +| `--config-json ` | string | no | Config JSON string (mutually exclusive with --config-file) | +| `--config-file ` | string | no | Path to a config JSON file (mutually exclusive with --config-json) | +| `--flow-version ` | string | no | Expected current version for optimistic locking (default: read from server) | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- The canvas config is a whole-document replacement of the draft. Export it first with `app workflow get --output-file`, edit, then save. Without --flow-version the current version is read from the server (requires a console token, also for --dry-run). + +#### Examples + +```bash +bl app workflow save --app-id abc123 --config-file ./canvas.json +``` + +```bash +bl app workflow save --app-id abc123 --config-json '{"nodes":[],"edges":[]}' +``` + +```bash +bl app workflow save --app-id abc123 --config-file ./canvas.json --flow-version 5 +``` diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index d78c7e4d..4d15fad2 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -27,7 +27,18 @@ Use this index for the skill-scoped quick index and global flags. | `bl alert template update` | Console | Update a custom alert template (full replacement of conditions) | [alert.md](alert.md) | | `bl alert update` | Console | Update a model alert rule (full replacement; same fields as create) | [alert.md](alert.md) | | `bl app call` | API Key | Call a Bailian application (agent or workflow) | [app.md](app.md) | +| `bl app copy` | Console | Copy a Bailian application (agent or workflow) | [app.md](app.md) | +| `bl app create` | Console | Create a Bailian application (agent or workflow) | [app.md](app.md) | +| `bl app delete` | Console | Delete a Bailian application (agent or workflow) | [app.md](app.md) | +| `bl app get` | Console | Get Bailian application details (agent or workflow) | [app.md](app.md) | | `bl app list` | Console | List Bailian applications | [app.md](app.md) | +| `bl app publish` | Console | Publish a Bailian agent application | [app.md](app.md) | +| `bl app update` | Console | Update a Bailian application (agent config or workflow metadata) | [app.md](app.md) | +| `bl app versions` | Console | List published versions of a Bailian application | [app.md](app.md) | +| `bl app workflow export` | Console | Export a workflow as a DSL schema (download URL) | [app.md](app.md) | +| `bl app workflow get` | Console | Get a workflow canvas (draft or a published version) | [app.md](app.md) | +| `bl app workflow publish` | Console | Publish a workflow (publishes the current draft canvas) | [app.md](app.md) | +| `bl app workflow save` | Console | Save a workflow canvas (replaces the current draft) | [app.md](app.md) | | `bl auth generate-access-token` | No Auth | Generate a CLI access token using OpenAPI AK/SK | [auth.md](auth.md) | | `bl auth login` | No Auth | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) | [auth.md](auth.md) | | `bl auth logout` | No Auth | Clear stored credentials; full logout also clears the model Base URL | [auth.md](auth.md) | @@ -130,7 +141,7 @@ Use this index for the skill-scoped quick index and global flags. | `advisor` | `recommend` | [advisor.md](advisor.md) | | `agents` | `security alerts`, `security overview` | [agents.md](agents.md) | | `alert` | `create`, `delete`, `disable`, `enable`, `history`, `list`, `metrics`, `template create`, `template delete`, `template list`, `template update`, `update` | [alert.md](alert.md) | -| `app` | `call`, `list` | [app.md](app.md) | +| `app` | `call`, `copy`, `create`, `delete`, `get`, `list`, `publish`, `update`, `versions`, `workflow export`, `workflow get`, `workflow publish`, `workflow save` | [app.md](app.md) | | `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) | | `batch` | `cancel`, `convert`, `create`, `download`, `get`, `list` | [batch.md](batch.md) | | `cmodel` | `base-models`, `delete`, `get`, `import`, `list`, `watch` | [cmodel.md](cmodel.md) | diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index 4aaaf7ee..4ac1a15b 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 3525ea26..4de343ce 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-knowledge/SKILL.md b/skills/bailian-knowledge/SKILL.md index 2b41c275..ef7d4086 100644 --- a/skills/bailian-knowledge/SKILL.md +++ b/skills/bailian-knowledge/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-knowledge metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 19235fb8..cc9ba960 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-memory/SKILL.md b/skills/bailian-memory/SKILL.md index 3b73ae46..8edba5d2 100644 --- a/skills/bailian-memory/SKILL.md +++ b/skills/bailian-memory/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-memory metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 61f915cb..44f2081c 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index 332746cb..ce52ddf6 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-sandbox metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index dea10f6c..f17e18ad 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-web-search metadata: - version: "2.8.6" + version: "2.9.0" requires: bins: ["bl"] description: >-