feat(console): resolve gateway URL from region + site, add switchAgent support

Console gateway URL and action are now resolved from a region + site
mapping table instead of a single hardcoded config value. Supports
cn-beijing and ap-southeast-1 with domestic/international site variants.

- Add ConsoleSite type, REGION_GATEWAYS mapping, and resolveGateway()
- Add switchAgent to cornerstoneParam for delegated access
- Add console_site, console_region, console_switch_agent to config
- Remove consoleGatewayUrl from Config (replaced by region+site resolution)
- login-console callback now persists baseUrl, site, region, switchAgent
- bl console call gains --site and --switch-agent flags
- All callers delegate region default to callConsoleGateway (no more hardcoded cn-beijing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lishengzxc
2026-06-14 20:17:46 +08:00
parent 173e5a7e45
commit 067e96689a
19 changed files with 164 additions and 47 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ 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 region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
@@ -213,6 +213,10 @@ function parseApiKeyFromRawBody(raw: string, contentType: string): string | null
interface CallbackCredentials {
accessToken: string | null;
apiKey: string | null;
baseUrl: string | null;
consoleSite: string | null;
consoleRegion: string | null;
consoleSwitchAgent: string | null;
}
async function extractCredentialsFromRequest(
@@ -222,12 +226,27 @@ 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 extras = {
baseUrl: baseUrlFromQuery?.trim() || null,
consoleSite: consoleSiteFromQuery?.trim() || null,
consoleRegion: consoleRegionFromQuery?.trim() || null,
consoleSwitchAgent: consoleSwitchAgentFromQuery?.trim() || null,
};
const m = req.method ?? "GET";
if (m !== "POST" && m !== "PUT" && m !== "PATCH") {
return {
accessToken: accessTokenFromQuery?.trim() || null,
apiKey: apiKeyFromQuery?.trim() || null,
...extras,
};
}
@@ -239,12 +258,13 @@ 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 };
return { accessToken, apiKey, ...extras };
}
function listenServerOnFreeLocalPort(server: http.Server): Promise<number> {
@@ -301,16 +321,40 @@ export async function runConsoleLogin(
return;
}
const { accessToken, apiKey } = await extractCredentialsFromRequest(req);
const { accessToken, apiKey, baseUrl, consoleSite, consoleRegion, consoleSwitchAgent } =
await extractCredentialsFromRequest(req);
if (accessToken || apiKey) {
if (accessToken || apiKey || baseUrl || consoleSite || consoleRegion || consoleSwitchAgent) {
try {
const existing = readConfigFile() as Record<string, unknown>;
let changed = false;
if (accessToken) {
const existing = readConfigFile() as Record<string, unknown>;
existing.access_token = accessToken;
await writeConfigFile(existing);
process.stderr.write(`access_token saved to ${getConfigPath()}\n`);
changed = true;
}
if (baseUrl) {
existing.base_url = baseUrl;
changed = true;
}
if (consoleSite) {
existing.console_site = consoleSite;
changed = true;
}
if (consoleRegion) {
existing.console_region = consoleRegion;
changed = true;
}
if (consoleSwitchAgent) {
existing.console_switch_agent = Number(consoleSwitchAgent);
changed = true;
}
if (changed) {
await writeConfigFile(existing);
process.stderr.write(`Config saved to ${getConfigPath()}\n`);
}
if (apiKey && opts?.onApiKey) {
await opts.onApiKey(apiKey);
}
@@ -329,7 +373,7 @@ export async function runConsoleLogin(
});
res.end("OK\n");
if (accessToken || apiKey) {
if (accessToken || apiKey || baseUrl || consoleSite || consoleRegion || consoleSwitchAgent) {
server.close();
}
} catch {
+15 -2
View File
@@ -7,6 +7,7 @@ import {
detectOutputFormat,
type Config,
type GlobalFlags,
type ConsoleSite,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult } from "../../output/output.ts";
@@ -28,7 +29,15 @@ export default defineCommand({
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
description: "Console region (e.g. cn-beijing, ap-southeast-1)",
},
{
flag: "--site <site>",
description: "Console site: domestic or international",
},
{
flag: "--switch-agent <uid>",
description: "Switch agent UID for delegated access",
},
],
examples: [
@@ -50,7 +59,9 @@ export default defineCommand({
process.exit(1);
}
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const site = ((flags.site as string) || undefined) as ConsoleSite | undefined;
const switchAgent = flags.switchAgent ? Number(flags.switchAgent) : undefined;
const format = detectOutputFormat(config.output);
let token: string | undefined;
@@ -71,6 +82,8 @@ export default defineCommand({
api,
data,
region,
site,
switchAgent,
});
emitResult(result, format);
+1 -1
View File
@@ -43,7 +43,7 @@ export default defineCommand({
const type = (flags.type as string) || "OFFICIAL";
const pageNo = (flags.page as number) || 1;
const pageSize = (flags.pageSize as number) || 30;
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const data = {
+3 -3
View File
@@ -94,7 +94,7 @@ function extractResponseData(result: Record<string, unknown>): Record<string, un
async function fetchAllModelsWithQpm(
config: Config,
token: string,
region: string,
region: string | undefined,
): Promise<ModelWithQpm[]> {
const allModels: ModelWithQpm[] = [];
let pageNo = 1;
@@ -130,7 +130,7 @@ async function fetchAllModelsWithQpm(
async function fetchMonitorData(
config: Config,
token: string,
region: string,
region: string | undefined,
modelName: string,
windowMinutes: number,
): Promise<{ rpm: number; tpm: number }> {
@@ -280,7 +280,7 @@ export default defineCommand({
process.exit(1);
}
const windowMinutes = rawPeriod;
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+1 -1
View File
@@ -130,7 +130,7 @@ export default defineCommand({
const page = Number(flags.page) || 1;
const pageSize = Number(flags.pageSize) || 10;
const modelFilter = (flags.model as string) || undefined;
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+2 -2
View File
@@ -68,7 +68,7 @@ function extractResponseData(result: Record<string, unknown>): Record<string, un
async function fetchAllModelsWithQpm(
config: Config,
token: string,
region: string,
region: string | undefined,
onlySelfService: boolean,
): Promise<ModelWithQpm[]> {
const allModels: ModelWithQpm[] = [];
@@ -186,7 +186,7 @@ export default defineCommand({
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const showAll = Boolean(flags.all);
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+2 -2
View File
@@ -53,7 +53,7 @@ function extractResponseData(result: Record<string, unknown>): Record<string, un
async function fetchModelQpmInfo(
config: Config,
token: string,
region: string,
region: string | undefined,
modelName: string,
): Promise<{ model: string; qpmInfo: Record<string, QpmInfoItem> } | undefined> {
const raw = await callConsoleGateway(config, token, {
@@ -122,7 +122,7 @@ export default defineCommand({
}
const autoConfirm = Boolean(flags.yes) || config.yes;
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+1 -1
View File
@@ -232,7 +232,7 @@ export default defineCommand({
);
process.exit(1);
}
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+2 -2
View File
@@ -63,7 +63,7 @@ async function pollUntilDone(
api: string,
requestKey: string,
models: string[],
region: string,
region: string | undefined,
): Promise<unknown> {
let nextTaskId: string | undefined;
@@ -140,7 +140,7 @@ export default defineCommand({
const modelFlag = (flags.model as string) || undefined;
const all = Boolean(flags.all);
const off = Boolean(flags.off);
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
if (!modelFlag && !all) {
+2 -2
View File
@@ -70,7 +70,7 @@ async function pollTelemetryApi(
token: string,
api: string,
reqDTO: Record<string, unknown>,
region: string,
region: string | undefined,
): Promise<unknown> {
let nextTaskId: string | undefined;
@@ -343,7 +343,7 @@ export default defineCommand({
const modelFlag = (flags.model as string) || undefined;
const daysFlag = Number(flags.days) || 7;
const typeFlag = (flags.type as string) || undefined;
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const format = detectOutputFormat(config.output);
const flagWorkspaceId = (flags.workspaceId as string) || undefined;
+1 -1
View File
@@ -100,7 +100,7 @@ export default defineCommand({
],
examples: ["bl workspace list", "bl workspace list --list 5", "bl workspace list --output json"],
async run(config: Config, flags: GlobalFlags) {
const region = (flags.region as string) || "cn-beijing";
const region = (flags.region as string) || undefined;
const limit = Number(flags.list) || 0;
const format = detectOutputFormat(config.output);
+3 -4
View File
@@ -84,10 +84,9 @@ export function loadConfig(flags: GlobalFlags): Config {
accessKeySecret:
process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET || file.access_key_secret || undefined,
workspaceId: process.env.BAILIAN_WORKSPACE_ID || file.workspace_id || undefined,
consoleGatewayUrl:
process.env.BAILIAN_CONSOLE_GATEWAY_URL ||
file.console_gateway_url ||
"https://bailian-cs.console.aliyun.com",
consoleSite: file.console_site || undefined,
consoleRegion: file.console_region || undefined,
consoleSwitchAgent: file.console_switch_agent || undefined,
verbose: flags.verbose || process.env.DASHSCOPE_VERBOSE === "1",
quiet: flags.quiet || false,
noColor: flags.noColor || process.env.NO_COLOR !== undefined || !process.stdout.isTTY,
+13 -1
View File
@@ -32,11 +32,15 @@ export interface ConfigFile {
access_key_secret?: string;
workspace_id?: string;
console_gateway_url?: string;
console_site?: "domestic" | "international";
console_region?: string;
console_switch_agent?: number;
telemetry?: boolean;
}
const VALID_REGIONS = new Set<string>(["cn", "us", "intl"]);
const VALID_OUTPUTS = new Set<string>(["text", "json"]);
const VALID_CONSOLE_SITES = new Set<string>(["domestic", "international"]);
/**
* A syntactically valid absolute http(s) URL. Used to validate `base_url` and
@@ -89,6 +93,12 @@ export function parseConfigFile(raw: unknown): ConfigFile {
out.workspace_id = obj.workspace_id;
if (typeof obj.console_gateway_url === "string" && isHttpUrl(obj.console_gateway_url))
out.console_gateway_url = obj.console_gateway_url;
if (typeof obj.console_site === "string" && VALID_CONSOLE_SITES.has(obj.console_site))
out.console_site = obj.console_site as ConfigFile["console_site"];
if (typeof obj.console_region === "string" && obj.console_region.length > 0)
out.console_region = obj.console_region;
if (typeof obj.console_switch_agent === "number" && obj.console_switch_agent > 0)
out.console_switch_agent = obj.console_switch_agent;
if (typeof obj.telemetry === "boolean") out.telemetry = obj.telemetry;
return out;
@@ -118,7 +128,9 @@ export interface Config {
accessKeyId?: string;
accessKeySecret?: string;
workspaceId?: string;
consoleGatewayUrl: string;
consoleSite?: "domestic" | "international";
consoleRegion?: string;
consoleSwitchAgent?: number;
verbose: boolean;
quiet: boolean;
noColor: boolean;
+57 -9
View File
@@ -2,18 +2,56 @@ import type { Config } from "../config/schema.ts";
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
const GATEWAY_ACTION = "BroadScopeAspnGateway";
const GATEWAY_PRODUCT = "sfm_bailian";
export type ConsoleSite = "domestic" | "international";
interface ConsoleGatewayInfo {
csGateway: string;
action: string;
}
const REGION_GATEWAYS: Record<string, Record<ConsoleSite, ConsoleGatewayInfo>> = {
"cn-beijing": {
domestic: { csGateway: "bailian-cs.console.aliyun.com", action: "BroadScopeAspnGateway" },
international: {
csGateway: "bailian-cs.console.alibabacloud.com",
action: "BroadScopeAspnGateway",
},
},
"ap-southeast-1": {
domestic: {
csGateway: "modelstudio-cs.console.aliyun.com",
action: "IntlBroadScopeAspnGateway",
},
international: {
csGateway: "bailian-singapore-cs.alibabacloud.com",
action: "IntlBroadScopeAspnGateway",
},
},
};
function resolveGateway(region: string, site: ConsoleSite): ConsoleGatewayInfo {
return REGION_GATEWAYS[region]?.[site] ?? REGION_GATEWAYS["cn-beijing"]![site];
}
export interface ConsoleGatewayRequest {
/** Console API name, e.g. zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota */
api: string;
data: Record<string, unknown>;
/** Console region (default: cn-beijing), distinct from DashScope `config.region`. */
/** Console region (e.g. cn-beijing, ap-southeast-1). Falls back to config.consoleRegion, then "cn-beijing". */
region?: string;
/** Console site. Falls back to config.consoleSite, then "domestic". */
site?: ConsoleSite;
/** Switch-agent UID for delegated access. Falls back to config.consoleSwitchAgent. */
switchAgent?: number;
}
function buildGatewayParams(api: string, data: Record<string, unknown>): string {
function buildGatewayParams(
api: string,
data: Record<string, unknown>,
switchAgent?: number,
): string {
return JSON.stringify({
Api: api,
V: "1.0",
@@ -24,6 +62,7 @@ function buildGatewayParams(api: string, data: Record<string, unknown>): string
console: "ONE_CONSOLE",
productCode: "p_efm",
consoleSite: "BAILIAN_ALIYUN",
...(switchAgent != null ? { switchAgent } : {}),
...(typeof data.cornerstoneParam === "object" && data.cornerstoneParam !== null
? (data.cornerstoneParam as Record<string, unknown>)
: {}),
@@ -37,17 +76,26 @@ function buildGatewayParams(api: string, data: Record<string, unknown>): string
* `token` is the console `access_token` (from `bl auth login --console`); when
* omitted the request is sent without an Authorization header, which works for
* public console APIs that don't require a login session.
*
* Gateway URL and action are resolved from `region + site` via {@link REGION_GATEWAYS}.
* Each parameter falls back to the corresponding config value, then to a hardcoded default.
*/
export async function callConsoleGateway(
config: Config,
token: string | undefined,
{ api, data, region = "cn-beijing" }: ConsoleGatewayRequest,
{ api, data, region, site, switchAgent }: ConsoleGatewayRequest,
): Promise<unknown> {
const params = buildGatewayParams(api, data);
const body = new URLSearchParams({ params, region });
const timeoutMs = config.timeout * 1000;
const effectiveRegion = region ?? config.consoleRegion ?? "cn-beijing";
const effectiveSite = site ?? config.consoleSite ?? "domestic";
const effectiveSwitchAgent = switchAgent ?? config.consoleSwitchAgent;
const gatewayBase = config.consoleGatewayUrl;
const resolved = resolveGateway(effectiveRegion, effectiveSite);
const gatewayBase = `https://${resolved.csGateway}`;
const action = resolved.action;
const params = buildGatewayParams(api, data, effectiveSwitchAgent);
const body = new URLSearchParams({ params, region: effectiveRegion });
const timeoutMs = config.timeout * 1000;
const headers: Record<string, string> = {
Accept: "*/*",
@@ -56,7 +104,7 @@ export async function callConsoleGateway(
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(
`${gatewayBase}/cli/api.json?action=${GATEWAY_ACTION}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`,
`${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`,
{
method: "POST",
headers,
+1 -1
View File
@@ -1,4 +1,4 @@
export type { ConsoleGatewayRequest } from "./gateway.ts";
export type { ConsoleGatewayRequest, ConsoleSite } from "./gateway.ts";
export { callConsoleGateway } from "./gateway.ts";
export type { ModelListParams, ModelListResult } from "./models.ts";
export { fetchModelList } from "./models.ts";
+1 -1
View File
@@ -28,7 +28,7 @@ export async function fetchModelList(
name = "",
providers = [],
capabilities = [],
region = "cn-beijing",
region,
} = params;
const result = (await callConsoleGateway(config, token, {
-1
View File
@@ -22,7 +22,6 @@ function testConfig(overrides: Partial<Config> = {}): Config {
nonInteractive: true,
async: false,
telemetry: true,
consoleGatewayUrl: "https://bailian-cs.console.aliyun.com",
...overrides,
};
}
+7 -5
View File
@@ -23,11 +23,13 @@ Index: [index.md](index.md)
#### Options
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------ |
| `--api <api>` | string | yes | API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries) |
| `--data <json>` | string | yes | Request data as JSON string |
| `--region <region>` | string | no | API region (default: cn-beijing) |
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | ------------------------------------------------------------------------ |
| `--api <api>` | string | yes | API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries) |
| `--data <json>` | string | yes | Request data as JSON string |
| `--region <region>` | string | no | Console region (e.g. cn-beijing, ap-southeast-1) |
| `--site <site>` | string | no | Console site: domestic or international |
| `--switch-agent <uid>` | string | no | Switch agent UID for delegated access |
#### Examples