├── .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 | 
6 | 
7 | 
8 | 
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 |

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 | [](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 | [](https://www.microsoft.com/) |
18 | | 微信 | [](https://pan.baidu.com/s/1FvSw0Fk54GGvmQq8xSrNjA?pwd=vsmj) |
19 | | Python | [](https://www.python.org/) **(不支持3.7.6和3.8.1)**|
20 |
21 |
22 |
23 | [](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 |
--------------------------------------------------------------------------------