开源 · MIT 协议 · MCP
让 AI 直接调试用户手机里的 WebView
在 H5 页面或 App 的 WebView 里加一个脚本。用户报出一个短短的设备码,Claude Code、Cursor 或任何 MCP 客户端就能看页面、查 console 和网络请求,亲手点一遍把问题复现出来——整个过程用户看得见,也随时可以接管。
- 不用数据线
- 不开远程调试端口
- App 只需注入一个脚本
K7Q2-9M4X在线演示
一个真实的 bug,在“真实”的手机上查出来
演示里的手机上,商城页面加载的就是真正的页面 SDK。网关在你的浏览器里模拟,任何数据都不会离开这个标签页;但下面每一次快照、点击和 console 输出都是真实的。
AI agent
已连接 rwd MCP 服务
看 agent 接入手机、复现结算页的 bug,并找到根因。
用户的手机K7Q2-9M4X
完全在浏览器里运行:iframe 里是真实的 SDK(sdk.js),WebSocket 垫片把它的连接通过 postMessage 转到页面里的模拟网关,再由一个脚本化的 agent 发出真实的 RPC。
工作原理
从“我手机上打不开”到找到根因,只要三步
-
页面接入 SDK
在
<head>里尽早加一个脚本,console 和网络从第一行就开始记录。App 里则在页面开始加载时注入同一个脚本。 -
用户报出设备码
每台手机都有一个固定的设备码,比如
K7Q2-9M4X,显示在一个小胶囊里。用户点一下「求助」,这台设备就会排到 agent 列表的最前面。 -
agent 通过 MCP 接入
用 API Key 把网关的托管 MCP 地址加进你的 agent,然后直接描述问题。agent 会自己找到设备、请求用户确认,然后开始排查。
<!-- 放在 <head> 里,越早越好 -->
<script src="https://rwd.example.com/v1/sdk.js"></script>
<script>
RemoteWebviewDebugger.run("https://rwd.example.com/v1", "rwd_pk_…", {
userId: currentUser?.id ?? null, // 可选:按用户查找设备
helpButton: true, // 显示「求助」按钮
})
</script>claude mcp add --transport http rwd https://rwd.example.com/v1/mcp \
--header "Authorization: Bearer rwd_sk_live_…"{
"mcpServers": {
"rwd": {
"url": "https://rwd.example.com/v1/mcp",
"headers": {
"Authorization": "Bearer rwd_sk_live_…"
}
}
}
}# 只支持 stdio 的客户端:本地桥接到同一个托管地址
claude mcp add rwd -e RWD_API_KEY=rwd_sk_live_… \
-- rwd mcp --gateway https://rwd.example.com/v1agent 能做什么
你熟悉的 DevTools 那一套,通过 MCP 用在别人的手机上
-
无障碍快照
Playwright 风格的 YAML,元素带 ref。agent 按结构理解页面,而不是盯着像素,再用 ref 精确操作。
-
真实的交互
点击、填写、输入、按键、下拉选择、悬停、滚动、拖拽,带可操作性检查,React、Vue 的受控组件也认。
-
console 与网络
从页面加载起就记录在有上限的缓冲里,agent 接入之前的报错也查得到;需要时还能看请求和响应正文。
-
跨页面会话
列表页 → 详情页 → 结算页,或者新开的 WebView,同一台设备的所有页面组成一个会话,带跨页面时间线。
-
变化自动推送
每个工具结果末尾都会附上发生了什么:新页面、跳转、报错、用户接管。agent 不用轮询。
-
手机上的任务卡片
agent 把计划和进度显示在设备顶部,用户随时知道 AI 在做什么、做到哪一步。
-
截图
App 提供原生截图桥时是真实像素,否则用 DOM 重绘。两种方式都会遮盖敏感信息。
-
任意 MCP 客户端
/v1/mcp是 Streamable HTTP,Claude Code、Cursor 直接可用;只支持 stdio 的客户端用一个很小的桥接命令。
全部工具 32
list_devicesconnect_devicewait_for_devicelist_pagesselect_pagewait_for_pagetake_snapshotfind_elementstake_screenshotclickfilltype_textpress_keyselect_optionhoverscrolldragbatch_actionsnavigate_pagego_backreload_pagewait_forlist_console_messageslist_network_requestsget_network_requestget_storageevaluate_scriptset_taskget_session_timelinewait_for_eventwait_for_controldisconnect_device
隐私与控制
用户的手机,始终由用户做主
远程调试的前提是信任。每一次接入都看得见、要确认、能随时打断。
-
先确认,再接入
agent 申请接入时,用户在手机上看到是谁、要什么权限,自己选择级别。只认真实的点击,脚本没法替用户点「允许」。
-
随时接管或结束
AI 操作时页面边缘亮起光圈,用户输入被锁定。点一下「接管」AI 立刻暂停,点「结束」直接断开。
-
默认遮盖敏感信息
密码框和带
data-rwd-mask的内容,在快照、截图、console 和网络数据里都显示为••••。还可以按项目配置选择器和正则。 -
敏感操作要用户点头
给「提交订单」「注销账号」这类按钮加上
data-rwd-confirm,agent 每次点击前都要用户确认。 -
权限分级
observe 只看,interact 可操作,advanced 还能执行脚本、读请求正文和存储。按 API Key 和项目分别设上限。
-
审计日志
每次接入、审批、RPC 调用和接管都会记录谁、做了什么、什么时候——但不记录页面内容和参数值。
App 原生接入
在 App 里,就是一个注入脚本
下载一次 SDK,在页面开始加载时注入:iOS 用 WKUserScript,Android 用 addDocumentStartJavaScript。网页本身一行都不用改。
一台手机一个设备码
传入固定的
deviceId(并保存deviceToken),设备码跨 WebView、跨域名都不变。原生控制按钮
用
nativeControl()做自己的接管 / 交还 / 结束按钮,页面脚本调用不了。原生审批弹窗
nativeApproval: true把接入审批和敏感操作确认交给 App 自己的界面。真实像素截图
一个很小的桥(WKWebView 快照、Android 位图),agent 拿到的是真实画面而不是 DOM 重绘。
import WebKit
/// 在 webView.load(…) 之前调用:下载一次 SDK,在 documentStart 注入。
func installRemoteDebugger(into controller: WKUserContentController,
userId: String?, done: @escaping () -> Void) {
let gateway = "https://rwd.example.com/v1"
URLSession.shared.dataTask(with: URL(string: gateway + "/sdk.js")!) { data, _, _ in
DispatchQueue.main.async {
defer { done() }
guard let data, let sdk = String(data: data, encoding: .utf8) else { return }
let options: [String: Any] = [
"deviceId": stableDeviceId, // 存在 UserDefaults 里的 UUID
"userId": userId ?? NSNull(),
"helpButton": true,
]
let json = String(data: try! JSONSerialization.data(withJSONObject: options), encoding: .utf8)!
let boot = sdk + "\n;RemoteWebviewDebugger.run(\"\(gateway)\", \"rwd_pk_…\", \(json));"
controller.addUserScript(WKUserScript(source: boot,
injectionTime: .atDocumentStart,
forMainFrameOnly: true))
}
}.resume()
}// 依赖:androidx.webkit:webkit:1.12.0;需开启 javaScriptEnabled 和 domStorageEnabled
// 在 webView.loadUrl(…) 之前调用
fun installRemoteDebugger(webView: WebView, userId: String?, onReady: () -> Unit) = thread {
val gateway = "https://rwd.example.com/v1"
val sdk = runCatching { URL("$gateway/sdk.js").readText() }.getOrNull()
webView.post {
if (sdk != null && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
val options = JSONObject()
.put("deviceId", stableDeviceId) // 存在 SharedPreferences 里的 UUID
.put("userId", userId ?: JSONObject.NULL)
.put("helpButton", true)
val boot = "$sdk\n;RemoteWebviewDebugger.run(${JSONObject.quote(gateway)}, \"rwd_pk_…\", $options);"
WebViewCompat.addDocumentStartJavaScript(webView, boot, setOf("https://m.example.com"))
}
onReady()
}
}<!-- 放在 <head> 里,越早越好 -->
<script src="https://rwd.example.com/v1/sdk.js"></script>
<script>
RemoteWebviewDebugger.run("https://rwd.example.com/v1", "rwd_pk_…", {
userId: currentUser?.id ?? null, // 可选:按用户查找设备
helpButton: true, // 显示「求助」按钮
})
</script>自建部署
一个 Go 二进制,部署在你自己的服务器上
网关内嵌了 Web 控制台和页面 SDK。默认用 SQLite,需要时换成 Postgres;Docker Compose 加 Caddy 负责 HTTPS。
控制台
设备、API Key、成员与角色、扫码邀请、安全策略、审计日志,还有一份填好你自己 Key 的接入指南。
数据库
默认是一个 SQLite 文件;设置
RWD_DATABASE_URL=postgres://…即可改用 Postgres。运维
/v1/metrics提供 Prometheus 指标,SDK 有带版本号和 SRI 校验的地址,/v1/healthz用于健康检查。
git clone https://github.com/ZaneL1u/remote-webview-debugger
cd remote-webview-debugger
# 构建二进制(内嵌控制台和 SDK),上传并启动
RWD_DOMAIN=rwd.example.com \
deploy/platform/deploy.sh you@your-server amd64
# → 控制台 https://rwd.example.com
# → 网关 https://rwd.example.com/v1(MCP 在 /v1/mcp)
# 或者在本机直接运行
make -j16
RWD_ADMIN_PASSWORD=change-me \
gateway/bin/rwd-darwin-arm64 serve --addr :8080构建需要 Go 和 Node;服务器上只需要 Docker。
常见问题
大家常问的问题
用户需要安装什么吗?
不需要。H5 页面里 SDK 就是页面的一部分;App 里由原生代码注入。用户只会看到一个显示设备码的小胶囊,以及 agent 申请接入时的确认框。
agent 能看到什么、做什么?
只限用户批准的范围:这台设备上的 WebView 页面,按授予的权限级别。WebView 之外的 App 界面和系统一概碰不到。遮盖的字段始终是遮盖的,页面内容也会作为不可信数据交给模型。
支持哪些 agent?
任何 MCP 客户端。Claude Code、Cursor 直接连托管的 Streamable HTTP 地址;只支持 stdio 的客户端用 rwd mcp,它只是把 stdio 转发到同一个地址。
支持哪些平台?
主流的移动端 WebView 和浏览器都可以:iOS 的 WKWebView、Android System WebView、各类 App 内置浏览器,以及手机上的 Safari、Chrome。桌面浏览器也能用。
网络差或者 WebSocket 被拦截怎么办?
页面只维持一条轻量的心跳 WebSocket,断开后指数退避重连。连续 3 次连不上,就改成每分钟轮询一次;agent 要接入时,网关会通知它立即重连。
会拖慢页面吗?
只是一个脚本。console 和网络记录放在有上限的环形缓冲里(各 500 条,响应正文总共不超过 5MB),空闲时连接上只有心跳。
能和 eruda、vConsole、PageSpy 一起用吗?
可以。快照和命中测试会跳过它们的节点,SDK 自己的流量也不会出现在它们的网络面板里,甚至可以作为一个 tab 收进 eruda。
上面的演示是真的吗?
SDK 是真实的,没有任何改动;只有网关是在你浏览器里模拟的。一个 WebSocket 垫片通过 postMessage 把 SDK 的连接转给它,再由脚本化的 agent 发出和真实网关一样的 RPC。