1. 移动端调试的痛点与破局思路
做过 H5 的同学都懂那种感觉:本地跑得好好的页面,一上真机就各种玄学问题。按钮点不动、接口偶尔 500、白屏闪一下又好了,最要命的是——你根本看不到控制台。手机连电脑、开远程调试、装驱动、配端口,一套流程走下来,问题可能已经复现不出来了。更别提有些场景压根连不上调试器,比如嵌在 App WebView 里的页面、第三方渠道的 H5、用户手机上的偶发 bug。
传统方案无非这么几种:一是用alert大法,把变量一个个弹出来看,效率低到令人发指;二是引入 vConsole 这类移动端调试面板,在页面右下角挂一个悬浮按钮,点开就能看日志、网络请求、DOM 结构。vConsole 确实好用,我自己在项目里也用了好几年,但它有个天然局限——你得用眼睛去看。日志多了要翻,请求多了要筛,而且它只存在于那台设备上,你没法把信息同步到别的地方做进一步分析。
这两年 AI 辅助编程越来越普及,我就在想一个问题:能不能让 AI 直接“看见” H5 页面里发生了什么?不是我把日志复制粘贴给它,而是它自己就能读取 vConsole 里的日志和网络请求,然后帮我分析问题出在哪。这个想法听起来有点科幻,但 MCP(Model Context Protocol)的出现让这件事变得可行。MCP 本质上是一套让 AI 模型和外部工具、数据源对话的协议,你可以把它理解成 AI 的“USB 接口”——只要设备支持这个接口,AI 就能直接调用它。
所以这个项目的核心思路就很清晰了:把 vConsole 采集到的日志和网络请求,通过 MCP 协议暴露出去,让 AI 能够实时读取并参与 debug。这样一来,你不需要把日志复制来复制去,AI 直接就能看到页面在真机上到底发生了什么。对于经常和 H5 打交道的前端、测试、甚至产品同学来说,这套东西能省下大量沟通和排查成本。接下来我会从整体设计、核心实现、实操步骤、踩坑经验几个维度,把整个方案拆开讲清楚。
2. 整体架构设计与技术选型考量
2.1 为什么是 vConsole + MCP 这个组合
先说 vConsole。它是一个轻量级的移动端调试面板,核心能力是劫持console对象、拦截XMLHttpRequest和fetch、监听window.onerror,然后把收集到的信息渲染成一个可交互的面板。它的优势在于侵入性小、接入简单,一行代码就能挂上去,而且对性能影响可控。市面上类似的还有 Eruda,功能更全但体积也更大。我选 vConsole 主要是因为它足够轻,而且 API 设计比较清晰,方便我做二次开发。
再说 MCP。MCP 是 Anthropic 推出的开放协议,目的是让 AI 模型能够安全、标准化地访问外部工具和数据。它的架构是典型的客户端-服务端模式:MCP Server 负责暴露资源(Resources)和工具(Tools),MCP Client(比如 Claude Desktop、Cursor 等)负责调用。对于我们的场景来说,vConsole 采集的数据就是“资源”,AI 通过 MCP 协议来读取这些资源,就能实现“看见日志”的效果。
那为什么不用 WebSocket 直接推给 AI 呢?因为 AI 模型本身并不具备主动连接 WebSocket 的能力,它需要一个中间层来桥接。MCP 就是这个中间层,它把“读取日志”这个动作标准化成了 AI 可以理解的接口。当然,底层的数据传输我确实用了 WebSocket,因为 H5 页面和本地服务之间需要实时通信,WebSocket 的全双工特性正好合适。
2.2 数据流转的完整链路
整个系统的数据流是这样的:H5 页面加载 vConsole 的定制版本,这个版本在原有功能基础上增加了一个 WebSocket 客户端。当页面产生日志或网络请求时,vConsole 照常收集,同时通过 WebSocket 把数据推送到本地运行的 MCP Server。MCP Server 收到数据后,一方面缓存起来,另一方面通过 MCP 协议暴露给 AI 客户端。AI 客户端在需要的时候调用 MCP 工具,就能拿到最新的日志和请求列表。
这里有个关键设计点:数据是推还是拉。我选择的是推拉结合——WebSocket 负责实时推送,保证数据不丢;MCP 工具负责按需拉取,保证 AI 拿到的是它真正需要的部分。如果只推不拉,AI 会被大量无关日志淹没;如果只拉不推,又可能错过瞬时错误。推拉结合的好处是,MCP Server 可以维护一个环形缓冲区,只保留最近 N 条记录,AI 查询时按时间戳或关键字过滤,既高效又不会丢关键信息。
另一个设计点是多页面支持。实际项目中经常同时开好几个 H5 页面,每个页面都有自己的 vConsole 实例。我在 WebSocket 连接建立时会给每个页面分配一个唯一的clientId,MCP Server 按clientId分组管理数据。AI 查询时可以指定clientId,也可以查询所有页面的汇总信息。这个设计在调试多页面应用时特别有用,比如你可以在一个页面操作,然后在另一个页面观察接口返回。
2.3 安全边界与本地化部署
安全方面我做了几层考虑。首先,MCP Server 只监听本地回环地址,不对外网开放,避免日志数据泄露。其次,WebSocket 连接需要携带一个简单的 token,这个 token 在服务启动时生成,页面接入时需要配置。虽然不是什么强安全机制,但能挡住大部分误连和扫描。最后,所有数据只存在内存里,不落盘,服务重启即清空,避免敏感信息残留。
本地化部署还有个好处是延迟低。WebSocket 走本地回环,日志从页面到 MCP Server 基本是毫秒级,AI 查询时几乎感觉不到延迟。我实测下来,从页面console.log到 AI 能读到,整个过程在 50ms 以内,对于 debug 场景完全够用。
3. 核心细节解析与实操要点
3.1 vConsole 定制版的改造要点
原版 vConsole 是不带 WebSocket 推送能力的,所以第一步是改造它。我的做法是继承 vConsole 的VConsole类,重写它的console插件和network插件,在原有逻辑之后追加推送逻辑。具体来说,console插件在printLog方法里,除了渲染到面板,还会调用ws.send()把日志对象序列化后发出去。network插件则在onResponse回调里推送请求详情。
这里有个细节要注意:序列化时要处理循环引用。日志里经常会有 DOM 对象、Window 对象,直接JSON.stringify会报错。我的做法是写一个safeStringify函数,遇到循环引用就替换成[Circular],遇到函数就替换成[Function],遇到 DOM 节点就取tagName和id。这样既能保留关键信息,又不会因为序列化失败丢日志。
另一个细节是日志分级。vConsole 本身支持log、info、warn、error等级别,我在推送时也保留了这个字段。MCP Server 收到后按级别分类存储,AI 查询时可以只查error级别的日志,快速定位问题。实测下来,这个过滤功能在日志量大的时候特别有用,能把排查范围从几百条缩小到几条。
3.2 MCP Server 的资源与工具设计
MCP Server 这边我定义了两个核心资源:logs和requests。logs资源返回所有日志的列表,支持按clientId、level、keyword过滤;requests资源返回所有网络请求,支持按clientId、status、url过滤。资源的设计遵循 MCP 规范,用 URI 模板来暴露,比如vconsole://logs/{clientId}就能拿到指定页面的日志。
工具方面我定义了三个:get_logs、get_requests、clear_data。get_logs接受clientId、level、keyword、limit四个参数,返回过滤后的日志数组。get_requests类似,多了status参数用来筛选 HTTP 状态码。clear_data用来清空指定页面的缓存,方便开始新一轮调试。工具的参数设计尽量简单,因为 AI 调用时不会做太复杂的推理,参数越直观越好。
这里有个经验:工具描述要写清楚。MCP 协议里每个工具都有description字段,AI 会根据这个描述来决定什么时候调用。我一开始写得太简略,AI 经常不知道该用哪个工具。后来我把每个参数的用途、返回值的格式、典型使用场景都写进去,AI 的调用准确率明显提升。比如get_logs的描述里我写了“当用户询问页面报错、日志输出、console 信息时使用此工具”,AI 就能在合适的时机自动调用。
3.3 WebSocket 通信的稳定性保障
WebSocket 连接是整套系统的命脉,一旦断了,日志就推不过来。所以我做了几层保障。第一层是心跳机制:客户端每 30 秒发一次 ping,服务端回 pong,如果连续两次没收到 pong,就认为连接已断,触发重连。第二层是自动重连:客户端检测到onclose事件后,延迟 1 秒重连,重连成功后把断线期间的日志补推上去。第三层是消息队列:如果 WebSocket 暂时不可用,日志先存到本地队列,等连接恢复后再批量发送。
心跳间隔的选择也有讲究。太短了浪费资源,太长了检测不及时。我试过 10 秒、30 秒、60 秒,最后定在 30 秒。因为 H5 页面在后台时,浏览器可能会节流定时器,30 秒是个比较平衡的值,既不会太频繁,又能在页面回到前台时快速恢复。另外,重连时的补推逻辑要注意去重,我给每条日志加了自增 ID,服务端收到后按 ID 去重,避免重复记录。
还有一个坑是页面刷新时的连接清理。H5 页面刷新后,旧的 WebSocket 连接会断开,但服务端可能还没感知到。我的做法是在beforeunload事件里主动发送一个close消息,服务端收到后立即清理对应的clientId数据。如果不做这一步,服务端会残留很多僵尸连接,时间长了内存会涨。
4. 完整实操流程与关键环节实现
4.1 环境准备与依赖安装
先列一下需要的东西。Node.js 版本建议 18 以上,因为 MCP SDK 用了一些较新的 API。包管理用 npm 或 pnpm 都行,我习惯用 pnpm,速度快一些。核心依赖有三个:@modelcontextprotocol/sdk用来实现 MCP Server,ws用来做 WebSocket 服务,vconsole作为基础库。另外还需要一个 MCP 客户端来测试,我用的是 Claude Desktop,你也可以用 Cursor 或其他支持 MCP 的工具。
安装命令很简单:
pnpm init pnpm add @modelcontextprotocol/sdk ws vconsole pnpm add -D typescript @types/ws @types/nodeTypeScript 配置里记得把target设为ES2022,module设为NodeNext,这样能直接用顶层的await。tsconfig.json的关键配置如下:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "strict": true, "esModuleInterop": true } }4.2 MCP Server 的核心代码实现
先写 WebSocket 服务部分。创建一个WebSocketServer实例,监听 8765 端口。每个连接进来时,从 URL 参数里取clientId,然后把这个连接存到一个 Map 里。收到消息时,解析 JSON,根据type字段分发到不同的处理函数。
import { WebSocketServer, WebSocket } from 'ws'; const clients = new Map<string, WebSocket>(); const logBuffer = new Map<string, any[]>(); const requestBuffer = new Map<string, any[]>(); const wss = new WebSocketServer({ port: 8765 }); wss.on('connection', (ws, req) => { const url = new URL(req.url!, `http://${req.headers.host}`); const clientId = url.searchParams.get('clientId') || 'default'; clients.set(clientId, ws); if (!logBuffer.has(clientId)) logBuffer.set(clientId, []); if (!requestBuffer.has(clientId)) requestBuffer.set(clientId, []); ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.type === 'log') { const buffer = logBuffer.get(clientId)!; buffer.push(msg.payload); if (buffer.length > 1000) buffer.shift(); } else if (msg.type === 'request') { const buffer = requestBuffer.get(clientId)!; buffer.push(msg.payload); if (buffer.length > 500) buffer.shift(); } }); ws.on('close', () => { clients.delete(clientId); }); });这段代码的关键点是环形缓冲区。日志最多存 1000 条,请求最多存 500 条,超出就丢掉最旧的。这样既能保证内存可控,又不会因为日志太多导致查询变慢。实际调试时,1000 条日志足够覆盖大部分场景,如果不够可以调大,但要注意内存占用。
接下来是 MCP Server 部分。用@modelcontextprotocol/sdk创建 Server 实例,注册资源和工具。资源用server.resource()注册,工具用server.tool()注册。每个工具的回调函数里,从缓冲区读取数据,按参数过滤后返回。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; const server = new Server( { name: 'vconsole-mcp', version: '1.0.0' }, { capabilities: { resources: {}, tools: {} } } ); server.tool( 'get_logs', '获取指定页面的控制台日志。当用户询问页面报错、日志输出、console 信息时使用此工具。', { clientId: { type: 'string', description: '页面标识,不传则返回所有页面' }, level: { type: 'string', description: '日志级别:log/info/warn/error' }, keyword: { type: 'string', description: '关键字过滤' }, limit: { type: 'number', description: '返回条数,默认 50' } }, async ({ clientId, level, keyword, limit = 50 }) => { let logs: any[] = []; if (clientId) { logs = logBuffer.get(clientId) || []; } else { for (const buf of logBuffer.values()) logs = logs.concat(buf); } if (level) logs = logs.filter(l => l.level === level); if (keyword) logs = logs.filter(l => JSON.stringify(l).includes(keyword)); logs = logs.slice(-limit); return { content: [{ type: 'text', text: JSON.stringify(logs, null, 2) }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);这里有个细节:返回格式要用content数组。MCP 协议规定工具返回值必须是{ content: [...] }结构,每个元素有type和text字段。我一开始直接返回数组,AI 客户端解析不了。后来改成标准格式就正常了。另外,text字段里我用JSON.stringify格式化了一下,加了缩进,AI 读起来更清晰。
4.3 H5 页面接入与 vConsole 改造
页面这边需要引入改造后的 vConsole。我把它打包成一个单独的 JS 文件,通过<script>标签引入。初始化时传入 WebSocket 地址和clientId,然后 vConsole 就会自动开始推送数据。
<script src="./vconsole-mcp.js"></script> <script> new VConsoleMCP({ wsUrl: 'ws://127.0.0.1:8765', clientId: 'page-' + Date.now(), token: 'your-token-here' }); </script>改造 vConsole 的核心是重写console插件的printLog方法。原版方法只负责渲染,我在后面追加了推送逻辑:
const originalPrintLog = VConsole.prototype.printLog; VConsole.prototype.printLog = function(level, args) { originalPrintLog.call(this, level, args); if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'log', payload: { level, args: args.map(safeStringify), timestamp: Date.now() } })); } };网络请求的拦截类似,重写XMLHttpRequest.prototype.open和send,在onreadystatechange里推送请求详情。注意要保留原始方法,不能影响页面正常请求。我试过直接替换XMLHttpRequest,结果有些库不兼容,后来改成只劫持open和send,在回调里追加逻辑,兼容性就好多了。
4.4 MCP 客户端配置与联调
以 Claude Desktop 为例,配置文件在~/Library/Application Support/Claude/claude_desktop_config.json(macOS)或%APPDATA%\Claude\claude_desktop_config.json(Windows)。在mcpServers字段里加上我们的服务:
{ "mcpServers": { "vconsole": { "command": "node", "args": ["/path/to/your/dist/server.js"] } } }配置好后重启 Claude Desktop,在对话框里输入“帮我看看页面有什么报错”,AI 就会自动调用get_logs工具,把 error 级别的日志拉出来分析。我实测下来,从页面报错到 AI 给出分析,整个过程不到 10 秒,比手动复制日志快太多了。
联调时有个小技巧:先用clear_data清空缓存。因为缓冲区里可能残留之前的日志,不清空的话 AI 会看到无关信息。我一般在开始新一轮调试前,先让 AI 调用clear_data,然后再操作页面复现问题,这样日志最干净。
5. 常见问题与排查技巧实录
5.1 WebSocket 连接失败排查
最常见的问题是 WebSocket 连不上。症状是页面控制台报WebSocket connection failed,MCP Server 那边没有任何连接记录。排查思路分三步:先确认服务是否启动,用netstat -an | grep 8765看端口有没有监听;再确认地址是否正确,ws://127.0.0.1:8765和ws://localhost:8765在某些环境下行为不一样,建议统一用127.0.0.1;最后确认防火墙有没有拦截,本地回环一般不会,但有些安全软件会管。
如果服务启动了、地址也对,还是连不上,那可能是端口被占用。换个端口试试,比如 8766。我遇到过好几次端口冲突,都是因为之前启动的服务没关干净。建议在服务启动时加个端口检测,如果被占用就自动换一个,或者直接报错提示。
5.2 日志丢失或延迟的排查
日志丢失通常有两个原因。一是缓冲区溢出,日志太多把旧记录挤掉了。解决办法是调大缓冲区,或者用clear_data及时清理。二是WebSocket 断线期间的数据没补推。检查重连逻辑里的补推队列是否正常工作,可以在onopen回调里打印一下队列长度,确认有没有积压。
延迟问题一般是心跳间隔太长导致的。如果页面在后台被节流,心跳可能延迟到几分钟才发一次,服务端会误判为断线。我的做法是在visibilitychange事件里监听页面可见性,页面回到前台时立即发一次心跳,这样能快速恢复连接状态。
5.3 AI 读不到日志的排查
有时候 MCP Server 明明收到了数据,但 AI 就是读不到。这种情况先检查工具调用是否成功。在 Claude Desktop 里,工具调用会有个折叠面板,展开能看到返回内容。如果返回空数组,说明过滤条件太严了,比如level设成了error但页面只有log。把条件放宽再试。
另一个可能是clientId 不匹配。页面初始化时生成的clientId和 AI 查询时传的不一致,就会查不到数据。建议在页面初始化时把clientId打印到控制台,AI 查询时直接复制这个值。我后来干脆在 MCP Server 里加了个list_clients工具,AI 可以先查有哪些页面在线,再指定clientId查询,省得手动对。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| WebSocket 连接失败 | 服务未启动/端口占用/地址错误 | 检查端口监听、确认地址 | 启动服务、换端口、用 127.0.0.1 |
| 日志丢失 | 缓冲区溢出/断线未补推 | 查看缓冲区大小、检查补推队列 | 调大缓冲区、修复重连逻辑 |
| 日志延迟 | 心跳间隔太长/页面后台节流 | 检查心跳日志、监听可见性 | 缩短心跳、页面回前台立即心跳 |
| AI 读不到日志 | 过滤条件太严/clientId 不匹配 | 放宽条件、核对 clientId | 调整参数、用 list_clients 查询 |
| 页面刷新后数据残留 | 旧连接未清理 | 检查服务端连接 Map | beforeunload 主动关闭连接 |
5.5 几个实操心得
第一个心得是日志分级要合理。不要把什么信息都往error级别塞,否则 AI 查询时会被大量误报淹没。我的做法是:真正的异常用error,警告用warn,普通信息用log,调试细节用info。这样 AI 查error时就能快速定位到真正的问题。
第二个心得是请求体要截断。有些接口的请求体特别大,比如上传文件,直接把整个 body 推过去会撑爆缓冲区。我的做法是只保留前 500 个字符,超出部分用...[truncated]代替。响应体同理,只保留前 1000 个字符。这样既能看清请求内容,又不会因为数据太大影响性能。
第三个心得是给 AI 加个上下文提示。在 MCP Server 的instructions字段里,我写了一段说明,告诉 AI 这个服务是用来调试 H5 页面的,日志和请求分别代表什么,查询时应该注意什么。这样 AI 在调用工具时会更准确,不会问一些无关的问题。实测下来,加了这段说明后,AI 的分析质量明显提升。
6. 扩展方向与个人体会
这套东西跑通之后,我又试了几个扩展方向。一个是把日志和请求关联起来,比如某个请求失败时,自动把前后 5 秒的日志一起返回,这样 AI 能看到完整的上下文。另一个是加个截图功能,页面报错时自动截个图,通过 MCP 传给 AI,让它能看到页面长什么样。截图用html2canvas实现,虽然有点重,但在排查样式问题时特别有用。
还有个方向是多端聚合。现在只支持 H5,但很多项目是 App + H5 混合的。如果能通过某种方式把 App 原生日志也接进来,AI 就能同时看到原生和 H5 的信息,排查跨端问题会更方便。这个还在探索中,主要难点是原生端的接入方式,Android 和 iOS 各有各的坑。
我个人在实际操作中的体会是,这套方案最大的价值不是“让 AI 看日志”这个动作本身,而是改变了 debug 的协作模式。以前排查问题,前端要看日志、测试要复现、后端要查接口,信息在几个人之间传来传去,效率很低。现在 AI 直接读日志,你只需要描述现象,它就能给出可能的原因和排查方向。虽然 AI 不一定能直接定位到根因,但它能帮你快速缩小范围,省下大量翻日志的时间。
最后再分享一个小技巧:把 MCP Server 做成常驻服务。我一开始每次调试都手动启动,后来用pm2或systemd把它做成后台服务,开机自启,这样随时都能用。配合clear_data工具,每次调试前清一下缓存,体验很流畅。如果你经常调试 H5,这套东西值得花半天时间搭起来,后面省下的时间绝对不止半天。