michaelkeithlewis/pirateship-bot
GitHub: michaelkeithlewis/pirateship-bot
通过驱动长期登录的 Chromium 浏览器调用 Pirate Ship 内部 GraphQL API,实现从命令行购买折扣 USPS/UPS 运单的自动化发货工具。
Stars: 0 | Forks: 0
# pirateship-bot
通过在一个运行于无头家庭服务器上的、始终保持登录状态的 Chromium 浏览器驱动
[Pirate Ship](https://www.pirateship.com),从命令行购买折扣的 USPS 和 UPS 运单。
Pirate Ship **没有公开的 API**。它的 Web 应用运行在一个未公开的内部
**GraphQL** endpoint (`/api/graphql`) 上,并使用普通的 session cookie 进行认证。因此,
该项目并没有为了获取所有数据而去爬取 UI,而是保持一个长期存活且
已登录的浏览器处于活动状态,并利用有效的 cookie 直接在页面内部*调用*该内部 API;仅在 API 自身无法完成任务时(如结账、运单下载、退款),
才回退到真实的 UI 点击操作。
整个流程已经使用真实资金进行了端到端验证:查询费率、购买运单、
下载并打印 PDF,以及申请退款。
## 为什么要这么做?
虽然存在直接访问 USPS API 的途径,但 Pirate Ship 增加了以下功能:
- 在 USPS 之外提供 **UPS 费率**,每笔发货并排显示报价
- **商业和 cubic USPS 定价**,通常比小型直接开户账户的费率更低
- 单一的余额/银行卡、统一的退款流程,以及一个作为备选方案的漂亮仪表盘
## 架构
其核心诀窍在于保持一个活动浏览器的运行,并且永不重启或强制重新加载:
```
Xvfb :95 -> openbox -> x11vnc :5904 -> websockify/noVNC :6084 (watch/login from any machine)
|
+-> headed Chromium, persistent ./profile, CDP on :9226
^
| playwright connectOverCDP (attach, never launch)
ship.js
```
- `serve.sh` 启动整个技术栈并阻塞运行;一个 systemd unit
(`pirateship-browser.service.example`) 让其在系统重启后依然保持运行。
- 该浏览器在 Xvfb 下以 **有头** 模式运行,这样该网站就不会检测到无头模式的
浏览器指纹。noVNC 让同一个屏幕可以通过浏览器标签页进行监视,这也是你
进行一次性登录的方式。
- `lib.js` 通过 CDP **附加 (attaches)** 到正在运行的浏览器。关闭 Playwright
连接只会执行分离操作;活动的浏览器及其 session 会继续存活。
- Cookies 持久化存储在 `./profile` (被 gitignored 忽略) 中,因此登录状态可以维持数周。
这也是我用于其他几个“无公开 API”服务的相同模式;只是
端口和特定网站的驱动程序有所不同。
## 一条严格规则:绝不执行硬导航
完整的页面加载 (`page.goto`,重新加载,打开应用 URL 的新标签页) 会触发
Cloudflare 验证并终止 session。一切都发生在已认证的活动
标签页内:
- 页面跳转都是 **SPA 链接点击** (点击 `` 元素,
让 React router 处理后续工作)。
- 数据请求通过页面内的 **`fetch()`** 发起至 `/api/graphql`,因此 httpOnly
`pirate_id` session cookie 会自动携带。Bot 本身永远不会
看到或存储该 cookie。
- 即使带有有效的 cookie,新打开的标签页也会被重定向至登录页,因此工作
始终在已认证的现有标签页中进行。
相关提示:永远不要伪造 user agent。Chromium 的 `Sec-CH-UA` client hints 会
与伪造的 UA 字符串产生冲突,从而导致你陷入验证循环。
## API 是如何被映射的
没有内省机制 (被禁用) 也没有文档,因此该 API 是从应用自身的
流量中映射出来的:
1. `gql_capture.js` 附加到活动的浏览器上,并记录每一个 GraphQL
请求和响应 (操作名称、变量、完整请求体),同时你可以
在 noVNC 中进行点击操作。几分钟的正常使用就能得出全部的
词汇表:`CreateBatchFromSingleShipmentMutation`, `BatchQuery`,
`BatchProcessStatusQuery` 等。
2. `drive_rates.js` 使用 Playwright 的 `fill()`
(React 控制的输入需要真实的事件) 重放单件运单表单,并捕获
“Get Rates” 流程触发的确切操作。
3. 被捕获的操作文档被签入到 `operations.json` 中,
并由 `ship.js` 使用新的变量原样重放。
在此过程中有两个有用的发现:
## 购买流程
1. **获取费率:** `createBatchFromSingleShipment(warehouseId, shipmentPresetId,
shipToAddress, ...)` 返回一个处于 `step: RATED` 状态的 batch。
2. **轮询:** `BatchQuery(id)` 直到 `rateGroups` 填充完毕;每个 `rateSummary`
都有 `uniqueId`, `totalPrice`, carrier, service, savings。
3. **购买:** 选择一个费率并点击应用自带的 **Buy Label** 按钮 (结账
的 mutation 涉及到支付状态,我宁愿让应用自己处理)。
通过监视 GraphQL 响应中的 `step: BILLED`
和 tracking number 来确认购买是否成功。
4. **获取运单:** PDF 的 URL *不* 在 `BatchQuery` 中。点击该发货的
Print/Reprint 按钮并捕获其触发的 `/download/.../label.pdf` 响应,
然后在页面内获取字节数据 (再次利用 cookies),并可选择通过
`lp` 发送到标签打印机。
5. **退款:** 在发货页面点击 Refund Label 并在弹窗中进行确认。
该弹窗会警告“最多 30 天”,但未使用的 USPS 标签会立即
将余额退回到 Pirate Ship 账户中。
## 安装说明
依赖项:Node 18+, Playwright (`npm i` 然后
`npx playwright install chromium`),以及 `xvfb`, `openbox`, `x11vnc`,
`websockify`/`novnc`。Tailscale 是可选的,但非常适合用于远程访问 noVNC
(脚本在没有配置时会回退到 localhost)。
```
./serve.sh # or install the systemd unit and enable it
./login.sh # prints the noVNC URL + login state
# 打开 URL,登录一次,勾选“Stay signed in”和 Cloudflare 选项
```
然后找到你账户的发货仓库 (ship-from) 和包裹预设 ID (在使用 `gql_capture.js` 加载 Ship 页面时捕获它们,或者读取单件运单表单上的 `