1+ import { join } from 'node:path' ;
2+
13import {
24 applyKimiOAuthConfig ,
35 applyMiniMaxOAuthConfig ,
@@ -6,11 +8,13 @@ import {
68 fetchKimiCodingModels ,
79 fetchOpenAICodexModels ,
810 fetchOpenPlatformModels ,
11+ FileTokenStorage ,
912 filterModelsByPrefix ,
1013 getOpenPlatformById ,
1114 miniMaxCodingModels ,
1215 minimaxCodingProviderId ,
1316 minimaxRegionLabel ,
17+ resolveOAuthTokenStorageName ,
1418 KIMI_CODING_PROVIDER_ID ,
1519 KIMI_OAUTH_PLATFORM_ID ,
1620 MINIMAX_OAUTH_PLATFORM_ID_CN ,
@@ -29,6 +33,7 @@ import {
2933 type OpenPlatformDefinition ,
3034 type ProviderModelInfo ,
3135 type PythinkerConfigShape ,
36+ type TokenInfo ,
3237} from '@pymodel/pythinker-code-oauth' ;
3338
3439import {
@@ -330,38 +335,53 @@ async function handleKimiOAuthLogin(ui: LoginUi): Promise<boolean> {
330335 return false ;
331336 }
332337
333- const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
334- id : KIMI_CODING_PROVIDER_ID ,
335- name : 'Kimi For Coding' ,
336- } ) ;
337- if ( picked === undefined ) return false ;
338- const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
339- if ( selectedModel === undefined ) return false ;
338+ try {
339+ const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
340+ id : KIMI_CODING_PROVIDER_ID ,
341+ name : 'Kimi For Coding' ,
342+ } ) ;
343+ if ( picked === undefined ) return false ;
344+ const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
345+ if ( selectedModel === undefined ) return false ;
340346
341- controller . signal . throwIfAborted ( ) ;
342- const current = await ui . harness . getConfig ( { reload : true } ) ;
343- controller . signal . throwIfAborted ( ) ;
344- const next = cloneConfig ( current ) ;
345- applyKimiOAuthConfig ( next as PythinkerConfigShape , {
346- accessToken : tokens . accessToken ,
347- refreshToken : tokens . refreshToken ,
348- deviceId : tokens . deviceId ,
349- models,
350- selectedModel,
351- thinking : picked . effort !== 'off' ,
352- effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
353- } ) ;
354- committing = true ;
355- await ui . harness . replaceConfigSections ( {
356- providers : next . providers ,
357- models : next . models ,
358- defaultModel : next . defaultModel ,
359- thinking : next . thinking ,
360- } ) ;
361- await ui . refreshConfigAfterLogin ( ) ;
362- ui . track ( 'login' , { provider : KIMI_CODING_PROVIDER_ID , method : 'oauth' } ) ;
363- ui . showStatus ( `Setup complete: Kimi For Coding · ${ selectedModel . id } ` ) ;
364- return true ;
347+ controller . signal . throwIfAborted ( ) ;
348+ const current = await ui . harness . getConfig ( { reload : true } ) ;
349+ controller . signal . throwIfAborted ( ) ;
350+ const next = cloneConfig ( current ) ;
351+ applyKimiOAuthConfig ( next as PythinkerConfigShape , {
352+ accessToken : tokens . accessToken ,
353+ refreshToken : tokens . refreshToken ,
354+ deviceId : tokens . deviceId ,
355+ models,
356+ selectedModel,
357+ thinking : picked . effort !== 'off' ,
358+ effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
359+ } ) ;
360+ await persistOAuthToken ( ui , KIMI_CODING_PROVIDER_ID , {
361+ accessToken : tokens . accessToken ,
362+ refreshToken : tokens . refreshToken ,
363+ expiresAt : Math . floor ( tokens . expiresAtMs / 1000 ) ,
364+ expiresIn : Math . max ( 1 , Math . floor ( ( tokens . expiresAtMs - Date . now ( ) ) / 1000 ) ) ,
365+ scope : tokens . scope ,
366+ tokenType : tokens . tokenType ,
367+ metadata : { provider : 'kimi' , deviceId : tokens . deviceId } ,
368+ } ) ;
369+ controller . signal . throwIfAborted ( ) ;
370+ committing = true ;
371+ await ui . harness . replaceConfigSections ( {
372+ providers : next . providers ,
373+ models : next . models ,
374+ defaultModel : next . defaultModel ,
375+ thinking : next . thinking ,
376+ } ) ;
377+ await ui . refreshConfigAfterLogin ( ) ;
378+ ui . track ( 'login' , { provider : KIMI_CODING_PROVIDER_ID , method : 'oauth' } ) ;
379+ ui . showStatus ( `Setup complete: Kimi For Coding · ${ selectedModel . id } ` ) ;
380+ return true ;
381+ } catch ( error ) {
382+ if ( controller . signal . aborted ) return false ;
383+ throw error ;
384+ }
365385 } finally {
366386 if ( ui . cancelInFlight === cancelLogin ) ui . cancelInFlight = undefined ;
367387 }
@@ -400,43 +420,63 @@ async function handleMiniMaxOAuthLogin(ui: LoginUi, region: MiniMaxRegion): Prom
400420 }
401421 spinner ?. stop ( { ok : true , label : 'Authorized.' } ) ;
402422
403- const providerId = minimaxCodingProviderId ( region ) ;
404- const models = [ ...miniMaxCodingModels ( ) ] ;
405- const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
406- id : providerId ,
407- name : regionLabel ,
408- } ) ;
409- if ( picked === undefined ) return false ;
410- const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
411- if ( selectedModel === undefined ) return false ;
423+ try {
424+ const providerId = minimaxCodingProviderId ( region ) ;
425+ const models = [ ...miniMaxCodingModels ( ) ] ;
426+ const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
427+ id : providerId ,
428+ name : regionLabel ,
429+ } ) ;
430+ if ( picked === undefined ) return false ;
431+ const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
432+ if ( selectedModel === undefined ) return false ;
412433
413- controller . signal . throwIfAborted ( ) ;
414- const current = await ui . harness . getConfig ( { reload : true } ) ;
415- controller . signal . throwIfAborted ( ) ;
416- const next = cloneConfig ( current ) ;
417- applyMiniMaxOAuthConfig ( next as PythinkerConfigShape , region , {
418- accessToken : tokens . accessToken ,
419- refreshToken : tokens . refreshToken ,
420- selectedModel,
421- thinking : picked . effort !== 'off' ,
422- effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
423- } ) ;
424- committing = true ;
425- await ui . harness . replaceConfigSections ( {
426- providers : next . providers ,
427- models : next . models ,
428- defaultModel : next . defaultModel ,
429- thinking : next . thinking ,
430- } ) ;
431- await ui . refreshConfigAfterLogin ( ) ;
432- ui . track ( 'login' , { provider : providerId , method : 'oauth' } ) ;
433- ui . showStatus ( `Setup complete: ${ regionLabel } · ${ selectedModel . id } ` ) ;
434- return true ;
434+ controller . signal . throwIfAborted ( ) ;
435+ const current = await ui . harness . getConfig ( { reload : true } ) ;
436+ controller . signal . throwIfAborted ( ) ;
437+ const next = cloneConfig ( current ) ;
438+ applyMiniMaxOAuthConfig ( next as PythinkerConfigShape , region , {
439+ accessToken : tokens . accessToken ,
440+ refreshToken : tokens . refreshToken ,
441+ selectedModel,
442+ thinking : picked . effort !== 'off' ,
443+ effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
444+ } ) ;
445+ await persistOAuthToken ( ui , providerId , {
446+ accessToken : tokens . accessToken ,
447+ refreshToken : tokens . refreshToken ,
448+ expiresAt : Math . floor ( tokens . expiresAtMs / 1000 ) ,
449+ expiresIn : Math . max ( 1 , Math . floor ( ( tokens . expiresAtMs - Date . now ( ) ) / 1000 ) ) ,
450+ scope : tokens . scope ,
451+ tokenType : tokens . tokenType ,
452+ metadata : { provider : 'minimax' , region } ,
453+ } ) ;
454+ controller . signal . throwIfAborted ( ) ;
455+ committing = true ;
456+ await ui . harness . replaceConfigSections ( {
457+ providers : next . providers ,
458+ models : next . models ,
459+ defaultModel : next . defaultModel ,
460+ thinking : next . thinking ,
461+ } ) ;
462+ await ui . refreshConfigAfterLogin ( ) ;
463+ ui . track ( 'login' , { provider : providerId , method : 'oauth' } ) ;
464+ ui . showStatus ( `Setup complete: ${ regionLabel } · ${ selectedModel . id } ` ) ;
465+ return true ;
466+ } catch ( error ) {
467+ if ( controller . signal . aborted ) return false ;
468+ throw error ;
469+ }
435470 } finally {
436471 if ( ui . cancelInFlight === cancelLogin ) ui . cancelInFlight = undefined ;
437472 }
438473}
439474
475+ async function persistOAuthToken ( ui : LoginUi , providerId : string , token : TokenInfo ) : Promise < void > {
476+ const storage = new FileTokenStorage ( join ( ui . harness . homeDir , 'credentials' ) ) ;
477+ await storage . save ( resolveOAuthTokenStorageName ( `oauth/${ providerId } ` ) , token ) ;
478+ }
479+
440480function cloneConfig ( config : PythinkerConfig ) : PythinkerConfig {
441481 return {
442482 ...config ,
0 commit comments