Skip to content

Commit bd8b08b

Browse files
committed
feat: cancel validate API key
1 parent e9479e4 commit bd8b08b

9 files changed

Lines changed: 216 additions & 379 deletions

File tree

docs/agents/auth-change.md

Lines changed: 3 additions & 3 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` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 验证成功后`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
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`
@@ -47,9 +47,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
4747
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
4848
- `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
4949
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
50-
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
50+
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
5151
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
52-
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
52+
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile;未传
5353
`--config` 时继续写当前激活项,失败和 dry-run 不切换
5454
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
5555
- `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_*`,新写入只写短字段

docs/agents/config-profile-change.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@
1818
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`
1919
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
2020
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
21-
`auth login --config ...`凭证验证并落盘成功后自动激活该 Profile。
21+
`auth login --config ...`凭证落盘成功后自动激活该 Profile。
2222
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
2323
- 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 解析。
2424
- 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 和失败登录均不修改该白名单。
25+
- 对命中内置 preset 的 Profile,API Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
2626
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get``video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
2727
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
2828
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
2929

3030
## 3. 保持读写命令交互一致
3131

3232
- `auth login``config set` 等写命令未传 `--config` 时修改当前激活项。
33-
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
33+
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
3434
创建和激活;`config set --config <name>` 可创建但不自动激活。
3535
- `config show``auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
3636
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。

packages/commands/src/commands/auth/login-api-key.ts

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
import {
2-
BailianError,
3-
ExitCode,
4-
chatPath,
5-
requestJson,
6-
normalizeModelBaseUrl,
7-
type AuthPersistPatch,
8-
type AuthStore,
9-
type Identity,
10-
type Settings,
11-
} from "bailian-cli-core";
1+
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
122

133
interface ApiKeyLoginDeps {
14-
identity: Identity;
15-
settings: Settings;
164
authStore: AuthStore;
175
}
186

197
interface ApiKeyLoginProfile {
20-
baseUrl: string;
218
persistBaseUrl?: string;
229
defaultTextModel?: string;
2310
defaultVideoModel?: string;
@@ -30,65 +17,19 @@ interface ApiKeyLoginProfile {
3017
persistPatch?: AuthPersistPatch;
3118
}
3219

33-
const RETRY_DELAY_BASE_MS = 500;
34-
35-
function canRetry(error: unknown): boolean {
36-
if (error instanceof BailianError) {
37-
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
38-
const status = error.api?.httpStatus;
39-
return status === 401 || (status !== undefined && status >= 500);
40-
}
41-
if (error instanceof Error) {
42-
return (
43-
error.name === "AbortError" ||
44-
error.name === "TimeoutError" ||
45-
error.message.includes("timed out") ||
46-
error.message === "fetch failed"
47-
);
48-
}
49-
return false;
50-
}
51-
52-
export async function validateAndPersistApiKey(
20+
/**
21+
* Persist an API key (and optional profile defaults) without a live model probe.
22+
* Login is credential storage; connectivity is verified on the first API command.
23+
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
24+
*/
25+
export async function persistApiKey(
5326
deps: ApiKeyLoginDeps,
5427
key: string,
5528
profile: ApiKeyLoginProfile,
5629
): Promise<void> {
57-
process.stderr.write("Testing key... ");
58-
const httpDeps = { identity: deps.identity, settings: deps.settings };
59-
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
6030
const persistBaseUrl = profile.persistBaseUrl
6131
? normalizeModelBaseUrl(profile.persistBaseUrl)
6232
: undefined;
63-
const validationModel = "qwen3.8-max";
64-
const requestOpts = {
65-
url: baseUrl + chatPath(),
66-
method: "POST",
67-
headers: { Authorization: `Bearer ${key}` },
68-
timeout: Math.min(deps.settings.timeout, 30),
69-
body: {
70-
model: validationModel,
71-
messages: [{ role: "user", content: "hi" }],
72-
max_tokens: 1,
73-
stream: false,
74-
},
75-
};
76-
77-
for (let attempt = 1; attempt <= 3; attempt++) {
78-
try {
79-
await requestJson<unknown>(httpDeps, requestOpts);
80-
break;
81-
} catch (error) {
82-
if (attempt >= 3 || !canRetry(error)) {
83-
process.stderr.write("Failed\n");
84-
throw error;
85-
}
86-
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
87-
await new Promise((resolve) => setTimeout(resolve, delayMs));
88-
}
89-
}
90-
91-
process.stderr.write("Valid\n");
9233
await deps.authStore.login({
9334
...profile.persistPatch,
9435
api_key: key,

packages/commands/src/commands/auth/login-console.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type Settings,
1212
} from "bailian-cli-core";
1313
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
14-
import { validateAndPersistApiKey } from "./login-api-key.ts";
14+
import { persistApiKey } from "./login-api-key.ts";
1515

1616
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
1717
export interface LoginDeps {
@@ -413,9 +413,7 @@ export async function runConsoleLogin(
413413
workspace_id: workspaceId || undefined,
414414
};
415415
if (apiKey) {
416-
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
417-
await validateAndPersistApiKey(deps, apiKey, {
418-
baseUrl: testBaseUrl,
416+
await persistApiKey(deps, apiKey, {
419417
persistBaseUrl: baseUrl || undefined,
420418
persistPatch: callbackPatch,
421419
});

packages/commands/src/commands/auth/login.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
normalizeModelBaseUrl,
66
} from "bailian-cli-core";
77
import { emitBare } from "bailian-cli-runtime";
8-
import { validateAndPersistApiKey } from "./login-api-key.ts";
8+
import { persistApiKey } from "./login-api-key.ts";
99
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
1010

1111
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
@@ -33,8 +33,8 @@ export default defineCommand({
3333
type: "string",
3434
valueHint: "<url>",
3535
description: {
36-
"en-US": "Model API base URL (used with --api-key for validation)",
37-
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
36+
"en-US": "Model API base URL to store with --api-key",
37+
"zh-CN": "--api-key 一并保存的模型 API Base URL",
3838
},
3939
},
4040
console: {
@@ -164,19 +164,17 @@ export default defineCommand({
164164
if (!key) return;
165165

166166
if (settings.dryRun) {
167-
emitBare("Would validate and save API key.");
167+
emitBare("Would save API key.");
168168
return;
169169
}
170170
const profilePreset = getModelProfilePreset(settings.configName);
171171
const stored = store.stored();
172172
const storedBaseUrl = stored.baseUrl;
173-
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
174173
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
175174
const apiKeyCapabilities = profilePreset
176175
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
177176
: stored.apiKeyCapabilities;
178-
await validateAndPersistApiKey(deps, key, {
179-
baseUrl: resolvedBaseUrl,
177+
await persistApiKey(deps, key, {
180178
persistBaseUrl,
181179
defaultTextModel: profilePreset?.defaultTextModel,
182180
defaultVideoModel: profilePreset?.defaultVideoModel,
@@ -187,5 +185,6 @@ export default defineCommand({
187185
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
188186
apiKeyCapabilities,
189187
});
188+
process.stderr.write(`API key saved to ${store.path}\n`);
190189
},
191190
});

0 commit comments

Comments
 (0)