yeahdongcn/duo-grab

★ 1Forks 0PythonGitHub ↗Compare

README

duo-grab — iPhone Duo 国行预购小助手

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)。

macOS / Linux

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 --version

Windows (PowerShell)

git 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>。

配置 config.json

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(日志目录,默认当前目录)。

duograb init

生成 config.json(父目录不存在会自动创建)。参数 --sku --fallback(可重复/逗号分隔,--fallback "" 表示无备选) --location --extra-location --lead-ms --force(覆盖已有文件)。写完会打印目标/备选 SKU 的购买页 URL。

duograb doctor

环境自检,一张 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 (有失败)。

duograb clock

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:静音(里程碑提示音和通知器的电脑喇叭都关;桌面通知和手机推送照常)。

duograb watch

开售前长期运行。每轮查 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 = 两个接口的耗时 · ↻ = 下次轮询间隔 · 退避 = 正在因限流/故障放慢。

duograb stock

开售后门店库存监控:每 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 轮转)。

duograb notify-test

向每个已配置渠道(console / log / 声音 / 桌面 / Bark / Telegram / Server酱 / PushPlus / 企业微信)发一条测试消息, 打印每个渠道 ok / skipped / warn / error。声音一栏若是 warn: … bell only,表示没有找到播放器、只响了终端铃—— 此刻你应该听到一声;听不到就装 paplay(Linux)或改依赖手机推送。请务必在开售前用它确认手机能收到推送。

duograb buy / duograb prepare

浏览器下单助手,属于 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 的服务条款。

Contributors

yeahdongcn

Issues