feat(config): activate explicit profile after successful login

This commit is contained in:
若麒
2026-07-17 09:59:14 +08:00
parent 052960e269
commit ece0c8dd1c
8 changed files with 73 additions and 20 deletions
+2
View File
@@ -45,6 +45,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `resolveApiKey()` — `auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一去除 query、fragment、尾斜杠和已知 SDK/API Base 后缀,同时保留自定义网关前缀
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
`--config` 时继续写当前激活项,失败和 dry-run 不切换
- `resolveConsole()` — `auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
- `resolveOpenApi()` — `auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
- `describeAuthState()` — `auth status` / banner / telemetry 使用的只读快照
+6 -2
View File
@@ -17,7 +17,8 @@
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
- `--config` 和 `auth login --config ...` 不得隐式修改持久化激活状态。
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
@@ -25,7 +26,8 @@
## 3. 保持读写命令交互一致
- `auth login`、`config set` 等写命令未传 `--config` 时修改当前激活项。
- 写命令显式指定不存在的 `--config <name>` 时,仅在业务操作成功并实际落盘时创建 Profile。
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
创建和激活;`config set --config <name>` 可创建但不自动激活。
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
- 按凭证域退出时必须清理该域的完整字段集合,例如 OpenAPI 同时清理 AK、SK 和 STS `security_token`。
@@ -56,6 +58,8 @@
- 悬空 `active_config` 明确失败。
- 删除激活 Profile 后切回 `default`。
- 登录、退出、`config set` 分别覆盖“当前激活项”和“显式不存在名称成功后创建”。
- 显式 `auth login --config <name>` 成功后激活该 Profile,失败或 dry-run 不创建、不切换;
`--config default` 成功后切回 `default`。
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
- `config list/show/use/ui`、`auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
- `config ui` 覆盖保存时保留未管理字段,并继续允许空值清除 UI 管理字段。
+14 -4
View File
@@ -72,6 +72,7 @@ CLI 应解析并保存以下配置:
```json
{
"active_config": "token-plan",
"token-plan": {
"api_key": "<TOKEN_PLAN_API_KEY>",
"base_url": "https://token-plan.cn-beijing.maas.aliyuncs.com",
@@ -81,6 +82,9 @@ CLI 应解析并保存以下配置:
}
```
凭证验证和配置落盘成功后,CLI 在同一次配置文件写入中将 `token-plan` 设为激活项;验证失败和
dry-run 不创建、不切换 Profile。
用户仍可显式覆盖 Base URL,用于代理、测试或未来新增地域:
```sh
@@ -109,7 +113,7 @@ bl image generate --config token-plan --prompt "一只猫"
### 3. 激活 Config
新增命令:
登录时显式选择的 Profile 会自动激活;之后也可以主动切换:
```sh
bl config use --name token-plan
@@ -225,7 +229,8 @@ Config 激活只改变配置文件 block 的选择,`--config` 本身不提升
- 配置文件中的 `active_config` 指向不存在的 Profile:命令失败并提示切回 `default`,不得静默使用其他凭证。
- 删除当前激活的 Profile:删除操作同时切回 `default`,或者要求用户先切换;不能保留悬空引用。
- `config use --name token-plan` 只切换状态,不创建 Profile,也不执行登录。
- `auth login --config token-plan` 只写入指定 Profile,不自动激活,避免登录命令产生隐藏的全局状态变化。
- `auth login --config token-plan` 在凭证验证并落盘成功后自动激活该 Profile;验证失败和
dry-run 不创建、不切换。
## `token-plan` 内置 Profile 预设
@@ -474,7 +479,10 @@ feat(config): add active profile selection
- 验证临时 `--config default` 不改变激活状态。
- 更新命令导出、`packages/cli/src/commands.ts`、E2E 和生成 reference。
实现选择:删除当前激活的命名 Profile 时,在同一次配置文件写入中将 `active_config` 重置为 `default`。`auth login --config <name>` 和所有显式 `--config` 仍只作用于本次命令,不修改激活状态。
实现选择:删除当前激活的命名 Profile 时,在同一次配置文件写入中将 `active_config` 重置为
`default`。普通命令的显式 `--config` 仍只作用于本次命令;`auth login --config <name>` 是
例外,在凭证验证和落盘成功的同一次配置写入中激活目标 Profile。`--config default` 登录成功后
切回默认配置。
相关写入交互统一为:`auth login`、`auth logout` 和 `config set` 未传 `--config` 时作用于当前激活项;显式指定名称时作用于该名称。写命令可在成功落盘时创建不存在的 Profile,读命令不创建。Console access token 自动刷新同样限定在当前选中的 Profile,不得回退读写顶层 default。
@@ -552,4 +560,6 @@ Token Plan 模型消费最终表现为一个可激活的内置 Profile:
-> 文本/图片 endpoint
```
用户既可以通过 `--config token-plan` 单次使用,也可以通过 `bl config use --name token-plan` 将其设为默认激活配置。整个过程不引入 Token Plan 模式,也不复制现有模型调用实现。
用户执行 `auth login --config token-plan` 成功后,该 Profile 会成为默认激活配置;仍可通过
显式 `--config` 做单次覆盖,或使用 `bl config use --name <name>` 主动切换。整个过程不引入
Token Plan 模式,也不复制现有模型调用实现。
+23 -2
View File
@@ -15,7 +15,12 @@ import { AUTH_ROUTES } from "./topic-routes.ts";
interface ValidationServer {
baseUrl: string;
requests: Array<{ path: string; body: Record<string, unknown> }>;
requests: Array<{
path: string;
body: Record<string, unknown>;
authorization?: string;
sourceConfig?: string;
}>;
close(): Promise<void>;
}
@@ -29,6 +34,8 @@ async function startValidationServer(statusCode = 200): Promise<ValidationServer
requests.push({
path: request.url ?? "",
body: rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : {},
authorization: request.headers.authorization,
sourceConfig: request.headers["x-dashscope-source-config"] as string | undefined,
});
response.writeHead(statusCode, { "Content-Type": "application/json" });
if (statusCode >= 400) {
@@ -207,6 +214,8 @@ describe("e2e: auth", () => {
expect(validationServer.requests).toHaveLength(1);
expect(validationServer.requests[0]).toMatchObject({
path: "/compatible-mode/v1/chat/completions",
authorization: "Bearer sk-e2e-placeholder",
sourceConfig: expect.any(String),
body: {
model: "qwen3.7-max",
stream: false,
@@ -297,6 +306,8 @@ describe("e2e: auth", () => {
expect(validationServer.requests).toHaveLength(1);
expect(validationServer.requests[0]).toMatchObject({
path: "/compatible-mode/v1/chat/completions",
authorization: "Bearer sk-sp-e2e-placeholder",
sourceConfig: expect.any(String),
body: {
model: "qwen3.7-max",
stream: false,
@@ -309,6 +320,7 @@ describe("e2e: auth", () => {
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("token-plan");
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
@@ -377,7 +389,16 @@ describe("e2e: auth", () => {
try {
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-invalid", "--base-url", validationServer.baseUrl],
[
"auth",
"login",
"--config",
"failed-profile",
"--api-key",
"sk-invalid",
"--base-url",
validationServer.baseUrl,
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
+3 -2
View File
@@ -40,7 +40,7 @@ export interface AuthStore {
stored(): { apiKey: boolean; console: boolean; openapi: boolean; baseUrl?: string };
/** model 域 baseUrl 链(flag > env > config file > fallback)。 */
resolveBaseUrl(fallback?: string): string;
/** 登录落盘:合并写入,undefined 键忽略。 */
/** 登录落盘:合并写入,undefined 键忽略;显式 --config 成功后同时激活目标 Profile。 */
login(patch: AuthPersistPatch): Promise<void>;
/** 清凭证:console/openapi 只删对应域;all 清全部登录凭证。返回是否有变更。 */
logout(scope: "console" | "openapi" | "all"): Promise<boolean>;
@@ -50,6 +50,7 @@ export interface AuthStore {
export function makeAuthStore(sources: ResolutionSources): AuthStore {
const configName = sources.configName;
const activateAfterLogin = sources.flags.config !== undefined;
return {
describe: () => describeAuthState(sources),
stored() {
@@ -69,7 +70,7 @@ export function makeAuthStore(sources: ResolutionSources): AuthStore {
existing[key] = key === "base_url" ? normalizeModelBaseUrl(String(value)) : value;
}
}
await writeConfigFile(existing, configName);
await writeConfigFile(existing, configName, { activate: activateAfterLogin });
},
async logout(scope) {
const existing = readConfigFile(configName) as Record<string, unknown>;
+5
View File
@@ -81,9 +81,11 @@ export function readConfigFile(configName?: string): ConfigFile {
return parseConfigFile(readRawConfigBlock(raw, configName));
}
/** 写入所选 Profile;登录流程可在同一次原子写入中将显式 Profile 设为激活项。 */
export async function writeConfigFile(
data: Record<string, unknown>,
configName?: string,
options: { activate?: boolean } = {},
): Promise<void> {
const raw = readRawConfigObject();
if (configName) {
@@ -94,6 +96,9 @@ export async function writeConfigFile(
}
Object.assign(raw, data);
}
if (options.activate) {
raw[ACTIVE_CONFIG_KEY] = configName ?? "default";
}
await writeRawConfigObject(raw);
}
+7 -1
View File
@@ -106,7 +106,7 @@ test("AuthStore:login 合并落盘,logout 按域清理并报告变更", async ()
});
});
test("AuthStore:未传 --config 时写当前激活项,显式不存在名称在登录成功后创建", async () => {
test("AuthStore:未传 --config 时写当前激活项,显式配置在登录成功后创建并激活", async () => {
await inTempConfigDir(async () => {
await writeConfigFile({ api_key: "sk-default" });
await writeConfigFile({ access_token: "tok-dev" }, "dev");
@@ -124,6 +124,12 @@ test("AuthStore:未传 --config 时写当前激活项,显式不存在名称在
const newStore = makeAuthStore(buildSources({ config: "new-profile" }));
await newStore.login({ access_token: "tok-new" });
expect(readConfigFile("new-profile").access_token).toBe("tok-new");
expect(readConfigProfiles().active).toBe("new-profile");
const defaultStore = makeAuthStore(buildSources({ config: "default" }));
await defaultStore.login({ api_key: "sk-default-updated" });
expect(readConfigFile().api_key).toBe("sk-default-updated");
expect(readConfigProfiles().active).toBe("default");
expect(await activeStore.logout("console")).toBe(true);
expect(readConfigFile("dev").access_token).toBeUndefined();
+13 -9
View File
@@ -43,19 +43,23 @@ Use the `PlainApiKey` returned by `bl token-plan create-key` as a model API key.
```bash
bl auth login --config token-plan --api-key sk-sp-xxx
bl text chat --config token-plan --message "Hello"
bl image generate --config token-plan --prompt "A cat"
```
To make Token Plan the default Profile for commands that omit `--config`, activate it explicitly after login:
```bash
bl config use --name token-plan
bl text chat --message "Hello"
bl image generate --prompt "A cat"
```
`auth login --config token-plan` saves that Profile but does not activate it. Use `bl config list` to inspect the active Profile, `bl config use --name default` to switch back, or `--config default` for a one-command override. Config selection follows explicit `--config` > persisted `active_config` > `default`; credential and endpoint fields inside the selected Profile still follow flag > environment > config.
Successful login automatically activates the explicitly selected Profile. Use `bl config list` to
inspect it, and switch back when needed:
```bash
bl config list
bl config use --name default
```
`auth login --config token-plan` creates or updates that Profile and activates it only after the
credential is validated and saved. Failed login and `--dry-run` do not switch Profiles. Use
`--config default` for a one-command override. Config selection follows explicit `--config` >
persisted `active_config` > `default`; credential and endpoint fields inside the selected Profile
still follow flag > environment > config.
Activation selects the entire Config for every credential domain, not only model consumption. After activating `token-plan`, Token Plan management and Console commands also read their OpenAPI or Console credentials from that Profile. If those credentials remain in `default`, invoke the command with `--config default` or log the corresponding credential domain into `token-plan`.