标签 自动化脚本 下的文章

https://linux.do/t/topic/1465569?u=yeahhe




📌 转载信息
原作者: yeahhe
转载时间: 2026/1/25 08:05:53

接:雨云无限白嫖 FRP 服务器攻略(无 aff)

@fatekey 大佬的方案是单独开一个容器,我想着反正我有青龙面板在跑脚本,站在巨人的肩膀上顺手写了一个,把多账户也写进来了

一、运行效果

先看实际运行的效果吧
【前两个账号运行已签到,仅测试多账户切换功能,临时注册的第三个号用于测试签到功能】

## 开始执行... 2026-01-23 16:57:53

2026-01-23 16:57:54.116950139 [W:onnxruntime:Default, cpuid_info.cc:91 LogEarlyWarning] Unknown CPU vendor. cpuinfo_vendor value: 16
2026-01-23 16:57:54,191 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,191 - INFO - 雨云签到工具 by SerendipityR ~
2026-01-23 16:57:54,191 - INFO - Github发布页: https://github.com/SerendipityR-2022/Rainyun-Qiandao
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO - 雨云签到工具容器版 by fatekey ~
2026-01-23 16:57:54,192 - INFO - Github发布页: https://github.com/fatekey/Rainyun-Qiandao
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO -                    项目为二次开发青龙脚本化运行
2026-01-23 16:57:54,192 - INFO -                      本项目基于上述项目开发
2026-01-23 16:57:54,192 - INFO -                 本项目仅作为学习参考,请勿用于其他用途
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO - ✅ 成功解析3个账号
2026-01-23 16:57:54,192 - INFO - 
================= 处理第1个账号 ==================
2026-01-23 16:57:54,192 - INFO - 
========== 开始处理账号:TACGN ==========
2026-01-23 16:57:54,192 - INFO - ⏳ 随机延时 0 分钟 35 秒
2026-01-23 16:58:29,794 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 16:58:29,809 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 16:58:29,809 - INFO - ⏳ 发起登录请求
2026-01-23 16:58:29,810 - INFO - 🌐 访问雨云登录页
2026-01-23 16:58:31,258 - INFO - 页面标题:登录 | 雨云
2026-01-23 16:58:31,258 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 16:58:31,376 - INFO - 📝 输入账号密码
2026-01-23 16:58:31,775 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 16:58:54,899 - INFO - ✅ 未触发登录验证码
2026-01-23 16:58:59,906 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 16:58:59,910 - INFO - 页面标题: 总览 | 雨云
2026-01-23 16:58:59,940 - INFO - ✅ 账号登录成功:TACGN
2026-01-23 16:58:59,940 - INFO - 🌐 访问赚取积分页
2026-01-23 16:59:00,958 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 16:59:00,962 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 16:59:00,962 - INFO - 🔍 查找每日签到按钮
2026-01-23 16:59:01,016 - INFO - 📌 签到状态:已完成,无需重复签到
2026-01-23 16:59:01,047 - INFO - 💰 当前积分:700(约0.35元)
2026-01-23 16:59:01,131 - INFO - ✅ 账号TACGN浏览器已关闭
2026-01-23 16:59:01,131 - INFO - ✅ 临时文件清理完成
2026-01-23 16:59:01,131 - INFO - 
========== 账号TACGN处理完成 ==========

2026-01-23 16:59:05,996 - INFO - 
================= 处理第2个账号 ==================
2026-01-23 16:59:05,996 - INFO - 
========== 开始处理账号:ACGN_T ==========
2026-01-23 16:59:05,996 - INFO - ⏳ 随机延时 4 分钟 3 秒
2026-01-23 17:03:09,549 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 17:03:09,564 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 17:03:09,564 - INFO - ⏳ 发起登录请求
2026-01-23 17:03:09,564 - INFO - 🌐 访问雨云登录页
2026-01-23 17:03:11,015 - INFO - 页面标题:登录 | 雨云
2026-01-23 17:03:11,016 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 17:03:11,155 - INFO - 📝 输入账号密码
2026-01-23 17:03:11,584 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 17:03:34,692 - INFO - ✅ 未触发登录验证码
2026-01-23 17:03:39,699 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 17:03:39,703 - INFO - 页面标题: 总览 | 雨云
2026-01-23 17:03:39,733 - INFO - ✅ 账号登录成功:ACGN_T
2026-01-23 17:03:39,733 - INFO - 🌐 访问赚取积分页
2026-01-23 17:03:40,777 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 17:03:40,782 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 17:03:40,783 - INFO - 🔍 查找每日签到按钮
2026-01-23 17:03:40,858 - INFO - 📌 签到状态:已完成,无需重复签到
2026-01-23 17:03:40,881 - INFO - 💰 当前积分:4684(约2.34元)
2026-01-23 17:03:40,966 - INFO - ✅ 账号ACGN_T浏览器已关闭
2026-01-23 17:03:40,966 - INFO - ✅ 临时文件清理完成
2026-01-23 17:03:40,966 - INFO - 
========== 账号ACGN_T处理完成 ==========

2026-01-23 17:03:43,883 - INFO - 
================= 处理第3个账号 ==================
2026-01-23 17:03:43,883 - INFO - 
========== 开始处理账号:ACGN ==========
2026-01-23 17:03:43,883 - INFO - ⏳ 随机延时 4 分钟 50 秒
2026-01-23 17:08:34,684 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 17:08:34,698 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 17:08:34,699 - INFO - ⏳ 发起登录请求
2026-01-23 17:08:34,699 - INFO - 🌐 访问雨云登录页
2026-01-23 17:08:36,143 - INFO - 页面标题:登录 | 雨云
2026-01-23 17:08:36,143 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 17:08:36,285 - INFO - 📝 输入账号密码
2026-01-23 17:08:36,657 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 17:08:59,802 - INFO - ✅ 未触发登录验证码
2026-01-23 17:09:04,809 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 17:09:04,813 - INFO - 页面标题: 总览 | 雨云
2026-01-23 17:09:04,842 - INFO - ✅ 账号登录成功:ACGN
2026-01-23 17:09:04,842 - INFO - 🌐 访问赚取积分页
2026-01-23 17:09:05,841 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 17:09:05,871 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 17:09:05,871 - INFO - 🔍 查找每日签到按钮
2026-01-23 17:09:05,940 - INFO - 📌 签到状态:领取奖励,开始领取
2026-01-23 17:09:06,072 - INFO - ⚠️ 触发签到验证码
2026-01-23 17:09:06,253 - INFO - 🔄 验证码处理第1次尝试(最大10次)
2026-01-23 17:09:06,810 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=02680900003d283800000015123b75d53fed&sess=s0HtD0kpY6pcWGI6FzFpt6ZiszTr2EhH-VfdHwPwxIdqv34Z-7I44K0-_RhKCQ_D1pczn56AhHTy7TzWXqVjayAnecALMlUWYf152tXUBM_URxYIPDxvoDD7jXbk7mwSIeJKDAUtmTTnuuaRcoqdw3DlBpEXv3Xc4RbCewuRGJZUAkZPrzzB8njktvXIOPrqAhs4UafKm96GgAUPJExW9_2PkkRGBKSTS43H1uLzB9el3g70xLMDYSd2TywoxM5Ps2idtfMPBn_aMw93gVXYLGwpX0Iztn4QG1vFv9VJj6NgCvOU2YSfCmTrGyEXxdzPnGglGJAKJFB0FfuxP4bM3-0O4DQt4l-5NsT52KR_8WcG7rvohxQXZy1sRw9MY84c31oFqKfUyPsa49v1VdtmranaOtiaDLX6SjgI6rJPvt2_kSelSHRNUWtA**
2026-01-23 17:09:07,077 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=02680900003d283800000015123b75d53fed&sess=s0HtD0kpY6pcWGI6FzFpt6ZiszTr2EhH-VfdHwPwxIdqv34Z-7I44K0-_RhKCQ_D1pczn56AhHTy7TzWXqVjayAnecALMlUWYf152tXUBM_URxYIPDxvoDD7jXbk7mwSIeJKDAUtmTTnuuaRcoqdw3DlBpEXv3Xc4RbCewuRGJZUAkZPrzzB8njktvXIOPrqAhs4UafKm96GgAUPJExW9_2PkkRGBKSTS43H1uLzB9el3g70xLMDYSd2TywoxM5Ps2idtfMPBn_aMw93gVXYLGwpX0Iztn4QG1vFv9VJj6NgCvOU2YSfCmTrGyEXxdzPnGglGJAKJFB0FfuxP4bM3-0O4DQt4l-5NsT52KR_8WcG7rvohxQXZy1sRw9MY84c31oFqKfUyPsa49v1VdtmranaOtiaDLX6SjgI6rJPvt2_kSelSHRNUWtA**
2026-01-23 17:09:07,375 - ERROR - ⚠️ 图案2识别率0.0000低于阈值0.4
2026-01-23 17:09:07,376 - ERROR - ❌ 验证码坐标重复,答案无效
2026-01-23 17:09:07,376 - ERROR - ❌ 验证码处理失败:验证码答案无效
2026-01-23 17:09:07,376 - ERROR - ⏳ 刷新验证码中,稍后重试……
2026-01-23 17:09:13,495 - INFO - 🔄 验证码处理第2次尝试(最大10次)
2026-01-23 17:09:13,518 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=0268090000946c2b0000000bb5e61fd63312&sess=s0_hrS7I5bVMRdivCWNVX_5xijZd5qBztok8b_H7bwMciiNFNIe3KMmj4IPktJO-cbs-8dl7upCI40ZosuxWWRjpXlIbF-P3ZWNoFjjg5G9dMFSybpTUgmgQO1lGEy1QSjGIghi44ITJTpCGcF4ym8wD4iU0xLCVakXfJvTvPiJbxl055LMVFM8W1FM1TtThPXpkg5h9JgYXRHols_wYhIgOI_dRxdgl3r_h-dSKI109RxypesTYee-w0m-Lw_41AM1etin4G_Iamp3lveRUaOtNV1JT4ssYxJ3DR1NZ8SEfN3yxvn9Z-_dxfifqGBxz8hkBmv4vsmx4M9imY60mxrr32HJt0K1ODVgIkzXKA0mgcq1DsSXM0AlcE765_pI_-NP9BgOPXEivjsEDpnxrS-nUFA1DJEz6urpWBwjgZN80OGgAAIs1XL1A**
2026-01-23 17:09:13,731 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=0268090000946c2b0000000bb5e61fd63312&sess=s0_hrS7I5bVMRdivCWNVX_5xijZd5qBztok8b_H7bwMciiNFNIe3KMmj4IPktJO-cbs-8dl7upCI40ZosuxWWRjpXlIbF-P3ZWNoFjjg5G9dMFSybpTUgmgQO1lGEy1QSjGIghi44ITJTpCGcF4ym8wD4iU0xLCVakXfJvTvPiJbxl055LMVFM8W1FM1TtThPXpkg5h9JgYXRHols_wYhIgOI_dRxdgl3r_h-dSKI109RxypesTYee-w0m-Lw_41AM1etin4G_Iamp3lveRUaOtNV1JT4ssYxJ3DR1NZ8SEfN3yxvn9Z-_dxfifqGBxz8hkBmv4vsmx4M9imY60mxrr32HJt0K1ODVgIkzXKA0mgcq1DsSXM0AlcE765_pI_-NP9BgOPXEivjsEDpnxrS-nUFA1DJEz6urpWBwjgZN80OGgAAIs1XL1A**
2026-01-23 17:09:14,002 - ERROR - ⚠️ 图案2识别率0.1515低于阈值0.4
2026-01-23 17:09:14,003 - ERROR - ❌ 验证码坐标重复,答案无效
2026-01-23 17:09:14,003 - ERROR - ❌ 验证码处理失败:验证码答案无效
2026-01-23 17:09:14,003 - ERROR - ⏳ 刷新验证码中,稍后重试……
2026-01-23 17:09:23,108 - INFO - 🔄 验证码处理第3次尝试(最大10次)
2026-01-23 17:09:23,131 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=0268090000f13d2300000015123b75d53f2f&sess=s02qPcN6ye2H2TPQfQ9ghy_0L3jB722YFRMCmx-rWnjm4UgxUo3F4WLoUzz5JczVgNJMtQwRWLFRo4OvXls1zjaajvPXch4RMoo6YZOavScFvdGaB-9B-ecxWvfcPx7ZTEb03-5MTmG-P2LipAwhLGAYKO0JOK6Rb6z3KYkAy9pxHIXYP9FaLlwdRvLsEDbqKWJZKCP4IHJ9mav4XH2EoTFfWGYMR-sA53gKcavXkSbzg2J_3ntSL6rszaLREZi9ZCSn1bPIDt16NYUXhHhlPFCJmBzIh41fG-nFTtpB-A8_i_vPaNo3mwlxJ9KojhSP37q7CfeASWq8-DTtI3OnT-mZbyVzoDHvBgQOiiQu5o0_VxQtxzWD9vNmVbErvVsP1VxQEVv0GCFywapI0H-R2DimaJI87vvVkVIzVce2MZQJ_lxWICubZ-RA**
2026-01-23 17:09:23,352 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=0268090000f13d2300000015123b75d53f2f&sess=s02qPcN6ye2H2TPQfQ9ghy_0L3jB722YFRMCmx-rWnjm4UgxUo3F4WLoUzz5JczVgNJMtQwRWLFRo4OvXls1zjaajvPXch4RMoo6YZOavScFvdGaB-9B-ecxWvfcPx7ZTEb03-5MTmG-P2LipAwhLGAYKO0JOK6Rb6z3KYkAy9pxHIXYP9FaLlwdRvLsEDbqKWJZKCP4IHJ9mav4XH2EoTFfWGYMR-sA53gKcavXkSbzg2J_3ntSL6rszaLREZi9ZCSn1bPIDt16NYUXhHhlPFCJmBzIh41fG-nFTtpB-A8_i_vPaNo3mwlxJ9KojhSP37q7CfeASWq8-DTtI3OnT-mZbyVzoDHvBgQOiiQu5o0_VxQtxzWD9vNmVbErvVsP1VxQEVv0GCFywapI0H-R2DimaJI87vvVkVIzVce2MZQJ_lxWICubZ-RA**
2026-01-23 17:09:23,677 - INFO - 🎯 图案 1 坐标(37,277),匹配率:0.6552
2026-01-23 17:09:23,677 - INFO - 🎯 图案 2 坐标(598,114),匹配率:0.5641
2026-01-23 17:09:23,677 - INFO - 🎯 图案 3 坐标(437,197),匹配率:0.6207
2026-01-23 17:09:26,517 - INFO - 📤 提交验证码
2026-01-23 17:09:31,670 - INFO - ✅ 验证码验证通过
2026-01-23 17:09:36,676 - INFO - ✅ 签到奖励领取成功
2026-01-23 17:09:36,699 - INFO - 💰 当前积分:700(约0.35元)
2026-01-23 17:09:36,788 - INFO - ✅ 账号ACGN浏览器已关闭
2026-01-23 17:09:36,788 - INFO - ✅ 临时文件清理完成
2026-01-23 17:09:36,788 - INFO - 
========== 账号ACGN处理完成 ==========

2026-01-23 17:09:40,305 - INFO - 
🎉 所有账号处理完成!

## 执行结束... 2026-01-23 17:09:40  耗时 707 秒   

二、前置条件

青龙面板:我是直接用的 1panel 应用商店里的青龙面板
雨云账号密码:自个注册去

三、准备工作

青龙面板安装依赖:安装不上的,不会的请自行搜索教程或者请教 AI 了哦
—— NodeJs:chromium
—— Python3:selenium
—— Linux:chromium-driver
青龙面板配置环境变量:想设置多少自己设置就好了
—— RAINYUN_ACCOUNT
—— [[“账号 1”,“账号 1 密码”],[“账号 2”,“账号 2 密码”]]

四、创建文件

同目录下创建以下两个文件

stealth.min.js
stealth.min.js.txt

rainyun.py
rainyun.py.txt

删掉 [.txt] 后缀上传就好了,顺便贴出 rainyun.py 的代码如下

import json
import logging
import os
import random
import re
import sys
import time
from typing import Tuple, Optional, List

import cv2
import ddddocr
import requests
from selenium import webdriver
from selenium.common import TimeoutException, WebDriverException, NoSuchElementException
from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

# ===================== 青龙面板专属配置(常量不抽离) =====================
CONFIG = {
    "timeout": 20,  # 青龙面板网络可能不稳定,延长超时时间
    "max_delay": 5,  # 最大随机等待分钟数
    "max_captcha_retry": 10,  # 验证码最大重试次数(防止递归栈溢出)
    "similarity_threshold": 0.4,  # 降低阈值提升识别率
    "script_path": os.path.dirname(os.path.abspath(__file__)),  # 青龙脚本所在目录
    "temp_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp"),  # 临时文件路径
    "rainyun_login_url": "https://app.rainyun.com/auth/login",
    "rainyun_earn_url": "https://app.rainyun.com/account/reward/earn"
}

# 全局日志对象(仅日志全局化,核心变量均函数内初始化)
logger = logging.getLogger(__name__)

def init_logger():
    """初始化青龙面板日志格式(增强版)"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[logging.StreamHandler(sys.stdout)]
    )
    # 打印项目信息
    logger.info("-"*80)
    logger.info("雨云签到工具 by SerendipityR ~")
    logger.info("Github发布页: https://github.com/SerendipityR-2022/Rainyun-Qiandao")
    logger.info("-"*80)
    logger.info("雨云签到工具容器版 by fatekey ~")
    logger.info("Github发布页: https://github.com/fatekey/Rainyun-Qiandao")
    logger.info("-"*80)
    logger.info("                   项目为二次开发青龙脚本化运行")
    logger.info("                     本项目基于上述项目开发")
    logger.info("                本项目仅作为学习参考,请勿用于其他用途")
    logger.info("-"*80)

def init_selenium() -> WebDriver:
    """初始化青龙面板专用Selenium驱动(每次调用新建实例,避免缓存污染)"""
    ops = Options()
    # 容器环境必需配置
    ops.add_argument("--no-sandbox")
    ops.add_argument("--disable-dev-shm-usage")
    ops.add_argument("--headless=new")
    ops.add_argument("--disable-gpu")
    ops.add_argument("--window-size=1920,1080")
    ops.add_argument("--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
    # 反爬配置
    ops.add_experimental_option("excludeSwitches", ["enable-automation"])
    ops.add_experimental_option('useAutomationExtension', False)
    ops.add_argument("--disable-blink-features=AutomationControlled")
    
    # 青龙面板固定驱动路径校验
    driver_path = "/usr/bin/chromedriver"
    if not os.path.exists(driver_path) or not os.access(driver_path, os.X_OK):
        raise FileNotFoundError(
            f"青龙面板未安装chromium-driver!\n"
            f"请在青龙终端执行:apt update && apt install -y chromium-driver"
        )

    try:
        service = Service(executable_path=driver_path)
        driver = webdriver.Chrome(service=service, options=ops)
        # 清空缓存(双重保障)
        driver.delete_all_cookies()
        logger.info(f"✅ Selenium驱动初始化成功,路径:{driver_path}")
        return driver
    except WebDriverException as e:
        logger.error(f"❌ 驱动启动失败:{str(e)}")
        raise

def check_stealth_js() -> str:
    """检查青龙脚本目录下的stealth.min.js"""
    js_path = os.path.join(CONFIG["script_path"], "stealth.min.js")
    if not os.path.exists(js_path):
        logger.error(f"❌ 未找到stealth.min.js!请将文件上传到青龙脚本目录:{CONFIG['script_path']}")
        logger.info("📥 下载地址:https://raw.githubusercontent.com/berstend/puppeteer-extra/master/packages/puppeteer-extra-plugin-stealth/evasions/stealth.min.js")
        sys.exit(1)
    return js_path

def inject_stealth_js(driver: WebDriver):
    """注入反检测脚本(传入driver实例,解耦全局变量)"""
    js_path = check_stealth_js()
    with open(js_path, "r", encoding="utf-8") as f:
        js = f.read()
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": js})
    logger.info("✅ 已注入stealth.min.js反检测脚本")

def download_image(url: str, filename: str, img_index: int) -> bool:
    """下载图片(带URL日志+青龙面板加请求头防拦截)"""
    os.makedirs(CONFIG["temp_path"], exist_ok=True)
    try:
        logger.info(f"开始下载验证码图片({img_index}):{url}")
        headers = {
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Referer": "https://app.rainyun.com/"
        }
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        path = os.path.join(CONFIG["temp_path"], filename)
        with open(path, "wb") as f:
            f.write(response.content)
        return True
    except Exception as e:
        logger.error(f"❌ 下载图片失败 {url}:{str(e)}")
        return False

# ========== 工具函数(精简+健壮性优化) ==========
def get_url_from_style(style: str) -> Optional[str]:
    """从style属性提取URL"""
    try:
        match = re.search(r'url\(["\']?(.*?)["\']?\)', style)
        return match.group(1) if match else None
    except Exception:
        return None

def get_width_from_style(style: str) -> str:
    """从style属性提取宽度"""
    match = re.search(r'width:\s*([\d.]+)px', style)
    return match.group(1) if match else "300"

def get_height_from_style(style: str) -> str:
    """从style属性提取高度"""
    match = re.search(r'height:\s*([\d.]+)px', style)
    return match.group(1) if match else "150"

def compute_similarity(img1_path: str, img2_path: str) -> Tuple[float, int]:
    """青龙面板适配:SIFT不可用时用ORB(增加异常兜底)"""
    try:
        img1 = cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE)
        img2 = cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE)
        if img1 is None or img2 is None:
            logger.warning(f"❌ 图片读取失败:{img1_path} 或 {img2_path}")
            return 0.0, 0

        # 优先SIFT,降级ORB
        try:
            sift = cv2.SIFT_create()
            norm = cv2.NORM_L2
        except AttributeError:
            sift = cv2.ORB_create()
            norm = cv2.NORM_HAMMING
            logger.warning("⚠️ SIFT不可用,使用ORB匹配")

        kp1, des1 = sift.detectAndCompute(img1, None)
        kp2, des2 = sift.detectAndCompute(img2, None)
        if des1 is None or des2 is None:
            return 0.0, 0

        bf = cv2.BFMatcher(norm, crossCheck=False)
        matches = bf.knnMatch(des1, des2, k=2)
        good = [m for m, n in matches if m.distance < 0.8 * n.distance]
        similarity = len(good) / len(matches) if matches else 0.0
        return similarity, len(good)
    except Exception as e:
        logger.error(f"❌ 相似度计算失败:{str(e)}")
        return 0.0, 0

def download_captcha_img(driver: WebDriver, wait: WebDriverWait) -> bool:
    """下载并分割验证码图片(解耦全局变量)"""
    try:
        # 清空旧临时文件
        if os.path.exists(CONFIG["temp_path"]):
            for f in os.listdir(CONFIG["temp_path"]):
                os.remove(os.path.join(CONFIG["temp_path"], f))

        # 定位验证码背景图
        slideBg = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="slideBg"]')))
        img1_url = get_url_from_style(slideBg.get_attribute("style"))
        if not img1_url or not download_image(img1_url, "captcha.jpg", 1):
            return False

        # 定位验证码碎片图
        sprite = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="instruction"]/div/img')))
        if not download_image(sprite.get_attribute("src"), "sprite.jpg", 2):
            return False

        # 分割碎片图
        raw = cv2.imread(os.path.join(CONFIG["temp_path"], "sprite.jpg"))
        if raw is None:
            logger.error("❌ 验证码碎片图读取失败")
            return False
        w = raw.shape[1]
        for i in range(3):
            cv2.imwrite(
                os.path.join(CONFIG["temp_path"], f"sprite_{i+1}.jpg"),
                raw[:, w//3*i : w//3*(i+1)]
            )
        return True
    except TimeoutException:
        logger.error("❌ 验证码图片加载超时")
        return False
    except Exception as e:
        logger.error(f"❌ 验证码图片处理失败:{str(e)}")
        return False

def check_answer(result: dict) -> bool:
    """检查验证码答案有效性(带识别率日志)"""
    valid = True
    for i in range(3):
        sim = float(result.get(f"sprite_{i+1}.similarity", 0))
        if sim < CONFIG["similarity_threshold"]:
            logger.error(f"⚠️ 图案{i+1}识别率{sim:.4f}低于阈值{CONFIG['similarity_threshold']}")
            valid = False
            break
    # 检查坐标唯一性
    positions = [result.get(f"sprite_{i+1}.position") for i in range(3)]
    if len(set(positions)) != 3:
        logger.error("❌ 验证码坐标重复,答案无效")
        valid = False
    return valid

def process_captcha(driver: WebDriver, wait: WebDriverWait) -> bool:
    """处理验证码(改递归为循环,提升健壮性,解耦全局变量)"""
    captcha_retry_count = 0
    ocr = ddddocr.DdddOcr(ocr=True, show_ad=False)
    det = ddddocr.DdddOcr(det=True, show_ad=False)

    while captcha_retry_count < CONFIG["max_captcha_retry"]:
        captcha_retry_count += 1
        logger.info(f"🔄 验证码处理第{captcha_retry_count}次尝试(最大{CONFIG['max_captcha_retry']}次)")
        try:
            # 下载验证码图片
            if not download_captcha_img(driver, wait):
                raise Exception("验证码图片下载失败")

            # 校验验证码有效性
            valid = True
            for i in range(3):
                sprite_path = os.path.join(CONFIG["temp_path"], f"sprite_{i+1}.jpg")
                with open(sprite_path, "rb") as f:
                    if ocr.classification(f.read()) in ["0", "1"]:
                        valid = False
                        break
            if not valid:
                raise Exception("验证码碎片无效")

            # 识别验证码
            captcha = cv2.imread(os.path.join(CONFIG["temp_path"], "captcha.jpg"))
            if captcha is None:
                raise Exception("验证码背景图读取失败")
            with open(os.path.join(CONFIG["temp_path"], "captcha.jpg"), "rb") as f:
                bboxes = det.detection(f.read())
            if not bboxes:
                raise Exception("未检测到验证码图案")

            # 匹配碎片与背景图
            result = {}
            for i, (x1, y1, x2, y2) in enumerate(bboxes):
                # 裁剪背景图中的图案
                cv2.imwrite(os.path.join(CONFIG["temp_path"], f"spec_{i+1}.jpg"), captcha[y1:y2, x1:x2])
                # 计算与每个碎片的相似度
                for j in range(3):
                    sim, _ = compute_similarity(
                        os.path.join(CONFIG["temp_path"], f"sprite_{j+1}.jpg"),
                        os.path.join(CONFIG["temp_path"], f"spec_{i+1}.jpg")
                    )
                    key_sim = f"sprite_{j+1}.similarity"
                    key_pos = f"sprite_{j+1}.position"
                    if sim > float(result.get(key_sim, 0)):
                        result[key_sim] = sim
                        result[key_pos] = f"{int((x1+x2)/2)},{int((y1+y2)/2)}"

            # 校验答案
            if not check_answer(result):
                raise Exception("验证码答案无效")

            # 打印匹配结果
            for i in range(3):
                pos = result[f"sprite_{i+1}.position"]
                sim = result[f"sprite_{i+1}.similarity"]
                x, y = pos.split(",")
                logger.info(f"🎯 图案 {i+1} 坐标({x},{y}),匹配率:{sim:.4f}")

            # 点击验证码图案
            slideBg = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="slideBg"]')))
            style = slideBg.get_attribute("style")
            width, height = float(get_width_from_style(style)), float(get_height_from_style(style))
            width_raw, height_raw = captcha.shape[1], captcha.shape[0]

            for i in range(3):
                pos = result[f"sprite_{i+1}.position"]
                x, y = map(int, pos.split(","))
                # 计算实际点击坐标(适配页面缩放)
                final_x = int(-width/2 + x/width_raw * width) + random.randint(-1, 1)
                final_y = int(-height/2 + y/height_raw * height) + random.randint(-1, 1)
                ActionChains(driver).move_to_element_with_offset(slideBg, final_x, final_y).click().perform()
                time.sleep(random.uniform(0.5, 1))

            # 提交验证码
            logger.info("📤 提交验证码")
            confirm = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="tcStatus"]/div[2]/div[2]/div/div')))
            confirm.click()
            time.sleep(5)

            # 校验验证码结果
            tc_operation = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="tcOperation"]')))
            if tc_operation.get_attribute("class") == "tc-opera pointer show-success":
                logger.info("✅ 验证码验证通过")
                return True
            else:
                raise Exception("验证码验证失败")

        except Exception as e:
            logger.error(f"❌ 验证码处理失败:{str(e)}")
            # 刷新验证码重试
            try:
                logger.error("⏳ 刷新验证码中,稍后重试……")
                reload = driver.find_element(By.XPATH, '//*[@id="reload"]')
                time.sleep(3)
                reload.click()
                time.sleep(min(3 * (2 ** (captcha_retry_count - 1)), 30))  # 指数退避重试间隔,上限30秒
            except NoSuchElementException:
                logger.error("❌ 验证码刷新按钮未找到,重试失败")
                return False

    logger.error(f"❌ 验证码重试{CONFIG['max_captcha_retry']}次仍失败,放弃")
    return False

def clean_temp():
    """清理青龙面板临时文件(增加容错)"""
    try:
        if os.path.exists(CONFIG["temp_path"]):
            for f in os.listdir(CONFIG["temp_path"]):
                file_path = os.path.join(CONFIG["temp_path"], f)
                try:
                    os.remove(file_path)
                except Exception as e:
                    logger.warning(f"⚠️ 删除临时文件{file_path}失败:{str(e)}")
            os.rmdir(CONFIG["temp_path"])
        logger.info("✅ 临时文件清理完成")
    except Exception as e:
        logger.warning(f"⚠️ 清理临时文件失败:{str(e)}")

def parse_accounts() -> List[List[str]]:
    """解析青龙面板RAINYUN_ACCOUNT环境变量"""
    account_str = os.getenv("RAINYUN_ACCOUNT")
    if not account_str:
        logger.error("❌ 未配置RAINYUN_ACCOUNT环境变量!格式应为[[账号1,密码1],[账号2,密码2]]")
        sys.exit(1)
    
    try:
        # 解析JSON格式的账号列表(兼容单引号/双引号)
        account_str = account_str.replace("'", "\"")  # 统一为双引号
        accounts = json.loads(account_str)
        # 校验格式
        if not isinstance(accounts, list):
            raise ValueError("环境变量值不是列表类型")
        for idx, account in enumerate(accounts):
            if not isinstance(account, list) or len(account) != 2:
                raise ValueError(f"第{idx+1}个账号格式错误,应为[账号,密码]")
            if not account[0] or not account[1]:
                raise ValueError(f"第{idx+1}个账号/密码为空")
        logger.info(f"✅ 成功解析{len(accounts)}个账号")
        return accounts
    except json.JSONDecodeError as e:
        logger.error(f"❌ RAINYUN_ACCOUNT格式解析失败:{str(e)},请检查格式是否为合法JSON")
        sys.exit(1)
    except ValueError as e:
        logger.error(f"❌ RAINYUN_ACCOUNT格式错误:{str(e)}")
        sys.exit(1)

def sign_in_rainyun(username: str, password: str):
    """单账号签到核心逻辑(独立封装,支持多账号循环调用)"""
    driver = None
    try:
        logger.info(f"\n========== 开始处理账号:{username} ==========")
        # 随机延时(可选)
        delay = random.randint(0, CONFIG["max_delay"])
        delay_sec = random.randint(0, 60)
        logger.info(f"⏳ 随机延时 {delay} 分钟 {delay_sec} 秒")
        time.sleep(delay * 60 + delay_sec)

        # 初始化Selenium(每次新建实例,清空缓存)
        driver = init_selenium()
        inject_stealth_js(driver)
        wait = WebDriverWait(driver, CONFIG["timeout"])

        # 访问登录页
        logger.info("⏳ 发起登录请求")
        logger.info("🌐 访问雨云登录页")
        driver.get(CONFIG["rainyun_login_url"])
        logger.info(f"页面标题:{driver.title}")

        # 输入账号密码
        logger.info("⏳ 等待登录表单元素加载...")
        username_elem = wait.until(EC.visibility_of_element_located((By.NAME, "login-field")))
        password_elem = wait.until(EC.visibility_of_element_located((By.NAME, "login-password")))
        login_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="app"]/div[1]/div[1]/div/div[2]/fade/div/div/span/form/button')))
        logger.info("📝 输入账号密码")
        username_elem.send_keys(username)
        password_elem.send_keys(password)
        login_btn.click()
        logger.info("⏳ 正在登录中,耗时较长请稍等……")
        time.sleep(3)

        # 处理登录验证码
        try:
            wait.until(EC.visibility_of_element_located((By.ID, "tcaptcha_iframe_dy")))
            logger.warning("⚠️ 触发登录验证码")
            driver.switch_to.frame("tcaptcha_iframe_dy")
            if not process_captcha(driver, wait):
                raise Exception("登录验证码验证失败")
        except TimeoutException:
            logger.info("✅ 未触发登录验证码")

        # 校验登录状态
        time.sleep(5)
        driver.switch_to.default_content()
        logger.info(f"当前页面: {driver.current_url}")
        logger.info(f"页面标题: {driver.title}")
        if driver.current_url != "https://app.rainyun.com/dashboard":
            raise Exception("登录失败!请检查账号密码或网络")
        user_name = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/nav/div[1]/ul/div[6]/li/a/div/div/p').text.strip()
        logger.info(f"✅ 账号登录成功:{user_name}")
        
        # 访问签到页
        logger.info("🌐 访问赚取积分页")
        driver.get(CONFIG["rainyun_earn_url"])
        driver.implicitly_wait(5)
        logger.info(f"当前页面: {driver.current_url}")
        logger.info(f"页面标题: {driver.title}")

        # 查找并点击签到按钮
        logger.info("🔍 查找每日签到按钮")
        earn_btn_qddiv = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/div[3]/div[2]/div/div/div[2]/div[2]/div/div/div/div[1]/div')
        earn_btn_qd = earn_btn_qddiv.find_element(By.XPATH, './/span[contains(text(),"每日签到")]')
        status_elem = earn_btn_qd.find_element(By.XPATH, './following-sibling::span[1]')
        status_text = status_elem.text.strip()

        if status_text == "领取奖励":
            earn_btn = status_elem.find_element(By.XPATH, './a')
            logger.info(f"📌 签到状态:{status_text},开始领取")
            earn_btn.click()

            # 处理签到验证码
            logger.info("⚠️ 触发签到验证码")
            driver.switch_to.frame("tcaptcha_iframe_dy")
            if not process_captcha(driver, wait):
                raise Exception("签到验证码验证失败")
            driver.switch_to.default_content()

            # 校验签到结果
            time.sleep(5)
            logger.info("✅ 签到奖励领取成功")
        else:
            logger.info(f"📌 签到状态:{status_text},无需重复签到")

        # 获取当前积分
        try:
            points_elem = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/div[3]/div[2]/div/div/div[2]/div[1]/div[1]/div/p/div/h3')
            current_points = int(''.join(re.findall(r'\d+', points_elem.text)))
            logger.info(f"💰 当前积分:{current_points}(约{current_points/2000:.2f}元)")
        except Exception as e:
            logger.warning(f"⚠️ 积分获取失败:{str(e)}")

        

    except Exception as e:
        logger.error(f"❌ 账号{username}处理失败:{str(e)}", exc_info=True)
    finally:
        # 关闭浏览器,彻底清空缓存
        if driver:
            try:
                driver.quit()
                logger.info(f"✅ 账号{username}浏览器已关闭")
            except Exception as e:
                logger.warning(f"⚠️ 关闭浏览器失败:{str(e)}")
        # 清理临时文件
        clean_temp()
    logger.info(f"\n========== 账号{username}处理完成 ==========\n")

def main():
    """主函数:解析多账号,依次执行签到"""
    init_logger()
    # 解析账号列表
    accounts = parse_accounts()
    # 依次处理每个账号
    for idx, (username, password) in enumerate(accounts, 1):
        logger.info(f"\n================= 处理第{idx}个账号 ==================")
        sign_in_rainyun(username, password)
        # 账号间间隔(可选)
        time.sleep(random.uniform(2, 5))
    
    logger.info("\n🎉 所有账号处理完成!")

if __name__ == "__main__":
    main()

五、最后

后面的设置定时任务之类的就懒得写啦,顺便我也懒得写通知之类的了,感谢前人的开发让我站于巨人肩膀上,要是有后来者优化了就更好了

哦,对了,懒得上传 github 了,就丢 linux.do 里好了


📌 转载信息
原作者:
T_ACGN
转载时间:
2026/1/24 07:01:47



简单来说就是最近在折腾 codex 反代,提升 codex 的使用体验,但是从 oauth 登录转到使用 api-key 时,发现以前的历史对话消失了,所以

压榨

研究出了这个小脚本

一些小思考

在现在的 vibe coding 时代,一些小需求真的可以很高效的解决了。这个问题从我产生解决这种需求的想法开始,到在论坛里搜索关键词没找到解决方案,再到我把 codex 仓库克隆下来丢给 antigravity 中的哈基米 3,第一次对话它就定位到了切换 provider 后历史对话消失的原因,后面告诉它用 py 脚本迁移之后就很快的给了我一个可用的版本,再丢给 chatgpt 网页版~~(codex 太慢了,小需求就没找它 debug)~~ 迭代了 2 次之后,整体用时不超过 20 分钟。

而在传统古法编程的时代,要解决这个小需求,首先要读 codex 仓库定位问题,光是这一步消耗的时间起码就得在 30 分钟以上…… 对我这样的小菜鸡来说,这个时间更是得翻倍的。 哦,刚注意到 codex 代码基本是用 rust 写的,那花费时间为 0。在我花时间学会 rust 之前,我是看不大懂 rust 代码的,所以我只能等某个大手子出手才能直接无脑使用。但为了这个很小的需求,我是不大可能去学 rust 的,有这时间去玩会不香吗?我会自适应历史对话消失的小问题,又不是不能用


📌 转载信息
原作者:
hello_world1024
转载时间:
2026/1/24 06:44:31

日常问很多生活问题和一些小代码问题一般很喜欢用 Google Ai Studio,里面的回复也很不错,冬天的时候上了一个可以打开雪花特效的按钮,雪花特效挺好看的,但是每次都需要手动打开,刷新一下又没有了,让 AI 写了个脚本可以自动打开,每次刷新会自动打开,挺不错的

// ==UserScript==
// @name         Google AI Studio - Auto Snow
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  自动开启 Google AI Studio 的 Let it snow 特效
// @author       You
// @match        https://aistudio.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 定义检测器
    const clickSnowButton = () => {
        // 在页面中查找所有可能的按钮或菜单项
        // 因为 Google 的类名经常变,我们直接找包含 "Let it snow" 文字的元素
        // 或者查找特定的图标/按钮位置(这里使用最通用的文本匹配法)
        const allElements = document.querySelectorAll('button, div[role="button"], span, li');

        for (let el of allElements) {
            // 忽略大小写,查找包含 Let it snow 的元素
            if (el.textContent && el.textContent.toLowerCase().includes('let it snow')) {
                // 找到后点击
                console.log('Found Snow button, clicking...', el);
                el.click();

                // 只有点击成功后才清除定时器,防止元素还没加载出来
                return true;
            }
        }
        return false;
    };

    // 使用定时器循环检查,因为 AI Studio 是动态加载的,按钮可能不会一开始就出现
    const checkInterval = setInterval(() => {
        const success = clickSnowButton();
        if (success) {
            // 如果成功点击了,就停止检查,避免重复点击(导致又关掉了)
            clearInterval(checkInterval);
        }
    }, 1000); // 每秒检查一次

    // 设置一个超时,比如 30 秒后还没找到就停止,省点资源
    setTimeout(() => {
        clearInterval(checkInterval);
    }, 30000);

})();

📌 转载信息
转载时间:
2026/1/20 10:46:02

日常问很多生活问题和一些小代码问题一般很喜欢用 Google Ai Studio,里面的回复也很不错,冬天的时候上了一个可以打开雪花特效的按钮,雪花特效挺好看的,但是每次都需要手动打开,刷新一下又没有了,让 AI 写了个脚本可以自动打开,每次刷新会自动打开,挺不错的

// ==UserScript==
// @name         Google AI Studio - Auto Snow
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  自动开启 Google AI Studio 的 Let it snow 特效
// @author       You
// @match        https://aistudio.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 定义检测器
    const clickSnowButton = () => {
        // 在页面中查找所有可能的按钮或菜单项
        // 因为 Google 的类名经常变,我们直接找包含 "Let it snow" 文字的元素
        // 或者查找特定的图标/按钮位置(这里使用最通用的文本匹配法)
        const allElements = document.querySelectorAll('button, div[role="button"], span, li');

        for (let el of allElements) {
            // 忽略大小写,查找包含 Let it snow 的元素
            if (el.textContent && el.textContent.toLowerCase().includes('let it snow')) {
                // 找到后点击
                console.log('Found Snow button, clicking...', el);
                el.click();

                // 只有点击成功后才清除定时器,防止元素还没加载出来
                return true;
            }
        }
        return false;
    };

    // 使用定时器循环检查,因为 AI Studio 是动态加载的,按钮可能不会一开始就出现
    const checkInterval = setInterval(() => {
        const success = clickSnowButton();
        if (success) {
            // 如果成功点击了,就停止检查,避免重复点击(导致又关掉了)
            clearInterval(checkInterval);
        }
    }, 1000); // 每秒检查一次

    // 设置一个超时,比如 30 秒后还没找到就停止,省点资源
    setTimeout(() => {
        clearInterval(checkInterval);
    }, 30000);

})();

📌 转载信息
转载时间:
2026/1/19 17:37:04

各位大佬好!最近笔记本更换了系统,从 Windows 换到了 Deepin 25。

以前在 Win 下习惯用 AHK 自动化,现在发现 Deepin 使用.sh 脚本非常强大,虽然完全没有编程基础,但主打一个折腾。随即尝试用 AI 帮我写脚本,结果可想而知:

就这一个脚本都磕磕绊绊、缝缝补补一两个月 (每天折腾一小会)。
后知后觉意识到不是 AI 不行,而是我作为 “提问者” 词不达意,不懂如何给 AI 下达清晰的指令。

想请教各位大佬:

  1. 入门路径: 想要系统性地提升 “提问力”,有没有公认的底层逻辑或框架值得学习?(比如大家常说的结构化提示词)

  2. 思维转变: 面对 AI 时,如何把模糊的需求拆解成它能听懂的逻辑指令?

  3. 进阶建议: 大家平时是如何打磨提示词的?有没有什么高质量的教程或者实战心得可以分享?

希望有经验的大佬指点迷津,在此谢过!

帖子也是通过 GPT 优化了一下,希望大佬们不要介意
快捷.txt


📌 转载信息
原作者:
15sir
转载时间:
2026/1/18 15:49:18

自己日常用青龙面板搞自动化,看到好项目都忍不住想往上搬

这次发现佬友 mumuladu 的 A 股 AI 分析神器,可惜不支持青龙,于是手痒改了一版,分享给同样用青龙的朋友们~

  • 适配青龙定时任务,收盘自动跑
  • 支持 20+ 推送渠道,报告直达手机

感谢原作者开源!开源精神万岁!

daily_stock_analysis_ql

附上几个截图


📌 转载信息
转载时间:
2026/1/18 09:09:09

importsysfromPyQt5.QtWidgetsimport(QApplication,QMainWindow,QListWidget,QVBoxLayout,QLineEdit,QWidget,QFrame)fromPyQt5.QtCoreimportQt,QEventfromPyQt5.QtWidgetsimportQDesktopWidgetimportpyautoguiimportpyperclipimporttimedefpowertoy_active_window(text):sleep_second=0.2time.sleep(sleep_second)pyautogui.hotkey("win","alt","k")time.sleep(sleep_second)pyperclip.copy(text)pyautogui.hotkey("ctrl","v")time.sleep(sleep_second)pyautogui.hotkey("enter") classCommandPalette(QWidget):def__init__(self,parent=None):super().__init__(parent)# 1. 设置 UI 属性:无边框、置顶、背景透明self.setWindowFlags(Qt.FramelessWindowHint|Qt.Dialog)self.setAttribute(Qt.WA_TranslucentBackground) # 外层容器(用于实现圆角和边框)self.container=QFrame(self)self.container.setFixedWidth(500)self.container.setStyleSheet("""QFrame {background-color: #252526;border: 1px solid #454545;border-radius: 6px;}""") layout=QVBoxLayout(self.container) # 2. 搜索输入框self.search_bar=QLineEdit()self.search_bar.setPlaceholderText("键入命令名称以筛选...")self.search_bar.setStyleSheet("""QLineEdit {background-color: #3c3c3c;color: #cccccc;border: 1px solid #007acc;padding: 6px;font-size: 14px;selection-background-color: #264f78;}""") # 3. 命令列表self.list_widget=QListWidget()self.list_widget.setStyleSheet("""QListWidget {background-color: #252526;color: #cccccc;border: none;font-size: 13px;outline: none;}QListWidget::item { padding: 10px; border-radius: 4px; }QListWidget::item:selected { background-color: #094771; color: white; }QListWidget::item:hover { background-color: #2a2d2e; }""") layout.addWidget(self.search_bar)layout.addWidget(self.list_widget) main_layout=QVBoxLayout(self)main_layout.setContentsMargins(0,0,0,0)main_layout.addWidget(self.container,alignment=Qt.AlignCenter) # 模拟命令数据# self.commands = [# "File: New File",# "Git: Commit All",# "Python: Run Current File",# "Settings: Open Settings",# "Terminal: Toggle Terminal",# "View: Layout Grid"# ]self.commands=[["1. book","book code"],["2. anti2","anti2 code"],["4. chrounim","chro"],["5.q-dir","q-dir"],]self.refresh_list("") # 信号连接self.search_bar.textChanged.connect(self.refresh_list)self.search_bar.returnPressed.connect(self.execute_selected)self.list_widget.itemClicked.connect(self.execute_selected) # 2. 当在列表中通过方向键选中某项并按回车时,触发 itemActivatedself.list_widget.itemActivated.connect(self.execute_selected) # 安装事件过滤器以捕获按键self.installEventFilter(self) defrefresh_list(self,text):self.list_widget.clear()# 简单的包含搜索(不区分大小写)# filtered = [cmd for cmd in self.commands if text.lower() in cmd.lower()]filtered=[info[0]forinfoinself.commandsiftext.lower()ininfo[0].lower()]self.list_widget.addItems(filtered)ifself.list_widget.count()>0:self.list_widget.setCurrentRow(0) defexecute_selected(self):item=self.list_widget.currentItem()ifitem:print(f"触发动作:{item.text()}")text=item.text()item=[eforeinself.commandsife[0]==text][0]send_text=item[1]print("#执行命令#:{}".format(send_text))powertoy_active_window(send_text)# self.hide()QApplication.instance().quit() defshow_palette(self):width=500height=400# 假设高度为 400self.setFixedSize(width,height) # 2. 获取屏幕中心# availableGeometry() 会避开任务栏,确保真正的视觉垂直居中screen_geo=QDesktopWidget().availableGeometry()screen_center=screen_geo.center() # 3. 计算面板位置,使其中心点与屏幕中心点重合# 我们可以通过 moveCenter 方便地实现qr=self.frameGeometry()qr.moveCenter(screen_center)self.move(qr.topLeft()) # 4. 显示并聚焦self.show()self.raise_()self.activateWindow()self.search_bar.setFocus()self.search_bar.selectAll()# 方便用户直接覆盖键入 defeventFilter(self,obj,event):ifevent.type()==QEvent.KeyPress:# Esc 键退出ifevent.key()==Qt.Key_Escape:self.hide()returnTrue# 下方向键快速进入列表选择ifevent.key()==Qt.Key_Downandself.search_bar.hasFocus():self.list_widget.setFocus()returnTruereturnsuper().eventFilter(obj,event) classMainWindow(QMainWindow):def__init__(self):super().__init__()self.setWindowTitle("PyQt5 VS Code Command Palette")self.resize(1000,700)# self.minimumSize()self.showMinimized()self.setStyleSheet("background-color: #1e1e1e;")# 模拟 VS Code 背景 # 初始化命令面板self.palette=CommandPalette(self)# self.palette.hide()self.palette.show_palette()defcenter_on_screen(self):# 获取窗口几何结构frame_geometry=self.frameGeometry() # 获取屏幕中心点# QDesktopWidget().availableGeometry() 会排除任务栏占据的空间screen_center=QDesktopWidget().availableGeometry().center() # 将窗口几何结构的中心点移动到屏幕中心点frame_geometry.moveCenter(screen_center) # 移动窗口左上角到计算出的位置self.move(frame_geometry.topLeft()) defkeyPressEvent(self,event):# 绑定快捷键 Ctrl+Shift+P (Qt.ControlModifier | Qt.ShiftModifier)if(event.modifiers()==(Qt.ControlModifier|Qt.ShiftModifier)andevent.key()==Qt.Key_P):self.palette.show_palette() if__name__=="__main__":app=QApplication(sys.argv)# 设置应用字体app.setStyle("Fusion")window=MainWindow()window.show()sys.exit(app.exec_())

这个东西的主要作用是当我输入 1 时,它会找到 ["1. book", "book code"]这一项,然后 取出book code,然后激活 window 窗口切换器
,我的窗口切换器的的快捷是Win+alt+k,本来想用 ahk 来写的,但是 ahk 写 command picker 实在是不会.

前因:

上次有佬友问如何自动定时同步上游仓库,当时我随手糊了一段脚本,结果发现 bug 满天飞,于是删除了。同时也推荐了 pull 这个工具,但是这个工具的同步比较随机,不可控。
于是就搞了现在这个脚本,支持多仓库、多用户、多分支、多平台通知

食用方法:
fork 仓库,然后根据 README.md 进行配置。
上游仓库:可以是任意公开仓库
目标仓库:可以是任意用户的仓库(需要具备 repo 权限的 token)

目标仓库支持你 fork 别人的,不影响 pr、创建分支等。也可以你自己创建一个空仓然后搬运。

该脚本运行于 GitHub Aactions,运行后的 actions 日志会显示上游仓库地址、目标仓库 owner/repo,但是不会暴露各种 token 等私密信息。可以把仓库设置为私密,不影响同步功能和效果。

推送的消息如下:


📌 转载信息
原作者:
binghe
转载时间:
2026/1/16 17:41:31

原贴地址。 Gemini Business/Team 一键注册,可无限循环,拉人 半自动化脚本

后续更新都委托

进行更新了。会在这位佬的项目上集成批量注册批量账号管理 + 刷新 json。后续可能会增加生态。

(如破甲,酒馆,有这方面研究的佬可以分享一下吗?)

注意更新在旁边发行包更新下载,一键配置不打算更新了。

注意更新在旁边发行包更新,一键配置不打算更新了。
注意更新在旁边发行包更新,一键配置不打算更新了。

首先哭一下穷,麻烦 LDC 富足的佬友支持一下。那么多天只有 50LDC 收入看得出来这位同志还是犹豫了很久的。可以看出心路历程。求求大家支持一下,这对一个新手小白来说真的很重要。后续项目也会接着造福大家!

这次主要是讲一下更新内容和你可以用来干嘛,增加一下生态。

因为大家都瞧不上这个模型,因为的确垃圾,而且甲很厚。(本人稍微研究了一下破甲,如某馆很麻烦,经常中断,导致突然兴致全无。有破甲高手可以私信我 ovo)

我通常是用来翻译。而且一次配置,可以一直使用 【开源】Gemini Nexus V4.2.0:开箱即用的 Chrome 浏览器控制插件,更新支持 OpenAI 兼容 API

然后是接入 Cherry Studio, 用大香蕉和他支持的模型。


修复的问题

| 问题 | 说明 |

首先是改为无头浏览器了,不能弹出来烦你。
现在只有一个命令行在那运行,注册速度大大加快了。

| 按钮匹配不正确 | 有大概率触发点击验证和重发验证码两个按钮,但是不影响功能
已修复,现在会精准点击

| 页面加载慢 | 网络不稳定时页面加载较慢,可能导致超时
已修复

| 存在僵尸进程 clash.exe| 在后台会占用资源,可自己手动进任务管理器结束 |
在命令行按 CTRL+C 然后 YES 会杀进程

然后虽然这个模型和套餐索然无味,大佬都看不上。本人都用在边边角角。

首先是 2API 的反代。

然后是你要部署好佬的项目

部署成功长这样子

然后看到他有一个过期时限 12h,不然会过期。
这个佬给出了方案

佬的方案是登录账号一键获取前面佬的 json 相关数据。

如果你打算只白嫖一两个号,然后丢进去反代是不是太容易满足了,毕竟我们可以做到批量注册。我是用了 22 个号,有人问为什么那么多。

22 个号一个个更新太慢了,然后 D 佬支持批量导入,于是我折腾了一个批量更新这些数据的 Web,
你可以一键更新这些账号。

这部分见最前面那位佬,由他更新。


📌 转载信息
原作者:
Zooo1
转载时间:
2026/1/15 10:19:30

详情见:GitHub - WeMingT/zaimanhua-auto-chick-in: 基于 GitHub Actions 的再漫画 (zaimanhua.com) 自动签到工具 | Auto check-in tool for zaimanhua.com

支持功能:

功能说明触发时间 (北京)
每日签到自动完成签到任务并领取积分8:00
每日评论在随机漫画下发表评论9:30
每日阅读自动阅读漫画10:00
每日抽奖完成任务并自动抽奖11:00
积分领取任务完成后自动领取积分随任务执行

📌 转载信息
原作者:
klama
转载时间:
2026/1/14 11:03:27

昨天看群里都在讨论 Kiro 闲着无聊我就研究一下了注册机
然后开发了一个脚本 全自动注册

但是我注册几个发现会封号呀

然后我就研究了半小时 我发现和邮箱无关 和 代理 ip 无关 只和环境有关

于是我手动注册了 10 个都没封 (不保证有效)

  1. 我打开 win 软件的 kiro
  2. 点击这个 awz 登录 会生成带参数一个 login?workflowStateHandle=a78fdd8f-9780-439f-8efa-be793967583a
  3. 然后使用域名邮箱注册 会提示授权 Kiro 点击拒绝 然后重复操作 这样手动的号居然没事


以上均为个人研究不保证可用 开源的注册机是会封号的(仅供学习思路)


📌 转载信息
原作者:
chinadoiphin
转载时间:
2026/1/13 11:02:42

25 年写的中行预约的填充信息的脚本,26 年应该还能用,可惜湖北今年不是在中行约了,发出来给大家参考,自行修改关键信息,主要是一键填充信息,预约还是要自己提交的,我记得预约界面还有短信验证码和图片验证码

中行的纪念币预约地址: 中国银行纪念币销售

脚本里还有填充预约查询的,可以试试看。记得修改脚本里要填写的内容

javascript: (function() {
	var t = 0,
	e = 0;
	try {
		document.getElementById("txt_name_1956714") && (document.getElementById("txt_name_1956714").value = "姓名") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("txt_mobile_1956715") && (document.getElementById("txt_mobile_1956715").value = "手机号") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("txt_phonenumber_1956668") && (document.getElementById("txt_phonenumber_1956668").value = "手机号") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("txt_identitynumber_1956717") && (document.getElementById("txt_identitynumber_1956717").value = "身份证号") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("txt_identitynumber_1956669") && (document.getElementById("txt_identitynumber_1956669").value = "身份证号") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("date-picker") && (document.getElementById("date-picker").value = "预约的日期如2025/05/10") ? t++:e++
	} catch(t) {
		e++
	}
	try {
		document.getElementById("sel_93844_1956719") && (document.getElementById("sel_93844_1956719").value = "数量") ? t++:e++
	} catch(t) {
		e++
	}
	console.log("完成!成功:" + t + "个,失败:" + e + "个")
})();

📌 转载信息
原作者:
nssiwi
转载时间:
2026/1/12 11:06:58

Git Hub

有空可以帮忙支持一下小白新人
** 有能力的老板可以资助一下 LDC,球球辣 **
50LDC 即可鼓励新人

半自动化是因为 API 化失败了,所以采用开浏览器半自动化的模式了。

欢迎有实力的佬友进行调优,本人是小白,用哈基米和 Codex 写的。写得很粗糙,不要见怪。

使用方法看 README

自备一个订阅机场,clash 核心自下。配置我个人是通过 clash 右键订阅机场,打开文件,复制到 local.yaml 覆盖全部使用。运行后,本地的 clash(或者其他代理)可以关了,或者切换为规则。不要开全局,系统代理。否则两个 clash 会冲突。

邮箱注册功能来自: DuckMail


@ 个人资料 - Syferie - LINUX DO

Business 登录地址:

后续登录也靠佬的前端:

运行界面:

账号密码保存在 csv 文件下:

待完善功能

| 功能 | 状态 | 说明 |

| 纯 API 注册 | 未实现 | Google 使用 reCAPTCHA Enterprise,纯 API 无法绕过

已知问题

| 问题 | 说明 |

| IP 封锁 | 部分代理节点被 Google 封禁,程序会自动切换节点 |

| 按钮匹配不正确 | 有大概率触发点击验证和重发验证码两个按钮,但是不影响功能 |

| 页面加载慢 | 网络不稳定时页面加载较慢,可能导致超时 |

| 存在僵尸进程 clash.exe| 在后台会占用资源,可自己手动进任务管理器结束 |

更新预告
大兵自助验证 API 已完成,完全全自动,使用方法参考市面上的 BOT,输入验证 URL 即可完成验证。这几天活动关了,等稳定了可能考虑开源给大家。


📌 转载信息
原作者:
Zooo1
转载时间:
2026/1/12 10:03:20

脚本是 AI 搓的,没看具体内容。
每天上班第一件事就是先运行一下脚本
运行结果:

脚本如下:

// update_ai_tools.js const { execSync } = require('child_process');
const os = require('os');

console.log('\n======================================');
console.log('       AI CLI 工具更新助手');
console.log('======================================\n');

// 1. 权限检查 function isRunningAsAdmin() {
  if (os.platform() === 'win32') {
    try {
      execSync('net session', { stdio: 'ignore' });
      return true;
    } catch (e) {
      return false;
    }
  }
  return process.geteuid && process.geteuid() === 0;
}

if (!isRunningAsAdmin()) {
  console.error('❌ 请以管理员身份运行此脚本。\n');
  process.stdin.once('data', () => process.exit());
  return;
}

const packages = [
  { name: 'Gemini CLI', npm: '@google/gemini-cli' },
  { name: 'GitHub Copilot', npm: '@github/copilot' },
  { name: 'Codex CLI', npm: '@openai/codex' },
  { name: 'Claude Code', npm: '@anthropic-ai/claude-code' },
  { name: 'Qwen Code', npm: '@qwen-code/qwen-code' }
];

// 2. 获取本地版本 (这一步很快) let localVersions = {};
try {
  const res = execSync('npm list -g --depth=0 --json', { 
    encoding: 'utf8', 
    stdio: ['ignore', 'pipe', 'ignore'] 
  });
  const parsed = JSON.parse(res);
  if (parsed.dependencies) {
    for (const key in parsed.dependencies) {
      localVersions[key] = parsed.dependencies[key].version;
    }
  }
} catch (e) {
  if (e.stdout) {
    try {
      const parsed = JSON.parse(e.stdout);
      if (parsed.dependencies) Object.assign(localVersions, parsed.dependencies);
    } catch (err) {}
  }
}

// 3. 核心逻辑:逐个检查并立即打印结果 console.log('正在检查版本状态...\n');
const tasks = [];

packages.forEach(pkg => {
  const localVer = localVersions[pkg.npm];
  let remoteVer = null;

  try {
    // 联网查询,可能会慢
    remoteVer = execSync(`npm view ${pkg.npm} version`, { encoding: 'utf8' }).trim();
  } catch (e) {
    console.log(`⚠️  [${pkg.name}] 查询失败,跳过。`);
    return; // 跳过当前循环
  }

  // 立即打印结果,提供实时反馈 if (!localVer) {
    console.log(`⚪ [${pkg.name}] 未安装 -> 🆕 ${remoteVer}`);
    tasks.push({ ...pkg, action: 'install' });
  } else if (localVer !== remoteVer) {
    console.log(`🔻 [${pkg.name}] 本地 ${localVer} -> 🆙 ${remoteVer}`);
    tasks.push({ ...pkg, action: 'update' });
  } else {
    console.log(`✅ [${pkg.name}] ${localVer} (已是最新)`);
  }
});

// 4. 执行更新 if (tasks.length === 0) {
  console.log('\n✨ 所有工具已是最新。');
} else {
  console.log();
  console.log(`🚀 开始更新 ${tasks.length} 个工具...`);
  console.log();

  tasks.forEach((task, index) => {
    process.stdout.write(`[${index + 1}/${tasks.length}] 更新 ${task.name}... `);
    try {
      execSync(`npm install -g ${task.npm}@latest`, { stdio: 'pipe' });
      console.log(`✔ 成功`);
    } catch (e) {
      console.log(`✖ 失败`);
      if (e.stderr) console.error('    ' + e.stderr.toString().split('\n')[0]); 
    }
  });
  
  console.log('\n✨ 全部完成。');
}

console.log('\n(按任意键退出)');
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', () => process.exit());

📌 转载信息
原作者:
Thousand_Star
转载时间:
2026/1/9 18:19:14

oketrust 的平台送了偶一张虚拟 U 卡
昨天我去试试注册 Gpt 能不能用

也就是 0 刀 Plus 我发现可以用 然后我发现一个神奇的事情就是可以无限绑定
什么意思呢 就是一卡多绑 我已经注册了快 100 个

一卡多绑的带来的问题就是封号 我已经封了一半了 但是可以无限绑定啊 然后我今天就基于上次 L 站佬友的脚本改一下了 脚本实现 自动注册 + 自动绑卡 + 自动取消订阅 + 自动记录 TXT 项目地址我放下面了

我同 IP 注册了几十卡 绑定没有发现弹验证啥的

封我就在注册 在绑定 在撸 反正 0 成本 无限子弹 冲!

项目地址

GitHub: https://github.com/7836246/gpt-auto-register


免责声明

本项目仅供 Python 自动化技术学习和研究使用。
请勿用于任何商业用途、大规模批量注册或其他违反 OpenAI 服务条款的行为。


📌 转载信息
原作者:
chinadoiphin
转载时间:
2026/1/6 18:40:41

前言

FOFA 注册用户每月有 3000 次 Web 查询下载额度,每个月会刷新。

但手动注册太麻烦了:填信息 → 识别验证码 → 等邮件 → 点激活链接…

于是就有了这个脚本,全自动完成注册流程。

功能

  • 自动识别图形验证码(ddddocr)
  • 临时邮箱自动收验证码并激活账号
  • 支持批量注册
  • 验证码错误自动重试
  • 固定密码 / 随机密码可选
  • 成功账号自动保存到文件

安装

pip install curl-cffi ddddocr

使用

# 交互式
python fofa.py
# 或指定数量
python fofa.py 5

项目地址


📌 转载信息
转载时间:
2026/1/6 12:14:29

但好在 2k 还是免费
只能搞个自动切换 2k 了

代码
// ==UserScript==
// @name         Genspark Nano Banana Auto 2K
// @namespace    https://genspark.ai/
// @version      0.1.0
// @description  Auto-select 2K image size on https://www.genspark.ai/agents?type=moa_generate_image
// @match        https://www.genspark.ai/agents*
// @match        https://genspark.ai/agents*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const TARGET_TYPE = 'moa_generate_image';
  const TARGET_SIZE_LABEL = '2K';

  const DEBUG = false;
  const LOG_PREFIX = '[Genspark Auto 2K]';
  const log = (...args) => {
    if (!DEBUG) return;
    // eslint-disable-next-line no-console
    console.log(LOG_PREFIX, ...args);
  };

  const normalizeText = (text) => (text || '').replace(/\s+/g, ' ').trim();

  const isTargetPage = () => {
    try {
      const url = new URL(location.href);
      return url.pathname === '/agents' && url.searchParams.get('type') === TARGET_TYPE;
    } catch {
      return false;
    }
  };

  const safeClick = (el) => {
    if (!el) return false;
    const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
    const x = rect ? Math.floor(rect.left + rect.width / 2) : 0;
    const y = rect ? Math.floor(rect.top + rect.height / 2) : 0;

    if (typeof PointerEvent === 'function' && rect && rect.width > 0 && rect.height > 0) {
      el.dispatchEvent(
        new PointerEvent('pointerdown', {
          bubbles: true,
          cancelable: true,
          view: window,
          pointerType: 'mouse',
          isPrimary: true,
          clientX: x,
          clientY: y,
          button: 0,
          buttons: 1,
        })
      );
      el.dispatchEvent(
        new PointerEvent('pointerup', {
          bubbles: true,
          cancelable: true,
          view: window,
          pointerType: 'mouse',
          isPrimary: true,
          clientX: x,
          clientY: y,
          button: 0,
          buttons: 0,
        })
      );
    }

    el.dispatchEvent(
      new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window,
        clientX: x,
        clientY: y,
        button: 0,
      })
    );

    return true;
  };

  const waitFor = (predicate, { timeoutMs = 15_000, pollMs = 250 } = {}) =>
    new Promise((resolve, reject) => {
      const start = Date.now();
      let done = false;

      const cleanup = () => {
        obs.disconnect();
        clearInterval(poller);
        clearTimeout(timeout);
      };

      const tryResolve = () => {
        if (done) return;

        let value = null;
        try {
          value = predicate();
        } catch {
          value = null;
        }

        if (value) {
          done = true;
          cleanup();
          resolve(value);
        }
      };

      const obs = new MutationObserver(tryResolve);
      const poller = setInterval(tryResolve, pollMs);
      const timeout = setTimeout(() => {
        if (done) return;
        done = true;
        cleanup();
        reject(new Error(`Timeout after ${timeoutMs}ms`));
      }, timeoutMs);

      obs.observe(document.documentElement, { childList: true, subtree: true });
      tryResolve();
    });

  const findSettingsPanel = () => {
    const panels = Array.from(document.querySelectorAll('.settings-panel'));
    return (
      panels.find((p) => normalizeText(p.textContent).includes('Image Size')) ||
      panels.find((p) => normalizeText(p.textContent).includes('Aspect Ratio')) ||
      null
    );
  };

  const findSettingTrigger = () => {
    const textNodes = Array.from(document.querySelectorAll('.text'));
    const settingText = textNodes.find((el) => normalizeText(el.textContent) === 'Setting');
    if (!settingText) return null;
    return (
      settingText.closest('[role="button"],button,a,.models-selected,.model-selected') || settingText
    );
  };

  const findSizeOption = (label) => {
    const panel = findSettingsPanel();
    if (!panel) return null;
    const options = Array.from(panel.querySelectorAll('.size-option'));
    const normalizedLabel = normalizeText(label);
    return (
      options.find((el) => normalizeText(el.textContent) === normalizedLabel) ||
      options.find((el) => normalizeText(el.textContent).startsWith(normalizedLabel)) ||
      null
    );
  };

  const ensure2KOnce = async () => {
    if (!isTargetPage()) return false;

    const trigger = await waitFor(findSettingTrigger).catch(() => null);
    if (!trigger) return false;

    const alreadyOpen = !!findSettingsPanel();
    if (!alreadyOpen) safeClick(trigger);

    const panel = await waitFor(findSettingsPanel).catch(() => null);
    if (!panel) return false;

    const option2k = findSizeOption(TARGET_SIZE_LABEL);
    if (!option2k) return false;

    if (!option2k.classList.contains('selected')) {
      log('Selecting', TARGET_SIZE_LABEL);
      safeClick(option2k);
    } else {
      log('Already selected', TARGET_SIZE_LABEL);
    }
    return true;
  };

  let runInFlight = null;
  const scheduleEnsure2K = () => {
    if (runInFlight) return;
    runInFlight = (async () => {
      try {
        await ensure2KOnce();
      } finally {
        runInFlight = null;
      }
    })();
  };

  const hookHistory = () => {
    const emit = () => window.dispatchEvent(new Event('genspark:auto-2k:urlchange'));
    const wrap = (method) =>
      function (...args) {
        const ret = method.apply(this, args);
        emit();
        return ret;
      };

    history.pushState = wrap(history.pushState);
    history.replaceState = wrap(history.replaceState);
    window.addEventListener('popstate', emit, { passive: true });
    window.addEventListener('genspark:auto-2k:urlchange', scheduleEnsure2K, { passive: true });
  };

  hookHistory();
  scheduleEnsure2K();
})();

📌 转载信息
原作者:
lueluelue
转载时间:
2025/12/30 12:10:07

全自动看网课 ((支持倍速))

这才是真正得解放了我的生产力,解决了某些网站没有脚本自动完成的难题

另外补一个 timer hooker 脚本(源自油猴某大佬,适用于所有网站加速)

// ==UserScript==
// @name            计时器掌控者|视频广告跳过|视频广告加速器
// @name:en         TimerHooker
// @namespace       https://gitee.com/HGJing/everthing-hook/
// @version         1.0.62
// @description     控制网页计时器速度|加速跳过页面计时广告|视频快进(慢放)|跳过广告|支持几乎所有网页.
// @description:en  it can hook the timer speed to change.
// @include         *
// @require         https://greasyfork.org/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251
// @author          Cangshi
// @match           http://*/*
// @run-at          document-start
// @grant           none
// @license         GPL-3.0-or-later
// @downloadURL https://update.greasyfork.org/scripts/372673/%E8%AE%A1%E6%97%B6%E5%99%A8%E6%8E%8C%E6%8E%A7%E8%80%85%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E8%B7%B3%E8%BF%87%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E5%8A%A0%E9%80%9F%E5%99%A8.user.js
// @updateURL https://update.greasyfork.org/scripts/372673/%E8%AE%A1%E6%97%B6%E5%99%A8%E6%8E%8C%E6%8E%A7%E8%80%85%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E8%B7%B3%E8%BF%87%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E5%8A%A0%E9%80%9F%E5%99%A8.meta.js
// ==/UserScript==
/**
 * ---------------------------
 * Time: 2017/11/20 19:28.
 * Author: Cangshi
 * View: http://palerock.cn
 * ---------------------------
 */

/**
 * 1. hook Object.defineProperty | Object.defineProperties
 * 2. set configurable: true
 * 3. delete property
 * 4. can set property for onxx event method
 */

window.isDOMLoaded = false;
window.isDOMRendered = false;

document.addEventListener('readystatechange', function () {
    if (document.readyState === "interactive" || document.readyState === "complete") {
        window.isDOMLoaded = true;
    }
});

~function (global) {

    var workerURLs = [];
    var extraElements = [];
    var suppressEvents = {};

    var helper = function (eHookContext, timerContext, util) {
        return {
            applyUI: function () {
                var style = '._th-container ._th-item{margin-bottom:3px;position:relative;width:0;height:0;cursor:pointer;opacity:.3;background-color:aquamarine;border-radius:100%;text-align:center;line-height:30px;-webkit-transition:all .35s;-o-transition:all .35s;transition:all .35s;right:30px}._th-container ._th-item,._th-container ._th-click-hover,._th_cover-all-show-times ._th_times{-webkit-box-shadow:-3px 4px 12px -5px black;box-shadow:-3px 4px 12px -5px black}._th-container:hover ._th-item._item-x2{margin-left:18px;width:40px;height:40px;line-height:40px}._th-container:hover ._th-item._item-x-2{margin-left:17px;width:38px;height:38px;line-height:38px}._th-container:hover ._th-item._item-xx2{width:36px;height:36px;margin-left:16px;line-height:36px}._th-container:hover ._th-item._item-xx-2{width:32px;height:32px;line-height:32px;margin-left:14px}._th-container:hover ._th-item._item-reset{width:30px;line-height:30px;height:30px;margin-left:10px}._th-click-hover{position:relative;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;height:45px;width:45px;cursor:pointer;opacity:.3;border-radius:100%;background-color:aquamarine;text-align:center;line-height:45px;right:0}._th-container:hover{left:-5px}._th-container{font-size:12px;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;left:-35px;top:20%;position:fixed;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:100000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}._th-container ._th-item:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th-container ._th-item:active{opacity:.9;background-color:#1b3a26;color:aliceblue}._th-container:hover ._th-click-hover{opacity:.8}._th-container:hover ._th-item{opacity:.6;right:0}._th-container ._th-click-hover:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th_cover-all-show-times{position:fixed;top:0;right:0;width:100%;height:100%;z-index:99999;opacity:1;font-weight:900;font-size:30px;color:#4f4f4f;background-color:rgba(0,0,0,0.1)}._th_cover-all-show-times._th_hidden{z-index:-99999;opacity:0;-webkit-transition:1s all;-o-transition:1s all;transition:1s all}._th_cover-all-show-times ._th_times{width:300px;height:300px;border-radius:50%;background-color:rgba(127,255,212,0.51);text-align:center;line-height:300px;position:absolute;top:50%;right:50%;margin-top:-150px;margin-right:-150px}';

                var displayNum = (1 / timerContext._percentage).toFixed(2);

                // 在页面左边添加一个半圆便于修改
                var html = '<div class="_th-container">\n' +
                    '    <div class="_th-click-hover _item-input">\n' +
                    '        x' + displayNum + '\n' +
                    '    </div>\n' +
                    '    <div class="_th-item _item-x2">&gt;</div>\n' +
                    '    <div class="_th-item _item-x-2">&lt;</div>\n' +
                    '    <div class="_th-item _item-xx2">&gt;&gt;</div>\n' +
                    '    <div class="_th-item _item-xx-2">&lt;&lt;</div>\n' +
                    '    <div class="_th-item _item-reset">O</div>\n' +
                    '</div>\n' +
                    '<div class="_th_cover-all-show-times _th_hidden">\n' +
                    '    <div class="_th_times">x' + displayNum + '</div>\n' +
                    '</div>' +
                    '';
                var stylenode = document.createElement('style');
                stylenode.setAttribute("type", "text/css");
                if (stylenode.styleSheet) {// IE
                    stylenode.styleSheet.cssText = style;
                } else {// w3c
                    var cssText = document.createTextNode(style);
                    stylenode.appendChild(cssText);
                }
                var node = document.createElement('div');
                node.innerHTML = html;

                var clickMapper = {
                    '_item-input': function () {
                        changeTime();
                    },
                    '_item-x2': function () {
                        changeTime(2, 0, true);
                    },
                    '_item-x-2': function () {
                        changeTime(-2, 0, true);
                    },
                    '_item-xx2': function () {
                        changeTime(0, 2);
                    },
                    '_item-xx-2': function () {
                        changeTime(0, -2);
                    },
                    '_item-reset': function () {
                        changeTime(0, 0, false, true);
                    }
                };

                Object.keys(clickMapper).forEach(function (className) {
                    var exec = clickMapper[className];
                    var targetEle = node.getElementsByClassName(className)[0];
                    if (targetEle) {
                        targetEle.onclick = exec;
                    }
                });

                if (!global.isDOMLoaded) {
                    document.addEventListener('readystatechange', function () {
                        if ((document.readyState === "interactive" || document.readyState === "complete") && !global.isDOMRendered) {
                            document.head.appendChild(stylenode);
                            document.body.appendChild(node);
                            global.isDOMRendered = true;
                            console.log('Time Hooker Works!');
                        }
                    });
                } else {
                    document.head.appendChild(stylenode);
                    document.body.appendChild(node);
                    global.isDOMRendered = true;
                    console.log('Time Hooker Works!');
                }
            },
            applyGlobalAction: function (timer) {
                // 界面半圆按钮点击的方法
                timer.changeTime = function (anum, cnum, isa, isr) {
                    if (isr) {
                        global.timer.change(1);
                        return;
                    }
                    if (!global.timer) {
                        return;
                    }
                    var result;
                    if (!anum && !cnum) {
                        var t = prompt("输入欲改变计时器变化倍率(当前:" + 1 / timerContext._percentage + ")");
                        if (t == null) {
                            return;
                        }
                        if (isNaN(parseFloat(t))) {
                            alert("请输入正确的数字");
                            timer.changeTime();
                            return;
                        }
                        if (parseFloat(t) <= 0) {
                            alert("倍率不能小于等于0");
                            timer.changeTime();
                            return;
                        }
                        result = 1 / parseFloat(t);
                    } else {
                        if (isa && anum) {
                            if (1 / timerContext._percentage <= 1 && anum < 0) {
                                return;
                            }
                            result = 1 / (1 / timerContext._percentage + anum);
                        } else {
                            if (cnum <= 0) {
                                cnum = 1 / -cnum
                            }
                            result = 1 / ((1 / timerContext._percentage) * cnum);
                        }
                    }
                    timer.change(result);
                };
                global.changeTime = timer.changeTime;
            },
            applyHooking: function () {
                var _this = this;
                // 劫持循环计时器
                eHookContext.hookReplace(window, 'setInterval', function (setInterval) {
                    return _this.getHookedTimerFunction('interval', setInterval);
                });
                // 劫持单次计时
                eHookContext.hookReplace(window, 'setTimeout', function (setTimeout) {
                    return _this.getHookedTimerFunction('timeout', setTimeout)
                });
                // 劫持循环计时器的清除方法
                eHookContext.hookBefore(window, 'clearInterval', function (method, args) {
                    _this.redirectNewestId(args);
                });
                // 劫持循环计时器的清除方法
                eHookContext.hookBefore(window, 'clearTimeout', function (method, args) {
                    _this.redirectNewestId(args);
                });
                var newFunc = this.getHookedDateConstructor();
                eHookContext.hookClass(window, 'Date', newFunc, '_innerDate', ['now']);
                Date.now = function () {
                    return new Date().getTime();
                };
                eHookContext.hookedToString(timerContext._Date.now, Date.now);
                var objToString = Object.prototype.toString;

                Object.prototype.toString = function toString() {
                    'use strict';
                    if (this instanceof timerContext._mDate) {
                        return '[object Date]';
                    } else {
                        return objToString.call(this);
                    }
                };

                eHookContext.hookedToString(objToString, Object.prototype.toString);
                eHookContext.hookedToString(timerContext._setInterval, setInterval);
                eHookContext.hookedToString(timerContext._setTimeout, setTimeout);
                eHookContext.hookedToString(timerContext._clearInterval, clearInterval);
                timerContext._mDate = window.Date;
                this.hookShadowRoot();
            },
            getHookedDateConstructor: function () {
                return function () {
                    if (arguments.length === 1) {
                        Object.defineProperty(this, '_innerDate', {
                            configurable: false,
                            enumerable: false,
                            value: new timerContext._Date(arguments[0]),
                            writable: false
                        });
                        return;
                    } else if (arguments.length > 1) {
                        var definedValue;
                        switch (arguments.length) {
                            case 2:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1]
                                );
                                break;
                            case 3:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                );
                                break;
                            case 4:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                );
                                break;
                            case 5:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4]
                                );
                                break;
                            case 6:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4],
                                    arguments[5]
                                );
                                break;
                            default:
                            case 7:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4],
                                    arguments[5],
                                    arguments[6]
                                );
                                break;
                        }

                        Object.defineProperty(this, '_innerDate', {
                            configurable: false,
                            enumerable: false,
                            value: definedValue,
                            writable: false
                        });
                        return;
                    }
                    var now = timerContext._Date.now();
                    var passTime = now - timerContext.__lastDatetime;
                    var hookPassTime = passTime * (1 / timerContext._percentage);
                    // console.log(__this.__lastDatetime + hookPassTime, now,__this.__lastDatetime + hookPassTime - now);
                    Object.defineProperty(this, '_innerDate', {
                        configurable: false,
                        enumerable: false,
                        value: new timerContext._Date(timerContext.__lastMDatetime + hookPassTime),
                        writable: false
                    });
                };
            },
            getHookedTimerFunction: function (type, timer) {
                var property = '_' + type + 'Ids';
                return function () {
                    var uniqueId = timerContext.genUniqueId();
                    var callback = arguments[0];
                    if (typeof callback === 'string') {
                        callback += ';timer.notifyExec(' + uniqueId + ')';
                        arguments[0] = callback;
                    }
                    if (typeof callback === 'function') {
                        arguments[0] = function () {
                            var returnValue = callback.apply(this, arguments);
                            timerContext.notifyExec(uniqueId);
                            return returnValue;
                        }
                    }
                    // 储存原始时间间隔
                    var originMS = arguments[1];
                    // 获取变速时间间隔
                    arguments[1] *= timerContext._percentage;
                    var resultId = timer.apply(window, arguments);
                    // 保存每次使用计时器得到的id以及参数等
                    timerContext[property][resultId] = {
                        args: arguments,
                        originMS: originMS,
                        originId: resultId,
                        nowId: resultId,
                        uniqueId: uniqueId,
                        oldPercentage: timerContext._percentage,
                        exceptNextFireTime: timerContext._Date.now() + originMS
                    };
                    return resultId;
                };
            },
            redirectNewestId: function (args) {
                var id = args[0];
                if (timerContext._intervalIds[id]) {
                    args[0] = timerContext._intervalIds[id].nowId;
                    // 清除该记录id
                    delete timerContext._intervalIds[id];
                }
                if (timerContext._timeoutIds[id]) {
                    args[0] = timerContext._timeoutIds[id].nowId;
                    // 清除该记录id
                    delete timerContext._timeoutIds[id];
                }
            },
            registerShortcutKeys: function (timer) {
                // 快捷键注册
                addEventListener('keydown', function (e) {
                    switch (e.keyCode) {
                        case 57:
                            if (e.ctrlKey || e.altKey) {
                                // custom
                                timer.changeTime();
                            }
                            break;
                        // [=]
                        case 190:
                        case 187: {
                            if (e.ctrlKey) {
                                // console.log('+2');
                                timer.changeTime(2, 0, true);
                            } else if (e.altKey) {
                                // console.log('xx2');
                                timer.changeTime(0, 2);
                            }
                            break;
                        }
                        // [-]
                        case 188:
                        case 189: {
                            if (e.ctrlKey) {
                                // console.log('-2');
                                timer.changeTime(-2, 0, true);
                            } else if (e.altKey) {
                                // console.log('xx-2');
                                timer.changeTime(0, -2);
                            }
                            break;
                        }
                        // [0]
                        case 48: {
                            if (e.ctrlKey || e.altKey) {
                                // console.log('reset');
                                timer.changeTime(0, 0, false, true);
                            }
                            break;
                        }
                        default:
                        // console.log(e);
                    }
                });
            },
            /**
             * 当计时器速率被改变时调用的回调方法
             * @param percentage
             * @private
             */
            percentageChangeHandler: function (percentage) {
                // 改变所有的循环计时
                util.ergodicObject(timerContext, timerContext._intervalIds, function (idObj, id) {
                    idObj.args[1] = Math.floor((idObj.originMS || 1) * percentage);
                    // 结束原来的计时器
                    this._clearInterval.call(window, idObj.nowId);
                    // 新开一个计时器
                    idObj.nowId = this._setInterval.apply(window, idObj.args);
                });
                // 改变所有的延时计时
                util.ergodicObject(timerContext, timerContext._timeoutIds, function (idObj, id) {
                    var now = this._Date.now();
                    var exceptTime = idObj.exceptNextFireTime;
                    var oldPercentage = idObj.oldPercentage;
                    var time = exceptTime - now;
                    if (time < 0) {
                        time = 0;
                    }
                    var changedTime = Math.floor(percentage / oldPercentage * time);
                    idObj.args[1] = changedTime;
                    // 重定下次执行时间
                    idObj.exceptNextFireTime = now + changedTime;
                    idObj.oldPercentage = percentage;
                    // 结束原来的计时器
                    this._clearTimeout.call(window, idObj.nowId);
                    // 新开一个计时器
                    idObj.nowId = this._setTimeout.apply(window, idObj.args);
                });
            },
            hookShadowRoot: function () {
                var origin = Element.prototype.attachShadow;
                eHookContext.hookAfter(Element.prototype, 'attachShadow',
                    function (m, args, result) {
                        extraElements.push(result);
                        return result;
                    }, false);
                eHookContext.hookedToString(origin, Element.prototype.attachShadow);
            },
            hookDefine: function () {
                const _this = this;
                eHookContext.hookBefore(Object, 'defineProperty', function (m, args) {
                    var option = args[2];
                    var ele = args[0];
                    var key = args[1];
                    var afterArgs = _this.hookDefineDetails(ele, key, option);
                    afterArgs.forEach((arg, i) => {
                        args[i] = arg;
                    })
                });
                eHookContext.hookBefore(Object, 'defineProperties', function (m, args) {
                    var option = args[1];
                    var ele = args[0];
                    if (ele && ele instanceof Element) {
                        Object.keys(option).forEach(key => {
                            var o = option[key];
                            var afterArgs = _this.hookDefineDetails(ele, key, o);
                            args[0] = afterArgs[0];
                            delete option[key];
                            option[afterArgs[1]] = afterArgs[2]
                        })
                    }
                })
            },
            hookDefineDetails: function (target, key, option) {
                if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) {
                    option.configurable = true;
                }
                if (target instanceof HTMLVideoElement && key === 'playbackRate') {
                    option.configurable = true;
                    console.warn('[Timer Hook]', '已阻止默认操作视频倍率');
                    key = 'playbackRate_hooked'
                }
                return [target, key, option];
            },
            suppressEvent: function (ele, eventName) {
                if (ele) {
                    delete ele['on' + eventName];
                    delete ele['on' + eventName];
                    delete ele['on' + eventName];
                    ele['on' + eventName] = undefined;
                }
                if (!suppressEvents[eventName]) {
                    eHookContext.hookBefore(EventTarget.prototype, 'addEventListener',
                        function (m, args) {
                            var eName = args[0];
                            if (eventName === eName) {
                                console.warn(eventName, 'event suppressed.')
                                args[0] += 'suppressed';
                            }
                        }, false);
                    suppressEvents[eventName] = true;
                }
            },
            changePlaybackRate: function (ele, rate) {
                delete ele.playbackRate;
                delete ele.playbackRate;
                delete ele.playbackRate;
                ele.playbackRate = rate
                if (rate !== 1) {
                    timerContext.defineProperty.call(Object, ele, 'playbackRate', {
                        configurable: true,
                        get: function () {
                            return 1;
                        },
                        set: function () {
                        }
                    });
                }
            }
        }
    };

    var normalUtil = {
        isInIframe: function () {
            let is = global.parent !== global;
            try {
                is = is && global.parent.document.body.tagName !== 'FRAMESET'
            } catch (e) {
                // ignore
            }
            return is;
        },
        listenParentEvent: function (handler) {
            global.addEventListener('message', function (e) {
                var data = e.data;
                var type = data.type || '';
                if (type === 'changePercentage') {
                    handler(data.percentage || 0);
                }
            })
        },
        sentChangesToIframe: function (percentage) {
            var iframes = document.querySelectorAll('iframe') || [];
            var frames = document.querySelectorAll('frame');
            if (iframes.length) {
                for (var i = 0; i < iframes.length; i++) {
                    iframes[i].contentWindow.postMessage(
                        {type: 'changePercentage', percentage: percentage}, '*');
                }
            }
            if (frames.length) {
                for (var j = 0; j < frames.length; j++) {
                    frames[j].contentWindow.postMessage(
                        {type: 'changePercentage', percentage: percentage}, '*');
                }
            }
        }
    };

    var querySelectorAll = function (ele, selector, includeExtra) {
        var elements = ele.querySelectorAll(selector);
        elements = Array.prototype.slice.call(elements || []);
        if (includeExtra) {
            extraElements.forEach(function (element) {
                elements = elements.concat(querySelectorAll(element, selector, false));
            })
        }
        return elements;
    };

    var generate = function () {
        return function (util) {
            // disable worker
            workerURLs.forEach(function (url) {
                if (util.urlMatching(location.href, 'http.*://.*' + url + '.*')) {
                    window['Worker'] = undefined;
                    console.log('Worker disabled');
                }
            });
            var eHookContext = this;
            var timerHooker = {
                // 用于储存计时器的id和参数
                _intervalIds: {},
                _timeoutIds: {},
                _auoUniqueId: 1,
                // 计时器速率
                __percentage: 1.0,
                // 劫持前的原始的方法
                _setInterval: window['setInterval'],
                _clearInterval: window['clearInterval'],
                _clearTimeout: window['clearTimeout'],
                _setTimeout: window['setTimeout'],
                _Date: window['Date'],
                __lastDatetime: new Date().getTime(),
                __lastMDatetime: new Date().getTime(),
                videoSpeedInterval: 1000,
                defineProperty: Object.defineProperty,
                defineProperties: Object.defineProperties,
                genUniqueId: function () {
                    return this._auoUniqueId++;
                },
                notifyExec: function (uniqueId) {
                    var _this = this;
                    if (uniqueId) {
                        // 清除 timeout 所储存的记录
                        var timeoutInfos = Object.values(this._timeoutIds).filter(
                            function (info) {
                                return info.uniqueId === uniqueId;
                            }
                        );
                        timeoutInfos.forEach(function (info) {
                            _this._clearTimeout.call(window, info.nowId);
                            delete _this._timeoutIds[info.originId]
                        })
                    }
                    // console.log(uniqueId, 'called')
                },
                /**
                 * 初始化方法
                 */
                init: function () {
                    var timerContext = this;
                    var h = helper(eHookContext, timerContext, util);

                    h.hookDefine();
                    h.applyHooking();

                    // 设定百分比属性被修改的回调
                    Object.defineProperty(timerContext, '_percentage', {
                        get: function () {
                            return timerContext.__percentage;
                        },
                        set: function (percentage) {
                            if (percentage === timerContext.__percentage) {
                                return percentage;
                            }
                            h.percentageChangeHandler(percentage);
                            timerContext.__percentage = percentage;
                            return percentage;
                        }
                    });

                    if (!normalUtil.isInIframe()) {
                        console.log('[TimeHooker]', 'loading outer window...');
                        h.applyUI();
                        h.applyGlobalAction(timerContext);
                        h.registerShortcutKeys(timerContext);
                    } else {
                        console.log('[TimeHooker]', 'loading inner window...');
                        normalUtil.listenParentEvent((function (percentage) {
                            console.log('[TimeHooker]', 'Inner Changed', percentage)
                            this.change(percentage);
                        }).bind(this))
                    }
                },
                /**
                 * 调用该方法改变计时器速率
                 * @param percentage
                 */
                change: function (percentage) {
                    this.__lastMDatetime = this._mDate.now();
                    this.__lastDatetime = this._Date.now();
                    this._percentage = percentage;
                    var oldNode = document.getElementsByClassName('_th-click-hover');
                    var oldNode1 = document.getElementsByClassName('_th_times');
                    var displayNum = (1 / this._percentage).toFixed(2);
                    (oldNode[0] || {}).innerHTML = 'x' + displayNum;
                    (oldNode1[0] || {}).innerHTML = 'x' + displayNum;
                    var a = document.getElementsByClassName('_th_cover-all-show-times')[0] || {};
                    a.className = '_th_cover-all-show-times';
                    this._setTimeout.bind(window)(function () {
                        a.className = '_th_cover-all-show-times _th_hidden';
                    }, 100);
                    this.changeVideoSpeed();
                    normalUtil.sentChangesToIframe(percentage);
                },
                changeVideoSpeed: function () {
                    var timerContext = this;
                    var h = helper(eHookContext, timerContext, util);
                    var rate = 1 / this._percentage;
                    rate > 16 && (rate = 16);
                    rate < 0.065 && (rate = 0.065);
                    var videos = querySelectorAll(document, 'video', true) || [];
                    if (videos.length) {
                        for (var i = 0; i < videos.length; i++) {
                            h.changePlaybackRate(videos[i], rate);
                        }
                    }
                }
            };
            // 默认初始化
            timerHooker.init();
            return timerHooker;
        }
    };

    if (global.eHook) {
        global.eHook.plugins({
            name: 'timer',
            /**
             * 插件装载
             * @param util
             */
            mount: generate()
        });
    }
}(window);


📌 转载信息
原作者:
Jaxon-jp
转载时间:
2025/12/30 10:23:32