用Node.js和Codex打造iPhone库存监控:从轮询到提醒的实战指南
1. 从一个抢购场景说起为什么要自己写库存监控iPhone 新机首发那几天官网的购买按钮基本就是玄学。你盯着页面刷新它显示“暂无供应”你切个外卖回来黄牛已经加价八百在朋友圈晒单了。我去年蹲 iPhone 17 首发的时候连续三天手动刷新结果连购物袋都没进去过。今年 iPhone 18 传闻备货更紧张我决定不再拼手速而是让程序替我盯着。这个项目的核心目标很朴素定时轮询苹果官网的库存接口一旦目标机型从“暂无供应”变成“有货”立刻通过本地通知或声音提醒我让我第一时间手动下单。注意这里说的是“提醒”不是“自动下单”。自动下单涉及账号风控、验证码、支付授权等一堆麻烦事而且容易触发平台的风控机制得不偿失。我们要做的是一个库存状态探测器把“什么时候有货”这个信息差抹平。技术选型上我用Codex作为主要的代码生成与辅助工具配合Node.js写核心轮询逻辑Chrome负责调试和观察网络请求JavaScript作为唯一的开发语言。这套组合的好处是Codex 能帮我快速生成重复性的 HTTP 请求代码和数据处理逻辑Node.js 天生适合做定时任务和网络 IOChrome 的开发者工具则是我分析苹果官网接口的“显微镜”。整个项目不需要复杂的框架一个index.js加上几个辅助模块就能跑起来。适合谁来参考这篇内容如果你有JavaScript 基础知道fetch或axios怎么用了解setInterval和Promise的基本概念那就可以直接抄作业。如果你完全没写过代码也不用慌我会把每一步的操作意图和参数选择都讲清楚你照着做也能跑通。这个项目的门槛不在于代码有多难而在于你能不能看懂浏览器里那些网络请求以及愿不愿意花半小时调试轮询频率。提示本文所有操作均基于公开的网页接口和本地开发环境不涉及任何账号登录、支付流程或自动化下单行为。库存监控的本质是信息聚合请合理使用避免对目标服务器造成不必要的压力。2. 整体设计思路与工具链拆解2.1 为什么选 Node.js 而不是 Python 或浏览器插件很多人第一反应是用 Python 写爬虫毕竟requests库很顺手。但我选 Node.js 有几个很实际的理由。第一JavaScript 的异步模型天然适合高频轮询。Node.js 的事件循环在处理大量并发 HTTP 请求时内存占用比 Python 的多线程方案低得多。你开十个轮询任务Node.js 可能只占几十兆内存Python 开十个线程就要小心 GIL 和上下文切换的开销。第二Chrome 开发者工具和 Node.js 共享同一套 JavaScript 生态。我在 Chrome 里分析苹果接口的时候可以直接把fetch请求复制成 Node.js 代码粘贴到项目里改改就能用。这种无缝切换的体验是 Python 做不到的。第三Codex 对 JavaScript 的支持非常成熟我让它生成一个带重试机制的轮询函数它能把AbortController、超时处理、指数退避这些细节都写出来省了我大量查文档的时间。至于浏览器插件方案我试过用 Chrome 扩展的chrome.alarmsAPI 做定时请求。问题是扩展的权限模型越来越严格跨域请求需要配置host_permissions而且后台脚本容易被浏览器休眠。Node.js 跑在本地终端里想怎么轮询就怎么轮询不受浏览器标签页生命周期的影响。2.2 Codex 在这个项目里到底帮了什么忙Codex 不是万能的但在几个特定环节它确实像有个结对编程的搭档。第一个环节是接口响应数据的解析。苹果官网返回的 JSON 结构嵌套很深手动写data.body.content.productSelection[0].items[1].availability这种路径很容易出错。我把一段脱敏后的响应样本丢给 Codex让它生成一个提取库存状态的函数它几秒钟就给出了带可选链和默认值的版本比我手写快得多。第二个环节是错误处理和重试逻辑。轮询最怕的就是网络抖动导致程序崩溃。我让 Codex 写一个带指数退避的重试包装器它给出了一个retryFetch函数支持最大重试次数、初始延迟、退避倍数等参数。我只需要调整参数值不用自己从头实现。第三个环节是通知模块的快速原型。我想在终端里用声音和文字同时提醒Codex 直接生成了调用系统命令播放提示音的代码还贴心地加了跨平台判断。当然Codex 生成的代码不能直接无脑用尤其是涉及具体接口地址和参数的地方它可能会“幻觉”出一些不存在的字段。我的做法是让 Codex 写骨架和通用逻辑我自己填业务细节和验证数据。2.3 轮询频率的取舍别把服务器惹毛了轮询频率是这个项目最关键的参数。设得太慢比如五分钟一次那基本等于没监控有货也被别人抢光了。设得太快比如一秒一次你的 IP 很快就会被限流甚至封禁而且对目标服务器也不公平。我的经验值是15 到 30 秒一次。这个频率在首发抢购场景下足够灵敏因为库存释放通常不是毫秒级的从“有货”到“被抢完”一般有几十秒的窗口期。15 秒的轮询间隔意味着你最多错过 15 秒但换来的是长时间稳定运行不被封。如果你监控的是非热门机型可以放宽到 60 秒一次。另外一定要加随机抖动。不要固定每 15 秒请求一次而是在 12 到 18 秒之间随机。这样请求的时间点不会形成规律降低被识别为机器行为的概率。实现上很简单把setInterval换成递归的setTimeout每次计算下一次延迟时加上Math.random() * 6000 - 3000的偏移量。注意轮询频率没有绝对标准你需要根据目标接口的响应速度和自身网络环境做调整。如果发现请求开始返回 429 或 403立刻降低频率或暂停一段时间。3. 核心细节解析与实操要点3.1 如何在 Chrome 里找到真正的库存接口这是整个项目最需要耐心的一步。打开苹果官网的购买页面按 F12 打开开发者工具切换到 Network 面板勾选Fetch/XHR过滤器。然后刷新页面你会看到一堆请求。这时候不要急着一个个点开看先用搜索框过滤关键词比如availability、stock、product或者你目标机型的型号代码。我实测下来苹果的库存状态通常藏在某个返回 JSON 的接口里响应体里会有类似availability: IN_STOCK或availability: OUT_OF_STOCK的字段。有时候它不会直接写IN_STOCK而是用isAvailable: true或者stockLevel: HIGH这种变体。你需要仔细看响应结构找到那个真正表示“能不能买”的字段。找到接口后右键点击该请求选择Copy-Copy as Node.js fetch。Chrome 会帮你生成一段包含完整请求头和请求体的代码。这段代码就是你轮询逻辑的起点。把它粘贴到一个临时文件里用node跑一下看看能不能拿到正确的响应。如果返回 403 或 401说明缺少某些请求头你需要把 Chrome 里看到的User-Agent、Accept、Referer等头信息补全。提示苹果的接口可能会校验Referer和Origin确保这两个头信息与官网域名一致。另外有些接口需要携带特定的Cookie但我不建议在监控工具里使用登录态 Cookie因为那会涉及账号安全而且 Cookie 过期后需要重新获取。3.2 用 JavaScript 写一个健壮的轮询函数轮询函数的核心逻辑不复杂但要写得健壮需要处理几个边界情况。第一是超时控制。如果某个请求卡住不返回整个轮询循环就会被阻塞。用AbortController配合setTimeout可以实现请求级别的超时。第二是错误隔离。单次请求失败不应该导致整个程序退出而是记录错误后继续下一次轮询。第三是状态变化检测。只有当库存状态从“无”变成“有”时才触发提醒避免重复通知。下面是我实际使用的轮询函数骨架基于 Node.js 的fetchAPINode 18 以上原生支持const AbortController globalThis.AbortController; async function checkStock(url, options, timeoutMs 10000) { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), timeoutMs); try { const response await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(HTTP ${response.status}); } const data await response.json(); return parseAvailability(data); } catch (error) { clearTimeout(timeoutId); if (error.name AbortError) { console.warn(请求超时跳过本次轮询); } else { console.warn(请求失败:, error.message); } return null; } }parseAvailability函数需要你根据实际响应结构来写。我的做法是先用console.log(JSON.stringify(data, null, 2))把完整响应打印出来找到表示库存的字段路径然后用可选链安全地提取。比如function parseAvailability(data) { const availability data?.body?.content?.productSelection?.[0] ?.items?.[0]?.availability; return availability IN_STOCK ? in_stock : out_of_stock; }这里用?.可选链是为了防止某一层字段缺失导致程序报错。如果接口返回结构变了parseAvailability会返回out_of_stock而不是崩溃这样轮询可以继续运行你只需要后续调整字段路径即可。3.3 状态管理与提醒触发逻辑轮询本身只是手段真正的价值在于状态变化时及时提醒。我维护一个简单的状态变量lastStatus初始为null。每次轮询拿到新状态后和lastStatus比较如果lastStatus是out_of_stock新状态是in_stock触发“有货”提醒。如果lastStatus是in_stock新状态是out_of_stock触发“售罄”提醒可选。如果状态没变什么都不做避免重复通知。提醒方式我用了两种终端文字高亮和系统提示音。文字部分用console.log配合 ANSI 颜色码让“有货”两个字在终端里闪烁。声音部分在 macOS 上调用afplay播放系统音效在 Windows 上用powershell播放SystemSounds。Codex 帮我生成了跨平台的判断逻辑我只需要把音效文件路径换成自己喜欢的就行。注意如果你在服务器上跑这个脚本声音提醒就没意义了。这时候可以换成发送邮件或写入日志文件。但邮件发送需要配置 SMTP相对麻烦我建议先用本地终端跑确认逻辑没问题后再考虑迁移。4. 完整实操流程从零跑通库存监控4.1 环境准备与依赖安装首先确认你的机器上装了 Node.js。打开终端输入node -v如果显示版本号大于等于 18就可以直接用原生的fetch。如果版本低于 18要么升级 Node.js要么安装node-fetch包。我推荐直接升级到最新的 LTS 版本Node.js 官网下载安装包一路下一步就行。然后创建一个项目目录比如stock-monitor进入目录后执行npm init -y生成package.json。这个项目不需要额外的 npm 包因为fetch和AbortController都是 Node.js 内置的。如果你想要更友好的终端输出可以装一个chalk但我觉得没必要用 ANSI 转义码就够了。目录结构建议这样组织stock-monitor/ ├── index.js # 主入口轮询循环 ├── config.js # 配置文件放接口地址和轮询参数 ├── parser.js # 响应解析函数 └── notifier.js # 提醒模块把配置单独抽出来是为了方便调整。比如你想换一个机型监控只需要改config.js里的接口地址和字段路径不用动主逻辑。4.2 配置文件与参数计算config.js里我放了几个关键参数module.exports { apiUrl: https://www.apple.com.cn/shop/fulfillment-messages?..., headers: { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..., Accept: application/json, Referer: https://www.apple.com.cn/shop/buy-iphone/..., }, pollIntervalMs: 15000, jitterMs: 3000, requestTimeoutMs: 10000, maxRetries: 3, };pollIntervalMs是基础轮询间隔我设了 15 秒。jitterMs是随机抖动范围实际延迟会在15000 - 3000到15000 3000之间也就是 12 到 18 秒。requestTimeoutMs是单次请求超时10 秒足够。maxRetries是单次轮询内的重试次数我设了 3 次配合指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒。这些参数不是拍脑袋定的。15 秒的基础间隔是基于“库存释放窗口通常大于 30 秒”这个观察。抖动是为了避免请求时间点过于规律。超时 10 秒是因为苹果接口正常响应在 1 到 3 秒超过 10 秒基本就是网络异常了。重试 3 次是在“及时性”和“不惹毛服务器”之间取的平衡。4.3 主循环的递归实现与优雅退出主循环我用递归setTimeout而不是setInterval原因是setInterval不会等待上一次请求完成如果请求耗时超过间隔会导致请求堆积。递归setTimeout保证上一次轮询完全结束后才计算下一次的延迟。const config require(./config); const { checkStock } require(./parser); const { notify } require(./notifier); let lastStatus null; let running true; async function poll() { if (!running) return; const status await checkStock(config.apiUrl, { headers: config.headers, }, config.requestTimeoutMs); if (status status ! lastStatus) { if (status in_stock) { notify(有货了赶紧下单); } else if (lastStatus in_stock) { notify(又售罄了继续蹲。); } lastStatus status; } const delay config.pollIntervalMs (Math.random() * 2 - 1) * config.jitterMs; setTimeout(poll, delay); } process.on(SIGINT, () { running false; console.log(\n监控已停止。); process.exit(0); }); poll();按CtrlC时触发SIGINT把running设为false当前轮询结束后就不再调度下一次。这样退出很干净不会留下悬挂的定时器。4.4 实测记录第一次跑通的全过程我第一次跑的时候接口返回的是 403。检查后发现是User-Agent用了 Node.js 默认的被苹果识别为爬虫。把 Chrome 里的User-Agent复制过来后请求成功返回 200。但解析出来的状态一直是out_of_stock我一度以为接口找错了。后来把完整响应打印出来发现库存字段在body.content.productSelection[0].items[0].availability下面而我之前写的是body.content.productSelection[0].availability少了一层items。修正路径后状态正确显示为out_of_stock。为了测试“有货”提醒我临时把parseAvailability改成随机返回in_stock或out_of_stock。跑了几轮后终端成功弹出“有货了赶紧下单”的提示声音也响了。确认逻辑没问题后再把解析函数改回真实逻辑。整个调试过程大概花了四十分钟其中大部分时间用在找接口和确认字段路径上。代码本身很简单难点在于理解目标网站的数据结构。5. 常见问题与排查技巧实录5.1 请求返回 403 或 429 怎么办403 通常是请求头不完整。重点检查User-Agent、Referer、Origin、Accept这四个头。苹果的接口对Referer比较敏感确保它和官网域名一致。如果还是 403尝试把 Chrome 里该请求的所有请求头都复制过来包括Accept-Language、Sec-Fetch-*这些看起来不重要的头。429 是请求过于频繁。这时候不要硬刚立刻把轮询间隔调大比如从 15 秒改成 60 秒然后等半小时再试。如果 429 持续出现可能需要更换网络环境或等待更长时间。我的经验是15 秒间隔配合随机抖动连续跑几个小时不会触发 429。但如果你同时监控多个机型每个机型都开一个轮询任务总请求频率就会翻倍这时候要相应降低每个任务的频率。注意不要用代理池或频繁更换 IP 来绕过限流这既不道德也可能违反服务条款。合理的做法是尊重服务器的承受能力用较低的频率换取长期稳定。5.2 接口返回的数据结构变了苹果官网的接口不是一成不变的。大促期间或者版本更新后字段路径可能会调整。应对策略是把解析逻辑和轮询逻辑解耦。parser.js里只负责从响应中提取库存状态如果提取失败就返回null主循环收到null时跳过状态比较继续下一次轮询。这样即使接口变了程序也不会崩溃只是不再触发提醒。你只需要重新抓包更新parser.js里的字段路径即可。我还会在parser.js里加一个“调试模式”通过环境变量控制是否打印完整响应。当发现状态一直不变时开启调试模式看一眼原始响应很快就能定位问题。5.3 程序跑了一晚上早上发现停了最常见的原因是未捕获的异常导致进程退出。虽然我在checkStock里做了 try-catch但notify函数如果抛错也会让主循环中断。解决办法是在poll函数的最外层再包一层 try-catch确保任何未预料的错误都不会终止循环。另一个原因是系统休眠。笔记本合盖后Node.js 进程会被挂起setTimeout不再触发。如果你用笔记本跑监控记得在电源设置里把“合盖不休眠”打开或者干脆用一台常开的设备。我后来换了一台闲置的小主机跑这个脚本稳定性好了很多。还有一个隐蔽的问题是内存泄漏。如果每次轮询都创建新的AbortController但没有正确释放长时间运行后内存会缓慢增长。Node.js 的垃圾回收通常能处理这种情况但如果你发现内存持续上涨可以用process.memoryUsage()打印一下确认没有持有不必要的引用。5.4 常见问题速查表问题现象可能原因排查方法解决方案请求返回 403请求头缺失或不匹配对比 Chrome 里的请求头补全 User-Agent、Referer 等请求返回 429轮询频率过高查看响应中的 Retry-After降低频率增加抖动状态一直不变字段路径错误打印完整响应重新抓包更新解析路径程序意外退出未捕获异常查看终端错误输出外层加 try-catch内存持续增长资源未释放打印 memoryUsage检查 AbortController 和定时器提醒不触发状态比较逻辑错误打印 lastStatus 和新状态修正比较条件5.5 几个我踩过的坑和对应技巧第一个坑是时区问题。苹果的库存释放有时和特定时间点有关比如每天上午 10 点。如果你用Date对象做时间判断注意 Node.js 默认使用系统时区。我建议统一用 UTC 时间做逻辑判断显示的时候再转成本地时间。第二个坑是JSON 解析失败。有时候接口返回的不是 JSON而是一段 HTML 错误页。直接response.json()会抛异常。稳妥的做法是先读response.text()然后尝试JSON.parse失败时打印原始文本。这样你能看到服务器到底返回了什么。第三个坑是终端颜色码在 Windows 上不生效。ANSI 转义码在 Windows 10 之前的 cmd 里不支持。如果你用 Windows要么升级到 Windows Terminal要么用chalk这类库做兼容处理。我后来在 Windows 上测试时直接去掉了颜色用[有货]这样的文字标记代替。第四个坑是声音提醒太吵。第一次触发时我设了循环播放结果有货的那几秒里声音响个不停。后来改成只播放一次并且加了一个“静默期”触发提醒后 60 秒内不再重复提醒避免状态在“有货”和“无货”之间快速切换时反复响铃。6. 后续可以扩展的方向与个人体会这个工具跑通之后我陆续加了一些小功能。比如多机型监控把配置改成数组每个机型一个轮询任务共用一个提醒模块。再比如日志记录每次状态变化写入一个log.txt方便事后分析库存释放的规律。我还试过把提醒推送到手机但涉及第三方推送服务配置起来比较麻烦后来觉得本地声音提醒已经够用了。Codex 在这个过程中扮演的角色更像一个“快速原型生成器”。它帮我省去了查 API 文档和写重复代码的时间但核心的业务逻辑——比如接口地址、字段路径、轮询策略——还是需要我自己判断和验证。我的体会是把 Codex 当成一个执行力很强但需要明确指令的助手而不是一个能替你思考的专家。你给它的上下文越具体它生成的代码越可用。最后分享一个小技巧如果你不确定某个接口是否值得监控可以先用 Chrome 的Copy as fetch手动请求几次观察响应时间和数据稳定性。如果接口本身响应很慢或者经常超时那就不适合做高频轮询换一个接口或者降低频率才是正道。库存监控的本质是信息获取不是技术炫技能稳定拿到数据比什么都重要。