AutoxJS实战进阶:从脚本稳定到性能优化的深度避坑手册

如果你已经用AutoxJS写过几个自动化脚本,尝到了解放双手的甜头,那么接下来大概率会遇到一些“成长的烦恼”。按钮死活点不到、脚本运行一半卡死、日志混乱难以排查……这些问题看似琐碎,却足以让一个精心设计的自动化流程功亏一篑。今天我们不谈基础操作,只聚焦于那些在真实项目打磨中才会暴露的深水区问题,分享一套经过实战检验的解决方案与高阶技巧。

1. 界面元素定位:超越id()text()的稳定策略

几乎所有AutoxJS脚本的起点都是与屏幕上的元素交互。当id("btn_confirm").findOne()这种理想化的定位方式失效时,很多开发者会陷入反复调整选择器的困境。实际上,界面元素的稳定性受安卓版本、应用框架、动态加载机制等多重因素影响,需要一套更鲁棒的定位策略。

1.1 应对clickablefalse的组件点击

在高版本安卓系统或某些特定UI框架(如Flutter、React Native)中,组件的clickable属性可能被设置为false,导致直接通过click()方法无效。此时,获取组件边界坐标进行模拟点击是更可靠的方法。

// 传统方式可能失效
let targetBtn = id("submit_button").findOne();
targetBtn.click(); // 可能无反应

// 改用坐标点击
let targetBtn = text("提交").findOne();
if (targetBtn) {
    let bounds = targetBtn.bounds();
    click(bounds.centerX(), bounds.centerY());
    log("通过坐标点击成功");
}

但坐标点击也有其局限性:屏幕分辨率变化、UI缩放、动态布局都可能让坐标失效。一个更健壮的方案是结合多种定位属性,并添加容错机制。

function robustClick(selector, fallbackText = null) {
    let element = selector.findOne(3000); // 等待3秒
    if (!element && fallbackText) {
        // 如果首选选择器失败,尝试备用文本
        element = text(fallbackText).findOne(2000);
    }
    
    if (element) {
        // 先尝试直接点击
        if (element.click()) {
            log("直接点击成功");
            return true;
        }
        
        // 直接点击失败,尝试坐标点击
        let bounds = element.bounds();
        if (bounds) {
            click(bounds.centerX(), bounds.centerY());
            sleep(500); // 点击后等待
            // 验证点击是否生效(例如通过界面变化)
            return true;
        }
    }
    
    log("元素定位或点击失败");
    return false;
}

// 使用示例
robustClick(id("action_button"), "确认");

1.2 动态加载内容的等待策略

现代应用大量使用异步加载和懒加载技术,这意味着你需要的元素可能不会立即出现在屏幕上。waitFor()方法虽然常用,但在某些复杂场景下可能不够可靠。

注意:过度使用sleep()进行固定时间等待会降低脚本效率,而轮询检查又会增加性能开销。需要根据具体场景平衡。

我推荐使用自适应等待策略,结合多种条件判断:

function waitForElement(selectors, timeout = 10000, checkInterval = 500) {
    let startTime = Date.now();
    let foundElement = null;
    
    while (Date.now() - startTime < timeout) {
        // 尝试多种选择器
        for (let selector of selectors) {
            let element = selector.findOne(100);
            if (element) {
                // 额外检查元素是否真正可用
                if (isElementReady(element)) {
                    foundElement = element;
                    break;
                }
            }
        }
        
        if (foundElement) break;
        sleep(checkInterval);
    }
    
    return foundElement;
}

function isElementReady(element) {
    // 检查元素是否在屏幕可见区域
    let bounds = element.bounds();
    if (!bounds) return false;
    
    // 检查元素是否被遮挡(简单版本)
    let screenHeight = device.height;
    let screenWidth = device.width;
    
    return bounds.top >= 0 && 
           bounds.left >= 0 && 
           bounds.bottom <= screenHeight && 
           bounds.right <= screenWidth;
}

// 使用示例:多种方式定位同一个元素
let submitButton = waitForElement([
    id("btn_submit"),
    textContains("提交"),
    className("android.widget.Button").clickable(true)
], 15000);

if (submitButton) {
    submitButton.click();
}

1.3 复杂列表和滚动界面的元素查找

在长列表或可滚动视图中定位特定项目时,简单的findOne()可能无法找到屏幕外的元素。这时需要结合滚动操作:

function findInScrollable(selector, scrollableSelector, maxScrolls = 10) {
    let scrollable = scrollableSelector.findOne(3000);
    if (!scrollable) {
        log("未找到可滚动容器");
        return null;
    }
    
    for (let i = 0; i < maxScrolls; i++) {
        // 在当前屏幕查找
        let target = selector.findOne(1000);
        if (target) return target;
        
        // 未找到,向下滚动
        let scrollBounds = scrollable.bounds();
        let startY = scrollBounds.bottom - 100;
        let endY = scrollBounds.top + 100;
        
        swipe(scrollBounds.centerX(), startY, 
              scrollBounds.centerX(), endY, 500);
        sleep(800); // 等待滚动动画
        
        // 防止无限滚动,检查是否已到底部
        if (i > 2 && isScrollAtBottom(scrollable)) {
            break;
        }
    }
    
    return null;
}

function isScrollAtBottom(scrollable) {
    // 简单实现:检查滚动位置(实际可能需要更复杂的判断)
    let info = scrollable.scrollInfo();
    return info && info.vertical && 
           Math.abs(info.vertical.position - info.vertical.range) < 10;
}

2. 脚本稳定性:避免主线程阻塞与异常处理

AutoxJS脚本运行在JavaScript环境中,虽然不像传统编程那样有严格的多线程要求,但不当的代码结构仍然会导致脚本“假死”或响应缓慢。

2.1 识别和避免主线程阻塞

主线程阻塞通常由以下原因引起:

  • 长时间的同步循环操作
  • 未设置超时的网络请求
  • 复杂的同步计算任务
  • 不当的sleep()使用
// 问题代码:同步处理大量数据
function processLargeData(dataArray) {
    let results = [];
    for (let i = 0; i < dataArray.length; i++) {
        // 每个处理都很耗时
        let result = complexCalculation(dataArray[i]);
        results.push(result);
    }
    return results; // 在处理完成前,脚本无法响应其他事件
}

// 改进方案:使用分片处理
async function processLargeDataSafely(dataArray, chunkSize = 50) {
    let results = [];
    
    for (let i = 0; i < dataArray.length; i += chunkSize) {
        let chunk = dataArray.slice(i, i + chunkSize);
        let chunkResults = await processChunk(chunk);
        results.push(...chunkResults);
        
        // 每处理完一个分片,让出控制权
        if (i + chunkSize < dataArray.length) {
            await delay(100); // 短暂延迟
        }
    }
    
    return results;
}

function processChunk(chunk) {
    return new Promise(resolve => {
        threads.start(function() {
            let results = chunk.map(item => complexCalculation(item));
            resolve(results);
        });
    });
}

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

2.2 实现可靠的超时控制机制

为关键操作添加超时控制是提升脚本稳定性的重要手段。下面是一个通用的带超时的操作包装器:

class TimeoutController {
    constructor(timeout = 30000) {
        this.timeout = timeout;
        this.timer = null;
        this.isTimeout = false;
    }
    
    async execute(operation) {
        return new Promise((resolve, reject) => {
            // 设置超时计时器
            this.timer = setTimeout(() => {
                this.isTimeout = true;
                reject(new Error(`操作超时 (${this.timeout}ms)`));
            }, this.timeout);
            
            // 执行实际操作
            try {
                let result = operation();
                if (!this.isTimeout) {
                    clearTimeout(this.timer);
                    resolve(result);
                }
            } catch (error) {
                if (!this.isTimeout) {
                    clearTimeout(this.timer);
                    reject(error);
                }
            }
        });
    }
    
    cancel() {
        if (this.timer) {
            clearTimeout(this.timer);
        }
    }
}

// 使用示例
async function safeNetworkRequest(url, options) {
    let controller = new TimeoutController(10000);
    
    try {
        let response = await controller.execute(() => {
            return http.get(url, options);
        });
        return response;
    } catch (error) {
        log(`请求失败: ${error.message}`);
        // 根据错误类型决定重试或退出
        if (error.message.includes('超时')) {
            // 超时重试逻辑
            return retryRequest(url, options);
        }
        throw error;
    }
}

2.3 异常恢复与状态持久化

当脚本因异常中断时,能够从断点恢复是专业脚本的标志。实现这一功能需要状态管理:

class ScriptStateManager {
    constructor(storageKey = 'script_state') {
        this.storageKey = storageKey;
        this.state = this.loadState() || {
            currentStep: 0,
            lastSuccessTime: null,
            errorCount: 0,
            customData: {}
        };
    }
    
    loadState() {
        try {
            let saved = storages.create(this.storageKey);
            let stateStr = saved.get('state', '{}');
            return JSON.parse(stateStr);
        } catch (e) {
            log(`加载状态失败: ${e}`);
            return null;
        }
    }
    
    saveState() {
        try {
            let saved = storages.create(this.storageKey);
            saved.put('state', JSON.stringify(this.state));
            return true;
        } catch (e) {
            log(`保存状态失败: ${e}`);
            return false;
        }
    }
    
    updateStep(step, data = {}) {
        this.state.currentStep = step;
        Object.assign(this.state.customData, data);
        this.state.lastSuccessTime = new Date().toISOString();
        this.saveState();
    }
    
    recordError(error) {
        this.state.errorCount = (this.state.errorCount || 0) + 1;
        this.state.lastError = {
            message: error.message,
            time: new Date().toISOString(),
            stack: error.stack
        };
        this.saveState();
    }
    
    shouldRetry(maxErrors = 3) {
        return (this.state.errorCount || 0) < maxErrors;
    }
    
    reset() {
        this.state = {
            currentStep: 0,
            lastSuccessTime: null,
            errorCount: 0,
            customData: {}
        };
        this.saveState();
    }
}

// 在脚本中使用
let stateManager = new ScriptStateManager('my_script_v1');

try {
    switch(stateManager.state.currentStep) {
        case 0:
            await step1();
            stateManager.updateStep(1);
        case 1:
            await step2();
            stateManager.updateStep(2);
        case 2:
            await step3();
            stateManager.updateStep(0); // 完成循环
    }
} catch (error) {
    stateManager.recordError(error);
    
    if (stateManager.shouldRetry()) {
        log(`步骤${stateManager.state.currentStep}失败,准备重试`);
        // 等待后重试当前步骤
        sleep(5000);
        continue;
    } else {
        log(`错误次数过多,停止脚本`);
        exit();
    }
}

3. 日志系统:从简单输出到结构化监控

在开发阶段,console.log()toast()可能足够使用。但在生产环境中,你需要一个能够持久化、可查询、分等级的完整日志系统。

3.1 实现分级日志系统

const LogLevel = {
    DEBUG: 0,
    INFO: 1,
    WARN: 2,
    ERROR: 3,
    CRITICAL: 4
};

class Logger {
    constructor(options = {}) {
        this.level = options.level || LogLevel.INFO;
        this.enableConsole = options.enableConsole !== false;
        this.enableFile = options.enableFile || false;
        this.logFile = options.logFile || '/sdcard/autoxjs_logs/';
        this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB
        this.context = options.context || 'main';
        
        this.ensureLogDirectory();
    }
    
    ensureLogDirectory() {
        if (this.enableFile) {
            files.ensureDir(this.logFile);
        }
    }
    
    log(level, message, data = null) {
        if (level < this.level) return;
        
        const levelNames = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'];
        const timestamp = new Date().toISOString();
        const logEntry = {
            timestamp,
            level: levelNames[level],
            context: this.context,
            message,
            data,
            device: device.brand + ' ' + device.model,
            appVersion: app.autojs.versionName
        };
        
        const logString = JSON.stringify(logEntry);
        
        // 控制台输出
        if (this.enableConsole) {
            let consoleMethod = level >= LogLevel.ERROR ? console.error : 
                              level >= LogLevel.WARN ? console.warn : console.log;
            consoleMethod(`[${levelNames[level]}] ${message}`, data || '');
        }
        
        // 文件输出
        if (this.enableFile) {
            this.writeToFile(logString);
        }
        
        // 重要日志显示Toast
        if (level >= LogLevel.ERROR) {
            toastLog(`${levelNames[level]}: ${message}`);
        }
        
        return logEntry;
    }
    
    writeToFile(logString) {
        try {
            let date = new Date();
            let fileName = `log_${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}.json`;
            let filePath = files.join(this.logFile, fileName);
            
            // 检查文件大小
            if (files.exists(filePath)) {
                let size = files.getSize(filePath);
                if (size > this.maxFileSize) {
                    // 文件过大,创建新的
                    let backupPath = filePath.replace('.json', `_${Date.now()}.json`);
                    files.copy(filePath, backupPath);
                    files.remove(filePath);
                }
            }
            
            // 写入日志
            files.append(filePath, logString + '\n');
        } catch (e) {
            console.error('写入日志文件失败:', e);
        }
    }
    
    debug(message, data) {
        return this.log(LogLevel.DEBUG, message, data);
    }
    
    info(message, data) {
        return this.log(LogLevel.INFO, message, data);
    }
    
    warn(message, data) {
        return this.log(LogLevel.WARN, message, data);
    }
    
    error(message, data) {
        return this.log(LogLevel.ERROR, message, data);
    }
    
    critical(message, data) {
        return this.log(LogLevel.CRITICAL, message, data);
    }
}

// 使用示例
let logger = new Logger({
    level: LogLevel.DEBUG,
    enableFile: true,
    context: 'checkin_script'
});

try {
    logger.info('脚本启动', {time: Date.now()});
    
    let result = await performCheckin();
    logger.info('打卡成功', result);
    
} catch (error) {
    logger.error('打卡失败', {
        error: error.message,
        stack: error.stack,
        lastAction: '点击确认按钮'
    });
}

3.2 日志分析与性能监控

有了结构化的日志,我们可以进一步实现性能监控和异常分析:

class PerformanceMonitor {
    constructor(logger) {
        this.logger = logger;
        this.markers = new Map();
        this.metrics = {
            operations: [],
            errors: [],
            performance: []
        };
    }
    
    startMark(name) {
        this.markers.set(name, {
            start: Date.now(),
            memory: this.getMemoryUsage()
        });
    }
    
    endMark(name, metadata = {}) {
        if (!this.markers.has(name)) {
            this.logger.warn(`未找到开始标记: ${name}`);
            return;
        }
        
        let mark = this.markers.get(name);
        let duration = Date.now() - mark.start;
        let memoryDiff = this.getMemoryUsage() - mark.memory;
        
        let metric = {
            name,
            duration,
            memoryDiff,
            timestamp: new Date().toISOString(),
            ...metadata
        };
        
        this.metrics.performance.push(metric);
        this.markers.delete(name);
        
        // 如果操作时间过长,记录警告
        if (duration > 5000) { // 5秒阈值
            this.logger.warn(`操作 ${name} 耗时过长`, metric);
        }
        
        return metric;
    }
    
    getMemoryUsage() {
        // 简单的内存使用估算
        if (typeof performance !== 'undefined' && performance.memory) {
            return performance.memory.usedJSHeapSize;
        }
        return 0;
    }
    
    recordOperation(name, success, data = {}) {
        let operation = {
            name,
            success,
            timestamp: new Date().toISOString(),
            ...data
        };
        
        this.metrics.operations.push(operation);
        
        if (!success) {
            this.metrics.errors.push(operation);
            this.logger.error(`操作失败: ${name}`, operation);
        }
        
        return operation;
    }
    
    generateReport() {
        let totalOps = this.metrics.operations.length;
        let successOps = this.metrics.operations.filter(op => op.success).length;
        let successRate = totalOps > 0 ? (successOps / totalOps * 100).toFixed(2) : 0;
        
        let avgDuration = 0;
        if (this.metrics.performance.length > 0) {
            let totalDuration = this.metrics.performance.reduce((sum, m) => sum + m.duration, 0);
            avgDuration = totalDuration / this.metrics.performance.length;
        }
        
        return {
            summary: {
                totalOperations: totalOps,
                successfulOperations: successOps,
                successRate: successRate + '%',
                averageDuration: avgDuration.toFixed(2) + 'ms',
                errorCount: this.metrics.errors.length,
                monitoringPeriod: this.getMonitoringPeriod()
            },
            recentErrors: this.metrics.errors.slice(-5),
            slowOperations: this.metrics.performance
                .filter(m => m.duration > 1000)
                .sort((a, b) => b.duration - a.duration)
                .slice(0, 5)
        };
    }
    
    getMonitoringPeriod() {
        if (this.metrics.operations.length === 0) return '无数据';
        
        let first = this.metrics.operations[0].timestamp;
        let last = this.metrics.operations[this.metrics.operations.length - 1].timestamp;
        return `${first} 至 ${last}`;
    }
}

// 集成使用
let logger = new Logger({enableFile: true});
let monitor = new PerformanceMonitor(logger);

async function monitoredOperation(name, operation) {
    monitor.startMark(name);
    
    try {
        let result = await operation();
        monitor.endMark(name, {success: true});
        monitor.recordOperation(name, true, {result: 'success'});
        return result;
    } catch (error) {
        monitor.endMark(name, {success: false, error: error.message});
        monitor.recordOperation(name, false, {error: error.message});
        throw error;
    }
}

// 在脚本关键点使用
await monitoredOperation('应用启动', () => launchApp('目标应用'));
await monitoredOperation('登录流程', () => performLogin());
await monitoredOperation('数据提交', () => submitData());

// 生成报告
let report = monitor.generateReport();
logger.info('性能报告', report);

4. 高级技巧:提升脚本的适应性与可维护性

4.1 配置驱动与环境适配

硬编码的参数会让脚本难以维护和适应不同环境。通过配置系统,我们可以让脚本更加灵活:

class ConfigManager {
    constructor(defaultConfig = {}) {
        this.defaultConfig = {
            // 超时设置
            timeouts: {
                elementWait: 10000,
                networkRequest: 30000,
                operation: 60000
            },
            
            // 重试策略
            retry: {
                maxAttempts: 3,
                delay: 2000,
                backoffFactor: 1.5
            },
            
            // 界面交互
            ui: {
                clickDelay: 300,
                swipeDuration: 500,
                screenshotOnError: true
            },
            
            // 功能开关
            features: {
                enableLogging: true,
                enableScreenshots: false,
                enableNotifications: true
            },
            
            // 应用特定配置
            appSpecific: {}
        };
        
        // 合并默认配置和传入配置
        Object.assign(this.defaultConfig, defaultConfig);
        
        // 尝试加载用户配置
        this.userConfig = this.loadUserConfig();
        this.currentConfig = this.mergeConfigs();
    }
    
    loadUserConfig() {
        try {
            let configPath = '/sdcard/autoxjs_config/config.json';
            if (files.exists(configPath)) {
                let content = files.read(configPath);
                return JSON.parse(content);
            }
        } catch (e) {
            console.warn('加载用户配置失败,使用默认配置:', e);
        }
        return {};
    }
    
    mergeConfigs() {
        // 深度合并配置
        function deepMerge(target, source) {
            for (let key in source) {
                if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
                    target[key] = deepMerge(target[key] || {}, source[key]);
                } else {
                    target[key] = source[key];
                }
            }
            return target;
        }
        
        return deepMerge(JSON.parse(JSON.stringify(this.defaultConfig)), this.userConfig);
    }
    
    get(keyPath, defaultValue = null) {
        let keys = keyPath.split('.');
        let value = this.currentConfig;
        
        for (let key of keys) {
            if (value && typeof value === 'object' && key in value) {
                value = value[key];
            } else {
                return defaultValue;
            }
        }
        
        return value !== undefined ? value : defaultValue;
    }
    
    set(keyPath, value) {
        let keys = keyPath.split('.');
        let config = this.currentConfig;
        
        for (let i = 0; i < keys.length - 1; i++) {
            if (!(keys[i] in config) || typeof config[keys[i]] !== 'object') {
                config[keys[i]] = {};
            }
            config = config[keys[i]];
        }
        
        config[keys[keys.length - 1]] = value;
        this.saveUserConfig();
    }
    
    saveUserConfig() {
        try {
            let configPath = '/sdcard/autoxjs_config/';
            files.ensureDir(configPath);
            
            files.write(
                files.join(configPath, 'config.json'),
                JSON.stringify(this.userConfig, null, 2)
            );
            return true;
        } catch (e) {
            console.error('保存用户配置失败:', e);
            return false;
        }
    }
    
    // 环境适配
    adaptToEnvironment() {
        let envConfig = {
            deviceType: device.brand.toLowerCase().includes('xiaomi') ? 'xiaomi' : 'other',
            androidVersion: parseInt(device.release.split('.')[0]),
            screenDensity: Math.min(device.width, device.height) / 160
        };
        
        // 根据设备类型调整配置
        if (envConfig.deviceType === 'xiaomi') {
            // 小米设备可能需要不同的延迟
            this.set('ui.clickDelay', 500);
        }
        
        if (envConfig.androidVersion >= 10) {
            // Android 10+ 可能需要额外的权限处理
            this.set('features.requireSpecialPermissions', true);
        }
        
        return envConfig;
    }
}

// 在脚本中使用配置
let config = new ConfigManager();
let env = config.adaptToEnvironment();

// 使用配置值
let waitTimeout = config.get('timeouts.elementWait');
let maxRetries = config.get('retry.maxAttempts');

// 动态调整配置
if (env.androidVersion >= 11) {
    config.set('ui.swipeDuration', 700); // Android 11需要更长的滑动时间
}

4.2 模块化与插件系统

随着脚本复杂度增加,将功能模块化可以大幅提升可维护性:

// 基础插件类
class ScriptPlugin {
    constructor(name, config = {}) {
        this.name = name;
        this.config = config;
        this.enabled = true;
        this.logger = null;
    }
    
    setLogger(logger) {
        this.logger = logger;
    }
    
    log(level, message, data) {
        if (this.logger) {
            this.logger.log(level, `[${this.name}] ${message}`, data);
        }
    }
    
    async initialize() {
        // 子类重写
    }
    
    async execute(context) {
        // 子类重写
        return context;
    }
    
    async cleanup() {
        // 子类重写
    }
}

// 具体插件实现
class ScreenshotPlugin extends ScriptPlugin {
    constructor(config) {
        super('screenshot', {
            enabled: true,
            savePath: '/sdcard/autoxjs_screenshots/',
            onError: true,
            onSuccess: false,
            ...config
        });
    }
    
    async initialize() {
        files.ensureDir(this.config.savePath);
        this.log('info', '截图插件初始化完成');
    }
    
    async execute(context) {
        if (!this.enabled || !this.config.onSuccess) return context;
        
        try {
            let timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            let filename = `success_${timestamp}.png`;
            let filepath = files.join(this.config.savePath, filename);
            
            captureScreen(filepath);
            this.log('debug', `截图已保存: ${filepath}`);
            
            context.lastScreenshot = filepath;
        } catch (error) {
            this.log('warn', `截图失败: ${error.message}`);
        }
        
        return context;
    }
}

class ErrorHandlerPlugin extends ScriptPlugin {
    constructor(config) {
        super('error_handler', {
            enabled: true,
            maxRetries: 3,
            retryDelay: 2000,
            fallbackActions: [],
            ...config
        });
        
        this.retryCount = 0;
    }
    
    async execute(context) {
        if (!context.error) return context;
        
        this.log('error', `处理错误: ${context.error.message}`);
        
        if (this.retryCount < this.config.maxRetries) {
            this.retryCount++;
            this.log('info', `准备第 ${this.retryCount} 次重试`);
            
            await sleep(this.config.retryDelay);
            context.shouldRetry = true;
        } else {
            this.log('critical', '重试次数用尽,执行备用方案');
            
            for (let action of this.config.fallbackActions) {
                try {
                    await action(context);
                } catch (fallbackError) {
                    this.log('error', `备用方案失败: ${fallbackError.message}`);
                }
            }
            
            context.shouldExit = true;
        }
        
        return context;
    }
}

// 插件管理器
class PluginManager {
    constructor() {
        this.plugins = new Map();
        this.executionOrder = [];
    }
    
    register(plugin) {
        this.plugins.set(plugin.name, plugin);
        this.executionOrder.push(plugin.name);
        return this;
    }
    
    setLogger(logger) {
        for (let plugin of this.plugins.values()) {
            plugin.setLogger(logger);
        }
        return this;
    }
    
    async initializeAll() {
        for (let name of this.executionOrder) {
            let plugin = this.plugins.get(name);
            if (plugin.enabled) {
                await plugin.initialize();
            }
        }
    }
    
    async executeAll(context = {}) {
        for (let name of this.executionOrder) {
            let plugin = this.plugins.get(name);
            if (plugin.enabled) {
                context = await plugin.execute(context);
                
                // 检查是否需要提前退出
                if (context.shouldExit) {
                    break;
                }
            }
        }
        return context;
    }
    
    async cleanupAll() {
        for (let name of this.executionOrder.reverse()) {
            let plugin = this.plugins.get(name);
            if (plugin.enabled) {
                await plugin.cleanup();
            }
        }
    }
}

// 使用插件系统构建脚本
async function runScriptWithPlugins() {
    let logger = new Logger();
    let pluginManager = new PluginManager();
    
    // 注册插件
    pluginManager
        .register(new ScreenshotPlugin({
            onSuccess: true,
            onError: true
        }))
        .register(new ErrorHandlerPlugin({
            maxRetries: 2,
            fallbackActions: [
                async (ctx) => {
                    toast('操作失败,已通知管理员');
                    // 发送通知的逻辑
                }
            ]
        }))
        .setLogger(logger);
    
    // 初始化插件
    await pluginManager.initializeAll();
    
    let context = {
        step: 'start',
        startTime: Date.now()
    };
    
    try {
        // 执行主逻辑
        context = await mainLogic(context);
        
        // 执行插件(成功流程)
        context = await pluginManager.executeAll(context);
        
    } catch (error) {
        // 执行插件(错误处理流程)
        context.error = error;
        context = await pluginManager.executeAll(context);
        
        if (context.shouldRetry) {
            logger.info('准备重试整个流程');
            return runScriptWithPlugins(); // 递归重试
        }
    } finally {
        // 清理插件
        await pluginManager.cleanupAll();
    }
    
    return context;
}

4.3 自动化测试与模拟环境

在真实设备上调试脚本既耗时又有风险。建立模拟测试环境可以显著提高开发效率:

class MockEnvironment {
    constructor() {
        this.mockElements = new Map();
        this.mockResponses = new Map();
        this.interceptMode = false;
    }
    
    // 模拟UI元素
    mockElement(selector, properties = {}) {
        let mockId = JSON.stringify(selector);
        this.mockElements.set(mockId, {
            selector,
            properties,
            findCount: 0,
            clickCount: 0
        });
        
        // 拦截原生的find方法
        if (!this.interceptMode) {
            this.interceptFindMethods();
            this.interceptMode = true;
        }
    }
    
    interceptFindMethods() {
        let originalFind = selector => selector.findOne;
        
        // 简化示例,实际需要更完整的拦截
        selectorPrototype.findOne = function(timeout) {
            let mockId = JSON.stringify(this);
            if (mockEnvironment.mockElements.has(mockId)) {
                let mock = mockEnvironment.mockElements.get(mockId);
                mock.findCount++;
                
                // 返回模拟元素
                return {
                    click: function() {
                        mock.clickCount++;
                        console.log(`模拟点击: ${mockId}`);
                        return true;
                    },
                    bounds: function() {
                        return {
                            centerX: 540,
                            centerY: 960,
                            left: 500,
                            top: 940,
                            right: 580,
                            bottom: 980
                        };
                    },
                    // 其他属性和方法...
                    ...mock.properties
                };
            }
            
            // 没有模拟,调用原始方法
            return originalFind.call(this, timeout);
        };
    }
    
    // 模拟网络请求
    mockHttpRequest(url, response, options = {}) {
        let key = `${url}_${JSON.stringify(options)}`;
        this.mockResponses.set(key, {
            response,
            delay: options.delay || 0,
            callCount: 0
        });
        
        // 拦截http请求
        if (typeof http !== 'undefined') {
            let originalGet = http.get;
            http.get = function(url, options, callback) {
                let mockKey = `${url}_${JSON.stringify(options)}`;
                if (mockEnvironment.mockResponses.has(mockKey)) {
                    let mock = mockEnvironment.mockResponses.get(mockKey);
                    mock.callCount++;
                    
                    return new Promise(resolve => {
                        setTimeout(() => {
                            resolve(mock.response);
                        }, mock.delay);
                    });
                }
                return originalGet(url, options, callback);
            };
        }
    }
    
    // 验证模拟调用
    verifyCalls() {
        let report = {
            elements: {},
            requests: {}
        };
        
        for (let [id, mock] of this.mockElements) {
            report.elements[id] = {
                findCount: mock.findCount,
                clickCount: mock.clickCount
            };
        }
        
        for (let [key, mock] of this.mockResponses) {
            report.requests[key] = {
                callCount: mock.callCount
            };
        }
        
        return report;
    }
    
    // 重置模拟环境
    reset() {
        this.mockElements.clear();
        this.mockResponses.clear();
        // 恢复原始方法(实际实现需要保存原始引用)
    }
}

// 使用模拟环境进行测试
async function testScriptLogic() {
    let mockEnv = new MockEnvironment();
    
    // 设置模拟元素
    mockEnv.mockElement(text("登录"), {
        clickable: true,
        text: "登录"
    });
    
    mockEnv.mockElement(id("username_input"), {
        setText: function(text) {
            console.log(`模拟输入用户名: ${text}`);
            return true;
        }
    });
    
    // 设置模拟网络响应
    mockEnv.mockHttpRequest('https://api.example.com/login', {
        statusCode: 200,
        body: {success: true, token: 'mock_token'},
        headers: {'Content-Type': 'application/json'}
    }, {delay: 1000});
    
    // 运行测试
    try {
        await runLoginScript();
        let report = mockEnv.verifyCalls();
        
        console.log('测试报告:', JSON.stringify(report, null, 2));
        
        // 验证预期行为
        if (report.elements[JSON.stringify(text("登录"))].clickCount === 1) {
            console.log('✓ 登录按钮点击测试通过');
        } else {
            console.log('✗ 登录按钮点击测试失败');
        }
        
    } finally {
        mockEnv.reset();
    }
}

// 单元测试辅助函数
function testCase(description, testFunction) {
    return async function() {
        console.log(`\n测试: ${description}`);
        try {
            await testFunction();
            console.log(`✓ ${description} 通过`);
            return true;
        } catch (error) {
            console.error(`✗ ${description} 失败:`, error.message);
            return false;
        }
    };
}

// 组织测试套件
async function runTestSuite() {
    let tests = [
        testCase('元素定位测试', testElementFinding),
        testCase('网络请求测试', testNetworkRequests),
        testCase('错误处理测试', testErrorHandling),
        testCase('性能测试', testPerformance)
    ];
    
    let passed = 0;
    let failed = 0;
    
    for (let test of tests) {
        let result = await test();
        if (result) passed++;
        else failed++;
    }
    
    console.log(`\n测试完成: ${passed} 通过, ${failed} 失败`);
    return failed === 0;
}

在实际项目中,我通常会将配置管理、插件系统和模拟测试结合使用。比如先通过配置系统调整不同设备的参数,然后用插件系统组织各个功能模块,最后通过模拟测试验证关键路径。这种架构让脚本不仅能在当前环境下工作,还能适应未来的变化和扩展。

调试复杂脚本时,最实用的技巧其实是保持耐心和系统性。每次遇到问题,不要急于寻找临时解决方案,而是思考:这个问题是特例还是普遍现象?能否通过架构设计避免?现有的日志和监控能否更快发现问题?把这些问题的答案沉淀到你的代码库中,慢慢就会形成自己的最佳实践。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐