ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

vConsole MCP:让AI直接看见H5页面调试日志

vConsole MCP:让AI直接看见H5页面调试日志

事情得从上个月调一个 H5 项目说起。客户那边报了个 bug:表单在安卓 WebView 里提交后偶发白屏,PC 端 Chrome 里怎么操作都复现不了。我连夜把 vConsole 接上,在真机上一顿操作,日志倒是都出来了,可我身边那堆 AI 编程助手全帮不上忙——它们能改代码,却看不见浏览器里到底报了啥错。折腾到最后,我干脆自己写了套工具,把 vConsole 能看到的日志、网络请求、存储信息全部结构化输出,再通过 MCP 暴露给 AI,让 Claude 这类助手直接"看见"H5 页面里正在发生什么。这就是今天要聊的 vConsole MCP 项目。

这个项目解决的痛点很实在:H5 调试链路越来越复杂,真机、WebView、小程序内嵌页各有各的环境,传统浏览器 devtools 在移动端基本帮不上忙,vConsole 是移动端调试的事实标准,但它只是把日志显示在页面上,AI 看不到。MCP 恰好补上这一环——它定义了 AI 读取外部数据的标准口径。把 vConsole 和 MCP 组合起来,等于给 AI 安了一双盯着页面的眼睛。适合谁看?天天跟 H5、移动端、内嵌页打交道的开发者,以及所有想让 AI 更深度参与 debug 流程的人。

1. 项目动机与整体设计思路

1.1 H5 调试的典型困境:日志看得见,摸不着

先聊点真实的调试场景。PC 端调试 H5 时,F12 打开就能看到 console、network、application 面板,信息量很全。但移动端不是这样:真机 Safari、安卓 WebView、微信里打开的 H5,你没法按 F12。常规套路是接 vConsole,页面上悬着一个绿色小按钮,点开是一个悬浮面板,能看到 console 日志、网络请求、本地存储,甚至能手动执行 JS。vConsole 解决的是"人怎么看到日志"的问题,可它没有能力把日志导出成结构化数据给另一个程序用。

我自己踩过的坑:有一次同事在安卓手机上复现问题,vConsole 里清清楚楚显示了一条ReferenceError: xxx is not defined,但报错对应的压缩代码只有一行,根本看不出是哪个模块出的问题。我让 AI 助手帮忙猜哪里越界了,AI 只能根据我的描述推断,来回问了七八轮,还是没定位到。问题在哪?AI 很有能力,但它是"瞎子"。它没有通道直接读取页面上的运行日志,只能靠人转述。转述过程会丢失细节,尤其是某个变量的实际值、某次请求的真实返回这类信息。

这也是 vConsole MCP 最核心的出发点:与其让人当传话筒,不如直接把 vConsole 捕获的数据流接到 AI 侧。任何 H5,不管跑在哪个环境,只要页面里有调试信息,AI 就能直接读取、筛选、分析,甚至反向在页面里执行 JS 验证修复效果。

1.2 MCP 到底是什么,为什么它适合做调试桥

MCP 全称 Model Context Protocol,是一个标准化协议,解决"AI 如何安全、统一地调用外部工具和数据源"的问题。可以把它理解成 AI 世界的 USB 接口:鼠标、键盘、显示器各自干各自的活,USB-C 口统一了物理连接标准,插上就能用。MCP Server 就是把某个数据源或能力封装成标准接口,AI 客户端(比如 Claude Desktop、Cherry Studio、Cline)通过这个接口发现工具、调用工具、拿到结果。

放在调试 H5 的场景里,MCP Server 就是一个"日志运输出口",它暗中连着远程浏览器的调试端口,监听 console 消息、网络请求、页面错误,然后暴露给 AI 几个工具:查看日志、查看请求、执行 JS、清理缓存。AI 不用知道 CDP(Chrome DevTools Protocol)怎么调用,也不用理解 WebSocket 握手细节,它只需要知道"有个工具叫 get_recent_logs,调用一下就能拿到最近 200 条 console 日志"。这套抽象非常有价值,因为 AI 的上下文窗口有限,它需要的是经过筛选、结构化的关键证据,而不是冷冰冰的原始事件流。

选 MCP 而不是自己写一套私有接口,还有一个很现实的原因:生态。目前主流 AI 客户端都原生支持 MCP,你写一个标准 MCP Server,就能同时服务多个工具。我自己就是先在 Claude Desktop 里测通,再切到 Cherry Studio 用,几乎零成本迁移。

1.3 整体架构:浏览器 CDP 加 MCP Server 双端协作

这个项目整体只有三条链路:

  1. 目标浏览器(或 WebView)开启远程调试端口。
  2. 一个 Node.js 进程作为 MCP Server,通过 CDP 连上浏览器,订阅 console 和 network 事件,并缓存到一个环形缓冲结构里。
  3. AI 客户端(Claude Desktop / Cherry Studio 等)通过 MCP 发现工具,读取缓存数据,按需分析。

为什么直接选 CDP,而不是去调 vConsole 的插件 API?我最初试过 vConsole 的插件机制。vConsole 确实支持自定义插件,插件里也能拿到 log 面板的数据,但有几个问题:一是要改业务代码去初始化插件,侵入性太强;二是 vConsole 插件拿到的数据格式偏 UI,很多内部字段还拿不到;三是如果页面加载早期就报错,vConsole 本身还没 ready,那部分日志就丢了。CDP 是浏览器底层协议,页面里不管跑的是不是 vConsole,console 消息、网络请求事件都会经过调试端口。从 CDP 拿数据,才是真正意义上的"任何 H5"。

我保留 vConsole 的真实价值在于:它负责给人类开发者看,CDP 通道负责给 AI 看。两者并行,互不干扰。但要注意一个坑——如果页面里装了 vConsole,它自己也会输出一些日志(比如插件初始化的 log),CDP 捕获的时候需要按来源过滤,不然 AI 会被 vConsole 自身的信息干扰。这个我在 2.3 节会细说。

2. 核心细节解析与实操要点

2.1 日志数据从哪里来:CDP 事件深度解读

CDP 是通过 WebSocket 和浏览器调试端口通信的。要监听 H5 页面里 console 日志,核心事件是Runtime.consoleAPICalled。每次页面调用console.log、console.error之类的方法,浏览器都会把这个事件推到调试端。但事件里的参数不是纯字符串,而是一堆RemoteObject,比如一个对象会被序列化成{type: "object", className: "Object", description: "{...}"},如果你直接把description丢给 AI,会丢掉很多细节,尤其嵌套对象会被折叠成{...}。

所以我在 MCP Server 里做了一个格式化层:递归展开 RemoteObject,把对象、数组、函数、DOM 节点都转成可读文本。简单对象直接抽preview.properties,深层对象调用Runtime.getProperties拿完整属性。函数就输出它的字符串表现形式。DOM 节点则补上标签、id、class 这些信息。这套展开逻辑是整个工具里最容易出 bug 的部分,因为实际项目里的日志千奇百怪,你永远猜不到用户会在 console 里扔一个什么玩意儿。

网络请求的监听靠两组事件:Network.requestWillBeSent和Network.responseReceived。前者记录请求 URL、方法、请求头;后者记录状态码、响应头、耗时。要做request body和response body的抓取,还得额外调用Network.getRequestPostData和Network.getResponseBody。这里有个很关键的性能点:不能每个请求都去拉响应体,尤其像上传图片、大 JSON 接口,响应体可能几百 KB。策略是默认只记录元信息,只有当 AI 明确请求查看某个链接的具体响应时,才按需去取 body。这是给 AI 省上下文也是最聪明的一步。

2.2 MCP Server 的工具设计:少而精最重要

MCP 工具设计跟写 REST API 完全是两个思路。API 面向人,能多做几个接口就多做,人不在乎多一次点击。但 MCP 面向 AI,工具越多,AI 选错工具的概率越大,上下文占用也越大。所以我第一版做的时候故意砍掉了很多"看起来有用"的功能,只留了五个工具:

工具名作用核心参数
get_recent_logs获取最近 console 日志limit, level, keyword
get_network_requests获取最近网络请求limit, url, status
get_page_info获取当前页面 URL、标题、UA无
evaluate_js在页面执行一段 JS 并返回结果expression
clear_debug_data清空本地缓存的日志/请求缓冲无

这五个工具撑起了 90% 的调试场景。get_recent_logs是主力,AI 遇到问题第一步通常是调它,看看有没有红色错误;get_network_requests用来排查接口挂了、参数传错、状态码异常这类问题;evaluate_js则是让 AI 在页面里做验证,比如执行一个表达式看看当前某个全局变量到底是什么值。有一次排查登录态失效问题,AI 就是靠 evaluate_js 执行localStorage.getItem('token'),发现 token 存的 key 名跟后端对不上,一秒钟定位。

工具参数也要克制。limit默认给 200,上限设 500。为什么?因为 AI 的上下文窗口有限,一次性塞 200 条日志已经很考验提取能力了,再多没意义。我还加了level参数,支持error/warn/info/debug,AI 可以先只筛 error,缩小范围,再决定要不要看完整信息。这个分层的设计非常实用,你可以让 AI "先看 error,如果没发现再看 warn"。

2.3 数据结构化、去重与上下文控制

CDP 原始事件是流式的,而且同一个错误经常被多次触发,比如 React 严格模式在开发环境下会执行两次渲染,导致console.error出现两遍。日志不去重,AI 很容易被重复信息带偏。我在缓存时维护了一个指纹机制:把消息内容 + 发生时间(秒级) + 日志级别做一次 hash,重复消息会累计 count,只保留首次出现的时间戳。这样 AI 看到的是"同样的错误出现了 12 次",而不是 12 条一模一样的记录。

还有一个细节:过滤 vConsole 自身日志。页面加载 vConsole 时,它会打印一些初始化信息,通常是[vConsole]开头或者是某个VConsoleclass 的 log。我在get_recent_logs返回之前会先做一次字符串扫描,把[vConsole]开头的记录剔除掉(但保留[vConsole]之外的报错内容)。实测下来,这个过滤能减少约 5% 的噪音,配合 level 参数后效果更明显。

上下文控制的核心思想是:给 AI"最小充分集"。不是把原始日志全部倒出去,而是先做裁剪、去重、时间排序,再交出去。比如一个 50MB 的日志文件,MCP 工具返回的可能是整理后的 2KB 摘要——但保留了错误级别、关键栈帧、重复次数。很多调试场景下,AI 拿到这 2KB 就够定位了,不够它再按需展开。这个思路跟你在生产环境排查日志的思路是一样的:先看错误,再看时间窗口,最后才看具体堆栈。

3. 实操:从零搭一套 vConsole MCP debug 链路

3.1 环境准备:浏览器调试端口怎么开

先说最基本的准备工作。你需要一个 Node.js 18+ 环境,一个 Chrome 或 Edge(Chromium 内核),以及一个支持 MCP 的 AI 客户端。浏览器调试端口是关键,CDP 建立在远程调试协议之上,浏览器必须以--remote-debugging-port=9222参数启动,否则调试端口根本不会开。

我平时开发 H5 常用的是独立调试用户目录,避免和日常浏览器的会话冲突。推荐这么启动浏览器:

# Windows chrome.exe --remote-debugging-port=9222 --user-data-dir=/tmp/vconsole-mcp-profile # macOS /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/vconsole-mcp-profile # Linux google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/vconsole-mcp-profile

--user-data-dir这个参数很重要。不指定的话,Chrome 会用默认用户目录,如果平时已经开着 Chrome,第二个实例启动时会把所有参数转发给已有实例,调试端口不会生效。把 user-data-dir 单独指出来,等于起了一个完全独立的浏览器实例,互不干扰。启动后访问 http://localhost:9222/json,能看 JSON 列表就说明调试端口生效了。

如果你需要测的是桌面版页面,直接用这个 Chrome 窗口打开 target 页面就行。要是测的是手机端,可以用 DevTools 的设备模拟模式;如果测的是 WebView,那就走 4.1 节的远程转发方案。

3.2 用 TypeScript 搭一个最小 MCP Server

我推荐直接用官方 SDK@modelcontextprotocol/sdk,它把协议细节都封装好了,你只需要注册工具和处理回调。先建一个 Node 项目:

mkdir vconsole-mcp-server cd vconsole-mcp-server npm init -y npm install @modelcontextprotocol/sdk ws

然后建一个index.ts,写一个最简骨架:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "vconsole-mcp", version: "1.0.0" }); server.tool( "get_recent_logs", "获取页面最近 console 日志,level 可选 error/warn/info/debug", { limit: z.number().default(200), level: z.string().optional(), keyword: z.string().optional() }, async ({ limit, level, keyword }) => { // TODO: 从 CDP 缓存里筛选日志 return { content: [{ type: "text", text: JSON.stringify([]) }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);

一个 MCP Server 其实就是一个 stdio 进程,通过标准输入输出和 AI 客户端通信。StdioServerTransport就是把协议消息接到 stdin/stdout 上。注册工具用的server.tool方法,参数分别是工具名、描述、参数 schema、执行函数。描述写得越清楚,AI 越不容易用错,比如"日志 time 字段是毫秒级时间戳",这类细节都要写进去。

3.3 核心难点:CDP 数据流接入

CDP 连接部分我直接用ws库连浏览器的 WebSocket 端点。先给每个页面 target 建立一个连接,然后启用 Runtime 和 Network 域:

import WebSocket from "ws"; const targetList = await fetch("http://localhost:9222/json").then(res => res.json()); const pageTarget = targetList.find(t => t.type === "page"); const ws = new WebSocket(pageTarget.webSocketDebuggerUrl); ws.on("open", () => { ws.send(JSON.stringify({ id: 1, method: "Runtime.enable" })); ws.send(JSON.stringify({ id: 2, method: "Network.enable" })); ws.send(JSON.stringify({ id: 3, method: "Page.enable" })); });

之后所有 CDP 事件都会以 JSON 消息推过来,你要做的就是在ws.on("message")里解析method字段,判断是不是Runtime.consoleAPICalled或Network.requestWillBeSent,然后走缓存逻辑。console 事件里args是 RemoteObject 数组,必须按 2.1 节说的方式展开,展开后的纯文本才是 AI 能直接看的东西。

网络事件要按requestId把对应的请求和响应拼起来。实际开发里 A 请求发出去,可能隔了一会儿响应包事件才到达,中间会穿插其他请求的事件。我维护了一个 Map,key 是 requestId,value 是记录对象,等到Network.responseReceived到了之后,把完整记录推入环形缓冲。这样 AI 看到的请求记录里,URL、参数、状态码、耗时都在同一条里,结构非常清晰。

3.4 挂到 MCP 客户端:Claude Desktop 和 Cherry Studio 的配置

Server 写好后,集成到 AI 客户端是最简单的一步。Claude Desktop 的配置文件在claude_desktop_config.json,加一段 mcpServers 配置:

{ "mcpServers": { "vconsole-mcp": { "command": "npx", "args": ["tsx", "/absolute/path/to/vconsole-mcp-server/index.ts"] } } }

Cherry Studio 这类开源客户端也在设置里提供 MCP 配置入口,填法几乎一样。配好后重启客户端,AI 助手就能看到所有注册的工具了。

怎么确认连上了?直接在聊天里让 AI 调用一下工具。比如问它"用 get_recent_logs 看看当前页面有没有报错"。如果配置没问题,它会自动调用工具,并在回复里把日志分析结果列出来。第一次跑通这个流程的时候,我有点兴奋——那一刻相当于真正让 AI 长出了一双可以盯 H5 页面的眼睛。

3.5 一次完整的 debug 实战记录

举一个真实跑通的案例。我在本地起了一个 SPA 项目,路由跳转到/order后白屏,Console 里有这么一条错误:Uncaught TypeError: Cannot read properties of undefined (reading 'map')。传统流程是我自己去 vConsole 面板里翻栈,找是哪个组件调用了.map。现在换成 AI 流程。

我输入的指令是:"用 vConsole MCP 查看最近日志,定位白屏原因。" AI 自动调用了get_recent_logs,拿到 200 条日志,发现 error 级别只有这一条,然后它追到get_network_requests,发现/api/order/list接口返回的data数组是null,而页面拿到null后直接.map就崩了。最后它用evaluate_js在页面上执行了一段表达式,验证了数据结构确实异常。整个定位过程不到 20 秒,以前我能找十分钟。

这还不是最爽的部分。最爽的是你能让它在改完代码后自己验证:让 AI 执行evaluate_js刷新接口数据,再重新调一次get_recent_logs看报错是否消失。这个"发现错误 -> 定位 -> 修复 -> 验证"的闭环一旦跑通,H5 debug 效率直接上一个台阶。当然,它不能替代你理解业务逻辑,但它能把最耗时的"找证据"环节压缩到极致。

4. 真实使用中的常见坑与排查方法

4.1 手机真机 / WebView 环境连不上 CDP 怎么办

本地 Chrome 调试没问题,但很多人要调的是 Android WebView 里的 H5。安卓 WebView 支持远程调试,只是要先在设备上开启开发者选项和 USB 调试,然后电脑浏览器打开chrome://inspect,找到对应 WebView,点击 inspect 会自动开一个调试窗口。这个调试窗口里同样有 CDP 调试端口,只是是"转发"出来的。实际操作里,chrome://inspect会自动映射本地端口,你可以从 http://localhost:9222/json 看到这些 target(前提是你把 chrome://inspect 的 Discover 开起来)。

麻烦的是 iOS WebView。Safari 的远程调试依赖 macOS 的 Safari 开发者菜单,而且 CDP 协议不适用(Safari 走的是 WebKit 私有的 Protocol)。如果你主要调 iOS,务实的替代方案是:在页面里内嵌一段轻量 SDK,专门把 console 和 fetch 拦截后的数据通过 WebSocket 上报到本地 MCP Server。这个方案不依赖 CDP,但侵入性更强,而且只对加了 SDK 的页面有效。我给项目留了一个 "SDK mode" 的扩展开关,需要时再切过去。它显然没有 CDP 通用,但在 iOS 上是目前最省事的路。

还有一个容易忽略的安全点:CDP 调试端口没有任何鉴权,任何人只要能访问到那个端口就能拿到页面数据、执行 JS。所以远程调试时,一定不要暴露公网,VPC 内网或者 localhost 就好。

4.2 AI 上下文被日志塞爆,回答质量断崖式下降

这个坑我摔得最重。最初启用 MCP 工具时,我把默认 limit 设成 1000,结果 AI 收到 1000 条日志后,开始疯狂分析日志里无关紧要的 warn,甚至把主要内容挤出了上下文窗口。MCP 工具返回值会被完整塞进对话上下文,你给它 1000 条日志,就等于让它带 1000 条"记忆"处理新问题,效率必然下降。

后面我把设计改成"分层返回":默认只返回等级为 error 和 warn 的最近 50 条,AI 觉得不够再显式请求所有日志或某一类日志。这个调整立竿见影,AI 的回答质量和速度都有改善。你可以把 MCP 工具设计想象成一个"电梯简报",自动帮你把核心信息总结好,而不是把整座大楼结构图都塞给你。

4.3 SPA 路由切换、iframe 嵌套导致日志丢失

SPA 路由切换本身不会丢日志,因为 CDP 的 console 事件是持续推送的,但如果你在页面跳转时刷新了 target 或整个 MCP Server 的缓存,之前积累的日志就被清了。所以两个选择:一是保持缓存长期不清,按时间窗口截取;二是监听Page.frameNavigated,在导航发生时打一个标记,让 AI 知道"日志从某条开始是新页面的"。我实际用的是后者,配合 vConsole 的日志时间戳,能比较准确地切分会话。

iframe 是个更隐蔽的问题。页面上内嵌了跨域 iframe,子 iframe 的 console 日志默认不在主 frame 的 CDP 事件里。要在 CDP 里捕获所有 frame 需要开启Target.setAutoAttach或使用flatten模式。对多数调试场景,推荐一个简单办法:让用户或 AI 切换到对应 target。http://localhost:9222/json 会把每个 frame 也列为 target,从 target 列表里挑出子 iframe,用它的 webSocketDebuggerUrl 单独连接。虽然麻烦一点,但不会漏信息。

4.4 端口冲突、浏览器实例崩溃与日志量过大的排查

开发环境里 9222 端口经常被 Webpack devServer 或 Vite 的旧进程占用。启动浏览器前先确认端口没有被其他进程占用,有就用--remote-debugging-port=9223换个端口,MCP Server 侧把端口配置抽成环境变量,改起来不费劲。

浏览器实例崩溃的常见诱因是动了--user-data-dir对应的目录权限,或者磁盘空间不足。排查方式很直接:看终端里浏览器进程是否还活着,访问/json是否能正常拉取。值得注意,Headless 模式下要加--remote-debugging-port才能调试;不加的话数据流也是通的,但很多 AI 客户端需要抓截图时就会失灵。

日志量过大导致的另一个表现是 MCP Server 内存飞涨。我维护的环形缓冲默认上限是每条日志 20KB、总条数 3000 条,超出后最旧的自动丢弃。这样能保证内存占用稳定在几十 MB 级别,跑一整天也不会有问题。

结合走查的经验谈一点小扩展

最后分享一个我实际用出来的体会:这个项目的价值不在于"替你做决定",而在于把散落在各种角落的证据串成一条线。vConsole 负责给人看,CDP 负责给机器看,MCP 负责把机器看到的翻译给 AI。三个环节缺一个,debug 闭环都不完整。个人经验里最建议的一点是,给你的 MCP 工具加上clear_debug_data,每次 start 一个新调试会话前清一次旧缓存,否则 AI 会被历史日志干扰。

后续我还计划扩展两件事:一是接 performance entries,把页面性能指标也暴露给 AI,排查白屏、卡顿可以直接看 LCP 和 FCP;二是做一个远程日志中心适配,让本地方案延伸到线上页面,前提是做好脱敏和白名单。这个方向我已经验证过可行性,跑通后会再整理一篇更完整的方案出来。在你搭建过程中如果遇到我上面没提到的坑,欢迎在评论区分享,我们互相补补课。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进