├── LICENSE ├── README.md ├── WechatIMG15377.jpg ├── WechatIMG15378.jpg └── console-monitor.js /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auto-cursor-google 2 | 自动桥接cursor和谷歌浏览器的一个小插件,能在cursor agent模式下自动测试和修复bug,全自动获取console日志和network日志,结合cursor最强claude3.7模型自动动态分析错误自动修复 3 | 4 | package.json 替换 5 | 6 | "predev": "pkill -f 'Google Chrome' || true && open -n -a \"Google Chrome\" --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile", 7 | "dev": "vite & sleep 2 && node console-monitor.js \"$npm_config_level\"", 8 | cursor使用方式看图片,最好是你会配置cursor的规则文件 把 Chrome 远程调试工具使用指南扔到规则文件中 9 | 参考图片配置图片 10 | https://github.com/YongFaGitHub/auto-cursor-google/blob/main/WechatIMG15377.jpg 11 | https://github.com/YongFaGitHub/auto-cursor-google/blob/main/WechatIMG15378.jpg 12 | 13 | 把下面文档复制到新文件chrome-debugging-setup.txt 14 | # Chrome 远程调试工具使用指南 15 | 16 | ## 1. 快速开始 17 | 18 | ### 启动调试模式的 Chrome: 19 | ```bash 20 | # macOS 21 | pnpm dev # 或使用: open -n -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile 22 | 23 | # Windows 24 | pnpm dev # 或使用: "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%TEMP%\chrome-debug-profile 25 | 26 | # Linux 27 | pnpm dev 28 | ``` 29 | 30 | ## 2. 基本命令 31 | 32 | ### 监控页面: 33 | ```bash 34 | # 列出可用页面 35 | node console-monitor.js 36 | 37 | # 监控特定页面(支持三种模式:open/normal/strict) 38 | node console-monitor.js [mode] 39 | ``` 40 | 41 | ### 常用操作: 42 | ```bash 43 | # 截图 44 | node console-monitor.js screenshot [filename] 45 | 46 | # 元素操作 47 | node console-monitor.js element-click "#button" # 点击元素 48 | node console-monitor.js wait-element ".loading" # 等待元素 49 | node console-monitor.js get-text ".message" # 获取文本 50 | 51 | # 页面操作 52 | node console-monitor.js goto "https://example.com" # 页面跳转 53 | node console-monitor.js refresh # 页面刷新 54 | ``` 55 | 56 | ## 3. 监控内容 57 | 58 | ### Console 日志: 59 | - normal:显示 info、warn、error 级别日志 60 | - strict:只显示错误和异常日志 61 | 62 | ### 网络请求: 63 | - 自动显示 API 请求和错误响应 64 | - 自动过滤静态资源和开发相关请求 65 | - 自动脱敏敏感信息 66 | 67 | ## 4. 注意事项 68 | 69 | 1. 确保 Chrome 完全关闭后再启动调试模式 70 | 2. 默认使用 9222 端口,确保端口未被占用 71 | 3. 使用 `pnpm dev` 是最简单的启动方式 72 | 4. 建议使用 normal 模式进行日常调试 73 | 74 | ## 5. 高级功能 75 | 76 | ### Cookie 操作: 77 | ```bash 78 | # 列出所有 Cookie 79 | node console-monitor.js cookie list 80 | 81 | # 设置/获取/删除 Cookie 82 | node console-monitor.js cookie set "name" "value" 83 | node console-monitor.js cookie get "name" 84 | node console-monitor.js cookie delete "name" 85 | ``` 86 | 87 | ### 存储操作: 88 | ```bash 89 | # localStorage 操作 90 | node console-monitor.js storage list # 列出所有项 91 | node console-monitor.js storage get "key" 92 | node console-monitor.js storage set "key" "value" 93 | ``` 94 | 95 | ### 设备模拟: 96 | ```bash 97 | # 移动设备模拟(默认 iPhone X) 98 | node console-monitor.js mobile 99 | 100 | # 网络限速(单位:Kbps) 101 | node console-monitor.js network 1024 # 设置为 1Mbps 102 | ``` 103 | 104 | ### 自动化测试: 105 | ```bash 106 | # 表单操作 107 | node console-monitor.js type "text" # 输入文本 108 | node console-monitor.js click 100 200 # 坐标点击 109 | 110 | # 等待操作 111 | node console-monitor.js wait 2000 # 等待 2 秒 112 | node console-monitor.js wait-element "#id" 5000 # 等待元素最多 5 秒 113 | ``` 114 | 115 | ### 性能分析: 116 | ```bash 117 | # 开始性能分析 118 | node console-monitor.js profile start 119 | 120 | # 停止并保存分析结果 121 | node console-monitor.js profile stop profile.json 122 | ``` 123 | 124 | ### 安全功能: 125 | - 自动脱敏密码和 token 126 | - 支持自定义敏感字段 127 | - HTTPS 证书处理 128 | -------------------------------------------------------------------------------- /WechatIMG15377.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YongFaGitHub/auto-cursor-google/56a774989b494830b8f12d80ac44a7382747d287/WechatIMG15377.jpg -------------------------------------------------------------------------------- /WechatIMG15378.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YongFaGitHub/auto-cursor-google/56a774989b494830b8f12d80ac44a7382747d287/WechatIMG15378.jpg -------------------------------------------------------------------------------- /console-monitor.js: -------------------------------------------------------------------------------- 1 | import WebSocket from "ws" 2 | import https from "https" 3 | import http from "http" 4 | import fs from "fs" 5 | import path from "path" 6 | import net from "net" 7 | import os from "os" 8 | 9 | // 日志级别枚举 10 | const LogLevel = { 11 | OPEN: "open", // 不过滤任何信息 12 | NORMAL: "normal", // 过滤掉对日常调试开发没用的信息 13 | STRICT: "strict", // 只包含必要的信息 14 | } 15 | 16 | // 定义需要过滤的URL模式 17 | const filterPatterns = { 18 | normal: [ 19 | /\/__uno\.css/, 20 | /\/node_modules\//, 21 | /\.(css|scss|less)$/, 22 | /\.(png|jpg|jpeg|gif|svg|ico)$/, 23 | /hot-update/, 24 | /favicon/, 25 | /\/@vite/, 26 | /\/@fs/, 27 | /\[vite\]/, 28 | /\[hmr\]/, 29 | /\?v=\w+$/, 30 | /\?t=\d+$/, 31 | /\?vue&type=style/, 32 | /\.vite\/deps\//, 33 | ], 34 | strict: [ 35 | /\/__uno\.css/, 36 | /\/node_modules\//, 37 | /\.(css|scss|less|js|ts|vue|png|jpg|jpeg|gif|svg|ico)$/, 38 | /hot-update/, 39 | /favicon/, 40 | /assets\//, 41 | /styles\//, 42 | /fonts\//, 43 | /\/@vite/, 44 | /\/@fs/, 45 | /\[vite\]/, 46 | /\[hmr\]/, 47 | /\?v=\w+$/, 48 | /\?t=\d+$/, 49 | /\?vue&type=style/, 50 | /\.vite\/deps\//, 51 | ], 52 | } 53 | 54 | // 定义重要的日志关键词 55 | const importantKeywords = { 56 | normal: [ 57 | "error", 58 | "warn", 59 | "info", 60 | "WebSocket", 61 | "订阅", 62 | "成功", 63 | "失败", 64 | "API", 65 | "异常", 66 | "超时", 67 | "权限", 68 | ], 69 | strict: ["error", "fail", "exception", "timeout", "失败", "异常", "超时", "权限拒绝"], 70 | } 71 | 72 | // 获取当前日志级别 73 | function getLogLevel() { 74 | // 如果第四个参数是命令,而不是日志级别,则返回默认值 75 | const potentialLogLevel = process.argv[3]; 76 | if (potentialLogLevel && ['open', 'normal', 'strict'].includes(potentialLogLevel)) { 77 | return potentialLogLevel; 78 | } 79 | return LogLevel.NORMAL; 80 | } 81 | 82 | // 获取Chrome调试端口 83 | function getDebugPort() { 84 | // 首先检查是否有CHROME_DEBUG_PORT环境变量 85 | if (process.env.CHROME_DEBUG_PORT) { 86 | const port = parseInt(process.env.CHROME_DEBUG_PORT, 10); 87 | console.log(`使用CHROME_DEBUG_PORT环境变量指定的调试端口: ${port}`); 88 | return port; 89 | } 90 | 91 | // 获取随机端口 92 | function getRandomPort() { 93 | return 9000 + Math.floor(Math.random() * 1000); 94 | } 95 | 96 | // 同步检查端口是否可用 97 | function isPortAvailable(port) { 98 | try { 99 | const server = net.createServer(); 100 | server.listen(port, '127.0.0.1', 0); 101 | server.close(); 102 | return true; // 端口可用 103 | } catch (e) { 104 | console.log(`端口 ${port} 已被占用`); 105 | return false; // 端口被占用 106 | } 107 | } 108 | 109 | // 检查是否需要使用随机端口 110 | if (process.env.RANDOM_PORT === 'true') { 111 | // 尝试最多10次找到可用端口 112 | for (let i = 0; i < 10; i++) { 113 | const port = getRandomPort(); 114 | if (isPortAvailable(port)) { 115 | console.log(`找到可用的调试端口: ${port}`); 116 | return port; 117 | } 118 | } 119 | 120 | // 如果找不到可用端口,默认使用9222 121 | console.log(`未找到可用端口,使用默认端口: 9222`); 122 | return 9222; 123 | } else { 124 | // 默认使用9222端口 125 | console.log(`使用固定调试端口: 9222`); 126 | return 9222; 127 | } 128 | } 129 | 130 | // 添加请求时间记录和清理机制 131 | const requestTimes = new Map() 132 | const REQUEST_TIMEOUT = 60000 // 60秒超时 133 | let cleanupInterval 134 | 135 | // 清理超时的请求记录 136 | function cleanupRequestTimes() { 137 | const now = Date.now() 138 | for (const [requestId, startTime] of requestTimes.entries()) { 139 | if (now - startTime > REQUEST_TIMEOUT) { 140 | console.log(`[Warning] Request ${requestId} timed out`) 141 | requestTimes.delete(requestId) 142 | } 143 | } 144 | } 145 | 146 | // 添加退出状态标记 147 | let isShuttingDown = false 148 | 149 | // 优化优雅退出处理 150 | async function gracefulShutdown(ws, exitCode = 0) { 151 | // 防止重复执行清理 152 | if (isShuttingDown) return 153 | isShuttingDown = true 154 | 155 | console.log("\n正在关闭监控...") 156 | 157 | try { 158 | // 清理定时器 159 | if (cleanupInterval) { 160 | clearInterval(cleanupInterval) 161 | cleanupInterval = null 162 | } 163 | 164 | // 清理请求记录 165 | requestTimes.clear() 166 | 167 | // 关闭WebSocket连接 168 | if (ws) { 169 | if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { 170 | // 先发送关闭命令 171 | try { 172 | ws.send(JSON.stringify({ method: "Network.disable" })) 173 | ws.send(JSON.stringify({ method: "Console.disable" })) 174 | ws.send(JSON.stringify({ method: "Runtime.disable" })) 175 | } catch (e) { 176 | // 忽略发送错误 177 | } 178 | 179 | // 立即终止连接,不等待重连 180 | ws.terminate() 181 | } 182 | } 183 | } catch (err) { 184 | console.error("清理过程出错:", err) 185 | } finally { 186 | // 强制退出进程,不等待重连 187 | process.exit(exitCode) 188 | } 189 | } 190 | 191 | // 格式化请求参数 192 | function formatRequestParams(url, postData) { 193 | let params = {} 194 | 195 | // 处理URL参数 196 | try { 197 | const urlObj = new URL(url) 198 | urlObj.searchParams.forEach((value, key) => { 199 | params[key] = value 200 | }) 201 | } catch (e) {} 202 | 203 | // 处理POST数据 204 | if (postData) { 205 | try { 206 | const postParams = JSON.parse(postData) 207 | params = { ...params, ...postParams } 208 | } catch (e) { 209 | try { 210 | // 处理 x-www-form-urlencoded 格式 211 | const searchParams = new URLSearchParams(postData) 212 | searchParams.forEach((value, key) => { 213 | params[key] = value 214 | }) 215 | } catch (e) {} 216 | } 217 | } 218 | 219 | return Object.keys(params).length > 0 ? params : null 220 | } 221 | 222 | // 判断是否需要过滤的URL 223 | function shouldFilter(url, logLevel) { 224 | if (logLevel === LogLevel.OPEN) return false 225 | 226 | // 在normal和strict模式下过滤node_modules相关请求 227 | if (logLevel !== LogLevel.OPEN && url.includes("node_modules")) { 228 | return true 229 | } 230 | 231 | const patterns = filterPatterns[logLevel] || filterPatterns.normal 232 | return patterns.some((pattern) => pattern.test(url)) 233 | } 234 | 235 | // 判断是否是重要的日志消息 236 | function isImportantLog(message, level, logLevel) { 237 | if (logLevel === LogLevel.OPEN) return true 238 | if (logLevel === LogLevel.NORMAL) { 239 | // 在normal模式下,始终显示info、warn和error级别的日志 240 | if (level && ["info", "warn", "error"].includes(level.toLowerCase())) { 241 | return true 242 | } 243 | const keywords = importantKeywords.normal 244 | return keywords.some((keyword) => message.toLowerCase().includes(keyword.toLowerCase())) 245 | } 246 | if (logLevel === LogLevel.STRICT) { 247 | const keywords = importantKeywords.strict 248 | return keywords.some((keyword) => message.toLowerCase().includes(keyword.toLowerCase())) 249 | } 250 | return false 251 | } 252 | 253 | // 判断是否是重要的响应 254 | function isImportantResponse(body, logLevel) { 255 | if (logLevel === LogLevel.OPEN) return true 256 | if (logLevel === LogLevel.STRICT) { 257 | return body.code !== 200 || body.success === false || body.error 258 | } 259 | return body.code !== undefined || body.msg !== undefined || body.error 260 | } 261 | 262 | // 格式化响应内容 263 | function formatResponseBody(body, maxLength = 10000) { 264 | // 深拷贝对象以避免修改原始数据 265 | const clone = JSON.parse(JSON.stringify(body)) 266 | 267 | // 脱敏处理 268 | const sensitiveFields = ["password", "token", "secret", "key"] 269 | function maskSensitiveData(obj) { 270 | if (typeof obj !== "object" || obj === null) return 271 | Object.keys(obj).forEach((key) => { 272 | if (sensitiveFields.includes(key.toLowerCase())) { 273 | obj[key] = "******" 274 | } else if (typeof obj[key] === "object") { 275 | maskSensitiveData(obj[key]) 276 | } 277 | }) 278 | } 279 | maskSensitiveData(clone) 280 | 281 | // 转换为字符串并截断 282 | const str = JSON.stringify(clone, null, 2) 283 | if (str.length > maxLength) { 284 | return str.substring(0, maxLength) + "\n... (truncated)" 285 | } 286 | return str 287 | } 288 | 289 | async function getAvailablePages() { 290 | const debugPort = getDebugPort(); 291 | console.log(`正在获取Chrome调试页面列表,端口: ${debugPort}`); 292 | return new Promise((resolve, reject) => { 293 | http 294 | .get(`http://localhost:${debugPort}/json/list`, (res) => { 295 | let data = ""; 296 | res.on("data", (chunk) => (data += chunk)); 297 | res.on("end", () => { 298 | try { 299 | const pages = JSON.parse(data); 300 | console.log(`成功获取到 ${pages.length} 个页面`); 301 | pages.forEach((page, index) => { 302 | console.log(`页面 ${index+1}/${pages.length}:`); 303 | console.log(`- ID: ${page.id}`); 304 | console.log(`- 标题: ${page.title}`); 305 | console.log(`- URL: ${page.url.substring(0, 100)}${page.url.length > 100 ? '...' : ''}`); 306 | console.log(`- WebSocket URL: ${page.webSocketDebuggerUrl || '无'}`); 307 | }); 308 | resolve(pages); 309 | } catch (e) { 310 | console.error(`解析页面列表失败: ${e.message}`); 311 | reject(e); 312 | } 313 | }); 314 | }) 315 | .on("error", (err) => { 316 | console.error(`获取页面列表出错: ${err.message}`); 317 | console.error(`确保Chrome在端口 ${debugPort} 上启用了远程调试`); 318 | reject(err); 319 | }); 320 | }); 321 | } 322 | 323 | // 确保在发送命令前启用所需功能 324 | async function enableFeatures(ws) { 325 | return new Promise((resolve) => { 326 | let enabledCount = 0 327 | const requiredFeatures = ["Console", "Runtime", "Network", "Page"] 328 | 329 | requiredFeatures.forEach((feature, index) => { 330 | ws.send( 331 | JSON.stringify({ 332 | id: 100 + index, 333 | method: `${feature}.enable`, 334 | }) 335 | ) 336 | }) 337 | 338 | const checkEnabled = (data) => { 339 | try { 340 | const message = JSON.parse(data) 341 | if (message.id >= 100 && message.id < 100 + requiredFeatures.length) { 342 | enabledCount++ 343 | if (enabledCount === requiredFeatures.length) { 344 | ws.removeListener("message", checkEnabled) 345 | resolve() 346 | } 347 | } 348 | } catch (err) { 349 | console.error("启用功能时出错:", err) 350 | } 351 | } 352 | 353 | ws.on("message", checkEnabled) 354 | }) 355 | } 356 | 357 | // 通用消息处理函数 358 | function handleResponse(ws, data, expectedId) { 359 | try { 360 | const message = JSON.parse(data) 361 | console.log("收到消息:", JSON.stringify(message)) 362 | 363 | // 检查是否是预期的响应 364 | if (message.id === expectedId) { 365 | if (message.error) { 366 | console.error("操作失败:", message.error) 367 | return gracefulShutdown(ws, 1) 368 | } 369 | if (message.result !== undefined) { 370 | console.log("操作成功:", message.result) 371 | return gracefulShutdown(ws) 372 | } 373 | } 374 | 375 | // 检查是否是页面事件 376 | if (message.method === "Page.loadEventFired") { 377 | console.log("页面加载完成") 378 | return gracefulShutdown(ws) 379 | } 380 | } catch (err) { 381 | console.error("消息处理错误:", err) 382 | } 383 | } 384 | 385 | // 日志输出函数 386 | function logWithTimestamp(message) { 387 | const timestamp = new Date().toISOString(); 388 | console.log(`\n[${timestamp}] ${message}`); 389 | } 390 | 391 | // 主命令处理逻辑 392 | async function executeCommand(ws, command, args, targetPage) { 393 | try { 394 | await enableFeatures(ws).then(() => { 395 | console.log("控制台和网络监控功能已启用"); 396 | }); 397 | 398 | // 一次性操作命令列表 399 | const oneTimeCommands = [ 400 | "goto", 401 | "refresh", 402 | "element-click", 403 | "get-text", 404 | "cookie", 405 | "storage", 406 | "mobile", 407 | "network", 408 | "type", 409 | "click", 410 | "scroll-to", 411 | "screenshot" 412 | ] 413 | 414 | // 需要持续监听的命令列表 415 | const continuousCommands = ["wait", "wait-element"] 416 | 417 | // 如果是一次性操作命令,设置超时保护 418 | if (oneTimeCommands.includes(command)) { 419 | setTimeout(() => { 420 | console.log("操作超时,自动退出") 421 | gracefulShutdown(ws, 1) 422 | }, 30000) // 30秒超时 423 | } 424 | 425 | switch (command) { 426 | case "cookie": 427 | const cookieId = 8 428 | switch (args[0]) { 429 | case "list": 430 | ws.send( 431 | JSON.stringify({ 432 | id: cookieId, 433 | method: "Network.getAllCookies", 434 | }) 435 | ) 436 | break 437 | case "set": 438 | if (!args[1] || !args[2]) { 439 | console.error("请提供cookie名称和值") 440 | return gracefulShutdown(ws, 1) 441 | } 442 | ws.send( 443 | JSON.stringify({ 444 | id: cookieId, 445 | method: "Network.setCookie", 446 | params: { 447 | name: args[1], 448 | value: args[2], 449 | url: targetPage.url, 450 | }, 451 | }) 452 | ) 453 | break 454 | case "get": 455 | if (!args[1]) { 456 | console.error("请提供cookie名称") 457 | return gracefulShutdown(ws, 1) 458 | } 459 | ws.send( 460 | JSON.stringify({ 461 | id: cookieId, 462 | method: "Network.getCookies", 463 | params: { urls: [targetPage.url] }, 464 | }) 465 | ) 466 | break 467 | case "delete": 468 | if (!args[1]) { 469 | console.error("请提供要删除的cookie名称") 470 | return gracefulShutdown(ws, 1) 471 | } 472 | ws.send( 473 | JSON.stringify({ 474 | id: cookieId, 475 | method: "Network.deleteCookies", 476 | params: { name: args[1], url: targetPage.url }, 477 | }) 478 | ) 479 | break 480 | default: 481 | console.error("未知的cookie操作") 482 | return gracefulShutdown(ws, 1) 483 | } 484 | ws.on("message", (data) => handleResponse(ws, data, cookieId)) 485 | break 486 | 487 | case "storage": 488 | const storageId = 9 489 | switch (args[0]) { 490 | case "list": 491 | ws.send( 492 | JSON.stringify({ 493 | id: storageId, 494 | method: "Runtime.evaluate", 495 | params: { 496 | expression: "JSON.stringify(Object.entries(localStorage))", 497 | returnByValue: true, 498 | }, 499 | }) 500 | ) 501 | break 502 | case "set": 503 | if (!args[1] || !args[2]) { 504 | console.error("请提供storage的key和value") 505 | return gracefulShutdown(ws, 1) 506 | } 507 | ws.send( 508 | JSON.stringify({ 509 | id: storageId, 510 | method: "Runtime.evaluate", 511 | params: { 512 | expression: `localStorage.setItem('${args[1]}', '${args[2]}')`, 513 | }, 514 | }) 515 | ) 516 | break 517 | case "get": 518 | if (!args[1]) { 519 | console.error("请提供storage的key") 520 | return gracefulShutdown(ws, 1) 521 | } 522 | ws.send( 523 | JSON.stringify({ 524 | id: storageId, 525 | method: "Runtime.evaluate", 526 | params: { 527 | expression: `localStorage.getItem('${args[1]}')`, 528 | }, 529 | }) 530 | ) 531 | break 532 | default: 533 | console.error("未知的storage操作") 534 | return gracefulShutdown(ws, 1) 535 | } 536 | ws.on("message", (data) => handleResponse(ws, data, storageId)) 537 | break 538 | 539 | case "goto": 540 | if (!args[0]) { 541 | console.error("请提供要跳转的URL") 542 | return gracefulShutdown(ws, 1) 543 | } 544 | ws.send( 545 | JSON.stringify({ 546 | id: 4, 547 | method: "Page.navigate", 548 | params: { url: args[0] }, 549 | }) 550 | ) 551 | console.log(`正在跳转到: ${args[0]}`) 552 | ws.on("message", (data) => handleResponse(ws, data, 4)) 553 | break 554 | 555 | case "refresh": 556 | ws.send( 557 | JSON.stringify({ 558 | id: 5, 559 | method: "Page.reload", 560 | params: { ignoreCache: true }, 561 | }) 562 | ) 563 | console.log("正在刷新页面...") 564 | ws.on("message", (data) => handleResponse(ws, data, 5)) 565 | break 566 | 567 | case "element-click": 568 | if (!args[0]) { 569 | console.error("请提供要点击的元素选择器") 570 | return gracefulShutdown(ws, 1) 571 | } 572 | ws.send( 573 | JSON.stringify({ 574 | id: 6, 575 | method: "Runtime.evaluate", 576 | params: { 577 | expression: ` 578 | (function() { 579 | const element = document.querySelector('${args[0]}'); 580 | if (!element) { 581 | return { error: '元素不存在' }; 582 | } 583 | element.click(); 584 | return { success: true }; 585 | })() 586 | `, 587 | returnByValue: true, 588 | }, 589 | }) 590 | ) 591 | // 等待点击操作完成后退出 592 | ws.once("message", (data) => { 593 | const message = JSON.parse(data) 594 | if (message.id === 6) { 595 | if (message.result && message.result.result) { 596 | const result = message.result.result 597 | if (result.error) { 598 | console.error(result.error) 599 | gracefulShutdown(ws, 1) 600 | } else { 601 | console.log("点击操作成功") 602 | gracefulShutdown(ws) 603 | } 604 | } 605 | } 606 | }) 607 | break 608 | 609 | case "get-text": 610 | if (!args[0]) { 611 | console.error("请提供要获取文本的元素选择器") 612 | return gracefulShutdown(ws, 1) 613 | } 614 | ws.send( 615 | JSON.stringify({ 616 | id: 7, 617 | method: "Runtime.evaluate", 618 | params: { 619 | expression: `document.querySelector('${args[0]}').textContent`, 620 | }, 621 | }) 622 | ) 623 | // 等待获取文本完成后退出 624 | ws.once("message", (data) => { 625 | const message = JSON.parse(data) 626 | if (message.id === 7) { 627 | if (message.result && message.result.result) { 628 | console.log("文本内容:", message.result.result.value) 629 | } 630 | gracefulShutdown(ws) 631 | } 632 | }) 633 | break 634 | 635 | case "mobile": 636 | ws.send( 637 | JSON.stringify({ 638 | id: 10, 639 | method: "Emulation.setDeviceMetricsOverride", 640 | params: { 641 | width: 375, 642 | height: 812, 643 | deviceScaleFactor: 3, 644 | mobile: true, 645 | }, 646 | }) 647 | ) 648 | // 等待设备模拟设置完成后退出 649 | ws.once("message", (data) => { 650 | const message = JSON.parse(data) 651 | if (message.id === 10) { 652 | if (message.error) { 653 | console.error("设备模拟设置失败:", message.error) 654 | gracefulShutdown(ws, 1) 655 | } else { 656 | console.log("移动设备模拟已启用") 657 | gracefulShutdown(ws) 658 | } 659 | } 660 | }) 661 | break 662 | 663 | case "network": 664 | // 处理可能带前缀的参数 665 | const speedArg = args[0] ? args[0].replace(/^arg_/, '') : '1024'; 666 | const speed = parseInt(speedArg) || 1024 667 | ws.send( 668 | JSON.stringify({ 669 | id: 11, 670 | method: "Network.emulateNetworkConditions", 671 | params: { 672 | offline: false, 673 | latency: 100, 674 | downloadThroughput: (speed * 1024) / 8, 675 | uploadThroughput: (speed * 1024) / 8, 676 | }, 677 | }) 678 | ) 679 | // 等待网络限速设置完成后退出 680 | ws.once("message", (data) => { 681 | const message = JSON.parse(data) 682 | if (message.id === 11) { 683 | console.log(`网络限速已设置为 ${speed}Kbps`) 684 | gracefulShutdown(ws) 685 | } 686 | }) 687 | break 688 | 689 | case "wait": 690 | // 处理可能带前缀的参数 691 | const waitTimeArg = args[0] ? args[0].replace(/^arg_/, '') : '1000'; 692 | const waitTime = parseInt(waitTimeArg) || 1000 693 | setTimeout(() => { 694 | console.log(`等待 ${waitTime}ms 完成`) 695 | gracefulShutdown(ws) 696 | }, waitTime) 697 | break 698 | 699 | case "wait-element": 700 | if (!args[0]) { 701 | console.error("请提供要等待的元素选择器") 702 | return gracefulShutdown(ws, 1) 703 | } 704 | // 处理可能带前缀的参数 705 | const timeoutArg = args[1] ? args[1].replace(/^arg_/, '') : '5000'; 706 | const timeout = parseInt(timeoutArg) || 5000 707 | const checkInterval = setInterval(() => { 708 | ws.send( 709 | JSON.stringify({ 710 | id: 12, 711 | method: "Runtime.evaluate", 712 | params: { 713 | expression: `document.querySelector('${args[0]}') !== null`, 714 | }, 715 | }) 716 | ) 717 | }, 100) 718 | 719 | // 设置超时 720 | setTimeout(() => { 721 | clearInterval(checkInterval) 722 | console.error(`等待元素 ${args[0]} 超时`) 723 | gracefulShutdown(ws, 1) 724 | }, timeout) 725 | 726 | // 检查元素是否存在 727 | ws.on("message", (data) => { 728 | const message = JSON.parse(data) 729 | if (message.id === 12 && message.result && message.result.result) { 730 | if (message.result.result.value === true) { 731 | clearInterval(checkInterval) 732 | console.log(`元素 ${args[0]} 已出现`) 733 | gracefulShutdown(ws) 734 | } 735 | } 736 | }) 737 | break 738 | 739 | case "scroll-to": 740 | if (!args[0]) { 741 | console.error("请提供元素选择器或坐标") 742 | return gracefulShutdown(ws, 1) 743 | } 744 | 745 | // 检查是否是坐标格式 (x,y) 746 | const isCoordinate = args[0].match(/^\d+,\d+$/); 747 | 748 | let expression; 749 | if (isCoordinate) { 750 | const [x, y] = args[0].split(',').map(Number); 751 | expression = ` 752 | (function() { 753 | window.scrollTo({ 754 | left: ${x}, 755 | top: ${y}, 756 | behavior: 'smooth' 757 | }); 758 | return { success: true, message: '已滚动到坐标 (${x}, ${y})' }; 759 | })() 760 | `; 761 | } else { 762 | // 假设是元素选择器 763 | expression = ` 764 | (function() { 765 | const element = document.querySelector('${args[0]}'); 766 | if (!element) { 767 | return { error: '元素不存在' }; 768 | } 769 | 770 | // 获取元素位置 771 | const rect = element.getBoundingClientRect(); 772 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 773 | const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 774 | 775 | // 计算元素的绝对位置 776 | const top = rect.top + scrollTop; 777 | const left = rect.left + scrollLeft; 778 | 779 | // 滚动到元素位置 780 | element.scrollIntoView({ 781 | behavior: 'smooth', 782 | block: 'center', 783 | inline: 'center' 784 | }); 785 | 786 | return { success: true, message: '已滚动到指定元素' }; 787 | })() 788 | `; 789 | } 790 | 791 | ws.send( 792 | JSON.stringify({ 793 | id: 22, 794 | method: "Runtime.evaluate", 795 | params: { 796 | expression, 797 | returnByValue: true, 798 | }, 799 | }) 800 | ); 801 | 802 | // 等待滚动操作完成后处理结果 803 | ws.once("message", (data) => { 804 | const message = JSON.parse(data); 805 | if (message.id === 22) { 806 | if (message.result && message.result.result) { 807 | const result = message.result.result; 808 | if (result.error) { 809 | console.error(result.error); 810 | gracefulShutdown(ws, 1); 811 | } else { 812 | console.log(result.message || "滚动操作成功"); 813 | gracefulShutdown(ws); 814 | } 815 | } 816 | } 817 | }); 818 | break 819 | 820 | case "screenshot": 821 | // 设置默认文件名和保存路径 822 | const screenshotDir = args[0] || "./screenshots" 823 | let filename = args[1] || `screenshot_${new Date().toISOString().replace(/[:.]/g, "-")}` 824 | // 确保文件名有.png扩展名 825 | if (!filename.toLowerCase().endsWith('.png')) { 826 | filename += '.png' 827 | } 828 | const fullPath = path.join(screenshotDir, filename) 829 | 830 | // 确保目录存在 831 | try { 832 | if (!fs.existsSync(screenshotDir)) { 833 | fs.mkdirSync(screenshotDir, { recursive: true }) 834 | } 835 | } catch (err) { 836 | console.error(`创建截图目录失败: ${err.message}`) 837 | return gracefulShutdown(ws, 1) 838 | } 839 | 840 | console.log(`正在捕获截图,保存到: ${fullPath}`) 841 | 842 | // 捕获截图 843 | ws.send( 844 | JSON.stringify({ 845 | id: 23, 846 | method: "Page.captureScreenshot", 847 | params: { 848 | format: "png", 849 | quality: 100, 850 | fromSurface: true, 851 | captureBeyondViewport: true 852 | } 853 | }) 854 | ) 855 | 856 | // 处理截图结果 857 | let screenshotTimeout = setTimeout(() => { 858 | console.error("截图超时") 859 | gracefulShutdown(ws, 1) 860 | }, 10000) // 10秒超时 861 | 862 | // 定义处理函数 863 | const handleScreenshotResponse = function(data) { 864 | clearTimeout(screenshotTimeout) // 清除超时定时器 865 | try { 866 | const message = JSON.parse(data) 867 | if (message.id === 23) { 868 | if (message.result && message.result.data) { 869 | try { 870 | // 将 Base64 数据写入文件 871 | const imageBuffer = Buffer.from(message.result.data, "base64") 872 | fs.writeFileSync(fullPath, imageBuffer) 873 | console.log(`截图已保存到: ${fullPath}`) 874 | gracefulShutdown(ws) 875 | } catch (err) { 876 | console.error(`保存截图失败: ${err.message}`) 877 | gracefulShutdown(ws, 1) 878 | } 879 | } else if (message.error) { 880 | console.error(`截图失败: ${message.error.message || JSON.stringify(message.error)}`) 881 | gracefulShutdown(ws, 1) 882 | } else { 883 | console.error("截图响应无效: 缺少数据") 884 | gracefulShutdown(ws, 1) 885 | } 886 | } else { 887 | // 如果不是我们期望的响应ID,继续等待正确的响应 888 | ws.once("message", handleScreenshotResponse) 889 | } 890 | } catch (err) { 891 | console.error(`处理截图响应出错: ${err.message}`) 892 | gracefulShutdown(ws, 1) 893 | } 894 | }; 895 | 896 | ws.once("message", handleScreenshotResponse) 897 | break 898 | 899 | case "type": 900 | if (!args[0] || !args[1]) { 901 | console.error("请提供要输入文本的元素选择器和文本") 902 | return gracefulShutdown(ws, 1) 903 | } 904 | ws.send( 905 | JSON.stringify({ 906 | id: 24, 907 | method: "Runtime.evaluate", 908 | params: { 909 | expression: ` 910 | (function() { 911 | const element = document.querySelector('${args[0]}'); 912 | if (!element) { 913 | return { error: '元素不存在' }; 914 | } 915 | element.value = '${args[1]}'; 916 | return { success: true }; 917 | })() 918 | `, 919 | returnByValue: true, 920 | }, 921 | }) 922 | ) 923 | // 等待输入完成 924 | ws.once("message", (data) => { 925 | const message = JSON.parse(data) 926 | if (message.id === 24) { 927 | if (message.result && message.result.result) { 928 | console.log("文本输入成功") 929 | gracefulShutdown(ws) 930 | } else if (message.error) { 931 | console.error(message.error.message) 932 | gracefulShutdown(ws, 1) 933 | } 934 | } 935 | }) 936 | break 937 | 938 | case "click": 939 | if (!args[0] || !args[1]) { 940 | console.error("请提供要点击的坐标"); 941 | return gracefulShutdown(ws, 1); 942 | } 943 | 944 | // 处理可能带前缀的参数 945 | let xRaw = args[0]; 946 | let yRaw = args[1]; 947 | 948 | const xStr = xRaw.replace(/^arg_/, ''); 949 | const yStr = yRaw.replace(/^arg_/, ''); 950 | 951 | const x = parseInt(xStr, 10); 952 | const y = parseInt(yStr, 10); 953 | 954 | if (isNaN(x) || isNaN(y)) { 955 | console.error("坐标必须是有效的数字"); 956 | return gracefulShutdown(ws, 1); 957 | } 958 | 959 | console.log(`准备点击坐标: (${x}, ${y})`); 960 | 961 | try { 962 | ws.send( 963 | JSON.stringify({ 964 | id: 25, 965 | method: "Emulation.setTouchEmulationEnabled", 966 | params: { 967 | enabled: true, 968 | configuration: "mobile", 969 | }, 970 | }) 971 | ); 972 | 973 | // 创建一个Promise来等待触摸模拟命令的响应 974 | const touchEmulationPromise = new Promise((resolve, reject) => { 975 | const timeout = setTimeout(() => { 976 | reject(new Error("触摸模拟命令响应超时")); 977 | }, 5000); 978 | 979 | const handleResponse = (data) => { 980 | try { 981 | const message = JSON.parse(data); 982 | 983 | if (message.id === 25) { 984 | clearTimeout(timeout); 985 | ws.removeListener("message", handleResponse); 986 | 987 | if (message.error) { 988 | reject(new Error(`触摸模拟启用失败: ${message.error.message}`)); 989 | } else { 990 | resolve(); 991 | } 992 | } 993 | } catch (err) { 994 | console.error(`解析触摸模拟响应出错: ${err.message}`); 995 | } 996 | }; 997 | 998 | ws.on("message", handleResponse); 999 | }); 1000 | 1001 | // 等待触摸模拟命令完成,然后发送点击命令 1002 | touchEmulationPromise.then(() => { 1003 | ws.send( 1004 | JSON.stringify({ 1005 | id: 26, 1006 | method: "Input.dispatchMouseEvent", // 改用Mouse事件替代Emulation.tap 1007 | params: { 1008 | type: "mousePressed", 1009 | x: x, 1010 | y: y, 1011 | button: "left", 1012 | clickCount: 1 1013 | }, 1014 | }) 1015 | ); 1016 | 1017 | // 然后发送mouseReleased事件完成点击 1018 | setTimeout(() => { 1019 | ws.send( 1020 | JSON.stringify({ 1021 | id: 27, 1022 | method: "Input.dispatchMouseEvent", 1023 | params: { 1024 | type: "mouseReleased", 1025 | x: x, 1026 | y: y, 1027 | button: "left", 1028 | clickCount: 1 1029 | }, 1030 | }) 1031 | ); 1032 | }, 100); 1033 | 1034 | // 设置一个较短的超时,用于点击命令的响应 1035 | let clickTimeout = setTimeout(() => { 1036 | console.log("点击命令响应超时,但操作可能已成功"); 1037 | gracefulShutdown(ws, 0); // 超时但返回成功状态 1038 | }, 5000); 1039 | 1040 | // 监听所有消息 1041 | const allMessageHandler = (data) => { 1042 | try { 1043 | const message = JSON.parse(data); 1044 | 1045 | if (message.id === 26 || message.id === 27) { 1046 | if (message.id === 27) { // 只有当mouseReleased响应到达时才清理 1047 | clearTimeout(clickTimeout); 1048 | ws.removeListener("message", allMessageHandler); 1049 | console.log("点击操作成功完成"); 1050 | gracefulShutdown(ws, 0); 1051 | } 1052 | } 1053 | } catch (err) { 1054 | console.error(`解析消息出错: ${err.message}`); 1055 | } 1056 | }; 1057 | 1058 | ws.on("message", allMessageHandler); 1059 | }).catch(err => { 1060 | console.error(`点击操作失败: ${err.message}`); 1061 | gracefulShutdown(ws, 1); 1062 | }); 1063 | 1064 | } catch (err) { 1065 | console.error(`发送点击命令时出错: ${err.message}`); 1066 | if (err.stack) console.error(`错误堆栈: ${err.stack}`); 1067 | return gracefulShutdown(ws, 1); 1068 | } 1069 | break 1070 | 1071 | default: 1072 | console.log("未知命令。可用命令:") 1073 | console.log("一次性操作命令:") 1074 | console.log("- goto : 跳转到指定URL") 1075 | console.log("- refresh: 刷新当前页面") 1076 | console.log("- element-click : 点击元素") 1077 | console.log("- get-text : 获取元素文本") 1078 | console.log("- cookie list/set/get/delete: Cookie操作") 1079 | console.log("- storage list/set/get: Storage操作") 1080 | console.log("- mobile: 移动设备模拟") 1081 | console.log("- network : 网络限速(Kbps)") 1082 | console.log("- scroll-to : 滚动到指定元素或坐标") 1083 | console.log("- screenshot : 捕获当前页面截图") 1084 | console.log("- type : 在元素中输入文本") 1085 | console.log("- click : 在指定坐标点击") 1086 | console.log("\n需要持续监听的命令:") 1087 | console.log("- wait : 等待指定时间") 1088 | console.log("- wait-element [timeout]: 等待元素出现") 1089 | console.log("\n日志级别 mode 可选值: open, normal, strict") 1090 | gracefulShutdown(ws, 1) 1091 | } 1092 | } catch (err) { 1093 | console.error("命令执行错误:", err) 1094 | gracefulShutdown(ws, 1) 1095 | } 1096 | } 1097 | 1098 | // 辅助函数: 将WebSocket readyState转换为可读字符串 1099 | function getReadyStateString(readyState) { 1100 | switch(readyState) { 1101 | case WebSocket.CONNECTING: return 'CONNECTING'; 1102 | case WebSocket.OPEN: return 'OPEN'; 1103 | case WebSocket.CLOSING: return 'CLOSING'; 1104 | case WebSocket.CLOSED: return 'CLOSED'; 1105 | default: return `UNKNOWN(${readyState})`; 1106 | } 1107 | } 1108 | 1109 | async function main() { 1110 | // 先打印原始参数 1111 | console.log(`原始命令行参数:`, process.argv); 1112 | 1113 | // 检查是否是help命令 1114 | if (process.argv[2] === 'help') { 1115 | console.log("可用命令:") 1116 | console.log("一次性操作命令:") 1117 | console.log("- goto : 跳转到指定URL") 1118 | console.log("- refresh: 刷新当前页面") 1119 | console.log("- element-click : 点击元素") 1120 | console.log("- get-text : 获取元素文本") 1121 | console.log("- cookie list/set/get/delete: Cookie操作") 1122 | console.log("- storage list/set/get: Storage操作") 1123 | console.log("- mobile: 移动设备模拟") 1124 | console.log("- network : 网络限速(Kbps)") 1125 | console.log("- scroll-to : 滚动到指定元素或坐标") 1126 | console.log("- screenshot : 捕获当前页面截图") 1127 | console.log("- type : 在元素中输入文本") 1128 | console.log("- click : 在指定坐标点击") 1129 | console.log("\n需要持续监听的命令:") 1130 | console.log("- wait : 等待指定时间") 1131 | console.log("- wait-element [timeout]: 等待元素出现") 1132 | console.log("\n实用功能命令:") 1133 | console.log("- help: 显示此帮助信息") 1134 | console.log("- list-pages: 列出当前可用的所有Chrome调试页面") 1135 | console.log("\n日志级别 mode 可选值: open, normal, strict") 1136 | console.log("\n环境变量配置:") 1137 | console.log("- CHROME_DEBUG_PORT=: 指定Chrome调试端口(默认9222)") 1138 | console.log("- RANDOM_PORT=true: 启用随机端口选择以避免端口冲突") 1139 | console.log("- PAGE_ID=: 预设页面ID,避免命令行传参") 1140 | console.log("\n多项目并行使用:") 1141 | console.log("1. 为不同项目指定不同端口: CHROME_DEBUG_PORT=9223 npm run dev") 1142 | console.log("2. 使用随机端口避免冲突: RANDOM_PORT=true npm run dev") 1143 | console.log("3. 通过页面ID直接操作: PAGE_ID= node console-monitor.js ") 1144 | console.log("\n使用方法:") 1145 | console.log("node console-monitor.js <页面ID> <命令> [参数...]") 1146 | console.log("示例: node console-monitor.js AB12CD34 goto https://example.com") 1147 | console.log("或使用环境变量: PAGE_ID=AB12CD34 node console-monitor.js goto https://example.com") 1148 | process.exit(0); 1149 | } 1150 | 1151 | // 检查是否是list-pages命令 1152 | if (process.argv[2] === 'list-pages') { 1153 | try { 1154 | const debugPort = getDebugPort(); 1155 | console.log(`使用调试端口: ${debugPort}`); 1156 | 1157 | const pages = await getAvailablePages(debugPort); 1158 | console.log(`找到 ${pages.length} 个可用页面:`); 1159 | 1160 | pages.forEach((page, index) => { 1161 | console.log(`\n页面 ${index + 1}:`); 1162 | console.log(`- ID: ${page.id}`); 1163 | console.log(`- 标题: ${page.title}`); 1164 | console.log(`- URL: ${page.url}`); 1165 | console.log(`- 类型: ${page.type}`); 1166 | }); 1167 | 1168 | process.exit(0); 1169 | } catch (err) { 1170 | console.error(`获取页面列表失败: ${err.message}`); 1171 | process.exit(1); 1172 | } 1173 | } 1174 | 1175 | let pageId = process.env.PAGE_ID || process.argv[2] 1176 | if (!pageId) { 1177 | console.error("请提供页面ID作为第一个参数,或设置PAGE_ID环境变量") 1178 | console.log("获取可用页面ID,请运行: node console-monitor.js list-pages") 1179 | process.exit(1) 1180 | } 1181 | 1182 | // 警告: 如果页面ID以数字开头,提示可能会出现连接问题 1183 | if (/^\d/.test(pageId)) { 1184 | console.warn("警告: 页面ID以数字开头,可能会导致连接问题"); 1185 | console.warn("建议使用以字母开头的ID或使用PAGE_ID环境变量"); 1186 | } 1187 | 1188 | const potentialLogLevel = process.argv[3]; 1189 | let logLevel, command, commandArgs; 1190 | 1191 | if (potentialLogLevel && ['open', 'normal', 'strict'].includes(potentialLogLevel)) { 1192 | // 如果是日志级别,那么没有命令 1193 | logLevel = potentialLogLevel; 1194 | command = null; 1195 | commandArgs = []; 1196 | } else { 1197 | // 否则第三个参数是命令 1198 | logLevel = LogLevel.NORMAL; 1199 | command = process.argv[3]; 1200 | 1201 | // 明确地从第4个参数开始收集所有命令参数,避免与其他配置混淆 1202 | commandArgs = []; 1203 | if (process.argv.length > 4) { 1204 | // 对于纯数字参数,添加前缀,避免被错误解析为端口 1205 | commandArgs = process.argv.slice(4).map(arg => { 1206 | // 如果参数是纯数字,添加前缀"arg_" 1207 | if (/^\d+$/.test(arg)) { 1208 | const prefixedArg = `arg_${arg}`; 1209 | return prefixedArg; 1210 | } 1211 | return arg; 1212 | }); 1213 | } 1214 | } 1215 | 1216 | console.log(`当前日志级别: ${logLevel}`); 1217 | console.log(`页面ID: "${pageId}"`); 1218 | console.log(`命令: "${command || "无"}"`); 1219 | console.log(`命令参数: [${commandArgs.length ? commandArgs.join(', ') : "无"}]`); 1220 | 1221 | let ws; 1222 | let reconnectAttempts = 0; 1223 | const MAX_RECONNECT_ATTEMPTS = 3; 1224 | 1225 | // 启动定时清理 1226 | cleanupInterval = setInterval(cleanupRequestTimes, 10000); 1227 | 1228 | // 处理 Ctrl+C 1229 | process.on("SIGINT", () => { 1230 | console.log("\n收到退出信号..."); 1231 | isShuttingDown = true; 1232 | if (ws) { 1233 | ws.terminate(); 1234 | } 1235 | process.exit(0); 1236 | }); 1237 | 1238 | // 处理未捕获的异常 1239 | process.on("uncaughtException", (err) => { 1240 | console.error("未捕获的异常:", err); 1241 | isShuttingDown = true; 1242 | if (ws) { 1243 | ws.terminate(); 1244 | } 1245 | process.exit(1); 1246 | }); 1247 | 1248 | // 处理未处理的Promise拒绝 1249 | process.on("unhandledRejection", (reason, promise) => { 1250 | console.error("未处理的Promise拒绝:", reason); 1251 | isShuttingDown = true; 1252 | if (ws) { 1253 | ws.terminate(); 1254 | } 1255 | process.exit(1); 1256 | }); 1257 | 1258 | async function connectWebSocket() { 1259 | try { 1260 | console.log(`开始连接到页面ID: "${pageId}"`); 1261 | 1262 | const pages = await getAvailablePages(); 1263 | console.log(`正在查找目标页面...`); 1264 | 1265 | let targetPage = pages.find((page) => page.id === pageId); 1266 | if (!targetPage) { 1267 | console.error(`未找到ID为 "${pageId}" 的页面`); 1268 | console.log(`可用页面ID列表:`); 1269 | pages.forEach(page => console.log(`- ${page.id}: ${page.title}`)); 1270 | process.exit(1); 1271 | } 1272 | 1273 | console.log(`已选择页面: ${targetPage.title} (${targetPage.url})`); 1274 | 1275 | // 确保WebSocket URL正确无误,不受命令参数影响 1276 | const wsUrl = targetPage.webSocketDebuggerUrl; 1277 | if (!wsUrl) { 1278 | console.error(`页面 ${pageId} 没有有效的WebSocket URL`); 1279 | process.exit(1); 1280 | } 1281 | 1282 | console.log(`准备连接到WebSocket URL: ${wsUrl}`); 1283 | 1284 | try { 1285 | ws = new WebSocket(wsUrl, { 1286 | handshakeTimeout: 5000, // 5秒超时 1287 | maxPayload: 50 * 1024 * 1024 // 50MB最大负载 1288 | }); 1289 | } catch (err) { 1290 | console.error(`WebSocket连接初始化失败: ${err.message}`); 1291 | process.exit(1); 1292 | } 1293 | 1294 | ws.on("open", function open() { 1295 | console.log(`已成功连接到页面 ${targetPage.id}`); 1296 | reconnectAttempts = 0; 1297 | // Enable console API 1298 | ws.send( 1299 | JSON.stringify({ 1300 | id: 1, 1301 | method: "Console.enable", 1302 | }) 1303 | ); 1304 | 1305 | // Enable runtime 1306 | ws.send( 1307 | JSON.stringify({ 1308 | id: 2, 1309 | method: "Runtime.enable", 1310 | }) 1311 | ); 1312 | 1313 | // Enable network monitoring 1314 | ws.send( 1315 | JSON.stringify({ 1316 | id: 3, 1317 | method: "Network.enable", 1318 | }) 1319 | ); 1320 | // // 如果有命令,执行命令 1321 | // if (command) { 1322 | // console.log(`准备执行命令: "${command}", 参数: [${commandArgs.join(', ')}]`); 1323 | // executeCommand(ws, command, commandArgs, targetPage).catch((err) => { 1324 | // console.error(`命令执行错误: ${err.message}`); 1325 | // gracefulShutdown(ws, 1); 1326 | // }); 1327 | // } 1328 | }); 1329 | 1330 | ws.on("close", async function handleClose() { 1331 | console.log("WebSocket连接已关闭") 1332 | 1333 | // 如果正在关闭,直接退出 1334 | if (isShuttingDown) { 1335 | process.exit(0) 1336 | } 1337 | 1338 | if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { 1339 | reconnectAttempts++ 1340 | console.log(`尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`) 1341 | await new Promise((resolve) => setTimeout(resolve, 2000)) // 等待2秒后重连 1342 | connectWebSocket() 1343 | } else { 1344 | console.error("已达到最大重连尝试次数,开始关闭监控") 1345 | gracefulShutdown(ws) 1346 | } 1347 | }) 1348 | 1349 | ws.on("error", function error(err) { 1350 | console.error("WebSocket错误:", err.message) 1351 | if (ws.readyState === WebSocket.OPEN) { 1352 | ws.close() 1353 | } 1354 | if (isShuttingDown) { 1355 | process.exit(1) 1356 | } 1357 | }) 1358 | 1359 | ws.on("message", function incoming(data) { 1360 | const message = JSON.parse(data) 1361 | 1362 | // Handle console messages 1363 | if (message.method === "Console.messageAdded") { 1364 | const logMessage = message.params.message 1365 | if (isImportantLog(logMessage.text, logMessage.level, logLevel)) { 1366 | console.log(`\n[${logMessage.level}] ${logMessage.text}`) 1367 | } 1368 | } 1369 | 1370 | // Handle console API calls 1371 | if (message.method === "Runtime.consoleAPICalled") { 1372 | const logMessage = message.params 1373 | const text = logMessage.args.map((arg) => arg.value).join(" ") 1374 | if (isImportantLog(text, logMessage.type, logLevel)) { 1375 | console.log(`\n[${logMessage.type}] ${text}`) 1376 | } 1377 | } 1378 | 1379 | // Handle network requests 1380 | if (message.method === "Network.requestWillBeSent") { 1381 | const request = message.params 1382 | if (!shouldFilter(request.request.url, logLevel)) { 1383 | requestTimes.set(request.requestId, Date.now()) 1384 | // 在normal和strict模式下不显示请求信息,只显示响应 1385 | if (logLevel === LogLevel.OPEN) { 1386 | const params = formatRequestParams(request.request.url, request.request.postData) 1387 | logWithTimestamp(`\n[Network Request] ${request.request.method} ${request.request.url} - Params: ${JSON.stringify(params)}`) 1388 | } 1389 | } 1390 | } 1391 | 1392 | // Handle network responses 1393 | if (message.method === "Network.responseReceived") { 1394 | const response = message.params 1395 | if (!shouldFilter(response.response.url, logLevel)) { 1396 | const startTime = requestTimes.get(response.requestId) 1397 | const duration = startTime ? Date.now() - startTime : null 1398 | const durationStr = duration ? ` (${duration}ms)` : "" 1399 | 1400 | // 只有在状态码不是200或者是API请求时才显示响应信息 1401 | const isApiRequest = 1402 | response.response.url.includes("/api/") || response.response.url.includes("/dev-api/") 1403 | const isNonSuccessResponse = response.response.status !== 200 1404 | 1405 | if (isApiRequest || isNonSuccessResponse) { 1406 | logWithTimestamp(`\n[Network Response] ${response.response.status} ${response.response.statusText}${durationStr} - URL: ${response.response.url}`) 1407 | 1408 | if (response.response.mimeType === "application/json") { 1409 | ws.send( 1410 | JSON.stringify({ 1411 | id: 4, 1412 | method: "Network.getResponseBody", 1413 | params: { requestId: response.requestId }, 1414 | }) 1415 | ) 1416 | } 1417 | } 1418 | requestTimes.delete(response.requestId) 1419 | } 1420 | } 1421 | 1422 | // Handle response body 1423 | if (message.id === 4 && message.result) { 1424 | try { 1425 | const body = JSON.parse(message.result.body) 1426 | if (isImportantResponse(body, logLevel)) { 1427 | logWithTimestamp("\n[Response Body] " + formatResponseBody(body)) 1428 | } 1429 | } catch (e) { 1430 | logWithTimestamp("\n[Response Body] " + message.result.body) 1431 | } 1432 | } 1433 | 1434 | // Handle network errors 1435 | if (message.method === "Network.loadingFailed") { 1436 | const failure = message.params 1437 | logWithTimestamp(`\n[Network Error] Failed to load ${failure.requestId}: ${failure.errorText}`) 1438 | } 1439 | }) 1440 | } catch (err) { 1441 | console.error("Connection error:", err) 1442 | if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { 1443 | reconnectAttempts++ 1444 | console.log(`Attempting to reconnect (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`) 1445 | await new Promise((resolve) => setTimeout(resolve, 2000)) 1446 | connectWebSocket() 1447 | } else { 1448 | console.error("Max reconnection attempts reached") 1449 | gracefulShutdown(ws) 1450 | } 1451 | } 1452 | } 1453 | 1454 | // 启动连接 1455 | await connectWebSocket() 1456 | 1457 | console.log("Monitoring console logs and network activity... Press Ctrl+C to stop.") 1458 | } 1459 | 1460 | main().catch((err) => { 1461 | console.error("Error:", err) 1462 | process.exit(1) 1463 | }) 1464 | --------------------------------------------------------------------------------