如何开通第一个 Cua Cloud Fleet:申领 Linux 桌面、运行命令、保存截图并清理云端资源?
2026/9/13 22:39:06 网站建设 项目流程

如何开通第一个 Cua Cloud Fleet:申领 Linux 桌面、运行命令、保存截图并清理云端资源?

【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cua

这篇文章对应 Cua 仓库中的教程 Your first Cloud Fleet:在run.cua.ai上开通一个单沙箱的Cloud Fleet,申领其中的 Linux 桌面,在桌面里执行uname -a,把桌面截图保存到本地,最后删除 Fleet 释放云端资源。Cua Sandbox SDK 把 Fleet 表示为一个Pool,而一块活跃桌面是该池上的一次 claim(申领)。

开始前需要准备三样东西:Python>=3.11,<3.14、uv 运行时,以及一个有权限管理 sandbox pool 的 Fleet 凭据。注意:replicas=1的 pool 在你删除它之前会一直保持一个云端沙箱处于 warm 状态,云端资源可能产生计费,所以本文最后一步清理是必做项。

准备环境并创建 Fleet API key

Fleet SDK 接受 OAuth 用户 API key 或 Fleet bearer access token,但cua auth login保存的交互式 CLI 会话不会导出这两种凭据给 SDK 使用,不能代替下面的 key 创建步骤(参见 Set up Fleet credentials)。

在浏览器中完成 API key 创建:

  1. 登录 Cua Fleet(https://run.cua.ai),在导航中打开API keys页面。
  2. Create API key下输入一个描述性名称,例如first-fleet-tutorialAllowed Namespaces (optional)保持默认的All namespaces (no restriction),因为本教程要创建一个全新的 pool namespace。
  3. 选择Create key,在API key created弹窗中把Client IDClient Secret复制到你的密钥管理器,再点I have copied the credentials——Client Secret 只显示这一次。

如果无法登录、页面提示API keys are unavailable或创建被拒绝,先联系 Cua 支持确认账号访问权限,再继续。另外 pool 创建可能要求账号已绑定支付方式:如果 Fleet 显示Payment method required,进入SettingsAdd payment method添加,并先确认计价条款,因为本教程会创建可计费资源。

拿到 key 后,在同一个 shell 中导出环境变量。CUA_CLIENT_IDCUA_CLIENT_SECRET分别替换为你刚才复制的 Client ID 与 Client Secret;CUA_TOKEN_URLrun.cua.ai的默认 token 端点(控制台弹窗中不会显示它):

export CUA_CLIENT_ID="<your-client-id>" export CUA_CLIENT_SECRET="<your-client-secret>" export CUA_TOKEN_URL="https://auth.cua.ai/realms/cyclops-cs/protocol/openid-connect/token" unset FLEETS_TOKEN

FLEETS_TOKEN的优先级高于 client credentials,如果你已经有一个有效的 Fleet access token,也可以只设置FLEETS_TOKEN并在整个清理流程结束前保持它有效;改用 user key 时务必unset它。

先做只读访问检查

在同一 shell 里用下面这条检查命令验证凭据:它用 user key 换一个短期 access token,然后请求GET /api/namespaces,只打印返回的 namespace 数量:

uv run --with 'httpx>=0.27,<1' python - <<'PY' import os import httpx with httpx.Client(timeout=30) as client: token_response = client.post( os.environ["CUA_TOKEN_URL"], auth=(os.environ["CUA_CLIENT_ID"], os.environ["CUA_CLIENT_SECRET"]), data={"grant_type": "client_credentials"}, ) token_response.raise_for_status() access_token = token_response.json()["access_token"] response = client.get( "https://run.cua.ai/api/namespaces", headers={"Authorization": f"Bearer {access_token}"}, ) response.raise_for_status() print(f"Fleet access verified: {len(response.json())} namespace(s).") PY

成功时输出形如Fleet access verified: 0 namespace(s).(文档示例;数量取决于你的账号)。返回零个 namespace 对没有 pool 的账号是合法的。这一步只验证了认证和 namespace 列表读取,pool 创建仍然取决于账号权限、准入规则和资源可用性。如果 token 请求失败,检查 client ID、secret 和 token 端点;如果 Fleet 请求返回401403,先解决账号访问问题再去开通资源。

创建工作目录并保存 Fleet 脚本

为这次运行单独建一个目录,脚本和它的输出都放在里面:

mkdir first-cloud-fleet cd first-cloud-fleet

脚本会先预留一个随机命名的新 namespace(前缀first-fleet-),再创建 pool。预留使用独占创建:cloud-fleet-run.json已存在时直接报错而不是覆盖,遇到已存在的 namespace 名称会拒绝而不是修改它。namespace 名称和创建时间戳保存在cloud-fleet-run.json中——不要编辑这个记录,也不要把这个 namespace 复用到其他工作,因为清理会删除该 namespace 及其全部资源。

把教程中的完整脚本保存为first_cloud_fleet.py(内联元数据声明了 Python 版本与固定依赖版本):

# /// script # requires-python = ">=3.11,<3.14" # dependencies = [ # "cua-sandbox==0.4.3", # "cua-fleet==0.1.14", # ] # /// import asyncio import json import os import sys from pathlib import Path from uuid import uuid4 from cua_sandbox import Image, Pool from fleet_sdk import ( CyclopsClient, CyclopsConfiguration, CyclopsCredentials, CyclopsTokenProviderConfiguration, ) IMAGE = ( "public.ecr.aws/k5j5w0x5/cua-ubuntu-24.04" "@sha256:c1e601dbb748fdc467c663136f7592e308a91a3c19c309b75261544432826a57" ) RECORD = Path("cloud-fleet-run.json") def fleet_client(): configuration = dict( base_url=os.environ.get("CUA_FLEET_BASE_URL", "https://run.cua.ai"), pool_poll_interval_ms=2000, pool_poll_limit=300, claim_poll_interval_ms=2000, claim_poll_limit=300, ) token = os.environ.get("FLEETS_TOKEN") if token: return CyclopsClient.connect_with_access_token_and_native_http_client( CyclopsTokenProviderConfiguration(**configuration), token ) return CyclopsClient.connect_with_native_http_client(CyclopsConfiguration( **configuration, token_url=os.environ["CUA_TOKEN_URL"], credentials=CyclopsCredentials( os.environ["CUA_CLIENT_ID"], os.environ["CUA_CLIENT_SECRET"] ), )) async def find_namespace(client, name): # A direct lookup outside an owned namespace can return 403, even when # the resource does not exist. Use a successful account inventory instead. namespaces = await client.list_namespaces() return next((item for item in namespaces if item.name == name), None) async def delete_namespace(client, name, created_at): current = await find_namespace(client, name) if current is None: print(f"Namespace no longer in account inventory: {name}") return if not created_at or current.created_at != created_at: raise RuntimeError("Namespace identity changed; refusing cleanup.") await client.delete_namespace(name) for _ in range(60): if await find_namespace(client, name) is None: print(f"Namespace no longer in account inventory: {name}") return await asyncio.sleep(2) raise TimeoutError("Namespace is still visible. Retry cleanup after it terminates.") async def cleanup(): record = json.loads(RECORD.read_text()) if not record.get("created_at"): raise RuntimeError( "Creation was not confirmed. Check this run's name in Fleet before deleting anything." ) await delete_namespace(fleet_client(), record["name"], record["created_at"]) async def run(): pool_name = f"first-fleet-{uuid4().hex}" # Exclusive creation prevents overwriting an earlier run's cleanup record. with RECORD.open("x") as output: json.dump({"name": pool_name, "created_at": None}, output) client = fleet_client() # Only HTTP 201 confirms reservation. A collision (409) or denied request # stops here, without reconciling or deleting another namespace's resources. namespace = await client.create_namespace(pool_name) try: RECORD.write_text(json.dumps({ "name": pool_name, "created_at": namespace.created_at, })) if not namespace.created_at: raise RuntimeError("Namespace creation timestamp missing; inspect Fleet before cleanup.") print(f"Provisioning Cloud Fleet: {pool_name}") pool = await Pool.apply( Image.from_registry(IMAGE), name=pool_name, replicas=1, cpu=4, memory_mb=4096, services={"server": 8000}, ttl_seconds_after_created=3600, ) async with pool.claim( name="first-claim", service="server", time_to_start=900, ) as sandbox: print(f"Connected to sandbox: {sandbox.name}") result = await sandbox.shell.run("uname -a") if not result.success: raise RuntimeError(result.stderr) print(result.stdout.strip()) screenshot = Path("cloud-fleet.png") screenshot.write_bytes(await sandbox.screenshot()) print(f"Screenshot saved to {screenshot.resolve()}") finally: print(f"Deleting Cloud Fleet: {pool_name}") await delete_namespace(client, pool_name, namespace.created_at) if __name__ == "__main__": if sys.argv[1:] == ["run"]: asyncio.run(run()) elif sys.argv[1:] == ["cleanup"]: asyncio.run(cleanup()) else: raise SystemExit("Usage: first_cloud_fleet.py run|cleanup")

脚本里几个影响运行的取值:镜像是固定 digest 的cua-ubuntu-24.04;pool 参数为replicas=1cpu=4memory_mb=4096、服务端口{"server": 8000}ttl_seconds_after_created=3600是一小时的 pool TTL,作为清理失败时的兜底过期;claim 的time_to_start=900是沙箱就绪等待上限。脚本同时提供runcleanup两个子命令,cleanup只读本地记录并删除 namespace。

运行脚本并核对结果

在同一 shell、同一目录下运行:

uv run first_cloud_fleet.py run

uv会读取文件头部的内联元数据,在隔离环境中安装cua-sandbox==0.4.3及其要求的 Fleet SDKcua-fleet==0.1.14。首次运行可能需要几分钟,期间 Fleet 在云端开通 Linux 沙箱。成功时的判断依据:

  • 终端打印出 sandbox 名称和uname -a输出的 Linux 内核信息(shell.run返回success=False时脚本会抛出RuntimeError,并仍然进入清理);
  • 当前目录生成cloud-fleet.png,终端打印Screenshot saved to <绝对路径>(文档示例);
  • claim 代码块退出后,SDK 释放已申领的沙箱;finally块请求删除预留的 namespace,其中包含 pool、template 和 sandbox 资源,并轮询账号清单直到该 namespace 不再出现。

这里要注意两个边界:403不被解释为“资源已不存在”;删除请求本身不能证明所有资源已经终止。如果清理失败或超时,按下一节的流程处理,并到 Fleet 控制台核对这次运行留下的资源。

这次运行实际发生了什么:Fleet SDK 为本次运行独占预留了一个 namespace;Pool.apply()在其中创建命名 pool 和 Linux 沙箱 template;pool.claim()预留一个沙箱并把 SDK 连接到它的server服务(端口8000)。这个server服务是沙箱的 computer server,不是 Cua Driver MCP 端点;shell 与截图调用都指向被申领的云端桌面。脚本本身只做连接检查和保存图片,不运行 AI agent,也不连接本地 Cua Driver 会话。

中断运行后的清理

如果进程在开通之后被杀掉或丢失连接,finally块可能没有执行到 Fleet。恢复网络和对同一账号的有效 Fleet 凭据后,在同一个目录下运行只删除不执行的命令:

uv run first_cloud_fleet.py cleanup

它读取cloud-fleet-run.json,确认记录的 namespace 仍带有相同的创建时间戳后才删除;它不会调用Pool.apply()、不会 claim 沙箱、不会重复执行工作负载,也能处理“预留了 namespace 但创建 pool 之前就失败”的情况。全程使用同一个账号:另一个账号清单里看不到该 namespace 不能当作清理成功的证据。删除会销毁沙箱及其全部状态,需要保留的远端文件请在清理前取回;脚本确认的是账号清单中 namespace 消失,不是对底层 guest 终止的独立检查。

两种会拒绝删除的情形:

  • 记录里没有创建时间戳(created_at为空):说明 namespace 预留从未确认,可能源于名称冲突(409)、请求被拒或响应中断。此时命令拒绝删除。正确做法是到 Fleet UI 里找到记录中的确切名称,人工核实这次运行是否创建了任何资源,再决定是否移除。不要为了绕开访问或计费错误去删除一个已存在的 pool。
  • namespace 身份变化(当前created_at与记录不一致):脚本抛出Namespace identity changed; refusing cleanup.,说明该名称下现在指向另一个 namespace,同样不能直接删。

本地保留cloud-fleet.png作为这次运行的结果。要再跑一次,请换一个新目录,让前一次的清理记录仍然可用。

常见失败与边界

以下判断来自仓库中的凭据指南和 Troubleshoot Fleet pools and claims:

  • 401/ token 交换失败:确认凭据没有过期或被吊销,且进程使用的是预期的 Fleet 与 token 端点;按凭据设置流程修正后,先重试对已存在资源的查询再尝试创建。
  • 403且响应体提示需要支付方式:需要账号所有者处理计费设置,换一个 pool 名称或反复重试都无法满足该要求。
  • 403访问已知 pool 被拒:确认凭据属于被授权访问该 pool 的账号;不要把403解释为资源不存在或清理已成功。
  • claim 迟迟不就绪:在 Fleet 控制台用记录的 namespace 和 claim 名依次核对 pool、template、claim 三个阶段。缩容到零的 pool 可能需要冷启动;template 缺失、镜像不可用或容量耗尽,不能靠延长 guest 服务超时解决。Python 示例预期server服务监听端口8000
  • 名称本地校验错误:pool/namespace 名用小写 DNS label(小写字母、数字、连字符,首尾为字母或数字,最长 63 字符)。

下一步

教程给出的延伸路径(均在仓库文档中):

  • Create a reusable sandbox pool with Python
  • Configure a sandbox pool with Terraform
  • Expire pools and claims automatically
  • Choose a Fleet image
  • Sandbox SDK API reference

【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cua

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询