shellx-ai 1.0.12 → 1.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +666 -0
  3. package/dist/automation/element-finder.d.ts +189 -0
  4. package/dist/automation/element-finder.js +322 -0
  5. package/dist/automation/element-finder.js.map +1 -0
  6. package/dist/automation/ui-action-handler.d.ts +330 -0
  7. package/dist/automation/ui-action-handler.js +873 -0
  8. package/dist/automation/ui-action-handler.js.map +1 -0
  9. package/dist/cbor-compat.d.ts +27 -0
  10. package/dist/cbor-compat.js +111 -0
  11. package/dist/cbor-compat.js.map +1 -0
  12. package/dist/domain-manager.d.ts +80 -0
  13. package/dist/domain-manager.js +161 -0
  14. package/dist/domain-manager.js.map +1 -0
  15. package/dist/error-handler.d.ts +87 -0
  16. package/dist/error-handler.js +151 -0
  17. package/dist/error-handler.js.map +1 -0
  18. package/dist/errors.d.ts +114 -0
  19. package/dist/errors.js +139 -0
  20. package/dist/errors.js.map +1 -0
  21. package/dist/index.d.ts +163 -54
  22. package/dist/index.js +678 -481
  23. package/dist/index.js.map +1 -0
  24. package/dist/logger.d.ts +81 -0
  25. package/dist/logger.js +128 -0
  26. package/dist/logger.js.map +1 -0
  27. package/dist/protocol.d.ts +147 -31
  28. package/dist/protocol.js +2 -2
  29. package/dist/protocol.js.map +1 -0
  30. package/dist/shell/output-buffer.d.ts +152 -0
  31. package/dist/shell/output-buffer.js +176 -0
  32. package/dist/shell/output-buffer.js.map +1 -0
  33. package/dist/shell/shell-command-executor.d.ts +182 -0
  34. package/dist/shell/shell-command-executor.js +404 -0
  35. package/dist/shell/shell-command-executor.js.map +1 -0
  36. package/dist/shellx.d.ts +681 -178
  37. package/dist/shellx.js +762 -1159
  38. package/dist/shellx.js.map +1 -0
  39. package/dist/types.d.ts +132 -57
  40. package/dist/types.js +4 -4
  41. package/dist/types.js.map +1 -0
  42. package/dist/utils/retry-helper.d.ts +73 -0
  43. package/dist/utils/retry-helper.js +95 -0
  44. package/dist/utils/retry-helper.js.map +1 -0
  45. package/dist/utils.d.ts +3 -3
  46. package/dist/utils.js +20 -23
  47. package/dist/utils.js.map +1 -0
  48. package/package.json +95 -62
@@ -0,0 +1,873 @@
1
+ /**
2
+ * UIActionHandler - A module for handling UI automation actions
3
+ *
4
+ * This module provides functionality for executing various UI actions such as
5
+ * clicking, inputting text, swiping, pressing keys, and more.
6
+ */
7
+ import { RetryHelper } from "../utils/retry-helper.js";
8
+ import { extractErrorMessage, createErrorResult, createSuccessResult, validateOneOfRequired, validateRequired, } from "../error-handler.js";
9
+ import { ValidationError } from "../errors.js";
10
+ import { ElementFinder } from "./element-finder.js";
11
+ import { createLogger } from "../logger.js";
12
+ /**
13
+ * UIActionHandler class handles UI automation operations
14
+ *
15
+ * This class provides methods to:
16
+ * - Execute UI actions (click, input, swipe, key press)
17
+ * - Wait for elements
18
+ * - Take screenshots
19
+ * - Execute action sequences
20
+ * - Navigate through app UI
21
+ */
22
+ export class UIActionHandler {
23
+ client;
24
+ elementFinder;
25
+ logger = createLogger("UIActionHandler");
26
+ /**
27
+ * Creates a UIActionHandler instance
28
+ *
29
+ * @param client - The ConnectionClient instance for UI operations
30
+ */
31
+ constructor(client) {
32
+ this.client = client;
33
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
34
+ this.elementFinder = new ElementFinder(client);
35
+ }
36
+ /**
37
+ * Validate that client is initialized
38
+ *
39
+ * @returns true if client is valid, false otherwise
40
+ */
41
+ validateClient(methodName) {
42
+ if (!this.client) {
43
+ this.logger.error(`❌ Cannot execute ${methodName}: Client is not initialized`);
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ /**
49
+ * Convert simplified selector to protocol selector
50
+ *
51
+ * @param selector - Simplified selector object
52
+ * @returns Protocol ElementSelector object
53
+ */
54
+ convertSelector(selector) {
55
+ return {
56
+ elementId: selector.elementId,
57
+ resourceId: selector.resourceId,
58
+ className: selector.class,
59
+ text: selector.text,
60
+ textContains: undefined,
61
+ visible: selector.visible,
62
+ clickable: selector.clickable,
63
+ };
64
+ }
65
+ /**
66
+ * Execute action and verify it succeeded
67
+ *
68
+ * @param action - The action sequence to execute
69
+ */
70
+ async executeAndVerifyAction(action) {
71
+ await this.client.executeAction(action);
72
+ }
73
+ /**
74
+ * Execute operation with retry and unified error handling
75
+ *
76
+ * @param operation - Async operation to execute
77
+ * @param startTime - Operation start time
78
+ * @param options - Retry and error handling options
79
+ * @returns Promise resolving to operation result
80
+ */
81
+ async executeWithRetry(operation, startTime, options = {}) {
82
+ const { retry = 3, delay = 500, logPrefix, defaultErrorMessage } = options;
83
+ for (let attempt = 1; attempt <= retry; attempt++) {
84
+ try {
85
+ const data = await operation();
86
+ return createSuccessResult(data, startTime);
87
+ }
88
+ catch (error) {
89
+ const isLastAttempt = attempt === retry;
90
+ const errorMessage = extractErrorMessage(error);
91
+ if (!isLastAttempt) {
92
+ if (logPrefix) {
93
+ this.logger.warn(`${logPrefix} - Attempt ${attempt}/${retry} failed: ${errorMessage}. Retrying in ${delay}ms...`);
94
+ }
95
+ await new Promise((resolve) => setTimeout(resolve, delay));
96
+ }
97
+ else {
98
+ if (logPrefix) {
99
+ this.logger.error(`${logPrefix} - Failed after ${retry} attempts: ${errorMessage}`);
100
+ }
101
+ return createErrorResult(error, startTime, {
102
+ logPrefix,
103
+ defaultErrorMessage,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ // This should never be reached
109
+ return createErrorResult(new Error("Operation failed unexpectedly"), startTime, {
110
+ logPrefix,
111
+ defaultErrorMessage,
112
+ });
113
+ }
114
+ /**
115
+ * Execute a click action
116
+ *
117
+ * @param clickData - Click configuration
118
+ * @returns Promise resolving to ClickResult
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const result = await actionHandler.click({
123
+ * elementId: 'element123',
124
+ * clickType: 'single',
125
+ * wait: 3000
126
+ * });
127
+ * ```
128
+ */
129
+ async click(clickData) {
130
+ const startTime = Date.now();
131
+ // Validate client
132
+ if (!this.validateClient("click")) {
133
+ return createErrorResult(new Error("Client not initialized"), startTime, {
134
+ logPrefix: "❌ [UIActionHandler] click",
135
+ });
136
+ }
137
+ // Validate target
138
+ validateOneOfRequired(clickData, ["elementId", "resourceId", "x", "y", "text", "class"]);
139
+ return await this.executeWithRetry(async () => {
140
+ // Ensure visibility for click operations
141
+ clickData.visible = true;
142
+ let target;
143
+ if (clickData.elementId) {
144
+ target = { type: "elementId", value: clickData.elementId };
145
+ }
146
+ else if (clickData.resourceId) {
147
+ target = { type: "resourceId", value: clickData.resourceId };
148
+ }
149
+ else if (clickData.x !== undefined && clickData.y !== undefined) {
150
+ target = { type: "coordinate", value: { x: clickData.x, y: clickData.y } };
151
+ }
152
+ else if (clickData.text || clickData.class) {
153
+ const element = await this.elementFinder.findElement(this.convertSelector(clickData), {
154
+ maxRetries: 1,
155
+ });
156
+ validateRequired(element, "Target element");
157
+ target = { type: "elementId", value: element.elementId };
158
+ }
159
+ else {
160
+ throw new ValidationError("Must specify target: elementId, resourceId, coordinates, or selector");
161
+ }
162
+ const action = {
163
+ title: `Click action: ${clickData.clickType || "single"}`,
164
+ actions: [
165
+ {
166
+ type: "click",
167
+ target,
168
+ options: {
169
+ clickType: clickData.clickType || "single",
170
+ waitAfterMs: clickData.wait || 3000,
171
+ },
172
+ },
173
+ ],
174
+ };
175
+ await this.executeAndVerifyAction(action);
176
+ return {
177
+ elementId: clickData.elementId,
178
+ resourceId: clickData.resourceId,
179
+ text: clickData.text,
180
+ class: clickData.class,
181
+ x: clickData.x,
182
+ y: clickData.y,
183
+ clickType: clickData.clickType,
184
+ };
185
+ }, startTime, { retry: clickData.retry, delay: 500, logPrefix: "🔨 [UIActionHandler] click" });
186
+ }
187
+ /**
188
+ * Execute an input action
189
+ *
190
+ * @param inputData - Input configuration
191
+ * @returns Promise resolving to ActionResult
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const result = await actionHandler.input({
196
+ * elementId: 'element123',
197
+ * text: 'Hello World',
198
+ * clear: true,
199
+ * hideKeyboard: false,
200
+ * wait: 500
201
+ * });
202
+ * ```
203
+ */
204
+ async input(inputData) {
205
+ const startTime = Date.now();
206
+ // Validate client
207
+ if (!this.validateClient("input")) {
208
+ return createErrorResult(new Error("Client not initialized"), startTime, {
209
+ logPrefix: "❌ [UIActionHandler] input",
210
+ });
211
+ }
212
+ // Validate target
213
+ validateOneOfRequired(inputData, ["elementId", "resourceId"]);
214
+ return await this.executeWithRetry(async () => {
215
+ let target;
216
+ if (inputData.elementId) {
217
+ target = { type: "elementId", value: inputData.elementId };
218
+ }
219
+ else if (inputData.resourceId) {
220
+ target = { type: "resourceId", value: inputData.resourceId };
221
+ }
222
+ else {
223
+ throw new ValidationError("Must specify target: elementId or resourceId");
224
+ }
225
+ const action = {
226
+ title: `Input text: ${inputData.text}`,
227
+ actions: [
228
+ {
229
+ type: "input",
230
+ text: inputData.text,
231
+ target,
232
+ options: {
233
+ replaceExisting: inputData.clear ?? true,
234
+ hideKeyboardAfter: inputData.hideKeyboard ?? false,
235
+ waitAfterMs: inputData.wait || 500,
236
+ },
237
+ },
238
+ ],
239
+ };
240
+ await this.executeAndVerifyAction(action);
241
+ return {
242
+ text: inputData.text,
243
+ elementId: inputData.elementId,
244
+ resourceId: inputData.resourceId,
245
+ cleared: inputData.clear,
246
+ };
247
+ }, startTime, { retry: inputData.retry, delay: 500, logPrefix: "🔨 [UIActionHandler] input" });
248
+ }
249
+ /**
250
+ * Execute a swipe action
251
+ *
252
+ * @param swipeData - Swipe configuration
253
+ * @returns Promise resolving to ActionResult
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * const result = await actionHandler.swipe({
258
+ * fromX: 500,
259
+ * fromY: 1000,
260
+ * toX: 500,
261
+ * toY: 500,
262
+ * duration: 800,
263
+ * wait: 500
264
+ * });
265
+ * ```
266
+ */
267
+ async swipe(swipeData) {
268
+ const startTime = Date.now();
269
+ // Validate client
270
+ if (!this.validateClient("swipe")) {
271
+ return createErrorResult(new Error("Client not initialized"), startTime, {
272
+ logPrefix: "❌ [UIActionHandler] swipe",
273
+ });
274
+ }
275
+ // Validate coordinates
276
+ validateRequired(swipeData.fromX, "fromX");
277
+ validateRequired(swipeData.fromY, "fromY");
278
+ validateRequired(swipeData.toX, "toX");
279
+ validateRequired(swipeData.toY, "toY");
280
+ return await this.executeWithRetry(async () => {
281
+ const from = { x: swipeData.fromX, y: swipeData.fromY };
282
+ const to = { x: swipeData.toX, y: swipeData.toY };
283
+ const action = {
284
+ title: `Swipe action: ${from.x},${from.y} → ${to.x},${to.y}`,
285
+ actions: [
286
+ {
287
+ type: "swipe",
288
+ from,
289
+ to,
290
+ options: {
291
+ durationMs: swipeData.duration || 800,
292
+ waitAfterMs: swipeData.wait || 500,
293
+ },
294
+ },
295
+ ],
296
+ };
297
+ await this.executeAndVerifyAction(action);
298
+ return {
299
+ fromX: swipeData.fromX,
300
+ fromY: swipeData.fromY,
301
+ toX: swipeData.toX,
302
+ toY: swipeData.toY,
303
+ };
304
+ }, startTime, { retry: swipeData.retry, delay: 500, logPrefix: "🔨 [UIActionHandler] swipe" });
305
+ }
306
+ /**
307
+ * Execute a key press action
308
+ *
309
+ * @param keyData - Key configuration
310
+ * @returns Promise resolving to ActionResult
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const result = await actionHandler.pressKey({
315
+ * key: 'KEYCODE_BACK',
316
+ * longPress: false,
317
+ * wait: 500
318
+ * });
319
+ * ```
320
+ */
321
+ async pressKey(keyData) {
322
+ const startTime = Date.now();
323
+ // Validate client
324
+ if (!this.validateClient("pressKey")) {
325
+ return createErrorResult(new Error("Client not initialized"), startTime, {
326
+ logPrefix: "❌ [UIActionHandler] pressKey",
327
+ });
328
+ }
329
+ // Validate key
330
+ validateRequired(keyData.key, "key");
331
+ return await this.executeWithRetry(async () => {
332
+ // Automatically add KEYCODE_ prefix if not present
333
+ const keyCode = keyData.key.startsWith("KEYCODE_")
334
+ ? keyData.key
335
+ : `KEYCODE_${keyData.key.toUpperCase()}`;
336
+ const action = {
337
+ title: `Key press: ${keyCode}${keyData.longPress ? " (long press)" : ""}`,
338
+ actions: [
339
+ {
340
+ type: "key",
341
+ keyCode: keyCode,
342
+ options: {
343
+ longPress: keyData.longPress || false,
344
+ },
345
+ },
346
+ ],
347
+ };
348
+ await this.executeAndVerifyAction(action);
349
+ if (keyData.wait) {
350
+ await new Promise((resolve) => setTimeout(resolve, keyData.wait));
351
+ }
352
+ return {
353
+ key: keyData.key,
354
+ longPress: keyData.longPress,
355
+ };
356
+ }, startTime, { retry: keyData.retry, delay: 500, logPrefix: "🔨 [UIActionHandler] pressKey" });
357
+ }
358
+ /**
359
+ * Wait for an element condition
360
+ *
361
+ * @param waitData - Wait configuration
362
+ * @returns Promise resolving to ActionResult
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * // Wait for element to appear
367
+ * const result = await actionHandler.wait({
368
+ * text: 'Submit',
369
+ * condition: 'visible',
370
+ * timeout: 10000
371
+ * });
372
+ *
373
+ * // Wait for element to disappear
374
+ * const result = await actionHandler.wait({
375
+ * text: 'Loading',
376
+ * condition: 'gone',
377
+ * timeout: 10000
378
+ * });
379
+ * ```
380
+ */
381
+ async wait(waitData) {
382
+ const startTime = Date.now();
383
+ // Validate client
384
+ if (!this.validateClient("wait")) {
385
+ return createErrorResult(new Error("Client not initialized"), startTime, {
386
+ logPrefix: "❌ [UIActionHandler] wait",
387
+ });
388
+ }
389
+ // Validate target
390
+ validateOneOfRequired(waitData, ["elementId", "resourceId", "text", "class"]);
391
+ return await this.executeWithRetry(async () => {
392
+ const selector = this.convertSelector(waitData);
393
+ const timeout = waitData.timeout || 10000;
394
+ const interval = 500;
395
+ const maxAttempts = Math.floor(timeout / interval);
396
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
397
+ try {
398
+ const findResult = (await this.client.findElement(selector, {
399
+ timeout: interval,
400
+ maxResults: 1,
401
+ visibleOnly: waitData.condition === "visible",
402
+ clickableOnly: waitData.condition === "clickable",
403
+ }));
404
+ if (!findResult || findResult.success === false) {
405
+ await new Promise((resolve) => setTimeout(resolve, interval));
406
+ continue;
407
+ }
408
+ if (findResult.elements && findResult.elements.length > 0) {
409
+ if (waitData.condition === "gone") {
410
+ await new Promise((resolve) => setTimeout(resolve, interval));
411
+ continue;
412
+ }
413
+ else {
414
+ return {
415
+ elementId: waitData.elementId,
416
+ resourceId: waitData.resourceId,
417
+ text: waitData.text,
418
+ class: waitData.class,
419
+ condition: waitData.condition,
420
+ found: true,
421
+ };
422
+ }
423
+ }
424
+ else if (waitData.condition === "gone") {
425
+ return {
426
+ elementId: waitData.elementId,
427
+ resourceId: waitData.resourceId,
428
+ text: waitData.text,
429
+ class: waitData.class,
430
+ condition: waitData.condition,
431
+ found: false,
432
+ };
433
+ }
434
+ }
435
+ catch {
436
+ // Continue waiting
437
+ }
438
+ await new Promise((resolve) => setTimeout(resolve, interval));
439
+ }
440
+ throw new ValidationError(`Wait timeout: ${waitData.condition || "visible"}`);
441
+ }, startTime, { retry: waitData.retry, delay: 500, logPrefix: "🔨 [UIActionHandler] wait" });
442
+ }
443
+ /**
444
+ * Execute a clipboard action
445
+ *
446
+ * @param clipboardData - Clipboard configuration
447
+ * @returns Promise resolving to ActionResult
448
+ *
449
+ * @example
450
+ * ```typescript
451
+ * // Get clipboard content
452
+ * const result = await actionHandler.clipboard({ get: true });
453
+ *
454
+ * // Set clipboard content
455
+ * const result = await actionHandler.clipboard({
456
+ * text: 'Hello',
457
+ * paste: false
458
+ * });
459
+ *
460
+ * // Paste clipboard content
461
+ * const result = await actionHandler.clipboard({ paste: true });
462
+ * ```
463
+ */
464
+ async clipboard(clipboardData) {
465
+ const startTime = Date.now();
466
+ const result = await RetryHelper.execute(async () => {
467
+ try {
468
+ if (clipboardData.get) {
469
+ const { promise } = await this.client.sendMessageWithTaskId({
470
+ action: {
471
+ action: {
472
+ type: "clipboard",
473
+ get: true,
474
+ },
475
+ },
476
+ }, "clipboard");
477
+ const response = await promise;
478
+ if (response) {
479
+ return {
480
+ success: response.success ?? true,
481
+ data: {
482
+ text: response.text,
483
+ },
484
+ error: response.error,
485
+ duration: Date.now() - startTime,
486
+ timestamp: startTime,
487
+ };
488
+ }
489
+ else {
490
+ throw new Error("Failed to get clipboard content");
491
+ }
492
+ }
493
+ // Write or paste operation
494
+ if (!clipboardData.paste && (!clipboardData.text || clipboardData.text.length === 0)) {
495
+ throw new Error("Must provide text when writing to clipboard");
496
+ }
497
+ await this.client.sendMessageWithTaskId({
498
+ action: {
499
+ action: {
500
+ type: "clipboard",
501
+ text: clipboardData.text,
502
+ paste: clipboardData.paste ?? false,
503
+ },
504
+ },
505
+ }, "action");
506
+ return {
507
+ success: true,
508
+ data: {
509
+ text: clipboardData.text,
510
+ paste: clipboardData.paste ?? false,
511
+ },
512
+ duration: Date.now() - startTime,
513
+ timestamp: startTime,
514
+ };
515
+ }
516
+ catch (error) {
517
+ throw new Error(`Clipboard action failed: ${error instanceof Error ? error.message : String(error)}`);
518
+ }
519
+ }, { retry: clipboardData.retry, delay: 500 });
520
+ if (!result) {
521
+ return {
522
+ success: false,
523
+ error: "Clipboard action failed",
524
+ duration: Date.now() - startTime,
525
+ timestamp: startTime,
526
+ };
527
+ }
528
+ return result;
529
+ }
530
+ /**
531
+ * Take a screenshot
532
+ *
533
+ * @param screenshotData - Screenshot configuration
534
+ * @returns Promise resolving to ActionResult
535
+ *
536
+ * @example
537
+ * ```typescript
538
+ * const result = await actionHandler.takeScreenshot({
539
+ * format: 'png',
540
+ * quality: 100,
541
+ * scale: 1,
542
+ * saveToFile: true
543
+ * });
544
+ * ```
545
+ */
546
+ async takeScreenshot(screenshotData = {}) {
547
+ const startTime = Date.now();
548
+ const result = await RetryHelper.execute(async () => {
549
+ try {
550
+ const options = {
551
+ format: screenshotData.format || "png",
552
+ quality: screenshotData.quality || 100,
553
+ scale: screenshotData.scale || 1,
554
+ };
555
+ if (screenshotData.saveToFile !== undefined) {
556
+ options.saveToFile = screenshotData.saveToFile;
557
+ }
558
+ if (screenshotData.regionX !== undefined &&
559
+ screenshotData.regionY !== undefined &&
560
+ screenshotData.regionWidth !== undefined &&
561
+ screenshotData.regionHeight !== undefined) {
562
+ options.region = {
563
+ left: screenshotData.regionX,
564
+ top: screenshotData.regionY,
565
+ width: screenshotData.regionWidth,
566
+ height: screenshotData.regionHeight,
567
+ };
568
+ }
569
+ const screenshot = await this.client.screenShot(options);
570
+ if (!screenshot || screenshot.success === false) {
571
+ throw new Error(screenshot?.error || "Screenshot failed");
572
+ }
573
+ // Return ScreenshotResult format (flat structure, not nested)
574
+ return {
575
+ success: true,
576
+ imageData: screenshot.imageData,
577
+ imagePath: screenshot.imagePath,
578
+ format: screenshot.format,
579
+ width: screenshot.dimensions?.width,
580
+ height: screenshot.dimensions?.height,
581
+ duration: Date.now() - startTime,
582
+ timestamp: screenshot.timestamp || startTime,
583
+ };
584
+ }
585
+ catch (error) {
586
+ throw new Error(`Screenshot action failed: ${error instanceof Error ? error.message : String(error)}`);
587
+ }
588
+ }, { retry: screenshotData.retry, delay: 500 });
589
+ if (!result) {
590
+ return {
591
+ success: false,
592
+ error: "Screenshot action failed",
593
+ duration: Date.now() - startTime,
594
+ timestamp: startTime,
595
+ };
596
+ }
597
+ return result;
598
+ }
599
+ /**
600
+ * Get application info
601
+ *
602
+ * @param appInfoData - App info configuration
603
+ * @returns Promise resolving to ActionResult
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * const result = await actionHandler.getAppInfo({
608
+ * package: 'com.example.app',
609
+ * retry: 3
610
+ * });
611
+ * ```
612
+ */
613
+ async getAppInfo(appInfoData) {
614
+ const startTime = Date.now();
615
+ const result = await RetryHelper.execute(async () => {
616
+ try {
617
+ const action = {
618
+ title: `Get app info: ${appInfoData.package}`,
619
+ actions: [
620
+ {
621
+ type: "get_app_info",
622
+ packageName: appInfoData.package,
623
+ },
624
+ ],
625
+ };
626
+ await this.client.executeAction(action);
627
+ return {
628
+ success: true,
629
+ package: appInfoData.package,
630
+ duration: Date.now() - startTime,
631
+ timestamp: startTime,
632
+ };
633
+ }
634
+ catch (error) {
635
+ throw new Error(`Get app info failed: ${error instanceof Error ? error.message : String(error)}`);
636
+ }
637
+ }, { retry: appInfoData.retry, delay: 500 });
638
+ if (!result) {
639
+ return {
640
+ success: false,
641
+ error: "Get app info failed",
642
+ duration: Date.now() - startTime,
643
+ timestamp: startTime,
644
+ package: appInfoData.package,
645
+ };
646
+ }
647
+ return result;
648
+ }
649
+ /**
650
+ * Get screen information
651
+ *
652
+ * @returns Promise resolving to ScreenInfoResponse
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * const screenInfo = await actionHandler.getScreenInfo();
657
+ * console.log(`Screen size: ${screenInfo.width}x${screenInfo.height}`);
658
+ * ```
659
+ */
660
+ async getScreenInfo() {
661
+ const result = await RetryHelper.execute(async () => {
662
+ try {
663
+ const info = await this.client.getScreenInfo();
664
+ if (!info || info.success === false) {
665
+ throw new Error(info?.error || "Failed to get screen info");
666
+ }
667
+ return info;
668
+ }
669
+ catch (error) {
670
+ throw new Error(`Get screen info failed: ${error instanceof Error ? error.message : String(error)}`);
671
+ }
672
+ }, { retry: 3, delay: 500 });
673
+ if (!result) {
674
+ return {
675
+ displayId: 0,
676
+ width: 0,
677
+ height: 0,
678
+ density: 0,
679
+ name: "",
680
+ visible: false,
681
+ foregroundPackageName: "",
682
+ foregroundActivityName: "",
683
+ screenOn: false,
684
+ screenUnlocked: false,
685
+ accurateForegroundActivity: "",
686
+ accurateForegroundApp: "",
687
+ bashStatus: 0,
688
+ };
689
+ }
690
+ return result;
691
+ }
692
+ /**
693
+ * Get list of installed applications
694
+ *
695
+ * @param options - Options for filtering app list
696
+ * @returns Promise resolving to app list response
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * const result = await actionHandler.getAppList({
701
+ * includeSystemApps: false,
702
+ * includeDisabledApps: false
703
+ * });
704
+ * console.log(`Found ${result.userAppCount} user apps`);
705
+ * ```
706
+ */
707
+ async getAppList(options) {
708
+ const startTime = Date.now();
709
+ try {
710
+ const result = await RetryHelper.execute(async () => {
711
+ try {
712
+ const { promise } = await this.client.sendMessageWithTaskId({ appList: options || {} }, "appList", { timeout: 10000 });
713
+ const response = await promise;
714
+ // 检查响应是否有效
715
+ if (!response) {
716
+ throw new Error("Empty response from server");
717
+ }
718
+ // 类型断言:服务器返回的 appList 数据
719
+ const appListData = response;
720
+ // 验证必需字段
721
+ if (!appListData.apps || !Array.isArray(appListData.apps)) {
722
+ throw new Error("Invalid app list format: missing or invalid apps array");
723
+ }
724
+ // ✅ 返回包含 success 字段的完整响应
725
+ return {
726
+ apps: appListData.apps,
727
+ totalCount: appListData.totalCount || appListData.apps.length,
728
+ systemAppCount: appListData.systemAppCount || 0,
729
+ userAppCount: appListData.userAppCount || appListData.apps.length,
730
+ timestamp: appListData.timestamp || startTime,
731
+ success: true, // ✅ 添加 success 字段
732
+ };
733
+ }
734
+ catch (error) {
735
+ throw new Error(`Get app list failed: ${error instanceof Error ? error.message : String(error)}`);
736
+ }
737
+ }, { retry: 2, delay: 500 });
738
+ if (!result) {
739
+ return {
740
+ apps: [],
741
+ totalCount: 0,
742
+ systemAppCount: 0,
743
+ userAppCount: 0,
744
+ timestamp: startTime,
745
+ success: false,
746
+ error: "Get app list failed",
747
+ };
748
+ }
749
+ return result;
750
+ }
751
+ catch (error) {
752
+ // 捕获所有异常,返回统一的错误响应
753
+ return {
754
+ apps: [],
755
+ totalCount: 0,
756
+ systemAppCount: 0,
757
+ userAppCount: 0,
758
+ timestamp: startTime,
759
+ success: false,
760
+ error: error instanceof Error ? error.message : String(error),
761
+ };
762
+ }
763
+ }
764
+ /**
765
+ * Navigate through app using a series of text-based clicks
766
+ *
767
+ * @param textPath - Array of text strings to click in sequence
768
+ * @returns Promise resolving to boolean indicating success
769
+ *
770
+ * @example
771
+ * ```typescript
772
+ * const success = await actionHandler.navigateByPath([
773
+ * 'Settings',
774
+ * 'Accounts',
775
+ * 'Add Account'
776
+ * ]);
777
+ * ```
778
+ */
779
+ async navigateByPath(textPath) {
780
+ try {
781
+ this.logger.info("Starting navigation path:", textPath.join(" → "));
782
+ for (const [index, text] of textPath.entries()) {
783
+ this.logger.info(`Navigation step ${index + 1}/${textPath.length}: Click "${text}"`);
784
+ await this.clickByText(text);
785
+ // Wait between clicks
786
+ await new Promise((resolve) => setTimeout(resolve, 1000));
787
+ }
788
+ this.logger.info("✅ Navigation completed");
789
+ return true;
790
+ }
791
+ catch (error) {
792
+ this.logger.error("❌ Navigation failed:", error);
793
+ return false;
794
+ }
795
+ }
796
+ /**
797
+ * Click element by text content
798
+ *
799
+ * @param text - Text content to search for
800
+ * @param exact - Whether to match exact text (default: false)
801
+ * @returns Promise resolving to boolean indicating success
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * // Click element containing text
806
+ * await actionHandler.clickByText('Submit');
807
+ *
808
+ * // Click element with exact text match
809
+ * await actionHandler.clickByText('Submit', true);
810
+ * ```
811
+ */
812
+ async clickByText(text, exact = false) {
813
+ const selector = exact
814
+ ? { text, clickable: false, visible: true }
815
+ : { textContains: text, clickable: false, visible: true };
816
+ const element = await this.elementFinder.findElement(selector, { maxRetries: 3 });
817
+ if (!element) {
818
+ return false;
819
+ }
820
+ const action = {
821
+ title: `Click by text: ${text}`,
822
+ actions: [
823
+ {
824
+ type: "click",
825
+ target: { type: "elementId", value: element.elementId },
826
+ options: { waitAfterMs: 2000 },
827
+ },
828
+ ],
829
+ };
830
+ await this.executeAndVerifyAction(action);
831
+ return true;
832
+ }
833
+ /**
834
+ * Input text into a field
835
+ *
836
+ * @param selector - Element selector
837
+ * @param text - Text to input
838
+ * @param options - Additional options
839
+ * @returns Promise resolving to boolean indicating success
840
+ *
841
+ * @example
842
+ * ```typescript
843
+ * await actionHandler.inputText(
844
+ * { resourceId: 'username_field' },
845
+ * 'john.doe',
846
+ * { clear: true, hideKeyboard: false }
847
+ * );
848
+ * ```
849
+ */
850
+ async inputText(selector, text, options) {
851
+ const element = await this.elementFinder.findElement(selector, { maxRetries: 3 });
852
+ if (!element) {
853
+ return false;
854
+ }
855
+ const action = {
856
+ title: `Input text: ${text}`,
857
+ actions: [
858
+ {
859
+ type: "input",
860
+ text,
861
+ target: { type: "elementId", value: element.elementId },
862
+ options: {
863
+ replaceExisting: options?.clear ?? true,
864
+ hideKeyboardAfter: options?.hideKeyboard ?? false,
865
+ },
866
+ },
867
+ ],
868
+ };
869
+ await this.executeAndVerifyAction(action);
870
+ return true;
871
+ }
872
+ }
873
+ //# sourceMappingURL=ui-action-handler.js.map