@phone-use/sdk 0.4.0 → 0.5.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/dist/index.d.mts +355 -136
- package/dist/index.mjs +1112 -362
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/backends/android-hierarchy.ts +153 -0
- package/src/backends/android.ts +890 -0
- package/src/backends/cloud-sandbox.ts +43 -4
- package/src/index.ts +17 -2
- package/src/lifecycle.ts +3 -3
- package/src/observe.ts +43 -9
|
@@ -42,7 +42,18 @@ export type SandboxRpcMethod =
|
|
|
42
42
|
| 'back'
|
|
43
43
|
| 'openApp'
|
|
44
44
|
| 'listApps'
|
|
45
|
-
| 'closeSession'
|
|
45
|
+
| 'closeSession'
|
|
46
|
+
| 'checkpoint'
|
|
47
|
+
| 'restoreCheckpoint'
|
|
48
|
+
| 'listCheckpoints'
|
|
49
|
+
| 'deleteCheckpoint';
|
|
50
|
+
|
|
51
|
+
/** One saved device state, as reported by the worker. */
|
|
52
|
+
export type SandboxCheckpointInfo = {
|
|
53
|
+
id: string;
|
|
54
|
+
label?: string | undefined;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
};
|
|
46
57
|
|
|
47
58
|
/** Body of `POST <endpoint>/rpc`: one verb and its positional arguments. */
|
|
48
59
|
export type SandboxRpcRequest = { method: SandboxRpcMethod; args: unknown[] };
|
|
@@ -162,6 +173,36 @@ export class CloudSandboxBackend extends BaseDeviceBackend {
|
|
|
162
173
|
return this.rpc('closeSession');
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
// -------------------------------------------------------------------------
|
|
177
|
+
// Checkpoints: save/restore the WHOLE device state (apps, logins, photos,
|
|
178
|
+
// settings). Not a UI snapshot — see `snapshot` for the accessibility tree.
|
|
179
|
+
// The device is briefly unavailable while a checkpoint saves or restores
|
|
180
|
+
// (shutdown → clone → boot on the worker); verbs issued meanwhile fail and
|
|
181
|
+
// should be retried after the checkpoint call resolves.
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/** Save the device's current state. Returns the checkpoint's id. */
|
|
185
|
+
checkpoint(opts?: { label?: string }): Promise<SandboxCheckpointInfo> {
|
|
186
|
+
return this.rpc('checkpoint', opts);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Restore a checkpoint by id or label. The checkpoint survives — restore as
|
|
190
|
+
* often as needed. Device identity may change server-side; the sandbox
|
|
191
|
+
* endpoint/token stay valid. */
|
|
192
|
+
restoreCheckpoint(ref: string): Promise<{ checkpointId: string }> {
|
|
193
|
+
return this.rpc('restoreCheckpoint', ref);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** List this sandbox's checkpoints. */
|
|
197
|
+
listCheckpoints(): Promise<SandboxCheckpointInfo[]> {
|
|
198
|
+
return this.rpc('listCheckpoints');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Delete a checkpoint by id or label. */
|
|
202
|
+
deleteCheckpoint(ref: string): Promise<{ deleted: boolean }> {
|
|
203
|
+
return this.rpc('deleteCheckpoint', ref);
|
|
204
|
+
}
|
|
205
|
+
|
|
165
206
|
/** Upload a zipped .app bundle (base64) and install it on the sandbox device. */
|
|
166
207
|
/**
|
|
167
208
|
* Upload a zipped .app as a raw stream. Prefer this over {@link installApp}:
|
|
@@ -186,9 +227,7 @@ export class CloudSandboxBackend extends BaseDeviceBackend {
|
|
|
186
227
|
const bytes = await zip.arrayBuffer();
|
|
187
228
|
return this.installApp(Buffer.from(bytes).toString('base64'));
|
|
188
229
|
}
|
|
189
|
-
const body = (await response.json().catch(() => ({}))) as
|
|
190
|
-
| SandboxRpcResponse
|
|
191
|
-
| { error?: string };
|
|
230
|
+
const body = (await response.json().catch(() => ({}))) as SandboxRpcResponse | { error?: string };
|
|
192
231
|
if (!response.ok || !('ok' in body)) {
|
|
193
232
|
const message =
|
|
194
233
|
('error' in body && typeof body.error === 'string' ? body.error : undefined) ??
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @phone-use/sdk — the device runtime SDK: engine-as-object lifecycle
|
|
3
|
-
* (ios.launch/connect → Device), Device backends, config, errors, capabilities,
|
|
3
|
+
* (ios.launch/connect and android.launch/connect → Device), Device backends, config, errors, capabilities,
|
|
4
4
|
* and the action verb surface.
|
|
5
5
|
*
|
|
6
6
|
* The test double (FakeBackend) lives on the "@phone-use/sdk/testing" subpath,
|
|
7
7
|
* deliberately not re-exported here.
|
|
8
8
|
*/
|
|
9
9
|
/** The published package version (kept in sync with package.json by the release flow). */
|
|
10
|
-
export const VERSION = '0.
|
|
10
|
+
export const VERSION = '0.5.0';
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
13
|
type Action,
|
|
@@ -31,10 +31,23 @@ export {
|
|
|
31
31
|
registerBackend,
|
|
32
32
|
} from './backend.ts';
|
|
33
33
|
export { createAgentDeviceBackend } from './backends/agent-device.ts';
|
|
34
|
+
export {
|
|
35
|
+
AndroidBackend,
|
|
36
|
+
type AndroidBackendOptions,
|
|
37
|
+
type AndroidConnectOptions,
|
|
38
|
+
type AndroidDeviceInfo,
|
|
39
|
+
type AndroidLaunchOptions,
|
|
40
|
+
type AndroidListOptions,
|
|
41
|
+
android,
|
|
42
|
+
type BinaryExecRunner,
|
|
43
|
+
createAndroidBackend,
|
|
44
|
+
type EmulatorProcess,
|
|
45
|
+
} from './backends/android.ts';
|
|
34
46
|
export {
|
|
35
47
|
CloudSandboxBackend,
|
|
36
48
|
type CloudSandboxBackendOptions,
|
|
37
49
|
createCloudSandboxBackend,
|
|
50
|
+
type SandboxCheckpointInfo,
|
|
38
51
|
type SandboxRpcFailure,
|
|
39
52
|
type SandboxRpcMethod,
|
|
40
53
|
type SandboxRpcRequest,
|
|
@@ -97,5 +110,7 @@ export { SecretStore } from './secrets.ts';
|
|
|
97
110
|
// of the *published* dist, where the barrel is the entry).
|
|
98
111
|
import { registerBackend as _register } from './backend.ts';
|
|
99
112
|
import { createAgentDeviceBackend as _createAd } from './backends/agent-device.ts';
|
|
113
|
+
import { createAndroidBackend as _createAndroid } from './backends/android.ts';
|
|
100
114
|
|
|
101
115
|
_register('agent-device', _createAd);
|
|
116
|
+
_register('android-adb', _createAndroid);
|
package/src/lifecycle.ts
CHANGED
|
@@ -21,8 +21,8 @@ export type DeviceStatus = 'running' | 'closed';
|
|
|
21
21
|
/**
|
|
22
22
|
* The Device lifecycle handle: id, pinned backend, close/dispose, idle lease +
|
|
23
23
|
* reaper — plus the action verb surface layered onto the same type.
|
|
24
|
-
* `ios.launch()` and `
|
|
25
|
-
*
|
|
24
|
+
* `ios.launch()`/`ios.connect()` and `android.launch()`/`android.connect()`
|
|
25
|
+
* all return it, built over `createDeviceHandle`.
|
|
26
26
|
*/
|
|
27
27
|
export interface Device {
|
|
28
28
|
/** udid (iOS) / serial (Android). */
|
|
@@ -177,7 +177,7 @@ export type CreateDeviceHandleOptions = {
|
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
179
|
* Assemble a Device handle over a backend: lease/reaper, verb surface,
|
|
180
|
-
* close/dispose semantics. Engine authors (ios
|
|
180
|
+
* close/dispose semantics. Engine authors (ios and android here,
|
|
181
181
|
* phone-backend-* third parties) build on this; tests fabricate devices with
|
|
182
182
|
* it over a FakeBackend.
|
|
183
183
|
*/
|
package/src/observe.ts
CHANGED
|
@@ -94,6 +94,8 @@ function keptViewportNodes(nodes: SnapshotNode[]): {
|
|
|
94
94
|
kept: SnapshotNode[];
|
|
95
95
|
above: number;
|
|
96
96
|
below: number;
|
|
97
|
+
aboveNames: string[];
|
|
98
|
+
belowNames: string[];
|
|
97
99
|
suppressFocused: boolean;
|
|
98
100
|
vw: number;
|
|
99
101
|
} {
|
|
@@ -109,23 +111,47 @@ function keptViewportNodes(nodes: SnapshotNode[]): {
|
|
|
109
111
|
const kept: SnapshotNode[] = [];
|
|
110
112
|
let above = 0;
|
|
111
113
|
let below = 0;
|
|
114
|
+
// Name the actionable controls hiding off-screen instead of only counting
|
|
115
|
+
// them. Measured failure (AndroidWorld MarkorCreateNote): the create-note
|
|
116
|
+
// FAB sat below the fold, the render said only "[9 elements below]", and the
|
|
117
|
+
// agent burned its whole budget guessing labels (`tap "+"`) for a button it
|
|
118
|
+
// was never told existed. Tappable roles only, capped, so a long list view
|
|
119
|
+
// doesn't flood the render with row labels.
|
|
120
|
+
const aboveNames: string[] = [];
|
|
121
|
+
const belowNames: string[] = [];
|
|
122
|
+
const controlName = (n: SnapshotNode): string | undefined => {
|
|
123
|
+
const role = n.role ?? n.type ?? '';
|
|
124
|
+
if (!TAPPABLE.has(role) || role === 'StaticText' || role === 'Cell') return undefined;
|
|
125
|
+
const label = (n.label ?? n.identifier ?? '').trim();
|
|
126
|
+
return label || undefined;
|
|
127
|
+
};
|
|
112
128
|
for (const n of nodes) {
|
|
113
129
|
if (isNoise(n)) continue;
|
|
114
130
|
if (!intersectsViewport(n.rect, vw, vh)) {
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
const name = controlName(n);
|
|
132
|
+
if (n.rect && n.rect.y >= vh) {
|
|
133
|
+
below += 1;
|
|
134
|
+
if (name && belowNames.length < 5 && !belowNames.includes(name)) belowNames.push(name);
|
|
135
|
+
} else {
|
|
136
|
+
above += 1;
|
|
137
|
+
if (name && aboveNames.length < 5 && !aboveNames.includes(name)) aboveNames.push(name);
|
|
138
|
+
}
|
|
117
139
|
continue;
|
|
118
140
|
}
|
|
119
141
|
kept.push(n);
|
|
120
142
|
}
|
|
121
|
-
return { kept, above, below, suppressFocused, vw };
|
|
143
|
+
return { kept, above, below, aboveNames, belowNames, suppressFocused, vw };
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
function compressNodes(nodes: SnapshotNode[]): string {
|
|
125
|
-
const { kept, above, below, suppressFocused, vw } = keptViewportNodes(nodes);
|
|
147
|
+
const { kept, above, below, aboveNames, belowNames, suppressFocused, vw } = keptViewportNodes(nodes);
|
|
126
148
|
const lines = kept.map((n) => formatNode(n, { suppressFocused, vw }));
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
const controls = (names: string[]) =>
|
|
150
|
+
names.length ? ` — controls there: ${names.map((n) => `"${n}"`).join(', ')}` : '';
|
|
151
|
+
if (above > 0)
|
|
152
|
+
lines.unshift(`[${above} elements above the viewport — scroll up to reach them${controls(aboveNames)}]`);
|
|
153
|
+
if (below > 0)
|
|
154
|
+
lines.push(`[${below} elements below the viewport — scroll down to reach them${controls(belowNames)}]`);
|
|
129
155
|
return lines.join('\n');
|
|
130
156
|
}
|
|
131
157
|
|
|
@@ -458,7 +484,9 @@ export class DeviceCore {
|
|
|
458
484
|
*/
|
|
459
485
|
renderObservation(full = false): string {
|
|
460
486
|
const app = this.lastApp.app;
|
|
461
|
-
const { kept, above, below, suppressFocused, vw } = keptViewportNodes(
|
|
487
|
+
const { kept, above, below, aboveNames, belowNames, suppressFocused, vw } = keptViewportNodes(
|
|
488
|
+
this.cachedNodes,
|
|
489
|
+
);
|
|
462
490
|
const keys = kept.map(elementKey);
|
|
463
491
|
const lineByKey = new Map<string, string>();
|
|
464
492
|
for (const n of kept) lineByKey.set(elementKey(n), formatNode(n, { suppressFocused, vw }));
|
|
@@ -489,8 +517,14 @@ export class DeviceCore {
|
|
|
489
517
|
// Full render + refresh the baseline.
|
|
490
518
|
this.lastRender = { app, keys, lineByKey };
|
|
491
519
|
const lines = kept.map((n) => formatNode(n, { suppressFocused, vw }));
|
|
492
|
-
|
|
493
|
-
|
|
520
|
+
const controls = (names: string[]) =>
|
|
521
|
+
names.length ? ` — controls there: ${names.map((n) => `"${n}"`).join(', ')}` : '';
|
|
522
|
+
if (above > 0)
|
|
523
|
+
lines.unshift(
|
|
524
|
+
`[${above} elements above the viewport — scroll up to reach them${controls(aboveNames)}]`,
|
|
525
|
+
);
|
|
526
|
+
if (below > 0)
|
|
527
|
+
lines.push(`[${below} elements below the viewport — scroll down to reach them${controls(belowNames)}]`);
|
|
494
528
|
return lines.join('\n');
|
|
495
529
|
}
|
|
496
530
|
|