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.
- package/LICENSE +21 -0
- package/README.md +666 -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 +111 -0
- package/dist/cbor-compat.js.map +1 -0
- package/dist/domain-manager.d.ts +80 -0
- package/dist/domain-manager.js +161 -0
- package/dist/domain-manager.js.map +1 -0
- package/dist/error-handler.d.ts +87 -0
- package/dist/error-handler.js +151 -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 +678 -481
- 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 +176 -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 +404 -0
- package/dist/shell/shell-command-executor.js.map +1 -0
- package/dist/shellx.d.ts +681 -178
- package/dist/shellx.js +762 -1159
- 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 +95 -0
- package/dist/utils/retry-helper.js.map +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +20 -23
- package/dist/utils.js.map +1 -0
- package/package.json +95 -62
package/dist/shellx.js
CHANGED
|
@@ -1,1222 +1,825 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.ShellX = void 0;
|
|
16
|
-
exports.createShellX = createShellX;
|
|
17
|
-
exports.createShellXWithShellMonitoring = createShellXWithShellMonitoring;
|
|
18
|
-
const uuid_1 = require("uuid");
|
|
19
|
-
const index_1 = __importDefault(require("./index"));
|
|
20
|
-
const COMMAND_PTY_SID = 999;
|
|
21
1
|
/**
|
|
22
|
-
* ShellX automation utilities for
|
|
2
|
+
* ShellX - High-level automation utilities for Android device control
|
|
3
|
+
*
|
|
4
|
+
* This module provides a comprehensive API for automating Android devices,
|
|
5
|
+
* including UI actions, shell commands, element finding, and device information.
|
|
6
|
+
*
|
|
7
|
+
* @module shellx
|
|
23
8
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
9
|
+
// Import refactored modules
|
|
10
|
+
import { UIActionHandler } from "./automation/ui-action-handler.js";
|
|
11
|
+
import { ElementFinder } from "./automation/element-finder.js";
|
|
12
|
+
import { ShellCommandExecutor } from "./shell/shell-command-executor.js";
|
|
13
|
+
// Import client
|
|
14
|
+
import ConnectionClient from "./index.js";
|
|
15
|
+
import { createLogger } from "./logger.js";
|
|
16
|
+
/**
|
|
17
|
+
* ShellX - High-level Android automation SDK (Recommended API)
|
|
18
|
+
*
|
|
19
|
+
* **This is the recommended API for 95% of users.**
|
|
20
|
+
*
|
|
21
|
+
* ShellX provides a simple, intuitive interface for Android automation with:
|
|
22
|
+
*
|
|
23
|
+
* - ✅ Automatic error handling and retry logic
|
|
24
|
+
* - ✅ Type-safe TypeScript API
|
|
25
|
+
* - ✅ Consistent method signatures
|
|
26
|
+
* - ✅ Built-in timing and performance tracking
|
|
27
|
+
* - ✅ Simplified element selection
|
|
28
|
+
*
|
|
29
|
+
* **Key Features:**
|
|
30
|
+
* - UI interaction: click, input, swipe, press, wait
|
|
31
|
+
* - Element finding with smart selectors
|
|
32
|
+
* - Shell command execution
|
|
33
|
+
* - Screen info and screenshots
|
|
34
|
+
* - Batch operations
|
|
35
|
+
*
|
|
36
|
+
* **Quick Start:**
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { ShellX } from '@shellx-ai/sdk';
|
|
39
|
+
*
|
|
40
|
+
* // Initialize
|
|
41
|
+
* const shellx = new ShellX({ deviceId: 'your-device-id' });
|
|
42
|
+
*
|
|
43
|
+
* // Connect
|
|
44
|
+
* await shellx.connect();
|
|
45
|
+
*
|
|
46
|
+
* // Automate
|
|
47
|
+
* await shellx.click('Submit');
|
|
48
|
+
* await shellx.input({ text: 'Hello World' });
|
|
49
|
+
* await shellx.swipe({ fromX: 500, fromY: 1000, toX: 500, toY: 500 });
|
|
50
|
+
*
|
|
51
|
+
* // Get info
|
|
52
|
+
* const screen = await shellx.getScreenInfo();
|
|
53
|
+
* console.log(`Screen: ${screen.width}x${screen.height}`);
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* **Note:** For low-level WebSocket access, see ConnectionClient (advanced users only).
|
|
57
|
+
*
|
|
58
|
+
* @see API-GUIDE.md for comprehensive documentation
|
|
59
|
+
* @see ConnectionClient for low-level protocol access
|
|
60
|
+
*/
|
|
61
|
+
export class ShellX {
|
|
62
|
+
/** Logger instance for ShellX */
|
|
63
|
+
logger;
|
|
64
|
+
/** UI action handler for executing UI operations */
|
|
65
|
+
uiActionHandler;
|
|
66
|
+
/** Element finder for locating UI elements */
|
|
67
|
+
elementFinder;
|
|
68
|
+
/** Shell command executor for running shell commands */
|
|
69
|
+
shellCommandExecutor;
|
|
70
|
+
/** Connection client for device communication */
|
|
71
|
+
client;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a ShellX instance
|
|
74
|
+
*
|
|
75
|
+
* @param options - Configuration options for ShellX instance
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const shellx = new ShellX({
|
|
80
|
+
* deviceId: 'device-id',
|
|
81
|
+
* onOpen: () => console.log('Connected!'),
|
|
82
|
+
* onMessage: (msg) => console.log(msg)
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* // Optionally wait for connection
|
|
86
|
+
* await shellx.ready();
|
|
87
|
+
*
|
|
88
|
+
* // Start using ShellX
|
|
89
|
+
* await shellx.click('Settings');
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
constructor(options) {
|
|
93
|
+
// Initialize logger with provided log level
|
|
94
|
+
this.logger = createLogger("ShellX", options.logLevel);
|
|
95
|
+
// Create ConnectionClient with options and logger
|
|
96
|
+
this.client = new ConnectionClient(options.deviceId, {
|
|
97
|
+
timeout: options.timeout ?? 5000,
|
|
98
|
+
reconnect: options.reconnect ?? true,
|
|
99
|
+
reconnectMaxAttempts: options.reconnectMaxAttempts ?? 5,
|
|
100
|
+
reconnectInterval: options.reconnectInterval ?? 1000,
|
|
101
|
+
pingInterval: options.pingInterval ?? 2000,
|
|
102
|
+
onOpen: () => {
|
|
103
|
+
this.logger.info("✅ WebSocket connection opened");
|
|
104
|
+
options.onOpen?.();
|
|
105
|
+
},
|
|
106
|
+
onClose: () => {
|
|
107
|
+
options.onClose?.();
|
|
108
|
+
},
|
|
109
|
+
onError: (error) => {
|
|
110
|
+
options.onError?.(error);
|
|
111
|
+
},
|
|
112
|
+
onReconnectFailed: () => {
|
|
113
|
+
options.onReconnectFailed?.();
|
|
114
|
+
},
|
|
115
|
+
onMessage: (message) => {
|
|
116
|
+
// Forward shell output to ShellX instance
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
|
118
|
+
const wsMessage = message;
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
120
|
+
if (wsMessage.chunks) {
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
|
122
|
+
this.handleShellOutput(wsMessage.chunks);
|
|
123
|
+
}
|
|
124
|
+
// Call user's onMessage callback
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
126
|
+
options.onMessage?.(message);
|
|
127
|
+
},
|
|
128
|
+
}, this.logger);
|
|
129
|
+
// Initialize handlers
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
131
|
+
this.uiActionHandler = new UIActionHandler(this.client);
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
133
|
+
this.elementFinder = new ElementFinder(this.client);
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
135
|
+
this.shellCommandExecutor = new ShellCommandExecutor(this.client);
|
|
136
|
+
// Link client and shellx
|
|
137
|
+
this.client.setShellX(this);
|
|
138
|
+
this.logger.info("⏳ Initializing connection...");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wait for connection to be ready
|
|
142
|
+
*
|
|
143
|
+
* This method waits for the WebSocket connection to be established.
|
|
144
|
+
* Most operations will automatically wait for connection, but you can
|
|
145
|
+
* call this method explicitly if you need to ensure connection before
|
|
146
|
+
* performing operations.
|
|
147
|
+
*
|
|
148
|
+
* @returns Promise that resolves when connection is ready
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
153
|
+
* await shellx.ready();
|
|
154
|
+
* console.log('Connection is ready!');
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
async ready() {
|
|
158
|
+
await this.client.waitForInitialization();
|
|
159
|
+
this.logger.info("✅ Connection is ready");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the underlying ConnectionClient instance
|
|
163
|
+
*
|
|
164
|
+
* @returns The ConnectionClient instance
|
|
35
165
|
*/
|
|
36
166
|
getClient() {
|
|
37
167
|
return this.client;
|
|
38
168
|
}
|
|
39
169
|
/**
|
|
40
|
-
*
|
|
170
|
+
* Get the current log level
|
|
171
|
+
*
|
|
172
|
+
* @returns Current log level
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
177
|
+
* console.log('Current log level:', shellx.getLogLevel());
|
|
178
|
+
* ```
|
|
41
179
|
*/
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
// 处理 chunks 数据(pty 终端输出)
|
|
45
|
-
if (message.chunks) {
|
|
46
|
-
helpers.handleShellOutput(message.chunks);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
180
|
+
getLogLevel() {
|
|
181
|
+
return this.logger.getLogLevel();
|
|
49
182
|
}
|
|
50
183
|
/**
|
|
51
|
-
*
|
|
184
|
+
* Set the log level
|
|
185
|
+
*
|
|
186
|
+
* @param level - Log level to set
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* import { ShellX, LogLevel } from '@shellx-ai/sdk';
|
|
191
|
+
*
|
|
192
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
193
|
+
* shellx.setLogLevel(LogLevel.NONE); // Disable all logging
|
|
194
|
+
* shellx.setLogLevel(LogLevel.ERROR); // Only errors
|
|
195
|
+
* shellx.setLogLevel(LogLevel.DEBUG); // All logs
|
|
196
|
+
* ```
|
|
52
197
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (sessionId === COMMAND_PTY_SID) {
|
|
56
|
-
try {
|
|
57
|
-
// 将 Uint8Array 数组转换为字符串
|
|
58
|
-
let output = '';
|
|
59
|
-
for (const data of dataArrays) {
|
|
60
|
-
output += new TextDecoder().decode(data);
|
|
61
|
-
}
|
|
62
|
-
/*console.log(
|
|
63
|
-
`📟 [Shell] 收到输出 (Session ${sessionId}): ${output.trim()}`,
|
|
64
|
-
);*/
|
|
65
|
-
// 为每个等待的命令累积输出
|
|
66
|
-
for (const [commandKey, commandPromise,] of this.shellCommandPromises.entries()) {
|
|
67
|
-
if (!commandPromise.sessionOutputs.has(sessionId)) {
|
|
68
|
-
commandPromise.sessionOutputs.set(sessionId, '');
|
|
69
|
-
}
|
|
70
|
-
const currentSessionOutput = commandPromise.sessionOutputs.get(sessionId) || '';
|
|
71
|
-
commandPromise.sessionOutputs.set(sessionId, currentSessionOutput + output);
|
|
72
|
-
commandPromise.output = this.combineSessionOutputs(commandPromise.sessionOutputs);
|
|
73
|
-
/*console.log(
|
|
74
|
-
`📊 [Shell] 命令 ${commandKey} 累积输出长度: ${commandPromise.output.length}`,
|
|
75
|
-
);*/
|
|
76
|
-
// 调用输出回调(传递清理后的输出)
|
|
77
|
-
if (commandPromise.options.onOutput) {
|
|
78
|
-
const cleanOutput = this.cleanCommandOutput(output, commandPromise.command);
|
|
79
|
-
commandPromise.options.onOutput(cleanOutput);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
console.error(`❌ [Shell] 处理输出数据失败:`, error);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
198
|
+
setLogLevel(level) {
|
|
199
|
+
this.logger.setLogLevel(level);
|
|
87
200
|
}
|
|
88
201
|
/**
|
|
89
|
-
*
|
|
202
|
+
* Enable all logging (equivalent to setLogLevel(LogLevel.DEBUG))
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
207
|
+
* shellx.enableDebugLogging();
|
|
208
|
+
* ```
|
|
90
209
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
210
|
+
enableDebugLogging() {
|
|
211
|
+
this.logger.enableDebugLogging();
|
|
93
212
|
}
|
|
94
213
|
/**
|
|
95
|
-
*
|
|
214
|
+
* Disable all logging (equivalent to setLogLevel(LogLevel.NONE))
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
219
|
+
* shellx.disableLogging();
|
|
220
|
+
* ```
|
|
96
221
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let combined = '';
|
|
100
|
-
for (const sessionId of sessions) {
|
|
101
|
-
const sessionOutput = sessionOutputs.get(sessionId) || '';
|
|
102
|
-
combined += sessionOutput;
|
|
103
|
-
}
|
|
104
|
-
return combined;
|
|
222
|
+
disableLogging() {
|
|
223
|
+
this.logger.disableLogging();
|
|
105
224
|
}
|
|
106
225
|
/**
|
|
107
|
-
*
|
|
226
|
+
* Ensure connection is ready before executing device operations
|
|
227
|
+
*
|
|
228
|
+
* @param timeout - Timeout in milliseconds (default: 10000ms)
|
|
229
|
+
* @throws Error if connection timeout
|
|
108
230
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const { retry = 3, delay = 1000, onRetry } = options;
|
|
112
|
-
for (let attempt = 1; attempt <= retry; attempt++) {
|
|
113
|
-
try {
|
|
114
|
-
return yield operation();
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
if (attempt === retry) {
|
|
118
|
-
console.log(`❌ [Retry] 重试 ${retry} 次后仍然失败,返回 undefined:`, error);
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
if (onRetry) {
|
|
122
|
-
onRetry(attempt, error);
|
|
123
|
-
}
|
|
124
|
-
console.log(`🔄 [Retry] 第 ${attempt} 次尝试失败,${delay}ms 后重试...`);
|
|
125
|
-
yield new Promise(resolve => setTimeout(resolve, delay));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return undefined;
|
|
129
|
-
});
|
|
231
|
+
async ensureConnectionReady(timeout = 10000) {
|
|
232
|
+
await this.client.ensureConnected(timeout);
|
|
130
233
|
}
|
|
131
234
|
/**
|
|
132
|
-
*
|
|
235
|
+
* Send chat message through ShellX
|
|
236
|
+
*
|
|
237
|
+
* @param message - The chat message to send
|
|
238
|
+
* @returns Promise that resolves when the message is sent
|
|
133
239
|
*/
|
|
134
|
-
|
|
240
|
+
async sendChat(message) {
|
|
241
|
+
return this.client.sendChat(message);
|
|
242
|
+
}
|
|
243
|
+
async click(selectorOrData, options) {
|
|
244
|
+
await this.ensureConnectionReady();
|
|
245
|
+
if (typeof selectorOrData === "string") {
|
|
246
|
+
return this.uiActionHandler.click({ text: selectorOrData, ...options });
|
|
247
|
+
}
|
|
248
|
+
return this.uiActionHandler.click(selectorOrData);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Execute an input action
|
|
252
|
+
*
|
|
253
|
+
* @param inputData - Input configuration
|
|
254
|
+
* @returns Promise resolving to InputResult
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const result = await shellx.input({
|
|
259
|
+
* elementId: 'field123',
|
|
260
|
+
* text: 'Hello World',
|
|
261
|
+
* clear: true
|
|
262
|
+
* });
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
async input(inputData) {
|
|
266
|
+
await this.ensureConnectionReady();
|
|
267
|
+
return this.uiActionHandler.input(inputData);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Execute a swipe action
|
|
271
|
+
*
|
|
272
|
+
* @param swipeData - Swipe configuration
|
|
273
|
+
* @returns Promise resolving to SwipeResult
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const result = await shellx.swipe({
|
|
278
|
+
* fromX: 500,
|
|
279
|
+
* fromY: 1000,
|
|
280
|
+
* toX: 500,
|
|
281
|
+
* toY: 500,
|
|
282
|
+
* duration: 800
|
|
283
|
+
* });
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
async swipe(swipeData) {
|
|
287
|
+
await this.ensureConnectionReady();
|
|
288
|
+
return this.uiActionHandler.swipe(swipeData);
|
|
289
|
+
}
|
|
290
|
+
async press(keyOrData, options) {
|
|
291
|
+
await this.ensureConnectionReady();
|
|
292
|
+
if (typeof keyOrData === "string") {
|
|
293
|
+
return this.uiActionHandler.pressKey({ key: keyOrData, ...options });
|
|
294
|
+
}
|
|
295
|
+
return this.uiActionHandler.pressKey(keyOrData);
|
|
296
|
+
}
|
|
297
|
+
async wait(selectorOrData, options) {
|
|
298
|
+
await this.ensureConnectionReady();
|
|
299
|
+
if (typeof selectorOrData === "string") {
|
|
300
|
+
return this.uiActionHandler.wait({ text: selectorOrData, ...options });
|
|
301
|
+
}
|
|
302
|
+
return this.uiActionHandler.wait(selectorOrData);
|
|
303
|
+
}
|
|
304
|
+
async find(selectorOrData, options) {
|
|
305
|
+
await this.ensureConnectionReady();
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
const findData = typeof selectorOrData === "string" ? { text: selectorOrData, ...options } : selectorOrData;
|
|
308
|
+
const selector = this.uiActionHandler["convertSelector"](findData);
|
|
309
|
+
const result = await this.client.findElement(selector, {
|
|
310
|
+
pressClick: findData.pressClick,
|
|
311
|
+
waitAfterMs: findData.waitAfterMs || 5000,
|
|
312
|
+
maxResults: findData.maxResults || 1000,
|
|
313
|
+
visibleOnly: true,
|
|
314
|
+
clickableOnly: false,
|
|
315
|
+
multiple: findData.multiple || false,
|
|
316
|
+
});
|
|
317
|
+
if (!result || result.success === false || !result.elements) {
|
|
318
|
+
return {
|
|
319
|
+
elements: [],
|
|
320
|
+
count: 0,
|
|
321
|
+
success: false,
|
|
322
|
+
found: false,
|
|
323
|
+
duration: 0,
|
|
324
|
+
timestamp: startTime,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
135
327
|
return {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
328
|
+
elements: result.elements.map((el) => ({
|
|
329
|
+
id: el.elementId,
|
|
330
|
+
text: el.text,
|
|
331
|
+
class: el.className,
|
|
332
|
+
left: el.bounds.left,
|
|
333
|
+
top: el.bounds.top,
|
|
334
|
+
right: el.bounds.right,
|
|
335
|
+
bottom: el.bounds.bottom,
|
|
336
|
+
visible: el.visible,
|
|
337
|
+
clickable: el.clickable,
|
|
338
|
+
})),
|
|
339
|
+
count: result.elements.length,
|
|
340
|
+
success: true,
|
|
341
|
+
found: true,
|
|
342
|
+
duration: 0,
|
|
343
|
+
timestamp: startTime,
|
|
143
344
|
};
|
|
144
345
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
346
|
+
async command(cmdOrData, options) {
|
|
347
|
+
await this.ensureConnectionReady();
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const commandData = typeof cmdOrData === "string" ? { cmd: cmdOrData, ...options } : cmdOrData;
|
|
350
|
+
const result = await this.shellCommandExecutor.executeShellCommand(commandData.cmd, {
|
|
351
|
+
title: `Execute command: ${commandData.cmd}`,
|
|
352
|
+
timeout: commandData.timeout,
|
|
353
|
+
waitAfterMs: commandData.wait,
|
|
354
|
+
});
|
|
355
|
+
this.getClient().appendExecutionLog(`✅ Command executed successfully - Output: ${result.output}`);
|
|
149
356
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
visible: uiElement.visible,
|
|
158
|
-
clickable: uiElement.clickable
|
|
357
|
+
success: result.success,
|
|
358
|
+
output: result.output,
|
|
359
|
+
error: result.error,
|
|
360
|
+
exitCode: result.exitCode,
|
|
361
|
+
duration: result.duration,
|
|
362
|
+
cmd: commandData.cmd,
|
|
363
|
+
timestamp: startTime,
|
|
159
364
|
};
|
|
160
365
|
}
|
|
161
|
-
// ==================== 精简 Action 封装函数 ====================
|
|
162
366
|
/**
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
throw new Error('必须指定目标:targetElementId、targetResourceId、坐标(targetX/targetY)或选择器(targetText/targetClass)');
|
|
193
|
-
}
|
|
194
|
-
const action = {
|
|
195
|
-
title: `点击操作: ${clickData.clickType || 'single'}`,
|
|
196
|
-
actions: [{
|
|
197
|
-
type: "click",
|
|
198
|
-
target,
|
|
199
|
-
options: {
|
|
200
|
-
clickType: clickData.clickType || 'single',
|
|
201
|
-
waitAfterMs: clickData.wait || 1000
|
|
202
|
-
}
|
|
203
|
-
}]
|
|
204
|
-
};
|
|
205
|
-
yield this.client.executeAction(action);
|
|
206
|
-
return {
|
|
207
|
-
success: true,
|
|
208
|
-
data: {
|
|
209
|
-
targetElementId: clickData.targetElementId,
|
|
210
|
-
targetResourceId: clickData.targetResourceId,
|
|
211
|
-
targetText: clickData.targetText,
|
|
212
|
-
targetClass: clickData.targetClass,
|
|
213
|
-
targetX: clickData.targetX,
|
|
214
|
-
targetY: clickData.targetY,
|
|
215
|
-
clickType: clickData.clickType
|
|
216
|
-
},
|
|
217
|
-
duration: Date.now() - startTime
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
throw new Error(`点击操作失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
|
-
}
|
|
223
|
-
}), { retry: clickData.retry, delay: 500 });
|
|
224
|
-
// 如果 withRetry 返回 undefined,返回失败的 ActionResult
|
|
225
|
-
if (!result) {
|
|
226
|
-
return {
|
|
227
|
-
success: false,
|
|
228
|
-
error: '点击操作失败',
|
|
229
|
-
duration: Date.now() - startTime
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
});
|
|
367
|
+
* Execute a clipboard action
|
|
368
|
+
*
|
|
369
|
+
* @param clipboardData - Clipboard configuration
|
|
370
|
+
* @returns Promise resolving to ClipboardResult
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* // Get clipboard content
|
|
375
|
+
* const result = await shellx.clipboard({ get: true });
|
|
376
|
+
* console.log(result.text);
|
|
377
|
+
*
|
|
378
|
+
* // Set clipboard content
|
|
379
|
+
* await shellx.clipboard({ text: 'Hello' });
|
|
380
|
+
*
|
|
381
|
+
* // Paste clipboard content
|
|
382
|
+
* await shellx.clipboard({ paste: true });
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
async clipboard(clipboardData) {
|
|
386
|
+
await this.ensureConnectionReady();
|
|
387
|
+
const result = await this.uiActionHandler.clipboard(clipboardData);
|
|
388
|
+
return {
|
|
389
|
+
...result,
|
|
390
|
+
text: result.text,
|
|
391
|
+
pasted: clipboardData.paste ? result.success : undefined,
|
|
392
|
+
getCopied: clipboardData.get ? result.success : undefined,
|
|
393
|
+
};
|
|
234
394
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
395
|
+
async getAppInfo(pkgOrData, options) {
|
|
396
|
+
await this.ensureConnectionReady();
|
|
397
|
+
const appInfoData = typeof pkgOrData === "string" ? { package: pkgOrData, ...options } : pkgOrData;
|
|
398
|
+
const result = await this.uiActionHandler.getAppInfo(appInfoData);
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get list of installed applications
|
|
403
|
+
*
|
|
404
|
+
* @param options - Options for filtering app list
|
|
405
|
+
* @returns Promise resolving to app list response
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* // Get all user apps
|
|
410
|
+
* const result = await shellx.getAppList();
|
|
411
|
+
* console.log(`Found ${result.userAppCount} user apps`);
|
|
412
|
+
* result.apps.forEach(app => {
|
|
413
|
+
* console.log(`${app.appName} (${app.packageName})`);
|
|
414
|
+
* });
|
|
415
|
+
*
|
|
416
|
+
* // Get user and system apps
|
|
417
|
+
* const allApps = await shellx.getAppList({
|
|
418
|
+
* includeSystemApps: true,
|
|
419
|
+
* includeDisabledApps: false
|
|
420
|
+
* });
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
async getAppList(options) {
|
|
424
|
+
await this.ensureConnectionReady();
|
|
425
|
+
return this.uiActionHandler.getAppList(options);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Take a screenshot
|
|
429
|
+
*
|
|
430
|
+
* @param screenshotData - Screenshot configuration
|
|
431
|
+
* @returns Promise resolving to ScreenshotResult
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```typescript
|
|
435
|
+
* const result = await shellx.takeScreenshot({
|
|
436
|
+
* format: 'png',
|
|
437
|
+
* quality: 100,
|
|
438
|
+
* saveToFile: true
|
|
439
|
+
* });
|
|
440
|
+
*
|
|
441
|
+
* console.log(result.imagePath);
|
|
442
|
+
* ```
|
|
443
|
+
*/
|
|
444
|
+
async takeScreenshot(screenshotData = {}) {
|
|
445
|
+
await this.ensureConnectionReady();
|
|
446
|
+
return await this.uiActionHandler.takeScreenshot(screenshotData);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get screen information
|
|
450
|
+
*
|
|
451
|
+
* @returns Promise resolving to ScreenInfoResult
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* const screenInfo = await shellx.getScreenInfo();
|
|
456
|
+
* console.log(`Screen: ${screenInfo.width}x${screenInfo.height}`);
|
|
457
|
+
* console.log(`Current app: ${screenInfo.foregroundApp}`);
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
async getScreenInfo() {
|
|
461
|
+
await this.ensureConnectionReady();
|
|
462
|
+
const startTime = Date.now();
|
|
463
|
+
const info = await this.uiActionHandler.getScreenInfo();
|
|
464
|
+
if (!info || info.success === false) {
|
|
465
|
+
throw new Error(info?.error || "Failed to get screen info");
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
duration: Date.now() - startTime,
|
|
470
|
+
timestamp: startTime,
|
|
471
|
+
width: info.width,
|
|
472
|
+
height: info.height,
|
|
473
|
+
density: info.density,
|
|
474
|
+
screenOn: info.screenOn,
|
|
475
|
+
screenUnlocked: info.screenUnlocked,
|
|
476
|
+
foregroundApp: info.accurateForegroundApp,
|
|
477
|
+
foregroundActivity: info.accurateForegroundActivity,
|
|
478
|
+
model: info.model,
|
|
479
|
+
androidVersion: info.androidVersion,
|
|
480
|
+
manufacturer: info.manufacturer,
|
|
481
|
+
};
|
|
300
482
|
}
|
|
301
483
|
/**
|
|
302
|
-
*
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
484
|
+
* Execute multiple actions in sequence
|
|
485
|
+
*
|
|
486
|
+
* @param actions - Array of actions to execute
|
|
487
|
+
* @returns Promise resolving to ExecuteActionsResult
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* const result = await shellx.executeActions([
|
|
492
|
+
* { text: 'Settings' },
|
|
493
|
+
* { text: 'Accounts' },
|
|
494
|
+
* { cmd: 'ls -la' }
|
|
495
|
+
* ]);
|
|
496
|
+
*
|
|
497
|
+
* console.log(`Success: ${result.successCount}, Failed: ${result.failureCount}`);
|
|
498
|
+
* result.results.forEach((res, index) => {
|
|
499
|
+
* console.log(`Action ${index + 1}: ${res.success ? 'Success' : 'Failed'}`);
|
|
500
|
+
* });
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
async executeActions(actions) {
|
|
504
|
+
await this.ensureConnectionReady();
|
|
505
|
+
const startTime = Date.now();
|
|
506
|
+
const results = [];
|
|
507
|
+
let successCount = 0;
|
|
508
|
+
let failureCount = 0;
|
|
509
|
+
for (const [index, action] of actions.entries()) {
|
|
510
|
+
try {
|
|
511
|
+
this.logger.info(`🔨 Executing action ${index + 1}/${actions.length}`);
|
|
512
|
+
let result;
|
|
513
|
+
// Check for clipboard first (before click/input which also have text)
|
|
514
|
+
if ("get" in action || "paste" in action) {
|
|
515
|
+
result = await this.clipboard(action);
|
|
332
516
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
success: false,
|
|
337
|
-
error: '滑动操作失败',
|
|
338
|
-
duration: Date.now() - startTime
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
return result;
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* 按键操作
|
|
346
|
-
*/
|
|
347
|
-
pressKey(keyData) {
|
|
348
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
349
|
-
const startTime = Date.now();
|
|
350
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
351
|
-
try {
|
|
352
|
-
const action = {
|
|
353
|
-
title: `按键操作: ${keyData.key}${keyData.longPress ? ' (长按)' : ''}`,
|
|
354
|
-
actions: [{
|
|
355
|
-
type: "key",
|
|
356
|
-
keyCode: keyData.key,
|
|
357
|
-
options: {
|
|
358
|
-
longPress: keyData.longPress || false
|
|
359
|
-
}
|
|
360
|
-
}]
|
|
361
|
-
};
|
|
362
|
-
yield this.client.executeAction(action);
|
|
363
|
-
if (keyData.wait) {
|
|
364
|
-
yield new Promise(resolve => setTimeout(resolve, keyData.wait));
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
success: true,
|
|
368
|
-
data: { key: keyData.key, longPress: keyData.longPress },
|
|
369
|
-
duration: Date.now() - startTime
|
|
370
|
-
};
|
|
517
|
+
else if ("text" in action && ("elementId" in action || "resourceId" in action)) {
|
|
518
|
+
result = await this.input(action);
|
|
371
519
|
}
|
|
372
|
-
|
|
373
|
-
|
|
520
|
+
else if ("elementId" in action ||
|
|
521
|
+
"resourceId" in action ||
|
|
522
|
+
"class" in action ||
|
|
523
|
+
"x" in action ||
|
|
524
|
+
"y" in action) {
|
|
525
|
+
result = await this.click(action);
|
|
374
526
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
success: false,
|
|
379
|
-
error: '按键操作失败',
|
|
380
|
-
duration: Date.now() - startTime
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
return result;
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* 等待操作 - 等待元素出现或消失
|
|
388
|
-
*/
|
|
389
|
-
wait(waitData) {
|
|
390
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
391
|
-
const startTime = Date.now();
|
|
392
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
393
|
-
try {
|
|
394
|
-
const selector = this.convertSelector(waitData);
|
|
395
|
-
const timeout = waitData.timeout || 10000;
|
|
396
|
-
const interval = 500;
|
|
397
|
-
const maxAttempts = Math.floor(timeout / interval);
|
|
398
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
399
|
-
try {
|
|
400
|
-
const result = yield this.client.findElement(selector, {
|
|
401
|
-
timeout: interval,
|
|
402
|
-
maxResults: 1,
|
|
403
|
-
visibleOnly: waitData.condition === 'visible',
|
|
404
|
-
clickableOnly: waitData.condition === 'clickable'
|
|
405
|
-
});
|
|
406
|
-
// 如果服务器返回失败,跳过本次尝试
|
|
407
|
-
if (!result || result.success === false) {
|
|
408
|
-
yield new Promise(resolve => setTimeout(resolve, interval));
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
if (result.elements && result.elements.length > 0) {
|
|
412
|
-
if (waitData.condition === 'gone') {
|
|
413
|
-
// 等待元素消失,继续等待
|
|
414
|
-
yield new Promise(resolve => setTimeout(resolve, interval));
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
// 等待元素出现,已找到
|
|
419
|
-
return {
|
|
420
|
-
success: true,
|
|
421
|
-
data: { element: this.convertElement(result.elements[0]) },
|
|
422
|
-
duration: Date.now() - startTime
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
else if (waitData.condition === 'gone') {
|
|
427
|
-
// 等待元素消失,已消失
|
|
428
|
-
return {
|
|
429
|
-
success: true,
|
|
430
|
-
data: { element: null },
|
|
431
|
-
duration: Date.now() - startTime
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
catch (error) {
|
|
436
|
-
// 查找失败,继续等待
|
|
437
|
-
}
|
|
438
|
-
yield new Promise(resolve => setTimeout(resolve, interval));
|
|
439
|
-
}
|
|
440
|
-
throw new Error(`等待超时: ${waitData.condition || 'visible'}`);
|
|
527
|
+
else if ("fromX" in action && "fromY" in action && "toX" in action && "toY" in action) {
|
|
528
|
+
result = await this.swipe(action);
|
|
441
529
|
}
|
|
442
|
-
|
|
443
|
-
|
|
530
|
+
else if ("key" in action) {
|
|
531
|
+
result = await this.press(action);
|
|
444
532
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
success: false,
|
|
449
|
-
error: '等待操作失败',
|
|
450
|
-
duration: Date.now() - startTime
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
return result;
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* 查找操作 - 查找元素
|
|
458
|
-
*/
|
|
459
|
-
find(findData) {
|
|
460
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
462
|
-
try {
|
|
463
|
-
const selector = this.convertSelector(findData);
|
|
464
|
-
const result = yield this.client.findElement(selector, {
|
|
465
|
-
pressClick: findData.pressClick,
|
|
466
|
-
waitAfterMs: findData.waitAfterMs || 5000,
|
|
467
|
-
maxResults: findData.maxResults || 1000,
|
|
468
|
-
visibleOnly: true,
|
|
469
|
-
clickableOnly: false,
|
|
470
|
-
multiple: findData.multiple || false
|
|
471
|
-
});
|
|
472
|
-
// 检查服务器是否返回了失败响应
|
|
473
|
-
if (!result || result.success === false) {
|
|
474
|
-
throw new Error((result === null || result === void 0 ? void 0 : result.errorMessage) || '查找元素失败');
|
|
475
|
-
}
|
|
476
|
-
if (!result.elements) {
|
|
477
|
-
throw new Error('未找到元素');
|
|
478
|
-
}
|
|
479
|
-
const elements = result.elements.map(element => this.convertElement(element));
|
|
480
|
-
return {
|
|
481
|
-
elements,
|
|
482
|
-
count: elements.length,
|
|
483
|
-
success: true,
|
|
484
|
-
found: result.found,
|
|
485
|
-
};
|
|
533
|
+
else if ("condition" in action &&
|
|
534
|
+
("elementId" in action || "resourceId" in action || "text" in action || "class" in action)) {
|
|
535
|
+
result = await this.wait(action);
|
|
486
536
|
}
|
|
487
|
-
|
|
488
|
-
|
|
537
|
+
else if ("multiple" in action &&
|
|
538
|
+
("elementId" in action || "resourceId" in action || "text" in action || "class" in action)) {
|
|
539
|
+
result = await this.find(action);
|
|
489
540
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
return {
|
|
493
|
-
elements: [],
|
|
494
|
-
count: 0,
|
|
495
|
-
success: false,
|
|
496
|
-
found: false
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
return result;
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* 执行命令 - 兼容现有的 shell 命令执行
|
|
504
|
-
*/
|
|
505
|
-
executeCommand(commandData) {
|
|
506
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
507
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
508
|
-
try {
|
|
509
|
-
const result = yield this.executeShellCommand(commandData.cmd, {
|
|
510
|
-
title: `执行命令: ${commandData.cmd}`,
|
|
511
|
-
timeout: commandData.timeout,
|
|
512
|
-
waitAfterMs: commandData.wait
|
|
513
|
-
});
|
|
514
|
-
return result;
|
|
541
|
+
else if ("cmd" in action) {
|
|
542
|
+
result = await this.command(action);
|
|
515
543
|
}
|
|
516
|
-
|
|
517
|
-
|
|
544
|
+
else if ("package" in action) {
|
|
545
|
+
result = await this.getAppInfo(action);
|
|
518
546
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
success: false,
|
|
523
|
-
output: '',
|
|
524
|
-
error: '命令执行失败',
|
|
525
|
-
duration: 0
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
return result;
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
executeCodeEval(context, code, timeout) {
|
|
532
|
-
const evalInstance = require('eval');
|
|
533
|
-
return evalInstance(code, context, true);
|
|
534
|
-
}
|
|
535
|
-
executeCode(agentCode, context, timeout) {
|
|
536
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
537
|
-
// console.log('executeCode', agentCode);
|
|
538
|
-
// Interpreter.global = context;
|
|
539
|
-
// const evalFunc = getEvalInstance(context);
|
|
540
|
-
// 如果没有指定 timeout,直接执行
|
|
541
|
-
if (!timeout) {
|
|
542
|
-
return yield this.executeCodeEval(context, agentCode);
|
|
543
|
-
}
|
|
544
|
-
// 使用 timeout 执行 - 修复:await 应该在 Promise.race 上,而不是在内部的 promise 上
|
|
545
|
-
return yield Promise.race([
|
|
546
|
-
this.executeCodeEval(context, agentCode),
|
|
547
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`代码执行超时: ${timeout}ms`)), timeout))
|
|
548
|
-
]);
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* 获取应用信息
|
|
553
|
-
*/
|
|
554
|
-
getAppInfo(appInfoData) {
|
|
555
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
556
|
-
const startTime = Date.now();
|
|
557
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
558
|
-
try {
|
|
559
|
-
const action = {
|
|
560
|
-
title: `获取应用信息: ${appInfoData.package}`,
|
|
561
|
-
actions: [{
|
|
562
|
-
type: "get_app_info",
|
|
563
|
-
packageName: appInfoData.package
|
|
564
|
-
}]
|
|
565
|
-
};
|
|
566
|
-
yield this.client.executeAction(action);
|
|
567
|
-
return {
|
|
568
|
-
success: true,
|
|
569
|
-
data: { package: appInfoData.package },
|
|
570
|
-
duration: Date.now() - startTime
|
|
571
|
-
};
|
|
547
|
+
else if ("text" in action) {
|
|
548
|
+
// Text-only action (without elementId/resourceId) is for clipboard
|
|
549
|
+
result = await this.clipboard(action);
|
|
572
550
|
}
|
|
573
|
-
|
|
574
|
-
|
|
551
|
+
else {
|
|
552
|
+
result = await this.takeScreenshot(action);
|
|
575
553
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
error: '获取应用信息失败',
|
|
581
|
-
duration: Date.now() - startTime
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
return result;
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* 截图操作
|
|
589
|
-
*/
|
|
590
|
-
takeScreenshot() {
|
|
591
|
-
return __awaiter(this, arguments, void 0, function* (screenshotData = {}) {
|
|
592
|
-
const startTime = Date.now();
|
|
593
|
-
const result = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
594
|
-
try {
|
|
595
|
-
const options = {
|
|
596
|
-
format: screenshotData.format || 'png',
|
|
597
|
-
quality: screenshotData.quality,
|
|
598
|
-
scale: 0.10,
|
|
599
|
-
};
|
|
600
|
-
if (screenshotData.regionX !== undefined && screenshotData.regionY !== undefined &&
|
|
601
|
-
screenshotData.regionWidth !== undefined && screenshotData.regionHeight !== undefined) {
|
|
602
|
-
options.region = {
|
|
603
|
-
left: screenshotData.regionX,
|
|
604
|
-
top: screenshotData.regionY,
|
|
605
|
-
width: screenshotData.regionWidth,
|
|
606
|
-
height: screenshotData.regionHeight
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
const screenshot = yield this.client.screenShot(options);
|
|
610
|
-
// 检查服务器是否返回了失败响应
|
|
611
|
-
if (!screenshot || screenshot.success === false) {
|
|
612
|
-
throw new Error((screenshot === null || screenshot === void 0 ? void 0 : screenshot.errorMessage) || '截图失败');
|
|
613
|
-
}
|
|
614
|
-
return {
|
|
615
|
-
success: true,
|
|
616
|
-
data: screenshot,
|
|
617
|
-
duration: Date.now() - startTime
|
|
618
|
-
};
|
|
554
|
+
results.push(result);
|
|
555
|
+
if (!result.success) {
|
|
556
|
+
failureCount++;
|
|
557
|
+
this.logger.warn(`⚠️ Action ${index + 1} failed: ${result.error}`);
|
|
619
558
|
}
|
|
620
|
-
|
|
621
|
-
|
|
559
|
+
else {
|
|
560
|
+
successCount++;
|
|
561
|
+
this.logger.info(`✅ Action ${index + 1} succeeded`);
|
|
622
562
|
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
const errorResult = {
|
|
626
566
|
success: false,
|
|
627
|
-
error:
|
|
628
|
-
duration:
|
|
567
|
+
error: error instanceof Error ? error.message : String(error),
|
|
568
|
+
duration: 0,
|
|
569
|
+
timestamp: Date.now(),
|
|
629
570
|
};
|
|
571
|
+
results.push(errorResult);
|
|
572
|
+
failureCount++;
|
|
573
|
+
this.logger.error(`❌ Action ${index + 1} failed:`, error);
|
|
630
574
|
}
|
|
631
|
-
return result;
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* 执行操作序列 - 支持多个操作连续执行
|
|
636
|
-
*/
|
|
637
|
-
executeActions(actions) {
|
|
638
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
639
|
-
const results = [];
|
|
640
|
-
for (const [index, action] of actions.entries()) {
|
|
641
|
-
try {
|
|
642
|
-
console.log(`🔨 [Actions] 执行第 ${index + 1}/${actions.length} 个操作`);
|
|
643
|
-
let result;
|
|
644
|
-
if ('text' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
645
|
-
result = yield this.input(action);
|
|
646
|
-
}
|
|
647
|
-
else if ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action || 'targetX' in action || 'targetY' in action) {
|
|
648
|
-
result = yield this.click(action);
|
|
649
|
-
}
|
|
650
|
-
else if ('fromX' in action && 'fromY' in action && 'toX' in action && 'toY' in action) {
|
|
651
|
-
result = yield this.swipe(action);
|
|
652
|
-
}
|
|
653
|
-
else if ('key' in action) {
|
|
654
|
-
result = yield this.pressKey(action);
|
|
655
|
-
}
|
|
656
|
-
else if ('condition' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
657
|
-
result = yield this.wait(action);
|
|
658
|
-
}
|
|
659
|
-
else if ('multiple' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
660
|
-
const findResult = yield this.find(action);
|
|
661
|
-
result = {
|
|
662
|
-
success: findResult.success,
|
|
663
|
-
data: findResult,
|
|
664
|
-
duration: 0
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
else if ('cmd' in action) {
|
|
668
|
-
const cmdResult = yield this.executeCommand(action);
|
|
669
|
-
result = {
|
|
670
|
-
success: cmdResult.success,
|
|
671
|
-
data: cmdResult,
|
|
672
|
-
duration: cmdResult.duration
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
else if ('package' in action) {
|
|
676
|
-
result = yield this.getAppInfo(action);
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
result = yield this.takeScreenshot(action);
|
|
680
|
-
}
|
|
681
|
-
results.push(result);
|
|
682
|
-
// 记录操作结果但不抛出错误,让调用者决定如何处理失败的操作
|
|
683
|
-
if (!result.success) {
|
|
684
|
-
console.warn(`⚠️ [Actions] 第 ${index + 1} 个操作失败: ${result.error}`);
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
console.log(`✅ [Actions] 第 ${index + 1} 个操作执行成功`);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
catch (error) {
|
|
691
|
-
const errorResult = {
|
|
692
|
-
success: false,
|
|
693
|
-
error: error instanceof Error ? error.message : String(error),
|
|
694
|
-
duration: 0
|
|
695
|
-
};
|
|
696
|
-
results.push(errorResult);
|
|
697
|
-
console.error(`❌ [Actions] 第 ${index + 1} 个操作执行失败:`, error);
|
|
698
|
-
// 不再抛出错误,继续执行后续操作
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return results;
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Smart element finder with retry logic
|
|
706
|
-
*/
|
|
707
|
-
findElementWithRetry(selector_1) {
|
|
708
|
-
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000) {
|
|
709
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
710
|
-
try {
|
|
711
|
-
const result = yield this.client.findElement(selector, {
|
|
712
|
-
timeout: 3000,
|
|
713
|
-
visibleOnly: true,
|
|
714
|
-
maxResults: 1
|
|
715
|
-
});
|
|
716
|
-
if (result.elements.length > 0) {
|
|
717
|
-
return result.elements[0];
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
catch (error) {
|
|
721
|
-
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
|
|
722
|
-
}
|
|
723
|
-
if (i < maxRetries - 1) {
|
|
724
|
-
yield new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return null;
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Smart multiple elements finder with retry logic
|
|
732
|
-
*/
|
|
733
|
-
findElementsWithRetry(selector_1) {
|
|
734
|
-
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000, options) {
|
|
735
|
-
var _a, _b, _c;
|
|
736
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
737
|
-
try {
|
|
738
|
-
const result = yield this.client.findElement(selector, {
|
|
739
|
-
timeout: 3000,
|
|
740
|
-
visibleOnly: (_a = options === null || options === void 0 ? void 0 : options.visibleOnly) !== null && _a !== void 0 ? _a : true,
|
|
741
|
-
clickableOnly: (_b = options === null || options === void 0 ? void 0 : options.clickableOnly) !== null && _b !== void 0 ? _b : false,
|
|
742
|
-
multiple: true,
|
|
743
|
-
maxResults: (_c = options === null || options === void 0 ? void 0 : options.maxResults) !== null && _c !== void 0 ? _c : 10
|
|
744
|
-
});
|
|
745
|
-
if (result.elements.length > 0) {
|
|
746
|
-
console.log(`🔍 [FindElements] 找到 ${result.elements.length} 个元素`);
|
|
747
|
-
return result.elements;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
|
|
752
|
-
}
|
|
753
|
-
if (i < maxRetries - 1) {
|
|
754
|
-
yield new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
console.log(`❌ [FindElements] 未找到任何元素`);
|
|
758
|
-
return [];
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* 打印元素信息的工具方法
|
|
763
|
-
*/
|
|
764
|
-
printElementInfo(element, index) {
|
|
765
|
-
const prefix = index !== undefined ? `📋 元素 ${index + 1}:` : `📋 元素信息:`;
|
|
766
|
-
console.log(`\n${prefix}`);
|
|
767
|
-
console.log(` - Element ID: ${element.elementId}`);
|
|
768
|
-
console.log(` - Class Name: ${element.className}`);
|
|
769
|
-
console.log(` - Resource ID: ${element.resourceId}`);
|
|
770
|
-
console.log(` - Text: "${element.text}"`);
|
|
771
|
-
console.log(` - Describe: "${element.describe}"`);
|
|
772
|
-
console.log(` - Visible: ${element.visible}`);
|
|
773
|
-
console.log(` - Clickable: ${element.clickable}`);
|
|
774
|
-
console.log(` - Bounds: {left: ${element.bounds.left}, top: ${element.bounds.top}, right: ${element.bounds.right}, bottom: ${element.bounds.bottom}}`);
|
|
775
|
-
console.log(` - Size: ${element.bounds.right - element.bounds.left} x ${element.bounds.bottom - element.bounds.top}`);
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* 打印多个元素信息的工具方法
|
|
779
|
-
*/
|
|
780
|
-
printElementsInfo(elements, title) {
|
|
781
|
-
if (elements.length === 0) {
|
|
782
|
-
console.log('❌ 没有元素可以打印');
|
|
783
|
-
return;
|
|
784
575
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const visibleCount = elements.filter(e => e.visible).length;
|
|
792
|
-
const clickableCount = elements.filter(e => e.clickable).length;
|
|
793
|
-
const withTextCount = elements.filter(e => e.text && e.text.trim().length > 0).length;
|
|
794
|
-
console.log(`\n📊 统计信息:`);
|
|
795
|
-
console.log(` - 总共元素: ${elements.length}`);
|
|
796
|
-
console.log(` - 可见元素: ${visibleCount}`);
|
|
797
|
-
console.log(` - 可点击元素: ${clickableCount}`);
|
|
798
|
-
console.log(` - 有文本内容元素: ${withTextCount}`);
|
|
799
|
-
// 展示不同的文本内容
|
|
800
|
-
const uniqueTexts = [...new Set(elements.map(e => e.text).filter(text => text && text.trim().length > 0))];
|
|
801
|
-
if (uniqueTexts.length > 0) {
|
|
802
|
-
console.log(`\n📝 不同的文本内容:`);
|
|
803
|
-
uniqueTexts.forEach((text, index) => {
|
|
804
|
-
console.log(` ${index + 1}. "${text}"`);
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Click element by text content
|
|
810
|
-
*/
|
|
811
|
-
clickByText(text_1) {
|
|
812
|
-
return __awaiter(this, arguments, void 0, function* (text, exact = false) {
|
|
813
|
-
const selector = exact
|
|
814
|
-
? { text, clickable: false, visible: true }
|
|
815
|
-
: { textContains: text, clickable: false, visible: true };
|
|
816
|
-
const element = yield this.findElementWithRetry(selector);
|
|
817
|
-
if (!element) {
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
const clickAction = {
|
|
821
|
-
title: `点击文本: ${text}`,
|
|
822
|
-
actions: [{
|
|
823
|
-
type: "click",
|
|
824
|
-
target: { type: "elementId", value: element.elementId },
|
|
825
|
-
options: { waitAfterMs: 2000 }
|
|
826
|
-
}]
|
|
827
|
-
};
|
|
828
|
-
yield this.client.executeAction(clickAction);
|
|
829
|
-
return true;
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Input text into field
|
|
834
|
-
*/
|
|
835
|
-
inputText(selector, text, options) {
|
|
836
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
837
|
-
var _a, _b;
|
|
838
|
-
const element = yield this.findElementWithRetry(selector);
|
|
839
|
-
if (!element) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
const inputAction = {
|
|
843
|
-
title: `输入文本: ${text}`,
|
|
844
|
-
actions: [{
|
|
845
|
-
type: "input",
|
|
846
|
-
text,
|
|
847
|
-
target: { type: "elementId", value: element.elementId },
|
|
848
|
-
options: {
|
|
849
|
-
replaceExisting: (_a = options === null || options === void 0 ? void 0 : options.clear) !== null && _a !== void 0 ? _a : true,
|
|
850
|
-
hideKeyboardAfter: (_b = options === null || options === void 0 ? void 0 : options.hideKeyboard) !== null && _b !== void 0 ? _b : false
|
|
851
|
-
}
|
|
852
|
-
}]
|
|
853
|
-
};
|
|
854
|
-
yield this.client.executeAction(inputAction);
|
|
855
|
-
return true;
|
|
856
|
-
});
|
|
576
|
+
return {
|
|
577
|
+
results,
|
|
578
|
+
successCount,
|
|
579
|
+
failureCount,
|
|
580
|
+
totalDuration: Date.now() - startTime,
|
|
581
|
+
};
|
|
857
582
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
583
|
+
// ==================== Element Finder Methods ====================
|
|
584
|
+
/**
|
|
585
|
+
* Find a single element with retry logic
|
|
586
|
+
*
|
|
587
|
+
* @param selector - Element selector
|
|
588
|
+
* @param maxRetries - Maximum retry attempts (default: 3)
|
|
589
|
+
* @param retryDelay - Delay between retries in milliseconds (default: 1000)
|
|
590
|
+
* @returns Promise resolving to UIElement or null
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* const element = await shellx.findElement(
|
|
595
|
+
* { text: 'Submit', visible: true },
|
|
596
|
+
* 3,
|
|
597
|
+
* 1000
|
|
598
|
+
* );
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
async findElementWithRetry(selector, maxRetries = 3, retryDelay = 1000) {
|
|
602
|
+
return this.elementFinder.findElement(selector, { maxRetries, retryDelay });
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Find multiple elements with retry logic
|
|
606
|
+
*
|
|
607
|
+
* @param selector - Element selector
|
|
608
|
+
* @param maxRetries - Maximum retry attempts (default: 3)
|
|
609
|
+
* @param retryDelay - Delay between retries in milliseconds (default: 1000)
|
|
610
|
+
* @param options - Additional find options
|
|
611
|
+
* @returns Promise resolving to array of UIElements
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```typescript
|
|
615
|
+
* const elements = await shellx.findElementsWithRetry(
|
|
616
|
+
* { className: 'Button', visible: true },
|
|
617
|
+
* 3,
|
|
618
|
+
* 1000,
|
|
619
|
+
* { maxResults: 10 }
|
|
620
|
+
* );
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
async findElementsWithRetry(selector, maxRetries = 3, retryDelay = 1000, options) {
|
|
624
|
+
return this.elementFinder.findElements(selector, {
|
|
625
|
+
maxRetries,
|
|
626
|
+
retryDelay,
|
|
627
|
+
...options,
|
|
873
628
|
});
|
|
874
629
|
}
|
|
875
630
|
/**
|
|
876
631
|
* Wait for any of multiple elements to appear
|
|
632
|
+
* @deprecated Use waitAnyElement() instead for consistency
|
|
877
633
|
*/
|
|
878
|
-
waitForAnyElement(
|
|
879
|
-
return
|
|
880
|
-
const startTime = Date.now();
|
|
881
|
-
while (Date.now() - startTime < timeout) {
|
|
882
|
-
for (let i = 0; i < selectors.length; i++) {
|
|
883
|
-
try {
|
|
884
|
-
const result = yield this.client.findElement(selectors[i], {
|
|
885
|
-
timeout: 1000,
|
|
886
|
-
maxResults: 1,
|
|
887
|
-
visibleOnly: true
|
|
888
|
-
});
|
|
889
|
-
if (result.elements.length > 0) {
|
|
890
|
-
return { element: result.elements[0], selectorIndex: i };
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
catch (error) {
|
|
894
|
-
// Continue to next selector
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
yield new Promise(resolve => setTimeout(resolve, 500));
|
|
898
|
-
}
|
|
899
|
-
return null;
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Navigate through app using a series of clicks
|
|
904
|
-
*/
|
|
905
|
-
navigateByPath(textPath) {
|
|
906
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
907
|
-
try {
|
|
908
|
-
console.log('开始导航路径:', textPath.join(' → '));
|
|
909
|
-
for (const [index, text] of textPath.entries()) {
|
|
910
|
-
console.log(`导航步骤 ${index + 1}/${textPath.length}: 点击 "${text}"`);
|
|
911
|
-
yield this.clickByText(text);
|
|
912
|
-
// Wait a bit between clicks
|
|
913
|
-
yield new Promise(resolve => setTimeout(resolve, 1000));
|
|
914
|
-
}
|
|
915
|
-
console.log('✅ 导航完成');
|
|
916
|
-
return true;
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
console.error('❌ 导航失败:', error);
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Scroll to find element
|
|
926
|
-
*/
|
|
927
|
-
scrollToFindElement(selector_1) {
|
|
928
|
-
return __awaiter(this, arguments, void 0, function* (selector, maxScrolls = 5, direction = 'down') {
|
|
929
|
-
// First try to find without scrolling
|
|
930
|
-
let element = yield this.findElementWithRetry(selector, 1, 0);
|
|
931
|
-
if (element)
|
|
932
|
-
return element;
|
|
933
|
-
// Try scrolling to find
|
|
934
|
-
for (let i = 0; i < maxScrolls; i++) {
|
|
935
|
-
console.log(`滚动查找第 ${i + 1} 次...`);
|
|
936
|
-
// 获取屏幕信息来计算坐标
|
|
937
|
-
const screenInfo = yield this.client.getScreenInfo();
|
|
938
|
-
const centerX = (screenInfo.width || 1080) / 2;
|
|
939
|
-
const centerY = (screenInfo.height || 1920) / 2;
|
|
940
|
-
let from, to;
|
|
941
|
-
const distance = 400;
|
|
942
|
-
if (direction === 'up') {
|
|
943
|
-
from = { x: centerX, y: centerY + distance / 2 };
|
|
944
|
-
to = { x: centerX, y: centerY - distance / 2 };
|
|
945
|
-
}
|
|
946
|
-
else if (direction === 'down') {
|
|
947
|
-
from = { x: centerX, y: centerY - distance / 2 };
|
|
948
|
-
to = { x: centerX, y: centerY + distance / 2 };
|
|
949
|
-
}
|
|
950
|
-
else if (direction === 'left') {
|
|
951
|
-
from = { x: centerX + distance / 2, y: centerY };
|
|
952
|
-
to = { x: centerX - distance / 2, y: centerY };
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
from = { x: centerX - distance / 2, y: centerY };
|
|
956
|
-
to = { x: centerX + distance / 2, y: centerY };
|
|
957
|
-
}
|
|
958
|
-
yield this.swipe({
|
|
959
|
-
fromX: from.x,
|
|
960
|
-
fromY: from.y,
|
|
961
|
-
toX: to.x,
|
|
962
|
-
toY: to.y,
|
|
963
|
-
duration: 800
|
|
964
|
-
});
|
|
965
|
-
element = yield this.findElementWithRetry(selector, 1, 0);
|
|
966
|
-
if (element) {
|
|
967
|
-
console.log('✅ 滚动后找到元素');
|
|
968
|
-
return element;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
console.log('❌ 滚动后仍未找到元素');
|
|
972
|
-
return null;
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Execute shell command action with output monitoring
|
|
977
|
-
*/
|
|
978
|
-
executeShellCommand(command_1) {
|
|
979
|
-
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
|
|
980
|
-
const startTime = Date.now();
|
|
981
|
-
const title = options.title || `执行命令: ${command}`;
|
|
982
|
-
const timeout = options.timeout || 20000; // 增加默认超时时间到20秒
|
|
983
|
-
console.log(`🔨 [Shell] ${title}`);
|
|
984
|
-
console.log(`⏱️ 超时时间: ${timeout}ms`);
|
|
985
|
-
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
986
|
-
const commandKey = (0, uuid_1.v4)();
|
|
987
|
-
console.log(`🔑 [Shell] 生成命令键: ${commandKey}`);
|
|
988
|
-
// 注册命令 Promise
|
|
989
|
-
this.shellCommandPromises.set(commandKey, {
|
|
990
|
-
resolve,
|
|
991
|
-
reject,
|
|
992
|
-
startTime,
|
|
993
|
-
options,
|
|
994
|
-
output: '',
|
|
995
|
-
sessionOutputs: new Map(),
|
|
996
|
-
command
|
|
997
|
-
});
|
|
998
|
-
console.log(`📋 [Shell] 当前待处理命令数: ${this.shellCommandPromises.size}`);
|
|
999
|
-
// 设置超时
|
|
1000
|
-
const timeoutId = setTimeout(() => {
|
|
1001
|
-
if (this.shellCommandPromises.has(commandKey)) {
|
|
1002
|
-
console.log(`✅ [Shell] 命令 ${command} 执行完成`);
|
|
1003
|
-
const commandPromise = this.shellCommandPromises.get(commandKey);
|
|
1004
|
-
this.shellCommandPromises.delete(commandKey);
|
|
1005
|
-
resolve({
|
|
1006
|
-
success: true,
|
|
1007
|
-
output: commandPromise ? commandPromise.output.trim() : "",
|
|
1008
|
-
duration: Date.now() - startTime
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
}, timeout);
|
|
1012
|
-
try {
|
|
1013
|
-
const shellAction = {
|
|
1014
|
-
title,
|
|
1015
|
-
actions: [{
|
|
1016
|
-
type: "command",
|
|
1017
|
-
command,
|
|
1018
|
-
title: options.title
|
|
1019
|
-
}],
|
|
1020
|
-
options: {
|
|
1021
|
-
timeoutMs: timeout
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
// 发送命令
|
|
1025
|
-
yield this.client.sendMessageWithTaskId({ actions: shellAction }, 'command', commandKey, timeout);
|
|
1026
|
-
console.log(`📤 [Shell] 命令已发送: ${commandKey}`);
|
|
1027
|
-
}
|
|
1028
|
-
catch (error) {
|
|
1029
|
-
clearTimeout(timeoutId);
|
|
1030
|
-
this.shellCommandPromises.delete(commandKey);
|
|
1031
|
-
console.error(`❌ [Shell] 命令发送失败: ${command}`, error);
|
|
1032
|
-
reject(error);
|
|
1033
|
-
}
|
|
1034
|
-
}));
|
|
1035
|
-
});
|
|
634
|
+
async waitForAnyElement(selectors, timeout = 10000) {
|
|
635
|
+
return this.waitAnyElement(selectors, timeout);
|
|
1036
636
|
}
|
|
1037
637
|
/**
|
|
1038
|
-
*
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
*
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
output: '',
|
|
1085
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1086
|
-
duration: Date.now() - Date.now() // 简单的时间戳
|
|
1087
|
-
});
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
throw error;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
console.log(`✅ [Shell] 所有命令执行完成`);
|
|
1096
|
-
return results;
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
console.error(`❌ [Shell] 批量命令执行失败:`, error);
|
|
1100
|
-
throw error;
|
|
1101
|
-
}
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Common ADB commands helper
|
|
1106
|
-
*/
|
|
1107
|
-
adbCommand(command, options) {
|
|
1108
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1109
|
-
const adbCmd = command;
|
|
1110
|
-
return this.executeShellCommand(adbCmd, {
|
|
1111
|
-
title: (options === null || options === void 0 ? void 0 : options.title) || `ADB命令: ${command}`,
|
|
1112
|
-
timeout: options === null || options === void 0 ? void 0 : options.timeout,
|
|
1113
|
-
waitAfterMs: options === null || options === void 0 ? void 0 : options.waitAfterMs,
|
|
1114
|
-
onOutput: options === null || options === void 0 ? void 0 : options.onOutput,
|
|
1115
|
-
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
1116
|
-
expectedOutput: options === null || options === void 0 ? void 0 : options.expectedOutput,
|
|
1117
|
-
successPattern: options === null || options === void 0 ? void 0 : options.successPattern,
|
|
638
|
+
* Wait for any of multiple elements to appear
|
|
639
|
+
*
|
|
640
|
+
* @param selectors - Array of element selectors
|
|
641
|
+
* @param timeout - Maximum wait time in milliseconds (default: 10000)
|
|
642
|
+
* @returns Promise resolving to the first found element and its index, or null
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```typescript
|
|
646
|
+
* const result = await shellx.waitAnyElement(
|
|
647
|
+
* [
|
|
648
|
+
* { text: 'Submit' },
|
|
649
|
+
* { text: 'OK' },
|
|
650
|
+
* { text: 'Confirm' }
|
|
651
|
+
* ],
|
|
652
|
+
* 10000
|
|
653
|
+
* );
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
async waitAnyElement(selectors, timeout = 10000) {
|
|
657
|
+
return this.elementFinder.waitAnyElement(selectors, timeout);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Scroll to find an element
|
|
661
|
+
*
|
|
662
|
+
* @param selector - Element selector
|
|
663
|
+
* @param maxScrolls - Maximum scroll attempts (default: 5)
|
|
664
|
+
* @param direction - Scroll direction (default: 'down')
|
|
665
|
+
* @returns Promise resolving to UIElement or null
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```typescript
|
|
669
|
+
* const element = await shellx.scrollToFindElement(
|
|
670
|
+
* { text: 'Target' },
|
|
671
|
+
* 5,
|
|
672
|
+
* 'down'
|
|
673
|
+
* );
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
async scrollToFindElement(selector, maxScrolls = 5, direction = "down") {
|
|
677
|
+
return this.elementFinder.scrollToFindElement(selector, async (from, to) => {
|
|
678
|
+
await this.swipe({
|
|
679
|
+
fromX: from.x,
|
|
680
|
+
fromY: from.y,
|
|
681
|
+
toX: to.x,
|
|
682
|
+
toY: to.y,
|
|
683
|
+
duration: 800,
|
|
1118
684
|
});
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
685
|
+
}, { maxScrolls, direction });
|
|
686
|
+
}
|
|
687
|
+
// ==================== Shell Command Methods ====================
|
|
688
|
+
/**
|
|
689
|
+
* Handle shell output from WebSocket messages
|
|
690
|
+
*
|
|
691
|
+
* This method should be called in the WebSocket message handler.
|
|
692
|
+
*
|
|
693
|
+
* @param chunks - Shell output chunks
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```typescript
|
|
697
|
+
* const client = new ConnectionClient(deviceId, {
|
|
698
|
+
* onMessage: (message) => {
|
|
699
|
+
* if (message.chunks) {
|
|
700
|
+
* shellx.handleShellOutput(message.chunks);
|
|
701
|
+
* }
|
|
702
|
+
* }
|
|
703
|
+
* });
|
|
704
|
+
* ```
|
|
1123
705
|
*/
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
*
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
706
|
+
handleShellOutput(chunks) {
|
|
707
|
+
this.shellCommandExecutor.handleShellOutput(chunks);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Send raw WebSocket message (for advanced use cases)
|
|
711
|
+
*
|
|
712
|
+
* This method allows you to send raw WebSocket protocol messages directly,
|
|
713
|
+
* bypassing the high-level ShellX API. This is useful for:
|
|
714
|
+
* - Custom protocol operations not exposed by ShellX
|
|
715
|
+
* - Testing and debugging
|
|
716
|
+
* - Advanced integrations requiring low-level control
|
|
717
|
+
*
|
|
718
|
+
* @param message - Raw WebSocket client message (WsClient type)
|
|
719
|
+
* @returns Promise resolving to the server response
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* ```typescript
|
|
723
|
+
* // Send a custom find element request
|
|
724
|
+
* await shellx.sendRawMessage({
|
|
725
|
+
* findElement: {
|
|
726
|
+
* type: 'find',
|
|
727
|
+
* selector: { text: 'Submit' },
|
|
728
|
+
* options: { maxResults: 10 }
|
|
729
|
+
* }
|
|
730
|
+
* });
|
|
731
|
+
*
|
|
732
|
+
* // Send a custom action sequence
|
|
733
|
+
* await shellx.sendRawMessage({
|
|
734
|
+
* actions: [
|
|
735
|
+
* { action: 'Launch', app: 'com.example.app' },
|
|
736
|
+
* { action: 'Tap', element: [500, 1000] }
|
|
737
|
+
* ]
|
|
738
|
+
* });
|
|
739
|
+
*
|
|
740
|
+
* // Send screen info request
|
|
741
|
+
* await shellx.sendRawMessage({
|
|
742
|
+
* screenInfo: { keepScreenOn: true, wakeApp: true }
|
|
743
|
+
* });
|
|
744
|
+
* ```
|
|
745
|
+
*/
|
|
746
|
+
async sendRawMessage(message) {
|
|
747
|
+
await this.ensureConnectionReady();
|
|
748
|
+
return this.client.sendRawMessage(message);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Start ASR (Automatic Speech Recognition)
|
|
752
|
+
* @returns Promise that resolves when speech recognition has started
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```typescript
|
|
756
|
+
* await shellx.startSpeechRecognition();
|
|
757
|
+
* console.log('Speech recognition started');
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
async startSpeechRecognition() {
|
|
761
|
+
await this.ensureConnectionReady();
|
|
762
|
+
return this.client.startAsr();
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Stop ASR (Automatic Speech Recognition)
|
|
766
|
+
* @returns Promise that resolves when speech recognition has stopped
|
|
767
|
+
*
|
|
768
|
+
* @example
|
|
769
|
+
* ```typescript
|
|
770
|
+
* await shellx.stopSpeechRecognition();
|
|
771
|
+
* console.log('Speech recognition stopped');
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
774
|
+
async stopSpeechRecognition() {
|
|
775
|
+
await this.ensureConnectionReady();
|
|
776
|
+
return this.client.stopAsr();
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Speak text aloud using TTS (Text-to-Speech)
|
|
780
|
+
* @param text - The text to speak
|
|
781
|
+
* @returns Promise that resolves when TTS has started speaking
|
|
782
|
+
*
|
|
783
|
+
* @example
|
|
784
|
+
* ```typescript
|
|
785
|
+
* await shellx.speak('Hello, world!');
|
|
786
|
+
* ```
|
|
787
|
+
*/
|
|
788
|
+
async speak(text) {
|
|
789
|
+
await this.ensureConnectionReady();
|
|
790
|
+
return this.client.speak(text);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Stop TTS (Text-to-Speech)
|
|
794
|
+
* @returns Promise that resolves when TTS has stopped speaking
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* await shellx.stopSpeaking();
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
async stopSpeaking() {
|
|
802
|
+
await this.ensureConnectionReady();
|
|
803
|
+
return this.client.stopTts();
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Static factory method to create shell output handler
|
|
807
|
+
* @deprecated This method is kept for backward compatibility but is no longer needed
|
|
808
|
+
* @param shellx - The ShellX instance
|
|
809
|
+
* @returns Message handler function
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```typescript
|
|
813
|
+
* const shellx = new ShellX({ deviceId: 'device-id' });
|
|
814
|
+
* const handler = ShellX.createShellOutputHandler(shellx);
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
static createShellOutputHandler(shellx) {
|
|
818
|
+
return (message) => {
|
|
819
|
+
if (message.chunks) {
|
|
820
|
+
shellx.handleShellOutput(message.chunks);
|
|
1175
821
|
}
|
|
1176
|
-
}
|
|
822
|
+
};
|
|
1177
823
|
}
|
|
1178
824
|
}
|
|
1179
|
-
|
|
1180
|
-
/**
|
|
1181
|
-
* Create ShellX instance
|
|
1182
|
-
*/
|
|
1183
|
-
function createShellX(client) {
|
|
1184
|
-
return new ShellX(client);
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Create ShellX instance with automatic authentication and shell output monitoring
|
|
1188
|
-
* 自动处理ShellX.ai认证和连接,无需外部提供连接地址
|
|
1189
|
-
*/
|
|
1190
|
-
function createShellXWithShellMonitoring() {
|
|
1191
|
-
return __awaiter(this, arguments, void 0, function* (config = {}) {
|
|
1192
|
-
try {
|
|
1193
|
-
const shellx = new ShellX(null);
|
|
1194
|
-
const client = new index_1.default(config.deviceId, Object.assign(Object.assign({}, config), { onMessage: (message) => {
|
|
1195
|
-
if (message.chunks) {
|
|
1196
|
-
shellx.handleShellOutput(message.chunks);
|
|
1197
|
-
}
|
|
1198
|
-
if (config.onMessage) {
|
|
1199
|
-
config.onMessage(message);
|
|
1200
|
-
}
|
|
1201
|
-
}, onOpen: () => {
|
|
1202
|
-
// 调用原始的onOpen处理器
|
|
1203
|
-
if (config.onOpen) {
|
|
1204
|
-
config.onOpen();
|
|
1205
|
-
}
|
|
1206
|
-
} }));
|
|
1207
|
-
// 等待ShellX.ai服务连接完成
|
|
1208
|
-
console.log('⏳ [ShellX] 等待ShellX.ai服务连接...');
|
|
1209
|
-
yield client.waitForInitialization();
|
|
1210
|
-
// 绑定客户端到 shellx
|
|
1211
|
-
shellx.client = client;
|
|
1212
|
-
// 将 shellx 实例关联到客户端
|
|
1213
|
-
client.setShellX(shellx);
|
|
1214
|
-
console.log('🚀 [ShellX] 初始化完成,等待ShellX.ai服务响应...');
|
|
1215
|
-
return shellx;
|
|
1216
|
-
}
|
|
1217
|
-
catch (error) {
|
|
1218
|
-
console.error('❌ [ShellX] 初始化失败:', error);
|
|
1219
|
-
throw error;
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
825
|
+
//# sourceMappingURL=shellx.js.map
|