shellx-ai 1.0.11 → 1.1.0
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/README.md +586 -0
- package/dist/automation/element-finder.d.ts +189 -0
- package/dist/automation/element-finder.js +322 -0
- package/dist/automation/element-finder.js.map +1 -0
- package/dist/automation/ui-action-handler.d.ts +330 -0
- package/dist/automation/ui-action-handler.js +873 -0
- package/dist/automation/ui-action-handler.js.map +1 -0
- package/dist/cbor-compat.d.ts +27 -0
- package/dist/cbor-compat.js +108 -0
- package/dist/cbor-compat.js.map +1 -0
- package/dist/domain-manager.d.ts +80 -0
- package/dist/domain-manager.js +158 -0
- package/dist/domain-manager.js.map +1 -0
- package/dist/error-handler.d.ts +87 -0
- package/dist/error-handler.js +148 -0
- package/dist/error-handler.js.map +1 -0
- package/dist/errors.d.ts +114 -0
- package/dist/errors.js +139 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +163 -54
- package/dist/index.js +712 -472
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +81 -0
- package/dist/logger.js +128 -0
- package/dist/logger.js.map +1 -0
- package/dist/protocol.d.ts +147 -31
- package/dist/protocol.js +2 -2
- package/dist/protocol.js.map +1 -0
- package/dist/shell/output-buffer.d.ts +152 -0
- package/dist/shell/output-buffer.js +163 -0
- package/dist/shell/output-buffer.js.map +1 -0
- package/dist/shell/shell-command-executor.d.ts +182 -0
- package/dist/shell/shell-command-executor.js +348 -0
- package/dist/shell/shell-command-executor.js.map +1 -0
- package/dist/shellx.d.ts +681 -176
- package/dist/shellx.js +763 -1047
- package/dist/shellx.js.map +1 -0
- package/dist/types.d.ts +132 -57
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -0
- package/dist/utils/retry-helper.d.ts +73 -0
- package/dist/utils/retry-helper.js +92 -0
- package/dist/utils/retry-helper.js.map +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +17 -23
- package/dist/utils.js.map +1 -0
- package/package.json +95 -59
|
@@ -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
|