先说它是干什么的
Bot-hosting.net 是一个免费托管 Discord Bot 的平台。免费套餐有个限制——服务每 4 天就过期,必须手动点击 Renew 才能续期。
手动操作?不存在的。eooce 写了 Auto-Renew-Bothosting,一个运行在 GitHub Actions 上的 Python 脚本,自动完成续期全流程。这篇文章就是拆解它的实现原理。
整体架构:4 个模块
脚本的逻辑分为四个核心职责:
- Cookie 注入登录 — 用 session_token 跳过账号密码登录
- Turnstile 验证绕过 — 处理 Cloudflare 的机器人检测
- 续期操作 — 找到 Renew 按钮并点击
- Token 自动刷新 — 把新的 session_token 写回 GitHub Secrets
依赖工具链
1
2
| import os, re, sys, time, requests, subprocess
from seleniumbase import SB
|
核心依赖是 SeleniumBase(简称 SB)。它是对 Selenium 的封装,内置了 uc(undetected-chrome)模式,专门用于绕过反爬检测。脚本以 with SB(uc=True, headless=HEADLESS) as sb: 方式启动浏览器实例,整个流程都在这个上下文里跑。
第一步:环境变量读取
1
2
3
4
| SESSION_TOKEN = os.environ.get("SESSION_TOKEN") or ""
GH_TOKEN = os.environ.get("GH_TOKEN") or ""
TG_BOT_TOKEN = os.environ.get("TG_BOT_TOKEN") or ""
TG_CHAT_ID = os.environ.get("TG_CHAT_ID") or ""
|
所有敏感信息都从环境变量读取,配合 GitHub Repository Secrets 使用。脚本启动时就校验 SESSION_TOKEN,为空直接 sys.exit(1) 退出——干净利落,不做无意义的后续操作。
第二步:Cookie 注入,绕过登录表单
直接打开登录页走账号密码流程?风险太高——容易触发 2FA,也容易被 Cloudflare 拦截。聪明的做法是注入已有的 session_token cookie:
1
2
3
4
5
6
7
8
9
10
11
12
| COOKIES = {
"session_token": SESSION_TOKEN,
"login": "true",
"theme": "system",
}
sb.open("https://bot-hosting.net/")
sb.wait_for_ready_state_complete()
for name, value in COOKIES.items():
if value:
sb.add_cookie({"name": name, "value": value, "domain": "bot-hosting.net"})
|
流程是:先访问主页让浏览器建立与目标域的连接,然后通过 sb.add_cookie() 把 session 塞进去,再跳转到账单页 /a/billings 验证是否登录成功。
登录验证逻辑很直接——检查页面 title 是否符合预期:
1
2
3
4
| if current_title != "Bot-Hosting.net | A Free Host For Discord Bots":
print("❌ 登录失败,Cookie 已失效")
send_telegram_message(...)
return
|
title 不对就说明 Cookie 失效了,直接退出并推送 TG 通知。
第三步:提取到期日期
登录成功后,脚本用正则从页面 HTML 源码里抓到期日期:
1
2
3
4
5
6
| patterns = [
r"[Ee]xpires\s*[:\-]?\s*(\d{4}/\d{2}/\d{2})", # Expires 2026/07/07
r"[Ee]xpires\s*[:\-]?\s*(\d{2}/\d{2}/\d{4})", # Expires 07/07/2026
r"(\d{4}/\d{2}/\d{2})\s*[\-–]\s*renew", # 2026/07/07 - renew
r"(\d{2}/\d{2}/\d{4})\s*[\-–]\s*renew", # 07/07/2026 - renew
]
|
同时还处理了 MM/DD/YYYY 和 YYYY/MM/DD 两种日期格式的转换,兼容性不错。
第四步:识别续期按钮状态
脚本需要判断当前是「可以续期」还是「还在倒计时」:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| possible_selectors = [
'button:contains("Renew")',
'a:contains("Renew")',
'[class*="renew"]',
'[class*="Renew"]',
]
for selector in possible_selectors:
if sb.is_element_visible(selector):
button_text = sb.get_text(selector)
if "Renew in" in button_text:
# 在倒计时,提取时间
countdown_text = re.search(r"Renew in (\d{2}:\d{2}:\d{2})", button_text).group(1)
else:
# 可以续期
outer_renew_selector = selector
|
关键区分点:按钮文字是 "Renew" 还是 "Renew in HH:MM:SS"。前者可以点,后者表示还没到时间。
第五步:Turnstile 验证绕过(最硬核的部分)
点击 Renew 后,页面会弹出一个含 Cloudflare Turnstile 的 modal。Turnstile 是比普通 reCAPTCHA 更难绕的验证机制,专门针对自动化脚本设计。
SeleniumBase 的 uc_gui_click_captcha() 是关键武器:
1
2
3
4
5
6
7
8
9
10
11
12
| for attempt in range(1, 4):
try:
sb.uc_gui_click_captcha()
time.sleep(8)
except Exception as e:
print(f"⚠️ 点击 Turnstile 出错: {e}")
if wait_for_turnstile_pass(sb, timeout=20):
turnstile_passed = True
break
else:
print(f"⏳ 第 {attempt} 次未通过,重试...")
|
验证通过的判断逻辑是检测页面中是否还存在 Turnstile 特征字符串:
1
2
3
4
| cf_indicators = ["verify you are human", "确认您是真人", "troubleshoot", "just a moment"]
page_lower = sb.get_page_source().lower()
if not any(x in page_lower for x in cf_indicators):
return True # 验证已通过
|
最多重试 3 次,全部失败则推送 TG 通知并退出。
第六步:点击续期 + 结果验证
Turnstile 过了之后,点击 modal 里的续期按钮:
1
| sb.click('button:contains("Renew for 4 days")', timeout=8)
|
然后等待 3 秒,再次抓取页面源码,用正则检测续期结果:
1
2
3
4
| new_match = re.search(r"Renew in (\d{2}:\d{2}:\d{2})", new_page_text)
if new_match:
# 出现倒计时 = 续期成功,发通知
send_telegram_message(format_notification("✅ 续期成功", ...))
|
逻辑判断链:续期后按钮变成 Renew in XX:XX:XX 就是成功;到期日期有变化也算成功;其他情况推送「请手动检查」警告。
第七步:SESSION_TOKEN 自动刷新
这是整个脚本最有价值的自动化设计之一。浏览器跑完整个流程后,session_token 可能已经刷新了(平台服务端轮转 Token 是常见安全实践)。
脚本会主动检测并回写:
1
2
3
4
5
6
7
8
9
10
| new_token, token_expiry = get_cookie_info(sb, "session_token")
def should_update_cookie(new_value, old_value, expiry_dt, days_threshold=3):
if new_value != old_value: # 值变了,必须更新
return True
if expiry_dt:
remaining = (expiry_dt - datetime.now()).total_seconds()
if remaining < days_threshold * 24 * 3600: # 3天内过期,提前更新
return True
return False
|
如果需要更新,调用 gh secret set 命令写回 GitHub Secrets:
1
2
3
4
| proc = subprocess.run(
["gh", "secret", "set", "SESSION_TOKEN", "--body", new_value],
capture_output=True, text=True, timeout=30, env=env
)
|
这意味着脚本是自维护的——只要 GH_TOKEN 有效,SESSION_TOKEN 过期了也能自动续上,形成闭环。
通知格式
每次执行结果都会格式化成一条 TG 消息,包含:账户(脱敏)、执行状态、到期时间、执行时间(上海时区)。
1
2
3
4
5
6
| 🇫🇮 Bot-hosting 续期通知
✅ 续期成功
👤 登录账户: zh****bi@163.com
📅 到期日期: 2026/07/09
⏱️ 登录时间: 2026-07-05 16:40:24
|
时区处理用了 time.gmtime(time.time() + 8 * 3600) 这种手动加 8 小时的方式——GitHub Actions Runner 跑在 UTC,不用这个就会显示成凌晨。
执行流程总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 启动浏览器(undetected-chrome 模式)
↓
注入 session_token Cookie
↓
跳转账单页 → 验证登录状态
↓
提取当前到期日期
↓
检测 Renew 按钮状态
├── 倒计时中 → 推送「未到续期时间」通知 → 退出
└── 可续期 → 点击按钮
↓
弹出 Turnstile 验证(最多重试 3 次)
↓
点击「Renew for 4 days」
↓
验证结果 → 推送通知
↓
检查 SESSION_TOKEN 是否需要更新
↓
如需更新 → gh secret set → 写回 GitHub Secrets
|
几个值得注意的细节
1. uc=True 是关键
普通 Selenium 早就被各大反爬系统列入黑名单。SB 的 uc 模式通过修改 Chrome 的 CDP 特征来规避检测,是跑这类自动化脚本的标配。
2. Cookie 注入 vs 账号密码登录
直接注入 session_token 比走登录流程风险低很多。登录流程容易触发 2FA、图形验证码、设备风控——而 Cookie 本身就是「已通过验证的凭证」。
3. 3 次 Turnstile 重试机制
Turnstile 不是每次都能一次过,加上 sleep(8) 等待页面响应,3 次重试覆盖了大多数情况。
4. 自维护的 Token 回写
这个设计让整个脚本真正做到了无人值守。部署一次,只要 GH_TOKEN 不失效,理论上可以无限期自动续期。
这个脚本结构清晰,错误处理完善,通知机制贴心。如果你有类似「需要定期点击某个按钮才能保活」的服务,这套 SeleniumBase + GitHub Actions + Secrets 自动回写的模式值得直接复用。
本文由 BOSH 的博客助手 HerMes 整理 🤖
原文链接:https://github.com/eooce/Auto-Renew-Bothosting/blob/main/app.py