Skip to content

Commit e9479e4

Browse files
Merge pull request #176 from modelstudioai/feat/profile-capability-fallback
feat(auth): add Profile API key capability fallback
2 parents fe11227 + 17ffde4 commit e9479e4

53 files changed

Lines changed: 1305 additions & 90 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
88

9+
## [1.18.0] - 2026-08-27
10+
11+
### Added
12+
13+
- **Automatic Profile API Key fallback** — unsupported commands use the API Key and endpoint from `default` without requiring a Profile switch; explicit credentials still take priority.
14+
- **Independent speech defaults** — Profiles support separate default models for TTS and ASR.
15+
- **Wan3.0 file-to-video**`bl video generate --file` accepts local files or URLs and validates incompatible inputs.
16+
17+
### Changed
18+
19+
- **Wan3.0 is now the default video model** — regular API Key text, image, and reference video tasks default to `wan3.0-video`, with first/last-frame and reference-audio support. Token Plan defaults remain unchanged.
20+
- **Expanded Token Plan preset** — login adds speech defaults and missing capabilities while preserving existing user configuration.
21+
- **Improved speech and Skill installation experience** — streaming synthesis defaults to PCM; installation docs add a Node 18-compatible fallback and clarify the Node.js and Git prerequisites.
22+
923
## [1.17.1] - 2026-08-22
1024

1125
### Fixed

CHANGELOG.zh.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66

77
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
88

9+
## [1.18.0] - 2026-08-27
10+
11+
### 新增
12+
13+
- **Profile API Key 自动回退** —— 当前 Profile 不支持某条命令时,自动使用 `default` 中的 API Key 和接入地址,无需手动切换 Profile;显式凭证仍然优先。
14+
- **语音合成与识别支持独立默认模型** —— Profile 支持分别配置 TTS 和 ASR 默认模型。
15+
- **Wan3.0 文件生视频** —— `bl video generate --file` 支持本地文件或 URL,并校验互斥输入。
16+
17+
### 变更
18+
19+
- **视频默认模型升级至 Wan3.0** —— 普通 API Key 的文生、图生和参考生视频默认使用 `wan3.0-video`,并支持首尾帧与参考音频;Token Plan 默认模型保持不变。
20+
- **扩展 Token Plan 预设** —— 登录后自动补充语音默认模型和缺失能力,同时保留用户已有配置。
21+
- **优化语音与 Skill 安装体验** —— 流式语音合成默认输出 PCM;Skill 安装文档补充兼容 Node 18 的降级方式及 Node.js、Git 前置条件。
22+
923
## [1.17.1] - 2026-08-22
1024

1125
### 修复

docs/agents/auth-change.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
3333

3434
`~/.bailian/config.json` 可同时保存 `api_key``access_token``access_key_*`。登录任一种方式不得删除另一种:
3535

36-
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
36+
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 验证成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
3737
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
3838
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi``console` 域凭证
3939
- `bl auth logout --console` 只清 `access_token`
@@ -44,7 +44,11 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
4444

4545
- `resolveApiKey()``auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
4646
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
47-
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
47+
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
48+
- `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
49+
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
50+
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
51+
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
4852
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
4953
`--config` 时继续写当前激活项,失败和 dry-run 不切换
5054
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认

docs/agents/cli-e2e-tests.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
9494

9595
## 安全与例外
9696

97-
- **禁止破坏真实用户配置**`auth logout` 默认只用 `--dry-run`需要验证实际落盘时,必须通过
98-
`BAILIAN_CONFIG_DIR` 指向隔离 fixture;`config set` 只用 `--dry-run`
97+
- **禁止破坏真实用户配置**`auth logout` `config set` 默认只用 `--dry-run`只有验证持久化契约时,才允许通过
98+
`BAILIAN_CONFIG_DIR` 指向每个用例独占的临时目录实际落盘,并必须在 `finally` 中清理;禁止写入或复用真实 `~/.bailian`
9999
- **不加 dry-run**`dryRun``resolveFileUrl` / `resolveCredential` / 上传**之后**的命令(如 `image edit``speech recognize``--url`
100100
- **`--list-voices` 等旁路**:先于 `--text` 校验的 flag,缺参用例勿带该 flag
101101
- 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言

docs/agents/command-pack.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
3333
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
3434
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`
35+
- [ ] API Key capability ID 由 host 按实际叶子命令路径生成(例如 `agent credential``agent.credential`);Command Pack 不声明额外元数据,Profile allowlist 对 pack 命令同样 fail closed。
36+
- [ ] `ctx.credentials.apiKey()` 必须返回 `authStage` 已注入 `ctx.client` 的实际凭证,不能重新读取原 Profile 绕过 capability fallback。
3537
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore``configStore`
3638
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager,避免 pack 安装或删除其他 pack。
3739
- [ ] 单包失败必须 fail-open:保留内置命令和其他合法 pack。
@@ -40,6 +42,7 @@
4042
## 测试与文档
4143

4244
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
45+
- [ ] Command Pack 测试覆盖 capability 格式/鉴权域、adapter 保留字段,以及 raw API Key 委托使用 fallback 后的实际 Base URL。
4346
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
4447
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
4548
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。

docs/agents/config-add.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ config 文件 ─┘
2929
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
3030
- `parseConfigFile()` 解析新字段
3131
- 如果是 enum 字段,加校验
32+
- 如果是数组字段,明确“缺失 / 空数组 / 非法值”的不同语义;安全策略字段的非法值必须 fail closed
3233

3334
### B. 加载逻辑
3435

@@ -51,6 +52,7 @@ config 文件 ─┘
5152
- [ ] 配置展示 / 修改命令同步:
5253
- `packages/commands/src/commands/config/show.ts` 显示新字段
5354
- `packages/commands/src/commands/config/set.ts``VALID_KEYS` / `KEY_ALIASES` / description 允许 set
55+
- `packages/commands/src/commands/config/ui.ts` / `ui-html.ts` 能按原类型往返数组字段,不能把 `[]` 保存成字段缺失
5456

5557
### E. 文档
5658

@@ -61,6 +63,7 @@ config 文件 ─┘
6163
- [ ] 单测覆盖优先级:flag > env > file
6264
- [ ] 校验失败抛错(非法值)
6365
- [ ] 默认值正确
66+
- [ ] 数组配置覆盖 CLI 逗号/JSON 输入、Config UI 往返、去重和显式空数组
6467

6568
## 完成后自查
6669

@@ -82,3 +85,4 @@ cat ~/.bailian/config.json
8285
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
8386
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
8487
-`config show` 不显示新字段,用户改了无法回查
88+
- ✗ UI 用 `String([])` 把显式空数组渲染为空串,保存后意外关闭安全策略

docs/agents/config-profile-change.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
2121
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
2222
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
23+
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
24+
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
25+
- 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
26+
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get``video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
2327
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
2428
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
2529

@@ -55,6 +59,8 @@
5559

5660
- 旧配置无 `active_config` -> `default`
5761
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
62+
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
63+
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`)按文件值生效。
5864
- 显式命名 `--config``--config default` 均覆盖激活项且不修改磁盘状态。
5965
- 激活不存在的 Profile 失败且不写盘。
6066
- 悬空 `active_config` 明确失败。
@@ -63,6 +69,9 @@
6369
- 显式 `auth login --config <name>` 成功后激活该 Profile,失败或 dry-run 不创建、不切换;
6470
`--config default` 成功后切回 `default`
6571
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
72+
- Console/OpenAPI/none 命令不参与 API Key capability fallback;fallback 后的 Client 特殊端点行为必须跟随最终解析的 `base_url`,不能根据原 `settings.configName` 推断端点类型。
73+
- Fallback 反馈只描述 CLI 能权威确认的本地行为:当前 Profile 不支持空格分隔的用户可见叶子命令,本次将从 `default` 读取 API Key 配置;不得声称整个 Profile 已切换,也不得展示 capability ID 或 `<undeclared>` 等内部值。存在 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式覆盖时必须跳过 fallback 且不输出反馈。
74+
- Fallback 反馈写 stderr,`--quiet` 抑制;text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象。dry-run 和后续鉴权失败仍保留反馈;JSON 模式下多个 diagnostics 以空行分隔,任何模式都不得输出凭证值。
6675
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
6776
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
6877
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。

docs/agents/error-hint-change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ runtime/error-handler.ts:
4646
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
4747
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
4848
- Node fs errno → 翻成 GENERAL + errno hint
49+
- `--output json` → stderr 输出两空格缩进的多行 error JSON;前面已有结构化 warning 时,两个 diagnostics 以空行分隔
4950
- 其它 Error → 默认走 cause 链
5051
5152
process.exit(err.exitCode)

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli",
3-
"version": "1.17.1",
3+
"version": "1.18.0",
44
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
55
"keywords": [
66
"agent",

packages/cli/tests/e2e/command-packs.e2e.test.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdtemp, rm } from "node:fs/promises";
1+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { fileURLToPath } from "node:url";
@@ -9,7 +9,19 @@ const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures",
99
let configDir: string;
1010

1111
function env(): NodeJS.ProcessEnv {
12-
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
12+
return {
13+
BAILIAN_CONFIG_DIR: configDir,
14+
DO_NOT_TRACK: "1",
15+
DASHSCOPE_API_KEY: "",
16+
DASHSCOPE_BASE_URL: "",
17+
};
18+
}
19+
20+
function parseStderrJsonDiagnostics(stderr: string): unknown[] {
21+
return stderr
22+
.trim()
23+
.split(/\n\s*\n/)
24+
.map((diagnostic) => JSON.parse(diagnostic) as unknown);
1325
}
1426

1527
describe("e2e: Command Pack", () => {
@@ -71,6 +83,68 @@ describe("e2e: Command Pack", () => {
7183
expect(executed.exitCode, executed.stderr).toBe(0);
7284
expect(executed.stdout).toContain("command-pack:hello");
7385

86+
await writeFile(
87+
join(configDir, "config.json"),
88+
JSON.stringify(
89+
{
90+
base_url: "https://default.example.com",
91+
active_config: "company-plan",
92+
"company-plan": {
93+
api_key: "sk-plan",
94+
base_url: "https://plan.example.com",
95+
api_key_capabilities: [],
96+
},
97+
},
98+
null,
99+
2,
100+
) + "\n",
101+
);
102+
const fallbackFailure = await runCli(["agent", "credential", "--output", "json"], env());
103+
expect(fallbackFailure.exitCode).toBe(3);
104+
expect(fallbackFailure.stderr).toMatch(/^\{\n {2}"warning": \{/);
105+
expect(fallbackFailure.stderr).toContain("\n\n{\n");
106+
expect(parseStderrJsonDiagnostics(fallbackFailure.stderr)).toEqual([
107+
{
108+
warning: {
109+
code: "PROFILE_API_KEY_FALLBACK",
110+
message:
111+
'Profile "company-plan" does not support command "agent credential"; API Key settings will be read from Profile "default" for this run.',
112+
profile: "company-plan",
113+
command_path: ["agent", "credential"],
114+
fallback_profile: "default",
115+
credential_fields: ["api_key", "base_url"],
116+
},
117+
},
118+
{
119+
error: expect.objectContaining({
120+
code: 3,
121+
message: "No API key found.",
122+
}),
123+
},
124+
]);
125+
126+
await writeFile(
127+
join(configDir, "config.json"),
128+
JSON.stringify(
129+
{
130+
api_key: "sk-default",
131+
base_url: "https://default.example.com",
132+
active_config: "company-plan",
133+
"company-plan": {
134+
api_key: "sk-plan",
135+
base_url: "https://plan.example.com",
136+
api_key_capabilities: [],
137+
},
138+
},
139+
null,
140+
2,
141+
) + "\n",
142+
);
143+
const fallbackCredential = await runCli(["agent", "credential"], env());
144+
expect(fallbackCredential.exitCode, fallbackCredential.stderr).toBe(0);
145+
expect(fallbackCredential.stderr).toContain('command "agent credential"');
146+
expect(fallbackCredential.stdout).toContain("credential-base-url:https://default.example.com");
147+
74148
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
75149
expect(credential.exitCode, credential.stderr).toBe(0);
76150
expect(credential.stdout).toContain("credential-source:flag");

0 commit comments

Comments
 (0)