@aiscene/android 1.3.6 → 1.6.0-cache
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 +3 -7
- package/bin/.yadb-version +1 -0
- package/bin/midscene-android +2 -0
- package/bin/scrcpy-server +0 -0
- package/bin/scrcpy-server.version +1 -0
- package/bin/yadb +0 -0
- package/dist/es/cli.mjs +1872 -0
- package/dist/es/index.mjs +126 -69
- package/dist/es/mcp-server.mjs +71 -69
- package/dist/lib/cli.js +1892 -0
- package/dist/lib/index.js +131 -67
- package/dist/lib/mcp-server.js +71 -68
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/index.d.ts +204 -0
- package/package.json +12 -13
package/dist/es/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as __rspack_external__ffmpeg_installer_ffmpeg_acfac5f1 from "@ffmpeg-installer/ffmpeg";
|
|
2
1
|
import * as __rspack_external__midscene_shared_logger_b1dc2426 from "@midscene/shared/logger";
|
|
3
2
|
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
4
3
|
import * as __rspack_external_node_module_ab9f2194 from "node:module";
|
|
@@ -12,6 +11,7 @@ import { createImgBase64ByFormat, isValidPNGImageBuffer } from "@midscene/shared
|
|
|
12
11
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
13
12
|
import { ADB } from "appium-adb";
|
|
14
13
|
import { Agent } from "@midscene/core/agent";
|
|
14
|
+
import { BaseMidsceneTools } from "@midscene/shared/mcp";
|
|
15
15
|
var __webpack_modules__ = {
|
|
16
16
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
17
17
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -33,24 +33,26 @@ var __webpack_modules__ = {
|
|
|
33
33
|
return obj;
|
|
34
34
|
}
|
|
35
35
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
36
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
37
|
+
console: true
|
|
38
|
+
});
|
|
36
39
|
const NAL_TYPE_IDR = 5;
|
|
37
40
|
const NAL_TYPE_SPS = 7;
|
|
38
41
|
const NAL_TYPE_PPS = 8;
|
|
39
42
|
const NAL_TYPE_MASK = 0x1f;
|
|
40
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
41
|
-
0x00,
|
|
42
|
-
0x00,
|
|
43
|
-
0x00,
|
|
44
|
-
0x01
|
|
45
|
-
]);
|
|
46
43
|
const DEFAULT_MAX_SIZE = 0;
|
|
47
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
44
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
45
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
48
46
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
49
47
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
50
48
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
51
49
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
52
50
|
const MAX_SCAN_BYTES = 1000;
|
|
53
51
|
const CONNECTION_WAIT_MS = 1000;
|
|
52
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
53
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
54
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
55
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
54
56
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
55
57
|
enabled: false,
|
|
56
58
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -90,21 +92,21 @@ var __webpack_modules__ = {
|
|
|
90
92
|
try {
|
|
91
93
|
this.isConnecting = true;
|
|
92
94
|
debugScrcpy('Starting scrcpy connection...');
|
|
93
|
-
const { AdbScrcpyClient,
|
|
95
|
+
const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
|
|
94
96
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
95
|
-
const {
|
|
96
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
97
|
+
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
97
98
|
const serverBinPath = this.resolveServerBinPath();
|
|
98
99
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
99
|
-
const scrcpyOptions = new
|
|
100
|
+
const scrcpyOptions = new AdbScrcpyOptions3_3_3({
|
|
100
101
|
audio: false,
|
|
101
102
|
control: false,
|
|
102
103
|
maxSize: this.options.maxSize,
|
|
103
104
|
videoBitRate: this.options.videoBitRate,
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
maxFps: 10,
|
|
106
|
+
sendFrameMeta: true,
|
|
107
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
106
108
|
});
|
|
107
|
-
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath,
|
|
109
|
+
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
|
|
108
110
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
109
111
|
if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
|
|
110
112
|
this.videoStream = await videoStreamPromise;
|
|
@@ -132,7 +134,8 @@ var __webpack_modules__ = {
|
|
|
132
134
|
}
|
|
133
135
|
getFfmpegPath() {
|
|
134
136
|
try {
|
|
135
|
-
const
|
|
137
|
+
const dynamicRequire = (0, node_module__rspack_import_1.createRequire)(import.meta.url);
|
|
138
|
+
const ffmpegInstaller = dynamicRequire('@ffmpeg-installer/ffmpeg');
|
|
136
139
|
debugScrcpy(`Using ffmpeg from npm package: ${ffmpegInstaller.path}`);
|
|
137
140
|
return ffmpegInstaller.path;
|
|
138
141
|
} catch (error) {
|
|
@@ -147,22 +150,47 @@ var __webpack_modules__ = {
|
|
|
147
150
|
this.consumeFramesLoop(reader);
|
|
148
151
|
}
|
|
149
152
|
async consumeFramesLoop(reader) {
|
|
153
|
+
let readCount = 0;
|
|
154
|
+
let windowStart = Date.now();
|
|
155
|
+
let lastBusyWarn = 0;
|
|
156
|
+
let totalReads = 0;
|
|
150
157
|
try {
|
|
151
158
|
while(true){
|
|
152
159
|
const { done, value } = await reader.read();
|
|
153
160
|
if (done) break;
|
|
161
|
+
totalReads++;
|
|
162
|
+
readCount++;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const elapsed = now - windowStart;
|
|
165
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
166
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
167
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
168
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
169
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
170
|
+
lastBusyWarn = now;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
173
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
174
|
+
readCount = 0;
|
|
175
|
+
windowStart = Date.now();
|
|
176
|
+
}
|
|
154
177
|
this.processFrame(value);
|
|
155
178
|
}
|
|
156
179
|
} catch (error) {
|
|
157
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
180
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
158
181
|
await this.disconnect();
|
|
159
182
|
}
|
|
183
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
160
184
|
}
|
|
161
185
|
processFrame(packet) {
|
|
186
|
+
if ('configuration' === packet.type) {
|
|
187
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
188
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
162
191
|
const frameBuffer = Buffer.from(packet.data);
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
192
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
193
|
+
if (isKeyFrame && this.spsHeader) {
|
|
166
194
|
this.lastRawKeyframe = frameBuffer;
|
|
167
195
|
if (this.keyframeResolvers.length > 0) {
|
|
168
196
|
const combined = Buffer.concat([
|
|
@@ -173,23 +201,7 @@ var __webpack_modules__ = {
|
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
}
|
|
176
|
-
|
|
177
|
-
if (!this.h264SearchConfigFn) return;
|
|
178
|
-
try {
|
|
179
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
180
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
181
|
-
this.spsHeader = Buffer.concat([
|
|
182
|
-
START_CODE_4_BYTE,
|
|
183
|
-
Buffer.from(config.sequenceParameterSet),
|
|
184
|
-
START_CODE_4_BYTE,
|
|
185
|
-
Buffer.from(config.pictureParameterSet)
|
|
186
|
-
]);
|
|
187
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async getScreenshotPng() {
|
|
204
|
+
async getScreenshotJpeg() {
|
|
193
205
|
const perfStart = Date.now();
|
|
194
206
|
const t1 = Date.now();
|
|
195
207
|
await this.ensureConnected();
|
|
@@ -219,7 +231,7 @@ var __webpack_modules__ = {
|
|
|
219
231
|
this.resetIdleTimer();
|
|
220
232
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
221
233
|
const t4 = Date.now();
|
|
222
|
-
const result = await this.
|
|
234
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
223
235
|
const decodeTime = Date.now() - t4;
|
|
224
236
|
const totalTime = Date.now() - perfStart;
|
|
225
237
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -282,7 +294,7 @@ var __webpack_modules__ = {
|
|
|
282
294
|
return false;
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
|
-
async
|
|
297
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
286
298
|
const { spawn } = await import("node:child_process");
|
|
287
299
|
return new Promise((resolve, reject)=>{
|
|
288
300
|
const ffmpegArgs = [
|
|
@@ -295,7 +307,9 @@ var __webpack_modules__ = {
|
|
|
295
307
|
'-f',
|
|
296
308
|
'image2pipe',
|
|
297
309
|
'-vcodec',
|
|
298
|
-
'
|
|
310
|
+
'mjpeg',
|
|
311
|
+
'-q:v',
|
|
312
|
+
'5',
|
|
299
313
|
'-loglevel',
|
|
300
314
|
'error',
|
|
301
315
|
'pipe:1'
|
|
@@ -318,13 +332,13 @@ var __webpack_modules__ = {
|
|
|
318
332
|
});
|
|
319
333
|
ffmpeg.on('close', (code)=>{
|
|
320
334
|
if (0 === code && chunks.length > 0) {
|
|
321
|
-
const
|
|
322
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
323
|
-
resolve(
|
|
335
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
336
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
337
|
+
resolve(jpegBuffer);
|
|
324
338
|
} else {
|
|
325
339
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
326
340
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
327
|
-
reject(new Error(`H.264 to
|
|
341
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
328
342
|
}
|
|
329
343
|
});
|
|
330
344
|
ffmpeg.on('error', (error)=>{
|
|
@@ -356,7 +370,6 @@ var __webpack_modules__ = {
|
|
|
356
370
|
this.spsHeader = null;
|
|
357
371
|
this.lastRawKeyframe = null;
|
|
358
372
|
this.isInitialized = false;
|
|
359
|
-
this.h264SearchConfigFn = null;
|
|
360
373
|
this.keyframeResolvers = [];
|
|
361
374
|
if (reader) try {
|
|
362
375
|
reader.cancel();
|
|
@@ -384,20 +397,19 @@ var __webpack_modules__ = {
|
|
|
384
397
|
_define_property(this, "keyframeResolvers", []);
|
|
385
398
|
_define_property(this, "lastRawKeyframe", null);
|
|
386
399
|
_define_property(this, "videoResolution", null);
|
|
387
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
388
400
|
_define_property(this, "streamReader", null);
|
|
389
401
|
this.adb = adb;
|
|
402
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
403
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
404
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
390
405
|
this.options = {
|
|
391
406
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
392
|
-
videoBitRate:
|
|
407
|
+
videoBitRate: clampedBitRate,
|
|
393
408
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
394
409
|
};
|
|
395
410
|
}
|
|
396
411
|
}
|
|
397
412
|
},
|
|
398
|
-
"@ffmpeg-installer/ffmpeg" (module) {
|
|
399
|
-
module.exports = __rspack_external__ffmpeg_installer_ffmpeg_acfac5f1;
|
|
400
|
-
},
|
|
401
413
|
"@midscene/shared/logger" (module) {
|
|
402
414
|
module.exports = __rspack_external__midscene_shared_logger_b1dc2426;
|
|
403
415
|
},
|
|
@@ -465,18 +477,13 @@ class ScrcpyDeviceAdapter {
|
|
|
465
477
|
resolveConfig(deviceInfo) {
|
|
466
478
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
467
479
|
const config = this.scrcpyConfig;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
471
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
472
|
-
maxSize = Math.round(physicalMax * scale);
|
|
473
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
474
|
-
}
|
|
480
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
481
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
475
482
|
this.resolvedConfig = {
|
|
476
483
|
enabled: this.isEnabled(),
|
|
477
484
|
maxSize,
|
|
478
485
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
479
|
-
videoBitRate
|
|
486
|
+
videoBitRate
|
|
480
487
|
};
|
|
481
488
|
return this.resolvedConfig;
|
|
482
489
|
}
|
|
@@ -511,8 +518,8 @@ class ScrcpyDeviceAdapter {
|
|
|
511
518
|
}
|
|
512
519
|
async screenshotBase64(deviceInfo) {
|
|
513
520
|
const manager = await this.ensureManager(deviceInfo);
|
|
514
|
-
const screenshotBuffer = await manager.
|
|
515
|
-
return createImgBase64ByFormat('
|
|
521
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
522
|
+
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
516
523
|
}
|
|
517
524
|
getResolution() {
|
|
518
525
|
return this.manager?.getResolution() ?? null;
|
|
@@ -523,8 +530,7 @@ class ScrcpyDeviceAdapter {
|
|
|
523
530
|
debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
|
|
524
531
|
return {
|
|
525
532
|
width: resolution.width,
|
|
526
|
-
height: resolution.height
|
|
527
|
-
dpr: deviceInfo.dpr
|
|
533
|
+
height: resolution.height
|
|
528
534
|
};
|
|
529
535
|
}
|
|
530
536
|
getScalingRatio(physicalWidth) {
|
|
@@ -543,16 +549,14 @@ class ScrcpyDeviceAdapter {
|
|
|
543
549
|
}
|
|
544
550
|
this.resolvedConfig = null;
|
|
545
551
|
}
|
|
546
|
-
constructor(deviceId, scrcpyConfig
|
|
552
|
+
constructor(deviceId, scrcpyConfig){
|
|
547
553
|
_define_property(this, "deviceId", void 0);
|
|
548
554
|
_define_property(this, "scrcpyConfig", void 0);
|
|
549
|
-
_define_property(this, "screenshotResizeScale", void 0);
|
|
550
555
|
_define_property(this, "manager", void 0);
|
|
551
556
|
_define_property(this, "resolvedConfig", void 0);
|
|
552
557
|
_define_property(this, "initFailed", void 0);
|
|
553
558
|
this.deviceId = deviceId;
|
|
554
559
|
this.scrcpyConfig = scrcpyConfig;
|
|
555
|
-
this.screenshotResizeScale = screenshotResizeScale;
|
|
556
560
|
this.manager = null;
|
|
557
561
|
this.resolvedConfig = null;
|
|
558
562
|
this.initFailed = false;
|
|
@@ -779,7 +783,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
779
783
|
});
|
|
780
784
|
}
|
|
781
785
|
getScrcpyAdapter() {
|
|
782
|
-
if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig
|
|
786
|
+
if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
|
|
783
787
|
return this.scrcpyAdapter;
|
|
784
788
|
}
|
|
785
789
|
async getDevicePhysicalInfo() {
|
|
@@ -1025,8 +1029,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1025
1029
|
const logicalHeight = Math.round(height * scale);
|
|
1026
1030
|
return {
|
|
1027
1031
|
width: logicalWidth,
|
|
1028
|
-
height: logicalHeight
|
|
1029
|
-
dpr: this.devicePixelRatio
|
|
1032
|
+
height: logicalHeight
|
|
1030
1033
|
};
|
|
1031
1034
|
}
|
|
1032
1035
|
async cacheFeatureForPoint(center, options) {
|
|
@@ -1804,4 +1807,58 @@ async function agentFromAdbDevice(deviceId, opts) {
|
|
|
1804
1807
|
await device.connect();
|
|
1805
1808
|
return new AndroidAgent(device, opts);
|
|
1806
1809
|
}
|
|
1807
|
-
|
|
1810
|
+
const debug = (0, logger_.getDebug)('mcp:android-tools');
|
|
1811
|
+
class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
1812
|
+
createTemporaryDevice() {
|
|
1813
|
+
return new AndroidDevice('temp-for-action-space', {});
|
|
1814
|
+
}
|
|
1815
|
+
async ensureAgent(deviceId) {
|
|
1816
|
+
if (this.agent && deviceId) {
|
|
1817
|
+
try {
|
|
1818
|
+
await this.agent.destroy?.();
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
debug('Failed to destroy agent during cleanup:', error);
|
|
1821
|
+
}
|
|
1822
|
+
this.agent = void 0;
|
|
1823
|
+
}
|
|
1824
|
+
if (this.agent) return this.agent;
|
|
1825
|
+
debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
|
|
1826
|
+
const agent = await agentFromAdbDevice(deviceId, {
|
|
1827
|
+
autoDismissKeyboard: false
|
|
1828
|
+
});
|
|
1829
|
+
this.agent = agent;
|
|
1830
|
+
return agent;
|
|
1831
|
+
}
|
|
1832
|
+
preparePlatformTools() {
|
|
1833
|
+
return [
|
|
1834
|
+
{
|
|
1835
|
+
name: 'android_connect',
|
|
1836
|
+
description: 'Connect to Android device via ADB. If deviceId not provided, uses the first available device.',
|
|
1837
|
+
schema: {
|
|
1838
|
+
deviceId: z.string().optional().describe('Android device ID (from adb devices)')
|
|
1839
|
+
},
|
|
1840
|
+
handler: async ({ deviceId })=>{
|
|
1841
|
+
const agent = await this.ensureAgent(deviceId);
|
|
1842
|
+
const screenshot = await agent.page.screenshotBase64();
|
|
1843
|
+
return {
|
|
1844
|
+
content: [
|
|
1845
|
+
{
|
|
1846
|
+
type: 'text',
|
|
1847
|
+
text: `Connected to Android device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}`
|
|
1848
|
+
},
|
|
1849
|
+
...this.buildScreenshotContent(screenshot)
|
|
1850
|
+
],
|
|
1851
|
+
isError: false
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
name: 'android_disconnect',
|
|
1857
|
+
description: 'Disconnect from current Android device and release ADB resources',
|
|
1858
|
+
schema: {},
|
|
1859
|
+
handler: this.createDisconnectHandler('Android device')
|
|
1860
|
+
}
|
|
1861
|
+
];
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
export { AndroidAgent, AndroidDevice, AndroidMidsceneTools, ScrcpyDeviceAdapter, agentFromAdbDevice, getConnectedDevices, overrideAIConfig };
|