├── .github
└── workflows
│ ├── arcticcloud-renew.yml
│ ├── clochat-checkin.yml
│ ├── nodeloc-checkin.yml
│ ├── nodeseek-checkin.yml
│ └── sfsy-checkin.yml
├── README.md
├── arcticcloud.py
├── bincloud.py
├── clochat.py
├── nodeloc.py
├── nodeseek.py
├── notify.py
├── requirements.txt
└── sfsy.py
/.github/workflows/arcticcloud-renew.yml:
--------------------------------------------------------------------------------
1 | name: ArcticCloud VPS 自动续期
2 |
3 | on:
4 | schedule:
5 | - cron: '0 12 * * *' # 每天中午12点运行
6 | workflow_dispatch:
7 |
8 | jobs:
9 | renew_vps:
10 | name: 执行 vps 续期
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: 🛎️ 检出代码
15 | uses: actions/checkout@v4
16 |
17 | - name: 🐍 设置 Python
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.11'
21 |
22 | - name: ⬇️ 安装依赖
23 | run: |
24 | pip install -r requirements.txt
25 |
26 | - name: 🚀 运行签到脚本
27 | env:
28 | ARCTIC_USERNAME: ${{ secrets.ARCTIC_USERNAME }}
29 | ARCTIC_PASSWORD: ${{ secrets.ARCTIC_PASSWORD }}
30 | HEADLESS: ${{ secrets.HEADLESS || 'true' }} # 可选:是否无头
31 | ARCTIC_LOG_LEVEL: ${{ secrets.ARCTIC_LOG_LEVEL || 'INFO' }}
32 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }}
33 | PUSH_PLUS_USER: ${{ secrets.PUSH_PLUS_USER }}
34 | DD_BOT_SECRET: ${{ secrets.DD_BOT_SECRET }}
35 | DD_BOT_TOKEN: ${{ secrets.DD_BOT_TOKEN }}
36 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
37 | TG_USER_ID: ${{ secrets.TG_USER_ID }}
38 | WXPUSHER_APP_TOKEN: ${{ secrets.WXPUSHER_APP_TOKEN }}
39 |
40 | run: |
41 | python arcticcloud.py
42 |
--------------------------------------------------------------------------------
/.github/workflows/clochat-checkin.yml:
--------------------------------------------------------------------------------
1 | name: CloChat 自动签到
2 |
3 | on:
4 | schedule:
5 | - cron: '10 10 * * *' # 每天 10:10 UTC(北京时间 18:10)
6 | workflow_dispatch: # 支持手动运行
7 |
8 | jobs:
9 | clochat-checkin:
10 | name: 执行 CloChat 签到
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: 🛎️ 检出代码
15 | uses: actions/checkout@v4
16 |
17 | - name: 🐍 设置 Python
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.11'
21 |
22 | - name: ⬇️ 安装依赖
23 | run: |
24 | pip install -r requirements.txt
25 |
26 | - name: 🚀 运行签到脚本
27 | env:
28 | CLOCHAT_USERNAME: ${{ secrets.CLOCHAT_USERNAME }}
29 | CLOCHAT_PASSWORD: ${{ secrets.CLOCHAT_PASSWORD }}
30 | HEADLESS: ${{ secrets.HEADLESS || 'true' }} # 可选:是否无头
31 | CLOCHAT_LOG_LEVEL: ${{ secrets.CLOCHAT_LOG_LEVEL || 'INFO' }}
32 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }}
33 | PUSH_PLUS_USER: ${{ secrets.PUSH_PLUS_USER }}
34 | DD_BOT_SECRET: ${{ secrets.DD_BOT_SECRET }}
35 | DD_BOT_TOKEN: ${{ secrets.DD_BOT_TOKEN }}
36 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
37 | TG_USER_ID: ${{ secrets.TG_USER_ID }}
38 | WXPUSHER_APP_TOKEN: ${{ secrets.WXPUSHER_APP_TOKEN }}
39 |
40 | run: |
41 | python clochat.py
42 |
--------------------------------------------------------------------------------
/.github/workflows/nodeloc-checkin.yml:
--------------------------------------------------------------------------------
1 | name: NodeLoc 自动签到
2 |
3 | on:
4 | schedule:
5 | # 每天 08:00 UTC 运行(即北京时间 16:00)
6 | - cron: '0 8 * * *'
7 | workflow_dispatch: # 允许手动触发
8 | inputs:
9 | debug:
10 | description: '是否开启调试模式'
11 | required: false
12 | default: 'false'
13 |
14 | jobs:
15 | checkin:
16 | name: 执行签到
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: 🛎️ 检出代码
21 | uses: actions/checkout@v4
22 |
23 | - name: 🐍 设置 Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: '3.11'
27 |
28 | - name: ⬇️ 安装依赖
29 | run: |
30 | python -m pip install --upgrade pip
31 | # 如果有 requirements.txt 可以启用下面这行
32 | pip install -r requirements.txt
33 |
34 | - name: 🏃 运行签到脚本
35 | env:
36 | NL_COOKIE: ${{ secrets.NL_COOKIE }}
37 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }}
38 | PUSH_PLUS_USER: ${{ secrets.PUSH_PLUS_USER }}
39 | DD_BOT_SECRET: ${{ secrets.DD_BOT_SECRET }}
40 | DD_BOT_TOKEN: ${{ secrets.DD_BOT_TOKEN }}
41 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
42 | TG_USER_ID: ${{ secrets.TG_USER_ID }}
43 | WXPUSHER_APP_TOKEN: ${{ secrets.WXPUSHER_APP_TOKEN }}
44 | run: |
45 | python nodeloc.py
46 |
47 | - name: 📝 输出完成日志
48 | run: |
49 | echo "✅ 签到任务已完成,查看上方日志获取结果。"
50 |
--------------------------------------------------------------------------------
/.github/workflows/nodeseek-checkin.yml:
--------------------------------------------------------------------------------
1 | name: NodeSeek 自动签到
2 |
3 | on:
4 | schedule:
5 | - cron: '30 8 * * *' # 北京时间 16:30(UTC 8:30)
6 | workflow_dispatch: # 支持手动触发
7 |
8 | jobs:
9 | nodeseek-checkin:
10 | name: 执行 NodeSeek 签到
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: 🛎️ 检出代码
15 | uses: actions/checkout@v4
16 |
17 | - name: 🐍 设置 Python
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.11'
21 |
22 | - name: ⬇️ 安装依赖
23 | run: |
24 | pip install -r requirements.txt
25 |
26 | - name: 🚀 运行签到脚本
27 | env:
28 | NS_COOKIE: ${{ secrets.NS_COOKIE }}
29 | NS_SIGN_MODE: ${{ secrets.NS_SIGN_MODE || 'chicken' }}
30 | NS_HEADLESS: ${{ secrets.NS_HEADLESS || 'true' }}
31 | NS_LOG_LEVEL: ${{ secrets.NS_LOG_LEVEL || 'INFO' }}
32 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }}
33 | PUSH_PLUS_USER: ${{ secrets.PUSH_PLUS_USER }}
34 | DD_BOT_SECRET: ${{ secrets.DD_BOT_SECRET }}
35 | DD_BOT_TOKEN: ${{ secrets.DD_BOT_TOKEN }}
36 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
37 | TG_USER_ID: ${{ secrets.TG_USER_ID }}
38 | WXPUSHER_APP_TOKEN: ${{ secrets.WXPUSHER_APP_TOKEN }}
39 | run: |
40 | python nodeseek.py
41 |
--------------------------------------------------------------------------------
/.github/workflows/sfsy-checkin.yml:
--------------------------------------------------------------------------------
1 | name: 顺丰速运自动签到
2 |
3 | on:
4 | schedule:
5 | - cron: '15 8,16 * * *'
6 | workflow_dispatch:
7 |
8 | jobs:
9 | renew_vps:
10 | name: 执行自动签到
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: 🛎️ 检出代码
15 | uses: actions/checkout@v4
16 |
17 | - name: 🐍 设置 Python
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.11'
21 |
22 | - name: ⬇️ 安装依赖
23 | run: |
24 | pip install -r requirements.txt
25 |
26 | - name: 🚀 运行签到脚本
27 | env:
28 | sfsyUrl: ${{ secrets.sfsyUrl }}
29 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }}
30 | PUSH_PLUS_USER: ${{ secrets.PUSH_PLUS_USER }}
31 | DD_BOT_SECRET: ${{ secrets.DD_BOT_SECRET }}
32 | DD_BOT_TOKEN: ${{ secrets.DD_BOT_TOKEN }}
33 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
34 | TG_USER_ID: ${{ secrets.TG_USER_ID }}
35 | WXPUSHER_APP_TOKEN: ${{ secrets.WXPUSHER_APP_TOKEN }}
36 |
37 | run: |
38 | python sfsy.py
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 自用签到脚本
2 |
3 | 自用脚本,请勿传播。适用于多种 app 与论坛,解放双手自动化。适配青龙面板。适配青龙notify通知。已适配GitHub Actions。
4 | selenium 自动化 python 脚本必须先安装 chromium 及 chromium-chromedriver。
5 |
6 | ## CloChat
7 |
8 | 适用于 CloChat 论坛,包含签到功能。
9 |
10 | ### 环境变量
11 | - `CLOCHAT_USERNAME`: CloChat 的用户名(必需)
12 | - `CLOCHAT_PASSWORD`: CloChat 的密码(必需)
13 |
14 | ## Nodeloc
15 |
16 | 适用于 Nodeloc 论坛,包含签到功能。
17 | ~~增加任务脚本,已实现自动点击话题及点赞功能。~~
18 | 为了论坛健康发展,已删除部分功能。
19 |
20 | ### 环境变量
21 | - `NL_COOKIE`: NodeLoc 的 Cookie(必需)
22 |
23 | ## NodeSeek
24 |
25 | 适用于 NodeSeek 论坛,包含签到~~评论和加鸡腿~~功能。
26 | ~~强烈建议修改随机词,慎用ai评论功能。否则容易被举报被禁言~~。
27 | 为了论坛健康发展,已删除部分功能。
28 |
29 | ### 环境变量
30 |
31 | - `NS_COOKIE`: NodeSeek 的 Cookie(必需)
32 | - `NS_RANDOM`: 是否随机选择奖励,true/false(可选)
33 | - `HEADLESS`: 是否使用无头模式,true/false(可选,默认 true)
34 |
35 | ## ArcticCloud
36 |
37 | 适用于 ArcticCloud,包含自动续期功能。
38 |
39 | ### 环境变量
40 | - `ARCTIC_USERNAME`: ARCTIC 的用户名(必需)
41 | - `ARCTIC_PASSWORD`: ARCTIC 的密码(必需)
42 |
43 | ## BinCloud
44 |
45 | 适用于 56IDC,包含登陆保号、检测vps状态、停机自动启动功能。
46 |
47 | ### 环境变量
48 | - `BC_COOKIES`: 56IDC 的Cookie(必需)
49 |
50 | ## sfsy
51 |
52 | 适用于顺丰app,包含签到、抽奖、蜂蜜任务等功能。
53 |
54 | ### 环境变量
55 | - `sfsyUrl`: 顺丰app捉包提取请求头中的url
56 |
--------------------------------------------------------------------------------
/arcticcloud.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | # -------------------------------
3 | # @Author : github@wh1te3zzz https://github.com/wh1te3zzz/checkin
4 | # @Time : 2025-07-18 16:03:22
5 | # ArcticCloud续期脚本
6 | # -------------------------------
7 | """
8 | ArcticCloud 免费vps自动续期
9 | 变量为账号密码,暂不支持多账户
10 | export ARCTIC_USERNAME = "ARCTIC账号"
11 | export ARCTIC_PASSWORD = "ARCTIC密码"
12 |
13 | cron: 0 12 * * *
14 | const $ = new Env("ArcticCloud续期");
15 | """
16 | import os
17 | import time
18 | import logging
19 | import traceback
20 | from notify import send
21 | from selenium import webdriver
22 | from selenium.webdriver.common.by import By
23 | from selenium.webdriver.support.ui import WebDriverWait
24 | from selenium.webdriver.support import expected_conditions as EC
25 | from selenium.webdriver.chrome.options import Options
26 | from selenium.webdriver.chrome.service import Service
27 | from selenium.common.exceptions import TimeoutException, NoSuchElementException
28 |
29 | # =================== 配置开关 ===================
30 | WAIT_TIMEOUT = 60 # 等待超时时间
31 | ENABLE_SCREENSHOT = False # 是否开启截图功能
32 | HEADLESS = os.environ.get("HEADLESS", "true").lower() == "true" # 配置无头模式
33 | LOG_LEVEL = os.environ.get("ARCTIC_LOG_LEVEL", "INFO").upper() # 配置日志级别
34 | # =================================================
35 |
36 | # 环境变量
37 | USERNAME = os.environ.get("ARCTIC_USERNAME")
38 | PASSWORD = os.environ.get("ARCTIC_PASSWORD")
39 |
40 | # 页面地址
41 | LOGIN_URL = "https://vps.polarbear.nyc.mn/index/login/?referer="
42 | CONTROL_INDEX_URL = "https://vps.polarbear.nyc.mn/control/index/"
43 |
44 | # 截图目录
45 | SCREENSHOT_DIR = "/ql/data/photo"
46 | os.makedirs(SCREENSHOT_DIR, exist_ok=True)
47 |
48 | # 设置日志输出等级
49 | numeric_level = getattr(logging, LOG_LEVEL, logging.INFO)
50 | logging.basicConfig(
51 | level=numeric_level,
52 | format='%(asctime)s - %(levelname)s - %(message)s',
53 | handlers=[
54 | logging.StreamHandler()
55 | ]
56 | )
57 |
58 | def take_screenshot(driver, filename="error.png"):
59 | """截图保存到指定目录"""
60 | if not ENABLE_SCREENSHOT:
61 | return
62 | path = os.path.join(SCREENSHOT_DIR, filename)
63 | driver.save_screenshot(path)
64 | logging.debug("📸 已保存报错截图至: %s", path)
65 |
66 | def setup_driver():
67 | """初始化浏览器"""
68 | options = Options()
69 | options.add_argument('--no-sandbox')
70 | options.add_argument('--disable-dev-shm-usage')
71 | options.add_argument('--disable-gpu')
72 | options.add_argument('--window-size=1920,1080')
73 | options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36')
74 | if HEADLESS:
75 | options.add_argument('--headless')
76 | options.add_argument('--disable-blink-features=AutomationControlled')
77 |
78 | options.add_experimental_option("excludeSwitches", ["enable-automation"])
79 | options.add_experimental_option("useAutomationExtension", False)
80 |
81 | service = Service(executable_path='/usr/bin/chromedriver')
82 | driver = webdriver.Chrome(service=service, options=options)
83 |
84 | if HEADLESS:
85 | driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
86 | driver.set_window_size(1920, 1080)
87 |
88 | return driver
89 |
90 | def login_with_credentials(driver):
91 | """使用账号密码登录"""
92 | logging.debug("使用账号密码登录...")
93 |
94 | if not USERNAME or not PASSWORD:
95 | raise ValueError("缺少 ARCTIC_USERNAME 或 ARCTIC_PASSWORD 环境变量!")
96 |
97 | driver.get(LOGIN_URL)
98 | logging.debug(f"当前页面URL: {driver.current_url}")
99 |
100 | try:
101 | email_input = WebDriverWait(driver, WAIT_TIMEOUT).until(
102 | EC.presence_of_element_located((By.NAME, "swapname"))
103 | )
104 | email_input.send_keys(USERNAME)
105 | logging.debug("✅ 用户名已输入")
106 | except Exception as e:
107 | logging.error("❌ 无法找到用户名输入框")
108 | take_screenshot(driver, "login_page_error.png")
109 | raise
110 |
111 | try:
112 | password_input = driver.find_element(By.NAME, "swappass")
113 | password_input.send_keys(PASSWORD)
114 | logging.debug("✅ 密码已输入")
115 | except Exception as e:
116 | logging.error("❌ 无法找到密码输入框")
117 | take_screenshot(driver, "password_input_error.png")
118 | raise
119 |
120 | try:
121 | login_button = WebDriverWait(driver, WAIT_TIMEOUT).until(
122 | EC.element_to_be_clickable((By.XPATH, "//button[contains(., '登录')]"))
123 | )
124 | login_button.click()
125 | logging.debug("✅ 登录按钮已点击")
126 | except Exception as e:
127 | logging.error("❌ 无法点击登录按钮")
128 | take_screenshot(driver, "login_button_error.png")
129 | raise
130 |
131 | try:
132 | WebDriverWait(driver, WAIT_TIMEOUT).until(
133 | EC.url_contains("index/index")
134 | )
135 | logging.info("✅ 登录成功!")
136 | except Exception as e:
137 | logging.error("❌ 登录失败:页面未跳转到预期页面")
138 | take_screenshot(driver, "login_redirect_error.png")
139 | raise
140 |
141 | def navigate_to_control_index(driver):
142 | """跳转到控制台首页"""
143 | logging.debug("正在访问控制台首页...")
144 | driver.get(CONTROL_INDEX_URL)
145 | try:
146 | WebDriverWait(driver, WAIT_TIMEOUT).until(
147 | EC.url_contains("control/index")
148 | )
149 | logging.debug("✅ 已进入控制台首页")
150 | except Exception as e:
151 | logging.error("❌ 控制台首页加载失败")
152 | take_screenshot(driver, "control_index_error.png")
153 | raise
154 |
155 | def find_and_navigate_to_instance_consoles(driver):
156 | """查找所有实例并进入实例控制台"""
157 | logging.debug("正在查找所有实例并进入实例控制台...")
158 | try:
159 | manage_buttons = WebDriverWait(driver, WAIT_TIMEOUT).until(
160 | EC.presence_of_all_elements_located((By.XPATH, "//a[contains(@class, 'btn btn-primary') and contains(@href, '/control/detail/')]"))
161 | )
162 |
163 | instance_ids = []
164 | for button in manage_buttons:
165 | href = button.get_attribute('href')
166 | instance_id = href.split('/')[-2]
167 | instance_ids.append(instance_id)
168 |
169 | if not instance_ids:
170 | raise ValueError("未找到任何实例")
171 |
172 | logging.info(f"共获取到 {len(instance_ids)} 个实例")
173 |
174 | # 保存当前首页的URL
175 | control_index_url = driver.current_url
176 |
177 | for i in range(len(instance_ids)):
178 | instance_id = instance_ids[i]
179 | logging.info(f"正在处理实例 ID {instance_id} ({i + 1}/{len(instance_ids)})...")
180 |
181 | # 直接访问实例控制台URL,而不是点击按钮
182 | detail_url = f"https://vps.polarbear.nyc.mn/control/detail/{instance_id}/"
183 | driver.get(detail_url)
184 |
185 | try:
186 | WebDriverWait(driver, WAIT_TIMEOUT).until(
187 | EC.url_contains(f"/control/detail/{instance_id}/")
188 | )
189 | logging.debug(f"✅ 已进入实例 ID {instance_id} 的控制台")
190 | renew_vps_instance(driver, instance_id)
191 | except Exception as e:
192 | logging.error(f"❌ 无法进入或处理实例 ID {instance_id} 的控制台")
193 | take_screenshot(driver, f"instance_console_error_{instance_id}.png")
194 | continue
195 |
196 | except Exception as e:
197 | logging.error(f"❌ 无法找到或点击管理按钮")
198 | take_screenshot(driver, "manage_button_click_error.png")
199 | raise
200 |
201 | def renew_vps_instance(driver, instance_id):
202 | """在实例控制台执行续费操作"""
203 | logging.debug("正在尝试续费 VPS 实例...")
204 | try:
205 | # 使用 data-target 定位“续费”按钮,避免依赖中文文本
206 | renew_button = WebDriverWait(driver, WAIT_TIMEOUT).until(
207 | EC.element_to_be_clickable((By.XPATH, "//button[@data-target='#addcontactmodal']"))
208 | )
209 | renew_button.click()
210 | logging.debug("✅ 续费按钮已点击")
211 | # 等待最多 10 秒,尝试处理自定义弹窗
212 | try:
213 | submit_button = WebDriverWait(driver, 10).until(
214 | EC.element_to_be_clickable((By.CSS_SELECTOR, "input.btn.m-b-xs.w-xs.btn-success.install-complete"))
215 | )
216 | submit_button.click()
217 | logging.debug("✅ 已点击 Submit 按钮")
218 | except TimeoutException:
219 | logging.error("❌ 未找到 Submit 按钮或超时")
220 | take_screenshot(driver, "submit_button_not_found.png")
221 | raise
222 | # 页面刷新后,等待续费成功的提示出现
223 | try:
224 | success_alert = WebDriverWait(driver, 30).until(
225 | EC.presence_of_element_located((By.XPATH, "//div[@class='alert alert-success']"))
226 | )
227 | logging.info("✅ 续期成功")
228 | logging.debug("提示信息:%s", success_alert.text)
229 | except Exception as e:
230 | logging.warning("⚠️ 未检测到续费成功提示,可能已续费成功但页面无反馈")
231 | take_screenshot(driver, "success_alert_not_found.png")
232 |
233 | # 读取到期时间
234 | try:
235 | # 获取所有 list-group-item
236 | list_group_items = WebDriverWait(driver, WAIT_TIMEOUT).until(
237 | EC.presence_of_all_elements_located((By.XPATH, "//li[@class='list-group-item']"))
238 | )
239 |
240 | if len(list_group_items) >= 5:
241 | full_text = list_group_items[4].text.strip() # 第五行内容(索引为4)
242 | logging.debug(f"📄 原始到期信息:{full_text}")
243 |
244 | # 提取“到期时间 xxxx-xx-xx”部分
245 | if "到期时间" in full_text:
246 | # 截取“到期时间”及其后日期部分
247 | start_index = full_text.find("到期时间")
248 | # 截取到“状态”前
249 | end_index = full_text.find("状态") if "状态" in full_text else len(full_text)
250 | expiration_text = full_text[start_index:end_index].strip()
251 | else:
252 | expiration_text = "未找到到期时间信息"
253 |
254 | logging.info(f"📅 实例 {instance_id} 续期成功,下次{expiration_text}")
255 | send(title=f"ArcticCloud续期成功", content=f"实例 {instance_id} 下次{expiration_text}")
256 | else:
257 | logging.warning("⚠️ 列表项不足五行")
258 | take_screenshot(driver, "list_group_item_not_enough.png")
259 |
260 | except Exception as e:
261 | logging.warning("⚠️ 读取列表项内容时发生异常:", e)
262 | take_screenshot(driver, "list_group_item_error.png")
263 |
264 | except Exception as e:
265 | logging.error("❌ 续费过程中发生异常:", exc_info=True)
266 | take_screenshot(driver, "renew_process_error.png")
267 | raise
268 |
269 | if __name__ == "__main__":
270 | driver = None
271 | try:
272 | logging.info("🚀 开始执行VPS自动续费脚本...")
273 | driver = setup_driver()
274 |
275 | login_with_credentials(driver)
276 | navigate_to_control_index(driver)
277 | find_and_navigate_to_instance_consoles(driver)
278 |
279 | except Exception as e:
280 | logging.error("🔴 主程序运行异常,已终止。", exc_info=True)
281 | finally:
282 | if driver:
283 | logging.info("关闭浏览器...")
284 | driver.quit()
285 | logging.info("✅ 脚本执行完成")
286 |
--------------------------------------------------------------------------------
/bincloud.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | # -------------------------------
3 | # @Author : github@wh1te3zzz https://github.com/wh1te3zzz/checkin
4 | # @Time : 2025-08-19 14:36:22
5 | # 56IDC保号脚本
6 | # -------------------------------
7 | """
8 | 56IDC 免费vps自动续期
9 | 变量为cookie,多账户换行隔开
10 | export BC_COOKIES = "cf_clearance=******; WHMCS2jRk8YCjn7Sg=******"
11 |
12 | cron: 0 */2 * * *
13 | const $ = new Env("56IDC续期");
14 | """
15 | import os
16 | import time
17 | import logging
18 | import undetected_chromedriver as uc
19 | from datetime import datetime
20 | from urllib.parse import urljoin
21 | from selenium.webdriver.common.by import By
22 | from selenium.webdriver.support.ui import WebDriverWait
23 | from selenium.webdriver.support import expected_conditions as EC
24 | from selenium.common.exceptions import TimeoutException, WebDriverException
25 |
26 | # ==================== 配置区 ====================
27 |
28 | ENABLE_SCREENSHOT = os.environ.get("ENABLE_SCREENSHOT", "true").lower() == "true"
29 | SCREENSHOT_DIR = os.environ.get("SCREENSHOT_DIR", "/ql/data/photo")
30 |
31 | logging.basicConfig(
32 | level=logging.INFO,
33 | format='%(asctime)s | %(levelname)s | %(message)s',
34 | datefmt='%Y-%m-%d %H:%M:%S'
35 | )
36 |
37 | if ENABLE_SCREENSHOT:
38 | os.makedirs(SCREENSHOT_DIR, exist_ok=True)
39 | logging.debug(f"📁 截图将保存至: {SCREENSHOT_DIR}")
40 |
41 | # ==================== 工具函数 ====================
42 |
43 | def parse_cookies(cookies_str):
44 | """解析多行 Cookie 字符串为字典列表"""
45 | cookie_dicts = []
46 | for line in cookies_str.strip().split('\n'):
47 | line = line.strip()
48 | if not line:
49 | continue
50 | cookies = {}
51 | for part in line.split(';'):
52 | part = part.strip()
53 | if '=' in part:
54 | key, value = part.split('=', 1)
55 | cookies[key.strip()] = value.strip()
56 | if cookies:
57 | cookie_dicts.append(cookies)
58 | return cookie_dicts
59 |
60 | def take_screenshot(driver, name="screenshot"):
61 | """保存截图(带时间戳)"""
62 | if not ENABLE_SCREENSHOT or not driver:
63 | return
64 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
65 | filename = f"{SCREENSHOT_DIR}/{name}_{timestamp}.png"
66 | try:
67 | driver.save_screenshot(filename)
68 | logging.info(f"📸 截图已保存: {filename}")
69 | except Exception as e:
70 | logging.error(f"❌ 截图失败: {e}")
71 |
72 | # ==================== 安全操作封装 ====================
73 |
74 | def safe_get(driver, url, timeout=15):
75 | """安全访问页面,等待加载完成"""
76 | try:
77 | driver.get(url)
78 | WebDriverWait(driver, timeout).until(
79 | lambda d: d.execute_script("return document.readyState") == "complete"
80 | )
81 | return True
82 | except Exception as e:
83 | logging.error(f"❌ 页面加载失败: {url} | {e}")
84 | return False
85 |
86 | def safe_scroll_to(driver, locator, timeout=10):
87 | """滚动到指定元素"""
88 | try:
89 | element = WebDriverWait(driver, timeout).until(EC.presence_of_element_located(locator))
90 | driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
91 | return element
92 | except TimeoutException:
93 | logging.warning(f"⚠️ 元素未找到,无法滚动: {locator}")
94 | return None
95 |
96 | def safe_switch_to_iframe(driver, iframe_locator, timeout=20):
97 | """安全进入 iframe"""
98 | try:
99 | WebDriverWait(driver, timeout).until(EC.frame_to_be_available_and_switch_to_it(iframe_locator))
100 | logging.debug("✅ 成功进入 virtualizor_manager iframe")
101 | return True
102 | except TimeoutException:
103 | logging.error("❌ iframe 加载超时或不可用")
104 | return False
105 |
106 | def get_visible_status(driver, status_ids):
107 | """使用 JS 检测真正可见的状态(基于 offsetWidth/Height)"""
108 | js = """
109 | const ids = %s;
110 | for (const id of ids) {
111 | const el = document.getElementById(id);
112 | if (el && el.offsetWidth > 0 && el.offsetHeight > 0) return id;
113 | }
114 | return null;
115 | """ % list(status_ids.keys())
116 | for _ in range(30):
117 | result = driver.execute_script(js)
118 | if result:
119 | return status_ids[result]
120 | time.sleep(1)
121 | return "⏱️ Timeout (状态未加载)"
122 |
123 | def click_start_button(driver, timeout=10):
124 | """尝试点击启动按钮 #startcell"""
125 | try:
126 | start_btn = WebDriverWait(driver, timeout).until(
127 | EC.element_to_be_clickable((By.ID, "startcell"))
128 | )
129 | driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", start_btn)
130 | driver.execute_script("arguments[0].click();", start_btn)
131 | logging.info("✅ 成功点击【启动】按钮")
132 | return True
133 | except TimeoutException:
134 | logging.warning("⚠️ 未找到【启动】按钮或不可点击")
135 | return False
136 | except Exception as e:
137 | logging.error(f"❌ 点击启动按钮失败: {e}")
138 | return False
139 |
140 | # ==================== 主程序 ====================
141 |
142 | def main():
143 | cookie_string = os.getenv('BC_COOKIES')
144 | if not cookie_string:
145 | logging.error("❌ 错误:环境变量 BC_COOKIES 未设置!")
146 | return
147 |
148 | cookies_list = parse_cookies(cookie_string)
149 | if not cookies_list:
150 | logging.error("❌ 错误:解析 BC_COOKIES 后未得到有效的 Cookie 信息。")
151 | return
152 |
153 | logging.info(f"✅ 已加载 {len(cookies_list)} 个账号的 Cookie")
154 |
155 | base_url = "https://56idc.net"
156 |
157 | for account_idx, cookies in enumerate(cookies_list, start=1):
158 | driver = None
159 | logging.info(f"{'='*50}")
160 | logging.info(f"正在处理第 {account_idx} 个账号...")
161 | logging.info(f"{'='*50}")
162 |
163 | # 浏览器配置
164 | options = uc.ChromeOptions()
165 | for arg in [
166 | "--no-sandbox",
167 | "--disable-dev-shm-usage",
168 | "--disable-gpu",
169 | "--disable-extensions",
170 | "--disable-plugins-discovery",
171 | "--disable-blink-features=AutomationControlled",
172 | "--start-maximized",
173 | "--headless=new",
174 | "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0 Safari/537.36"
175 | ]:
176 | options.add_argument(arg)
177 | driver = None
178 |
179 | try:
180 | # 启动浏览器
181 | driver = uc.Chrome(
182 | options=options,
183 | driver_executable_path='/usr/bin/chromedriver',
184 | version_main=138,
185 | use_subprocess=True
186 | )
187 |
188 | # 注入防检测脚本
189 | driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
190 | "source": """
191 | Object.defineProperty(navigator, 'webdriver', { get: () => false });
192 | window.navigator.chrome = { runtime: {} };
193 | Object.defineProperty(navigator, 'plugins', { get: () => [1,2,3,4,5] });
194 | """
195 | })
196 |
197 | # 登录主站
198 | if not safe_get(driver, f"{base_url}/clientarea.php?language=english"):
199 | take_screenshot(driver, f"login_failed_{account_idx}")
200 | continue
201 |
202 | # 注入 Cookie
203 | driver.delete_all_cookies()
204 | for name, value in cookies.items():
205 | driver.add_cookie({
206 | 'name': name,
207 | 'value': value,
208 | 'domain': '.56idc.net',
209 | 'path': '/',
210 | 'secure': True,
211 | 'httpOnly': False
212 | })
213 |
214 | # 重新加载
215 | if not safe_get(driver, f"{base_url}/clientarea.php?language=english"):
216 | take_screenshot(driver, f"reload_failed_{account_idx}")
217 | continue
218 |
219 | # 获取用户名
220 | try:
221 | username = WebDriverWait(driver, 10).until(
222 | EC.visibility_of_element_located((By.CSS_SELECTOR, "a.dropdown-toggle .active-client span.item-text"))
223 | ).text.strip()
224 | logging.info(f"✅ 登录成功,当前用户:{username}")
225 | except Exception:
226 | logging.error("❌ 登录失败:未找到用户名")
227 | take_screenshot(driver, f"login_failed_{account_idx}")
228 | continue
229 |
230 | # 提取产品列表
231 | try:
232 | panel = WebDriverWait(driver, 10).until(
233 | EC.presence_of_element_located((By.XPATH, "//div[@menuitemname='Active Products/Services']"))
234 | )
235 | list_group = panel.find_element(By.CLASS_NAME, "list-group")
236 | items = list_group.find_elements(By.CLASS_NAME, "list-group-item")
237 | except Exception as e:
238 | logging.error(f"❌ 未找到产品列表: {e}")
239 | take_screenshot(driver, f"products_failed_{account_idx}")
240 | continue
241 |
242 | products = []
243 | for item in items:
244 | try:
245 | content = item.find_element(By.CLASS_NAME, "list-group-item-content")
246 | name_div = item.find_element(By.CLASS_NAME, "list-group-item-name")
247 | href = content.get_attribute("data-href")
248 | if not href:
249 | continue
250 |
251 | # 提取服务名
252 | try:
253 | prefix = name_div.find_element(By.TAG_NAME, "b").text.strip()
254 | except:
255 | prefix = ""
256 | spans = name_div.find_elements(By.TAG_NAME, "span")
257 | other = spans[0].text.strip() if spans else ""
258 | full_name = f"{prefix} - {other.replace(prefix, '', 1).strip(' -')}" if other else prefix
259 |
260 | # 提取域名
261 | try:
262 | domain = name_div.find_element(By.CSS_SELECTOR, "span.text-domain").text.strip()
263 | except:
264 | domain = ""
265 |
266 | products.append({
267 | 'name': full_name,
268 | 'domain': domain,
269 | 'url': urljoin(base_url, href)
270 | })
271 | except Exception as e:
272 | logging.warning(f"⚠️ 跳过无效产品项: {e}")
273 | continue
274 |
275 | logging.info(f"📊 检测到 {len(products)} 个已激活服务")
276 |
277 | # === 遍历产品检查 VPS 状态并自动启动 ===
278 | for i, product in enumerate(products, 1):
279 | logging.debug(f"➡️ 正在检查服务 [{i}/{len(products)}]: {product['name']} | 主机名: {product['domain']}")
280 |
281 | if not safe_get(driver, product['url']):
282 | continue
283 |
284 | # 滚动到 Primary IP
285 | safe_scroll_to(driver, (By.XPATH, "//span[@class='list-info-title' and text()='Primary IP']"))
286 |
287 | # 进入 iframe
288 | if not safe_switch_to_iframe(driver, (By.ID, "virtualizor_manager")):
289 | driver.switch_to.default_content()
290 | continue
291 |
292 | # 检测状态
293 | status_map = {
294 | 'vm_status_online': '🟢 Online',
295 | 'vm_status_offline': '🔴 Offline',
296 | 'vm_status_suspended': '🟡 Suspended',
297 | 'vm_status_nw_suspended': '🟠 Network Suspended'
298 | }
299 | status = get_visible_status(driver, status_map)
300 | logging.info(f"📊 【VPS 状态】{product['domain']} | {status}")
301 |
302 | # 如果是 Offline,尝试启动
303 | if "Offline" in status:
304 | logging.info("🔧 检测到 VPS 已关机,正在尝试启动...")
305 | if click_start_button(driver):
306 | logging.debug("🔄 已发送启动指令,等待状态刷新...")
307 | time.sleep(5) # 等待响应
308 | else:
309 | take_screenshot(driver, f"start_failed_{account_idx}_{i}")
310 | logging.warning("⚠️ 启动操作失败,可能按钮被禁用或网络问题")
311 |
312 | # 返回主文档
313 | driver.switch_to.default_content()
314 |
315 | except Exception as e:
316 | logging.error(f"❌ 账号 {account_idx} 发生未预期错误: {e}")
317 | take_screenshot(driver, f"unexpected_error_{account_idx}")
318 | finally:
319 | if driver:
320 | try:
321 | driver.quit()
322 | logging.info(f"🔚 第 {account_idx} 个账号处理完成")
323 | except:
324 | pass
325 | time.sleep(2)
326 |
327 | logging.info(f"{'='*50}")
328 | logging.info("✅ 所有账号处理完毕。")
329 | logging.info(f"{'='*50}")
330 |
331 | # ==================== 启动 ====================
332 |
333 | if __name__ == "__main__":
334 | main()
335 |
--------------------------------------------------------------------------------
/clochat.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | # -------------------------------
3 | # @Author : github@wh1te3zzz https://github.com/wh1te3zzz/checkin
4 | # @Time : 2025-07-11 14:06:56
5 | # CloChat签到脚本
6 | # -------------------------------
7 | """
8 | CloChat签到
9 | 变量为账号密码
10 | export CLOCHAT_USERNAME="账号"
11 | export CLOCHAT_PASSWORD="密码"
12 |
13 | cron: 10 10 * * *
14 | const $ = new Env("clochat签到");
15 | """
16 | import os
17 | import time
18 | import traceback
19 | import logging
20 | from notify import send
21 | from selenium import webdriver
22 | from selenium.webdriver.common.by import By
23 | from selenium.webdriver.support.ui import WebDriverWait
24 | from selenium.webdriver.support import expected_conditions as EC
25 | from selenium.webdriver.chrome.options import Options
26 |
27 | # 配置信息
28 | USERNAME = os.environ.get("CLOCHAT_USERNAME")
29 | PASSWORD = os.environ.get("CLOCHAT_PASSWORD")
30 | HEADLESS = os.environ.get("HEADLESS", "true").lower() == "true"
31 | LOG_LEVEL = os.environ.get("CLOCHAT_LOG_LEVEL", "DEBUG").upper() # 日志级别
32 |
33 | # 初始化日志系统
34 | logging.basicConfig(
35 | level=logging.INFO,
36 | format='%(asctime)s - %(levelname)s - %(message)s'
37 | )
38 | log = logging.getLogger(__name__)
39 | # 设置日志输出等级
40 | log.setLevel(logging.INFO if LOG_LEVEL == "INFO" else logging.DEBUG)
41 |
42 | def setup_driver():
43 | """初始化浏览器"""
44 | options = Options()
45 | options.add_argument('--no-sandbox')
46 | options.add_argument('--disable-dev-shm-usage')
47 |
48 | if HEADLESS:
49 | options.add_argument('--headless')
50 | options.add_argument('--disable-blink-features=AutomationControlled')
51 | options.add_argument('--disable-gpu')
52 | options.add_argument('--window-size=1920,1080')
53 | options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36')
54 |
55 | driver = webdriver.Chrome(options=options)
56 |
57 | if HEADLESS:
58 | driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
59 | driver.set_window_size(1920, 1080)
60 |
61 | return driver
62 |
63 | def login(driver):
64 | """使用账号密码登录"""
65 | if not USERNAME or not PASSWORD:
66 | log.error("未找到CLOCHAT_USERNAME或CLOCHAT_PASSWORD环境变量")
67 | return False
68 |
69 | log.debug("跳转至登录页面...")
70 | driver.get('https://clochat.com/login')
71 |
72 | try:
73 | # 输入用户名
74 | username_input = WebDriverWait(driver, 30).until(
75 | EC.presence_of_element_located((By.ID, "login-account-name"))
76 | )
77 | username_input.send_keys(USERNAME)
78 |
79 | # 输入密码
80 | password_input = WebDriverWait(driver, 30).until(
81 | EC.presence_of_element_located((By.ID, "login-account-password"))
82 | )
83 | password_input.send_keys(PASSWORD)
84 |
85 | # 点击登录按钮
86 | login_button = WebDriverWait(driver, 30).until(
87 | EC.element_to_be_clickable((By.ID, "login-button"))
88 | )
89 | login_button.click()
90 |
91 | # 等待登录完成(URL变化)
92 | WebDriverWait(driver, 30).until_not(
93 | EC.url_contains("login")
94 | )
95 |
96 | log.info("登录成功!")
97 |
98 | return True
99 |
100 | except Exception as e:
101 | log.error(f"登录过程中出错: {str(e)}")
102 | log.debug(f"当前页面源码片段: {driver.page_source[:1000]}...")
103 | return False
104 |
105 | def send_sign_in_message_in_chat(driver):
106 | """在指定聊天室发送签到消息并检查结果"""
107 | CHAT_URL = "https://clochat.com/chat/c/-/2"
108 |
109 | try:
110 | log.debug(f"跳转至聊天室: {CHAT_URL}")
111 | driver.get(CHAT_URL)
112 | time.sleep(5)
113 |
114 | log.debug("等待输入框加载...")
115 | input_box = WebDriverWait(driver, 30).until(
116 | EC.presence_of_element_located((By.ID, "channel-composer"))
117 | )
118 |
119 | log.debug("清空输入框...")
120 | input_box.clear()
121 |
122 | log.debug("输入'签到'")
123 | input_box.send_keys("签到")
124 |
125 | log.debug("等待发送按钮可用...")
126 | send_button = WebDriverWait(driver, 30).until(
127 | EC.element_to_be_clickable((By.CSS_SELECTOR, ".chat-composer-button.-send"))
128 | )
129 |
130 | log.debug("点击发送按钮...")
131 | send_button.click()
132 | log.info("✅ 签到消息已发送!")
133 |
134 | # 检查是否有机器人回复
135 | time.sleep(5)
136 | messages = driver.find_elements(By.CSS_SELECTOR, ".chat-message-container.is-bot,.chat-message-container.is-current-user")
137 | if messages:
138 | last_message = messages[-1]
139 | chat_content = last_message.find_element(By.CSS_SELECTOR, ".chat-cooked p").text.strip()
140 |
141 | log.info(f"🔍 签到结果: {chat_content}")
142 | send(title="CLOCHAT 签到通知", content=chat_content)
143 | else:
144 | log.error("❌ 未检测到任何消息,请检查网络或页面是否加载完成。")
145 |
146 | except Exception as e:
147 | log.error(f"聊天室签到失败: {e}")
148 | log.debug(traceback.format_exc())
149 | #driver.save_screenshot(f"/ql/data/photo/chat_error_{int(time.time())}.png")
150 |
151 | if __name__ == "__main__":
152 | driver = None
153 | try:
154 | log.info("开始执行CloChat签到脚本...")
155 | driver = setup_driver()
156 |
157 | if login(driver):
158 | log.debug("开始执行聊天室签到流程...")
159 | send_sign_in_message_in_chat(driver)
160 | else:
161 | log.debug("登录失败,无法继续签到。")
162 |
163 | finally:
164 | if driver:
165 | log.info("关闭浏览器...")
166 | driver.quit()
167 | log.info("脚本执行完成")
168 |
--------------------------------------------------------------------------------
/nodeloc.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | # -------------------------------
3 | # @Author : github@wh1te3zzz
4 | # @Time : 2025-09-01
5 | # NodeLoc 签到脚本
6 | # -------------------------------
7 | """
8 | NodeLoc签到
9 | 自行网页捉包提取请求头中的cookie和x-csrf-token填到变量 NLCookie 中,用#号拼接,多账号换行隔开
10 | export NL_COOKIE="_t=******; _forum_session=xxxxxx#XXXXXX"
11 |
12 | cron: 59 8 * * *
13 | const $ = new Env("NodeLoc签到");
14 | """
15 | import os
16 | import time
17 | import logging
18 | from datetime import datetime
19 | import undetected_chromedriver as uc
20 | from selenium.webdriver.common.by import By
21 | from selenium.webdriver.support.ui import WebDriverWait
22 | from selenium.webdriver.support import expected_conditions as EC
23 | from selenium.webdriver.common.action_chains import ActionChains
24 |
25 | # ==================== 固定配置 ====================
26 | DOMAIN = "www.nodeloc.com"
27 | HOME_URL = f"https://{DOMAIN}/u/" # 用户列表页
28 | CHECKIN_BUTTON_SELECTOR = 'li.header-dropdown-toggle.checkin-icon button.checkin-button'
29 | USERNAME_SELECTOR = 'div.directory-table__row.me a[data-user-card]' # 当前登录用户
30 | SCREENSHOT_DIR = "./photo"
31 | LOG_LEVEL = logging.INFO
32 | # =================================================
33 | os.makedirs(SCREENSHOT_DIR, exist_ok=True)
34 |
35 | logging.basicConfig(
36 | level=LOG_LEVEL,
37 | format='%(asctime)s [%(levelname)s] %(message)s',
38 | datefmt='%Y-%m-%d %H:%M:%S'
39 | )
40 | log = logging.getLogger(__name__)
41 |
42 | results = []
43 |
44 | def generate_screenshot_path(prefix: str) -> str:
45 | ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
46 | return os.path.join(SCREENSHOT_DIR, f"{prefix}_{ts}.png")
47 |
48 | def get_username_from_user_page(driver) -> str:
49 | log.debug("🔍 正在提取用户名...")
50 | try:
51 | element = WebDriverWait(driver, 10).until(
52 | EC.presence_of_element_located((By.CSS_SELECTOR, USERNAME_SELECTOR))
53 | )
54 | username = element.get_attribute("data-user-card")
55 | return username.strip() if username else "未知用户"
56 | except Exception as e:
57 | log.error(f"❌ 提取用户名失败: {e}")
58 | return "未知用户"
59 |
60 | def check_login_status(driver):
61 | log.debug("🔐 正在检测登录状态...")
62 | try:
63 | WebDriverWait(driver, 10).until(
64 | EC.any_of(
65 | EC.presence_of_element_located((By.CSS_SELECTOR, "div.directory-table__row.me")),
66 | EC.presence_of_element_located((By.CSS_SELECTOR, "button.checkin-button"))
67 | )
68 | )
69 | log.info("✅ 登录成功")
70 | return True
71 | except Exception as e:
72 | log.error(f"❌ 登录失败或 Cookie 无效: {e}")
73 | screenshot_path = generate_screenshot_path('login_failed')
74 | driver.save_screenshot(screenshot_path)
75 | log.info(f"📸 已保存登录失败截图:{screenshot_path}")
76 | return False
77 |
78 | def setup_browser():
79 | options = uc.ChromeOptions()
80 | options.add_argument('--no-sandbox')
81 | options.add_argument('--disable-dev-shm-usage')
82 | options.add_argument('--disable-gpu')
83 | options.add_argument('--window-size=1920,1080')
84 | options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0 Safari/537.36')
85 | options.add_argument('--disable-blink-features=AutomationControlled')
86 | options.add_argument('--disable-extensions')
87 | options.add_argument('--disable-infobars')
88 | options.add_argument('--disable-popup-blocking')
89 | options.add_argument('--headless=new')
90 | log.debug("🌐 启动 Chrome(无头模式)...")
91 | try:
92 | driver = uc.Chrome(
93 | options=options,
94 | driver_executable_path='/usr/bin/chromedriver',
95 | version_main=138,
96 | use_subprocess=True
97 | )
98 | driver.set_window_size(1920, 1080)
99 | driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => false});")
100 | driver.execute_script("window.chrome = { runtime: {} };")
101 | driver.execute_script("Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]});")
102 | driver.execute_script("Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']});")
103 |
104 | return driver
105 | except Exception as e:
106 | log.error(f"❌ 浏览器启动失败: {e}")
107 | return None
108 |
109 | def hover_checkin_button(driver):
110 | try:
111 | wait = WebDriverWait(driver, 10)
112 | button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, CHECKIN_BUTTON_SELECTOR)))
113 | ActionChains(driver).move_to_element(button).perform()
114 | time.sleep(1)
115 | except Exception as e:
116 | log.warning(f"⚠️ 刷新签到状态失败: {e}")
117 |
118 | def perform_checkin(driver, username: str):
119 | try:
120 | driver.get("https://www.nodeloc.com/")
121 | time.sleep(3)
122 | hover_checkin_button(driver)
123 | wait = WebDriverWait(driver, 10)
124 | button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, CHECKIN_BUTTON_SELECTOR)))
125 |
126 | if "checked-in" in button.get_attribute("class"):
127 | msg = f"[✅] {username} 今日已签到"
128 | log.info(msg)
129 | return msg
130 |
131 | log.info(f"📌 {username} - 准备签到")
132 | driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
133 | time.sleep(1)
134 | driver.execute_script("arguments[0].click();", button)
135 | time.sleep(3)
136 |
137 | hover_checkin_button(driver)
138 |
139 | if "checked-in" in button.get_attribute("class"):
140 | msg = f"[🎉] {username} 签到成功!"
141 | log.info(msg)
142 | return msg
143 | else:
144 | msg = f"[⚠️] {username} 点击后状态未更新,可能失败"
145 | log.warning(msg)
146 | path = generate_screenshot_path("checkin_uncertain")
147 | driver.save_screenshot(path)
148 | log.info(f"📸 已保存状态存疑截图:{path}")
149 | return msg
150 |
151 | except Exception as e:
152 | msg = f"[❌] {username} 签到异常: {e}"
153 | log.error(msg)
154 | path = generate_screenshot_path("checkin_error")
155 | try:
156 | driver.save_screenshot(path)
157 | log.info(f"📸 已保存错误截图:{path}")
158 | except:
159 | pass
160 | return msg
161 |
162 | def process_account(cookie_str: str):
163 | cookie = cookie_str.split("#", 1)[0].strip()
164 | if not cookie:
165 | log.error("❌ Cookie 为空")
166 | return "[❌] Cookie 为空"
167 |
168 | driver = None
169 | try:
170 | driver = setup_browser()
171 | if not driver:
172 | return "[❌] 浏览器启动失败"
173 |
174 | log.info("🚀 正在打开用户列表页...")
175 | driver.get(HOME_URL)
176 | time.sleep(3)
177 |
178 | log.debug("🍪 正在设置 Cookie...")
179 | for item in cookie.split(";"):
180 | item = item.strip()
181 | if not item or "=" not in item:
182 | continue
183 | try:
184 | name, value = item.split("=", 1)
185 | driver.add_cookie({
186 | 'name': name.strip(),
187 | 'value': value.strip(),
188 | 'domain': '.nodeloc.com',
189 | 'path': '/',
190 | 'secure': True,
191 | 'httpOnly': False
192 | })
193 | except Exception as e:
194 | log.warning(f"[⚠️] 添加 Cookie 失败: {item} -> {e}")
195 | continue
196 |
197 | driver.refresh()
198 | time.sleep(5)
199 |
200 | if not check_login_status(driver):
201 | return "[❌] 登录失败,Cookie 可能失效"
202 |
203 | username = get_username_from_user_page(driver)
204 | log.info(f"👤 当前用户: {username}")
205 |
206 | result = perform_checkin(driver, username)
207 | return result
208 |
209 | except Exception as e:
210 | msg = f"[🔥] 处理异常: {e}"
211 | log.error(msg)
212 | return msg
213 | finally:
214 | if driver:
215 | try:
216 | driver.quit()
217 | except:
218 | pass
219 |
220 | def main():
221 | global results
222 | if 'NL_COOKIE' not in os.environ:
223 | msg = "❌ 未设置 NL_COOKIE 环境变量"
224 | print(msg)
225 | results.append(msg)
226 | return
227 |
228 | raw_lines = os.environ.get("NL_COOKIE").strip().split("\n")
229 | cookies = [line.strip() for line in raw_lines if line.strip()]
230 |
231 | if not cookies:
232 | msg = "❌ 未解析到有效 Cookie"
233 | print(msg)
234 | results.append(msg)
235 | return
236 |
237 | log.info(f"✅ 查找到 {len(cookies)} 个账号,开始顺序签到...")
238 |
239 | for cookie_str in cookies:
240 | result = process_account(cookie_str)
241 | results.append(result)
242 | time.sleep(5)
243 |
244 | log.info("✅ 全部签到完成")
245 |
246 | if __name__ == '__main__':
247 | main()
248 |
--------------------------------------------------------------------------------
/nodeseek.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | # -------------------------------
3 | # @Author : github@wh1te3zzz https://github.com/wh1te3zzz/checkin
4 | # @Time : 2025-07-16 15:42:16
5 | # NodeSeek签到脚本
6 | # -------------------------------
7 | """
8 | NodeSeek签到
9 | 自行网页捉包提取请求头中的cookie填到变量 NS_COOKIE 中
10 | export NS_COOKIE="XXXXXX"
11 |
12 | cron: 30 8 * * *
13 | const $ = new Env("NodeSeek签到");
14 | """
15 | # -*- coding: utf-8 -*-
16 | import os
17 | import time
18 | import traceback
19 | import logging
20 | from notify import send
21 | from datetime import datetime
22 | from selenium import webdriver
23 | from selenium.webdriver.common.by import By
24 | from selenium.webdriver.support.ui import WebDriverWait
25 | from selenium.webdriver.support import expected_conditions as EC
26 | from selenium.webdriver.chrome.options import Options
27 | import undetected_chromedriver as uc
28 |
29 | # ========== 环境变量 ==========
30 | COOKIE = os.environ.get("NS_COOKIE")
31 | SIGN_MODE = os.environ.get("NS_SIGN_MODE", "chicken") # 签到模式 chicken / lucky
32 | ENABLE_SCREENSHOT = os.environ.get("NS_ENABLE_SCREENSHOT", "false").lower() == "true" # 是否启用截图
33 | HEADLESS = os.environ.get("NS_HEADLESS", "true").lower() == "true" # 是否启用无头模式
34 | LOG_LEVEL = os.environ.get("NS_LOG_LEVEL", "INFO").upper() # 获取日志级别
35 |
36 | # 设置日志
37 | logging.basicConfig(
38 | level=LOG_LEVEL,
39 | format="%(asctime)s [%(levelname)s] %(message)s",
40 | handlers=[logging.StreamHandler()]
41 | )
42 |
43 | SCREENSHOT_DIR = "/ql/data/photo"
44 | if not os.path.exists(SCREENSHOT_DIR):
45 | os.makedirs(SCREENSHOT_DIR)
46 |
47 | def take_screenshot(driver, filename_prefix="screenshot"):
48 | """统一截图函数,仅在启用截图时执行"""
49 | if not ENABLE_SCREENSHOT:
50 | return None
51 | try:
52 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
53 | screenshot_path = os.path.join(SCREENSHOT_DIR, f"{filename_prefix}_{timestamp}.png")
54 | driver.save_screenshot(screenshot_path)
55 | logging.debug(f"📸 截图已保存至: {screenshot_path}")
56 | return screenshot_path
57 | except Exception as e:
58 | logging.warning(f"⚠️ 截图保存失败: {str(e)}")
59 | return None
60 |
61 | # ========== 浏览器初始化 ==========
62 | def setup_browser():
63 | """初始化浏览器并设置 Cookie"""
64 | if not COOKIE:
65 | logging.error("❌ 环境变量中未找到 COOKIE,请设置 NS_COOKIE 或 COOKIE")
66 | return None
67 |
68 | chrome_options = Options()
69 | chrome_options.add_argument("--no-sandbox")
70 | chrome_options.add_argument("--disable-dev-shm-usage")
71 | chrome_options.add_argument("--disable-gpu")
72 | chrome_options.add_argument("--window-size=1920,1080")
73 |
74 | if HEADLESS:
75 | logging.debug("✅ 启用无头模式")
76 | chrome_options.add_argument("--headless=new")
77 | chrome_options.add_argument("--disable-blink-features=AutomationControlled")
78 | chrome_options.add_argument(
79 | "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0 Safari/537.36"
80 | )
81 |
82 | driver = uc.Chrome(
83 | options=chrome_options,
84 | driver_executable_path='/usr/bin/chromedriver',
85 | version_main=138
86 | )
87 |
88 | # 隐藏自动化特征
89 | driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
90 | "source": """
91 | Object.defineProperty(navigator, 'webdriver', {
92 | get: () => undefined
93 | })
94 | """
95 | })
96 |
97 | logging.debug("🌐 正在访问 nodeseek.com...")
98 | try:
99 | driver.get("https://www.nodeseek.com")
100 | WebDriverWait(driver, 60).until(
101 | EC.presence_of_element_located((By.TAG_NAME, "body"))
102 | )
103 | logging.debug("🎉 页面加载成功")
104 | except Exception as e:
105 | logging.error(f"❌ 页面加载失败: {str(e)}")
106 | return None
107 |
108 | # 添加 Cookie
109 | for item in COOKIE.split(";"):
110 | try:
111 | name, value = item.strip().split("=", 1)
112 | driver.add_cookie({
113 | "name": name,
114 | "value": value,
115 | "domain": ".nodeseek.com",
116 | "path": "/",
117 | })
118 | except Exception as e:
119 | logging.warning(f"⚠️ 添加 Cookie 失败: {e}")
120 | continue
121 |
122 | logging.debug("🔄 刷新页面以应用 Cookie...")
123 | try:
124 | driver.refresh()
125 | WebDriverWait(driver, 60).until(
126 | EC.presence_of_element_located((By.TAG_NAME, "body"))
127 | )
128 | logging.debug("🎉 页面刷新成功")
129 | except Exception as e:
130 | logging.error(f"❌ 页面刷新失败: {str(e)}")
131 | return None
132 |
133 | time.sleep(5)
134 |
135 | # 验证是否登录成功并获取用户名
136 | try:
137 | username_element = WebDriverWait(driver, 30).until(
138 | EC.presence_of_element_located((By.CSS_SELECTOR, "a.Username"))
139 | )
140 | username = username_element.text.strip()
141 | logging.info(f"🔐 登录成功,当前账号为: {username}")
142 | except Exception as e:
143 | logging.error("❌ 未检测到用户名元素,可能登录失败或 Cookie 无效")
144 | take_screenshot(driver, "login_failure")
145 | return None
146 |
147 | return driver
148 |
149 | # ========== 点击签到图标 ==========
150 | def click_sign_icon(driver):
151 | """点击首页的签到图标"""
152 | try:
153 | sign_icon = WebDriverWait(driver, 30).until(
154 | EC.element_to_be_clickable((By.XPATH, "//span[@title='签到']"))
155 | )
156 | sign_icon.click()
157 | logging.debug("🎉 签到图标点击成功")
158 | return True
159 | except Exception as e:
160 | logging.error(f"❌ 点击签到图标失败: {str(e)}")
161 | take_screenshot(driver, "sign_icon_click_failure")
162 | return False
163 |
164 | # ========== 检查签到状态 ==========
165 | def check_sign_status(driver):
166 | """检查签到状态:通过是否有 button 来判断是否已签到。等待签到信息加载完成并发送通知"""
167 | try:
168 | logging.debug("🔄 正在访问签到页面...")
169 | driver.get("https://www.nodeseek.com/board")
170 | WebDriverWait(driver, 60).until(
171 | EC.presence_of_element_located((By.TAG_NAME, "body"))
172 | )
173 |
174 | # 定位 head-info 区域
175 | head_info_div = WebDriverWait(driver, 60).until(
176 | EC.presence_of_element_located((By.CSS_SELECTOR, ".head-info > div"))
177 | )
178 | # 等待文本不再是 "Loading"
179 | WebDriverWait(driver, 60).until(
180 | lambda d: d.find_element(By.CSS_SELECTOR, ".head-info > div").text.strip() != "Loading"
181 | )
182 |
183 | # 检查是否有 button 存在
184 | buttons = head_info_div.find_elements(By.TAG_NAME, "button")
185 | if buttons:
186 | logging.info("🔄 今日尚未签到")
187 | return False # 尚未签到
188 | else:
189 | sign_info = head_info_div.text.strip()
190 | logging.info(f"✅ {sign_info}")
191 | send(title="NodeSeek 签到通知", content=f"{sign_info}")
192 | return True # 已签到
193 |
194 | except Exception as e:
195 | logging.error(f"❌ 检查签到状态失败: {str(e)}")
196 | take_screenshot(driver, "check_sign_status_failure")
197 | return False
198 |
199 | def click_sign_button(driver):
200 | """查找并点击签到按钮,兼容已签到情况"""
201 | try:
202 | logging.debug("🔍 开始查找签到区域...")
203 |
204 | # 查找签到按钮容器
205 | sign_div = WebDriverWait(driver, 30).until(
206 | EC.presence_of_element_located((
207 | By.XPATH,
208 | "//div[button[text()='鸡腿 x 5'] and button[text()='试试手气']]"
209 | ))
210 | )
211 | logging.debug("✅ 找到签到区域")
212 |
213 | # 根据 SIGN_MODE 决定点击哪个按钮
214 | if SIGN_MODE == "chicken":
215 | logging.info("🍗 准备点击「鸡腿 x 5」按钮")
216 | button = sign_div.find_element(By.XPATH, ".//button[text()='鸡腿 x 5']")
217 | elif SIGN_MODE == "lucky":
218 | logging.info("🎲 准备点击「试试手气」按钮")
219 | button = sign_div.find_element(By.XPATH, ".//button[text()='试试手气']")
220 | else:
221 | logging.error(f"❌ 未知的签到模式: {SIGN_MODE},请设置 chicken 或 lucky")
222 | return False
223 |
224 | driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
225 | time.sleep(0.5)
226 | button.click()
227 | logging.debug("🎉 按钮点击成功!签到完成")
228 | send(title="NodeSeek 签到通知", content="🎉 签到成功!")
229 | return True
230 |
231 | except Exception as e:
232 | logging.error(f"❌ 签到过程中出错: {str(e)}")
233 | logging.debug(traceback.format_exc())
234 |
235 | # 输出当前页面信息用于调试
236 | logging.debug("📄 当前页面 URL:", driver.current_url)
237 | logging.debug("📄 页面源码片段:\n", driver.page_source[:1000])
238 | take_screenshot(driver, "sign_in_failure")
239 | return False
240 |
241 | if __name__ == "__main__":
242 | logging.info("🚀 开始执行 NodeSeek 签到脚本...")
243 |
244 | driver = setup_browser()
245 | if not driver:
246 | logging.error("🚫 浏览器初始化失败")
247 | exit(1)
248 |
249 | try:
250 | if not click_sign_icon(driver):
251 | logging.error("🚫 点击签到图标失败")
252 | exit(1)
253 |
254 | if check_sign_status(driver):
255 | exit(0)
256 | else:
257 | click_sign_button(driver)
258 |
259 | finally:
260 | logging.info("🛑 脚本执行完毕,关闭浏览器")
261 | driver.quit()
262 |
--------------------------------------------------------------------------------
/notify.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # _*_ coding:utf-8 _*_
3 | import base64
4 | import hashlib
5 | import hmac
6 | import json
7 | import os
8 | import re
9 | import threading
10 | import time
11 | import urllib.parse
12 | import smtplib
13 | from email.mime.text import MIMEText
14 | from email.header import Header
15 | from email.utils import formataddr
16 |
17 | import requests
18 |
19 | # 原先的 print 函数和主线程的锁
20 | _print = print
21 | mutex = threading.Lock()
22 |
23 |
24 | # 定义新的 print 函数
25 | def print(text, *args, **kw):
26 | """
27 | 使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
28 | """
29 | with mutex:
30 | _print(text, *args, **kw)
31 |
32 |
33 | # 通知服务
34 | # fmt: off
35 | push_config = {
36 | #'HITOKOTO': False, # 启用一言(随机句子)
37 |
38 | 'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
39 | 'BARK_ARCHIVE': '', # bark 推送是否存档
40 | 'BARK_GROUP': '', # bark 推送分组
41 | 'BARK_SOUND': '', # bark 推送声音
42 | 'BARK_ICON': '', # bark 推送图标
43 | 'BARK_LEVEL': '', # bark 推送时效性
44 | 'BARK_URL': '', # bark 推送跳转URL
45 |
46 | 'CONSOLE': False, # 控制台输出
47 |
48 | 'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
49 | 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
50 |
51 | 'FSKEY': '', # 飞书机器人的 FSKEY
52 |
53 | 'GOBOT_URL': '', # go-cqhttp
54 | # 推送到个人QQ:http://127.0.0.1/send_private_msg
55 | # 群:http://127.0.0.1/send_group_msg
56 | 'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
57 | # GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
58 | # /send_group_msg 时填入 group_id=QQ群
59 | 'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
60 |
61 | 'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
62 | 'GOTIFY_TOKEN': '', # gotify的消息应用token
63 | 'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
64 |
65 | 'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
66 |
67 | 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
68 |
69 | 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
70 | 'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
71 |
72 | 'CHAT_URL': '', # synology chat url
73 | 'CHAT_TOKEN': '', # synology chat token
74 |
75 | 'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌
76 | 'PUSH_PLUS_USER': '', # pushplus 推送的群组编码
77 | 'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
78 | 'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
79 | 'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
80 | 'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
81 | 'PUSH_PLUS_TO': '', # pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
82 |
83 | 'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
84 | 'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
85 | 'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本
86 |
87 | 'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
88 | 'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
89 |
90 | 'QYWX_ORIGIN': '', # 企业微信代理地址
91 |
92 | 'QYWX_AM': '', # 企业微信应用
93 |
94 | 'QYWX_KEY': '', # 企业微信机器人
95 |
96 | 'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
97 | 'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
98 | 'TG_API_HOST': '', # tg 代理 api
99 | 'TG_PROXY_AUTH': '', # tg 代理认证参数
100 | 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
101 | 'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
102 |
103 | 'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
104 | 'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
105 | 'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
106 |
107 | 'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
108 | 'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
109 | 'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
110 | 'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
111 | 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
112 |
113 | 'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY
114 | 'PUSHME_URL': '', # PushMe 的 PUSHME_URL
115 |
116 | 'CHRONOCAT_QQ': '', # qq号
117 | 'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
118 | 'CHRONOCAT_URL': '', # CHRONOCAT的url地址
119 |
120 | 'WEBHOOK_URL': '', # 自定义通知 请求地址
121 | 'WEBHOOK_BODY': '', # 自定义通知 请求体
122 | 'WEBHOOK_HEADERS': '', # 自定义通知 请求头
123 | 'WEBHOOK_METHOD': '', # 自定义通知 请求方法
124 | 'WEBHOOK_CONTENT_TYPE': '', # 自定义通知 content-type
125 |
126 | 'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
127 | 'NTFY_TOPIC': '', # ntfy的消息应用topic
128 | 'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
129 |
130 | 'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
131 | 'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
132 | 'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
133 | }
134 | # fmt: on
135 |
136 | for k in push_config:
137 | if os.getenv(k):
138 | v = os.getenv(k)
139 | push_config[k] = v
140 |
141 |
142 | def bark(title: str, content: str) -> None:
143 | """
144 | 使用 bark 推送消息。
145 | """
146 | if not push_config.get("BARK_PUSH"):
147 | return
148 | print("bark 服务启动")
149 |
150 | if push_config.get("BARK_PUSH").startswith("http"):
151 | url = f'{push_config.get("BARK_PUSH")}'
152 | else:
153 | url = f'https://api.day.app/{push_config.get("BARK_PUSH")}'
154 |
155 | bark_params = {
156 | "BARK_ARCHIVE": "isArchive",
157 | "BARK_GROUP": "group",
158 | "BARK_SOUND": "sound",
159 | "BARK_ICON": "icon",
160 | "BARK_LEVEL": "level",
161 | "BARK_URL": "url",
162 | }
163 | data = {
164 | "title": title,
165 | "body": content,
166 | }
167 | for pair in filter(
168 | lambda pairs: pairs[0].startswith("BARK_")
169 | and pairs[0] != "BARK_PUSH"
170 | and pairs[1]
171 | and bark_params.get(pairs[0]),
172 | push_config.items(),
173 | ):
174 | data[bark_params.get(pair[0])] = pair[1]
175 | headers = {"Content-Type": "application/json;charset=utf-8"}
176 | response = requests.post(
177 | url=url, data=json.dumps(data), headers=headers, timeout=15
178 | ).json()
179 |
180 | if response["code"] == 200:
181 | print("bark 推送成功!")
182 | else:
183 | print("bark 推送失败!")
184 |
185 |
186 | def console(title: str, content: str) -> None:
187 | """
188 | 使用 控制台 推送消息。
189 | """
190 | print(f"{title}\n\n{content}")
191 |
192 |
193 | def dingding_bot(title: str, content: str) -> None:
194 | """
195 | 使用 钉钉机器人 推送消息。
196 | """
197 | if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
198 | return
199 | print("钉钉机器人 服务启动")
200 |
201 | timestamp = str(round(time.time() * 1000))
202 | secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
203 | string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
204 | string_to_sign_enc = string_to_sign.encode("utf-8")
205 | hmac_code = hmac.new(
206 | secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
207 | ).digest()
208 | sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
209 | url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}'
210 | headers = {"Content-Type": "application/json;charset=utf-8"}
211 | data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
212 | response = requests.post(
213 | url=url, data=json.dumps(data), headers=headers, timeout=15
214 | ).json()
215 |
216 | if not response["errcode"]:
217 | print("钉钉机器人 推送成功!")
218 | else:
219 | print("钉钉机器人 推送失败!")
220 |
221 |
222 | def feishu_bot(title: str, content: str) -> None:
223 | """
224 | 使用 飞书机器人 推送消息。
225 | """
226 | if not push_config.get("FSKEY"):
227 | return
228 | print("飞书 服务启动")
229 |
230 | url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
231 | data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
232 | response = requests.post(url, data=json.dumps(data)).json()
233 |
234 | if response.get("StatusCode") == 0 or response.get("code") == 0:
235 | print("飞书 推送成功!")
236 | else:
237 | print("飞书 推送失败!错误信息如下:\n", response)
238 |
239 |
240 | def go_cqhttp(title: str, content: str) -> None:
241 | """
242 | 使用 go_cqhttp 推送消息。
243 | """
244 | if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
245 | return
246 | print("go-cqhttp 服务启动")
247 |
248 | url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
249 | response = requests.get(url).json()
250 |
251 | if response["status"] == "ok":
252 | print("go-cqhttp 推送成功!")
253 | else:
254 | print("go-cqhttp 推送失败!")
255 |
256 |
257 | def gotify(title: str, content: str) -> None:
258 | """
259 | 使用 gotify 推送消息。
260 | """
261 | if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
262 | return
263 | print("gotify 服务启动")
264 |
265 | url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
266 | data = {
267 | "title": title,
268 | "message": content,
269 | "priority": push_config.get("GOTIFY_PRIORITY"),
270 | }
271 | response = requests.post(url, data=data).json()
272 |
273 | if response.get("id"):
274 | print("gotify 推送成功!")
275 | else:
276 | print("gotify 推送失败!")
277 |
278 |
279 | def iGot(title: str, content: str) -> None:
280 | """
281 | 使用 iGot 推送消息。
282 | """
283 | if not push_config.get("IGOT_PUSH_KEY"):
284 | return
285 | print("iGot 服务启动")
286 |
287 | url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
288 | data = {"title": title, "content": content}
289 | headers = {"Content-Type": "application/x-www-form-urlencoded"}
290 | response = requests.post(url, data=data, headers=headers).json()
291 |
292 | if response["ret"] == 0:
293 | print("iGot 推送成功!")
294 | else:
295 | print(f'iGot 推送失败!{response["errMsg"]}')
296 |
297 |
298 | def serverJ(title: str, content: str) -> None:
299 | """
300 | 通过 serverJ 推送消息。
301 | """
302 | if not push_config.get("PUSH_KEY"):
303 | return
304 | print("serverJ 服务启动")
305 |
306 | data = {"text": title, "desp": content.replace("\n", "\n\n")}
307 |
308 | match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY"))
309 | if match:
310 | num = match.group(1)
311 | url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
312 | else:
313 | url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
314 |
315 | response = requests.post(url, data=data).json()
316 |
317 | if response.get("errno") == 0 or response.get("code") == 0:
318 | print("serverJ 推送成功!")
319 | else:
320 | print(f'serverJ 推送失败!错误码:{response["message"]}')
321 |
322 |
323 | def pushdeer(title: str, content: str) -> None:
324 | """
325 | 通过PushDeer 推送消息
326 | """
327 | if not push_config.get("DEER_KEY"):
328 | return
329 | print("PushDeer 服务启动")
330 | data = {
331 | "text": title,
332 | "desp": content,
333 | "type": "markdown",
334 | "pushkey": push_config.get("DEER_KEY"),
335 | }
336 | url = "https://api2.pushdeer.com/message/push"
337 | if push_config.get("DEER_URL"):
338 | url = push_config.get("DEER_URL")
339 |
340 | response = requests.post(url, data=data).json()
341 |
342 | if len(response.get("content").get("result")) > 0:
343 | print("PushDeer 推送成功!")
344 | else:
345 | print("PushDeer 推送失败!错误信息:", response)
346 |
347 |
348 | def chat(title: str, content: str) -> None:
349 | """
350 | 通过Chat 推送消息
351 | """
352 | if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
353 | return
354 | print("chat 服务启动")
355 | data = "payload=" + json.dumps({"text": title + "\n" + content})
356 | url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
357 | response = requests.post(url, data=data)
358 |
359 | if response.status_code == 200:
360 | print("Chat 推送成功!")
361 | else:
362 | print("Chat 推送失败!错误信息:", response)
363 |
364 |
365 | def pushplus_bot(title: str, content: str) -> None:
366 | """
367 | 通过 pushplus 推送消息。
368 | """
369 | if not push_config.get("PUSH_PLUS_TOKEN"):
370 | return
371 | print("PUSHPLUS 服务启动")
372 |
373 | url = "https://www.pushplus.plus/send"
374 | data = {
375 | "token": push_config.get("PUSH_PLUS_TOKEN"),
376 | "title": title,
377 | "content": content,
378 | "topic": push_config.get("PUSH_PLUS_USER"),
379 | "template": push_config.get("PUSH_PLUS_TEMPLATE"),
380 | "channel": push_config.get("PUSH_PLUS_CHANNEL"),
381 | "webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
382 | "callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
383 | "to": push_config.get("PUSH_PLUS_TO"),
384 | }
385 | body = json.dumps(data).encode(encoding="utf-8")
386 | headers = {"Content-Type": "application/json"}
387 | response = requests.post(url=url, data=body, headers=headers).json()
388 |
389 | code = response["code"]
390 | if code == 200:
391 | print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
392 | print(
393 | "注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
394 | )
395 | elif code == 900 or code == 903 or code == 905 or code == 999:
396 | print(response["msg"])
397 |
398 | else:
399 | url_old = "http://pushplus.hxtrip.com/send"
400 | headers["Accept"] = "application/json"
401 | response = requests.post(url=url_old, data=body, headers=headers).json()
402 |
403 | if response["code"] == 200:
404 | print("PUSHPLUS(hxtrip) 推送成功!")
405 |
406 | else:
407 | print("PUSHPLUS 推送失败!")
408 |
409 |
410 | def weplus_bot(title: str, content: str) -> None:
411 | """
412 | 通过 微加机器人 推送消息。
413 | """
414 | if not push_config.get("WE_PLUS_BOT_TOKEN"):
415 | return
416 | print("微加机器人 服务启动")
417 |
418 | template = "txt"
419 | if len(content) > 800:
420 | template = "html"
421 |
422 | url = "https://www.weplusbot.com/send"
423 | data = {
424 | "token": push_config.get("WE_PLUS_BOT_TOKEN"),
425 | "title": title,
426 | "content": content,
427 | "template": template,
428 | "receiver": push_config.get("WE_PLUS_BOT_RECEIVER"),
429 | "version": push_config.get("WE_PLUS_BOT_VERSION"),
430 | }
431 | body = json.dumps(data).encode(encoding="utf-8")
432 | headers = {"Content-Type": "application/json"}
433 | response = requests.post(url=url, data=body, headers=headers).json()
434 |
435 | if response["code"] == 200:
436 | print("微加机器人 推送成功!")
437 | else:
438 | print("微加机器人 推送失败!")
439 |
440 |
441 | def qmsg_bot(title: str, content: str) -> None:
442 | """
443 | 使用 qmsg 推送消息。
444 | """
445 | if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
446 | return
447 | print("qmsg 服务启动")
448 |
449 | url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
450 | payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
451 | response = requests.post(url=url, params=payload).json()
452 |
453 | if response["code"] == 0:
454 | print("qmsg 推送成功!")
455 | else:
456 | print(f'qmsg 推送失败!{response["reason"]}')
457 |
458 |
459 | def wecom_app(title: str, content: str) -> None:
460 | """
461 | 通过 企业微信 APP 推送消息。
462 | """
463 | if not push_config.get("QYWX_AM"):
464 | return
465 | QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
466 | if 4 < len(QYWX_AM_AY) > 5:
467 | print("QYWX_AM 设置错误!!")
468 | return
469 | print("企业微信 APP 服务启动")
470 |
471 | corpid = QYWX_AM_AY[0]
472 | corpsecret = QYWX_AM_AY[1]
473 | touser = QYWX_AM_AY[2]
474 | agentid = QYWX_AM_AY[3]
475 | try:
476 | media_id = QYWX_AM_AY[4]
477 | except IndexError:
478 | media_id = ""
479 | wx = WeCom(corpid, corpsecret, agentid)
480 | # 如果没有配置 media_id 默认就以 text 方式发送
481 | if not media_id:
482 | message = title + "\n\n" + content
483 | response = wx.send_text(message, touser)
484 | else:
485 | response = wx.send_mpnews(title, content, media_id, touser)
486 |
487 | if response == "ok":
488 | print("企业微信推送成功!")
489 | else:
490 | print("企业微信推送失败!错误信息如下:\n", response)
491 |
492 |
493 | class WeCom:
494 | def __init__(self, corpid, corpsecret, agentid):
495 | self.CORPID = corpid
496 | self.CORPSECRET = corpsecret
497 | self.AGENTID = agentid
498 | self.ORIGIN = "https://qyapi.weixin.qq.com"
499 | if push_config.get("QYWX_ORIGIN"):
500 | self.ORIGIN = push_config.get("QYWX_ORIGIN")
501 |
502 | def get_access_token(self):
503 | url = f"{self.ORIGIN}/cgi-bin/gettoken"
504 | values = {
505 | "corpid": self.CORPID,
506 | "corpsecret": self.CORPSECRET,
507 | }
508 | req = requests.post(url, params=values)
509 | data = json.loads(req.text)
510 | return data["access_token"]
511 |
512 | def send_text(self, message, touser="@all"):
513 | send_url = (
514 | f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
515 | )
516 | send_values = {
517 | "touser": touser,
518 | "msgtype": "text",
519 | "agentid": self.AGENTID,
520 | "text": {"content": message},
521 | "safe": "0",
522 | }
523 | send_msges = bytes(json.dumps(send_values), "utf-8")
524 | respone = requests.post(send_url, send_msges)
525 | respone = respone.json()
526 | return respone["errmsg"]
527 |
528 | def send_mpnews(self, title, message, media_id, touser="@all"):
529 | send_url = (
530 | f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
531 | )
532 | send_values = {
533 | "touser": touser,
534 | "msgtype": "mpnews",
535 | "agentid": self.AGENTID,
536 | "mpnews": {
537 | "articles": [
538 | {
539 | "title": title,
540 | "thumb_media_id": media_id,
541 | "author": "Author",
542 | "content_source_url": "",
543 | "content": message.replace("\n", "
"),
544 | "digest": message,
545 | }
546 | ]
547 | },
548 | }
549 | send_msges = bytes(json.dumps(send_values), "utf-8")
550 | respone = requests.post(send_url, send_msges)
551 | respone = respone.json()
552 | return respone["errmsg"]
553 |
554 |
555 | def wecom_bot(title: str, content: str) -> None:
556 | """
557 | 通过 企业微信机器人 推送消息。
558 | """
559 | if not push_config.get("QYWX_KEY"):
560 | return
561 | print("企业微信机器人服务启动")
562 |
563 | origin = "https://qyapi.weixin.qq.com"
564 | if push_config.get("QYWX_ORIGIN"):
565 | origin = push_config.get("QYWX_ORIGIN")
566 |
567 | url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
568 | headers = {"Content-Type": "application/json;charset=utf-8"}
569 | data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
570 | response = requests.post(
571 | url=url, data=json.dumps(data), headers=headers, timeout=15
572 | ).json()
573 |
574 | if response["errcode"] == 0:
575 | print("企业微信机器人推送成功!")
576 | else:
577 | print("企业微信机器人推送失败!")
578 |
579 |
580 | def telegram_bot(title: str, content: str) -> None:
581 | """
582 | 使用 telegram 机器人 推送消息。
583 | """
584 | if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
585 | return
586 | print("tg 服务启动")
587 |
588 | if push_config.get("TG_API_HOST"):
589 | url = f"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
590 | else:
591 | url = (
592 | f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
593 | )
594 | headers = {"Content-Type": "application/x-www-form-urlencoded"}
595 | payload = {
596 | "chat_id": str(push_config.get("TG_USER_ID")),
597 | "text": f"{title}\n\n{content}",
598 | "disable_web_page_preview": "true",
599 | }
600 | proxies = None
601 | if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
602 | if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
603 | "TG_PROXY_HOST"
604 | ):
605 | push_config["TG_PROXY_HOST"] = (
606 | push_config.get("TG_PROXY_AUTH")
607 | + "@"
608 | + push_config.get("TG_PROXY_HOST")
609 | )
610 | proxyStr = "http://{}:{}".format(
611 | push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
612 | )
613 | proxies = {"http": proxyStr, "https": proxyStr}
614 | response = requests.post(
615 | url=url, headers=headers, params=payload, proxies=proxies
616 | ).json()
617 |
618 | if response["ok"]:
619 | print("tg 推送成功!")
620 | else:
621 | print("tg 推送失败!")
622 |
623 |
624 | def aibotk(title: str, content: str) -> None:
625 | """
626 | 使用 智能微秘书 推送消息。
627 | """
628 | if (
629 | not push_config.get("AIBOTK_KEY")
630 | or not push_config.get("AIBOTK_TYPE")
631 | or not push_config.get("AIBOTK_NAME")
632 | ):
633 | return
634 | print("智能微秘书 服务启动")
635 |
636 | if push_config.get("AIBOTK_TYPE") == "room":
637 | url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
638 | data = {
639 | "apiKey": push_config.get("AIBOTK_KEY"),
640 | "roomName": push_config.get("AIBOTK_NAME"),
641 | "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
642 | }
643 | else:
644 | url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
645 | data = {
646 | "apiKey": push_config.get("AIBOTK_KEY"),
647 | "name": push_config.get("AIBOTK_NAME"),
648 | "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
649 | }
650 | body = json.dumps(data).encode(encoding="utf-8")
651 | headers = {"Content-Type": "application/json"}
652 | response = requests.post(url=url, data=body, headers=headers).json()
653 | print(response)
654 | if response["code"] == 0:
655 | print("智能微秘书 推送成功!")
656 | else:
657 | print(f'智能微秘书 推送失败!{response["error"]}')
658 |
659 |
660 | def smtp(title: str, content: str) -> None:
661 | """
662 | 使用 SMTP 邮件 推送消息。
663 | """
664 | if (
665 | not push_config.get("SMTP_SERVER")
666 | or not push_config.get("SMTP_SSL")
667 | or not push_config.get("SMTP_EMAIL")
668 | or not push_config.get("SMTP_PASSWORD")
669 | or not push_config.get("SMTP_NAME")
670 | ):
671 | return
672 | print("SMTP 邮件 服务启动")
673 |
674 | message = MIMEText(content, "plain", "utf-8")
675 | message["From"] = formataddr(
676 | (
677 | Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
678 | push_config.get("SMTP_EMAIL"),
679 | )
680 | )
681 | message["To"] = formataddr(
682 | (
683 | Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
684 | push_config.get("SMTP_EMAIL"),
685 | )
686 | )
687 | message["Subject"] = Header(title, "utf-8")
688 |
689 | try:
690 | smtp_server = (
691 | smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
692 | if push_config.get("SMTP_SSL") == "true"
693 | else smtplib.SMTP(push_config.get("SMTP_SERVER"))
694 | )
695 | smtp_server.login(
696 | push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
697 | )
698 | smtp_server.sendmail(
699 | push_config.get("SMTP_EMAIL"),
700 | push_config.get("SMTP_EMAIL"),
701 | message.as_bytes(),
702 | )
703 | smtp_server.close()
704 | print("SMTP 邮件 推送成功!")
705 | except Exception as e:
706 | print(f"SMTP 邮件 推送失败!{e}")
707 |
708 |
709 | def pushme(title: str, content: str) -> None:
710 | """
711 | 使用 PushMe 推送消息。
712 | """
713 | if not push_config.get("PUSHME_KEY"):
714 | return
715 | print("PushMe 服务启动")
716 |
717 | url = (
718 | push_config.get("PUSHME_URL")
719 | if push_config.get("PUSHME_URL")
720 | else "https://push.i-i.me/"
721 | )
722 | data = {
723 | "push_key": push_config.get("PUSHME_KEY"),
724 | "title": title,
725 | "content": content,
726 | "date": push_config.get("date") if push_config.get("date") else "",
727 | "type": push_config.get("type") if push_config.get("type") else "",
728 | }
729 | response = requests.post(url, data=data)
730 |
731 | if response.status_code == 200 and response.text == "success":
732 | print("PushMe 推送成功!")
733 | else:
734 | print(f"PushMe 推送失败!{response.status_code} {response.text}")
735 |
736 |
737 | def chronocat(title: str, content: str) -> None:
738 | """
739 | 使用 CHRONOCAT 推送消息。
740 | """
741 | if (
742 | not push_config.get("CHRONOCAT_URL")
743 | or not push_config.get("CHRONOCAT_QQ")
744 | or not push_config.get("CHRONOCAT_TOKEN")
745 | ):
746 | return
747 |
748 | print("CHRONOCAT 服务启动")
749 |
750 | user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
751 | group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
752 |
753 | url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
754 | headers = {
755 | "Content-Type": "application/json",
756 | "Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}',
757 | }
758 |
759 | for chat_type, ids in [(1, user_ids), (2, group_ids)]:
760 | if not ids:
761 | continue
762 | for chat_id in ids:
763 | data = {
764 | "peer": {"chatType": chat_type, "peerUin": chat_id},
765 | "elements": [
766 | {
767 | "elementType": 1,
768 | "textElement": {"content": f"{title}\n\n{content}"},
769 | }
770 | ],
771 | }
772 | response = requests.post(url, headers=headers, data=json.dumps(data))
773 | if response.status_code == 200:
774 | if chat_type == 1:
775 | print(f"QQ个人消息:{ids}推送成功!")
776 | else:
777 | print(f"QQ群消息:{ids}推送成功!")
778 | else:
779 | if chat_type == 1:
780 | print(f"QQ个人消息:{ids}推送失败!")
781 | else:
782 | print(f"QQ群消息:{ids}推送失败!")
783 |
784 |
785 | def ntfy(title: str, content: str) -> None:
786 | """
787 | 通过 Ntfy 推送消息
788 | """
789 |
790 | def encode_rfc2047(text: str) -> str:
791 | """将文本编码为符合 RFC 2047 标准的格式"""
792 | encoded_bytes = base64.b64encode(text.encode("utf-8"))
793 | encoded_str = encoded_bytes.decode("utf-8")
794 | return f"=?utf-8?B?{encoded_str}?="
795 |
796 | if not push_config.get("NTFY_TOPIC"):
797 | return
798 | print("ntfy 服务启动")
799 | priority = "3"
800 | if not push_config.get("NTFY_PRIORITY"):
801 | print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
802 | else:
803 | priority = push_config.get("NTFY_PRIORITY")
804 |
805 | # 使用 RFC 2047 编码 title
806 | encoded_title = encode_rfc2047(title)
807 |
808 | data = content.encode(encoding="utf-8")
809 | headers = {"Title": encoded_title, "Priority": priority} # 使用编码后的 title
810 |
811 | url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
812 | response = requests.post(url, data=data, headers=headers)
813 | if response.status_code == 200: # 使用 response.status_code 进行检查
814 | print("Ntfy 推送成功!")
815 | else:
816 | print("Ntfy 推送失败!错误信息:", response.text)
817 |
818 |
819 | def wxpusher_bot(title: str, content: str) -> None:
820 | """
821 | 通过 wxpusher 推送消息。
822 | 支持的环境变量:
823 | - WXPUSHER_APP_TOKEN: appToken
824 | - WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
825 | - WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
826 | """
827 | if not push_config.get("WXPUSHER_APP_TOKEN"):
828 | return
829 |
830 | url = "https://wxpusher.zjiecode.com/api/send/message"
831 |
832 | # 处理topic_ids和uids,将分号分隔的字符串转为数组
833 | topic_ids = []
834 | if push_config.get("WXPUSHER_TOPIC_IDS"):
835 | topic_ids = [
836 | int(id.strip())
837 | for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
838 | if id.strip()
839 | ]
840 |
841 | uids = []
842 | if push_config.get("WXPUSHER_UIDS"):
843 | uids = [
844 | uid.strip()
845 | for uid in push_config.get("WXPUSHER_UIDS").split(";")
846 | if uid.strip()
847 | ]
848 |
849 | # topic_ids uids 至少有一个
850 | if not topic_ids and not uids:
851 | print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
852 | return
853 |
854 | print("wxpusher 服务启动")
855 |
856 | data = {
857 | "appToken": push_config.get("WXPUSHER_APP_TOKEN"),
858 | "content": f"