Skip to content

Commit 8277d2b

Browse files
committed
feat: 添加区域切换功能,支持根据用户配置动态设置域名和API URL
1 parent 0e7bec1 commit 8277d2b

6 files changed

Lines changed: 207 additions & 24 deletions

File tree

packages/js-sdk/src/connectionConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export class ConnectionConfig {
102102
}
103103

104104
private static get domain() {
105-
return getEnvVar('E2B_DOMAIN') || 'sandbox.ucloudai.com'
105+
return getEnvVar('UCLOUD_SANDBOX_DOMAIN') || getEnvVar('E2B_DOMAIN') || 'sandbox.ucloudai.com'
106106
}
107107

108108
private static get apiUrl() {
109-
return getEnvVar('E2B_API_URL')
109+
return getEnvVar('UCLOUD_SANDBOX_API_URL') || getEnvVar('E2B_API_URL')
110110
}
111111

112112
private static get sandboxUrl() {

src/api.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as e2b from 'e2b'
33

44
import { getUserConfig, UserConfig } from './user'
55
import { asBold, asPrimary } from './utils/format'
6+
import { getRegionDomain, getRegionApiUrl } from './commands/region'
67

78
export let apiKey = process.env.AGENTBOX_API_KEY
89
export let accessToken = process.env.AGENTBOX_ACCESS_TOKEN
@@ -88,10 +89,20 @@ export function ensureAccessToken() {
8889

8990
const userConfig = getUserConfig()
9091

92+
// Inject region-based domain/apiUrl into env so SDK internal calls (Sandbox.list, etc.) also use them
93+
if (userConfig?.region) {
94+
if (!process.env.UCLOUD_SANDBOX_DOMAIN) {
95+
process.env.UCLOUD_SANDBOX_DOMAIN = getRegionDomain(userConfig.region)
96+
}
97+
if (!process.env.UCLOUD_SANDBOX_API_URL) {
98+
process.env.UCLOUD_SANDBOX_API_URL = getRegionApiUrl(userConfig.region)
99+
}
100+
}
101+
91102
export const connectionConfig = new e2b.ConnectionConfig({
92103
accessToken: process.env.AGENTBOX_ACCESS_TOKEN || userConfig?.accessToken,
93104
apiKey: process.env.AGENTBOX_API_KEY || userConfig?.teamApiKey,
94-
domain: process.env.UCLOUD_SANDBOX_DOMAIN || 'sandbox.ucloudai.com',
95-
apiUrl: process.env.UCLOUD_SANDBOX_API_URL || 'https://api.sandbox.ucloudai.com',
105+
domain: process.env.UCLOUD_SANDBOX_DOMAIN || getRegionDomain(userConfig?.region),
106+
apiUrl: process.env.UCLOUD_SANDBOX_API_URL || getRegionApiUrl(userConfig?.region),
96107
})
97108
export const client = new e2b.ApiClient(connectionConfig)

src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { asPrimary } from 'src/utils/format'
44
import { templateCommand } from './template'
55
import { sandboxCommand } from './sandbox'
66
import { authCommand } from './auth'
7+
import { regionCommand } from './region'
78

89
export const program = new commander.Command()
910
.description(
@@ -19,6 +20,7 @@ Visit ${asPrimary(
1920
.addCommand(authCommand)
2021
.addCommand(templateCommand)
2122
.addCommand(sandboxCommand)
23+
.addCommand(regionCommand)
2224

2325
function addDebugOption(cmd: commander.Command) {
2426
cmd.option('--debug', 'print Trace ID for debugging')

src/commands/region.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as commander from 'commander'
2+
import * as fs from 'fs'
3+
import * as path from 'path'
4+
import * as chalk from 'chalk'
5+
6+
import { getUserConfig, USER_CONFIG_PATH } from 'src/user'
7+
8+
const REGIONS = [
9+
{ id: 'cn-wlcb', name: 'cn-wlcb (China - North)' },
10+
{ id: 'us-ca', name: 'us-ca (US - West)' },
11+
] as const
12+
13+
type RegionId = (typeof REGIONS)[number]['id']
14+
15+
export function getRegionDomain(region?: string): string {
16+
if (!region) return 'sandbox.ucloudai.com'
17+
return `${region}.sandbox.ucloudai.com`
18+
}
19+
20+
export function getRegionApiUrl(region?: string): string {
21+
if (!region) return 'https://api.sandbox.ucloudai.com'
22+
return `https://api.${region}.sandbox.ucloudai.com`
23+
}
24+
25+
function switchRegion(region: string) {
26+
const valid = REGIONS.find((r) => r.id === region)
27+
if (!valid) {
28+
console.error(
29+
`Unknown region: ${region}\nAvailable regions: ${REGIONS.map((r) => r.id).join(', ')}`
30+
)
31+
process.exit(1)
32+
}
33+
34+
const userConfig = getUserConfig() || ({} as any)
35+
const updatedConfig = { ...userConfig, region }
36+
fs.mkdirSync(path.dirname(USER_CONFIG_PATH), { recursive: true })
37+
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(updatedConfig, null, 2))
38+
39+
console.log(`Switched to region ${chalk.default.green(valid.name)}`)
40+
console.log(` Domain: ${getRegionDomain(region)}`)
41+
console.log(` API: ${getRegionApiUrl(region)}`)
42+
}
43+
44+
export const regionCommand = new commander.Command('region')
45+
.description('switch region for sandbox services')
46+
.argument('[region]', 'region to switch to (e.g. cn-wlcb, us-ca)')
47+
.action(async (region?: string) => {
48+
if (region) {
49+
switchRegion(region)
50+
process.exit(0)
51+
}
52+
53+
// Interactive selection
54+
const userConfig = getUserConfig()
55+
const currentRegion = userConfig?.region || ''
56+
57+
const inquirer = await import('inquirer')
58+
const { selected } = await inquirer.default.prompt([
59+
{
60+
name: 'selected',
61+
type: 'list',
62+
message: 'Select region:',
63+
choices: REGIONS.map((r) => ({
64+
name: r.id === currentRegion
65+
? chalk.default.green(`${r.name} \u2713`)
66+
: r.name,
67+
value: r.id,
68+
})),
69+
default: currentRegion || REGIONS[0].id,
70+
},
71+
])
72+
73+
switchRegion(selected)
74+
process.exit(0)
75+
})

src/commands/template/build.ts

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
fallbackDockerfileName,
1313
} from 'src/docker/constants'
1414
import { configOption, pathOption, teamOption } from 'src/options'
15-
import { getUserConfig } from 'src/user'
15+
import { getUserConfig, USER_CONFIG_PATH } from 'src/user'
1616
import { getRoot } from 'src/utils/filesystem'
1717
import { wait } from 'src/utils/wait'
1818
import * as stripAnsi from 'strip-ansi'
@@ -28,13 +28,15 @@ import {
2828
asTypescript,
2929
withDelimiter,
3030
} from '../../utils/format'
31-
import { buildWithProxy } from './buildWithProxy'
3231

3332
const templateCheckInterval = 500 // 0.5 sec
3433

3534
// Custom image URI is used for Bring Your Own Compute with self-hosted Docker registry
3635
export const imageUriMask = process.env.UCLOUD_SANDBOX_IMAGE_URI_MASK
3736

37+
// UHub Docker Registry
38+
const UHUB_REGISTRY = 'uhub.service.ucloud.cn'
39+
3840
async function getTemplateBuildLogs({
3941
templateID,
4042
buildID,
@@ -87,18 +89,36 @@ async function requestTemplateRebuild(
8789
})
8890
}
8991

90-
async function triggerTemplateBuild(templateID: string, buildID: string) {
92+
async function triggerTemplateBuild(
93+
templateID: string,
94+
buildID: string,
95+
fromImage?: string,
96+
registryCredentials?: { username: string; password: string }
97+
) {
9198
let res
9299
const maxRetries = 3
93100
for (let i = 0; i < maxRetries; i++) {
94101
try {
102+
const body: Record<string, unknown> = {}
103+
if (fromImage) {
104+
body.fromImage = fromImage
105+
}
106+
if (registryCredentials) {
107+
body.fromImageRegistry = {
108+
type: 'registry',
109+
username: registryCredentials.username,
110+
password: registryCredentials.password,
111+
}
112+
}
113+
95114
res = await client.api.POST('/templates/{templateID}/builds/{buildID}', {
96115
params: {
97116
path: {
98117
templateID,
99118
buildID,
100119
},
101120
},
121+
body: body as any,
102122
})
103123

104124
break
@@ -197,6 +217,8 @@ export const buildCommand = new commander.Command('build')
197217
noCache?: boolean
198218
}
199219
) => {
220+
let uhubRepoPath = ''
221+
let uhubCreds: { username: string; password: string } | undefined
200222
try {
201223
// Display deprecation warning
202224
const deprecationMessage = `${asBold('DEPRECATION WARNING')}
@@ -241,7 +263,7 @@ Migration guide: ${asPrimary('https://docs.ucloud.cn/modelverse/README')}`
241263
})
242264
}
243265

244-
const accessToken = ensureAccessToken()
266+
ensureAccessToken()
245267
process.stdout.write('\n')
246268

247269
const newName = opts.name?.trim()
@@ -374,20 +396,85 @@ Migration guide: ${asPrimary('https://docs.ucloud.cn/modelverse/README')}`
374396
)
375397

376398
if (imageUriMask == undefined) {
399+
// Login to UHub registry — reuse saved credentials or prompt
400+
const inquirer = await import('inquirer')
401+
let savedConfig = getUserConfig()
402+
let uhubUsername = savedConfig?.uhubUsername || ''
403+
let uhubPassword = savedConfig?.uhubPassword || ''
404+
405+
if (uhubUsername && uhubPassword) {
406+
console.log(`Using saved UHub credentials (username: ${uhubUsername})`)
407+
} else {
408+
console.log(`Logging in to UHub registry (${UHUB_REGISTRY})...`)
409+
const answers = await inquirer.default.prompt([
410+
{
411+
name: 'uhubUsername',
412+
type: 'input',
413+
message: 'UHub username:',
414+
},
415+
{
416+
name: 'uhubPassword',
417+
type: 'password',
418+
message: 'UHub password (token):',
419+
mask: '*',
420+
},
421+
])
422+
uhubUsername = answers.uhubUsername
423+
uhubPassword = answers.uhubPassword
424+
}
425+
377426
try {
378427
child_process.execSync(
379-
`echo "${accessToken}" | docker login docker.${connectionConfig.domain} -u _sandbox_access_token --password-stdin`,
428+
`docker login ${UHUB_REGISTRY} -u ${uhubUsername} --password-stdin`,
380429
{
381-
stdio: 'inherit',
430+
stdio: ['pipe', 'inherit', 'inherit'],
382431
cwd: root,
432+
input: uhubPassword,
383433
}
384434
)
435+
uhubCreds = { username: uhubUsername, password: uhubPassword }
385436
} catch (err: any) {
386437
console.error(
387-
'Docker login failed. Please try to log in with `ucloud-sandbox-cli auth login` and try again.'
438+
`Docker login to ${UHUB_REGISTRY} failed. Please check your UHub credentials and try again.`
388439
)
440+
// Clear saved credentials on failure
441+
if (savedConfig) {
442+
const { uhubUsername: _u, uhubPassword: _p, uhubRepo: _r, ...rest } = savedConfig
443+
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(rest, null, 2))
444+
}
389445
process.exit(1)
390446
}
447+
448+
// Prompt for UHub namespace and repo name, reuse saved or ask
449+
let uhubRepo = savedConfig?.uhubRepo || ''
450+
if (uhubRepo) {
451+
console.log(`Using saved UHub repo: ${uhubRepo}`)
452+
} else {
453+
const repoAnswer = await inquirer.default.prompt([
454+
{
455+
name: 'uhubNamespace',
456+
type: 'input',
457+
message: 'UHub namespace (e.g. ucloud-uhub):',
458+
validate: (input: string) => input ? true : 'Namespace is required',
459+
},
460+
{
461+
name: 'uhubRepoName',
462+
type: 'input',
463+
message: `UHub repo name:`,
464+
default: templateID,
465+
validate: (input: string) => input ? true : 'Repo name is required',
466+
},
467+
])
468+
uhubRepo = `${repoAnswer.uhubNamespace}/${repoAnswer.uhubRepoName}`
469+
}
470+
471+
// Save all UHub settings to config
472+
if (savedConfig) {
473+
const updatedConfig = { ...savedConfig, uhubUsername, uhubPassword, uhubRepo }
474+
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(updatedConfig, null, 2))
475+
}
476+
477+
uhubRepoPath = uhubRepo
391478
}
392479

393480
process.stdout.write('\n')
@@ -402,7 +489,8 @@ Migration guide: ${asPrimary('https://docs.ucloud.cn/modelverse/README')}`
402489
templateID,
403490
template.buildID,
404491
connectionConfig.domain,
405-
imageUriMask
492+
imageUriMask,
493+
uhubRepoPath
406494
)
407495
if (imageUriMask != undefined) {
408496
console.log('Using custom docker image URI:', imageUrl)
@@ -444,18 +532,15 @@ Migration guide: ${asPrimary('https://docs.ucloud.cn/modelverse/README')}`
444532
cwd: root,
445533
})
446534
} catch (err: any) {
447-
await buildWithProxy(
448-
userConfig,
449-
connectionConfig,
450-
accessToken,
451-
template,
452-
root
535+
console.error(
536+
`Docker push to ${UHUB_REGISTRY} failed. Please check your UHub credentials and repository permissions.`
453537
)
538+
process.exit(1)
454539
}
455540
console.log('> Docker image pushed.\n')
456541

457542
console.log('Triggering build...')
458-
await triggerBuild(templateID, template.buildID)
543+
await triggerBuild(templateID, template.buildID, imageUrl, uhubCreds)
459544

460545
console.log(
461546
`> Triggered build for the sandbox template ${asFormattedSandboxTemplate(
@@ -656,20 +741,26 @@ async function requestBuildTemplate(
656741
return res.data
657742
}
658743

659-
async function triggerBuild(templateID: string, buildID: string) {
660-
await triggerTemplateBuild(templateID, buildID)
744+
async function triggerBuild(
745+
templateID: string,
746+
buildID: string,
747+
fromImage?: string,
748+
registryCredentials?: { username: string; password: string }
749+
) {
750+
await triggerTemplateBuild(templateID, buildID, fromImage, registryCredentials)
661751

662752
return
663753
}
664754

665755
function dockerImageUrl(
666756
templateID: string,
667757
buildID: string,
668-
defaultDomain: string,
669-
imageUrlMask?: string
758+
_defaultDomain: string,
759+
imageUrlMask?: string,
760+
uhubRepo?: string
670761
): string {
671762
if (imageUrlMask == undefined) {
672-
return `docker.${defaultDomain}/sandbox/custom-envs/${templateID}:${buildID}`
763+
return `${UHUB_REGISTRY}/${uhubRepo}:${buildID}`
673764
}
674765

675766
return imageUrlMask

src/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export interface UserConfig {
1212
teamId: string
1313
teamApiKey: string
1414
dockerProxySet?: boolean
15+
uhubUsername?: string
16+
uhubPassword?: string
17+
uhubRepo?: string
18+
region?: string
1519
}
1620

1721
export const USER_CONFIG_PATH = path.join(os.homedir(), '.ucloud-sandbox-cli', 'config.json')

0 commit comments

Comments
 (0)