内容
# scanner-fly
一个 Rust 单文件二进制程序,一次对比六个机票网站,提供搜索结果、特价列表和直达链接。同一个二进制既可以作为 CLI 直接运行,也可以作为 MCP stdio 服务器接入客户端。
## 对比网站
| 网站 | CLI 提供的信息 |
| --- | --- |
| [LuckyGlide](https://luckyglide.vercel.app) | 临近日期各目的地的实时最低价;条件预订走 MyRealTrip 路径,LuckyGlide 首页作为参考链接保留 |
| [CatchFrog](https://catchfrog.ai) | 相对均价大幅下跌的特价,以及填好航线、日期、住宿天数的深链接 |
| [Google Flights](https://www.google.com/travel/flights) | 实时搜索运价和搜索链接 |
| [Naver 机票](https://flight.naver.com) | 填入搜索条件的对比链接 |
| [Skyscanner](https://www.skyscanner.co.kr) | 填入搜索条件的对比链接 |
| [Skiplagged](https://skiplagged.com) | 填入搜索条件的对比链接 |
支持的情况下,搜索结果还会一并显示 MyRealTrip 的实时运价。各网站对外暴露的能力不同,不要假设所有站点都能直接抓价。无法直接查询的站点会提供填好搜索条件的链接,方便在浏览器里交叉核对最终价格。
## 运行截图
下面两张截图都是正式发布二进制(release binary)的真实运行捕获,不是示意图,也不是后期合成的图片。


## 构建
需要 Rust 1.85 或更高版本的 stable toolchain。
```bash
cargo build --release
```
生成的单文件二进制位于以下路径。
```text
target/release/scanner-fly
```
下面的示例假设 `scanner-fly` 已经在 `PATH` 中。不安装直接运行时,把 `scanner-fly` 换成 `./target/release/scanner-fly` 即可。
```bash
cargo install --path .
scanner-fly --help
```
项目本身按 `LICENSE` 中的 MIT 条款分发,静态链接的原生 TLS 代码和 vendored 依赖的许可证原文收录在 `THIRD_PARTY_NOTICES.md` 和 `licenses/` 目录中。
## CLI 用法
五个 CLI 命令均支持 `--json`。`--open` 适用于 `search`、`deals`、`links` 和 `multi-city`,不适用于 `compare-fares`。`search --open` 和 `links --open` 会打开六个对比网站的动作/参考 URL,加上填好条件的 MyRealTrip 预订 URL,去重后共七个;`deals --open` 打开 LuckyGlide 和 CatchFrog。
### 机票搜索
```bash
scanner-fly search \
--from ICN \
--to NRT \
--depart 2026-09-15 \
--return 2026-09-19 \
--adults 1
```
`search` 把通过 MyRealTrip 价格日历获取的 LuckyGlide 数据、Google Flights 查到的实时运价、六个对比网站链接以及 MyRealTrip 预订链接汇总到同一份结果中。当前搜索仅支持往返,`--return` 为必填参数。
### 多程机票
```bash
scanner-fly multi-city \
--leg YVR,YLW,2026-09-10 \
--leg YLW,SFO,2026-09-12 \
--leg SFO,JFK,2026-09-15 \
--adults 1 \
--json
```
`--leg` 可以重复,格式为 `ORIGIN,DESTINATION,YYYY-MM-DD`。至少需要两段;前一段的到达机场必须与下一段的出发机场衔接,日期必须相同或更晚。Google Flights 和 Naver 提供 `all_legs` URL,把完整顺序行程作为一次搜索传入。Skyscanner 和 Skiplagged 的 `complete_legs` 是所请求每一段的**独立单程搜索 URL**,不是联程行程、通程运价或合计运价。Skyscanner URL 显式携带成人数、Economy 舱、无儿童、单程状态,Skiplagged URL 传递成人数。LuckyGlide 和 CatchFrog 不支持多程搜索,只显示 `reference_only` 参考 URL。CatchFrog 的参考 URL 是真实服务首页 `https://catchfrog.ai/`,不表达多程行程。程序不会对独立航段的价格求和,也不显示行程总价。
### 暴跌特价查询
```bash
scanner-fly deals \
--from ICN \
--region 亚洲 \
--trip-days 4 \
--days 30 \
--top 20
```
`--from` 默认值为 `ICN`,`--days` 默认 30,`--top` 默认 20。省略 `--trip-days` 时查询 3 到 7 天的行程。`--region` 可选值为 `亚洲`、`欧洲`、`美洲`、`大洋洲`、`中东`、`拉丁美洲`、`非洲`。
```bash
scanner-fly deals --from ICN --top 10
```
### 六个网站直达链接
```bash
scanner-fly links \
--from ICN \
--to NRT \
--depart 2026-09-15 \
--return 2026-09-19 \
--adults 1
```
默认行为是在终端输出:LuckyGlide 的 MyRealTrip 条件预订路径和 LuckyGlide 参考首页、CatchFrog 填好航线、日期、住宿天数的深链接,以及 Google Flights、Naver 机票、Skyscanner、Skiplagged 的链接。CatchFrog 不支持成人数的 URL 状态。想在默认浏览器里一次性打开所有链接,加上 `--open`。
```bash
scanner-fly links \
--from ICN \
--to NRT \
--depart 2026-09-15 \
--return 2026-09-19 \
--open
```
### JSON 输出
给 `search`、`deals`、`links` 加上 `--json`,就会输出结构化 JSON,而不是给人看的表格。
```bash
scanner-fly search \
--from ICN \
--to NRT \
--depart 2026-09-15 \
--return 2026-09-19 \
--json
scanner-fly deals --from ICN --top 5 --json
scanner-fly links \
--from ICN \
--to NRT \
--depart 2026-09-15 \
--return 2026-09-19 \
--json
```
输入的机场代码必须是三字母 IATA 格式,日期必须是 `YYYY-MM-DD` 格式。返程日期早于去程日期,或者出发地与目的地相同,请求会被拒绝。
## MCP 服务器
`mcp` 命令把同一个二进制作为使用 newline-delimited JSON-RPC 的 MCP stdio 服务器运行。
```bash
scanner-fly mcp
```
MCP 客户端需要自行启动这个进程,不是在终端手动挂一个服务器再注册地址。stdio 模式下整个 stdout 都用于 MCP 协议。
服务器恰好暴露五个工具。
| 工具 | 用途 | 主要输入 |
| --- | --- | --- |
| `search_flights` | 实时运价与网站链接的整合搜索 | `from`, `to`, `depart`, `return`, 可选 `adults` |
| `flight_deals` | LuckyGlide/CatchFrog 特价搜索 | 可选 `from`, `region`, `trip_days`, `days`, `top` |
| `flight_links` | 生成六个对比网站与 MyRealTrip 预订链接 | `from`, `to`, `depart`, `return`, 可选 `adults` |
| `multi_city_flights` | 生成多程行程 URL 与各供应商覆盖情况 | `legs: [{from,to,depart}, ...]`(至少 2 段), `adults`(1~9) |
| `compare_fares` | 对比 Google/Skiplagged 精确单程 Economy USD 搜索快照 | `from`, `to`, `start`, 可选 `days`(1~31), `adults`(1~9), `currency: USD` |
每个工具的结果都会同时返回人类可读的 JSON 文本和装着同一个对象的 `structuredContent`。
### Claude Code
先确认 release 二进制的绝对路径,然后注册为用户范围的 stdio 服务器。
```bash
claude mcp add \
--transport stdio \
--scope user \
scanner-fly \
-- /absolute/path/to/scanner-fly mcp
```
注册状态可以在 Claude Code 里用 `/mcp` 命令查看,也可以用下面的 CLI 命令确认。
```bash
claude mcp get scanner-fly
```
### Codex
```bash
codex mcp add scanner-fly -- /absolute/path/to/scanner-fly mcp
codex mcp get scanner-fly
```
### Senpi
项目级配置写在项目根目录的 `.senpi/mcp.json`。想在所有项目里使用,把同样的内容放到 `~/.senpi/agent/mcp.json`。
```json
{
"mcpServers": {
"scanner-fly": {
"type": "stdio",
"command": "/absolute/path/to/scanner-fly",
"args": ["mcp"]
}
}
}
```
重启 Senpi 后用 `/mcp status` 确认连接状态,更详细的状态和日志可以在 `/mcp` 面板里查看。
### OMP
项目级配置写在项目根目录的 `.omp/mcp.json`,配置 schema 与 Senpi 相同。
```json
{
"mcpServers": {
"scanner-fly": {
"type": "stdio",
"command": "/absolute/path/to/scanner-fly",
"args": ["mcp"]
}
}
}
```
重启 OMP 后用 `/mcp status` 或 `/mcp test scanner-fly` 确认连接。也可以在 OMP 里直接用下面的 slash command 注册。
```text
/mcp add scanner-fly -- /absolute/path/to/scanner-fly mcp
```
## 数据与运营限制
- 机票价格是请求时刻的快照。最终价格会因支付货币、行李、座位、人数、税费和销售方条款而不同,必须在链接里再次确认。
- LuckyGlide 请求会把目的地拆成小批次,降低上游超时概率。只有部分批次失败时,成功的结果和失败内容会一起显示。
- 各供应商独立查询。某个网站因为超时、响应格式变化、bot wall 或 rate limit 失败时,其他网站的成功结果不受影响。
- 部分对比网站会对自动 HTTP 请求返回 `403` 或 `429`,但普通浏览器可能照常打开。链接能否到达、实际能否预订,需要在浏览器里最终确认。
- `skiplagging.com` 会跳转到真实服务 `skiplagged.com`,所以生成的链接一律使用 `skiplagged.com`。
- CatchFrog 支持 `dep`、`dest_a`、`dates=YYYYMMDD-YYYYMMDD`、`nights` 这些 URL 状态,但不支持成人数的 URL 状态。是否暴跌以及折扣幅度以网站提供的数据为准,`scanner-fly` 不会自行推算历史价格。
- LuckyGlide 不读取航线、日期、成人数的 URL 状态。因此填好条件的主动作链接使用其底层 MyRealTrip 预订路径,LuckyGlide 首页则作为保留出处的参考链接一并输出。
- 多程结果中的 `coverage` 是 `all_legs`、`complete_legs`、`reference_only` 之一。`all_legs` 把完整顺序行程作为一次搜索传入。Skyscanner 和 Skiplagged 的 `complete_legs` 分别提供携带成人数的所有独立单程航段,但不是联程行程、通程运价或合计运价,scanner-fly 不会对其求和,也不会生成行程总价。LuckyGlide 和 CatchFrog 是 `reference_only`,CatchFrog 只提供 `https://catchfrog.ai/` 参考 URL,不表达多程行程。
- Skyscanner 的多程链接格式在带种子数据的真实 Chrome 会话中验证过,没有触发 PerimeterX CAPTCHA,但这不代表 Rust 二进制能绕过 bot wall。二进制只生成验证过的 URL,实际访问结果取决于供应商的浏览器安全策略。
## compare-fares:单程 USD 快照
`compare-fares` 针对精确的单程 Economy 请求(1 到 9 名成人)扫描 1 到 31 个连续出发日期,对比 Google Flights 和 Skiplagged 的 **USD 搜索快照**。`--currency` 默认且只接受 `USD`;开始日期早于本地当前日期、IATA 代码非法、出发到达机场相同,这些情况在发起网络请求之前就会被拒绝。
```bash
scanner-fly compare-fares --from YLW --to SFO --start 2026-09-12 --days 7 --adults 1 --currency USD --json
```
展示的金额只是 `search_snapshot` 观测值:不是含税价,不是最终价,也不是可预订总额,并且没有税费明细。一对观测只有在航线、日期、单程、Economy、成人数、USD 完全一致,且两次观测间隔不超过五分钟时才具备可比性。`delta_minor` 是 Google 减去 Skiplagged 的美分差值,所以正数表示 Skiplagged 更低。缺失、格式异常、被拦截、被限流、不支持、非 USD、无法匹配、hidden-city、virtual-interline 的数据都有明确的状态或原因,绝不会变成零价格或胜出方。
| 结果 | 含义 |
| --- | --- |
| `available` | 精确的 USD 供应商快照被接受。 |
| `eligible` | 两条可用观测的身份信息完全一致,且间隔在五分钟以内。 |
| `skiplagged_cheaper_case` | 某个起初更低的 Skiplagged 日期通过了向两家供应商发起的强制二次抓取。 |
| `no_eligible_cheaper_case_found` | 每个日期都可比,但没有一天 Skiplagged 更低。 |
| `scan_inconclusive` | 某个日期不可比、拦截或限流中止了扫描,或者复验推翻了候选结果。 |
Skiplagged 只接受标准的、非 hidden-city、非 virtual-interline,且实际最终到达机场就是所请求机场的行程。`403` 或 `429` 会中止后续日期的请求,报告中保留 `not_attempted` 行。每个起初更便宜的日期都会被重新抓取,价格变化或不再可用的候选会以 `superseded` 保留在结果里,而不是被悄悄降级。
MCP 目前恰好暴露五个工具:上面列出的四个,加上 `compare_fares`,`content[0].text` 和 `structuredContent` 中是同一份规范 JSON。
### 原生构建要求
G006 保留了一个面向旧版和 Google 流量的 Rustls `reqwest` 客户端,以及一个面向 Skiplagged 的独立 Chrome136 模拟 `wreq` 客户端。Skiplagged 请求只发送固定的 `currency=USD` cookie,不启用持久化 cookie store。项目 MSRV 是 Rust 1.85,锁定的依赖图用 Rust 1.85.1 完成解析和检查。macOS arm64 构建需要 Xcode Command Line Tools、CMake、Perl 和 libclang。Linux x86_64 GNU release 构建还需要:
```bash
sudo apt-get install -y build-essential cmake perl pkg-config libclang-dev binutils
```
只有 Linux 会启用 BoringSSL 符号前缀。发布产物仍然是单个可执行文件,不依赖浏览器、Python、curl-impersonate 或任何 sidecar 进程。wreq/BoringSSL 的相关声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
实现依据和各站点的调研记录见 [`docs/research.md`](docs/research.md)。