ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

vConsole + MCP:让AI直接读取H5日志与网络请求

vConsole + MCP:让AI直接读取H5日志与网络请求 1. 移动端调试的困局为什么AI看不见你的H5日志做过H5开发的人都有一个共同的痛页面在手机上跑日志在手机里而你坐在电脑前。Chrome DevTools 的远程调试虽然能用但限制极多——需要USB连接、需要开启开发者模式、部分浏览器内核根本不支持、iOS上的Safari Inspector还经常断连。更别提那些嵌在App WebView里的页面你连入口都找不到。传统的做法是在页面里手动挂一个vConsole然后在手机屏幕上戳来戳去。这个方案本身没问题vConsole确实是移动端调试的利器它能在页面上生成一个悬浮面板展示console日志、网络请求、DOM结构、Storage等信息。但问题在于这些信息只有人眼能看到AI看不到。现在大家都在用AI辅助debug你把代码贴给AIAI帮你分析问题。但AI能拿到的只有你手动复制粘贴的那几行日志上下文严重缺失。你告诉AI接口报错了AI问你报什么错你再去vConsole里翻翻到了再贴过去。这个来回过程极其低效而且经常漏掉关键信息——比如请求头里的某个字段、响应体的完整结构、报错前后的日志顺序。MCPModel Context Protocol的出现改变了这个局面。MCP本质上是一套让AI模型能够调用外部工具的协议标准你可以把它理解成给AI装了一双手。通过MCPAI不再只能被动地接收你粘贴的文本而是可以主动去调用工具、获取数据、执行操作。把vConsole和MCP结合起来就意味着AI可以直接读取H5页面上的日志和网络请求不需要你手动搬运。这篇文章要讲的就是怎么把这两件事串起来。核心思路是在H5页面里集成vConsole通过WebSocket把vConsole捕获的日志和请求数据实时推送到一个本地服务再把这个服务封装成MCP Server让AI通过MCP协议直接查询。整条链路打通之后你只需要对AI说帮我看看刚才那个页面为什么白屏AI就能自己去拉日志、分析请求、定位问题。适合阅读这篇文章的人有H5开发经验的前端工程师、正在探索AI辅助开发流程的技术人、对MCP协议感兴趣但还没找到落地场景的开发者。不需要你精通WebSocket或MCP协议细节我会把每一步的原理和操作都讲清楚。2. 拆解这条链路vConsole、WebSocket与MCP各自扮演什么角色2.1 vConsole的能力边界与数据出口vConsole是腾讯开源的一个轻量级移动端调试面板压缩后只有几十KB对页面性能影响极小。它的核心能力包括Console面板捕获所有console.log/warn/error/info输出包括对象展开Network面板记录所有XMLHttpRequest和fetch请求的URL、方法、状态码、请求头、响应头、响应体、耗时Element面板查看和修改DOM结构Storage面板查看Cookie、LocalStorage、SessionStorageSystem面板显示UA、屏幕尺寸、网络类型等关键点在于vConsole提供了一个插件机制和事件系统。你可以通过vConsole.VConsolePlugin自定义插件也可以直接hook它的内部方法来获取数据。但更优雅的方式是利用vConsole的vConsole.core对象它暴露了一些内部API。不过直接hook vConsole内部API有个问题版本升级可能导致API变化。所以更稳妥的方案是自己拦截console方法和XHR/fetch把数据同时输出给vConsole和WebSocket。这样即使vConsole升级你的数据通道也不受影响。具体做法是在页面加载vConsole之前先包装原生的console方法const originalLog console.log; const originalWarn console.warn; const originalError console.error; function sendToServer(type, args) { const payload { type: type, timestamp: Date.now(), args: Array.from(args).map(arg { try { return typeof arg object ? JSON.stringify(arg) : String(arg); } catch (e) { return [Unserializable]; } }) }; ws.send(JSON.stringify(payload)); } console.log function(...args) { originalLog.apply(console, args); sendToServer(log, args); }; // 同理处理warn和error对于网络请求拦截XMLHttpRequest的open和send方法以及fetch函数。这里要注意的是拦截必须在vConsole初始化之前完成否则vConsole会先捕获到原始方法。2.2 WebSocket作为实时数据通道的选型理由为什么用WebSocket而不是HTTP轮询原因很简单日志和请求是实时产生的轮询有延迟且浪费资源。WebSocket建立一次连接后可以双向通信服务端可以主动推送指令比如开始录制、清空日志客户端可以实时上报数据。WebSocket的心跳机制也是必须考虑的。移动端网络环境复杂连接可能因为切后台、网络切换等原因断开。标准做法是客户端每隔30秒发送一个ping帧服务端回复pong帧。如果连续3次没有收到pong就认为连接断开触发重连。let heartbeatTimer null; let pongReceived true; function startHeartbeat() { heartbeatTimer setInterval(() { if (!pongReceived) { console.warn(WebSocket heartbeat lost, reconnecting...); ws.close(); reconnect(); return; } pongReceived false; ws.send(JSON.stringify({ type: ping })); }, 30000); } ws.onmessage (event) { const data JSON.parse(event.data); if (data.type pong) { pongReceived true; } };重连策略采用指数退避第一次1秒后重连第二次2秒第三次4秒最多到30秒。这样既不会频繁重连造成服务端压力也不会在短暂断网后长时间失联。2.3 MCP Server的角色定位与协议要点MCP Server在这条链路里的角色是数据中转和查询接口。它做三件事接收WebSocket客户端上报的日志和请求数据存入内存或本地文件暴露MCP工具Tools让AI可以查询这些数据提供资源Resources让AI可以读取完整的日志文件MCP协议的核心概念包括ToolsAI可以调用的函数比如get_logs、get_requests、clear_logsResourcesAI可以读取的数据源比如logs://latest、requests://allPrompts预定义的提示模板这个场景下用得少一个典型的MCP工具定义长这样{ name: get_logs, description: 获取指定时间范围内的console日志, inputSchema: { type: object, properties: { since: { type: number, description: 起始时间戳毫秒不传则返回最近100条 }, level: { type: string, enum: [log, warn, error, info], description: 日志级别过滤 }, keyword: { type: string, description: 关键词搜索 } } } }AI通过MCP协议调用这个工具时会传入参数MCP Server执行查询并返回结果。整个过程对AI来说是透明的它只需要知道我有一个工具可以查日志。3. 从零搭建vConsole数据上报与MCP Server的完整实现3.1 页面端改造在不破坏原有调试体验的前提下增加数据出口页面端的改造原则是最小侵入。你不需要修改业务代码只需要在页面入口处引入一个独立的脚本。这个脚本做四件事初始化WebSocket连接拦截console方法拦截XHR和fetch初始化vConsole保持原有调试面板可用先看WebSocket连接的建立。这里有一个细节连接地址不能硬编码。开发环境、测试环境、生产环境的服务器地址不同而且同一页面可能在多个环境下运行。解决方案是通过URL参数传递function getWsUrl() { const params new URLSearchParams(location.search); const debugHost params.get(debug_host); if (debugHost) { return ws://${debugHost}/ws; } // 默认连接本地 return ws://127.0.0.1:8765/ws; }这样你在手机访问页面时只需要在URL后面加上?debug_host192.168.1.100:8765就能把数据推到你的开发机上。拦截XHR的代码需要小心处理因为很多框架比如axios内部就是用XHR实现的。拦截不当可能导致请求失败或数据丢失。正确做法是保存原始方法在原始方法执行前后插入逻辑const OriginalXHR window.XMLHttpRequest; function interceptXHR() { const originalOpen OriginalXHR.prototype.open; const originalSend OriginalXHR.prototype.send; OriginalXHR.prototype.open function(method, url, ...rest) { this._debugMethod method; this._debugUrl url; this._debugStartTime Date.now(); return originalOpen.apply(this, [method, url, ...rest]); }; OriginalXHR.prototype.send function(body) { this._debugBody body; this.addEventListener(loadend, () { const duration Date.now() - this._debugStartTime; sendToServer(request, [{ method: this._debugMethod, url: this._debugUrl, status: this.status, duration: duration, requestBody: this._debugBody, responseBody: this.responseText?.slice(0, 10000), requestHeaders: this._debugRequestHeaders, responseHeaders: this.getAllResponseHeaders() }]); }); return originalSend.apply(this, [body]); }; }注意responseText可能非常大必须截断否则WebSocket消息可能超过服务端的接收限制。一般截取前10000个字符就够了足够AI分析问题。对于fetch的拦截类似但要注意fetch返回的是Promise需要在then链中插入逻辑const originalFetch window.fetch; window.fetch function(...args) { const startTime Date.now(); const url typeof args[0] string ? args[0] : args[0].url; const method args[1]?.method || GET; return originalFetch.apply(this, args).then(response { const clone response.clone(); clone.text().then(text { sendToServer(request, [{ method: method, url: url, status: response.status, duration: Date.now() - startTime, responseBody: text.slice(0, 10000) }]); }); return response; }); };这里用response.clone()是因为response body只能被读取一次不clone的话业务代码就读不到了。3.2 服务端实现用Node.js同时承载WebSocket和MCP服务端选Node.js的原因很简单前后端同语言调试方便生态成熟。核心依赖只有两个ws用于WebSocketmodelcontextprotocol/sdk用于MCP。先初始化项目mkdir vconsole-mcp-server cd vconsole-mcp-server npm init -y npm install ws modelcontextprotocol/sdk服务端的主逻辑分两块WebSocket服务接收数据MCP服务暴露查询接口。两者共享同一个数据存储。数据存储用简单的内存数组加文件持久化const fs require(fs); const path require(path); const DATA_DIR path.join(__dirname, data); if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); class LogStore { constructor() { this.logs []; this.requests []; this.maxSize 5000; this.loadFromDisk(); } addLog(log) { this.logs.push(log); if (this.logs.length this.maxSize) { this.logs this.logs.slice(-this.maxSize); } this.persist(logs); } addRequest(req) { this.requests.push(req); if (this.requests.length this.maxSize) { this.requests this.requests.slice(-this.maxSize); } this.persist(requests); } queryLogs({ since, level, keyword, limit 100 }) { let result this.logs; if (since) result result.filter(l l.timestamp since); if (level) result result.filter(l l.type level); if (keyword) { result result.filter(l l.args.some(arg String(arg).includes(keyword)) ); } return result.slice(-limit); } persist(type) { const file path.join(DATA_DIR, ${type}.json); fs.writeFileSync(file, JSON.stringify(this[type], null, 2)); } loadFromDisk() { try { const logsFile path.join(DATA_DIR, logs.json); if (fs.existsSync(logsFile)) { this.logs JSON.parse(fs.readFileSync(logsFile, utf8)); } const reqFile path.join(DATA_DIR, requests.json); if (fs.existsSync(reqFile)) { this.requests JSON.parse(fs.readFileSync(reqFile, utf8)); } } catch (e) { console.error(Failed to load data from disk:, e.message); } } }WebSocket服务的实现const WebSocket require(ws); const store new LogStore(); const wss new WebSocket.Server({ port: 8765 }); wss.on(connection, (ws) { console.log(Client connected); ws.on(message, (message) { try { const data JSON.parse(message); if (data.type ping) { ws.send(JSON.stringify({ type: pong })); return; } if (data.type log) { store.addLog({ timestamp: data.timestamp, type: data.args[0]?.startsWith([error]) ? error : log, args: data.args }); } if (data.type request) { store.addRequest({ timestamp: Date.now(), ...data.args[0] }); } } catch (e) { console.error(Failed to process message:, e.message); } }); ws.on(close, () console.log(Client disconnected)); });MCP Server的实现需要用到SDK提供的Server类const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const server new Server( { name: vconsole-mcp, version: 1.0.0 }, { capabilities: { tools: {}, resources: {} } } ); server.setRequestHandler(tools/list, async () ({ tools: [ { name: get_logs, description: 获取H5页面的console日志, inputSchema: { type: object, properties: { since: { type: number, description: 起始时间戳 }, level: { type: string, enum: [log, warn, error, info] }, keyword: { type: string }, limit: { type: number, default: 100 } } } }, { name: get_requests, description: 获取H5页面的网络请求记录, inputSchema: { type: object, properties: { since: { type: number }, urlKeyword: { type: string }, status: { type: number }, limit: { type: number, default: 50 } } } }, { name: clear_data, description: 清空所有日志和请求记录, inputSchema: { type: object, properties: {} } } ] })); server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; if (name get_logs) { const logs store.queryLogs(args); return { content: [{ type: text, text: JSON.stringify(logs, null, 2) }] }; } if (name get_requests) { let requests store.requests; if (args.since) requests requests.filter(r r.timestamp args.since); if (args.urlKeyword) { requests requests.filter(r r.url.includes(args.urlKeyword)); } if (args.status) requests requests.filter(r r.status args.status); requests requests.slice(-(args.limit || 50)); return { content: [{ type: text, text: JSON.stringify(requests, null, 2) }] }; } if (name clear_data) { store.logs []; store.requests []; store.persist(logs); store.persist(requests); return { content: [{ type: text, text: Data cleared }] }; } }); const transport new StdioServerTransport(); server.connect(transport);3.3 把MCP Server接入AI客户端MCP Server写好了怎么让AI用上不同的AI客户端接入方式不同但核心都是配置一个MCP Server的启动命令。以Claude Desktop为例配置文件在~/Library/Application Support/Claude/claude_desktop_config.jsonmacOS或%APPDATA%\Claude\claude_desktop_config.jsonWindows{ mcpServers: { vconsole: { command: node, args: [/path/to/vconsole-mcp-server/index.js] } } }配置完成后重启AI客户端AI就能看到get_logs、get_requests这些工具了。如果你用的是支持MCP的其他客户端比如某些IDE插件、命令行工具配置方式类似都是指定命令和参数。关键点是MCP Server通过stdio通信所以AI客户端会以子进程的方式启动你的Node.js脚本。这里有一个容易踩的坑stdio模式下不能用console.log输出调试信息因为stdout被MCP协议占用了。调试信息必须用console.error输出到stderr否则会干扰MCP协议通信导致AI客户端解析失败。4. 实战验证用AI定位一个真实的H5白屏问题4.1 构造一个典型的白屏场景为了验证整条链路我构造了一个典型的H5白屏问题页面加载时调用一个用户信息接口接口返回的数据结构跟前端预期不一致导致渲染时报错页面白屏。问题代码大概长这样async function initPage() { const res await fetch(/api/user/info); const data await res.json(); // 预期data.user.name但实际接口返回的是data.data.userName document.getElementById(username).textContent data.user.name; }这段代码在data.user为undefined时会抛出TypeError导致后续渲染中断页面白屏。4.2 让AI自己拉日志和请求页面在手机上打开后vConsole面板会显示报错同时数据通过WebSocket推到了MCP Server。现在我在AI客户端里输入帮我看看最近这个H5页面为什么白屏了先拉一下最近的错误日志和网络请求。AI会调用get_logs工具传入level: error拿到类似这样的结果[ { timestamp: 1712345678901, type: error, args: [ TypeError: Cannot read properties of undefined (reading name), at initPage (app.js:15:45) ] } ]然后AI会调用get_requests传入urlKeyword: /api/user/info拿到[ { timestamp: 1712345678800, method: GET, url: /api/user/info, status: 200, duration: 120, responseBody: {\code\:0,\data\:{\userName\:\张三\,\userId\:123}} } ]有了这两条信息AI就能直接给出结论接口返回的字段是data.data.userName但代码里访问的是data.user.name字段路径不匹配导致TypeError。整个分析过程不需要我手动复制任何日志。4.3 对比传统流程的效率差异传统流程下这个问题的排查步骤是在手机上打开页面看到白屏打开vConsole切到Console面板看到报错截图或复制报错信息切到Network面板找到接口请求查看响应体复制响应体打开AI对话窗口粘贴报错和响应体描述问题AI分析后给出结论整个过程至少需要5-6次手动操作耗时2-3分钟。而通过MCP链路只需要对AI说一句话AI自己完成数据拉取和分析耗时10-20秒。更重要的是AI拿到的是完整数据不会因为手动复制遗漏关键信息。4.4 几个实测中发现的细节问题第一个问题日志顺序错乱。WebSocket消息是异步到达的如果页面瞬间产生大量日志服务端接收顺序可能跟实际产生顺序不一致。解决方案是在每条日志里带上客户端时间戳服务端按时间戳排序后再返回给AI。第二个问题循环日志导致数据爆炸。有些页面会在定时器里不断打日志几秒钟就能产生上千条。如果不加限制内存和磁盘都会被撑爆。解决方案是在页面端做采样同样的日志内容在1秒内只上报一次const recentLogs new Map(); function shouldReport(content) { const now Date.now(); const lastTime recentLogs.get(content); if (lastTime now - lastTime 1000) { return false; } recentLogs.set(content, now); // 清理过期记录 if (recentLogs.size 1000) { for (const [key, time] of recentLogs) { if (now - time 5000) recentLogs.delete(key); } } return true; }第三个问题敏感信息泄露。请求体里可能包含密码、token等敏感字段。如果这些数据被推送到服务端并持久化到磁盘存在安全风险。解决方案是在页面端做脱敏处理对包含password、token、authorization等关键词的字段进行掩码function sanitize(obj) { if (typeof obj ! object || obj null) return obj; const sensitiveKeys [password, token, authorization, secret, cookie]; const result Array.isArray(obj) ? [] : {}; for (const [key, value] of Object.entries(obj)) { if (sensitiveKeys.some(k key.toLowerCase().includes(k))) { result[key] ***REDACTED***; } else if (typeof value object) { result[key] sanitize(value); } else { result[key] value; } } return result; }5. 进阶玩法让MCP Server不只是查日志5.1 增加主动执行能力远程执行JS基础的查询功能只能让AI看但如果AI能做调试效率会更高。比如AI发现某个变量值不对可以直接在页面里执行一段JS来验证假设。实现方式是在MCP Server里增加一个eval_js工具通过WebSocket向页面发送指令页面执行后把结果返回// MCP Server端 { name: eval_js, description: 在H5页面中执行JavaScript代码并返回结果, inputSchema: { type: object, properties: { code: { type: string, description: 要执行的JS代码 } }, required: [code] } }页面端收到eval指令后执行ws.onmessage (event) { const data JSON.parse(event.data); if (data.type eval) { try { const result eval(data.code); ws.send(JSON.stringify({ type: eval_result, id: data.id, result: String(result) })); } catch (e) { ws.send(JSON.stringify({ type: eval_result, id: data.id, error: e.message })); } } };这个功能非常强大但也有风险。必须限制执行权限只允许在开发环境使用生产环境要禁用。可以在页面端加一个开关只有URL参数里带了debug1才启用eval功能。5.2 日志与请求的关联分析单独看日志和单独看请求有时候很难定位问题。比如一个接口报错但日志里没有明显异常。这时候需要把请求和它前后的日志关联起来。实现思路是在页面端记录每条日志和请求的序列号服务端按序列号排序。查询时如果AI指定了某个请求就返回该请求前后各10条日志function getContextAroundRequest(requestId, windowSize 10) { const allEvents [ ...store.logs.map(l ({ ...l, eventType: log })), ...store.requests.map(r ({ ...r, eventType: request })) ].sort((a, b) a.timestamp - b.timestamp); const index allEvents.findIndex(e e.id requestId); if (index -1) return []; return allEvents.slice( Math.max(0, index - windowSize), Math.min(allEvents.length, index windowSize 1) ); }这样AI在分析问题时能看到完整的上下文而不是孤立的一条日志或一个请求。5.3 多页面同时调试的支持实际项目中一个H5应用可能有多个页面或者同时打开多个WebView。如果所有页面的数据都混在一起AI很难区分。解决方案是在WebSocket连接建立时客户端发送一个页面标识const pageId ${location.pathname}_${Date.now()}; ws.onopen () { ws.send(JSON.stringify({ type: register, pageId, url: location.href })); };服务端按pageId分组存储数据。MCP工具增加pageId参数AI可以指定查询哪个页面的数据。如果不指定就返回所有页面的汇总但每条数据都带上pageId标识。5.4 与Playwright MCP的联动如果你同时在使用Playwright MCP做自动化测试可以把两者结合起来。Playwright负责操作页面点击、输入、导航vConsole MCP负责收集页面运行时的日志和请求。AI可以在执行完一个操作后自动拉取该操作产生的日志和请求验证是否符合预期。这种联动方式特别适合做回归测试AI自动跑一遍核心流程然后检查有没有报错日志、有没有失败的请求。比人工写断言灵活得多因为AI可以根据上下文判断这个报错是否正常。6. 踩过的坑与稳定性优化6.1 WebSocket连接在移动端的断连问题移动端浏览器在切后台、锁屏、网络切换时WebSocket连接大概率会断开。而且不同浏览器的行为不一致有些会立即触发onclose有些会静默断开onclose延迟很久才触发。解决方案是双保险客户端心跳 服务端超时检测。客户端每30秒发ping服务端如果60秒没收到任何消息就主动关闭连接。客户端检测到连接关闭后用指数退避重连。还有一个细节页面从后台恢复时要立即检查连接状态。可以通过visibilitychange事件监听document.addEventListener(visibilitychange, () { if (!document.hidden ws.readyState ! WebSocket.OPEN) { reconnect(); } });6.2 大数据量下的性能问题当页面产生大量日志时WebSocket消息可能非常大。比如一个接口返回了1MB的JSON直接通过WebSocket发送会导致消息分片增加延迟。解决方案是在页面端做压缩和截断。对于超过10KB的响应体只发送前10KB并标记truncated: true。如果AI需要完整数据可以通过另一个工具get_full_response按请求ID查询服务端从磁盘读取完整数据。另外日志上报要做批量处理。不要每产生一条日志就发一次WebSocket消息而是攒够10条或每隔500毫秒发一次let logBuffer []; let flushTimer null; function bufferLog(log) { logBuffer.push(log); if (logBuffer.length 10) { flushLogs(); } else if (!flushTimer) { flushTimer setTimeout(flushLogs, 500); } } function flushLogs() { if (logBuffer.length 0) return; ws.send(JSON.stringify({ type: logs_batch, logs: logBuffer })); logBuffer []; clearTimeout(flushTimer); flushTimer null; }6.3 MCP Server的启动与调试技巧MCP Server通过stdio通信调试起来不太直观。我的做法是同时启动一个HTTP调试端口在开发阶段可以通过浏览器或curl直接调用MCP工具验证逻辑是否正确const http require(http); if (process.env.DEBUG_MCP 1) { http.createServer((req, res) { if (req.url /logs) { res.end(JSON.stringify(store.queryLogs({ limit: 50 }))); } else if (req.url /requests) { res.end(JSON.stringify(store.requests.slice(-50))); } }).listen(8766); }这样在接入AI客户端之前可以先通过http://localhost:8766/logs确认数据是否正常上报。另一个坑是MCP Server的进程生命周期。AI客户端启动时会拉起MCP Server进程客户端关闭时会杀掉进程。如果你的WebSocket服务也在同一个进程里客户端关闭后WebSocket服务也会停止页面就断连了。解决方案是把WebSocket服务和MCP Server拆成两个独立进程MCP Server通过HTTP或Unix Socket向WebSocket服务查询数据。6.4 安全边界什么时候不该用这套方案这套方案的核心是把页面运行时数据暴露给本地服务。在开发环境没问题但在生产环境绝对不能用。几个必须遵守的原则生产环境构建时通过环境变量彻底移除调试脚本WebSocket服务只监听本地回环地址127.0.0.1不要监听0.0.0.0如果必须跨设备调试手机连电脑确保在同一个可信局域网内且调试完成后立即关闭服务敏感数据脱敏必须在页面端完成不要依赖服务端处理我在实际项目中会把调试脚本的引入逻辑放在构建工具的配置里通过process.env.NODE_ENV判断生产构建时直接不打包这个脚本。这样即使有人手动在URL里加参数也不会有任何调试代码执行。这套方案我用了大概三个月从最初的简单日志转发到后来加上请求拦截、eval执行、多页面支持基本上覆盖了H5调试的常见场景。最大的感受是AI辅助debug的前提是AI能拿到足够的数据而MCP正好解决了数据通道的问题。vConsole负责采集WebSocket负责传输MCP负责暴露给AI三者各司其职组合起来就是一个完整的AI辅助调试闭环。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表