Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Latest commit

 

History

History
159 lines (117 loc) · 4.92 KB

File metadata and controls

159 lines (117 loc) · 4.92 KB

TypeScript 手机自动化实战 Skill(uiautomator2 JSON-RPC)

目标

让 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 包调用

安装后(包名以实际发布名为准):

npm i <your-package-name>

CommonJS:

const { Device } = require("<your-package-name>");

ESM:

import { Device } from "<your-package-name>";

Device 初始化

最小初始化(推荐):

const d = new Device({
  rpcUrl: "http://192.168.31.179:9008/jsonrpc/0",
  defaultTimeoutMs: 30000,
});

参数说明:

  • rpcUrl: uiautomator2 JSON-RPC 地址,通常是 http://<ip>:9008/jsonrpc/0
  • defaultTimeoutMs: 单次 RPC 默认超时,建议 30000

初始化后建议先探活:

await d.ping();
console.log(await d.app_current());

推荐 API 顺序

  1. 优先:d.xpath(...)
  • 动态页面、中文文案、复杂结构优先使用。
  • click/set_text 有 JSON-RPC 失败时的 fallback(按 bounds 点击)。
  1. 次选:d.$({...})
  • 适合 Python 风格 selector 迁移。
  • 已支持:
    • exists/wait/wait_gone
    • click/click_exists
    • set_text/get_text/clear_text
    • count/instance/at
    • right/left/up/down
    • center/screenshot
  1. 尽量避免直接依赖服务端 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,再决定是否继续点。

通用交互模式(重点)

1) 二段点击入口(菜单展开型)

很多 App 的“新建/添加/发布”入口是二段式:

  • 第一次点击只展开浮层或菜单,页面主 activity 不变。
  • 第二次点击(同一入口或浮层中的可点击项)才真正进入目标页。

通用判定规则:

  1. 点击入口后读取 app_current()
  2. 如果 activity 未变化,抓当前 XML 看是否出现新浮层节点。
  3. 在新浮层内只点 @clickable="true" 的候选节点。
  4. 不要点同名但 clickable=false 的文本标签。

2) 二段退出(导航返回 + 确认弹窗)

很多编辑页退出不是单步 back

  • 第一步:点导航返回(例如 Navigate up / 返回图标)。
  • 第二步:处理确认弹窗(例如 删除/放弃/不保存 vs 保存)。

通用退出策略:

  1. 优先查找并点击“导航返回”控件。
  2. 立即抓弹窗节点,按语义优先选“不保留内容”按钮(如 删除/放弃/Discard)。
  3. 若无弹窗再尝试 press("back") 兜底。
  4. 每一步后都检查 app_current(),直到回到目标主页面。

输入策略(中文)

推荐:

  1. 先确保目标输入框可见且已聚焦(exists + click)。
  2. 使用 set_text(当前封装会做稳定 fallback)。
  3. 输入后再次检测发布按钮是否出现。

调试清单(出现“点了没反应”时)

  1. 看 activity 是否变化:app_current()
  2. 看节点是否真可点击:@clickable="true"
  3. 抓节点原始 rawcontent-desc/text/bounds
  4. 如果是菜单入口,验证是否需要二段点击
  5. 如果退出失败,先抓弹窗按钮真实文案再定向点击

迁移对照(Python -> TS)

  • 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 服务端差异影响。