上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

用 adb + CDP 远程调试手机浏览器里的油猴脚本

[复制链接]
  • TA的每日心情
    衰
    4 小时前
  • 签到天数: 351 天

    [LV.8]以坛为家I

    15

    主题

    68

    回帖

    715

    积分

    荣誉开发者

    积分
    715

    荣誉开发者生态建设者

    发表于 昨天 08:46 | 显示全部楼层 | 阅读模式

    本帖最后由 朱焱伟 于 2026-10-3 09:22 编辑

    本帖最后由 朱焱伟 于 2026-10-3 09:21 编辑

    用 adb + CDP 远程调试手机浏览器里的油猴脚本

    脚本被打败,爱已不存在😭
    在AI大人已经成为脚本之神的时代,大家也似乎渐渐丧失了技术分享的乐趣。
    我发帖之前,也在想,似乎没有发布的必要或价值。
    不过,还是再灌水一篇吧。
    当冥想的日子飞逝,喧嚣的日子把我们唤去,且在此地留下些微的痕迹~

    手机上跑油猴脚本,最大的痛苦不是写,而是调:没有 DevTools,没有控制台,出了问题全靠 alert() 和猜。
    这篇文章介绍一套实测可用的远程调试链路:adb 端口转发 + Chrome DevTools Protocol(CDP),让你在电脑上直接驱动手机浏览器里的活页面——执行任意 JS、查事件监听器、在页面脚本之前注入探针、程序化开关标签页。
    文末附两个配套工具的用法:Via 浏览器(WebView 系轻量浏览器,调试口默认开启)和 miniserve(一行命令把脚本目录变成局域网更新源)。

    一、原理:手机浏览器自带一个调试服务器

    所有 Chromium 内核的浏览器(Chrome 安卓版、以及用 Android System WebView 实现的各类轻量浏览器)内部都运行着一个 DevTools 调试服务,监听在一个本地 Unix socket 上:

    • Chrome 安卓版:chrome_devtools_remote
    • WebView 系浏览器(如 Via):webview_devtools_remote_<pid>

    这个 socket 平时不对外,但通过 adb 端口转发可以把它映射到电脑的 localhost,之后用 HTTP + WebSocket 说 CDP 协议,就能完全操控页面。chrome://inspect 的「Inspect」按钮背后走的就是这套东西——区别是:用脚本直连 CDP,能做的事多得多,而且可以自动化、可以常驻。

    有一个前提差异要知道:

    浏览器 调试口状态
    Chrome 安卓版 始终开启
    WebView 系(Via/X浏览器 等) 取决于 App 是否调用了 WebView.setWebContentsDebuggingEnabled(true);Via 是开着的,很多浏览器不开

    WebView 系浏览器如果没开调试口,这条路就走不通(socket 不存在),只能换 Chrome 安卓版复现问题。

    二、环境准备(一次性)

    电脑侧:

    # 1) Android platform-tools(含 adb)
    #    国内可走代理下载:https://dl.google.com/android/repository/platform-tools-latest-windows.zip
    #    解压到任意目录,例如 D:\tools\platform-tools
    
    # 2) Node.js + ws 包(CDP 的 WebSocket 客户端)
    mkdir cdp-debug && cd cdp-debug
    npm init -y
    npm install ws

    手机侧:

    1. 设置 → 关于手机 → 连点「版本号 / OS 版本」7 次,开启开发者选项(MIUI/HyperOS 在「我的设备 → 全部参数」里,部分机型要求登录账号)
    2. 开发者选项 → 打开「USB 调试」
    3. USB 连电脑,手机弹出的 RSA 指纹确认框勾选「一律允许」
    4. 验证:电脑上 adb devices 能看到设备且状态为 device(不是 unauthorized)

    小米/OPPO/vivo 等机型如果 adb devices 一直看不到,检查 Windows 是否装上了手机的 ADB 驱动(设备管理器里看有没有带黄色感叹号的 Android 设备)。

    三、连接:三步找到活页面

    # 1) 找调试 socket(记住 webview_devtools_remote_<pid> 或 chrome_devtools_remote 的全名)
    adb shell "cat /proc/net/unix | grep devtools"
    
    # 2) 端口转发(socket 名以上一步实际输出为准)
    adb forward tcp:9222 localabstract:webview_devtools_remote_12345
    
    # 3) 列出可调试的页面,拿到 webSocketDebuggerUrl
    curl http://localhost:9222/json

    /json 返回的每个条目就是手机浏览器里的一个标签页,包含 url、title 和最关键的 webSocketDebuggerUrl。连上这个 WebSocket,就能对那个页面为所欲为。

    不想写代码的话,电脑 Chrome 打开 chrome://inspect 勾选 Discover USB devices,点页面旁的 inspect 就能直接开可视化的 DevTools——适合手动戳两下;要做系统化的诊断,还是脚本化 CDP 顺手。

    四、CDP 最小客户端

    一个能用的骨架只要三十行:

    // eval.js —— 用法: node eval.js <webSocketDebuggerUrl> <要执行的JS表达式>
    const WebSocket = require('ws');
    const ws = new WebSocket(process.argv[2], { perMessageDeflate: false });
    
    let id = 0;
    const pending = new Map();
    const send = (method, params = {}) => new Promise((res, rej) => {
      const i = ++id;
      pending.set(i, { res, rej });
      ws.send(JSON.stringify({ id: i, method, params }));
    });
    ws.on('message', (d) => {
      const m = JSON.parse(d);
      if (m.id && pending.has(m.id)) {
        const p = pending.get(m.id); pending.delete(m.id);
        m.error ? p.rej(new Error(m.error.message)) : p.res(m.result);
      }
    });
    
    ws.on('open', async () => {
      const r = await send('Runtime.evaluate', {
        expression: process.argv[3],
        returnByValue: true,
      });
      console.log(r.result.value);
      ws.close(); process.exit(0);
    });
    node eval.js "ws://localhost:9222/devtools/page/XXXX" \
      "JSON.stringify({url: location.href, title: document.title, divs: document.querySelectorAll('div').length})"

    这一步已经能解决 80% 的问题:脚本状态、元素数量、localStorage、任何你想检查的东西。

    五、调试油猴脚本的四个杀手锏

    1. Runtime.evaluate:读写活页面

    除了查状态,还能直接注入修复代码做实验——改完觉得对了再写进脚本。比"改脚本→传手机→刷新→看现象"的循环快一个数量级。

    2. DOMDebugger.getEventListeners:谁挂的监听器?

    脚本冲突排查神器。给一个元素(或 document/window)远程对象引用,返回它身上所有监听器的类型和注册位置(scriptId:行:列):

    // 先拿引用,再查监听器(两次 evaluate,或者在一个客户端里连做)
    const r = await send('Runtime.evaluate', { expression: 'document' });
    const listeners = await send('DOMDebugger.getEventListeners', {
      objectId: r.result.objectId, depth: 0,
    });
    // output: [{type:'touchend', scriptId:'1603', lineNumber:2, ...}, ...]

    "页面上有个看不见的东西在吃我的点击"这类问题,一查便知是哪个脚本挂的。

    3. Page.addScriptToEvaluateOnNewDocument:抢在所有页面脚本之前

    这是整套链路里价值最高的一招。很多手机浏览器(尤其 WebView 系)注入油猴脚本的时机偏晚,页面脚本先跑了——你的钩子、你的补丁,对已经保存了原始引用的页面代码全部失效。

    这个 CDP 指令能在每个新文档的任何脚本执行之前注入你的代码,等于给浏览器补上了一个真正的 document-start:

    await send('Page.addScriptToEvaluateOnNewDocument', { source: `
      window.__bootlog = [];
      const origAEL = EventTarget.prototype.addEventListener;
      EventTarget.prototype.addEventListener = function (type) {
        if (/touch|click/.test(String(type))) {
          window.__bootlog.push({
            type: String(type),
            target: this === document ? 'document' : this === window ? 'window' : this.tagName,
            stack: (new Error().stack || '').split('\\n').slice(2, 5).join(' <= '),
          });
        }
        return origAEL.apply(this, arguments);
      };
      // 同理可以包 window.open / document.write / fetch …
    ` });

    之后刷新页面,window.__bootlog 里就是一份带调用栈的完整记录:谁注册了 touchend、谁调了 window.open,一目了然。

    4. 浏览器级端点 + Target.setAutoAttach:覆盖所有标签页

    页面级 WebSocket 只管一个标签。连浏览器级端点(curl http://localhost:9222/json/version 里的 webSocketDebuggerUrl),配合 auto-attach,可以对现在和未来所有标签页统一挂探针、统一收事件:

    // 连 ws://localhost:9222/devtools/browser 之后
    await send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
    await send('Target.setDiscoverTargets', { discover: true });
    
    // 收到 Target.attachedToTarget 事件时,对 m.params.sessionId:
    //   Runtime.enable + Page.enable + Page.addScriptToEvaluateOnNewDocument —— 全标签探针
    // 收到 Target.targetInfoChanged 事件时检查 URL —— 可以做"弹广告标签秒关"这类自动化
    // Target.closeTarget —— 程序化关标签

    页面里的探针用 console.log('[PROBE]' + JSON.stringify(...)) 回传,CDP 侧开 Runtime.enable 后通过 Runtime.consoleAPICalled 事件实时接收——一个常驻进程就把整个浏览器的行为流接管了。

    六、踩坑记录(都是实际掉进去过的)

    1. waitForDebuggerOnStart: true 会卡死部分 WebView 的新标签——新标签停在 about:blank,放行指令(Runtime.runIfWaitingForDebugger)不生效。稳妥起见用 false,接受新标签创建瞬间的小竞速。
    2. addScriptToEvaluateOnNewDocument 只对挂载之后发生的导航生效——已经加载完的页面不会被注入,挂载前创建的新标签也可能错过。对策:auto-attach + 监听 targetInfoChanged 补挂。
    3. 新标签创建时 URL 是 about:blank——想在标签出现时按 URL 做过滤/拦截,必须监听 Target.targetInfoChanged 等 URL 变出来,attach 那一刻是匹配不到的。
    4. Debugger.getScriptSource 拿不到 Debugger.enable 之前就已 parse 的脚本——要拿全量源码,先 enable 再刷新页面;内联脚本更简单,直接从 DOM 里读 <script> 的 textContent。
    5. 安全:调试口威力巨大,务必只通过 USB 的 adb forward 使用(绑定在 localhost),不要把 9222 转发到局域网。调完拔线,adb forward --remove tcp:9222 收尾。

    七、Via 浏览器:轻,但适合这套玩法

    Via 是 Android 上的 WebView 系超轻量浏览器(安装包 1MB 级),对油猴脚本用户有三个关键特性:

    1. 内置脚本支持:设置 → 脚本,直接粘贴 userscript 即可运行,无需装任何管理器。没有 GM_* API(脚本里要做退化处理,比如 GM_getValue 退回 localStorage),@match/@include 基本可用。
    2. WebView 调试口默认开启——本文整套 CDP 链路对它直接可用,这是很多同类浏览器做不到的。
    3. 注入时机偏晚(经常晚于页面脚本)——写脚本时要假设"页面脚本可能已经跑过了":钩子可能被保存的原始引用绕过、DOM 可能已被改过。诊断这类问题时,第五节的「抢跑注入」就是标准解法。

    八、miniserve:一行命令的脚本分发渠道

    调脚本免不了反复往手机上更新。用 U 盘/网盘/聊天软件传文件太原始,miniserve 一行命令解决:

    # 安装(Windows)
    winget install miniserve
    
    # 在脚本所在目录起服务
    cd my-scripts/
    miniserve .
    
    # 输出里会列出本机局域网 IP,手机浏览器访问:
    #   http://192.168.x.x:8080/xxx.user.js

    手机上打开 URL → 全选复制 → 粘贴进 Via 的脚本设置 (其实不用手动复制,via浏览器里重载这个js的url就会提示你去安装或者更新),就是一个完整的更新循环。改完文件立即生效(miniserve 直接读磁盘),比任何同步方案都快。调试期间配合前面的 CDP 链路:电脑改脚本 → 手机一键更新 → 远程注入验证,全程不用碰数据线以外的任何东西。

    九、总结

    需求 工具
    看脚本/页面状态 Runtime.evaluate
    手动戳两下 chrome://inspect
    查"谁挂的监听器" DOMDebugger.getEventListeners
    抢在页面脚本之前注入 Page.addScriptToEvaluateOnNewDocument
    全标签覆盖/自动化 浏览器级端点 + Target.setAutoAttach
    往手机分发脚本 miniserve
    轻量试验田 Via 浏览器

    核心心法一句话:手机脚本调试的瓶颈从来不是写代码,而是"看不见"。adb + CDP 把电脑上的 DevTools 能力完整搬到了手机浏览器上——一旦可以看见活页面的真实状态,大部分"玄学问题"半小时内都会变成普通问题。

    当冥想的日子飞逝,喧嚣的日子把我们唤去,且在此地留下些微的痕迹

    发表回复

    本版积分规则