diff --git a/README.md b/README.md index c5db6a4..96ea57e 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,15 @@ ucloud-sandbox-cli sandbox create desktop ucloud-sandbox-cli sandbox create base ``` +创建沙箱时可以按 Volume 名称挂载一个或多个持久化 Volume: + +```bash +ucloud-sandbox-cli sandbox create base --mount :/data +ucloud-sandbox-cli sandbox create base \ + --mount :/data \ + --mount :/cache +``` + > 创建成功后,CLI 会自动连接终端,您可以像操作本地 Shell 一样执行命令。按`Ctrl+D`或输入`exit`退出连接(沙箱继续运行)。 ### 连接现有沙箱 @@ -166,6 +175,27 @@ ucloud-sandbox-cli sandbox metrics ucloud-sandbox-cli sandbox metrics -w ``` +## Volume 管理 + +创建持久化 Volume: + +```bash +ucloud-sandbox-cli vol create +``` + +列出 Volume: + +```bash +ucloud-sandbox-cli vol list +ucloud-sandbox-cli vol list --format json +``` + +删除一个或多个 Volume: + +```bash +ucloud-sandbox-cli vol delete +``` + ## 模板构建管理 ### 初始化模板项目 diff --git a/cmd/sandbox/create.go b/cmd/sandbox/create.go index 7b16792..37b8716 100644 --- a/cmd/sandbox/create.go +++ b/cmd/sandbox/create.go @@ -3,21 +3,23 @@ package sandbox import ( "context" "fmt" + "strings" "github.com/spf13/cobra" - sdk "github.com/ucloud/ucloud-sandbox-sdk-go" "github.com/ucloud/ucloud-sandbox-cli/internal/config" + sdk "github.com/ucloud/ucloud-sandbox-sdk-go" ) func newCreateCmd() *cobra.Command { var timeout int var detach bool + var mountSpecs []string cmd := &cobra.Command{ Use: "create [template]", Aliases: []string{"cr"}, Short: "Create a new sandbox", - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { template := "base" if len(args) > 0 { @@ -38,6 +40,13 @@ func newCreateCmd() *cobra.Command { if timeout > 0 { opts = append(opts, sdk.WithTimeout(timeout)) } + if len(mountSpecs) > 0 { + mounts, err := parseVolumeMounts(mountSpecs) + if err != nil { + return err + } + opts = append(opts, sdk.WithVolumeMounts(mounts)) + } sbx, err := client.CreateSandbox(ctx, opts...) if err != nil { @@ -55,5 +64,21 @@ func newCreateCmd() *cobra.Command { cmd.Flags().IntVar(&timeout, "timeout", 0, "Sandbox timeout in seconds") cmd.Flags().BoolVar(&detach, "detach", false, "Do not connect to the sandbox after creation") + cmd.Flags().StringArrayVar(&mountSpecs, "mount", nil, "Mount volume as : (repeatable)") return cmd } + +func parseVolumeMounts(values []string) ([]sdk.VolumeMount, error) { + mounts := make([]sdk.VolumeMount, 0, len(values)) + for _, value := range values { + volumeName, mountPath, ok := strings.Cut(value, ":") + if !ok || volumeName == "" || mountPath == "" { + return nil, fmt.Errorf("invalid mount %q: expected :", value) + } + if !strings.HasPrefix(mountPath, "/") { + return nil, fmt.Errorf("invalid mount %q: mount path must be absolute", value) + } + mounts = append(mounts, sdk.VolumeMount{Name: volumeName, Path: mountPath}) + } + return mounts, nil +} diff --git a/cmd/volume/create.go b/cmd/volume/create.go new file mode 100644 index 0000000..c2463a1 --- /dev/null +++ b/cmd/volume/create.go @@ -0,0 +1,37 @@ +package volume + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sandbox-cli/internal/config" +) + +func newCreateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Aliases: []string{"cr"}, + Short: "Create a volume", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return err + } + client, err := config.NewClient(cfg) + if err != nil { + return err + } + + volume, err := client.CreateVolume(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to create volume: %w", err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "Volume created: %s\n", volume.ID) + return nil + }, + } + + return cmd +} diff --git a/cmd/volume/delete.go b/cmd/volume/delete.go new file mode 100644 index 0000000..0bb7346 --- /dev/null +++ b/cmd/volume/delete.go @@ -0,0 +1,43 @@ +package volume + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sandbox-cli/internal/config" +) + +func newDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Aliases: []string{"dl"}, + Short: "Delete one or more volumes", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return err + } + client, err := config.NewClient(cfg) + if err != nil { + return err + } + + for _, id := range args { + deleted, err := client.DeleteVolume(cmd.Context(), id) + if err != nil { + return fmt.Errorf("failed to delete volume %s: %w", id, err) + } + if !deleted { + fmt.Fprintf(cmd.OutOrStdout(), "Volume not found: %s\n", id) + continue + } + fmt.Fprintf(cmd.OutOrStdout(), "Deleted volume: %s\n", id) + } + + return nil + }, + } + + return cmd +} diff --git a/cmd/volume/list.go b/cmd/volume/list.go new file mode 100644 index 0000000..084f47a --- /dev/null +++ b/cmd/volume/list.go @@ -0,0 +1,73 @@ +package volume + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-sandbox-cli/internal/config" + "github.com/ucloud/ucloud-sandbox-cli/internal/table" + sdk "github.com/ucloud/ucloud-sandbox-sdk-go" +) + +// listedVolume is a display-friendly view of VolumeInfo for table rendering. +type listedVolume struct { + VolumeID string `table_field:"Volume ID"` + Name string `table_field:"Name"` +} + +func toListedVolume(v sdk.VolumeInfo) listedVolume { + return listedVolume{ + VolumeID: v.VolumeID, + Name: v.Name, + } +} + +func newListCmd() *cobra.Command { + var format string + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List volumes", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.Load() + if err != nil { + return err + } + client, err := config.NewClient(cfg) + if err != nil { + return err + } + + volumes, err := client.ListVolumes(cmd.Context()) + if err != nil { + return err + } + + if format == "json" { + return json.NewEncoder(cmd.OutOrStdout()).Encode(volumes) + } + + if len(volumes) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No volumes found.") + return nil + } + + rows := make([]listedVolume, len(volumes)) + for i, volume := range volumes { + rows[i] = toListedVolume(volume) + } + + out, err := table.Render(rows, 1, 0, int64(len(rows))) + if err != nil { + return err + } + fmt.Fprint(cmd.OutOrStdout(), out) + return nil + }, + } + + cmd.Flags().StringVarP(&format, "format", "f", "pretty", "Output format (pretty, json)") + return cmd +} diff --git a/cmd/volume/volume.go b/cmd/volume/volume.go new file mode 100644 index 0000000..f535fb2 --- /dev/null +++ b/cmd/volume/volume.go @@ -0,0 +1,16 @@ +package volume + +import "github.com/spf13/cobra" + +// NewVolumeCmd returns the root volume command group. +func NewVolumeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "volume", + Aliases: []string{"vol"}, + Short: "Manage volumes", + } + cmd.AddCommand(newCreateCmd()) + cmd.AddCommand(newDeleteCmd()) + cmd.AddCommand(newListCmd()) + return cmd +} diff --git a/go.mod b/go.mod index 13e6f47..bc6cdf4 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144 + github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25 golang.org/x/term v0.44.0 ) diff --git a/go.sum b/go.sum index 8592bca..f447f12 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,12 @@ github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260626103758-60b4523c8358 h1:q9 github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260626103758-60b4523c8358/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc= github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144 h1:PHVW7cDYIAfl1vv/6qKbvzs52HvhlSW5j1h4DuJO2uc= github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260724023741-905601aa3144/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260803102739-bc8a7a31fc6a h1:NAPrfCttc0J30dMXBYd71oqpB0+Z1hJi7o/JbqZc1u0= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260803102739-bc8a7a31fc6a/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260804080039-35258adf309e h1:HUxJzyqzi7HfasPmtGB6894+7a60i7kJCzzxFzMr6fk= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260804080039-35258adf309e/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25 h1:l9KU2bJKvLGZkh8NDKq0fnzbSF7B2P7Eo8pN6b20ccw= +github.com/ucloud/ucloud-sandbox-sdk-go v0.0.0-20260811030544-8dd599016a25/go.mod h1:097WABFNud50hH2KImyZ8WOJeb0lM2A4+gLvQ1kllsc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= diff --git a/main.go b/main.go index 98e5ddb..79e3bb4 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( sandboxcmd "github.com/ucloud/ucloud-sandbox-cli/cmd/sandbox" snapshotcmd "github.com/ucloud/ucloud-sandbox-cli/cmd/snapshot" templatecmd "github.com/ucloud/ucloud-sandbox-cli/cmd/template" + volumecmd "github.com/ucloud/ucloud-sandbox-cli/cmd/volume" ) var ( @@ -36,6 +37,7 @@ func newCommand() *cobra.Command { c.AddCommand(fscmd.NewFsCmd()) c.AddCommand(snapshotcmd.NewSnapshotCmd()) c.AddCommand(templatecmd.NewTemplateCmd()) + c.AddCommand(volumecmd.NewVolumeCmd()) versionCmd := &cobra.Command{ Use: "version", diff --git a/skills/astraflow-api/SKILL.md b/skills/astraflow-api/SKILL.md index d5a318e..d27f8a2 100644 --- a/skills/astraflow-api/SKILL.md +++ b/skills/astraflow-api/SKILL.md @@ -85,23 +85,20 @@ export ASTRAFLOW_PROJECT_ID="" - 浮点数不能用科学计数法表示。 - 数组类型参数(例如 `ModelNames.N`)按其展开后的实际键名参与排序和拼接,比如 `ModelNames.0`、`ModelNames.1`。 -示例(来自官方文档,用于校验实现是否正确): +安全示例(只使用占位符和环境变量,不在文档中写入任何可用密钥): -- `PublicKey`: `ucloudsomeone@example.com1296235120854146120` -- `PrivateKey`: `46f09bb9fab4f12dfc160dae12273d5332b5debe` -- 请求参数:`Action=DescribeUHostInstance`、`Region=cn-bj2`、`Limit=10` -- 拼接后的待签名字符串: +- `PublicKey`:从 `ASTRAFLOW_PUBLIC_KEY` 读取。 +- `PrivateKey`:只从 `ASTRAFLOW_PRIVATE_KEY` 读取,不写入命令字面量、日志或仓库文件。 +- 请求参数:`Action=DescribeUHostInstance`、`Region=cn-bj2`、`Limit=10`。 -``` -ActionDescribeUHostInstanceLimit10PublicKeyucloudsomeone@example.com1296235120854146120Regioncn-bj246f09bb9fab4f12dfc160dae12273d5332b5debe -``` - -- 对上面字符串做SHA1,得到 `Signature`:`cba5cf5ec4d4233d206b1b54951e3787350a642f` - -用shell快速验证的写法(仅用于本地校验签名算法实现,实际调用时按参数升序拼接对应接口的真实参数): +用 shell 在本地计算签名,并将结果保存到环境变量(执行前确认未开启 `set -x`): ```bash -printf '%s' 'ActionDescribeUHostInstanceLimit10PublicKeyucloudsomeone@example.com1296235120854146120Regioncn-bj246f09bb9fab4f12dfc160dae12273d5332b5debe' | sha1sum +ASTRAFLOW_SIGNATURE="$( + printf '%s' "ActionDescribeUHostInstanceLimit10PublicKey${ASTRAFLOW_PUBLIC_KEY}Regioncn-bj2${ASTRAFLOW_PRIVATE_KEY}" | + sha1sum | + awk '{print $1}' +)" ``` 计算出Signature后,把它作为一个普通参数加进最终请求里,和其余参数一起发送。 @@ -115,9 +112,9 @@ curl -X POST \ -d '{ "Action" : "DescribeUHostInstance", "Limit" : 10, - "PublicKey" : "ucloudsomeone@example.com1296235120854146120", + "PublicKey" : "", "Region" : "cn-bj2", - "Signature" : "cba5cf5ec4d4233d206b1b54951e3787350a642f" + "Signature" : "" }' ``` diff --git a/skills/ucloud-sandbox/SKILL.md b/skills/ucloud-sandbox/SKILL.md index 9e3e7f9..80bc9e8 100644 --- a/skills/ucloud-sandbox/SKILL.md +++ b/skills/ucloud-sandbox/SKILL.md @@ -1,11 +1,11 @@ --- name: ucloud-sandbox -description: 当用户需要在 Linux、macOS 或 Windows 中用 UCloud Sandbox CLI 操作沙箱服务时使用,包括安装或配置 ucloud-sandbox-cli、设置 API Key 和地域、创建/连接/执行/暂停/终止沙箱、浏览和管理沙箱文件、上传或下载文件、查看端口地址和监控指标、管理快照与模板,以及在 Claude Code、Codex、Gemini 等 Agent 中安装本技能。 +description: 当用户需要在 Linux、macOS 或 Windows 中用 UCloud Sandbox CLI 操作沙箱服务时使用,包括安装或配置 ucloud-sandbox-cli、设置 API Key 和地域、创建/连接/执行/暂停/终止沙箱、管理持久化 Volume、创建沙箱时挂载 Volume、浏览和管理沙箱文件、上传或下载文件、查看端口地址和监控指标、管理快照与模板,以及在 Claude Code、Codex、Gemini 等 Agent 中安装本技能。 --- # UCloud Sandbox CLI -使用 `ucloud-sandbox-cli` 管理 UCloud Sandbox 沙箱、快照和模板。优先用 CLI 完成操作;如果用户只是在询问命令,给出可复制的命令即可。 +使用 `ucloud-sandbox-cli` 管理 UCloud Sandbox 沙箱、持久化 Volume、快照和模板。优先用 CLI 完成操作;如果用户只是在询问命令,给出可复制的命令即可。 ## 前置检查:公网权限 @@ -275,7 +275,7 @@ ucloud-sandbox-cli logout - 不要执行 `ucloud-sandbox-cli login` 或 `ucloud-sandbox-cli region`;让用户在真实终端执行登录,Agent 只通过修改已有配置文件切换地域。 - 当本地没有 API Key 配置时,不要向用户索取 API Key 并代写配置;提示用户在真实终端运行 `ucloud-sandbox-cli login`,API Key 从星图平台 Key 管理获取。 - 需要解析列表时优先用 `--format json` 或 `-f json`。 -- 执行破坏性命令前先确认用户意图:`sandbox kill`、`sandbox kill --all`、`fs rm`、`snapshot delete`、`template delete`、`template publish --unpublish`。 +- 执行破坏性命令前先确认用户意图:`sandbox kill`、`sandbox kill --all`、`volume delete`、`fs rm`、`snapshot delete`、`template delete`、`template publish --unpublish`。 - 不要在回复、日志或命令输出中泄露 API Key;读取 `~/.ucloud-sandbox-cli/config.json` 时必须隐藏 `api_key`,只展示地域、域名等必要字段。 - 用户要打开交互式终端时,建议让用户在真实终端中运行 `sandbox connect`。 @@ -290,10 +290,21 @@ ucloud-sandbox-cli sbx cr base --detach # 指定超时时间,单位秒 ucloud-sandbox-cli sandbox create base --timeout 3600 --detach + +# 按名称挂载一个持久化 Volume +ucloud-sandbox-cli sandbox create base --mount workspace:/data --detach + +# 重复 --mount 可挂载多个 Volume +ucloud-sandbox-cli sandbox create base \ + --mount workspace:/data \ + --mount model-cache:/cache \ + --detach ``` 常见模板:`base`、`code-interpreter-v1`、`desktop`,也可以使用用户自己的模板 ID 或名称。 +`--mount` 的格式是 `:`。左侧必须是 Volume 名称而不是 Volume ID,右侧必须是沙箱内的绝对路径;需要多个挂载时重复传入 `--mount`。 + 列出沙箱: ```bash @@ -339,6 +350,37 @@ ucloud-sandbox-cli sandbox kill ucloud-sandbox-cli sandbox kill --all --state running ``` +## Volume 常用操作 + +创建持久化 Volume;参数是 Volume 名称: + +```bash +ucloud-sandbox-cli volume create workspace +ucloud-sandbox-cli vol cr workspace +``` + +列出 Volume。需要稳定解析名称和 ID 时使用 JSON 格式: + +```bash +ucloud-sandbox-cli volume list +ucloud-sandbox-cli volume list --format json +ucloud-sandbox-cli vol ls -f json +``` + +删除 Volume 时使用 `volume list` 返回的 Volume ID,而不是名称。删除属于破坏性操作,执行前确认目标 ID;可以一次删除多个: + +```bash +ucloud-sandbox-cli volume delete +ucloud-sandbox-cli vol dl +``` + +当前 CLI 只提供 Volume 的创建、列表、删除和沙箱挂载,不提供直接操作 Volume 内文件或目录的命令。需要访问内容时,先创建沙箱并用 Volume 名称挂载,再通过挂载路径执行 `sandbox exec` 或 `fs` 命令: + +```bash +ucloud-sandbox-cli sandbox create base --mount workspace:/data --detach +ucloud-sandbox-cli sandbox exec "ls -la /data" +``` + ## 文件系统常用操作 文件系统命令统一使用 `ucloud-sandbox-cli fs`。除 `cp` 外,第一个参数都是沙箱 ID;路径可以是沙箱内的绝对路径或相对路径。 @@ -527,6 +569,14 @@ ucloud-sandbox-cli sandbox exec "echo hello from sandbox" ucloud-sandbox-cli sandbox kill ``` +创建并挂载持久化 Volume: + +```bash +ucloud-sandbox-cli volume create workspace +ucloud-sandbox-cli sandbox create base --mount workspace:/data --detach +ucloud-sandbox-cli sandbox exec "touch /data/example.txt && ls -la /data" +``` + 保存沙箱状态并复用: ```bash @@ -553,6 +603,7 @@ ucloud-sandbox-cli sandbox create --detach | 创建沙箱后卡在终端 | Agent/CI 中使用 `sandbox create ... --detach` | | `template not found` | 运行 `template list --format json` 确认模板 ID/名称 | | `sandbox not found` | 运行 `sandbox list --format json` 确认沙箱仍在运行 | +| 挂载 Volume 失败或 Volume 不存在 | 运行 `volume list --format json` 确认 Volume 名称;`--mount` 使用名称,不使用 Volume ID | | metrics 时间参数不识别 | 使用 `--since 1h`、`--start "2026-06-23 12:00"` 这类格式 | 更多 CLI 用法参考:`https://astraflow.ucloud.cn/docs/agent-sandbox/product/cli`。 diff --git a/skills/ucloud-sandbox/references/windows.md b/skills/ucloud-sandbox/references/windows.md index f3d5b29..4e6d8fd 100644 --- a/skills/ucloud-sandbox/references/windows.md +++ b/skills/ucloud-sandbox/references/windows.md @@ -150,13 +150,15 @@ $Summary | ConvertTo-Json -Depth 10 ## PowerShell 调用 -- 变量后紧跟远端端点冒号时写成 `${SandboxId}:/path`,避免 PowerShell 把冒号解析为变量作用域。 +- 变量后紧跟远端端点或 Volume 挂载参数的冒号时,写成 `${SandboxId}:/path` 或 `${VolumeName}:/path`,避免 PowerShell 把冒号解析为变量作用域。 - 包含 `$`、`$()` 或多行 Shell 的远端命令使用单引号 here-string,并把 CRLF 转换为 LF。 ```powershell $SandboxId = "" +$VolumeName = "workspace" ucloud-sandbox-cli sandbox exec $SandboxId "pwd && ls -la" ucloud-sandbox-cli fs cp "C:\work\index.html" "${SandboxId}:/home/user/app/index.html" +ucloud-sandbox-cli sandbox create base --mount "${VolumeName}:/data" --detach $RemoteCommand = @' printf 'HOME=%s\n' "$HOME" diff --git a/submodules/e2b b/submodules/e2b index 761ee5e..11912ff 160000 --- a/submodules/e2b +++ b/submodules/e2b @@ -1 +1 @@ -Subproject commit 761ee5ebf9e47157bb98947034fe8d4dc6b31362 +Subproject commit 11912ffa0473994c081d2b1a8baf16033ef558e2 diff --git a/submodules/ucloud-sandbox-sdk-go b/submodules/ucloud-sandbox-sdk-go index 905601a..8dd5990 160000 --- a/submodules/ucloud-sandbox-sdk-go +++ b/submodules/ucloud-sandbox-sdk-go @@ -1 +1 @@ -Subproject commit 905601aa3144172626f772b31f725cfb1d7b3577 +Subproject commit 8dd599016a25ee5d2dd496f1b4a8998f0f65bf45