UNPKG

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
"use strict"; 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; } }); }