1. 项目概述:为什么我们需要“防撤回”?

在即时通讯软件深度融入我们工作和生活的今天,微信和QQ几乎成了每个人的数字社交中心。无论是工作群里的重要通知、朋友间的关键约定,还是家人发来的温馨叮嘱,这些消息构成了我们数字记忆的重要部分。然而,“消息撤回”功能的存在,就像一把双刃剑。发送者固然获得了“后悔药”,可以修正错别字或不当言论,但对于接收方而言,一条已经看到甚至已经产生思考的消息突然从屏幕上消失,留下的只有“对方已撤回一条消息”的冰冷提示,这种感觉常常伴随着困惑、好奇,甚至可能因信息缺失导致误解或工作失误。

尤其是在工作场景中,领导或同事在群聊中发布的任务要求、数据指标被撤回,而你可能还没来得及仔细阅读或截图保存,后续沟通就可能陷入“罗生门”。从技术角度看,微信和QQ的消息撤回机制是在服务器和客户端协同完成的:当发送者点击撤回时,指令会上传到服务器,服务器再向所有接收者的客户端发送删除指令,客户端本地执行删除和替换显示的操作。这个过程本质上是客户端对本地已渲染消息的一次“自我清理”。

因此,“防撤回”工具的核心理念并非去攻击或破解服务器,而是在客户端层面进行“拦截”和“保全”。它像一个忠实的数字记录员,在消息抵达你屏幕的那一刻,就为其拍下一张“快照”并妥善保存,无论后续服务器传来什么指令,这份本地副本都安然无恙。这完全是在个人设备上进行的本地化操作,旨在弥补官方功能带来的信息不对称,夺回对已接收信息的控制权。对于需要留存沟通证据、有信息归档习惯,或单纯是好奇心旺盛的用户来说,这样一个工具具有实实在在的实用价值。

2. 核心原理与方案选型:防撤回是如何实现的?

要实现防撤回,我们必须先理解微信和QQ客户端处理消息的完整链条。以Windows桌面版为例,其架构通常是这样的:一个用户界面(UI)层负责显示,一个核心逻辑层负责业务处理(如网络通信、消息解析),而数据最终会存储在本地数据库或文件中。撤回指令生效的关键节点,在于核心逻辑层接收到服务器命令后,对UI层和数据层发起“删除”操作。

因此,防撤回的思路主要围绕“拦截”和“备份”这两个关键动作展开,由此衍生出几种主流的技术方案:

2.1 内存补丁(Hook)方案

这是最经典、最直接的方法。其原理是修改程序运行时的内存代码,将关键函数调用“转向”到我们自己的代码上。

  • 目标函数 :通常是负责从聊天窗口移除消息项或更新数据库状态的那个函数。例如,在微信中,可能是类似于 ChatMsgManager::DeleteMessage 这样的内部函数。
  • 实现方式 :使用诸如 Detours MinHook 这样的Hook库。通过编写一个动态链接库(DLL),在目标进程(WeChat.exe或QQ.exe)启动时注入进去,然后找到目标函数在内存中的地址,修改其函数头部的几个字节,使其跳转到我们自定义的函数中。
  • 自定义函数逻辑 :在我们的函数里,我们首先可以获取到即将被删除的消息的完整内容(ID、发送者、时间、文本/图片/文件等),将其记录到本地日志文件或另一个安全的地方。然后,再选择是否继续调用原始的函数。如果选择不调用,那么消息在界面上就不会消失,实现了“视觉防撤回”;如果选择调用,但我们已经备份了内容,那么就是“记录式防撤回”。
  • 优点 :效果强大,可以实现实时、无缝的防撤回,用户无感知。
  • 缺点 :技术门槛高,需要逆向分析客户端,且每次客户端更新都可能导致函数签名或地址变化,需要重新适配,稳定性依赖开发者维护。

2.2 进程间通信与消息监听方案

这种方案相对“温和”,它不直接修改目标进程的代码,而是作为一个独立的“监视者”程序运行。

  • 实现方式 :利用Windows API,监听微信或QQ进程的窗口消息、内存读写事件,或者直接读取其开放的进程内存(如果存在相关数据结构)。更高级的做法是,通过 Windows UI Automation 或类似技术,直接获取聊天窗口控件的文本内容。
  • 工作流程 :监视程序定时(例如每秒)扫描一次所有聊天窗口的显示区域,获取最新的消息气泡文本,并与上一次扫描的结果进行比对。发现新消息就保存下来。当发生撤回时,由于撤回操作无法删除监视程序已经独立保存的副本,因此内容得以保留。
  • 优点 :实现相对简单,无需深入逆向,对客户端更新不敏感,通用性较强。
  • 缺点 :实时性可能稍差(取决于扫描频率),可能无法获取非文本消息(如图片、文件)的原始内容,且如果客户端UI结构大变,可能需要调整元素定位逻辑。性能消耗也比Hook方案稍高。

2.3 本地数据库/文件备份方案

这是最“底层”和“彻底”的方案。微信和QQ的所有聊天记录,最终都会以某种格式(如SQLite数据库、加密的.dat文件)保存在用户的电脑上。

  • 实现方式 :直接定位到聊天记录存储的文件(例如微信的 Msg 文件夹下的 .db 文件),对其进行实时或定时的备份。当发生撤回时,虽然主数据库中的记录可能被标记为删除或清除,但我们备份的副本依然存在。
  • 工具与挑战 :需要知道数据库的路径、结构以及可能的解密密钥。对于微信,可能需要使用 PC版微信数据库解密工具 先对数据库进行解密,才能用SQLite浏览器查看。可以编写脚本,定时将解密后的数据库拷贝到备份位置,或直接监听数据库文件的写入事件进行增量备份。
  • 优点 :数据最全,可以恢复包括已撤回消息在内的所有历史记录,且不受客户端界面变化影响。
  • 缺点 :技术门槛最高,涉及逆向和加解密知识;操作不当可能导致原始数据损坏;无法实现“实时视觉防撤回”,更适合用于事后恢复。

注意 :任何对第三方软件进行修改或深度交互的行为,都存在一定风险,包括但不限于客户端崩溃、账号异常警告(虽然概率极低,但理论上存在)等。请仅用于学习、研究和个人合法用途,并自行承担风险。

3. 实操指南:手把手构建你的防撤回工具

考虑到普适性、安全性和可操作性,我们将采用 “进程间通信与消息监听” 方案中的 Windows UI Automation 技术,用 Python 来实现一个通用性较强的防撤回工具。这个方案不修改任何客户端文件,只是“读取”屏幕信息,风险最低。

3.1 环境准备与依赖安装

首先,确保你的Windows电脑上已经安装了Python(建议3.8及以上版本)。我们将使用几个关键的库:

  • pywin32 :用于调用Windows API,特别是UI Automation的核心接口。
  • psutil :用于方便地查找和管理进程。
  • pyperclip pynput :可选,用于处理剪贴板或键盘监听,实现一些增强功能。

打开命令行(CMD或PowerShell),使用pip安装所需依赖:

pip install pywin32 psutil pyperclip pynput

如果安装 pywin32 遇到问题,可以尝试从 https://github.com/mhammond/pywin32/releases 下载对应版本的离线安装包。

3.2 核心代码实现:监听微信聊天窗口

我们将创建一个名为 message_monitor.py 的Python脚本。核心思路是:找到微信主窗口,然后遍历其子窗口,找到显示聊天内容的那个列表控件(通常是 ListView 或自定义控件),并定时获取其内容。

import win32gui
import win32process
import win32con
import psutil
import time
import json
from datetime import datetime
import threading

class WeChatMessageMonitor:
    def __init__(self):
        self.wechat_hwnd = None
        self.chat_list_hwnd = None
        self.last_messages = []  # 用于缓存上一次获取的消息,用于比对
        self.log_file = f"wechat_msg_backup_{datetime.now().strftime('%Y%m%d')}.log"
        self.running = False

    def find_wechat_window(self):
        """查找微信主窗口"""
        def callback(hwnd, extra):
            if win32gui.IsWindowVisible(hwnd):
                window_title = win32gui.GetWindowText(hwnd)
                # 微信主窗口标题通常包含“微信”二字
                if '微信' in window_title and len(window_title) < 20:
                    # 进一步确认进程名
                    _, pid = win32process.GetWindowThreadProcessId(hwnd)
                    try:
                        process = psutil.Process(pid)
                        if process.name().lower() == 'wechat.exe':
                            self.wechat_hwnd = hwnd
                            print(f"[+] 找到微信窗口: {window_title}, HWND: {hwnd}")
                            return False  # 停止枚举
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        pass
            return True  # 继续枚举

        win32gui.EnumWindows(callback, None)
        return self.wechat_hwnd is not None

    def find_chat_list_control(self, parent_hwnd):
        """递归查找聊天消息列表控件,这里以查找特定类名(如'ChatWnd'或'ListView')为例,实际需要根据微信版本调整"""
        target_hwnd = []

        def enum_child(hwnd, lparam):
            class_name = win32gui.GetClassName(hwnd)
            # 不同版本的微信,聊天列表的类名可能不同,需要自行探索
            # 可以使用SPY++或Inspect.exe(Windows SDK自带)来查看微信窗口结构
            if class_name in ['ChatWnd', 'ChatRoomWnd', 'ListView']:
                # 简单通过大小和位置再过滤一下
                rect = win32gui.GetWindowRect(hwnd)
                width = rect[2] - rect[0]
                height = rect[3] - rect[1]
                if width > 300 and height > 200:  # 聊天区域通常比较大
                    target_hwnd.append(hwnd)
            return True

        win32gui.EnumChildWindows(parent_hwnd, enum_child, None)
        if target_hwnd:
            # 可能找到多个,取最后一个或根据z-order判断,这里简单取第一个
            self.chat_list_hwnd = target_hwnd[0]
            print(f"[+] 找到聊天列表控件,HWND: {self.chat_list_hwnd}")
            return True
        return False

    def get_chat_messages_via_uia(self):
        """使用UI Automation获取控件文本(更现代、更强大的方法)"""
        # 此方法需要更复杂的UI Automation代码,涉及comtypes或uiautomation库
        # 作为示例,这里提供一个思路框架。实际推荐使用 `uiautomation` 库。
        print("[-] 基础方法受限,建议使用uiautomation库进行增强。")
        return []

    def get_chat_messages_simple(self):
        """一个简单的、基于窗口文本的获取方法(作为备选,可能不完整)"""
        if not self.chat_list_hwnd:
            return []
        # 这个方法只能获取有限的文本,对于复杂渲染的消息可能无效
        try:
            # 发送WM_GETTEXT消息获取控件文本(对某些控件有效)
            length = win32gui.SendMessage(self.chat_list_hwnd, win32con.WM_GETTEXTLENGTH, 0, 0)
            buffer = win32gui.PyMakeBuffer(length + 1)
            win32gui.SendMessage(self.chat_list_hwnd, win32con.WM_GETTEXT, length + 1, buffer)
            text = buffer.tobytes().decode('utf-16le', errors='ignore').rstrip('\x00')
            if text:
                # 简单按行分割,实际消息结构复杂得多
                lines = [line.strip() for line in text.split('\n') if line.strip()]
                return lines[-10:]  # 返回最近10行
        except Exception as e:
            print(f"[-] 获取文本失败: {e}")
        return []

    def monitor_loop(self):
        """主监控循环"""
        print("[*] 开始监控微信聊天消息... (按Ctrl+C停止)")
        self.running = True
        while self.running:
            if not self.wechat_hwnd or not win32gui.IsWindow(self.wechat_hwnd):
                print("[-] 微信窗口丢失,尝试重新查找...")
                if not self.find_wechat_window():
                    time.sleep(5)
                    continue
                self.chat_list_hwnd = None

            if not self.chat_list_hwnd:
                if not self.find_chat_list_control(self.wechat_hwnd):
                    time.sleep(3)
                    continue

            # 尝试获取消息
            current_messages = self.get_chat_messages_simple()  # 或使用更高级的get_chat_messages_via_uia
            if current_messages:
                # 与上一次的消息进行比对,找出新增的
                new_messages = [msg for msg in current_messages if msg not in self.last_messages]
                for msg in new_messages:
                    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    log_entry = f"[{timestamp}] {msg}\n"
                    print(f"[+] 新消息: {msg}")
                    # 保存到日志文件
                    with open(self.log_file, 'a', encoding='utf-8') as f:
                        f.write(log_entry)
                self.last_messages = current_messages[-20:]  # 更新缓存,保留最近20条

            time.sleep(2)  # 每2秒检查一次

    def start(self):
        if self.find_wechat_window() and self.find_chat_list_control(self.wechat_hwnd):
            monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
            monitor_thread.start()
            try:
                while True:
                    time.sleep(1)
            except KeyboardInterrupt:
                print("\n[*] 收到停止信号,正在退出...")
                self.running = False
        else:
            print("[-] 初始化失败,请确保微信已登录并打开一个聊天窗口。")

if __name__ == "__main__":
    monitor = WeChatMessageMonitor()
    monitor.start()

代码解析与注意事项

  1. 窗口查找 find_wechat_window 函数通过枚举所有顶层窗口,根据标题和进程名精确定位微信。这是最可靠的一步。
  2. 控件查找 find_chat_list_control 函数是关键难点。微信的聊天列表控件类名并非公开,且可能随版本更新而变化。代码中列出的 ‘ChatWnd‘ , ‘ListView‘ 是常见可能,但 不一定准确 。你需要使用微软的 Inspect.exe (Windows SDK或Accessibility Insights中附带)工具来实际探测。运行Inspect,将鼠标移动到微信聊天消息区域,查看 RuntimeId ClassName
  3. 消息获取 get_chat_messages_simple 方法是一个简陋的备选方案,对于新版微信可能完全无效。 真正有效的方案是使用 uiautomation 。你需要先安装它 ( pip install uiautomation ),然后使用它来遍历UI树,找到消息项并提取文本。这需要一定的学习成本,但功能强大且稳定。
  4. 日志保存 :所有监测到的新消息都会以时间戳格式追加写入到当天的日志文件中,即使消息被撤回,日志里依然存在。

3.3 使用更强大的uiautomation库(推荐)

下面提供一个使用 uiautomation 库的增强思路片段:

import uiautomation as auto

def get_messages_with_uia(wechat_window_control):
    """
    通过uiautomation获取消息
    :param wechat_window_control: 通过uiautomation找到的微信窗口Control对象
    """
    messages = []
    # 假设聊天消息列表是第一个符合条件的ListControl或PaneControl
    # 这需要你用Inspect工具实际确认控件的类型和属性
    chat_list = wechat_window_control.ListControl(searchDepth=3)  # 调整searchDepth
    if chat_list.Exists():
        for item in chat_list.GetChildren():
            # 每个消息项可能是一个ListItem或CustomControl
            item_text = item.Name  # 或者使用 .LegacyIAccessibleObject.Value
            if item_text:
                messages.append(item_text)
    return messages[-15:]  # 返回最近15条

在你的主类中,可以将 find_chat_list_control get_chat_messages_simple 替换为基于 uiautomation 的查找和获取逻辑,成功率会高很多。

4. 高级技巧、问题排查与安全考量

4.1 提升工具稳定性和用户体验

  1. 多聊天窗口支持 :上述示例只监控了一个窗口。你可以扩展代码,维护一个窗口句柄列表,循环监控所有打开的聊天窗口(包括群聊)。
  2. 过滤系统消息 :在保存消息时,可以过滤掉“对方已撤回一条消息”、“你已添加了XXX”等系统通知,只保留真实聊天内容。
  3. 结构化存储 :将消息按聊天对象(好友名、群名)、时间、发送者进行结构化存储(如JSON或SQLite),方便后期检索。可以从窗口标题提取聊天对象名称。
  4. 开机自启与后台运行 :将Python脚本打包成 .exe 文件(使用 PyInstaller ),然后将其快捷方式放入Windows的启动文件夹,实现开机自启。为了隐蔽,可以编译为控制台程序但运行时隐藏窗口(使用 pythonw.exe 或修改打包配置)。

4.2 常见问题与排查技巧

问题现象 可能原因 排查步骤与解决方案
找不到微信窗口 1. 微信未启动或未登录。
2. 窗口标题判断条件太严格。
1. 确保微信已登录并打开主界面。
2. 修改 find_wechat_window 中的标题判断逻辑,例如只判断进程名。使用 psutil 遍历所有 wechat.exe 进程。
找到窗口但找不到聊天列表控件 1. 控件类名或结构已随微信更新改变。
2. 查找深度或条件不对。
这是最常见的问题。 必须使用 Inspect.exe Accessibility Insights 重新分析微信聊天窗口的UI结构。重点关注消息滚动区域的控件类型、 ClassName AutomationId 。根据新发现调整代码中的查找条件。
能找到控件但获取不到文本 1. 消息是图片、表情等非文本内容。
2. 控件文本不在 Name 属性,可能在 Value 或其他属性。
3. 消息是动态虚拟化加载的。
1. 非文本内容需要更复杂的处理(如截图识别),本方案主要针对文本。
2. 在Inspect工具中查看控件的所有属性,尝试 LegacyIAccessibleObject.Value
3. 尝试模拟滚动或鼠标悬停,触发消息加载。
程序运行占用CPU高 监控循环的休眠时间 ( time.sleep ) 太短,或查找控件的操作太频繁。 增加循环间隔,例如从2秒改为3-5秒。将查找控件的操作放在循环外,只有窗口丢失时才重新查找。
防撤回无效,消息还是消失了 1. 工具获取消息的速度慢于撤回指令执行的速度。
2. 工具监控的窗口不是当前活动聊天窗口。
1. 极短时间内(2-3秒内)的撤回可能无法捕获。这是本方案原理上的局限。
2. 确保工具绑定的是你正在查看的聊天窗口。可以增加逻辑,自动切换到前台最活跃的聊天窗口进行监控。

4.3 安全与合规使用建议

  1. 本地化原则 :本工具设计理念应严格限定在 本地操作 ,即只读取运行在自己电脑上的客户端软件的公开界面信息或已解密的本机数据。绝不涉及破解通信协议、干扰服务器、窃取他人信息等行为。
  2. 隐私尊重 :工具备份的所有聊天记录,应视为个人隐私数据妥善保管,切勿分享、传播,尤其避免泄露他人隐私信息。
  3. 风险自知 :尽管我们采用了风险最低的UI监听方案,但任何自动化脚本如果行为过于频繁(如极高频率的屏幕扫描),仍可能被客户端的安全模块注意(虽然概率很小)。请勿用于商业或非法用途。
  4. 学习目的 :将此项目视为一个学习Windows编程、UI自动化、进程通信的绝佳案例。理解其原理比单纯使用工具更有价值。

5. 替代方案与现成工具浅析

如果你觉得从头开发太麻烦,网络上确实存在一些现成的防撤回工具。了解它们,也能帮助你更好地理解这个领域。

  1. 开源项目 :在GitHub等平台搜索 “WeChatAntiRevoke” 或 “QQAntiRevoke”,可以找到一些开源实现。这些项目通常是Hook方案或内存扫描方案, 需要一定的计算机知识来编译和运行 。使用前务必阅读代码,确保没有恶意行为。
  2. 封装好的工具 :有些开发者会将工具打包成绿色软件。对于这类工具, 需要极度谨慎
    • 来源审查 :尽量从知名的技术论坛、博主分享的链接下载,避免来路不明的网站。
    • 杀毒扫描 :下载后第一时间用杀毒软件扫描。
    • 虚拟机测试 :如果可能,先在虚拟机中运行测试。
    • 权限警惕 :如果工具要求管理员权限、关闭杀毒软件等,需高度怀疑。
  3. 浏览器插件 :对于网页版微信,可以尝试浏览器的Tampermonkey插件,安装防撤回脚本。其原理是拦截网页端的JavaScript撤回指令。这种方法相对安全,但只适用于网页版。

我个人在实际探索中的体会是 ,自己动手写一个简单的监听工具,哪怕功能简陋,其安全感和可控性是使用任何未知来源的二进制工具都无法比拟的。这个过程会让你对Windows应用的结构、消息机制有更深的理解。最初的版本可能只能监控一个窗口、文本获取也不全,但随着你不断使用Inspect工具去分析、调整代码,它会变得越来越好用。这个从无到有、不断迭代解决问题的过程,本身就是一种巨大的乐趣和技术提升。最后记住,工具是死的,人是活的,清晰沟通、及时确认永远是避免信息误解的最佳实践,技术手段只是辅助。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐