@mobileaidev/ai-app-bridge 0.2.0 → 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 +13 -5
- package/bin/ai-app-bridge.js +192 -47
- package/bin/mcp-server.js +105 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
npm install -g @mobileaidev/ai-app-bridge
|
|
5
5
|
|
|
6
6
|
ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
|
|
7
|
-
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
8
|
-
ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
|
|
9
|
-
ai-app-bridge
|
|
7
|
+
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
8
|
+
ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
|
|
9
|
+
ai-app-bridge launch-app --package-name io.github.mobileaidev.aiappbridge.sample
|
|
10
|
+
ai-app-bridge launch-activity --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
|
|
11
|
+
ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
|
|
10
12
|
ai-app-bridge input-text --package-name io.github.mobileaidev.aiappbridge.sample --text "中文输入" --hide-keyboard
|
|
11
13
|
ai-app-bridge network --package-name io.github.mobileaidev.aiappbridge.sample --compact --url-filter /api/
|
|
12
14
|
ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
|
|
@@ -25,5 +27,11 @@ When `screenshot` or `smoke` runs without `--out-file`, the CLI writes a unique
|
|
|
25
27
|
PNG under `build/ai_app_bridge_artifacts` instead of reusing a stable filename
|
|
26
28
|
or creating files in the project root.
|
|
27
29
|
It keeps the newest 20 generated screenshots for each command prefix. Use
|
|
28
|
-
`--artifact-dir` to choose that directory, or `--out-file` when a fixed path is
|
|
29
|
-
intentional.
|
|
30
|
+
`--artifact-dir` to choose that directory, or `--out-file` when a fixed path is
|
|
31
|
+
intentional.
|
|
32
|
+
|
|
33
|
+
`launch-app` queries Android LAUNCHER activities before starting the app. If a
|
|
34
|
+
debug dependency exposes multiple launcher entries, it returns
|
|
35
|
+
`launcher_ambiguous` with the candidates instead of guessing. Use
|
|
36
|
+
`launch-activity` or `launch-app --activity/--component` to choose the intended
|
|
37
|
+
entry point explicitly.
|
package/bin/ai-app-bridge.js
CHANGED
|
@@ -68,10 +68,12 @@ Device/action commands:
|
|
|
68
68
|
keyboard-state Read Android soft keyboard visibility from dumpsys.
|
|
69
69
|
hide-keyboard Hide the Android soft keyboard when it is visible.
|
|
70
70
|
|
|
71
|
-
App/permission commands:
|
|
72
|
-
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
-
launch-
|
|
74
|
-
launch-
|
|
71
|
+
App/permission commands:
|
|
72
|
+
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
+
launch-app Launch the target package LAUNCHER Activity.
|
|
74
|
+
launch-activity Launch an explicit Android Activity component.
|
|
75
|
+
launch-native-test Launch the debug native Android bridge test Activity.
|
|
76
|
+
launch-flutter Launch the Flutter Activity.
|
|
75
77
|
permission-state Read Android runtime permission state.
|
|
76
78
|
permission-grant Grant an Android runtime permission.
|
|
77
79
|
permission-revoke Revoke an Android runtime permission.
|
|
@@ -92,7 +94,13 @@ Options:
|
|
|
92
94
|
--adb-timeout-ms <ms> Timeout for ADB subprocesses.
|
|
93
95
|
--out-file <path> Screenshot output path.
|
|
94
96
|
--artifact-dir <path> Directory for generated screenshot/artifact defaults.
|
|
95
|
-
--apk-path <path> APK path used by install-apk.
|
|
97
|
+
--apk-path <path> APK path used by install-apk.
|
|
98
|
+
--activity <name> Activity class for launch-activity or launch-app override.
|
|
99
|
+
--component <pkg/act> Explicit Android component for launch-activity.
|
|
100
|
+
--action <name> Intent action for launch-activity.
|
|
101
|
+
--category <name> Intent category for launch-activity; may be repeated.
|
|
102
|
+
--data <uri> Intent data URI for launch-activity.
|
|
103
|
+
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
96
104
|
--text <text> Text used by Unicode-safe bridge input commands.
|
|
97
105
|
--value <text> Text value used by h5-input or flutter-h5-input.
|
|
98
106
|
--selector <css> CSS selector used by H5 commands.
|
|
@@ -319,11 +327,15 @@ async function runCommand(command, options, ctx) {
|
|
|
319
327
|
return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
|
|
320
328
|
case 'tap-uia-text':
|
|
321
329
|
return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
|
|
322
|
-
case 'permission-dialog':
|
|
323
|
-
return permissionDialog(ctx, options);
|
|
324
|
-
case 'launch-
|
|
325
|
-
return
|
|
326
|
-
case 'launch-
|
|
330
|
+
case 'permission-dialog':
|
|
331
|
+
return permissionDialog(ctx, options);
|
|
332
|
+
case 'launch-app':
|
|
333
|
+
return launchApp(ctx, options);
|
|
334
|
+
case 'launch-activity':
|
|
335
|
+
return launchActivity(ctx, options);
|
|
336
|
+
case 'launch-native-test':
|
|
337
|
+
return launchNativeTest(ctx);
|
|
338
|
+
case 'launch-flutter':
|
|
327
339
|
return launchFlutter(ctx, options.initialRoute || '');
|
|
328
340
|
case 'smoke':
|
|
329
341
|
return smoke(ctx, options);
|
|
@@ -347,16 +359,25 @@ function parseArgs(argv) {
|
|
|
347
359
|
const rawName = arg.slice(2);
|
|
348
360
|
const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
349
361
|
const next = argv[index + 1];
|
|
350
|
-
if (next === undefined || next.startsWith('--')) {
|
|
351
|
-
options
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
options
|
|
355
|
-
index += 1;
|
|
356
|
-
}
|
|
357
|
-
return { command, options };
|
|
358
|
-
}
|
|
359
|
-
|
|
362
|
+
if (next === undefined || next.startsWith('--')) {
|
|
363
|
+
appendOption(options, name, true);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
appendOption(options, name, next);
|
|
367
|
+
index += 1;
|
|
368
|
+
}
|
|
369
|
+
return { command, options };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function appendOption(options, name, value) {
|
|
373
|
+
if (name === 'extra' || name === 'category') {
|
|
374
|
+
if (!Array.isArray(options[name])) options[name] = [];
|
|
375
|
+
options[name].push(value);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
options[name] = value;
|
|
379
|
+
}
|
|
380
|
+
|
|
360
381
|
async function adb(ctx, args, { binary = false } = {}) {
|
|
361
382
|
const allArgs = adbArgs(ctx, args);
|
|
362
383
|
return new Promise((resolve, reject) => {
|
|
@@ -3664,13 +3685,17 @@ async function resolveLogcatPid(ctx, options) {
|
|
|
3664
3685
|
return '';
|
|
3665
3686
|
}
|
|
3666
3687
|
|
|
3667
|
-
function filterLogcat(text, options) {
|
|
3668
|
-
const tags = splitCsv(options.tag || options.tags);
|
|
3669
|
-
const grep = options.grep ? String(options.grep) : '';
|
|
3670
|
-
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3671
|
-
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3672
|
-
const pid = options.pid ? String(options.pid) : '';
|
|
3673
|
-
const
|
|
3688
|
+
function filterLogcat(text, options) {
|
|
3689
|
+
const tags = splitCsv(options.tag || options.tags);
|
|
3690
|
+
const grep = options.grep ? String(options.grep) : '';
|
|
3691
|
+
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3692
|
+
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3693
|
+
const pid = options.pid ? String(options.pid) : '';
|
|
3694
|
+
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3695
|
+
if (requiresAppPid && !pid) {
|
|
3696
|
+
return '';
|
|
3697
|
+
}
|
|
3698
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
3674
3699
|
const filtered = [];
|
|
3675
3700
|
let previousIncluded = false;
|
|
3676
3701
|
for (const line of lines) {
|
|
@@ -3753,19 +3778,134 @@ function adbFollow(ctx, args, durationMs) {
|
|
|
3753
3778
|
});
|
|
3754
3779
|
}
|
|
3755
3780
|
|
|
3756
|
-
async function
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
}
|
|
3781
|
+
async function launchApp(ctx, options = {}) {
|
|
3782
|
+
if (options.component || options.activity) {
|
|
3783
|
+
return launchActivity(ctx, options);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
const candidates = await launcherActivityCandidates(ctx);
|
|
3787
|
+
if (candidates.length === 0) {
|
|
3788
|
+
return {
|
|
3789
|
+
ok: false,
|
|
3790
|
+
error: 'launcher_not_found',
|
|
3791
|
+
packageName: ctx.packageName,
|
|
3792
|
+
launcherCandidates: candidates,
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
if (candidates.length > 1) {
|
|
3796
|
+
return {
|
|
3797
|
+
ok: false,
|
|
3798
|
+
error: 'launcher_ambiguous',
|
|
3799
|
+
packageName: ctx.packageName,
|
|
3800
|
+
launcherCandidates: candidates,
|
|
3801
|
+
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
return startActivity(ctx, candidates[0], options, {
|
|
3806
|
+
packageName: ctx.packageName,
|
|
3807
|
+
launcherCandidates: candidates,
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
async function launchActivity(ctx, options = {}) {
|
|
3812
|
+
const component = normalizeActivityComponent(
|
|
3813
|
+
ctx.packageName,
|
|
3814
|
+
options.component || requiredString(options.activity, 'activity'),
|
|
3815
|
+
);
|
|
3816
|
+
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
async function launchNativeTest(ctx) {
|
|
3820
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
3821
|
+
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
async function launchFlutter(ctx, initialRoute) {
|
|
3825
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
3826
|
+
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
3827
|
+
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3828
|
+
return { ...result, initialRoute };
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
async function launcherActivityCandidates(ctx) {
|
|
3832
|
+
const result = await adb(ctx, [
|
|
3833
|
+
'shell',
|
|
3834
|
+
'cmd',
|
|
3835
|
+
'package',
|
|
3836
|
+
'query-activities',
|
|
3837
|
+
'--brief',
|
|
3838
|
+
'-a',
|
|
3839
|
+
'android.intent.action.MAIN',
|
|
3840
|
+
'-c',
|
|
3841
|
+
'android.intent.category.LAUNCHER',
|
|
3842
|
+
ctx.packageName,
|
|
3843
|
+
]);
|
|
3844
|
+
return parseLauncherActivityCandidates(result.stdout);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
function parseLauncherActivityCandidates(stdout) {
|
|
3848
|
+
const candidates = [];
|
|
3849
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
3850
|
+
const candidate = line.trim();
|
|
3851
|
+
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
3852
|
+
candidates.push(candidate);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return candidates;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
3859
|
+
const value = requiredString(activityOrComponent, 'activity');
|
|
3860
|
+
if (value.includes('/')) return value;
|
|
3861
|
+
return `${packageName}/${value}`;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
3865
|
+
const args = buildAmStartArgs(component, options);
|
|
3866
|
+
const result = await adb(ctx, args);
|
|
3867
|
+
return {
|
|
3868
|
+
ok: true,
|
|
3869
|
+
transport: 'adb',
|
|
3870
|
+
component,
|
|
3871
|
+
...extraResult,
|
|
3872
|
+
stdout: result.stdout.trim(),
|
|
3873
|
+
stderr: result.stderr.trim(),
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
function buildAmStartArgs(component, options = {}) {
|
|
3878
|
+
const args = ['shell', 'am', 'start'];
|
|
3879
|
+
if (options.action) args.push('-a', options.action);
|
|
3880
|
+
for (const category of optionList(options.category)) {
|
|
3881
|
+
args.push('-c', category);
|
|
3882
|
+
}
|
|
3883
|
+
if (options.data) args.push('-d', options.data);
|
|
3884
|
+
for (const extra of parseStartExtras(options.extra)) {
|
|
3885
|
+
args.push('-e', extra.key, extra.value);
|
|
3886
|
+
}
|
|
3887
|
+
args.push('-n', component);
|
|
3888
|
+
return args;
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
function parseStartExtras(rawExtras) {
|
|
3892
|
+
return optionList(rawExtras).map((rawExtra) => {
|
|
3893
|
+
const value = String(rawExtra);
|
|
3894
|
+
const separator = value.indexOf('=');
|
|
3895
|
+
if (separator <= 0) {
|
|
3896
|
+
throw new Error('extra must use key=value');
|
|
3897
|
+
}
|
|
3898
|
+
return {
|
|
3899
|
+
key: value.slice(0, separator),
|
|
3900
|
+
value: value.slice(separator + 1),
|
|
3901
|
+
};
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
function optionList(value) {
|
|
3906
|
+
if (value === undefined || value === null || value === false || value === '') return [];
|
|
3907
|
+
return Array.isArray(value) ? value : [value];
|
|
3908
|
+
}
|
|
3769
3909
|
|
|
3770
3910
|
async function smoke(ctx, options) {
|
|
3771
3911
|
const summary = {
|
|
@@ -4131,9 +4271,10 @@ module.exports = {
|
|
|
4131
4271
|
compactUiaTree,
|
|
4132
4272
|
defaultArtifactDirectory,
|
|
4133
4273
|
defaultArtifactPath,
|
|
4134
|
-
findFlutterNode,
|
|
4135
|
-
findTappableNodeByText,
|
|
4136
|
-
|
|
4274
|
+
findFlutterNode,
|
|
4275
|
+
findTappableNodeByText,
|
|
4276
|
+
filterLogcat,
|
|
4277
|
+
firstErrorLine,
|
|
4137
4278
|
flutterNodePoint,
|
|
4138
4279
|
flutterPhysicalViewport,
|
|
4139
4280
|
helpText,
|
|
@@ -4141,10 +4282,14 @@ module.exports = {
|
|
|
4141
4282
|
isAdbInputTextSafe,
|
|
4142
4283
|
isLikelyInstallerSurface,
|
|
4143
4284
|
nodeTapState,
|
|
4144
|
-
normalizeBridgeError,
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4285
|
+
normalizeBridgeError,
|
|
4286
|
+
normalizeActivityComponent,
|
|
4287
|
+
parseWebViewDevToolsSockets,
|
|
4288
|
+
parseArgs,
|
|
4289
|
+
parseKeyboardState,
|
|
4290
|
+
parseLauncherActivityCandidates,
|
|
4291
|
+
parseStartExtras,
|
|
4292
|
+
parseUiaBounds,
|
|
4148
4293
|
parseUiaViewport,
|
|
4149
4294
|
parseComponentFromWindowLine,
|
|
4150
4295
|
parseForegroundWindow,
|
package/bin/mcp-server.js
CHANGED
|
@@ -219,8 +219,10 @@ function toolDefinitions() {
|
|
|
219
219
|
installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
|
|
220
220
|
intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
|
|
221
221
|
}, ['apkPath']),
|
|
222
|
-
bridgeTool('
|
|
223
|
-
bridgeTool('
|
|
222
|
+
bridgeTool('launch_app', 'Launch the target package LAUNCHER Activity. If multiple launcher Activities exist, returns launcher_ambiguous with candidates unless activity or component is explicit.', launchProperties()),
|
|
223
|
+
bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
|
|
224
|
+
bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
|
|
225
|
+
bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
|
|
224
226
|
bridgeTool('tap', 'Tap device coordinates through ADB.', {
|
|
225
227
|
tapX: { type: 'number' },
|
|
226
228
|
tapY: { type: 'number' },
|
|
@@ -233,12 +235,12 @@ function toolDefinitions() {
|
|
|
233
235
|
targetText: { type: 'string' },
|
|
234
236
|
timeoutSec: { type: 'number' },
|
|
235
237
|
}, ['targetText']),
|
|
236
|
-
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode;
|
|
238
|
+
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode; always pass packageName so the tool targets the intended app.', {
|
|
237
239
|
text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
|
|
238
240
|
tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
|
|
239
241
|
tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
|
|
240
242
|
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
241
|
-
}, ['text']),
|
|
243
|
+
}, ['text', 'packageName']),
|
|
242
244
|
bridgeTool('keyboard_state', 'Read Android soft-keyboard visibility from dumpsys input_method.'),
|
|
243
245
|
bridgeTool('hide_keyboard', 'Hide the Android soft keyboard when it is visible.', {
|
|
244
246
|
force: { type: 'boolean', description: 'Send keyboard-dismiss keys even when the visibility probe says the keyboard is hidden.' },
|
|
@@ -287,14 +289,38 @@ function toolDefinitions() {
|
|
|
287
289
|
];
|
|
288
290
|
}
|
|
289
291
|
|
|
290
|
-
function bridgeTool(name, description, properties = {}, required = []) {
|
|
291
|
-
return {
|
|
292
|
-
name,
|
|
293
|
-
description,
|
|
294
|
-
inputSchema: baseSchema(properties, required),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
292
|
+
function bridgeTool(name, description, properties = {}, required = []) {
|
|
293
|
+
return {
|
|
294
|
+
name,
|
|
295
|
+
description,
|
|
296
|
+
inputSchema: baseSchema(properties, required),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function launchProperties() {
|
|
301
|
+
return {
|
|
302
|
+
activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
|
|
303
|
+
component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
|
|
304
|
+
action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
|
|
305
|
+
category: {
|
|
306
|
+
oneOf: [
|
|
307
|
+
{ type: 'string' },
|
|
308
|
+
{ type: 'array', items: { type: 'string' } },
|
|
309
|
+
],
|
|
310
|
+
description: 'Intent category or categories.',
|
|
311
|
+
},
|
|
312
|
+
data: { type: 'string', description: 'Intent data URI.' },
|
|
313
|
+
extra: {
|
|
314
|
+
oneOf: [
|
|
315
|
+
{ type: 'string' },
|
|
316
|
+
{ type: 'array', items: { type: 'string' } },
|
|
317
|
+
{ type: 'object', additionalProperties: { type: 'string' } },
|
|
318
|
+
],
|
|
319
|
+
description: 'String extras. Use key=value strings or an object of string values.',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
298
324
|
function baseSchema(extraProperties = {}, extraRequired = []) {
|
|
299
325
|
return {
|
|
300
326
|
type: 'object',
|
|
@@ -324,11 +350,14 @@ function h5TargetSchema() {
|
|
|
324
350
|
};
|
|
325
351
|
}
|
|
326
352
|
|
|
327
|
-
async function callTool(name, args) {
|
|
328
|
-
if (name === 'run_smoke') {
|
|
329
|
-
return runSmoke(args);
|
|
330
|
-
}
|
|
331
|
-
|
|
353
|
+
async function callTool(name, args) {
|
|
354
|
+
if (name === 'run_smoke') {
|
|
355
|
+
return runSmoke(args);
|
|
356
|
+
}
|
|
357
|
+
if (name === 'input_text' && !args.packageName) {
|
|
358
|
+
return toolText('packageName is required for input_text so text input is routed to the intended app bridge.', true);
|
|
359
|
+
}
|
|
360
|
+
const commandMap = {
|
|
332
361
|
flutter_tree: 'flutter-tree',
|
|
333
362
|
h5_dom: 'h5-dom',
|
|
334
363
|
h5_eval: 'h5-eval',
|
|
@@ -346,9 +375,11 @@ async function callTool(name, args) {
|
|
|
346
375
|
tap_flutter_text: 'tap-flutter-text',
|
|
347
376
|
input_flutter_text: 'input-flutter-text',
|
|
348
377
|
uia_tree: 'uia-tree',
|
|
349
|
-
install_apk: 'install-apk',
|
|
350
|
-
|
|
351
|
-
|
|
378
|
+
install_apk: 'install-apk',
|
|
379
|
+
launch_app: 'launch-app',
|
|
380
|
+
launch_activity: 'launch-activity',
|
|
381
|
+
launch_native_test: 'launch-native-test',
|
|
382
|
+
launch_flutter: 'launch-flutter',
|
|
352
383
|
tap_text: 'tap-text',
|
|
353
384
|
wait_text: 'wait-text',
|
|
354
385
|
input_text: 'input-text',
|
|
@@ -371,8 +402,14 @@ async function callTool(name, args) {
|
|
|
371
402
|
async function runBridge(command, args) {
|
|
372
403
|
const cliArgs = [cliScript, command];
|
|
373
404
|
addCommonArgs(cliArgs, args);
|
|
374
|
-
addArg(cliArgs, 'initial-route', args.initialRoute);
|
|
375
|
-
addArg(cliArgs, '
|
|
405
|
+
addArg(cliArgs, 'initial-route', args.initialRoute);
|
|
406
|
+
addArg(cliArgs, 'activity', args.activity);
|
|
407
|
+
addArg(cliArgs, 'component', args.component);
|
|
408
|
+
addArg(cliArgs, 'action', args.action);
|
|
409
|
+
addRepeatedArg(cliArgs, 'category', args.category);
|
|
410
|
+
addArg(cliArgs, 'data', args.data);
|
|
411
|
+
addExtraArgs(cliArgs, args.extra);
|
|
412
|
+
addArg(cliArgs, 'out-file', args.outFile);
|
|
376
413
|
addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor(command, args));
|
|
377
414
|
addArg(cliArgs, 'apk-path', args.apkPath);
|
|
378
415
|
addArg(cliArgs, 'tap-x', args.tapX);
|
|
@@ -459,13 +496,35 @@ function addCommonArgs(cliArgs, args) {
|
|
|
459
496
|
addArg(cliArgs, 'package-name', args.packageName);
|
|
460
497
|
}
|
|
461
498
|
|
|
462
|
-
function addArg(cliArgs, name, value) {
|
|
463
|
-
if (value === undefined || value === null || value === '' || value === false) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
cliArgs.push(`--${name}`, String(value));
|
|
467
|
-
}
|
|
468
|
-
|
|
499
|
+
function addArg(cliArgs, name, value) {
|
|
500
|
+
if (value === undefined || value === null || value === '' || value === false) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
cliArgs.push(`--${name}`, String(value));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function addRepeatedArg(cliArgs, name, value) {
|
|
507
|
+
if (Array.isArray(value)) {
|
|
508
|
+
for (const item of value) addArg(cliArgs, name, item);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
addArg(cliArgs, name, value);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function addExtraArgs(cliArgs, value) {
|
|
515
|
+
if (Array.isArray(value)) {
|
|
516
|
+
for (const item of value) addArg(cliArgs, 'extra', item);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (value && typeof value === 'object') {
|
|
520
|
+
for (const [key, extraValue] of Object.entries(value)) {
|
|
521
|
+
addArg(cliArgs, 'extra', `${key}=${extraValue}`);
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
addArg(cliArgs, 'extra', value);
|
|
526
|
+
}
|
|
527
|
+
|
|
469
528
|
function runProcess(cliArgs) {
|
|
470
529
|
return new Promise((resolve) => {
|
|
471
530
|
const child = spawn(nodeBinary, cliArgs, {
|
|
@@ -490,12 +549,23 @@ function runProcess(cliArgs) {
|
|
|
490
549
|
code === 0 ? '' : `exitCode: ${code}`,
|
|
491
550
|
retryWithPackageNameHint(cliArgs, stdout, stderr, code),
|
|
492
551
|
].filter(Boolean).join('\n\n');
|
|
493
|
-
resolve(toolText(text ||
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function
|
|
552
|
+
resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function emptyProcessText(cliArgs) {
|
|
558
|
+
const command = cliArgs[1] || '';
|
|
559
|
+
if (command === 'logcat' && cliArgs.includes('--app-pid')) {
|
|
560
|
+
return 'logcat: no matching lines for current app pid';
|
|
561
|
+
}
|
|
562
|
+
if (command === 'logcat') {
|
|
563
|
+
return 'logcat: no matching lines';
|
|
564
|
+
}
|
|
565
|
+
return 'ok';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
499
569
|
if (code === 0 || cliArgs.includes('--package-name') || cliArgs.includes('--port')) {
|
|
500
570
|
return '';
|
|
501
571
|
}
|