├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.linux.md ├── README.md ├── api ├── qinglong.py └── send.py ├── charsets.json ├── config_example.py ├── img ├── linux.png ├── main.gif └── w.jpg ├── main.py ├── make_config.py ├── myocr_v1.onnx ├── requirements.txt ├── schedule_main.py ├── utils ├── ck.py ├── consts.py └── tools.py └── 配置文件说明.md /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__ 3 | *$py.class 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | # For a library or package, you might want to ignore these files since the code is 86 | # intended to run in multiple environments; otherwise, check them in: 87 | # .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # pytype static type analyzer 134 | .pytype/ 135 | 136 | # Cython debug symbols 137 | cython_debug/ 138 | 139 | # png 140 | *.png 141 | 142 | .idea 143 | 144 | # config 145 | config.py 146 | config_example.py 147 | 148 | # md 149 | README.md 150 | README.linux.md 151 | 配置文件说明.md 152 | 153 | # dir 154 | img/ 155 | tmp/ 156 | 157 | # docker 158 | .dockerignore 159 | Dockerfile 160 | 161 | # git 162 | .gitignore 163 | .git 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # config 141 | config.py 142 | 143 | # png 144 | *.png 145 | 146 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方 Python 3.10.14 基础镜像 2 | FROM python:3.10.14-slim 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制 requirements.txt 并安装依赖 8 | COPY requirements.txt . 9 | 10 | # 安装依赖 11 | RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ 12 | RUN playwright install chromium 13 | RUN playwright install-deps 14 | 15 | 16 | # 时区 17 | RUN apt-get install -y tzdata 18 | ENV TZ=Asia/Shanghai 19 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 20 | # 复制应用文件 21 | COPY . . 22 | 23 | # 定义启动命令,运行 main.py 24 | CMD ["python", "schedule_main.py"] 25 | -------------------------------------------------------------------------------- /README.linux.md: -------------------------------------------------------------------------------- 1 | # linux无GUI使用文档 2 | 3 | ## 介绍 4 | - 作者认为要用LINUX就用无GUI的,所以未对GUI版本进行测试。 5 | - 主要的卡点在于短信验证码识别,目前支持了,所以可以LINUX上运行。 6 | - 使用手动输入验证码方式进行登录,整体过程如下图 7 | - 支持docker部署 8 | - 支持的账号类型有: 9 | - 账号密码登录 10 | - QQ登录 11 | - 支持代理 12 | ![PNG](./img/linux.png) 13 | 14 | 15 | ## 使用文档 16 | ## 1、docker部署(推荐) 17 | 18 | ### 下载镜像 19 | ```shell 20 | docker pull icepage/aujc:latest 21 | ``` 22 | 23 | ### 生成config.py 24 | ```python 25 | # 新建一个config.py 26 | touch config.py 27 | # 执行生成make_config.py, 记得最后要按y覆盖config.py 28 | docker run -i --rm \ 29 | -v $PWD/config.py:/app/config.py \ 30 | icepage/aujc python make_config.py 31 | ``` 32 | 33 | 说明: 34 | - 执行make_config.py, 会生成config.py 35 | - config.py的说明请转向 [配置文件说明](https://github.com/icepage/AutoUpdateJdCookie/blob/main/配置文件说明.md) 36 | - Linux的**无头模式(headless)一定要设为True!!!!** 37 | - 如果不会python的,参考config_example.py, 自己配置一个config.py, 我们基于这个config.py运行程序; 38 | 39 | ### 手动执行 40 | - 2种场景下需要手动main.py 41 | - 1、需要短信验证时需要手动, 本应用在新设备首次更新时必现. 42 | - 2、定时时间外需要执行脚本. 43 | - 配置中的sms_func设为manual_input时, 才能在终端填入短信验证码。 44 | - 当需要手动输入验证码时, docker运行需加-i参数。否则在触发短信验证码时会报错Operation not permitted 45 | ```bash 46 | docker run -i -v $PWD/config.py:/app/config.py icepage/aujc:latest python main.py 47 | ``` 48 | 49 | ![PNG](./img/linux.png) 50 | 51 | ### 长期运行 52 | - 程序读config.py中的cron_expression, 定期进行更新任务 53 | - 当sms_func设置为manual_input, 长期运行时会自动将manual_input转成no,避免滥发短信验证码, 因为没地方可填验证码. 54 | ```bash 55 | docker run -v $PWD/config.py:/app/config.py icepage/aujc:latest 56 | ``` 57 | 58 | ## 2、本地部署 59 | ### 安装依赖 60 | ```commandline 61 | pip install -r requirements.txt 62 | ``` 63 | 64 | ### 安装浏览器驱动 65 | ```commandline 66 | playwright install-deps 67 | ``` 68 | 69 | ### 安装chromium插件 70 | ```commandline 71 | playwright install chromium 72 | ``` 73 | 74 | ### 生成config.py 75 | ```python 76 | python make_config.py 77 | ``` 78 | 79 | 说明: 80 | - 执行make_config.py, 会生成config.py 81 | - config.py的说明请转向 [配置文件说明](https://github.com/icepage/AutoUpdateJdCookie/blob/main/配置文件说明.md) 82 | - Linux的**无头模式(headless)一定要设为True!!!!** 83 | - 如果不会python的,参考config_example.py, 自己配置一个config.py, 我们基于这个config.py运行程序; 84 | 85 | 86 | ### 运行脚本 87 | #### 1、单次手动执行 88 | ```commandline 89 | python main.py 90 | ``` 91 | 92 | #### 2、常驻进程 93 | 进程会读取config.py里的cron_expression,定期进行更新任务 94 | ```commandline 95 | python schedule_main.py 96 | ``` 97 | 98 | ### 3、定时任务 99 | 使用crontab. 模式定为cron, 会自动将短信配置为manual_input转成no,避免滥发短信验证码. 100 | ```commandline 101 | 0 3,4 * * * python main.py --mode cron 102 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aujc 2 | 3 | ## 20250327 4 | 5 | 提供自己训练模型,优化点选验证码通过率,aujc_trainer项目:[https://github.com/icepage/aujc_trainer](https://github.com/icepage/aujc_trainer) 6 | 7 | 训练完后有onnx和charsets.json文件,docker启动可以通过挂载方式使用 8 | 9 | ```python 10 | docker run -i \ 11 | -v $PWD/config.py:/app/config.py \ 12 | -v $PWD/myocr_v1.onnx:/app/myocr_v1.onnx \ 13 | -v $PWD/charsets.json:/app/charsets.json \ 14 | icepage/aujc:latest python main.py 15 | ``` 16 | 17 | ## 介绍 18 | - 用来自动化更新青龙面板的失效JD_COOKIE, 主要有三步 19 | - 自动检测并获取青龙面板的失效JD_COOKIE; 20 | - 拿到失效JD_COOKIE内容后, 根据配置的账号信息, 自动化登录JD页面, 拿到key; 21 | - 根据拿到的key, 自动化更新青龙面板的失效JD_COOKIE。 22 | - 支持的验证码类型有: 23 | - 滑块验证码; 24 | - 形状颜色验证码(基本不会出现了); 25 | - 点选验证码; 26 | - 短信验证码,支持手动输入和webhook(首次登录大概率出现, 其它时间出现频率低。webhook配置流程繁琐, 不爱折腾的建议使用手动输入或关闭。) 27 | - 手机语音识别验证码 28 | - 支持的账号类型有: 29 | - 账号密码登录 30 | - QQ登录 31 | - python >= 3.9 (playwright依赖的typing,在3.7和3.8会报错typing.NoReturn的BUG) 32 | - 支持windows,linux(无GUI) 33 | - 支持docker部署 34 | - 支持代理 35 | - linux无GUI使用文档请转向 [linux无GUI使用文档](https://github.com/icepage/AutoUpdateJdCookie/blob/main/README.linux.md) 36 | - WINDOWS整体效果如下图 37 | 38 | ![GIF](./img/main.gif) 39 | 40 | 41 | ## 使用文档 42 | ## 1、docker部署(推荐) 43 | 44 | ### 下载镜像 45 | ```shell 46 | docker pull icepage/aujc:latest 47 | ``` 48 | 49 | ### 生成config.py 50 | ```python 51 | # 新建一个config.py 52 | touch config.py 53 | # 执行生成make_config.py, 记得最后要按y覆盖config.py 54 | docker run -i --rm \ 55 | -v $PWD/config.py:/app/config.py \ 56 | icepage/aujc python make_config.py 57 | ``` 58 | 59 | 说明: 60 | - 执行make_config.py, 会生成config.py 61 | - config.py的说明请转向 [配置文件说明](https://github.com/icepage/AutoUpdateJdCookie/blob/main/配置文件说明.md) 62 | - Linux的**无头模式(headless)一定要设为True!!!!** 63 | - 如果不会python的,参考config_example.py, 自己配置一个config.py, 我们基于这个config.py运行程序; 64 | 65 | ### 手动执行 66 | - 2种场景下需要手动 67 | - 1、需要短信验证时需要手动, 本应用在新设备首次更新时必现. 68 | - 2、定时时间外需要执行脚本. 69 | - 配置中的sms_func设为manual_input时, 才能在终端填入短信验证码。 70 | - 当需要手动输入验证码时, docker运行需加-i参数。否则在触发短信验证码时会报错Operation not permitted 71 | ```bash 72 | docker run -i -v $PWD/config.py:/app/config.py icepage/aujc:latest python main.py 73 | ``` 74 | 75 | ![PNG](./img/linux.png) 76 | 77 | ### 长期运行 78 | - 程序读config.py中的cron_expression, 定期进行更新任务 79 | - 当sms_func设置为manual_input, 长期运行时会自动将manual_input转成no,避免滥发短信验证码, 因为没地方可填验证码. 80 | ```bash 81 | docker run -v $PWD/config.py:/app/config.py icepage/aujc:latest 82 | ``` 83 | 84 | ## 2、本地部署 85 | ### 安装依赖 86 | ```commandline 87 | pip install -r requirements.txt 88 | ``` 89 | 90 | ### 安装chromium插件 91 | ```commandline 92 | playwright install chromium 93 | ``` 94 | 95 | ### 生成config.py 96 | ```python 97 | python make_config.py 98 | ``` 99 | 100 | 说明: 101 | - 执行make_config.py, 会生成config.py 102 | - config.py的说明请转向 [配置文件说明](https://github.com/icepage/AutoUpdateJdCookie/blob/main/配置文件说明.md) 103 | - 如果不会python的,参考config_example.py, 自己配置一个config.py, 我们基于这个config.py运行程序; 104 | 105 | ### 运行脚本 106 | #### 1、单次手动执行 107 | ```commandline 108 | python main.py 109 | ``` 110 | 111 | #### 2、常驻进程 112 | 进程会读取config.py里的cron_expression,定期进行更新任务 113 | ```commandline 114 | python schedule_main.py 115 | ``` 116 | 117 | ## 特别感谢 118 | - 感谢 [所有赞助本项目的热心网友 --> 打赏名单](https://github.com/icepage/AutoUpdateJdCookie/wiki/%E6%89%93%E8%B5%8F%E5%90%8D%E5%8D%95) 119 | - 感谢 **https://github.com/sml2h3/ddddocr** 项目,牛逼项目 120 | - 感谢 **https://github.com/zzhjj/svjdck** 项目,牛逼项目 121 | 122 | ## 创作不易,如果项目有帮助到你,大佬点个星或打个赏吧 123 | ![JPG](./img/w.jpg) 124 | -------------------------------------------------------------------------------- /api/qinglong.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | import aiohttp 3 | from enum import Enum 4 | from typing import Union 5 | from utils.tools import send_request 6 | 7 | class QlUri(Enum): 8 | user_login = "api/user/login" 9 | envs = "api/envs" 10 | envs_enable = "api/envs/enable" 11 | envs_disable = "api/envs/disable" 12 | 13 | 14 | class QlOpenUri(Enum): 15 | auth_token = "open/auth/token" 16 | envs = "open/envs" 17 | envs_enable = "open/envs/enable" 18 | envs_disable = "open/envs/disable" 19 | 20 | 21 | class QlApi(object): 22 | def __init__(self, url: str): 23 | self.token = None 24 | self.url = url 25 | self.headers = None 26 | 27 | def login_by_token(self, token: str): 28 | headers = { 29 | 'Content-Type': 'application/json' 30 | } 31 | self.token = token 32 | headers['Authorization'] = self.token 33 | self.headers = headers 34 | 35 | async def login_by_username(self, user: str, password: str): 36 | data = { 37 | "username": user, 38 | "password": password 39 | } 40 | headers = { 41 | 'Content-Type': 'application/json' 42 | } 43 | response = await send_request(url=urljoin(self.url, QlUri.user_login.value), method="post", headers=headers, data=data) 44 | if response['code'] == 200: 45 | self.token = "Bearer " + response["data"]["token"] 46 | headers['Authorization'] = self.token 47 | self.headers = headers 48 | return response 49 | 50 | async def get_envs(self): 51 | async with aiohttp.ClientSession() as session: 52 | async with session.get(url=urljoin(self.url, QlUri.envs.value), headers=self.headers) as response: 53 | return await response.json() 54 | 55 | async def set_envs(self, data: Union[str, None] = None): 56 | async with aiohttp.ClientSession() as session: 57 | async with session.put(url=urljoin(self.url, QlUri.envs.value), data=data, headers=self.headers) as response: 58 | return await response.json() 59 | 60 | async def envs_enable(self, data: bytes): 61 | async with aiohttp.ClientSession() as session: 62 | async with session.put(url=urljoin(self.url, QlUri.envs_enable.value), data=data, headers=self.headers) as response: 63 | return await response.json() 64 | 65 | async def envs_disable(self, data: bytes): 66 | async with aiohttp.ClientSession() as session: 67 | async with session.put(url=urljoin(self.url, QlUri.envs_disable.value), data=data, headers=self.headers) as response: 68 | return await response.json() 69 | 70 | 71 | class QlOpenApi(object): 72 | def __init__(self, url: str): 73 | self.token = None 74 | self.url = url 75 | self.headers = None 76 | 77 | async def login(self, client_id: str, client_secret: str): 78 | headers = { 79 | 'Content-Type': 'application/json' 80 | } 81 | params = { 82 | "client_id": client_id, 83 | "client_secret": client_secret 84 | } 85 | response = await send_request(url=urljoin(self.url, QlOpenUri.auth_token.value), method="get", headers=headers, params=params) 86 | if response['code'] == 200: 87 | self.token = "Bearer " + response["data"]["token"] 88 | headers['Authorization'] = self.token 89 | self.headers = headers 90 | return response 91 | 92 | async def get_envs(self): 93 | async with aiohttp.ClientSession() as session: 94 | async with session.get(url=urljoin(self.url, QlOpenUri.envs.value), headers=self.headers) as response: 95 | return await response.json() 96 | 97 | async def set_envs(self, data: Union[str, None] = None): 98 | async with aiohttp.ClientSession() as session: 99 | async with session.put(url=urljoin(self.url, QlOpenUri.envs.value), data=data, headers=self.headers) as response: 100 | return await response.json() 101 | 102 | async def envs_enable(self, data: bytes): 103 | async with aiohttp.ClientSession() as session: 104 | async with session.put(url=urljoin(self.url, QlOpenUri.envs_enable.value), data=data, headers=self.headers) as response: 105 | return await response.json() 106 | 107 | async def envs_disable(self, data: bytes): 108 | async with aiohttp.ClientSession() as session: 109 | async with session.put(url=urljoin(self.url, QlOpenUri.envs_disable.value), data=data, headers=self.headers) as response: 110 | return await response.json() 111 | -------------------------------------------------------------------------------- /api/send.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import base64 3 | import hashlib 4 | import hmac 5 | import time 6 | from typing import Dict, Any 7 | import urllib 8 | 9 | def generate_sign(secret): 10 | """ 11 | 钉钉加签 12 | """ 13 | timestamp = str(round(time.time() * 1000)) 14 | secret_enc = secret.encode('utf-8') 15 | string_to_sign = f'{timestamp}\n{secret}'.encode('utf-8') 16 | hmac_code = hmac.new(secret_enc, string_to_sign, digestmod=hashlib.sha256).digest() 17 | sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) 18 | return timestamp, sign 19 | 20 | async def send_message(url: str, data: Dict[str, Any]) -> Dict[str, Any]: 21 | """ 22 | 发消息的通用方法 23 | """ 24 | headers = { 25 | 'Content-Type': 'application/json', 26 | } 27 | async with aiohttp.ClientSession() as session: 28 | async with session.post(url, json=data, headers=headers) as response: 29 | return await response.json() 30 | 31 | 32 | class SendApi(object): 33 | def __init__(self, name): 34 | self.name = name 35 | 36 | @staticmethod 37 | async def send_webhook(url, msg): 38 | """ 39 | webhook 40 | """ 41 | data = { 42 | "content": msg 43 | } 44 | return await send_message(url, data) 45 | 46 | @staticmethod 47 | async def send_wecom(url, msg): 48 | """ 49 | 企业微信 50 | """ 51 | data = { 52 | "msgtype": "text", 53 | "text": { 54 | "content": msg 55 | } 56 | } 57 | return await send_message(url, data) 58 | 59 | @staticmethod 60 | async def send_dingtalk(url: str, msg: str) -> Dict[str, Any]: 61 | """ 62 | 钉钉 63 | """ 64 | parsed_url = urllib.parse.urlparse(url) 65 | query_params = urllib.parse.parse_qs(parsed_url.query) 66 | # 必须有access_token 67 | access_token = query_params["access_token"][0] 68 | secret = query_params.get("secret", [None])[0] 69 | url = f"https://oapi.dingtalk.com/robot/send?access_token={access_token}" 70 | 71 | if secret: 72 | timestamp, sign = generate_sign(secret) 73 | url = f"{url}×tamp={timestamp}&sign={sign}" 74 | data = { 75 | "msgtype": "text", 76 | "text": { 77 | "content": msg 78 | } 79 | } 80 | return await send_message(url, data) 81 | 82 | @staticmethod 83 | async def send_feishu(url: str, msg: str) -> Dict[str, Any]: 84 | """ 85 | 飞书 86 | """ 87 | data = { 88 | "msg_type": "text", 89 | "content": { 90 | "text": msg 91 | } 92 | } 93 | return await send_message(url, data) 94 | 95 | @staticmethod 96 | async def send_pushplus(url: str, msg: str) -> Dict[str, Any]: 97 | """ 98 | 发送 Pushplus 消息。 99 | 100 | Args: 101 | url (str): Pushplus 的消息接收 URL: http://www.pushplus.plus/send?token=xxxxxxxxx。 102 | msg (str): 要发送的消息内容。 103 | 104 | Returns: 105 | Dict[str, Any]: 返回发送消息的结果。 106 | """ 107 | data = { 108 | "content": msg 109 | } 110 | return await send_message(url, data) 111 | -------------------------------------------------------------------------------- /charsets.json: -------------------------------------------------------------------------------- 1 | {"charset": [" ", "睦", "希", "程", "神", "煌", "灵", "达", "未", "乐", "似", "情", "得", "庭", "洋", "寿", "无", "飞", "快", "辉", "想", "扬", "智", "志", "气", "驻", "青", "黄", "荣", "康", "观", "和", "功", "烂", "万", "平", "福", "喜", "爱", "勇", "步", "笑", "安", "水", "心", "意", "眉", "开", "春", "有", "雅", "云", "理", "静", "吉", "精", "幸", "顺", "兴", "成", "烈", "锦", "直", "四", "如", "丽", "力", "祥", "限", "来", "灿", "腾", "望", "梦", "好", "事", "发", "必", "前", "采", "一", "善", "业", "动", "长", "焕", "家", "活", "人", "上", "真", "高", "感", "风", "满", "怀", "群", "充", "眼", "帆", "慈", "沛", "在", "向", "美", "永", "往", "超", "思", "慧", "溢", "量", "健"], "image": [-1, 64], "word": false, "channel": 1} -------------------------------------------------------------------------------- /config_example.py: -------------------------------------------------------------------------------- 1 | # JD用户信息 2 | user_datas = { 3 | "13500000000": { 4 | "password": "123456", 5 | "pt_pin": "123456", 6 | "sms_func": "webhook", 7 | "sms_webhook": "https://127.0.0.1:3000/getCode", 8 | # 设置为True时, 即使账号未失效也更新 9 | "force_update": False 10 | }, 11 | # QQ账号 12 | "168465451": { 13 | # qq密码 14 | "password": "123456", 15 | "pt_pin": "123456", 16 | # 指定为qq账号 17 | "user_type": "qq", 18 | "force_update": True 19 | }, 20 | "13500000001": { 21 | "password": "123456", 22 | "pt_pin": "123456", 23 | "sms_func": "no" 24 | }, 25 | "13500000002": { 26 | "password": "123456", 27 | "pt_pin": "123456", 28 | # auto_switch设置为False时,关闭全自动过验证码,改为手动过验证码 29 | "auto_switch": False 30 | }, 31 | } 32 | 33 | # ql信息 34 | qinglong_data = { 35 | "url": "http://127.0.0.1:5700/", 36 | "client_id": "", 37 | "client_secret": "", 38 | "username": "admin", 39 | "password": "123456", 40 | # 可选参数,QL面板的sessionid,主要是避免抢占QL后台的登录。需要在浏览器上,F12上获取Authorization的请求头。如果为空或不设置则账号密码登录 41 | "token": "" 42 | } 43 | 44 | # 定时器 45 | cron_expression = "0 5-6 * * *" 46 | 47 | # 浏览器是否开启无头模式,即是否展示整个登录过程 48 | headless = True 49 | 50 | # 是否开启发消息 51 | is_send_msg = False 52 | # 更新成功后是否发消息的开关 53 | is_send_success_msg = True 54 | # 更新失败后是否发消息的开关 55 | is_send_fail_msg = True 56 | # 配置发送地址 57 | send_info = { 58 | "send_wecom": [ 59 | ], 60 | "send_webhook": [ 61 | ], 62 | "send_dingtalk": [ 63 | ], 64 | "send_feishu": [ 65 | ], 66 | "send_pushplus": [ 67 | ] 68 | } 69 | 70 | # sms_func为填写验信验证码的模式,有3种可选,如下: 71 | # no 关闭短信验证码识别 72 | # manual_input 手动在终端输入验证码 73 | # webhook 调用api获取验证码,可实现全自动填写验证码 74 | sms_func = "manual_input" 75 | sms_webhook = "https://127.0.0.1:3000/getCode" 76 | 77 | # voice_func为手机语音验证码的模式,no为关闭识别,manual_input为手动在终端输入验证码 78 | voice_func = "manual_input" 79 | 80 | # 代理的配置,只代理登录,不代理请求QL面板和发消息 81 | proxy = { 82 | # 代理服务器地址, 支持http, https, socks5 83 | "server": "http://", 84 | # 代理服务器账号 85 | "username": "", 86 | # 代理服务器密码 87 | "password": "" 88 | } -------------------------------------------------------------------------------- /img/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icepage/AutoUpdateJdCookie/cecc5d66e0d152123bb7179d591d97117ff110d6/img/linux.png -------------------------------------------------------------------------------- /img/main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icepage/AutoUpdateJdCookie/cecc5d66e0d152123bb7179d591d97117ff110d6/img/main.gif -------------------------------------------------------------------------------- /img/w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icepage/AutoUpdateJdCookie/cecc5d66e0d152123bb7179d591d97117ff110d6/img/w.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import argparse 3 | import asyncio 4 | from api.qinglong import QlApi, QlOpenApi 5 | from api.send import SendApi 6 | from utils.ck import get_invalid_ck_ids 7 | from config import ( 8 | qinglong_data, 9 | user_datas, 10 | ) 11 | import cv2 12 | import json 13 | from loguru import logger 14 | import os 15 | from playwright.async_api import Playwright, async_playwright 16 | from playwright._impl._errors import TimeoutError 17 | import random 18 | import re 19 | from PIL import Image # 用于图像处理 20 | import traceback 21 | from typing import Union 22 | from utils.consts import ( 23 | jd_login_url, 24 | supported_types, 25 | supported_colors, 26 | supported_sms_func 27 | ) 28 | from utils.tools import ( 29 | get_tmp_dir, 30 | get_img_bytes, 31 | get_forbidden_users_dict, 32 | filter_forbidden_users, 33 | save_img, 34 | get_ocr, 35 | get_word, 36 | get_shape_location_by_type, 37 | get_shape_location_by_color, 38 | rgba2rgb, 39 | send_msg, 40 | new_solve_slider_captcha, 41 | ddddocr_find_files_pic, 42 | expand_coordinates, 43 | cv2_save_img, 44 | ddddocr_find_bytes_pic, 45 | solve_slider_captcha, 46 | validate_proxy_config, 47 | is_valid_verification_code, 48 | filter_cks, 49 | extract_pt_pin, 50 | desensitize_account 51 | ) 52 | 53 | """ 54 | 基于playwright做的 55 | """ 56 | logger.add( 57 | sink="main.log", 58 | level="DEBUG" 59 | ) 60 | 61 | try: 62 | # 账号是否脱敏的开关 63 | from config import enable_desensitize 64 | except ImportError: 65 | enable_desensitize = False 66 | 67 | async def download_image(url, filepath): 68 | async with aiohttp.ClientSession() as session: 69 | async with session.get(url) as response: 70 | if response.status == 200: 71 | with open(filepath, 'wb') as f: 72 | f.write(await response.read()) 73 | print(f"Image downloaded to {filepath}") 74 | else: 75 | print(f"Failed to download image. Status code: {response.status}") 76 | 77 | 78 | async def check_notice(page): 79 | try: 80 | logger.info("检查登录是否报错") 81 | notice = await page.wait_for_function( 82 | """ 83 | () => { 84 | const notice = document.querySelectorAll('.notice')[1]; 85 | return notice && notice.textContent.trim() !== '' ? notice.textContent.trim() : false; 86 | } 87 | """, 88 | timeout = 3000 89 | ) 90 | raise RuntimeError(notice) 91 | except TimeoutError: 92 | logger.info("登录未发现报错") 93 | return 94 | 95 | async def auto_move_slide_v2(page, retry_times: int = 2, slider_selector: str = 'img.move-img', move_solve_type: str = ""): 96 | for i in range(retry_times): 97 | logger.info(f'第{i + 1}次开启滑块验证') 98 | # 查找小图 99 | try: 100 | # 查找小图 101 | await page.wait_for_selector('.captcha_drop', state='visible', timeout=3000) 102 | except Exception as e: 103 | logger.info('未找到验证码框, 退出滑块验证') 104 | return 105 | await auto_move_slide(page, retry_times=5, slider_selector = slider_selector, move_solve_type = move_solve_type) 106 | 107 | # 判断是否一次过了滑块 108 | captcha_drop_visible = await page.is_visible('.captcha_drop') 109 | 110 | # 存在就重新滑一次 111 | if captcha_drop_visible: 112 | if i == retry_times - 1: 113 | return 114 | logger.info('一次过滑块失败, 再次尝试滑块验证') 115 | await page.wait_for_selector('.captcha_drop', state='visible', timeout=3000) 116 | # 点外键 117 | sign_locator = page.locator('#header').locator('.text-header') 118 | sign_locator_box = await sign_locator.bounding_box() 119 | sign_locator_left_x = sign_locator_box['x'] 120 | sign_locator_left_y = sign_locator_box['y'] 121 | await page.mouse.click(sign_locator_left_x, sign_locator_left_y) 122 | await asyncio.sleep(1) 123 | # 提交键 124 | submit_locator = page.locator('.btn.J_ping.active') 125 | await submit_locator.click() 126 | await asyncio.sleep(1) 127 | continue 128 | return 129 | 130 | async def auto_move_slide(page, retry_times: int = 2, slider_selector: str = 'img.move-img', move_solve_type: str = ""): 131 | """ 132 | 自动识别移动滑块验证码 133 | """ 134 | logger.info("开始滑块验证") 135 | for i in range(retry_times + 1): 136 | try: 137 | # 查找小图 138 | await page.wait_for_selector('#small_img', state='visible', timeout=3000) 139 | except Exception as e: 140 | # 未找到元素,认为成功,退出循环 141 | logger.info('未找到滑块,退出滑块验证') 142 | break 143 | 144 | # 滑块验证失败了 145 | if i + 1 == retry_times + 1: 146 | raise Exception("滑块验证失败了") 147 | 148 | logger.info(f'第{i + 1}次尝试自动移动滑块中...') 149 | # 获取 src 属性 150 | small_src = await page.locator('#small_img').get_attribute('src') 151 | background_src = await page.locator('#cpc_img').get_attribute('src') 152 | 153 | # 获取 bytes 154 | small_img_bytes = get_img_bytes(small_src) 155 | background_img_bytes = get_img_bytes(background_src) 156 | 157 | # 保存小图 158 | small_img_path = save_img('small_img', small_img_bytes) 159 | small_img_width = await page.evaluate('() => { return document.getElementById("small_img").clientWidth; }') # 获取网页的图片尺寸 160 | small_img_height = await page.evaluate('() => { return document.getElementById("small_img").clientHeight; }') # 获取网页的图片尺寸 161 | small_image = Image.open(small_img_path) # 打开图像 162 | resized_small_image = small_image.resize((small_img_width, small_img_height)) # 调整图像尺寸 163 | resized_small_image.save(small_img_path) # 保存调整后的图像 164 | 165 | # 保存大图 166 | background_img_path = save_img('background_img', background_img_bytes) 167 | background_img_width = await page.evaluate('() => { return document.getElementById("cpc_img").clientWidth; }') # 获取网页的图片尺寸 168 | background_img_height = await page.evaluate('() => { return document.getElementById("cpc_img").clientHeight; }') # 获取网页的图片尺寸 169 | background_image = Image.open(background_img_path) # 打开图像 170 | resized_background_image = background_image.resize((background_img_width, background_img_height)) # 调整图像尺寸 171 | resized_background_image.save(background_img_path) # 保存调整后的图像 172 | 173 | # 获取滑块 174 | slider = page.locator(slider_selector) 175 | await asyncio.sleep(1) 176 | 177 | # 这里是一个标准算法偏差 178 | slide_difference = 10 179 | 180 | if move_solve_type == "old": 181 | # 用于调试 182 | distance = ddddocr_find_bytes_pic(small_img_bytes, background_img_bytes) 183 | await asyncio.sleep(1) 184 | await solve_slider_captcha(page, slider, distance, slide_difference) 185 | await asyncio.sleep(1) 186 | continue 187 | # 获取要移动的长度 188 | distance = ddddocr_find_files_pic(small_img_path, background_img_path) 189 | await asyncio.sleep(1) 190 | # 移动滑块 191 | await new_solve_slider_captcha(page, slider, distance, slide_difference) 192 | await asyncio.sleep(1) 193 | 194 | 195 | async def auto_shape(page, retry_times: int = 5): 196 | logger.info("开始二次验证") 197 | # 图像识别 198 | ocr = get_ocr(beta=True) 199 | # 文字识别 200 | det = get_ocr(det=True) 201 | # 自己训练的ocr, 提高文字识别度 202 | my_ocr = get_ocr(det=False, ocr=False, import_onnx_path="myocr_v1.onnx", charsets_path="charsets.json") 203 | """ 204 | 自动识别滑块验证码 205 | """ 206 | for i in range(retry_times + 1): 207 | try: 208 | # 查找小图 209 | await page.wait_for_selector('div.captcha_footer img', state='visible', timeout=3000) 210 | except Exception as e: 211 | # 未找到元素,认为成功,退出循环 212 | logger.info('未找到二次验证图,退出二次验证识别') 213 | break 214 | 215 | # 二次验证失败了 216 | if i + 1 == retry_times + 1: 217 | raise Exception("二次验证失败了") 218 | 219 | logger.info(f'第{i + 1}次自动识别形状中...') 220 | tmp_dir = get_tmp_dir() 221 | 222 | background_img_path = os.path.join(tmp_dir, f'background_img.png') 223 | # 获取大图元素 224 | background_locator = page.locator('#cpc_img') 225 | # 获取元素的位置和尺寸 226 | backend_bounding_box = await background_locator.bounding_box() 227 | backend_top_left_x = backend_bounding_box['x'] 228 | backend_top_left_y = backend_bounding_box['y'] 229 | 230 | # 截取元素区域 231 | await page.screenshot(path=background_img_path, clip=backend_bounding_box) 232 | 233 | # 获取 图片的src 属性和button按键 234 | word_img_src = await page.locator('div.captcha_footer img').get_attribute('src') 235 | button = page.locator('div.captcha_footer button#submit-btn') 236 | 237 | # 找到刷新按钮 238 | refresh_button = page.locator('.jcap_refresh') 239 | 240 | 241 | # 获取文字图并保存 242 | word_img_bytes = get_img_bytes(word_img_src) 243 | rgba_word_img_path = save_img('rgba_word_img', word_img_bytes) 244 | 245 | # 文字图是RGBA的,有蒙板识别不了,需要转成RGB 246 | rgb_word_img_path = rgba2rgb('rgb_word_img', rgba_word_img_path) 247 | 248 | # 获取问题的文字 249 | word = get_word(ocr, rgb_word_img_path) 250 | 251 | if word.find('色') > 0: 252 | target_color = word.split('请选出图中')[1].split('的图形')[0] 253 | if target_color in supported_colors: 254 | logger.info(f'正在点击中......') 255 | # 获取点的中心点 256 | center_x, center_y = get_shape_location_by_color(background_img_path, target_color) 257 | if center_x is None and center_y is None: 258 | logger.info(f'识别失败,刷新中......') 259 | await refresh_button.click() 260 | await asyncio.sleep(random.uniform(2, 4)) 261 | continue 262 | # 得到网页上的中心点 263 | x, y = backend_top_left_x + center_x, backend_top_left_y + center_y 264 | # 点击图片 265 | await page.mouse.click(x, y) 266 | await asyncio.sleep(random.uniform(1, 4)) 267 | # 点击确定 268 | await button.click() 269 | await asyncio.sleep(random.uniform(2, 4)) 270 | continue 271 | else: 272 | logger.info(f'不支持{target_color},刷新中......') 273 | # 刷新 274 | await refresh_button.click() 275 | await asyncio.sleep(random.uniform(2, 4)) 276 | continue 277 | 278 | # 这里是文字验证码了 279 | elif word.find('依次') > 0 or word.find('按照次序点选') > 0: 280 | logger.info(f'开始文字识别,点击中......') 281 | # 获取文字的顺序列表 282 | try: 283 | if word.find('依次') > 0: 284 | target_char_list = list(re.findall(r'[\u4e00-\u9fff]+', word)[1]) 285 | if word.find('按照次序点选') > 0: 286 | target_char_list = list(word.split('请按照次序点选')[1]) 287 | except IndexError: 288 | logger.info(f'识别文字出错,刷新中......') 289 | await refresh_button.click() 290 | await asyncio.sleep(random.uniform(2, 4)) 291 | continue 292 | 293 | target_char_len = len(target_char_list) 294 | 295 | # 识别字数不对 296 | if target_char_len < 4: 297 | logger.info(f'识别的字数小于4,刷新中......') 298 | await refresh_button.click() 299 | await asyncio.sleep(random.uniform(2, 4)) 300 | continue 301 | 302 | # 取前4个的文字 303 | target_char_list = target_char_list[:4] 304 | 305 | # 定义【文字, 坐标】的列表 306 | target_list = [[x, []] for x in target_char_list] 307 | 308 | # 获取大图的二进制 309 | background_locator = page.locator('#cpc_img') 310 | background_locator_src = await background_locator.get_attribute('src') 311 | background_locator_bytes = get_img_bytes(background_locator_src) 312 | bboxes = det.detection(background_locator_bytes) 313 | 314 | count = 0 315 | im = cv2.imread(background_img_path) 316 | for bbox in bboxes: 317 | # 左上角 318 | x1, y1, x2, y2 = bbox 319 | # 做了一下扩大 320 | expanded_x1, expanded_y1, expanded_x2, expanded_y2 = expand_coordinates(x1, y1, x2, y2, 10) 321 | im2 = im[expanded_y1:expanded_y2, expanded_x1:expanded_x2] 322 | img_path = cv2_save_img('word', im2) 323 | image_bytes = open(img_path, "rb").read() 324 | result = my_ocr.classification(image_bytes) 325 | if result in target_char_list: 326 | for index, target in enumerate(target_list): 327 | if result == target[0] and target[0] is not None: 328 | x = x1 + (x2 - x1) / 2 329 | y = y1 + (y2 - y1) / 2 330 | target_list[index][1] = [x, y] 331 | count += 1 332 | 333 | if count != target_char_len: 334 | logger.info(f'文字识别失败,刷新中......') 335 | await refresh_button.click() 336 | await asyncio.sleep(random.uniform(2, 4)) 337 | continue 338 | 339 | await asyncio.sleep(random.uniform(0, 1)) 340 | try: 341 | for char in target_list: 342 | center_x = char[1][0] 343 | center_y = char[1][1] 344 | # 得到网页上的中心点 345 | x, y = backend_top_left_x + center_x, backend_top_left_y + center_y 346 | # 点击图片 347 | await page.mouse.click(x, y) 348 | await asyncio.sleep(random.uniform(1, 4)) 349 | except IndexError: 350 | logger.info(f'识别文字出错,刷新中......') 351 | await refresh_button.click() 352 | await asyncio.sleep(random.uniform(2, 4)) 353 | continue 354 | # 点击确定 355 | await button.click() 356 | await asyncio.sleep(random.uniform(2, 4)) 357 | 358 | else: 359 | shape_type = word.split('请选出图中的')[1] 360 | if shape_type in supported_types: 361 | logger.info(f'已找到图形,点击中......') 362 | if shape_type == "圆环": 363 | shape_type = shape_type.replace('圆环', '圆形') 364 | # 获取点的中心点 365 | center_x, center_y = get_shape_location_by_type(background_img_path, shape_type) 366 | if center_x is None and center_y is None: 367 | logger.info(f'识别失败,刷新中......') 368 | await refresh_button.click() 369 | await asyncio.sleep(random.uniform(2, 4)) 370 | continue 371 | # 得到网页上的中心点 372 | x, y = backend_top_left_x + center_x, backend_top_left_y + center_y 373 | # 点击图片 374 | await page.mouse.click(x, y) 375 | await asyncio.sleep(random.uniform(1, 4)) 376 | # 点击确定 377 | await button.click() 378 | await asyncio.sleep(random.uniform(2, 4)) 379 | continue 380 | else: 381 | logger.info(f'不支持{shape_type},刷新中......') 382 | # 刷新 383 | await refresh_button.click() 384 | await asyncio.sleep(random.uniform(2, 4)) 385 | continue 386 | 387 | 388 | async def sms_recognition(page, user, mode): 389 | try: 390 | from config import sms_func 391 | except ImportError: 392 | sms_func = "no" 393 | 394 | sms_func = user_datas[user].get("sms_func", sms_func) 395 | 396 | if sms_func not in supported_sms_func: 397 | raise Exception(f"sms_func只支持{supported_sms_func}") 398 | 399 | if mode == "cron" and sms_func == "manual_input": 400 | sms_func = "no" 401 | 402 | if sms_func == "no": 403 | raise Exception("sms_func为no关闭, 跳过短信验证码识别环节") 404 | 405 | logger.info('点击【获取验证码】中') 406 | await page.click('button.getMsg-btn') 407 | await asyncio.sleep(1) 408 | # 自动识别滑块 409 | await auto_move_slide(page, retry_times=5, slider_selector='div.bg-blue') 410 | await auto_shape(page, retry_times=30) 411 | 412 | # 识别是否成功发送验证码 413 | await page.wait_for_selector('button.getMsg-btn:has-text("重新发送")', timeout=3000) 414 | logger.info("发送短信验证码成功") 415 | 416 | # 手动输入 417 | # 用户在60S内,手动在终端输入验证码 418 | if sms_func == "manual_input": 419 | from inputimeout import inputimeout, TimeoutOccurred 420 | try: 421 | verification_code = inputimeout(prompt="请输入验证码:", timeout=60) 422 | except TimeoutOccurred: 423 | return 424 | 425 | # 通过调用web_hook的方式来实现全自动输入验证码 426 | elif sms_func == "webhook": 427 | from utils.tools import send_request 428 | try: 429 | from config import sms_webhook 430 | except ImportError: 431 | sms_webhook = "" 432 | sms_webhook = user_datas[user].get("sms_webhook", sms_webhook) 433 | 434 | if sms_webhook is None: 435 | raise Exception(f"sms_webhook未配置") 436 | 437 | headers = { 438 | 'Content-Type': 'application/json', 439 | } 440 | data = {"phone_number": user} 441 | response = await send_request(url=sms_webhook, method="post", headers=headers, data=data) 442 | verification_code = response['data']['code'] 443 | 444 | await asyncio.sleep(1) 445 | if not is_valid_verification_code(verification_code): 446 | logger.error(f"验证码需为6位数字, 输入的验证码为{verification_code}, 异常") 447 | raise Exception(f"验证码异常") 448 | 449 | logger.info('填写验证码中...') 450 | verification_code_input = page.locator('input.acc-input.msgCode') 451 | for v in verification_code: 452 | await verification_code_input.type(v, no_wait_after=True) 453 | await asyncio.sleep(random.random() / 10) 454 | 455 | logger.info('点击提交中...') 456 | await page.click('a.btn') 457 | 458 | 459 | async def voice_verification(page, user, mode): 460 | from utils.consts import supported_voice_func 461 | try: 462 | from config import voice_func 463 | except ImportError: 464 | voice_func = "no" 465 | 466 | voice_func = user_datas[user].get("voice_func", voice_func) 467 | 468 | if voice_func not in supported_voice_func: 469 | raise Exception(f"voice_func只支持{supported_voice_func}") 470 | 471 | if mode == "cron" and voice_func == "manual_input": 472 | voice_func = "no" 473 | 474 | if voice_func == "no": 475 | raise Exception("voice_func为no关闭, 跳过手机语音识别") 476 | 477 | logger.info('点击获取验证码中') 478 | await page.click('button.getMsg-btn:has-text("点击获取验证码")') 479 | await asyncio.sleep(1) 480 | # 自动识别滑块 481 | await auto_move_slide(page, retry_times=5, slider_selector='div.bg-blue') 482 | await auto_shape(page, retry_times=30) 483 | 484 | # 识别是否成功发送验证码 485 | await page.wait_for_selector('button.getMsg-btn:has-text("重新发送")', timeout=3000) 486 | logger.info("发送手机语音识别验证码成功") 487 | 488 | # 手动输入 489 | # 用户在60S内,手动在终端输入验证码 490 | if voice_func == "manual_input": 491 | from inputimeout import inputimeout, TimeoutOccurred 492 | try: 493 | verification_code = inputimeout(prompt="请输入验证码:", timeout=60) 494 | except TimeoutOccurred: 495 | return 496 | 497 | await asyncio.sleep(1) 498 | if not is_valid_verification_code(verification_code): 499 | logger.error(f"验证码需为6位数字, 输入的验证码为{verification_code}, 异常") 500 | raise Exception(f"验证码异常") 501 | 502 | logger.info('填写验证码中...') 503 | verification_code_input = page.locator('input.acc-input.msgCode') 504 | for v in verification_code: 505 | await verification_code_input.type(v, no_wait_after=True) 506 | await asyncio.sleep(random.random() / 10) 507 | 508 | logger.info('点击提交中...') 509 | await page.click('a.btn') 510 | 511 | 512 | async def check_dialog(page): 513 | logger.info("开始弹窗检测") 514 | try: 515 | # 等待 dialog 出现 516 | await page.wait_for_selector(".dialog", timeout=4000) 517 | except Exception as e: 518 | logger.info('未找到弹框, 退出弹框检测') 519 | return 520 | # 获取 dialog-des 的文本内容 521 | dialog_text = await page.locator(".dialog-des").text_content() 522 | if dialog_text == "您的账号存在风险,为了账号安全需实名认证,是否继续?": 523 | raise Exception("检测到实名认证弹窗,请前往移动端做实名认证") 524 | 525 | else: 526 | raise Exception("检测到不支持的弹窗, 更新异常") 527 | 528 | 529 | async def get_jd_pt_key(playwright: Playwright, user, mode) -> Union[str, None]: 530 | """ 531 | 获取jd的pt_key 532 | """ 533 | 534 | try: 535 | from config import headless 536 | except ImportError: 537 | headless = False 538 | 539 | args = '--no-sandbox', '--disable-setuid-sandbox', '--disable-software-rasterizer', '--disable-gpu' 540 | 541 | try: 542 | # 引入代理 543 | from config import proxy 544 | # 检查代理的配置 545 | is_proxy_valid, msg = validate_proxy_config(proxy) 546 | if not is_proxy_valid: 547 | logger.error(msg) 548 | proxy = None 549 | if msg == "未配置代理": 550 | logger.info(msg) 551 | proxy = None 552 | except ImportError: 553 | logger.info("未配置代理") 554 | proxy = None 555 | 556 | browser = await playwright.chromium.launch(headless=headless, args=args, proxy=proxy) 557 | try: 558 | # 引入UA 559 | from config import user_agent 560 | except ImportError: 561 | from utils.consts import user_agent 562 | context = await browser.new_context(user_agent=user_agent) 563 | 564 | try: 565 | page = await context.new_page() 566 | await page.set_viewport_size({"width": 360, "height": 640}) 567 | await page.goto(jd_login_url) 568 | 569 | if user_datas[user].get("user_type") == "qq": 570 | await page.get_by_role("checkbox").check() 571 | await asyncio.sleep(1) 572 | # 点击QQ登录 573 | await page.locator("a.quick-qq").click() 574 | await asyncio.sleep(1) 575 | 576 | # 等待 iframe 加载完成 577 | await page.wait_for_selector("#ptlogin_iframe") 578 | # 切换到 iframe 579 | iframe = page.frame(name="ptlogin_iframe") 580 | 581 | # 通过 id 选择 "密码登录" 链接并点击 582 | await iframe.locator("#switcher_plogin").click() 583 | await asyncio.sleep(1) 584 | # 填写账号 585 | username_input = iframe.locator("#u") # 替换为实际的账号 586 | for u in user: 587 | await username_input.type(u, no_wait_after=True) 588 | await asyncio.sleep(random.random() / 10) 589 | await asyncio.sleep(1) 590 | # 填写密码 591 | password_input = iframe.locator("#p") # 替换为实际的密码 592 | password = user_datas[user]["password"] 593 | for p in password: 594 | await password_input.type(p, no_wait_after=True) 595 | await asyncio.sleep(random.random() / 10) 596 | await asyncio.sleep(1) 597 | # 点击登录按钮 598 | await iframe.locator("#login_button").click() 599 | await asyncio.sleep(1) 600 | # 这里检测安全验证 601 | new_vcode_area = iframe.locator("div#newVcodeArea") 602 | style = await new_vcode_area.get_attribute("style") 603 | if style and "display: block" in style: 604 | if await new_vcode_area.get_by_text("安全验证").text_content() == "安全验证": 605 | logger.error(f"QQ号{user}需要安全验证, 登录失败,请使用其它账号类型") 606 | raise Exception(f"QQ号{user}需要安全验证, 登录失败,请使用其它账号类型") 607 | 608 | else: 609 | await page.get_by_text("账号密码登录").click() 610 | 611 | username_input = page.locator("#username") 612 | for u in user: 613 | await username_input.type(u, no_wait_after=True) 614 | await asyncio.sleep(random.random() / 10) 615 | 616 | password_input = page.locator("#pwd") 617 | password = user_datas[user]["password"] 618 | for p in password: 619 | await password_input.type(p, no_wait_after=True) 620 | await asyncio.sleep(random.random() / 10) 621 | 622 | await asyncio.sleep(random.random()) 623 | await page.locator('.policy_tip-checkbox').click() 624 | await asyncio.sleep(random.random()) 625 | await page.locator('.btn.J_ping.active').click() 626 | 627 | if user_datas[user].get("auto_switch", True): 628 | # 自动识别移动滑块验证码 629 | await asyncio.sleep(1) 630 | await auto_move_slide(page, retry_times=30) 631 | 632 | # 自动验证形状验证码 633 | await asyncio.sleep(1) 634 | await auto_shape(page, retry_times=30) 635 | 636 | # 进行短信验证识别 637 | await asyncio.sleep(1) 638 | if await page.locator('text="手机短信验证"').count() != 0: 639 | logger.info("开始短信验证码识别环节") 640 | await sms_recognition(page, user, mode) 641 | 642 | # 进行手机语音验证识别 643 | if await page.locator('div#header .text-header:has-text("手机语音验证")').count() > 0: 644 | logger.info("检测到手机语音验证页面,开始识别") 645 | await voice_verification(page, user, mode) 646 | 647 | # 弹窗检测 648 | await check_dialog(page) 649 | 650 | # 检查警告,如账号存在风险或账密不正确等 651 | await check_notice(page) 652 | 653 | else: 654 | logger.info("自动过验证码开关已关, 请手动操作") 655 | 656 | # 等待验证码通过 657 | logger.info("等待获取cookie...") 658 | await page.wait_for_selector('#msShortcutMenu', state='visible', timeout=120000) 659 | 660 | cookies = await context.cookies() 661 | for cookie in cookies: 662 | if cookie['name'] == 'pt_key': 663 | pt_key = cookie["value"] 664 | return pt_key 665 | 666 | return None 667 | 668 | except Exception as e: 669 | traceback.print_exc() 670 | return None 671 | 672 | finally: 673 | await context.close() 674 | await browser.close() 675 | 676 | 677 | async def get_ql_api(ql_data): 678 | """ 679 | 封装了QL的登录 680 | """ 681 | logger.info("开始获取QL登录态......") 682 | 683 | # 优化client_id和client_secret 684 | client_id = ql_data.get('client_id') 685 | client_secret = ql_data.get('client_secret') 686 | if client_id and client_secret: 687 | logger.info("使用client_id和client_secret登录......") 688 | qlapi = QlOpenApi(ql_data["url"]) 689 | response = await qlapi.login(client_id=client_id, client_secret=client_secret) 690 | if response['code'] == 200: 691 | logger.info("client_id和client_secret正常可用......") 692 | return qlapi 693 | else: 694 | logger.info("client_id和client_secret异常......") 695 | 696 | qlapi = QlApi(ql_data["url"]) 697 | 698 | # 其次用token 699 | token = ql_data.get('token') 700 | if token: 701 | logger.info("已设置TOKEN,开始检测TOKEN状态......") 702 | qlapi.login_by_token(token) 703 | 704 | # 如果token失效,就用账号密码登录 705 | response = await qlapi.get_envs() 706 | if response['code'] == 401: 707 | logger.info("Token已失效, 正使用账号密码获取QL登录态......") 708 | response = await qlapi.login_by_username(ql_data.get("username"), ql_data.get("password")) 709 | if response['code'] != 200: 710 | logger.error(f"账号密码登录失败. response: {response}") 711 | raise Exception(f"账号密码登录失败. response: {response}") 712 | else: 713 | logger.info("Token正常可用......") 714 | else: 715 | # 最后用账号密码 716 | logger.info("正使用账号密码获取QL登录态......") 717 | response = await qlapi.login_by_username(ql_data.get("username"), ql_data.get("password")) 718 | if response['code'] != 200: 719 | logger.error(f"账号密码登录失败. response: {response}") 720 | raise Exception(f"账号密码登录失败.response: {response}") 721 | return qlapi 722 | 723 | 724 | async def main(mode: str = None): 725 | """ 726 | :param mode 运行模式, 当mode = cron时,sms_func为 manual_input时,将自动传成no 727 | """ 728 | try: 729 | qlapi = await get_ql_api(qinglong_data) 730 | send_api = SendApi("ql") 731 | # 拿到禁用的用户列表 732 | response = await qlapi.get_envs() 733 | if response['code'] == 200: 734 | logger.info("获取环境变量成功") 735 | else: 736 | logger.error(f"获取环境变量失败, response: {response}") 737 | raise Exception(f"获取环境变量失败, response: {response}") 738 | 739 | env_data = response['data'] 740 | # 获取值为JD_COOKIE的环境变量 741 | jd_ck_env_datas = filter_cks(env_data, name='JD_COOKIE') 742 | # 从value中过滤出pt_pin, 注意只支持单行单pt_pin 743 | jd_ck_env_datas = [ {**x, 'pt_pin': extract_pt_pin(x['value'])} for x in jd_ck_env_datas if extract_pt_pin(x['value'])] 744 | 745 | try: 746 | logger.info("检测CK任务开始") 747 | # 先获取启用中的env_data 748 | up_jd_ck_list = filter_cks(jd_ck_env_datas, status=0, name='JD_COOKIE') 749 | # 这一步会去检测这些JD_COOKIE 750 | invalid_cks_id_list = await get_invalid_ck_ids(up_jd_ck_list) 751 | if invalid_cks_id_list: 752 | # 禁用QL的失效环境变量 753 | ck_ids_datas = bytes(json.dumps(invalid_cks_id_list), 'utf-8') 754 | await qlapi.envs_disable(data=ck_ids_datas) 755 | # 更新jd_ck_env_datas 756 | jd_ck_env_datas = [{**x, 'status': 1} if x.get('id') in invalid_cks_id_list or x.get('_id') in invalid_cks_id_list else x for x in jd_ck_env_datas] 757 | logger.info("检测CK任务完成") 758 | except Exception as e: 759 | traceback.print_exc() 760 | logger.error(f"检测CK任务失败, 跳过检测, 报错原因为{e}") 761 | 762 | # 获取需强制更新pt_pin 763 | force_update_pt_pins = [user_datas[key]["pt_pin"] for key in user_datas if user_datas[key].get("force_update") is True] 764 | # 获取禁用和需要强制更新的users 765 | forbidden_users = [x for x in jd_ck_env_datas if (x['status'] == 1 or x['pt_pin'] in force_update_pt_pins)] 766 | 767 | if not forbidden_users: 768 | logger.info("所有COOKIE环境变量正常,无需更新") 769 | return 770 | 771 | # 获取需要的字段 772 | filter_users_list = filter_forbidden_users(forbidden_users, ['_id', 'id', 'value', 'remarks', 'name']) 773 | 774 | # 生成字典 775 | user_dict = get_forbidden_users_dict(filter_users_list, user_datas) 776 | if not user_dict: 777 | logger.info("失效的CK信息未配置在user_datas内,无需更新") 778 | return 779 | 780 | # 登录JD获取pt_key 781 | async with async_playwright() as playwright: 782 | for user in user_dict: 783 | logger.info(f"开始更新{desensitize_account(user, enable_desensitize)}") 784 | pt_key = await get_jd_pt_key(playwright, user, mode) 785 | if pt_key is None: 786 | logger.error(f"获取pt_key失败") 787 | await send_msg(send_api, send_type=1, msg=f"{desensitize_account(user, enable_desensitize)} 更新失败") 788 | continue 789 | 790 | req_data = user_dict[user] 791 | req_data["value"] = f"pt_key={pt_key};pt_pin={user_datas[user]['pt_pin']};" 792 | logger.info(f"更新内容为{req_data}") 793 | data = json.dumps(req_data) 794 | response = await qlapi.set_envs(data=data) 795 | if response['code'] == 200: 796 | logger.info(f"{desensitize_account(user, enable_desensitize)}更新成功") 797 | else: 798 | logger.error(f"{desensitize_account(user, enable_desensitize)}更新失败, response: {response}") 799 | await send_msg(send_api, send_type=1, msg=f"{desensitize_account(user, enable_desensitize)} 更新失败") 800 | continue 801 | 802 | req_id = f"[{req_data['id']}]" if 'id' in req_data.keys() else f'[\"{req_data["_id"]}\"]' 803 | data = bytes(req_id, 'utf-8') 804 | response = await qlapi.envs_enable(data=data) 805 | if response['code'] == 200: 806 | logger.info(f"{desensitize_account(user, enable_desensitize)}启用成功") 807 | await send_msg(send_api, send_type=0, msg=f"{desensitize_account(user, enable_desensitize)} 更新成功") 808 | else: 809 | logger.error(f"{desensitize_account(user, enable_desensitize)}启用失败, response: {response}") 810 | 811 | except Exception as e: 812 | traceback.print_exc() 813 | 814 | 815 | def parse_args(): 816 | """ 817 | 解析参数 818 | """ 819 | parser = argparse.ArgumentParser() 820 | parser.add_argument('-m', '--mode', choices=['cron'], help="运行的main的模式(例如: 'cron')") 821 | return parser.parse_args() 822 | 823 | if __name__ == '__main__': 824 | # 使用解析参数的函数 825 | args = parse_args() 826 | asyncio.run(main(mode=args.mode)) 827 | -------------------------------------------------------------------------------- /make_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def prompt_input(prompt, default=None, required=False, choices=None): 5 | while True: 6 | suffix = f" (默认: {default})" if default is not None else "" 7 | user_input = input(f"{prompt}{suffix}: ").strip() 8 | if not user_input and default is not None: 9 | return default 10 | if choices and user_input not in choices: 11 | print(f"⚠️ 请输入以下选项之一: {', '.join(choices)}") 12 | continue 13 | if user_input or not required: 14 | return user_input 15 | print("⚠️ 此项为必填,请重新输入。") 16 | 17 | 18 | def prompt_yes_no(prompt, default='y'): 19 | choices_show = 'Y/n' if default.lower() == 'y' else 'y/N' 20 | while True: 21 | choice = input(f"{prompt} ({choices_show}): ").strip().lower() 22 | if not choice: 23 | return default.lower() == 'y' 24 | if choice in ['y', 'n']: 25 | return choice == 'y' 26 | print("⚠️ 请输入 'y' 或 'n'。") 27 | 28 | 29 | def collect_user_datas(): 30 | print("\n================= 配置账号信息 =================") 31 | user_datas = {} 32 | index = 1 33 | while True: 34 | print(f"\n第 {index} 个账号配置:") 35 | username = prompt_input("用户名 (留空则结束输入)") 36 | if not username: 37 | if index == 1: 38 | print("⚠️ 至少需要配置一个账号!") 39 | continue 40 | break 41 | user_type = prompt_input("账号类型 (jd/qq)", default="jd", choices=["jd", "qq"]) 42 | password = prompt_input("密码", required=True) 43 | pt_pin = prompt_input("pt_pin (必填)", required=True) 44 | force_update = prompt_yes_no("是否强制更新?(默认为n)", default='n') 45 | auto_switch = prompt_yes_no("是否自动处理验证码?(默认为y)", default='y') 46 | individual_sms = prompt_yes_no("是否单独配置短信验证码处理方式?(默认为n)", default='n') 47 | sms_func = None 48 | sms_webhook = None 49 | if individual_sms: 50 | sms_func = prompt_input("短信验证码处理方式 (no/manual_input/webhook) (默认为manual_input)", default="manual_input", 51 | choices=["no", "manual_input", "webhook"]) 52 | if sms_func == "webhook": 53 | sms_webhook = prompt_input("请输入短信验证码 webhook 地址", required=True) 54 | 55 | user_data = { 56 | "user_type": user_type, 57 | "password": password, 58 | "pt_pin": pt_pin, 59 | "force_update": force_update, 60 | "auto_switch": auto_switch 61 | } 62 | if individual_sms: 63 | user_data["sms_func"] = sms_func 64 | if sms_webhook: 65 | user_data["sms_webhook"] = sms_webhook 66 | 67 | user_datas[username] = user_data 68 | index += 1 69 | return user_datas 70 | 71 | 72 | def collect_qinglong_data(): 73 | print("\n================= 配置青龙面板 =================") 74 | url = prompt_input("青龙面板 URL (如 http://127.0.0.1:5700)", required=True) 75 | while True: 76 | client_id = prompt_input("client_id (可选)") 77 | client_secret = prompt_input("client_secret (可选)") 78 | token = prompt_input("token (可选)") 79 | username = prompt_input("青龙用户名 (可选)") 80 | password = prompt_input("青龙密码 (可选)") 81 | if (client_id and client_secret) or token or (username and password): 82 | break 83 | print("⚠️ 必须填写以下认证方式之一:client_id+client_secret、token、用户名+密码") 84 | qinglong_data = { 85 | "url": url, 86 | "client_id": client_id if client_id else "", 87 | "client_secret": client_secret if client_secret else "", 88 | "token": token if token else "", 89 | "username": username if username else "", 90 | "password": password if password else "" 91 | } 92 | 93 | return qinglong_data 94 | 95 | 96 | def collect_send_info(): 97 | print("\n================= 配置消息通知 =================") 98 | 99 | def collect_urls(name, display_name): 100 | urls = [] 101 | print(f"请输入 {display_name} 的通知地址(支持多个,输入为空结束):") 102 | while True: 103 | url = input(f"{display_name} 通知地址: ").strip() 104 | if not url: 105 | break 106 | urls.append(url) 107 | return urls if urls else None 108 | 109 | send_info = {} 110 | 111 | services = [ 112 | ("send_wecom", "企业微信"), 113 | ("send_webhook", "自定义 Webhook"), 114 | ("send_dingtalk", "钉钉"), 115 | ("send_feishu", "飞书"), 116 | ("send_pushplus", "PushPlus"), 117 | ] 118 | 119 | for key, display in services: 120 | urls = collect_urls(key, display) 121 | if urls: 122 | send_info[key] = urls 123 | 124 | return send_info 125 | 126 | 127 | 128 | def collect_proxy(): 129 | print("\n================= 配置代理 =================") 130 | server = prompt_input("代理服务器地址 (如 http://127.0.0.1:7890,留空则不使用代理)") 131 | if not server: 132 | return None 133 | username = prompt_input("代理服务器用户名 (可选)") 134 | password = prompt_input("代理服务器密码 (可选)") 135 | proxy = { 136 | "server": server 137 | } 138 | if username: 139 | proxy["username"] = username 140 | if password: 141 | proxy["password"] = password 142 | return proxy 143 | 144 | 145 | def write_config(user_datas, qinglong_data, headless, cron_expression, is_send_msg, is_send_success_msg, 146 | is_send_fail_msg, send_info, sms_func, voice_func, proxy, user_agent, enable_desensitize): 147 | if os.path.exists("config.py"): 148 | print("\n------------------- 文件存在 -------------------") 149 | overwrite = prompt_yes_no("检测到已有 config.py,是否覆盖?", default='n') 150 | if not overwrite: 151 | print("⚠️ 已取消生成 config.py") 152 | return 153 | 154 | with open("config.py", "w", encoding="utf-8") as f: 155 | f.write("user_datas = {\n") 156 | for uid, user in user_datas.items(): 157 | f.write(f" '{uid}': {{\n") 158 | for key, value in user.items(): 159 | if isinstance(value, bool): 160 | f.write(f" '{key}': {value},\n") 161 | else: 162 | f.write(f" '{key}': '{value}',\n") 163 | f.write(" },\n") 164 | f.write("}\n\n") 165 | 166 | f.write("qinglong_data = {\n") 167 | for key, value in qinglong_data.items(): 168 | f.write(f" '{key}': '{value}',\n") 169 | f.write("}\n\n") 170 | 171 | f.write(f"headless = {headless}\n") 172 | f.write(f"cron_expression = '{cron_expression}'\n") 173 | f.write(f"is_send_msg = {is_send_msg}\n") 174 | f.write(f"is_send_success_msg = {is_send_success_msg}\n") 175 | f.write(f"is_send_fail_msg = {is_send_fail_msg}\n") 176 | 177 | if send_info: 178 | f.write("send_info = {\n") 179 | for key, value in send_info.items(): 180 | f.write(f" '{key}': {value},\n") 181 | f.write("}\n") 182 | 183 | f.write(f"sms_func = '{sms_func}'\n") 184 | f.write(f"voice_func = '{voice_func}'\n") 185 | 186 | if proxy: 187 | f.write("proxy = {\n") 188 | for key, value in proxy.items(): 189 | f.write(f" '{key}': '{value}',\n") 190 | f.write("}\n") 191 | 192 | if user_agent: 193 | f.write(f"user_agent = '{user_agent}'\n") 194 | 195 | f.write(f"enable_desensitize = {enable_desensitize}\n") 196 | 197 | print("\n✅ 成功生成 config.py!") 198 | 199 | 200 | def main(): 201 | print("============== 欢迎使用 AutoUpdateJdCookie 配置生成器 ==============") 202 | 203 | user_datas = collect_user_datas() 204 | qinglong_data = collect_qinglong_data() 205 | 206 | print("\n================= 配置全局参数 =================") 207 | headless = prompt_yes_no("是否启用无头模式?(默认启用)", default='y') 208 | cron_expression = prompt_input("定时任务 Cron 表达式", default="15 0 * * *", required=True) 209 | 210 | print("\n================= 配置消息通知 =================") 211 | is_send_msg = prompt_yes_no("是否启用消息通知?(默认为n)", default='n') 212 | is_send_success_msg = False 213 | is_send_fail_msg = False 214 | send_info = {} 215 | if is_send_msg: 216 | is_send_success_msg = prompt_yes_no("更新成功后通知?", default='y') 217 | is_send_fail_msg = prompt_yes_no("更新失败后通知?", default='y') 218 | send_info = collect_send_info() 219 | 220 | print("\n================= 配置短信验证码方式 =================") 221 | sms_func = prompt_input("全局短信验证码处理方式 (no/manual_input/webhook)", default="manual_input", choices=["no", "manual_input", "webhook"]) 222 | 223 | print("\n================= 其它配置 =================") 224 | voice_func = prompt_input("语音验证码处理方式 (no/manual_input)", default="no", choices=["no", "manual_input"]) 225 | proxy = collect_proxy() 226 | user_agent = prompt_input("User-Agent (留空使用默认)") 227 | enable_desensitize = prompt_yes_no("是否启用日志和发送消息脱敏?(默认不开启)", default='n') 228 | 229 | write_config( 230 | user_datas, qinglong_data, headless, cron_expression, 231 | is_send_msg, is_send_success_msg, is_send_fail_msg, 232 | send_info, sms_func, voice_func, proxy, user_agent, enable_desensitize 233 | ) 234 | 235 | 236 | 237 | if __name__ == "__main__": 238 | main() 239 | -------------------------------------------------------------------------------- /myocr_v1.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icepage/AutoUpdateJdCookie/cecc5d66e0d152123bb7179d591d97117ff110d6/myocr_v1.onnx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ddddocr 2 | aiohttp 3 | playwright 4 | loguru 5 | croniter 6 | inputimeout -------------------------------------------------------------------------------- /schedule_main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime, timedelta 3 | from croniter import croniter 4 | from utils.consts import program 5 | from config import cron_expression 6 | from main import main 7 | from loguru import logger 8 | 9 | 10 | def get_next_runtime(cron_expression, base_time=None): 11 | base_time = base_time or datetime.now() 12 | cron = croniter(cron_expression, base_time) 13 | return cron.get_next(datetime) 14 | 15 | 16 | async def run_scheduled_tasks(cron_expression): 17 | logger.info(f"{program}运行中") 18 | next_run = get_next_runtime(cron_expression) 19 | logger.info(f"下次更新任务时间为{next_run}") 20 | while True: 21 | now = datetime.now() 22 | if now >= next_run: 23 | await main(mode="cron") 24 | next_run = get_next_runtime(cron_expression, now + timedelta(seconds=1)) 25 | logger.info(f"下次更新任务时间为{next_run}") 26 | await asyncio.sleep(1) 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(run_scheduled_tasks(cron_expression)) 31 | -------------------------------------------------------------------------------- /utils/ck.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from enum import Enum 3 | import random 4 | from utils.tools import send_request, sanitize_header_value 5 | from typing import List, Any 6 | 7 | 8 | class CheckCkCode(Enum): 9 | not_login = 1001 10 | 11 | 12 | async def check_ck( 13 | cookie: str 14 | ) -> dict[str, Any]: 15 | """ 16 | 检测JD_COOKIE是否失效 17 | 18 | :param cookie: 就是cookie 19 | """ 20 | url = "https://me-api.jd.com/user_new/info/GetJDUserInfoUnion" 21 | method = 'get' 22 | headers = { 23 | "Host": "me-api.jd.com", 24 | "Accept": "*/*", 25 | "Connection": "keep-alive", 26 | "Cookie": sanitize_header_value(cookie), 27 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.42", 28 | "Accept-Language": "zh-cn", 29 | "Referer": "https://home.m.jd.com/myJd/newhome.action?sceneval=2&ufc=&", 30 | "Accept-Encoding": "gzip, deflate, br" 31 | } 32 | r = await send_request(url, method, headers) 33 | # 检测这里太快了, sleep一会儿, 避免FK 34 | await asyncio.sleep(random.uniform(0.5,2)) 35 | return r 36 | 37 | 38 | async def get_invalid_cks( 39 | jd_ck_list: list 40 | ) -> List[dict]: 41 | """ 42 | 传入CK列表,过滤失效CK列表 43 | """ 44 | ck_list = [] 45 | for jd_ck in jd_ck_list: 46 | cookie = jd_ck['value'] 47 | r = await check_ck(cookie) 48 | if r.get('retcode') == str(CheckCkCode.not_login.value): 49 | ck_list.append(jd_ck) 50 | 51 | return ck_list 52 | 53 | 54 | async def get_invalid_ck_ids(env_data): 55 | # 检测CK是否失效 56 | invalid_cks_list = await get_invalid_cks(env_data) 57 | 58 | invalid_cks_id_list = [ck['id'] if 'id' in ck.keys() else ck["_id"] for ck in invalid_cks_list] 59 | return invalid_cks_id_list 60 | -------------------------------------------------------------------------------- /utils/consts.py: -------------------------------------------------------------------------------- 1 | # 项目名 2 | program = "AutoUpdateJdCookie" 3 | # JD登录页 4 | jd_login_url = "https://plogin.m.jd.com/login/login?appid=300&returnurl=https%3A%2F%2Fwq.jd.com%2Fpassport%2FLoginRedirect%3Fstate%3D1103073577433%26returnurl%3Dhttps%253A%252F%252Fhome.m.jd.com%252FmyJd%252Fhome.action&source=wq_passport" 5 | # 支持的形状类型 6 | supported_types = [ 7 | "三角形", 8 | "正方形", 9 | "长方形", 10 | "五角星", 11 | "六边形", 12 | "圆形", 13 | "梯形", 14 | "圆环" 15 | ] 16 | # 定义了支持的每种颜色的 HSV 范围 17 | supported_colors = { 18 | '紫色': ([125, 50, 50], [145, 255, 255]), 19 | '灰色': ([0, 0, 50], [180, 50, 255]), 20 | '粉色': ([160, 50, 50], [180, 255, 255]), 21 | '蓝色': ([100, 50, 50], [130, 255, 255]), 22 | '绿色': ([40, 50, 50], [80, 255, 255]), 23 | '橙色': ([10, 50, 50], [25, 255, 255]), 24 | '黄色': ([25, 50, 50], [35, 255, 255]), 25 | '红色': ([0, 50, 50], [10, 255, 255]) 26 | } 27 | supported_sms_func = [ 28 | "no", 29 | "webhook", 30 | "manual_input" 31 | ] 32 | supported_voice_func = [ 33 | "no", 34 | "manual_input" 35 | ] 36 | # 默认的UA, 可以在config.py里配置 37 | user_agent = 'Mozilla/5.0 Chrome' -------------------------------------------------------------------------------- /utils/tools.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | import base64 4 | import cv2 5 | import ddddocr 6 | from enum import Enum 7 | import io 8 | from loguru import logger 9 | import numpy as np 10 | import random 11 | import os 12 | from PIL import Image 13 | import re 14 | from typing import Dict, Any 15 | from utils.consts import supported_colors 16 | from typing import Union, List 17 | 18 | def get_tmp_dir(tmp_dir:str = './tmp'): 19 | # 检查并创建 tmp 目录(如果不存在) 20 | if not os.path.exists(tmp_dir): 21 | os.makedirs(tmp_dir) 22 | return tmp_dir 23 | 24 | 25 | def ddddocr_find_files_pic(target_file, background_file) -> int: 26 | """ 27 | 比对文件获取滚动长度 28 | """ 29 | with open(target_file, 'rb') as f: 30 | target_bytes = f.read() 31 | with open(background_file, 'rb') as f: 32 | background_bytes = f.read() 33 | target = ddddocr_find_bytes_pic(target_bytes, background_bytes) 34 | return target 35 | 36 | 37 | def ddddocr_find_bytes_pic(target_bytes, background_bytes) -> int: 38 | """ 39 | 比对bytes获取滚动长度 40 | """ 41 | det = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False) 42 | res = det.slide_match(target_bytes, background_bytes, simple_target=True) 43 | return res['target'][0] 44 | 45 | 46 | def get_img_bytes(img_src: str) -> bytes: 47 | """ 48 | 获取图片的bytes 49 | """ 50 | img_base64 = re.search(r'base64,(.*)', img_src) 51 | if img_base64: 52 | base64_code = img_base64.group(1) 53 | # print("提取的Base64编码:", base64_code) 54 | # 解码Base64字符串 55 | img_bytes = base64.b64decode(base64_code) 56 | return img_bytes 57 | else: 58 | raise "image is empty" 59 | 60 | 61 | def get_ocr(**kwargs): 62 | return ddddocr.DdddOcr(show_ad=False, **kwargs) 63 | 64 | 65 | def save_img(img_name, img_bytes): 66 | tmp_dir = get_tmp_dir() 67 | img_path = os.path.join(tmp_dir, f'{img_name}.png') 68 | # with open(img_path, 'wb') as file: 69 | # file.write(img_bytes) 70 | # 使用 Pillow 打开图像 71 | with Image.open(io.BytesIO(img_bytes)) as img: 72 | # 保存图像到文件 73 | img.save(img_path) 74 | return img_path 75 | 76 | 77 | def get_word(ocr, img_path): 78 | image_bytes = open(img_path, "rb").read() 79 | result = ocr.classification(image_bytes, png_fix=True) 80 | return result 81 | 82 | 83 | def filter_forbidden_users(user_info: list, fields: list = None) -> list: 84 | """ 85 | 过滤出想要的字段的字典列表 86 | """ 87 | return [{key: d[key] for key in fields if key in d} for d in user_info] 88 | 89 | 90 | def get_forbidden_users_dict(users_list: list, user_datas: dict) -> dict: 91 | """ 92 | 获取用户phone:信息的列表 93 | """ 94 | users_dict = {} 95 | for info in users_list: 96 | for key in user_datas: 97 | user_pt_pin = user_datas[key]['pt_pin'] 98 | if user_pt_pin == extract_pt_pin(info['value']): 99 | users_dict[key] = info 100 | break 101 | return users_dict 102 | 103 | 104 | async def human_like_mouse_move(page, from_x, to_x, y): 105 | """ 106 | 移动鼠标 107 | """ 108 | # 第一阶段:快速移动到目标附近,耗时 0.28 秒 109 | fast_duration = 0.28 110 | fast_steps = 50 111 | fast_target_x = from_x + (to_x - from_x) * 0.8 112 | fast_dx = (fast_target_x - from_x) / fast_steps 113 | 114 | for _ in range(fast_steps): 115 | from_x += fast_dx 116 | await page.mouse.move(from_x, y) 117 | await asyncio.sleep(fast_duration / fast_steps) 118 | 119 | # 第二阶段:稍微慢一些,耗时随机 20 到 31 毫秒 120 | slow_duration = random.randint(20, 31) / 1000 121 | slow_steps = 10 122 | slow_target_x = from_x + (to_x - from_x) * 0.9 123 | slow_dx = (slow_target_x - from_x) / slow_steps 124 | 125 | for _ in range(slow_steps): 126 | from_x += slow_dx 127 | await page.mouse.move(from_x, y) 128 | await asyncio.sleep(slow_duration / slow_steps) 129 | 130 | # 第三阶段:缓慢移动到目标位置,耗时 0.3 秒 131 | final_duration = 0.3 132 | final_steps = 20 133 | final_dx = (to_x - from_x) / final_steps 134 | 135 | for _ in range(final_steps): 136 | from_x += final_dx 137 | await page.mouse.move(from_x, y) 138 | await asyncio.sleep(final_duration / final_steps) 139 | 140 | 141 | async def solve_slider_captcha(page, slider, distance, slide_difference): 142 | """ 143 | 解决移动滑块 144 | """ 145 | # 等待滑块元素出现 146 | box = await slider.bounding_box() 147 | 148 | # 计算滑块的中心坐标 149 | from_x = box['x'] + box['width'] / 2 150 | to_y = from_y = box['y'] + box['height'] / 2 151 | 152 | # 模拟按住滑块 153 | await page.mouse.move(from_x, from_y) 154 | await page.mouse.down() 155 | 156 | to_x = from_x + distance + slide_difference 157 | # 平滑移动到目标位置 158 | await human_like_mouse_move(page, from_x, to_x, to_y) 159 | 160 | # 放开滑块 161 | await page.mouse.up() 162 | 163 | 164 | async def new_solve_slider_captcha(page, slider, distance, slide_difference): 165 | # 等待滑块元素出现 166 | distance = distance + slide_difference 167 | box = await slider.bounding_box() 168 | await page.mouse.move(box['x'] + 10 , box['y'] + 10) 169 | await page.mouse.down() # 模拟鼠标按下 170 | await page.mouse.move(box['x'] + distance + random.uniform(8, 10), box['y'], steps=5) # 模拟鼠标拖动,考虑到实际操作中可能存在的轻微误差和波动,加入随机偏移量 171 | await asyncio.sleep(random.randint(1, 5) / 10) # 随机等待一段时间,模仿人类操作的不确定性 172 | await page.mouse.move(box['x'] + distance, box['y'], steps=10) # 继续拖动滑块到目标位置 173 | await page.mouse.up() # 模拟鼠标释放,完成滑块拖动 174 | await asyncio.sleep(3) # 等待3秒,等待滑块验证结果 175 | 176 | def sort_rectangle_vertices(vertices): 177 | """ 178 | 获取左上、右上、右下、左下顺序的坐标 179 | """ 180 | # 根据 y 坐标对顶点排序 181 | vertices = sorted(vertices, key=lambda x: x[1]) 182 | 183 | # 根据 x 坐标对前两个和后两个顶点分别排序 184 | top_left, top_right = sorted(vertices[:2], key=lambda x: x[0]) 185 | bottom_left, bottom_right = sorted(vertices[2:], key=lambda x: x[0]) 186 | 187 | return [top_left, top_right, bottom_right, bottom_left] 188 | 189 | 190 | def is_trapezoid(vertices): 191 | """ 192 | 判断四边形是否为梯形。 193 | vertices: 四个顶点按顺序排列的列表。 194 | 返回值: 如果是梯形返回 True,否则返回 False。 195 | """ 196 | top_width = abs(vertices[1][0] - vertices[0][0]) 197 | bottom_width = abs(vertices[2][0] - vertices[3][0]) 198 | return top_width < bottom_width 199 | 200 | 201 | def get_shape_location_by_type(img_path, type: str): 202 | """ 203 | 获取指定形状在图片中的坐标 204 | """ 205 | img = cv2.imread(img_path) 206 | imgGray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 转灰度图 207 | imgBlur = cv2.GaussianBlur(imgGray, (5, 5), 1) # 高斯模糊 208 | imgCanny = cv2.Canny(imgBlur, 60, 60) # Canny算子边缘检测 209 | contours, hierarchy = cv2.findContours(imgCanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # 寻找轮廓点 210 | for obj in contours: 211 | perimeter = cv2.arcLength(obj, True) # 计算轮廓周长 212 | approx = cv2.approxPolyDP(obj, 0.02 * perimeter, True) # 获取轮廓角点坐标 213 | CornerNum = len(approx) # 轮廓角点的数量 214 | x, y, w, h = cv2.boundingRect(approx) # 获取坐标值和宽度、高度 215 | 216 | # 轮廓对象分类 217 | if CornerNum == 3: 218 | obj_type = "三角形" 219 | elif CornerNum == 4: 220 | if w == h: 221 | obj_type = "正方形" 222 | else: 223 | approx = sort_rectangle_vertices([vertex[0] for vertex in approx]) 224 | if is_trapezoid(approx): 225 | obj_type = "梯形" 226 | else: 227 | obj_type = "长方形" 228 | elif CornerNum == 6: 229 | obj_type = "六边形" 230 | elif CornerNum == 8: 231 | obj_type = "圆形" 232 | elif CornerNum == 20: 233 | obj_type = "五角星" 234 | else: 235 | obj_type = "未知" 236 | 237 | if obj_type == type: 238 | # 获取中心点 239 | center_x, center_y = x + w // 2, y + h // 2 240 | return center_x, center_y 241 | 242 | # 如果获取不到,则返回空 243 | return None, None 244 | 245 | 246 | def get_shape_location_by_color(img_path, target_color): 247 | """ 248 | 根据颜色获取指定形状在图片中的坐标 249 | """ 250 | 251 | # 读取图像 252 | image = cv2.imread(img_path) 253 | # 读取图像并转换为 HSV 色彩空间。 254 | hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 255 | 256 | # 获取目标颜色的范围 257 | lower, upper = supported_colors[target_color] 258 | lower = np.array(lower, dtype="uint8") 259 | upper = np.array(upper, dtype="uint8") 260 | 261 | # 创建掩码并找到轮廓 262 | mask = cv2.inRange(hsv_image, lower, upper) 263 | contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 264 | 265 | # 遍历轮廓并在中心点画点 266 | for contour in contours: 267 | # 过滤掉太小的区域 268 | if cv2.contourArea(contour) > 100: 269 | M = cv2.moments(contour) 270 | if M["m00"] != 0: 271 | cX = int(M["m10"] / M["m00"]) 272 | cY = int(M["m01"] / M["m00"]) 273 | return cX, cY 274 | 275 | return None, None 276 | 277 | 278 | def rgba2rgb(img_name, rgba_img_path, tmp_dir: str = './tmp'): 279 | """ 280 | rgba图片转rgb 281 | """ 282 | tmp_dir = get_tmp_dir(tmp_dir=tmp_dir) 283 | 284 | # 打开一个带透明度的RGBA图像 285 | rgba_image = Image.open(rgba_img_path) 286 | # 创建一个白色背景图像 287 | rgb_image = Image.new("RGB", rgba_image.size, (255, 255, 255)) 288 | # 将RGBA图像粘贴到背景图像上,使用透明度作为蒙版 289 | rgb_image.paste(rgba_image, (0, 0), rgba_image) 290 | 291 | rgb_image_path = os.path.join(tmp_dir, f"{img_name}.png") 292 | rgb_image.save(rgb_image_path) 293 | 294 | return rgb_image_path 295 | 296 | 297 | class SendType(Enum): 298 | success = 0 299 | fail = 1 300 | 301 | 302 | async def send_call_method(obj, method_name, *args, **kwargs): 303 | """ 304 | 使用反射调用发送消息的方法。 305 | 306 | :param obj: 对象实例 307 | :param method_name: 方法名称 308 | :param args: 位置参数 309 | :param kwargs: 关键字参数 310 | :return: 方法的返回值 311 | """ 312 | # 检查对象是否具有指定的方法 313 | if hasattr(obj, method_name): 314 | method = getattr(obj, method_name) 315 | # 检查获取的属性是否是可调用的 316 | return await method(*args, **kwargs) 317 | 318 | 319 | async def send_msg(send_api, send_type: int, msg: str): 320 | """ 321 | 读取配置文件,调用send_call_method发消息 322 | """ 323 | from config import is_send_msg 324 | if not is_send_msg: 325 | return 326 | 327 | from config import send_info, is_send_success_msg, is_send_fail_msg 328 | if (send_type == SendType.success.value and is_send_success_msg) or (send_type == SendType.fail.value and is_send_fail_msg): 329 | for key in send_info: 330 | for url in send_info[key]: 331 | rep = await send_call_method(send_api, key, url, msg) 332 | logger.info(f"发送消息到 {url}, 响应:{rep}") 333 | 334 | return 335 | 336 | 337 | def get_zero_or_not(v): 338 | if v < 0: 339 | return 0 340 | return v 341 | 342 | 343 | def expand_coordinates(x1, y1, x2, y2, N): 344 | # Calculate expanded coordinates 345 | new_x1 = get_zero_or_not(x1 - N) 346 | new_y1 = get_zero_or_not(y1 - N) 347 | new_x2 = x2 + N 348 | new_y2 = y2 + N 349 | return new_x1, new_y1, new_x2, new_y2 350 | 351 | 352 | def cv2_save_img(img_name, img, tmp_dir:str = './tmp'): 353 | tmp_dir = get_tmp_dir(tmp_dir) 354 | img_path = os.path.join(tmp_dir, f'{img_name}.png') 355 | cv2.imwrite(img_path, img) 356 | return img_path 357 | 358 | 359 | async def send_request(url: str, method: str, headers: Dict[str, Any], data: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: 360 | """ 361 | 发请求的通用方法 362 | """ 363 | async with aiohttp.ClientSession() as session: 364 | async with session.request(method, url=url, json=data, headers=headers, **kwargs) as response: 365 | return await response.json() 366 | 367 | 368 | def validate_proxy_config(proxy): 369 | """ 370 | 验证 server 是否为有效的 URL 地址 371 | """ 372 | server = proxy.get("server") 373 | # 排除缺省值 374 | if server == "http://": 375 | return True, "未配置代理" 376 | 377 | username = proxy.get("username") 378 | password = proxy.get("password") 379 | 380 | # 使用正则表达式来检查 server 是否是有效的 URL 381 | url_pattern = re.compile( 382 | r'^(http|https|socks5)://' 383 | r'(?:(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # 域名 384 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # 或者IP地址 385 | r'(?::\d+)?' # 可选端口 386 | r'(?:/.*)?$' # 可选路径 387 | ) 388 | 389 | if not server or not url_pattern.match(server): 390 | return False, "代理的server URL异常" 391 | 392 | # 检查 username 是否为空,若为空则忽略 password 的检查 393 | if username: 394 | if not password: 395 | return False, "代理只有账号, 缺少密码配置" 396 | else: 397 | if password: 398 | return False, "代理只有密码, 缺少账号配置" 399 | 400 | return True, "代理配置正常可用" 401 | 402 | 403 | def is_valid_verification_code(code: str): 404 | """ 405 | 判断验证码格式是否正确 406 | """ 407 | return bool(re.match(r"^\d{6}$", code)) 408 | 409 | 410 | def extract_pt_pin(value: str) -> Union[str, None]: 411 | """ 412 | 用正则提取value中pt_pin的值, 返回一个pt_pin,如果返回多个或没匹配上则返回空, 支持以下几种格式: 413 | pt_key=xxx;pt_pin=xxx; 414 | pt_key=xxx;pt_pin="xxx"; 415 | pt_key=xxx;pt_pin='xxx'; 416 | pt_pin=xxx;pt_key=xxx; 417 | pt_pin=xxx;pt_key="xxx"; 418 | pt_pin=xxx;pt_key='xxx'; 419 | """ 420 | pattern = r'pt_pin\s*=\s*(["\']?)([^"\';]+)\1' # 捕获 pt_pin 的值,并匹配可能的引号 421 | matches = re.findall(pattern, value) 422 | # 如果找到了多个匹配或没有匹配,则返回空 423 | if len(matches) == 1: 424 | # 返回 pt_pin 的值 425 | return matches[0][1] 426 | return None 427 | 428 | 429 | def filter_cks( 430 | env_data: List[Dict[str, Any]], 431 | *, 432 | status: int = None, 433 | id: int = None, 434 | **kwargs 435 | ) -> List[Dict[str, Any]]: 436 | """ 437 | 过滤env_data中符合条件的字典。 438 | 439 | :param env_data: ql环境变量数据 440 | :param status: 过滤条件之一,status字段的值。 441 | :param id: 过滤条件之一,id字段的值。 442 | :param kwargs: 其他过滤条件。 443 | :return: 符合条件的字典列表。 444 | """ 445 | # 检查必传参数是否至少传了一个 446 | if status is None and id is None and not kwargs: 447 | raise ValueError("至少需要传入一个过滤条件(status、id或其他字段)。") 448 | 449 | # 合并所有过滤条件 450 | filters = {} 451 | if status is not None: 452 | filters["status"] = status 453 | if id is not None: 454 | filters["id"] = id 455 | # 添加其他过滤条件 456 | filters.update(kwargs) 457 | 458 | # 过滤数据 459 | filtered_list = [] 460 | 461 | for item in env_data: 462 | if all(item.get(key) == value for key, value in filters.items()): 463 | filtered_list.append(item) 464 | 465 | return filtered_list 466 | 467 | 468 | def desensitize_account(account, enable_desensitize=True): 469 | """ 470 | 对传入的账号(QQ号或手机号)进行脱敏处理 471 | :param account: 账号(QQ号或手机号) 472 | :param enable_desensitize: 是否开启脱敏,默认为 True 473 | :return: 脱敏后的账号或原账号 474 | """ 475 | if not account or not enable_desensitize: 476 | # 如果账号为空或脱敏未开启,直接返回原账号 477 | return account 478 | 479 | # 判断是否为手机号(假设手机号为11位数字) 480 | if account.isdigit() and len(account) == 11: 481 | # 手机号脱敏:前3后4,中间4位用*代替 482 | return account[:3] + '****' + account[-4:] 483 | 484 | # 判断是否为QQ号(假设QQ号为5位以上数字) 485 | if account.isdigit() and len(account) >= 5: 486 | # QQ号脱敏:前2后2,中间用*代替 487 | return account[:2] + '***' + account[-2:] 488 | 489 | # 如果不是手机号或QQ号,直接返回原账号 490 | return account 491 | 492 | def sanitize_header_value(value: str) -> str: 493 | """ 494 | 清除 HTTP 头部值中的换行符,防止 header 注入 495 | """ 496 | return value.replace('\n', '').replace('\r', '').strip() 497 | -------------------------------------------------------------------------------- /配置文件说明.md: -------------------------------------------------------------------------------- 1 | # config.py配置详解 2 | 3 | ### 1、基础类配置 4 | - user_datas为JD用户数据, key为账号信息 5 | - force_update设置为True时, 即使账号未失效也更新; 6 | - 支持QQ账号,user_type指定为qq, 7 | - 默认全自动过验证码。如需手动, 设置auto_switch为False。 8 | - qinglong_data为QL数据,按照实际信息填写; 9 | - 1、系统优先使用client_id和client_secret调用QL, 获取方法如下: 10 | ```commandline 11 | 1、登录ql面板, 12 | 2、在系统设置 -> 应用设置 -> 添加应用,进行添加 13 | 3、需要【环境变量】的权限 14 | 4、此功能支持青龙2.9+ 15 | ``` 16 | - 2、无client_id和client_secret时, 系统将使用token调用QL,获取方法如下: 17 | ```commandline 18 | 1、登录ql面板, 19 | 2、点击F12打开开发者工具, 点击Network 20 | 3、刷新一下页面, 找到其中任意一个接口的请求, 打开header, 将header的Authorization的值填入。 21 | ``` 22 | - 3、当以上2种都无配置时, 将使用账号密码调用QL. 这种方式极不推荐会抢占QL后台的登录。 23 | - 4、三种登录方式任选其一即可 24 | 25 | - headless: 无头模式,True表示工具运行时不会显示用户界面的窗口, **docker和linux本地部署时一定要为True!!!!** 26 | - cron_expression: 基于cron的表达式, 定期进行更新任务; 27 | 28 | ### 2、消息通知配置 29 | #### 1)is_send_msg, 发消息开关 30 | 如果不需要发消息,请关掉消息开关,忽略消息配置 31 | ```commandline 32 | # 是否开启发消息 33 | is_send_msg = False 34 | ``` 35 | #### 2)成功消息和失败消息也可以开关 36 | ```commandline 37 | # 更新成功后是否发消息的开关 38 | is_send_success_msg = True 39 | # 更新失败后是否发消息的开关 40 | is_send_fail_msg = True 41 | ``` 42 | 43 | #### 3)可以发企微、钉钉、飞书机器人,其它的就自写webhook 44 | ```python 45 | # 配置发送地址 46 | send_info = { 47 | "send_wecom": [ 48 | "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" 49 | ], 50 | "send_webhook": [ 51 | "http://127.0.0.1:3000/webhook", 52 | "http://127.0.0.1:4000/webhook" 53 | ], 54 | "send_dingtalk": [ 55 | "https://oapi.dingtalk.com/robot/send?access_token=123456", 56 | # 如果有加签, URL里带上secret 57 | "https://oapi.dingtalk.com/robot/send?access_token=123456&secret=123456" 58 | ], 59 | "send_feishu": [ 60 | ] 61 | } 62 | ``` 63 | 64 | ### 3、短信配置 65 | - sms_func: 短信验证码的模式, 有以下三种 66 | - no: 关闭短信验证码识别; 67 | - manual_input: 手动在终端输入验证码; 68 | - webhook: 用户实现一个自定义webhook, 当系统需要填写短信验证码时, 调用webhook获取验证码, 可实现全自动填写验证码; 69 | - sms_webhook:当sms_func为webhook, 填写用户自定义webhook地址; 70 | - 账号可以配置个性化的sms_func和sms_webhook 71 | 72 | 以下面的配置为例 73 | - 1、13500000000配置了sms_func和sms_webhook, 所以当更新13500000000时需要短信验证时, 系统会去https://127.0.0.1:3000/api/getCode获取短信验证码更新; 74 | - 2、13500000001配置了sms_func为no, 当更新13500000001需要短信验证时, 直接报错需要短信验证, 并通知用户更新失败; 75 | - 3、13500000002未配置sms_func, 所以读取了全局配置的sms_func为manual_input, 当更新13500000002需要短信验证时, 使用手动在终端输入验证码; 76 | ```python 77 | user_datas = { 78 | "13500000000": { 79 | "password": "123456", 80 | "pt_pin": "123456", 81 | "sms_func": "webhook", 82 | "sms_webhook": "https://127.0.0.1:3000/api/getCode" 83 | }, 84 | "13500000001": { 85 | "password": "123456", 86 | "pt_pin": "123456", 87 | "sms_func": "no" 88 | }, 89 | "13500000002": { 90 | "password": "123456", 91 | "pt_pin": "123456", 92 | } 93 | } 94 | ... 95 | sms_func = "manual_input" 96 | sms_webhook = "https://127.0.0.1:3000/getCode" 97 | ``` 98 | 99 | ##### 自定义webhook说明 100 | 101 | ##### webhook请求方法 102 | POST 103 | 104 | ##### webhook的请求体 105 | ```json 106 | { 107 | "phone_number": "13500000000" 108 | } 109 | ``` 110 | 111 | ##### webhook的响应体 112 | ```json 113 | { 114 | "err_code": 0, 115 | "message": "Success", 116 | "data": { 117 | "code": "475431" 118 | } 119 | } 120 | ``` 121 | 122 | ##### webhook可参考项目:[SmsCodeWebhook](https://github.com/icepage/SmsCodeWebhook) 123 | 124 | 125 | ### 4、其它配置 126 | - voice_func: 手机语音验证码方法配置。设置为no就关闭识别,manual_input就可手动在终端输入验证码。 127 | - proxy: 配置代理, 可选。 128 | - user_agent: 设置登录JD的user_agent。 当执行await page.goto(jd_login_url)时,报错playwright._impl._errors.TimeoutError, 需自定义配置。可选。 129 | - enable_desensitize: 设置是否开启账号脱敏。若设置为True,日志打印和消息发送的账号信息做脱敏处理。可选,默认关闭。 130 | --------------------------------------------------------------------------------