本帖最后由 朱焱伟 于 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
手机侧:
- 设置 → 关于手机 → 连点「版本号 / OS 版本」7 次,开启开发者选项(MIUI/HyperOS 在「我的设备 → 全部参数」里,部分机型要求登录账号)
- 开发者选项 → 打开「USB 调试」
- USB 连电脑,手机弹出的 RSA 指纹确认框勾选「一律允许」
- 验证:电脑上
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 事件实时接收——一个常驻进程就把整个浏览器的行为流接管了。
六、踩坑记录(都是实际掉进去过的)
waitForDebuggerOnStart: true 会卡死部分 WebView 的新标签——新标签停在 about:blank,放行指令(Runtime.runIfWaitingForDebugger)不生效。稳妥起见用 false,接受新标签创建瞬间的小竞速。
addScriptToEvaluateOnNewDocument 只对挂载之后发生的导航生效——已经加载完的页面不会被注入,挂载前创建的新标签也可能错过。对策:auto-attach + 监听 targetInfoChanged 补挂。
- 新标签创建时 URL 是 about:blank——想在标签出现时按 URL 做过滤/拦截,必须监听
Target.targetInfoChanged 等 URL 变出来,attach 那一刻是匹配不到的。
Debugger.getScriptSource 拿不到 Debugger.enable 之前就已 parse 的脚本——要拿全量源码,先 enable 再刷新页面;内联脚本更简单,直接从 DOM 里读 <script> 的 textContent。
- 安全:调试口威力巨大,务必只通过 USB 的 adb forward 使用(绑定在 localhost),不要把 9222 转发到局域网。调完拔线,
adb forward --remove tcp:9222 收尾。
七、Via 浏览器:轻,但适合这套玩法
Via 是 Android 上的 WebView 系超轻量浏览器(安装包 1MB 级),对油猴脚本用户有三个关键特性:
- 内置脚本支持:设置 → 脚本,直接粘贴 userscript 即可运行,无需装任何管理器。没有
GM_* API(脚本里要做退化处理,比如 GM_getValue 退回 localStorage),@match/@include 基本可用。
- WebView 调试口默认开启——本文整套 CDP 链路对它直接可用,这是很多同类浏览器做不到的。
- 注入时机偏晚(经常晚于页面脚本)——写脚本时要假设"页面脚本可能已经跑过了":钩子可能被保存的原始引用绕过、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 能力完整搬到了手机浏览器上——一旦可以看见活页面的真实状态,大部分"玄学问题"半小时内都会变成普通问题。