English summary. duo-grab is a personal, single-unit helper for the iPhone Duo pre-order on apple.com.cn, which opens Friday 2026-10-16 20:00:00 Asia/Shanghai (12:00 UTC). It gives you an NTP-corrected countdown, a long-running go-live / Get-Ready watcher with sound, desktop and push alerts (Bark / Telegram / ServerChan / PushPlus / WeCom), a post-launch pickup-stock monitor across Apple Stores, an environment self-check, and (workflow #2, not yet integrated) a Playwright checkout assistant. It cannot guarantee an order: it only removes the avoidable delays on your side. Python 3.10+, runs on macOS / Windows / Linux. Deps:
requests,rich,playwright.
是:一台笔记本上的“第二通道 + 情报层”。开售前它盯着 Apple 未缓存的接口,第一时间告诉你 「提前准备 / Get Ready」开放了、预购开放了 (GO LIVE)、哪家门店可取货;它用 NTP 校准你的电脑时钟, 毫秒级倒计时;它把提醒同时推到电脑喇叭、桌面通知和你的手机。
不是:黄牛脚本,不能保证抢到。Apple 侧的任何限流、排队、审核我们都无法绕开。 主通道仍然是 Apple Store app 的「提前准备」一键下单;本工具在旁边并行运行。
诚实的局限
- 开售瞬间的接口行为是根据 2026-09-16 的探测推断的 (见
FACTS.md),10/16 当天 Apple 可能改。 - SKU 页面 HTML 被 CDN 缓存约 60–120 s;工具因此主要盯未缓存的
buyability-message/pickup-message/updateSummary接口,HTML 只作辅助信号。 buy/prepare(浏览器自动下单)属于 workflow #2,本版本只有占位命令(打印提示并退出码 2)。- 请在中国大陆网络下使用。从海外 IP 访问时 Apple 会把部分接口重定向到美国商店 (404),
doctor的 geo 检查会提醒你。 - 一切都是尽力而为:通知失败、NTP 无响应都不会让程序崩溃,但也不会替你下单。
- 「GO LIVE」≠「抢到了」:它只表示 Apple 的接口显示可购买了;之后仍要靠你在 app / 网页里完成下单和付款。
需要 Python 3.10 或更新版本(python3 --version)。
git clone <this repo> duo-grab && cd duo-grab
python3 -m venv .venv
. .venv/bin/activate
pip install -e .
python -m playwright install chromium # 只有 buy/prepare 需要;现在装好省得当天着急
duograb --versiongit clone <this repo> duo-grab; cd duo-grab
py -3 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .
python -m playwright install chromium
duograb --version如果 PowerShell 拒绝执行激活脚本:Set-ExecutionPolicy -Scope CurrentUser RemoteSigned。
中文/emoji 显示乱码时用 Windows Terminal,或先执行 chcp 65001。
不想装成命令也可以直接 python -m duograb <command>。
duograb init --sku 白/512 --fallback nightsky/512gb --location "上海 上海" --lead-ms 300生成的文件(示例见 config.example.json):
{
"sku": "starwhite/256gb",
"fallback_skus": ["nightsky/256gb"],
"location": "上海 上海",
"extra_locations": ["江苏 苏州"],
"lead_ms": 300,
"poll": {"far_s": 30, "near_s": 2, "final_s": 0.5, "near_window_s": 600, "final_window_s": 60, "dedupe_s": 300},
"stock": {"interval_s": 5},
"notify": {"sound": true, "desktop": true, "bark_url": "", "telegram_token": "", "telegram_chat_id": "",
"serverchan_key": "", "pushplus_token": "", "wecom_webhook": ""},
"browser": {"profile_dir": ".chrome-profile", "channel": "chrome", "headless": false},
"checkout": {"fulfillment": "delivery", "store_number": "", "auto_submit": false}
}| 项 | 说明 |
|---|---|
sku |
目标机型。写法任选:MK2P4CH/A、starwhite/512gb、白 512、nightsky-1tb |
fallback_skus |
主目标没了时依次尝试的备选 (buy 用;watch/stock 也一起盯) |
location |
门店取货查询地点,必须是「省 市」中间带空格(上海 上海、广东 深圳)或邮编 200000 |
extra_locations |
stock 额外监控的地点 |
lead_ms |
buy 在 20:00:00.000 真实时间之前多少毫秒动手 (0–5000) |
poll.* |
watch 的轮询间隔:平时 far_s,开售前 near_window_s 秒内 near_s,最后 final_window_s 秒内 final_s(最小 0.5 s) |
notify.* |
通知渠道;留空的渠道跳过。Bark 填完整 URL(https://api.day.app/<key>),Telegram 填 bot token + chat id。Bark:请在 app 里允许「重要警告 (Critical Alerts)」,否则手机静音时 GO LIVE 推送无声;GO LIVE 会带 call=1(持续响铃)和点开即到 SKU 页的链接。Telegram 在大陆需要代理,而全局代理会同时改变 Apple 接口的 geo(非 CN)——推荐 Bark / Server酱 / PushPlus;doctor 会提示检测到的代理环境变量 |
poll.dedupe_s |
同一 (事件, 门店, SKU) 在这么多秒内不重复提醒(GO LIVE 永不去重);watch/stock 和通知器共用这一个值 |
browser.* / checkout.* |
给 workflow #2 的 buy 用,现在填了也不会生效 |
8 个 SKU(duograb init --sku 错误值 会把这张表打印出来)。每个 SKU 都有自己的购买页
https://www.apple.com.cn/shop/buy-iphone/iphone-duo/<part 小写,保留斜杠>;duograb init、watch 启动时和
clock 面板都会把目标 URL 打印出来,19:55 直接在 Chrome 里打开它(而不是家族页再选颜色/容量)。
加上 ?purchaseOption=fullPrice&igt=true&acpart=none 可跳过以旧换新模块,少点一步。
| part | 容量 | 颜色 | 价格 | 购买页 |
|---|---|---|---|---|
| MK2M4CH/A | 256GB | 星光白色 | ¥15999 | …/iphone-duo/mk2m4ch/a |
| MK2N4CH/A | 256GB | 夜空色 | ¥15999 | …/iphone-duo/mk2n4ch/a |
| MK2P4CH/A | 512GB | 星光白色 | ¥17999 | …/iphone-duo/mk2p4ch/a |
| MK2Q4CH/A | 512GB | 夜空色 | ¥17999 | …/iphone-duo/mk2q4ch/a |
| MK2T4CH/A | 1TB | 星光白色 | ¥21499 | …/iphone-duo/mk2t4ch/a |
| MK2U4CH/A | 1TB | 夜空色 | ¥21499 | …/iphone-duo/mk2u4ch/a |
| MK2V4CH/A | 2TB | 星光白色 | ¥26499 | …/iphone-duo/mk2v4ch/a |
| MK2W4CH/A | 2TB | 夜空色 | ¥26499 | …/iphone-duo/mk2w4ch/a |
JSON 不支持 // 注释。配置文件缺失时所有命令用默认值运行并提示你 duograb init。
全局参数(放在子命令前后都可以):--config PATH(默认 ./config.json)、--log-dir DIR(日志目录,默认当前目录)。
生成 config.json(父目录不存在会自动创建)。参数 --sku --fallback(可重复/逗号分隔,--fallback "" 表示无备选)
--location --extra-location --lead-ms --force(覆盖已有文件)。写完会打印目标/备选 SKU 的购买页 URL。
环境自检,一张 pass / warn / fail 表:Python 版本、距开售时间、配置合法性、Playwright + Chromium/Chrome、
到 www.apple.com.cn 的 RTT(>500 ms 警告)、Apple geo cookie(非 CN 警告;同时提示是否检测到代理环境变量)、
NTP 偏差(>200 ms 警告)、watch 依赖的三个接口——pickup-message(isComingSoon)、buyability-message(isBuyable,
最快的开售信号)、updateSummary(isBuyable / isGetReadyEnabled)——SKU 页 comingSoon/getReady、浏览器配置目录的磁盘空间。
退出码 0 / 1 (有警告) / 2 (有失败)。
rich 倒计时面板:真实北京时间(毫秒)、目标 SKU 购买页 URL、本机时钟偏差和用了哪些 NTP 服务器、
apple.com.cn RTT、Apple Date 头偏差、T-hh:mm:ss.mmm。T-60s、T-10s、T-5…T-1 各响一声;
T0 响两声并提示「⏰ 20:00:00 到点」——这只代表时间到了,不代表 Apple 真的开放了;
真正的开放以 watch 的 🟢 GO LIVE(三声、critical 推送)为准,两者提示音的节奏不同,好区分。
每个里程碑只响一次(NTP 重新校时把时间往回拨也不会再响)。启动时复用一次 NTP 同步,不再等第二次;
之后每 60 s 重新同步,最后 5 分钟每 10 s。开售后显示 T+ 5 分钟然后退出。
--demo [N]:目标设为 N 秒后(默认 20),T+10 s 自动退出,用来彩排。--target 2026-10-16T20:00:00+08:00:自定义目标(不带时区 = 北京时间)。--no-beep:静音(里程碑提示音和通知器的电脑喇叭都关;桌面通知和手机推送照常)。
开售前长期运行。每轮查 buyability-message(8 个 SKU,最快的未缓存信号)和 pickup-message
(isComingSoon + 各门店 pickupDisplay);每 60 s 查一次 SKU 页面 HTML 和 updateSummary
(isGetReadyEnabled)——但在开售前后 90 s 内不查这两个慢接口(它们的超时最长 10 s,不能挡住 0.5 s 的快轮询),
开售前 10 分钟内它们的超时缩到 3 s。发生变化时提醒:
- 🟡
getReady/isGetReadyEnabled变 true → “Apple 已开放「提前准备」— 现在去 Apple Store app 预先配置!” - 🟢
isBuyable变 true 或isComingSoon/comingSoon变 false → GO LIVE(critical,响 3 声,永不去重)。 推送正文里给出 Apple 翻转的真实北京时间区间(上一次“还没开”的响应 ~ 这次响应之间),并把同时开放的其他机型 折叠成“全部 8 款同时开放 / 另有 N 款”。若pickup-message的availability字段直接消失且门店状态变了,也算 GO LIVE。 - 🟢 若
watch在 Apple 已经开放之后才启动/重启,第一轮就会提示「启动时已处于开放状态」(开售前后 2 小时内 critical,否则 info)。 - 🏬 目标/备选 SKU 在某门店
pickupDisplay变available ⚠️ 连续 5 次接口失败提醒一次;5xx/429/超时时退避 ×2 最多 30 s(开售前后 60 s 内最多 1 s),永远不快于 0.5 s。 任何单次异常(接口返回坏 JSON 等)只记日志、退避、继续;只有 Ctrl-C 能停下它。
参数:--once(只查一轮打印状态,不发通知)、--until ISO8601(自动停止时间)、--location(临时覆盖)。
每行都带 NTP 校准时间戳写入 watch.log(JSON lines,含 ntp_offset_ms;超过 20 MB 自动轮转为 watch.log.1)。
后台每 10 分钟重新 NTP 校时(开售前后 5 分钟内每 30 s),偏差变化超过 50 ms 会打印「重新校时」。
状态行图例(启动时也会打印一次):可买 = buyability-message isBuyable · 即将推出 = pickup-message isComingSoon ·
页面即将推出 = SKU 页 comingSoon · 提前准备 = getReady / isGetReadyEnabled · 取货 n/m店 = 可取货 (门店,SKU) 数 / 返回门店数 ·
pm/buy = 两个接口的耗时 · ↻ = 下次轮询间隔 · 退避 = 正在因限流/故障放慢。
开售后门店库存监控:每 interval_s 秒查每个地点的所有目标 SKU,新出现的可取货 (门店, SKU) 立即提醒,
终端显示一张紧凑表格。参数:--parts "白 512,MK2Q4"、--locations "上海 上海,江苏 苏州"、
--interval 5、--once、--until、--beep-until-ack(有货时每秒用系统声音——afplay / paplay / winsound——响铃直到按 Enter;
单纯的终端 \a 在 iTerm2 / macOS 终端 / GNOME Terminal 里默认是静音的,所以不只靠它)。日志 stock.log(同样 20 MB 轮转)。
向每个已配置渠道(console / log / 声音 / 桌面 / Bark / Telegram / Server酱 / PushPlus / 企业微信)发一条测试消息,
打印每个渠道 ok / skipped / warn / error。声音一栏若是 warn: … bell only,表示没有找到播放器、只响了终端铃——
此刻你应该听到一声;听不到就装 paplay(Linux)或改依赖手机推送。请务必在开售前用它确认手机能收到推送。
浏览器下单助手,属于 workflow #2,本版本只打印 buy.py 尚未集成 (workflow #2) 并以退出码 2 退出。
详细作战手册以后放在 PLAYBOOK.md;这里是骨架:
| 时间 | 做什么 |
|---|---|
| 现在 | 安装;duograb init;duograb doctor 全绿或只剩可接受的警告;填好 notify 并跑 duograb notify-test 确认手机收到 |
| 现在 → 10/16 | 在一台常开的电脑上跑 duograb watch(--log-dir 指到固定目录)。它会在 Apple 打开「提前准备」时第一时间提醒你 |
| 「提前准备」开放后 | 去 Apple Store app 预先选好机型、地址、支付方式;同时在 Chrome 里登录 apple.com.cn 并保存收货地址 |
| 10/16 19:00 | 重新 duograb doctor(关掉 VPN/全局代理,geo 必须是 CN);开一个终端 duograb clock;另一个终端确认 watch 还在跑;手机 app 停在「提前准备」页面 |
| 10/16 19:55 | clock 每 10 s 重新校时;手指放到 app 的按钮上;电脑上的 Chrome 打开 clock 面板 / watch 启动时打印的目标 SKU URL(带 ?purchaseOption=fullPrice&igt=true&acpart=none 可跳过以旧换新) |
| 20:00:00 | clock 的「⏰ 到点」(两声)只是时间到了;watch 的「🟢 GO LIVE」(三声 + 手机推送)才是 Apple 真的开放了。到点后先在 app 试,同时刷新 Chrome 的 SKU 页;Apple 常晚开几秒到几分钟,watch 一响立即点。也可能提前开——watch 的提醒可以早于 clock。先 app 后网页,哪个先出结账页用哪个 |
| 20:05 仍没动静 | 别关 app;另开终端 duograb watch --once 看接口现状;app 排队页超过 2–3 分钟可杀掉重开 / 切 5G↔Wi-Fi(见 FACTS.md) |
| 20:00 之后 | 若没抢到:duograb stock --beep-until-ack 盯门店取货,extra_locations 多写几个城市 |
. .venv/bin/activate
pip install -e ".[dev]"
python -m pytest -q # 单元测试不联网
python -m pytest -q -m live # 可选:真的访问一次 Apple / NTP模块:parts(SKU 表、GO_LIVE)· apple_api(四个只读接口客户端)· clock(SNTP)· config ·
notify · watch · stock · countdown · doctor · cli。设计约定见 DESIGN.md,探测到的事实见 FACTS.md。
MIT License. 仅供个人购买一台使用;请遵守 Apple 的服务条款。