Skip to content

Commit de9f1a3

Browse files
committed
feat(config): add active profile selection
- persist the active profile in config.json - resolve config with --config > active_config > default - add config list and config use commands - make auth and config writes target the selected profile - reset activation to default when deleting the active profile - update config UI with profile activation controls - keep token refresh and pipeline execution profile-aware - add loader, UI, auth, and CLI interaction coverage
1 parent 75a45e1 commit de9f1a3

26 files changed

Lines changed: 825 additions & 102 deletions

AGENTS.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,22 @@ Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/
5656

5757
按当前任务从下表挑一条进入对应文档:
5858

59-
| 场景 | 何时进入 | 详见 |
60-
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
61-
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
62-
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
63-
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
64-
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
65-
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
66-
| 错误文案变更 |`BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
67-
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
68-
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
69-
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
70-
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
71-
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
72-
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
73-
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
59+
| 场景 | 何时进入 | 详见 |
60+
| -------------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
61+
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
62+
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
63+
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
64+
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
65+
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
66+
| 错误文案变更 |`BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
67+
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
68+
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
69+
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
70+
| Profile / 激活 | 改命名 Profile、预设或 `active_config` | [docs/agents/config-profile-change.md](docs/agents/config-profile-change.md) |
71+
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
72+
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
73+
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
74+
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
7475

7576
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增 `docs/agents/<scenario>.md`,把清单沉淀下来。
7677

docs/agents/auth-change.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
3737
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
3838
- `bl auth login --open-api ...` 只更新 `access_key_id` / `access_key_secret`
3939
- `bl auth logout --console` 只清 `access_token`
40-
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret`
40+
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
4141
- `bl auth logout``api_key` + `access_token` + `access_key_*`
4242

4343
解析分工:
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Config Profile 与激活状态变更清单
2+
3+
适用于新增 Profile 预设、修改命名 Profile 选择规则、调整 `active_config`,或新增/修改 `bl config list/use/show/ui` 等 Profile 管理能力。
4+
5+
## 1. 保持存储边界
6+
7+
- Profile 业务字段继续由 `ConfigFile` / `CONFIG_FILE_KEYS` 管理。
8+
- `active_config``config.json` 顶层元数据,不得进入命名 Profile block,也不得被 `config set` 当作普通字段写入。
9+
- 识别命名 Profile 时必须排除业务字段和顶层元数据。
10+
- 旧配置缺少 `active_config` 时继续等价于激活 `default`
11+
12+
## 2. 保持选择语义
13+
14+
```text
15+
显式 --config <name> > active_config > default
16+
```
17+
18+
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`
19+
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
20+
- `--config``auth login --config ...` 不得隐式修改持久化激活状态。
21+
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
22+
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
23+
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
24+
25+
## 3. 保持读写命令交互一致
26+
27+
- `auth login``config set` 等写命令未传 `--config` 时修改当前激活项。
28+
- 写命令显式指定不存在的 `--config <name>` 时,仅在业务操作成功并实际落盘时创建 Profile。
29+
- `config show``auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
30+
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
31+
- 按凭证域退出时必须清理该域的完整字段集合,例如 OpenAPI 同时清理 AK、SK 和 STS `security_token`
32+
- 所有生产代码读取“当前配置”时优先经过 `buildSources()` 或携带解析后的 `configName`;直接调用无名称的 `readConfigFile()` / `writeConfigFile()` 只适用于明确操作顶层 default 的底层能力。
33+
34+
## 4. 保持状态一致性
35+
36+
- `config use` 只能激活已经存在的命名 Profile;`default` 始终有效。
37+
- 配置文件中的 `active_config` 指向不存在的 Profile 时返回 usage error,不静默回退。
38+
- 删除当前激活的命名 Profile 时,同一次落盘切回 `default`,不得留下悬空引用。
39+
- 配置写入继续使用临时文件 + rename,避免中断后留下半写文件。
40+
41+
## 5. 命令与展示联动
42+
43+
- 新增/重命名命令时同步 `packages/commands/src/index.ts` 和产品入口 `packages/cli/src/commands.ts`
44+
- `config list` 标识所有 Profile 与当前激活项。
45+
- `config show``auth status` 只输出本次最终选择的 `config``config_file`,不重复携带激活状态。
46+
- `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`
47+
- 同步 E2E topic routes、Skill setup 和自动生成 reference。
48+
49+
## 6. 最小测试矩阵
50+
51+
- 旧配置无 `active_config` -> `default`
52+
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
53+
- 显式命名 `--config``--config default` 均覆盖激活项且不修改磁盘状态。
54+
- 激活不存在的 Profile 失败且不写盘。
55+
- 悬空 `active_config` 明确失败。
56+
- 删除激活 Profile 后切回 `default`
57+
- 登录、退出、`config set` 分别覆盖“当前激活项”和“显式不存在名称成功后创建”。
58+
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
59+
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
60+
61+
## 7. 完成检查
62+
63+
```sh
64+
pnpm run sync:skill-assets
65+
vp check
66+
vp test
67+
```
68+
69+
命令 E2E 会启动本地子进程,Config UI 测试还会监听 `127.0.0.1` 临时端口;受限沙箱内出现 `EPERM` 时,需要在允许本地进程和端口的环境中复跑。

docs/token-plan-profile-integration.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Token Plan Profile 与激活配置接入方案
22

3-
> 状态:Token Plan 模型消费 MVP 已实现;Config 激活状态和通用 Base URL 归一化待实现。
3+
> 状态:Token Plan 模型消费 MVP Config 激活状态已实现;通用 Base URL 归一化待实现。
44
>
55
> 目标分支:`feat/cli-access-token`
66
@@ -163,7 +163,7 @@ token-plan *
163163

164164
- 未传 `--config`:展示当前激活的 Config。
165165
-`--config <name>`:展示指定 Config,不改变激活状态。
166-
- 输出中包含 `config``active``config_file`
166+
- 输出中包含最终选择的 `config``config_file`;激活状态统一由 `config list` / `config ui` 展示
167167

168168
`config ui` 应展示当前激活项,并提供激活操作。
169169

@@ -453,7 +453,7 @@ feat(cli): enable token-plan text and image consumption
453453
- [ ] 增加 `auth login --config token-plan --api-key ...`、文本消费和图片消费示例。
454454
- [ ] 与届时实际上线范围核对模型名称、服务地域、限制条件和用户措辞。
455455

456-
### Commit 4:Config 激活状态与切换命令(待实现
456+
### Commit 4:Config 激活状态与切换命令(已实现
457457

458458
建议提交信息:
459459

@@ -468,12 +468,18 @@ feat(config): add active profile selection
468468
- 保证 `--config default` 能显式覆盖激活项。
469469
- 新增 `bl config list`
470470
- 新增 `bl config use --name <name>`
471-
- `config show``auth status``config ui` 展示激活状态。
471+
- `config show``auth status` 展示最终选择项,`config list` `config ui` 展示激活状态。
472472
- 删除激活 Profile 时处理状态一致性。
473473
- 验证激活 `token-plan` 后不传 `--config` 的文本和图片请求。
474474
- 验证临时 `--config default` 不改变激活状态。
475475
- 更新命令导出、`packages/cli/src/commands.ts`、E2E 和生成 reference。
476476

477+
实现选择:删除当前激活的命名 Profile 时,在同一次配置文件写入中将 `active_config` 重置为 `default``auth login --config <name>` 和所有显式 `--config` 仍只作用于本次命令,不修改激活状态。
478+
479+
相关写入交互统一为:`auth login``auth logout``config set` 未传 `--config` 时作用于当前激活项;显式指定名称时作用于该名称。写命令可在成功落盘时创建不存在的 Profile,读命令不创建。Console access token 自动刷新同样限定在当前选中的 Profile,不得回退读写顶层 default。
480+
481+
激活项选择的是完整 Config,而不是只选择模型消费凭证。激活 `token-plan` 后,Token Plan 管控命令也会从该 Profile 解析 OpenAPI AK/SK,Console 命令也会从该 Profile 解析 Console 凭证。如果相应凭证仍保存在顶层 `default`,用户需要为单次命令显式传入 `--config default`,或将对应凭证域登录到 `token-plan`;CLI 不为不同鉴权域做隐式跨 Profile 回退。
482+
477483
### Commit 5:通用模型 Base URL 归一化(待实现)
478484

479485
建议提交信息:
@@ -532,7 +538,7 @@ vp check
532538
vp test
533539
```
534540

535-
完成改动后,应评估“Profile 预设与激活状态”是否需要沉淀为新的 `docs/agents/config-profile-change.md` 场景清单
541+
“Profile 预设与激活状态”的维护要求已沉淀到 `docs/agents/config-profile-change.md`
536542

537543
## 最终结论
538544

packages/cli/src/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
visionDescribe,
1717
configShow,
1818
configSet,
19+
configList,
20+
configUse,
1921
configUi,
2022
update,
2123
appCall,
@@ -110,6 +112,8 @@ export const commands: Record<string, AnyCommand> = {
110112
"vision describe": visionDescribe,
111113
"config show": configShow,
112114
"config set": configSet,
115+
"config list": configList,
116+
"config use": configUse,
113117
"config ui": configUi,
114118
update,
115119
"app call": appCall,

packages/cli/tests/e2e/config-profile.e2e.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ describe("e2e: named config", () => {
8989

9090
const devStatus = await runCli(["auth", "status", "--config", "dev", "--output", "json"], {
9191
BAILIAN_CONFIG_DIR: dir,
92+
DASHSCOPE_API_KEY: "",
93+
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
94+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
9295
});
9396
expect(devStatus.exitCode, devStatus.stderr).toBe(0);
9497
const devData = parseStdoutJson<Record<string, unknown>>(devStatus.stdout);
@@ -97,6 +100,9 @@ describe("e2e: named config", () => {
97100

98101
const defaultStatus = await runCli(["auth", "status", "--output", "json"], {
99102
BAILIAN_CONFIG_DIR: dir,
103+
DASHSCOPE_API_KEY: "",
104+
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
105+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
100106
});
101107
expect(defaultStatus.exitCode, defaultStatus.stderr).toBe(0);
102108
const defaultData = parseStdoutJson<Record<string, unknown>>(defaultStatus.stdout);
@@ -107,15 +113,26 @@ describe("e2e: named config", () => {
107113

108114
test("--config default 等价默认配置", async () => {
109115
await withTempConfigDir(async (dir) => {
110-
writeConfig(dir, { output: "json", api_key: "sk-default" });
116+
writeConfig(dir, {
117+
active_config: "token-plan",
118+
output: "json",
119+
api_key: "sk-default",
120+
"token-plan": { output: "text", api_key: "sk-token" },
121+
});
111122
const { stdout, stderr, exitCode } = await runCli(
112123
["config", "show", "--config", "default", "--output", "json"],
113124
{ BAILIAN_CONFIG_DIR: dir },
114125
);
115126
expect(exitCode, stderr).toBe(0);
116127
const data = parseStdoutJson<Record<string, unknown>>(stdout);
117128
expect(data.config).toBe("default");
129+
expect(data.active).toBeUndefined();
118130
expect(data.api_key).toBeDefined();
131+
const raw = JSON.parse(readFileSync(join(dir, "config.json"), "utf8")) as Record<
132+
string,
133+
unknown
134+
>;
135+
expect(raw.active_config).toBe("token-plan");
119136
});
120137
});
121138

@@ -124,4 +141,24 @@ describe("e2e: named config", () => {
124141
expect(exitCode).toBe(2);
125142
expect(stderr).toMatch(/Invalid config name/);
126143
});
144+
145+
test("auth status 文本输出分行展示选中 Config 和配置文件", async () => {
146+
await withTempConfigDir(async (dir) => {
147+
writeConfig(dir, {
148+
active_config: "token-plan",
149+
"token-plan": { api_key: "sk-token" },
150+
});
151+
152+
const result = await runCli(["auth", "status", "--output", "text"], {
153+
BAILIAN_CONFIG_DIR: dir,
154+
DASHSCOPE_API_KEY: "",
155+
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
156+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
157+
});
158+
expect(result.exitCode, result.stderr).toBe(0);
159+
expect(result.stdout).toContain("Config: token-plan\n");
160+
expect(result.stdout).toContain(`Config file: ${join(dir, "config.json")}\n`);
161+
expect(result.stdout).not.toContain("Active config:");
162+
});
163+
});
127164
});

packages/commands/src/commands/auth/status.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export default defineCommand({
7272
return;
7373
}
7474

75-
emitBare(`Config: ${configName} (${configFile})`);
75+
emitBare(`Config: ${configName}`);
76+
emitBare(`Config file: ${configFile}`);
7677
emitBare("Authentication Status:");
7778
if (apiKey) {
7879
emitBare(` API key (model): ${apiKey.source} ${apiKey.masked}`);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
2+
import { emitBare, emitResult } from "bailian-cli-runtime";
3+
4+
export default defineCommand({
5+
description: "List config profiles and show the active profile",
6+
auth: "none",
7+
exampleArgs: ["", "--output json"],
8+
async run(ctx) {
9+
const profiles = ctx.configStore.profiles();
10+
const names = ["default", ...Object.keys(profiles.named).sort()];
11+
const format = detectOutputFormat(ctx.settings.output);
12+
13+
if (format === "json") {
14+
emitResult(
15+
{
16+
active_config: profiles.active,
17+
profiles: names,
18+
config_file: ctx.configStore.path,
19+
},
20+
format,
21+
);
22+
return;
23+
}
24+
25+
const nameWidth = Math.max("NAME".length, ...names.map((name) => name.length));
26+
emitBare(`${"NAME".padEnd(nameWidth)} ACTIVE`);
27+
for (const name of names) {
28+
emitBare(`${name.padEnd(nameWidth)} ${name === profiles.active ? "*" : ""}`);
29+
}
30+
},
31+
});

0 commit comments

Comments
 (0)