dsh-mobilecode 0.7.1 → 0.8.0
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 +58 -2
- package/lib/device-build.js +121 -0
- package/lib/index.js +745 -1
- package/lib/mesh-hub.js +343 -0
- package/lib/vision.js +9 -6
- package/package.json +3 -2
- package/sdk/mesh-client.mjs +78 -0
package/README.md
CHANGED
|
@@ -205,6 +205,54 @@ classified adb boundary (argv-based, quoted, replay-safe):
|
|
|
205
205
|
(`engine: "anr-trace" | "logcat-crash"`). SIGQUIT refusal (system-uid or
|
|
206
206
|
non-debuggable process) degrades to the crash buffer with an explanatory note
|
|
207
207
|
instead of failing. Pass `package_name` or `pid`.
|
|
208
|
+
- `device_display` — read or change a device's **DPI and resolution**
|
|
209
|
+
(`wm density` / `wm size`): `get` reports physical + override values, `set`
|
|
210
|
+
overrides density and/or `width`x`height`, `reset` restores. Layouts reflow
|
|
211
|
+
instantly — re-observe with `device_screen` after a change.
|
|
212
|
+
- `device_avd_create` — create a **second virtual device** via `avdmanager`;
|
|
213
|
+
`clone_from` copies an existing AVD's full hardware config (same image, DPI,
|
|
214
|
+
RAM) so both players behave identically. Boot the new AVD with `device_boot`
|
|
215
|
+
(a running emulator holds 5554, so device #2 lands on `emulator-5556`).
|
|
216
|
+
- `device_batch` — fire 1..16 input actions **concurrently across devices**
|
|
217
|
+
(each step carries its own serial + the same fields `device_input` takes).
|
|
218
|
+
The co-op primitive for "both players press attack on the same frame"; a
|
|
219
|
+
failing step never blocks the others, and per-step results say what landed.
|
|
220
|
+
- `device_pair_capture` — capture **both devices at the same instant**: one
|
|
221
|
+
call, parallel screenshots + UI digests, both attached as real image blocks
|
|
222
|
+
so a vision model watches both players at once. `ocr: true` adds PaddleOCR.
|
|
223
|
+
|
|
224
|
+
**Co-op mesh (v0.8.0)** — two emulators cannot multicast-discover each other
|
|
225
|
+
(each sits behind its own slirp NAT), but every one of them reaches the host at
|
|
226
|
+
`10.0.2.2`. So the plugin hosts a LocalSend-style JSON pub/sub **hub**: games
|
|
227
|
+
join it and talk through it, and the AI model reads and steers the same wire.
|
|
228
|
+
|
|
229
|
+
- **Identity**: `POST /mesh/join {serial?, name?, role?}` — the serial pins a
|
|
230
|
+
stable, random, LocalSend-style callsign (`amber-fox`, `brisk-owl`…); the
|
|
231
|
+
join returns a per-process token that authorizes every later call.
|
|
232
|
+
- **Sessions**: `POST /mesh/link {with: [names…]}` (2–8 members), then
|
|
233
|
+
`POST /mesh/send {session, body}` and `GET /mesh/poll?id&token&after&wait`
|
|
234
|
+
(long-polls up to 30 s). `GET /mesh/peers` shows the roster; `POST /mesh/leave`
|
|
235
|
+
departs. Everything is JSON, 64 KiB per message.
|
|
236
|
+
- **Network conditions**: each session carries a policy — `latencyMs`,
|
|
237
|
+
`jitterMs`, `dropPct`, `dupPct`, `throttleKbps` (size-proportional delay) —
|
|
238
|
+
enforced per recipient by the hub. This is desync testing without touching
|
|
239
|
+
app code: make player B lag 400 ms with 100 ms jitter and watch the pair cope.
|
|
240
|
+
- **Agent side**: `mesh_status` (who joined, sessions, policies, undelivered
|
|
241
|
+
mail), `mesh_send` (ghost a message as any peer — the other game receives it
|
|
242
|
+
as if its partner sent it), `mesh_log` (every join/link/send/drop/dup/tune
|
|
243
|
+
event, timestamped), `mesh_tune` (set the policy), `mesh_reset`.
|
|
244
|
+
- **Security**: the routes are loopback-only (emulators reach the host loopback
|
|
245
|
+
via slirp); a request presenting browser headers (`Origin`/`Sec-Fetch-*`)
|
|
246
|
+
must additionally pass the trusted-browser stream fence, so a random web page
|
|
247
|
+
can never join a game or read its messages.
|
|
248
|
+
- **Client SDK**: `sdk/mesh-client.mjs` — a ~90-line dependency-free `fetch`
|
|
249
|
+
client (`join/link/send/inbox`) for Node/Deno/Bun/WebView/React-Native game
|
|
250
|
+
code; plain HTTP works from any other engine (Unity, Godot, Kotlin).
|
|
251
|
+
|
|
252
|
+
Typical loop: `device_avd_create` + `device_boot` a clone → both apps
|
|
253
|
+
`join` → `mesh_link` → `device_batch` inputs at both → `device_pair_capture`
|
|
254
|
+
to watch → `mesh_log` to see the traffic → `mesh_tune` to inject real-world
|
|
255
|
+
network pain.
|
|
208
256
|
|
|
209
257
|
**Conversation surface (v0.7.0)** — the transcript integration ported from
|
|
210
258
|
dsh-android's UI/UX:
|
|
@@ -259,7 +307,9 @@ plus the live-stream routes `GET /stream/status`, `POST /stream/grant`,
|
|
|
259
307
|
`POST /stream/devices` (online devices **and** configured AVDs),
|
|
260
308
|
`POST /stream/still` (one `screencap` as a data URL up to 4 MB),
|
|
261
309
|
`POST /stream/device-action` (the `device_action` verbs over the panel fence)
|
|
262
|
-
and `GET /stream/{token}` (the multipart frame body)
|
|
310
|
+
and `GET /stream/{token}` (the multipart frame body) — plus the co-op mesh
|
|
311
|
+
routes `POST /mesh/join`, `GET /mesh/peers`, `POST /mesh/{link,send,leave}`,
|
|
312
|
+
`GET /mesh/poll` (see **Co-op mesh** above). Every stream error carries
|
|
263
313
|
a machine-readable `code` next to the HTTP status — `token_invalid`,
|
|
264
314
|
`stream_not_running`, `stream_start_failed`, `devices_unavailable`,
|
|
265
315
|
`device_not_found`, `device_offline`, `unknown_action`, `bad_request`,
|
|
@@ -294,8 +344,14 @@ say *why* instead of "something failed".
|
|
|
294
344
|
lifecycle (park → halt → settle → execute; one project at a time; process
|
|
295
345
|
exit/signal hooks; win32 taskkill tree-kill).
|
|
296
346
|
- `lib/index.js` — host half: engine, `/api/dsh-mobilecode/*` routes (run
|
|
297
|
-
controls + welcome / doctor / ocr / settings), agent tools, system-prompt
|
|
347
|
+
controls + welcome / doctor / ocr / settings + mesh), agent tools, system-prompt
|
|
298
348
|
guidance section, `ctx.provide('mobilecode', handle)`.
|
|
349
|
+
- `lib/mesh-hub.js` — the co-op mesh core: peer identity (serial-pinned
|
|
350
|
+
callsigns + HMAC tokens), sessions, per-session network policy
|
|
351
|
+
(latency/jitter/drop/dup/throttle), a long-poll inbox, and an event log.
|
|
352
|
+
Pure + injectable clock/RNG, so `test/mesh-hub.mjs` covers it offline.
|
|
353
|
+
- `sdk/mesh-client.mjs` — dependency-free `fetch` client (join/link/send/
|
|
354
|
+
poll/inbox/leave) for game code running inside the emulators.
|
|
299
355
|
- `lib/setup.js` — settings store (~/.dsh/mobilecode/settings.json), the plugin
|
|
300
356
|
doctor (health checks + auto-fix), and the detached PaddleOCR installer
|
|
301
357
|
(writes an install script to disk and spawns it via cmd.exe, so the install
|
package/lib/device-build.js
CHANGED
|
@@ -1335,3 +1335,124 @@ export async function ocrImage(pngPath, lang = "ch") {
|
|
|
1335
1335
|
}
|
|
1336
1336
|
return []
|
|
1337
1337
|
}
|
|
1338
|
+
|
|
1339
|
+
// ── AVD creation + display overrides (v0.8.0) ────────────────────────────────
|
|
1340
|
+
|
|
1341
|
+
/** avdmanager.bat from the SDK cmdline-tools, or undefined. */
|
|
1342
|
+
export function avdmanagerBinary() {
|
|
1343
|
+
const sdk = androidSdk()
|
|
1344
|
+
if (!sdk) return undefined
|
|
1345
|
+
const exe = process.platform === "win32" ? "avdmanager.bat" : "avdmanager"
|
|
1346
|
+
const roots = [path.join(sdk, "cmdline-tools", "latest"), path.join(sdk, "cmdline-tools")]
|
|
1347
|
+
for (const root of roots) {
|
|
1348
|
+
const direct = path.join(root, "bin", exe)
|
|
1349
|
+
if (existsSync(direct)) return direct
|
|
1350
|
+
if (!existsSync(path.join(root, "bin"))) {
|
|
1351
|
+
try {
|
|
1352
|
+
for (const entry of readdirSync(root)) {
|
|
1353
|
+
const candidate = path.join(root, entry, "bin", exe)
|
|
1354
|
+
if (existsSync(candidate)) return candidate
|
|
1355
|
+
}
|
|
1356
|
+
} catch { /* nothing to enumerate */ }
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return undefined
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Installed system-image packages as `system-images;<api>;<tag>;<abi>` ids. */
|
|
1363
|
+
export async function installedSystemImages() {
|
|
1364
|
+
const sdk = androidSdk()
|
|
1365
|
+
if (!sdk) return []
|
|
1366
|
+
const ids = []
|
|
1367
|
+
const base = path.join(sdk, "system-images")
|
|
1368
|
+
for (const api of readdirSafe(base)) {
|
|
1369
|
+
for (const tag of readdirSafe(path.join(base, api))) {
|
|
1370
|
+
for (const abi of readdirSafe(path.join(base, api, tag))) {
|
|
1371
|
+
ids.push(`system-images;${api};${tag};${abi}`)
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return ids.sort()
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function readdirSafe(dir) {
|
|
1379
|
+
try { return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) } catch { return [] }
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/** config.ini path of a created AVD. */
|
|
1383
|
+
export function avdConfigPath(name) {
|
|
1384
|
+
return path.join(os.homedir(), ".android", "avd", `${name}.avd`, "config.ini")
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function parseIni(text) {
|
|
1388
|
+
const entries = []
|
|
1389
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
1390
|
+
const match = /^\s*([\w.]+)\s*=\s*(.*)$/.exec(line)
|
|
1391
|
+
if (match) entries.push([match[1], match[2].trim()])
|
|
1392
|
+
}
|
|
1393
|
+
return entries
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Merge an AVD config for hardware-parity cloning: identity/path keys of the
|
|
1398
|
+
* NEW avd stay theirs; every other key (hw.*, image.sysdir, runtime perms)
|
|
1399
|
+
* comes from the source. Pure so it can be unit-tested.
|
|
1400
|
+
*/
|
|
1401
|
+
export function mergeAvdConfig(newText, sourceText) {
|
|
1402
|
+
const KEEP = new Set(["avd.id", "avd.name", "path", "path.rel", "basedir", "displayname"])
|
|
1403
|
+
const base = parseIni(newText)
|
|
1404
|
+
const source = new Map(parseIni(sourceText).filter(([key]) => !KEEP.has(key)))
|
|
1405
|
+
const seen = new Set()
|
|
1406
|
+
const lines = base.map(([key, value]) => {
|
|
1407
|
+
seen.add(key)
|
|
1408
|
+
return source.has(key) ? `${key}=${source.get(key)}` : `${key}=${value}`
|
|
1409
|
+
})
|
|
1410
|
+
for (const [key, value] of source) if (!seen.has(key)) lines.push(`${key}=${value}`)
|
|
1411
|
+
return lines.join("\n") + "\n"
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Create an AVD via avdmanager. `cloneConfigText` (optional) is the source
|
|
1416
|
+
* AVD's config.ini — merged in afterwards for hardware parity. Throws with
|
|
1417
|
+
* avdmanager's own error text on failure.
|
|
1418
|
+
*/
|
|
1419
|
+
export async function createAvd({ name, imageId, deviceProfile, cloneConfigText }) {
|
|
1420
|
+
const binary = avdmanagerBinary()
|
|
1421
|
+
if (!binary) throw new Error("avdmanager not found in the SDK cmdline-tools — install it via sdkmanager.")
|
|
1422
|
+
const args = ["create", "avd", "-n", name, "-k", imageId]
|
|
1423
|
+
if (deviceProfile) args.push("-d", deviceProfile)
|
|
1424
|
+
args.push("--force")
|
|
1425
|
+
const result = await captureFull(binary, args, { env: javaEnv() })
|
|
1426
|
+
if (result.code !== 0) {
|
|
1427
|
+
const text = [result.out, result.err].filter(Boolean).join("\n")
|
|
1428
|
+
throw new Error(`avdmanager failed (exit ${result.code}):\n${text.slice(-600)}`)
|
|
1429
|
+
}
|
|
1430
|
+
const configPath = avdConfigPath(name)
|
|
1431
|
+
if (!existsSync(configPath)) throw new Error(`AVD "${name}" was created but config.ini is missing at ${configPath}.`)
|
|
1432
|
+
if (cloneConfigText) {
|
|
1433
|
+
const merged = mergeAvdConfig(readFileSync(configPath, "utf8"), cloneConfigText)
|
|
1434
|
+
writeFileSync(configPath, merged)
|
|
1435
|
+
}
|
|
1436
|
+
return { name, configPath }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** avdmanager/`sdkmanager` are Java tools: make sure JAVA_HOME-less shells still find java. */
|
|
1440
|
+
function javaEnv() {
|
|
1441
|
+
return { ANDROID_HOME: androidSdk(), ANDROID_SDK_ROOT: androidSdk() }
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/** Parse `wm density` output → {physical, override|null}. */
|
|
1445
|
+
export function parseWmDensity(text) {
|
|
1446
|
+
const physical = Number(/Physical density:\s*(\d+)/.exec(text)?.[1]) || undefined
|
|
1447
|
+
const override = Number(/Override density:\s*(\d+)/.exec(text)?.[1]) || undefined
|
|
1448
|
+
return { physical, override: override && override !== physical ? override : null }
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** Parse `wm size` output → {physical:{width,height}, override|null}. */
|
|
1452
|
+
export function parseWmSize(text) {
|
|
1453
|
+
const toSize = (m) => (m ? { width: Number(m[1]), height: Number(m[2]) } : undefined)
|
|
1454
|
+
const physical = toSize(/Physical size:\s*(\d+)x(\d+)/.exec(text))
|
|
1455
|
+
const override = toSize(/Override size:\s*(\d+)x(\d+)/.exec(text))
|
|
1456
|
+
const same = override && physical && override.width === physical.width && override.height === physical.height
|
|
1457
|
+
return { physical, override: same ? null : (override ?? null) }
|
|
1458
|
+
}
|