Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(console): resolve gateway URL from region + site, add switchAgent by lishengzxc · Pull Request #56 · modelstudioai/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
d24b41d
feat: Remove the logic related to region
qcq01083097 Jun 16, 2026
685c017
feat: clean up region remnants
qcq01083097 Jun 16, 2026
a5d078b
Merge branch 'feat/console-gateway-region-site' of github.com:modelst…
qcq01083097 Jun 16, 2026
60cad10
chore: remove redundant "(global flag)" from option descriptions
lishengzxc Jun 16, 2026
e9feb38
Merge branch 'main' of github.com:modelstudioai/cli into feat/console…
lishengzxc Jun 17, 2026
14583cf
feat: update access token logic
gujieye Jun 17, 2026
d4c0809
feat: resolve conflict
qcq01083097 Jun 17, 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```

若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、region 等)。全局 region:`--region cn|us|intl`,默认 `cn`。
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,7 +172,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# Set defaults
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.zh.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,7 +167,7 @@ export BAILIAN_WORKSPACE_ID=ws-...
bl config show

# 设置默认值
bl config set --key region --value us
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/app/list.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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