shellx-ai
Version:
shellx is a powerful WebSocket-based client for controlling shell commands and UI automation on remote devices.
929 lines (928 loc) • 40.4 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShellX = exports.AutomationHelpers = void 0;
exports.createShellX = createShellX;
exports.createHelpers = createShellX;
exports.createShellXWithShellMonitoring = createShellXWithShellMonitoring;
exports.createHelpersWithShellMonitoring = createShellXWithShellMonitoring;
const uuid_1 = require("uuid");
const utils_1 = require("./utils");
// 导入 WebSocketTaskClient 类
const index_1 = __importDefault(require("./index"));
// 安全地获取环境变量,兼容浏览器和Node.js环境
let authKey = (0, utils_1.getEnvVar)('SHELLX_AUTH_KEY');
const COMMAND_PTY_SID = 999;
/**
* ShellX automation utilities for common patterns
*/
class ShellX {
constructor(client) {
this.client = client;
this.shellOutputBuffers = new Map();
this.shellCommandPromises = new Map();
// 注意:由于 WebSocketTaskClient 的 config 是私有的,
// 监听 shell 输出需要在创建 WebSocketTaskClient 时设置 onMessage 回调
console.log('⚠️ [Shell] 注意:要监听 shell 输出,请在创建 WebSocketTaskClient 时设置 onMessage 回调');
}
/**
* Get the WebSocket client instance
*/
getClient() {
return this.client;
}
/**
* 设置 shell 输出监听器(需要在创建 WebSocketTaskClient 时调用)
*/
static createShellOutputHandler(helpers) {
return (message) => {
// 处理 chunks 数据(pty 终端输出)
if (message.chunks) {
helpers.handleShellOutput(message.chunks);
}
};
}
/**
* 处理 shell 输出数据
*/
handleShellOutput(chunks) {
const [sessionId, len, dataArrays] = chunks;
if (sessionId === COMMAND_PTY_SID) {
try {
// 将 Uint8Array 数组转换为字符串
let output = '';
for (const data of dataArrays) {
output += new TextDecoder().decode(data);
}
console.log(`📟 [Shell] 收到输出 (Session ${sessionId}): ${output.trim()}`);
// 为每个等待的命令累积输出
for (const [commandKey, commandPromise] of this.shellCommandPromises.entries()) {
// 检查是否该命令相关的输出
if (this.isOutputForCommand(output, commandPromise.command, sessionId)) {
// 存储该 session 的输出
if (!commandPromise.sessionOutputs.has(sessionId)) {
commandPromise.sessionOutputs.set(sessionId, '');
}
const currentSessionOutput = commandPromise.sessionOutputs.get(sessionId) || '';
commandPromise.sessionOutputs.set(sessionId, currentSessionOutput + output);
// 更新总输出
commandPromise.output = this.combineSessionOutputs(commandPromise.sessionOutputs);
console.log(`📊 [Shell] 命令 ${commandKey} 累积输出长度: ${commandPromise.output.length}`);
// 调用输出回调(传递清理后的输出)
if (commandPromise.options.onOutput) {
const cleanOutput = this.cleanCommandOutput(output, commandPromise.command);
commandPromise.options.onOutput(cleanOutput);
}
// 检查是否满足完成条件
this.checkCommandCompletion(commandKey, commandPromise, output);
}
}
}
catch (error) {
console.error(`❌ [Shell] 处理输出数据失败:`, error);
}
}
}
/**
* 判断输出是否属于特定命令
*/
isOutputForCommand(output, command, sessionId) {
// 如果输出包含命令本身,说明是命令回显
if (output.includes(command)) {
return true;
}
// 如果有多个命令在等待,按时间顺序分配
const waitingCommands = Array.from(this.shellCommandPromises.keys());
if (waitingCommands.length === 1) {
return true; // 只有一个命令在等待,所有输出都属于它
}
// 多个命令时,根据 sessionId 和命令创建时间进行启发式匹配
return true; // 暂时返回 true,让所有命令都接收输出
}
/**
* 清理命令输出,去除输入的命令内容
*/
cleanCommandOutput(output, command) {
if (!output || !command) {
return output;
}
let cleanOutput = output;
// 去除命令回显(命令本身)
cleanOutput = cleanOutput.replace(new RegExp(this.escapeRegExp(command), 'g'), '');
// 去除可能的命令提示符前缀和后缀
const promptPatterns = [
/^\s*[^$#>\s]*[#$>]\s*/, // 匹配提示符前缀
/^\s*[^$#>\s]*@[^$#>\s]*[#$>]\s*/, // 匹配 user@host 格式
/^\s*[^$#>\s]*:\s*[^$#>\s]*[#$>]\s*/, // 匹配 path: 格式
/\s*[^$#>\s]*[#$>]\s*$/, // 匹配提示符后缀
/\s*[^$#>\s]*@[^$#>\s]*[#$>]\s*$/, // 匹配 user@host 后缀
/\s*[^$#>\s]*:\s*[^$#>\s]*[#$>]\s*$/, // 匹配 path: 后缀
/\s*\d+\|[^$#>\s]*[#$>]\s*$/, // 匹配 Android 格式后缀
];
for (const pattern of promptPatterns) {
cleanOutput = cleanOutput.replace(pattern, '');
}
// 去除多余的空行和空格
cleanOutput = cleanOutput.replace(/^\s+|\s+$/g, ''); // 去除首尾空白
cleanOutput = cleanOutput.replace(/\n\s*\n/g, '\n'); // 去除多余空行
// 如果清理后为空,返回空字符串
if (cleanOutput.trim().length === 0) {
return '';
}
console.log(`🧹 [Shell] 清理命令输出:`);
console.log(` 原始: "${output.trim()}"`);
console.log(` 清理后: "${cleanOutput.trim()}"`);
return cleanOutput;
}
/**
* 合并多个 session 的输出
*/
combineSessionOutputs(sessionOutputs) {
const sessions = Array.from(sessionOutputs.keys()).sort();
let combined = '';
for (const sessionId of sessions) {
const sessionOutput = sessionOutputs.get(sessionId) || '';
combined += sessionOutput;
}
return combined;
}
/**
* 转义正则表达式特殊字符
*/
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* 检查命令是否完成
*/
checkCommandCompletion(commandKey, commandPromise, newOutput) {
const { resolve, startTime, options, output, command } = commandPromise;
console.log(`🔍 [Shell] 检查命令 "${command}" 完成条件,当前输出长度: ${output.length}`);
if (this.isCommandComplete(output, command)) {
console.log(`✅ [Shell] 命令 "${command}" 执行完成 (检测到提示符)`);
this.resolveCommand(commandKey, {
success: true,
output: output.trim(),
duration: Date.now() - startTime
});
return;
}
console.log(`⏳ [Shell] 命令 "${command}" 继续等待完成...`);
}
/**
* 检查输出是否包含错误指示器
*/
hasErrorIndicators(output) {
const errorPatterns = [
/error/i,
/failed/i,
/not found/i,
/permission denied/i,
/command not found/i,
/no such file/i,
/syntax error/i
];
return errorPatterns.some(pattern => pattern.test(output));
}
/**
* 判断命令是否完成(基于时间)
*/
isCommandComplete(output, command) {
// 简化逻辑:只基于时间判断,不依赖提示符检测
// 命令完成由超时机制控制
return false; // 让超时机制处理命令完成
}
/**
* 解析命令 Promise
*/
resolveCommand(commandKey, result) {
const commandPromise = this.shellCommandPromises.get(commandKey);
if (commandPromise) {
console.log(`✅ [Shell] 解析命令 Promise: ${commandKey}`);
// 清理最终输出结果
const cleanResult = Object.assign(Object.assign({}, result), { output: this.cleanCommandOutput(result.output, commandPromise.command) });
commandPromise.resolve(cleanResult);
this.shellCommandPromises.delete(commandKey);
}
else {
console.log(`⚠️ [Shell] 未找到命令 Promise: ${commandKey}`);
}
}
/**
* 生成命令唯一标识
*/
generateCommandKey(command) {
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Smart element finder with retry logic
*/
findElementWithRetry(selector_1) {
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = yield this.client.findElement(selector, {
timeout: 3000,
visibleOnly: true,
maxResults: 1
});
if (result.elements.length > 0) {
return result.elements[0];
}
}
catch (error) {
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
}
if (i < maxRetries - 1) {
yield new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
return null;
});
}
/**
* Smart multiple elements finder with retry logic
*/
findElementsWithRetry(selector_1) {
return __awaiter(this, arguments, void 0, function* (selector, maxRetries = 3, retryDelay = 1000, options) {
var _a, _b, _c;
for (let i = 0; i < maxRetries; i++) {
try {
const result = yield this.client.findElement(selector, {
timeout: 3000,
visibleOnly: (_a = options === null || options === void 0 ? void 0 : options.visibleOnly) !== null && _a !== void 0 ? _a : true,
clickableOnly: (_b = options === null || options === void 0 ? void 0 : options.clickableOnly) !== null && _b !== void 0 ? _b : false,
multiple: true,
maxResults: (_c = options === null || options === void 0 ? void 0 : options.maxResults) !== null && _c !== void 0 ? _c : 10
});
if (result.elements.length > 0) {
console.log(`🔍 [FindElements] 找到 ${result.elements.length} 个元素`);
return result.elements;
}
}
catch (error) {
console.log(`查找尝试 ${i + 1}/${maxRetries} 失败:`, error);
}
if (i < maxRetries - 1) {
yield new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
console.log(`❌ [FindElements] 未找到任何元素`);
return [];
});
}
/**
* 打印元素信息的工具方法
*/
printElementInfo(element, index) {
const prefix = index !== undefined ? `📋 元素 ${index + 1}:` : `📋 元素信息:`;
console.log(`\n${prefix}`);
console.log(` - Element ID: ${element.elementId}`);
console.log(` - Class Name: ${element.className}`);
console.log(` - Resource ID: ${element.resourceId}`);
console.log(` - Text: "${element.text}"`);
console.log(` - Describe: "${element.describe}"`);
console.log(` - Visible: ${element.visible}`);
console.log(` - Clickable: ${element.clickable}`);
console.log(` - Bounds: {left: ${element.bounds.left}, top: ${element.bounds.top}, right: ${element.bounds.right}, bottom: ${element.bounds.bottom}}`);
console.log(` - Size: ${element.bounds.right - element.bounds.left} x ${element.bounds.bottom - element.bounds.top}`);
}
/**
* 打印多个元素信息的工具方法
*/
printElementsInfo(elements, title) {
if (elements.length === 0) {
console.log('❌ 没有元素可以打印');
return;
}
console.log(`\n${title || `✅ 找到 ${elements.length} 个元素`}:`);
elements.forEach((element, index) => {
this.printElementInfo(element, index);
});
// 统计信息
const visibleCount = elements.filter(e => e.visible).length;
const clickableCount = elements.filter(e => e.clickable).length;
const withTextCount = elements.filter(e => e.text && e.text.trim().length > 0).length;
console.log(`\n📊 统计信息:`);
console.log(` - 总共元素: ${elements.length}`);
console.log(` - 可见元素: ${visibleCount}`);
console.log(` - 可点击元素: ${clickableCount}`);
console.log(` - 有文本内容元素: ${withTextCount}`);
// 展示不同的文本内容
const uniqueTexts = [...new Set(elements.map(e => e.text).filter(text => text && text.trim().length > 0))];
if (uniqueTexts.length > 0) {
console.log(`\n📝 不同的文本内容:`);
uniqueTexts.forEach((text, index) => {
console.log(` ${index + 1}. "${text}"`);
});
}
}
/**
* Click element by text content
*/
clickByText(text_1) {
return __awaiter(this, arguments, void 0, function* (text, exact = false) {
const selector = exact
? { text, clickable: false, visible: true }
: { textContains: text, clickable: false, visible: true };
const element = yield this.findElementWithRetry(selector);
if (!element) {
return false;
}
const clickAction = {
title: `点击文本: ${text}`,
actions: [{
type: "click",
target: { type: "elementId", value: element.elementId },
options: { waitAfterMs: 2000 }
}]
};
yield this.client.executeAction(clickAction);
return true;
});
}
/**
* Input text into field
*/
inputText(selector, text, options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const element = yield this.findElementWithRetry(selector);
if (!element) {
return false;
}
const inputAction = {
title: `输入文本: ${text}`,
actions: [{
type: "input",
text,
target: { type: "elementId", value: element.elementId },
options: {
replaceExisting: (_a = options === null || options === void 0 ? void 0 : options.clear) !== null && _a !== void 0 ? _a : true,
hideKeyboardAfter: (_b = options === null || options === void 0 ? void 0 : options.hideKeyboard) !== null && _b !== void 0 ? _b : false
}
}]
};
yield this.client.executeAction(inputAction);
return true;
});
}
/**
* Swipe in a direction
*/
swipe(direction_1) {
return __awaiter(this, arguments, void 0, function* (direction, distance = 400, duration = 800) {
// Get screen info to calculate coordinates
const screenInfo = yield this.client.getScreenInfo();
const centerX = (screenInfo.width || 1080) / 2;
const centerY = (screenInfo.height || 1920) / 2;
let from;
let to;
switch (direction) {
case 'up':
from = { x: centerX, y: centerY + distance / 2 };
to = { x: centerX, y: centerY - distance / 2 };
break;
case 'down':
from = { x: centerX, y: centerY - distance / 2 };
to = { x: centerX, y: centerY + distance / 2 };
break;
case 'left':
from = { x: centerX + distance / 2, y: centerY };
to = { x: centerX - distance / 2, y: centerY };
break;
case 'right':
from = { x: centerX - distance / 2, y: centerY };
to = { x: centerX + distance / 2, y: centerY };
break;
}
const swipeAction = {
title: `向${direction}滑动`,
actions: [{
type: "swipe",
from,
to,
options: { durationMs: duration, waitAfterMs: 500 }
}]
};
yield this.client.executeAction(swipeAction);
});
}
/**
* Take screenshot and save info
*/
captureScreen(options) {
return __awaiter(this, void 0, void 0, function* () {
const screenshot = yield this.client.screenShot(options);
if (options === null || options === void 0 ? void 0 : options.saveInfo) {
console.log('截图信息:', {
格式: screenshot.format,
尺寸: screenshot.dimensions,
大小: `${Math.round(screenshot.imageData.length / 1024)}KB`,
时间: new Date(Number(screenshot.timestamp)).toLocaleString()
});
}
return screenshot;
});
}
/**
* Wait for any of multiple elements to appear
*/
waitForAnyElement(selectors_1) {
return __awaiter(this, arguments, void 0, function* (selectors, timeout = 10000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
for (let i = 0; i < selectors.length; i++) {
try {
const result = yield this.client.findElement(selectors[i], {
timeout: 1000,
maxResults: 1,
visibleOnly: true
});
if (result.elements.length > 0) {
return { element: result.elements[0], selectorIndex: i };
}
}
catch (error) {
// Continue to next selector
}
}
yield new Promise(resolve => setTimeout(resolve, 500));
}
return null;
});
}
/**
* Navigate through app using a series of clicks
*/
navigateByPath(textPath) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log('开始导航路径:', textPath.join(' → '));
for (const [index, text] of textPath.entries()) {
console.log(`导航步骤 ${index + 1}/${textPath.length}: 点击 "${text}"`);
yield this.clickByText(text);
// Wait a bit between clicks
yield new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('✅ 导航完成');
return true;
}
catch (error) {
console.error('❌ 导航失败:', error);
return false;
}
});
}
/**
* Scroll to find element
*/
scrollToFindElement(selector_1) {
return __awaiter(this, arguments, void 0, function* (selector, maxScrolls = 5, direction = 'down') {
// First try to find without scrolling
let element = yield this.findElementWithRetry(selector, 1, 0);
if (element)
return element;
// Try scrolling to find
for (let i = 0; i < maxScrolls; i++) {
console.log(`滚动查找第 ${i + 1} 次...`);
yield this.swipe(direction);
element = yield this.findElementWithRetry(selector, 1, 0);
if (element) {
console.log('✅ 滚动后找到元素');
return element;
}
}
console.log('❌ 滚动后仍未找到元素');
return null;
});
}
/**
* Execute shell command action with output monitoring
*/
executeShellCommand(command_1) {
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
const startTime = Date.now();
const title = options.title || `执行命令: ${command}`;
const timeout = options.timeout || 20000; // 增加默认超时时间到20秒
console.log(`🔨 [Shell] ${title}`);
console.log(`⏱️ 超时时间: ${timeout}ms`);
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const commandKey = (0, uuid_1.v4)();
console.log(`🔑 [Shell] 生成命令键: ${commandKey}`);
// 注册命令 Promise
this.shellCommandPromises.set(commandKey, {
resolve,
reject,
startTime,
options,
output: '',
sessionOutputs: new Map(),
command
});
console.log(`📋 [Shell] 当前待处理命令数: ${this.shellCommandPromises.size}`);
// 设置超时
const timeoutId = setTimeout(() => {
if (this.shellCommandPromises.has(commandKey)) {
const commandPromise = this.shellCommandPromises.get(commandKey);
this.shellCommandPromises.delete(commandKey);
if (commandPromise && commandPromise.output.trim().length > 0) {
resolve({
success: true,
output: commandPromise.output.trim(),
duration: Date.now() - startTime
});
}
else {
resolve({
success: true,
output: "",
error: `Command timeout after ${timeout}ms, but got partial output`,
duration: Date.now() - startTime
});
}
}
}, timeout);
try {
// 构建 shell 命令操作
const shellAction = {
title,
actions: [{
type: "command",
command,
title: options.title
}],
options: {
timeoutMs: timeout
}
};
// 发送命令
yield this.client.executeAction(shellAction, commandKey);
console.log(`📤 [Shell] 命令已发送: ${commandKey}`);
}
catch (error) {
clearTimeout(timeoutId);
this.shellCommandPromises.delete(commandKey);
console.error(`❌ [Shell] 命令发送失败: ${command}`, error);
reject(error);
}
}));
});
}
/**
* Execute shell command with simple output (for backward compatibility)
*/
executeSimpleShellCommand(command, options) {
return __awaiter(this, void 0, void 0, function* () {
try {
const result = yield this.executeShellCommand(command, {
title: options === null || options === void 0 ? void 0 : options.title,
timeout: options === null || options === void 0 ? void 0 : options.timeout
});
// 如果设置了等待时间,则等待
if (options === null || options === void 0 ? void 0 : options.waitAfterMs) {
yield new Promise(resolve => setTimeout(resolve, options.waitAfterMs));
}
console.log(`✅ [Shell] 命令执行完成: ${command}`);
return result;
}
catch (error) {
console.error(`❌ [Shell] 命令执行失败: ${command}`, error);
throw error;
}
});
}
/**
* Execute multiple shell commands in sequence
*/
executeShellCommands(commands, options) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
try {
console.log(`🔨 [Shell] 开始执行 ${commands.length} 个命令`);
for (const [index, cmd] of commands.entries()) {
try {
const title = cmd.title || `命令 ${index + 1}/${commands.length}: ${cmd.command}`;
console.log(`🔨 [Shell] ${title}`);
const result = yield this.executeShellCommand(cmd.command, {
title,
timeout: options === null || options === void 0 ? void 0 : options.timeout,
waitAfterMs: cmd.waitAfterMs
});
results.push(result);
}
catch (error) {
console.error(`❌ [Shell] 命令 ${index + 1} 执行失败:`, error);
if (options === null || options === void 0 ? void 0 : options.continueOnError) {
results.push({
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - Date.now() // 简单的时间戳
});
continue;
}
else {
throw error;
}
}
}
console.log(`✅ [Shell] 所有命令执行完成`);
return results;
}
catch (error) {
console.error(`❌ [Shell] 批量命令执行失败:`, error);
throw error;
}
});
}
/**
* Common ADB commands helper
*/
adbCommand(command, options) {
return __awaiter(this, void 0, void 0, function* () {
const adbCmd = command;
return this.executeShellCommand(adbCmd, {
title: (options === null || options === void 0 ? void 0 : options.title) || `ADB命令: ${command}`,
timeout: options === null || options === void 0 ? void 0 : options.timeout,
waitAfterMs: options === null || options === void 0 ? void 0 : options.waitAfterMs,
onOutput: options === null || options === void 0 ? void 0 : options.onOutput,
onError: options === null || options === void 0 ? void 0 : options.onError,
expectedOutput: options === null || options === void 0 ? void 0 : options.expectedOutput,
successPattern: options === null || options === void 0 ? void 0 : options.successPattern,
});
});
}
/**
* Device info commands
*/
getDeviceInfo() {
return __awaiter(this, void 0, void 0, function* () {
const commands = [
{ command: 'getprop ro.product.model', title: '获取设备型号' },
{ command: 'getprop ro.build.version.release', title: '获取Android版本' },
{ command: 'wm size', title: '获取屏幕尺寸' },
{ command: 'dumpsys battery', title: '获取电池信息' }
];
console.log('📱 [Device] 开始获取设备信息...');
try {
const results = yield this.executeShellCommands(commands, {
continueOnError: true,
timeout: 5000
});
console.log('📱 [Device] 设备信息获取完成');
return results;
}
catch (error) {
console.error('❌ [Device] 获取设备信息失败:', error);
throw error;
}
});
}
/**
* Execute key action (press a key)
*/
executeKeyAction(keyCode_1) {
return __awaiter(this, arguments, void 0, function* (keyCode, options = {}) {
try {
console.log(`🔑 [Key] 执行按键操作: ${keyCode}${options.longPress ? ' (长按)' : ''}`);
const keyAction = {
title: `按键: ${keyCode}${options.longPress ? ' (长按)' : ''}`,
actions: [{
type: "key",
keyCode,
options: {
longPress: options.longPress
}
}]
};
yield this.client.executeAction(keyAction);
// 如果设置了等待时间,则等待
if (options.waitAfterMs) {
yield new Promise(resolve => setTimeout(resolve, options.waitAfterMs));
}
console.log(`✅ [Key] 按键操作完成: ${keyCode}`);
return true;
}
catch (error) {
console.error(`❌ [Key] 按键操作失败: ${keyCode}`, error);
return false;
}
});
}
}
exports.ShellX = ShellX;
exports.AutomationHelpers = ShellX;
/**
* Create ShellX instance
*/
function createShellX(client) {
return new ShellX(client);
}
/**
* 获取ofetch实例
*/
function getfetch() {
return __awaiter(this, void 0, void 0, function* () {
try {
// @ts-ignore - Dynamic import may not have types
const { ofetch } = yield Promise.resolve().then(() => __importStar(require('ofetch')));
// 创建配置好的ofetch实例
const fetchInstance = ofetch.create({
timeout: 10000,
retry: 1,
retryDelay: 1000,
headers: {
'User-Agent': 'ShellX/1.0.1'
},
onRequest({ request, options }) {
console.log(`🌐 [Fetch] 请求: ${options.method || 'GET'} ${request}`);
},
onResponse({ response }) {
console.log(`📡 [Fetch] 响应: ${response.status} ${response.statusText}`);
},
onRequestError({ error }) {
console.error('❌ [Fetch] 请求错误:', error.message);
},
onResponseError({ response }) {
console.error(`❌ [Fetch] 响应错误: ${response.status} ${response.statusText}`);
}
});
return fetchInstance;
}
catch (error) {
console.warn('⚠️ [Auth] ofetch不可用,使用降级方案');
return createFallbackFetch();
}
});
}
/**
* 降级fetch实现
*/
function createFallbackFetch() {
return (url, options) => __awaiter(this, void 0, void 0, function* () {
// 尝试使用全局fetch
if (typeof globalThis.fetch !== 'undefined') {
console.log(`🌐 [Fetch] 请求: ${(options === null || options === void 0 ? void 0 : options.method) || 'GET'} ${url}`);
const response = yield globalThis.fetch(url, options);
console.log(`📡 [Fetch] 响应: ${response.status} ${response.statusText}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Node.js环境降级
try {
const { default: fetch } = yield Promise.resolve().then(() => __importStar(require('node-fetch')));
console.log(`🌐 [Fetch] 请求: ${(options === null || options === void 0 ? void 0 : options.method) || 'GET'} ${url}`);
const response = yield fetch(url, options);
console.log(`📡 [Fetch] 响应: ${response.status} ${response.statusText}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
catch (fetchError) {
throw new Error(`Fetch not available: ${fetchError.message}`);
}
});
}
/**
* 从ShellX.ai服务认证并获取WebSocket连接信息
*/
function authenticateDevice(deviceId) {
return __awaiter(this, void 0, void 0, function* () {
const fallbackUrl = `ws://127.0.0.1:9091/api/s/${deviceId}`;
// 1. 优先检测本地服务
try {
// fetch超时实现
const fetchWithTimeout = (url, options, timeout = 1000) => Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
const localResp = yield fetchWithTimeout('http://127.0.0.1:9091/info', { method: 'GET', headers: { 'Accept': 'application/json' } }, 1000);
if (localResp.ok) {
const info = yield localResp.json();
if (info && (info.status === 'ok' || info.status === 1)) {
console.log('✅ [Auth] 本地ShellX服务可用,直接使用本地服务:', fallbackUrl);
return fallbackUrl;
}
}
}
catch (e) {
// 本地不可用,继续走远程
}
// 2. 远程认证逻辑
authKey = (0, utils_1.getEnvVar)('SHELLX_AUTH_KEY');
if (!authKey) {
throw new Error('SHELLX_AUTH_KEY environment variable is required');
}
try {
console.log('🔑 [Auth] 正在认证设备...');
// 获取ofetch实例
const fetchFn = yield getfetch();
// ofetch自动处理JSON解析和错误处理
const data = yield fetchFn(`https://shellx.ai/api/device/${deviceId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
const jsonData = JSON.parse(data);
console.log('✅ [Auth] ShellX.ai设备认证成功');
console.log(`📡 [Auth] 设备ID: ${jsonData.authenticate}`);
console.log(`📡 [Auth] ShellX.ai服务地址: ${jsonData.machine}`);
console.log(`📡 [Auth] 注册时间: ${jsonData.registered_at}`);
console.log(`📡 [Auth] 最后更新: ${jsonData.last_updated}`);
return jsonData.machine + '/api/s/' + jsonData.authenticate;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn('⚠️ [Auth] ShellX.ai在线认证失败,使用本地服务:', errorMessage);
console.log(`🔄 [Auth] 使用本地ShellX服务: ${fallbackUrl}`);
return fallbackUrl;
}
});
}
/**
* Create ShellX instance with automatic authentication and shell output monitoring
* 自动处理ShellX.ai认证和连接,无需外部提供连接地址
*/
function createShellXWithShellMonitoring() {
return __awaiter(this, arguments, void 0, function* (config = {}) {
try {
const wsUrl = yield authenticateDevice(config.deviceId);
const shellx = new ShellX(null);
const client = new index_1.default(wsUrl, Object.assign(Object.assign({}, config), { onMessage: (message) => {
if (message.chunks) {
shellx.handleShellOutput(message.chunks);
}
if (config.onMessage) {
config.onMessage(message);
}
}, onOpen: () => {
// 调用原始的onOpen处理器
if (config.onOpen) {
config.onOpen();
}
} }));
// 等待ShellX.ai服务连接完成
console.log('⏳ [ShellX] 等待ShellX.ai服务连接...');
yield client.waitForInitialization();
// 绑定客户端到 shellx
shellx.client = client;
// 将 shellx 实例关联到客户端
client.setShellX(shellx);
console.log('🚀 [ShellX] 初始化完成,等待ShellX.ai服务响应...');
return { client, shellx };
}
catch (error) {
console.error('❌ [ShellX] 初始化失败:', error);
throw error;
}
});
}
;