@@ -5,18 +5,24 @@ import http from "node:http";
55import {
66 BailianError ,
77 ExitCode ,
8+ chatEndpoint ,
89 getConfigPath ,
910 readConfigFile ,
11+ requestJson ,
1012 writeConfigFile ,
13+ type Config ,
1114} from "bailian-cli-core" ;
1215
1316const CONSOLE_LOGIN_TIMEOUT_MS = 15 * 60 * 1000 ;
1417const MAX_AUTH_CALLBACK_BODY = 65536 ;
1518
16- const DEFAULT_CONSOLE_ORIGIN = "https://bailian.console.aliyun.com" ;
19+ const CONSOLE_ORIGINS : Record < string , string > = {
20+ domestic : "https://bailian.console.aliyun.com" ,
21+ international : "https://modelstudio.console.alibabacloud.com" ,
22+ } ;
1723
18- export function resolveConsoleOrigin ( ) : string {
19- return process . env . BAILIAN_CONSOLE_ORIGIN || DEFAULT_CONSOLE_ORIGIN ;
24+ export function resolveConsoleOrigin ( site ?: string ) : string {
25+ return ( site && CONSOLE_ORIGINS [ site ] ) || CONSOLE_ORIGINS . domestic ! ;
2026}
2127
2228function readBodyBounded ( req : http . IncomingMessage ) : Promise < string > {
@@ -210,9 +216,76 @@ function parseApiKeyFromRawBody(raw: string, contentType: string): string | null
210216 return null ;
211217}
212218
219+ type CallbackExtras = Pick <
220+ CallbackCredentials ,
221+ "baseUrl" | "consoleSite" | "consoleRegion" | "consoleSwitchAgent" | "workspaceId"
222+ > ;
223+
224+ function stringField ( o : Record < string , unknown > , ...keys : string [ ] ) : string | null {
225+ for ( const k of keys ) {
226+ const v = o [ k ] ;
227+ if ( typeof v === "string" && v . trim ( ) ) return v . trim ( ) ;
228+ }
229+ return null ;
230+ }
231+
232+ function parseExtrasFromRawBody ( raw : string , contentType : string ) : CallbackExtras {
233+ const empty : CallbackExtras = {
234+ baseUrl : null ,
235+ consoleSite : null ,
236+ consoleRegion : null ,
237+ consoleSwitchAgent : null ,
238+ workspaceId : null ,
239+ } ;
240+ if ( ! raw . trim ( ) ) return empty ;
241+
242+ let obj : Record < string , unknown > | null = null ;
243+
244+ const ct = contentType . toLowerCase ( ) ;
245+ if ( ct . includes ( "application/json" ) || ct . includes ( "text/json" ) ) {
246+ try {
247+ const parsed = JSON . parse ( raw . trim ( ) ) ;
248+ if ( parsed && typeof parsed === "object" && ! Array . isArray ( parsed ) ) obj = parsed ;
249+ } catch {
250+ /* */
251+ }
252+ }
253+ if ( ! obj && ct . includes ( "application/x-www-form-urlencoded" ) ) {
254+ try {
255+ const params = new URLSearchParams ( raw . trim ( ) ) ;
256+ obj = Object . fromEntries ( params ) ;
257+ } catch {
258+ /* */
259+ }
260+ }
261+ if ( ! obj ) {
262+ try {
263+ const parsed = JSON . parse ( raw . trim ( ) ) ;
264+ if ( parsed && typeof parsed === "object" && ! Array . isArray ( parsed ) ) obj = parsed ;
265+ } catch {
266+ /* */
267+ }
268+ }
269+
270+ if ( ! obj ) return empty ;
271+
272+ return {
273+ baseUrl : stringField ( obj , "base_url" , "baseUrl" ) ,
274+ consoleSite : stringField ( obj , "console_site" , "consoleSite" ) ,
275+ consoleRegion : stringField ( obj , "console_region" , "consoleRegion" ) ,
276+ consoleSwitchAgent : stringField ( obj , "console_switch_agent" , "consoleSwitchAgent" ) ,
277+ workspaceId : stringField ( obj , "workspace_id" , "workspaceId" ) ,
278+ } ;
279+ }
280+
213281interface CallbackCredentials {
214282 accessToken : string | null ;
215283 apiKey : string | null ;
284+ baseUrl : string | null ;
285+ consoleSite : string | null ;
286+ consoleRegion : string | null ;
287+ consoleSwitchAgent : string | null ;
288+ workspaceId : string | null ;
216289}
217290
218291async function extractCredentialsFromRequest (
@@ -222,12 +295,30 @@ async function extractCredentialsFromRequest(
222295 const accessTokenFromQuery =
223296 u . searchParams . get ( "access_token" ) ?? u . searchParams . get ( "accessToken" ) ;
224297 const apiKeyFromQuery = u . searchParams . get ( "api_key" ) ?? u . searchParams . get ( "apiKey" ) ;
298+ const baseUrlFromQuery = u . searchParams . get ( "base_url" ) ?? u . searchParams . get ( "baseUrl" ) ;
299+ const consoleSiteFromQuery =
300+ u . searchParams . get ( "console_site" ) ?? u . searchParams . get ( "consoleSite" ) ;
301+ const consoleRegionFromQuery =
302+ u . searchParams . get ( "console_region" ) ?? u . searchParams . get ( "consoleRegion" ) ;
303+ const consoleSwitchAgentFromQuery =
304+ u . searchParams . get ( "console_switch_agent" ) ?? u . searchParams . get ( "consoleSwitchAgent" ) ;
305+ const workspaceIdFromQuery =
306+ u . searchParams . get ( "workspace_id" ) ?? u . searchParams . get ( "workspaceId" ) ;
307+
308+ const extras = {
309+ baseUrl : baseUrlFromQuery ?. trim ( ) || null ,
310+ consoleSite : consoleSiteFromQuery ?. trim ( ) || null ,
311+ consoleRegion : consoleRegionFromQuery ?. trim ( ) || null ,
312+ consoleSwitchAgent : consoleSwitchAgentFromQuery ?. trim ( ) || null ,
313+ workspaceId : workspaceIdFromQuery ?. trim ( ) || null ,
314+ } ;
225315
226316 const m = req . method ?? "GET" ;
227317 if ( m !== "POST" && m !== "PUT" && m !== "PATCH" ) {
228318 return {
229319 accessToken : accessTokenFromQuery ?. trim ( ) || null ,
230320 apiKey : apiKeyFromQuery ?. trim ( ) || null ,
321+ ...extras ,
231322 } ;
232323 }
233324
@@ -239,12 +330,24 @@ async function extractCredentialsFromRequest(
239330 return {
240331 accessToken : accessTokenFromQuery ?. trim ( ) || null ,
241332 apiKey : apiKeyFromQuery ?. trim ( ) || null ,
333+ ...extras ,
242334 } ;
243335 }
244336
245337 const accessToken = accessTokenFromQuery ?. trim ( ) || parseAccessTokenFromRawBody ( raw , contentType ) ;
246338 const apiKey = apiKeyFromQuery ?. trim ( ) || parseApiKeyFromRawBody ( raw , contentType ) ;
247- return { accessToken, apiKey } ;
339+
340+ const bodyExtras = parseExtrasFromRawBody ( raw , contentType ) ;
341+
342+ return {
343+ accessToken,
344+ apiKey,
345+ baseUrl : extras . baseUrl || bodyExtras . baseUrl ,
346+ consoleSite : extras . consoleSite || bodyExtras . consoleSite ,
347+ consoleRegion : extras . consoleRegion || bodyExtras . consoleRegion ,
348+ consoleSwitchAgent : extras . consoleSwitchAgent || bodyExtras . consoleSwitchAgent ,
349+ workspaceId : extras . workspaceId || bodyExtras . workspaceId ,
350+ } ;
248351}
249352
250353function listenServerOnFreeLocalPort ( server : http . Server ) : Promise < number > {
@@ -276,9 +379,69 @@ function openInBrowser(url: string): Promise<void> {
276379 } ) ;
277380}
278381
382+ const RETRY_DELAY_BASE_MS = 500 ;
383+
384+ function canRetry ( err : unknown ) : boolean {
385+ if ( err instanceof BailianError ) {
386+ if ( err . exitCode === ExitCode . NETWORK || err . exitCode === ExitCode . TIMEOUT ) return true ;
387+ const status = err . api ?. httpStatus ;
388+ return status === 401 || ( status !== undefined && status >= 500 ) ;
389+ }
390+ if ( err instanceof Error ) {
391+ return (
392+ err . name === "AbortError" ||
393+ err . name === "TimeoutError" ||
394+ err . message . includes ( "timed out" ) ||
395+ err . message === "fetch failed"
396+ ) ;
397+ }
398+ return false ;
399+ }
400+
401+ export async function validateAndPersistApiKey (
402+ config : Config ,
403+ key : string ,
404+ baseUrl : string ,
405+ ) : Promise < void > {
406+ process . stderr . write ( "Testing key... " ) ;
407+ const testConfig = { ...config , apiKey : key , baseUrl } ;
408+ const requestOpts = {
409+ url : chatEndpoint ( testConfig . baseUrl ) ,
410+ method : "POST" ,
411+ timeout : Math . min ( config . timeout , 30 ) ,
412+ body : {
413+ model : "qwen3.7-max" ,
414+ messages : [ { role : "user" , content : "hi" } ] ,
415+ max_tokens : 1 ,
416+ } ,
417+ } ;
418+
419+ for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
420+ try {
421+ await requestJson < unknown > ( testConfig , requestOpts ) ;
422+ break ;
423+ } catch ( err ) {
424+ if ( attempt >= 3 || ! canRetry ( err ) ) {
425+ process . stderr . write ( "Failed\n" ) ;
426+ throw new BailianError ( "API key validation failed" , ExitCode . AUTH , "Invalid API key." , {
427+ cause : err ,
428+ } ) ;
429+ }
430+ const delayMs = RETRY_DELAY_BASE_MS * 2 ** ( attempt - 1 ) ;
431+ await new Promise ( ( resolve ) => setTimeout ( resolve , delayMs ) ) ;
432+ }
433+ }
434+
435+ process . stderr . write ( "Valid\n" ) ;
436+ const existing = readConfigFile ( ) as Record < string , unknown > ;
437+ existing . api_key = key ;
438+ await writeConfigFile ( existing ) ;
439+ }
440+
279441export async function runConsoleLogin (
280442 consoleOrigin : string ,
281- opts ?: { needApiKey ?: boolean ; onApiKey ?: ( key : string ) => Promise < void > } ,
443+ config : Config ,
444+ opts ?: { needApiKey ?: boolean } ,
282445) : Promise < void > {
283446 const state = randomBytes ( 16 ) . toString ( "hex" ) ;
284447 let callbackError : unknown ;
@@ -301,18 +464,35 @@ export async function runConsoleLogin(
301464 return ;
302465 }
303466
304- const { accessToken, apiKey } = await extractCredentialsFromRequest ( req ) ;
467+ const {
468+ accessToken,
469+ apiKey,
470+ baseUrl,
471+ consoleSite,
472+ consoleRegion,
473+ consoleSwitchAgent,
474+ workspaceId,
475+ } = await extractCredentialsFromRequest ( req ) ;
476+
477+ const hasConfig =
478+ accessToken || baseUrl || consoleSite || consoleRegion || consoleSwitchAgent || workspaceId ;
305479
306- if ( accessToken || apiKey ) {
480+ if ( hasConfig || apiKey ) {
307481 try {
308- if ( accessToken ) {
482+ if ( hasConfig ) {
309483 const existing = readConfigFile ( ) as Record < string , unknown > ;
310- existing . access_token = accessToken ;
484+ if ( accessToken ) existing . access_token = accessToken ;
485+ if ( baseUrl ) existing . base_url = baseUrl ;
486+ if ( consoleSite ) existing . console_site = consoleSite ;
487+ if ( consoleRegion ) existing . console_region = consoleRegion ;
488+ if ( consoleSwitchAgent ) existing . console_switch_agent = Number ( consoleSwitchAgent ) ;
489+ if ( workspaceId ) existing . workspace_id = workspaceId ;
311490 await writeConfigFile ( existing ) ;
312- process . stderr . write ( `access_token saved to ${ getConfigPath ( ) } \n` ) ;
491+ process . stderr . write ( `Config saved to ${ getConfigPath ( ) } \n` ) ;
313492 }
314- if ( apiKey && opts ?. onApiKey ) {
315- await opts . onApiKey ( apiKey ) ;
493+ if ( apiKey ) {
494+ const testBaseUrl = baseUrl || config . baseUrl ;
495+ await validateAndPersistApiKey ( config , apiKey , testBaseUrl ) ;
316496 }
317497 } catch ( err : unknown ) {
318498 callbackError = err ;
@@ -329,7 +509,7 @@ export async function runConsoleLogin(
329509 } ) ;
330510 res . end ( "OK\n" ) ;
331511
332- if ( accessToken || apiKey ) {
512+ if ( hasConfig || apiKey ) {
333513 server . close ( ) ;
334514 }
335515 } catch {
0 commit comments