Skip to content

Commit f4e479c

Browse files
committed
feat(sandbox): add official image presets and template image selection
1 parent e1eadad commit f4e479c

10 files changed

Lines changed: 484 additions & 53 deletions

File tree

‎packages/cli/src/commands.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ import {
207207
managedAgentFileDownload,
208208
managedAgentFileDelete,
209209
sandboxCreate,
210+
sandboxOfficialImages,
210211
sandboxList,
211212
sandboxGet,
212213
sandboxConnect,
@@ -437,6 +438,7 @@ export const commands: Record<string, AnyCommand> = {
437438
"managed-agent file download": managedAgentFileDownload,
438439
"managed-agent file delete": managedAgentFileDelete,
439440
"sandbox create": sandboxCreate,
441+
"sandbox official-images": sandboxOfficialImages,
440442
"sandbox list": sandboxList,
441443
"sandbox get": sandboxGet,
442444
"sandbox connect": sandboxConnect,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { defineCommand, detectOutputFormat, UsageError } from "bailian-cli-core";
2+
import { emitBare, emitResult } from "bailian-cli-runtime";
3+
4+
/** Pinned CLI presets, not a live catalog or a guarantee of regional availability. */
5+
export const SANDBOX_IMAGES = [
6+
{
7+
id: "code-interpreter",
8+
imageName: "代码解释器",
9+
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/code-interpreter-v1:v0.0.44",
10+
icon: "https://img.alicdn.com/imgextra/i2/O1CN01iX8RH9ckFvC093x2_!!6000000000905-2-tps-72-72.png",
11+
description: {
12+
"en-US": "Python / Node.js runtimes with common data-processing libraries",
13+
"zh-CN": "Python / Node.js 运行时+常用数据处理库",
14+
},
15+
},
16+
{
17+
id: "browser",
18+
imageName: "浏览器",
19+
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
20+
icon: "https://img.alicdn.com/imgextra/i4/O1CN01ShCbPEunrRI093x2_!!6000000000155-2-tps-72-72.png",
21+
description: {
22+
"en-US": "Chromium and a visual desktop for clicking, filling forms, and screenshots",
23+
"zh-CN": "Chromium +可视化桌面,支持点击/ 填表/截图",
24+
},
25+
},
26+
{
27+
id: "all-in-one",
28+
imageName: "全能型",
29+
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/all-in-one:v0.0.44",
30+
icon: "https://img.alicdn.com/imgextra/i1/O1CN01smhklUahsyE093x2_!!6000000006657-2-tps-72-72.png",
31+
description: {
32+
"en-US": "Code execution and browser capabilities together",
33+
"zh-CN": "代码执行+浏览器双能力",
34+
},
35+
},
36+
] as const;
37+
38+
export const SANDBOX_IMAGE_CHOICES = SANDBOX_IMAGES.flatMap((image) => [image.id, image.imageName]);
39+
40+
export const SANDBOX_IMAGE_NOTES = SANDBOX_IMAGES.map((image) => ({
41+
"en-US": `${image.id} (${image.imageName}): ${image.description["en-US"]}. ${image.imageUrl}`,
42+
"zh-CN": `${image.id}(${image.imageName}):${image.description["zh-CN"]}。${image.imageUrl}`,
43+
}));
44+
45+
export function resolveSandboxImage(selector: string) {
46+
const image = SANDBOX_IMAGES.find(
47+
(candidate) => candidate.id === selector || candidate.imageName === selector,
48+
);
49+
if (!image) {
50+
throw new UsageError(
51+
`Unknown built-in image / 未知内置镜像: ${selector}. ${SANDBOX_IMAGE_CHOICES.join(", ")}`,
52+
);
53+
}
54+
return image;
55+
}
56+
57+
export const sandboxOfficialImages = defineCommand({
58+
description: {
59+
"en-US": "List the built-in Sandbox base images (offline)",
60+
"zh-CN": "列出 CLI 内置的 Sandbox 基础镜像(离线)",
61+
},
62+
auth: "none",
63+
exampleArgs: ["", "--output json", "--quiet"],
64+
notes: [
65+
{
66+
"en-US":
67+
"Use a preset ID or Chinese name with template create/update --image. These are pinned cn-beijing images; other environments may differ. Use --from-image for a custom image.",
68+
"zh-CN":
69+
"在 template create/update 中通过 --image 传入预设 ID 或中文名。这些是固定版本的 cn-beijing 镜像,其他环境可能不同;自定义镜像使用 --from-image。",
70+
},
71+
...SANDBOX_IMAGE_NOTES,
72+
],
73+
async run(ctx) {
74+
if (ctx.settings.quiet) {
75+
for (const image of SANDBOX_IMAGES) emitBare(image.id);
76+
return;
77+
}
78+
if (detectOutputFormat(ctx.settings.output) === "json") {
79+
emitResult(SANDBOX_IMAGES, "json");
80+
return;
81+
}
82+
for (const image of SANDBOX_IMAGES) {
83+
emitBare(`${image.id} (${image.imageName})`);
84+
emitBare(` ${image.imageUrl}`);
85+
emitBare(` ${image.description["en-US"]} / ${image.description["zh-CN"]}`);
86+
}
87+
},
88+
});

‎packages/commands/src/commands/sandbox/template.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type Settings,
1414
} from "bailian-cli-core";
1515
import { createSpinner, emitBare, emitResult, formatTable } from "bailian-cli-runtime";
16+
import { resolveSandboxImage, SANDBOX_IMAGE_CHOICES, SANDBOX_IMAGE_NOTES } from "./images.ts";
1617
import {
1718
BODY_FLAG,
1819
displayValue,
@@ -69,6 +70,15 @@ const TEMPLATE_ID_FLAG = {
6970
} satisfies FlagsDef;
7071

7172
const TEMPLATE_MUTATION_FIELDS = {
73+
image: {
74+
type: "string",
75+
valueHint: "<preset>",
76+
choices: SANDBOX_IMAGE_CHOICES,
77+
description: {
78+
"en-US": "Built-in image ID or Chinese name; fills fromImage and imageName",
79+
"zh-CN": "内置镜像 ID 或中文名;自动填写 fromImage 和 imageName",
80+
},
81+
},
7282
name: {
7383
type: "string",
7484
valueHint: "<name>",
@@ -198,6 +208,11 @@ const BUILD_STATUS_FLAGS = {
198208
} satisfies FlagsDef;
199209

200210
function applyTemplateMutationFlags(body: JsonObject, flags: CreateFlags | UpdateFlags): void {
211+
if (flags.image !== undefined) {
212+
const image = resolveSandboxImage(flags.image);
213+
body.fromImage = image.imageUrl;
214+
body.imageName = image.imageName;
215+
}
201216
setDefined(body, "name", flags.name);
202217
setDefined(body, "cpuCount", flags.cpuCount);
203218
setDefined(body, "memoryMB", flags.memoryMb);
@@ -356,12 +371,20 @@ export const sandboxTemplateCreate = defineCommand({
356371
usageArgs: "(--name <name> --cpu-count <cores> --memory-mb <mb> | --body <json|@path>) [flags]",
357372
flags: CREATE_FLAGS,
358373
exampleArgs: [
374+
"--name browser --image browser --cpu-count 1 --memory-mb 2048",
359375
"--name python --cpu-count 1 --memory-mb 2048",
360376
"--body @template.json --async --output json",
361377
"--name browser --cpu-count 4 --memory-mb 8192 --dry-run --output json",
362378
],
363379
notes: [
364380
...SANDBOX_NOTES,
381+
...SANDBOX_IMAGE_NOTES,
382+
{
383+
"en-US":
384+
"--image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.",
385+
"zh-CN":
386+
"--image 覆盖 body 中的 fromImage/imageName;显式 --from-image/--image-name 再覆盖对应预设字段。不传 --image 时保持原有镜像行为。",
387+
},
365388
{
366389
"en-US": "File mounts and other complete nested structures can be supplied through --body.",
367390
"zh-CN": "文件挂载等完整嵌套结构可通过 --body 提供。",
@@ -480,12 +503,20 @@ export const sandboxTemplateUpdate = defineCommand({
480503
usageArgs: "--template-id <id> (--body <json|@path> | [fields])",
481504
flags: UPDATE_FLAGS,
482505
exampleArgs: [
506+
"--template-id tpl-xxx --image all-in-one",
483507
"--template-id tpl-xxx --cpu-count 4 --memory-mb 8192",
484508
"--template-id tpl-xxx --body @template-update.json --async --output json",
485509
"--template-id tpl-xxx --description updated --dry-run --output json",
486510
],
487511
notes: [
488512
...SANDBOX_NOTES,
513+
...SANDBOX_IMAGE_NOTES,
514+
{
515+
"en-US":
516+
"--image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.",
517+
"zh-CN":
518+
"--image 覆盖 body 中的 fromImage/imageName;显式 --from-image/--image-name 再覆盖对应预设字段。不传 --image 时保持原有镜像行为。",
519+
},
489520
{
490521
"en-US": "Supplying envConfig or --env replaces the template's complete environment map.",
491522
"zh-CN": "传入 envConfig 或 --env 会整体替换模版的环境变量 Map。",

‎packages/commands/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export { default as skillUpdate } from "./commands/skill/update.ts";
223223
export { default as skillRemove } from "./commands/skill/remove.ts";
224224
export { default as skillList } from "./commands/skill/list.ts";
225225
export { default as skillInit } from "./commands/skill/init.ts";
226+
export { sandboxOfficialImages } from "./commands/sandbox/images.ts";
226227
export {
227228
sandboxConnect,
228229
sandboxCreate,

‎packages/commands/tests/e2e/sandbox.e2e.test.ts‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ function runCommandE2e(routes: typeof SANDBOX_ROUTES, args: string[]) {
2727
}
2828

2929
describe("e2e: Sandbox command discovery", () => {
30+
test("built-in images are discoverable without API Key authentication", async () => {
31+
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
32+
"sandbox",
33+
"official-images",
34+
"--help",
35+
]);
36+
expect(exitCode, stderr).toBe(0);
37+
expect(stderr).toContain("Usage: bl sandbox official-images");
38+
expect(stderr).not.toContain("Authentication: API Key");
39+
expect(stderr).toContain("code-interpreter");
40+
expect(stderr).toContain("all-in-one");
41+
});
3042
test.each([
3143
["sandbox", "create"],
3244
["sandbox", "list"],
@@ -69,13 +81,118 @@ describe("e2e: Sandbox command discovery", () => {
6981
]);
7082
expect(exitCode, stderr).toBe(0);
7183
expect(stderr).toMatch(/--max-running-time <seconds>/);
84+
expect(stderr).toMatch(/--image <[^>]*code-interpreter[^>]*browser[^>]*all-in-one[^>]*>/);
85+
expect(stderr).toContain("browser:v0.0.44");
7286
expect(stderr).toMatch(/--async/);
7387
expect(stderr).toMatch(/--poll-interval <seconds>/);
7488
expect(stderr).toMatch(/waits for build status ready/i);
7589
});
7690
});
7791

7892
describe("e2e: Sandbox offline validation and dry-run", () => {
93+
test("image catalog is available offline in JSON, text, and quiet output", async () => {
94+
const jsonResult = await runCommandE2e(SANDBOX_ROUTES, [
95+
"sandbox",
96+
"official-images",
97+
"--output",
98+
"json",
99+
]);
100+
expect(jsonResult.exitCode, jsonResult.stderr).toBe(0);
101+
const images = JSON.parse(jsonResult.stdout) as {
102+
id: string;
103+
imageName: string;
104+
imageUrl: string;
105+
}[];
106+
expect(images.map((image) => image.id)).toEqual(["code-interpreter", "browser", "all-in-one"]);
107+
expect(images[1]).toMatchObject({
108+
imageName: "浏览器",
109+
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
110+
});
111+
const textResult = await runCommandE2e(SANDBOX_ROUTES, [
112+
"sandbox",
113+
"official-images",
114+
"--output",
115+
"text",
116+
]);
117+
expect(textResult.exitCode, textResult.stderr).toBe(0);
118+
expect(textResult.stdout).toContain("代码解释器");
119+
expect(textResult.stdout).toContain("browser:v0.0.44");
120+
const quietResult = await runCommandE2e(SANDBOX_ROUTES, [
121+
"sandbox",
122+
"official-images",
123+
"--quiet",
124+
]);
125+
expect(quietResult.exitCode, quietResult.stderr).toBe(0);
126+
expect(quietResult.stdout.trim().split("\n")).toEqual([
127+
"code-interpreter",
128+
"browser",
129+
"all-in-one",
130+
]);
131+
});
132+
133+
test.each([
134+
{
135+
operation: "create",
136+
args: ["--name", "test", "--cpu-count", "1", "--memory-mb", "2048"],
137+
selector: "browser",
138+
method: "POST",
139+
},
140+
{
141+
operation: "update",
142+
args: ["--template-id", "template-test"],
143+
selector: "浏览器",
144+
method: "PUT",
145+
},
146+
])(
147+
"template $operation resolves --image before dry-run output",
148+
async ({ operation, args, selector, method }) => {
149+
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
150+
"sandbox",
151+
"template",
152+
operation,
153+
...args,
154+
"--image",
155+
selector,
156+
...AUTH_ARGS,
157+
"--dry-run",
158+
"--output",
159+
"json",
160+
]);
161+
expect(exitCode, stderr).toBe(0);
162+
expect(parseStdoutJson(stdout)).toMatchObject({
163+
method,
164+
request: {
165+
fromImage: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
166+
imageName: "浏览器",
167+
},
168+
});
169+
},
170+
);
171+
172+
test("unknown image selector fails before a request", async () => {
173+
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
174+
"sandbox",
175+
"template",
176+
"create",
177+
"--name",
178+
"test",
179+
"--cpu-count",
180+
"1",
181+
"--memory-mb",
182+
"2048",
183+
"--image",
184+
"unknown",
185+
...AUTH_ARGS,
186+
"--dry-run",
187+
"--output",
188+
"json",
189+
]);
190+
expect(exitCode).toBe(2);
191+
expect(JSON.parse(stderr)).toMatchObject({
192+
error: { code: 2, message: expect.stringContaining("--image") },
193+
});
194+
});
195+
79196
test("create requires a template in flags or body", async () => {
80197
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
81198
"sandbox",

‎packages/commands/tests/e2e/topic-routes.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export const SKILL_ROUTES: E2eRouteExports = {
184184

185185
export const SANDBOX_ROUTES: E2eRouteExports = {
186186
"sandbox create": "sandboxCreate",
187+
"sandbox official-images": "sandboxOfficialImages",
187188
"sandbox list": "sandboxList",
188189
"sandbox get": "sandboxGet",
189190
"sandbox connect": "sandboxConnect",

0 commit comments

Comments
 (0)