wxhook + nodeJS实现对微信数据的整合

🤖 微信群机器人实战:WxHook + Node.js 后端完整实现

深入剖析微信 Hook 的运行机制和 Node.js 后端架构,从消息监听到图表生成的完整技术方案。

📋 目录


🎯 项目概述

本项目是一个基于微信 Hook 技术的群管理系统,核心功能包括:

  • 微信消息监听:通过 DLL 注入监听群消息
  • 智能指令识别:识别 @机器人 的指令并执行
  • 实时图表生成:使用 Canvas 生成资金赔率图和账目表
  • 跨服务通信:Python Hook 服务与 Node.js 后端的协同工作

技术栈

  • WxHookHelper_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 核心流程

  1. DLL 加载与初始化:使用 ctypes 加载 Hook DLL
  2. 消息监听与回调:注册回调函数接收微信消息
  3. 消息解析与规范化:提取群名、发送者、内容等信息
  4. 指令识别与执行:识别 @机器人 的指令并执行相应操作
  5. 消息上报:将消息同步到 Node.js 后端

Node.js 后端架构

  1. Express 服务器初始化:CORS、请求体解析、静态文件服务
  2. 字体注册:支持 Canvas 中文显示
  3. 核心 API 设计:微信消息接收、前端状态同步、图表生成
  4. 数据计算逻辑:费率优先级、场次推断、资金赔率计算
  5. 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"
}

🔮 未来优化方向

  1. 数据持久化(SQLite/MongoDB)
  2. 实时通信(WebSocket)
  3. 权限管理(JWT)
  4. 图表增强(更多类型)

📝 总结

本项目展示了如何使用 Node.js + Python 构建一个完整的微信群管理系统,涉及:

  • Canvas 服务端图表生成
  • 微信 Hook 消息监听
  • RESTful API 设计
  • 跨设备访问支持

希望这篇文章能给你带来启发!


关键词:Node.js, Express, Canvas, Python, 微信Hook, 图表生成, RESTful API

GitHub : [github.com/1062558134/...]

作者声明: [本项目仅限个人开发者学习使用]

相关推荐
用户572467098895621 分钟前
🔍 fzf:终端模糊查找神器,效率提升利器!🚀
后端
Jing_Rainbow22 分钟前
【AI-5 全栈-1 /Lesson9(2025-10-29)】构建一个现代前端 AI 图标生成器:从零到完整实现 (含 AIGC 与后端工程详解)🧠
前端·后端
追风少年浪子彦1 小时前
Spring Boot 使用自定义 JsonDeserializer 同时支持多种日期格式
java·spring boot·后端
bcbnb1 小时前
Charles抓包在复杂系统中的应用,高难度问题的诊断与验证方法
后端
tan180°1 小时前
Linux网络IP(下)(16)
linux·网络·后端·tcp/ip
非优秀程序员1 小时前
教程:如何修改 Docker 容器 bisheng-frontend 中的静态文件
后端
我叫黑大帅1 小时前
六边形架构?小白也能秒懂的「抗造代码秘诀」
java·后端·架构
kevinzeng1 小时前
结合Condition实现生产者与消费者示例,来进一步分析AbstractQueuedSynchronizer的内部工作机制
后端
戴着眼镜的平头哥1 小时前
前端卷Java系列之一个接口的诞生
后端