┌─────────────────────────────────────────────────────────────────┐
│ 启动脚本 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 检测章节类型 │
│ (学习目标? 播放按钮? 章节测验小标题?) │
└─────────────────────────────────────────────────────────────────┘

┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 类型1 │ │ 类型2 │ │ 类型3 │
│ 直接播放型 │ │ 学习目标型 │ │ 只有章节测验 │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
▼ ▼ │
┌───────────────┐ ┌───────────────┐ │
│ 点击播放 │ │点击学习目标 │ │
│ │ │ 下一节 │ │
└───────┬───────┘ └───────┬───────┘ │
│ │ │
└───────────┬───────────┘ │
▼ │
┌───────────────────────┐ │
│ 等待视频结束 │ │
│ (进度条消失→重现) │ │
└───────────┬───────────┘ │
▼ │
┌───────────────────────┐ │
│ 点击返回 │ │
└───────────┬───────────┘ │
▼ │
┌───────────────────────┐ │
│ 点击1下一节 │ │
│ (控件ID→区域扫描) │ │
└───────────┬───────────┘ │
▼ │
┌───────────────────────┐ │
│ 等待小标题出现 │ │
└───────────┬───────────┘ │
▼ │
┌───────────────────────┐ ┌───────────────┐
│ 有章节测验小标题? │──────▶│ 滑动到底部 │
└───────────────────────┘ │ 点击2下一节 │
│ └───────┬───────┘
▼ │
┌───────────────────────┐ │
│ 有下一章节按钮? │◀──────────────┘
└───────────────────────┘


┌───────────────────────┐
│ 点击下一章节 │
└───────────────────────┘


┌───────────────────────┐
│ 等待5秒 │
└───────────────────────┘


┌───────────────────────┐
│ 还有下一章节? │
└───────────────────────┘
│ │
是 否
│ │
▼ ▼
循环 结束

HAMI_1573765277 · 2 小时前

回答

Hamibot AI
1
· 2 小时前

你提供的流程图逻辑清晰,非常适合用 Hamibot 实现自动化学习流程。下面我为你提供一套完整、可直接运行的脚本框架,并针对流程图中的关键节点给出实现方案与稳定性建议。代码已严格适配 Hamibot 使用的 Rhino 1.7.15 引擎(无箭头函数、无默认参数)。

📜 完整脚本框架

// 文档地址 https://docs.hamibot.com/
auto.waitFor();
setScreenMetrics(1080, 1920); // 按你设计脚本时的手机分辨率设置,自动适配其他机型

function main() {
    while (true) {
        var chapterType = detectChapterType();
        if (chapterType === 0) {
            log('未识别到有效章节,流程终止');
            break;
        }

        handleChapterType(chapterType);
        waitForVideoEnd();
        clickBack();
        clickNextSection1();
        waitForSubtitle();

        if (hasQuizSubtitle()) {
            scrollToBottom();
            clickNextSection2();
        }

        if (!hasNextChapterButton()) {
            log('已无下一章节,脚本结束');
            break;
        }

        clickNextChapter();
        sleep(5000);
    }
    threads.shutDownAll();
}

// 1. 检测章节类型
function detectChapterType() {
    if (text('播放').exists() || id('play_btn').exists()) return 1;
    if (text('学习目标').exists()) return 2;
    if (text('章节测验').exists()) return 3;
    return 0;
}

// 2. 处理不同类型
function handleChapterType(type) {
    if (type === 1) {
        clickControl(text('播放'));
    } else if (type === 2) {
        clickControl(text('学习目标'));
        sleep(1000);
        clickControl(text('下一节'));
    } else if (type === 3) {
        log('当前为纯测验章节,跳过视频逻辑');
    }
}

// 3. 等待视频结束(进度条消失→重现逻辑)
function waitForVideoEnd() {
    log('等待视频播放结束...');
    var progressBar = id('progress_bar'); // 替换为实际进度条ID
    var lastVisible = true;
    while (true) {
        var currentVisible = progressBar.exists();
        if (lastVisible && !currentVisible) {
            // 进度条刚消失,可能进入缓冲或结束,继续观察
            sleep(2000);
            if (!progressBar.exists()) {
                log('视频已结束');
                break;
            }
        }
        lastVisible = currentVisible;
        sleep(1000);
    }
}

// 4. 点击返回
function clickBack() {
    back();
    sleep(1000);
}

// 5. 点击下一节(控件ID → 坐标 fallback)
function clickNextSection1() {
    var btn = id('next_section_btn').findOnce();
    if (btn && btn.clickable()) {
        btn.click();
    } else {
        // 坐标 fallback(需根据实际机型调整)
        click(900, 1800);
    }
    sleep(1500);
}

// 6. 等待小标题出现
function waitForSubtitle() {
    textMatches(/.*小标题.*/).waitFor();
    sleep(500);
}

// 7. 判断是否有章节测验小标题
function hasQuizSubtitle() {
    return text('章节测验').exists();
}

// 8. 滑动到底部并点击下一节2
function scrollToBottom() {
    scrollDown();
    sleep(800);
}
function clickNextSection2() {
    clickControl(text('下一节'));
    sleep(1500);
}

// 9. 判断/点击下一章节
function hasNextChapterButton() {
    return text('下一章节').exists() || id('next_chapter_btn').exists();
}
function clickNextChapter() {
    clickControl(text('下一章节'));
}

// 通用点击封装(增强成功率)
function clickControl(selector) {
    var target = selector.findOnce();
    if (target) {
        if (target.clickable()) {
            target.click();
        } else {
            // 控件不可点击时,点击其中心坐标
            var bounds = target.bounds();
            click(bounds.centerX(), bounds.centerY());
        }
        return true;
    }
    return false;
}

main();

🔍 核心节点实现说明

  1. 章节类型检测:使用 text()id() 组合判断。建议你在 Hamibot 控制台使用「布局分析」或「设备调试」获取准确的控件 ID 或文本,替换代码中的占位符。
  2. 视频结束判定:流程图要求“进度条消失→重现”。代码通过轮询进度条控件的 exists() 状态实现。若目标 APP 进度条为自定义 View,可改为检测播放按钮状态变化(如 text('重播').exists())。
  3. 点击 Fallback 机制clickNextSection1() 优先尝试控件点击,失败后降级为坐标点击。配合 setScreenMetrics() 可保证坐标在不同分辨率下自动放缩。
  4. 不可点击控件处理clickControl() 封装了 clickable() 判断。若控件 clickablefalse,自动提取 bounds() 中心坐标进行点击,避免点击失效。
  5. 循环与退出:主循环通过 hasNextChapterButton() 控制。检测不到下一章节时安全退出,并调用 threads.shutDownAll() 清理残留线程,符合 Hamibot 最佳实践。

🛡️ 稳定性最佳实践

  • 分辨率适配:务必在脚本开头调用 setScreenMetrics(设计宽, 设计高),后续所有 click(x, y) 会自动按比例缩放。
  • 控件定位优先级id() > text() > desc() > className() > 坐标。尽量使用 ID 或精确文本,避免使用模糊匹配导致误触。
  • 等待机制:避免固定 sleep() 硬等待。优先使用 selector.waitFor() 或状态轮询,提升执行效率与抗干扰能力。
  • 调试建议:在 Hamibot 控制台开启「显示布局边界」和「指针位置」,实时核对控件属性与坐标。遇到点击不响应时,检查控件是否被遮罩层覆盖或 clickable 属性为 false

如果你能提供目标 APP 的包名、关键控件的实际 ID/文本,或具体哪一步执行不稳定,我可以为你进一步精准适配选择器与等待逻辑。

ℹ️ 内容由 AI 生成,可能不准确,请注意核实