@mobileaidev/ai-app-bridge 0.2.6 → 0.2.8
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 +85 -65
- package/bin/ai-app-bridge.js +561 -465
- package/bin/mcp-server.js +755 -741
- package/package.json +7 -6
- package/skills/ai-app-bridge-use/SKILL.md +183 -0
- package/skills/ai-app-bridge-use/agents/openai.yaml +4 -0
package/bin/ai-app-bridge.js
CHANGED
|
@@ -31,6 +31,8 @@ Commands:
|
|
|
31
31
|
network Read generic in-app network records.
|
|
32
32
|
state Read generic in-app state records.
|
|
33
33
|
events Read generic in-app event records.
|
|
34
|
+
freeze-app Stop the target app processes with SIGSTOP.
|
|
35
|
+
thaw-app Resume the target app processes with SIGCONT.
|
|
34
36
|
|
|
35
37
|
H5/WebView commands:
|
|
36
38
|
h5-dom Read native Android WebView DOM.
|
|
@@ -48,7 +50,7 @@ Flutter commands:
|
|
|
48
50
|
flutter-nodes Read Flutter operable nodes.
|
|
49
51
|
flutter-action Dispatch a raw Flutter action payload.
|
|
50
52
|
tap-flutter-text Tap a Flutter node by text.
|
|
51
|
-
input-flutter-text Set Flutter text through the Flutter action bridge.
|
|
53
|
+
input-flutter-text Set Flutter text through the Flutter action bridge.
|
|
52
54
|
scroll-flutter Scroll Flutter content.
|
|
53
55
|
flutter-h5-dom Read DOM through a Flutter H5 adapter.
|
|
54
56
|
flutter-h5-eval Execute JavaScript through a Flutter H5 adapter.
|
|
@@ -62,19 +64,19 @@ Device/action commands:
|
|
|
62
64
|
tap-text Tap a visible node by exact text or content description.
|
|
63
65
|
tap-uia-text Tap a UIAutomator node by text.
|
|
64
66
|
wait-text Wait until text appears in bridge or UIAutomator output.
|
|
65
|
-
input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
|
|
67
|
+
input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
|
|
66
68
|
swipe Swipe device coordinates through ADB.
|
|
67
69
|
keyevent Send an Android keyevent through ADB.
|
|
68
70
|
keyboard-state Read Android soft keyboard visibility from dumpsys.
|
|
69
71
|
hide-keyboard Hide the Android soft keyboard when it is visible.
|
|
70
72
|
|
|
71
|
-
App/permission commands:
|
|
72
|
-
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
-
clear-app-data Clear target app local data through the bridge runtime.
|
|
74
|
-
launch-app Launch the target package LAUNCHER Activity.
|
|
75
|
-
launch-activity Launch an explicit Android Activity component.
|
|
76
|
-
launch-native-test Launch the debug native Android bridge test Activity.
|
|
77
|
-
launch-flutter Launch the Flutter Activity.
|
|
73
|
+
App/permission commands:
|
|
74
|
+
install-apk Install an APK and assist device-side installer screens.
|
|
75
|
+
clear-app-data Clear target app local data through the bridge runtime.
|
|
76
|
+
launch-app Launch the target package LAUNCHER Activity.
|
|
77
|
+
launch-activity Launch an explicit Android Activity component.
|
|
78
|
+
launch-native-test Launch the debug native Android bridge test Activity.
|
|
79
|
+
launch-flutter Launch the Flutter Activity.
|
|
78
80
|
permission-state Read Android runtime permission state.
|
|
79
81
|
permission-grant Grant an Android runtime permission.
|
|
80
82
|
permission-revoke Revoke an Android runtime permission.
|
|
@@ -95,14 +97,14 @@ Options:
|
|
|
95
97
|
--adb-timeout-ms <ms> Timeout for ADB subprocesses.
|
|
96
98
|
--out-file <path> Screenshot output path.
|
|
97
99
|
--artifact-dir <path> Directory for generated screenshot/artifact defaults.
|
|
98
|
-
--apk-path <path> APK path used by install-apk.
|
|
99
|
-
--activity <name> Activity class for launch-activity or launch-app override.
|
|
100
|
-
--component <pkg/act> Explicit Android component for launch-activity.
|
|
101
|
-
--action <name> Intent action for launch-activity.
|
|
102
|
-
--category <name> Intent category for launch-activity; may be repeated.
|
|
103
|
-
--data <uri> Intent data URI for launch-activity.
|
|
104
|
-
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
105
|
-
--text <text> Text used by Unicode-safe bridge input commands.
|
|
100
|
+
--apk-path <path> APK path used by install-apk.
|
|
101
|
+
--activity <name> Activity class for launch-activity or launch-app override.
|
|
102
|
+
--component <pkg/act> Explicit Android component for launch-activity.
|
|
103
|
+
--action <name> Intent action for launch-activity.
|
|
104
|
+
--category <name> Intent category for launch-activity; may be repeated.
|
|
105
|
+
--data <uri> Intent data URI for launch-activity.
|
|
106
|
+
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
107
|
+
--text <text> Text used by Unicode-safe bridge input commands.
|
|
106
108
|
--value <text> Text value used by h5-input or flutter-h5-input.
|
|
107
109
|
--selector <css> CSS selector used by H5 commands.
|
|
108
110
|
--target-text <text> Text used by tap-text or wait-text.
|
|
@@ -156,7 +158,7 @@ Options:
|
|
|
156
158
|
--permission <name> Android permission for permission-* commands.
|
|
157
159
|
--op <name> App-op name for appops-set.
|
|
158
160
|
--mode <mode> App-op mode for appops-set.
|
|
159
|
-
--pid <pid|current> PID filter for logcat.
|
|
161
|
+
--pid <pid|current> PID filter for logcat, or explicit process for freeze/thaw.
|
|
160
162
|
--app-pid Filter logcat by current app pid.
|
|
161
163
|
--tag <tag[,tag]> Logcat tag filter.
|
|
162
164
|
--level <level> Minimum logcat level.
|
|
@@ -230,19 +232,19 @@ async function runCommand(command, options, ctx) {
|
|
|
230
232
|
}
|
|
231
233
|
case 'flutter-nodes':
|
|
232
234
|
return flutterNodes(ctx);
|
|
233
|
-
case 'tap-flutter-text':
|
|
234
|
-
return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
|
|
235
|
-
case 'input-flutter-text': {
|
|
236
|
-
const result = await flutterAction(ctx, {
|
|
237
|
-
action: 'inputText',
|
|
238
|
-
text: requiredString(options.text, 'text'),
|
|
239
|
-
...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
|
|
240
|
-
});
|
|
241
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
242
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
243
|
-
}
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
235
|
+
case 'tap-flutter-text':
|
|
236
|
+
return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
|
|
237
|
+
case 'input-flutter-text': {
|
|
238
|
+
const result = await flutterAction(ctx, {
|
|
239
|
+
action: 'inputText',
|
|
240
|
+
text: requiredString(options.text, 'text'),
|
|
241
|
+
...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
|
|
242
|
+
});
|
|
243
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
244
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
246
248
|
case 'scroll-flutter':
|
|
247
249
|
return options.targetText
|
|
248
250
|
? flutterAction(ctx, { action: 'scrollUntilText', text: options.targetText, maxSwipes: Number(options.maxSwipes || 12) })
|
|
@@ -257,6 +259,10 @@ async function runCommand(command, options, ctx) {
|
|
|
257
259
|
return bridgeGet(ctx, withQuery('/v1/state', captureQuery(options)));
|
|
258
260
|
case 'events':
|
|
259
261
|
return bridgeGet(ctx, withQuery('/v1/events', captureQuery(options)));
|
|
262
|
+
case 'freeze-app':
|
|
263
|
+
return freezeApp(ctx, options);
|
|
264
|
+
case 'thaw-app':
|
|
265
|
+
return thawApp(ctx, options);
|
|
260
266
|
case 'h5-dom':
|
|
261
267
|
return bridgeGet(ctx, '/v1/h5/dom');
|
|
262
268
|
case 'h5-eval':
|
|
@@ -297,12 +303,12 @@ async function runCommand(command, options, ctx) {
|
|
|
297
303
|
return keyboardState(ctx);
|
|
298
304
|
case 'hide-keyboard':
|
|
299
305
|
return hideKeyboard(ctx, options);
|
|
300
|
-
case 'install-apk':
|
|
301
|
-
return installApk(ctx, options);
|
|
302
|
-
case 'clear-app-data':
|
|
303
|
-
return clearAppData(ctx);
|
|
304
|
-
case 'webview-pages':
|
|
305
|
-
return webviewPages(ctx, options);
|
|
306
|
+
case 'install-apk':
|
|
307
|
+
return installApk(ctx, options);
|
|
308
|
+
case 'clear-app-data':
|
|
309
|
+
return clearAppData(ctx);
|
|
310
|
+
case 'webview-pages':
|
|
311
|
+
return webviewPages(ctx, options);
|
|
306
312
|
case 'webview-network':
|
|
307
313
|
return webviewCdpCapture(ctx, { ...options, captureNetwork: true, captureConsole: true });
|
|
308
314
|
case 'webview-console':
|
|
@@ -330,15 +336,15 @@ async function runCommand(command, options, ctx) {
|
|
|
330
336
|
return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
|
|
331
337
|
case 'tap-uia-text':
|
|
332
338
|
return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
|
|
333
|
-
case 'permission-dialog':
|
|
334
|
-
return permissionDialog(ctx, options);
|
|
335
|
-
case 'launch-app':
|
|
336
|
-
return launchApp(ctx, options);
|
|
337
|
-
case 'launch-activity':
|
|
338
|
-
return launchActivity(ctx, options);
|
|
339
|
-
case 'launch-native-test':
|
|
340
|
-
return launchNativeTest(ctx);
|
|
341
|
-
case 'launch-flutter':
|
|
339
|
+
case 'permission-dialog':
|
|
340
|
+
return permissionDialog(ctx, options);
|
|
341
|
+
case 'launch-app':
|
|
342
|
+
return launchApp(ctx, options);
|
|
343
|
+
case 'launch-activity':
|
|
344
|
+
return launchActivity(ctx, options);
|
|
345
|
+
case 'launch-native-test':
|
|
346
|
+
return launchNativeTest(ctx);
|
|
347
|
+
case 'launch-flutter':
|
|
342
348
|
return launchFlutter(ctx, options.initialRoute || '');
|
|
343
349
|
case 'smoke':
|
|
344
350
|
return smoke(ctx, options);
|
|
@@ -362,25 +368,25 @@ function parseArgs(argv) {
|
|
|
362
368
|
const rawName = arg.slice(2);
|
|
363
369
|
const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
364
370
|
const next = argv[index + 1];
|
|
365
|
-
if (next === undefined || next.startsWith('--')) {
|
|
366
|
-
appendOption(options, name, true);
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
appendOption(options, name, next);
|
|
370
|
-
index += 1;
|
|
371
|
-
}
|
|
372
|
-
return { command, options };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function appendOption(options, name, value) {
|
|
376
|
-
if (name === 'extra' || name === 'category') {
|
|
377
|
-
if (!Array.isArray(options[name])) options[name] = [];
|
|
378
|
-
options[name].push(value);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
options[name] = value;
|
|
382
|
-
}
|
|
383
|
-
|
|
371
|
+
if (next === undefined || next.startsWith('--')) {
|
|
372
|
+
appendOption(options, name, true);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
appendOption(options, name, next);
|
|
376
|
+
index += 1;
|
|
377
|
+
}
|
|
378
|
+
return { command, options };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function appendOption(options, name, value) {
|
|
382
|
+
if (name === 'extra' || name === 'category') {
|
|
383
|
+
if (!Array.isArray(options[name])) options[name] = [];
|
|
384
|
+
options[name].push(value);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
options[name] = value;
|
|
388
|
+
}
|
|
389
|
+
|
|
384
390
|
async function adb(ctx, args, { binary = false } = {}) {
|
|
385
391
|
const allArgs = adbArgs(ctx, args);
|
|
386
392
|
return new Promise((resolve, reject) => {
|
|
@@ -785,41 +791,41 @@ async function ensureForward(ctx) {
|
|
|
785
791
|
};
|
|
786
792
|
}
|
|
787
793
|
|
|
788
|
-
async function resolveDevicePort(ctx) {
|
|
789
|
-
if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
|
|
790
|
-
try {
|
|
791
|
-
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
|
|
792
|
-
const state = JSON.parse(result.stdout.trim());
|
|
793
|
-
const discoveredPort = Number(state.port);
|
|
794
|
-
if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
|
|
795
|
-
return { port: discoveredPort, source: 'package-port-file', state };
|
|
796
|
-
}
|
|
797
|
-
} catch (error) {
|
|
798
|
-
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
799
|
-
ctx.devicePortSource = 'package-port-file';
|
|
800
|
-
ctx.devicePortError = firstErrorLine(error);
|
|
801
|
-
error.aiAppBridgePortDiscovery = true;
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
return {
|
|
805
|
-
port: ctx.port,
|
|
806
|
-
source: 'default-port',
|
|
807
|
-
error: firstErrorLine(error),
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
811
|
-
const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
|
|
812
|
-
error.aiAppBridgePortDiscovery = true;
|
|
813
|
-
ctx.devicePortSource = 'package-port-file';
|
|
814
|
-
ctx.devicePortError = firstErrorLine(error);
|
|
815
|
-
throw error;
|
|
816
|
-
}
|
|
817
|
-
return { port: ctx.port, source: 'default-port' };
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function shouldUseDefaultPortFallback(ctx) {
|
|
821
|
-
return !ctx.explicitPackageName;
|
|
822
|
-
}
|
|
794
|
+
async function resolveDevicePort(ctx) {
|
|
795
|
+
if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
|
|
796
|
+
try {
|
|
797
|
+
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
|
|
798
|
+
const state = JSON.parse(result.stdout.trim());
|
|
799
|
+
const discoveredPort = Number(state.port);
|
|
800
|
+
if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
|
|
801
|
+
return { port: discoveredPort, source: 'package-port-file', state };
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
805
|
+
ctx.devicePortSource = 'package-port-file';
|
|
806
|
+
ctx.devicePortError = firstErrorLine(error);
|
|
807
|
+
error.aiAppBridgePortDiscovery = true;
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
port: ctx.port,
|
|
812
|
+
source: 'default-port',
|
|
813
|
+
error: firstErrorLine(error),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
817
|
+
const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
|
|
818
|
+
error.aiAppBridgePortDiscovery = true;
|
|
819
|
+
ctx.devicePortSource = 'package-port-file';
|
|
820
|
+
ctx.devicePortError = firstErrorLine(error);
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
return { port: ctx.port, source: 'default-port' };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function shouldUseDefaultPortFallback(ctx) {
|
|
827
|
+
return !ctx.explicitPackageName;
|
|
828
|
+
}
|
|
823
829
|
|
|
824
830
|
async function bridgeStatus(ctx, options = {}) {
|
|
825
831
|
try {
|
|
@@ -972,24 +978,24 @@ function normalizeBridgeError(error) {
|
|
|
972
978
|
suggestion: 'Confirm the target app is running and that the resolved bridge port belongs to this package.',
|
|
973
979
|
};
|
|
974
980
|
}
|
|
975
|
-
if (lower.includes('adb timed out')) {
|
|
976
|
-
return {
|
|
977
|
-
code: 'adb_timeout',
|
|
978
|
-
message,
|
|
979
|
-
suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
|
|
983
|
-
return {
|
|
984
|
-
code: 'bridge_package_mismatch',
|
|
985
|
-
message,
|
|
986
|
-
suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
|
|
990
|
-
return {
|
|
991
|
-
code: 'bridge_port_discovery_failed',
|
|
992
|
-
message,
|
|
981
|
+
if (lower.includes('adb timed out')) {
|
|
982
|
+
return {
|
|
983
|
+
code: 'adb_timeout',
|
|
984
|
+
message,
|
|
985
|
+
suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
|
|
989
|
+
return {
|
|
990
|
+
code: 'bridge_package_mismatch',
|
|
991
|
+
message,
|
|
992
|
+
suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
|
|
996
|
+
return {
|
|
997
|
+
code: 'bridge_port_discovery_failed',
|
|
998
|
+
message,
|
|
993
999
|
suggestion: 'Install a debuggable build for the requested package or pass --port explicitly.',
|
|
994
1000
|
};
|
|
995
1001
|
}
|
|
@@ -1011,38 +1017,38 @@ function firstErrorLine(error) {
|
|
|
1011
1017
|
return String(error?.message || error || 'unknown_error').split(/\r?\n/).find(Boolean) || 'unknown_error';
|
|
1012
1018
|
}
|
|
1013
1019
|
|
|
1014
|
-
async function bridgeGet(ctx, requestPath) {
|
|
1015
|
-
await ensureForward(ctx);
|
|
1016
|
-
const body = await httpGet(bridgeUrl(ctx, requestPath));
|
|
1017
|
-
const payload = JSON.parse(body);
|
|
1018
|
-
verifyBridgeTargetPackage(ctx, payload, requestPath);
|
|
1019
|
-
return payload;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function bridgePost(ctx, requestPath, payload) {
|
|
1023
|
-
await ensureForward(ctx);
|
|
1024
|
-
const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
|
|
1025
|
-
const responsePayload = JSON.parse(body);
|
|
1026
|
-
verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
|
|
1027
|
-
return responsePayload;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
function bridgeUrl(ctx, requestPath) {
|
|
1031
|
-
return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function verifyBridgeTargetPackage(ctx, payload, requestPath) {
|
|
1035
|
-
if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
|
|
1036
|
-
const responsePackageName = payload.app && typeof payload.app === 'object'
|
|
1037
|
-
? payload.app.packageName
|
|
1038
|
-
: undefined;
|
|
1039
|
-
if (!responsePackageName || responsePackageName === ctx.packageName) return;
|
|
1040
|
-
const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
|
|
1041
|
-
error.aiAppBridgePackageMismatch = true;
|
|
1042
|
-
error.expectedPackageName = ctx.packageName;
|
|
1043
|
-
error.actualPackageName = responsePackageName;
|
|
1044
|
-
throw error;
|
|
1045
|
-
}
|
|
1020
|
+
async function bridgeGet(ctx, requestPath) {
|
|
1021
|
+
await ensureForward(ctx);
|
|
1022
|
+
const body = await httpGet(bridgeUrl(ctx, requestPath));
|
|
1023
|
+
const payload = JSON.parse(body);
|
|
1024
|
+
verifyBridgeTargetPackage(ctx, payload, requestPath);
|
|
1025
|
+
return payload;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function bridgePost(ctx, requestPath, payload) {
|
|
1029
|
+
await ensureForward(ctx);
|
|
1030
|
+
const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
|
|
1031
|
+
const responsePayload = JSON.parse(body);
|
|
1032
|
+
verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
|
|
1033
|
+
return responsePayload;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function bridgeUrl(ctx, requestPath) {
|
|
1037
|
+
return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function verifyBridgeTargetPackage(ctx, payload, requestPath) {
|
|
1041
|
+
if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
|
|
1042
|
+
const responsePackageName = payload.app && typeof payload.app === 'object'
|
|
1043
|
+
? payload.app.packageName
|
|
1044
|
+
: undefined;
|
|
1045
|
+
if (!responsePackageName || responsePackageName === ctx.packageName) return;
|
|
1046
|
+
const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
|
|
1047
|
+
error.aiAppBridgePackageMismatch = true;
|
|
1048
|
+
error.expectedPackageName = ctx.packageName;
|
|
1049
|
+
error.actualPackageName = responsePackageName;
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1046
1052
|
|
|
1047
1053
|
function httpGet(url) {
|
|
1048
1054
|
return new Promise((resolve, reject) => {
|
|
@@ -1531,6 +1537,95 @@ async function packagePidsFor(ctx) {
|
|
|
1531
1537
|
}
|
|
1532
1538
|
}
|
|
1533
1539
|
|
|
1540
|
+
async function freezeApp(ctx, options = {}) {
|
|
1541
|
+
return signalAppProcesses(ctx, options, 'SIGSTOP', 'freeze-app');
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function thawApp(ctx, options = {}) {
|
|
1545
|
+
return signalAppProcesses(ctx, options, 'SIGCONT', 'thaw-app');
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function signalAppProcesses(ctx, options, signal, action) {
|
|
1549
|
+
const pids = await targetAppPids(ctx, options);
|
|
1550
|
+
if (pids.length === 0) {
|
|
1551
|
+
return {
|
|
1552
|
+
ok: false,
|
|
1553
|
+
packageName: ctx.packageName,
|
|
1554
|
+
action,
|
|
1555
|
+
signal,
|
|
1556
|
+
error: 'app_process_not_found',
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const results = [];
|
|
1561
|
+
for (const pid of pids) {
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'kill', `-${signal}`, pid]);
|
|
1564
|
+
results.push({
|
|
1565
|
+
pid,
|
|
1566
|
+
ok: true,
|
|
1567
|
+
stdout: result.stdout.trim(),
|
|
1568
|
+
stderr: result.stderr.trim(),
|
|
1569
|
+
});
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
results.push({
|
|
1572
|
+
pid,
|
|
1573
|
+
ok: false,
|
|
1574
|
+
error: firstErrorLine(error),
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const failed = results.filter((item) => !item.ok);
|
|
1580
|
+
return {
|
|
1581
|
+
ok: failed.length === 0,
|
|
1582
|
+
packageName: ctx.packageName,
|
|
1583
|
+
action,
|
|
1584
|
+
signal,
|
|
1585
|
+
pids,
|
|
1586
|
+
results,
|
|
1587
|
+
...(failed.length > 0 ? { error: 'signal_failed' } : {}),
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
async function targetAppPids(ctx, options = {}) {
|
|
1592
|
+
if (options.pid && options.pid !== true && options.pid !== 'current') {
|
|
1593
|
+
const explicitPid = String(options.pid).trim();
|
|
1594
|
+
return /^\d+$/.test(explicitPid) ? [explicitPid] : [];
|
|
1595
|
+
}
|
|
1596
|
+
const psPids = await packagePidsFromPs(ctx);
|
|
1597
|
+
if (psPids.length > 0) return psPids;
|
|
1598
|
+
return packagePidsFor(ctx);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
async function packagePidsFromPs(ctx) {
|
|
1602
|
+
if (!ctx.packageName) return [];
|
|
1603
|
+
try {
|
|
1604
|
+
const result = await adb(ctx, ['shell', 'ps', '-A', '-o', 'PID,NAME']);
|
|
1605
|
+
return parsePackagePidsFromPs(result.stdout, ctx.packageName);
|
|
1606
|
+
} catch (_) {
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function parsePackagePidsFromPs(stdout, packageName) {
|
|
1612
|
+
const pids = [];
|
|
1613
|
+
const seen = new Set();
|
|
1614
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1615
|
+
const trimmed = line.trim();
|
|
1616
|
+
if (!trimmed || /^PID\s+NAME$/i.test(trimmed)) continue;
|
|
1617
|
+
const match = /^(\d+)\s+(\S+)$/.exec(trimmed);
|
|
1618
|
+
if (!match) continue;
|
|
1619
|
+
const pid = match[1];
|
|
1620
|
+
const processName = match[2];
|
|
1621
|
+
if (processName !== packageName && !processName.startsWith(`${packageName}:`)) continue;
|
|
1622
|
+
if (seen.has(pid)) continue;
|
|
1623
|
+
seen.add(pid);
|
|
1624
|
+
pids.push(pid);
|
|
1625
|
+
}
|
|
1626
|
+
return pids;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1534
1629
|
function parseWebViewDevToolsSockets(procNetUnix, packagePids = []) {
|
|
1535
1630
|
const packagePidSet = new Set(packagePids.map(String));
|
|
1536
1631
|
const sockets = [];
|
|
@@ -2846,19 +2941,19 @@ async function tapFlutterText(ctx, targetText, options = {}) {
|
|
|
2846
2941
|
};
|
|
2847
2942
|
}
|
|
2848
2943
|
|
|
2849
|
-
async function inputFlutterText(ctx, targetText, text) {
|
|
2850
|
-
const operable = await flutterNodes(ctx);
|
|
2851
|
-
const node = findFlutterNode(operable, targetText, 'input');
|
|
2852
|
-
if (!node) {
|
|
2853
|
-
throw new Error(`flutter input node not found: ${targetText}`);
|
|
2854
|
-
}
|
|
2855
|
-
const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
|
|
2856
|
-
const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
|
|
2857
|
-
return {
|
|
2858
|
-
...result,
|
|
2859
|
-
source: 'flutter-operable-tree',
|
|
2860
|
-
targetText,
|
|
2861
|
-
text,
|
|
2944
|
+
async function inputFlutterText(ctx, targetText, text) {
|
|
2945
|
+
const operable = await flutterNodes(ctx);
|
|
2946
|
+
const node = findFlutterNode(operable, targetText, 'input');
|
|
2947
|
+
if (!node) {
|
|
2948
|
+
throw new Error(`flutter input node not found: ${targetText}`);
|
|
2949
|
+
}
|
|
2950
|
+
const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
|
|
2951
|
+
const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
|
|
2952
|
+
return {
|
|
2953
|
+
...result,
|
|
2954
|
+
source: 'flutter-operable-tree',
|
|
2955
|
+
targetText,
|
|
2956
|
+
text,
|
|
2862
2957
|
node,
|
|
2863
2958
|
x: point.x,
|
|
2864
2959
|
y: point.y,
|
|
@@ -3532,78 +3627,78 @@ function shouldDismissKeyboardForPoint({ point, viewport, keyboardVisible }) {
|
|
|
3532
3627
|
};
|
|
3533
3628
|
}
|
|
3534
3629
|
|
|
3535
|
-
async function inputText(ctx, text, options = {}) {
|
|
3536
|
-
const bridgePayload = inputTextBridgePayload(text, options);
|
|
3537
|
-
let bridgeAttempt = null;
|
|
3538
|
-
try {
|
|
3539
|
-
const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
|
|
3540
|
-
if (bridgeResult?.ok) {
|
|
3541
|
-
const result = {
|
|
3542
|
-
...bridgeResult,
|
|
3543
|
-
transport: 'bridge',
|
|
3544
|
-
source: bridgeResult.source || 'native-view',
|
|
3545
|
-
request: bridgePayload,
|
|
3546
|
-
};
|
|
3547
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
3548
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
3549
|
-
}
|
|
3550
|
-
return result;
|
|
3551
|
-
}
|
|
3552
|
-
bridgeAttempt = {
|
|
3553
|
-
ok: false,
|
|
3554
|
-
command: 'input-text',
|
|
3555
|
-
requestPath: '/v1/action/input-text',
|
|
3556
|
-
result: bridgeResult,
|
|
3557
|
-
error: bridgeResult?.error || 'bridge_input_failed',
|
|
3558
|
-
message: bridgeResult?.message || 'The app bridge did not accept native text input.',
|
|
3559
|
-
};
|
|
3560
|
-
} catch (error) {
|
|
3561
|
-
bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
if (!isAdbInputTextSafe(text)) {
|
|
3565
|
-
return {
|
|
3566
|
-
ok: false,
|
|
3567
|
-
error: 'unicode_text_requires_bridge_input',
|
|
3568
|
-
message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
|
|
3569
|
-
textLength: text.length,
|
|
3570
|
-
bridge: bridgeAttempt,
|
|
3571
|
-
suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
|
|
3572
|
-
};
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
|
|
3576
|
-
const result = {
|
|
3577
|
-
ok: true,
|
|
3578
|
-
transport: 'adb',
|
|
3579
|
-
source: 'adb-input-text-fallback',
|
|
3580
|
-
text,
|
|
3581
|
-
bridge: bridgeAttempt,
|
|
3582
|
-
};
|
|
3583
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
3584
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
3585
|
-
}
|
|
3586
|
-
return result;
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
function inputTextBridgePayload(text, options = {}) {
|
|
3590
|
-
const payload = { text };
|
|
3591
|
-
const x = Number(options.tapX);
|
|
3592
|
-
const y = Number(options.tapY);
|
|
3593
|
-
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
3594
|
-
payload.x = x;
|
|
3595
|
-
payload.y = y;
|
|
3596
|
-
}
|
|
3597
|
-
return payload;
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
function isAdbInputTextSafe(text) {
|
|
3601
|
-
return /^[\x20-\x7e]*$/.test(String(text));
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
async function swipe(ctx, startX, startY, endX, endY, durationMs) {
|
|
3605
|
-
await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
|
|
3606
|
-
return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
|
|
3630
|
+
async function inputText(ctx, text, options = {}) {
|
|
3631
|
+
const bridgePayload = inputTextBridgePayload(text, options);
|
|
3632
|
+
let bridgeAttempt = null;
|
|
3633
|
+
try {
|
|
3634
|
+
const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
|
|
3635
|
+
if (bridgeResult?.ok) {
|
|
3636
|
+
const result = {
|
|
3637
|
+
...bridgeResult,
|
|
3638
|
+
transport: 'bridge',
|
|
3639
|
+
source: bridgeResult.source || 'native-view',
|
|
3640
|
+
request: bridgePayload,
|
|
3641
|
+
};
|
|
3642
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
3643
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
3644
|
+
}
|
|
3645
|
+
return result;
|
|
3646
|
+
}
|
|
3647
|
+
bridgeAttempt = {
|
|
3648
|
+
ok: false,
|
|
3649
|
+
command: 'input-text',
|
|
3650
|
+
requestPath: '/v1/action/input-text',
|
|
3651
|
+
result: bridgeResult,
|
|
3652
|
+
error: bridgeResult?.error || 'bridge_input_failed',
|
|
3653
|
+
message: bridgeResult?.message || 'The app bridge did not accept native text input.',
|
|
3654
|
+
};
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
if (!isAdbInputTextSafe(text)) {
|
|
3660
|
+
return {
|
|
3661
|
+
ok: false,
|
|
3662
|
+
error: 'unicode_text_requires_bridge_input',
|
|
3663
|
+
message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
|
|
3664
|
+
textLength: text.length,
|
|
3665
|
+
bridge: bridgeAttempt,
|
|
3666
|
+
suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
|
|
3671
|
+
const result = {
|
|
3672
|
+
ok: true,
|
|
3673
|
+
transport: 'adb',
|
|
3674
|
+
source: 'adb-input-text-fallback',
|
|
3675
|
+
text,
|
|
3676
|
+
bridge: bridgeAttempt,
|
|
3677
|
+
};
|
|
3678
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
3679
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
3680
|
+
}
|
|
3681
|
+
return result;
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function inputTextBridgePayload(text, options = {}) {
|
|
3685
|
+
const payload = { text };
|
|
3686
|
+
const x = Number(options.tapX);
|
|
3687
|
+
const y = Number(options.tapY);
|
|
3688
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
3689
|
+
payload.x = x;
|
|
3690
|
+
payload.y = y;
|
|
3691
|
+
}
|
|
3692
|
+
return payload;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function isAdbInputTextSafe(text) {
|
|
3696
|
+
return /^[\x20-\x7e]*$/.test(String(text));
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
async function swipe(ctx, startX, startY, endX, endY, durationMs) {
|
|
3700
|
+
await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
|
|
3701
|
+
return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
|
|
3607
3702
|
}
|
|
3608
3703
|
|
|
3609
3704
|
async function keyevent(ctx, keyCode) {
|
|
@@ -3703,70 +3798,70 @@ async function safePermissionState(ctx, permission) {
|
|
|
3703
3798
|
}
|
|
3704
3799
|
}
|
|
3705
3800
|
|
|
3706
|
-
async function appopsSet(ctx, op, mode) {
|
|
3707
|
-
await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
|
|
3708
|
-
return {
|
|
3801
|
+
async function appopsSet(ctx, op, mode) {
|
|
3802
|
+
await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
|
|
3803
|
+
return {
|
|
3709
3804
|
ok: true,
|
|
3710
3805
|
packageName: ctx.packageName,
|
|
3711
3806
|
op,
|
|
3712
3807
|
mode,
|
|
3713
|
-
};
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
async function clearAppData(ctx) {
|
|
3717
|
-
if (!ctx.explicitPackageName) {
|
|
3718
|
-
throw new Error('packageName is required for clear-app-data');
|
|
3719
|
-
}
|
|
3720
|
-
let bridgeError = null;
|
|
3721
|
-
try {
|
|
3722
|
-
const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
|
|
3723
|
-
return {
|
|
3724
|
-
...bridgeResult,
|
|
3725
|
-
packageName: ctx.packageName,
|
|
3726
|
-
method: 'bridge-runtime',
|
|
3727
|
-
};
|
|
3728
|
-
} catch (error) {
|
|
3729
|
-
bridgeError = firstErrorLine(error);
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
let result;
|
|
3733
|
-
try {
|
|
3734
|
-
result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
|
|
3735
|
-
} catch (error) {
|
|
3736
|
-
return {
|
|
3737
|
-
ok: false,
|
|
3738
|
-
packageName: ctx.packageName,
|
|
3739
|
-
action: 'clear-app-data',
|
|
3740
|
-
error: 'clear_app_data_failed',
|
|
3741
|
-
bridgeError,
|
|
3742
|
-
message: error.message || String(error),
|
|
3743
|
-
};
|
|
3744
|
-
}
|
|
3745
|
-
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
3746
|
-
if (!/^Success\b/im.test(output)) {
|
|
3747
|
-
return {
|
|
3748
|
-
ok: false,
|
|
3749
|
-
packageName: ctx.packageName,
|
|
3750
|
-
error: 'clear_app_data_failed',
|
|
3751
|
-
bridgeError,
|
|
3752
|
-
output,
|
|
3753
|
-
};
|
|
3754
|
-
}
|
|
3755
|
-
return {
|
|
3756
|
-
ok: true,
|
|
3757
|
-
packageName: ctx.packageName,
|
|
3758
|
-
action: 'clear-app-data',
|
|
3759
|
-
method: 'pm-clear',
|
|
3760
|
-
bridgeError,
|
|
3761
|
-
output,
|
|
3762
|
-
};
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
function clearAppDataAdbArgs(packageName) {
|
|
3766
|
-
return ['shell', 'pm', 'clear', packageName];
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
async function resolveLogcatPid(ctx, options) {
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
async function clearAppData(ctx) {
|
|
3812
|
+
if (!ctx.explicitPackageName) {
|
|
3813
|
+
throw new Error('packageName is required for clear-app-data');
|
|
3814
|
+
}
|
|
3815
|
+
let bridgeError = null;
|
|
3816
|
+
try {
|
|
3817
|
+
const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
|
|
3818
|
+
return {
|
|
3819
|
+
...bridgeResult,
|
|
3820
|
+
packageName: ctx.packageName,
|
|
3821
|
+
method: 'bridge-runtime',
|
|
3822
|
+
};
|
|
3823
|
+
} catch (error) {
|
|
3824
|
+
bridgeError = firstErrorLine(error);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
let result;
|
|
3828
|
+
try {
|
|
3829
|
+
result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
|
|
3830
|
+
} catch (error) {
|
|
3831
|
+
return {
|
|
3832
|
+
ok: false,
|
|
3833
|
+
packageName: ctx.packageName,
|
|
3834
|
+
action: 'clear-app-data',
|
|
3835
|
+
error: 'clear_app_data_failed',
|
|
3836
|
+
bridgeError,
|
|
3837
|
+
message: error.message || String(error),
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
3841
|
+
if (!/^Success\b/im.test(output)) {
|
|
3842
|
+
return {
|
|
3843
|
+
ok: false,
|
|
3844
|
+
packageName: ctx.packageName,
|
|
3845
|
+
error: 'clear_app_data_failed',
|
|
3846
|
+
bridgeError,
|
|
3847
|
+
output,
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
return {
|
|
3851
|
+
ok: true,
|
|
3852
|
+
packageName: ctx.packageName,
|
|
3853
|
+
action: 'clear-app-data',
|
|
3854
|
+
method: 'pm-clear',
|
|
3855
|
+
bridgeError,
|
|
3856
|
+
output,
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
function clearAppDataAdbArgs(packageName) {
|
|
3861
|
+
return ['shell', 'pm', 'clear', packageName];
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
async function resolveLogcatPid(ctx, options) {
|
|
3770
3865
|
if (options.pid && options.pid !== true && options.pid !== 'current') {
|
|
3771
3866
|
return String(options.pid);
|
|
3772
3867
|
}
|
|
@@ -3781,17 +3876,17 @@ async function resolveLogcatPid(ctx, options) {
|
|
|
3781
3876
|
return '';
|
|
3782
3877
|
}
|
|
3783
3878
|
|
|
3784
|
-
function filterLogcat(text, options) {
|
|
3785
|
-
const tags = splitCsv(options.tag || options.tags);
|
|
3786
|
-
const grep = options.grep ? String(options.grep) : '';
|
|
3787
|
-
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3788
|
-
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3789
|
-
const pid = options.pid ? String(options.pid) : '';
|
|
3790
|
-
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3791
|
-
if (requiresAppPid && !pid) {
|
|
3792
|
-
return '';
|
|
3793
|
-
}
|
|
3794
|
-
const lines = String(text || '').split(/\r?\n/);
|
|
3879
|
+
function filterLogcat(text, options) {
|
|
3880
|
+
const tags = splitCsv(options.tag || options.tags);
|
|
3881
|
+
const grep = options.grep ? String(options.grep) : '';
|
|
3882
|
+
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3883
|
+
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3884
|
+
const pid = options.pid ? String(options.pid) : '';
|
|
3885
|
+
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3886
|
+
if (requiresAppPid && !pid) {
|
|
3887
|
+
return '';
|
|
3888
|
+
}
|
|
3889
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
3795
3890
|
const filtered = [];
|
|
3796
3891
|
let previousIncluded = false;
|
|
3797
3892
|
for (const line of lines) {
|
|
@@ -3874,134 +3969,134 @@ function adbFollow(ctx, args, durationMs) {
|
|
|
3874
3969
|
});
|
|
3875
3970
|
}
|
|
3876
3971
|
|
|
3877
|
-
async function launchApp(ctx, options = {}) {
|
|
3878
|
-
if (options.component || options.activity) {
|
|
3879
|
-
return launchActivity(ctx, options);
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
const candidates = await launcherActivityCandidates(ctx);
|
|
3883
|
-
if (candidates.length === 0) {
|
|
3884
|
-
return {
|
|
3885
|
-
ok: false,
|
|
3886
|
-
error: 'launcher_not_found',
|
|
3887
|
-
packageName: ctx.packageName,
|
|
3888
|
-
launcherCandidates: candidates,
|
|
3889
|
-
};
|
|
3890
|
-
}
|
|
3891
|
-
if (candidates.length > 1) {
|
|
3892
|
-
return {
|
|
3893
|
-
ok: false,
|
|
3894
|
-
error: 'launcher_ambiguous',
|
|
3895
|
-
packageName: ctx.packageName,
|
|
3896
|
-
launcherCandidates: candidates,
|
|
3897
|
-
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3898
|
-
};
|
|
3899
|
-
}
|
|
3900
|
-
|
|
3901
|
-
return startActivity(ctx, candidates[0], options, {
|
|
3902
|
-
packageName: ctx.packageName,
|
|
3903
|
-
launcherCandidates: candidates,
|
|
3904
|
-
});
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
async function launchActivity(ctx, options = {}) {
|
|
3908
|
-
const component = normalizeActivityComponent(
|
|
3909
|
-
ctx.packageName,
|
|
3910
|
-
options.component || requiredString(options.activity, 'activity'),
|
|
3911
|
-
);
|
|
3912
|
-
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3913
|
-
}
|
|
3914
|
-
|
|
3915
|
-
async function launchNativeTest(ctx) {
|
|
3916
|
-
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
3917
|
-
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
async function launchFlutter(ctx, initialRoute) {
|
|
3921
|
-
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
3922
|
-
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
3923
|
-
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3924
|
-
return { ...result, initialRoute };
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
async function launcherActivityCandidates(ctx) {
|
|
3928
|
-
const result = await adb(ctx, [
|
|
3929
|
-
'shell',
|
|
3930
|
-
'cmd',
|
|
3931
|
-
'package',
|
|
3932
|
-
'query-activities',
|
|
3933
|
-
'--brief',
|
|
3934
|
-
'-a',
|
|
3935
|
-
'android.intent.action.MAIN',
|
|
3936
|
-
'-c',
|
|
3937
|
-
'android.intent.category.LAUNCHER',
|
|
3938
|
-
ctx.packageName,
|
|
3939
|
-
]);
|
|
3940
|
-
return parseLauncherActivityCandidates(result.stdout);
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
function parseLauncherActivityCandidates(stdout) {
|
|
3944
|
-
const candidates = [];
|
|
3945
|
-
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
3946
|
-
const candidate = line.trim();
|
|
3947
|
-
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
3948
|
-
candidates.push(candidate);
|
|
3949
|
-
}
|
|
3950
|
-
}
|
|
3951
|
-
return candidates;
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
|
-
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
3955
|
-
const value = requiredString(activityOrComponent, 'activity');
|
|
3956
|
-
if (value.includes('/')) return value;
|
|
3957
|
-
return `${packageName}/${value}`;
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
3961
|
-
const args = buildAmStartArgs(component, options);
|
|
3962
|
-
const result = await adb(ctx, args);
|
|
3963
|
-
return {
|
|
3964
|
-
ok: true,
|
|
3965
|
-
transport: 'adb',
|
|
3966
|
-
component,
|
|
3967
|
-
...extraResult,
|
|
3968
|
-
stdout: result.stdout.trim(),
|
|
3969
|
-
stderr: result.stderr.trim(),
|
|
3970
|
-
};
|
|
3971
|
-
}
|
|
3972
|
-
|
|
3973
|
-
function buildAmStartArgs(component, options = {}) {
|
|
3974
|
-
const args = ['shell', 'am', 'start'];
|
|
3975
|
-
if (options.action) args.push('-a', options.action);
|
|
3976
|
-
for (const category of optionList(options.category)) {
|
|
3977
|
-
args.push('-c', category);
|
|
3978
|
-
}
|
|
3979
|
-
if (options.data) args.push('-d', options.data);
|
|
3980
|
-
for (const extra of parseStartExtras(options.extra)) {
|
|
3981
|
-
args.push('-e', extra.key, extra.value);
|
|
3982
|
-
}
|
|
3983
|
-
args.push('-n', component);
|
|
3984
|
-
return args;
|
|
3985
|
-
}
|
|
3986
|
-
|
|
3987
|
-
function parseStartExtras(rawExtras) {
|
|
3988
|
-
return optionList(rawExtras).map((rawExtra) => {
|
|
3989
|
-
const value = String(rawExtra);
|
|
3990
|
-
const separator = value.indexOf('=');
|
|
3991
|
-
if (separator <= 0) {
|
|
3992
|
-
throw new Error('extra must use key=value');
|
|
3993
|
-
}
|
|
3994
|
-
return {
|
|
3995
|
-
key: value.slice(0, separator),
|
|
3996
|
-
value: value.slice(separator + 1),
|
|
3997
|
-
};
|
|
3998
|
-
});
|
|
3999
|
-
}
|
|
4000
|
-
|
|
4001
|
-
function optionList(value) {
|
|
4002
|
-
if (value === undefined || value === null || value === false || value === '') return [];
|
|
4003
|
-
return Array.isArray(value) ? value : [value];
|
|
4004
|
-
}
|
|
3972
|
+
async function launchApp(ctx, options = {}) {
|
|
3973
|
+
if (options.component || options.activity) {
|
|
3974
|
+
return launchActivity(ctx, options);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
const candidates = await launcherActivityCandidates(ctx);
|
|
3978
|
+
if (candidates.length === 0) {
|
|
3979
|
+
return {
|
|
3980
|
+
ok: false,
|
|
3981
|
+
error: 'launcher_not_found',
|
|
3982
|
+
packageName: ctx.packageName,
|
|
3983
|
+
launcherCandidates: candidates,
|
|
3984
|
+
};
|
|
3985
|
+
}
|
|
3986
|
+
if (candidates.length > 1) {
|
|
3987
|
+
return {
|
|
3988
|
+
ok: false,
|
|
3989
|
+
error: 'launcher_ambiguous',
|
|
3990
|
+
packageName: ctx.packageName,
|
|
3991
|
+
launcherCandidates: candidates,
|
|
3992
|
+
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
return startActivity(ctx, candidates[0], options, {
|
|
3997
|
+
packageName: ctx.packageName,
|
|
3998
|
+
launcherCandidates: candidates,
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
async function launchActivity(ctx, options = {}) {
|
|
4003
|
+
const component = normalizeActivityComponent(
|
|
4004
|
+
ctx.packageName,
|
|
4005
|
+
options.component || requiredString(options.activity, 'activity'),
|
|
4006
|
+
);
|
|
4007
|
+
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
async function launchNativeTest(ctx) {
|
|
4011
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
4012
|
+
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
async function launchFlutter(ctx, initialRoute) {
|
|
4016
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
4017
|
+
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
4018
|
+
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
4019
|
+
return { ...result, initialRoute };
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
async function launcherActivityCandidates(ctx) {
|
|
4023
|
+
const result = await adb(ctx, [
|
|
4024
|
+
'shell',
|
|
4025
|
+
'cmd',
|
|
4026
|
+
'package',
|
|
4027
|
+
'query-activities',
|
|
4028
|
+
'--brief',
|
|
4029
|
+
'-a',
|
|
4030
|
+
'android.intent.action.MAIN',
|
|
4031
|
+
'-c',
|
|
4032
|
+
'android.intent.category.LAUNCHER',
|
|
4033
|
+
ctx.packageName,
|
|
4034
|
+
]);
|
|
4035
|
+
return parseLauncherActivityCandidates(result.stdout);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
function parseLauncherActivityCandidates(stdout) {
|
|
4039
|
+
const candidates = [];
|
|
4040
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
4041
|
+
const candidate = line.trim();
|
|
4042
|
+
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
4043
|
+
candidates.push(candidate);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return candidates;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
4050
|
+
const value = requiredString(activityOrComponent, 'activity');
|
|
4051
|
+
if (value.includes('/')) return value;
|
|
4052
|
+
return `${packageName}/${value}`;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
4056
|
+
const args = buildAmStartArgs(component, options);
|
|
4057
|
+
const result = await adb(ctx, args);
|
|
4058
|
+
return {
|
|
4059
|
+
ok: true,
|
|
4060
|
+
transport: 'adb',
|
|
4061
|
+
component,
|
|
4062
|
+
...extraResult,
|
|
4063
|
+
stdout: result.stdout.trim(),
|
|
4064
|
+
stderr: result.stderr.trim(),
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function buildAmStartArgs(component, options = {}) {
|
|
4069
|
+
const args = ['shell', 'am', 'start'];
|
|
4070
|
+
if (options.action) args.push('-a', options.action);
|
|
4071
|
+
for (const category of optionList(options.category)) {
|
|
4072
|
+
args.push('-c', category);
|
|
4073
|
+
}
|
|
4074
|
+
if (options.data) args.push('-d', options.data);
|
|
4075
|
+
for (const extra of parseStartExtras(options.extra)) {
|
|
4076
|
+
args.push('-e', extra.key, extra.value);
|
|
4077
|
+
}
|
|
4078
|
+
args.push('-n', component);
|
|
4079
|
+
return args;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
function parseStartExtras(rawExtras) {
|
|
4083
|
+
return optionList(rawExtras).map((rawExtra) => {
|
|
4084
|
+
const value = String(rawExtra);
|
|
4085
|
+
const separator = value.indexOf('=');
|
|
4086
|
+
if (separator <= 0) {
|
|
4087
|
+
throw new Error('extra must use key=value');
|
|
4088
|
+
}
|
|
4089
|
+
return {
|
|
4090
|
+
key: value.slice(0, separator),
|
|
4091
|
+
value: value.slice(separator + 1),
|
|
4092
|
+
};
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function optionList(value) {
|
|
4097
|
+
if (value === undefined || value === null || value === false || value === '') return [];
|
|
4098
|
+
return Array.isArray(value) ? value : [value];
|
|
4099
|
+
}
|
|
4005
4100
|
|
|
4006
4101
|
async function smoke(ctx, options) {
|
|
4007
4102
|
const summary = {
|
|
@@ -4359,50 +4454,51 @@ function requiredNumber(value, name) {
|
|
|
4359
4454
|
}
|
|
4360
4455
|
|
|
4361
4456
|
module.exports = {
|
|
4362
|
-
buildBridgeFailureResult,
|
|
4363
|
-
clearAppDataAdbArgs,
|
|
4364
|
-
defaultInstallerButtonTexts,
|
|
4457
|
+
buildBridgeFailureResult,
|
|
4458
|
+
clearAppDataAdbArgs,
|
|
4459
|
+
defaultInstallerButtonTexts,
|
|
4365
4460
|
artifactTimestamp,
|
|
4366
4461
|
compactBridgeTree,
|
|
4367
4462
|
compactStatus,
|
|
4368
4463
|
compactUiaTree,
|
|
4369
4464
|
defaultArtifactDirectory,
|
|
4370
4465
|
defaultArtifactPath,
|
|
4371
|
-
findFlutterNode,
|
|
4372
|
-
findTappableNodeByText,
|
|
4373
|
-
filterLogcat,
|
|
4374
|
-
firstErrorLine,
|
|
4466
|
+
findFlutterNode,
|
|
4467
|
+
findTappableNodeByText,
|
|
4468
|
+
filterLogcat,
|
|
4469
|
+
firstErrorLine,
|
|
4375
4470
|
flutterNodePoint,
|
|
4376
4471
|
flutterPhysicalViewport,
|
|
4377
4472
|
helpText,
|
|
4378
|
-
installerButtonTextsForSurface,
|
|
4379
|
-
isAdbInputTextSafe,
|
|
4380
|
-
isLikelyInstallerSurface,
|
|
4473
|
+
installerButtonTextsForSurface,
|
|
4474
|
+
isAdbInputTextSafe,
|
|
4475
|
+
isLikelyInstallerSurface,
|
|
4381
4476
|
nodeTapState,
|
|
4382
|
-
normalizeBridgeError,
|
|
4383
|
-
normalizeActivityComponent,
|
|
4384
|
-
parseWebViewDevToolsSockets,
|
|
4385
|
-
parseArgs,
|
|
4386
|
-
parseKeyboardState,
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4477
|
+
normalizeBridgeError,
|
|
4478
|
+
normalizeActivityComponent,
|
|
4479
|
+
parseWebViewDevToolsSockets,
|
|
4480
|
+
parseArgs,
|
|
4481
|
+
parseKeyboardState,
|
|
4482
|
+
parsePackagePidsFromPs,
|
|
4483
|
+
parseLauncherActivityCandidates,
|
|
4484
|
+
parseStartExtras,
|
|
4485
|
+
parseUiaBounds,
|
|
4390
4486
|
parseUiaViewport,
|
|
4391
4487
|
parseComponentFromWindowLine,
|
|
4392
4488
|
parseForegroundWindow,
|
|
4393
4489
|
chooseWebViewDevToolsSocket,
|
|
4394
4490
|
chooseWebViewPage,
|
|
4395
|
-
shapeNetworkCapture,
|
|
4396
|
-
compactNetworkRecord,
|
|
4397
|
-
pruneGeneratedArtifacts,
|
|
4398
|
-
shouldSkipInstallerTapForInstalledPackage,
|
|
4399
|
-
shouldDismissKeyboardForPoint,
|
|
4400
|
-
shouldUseDefaultPortFallback,
|
|
4401
|
-
screenshotOutputPath,
|
|
4402
|
-
statusSearchText,
|
|
4403
|
-
uiautomatorLockPath,
|
|
4404
|
-
verifyBridgeTargetPackage,
|
|
4405
|
-
waitTextConditionsMet,
|
|
4406
|
-
withFileLock,
|
|
4407
|
-
};
|
|
4491
|
+
shapeNetworkCapture,
|
|
4492
|
+
compactNetworkRecord,
|
|
4493
|
+
pruneGeneratedArtifacts,
|
|
4494
|
+
shouldSkipInstallerTapForInstalledPackage,
|
|
4495
|
+
shouldDismissKeyboardForPoint,
|
|
4496
|
+
shouldUseDefaultPortFallback,
|
|
4497
|
+
screenshotOutputPath,
|
|
4498
|
+
statusSearchText,
|
|
4499
|
+
uiautomatorLockPath,
|
|
4500
|
+
verifyBridgeTargetPackage,
|
|
4501
|
+
waitTextConditionsMet,
|
|
4502
|
+
withFileLock,
|
|
4503
|
+
};
|
|
4408
4504
|
|