dsh-mobilecode 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/lib/device-build.js +60 -1
- package/lib/index.js +482 -5
- package/lib/skill.js +104 -0
- package/lib/uitree.js +638 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,14 +46,47 @@ drawer:
|
|
|
46
46
|
failed or incomplete), the Metro state, and `complete`.
|
|
47
47
|
- `device_detect` — which platforms a directory supports, the framework, and
|
|
48
48
|
the first attached Android device.
|
|
49
|
+
- A bundled **`device-ui-automation` playbook skill** (registered through
|
|
50
|
+
`ctx.skills.register()`, defensively — hosts without the skill service just
|
|
51
|
+
skip it): the observe-once → act-with-an-assertion → observe-again workflow,
|
|
52
|
+
which observer to reach for first, how to confirm an action landed via
|
|
53
|
+
`expect_text`/`expect_gone`, and the real-device safety rules.
|
|
49
54
|
- `device_screen` — see what is on an attached Android device: a PNG screenshot
|
|
50
55
|
plus the uiautomator UI hierarchy (text + pixel bounds) and **local PaddleOCR**
|
|
51
56
|
text recognition (text + confidence + box), so an agent can read the screen
|
|
52
57
|
and tap by coordinates. Also returns the foreground activity and screen size.
|
|
58
|
+
- `device_ui_tree` — the default screen observer: the uiautomator hierarchy as a
|
|
59
|
+
typed node tree (`type`/`text`/`contentDesc`/`resourceId`/`bounds`, with
|
|
60
|
+
`enabled`/`focused`/`clickable`/`scrollable` emitted only in their interesting
|
|
61
|
+
state), case-insensitive `filter` that keeps ancestors of matches, `max_depth`,
|
|
62
|
+
and a 40 KB cap that prunes the deepest levels first. Resource-ids are the most
|
|
63
|
+
stable tap handles.
|
|
64
|
+
- `device_tap_element` — tap a control by identity: `resource_id` matches the
|
|
65
|
+
node's resource-id, `text` matches its text or content-desc; exact match wins
|
|
66
|
+
over substring, nested duplicates collapse to the outermost control, ambiguity
|
|
67
|
+
lists up to 8 candidates instead of guessing, and disabled / off-screen nodes
|
|
68
|
+
are refused with the fix. `expect_text` / `expect_gone` verify the tap in the
|
|
69
|
+
same call — one round trip, no separate screenshot.
|
|
70
|
+
- `device_wait_for` — wait for text to appear or disappear: polls the UI tree
|
|
71
|
+
every ~600 ms and falls back to local PaddleOCR on textless surfaces (WebView /
|
|
72
|
+
Compose / canvas). A timeout is a normal `matched: false` result, never an
|
|
73
|
+
error — one call replaces an agent-side poll loop.
|
|
53
74
|
- `device_input` — act on the device: tap / swipe / type / press a key at
|
|
54
75
|
**absolute pixel coordinates** (the same space `device_screen` returns — take
|
|
55
76
|
the box center `x=(x1+x2)/2, y=(y1+y2)/2`). The deterministic control loop is
|
|
56
|
-
`
|
|
77
|
+
`device_ui_tree → device_tap_element`, falling back to
|
|
78
|
+
`device_screen → device_input` when a surface exposes no accessibility tree.
|
|
79
|
+
Typing is ASCII over plain adb; non-ASCII (CJK, emoji) is routed through the
|
|
80
|
+
ADBKeyboard IME when installed and refused with the install hint otherwise.
|
|
81
|
+
- `device_action` — device-level verbs beyond touches: `notifications`,
|
|
82
|
+
`quick_settings`, `collapse`, `lock`, `wake`, `assistant`, `rotate` (cycles
|
|
83
|
+
0→90→180→270 and pins auto-rotate off).
|
|
84
|
+
- `device_boot` / `device_shutdown` — boot an AVD by name and wait until it
|
|
85
|
+
finishes booting (adopts a running emulator for the same AVD) / shut an
|
|
86
|
+
emulator down (`adb emu kill`; refuses physical devices).
|
|
87
|
+
- `device_apps` / `device_launch_app` — list installed packages (third-party by
|
|
88
|
+
default) so a package name is never guessed / launch one by package or a
|
|
89
|
+
unique substring, with `relaunch` for a cold start.
|
|
57
90
|
- `device_log` — device logs: logcat `main`/`crash`/`events`/`kernel` buffers
|
|
58
91
|
(kernel = dmesg, needs adb root — works on emulators) with an optional
|
|
59
92
|
case-insensitive substring filter, capped line count.
|
package/lib/device-build.js
CHANGED
|
@@ -108,11 +108,19 @@ export async function capture(command, args, options = {}) {
|
|
|
108
108
|
})
|
|
109
109
|
running.stdout?.setEncoding("utf8")
|
|
110
110
|
running.stdout?.on("data", (chunk) => out.push(chunk))
|
|
111
|
+
let timer
|
|
112
|
+
if (options.timeoutMs) {
|
|
113
|
+
timer = setTimeout(() => running.kill(), options.timeoutMs)
|
|
114
|
+
timer.unref?.()
|
|
115
|
+
}
|
|
111
116
|
const code = await new Promise((resolve) => {
|
|
112
117
|
running.once("error", () => resolve(-1))
|
|
113
118
|
running.once("close", (value) => resolve(value ?? -1))
|
|
114
119
|
})
|
|
115
|
-
|
|
120
|
+
if (timer) clearTimeout(timer)
|
|
121
|
+
if (code !== 0) return ""
|
|
122
|
+
const text = out.join("")
|
|
123
|
+
return options.maxBytes && Buffer.byteLength(text) > options.maxBytes ? text.slice(0, options.maxBytes) : text
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/** Direct child pids of `pid`. POSIX only; returns nothing when pgrep is unavailable. */
|
|
@@ -732,6 +740,57 @@ export async function androidBooted(serial) {
|
|
|
732
740
|
return output.trim() === "1"
|
|
733
741
|
}
|
|
734
742
|
|
|
743
|
+
/** AVD name of an emulator serial (`adb emu avd name`); undefined for physical/offline. */
|
|
744
|
+
export async function avdName(serial) {
|
|
745
|
+
const output = await capture(adb(), ["-s", serial, "emu", "avd", "name"])
|
|
746
|
+
const line = output
|
|
747
|
+
.split(/\r?\n/)
|
|
748
|
+
.map((item) => item.trim())
|
|
749
|
+
.find((item) => item && !/^OK$/i.test(item) && !/^KO:/i.test(item))
|
|
750
|
+
return line && /^[A-Za-z0-9._-]+$/.test(line) ? line : undefined
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Poll until sys.boot_completed == 1 or the timeout; true when booted. */
|
|
754
|
+
export async function waitForBoot(serial, timeoutMs = 180_000) {
|
|
755
|
+
const deadline = Date.now() + timeoutMs
|
|
756
|
+
for (;;) {
|
|
757
|
+
if (await androidBooted(serial)) return true
|
|
758
|
+
if (Date.now() >= deadline) return false
|
|
759
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Launch an AVD detached (survives this process); undefined when no emulator binary. */
|
|
764
|
+
export function bootEmulator(avd) {
|
|
765
|
+
const emulator = emulatorBinary()
|
|
766
|
+
if (!emulator || !/^[A-Za-z0-9._-]+$/.test(avd)) return undefined
|
|
767
|
+
const child = launch(emulator, ["-avd", avd], { stdio: "ignore", detached: true })
|
|
768
|
+
child.unref?.()
|
|
769
|
+
return child
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** True when the ADBKeyboard IME is installed (the only way to type non-ASCII over adb). */
|
|
773
|
+
export async function adbKeyboardReady(serial) {
|
|
774
|
+
const output = await capture(adb(), ["-s", serial, "shell", "ime", "list", "-s"])
|
|
775
|
+
return /com\.android\.adbkeyboard/i.test(output)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Type one string via the ADBKeyboard broadcast (base64, so any codepoint survives the shell). */
|
|
779
|
+
export async function typeViaAdbKeyboard(serial, text) {
|
|
780
|
+
const msg = Buffer.from(text, "utf8").toString("base64")
|
|
781
|
+
await exec(adb(), ["-s", serial, "shell", "am", "broadcast", "-a", "ADB_INPUT_B64", "--es", "msg", msg]).exit
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** True when every codepoint is safe for `adb shell input text` (printable ASCII, no shell metachars). */
|
|
785
|
+
export function isAsciiInput(text) {
|
|
786
|
+
return [...text].every((ch) => ch.codePointAt(0) >= 0x20 && ch.codePointAt(0) <= 0x7e)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Escape one `input text` argument for the device shell: backslash the metachars, spaces to %s. */
|
|
790
|
+
export function escapeInputText(text) {
|
|
791
|
+
return text.replace(/[\\()<>|;&*~"'`$#!{}[\]]/g, "\\$&").replace(/ /g, "%s")
|
|
792
|
+
}
|
|
793
|
+
|
|
735
794
|
function aapt2() {
|
|
736
795
|
const sdk = androidSdk()
|
|
737
796
|
if (!sdk) return undefined
|