@aiscene/android 1.6.7 → 1.6.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/LICENSE +21 -0
- package/dist/es/cli.mjs +125 -95
- package/dist/es/index.mjs +124 -94
- package/dist/es/mcp-server.mjs +125 -95
- package/dist/lib/cli.js +123 -93
- package/dist/lib/index.js +122 -92
- package/dist/lib/mcp-server.js +123 -93
- package/dist/types/index.d.ts +40 -31
- package/dist/types/mcp-server.d.ts +40 -31
- package/package.json +24 -21
package/dist/lib/mcp-server.js
CHANGED
|
@@ -564,6 +564,7 @@ var __webpack_exports__ = {};
|
|
|
564
564
|
};
|
|
565
565
|
const external_node_assert_namespaceObject = require("node:assert");
|
|
566
566
|
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
567
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
567
568
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
568
569
|
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
|
|
569
570
|
var external_node_module_ = __webpack_require__("node:module");
|
|
@@ -705,6 +706,9 @@ var __webpack_exports__ = {};
|
|
|
705
706
|
const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
|
|
706
707
|
const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
|
|
707
708
|
const debugDevice = (0, logger_.getDebug)('android:device');
|
|
709
|
+
function escapeForShell(text) {
|
|
710
|
+
return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
|
|
711
|
+
}
|
|
708
712
|
class AndroidDevice {
|
|
709
713
|
actionSpace() {
|
|
710
714
|
const defaultActions = [
|
|
@@ -732,6 +736,12 @@ var __webpack_exports__ = {};
|
|
|
732
736
|
]).default('replace').optional().describe('Input mode: "replace" (default) - clear the field and input the value; "typeOnly" - type the value directly without clearing the field first; "clear" - clear the field without inputting new text.')),
|
|
733
737
|
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The input field to be filled').optional()
|
|
734
738
|
}),
|
|
739
|
+
sample: {
|
|
740
|
+
value: 'test@example.com',
|
|
741
|
+
locate: {
|
|
742
|
+
prompt: 'the email input field'
|
|
743
|
+
}
|
|
744
|
+
},
|
|
735
745
|
call: async (param)=>{
|
|
736
746
|
const element = param.locate;
|
|
737
747
|
if ('typeOnly' !== param.mode) await this.clearInput(element);
|
|
@@ -777,9 +787,21 @@ var __webpack_exports__ = {};
|
|
|
777
787
|
y: to.center[1]
|
|
778
788
|
});
|
|
779
789
|
}),
|
|
790
|
+
(0, device_namespaceObject.defineActionSwipe)(async (param)=>{
|
|
791
|
+
const { startPoint, endPoint, duration, repeatCount } = (0, device_namespaceObject.normalizeMobileSwipeParam)(param, await this.size());
|
|
792
|
+
for(let i = 0; i < repeatCount; i++)await this.mouseDrag(startPoint, endPoint, duration);
|
|
793
|
+
}),
|
|
780
794
|
(0, device_namespaceObject.defineActionKeyboardPress)(async (param)=>{
|
|
781
795
|
await this.keyboardPress(param.keyName);
|
|
782
796
|
}),
|
|
797
|
+
(0, device_namespaceObject.defineActionCursorMove)(async (param)=>{
|
|
798
|
+
const arrowKey = 'left' === param.direction ? 'ArrowLeft' : 'ArrowRight';
|
|
799
|
+
const times = param.times ?? 1;
|
|
800
|
+
for(let i = 0; i < times; i++){
|
|
801
|
+
await this.keyboardPress(arrowKey);
|
|
802
|
+
await (0, core_utils_namespaceObject.sleep)(100);
|
|
803
|
+
}
|
|
804
|
+
}),
|
|
783
805
|
(0, device_namespaceObject.defineAction)({
|
|
784
806
|
name: 'LongPress',
|
|
785
807
|
description: 'Trigger a long press on the screen at specified element',
|
|
@@ -787,6 +809,11 @@ var __webpack_exports__ = {};
|
|
|
787
809
|
duration: core_namespaceObject.z.number().optional().describe('The duration of the long press in milliseconds'),
|
|
788
810
|
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The element to be long pressed')
|
|
789
811
|
}),
|
|
812
|
+
sample: {
|
|
813
|
+
locate: {
|
|
814
|
+
prompt: 'the message bubble'
|
|
815
|
+
}
|
|
816
|
+
},
|
|
790
817
|
call: async (param)=>{
|
|
791
818
|
const element = param.locate;
|
|
792
819
|
if (!element) throw new Error('LongPress requires an element to be located');
|
|
@@ -806,6 +833,12 @@ var __webpack_exports__ = {};
|
|
|
806
833
|
duration: core_namespaceObject.z.number().optional().describe('The duration of the pull (in milliseconds)'),
|
|
807
834
|
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().optional().describe('The element to start the pull from (optional)')
|
|
808
835
|
}),
|
|
836
|
+
sample: {
|
|
837
|
+
direction: 'down',
|
|
838
|
+
locate: {
|
|
839
|
+
prompt: 'the center of the content list area'
|
|
840
|
+
}
|
|
841
|
+
},
|
|
809
842
|
call: async (param)=>{
|
|
810
843
|
const element = param.locate;
|
|
811
844
|
const startPoint = element ? {
|
|
@@ -818,6 +851,16 @@ var __webpack_exports__ = {};
|
|
|
818
851
|
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
819
852
|
}
|
|
820
853
|
}),
|
|
854
|
+
(0, device_namespaceObject.defineActionPinch)(async (param)=>{
|
|
855
|
+
const { centerX, centerY, startDistance, endDistance, duration } = (0, device_namespaceObject.normalizePinchParam)(param, await this.size());
|
|
856
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(centerX, centerY);
|
|
857
|
+
const ratio = 0 !== adjCenterX && 0 !== centerX ? adjCenterX / centerX : 1;
|
|
858
|
+
const adjStartDist = Math.round(startDistance * ratio);
|
|
859
|
+
const adjEndDist = Math.round(endDistance * ratio);
|
|
860
|
+
await this.ensureYadb();
|
|
861
|
+
const adb = await this.getAdb();
|
|
862
|
+
await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${duration}`);
|
|
863
|
+
}),
|
|
821
864
|
(0, device_namespaceObject.defineActionClearInput)(async (param)=>{
|
|
822
865
|
await this.clearInput(param.locate);
|
|
823
866
|
})
|
|
@@ -962,66 +1005,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
962
1005
|
async execYadb(keyboardContent) {
|
|
963
1006
|
await this.ensureYadb();
|
|
964
1007
|
const adb = await this.getAdb();
|
|
965
|
-
|
|
966
|
-
const command = `app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`;
|
|
967
|
-
debugDevice(`Executing YADB input: "${keyboardContent}"`);
|
|
968
|
-
const inputPromise = adb.shell(command);
|
|
969
|
-
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('YADB timeout')), 1000));
|
|
970
|
-
await Promise.race([
|
|
971
|
-
inputPromise,
|
|
972
|
-
timeoutPromise
|
|
973
|
-
]);
|
|
974
|
-
debugDevice(`YADB input completed: "${keyboardContent}"`);
|
|
975
|
-
} catch (error) {
|
|
976
|
-
const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
|
|
977
|
-
if (isAccessibilityConflict) {
|
|
978
|
-
debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
|
|
979
|
-
await this.inputViaClipboard(keyboardContent);
|
|
980
|
-
} else 'YADB timeout' === error.message ? debugDevice(`YADB timed out after 2s, assuming input succeeded: "${keyboardContent}"`) : debugDevice(`YADB execution may have completed despite error: ${error}`);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
async inputViaClipboard(text) {
|
|
984
|
-
const adb = await this.getAdb();
|
|
985
|
-
try {
|
|
986
|
-
debugDevice(`Inputting via clipboard: "${text}"`);
|
|
987
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
988
|
-
const setClipboardCmd = `
|
|
989
|
-
content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
|
|
990
|
-
`;
|
|
991
|
-
await adb.shell(setClipboardCmd);
|
|
992
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
993
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
994
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
995
|
-
debugDevice(`Clipboard input completed via content provider: "${text}"`);
|
|
996
|
-
} catch (error1) {
|
|
997
|
-
debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
|
|
998
|
-
try {
|
|
999
|
-
const base64Text = Buffer.from(text, 'utf-8').toString('base64');
|
|
1000
|
-
await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
|
|
1001
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
1002
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
1003
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
1004
|
-
debugDevice(`Clipboard input completed via clipper: "${text}"`);
|
|
1005
|
-
} catch (error2) {
|
|
1006
|
-
debugDevice(`All clipboard methods failed: ${error2}`);
|
|
1007
|
-
const isPureAscii = /^[\x00-\x7F]*$/.test(text);
|
|
1008
|
-
if (isPureAscii) {
|
|
1009
|
-
debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
|
|
1010
|
-
await adb.inputText(text);
|
|
1011
|
-
} else await this.inputCharByChar(text);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
async inputCharByChar(text) {
|
|
1016
|
-
const adb = await this.getAdb();
|
|
1017
|
-
debugDevice(`Inputting character by character (slow method): "${text}"`);
|
|
1018
|
-
const chars = Array.from(text);
|
|
1019
|
-
for (const char of chars){
|
|
1020
|
-
if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
|
|
1021
|
-
else await adb.shell(`input text "${char}"`);
|
|
1022
|
-
await (0, core_utils_namespaceObject.sleep)(50);
|
|
1023
|
-
}
|
|
1024
|
-
debugDevice("Character-by-character input completed");
|
|
1008
|
+
await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
|
|
1025
1009
|
}
|
|
1026
1010
|
async getElementsInfo() {
|
|
1027
1011
|
return [];
|
|
@@ -1034,6 +1018,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1034
1018
|
}
|
|
1035
1019
|
async getScreenSize() {
|
|
1036
1020
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1021
|
+
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
1037
1022
|
if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
|
|
1038
1023
|
const adb = await this.getAdb();
|
|
1039
1024
|
if ('number' == typeof this.options?.displayId) try {
|
|
@@ -1167,6 +1152,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1167
1152
|
}
|
|
1168
1153
|
async getDisplayOrientation() {
|
|
1169
1154
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1155
|
+
debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
|
|
1170
1156
|
if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
|
|
1171
1157
|
const adb = await this.getAdb();
|
|
1172
1158
|
let orientation = 0;
|
|
@@ -1192,6 +1178,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1192
1178
|
if (shouldCache) this.cachedOrientation = orientation;
|
|
1193
1179
|
return orientation;
|
|
1194
1180
|
}
|
|
1181
|
+
async getOrientedPhysicalSize() {
|
|
1182
|
+
const info = await this.getDevicePhysicalInfo();
|
|
1183
|
+
const isLandscape = 1 === info.orientation || 3 === info.orientation;
|
|
1184
|
+
const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
|
|
1185
|
+
return {
|
|
1186
|
+
width: shouldSwap ? info.physicalHeight : info.physicalWidth,
|
|
1187
|
+
height: shouldSwap ? info.physicalWidth : info.physicalHeight
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1195
1190
|
async size() {
|
|
1196
1191
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1197
1192
|
const adapter = this.getScrcpyAdapter();
|
|
@@ -1218,7 +1213,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1218
1213
|
height: logicalHeight
|
|
1219
1214
|
};
|
|
1220
1215
|
}
|
|
1221
|
-
async cacheFeatureForPoint(center
|
|
1216
|
+
async cacheFeatureForPoint(center) {
|
|
1222
1217
|
const { width, height } = await this.size();
|
|
1223
1218
|
debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
|
|
1224
1219
|
return {
|
|
@@ -1298,14 +1293,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1298
1293
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1299
1294
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1300
1295
|
try {
|
|
1301
|
-
if (useShellScreencap
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1296
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1297
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1298
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1299
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1300
|
+
if (!screenshotBuffer) {
|
|
1301
|
+
this.takeScreenshotFailCount++;
|
|
1302
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1303
|
+
}
|
|
1304
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1305
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1306
|
+
this.takeScreenshotFailCount++;
|
|
1307
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1308
|
+
}
|
|
1309
|
+
this.takeScreenshotFailCount = 0;
|
|
1310
|
+
} else {
|
|
1311
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1312
|
+
throw new Error('Using shell screencap directly');
|
|
1309
1313
|
}
|
|
1310
1314
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1311
1315
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1334,12 +1338,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1334
1338
|
screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
|
|
1335
1339
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1336
1340
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1337
|
-
if (!(0, img_namespaceObject.
|
|
1341
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1338
1342
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1339
1343
|
} finally{
|
|
1340
|
-
|
|
1341
|
-
|
|
1344
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1345
|
+
const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
|
|
1346
|
+
'-s',
|
|
1347
|
+
this.deviceId,
|
|
1348
|
+
'shell',
|
|
1349
|
+
`rm ${androidScreenshotPath}`
|
|
1350
|
+
], {
|
|
1351
|
+
timeout: 3000
|
|
1352
|
+
}, (err)=>{
|
|
1353
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1342
1354
|
});
|
|
1355
|
+
child.unref();
|
|
1343
1356
|
}
|
|
1344
1357
|
}
|
|
1345
1358
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1502,24 +1515,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1502
1515
|
if (!this.yadbPushed) {
|
|
1503
1516
|
const adb = await this.getAdb();
|
|
1504
1517
|
const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
|
|
1505
|
-
const
|
|
1506
|
-
await adb.push(
|
|
1518
|
+
const yadbBin = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin', 'yadb');
|
|
1519
|
+
await adb.push(yadbBin, '/data/local/tmp');
|
|
1507
1520
|
this.yadbPushed = true;
|
|
1508
1521
|
}
|
|
1509
1522
|
}
|
|
1510
1523
|
shouldUseYadbForText(text) {
|
|
1511
1524
|
const hasNonAscii = /[\x80-\uFFFF]/.test(text);
|
|
1512
1525
|
const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
|
|
1513
|
-
|
|
1526
|
+
const hasShellSpecialChars = /[\\`$]/.test(text);
|
|
1527
|
+
const hasBothQuotes = text.includes('"') && text.includes("'");
|
|
1528
|
+
return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
|
|
1514
1529
|
}
|
|
1515
1530
|
async keyboardType(text, options) {
|
|
1516
1531
|
if (!text) return;
|
|
1517
1532
|
const adb = await this.getAdb();
|
|
1518
|
-
const shouldUseYadb = this.shouldUseYadbForText(text);
|
|
1519
1533
|
const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1520
1534
|
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
1521
|
-
|
|
1522
|
-
|
|
1535
|
+
const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
|
|
1536
|
+
if (useYadb) await this.execYadb(escapeForShell(text));
|
|
1537
|
+
else {
|
|
1538
|
+
const segments = text.split('\n');
|
|
1539
|
+
for(let i = 0; i < segments.length; i++){
|
|
1540
|
+
if (segments[i].length > 0) await adb.inputText(segments[i]);
|
|
1541
|
+
if (i < segments.length - 1) await adb.keyevent(66);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1523
1544
|
if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
1524
1545
|
}
|
|
1525
1546
|
normalizeKeyName(key) {
|
|
@@ -1567,12 +1588,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1567
1588
|
}
|
|
1568
1589
|
async mouseClick(x, y) {
|
|
1569
1590
|
const adb = await this.getAdb();
|
|
1570
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1591
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1571
1592
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
|
|
1572
1593
|
}
|
|
1573
1594
|
async mouseDoubleClick(x, y) {
|
|
1574
1595
|
const adb = await this.getAdb();
|
|
1575
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1596
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1576
1597
|
const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
|
|
1577
1598
|
await adb.shell(tapCommand);
|
|
1578
1599
|
await (0, core_utils_namespaceObject.sleep)(50);
|
|
@@ -1583,8 +1604,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1583
1604
|
}
|
|
1584
1605
|
async mouseDrag(from, to, duration) {
|
|
1585
1606
|
const adb = await this.getAdb();
|
|
1586
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1587
|
-
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
1607
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1608
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1588
1609
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1589
1610
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
|
|
1590
1611
|
}
|
|
@@ -1594,16 +1615,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1594
1615
|
const n = 4;
|
|
1595
1616
|
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
1596
1617
|
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
1599
|
-
const
|
|
1600
|
-
const
|
|
1618
|
+
const maxPositiveDeltaX = startX;
|
|
1619
|
+
const maxNegativeDeltaX = width - startX;
|
|
1620
|
+
const maxPositiveDeltaY = startY;
|
|
1621
|
+
const maxNegativeDeltaY = height - startY;
|
|
1601
1622
|
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
1602
1623
|
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
1603
1624
|
const endX = Math.round(startX - deltaX);
|
|
1604
1625
|
const endY = Math.round(startY - deltaY);
|
|
1605
|
-
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
|
|
1606
|
-
const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY);
|
|
1626
|
+
const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
|
|
1627
|
+
const { x: adjustedEndX, y: adjustedEndY } = await this.adjustCoordinates(endX, endY);
|
|
1607
1628
|
const adb = await this.getAdb();
|
|
1608
1629
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1609
1630
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
|
|
@@ -1614,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1614
1635
|
this.cachedPhysicalDisplayId = void 0;
|
|
1615
1636
|
this.cachedScreenSize = null;
|
|
1616
1637
|
this.cachedOrientation = null;
|
|
1638
|
+
this.scalingRatio = 1;
|
|
1617
1639
|
if (this.scrcpyAdapter) {
|
|
1618
1640
|
await this.scrcpyAdapter.disconnect();
|
|
1619
1641
|
this.scrcpyAdapter = null;
|
|
@@ -1653,7 +1675,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1653
1675
|
}
|
|
1654
1676
|
async longPress(x, y, duration = 2000) {
|
|
1655
1677
|
const adb = await this.getAdb();
|
|
1656
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1678
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1657
1679
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
1658
1680
|
}
|
|
1659
1681
|
async pullDown(startPoint, distance, duration = 800) {
|
|
@@ -1675,8 +1697,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1675
1697
|
}
|
|
1676
1698
|
async pullDrag(from, to, duration) {
|
|
1677
1699
|
const adb = await this.getAdb();
|
|
1678
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1679
|
-
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
1700
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1701
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1680
1702
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1681
1703
|
}
|
|
1682
1704
|
async pullUp(startPoint, distance, duration = 600) {
|
|
@@ -1763,7 +1785,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1763
1785
|
device_define_property(this, "yadbPushed", false);
|
|
1764
1786
|
device_define_property(this, "devicePixelRatio", 1);
|
|
1765
1787
|
device_define_property(this, "devicePixelRatioInitialized", false);
|
|
1766
|
-
device_define_property(this, "scalingRatio", 1);
|
|
1767
1788
|
device_define_property(this, "adb", null);
|
|
1768
1789
|
device_define_property(this, "connectingAdb", null);
|
|
1769
1790
|
device_define_property(this, "destroyed", false);
|
|
@@ -1774,6 +1795,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1774
1795
|
device_define_property(this, "cachedPhysicalDisplayId", void 0);
|
|
1775
1796
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1776
1797
|
device_define_property(this, "appNameMapping", {});
|
|
1798
|
+
device_define_property(this, "scalingRatio", 1);
|
|
1799
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1777
1800
|
device_define_property(this, "interfaceType", 'android');
|
|
1778
1801
|
device_define_property(this, "uri", void 0);
|
|
1779
1802
|
device_define_property(this, "options", void 0);
|
|
@@ -1783,6 +1806,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1783
1806
|
this.customActions = options?.customActions;
|
|
1784
1807
|
}
|
|
1785
1808
|
}
|
|
1809
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1786
1810
|
const runAdbShellParamSchema = core_namespaceObject.z.object({
|
|
1787
1811
|
command: core_namespaceObject.z.string().describe('ADB shell command to execute')
|
|
1788
1812
|
});
|
|
@@ -1795,6 +1819,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1795
1819
|
description: 'Execute ADB shell command on Android device',
|
|
1796
1820
|
interfaceAlias: 'runAdbShell',
|
|
1797
1821
|
paramSchema: runAdbShellParamSchema,
|
|
1822
|
+
sample: {
|
|
1823
|
+
command: 'dumpsys window displays | grep -E "mCurrentFocus"'
|
|
1824
|
+
},
|
|
1798
1825
|
call: async (param)=>{
|
|
1799
1826
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
1800
1827
|
const adb = await device.getAdb();
|
|
@@ -1806,6 +1833,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1806
1833
|
description: 'Launch an Android app or URL',
|
|
1807
1834
|
interfaceAlias: 'launch',
|
|
1808
1835
|
paramSchema: launchParamSchema,
|
|
1836
|
+
sample: {
|
|
1837
|
+
uri: 'com.example.app'
|
|
1838
|
+
},
|
|
1809
1839
|
call: async (param)=>{
|
|
1810
1840
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
1811
1841
|
await device.launch(param.uri);
|
|
@@ -1958,7 +1988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1958
1988
|
constructor(toolsManager){
|
|
1959
1989
|
super({
|
|
1960
1990
|
name: '@midscene/android-mcp',
|
|
1961
|
-
version: "1.6.
|
|
1991
|
+
version: "1.6.8",
|
|
1962
1992
|
description: 'Control the Android device using natural language commands'
|
|
1963
1993
|
}, toolsManager);
|
|
1964
1994
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,13 +10,10 @@ import { AndroidDeviceOpt } from '@midscene/core/device';
|
|
|
10
10
|
import { BaseMidsceneTools } from '@midscene/shared/mcp';
|
|
11
11
|
import { Device } from 'appium-adb';
|
|
12
12
|
import { DeviceAction } from '@midscene/core';
|
|
13
|
-
import { ElementCacheFeature } from '@midscene/core';
|
|
14
13
|
import type { ElementInfo } from '@midscene/shared/extractor';
|
|
15
|
-
import { IModelConfig } from '@midscene/shared/env';
|
|
16
14
|
import { InterfaceType } from '@midscene/core';
|
|
17
15
|
import { overrideAIConfig } from '@midscene/shared/env';
|
|
18
16
|
import { Point } from '@midscene/core';
|
|
19
|
-
import { Rect } from '@midscene/core';
|
|
20
17
|
import { Size } from '@midscene/core';
|
|
21
18
|
import { ToolDefinition } from '@midscene/shared/mcp';
|
|
22
19
|
|
|
@@ -68,7 +65,6 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
68
65
|
private yadbPushed;
|
|
69
66
|
private devicePixelRatio;
|
|
70
67
|
private devicePixelRatioInitialized;
|
|
71
|
-
private scalingRatio;
|
|
72
68
|
private adb;
|
|
73
69
|
private connectingAdb;
|
|
74
70
|
private destroyed;
|
|
@@ -79,6 +75,9 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
79
75
|
private cachedPhysicalDisplayId;
|
|
80
76
|
private scrcpyAdapter;
|
|
81
77
|
private appNameMapping;
|
|
78
|
+
private scalingRatio;
|
|
79
|
+
private takeScreenshotFailCount;
|
|
80
|
+
private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
|
|
82
81
|
interfaceType: InterfaceType;
|
|
83
82
|
uri: string | undefined;
|
|
84
83
|
options?: AndroidDeviceOpt;
|
|
@@ -109,16 +108,6 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
109
108
|
private resolvePackageName;
|
|
110
109
|
launch(uri: string): Promise<AndroidDevice>;
|
|
111
110
|
execYadb(keyboardContent: string): Promise<void>;
|
|
112
|
-
/**
|
|
113
|
-
* 通过剪贴板输入文本(备用方案,当YADB不可用时)
|
|
114
|
-
* 支持中文等非ASCII字符
|
|
115
|
-
*/
|
|
116
|
-
private inputViaClipboard;
|
|
117
|
-
/**
|
|
118
|
-
* 逐字符输入文本(最后备用方案)
|
|
119
|
-
* 通过模拟按键输入,支持中文(如果有对应输入法)
|
|
120
|
-
*/
|
|
121
|
-
private inputCharByChar;
|
|
122
111
|
getElementsInfo(): Promise<ElementInfo[]>;
|
|
123
112
|
getElementsNodeTree(): Promise<any>;
|
|
124
113
|
getScreenSize(): Promise<{
|
|
@@ -130,22 +119,38 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
130
119
|
private initializeDevicePixelRatio;
|
|
131
120
|
getDisplayDensity(): Promise<number>;
|
|
132
121
|
getDisplayOrientation(): Promise<number>;
|
|
133
|
-
size(): Promise<Size>;
|
|
134
122
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
123
|
+
* Get physical screen dimensions adjusted for current orientation.
|
|
124
|
+
* Swaps width/height when the device is in landscape and the reported
|
|
125
|
+
* dimensions do not already reflect the current orientation.
|
|
138
126
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
private getOrientedPhysicalSize;
|
|
128
|
+
size(): Promise<Size>;
|
|
129
|
+
cacheFeatureForPoint(center: [number, number]): Promise<{
|
|
130
|
+
centerX: number;
|
|
131
|
+
centerY: number;
|
|
132
|
+
screenSize: {
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
};
|
|
136
|
+
}>;
|
|
137
|
+
rectMatchesCacheFeature(feature: {
|
|
138
|
+
centerX: number;
|
|
139
|
+
centerY: number;
|
|
140
|
+
screenSize: {
|
|
141
|
+
width: number;
|
|
142
|
+
height: number;
|
|
143
|
+
};
|
|
144
|
+
}): Promise<{
|
|
145
|
+
left: number;
|
|
146
|
+
top: number;
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
}>;
|
|
143
150
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* We return the exact cached coordinates for maximum accuracy.
|
|
151
|
+
* Convert logical coordinates (from AI) back to physical coordinates (for ADB).
|
|
152
|
+
* The ratio is derived from size(), so overriding size() alone is sufficient.
|
|
147
153
|
*/
|
|
148
|
-
rectMatchesCacheFeature(feature: ElementCacheFeature): Promise<Rect>;
|
|
149
154
|
private adjustCoordinates;
|
|
150
155
|
/**
|
|
151
156
|
* Calculate the end point for scroll operations based on start point, scroll delta, and screen boundaries.
|
|
@@ -174,11 +179,15 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
174
179
|
scrollRight(distance?: number, startPoint?: Point): Promise<void>;
|
|
175
180
|
ensureYadb(): Promise<void>;
|
|
176
181
|
/**
|
|
177
|
-
* Check if text contains characters that may cause issues with ADB inputText
|
|
178
|
-
*
|
|
179
|
-
* -
|
|
180
|
-
* -
|
|
181
|
-
* -
|
|
182
|
+
* Check if text contains characters that may cause issues with ADB inputText.
|
|
183
|
+
* appium-adb's inputText has known bugs with certain characters:
|
|
184
|
+
* - Backslash causes broken shell quoting
|
|
185
|
+
* - Backtick is not escaped at all
|
|
186
|
+
* - Text containing both " and ' throws an error
|
|
187
|
+
* - Dollar sign can cause variable expansion issues
|
|
188
|
+
*
|
|
189
|
+
* For these characters, we route through yadb which handles them correctly
|
|
190
|
+
* via escapeForShell + double-quoted shell context.
|
|
182
191
|
*/
|
|
183
192
|
private shouldUseYadbForText;
|
|
184
193
|
keyboardType(text: string, options?: AndroidDeviceInputOpt): Promise<void>;
|