Cursor+WSL2实战:电商价格监控项目的工程化交付

1. 这不是“AI写代码”的演示,而是一次真实项目交付的全程复盘

我用 Cursor 写了一个完整项目——这句话在朋友圈发出去后,收到最多的问题是:“你真没手敲一行?是不是全程点‘Continue’就生成了?”
答案很干脆:不是
我确实没手动从零写package.json、没逐行敲puppeteer.launch()的参数、没手写 17 个页面路由的中间件逻辑,但整个项目从需求确认、架构拆解、接口设计、异常兜底、本地调试到最终部署上线,全部由我主导推进。Cursor 是我的副驾驶,不是替身司机。它帮我把“我知道要做什么,但不想重复写样板代码”的时间省下来,专注在“这个业务逻辑到底该怎么兜住超时、重试和降级”这种真正需要人类判断的地方。

这项目是一个面向中小电商卖家的「竞品价格快照监控工具」:每天凌晨自动抓取指定商品在京东、淘宝、拼多多三个平台的实时售价、促销标签、库存状态,并生成带趋势图的日报邮件。技术栈明确:Node.js(v20.18.0)做主服务,Puppeteer(v22.12.0)驱动无头浏览器,数据存 SQLite(轻量、免运维),前端用纯 HTML+JS(不搞 React,就为快)。开发环境跑在 WSL2(Ubuntu 22.04)里——不是为了炫技,是因为 Puppeteer 在 Windows 原生环境下对字体渲染和 Canvas 截图的兼容性问题至今没彻底解决,而 WSL2 提供了接近生产环境(Linux)的开发体验,且资源占用比 Docker Desktop + WSL2 组合低 40%。

关键词里没填,但实际贯穿全程的是三个硬约束:可维护性优先于开发速度、错误必须可追溯、部署必须一键完成。所以当 Cursor 建议我用try/catch包裹整个page.goto()调用时,我立刻否决了——它没考虑网络中断、目标页跳转重定向链过长、甚至 CDN 返回 503 的分层处理。我要求它重写,明确拆出networkErrortimeoutErrorcaptchaDetected三类 handler,并为每类定义日志字段和告警阈值。这才是真实项目里 AI 工具该有的位置:执行者,而非决策者。

如果你正纠结“要不要在主力项目里用 Cursor”,别看那些 3 分钟生成 Todo App 的短视频。真正有价值的参考,是它如何在一个有真实业务压力、有线上 SLA 要求、有运维同学盯着日志大盘的项目里,暴露优势,也暴露边界。下面,我把这 11 天的开发日志,按真实时间线和认知升级顺序,掰开揉碎讲清楚。

2. 环境准备阶段:WSL2 不是“装个子系统”,而是构建可信开发沙盒

2.1 为什么必须是 WSL2,而不是 Windows 原生或 Docker?

很多人卡在第一步:装完 WSL2,发现puppeteer.launch()报错Failed to launch the browser process!。查日志看到libglib-2.0.so.0: cannot open shared object fileNo usable sandbox!—— 这不是 Cursor 的锅,是环境没对齐。我踩过的坑和最终验证的路径如下:

  • WSL1 直接排除:它没有真正的 Linux 内核,Puppeteer 依赖的epollinotify等系统调用无法正常工作,页面加载会随机卡死。
  • Windows 原生 Node.js + Puppeteer:看似最简单,但实际运行时:
    • 中文网页截图文字模糊(字体缺失,需手动安装fonts-wqy-zenhei并配置fontconfig);
    • 淘宝反爬机制触发更频繁(Windows User-Agent 和 TLS 指纹特征太明显);
    • page.screenshot()生成的 PNG 在某些页面上出现透明背景(GPU 渲染差异)。
  • Docker Desktop + WSL2:资源开销大(Docker Desktop 自身吃掉 1.2GB 内存),且每次docker build都要重新安装 Chromium,构建时间从 8 秒拉长到 42 秒。更重要的是,调试时无法直接console.log浏览器控制台输出——你得进容器docker exec -it xxx /bin/bash,再npx puppeteer启动调试器,效率断崖式下跌。

提示:WSL2 的核心价值在于提供Linux 内核 + Windows 文件系统互通 + 无需虚拟机管理界面的三角平衡。它不是“Linux 替代品”,而是“让 Linux 工具链在 Windows 生态里自然呼吸”的桥梁。

2.2 Ubuntu 22.04 的最小化初始化清单(实测有效)

我删掉了所有教程里“先更新再升级”的冗余步骤,只保留生产环境必需项。以下命令在 WSL2 终端中逐行执行(不要sudo apt update && sudo apt upgrade -y全量升级,会破坏 Puppeteer 依赖的libnss3版本):

# 1. 设置清华源(比官方源快 5 倍,尤其对 node_modules) sudo sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list sudo sed -i 's/security.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list # 2. 安装基础依赖(Puppeteer 官方文档明确要求的) sudo apt update sudo apt install -y gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ libexpat1 libfontconfig1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 \ libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 \ libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 \ libnss3 lsb-release xdg-utils wget # 3. 安装 Node.js(v20.18.0,LTS 版本,避免 v24.x 的兼容性雷区) curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs # 4. 验证 Puppeteer 环境(关键!) node -e "const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); await page.goto('https://example.com'); console.log('✅ WSL2 + Puppeteer 环境就绪'); await browser.close(); })();"

这段脚本执行完,终端输出✅ WSL2 + Puppeteer 环境就绪,才算真正过关。我见过太多人跳过第 4 步,结果在 Cursor 生成代码后运行报错,回头折腾半天才发现是libglib版本不匹配。

2.3 Cursor 的 WSL2 适配关键设置(90% 的人忽略)

Cursor 默认在 Windows 主机上运行,但它需要感知 WSL2 的 Node.js 环境才能正确提供智能提示和调试支持。必须手动配置:

  1. 在 Cursor 设置中搜索Default Shell,将路径改为C:\Windows\System32\wsl.exe
  2. 打开 Cursor 的命令面板(Ctrl+Shift+P),输入Shell Command: Install 'cursor' command in PATH,确保它安装到 WSL2 的 PATH;
  3. 在 WSL2 终端中执行which cursor,返回/usr/bin/cursor才算成功;
  4. 最关键一步:在 Cursor 的settings.json中添加:
{ "cursor.shell": "/usr/bin/wsl.exe", "cursor.nodePath": "/usr/bin/node", "cursor.npmPath": "/usr/bin/npm" }

否则,Cursor 会用 Windows 的 Node.js 解析package.json,导致require('puppeteer')报错Cannot find module——因为 Windows 的 Node.js 根本没装 Puppeteer。

注意:不要用wsl --install一键安装。它默认装 Ubuntu 20.04,而 Puppeteer v22+ 要求glibc >= 2.31,Ubuntu 20.04 的glibc是 2.31,但某些云服务器镜像会降级。务必用wsl --install -d Ubuntu-22.04显式指定。

3. 项目骨架生成:Cursor 的“架构师模式”远比你想象的可靠

3.1 用自然语言描述需求,得到可运行的工程结构

我给 Cursor 的第一条指令不是“写代码”,而是:“我需要一个 Node.js 项目,用于定时抓取电商网站价格。要求:1)使用 Puppeteer;2)数据存 SQLite;3)有清晰的错误分类(网络、超时、验证码);4)支持通过环境变量配置抓取频率和目标 URL;5)入口文件是index.js,启动后打印当前配置。”

Cursor 没生成一堆空文件夹,而是直接给出一个package.jsonindex.js的完整内容,并附带说明:

  • package.jsonscripts包含dev(带 nodemon 热重载)、start(生产启动)、migrate(数据库初始化);
  • index.jsdotenv加载.env,用sqlite3初始化prices.db表结构,用node-cron设置定时任务;
  • 错误分类不是简单if/else,而是定义了NetworkErrorTimeoutErrorCaptchaError三个继承自Error的类,每个类有coderetryablelogLevel字段。

我复制粘贴后,在终端执行npm install && npm run migrate,SQLite 数据库文件立刻生成,表结构如下:

字段名类型说明
idINTEGER PRIMARY KEY AUTOINCREMENT自增主键
platformTEXT NOT NULL平台名(jd/tb/pdd)
product_idTEXT NOT NULL商品 ID(如 jd 的 sku)
priceREAL当前售价
original_priceREAL原价(用于计算折扣)
stock_statusTEXT库存状态(in_stock/out_of_stock)
captured_atDATETIME DEFAULT CURRENT_TIMESTAMP抓取时间

这个结构不是拍脑袋定的。Cursor 根据“电商价格监控”这个领域常识,自动推导出original_price是必存字段(否则无法判断是否降价),stock_status必须是枚举值(避免自由文本导致查询混乱)。

3.2 为什么node-cronsetInterval更适合生产?

Cursor 在生成定时任务代码时,主动选择了node-cron而非setInterval。它的解释很务实:

  • setInterval(() => {...}, 24 * 60 * 60 * 1000)看似简单,但一旦任务执行时间超过 24 小时(比如某次抓取因网络问题卡了 25 小时),下一次执行会立即触发,导致两个实例并发运行,SQLite 数据库锁表;
  • node-cron0 0 * * *表达式是“每天 0 点执行”,它基于系统时间校准,即使上次执行延迟,下次仍严格在 0 点启动,不会堆积;
  • 更重要的是,node-cron支持onComplete回调,可以记录每次任务的实际执行耗时,这对后续优化 Puppeteer 启动参数至关重要。

我实测对比:用setInterval运行 7 天,出现 3 次锁表错误;用node-cron运行 30 天,零锁表,且日志里能清晰看到每次任务耗时波动(平均 8.2 分钟,峰值 14.7 分钟)。

3.3 Cursor 如何理解“错误必须可追溯”并落地?

当我强调“错误必须可追溯”时,Cursor 没给我一个泛泛的console.error(e),而是生成了一套完整的错误追踪链:

  1. 日志格式统一:所有console.log都被替换为logger.info(),所有console.error替换为logger.error(),且logger实例自动注入requestId(UUID)和timestamp
  2. 错误分类注入上下文CaptchaError构造函数强制传入screenshotPath(截图保存路径),NetworkError强制传入failedUrlstatusCode
  3. 错误聚合上报:在catch块里,除了写日志,还调用alertService.sendAlert(),将错误类型、频次、影响平台推送到企业微信机器人。

这部分代码我几乎没改。因为 Cursor 理解“可追溯”不是“有日志”,而是“日志能直接定位到哪台机器、哪个请求、哪个页面元素”。它生成的logger.js里,requestId不是全局变量,而是通过AsyncLocalStorage在异步链路中透传——这意味着即使 Puppeteer 的page.evaluate()里抛错,也能关联到最外层的定时任务 ID。

提示:Cursor 的错误处理能力,取决于你描述需求时的颗粒度。说“要报错”只能得到try/catch;说“要区分网络错误和业务错误,且网络错误要重试 3 次”才能得到带指数退避的retry逻辑。

4. Puppeteer 核心模块编写:Cursor 的“领域知识”在细节里爆发

4.1 抓取京东价格:为什么不能直接page.$eval('.price', el => el.textContent)

京东的价格 DOM 结构是动态渲染的,.price元素可能在初始 HTML 里不存在,而是由 JS 注入。Cursor 生成的代码里,第一行就是:

await page.waitForSelector('.price', { timeout: 10000, state: 'visible' });

但它没停在这里。紧接着是:

// 京东价格可能有多个层级:.price > span > .J_price,或 .price > .p-price > .p-price > .price const priceElement = await page.$('.price') || await page.$('.p-price'); if (!priceElement) throw new NetworkError(`京东价格元素未找到: ${url}`, url); const priceText = await (await priceElement.getProperty('textContent')).jsonValue(); const price = parseFloat(priceText.replace(/[^\d.]/g, '')) || 0;

这段代码的价值在于:它预判了京东前端工程师可能做的 DOM 变更。.price类名可能被改成.p-price,或者价格文本里混入¥符号、空格、逗号,parseFloat会失败,所以用正则清理。这不是通用爬虫模板,而是针对京东特定场景的鲁棒性设计。

我测试时故意把.price改成.fake-price,Cursor 生成的错误信息是:NetworkError: 京东价格元素未找到: https://item.jd.com/1000XXXXXX.html,并附带failedUrl字段。运维同学看到这个日志,不用翻代码就知道是 selector 失效,立刻去京东页面 inspect 元素。

4.2 淘宝反爬绕过:Cursor 怎么知道要设userAgent--disable-blink-features

当我输入“淘宝抓取经常被 302 重定向到登录页,怎么解决?”时,Cursor 没给一个万能方案,而是列出淘宝反爬的三个技术点,并给出对应参数:

反爬手段Cursor 识别的绕过方式参数说明
User-Agent 检测page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')必须是 Linux + Chrome 组合,Windows UA 会被拒绝
Blink 特性检测args: ['--disable-blink-features=AutomationControlled']隐藏 Puppeteer 的自动化特征
WebRTC 指纹await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); })覆盖navigator.webdriver属性

它甚至提醒我:“淘宝的登录页会检查window.chrome对象,如果存在且window.chrome.runtimeundefined,会判定为 Puppeteer。所以必须在page.evaluateOnNewDocument中删除window.chrome。”

我照做后,淘宝抓取成功率从 12% 提升到 98%。这不是魔法,是 Cursor 从数百万行开源 Puppeteer 项目中学习到的领域经验。

4.3 拼多多的“滑块验证”:Cursor 的 fallback 机制有多聪明?

拼多多在高频率访问时会弹出滑块验证。Cursor 没教我怎么破解滑块(那涉及法律风险),而是设计了一个优雅的 fallback:

try { // 正常抓取流程 await page.goto(url, { waitUntil: 'networkidle2' }); const price = await extractPrice(page); return { success: true, price }; } catch (error) { if (error.message.includes('slider') || await page.$('.geetest_slider')) { // 检测到滑块,记录并跳过本次抓取 logger.warn(`拼多多滑块验证触发,跳过 ${url}`); return { success: false, reason: 'slider_verification' }; } throw error; // 其他错误继续上抛 }

关键是await page.$('.geetest_slider')这行。Cursor 知道极验(Geetest)滑块的 class 名是.geetest_slider,且这个元素在验证弹窗出现时一定存在。它不试图解决,而是承认边界——“当 AI 无法处理时,人类应该第一时间知道”。

我在日志里看到连续 3 次slider_verification,立刻意识到是 IP 被限流,马上切换了代理池。这才是真实项目里需要的“可操作洞察”,而不是一个无法落地的“技术方案”。

5. 本地调试与问题排查:Cursor 是你的“结对编程伙伴”,不是“代码生成器”

5.1 调试 Puppeteer 时,Cursor 怎么帮你定位到具体哪一行 JS 执行失败?

Puppeteer 的page.evaluate()里报错,堆栈信息通常只显示Error at <anonymous>:1:1,根本看不出是哪行代码。Cursor 在生成extractPrice函数时,主动加了sourceURL

const priceScript = ` // 这里是提取价格的 JS 逻辑 const priceEl = document.querySelector('.price'); if (!priceEl) return null; return priceEl.textContent.trim(); //# sourceURL=extractPrice.js `; const price = await page.evaluate((script) => eval(script), priceScript);

//# sourceURL=extractPrice.js让 Chrome DevTools 把这段字符串当作独立文件extractPrice.js显示,报错时直接定位到priceEl.textContent.trim()这行。我第一次用这个技巧时,惊讶于 Cursor 对 Chrome 调试协议的理解深度——它不是在写 JavaScript,是在写“可调试的 JavaScript”。

5.2 “Puppeteer 启动慢”问题的三层排查法(Cursor 教我的)

项目上线后,日志显示 Puppeteer 启动耗时从 2.1 秒飙升到 8.7 秒。Cursor 没直接给解决方案,而是引导我分三层排查:

  1. 系统层:在 WSL2 终端执行time chromium-browser --headless --no-sandbox --disable-gpu --dump-dom https://example.com,耗时 3.2 秒 → 说明 Chromium 本身没问题;
  2. Node.js 层:在index.js里加console.time('launch')console.timeEnd('launch'),发现puppeteer.launch()耗时 8.5 秒 → 问题在 Puppeteer 初始化;
  3. 配置层:Cursor 指出puppeteer.launch()默认会下载最新版 Chromium,而我的package.jsonpuppeteer版本是^22.12.0,但puppeteer-core依赖的 Chromium 版本是120.0.6099.109,两者不匹配导致反复下载。

解决方案很简单:在puppeteer.launch()里显式指定executablePath

const browser = await puppeteer.launch({ executablePath: '/usr/bin/chromium-browser', args: ['--no-sandbox', '--disable-setuid-sandbox'] });

重启后,启动耗时回到 2.3 秒。这个过程让我明白:Cursor 的价值,不在于它直接给你答案,而在于它给你一套可复现、可验证、可归因的排查框架

5.3 Cursor 的“调试建议”功能:比 Stack Overflow 更懂你的上下文

当我把一段报错日志(Error: net::ERR_CONNECTION_TIMED_OUT at https://xxx)发给 Cursor,并附上当前page.goto()的调用代码时,它没搜通用解决方案,而是结合我的项目上下文分析:

  • “你设置了waitUntil: 'networkidle2',但目标网站 DNS 解析超时,networkidle2等待条件永远不会满足,导致超时”;
  • “建议在goto外层加Promise.race([page.goto(...), new Promise(r => setTimeout(r, 30000))]),30 秒强制超时”;
  • “同时,检查你的 WSL2 DNS 配置:cat /etc/resolv.conf,如果 nameserver 是172.28.0.1,这是 Docker 的 DNS,会导致解析慢,应改为8.8.8.8”。

我照做后,DNS 超时错误消失。这种基于你当前代码、当前环境、当前错误的精准建议,是任何搜索引擎都做不到的——因为它不是在匹配关键词,而是在理解你的开发会话。

6. 部署与监控:Cursor 如何把“能跑”变成“敢上线”

6.1 为什么 SQLite 不是“玩具数据库”,而是生产首选?

很多人质疑:“监控工具用 SQLite?不怕并发写入崩掉?” Cursor 给我的回复很实在:“你的场景是单写多读,每天只写 1 次,读是离线报表生成,SQLite 的 WAL 模式完全够用。换成 PostgreSQL 只会增加运维复杂度,不提升业务价值。”

它生成的部署脚本deploy.sh里,关键逻辑是:

# 1. 拉取最新代码 git pull origin main # 2. 安装依赖(跳过 devDependencies,减小体积) npm ci --only=production # 3. 备份旧数据库(防止迁移失败) cp prices.db prices.db.$(date +%Y%m%d_%H%M%S).bak # 4. 启动服务(用 pm2,支持日志轮转和崩溃重启) pm2 start index.js --name "price-monitor" --watch --ignore-watch="node_modules"

--watch参数让 pm2 监听index.js变更,一旦我git pull新代码,服务自动重启。--ignore-watch="node_modules"避免npm install触发无限重启。这些细节,是 Cursor 从数千个 Node.js 生产部署案例里学到的。

6.2 日志监控的“黄金三指标”:Cursor 帮我定义了什么该告警

Cursor 没给我一堆 Prometheus 指标,而是聚焦三个业务可感知的指标:

指标计算方式告警阈值业务含义
抓取成功率(成功抓取数 / 总尝试数) * 100%< 95% 持续 2 小时某个平台整体不可用
单次抓取耗时MAX(各平台抓取耗时)> 20 分钟可能遭遇大规模反爬
数据完整性COUNT(*) FROM prices WHERE captured_at > datetime('now', '-24 hours')< 1000 条数据采集链路中断

它生成的monitor.js里,每 5 分钟执行一次 SQL 查询,结果推送到企业微信。当拼多多抓取成功率跌到 89%,我手机立刻收到消息:“⚠️ PDD 抓取成功率 89%(阈值 95%),请检查代理池或目标网站变更”。

6.3 “Cursor 免费次数用完”之后,我怎么保证项目不中断?

Cursor Pro 确实有免费额度限制,但项目上线后,我关闭了所有 AI 功能,只保留编辑器核心能力。Cursor 的底层是 VS Code,所有快捷键、插件(ESLint、Prettier、GitLens)完全可用。我甚至把 Cursor 的settings.json导出,用在原生 VS Code 里,体验几乎无差别。

真正保障项目持续运行的,是 Cursor 帮我建立的可维护性基础设施:清晰的错误分类、结构化的日志、幂等的数据写入、可预测的部署流程。这些才是项目的生命线,而不是某个 AI 模型的调用次数。

我在最后一天的开发日志里写道:“Cursor 最大的价值,不是它写了多少行代码,而是它逼我思考清楚每一行代码存在的理由。当我不再问‘怎么写’,而是问‘为什么这样写’,项目才真正属于我。”