1. 项目缘起:一个被微信消息淹没的桌面
不知道你有没有过这样的体验:在Windows电脑上专注写代码、看文档或者处理一个复杂的表格时,手机微信的提示音此起彼伏。每次拿起手机,可能只是一个群里的闲聊,或者一条无关紧要的公众号推送,但你的工作流已经被打断了。更麻烦的是,有时候手机放在一边充电,或者开了静音,结果错过了真正重要的消息。
我之前就长期被这个问题困扰。作为一个重度依赖电脑工作的人,我希望所有重要的信息都能汇聚到我的桌面操作中心——Windows通知中心。这样,我既能及时看到消息预览,又不用频繁切换设备。更重要的是,如果能直接在通知上点击回复,那就完美了,可以省去解锁手机、打开微信、找到联系人、打字这一系列繁琐操作。
市面上有一些现成的工具,比如一些安卓模拟器的通知同步功能,或者一些第三方商业软件。但它们要么太笨重(开个模拟器就为了收通知?),要么有隐私顾虑(你的聊天记录要经过别人的服务器?),要么功能不满足(只能看,不能回)。所以,我决定自己动手,用Python造一个轮子:一个轻量、本地化、支持快捷回复的微信消息转发到Windows通知中心的工具。
这个项目的核心价值在于:将移动端的即时通讯体验无缝整合到桌面工作流中,实现消息的集中管理与高效处理,同时完全掌控自己的数据。它非常适合开发者、远程工作者、多任务处理者,或者任何希望提升桌面端信息处理效率的人。
2. 技术栈选型与核心原理拆解
要实现这个功能,我们需要解决几个核心问题:1. 如何从手机获取微信消息?2. 如何发送到Windows通知中心?3. 如何实现快捷回复并送回手机?这涉及到移动端抓取、桌面端交互和网络通信三个层面。
2.1 消息获取:为什么选择“微信机器人”而非Hook
获取微信消息,最直接的想法可能是去逆向安卓微信的协议,但这门槛高、风险大,且极易因微信更新而失效。更可行的方案是使用“微信机器人”框架。这类框架通常基于无障碍服务(AccessibilityService)或设备控制协议(如ADB),模拟用户操作或监听系统通知栏来获取消息。
我最终选择了itchat的继任者之一,结合wxauto或uiautomator2的思路,但进行大幅改造。为什么不直接用itchat?因为itchat基于Web协议,早已被微信官方封杀,完全不可用。现在的可行路径是在电脑上运行一个安卓模拟器(如夜神、MuMu),在模拟器里登录微信,然后通过Python脚本控制模拟器内的微信。
这里的关键技术点是Android Debug Bridge (ADB)。ADB允许我们从电脑上对安卓设备(或模拟器)发送指令、获取屏幕内容、监听日志等。我们的Python脚本将通过ADB与模拟器内的微信交互。
核心原理链如下:
- 监听通知:通过ADB命令
adb shell dumpsys notification可以获取模拟器当前的所有通知列表,从中解析出来自微信的通知,提取发送人、消息内容等信息。这是最稳定、对微信侵入性最低的方式,因为它只是读取系统已经产生的通知。 - 模拟点击与抓取(备选方案):如果通知内容不全(比如只显示“一条新消息”),则需要更深入的操作。我们可以用ADB模拟点击打开微信,然后使用
uiautomator2这个Python库来获取当前聊天窗口的UI控件树,从中精准定位并提取消息文本和联系人。uiautomator2提供了比原生ADB更强大的UI自动化能力。 - 消息去重:需要维护一个简单的消息ID或时间戳缓存,避免同一条消息被重复抓取并推送。
注意:直接解析通知栏是最优方案。模拟点击打开微信应用虽然能获取更完整的信息流,但会干扰你实际使用模拟器内的微信,并且更耗资源。优先尝试从通知栏提取足够的信息(通常包含发送人和消息前一段内容)。
2.2 通知推送:Windows原生接口 vs. 第三方库
将消息推送到Windows通知中心,同样有多个选择。最正统的方式是使用Windows Toast Notification (Windows.Runtime)API,这是UWP应用的标准方式,效果原生、体验好。但它的缺点是通常需要打包成UWP应用,或者以特定的方式从Python调用,配置较为复杂。
为了快速实现和跨版本兼容(兼容Win10和Win11),我选择了win10toast和plyer库的组合,并进行了功能增强。win10toast库简单易用,但其默认的通知不支持按钮(快捷回复)。而plyer库提供了一个跨平台的抽象层,在Windows后端它调用的是wintoast,这个库支持添加按钮操作。
核心代码逻辑:
from plyer import notification import json def send_windows_notification(title, message, conversation_id): # 构建通知参数 toast_args = { 'title': title, 'message': message, 'app_name': '微信消息助手', 'timeout': 10, # 通知停留时间 'toast_flags': 0, } # 添加快捷回复按钮(关键步骤) # 我们需要将回复动作和对应的会话ID编码传递给回调处理函数 actions = [] quick_replies = ["收到", "稍后回复", "好的"] # 预定义的快捷回复词 for reply in quick_replies: # 将动作数据序列化,通常包含回复文本和会话标识 action_data = json.dumps({'type': 'quick_reply', 'text': reply, 'cid': conversation_id}) # plyer/wintoast 允许通过 `actions` 参数添加按钮,但需要正确设置回调 # 注意:plyer的API可能不直接暴露此功能,可能需要深入其Windows后端或使用winrt库直接操作 actions.append((reply, action_data)) # 这是一个概念性结构,实际API调用需调整 # 实际上,更直接的方式是使用 `winrt` 库来创建包含按钮的Toast # 以下是简化后的思路 notification.notify(**toast_args) # 真正的按钮绑定需要更底层的Windows API调用,下文会详细展开。实际上,plyer对高级Toast特性的支持有限。为了实现可点击的按钮并触发Python回调,我们必须直接使用winrt库来调用Windows Runtime API。这是本项目的技术难点之一。
2.3 快捷回复:闭环交互的设计与实现
快捷回复的难点在于“闭环”。Windows通知按钮被点击后,系统需要知道执行什么代码,并且这段代码需要将回复文本“送回”到模拟器内的微信中。
实现闭环的步骤:
- 注册应用与COM组件:为了让Windows能够激活我们的Python脚本,我们需要将脚本“注册”为一个COM服务器,或者打包成一个具有明确AppUserModelID (AUMID) 的快捷方式。这里我们采用一种更轻量级的方法:在创建Toast通知时,指定一个
launch参数,这个参数是一个协议URL(例如wechatreply://reply?cid=xxx&text=预设文本)。 - 协议处理器:我们在系统中注册一个自定义协议(如
wechatreply),并将其关联到我们的Python脚本。当Toast按钮被点击,系统会尝试打开这个URL,从而启动我们的Python脚本,并附带回复参数。 - 脚本处理与回传:Python脚本被启动后,解析URL中的参数(会话ID和回复文本),然后通过ADB命令,将回复文本输入到模拟器微信的对应聊天输入框中,并模拟点击发送。
- 定位输入框:使用
uiautomator2的定位功能,找到当前聊天窗口的输入框控件(通常其resource-id包含com.tencent.mm:id/bXX之类的模式)。 - 输入文本:
d(text="输入框").set_text("回复内容") - 模拟发送:找到发送按钮并点击
d(resourceId="com.tencent.mm:id/bXX").click()
- 定位输入框:使用
这个流程涉及桌面端、移动端和系统级的集成,是项目中最精巧的部分。
3. 手把手搭建开发与运行环境
理论讲完了,我们开始实战。请严格按照步骤操作,避免环境问题导致后续失败。
3.1 第一步:安卓模拟器准备与ADB连接
- 安装模拟器:推荐使用夜神模拟器或MuMu模拟器。它们对ADB的支持比较友好。安装后,在设置中开启“Root权限”(部分操作需要)和“允许ADB调试”。
- 配置模拟器ADB端口:夜神模拟器的默认ADB端口通常是
62001,MuMu是7555。你需要找到模拟器安装目录下的nox_adb.exe(夜神)或使用你自己的ADB工具连接指定端口。 - 连接测试:打开命令行,使用以下命令连接:
连接成功后,使用# 如果你使用模拟器自带的adb cd "D:\Program Files\Nox\bin" # 切换到夜神安装目录的bin文件夹 nox_adb connect 127.0.0.1:62001 # 如果你使用系统全局的adb(需要先安装Android SDK Platform-Tools) adb connect 127.0.0.1:62001adb devices命令应能看到类似127.0.0.1:62001 device的设备。 - 安装微信:在模拟器的应用市场里安装微信,并登录你的账号(建议使用小号,避免安全风险)。
3.2 第二步:Python环境与核心库安装
建议使用Python 3.8及以上版本,并创建虚拟环境。
# 创建并激活虚拟环境(可选但推荐) python -m venv wechat_notify_env wechat_notify_env\Scripts\activate # Windows # 安装核心库 pip install uiautomator2 # 用于安卓UI自动化 pip install pillow # 可能需要用于截图处理 pip install plyer # 跨平台通知(基础功能) # 安装winrt库,用于创建高级Windows Toast通知 pip install winrtuiautomator2安装后,还需要在模拟器中初始化服务:
python -m uiautomator2 init执行此命令会自动向连接的模拟器设备安装一个守护应用atx-agent。
3.3 第三步:获取微信通知栏信息
我们首先实现最核心的消息抓取功能。创建一个message_grabber.py文件。
import subprocess import re import time from datetime import datetime def get_android_notifications(adb_path="adb"): """ 通过ADB获取安卓设备的通知列表,并解析出微信通知。 返回列表,每个元素是包含`package`, `title`, `text`, `time`的字典。 """ try: # 执行adb shell dumpsys notification命令 result = subprocess.run( [adb_path, "shell", "dumpsys", "notification", "--noredact"], capture_output=True, text=True, encoding='utf-8', errors='ignore', timeout=5 ) output = result.stdout except subprocess.TimeoutExpired: print("ADB命令执行超时") return [] except Exception as e: print(f"执行ADB命令出错: {e}") return [] notifications = [] current_pkg = None current_title = None current_text = None # 这是一个简化的解析器。实际输出格式复杂,需要更健壮的解析。 lines = output.split('\n') i = 0 while i < len(lines): line = lines[i] # 查找通知条目开始的标志 if 'NotificationRecord(' in line and 'pkg=' in line: # 提取包名,例如 pkg=com.tencent.mm pkg_match = re.search(r'pkg=([^\s]+)', line) current_pkg = pkg_match.group(1) if pkg_match else None # 向前查找title和text title, text = None, None for j in range(i, min(i+20, len(lines))): # 在附近行查找 if 'android.title=' in lines[j]: title_match = re.search(r'android.title=\s*"([^"]*)"', lines[j]) if title_match: title = title_match.group(1) if 'android.text=' in lines[j]: text_match = re.search(r'android.text=\s*"([^"]*)"', lines[j]) if text_match: text = text_match.group(1) if title and text: break if current_pkg == 'com.tencent.mm' and (title or text): # 微信包名 # 简单的去重:如果上一条通知的标题和文本完全相同,则跳过 if not notifications or not (notifications[-1]['title'] == title and notifications[-1]['text'] == text): notifications.append({ 'package': current_pkg, 'title': title, 'text': text, 'time': datetime.now().strftime("%H:%M:%S") }) i += 1 return notifications if __name__ == "__main__": # 测试函数,每隔5秒抓取一次 seen_messages = set() while True: notifs = get_android_notifications() for n in notifs: # 使用标题+文本前50字符作为唯一标识,防止重复 msg_key = f"{n['title']}|{n['text'][:50]}" if msg_key not in seen_messages: seen_messages.add(msg_key) print(f"[{n['time']}] {n['title']}: {n['text']}") time.sleep(5)运行这个脚本,你应该能在控制台看到模拟器微信的通知被打印出来。这证明了我们抓取链路的第一步是通的。
实操心得:
dumpsys notification的输出格式随安卓版本变化,上述解析规则可能不完美。如果发现抓不到或抓不全,可以先将完整的output保存到文件里,分析其结构,调整正则表达式。另一个更稳定的方法是使用uiautomator2直接抓取通知栏UI,但代码会更复杂。
4. 创建支持按钮的Windows Toast通知
这是项目的精华部分。我们将使用winrt库直接与Windows API交互。创建一个toast_sender.py文件。
import asyncio from winrt.windows.ui.notifications import ToastNotification, ToastNotificationManager, ToastTemplateType from winrt.windows.data.xml.dom import XmlDocument import os import sys def create_toast_with_reply(title, message, conversation_id, quick_replies=["收到", "稍后处理", "OK"]): """ 创建并显示一个带有快捷回复按钮的Windows Toast通知。 conversation_id: 用于标识回复目标会话的字符串。 quick_replies: 快捷回复文本列表。 """ # 1. 定义Toast的XML模板 # 我们使用ToastGeneric模板,它支持自适应内容。 toast_xml = f""" <toast launch="action=viewConversation&conversationId={conversation_id}"> <visual> <binding template="ToastGeneric"> <text>{title}</text> <text>{message}</text> <image placement="appLogoOverride" src="file:///{os.path.abspath('wechat.ico')}" /> </binding> </visual> <actions> <input id="quickReplyInput" type="text" placeHolderContent="输入回复..."/> <!-- 预定义按钮 --> """ # 动态添加快捷回复按钮 for reply in quick_replies: # 每个按钮的`activationType`设为`background`,这样点击后不会前台启动应用。 # `arguments` 包含了我们自定义的协议和参数。 toast_xml += f'<action activationType="background" content="{reply}" arguments="wechatreply://reply?cid={conversation_id}&text={reply}" />\n' # 添加一个手动输入发送的按钮 toast_xml += """ <action activationType="background" content="发送" arguments="wechatreply://send" hint-inputId="quickReplyInput"/> </actions> </toast> """ # 2. 解析XML并创建Toast对象 xml_doc = XmlDocument() xml_doc.load_xml(toast_xml) # 3. 创建Toast通知 # 需要指定AppUserModelId (AUMID),Windows用它来分组通知。 # 你可以使用任何字符串,但最好保持唯一性。这里我们使用一个虚构的ID。 app_id = "PythonWeChatNotifier" toast_notifier = ToastNotificationManager.create_toast_notifier(app_id) toast = ToastNotification(xml_doc) # 4. 注册激活事件处理程序(用于处理按钮点击) # 注意:`winrt`的事件处理需要在异步上下文中,且后台激活需要应用有后台任务声明。 # 对于我们的脚本,一个更简单的方法是使用协议处理器(见下一节),而非直接在这里处理事件。 # 因此,我们先不在这里处理事件,而是依靠协议URL启动我们的回复处理器。 # 5. 显示通知 toast_notifier.show(toast) print(f"Toast通知已发送: {title}") if __name__ == "__main__": # 测试发送一个带按钮的通知 create_toast_with_reply( title="测试联系人", message="这是一条测试微信消息,你可以尝试点击下方的快捷回复按钮。", conversation_id="test_conv_123", quick_replies=["明白", "正在忙", "谢谢"] )运行这段代码,你应该能在Windows通知中心看到一个带有“明白”、“正在忙”、“谢谢”三个按钮的通知。点击它们暂时不会有反应,因为我们还没有注册协议处理器。
5. 注册协议处理器与回复闭环实现
现在,我们需要让系统在点击Toast按钮时,能够启动我们的Python脚本并传递参数。
5.1 创建协议处理器脚本
创建一个reply_handler.py脚本,它负责解析参数并执行回复。
import sys import json import urllib.parse import subprocess import uiautomator2 as u2 def send_reply_to_wechat(conversation_id, reply_text): """ 核心函数:通过uiautomator2将回复文本发送到模拟器微信。 这里假设conversation_id能映射到微信的某个聊天窗口(例如聊天对象名)。 实际实现中,你可能需要维护一个映射关系,或者通过其他方式定位会话。 本例采用简化方案:假设需要回复的会话就是当前微信前台窗口。 """ print(f"准备回复: 会话[{conversation_id}] -> 内容[{reply_text}]") try: # 1. 连接设备(假设只有一个设备,即我们的模拟器) d = u2.connect() # 默认连接adb devices列表中的第一个设备 # 2. 确保微信在前台。这里简化处理,直接启动微信主界面。 # 更优做法:根据conversation_id,通过ADB Intent打开特定聊天窗口。 d.app_start("com.tencent.mm") # 3. 等待微信启动并找到输入框 # 微信的输入框resource-id经常变,需要自行用`weditor`工具探查。 # 假设当前已经是目标聊天窗口,我们直接定位输入框。 input_box = d(resourceId="com.tencent.mm:id/bXX") # 这个ID需要替换为实际值! if not input_box.exists(timeout=5): print("未找到输入框,可能不在聊天界面。尝试点击返回键回到主界面再进入...") # 这里可以添加更复杂的导航逻辑,比如根据conversation_id从聊天列表进入。 # 作为示例,我们简单处理,直接向当前输入框发送文本(如果存在)。 return # 4. 清空原有文本(如果有),并输入回复内容 input_box.set_text(reply_text) # 5. 找到发送按钮并点击 send_button = d(resourceId="com.tencent.mm:id/bXX") # 这个ID也需要替换! if send_button.exists: send_button.click() print("回复发送成功!") else: print("未找到发送按钮。") except Exception as e: print(f"发送回复时发生错误: {e}") def main(): # 协议处理器会被系统调用,参数通常是完整的URL。 if len(sys.argv) > 1: url = sys.argv[1] print(f"接收到参数: {url}") # 解析URL,提取参数 parsed = urllib.parse.urlparse(url) if parsed.scheme == 'wechatreply': query_params = urllib.parse.parse_qs(parsed.query) cid = query_params.get('cid', [''])[0] text = query_params.get('text', [''])[0] if parsed.path == '/reply' and cid and text: send_reply_to_wechat(cid, text) elif parsed.path == '/send': # 处理来自输入框的“发送”按钮,这里需要获取输入框的文本 # 这需要更复杂的IPC机制,本例暂不实现。 print("手动发送功能暂未实现") else: print("无法识别的操作或参数不全") else: print("未接收到URL参数。") if __name__ == "__main__": main()5.2 注册自定义协议到系统
我们需要在Windows注册表中添加一个条目,将wechatreply://协议关联到我们的Python解释器和脚本。
重要警告:修改注册表有风险,请先备份。建议在虚拟机或测试机上操作。
创建一个register_protocol.reg文件(文本文件,后缀改为.reg),内容如下:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\wechatreply] @="URL:WeChat Reply Protocol" "URL Protocol"="" [HKEY_CLASSES_ROOT\wechatreply\shell] [HKEY_CLASSES_ROOT\wechatreply\shell\open] [HKEY_CLASSES_ROOT\wechatreply\shell\open\command] @="\"C:\\Path\\To\\Your\\Python\\python.exe\" \"C:\\Path\\To\\Your\\Project\\reply_handler.py\" \"%1\""请务必将两处C:\\Path\\To\\Your\\替换成你实际的路径。
- 第一处:你的Python解释器的完整路径(例如
C:\\Users\\YourName\\wechat_notify_env\\Scripts\\python.exe)。 - 第二处:你的
reply_handler.py脚本的完整路径。
保存后,双击这个.reg文件,将其导入注册表。这样,当系统或任何程序尝试打开wechatreply://...这样的链接时,就会启动你指定的Python脚本来处理。
6. 整合与优化:打造稳定可用的服务
现在,我们把所有模块串联起来,并解决一些实际问题。
6.1 主服务循环与消息去重
创建一个main_service.py作为主程序。
import time import hashlib from message_grabber import get_android_notifications from toast_sender import create_toast_with_reply class WeChatNotifyService: def __init__(self): self.message_history = set() # 用于去重的消息历史集合 # 一个简单的映射:从通知标题(可能是联系人/群名)到我们内部会话ID的映射。 # 实际应用中,这个映射可能需要持久化存储。 self.conversation_map = {} def extract_conversation_id(self, title): """从通知标题生成一个简单的会话ID。更复杂的系统可能需要维护一个全局ID表。""" # 简单处理:用标题的MD5前8位作为ID return hashlib.md5(title.encode('utf-8')).hexdigest()[:8] def process_new_notifications(self): notifs = get_android_notifications() new_messages = [] for notif in notifs: if notif['package'] != 'com.tencent.mm': continue # 构建消息的唯一指纹:包名+标题+内容+时间(精确到分钟,避免秒级抖动) msg_fingerprint = f"{notif['package']}|{notif['title']}|{notif['text']}|{notif['time'][:5]}" if msg_fingerprint not in self.message_history: self.message_history.add(msg_fingerprint) new_messages.append(notif) for msg in new_messages: print(f"新消息 -> {msg['title']}: {msg['text']}") cid = self.extract_conversation_id(msg['title']) self.conversation_map[cid] = msg['title'] # 更新映射 # 发送带按钮的Toast通知 create_toast_with_reply( title=msg['title'], message=msg['text'][:100], # 通知内容不宜过长 conversation_id=cid, quick_replies=["好的", "收到", "稍等", "嗯"] ) def run(self, interval_seconds=3): print("微信消息转发服务已启动...") try: while True: self.process_new_notifications() time.sleep(interval_seconds) except KeyboardInterrupt: print("\n服务被用户中断。") if __name__ == "__main__": service = WeChatNotifyService() service.run()6.2 提升稳定性的关键技巧
ADB连接保活:长时间运行后,ADB连接可能断开。需要在主循环中加入重连机制。
def check_adb_connection(self): result = subprocess.run(['adb', 'devices'], capture_output=True, text=True) if 'device' not in result.stdout: print("ADB设备断开,尝试重连...") subprocess.run(['adb', 'connect', '127.0.0.1:62001'])在
process_new_notifications开始时调用此函数。微信界面状态判断:在
send_reply_to_wechat函数中,直接假设当前前台是目标聊天窗口很不可靠。更好的做法是:- 点击通知时,传递的
conversation_id可以包含聊天对象的昵称或备注。 - 回复脚本先启动微信,然后利用
uiautomator2的d(text=昵称).click()来打开对应的聊天窗口,再进行输入和发送。这要求你能获取到完整的联系人列表或聊天列表,实现起来更复杂,但更精准。
- 点击通知时,传递的
资源清理与防内存泄漏:
uiautomator2的连接对象和winrt的异步操作要注意正确释放。主循环中避免创建大量短期对象。错误处理与日志:将所有操作(收到通知、发送Toast、点击回复、发送消息)都记录到日志文件中,方便排查问题。
7. 部署为后台服务与开机自启
要让这个工具真正实用,需要让它像服务一样在后台静默运行。
7.1 使用Pythonw后台运行
将主脚本main_service.py重命名为main_service.pyw(.pyw扩展名在Windows上会默认用pythonw.exe运行,不显示控制台窗口)。然后你可以直接双击.pyw文件,它会在后台运行。
7.2 创建Windows计划任务实现开机自启
这是更可靠的方式。
- 按
Win + R,输入taskschd.msc打开“任务计划程序”。 - 点击右侧“创建任务”。
- 常规选项卡:
- 名称:
微信消息助手 - 勾选“不管用户是否登录都要运行”和“使用最高权限运行”。
- 名称:
- 触发器选项卡:点击“新建”,选择“登录时”(或“启动时”)。
- 操作选项卡:点击“新建”。
- 操作:
启动程序 - 程序或脚本:浏览到你的
pythonw.exe路径(在虚拟环境的Scripts文件夹里)。 - 添加参数:你的
main_service.pyw文件的完整路径。 - 起始于:你的项目根目录。
- 操作:
- 条件选项卡:可以取消“只有在计算机使用交流电源时才启动此任务”(如果你用的是笔记本)。
- 设置选项卡:建议勾选“如果任务失败,按以下频率重新启动”,设置每5分钟尝试一次,最多3次。
- 点击“确定”,输入你的Windows登录密码。
这样,每次开机,你的微信消息转发服务就会自动在后台启动了。
整个项目从构思到实现,涉及了移动端自动化、Windows桌面开发、系统集成等多个环节。虽然核心代码不算长,但要把所有细节调通,确实需要踩不少坑。最花时间的部分往往不是写代码,而是调试ADB命令的稳定性、分析微信UI布局的变化、以及处理Windows Toast通知那些略显晦涩的API。但当你最终在电脑上点击一下通知,消息就自动回复出去的那一刻,那种流畅感和效率提升,会让你觉得这一切都是值得的。这个项目不仅是一个工具,更是一个理解端到端自动化如何落地的绝佳案例。你可以在此基础上继续扩展,比如增加消息过滤规则、支持更多通讯软件、甚至加入简单的AI自动回复,让它变得更加强大。