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
package/dist/shellx.js
CHANGED
|
@@ -1,1109 +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
|
-
// 导入 WebSocketTaskClient 类
|
|
20
|
-
const index_1 = __importDefault(require("./index"));
|
|
21
|
-
const COMMAND_PTY_SID = 999;
|
|
22
1
|
/**
|
|
23
|
-
* 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
|
|
24
8
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
36
165
|
*/
|
|
37
166
|
getClient() {
|
|
38
167
|
return this.client;
|
|
39
168
|
}
|
|
40
169
|
/**
|
|
41
|
-
*
|
|
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
|
+
* ```
|
|
42
179
|
*/
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
// 处理 chunks 数据(pty 终端输出)
|
|
46
|
-
if (message.chunks) {
|
|
47
|
-
helpers.handleShellOutput(message.chunks);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
180
|
+
getLogLevel() {
|
|
181
|
+
return this.logger.getLogLevel();
|
|
50
182
|
}
|
|
51
183
|
/**
|
|
52
|
-
*
|
|
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
|
+
* ```
|
|
53
197
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (sessionId === COMMAND_PTY_SID) {
|
|
57
|
-
try {
|
|
58
|
-
// 将 Uint8Array 数组转换为字符串
|
|
59
|
-
let output = '';
|
|
60
|
-
for (const data of dataArrays) {
|
|
61
|
-
output += new TextDecoder().decode(data);
|
|
62
|
-
}
|
|
63
|
-
/*console.log(
|
|
64
|
-
`📟 [Shell] 收到输出 (Session ${sessionId}): ${output.trim()}`,
|
|
65
|
-
);*/
|
|
66
|
-
// 为每个等待的命令累积输出
|
|
67
|
-
for (const [commandKey, commandPromise,] of this.shellCommandPromises.entries()) {
|
|
68
|
-
if (!commandPromise.sessionOutputs.has(sessionId)) {
|
|
69
|
-
commandPromise.sessionOutputs.set(sessionId, '');
|
|
70
|
-
}
|
|
71
|
-
const currentSessionOutput = commandPromise.sessionOutputs.get(sessionId) || '';
|
|
72
|
-
commandPromise.sessionOutputs.set(sessionId, currentSessionOutput + output);
|
|
73
|
-
commandPromise.output = this.combineSessionOutputs(commandPromise.sessionOutputs);
|
|
74
|
-
/*console.log(
|
|
75
|
-
`📊 [Shell] 命令 ${commandKey} 累积输出长度: ${commandPromise.output.length}`,
|
|
76
|
-
);*/
|
|
77
|
-
// 调用输出回调(传递清理后的输出)
|
|
78
|
-
if (commandPromise.options.onOutput) {
|
|
79
|
-
const cleanOutput = this.cleanCommandOutput(output, commandPromise.command);
|
|
80
|
-
commandPromise.options.onOutput(cleanOutput);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
console.error(`❌ [Shell] 处理输出数据失败:`, error);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
198
|
+
setLogLevel(level) {
|
|
199
|
+
this.logger.setLogLevel(level);
|
|
88
200
|
}
|
|
89
201
|
/**
|
|
90
|
-
*
|
|
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
|
+
* ```
|
|
91
209
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
210
|
+
enableDebugLogging() {
|
|
211
|
+
this.logger.enableDebugLogging();
|
|
94
212
|
}
|
|
95
213
|
/**
|
|
96
|
-
*
|
|
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
|
+
* ```
|
|
97
221
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let combined = '';
|
|
101
|
-
for (const sessionId of sessions) {
|
|
102
|
-
const sessionOutput = sessionOutputs.get(sessionId) || '';
|
|
103
|
-
combined += sessionOutput;
|
|
104
|
-
}
|
|
105
|
-
return combined;
|
|
222
|
+
disableLogging() {
|
|
223
|
+
this.logger.disableLogging();
|
|
106
224
|
}
|
|
107
225
|
/**
|
|
108
|
-
*
|
|
226
|
+
* Ensure connection is ready before executing device operations
|
|
227
|
+
*
|
|
228
|
+
* @param timeout - Timeout in milliseconds (default: 10000ms)
|
|
229
|
+
* @throws Error if connection timeout
|
|
109
230
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const { retry = 3, delay = 1000, onRetry } = options;
|
|
113
|
-
for (let attempt = 1; attempt <= retry; attempt++) {
|
|
114
|
-
try {
|
|
115
|
-
return yield operation();
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (attempt === retry) {
|
|
119
|
-
throw error;
|
|
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
|
-
throw new Error('重试次数已用完');
|
|
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
|
-
|
|
135
|
-
return
|
|
136
|
-
elementId: selector.targetElementId,
|
|
137
|
-
resourceId: selector.targetResourceId,
|
|
138
|
-
className: selector.targetClass,
|
|
139
|
-
text: selector.targetText,
|
|
140
|
-
textContains: undefined,
|
|
141
|
-
visible: selector.visible,
|
|
142
|
-
clickable: selector.clickable
|
|
143
|
-
};
|
|
240
|
+
async sendChat(message) {
|
|
241
|
+
return this.client.sendChat(message);
|
|
144
242
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
});
|
|
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);
|
|
225
296
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
throw new Error('未找到目标元素');
|
|
246
|
-
}
|
|
247
|
-
target = { type: "elementId", value: element.elementId };
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
throw new Error('必须指定目标:targetElementId、targetResourceId、targetText或targetClass');
|
|
251
|
-
}
|
|
252
|
-
const action = {
|
|
253
|
-
title: `输入文本: ${inputData.text}`,
|
|
254
|
-
actions: [{
|
|
255
|
-
type: "input",
|
|
256
|
-
text: inputData.text,
|
|
257
|
-
target,
|
|
258
|
-
options: {
|
|
259
|
-
replaceExisting: (_a = inputData.clear) !== null && _a !== void 0 ? _a : true,
|
|
260
|
-
hideKeyboardAfter: (_b = inputData.hideKeyboard) !== null && _b !== void 0 ? _b : false,
|
|
261
|
-
waitAfterMs: inputData.wait || 500
|
|
262
|
-
}
|
|
263
|
-
}]
|
|
264
|
-
};
|
|
265
|
-
yield this.client.executeAction(action);
|
|
266
|
-
return {
|
|
267
|
-
success: true,
|
|
268
|
-
data: {
|
|
269
|
-
text: inputData.text,
|
|
270
|
-
targetElementId: inputData.targetElementId,
|
|
271
|
-
targetResourceId: inputData.targetResourceId,
|
|
272
|
-
targetText: inputData.targetText,
|
|
273
|
-
targetClass: inputData.targetClass
|
|
274
|
-
},
|
|
275
|
-
duration: Date.now() - startTime
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
throw new Error(`输入操作失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
-
}
|
|
281
|
-
}), { retry: inputData.retry, delay: 500 });
|
|
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,
|
|
282
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
|
+
}
|
|
327
|
+
return {
|
|
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,
|
|
344
|
+
};
|
|
283
345
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const from = { x: swipeData.fromX, y: swipeData.fromY };
|
|
293
|
-
const to = { x: swipeData.toX, y: swipeData.toY };
|
|
294
|
-
const action = {
|
|
295
|
-
title: `滑动操作: ${from.x},${from.y} → ${to.x},${to.y}`,
|
|
296
|
-
actions: [{
|
|
297
|
-
type: "swipe",
|
|
298
|
-
from,
|
|
299
|
-
to,
|
|
300
|
-
options: {
|
|
301
|
-
durationMs: swipeData.duration || 800,
|
|
302
|
-
waitAfterMs: swipeData.wait || 500
|
|
303
|
-
}
|
|
304
|
-
}]
|
|
305
|
-
};
|
|
306
|
-
yield this.client.executeAction(action);
|
|
307
|
-
return {
|
|
308
|
-
success: true,
|
|
309
|
-
data: { from, to, duration: swipeData.duration },
|
|
310
|
-
duration: Date.now() - startTime
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
throw new Error(`滑动操作失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
315
|
-
}
|
|
316
|
-
}), { retry: swipeData.retry, delay: 500 });
|
|
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,
|
|
317
354
|
});
|
|
355
|
+
this.getClient().appendExecutionLog(`✅ Command executed successfully - Output: ${result.output}`);
|
|
356
|
+
return {
|
|
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,
|
|
364
|
+
};
|
|
318
365
|
}
|
|
319
366
|
/**
|
|
320
|
-
*
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
catch (error) {
|
|
348
|
-
throw new Error(`按键操作失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
349
|
-
}
|
|
350
|
-
}), { retry: keyData.retry, delay: 500 });
|
|
351
|
-
});
|
|
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
|
+
};
|
|
352
394
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
};
|
|
409
482
|
}
|
|
410
483
|
/**
|
|
411
|
-
*
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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);
|
|
433
516
|
}
|
|
434
|
-
|
|
435
|
-
|
|
517
|
+
else if ("text" in action && ("elementId" in action || "resourceId" in action)) {
|
|
518
|
+
result = await this.input(action);
|
|
436
519
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
executeCommand(commandData) {
|
|
444
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
445
|
-
return this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
446
|
-
try {
|
|
447
|
-
const result = yield this.executeShellCommand(commandData.cmd, {
|
|
448
|
-
title: `执行命令: ${commandData.cmd}`,
|
|
449
|
-
timeout: commandData.timeout,
|
|
450
|
-
waitAfterMs: commandData.wait
|
|
451
|
-
});
|
|
452
|
-
return result;
|
|
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);
|
|
453
526
|
}
|
|
454
|
-
|
|
455
|
-
|
|
527
|
+
else if ("fromX" in action && "fromY" in action && "toX" in action && "toY" in action) {
|
|
528
|
+
result = await this.swipe(action);
|
|
456
529
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* 获取应用信息
|
|
462
|
-
*/
|
|
463
|
-
getAppInfo(appInfoData) {
|
|
464
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
465
|
-
const startTime = Date.now();
|
|
466
|
-
return this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
467
|
-
try {
|
|
468
|
-
const action = {
|
|
469
|
-
title: `获取应用信息: ${appInfoData.package}`,
|
|
470
|
-
actions: [{
|
|
471
|
-
type: "get_app_info",
|
|
472
|
-
packageName: appInfoData.package
|
|
473
|
-
}]
|
|
474
|
-
};
|
|
475
|
-
yield this.client.executeAction(action);
|
|
476
|
-
return {
|
|
477
|
-
success: true,
|
|
478
|
-
data: { package: appInfoData.package },
|
|
479
|
-
duration: Date.now() - startTime
|
|
480
|
-
};
|
|
530
|
+
else if ("key" in action) {
|
|
531
|
+
result = await this.press(action);
|
|
481
532
|
}
|
|
482
|
-
|
|
483
|
-
|
|
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);
|
|
484
536
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* 截图操作
|
|
490
|
-
*/
|
|
491
|
-
takeScreenshot() {
|
|
492
|
-
return __awaiter(this, arguments, void 0, function* (screenshotData = {}) {
|
|
493
|
-
const startTime = Date.now();
|
|
494
|
-
return this.withRetry(() => __awaiter(this, void 0, void 0, function* () {
|
|
495
|
-
try {
|
|
496
|
-
const options = {
|
|
497
|
-
format: screenshotData.format || 'png',
|
|
498
|
-
quality: screenshotData.quality,
|
|
499
|
-
scale: 0.10,
|
|
500
|
-
};
|
|
501
|
-
if (screenshotData.regionX !== undefined && screenshotData.regionY !== undefined &&
|
|
502
|
-
screenshotData.regionWidth !== undefined && screenshotData.regionHeight !== undefined) {
|
|
503
|
-
options.region = {
|
|
504
|
-
left: screenshotData.regionX,
|
|
505
|
-
top: screenshotData.regionY,
|
|
506
|
-
width: screenshotData.regionWidth,
|
|
507
|
-
height: screenshotData.regionHeight
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
const screenshot = yield this.client.screenShot(options);
|
|
511
|
-
return {
|
|
512
|
-
success: true,
|
|
513
|
-
data: screenshot,
|
|
514
|
-
duration: Date.now() - startTime
|
|
515
|
-
};
|
|
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);
|
|
516
540
|
}
|
|
517
|
-
|
|
518
|
-
|
|
541
|
+
else if ("cmd" in action) {
|
|
542
|
+
result = await this.command(action);
|
|
519
543
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* 执行操作序列 - 支持多个操作连续执行
|
|
525
|
-
*/
|
|
526
|
-
executeActions(actions) {
|
|
527
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
528
|
-
const results = [];
|
|
529
|
-
for (const [index, action] of actions.entries()) {
|
|
530
|
-
try {
|
|
531
|
-
console.log(`🔨 [Actions] 执行第 ${index + 1}/${actions.length} 个操作`);
|
|
532
|
-
let result;
|
|
533
|
-
if ('text' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
534
|
-
result = yield this.input(action);
|
|
535
|
-
}
|
|
536
|
-
else if ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action || 'targetX' in action || 'targetY' in action) {
|
|
537
|
-
result = yield this.click(action);
|
|
538
|
-
}
|
|
539
|
-
else if ('fromX' in action && 'fromY' in action && 'toX' in action && 'toY' in action) {
|
|
540
|
-
result = yield this.swipe(action);
|
|
541
|
-
}
|
|
542
|
-
else if ('key' in action) {
|
|
543
|
-
result = yield this.pressKey(action);
|
|
544
|
-
}
|
|
545
|
-
else if ('condition' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
546
|
-
result = yield this.wait(action);
|
|
547
|
-
}
|
|
548
|
-
else if ('multiple' in action && ('targetElementId' in action || 'targetResourceId' in action || 'targetText' in action || 'targetClass' in action)) {
|
|
549
|
-
const findResult = yield this.find(action);
|
|
550
|
-
result = {
|
|
551
|
-
success: findResult.success,
|
|
552
|
-
data: findResult,
|
|
553
|
-
duration: 0
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
else if ('cmd' in action) {
|
|
557
|
-
const cmdResult = yield this.executeCommand(action);
|
|
558
|
-
result = {
|
|
559
|
-
success: cmdResult.success,
|
|
560
|
-
data: cmdResult,
|
|
561
|
-
duration: cmdResult.duration
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
else if ('package' in action) {
|
|
565
|
-
result = yield this.getAppInfo(action);
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
result = yield this.takeScreenshot(action);
|
|
569
|
-
}
|
|
570
|
-
results.push(result);
|
|
571
|
-
// 如果操作失败且不允许继续,则抛出错误
|
|
572
|
-
if (!result.success) {
|
|
573
|
-
throw new Error(`操作 ${index + 1} 失败: ${result.error}`);
|
|
574
|
-
}
|
|
575
|
-
console.log(`✅ [Actions] 第 ${index + 1} 个操作执行成功`);
|
|
544
|
+
else if ("package" in action) {
|
|
545
|
+
result = await this.getAppInfo(action);
|
|
576
546
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
error: error instanceof Error ? error.message : String(error),
|
|
581
|
-
duration: 0
|
|
582
|
-
};
|
|
583
|
-
results.push(errorResult);
|
|
584
|
-
console.error(`❌ [Actions] 第 ${index + 1} 个操作执行失败:`, error);
|
|
585
|
-
throw error; // 停止执行后续操作
|
|
547
|
+
else if ("text" in action) {
|
|
548
|
+
// Text-only action (without elementId/resourceId) is for clipboard
|
|
549
|
+
result = await this.clipboard(action);
|
|
586
550
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Smart element finder with retry logic
|
|
593
|
-
*/
|
|
594
|
-
findElementWithRetry(selector_1) {
|
|
595
|
-
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000) {
|
|
596
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
597
|
-
try {
|
|
598
|
-
const result = yield this.client.findElement(selector, {
|
|
599
|
-
timeout: 3000,
|
|
600
|
-
visibleOnly: true,
|
|
601
|
-
maxResults: 1
|
|
602
|
-
});
|
|
603
|
-
if (result.elements.length > 0) {
|
|
604
|
-
return result.elements[0];
|
|
605
|
-
}
|
|
551
|
+
else {
|
|
552
|
+
result = await this.takeScreenshot(action);
|
|
606
553
|
}
|
|
607
|
-
|
|
608
|
-
|
|
554
|
+
results.push(result);
|
|
555
|
+
if (!result.success) {
|
|
556
|
+
failureCount++;
|
|
557
|
+
this.logger.warn(`⚠️ Action ${index + 1} failed: ${result.error}`);
|
|
609
558
|
}
|
|
610
|
-
|
|
611
|
-
|
|
559
|
+
else {
|
|
560
|
+
successCount++;
|
|
561
|
+
this.logger.info(`✅ Action ${index + 1} succeeded`);
|
|
612
562
|
}
|
|
613
563
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
const result = yield this.client.findElement(selector, {
|
|
626
|
-
timeout: 3000,
|
|
627
|
-
visibleOnly: (_a = options === null || options === void 0 ? void 0 : options.visibleOnly) !== null && _a !== void 0 ? _a : true,
|
|
628
|
-
clickableOnly: (_b = options === null || options === void 0 ? void 0 : options.clickableOnly) !== null && _b !== void 0 ? _b : false,
|
|
629
|
-
multiple: true,
|
|
630
|
-
maxResults: (_c = options === null || options === void 0 ? void 0 : options.maxResults) !== null && _c !== void 0 ? _c : 10
|
|
631
|
-
});
|
|
632
|
-
if (result.elements.length > 0) {
|
|
633
|
-
console.log(`🔍 [FindElements] 找到 ${result.elements.length} 个元素`);
|
|
634
|
-
return result.elements;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
catch (error) {
|
|
638
|
-
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
|
|
639
|
-
}
|
|
640
|
-
if (i < maxRetries - 1) {
|
|
641
|
-
yield new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
642
|
-
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
const errorResult = {
|
|
566
|
+
success: false,
|
|
567
|
+
error: error instanceof Error ? error.message : String(error),
|
|
568
|
+
duration: 0,
|
|
569
|
+
timestamp: Date.now(),
|
|
570
|
+
};
|
|
571
|
+
results.push(errorResult);
|
|
572
|
+
failureCount++;
|
|
573
|
+
this.logger.error(`❌ Action ${index + 1} failed:`, error);
|
|
643
574
|
}
|
|
644
|
-
console.log(`❌ [FindElements] 未找到任何元素`);
|
|
645
|
-
return [];
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* 打印元素信息的工具方法
|
|
650
|
-
*/
|
|
651
|
-
printElementInfo(element, index) {
|
|
652
|
-
const prefix = index !== undefined ? `📋 元素 ${index + 1}:` : `📋 元素信息:`;
|
|
653
|
-
console.log(`\n${prefix}`);
|
|
654
|
-
console.log(` - Element ID: ${element.elementId}`);
|
|
655
|
-
console.log(` - Class Name: ${element.className}`);
|
|
656
|
-
console.log(` - Resource ID: ${element.resourceId}`);
|
|
657
|
-
console.log(` - Text: "${element.text}"`);
|
|
658
|
-
console.log(` - Describe: "${element.describe}"`);
|
|
659
|
-
console.log(` - Visible: ${element.visible}`);
|
|
660
|
-
console.log(` - Clickable: ${element.clickable}`);
|
|
661
|
-
console.log(` - Bounds: {left: ${element.bounds.left}, top: ${element.bounds.top}, right: ${element.bounds.right}, bottom: ${element.bounds.bottom}}`);
|
|
662
|
-
console.log(` - Size: ${element.bounds.right - element.bounds.left} x ${element.bounds.bottom - element.bounds.top}`);
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* 打印多个元素信息的工具方法
|
|
666
|
-
*/
|
|
667
|
-
printElementsInfo(elements, title) {
|
|
668
|
-
if (elements.length === 0) {
|
|
669
|
-
console.log('❌ 没有元素可以打印');
|
|
670
|
-
return;
|
|
671
575
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const visibleCount = elements.filter(e => e.visible).length;
|
|
679
|
-
const clickableCount = elements.filter(e => e.clickable).length;
|
|
680
|
-
const withTextCount = elements.filter(e => e.text && e.text.trim().length > 0).length;
|
|
681
|
-
console.log(`\n📊 统计信息:`);
|
|
682
|
-
console.log(` - 总共元素: ${elements.length}`);
|
|
683
|
-
console.log(` - 可见元素: ${visibleCount}`);
|
|
684
|
-
console.log(` - 可点击元素: ${clickableCount}`);
|
|
685
|
-
console.log(` - 有文本内容元素: ${withTextCount}`);
|
|
686
|
-
// 展示不同的文本内容
|
|
687
|
-
const uniqueTexts = [...new Set(elements.map(e => e.text).filter(text => text && text.trim().length > 0))];
|
|
688
|
-
if (uniqueTexts.length > 0) {
|
|
689
|
-
console.log(`\n📝 不同的文本内容:`);
|
|
690
|
-
uniqueTexts.forEach((text, index) => {
|
|
691
|
-
console.log(` ${index + 1}. "${text}"`);
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Click element by text content
|
|
697
|
-
*/
|
|
698
|
-
clickByText(text_1) {
|
|
699
|
-
return __awaiter(this, arguments, void 0, function* (text, exact = false) {
|
|
700
|
-
const selector = exact
|
|
701
|
-
? { text, clickable: false, visible: true }
|
|
702
|
-
: { textContains: text, clickable: false, visible: true };
|
|
703
|
-
const element = yield this.findElementWithRetry(selector);
|
|
704
|
-
if (!element) {
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
const clickAction = {
|
|
708
|
-
title: `点击文本: ${text}`,
|
|
709
|
-
actions: [{
|
|
710
|
-
type: "click",
|
|
711
|
-
target: { type: "elementId", value: element.elementId },
|
|
712
|
-
options: { waitAfterMs: 2000 }
|
|
713
|
-
}]
|
|
714
|
-
};
|
|
715
|
-
yield this.client.executeAction(clickAction);
|
|
716
|
-
return true;
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Input text into field
|
|
721
|
-
*/
|
|
722
|
-
inputText(selector, text, options) {
|
|
723
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
724
|
-
var _a, _b;
|
|
725
|
-
const element = yield this.findElementWithRetry(selector);
|
|
726
|
-
if (!element) {
|
|
727
|
-
return false;
|
|
728
|
-
}
|
|
729
|
-
const inputAction = {
|
|
730
|
-
title: `输入文本: ${text}`,
|
|
731
|
-
actions: [{
|
|
732
|
-
type: "input",
|
|
733
|
-
text,
|
|
734
|
-
target: { type: "elementId", value: element.elementId },
|
|
735
|
-
options: {
|
|
736
|
-
replaceExisting: (_a = options === null || options === void 0 ? void 0 : options.clear) !== null && _a !== void 0 ? _a : true,
|
|
737
|
-
hideKeyboardAfter: (_b = options === null || options === void 0 ? void 0 : options.hideKeyboard) !== null && _b !== void 0 ? _b : false
|
|
738
|
-
}
|
|
739
|
-
}]
|
|
740
|
-
};
|
|
741
|
-
yield this.client.executeAction(inputAction);
|
|
742
|
-
return true;
|
|
743
|
-
});
|
|
576
|
+
return {
|
|
577
|
+
results,
|
|
578
|
+
successCount,
|
|
579
|
+
failureCount,
|
|
580
|
+
totalDuration: Date.now() - startTime,
|
|
581
|
+
};
|
|
744
582
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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,
|
|
760
628
|
});
|
|
761
629
|
}
|
|
762
630
|
/**
|
|
763
631
|
* Wait for any of multiple elements to appear
|
|
632
|
+
* @deprecated Use waitAnyElement() instead for consistency
|
|
764
633
|
*/
|
|
765
|
-
waitForAnyElement(
|
|
766
|
-
return
|
|
767
|
-
const startTime = Date.now();
|
|
768
|
-
while (Date.now() - startTime < timeout) {
|
|
769
|
-
for (let i = 0; i < selectors.length; i++) {
|
|
770
|
-
try {
|
|
771
|
-
const result = yield this.client.findElement(selectors[i], {
|
|
772
|
-
timeout: 1000,
|
|
773
|
-
maxResults: 1,
|
|
774
|
-
visibleOnly: true
|
|
775
|
-
});
|
|
776
|
-
if (result.elements.length > 0) {
|
|
777
|
-
return { element: result.elements[0], selectorIndex: i };
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
catch (error) {
|
|
781
|
-
// Continue to next selector
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
yield new Promise(resolve => setTimeout(resolve, 500));
|
|
785
|
-
}
|
|
786
|
-
return null;
|
|
787
|
-
});
|
|
634
|
+
async waitForAnyElement(selectors, timeout = 10000) {
|
|
635
|
+
return this.waitAnyElement(selectors, timeout);
|
|
788
636
|
}
|
|
789
637
|
/**
|
|
790
|
-
*
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Scroll to find element
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
else if (direction === 'left') {
|
|
838
|
-
from = { x: centerX + distance / 2, y: centerY };
|
|
839
|
-
to = { x: centerX - distance / 2, y: centerY };
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
from = { x: centerX - distance / 2, y: centerY };
|
|
843
|
-
to = { x: centerX + distance / 2, y: centerY };
|
|
844
|
-
}
|
|
845
|
-
yield this.swipe({
|
|
846
|
-
fromX: from.x,
|
|
847
|
-
fromY: from.y,
|
|
848
|
-
toX: to.x,
|
|
849
|
-
toY: to.y,
|
|
850
|
-
duration: 800
|
|
851
|
-
});
|
|
852
|
-
element = yield this.findElementWithRetry(selector, 1, 0);
|
|
853
|
-
if (element) {
|
|
854
|
-
console.log('✅ 滚动后找到元素');
|
|
855
|
-
return element;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
console.log('❌ 滚动后仍未找到元素');
|
|
859
|
-
return null;
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
/**
|
|
863
|
-
* Execute shell command action with output monitoring
|
|
864
|
-
*/
|
|
865
|
-
executeShellCommand(command_1) {
|
|
866
|
-
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
|
|
867
|
-
const startTime = Date.now();
|
|
868
|
-
const title = options.title || `执行命令: ${command}`;
|
|
869
|
-
const timeout = options.timeout || 20000; // 增加默认超时时间到20秒
|
|
870
|
-
console.log(`🔨 [Shell] ${title}`);
|
|
871
|
-
console.log(`⏱️ 超时时间: ${timeout}ms`);
|
|
872
|
-
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
873
|
-
const commandKey = (0, uuid_1.v4)();
|
|
874
|
-
console.log(`🔑 [Shell] 生成命令键: ${commandKey}`);
|
|
875
|
-
// 注册命令 Promise
|
|
876
|
-
this.shellCommandPromises.set(commandKey, {
|
|
877
|
-
resolve,
|
|
878
|
-
reject,
|
|
879
|
-
startTime,
|
|
880
|
-
options,
|
|
881
|
-
output: '',
|
|
882
|
-
sessionOutputs: new Map(),
|
|
883
|
-
command
|
|
884
|
-
});
|
|
885
|
-
console.log(`📋 [Shell] 当前待处理命令数: ${this.shellCommandPromises.size}`);
|
|
886
|
-
// 设置超时
|
|
887
|
-
const timeoutId = setTimeout(() => {
|
|
888
|
-
if (this.shellCommandPromises.has(commandKey)) {
|
|
889
|
-
console.log(`✅ [Shell] 命令 ${command} 执行完成`);
|
|
890
|
-
const commandPromise = this.shellCommandPromises.get(commandKey);
|
|
891
|
-
this.shellCommandPromises.delete(commandKey);
|
|
892
|
-
resolve({
|
|
893
|
-
success: true,
|
|
894
|
-
output: commandPromise ? commandPromise.output.trim() : "",
|
|
895
|
-
duration: Date.now() - startTime
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}, timeout);
|
|
899
|
-
try {
|
|
900
|
-
const shellAction = {
|
|
901
|
-
title,
|
|
902
|
-
actions: [{
|
|
903
|
-
type: "command",
|
|
904
|
-
command,
|
|
905
|
-
title: options.title
|
|
906
|
-
}],
|
|
907
|
-
options: {
|
|
908
|
-
timeoutMs: timeout
|
|
909
|
-
}
|
|
910
|
-
};
|
|
911
|
-
// 发送命令
|
|
912
|
-
yield this.client.sendMessageWithTaskId({ actions: shellAction }, 'command', commandKey, timeout);
|
|
913
|
-
console.log(`📤 [Shell] 命令已发送: ${commandKey}`);
|
|
914
|
-
}
|
|
915
|
-
catch (error) {
|
|
916
|
-
clearTimeout(timeoutId);
|
|
917
|
-
this.shellCommandPromises.delete(commandKey);
|
|
918
|
-
console.error(`❌ [Shell] 命令发送失败: ${command}`, error);
|
|
919
|
-
reject(error);
|
|
920
|
-
}
|
|
921
|
-
}));
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Execute shell command with simple output (for backward compatibility)
|
|
926
|
-
*/
|
|
927
|
-
executeSimpleShellCommand(command, options) {
|
|
928
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
929
|
-
try {
|
|
930
|
-
const result = yield this.executeShellCommand(command, {
|
|
931
|
-
title: options === null || options === void 0 ? void 0 : options.title,
|
|
932
|
-
timeout: options === null || options === void 0 ? void 0 : options.timeout
|
|
933
|
-
});
|
|
934
|
-
// 如果设置了等待时间,则等待
|
|
935
|
-
if (options === null || options === void 0 ? void 0 : options.waitAfterMs) {
|
|
936
|
-
yield new Promise(resolve => setTimeout(resolve, options.waitAfterMs));
|
|
937
|
-
}
|
|
938
|
-
console.log(`✅ [Shell] 命令执行完成: ${command}`);
|
|
939
|
-
return result;
|
|
940
|
-
}
|
|
941
|
-
catch (error) {
|
|
942
|
-
console.error(`❌ [Shell] 命令执行失败: ${command}`, error);
|
|
943
|
-
throw error;
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Execute multiple shell commands in sequence
|
|
949
|
-
*/
|
|
950
|
-
executeShellCommands(commands, options) {
|
|
951
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
952
|
-
const results = [];
|
|
953
|
-
try {
|
|
954
|
-
console.log(`🔨 [Shell] 开始执行 ${commands.length} 个命令`);
|
|
955
|
-
for (const [index, cmd] of commands.entries()) {
|
|
956
|
-
try {
|
|
957
|
-
const title = cmd.title || `命令 ${index + 1}/${commands.length}: ${cmd.command}`;
|
|
958
|
-
console.log(`🔨 [Shell] ${title}`);
|
|
959
|
-
const result = yield this.executeShellCommand(cmd.command, {
|
|
960
|
-
title,
|
|
961
|
-
timeout: options === null || options === void 0 ? void 0 : options.timeout,
|
|
962
|
-
waitAfterMs: cmd.waitAfterMs
|
|
963
|
-
});
|
|
964
|
-
results.push(result);
|
|
965
|
-
}
|
|
966
|
-
catch (error) {
|
|
967
|
-
console.error(`❌ [Shell] 命令 ${index + 1} 执行失败:`, error);
|
|
968
|
-
if (options === null || options === void 0 ? void 0 : options.continueOnError) {
|
|
969
|
-
results.push({
|
|
970
|
-
success: false,
|
|
971
|
-
output: '',
|
|
972
|
-
error: error instanceof Error ? error.message : String(error),
|
|
973
|
-
duration: Date.now() - Date.now() // 简单的时间戳
|
|
974
|
-
});
|
|
975
|
-
continue;
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
throw error;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
console.log(`✅ [Shell] 所有命令执行完成`);
|
|
983
|
-
return results;
|
|
984
|
-
}
|
|
985
|
-
catch (error) {
|
|
986
|
-
console.error(`❌ [Shell] 批量命令执行失败:`, error);
|
|
987
|
-
throw error;
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Common ADB commands helper
|
|
993
|
-
*/
|
|
994
|
-
adbCommand(command, options) {
|
|
995
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
996
|
-
const adbCmd = command;
|
|
997
|
-
return this.executeShellCommand(adbCmd, {
|
|
998
|
-
title: (options === null || options === void 0 ? void 0 : options.title) || `ADB命令: ${command}`,
|
|
999
|
-
timeout: options === null || options === void 0 ? void 0 : options.timeout,
|
|
1000
|
-
waitAfterMs: options === null || options === void 0 ? void 0 : options.waitAfterMs,
|
|
1001
|
-
onOutput: options === null || options === void 0 ? void 0 : options.onOutput,
|
|
1002
|
-
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
1003
|
-
expectedOutput: options === null || options === void 0 ? void 0 : options.expectedOutput,
|
|
1004
|
-
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,
|
|
1005
684
|
});
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
return results;
|
|
1027
|
-
}
|
|
1028
|
-
catch (error) {
|
|
1029
|
-
console.error('❌ [Device] 获取设备信息失败:', error);
|
|
1030
|
-
throw error;
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Execute key action (press a key)
|
|
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
|
+
* ```
|
|
1036
705
|
*/
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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);
|
|
1062
821
|
}
|
|
1063
|
-
}
|
|
822
|
+
};
|
|
1064
823
|
}
|
|
1065
824
|
}
|
|
1066
|
-
|
|
1067
|
-
/**
|
|
1068
|
-
* Create ShellX instance
|
|
1069
|
-
*/
|
|
1070
|
-
function createShellX(client) {
|
|
1071
|
-
return new ShellX(client);
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Create ShellX instance with automatic authentication and shell output monitoring
|
|
1075
|
-
* 自动处理ShellX.ai认证和连接,无需外部提供连接地址
|
|
1076
|
-
*/
|
|
1077
|
-
function createShellXWithShellMonitoring() {
|
|
1078
|
-
return __awaiter(this, arguments, void 0, function* (config = {}) {
|
|
1079
|
-
try {
|
|
1080
|
-
const shellx = new ShellX(null);
|
|
1081
|
-
const client = new index_1.default(config.deviceId, Object.assign(Object.assign({}, config), { onMessage: (message) => {
|
|
1082
|
-
if (message.chunks) {
|
|
1083
|
-
shellx.handleShellOutput(message.chunks);
|
|
1084
|
-
}
|
|
1085
|
-
if (config.onMessage) {
|
|
1086
|
-
config.onMessage(message);
|
|
1087
|
-
}
|
|
1088
|
-
}, onOpen: () => {
|
|
1089
|
-
// 调用原始的onOpen处理器
|
|
1090
|
-
if (config.onOpen) {
|
|
1091
|
-
config.onOpen();
|
|
1092
|
-
}
|
|
1093
|
-
} }));
|
|
1094
|
-
// 等待ShellX.ai服务连接完成
|
|
1095
|
-
console.log('⏳ [ShellX] 等待ShellX.ai服务连接...');
|
|
1096
|
-
yield client.waitForInitialization();
|
|
1097
|
-
// 绑定客户端到 shellx
|
|
1098
|
-
shellx.client = client;
|
|
1099
|
-
// 将 shellx 实例关联到客户端
|
|
1100
|
-
client.setShellX(shellx);
|
|
1101
|
-
console.log('🚀 [ShellX] 初始化完成,等待ShellX.ai服务响应...');
|
|
1102
|
-
return shellx;
|
|
1103
|
-
}
|
|
1104
|
-
catch (error) {
|
|
1105
|
-
console.error('❌ [ShellX] 初始化失败:', error);
|
|
1106
|
-
throw error;
|
|
1107
|
-
}
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
825
|
+
//# sourceMappingURL=shellx.js.map
|