shellx-ai 1.0.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/README.md +335 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +14 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +398 -0
- package/dist/protocol.d.ts +319 -0
- package/dist/protocol.js +2 -0
- package/dist/shellx.d.ts +175 -0
- package/dist/shellx.js +866 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +37 -0
- package/package.json +61 -0
package/dist/shellx.js
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
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 = exports.AutomationHelpers = void 0;
|
|
16
|
+
exports.createShellX = createShellX;
|
|
17
|
+
exports.createHelpers = createShellX;
|
|
18
|
+
exports.createShellXWithShellMonitoring = createShellXWithShellMonitoring;
|
|
19
|
+
exports.createHelpersWithShellMonitoring = createShellXWithShellMonitoring;
|
|
20
|
+
const index_1 = __importDefault(require("./index"));
|
|
21
|
+
/**
|
|
22
|
+
* ShellX automation utilities for common patterns
|
|
23
|
+
*/
|
|
24
|
+
class ShellX {
|
|
25
|
+
constructor(client) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
this.shellOutputBuffers = new Map();
|
|
28
|
+
this.shellCommandPromises = new Map();
|
|
29
|
+
// 注意:由于 WebSocketTaskClient 的 config 是私有的,
|
|
30
|
+
// 监听 shell 输出需要在创建 WebSocketTaskClient 时设置 onMessage 回调
|
|
31
|
+
console.log('⚠️ [Shell] 注意:要监听 shell 输出,请在创建 WebSocketTaskClient 时设置 onMessage 回调');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 设置 shell 输出监听器(需要在创建 WebSocketTaskClient 时调用)
|
|
35
|
+
*/
|
|
36
|
+
static createShellOutputHandler(helpers) {
|
|
37
|
+
return (message) => {
|
|
38
|
+
// 处理 chunks 数据(pty 终端输出)
|
|
39
|
+
if (message.chunks) {
|
|
40
|
+
helpers.handleShellOutput(message.chunks);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 处理 shell 输出数据
|
|
46
|
+
*/
|
|
47
|
+
handleShellOutput(chunks) {
|
|
48
|
+
const [sid, sessionId, dataArrays] = chunks;
|
|
49
|
+
try {
|
|
50
|
+
// 将 Uint8Array 数组转换为字符串
|
|
51
|
+
let output = '';
|
|
52
|
+
for (const data of dataArrays) {
|
|
53
|
+
output += new TextDecoder().decode(data);
|
|
54
|
+
}
|
|
55
|
+
console.log(`📟 [Shell] 收到输出 (Session ${sessionId}): ${output.trim()}`);
|
|
56
|
+
// 为每个等待的命令累积输出
|
|
57
|
+
for (const [commandKey, commandPromise] of this.shellCommandPromises.entries()) {
|
|
58
|
+
// 检查是否该命令相关的输出
|
|
59
|
+
if (this.isOutputForCommand(output, commandPromise.command, sessionId)) {
|
|
60
|
+
// 存储该 session 的输出
|
|
61
|
+
if (!commandPromise.sessionOutputs.has(sessionId)) {
|
|
62
|
+
commandPromise.sessionOutputs.set(sessionId, '');
|
|
63
|
+
}
|
|
64
|
+
const currentSessionOutput = commandPromise.sessionOutputs.get(sessionId) || '';
|
|
65
|
+
commandPromise.sessionOutputs.set(sessionId, currentSessionOutput + output);
|
|
66
|
+
// 更新总输出
|
|
67
|
+
commandPromise.output = this.combineSessionOutputs(commandPromise.sessionOutputs);
|
|
68
|
+
console.log(`📊 [Shell] 命令 ${commandKey} 累积输出长度: ${commandPromise.output.length}`);
|
|
69
|
+
// 调用输出回调
|
|
70
|
+
if (commandPromise.options.onOutput) {
|
|
71
|
+
commandPromise.options.onOutput(output);
|
|
72
|
+
}
|
|
73
|
+
// 检查是否满足完成条件
|
|
74
|
+
this.checkCommandCompletion(commandKey, commandPromise, output);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(`❌ [Shell] 处理输出数据失败:`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 判断输出是否属于特定命令
|
|
84
|
+
*/
|
|
85
|
+
isOutputForCommand(output, command, sessionId) {
|
|
86
|
+
// 如果输出包含命令本身,说明是命令回显
|
|
87
|
+
if (output.includes(command)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// 如果输出包含提示符,说明是命令完成
|
|
91
|
+
if (this.containsPrompt(output)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// 如果有多个命令在等待,按时间顺序分配
|
|
95
|
+
const waitingCommands = Array.from(this.shellCommandPromises.keys());
|
|
96
|
+
if (waitingCommands.length === 1) {
|
|
97
|
+
return true; // 只有一个命令在等待,所有输出都属于它
|
|
98
|
+
}
|
|
99
|
+
// 多个命令时,根据 sessionId 和命令创建时间进行启发式匹配
|
|
100
|
+
return true; // 暂时返回 true,让所有命令都接收输出
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 合并多个 session 的输出
|
|
104
|
+
*/
|
|
105
|
+
combineSessionOutputs(sessionOutputs) {
|
|
106
|
+
const sessions = Array.from(sessionOutputs.keys()).sort();
|
|
107
|
+
let combined = '';
|
|
108
|
+
for (const sessionId of sessions) {
|
|
109
|
+
const sessionOutput = sessionOutputs.get(sessionId) || '';
|
|
110
|
+
combined += sessionOutput;
|
|
111
|
+
}
|
|
112
|
+
return combined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 检查输出是否包含提示符
|
|
116
|
+
*/
|
|
117
|
+
containsPrompt(output) {
|
|
118
|
+
const promptPatterns = [
|
|
119
|
+
/\$ $/,
|
|
120
|
+
/# $/,
|
|
121
|
+
/> $/,
|
|
122
|
+
/\w+@\w+.*\$ $/,
|
|
123
|
+
/shell@.*:.*\$ $/,
|
|
124
|
+
/houji:\/.*\$ $/,
|
|
125
|
+
/\d+\|\w+:\/.*\$ $/, // Android pattern like "127|bluejay:/ $"
|
|
126
|
+
/\w+:\/.*\$ $/ // Android pattern like "bluejay:/ $"
|
|
127
|
+
];
|
|
128
|
+
return promptPatterns.some(pattern => pattern.test(output));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 检查命令是否完成
|
|
132
|
+
*/
|
|
133
|
+
checkCommandCompletion(commandKey, commandPromise, newOutput) {
|
|
134
|
+
const { resolve, startTime, options, output, command } = commandPromise;
|
|
135
|
+
console.log(`🔍 [Shell] 检查命令 "${command}" 完成条件,当前输出长度: ${output.length}`);
|
|
136
|
+
// 检查成功模式
|
|
137
|
+
if (options.successPattern) {
|
|
138
|
+
const pattern = options.successPattern;
|
|
139
|
+
const isMatch = typeof pattern === 'string'
|
|
140
|
+
? output.includes(pattern)
|
|
141
|
+
: pattern.test(output);
|
|
142
|
+
if (isMatch) {
|
|
143
|
+
console.log(`✅ [Shell] 命令 "${command}" 成功完成 (匹配成功模式)`);
|
|
144
|
+
this.resolveCommand(commandKey, {
|
|
145
|
+
success: true,
|
|
146
|
+
output: output.trim(),
|
|
147
|
+
duration: Date.now() - startTime
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 检查错误模式
|
|
153
|
+
if (options.errorPattern) {
|
|
154
|
+
const pattern = options.errorPattern;
|
|
155
|
+
const isMatch = typeof pattern === 'string'
|
|
156
|
+
? output.includes(pattern)
|
|
157
|
+
: pattern.test(output);
|
|
158
|
+
if (isMatch) {
|
|
159
|
+
console.log(`❌ [Shell] 命令 "${command}" 执行失败 (匹配错误模式)`);
|
|
160
|
+
this.resolveCommand(commandKey, {
|
|
161
|
+
success: false,
|
|
162
|
+
output: output.trim(),
|
|
163
|
+
error: 'Command failed with error pattern',
|
|
164
|
+
duration: Date.now() - startTime
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 检查通用完成模式(如命令提示符)
|
|
170
|
+
// 这是最主要的完成检测逻辑,包括对没有输出的命令的支持
|
|
171
|
+
if (this.isCommandComplete(output, command)) {
|
|
172
|
+
console.log(`✅ [Shell] 命令 "${command}" 执行完成 (检测到提示符)`);
|
|
173
|
+
this.resolveCommand(commandKey, {
|
|
174
|
+
success: true,
|
|
175
|
+
output: output.trim(),
|
|
176
|
+
duration: Date.now() - startTime
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// 额外的完成检测:仅当有输出时才进行的检查
|
|
181
|
+
if (output.trim().length > 0) {
|
|
182
|
+
// 检查是否有错误指示器
|
|
183
|
+
if (this.hasErrorIndicators(output)) {
|
|
184
|
+
console.log(`❌ [Shell] 命令 "${command}" 可能执行失败 (检测到错误指示器)`);
|
|
185
|
+
// 不立即失败,继续等待完整的提示符
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// 检查是否包含命令和结果(备用检测方式)
|
|
189
|
+
if (output.includes(command) && this.containsPrompt(output)) {
|
|
190
|
+
console.log(`✅ [Shell] 命令 "${command}" 执行完成 (命令+提示符备用检测)`);
|
|
191
|
+
this.resolveCommand(commandKey, {
|
|
192
|
+
success: true,
|
|
193
|
+
output: output.trim(),
|
|
194
|
+
duration: Date.now() - startTime
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// 如果没有检测到完成条件,继续等待
|
|
200
|
+
console.log(`⏳ [Shell] 命令 "${command}" 继续等待完成...`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 检查输出是否包含错误指示器
|
|
204
|
+
*/
|
|
205
|
+
hasErrorIndicators(output) {
|
|
206
|
+
const errorPatterns = [
|
|
207
|
+
/error/i,
|
|
208
|
+
/failed/i,
|
|
209
|
+
/not found/i,
|
|
210
|
+
/permission denied/i,
|
|
211
|
+
/command not found/i,
|
|
212
|
+
/no such file/i,
|
|
213
|
+
/syntax error/i
|
|
214
|
+
];
|
|
215
|
+
return errorPatterns.some(pattern => pattern.test(output));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 判断命令是否完成(基于常见的提示符模式)
|
|
219
|
+
*/
|
|
220
|
+
isCommandComplete(output, command) {
|
|
221
|
+
// 常见的命令提示符模式
|
|
222
|
+
const promptPatterns = [
|
|
223
|
+
/\$ $/, // Unix shell prompt
|
|
224
|
+
/# $/, // Root shell prompt
|
|
225
|
+
/> $/, // Windows command prompt
|
|
226
|
+
/~\$ $/, // Home directory prompt
|
|
227
|
+
/\w+@\w+.*\$ $/, // User@host prompt
|
|
228
|
+
/shell@.*:.*\$ $/, // Android shell prompt
|
|
229
|
+
/houji:\/.*\$ $/, // Specific Android shell prompt
|
|
230
|
+
/\d+\|\w+:\/.*\$ $/, // Android pattern like "127|bluejay:/ $"
|
|
231
|
+
/\w+:\/.*\$ $/, // Android pattern like "bluejay:/ $"
|
|
232
|
+
/\[.*\]\$ $/, // Bracketed prompt
|
|
233
|
+
/\w+:\w+\$ $/, // Simple path prompt
|
|
234
|
+
/\w+> $/, // Simple greater-than prompt
|
|
235
|
+
/.*:\w+\$ $/, // Path-based prompt
|
|
236
|
+
/\w+@\w+:.* $/, // User@host with path
|
|
237
|
+
/\d+\|.*\$ $/, // Numbered prompt with pipe
|
|
238
|
+
/.*#\s*$/, // Hash prompt with optional space
|
|
239
|
+
/.*\$\s*$/ // Dollar prompt with optional space
|
|
240
|
+
];
|
|
241
|
+
// 分割输出为行,检查最后几行
|
|
242
|
+
const lines = output.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
243
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
244
|
+
// 清理最后一行的控制字符
|
|
245
|
+
const cleanLastLine = lastLine.replace(/\r/g, '').trim();
|
|
246
|
+
console.log(`🔍 [Shell] 检查命令 "${command}" 最后一行提示符: "${cleanLastLine}"`);
|
|
247
|
+
const hasPrompt = promptPatterns.some(pattern => {
|
|
248
|
+
const matches = pattern.test(cleanLastLine);
|
|
249
|
+
if (matches) {
|
|
250
|
+
console.log(`✅ [Shell] 命令 "${command}" 匹配到提示符模式: ${pattern}`);
|
|
251
|
+
}
|
|
252
|
+
return matches;
|
|
253
|
+
});
|
|
254
|
+
// 如果检测到提示符,则认为命令完成
|
|
255
|
+
// 不再要求输出必须包含命令本身,因为有些命令可能没有输出
|
|
256
|
+
if (hasPrompt) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
// 如果没有检测到提示符,但输出包含命令本身,可能命令还在执行中
|
|
260
|
+
// 这种情况下返回false,继续等待
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 解析命令 Promise
|
|
265
|
+
*/
|
|
266
|
+
resolveCommand(commandKey, result) {
|
|
267
|
+
const commandPromise = this.shellCommandPromises.get(commandKey);
|
|
268
|
+
if (commandPromise) {
|
|
269
|
+
console.log(`✅ [Shell] 解析命令 Promise: ${commandKey}`);
|
|
270
|
+
commandPromise.resolve(result);
|
|
271
|
+
this.shellCommandPromises.delete(commandKey);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log(`⚠️ [Shell] 未找到命令 Promise: ${commandKey}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 生成命令唯一标识
|
|
279
|
+
*/
|
|
280
|
+
generateCommandKey(command) {
|
|
281
|
+
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Smart element finder with retry logic
|
|
285
|
+
*/
|
|
286
|
+
findElementWithRetry(selector_1) {
|
|
287
|
+
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000) {
|
|
288
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
289
|
+
try {
|
|
290
|
+
const result = yield this.client.findElement(selector, {
|
|
291
|
+
timeout: 3000,
|
|
292
|
+
visibleOnly: true,
|
|
293
|
+
maxResults: 1
|
|
294
|
+
});
|
|
295
|
+
if (result.elements.length > 0) {
|
|
296
|
+
return result.elements[0];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
|
|
301
|
+
}
|
|
302
|
+
if (i < maxRetries - 1) {
|
|
303
|
+
yield new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Smart multiple elements finder with retry logic
|
|
311
|
+
*/
|
|
312
|
+
findElementsWithRetry(selector_1) {
|
|
313
|
+
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000, options) {
|
|
314
|
+
var _a, _b, _c;
|
|
315
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
316
|
+
try {
|
|
317
|
+
const result = yield this.client.findElement(selector, {
|
|
318
|
+
timeout: 3000,
|
|
319
|
+
visibleOnly: (_a = options === null || options === void 0 ? void 0 : options.visibleOnly) !== null && _a !== void 0 ? _a : true,
|
|
320
|
+
clickableOnly: (_b = options === null || options === void 0 ? void 0 : options.clickableOnly) !== null && _b !== void 0 ? _b : false,
|
|
321
|
+
multiple: true,
|
|
322
|
+
maxResults: (_c = options === null || options === void 0 ? void 0 : options.maxResults) !== null && _c !== void 0 ? _c : 10
|
|
323
|
+
});
|
|
324
|
+
if (result.elements.length > 0) {
|
|
325
|
+
console.log(`🔍 [FindElements] 找到 ${result.elements.length} 个元素`);
|
|
326
|
+
return result.elements;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
|
|
331
|
+
}
|
|
332
|
+
if (i < maxRetries - 1) {
|
|
333
|
+
yield new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
console.log(`❌ [FindElements] 未找到任何元素`);
|
|
337
|
+
return [];
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 打印元素信息的工具方法
|
|
342
|
+
*/
|
|
343
|
+
printElementInfo(element, index) {
|
|
344
|
+
const prefix = index !== undefined ? `📋 元素 ${index + 1}:` : `📋 元素信息:`;
|
|
345
|
+
console.log(`\n${prefix}`);
|
|
346
|
+
console.log(` - Element ID: ${element.elementId}`);
|
|
347
|
+
console.log(` - Class Name: ${element.className}`);
|
|
348
|
+
console.log(` - Resource ID: ${element.resourceId}`);
|
|
349
|
+
console.log(` - Text: "${element.text}"`);
|
|
350
|
+
console.log(` - Describe: "${element.describe}"`);
|
|
351
|
+
console.log(` - Visible: ${element.visible}`);
|
|
352
|
+
console.log(` - Clickable: ${element.clickable}`);
|
|
353
|
+
console.log(` - Bounds: {left: ${element.bounds.left}, top: ${element.bounds.top}, right: ${element.bounds.right}, bottom: ${element.bounds.bottom}}`);
|
|
354
|
+
console.log(` - Size: ${element.bounds.right - element.bounds.left} x ${element.bounds.bottom - element.bounds.top}`);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 打印多个元素信息的工具方法
|
|
358
|
+
*/
|
|
359
|
+
printElementsInfo(elements, title) {
|
|
360
|
+
if (elements.length === 0) {
|
|
361
|
+
console.log('❌ 没有元素可以打印');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.log(`\n${title || `✅ 找到 ${elements.length} 个元素`}:`);
|
|
365
|
+
elements.forEach((element, index) => {
|
|
366
|
+
this.printElementInfo(element, index);
|
|
367
|
+
});
|
|
368
|
+
// 统计信息
|
|
369
|
+
const visibleCount = elements.filter(e => e.visible).length;
|
|
370
|
+
const clickableCount = elements.filter(e => e.clickable).length;
|
|
371
|
+
const withTextCount = elements.filter(e => e.text && e.text.trim().length > 0).length;
|
|
372
|
+
console.log(`\n📊 统计信息:`);
|
|
373
|
+
console.log(` - 总共元素: ${elements.length}`);
|
|
374
|
+
console.log(` - 可见元素: ${visibleCount}`);
|
|
375
|
+
console.log(` - 可点击元素: ${clickableCount}`);
|
|
376
|
+
console.log(` - 有文本内容元素: ${withTextCount}`);
|
|
377
|
+
// 展示不同的文本内容
|
|
378
|
+
const uniqueTexts = [...new Set(elements.map(e => e.text).filter(text => text && text.trim().length > 0))];
|
|
379
|
+
if (uniqueTexts.length > 0) {
|
|
380
|
+
console.log(`\n📝 不同的文本内容:`);
|
|
381
|
+
uniqueTexts.forEach((text, index) => {
|
|
382
|
+
console.log(` ${index + 1}. "${text}"`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Click element by text content
|
|
388
|
+
*/
|
|
389
|
+
clickByText(text_1) {
|
|
390
|
+
return __awaiter(this, arguments, void 0, function* (text, exact = false) {
|
|
391
|
+
const selector = exact
|
|
392
|
+
? { text, clickable: false, visible: true }
|
|
393
|
+
: { textContains: text, clickable: false, visible: true };
|
|
394
|
+
const element = yield this.findElementWithRetry(selector);
|
|
395
|
+
if (!element) {
|
|
396
|
+
throw new Error(`未找到包含文本 "${text}" 的可点击元素`);
|
|
397
|
+
}
|
|
398
|
+
const clickAction = {
|
|
399
|
+
title: `点击文本: ${text}`,
|
|
400
|
+
actions: [{
|
|
401
|
+
type: "click",
|
|
402
|
+
target: { type: "elementId", value: element.elementId },
|
|
403
|
+
options: { waitAfterMs: 2000 }
|
|
404
|
+
}]
|
|
405
|
+
};
|
|
406
|
+
yield this.client.executeAction(clickAction);
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Input text into field
|
|
412
|
+
*/
|
|
413
|
+
inputText(selector, text, options) {
|
|
414
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
415
|
+
var _a, _b;
|
|
416
|
+
const element = yield this.findElementWithRetry(selector);
|
|
417
|
+
if (!element) {
|
|
418
|
+
throw new Error('未找到输入框元素');
|
|
419
|
+
}
|
|
420
|
+
const inputAction = {
|
|
421
|
+
title: `输入文本: ${text}`,
|
|
422
|
+
actions: [{
|
|
423
|
+
type: "input",
|
|
424
|
+
text,
|
|
425
|
+
target: { type: "elementId", value: element.elementId },
|
|
426
|
+
options: {
|
|
427
|
+
replaceExisting: (_a = options === null || options === void 0 ? void 0 : options.clear) !== null && _a !== void 0 ? _a : true,
|
|
428
|
+
hideKeyboardAfter: (_b = options === null || options === void 0 ? void 0 : options.hideKeyboard) !== null && _b !== void 0 ? _b : false
|
|
429
|
+
}
|
|
430
|
+
}]
|
|
431
|
+
};
|
|
432
|
+
yield this.client.executeAction(inputAction);
|
|
433
|
+
return true;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Swipe in a direction
|
|
438
|
+
*/
|
|
439
|
+
swipe(direction_1) {
|
|
440
|
+
return __awaiter(this, arguments, void 0, function* (direction, distance = 400, duration = 800) {
|
|
441
|
+
// Get screen info to calculate coordinates
|
|
442
|
+
const screenInfo = yield this.client.getScreenInfo();
|
|
443
|
+
const centerX = (screenInfo.width || 1080) / 2;
|
|
444
|
+
const centerY = (screenInfo.height || 1920) / 2;
|
|
445
|
+
let from;
|
|
446
|
+
let to;
|
|
447
|
+
switch (direction) {
|
|
448
|
+
case 'up':
|
|
449
|
+
from = { x: centerX, y: centerY + distance / 2 };
|
|
450
|
+
to = { x: centerX, y: centerY - distance / 2 };
|
|
451
|
+
break;
|
|
452
|
+
case 'down':
|
|
453
|
+
from = { x: centerX, y: centerY - distance / 2 };
|
|
454
|
+
to = { x: centerX, y: centerY + distance / 2 };
|
|
455
|
+
break;
|
|
456
|
+
case 'left':
|
|
457
|
+
from = { x: centerX + distance / 2, y: centerY };
|
|
458
|
+
to = { x: centerX - distance / 2, y: centerY };
|
|
459
|
+
break;
|
|
460
|
+
case 'right':
|
|
461
|
+
from = { x: centerX - distance / 2, y: centerY };
|
|
462
|
+
to = { x: centerX + distance / 2, y: centerY };
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const swipeAction = {
|
|
466
|
+
title: `向${direction}滑动`,
|
|
467
|
+
actions: [{
|
|
468
|
+
type: "swipe",
|
|
469
|
+
from,
|
|
470
|
+
to,
|
|
471
|
+
options: { durationMs: duration, waitAfterMs: 500 }
|
|
472
|
+
}]
|
|
473
|
+
};
|
|
474
|
+
yield this.client.executeAction(swipeAction);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Take screenshot and save info
|
|
479
|
+
*/
|
|
480
|
+
captureScreen(options) {
|
|
481
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
482
|
+
const screenshot = yield this.client.screenShot(options);
|
|
483
|
+
if (options === null || options === void 0 ? void 0 : options.saveInfo) {
|
|
484
|
+
console.log('截图信息:', {
|
|
485
|
+
格式: screenshot.format,
|
|
486
|
+
尺寸: screenshot.dimensions,
|
|
487
|
+
大小: `${Math.round(screenshot.imageData.length / 1024)}KB`,
|
|
488
|
+
时间: new Date(Number(screenshot.timestamp)).toLocaleString()
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return screenshot;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Wait for any of multiple elements to appear
|
|
496
|
+
*/
|
|
497
|
+
waitForAnyElement(selectors_1) {
|
|
498
|
+
return __awaiter(this, arguments, void 0, function* (selectors, timeout = 10000) {
|
|
499
|
+
const startTime = Date.now();
|
|
500
|
+
while (Date.now() - startTime < timeout) {
|
|
501
|
+
for (let i = 0; i < selectors.length; i++) {
|
|
502
|
+
try {
|
|
503
|
+
const result = yield this.client.findElement(selectors[i], {
|
|
504
|
+
timeout: 1000,
|
|
505
|
+
maxResults: 1,
|
|
506
|
+
visibleOnly: true
|
|
507
|
+
});
|
|
508
|
+
if (result.elements.length > 0) {
|
|
509
|
+
return { element: result.elements[0], selectorIndex: i };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
// Continue to next selector
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
yield new Promise(resolve => setTimeout(resolve, 500));
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Login helper for common login flows
|
|
523
|
+
*/
|
|
524
|
+
performLogin(usernameSelector, passwordSelector, loginButtonSelector, username, password) {
|
|
525
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
526
|
+
try {
|
|
527
|
+
console.log('开始登录流程...');
|
|
528
|
+
// Input username
|
|
529
|
+
yield this.inputText(usernameSelector, username, { clear: true });
|
|
530
|
+
console.log('✅ 用户名已输入');
|
|
531
|
+
// Input password
|
|
532
|
+
yield this.inputText(passwordSelector, password, { clear: true });
|
|
533
|
+
console.log('✅ 密码已输入');
|
|
534
|
+
// Click login button
|
|
535
|
+
const loginElement = yield this.findElementWithRetry(loginButtonSelector);
|
|
536
|
+
if (!loginElement) {
|
|
537
|
+
throw new Error('未找到登录按钮');
|
|
538
|
+
}
|
|
539
|
+
const loginAction = {
|
|
540
|
+
title: "执行登录",
|
|
541
|
+
actions: [{
|
|
542
|
+
type: "click",
|
|
543
|
+
target: { type: "elementId", value: loginElement.elementId },
|
|
544
|
+
options: { waitAfterMs: 2000 }
|
|
545
|
+
}]
|
|
546
|
+
};
|
|
547
|
+
yield this.client.executeAction(loginAction);
|
|
548
|
+
console.log('✅ 登录按钮已点击');
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
console.error('❌ 登录失败:', error);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Navigate through app using a series of clicks
|
|
559
|
+
*/
|
|
560
|
+
navigateByPath(textPath) {
|
|
561
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
562
|
+
try {
|
|
563
|
+
console.log('开始导航路径:', textPath.join(' → '));
|
|
564
|
+
for (const [index, text] of textPath.entries()) {
|
|
565
|
+
console.log(`导航步骤 ${index + 1}/${textPath.length}: 点击 "${text}"`);
|
|
566
|
+
yield this.clickByText(text);
|
|
567
|
+
// Wait a bit between clicks
|
|
568
|
+
yield new Promise(resolve => setTimeout(resolve, 1000));
|
|
569
|
+
}
|
|
570
|
+
console.log('✅ 导航完成');
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.error('❌ 导航失败:', error);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Scroll to find element
|
|
581
|
+
*/
|
|
582
|
+
scrollToFindElement(selector_1) {
|
|
583
|
+
return __awaiter(this, arguments, void 0, function* (selector, maxScrolls = 5, direction = 'down') {
|
|
584
|
+
// First try to find without scrolling
|
|
585
|
+
let element = yield this.findElementWithRetry(selector, 1, 0);
|
|
586
|
+
if (element)
|
|
587
|
+
return element;
|
|
588
|
+
// Try scrolling to find
|
|
589
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
590
|
+
console.log(`滚动查找第 ${i + 1} 次...`);
|
|
591
|
+
yield this.swipe(direction);
|
|
592
|
+
element = yield this.findElementWithRetry(selector, 1, 0);
|
|
593
|
+
if (element) {
|
|
594
|
+
console.log('✅ 滚动后找到元素');
|
|
595
|
+
return element;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
console.log('❌ 滚动后仍未找到元素');
|
|
599
|
+
return null;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Execute shell command action with output monitoring
|
|
604
|
+
*/
|
|
605
|
+
executeShellCommand(command_1) {
|
|
606
|
+
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
|
|
607
|
+
const startTime = Date.now();
|
|
608
|
+
const title = options.title || `执行命令: ${command}`;
|
|
609
|
+
const timeout = options.timeout || 20000; // 增加默认超时时间到20秒
|
|
610
|
+
console.log(`🔨 [Shell] ${title}`);
|
|
611
|
+
console.log(`⏱️ 超时时间: ${timeout}ms`);
|
|
612
|
+
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
613
|
+
const commandKey = this.generateCommandKey(command);
|
|
614
|
+
console.log(`🔑 [Shell] 生成命令键: ${commandKey}`);
|
|
615
|
+
// 注册命令 Promise
|
|
616
|
+
this.shellCommandPromises.set(commandKey, {
|
|
617
|
+
resolve,
|
|
618
|
+
reject,
|
|
619
|
+
startTime,
|
|
620
|
+
options,
|
|
621
|
+
output: '',
|
|
622
|
+
sessionOutputs: new Map(),
|
|
623
|
+
command
|
|
624
|
+
});
|
|
625
|
+
console.log(`📋 [Shell] 当前待处理命令数: ${this.shellCommandPromises.size}`);
|
|
626
|
+
// 设置超时
|
|
627
|
+
const timeoutId = setTimeout(() => {
|
|
628
|
+
if (this.shellCommandPromises.has(commandKey)) {
|
|
629
|
+
const commandPromise = this.shellCommandPromises.get(commandKey);
|
|
630
|
+
console.log(`⏰ [Shell] 命令超时: ${command}`);
|
|
631
|
+
// 即使超时,如果有输出也返回输出
|
|
632
|
+
if (commandPromise && commandPromise.output.trim().length > 0) {
|
|
633
|
+
console.log(`📤 [Shell] 超时但有输出,返回部分结果`);
|
|
634
|
+
this.shellCommandPromises.delete(commandKey);
|
|
635
|
+
resolve({
|
|
636
|
+
success: false,
|
|
637
|
+
output: commandPromise.output.trim(),
|
|
638
|
+
error: `Command timeout after ${timeout}ms, but got partial output`,
|
|
639
|
+
duration: Date.now() - startTime
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
this.shellCommandPromises.delete(commandKey);
|
|
644
|
+
reject(new Error(`Command timeout after ${timeout}ms`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}, timeout);
|
|
648
|
+
try {
|
|
649
|
+
// 构建 shell 命令操作
|
|
650
|
+
const shellAction = {
|
|
651
|
+
title,
|
|
652
|
+
actions: [{
|
|
653
|
+
type: "command",
|
|
654
|
+
command,
|
|
655
|
+
title: options.title
|
|
656
|
+
}],
|
|
657
|
+
options: {
|
|
658
|
+
timeoutMs: timeout
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
// 发送命令
|
|
662
|
+
yield this.client.executeAction(shellAction);
|
|
663
|
+
console.log(`📤 [Shell] 命令已发送: ${command}`);
|
|
664
|
+
// 等待命令完成(通过 chunks 数据流)
|
|
665
|
+
// Promise 将由 handleShellOutput 解析
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
clearTimeout(timeoutId);
|
|
669
|
+
this.shellCommandPromises.delete(commandKey);
|
|
670
|
+
console.error(`❌ [Shell] 命令发送失败: ${command}`, error);
|
|
671
|
+
reject(error);
|
|
672
|
+
}
|
|
673
|
+
}));
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Execute shell command with simple output (for backward compatibility)
|
|
678
|
+
*/
|
|
679
|
+
executeSimpleShellCommand(command, options) {
|
|
680
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
681
|
+
try {
|
|
682
|
+
const result = yield this.executeShellCommand(command, {
|
|
683
|
+
title: options === null || options === void 0 ? void 0 : options.title,
|
|
684
|
+
timeout: options === null || options === void 0 ? void 0 : options.timeout
|
|
685
|
+
});
|
|
686
|
+
// 如果设置了等待时间,则等待
|
|
687
|
+
if (options === null || options === void 0 ? void 0 : options.waitAfterMs) {
|
|
688
|
+
yield new Promise(resolve => setTimeout(resolve, options.waitAfterMs));
|
|
689
|
+
}
|
|
690
|
+
console.log(`✅ [Shell] 命令执行完成: ${command}`);
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
console.error(`❌ [Shell] 命令执行失败: ${command}`, error);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Execute multiple shell commands in sequence
|
|
701
|
+
*/
|
|
702
|
+
executeShellCommands(commands, options) {
|
|
703
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
704
|
+
const results = [];
|
|
705
|
+
try {
|
|
706
|
+
console.log(`🔨 [Shell] 开始执行 ${commands.length} 个命令`);
|
|
707
|
+
for (const [index, cmd] of commands.entries()) {
|
|
708
|
+
try {
|
|
709
|
+
const title = cmd.title || `命令 ${index + 1}/${commands.length}: ${cmd.command}`;
|
|
710
|
+
console.log(`🔨 [Shell] ${title}`);
|
|
711
|
+
const result = yield this.executeShellCommand(cmd.command, {
|
|
712
|
+
title,
|
|
713
|
+
timeout: options === null || options === void 0 ? void 0 : options.timeout,
|
|
714
|
+
waitAfterMs: cmd.waitAfterMs
|
|
715
|
+
});
|
|
716
|
+
results.push(result);
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
console.error(`❌ [Shell] 命令 ${index + 1} 执行失败:`, error);
|
|
720
|
+
if (options === null || options === void 0 ? void 0 : options.continueOnError) {
|
|
721
|
+
results.push({
|
|
722
|
+
success: false,
|
|
723
|
+
output: '',
|
|
724
|
+
error: error instanceof Error ? error.message : String(error),
|
|
725
|
+
duration: Date.now() - Date.now() // 简单的时间戳
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
console.log(`✅ [Shell] 所有命令执行完成`);
|
|
735
|
+
return results;
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
console.error(`❌ [Shell] 批量命令执行失败:`, error);
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Common ADB commands helper
|
|
745
|
+
*/
|
|
746
|
+
adbCommand(command, options) {
|
|
747
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
748
|
+
const adbCmd = command;
|
|
749
|
+
return this.executeShellCommand(adbCmd, {
|
|
750
|
+
title: (options === null || options === void 0 ? void 0 : options.title) || `ADB命令: ${command}`,
|
|
751
|
+
timeout: options === null || options === void 0 ? void 0 : options.timeout,
|
|
752
|
+
waitAfterMs: options === null || options === void 0 ? void 0 : options.waitAfterMs,
|
|
753
|
+
onOutput: options === null || options === void 0 ? void 0 : options.onOutput,
|
|
754
|
+
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
755
|
+
expectedOutput: options === null || options === void 0 ? void 0 : options.expectedOutput,
|
|
756
|
+
successPattern: options === null || options === void 0 ? void 0 : options.successPattern,
|
|
757
|
+
errorPattern: options === null || options === void 0 ? void 0 : options.errorPattern
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Device info commands
|
|
763
|
+
*/
|
|
764
|
+
getDeviceInfo() {
|
|
765
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
766
|
+
const commands = [
|
|
767
|
+
{ command: 'getprop ro.product.model', title: '获取设备型号' },
|
|
768
|
+
{ command: 'getprop ro.build.version.release', title: '获取Android版本' },
|
|
769
|
+
{ command: 'wm size', title: '获取屏幕尺寸' },
|
|
770
|
+
{ command: 'dumpsys battery', title: '获取电池信息' }
|
|
771
|
+
];
|
|
772
|
+
console.log('📱 [Device] 开始获取设备信息...');
|
|
773
|
+
try {
|
|
774
|
+
const results = yield this.executeShellCommands(commands, {
|
|
775
|
+
continueOnError: true,
|
|
776
|
+
timeout: 5000
|
|
777
|
+
});
|
|
778
|
+
console.log('📱 [Device] 设备信息获取完成');
|
|
779
|
+
return results;
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
console.error('❌ [Device] 获取设备信息失败:', error);
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
exports.ShellX = ShellX;
|
|
789
|
+
exports.AutomationHelpers = ShellX;
|
|
790
|
+
/**
|
|
791
|
+
* Create ShellX instance
|
|
792
|
+
*/
|
|
793
|
+
function createShellX(client) {
|
|
794
|
+
return new ShellX(client);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* 从ShellX.ai服务认证并获取WebSocket连接信息
|
|
798
|
+
*/
|
|
799
|
+
function authenticateDevice() {
|
|
800
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
801
|
+
const authKey = process.env.SHELLX_AUTH_KEY;
|
|
802
|
+
if (!authKey) {
|
|
803
|
+
throw new Error('SHELLX_AUTH_KEY environment variable is required');
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
console.log('🔑 [Auth] 正在认证设备...');
|
|
807
|
+
const response = yield fetch(`https://shellx.ai/api/device/${authKey}`, {
|
|
808
|
+
method: 'GET',
|
|
809
|
+
headers: {
|
|
810
|
+
'Content-Type': 'application/json',
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
if (!response.ok) {
|
|
814
|
+
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
|
|
815
|
+
}
|
|
816
|
+
const data = yield response.json();
|
|
817
|
+
if (data.status !== 'success') {
|
|
818
|
+
throw new Error(`Authentication failed: ${data.message}`);
|
|
819
|
+
}
|
|
820
|
+
console.log('✅ [Auth] 设备认证成功');
|
|
821
|
+
console.log(`📡 [Auth] 连接信息: ${data.machine}`);
|
|
822
|
+
return data.machine;
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
826
|
+
console.warn('⚠️ [Auth] 在线认证失败,使用本地默认连接:', errorMessage);
|
|
827
|
+
// 认证失败时使用默认的本地WebSocket URL
|
|
828
|
+
const fallbackUrl = `ws://127.0.0.1:9091/api/s/${authKey}`;
|
|
829
|
+
console.log(`🔄 [Auth] 使用默认连接: ${fallbackUrl}`);
|
|
830
|
+
return fallbackUrl;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Create ShellX instance with automatic authentication and shell output monitoring
|
|
836
|
+
* 自动处理认证和连接,无需外部提供WebSocket URL
|
|
837
|
+
*/
|
|
838
|
+
function createShellXWithShellMonitoring() {
|
|
839
|
+
return __awaiter(this, arguments, void 0, function* (config = {}) {
|
|
840
|
+
try {
|
|
841
|
+
// 认证并获取WebSocket URL
|
|
842
|
+
const wsUrl = yield authenticateDevice();
|
|
843
|
+
// 先创建 shellx 实例,但不绑定客户端
|
|
844
|
+
const shellx = new ShellX(null);
|
|
845
|
+
// 创建客户端时设置消息监听器
|
|
846
|
+
const client = new index_1.default(wsUrl, Object.assign(Object.assign({}, config), { onMessage: (message) => {
|
|
847
|
+
// 处理 chunks 数据(pty 终端输出)
|
|
848
|
+
if (message.chunks) {
|
|
849
|
+
shellx.handleShellOutput(message.chunks);
|
|
850
|
+
}
|
|
851
|
+
// 调用原始的消息处理器
|
|
852
|
+
if (config.onMessage) {
|
|
853
|
+
config.onMessage(message);
|
|
854
|
+
}
|
|
855
|
+
} }));
|
|
856
|
+
// 绑定客户端到 shellx
|
|
857
|
+
shellx.client = client;
|
|
858
|
+
console.log('🚀 [ShellX] 初始化完成,可以开始使用自动化功能');
|
|
859
|
+
return { client, shellx };
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
console.error('❌ [ShellX] 初始化失败:', error);
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|