├── .env ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── __init__.py ├── api │ ├── admin_routes.py │ ├── chat_window.py │ ├── message_api.py │ ├── plugin_routes.py │ └── routes.py ├── api_queue.py ├── api_service.py ├── app_mutex.py ├── app_ui.py ├── auth.py ├── config.py ├── config_manager.py ├── dynamic_package_manager.py ├── fix_path.py ├── initialize_wechat.bat ├── install_wxauto.py ├── logs.py ├── plugin_manager.py ├── run.py ├── start_api.bat ├── start_api_packaged.bat ├── start_ui.bat ├── start_ui.py ├── static │ └── js │ │ └── main.js ├── system_monitor.py ├── templates │ └── index.html ├── ui_service.py ├── unicode_fix.py ├── utils │ └── image_utils.py ├── wechat.py ├── wechat_adapter.py ├── wechat_init.py └── wxauto_wrapper │ ├── __init__.py │ └── wrapper.py ├── build_tools ├── build_app.bat ├── build_app.py ├── build_with_utf8.bat └── create_icon.py ├── docs ├── ARCHITECTURE_README.md ├── IMG │ └── 01.png ├── PACKAGING_README.md ├── api_documentation.md ├── api_usage.md ├── development_plan.md ├── message_monitor_service.md └── wxautoxdoc_2246.txt ├── icons └── wxauto_icon.ico ├── initialize_wechat.bat ├── main.py ├── requirements.txt ├── start_api.bat ├── start_ui.bat ├── wxauto ├── LICENSE ├── README.md ├── demo │ └── README.md ├── requirements.txt ├── utils │ ├── alipay.png │ ├── version.png │ ├── wxauto.png │ ├── wxpay.png │ └── wxqrcode.png └── wxauto │ ├── __init__.py │ ├── color.py │ ├── elements.py │ ├── errors.py │ ├── languages.py │ ├── uiautomation.py │ ├── utils.py │ └── wxauto.py └── wxauto_import.py /.env: -------------------------------------------------------------------------------- 1 | # API配置 2 | API_KEYS=test-key-2 3 | SECRET_KEY=your-secret-key-here 4 | 5 | # 服务器配置 6 | DEBUG=True 7 | HOST=0.0.0.0 8 | PORT=5000 9 | 10 | # 日志配置 11 | LOG_LEVEL=INFO 12 | LOG_FILE=app.log 13 | 14 | # 微信监控配置 15 | WECHAT_CHECK_INTERVAL=60 16 | WECHAT_AUTO_RECONNECT=true 17 | WECHAT_RECONNECT_DELAY=30 18 | WECHAT_MAX_RETRY=3 19 | 20 | # 微信库选择 (wxauto 或 wxautox) 21 | WECHAT_LIB=wxauto 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Python files 5 | *.py text diff=python 6 | 7 | # Exclude __pycache__ directories 8 | __pycache__/ export-ignore 9 | */__pycache__/ export-ignore 10 | */*/__pycache__/ export-ignore 11 | */*/*/__pycache__/ export-ignore 12 | */*/*/*/__pycache__/ export-ignore 13 | 14 | # Exclude .pyc files 15 | *.pyc export-ignore 16 | *.pyo export-ignore 17 | *.pyd export-ignore 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | */__pycache__/ 4 | */*/__pycache__/ 5 | */*/*/__pycache__/ 6 | */*/*/*/__pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # wxauto文件\ 11 | # wxauto文件/ 12 | tests\ 13 | app.log 14 | data\ 15 | data/ 16 | 17 | # C extensions 18 | *.so 19 | 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | wxautox-3.9.11.17.25b48-cp311-cp311-win_amd64.whl 37 | MANIFEST 38 | 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | cover/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | .pybuilder/ 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyder-py3 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # pytype static type analyzer 132 | .pytype/ 133 | 134 | # Cython debug symbols 135 | cython_debug/ 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 wxauto_http_api 2 | 3 |
4 | 5 | ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg?cacheSeconds=2592000) 6 | ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) 7 | ![Platform](https://img.shields.io/badge/platform-Windows-lightgrey.svg) 8 | ![Python](https://img.shields.io/badge/python-3.7%2B-blue.svg) 9 | 10 |
11 | 12 | > **致谢**:本项目基于 [wxauto](https://github.com/cluic/wxauto) 项目做二次开发,感谢原作者的贡献。 13 | > 14 | > **推荐搭配**:本项目建议配合 [WXAUTO-MGT](https://github.com/zj591227045/WXAUTO-MGT) 项目使用,可以获得最佳体验。 15 | 16 | 基于wxauto的微信HTTP API接口,提供简单易用的HTTP API调用微信功能,同时支持可选的wxautox扩展功能。通过本项目,您可以轻松地将微信集成到您的应用程序中。 17 | 18 |
19 | wxauto_http_api界面预览 20 |

wxauto_http_api 管理界面预览 - 提供直观的服务管理、配置和日志监控功能

21 |
22 | 23 | ## ✨ 功能特点 24 | 25 | - 📱 **内置wxauto微信自动化库**:开箱即用,无需额外安装 26 | - 🚀 **可选支持wxautox扩展功能**:提供更强大的功能和更高的稳定性(需单独安装) 27 | - 🌐 **完整的HTTP API接口**:简单易用的RESTful API 28 | - 💬 **丰富的消息功能**:支持文本、图片、文件、视频等多种消息类型 29 | - 👥 **群聊和好友管理**:支持群聊创建、成员管理、好友添加等功能 30 | - 🔄 **实时消息监听**:支持实时获取新消息 31 | - 🖥️ **图形化管理界面**:提供直观的服务管理和配置界面 32 | 33 | ## 🔧 安装说明 34 | 35 | ### 前置条件 36 | 37 | - **Python 3.7+**:确保已安装Python 3.7或更高版本 38 | - **Windows操作系统**:目前仅支持Windows平台 39 | - **微信PC客户端**:确保已安装并登录微信PC客户端,建议使用微信3.9版本 40 | 41 | ### 安装步骤 42 | 43 | #### 方式一:使用打包版本(推荐) 44 | 45 | 1. **下载最新发布版本** 46 | - 从 [Releases](https://github.com/yourusername/wxauto_http_api/releases) 页面下载最新的 `wxauto_http_api.zip` 文件 47 | - 解压到任意目录(路径中最好不要包含中文和特殊字符) 48 | 49 | 2. **直接运行** 50 | - 双击 `wxauto_http_api.exe` 启动图形界面(推荐) 51 | - 或者使用 `start_ui.bat` 启动图形界面 52 | - 或者使用 `start_api.bat` 仅启动API服务 53 | 54 | 3. **安装wxautox(可选)** 55 | - 如果需要使用wxautox增强功能,可以在图形界面中点击"安装wxautox"按钮 56 | - 选择解压目录中的 `lib/wxautox-*.whl` 文件进行安装 57 | 58 | #### 方式二:从源码安装 59 | 60 | 1. **克隆本仓库** 61 | 62 | ```bash 63 | git clone https://github.com/yourusername/wxauto_http_api.git 64 | cd wxauto_http_api 65 | ``` 66 | 67 | 2. **安装依赖** 68 | 69 | ```bash 70 | # 安装基础依赖 71 | pip install -r requirements.txt 72 | ``` 73 | 74 | 3. **启动应用** 75 | 76 | ```bash 77 | # 启动图形界面 78 | python main.py --service ui 79 | # 或者直接双击 start_ui.bat 80 | ``` 81 | 82 | ## 🚀 使用说明 83 | 84 | ### 启动方式 85 | 86 | 本项目提供三种启动方式: 87 | 88 | 1. **可执行文件直接运行**(最简单) 89 | 90 | ```bash 91 | # 直接双击可执行文件 92 | wxauto_http_api.exe 93 | ``` 94 | 95 | 2. **图形界面模式**(开发环境) 96 | 97 | ```bash 98 | # 方式1:使用Python命令 99 | python main.py --service ui 100 | 101 | # 方式2:直接双击批处理文件 102 | start_ui.bat 103 | ``` 104 | 105 | 3. **仅API服务模式** 106 | 107 | ```bash 108 | # 方式1:使用Python命令 109 | python main.py --service api 110 | 111 | # 方式2:直接双击批处理文件 112 | start_api.bat 113 | ``` 114 | 115 | 默认情况下,API服务将在 `http://0.0.0.0:5000` 上启动。 116 | 117 | ### 图形界面功能 118 | 119 | 图形界面提供以下功能: 120 | 121 | - **服务管理**:启动/停止API服务 122 | - **库选择**:选择使用wxauto或wxautox库 123 | - **插件管理**:安装/更新wxautox库 124 | - **配置管理**:修改端口、API密钥等配置 125 | - **日志查看**:实时查看API服务日志 126 | - **状态监控**:监控服务状态、资源使用情况 127 | 128 | ### API密钥配置 129 | 130 | 有两种方式配置API密钥: 131 | 132 | 1. **通过图形界面配置**: 133 | - 点击"插件配置"按钮 134 | - 在弹出的对话框中设置API密钥 135 | 136 | 2. **通过配置文件配置**: 137 | - 在 `.env` 文件中添加以下内容: 138 | ``` 139 | API_KEYS=your_api_key1,your_api_key2 140 | ``` 141 | 142 | ### 📚 API接口说明 143 | 144 | 所有API请求都需要在请求头中包含API密钥: 145 | 146 | ``` 147 | X-API-Key: your_api_key 148 | ``` 149 | 150 | #### 初始化微信 151 | 152 | ```http 153 | POST /api/wechat/initialize 154 | ``` 155 | 156 | #### 发送消息 157 | 158 | ```http 159 | POST /api/message/send 160 | Content-Type: application/json 161 | 162 | { 163 | "receiver": "接收者名称", 164 | "message": "消息内容", 165 | "at_list": ["@的人1", "@的人2"], 166 | "clear": true 167 | } 168 | ``` 169 | 170 | #### 获取新消息 171 | 172 | ```http 173 | GET /api/message/get-next-new?savepic=true&savevideo=true&savefile=true&savevoice=true&parseurl=true 174 | ``` 175 | 176 | #### 添加监听对象 177 | 178 | ```http 179 | POST /api/message/listen/add 180 | Content-Type: application/json 181 | 182 | { 183 | "who": "群名称或好友名称", 184 | "savepic": true 185 | } 186 | ``` 187 | 188 | #### 获取监听消息 189 | 190 | ```http 191 | GET /api/message/listen/get?who=群名称或好友名称 192 | ``` 193 | 194 | 更多API接口请在图形界面中点击"API说明"按钮查看详细文档。 195 | 196 | ## 🔄 库的选择 197 | 198 | 本项目支持两种微信自动化库: 199 | 200 | - **wxauto** 📱:开源的微信自动化库,功能相对基础,**默认内置** 201 | - **wxautox** 🚀:增强版的微信自动化库,提供更多功能和更高的稳定性,**需单独安装** 202 | 203 | ### 选择使用哪个库 204 | 205 | 您可以通过以下方式选择使用哪个库: 206 | 207 | 1. **通过图形界面选择**(推荐): 208 | - 在图形界面中选择"wxauto"或"wxautox"单选按钮 209 | - 点击"重载配置"按钮使配置生效 210 | 211 | 2. **通过配置文件选择**: 212 | - 在 `.env` 文件中设置 `WECHAT_LIB` 参数 213 | ```bash 214 | # 使用wxauto库 215 | WECHAT_LIB=wxauto 216 | 217 | # 或者使用wxautox库 218 | WECHAT_LIB=wxautox 219 | ``` 220 | 221 | ### 安装wxautox库 222 | 223 | 如果您选择使用wxautox库,有以下几种安装方式: 224 | 225 | 1. **通过图形界面安装**(推荐): 226 | - 启动程序后,在图形界面点击"安装wxautox"按钮 227 | - 选择您已下载的wxautox wheel文件(位于 `lib` 目录或您自己下载的位置) 228 | - 系统将自动安装并配置wxautox库 229 | 230 | 2. **通过命令行安装**: 231 | ```bash 232 | # 如果您有wxautox的wheel文件 233 | pip install lib/wxautox-x.x.x.x-cpxxx-cpxxx-xxx.whl 234 | ``` 235 | 236 | > **注意**:程序会严格按照您的选择使用指定的库。如果您选择了wxautox但未安装,程序会自动降级使用wxauto库。 237 | 238 | ## ⚙️ 自定义配置 239 | 240 | ### 通过图形界面配置 241 | 242 | 推荐使用图形界面进行配置,点击"插件配置"按钮可以修改以下配置: 243 | 244 | - **端口号**:API服务监听的端口号 245 | - **API密钥**:访问API所需的密钥 246 | 247 | ### 通过配置文件配置 248 | 249 | 您也可以在 `.env` 文件中进行更多高级配置: 250 | 251 | ```ini 252 | # API配置 253 | API_KEYS=your_api_key1,your_api_key2 254 | SECRET_KEY=your_secret_key 255 | 256 | # 服务配置 257 | PORT=5000 258 | 259 | # 微信监控配置 260 | WECHAT_CHECK_INTERVAL=60 261 | WECHAT_AUTO_RECONNECT=true 262 | WECHAT_RECONNECT_DELAY=30 263 | WECHAT_MAX_RETRY=3 264 | ``` 265 | 266 | ## 📁 项目结构 267 | 268 | ``` 269 | wxauto_http_api/ 270 | ├── app/ # 应用程序核心代码 271 | │ ├── api/ # API接口实现 272 | │ ├── config.py # 配置模块 273 | │ ├── logs.py # 日志模块 274 | │ ├── plugin_manager.py # 插件管理模块 275 | │ ├── wechat/ # 微信功能实现 276 | │ ├── api_service.py # API服务实现 277 | │ ├── app_ui.py # UI界面实现 278 | │ ├── app_mutex.py # 互斥锁机制 279 | │ ├── ui_service.py # UI服务实现 280 | │ └── run.py # API运行模块 281 | ├── build_tools/ # 打包工具 282 | │ ├── build_app.py # 打包脚本 283 | │ ├── build_app.bat # 打包批处理文件 284 | │ ├── create_icon.py # 创建图标脚本 285 | │ └── *.spec # PyInstaller规范文件 286 | ├── data/ # 数据文件 287 | │ ├── api/ # API数据 288 | │ │ ├── config/ # 配置文件 289 | │ │ ├── logs/ # API日志 290 | │ │ └── temp/ # 临时文件 291 | │ └── logs/ # 系统日志 292 | ├── docs/ # 文档 293 | │ ├── ARCHITECTURE_README.md # 架构说明 294 | │ └── PACKAGING_README.md # 打包说明 295 | ├── lib/ # 第三方库 296 | │ └── wxautox-*.whl # wxautox wheel文件 297 | ├── wxauto/ # wxauto库 298 | ├── .env # 环境变量配置 299 | ├── main.py # 主入口点 300 | ├── requirements.txt # 依赖项列表 301 | ├── initialize_wechat.bat # 初始化微信批处理文件 302 | ├── start_api.bat # 启动API服务的批处理文件 303 | └── start_ui.bat # 启动UI服务的批处理文件 304 | ``` 305 | 306 | ## ⚠️ 注意事项 307 | 308 | - **微信客户端**:请确保微信PC客户端已登录,建议使用微信3.9版本 309 | - **窗口状态**:使用过程中请勿关闭微信窗口 310 | - **安全性**:API密钥请妥善保管,避免泄露 311 | - **兼容性**:本项目仅支持Windows操作系统 312 | - **依赖项**:使用打包版本无需安装依赖,从源码安装时需确保安装所有必要的依赖项 313 | - **打包版本**:打包版本已包含所有必要的依赖,但不包含wxautox库,需要单独安装 314 | 315 | ## 📝 许可证 316 | 317 | [MIT License](LICENSE) 318 | 319 | ## 🤝 贡献 320 | 321 | 欢迎提交问题和功能请求!如果您想贡献代码,请提交拉取请求。 322 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | # 确保当前目录在Python路径中 6 | current_dir = os.path.dirname(os.path.abspath(__file__)) 7 | if current_dir not in sys.path: 8 | sys.path.insert(0, current_dir) 9 | 10 | # 确保父目录在Python路径中 11 | parent_dir = os.path.dirname(current_dir) 12 | if parent_dir not in sys.path: 13 | sys.path.insert(0, parent_dir) 14 | 15 | try: 16 | from flask import Flask 17 | from flask_limiter import Limiter 18 | from flask_limiter.util import get_remote_address 19 | except ImportError as e: 20 | logging.error(f"导入Flask相关模块失败: {str(e)}") 21 | logging.error("请确保已安装Flask和flask-limiter") 22 | raise 23 | 24 | try: 25 | from app.config import Config 26 | except ImportError: 27 | try: 28 | # 尝试直接导入 29 | from config import Config 30 | except ImportError: 31 | logging.error("无法导入Config模块,请确保app/config.py文件存在") 32 | raise 33 | 34 | def create_app(): 35 | """创建并配置Flask应用""" 36 | logging.info("开始创建Flask应用...") 37 | 38 | # 配置 Werkzeug 日志 39 | werkzeug_logger = logging.getLogger('werkzeug') 40 | werkzeug_logger.setLevel(logging.ERROR) # 只显示错误级别的日志 41 | 42 | # 确保所有日志处理器立即刷新 43 | for handler in logging.getLogger().handlers: 44 | handler.setLevel(logging.DEBUG) 45 | handler.flush() 46 | 47 | # 初始化微信相关配置 48 | try: 49 | logging.info("正在初始化微信相关配置...") 50 | from app.wechat_init import initialize as init_wechat 51 | init_wechat() 52 | logging.info("微信相关配置初始化完成") 53 | except ImportError as e: 54 | logging.error(f"导入微信初始化模块失败: {str(e)}") 55 | logging.warning("将继续创建Flask应用,但微信功能可能不可用") 56 | except Exception as e: 57 | logging.error(f"初始化微信配置时出错: {str(e)}") 58 | logging.warning("将继续创建Flask应用,但微信功能可能不可用") 59 | 60 | # 导入插件管理模块,确保它被初始化 61 | try: 62 | logging.info("正在导入插件管理模块...") 63 | from app import plugin_manager 64 | logging.info("插件管理模块导入成功") 65 | except ImportError as e: 66 | logging.error(f"导入插件管理模块失败: {str(e)}") 67 | logging.warning("将继续创建Flask应用,但插件功能可能不可用") 68 | 69 | # 创建 Flask 应用 70 | logging.info("正在创建Flask实例...") 71 | app = Flask(__name__) 72 | app.config.from_object(Config) 73 | logging.info("Flask实例创建成功") 74 | 75 | # 配置Flask日志处理 76 | if not app.debug: 77 | # 在非调试模式下,禁用自动重载器 78 | app.config['USE_RELOADER'] = False 79 | logging.info("已禁用Flask自动重载器") 80 | 81 | # 初始化限流器 82 | try: 83 | logging.info("正在初始化限流器...") 84 | limiter = Limiter( 85 | app=app, 86 | key_func=get_remote_address, 87 | default_limits=[Config.RATELIMIT_DEFAULT], 88 | storage_uri=Config.RATELIMIT_STORAGE_URL 89 | ) 90 | logging.info("限流器初始化成功") 91 | except Exception as e: 92 | logging.error(f"初始化限流器时出错: {str(e)}") 93 | logging.warning("将继续创建Flask应用,但API限流功能可能不可用") 94 | 95 | # 注册蓝图 96 | try: 97 | logging.info("正在注册蓝图...") 98 | try: 99 | from app.api.routes import api_bp 100 | from app.api.admin_routes import admin_bp 101 | from app.api.plugin_routes import plugin_bp 102 | except ImportError as e: 103 | logging.error(f"导入蓝图模块失败: {str(e)}") 104 | logging.error("请确保app/api目录下的routes.py、admin_routes.py和plugin_routes.py文件存在") 105 | raise 106 | 107 | app.register_blueprint(api_bp, url_prefix='/api') 108 | app.register_blueprint(admin_bp, url_prefix='/api/admin') 109 | app.register_blueprint(plugin_bp, url_prefix='/admin/plugins') 110 | logging.info("蓝图注册成功") 111 | except Exception as e: 112 | logging.error(f"注册蓝图时出错: {str(e)}") 113 | logging.error("无法继续创建Flask应用") 114 | raise 115 | 116 | # 添加健康检查路由 117 | @app.route('/health') 118 | def health_check(): 119 | """健康检查路由""" 120 | return {'status': 'ok'} 121 | 122 | logging.info("Flask应用创建完成") 123 | return app -------------------------------------------------------------------------------- /app/api/admin_routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | 管理员API路由 3 | 提供配置重载、服务状态查询等功能 4 | """ 5 | 6 | from flask import Blueprint, jsonify, request, g 7 | from app.auth import require_api_key 8 | from app.logs import logger 9 | import importlib 10 | import os 11 | import time 12 | from app.config import Config 13 | 14 | admin_bp = Blueprint('admin', __name__) 15 | 16 | @admin_bp.route('/reload-config', methods=['POST']) 17 | @require_api_key 18 | def reload_config(): 19 | """重新加载配置""" 20 | try: 21 | # 重新加载配置模块 22 | importlib.reload(importlib.import_module('app.config')) 23 | 24 | # 记录日志 25 | logger.info("配置已重新加载") 26 | 27 | return jsonify({ 28 | 'code': 0, 29 | 'message': '配置已重新加载', 30 | 'data': None 31 | }) 32 | except Exception as e: 33 | logger.error(f"重新加载配置失败: {str(e)}") 34 | return jsonify({ 35 | 'code': 5001, 36 | 'message': f'重新加载配置失败: {str(e)}', 37 | 'data': None 38 | }), 500 39 | 40 | @admin_bp.route('/stats', methods=['GET']) 41 | @require_api_key 42 | def get_stats(): 43 | """获取服务统计信息""" 44 | try: 45 | # 获取进程信息 46 | import psutil 47 | process = psutil.Process(os.getpid()) 48 | 49 | # CPU使用率 50 | cpu_percent = process.cpu_percent(interval=0.1) 51 | 52 | # 内存使用 53 | memory_info = process.memory_info() 54 | memory_mb = memory_info.rss / (1024 * 1024) 55 | 56 | # 运行时间 57 | start_time = process.create_time() 58 | uptime_seconds = int(time.time() - start_time) 59 | 60 | # 返回统计信息 61 | return jsonify({ 62 | 'code': 0, 63 | 'message': '获取成功', 64 | 'data': { 65 | 'cpu_percent': cpu_percent, 66 | 'memory_mb': memory_mb, 67 | 'uptime_seconds': uptime_seconds, 68 | 'pid': os.getpid(), 69 | 'threads': len(process.threads()), 70 | 'connections': len(process.connections()) 71 | } 72 | }) 73 | except Exception as e: 74 | logger.error(f"获取统计信息失败: {str(e)}") 75 | return jsonify({ 76 | 'code': 5002, 77 | 'message': f'获取统计信息失败: {str(e)}', 78 | 'data': None 79 | }), 500 80 | -------------------------------------------------------------------------------- /app/api/chat_window.py: -------------------------------------------------------------------------------- 1 | @router.post("/message/send-typing") 2 | async def send_typing_text( 3 | request: Request, 4 | data: SendTypingTextRequest 5 | ): 6 | try: 7 | # Format message with @ mentions if at_list is provided 8 | message = data.message 9 | if data.at_list: 10 | # Add line break before @ mentions if message is not empty 11 | if message and not message.endswith('\n'): 12 | message += '\n' 13 | # Add @ mentions in correct format 14 | for user in data.at_list: 15 | message += f"{{@{user}}}" 16 | if user != data.at_list[-1]: # Add line break between mentions 17 | message += '\n' 18 | 19 | chat_window = await get_chat_window(data.who) 20 | if not chat_window: 21 | raise HTTPException( 22 | status_code=404, 23 | detail=f"Chat window not found for {data.who}" 24 | ) 25 | 26 | chat_window.SendTypingText(message, clear=data.clear) 27 | return success_response() 28 | 29 | except Exception as e: 30 | return error_response(3001, f"发送失败: {str(e)}") -------------------------------------------------------------------------------- /app/api/message_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息相关API 3 | """ 4 | 5 | import os 6 | import sys 7 | import time 8 | import logging 9 | from flask import Blueprint, request, jsonify 10 | from app.auth import require_api_key 11 | from app.wechat_adapter import WeChatAdapter 12 | import config_manager 13 | 14 | # 配置日志 15 | logger = logging.getLogger(__name__) 16 | 17 | # 创建蓝图 18 | message_bp = Blueprint('message', __name__) 19 | 20 | # 应用wxauto补丁 21 | wxauto_patch_path = os.path.join(os.getcwd(), "wxauto_patch.py") 22 | if os.path.exists(wxauto_patch_path): 23 | sys.path.insert(0, os.getcwd()) 24 | try: 25 | import wxauto_patch 26 | logger.info("已应用wxauto补丁,增强了图片保存功能") 27 | except Exception as e: 28 | logger.error(f"应用wxauto补丁失败: {str(e)}") 29 | 30 | @message_bp.route('/get-next-new', methods=['GET']) 31 | @require_api_key 32 | def get_next_new_message(): 33 | """获取下一条新消息""" 34 | try: 35 | # 获取参数 36 | savepic = request.args.get('savepic', 'false').lower() == 'true' 37 | savefile = request.args.get('savefile', 'false').lower() == 'true' 38 | savevoice = request.args.get('savevoice', 'false').lower() == 'true' 39 | parseurl = request.args.get('parseurl', 'false').lower() == 'true' 40 | 41 | # 确保临时目录存在 42 | config_manager.ensure_dirs() 43 | 44 | # 获取微信适配器 45 | adapter = WeChatAdapter() 46 | 47 | # 获取下一条新消息 48 | messages = adapter.GetNextNewMessage( 49 | savepic=savepic, 50 | savefile=savefile, 51 | savevoice=savevoice, 52 | parseurl=parseurl 53 | ) 54 | 55 | # 转换消息格式 56 | result = {} 57 | if messages: 58 | for chat_name, msg_list in messages.items(): 59 | result[chat_name] = [] 60 | for msg in msg_list: 61 | # 检查文件路径是否存在 62 | file_path = None 63 | if hasattr(msg, 'content') and msg.content and isinstance(msg.content, str): 64 | if os.path.exists(msg.content): 65 | file_path = msg.content 66 | 67 | # 构建消息对象 68 | msg_obj = { 69 | 'id': getattr(msg, 'id', None), 70 | 'type': getattr(msg, 'type', None), 71 | 'sender': getattr(msg, 'sender', None), 72 | 'sender_remark': getattr(msg, 'sender_remark', None) if hasattr(msg, 'sender_remark') else None, 73 | 'content': getattr(msg, 'content', None), 74 | 'file_path': file_path, 75 | 'mtype': None # 消息类型,如图片、文件等 76 | } 77 | 78 | # 判断消息类型 79 | if file_path and file_path.endswith(('.jpg', '.jpeg', '.png', '.gif')): 80 | msg_obj['mtype'] = 'image' 81 | elif file_path and file_path.endswith(('.mp3', '.wav', '.amr')): 82 | msg_obj['mtype'] = 'voice' 83 | elif file_path: 84 | msg_obj['mtype'] = 'file' 85 | 86 | result[chat_name].append(msg_obj) 87 | 88 | return jsonify({ 89 | 'code': 0, 90 | 'message': '获取成功', 91 | 'data': { 92 | 'messages': result 93 | } 94 | }) 95 | except Exception as e: 96 | logger.error(f"获取下一条新消息失败: {str(e)}") 97 | import traceback 98 | traceback.print_exc() 99 | return jsonify({ 100 | 'code': 1, 101 | 'message': f'获取失败: {str(e)}', 102 | 'data': None 103 | }) 104 | -------------------------------------------------------------------------------- /app/api/plugin_routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | 插件管理API路由 3 | 提供wxauto和wxautox库的安装和管理功能 4 | """ 5 | 6 | import os 7 | import sys 8 | import tempfile 9 | import shutil 10 | import logging 11 | from flask import Blueprint, jsonify, request, current_app 12 | from werkzeug.utils import secure_filename 13 | from app.auth import require_api_key 14 | from app import plugin_manager 15 | import config_manager 16 | 17 | # 配置日志 18 | logger = logging.getLogger(__name__) 19 | 20 | # 创建蓝图 21 | plugin_bp = Blueprint('plugins', __name__) 22 | 23 | @plugin_bp.route('/status', methods=['GET']) 24 | @require_api_key 25 | def get_plugins_status(): 26 | """获取插件状态""" 27 | try: 28 | status = plugin_manager.get_plugins_status() 29 | return jsonify({ 30 | 'code': 0, 31 | 'message': '获取成功', 32 | 'data': status 33 | }) 34 | except Exception as e: 35 | logger.error(f"获取插件状态失败: {str(e)}") 36 | return jsonify({ 37 | 'code': 5001, 38 | 'message': f'获取插件状态失败: {str(e)}', 39 | 'data': None 40 | }), 500 41 | 42 | @plugin_bp.route('/install-wxauto', methods=['POST']) 43 | @require_api_key 44 | def install_wxauto(): 45 | """安装/修复wxauto库""" 46 | try: 47 | # 检查wxauto文件夹是否存在 48 | wxauto_path = os.path.join(os.getcwd(), "wxauto") 49 | if not os.path.exists(wxauto_path) or not os.path.isdir(wxauto_path): 50 | return jsonify({ 51 | 'code': 4001, 52 | 'message': 'wxauto文件夹不存在', 53 | 'data': None 54 | }), 400 55 | 56 | # 检查wxauto文件夹中是否包含必要的文件 57 | init_file = os.path.join(wxauto_path, "wxauto", "__init__.py") 58 | wxauto_file = os.path.join(wxauto_path, "wxauto", "wxauto.py") 59 | 60 | if not os.path.exists(init_file) or not os.path.exists(wxauto_file): 61 | return jsonify({ 62 | 'code': 4002, 63 | 'message': 'wxauto文件夹结构不完整,缺少必要文件', 64 | 'data': None 65 | }), 400 66 | 67 | # 确保本地wxauto文件夹在Python路径中 68 | if wxauto_path not in sys.path: 69 | sys.path.insert(0, wxauto_path) 70 | 71 | # 尝试导入 72 | try: 73 | import wxauto 74 | import importlib 75 | importlib.reload(wxauto) # 重新加载模块,确保使用最新版本 76 | 77 | # 更新配置文件 78 | config = config_manager.load_app_config() 79 | config['wechat_lib'] = 'wxauto' 80 | config_manager.save_app_config(config) 81 | 82 | return jsonify({ 83 | 'code': 0, 84 | 'message': 'wxauto库安装/修复成功', 85 | 'data': {'path': wxauto_path} 86 | }) 87 | except ImportError as e: 88 | return jsonify({ 89 | 'code': 4003, 90 | 'message': f'wxauto库导入失败: {str(e)}', 91 | 'data': None 92 | }), 500 93 | except Exception as e: 94 | logger.error(f"安装/修复wxauto库失败: {str(e)}") 95 | return jsonify({ 96 | 'code': 5001, 97 | 'message': f'安装/修复wxauto库失败: {str(e)}', 98 | 'data': None 99 | }), 500 100 | 101 | @plugin_bp.route('/upload-wxautox', methods=['POST']) 102 | @require_api_key 103 | def upload_wxautox(): 104 | """上传并安装wxautox库""" 105 | try: 106 | # 检查是否有文件上传 107 | if 'file' not in request.files: 108 | return jsonify({ 109 | 'code': 4001, 110 | 'message': '没有上传文件', 111 | 'data': None 112 | }), 400 113 | 114 | file = request.files['file'] 115 | 116 | # 检查文件名是否为空 117 | if file.filename == '': 118 | return jsonify({ 119 | 'code': 4002, 120 | 'message': '文件名为空', 121 | 'data': None 122 | }), 400 123 | 124 | # 检查文件扩展名 125 | if not file.filename.endswith('.whl'): 126 | return jsonify({ 127 | 'code': 4003, 128 | 'message': '文件不是有效的wheel文件', 129 | 'data': None 130 | }), 400 131 | 132 | # 检查文件名是否包含wxautox 133 | if 'wxautox-' not in file.filename: 134 | return jsonify({ 135 | 'code': 4004, 136 | 'message': '文件不是wxautox wheel文件', 137 | 'data': None 138 | }), 400 139 | 140 | # 确保临时目录存在 141 | config_manager.ensure_dirs() 142 | temp_dir = config_manager.TEMP_DIR 143 | 144 | # 保存文件到临时目录 145 | filename = secure_filename(file.filename) 146 | file_path = os.path.join(temp_dir, filename) 147 | file.save(file_path) 148 | 149 | logger.info(f"wxautox wheel文件已保存到: {file_path}") 150 | 151 | # 安装wxautox 152 | success, message = plugin_manager.install_wxautox(file_path) 153 | 154 | if success: 155 | return jsonify({ 156 | 'code': 0, 157 | 'message': message, 158 | 'data': {'file_path': file_path} 159 | }) 160 | else: 161 | return jsonify({ 162 | 'code': 4005, 163 | 'message': message, 164 | 'data': None 165 | }), 500 166 | except Exception as e: 167 | logger.error(f"上传并安装wxautox库失败: {str(e)}") 168 | return jsonify({ 169 | 'code': 5001, 170 | 'message': f'上传并安装wxautox库失败: {str(e)}', 171 | 'data': None 172 | }), 500 173 | -------------------------------------------------------------------------------- /app/api_queue.py: -------------------------------------------------------------------------------- 1 | """ 2 | API请求队列处理模块 3 | 提供高并发支持和请求队列管理 4 | """ 5 | 6 | import queue 7 | import threading 8 | import time 9 | import traceback 10 | from functools import wraps 11 | from app.logs import logger 12 | 13 | # 全局请求队列 14 | request_queue = queue.Queue() 15 | 16 | # 请求计数器 17 | request_counter = 0 18 | error_counter = 0 19 | 20 | # 队列处理线程数量 21 | WORKER_THREADS = 5 22 | 23 | # 队列处理线程列表 24 | worker_threads = [] 25 | 26 | # 队列状态 27 | queue_running = False 28 | 29 | # 锁,用于线程安全的计数器更新 30 | counter_lock = threading.Lock() 31 | 32 | def enqueue_request(func, *args, **kwargs): 33 | """ 34 | 将请求加入队列 35 | 36 | Args: 37 | func: 要执行的函数 38 | args: 位置参数 39 | kwargs: 关键字参数 40 | 41 | Returns: 42 | 任务ID 43 | """ 44 | global request_counter 45 | 46 | with counter_lock: 47 | request_counter += 1 48 | task_id = request_counter 49 | 50 | # 创建任务 51 | task = { 52 | 'id': task_id, 53 | 'func': func, 54 | 'args': args, 55 | 'kwargs': kwargs, 56 | 'result_queue': queue.Queue(), 57 | 'timestamp': time.time() 58 | } 59 | 60 | # 加入队列 61 | request_queue.put(task) 62 | logger.debug(f"任务 {task_id} 已加入队列") 63 | 64 | return task 65 | 66 | def queue_processor(): 67 | """队列处理线程函数""" 68 | global error_counter 69 | 70 | logger.info("队列处理线程已启动") 71 | 72 | while queue_running: 73 | try: 74 | # 从队列获取任务,超时1秒 75 | try: 76 | task = request_queue.get(timeout=1) 77 | except queue.Empty: 78 | continue 79 | 80 | # 处理任务 81 | try: 82 | logger.debug(f"处理任务 {task['id']}") 83 | result = task['func'](*task['args'], **task['kwargs']) 84 | task['result_queue'].put(('success', result)) 85 | except Exception as e: 86 | with counter_lock: 87 | error_counter += 1 88 | logger.error(f"任务 {task['id']} 处理失败: {str(e)}") 89 | logger.debug(traceback.format_exc()) 90 | task['result_queue'].put(('error', str(e))) 91 | finally: 92 | request_queue.task_done() 93 | 94 | except Exception as e: 95 | with counter_lock: 96 | error_counter += 1 97 | logger.error(f"队列处理线程异常: {str(e)}") 98 | logger.debug(traceback.format_exc()) 99 | 100 | logger.info("队列处理线程已停止") 101 | 102 | def start_queue_processors(): 103 | """启动队列处理线程""" 104 | global queue_running, worker_threads 105 | 106 | if queue_running: 107 | return 108 | 109 | queue_running = True 110 | worker_threads = [] 111 | 112 | # 创建并启动工作线程 113 | for i in range(WORKER_THREADS): 114 | thread = threading.Thread(target=queue_processor, daemon=True, name=f"QueueProcessor-{i}") 115 | thread.start() 116 | worker_threads.append(thread) 117 | 118 | logger.info(f"已启动 {WORKER_THREADS} 个队列处理线程") 119 | 120 | def stop_queue_processors(): 121 | """停止队列处理线程""" 122 | global queue_running 123 | 124 | if not queue_running: 125 | return 126 | 127 | queue_running = False 128 | 129 | # 等待所有线程结束 130 | for thread in worker_threads: 131 | thread.join(timeout=2) 132 | 133 | logger.info("所有队列处理线程已停止") 134 | 135 | def queue_task(timeout=30): 136 | """ 137 | 将API请求加入队列的装饰器 138 | 139 | Args: 140 | timeout: 超时时间(秒) 141 | 142 | Returns: 143 | 装饰器函数 144 | """ 145 | def decorator(func): 146 | @wraps(func) 147 | def wrapper(*args, **kwargs): 148 | # 将请求加入队列 149 | task = enqueue_request(func, *args, **kwargs) 150 | 151 | # 等待结果 152 | try: 153 | result_type, result = task['result_queue'].get(timeout=timeout) 154 | if result_type == 'error': 155 | raise Exception(result) 156 | return result 157 | except queue.Empty: 158 | raise TimeoutError(f"任务 {task['id']} 处理超时") 159 | 160 | return wrapper 161 | return decorator 162 | 163 | def get_queue_stats(): 164 | """获取队列统计信息""" 165 | return { 166 | 'queue_size': request_queue.qsize(), 167 | 'request_count': request_counter, 168 | 'error_count': error_counter, 169 | 'worker_threads': len(worker_threads), 170 | 'queue_running': queue_running 171 | } 172 | 173 | # 启动队列处理器 174 | start_queue_processors() 175 | -------------------------------------------------------------------------------- /app/api_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | API服务逻辑 3 | 专门用于启动和管理API服务 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | import traceback 10 | 11 | # 配置日志 12 | logger = logging.getLogger(__name__) 13 | 14 | def check_mutex(): 15 | """检查互斥锁,确保同一时间只有一个API服务实例在运行""" 16 | # 如果禁用互斥锁检查,则跳过 17 | if os.environ.get("WXAUTO_NO_MUTEX_CHECK") == "1": 18 | logger.info("已禁用互斥锁检查,跳过") 19 | return True 20 | 21 | try: 22 | # 导入互斥锁模块 23 | try: 24 | # 首先尝试从app包导入 25 | from app import app_mutex 26 | logger.info("成功从app包导入 app_mutex 模块") 27 | except ImportError: 28 | # 如果失败,尝试直接导入(兼容旧版本) 29 | import app_mutex 30 | logger.info("成功直接导入 app_mutex 模块") 31 | 32 | # 导入配置模块 33 | from app.config import Config 34 | port = Config.PORT 35 | logger.info(f"API服务端口: {port}") 36 | 37 | # 创建API服务互斥锁 38 | api_mutex = app_mutex.create_api_mutex(port) 39 | 40 | # 尝试获取API服务互斥锁 41 | if not api_mutex.acquire(): 42 | logger.warning(f"端口 {port} 已被占用,API服务可能已在运行") 43 | return False 44 | 45 | logger.info(f"成功获取API服务互斥锁,端口: {port}") 46 | return True 47 | except ImportError as e: 48 | logger.warning(f"无法导入互斥锁模块或配置模块: {str(e)}") 49 | logger.warning(traceback.format_exc()) 50 | return True 51 | except Exception as e: 52 | logger.error(f"互斥锁检查失败: {str(e)}") 53 | logger.error(traceback.format_exc()) 54 | return True 55 | 56 | def check_dependencies(): 57 | """检查依赖项""" 58 | try: 59 | # 尝试导入必要的模块 60 | import flask 61 | import requests 62 | import psutil 63 | 64 | # 使用wxauto_wrapper模块确保wxauto库能够被正确导入 65 | try: 66 | from app.wxauto_wrapper import get_wxauto 67 | wxauto = get_wxauto() 68 | if wxauto: 69 | 70 | # 尝试导入wxauto包装器 71 | try: 72 | from app.wxauto_wrapper.wrapper import get_wrapper 73 | wrapper = get_wrapper() 74 | 75 | except Exception as e: 76 | logger.error(f"初始化wxauto包装器失败: {str(e)}") 77 | else: 78 | logger.error("wxauto库导入失败,无法启动API服务") 79 | return False 80 | except ImportError as e: 81 | logger.error(f"导入wxauto_wrapper模块失败: {str(e)}") 82 | logger.error("尝试使用传统方式导入wxauto...") 83 | 84 | # 尝试从本地目录导入 85 | wxauto_path = os.path.join(os.getcwd(), "wxauto") 86 | if os.path.exists(wxauto_path) and os.path.isdir(wxauto_path): 87 | if wxauto_path not in sys.path: 88 | sys.path.insert(0, wxauto_path) 89 | try: 90 | import wxauto 91 | logger.info(f"成功从本地目录导入wxauto: {wxauto_path}") 92 | except ImportError as e: 93 | logger.error(f"从本地目录导入wxauto失败: {str(e)}") 94 | return False 95 | else: 96 | logger.error("wxauto库未安装且本地目录不存在") 97 | return False 98 | 99 | # 检查wxautox是否可用(可选) 100 | try: 101 | import wxautox 102 | logger.info("wxautox库已安装") 103 | except ImportError: 104 | logger.info("wxautox库未安装,将使用wxauto库") 105 | 106 | logger.info("依赖项检查成功") 107 | return True 108 | except ImportError as e: 109 | logger.error(f"依赖项检查失败: {str(e)}") 110 | logger.error(traceback.format_exc()) 111 | return False 112 | except Exception as e: 113 | logger.error(f"检查依赖项时出错: {str(e)}") 114 | logger.error(traceback.format_exc()) 115 | return False 116 | 117 | def start_queue_processors(): 118 | """启动队列处理器""" 119 | try: 120 | # 导入队列处理器模块 121 | try: 122 | from app.api_queue import start_queue_processors as start_queue 123 | 124 | # 启动队列处理器 125 | start_queue() 126 | logger.info("队列处理器已启动") 127 | return True 128 | except ImportError as e: 129 | # 如果找不到队列处理器模块,记录警告但继续执行 130 | logger.warning(f"导入队列处理器模块失败: {str(e)}") 131 | logger.warning("将继续执行,但某些功能可能不可用") 132 | return True 133 | except Exception as e: 134 | logger.error(f"启动队列处理器时出错: {str(e)}") 135 | logger.error(traceback.format_exc()) 136 | # 即使队列处理器启动失败,也继续执行 137 | logger.warning("将继续执行,但某些功能可能不可用") 138 | return True 139 | 140 | def start_api(): 141 | """启动API服务""" 142 | try: 143 | logger.info("===== 开始启动API服务 =====") 144 | 145 | # 检查互斥锁 146 | logger.info("正在检查互斥锁...") 147 | if not check_mutex(): 148 | logger.info("互斥锁检查失败,退出") 149 | sys.exit(0) 150 | logger.info("互斥锁检查通过") 151 | 152 | # 检查依赖项 153 | logger.info("正在检查依赖项...") 154 | if not check_dependencies(): 155 | logger.error("依赖项检查失败,无法启动API服务") 156 | sys.exit(1) 157 | logger.info("依赖项检查通过") 158 | 159 | # 启动队列处理器 160 | logger.info("正在启动队列处理器...") 161 | if not start_queue_processors(): 162 | logger.error("队列处理器启动失败,无法启动API服务") 163 | sys.exit(1) 164 | logger.info("队列处理器启动成功") 165 | 166 | # 创建并启动Flask应用 167 | try: 168 | # 记录当前环境信息 169 | logger.info(f"当前工作目录: {os.getcwd()}") 170 | logger.info(f"Python路径: {sys.path}") 171 | 172 | # 确保app目录在Python路径中 173 | app_dir = os.path.join(os.getcwd(), "app") 174 | if os.path.exists(app_dir) and app_dir not in sys.path: 175 | sys.path.insert(0, app_dir) 176 | logger.info(f"已将app目录添加到Python路径: {app_dir}") 177 | 178 | # 导入Flask应用创建函数 179 | logger.info("正在尝试导入Flask应用创建函数...") 180 | try: 181 | # 首先尝试从app包导入 182 | from app import create_app 183 | logger.info("成功从app包导入Flask应用创建函数") 184 | except ImportError as e: 185 | logger.warning(f"从app包导入Flask应用创建函数失败: {str(e)}") 186 | logger.warning(traceback.format_exc()) 187 | 188 | # 尝试直接导入 189 | try: 190 | import create_app 191 | logger.info("成功直接导入Flask应用创建函数") 192 | except ImportError: 193 | # 尝试直接导入app模块 194 | try: 195 | import app 196 | logger.info("成功导入app模块") 197 | 198 | # 尝试从app模块中获取create_app函数 199 | if hasattr(app, 'create_app'): 200 | create_app = app.create_app 201 | logger.info("成功从app模块中获取create_app函数") 202 | else: 203 | # 尝试从app.__init__模块导入 204 | try: 205 | from app.__init__ import create_app 206 | logger.info("成功从app.__init__模块导入create_app函数") 207 | except ImportError: 208 | logger.error("app模块中没有create_app函数") 209 | sys.exit(1) 210 | except ImportError as e: 211 | logger.error(f"导入app模块失败: {str(e)}") 212 | logger.error(traceback.format_exc()) 213 | sys.exit(1) 214 | 215 | # 创建应用 216 | logger.info("正在创建Flask应用...") 217 | app = create_app() 218 | logger.info("成功创建Flask应用") 219 | 220 | # 获取配置信息 221 | host = app.config.get('HOST', '0.0.0.0') 222 | port = app.config.get('PORT', 5000) 223 | debug = app.config.get('DEBUG', False) 224 | 225 | logger.info(f"正在启动Flask应用...") 226 | logger.info(f"监听地址: {host}:{port}") 227 | logger.info(f"调试模式: {debug}") 228 | 229 | # 禁用 werkzeug 的重新加载器,避免可能的端口冲突 230 | app.run( 231 | host=host, 232 | port=port, 233 | debug=debug, 234 | use_reloader=False, 235 | threaded=True 236 | ) 237 | except ImportError as e: 238 | logger.error(f"导入Flask应用创建函数失败: {str(e)}") 239 | logger.error(traceback.format_exc()) 240 | sys.exit(1) 241 | except Exception as e: 242 | logger.error(f"启动Flask应用时出错: {str(e)}") 243 | logger.error(traceback.format_exc()) 244 | sys.exit(1) 245 | except Exception as e: 246 | logger.error(f"API服务启动过程中发生未捕获的异常: {str(e)}") 247 | logger.error(traceback.format_exc()) 248 | sys.exit(1) 249 | 250 | if __name__ == "__main__": 251 | # 设置环境变量,标记为API服务进程 252 | os.environ["WXAUTO_SERVICE_TYPE"] = "api" 253 | 254 | # 启动API服务 255 | start_api() 256 | -------------------------------------------------------------------------------- /app/app_mutex.py: -------------------------------------------------------------------------------- 1 | """ 2 | 应用程序互斥锁模块 3 | 确保同一时间只能有一个UI实例和一个API服务实例在运行 4 | """ 5 | 6 | import os 7 | import sys 8 | import time 9 | import logging 10 | import tempfile 11 | import atexit 12 | import socket 13 | from pathlib import Path 14 | 15 | # 配置日志 16 | logger = logging.getLogger(__name__) 17 | 18 | class AppMutex: 19 | """应用程序互斥锁""" 20 | 21 | def __init__(self, name, port=None): 22 | """ 23 | 初始化互斥锁 24 | 25 | Args: 26 | name (str): 互斥锁名称,用于区分不同的互斥锁 27 | port (int, optional): 如果指定,则使用端口锁定机制 28 | """ 29 | self.name = name 30 | self.port = port 31 | self.lock_file = None 32 | self.lock_socket = None 33 | self.is_locked = False 34 | 35 | def acquire(self): 36 | """ 37 | 获取互斥锁 38 | 39 | Returns: 40 | bool: 是否成功获取互斥锁 41 | """ 42 | # 如果指定了端口,则使用端口锁定机制 43 | if self.port is not None: 44 | return self._acquire_port_lock() 45 | 46 | # 否则使用文件锁定机制 47 | return self._acquire_file_lock() 48 | 49 | def _acquire_port_lock(self): 50 | """ 51 | 使用端口锁定机制获取互斥锁 52 | 53 | Returns: 54 | bool: 是否成功获取互斥锁 55 | """ 56 | try: 57 | logger.info(f"尝试获取端口锁: {self.port}") 58 | self.lock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 59 | 60 | # 设置端口重用选项,避免TIME_WAIT状态导致的问题 61 | self.lock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 62 | 63 | # 尝试绑定端口 64 | try: 65 | self.lock_socket.bind(('localhost', self.port)) 66 | self.lock_socket.listen(1) 67 | self.is_locked = True 68 | logger.info(f"成功获取端口锁: {self.port}") 69 | return True 70 | except socket.error as e: 71 | logger.warning(f"无法绑定端口 {self.port}: {str(e)}") 72 | 73 | # 尝试使用0.0.0.0地址绑定 74 | try: 75 | self.lock_socket.close() 76 | self.lock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | self.lock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 78 | self.lock_socket.bind(('0.0.0.0', self.port)) 79 | self.lock_socket.listen(1) 80 | self.is_locked = True 81 | logger.info(f"成功获取端口锁(0.0.0.0): {self.port}") 82 | return True 83 | except socket.error as e2: 84 | logger.warning(f"无法绑定端口(0.0.0.0) {self.port}: {str(e2)}") 85 | self.lock_socket = None 86 | return False 87 | except Exception as e: 88 | logger.error(f"获取端口锁时出错: {str(e)}") 89 | self.lock_socket = None 90 | return False 91 | 92 | def _acquire_file_lock(self): 93 | """ 94 | 使用文件锁定机制获取互斥锁 95 | 96 | Returns: 97 | bool: 是否成功获取互斥锁 98 | """ 99 | try: 100 | # 在临时目录中创建锁文件 101 | temp_dir = tempfile.gettempdir() 102 | lock_file_path = os.path.join(temp_dir, f"{self.name}.lock") 103 | 104 | # 检查锁文件是否存在 105 | if os.path.exists(lock_file_path): 106 | # 检查锁文件是否过期(超过1小时) 107 | if time.time() - os.path.getmtime(lock_file_path) > 3600: 108 | logger.warning(f"发现过期的锁文件: {lock_file_path},将删除") 109 | os.remove(lock_file_path) 110 | else: 111 | logger.warning(f"发现有效的锁文件: {lock_file_path},无法获取互斥锁") 112 | return False 113 | 114 | # 创建锁文件 115 | self.lock_file = open(lock_file_path, 'w') 116 | self.lock_file.write(f"{os.getpid()}") 117 | self.lock_file.flush() 118 | self.is_locked = True 119 | 120 | # 注册退出时的清理函数 121 | atexit.register(self.release) 122 | 123 | logger.info(f"成功获取文件锁: {lock_file_path}") 124 | return True 125 | except Exception as e: 126 | logger.error(f"获取文件锁失败: {str(e)}") 127 | return False 128 | 129 | def release(self): 130 | """释放互斥锁""" 131 | if not self.is_locked: 132 | return 133 | 134 | # 释放端口锁 135 | if self.lock_socket is not None: 136 | try: 137 | self.lock_socket.close() 138 | logger.info(f"已释放端口锁: {self.port}") 139 | except Exception as e: 140 | logger.error(f"释放端口锁失败: {str(e)}") 141 | finally: 142 | self.lock_socket = None 143 | 144 | # 释放文件锁 145 | if self.lock_file is not None: 146 | try: 147 | lock_file_path = self.lock_file.name 148 | self.lock_file.close() 149 | if os.path.exists(lock_file_path): 150 | os.remove(lock_file_path) 151 | logger.info(f"已释放文件锁: {lock_file_path}") 152 | except Exception as e: 153 | logger.error(f"释放文件锁失败: {str(e)}") 154 | finally: 155 | self.lock_file = None 156 | 157 | self.is_locked = False 158 | 159 | # 创建UI互斥锁 160 | ui_mutex = AppMutex("wxauto_http_api_ui") 161 | 162 | # 创建API服务互斥锁(使用端口锁定机制) 163 | def create_api_mutex(port=5000): 164 | """ 165 | 创建API服务互斥锁 166 | 167 | Args: 168 | port (int): API服务端口 169 | 170 | Returns: 171 | AppMutex: API服务互斥锁 172 | """ 173 | return AppMutex("wxauto_http_api_api", port=port) 174 | -------------------------------------------------------------------------------- /app/auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import request, jsonify 3 | from app.config import Config 4 | 5 | def require_api_key(f): 6 | @wraps(f) 7 | def decorated_function(*args, **kwargs): 8 | api_key = request.headers.get('X-API-Key') 9 | if not api_key: 10 | return jsonify({ 11 | 'code': 1001, 12 | 'message': '缺少API密钥', 13 | 'data': None 14 | }), 401 15 | 16 | if api_key not in Config.API_KEYS: 17 | return jsonify({ 18 | 'code': 1001, 19 | 'message': 'API密钥无效', 20 | 'data': None 21 | }), 401 22 | 23 | return f(*args, **kwargs) 24 | return decorated_function -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import sys 4 | from dotenv import load_dotenv 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | # 确保config_manager可以被导入 9 | current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 10 | if current_dir not in sys.path: 11 | sys.path.insert(0, current_dir) 12 | 13 | # 导入配置管理模块 14 | try: 15 | import config_manager 16 | except ImportError: 17 | # 如果无法导入,则使用环境变量 18 | load_dotenv() 19 | config_manager = None 20 | 21 | class Config: 22 | # 从配置文件或环境变量加载配置 23 | if config_manager: 24 | # 确保目录存在 25 | config_manager.ensure_dirs() 26 | 27 | # 加载应用配置 28 | app_config = config_manager.load_app_config() 29 | 30 | # API配置 31 | API_KEYS = app_config.get('api_keys', ['test-key-2']) 32 | 33 | # Flask配置 34 | PORT = app_config.get('port', 5000) 35 | 36 | # 微信库选择配置 37 | WECHAT_LIB = app_config.get('wechat_lib', 'wxauto').lower() 38 | else: 39 | # 如果无法导入config_manager,则使用环境变量 40 | API_KEYS = os.getenv('API_KEYS', 'test-key-2').split(',') 41 | PORT = int(os.getenv('PORT', 5000)) 42 | WECHAT_LIB = os.getenv('WECHAT_LIB', 'wxauto').lower() 43 | 44 | # 其他固定配置 45 | SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key') 46 | DEBUG = True 47 | HOST = '0.0.0.0' # 允许所有IP访问 48 | 49 | # 限流配置 50 | RATELIMIT_DEFAULT = "100 per minute" 51 | RATELIMIT_STORAGE_URL = "memory://" 52 | 53 | # 日志配置 54 | LOG_LEVEL = logging.INFO # 设置为INFO级别,减少DEBUG日志 55 | LOG_FORMAT = '%(asctime)s - [%(wechat_lib)s] - %(levelname)s - %(message)s' 56 | LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' # 统一的时间戳格式 57 | LOG_MAX_BYTES = 20 * 1024 * 1024 # 20MB 58 | LOG_BACKUP_COUNT = 5 # 保留5个备份文件 59 | 60 | # 日志文件路径 61 | DATA_DIR = Path("data") 62 | API_DIR = DATA_DIR / "api" 63 | LOGS_DIR = API_DIR / "logs" 64 | LOG_FILENAME = f"api_{datetime.now().strftime('%Y%m%d')}.log" 65 | LOG_FILE = str(LOGS_DIR / LOG_FILENAME) 66 | 67 | # 微信监控配置 68 | WECHAT_CHECK_INTERVAL = int(os.getenv('WECHAT_CHECK_INTERVAL', 60)) # 连接检查间隔(秒) 69 | WECHAT_AUTO_RECONNECT = os.getenv('WECHAT_AUTO_RECONNECT', 'true').lower() == 'true' 70 | WECHAT_RECONNECT_DELAY = int(os.getenv('WECHAT_RECONNECT_DELAY', 30)) # 重连延迟(秒) 71 | WECHAT_MAX_RETRY = int(os.getenv('WECHAT_MAX_RETRY', 3)) # 最大重试次数 -------------------------------------------------------------------------------- /app/config_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置管理模块 3 | 用于处理配置文件的读写 4 | """ 5 | 6 | import os 7 | import json 8 | import logging 9 | from pathlib import Path 10 | from dotenv import load_dotenv 11 | 12 | # 配置目录 13 | DATA_DIR = Path("data") 14 | API_DIR = DATA_DIR / "api" 15 | CONFIG_DIR = API_DIR / "config" 16 | LOGS_DIR = API_DIR / "logs" 17 | TEMP_DIR = API_DIR / "temp" # 临时文件目录,用于保存图片、文件等 18 | 19 | # 配置文件路径 20 | LOG_FILTER_CONFIG = CONFIG_DIR / "log_filter.json" 21 | APP_CONFIG_FILE = CONFIG_DIR / "app_config.json" 22 | 23 | # 确保目录存在 24 | def ensure_dirs(): 25 | """确保所有必要的目录都存在""" 26 | for directory in [DATA_DIR, API_DIR, CONFIG_DIR, LOGS_DIR, TEMP_DIR]: 27 | directory.mkdir(exist_ok=True, parents=True) 28 | 29 | # 默认配置 30 | DEFAULT_LOG_FILTER = { 31 | "hide_status_check": True, # 默认隐藏微信状态检查日志 32 | "hide_debug": True, # 默认隐藏DEBUG级别日志 33 | "custom_filter": "" 34 | } 35 | 36 | # 默认应用配置 37 | DEFAULT_APP_CONFIG = { 38 | "api_keys": ["test-key-2"], 39 | "port": 5000, 40 | "wechat_lib": "wxauto" 41 | } 42 | 43 | def load_log_filter_config(force_defaults=False): 44 | """ 45 | 加载日志过滤器配置 46 | 47 | Args: 48 | force_defaults (bool): 是否强制使用默认值覆盖现有配置 49 | 50 | Returns: 51 | dict: 日志过滤器配置 52 | """ 53 | ensure_dirs() 54 | 55 | if not LOG_FILTER_CONFIG.exists() or force_defaults: 56 | # 如果配置文件不存在或强制使用默认值,创建默认配置 57 | save_log_filter_config(DEFAULT_LOG_FILTER) 58 | return DEFAULT_LOG_FILTER 59 | 60 | try: 61 | with open(LOG_FILTER_CONFIG, 'r', encoding='utf-8') as f: 62 | config = json.load(f) 63 | 64 | # 确保所有必要的键都存在,并使用默认值 65 | config_updated = False 66 | for key in DEFAULT_LOG_FILTER: 67 | if key not in config: 68 | config[key] = DEFAULT_LOG_FILTER[key] 69 | config_updated = True 70 | # 对于特定的键,强制使用默认值 71 | elif key in ['hide_status_check', 'hide_debug'] and not config[key]: 72 | config[key] = DEFAULT_LOG_FILTER[key] 73 | config_updated = True 74 | 75 | # 如果配置被更新,保存回文件 76 | if config_updated: 77 | save_log_filter_config(config) 78 | logging.info("日志过滤器配置已更新为默认值") 79 | 80 | return config 81 | except Exception as e: 82 | logging.error(f"加载日志过滤器配置失败: {str(e)}") 83 | return DEFAULT_LOG_FILTER 84 | 85 | def save_log_filter_config(config): 86 | """ 87 | 保存日志过滤器配置 88 | 89 | Args: 90 | config (dict): 日志过滤器配置 91 | """ 92 | ensure_dirs() 93 | 94 | try: 95 | with open(LOG_FILTER_CONFIG, 'w', encoding='utf-8') as f: 96 | json.dump(config, f, ensure_ascii=False, indent=4) 97 | logging.debug("日志过滤器配置已保存") 98 | except Exception as e: 99 | logging.error(f"保存日志过滤器配置失败: {str(e)}") 100 | 101 | def load_app_config(): 102 | """ 103 | 加载应用配置,如果配置文件不存在,则从.env文件读取默认配置并创建配置文件 104 | 105 | Returns: 106 | dict: 应用配置 107 | """ 108 | ensure_dirs() 109 | 110 | # 如果配置文件不存在,从.env文件读取默认配置 111 | if not APP_CONFIG_FILE.exists(): 112 | # 加载.env文件 113 | env_file = Path(".env") 114 | if env_file.exists(): 115 | load_dotenv(env_file) 116 | 117 | # 从环境变量读取配置 118 | api_keys = os.getenv('API_KEYS', 'test-key-2').split(',') 119 | port = int(os.getenv('PORT', 5000)) 120 | wechat_lib = os.getenv('WECHAT_LIB', 'wxauto').lower() 121 | 122 | # 创建配置 123 | config = { 124 | "api_keys": api_keys, 125 | "port": port, 126 | "wechat_lib": wechat_lib 127 | } 128 | else: 129 | # 如果.env文件不存在,使用默认配置 130 | config = DEFAULT_APP_CONFIG.copy() 131 | 132 | # 保存配置 133 | save_app_config(config) 134 | return config 135 | 136 | # 如果配置文件存在,直接读取 137 | try: 138 | with open(APP_CONFIG_FILE, 'r', encoding='utf-8') as f: 139 | config = json.load(f) 140 | 141 | # 确保所有必要的键都存在 142 | for key, value in DEFAULT_APP_CONFIG.items(): 143 | if key not in config: 144 | config[key] = value 145 | 146 | return config 147 | except Exception as e: 148 | logging.error(f"加载应用配置失败: {str(e)}") 149 | return DEFAULT_APP_CONFIG.copy() 150 | 151 | def save_app_config(config): 152 | """ 153 | 保存应用配置 154 | 155 | Args: 156 | config (dict): 应用配置 157 | """ 158 | ensure_dirs() 159 | 160 | try: 161 | with open(APP_CONFIG_FILE, 'w', encoding='utf-8') as f: 162 | json.dump(config, f, ensure_ascii=False, indent=4) 163 | logging.debug("应用配置已保存") 164 | except Exception as e: 165 | logging.error(f"保存应用配置失败: {str(e)}") 166 | 167 | def get_log_file_path(filename=None): 168 | """ 169 | 获取日志文件路径 170 | 171 | Args: 172 | filename (str, optional): 日志文件名。如果为None,则使用默认文件名。 173 | 174 | Returns: 175 | Path: 日志文件路径 176 | """ 177 | ensure_dirs() 178 | 179 | if filename is None: 180 | filename = "api.log" 181 | 182 | return LOGS_DIR / filename 183 | -------------------------------------------------------------------------------- /app/fix_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | 修复路径模块 3 | 用于在打包环境中修复路径问题 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | 10 | # 配置日志 11 | logger = logging.getLogger(__name__) 12 | 13 | def fix_paths(): 14 | """ 15 | 修复路径问题 16 | 在打包环境中,确保所有必要的路径都被正确设置 17 | """ 18 | # 记录当前环境信息 19 | logger.info(f"Python版本: {sys.version}") 20 | logger.info(f"当前工作目录: {os.getcwd()}") 21 | logger.info(f"Python路径: {sys.path}") 22 | logger.info(f"是否在PyInstaller环境中运行: {getattr(sys, 'frozen', False)}") 23 | 24 | # 获取应用根目录 25 | if getattr(sys, 'frozen', False): 26 | # 如果是打包后的环境 27 | app_root = os.path.dirname(sys.executable) 28 | logger.info(f"检测到打包环境,应用根目录: {app_root}") 29 | 30 | # 在打包环境中,确保_MEIPASS目录也在Python路径中 31 | meipass = getattr(sys, '_MEIPASS', None) 32 | if meipass and meipass not in sys.path: 33 | sys.path.insert(0, meipass) 34 | logger.info(f"已将_MEIPASS目录添加到Python路径: {meipass}") 35 | else: 36 | # 如果是开发环境 37 | app_root = os.path.dirname(os.path.abspath(__file__)) 38 | logger.info(f"检测到开发环境,应用根目录: {app_root}") 39 | 40 | # 确保应用根目录在Python路径中 41 | if app_root not in sys.path: 42 | sys.path.insert(0, app_root) 43 | logger.info(f"已将应用根目录添加到Python路径: {app_root}") 44 | 45 | # 确保wxauto目录在Python路径中 46 | wxauto_path = os.path.join(app_root, "wxauto") 47 | if os.path.exists(wxauto_path) and wxauto_path not in sys.path: 48 | sys.path.insert(0, wxauto_path) 49 | logger.info(f"已将wxauto目录添加到Python路径: {wxauto_path}") 50 | 51 | # 设置工作目录为应用根目录 52 | os.chdir(app_root) 53 | logger.info(f"已将工作目录设置为应用根目录: {app_root}") 54 | 55 | # 再次记录环境信息,确认修改已生效 56 | logger.info(f"修复后的工作目录: {os.getcwd()}") 57 | logger.info(f"修复后的Python路径: {sys.path}") 58 | 59 | return app_root 60 | 61 | if __name__ == "__main__": 62 | # 配置日志 63 | logging.basicConfig( 64 | level=logging.INFO, 65 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 66 | ) 67 | 68 | # 修复路径 69 | fix_paths() 70 | -------------------------------------------------------------------------------- /app/initialize_wechat.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在初始化微信... 3 | curl -X POST -H "X-API-Key: test-key-2" http://localhost:5000/api/wechat/initialize 4 | echo. 5 | echo 初始化完成! 6 | pause 7 | -------------------------------------------------------------------------------- /app/install_wxauto.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxauto安装脚本 3 | 用于在打包环境中安装wxauto库 4 | """ 5 | 6 | import os 7 | import sys 8 | import shutil 9 | import logging 10 | import importlib.util 11 | from pathlib import Path 12 | 13 | # 配置日志 14 | logger = logging.getLogger(__name__) 15 | 16 | def find_wxauto_paths(): 17 | """ 18 | 查找所有可能的wxauto路径 19 | 20 | Returns: 21 | list: 可能的wxauto路径列表 22 | """ 23 | possible_paths = [] 24 | 25 | # 获取应用根目录 26 | if getattr(sys, 'frozen', False): 27 | # 如果是打包后的环境 28 | app_root = os.path.dirname(sys.executable) 29 | logger.info(f"检测到打包环境,应用根目录: {app_root}") 30 | 31 | # 在打包环境中,确保_MEIPASS目录也在Python路径中 32 | meipass = getattr(sys, '_MEIPASS', None) 33 | if meipass: 34 | logger.info(f"PyInstaller _MEIPASS目录: {meipass}") 35 | possible_paths.extend([ 36 | os.path.join(meipass, "wxauto"), 37 | os.path.join(meipass, "app", "wxauto"), 38 | os.path.join(app_root, "wxauto"), 39 | os.path.join(app_root, "app", "wxauto"), 40 | ]) 41 | else: 42 | # 如果是开发环境 43 | app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 44 | logger.info(f"检测到开发环境,应用根目录: {app_root}") 45 | possible_paths.extend([ 46 | os.path.join(app_root, "wxauto"), 47 | os.path.join(app_root, "app", "wxauto"), 48 | ]) 49 | 50 | # 记录所有可能的路径 51 | logger.info(f"可能的wxauto路径: {possible_paths}") 52 | 53 | # 过滤出存在的路径 54 | existing_paths = [path for path in possible_paths if os.path.exists(path) and os.path.isdir(path)] 55 | logger.info(f"存在的wxauto路径: {existing_paths}") 56 | 57 | return existing_paths 58 | 59 | def install_wxauto(): 60 | """ 61 | 安装wxauto库 62 | 63 | Returns: 64 | bool: 是否安装成功 65 | """ 66 | # 查找所有可能的wxauto路径 67 | wxauto_paths = find_wxauto_paths() 68 | 69 | if not wxauto_paths: 70 | logger.error("找不到wxauto路径,无法安装wxauto库") 71 | return False 72 | 73 | # 尝试从每个路径安装 74 | for wxauto_path in wxauto_paths: 75 | logger.info(f"尝试从路径安装wxauto: {wxauto_path}") 76 | 77 | # 检查wxauto路径下是否有wxauto子目录 78 | wxauto_inner_path = os.path.join(wxauto_path, "wxauto") 79 | if os.path.exists(wxauto_inner_path) and os.path.isdir(wxauto_inner_path): 80 | logger.info(f"找到wxauto内部目录: {wxauto_inner_path}") 81 | 82 | # 检查是否包含必要的文件 83 | wxauto_file = os.path.join(wxauto_inner_path, "wxauto.py") 84 | elements_file = os.path.join(wxauto_inner_path, "elements.py") 85 | init_file = os.path.join(wxauto_inner_path, "__init__.py") 86 | 87 | if os.path.exists(wxauto_file) and os.path.exists(elements_file): 88 | logger.info(f"找到必要的wxauto文件: {wxauto_file}, {elements_file}") 89 | 90 | # 确保wxauto路径在Python路径中 91 | if wxauto_path not in sys.path: 92 | sys.path.insert(0, wxauto_path) 93 | logger.info(f"已将wxauto路径添加到Python路径: {wxauto_path}") 94 | 95 | # 尝试导入wxauto 96 | try: 97 | # 使用importlib.util.spec_from_file_location动态导入模块 98 | spec = importlib.util.spec_from_file_location("wxauto.wxauto", wxauto_file) 99 | if spec: 100 | module = importlib.util.module_from_spec(spec) 101 | spec.loader.exec_module(module) 102 | logger.info(f"成功导入wxauto模块: {module}") 103 | return True 104 | else: 105 | logger.warning(f"无法从文件创建模块规范: {wxauto_file}") 106 | except Exception as e: 107 | logger.error(f"导入wxauto模块失败: {str(e)}") 108 | 109 | logger.error("所有路径都无法安装wxauto库") 110 | return False 111 | 112 | def ensure_wxauto_installed(): 113 | """ 114 | 确保wxauto库已安装 115 | 116 | Returns: 117 | bool: 是否已安装 118 | """ 119 | # 首先尝试直接导入 120 | try: 121 | import wxauto 122 | logger.info("wxauto库已安装") 123 | return True 124 | except ImportError: 125 | logger.warning("无法直接导入wxauto库,尝试安装") 126 | 127 | # 尝试安装 128 | return install_wxauto() 129 | 130 | if __name__ == "__main__": 131 | # 配置日志 132 | logging.basicConfig( 133 | level=logging.INFO, 134 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 135 | ) 136 | 137 | # 确保wxauto库已安装 138 | if ensure_wxauto_installed(): 139 | print("wxauto库已成功安装") 140 | sys.exit(0) 141 | else: 142 | print("wxauto库安装失败") 143 | sys.exit(1) 144 | -------------------------------------------------------------------------------- /app/logs.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import re 4 | from logging.handlers import RotatingFileHandler 5 | from app.config import Config 6 | 7 | # 创建一个内存日志处理器,用于捕获最近的错误日志 8 | class MemoryLogHandler(logging.Handler): 9 | """内存日志处理器,用于捕获最近的错误日志""" 10 | 11 | def __init__(self, capacity=100): 12 | super().__init__() 13 | self.capacity = capacity 14 | self.buffer = [] 15 | self.error_logs = [] 16 | 17 | def emit(self, record): 18 | try: 19 | # 将日志记录添加到缓冲区 20 | msg = self.format(record) 21 | self.buffer.append(msg) 22 | 23 | # 如果是错误日志,单独保存 24 | if record.levelno >= logging.ERROR: 25 | self.error_logs.append(msg) 26 | 27 | # 如果缓冲区超过容量,移除最旧的记录 28 | if len(self.buffer) > self.capacity: 29 | self.buffer.pop(0) 30 | 31 | # 如果错误日志超过容量,移除最旧的记录 32 | if len(self.error_logs) > self.capacity: 33 | self.error_logs.pop(0) 34 | except Exception: 35 | self.handleError(record) 36 | 37 | def get_logs(self): 38 | """获取所有日志""" 39 | return self.buffer 40 | 41 | def get_error_logs(self): 42 | """获取错误日志""" 43 | return self.error_logs 44 | 45 | def clear(self): 46 | """清空日志缓冲区""" 47 | self.buffer = [] 48 | self.error_logs = [] 49 | 50 | def has_error(self, error_pattern): 51 | """检查是否有匹配指定模式的错误日志""" 52 | for log in self.error_logs: 53 | if error_pattern in log: 54 | return True 55 | 56 | # 如果没有找到匹配的错误日志,尝试更宽松的匹配 57 | for log in self.error_logs: 58 | if error_pattern.lower() in log.lower(): 59 | return True 60 | 61 | return False 62 | 63 | # 创建一个自定义的日志记录器,用于添加当前使用的库信息 64 | class WeChatLibAdapter(logging.LoggerAdapter): 65 | """添加当前使用的库信息到日志记录""" 66 | 67 | def __init__(self, logger, lib_name='wxauto'): 68 | super().__init__(logger, {'wechat_lib': lib_name}) 69 | 70 | def process(self, msg, kwargs): 71 | # 确保额外参数中包含当前使用的库信息 72 | if 'extra' not in kwargs: 73 | kwargs['extra'] = self.extra 74 | else: 75 | # 如果已经有extra参数,添加wechat_lib 76 | kwargs['extra'].update(self.extra) 77 | return msg, kwargs 78 | 79 | def set_lib_name(self, lib_name): 80 | """更新当前使用的库名称""" 81 | self.extra['wechat_lib'] = lib_name 82 | 83 | @classmethod 84 | def set_lib_name_static(cls, lib_name): 85 | """静态方法,用于更新全局logger的库名称""" 86 | global logger 87 | if isinstance(logger, cls): 88 | logger.set_lib_name(lib_name) 89 | 90 | # 创建一个过滤器类,用于过滤掉重复的HTTP请求处理日志 91 | class HttpRequestFilter(logging.Filter): 92 | """过滤掉重复的HTTP请求处理日志和详细的错误堆栈跟踪""" 93 | 94 | def __init__(self): 95 | super().__init__() 96 | # 匹配HTTP请求处理相关的日志模式 97 | self.http_patterns = [ 98 | re.compile(r'BaseHTTPRequestHandler\.handle'), 99 | re.compile(r'handle_one_request'), 100 | re.compile(r'run_wsgi'), 101 | re.compile(r'execute\(self\.server\.app\)'), 102 | re.compile(r'File ".*?\\werkzeug\\serving\.py"'), 103 | re.compile(r'File ".*?\\http\\server\.py"'), 104 | re.compile(r'self\.handle_one_request\(\)'), 105 | re.compile(r'miniconda3\\envs\\wxauto-api') 106 | ] 107 | 108 | # 匹配详细错误堆栈跟踪和内部错误信息的模式 109 | self.error_patterns = [ 110 | re.compile(r'Traceback \(most recent call last\):'), 111 | re.compile(r'File ".*?", line \d+, in .*?'), 112 | re.compile(r'^\s*\^\^\^+\s*$'), # 匹配错误指示符 ^^^^^^ 113 | re.compile(r'pywintypes\.error:'), 114 | re.compile(r'获取聊天对象 .* 的新消息失败:'), 115 | re.compile(r'获取监听消息失败:'), 116 | re.compile(r'检测到窗口激活失败,重新抛出异常:'), 117 | re.compile(r'捕获到错误日志:'), 118 | re.compile(r'当前错误日志列表:'), 119 | re.compile(r'检查错误模式:'), 120 | re.compile(r'找到匹配的错误日志:'), 121 | re.compile(r'找到宽松匹配的错误日志:') 122 | ] 123 | 124 | def filter(self, record): 125 | # 检查日志消息是否匹配任何HTTP请求处理模式 126 | msg = record.getMessage() 127 | 128 | # 过滤掉HTTP请求处理相关的日志 129 | for pattern in self.http_patterns: 130 | if pattern.search(msg): 131 | return False # 过滤掉匹配的日志 132 | 133 | # 过滤掉详细的错误堆栈跟踪和内部错误信息 134 | for pattern in self.error_patterns: 135 | if pattern.search(msg): 136 | return False # 过滤掉匹配的日志 137 | 138 | # 过滤掉重复的HTTP请求处理堆栈日志 139 | if record.levelno == logging.ERROR and any(x in msg for x in [ 140 | "BaseHTTPRequestHandler.handle", 141 | "handle_one_request", 142 | "run_wsgi", 143 | "execute(self.server.app)" 144 | ]): 145 | return False 146 | 147 | # 过滤掉特定的错误日志,但保留关键的操作日志 148 | if record.levelno == logging.ERROR: 149 | # 保留关键的错误日志,如"检测到窗口激活失败,尝试重新添加监听对象" 150 | if "检测到窗口激活失败,尝试重新添加监听对象" in msg: 151 | return True 152 | # 过滤掉其他错误日志,如"激活聊天窗口失败" 153 | if "激活聊天窗口失败" in msg or "SetWindowPos" in msg or "无效的窗口句柄" in msg: 154 | return False 155 | 156 | return True # 保留其他日志 157 | 158 | def setup_logger(): 159 | # 确保日志目录存在 160 | Config.LOGS_DIR.mkdir(parents=True, exist_ok=True) 161 | 162 | # 设置第三方库的日志级别 163 | logging.getLogger('werkzeug').setLevel(logging.WARNING) # 减少werkzeug的日志 164 | logging.getLogger('http.server').setLevel(logging.WARNING) # 减少HTTP服务器的日志 165 | 166 | # 创建logger实例 167 | logger = logging.getLogger('wxauto-api') 168 | logger.setLevel(Config.LOG_LEVEL) # 使用配置文件中的日志级别 169 | 170 | # 如果logger已经有处理器,先清除 171 | if logger.handlers: 172 | logger.handlers.clear() 173 | 174 | # 创建格式化器,使用统一的时间戳格式 175 | formatter = logging.Formatter(Config.LOG_FORMAT, Config.LOG_DATE_FORMAT) 176 | 177 | # 创建HTTP请求过滤器 178 | http_filter = HttpRequestFilter() 179 | 180 | # 添加内存日志处理器,用于捕获最近的错误日志 181 | memory_handler = MemoryLogHandler(capacity=100) 182 | memory_handler.setFormatter(formatter) 183 | memory_handler.setLevel(logging.DEBUG) # 捕获所有级别的日志,但只保存错误日志 184 | logger.addHandler(memory_handler) 185 | 186 | # 添加文件处理器 - 使用大小轮转的日志文件 187 | file_handler = RotatingFileHandler( 188 | Config.LOG_FILE, 189 | maxBytes=Config.LOG_MAX_BYTES, 190 | backupCount=Config.LOG_BACKUP_COUNT, 191 | encoding='utf-8' 192 | ) 193 | file_handler.setFormatter(formatter) 194 | file_handler.addFilter(http_filter) # 添加过滤器 195 | # 设置为INFO级别,减少日志量 196 | file_handler.setLevel(logging.INFO) 197 | logger.addHandler(file_handler) 198 | 199 | # 添加控制台处理器 200 | console_handler = logging.StreamHandler(sys.stdout) 201 | console_handler.setFormatter(formatter) 202 | console_handler.addFilter(http_filter) # 添加过滤器 203 | # 设置为INFO级别,减少日志量 204 | console_handler.setLevel(logging.INFO) 205 | logger.addHandler(console_handler) 206 | 207 | # 设置传播标志为False,避免日志重复 208 | logger.propagate = False 209 | 210 | return logger, memory_handler 211 | 212 | # 创建基础logger实例和内存日志处理器 213 | base_logger, memory_handler = setup_logger() 214 | 215 | # 使用适配器包装logger,添加当前使用的库信息 216 | logger = WeChatLibAdapter(base_logger, Config.WECHAT_LIB) 217 | 218 | # 导出内存日志处理器,供其他模块使用 219 | log_memory_handler = memory_handler -------------------------------------------------------------------------------- /app/plugin_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | 插件管理模块 3 | 用于管理wxauto和wxautox库的安装和卸载 4 | """ 5 | 6 | import os 7 | import sys 8 | import subprocess 9 | import tempfile 10 | import shutil 11 | import logging 12 | import importlib 13 | from pathlib import Path 14 | import config_manager 15 | 16 | # 配置日志 17 | logger = logging.getLogger(__name__) 18 | 19 | # 导入动态包管理器 20 | try: 21 | from dynamic_package_manager import get_package_manager 22 | package_manager = get_package_manager() 23 | logger.info("成功导入动态包管理器") 24 | except ImportError as e: 25 | logger.warning(f"导入动态包管理器失败: {str(e)}") 26 | package_manager = None 27 | 28 | def check_wxauto_status(): 29 | """ 30 | 检查wxauto库的安装状态 31 | 32 | Returns: 33 | bool: 是否已安装 34 | """ 35 | try: 36 | # 确保本地wxauto文件夹在Python路径中 37 | wxauto_path = os.path.join(os.getcwd(), "wxauto") 38 | if wxauto_path not in sys.path: 39 | sys.path.insert(0, wxauto_path) 40 | 41 | # 尝试导入 42 | import wxauto 43 | logger.info(f"成功从本地文件夹导入wxauto: {wxauto_path}") 44 | return True 45 | except ImportError as e: 46 | logger.warning(f"无法导入wxauto库: {str(e)}") 47 | return False 48 | 49 | def check_wxautox_status(): 50 | """ 51 | 检查wxautox库的安装状态 52 | 53 | Returns: 54 | bool: 是否已安装 55 | """ 56 | # 首先尝试使用动态包管理器检查 57 | if package_manager: 58 | #logger.info("使用动态包管理器检查wxautox状态") 59 | is_installed = package_manager.is_package_installed("wxautox") 60 | if is_installed: 61 | #logger.info("动态包管理器报告wxautox已安装") 62 | return True 63 | 64 | # 如果动态包管理器不可用或报告未安装,尝试直接导入 65 | try: 66 | import wxautox 67 | logger.info("wxautox库已安装") 68 | return True 69 | except ImportError: 70 | logger.warning("wxautox库未安装") 71 | return False 72 | 73 | def install_wxautox(wheel_file_path): 74 | """ 75 | 安装wxautox库 76 | 77 | Args: 78 | wheel_file_path (str): wheel文件路径 79 | 80 | Returns: 81 | tuple: (成功状态, 消息) 82 | """ 83 | logger.info(f"开始安装wxautox: {wheel_file_path}") 84 | 85 | # 验证文件是否存在 86 | if not os.path.exists(wheel_file_path): 87 | return False, f"文件不存在: {wheel_file_path}" 88 | 89 | # 验证文件是否是wheel文件 90 | if not wheel_file_path.endswith('.whl'): 91 | return False, "文件不是有效的wheel文件" 92 | 93 | # 验证文件名是否包含wxautox 94 | if 'wxautox-' not in os.path.basename(wheel_file_path): 95 | return False, "文件不是wxautox wheel文件" 96 | 97 | # 优先使用动态包管理器安装 98 | if package_manager: 99 | logger.info("使用动态包管理器安装wxautox") 100 | try: 101 | module = package_manager.install_and_import(wheel_file_path, "wxautox") 102 | if module: 103 | #logger.info("动态包管理器成功安装并导入wxautox") 104 | 105 | # 更新配置文件 106 | update_config_for_wxautox() 107 | 108 | return True, "wxautox库安装成功" 109 | else: 110 | logger.error("动态包管理器安装wxautox失败") 111 | return False, "动态包管理器安装wxautox失败" 112 | except Exception as e: 113 | logger.error(f"动态包管理器安装wxautox出错: {str(e)}") 114 | # 如果动态包管理器失败,继续尝试传统方法 115 | 116 | # 如果动态包管理器不可用或失败,使用传统方法 117 | try: 118 | # 使用pip安装wheel文件 119 | result = subprocess.run( 120 | [sys.executable, "-m", "pip", "install", wheel_file_path], 121 | capture_output=True, 122 | text=True, 123 | check=True 124 | ) 125 | 126 | # 检查安装结果 127 | if result.returncode == 0: 128 | logger.info("wxautox库安装成功") 129 | 130 | # 尝试导入验证 131 | try: 132 | import wxautox 133 | importlib.reload(wxautox) # 重新加载模块,确保使用最新版本 134 | logger.info("wxautox库导入验证成功") 135 | 136 | # 更新配置文件 137 | update_config_for_wxautox() 138 | 139 | return True, "wxautox库安装成功" 140 | except ImportError as e: 141 | logger.error(f"wxautox库安装后导入失败: {str(e)}") 142 | return False, f"wxautox库安装后导入失败: {str(e)}" 143 | else: 144 | logger.error(f"wxautox库安装失败: {result.stderr}") 145 | return False, f"wxautox库安装失败: {result.stderr}" 146 | except subprocess.CalledProcessError as e: 147 | logger.error(f"wxautox库安装过程出错: {e.stderr}") 148 | return False, f"wxautox库安装过程出错: {e.stderr}" 149 | except Exception as e: 150 | logger.error(f"wxautox库安装过程出现未知错误: {str(e)}") 151 | return False, f"wxautox库安装过程出现未知错误: {str(e)}" 152 | 153 | def update_config_for_wxautox(): 154 | """ 155 | 更新配置文件,设置使用wxautox库 156 | """ 157 | try: 158 | # 加载当前配置 159 | config = config_manager.load_app_config() 160 | 161 | # 更新库配置 162 | config['wechat_lib'] = 'wxautox' 163 | 164 | # 保存配置 165 | config_manager.save_app_config(config) 166 | 167 | logger.info("已更新配置文件,设置使用wxautox库") 168 | except Exception as e: 169 | logger.error(f"更新配置文件失败: {str(e)}") 170 | 171 | def get_plugins_status(): 172 | """ 173 | 获取插件状态 174 | 175 | Returns: 176 | dict: 插件状态信息 177 | """ 178 | wxauto_status = check_wxauto_status() 179 | wxautox_status = check_wxautox_status() 180 | 181 | return { 182 | 'wxauto': { 183 | 'installed': wxauto_status, 184 | 'path': os.path.join(os.getcwd(), "wxauto") if wxauto_status else None 185 | }, 186 | 'wxautox': { 187 | 'installed': wxautox_status, 188 | 'version': get_wxautox_version() if wxautox_status else None 189 | } 190 | } 191 | 192 | def get_wxautox_version(): 193 | """ 194 | 获取wxautox版本号 195 | 196 | Returns: 197 | str: 版本号,如果未安装则返回None 198 | """ 199 | # 首先尝试使用动态包管理器 200 | if package_manager: 201 | logger.info("使用动态包管理器获取wxautox版本") 202 | module = package_manager.import_package("wxautox") 203 | if module: 204 | version = getattr(module, 'VERSION', '未知版本') 205 | logger.info(f"动态包管理器获取到wxautox版本: {version}") 206 | return version 207 | 208 | # 如果动态包管理器不可用或失败,尝试直接导入 209 | try: 210 | import wxautox 211 | version = getattr(wxautox, 'VERSION', '未知版本') 212 | logger.info(f"直接导入获取到wxautox版本: {version}") 213 | return version 214 | except ImportError: 215 | logger.warning("无法导入wxautox,无法获取版本号") 216 | return None 217 | -------------------------------------------------------------------------------- /app/run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | import os 4 | import subprocess 5 | import atexit 6 | import signal 7 | import logging 8 | import argparse 9 | from app import create_app 10 | from app.logs import logger 11 | from app.config import Config 12 | from app.api_queue import start_queue_processors, stop_queue_processors 13 | 14 | # 导入互斥锁模块 15 | try: 16 | import app_mutex 17 | except ImportError: 18 | logger.warning("无法导入互斥锁模块,跳过互斥锁检查") 19 | 20 | # 配置 Werkzeug 日志 21 | werkzeug_logger = logging.getLogger('werkzeug') 22 | werkzeug_logger.setLevel(logging.ERROR) # 只显示错误级别的日志 23 | 24 | # 自定义 Werkzeug 日志格式处理器 25 | class WerkzeugLogFilter(logging.Filter): 26 | def filter(self, record): 27 | # 移除 Werkzeug 日志中的时间戳 28 | if hasattr(record, 'msg') and isinstance(record.msg, str): 29 | # 移除类似 "127.0.0.1 - - [08/May/2025 12:04:46]" 这样的时间戳 30 | if '] "' in record.msg and ' - - [' in record.msg: 31 | parts = record.msg.split('] "', 1) 32 | if len(parts) > 1: 33 | ip_part = parts[0].split(' - - [')[0] 34 | request_part = parts[1] 35 | record.msg = f"{ip_part} - {request_part}" 36 | return True 37 | 38 | # 添加过滤器到 Werkzeug 日志处理器 39 | for handler in werkzeug_logger.handlers: 40 | handler.addFilter(WerkzeugLogFilter()) 41 | 42 | def check_dependencies(): 43 | """检查依赖是否已安装""" 44 | # 获取配置的微信库 45 | wechat_lib = Config.WECHAT_LIB 46 | logger.info(f"配置的微信库: {wechat_lib}") 47 | 48 | # 检查wxauto库 49 | if wechat_lib == 'wxauto': 50 | try: 51 | # 确保本地wxauto文件夹在Python路径中 52 | wxauto_path = os.path.join(os.getcwd(), "wxauto") 53 | if wxauto_path not in sys.path: 54 | sys.path.insert(0, wxauto_path) 55 | 56 | # 尝试直接从本地文件夹导入wxauto 57 | import wxauto 58 | logger.info(f"成功从本地文件夹导入wxauto: {wxauto_path}") 59 | except ImportError as e: 60 | logger.error(f"无法从本地文件夹导入wxauto: {str(e)}") 61 | logger.error("请确保wxauto文件夹存在且包含正确的wxauto模块") 62 | sys.exit(1) 63 | 64 | # 检查wxautox库 65 | elif wechat_lib == 'wxautox': 66 | try: 67 | # 尝试导入wxautox 68 | import wxautox 69 | logger.info("wxautox库已安装") 70 | except ImportError: 71 | logger.error("wxautox库未安装,但配置要求使用wxautox") 72 | logger.error("请手动安装wxautox wheel文件,或者修改配置使用wxauto库") 73 | logger.error("如需使用wxauto库,请在.env文件中设置 WECHAT_LIB=wxauto") 74 | sys.exit(1) 75 | 76 | # 不支持的库 77 | else: 78 | logger.error(f"不支持的微信库: {wechat_lib}") 79 | logger.error("请在.env文件中设置 WECHAT_LIB=wxauto 或 WECHAT_LIB=wxautox") 80 | sys.exit(1) 81 | 82 | # 退出时清理资源 83 | def cleanup(): 84 | """退出时清理资源""" 85 | logger.info("正在停止队列处理器...") 86 | stop_queue_processors() 87 | logger.info("资源清理完成") 88 | 89 | # 注册退出处理函数 90 | atexit.register(cleanup) 91 | 92 | # 注册信号处理 93 | def signal_handler(sig, frame): 94 | """信号处理函数""" 95 | logger.info(f"接收到信号 {sig},正在退出...") 96 | cleanup() 97 | sys.exit(0) 98 | 99 | # 注册SIGINT和SIGTERM信号处理 100 | signal.signal(signal.SIGINT, signal_handler) 101 | signal.signal(signal.SIGTERM, signal_handler) 102 | 103 | try: 104 | # 记录启动环境信息 105 | logger.info(f"Python版本: {sys.version}") 106 | logger.info(f"当前工作目录: {os.getcwd()}") 107 | logger.info(f"Python路径: {sys.path}") 108 | logger.info(f"是否在PyInstaller环境中运行: {getattr(sys, 'frozen', False)}") 109 | logger.info(f"进程类型: API服务") 110 | 111 | # 解析命令行参数 112 | parser = argparse.ArgumentParser(description="启动wxauto_http_api API服务") 113 | parser.add_argument("--no-mutex-check", action="store_true", help="禁用互斥锁检查") 114 | parser.add_argument("--debug", action="store_true", help="启用调试模式") 115 | args = parser.parse_args() 116 | 117 | # 如果启用调试模式,设置更详细的日志级别 118 | if args.debug: 119 | logger.setLevel(logging.DEBUG) 120 | logger.info("已启用调试模式") 121 | 122 | # 检查互斥锁 123 | if not args.no_mutex_check and 'app_mutex' in globals(): 124 | try: 125 | # 获取端口号 126 | port = Config.PORT 127 | logger.info(f"API服务端口: {port}") 128 | 129 | # 创建API服务互斥锁 130 | api_mutex = app_mutex.create_api_mutex(port) 131 | 132 | # 尝试获取API服务互斥锁 133 | if not api_mutex.acquire(): 134 | logger.warning(f"端口 {port} 已被占用,API服务可能已在运行") 135 | sys.exit(0) 136 | 137 | logger.info(f"成功获取API服务互斥锁,端口: {port}") 138 | except Exception as e: 139 | logger.error(f"互斥锁检查失败: {str(e)}") 140 | logger.error(traceback.format_exc()) 141 | # 继续执行,不要因为互斥锁检查失败而退出 142 | 143 | # 检查依赖 144 | try: 145 | check_dependencies() 146 | except Exception as e: 147 | logger.error(f"依赖检查失败: {str(e)}") 148 | logger.error(traceback.format_exc()) 149 | sys.exit(2) # 返回码2表示依赖检查失败 150 | 151 | # 启动队列处理器 152 | try: 153 | start_queue_processors() 154 | logger.info("队列处理器已启动") 155 | except Exception as e: 156 | logger.error(f"启动队列处理器失败: {str(e)}") 157 | logger.error(traceback.format_exc()) 158 | sys.exit(3) # 返回码3表示队列处理器启动失败 159 | 160 | # 创建应用 161 | try: 162 | app = create_app() 163 | logger.info("正在启动Flask应用...") 164 | except Exception as e: 165 | logger.error(f"创建Flask应用失败: {str(e)}") 166 | logger.error(traceback.format_exc()) 167 | sys.exit(4) # 返回码4表示创建Flask应用失败 168 | 169 | if __name__ == '__main__': 170 | logger.info(f"监听地址: {app.config['HOST']}:{app.config['PORT']}") 171 | # 禁用 werkzeug 的重新加载器,避免可能的端口冲突 172 | app.run( 173 | host=app.config['HOST'], 174 | port=app.config['PORT'], 175 | debug=app.config['DEBUG'], 176 | use_reloader=False, 177 | threaded=True 178 | ) 179 | except Exception as e: 180 | logger.error(f"启动失败: {str(e)}") 181 | traceback.print_exc() 182 | # 确保清理资源 183 | cleanup() 184 | sys.exit(1) -------------------------------------------------------------------------------- /app/start_api.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在启动wxauto_http_api API服务... 3 | python main.py --service api 4 | pause 5 | -------------------------------------------------------------------------------- /app/start_api_packaged.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在启动wxauto_http_api API服务... 3 | "%~dp0wxauto_http_api.exe" --service api --debug 4 | pause 5 | -------------------------------------------------------------------------------- /app/start_ui.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在启动wxauto_http_api管理界面... 3 | python main.py --service ui 4 | pause 5 | -------------------------------------------------------------------------------- /app/start_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | 启动UI的辅助脚本 3 | 确保所有模块都能被正确导入 4 | """ 5 | 6 | import os 7 | import sys 8 | import subprocess 9 | import traceback 10 | import logging 11 | import argparse 12 | 13 | # 配置基本的控制台日志记录 14 | logging.basicConfig( 15 | level=logging.DEBUG, 16 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 17 | handlers=[ 18 | logging.StreamHandler(), 19 | logging.FileHandler('startup_log.txt', 'w', 'utf-8') 20 | ] 21 | ) 22 | logger = logging.getLogger(__name__) 23 | 24 | # 记录启动信息 25 | logger.info("启动UI脚本开始执行") 26 | logger.info(f"Python版本: {sys.version}") 27 | logger.info(f"当前工作目录: {os.getcwd()}") 28 | logger.info(f"Python路径: {sys.path}") 29 | logger.info(f"是否在PyInstaller环境中运行: {getattr(sys, 'frozen', False)}") 30 | 31 | # 导入路径修复模块 32 | try: 33 | logger.info("尝试导入路径修复模块") 34 | import fix_path 35 | app_root = fix_path.fix_paths() 36 | logger.info(f"路径修复完成,应用根目录: {app_root}") 37 | except ImportError as e: 38 | logger.error(f"导入路径修复模块失败: {str(e)}") 39 | logger.error(traceback.format_exc()) 40 | except Exception as e: 41 | logger.error(f"路径修复时出错: {str(e)}") 42 | logger.error(traceback.format_exc()) 43 | 44 | # 导入Unicode编码修复模块 45 | try: 46 | logger.info("尝试导入Unicode编码修复模块") 47 | import app.unicode_fix 48 | logger.info("成功导入Unicode编码修复模块") 49 | except ImportError as e: 50 | logger.warning(f"导入Unicode编码修复模块失败: {str(e)}") 51 | logger.warning("这可能会导致在处理包含Unicode表情符号的微信名称时出现问题") 52 | except Exception as e: 53 | logger.error(f"应用Unicode编码修复时出错: {str(e)}") 54 | logger.error(traceback.format_exc()) 55 | 56 | def main(): 57 | try: 58 | # 获取当前脚本的绝对路径 59 | current_dir = os.path.dirname(os.path.abspath(__file__)) 60 | logger.info(f"当前脚本目录: {current_dir}") 61 | 62 | # 确保当前目录在Python路径中 63 | if current_dir not in sys.path: 64 | sys.path.insert(0, current_dir) 65 | logger.info(f"已添加当前目录到Python路径: {current_dir}") 66 | 67 | # 记录当前工作目录和Python路径 68 | logger.info(f"当前工作目录: {os.getcwd()}") 69 | logger.info(f"Python路径: {sys.path}") 70 | 71 | # 尝试导入关键模块,验证路径设置是否正确 72 | try: 73 | logger.info("尝试导入config_manager模块") 74 | import config_manager 75 | logger.info("成功导入 config_manager 模块") 76 | 77 | # 确保目录存在 78 | logger.info("确保必要目录存在") 79 | config_manager.ensure_dirs() 80 | logger.info("已确保所有必要目录存在") 81 | 82 | # 尝试导入app模块 83 | logger.info("尝试导入app.config模块") 84 | from app.config import Config 85 | logger.info("成功导入 app.config 模块") 86 | 87 | logger.info("尝试导入app.logs模块") 88 | from app.logs import logger as app_logger 89 | logger.info("成功导入 app.logs 模块") 90 | 91 | # 启动UI 92 | logger.info("正在启动UI...") 93 | import app_ui 94 | app_ui.main() 95 | 96 | except ImportError as e: 97 | logger.error(f"导入模块失败: {str(e)}") 98 | logger.error(traceback.format_exc()) 99 | print(f"导入模块失败: {e}") 100 | print("请确保在正确的目录中运行此脚本") 101 | # 创建一个错误对话框 102 | try: 103 | import tkinter as tk 104 | from tkinter import messagebox 105 | root = tk.Tk() 106 | root.withdraw() 107 | messagebox.showerror("启动错误", f"导入模块失败: {e}\n\n请确保在正确的目录中运行此脚本") 108 | except: 109 | pass 110 | sys.exit(1) 111 | except Exception as e: 112 | logger.error(f"启动UI时出错: {str(e)}") 113 | logger.error(traceback.format_exc()) 114 | print(f"启动UI时出错: {e}") 115 | # 创建一个错误对话框 116 | try: 117 | import tkinter as tk 118 | from tkinter import messagebox 119 | root = tk.Tk() 120 | root.withdraw() 121 | messagebox.showerror("启动错误", f"启动UI时出错: {e}") 122 | except: 123 | pass 124 | sys.exit(1) 125 | 126 | if __name__ == "__main__": 127 | # 解析命令行参数 128 | parser = argparse.ArgumentParser(description="启动wxauto_http_api管理界面") 129 | parser.add_argument("--no-mutex-check", action="store_true", help="禁用互斥锁检查") 130 | parser.add_argument("--no-auto-start", action="store_true", help="禁用自动启动API服务") 131 | args = parser.parse_args() 132 | 133 | # 检查互斥锁 134 | if not args.no_mutex_check: 135 | try: 136 | # 导入互斥锁模块 137 | import app_mutex 138 | 139 | # 尝试获取UI互斥锁 140 | if not app_mutex.ui_mutex.acquire(): 141 | logger.warning("另一个UI实例已在运行,将退出") 142 | print("另一个UI实例已在运行,请不要重复启动") 143 | try: 144 | import tkinter as tk 145 | from tkinter import messagebox 146 | root = tk.Tk() 147 | root.withdraw() 148 | messagebox.showwarning("警告", "另一个UI实例已在运行,请不要重复启动") 149 | except: 150 | pass 151 | sys.exit(0) 152 | 153 | logger.info("成功获取UI互斥锁") 154 | except ImportError: 155 | logger.warning("无法导入互斥锁模块,跳过互斥锁检查") 156 | except Exception as e: 157 | logger.error(f"互斥锁检查失败: {str(e)}") 158 | 159 | # 设置环境变量,用于控制自动启动 160 | if args.no_auto_start: 161 | os.environ["WXAUTO_NO_AUTO_START"] = "1" 162 | logger.info("已禁用自动启动API服务") 163 | 164 | # 启动主程序 165 | main() 166 | -------------------------------------------------------------------------------- /app/static/js/main.js: -------------------------------------------------------------------------------- 1 | // 全局变量 2 | let socket = null; 3 | let apiRequestCount = 0; 4 | let queueLength = 0; 5 | 6 | // 页面加载完成后执行 7 | $(document).ready(function() { 8 | // 初始化WebSocket连接 9 | initWebSocket(); 10 | 11 | // 初始化页面状态 12 | checkApiStatus(); 13 | checkPluginStatus(); 14 | getCurrentLib(); 15 | getWeChatStatus(); 16 | 17 | // 设置定时刷新 18 | setInterval(checkApiStatus, 10000); 19 | setInterval(getWeChatStatus, 5000); 20 | setInterval(getQueueStatus, 3000); 21 | 22 | // 绑定按钮事件 23 | bindButtonEvents(); 24 | }); 25 | 26 | // 初始化WebSocket连接 27 | function initWebSocket() { 28 | // 获取当前URL的协议和主机部分 29 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 30 | const host = window.location.host; 31 | 32 | // 创建WebSocket连接 33 | socket = new WebSocket(`${protocol}//${host}/ws/logs`); 34 | 35 | // 连接打开时的处理 36 | socket.onopen = function(event) { 37 | addLogEntry('WebSocket连接已建立', 'success'); 38 | }; 39 | 40 | // 接收到消息时的处理 41 | socket.onmessage = function(event) { 42 | const logData = JSON.parse(event.data); 43 | addLogEntry(logData.message, logData.level); 44 | }; 45 | 46 | // 连接关闭时的处理 47 | socket.onclose = function(event) { 48 | addLogEntry('WebSocket连接已关闭,5秒后尝试重连...', 'warning'); 49 | setTimeout(initWebSocket, 5000); 50 | }; 51 | 52 | // 连接错误时的处理 53 | socket.onerror = function(error) { 54 | addLogEntry('WebSocket连接错误', 'error'); 55 | }; 56 | } 57 | 58 | // 添加日志条目 59 | function addLogEntry(message, level) { 60 | const logContainer = $('#log-container'); 61 | const timestamp = new Date().toLocaleTimeString(); 62 | const logClass = level ? `log-${level}` : ''; 63 | 64 | // 创建日志条目 65 | const logEntry = $('
') 66 | .addClass('log-entry') 67 | .addClass(logClass) 68 | .text(`[${timestamp}] ${message}`); 69 | 70 | // 添加到日志容器 71 | logContainer.append(logEntry); 72 | 73 | // 滚动到底部 74 | logContainer.scrollTop(logContainer[0].scrollHeight); 75 | 76 | // 如果日志条目过多,删除旧的 77 | const maxLogEntries = 1000; 78 | const logEntries = $('.log-entry'); 79 | if (logEntries.length > maxLogEntries) { 80 | logEntries.slice(0, logEntries.length - maxLogEntries).remove(); 81 | } 82 | } 83 | 84 | // 检查API状态 85 | function checkApiStatus() { 86 | $.ajax({ 87 | url: '/admin/api/status', 88 | method: 'GET', 89 | success: function(response) { 90 | if (response.status === 'online') { 91 | $('#api-status').removeClass('status-offline status-warning').addClass('status-online'); 92 | $('#api-status-text').text('在线'); 93 | } else { 94 | $('#api-status').removeClass('status-online status-warning').addClass('status-offline'); 95 | $('#api-status-text').text('离线'); 96 | } 97 | }, 98 | error: function() { 99 | $('#api-status').removeClass('status-online status-warning').addClass('status-offline'); 100 | $('#api-status-text').text('离线'); 101 | } 102 | }); 103 | } 104 | 105 | // 检查插件状态 106 | function checkPluginStatus() { 107 | $.ajax({ 108 | url: '/admin/plugins/status', 109 | method: 'GET', 110 | success: function(response) { 111 | // 更新wxauto状态 112 | if (response.wxauto.installed) { 113 | $('#wxauto-status').removeClass('bg-danger bg-warning').addClass('bg-success').text('已安装'); 114 | $('#wxauto-progress').css('width', '100%'); 115 | } else { 116 | $('#wxauto-status').removeClass('bg-success bg-warning').addClass('bg-danger').text('未安装'); 117 | $('#wxauto-progress').css('width', '0%'); 118 | } 119 | 120 | // 更新wxautox状态 121 | if (response.wxautox.installed) { 122 | $('#wxautox-status').removeClass('bg-danger bg-warning').addClass('bg-success').text('已安装'); 123 | $('#wxautox-progress').css('width', '100%'); 124 | } else { 125 | $('#wxautox-status').removeClass('bg-success bg-warning').addClass('bg-danger').text('未安装'); 126 | $('#wxautox-progress').css('width', '0%'); 127 | } 128 | }, 129 | error: function() { 130 | $('#wxauto-status').removeClass('bg-success bg-warning').addClass('bg-danger').text('检查失败'); 131 | $('#wxautox-status').removeClass('bg-success bg-warning').addClass('bg-danger').text('检查失败'); 132 | } 133 | }); 134 | } 135 | 136 | // 获取当前使用的库 137 | function getCurrentLib() { 138 | $.ajax({ 139 | url: '/admin/config/current-lib', 140 | method: 'GET', 141 | success: function(response) { 142 | $('#current-lib').val(response.lib); 143 | 144 | // 设置单选按钮状态 145 | if (response.lib === 'wxauto') { 146 | $('#wxauto-select').prop('checked', true); 147 | } else if (response.lib === 'wxautox') { 148 | $('#wxautox-select').prop('checked', true); 149 | } 150 | } 151 | }); 152 | } 153 | 154 | // 获取微信状态 155 | function getWeChatStatus() { 156 | $.ajax({ 157 | url: '/admin/wechat/status', 158 | method: 'GET', 159 | success: function(response) { 160 | $('#wechat-status').val(response.status); 161 | }, 162 | error: function() { 163 | $('#wechat-status').val('未连接'); 164 | } 165 | }); 166 | } 167 | 168 | // 获取队列状态 169 | function getQueueStatus() { 170 | $.ajax({ 171 | url: '/admin/queue/status', 172 | method: 'GET', 173 | success: function(response) { 174 | apiRequestCount = response.total_requests; 175 | queueLength = response.queue_length; 176 | 177 | $('#api-requests').val(apiRequestCount); 178 | $('#queue-length').val(queueLength); 179 | } 180 | }); 181 | } 182 | 183 | // 绑定按钮事件 184 | function bindButtonEvents() { 185 | // 安装/修复wxauto 186 | $('#install-wxauto').click(function() { 187 | $(this).prop('disabled', true).html(' 安装中...'); 188 | 189 | $.ajax({ 190 | url: '/admin/plugins/install-wxauto', 191 | method: 'POST', 192 | success: function(response) { 193 | addLogEntry(response.message, 'success'); 194 | checkPluginStatus(); 195 | }, 196 | error: function(xhr) { 197 | addLogEntry('安装wxauto失败: ' + xhr.responseJSON.message, 'error'); 198 | }, 199 | complete: function() { 200 | $('#install-wxauto').prop('disabled', false).html(' 安装/修复 wxauto'); 201 | } 202 | }); 203 | }); 204 | 205 | // 上传wxautox按钮点击 206 | $('#upload-wxautox').click(function() { 207 | $('#wxautox-file').click(); 208 | }); 209 | 210 | // 文件选择改变 211 | $('#wxautox-file').change(function() { 212 | if (this.files.length > 0) { 213 | uploadWxautox(this.files[0]); 214 | } 215 | }); 216 | 217 | // 应用库选择 218 | $('#apply-lib-select').click(function() { 219 | const selectedLib = $('input[name="lib-select"]:checked').val(); 220 | if (!selectedLib) { 221 | addLogEntry('请先选择一个库', 'warning'); 222 | return; 223 | } 224 | 225 | $(this).prop('disabled', true).html(' 应用中...'); 226 | 227 | $.ajax({ 228 | url: '/admin/config/set-lib', 229 | method: 'POST', 230 | data: JSON.stringify({ lib: selectedLib }), 231 | contentType: 'application/json', 232 | success: function(response) { 233 | addLogEntry(response.message, 'success'); 234 | getCurrentLib(); 235 | }, 236 | error: function(xhr) { 237 | addLogEntry('设置库失败: ' + xhr.responseJSON.message, 'error'); 238 | }, 239 | complete: function() { 240 | $('#apply-lib-select').prop('disabled', false).html(' 应用选择'); 241 | } 242 | }); 243 | }); 244 | 245 | // 启动服务 246 | $('#start-service').click(function() { 247 | controlService('start'); 248 | }); 249 | 250 | // 停止服务 251 | $('#stop-service').click(function() { 252 | controlService('stop'); 253 | }); 254 | 255 | // 重启服务 256 | $('#restart-service').click(function() { 257 | controlService('restart'); 258 | }); 259 | 260 | // 重载配置 261 | $('#reload-config').click(function() { 262 | $(this).prop('disabled', true).html(' 重载中...'); 263 | 264 | $.ajax({ 265 | url: '/admin/config/reload', 266 | method: 'POST', 267 | success: function(response) { 268 | addLogEntry(response.message, 'success'); 269 | getCurrentLib(); 270 | checkPluginStatus(); 271 | }, 272 | error: function(xhr) { 273 | addLogEntry('重载配置失败: ' + xhr.responseJSON.message, 'error'); 274 | }, 275 | complete: function() { 276 | $('#reload-config').prop('disabled', false).html(' 重载配置'); 277 | } 278 | }); 279 | }); 280 | 281 | // 清空日志 282 | $('#clear-logs').click(function() { 283 | $('#log-container').empty(); 284 | addLogEntry('日志已清空', 'info'); 285 | }); 286 | } 287 | 288 | // 上传wxautox文件 289 | function uploadWxautox(file) { 290 | const formData = new FormData(); 291 | formData.append('file', file); 292 | 293 | $('#upload-wxautox').prop('disabled', true).html(' 上传中...'); 294 | 295 | $.ajax({ 296 | url: '/admin/plugins/upload-wxautox', 297 | method: 'POST', 298 | data: formData, 299 | processData: false, 300 | contentType: false, 301 | success: function(response) { 302 | addLogEntry(response.message, 'success'); 303 | checkPluginStatus(); 304 | }, 305 | error: function(xhr) { 306 | addLogEntry('上传wxautox失败: ' + xhr.responseJSON.message, 'error'); 307 | }, 308 | complete: function() { 309 | $('#upload-wxautox').prop('disabled', false).html(' 上传并安装 wxautox'); 310 | $('#wxautox-file').val(''); 311 | } 312 | }); 313 | } 314 | 315 | // 控制服务 316 | function controlService(action) { 317 | const buttonMap = { 318 | 'start': $('#start-service'), 319 | 'stop': $('#stop-service'), 320 | 'restart': $('#restart-service') 321 | }; 322 | 323 | const textMap = { 324 | 'start': '启动中...', 325 | 'stop': '停止中...', 326 | 'restart': '重启中...' 327 | }; 328 | 329 | const iconMap = { 330 | 'start': ' 启动服务', 331 | 'stop': ' 停止服务', 332 | 'restart': ' 重启服务' 333 | }; 334 | 335 | // 禁用所有按钮 336 | Object.values(buttonMap).forEach(btn => btn.prop('disabled', true)); 337 | buttonMap[action].html(` ${textMap[action]}`); 338 | 339 | $.ajax({ 340 | url: `/admin/service/${action}`, 341 | method: 'POST', 342 | success: function(response) { 343 | addLogEntry(response.message, 'success'); 344 | checkApiStatus(); 345 | }, 346 | error: function(xhr) { 347 | addLogEntry(`${action}服务失败: ` + xhr.responseJSON.message, 'error'); 348 | }, 349 | complete: function() { 350 | // 恢复所有按钮 351 | Object.entries(buttonMap).forEach(([key, btn]) => { 352 | btn.prop('disabled', false).html(iconMap[key]); 353 | }); 354 | } 355 | }); 356 | } 357 | -------------------------------------------------------------------------------- /app/system_monitor.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | def get_system_resources(): 4 | """ 5 | 获取系统CPU和内存使用情况 6 | 7 | Returns: 8 | dict: 包含CPU和内存使用情况的字典 9 | """ 10 | # 获取CPU信息 11 | cpu_percent = psutil.cpu_percent(interval=1) 12 | cpu_count = psutil.cpu_count() 13 | 14 | # 获取内存信息 15 | memory = psutil.virtual_memory() 16 | 17 | return { 18 | 'cpu': { 19 | 'usage_percent': cpu_percent, 20 | 'core_count': cpu_count 21 | }, 22 | 'memory': { 23 | 'total': int(memory.total / (1024 * 1024)), # 转换为MB 24 | 'used': int(memory.used / (1024 * 1024)), # 转换为MB 25 | 'free': int(memory.available / (1024 * 1024)), # 转换为MB 26 | 'usage_percent': memory.percent 27 | } 28 | } -------------------------------------------------------------------------------- /app/ui_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | UI服务逻辑 3 | 专门用于启动和管理UI服务 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | import traceback 10 | 11 | # 配置日志 12 | logger = logging.getLogger(__name__) 13 | 14 | def check_mutex(): 15 | """检查互斥锁,确保同一时间只有一个UI实例在运行""" 16 | # 如果禁用互斥锁检查,则跳过 17 | if os.environ.get("WXAUTO_NO_MUTEX_CHECK") == "1": 18 | logger.info("已禁用互斥锁检查,跳过") 19 | return True 20 | 21 | try: 22 | # 导入互斥锁模块 23 | try: 24 | # 首先尝试从app包导入 25 | from app import app_mutex 26 | logger.info("成功从app包导入 app_mutex 模块") 27 | except ImportError: 28 | # 如果失败,尝试直接导入(兼容旧版本) 29 | import app_mutex 30 | logger.info("成功直接导入 app_mutex 模块") 31 | 32 | # 尝试获取UI互斥锁 33 | if not app_mutex.ui_mutex.acquire(): 34 | logger.warning("另一个UI实例已在运行,将退出") 35 | print("另一个UI实例已在运行,请不要重复启动") 36 | try: 37 | import tkinter as tk 38 | from tkinter import messagebox 39 | root = tk.Tk() 40 | root.withdraw() 41 | messagebox.showwarning("警告", "另一个UI实例已在运行,请不要重复启动") 42 | except: 43 | pass 44 | return False 45 | 46 | logger.info("成功获取UI互斥锁") 47 | return True 48 | except ImportError: 49 | logger.warning("无法导入互斥锁模块,跳过互斥锁检查") 50 | return True 51 | except Exception as e: 52 | logger.error(f"互斥锁检查失败: {str(e)}") 53 | logger.error(traceback.format_exc()) 54 | return True 55 | 56 | def check_dependencies(): 57 | """检查依赖项""" 58 | try: 59 | # 导入config_manager模块 60 | try: 61 | # 首先尝试从app包导入 62 | from app import config_manager 63 | logger.info("成功从app包导入 config_manager 模块") 64 | except ImportError: 65 | # 如果失败,尝试直接导入(兼容旧版本) 66 | import config_manager 67 | logger.info("成功直接导入 config_manager 模块") 68 | 69 | # 确保目录存在 70 | config_manager.ensure_dirs() 71 | logger.info("已确保所有必要目录存在") 72 | return True 73 | except ImportError as e: 74 | logger.error(f"导入config_manager模块失败: {str(e)}") 75 | logger.error(traceback.format_exc()) 76 | return False 77 | except Exception as e: 78 | logger.error(f"检查依赖项时出错: {str(e)}") 79 | logger.error(traceback.format_exc()) 80 | return False 81 | 82 | def start_ui(): 83 | """启动UI服务""" 84 | # 检查互斥锁 85 | if not check_mutex(): 86 | sys.exit(0) 87 | 88 | # 检查依赖项 89 | if not check_dependencies(): 90 | logger.error("依赖项检查失败,无法启动UI服务") 91 | sys.exit(1) 92 | 93 | # 导入app_ui模块 94 | try: 95 | try: 96 | # 首先尝试从app包导入 97 | from app import app_ui 98 | logger.info("成功从app包导入 app_ui 模块") 99 | except ImportError: 100 | # 如果失败,尝试直接导入(兼容旧版本) 101 | import app_ui 102 | logger.info("成功直接导入 app_ui 模块") 103 | 104 | # 启动UI 105 | logger.info("正在启动UI...") 106 | app_ui.main() 107 | except ImportError as e: 108 | logger.error(f"导入app_ui模块失败: {str(e)}") 109 | logger.error(traceback.format_exc()) 110 | sys.exit(1) 111 | except Exception as e: 112 | logger.error(f"启动UI时出错: {str(e)}") 113 | logger.error(traceback.format_exc()) 114 | sys.exit(1) 115 | 116 | if __name__ == "__main__": 117 | # 设置环境变量,标记为UI服务进程 118 | os.environ["WXAUTO_SERVICE_TYPE"] = "ui" 119 | 120 | # 启动UI服务 121 | start_ui() 122 | -------------------------------------------------------------------------------- /app/unicode_fix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unicode编码修复模块 3 | 用于解决微信名称中包含Unicode表情符号导致的GBK编码错误问题 4 | """ 5 | 6 | import sys 7 | import logging 8 | import traceback 9 | 10 | # 获取logger 11 | logger = logging.getLogger(__name__) 12 | 13 | def patch_print_function(): 14 | """ 15 | 修补print函数,处理Unicode编码问题 16 | 当遇到GBK编码错误时,使用UTF-8编码输出 17 | """ 18 | try: 19 | # 保存原始的print函数 20 | original_print = print 21 | 22 | # 定义新的print函数 23 | def safe_print(*args, **kwargs): 24 | try: 25 | # 尝试使用原始print函数 26 | original_print(*args, **kwargs) 27 | except UnicodeEncodeError as e: 28 | # 如果是GBK编码错误,使用UTF-8编码输出 29 | if 'gbk' in str(e).lower(): 30 | # 将所有参数转换为字符串并连接 31 | message = " ".join(str(arg) for arg in args) 32 | # 使用sys.stdout.buffer直接写入UTF-8编码的字节 33 | try: 34 | if hasattr(sys.stdout, 'buffer'): 35 | sys.stdout.buffer.write(message.encode('utf-8')) 36 | sys.stdout.buffer.write(b'\n') 37 | sys.stdout.buffer.flush() 38 | else: 39 | # 如果没有buffer属性,尝试使用logger 40 | logger.info(message) 41 | except Exception as inner_e: 42 | # 如果还是失败,记录到日志 43 | logger.error(f"安全打印失败: {str(inner_e)}") 44 | else: 45 | # 如果不是GBK编码错误,重新抛出 46 | raise 47 | 48 | # 替换全局print函数 49 | import builtins 50 | builtins.print = safe_print 51 | 52 | #logger.info("成功修补print函数,解决Unicode编码问题") 53 | return True 54 | except Exception as e: 55 | logger.error(f"修补print函数失败: {str(e)}") 56 | logger.error(traceback.format_exc()) 57 | return False 58 | 59 | def patch_wechat_adapter(): 60 | """ 61 | 修补WeChatAdapter类,处理Unicode编码问题 62 | 在初始化微信实例时捕获并处理GBK编码错误 63 | """ 64 | try: 65 | # 尝试导入WeChatAdapter类 66 | from app.wechat_adapter import WeChatAdapter 67 | 68 | # 保存原始的initialize方法 69 | original_initialize = WeChatAdapter.initialize 70 | 71 | # 定义新的initialize方法 72 | def patched_initialize(self): 73 | """初始化微信实例,添加Unicode编码处理""" 74 | try: 75 | # 先修补print函数,确保后续操作不会因编码问题失败 76 | patch_print_function() 77 | 78 | # 调用原始的initialize方法 79 | return original_initialize(self) 80 | except UnicodeEncodeError as e: 81 | if 'gbk' in str(e).lower(): 82 | logger.warning(f"捕获到GBK编码错误: {str(e)}") 83 | logger.info("尝试修复Unicode编码问题...") 84 | 85 | # 修补print函数 86 | patch_print_function() 87 | 88 | # 再次尝试调用原始的initialize方法 89 | return original_initialize(self) 90 | else: 91 | # 如果不是GBK编码错误,重新抛出 92 | raise 93 | 94 | # 替换WeChatAdapter.initialize方法 95 | WeChatAdapter.initialize = patched_initialize 96 | 97 | logger.info("成功修补WeChatAdapter.initialize方法,解决Unicode编码问题") 98 | return True 99 | except ImportError: 100 | logger.warning("无法导入WeChatAdapter类,跳过修补") 101 | return False 102 | except Exception as e: 103 | logger.error(f"修补WeChatAdapter.initialize方法失败: {str(e)}") 104 | logger.error(traceback.format_exc()) 105 | return False 106 | 107 | def apply_patches(): 108 | """应用所有补丁""" 109 | logger.info("开始应用Unicode编码修复补丁...") 110 | 111 | # 修补print函数 112 | print_patched = patch_print_function() 113 | logger.info(f"print函数修补结果: {'成功' if print_patched else '失败'}") 114 | 115 | # 修补WeChatAdapter类 116 | adapter_patched = patch_wechat_adapter() 117 | logger.info(f"WeChatAdapter类修补结果: {'成功' if adapter_patched else '失败'}") 118 | 119 | return print_patched or adapter_patched # 只要有一个成功就返回True 120 | 121 | # 自动应用补丁 122 | patched = apply_patches() 123 | -------------------------------------------------------------------------------- /app/utils/image_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | 图片处理工具 3 | 用于处理wxauto保存图片时的路径问题 4 | """ 5 | 6 | import os 7 | import time 8 | import glob 9 | import logging 10 | from pathlib import Path 11 | 12 | # 配置日志 13 | logger = logging.getLogger(__name__) 14 | 15 | # 可能的图片保存位置 16 | POSSIBLE_SAVE_LOCATIONS = [ 17 | os.path.expanduser("~/Documents"), 18 | os.path.expanduser("~/Pictures"), 19 | os.path.expanduser("~/Downloads"), 20 | os.path.join(os.getcwd(), "wxauto文件"), 21 | os.path.join(os.getcwd(), "data", "api", "temp") 22 | ] 23 | 24 | def find_actual_image_path(expected_path, created_after=None, max_wait_seconds=3): 25 | """ 26 | 查找图片的实际保存路径 27 | 28 | Args: 29 | expected_path (str): wxauto返回的预期路径 30 | created_after (float, optional): 文件创建时间必须晚于此时间戳 31 | max_wait_seconds (int, optional): 最长等待时间(秒) 32 | 33 | Returns: 34 | str: 实际的文件路径,如果找不到则返回原始路径 35 | """ 36 | if not expected_path: 37 | logger.warning("预期路径为空") 38 | return expected_path 39 | 40 | # 如果文件存在于预期位置,直接返回 41 | if os.path.exists(expected_path) and os.path.getsize(expected_path) > 0: 42 | logger.debug(f"文件存在于预期位置: {expected_path}") 43 | return expected_path 44 | 45 | # 设置创建时间过滤器 46 | if created_after is None: 47 | created_after = time.time() - 60 # 默认查找最近60秒内创建的文件 48 | 49 | # 获取文件名 50 | file_name = os.path.basename(expected_path) 51 | 52 | # 等待一段时间,看文件是否会出现 53 | end_time = time.time() + max_wait_seconds 54 | while time.time() < end_time: 55 | # 检查预期位置 56 | if os.path.exists(expected_path) and os.path.getsize(expected_path) > 0: 57 | logger.debug(f"文件已出现在预期位置: {expected_path}") 58 | return expected_path 59 | 60 | # 检查其他可能的位置 61 | for location in POSSIBLE_SAVE_LOCATIONS: 62 | if not os.path.exists(location): 63 | continue 64 | 65 | # 精确匹配文件名 66 | exact_path = os.path.join(location, file_name) 67 | if os.path.exists(exact_path) and os.path.getsize(exact_path) > 0: 68 | file_time = os.path.getmtime(exact_path) 69 | if file_time >= created_after: 70 | logger.info(f"文件找到于替代位置: {exact_path}") 71 | return exact_path 72 | 73 | # 查找相似文件名(时间戳可能不完全匹配) 74 | pattern = os.path.join(location, "微信图片_*.jpg") 75 | for file_path in glob.glob(pattern): 76 | file_time = os.path.getmtime(file_path) 77 | if file_time >= created_after: 78 | logger.info(f"找到可能匹配的文件: {file_path}") 79 | return file_path 80 | 81 | # 短暂等待后重试 82 | time.sleep(0.5) 83 | 84 | # 如果找不到,记录警告并返回原始路径 85 | logger.warning(f"无法找到文件的实际位置,返回预期路径: {expected_path}") 86 | return expected_path 87 | 88 | def save_image_with_verification(wx_instance, msg_item): 89 | """ 90 | 保存图片并验证实际保存位置 91 | 92 | Args: 93 | wx_instance: wxauto的WeChat实例 94 | msg_item: 消息项控件 95 | 96 | Returns: 97 | str: 实际的文件路径 98 | """ 99 | # 记录开始时间 100 | start_time = time.time() 101 | 102 | try: 103 | # 调用原始的_download_pic方法 104 | expected_path = wx_instance._download_pic(msg_item) 105 | 106 | # 验证实际保存位置 107 | actual_path = find_actual_image_path(expected_path, created_after=start_time) 108 | 109 | return actual_path 110 | except Exception as e: 111 | logger.error(f"保存图片时出错: {str(e)}") 112 | return None 113 | 114 | def process_image_paths(messages): 115 | """ 116 | 处理消息中的图片路径,确保它们指向实际文件 117 | 118 | Args: 119 | messages (dict): wxauto返回的消息字典 120 | 121 | Returns: 122 | dict: 处理后的消息字典 123 | """ 124 | if not messages: 125 | return messages 126 | 127 | # 记录开始时间 128 | start_time = time.time() 129 | 130 | # 处理每个聊天的消息 131 | for chat_name, msg_list in messages.items(): 132 | for i, msg in enumerate(msg_list): 133 | # 检查是否为图片消息 134 | if hasattr(msg, 'content') and msg.content and isinstance(msg.content, str): 135 | if msg.content.startswith("[图片]"): 136 | # 这是一个未保存的图片消息 137 | logger.debug(f"跳过未保存的图片消息: {msg.content}") 138 | continue 139 | 140 | # 检查内容是否为图片路径 141 | if "微信图片_" in msg.content and (msg.content.endswith(".jpg") or msg.content.endswith(".png")): 142 | # 验证实际保存位置 143 | actual_path = find_actual_image_path(msg.content, created_after=start_time) 144 | 145 | # 更新消息内容 146 | if actual_path != msg.content: 147 | logger.info(f"更新图片路径: {msg.content} -> {actual_path}") 148 | msg.content = actual_path 149 | # 如果消息有info属性,也更新它 150 | if hasattr(msg, 'info') and isinstance(msg.info, list) and len(msg.info) > 1: 151 | msg.info[1] = actual_path 152 | 153 | return messages 154 | -------------------------------------------------------------------------------- /app/wechat.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import pythoncom 4 | from app.logs import logger 5 | from app.config import Config 6 | from app.wechat_adapter import wechat_adapter 7 | 8 | class WeChatManager: 9 | def __init__(self): 10 | self._instance = None 11 | self._lock = threading.Lock() 12 | self._last_check = 0 13 | self._check_interval = Config.WECHAT_CHECK_INTERVAL 14 | self._reconnect_delay = Config.WECHAT_RECONNECT_DELAY 15 | self._max_retry = Config.WECHAT_MAX_RETRY 16 | self._monitor_thread = None 17 | self._running = False 18 | self._retry_count = 0 19 | self._adapter = wechat_adapter 20 | 21 | def initialize(self): 22 | """初始化微信实例""" 23 | with self._lock: 24 | success = self._adapter.initialize() 25 | if success: 26 | self._instance = self._adapter.get_instance() 27 | if Config.WECHAT_AUTO_RECONNECT: 28 | self._start_monitor() 29 | self._retry_count = 0 30 | logger.info(f"微信初始化成功,使用库: {self._adapter.get_lib_name()}") 31 | return success 32 | 33 | def get_instance(self): 34 | """获取微信实例""" 35 | # 返回适配器而不是直接返回实例,这样可以使用适配器的参数处理功能 36 | return self._adapter 37 | 38 | def check_connection(self): 39 | """检查微信连接状态""" 40 | if not self._instance: 41 | return False 42 | 43 | try: 44 | result = self._adapter.check_connection() 45 | if result: 46 | self._retry_count = 0 # 重置重试计数 47 | return result 48 | except Exception as e: 49 | logger.error(f"微信连接检查失败: {str(e)}") 50 | return False 51 | 52 | def _monitor_connection(self): 53 | """监控微信连接状态""" 54 | # 为监控线程初始化COM环境 55 | pythoncom.CoInitialize() 56 | 57 | while self._running: 58 | try: 59 | if not self.check_connection(): 60 | if self._retry_count < self._max_retry: 61 | logger.warning(f"微信连接已断开,正在尝试重新连接 (尝试 {self._retry_count + 1}/{self._max_retry})...") 62 | self._instance = None 63 | self.initialize() 64 | self._retry_count += 1 65 | time.sleep(self._reconnect_delay) # 重连等待时间 66 | else: 67 | logger.error("重连次数超过最大限制,停止自动重连") 68 | self._running = False 69 | else: 70 | time.sleep(self._check_interval) 71 | except Exception as e: 72 | logger.error(f"连接监控异常: {str(e)}") 73 | time.sleep(self._check_interval) 74 | 75 | # 监控线程结束时清理COM环境 76 | pythoncom.CoUninitialize() 77 | 78 | def _start_monitor(self): 79 | """启动监控线程""" 80 | if not self._monitor_thread or not self._monitor_thread.is_alive(): 81 | self._running = True 82 | self._monitor_thread = threading.Thread( 83 | target=self._monitor_connection, 84 | daemon=True, 85 | name="WeChatMonitor" 86 | ) 87 | self._monitor_thread.start() 88 | logger.info("微信连接监控已启动") 89 | 90 | def stop(self): 91 | """停止监控""" 92 | self._running = False 93 | if self._monitor_thread and self._monitor_thread.is_alive(): 94 | self._monitor_thread.join(timeout=5) 95 | logger.info("微信连接监控已停止") 96 | 97 | # 创建全局WeChat管理器实例 98 | wechat_manager = WeChatManager() -------------------------------------------------------------------------------- /app/wechat_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | 微信初始化模块 3 | 用于在应用启动时初始化微信相关配置 4 | """ 5 | 6 | import os 7 | import logging 8 | from pathlib import Path 9 | import config_manager 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | def setup_wxauto_paths(): 14 | """ 15 | 设置wxauto库的文件保存路径 16 | 将默认的保存路径修改为data/api/temp 17 | """ 18 | try: 19 | # 导入wxauto库 20 | from wxauto.elements import WxParam 21 | 22 | # 确保目录存在 23 | config_manager.ensure_dirs() 24 | 25 | # 获取临时目录路径 26 | temp_dir = str(config_manager.TEMP_DIR.absolute()) 27 | 28 | # 记录原始保存路径 29 | original_path = WxParam.DEFALUT_SAVEPATH 30 | logger.info(f"原始wxauto保存路径: {original_path}") 31 | 32 | # 修改为新的保存路径 33 | WxParam.DEFALUT_SAVEPATH = temp_dir 34 | logger.info(f"已修改wxauto保存路径为: {temp_dir}") 35 | 36 | return True 37 | except ImportError: 38 | logger.warning("无法导入wxauto库,跳过路径设置") 39 | return False 40 | except Exception as e: 41 | logger.error(f"设置wxauto路径时出错: {str(e)}") 42 | return False 43 | 44 | def initialize(): 45 | """ 46 | 初始化微信相关配置 47 | """ 48 | # 设置wxauto路径 49 | setup_wxauto_paths() 50 | 51 | # 可以在这里添加其他初始化操作 52 | 53 | logger.info("微信初始化配置完成") 54 | -------------------------------------------------------------------------------- /app/wxauto_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxauto包装器 3 | 提供对wxauto库的统一访问接口,无论是在开发环境还是打包环境中 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | import importlib.util 10 | from pathlib import Path 11 | 12 | # 配置日志 13 | logger = logging.getLogger(__name__) 14 | 15 | # 全局变量,用于存储已导入的wxauto模块 16 | _wxauto_module = None 17 | 18 | def _find_module_path(module_name, possible_paths): 19 | """ 20 | 在可能的路径中查找模块 21 | 22 | Args: 23 | module_name (str): 模块名称 24 | possible_paths (list): 可能的路径列表 25 | 26 | Returns: 27 | str: 模块路径,如果找不到则返回None 28 | """ 29 | for path in possible_paths: 30 | if not os.path.exists(path): 31 | continue 32 | 33 | # 检查是否是目录 34 | if os.path.isdir(path): 35 | # 检查是否包含__init__.py文件 36 | init_file = os.path.join(path, "__init__.py") 37 | if os.path.exists(init_file): 38 | return path 39 | 40 | # 检查是否包含模块名称的子目录 41 | subdir = os.path.join(path, module_name) 42 | if os.path.exists(subdir) and os.path.isdir(subdir): 43 | # 检查子目录是否包含__init__.py文件 44 | sub_init_file = os.path.join(subdir, "__init__.py") 45 | if os.path.exists(sub_init_file): 46 | return subdir 47 | 48 | # 检查是否是.py文件 49 | py_file = f"{path}.py" 50 | if os.path.exists(py_file): 51 | return py_file 52 | 53 | return None 54 | 55 | def _get_possible_paths(): 56 | """ 57 | 获取可能的wxauto路径 58 | 59 | Returns: 60 | list: 可能的wxauto路径列表 61 | """ 62 | possible_paths = [] 63 | 64 | # 获取应用根目录 65 | if getattr(sys, 'frozen', False): 66 | # 如果是打包后的环境 67 | app_root = os.path.dirname(sys.executable) 68 | logger.info(f"检测到打包环境,应用根目录: {app_root}") 69 | 70 | # 在打包环境中,确保_MEIPASS目录也在Python路径中 71 | meipass = getattr(sys, '_MEIPASS', None) 72 | if meipass: 73 | logger.info(f"PyInstaller _MEIPASS目录: {meipass}") 74 | possible_paths.extend([ 75 | os.path.join(meipass, "wxauto"), 76 | os.path.join(meipass, "app", "wxauto"), 77 | os.path.join(meipass, "site-packages", "wxauto"), 78 | os.path.join(app_root, "wxauto"), 79 | os.path.join(app_root, "app", "wxauto"), 80 | ]) 81 | else: 82 | # 如果是开发环境 83 | app_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 84 | logger.info(f"检测到开发环境,应用根目录: {app_root}") 85 | possible_paths.extend([ 86 | os.path.join(app_root, "wxauto"), 87 | os.path.join(app_root, "app", "wxauto"), 88 | ]) 89 | 90 | # 添加Python路径中的所有目录 91 | for path in sys.path: 92 | possible_paths.append(os.path.join(path, "wxauto")) 93 | 94 | # 记录所有可能的路径 95 | logger.info(f"可能的wxauto路径: {possible_paths}") 96 | 97 | return possible_paths 98 | 99 | def _import_module(module_path, module_name): 100 | """ 101 | 从指定路径导入模块 102 | 103 | Args: 104 | module_path (str): 模块路径 105 | module_name (str): 模块名称 106 | 107 | Returns: 108 | module: 导入的模块,如果导入失败则返回None 109 | """ 110 | try: 111 | # 如果是目录,则导入整个包 112 | if os.path.isdir(module_path): 113 | # 确保目录在Python路径中 114 | if module_path not in sys.path: 115 | sys.path.insert(0, os.path.dirname(module_path)) 116 | logger.info(f"已将目录添加到Python路径: {os.path.dirname(module_path)}") 117 | 118 | # 导入模块 119 | spec = importlib.util.find_spec(module_name) 120 | if spec: 121 | module = importlib.util.module_from_spec(spec) 122 | spec.loader.exec_module(module) 123 | logger.info(f"成功导入模块: {module_name}") 124 | return module 125 | 126 | # 如果是.py文件,则直接导入 127 | elif module_path.endswith('.py'): 128 | # 确保目录在Python路径中 129 | dir_path = os.path.dirname(module_path) 130 | if dir_path not in sys.path: 131 | sys.path.insert(0, dir_path) 132 | logger.info(f"已将目录添加到Python路径: {dir_path}") 133 | 134 | # 导入模块 135 | spec = importlib.util.spec_from_file_location(module_name, module_path) 136 | if spec: 137 | module = importlib.util.module_from_spec(spec) 138 | spec.loader.exec_module(module) 139 | logger.info(f"成功从文件导入模块: {module_path}") 140 | return module 141 | except Exception as e: 142 | logger.error(f"导入模块失败: {str(e)}") 143 | 144 | return None 145 | 146 | def get_wxauto(): 147 | """ 148 | 获取wxauto模块 149 | 150 | Returns: 151 | module: wxauto模块,如果导入失败则返回None 152 | """ 153 | global _wxauto_module 154 | 155 | # 如果已经导入过,则直接返回 156 | if _wxauto_module: 157 | return _wxauto_module 158 | 159 | # 获取可能的wxauto路径 160 | possible_paths = _get_possible_paths() 161 | 162 | # 查找wxauto模块路径 163 | module_path = _find_module_path("wxauto", possible_paths) 164 | if not module_path: 165 | logger.error("找不到wxauto模块路径") 166 | return None 167 | 168 | # 导入wxauto模块 169 | _wxauto_module = _import_module(module_path, "wxauto") 170 | 171 | # 如果导入失败,尝试直接导入 172 | if not _wxauto_module: 173 | try: 174 | import wxauto 175 | _wxauto_module = wxauto 176 | logger.info("成功直接导入wxauto模块") 177 | except ImportError as e: 178 | logger.error(f"直接导入wxauto模块失败: {str(e)}") 179 | 180 | return _wxauto_module 181 | 182 | # 导出所有wxauto模块的属性 183 | wxauto = get_wxauto() 184 | if wxauto: 185 | # 导出所有wxauto模块的属性 186 | for attr_name in dir(wxauto): 187 | if not attr_name.startswith('_'): 188 | globals()[attr_name] = getattr(wxauto, attr_name) 189 | else: 190 | logger.error("无法导入wxauto模块,功能将不可用") 191 | -------------------------------------------------------------------------------- /app/wxauto_wrapper/wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxauto包装器的主要接口 3 | 提供对wxauto库的所有主要功能的访问 4 | """ 5 | 6 | import os 7 | import sys 8 | import logging 9 | from . import get_wxauto 10 | 11 | # 配置日志 12 | logger = logging.getLogger(__name__) 13 | 14 | # 获取wxauto模块 15 | wxauto = get_wxauto() 16 | 17 | class WxAutoWrapper: 18 | """wxauto包装器类,提供对wxauto库的所有主要功能的访问""" 19 | 20 | def __init__(self): 21 | """初始化wxauto包装器""" 22 | self.wxauto = wxauto 23 | if not self.wxauto: 24 | logger.error("无法初始化wxauto包装器,wxauto模块不可用") 25 | raise ImportError("无法导入wxauto模块") 26 | 27 | # 记录wxauto模块信息 28 | logger.info(f"wxauto模块版本: {getattr(self.wxauto, 'VERSION', '未知')}") 29 | logger.info(f"wxauto模块路径: {self.wxauto.__file__}") 30 | 31 | def __getattr__(self, name): 32 | """获取wxauto模块的属性""" 33 | if self.wxauto: 34 | return getattr(self.wxauto, name) 35 | raise AttributeError(f"'WxAutoWrapper' object has no attribute '{name}', wxauto module is not available") 36 | 37 | # 创建全局wxauto包装器实例 38 | _wrapper_instance = None 39 | 40 | def get_wrapper(): 41 | """ 42 | 获取wxauto包装器实例 43 | 44 | Returns: 45 | WxAutoWrapper: wxauto包装器实例,如果初始化失败则返回None 46 | """ 47 | global _wrapper_instance 48 | 49 | if _wrapper_instance: 50 | return _wrapper_instance 51 | 52 | try: 53 | _wrapper_instance = WxAutoWrapper() 54 | return _wrapper_instance 55 | except Exception as e: 56 | logger.error(f"初始化wxauto包装器失败: {str(e)}") 57 | return None 58 | 59 | # 导出所有wxauto模块的函数和类 60 | if wxauto: 61 | # 导出所有wxauto模块的属性 62 | for attr_name in dir(wxauto): 63 | if not attr_name.startswith('_'): 64 | globals()[attr_name] = getattr(wxauto, attr_name) 65 | -------------------------------------------------------------------------------- /build_tools/build_app.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在打包应用程序... 3 | python build_app.py 4 | echo 打包完成! 5 | pause 6 | -------------------------------------------------------------------------------- /build_tools/build_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | 打包应用程序 3 | 使用PyInstaller将应用程序打包为Windows可执行文件 4 | """ 5 | 6 | import os 7 | import sys 8 | import shutil 9 | import subprocess 10 | import argparse 11 | import importlib 12 | from pathlib import Path 13 | 14 | def check_dependencies(): 15 | """检查依赖是否已安装""" 16 | try: 17 | import PyInstaller 18 | print(f"PyInstaller已安装,版本: {PyInstaller.__version__}") 19 | except ImportError: 20 | print("PyInstaller未安装,正在安装...") 21 | subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"]) 22 | print("PyInstaller安装完成") 23 | 24 | try: 25 | from PIL import Image, ImageDraw, ImageFont 26 | print("Pillow已安装") 27 | except ImportError: 28 | print("Pillow未安装,正在安装...") 29 | subprocess.check_call([sys.executable, "-m", "pip", "install", "pillow"]) 30 | print("Pillow安装完成") 31 | 32 | # 检查pywin32是否已安装 33 | try: 34 | import win32ui 35 | print("PyWin32已安装") 36 | except ImportError: 37 | print("PyWin32未安装或win32ui模块不可用,正在安装...") 38 | subprocess.check_call([sys.executable, "-m", "pip", "install", "pywin32"]) 39 | # 安装后运行pywin32的post-install脚本 40 | try: 41 | import site 42 | site_packages = site.getsitepackages()[0] 43 | post_install_script = os.path.join(site_packages, 'pywin32_system32', 'scripts', 'pywin32_postinstall.py') 44 | if os.path.exists(post_install_script): 45 | print("运行pywin32安装后脚本...") 46 | subprocess.check_call([sys.executable, post_install_script, "-install"]) 47 | else: 48 | print("找不到pywin32安装后脚本,可能需要手动运行") 49 | except Exception as e: 50 | print(f"运行pywin32安装后脚本失败: {e}") 51 | print("PyWin32安装完成") 52 | 53 | # 检查wxautox可能需要的依赖 54 | dependencies = ["tenacity", "requests", "urllib3", "certifi", "idna", "charset_normalizer"] 55 | for dep in dependencies: 56 | try: 57 | importlib.import_module(dep) 58 | print(f"{dep}已安装") 59 | except ImportError: 60 | print(f"{dep}未安装,正在安装...") 61 | subprocess.check_call([sys.executable, "-m", "pip", "install", dep]) 62 | print(f"{dep}安装完成") 63 | 64 | def create_icon(): 65 | """创建图标""" 66 | try: 67 | import create_icon 68 | icon_path = create_icon.create_wechat_style_icon() 69 | print(f"图标已创建: {icon_path}") 70 | return icon_path 71 | except Exception as e: 72 | print(f"创建图标失败: {e}") 73 | return None 74 | 75 | def build_app(debug=False, onefile=False): 76 | """ 77 | 打包应用程序 78 | 79 | Args: 80 | debug (bool): 是否为调试模式 81 | onefile (bool): 是否打包为单个文件 82 | """ 83 | # 检查依赖 84 | check_dependencies() 85 | 86 | # 创建图标 87 | icon_path = create_icon() 88 | if not icon_path: 89 | print("警告: 未能创建图标,将使用默认图标") 90 | icon_path = "icons/wxauto_icon.ico" 91 | 92 | # 确保输出目录存在 93 | dist_dir = Path("dist") 94 | dist_dir.mkdir(exist_ok=True) 95 | 96 | # 构建命令 97 | cmd = [ 98 | sys.executable, 99 | "-m", 100 | "PyInstaller", 101 | "--clean", 102 | "--noconfirm", 103 | ] 104 | 105 | # 添加图标 106 | if os.path.exists(icon_path): 107 | cmd.extend(["--icon", icon_path]) 108 | 109 | # 添加调试选项 110 | if debug: 111 | cmd.append("--debug") 112 | cmd.append("--console") 113 | app_name = "wxauto_http_api_debug" 114 | else: 115 | cmd.append("--windowed") 116 | app_name = "wxauto_http_api" 117 | 118 | # 添加单文件选项 119 | if onefile: 120 | cmd.append("--onefile") 121 | else: 122 | cmd.append("--onedir") 123 | 124 | # 确保包含pywin32的所有必要组件 125 | cmd.extend([ 126 | "--hidden-import", "win32api", 127 | "--hidden-import", "win32con", 128 | "--hidden-import", "win32gui", 129 | "--hidden-import", "win32ui", 130 | "--hidden-import", "win32wnet", 131 | "--hidden-import", "win32com", 132 | "--hidden-import", "win32com.client", 133 | "--hidden-import", "pythoncom", 134 | "--hidden-import", "pywintypes", 135 | ]) 136 | 137 | # 添加wxauto模块及其子模块 138 | cmd.extend([ 139 | "--hidden-import", "wxauto", 140 | "--hidden-import", "wxauto.wxauto", 141 | "--hidden-import", "wxauto.elements", 142 | "--hidden-import", "wxauto.languages", 143 | "--hidden-import", "wxauto.utils", 144 | "--hidden-import", "wxauto.color", 145 | "--hidden-import", "wxauto.errors", 146 | "--hidden-import", "wxauto.uiautomation", 147 | ]) 148 | 149 | # 添加wxauto_wrapper模块及其子模块 150 | cmd.extend([ 151 | "--hidden-import", "app.wxauto_wrapper", 152 | "--hidden-import", "app.wxauto_wrapper.wrapper", 153 | ]) 154 | 155 | # 添加wxautox可能需要的依赖 156 | cmd.extend([ 157 | "--hidden-import", "tenacity", 158 | "--hidden-import", "requests", 159 | "--hidden-import", "urllib3", 160 | "--hidden-import", "certifi", 161 | "--hidden-import", "idna", 162 | "--hidden-import", "charset_normalizer", 163 | ]) 164 | 165 | # 添加编码模块,确保UTF-8编码支持 166 | cmd.extend([ 167 | "--hidden-import", "encodings", 168 | "--hidden-import", "encodings.utf_8", 169 | "--hidden-import", "encodings.gbk", 170 | "--hidden-import", "encodings.gb2312", 171 | "--hidden-import", "encodings.gb18030", 172 | "--hidden-import", "encodings.big5", 173 | "--hidden-import", "encodings.latin_1", 174 | ]) 175 | 176 | # 添加pywin32的DLL文件 177 | try: 178 | import site 179 | import win32api 180 | 181 | # 获取pywin32的安装路径 182 | site_packages = site.getsitepackages()[0] 183 | pywin32_path = os.path.dirname(win32api.__file__) 184 | 185 | # 添加pywin32的DLL文件 186 | pywin32_system32_path = os.path.join(os.path.dirname(pywin32_path), 'pywin32_system32') 187 | if os.path.exists(pywin32_system32_path): 188 | for file in os.listdir(pywin32_system32_path): 189 | if file.endswith('.dll'): 190 | cmd.extend(["--add-binary", f"{os.path.join(pywin32_system32_path, file)}{os.pathsep}."]) 191 | print(f"添加pywin32 DLL文件: {file}") 192 | except (ImportError, AttributeError) as e: 193 | print(f"无法添加pywin32 DLL文件: {e}") 194 | 195 | # 添加数据文件 196 | data_files = [ 197 | (".env", "."), 198 | ("data", "data"), 199 | ("app", "app"), 200 | ("icons", "icons"), 201 | ("fix_path.py", "."), 202 | ("config_manager.py", "."), 203 | ("fix_dependencies.py", "."), 204 | ("requirements.txt", "."), 205 | ("app_ui.py", "."), 206 | ("app_mutex.py", "."), 207 | ("ui_service.py", "."), 208 | ("api_service.py", "."), 209 | ("main.py", "."), 210 | ("run.py", "."), 211 | ("start_ui.bat", "."), 212 | ("start_api.bat", "."), 213 | ("start_api_packaged.bat", "."), 214 | ("initialize_wechat.bat", "."), 215 | ("create_icon.py", "."), 216 | ("dynamic_package_manager.py", "."), 217 | ("wxauto_import.py", "."), # 添加wxauto导入辅助模块 218 | ] 219 | 220 | # 特殊处理wxauto文件夹 221 | wxauto_path = os.path.join(os.getcwd(), "wxauto") 222 | if os.path.exists(wxauto_path) and os.path.isdir(wxauto_path): 223 | print(f"找到wxauto文件夹: {wxauto_path}") 224 | # 添加wxauto文件夹 225 | data_files.append(("wxauto", "wxauto")) 226 | 227 | # 检查wxauto/wxauto子目录 228 | wxauto_inner_path = os.path.join(wxauto_path, "wxauto") 229 | if os.path.exists(wxauto_inner_path) and os.path.isdir(wxauto_inner_path): 230 | print(f"找到wxauto内部目录: {wxauto_inner_path}") 231 | # 确保wxauto/wxauto目录中的所有文件都被包含 232 | for root, dirs, files in os.walk(wxauto_inner_path): 233 | for file in files: 234 | if file.endswith('.py'): 235 | rel_dir = os.path.relpath(root, wxauto_path) 236 | src_file = os.path.join(root, file) 237 | dst_dir = os.path.join("wxauto", rel_dir) 238 | data_files.append((src_file, dst_dir)) 239 | print(f"添加wxauto模块文件: {src_file} -> {dst_dir}") 240 | 241 | # 直接将wxauto模块复制到site-packages目录 242 | print("将wxauto模块复制到site-packages目录,确保能够被正确导入") 243 | cmd.extend([ 244 | "--add-data", f"{wxauto_path}{os.pathsep}.", 245 | ]) 246 | else: 247 | print("警告: 找不到wxauto文件夹,将无法包含wxauto库") 248 | 249 | for src, dst in data_files: 250 | if os.path.exists(src): 251 | cmd.extend(["--add-data", f"{src}{os.pathsep}{dst}"]) 252 | 253 | # 排除wxautox库 254 | cmd.extend(["--exclude-module", "wxautox"]) 255 | 256 | # 添加名称 257 | cmd.extend(["--name", app_name]) 258 | 259 | # 添加入口点 260 | cmd.append("main.py") 261 | 262 | # 执行命令 263 | print(f"执行命令: {' '.join(cmd)}") 264 | subprocess.check_call(cmd) 265 | 266 | print(f"打包完成,输出目录: {dist_dir / app_name}") 267 | 268 | # 复制wxautox wheel文件到输出目录 269 | wheel_files = [f for f in os.listdir() if f.startswith("wxautox-") and f.endswith(".whl")] 270 | if wheel_files: 271 | wheel_file = wheel_files[0] 272 | wheel_dest = dist_dir / app_name / wheel_file 273 | shutil.copy2(wheel_file, wheel_dest) 274 | print(f"已复制wxautox wheel文件到输出目录: {wheel_dest}") 275 | 276 | return dist_dir / app_name 277 | 278 | if __name__ == "__main__": 279 | parser = argparse.ArgumentParser(description="打包应用程序") 280 | parser.add_argument("--debug", action="store_true", help="是否为调试模式") 281 | parser.add_argument("--onefile", action="store_true", help="是否打包为单个文件") 282 | args = parser.parse_args() 283 | 284 | build_app(debug=args.debug, onefile=args.onefile) 285 | -------------------------------------------------------------------------------- /build_tools/build_with_utf8.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 设置环境变量 PYTHONIOENCODING=utf-8 3 | set PYTHONIOENCODING=utf-8 4 | echo 设置环境变量 PYTHONLEGACYWINDOWSSTDIO=0 5 | set PYTHONLEGACYWINDOWSSTDIO=0 6 | 7 | echo 开始打包应用程序... 8 | cd .. 9 | python -m build_tools.build_app %* 10 | 11 | echo 打包完成! 12 | pause 13 | -------------------------------------------------------------------------------- /build_tools/create_icon.py: -------------------------------------------------------------------------------- 1 | """ 2 | 创建微信风格的图标 3 | """ 4 | 5 | import os 6 | from PIL import Image, ImageDraw, ImageFont 7 | import io 8 | 9 | def create_wechat_style_icon(output_path="icons/wxauto_icon.ico", sizes=[16, 32, 48, 64, 128, 256]): 10 | """ 11 | 创建微信风格的图标 12 | 13 | Args: 14 | output_path (str): 输出路径 15 | sizes (list): 图标尺寸列表 16 | """ 17 | # 确保输出目录存在 18 | os.makedirs(os.path.dirname(output_path), exist_ok=True) 19 | 20 | # 创建不同尺寸的图标 21 | images = [] 22 | for size in sizes: 23 | # 创建一个正方形图像,绿色背景 24 | img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) 25 | draw = ImageDraw.Draw(img) 26 | 27 | # 绘制绿色圆形背景 28 | circle_margin = int(size * 0.05) 29 | circle_size = size - 2 * circle_margin 30 | circle_pos = (circle_margin, circle_margin) 31 | draw.ellipse( 32 | [circle_pos[0], circle_pos[1], 33 | circle_pos[0] + circle_size, circle_pos[1] + circle_size], 34 | fill=(7, 193, 96) # 微信绿色 35 | ) 36 | 37 | # 绘制白色"W"字母 38 | font_size = int(size * 0.6) 39 | try: 40 | # 尝试使用Arial字体 41 | font = ImageFont.truetype("arial.ttf", font_size) 42 | except IOError: 43 | # 如果找不到Arial,使用默认字体 44 | font = ImageFont.load_default() 45 | 46 | # 计算文本位置,使其居中 47 | text = "W" 48 | try: 49 | # PIL 9.0.0及以上版本 50 | text_width, text_height = font.getbbox(text)[2:] 51 | except (AttributeError, TypeError): 52 | try: 53 | # PIL 8.0.0及以上版本 54 | text_width, text_height = font.getsize(text) 55 | except (AttributeError, TypeError): 56 | # 旧版本PIL 57 | text_width, text_height = draw.textsize(text, font=font) 58 | 59 | text_pos = ( 60 | circle_pos[0] + (circle_size - text_width) // 2, 61 | circle_pos[1] + (circle_size - text_height) // 2 62 | ) 63 | 64 | # 绘制文本 65 | draw.text(text_pos, text, fill=(255, 255, 255), font=font) 66 | 67 | # 添加到图像列表 68 | images.append(img) 69 | 70 | # 保存为ICO文件 71 | images[0].save( 72 | output_path, 73 | format='ICO', 74 | sizes=[(size, size) for size in sizes], 75 | append_images=images[1:] 76 | ) 77 | 78 | print(f"图标已创建: {output_path}") 79 | return output_path 80 | 81 | if __name__ == "__main__": 82 | create_wechat_style_icon() 83 | -------------------------------------------------------------------------------- /docs/ARCHITECTURE_README.md: -------------------------------------------------------------------------------- 1 | # wxauto_http_api 架构说明 2 | 3 | ## 项目架构 4 | 5 | 本项目采用了清晰的模块化架构,将UI服务和API服务完全分离,避免了相互干扰。 6 | 7 | ### 主要模块 8 | 9 | 1. **main.py**:主入口点,负责解析命令行参数并决定启动UI还是API服务 10 | 2. **ui_service.py**:UI服务逻辑,负责启动和管理UI界面 11 | 3. **api_service.py**:API服务逻辑,负责启动和管理API服务 12 | 4. **app_ui.py**:UI界面的具体实现 13 | 5. **run.py**:API服务的具体实现 14 | 6. **app_mutex.py**:互斥锁机制,确保同一时间只有一个UI实例和一个API服务实例在运行 15 | 7. **fix_path.py**:路径修复模块,确保在打包环境中正确处理路径 16 | 8. **config_manager.py**:配置管理模块,负责加载和保存配置 17 | 18 | ### 启动流程 19 | 20 | 1. **启动UI服务**: 21 | ``` 22 | python main.py --service ui 23 | ``` 24 | 或者双击 `start_ui.bat` 25 | 26 | 2. **启动API服务**: 27 | ``` 28 | python main.py --service api 29 | ``` 30 | 或者双击 `start_api.bat` 31 | 32 | 3. **通过UI启动API服务**: 33 | 在UI界面中点击"启动服务"按钮,会调用 `main.py --service api` 启动API服务 34 | 35 | ### 环境变量 36 | 37 | 项目使用以下环境变量来控制行为: 38 | 39 | - **WXAUTO_SERVICE_TYPE**:服务类型,可以是 `ui` 或 `api` 40 | - **WXAUTO_DEBUG**:是否启用调试模式,值为 `1` 时启用 41 | - **WXAUTO_NO_MUTEX_CHECK**:是否禁用互斥锁检查,值为 `1` 时禁用 42 | 43 | ## 文件说明 44 | 45 | ### 核心文件 46 | 47 | - **main.py**:主入口点,解析命令行参数并启动相应的服务 48 | - **ui_service.py**:UI服务逻辑,负责启动和管理UI界面 49 | - **api_service.py**:API服务逻辑,负责启动和管理API服务 50 | - **app_ui.py**:UI界面的具体实现 51 | - **run.py**:API服务的具体实现 52 | - **app_mutex.py**:互斥锁机制,确保同一时间只有一个UI实例和一个API服务实例在运行 53 | - **fix_path.py**:路径修复模块,确保在打包环境中正确处理路径 54 | - **config_manager.py**:配置管理模块,负责加载和保存配置 55 | 56 | ### 辅助文件 57 | 58 | - **start_ui.bat**:启动UI服务的批处理文件 59 | - **start_api.bat**:启动API服务的批处理文件 60 | - **build_app.py**:打包应用程序的脚本 61 | - **create_icon.py**:创建应用程序图标的脚本 62 | - **fix_dependencies.py**:检查和安装依赖项的脚本 63 | 64 | ### 配置文件 65 | 66 | - **.env**:环境变量配置文件 67 | - **data/api/config/app_config.json**:应用程序配置文件 68 | 69 | ## 打包说明 70 | 71 | 使用 `build_app.py` 脚本打包应用程序: 72 | 73 | ``` 74 | python build_app.py 75 | ``` 76 | 77 | 打包选项: 78 | 79 | - **--debug**:生成调试版本,包含控制台窗口 80 | - **--onefile**:生成单文件版本,而不是文件夹 81 | 82 | 打包后的文件位于 `dist/wxauto_http_api` 目录下。 83 | 84 | ## 架构优势 85 | 86 | 1. **清晰的职责分离**:UI服务和API服务完全分离,职责明确 87 | 2. **避免相互干扰**:UI服务和API服务使用不同的启动脚本,避免了相互干扰 88 | 3. **更灵活的启动方式**:用户可以选择只启动UI、只启动API服务,或者通过UI启动API服务 89 | 4. **更好的错误处理**:每个服务都有详细的错误处理和日志记录,方便诊断问题 90 | 5. **避免重复启动**:通过互斥锁机制,确保同一时间只有一个UI实例和一个API服务实例在运行 91 | 92 | ## 常见问题 93 | 94 | ### 1. UI启动后无法启动API服务 95 | 96 | 可能原因: 97 | - API服务已经在运行 98 | - 端口被占用 99 | 100 | 解决方法: 101 | - 检查是否已有API服务在运行,可以使用任务管理器查看 102 | - 尝试修改配置文件中的端口号 103 | 104 | ### 2. API服务启动失败 105 | 106 | 可能原因: 107 | - 依赖项缺失 108 | - 端口被占用 109 | - 路径问题 110 | 111 | 解决方法: 112 | - 确保所有依赖项都已安装 113 | - 检查端口是否被占用,可以尝试修改配置文件中的端口号 114 | - 查看日志文件,了解详细错误信息 115 | 116 | ### 3. 打包后的应用程序无法启动 117 | 118 | 可能原因: 119 | - 缺少必要的系统依赖 120 | - 路径问题 121 | 122 | 解决方法: 123 | - 确保系统安装了所有必要的依赖 124 | - 尝试将应用程序移动到路径较短且不包含特殊字符的目录 125 | - 查看日志文件,了解详细错误信息 126 | -------------------------------------------------------------------------------- /docs/IMG/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/docs/IMG/01.png -------------------------------------------------------------------------------- /docs/PACKAGING_README.md: -------------------------------------------------------------------------------- 1 | # 打包说明 2 | 3 | 本文档说明如何将项目打包为Windows可执行程序,以及打包后的使用方法。 4 | 5 | ## 打包方法 6 | 7 | ### 方法一:使用批处理文件(推荐) 8 | 9 | 1. 双击运行 `build_app.bat` 10 | 2. 等待打包完成 11 | 3. 打包完成后,可执行文件将位于 `dist/wxauto_http_api` 目录下 12 | 13 | ### 方法二:使用Python脚本 14 | 15 | 1. 打开命令提示符或PowerShell 16 | 2. 切换到项目根目录 17 | 3. 运行以下命令: 18 | ``` 19 | python build_app.py 20 | ``` 21 | 4. 等待打包完成 22 | 5. 打包完成后,可执行文件将位于 `dist/wxauto_http_api` 目录下 23 | 24 | ### 高级选项 25 | 26 | - 调试模式:`python build_app.py --debug` 27 | - 生成带控制台窗口的可执行文件,方便查看日志 28 | - 输出目录:`dist/wxauto_http_api_debug` 29 | 30 | - 单文件模式:`python build_app.py --onefile` 31 | - 生成单个可执行文件,而不是文件夹 32 | - 注意:单文件模式可能会导致启动速度变慢,且可能存在兼容性问题 33 | 34 | ## 打包后的使用方法 35 | 36 | 1. 将 `dist/wxauto_http_api` 目录复制到目标计算机上的任意位置 37 | 2. 启动方式: 38 | - **启动UI界面**:双击运行 `wxauto_http_api.exe` 或 `start_ui.bat` 39 | - **直接启动API服务**:双击运行 `start_api_packaged.bat` 40 | 3. 微信初始化: 41 | - 通过UI界面,API服务启动后会自动尝试初始化微信 42 | - 如果自动初始化失败,可以双击运行 `initialize_wechat.bat` 手动初始化 43 | 4. 如需使用wxautox库,可通过UI界面的"安装wxautox"按钮进行安装 44 | 45 | ## 注意事项 46 | 47 | 1. 打包后的程序包含wxauto库,但不包含wxautox库 48 | 2. wxautox库需要单独安装,可通过UI界面进行安装 49 | 3. 打包后的程序会自动创建必要的目录和配置文件 50 | 4. 如果遇到问题,可尝试使用调试模式打包,查看详细日志 51 | 52 | ## 常见问题 53 | 54 | ### 1. 程序无法启动 55 | 56 | 可能原因: 57 | - 缺少必要的系统依赖 58 | - 权限不足 59 | - 路径中包含特殊字符 60 | 61 | 解决方法: 62 | - 尝试以管理员身份运行 63 | - 将程序移动到路径不包含特殊字符的目录 64 | - 使用调试模式打包,查看详细错误信息 65 | 66 | ### 2. wxauto库无法正常工作 67 | 68 | 可能原因: 69 | - 路径问题 70 | - 缺少必要的系统依赖 71 | 72 | 解决方法: 73 | - 确保程序运行目录下存在wxauto文件夹 74 | - 检查是否安装了必要的系统依赖(如pywin32) 75 | 76 | ### 3. wxautox库安装失败 77 | 78 | 可能原因: 79 | - 网络问题 80 | - 权限不足 81 | - wheel文件不兼容 82 | 83 | 解决方法: 84 | - 确保wheel文件位于程序运行目录下 85 | - 尝试以管理员身份运行程序 86 | - 手动安装wheel文件:`pip install wxautox-xxx.whl` 87 | 88 | ### 4. API服务无法启动 89 | 90 | 可能原因: 91 | - 端口被占用 92 | - 权限不足 93 | - 路径问题 94 | 95 | 解决方法: 96 | - 检查端口是否被占用,可以尝试修改配置文件中的端口号 97 | - 尝试以管理员身份运行 98 | - 查看日志文件,了解详细错误信息 99 | 100 | ### 5. UI启动后无法启动API服务 101 | 102 | 可能原因: 103 | - API服务已经在运行 104 | - 互斥锁问题 105 | 106 | 解决方法: 107 | - 检查是否已有API服务在运行,可以使用任务管理器查看 108 | - 尝试使用`--no-mutex-check`参数启动API服务: 109 | ``` 110 | wxauto_http_api.exe --service api --no-mutex-check 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/api_usage.md: -------------------------------------------------------------------------------- 1 | # WxAuto HTTP API 使用文档 2 | 3 | ## 接口认证 4 | 5 | 所有API请求都需要在请求头中包含API密钥: 6 | ```http 7 | X-API-Key: test-key-2 8 | ``` 9 | 10 | ## API接口示例 11 | 12 | ### 1. API密钥验证 13 | 14 | 验证API密钥是否有效。 15 | 16 | ```bash 17 | curl -X POST http://10.255.0.90:5000/api/auth/verify \ 18 | -H "X-API-Key: test-key-2" 19 | ``` 20 | 21 | 响应示例: 22 | ```json 23 | { 24 | "code": 0, 25 | "message": "验证成功", 26 | "data": { 27 | "valid": true 28 | } 29 | } 30 | ``` 31 | 32 | ### 2. 微信基础功能 33 | 34 | #### 2.1 初始化微信 35 | 36 | 初始化微信实例,建议在使用其他接口前先调用此接口。 37 | 38 | ```bash 39 | curl -X POST http://10.255.0.90:5000/api/wechat/initialize \ 40 | -H "X-API-Key: test-key-2" 41 | ``` 42 | 43 | 响应示例: 44 | ```json 45 | { 46 | "code": 0, 47 | "message": "初始化成功", 48 | "data": { 49 | "status": "connected" 50 | } 51 | } 52 | ``` 53 | 54 | #### 2.2 获取微信状态 55 | 56 | 检查微信连接状态。 57 | 58 | ```bash 59 | curl -X GET http://10.255.0.90:5000/api/wechat/status \ 60 | -H "X-API-Key: test-key-2" 61 | ``` 62 | 63 | 响应示例: 64 | ```json 65 | { 66 | "code": 0, 67 | "message": "获取成功", 68 | "data": { 69 | "status": "online" 70 | } 71 | } 72 | ``` 73 | 74 | ### 3. 消息相关接口 75 | 76 | #### 3.1 发送普通文本消息 77 | 78 | 发送普通文本消息到指定联系人或群组。 79 | 80 | ```bash 81 | curl -X POST http://10.255.0.90:5000/api/message/send \ 82 | -H "X-API-Key: test-key-2" \ 83 | -H "Content-Type: application/json" \ 84 | -d '{ 85 | "receiver": "文件传输助手", 86 | "message": "这是一条测试消息", 87 | "at_list": ["张三", "李四"], 88 | "clear": true 89 | }' 90 | ``` 91 | 92 | 参数说明: 93 | - receiver: 接收者(联系人或群组名称) 94 | - message: 消息内容 95 | - at_list: (可选)需要@的群成员列表 96 | - clear: (可选)是否清除输入框,默认为true 97 | 98 | 响应示例: 99 | ```json 100 | { 101 | "code": 0, 102 | "message": "发送成功", 103 | "data": { 104 | "message_id": "success" 105 | } 106 | } 107 | ``` 108 | 109 | #### 3.2 发送打字机模式消息 110 | 111 | 使用打字机模式发送消息(模拟真人打字效果)。 112 | 113 | ```bash 114 | curl -X POST http://10.255.0.90:5000/api/message/send-typing \ 115 | -H "X-API-Key: test-key-2" \ 116 | -H "Content-Type: application/json" \ 117 | -d '{ 118 | "receiver": "文件传输助手", 119 | "message": "这是一条打字机模式消息\n这是第二行", 120 | "at_list": ["张三"], 121 | "clear": true 122 | }' 123 | ``` 124 | 125 | 参数说明同普通消息发送。 126 | 127 | #### 3.3 发送文件 128 | 129 | 发送一个或多个文件。 130 | 131 | ```bash 132 | curl -X POST http://10.255.0.90:5000/api/message/send-file \ 133 | -H "X-API-Key: test-key-2" \ 134 | -H "Content-Type: application/json" \ 135 | -d '{ 136 | "receiver": "文件传输助手", 137 | "file_paths": [ 138 | "D:/test/file1.txt", 139 | "D:/test/image.jpg" 140 | ] 141 | }' 142 | ``` 143 | 144 | 参数说明: 145 | - receiver: 接收者 146 | - file_paths: 要发送的文件路径列表 147 | 148 | 响应示例: 149 | ```json 150 | { 151 | "code": 0, 152 | "message": "发送成功", 153 | "data": { 154 | "success_count": 2, 155 | "failed_files": [] 156 | } 157 | } 158 | ``` 159 | 160 | ### 4. 群组相关接口 161 | 162 | #### 4.1 获取群列表 163 | 164 | 获取当前账号的群聊列表。 165 | 166 | ```bash 167 | curl -X GET http://10.255.0.90:5000/api/group/list \ 168 | -H "X-API-Key: test-key-2" 169 | ``` 170 | 171 | 响应示例: 172 | ```json 173 | { 174 | "code": 0, 175 | "message": "获取成功", 176 | "data": { 177 | "groups": [ 178 | {"name": "测试群1"}, 179 | {"name": "测试群2"} 180 | ] 181 | } 182 | } 183 | ``` 184 | 185 | #### 4.2 群组管理 186 | 187 | 执行群组管理操作(如重命名、退群等)。 188 | 189 | ```bash 190 | curl -X POST http://10.255.0.90:5000/api/group/manage \ 191 | -H "X-API-Key: test-key-2" \ 192 | -H "Content-Type: application/json" \ 193 | -d '{ 194 | "group_name": "测试群", 195 | "action": "rename", 196 | "params": { 197 | "new_name": "新群名称" 198 | } 199 | }' 200 | ``` 201 | 202 | 支持的操作类型: 203 | - rename: 重命名群组 204 | - quit: 退出群组 205 | 206 | ### 5. 联系人相关接口 207 | 208 | #### 5.1 获取好友列表 209 | 210 | 获取当前账号的好友列表。 211 | 212 | ```bash 213 | curl -X GET http://10.255.0.90:5000/api/contact/list \ 214 | -H "X-API-Key: test-key-2" 215 | ``` 216 | 217 | 响应示例: 218 | ```json 219 | { 220 | "code": 0, 221 | "message": "获取成功", 222 | "data": { 223 | "friends": [ 224 | {"nickname": "张三"}, 225 | {"nickname": "李四"} 226 | ] 227 | } 228 | } 229 | ``` 230 | 231 | ### 6. 健康检查接口 232 | 233 | 获取服务和微信连接状态。 234 | 235 | ```bash 236 | curl -X GET http://10.255.0.90:5000/api/health \ 237 | -H "X-API-Key: test-key-2" 238 | ``` 239 | 240 | 响应示例: 241 | ```json 242 | { 243 | "code": 0, 244 | "message": "服务正常", 245 | "data": { 246 | "status": "ok", 247 | "wechat_status": "connected", 248 | "uptime": 3600 249 | } 250 | } 251 | ``` 252 | 253 | ## 错误码说明 254 | 255 | - 0: 成功 256 | - 1001: 认证失败(API密钥无效) 257 | - 1002: 参数错误 258 | - 2001: 微信未初始化 259 | - 2002: 微信已掉线 260 | - 3001: 发送消息失败 261 | - 3002: 获取消息失败 262 | - 4001: 群操作失败 263 | - 5001: 好友操作失败 264 | - 5000: 服务器内部错误 265 | 266 | ## 使用建议 267 | 268 | 1. 在使用其他接口前,先调用初始化接口 269 | 2. 使用健康检查接口监控服务状态 270 | 3. 合理处理错误码,做好重试机制 271 | 4. 注意文件发送时的路径正确性 272 | 5. 群发消息时建议加入适当延时 273 | 274 | ## PowerShell示例 275 | 276 | 如果您使用PowerShell,可以使用以下格式发送请求: 277 | 278 | ```powershell 279 | $headers = @{ 280 | "X-API-Key" = "test-key-2" 281 | "Content-Type" = "application/json" 282 | } 283 | 284 | $body = @{ 285 | receiver = "文件传输助手" 286 | message = "测试消息" 287 | } | ConvertTo-Json 288 | 289 | Invoke-RestMethod -Method Post -Uri "http://10.255.0.90:5000/api/message/send" -Headers $headers -Body $body 290 | ``` 291 | 292 | ## Python示例 293 | 294 | 使用Python requests库的示例: 295 | 296 | ```python 297 | import requests 298 | 299 | API_KEY = "test-key-2" 300 | BASE_URL = "http://10.255.0.90:5000/api" 301 | 302 | headers = { 303 | "X-API-Key": API_KEY, 304 | "Content-Type": "application/json" 305 | } 306 | 307 | # 发送消息示例 308 | response = requests.post( 309 | f"{BASE_URL}/message/send", 310 | headers=headers, 311 | json={ 312 | "receiver": "文件传输助手", 313 | "message": "测试消息" 314 | } 315 | ) 316 | 317 | print(response.json()) -------------------------------------------------------------------------------- /docs/development_plan.md: -------------------------------------------------------------------------------- 1 | # WxAuto HTTP API 开发规划 2 | 3 | ## 1. 项目概述 4 | 5 | 将wxauto的功能通过HTTP API方式提供服务,使用Flask框架实现,支持密钥验证机制。 6 | 7 | ## 2. 技术栈选择 8 | 9 | - Web框架:Flask 10 | - 认证机制:API Key认证 11 | - 文档生成:Swagger/OpenAPI 12 | - 日志管理:Python logging 13 | - 配置管理:Python dotenv 14 | 15 | ## 3. 项目结构 16 | 17 | ``` 18 | wxauto-ui/ 19 | │ 20 | ├── app/ 21 | │ ├── __init__.py 22 | │ ├── config.py # 配置文件 23 | │ ├── auth.py # 认证相关 24 | │ ├── utils.py # 工具函数 25 | │ ├── logs.py # 日志配置 26 | │ └── api/ 27 | │ ├── __init__.py 28 | │ ├── routes.py # API路由 29 | │ ├── models.py # 数据模型 30 | │ └── schemas.py # 请求/响应模式 31 | │ 32 | ├── docs/ 33 | │ ├── development_plan.md # 开发规划 34 | │ └── api_documentation.md # API文档 35 | │ 36 | ├── tests/ # 测试用例 37 | │ └── test_api.py 38 | │ 39 | ├── .env # 环境变量 40 | ├── requirements.txt # 项目依赖 41 | └── run.py # 启动文件 42 | ``` 43 | 44 | ## 4. API 端点设计 45 | 46 | ### 4.1 认证相关 47 | 48 | - `POST /api/auth/verify` 49 | - 功能:验证API密钥 50 | - 请求头:`X-API-Key: ` 51 | 52 | ### 4.2 微信基础功能 53 | 54 | - `POST /api/wechat/initialize` 55 | - 功能:初始化微信实例 56 | - 返回:实例状态 57 | 58 | - `GET /api/wechat/status` 59 | - 功能:获取微信连接状态 60 | - 返回:在线状态 61 | 62 | ### 4.3 消息相关接口 63 | 64 | - `POST /api/message/send` 65 | - 功能:发送普通文本消息 66 | - 参数:接收人、消息内容、是否@功能等 67 | 68 | - `POST /api/message/send-typing` 69 | - 功能:发送打字机模式消息 70 | - 参数:接收人、消息内容、是否@功能等 71 | 72 | - `POST /api/message/send-file` 73 | - 功能:发送文件消息 74 | - 参数:接收人、文件路径 75 | 76 | - `POST /api/message/get-history` 77 | - 功能:获取聊天记录 78 | - 参数:聊天对象、时间范围等 79 | 80 | ### 4.4 群组相关接口 81 | 82 | - `GET /api/group/list` 83 | - 功能:获取群列表 84 | 85 | - `POST /api/group/manage` 86 | - 功能:群管理操作 87 | - 参数:群名称、操作类型等 88 | 89 | ### 4.5 好友相关接口 90 | 91 | - `GET /api/contact/list` 92 | - 功能:获取好友列表 93 | 94 | - `POST /api/contact/add` 95 | - 功能:添加好友 96 | - 参数:微信号、验证信息等 97 | 98 | ## 5. 安全机制 99 | 100 | ### 5.1 API密钥认证 101 | - 使用环境变量存储API密钥 102 | - 实现中间件进行密钥验证 103 | - 支持多个API密钥管理 104 | 105 | ### 5.2 请求限流 106 | - 实现基于IP的请求限流 107 | - 按API密钥进行请求限制 108 | 109 | ### 5.3 日志记录 110 | - 记录所有API调用 111 | - 记录错误和异常情况 112 | - 支持日志轮转 113 | 114 | ## 6. 实现步骤 115 | 116 | ### 第一阶段:基础框架搭建(3天) 117 | 1. 设置项目结构 118 | 2. 实现基础Flask应用 119 | 3. 配置开发环境 120 | 4. 实现API密钥认证机制 121 | 122 | ### 第二阶段:核心功能实现(5天) 123 | 1. 实现微信初始化接口 124 | 2. 实现消息发送相关接口 125 | 3. 实现群组管理接口 126 | 4. 实现好友管理接口 127 | 5. 添加错误处理 128 | 129 | ### 第三阶段:功能完善(4天) 130 | 1. 实现请求限流 131 | 2. 完善日志系统 132 | 3. 添加单元测试 133 | 4. 编写API文档 134 | 135 | ### 第四阶段:测试和优化(3天) 136 | 1. 进行功能测试 137 | 2. 进行性能测试 138 | 3. 优化代码 139 | 4. 完善文档 140 | 141 | ## 7. 依赖项目 142 | 143 | ```python 144 | # requirements.txt 内容 145 | flask==2.0.1 146 | flask-restful==0.3.9 147 | python-dotenv==0.19.0 148 | flask-limiter==2.4.0 149 | pyyaml==5.4.1 150 | python-jose==3.3.0 151 | requests==2.26.0 152 | wxautox==3.9.11.17.25 153 | ``` 154 | 155 | ## 8. 注意事项 156 | 157 | 1. wxauto的限制和要求: 158 | - 仅支持Windows系统 159 | - 需要微信保持登录状态 160 | - 避免频繁操作导致掉线 161 | 162 | 2. 安全考虑: 163 | - API密钥定期轮换 164 | - 限制API调用频率 165 | - 记录所有操作日志 166 | 167 | 3. 异常处理: 168 | - 微信掉线自动重连 169 | - 超时处理 170 | - 错误重试机制 171 | 172 | ## 9. 后续优化方向 173 | 174 | 1. 添加WebSocket支持,实现消息推送 175 | 2. 实现集群部署方案 176 | 3. 添加管理后台 177 | 4. 优化性能和并发处理 178 | 5. 添加更多的API功能 -------------------------------------------------------------------------------- /docs/message_monitor_service.md: -------------------------------------------------------------------------------- 1 | # 微信消息监控服务实现规划 2 | 3 | ## 1. 功能概述 4 | 5 | 实现一个基于Python的UI程序,提供以下核心功能: 6 | 7 | 1. 自动监控新的未读会话 8 | 2. 自动管理监听列表 9 | 3. 实时获取最新消息 10 | 4. 监控微信接口状态 11 | 5. 可视化界面展示 12 | 13 | ## 2. 技术栈选择 14 | 15 | - UI框架:PyQt6(现代、稳定、跨平台) 16 | - 网络请求:requests 17 | - 状态管理:自定义状态管理器 18 | - 定时任务:APScheduler 19 | - 日志管理:logging 20 | - 配置管理:Python dotenv 21 | 22 | ## 3. 系统架构设计 23 | 24 | ### 3.1 核心模块 25 | 26 | 1. **MonitorService模块** 27 | - 负责消息监控的核心逻辑 28 | - 维护监听列表 29 | - 定时任务调度 30 | - 与WxAuto HTTP API交互 31 | 32 | 2. **UI模块** 33 | - 主窗口界面 34 | - 消息列表展示 35 | - 状态监控面板 36 | - 配置管理界面 37 | 38 | 3. **数据管理模块** 39 | - 监听对象管理 40 | - 消息历史记录 41 | - 配置信息存储 42 | 43 | ### 3.2 数据结构设计 44 | 45 | ```python 46 | # 监听对象数据结构 47 | class ListenTarget: 48 | who: str # 监听对象名称 49 | last_message_time: datetime # 最后一条消息时间 50 | added_time: datetime # 加入监听的时间 51 | message_count: int # 消息计数 52 | is_active: bool # 是否活跃 53 | 54 | # 消息数据结构 55 | class Message: 56 | id: str # 消息ID 57 | type: str # 消息类型 58 | content: str # 消息内容 59 | sender: str # 发送者 60 | time: datetime # 消息时间 61 | chat_name: str # 会话名称 62 | ``` 63 | 64 | ## 4. 具体实现方案 65 | 66 | ### 4.1 定时任务实现 67 | 68 | ```python 69 | class MonitorScheduler: 70 | def __init__(self): 71 | self.scheduler = APScheduler() 72 | self.listen_targets = {} 73 | 74 | def start(self): 75 | # 每30秒检查新的未读会话 76 | self.scheduler.add_job(self.check_new_chats, 'interval', seconds=30) 77 | # 每5秒获取监听列表的最新消息 78 | self.scheduler.add_job(self.check_listen_messages, 'interval', seconds=5) 79 | # 每分钟检查不活跃的监听对象 80 | self.scheduler.add_job(self.clean_inactive_targets, 'interval', minutes=1) 81 | ``` 82 | 83 | ### 4.2 监听对象管理 84 | 85 | - 最多同时监听30个对象 86 | - 超过30分钟无新消息自动移除 87 | - 使用优先队列管理监听对象 88 | - 按最后活跃时间排序 89 | 90 | ### 4.3 UI界面设计 91 | 92 | 1. **主窗口布局** 93 | - 状态栏:显示微信连接状态、监听对象数量 94 | - 监听列表:显示当前监听的对象 95 | - 消息面板:实时显示最新消息 96 | - 配置面板:可调整监听参数 97 | 98 | 2. **功能区域** 99 | - 手动添加/移除监听对象 100 | - 查看历史消息 101 | - 导出消息记录 102 | - 调整监控参数 103 | 104 | ## 5. 实现步骤 105 | 106 | ### 5.1 第一阶段:核心服务(3天) 107 | 108 | 1. 实现MonitorService 109 | - 消息监控核心逻辑 110 | - 监听对象管理 111 | - API接口封装 112 | 113 | 2. 实现定时任务 114 | - 新会话检查 115 | - 消息轮询 116 | - 超时清理 117 | 118 | ### 5.2 第二阶段:UI开发(4天) 119 | 120 | 1. 设计并实现主窗口 121 | 2. 实现消息展示功能 122 | 3. 实现状态监控面板 123 | 4. 添加配置管理界面 124 | 125 | ### 5.3 第三阶段:功能完善(3天) 126 | 127 | 1. 添加消息存储功能 128 | 2. 实现消息导出功能 129 | 3. 添加过滤和搜索功能 130 | 4. 优化性能和资源占用 131 | 132 | ## 6. 关键代码结构 133 | 134 | ### 6.1 监控服务核心 135 | 136 | ```python 137 | class MessageMonitorService: 138 | def __init__(self): 139 | self.scheduler = MonitorScheduler() 140 | self.api_client = WxAutoApiClient() 141 | self.listen_targets = PriorityQueue(maxsize=30) 142 | 143 | async def check_new_chats(self): 144 | # 获取新的未读会话 145 | new_messages = await self.api_client.get_next_new_message() 146 | for chat in new_messages: 147 | if not self.is_full(): 148 | await self.add_listen_target(chat) 149 | 150 | async def check_listen_messages(self): 151 | # 获取监听列表的最新消息 152 | messages = await self.api_client.get_listen_messages() 153 | self.update_message_status(messages) 154 | 155 | def clean_inactive_targets(self): 156 | # 清理不活跃的监听对象 157 | current_time = datetime.now() 158 | for target in self.listen_targets: 159 | if (current_time - target.last_message_time).seconds > 1800: 160 | self.remove_listen_target(target) 161 | ``` 162 | 163 | ### 6.2 UI主窗口 164 | 165 | ```python 166 | class MainWindow(QMainWindow): 167 | def __init__(self): 168 | super().__init__() 169 | self.monitor_service = MessageMonitorService() 170 | self.init_ui() 171 | 172 | def init_ui(self): 173 | # 初始化UI组件 174 | self.status_bar = self.statusBar() 175 | self.create_main_layout() 176 | self.create_message_panel() 177 | self.create_config_panel() 178 | ``` 179 | 180 | ## 7. 异常处理 181 | 182 | 1. **网络异常** 183 | - 自动重试机制 184 | - 错误状态显示 185 | - 断线重连 186 | 187 | 2. **资源管理** 188 | - 内存使用监控 189 | - 消息缓存清理 190 | - 文件句柄管理 191 | 192 | 3. **微信状态** 193 | - 定期检查连接 194 | - 自动重新初始化 195 | - 状态变化通知 196 | 197 | ## 8. 性能优化 198 | 199 | 1. **消息处理** 200 | - 使用异步处理 201 | - 批量处理消息 202 | - 消息缓存机制 203 | 204 | 2. **资源利用** 205 | - 合理设置轮询间隔 206 | - 优化数据结构 207 | - 内存使用控制 208 | 209 | ## 9. 测试计划 210 | 211 | 1. **单元测试** 212 | - 核心功能测试 213 | - 异常处理测试 214 | - 边界条件测试 215 | 216 | 2. **集成测试** 217 | - UI功能测试 218 | - 系统稳定性测试 219 | - 性能压力测试 220 | 221 | ## 10. 部署说明 222 | 223 | 1. **环境要求** 224 | - Python 3.11+ 225 | - Windows系统 226 | - 微信客户端 227 | 228 | 2. **依赖安装** 229 | ``` 230 | pip install PyQt6 231 | pip install APScheduler 232 | pip install requests 233 | ``` 234 | 235 | 3. **配置说明** 236 | - API密钥配置 237 | - 监控参数设置 238 | - 日志级别设置 239 | 240 | ## 11. 后续优化方向 241 | 242 | 1. 添加消息关键词监控 243 | 2. 支持自定义消息处理规则 244 | 3. 添加消息统计分析功能 245 | 4. 优化UI交互体验 246 | 5. 添加多语言支持 247 | 248 | ## 12. 注意事项 249 | 250 | 1. 微信API限制 251 | - 遵守接口调用频率 252 | - 处理接口超时 253 | - 错误重试策略 254 | 255 | 2. 资源管理 256 | - 控制内存使用 257 | - 及时清理缓存 258 | - 优化性能 259 | 260 | 3. 用户体验 261 | - 界面响应及时 262 | - 状态提示清晰 263 | - 操作简单直观 -------------------------------------------------------------------------------- /icons/wxauto_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/icons/wxauto_icon.ico -------------------------------------------------------------------------------- /initialize_wechat.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在初始化微信... 3 | curl -X POST -H "X-API-Key: test-key-2" http://localhost:5000/api/wechat/initialize 4 | echo. 5 | echo 初始化完成! 6 | pause 7 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxauto_http_api 主入口点 3 | 负责解析命令行参数并决定启动UI还是API服务 4 | """ 5 | 6 | import os 7 | import sys 8 | import io 9 | import logging 10 | import argparse 11 | import traceback 12 | 13 | # 设置标准输出和标准错误的编码为UTF-8 14 | try: 15 | if hasattr(sys.stdout, 'buffer'): 16 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') 17 | if hasattr(sys.stderr, 'buffer'): 18 | sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') 19 | except Exception as e: 20 | print(f"设置标准输出编码失败: {str(e)}") 21 | 22 | # 设置环境变量,确保Python使用UTF-8编码 23 | os.environ['PYTHONIOENCODING'] = 'utf-8' 24 | os.environ['PYTHONLEGACYWINDOWSSTDIO'] = '0' # 禁用旧版Windows标准IO处理 25 | 26 | # 配置日志 27 | import os 28 | 29 | # 确保日志目录存在 30 | os.makedirs('data/logs', exist_ok=True) 31 | 32 | # 创建日志处理器 33 | # 控制台处理器 34 | console_handler = logging.StreamHandler() 35 | console_handler.setLevel(logging.INFO) 36 | console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 37 | console_handler.setFormatter(console_formatter) 38 | 39 | # 配置根日志记录器 - 只使用控制台处理器,不创建额外的日志文件 40 | # 详细的日志会由app/logs.py模块处理 41 | root_logger = logging.getLogger() 42 | root_logger.setLevel(logging.INFO) 43 | root_logger.addHandler(console_handler) 44 | 45 | # 获取当前模块的日志记录器 46 | logger = logging.getLogger(__name__) 47 | 48 | def setup_environment(): 49 | """设置环境变量""" 50 | # 记录启动环境信息 51 | logger.info(f"Python版本: {sys.version}") 52 | logger.info(f"当前工作目录: {os.getcwd()}") 53 | logger.info(f"Python路径: {sys.path}") 54 | logger.info(f"是否在PyInstaller环境中运行: {getattr(sys, 'frozen', False)}") 55 | 56 | # 获取应用根目录 57 | if getattr(sys, 'frozen', False): 58 | # 如果是打包后的环境 59 | app_root = os.path.dirname(sys.executable) 60 | logger.info(f"检测到打包环境,应用根目录: {app_root}") 61 | 62 | # 在打包环境中,确保_MEIPASS目录也在Python路径中 63 | meipass = getattr(sys, '_MEIPASS', None) 64 | if meipass and meipass not in sys.path: 65 | sys.path.insert(0, meipass) 66 | logger.info(f"已将_MEIPASS目录添加到Python路径: {meipass}") 67 | else: 68 | # 如果是开发环境 69 | app_root = os.path.dirname(os.path.abspath(__file__)) 70 | logger.info(f"检测到开发环境,应用根目录: {app_root}") 71 | 72 | # 确保应用根目录在Python路径中 73 | if app_root not in sys.path: 74 | sys.path.insert(0, app_root) 75 | logger.info(f"已将应用根目录添加到Python路径: {app_root}") 76 | 77 | # 确保wxauto目录在Python路径中 78 | wxauto_path = os.path.join(app_root, "wxauto") 79 | if os.path.exists(wxauto_path) and wxauto_path not in sys.path: 80 | sys.path.insert(0, wxauto_path) 81 | logger.info(f"已将wxauto目录添加到Python路径: {wxauto_path}") 82 | 83 | # 设置工作目录为应用根目录 84 | os.chdir(app_root) 85 | logger.info(f"已将工作目录设置为应用根目录: {app_root}") 86 | 87 | # 再次记录环境信息,确认修改已生效 88 | logger.info(f"修复后的工作目录: {os.getcwd()}") 89 | logger.info(f"修复后的Python路径: {sys.path}") 90 | 91 | return app_root 92 | 93 | def main(): 94 | """主函数,解析命令行参数并启动相应的服务""" 95 | # 设置环境 96 | setup_environment() 97 | 98 | # 记录命令行参数 99 | logger.info(f"命令行参数: {sys.argv}") 100 | 101 | # 解析命令行参数 102 | parser = argparse.ArgumentParser(description="wxauto_http_api") 103 | parser.add_argument("--service", choices=["ui", "api"], default="ui", 104 | help="指定要启动的服务类型: ui 或 api") 105 | parser.add_argument("--debug", action="store_true", help="启用调试模式") 106 | parser.add_argument("--no-mutex-check", action="store_true", help="禁用互斥锁检查") 107 | 108 | # 在打包环境中,可能会有额外的参数,如main.py 109 | if getattr(sys, 'frozen', False) and len(sys.argv) > 1 and sys.argv[1].endswith('.py'): 110 | # 移除第一个参数(可能是main.py) 111 | logger.info(f"检测到打包环境中的脚本参数: {sys.argv[1]},将移除") 112 | args = parser.parse_args(sys.argv[2:]) 113 | else: 114 | args = parser.parse_args() 115 | 116 | # 记录解析后的参数 117 | logger.info(f"解析后的参数: service={args.service}, debug={args.debug}, no_mutex_check={args.no_mutex_check}") 118 | 119 | # 设置环境变量,标记服务类型 120 | os.environ["WXAUTO_SERVICE_TYPE"] = args.service 121 | 122 | # 设置调试模式 123 | if args.debug: 124 | logging.getLogger().setLevel(logging.DEBUG) 125 | os.environ["WXAUTO_DEBUG"] = "1" 126 | logger.debug("已启用调试模式") 127 | 128 | # 设置互斥锁检查 129 | if args.no_mutex_check: 130 | os.environ["WXAUTO_NO_MUTEX_CHECK"] = "1" 131 | logger.info("已禁用互斥锁检查") 132 | 133 | # 导入fix_path模块 134 | try: 135 | # 先尝试直接导入 136 | try: 137 | # 尝试从app目录导入 138 | sys.path.insert(0, os.path.join(os.getcwd(), "app")) 139 | from app import fix_path 140 | app_root = fix_path.fix_paths() 141 | logger.info(f"路径修复完成,应用根目录: {app_root}") 142 | except ImportError: 143 | # 如果直接导入失败,尝试创建一个简单的fix_paths函数 144 | logger.warning("无法导入fix_path模块,将使用内置的路径修复函数") 145 | 146 | def fix_paths(): 147 | """简单的路径修复函数""" 148 | app_root = os.getcwd() 149 | 150 | # 确保wxauto目录在Python路径中 151 | wxauto_path = os.path.join(app_root, "wxauto") 152 | if os.path.exists(wxauto_path) and wxauto_path not in sys.path: 153 | sys.path.insert(0, wxauto_path) 154 | logger.info(f"已将wxauto目录添加到Python路径: {wxauto_path}") 155 | 156 | # 确保app目录在Python路径中 157 | app_path = os.path.join(app_root, "app") 158 | if os.path.exists(app_path) and app_path not in sys.path: 159 | sys.path.insert(0, app_path) 160 | logger.info(f"已将app目录添加到Python路径: {app_path}") 161 | 162 | return app_root 163 | 164 | app_root = fix_paths() 165 | logger.info(f"使用内置路径修复函数,应用根目录: {app_root}") 166 | except Exception as e: 167 | logger.error(f"路径修复时出错: {str(e)}") 168 | logger.error(traceback.format_exc()) 169 | 170 | # 导入Unicode编码修复模块 171 | try: 172 | logger.info("尝试导入Unicode编码修复模块") 173 | from app import unicode_fix 174 | logger.info("成功导入Unicode编码修复模块") 175 | except ImportError as e: 176 | logger.warning(f"导入Unicode编码修复模块失败: {str(e)}") 177 | logger.warning("这可能会导致在处理包含Unicode表情符号的微信名称时出现问题") 178 | except Exception as e: 179 | logger.error(f"应用Unicode编码修复时出错: {str(e)}") 180 | logger.error(traceback.format_exc()) 181 | 182 | # 确保wxauto库能够被正确导入 183 | try: 184 | logger.info("尝试导入wxauto库") 185 | from app.wxauto_wrapper import get_wxauto 186 | wxauto = get_wxauto() 187 | if wxauto: 188 | 189 | # 尝试导入wxauto包装器 190 | try: 191 | from app.wxauto_wrapper.wrapper import get_wrapper 192 | wrapper = get_wrapper() 193 | 194 | except Exception as e: 195 | logger.error(f"初始化wxauto包装器失败: {str(e)}") 196 | logger.error(traceback.format_exc()) 197 | else: 198 | logger.warning("wxauto库导入失败,可能会导致功能不可用") 199 | except ImportError as e: 200 | logger.warning(f"导入wxauto_wrapper模块失败: {str(e)}") 201 | except Exception as e: 202 | logger.error(f"导入wxauto库时出错: {str(e)}") 203 | logger.error(traceback.format_exc()) 204 | 205 | # 初始化动态包管理器 206 | try: 207 | from app.dynamic_package_manager import get_package_manager 208 | package_manager = get_package_manager() 209 | logger.info("成功初始化动态包管理器") 210 | 211 | # 检查wxautox是否已安装 212 | if package_manager.is_package_installed("wxautox"): 213 | logger.info("动态包管理器检测到wxautox已安装") 214 | else: 215 | logger.info("动态包管理器未检测到wxautox,将使用wxauto") 216 | 217 | # 注意:不再自动安装wxautox,它是可选的,只在用户手动选择时才安装 218 | except ImportError as e: 219 | logger.warning(f"导入动态包管理器失败: {str(e)}") 220 | except Exception as e: 221 | logger.error(f"初始化动态包管理器时出错: {str(e)}") 222 | logger.error(traceback.format_exc()) 223 | 224 | # 根据服务类型启动相应的服务 225 | if args.service == "ui": 226 | logger.info("正在启动UI服务...") 227 | try: 228 | from app.ui_service import start_ui 229 | start_ui() 230 | except ImportError as e: 231 | logger.error(f"导入UI服务模块失败: {str(e)}") 232 | logger.error(traceback.format_exc()) 233 | sys.exit(1) 234 | except Exception as e: 235 | logger.error(f"启动UI服务时出错: {str(e)}") 236 | logger.error(traceback.format_exc()) 237 | sys.exit(1) 238 | elif args.service == "api": 239 | logger.info("正在启动API服务...") 240 | try: 241 | from app.api_service import start_api 242 | start_api() 243 | except ImportError as e: 244 | logger.error(f"导入API服务模块失败: {str(e)}") 245 | logger.error(traceback.format_exc()) 246 | sys.exit(1) 247 | except Exception as e: 248 | logger.error(f"启动API服务时出错: {str(e)}") 249 | logger.error(traceback.format_exc()) 250 | sys.exit(1) 251 | 252 | if __name__ == "__main__": 253 | main() 254 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=2.3.1 2 | flask-restful==0.3.9 3 | python-dotenv==0.19.0 4 | flask-limiter 5 | pyyaml>=6.0.1 6 | python-jose==3.3.0 7 | requests==2.26.0 8 | pywin32==310 9 | psutil==5.9.8 10 | comtypes>=1.4.0 11 | pyperclip>=1.9.0 12 | tenacity>=9.1.0 13 | typing-extensions>=4.0.0 14 | uiautomation 15 | Pillow 16 | pywin32 17 | psutil 18 | pyperclip 19 | 20 | # wxauto库将在运行时安装 -------------------------------------------------------------------------------- /start_api.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在启动wxauto_http_api API服务... 3 | python main.py --service api 4 | pause 5 | -------------------------------------------------------------------------------- /start_ui.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo 正在启动wxauto_http_api管理界面... 3 | python main.py --service ui 4 | pause 5 | -------------------------------------------------------------------------------- /wxauto/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Cluic 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /wxauto/README.md: -------------------------------------------------------------------------------- 1 | [![wxauto](https://github.com/cluic/wxauto/blob/WeChat3.9.11/utils/wxauto.png)](https://docs.wxauto.org) 2 | # wxauto (适用PC微信3.9.11.17版本) 3 | 4 | ### 欢迎指出bug,欢迎pull requests 5 | 6 | Windows版本微信客户端自动化,可实现简单的发送、接收微信消息、保存聊天图片 7 | 8 | **3.9.11.17版本微信安装包下载**: 9 | [点击下载](https://github.com/tom-snow/wechat-windows-versions/releases/download/v3.9.11.17/WeChatSetup-3.9.11.17.exe) 10 | 11 | **文档**: 12 | [使用文档](https://docs.wxauto.org) | 13 | [云服务器wxauto部署指南](https://docs.wxauto.org/other/deploy) 14 | 15 | | 环境 | 版本 | 16 | | :----: | :--: | 17 | | OS | [![Windows](https://img.shields.io/badge/Windows-10\|11\|Server2016+-white?logo=windows&logoColor=white)](https://www.microsoft.com/) | 18 | | 微信 | [![Wechat](https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1-3.9.11.X-07c160?logo=wechat&logoColor=white)](https://pan.baidu.com/s/1FvSw0Fk54GGvmQq8xSrNjA?pwd=vsmj) | 19 | | Python | [![Python](https://img.shields.io/badge/Python-3.X-blue?logo=python&logoColor=white)](https://www.python.org/) **(不支持3.7.6和3.8.1)**| 20 | 21 | 22 | 23 | [![Star History Chart](https://api.star-history.com/svg?repos=cluic/wxauto&type=Date)](https://star-history.com/#cluic/wxauto) 24 | 25 | ## 获取wxauto 26 | cmd窗口: 27 | ```shell 28 | pip install wxauto 29 | ``` 30 | python窗口: 31 | ```python 32 | >>> import wxauto 33 | >>> wxauto.VERSION 34 | '3.9.11.17' 35 | >>> wx = wxauto.WeChat() 36 | 初始化成功,获取到已登录窗口:xxx 37 | ``` 38 | 39 | 40 | ## 示例 41 | > [!NOTE] 42 | > 如有问题请先查看[使用文档](https://docs.wxauto.org) 43 | 44 | **请先登录PC微信客户端** 45 | 46 | ```python 47 | from wxauto import * 48 | 49 | 50 | # 获取当前微信客户端 51 | wx = WeChat() 52 | 53 | 54 | # 获取会话列表 55 | wx.GetSessionList() 56 | 57 | # 向某人发送消息(以`文件传输助手`为例) 58 | msg = '你好~' 59 | who = '文件传输助手' 60 | wx.SendMsg(msg, who) # 向`文件传输助手`发送消息:你好~ 61 | 62 | 63 | # 向某人发送文件(以`文件传输助手`为例,发送三个不同类型文件) 64 | files = [ 65 | 'D:/test/wxauto.py', 66 | 'D:/test/pic.png', 67 | 'D:/test/files.rar' 68 | ] 69 | who = '文件传输助手' 70 | wx.SendFiles(filepath=files, who=who) # 向`文件传输助手`发送上述三个文件 71 | 72 | 73 | # 下载当前聊天窗口的聊天记录及图片 74 | msgs = wx.GetAllMessage(savepic=True) # 获取聊天记录,及自动下载图片 75 | ``` 76 | ## 注意事项 77 | 目前还在开发中,测试案例较少,使用过程中可能遇到各种Bug 78 | 79 | ## 交流 80 | 81 | [微信交流群](https://wxauto.loux.cc/docs/intro#-%E4%BA%A4%E6%B5%81) 82 | 83 | ## 最后 84 | 如果对您有帮助,希望可以帮忙点个Star,如果您正在使用这个项目,可以将右上角的 Unwatch 点为 Watching,以便在我更新或修复某些 Bug 后即使收到反馈,感谢您的支持,非常感谢! 85 | 86 | ## 免责声明 87 | 代码仅用于对UIAutomation技术的交流学习使用,禁止用于实际生产项目,请勿用于非法用途和商业用途!如因此产生任何法律纠纷,均与作者无关! 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /wxauto/demo/README.md: -------------------------------------------------------------------------------- 1 | ## 这里放一些使用案例 2 | 3 | 欢迎pr补充 4 | -------------------------------------------------------------------------------- /wxauto/requirements.txt: -------------------------------------------------------------------------------- 1 | uiautomation 2 | Pillow 3 | pywin32 4 | psutil 5 | pyperclip 6 | -------------------------------------------------------------------------------- /wxauto/utils/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/wxauto/utils/alipay.png -------------------------------------------------------------------------------- /wxauto/utils/version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/wxauto/utils/version.png -------------------------------------------------------------------------------- /wxauto/utils/wxauto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/wxauto/utils/wxauto.png -------------------------------------------------------------------------------- /wxauto/utils/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/wxauto/utils/wxpay.png -------------------------------------------------------------------------------- /wxauto/utils/wxqrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zj591227045/WXAUTO-HTTP-API/b25aa5fc578db160f1b8c74e9ac11bddddb2f88d/wxauto/utils/wxqrcode.png -------------------------------------------------------------------------------- /wxauto/wxauto/__init__.py: -------------------------------------------------------------------------------- 1 | from .wxauto import WeChat 2 | from .utils import * 3 | 4 | __version__ = VERSION 5 | 6 | __all__ = [ 7 | 'WeChat', 8 | 'VERSION', 9 | ] 10 | -------------------------------------------------------------------------------- /wxauto/wxauto/color.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import random 3 | import os 4 | 5 | os.system('') 6 | 7 | color_dict = { 8 | 'BLACK': '\x1b[30m', 9 | 'BLUE': '\x1b[34m', 10 | 'CYAN': '\x1b[36m', 11 | 'GREEN': '\x1b[32m', 12 | 'LIGHTBLACK_EX': '\x1b[90m', 13 | 'LIGHTBLUE_EX': '\x1b[94m', 14 | 'LIGHTCYAN_EX': '\x1b[96m', 15 | 'LIGHTGREEN_EX': '\x1b[92m', 16 | 'LIGHTMAGENTA_EX': '\x1b[95m', 17 | 'LIGHTRED_EX': '\x1b[91m', 18 | 'LIGHTWHITE_EX': '\x1b[97m', 19 | 'LIGHTYELLOW_EX': '\x1b[93m', 20 | 'MAGENTA': '\x1b[35m', 21 | 'RED': '\x1b[31m', 22 | 'WHITE': '\x1b[37m', 23 | 'YELLOW': '\x1b[33m' 24 | } 25 | 26 | color_reset = '\x1b[0m' 27 | 28 | class Print: 29 | @staticmethod 30 | def black(text, *args, **kwargs): 31 | print(color_dict['BLACK'] + text + color_reset, *args, **kwargs) 32 | 33 | @staticmethod 34 | def blue(text, *args, **kwargs): 35 | print(color_dict['BLUE'] + text + color_reset, *args, **kwargs) 36 | 37 | @staticmethod 38 | def cyan(text, *args, **kwargs): 39 | print(color_dict['CYAN'] + text + color_reset, *args, **kwargs) 40 | 41 | @staticmethod 42 | def green(text, *args, **kwargs): 43 | print(color_dict['GREEN'] + text + color_reset, *args, **kwargs) 44 | 45 | @staticmethod 46 | def lightblack(text, *args, **kwargs): 47 | print(color_dict['LIGHTBLACK_EX'] + text + color_reset, *args, **kwargs) 48 | 49 | @staticmethod 50 | def lightblue(text, *args, **kwargs): 51 | print(color_dict['LIGHTBLUE_EX'] + text + color_reset, *args, **kwargs) 52 | 53 | @staticmethod 54 | def lightcyan(text, *args, **kwargs): 55 | print(color_dict['LIGHTCYAN_EX'] + text + color_reset, *args, **kwargs) 56 | 57 | @staticmethod 58 | def lightgreen(text, *args, **kwargs): 59 | print(color_dict['LIGHTGREEN_EX'] + text + color_reset, *args, **kwargs) 60 | 61 | @staticmethod 62 | def lightmagenta(text, *args, **kwargs): 63 | print(color_dict['LIGHTMAGENTA_EX'] + text + color_reset, *args, **kwargs) 64 | 65 | @staticmethod 66 | def lightred(text, *args, **kwargs): 67 | print(color_dict['LIGHTRED_EX'] + text + color_reset, *args, **kwargs) 68 | 69 | @staticmethod 70 | def lightwhite(text, *args, **kwargs): 71 | print(color_dict['LIGHTWHITE_EX'] + text + color_reset, *args, **kwargs) 72 | 73 | @staticmethod 74 | def lightyellow(text, *args, **kwargs): 75 | print(color_dict['LIGHTYELLOW_EX'] + text + color_reset, *args, **kwargs) 76 | 77 | @staticmethod 78 | def magenta(text, *args, **kwargs): 79 | print(color_dict['MAGENTA'] + text + color_reset, *args, **kwargs) 80 | 81 | @staticmethod 82 | def red(text, *args, **kwargs): 83 | print(color_dict['RED'] + text + color_reset, *args, **kwargs) 84 | 85 | @staticmethod 86 | def white(text, *args, **kwargs): 87 | print(color_dict['WHITE'] + text + color_reset, *args, **kwargs) 88 | 89 | @staticmethod 90 | def yellow(text, *args, **kwargs): 91 | print(color_dict['YELLOW'] + text + color_reset, *args, **kwargs) 92 | 93 | @staticmethod 94 | def random(text, *args, **kwargs): 95 | print(random.choice(list(color_dict.values())) + text + color_reset, *args, **kwargs) 96 | 97 | 98 | class Input: 99 | @staticmethod 100 | def black(text, *args, **kwargs): 101 | print(color_dict['BLACK'] + text + color_reset, end='') 102 | result = input(*args, **kwargs) 103 | return result 104 | 105 | @staticmethod 106 | def blue(text, *args, **kwargs): 107 | print(color_dict['BLUE'] + text + color_reset, end='') 108 | result = input(*args, **kwargs) 109 | return result 110 | 111 | @staticmethod 112 | def cyan(text, *args, **kwargs): 113 | print(color_dict['CYAN'] + text + color_reset, end='') 114 | result = input(*args, **kwargs) 115 | return result 116 | 117 | @staticmethod 118 | def green(text, *args, **kwargs): 119 | print(color_dict['GREEN'] + text + color_reset, end='') 120 | result = input(*args, **kwargs) 121 | return result 122 | 123 | @staticmethod 124 | def lightblack(text, *args, **kwargs): 125 | print(color_dict['LIGHTBLACK_EX'] + text + color_reset, end='') 126 | result = input(*args, **kwargs) 127 | return result 128 | 129 | @staticmethod 130 | def lightblue(text, *args, **kwargs): 131 | print(color_dict['LIGHTBLUE_EX'] + text + color_reset, end='') 132 | result = input(*args, **kwargs) 133 | return result 134 | 135 | @staticmethod 136 | def lightcyan(text, *args, **kwargs): 137 | print(color_dict['LIGHTCYAN_EX'] + text + color_reset, end='') 138 | result = input(*args, **kwargs) 139 | return result 140 | 141 | @staticmethod 142 | def lightgreen(text, *args, **kwargs): 143 | print(color_dict['LIGHTGREEN_EX'] + text + color_reset, end='') 144 | result = input(*args, **kwargs) 145 | return result 146 | 147 | @staticmethod 148 | def lightmagenta(text, *args, **kwargs): 149 | print(color_dict['LIGHTMAGENTA_EX'] + text + color_reset, end='') 150 | result = input(*args, **kwargs) 151 | return result 152 | 153 | @staticmethod 154 | def lightred(text, *args, **kwargs): 155 | print(color_dict['LIGHTRED_EX'] + text + color_reset, end='') 156 | result = input(*args, **kwargs) 157 | return result 158 | 159 | @staticmethod 160 | def lightwhite(text, *args, **kwargs): 161 | print(color_dict['LIGHTWHITE_EX'] + text + color_reset, end='') 162 | result = input(*args, **kwargs) 163 | return result 164 | 165 | @staticmethod 166 | def lightyellow(text, *args, **kwargs): 167 | print(color_dict['LIGHTYELLOW_EX'] + text + color_reset, end='') 168 | result = input(*args, **kwargs) 169 | return result 170 | 171 | @staticmethod 172 | def magenta(text, *args, **kwargs): 173 | print(color_dict['MAGENTA'] + text + color_reset, end='') 174 | result = input(*args, **kwargs) 175 | return result 176 | 177 | @staticmethod 178 | def red(text, *args, **kwargs): 179 | print(color_dict['RED'] + text + color_reset, end='') 180 | result = input(*args, **kwargs) 181 | return result 182 | 183 | @staticmethod 184 | def white(text, *args, **kwargs): 185 | print(color_dict['WHITE'] + text + color_reset, end='') 186 | result = input(*args, **kwargs) 187 | return result 188 | 189 | @staticmethod 190 | def yellow(text, *args, **kwargs): 191 | print(color_dict['YELLOW'] + text + color_reset, end='') 192 | result = input(*args, **kwargs) 193 | return result 194 | 195 | @staticmethod 196 | def random(text, *args, **kwargs): 197 | print(random.choice(list(color_dict.values())) + text + color_reset, end='') 198 | result = input(*args, **kwargs) 199 | return result 200 | 201 | 202 | class Warnings: 203 | @staticmethod 204 | def black(text, *args, **kwargs): 205 | warnings.warn('\n' + color_dict['BLACK'] + text + color_reset, *args, **kwargs) 206 | 207 | @staticmethod 208 | def blue(text, *args, **kwargs): 209 | warnings.warn('\n' + color_dict['BLUE'] + text + color_reset, *args, **kwargs) 210 | 211 | @staticmethod 212 | def cyan(text, *args, **kwargs): 213 | warnings.warn('\n' + color_dict['CYAN'] + text + color_reset, *args, **kwargs) 214 | 215 | @staticmethod 216 | def green(text, *args, **kwargs): 217 | warnings.warn('\n' + color_dict['GREEN'] + text + color_reset, *args, **kwargs) 218 | 219 | @staticmethod 220 | def lightblack(text, *args, **kwargs): 221 | warnings.warn('\n' + color_dict['LIGHTBLACK_EX'] + text + color_reset, *args, **kwargs) 222 | 223 | @staticmethod 224 | def lightblue(text, *args, **kwargs): 225 | warnings.warn('\n' + color_dict['LIGHTBLUE_EX'] + text + color_reset, *args, **kwargs) 226 | 227 | @staticmethod 228 | def lightcyan(text, *args, **kwargs): 229 | warnings.warn('\n' + color_dict['LIGHTCYAN_EX'] + text + color_reset, *args, **kwargs) 230 | 231 | @staticmethod 232 | def lightgreen(text, *args, **kwargs): 233 | warnings.warn('\n' + color_dict['LIGHTGREEN_EX'] + text + color_reset, *args, **kwargs) 234 | 235 | @staticmethod 236 | def lightmagenta(text, *args, **kwargs): 237 | warnings.warn('\n' + color_dict['LIGHTMAGENTA_EX'] + text + color_reset, *args, **kwargs) 238 | 239 | @staticmethod 240 | def lightred(text, *args, **kwargs): 241 | warnings.warn('\n' + color_dict['LIGHTRED_EX'] + text + color_reset, *args, **kwargs) 242 | 243 | @staticmethod 244 | def lightwhite(text, *args, **kwargs): 245 | warnings.warn('\n' + color_dict['LIGHTWHITE_EX'] + text + color_reset, *args, **kwargs) 246 | 247 | @staticmethod 248 | def lightyellow(text, *args, **kwargs): 249 | warnings.warn('\n' + color_dict['LIGHTYELLOW_EX'] + text + color_reset, *args, **kwargs) 250 | 251 | @staticmethod 252 | def magenta(text, *args, **kwargs): 253 | warnings.warn('\n' + color_dict['MAGENTA'] + text + color_reset, *args, **kwargs) 254 | -------------------------------------------------------------------------------- /wxauto/wxauto/errors.py: -------------------------------------------------------------------------------- 1 | 2 | class TargetNotFoundError(Exception): 3 | pass 4 | 5 | class FriendNotFoundError(Exception): 6 | pass -------------------------------------------------------------------------------- /wxauto/wxauto/languages.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 多语言关键字尚未收集完整,欢迎多多pull requests帮忙补充,感谢 3 | ''' 4 | 5 | MAIN_LANGUAGE = { 6 | # 导航栏 7 | '导航': {'cn': '导航', 'cn_t': '導航', 'en': 'Navigation'}, 8 | '聊天': {'cn': '聊天', 'cn_t': '聊天', 'en': 'Chats'}, 9 | '通讯录': {'cn': '通讯录', 'cn_t': '通訊錄', 'en': 'Contacts'}, 10 | '收藏': {'cn': '收藏', 'cn_t': '收藏', 'en': 'Favorites'}, 11 | '聊天文件': {'cn': '聊天文件', 'cn_t': '聊天室檔案', 'en': 'Chat Files'}, 12 | '朋友圈': {'cn': '朋友圈', 'cn_t': '朋友圈', 'en': 'Moments'}, 13 | '小程序面板': {'cn': '小程序面板', 'cn_t': '小程式面板', 'en': 'Mini Programs Panel'}, 14 | '手机': {'cn': '手机', 'cn_t': '手機', 'en': 'Phone'}, 15 | '设置及其他': {'cn': '设置及其他', 'cn_t': '設定與其他', 'en': 'Settings and Others'}, 16 | 17 | # 好友列表栏 18 | '搜索': {'cn': '搜索', 'cn_t': '搜尋', 'en': 'Search'}, 19 | '发起群聊': {'cn': '发起群聊', 'cn_t': '建立群組', 'en': 'Start Group Chat'}, 20 | '文件传输助手': {'cn': '文件传输助手', 'cn_t': '檔案傳輸', 'en': 'File Transfer'}, 21 | '订阅号': {'cn': '订阅号', 'cn_t': '官方賬號', 'en': 'Subscriptions'}, 22 | '消息': {'cn': '消息', 'cn_t': '消息', 'en': ''}, 23 | 24 | # 右上角工具栏 25 | '置顶': {'cn': '置顶', 'cn_t': '置頂', 'en': 'Sticky on Top'}, 26 | '最小化': {'cn': '最小化', 'cn_t': '最小化', 'en': 'Minimize'}, 27 | '最大化': {'cn': '最大化', 'cn_t': '最大化', 'en': ''}, 28 | '关闭': {'cn': '关闭', 'cn_t': '關閉', 'en': ''}, 29 | 30 | # 聊天框 31 | '聊天信息': {'cn': '聊天信息', 'cn_t': '聊天資訊', 'en': 'Chat Info'}, 32 | '表情': {'cn': '表情', 'cn_t': '貼圖', 'en': 'Sticker'}, 33 | '发送文件': {'cn': '发送文件', 'cn_t': '傳送檔案', 'en': 'Send File'}, 34 | '截图': {'cn': '截图', 'cn_t': '截圖', 'en': 'Screenshot'}, 35 | '聊天记录': {'cn': '聊天记录', 'cn_t': '聊天記錄', 'en': 'Chat History'}, 36 | '语音聊天': {'cn': '语音聊天', 'cn_t': '語音通話', 'en': 'Voice Call'}, 37 | '视频聊天': {'cn': '视频聊天', 'cn_t': '視頻通話', 'en': 'Video Call'}, 38 | '发送': {'cn': '发送', 'cn_t': '傳送', 'en': 'Send'}, 39 | '输入': {'cn': '输入', 'cn_t': '輸入', 'en': 'Enter'}, 40 | 41 | # 消息类型 42 | '链接': {'cn': '链接', 'cn_t': '鏈接', 'en': 'Link'}, 43 | '视频': {'cn': '视频', 'cn_t': '視頻', 'en': 'Video'}, 44 | '图片': {'cn': '图片', 'cn_t': '圖片', 'en': 'Photo'}, 45 | '文件': {'cn': '文件', 'cn_t': '文件', 'en': 'File'}, 46 | '语音': {'cn': '语音', 'cn_t': '語音', 'en': 'Voice'}, 47 | '查看更多消息': {'cn': '查看更多消息', 'cn_t': '', 'en': ''}, 48 | 49 | 50 | # 选项 51 | '复制': {'cn': '复制', 'cn_t': '複製', 'en': 'Copy'}, 52 | '转发': {'cn': '转发', 'cn_t': '轉發', 'en': 'Forward'}, 53 | '收藏': {'cn': '收藏', 'cn_t': '收藏', 'en': 'Add to Favorites'}, 54 | '撤回': {'cn': '撤回', 'cn_t': '撤回', 'en': 'Recall'}, 55 | '引用': {'cn': '引用', 'cn_t': '引用', 'en': 'Quote'}, 56 | 57 | # 其他 58 | '联系人': {'cn': '联系人', 'cn_t': '聯絡人', 'en': 'Contacts'}, 59 | } 60 | 61 | 62 | 63 | 64 | IMAGE_LANGUAGE = { 65 | '上一张': {'cn': '上一张', 'cn_t': '上一張', 'en': 'Previous'}, 66 | '下一张': {'cn': '下一张', 'cn_t': '下一張', 'en': 'Next'}, 67 | '预览': {'cn': '预览', 'cn_t': '預覽', 'en': 'Preview'}, 68 | '放大': {'cn': '放大', 'cn_t': '放大', 'en': 'Zoom'}, 69 | '缩小': {'cn': '缩小', 'cn_t': '縮小', 'en': 'Shrink'}, 70 | '图片原始大小': {'cn': '图片原始大小', 'cn_t': '圖片原始大小', 'en': 'Original image size'}, 71 | '旋转': {'cn': '旋转', 'cn_t': '旋轉', 'en': 'Rotate'}, 72 | '编辑': {'cn': '编辑', 'cn_t': '編輯', 'en': 'Edit'}, 73 | '翻译': {'cn': '翻译', 'cn_t': '翻譯', 'en': 'Translate'}, 74 | '提取文字': {'cn': '提取文字', 'cn_t': '提取文字', 'en': 'Extract Text'}, 75 | '识别图中二维码': {'cn': '识别图中二维码', 'cn_t': '識别圖中QR Code', 'en': 'Extract QR Code'}, 76 | '另存为...': {'cn': '另存为...', 'cn_t': '另存爲...', 'en': 'Save as…'}, 77 | '更多': {'cn': '更多', 'cn_t': '更多', 'en': 'More'}, 78 | '最小化': {'cn': '最小化', 'cn_t': '最小化', 'en': 'Minimize'}, 79 | '最大化': {'cn': '最大化', 'cn_t': '最大化', 'en': 'Maximize'}, 80 | '关闭': {'cn': '关闭', 'cn_t': '關閉', 'en': 'Close'}, 81 | '': {'cn': '', 'cn_t': '', 'en': ''}} 82 | 83 | FILE_LANGUAGE = { 84 | '最小化': {'cn': '最小化', 'cn_t': '最小化', 'en': 'Minimize'}, 85 | '最大化': {'cn': '最大化', 'cn_t': '最大化', 'en': 'Maximize'}, 86 | '关闭': {'cn': '关闭', 'cn_t': '關閉', 'en': 'Close'}, 87 | '全部': {'cn': '全部', 'cn_t': '全部', 'en': 'All'}, 88 | '最近使用': {'cn': '最近使用', 'cn_t': '最近使用', 'en': 'Recent'}, 89 | '发送者': {'cn': '发送者', 'cn_t': '發送者', 'en': 'Sender'}, 90 | '聊天': {'cn': '聊天', 'cn_t': '聊天', 'en': 'Chat'}, 91 | '类型': {'cn': '类型', 'cn_t': '類型', 'en': 'Type'}, 92 | '按最新时间': {'cn': '按最新时间', 'cn_t': '按最新時間', 'en': 'Sort by Newest'}, 93 | '按最旧时间': {'cn': '按最旧时间', 'cn_t': '按最舊時間', 'en': 'Sort by Oldest'}, 94 | '按从大到小': {'cn': '按从大到小', 'cn_t': '按從大到小', 'en': 'Sort by Largest'}, 95 | '按从小到大': {'cn': '按从小到大', 'cn_t': '按從小到大', 'en': 'Sort by Smallest'}, 96 | '': {'cn': '', 'cn_t': '', 'en': ''} 97 | } 98 | 99 | WARNING = { 100 | '版本不一致': { 101 | 'cn': '当前微信客户端版本为{},与当前库版本{}不一致,可能会导致部分功能无法正常使用,请注意判断', 102 | 'cn_t': '當前微信客戶端版本為{},與當前庫版本{}不一致,可能會導致部分功能無法正常使用,請注意判斷', 103 | 'en': 'The current WeChat client version is {}, which is inconsistent with the current library version {}, which may cause some functions to fail to work properly. Please pay attention to judgment' 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /wxauto/wxauto/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from . import uiautomation as uia 3 | from PIL import ImageGrab 4 | import win32clipboard 5 | import win32process 6 | import win32gui 7 | import win32api 8 | import win32con 9 | import pyperclip 10 | import ctypes 11 | import psutil 12 | import shutil 13 | import winreg 14 | import logging 15 | import time 16 | import os 17 | import re 18 | 19 | VERSION = "3.9.11.17" 20 | 21 | def set_cursor_pos(x, y): 22 | win32api.SetCursorPos((x, y)) 23 | 24 | def Click(rect): 25 | x = (rect.left + rect.right) // 2 26 | y = (rect.top + rect.bottom) // 2 27 | set_cursor_pos(x, y) 28 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) 29 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) 30 | 31 | def GetPathByHwnd(hwnd): 32 | try: 33 | thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd) 34 | process = psutil.Process(process_id) 35 | return process.exe() 36 | except Exception as e: 37 | print(f"Error: {e}") 38 | return None 39 | 40 | def GetVersionByPath(file_path): 41 | try: 42 | info = win32api.GetFileVersionInfo(file_path, '\\') 43 | version = "{}.{}.{}.{}".format(win32api.HIWORD(info['FileVersionMS']), 44 | win32api.LOWORD(info['FileVersionMS']), 45 | win32api.HIWORD(info['FileVersionLS']), 46 | win32api.LOWORD(info['FileVersionLS'])) 47 | except: 48 | version = None 49 | return version 50 | 51 | 52 | def IsRedPixel(uicontrol): 53 | rect = uicontrol.BoundingRectangle 54 | bbox = (rect.left, rect.top, rect.right, rect.bottom) 55 | img = ImageGrab.grab(bbox=bbox, all_screens=True) 56 | return any(p[0] > p[1] and p[0] > p[2] for p in img.getdata()) 57 | 58 | class DROPFILES(ctypes.Structure): 59 | _fields_ = [ 60 | ("pFiles", ctypes.c_uint), 61 | ("x", ctypes.c_long), 62 | ("y", ctypes.c_long), 63 | ("fNC", ctypes.c_int), 64 | ("fWide", ctypes.c_bool), 65 | ] 66 | 67 | pDropFiles = DROPFILES() 68 | pDropFiles.pFiles = ctypes.sizeof(DROPFILES) 69 | pDropFiles.fWide = True 70 | matedata = bytes(pDropFiles) 71 | 72 | def SetClipboardText(text: str): 73 | pyperclip.copy(text) 74 | # if not isinstance(text, str): 75 | # raise TypeError(f"参数类型必须为str --> {text}") 76 | # t0 = time.time() 77 | # while True: 78 | # if time.time() - t0 > 10: 79 | # raise TimeoutError(f"设置剪贴板超时! --> {text}") 80 | # try: 81 | # win32clipboard.OpenClipboard() 82 | # win32clipboard.EmptyClipboard() 83 | # win32clipboard.SetClipboardData(win32con.CF_UNICODETEXT, text) 84 | # break 85 | # except: 86 | # pass 87 | # finally: 88 | # try: 89 | # win32clipboard.CloseClipboard() 90 | # except: 91 | # pass 92 | 93 | try: 94 | from anytree import Node, RenderTree 95 | 96 | def PrintAllControlTree(ele): 97 | def findall(ele, n=0, node=None): 98 | nn = '\n' 99 | nodename = f"[{ele.ControlTypeName} {n}](\"{ele.ClassName}\", \"{ele.Name.replace(nn, '')}\", \"{''.join([str(i) for i in ele.GetRuntimeId()])}\")" 100 | if not node: 101 | node1 = Node(nodename) 102 | else: 103 | node1 = Node(nodename, parent=node) 104 | eles = ele.GetChildren() 105 | for ele1 in eles: 106 | findall(ele1, n+1, node1) 107 | return node1 108 | tree = RenderTree(findall(ele)) 109 | for pre, fill, node in tree: 110 | print(f"{pre}{node.name}") 111 | except: 112 | pass 113 | 114 | def GetAllControlList(ele): 115 | def findall(ele, n=0, text=[]): 116 | if ele.Name: 117 | text.append(ele) 118 | eles = ele.GetChildren() 119 | for ele1 in eles: 120 | text = findall(ele1, n+1, text) 121 | return text 122 | text_list = findall(ele) 123 | return text_list 124 | 125 | def GetAllControl(ele): 126 | def findall(ele, n=0, controls=[]): 127 | # if ele.Name: 128 | controls.append(ele) 129 | eles = ele.GetChildren() 130 | for ele1 in eles: 131 | controls = findall(ele1, n+1, controls) 132 | return controls 133 | text_list = findall(ele)[1:] 134 | return text_list 135 | 136 | def SetClipboardFiles(paths): 137 | for file in paths: 138 | if not os.path.exists(file): 139 | raise FileNotFoundError(f"file ({file}) not exists!") 140 | files = ("\0".join(paths)).replace("/", "\\") 141 | data = files.encode("U16")[2:]+b"\0\0" 142 | t0 = time.time() 143 | while True: 144 | if time.time() - t0 > 10: 145 | raise TimeoutError(f"设置剪贴板文件超时! --> {paths}") 146 | try: 147 | win32clipboard.OpenClipboard() 148 | win32clipboard.EmptyClipboard() 149 | win32clipboard.SetClipboardData(win32clipboard.CF_HDROP, matedata+data) 150 | break 151 | except: 152 | pass 153 | finally: 154 | try: 155 | win32clipboard.CloseClipboard() 156 | except: 157 | pass 158 | 159 | def PasteFile(folder): 160 | folder = os.path.realpath(folder) 161 | if not os.path.exists(folder): 162 | os.makedirs(folder) 163 | 164 | t0 = time.time() 165 | while True: 166 | if time.time() - t0 > 10: 167 | raise TimeoutError(f"读取剪贴板文件超时!") 168 | try: 169 | win32clipboard.OpenClipboard() 170 | if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP): 171 | files = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP) 172 | for file in files: 173 | filename = os.path.basename(file) 174 | dest_file = os.path.join(folder, filename) 175 | shutil.copy2(file, dest_file) 176 | return True 177 | else: 178 | print("剪贴板中没有文件") 179 | return False 180 | except: 181 | pass 182 | finally: 183 | win32clipboard.CloseClipboard() 184 | 185 | def GetText(HWND): 186 | length = win32gui.SendMessage(HWND, win32con.WM_GETTEXTLENGTH)*2 187 | buffer = win32gui.PyMakeBuffer(length) 188 | win32api.SendMessage(HWND, win32con.WM_GETTEXT, length, buffer) 189 | address, length_ = win32gui.PyGetBufferAddressAndLen(buffer[:-1]) 190 | text = win32gui.PyGetString(address, length_)[:int(length/2)] 191 | buffer.release() 192 | return text 193 | 194 | def GetAllWindowExs(HWND): 195 | if not HWND: 196 | return 197 | handles = [] 198 | win32gui.EnumChildWindows( 199 | HWND, lambda hwnd, param: param.append([hwnd, win32gui.GetClassName(hwnd), GetText(hwnd)]), handles) 200 | return handles 201 | 202 | def FindWindow(classname=None, name=None) -> int: 203 | return win32gui.FindWindow(classname, name) 204 | 205 | def FindWinEx(HWND, classname=None, name=None) -> list: 206 | hwnds_classname = [] 207 | hwnds_name = [] 208 | def find_classname(hwnd, classname): 209 | classname_ = win32gui.GetClassName(hwnd) 210 | if classname_ == classname: 211 | if hwnd not in hwnds_classname: 212 | hwnds_classname.append(hwnd) 213 | def find_name(hwnd, name): 214 | name_ = GetText(hwnd) 215 | if name in name_: 216 | if hwnd not in hwnds_name: 217 | hwnds_name.append(hwnd) 218 | if classname: 219 | win32gui.EnumChildWindows(HWND, find_classname, classname) 220 | if name: 221 | win32gui.EnumChildWindows(HWND, find_name, name) 222 | if classname and name: 223 | hwnds = [hwnd for hwnd in hwnds_classname if hwnd in hwnds_name] 224 | else: 225 | hwnds = hwnds_classname + hwnds_name 226 | return hwnds 227 | 228 | def ClipboardFormats(unit=0, *units): 229 | units = list(units) 230 | win32clipboard.OpenClipboard() 231 | u = win32clipboard.EnumClipboardFormats(unit) 232 | win32clipboard.CloseClipboard() 233 | units.append(u) 234 | if u: 235 | units = ClipboardFormats(u, *units) 236 | return units 237 | 238 | def ReadClipboardData(): 239 | Dict = {} 240 | for i in ClipboardFormats(): 241 | if i == 0: 242 | continue 243 | win32clipboard.OpenClipboard() 244 | try: 245 | filenames = win32clipboard.GetClipboardData(i) 246 | win32clipboard.CloseClipboard() 247 | except: 248 | win32clipboard.CloseClipboard() 249 | raise ValueError 250 | Dict[str(i)] = filenames 251 | return Dict 252 | 253 | def ParseWeChatTime(time_str): 254 | """ 255 | 时间格式转换函数 256 | 257 | Args: 258 | time_str: 输入的时间字符串 259 | 260 | Returns: 261 | 转换后的时间字符串 262 | """ 263 | 264 | match = re.match(r'^(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$', time_str) 265 | if match: 266 | month, day, hour, minute, second = match.groups() 267 | current_year = datetime.now().year 268 | return datetime(current_year, int(month), int(day), int(hour), int(minute), int(second)).strftime('%Y-%m-%d %H:%M:%S') 269 | 270 | match = re.match(r'^(\d{1,2}):(\d{1,2})$', time_str) 271 | if match: 272 | hour, minute = match.groups() 273 | return datetime.now().strftime('%Y-%m-%d') + f' {hour}:{minute}:00' 274 | 275 | match = re.match(r'^昨天 (\d{1,2}):(\d{1,2})$', time_str) 276 | if match: 277 | hour, minute = match.groups() 278 | yesterday = datetime.now() - timedelta(days=1) 279 | return yesterday.strftime('%Y-%m-%d') + f' {hour}:{minute}:00' 280 | 281 | match = re.match(r'^星期([一二三四五六日]) (\d{1,2}):(\d{1,2})$', time_str) 282 | if match: 283 | weekday, hour, minute = match.groups() 284 | weekday_num = ['一', '二', '三', '四', '五', '六', '日'].index(weekday) 285 | today_weekday = datetime.now().weekday() 286 | delta_days = (today_weekday - weekday_num) % 7 287 | target_day = datetime.now() - timedelta(days=delta_days) 288 | return target_day.strftime('%Y-%m-%d') + f' {hour}:{minute}:00' 289 | 290 | match = re.match(r'^(\d{4})年(\d{1,2})月(\d{1,2})日 (\d{1,2}):(\d{1,2})$', time_str) 291 | if match: 292 | year, month, day, hour, minute = match.groups() 293 | return datetime(*[int(i) for i in [year, month, day, hour, minute]]).strftime('%Y-%m-%d %H:%M:%S') 294 | 295 | 296 | def RollIntoView(win, ele, equal=False): 297 | if ele.BoundingRectangle.top < win.BoundingRectangle.top: 298 | # 上滚动 299 | while True: 300 | win.WheelUp(wheelTimes=1, waitTime=0.1) 301 | if ele.BoundingRectangle.top >= win.BoundingRectangle.top: 302 | break 303 | 304 | elif ele.BoundingRectangle.bottom >= win.BoundingRectangle.bottom: 305 | # 下滚动 306 | while True: 307 | win.WheelDown(wheelTimes=1, waitTime=0.1) 308 | if equal: 309 | if ele.BoundingRectangle.bottom <= win.BoundingRectangle.bottom: 310 | break 311 | else: 312 | if ele.BoundingRectangle.bottom < win.BoundingRectangle.bottom: 313 | break 314 | 315 | wxlog = logging.getLogger('wxauto') 316 | wxlog.setLevel(logging.DEBUG) 317 | console_handler = logging.StreamHandler() 318 | console_handler.setLevel(logging.DEBUG) 319 | formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s (%(filename)s:%(lineno)d): %(message)s') 320 | console_handler.setFormatter(formatter) 321 | wxlog.addHandler(console_handler) 322 | wxlog.propagate = False 323 | 324 | def set_debug(debug: bool): 325 | if debug: 326 | wxlog.setLevel(logging.DEBUG) 327 | console_handler.setLevel(logging.DEBUG) 328 | else: 329 | wxlog.setLevel(logging.INFO) 330 | console_handler.setLevel(logging.INFO) 331 | -------------------------------------------------------------------------------- /wxauto_import.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxauto导入辅助模块 3 | 用于确保wxauto库能够被正确导入 4 | """ 5 | 6 | import os 7 | import sys 8 | import importlib 9 | import logging 10 | 11 | # 配置日志 12 | logger = logging.getLogger(__name__) 13 | 14 | def ensure_wxauto_importable(): 15 | """ 16 | 确保wxauto库能够被正确导入 17 | 18 | Returns: 19 | bool: 是否成功导入wxauto库 20 | """ 21 | # 记录当前Python路径 22 | logger.info(f"当前Python路径: {sys.path}") 23 | 24 | # 获取应用根目录 25 | if getattr(sys, 'frozen', False): 26 | # 如果是打包后的环境 27 | app_root = os.path.dirname(sys.executable) 28 | logger.info(f"检测到打包环境,应用根目录: {app_root}") 29 | 30 | # 在打包环境中,确保_MEIPASS目录也在Python路径中 31 | meipass = getattr(sys, '_MEIPASS', None) 32 | if meipass: 33 | logger.info(f"PyInstaller _MEIPASS目录: {meipass}") 34 | if meipass not in sys.path: 35 | sys.path.insert(0, meipass) 36 | logger.info(f"已将_MEIPASS目录添加到Python路径: {meipass}") 37 | else: 38 | # 如果是开发环境 39 | app_root = os.path.dirname(os.path.abspath(__file__)) 40 | logger.info(f"检测到开发环境,应用根目录: {app_root}") 41 | 42 | # 确保应用根目录在Python路径中 43 | if app_root not in sys.path: 44 | sys.path.insert(0, app_root) 45 | logger.info(f"已将应用根目录添加到Python路径: {app_root}") 46 | 47 | # 尝试多种可能的wxauto路径 48 | possible_paths = [ 49 | os.path.join(app_root, "wxauto"), # 标准路径 50 | os.path.join(app_root, "app", "wxauto"), # 可能的子目录 51 | ] 52 | 53 | # 如果是打包环境,添加更多可能的路径 54 | if getattr(sys, 'frozen', False): 55 | meipass = getattr(sys, '_MEIPASS', None) 56 | if meipass: 57 | possible_paths.extend([ 58 | os.path.join(meipass, "wxauto"), # PyInstaller临时目录中的wxauto 59 | os.path.join(meipass, "app", "wxauto"), # PyInstaller临时目录中的app/wxauto 60 | ]) 61 | 62 | # 记录所有可能的路径 63 | logger.info(f"可能的wxauto路径: {possible_paths}") 64 | 65 | # 尝试从每个路径导入 66 | for wxauto_path in possible_paths: 67 | if os.path.exists(wxauto_path) and os.path.isdir(wxauto_path): 68 | logger.info(f"找到wxauto路径: {wxauto_path}") 69 | 70 | # 检查wxauto路径下是否有wxauto子目录 71 | wxauto_inner_path = os.path.join(wxauto_path, "wxauto") 72 | if os.path.exists(wxauto_inner_path) and os.path.isdir(wxauto_inner_path): 73 | logger.info(f"找到wxauto内部目录: {wxauto_inner_path}") 74 | 75 | # 将wxauto/wxauto目录添加到路径 76 | if wxauto_inner_path not in sys.path: 77 | sys.path.insert(0, wxauto_inner_path) 78 | logger.info(f"已将wxauto/wxauto目录添加到Python路径: {wxauto_inner_path}") 79 | 80 | # 将wxauto目录添加到路径 81 | if wxauto_path not in sys.path: 82 | sys.path.insert(0, wxauto_path) 83 | logger.info(f"已将wxauto目录添加到Python路径: {wxauto_path}") 84 | 85 | # 尝试导入 86 | try: 87 | import wxauto 88 | logger.info(f"成功从路径导入wxauto: {wxauto_path}") 89 | return True 90 | except ImportError as e: 91 | logger.warning(f"从路径 {wxauto_path} 导入wxauto失败: {str(e)}") 92 | # 继续尝试下一个路径 93 | 94 | # 如果所有路径都失败,尝试直接导入 95 | try: 96 | logger.info("所有路径尝试失败,尝试直接导入wxauto") 97 | import wxauto 98 | logger.info("成功直接导入wxauto") 99 | return True 100 | except ImportError as e: 101 | logger.error(f"wxauto导入失败: {str(e)}") 102 | return False 103 | 104 | def get_wxauto(): 105 | """ 106 | 获取wxauto模块 107 | 108 | Returns: 109 | module: wxauto模块,如果导入失败则返回None 110 | """ 111 | if ensure_wxauto_importable(): 112 | try: 113 | import wxauto 114 | return wxauto 115 | except ImportError: 116 | logger.error("无法导入wxauto模块") 117 | return None 118 | return None 119 | 120 | if __name__ == "__main__": 121 | # 配置日志 122 | logging.basicConfig( 123 | level=logging.INFO, 124 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 125 | ) 126 | 127 | # 确保wxauto库能够被正确导入 128 | if ensure_wxauto_importable(): 129 | print("wxauto库已成功导入") 130 | sys.exit(0) 131 | else: 132 | print("wxauto库导入失败") 133 | sys.exit(1) 134 | --------------------------------------------------------------------------------