├── .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"

{title}


{content}
", 859 | "summary": title, 860 | "contentType": 2, 861 | "topicIds": topic_ids, 862 | "uids": uids, 863 | "verifyPayType": 0, 864 | } 865 | 866 | headers = {"Content-Type": "application/json"} 867 | response = requests.post(url=url, json=data, headers=headers).json() 868 | 869 | if response.get("code") == 1000: 870 | print("wxpusher 推送成功!") 871 | else: 872 | print(f"wxpusher 推送失败!错误信息:{response.get('msg')}") 873 | 874 | 875 | def parse_headers(headers): 876 | if not headers: 877 | return {} 878 | 879 | parsed = {} 880 | lines = headers.split("\n") 881 | 882 | for line in lines: 883 | i = line.find(":") 884 | if i == -1: 885 | continue 886 | 887 | key = line[:i].strip().lower() 888 | val = line[i + 1 :].strip() 889 | parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val 890 | 891 | return parsed 892 | 893 | 894 | def parse_string(input_string, value_format_fn=None): 895 | matches = {} 896 | pattern = r"(\w+):\s*((?:(?!\n\w+:).)*)" 897 | regex = re.compile(pattern) 898 | for match in regex.finditer(input_string): 899 | key, value = match.group(1).strip(), match.group(2).strip() 900 | try: 901 | value = value_format_fn(value) if value_format_fn else value 902 | json_value = json.loads(value) 903 | matches[key] = json_value 904 | except: 905 | matches[key] = value 906 | return matches 907 | 908 | 909 | def parse_body(body, content_type, value_format_fn=None): 910 | if not body or content_type == "text/plain": 911 | return value_format_fn(body) if value_format_fn and body else body 912 | 913 | parsed = parse_string(body, value_format_fn) 914 | 915 | if content_type == "application/x-www-form-urlencoded": 916 | data = urllib.parse.urlencode(parsed, doseq=True) 917 | return data 918 | 919 | if content_type == "application/json": 920 | data = json.dumps(parsed) 921 | return data 922 | 923 | return parsed 924 | 925 | 926 | def custom_notify(title: str, content: str) -> None: 927 | """ 928 | 通过 自定义通知 推送消息。 929 | """ 930 | if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"): 931 | return 932 | 933 | print("自定义通知服务启动") 934 | 935 | WEBHOOK_URL = push_config.get("WEBHOOK_URL") 936 | WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD") 937 | WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE") 938 | WEBHOOK_BODY = push_config.get("WEBHOOK_BODY") 939 | WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS") 940 | 941 | if "$title" not in WEBHOOK_URL and "$title" not in WEBHOOK_BODY: 942 | print("请求头或者请求体中必须包含 $title 和 $content") 943 | return 944 | 945 | headers = parse_headers(WEBHOOK_HEADERS) 946 | body = parse_body( 947 | WEBHOOK_BODY, 948 | WEBHOOK_CONTENT_TYPE, 949 | lambda v: v.replace("$title", title.replace("\n", "\\n")).replace( 950 | "$content", content.replace("\n", "\\n") 951 | ), 952 | ) 953 | formatted_url = WEBHOOK_URL.replace( 954 | "$title", urllib.parse.quote_plus(title) 955 | ).replace("$content", urllib.parse.quote_plus(content)) 956 | response = requests.request( 957 | method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body 958 | ) 959 | 960 | if response.status_code == 200: 961 | print("自定义通知推送成功!") 962 | else: 963 | print(f"自定义通知推送失败!{response.status_code} {response.text}") 964 | 965 | 966 | #def one() -> str: 967 | """ 968 | 获取一条一言。 969 | :return: 970 | """ 971 | # url = "https://v1.hitokoto.cn/" 972 | # res = requests.get(url).json() 973 | # return res["hitokoto"] + " ----" + res["from"] 974 | 975 | 976 | def add_notify_function(): 977 | notify_function = [] 978 | if push_config.get("BARK_PUSH"): 979 | notify_function.append(bark) 980 | if push_config.get("CONSOLE"): 981 | notify_function.append(console) 982 | if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"): 983 | notify_function.append(dingding_bot) 984 | if push_config.get("FSKEY"): 985 | notify_function.append(feishu_bot) 986 | if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"): 987 | notify_function.append(go_cqhttp) 988 | if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"): 989 | notify_function.append(gotify) 990 | if push_config.get("IGOT_PUSH_KEY"): 991 | notify_function.append(iGot) 992 | if push_config.get("PUSH_KEY"): 993 | notify_function.append(serverJ) 994 | if push_config.get("DEER_KEY"): 995 | notify_function.append(pushdeer) 996 | if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"): 997 | notify_function.append(chat) 998 | if push_config.get("PUSH_PLUS_TOKEN"): 999 | notify_function.append(pushplus_bot) 1000 | if push_config.get("WE_PLUS_BOT_TOKEN"): 1001 | notify_function.append(weplus_bot) 1002 | if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): 1003 | notify_function.append(qmsg_bot) 1004 | if push_config.get("QYWX_AM"): 1005 | notify_function.append(wecom_app) 1006 | if push_config.get("QYWX_KEY"): 1007 | notify_function.append(wecom_bot) 1008 | if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): 1009 | notify_function.append(telegram_bot) 1010 | if ( 1011 | push_config.get("AIBOTK_KEY") 1012 | and push_config.get("AIBOTK_TYPE") 1013 | and push_config.get("AIBOTK_NAME") 1014 | ): 1015 | notify_function.append(aibotk) 1016 | if ( 1017 | push_config.get("SMTP_SERVER") 1018 | and push_config.get("SMTP_SSL") 1019 | and push_config.get("SMTP_EMAIL") 1020 | and push_config.get("SMTP_PASSWORD") 1021 | and push_config.get("SMTP_NAME") 1022 | ): 1023 | notify_function.append(smtp) 1024 | if push_config.get("PUSHME_KEY"): 1025 | notify_function.append(pushme) 1026 | if ( 1027 | push_config.get("CHRONOCAT_URL") 1028 | and push_config.get("CHRONOCAT_QQ") 1029 | and push_config.get("CHRONOCAT_TOKEN") 1030 | ): 1031 | notify_function.append(chronocat) 1032 | if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"): 1033 | notify_function.append(custom_notify) 1034 | if push_config.get("NTFY_TOPIC"): 1035 | notify_function.append(ntfy) 1036 | if push_config.get("WXPUSHER_APP_TOKEN") and ( 1037 | push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS") 1038 | ): 1039 | notify_function.append(wxpusher_bot) 1040 | if not notify_function: 1041 | print(f"无推送渠道,请检查通知变量是否正确") 1042 | return notify_function 1043 | 1044 | 1045 | def send(title: str, content: str, ignore_default_config: bool = False, **kwargs): 1046 | if kwargs: 1047 | global push_config 1048 | if ignore_default_config: 1049 | push_config = kwargs # 清空从环境变量获取的配置 1050 | else: 1051 | push_config.update(kwargs) 1052 | 1053 | if not content: 1054 | print(f"{title} 推送内容为空!") 1055 | return 1056 | 1057 | # 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 1058 | skipTitle = os.getenv("SKIP_PUSH_TITLE") 1059 | if skipTitle: 1060 | if title in re.split("\n", skipTitle): 1061 | print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!") 1062 | return 1063 | 1064 | # hitokoto = push_config.get("HITOKOTO") 1065 | # content += "\n\n" + one() if hitokoto != "False" else "" 1066 | 1067 | notify_function = add_notify_function() 1068 | ts = [ 1069 | threading.Thread(target=mode, args=(title, content), name=mode.__name__) 1070 | for mode in notify_function 1071 | ] 1072 | [t.start() for t in ts] 1073 | [t.join() for t in ts] 1074 | 1075 | 1076 | def main(): 1077 | send("title", "content") 1078 | 1079 | 1080 | if __name__ == "__main__": 1081 | main() 1082 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium>=4.18.1 2 | requests>=2.31.0 3 | curl_cffi>=0.6.2 4 | beautifulsoup4>=4.12.3 5 | undetected-chromedriver>=3.5.5 6 | webdriver_manager>=4.0.1 7 | dashscope 8 | ImageHash 9 | pytesseract 10 | Pillow 11 | -------------------------------------------------------------------------------- /sfsy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # ------------------------------- 3 | # @Author : github@wh1te3zzz https://github.com/wh1te3zzz/checkin 4 | # @Time : 2025-06-19 09:06:56 5 | # 顺丰速运任务脚本 6 | # ------------------------------- 7 | """ 8 | 顺丰任务 9 | 自行app捉包提取请求头中的url,多账号换行隔开 10 | export sfsyUrl=https://mcs-mimp-web.sf-express.com/mcs-mimp/share/weChat/shareGiftReceiveRedirect?xxxx=xxxx&xxxxx; 11 | 12 | cron: 15 8,16 * * * 13 | const $ = new Env("顺丰速运"); 14 | """ 15 | import asyncio 16 | import hashlib 17 | import os 18 | import time 19 | import httpx 20 | 21 | class SFExpress: 22 | """ 23 | 顺丰速运任务 24 | """ 25 | 26 | def __init__(self, url, timeout, proxy_url=None): 27 | self.base_url = 'https://mcs-mimp-web.sf-express.com' 28 | self.login_url = url 29 | headers = { 30 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " 31 | "Chrome/116.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI " 32 | "MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090b11)XWEB/9185", 33 | "content-type": "application/json", 34 | "accept": "application/json, text/plain, */*", 35 | "platform": "MINI_PROGRAM", 36 | "syscode": "MCS-MIMP-CORE", 37 | "channel": "wxwd26mem1", 38 | } 39 | if proxy_url: 40 | self.client = httpx.AsyncClient(http2=True, headers=headers, proxy=proxy_url) 41 | else: 42 | self.client = httpx.AsyncClient(http2=True, headers=headers) 43 | self.timeout = timeout 44 | 45 | def signature(self): 46 | token = 'wwesldfs29aniversaryvdld29' 47 | timestamp = str(int(time.time() * 1000)) 48 | text = "token=" + token + "×tamp=" + timestamp + "&sysCode=" + self.client.headers.get("syscode") 49 | signature = hashlib.md5(text.encode()).hexdigest() 50 | self.client.headers.update({'timestamp': timestamp, 'signature': signature}) 51 | 52 | async def sign(self): 53 | 54 | data = await self.post( 55 | '/mcs-mimp/commonPost/~memberNonactivity~integralTaskSignPlusService~automaticSignFetchPackage', 56 | {"comeFrom": "vioin", "channelFrom": "WEIXIN"}) 57 | if data['success']: 58 | print(f'已连续签到{data["countDay"]}天!') 59 | else: 60 | print("签到失败") 61 | 62 | async def post(self, path, params=None): 63 | self.signature() 64 | try: 65 | if params is None: 66 | params = {} 67 | url = f'{self.base_url}{path}' 68 | response = await self.client.post(url, json=params) 69 | data = response.json() 70 | if not data['success']: 71 | return data 72 | data = data['obj'] 73 | if isinstance(data, dict): 74 | data['success'] = True 75 | else: 76 | data = {'data': data, 'success': True} 77 | return data 78 | except Exception as e: 79 | print(e.args) 80 | return { 81 | "success": False, 82 | "errorMessage": "POST请求异常!" 83 | } 84 | 85 | async def login(self): 86 | await self.client.get(self.login_url) 87 | data = await self.post('/mcs-mimp/ifLogin') 88 | if not data['success']: 89 | return -1 90 | return data['loginStatus'] 91 | 92 | async def get_user_info(self): 93 | """ 94 | :return: 95 | """ 96 | data = await self.post('/mcs-mimp/commonPost/~memberIntegral~userInfoService~personalInfoNew') 97 | return data 98 | 99 | async def get_member_day_task(self): 100 | """ 101 | :return: 102 | """ 103 | params = { 104 | "activityCode": "MEMBER_DAY", 105 | "channelType": "MINI_PROGRAM" 106 | } 107 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~activityTaskService~taskList', params) 108 | return data 109 | 110 | async def get_member_day_lottery_info(self): 111 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~memberDayIndexService~index') 112 | return data 113 | 114 | async def member_day_lottery(self): 115 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~memberDayLotteryService~lottery') 116 | return data 117 | 118 | async def get_task_list(self): 119 | params = { 120 | "channelType": "3", 121 | "deviceId": "0e88cd05-4785-b232" 122 | } 123 | data = await self.post( 124 | '/mcs-mimp/commonPost/~memberNonactivity~integralTaskStrategyService~queryPointTaskAndSignFromES', params) 125 | return data 126 | 127 | async def finish_task(self, task_code): 128 | data = await self.post('/mcs-mimp/commonRoutePost/memberEs/taskRecord/finishTask', { 129 | "taskCode": task_code 130 | }) 131 | return data 132 | 133 | async def fetch_award(self, task): 134 | """ 135 | :return: 136 | """ 137 | params = { 138 | "strategyId": task['strategyId'], 139 | "taskId": task['taskId'], 140 | "taskCode": task['taskCode'], 141 | "channelType": "3", 142 | "deviceId": "0e88cd05-4785-b232" 143 | } 144 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~integralTaskStrategyService~fetchIntegral', 145 | params) 146 | return data 147 | 148 | async def receive_welfare(self, goods_no, task_code): 149 | """ 150 | :return: 151 | """ 152 | params = { 153 | "from": "Point_Mall", 154 | "orderSource": "POINT_MALL_EXCHANGE", 155 | "goodsNo": goods_no, 156 | "quantity": 1, 157 | "taskCode": task_code 158 | } 159 | data = await self.post('/mcs-mimp/commonPost/~memberGoods~pointMallService~createOrder', params) 160 | return data 161 | 162 | async def get_bee_task_list(self): 163 | """ 164 | :return: 165 | """ 166 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~receiveExchangeIndexService~taskDetail') 167 | return data 168 | 169 | async def bee_finsh_task(self, task_code): 170 | params = { 171 | "taskCode": task_code 172 | } 173 | data = await self.post('/mcs-mimp/commonPost/~memberEs~taskRecord~finishTask', params) 174 | return data 175 | 176 | async def get_bee_index_info(self): 177 | """ 178 | :return: 179 | """ 180 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~receiveExchangeIndexService~indexData') 181 | return data 182 | 183 | async def bee_receive_honey(self, task_type): 184 | params = { 185 | "taskType": task_type 186 | } 187 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~receiveExchangeIndexService~receiveHoney', 188 | params) 189 | return data 190 | 191 | async def get_goods_no(self): 192 | """ 193 | :return: 194 | """ 195 | params = { 196 | "memGrade": 2, 197 | "categoryCode": "SHTQ", 198 | "showCode": "SHTQWNTJ" 199 | } 200 | data = await self.post('/mcs-mimp/commonPost/~memberGoods~mallGoodsLifeService~list', params) 201 | for item in data['data']: 202 | for goods in item['goodsList']: 203 | if goods['currentStore'] > 0: 204 | return goods['goodsNo'] 205 | return '' 206 | 207 | async def bee_game_report(self): 208 | params = { 209 | "gatherHoney": 20 210 | } 211 | data = await self.post('/mcs-mimp/commonPost/~memberNonactivity~receiveExchangeGameService~gameReport', params) 212 | return data 213 | 214 | async def run(self): 215 | login_status = await self.login() 216 | if login_status != 1: 217 | print("登录失败, 跳过该账号!") 218 | await self.client.aclose() 219 | return 220 | 221 | time.sleep(self.timeout) 222 | user_info = await self.get_user_info() 223 | if not user_info['success']: 224 | print("获取用户信息失败, 跳过该账号!") 225 | return 226 | 227 | wechat_name = user_info.get('weChatName', '微信昵称未知') 228 | level_name = user_info.get('levelName', '') 229 | points = user_info.get('availablePoints', '0') 230 | print(f'用户昵称:{wechat_name}\n用户等级:{level_name}\n可用积分:{points}') 231 | 232 | time.sleep(self.timeout) 233 | 234 | await self.sign() 235 | time.sleep(self.timeout) 236 | 237 | data = await self.get_member_day_lottery_info() 238 | lottery_num = data.get('lotteryNum', 0) 239 | print(f'26会员日抽奖次数为: {lottery_num}') 240 | for i in range(lottery_num): 241 | data = await self.member_day_lottery() 242 | print('26会员日抽奖结果:', data) 243 | time.sleep(self.timeout) 244 | 245 | data = await self.get_task_list() 246 | task_list = data['taskTitleLevels'] 247 | for task in task_list: 248 | 249 | if task['status'] == 3: 250 | print(f'今日已完成任务:《{task["title"]}》') 251 | continue 252 | if '积分兑' in task["title"] or '寄件下单' in task["title"] or '参与积分活动' in task['title']: 253 | print(f'跳过任务:《{task["title"]}》') 254 | continue 255 | 256 | if '领任意生活特权福利' in task['title']: 257 | goods_no = await self.get_goods_no() 258 | data = await self.receive_welfare(goods_no, task['taskCode']) 259 | else: 260 | data = await self.finish_task(task['taskCode']) 261 | 262 | time.sleep(self.timeout) 263 | 264 | if data['success']: 265 | print(f'成功完成任务:《{task["title"]}》') 266 | data = await self.fetch_award(task) 267 | if data['success']: 268 | print(f'成功领取任务:《{task["title"]}》奖励, 获得{data["point"]}积分') 269 | else: 270 | print(f'无法领取任务:《{task["title"]}》奖励') 271 | else: 272 | print(f'无法完成任务:《{task["title"]}》') 273 | 274 | time.sleep(self.timeout) 275 | 276 | data = await self.get_bee_task_list() 277 | if not data['success']: 278 | print('获取采蜜任务列表失败, 退出!') 279 | return 280 | 281 | for task in data['list']: 282 | if task['status'] == 3: 283 | continue 284 | if task['taskType'] == 'DAILY_VIP_TASK_TYPE': 285 | goods_no = await self.get_goods_no() 286 | time.sleep(self.timeout) 287 | data = await self.receive_welfare(goods_no, task['taskCode']) 288 | if not data['success']: 289 | print("无法完成任务:《领取生活特权》!") 290 | continue 291 | print('成功完成任务:《领取生活特权》!') 292 | data = await self.bee_receive_honey(task['taskType']) 293 | print(data) 294 | 295 | if task['taskType'] == 'BROWSER_CENTER_TASK_TYPE': 296 | data = await self.bee_finsh_task(task['taskCode']) 297 | time.sleep(self.timeout) 298 | if not data['success']: 299 | print('无法完成任务:《浏览会员中心10秒!》') 300 | continue 301 | print("成功完成任务:《浏览会员中心10秒》") 302 | data = await self.bee_receive_honey(task['taskType']) 303 | print(data) 304 | 305 | if task['taskType'] == 'BEES_GAME_TASK_TYPE' and task['count'] < 3: 306 | for i in range(3 - task['count']): 307 | data = await self.bee_game_report() 308 | print(f'完成第{i + 1}次采蜜大冒险任务:', data) 309 | time.sleep(self.timeout) 310 | 311 | time.sleep(self.timeout) 312 | 313 | user_info = await self.get_user_info() 314 | wechat_name = user_info.get('weChatName', '微信昵称未知') 315 | level_name = user_info.get('levelName', '') 316 | points = user_info.get('availablePoints', '0') 317 | 318 | time.sleep(self.timeout) 319 | data = await self.get_bee_index_info() 320 | if not data['success']: 321 | print('获取蜂蜜信息失败!') 322 | return 323 | capacity = data.get('capacity', '未知') 324 | usable_honey = data.get('usableHoney', '未知') 325 | print(f'蜜罐容量:{capacity}ml, 当前可用:{usable_honey}ml!\n\n') 326 | template = '=' * 20 + f'\n用户昵称:{wechat_name}\n用户等级:{level_name}\n用户积分:{points}\n蜜罐容量:{capacity}ml\n可用蜂蜜:{usable_honey}ml\n\n' 327 | return template 328 | 329 | 330 | async def main(): 331 | sfsy_url = os.environ.get('sfsyUrl', '') 332 | sfsy_url_list = sfsy_url.split('\n') 333 | sfsy_url_list = [i for i in sfsy_url_list if i] 334 | timeout = int(os.environ.get('sfsyTimeout', 500)) / 1000 335 | proxy_url = os.environ.get('ProxyUrl', None) 336 | 337 | if len(sfsy_url_list) < 1: 338 | print('未找到账号信息, 请先配置环境变量sfsyUrl后再来!') 339 | return 340 | 341 | print(f'检测到{len(sfsy_url_list)}个账号, 开始执行任务!') 342 | if proxy_url: 343 | print(f"当前使用代理:{proxy_url}") 344 | notify_msg = '' 345 | for url in sfsy_url_list: 346 | try: 347 | app = SFExpress(url, timeout, proxy_url) 348 | template = await app.run() 349 | notify_msg += "\n" + template 350 | except Exception as e: 351 | print(e.args) 352 | 353 | from notify import send 354 | send("顺丰速运", notify_msg) 355 | 356 | 357 | if __name__ == '__main__': 358 | asyncio.run(main()) 359 | --------------------------------------------------------------------------------