让 LLM 用当前目录的 TypeScript Device 客户端,稳定执行“单步观察 -> 单步动作 -> 再观察”的手机自动化。
适用场景:
- 需要真实设备 UI 自动化(不走 Python 端流程脚本)。
- 每次只执行一个动作,由 LLM 根据返回结果决定下一步。
- 重点支持
X(Twitter)、小红书等动态 UI 应用。
- 连接地址优先使用:
http://<device-ip>:9008/jsonrpc/0 - 当前环境实测可用示例:
http://192.168.31.179:9008/jsonrpc/0 - 不要一次写长流程脚本;优先
tsx/ts-node单步执行。 - 每一步都记录:
app_current()- 关键元素
exists()/count()/center() - 动作返回值(
click/set_text)
安装后(包名以实际发布名为准):
npm i <your-package-name>CommonJS:
const { Device } = require("<your-package-name>");ESM:
import { Device } from "<your-package-name>";最小初始化(推荐):
const d = new Device({
rpcUrl: "http://192.168.31.179:9008/jsonrpc/0",
defaultTimeoutMs: 30000,
});参数说明:
rpcUrl: uiautomator2 JSON-RPC 地址,通常是http://<ip>:9008/jsonrpc/0defaultTimeoutMs: 单次 RPC 默认超时,建议30000
初始化后建议先探活:
await d.ping();
console.log(await d.app_current());- 优先:
d.xpath(...)
- 动态页面、中文文案、复杂结构优先使用。
click/set_text有 JSON-RPC 失败时的 fallback(按 bounds 点击)。
- 次选:
d.$({...})
- 适合 Python 风格 selector 迁移。
- 已支持:
exists/wait/wait_goneclick/click_existsset_text/get_text/clear_textcount/instance/atright/left/up/downcenter/screenshot
- 尽量避免直接依赖服务端
exist/count语义
- 某些页面会出现服务端
NullPointerException或计数异常。 - 当前实现已将
d.$({...})主路径切到本地 XPath 匹配,稳定性更高。
const { Device } = require("./dist/cjs/device");
async function main() {
const d = new Device({ rpcUrl: "http://192.168.31.179:9008/jsonrpc/0" });
console.log(await d.app_current());
console.log(await d.$({ resourceId: "xxx" }).exists(3));
console.log(await d.$({ resourceId: "xxx" }).click(3));
console.log(await d.app_current());
}
main().catch(console.error);执行建议:
- 每次只做一个动作。
- 动作后
sleep(0.6~1.2s)再读取状态。 - 先看 activity,再决定是否继续点。
很多 App 的“新建/添加/发布”入口是二段式:
- 第一次点击只展开浮层或菜单,页面主
activity不变。 - 第二次点击(同一入口或浮层中的可点击项)才真正进入目标页。
通用判定规则:
- 点击入口后读取
app_current()。 - 如果
activity未变化,抓当前 XML 看是否出现新浮层节点。 - 在新浮层内只点
@clickable="true"的候选节点。 - 不要点同名但
clickable=false的文本标签。
很多编辑页退出不是单步 back:
- 第一步:点导航返回(例如
Navigate up/ 返回图标)。 - 第二步:处理确认弹窗(例如
删除/放弃/不保存vs保存)。
通用退出策略:
- 优先查找并点击“导航返回”控件。
- 立即抓弹窗节点,按语义优先选“不保留内容”按钮(如
删除/放弃/Discard)。 - 若无弹窗再尝试
press("back")兜底。 - 每一步后都检查
app_current(),直到回到目标主页面。
推荐:
- 先确保目标输入框可见且已聚焦(
exists+click)。 - 使用
set_text(当前封装会做稳定 fallback)。 - 输入后再次检测发布按钮是否出现。
- 看 activity 是否变化:
app_current() - 看节点是否真可点击:
@clickable="true" - 抓节点原始
raw看content-desc/text/bounds - 如果是菜单入口,验证是否需要二段点击
- 如果退出失败,先抓弹窗按钮真实文案再定向点击
d(text="A", className="B")->d.$({ text: "A", className: "B" })d(...).count->await d.$(...).count()d(...)[0]->d.$(...).at(0)d(...).right(className="X")->d.$(...).right({ className: "X" })d(...).center()->await d.$(...).center()d(...).screenshot().save("a.jpg")->const im = await d.$(...).screenshot(); await im?.save("a.jpg")
- 只做当前一步,不提前假设后续页面。
- 每一步都基于“最新返回值”决策下一步。
- 能用 XPath 时优先 XPath,减少 selector 服务端差异影响。