开源 · MIT 协议 · MCP

让 AI 直接调试用户手机里的 WebView

在 H5 页面或 App 的 WebView 里加一个脚本。用户报出一个短短的设备码,Claude Code、Cursor 或任何 MCP 客户端就能看页面、查 console 和网络请求,亲手点一遍把问题复现出来——整个过程用户看得见,也随时可以接管。

  • 不用数据线
  • 不开远程调试端口
  • App 只需注入一个脚本

在线演示

一个真实的 bug,在“真实”的手机上查出来

演示里的手机上,商城页面加载的就是真正的页面 SDK。网关在你的浏览器里模拟,任何数据都不会离开这个标签页;但下面每一次快照、点击和 console 输出都是真实的。

准备就绪

AI agent

已连接 rwd MCP 服务

看 agent 接入手机、复现结算页的 bug,并找到根因。

用户的手机K7Q2-9M4X

完全在浏览器里运行:iframe 里是真实的 SDK(sdk.js),WebSocket 垫片把它的连接通过 postMessage 转到页面里的模拟网关,再由一个脚本化的 agent 发出真实的 RPC。

工作原理

从“我手机上打不开”到找到根因,只要三步

  1. 页面接入 SDK

    在 <head> 里尽早加一个脚本,console 和网络从第一行就开始记录。App 里则在页面开始加载时注入同一个脚本。

  2. 用户报出设备码

    每台手机都有一个固定的设备码,比如 K7Q2-9M4X,显示在一个小胶囊里。用户点一下「求助」,这台设备就会排到 agent 列表的最前面。

  3. agent 通过 MCP 接入

    用 API Key 把网关的托管 MCP 地址加进你的 agent,然后直接描述问题。agent 会自己找到设备、请求用户确认,然后开始排查。

index.html
<!-- 放在 <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>
terminal
claude mcp add --transport http rwd https://rwd.example.com/v1/mcp \
  --header "Authorization: Bearer rwd_sk_live_…"
设备 K7Q2-9M4X 结算不了,合计显示「--」,帮我查一下原因。

agent 能做什么

你熟悉的 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_devices
  • connect_device
  • wait_for_device
  • list_pages
  • select_page
  • wait_for_page
  • take_snapshot
  • find_elements
  • take_screenshot
  • click
  • fill
  • type_text
  • press_key
  • select_option
  • hover
  • scroll
  • drag
  • batch_actions
  • navigate_page
  • go_back
  • reload_page
  • wait_for
  • list_console_messages
  • list_network_requests
  • get_network_request
  • get_storage
  • evaluate_script
  • set_task
  • get_session_timeline
  • wait_for_event
  • wait_for_control
  • disconnect_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 重绘。

RemoteDebugger.swift
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()
}

自建部署

一个 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。