@aiscene/android 1.6.6 → 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 -90
- package/dist/es/index.mjs +124 -89
- package/dist/es/mcp-server.mjs +125 -90
- package/dist/lib/cli.js +123 -88
- package/dist/lib/index.js +122 -87
- package/dist/lib/mcp-server.js +123 -88
- 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,61 +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
|
-
await adb.shell(command);
|
|
969
|
-
debugDevice(`YADB input completed: "${keyboardContent}"`);
|
|
970
|
-
} catch (error) {
|
|
971
|
-
const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
|
|
972
|
-
if (isAccessibilityConflict) {
|
|
973
|
-
debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
|
|
974
|
-
await this.inputViaClipboard(keyboardContent);
|
|
975
|
-
} else debugDevice(`YADB execution may have completed despite error: ${error}`);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
async inputViaClipboard(text) {
|
|
979
|
-
const adb = await this.getAdb();
|
|
980
|
-
try {
|
|
981
|
-
debugDevice(`Inputting via clipboard: "${text}"`);
|
|
982
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
983
|
-
const setClipboardCmd = `
|
|
984
|
-
content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
|
|
985
|
-
`;
|
|
986
|
-
await adb.shell(setClipboardCmd);
|
|
987
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
988
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
989
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
990
|
-
debugDevice(`Clipboard input completed via content provider: "${text}"`);
|
|
991
|
-
} catch (error1) {
|
|
992
|
-
debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
|
|
993
|
-
try {
|
|
994
|
-
const base64Text = Buffer.from(text, 'utf-8').toString('base64');
|
|
995
|
-
await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
|
|
996
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
997
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
998
|
-
await (0, core_utils_namespaceObject.sleep)(100);
|
|
999
|
-
debugDevice(`Clipboard input completed via clipper: "${text}"`);
|
|
1000
|
-
} catch (error2) {
|
|
1001
|
-
debugDevice(`All clipboard methods failed: ${error2}`);
|
|
1002
|
-
const isPureAscii = /^[\x00-\x7F]*$/.test(text);
|
|
1003
|
-
if (isPureAscii) {
|
|
1004
|
-
debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
|
|
1005
|
-
await adb.inputText(text);
|
|
1006
|
-
} else await this.inputCharByChar(text);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
async inputCharByChar(text) {
|
|
1011
|
-
const adb = await this.getAdb();
|
|
1012
|
-
debugDevice(`Inputting character by character (slow method): "${text}"`);
|
|
1013
|
-
const chars = Array.from(text);
|
|
1014
|
-
for (const char of chars){
|
|
1015
|
-
if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
|
|
1016
|
-
else await adb.shell(`input text "${char}"`);
|
|
1017
|
-
await (0, core_utils_namespaceObject.sleep)(50);
|
|
1018
|
-
}
|
|
1019
|
-
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}'`);
|
|
1020
1009
|
}
|
|
1021
1010
|
async getElementsInfo() {
|
|
1022
1011
|
return [];
|
|
@@ -1029,6 +1018,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1029
1018
|
}
|
|
1030
1019
|
async getScreenSize() {
|
|
1031
1020
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1021
|
+
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
1032
1022
|
if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
|
|
1033
1023
|
const adb = await this.getAdb();
|
|
1034
1024
|
if ('number' == typeof this.options?.displayId) try {
|
|
@@ -1162,6 +1152,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1162
1152
|
}
|
|
1163
1153
|
async getDisplayOrientation() {
|
|
1164
1154
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1155
|
+
debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
|
|
1165
1156
|
if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
|
|
1166
1157
|
const adb = await this.getAdb();
|
|
1167
1158
|
let orientation = 0;
|
|
@@ -1187,6 +1178,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1187
1178
|
if (shouldCache) this.cachedOrientation = orientation;
|
|
1188
1179
|
return orientation;
|
|
1189
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
|
+
}
|
|
1190
1190
|
async size() {
|
|
1191
1191
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1192
1192
|
const adapter = this.getScrcpyAdapter();
|
|
@@ -1213,7 +1213,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1213
1213
|
height: logicalHeight
|
|
1214
1214
|
};
|
|
1215
1215
|
}
|
|
1216
|
-
async cacheFeatureForPoint(center
|
|
1216
|
+
async cacheFeatureForPoint(center) {
|
|
1217
1217
|
const { width, height } = await this.size();
|
|
1218
1218
|
debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
|
|
1219
1219
|
return {
|
|
@@ -1293,14 +1293,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1293
1293
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1294
1294
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1295
1295
|
try {
|
|
1296
|
-
if (useShellScreencap
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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');
|
|
1304
1313
|
}
|
|
1305
1314
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1306
1315
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1329,12 +1338,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1329
1338
|
screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
|
|
1330
1339
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1331
1340
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1332
|
-
if (!(0, img_namespaceObject.
|
|
1341
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1333
1342
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1334
1343
|
} finally{
|
|
1335
|
-
|
|
1336
|
-
|
|
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);
|
|
1337
1354
|
});
|
|
1355
|
+
child.unref();
|
|
1338
1356
|
}
|
|
1339
1357
|
}
|
|
1340
1358
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1497,24 +1515,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1497
1515
|
if (!this.yadbPushed) {
|
|
1498
1516
|
const adb = await this.getAdb();
|
|
1499
1517
|
const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
|
|
1500
|
-
const
|
|
1501
|
-
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');
|
|
1502
1520
|
this.yadbPushed = true;
|
|
1503
1521
|
}
|
|
1504
1522
|
}
|
|
1505
1523
|
shouldUseYadbForText(text) {
|
|
1506
1524
|
const hasNonAscii = /[\x80-\uFFFF]/.test(text);
|
|
1507
1525
|
const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
|
|
1508
|
-
|
|
1526
|
+
const hasShellSpecialChars = /[\\`$]/.test(text);
|
|
1527
|
+
const hasBothQuotes = text.includes('"') && text.includes("'");
|
|
1528
|
+
return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
|
|
1509
1529
|
}
|
|
1510
1530
|
async keyboardType(text, options) {
|
|
1511
1531
|
if (!text) return;
|
|
1512
1532
|
const adb = await this.getAdb();
|
|
1513
|
-
const shouldUseYadb = this.shouldUseYadbForText(text);
|
|
1514
1533
|
const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1515
1534
|
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
+
}
|
|
1518
1544
|
if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
1519
1545
|
}
|
|
1520
1546
|
normalizeKeyName(key) {
|
|
@@ -1562,12 +1588,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1562
1588
|
}
|
|
1563
1589
|
async mouseClick(x, y) {
|
|
1564
1590
|
const adb = await this.getAdb();
|
|
1565
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1591
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1566
1592
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
|
|
1567
1593
|
}
|
|
1568
1594
|
async mouseDoubleClick(x, y) {
|
|
1569
1595
|
const adb = await this.getAdb();
|
|
1570
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1596
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1571
1597
|
const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
|
|
1572
1598
|
await adb.shell(tapCommand);
|
|
1573
1599
|
await (0, core_utils_namespaceObject.sleep)(50);
|
|
@@ -1578,8 +1604,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1578
1604
|
}
|
|
1579
1605
|
async mouseDrag(from, to, duration) {
|
|
1580
1606
|
const adb = await this.getAdb();
|
|
1581
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1582
|
-
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);
|
|
1583
1609
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1584
1610
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
|
|
1585
1611
|
}
|
|
@@ -1589,16 +1615,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1589
1615
|
const n = 4;
|
|
1590
1616
|
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
1591
1617
|
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
1592
|
-
const
|
|
1593
|
-
const
|
|
1594
|
-
const
|
|
1595
|
-
const
|
|
1618
|
+
const maxPositiveDeltaX = startX;
|
|
1619
|
+
const maxNegativeDeltaX = width - startX;
|
|
1620
|
+
const maxPositiveDeltaY = startY;
|
|
1621
|
+
const maxNegativeDeltaY = height - startY;
|
|
1596
1622
|
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
1597
1623
|
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
1598
1624
|
const endX = Math.round(startX - deltaX);
|
|
1599
1625
|
const endY = Math.round(startY - deltaY);
|
|
1600
|
-
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
|
|
1601
|
-
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);
|
|
1602
1628
|
const adb = await this.getAdb();
|
|
1603
1629
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1604
1630
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
|
|
@@ -1609,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1609
1635
|
this.cachedPhysicalDisplayId = void 0;
|
|
1610
1636
|
this.cachedScreenSize = null;
|
|
1611
1637
|
this.cachedOrientation = null;
|
|
1638
|
+
this.scalingRatio = 1;
|
|
1612
1639
|
if (this.scrcpyAdapter) {
|
|
1613
1640
|
await this.scrcpyAdapter.disconnect();
|
|
1614
1641
|
this.scrcpyAdapter = null;
|
|
@@ -1648,7 +1675,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1648
1675
|
}
|
|
1649
1676
|
async longPress(x, y, duration = 2000) {
|
|
1650
1677
|
const adb = await this.getAdb();
|
|
1651
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1678
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1652
1679
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
1653
1680
|
}
|
|
1654
1681
|
async pullDown(startPoint, distance, duration = 800) {
|
|
@@ -1670,8 +1697,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1670
1697
|
}
|
|
1671
1698
|
async pullDrag(from, to, duration) {
|
|
1672
1699
|
const adb = await this.getAdb();
|
|
1673
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1674
|
-
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);
|
|
1675
1702
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1676
1703
|
}
|
|
1677
1704
|
async pullUp(startPoint, distance, duration = 600) {
|
|
@@ -1758,7 +1785,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1758
1785
|
device_define_property(this, "yadbPushed", false);
|
|
1759
1786
|
device_define_property(this, "devicePixelRatio", 1);
|
|
1760
1787
|
device_define_property(this, "devicePixelRatioInitialized", false);
|
|
1761
|
-
device_define_property(this, "scalingRatio", 1);
|
|
1762
1788
|
device_define_property(this, "adb", null);
|
|
1763
1789
|
device_define_property(this, "connectingAdb", null);
|
|
1764
1790
|
device_define_property(this, "destroyed", false);
|
|
@@ -1769,6 +1795,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1769
1795
|
device_define_property(this, "cachedPhysicalDisplayId", void 0);
|
|
1770
1796
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1771
1797
|
device_define_property(this, "appNameMapping", {});
|
|
1798
|
+
device_define_property(this, "scalingRatio", 1);
|
|
1799
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1772
1800
|
device_define_property(this, "interfaceType", 'android');
|
|
1773
1801
|
device_define_property(this, "uri", void 0);
|
|
1774
1802
|
device_define_property(this, "options", void 0);
|
|
@@ -1778,6 +1806,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1778
1806
|
this.customActions = options?.customActions;
|
|
1779
1807
|
}
|
|
1780
1808
|
}
|
|
1809
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1781
1810
|
const runAdbShellParamSchema = core_namespaceObject.z.object({
|
|
1782
1811
|
command: core_namespaceObject.z.string().describe('ADB shell command to execute')
|
|
1783
1812
|
});
|
|
@@ -1790,6 +1819,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1790
1819
|
description: 'Execute ADB shell command on Android device',
|
|
1791
1820
|
interfaceAlias: 'runAdbShell',
|
|
1792
1821
|
paramSchema: runAdbShellParamSchema,
|
|
1822
|
+
sample: {
|
|
1823
|
+
command: 'dumpsys window displays | grep -E "mCurrentFocus"'
|
|
1824
|
+
},
|
|
1793
1825
|
call: async (param)=>{
|
|
1794
1826
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
1795
1827
|
const adb = await device.getAdb();
|
|
@@ -1801,6 +1833,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1801
1833
|
description: 'Launch an Android app or URL',
|
|
1802
1834
|
interfaceAlias: 'launch',
|
|
1803
1835
|
paramSchema: launchParamSchema,
|
|
1836
|
+
sample: {
|
|
1837
|
+
uri: 'com.example.app'
|
|
1838
|
+
},
|
|
1804
1839
|
call: async (param)=>{
|
|
1805
1840
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
1806
1841
|
await device.launch(param.uri);
|
|
@@ -1953,7 +1988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1953
1988
|
constructor(toolsManager){
|
|
1954
1989
|
super({
|
|
1955
1990
|
name: '@midscene/android-mcp',
|
|
1956
|
-
version: "1.6.
|
|
1991
|
+
version: "1.6.8",
|
|
1957
1992
|
description: 'Control the Android device using natural language commands'
|
|
1958
1993
|
}, toolsManager);
|
|
1959
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>;
|