Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
067e966
feat(console): resolve gateway URL from region + site, add switchAgen…
lishengzxc Jun 14, 2026
e68abb6
refactor(auth): simplify login-console config persistence logic
lishengzxc Jun 14, 2026
51d9833
fix(auth): parse baseUrl/consoleSite/consoleRegion/consoleSwitchAgent…
lishengzxc Jun 14, 2026
2182a22
refactor(auth): unify console callback persistence — validate apiKey …
lishengzxc Jun 14, 2026
6c716e5
feat(auth): add --base-url flag to bl auth login
lishengzxc Jun 14, 2026
36a60a2
chore(pnpm): 移除 vite 和 vitest 的 overrides 配置
lishengzxc Jun 14, 2026
270412d
refactor(auth): deduplicate canRetry/validateKey between login.ts and…
lishengzxc Jun 14, 2026
f45b19c
chore: remove console_gateway_url remnants from schema and tests
lishengzxc Jun 14, 2026
7a65fb8
feat(auth): parse and persist workspace_id from console login callback
lishengzxc Jun 14, 2026
4749b49
feat(auth): 更新默认控制台登录页为中国站地址
lishengzxc Jun 15, 2026
e5abd1b
feat(auth): 更新默认控制台登录页为正式中国站地址
lishengzxc Jun 15, 2026
ce64d62
feat: The command "config show" does not display the "region" field, …
qcq01083097 Jun 16, 2026
2bcbf56
feat: Fix lint errors in mcp.ts
qcq01083097 Jun 16, 2026
8c398ba
refactor(console): promote --console-region, --console-site, --consol…
lishengzxc Jun 16, 2026
9a5797d
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
lishengzxc Jun 16, 2026
f847476
refactor(auth): 修改 --console 标志以简化登录命令
lishengzxc Jun 16, 2026
6823212
feat: Delete invalid code
qcq01083097 Jun 16, 2026
631a9c1
feat: Complete the missing changes for E2E Test
qcq01083097 Jun 16, 2026
a5d055f
feat: Adjust the priority of base_url in config to be higher than tha…
qcq01083097 Jun 16, 2026
d320d36
docs: add console gateway flags convention to AGENTS.md and command help
lishengzxc Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的

不要扮演服务端错误的翻译官——我们没有最新的错误码体系认知,二次包装只会撒谎(详见 `docs/agents/error-hint-change.md` 中的反面 case)。

### 4. Console Gateway 命令必须声明 console 全局 flags

如果新命令使用了 `callConsoleGateway`,必须在 `options` 中添加以下三个全局 flag 的说明,以便 `--help` 中展示:

```ts
{ flag: "--console-region <region>", description: "Console region" },
{ flag: "--console-site <site>", description: "Console site: domestic, international" },
{ flag: "--console-switch-agent <uid>", description: "Switch agent UID", type: "number" },
```

这些 flag 已在 `GLOBAL_OPTIONS`(`packages/core/src/types/command.ts`)中注册,由 `loadConfig` 写入 `config.consoleRegion` / `config.consoleSite` / `config.consoleSwitchAgent`,`callConsoleGateway` 自动读取——命令无需手动提取或传递。

## 完成改动后的快速验证

```sh
Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ export default defineCommand({
description: "Results per page (default: 30)",
type: "number",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
flag: "--console-site <site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
examples: [
Expand All @@ -44,7 +50,6 @@ export default defineCommand({
const name = (flags.name as string) || "";
const pageNo = (flags.page as number) || 1;
const pageSize = (flags.pageSize as number) || 30;
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);

const credential = await resolveConsoleGatewayCredential(config);
Expand All @@ -61,17 +66,13 @@ export default defineCommand({
};

if (config.dryRun) {
emitResult(
{ api: APP_LIST_API, data, region, token: credential.token.slice(0, 8) + "..." },
format,
);
emitResult({ api: APP_LIST_API, data, token: credential.token.slice(0, 8) + "..." }, format);
return;
}

const result = (await callConsoleGateway(config, credential.token, {
api: APP_LIST_API,
data,
region,
})) as any;

const list: unknown[] = result?.data?.DataV2?.data?.data?.list ?? [];
Expand Down
206 changes: 193 additions & 13 deletions packages/cli/src/commands/auth/login-console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import http from "node:http";
import {
BailianError,
ExitCode,
chatEndpoint,
getConfigPath,
readConfigFile,
requestJson,
writeConfigFile,
type Config,
} from "bailian-cli-core";

const CONSOLE_LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
const MAX_AUTH_CALLBACK_BODY = 65536;

const DEFAULT_CONSOLE_ORIGIN = "https://bailian.console.aliyun.com";
const CONSOLE_ORIGINS: Record<string, string> = {
domestic: "https://bailian.console.aliyun.com",
international: "https://modelstudio.console.alibabacloud.com",
};

export function resolveConsoleOrigin(): string {
return process.env.BAILIAN_CONSOLE_ORIGIN || DEFAULT_CONSOLE_ORIGIN;
export function resolveConsoleOrigin(site?: string): string {
return (site && CONSOLE_ORIGINS[site]) || CONSOLE_ORIGINS.domestic!;
}

function readBodyBounded(req: http.IncomingMessage): Promise<string> {
Expand Down Expand Up @@ -210,9 +216,76 @@ function parseApiKeyFromRawBody(raw: string, contentType: string): string | null
return null;
}

type CallbackExtras = Pick<
CallbackCredentials,
"baseUrl" | "consoleSite" | "consoleRegion" | "consoleSwitchAgent" | "workspaceId"
>;

function stringField(o: Record<string, unknown>, ...keys: string[]): string | null {
for (const k of keys) {
const v = o[k];
if (typeof v === "string" && v.trim()) return v.trim();
}
return null;
}

function parseExtrasFromRawBody(raw: string, contentType: string): CallbackExtras {
const empty: CallbackExtras = {
baseUrl: null,
consoleSite: null,
consoleRegion: null,
consoleSwitchAgent: null,
workspaceId: null,
};
if (!raw.trim()) return empty;

let obj: Record<string, unknown> | null = null;

const ct = contentType.toLowerCase();
if (ct.includes("application/json") || ct.includes("text/json")) {
try {
const parsed = JSON.parse(raw.trim());
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) obj = parsed;
} catch {
/* */
}
}
if (!obj && ct.includes("application/x-www-form-urlencoded")) {
try {
const params = new URLSearchParams(raw.trim());
obj = Object.fromEntries(params);
} catch {
/* */
}
}
if (!obj) {
try {
const parsed = JSON.parse(raw.trim());
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) obj = parsed;
} catch {
/* */
}
}

if (!obj) return empty;

return {
baseUrl: stringField(obj, "base_url", "baseUrl"),
consoleSite: stringField(obj, "console_site", "consoleSite"),
consoleRegion: stringField(obj, "console_region", "consoleRegion"),
consoleSwitchAgent: stringField(obj, "console_switch_agent", "consoleSwitchAgent"),
workspaceId: stringField(obj, "workspace_id", "workspaceId"),
};
}

interface CallbackCredentials {
accessToken: string | null;
apiKey: string | null;
baseUrl: string | null;
consoleSite: string | null;
consoleRegion: string | null;
consoleSwitchAgent: string | null;
workspaceId: string | null;
}

async function extractCredentialsFromRequest(
Expand All @@ -222,12 +295,30 @@ async function extractCredentialsFromRequest(
const accessTokenFromQuery =
u.searchParams.get("access_token") ?? u.searchParams.get("accessToken");
const apiKeyFromQuery = u.searchParams.get("api_key") ?? u.searchParams.get("apiKey");
const baseUrlFromQuery = u.searchParams.get("base_url") ?? u.searchParams.get("baseUrl");
const consoleSiteFromQuery =
u.searchParams.get("console_site") ?? u.searchParams.get("consoleSite");
const consoleRegionFromQuery =
u.searchParams.get("console_region") ?? u.searchParams.get("consoleRegion");
const consoleSwitchAgentFromQuery =
u.searchParams.get("console_switch_agent") ?? u.searchParams.get("consoleSwitchAgent");
const workspaceIdFromQuery =
u.searchParams.get("workspace_id") ?? u.searchParams.get("workspaceId");

const extras = {
baseUrl: baseUrlFromQuery?.trim() || null,
consoleSite: consoleSiteFromQuery?.trim() || null,
consoleRegion: consoleRegionFromQuery?.trim() || null,
consoleSwitchAgent: consoleSwitchAgentFromQuery?.trim() || null,
workspaceId: workspaceIdFromQuery?.trim() || null,
};

const m = req.method ?? "GET";
if (m !== "POST" && m !== "PUT" && m !== "PATCH") {
return {
accessToken: accessTokenFromQuery?.trim() || null,
apiKey: apiKeyFromQuery?.trim() || null,
...extras,
};
}

Expand All @@ -239,12 +330,24 @@ async function extractCredentialsFromRequest(
return {
accessToken: accessTokenFromQuery?.trim() || null,
apiKey: apiKeyFromQuery?.trim() || null,
...extras,
};
}

const accessToken = accessTokenFromQuery?.trim() || parseAccessTokenFromRawBody(raw, contentType);
const apiKey = apiKeyFromQuery?.trim() || parseApiKeyFromRawBody(raw, contentType);
return { accessToken, apiKey };

const bodyExtras = parseExtrasFromRawBody(raw, contentType);

return {
accessToken,
apiKey,
baseUrl: extras.baseUrl || bodyExtras.baseUrl,
consoleSite: extras.consoleSite || bodyExtras.consoleSite,
consoleRegion: extras.consoleRegion || bodyExtras.consoleRegion,
consoleSwitchAgent: extras.consoleSwitchAgent || bodyExtras.consoleSwitchAgent,
workspaceId: extras.workspaceId || bodyExtras.workspaceId,
};
}

function listenServerOnFreeLocalPort(server: http.Server): Promise<number> {
Expand Down Expand Up @@ -276,9 +379,69 @@ function openInBrowser(url: string): Promise<void> {
});
}

const RETRY_DELAY_BASE_MS = 500;

function canRetry(err: unknown): boolean {
if (err instanceof BailianError) {
if (err.exitCode === ExitCode.NETWORK || err.exitCode === ExitCode.TIMEOUT) return true;
const status = err.api?.httpStatus;
return status === 401 || (status !== undefined && status >= 500);
}
if (err instanceof Error) {
return (
err.name === "AbortError" ||
err.name === "TimeoutError" ||
err.message.includes("timed out") ||
err.message === "fetch failed"
);
}
return false;
}

export async function validateAndPersistApiKey(
config: Config,
key: string,
baseUrl: string,
): Promise<void> {
process.stderr.write("Testing key... ");
const testConfig = { ...config, apiKey: key, baseUrl };
const requestOpts = {
url: chatEndpoint(testConfig.baseUrl),
method: "POST",
timeout: Math.min(config.timeout, 30),
body: {
model: "qwen3.7-max",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
},
};

for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(testConfig, requestOpts);
break;
} catch (err) {
if (attempt >= 3 || !canRetry(err)) {
process.stderr.write("Failed\n");
throw new BailianError("API key validation failed", ExitCode.AUTH, "Invalid API key.", {
cause: err,
});
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}

process.stderr.write("Valid\n");
const existing = readConfigFile() as Record<string, unknown>;
existing.api_key = key;
await writeConfigFile(existing);
}

export async function runConsoleLogin(
consoleOrigin: string,
opts?: { needApiKey?: boolean; onApiKey?: (key: string) => Promise<void> },
config: Config,
opts?: { needApiKey?: boolean },
): Promise<void> {
const state = randomBytes(16).toString("hex");
let callbackError: unknown;
Expand All @@ -301,18 +464,35 @@ export async function runConsoleLogin(
return;
}

const { accessToken, apiKey } = await extractCredentialsFromRequest(req);
const {
accessToken,
apiKey,
baseUrl,
consoleSite,
consoleRegion,
consoleSwitchAgent,
workspaceId,
} = await extractCredentialsFromRequest(req);

const hasConfig =
accessToken || baseUrl || consoleSite || consoleRegion || consoleSwitchAgent || workspaceId;

if (accessToken || apiKey) {
if (hasConfig || apiKey) {
try {
if (accessToken) {
if (hasConfig) {
const existing = readConfigFile() as Record<string, unknown>;
existing.access_token = accessToken;
if (accessToken) existing.access_token = accessToken;
if (baseUrl) existing.base_url = baseUrl;
if (consoleSite) existing.console_site = consoleSite;
if (consoleRegion) existing.console_region = consoleRegion;
if (consoleSwitchAgent) existing.console_switch_agent = Number(consoleSwitchAgent);
if (workspaceId) existing.workspace_id = workspaceId;
await writeConfigFile(existing);
process.stderr.write(`access_token saved to ${getConfigPath()}\n`);
process.stderr.write(`Config saved to ${getConfigPath()}\n`);
}
if (apiKey && opts?.onApiKey) {
await opts.onApiKey(apiKey);
if (apiKey) {
const testBaseUrl = baseUrl || config.baseUrl;
await validateAndPersistApiKey(config, apiKey, testBaseUrl);
}
} catch (err: unknown) {
callbackError = err;
Expand All @@ -329,7 +509,7 @@ export async function runConsoleLogin(
});
res.end("OK\n");

if (accessToken || apiKey) {
if (hasConfig || apiKey) {
server.close();
}
} catch {
Expand Down
Loading
Loading