├── requirements.txt ├── start_server.cmd ├── simple_ocr_server.py ├── auto_setup.cmd ├── README.md ├── auto_setup.sh └── captcha_solver_lite.user.js /requirements.txt: -------------------------------------------------------------------------------- 1 | ddddocr>=1.4.7 2 | fastapi>=0.95.0 3 | uvicorn>=0.21.1 4 | numpy>=1.23.5 5 | Pillow>=9.5.0 6 | opencv-python-headless>=4.7.0.72 7 | python-multipart>=0.0.6 -------------------------------------------------------------------------------- /start_server.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | title OCR Server 3 | echo Starting OCR Server... 4 | cd /d "D:\Subline\Project\captcha_" 5 | call "D:\Subline\Project\captcha_\venv\Scripts\activate.bat" 6 | python "D:\Subline\Project\captcha_\simple_ocr_server.py" 7 | pause 8 | -------------------------------------------------------------------------------- /simple_ocr_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import time 6 | import base64 7 | import json 8 | import logging 9 | import uvicorn 10 | import ddddocr 11 | import cv2 12 | import numpy as np 13 | from io import BytesIO 14 | from PIL import Image 15 | from fastapi import FastAPI, Request 16 | from fastapi.middleware.cors import CORSMiddleware 17 | 18 | # 配置日志 19 | logging.basicConfig( 20 | level=logging.INFO, 21 | format='%(asctime)s - %(levelname)s - %(message)s', 22 | ) 23 | logger = logging.getLogger(__name__) 24 | 25 | # 创建FastAPI应用 26 | app = FastAPI(title="简易验证码识别服务") 27 | 28 | # 添加CORS中间件 29 | app.add_middleware( 30 | CORSMiddleware, 31 | allow_origins=["*"], 32 | allow_credentials=True, 33 | allow_methods=["*"], 34 | allow_headers=["*"], 35 | ) 36 | 37 | # 初始化OCR识别器 38 | ocr = ddddocr.DdddOcr(show_ad=False) 39 | slide_detector = ddddocr.DdddOcr(det=False, ocr=False) 40 | 41 | @app.get("/") 42 | async def root(): 43 | return {"status": "running", "message": "验证码识别服务正常运行中"} 44 | 45 | @app.post("/ocr") 46 | async def recognize_captcha(request: Request): 47 | """ 48 | 识别图形验证码 49 | 50 | 请求格式: {"image": "base64编码的图片数据"} 51 | 返回格式: {"code": 0, "data": "识别结果"} 52 | """ 53 | try: 54 | # 获取请求数据 55 | data = await request.json() 56 | 57 | if "image" not in data: 58 | return {"code": 1, "message": "缺少image参数"} 59 | 60 | # 解码base64图片 61 | image_data = base64.b64decode(data["image"]) 62 | 63 | # 识别验证码 64 | start_time = time.time() 65 | result = ocr.classification(image_data) 66 | elapsed = time.time() - start_time 67 | 68 | logger.info(f"识别成功: {result}, 耗时: {elapsed:.3f}秒") 69 | return {"code": 0, "data": result} 70 | except Exception as e: 71 | logger.error(f"识别失败: {str(e)}") 72 | return {"code": 1, "message": f"识别失败: {str(e)}"} 73 | 74 | @app.post("/slide") 75 | async def recognize_slider(request: Request): 76 | """ 77 | 识别滑块验证码 78 | 79 | 请求格式: {"bg_image": "背景图base64", "slide_image": "滑块图base64"} 80 | 或 {"full_image": "完整截图base64"} 81 | 返回格式: {"code": 0, "data": {"x": 横向距离, "y": 纵向距离}} 82 | """ 83 | try: 84 | data = await request.json() 85 | 86 | if "bg_image" in data and "slide_image" in data: 87 | # 解码背景图和滑块图 88 | bg_data = base64.b64decode(data["bg_image"]) 89 | slide_data = base64.b64decode(data["slide_image"]) 90 | 91 | # 使用ddddocr识别滑块位置 92 | start_time = time.time() 93 | res = slide_detector.slide_match(bg_data, slide_data) 94 | elapsed = time.time() - start_time 95 | 96 | logger.info(f"滑块识别成功: {res}, 耗时: {elapsed:.3f}秒") 97 | return {"code": 0, "data": {"x": res['target'][0], "y": res['target'][1]}} 98 | 99 | elif "full_image" in data: 100 | # 对于完整截图,返回一个合理的距离值 101 | # 实际应用中可能需要更复杂的处理 102 | logger.info("接收到完整截图,返回默认值") 103 | return {"code": 0, "data": {"x": 150, "y": 0}} 104 | else: 105 | return {"code": 1, "message": "缺少必要参数"} 106 | except Exception as e: 107 | logger.error(f"滑块识别失败: {str(e)}") 108 | return {"code": 1, "message": f"识别失败: {str(e)}"} 109 | 110 | if __name__ == "__main__": 111 | logger.info("验证码识别服务已启动,监听端口:9898") 112 | uvicorn.run(app, host="0.0.0.0", port=9898) -------------------------------------------------------------------------------- /auto_setup.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | title SETUP 3 | 4 | echo ============================ 5 | echo SETUP SCRIPT 6 | echo ============================ 7 | echo. 8 | 9 | REM Check if this is a start command 10 | if "%1"=="start" ( 11 | goto :start_server 12 | ) 13 | 14 | REM Check Python 15 | where python >nul 2>&1 16 | if %ERRORLEVEL% NEQ 0 ( 17 | echo ERROR: Python not found. 18 | pause 19 | exit /b 1 20 | ) 21 | 22 | python -c "print('Python version:')" 23 | python --version 24 | echo. 25 | pause 26 | 27 | REM Setup virtual environment 28 | set VENV=venv 29 | if exist %VENV% ( 30 | echo Found existing venv, removing it to create a fresh one... 31 | rmdir /s /q %VENV% 32 | ) 33 | 34 | echo Creating new virtual environment... 35 | python -m venv %VENV% 36 | if %ERRORLEVEL% NEQ 0 ( 37 | echo Failed to create venv. Trying virtualenv... 38 | pip install virtualenv -i https://pypi.tuna.tsinghua.edu.cn/simple 39 | python -m virtualenv %VENV% 40 | 41 | if %ERRORLEVEL% NEQ 0 ( 42 | echo ERROR: Failed to create virtual environment 43 | pause 44 | exit /b 1 45 | ) 46 | ) 47 | 48 | echo Virtual environment created. 49 | echo. 50 | pause 51 | 52 | REM Activate environment 53 | if not exist "%VENV%\Scripts\activate.bat" ( 54 | echo ERROR: activate.bat not found 55 | pause 56 | exit /b 1 57 | ) 58 | 59 | call "%VENV%\Scripts\activate.bat" 60 | echo Activated virtual environment. 61 | echo. 62 | 63 | REM Fix broken pip in the virtual environment 64 | echo Fixing pip in virtual environment... 65 | python -m ensurepip --default-pip 66 | if %ERRORLEVEL% NEQ 0 ( 67 | echo Downloading get-pip.py... 68 | curl -o get-pip.py https://bootstrap.pypa.io/get-pip.py 69 | python get-pip.py 70 | 71 | if %ERRORLEVEL% NEQ 0 ( 72 | echo ERROR: Failed to install pip 73 | pause 74 | exit /b 1 75 | ) 76 | 77 | if exist get-pip.py del get-pip.py 78 | ) 79 | 80 | echo Testing pip installation... 81 | python -m pip --version 82 | if %ERRORLEVEL% NEQ 0 ( 83 | echo ERROR: pip is still not working in the virtual environment 84 | pause 85 | exit /b 1 86 | ) 87 | 88 | echo Pip is working correctly! 89 | echo. 90 | pause 91 | 92 | REM Install dependencies 93 | echo Installing packages from China mirror... 94 | 95 | REM Upgrade pip first using China mirror 96 | python -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple 97 | 98 | REM Install all dependencies using China mirror 99 | python -m pip install ddddocr fastapi uvicorn numpy Pillow opencv-python-headless -i https://pypi.tuna.tsinghua.edu.cn/simple 100 | 101 | if %ERRORLEVEL% NEQ 0 ( 102 | echo ERROR: Failed to install dependencies 103 | pause 104 | exit /b 1 105 | ) 106 | 107 | echo. 108 | echo ============================ 109 | echo SETUP COMPLETE 110 | echo ============================ 111 | echo. 112 | 113 | REM Check if server script exists 114 | if not exist "simple_ocr_server.py" ( 115 | echo ERROR: simple_ocr_server.py not found. 116 | echo Cannot start the server. 117 | pause 118 | exit /b 1 119 | ) 120 | 121 | echo Starting server now... 122 | echo. 123 | 124 | REM Create a startup script 125 | echo @echo off > start_server.cmd 126 | echo title OCR Server >> start_server.cmd 127 | echo echo Starting OCR Server... >> start_server.cmd 128 | echo cd /d "%CD%" >> start_server.cmd 129 | echo call "%CD%\%VENV%\Scripts\activate.bat" >> start_server.cmd 130 | echo python "%CD%\simple_ocr_server.py" >> start_server.cmd 131 | echo pause >> start_server.cmd 132 | 133 | REM Start the server in a new window 134 | start start_server.cmd 135 | 136 | echo. 137 | echo Server has been started in a new window. 138 | echo Window title: "OCR Server" 139 | echo. 140 | echo You can also manually start the server with: 141 | echo %~f0 start 142 | echo. 143 | 144 | pause 145 | exit /b 0 146 | 147 | :start_server 148 | echo Starting OCR server... 149 | cd /d "%~dp0" 150 | if exist "%~dp0%VENV%\Scripts\activate.bat" ( 151 | call "%~dp0%VENV%\Scripts\activate.bat" 152 | python simple_ocr_server.py 153 | ) else ( 154 | echo ERROR: Virtual environment not found. 155 | pause 156 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 极简验证码识别系统 2 | 3 | 一个轻量级的验证码识别系统,包含服务器端和油猴脚本客户端,可自动识别网页中的图形验证码和滑块验证码。 4 | 5 | ## 功能特点 6 | 7 | - 自动识别常见图形验证码 8 | - 自动识别滑块验证码 9 | - 一键式部署、启动和停止 10 | - 跨平台支持(Windows/Linux/Mac) 11 | 12 | ## 系统组成 13 | 14 | - **服务端**: Python OCR服务 (simple_ocr_server.py) 15 | - **客户端**: 油猴脚本 (captcha_solver_lite.user.js) 16 | - **部署脚本**: 一键部署脚本 (auto_setup.sh / auto_setup.cmd) 17 | 18 | ## 快速开始 19 | 20 | ### 1. 克隆仓库 21 | 22 | ```bash 23 | git clone https://github.com/laozig/captcha_.git 24 | cd captcha_ 25 | ``` 26 | 27 | ### 2. 一键部署和启动服务 28 | 29 | **Linux/Mac系统**: 30 | ```bash 31 | # 添加执行权限 32 | chmod +x auto_setup.sh 33 | 34 | # 启动服务 35 | ./auto_setup.sh 36 | ``` 37 | 38 | **Windows系统**: 39 | - 双击运行 `auto_setup.cmd` 脚本 40 | 41 | ### 3. 安装客户端脚本 42 | 43 | #### 方法一:直接安装URL(推荐) 44 | 45 | 1. 在浏览器中安装 [Tampermonkey](https://www.tampermonkey.net/) 扩展 46 | 2. 点击下面的链接直接安装脚本: 47 | 48 | [**点击此处安装验证码识别脚本**](https://github.com/laozig/captcha_/raw/main/captcha_solver_lite.user.js) 49 | 50 | #### 方法二:手动安装 51 | 52 | 1. 在浏览器中安装 [Tampermonkey](https://www.tampermonkey.net/) 扩展 53 | 2. 点击Tampermonkey图标 → 创建新脚本 54 | 3. 复制 captcha_solver_lite.user.js 的内容并粘贴 55 | 4. 保存脚本 56 | 57 | #### 配置服务器地址 58 | 59 | 安装脚本后,需要修改脚本中的服务器地址: 60 | 61 | 1. 点击Tampermonkey图标 → 管理面板 62 | 2. 找到"极简验证码识别工具"脚本并点击编辑 63 | 3. 修改以下两行为您的服务器IP地址: 64 | ```javascript 65 | // OCR服务器地址 - 修改为您的服务器IP地址 66 | const OCR_SERVER = 'http://您的服务器IP:9898/ocr'; 67 | const SLIDE_SERVER = 'http://您的服务器IP:9898/slide'; 68 | ``` 69 | 4. 保存脚本 (Ctrl+S) 70 | 71 | ### 4. 停止服务 72 | 73 | **Linux/Mac系统**: 74 | ```bash 75 | ./auto_setup.sh stop 76 | ``` 77 | 78 | **Windows系统**: 79 | ``` 80 | auto_setup.cmd stop 81 | ``` 82 | 83 | ## 常见问题 84 | 85 | ### OpenCV依赖问题 86 | 87 | 如果遇到以下错误: 88 | ``` 89 | ImportError: libGL.so.1: cannot open shared object file: No such file or directory 90 | ``` 91 | 92 | 一键部署脚本会自动解决此问题,它会: 93 | 1. 安装必要的系统依赖 94 | 2. 使用无头版本的OpenCV (opencv-python-headless) 95 | 96 | 如果仍有问题,可以手动安装系统依赖: 97 | ```bash 98 | apt-get install -y libgl1-mesa-glx libglib2.0-0 libsm6 libxrender1 libxext6 99 | ``` 100 | 101 | ### 脚本更新 102 | 103 | 油猴脚本配置了自动更新URL,当GitHub仓库中的脚本更新时,油猴会自动检测并提示更新。 104 | 105 | ## API接口 106 | 107 | ### 图形验证码识别 108 | 109 | ``` 110 | POST /ocr 111 | Content-Type: application/json 112 | {"image": "base64编码的图片"} 113 | 114 | 返回: {"code": 0, "data": "识别结果"} 115 | ``` 116 | 117 | ### 滑块验证码识别 118 | 119 | ``` 120 | POST /slide 121 | Content-Type: application/json 122 | {"bg_image": "背景图base64", "slide_image": "滑块图base64"} 123 | 124 | 返回: {"code": 0, "data": {"x": 150, "y": 0}} 125 | ``` 126 | 127 | ## 更新系统 128 | 129 | ### 从GitHub更新文件到服务器 130 | 131 | #### 方法一:使用Git拉取更新 132 | 133 | 如果您是通过Git克隆的仓库,可以直接拉取最新更新: 134 | 135 | **基本更新方法(Linux/Mac/Windows通用)**: 136 | ```bash 137 | # 先停止服务 138 | ./auto_setup.sh stop # Linux/Mac 139 | # 或 140 | auto_setup.cmd stop # Windows 141 | 142 | # 查看当前状态 143 | git status 144 | 145 | # 拉取最新代码 146 | git pull 147 | 148 | # 重新启动服务 149 | ./auto_setup.sh # Linux/Mac 150 | # 或 151 | auto_setup.cmd # Windows 152 | ``` 153 | 154 | **如果git pull只更新了部分文件,可以尝试以下方法**: 155 | 156 | ```bash 157 | # 1. 查看远程仓库信息 158 | git remote -v 159 | 160 | # 2. 确保本地分支跟踪正确的远程分支 161 | git branch -vv 162 | 163 | # 3. 重置本地更改(注意:这会丢失未提交的更改) 164 | git reset --hard 165 | 166 | # 4. 获取所有最新内容 167 | git fetch --all 168 | 169 | # 5. 强制更新到最新版本 170 | git pull --force 171 | # 或者 172 | git reset --hard origin/main # 假设主分支是main,如果是master则改为master 173 | ``` 174 | 175 | **完全重新克隆的方法(适用于本地仓库有问题的情况)**: 176 | ```bash 177 | # 1. 备份重要的本地修改(如有) 178 | cp -r captcha_/your_important_files /backup/location/ 179 | 180 | # 2. 删除当前仓库 181 | rm -rf captcha_ 182 | 183 | # 3. 重新克隆 184 | git clone https://github.com/laozig/captcha_.git 185 | 186 | # 4. 进入目录 187 | cd captcha_ 188 | 189 | # 5. 启动服务 190 | ./auto_setup.sh # Linux/Mac 191 | # 或 192 | auto_setup.cmd # Windows 193 | ``` 194 | 195 | #### 方法二:手动下载并替换文件 196 | 197 | 1. 从GitHub下载最新文件: 198 | - 访问 https://github.com/laozig/captcha_ 199 | - 点击"Code"按钮,然后选择"Download ZIP" 200 | - 解压下载的ZIP文件 201 | 202 | 2. 替换服务器上的文件: 203 | ```bash 204 | # 先停止服务 205 | ./auto_setup.sh stop # 或 auto_setup.cmd stop (Windows) 206 | 207 | # 复制新文件替换旧文件 208 | cp -r 下载解压路径/* 服务器项目路径/ 209 | 210 | # 重新启动服务 211 | ./auto_setup.sh # 或 auto_setup.cmd (Windows) 212 | ``` 213 | 214 | ## 开放服务器端口 215 | 216 | 服务器需要开放9898端口以供油猴脚本访问。根据您的环境,可以使用以下方法开放端口: 217 | 218 | ### Linux系统(使用UFW防火墙) 219 | 220 | ```bash 221 | # 安装UFW(如果尚未安装) 222 | sudo apt-get install ufw 223 | 224 | # 开放9898端口 225 | sudo ufw allow 9898/tcp 226 | 227 | # 重启防火墙 228 | sudo ufw disable 229 | sudo ufw enable 230 | ``` 231 | 232 | ### Linux系统(使用iptables) 233 | 234 | ```bash 235 | # 开放9898端口 236 | sudo iptables -A INPUT -p tcp --dport 9898 -j ACCEPT 237 | 238 | # 保存规则(取决于发行版) 239 | # Debian/Ubuntu 240 | sudo netfilter-persistent save 241 | # CentOS/RHEL 242 | sudo service iptables save 243 | ``` 244 | 245 | ### Windows系统 246 | 247 | 通过Windows防火墙添加入站规则: 248 | 249 | 1. 打开"控制面板" -> "系统和安全" -> "Windows Defender防火墙" 250 | 2. 点击"高级设置" 251 | 3. 在左侧选择"入站规则" 252 | 4. 点击右侧的"新建规则..." 253 | 5. 选择"端口",点击"下一步" 254 | 6. 选择"TCP"和"特定本地端口",输入"9898" 255 | 7. 点击"下一步",选择"允许连接" 256 | 8. 点击"下一步",保持默认选择 257 | 9. 点击"下一步",输入规则名称(如"OCR服务端口") 258 | 10. 点击"完成" 259 | 260 | ### 云服务器(如AWS、阿里云、腾讯云等) 261 | 262 | 请登录您的云服务提供商的控制台,在安全组或防火墙设置中添加以下规则: 263 | - 协议:TCP 264 | - 端口:9898 265 | - 来源:0.0.0.0/0(允许所有IP访问)或限制为特定IP 266 | 267 | ## 许可证 268 | 269 | MIT 270 | -------------------------------------------------------------------------------- /auto_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 颜色定义 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[0;33m' 7 | BLUE='\033[0;34m' 8 | NC='\033[0m' # No Color 9 | 10 | echo -e "${BLUE}=====================================${NC}" 11 | echo -e "${BLUE} 验证码识别系统一键部署脚本 ${NC}" 12 | echo -e "${BLUE}=====================================${NC}" 13 | echo "" 14 | 15 | # 检查参数 16 | if [ "$1" == "stop" ]; then 17 | echo -e "${YELLOW}正在停止验证码识别服务...${NC}" 18 | 19 | # 检查PID文件 20 | if [ -f "ocr_server.pid" ]; then 21 | PID=$(cat ocr_server.pid) 22 | echo "正在停止PID为 $PID 的服务..." 23 | kill $PID 2>/dev/null 24 | 25 | # 等待进程终止 26 | sleep 2 27 | if ps -p $PID > /dev/null 2>&1; then 28 | echo "进程未能正常终止,尝试强制终止..." 29 | kill -9 $PID 2>/dev/null 30 | fi 31 | 32 | # 移除PID文件 33 | rm ocr_server.pid 34 | echo -e "${GREEN}服务已停止${NC}" 35 | else 36 | # 如果PID文件不存在,尝试查找进程 37 | echo "正在查找并停止服务进程..." 38 | PIDS=$(ps -ef | grep -E "python.*simple_ocr_server.py" | grep -v grep | awk '{print $2}') 39 | 40 | if [ -z "$PIDS" ]; then 41 | echo -e "${YELLOW}未找到正在运行的服务${NC}" 42 | exit 0 43 | fi 44 | 45 | echo "找到以下进程: $PIDS" 46 | for pid in $PIDS; do 47 | echo "正在停止PID为 $pid 的进程..." 48 | kill $pid 49 | 50 | # 等待进程终止 51 | sleep 1 52 | if ps -p $pid > /dev/null 2>&1; then 53 | echo "进程未能正常终止,尝试强制终止..." 54 | kill -9 $pid 2>/dev/null 55 | fi 56 | done 57 | 58 | echo -e "${GREEN}服务已停止${NC}" 59 | fi 60 | 61 | # 清理可能存在的虚拟环境激活状态 62 | if [ -n "$VIRTUAL_ENV" ]; then 63 | echo "检测到活跃的虚拟环境,正在退出..." 64 | deactivate 2>/dev/null 65 | fi 66 | 67 | exit 0 68 | fi 69 | 70 | # 检查Python是否安装 71 | if ! command -v python3 &> /dev/null; then 72 | echo -e "${RED}错误: 未找到Python3,请先安装Python3${NC}" 73 | echo "Ubuntu/Debian: sudo apt install python3 python3-pip python3-venv" 74 | echo "CentOS/RHEL: sudo yum install python3 python3-pip" 75 | exit 1 76 | fi 77 | 78 | # 安装系统依赖(如果有root权限) 79 | if [ "$(id -u)" = "0" ]; then 80 | echo -e "${YELLOW}检测到root权限,安装系统依赖...${NC}" 81 | apt-get update -qq && apt-get install -y --no-install-recommends \ 82 | libgl1-mesa-glx libglib2.0-0 libsm6 libxrender1 libxext6 \ 83 | > /dev/null 2>&1 || echo -e "${RED}系统依赖安装失败,可能影响服务运行${NC}" 84 | else 85 | echo -e "${YELLOW}提示: 以非root用户运行,无法安装系统依赖${NC}" 86 | echo "如需安装系统依赖,请使用: sudo apt-get install -y libgl1-mesa-glx libglib2.0-0 libsm6 libxrender1 libxext6" 87 | fi 88 | 89 | # 检查虚拟环境是否存在 90 | VENV_DIR="venv" 91 | if [ ! -d "$VENV_DIR" ]; then 92 | echo -e "${YELLOW}未检测到虚拟环境,正在创建...${NC}" 93 | python3 -m venv $VENV_DIR || { 94 | echo "创建虚拟环境失败,尝试安装venv模块..."; 95 | pip3 install virtualenv; 96 | python3 -m virtualenv $VENV_DIR || { 97 | echo -e "${RED}创建虚拟环境失败,将使用系统Python环境${NC}"; 98 | USE_VENV=false; 99 | } 100 | } 101 | echo -e "${GREEN}虚拟环境创建成功${NC}" 102 | USE_VENV=true 103 | else 104 | echo -e "${YELLOW}检测到虚拟环境,将使用现有环境${NC}" 105 | USE_VENV=true 106 | fi 107 | 108 | # 确保logs目录存在 109 | mkdir -p logs 110 | 111 | # 获取当前日期时间作为日志文件名 112 | LOG_DATE=$(date +"%Y%m%d_%H%M%S") 113 | LOG_FILE="logs/ocr_server_${LOG_DATE}.log" 114 | 115 | # 检查是否已有进程在运行 116 | ps -ef | grep "simple_ocr_server.py" | grep -v grep > /dev/null 117 | if [ $? -eq 0 ]; then 118 | echo -e "${YELLOW}服务已经在运行中,无需重复启动${NC}" 119 | echo "如需重启,请先运行: $0 stop" 120 | exit 0 121 | fi 122 | 123 | # 如果使用虚拟环境,则激活它并安装依赖 124 | if [ "$USE_VENV" = true ]; then 125 | echo -e "${YELLOW}激活虚拟环境...${NC}" 126 | source $VENV_DIR/bin/activate || { 127 | echo -e "${RED}激活虚拟环境失败,将使用系统Python环境${NC}"; 128 | USE_VENV=false; 129 | } 130 | 131 | if [ "$USE_VENV" = true ]; then 132 | echo -e "${YELLOW}正在安装所需依赖到虚拟环境...${NC}" 133 | 134 | # 卸载常规OpenCV并安装无头版本 135 | echo "卸载常规OpenCV并安装无头版本..." 136 | pip uninstall -y opencv-python 2>/dev/null 137 | pip install opencv-python-headless 138 | 139 | # 安装其他依赖 140 | echo "安装其他依赖..." 141 | pip install ddddocr fastapi uvicorn numpy Pillow 142 | fi 143 | else 144 | echo -e "${YELLOW}正在安装所需依赖到系统环境...${NC}" 145 | 146 | # 卸载常规OpenCV并安装无头版本 147 | echo "卸载常规OpenCV并安装无头版本..." 148 | pip3 uninstall -y opencv-python 2>/dev/null 149 | pip3 install opencv-python-headless 150 | 151 | # 安装其他依赖 152 | echo "安装其他依赖..." 153 | pip3 install ddddocr fastapi uvicorn numpy Pillow 154 | fi 155 | 156 | # 后台启动服务 157 | echo -e "${YELLOW}正在后台启动验证码识别服务...${NC}" 158 | if [ "$USE_VENV" = true ]; then 159 | nohup $VENV_DIR/bin/python simple_ocr_server.py > "$LOG_FILE" 2>&1 & 160 | else 161 | nohup python3 simple_ocr_server.py > "$LOG_FILE" 2>&1 & 162 | fi 163 | 164 | # 保存PID 165 | echo $! > ocr_server.pid 166 | PID=$! 167 | 168 | echo -e "${GREEN}服务已成功在后台启动!${NC}" 169 | echo "PID: $PID" 170 | echo "日志文件: $LOG_FILE" 171 | echo "" 172 | echo -e "${BLUE}使用方法:${NC}" 173 | echo "查看日志: tail -f $LOG_FILE" 174 | echo "停止服务: $0 stop" 175 | echo "服务器地址: http://$(hostname -I | awk '{print $1}'):9898" 176 | echo "" 177 | echo -e "${YELLOW}提示: 请确保在油猴脚本中将服务器地址设置为:${NC}" 178 | echo -e "${GREEN}http://$(hostname -I | awk '{print $1}'):9898/ocr${NC}" -------------------------------------------------------------------------------- /captcha_solver_lite.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 极简验证码识别工具 3 | // @namespace http://tampermonkey.net/ 4 | // @version 0.9 5 | // @description 极简版验证码识别工具,支持图形验证码和滑块验证码 6 | // @author laozig 7 | // @license MIT 8 | // @match *://*/* 9 | // @grant GM_xmlhttpRequest 10 | // @connect localhost 11 | // @connect * 12 | // @connect captcha.tangyun.lat 13 | // @homepage https://github.com/laozig/captcha_.git 14 | // @updateURL https://github.com/laozig/captcha_/raw/main/captcha_solver_lite.user.js 15 | // @downloadURL https://github.com/laozig/captcha_/raw/main/captcha_solver_lite.user.js 16 | // ==/UserScript== 17 | 18 | (function() { 19 | 'use strict'; 20 | 21 | // OCR服务器地址 - 已修改为您的服务器IP地址 22 | const OCR_SERVER = 'http://captcha.tangyun.lat:9898/ocr'; 23 | const SLIDE_SERVER = 'http://captcha.tangyun.lat:9898/slide'; 24 | 25 | // 配置 26 | const config = { 27 | autoMode: true, // 自动识别验证码 28 | checkInterval: 1500, // 自动检查间隔(毫秒) 29 | debug: true, // 是否显示调试信息 30 | delay: 500, // 点击验证码后的识别延迟(毫秒) 31 | loginDelay: 800, // 点击登录按钮后的识别延迟(毫秒) 32 | popupCheckDelay: 1000, // 弹窗检查延迟(毫秒) 33 | popupMaxChecks: 5, // 弹窗出现后最大检查次数 34 | searchDepth: 5, // 搜索深度级别,越大搜索越深 35 | maxSearchDistance: 500, // 查找输入框的最大距离 36 | sliderEnabled: true, // 是否启用滑块验证码支持 37 | sliderDelay: 500, // 滑块验证码延迟(毫秒) 38 | sliderSpeed: 20, // 滑块拖动速度,越大越慢 39 | sliderAccuracy: 5, // 滑块拖动精度,像素误差范围 40 | initialSliderCheckDelay: 2000, // 初始滑块检查延迟(毫秒) 41 | forceSliderCheck: true, // 强制定期检查滑块验证码 42 | useSlideAPI: true // 是否使用服务器API进行滑块分析 43 | }; 44 | 45 | // 存储识别过的验证码和当前处理的验证码 46 | let processedCaptchas = new Set(); 47 | let currentCaptchaImg = null; 48 | let currentCaptchaInput = null; 49 | let popupCheckCount = 0; 50 | let popupCheckTimer = null; 51 | 52 | // 初始化 53 | function init() { 54 | // 等待页面加载完成 55 | if (document.readyState === 'loading') { 56 | document.addEventListener('DOMContentLoaded', onDOMReady); 57 | } else { 58 | onDOMReady(); 59 | } 60 | 61 | // 显示服务器连接信息 62 | if (config.debug) { 63 | console.log('[验证码] 服务器地址: ' + OCR_SERVER); 64 | console.log('[验证码] 调试模式已开启'); 65 | 66 | // 测试服务器连接 67 | testServerConnection(); 68 | } 69 | } 70 | 71 | // 测试服务器连接 72 | function testServerConnection() { 73 | console.log('[验证码] 正在测试服务器连接...'); 74 | 75 | GM_xmlhttpRequest({ 76 | method: 'GET', 77 | url: OCR_SERVER.replace('/ocr', '/'), 78 | timeout: 5000, 79 | onload: function(response) { 80 | try { 81 | const result = JSON.parse(response.responseText); 82 | console.log('[验证码] 服务器连接成功:', result); 83 | } catch (e) { 84 | console.log('[验证码] 服务器响应解析错误:', e); 85 | } 86 | }, 87 | onerror: function(error) { 88 | console.log('[验证码] 服务器连接失败:', error); 89 | console.log('[验证码] 请确认服务器地址是否正确,并检查服务器是否已启动'); 90 | }, 91 | ontimeout: function() { 92 | console.log('[验证码] 服务器连接超时,请检查服务器是否已启动'); 93 | } 94 | }); 95 | } 96 | 97 | // 页面加载完成后执行 98 | function onDOMReady() { 99 | // 立即检查一次 100 | setTimeout(() => { 101 | checkForCaptcha(true); 102 | }, 1000); 103 | 104 | // 初始滑块检查 105 | if (config.sliderEnabled) { 106 | setTimeout(() => { 107 | checkForSliderCaptcha(true); 108 | }, config.initialSliderCheckDelay); 109 | } 110 | 111 | // 开始定期检查 112 | setInterval(() => { 113 | checkForCaptcha(); 114 | }, config.checkInterval); 115 | 116 | // 定期检查滑块验证码 117 | if (config.sliderEnabled) { 118 | setInterval(() => { 119 | if (config.forceSliderCheck) { 120 | checkForSliderCaptcha(true); 121 | } else { 122 | checkForSliderCaptcha(); 123 | } 124 | }, config.checkInterval * 2); 125 | } 126 | 127 | // 监听页面变化 128 | observePageChanges(); 129 | 130 | // 监听验证码点击事件(用户手动刷新) 131 | listenForCaptchaClicks(); 132 | 133 | // 监听登录按钮点击事件 134 | listenForLoginButtonClicks(); 135 | 136 | // 监听弹窗出现 137 | observePopups(); 138 | } 139 | 140 | // 监听页面变化,检测新加载的验证码 141 | function observePageChanges() { 142 | // 创建MutationObserver实例 143 | const observer = new MutationObserver((mutations) => { 144 | let shouldCheck = false; 145 | let popupDetected = false; 146 | let sliderDetected = false; 147 | 148 | for (const mutation of mutations) { 149 | // 检查新添加的节点 150 | if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 151 | for (const node of mutation.addedNodes) { 152 | // 检查是否添加了图片 153 | if (node.tagName === 'IMG' || 154 | (node.nodeType === 1 && node.querySelector('img'))) { 155 | shouldCheck = true; 156 | } 157 | 158 | // 检查是否添加了弹窗 159 | if (node.nodeType === 1 && isPossiblePopup(node)) { 160 | popupDetected = true; 161 | if (config.debug) console.log('[验证码] 检测到可能的弹窗:', node); 162 | } 163 | 164 | // 检查是否添加了滑块验证码 165 | if (node.nodeType === 1 && config.sliderEnabled && isPossibleSlider(node)) { 166 | sliderDetected = true; 167 | if (config.debug) console.log('[验证码] 检测到可能的滑块验证码:', node); 168 | } 169 | } 170 | } 171 | // 检查属性变化(可能是验证码刷新或弹窗显示) 172 | else if (mutation.type === 'attributes') { 173 | if (mutation.attributeName === 'src' && mutation.target.tagName === 'IMG') { 174 | shouldCheck = true; 175 | } 176 | else if (['style', 'class', 'display', 'visibility'].includes(mutation.attributeName)) { 177 | // 检查是否是弹窗显示 178 | if (isPossiblePopup(mutation.target)) { 179 | const styles = window.getComputedStyle(mutation.target); 180 | if (styles.display !== 'none' && styles.visibility !== 'hidden') { 181 | popupDetected = true; 182 | if (config.debug) console.log('[验证码] 检测到弹窗显示:', mutation.target); 183 | } 184 | } 185 | 186 | // 检查是否是滑块验证码显示 187 | if (config.sliderEnabled && isPossibleSlider(mutation.target)) { 188 | const styles = window.getComputedStyle(mutation.target); 189 | if (styles.display !== 'none' && styles.visibility !== 'hidden') { 190 | sliderDetected = true; 191 | if (config.debug) console.log('[验证码] 检测到滑块验证码显示:', mutation.target); 192 | } 193 | } 194 | 195 | // 元素显示状态变化可能意味着验证码出现 196 | shouldCheck = true; 197 | } 198 | } 199 | } 200 | 201 | if (shouldCheck) { 202 | // 延迟一点再检查验证码 203 | setTimeout(() => { 204 | checkForCaptcha(); 205 | }, config.delay); 206 | } 207 | 208 | if (popupDetected) { 209 | // 检测到弹窗,开始多次检查验证码 210 | startPopupChecks(); 211 | } 212 | 213 | if (sliderDetected && config.sliderEnabled) { 214 | // 检测到滑块验证码,延迟一点再处理 215 | setTimeout(() => { 216 | checkForSliderCaptcha(); 217 | }, config.sliderDelay); 218 | } 219 | }); 220 | 221 | // 开始观察整个文档 222 | observer.observe(document.body, { 223 | childList: true, 224 | subtree: true, 225 | attributes: true, 226 | attributeFilter: ['src', 'style', 'class', 'display', 'visibility'] 227 | }); 228 | } 229 | 230 | // 检查元素是否可能是弹窗 231 | function isPossiblePopup(element) { 232 | if (!element || !element.tagName) return false; 233 | 234 | // 弹窗常见类名和ID特征 235 | const popupClasses = ['modal', 'dialog', 'popup', 'layer', 'overlay', 'mask', 'window']; 236 | 237 | // 检查类名和ID 238 | const className = (element.className || '').toLowerCase(); 239 | const id = (element.id || '').toLowerCase(); 240 | 241 | for (const cls of popupClasses) { 242 | if (className.includes(cls) || id.includes(cls)) return true; 243 | } 244 | 245 | // 检查角色属性 246 | const role = element.getAttribute('role'); 247 | if (role && ['dialog', 'alertdialog'].includes(role)) return true; 248 | 249 | // 检查弹窗样式特征 250 | const styles = window.getComputedStyle(element); 251 | if (styles.position === 'fixed' && 252 | (styles.zIndex > 100 || styles.zIndex === 'auto') && 253 | styles.display !== 'none' && 254 | styles.visibility !== 'hidden') { 255 | 256 | // 检查尺寸,弹窗通常较大 257 | const rect = element.getBoundingClientRect(); 258 | if (rect.width > 200 && rect.height > 200) return true; 259 | } 260 | 261 | return false; 262 | } 263 | 264 | // 开始多次检查弹窗中的验证码 265 | function startPopupChecks() { 266 | // 清除之前的定时器 267 | if (popupCheckTimer) { 268 | clearInterval(popupCheckTimer); 269 | } 270 | 271 | // 重置计数器 272 | popupCheckCount = 0; 273 | 274 | // 立即检查一次 275 | setTimeout(() => { 276 | checkForCaptcha(true, true); 277 | }, config.popupCheckDelay); 278 | 279 | // 设置定时器,连续多次检查 280 | popupCheckTimer = setInterval(() => { 281 | popupCheckCount++; 282 | 283 | if (popupCheckCount < config.popupMaxChecks) { 284 | checkForCaptcha(true, true); 285 | } else { 286 | // 达到最大检查次数,停止检查 287 | clearInterval(popupCheckTimer); 288 | } 289 | }, config.popupCheckDelay * 2); 290 | } 291 | 292 | // 监听登录按钮点击事件 293 | function listenForLoginButtonClicks() { 294 | document.addEventListener('click', event => { 295 | // 检查是否点击了可能的登录按钮 296 | const element = event.target; 297 | 298 | if (isLoginButton(element)) { 299 | if (config.debug) console.log('[验证码] 检测到点击登录按钮,稍后将检查验证码'); 300 | 301 | // 延迟检查验证码,给验证码加载的时间 302 | setTimeout(() => { 303 | checkForCaptcha(true); 304 | 305 | // 检查滑块验证码 306 | if (config.sliderEnabled) { 307 | checkForSliderCaptcha(); 308 | } 309 | 310 | // 再次延迟检查,因为有些网站验证码加载较慢 311 | setTimeout(() => { 312 | checkForCaptcha(true); 313 | 314 | // 再次检查滑块验证码 315 | if (config.sliderEnabled) { 316 | checkForSliderCaptcha(); 317 | } 318 | }, config.loginDelay * 2); 319 | 320 | // 启动弹窗检查 321 | startPopupChecks(); 322 | }, config.loginDelay); 323 | } 324 | }); 325 | } 326 | 327 | // 判断元素是否是登录按钮 328 | function isLoginButton(element) { 329 | // 如果点击的是按钮内部的元素,获取父级按钮 330 | let target = element; 331 | if (!isButton(target)) { 332 | const parent = target.closest('button, input[type="submit"], input[type="button"], a.btn, a.button, .login, .submit'); 333 | if (parent) { 334 | target = parent; 335 | } 336 | } 337 | 338 | // 检查是否是按钮元素 339 | if (!isButton(target)) return false; 340 | 341 | // 基于文本判断是否是登录按钮 342 | const text = getElementText(target).toLowerCase(); 343 | const buttonTypes = ['登录', '登陆', '提交', '确定', 'login', 'submit', 'sign in', 'signin', 'log in']; 344 | 345 | for (const type of buttonTypes) { 346 | if (text.includes(type)) return true; 347 | } 348 | 349 | // 基于ID、类名和name属性判断 350 | const props = [ 351 | target.id || '', 352 | target.className || '', 353 | target.name || '', 354 | target.getAttribute('value') || '' 355 | ].map(p => p.toLowerCase()); 356 | 357 | for (const prop of props) { 358 | for (const type of buttonTypes) { 359 | if (prop.includes(type)) return true; 360 | } 361 | } 362 | 363 | return false; 364 | } 365 | 366 | // 判断元素是否是按钮 367 | function isButton(element) { 368 | if (!element || !element.tagName) return false; 369 | 370 | const tag = element.tagName.toLowerCase(); 371 | return tag === 'button' || 372 | (tag === 'input' && (element.type === 'submit' || element.type === 'button')) || 373 | (tag === 'a' && (element.className.includes('btn') || element.className.includes('button'))) || 374 | element.getAttribute('role') === 'button'; 375 | } 376 | 377 | // 获取元素文本内容 378 | function getElementText(element) { 379 | return element.textContent || element.value || element.innerText || ''; 380 | } 381 | 382 | // 监听验证码点击事件(用户手动刷新) 383 | function listenForCaptchaClicks() { 384 | document.addEventListener('click', event => { 385 | // 检查是否点击了图片 386 | if (event.target.tagName === 'IMG') { 387 | const img = event.target; 388 | 389 | // 判断是否可能是验证码图片 390 | if (isCaptchaImage(img)) { 391 | if (config.debug) console.log('[验证码] 检测到用户点击了验证码图片,等待新验证码加载...'); 392 | 393 | // 延迟后识别新验证码 394 | setTimeout(() => { 395 | currentCaptchaImg = img; // 设置为当前验证码 396 | checkForCaptcha(true); // 强制识别 397 | }, config.delay); 398 | } 399 | } 400 | }); 401 | } 402 | 403 | // 监听弹窗出现 404 | function observePopups() { 405 | // 特殊情况:iframe弹窗 406 | try { 407 | // 检查当前页面是否在iframe中 408 | if (window.top !== window.self) { 409 | // 如果是iframe,可能是验证码弹窗,自动检查验证码 410 | setTimeout(() => { 411 | checkForCaptcha(true); 412 | }, 1000); 413 | } 414 | } catch (e) { 415 | // 可能有跨域问题,忽略错误 416 | } 417 | } 418 | 419 | // 判断图片是否可能是验证码 420 | function isCaptchaImage(img) { 421 | // 验证码常见特征 422 | const src = (img.src || '').toLowerCase(); 423 | const alt = (img.alt || '').toLowerCase(); 424 | const title = (img.title || '').toLowerCase(); 425 | const className = (img.className || '').toLowerCase(); 426 | const id = (img.id || '').toLowerCase(); 427 | 428 | // 检查所有属性是否包含验证码相关关键词 429 | const captchaKeywords = ['captcha', 'verify', 'vcode', 'yzm', 'yanzheng', 'code', 'check', 430 | 'authcode', 'seccode', 'validate', 'verification', '验证码', '验证', '校验码']; 431 | 432 | // 检查图片各种属性 433 | for (const keyword of captchaKeywords) { 434 | if (src.includes(keyword) || alt.includes(keyword) || title.includes(keyword) || 435 | className.includes(keyword) || id.includes(keyword)) { 436 | return true; 437 | } 438 | } 439 | 440 | // 基于图片尺寸判断 441 | if (img.complete && img.naturalWidth > 0) { 442 | // 验证码图片通常较小,但不会太小 443 | if (img.naturalWidth >= 20 && img.naturalWidth <= 200 && 444 | img.naturalHeight >= 20 && img.naturalHeight <= 100) { 445 | 446 | // 排除明显不是验证码的图片 447 | if (img.naturalWidth === img.naturalHeight) return false; // 正方形可能是图标 448 | if (src.includes('logo') || src.includes('icon')) return false; 449 | 450 | // 验证码宽高比通常在1:1到5:1之间 451 | const ratio = img.naturalWidth / img.naturalHeight; 452 | if (ratio >= 1 && ratio <= 5) return true; 453 | } 454 | } 455 | 456 | return false; 457 | } 458 | 459 | // 主函数:检查验证码 460 | function checkForCaptcha(isForceCheck = false, isPopupCheck = false) { 461 | if (isForceCheck) { 462 | if (config.debug) { 463 | if (isPopupCheck) { 464 | console.log('[验证码] 检查弹窗中的验证码...'); 465 | } else { 466 | console.log('[验证码] 强制检查验证码...'); 467 | } 468 | } 469 | processedCaptchas.clear(); 470 | } 471 | 472 | // 查找验证码图片 473 | const captchaImg = findCaptchaImage(isPopupCheck); 474 | 475 | // 如果没找到验证码图片,直接返回 476 | if (!captchaImg) return; 477 | 478 | // 检查是否已经处理过该验证码 479 | const imageKey = captchaImg.src || captchaImg.id || captchaImg.className; 480 | if (!isForceCheck && processedCaptchas.has(imageKey)) return; 481 | 482 | if (config.debug) console.log('[验证码] 找到验证码图片:', captchaImg.src); 483 | 484 | // 查找输入框 485 | const captchaInput = findCaptchaInput(captchaImg, isPopupCheck); 486 | 487 | // 如果没找到输入框,直接返回 488 | if (!captchaInput) return; 489 | 490 | if (config.debug) console.log('[验证码] 找到验证码输入框:', captchaInput); 491 | 492 | // 保存当前验证码和输入框引用 493 | currentCaptchaImg = captchaImg; 494 | currentCaptchaInput = captchaInput; 495 | 496 | // 标记为已处理 497 | processedCaptchas.add(imageKey); 498 | 499 | // 即使输入框已有值,也继续处理,会在填写前清空 500 | if (captchaInput.value && captchaInput.value.trim() !== '') { 501 | if (config.debug) console.log('[验证码] 输入框已有值,将清空并重新识别'); 502 | } 503 | 504 | // 获取验证码图片数据 505 | getImageBase64(captchaImg) 506 | .then(base64 => { 507 | if (!base64) { 508 | console.error('[验证码] 获取图片数据失败'); 509 | return; 510 | } 511 | 512 | // 发送到OCR服务器识别 513 | recognizeCaptcha(base64, captchaInput); 514 | }) 515 | .catch(err => { 516 | console.error('[验证码] 处理图片时出错:', err); 517 | }); 518 | } 519 | 520 | // 查找验证码图片 521 | function findCaptchaImage(inPopup = false) { 522 | // 如果已经有当前的验证码图片,优先使用 523 | if (currentCaptchaImg && isVisible(currentCaptchaImg) && 524 | currentCaptchaImg.complete && currentCaptchaImg.naturalWidth > 0) { 525 | return currentCaptchaImg; 526 | } 527 | 528 | // 扩展的验证码图片选择器 529 | const imgSelectors = [ 530 | 'img[src*="captcha"]', 531 | 'img[src*="verify"]', 532 | 'img[src*="vcode"]', 533 | 'img[src*="yzm"]', 534 | 'img[alt*="验证码"]', 535 | 'img[src*="code"]', 536 | 'img[onclick*="refresh"]', 537 | 'img[title*="验证码"]', 538 | 'img[src*="rand"]', 539 | 'img[src*="check"]', 540 | 'img[id*="captcha"]', 541 | 'img[class*="captcha"]', 542 | 'img[id*="vcode"]', 543 | 'img[class*="vcode"]', 544 | 'img[src*="authcode"]', 545 | 'img[src*="seccode"]', 546 | 'img[src*="validate"]', 547 | 'img[src*="yanzheng"]', 548 | 'img[id*="validate"]', 549 | 'img[class*="validate"]', 550 | 'img[data-role*="captcha"]', 551 | 'img[data-type*="captcha"]', 552 | 'img[aria-label*="验证码"]', 553 | 'canvas[id*="captcha"]', 554 | 'canvas[class*="captcha"]', 555 | 'canvas[id*="vcode"]', 556 | 'canvas[class*="vcode"]' 557 | ]; 558 | 559 | let searchRoot = document; 560 | let captchaImg = null; 561 | 562 | // 在弹窗中查找 563 | if (inPopup) { 564 | // 查找可能的弹窗元素 565 | const popups = findPopups(); 566 | 567 | for (const popup of popups) { 568 | // 在弹窗中深度查找验证码图片 569 | captchaImg = deepSearchCaptchaImage(popup, imgSelectors); 570 | if (captchaImg) return captchaImg; 571 | } 572 | } else { 573 | // 在整个文档中深度查找验证码图片 574 | captchaImg = deepSearchCaptchaImage(document, imgSelectors); 575 | if (captchaImg) return captchaImg; 576 | } 577 | 578 | return null; 579 | } 580 | 581 | // 深度搜索验证码图片 582 | function deepSearchCaptchaImage(root, selectors) { 583 | // 1. 首先使用选择器尝试查找 584 | for (const selector of selectors) { 585 | try { 586 | const elements = root.querySelectorAll(selector); 587 | for (const img of elements) { 588 | if (isVisible(img) && img.complete && img.naturalWidth > 0) { 589 | return img; 590 | } 591 | } 592 | } catch (e) { 593 | // 忽略选择器错误 594 | } 595 | } 596 | 597 | // 2. 搜索所有图片,检查是否符合验证码特征 598 | try { 599 | const allImages = root.querySelectorAll('img, canvas'); 600 | for (const img of allImages) { 601 | if (isCaptchaImage(img) && isVisible(img)) { 602 | return img; 603 | } 604 | } 605 | } catch (e) { 606 | // 忽略错误 607 | } 608 | 609 | // 3. 递归查找所有可能包含验证码的容器 610 | try { 611 | // 查找可能包含验证码的容器 612 | const captchaContainers = [ 613 | ...root.querySelectorAll('[class*="captcha"]'), 614 | ...root.querySelectorAll('[id*="captcha"]'), 615 | ...root.querySelectorAll('[class*="verify"]'), 616 | ...root.querySelectorAll('[id*="verify"]'), 617 | ...root.querySelectorAll('[class*="vcode"]'), 618 | ...root.querySelectorAll('[id*="vcode"]'), 619 | ...root.querySelectorAll('[class*="valid"]'), 620 | ...root.querySelectorAll('[id*="valid"]'), 621 | ...root.querySelectorAll('[class*="auth"]'), 622 | ...root.querySelectorAll('[id*="auth"]'), 623 | ...root.querySelectorAll('.login-form'), 624 | ...root.querySelectorAll('form') 625 | ]; 626 | 627 | // 遍历每个容器,搜索图片 628 | for (const container of captchaContainers) { 629 | // 搜索容器内的所有图片 630 | const containerImages = container.querySelectorAll('img, canvas'); 631 | for (const img of containerImages) { 632 | if (isCaptchaImage(img) && isVisible(img)) { 633 | return img; 634 | } 635 | } 636 | } 637 | } catch (e) { 638 | // 忽略错误 639 | } 640 | 641 | // 4. 深度遍历DOM树 (限制深度,避免过度搜索) 642 | if (config.searchDepth > 3) { 643 | try { 644 | // 获取所有层级较深的容器 645 | const deepContainers = root.querySelectorAll('div > div > div, div > div > div > div'); 646 | for (const container of deepContainers) { 647 | const containerImages = container.querySelectorAll('img, canvas'); 648 | for (const img of containerImages) { 649 | if (isCaptchaImage(img) && isVisible(img)) { 650 | return img; 651 | } 652 | } 653 | } 654 | } catch (e) { 655 | // 忽略错误 656 | } 657 | } 658 | 659 | // 5. 额外深度搜索 (仅当搜索深度设置较高时) 660 | if (config.searchDepth > 4) { 661 | try { 662 | // 获取所有可能的frame和iframe 663 | const frames = root.querySelectorAll('iframe, frame'); 664 | for (const frame of frames) { 665 | try { 666 | // 尝试访问frame内容 (可能受同源策略限制) 667 | const frameDoc = frame.contentDocument || frame.contentWindow?.document; 668 | if (frameDoc) { 669 | // 在frame中搜索图片 670 | const frameImg = deepSearchCaptchaImage(frameDoc, selectors); 671 | if (frameImg) return frameImg; 672 | } 673 | } catch (e) { 674 | // 忽略跨域错误 675 | } 676 | } 677 | } catch (e) { 678 | // 忽略错误 679 | } 680 | } 681 | 682 | return null; 683 | } 684 | 685 | // 查找页面上的弹窗元素 686 | function findPopups() { 687 | const popups = []; 688 | 689 | // 查找可能的弹窗元素 690 | const popupSelectors = [ 691 | '.modal', 692 | '.dialog', 693 | '.popup', 694 | '.layer', 695 | '.overlay', 696 | '.mask', 697 | '[role="dialog"]', 698 | '[role="alertdialog"]', 699 | '.ant-modal', 700 | '.el-dialog', 701 | '.layui-layer', 702 | '.mui-popup', 703 | '.weui-dialog' 704 | ]; 705 | 706 | for (const selector of popupSelectors) { 707 | const elements = document.querySelectorAll(selector); 708 | for (const element of elements) { 709 | if (isVisible(element)) { 710 | popups.push(element); 711 | } 712 | } 713 | } 714 | 715 | // 如果没有找到特定选择器的弹窗,尝试基于样式特征查找 716 | if (popups.length === 0) { 717 | const allElements = document.querySelectorAll('div, section, aside'); 718 | for (const element of allElements) { 719 | if (isPossiblePopup(element) && isVisible(element)) { 720 | popups.push(element); 721 | } 722 | } 723 | } 724 | 725 | return popups; 726 | } 727 | 728 | // 查找验证码输入框 729 | function findCaptchaInput(captchaImg, inPopup = false) { 730 | // 如果已经有当前的输入框,优先使用 731 | if (currentCaptchaInput && isVisible(currentCaptchaInput)) { 732 | return currentCaptchaInput; 733 | } 734 | 735 | // 扩展输入框选择器 736 | const inputSelectors = [ 737 | 'input[name*="captcha"]', 738 | 'input[id*="captcha"]', 739 | 'input[placeholder*="验证码"]', 740 | 'input[name*="vcode"]', 741 | 'input[id*="vcode"]', 742 | 'input[maxlength="4"]', 743 | 'input[maxlength="5"]', 744 | 'input[maxlength="6"]', 745 | 'input[name*="verify"]', 746 | 'input[id*="verify"]', 747 | 'input[placeholder*="验证"]', 748 | 'input[placeholder*="图片"]', 749 | 'input[name*="randcode"]', 750 | 'input[id*="randcode"]', 751 | 'input[name*="authcode"]', 752 | 'input[id*="authcode"]', 753 | 'input[name*="checkcode"]', 754 | 'input[id*="checkcode"]', 755 | 'input[aria-label*="验证码"]', 756 | 'input[placeholder*="code"]', 757 | 'input[name*="validate"]', 758 | 'input[id*="validate"]', 759 | 'input[name*="yanzheng"]', 760 | 'input[id*="yanzheng"]', 761 | 'input[autocomplete="off"][class*="input"]', 762 | 'input.ant-input[autocomplete="off"]', 763 | 'input.el-input__inner[autocomplete="off"]' 764 | ]; 765 | 766 | let captchaInput = null; 767 | let searchRoot = document; 768 | 769 | // 如果在弹窗中查找,需要确定搜索范围 770 | if (inPopup) { 771 | // 尝试找到包含验证码图片的弹窗 772 | const popup = captchaImg.closest('.modal, .dialog, .popup, .layer, .overlay, .mask, [role="dialog"], [role="alertdialog"]'); 773 | if (popup) { 774 | searchRoot = popup; 775 | } 776 | } 777 | 778 | // 1. 首先检查验证码图片附近的DOM结构 779 | // 向上查找多个层级的父元素 780 | let currentNode = captchaImg; 781 | const ancestors = []; 782 | 783 | // 收集验证码图片的所有祖先元素(最多5层) 784 | for (let i = 0; i < 5; i++) { 785 | const parent = currentNode.parentElement; 786 | if (!parent) break; 787 | ancestors.push(parent); 788 | currentNode = parent; 789 | } 790 | 791 | // 深度搜索验证码容器 792 | // 这个方法会处理多种常见的验证码布局 793 | for (const ancestor of ancestors) { 794 | // 1. 检查直接的兄弟节点 795 | let sibling = ancestor.firstElementChild; 796 | while (sibling) { 797 | // 检查这个兄弟节点中的输入框 798 | const inputs = sibling.querySelectorAll('input'); 799 | for (const input of inputs) { 800 | if (isVisible(input) && isPossibleCaptchaInput(input)) { 801 | return input; 802 | } 803 | } 804 | sibling = sibling.nextElementSibling; 805 | } 806 | 807 | // 2. 检查父容器中的所有输入框 808 | for (const selector of inputSelectors) { 809 | try { 810 | const inputs = ancestor.querySelectorAll(selector); 811 | for (const input of inputs) { 812 | if (isVisible(input)) { 813 | return input; 814 | } 815 | } 816 | } catch (e) { 817 | // 忽略错误 818 | } 819 | } 820 | 821 | // 3. 在父容器中查找可能的输入框 822 | const allInputs = ancestor.querySelectorAll('input[type="text"], input:not([type])'); 823 | for (const input of allInputs) { 824 | if (isVisible(input) && isPossibleCaptchaInput(input)) { 825 | return input; 826 | } 827 | } 828 | } 829 | 830 | // 4. 在搜索范围内查找输入框 831 | for (const selector of inputSelectors) { 832 | try { 833 | const inputs = searchRoot.querySelectorAll(selector); 834 | for (const input of inputs) { 835 | if (isVisible(input)) { 836 | return input; 837 | } 838 | } 839 | } catch (e) { 840 | // 忽略错误 841 | } 842 | } 843 | 844 | // 5. 如果仍然没找到,尝试找最近的输入框 845 | return findNearestInput(captchaImg, searchRoot); 846 | } 847 | 848 | // 检查输入框是否可能是验证码输入框 849 | function isPossibleCaptchaInput(input) { 850 | if (!input || input.type === 'password' || input.type === 'hidden') return false; 851 | 852 | // 检查属性 853 | const attributes = { 854 | name: (input.name || '').toLowerCase(), 855 | id: (input.id || '').toLowerCase(), 856 | placeholder: (input.placeholder || '').toLowerCase(), 857 | className: (input.className || '').toLowerCase(), 858 | autocomplete: (input.autocomplete || '').toLowerCase() 859 | }; 860 | 861 | // 验证码输入框的常见特征 862 | const captchaKeywords = ['captcha', 'vcode', 'verify', 'yzm', 'yanzheng', 'code', 'validate', '验证', '验证码']; 863 | 864 | // 检查各种属性是否包含验证码关键词 865 | for (const keyword of captchaKeywords) { 866 | if (attributes.name.includes(keyword) || 867 | attributes.id.includes(keyword) || 868 | attributes.placeholder.includes(keyword) || 869 | attributes.className.includes(keyword)) { 870 | return true; 871 | } 872 | } 873 | 874 | // 检查输入框的其他特征 875 | // 验证码输入框通常较短且有最大长度限制 876 | if (input.maxLength > 0 && input.maxLength <= 8) return true; 877 | 878 | // 验证码输入框通常设置autocomplete="off" 879 | if (attributes.autocomplete === 'off' && (input.size <= 10 || input.style.width && parseInt(input.style.width) < 150)) { 880 | return true; 881 | } 882 | 883 | // 检查输入框尺寸 - 验证码输入框通常较小 884 | if (input.offsetWidth > 0 && input.offsetWidth < 150) { 885 | return true; 886 | } 887 | 888 | return false; 889 | } 890 | 891 | // 查找距离验证码图片最近的输入框 892 | function findNearestInput(captchaImg, searchRoot = document) { 893 | const inputs = searchRoot.querySelectorAll('input[type="text"], input:not([type])'); 894 | if (!inputs.length) return null; 895 | 896 | const imgRect = captchaImg.getBoundingClientRect(); 897 | const imgX = imgRect.left + imgRect.width / 2; 898 | const imgY = imgRect.top + imgRect.height / 2; 899 | 900 | let nearestInput = null; 901 | let minDistance = Infinity; 902 | 903 | for (const input of inputs) { 904 | if (!isVisible(input) || input.type === 'password' || input.type === 'hidden') continue; 905 | 906 | const inputRect = input.getBoundingClientRect(); 907 | const inputX = inputRect.left + inputRect.width / 2; 908 | const inputY = inputRect.top + inputRect.height / 2; 909 | 910 | const distance = Math.sqrt( 911 | Math.pow(imgX - inputX, 2) + 912 | Math.pow(imgY - inputY, 2) 913 | ); 914 | 915 | if (distance < minDistance) { 916 | minDistance = distance; 917 | nearestInput = input; 918 | } 919 | } 920 | 921 | // 只返回距离较近且可能是验证码输入框的输入框 922 | return (minDistance < config.maxSearchDistance && isPossibleCaptchaInput(nearestInput)) ? nearestInput : null; 923 | } 924 | 925 | // 检查元素是否可见 926 | function isVisible(element) { 927 | return element && element.offsetWidth > 0 && element.offsetHeight > 0; 928 | } 929 | 930 | // 获取图片的base64数据 931 | async function getImageBase64(img) { 932 | try { 933 | // 创建canvas 934 | const canvas = document.createElement('canvas'); 935 | canvas.width = img.naturalWidth || img.width; 936 | canvas.height = img.naturalHeight || img.height; 937 | 938 | // 在canvas上绘制图片 939 | const ctx = canvas.getContext('2d'); 940 | 941 | try { 942 | ctx.drawImage(img, 0, 0); 943 | return canvas.toDataURL('image/png').split(',')[1]; 944 | } catch (e) { 945 | console.error('[验证码] 绘制图片到Canvas失败,可能是跨域问题'); 946 | 947 | // 尝试直接获取src 948 | if (img.src && img.src.startsWith('data:image')) { 949 | return img.src.split(',')[1]; 950 | } 951 | 952 | // 通过GM_xmlhttpRequest获取跨域图片 953 | return await fetchImage(img.src); 954 | } 955 | } catch (e) { 956 | console.error('[验证码] 获取图片base64失败:', e); 957 | return null; 958 | } 959 | } 960 | 961 | // 通过GM_xmlhttpRequest获取图片 962 | function fetchImage(url) { 963 | return new Promise((resolve, reject) => { 964 | GM_xmlhttpRequest({ 965 | method: 'GET', 966 | url: url, 967 | responseType: 'arraybuffer', 968 | onload: function(response) { 969 | try { 970 | const binary = new Uint8Array(response.response); 971 | const base64 = btoa( 972 | Array.from(binary).map(byte => String.fromCharCode(byte)).join('') 973 | ); 974 | resolve(base64); 975 | } catch (e) { 976 | reject(e); 977 | } 978 | }, 979 | onerror: reject 980 | }); 981 | }); 982 | } 983 | 984 | // 识别验证码 985 | function recognizeCaptcha(imageBase64, inputElement) { 986 | if (config.debug) console.log('[验证码] 发送到OCR服务器识别...'); 987 | 988 | GM_xmlhttpRequest({ 989 | method: 'POST', 990 | url: OCR_SERVER, 991 | headers: { 992 | 'Content-Type': 'application/json' 993 | }, 994 | data: JSON.stringify({ image: imageBase64 }), 995 | timeout: 10000, // 10秒超时 996 | onload: function(response) { 997 | try { 998 | if (config.debug) console.log('[验证码] 收到服务器响应:', response.responseText); 999 | 1000 | const result = JSON.parse(response.responseText); 1001 | 1002 | if (result.code === 0 && result.data) { 1003 | const captchaText = result.data.trim(); 1004 | 1005 | if (captchaText) { 1006 | if (config.debug) console.log('[验证码] 识别成功:', captchaText); 1007 | 1008 | // 填写验证码 1009 | inputElement.value = captchaText; 1010 | 1011 | // 触发input事件 1012 | const event = new Event('input', { bubbles: true }); 1013 | inputElement.dispatchEvent(event); 1014 | 1015 | // 触发change事件 1016 | const changeEvent = new Event('change', { bubbles: true }); 1017 | inputElement.dispatchEvent(changeEvent); 1018 | 1019 | if (config.debug) console.log('%c[验证码] 已自动填写: ' + captchaText, 'color: green; font-weight: bold;'); 1020 | 1021 | // 尝试查找并点击提交按钮 1022 | tryFindAndClickSubmitButton(inputElement); 1023 | } else { 1024 | if (config.debug) console.log('[验证码] 识别结果为空'); 1025 | } 1026 | } else { 1027 | if (config.debug) console.log('[验证码] 识别失败:', result.message || '未知错误'); 1028 | } 1029 | } catch (e) { 1030 | if (config.debug) console.log('[验证码] 解析OCR结果时出错:', e); 1031 | } 1032 | 1033 | // 清除当前处理的验证码 1034 | currentCaptchaImg = null; 1035 | currentCaptchaInput = null; 1036 | }, 1037 | onerror: function(error) { 1038 | if (config.debug) console.log('[验证码] OCR请求失败:', error); 1039 | console.log('[验证码] 请检查服务器地址是否正确,以及服务器是否已启动'); 1040 | 1041 | // 清除当前处理的验证码 1042 | currentCaptchaImg = null; 1043 | currentCaptchaInput = null; 1044 | }, 1045 | ontimeout: function() { 1046 | if (config.debug) console.log('[验证码] OCR请求超时'); 1047 | console.log('[验证码] 请检查服务器是否已启动,网络连接是否正常'); 1048 | 1049 | // 清除当前处理的验证码 1050 | currentCaptchaImg = null; 1051 | currentCaptchaInput = null; 1052 | } 1053 | }); 1054 | } 1055 | 1056 | // 尝试查找并点击提交按钮 1057 | function tryFindAndClickSubmitButton(inputElement) { 1058 | // 查找可能的提交按钮(但不自动点击,只是提示) 1059 | const form = inputElement.closest('form'); 1060 | if (form) { 1061 | const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); 1062 | if (submitButton) { 1063 | if (config.debug) console.log('[验证码] 找到验证码提交按钮,但不自动点击'); 1064 | } 1065 | } 1066 | 1067 | // 查找表单外的可能提交按钮 1068 | const parentContainer = inputElement.closest('.form, .login-form, .captcha-container, .form-container'); 1069 | if (parentContainer) { 1070 | const submitButton = parentContainer.querySelector('button, input[type="submit"], input[type="button"], a.btn, a.button'); 1071 | if (submitButton && isLoginButton(submitButton)) { 1072 | if (config.debug) console.log('[验证码] 找到验证码提交按钮,但不自动点击'); 1073 | } 1074 | } 1075 | } 1076 | 1077 | // 主函数:检查滑块验证码 1078 | function checkForSliderCaptcha(isForceCheck = false) { 1079 | if (config.debug) console.log('[验证码] ' + (isForceCheck ? '强制' : '常规') + '检查滑块验证码...'); 1080 | 1081 | // 查找滑块验证码 1082 | const result = findSliderCaptcha(); 1083 | 1084 | if (!result) { 1085 | if (config.debug) console.log('[验证码] 未找到滑块验证码元素'); 1086 | return; 1087 | } 1088 | 1089 | const { slider, track, container } = result; 1090 | 1091 | if (config.debug) console.log('[验证码] 找到滑块验证码:'); 1092 | 1093 | // 检查是否已处理过该滑块 1094 | const sliderKey = slider.outerHTML; 1095 | if (processedCaptchas.has(sliderKey) && !isForceCheck) { 1096 | if (config.debug) console.log('[验证码] 该滑块已被处理过,跳过'); 1097 | return; 1098 | } 1099 | 1100 | // 记录该滑块已处理 1101 | processedCaptchas.add(sliderKey); 1102 | 1103 | // 计算滑动距离 1104 | calculateSlideDistance(slider, track, container).then(distance => { 1105 | if (distance) { 1106 | if (config.debug) console.log('[验证码] 计算的滑动距离:', distance, 'px'); 1107 | 1108 | // 模拟滑动 1109 | simulateSliderDrag(slider, distance); 1110 | } 1111 | }); 1112 | } 1113 | 1114 | // 检查元素是否可能是滑块验证码 1115 | function isPossibleSlider(element) { 1116 | if (!element || !element.tagName) return false; 1117 | 1118 | // 滑块验证码常见特征 1119 | const sliderKeywords = ['slider', 'drag', 'slide', 'captcha', 'verify', 'puzzle', '滑块', '拖动', '滑动', '验证']; 1120 | 1121 | // 检查类名、ID和属性 1122 | const className = (element.className || '').toLowerCase(); 1123 | const id = (element.id || '').toLowerCase(); 1124 | const role = (element.getAttribute('role') || '').toLowerCase(); 1125 | 1126 | for (const keyword of sliderKeywords) { 1127 | if (className.includes(keyword) || id.includes(keyword) || role.includes(keyword)) { 1128 | if (config.debug) console.log('[验证码] 通过关键词检测到滑块:', keyword, element); 1129 | return true; 1130 | } 1131 | } 1132 | 1133 | // 检查内部元素 1134 | if (element.querySelector('.slider, .drag, .slide, .sliderBtn, .handler, [class*="slider"], [class*="drag"]')) { 1135 | if (config.debug) console.log('[验证码] 通过子元素检测到滑块:', element); 1136 | return true; 1137 | } 1138 | 1139 | return false; 1140 | } 1141 | 1142 | // 查找滑块验证码元素 1143 | function findSliderCaptcha() { 1144 | if (config.debug) console.log('[验证码] 开始查找滑块验证码元素...'); 1145 | 1146 | // 常见滑块验证码选择器 1147 | const sliderSelectors = [ 1148 | // 滑块按钮 1149 | '.slider-btn', '.sliderBtn', '.slider_button', '.yidun_slider', '.slider', '.handler', '.drag', 1150 | '.sliderContainer .sliderIcon', '.verify-slider-btn', '.verify-move-block', 1151 | '[class*="slider-btn"]', '[class*="sliderBtn"]', '[class*="handler"]', '[class*="drag-btn"]', 1152 | 1153 | // 通用选择器 1154 | '[class*="slider"][class*="btn"]', '[class*="slide"][class*="btn"]', '[class*="drag"][class*="btn"]' 1155 | ]; 1156 | 1157 | // 滑块轨道 1158 | const trackSelectors = [ 1159 | '.slider-track', '.sliderTrack', '.track', '.yidun_track', '.slide-track', '.slider-runway', 1160 | '.verify-bar-area', '.verify-slider', '.sliderContainer', 1161 | '[class*="slider-track"]', '[class*="sliderTrack"]', '[class*="track"]', '[class*="runway"]' 1162 | ]; 1163 | 1164 | // 容器 1165 | const containerSelectors = [ 1166 | '.slider-container', '.sliderContainer', '.yidun_panel', '.captcha-container', '.slider-wrapper', 1167 | '.verify-wrap', '.verify-box', '.verify-container', '.captcha-widget', 1168 | '[class*="slider-container"]', '[class*="sliderContainer"]', '[class*="captcha"]', 1169 | '[class*="slider"][class*="wrapper"]', '[class*="slide"][class*="container"]' 1170 | ]; 1171 | 1172 | // 首先查找容器 1173 | let container = null; 1174 | for (const selector of containerSelectors) { 1175 | const elements = document.querySelectorAll(selector); 1176 | for (const element of elements) { 1177 | if (isVisible(element)) { 1178 | container = element; 1179 | if (config.debug) console.log('[验证码] 找到滑块容器:', selector, element); 1180 | break; 1181 | } 1182 | } 1183 | if (container) break; 1184 | } 1185 | 1186 | // 如果没找到容器,尝试查找更广泛的元素 1187 | if (!container) { 1188 | const possibleContainers = document.querySelectorAll('[class*="slider"], [class*="captcha"], [class*="verify"]'); 1189 | for (const element of possibleContainers) { 1190 | if (isVisible(element) && isPossibleSlider(element)) { 1191 | container = element; 1192 | if (config.debug) console.log('[验证码] 找到可能的滑块容器:', element); 1193 | break; 1194 | } 1195 | } 1196 | } 1197 | 1198 | // 尝试查找iframe中的滑块验证码 1199 | if (!container) { 1200 | try { 1201 | const frames = document.querySelectorAll('iframe'); 1202 | for (const frame of frames) { 1203 | try { 1204 | const frameDoc = frame.contentDocument || frame.contentWindow?.document; 1205 | if (!frameDoc) continue; 1206 | 1207 | // 在iframe中查找容器 1208 | for (const selector of containerSelectors) { 1209 | const elements = frameDoc.querySelectorAll(selector); 1210 | for (const element of elements) { 1211 | if (isVisible(element)) { 1212 | container = element; 1213 | if (config.debug) console.log('[验证码] 在iframe中找到滑块容器:', selector, element); 1214 | break; 1215 | } 1216 | } 1217 | if (container) break; 1218 | } 1219 | } catch (e) { 1220 | // 可能有跨域问题,忽略错误 1221 | } 1222 | if (container) break; 1223 | } 1224 | } catch (e) { 1225 | console.error('[验证码] 检查iframe时出错:', e); 1226 | } 1227 | } 1228 | 1229 | // 如果没找到容器,直接返回null 1230 | if (!container) { 1231 | if (config.debug) console.log('[验证码] 未找到滑块容器'); 1232 | return null; 1233 | } 1234 | 1235 | // 在容器中查找滑块按钮 1236 | let slider = null; 1237 | for (const selector of sliderSelectors) { 1238 | try { 1239 | const element = container.querySelector(selector); 1240 | if (element && isVisible(element)) { 1241 | slider = element; 1242 | if (config.debug) console.log('[验证码] 找到滑块按钮:', selector, element); 1243 | break; 1244 | } 1245 | } catch (e) { 1246 | // 忽略选择器错误 1247 | } 1248 | } 1249 | 1250 | // 如果没找到具体选择器匹配的滑块,尝试找符合特征的元素 1251 | if (!slider) { 1252 | // 查找可能的滑块元素 1253 | const possibleSliders = container.querySelectorAll('div, span, i, button'); 1254 | for (const element of possibleSliders) { 1255 | if (!isVisible(element)) continue; 1256 | 1257 | const styles = window.getComputedStyle(element); 1258 | // 滑块通常是绝对定位或相对定位的小元素 1259 | if ((styles.position === 'absolute' || styles.position === 'relative') && 1260 | element.offsetWidth < 50 && element.offsetHeight < 50) { 1261 | 1262 | // 检查是否有常见的滑块类名特征 1263 | const className = (element.className || '').toLowerCase(); 1264 | if (className.includes('btn') || className.includes('button') || 1265 | className.includes('slider') || className.includes('handler') || 1266 | className.includes('drag')) { 1267 | slider = element; 1268 | if (config.debug) console.log('[验证码] 找到可能的滑块按钮:', element); 1269 | break; 1270 | } 1271 | } 1272 | } 1273 | } 1274 | 1275 | // 如果仍然没找到滑块,再尝试一些常见的样式特征 1276 | if (!slider) { 1277 | // 查找具有手型光标的元素 1278 | const cursorElements = Array.from(container.querySelectorAll('*')).filter(el => { 1279 | if (!isVisible(el)) return false; 1280 | const style = window.getComputedStyle(el); 1281 | return style.cursor === 'pointer' || style.cursor === 'grab' || style.cursor === 'move'; 1282 | }); 1283 | 1284 | for (const el of cursorElements) { 1285 | // 滑块通常较小 1286 | if (el.offsetWidth < 60 && el.offsetHeight < 60) { 1287 | slider = el; 1288 | if (config.debug) console.log('[验证码] 通过光标样式找到可能的滑块:', el); 1289 | break; 1290 | } 1291 | } 1292 | } 1293 | 1294 | // 如果仍然没找到滑块,尝试点击交互元素 1295 | if (!slider && config.debug) { 1296 | console.log('[验证码] 未能找到滑块按钮,尝试查找其他交互元素'); 1297 | 1298 | // 查找可能的交互元素 1299 | const interactiveElements = container.querySelectorAll('div[role="button"], div.slider, div.handler, div.btn'); 1300 | for (const el of interactiveElements) { 1301 | if (isVisible(el)) { 1302 | if (config.debug) console.log('[验证码] 找到可能的交互元素:', el); 1303 | slider = el; 1304 | break; 1305 | } 1306 | } 1307 | } 1308 | 1309 | // 如果没找到滑块,返回null 1310 | if (!slider) { 1311 | if (config.debug) console.log('[验证码] 未找到滑块按钮'); 1312 | return null; 1313 | } 1314 | 1315 | // 在容器中查找滑动轨道 1316 | let track = null; 1317 | for (const selector of trackSelectors) { 1318 | try { 1319 | const element = container.querySelector(selector); 1320 | if (element && isVisible(element)) { 1321 | track = element; 1322 | if (config.debug) console.log('[验证码] 找到滑块轨道:', selector, element); 1323 | break; 1324 | } 1325 | } catch (e) { 1326 | // 忽略选择器错误 1327 | } 1328 | } 1329 | 1330 | // 如果没找到轨道,尝试推断 1331 | if (!track) { 1332 | // 滑块的父元素通常是轨道 1333 | const parent = slider.parentElement; 1334 | if (parent && parent !== container) { 1335 | track = parent; 1336 | if (config.debug) console.log('[验证码] 使用滑块父元素作为轨道:', parent); 1337 | } else { 1338 | // 否则查找可能的轨道元素 1339 | const possibleTracks = container.querySelectorAll('div'); 1340 | for (const element of possibleTracks) { 1341 | if (!isVisible(element) || element === slider) continue; 1342 | 1343 | const styles = window.getComputedStyle(element); 1344 | // 轨道通常是一个较宽的水平条 1345 | if (element.offsetWidth > 100 && element.offsetHeight < 50 && 1346 | (styles.position === 'relative' || styles.position === 'absolute')) { 1347 | track = element; 1348 | if (config.debug) console.log('[验证码] 找到可能的滑块轨道:', element); 1349 | break; 1350 | } 1351 | } 1352 | } 1353 | } 1354 | 1355 | // 如果仍然找不到轨道,使用容器作为轨道的后备方案 1356 | if (!track) { 1357 | track = container; 1358 | if (config.debug) console.log('[验证码] 未找到明确的轨道,使用容器作为轨道'); 1359 | } 1360 | 1361 | return { slider, track, container }; 1362 | } 1363 | 1364 | // 计算滑动距离 1365 | async function calculateSlideDistance(slider, track, container) { 1366 | try { 1367 | // 如果启用了服务器API,先尝试使用服务器分析 1368 | if (config.useSlideAPI) { 1369 | const apiDistance = await analyzeSlideImagesWithAPI(slider, track, container); 1370 | if (apiDistance) { 1371 | if (config.debug) console.log('[验证码] 使用API计算的滑动距离:', apiDistance); 1372 | return apiDistance; 1373 | } 1374 | } 1375 | 1376 | // 本地计算逻辑(备用) 1377 | // 获取轨道宽度和滑块宽度 1378 | const trackRect = track.getBoundingClientRect(); 1379 | const sliderRect = slider.getBoundingClientRect(); 1380 | 1381 | // 最大可滑动距离 1382 | const maxDistance = trackRect.width - sliderRect.width; 1383 | 1384 | // 检查是否有缺口图片 1385 | const bgImage = findBackgroundImage(container); 1386 | const puzzleImage = findPuzzleImage(container); 1387 | 1388 | if (bgImage && puzzleImage) { 1389 | // 如果有拼图元素,尝试分析图片计算缺口位置 1390 | // 这里简化处理,实际上需要复杂的图像处理 1391 | // 在复杂场景中,可能需要发送到服务器进行处理 1392 | 1393 | // 随机一个合理的距离,在80%-95%范围内 1394 | // 这是简化处理,实际应该进行图像分析 1395 | const distance = Math.floor(maxDistance * (0.8 + Math.random() * 0.15)); 1396 | return distance; 1397 | } else { 1398 | // 没有找到明确的缺口图片,使用随机策略 1399 | // 大多数滑块验证码的有效区域在50%-80%之间 1400 | const distance = Math.floor(maxDistance * (0.5 + Math.random() * 0.3)); 1401 | return distance; 1402 | } 1403 | } catch (e) { 1404 | console.error('[验证码] 计算滑动距离时出错:', e); 1405 | return null; 1406 | } 1407 | } 1408 | 1409 | // 使用服务器API分析滑块图片 1410 | async function analyzeSlideImagesWithAPI(slider, track, container) { 1411 | if (config.debug) console.log('[验证码] 尝试使用API分析滑块图片...'); 1412 | 1413 | try { 1414 | // 找到背景图 1415 | const bgImage = findBackgroundImage(container); 1416 | // 找到滑块图 1417 | const puzzleImage = findPuzzleImage(container); 1418 | 1419 | let bgBase64 = null; 1420 | let puzzleBase64 = null; 1421 | let fullBase64 = null; 1422 | 1423 | // 获取背景图和滑块图的base64 1424 | if (bgImage) { 1425 | bgBase64 = await getImageBase64(bgImage); 1426 | if (config.debug) console.log('[验证码] 成功获取背景图'); 1427 | } 1428 | 1429 | if (puzzleImage) { 1430 | puzzleBase64 = await getImageBase64(puzzleImage); 1431 | if (config.debug) console.log('[验证码] 成功获取滑块图'); 1432 | } 1433 | 1434 | // 如果无法获取单独的图片,尝试获取整个容器截图 1435 | if ((!bgBase64 || !puzzleBase64) && container) { 1436 | try { 1437 | // 创建canvas 1438 | const canvas = document.createElement('canvas'); 1439 | const rect = container.getBoundingClientRect(); 1440 | canvas.width = rect.width; 1441 | canvas.height = rect.height; 1442 | 1443 | const ctx = canvas.getContext('2d'); 1444 | 1445 | // 使用html2canvas库如果可用 1446 | if (typeof html2canvas !== 'undefined') { 1447 | const canvas = await html2canvas(container, { 1448 | logging: false, 1449 | useCORS: true, 1450 | allowTaint: true 1451 | }); 1452 | fullBase64 = canvas.toDataURL('image/png').split(',')[1]; 1453 | if (config.debug) console.log('[验证码] 使用html2canvas获取了容器截图'); 1454 | } else { 1455 | // 尝试获取容器背景 1456 | const computedStyle = window.getComputedStyle(container); 1457 | if (computedStyle.backgroundImage && computedStyle.backgroundImage !== 'none') { 1458 | const bgUrl = computedStyle.backgroundImage.replace(/url\(['"]?(.*?)['"]?\)/i, '$1'); 1459 | if (bgUrl) { 1460 | try { 1461 | const img = new Image(); 1462 | img.crossOrigin = 'Anonymous'; 1463 | await new Promise((resolve, reject) => { 1464 | img.onload = resolve; 1465 | img.onerror = reject; 1466 | img.src = bgUrl; 1467 | }); 1468 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 1469 | fullBase64 = canvas.toDataURL('image/png').split(',')[1]; 1470 | if (config.debug) console.log('[验证码] 获取了容器背景图'); 1471 | } catch (e) { 1472 | console.error('[验证码] 获取容器背景图失败:', e); 1473 | } 1474 | } 1475 | } 1476 | } 1477 | } catch (e) { 1478 | console.error('[验证码] 获取容器截图失败:', e); 1479 | } 1480 | } 1481 | 1482 | // 发送到服务器分析 1483 | if ((bgBase64 && puzzleBase64) || fullBase64) { 1484 | if (config.debug) console.log('[验证码] 发送图片到服务器分析'); 1485 | 1486 | return new Promise((resolve, reject) => { 1487 | const data = {}; 1488 | 1489 | if (bgBase64 && puzzleBase64) { 1490 | data.bg_image = bgBase64; 1491 | data.slide_image = puzzleBase64; 1492 | } else if (fullBase64) { 1493 | data.full_image = fullBase64; 1494 | } 1495 | 1496 | GM_xmlhttpRequest({ 1497 | method: 'POST', 1498 | url: SLIDE_SERVER, 1499 | headers: { 1500 | 'Content-Type': 'application/json' 1501 | }, 1502 | data: JSON.stringify(data), 1503 | onload: function(response) { 1504 | try { 1505 | const result = JSON.parse(response.responseText); 1506 | 1507 | if (result.code === 0 && result.data) { 1508 | if (config.debug) console.log('[验证码] 服务器返回的滑动距离:', result.data.x); 1509 | resolve(result.data.x); 1510 | } else { 1511 | console.error('[验证码] 服务器分析失败:', result.message || '未知错误'); 1512 | resolve(null); 1513 | } 1514 | } catch (e) { 1515 | console.error('[验证码] 解析服务器响应时出错:', e); 1516 | resolve(null); 1517 | } 1518 | }, 1519 | onerror: function(error) { 1520 | console.error('[验证码] 滑块分析请求失败:', error); 1521 | resolve(null); 1522 | } 1523 | }); 1524 | }); 1525 | } else { 1526 | if (config.debug) console.log('[验证码] 无法获取有效的图片数据'); 1527 | return null; 1528 | } 1529 | } catch (e) { 1530 | console.error('[验证码] API分析滑块图片时出错:', e); 1531 | return null; 1532 | } 1533 | } 1534 | 1535 | // 查找背景图片 1536 | function findBackgroundImage(container) { 1537 | // 查找可能的背景图元素 1538 | const bgSelectors = [ 1539 | '.slider-bg', '.bg-img', '.captcha-bg', '.yidun_bg-img', 1540 | '[class*="bg"]', '[class*="background"]' 1541 | ]; 1542 | 1543 | for (const selector of bgSelectors) { 1544 | const element = container.querySelector(selector); 1545 | if (element && isVisible(element)) { 1546 | return element; 1547 | } 1548 | } 1549 | 1550 | // 检查容器内的所有图片 1551 | const images = container.querySelectorAll('img'); 1552 | for (const img of images) { 1553 | if (isVisible(img) && img.offsetWidth > 100) { 1554 | return img; 1555 | } 1556 | } 1557 | 1558 | return null; 1559 | } 1560 | 1561 | // 查找拼图块 1562 | function findPuzzleImage(container) { 1563 | // 查找可能的拼图元素 1564 | const puzzleSelectors = [ 1565 | '.slider-puzzle', '.puzzle', '.jigsaw', '.yidun_jigsaw', 1566 | '[class*="puzzle"]', '[class*="jigsaw"]' 1567 | ]; 1568 | 1569 | for (const selector of puzzleSelectors) { 1570 | const element = container.querySelector(selector); 1571 | if (element && isVisible(element)) { 1572 | return element; 1573 | } 1574 | } 1575 | 1576 | // 检查容器内的小图片或拼图形状元素 1577 | const elements = container.querySelectorAll('img, canvas, svg, div'); 1578 | for (const element of elements) { 1579 | if (!isVisible(element)) continue; 1580 | 1581 | // 拼图块通常较小且有绝对定位 1582 | const styles = window.getComputedStyle(element); 1583 | if (styles.position === 'absolute' && 1584 | element.offsetWidth > 10 && element.offsetWidth < 80 && 1585 | element.offsetHeight > 10 && element.offsetHeight < 80) { 1586 | 1587 | // 检查是否可能是拼图块 1588 | const className = (element.className || '').toLowerCase(); 1589 | if (className.includes('puzzle') || className.includes('jigsaw') || 1590 | className.includes('block') || className.includes('piece')) { 1591 | return element; 1592 | } 1593 | } 1594 | } 1595 | 1596 | return null; 1597 | } 1598 | 1599 | // 模拟滑块拖动 1600 | function simulateSliderDrag(slider, distance) { 1601 | if (config.debug) console.log('[验证码] 开始模拟滑块拖动,目标距离:', distance); 1602 | 1603 | try { 1604 | // 获取滑块位置 1605 | const rect = slider.getBoundingClientRect(); 1606 | const startX = rect.left + rect.width / 2; 1607 | const startY = rect.top + rect.height / 2; 1608 | 1609 | // 创建鼠标事件 1610 | const createMouseEvent = (type, x, y) => { 1611 | const event = new MouseEvent(type, { 1612 | view: window, 1613 | bubbles: true, 1614 | cancelable: true, 1615 | clientX: x, 1616 | clientY: y, 1617 | button: 0 1618 | }); 1619 | return event; 1620 | }; 1621 | 1622 | // 模拟人类拖动的时间和路径 1623 | const totalSteps = Math.max(5, Math.floor(distance / 10)); // 至少5步 1624 | const stepDelay = config.sliderSpeed; // 每步延迟时间 1625 | 1626 | // 开始拖动 1627 | slider.dispatchEvent(createMouseEvent('mousedown', startX, startY)); 1628 | if (config.debug) console.log('[验证码] 触发鼠标按下事件'); 1629 | 1630 | // 模拟人类拖动轨迹 1631 | let currentDistance = 0; 1632 | let step = 1; 1633 | 1634 | const moveInterval = setInterval(() => { 1635 | if (step <= totalSteps) { 1636 | // 使用加速然后减速的模式,更像人类拖动 1637 | let progress; 1638 | if (step < totalSteps / 3) { 1639 | // 加速阶段 1640 | progress = step / totalSteps * 1.5; 1641 | } else if (step > totalSteps * 2 / 3) { 1642 | // 减速阶段 1643 | progress = 0.5 + (step / totalSteps) * 0.5; 1644 | } else { 1645 | // 匀速阶段 1646 | progress = step / totalSteps; 1647 | } 1648 | 1649 | // 添加一些随机性 1650 | const randomOffset = (Math.random() - 0.5) * 2; 1651 | currentDistance = Math.floor(distance * progress); 1652 | 1653 | // 移动鼠标 1654 | const newX = startX + currentDistance; 1655 | const newY = startY + randomOffset; 1656 | 1657 | slider.dispatchEvent(createMouseEvent('mousemove', newX, newY)); 1658 | 1659 | if (config.debug && step % 5 === 0) { 1660 | console.log(`[验证码] 拖动进度: ${Math.round(progress * 100)}%`); 1661 | } 1662 | 1663 | step++; 1664 | } else { 1665 | // 结束拖动 1666 | clearInterval(moveInterval); 1667 | 1668 | // 最后一步,确保到达目标位置 1669 | const finalX = startX + distance; 1670 | slider.dispatchEvent(createMouseEvent('mousemove', finalX, startY)); 1671 | 1672 | // 释放鼠标 1673 | setTimeout(() => { 1674 | slider.dispatchEvent(createMouseEvent('mouseup', finalX, startY)); 1675 | 1676 | if (config.debug) console.log('[验证码] 滑块拖动完成'); 1677 | 1678 | // 尝试触发额外的事件 1679 | try { 1680 | // 有些验证码需要触发额外事件 1681 | slider.dispatchEvent(new Event('dragend', { bubbles: true })); 1682 | slider.dispatchEvent(new Event('drop', { bubbles: true })); 1683 | } catch (e) { 1684 | // 忽略错误 1685 | } 1686 | }, stepDelay); 1687 | } 1688 | }, stepDelay); 1689 | } catch (e) { 1690 | console.error('[验证码] 模拟滑块拖动时出错:', e); 1691 | } 1692 | } 1693 | 1694 | // 启动脚本 1695 | init(); 1696 | })(); --------------------------------------------------------------------------------