🤖 微信群机器人实战:WxHook + Node.js 后端完整实现
深入剖析微信 Hook 的运行机制和 Node.js 后端架构,从消息监听到图表生成的完整技术方案。
📋 目录
- 项目概述
- [WxHook 运行流程详解](#WxHook 运行流程详解 "#wxhook-%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3")
- [Node.js 后端架构](#Node.js 后端架构 "#nodejs-%E5%90%8E%E7%AB%AF%E6%9E%B6%E6%9E%84")
- 消息处理与自动回复
- 图表生成引擎
- 数据同步机制
- 实战经验总结
🎯 项目概述
本项目是一个基于微信 Hook 技术的群管理系统,核心功能包括:
- 微信消息监听:通过 DLL 注入监听群消息
- 智能指令识别:识别 @机器人 的指令并执行
- 实时图表生成:使用 Canvas 生成资金赔率图和账目表
- 跨服务通信:Python Hook 服务与 Node.js 后端的协同工作
技术栈
- WxHook :
Helper_4.1.2.17.dll+Loader_4.1.2.17.dll - Python 3.x:Hook 服务封装和消息处理
- Node.js + Express:后端 API 服务
- @napi-rs/canvas:服务端图表生成
🔌 WxHook 运行流程详解
1. Hook 服务启动流程
python
# demo.py - 核心启动流程
import ctypes
from ctypes import WinDLL, create_string_buffer, WINFUNCTYPE
# 1. 加载 Hook DLL
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
helper_dll_path = os.path.join(SCRIPT_DIR, "Helper_4.1.2.17.dll")
loader_dll_path = os.path.join(SCRIPT_DIR, "Loader_4.1.2.17.dll")
# 2. 初始化 DLL
helper_dll = WinDLL(helper_dll_path)
loader_dll = WinDLL(loader_dll_path)
# 3. 定义回调函数类型
CALLBACK_TYPE = WINFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
# 4. 注册消息回调
def message_callback(msg_type, data):
"""微信消息回调处理"""
logger.info(f"收到消息类型: {msg_type}")
# 解析消息数据
message_data = parse_message(data)
# 处理消息
handle_message(msg_type, message_data)
# 5. 设置回调并启动监听
callback_func = CALLBACK_TYPE(message_callback)
helper_dll.SetMessageCallback(callback_func)
helper_dll.StartListen()
logger.info("✅ WxHook 服务已启动,开始监听微信消息...")
2. 消息监听与解析
python
def normalize_wechat_data(message_type, data):
"""解析并规范化微信消息数据"""
normalized = {}
raw_dict = None
# 1. 解析 JSON 数据
if isinstance(data, str):
try:
raw_dict = json.loads(data)
except (TypeError, ValueError):
raw_dict = None
normalized["raw"] = data
elif isinstance(data, dict):
raw_dict = copy.deepcopy(data)
if isinstance(raw_dict, dict):
# 2. 提取关键字段
for key, value in raw_dict.items():
normalized[key] = sanitize_value(value)
# 3. 提取群聊名称(多种字段兼容)
def pick(*keys):
for key in keys:
value = raw_dict.get(key)
if value:
return value
return None
chat_name = pick("room_nickname", "chatroom_nickname",
"group_nickname", "chat_name")
# 4. 映射群聊 ID 到友好名称
if not chat_name:
room_id = pick("room_id", "chatroom_id")
if room_id:
chat_name = ROOM_NAME_MAP.get(room_id, room_id)
normalized["chat_name"] = chat_name
# 5. 提取发送者信息
sender = pick("sender", "from_user", "from_wxid")
sender_name = pick("sender_name", "from_name")
# 映射成员 ID 到昵称
if sender and room_id:
member_map = ROOM_MEMBER_NAME_MAP.get(room_id, {})
sender_name = member_map.get(sender, sender_name or sender)
normalized["sender"] = sender
normalized["sender_name"] = sender_name
# 6. 提取消息内容
content = pick("content", "message", "text")
normalized["content"] = content
return normalized
3. 群聊映射配置
json
// room_mapping.json - 群聊 ID 映射表
{
"room_names": {
"48846718088@chatroom": "测试1群",
},
"room_members": {
"48846718088@chatroom": {
"wxid_hf1ej5rvdcat22": "我很忙",
}
}
}
映射表加载机制:
python
def load_mappings(force: bool = False):
"""从外部 JSON 文件加载群聊/成员映射"""
global ROOM_NAME_MAP, ROOM_MEMBER_NAME_MAP, _MAPPING_MTIME
try:
# 1. 检查文件修改时间(支持热更新)
mtime = os.path.getmtime(MAPPING_FILE)
if not force and _MAPPING_MTIME == mtime:
return # 文件未修改,跳过加载
# 2. 读取 JSON 配置
with open(MAPPING_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
room_names = data.get("room_names", {})
room_members = data.get("room_members", {})
# 3. 转换为字典格式
ROOM_NAME_MAP = {str(k): str(v) for k, v in room_names.items()}
ROOM_MEMBER_NAME_MAP = {
str(room): {str(member): str(name) for member, name in members.items()}
for room, members in room_members.items()
if isinstance(members, dict)
}
_MAPPING_MTIME = mtime
logger.info(f"映射表已加载:群聊 {len(ROOM_NAME_MAP)} 个")
except FileNotFoundError:
logger.warning(f"未找到映射文件 {MAPPING_FILE}")
ROOM_NAME_MAP = {}
ROOM_MEMBER_NAME_MAP = {}
# 启动时加载映射表
load_mappings(force=True)
4. 消息上报到 Node.js 后端
python
NODE_SERVER_URL = os.environ.get('NODE_SERVER_URL',
'http://localhost:3000/api/wechat/messages')
def send_message_to_node_server(message_type, data, source="wechat_demo"):
"""将抓取到的消息上报到 Node 服务器"""
payload = {
"type": message_type,
"data": data,
"timestamp": datetime.now().isoformat(),
"source": source
}
# 确保 payload 可序列化
try:
json.dumps(payload, ensure_ascii=False)
except TypeError:
payload["data"] = str(data)
try:
response = requests.post(NODE_SERVER_URL, json=payload, timeout=3)
if response.status_code != 200:
logger.warning(f"Node 服务器响应异常: {response.status_code}")
except requests.RequestException as exc:
logger.error(f"发送消息到 Node 服务器失败: {exc}")
1. 服务器初始化
javascript
// server.js - Express 服务器启动
const express = require("express");
const bodyParser = require("body-parser");
const { createCanvas, registerFont } = require("@napi-rs/canvas");
const path = require("path");
const fs = require("fs");
const app = express();
// 1. CORS 跨域配置(支持前端跨域访问)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// 2. 请求体解析(支持大数据量)
// 默认 100kb 太小,前端推送大量投注数据会超限
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
// 3. 静态文件服务(图片访问)
const OUTPUT_IMAGE_DIR = path.join(__dirname, "generated_outputs");
if (!fs.existsSync(OUTPUT_IMAGE_DIR)) {
fs.mkdirSync(OUTPUT_IMAGE_DIR, { recursive: true });
}
app.use("/generated_outputs", express.static(OUTPUT_IMAGE_DIR));
// 4. 字体注册(Canvas 中文显示)
const WINDOWS_FONT_DIR = process.env.WINDIR
? path.join(process.env.WINDIR, "Fonts")
: "C:/Windows/Fonts";
const FONT_CANDIDATES = [
{ file: "msyh.ttf", family: "Microsoft YaHei" },
{ file: "msyh.ttc", family: "Microsoft YaHei" },
{ file: "msyhbd.ttc", family: "Microsoft YaHei Bold", weight: "bold" },
{ file: "simhei.ttf", family: "SimHei" }
];
FONT_CANDIDATES.forEach((font) => {
const fontPath = path.join(WINDOWS_FONT_DIR, font.file);
if (fs.existsSync(fontPath)) {
try {
registerFont(fontPath, {
family: font.family,
weight: font.weight || "normal"
});
console.log(`✅ 字体已注册: ${font.family}`);
} catch (err) {
console.warn(`⚠️ 字体注册失败: ${fontPath}`, err.message);
}
}
});
2. 核心 API 路由设计
2.1 微信消息接收端点
javascript
// 微信消息存储端点(支持按代理名自动分组存储)
app.post("/api/wechat/messages", (req, res) => {
try {
const { type, data, timestamp, source } = req.body;
// 提取群聊名称(代理名)
const chatName = data?.chat_name || '';
const agentName = extractAgentNameFromChat(chatName);
console.log("📱 收到微信消息:", {
type: type,
content: data?.content?.substring(0, 50) + "...",
sender: data?.sender,
chat_name: chatName,
agent_name: agentName || '未识别',
timestamp: timestamp
});
// 存储到全局内存
if (!global.wechatMessages) {
global.wechatMessages = [];
}
global.wechatMessages.push({
type: type,
data: data,
timestamp: timestamp,
source: source,
received_at: new Date().toISOString()
});
res.json({
success: true,
message: agentName ? `微信消息已存储(代理: ${agentName})` : "微信消息已存储",
total_messages: global.wechatMessages.length,
agent_name: agentName || null
});
} catch (error) {
console.log("❌ 存储微信消息失败:", error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// 从群名提取代理名
function extractAgentNameFromChat(chatName) {
if (!chatName) return null;
// 群名应该就是代理名,直接返回(去除首尾空格)
return chatName.trim();
}
2.2 前端状态同步端点
javascript
// 存储前端数据(全局变量)
let frontendStateData = null;
// 接收前端状态数据(核心接口)
app.post("/api/frontend-state", (req, res) => {
try {
frontendStateData = req.body;
console.log("📊 收到前端状态数据");
console.log("📊 数据详情:", {
hasState: !!frontendStateData.state,
hasBets: !!frontendStateData.state?.bets,
dateStr: frontendStateData.dateStr,
sessionKey: frontendStateData.sessionKey,
selectedAgentIds: frontendStateData.selectedAgentIds,
agentFeePercent: frontendStateData.agentFeePercent,
agentsCount: frontendStateData.state?.agents?.length || 0,
groupsCount: frontendStateData.state?.groups?.length || 0
});
// 打印投注数据概览
if (frontendStateData.state?.bets) {
const betsKeys = Object.keys(frontendStateData.state.bets);
console.log("📊 投注数据日期:", betsKeys);
if (betsKeys.length > 0) {
const firstDate = betsKeys[0];
const firstDateData = frontendStateData.state.bets[firstDate];
console.log("📊 第一个日期的代理:", Object.keys(firstDateData));
}
}
res.json({
success: true,
message: "前端状态数据已更新"
});
} catch (error) {
console.log("❌ 接收前端数据失败:", error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// 获取前端状态数据
app.get("/api/frontend-state", (req, res) => {
try {
if (frontendStateData) {
res.json(frontendStateData);
} else {
res.status(404).json({
success: false,
message: "前端数据未同步",
hint: "请确保前端应用正在运行并已推送数据"
});
}
} catch (error) {
console.error('❌ 获取前端状态失败:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
🔄 数据同步机制
完整同步(更改监听群组时使用)
javascript
// 更新配置文件并重启监听器
app.post("/api/sync-agents", (req, res) => {
try {
const { agent_names, agents } = req.body;
console.log("🤖 收到代理同步请求:", {
agent_names: agent_names,
agents_count: agents?.length || 0
});
// 1. 保存同步数据
const syncData = {
agent_names: agent_names || [],
agents: agents || [],
last_sync: new Date().toISOString()
};
fs.writeFileSync('agent_sync_data.json', JSON.stringify(syncData, null, 2), 'utf8');
// 2. 更新 config.json 中的监听群组列表
const configPath = path.join(__dirname, 'config.json');
let config = {};
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configContent);
}
// 更新监听群组列表(代理名就是群名)
config['监听群组列表'] = agent_names || [];
config['群机器人开关'] = agent_names && agent_names.length > 0 ? 'True' : 'False';
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'utf8');
console.log("✅ 已更新 config.json 的监听群组列表:", agent_names);
// 3. 通知 Python 机器人重新加载配置
const http = require('http');
const postData = JSON.stringify({});
const options = {
hostname: 'localhost',
port: 5001,
path: '/reload-config',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const botRequest = http.request(options, (botRes) => {
let responseData = '';
botRes.on('data', (chunk) => {
responseData += chunk;
});
botRes.on('end', () => {
console.log("✅ 已通知 Python 机器人重新加载配置:", responseData);
});
});
botRequest.on('error', (error) => {
console.log("⚠️ 通知机器人失败(机器人可能未启动):", error.message);
});
botRequest.write(postData);
botRequest.end();
res.json({
success: true,
message: `✅ 已同步 ${agent_names?.length || 0} 个代理到机器人\n\n⚠️ 监听器已重启(可能短暂影响消息接收)`,
data: syncData,
config_updated: true,
listener_restarted: true
});
} catch (error) {
console.log("❌ 同步代理失败:", error);
res.status(500).json({
success: false,
error: error.message
});
}
});
优点:
- 更新监听群组配置
- 持久化配置到文件
- 适合初始化或更改监听列表
🚀 服务启动与监控
Node.js 服务启动
javascript
const os = require('os');
// 获取本机 IP 地址
function getLocalIP() {
const interfaces = os.networkInterfaces();
for (const devName in interfaces) {
const iface = interfaces[devName];
for (let i = 0; i < iface.length; i++) {
const alias = iface[i];
if (alias.family === 'IPv4' && !alias.internal) {
return alias.address;
}
}
}
return 'localhost';
}
const localIP = getLocalIP();
app.listen(3000, '0.0.0.0', () => {
console.log("============================================");
console.log("✅ Node服务器已启动(支持跨设备访问)");
console.log("============================================");
console.log(`📡 本机访问: http://localhost:3000`);
console.log(`📡 局域网访问: http://${localIP}:3000`);
console.log(`📡 健康检查: http://${localIP}:3000/health`);
console.log("--------------------------------------------");
console.log("📱 微信消息API:");
console.log(" - 接收消息: POST /api/wechat/messages");
console.log("📋 数据获取API:");
console.log(" - 按微信名获取: GET /api/data/:agentName");
console.log("============================================");
console.log(`💡 前端配置: 修改 api-config.json 中的 apiBaseUrl 为: http://${localIP}:3000`);
console.log("============================================");
});
健康检查端点
javascript
app.get("/health", (req, res) => {
res.json({
status: "ok",
message: "服务器运行正常",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
frontendDataSynced: !!frontendStateData,
wechatMessagesCount: global.wechatMessages?.length || 0
});
});
🎓 实战经验总结
1. WxHook DLL 调用注意事项
问题:DLL 函数调用失败或程序崩溃
解决方案:
python
# 1. 正确定义函数签名
from ctypes import WINFUNCTYPE, c_int, c_char_p, c_void_p
# 定义回调函数类型
CALLBACK_TYPE = WINFUNCTYPE(None, c_int, c_char_p)
# 2. 保持回调函数引用(防止被垃圾回收)
callback_func = CALLBACK_TYPE(message_callback)
# 必须保存到全局变量或类成员变量
global_callback_ref = callback_func
# 3. 字符串编码处理
chat_name_bytes = chat_name.encode('utf-8')
helper_dll.SendMessage(chat_name_bytes)
2. Canvas 中文字体显示
问题:中文显示为方块或乱码
解决方案:
javascript
// 1. 启动时注册字体(必须在创建 Canvas 之前)
const fontPath = "C:/Windows/Fonts/msyh.ttf";
registerFont(fontPath, { family: "Microsoft YaHei" });
// 2. 使用时指定字体族
ctx.font = '20px "Microsoft YaHei", "SimHei", sans-serif';
// 3. 提供字体回退方案
const FONT_CANDIDATES = [
"msyh.ttf", // 微软雅黑
"simhei.ttf", // 黑体
"simsun.ttc" // 宋体
];
3. 跨域问题
问题:前端无法访问后端 API
解决方案:
javascript
// 1. 配置 CORS 中间件
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// 2. 监听所有网络接口
app.listen(3000, '0.0.0.0');
4. 大数据量传输
问题:前端推送大量投注数据时请求失败
解决方案:
javascript
// 增加请求体大小限制
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
5. 数据同步时序
问题:前端更新数据后,后端立即生成图表,但数据还是旧的
解决方案:
javascript
// 前端:先同步数据,等待确认,再请求图表
async function generateChart() {
// 1. 同步数据
await fetch('/api/sync-chart-data', {
method: 'POST',
body: JSON.stringify(frontendState)
});
// 2. 等待 100ms 确保数据已更新
await new Promise(resolve => setTimeout(resolve, 100));
// 3. 请求图表
const response = await fetch('/api/fund-chart-by-group?format=image', {
method: 'POST',
body: JSON.stringify({ group_name: 'xxx' })
});
}
📝 总结
本文详细介绍了一个完整的微信群机器人系统,包括:
WxHook 核心流程
- DLL 加载与初始化:使用 ctypes 加载 Hook DLL
- 消息监听与回调:注册回调函数接收微信消息
- 消息解析与规范化:提取群名、发送者、内容等信息
- 指令识别与执行:识别 @机器人 的指令并执行相应操作
- 消息上报:将消息同步到 Node.js 后端
Node.js 后端架构
- Express 服务器初始化:CORS、请求体解析、静态文件服务
- 字体注册:支持 Canvas 中文显示
- 核心 API 设计:微信消息接收、前端状态同步、图表生成
- 数据计算逻辑:费率优先级、场次推断、资金赔率计算
- Canvas 图表渲染:绘制资金赔率图和账目表
关键技术点
- 跨服务通信:Python 与 Node.js 的 HTTP 通信
- 数据同步机制:轻量级同步
- 错误处理:详细的错误提示和建议
- 性能优化:字体缓存、请求体限制
- 跨设备访问:局域网内多设备访问支持
希望这篇文章能帮助你理解微信 Hook 和 Node.js 后端的完整实现!
RESTful API 设计
核心接口
javascript
// 1. 前端状态同步
app.post("/api/frontend-state", (req, res) => {
frontendStateData = req.body;
res.json({ success: true });
});
// 3. 代理同步到机器人
app.post("/api/sync-agents", (req, res) => {
const { agent_names } = req.body;
config['监听群组列表'] = agent_names;
fs.writeFileSync('config.json', JSON.stringify(config, null, 4));
res.json({ success: true });
});
🌟 技术亮点
1. 跨域与跨设备支持
javascript
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
});
app.listen(3000, '0.0.0.0', () => {
console.log(`📡 局域网访问: http://${localIP}:3000`);
});
📊 性能优化
1. Canvas 字体缓存
javascript
const FONT_CANDIDATES = [
{ file: "msyh.ttf", family: "Microsoft YaHei" },
{ file: "simhei.ttf", family: "SimHei" }
];
FONT_CANDIDATES.forEach((font) => {
const fontPath = path.join(WINDOWS_FONT_DIR, font.file);
if (fs.existsSync(fontPath)) {
registerFont(fontPath, { family: font.family });
}
});
2. 请求体大小限制
javascript
app.use(bodyParser.json({ limit: '50mb' }));
🎓 实战经验总结
1. Canvas 中文字体问题
问题:中文显示为方块
解决:必须先注册字体
javascript
registerFont(fontPath, { family: "Microsoft YaHei" });
ctx.font = '20px "Microsoft YaHei"';
2. 数据同步时序
问题:前端更新后,后端数据还是旧的
解决:先同步数据,等待100ms,再请求图表
🚀 部署指南
启动服务
bash
# Node.js 后端
node server.js
# Python 微信服务
python vxhook/demo.py
配置文件
config.json
json
{
"监听群组列表": ["测试1群"],
"群机器人开关": "True"
}
🔮 未来优化方向
- 数据持久化(SQLite/MongoDB)
- 实时通信(WebSocket)
- 权限管理(JWT)
- 图表增强(更多类型)
📝 总结
本项目展示了如何使用 Node.js + Python 构建一个完整的微信群管理系统,涉及:
- Canvas 服务端图表生成
- 微信 Hook 消息监听
- RESTful API 设计
- 跨设备访问支持
希望这篇文章能给你带来启发!
关键词:Node.js, Express, Canvas, Python, 微信Hook, 图表生成, RESTful API
GitHub : [github.com/1062558134/...]
作者声明: [本项目仅限个人开发者学习使用]