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 页面时捕获它们,或者读取单件运单表单上的 `