触发规则(重要)
本 SKILL 有两类触发方式,请按优先级理解:
强触发(最高优先级)
只要用户消息中包含"优测设备"这一前缀关键词(无论后面接什么内容,如"优测设备帮我截图"、"优测设备连接一下 xxx"、"优测设备跑一下自动化脚本"等),必须调用本 SKILL。
- 这是用户显式表明使用优测云真机(SAAS 通路)意图的强信号;
- 在多 SKILL 环境中具备最高优先级,用于避免与其他"设备/自动化/移动端/浏览器"类 SKILL 相互冲突或误召回;
- 命中该前缀后:直接进入下方「运行逻辑」的第一步开始处理,无需额外判断。
常规召回(保持兼容)
用户没有使用"优测设备"前缀,但描述涉及以下场景时,本 SKILL 仍可被正常召回:
- 手机设备操作 / 云真机 / 云手机自动化
- 设备列表 / 连接设备 / 断开设备 / 续期设备
- 截图 / Base64 截图 / 屏幕录制 / 视频流
- 点击(UI / OCR / CV / 坐标)/ 长按 / 滑动 / 输入文本 / 按键操作
- UI 查找(XPath / OCR)/ 获取 UI 控件树
- 应用管理(安装 URL / 安装本地 / 卸载 / 启动 / 停止 / 前台 / 运行列表)
- 剪贴板设置与获取
- ADB 命令 / ADB 远程调试 / Logcat 日志采集 / 性能采集 / ANR / Crash 监控
- 官网 OpenAPI / SAAS 三元组认证(user_token + project_token + group_id)
这种情况下,你需要根据上下文判断是否真的是"优测云真机 SAAS 通路"场景(例如用户之前配置过 UBOX_USER_TOKEN / UBOX_PROJECT_TOKEN / UBOX_GROUP_ID);若不确定,可以先向用户确认。
适用场景
本 SKILL 面向优测官网 OpenAPI(SAAS) 通路——所有流量走 cloudphone-labpaas-adapter 网关。
与 PAAS 版本的区别:
- 认证方式:不再使用单个 UBOX_AUTH_TOKEN,而是使用 三元组:user_token + project_token + group_id(团队 ID)。
- 网关:所有请求经由 utest-api.21kunpeng.com/cloudphone-labpaas-adapter/*。
- 运行模式:CLI 内部使用 SAAS_CLI 模式,无自动续期、无自动释放,操作结束需手动释放设备。
- 命令区分:每一条 SAAS 命令都必须显式带 --mode saas 以与 PAAS 通路严格区分(--mode 加在主命令 ubox-cli 之后、命令组之前)。
- 其它所有命令名称、参数与 PAAS 版本完全对齐。
如果用户是通过内部 PAAS 通路接入(使用 auth_token),请使用 ../SKILL.md。
运行逻辑
第 0 步:SKILL 触发首屏提示(能力介绍 + 官方文档,每次触发都必须最先输出)
> 强制指令:本 SKILL 一旦被触发(无论是"优测设备"强前缀触发、还是常规能力关键词召回),在执行任何环境探测、安装、鉴权、CLI 命令之前,都必须先向用户原样输出下方 Markdown 提示卡片(一字不差,禁止缩减、翻译、改写、合并到其它段落)。用于让用户第一时间明确:本 SKILL 能做什么、Token 从哪里申请、云真机资源如何购买。
>
> 输出时机:
> - 触发后的第一条助手回复中就必须出现这段提示;
> - 后续同一对话内若用户又发起新的优测设备任务,无需重复输出(避免刷屏),只需在首次触发时给出一次;
> - 若用户在同一对话中显式说"再介绍一下能力"、"文档链接是什么"等,可以再次输出该卡片。
>
> 输出内容(原文照抄):
>
> ---
>
> 👋 已为您唤起 优测云真机 SKILL(SAAS 版),可为您提供以下能力:
>
> - 📱 设备管理:可用设备列表(Android / iOS / 鸿蒙 HarmonyOS / HarmonyOS NEXT)、连接、断开、状态查询、硬件信息
> - 📸 截图与录屏:本地截图 / Base64 截图 / 屏幕录制 / 实时视频流播放链接
> - 🖱️ UI 自动化交互:智能点击(XPath / OCR / 图像模板 / 坐标)、长按、滑动、文本输入、按键(HOME / BACK / 音量 / 电源 等)
> - 🔎 UI 查找:XPath 控件定位、OCR 文字定位、UI 控件树导出(JSON / XML)
> - 📦 应用管理:通过 URL / 本地文件安装、卸载、启动、停止、前台应用、运行中应用列表
> - 📋 剪贴板:读取 / 写入设备剪贴板
> - 🛠️ 调试诊断:ADB 远程调试、Logcat 日志采集、性能采集、ANR / Crash 监控
>
> 🔑 Token 申请 & 云真机资源购买:本 SKILL 走优测官网 OpenAPI(SAAS)通路,需要 user_token + project_token + group_id 三元组鉴权。权限申请方式、Token 获取、云真机资源购买与计费等,请统一参考优测官方文档:
>
> 👉 https://utest.21kunpeng.com/home/topic/ubox0811
>
> 💡 自动引导能力:若检测到您尚未配置三元组(首次使用),我会优先使用当前 Agent 内置浏览器为您打开上方权限申请页;若当前 Agent 不支持内部浏览器访问,则会自动唤起您本地 PC 的默认浏览器打开该链接。若设备连接因余额不足/欠费失败,我会按相同策略为您打开云真机充值页(https://utest.21kunpeng.com/workbench/resource/payment/PayDetailed/1002)。
>
> ---
>
> 输出完上述卡片后,继续按下方「首次安装前置提示」→「第一步 / 第二步 / ...」正常执行用户的实际请求,无需等待用户确认。
第 0.5 步:自动打开引导 URL 的通用规则(🆕 强制 · 二级策略)
> 动机:workBuddy / CodeBuddy / Cursor / Windsurf 等宿主 Agent 大多提供「在内置浏览器 / 侧栏 WebView / 内嵌预览面板中打开 URL」的能力;即便宿主不提供,用户所在的本地 PC 也一定有默认浏览器(Chrome / Edge / Safari / Firefox)。SKILL 在两个关键场景下——(A)首次使用尚未配置三元组 与 (B)设备连接因欠费/余额不足失败——必须主动把对应引导页直接展示给用户,而不是只在对话里贴一个链接让用户手动复制粘贴。这样能显著降低用户跳出成本、提高 Token 申请与充值转化率。
> 本节只定义"如何自动打开一个 URL"的通用动作;具体在什么条件下、打开哪个 URL,由「3.2.0 首次使用自动打开权限申请页」和「第五步 5.2 计费失败/余额不足自动引导购买页」两处分别指定。
核心策略(二级兜底 · LLM 必读一句话总结):
> 优先使用当前 Agent 自带的内部浏览器打开目标 URL;若当前 Agent 不支持内部浏览器访问,则自动唤起用户本地 PC 的默认浏览器打开;两者都不可用时,才在对话中以 Markdown 链接形式展示,明确告知用户需手动点击。
执行顺序(LLM 必须严格按此顺序尝试,前一档命中即停止,不再往下降级):
【第 1 档 · 首选】Agent 内置浏览器(内部预览 / 侧栏 WebView / 内嵌标签页)
识别并调用当前宿主 Agent 提供的「打开 URL / 打开浏览器 / 打开预览 / 内嵌 WebView」类工具或函数(不同宿主命名不一,能识别哪种就用哪种):
open_browser(url=...)/open_url(url=...)/browser.open(url=...)preview_url(url=...)/open_preview(url=...)/preview.show(url=...)webview.open(url=...)/open_in_side_panel(url=...)/open_in_editor(url=...)- MCP
browser/playwright/puppeteer类 server 的navigate/open/goto工具 - WorkBuddy 宿主专属:
present_files(files=["<TARGET_URL>"])—— WorkBuddy 内部打开 URL 的正确工具。其名称不含browser/preview等关键字,但传入 http/https URL 时会在内置浏览器预览面板打开,属于第 1 档,必须显式识别,不得降级到第 2 档open - 任何名称含
browser/preview/webview/open_url/open_link关键字的可用工具 - 任何能接收 URL 参数并在宿主 UI 内展示的工具(如
present_files、preview类),即使名称不含上述关键字,也按第 1 档处理
行为约定:
- 只要上述任意一个工具在当前会话中可被调用,就立即调用它打开目标 URL,不再往下降级;
- 无需再向用户征询同意——用户在此场景下的核心诉求就是"知道去哪里申请 Token / 充值",主动打开是符合预期的辅助行为;
- 调用成功后,仍需在对话中把该 URL 以 Markdown 链接形式显式贴出(便于内嵌页出问题时手动兜底),并附加对应场景的引导话术。
【第 2 档 · 次选】本地 PC 默认浏览器(当第 1 档不可用时)
若在当前会话工具列表中未找到任何内嵌浏览器 / URL 打开类工具,说明当前 Agent 不支持内部浏览器访问 —— 此时必须通过 shell 命令唤起用户本地 PC 的默认浏览器打开目标 URL。执行前先识别宿主操作系统(macOS / Linux / Windows),再选对应命令:
macOS:
open "<TARGET_URL>"
Linux(含 WSL):
xdg-open "<TARGET_URL>" >/dev/null 2>&1 &
Windows(CMD):
start "" "<TARGET_URL>"
Windows(PowerShell):
Start-Process "<TARGET_URL>"
跨平台一行式兜底(当无法准确判断 OS 时使用,会依次尝试 macOS/Linux/Windows):
(command -v open >/dev/null 2>&1 && open "<TARGET_URL>") || \
(command -v xdg-open >/dev/null 2>&1 && xdg-open "<TARGET_URL>" >/dev/null 2>&1 &) || \
(command -v cmd.exe >/dev/null 2>&1 && cmd.exe /c start "" "<TARGET_URL>") || \
echo "AUTO_OPEN_FAILED"
行为约定:
- 命令返回码为 0 或未输出 AUTO_OPEN_FAILED 即视为已成功唤起本地默认浏览器,此时应向用户简报「已在您本地 PC 的默认浏览器中打开」;
- 若命令报错(例如沙箱禁止执行 shell、open / xdg-open / start 都不可用),才降级到第 3 档;
- 沙箱类环境(部分 CI / 无头容器)通常不允许调用 GUI 应用,此时命令会失败或静默 no-op,需要观察输出判断。
【第 3 档 · 最后兜底】对话中展示 Markdown 链接(前两档都不可用时)
当第 1 档、第 2 档均确认不可用时,才降级为在对话正文中以 Markdown 链接形式清晰展示 URL,并必须附加以下明确提示(避免用户误以为链接已被自动打开):
> ⚠️ 当前运行环境未提供自动打开浏览器的能力(既没有 Agent 内置浏览器,也无法唤起本地 PC 默认浏览器),请您手动点击下方链接,或复制到浏览器地址栏访问:
>
> 👉 <TARGET_URL>
通用后置动作(无论最终走到哪一档):
执行完打开动作后,都必须继续在对话中把该 URL 用 Markdown 链接形式显式贴出(便于自动打开失败时手动兜底),并附加对应场景的引导话术(由 3.2.0 或 5.2 提供)。
幂等 / 频次约束:
- 同一场景(例如"首次使用打开权限申请页")在同一对话内只需自动打开一次;用户若明确说"再打开一下"、"我没看到",才可以再次触发。
- 两个场景(A 权限申请 / B 充值购买)互相独立,各自最多触发一次。
- 用户明确说"不用打开"、"我自己去访问",立即停止自动打开动作,仅在对话里贴链接即可。
首次安装前置提示(重要)
在执行下方"第一步 / 第二步"之前,必须先做一次"首次安装"检测:
-
每次调用本 SKILL 都必须先执行下面的"环境 + 版本一体化探测"——不能因为"上次已经装过"就跳过。原因:
references/下的 CLI/SDK 版本会随 SKILL 迭代持续升级;一旦本地已装版本落后于references/声明版本,新特性(如--client-source/--client-label参数、X-Ubox-*埋点 header 全局注入等)就会静默失效,请求最终会被服务端按兜底路径归类,出现"命令跑成功但埋点渠道错"的隐性 bug。 -
依次运行下面的探测脚本,脚本会同时判断"是否安装"与"版本是否 ≥ references/ 声明版本",最终输出唯一状态值
SETUP_STATUS:OK/NEED_UV/NEED_INSTALL/NEED_UPGRADE。
macOS / Linux(bash / zsh):
bash
# 1) 先探测 uv
if ! command -v uv >/dev/null 2>&1; then
echo "SETUP_STATUS=NEED_UV"
# 2) 再探测 ubox-cli(未安装 → NEED_INSTALL)
elif ! command -v ubox-cli >/dev/null 2>&1; then
echo "SETUP_STATUS=NEED_INSTALL"
else
# 3) 已安装:跳到第二步的 2.2 设置 SKILL_DIR,再回来执行 2.1 版本比较脚本
# 2.1 脚本会输出 NEED_UPGRADE=yes/no。请把该结果映射到 SETUP_STATUS:
# NEED_UPGRADE=yes → SETUP_STATUS=NEED_UPGRADE
# NEED_UPGRADE=no → SETUP_STATUS=OK
echo "SETUP_STATUS=CHECK_VERSION # 请继续执行 2.2 + 2.1 得到最终结论"
fi
Windows(PowerShell):
powershell
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
"SETUP_STATUS=NEED_UV"
} elseif (-not (Get-Command ubox-cli -ErrorAction SilentlyContinue)) {
"SETUP_STATUS=NEED_INSTALL"
} else {
"SETUP_STATUS=CHECK_VERSION # 请继续执行 2.2 + 2.1 得到最终结论"
}
- 强制流程(禁止绕过版本校验):
SETUP_STATUS=NEED_UV→ 先执行第一步安装 uv,再回到本探测;SETUP_STATUS=NEED_INSTALL→ 走第二步完整流程(2.2 → 2.3 → 2.4);SETUP_STATUS=CHECK_VERSION→ 必须继续跑 2.2 + 2.1 得到NEED_UPGRADE=yes/no,不允许因为"命令已存在"就直接跳到第三步;NEED_UPGRADE=yes→ 走 2.3 强制--reinstall覆盖升级,再跑 2.4 校验;NEED_UPGRADE=no→ 才可以进入第三步。
-
只有 2.4 校验通过(
ubox-cli version输出的版本号与references/ubox-cli/pyproject.toml声明的一致,且 2.1 复检输出NEED_UPGRADE=no),才可以进入第三步。 -
首次安装 / 需要升级时,必须先向用户输出以下提示(原文照抄,不要缩减):
> 📦 当前 SKILL 需要安装/升级优测 ubox(含 uv 运行时 + ubox-cli + ubox-py-sdk),会自动完成,请您耐心等待(通常需要 30 秒 ~ 2 分钟,取决于网络速度)。安装过程完全走本 SKILL 目录下的 references/ 本地源码 + 清华 PyPI 镜像,不需要访问任何内部源。
- 输出提示后,再按"第一步 / 第二步"依次自动执行,全程无需再向用户征询同意;仅在完成后向用户简报:"✅ 优测 ubox 已就绪(cli=<版本> sdk=<版本>),开始处理您的请求"。
按以下步骤依次执行:
第一步:检查 uv 是否安装
uv --version
- 如果
uv未安装,先安装 uv:
macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows(PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
第二步:从本地源码安装 ubox-cli(SAAS 版本强制本地安装)
> ⚠️ 与 PAAS 版本的关键区别:SAAS 版本不使用 PyPI/私有源在线安装,必须从本 SKILL 目录下的 references/ 文件夹进行本地源码安装。这是因为部分外部用户所在网络环境无法访问 PyPI/内部源,本地源码安装可确保开箱即用。
>
> references/ 目录结构:
> > ubox-skill/SAAS/utest-ubox-skill/references/
> ├── ubox-cli/ # ubox-cli 源码(本 CLI)
> │ ├── pyproject.toml
> │ └── src/ubox_cli/
> └── ubox-py-sdk/ # ubox-py-sdk 源码(CLI 依赖的底层 SDK)
> ├── pyproject.toml
> └── src/ubox_py_sdk/
>
2.1 检查已安装版本,按需自动升级(每次调用 SKILL 都必须执行,不可跳过)
> ⛔ 强制指令:本节是唯一的版本校验入口。不允许出现"因为 ubox-cli 命令已经存在就直接进入第三步"的操作路径。历史事故:SKILL v0 里存在这条捷径,导致本地已装的老版 ubox-cli(如 v0.2.3,不认识 --client-source / --client-label)持续被使用,X-Ubox-* 埋点 header 无法发送,服务端把请求错误归类为 client_source=openapi。
升级判定原则:将本地已安装的 ubox-cli 与 ubox-py-sdk 的版本号,与当前 SKILL 目录下 references/*/pyproject.toml 中声明的版本号做语义化比较——只要其中任一个包本地已装版本低于 SKILL 声明版本,或根本未安装,都必须执行 2.2 + 2.3 重新安装(--reinstall 强制覆盖)。
> 版本号统一以 references/ubox-cli/pyproject.toml 和 references/ubox-py-sdk/pyproject.toml 中的 version 字段为权威来源(__init__.py 内嵌 __version__ 仅供参考,不用于判断)。
**一行命令得出“是否需要升级”(将 $SKILL_DIR 替换为实际目录)——同时支持首次安装、版本不足、两个包版本不一致三种情形:
macOS / Linux(bash / zsh):
# 前提:已在 2.2 里 export SKILL_DIR=... (若未定义,先参照 2.2 设置)
python3 - <<'PY'
import os, re, subprocess, sys
try:
from packaging.version import Version
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "packaging"])
from packaging.version import Version
SKILL_DIR = os.environ["SKILL_DIR"]
def read_expected(pyproject_path):
with open(pyproject_path, encoding="utf-8") as f:
m = re.search(r'^version\s*=\s*"([^"]+)"', f.read(), re.M)
return m.group(1) if m else None
def read_installed(pkg):
"""读取 `uv tool install` 后 ubox-cli venv 中 `pkg` 的真实已装版本(本地文件系统解析,不联网)。"""
try:
tool_root = subprocess.check_output(
["uv", "tool", "dir"], stderr=subprocess.DEVNULL, text=True, timeout=15,
).strip()
cand = [
os.path.join(tool_root, "ubox-cli", "bin", "python"), # macOS/Linux
os.path.join(tool_root, "ubox-cli", "bin", "python3"), # macOS/Linux fallback
os.path.join(tool_root, "ubox-cli", "Scripts", "python.exe"), # Windows
]
py = next((p for p in cand if os.path.exists(p)), None)
if not py:
return None
out = subprocess.check_output(
[py, "-c",
f"from importlib.metadata import version; print(version('{pkg}'))"],
stderr=subprocess.DEVNULL, text=True, timeout=15,
).strip()
return out or None
except Exception:
return None
exp_cli = read_expected(f"{SKILL_DIR}/references/ubox-cli/pyproject.toml")
exp_sdk = read_expected(f"{SKILL_DIR}/references/ubox-py-sdk/pyproject.toml")
inst_cli = read_installed("ubox-cli")
inst_sdk = read_installed("ubox-py-sdk")
need = (
inst_cli is None or inst_sdk is None or
Version(inst_cli) < Version(exp_cli) or
Version(inst_sdk) < Version(exp_sdk)
)
print(f"expected: cli={exp_cli} sdk={exp_sdk}")
print(f"installed: cli={inst_cli or 'NOT_INSTALLED'} sdk={inst_sdk or 'NOT_INSTALLED'}")
print(f"NEED_UPGRADE={'yes' if need else 'no'}")
PY
Windows(PowerShell):
# 前提:已在 2.2 里 $env:SKILL_DIR = ... (若未定义,先参照 2.2 设置)
@"
import os, re, subprocess, sys
try:
from packaging.version import Version
except ImportError:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'packaging'])
from packaging.version import Version
SKILL_DIR = os.environ['SKILL_DIR']
def read_expected(p):
with open(p, encoding='utf-8') as f:
m = re.search(r'^version\s*=\s*"([^"]+)"', f.read(), re.M)
return m.group(1) if m else None
def read_installed(pkg):
"""读取 uv tool venv 中 pkg 的真实已装版本(本地文件系统解析,不联网)。"""
try:
tool_root = subprocess.check_output(
['uv', 'tool', 'dir'], stderr=subprocess.DEVNULL, text=True, timeout=15,
).strip()
cand = [
os.path.join(tool_root, 'ubox-cli', 'Scripts', 'python.exe'), # Windows
os.path.join(tool_root, 'ubox-cli', 'bin', 'python'), # POSIX
os.path.join(tool_root, 'ubox-cli', 'bin', 'python3'),
]
py = next((p for p in cand if os.path.exists(p)), None)
if not py:
return None
out = subprocess.check_output(
[py, '-c',
f"from importlib.metadata import version; print(version('{pkg}'))"],
stderr=subprocess.DEVNULL, text=True, timeout=15,
).strip()
return out or None
except Exception:
return None
exp_cli = read_expected(os.path.join(SKILL_DIR, 'references', 'ubox-cli', 'pyproject.toml'))
exp_sdk = read_expected(os.path.join(SKILL_DIR, 'references', 'ubox-py-sdk', 'pyproject.toml'))
inst_cli = read_installed('ubox-cli')
inst_sdk = read_installed('ubox-py-sdk')
need = (
inst_cli is None or inst_sdk is None or
Version(inst_cli) < Version(exp_cli) or
Version(inst_sdk) < Version(exp_sdk)
)
print(f'expected: cli={exp_cli} sdk={exp_sdk}')
print(f'installed: cli={inst_cli or "NOT_INSTALLED"} sdk={inst_sdk or "NOT_INSTALLED"}')
print(f"NEED_UPGRADE={'yes' if need else 'no'}")
"@ | python
判定与路径(硬门槛,禁止跳过):
- 输出中包含 NEED_UPGRADE=yes → 必须执行 2.2 + 2.3(安装命令自带 --reinstall,既支持首次安装也支持升级覆盖),完成后必须回到 2.1 再跑一次,直到输出 NEED_UPGRADE=no 才能继续;
- 输出中包含 NEED_UPGRADE=no → 才可以跳过 2.2 + 2.3,直接进入 2.4 校验。
- 禁止在 NEED_UPGRADE=yes 时直接进入第三步——那会导致老版 ubox-cli / ubox-py-sdk 继续被使用,--client-source / --client-label 参数被静默忽略,X-Ubox-* 埋点 header 无法生成。
> 为什么不硬编码阈值:references/ 下的 CLI/SDK 版本号会随 SKILL 包迭代不断提升。硬编码 >= x.y.z 会导致每次发版都需手工同步阈值到 SKILL.md,很容易遗漏从而让旧包用户无感知地继续使用旧代码。读取 pyproject.toml 后自动比较可一劳永逸避免这个问题。
2.2 定位 SKILL 所在目录(跨平台)
references/ 文件夹位于本 SKILL.md 同级目录下。请让用户在终端中进入 SKILL 目录,或者直接使用绝对路径。以下命令示例假设当前工作目录就是本 SKILL 所在目录(即 ubox-cli/ubox-skill/SAAS/utest-ubox-skill/)。
macOS / Linux(bash / zsh):
# 假设仓库根目录为 <REPO_ROOT>,例如 /Users/xxx/workspace/utest/ubox-cli
export SKILL_DIR="<REPO_ROOT>/ubox-skill/SAAS"
ls "$SKILL_DIR/references/ubox-cli/pyproject.toml" "$SKILL_DIR/references/ubox-py-sdk/pyproject.toml"
Windows(PowerShell):
# 假设仓库根目录为 <REPO_ROOT>,例如 D:\workspace\utest\ubox-cli
$env:SKILL_DIR = "<REPO_ROOT>\ubox-skill\SAAS"
Get-Item "$env:SKILL_DIR\references\ubox-cli\pyproject.toml", "$env:SKILL_DIR\references\ubox-py-sdk\pyproject.toml"
若两个 pyproject.toml 都存在,说明 references 完整,可以继续。
2.3 本地源码安装(先 SDK,后 CLI,强制 --reinstall 覆盖)
由于 ubox-cli 依赖 ubox-py-sdk==0.2.33,必须先安装 SDK,再安装 CLI。uv tool install --reinstall 支持传入本地目录路径,并可通过 --with 引入额外的本地依赖,确保依赖解析走本地源码而非 PyPI。加 --reinstall 能同时覆盖“同版本号但源码已更新”的情况,保证拿到 references/ 当前快照。
macOS / Linux(bash / zsh):
# 一步到位:--reinstall 强制从本地 references/ 完全重装(首次安装也兼容)
uv tool install --reinstall --python 3.10.12 \
--with "$SKILL_DIR/references/ubox-py-sdk" \
"$SKILL_DIR/references/ubox-cli"
Windows(PowerShell):
# 一步到位:--reinstall 强制从本地 references/ 完全重装
uv tool install --reinstall --python 3.10.12 `
--with "$env:SKILL_DIR\references\ubox-py-sdk" `
"$env:SKILL_DIR\references\ubox-cli"
说明:
- --reinstall 强制 uv 删除旧隔离环境后从 0 创建——避免“同版本号但源码已变”时 uv 误判定为“已是最新”而跳过;
- --with <local-path> 会把本地 ubox-py-sdk 目录作为 wheel 构建源装入 ubox-cli 的隔离环境,ubox-cli 声明的 ubox-py-sdk==0.2.33 依赖会被本地 SDK(版本恰为 0.2.33)满足,不会再向 PyPI 请求。
- 其余三方依赖(click / websocket-client / requests / pydantic / openai / numpy / opencv-python-headless / pillow 等)仍会从 pyproject.toml 中配置的镜像源(默认清华源)拉取。若用户处于完全离线环境,请自行准备离线 wheel 包并加 --find-links <本地wheel目录>。
2.4 校验安装(硬门槛,未通过不得进入第三步)
ubox-cli version
通过条件(三者全部满足才算通过):
1. 输出中 success: true;
2. 版本号与 references/ubox-cli/pyproject.toml 中声明的 version 完全一致;
3. 再次运行 2.1 的升级判定脚本,输出必须包含 NEED_UPGRADE=no。
未通过时的处理(禁止无视错误继续):
- 版本号仍低于 references/ 声明版本 → 说明 2.3 的 uv tool install --reinstall 未生效,请检查:
1. uv --version 是否 >= 0.4(老版本不支持 --with <path>);
2. $SKILL_DIR/references/ubox-cli 与 $SKILL_DIR/references/ubox-py-sdk 目录下的 pyproject.toml 是否存在;
3. 镜像源是否可访问(不可访问时改用离线 wheel 目录,加 --find-links <本地wheel目录>);
4. 是否存在多个 Python 解释器下的 ubox-cli(which ubox-cli / Get-Command ubox-cli 确认走的是 uv tool 的那份)。
- 排查完成后回到 2.3 重跑安装,直至 2.4 三条件全部满足;未满足前禁止进入第三步。
第三步:检查 SAAS 三元组凭证(两个 Token + 团队 ID)
SAAS 通路必须同时提供以下 3 个凭证,缺一不可:
| 凭证 | 环境变量 | 说明 | 来源 |
|---|---|---|---|
user_token |
UBOX_USER_TOKEN |
用户维度凭证 | 权限申请见:https://utest.21kunpeng.com/home/topic/ubox0811 |
project_token |
UBOX_PROJECT_TOKEN |
项目维度凭证 | 权限申请见:https://utest.21kunpeng.com/home/topic/ubox0811 |
group_id |
UBOX_GROUP_ID |
团队 ID | 登录优测网站,右上角复制(权限申请见:https://utest.21kunpeng.com/home/topic/ubox0811) |
> 📌 首次使用必读:如果您尚未获得上述任一凭证,请先访问 权限申请指引:https://utest.21kunpeng.com/home/topic/ubox0811 完成申请。该页面详细说明了 user_token / project_token 的获取方式与 group_id 的查看位置。
3.1 多来源读取凭证(🆕 解决 workBuddy 等非交互式 shell 拿不到用户环境变量的问题)
> 背景问题:workBuddy / AI Agent 沙箱等宿主为 SKILL 拉起的通常是非登录、非交互式子 shell——这类 shell 不会自动加载 ~/.zshrc / ~/.bashrc,因此即便用户此前把 UBOX_USER_TOKEN 等 export 写到了 rc 文件里,SKILL 进程 printenv 也读不到,导致每次都被要求重新提供。必须按下列顺序去多个来源主动读取,避免误判为"未配置"。
读取优先级链(前者优先,前者已配置则跳过后续来源):
1. 当前进程环境变量(宿主进程直接注入的);
2. 项目 .env 文件(当前工作目录 ./.env,其次 $SKILL_DIR/.env);
3. 用户 shell rc 文件(~/.zshrc → ~/.bash_profile → ~/.bashrc → ~/.profile,找到即停);
4. Windows 用户级环境变量注册表(PowerShell 下的 [Environment]::GetEnvironmentVariable(..., 'User'))。
统一检查脚本(macOS / Linux / WSL / Git Bash · bash/zsh 通用)——只输出脱敏结果,不打印完整 Token:
# 说明:
# 1. 先尝试从当前 env 读取;空则依次尝试 .env、~/.zshrc、~/.bash_profile、~/.bashrc、~/.profile
# 2. 读到值后就地 export,让本次 SKILL 会话及后续 ubox-cli 命令能立即使用
# 3. 输出仅显示"已配置(len=N)/未配置"以及脱敏预览,不回显完整 Token
_ubox_load_from_env_file() {
local file="$1" key="$2"
[ -f "$file" ] || return 1
local line
line=$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file" | tail -n1)
[ -z "$line" ] && return 1
# 去掉可能的 export 前缀与两端引号
local val="${line#*=}"
val="${val%\"}"; val="${val#\"}"; val="${val%\'}"; val="${val#\'}"
[ -z "$val" ] && return 1
export "$key=$val"
return 0
}
_ubox_ensure_var() {
local key="$1"
# 1. 当前 env 已存在则直接返回
eval "[ -n \"\$$key\" ]" && return 0
# 2. 尝试项目 .env / SKILL 目录 .env
_ubox_load_from_env_file "./.env" "$key" && return 0
[ -n "$SKILL_DIR" ] && _ubox_load_from_env_file "$SKILL_DIR/.env" "$key" && return 0
# 3. 尝试用户 rc 文件
for rc in "$HOME/.zshrc" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do
_ubox_load_from_env_file "$rc" "$key" && return 0
done
return 1
}
_ubox_mask() {
local v="$1"
local n=${#v}
if [ "$n" -lt 12 ]; then echo "****"; else echo "${v:0:4}****${v: -4}"; fi
}
for _k in UBOX_USER_TOKEN UBOX_PROJECT_TOKEN UBOX_GROUP_ID; do
_ubox_ensure_var "$_k" >/dev/null 2>&1
done
[ -n "$UBOX_USER_TOKEN" ] && echo "UBOX_USER_TOKEN=已配置(len=${#UBOX_USER_TOKEN}, preview=$(_ubox_mask "$UBOX_USER_TOKEN"))" || echo "UBOX_USER_TOKEN=未配置"
[ -n "$UBOX_PROJECT_TOKEN" ] && echo "UBOX_PROJECT_TOKEN=已配置(len=${#UBOX_PROJECT_TOKEN}, preview=$(_ubox_mask "$UBOX_PROJECT_TOKEN"))" || echo "UBOX_PROJECT_TOKEN=未配置"
[ -n "$UBOX_GROUP_ID" ] && echo "UBOX_GROUP_ID=$UBOX_GROUP_ID" || echo "UBOX_GROUP_ID=未配置"
Windows PowerShell 版本:
function Load-UboxVar($key) {
# 1. 当前 env
if ($env:$key) { return }
# 2. 项目 .env
foreach ($envFile in @(".\.env", "$env:SKILL_DIR\.env")) {
if ($envFile -and (Test-Path $envFile)) {
$line = Select-String -Path $envFile -Pattern "^\s*(export\s+)?$key=" | Select-Object -Last 1
if ($line) {
$val = ($line.Line -split '=', 2)[1].Trim('"').Trim("'")
if ($val) { Set-Item "Env:$key" $val; return }
}
}
}
# 3. Windows 用户级环境变量注册表
$regVal = [Environment]::GetEnvironmentVariable($key, 'User')
if ($regVal) { Set-Item "Env:$key" $regVal; return }
}
foreach ($k in 'UBOX_USER_TOKEN','UBOX_PROJECT_TOKEN','UBOX_GROUP_ID') { Load-UboxVar $k }
function Mask-Token($v) {
if (-not $v) { return '' }
if ($v.Length -lt 12) { return '****' }
return $v.Substring(0,4) + '****' + $v.Substring($v.Length-4,4)
}
if ($env:UBOX_USER_TOKEN) { "UBOX_USER_TOKEN=已配置(len=$($env:UBOX_USER_TOKEN.Length), preview=$(Mask-Token $env:UBOX_USER_TOKEN))" } else { "UBOX_USER_TOKEN=未配置" }
if ($env:UBOX_PROJECT_TOKEN) { "UBOX_PROJECT_TOKEN=已配置(len=$($env:UBOX_PROJECT_TOKEN.Length), preview=$(Mask-Token $env:UBOX_PROJECT_TOKEN))" } else { "UBOX_PROJECT_TOKEN=未配置" }
if ($env:UBOX_GROUP_ID) { "UBOX_GROUP_ID=$env:UBOX_GROUP_ID" } else { "UBOX_GROUP_ID=未配置" }
> ⚠️ 安全强制要求(鉴权信息脱敏显示):
> - 严禁在对话回复、日志、命令示例中原样打印/回显 UBOX_USER_TOKEN、UBOX_PROJECT_TOKEN 的完整值;
> - 如需展示,必须脱敏为 仅保留首 4 位 + **** + 末 4 位(例如 abcd****wxyz),Token 长度不足 12 位时全部用 **** 替代;
> - 禁止执行 echo $UBOX_USER_TOKEN / echo $UBOX_PROJECT_TOKEN / cat ~/.zshrc / env | grep UBOX 等任何可能打印完整 Token 的命令;
> - 从用户输入、.env、rc 文件、env / printenv 输出等任何来源读取到 Token 后,向用户复述时必须先脱敏;
> - UBOX_GROUP_ID(团队 ID)不属于敏感凭证,可原样展示。
3.2 三元组仍缺失时向用户索要
3.2.0 首次使用自动打开权限申请页(🆕 强制,先于索要话术执行)
> 触发条件:3.1 的多来源读取(当前 env → ./.env → $SKILL_DIR/.env → 用户 rc → Windows 用户注册表)全部未读取到 UBOX_USER_TOKEN / UBOX_PROJECT_TOKEN / UBOX_GROUP_ID 中的任意一个 —— 即被判定为首次使用(尚未申请到三元组 / 尚未在本机配置过)。
执行动作(必须在输出 3.2 索要话术之前完成):
- 按「第 0.5 步」的通用二级策略,自动打开权限申请指引页:
https://utest.21kunpeng.com/home/topic/ubox0811
- 优先 调用宿主 Agent(workBuddy / CodeBuddy / Cursor 等)提供的"打开 URL / 内置浏览器 / 侧栏预览 / 内嵌 WebView"类工具,直接在 Agent 内部把该页面展示给用户;
- 若当前 Agent 不支持内部浏览器访问(工具列表中找不到任何 URL 打开类工具),退回到 OS 级命令唤起 用户本地 PC 的默认浏览器 打开该 URL(macOS
open/ Linuxxdg-open/ Windowsstart/ PowerShellStart-Process); - 两者都不可用时,才降级为在对话中用 Markdown 链接展示,并明确提示用户手动点击。
- 在打开动作之后(或同时),向用户输出以下引导话术(原文照抄,链接务必保留;如果实际走的是「本地 PC 默认浏览器」档,请把首句「在当前 Agent 内」相应改为「在您本地 PC 的默认浏览器中」以如实反映实际动作):
> 🔑 检测到您尚未在当前环境配置优测云真机 SAAS 三元组(user_token / project_token / group_id),可能是首次使用本 SKILL。我已为您自动打开 权限申请指引页(优先使用当前 Agent 内置浏览器;若不支持则已唤起您本地 PC 的默认浏览器):
>
> 👉 https://utest.21kunpeng.com/home/topic/ubox0811
>
> 请在该页面按指引完成:
> 1. user_token(用户维度凭证)申请
> 2. project_token(项目维度凭证)申请
> 3. group_id(团队 ID,登录网站后右上角可见)复制
>
> 如果自动打开未生效,请手动点击上方链接。申请通过后,将 3 个凭证一并回复给我即可,我会自动帮您持久化到本地,下次无需再提供。
- 仅在完成动作 1 + 2 后,再继续执行 3.2 的索要话术(等待用户回复三元组),然后进入 3.3 持久化流程。不得跳过自动打开动作直接索要凭证。
3.2 索要话术(在 3.2.0 自动打开动作完成后使用)
- 若用户在收到 3.2.0 的引导后回复了 3 个凭证 → 直接进入「3.3 首次配置后自动持久化到全局」;
- 若用户回复"还没申请到 / 稍后再说" → 停止索要,告知用户"申请到 Token 后随时回来继续";
- 若用户明确说"我已经有 Token 但你没读到" → 请用户直接把 3 个凭证发出来,同样进入 3.3。
3.3 首次配置后自动持久化到全局(🆕 避免下次再问)
用户首次提供三元组后,除了在当前 shell 会话 export,必须主动持久化到"下次也能自动生效"的位置。默认策略:同时写入用户 shell rc 文件(macOS/Linux)或用户级环境变量注册表(Windows),并且在项目工作目录写一份 .env 做二次兜底。这样:
- 用户下次开新终端 → rc 文件生效;
- workBuddy 等非交互式 shell 拉起 SKILL → 3.1 的多来源读取会从 .env / rc 文件里自动补齐,不再重问。
> 告知义务:写入 rc / 注册表 / .env 前必须先向用户简短告知并请求确认,例如:
> > 我将把三元组凭证持久化到 ~/.zshrc 与当前工作目录的 .env 文件(用于下次自动加载)。是否同意?(回复"是/否",回复"否"则仅在当前会话生效)
>
> 用户回答"否"时,仅在当前会话 export,跳过 3.3.1 / 3.3.2 / 3.3.3;回答"是"或未回答但明显是首次一次性配置场景则按下方执行。
3.3.1 当前会话立即 export(无论用户是否同意持久化,此步都要做):
export UBOX_USER_TOKEN="<用户提供的 user_token>"
export UBOX_PROJECT_TOKEN="<用户提供的 project_token>"
export UBOX_GROUP_ID="<用户提供的 group_id>"
3.3.2 macOS / Linux 持久化到 rc 文件(幂等:先删除已有条目再追加,避免重复):
# 自动选择用户默认 shell 对应的 rc 文件
_ubox_pick_rc() {
case "${SHELL##*/}" in
zsh) echo "$HOME/.zshrc" ;;
bash) [ -f "$HOME/.bash_profile" ] && echo "$HOME/.bash_profile" || echo "$HOME/.bashrc" ;;
*) echo "$HOME/.profile" ;;
esac
}
RC_FILE=$(_ubox_pick_rc)
touch "$RC_FILE"
# 幂等写入:先删除旧的三元组 export 行,再追加新的
tmp=$(mktemp)
grep -vE '^[[:space:]]*export[[:space:]]+(UBOX_USER_TOKEN|UBOX_PROJECT_TOKEN|UBOX_GROUP_ID)=' "$RC_FILE" > "$tmp" && mv "$tmp" "$RC_FILE"
{
echo ""
echo "# >>> ubox-cli SAAS credentials (auto-managed by utest-ubox-skill) >>>"
echo "export UBOX_USER_TOKEN=\"$UBOX_USER_TOKEN\""
echo "export UBOX_PROJECT_TOKEN=\"$UBOX_PROJECT_TOKEN\""
echo "export UBOX_GROUP_ID=\"$UBOX_GROUP_ID\""
echo "# <<< ubox-cli SAAS credentials <<<"
} >> "$RC_FILE"
echo "✅ 已持久化到 $RC_FILE(新终端会自动生效)"
3.3.3 Windows 持久化到用户级环境变量注册表(PowerShell 下 setx 等价形式,永久生效):
[Environment]::SetEnvironmentVariable('UBOX_USER_TOKEN', $env:UBOX_USER_TOKEN, 'User')
[Environment]::SetEnvironmentVariable('UBOX_PROJECT_TOKEN', $env:UBOX_PROJECT_TOKEN, 'User')
[Environment]::SetEnvironmentVariable('UBOX_GROUP_ID', $env:UBOX_GROUP_ID, 'User')
"✅ 已持久化到 Windows 用户级环境变量(新终端会自动生效)"
3.3.4 同步写入项目 .env 兜底(跨平台,覆盖沙箱/CI 场景):
ENV_FILE="./.env"
touch "$ENV_FILE"
tmp=$(mktemp)
grep -vE '^[[:space:]]*(UBOX_USER_TOKEN|UBOX_PROJECT_TOKEN|UBOX_GROUP_ID)=' "$ENV_FILE" > "$tmp" && mv "$tmp" "$ENV_FILE"
{
echo "UBOX_USER_TOKEN=$UBOX_USER_TOKEN"
echo "UBOX_PROJECT_TOKEN=$UBOX_PROJECT_TOKEN"
echo "UBOX_GROUP_ID=$UBOX_GROUP_ID"
} >> "$ENV_FILE"
# 建议同步把 .env 加进 .gitignore,避免误提交
if [ -f ./.gitignore ] && ! grep -qxF ".env" ./.gitignore; then
echo ".env" >> ./.gitignore
fi
echo "✅ 已写入 $(pwd)/.env(3.1 多来源读取会优先命中)"
3.3.5 完成后向用户确认(必须脱敏):
> 已完成配置:UBOX_USER_TOKEN=abcd****wxyz(长度 32)、UBOX_PROJECT_TOKEN=1234****7890(长度 32)、UBOX_GROUP_ID=888888。已同步持久化到 ~/.zshrc 与项目 .env,下次无需再提供。
> 重要:
> - UBOX_AUTH_TOKEN(PAAS 的单 Token)在 SAAS 模式下不生效,请勿混用。
> - 本 SKILL 生成的每一条命令都会显式带上 --mode saas,以强制走 SAAS 通路,与 PAAS 严格区分。
> - --mode saas 必须写在主命令 ubox-cli 之后、命令组(device/app/action/...)之前,例如 ubox-cli --mode saas device list。
> - 敏感凭证脱敏:与用户交互过程中提及 UBOX_USER_TOKEN / UBOX_PROJECT_TOKEN 时,一律使用 前4位****后4位 的脱敏格式,禁止在回复正文、代码块示例、命令输出、日志片段中出现完整 Token 明文。
> - 持久化 vs 客户端来源标识的区别:三元组凭证属于长期用户身份,可以且应该写入 rc / 注册表 / .env;而第三步·B 的 UBOX_CLIENT_SOURCE / UBOX_CLIENT_LABEL 是会话级临时标记,不得写入 rc / 注册表(否则会污染用户手工调用时的统计口径)。二者持久化策略不同,勿混淆。
第三步·B:客户端来源标识(🆕 服务端埋点,跨平台)
> 目的:让服务端能明确区分"用户手工 CLI"和"AI 助手 SKILL 触发"两种调用来源,用于分渠道运营统计。不影响主流程——未设置也不会 break,服务端会通过 User-Agent 兜底识别。
本 SKILL 激活后必须为当前 shell 会话额外设置两个环境变量:UBOX_CLIENT_SOURCE 与 UBOX_CLIENT_LABEL。
AI 助手请先判定宿主 shell 类型,然后选择对应一段执行(切勿把 export 直接丢给 cmd.exe / powershell.exe):
方式一:macOS / Linux / WSL / Git Bash(bash / zsh)
# 声明本次调用由 AI 助手 SKILL 触发,服务端会将其记为 client_source=skill
export UBOX_CLIENT_SOURCE=skill
# 可选:声明 SKILL 具体宿主与版本,用于细分统计
export UBOX_CLIENT_LABEL=codebuddy-utest-ubox-skill/1.0
方式二:Windows CMD(cmd.exe)
rem 只对当前 cmd 会话生效,不写入注册表
set UBOX_CLIENT_SOURCE=skill
set UBOX_CLIENT_LABEL=codebuddy-utest-ubox-skill/1.0
方式三:Windows PowerShell
# 只对当前 PowerShell 会话生效
$env:UBOX_CLIENT_SOURCE = "skill"
$env:UBOX_CLIENT_LABEL = "codebuddy-utest-ubox-skill/1.0"
方式四(推荐 · 跨平台零差异 · 无需判定 shell):CLI 参数直接注入
在每条 ubox-cli 命令上直接追加参数,效果与设置 env 完全等价:
ubox-cli --mode saas --client-source skill --client-label codebuddy-utest-ubox-skill/1.0 <subcommand>
> AI 助手实现要点:
> - 优先方式四:无需判定 shell 类型,--client-source / --client-label 由 argparse/click 处理,Windows / macOS / Linux 命令一致;
> - 若选择方式一/二/三,必须先判定宿主 shell 类型(macOS/Linux 默认 bash/zsh;Windows 默认 cmd.exe 或 powershell.exe),再选对应片段执行。Windows 原生 shell 不认识 export,会直接报错导致 SKILL 无法启动;
> - 保底兜底:如果无法准确判断 shell 类型,一律回退到方式四。
取值优先级链(服务端最终看到的 client_source):
1. CLI 命令行参数 --client-source(显式,最高优先级)
2. 环境变量 UBOX_CLIENT_SOURCE(SKILL / 脚本注入)
3. 默认值 cli(用户直接跑命令时的兜底)
行为约定:
- 这两个 env 只应作用于本 SKILL 的会话上下文——不要写入用户 shell rc 文件、也不要用 setx 写入 Windows 用户环境变量注册表永久生效;那会污染用户手工执行时的统计精度;
- 若用户手工清除或未设置这两个变量,则该终端后续所有 CLI 命令降级为 cli(这是期望行为):
- macOS / Linux:unset UBOX_CLIENT_SOURCE UBOX_CLIENT_LABEL
- Windows CMD:set UBOX_CLIENT_SOURCE= 和 set UBOX_CLIENT_LABEL=(等号后留空)
- Windows PowerShell:Remove-Item Env:UBOX_CLIENT_SOURCE, Env:UBOX_CLIENT_LABEL -ErrorAction SilentlyContinue
- 未设置也不影响功能,只是服务端会降级识别(走 User-Agent 兜底判为 cli,或归为 openapi)。
第 3.5 步:跨对话辅助检查(可选,仅用于选对 UDID)
> 🔴 主线原则:设备的连接和释放,主线永远是直接执行对应的 CLI 命令——用户说"连接 XXX" → 立即执行 device connect --udid XXX;用户说"释放" → 立即执行 device disconnect。不要先看本地保活/连接信息记录后再决定是否执行主线命令,本地记录只是分支辅助信息,不能反过来控制主线是否发起。
如果用户没有明确给出 UDID,可以先跑一次 device status 帮助定位上次用过的默认设备:
ubox-cli --mode saas device status
- 无论 status 返回何种内容,用户一旦表达连接意图,就直接调用
device connect(CLI 内部对已有有效连接做幂等处理:可用则复用,不可用则重建;调用方无需在外层做前置判断); - 无论 status 是否显示保活运行中,用户一旦表达释放意图,就直接调用
device disconnect——不要"先看保活状态再决定要不要 disconnect"。
第四步:获取可用设备(SAAS 模式)
# 查看所有可用设备(SAAS)
ubox-cli --mode saas device list
# 按平台过滤
ubox-cli --mode saas device list --platform android
ubox-cli --mode saas device list --platform ios
ubox-cli --mode saas device list --platform android --platform ios # 多平台
# 分页
ubox-cli --mode saas device list --page-num 1 --page-size 10
从返回结果中获取设备的 udid、os_type 和 pool_id,后续操作都需要使用。
返回示例:
{
"success": true,
"message": "获取设备列表成功,共 5 个设备",
"data": {
"devices": [
{
"udid": "DEVICE_UDID_1",
"os_type": "android",
"manufacturer": "Samsung",
"model": "Galaxy S23",
"status": "在线",
"pool_id": "abc123",
"occupyStatus": 1,
"remotePhoneOccupy": 1
}
],
"total": 5,
"page_num": 1,
"page_size": 50
}
}
空闲设备判定:onlineStatus == 1 且 occupyStatus != 2 且 remotePhoneOccupy != 2。
第五步:连接设备(🔴 主线:立即直接调用 device connect)
> 🔴 主线原则(重要):
> - 用户表达连接意图("连接"、"链接"、"帮我用一台"、"connect"、"打开设备"等)时,立即直接执行 ubox-cli --mode saas device connect --udid <UDID> --os-type <类型>——不要先跑 device status 判断"上次是否连过"、不要先看本地保活记录、不要做任何前置判断。CLI 内部已内置幂等处理:已有有效连接则复用,不可用则重建,无需外层手动管。
在进行任何设备操作(截图、点击、安装应用等)之前,必须先连接设备。SAAS 模式底层调用 POST /utest-paas-manager/cloudphone/debug 完成占用。
> ⚠️ 保活策略变更(v0.2.6 起):device connect 不再自动拉起心跳保活——因为自动保活会持续续期并产生费用,必须由用户显式确认后手动开启。连接成功后 LLM 应告知用户"设备已连接,但未开启自动保活",并说明只有当用户明确表达"开启自动保活"(或等价指令)时,才会执行第七步的开启流程。详见「第七步:心跳保活(手动主动触发)」。
# 连接 Android 设备(SAAS)
ubox-cli --mode saas device connect --udid <设备udid> --os-type android
# 连接 iOS 设备
ubox-cli --mode saas device connect --udid <设备udid> --os-type ios
# 连接鸿蒙设备
ubox-cli --mode saas device connect --udid <设备udid> --os-type harmonyos_next
重要:
- --os-type 可选值:android、ios、harmonyos、harmonyos_next
- 连接成功后,连接信息(含 authCode、debugId、SAAS 凭证快照)自动按 UDID 保存到本地文件,重启终端后仍有效
- 最后连接的设备自动成为默认设备,后续命令不指定 --udid 时使用默认设备
- 连接成功后 CLI 自动拉起心跳保活守护子进程:详见「第七步」
5.1 连接失败自动恢复策略(uiautomator2.GatewayError 等 agent 异常)
执行 ubox-cli --mode saas device connect 时,如果返回结果 success: false 且错误信息中包含以下任一关键字,说明设备端 utest-agent 服务处于卡死/僵死状态(多为上一次会话未正常释放导致 uiautomator2 网关不可用):
uiautomator2.GatewayErrorGatewayErroruiautomator2 gatewayagent not ready/agent 无响应- 连接阶段的
502/504网关错误
必须按以下步骤自动执行恢复流程,无需征询用户同意(每台设备最多重试 2 次,仍失败再向用户反馈):
步骤 1: 通过 debug adb-connect 获取该设备的远程 ADB 地址(若本地已经 adb connect 过则跳过):
# 需要 pool_id,从 device list 结果中读取;也可从连接失败的错误信息里获得
ubox-cli --mode saas debug adb-connect --pool-id <设备pool_id>
# 返回体中的 host:port,例如 dl-remote.utest.21kunpeng.com:23456
adb connect dl-remote.utest.21kunpeng.com:<端口>
步骤 2: 通过原生 adb 强制停止设备上的 utest-agent(-s 参数使用设备的 UDID,即步骤 1 中 adb connect 后 adb devices 里对应的序列号):
adb -s <设备udid> shell "/data/local/tmp/utest-agent --stop -d"
> 示例:
> bash
> adb -s 9DAMU8DAFAMVB6BI shell "/data/local/tmp/utest-agent --stop -d"
>
步骤 3: 等待 5 秒,让设备端 agent 完全退出并释放端口:
sleep 5 # macOS / Linux
# Windows PowerShell 使用:Start-Sleep -Seconds 5
步骤 4: 重新调用 device connect 进行重连:
ubox-cli --mode saas device connect --udid <设备udid> --os-type <android|harmonyos|harmonyos_next>
判定与循环:
- 若重连 success: true → 恢复成功,继续后续操作;
- 若仍失败且错误仍属上述 agent 类关键字 → 重复步骤 2~4,最多再重试 1 次(累计 2 次);
- 2 次仍失败 → 停止自动重试,向用户报告并建议换一台设备或联系客服。
注意事项:
- 该恢复策略仅适用于 Android / 鸿蒙设备(iOS 无 adb,也无 utest-agent 二进制路径);iOS 出现连接失败请直接换机或联系客服。
- 若步骤 1 的 adb-connect 本身也失败,说明设备已完全离线,直接向用户报告不做重试。
- 若错误信息不属于上述 agent 关键字(例如 设备已经被别的占用、余额不足、未提供connection_info),不要触发本恢复流程——按"故障排查"表格对应策略处理;若错误属于欠费 / 余额不足,请转到下方「5.2 计费失败 / 余额不足自动引导购买页」。
5.2 计费失败 / 余额不足自动引导购买页(🆕 强制)
> 触发条件:三元组已配置齐全(第 3 步 3.1 已成功读取到 UBOX_USER_TOKEN / UBOX_PROJECT_TOKEN / UBOX_GROUP_ID)、ubox-cli 版本也已达标,但执行 ubox-cli --mode saas device connect / device renew / 其它计费类命令时返回 success: false,且错误信息中命中任一以下关键字:
>
> - 余额不足 / 余额不足,计费失败 / 余额不足,充值失败
> - 计费失败 / 扣费失败 / 扣款失败
> - 账户欠费 / 欠费 / arrears / insufficient balance
> - vum 不足 / VUM不足 / 资源不足需充值
> - HTTP 402 Payment Required(部分场景服务端会以此状态返回)
执行动作(必须依次完成,不得跳过):
- 按「第 0.5 步」的通用二级策略,自动打开云真机购买/充值页:
https://utest.21kunpeng.com/workbench/resource/payment/PayDetailed/1002
- 优先 调用宿主 Agent(workBuddy / CodeBuddy / Cursor 等)提供的"打开 URL / 内置浏览器 / 侧栏预览 / 内嵌 WebView"类工具,直接在 Agent 内部把该页面展示给用户;
- 若当前 Agent 不支持内部浏览器访问(工具列表中找不到任何 URL 打开类工具),退回到 OS 级命令唤起 用户本地 PC 的默认浏览器 打开该 URL(macOS
open/ Linuxxdg-open/ Windowsstart/ PowerShellStart-Process); - 两者都不可用时,才降级为在对话中用 Markdown 链接展示,并明确提示用户手动点击。
- 在打开动作之后(或同时),向用户输出以下引导话术(原文照抄,链接务必保留,Token 相关字段展示时必须脱敏;如果实际走的是「本地 PC 默认浏览器」档,请把首句「在当前 Agent 内」相应改为「在您本地 PC 的默认浏览器中」以如实反映实际动作):
> 💸 检测到设备连接失败,原因是账户余额不足 / 计费失败(原始错误:<把 CLI 返回的原始错误信息脱敏后贴到这里>)。我已为您自动打开 云真机资源购买/充值页(优先使用当前 Agent 内置浏览器;若不支持则已唤起您本地 PC 的默认浏览器):
>
> 👉 https://utest.21kunpeng.com/workbench/resource/payment/PayDetailed/1002
>
> 请在该页面按指引完成充值/购买套餐。如果自动打开未生效,请手动点击上方链接。充值成功后,请告诉我"充值好了",我会立即为您重试连接设备。
-
在用户回复"充值好了"(或等价表述:"付款完成"、"已经充值"、"再试一下")之前,不得反复重试连接——避免持续触发计费失败告警。用户明确表示已充值后,自动重试一次
ubox-cli --mode saas device connect --udid <设备udid> --os-type <类型>:
- 若成功 → 继续原用户请求;
- 若仍返回"余额不足/计费失败" → 提示用户"充值可能未到账,请稍等 1~2 分钟后再让我重试",不再自动打开购买页(幂等约束,避免刷屏);
- 若返回其它错误 → 按故障排查表格常规处理。 -
禁止行为:
- 禁止在未触达欠费/余额不足关键字前擅自打开购买页(例如设备已被别人占用、GatewayError等错误不属于本节触发条件);
- 禁止把该 URL 与「权限申请指引页」混淆——两者用途完全不同:home/topic/ubox0811是文档,workbench/resource/payment/PayDetailed/1002是充值页;
- 禁止在打开购买页的引导话术中原样回显UBOX_USER_TOKEN/UBOX_PROJECT_TOKEN明文,遵循全局脱敏规则。
连接成功后,生成实时画面访问链接:
# 使用默认设备
ubox-cli --mode saas screen play-url
# 指定设备
ubox-cli --mode saas screen play-url --udid <设备udid>
生成的 URL 可在浏览器中打开,实时查看设备屏幕画面。
> 说明:SAAS 通路仅提供 API 侧的设备占用/列表/操作接口,视频流与操作流长链接沿用 PAAS 域名(labpaas.utest.21kunpeng.com / dl.utest.21kunpeng.com)——这是官网 SAAS OpenAPI 的既定实现,用户无感知,不影响正常观看。
第六步:处理用户请求
根据用户的实际需求,选择合适的命令进行调用。所有命令都显式带上 --mode saas。所有命令输出均为 JSON 格式,通过 success 字段判断操作是否成功。
提示:任何命令都可以通过 --help 查看详细的参数说明和用法:
# 查看顶层帮助
ubox-cli --help
# 查看某个命令组的帮助
ubox-cli device --help
ubox-cli action --help
# 查看具体子命令的帮助
ubox-cli device connect --help
ubox-cli action click --help
第七步:心跳保活(🔴 手动主动触发,LLM 严禁自动开启)
> 🚨 v0.2.6 起的重大变更:以前 device connect 成功后 CLI 会自动拉起心跳保活守护子进程,现在已改为完全手动主动触发。LLM 在任何情况下都不得在用户没有明确要求的前提下擅自开启自动保活,因为自动保活会持续续期并产生费用。
核心机制:
心跳保活由 CLI 后台的独立守护子进程 keepalive_daemon 承担,一旦开启后会在后台常驻:
- 维持视频流 WebSocket 长连接:连接
wss://dl.utest.21kunpeng.com/utest-paas-commissioner/{env}/scrcpy/screen/middle?authCode=xxx&agentVersion=v2&resolution=standard,断线按指数退避自动重连; - 周期上报心跳请求:每 25 秒向 adapter 网关
POST /local/debug/heartbeat发一次{debugId, idle:0, streamNormal:true}。
云真机后端通过 WebSocket 存活状态 + heartbeat 上报时间两条信号共同判断用户是否仍在使用设备,因此保活必须长连接常驻,不能靠一次性调用完成。
7.1 开启自动保活(用户显式指令触发)
触发关键词(用户消息中出现以下任一表述即视为"开启自动保活"意图):
- 「开启自动保活」/「打开自动保活」/「启用自动保活」/「开启保活」
- 「保持在线」/「持续续期」/「防止被回收」/「让设备一直连着」
- 「start keepalive」/「enable keepalive」
触发后 LLM 必须执行的完整动作:
- 执行开启命令:
bash
ubox-cli --mode saas device renew
# 或指定设备:
ubox-cli --mode saas device renew --udid <设备udid>
device renew 内部行为:若守护进程未运行则拉起(首次开启即走这条路径);若已运行则复用并立即追发一次心跳。这是开启自动保活的唯一入口命令。
- 命令返回
success: true后,必须原样向用户输出以下提示文案(禁止缩减、改写、翻译或省略):
> 开启自动保活成功,自动保活会自动续期持续扣费,请在不需要使用设备时主动释放设备。
- 提示后可继续处理用户的其他请求(若用户在同一句话里既要求开启保活又要求做其他操作)。
> ⚠️ 严格约束(LLM 必读):
> - 未收到用户明确的"开启自动保活"等价指令前,绝对不得调用 device renew——device connect 后即使用户接着做点击/截图等操作,也不要"顺手"帮他开保活;
> - 未开启保活时,长时间挂机(一般超过 1~2 分钟无任何 CLI 命令)设备可能被服务端回收,这是预期行为,不是 bug;用户若抱怨"连着连着就断了",先向他解释保活策略,再询问是否需要开启;
> - 提示文案是硬性要求:"开启自动保活成功,自动保活会自动续期持续扣费,请在不需要使用设备时主动释放设备。"——一字不差原样输出,不要翻译、不要加 emoji 修饰、不要合并到其它段落里。
7.2 保活续期兜底(沙箱环境)
在已经开启过自动保活后,若运行环境属于以下场景,LLM 需要在每轮用户交互开始时主动调一次 device renew 作为兜底:
| 场景 | 是否需要 LLM 每轮主动调用 device renew |
|---|---|
| 普通终端 / CI 环境(守护子进程可长期存活) | 无需;守护进程会自行按 25s 周期上报 |
| workBuddy 沙箱 / AI Agent 沙箱等可能在轮次结束后回收子进程的环境 | 必须:LLM 在每一次用户交互开始时(收到用户消息、准备调用任何 ubox-cli 命令之前),主动执行一次 ubox-cli --mode saas device renew——守护仍在则等价于追发心跳,被沙箱回收则会重新拉起 |
| 长时间挂机不做任何操作 | 视环境而定;只要用户已开启过保活且环境不杀后台进程,就无需额外兜底 |
> 注意:
> - device renew 是幂等的轻量操作,反复调用无副作用;开启后宁多勿少;
> - 但 device renew 的"兜底续期"作用有前提:用户必须先用它显式开启过一次保活。如果用户从头到尾都没说"开启自动保活",LLM 不应偷偷在轮次开头调 device renew 帮他"续命"——这违背了保活策略变更的初衷(避免用户在不知情的情况下被扣费)。
7.3 关闭自动保活
用户明确说"关闭自动保活"、"停止保活"等指令时,直接调用 device disconnect 即可——它会一并停止后台守护进程。若用户只想"停保活但保留连接",可以引导用户直接 disconnect 后重新 connect(新连接默认不开保活),或联系客服反馈进一步需求。
第八步:释放设备(🔴 主线:立即直接调用 device disconnect)
> 🔴 主线原则(重要):
> - 用户在当前对话中明确说"释放"、"断开"、"disconnect"、"释放设备"、"用完了"、"关掉"等等价指令时,立即直接执行 ubox-cli --mode saas device disconnect——不要先跑 device status、不要先查本地保活/连接信息记录、不要先停止心跳、不要做任何前置判断;
> - 反过来:用户没有明确表达释放意图时,任何情况下都不得调用 device disconnect。任务完成 ≠ 释放;出错时按故障排查表给出的动作重连而非释放。
主线执行(直接调用,不做任何前置检查):
# 释放当前默认设备(用户未指定 UDID 时)
ubox-cli --mode saas device disconnect
# 释放指定设备(用户指定了 UDID 时)
ubox-cli --mode saas device disconnect --udid <设备udid>
分支辅助说明(仅供理解,不影响主线执行):
- device disconnect 会同步通知服务端断开占用,避免持续计费/占用配额;
- device disconnect 会自动停止后台心跳保活守护进程(keepalive_daemon),无需手动 kill;disconnect 之后也无需再调 device renew。
命令结构总览(与 PAAS 对齐)
> 除 --mode saas 全局参数外,SAAS 版本命令名称、子命令、参数与 PAAS 版本完全一致。区别仅在于每条命令都需要在 ubox-cli 后显式带 --mode saas。
ubox-cli --mode saas
├── device 设备管理(列表、连接、断开、状态、信息、心跳)
│ ├── list 获取可用设备列表 [无需连接]
│ ├── connect 连接设备并保存连接信息 [无需连接](**v0.2.6 起不再自动拉起保活**)
│ ├── disconnect 断开设备连接(SAAS 必做) [需要已连接](**自动停止心跳保活守护进程**)
│ ├── renew **开启/续期自动保活的唯一入口** [需要已连接](首次调用即开启,后续为兜底续期)
│ ├── status 查看所有已保存的连接状态 [无需连接]
│ └── info 获取设备详细硬件信息 [需要已连接]
├── app 应用管理
│ ├── start 启动应用 [需要已连接]
│ ├── stop 停止应用 [需要已连接]
│ ├── current 获取当前前台应用 [需要已连接]
│ ├── running 获取所有运行中应用 [需要已连接]
│ ├── install 通过URL安装应用 [需要已连接]
│ ├── install-local 安装本地应用文件 [需要已连接]
│ └── uninstall 卸载应用 [需要已连接]
├── action 交互操作
│ ├── click 智能点击(UI/OCR/CV/POS) [需要已连接]
│ ├── click-pos 基于坐标点击 [需要已连接]
│ ├── long-click 长按操作 [需要已连接]
│ ├── slide 智能滑动(UI/OCR/CV/POS) [需要已连接]
│ ├── slide-pos 基于坐标滑动 [需要已连接]
│ ├── input-text 输入文本 [需要已连接]
│ └── press 按键操作 [需要已连接]
├── screen 截图录制
│ ├── screenshot 截图保存到本地【✅默认截图入口】 [需要已连接]
│ ├── screenshot-base64 获取Base64格式截图【⚠️仅特殊场景】 [需要已连接]
│ ├── play-url 生成视频流播放链接 [需要已连接]
│ ├── record-start 开始屏幕录制 [需要已连接]
│ └── record-stop 停止屏幕录制 [需要已连接]
├── find UI查找
│ ├── ui XPath控件查找 [需要已连接]
│ ├── ocr OCR文字查找 [需要已连接]
│ └── ui-tree 获取UI控件树结构 [需要已连接]
├── clipboard 剪贴板操作
│ ├── set 设置剪贴板 [需要已连接]
│ └── get 获取剪贴板 [需要已连接]
├── debug 调试诊断
│ ├── adb 执行ADB命令 [仅Android/鸿蒙]
│ ├── adb-connect 获取ADB远程调试连接地址 [仅Android/鸿蒙]
│ ├── logcat-start 开始Logcat日志采集 [仅Android/鸿蒙]
│ ├── logcat-stop-all 停止所有Logcat任务 [仅Android/鸿蒙]
│ ├── logcat-list 列出Logcat任务 [仅Android/鸿蒙]
│ ├── perf-start 开始性能采集 [全平台]
│ ├── perf-stop 停止性能采集 [全平台]
│ ├── anr-start 开始ANR/Crash监控 [仅Android/鸿蒙]
│ └── anr-stop 停止ANR/Crash监控 [仅Android/鸿蒙]
└── version 获取版本信息 [无需连接]
全局参数速查(SAAS 模式)
| 参数 | 环境变量 | 说明 |
|---|---|---|
--mode saas |
— | 必须:显式启用 SAAS 通路(内部使用 SAAS_CLI 模式)。位置:ubox-cli 之后、命令组之前 |
| — | UBOX_USER_TOKEN |
SAAS 用户维度凭证 |
| — | UBOX_PROJECT_TOKEN |
SAAS 项目维度凭证 |
| — | UBOX_GROUP_ID |
团队 ID |
| — | UBOX_ENV |
环境,formal(默认)/test |
常用操作示例
> 以下所有示例都显式带 --mode saas,位置:ubox-cli 之后、命令组之前。
设备管理
# 查看连接状态(会展示每台设备的 auth_mode: paas / saas)
ubox-cli --mode saas device status
# 开启自动保活(v0.2.6 起 connect 不再自动开保活,需用户显式要求后调用)
# ⚠️ 一旦开启会持续续期并扣费,用完记得 disconnect
ubox-cli --mode saas device renew
ubox-cli --mode saas device renew --udid DEVICE_UDID_1
# 断开默认设备(SAAS 必做,仅在用户明确要求释放时执行)
ubox-cli --mode saas device disconnect
# 断开指定设备
ubox-cli --mode saas device disconnect --udid DEVICE_UDID_1
# 获取设备硬件信息
ubox-cli --mode saas device info
截图操作
> 🔴 默认截图入口 = screen screenshot(本地截图)
>
> 用户在对话中只说"截图" / "帮我截个图" / "截一下屏幕" / "screenshot" / "截屏" / "给我看看当前画面" 等未明确指定编码格式的表述时,必须默认使用 screen screenshot(保存到本地文件),不得擅自使用 screen screenshot-base64。
>
> 判定规则(LLM 必读):
> - ✅ 默认走 screen screenshot:用户消息里只出现"截图 / 截屏 / 截个图 / screenshot"等通用词,或未提及"base64 / 内联 / 内存 / 直接给我图 / 视觉分析 / VLM / 喂给大模型"等特殊场景关键词;
> - ⚠️ 仅当用户明确提出下列诉求时才使用 screen screenshot-base64:
> - 显式说"base64 截图" / "给我 base64" / "截图 base64 编码";
> - 显式说"截图后直接给视觉模型 / VLM / GPT-4V / Claude Vision 分析";
> - 显式说"不落盘 / 不写文件 / 内存里处理 / 直接给我图像字节";
> - 保底原则:如无法判断用户是否属于特殊场景,一律选默认 screen screenshot。理由:返回体轻、可追溯、体积比 base64 小 33%,且不会污染对话上下文。
>
> --img-path 目录选择规则(默认截图必备参数):
> - 用户未指定保存路径时,默认使用当前工作目录(如 ./ 或 $PWD),并给文件加时间戳 label 便于区分;
> - 用户指定了绝对路径 → 直接使用用户提供的绝对路径;
> - 用户指定了相对路径 → 拼接当前工作目录变成绝对路径后再传入。
# ✅ 默认场景:用户只说"截图"→ 直接用本地截图(保存到当前工作目录)
ubox-cli --mode saas screen screenshot --label "screenshot_$(date +%Y%m%d_%H%M%S)" --img-path "$PWD"
# ✅ 用户指定了保存目录:使用绝对路径
ubox-cli --mode saas screen screenshot --label "my_screenshot" --img-path /Users/xxx/Desktop
# ⚠️ 特殊场景才用 base64(用户明确要求 / 需要喂给视觉模型 / 不落盘处理)
# 说明:SAAS 模式本地截图底层也走 base64→文件路径(绕开仅支持 PAAS JWT 的 fileserver),
# 两者取图源相同,差异仅在"是否落盘 + 返回什么给调用方";base64 返回体大 33%,慎用。
ubox-cli --mode saas screen screenshot-base64
# 开始/停止录屏
ubox-cli --mode saas screen record-start --video-path /Users/xxx/Desktop/record.mp4
ubox-cli --mode saas screen record-stop
# 生成视频流播放链接
ubox-cli --mode saas screen play-url # 使用默认设备
ubox-cli --mode saas screen play-url --udid ABC123 # 指定设备
说明:play-url 会根据 SAAS 环境配置与 authCode 生成用于在浏览器中观看设备实时画面的视频流链接。需要先通过 device connect 连接设备。视频流长链接域名沿用 PAAS,无感知。
按键操作
# 返回主页
ubox-cli --mode saas action press --button HOME
# 返回上一页
ubox-cli --mode saas action press --button BACK
# 打开最近任务
ubox-cli --mode saas action press --button RECENT_APP
# 唤醒屏幕
ubox-cli --mode saas action press --button WAKE_UP
可用按键:HOME、BACK、VOLUME_UP、VOLUME_DOWN、POWER、DEL、FORWARD_DEL、MENU、RECENT_APP、WAKE_UP、SLEEP
点击操作
# XPath 定位点击(默认 --by UI)
ubox-cli --mode saas action click --loc "//button[@text='确定']"
# OCR 文字定位点击
ubox-cli --mode saas action click --loc "登录" --by OCR
# 图像模板定位点击
ubox-cli --mode saas action click --loc ./icon.png --by CV
# 坐标定位点击(屏幕中心,坐标范围 0~1)
ubox-cli --mode saas action click --loc "0.5,0.5" --by POS
# 直接坐标点击
ubox-cli --mode saas action click-pos --pos 0.5,0.5
# 双击
ubox-cli --mode saas action click --loc "//button" --times 2
# 长按
ubox-cli --mode saas action long-click --loc "//item" --duration 3.0
定位方式说明(--by 参数)
| 定位方式 | 说明 | --loc 传入内容 | 适用场景 |
|---|---|---|---|
UI |
XPath 控件定位(默认) | XPath 表达式 | 原生 UI 页面 |
OCR |
OCR 文字识别定位 | 要查找的文字 | WebView/H5/游戏/任意文字 |
POS |
相对坐标定位 | "x,y" 格式(0~1) | 已知坐标位置 |
滑动操作
# 向上滑动(刷新/滚动列表)
ubox-cli --mode saas action slide-pos --from 0.5,0.8 --to 0.5,0.2
# 向下滑动
ubox-cli --mode saas action slide-pos --from 0.5,0.2 --to 0.5,0.8
# 向左滑动
ubox-cli --mode saas action slide-pos --from 0.8,0.5 --to 0.2,0.5
# 向右滑动
ubox-cli --mode saas action slide-pos --from 0.2,0.5 --to 0.8,0.5
# 慢速滑动
ubox-cli --mode saas action slide-pos --from 0.5,0.8 --to 0.5,0.2 --slide-duration 1
坐标范围 0~1,(0,0) 为左上角,(1,1) 为右下角。--slide-duration 值越大滑动越慢。
文本输入
# 1. 先点击输入框使其获得焦点
ubox-cli --mode saas action click --loc "//android.widget.EditText"
# 2. 再输入文本
ubox-cli --mode saas action input-text --text "要输入的内容"
重要:使用前需确保设备上已有输入框处于聚焦状态。
应用管理
# 安装应用(通过URL)
ubox-cli --mode saas app install --app-url https://example.com/app.apk
# 安装本地应用
ubox-cli --mode saas app install-local --local-path /path/to/app.apk
# 卸载应用
ubox-cli --mode saas app uninstall --package com.example.app
# 启动应用(可选清除数据)
ubox-cli --mode saas app start --package com.example.app --clear-data
# 停止应用
ubox-cli --mode saas app stop --package com.example.app
# 获取当前前台应用
ubox-cli --mode saas app current
# 获取所有运行中应用
ubox-cli --mode saas app running
UI 查找
# XPath 控件查找
ubox-cli --mode saas find ui --xpath "//button[@text='确定']"
ubox-cli --mode saas find ui --xpath "//*[contains(@text,'登录')]"
ubox-cli --mode saas find ui --xpath "//*[@resource-id='com.app:id/btn']"
# OCR 文字查找
ubox-cli --mode saas find ocr --word "登录"
# 获取 UI 控件树(JSON 格式)
ubox-cli --mode saas find ui-tree
# 获取 UI 控件树(XML 格式)
ubox-cli --mode saas find ui-tree --xml
剪贴板操作
ubox-cli --mode saas clipboard set --text "复制的内容"
ubox-cli --mode saas clipboard get
调试诊断
# 执行 ADB 命令(仅 Android/鸿蒙)
ubox-cli --mode saas debug adb --command "dumpsys activity"
ubox-cli --mode saas debug adb --command "pm list packages"
# 获取 ADB 远程调试连接地址(仅 Android/鸿蒙)
# pool_id 可以从 `device list` 返回结果中获取
ubox-cli --mode saas debug adb-connect --pool-id <设备pool_id>
# 连接远程 ADB 后,即可在本地使用 adb 命令调试设备
adb connect dl-remote.utest.21kunpeng.com:<端口>
adb shell dumpsys activity
# 断开 ADB 远程调试连接(使用系统原生 adb 命令)
adb disconnect dl-remote.utest.21kunpeng.com:<端口>
# 日志采集(仅 Android/鸿蒙)
ubox-cli --mode saas debug logcat-start --file ./logs/test.log --clear
ubox-cli --mode saas debug logcat-list
ubox-cli --mode saas debug logcat-stop-all
# 性能采集(全平台)
ubox-cli --mode saas debug perf-start --package com.example.app --case-name "首页浏览"
ubox-cli --mode saas debug perf-stop --output-dir ./perf_output
# ANR/Crash 监控(仅 Android/鸿蒙)
ubox-cli --mode saas debug anr-start --package com.example.app --collect-am-monitor
ubox-cli --mode saas debug anr-stop --output-dir ./anr_report
多设备操作
# 连接多台设备
ubox-cli --mode saas device connect --udid DEVICE_1 --os-type android
ubox-cli --mode saas device connect --udid DEVICE_2 --os-type ios
# 查看所有已连接设备
ubox-cli --mode saas device status
# 操作指定设备(通过 --udid 参数)
ubox-cli --mode saas screen screenshot --udid DEVICE_1
ubox-cli --mode saas action click --loc "//button[@text='OK']" --udid DEVICE_2
# 任务结束批量释放
ubox-cli --mode saas device disconnect --udid DEVICE_1
ubox-cli --mode saas device disconnect --udid DEVICE_2
默认设备规则:不指定 --udid 时使用最后连接的设备。
故障排查
常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
SAAS 模式必须同时配置以下环境变量: UBOX_USER_TOKEN, UBOX_PROJECT_TOKEN, UBOX_GROUP_ID |
三元组缺失 | 补齐全部 3 个凭证;若尚未申请,请查看权限申请指引:https://utest.21kunpeng.com/home/topic/ubox0811 |
PAAS 模式必须配置 UBOX_AUTH_TOKEN |
未加 --mode saas,走了默认 PAAS 通路 |
命令组前加 --mode saas |
--mode 必须为 'paas' 或 'saas' |
--mode 参数值不合法 |
只能使用 paas 或 saas |
No such option: --mode |
--mode 位置错误(写到了子命令末尾) |
移到 ubox-cli 之后、命令组之前 |
余额不足,计费失败 |
①账户/项目 vum 余额不足;②高频调用命中 SAAS 侧 debugId 分布式锁(60~120s TTL);③计费订单创建异常 | ①自动触发「第五步 5.2 计费失败/余额不足自动引导购买页」:按「第 0.5 步」二级策略(Agent 内置浏览器优先 → 本地 PC 默认浏览器兜底 → 对话 Markdown 链接最后兜底)打开 https://utest.21kunpeng.com/workbench/resource/payment/PayDetailed/1002 并原样输出引导文案,等待用户充值完成后再重试;②等待 60~120s 后重试(分布式锁);③稍后重试 |
设备已经被别的占用 / debug start error |
设备已被他人占用 | 换一台设备或稍后重试;先用 ubox-cli --mode saas device list 筛选空闲设备 |
未提供connection_info |
未连接设备 | 先执行 ubox-cli --mode saas device connect |
未找到设备 xxx 的连接信息 |
指定的 UDID 未连接 | 检查 UDID 是否正确,重新连接 |
connection refused / timeout |
连接过期或网络异常 | 手动重新 ubox-cli --mode saas device connect |
uiautomator2.GatewayError / GatewayError / agent not ready |
设备端 utest-agent 卡死或未正常释放 |
自动触发"第五步 5.1 连接失败自动恢复策略":adb -s <udid> shell "/data/local/tmp/utest-agent --stop -d" → sleep 5 → 重新 device connect(最多 2 次) |
ADB命令仅支持Android和鸿蒙设备 |
在 iOS 设备上执行了 ADB 命令 | 改用其他方式操作 |
获取ADB调试链接失败 |
pool_id 不正确或未指定 | 先从 ubox-cli --mode saas device list 结果中获取对应设备的 pool_id |
获取UI树失败 |
页面未加载完成或 WebView 页面 | 等待页面加载,或改用 OCR 方式 |
HTTP 404 |
路径 404 | 运行 2.1 的升级判定脚本,若 NEED_UPGRADE=yes 则重新跑 2.3;检查环境变量 UBOX_ENV 是否指定正确(formal/test) |
| 长时间挂机后设备被回收 / 操作报连接过期 | ①用户从未开启过自动保活(v0.2.6 起 connect 不再自动开启);②已开启保活但守护子进程被沙箱回收 | ①先向用户说明"未开启自动保活,长时间挂机会被服务端回收",询问是否需要开启,得到确认后按第七步 7.1 执行;②已开启过保活的场景下,让 LLM 每一轮用户交互开始时主动 ubox-cli --mode saas device renew(守护挂了会自动重新拉起) |
device renew 返回成功,但截图/点击等实际操作仍失败 |
服务端 debug 会话可能已退化(authCode 失效等) | 属正常现象:device disconnect 后重新 device connect;不要因为心跳通过就跳过重连 |
| 每次进 workBuddy / AI Agent 沙箱都被要求重新提供三元组凭证 | 宿主为 SKILL 拉起的是非登录非交互式子 shell,不加载 ~/.zshrc / ~/.bashrc,导致用户此前 export 的 UBOX_* 变量读不到;且此前未在项目 .env 里持久化 |
按「第三步 3.1」执行多来源读取(当前 env → ./.env → $SKILL_DIR/.env → 用户 rc → Windows 用户注册表)自动补齐;仍读不到再向用户索要,索要后立即按「3.3」持久化到 rc 文件 + 项目 .env,下次不再重问 |
注意事项
- 🚀 自动引导页面(首次使用 / 欠费场景):
- 首次使用(三元组未配置):必须按「第三步 3.2.0」自动打开权限申请页
https://utest.21kunpeng.com/home/topic/ubox0811,不得仅在对话里贴链接了事; - 设备连接因欠费/余额不足失败:必须按「第五步 5.2」自动打开充值页
https://utest.21kunpeng.com/workbench/resource/payment/PayDetailed/1002,并等待用户确认充值后才重试; - 实现优先级见「第 0.5 步」二级策略:第 1 档 当前 Agent 内置浏览器(
open_url/preview_url/webview.open/ MCP browser / WorkBuddy 的present_files等)→ 第 2 档 用户本地 PC 默认浏览器(open/xdg-open/start/Start-Process)→ 第 3 档 对话中 Markdown 链接兜底并明确提示手动点击; - 同一对话内同一场景只自动打开一次,遵守幂等与频次约束。
- 🔐 鉴权信息脱敏显示(最高优先级安全规则):
- 在任何对话回复、命令示例、错误诊断输出、日志片段中,绝对禁止原样显示
UBOX_USER_TOKEN、UBOX_PROJECT_TOKEN的完整值; - 展示时统一使用 前 4 位 +
****+ 后 4 位 的脱敏格式(如abcd****wxyz),长度不足 12 位时整体用****替代; - 禁止建议或执行任何直接打印完整 Token 的命令(如
echo $UBOX_USER_TOKEN、env | grep UBOX、cat .env、cat ~/.zshrc); - 例外:第三步 3.1 提供的多来源读取脚本,仅通过
grep + export把特定行的值送入进程环境变量,从不把值echo到 stdout;这是允许的合规读取方式,不违反本规则; - 若命令返回体、报错栈中意外包含 Token 明文,转述给用户前必须先手动脱敏;
UBOX_GROUP_ID(团队 ID)非敏感凭证,可原样展示。- 认证方式:SAAS 通路使用三元组
user_token + project_token + group_id(两个 Token + 团队 ID),与 PAAS 完全不同,不要混用。 - 凭证读取与持久化(🆕 workBuddy / 沙箱场景强制要求):
- 检查阶段必须按「第三步 3.1」多来源顺序读取(当前 env → 项目
.env→ 用户 rc 文件 → Windows 用户注册表),不得只判断当前进程 env 就宣告"未配置"——非交互式子 shell 拿不到用户 rc 里的 export 是常态; - 首次向用户索要到三元组后,必须按「第三步 3.3」执行"当前会话 export + rc/注册表持久化 + 项目
.env兜底"三件套(在写入前告知用户并征得同意;用户明确拒绝持久化才只在当前会话生效); - 这样在同一台机器上,下次任何终端 / workBuddy 会话 / CI Runner 都能自动读到,不再重问。
- 每条命令都要
--mode saas:本 SKILL 生成的每一条命令都必须显式带--mode saas,与 PAAS 严格区分;--mode参数写在ubox-cli之后、命令组之前。 - 🔴 主线-分支总原则:设备的连接和释放是主线逻辑——用户说连接就立即调
device connect,用户说释放就立即调device disconnect,不要先看device status/ 本地保活记录 / 心跳状态后再决定是否发起主线命令。保活、心跳、本地连接记录都是分支辅助逻辑,不能反过来控制主线。 - 释放责任:SAAS_CLI 服务端无自动释放。只有用户在当前对话中明确要求"释放/断开/disconnect"时,才可调用
device disconnect,且一旦用户提出就立即直接调用,不作任何前置检查。任务完成 ≠ 释放。 - 心跳保活(v0.2.6 起改为手动主动触发):
device connect不再自动拉起心跳保活守护进程——因为自动保活会持续续期并产生费用,必须由用户显式确认后再开启。LLM 严禁在未收到用户明确"开启自动保活"等价指令前擅自调用device renew。用户明确要求开启后,执行ubox-cli --mode saas device renew并原样输出以下文案(一字不差):
> 开启自动保活成功,自动保活会自动续期持续扣费,请在不需要使用设备时主动释放设备。
已开启保活后:普通终端/CI 环境守护进程自行按 25s 周期上报,无需 LLM 介入;沙箱环境需 LLM 每一轮用户交互开始时主动调一次 device renew 作为兜底重启触发器(详见「第七步」)。device disconnect 会自动停止守护进程。
- 视频流 / 操作流长链接沿用 PAAS 域名(SAAS 未独立提供),用户无感知。
- 设备操作前必须先连接设备。
- 📸 截图默认走本地截图(screen screenshot):用户消息里只出现"截图 / 截屏 / screenshot"等通用词时,一律使用 screen screenshot 保存到本地文件;仅当用户明确提出 base64 / 视觉模型输入 / 不落盘 / 内存处理等特殊诉求时,才切换到 screen screenshot-base64。判断不确定时一律走默认(本地截图)。
- 文件路径优先使用绝对路径。
- 所有命令输出均为 JSON 格式,通过 success 字段判断是否成功。
- 常见的 Android 应用包名格式:com.company.appname;iOS 应用使用 Bundle ID 格式:com.company.appname。
- 环境切换:正式环境 UBOX_ENV=formal(默认),测试环境 UBOX_ENV=test。