diff --git a/docs/agents/auth-change.md b/docs/agents/auth-change.md index 301e009..4c6dbe8 100644 --- a/docs/agents/auth-change.md +++ b/docs/agents/auth-change.md @@ -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 ` 在凭证验证并落盘成功后自动激活目标 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 使用的只读快照 diff --git a/docs/agents/config-profile-change.md b/docs/agents/config-profile-change.md index e382c11..4e79e7d 100644 --- a/docs/agents/config-profile-change.md +++ b/docs/agents/config-profile-change.md @@ -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 ` 时,仅在业务操作成功并实际落盘时创建 Profile。 +- `auth login --config ` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时 + 创建和激活;`config set --config ` 可创建但不自动激活。 - `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 ` 成功后激活该 Profile,失败或 dry-run 不创建、不切换; + `--config default` 成功后切回 `default`。 - Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。 - `config list/show/use/ui`、`auth status` 和依赖默认模型的消费命令覆盖对应 E2E。 - `config ui` 覆盖保存时保留未管理字段,并继续允许空值清除 UI 管理字段。 diff --git a/docs/token-plan-profile-integration.md b/docs/token-plan-profile-integration.md index 9949120..f62adf8 100644 --- a/docs/token-plan-profile-integration.md +++ b/docs/token-plan-profile-integration.md @@ -72,6 +72,7 @@ CLI 应解析并保存以下配置: ```json { + "active_config": "token-plan", "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 ` 和所有显式 `--config` 仍只作用于本次命令,不修改激活状态。 +实现选择:删除当前激活的命名 Profile 时,在同一次配置文件写入中将 `active_config` 重置为 +`default`。普通命令的显式 `--config` 仍只作用于本次命令;`auth login --config ` 是 +例外,在凭证验证和落盘成功的同一次配置写入中激活目标 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 ` 主动切换。整个过程不引入 +Token Plan 模式,也不复制现有模型调用实现。 diff --git a/packages/commands/tests/e2e/auth.e2e.test.ts b/packages/commands/tests/e2e/auth.e2e.test.ts index 7630f53..f1461e7 100644 --- a/packages/commands/tests/e2e/auth.e2e.test.ts +++ b/packages/commands/tests/e2e/auth.e2e.test.ts @@ -15,7 +15,12 @@ import { AUTH_ROUTES } from "./topic-routes.ts"; interface ValidationServer { baseUrl: string; - requests: Array<{ path: string; body: Record }>; + requests: Array<{ + path: string; + body: Record; + authorization?: string; + sourceConfig?: string; + }>; close(): Promise; } @@ -29,6 +34,8 @@ async function startValidationServer(statusCode = 200): Promise) : {}, + 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: "", diff --git a/packages/core/src/auth/store.ts b/packages/core/src/auth/store.ts index 45200e5..c762188 100644 --- a/packages/core/src/auth/store.ts +++ b/packages/core/src/auth/store.ts @@ -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; /** 清凭证:console/openapi 只删对应域;all 清全部登录凭证。返回是否有变更。 */ logout(scope: "console" | "openapi" | "all"): Promise; @@ -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; diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index dc24357..9f51c4e 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -81,9 +81,11 @@ export function readConfigFile(configName?: string): ConfigFile { return parseConfigFile(readRawConfigBlock(raw, configName)); } +/** 写入所选 Profile;登录流程可在同一次原子写入中将显式 Profile 设为激活项。 */ export async function writeConfigFile( data: Record, configName?: string, + options: { activate?: boolean } = {}, ): Promise { 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); } diff --git a/packages/core/tests/config-store.test.ts b/packages/core/tests/config-store.test.ts index 438f12e..8cc12a0 100644 --- a/packages/core/tests/config-store.test.ts +++ b/packages/core/tests/config-store.test.ts @@ -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(); diff --git a/skills/bailian-cli/assets/setup.md b/skills/bailian-cli/assets/setup.md index b75f99e..4869de9 100644 --- a/skills/bailian-cli/assets/setup.md +++ b/skills/bailian-cli/assets/setup.md @@ -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`.