Skip to content

Commit 140ac47

Browse files
prepare github open-source release
1 parent 2d64591 commit 140ac47

44 files changed

Lines changed: 4416 additions & 354 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
APP_DB_PATH=data/app.db
22
PLAYWRIGHT_PROFILE_DIR=data/playwright-profile
33
PLAYWRIGHT_BROWSERS_PATH=runtime/ms-playwright
4-
CTRIP_SNAPSHOT_DIR=tests/fixtures
4+
CTRIP_SNAPSHOT_DIR=data/debug
5+
CTRIP_SAVE_DEBUG_SNAPSHOT=0
6+
CTRIP_DEBUG_SNAPSHOT_DIR=data/debug
57
CTRIP_SEARCH_URL_TEMPLATE=https://flights.ctrip.com/online/list/oneway-{origin}-{destination}?depdate={departure_date}&cabin=y_s_c_f&adult=1&child=0&infant=0&containstax=1
68
CTRIP_SESSION_URL=https://flights.ctrip.com/online/channel/domestic
79
APP_BASE_URL=http://127.0.0.1:8000
10+
MONITOR_REALERT_COOLDOWN_ENABLED=1
11+
MONITOR_REALERT_COOLDOWN_HOURS=6
12+
MONITOR_FAILURE_BACKOFF_MINUTES=5
13+
CTRIP_AUTO_RELOGIN_COOLDOWN_MINUTES=30

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
release-hygiene:
9+
runs-on: windows-latest
10+
11+
steps:
12+
- name: Check out repository
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.12"
19+
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
python -m pip install -r requirements-dev.txt
24+
25+
- name: Check installed packages
26+
run: python -m pip check
27+
28+
- name: Compile Python sources
29+
run: python -m compileall -q app scripts
30+
31+
- name: Run tests
32+
run: python -m pytest -q

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ __pycache__/
66
*.pyo
77
*.db
88
*.sqlite3
9-
/data/playwright-profile/
10-
/data/*.html
9+
/data/*
10+
!/data/.gitkeep
1111
/tests/fixtures/last_live_search.html
1212
.superpowers/
1313
.worktrees/
14+
.hermes/
1415
dist/
16+
/runtime/
17+
*.zip
18+
*.tmp

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 tangtaizhong
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# 飞票监控
1+
# 票价哨兵
22

3-
飞票监控是一个本地运行的机票监控工作台。它可以搜索携程国内单程机票,保存目标价监控任务,并在价格达到目标时通过本机提醒、页面高亮和声音提示把你叫回来。
3+
票价哨兵是一个本地运行的机票监控工作台。它可以搜索携程国内单程机票,保存目标价监控任务,并在价格达到目标时通过本机提醒、页面高亮和声音提示把你叫回来。
44

55
它适合这些场景:
66

@@ -11,15 +11,23 @@
1111

1212
## 普通用户路径
1313

14-
从 GitHub Release 下载:
14+
你可以直接从 GitHub 项目页下载源码:
15+
16+
1. 点击 `Code → Download ZIP`
17+
2. 解压下载到的 zip。
18+
3. 双击根目录里的 `start_fly_ticket.bat`
19+
20+
首次运行会联网下载内置 Python、安装运行依赖和 Playwright Chromium,不需要提前安装 Python。之后再次双击会复用本地 `runtime/``.env`,启动会更快。
21+
22+
也可以从 GitHub Release 下载已经打包好的便携版:
1523

1624
`FlyTicket-Windows-<version>.zip`
1725

1826
解压后双击:
1927

2028
`启动机票监控.bat`
2129

22-
发布包自带 Python 运行环境、依赖和 Playwright Chromium。普通用户不需要手动安装 Python、pip 依赖或浏览器组件。
30+
Release 发布包自带 Python 运行环境、依赖和 Playwright Chromium。普通用户不需要手动安装 Python、pip 依赖或浏览器组件。压缩包体积较大是因为内置了这些运行环境
2331

2432
启动后按这个顺序使用:
2533

@@ -80,13 +88,22 @@
8088
- 如果价格继续下降,会再次提醒
8189
- 如果价格一直命中但没有更低,会在冷却时间后再次提醒
8290

83-
默认冷却时间是 6 小时,可通过 `.env` 调整:
91+
默认开启重复提醒冷却,冷却时间是 6 小时,可通过 `.env` 调整:
8492

8593
```env
94+
MONITOR_REALERT_COOLDOWN_ENABLED=1
8695
MONITOR_REALERT_COOLDOWN_HOURS=6
8796
```
8897

89-
即使桌面通知没有弹出,命中记录仍会写入本地数据库,你可以在网页里查看。
98+
如果希望每次检查到目标价都提醒,把冷却开关关掉:
99+
100+
```env
101+
MONITOR_REALERT_COOLDOWN_ENABLED=0
102+
```
103+
104+
关闭后,重复提醒频率会跟监控任务的检查间隔一致。例如任务每 30 分钟检查一次,只要每次检查仍命中目标价,就会约每 30 分钟提醒一次。
105+
106+
即使桌面通知没有弹出,命中记录仍会写入本地数据库,你可以在网页里查看。任务栏提醒依赖系统通知能力,当前以页面高亮、桌面通知和声音提示为主。
90107

91108
## 开发者路径
92109

@@ -96,19 +113,28 @@ MONITOR_REALERT_COOLDOWN_HOURS=6
96113

97114
它会自动尝试:
98115

99-
1. 检查 Python
100-
2. 创建 `.venv`
101-
3. 安装依赖
102-
4. 安装 Playwright Chromium
103-
5.`.env.example` 生成 `.env`
104-
6. 启动本地网页服务
105-
7. 自动打开浏览器
116+
1. 使用已有 Python 3.10+ 创建 `.venv`,或在没有 Python 时下载内置 Python
117+
2. 安装运行依赖
118+
3. 安装 Playwright Chromium
119+
4.`.env.example` 生成 `.env`
120+
5. 启动本地网页服务
121+
6. 自动打开浏览器
106122

107-
手动运行方式:
123+
手动运行方式(Windows PowerShell):
124+
125+
```powershell
126+
py -3 -m venv .venv
127+
.\.venv\Scripts\python.exe -m pip install -r requirements-dev.txt
128+
.\.venv\Scripts\python.exe -m playwright install chromium
129+
Copy-Item .env.example .env
130+
.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000
131+
```
132+
133+
手动运行方式(macOS / Linux):
108134

109135
```bash
110136
python -m venv .venv
111-
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
137+
.venv/bin/python -m pip install -r requirements-dev.txt
112138
.venv/bin/python -m playwright install chromium
113139
cp .env.example .env
114140
.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
@@ -133,6 +159,8 @@ powershell -ExecutionPolicy Bypass -File scripts/build_windows_portable.ps1 -Ver
133159
- `dist/FlyTicket-Windows/`
134160
- `dist/FlyTicket-Windows-dev.zip`
135161

162+
发布 GitHub Release 时,建议同时在 Release 说明里提供压缩包 SHA256,方便用户确认文件没有损坏或被替换。
163+
136164
## 配置说明
137165

138166
项目读取根目录下的 `.env`
@@ -143,10 +171,13 @@ powershell -ExecutionPolicy Bypass -File scripts/build_windows_portable.ps1 -Ver
143171
APP_DB_PATH=data/app.db
144172
PLAYWRIGHT_PROFILE_DIR=data/playwright-profile
145173
PLAYWRIGHT_BROWSERS_PATH=runtime/ms-playwright
146-
CTRIP_SNAPSHOT_DIR=tests/fixtures
174+
CTRIP_SNAPSHOT_DIR=data/debug
175+
CTRIP_SAVE_DEBUG_SNAPSHOT=0
176+
CTRIP_DEBUG_SNAPSHOT_DIR=data/debug
147177
CTRIP_SEARCH_URL_TEMPLATE=
148178
CTRIP_SESSION_URL=
149179
APP_BASE_URL=http://127.0.0.1:8000
180+
MONITOR_REALERT_COOLDOWN_ENABLED=1
150181
MONITOR_REALERT_COOLDOWN_HOURS=6
151182
```
152183

@@ -167,6 +198,45 @@ CTRIP_SESSION_URL=https://flights.ctrip.com/online/channel/domestic
167198

168199
升级新版本时,保留 `data/``.env` 即可继续使用原来的数据。
169200

201+
## 隐私和本地数据
202+
203+
本项目默认只在本机保存数据,不会把你的搜索历史、监控任务或携程登录状态上传到项目作者的服务器。
204+
205+
但是请注意:
206+
207+
- `data/playwright-profile/` 可能包含携程登录态、Cookie、浏览历史、Local Storage 等敏感信息。
208+
- `data/app.db` 可能包含你的搜索历史、监控任务、目标价和命中记录。
209+
- `.env` 可能包含你的本地配置。
210+
- 如果开启 `CTRIP_SAVE_DEBUG_SNAPSHOT=1``data/debug/` 里可能保存携程页面 HTML,里面可能包含个人化内容。
211+
212+
不要把这些文件上传到 GitHub、网盘、论坛、Issue 或发给别人:
213+
214+
```text
215+
.env
216+
data/
217+
runtime/
218+
dist/
219+
.venv/
220+
*.log
221+
*.err.log
222+
```
223+
224+
## 安全边界
225+
226+
票价哨兵是本地工具,默认只监听 `127.0.0.1`
227+
228+
请不要把服务改成 `0.0.0.0` 后暴露到公网或局域网。如果你确实需要远程访问,请自行增加登录认证、防火墙和网络安全措施。
229+
230+
## 免责声明
231+
232+
本项目仅供个人本地学习和自用。
233+
234+
- 不提供自动购票、抢票、下单功能。
235+
- 不提供绕过验证码、绕过风控或批量抓取能力。
236+
- 用户应自行遵守相关网站的服务条款以及所在地法律法规。
237+
- 携程页面结构、登录策略或风控策略变化时,本工具可能无法正常搜索。
238+
- 使用本项目产生的账号、网络、数据或法律风险由使用者自行承担。
239+
170240
## 问题排查
171241

172242
### 双击后页面没打开
@@ -194,11 +264,15 @@ CTRIP_SESSION_URL=https://flights.ctrip.com/online/channel/domestic
194264

195265
不同系统环境的桌面通知后端表现不完全一致。即使通知没弹出,命中记录仍会保存,页面里也会显示最近命中。
196266

267+
### Windows 或杀毒软件提示风险
268+
269+
请确认压缩包来自你信任的 GitHub Release,并优先核对 Release 页面提供的 SHA256。便携版内置 Python 和浏览器运行环境,部分安全软件可能对未知压缩包或脚本启动器给出提示。
270+
197271
## 目录说明
198272

199273
- `app/`:后端代码、模板、静态资源
200274
- `tests/`:自动化测试
201-
- `data/`:本地数据库和浏览器登录状态
275+
- `data/`:本地数据库和浏览器登录状态,默认不应提交
202276
- `scripts/`:启动和构建脚本
203277
- `docs/superpowers/`:设计和实现计划文档
204278

@@ -209,3 +283,7 @@ CTRIP_SESSION_URL=https://flights.ctrip.com/online/channel/domestic
209283
- 不做自动购票或自动下单
210284
- 不接入邮件、微信、Telegram 等外部通知
211285
- 不做云端长期运行;程序关闭后不会继续监控
286+
287+
## 许可证
288+
289+
本项目使用 MIT License,详见 `LICENSE`

README_使用说明.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@
2323
本地数据库、监控任务和登录状态保存在 data 文件夹。
2424
升级新版本时,保留旧版本的 data 文件夹和 .env 文件即可继续使用原来的数据。
2525

26+
请注意:data 文件夹可能包含你的携程登录状态、Cookie、浏览历史、搜索历史和监控任务,不要把 data 文件夹、.env 文件或日志文件发给别人,也不要上传到 GitHub、网盘、论坛或 Issue。
27+
2628
五、常见问题
27-
如果 Windows 或杀毒软件提示风险,请确认压缩包来自你信任的 GitHub Release。
29+
如果 Windows 或杀毒软件提示风险,请确认压缩包来自你信任的 GitHub Release,并优先核对 Release 页面提供的 SHA256
2830
如果页面打不开,可能是端口被占用,重新双击启动脚本通常会自动换端口。
2931
如果搜索失败,请重新登录携程,或检查网络连接。
3032
如果登录失效,请在首页点击“重新登录”。
3133
如果没有看到桌面通知,请回到网页查看最近命中记录。
34+
35+
六、使用边界
36+
本工具仅供个人本地学习和自用。
37+
它不提供自动购票、抢票、下单、绕过验证码或绕过风控功能。
38+
请自行遵守相关网站服务条款和所在地法律法规。

app/ctrip_capture.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def save_live_snapshot(
1010
html: str,
1111
filename: str = "last_live_search.html",
1212
) -> Path:
13-
settings.ctrip_snapshot_dir.mkdir(parents=True, exist_ok=True)
14-
path = settings.ctrip_snapshot_dir / filename
13+
settings.ctrip_debug_snapshot_dir.mkdir(parents=True, exist_ok=True)
14+
path = settings.ctrip_debug_snapshot_dir / filename
1515
path.write_text(html, encoding="utf-8")
1616
return path

app/ctrip_parser.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
EMBEDDED_FLIGHT_NO_RE = re.compile(r"([A-Z]{2}\d{3,4})(?=[^A-Z0-9]|$)")
2020
TIME_RE = re.compile(r"\b(\d{2}:\d{2})\b")
2121
PRICE_RE = re.compile(r"[¥¥]\s*(\d+)")
22+
PRICE_WITH_START_RE = re.compile(r"[¥¥]\s*(\d+)\s*起")
2223
AIRLINE_FALLBACK_RE = re.compile(r"([一-鿿]{2,}(?:航空|航司))")
2324
DIRECT_WORDS = ("直飞", "无中转")
2425
STOP_WORDS = ("经停", "中转")
@@ -68,15 +69,15 @@ def _parse_card(
6869
price = _extract_price(card, text)
6970
airline = _extract_airline(card, text)
7071

71-
if not flight_no or len(times) < 2 or price is None or not airline:
72+
if len(times) < 2 or price is None or not airline:
7273
return None
7374

7475
href = _extract_deeplink(card, fallback_search_url)
7576
stop_info = _extract_stop_info(card, text)
7677
is_direct = stop_info == "直飞"
7778

7879
return FlightResult(
79-
flight_no=flight_no,
80+
flight_no=flight_no or "未知航班",
8081
airline=airline,
8182
origin_city=request.origin_city,
8283
destination_city=request.destination_city,
@@ -103,14 +104,32 @@ def _extract_flight_no(card: Tag, text: str) -> str | None:
103104

104105

105106
def _extract_price(card: Tag, text: str) -> int | None:
106-
match = PRICE_RE.search(text)
107+
price_node = card.select_one(
108+
".price.over-size .price, .flight-price .price, "
109+
"[class*='price'] [class='price'], [class*='price']"
110+
)
111+
if price_node is None:
112+
start_match = PRICE_WITH_START_RE.search(text)
113+
if start_match:
114+
return int(start_match.group(1))
115+
116+
for match in PRICE_RE.finditer(text):
117+
prefix = text[max(0, match.start() - 6) : match.start()]
118+
if "已减" in prefix or "优惠" in prefix:
119+
continue
120+
return int(match.group(1))
121+
return None
122+
123+
price_text = price_node.get_text(" ", strip=True)
124+
match = PRICE_RE.search(price_text)
107125
if match:
108126
return int(match.group(1))
109127

110-
price_node = card.select_one(".price.over-size .price, .flight-price .price")
111-
if price_node is None:
112-
return None
113-
digits = "".join(ch for ch in price_node.get_text(strip=True) if ch.isdigit())
128+
start_match = PRICE_WITH_START_RE.search(price_text)
129+
if start_match:
130+
return int(start_match.group(1))
131+
132+
digits = "".join(ch for ch in price_text if ch.isdigit())
114133
return int(digits) if digits else None
115134

116135

0 commit comments

Comments
 (0)