feat:初版

This commit is contained in:
2025-12-03 15:48:44 +08:00
commit b4df26f61d
199 changed files with 23434 additions and 0 deletions

19
WechatHook/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
"""
WechatHook - 微信 Hook API 封装层
基于个微大客户版 DLL 实现的微信 Hook 接口封装
"""
from .loader import NoveLoader
from .client import WechatHookClient
from .message_types import MessageType, MESSAGE_TYPE_MAP, normalize_message
__all__ = [
'NoveLoader',
'WechatHookClient',
'MessageType',
'MESSAGE_TYPE_MAP',
'normalize_message',
]
__version__ = '1.0.0'

192
WechatHook/callbacks.py Normal file
View File

@@ -0,0 +1,192 @@
"""
回调处理器
实现 Socket 回调的装饰器和处理机制
"""
import copy
import ctypes
from ctypes import WINFUNCTYPE
from functools import wraps
from typing import Callable, List
from loguru import logger
# 全局回调列表
_GLOBAL_CONNECT_CALLBACK_LIST: List[Callable] = []
_GLOBAL_RECV_CALLBACK_LIST: List[Callable] = []
_GLOBAL_CLOSE_CALLBACK_LIST: List[Callable] = []
def CONNECT_CALLBACK(in_class: bool = False):
"""
连接回调装饰器
Args:
in_class: 是否是类方法
Usage:
@CONNECT_CALLBACK()
def on_connect(client_id):
pass
@CONNECT_CALLBACK(in_class=True)
def on_connect(self, client_id):
pass
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
if in_class:
wrapper._wx_connect_handled = True
else:
_GLOBAL_CONNECT_CALLBACK_LIST.append(wrapper)
return wrapper
return decorator
def RECV_CALLBACK(in_class: bool = False):
"""
接收消息回调装饰器
Args:
in_class: 是否是类方法
Usage:
@RECV_CALLBACK()
def on_receive(client_id, message_type, data):
pass
@RECV_CALLBACK(in_class=True)
def on_receive(self, client_id, message_type, data):
pass
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
if in_class:
wrapper._wx_recv_handled = True
else:
_GLOBAL_RECV_CALLBACK_LIST.append(wrapper)
return wrapper
return decorator
def CLOSE_CALLBACK(in_class: bool = False):
"""
断开连接回调装饰器
Args:
in_class: 是否是类方法
Usage:
@CLOSE_CALLBACK()
def on_close(client_id):
pass
@CLOSE_CALLBACK(in_class=True)
def on_close(self, client_id):
pass
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
if in_class:
wrapper._wx_close_handled = True
else:
_GLOBAL_CLOSE_CALLBACK_LIST.append(wrapper)
return wrapper
return decorator
def add_callback_handler(callback_handler):
"""
添加回调处理器实例
Args:
callback_handler: 包含回调方法的对象
"""
import inspect
for name, method in inspect.getmembers(callback_handler, callable):
if hasattr(method, '_wx_connect_handled'):
_GLOBAL_CONNECT_CALLBACK_LIST.append(method)
logger.debug(f"注册连接回调: {name}")
elif hasattr(method, '_wx_recv_handled'):
_GLOBAL_RECV_CALLBACK_LIST.append(method)
logger.debug(f"注册接收回调: {name}")
elif hasattr(method, '_wx_close_handled'):
_GLOBAL_CLOSE_CALLBACK_LIST.append(method)
logger.debug(f"注册断开回调: {name}")
@WINFUNCTYPE(None, ctypes.c_void_p)
def wechat_connect_callback(client_id):
"""
微信连接回调C 函数)
Args:
client_id: 客户端 ID
"""
logger.info(f"[回调] 客户端连接: {client_id}")
for func in _GLOBAL_CONNECT_CALLBACK_LIST:
try:
func(client_id)
except Exception as e:
logger.error(f"连接回调执行失败: {e}")
@WINFUNCTYPE(None, ctypes.c_long, ctypes.c_char_p, ctypes.c_ulong)
def wechat_recv_callback(client_id, data, length):
"""
微信接收消息回调C 函数)
Args:
client_id: 客户端 ID
data: 消息数据JSON 字符串)
length: 数据长度
"""
try:
import json
# 深拷贝数据
data = copy.deepcopy(data)
json_data = data.decode('utf-8')
dict_data = json.loads(json_data)
msg_type = dict_data.get('type')
msg_data = dict_data.get('data', {})
logger.info(f"[回调] 收到消息: type={msg_type}, data={msg_data}")
# 调用所有注册的回调
for func in _GLOBAL_RECV_CALLBACK_LIST:
try:
func(client_id, msg_type, msg_data)
except Exception as e:
logger.error(f"接收回调执行失败: {e}")
except Exception as e:
logger.error(f"解析消息失败: {e}")
@WINFUNCTYPE(None, ctypes.c_ulong)
def wechat_close_callback(client_id):
"""
微信断开连接回调C 函数)
Args:
client_id: 客户端 ID
"""
logger.warning(f"[回调] 客户端断开: {client_id}")
for func in _GLOBAL_CLOSE_CALLBACK_LIST:
try:
func(client_id)
except Exception as e:
logger.error(f"断开回调执行失败: {e}")

1331
WechatHook/client.py Normal file

File diff suppressed because it is too large Load Diff

311
WechatHook/loader.py Normal file
View File

@@ -0,0 +1,311 @@
"""
NoveLoader - DLL 加载器和函数封装
基于个微大客户版 Loader.dll 的 Python 封装
"""
import ctypes
import os
from ctypes import WinDLL, create_string_buffer, WINFUNCTYPE
from typing import Callable
from loguru import logger
def c_string(data: str) -> ctypes.c_char_p:
"""将 Python 字符串转换为 C 字符串"""
return ctypes.c_char_p(data.encode('utf-8'))
def create_shared_memory():
"""创建共享内存用于DLL通信"""
try:
kernel32 = ctypes.WinDLL('kernel32')
# 创建文件映射
file_handle = kernel32.CreateFileMappingA(
-1,
None,
4, # PAGE_READWRITE
0,
33,
"windows_shell_global__".encode('utf-8')
)
if not file_handle:
logger.warning("创建共享内存失败")
return None, None
# 映射到内存
data_address = kernel32.MapViewOfFile(
file_handle,
983071, # FILE_MAP_ALL_ACCESS
0,
0,
0
)
if not data_address:
logger.warning("映射共享内存失败")
kernel32.CloseHandle(file_handle)
return None, None
# 写入Key数据
key = "3101b223dca7715b0154924f0eeeee20".encode('utf-8')
kernel32.RtlMoveMemory(data_address, key, len(key))
logger.success("共享内存创建成功")
return file_handle, data_address
except Exception as e:
logger.warning(f"创建共享内存异常: {e}")
return None, None
class NoveLoader:
"""
Loader.dll 封装类
通过内存偏移调用 DLL 中的未导出函数
"""
# 加载器模块基址
loader_module_base: int = 0
# 函数偏移地址(基于 Loader.dll
_InitWeChatSocket: int = 0xB080
_GetUserWeChatVersion: int = 0xCB80
_InjectWeChat: int = 0xCC10
_SendWeChatData: int = 0xAF90
_DestroyWeChat: int = 0xC540
_UseUtf8: int = 0xC680
_InjectWeChat2: int = 0x14D7
_InjectWeChatPid: int = 0xB750
_InjectWeChatMultiOpen: int = 0x33B2
def __init__(self, loader_path: str):
"""
初始化 Loader
Args:
loader_path: Loader.dll 的路径
"""
loader_path = os.path.realpath(loader_path)
if not os.path.exists(loader_path):
logger.error(f'Loader.dll 不存在: {loader_path}')
raise FileNotFoundError(f'Loader.dll 不存在: {loader_path}')
logger.info(f"加载 Loader.dll: {loader_path}")
loader_module = WinDLL(loader_path)
self.loader_module_base = loader_module._handle
# 使用 UTF-8 编码
self.UseUtf8()
logger.success("Loader.dll 加载成功")
# 初始化回调
from WechatHook.callbacks import (
wechat_connect_callback,
wechat_recv_callback,
wechat_close_callback
)
self.InitWeChatSocket(
wechat_connect_callback,
wechat_recv_callback,
wechat_close_callback
)
def __get_non_exported_func(self, offset: int, arg_types, return_type):
"""
通过内存偏移获取未导出的函数
Args:
offset: 函数相对于模块基址的偏移
arg_types: 参数类型列表
return_type: 返回值类型
Returns:
可调用的函数对象
"""
func_addr = self.loader_module_base + offset
if arg_types:
func_type = ctypes.WINFUNCTYPE(return_type, *arg_types)
else:
func_type = ctypes.WINFUNCTYPE(return_type)
return func_type(func_addr)
def InitWeChatSocket(
self,
connect_callback: Callable,
recv_callback: Callable,
close_callback: Callable
) -> bool:
"""
初始化微信 Socket 回调
Args:
connect_callback: 连接回调函数
recv_callback: 接收消息回调函数
close_callback: 断开连接回调函数
Returns:
是否初始化成功
"""
func = self.__get_non_exported_func(
self._InitWeChatSocket,
[ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p],
ctypes.c_bool
)
result = func(connect_callback, recv_callback, close_callback)
logger.info(f"InitWeChatSocket: {result}")
return result
def GetUserWeChatVersion(self) -> str:
"""
获取用户电脑上安装的微信版本
Returns:
微信版本号,如 "3.9.10.19"
"""
func = self.__get_non_exported_func(
self._GetUserWeChatVersion,
[ctypes.c_char_p],
ctypes.c_bool
)
out = create_string_buffer(30)
if func(out):
version = out.value.decode('utf-8')
logger.info(f"微信版本: {version}")
return version
else:
logger.warning("获取微信版本失败")
return ''
def InjectWeChat(self, dll_path: str) -> int:
"""
注入微信(智能多开)
Args:
dll_path: Helper.dll 的路径
Returns:
客户端 ID进程 ID失败返回 0
"""
dll_path = os.path.realpath(dll_path)
if not os.path.exists(dll_path):
logger.error(f'Helper.dll 不存在: {dll_path}')
return 0
func = self.__get_non_exported_func(
self._InjectWeChat,
[ctypes.c_char_p],
ctypes.c_uint32
)
client_id = func(c_string(dll_path))
if client_id:
logger.success(f"注入微信成功,客户端 ID: {client_id}")
else:
logger.error("注入微信失败")
return client_id
def SendWeChatData(self, client_id: int, message: str) -> bool:
"""
向微信发送数据
Args:
client_id: 客户端 ID
message: JSON 格式的消息
Returns:
是否发送成功
"""
func = self.__get_non_exported_func(
self._SendWeChatData,
[ctypes.c_uint32, ctypes.c_char_p],
ctypes.c_bool
)
result = func(client_id, c_string(message))
return result
def DestroyWeChat(self) -> bool:
"""
销毁微信连接
Returns:
是否成功
"""
func = self.__get_non_exported_func(
self._DestroyWeChat,
None,
ctypes.c_bool
)
result = func()
logger.info(f"DestroyWeChat: {result}")
return result
def UseUtf8(self) -> bool:
"""
设置使用 UTF-8 编码
Returns:
是否成功
"""
func = self.__get_non_exported_func(
self._UseUtf8,
None,
ctypes.c_bool
)
return func()
def InjectWeChat2(self, dll_path: str, exe_path: str) -> int:
"""
注入微信(指定微信路径)
Args:
dll_path: Helper.dll 的路径
exe_path: WeChat.exe 的路径
Returns:
客户端 ID失败返回 0
"""
func = self.__get_non_exported_func(
self._InjectWeChat2,
[ctypes.c_char_p, ctypes.c_char_p],
ctypes.c_uint32
)
return func(c_string(dll_path), c_string(exe_path))
def InjectWeChatPid(self, pid: int, dll_path: str) -> int:
"""
注入指定的微信进程
Args:
pid: 微信进程 ID
dll_path: Helper.dll 的路径
Returns:
客户端 ID失败返回 0
"""
func = self.__get_non_exported_func(
self._InjectWeChatPid,
[ctypes.c_uint32, ctypes.c_char_p],
ctypes.c_uint32
)
return func(pid, c_string(dll_path))
def InjectWeChatMultiOpen(self, dll_path: str, exe_path: str = "") -> int:
"""
多开一个新的微信进程并注入
Args:
dll_path: Helper.dll 的路径
exe_path: WeChat.exe 的路径(可选)
Returns:
客户端 ID失败返回 0
"""
func = self.__get_non_exported_func(
self._InjectWeChatMultiOpen,
[ctypes.c_char_p, ctypes.c_char_p],
ctypes.c_uint32
)
return func(c_string(dll_path), c_string(exe_path) if exe_path else c_string(""))

213
WechatHook/message_types.py Normal file
View File

@@ -0,0 +1,213 @@
"""
消息类型定义和映射
定义个微 API 的消息类型常量,以及到内部事件的映射关系
"""
class MessageType:
"""消息类型常量(基于实际测试)"""
# 系统消息类型
MT_DEBUG_LOG = 11024 # 调试日志
MT_USER_LOGIN = 11025 # 用户登录
MT_USER_LOGOUT = 11026 # 用户登出
MT_GET_LOGIN_INFO = 11028 # 获取登录信息
# 消息通知类型(基于实际测试修正)
MT_TEXT = 11046 # 文本消息
MT_IMAGE = 11047 # 图片消息
MT_VOICE = 11048 # 语音消息
MT_VIDEO = 11051 # 视频消息
MT_EMOJI = 11052 # 表情消息
MT_REVOKE = 11057 # 撤回消息
MT_SYSTEM = 11058 # 系统消息
MT_QUOTE = 11061 # 引用消息
MT_FRIEND_REQUEST = 11056 # 好友请求
# 实际测试得出的正确映射
MT_LOCATION = 11053 # 位置消息(实际)
MT_LINK = 11054 # 链接消息(实际)
MT_FILE = 11055 # 文件消息(实际)
# 兼容性定义
MT_CARD = 11055 # 名片消息(临时兼容,实际可能是文件类型)
MT_MINIAPP = 11054 # 小程序消息(临时兼容,实际可能是链接类型)
# 群聊通知类型
MT_CHATROOM_MEMBER_ADD = 11098 # 群成员新增
MT_CHATROOM_MEMBER_REMOVE = 11099 # 群成员删除
MT_CHATROOM_INFO_CHANGE = 11100 # 群信息变化(成员数量变化等)
# 发送消息类型
MT_SEND_TEXT = 11036 # 发送文本
# 消息类型到事件名称的映射
MESSAGE_TYPE_MAP = {
MessageType.MT_TEXT: "text_message",
MessageType.MT_IMAGE: "image_message",
MessageType.MT_VOICE: "voice_message",
MessageType.MT_VIDEO: "video_message",
MessageType.MT_EMOJI: "emoji_message",
MessageType.MT_REVOKE: "revoke_message",
MessageType.MT_SYSTEM: "system_message",
MessageType.MT_FRIEND_REQUEST: "friend_request",
MessageType.MT_QUOTE: "quote_message",
MessageType.MT_CHATROOM_MEMBER_ADD: "chatroom_member_add",
MessageType.MT_CHATROOM_MEMBER_REMOVE: "chatroom_member_remove",
MessageType.MT_CHATROOM_INFO_CHANGE: "chatroom_info_change",
# 修正后的映射(基于实际测试)
MessageType.MT_LOCATION: "location_message", # 11053 -> 位置消息
MessageType.MT_LINK: "link_message", # 11054 -> 链接消息
MessageType.MT_FILE: "file_message", # 11055 -> 文件消息
}
def normalize_message(msg_type: int, data: dict) -> dict:
"""
将个微 API 的消息格式转换为统一的内部格式(兼容 XYBot
Args:
msg_type: 消息类型
data: 原始消息数据
Returns:
标准化的消息字典
"""
# 基础消息结构
message = {
"MsgType": msg_type,
"FromWxid": data.get("from_wxid", ""),
"ToWxid": data.get("to_wxid", ""),
"Content": data.get("msg", data.get("content", data.get("raw_msg", ""))), # 系统消息使用 raw_msg
"CreateTime": data.get("timestamp", data.get("create_time", 0)),
"IsGroup": False,
"SenderWxid": data.get("from_wxid", ""),
}
# 判断是否是群聊消息room_wxid 不为空)
room_wxid = data.get("room_wxid", "")
if room_wxid:
message["IsGroup"] = True
message["FromWxid"] = room_wxid
message["SenderWxid"] = data.get("from_wxid", "")
# @ 消息处理
if "at_user_list" in data:
message["Ats"] = data["at_user_list"]
elif "at_list" in data:
message["Ats"] = data["at_list"]
# 图片消息
if msg_type == MessageType.MT_IMAGE:
message["ImagePath"] = data.get("image_path", "")
# 文件消息实际类型11055
if msg_type == MessageType.MT_FILE:
message["Filename"] = data.get("filename", "")
message["FileExtend"] = data.get("file_extend", "")
message["File"] = data.get("file_data", "")
# 语音消息
if msg_type == MessageType.MT_VOICE:
message["ImgBuf"] = {"buffer": data.get("voice_data", "")}
# 视频消息
if msg_type == MessageType.MT_VIDEO:
message["Video"] = data.get("video_data", "")
# 引用消息
if "quote" in data:
message["Quote"] = data["quote"]
# 引用消息的 @ 提取(从 XML 中解析)
if msg_type == MessageType.MT_QUOTE:
try:
import xml.etree.ElementTree as ET
content = message.get("Content", "")
if content:
root = ET.fromstring(content)
title = root.find(".//title")
if title is not None and title.text:
title_text = title.text
# 检查 title 中是否包含 @
if "@" in title_text:
# 从 main_config.toml 读取机器人昵称
import tomllib
from pathlib import Path
config_path = Path("main_config.toml")
if config_path.exists():
with open(config_path, "rb") as f:
main_config = tomllib.load(f)
bot_nickname = main_config.get("Bot", {}).get("nickname", "")
bot_wxid = main_config.get("Bot", {}).get("wxid", "")
# 检查是否 @ 了机器人
if bot_nickname and f"@{bot_nickname}" in title_text:
message["Ats"] = [bot_wxid] if bot_wxid else []
except Exception:
pass # 解析失败则忽略
# 位置消息实际类型11053
if msg_type == MessageType.MT_LOCATION:
message["Latitude"] = data.get("latitude", 0)
message["Longitude"] = data.get("longitude", 0)
message["LocationTitle"] = data.get("title", "")
message["LocationAddress"] = data.get("address", "")
# 链接消息实际类型11054
if msg_type == MessageType.MT_LINK:
message["LinkTitle"] = data.get("title", "")
message["LinkDesc"] = data.get("desc", "")
message["LinkUrl"] = data.get("url", "")
message["LinkThumb"] = data.get("thumb_url", "")
message["MiniappPagePath"] = data.get("page_path", "")
# 好友请求
if msg_type == MessageType.MT_FRIEND_REQUEST:
message["V3"] = data.get("v3", "")
message["V4"] = data.get("v4", "")
message["Scene"] = data.get("scene", 0)
# 群成员新增 (type=11098)
if msg_type == MessageType.MT_CHATROOM_MEMBER_ADD:
message["FromWxid"] = data.get("room_wxid", "")
message["IsGroup"] = True
message["RoomWxid"] = data.get("room_wxid", "")
message["RoomNickname"] = data.get("nickname", "")
message["MemberList"] = data.get("member_list", [])
message["TotalMember"] = data.get("total_member", 0)
message["ManagerWxid"] = data.get("manager_wxid", "")
# 群成员删除 (type=11099)
if msg_type == MessageType.MT_CHATROOM_MEMBER_REMOVE:
message["FromWxid"] = data.get("room_wxid", "")
message["IsGroup"] = True
message["RoomWxid"] = data.get("room_wxid", "")
message["RoomNickname"] = data.get("nickname", "")
message["MemberList"] = data.get("member_list", [])
message["TotalMember"] = data.get("total_member", 0)
message["ManagerWxid"] = data.get("manager_wxid", "")
# 系统消息 (type=11058)
if msg_type == MessageType.MT_SYSTEM:
# 系统消息的内容在 raw_msg 字段
message["Content"] = data.get("raw_msg", "")
# 系统消息也可能是群聊消息
if room_wxid:
message["IsGroup"] = True
message["FromWxid"] = room_wxid
# 群信息变化 (type=11100)
if msg_type == MessageType.MT_CHATROOM_INFO_CHANGE:
message["FromWxid"] = data.get("room_wxid", "")
message["IsGroup"] = True
message["RoomWxid"] = data.get("room_wxid", "")
message["RoomNickname"] = data.get("nickname", "")
message["TotalMember"] = data.get("total_member", 0)
message["ManagerWxid"] = data.get("manager_wxid", "")
# member_list 可能存在也可能不存在
message["MemberList"] = data.get("member_list", [])
return message