未播放
点击下方歌曲开始播放
0:00 0:00
歌单 (0)
正在加载歌单...

这篇文章记录我从零搭一个微信群聊机器人的完整过程。它不是那种"装个库就能跑"的玩具——目前它在真实的群里稳定跑了几个月,带 60 多个功能、五级管理员体系、金币经济系统、婚姻/宝宝/闺蜜/门派这些社交玩法,还有一个能远程配置的 Web 管理后台。

整套东西的技术栈非常朴素:WeChatFerry(Hook) + Python(业务) + Flask(后台) + SQLite/MySQL(存储)。没有高深的东西,难的全在细节和坑上——微信版本、消息解析、并发、穿透部署。这些我都会写在下面。

先说风险:这类机器人基于微信 PC 客户端的 Hook,属于微信官方明确不支持的行为,有封号风险。请务必使用不重要的小号,不要用于商业营销、 spam 或任何违规用途。本文只作技术学习交流。

一、它最终长什么样

先给个直观印象,机器人进群后,群成员可以直接发指令(不需要 @):

  • 签到 — 每日打卡,累计连续天数,发金币和积分
  • 查币 / 金币排行 / 富豪榜 — 一整套虚拟经济系统
  • 求婚 @某人 / 我的婚姻 / 领养宝宝 — 社交养成玩法
  • 点歌 歌名 / 猜歌 / 成语接龙 — 群内互动游戏
  • 人品 / 黄历 / 天气 郑州 / 历史上的今天 — 趣味查询
  • 群空间 — 机器人回一条带签名的链接,点开就是本群的管理面板
  • @机器人 讲个笑话 — 接 AI 模型,支持上下文记忆

管理员则有另一套 # 指令:开关任意功能、改话术模板、拉黑成员、发钻石兑换码、任命各级管理员……

二、架构:为什么要分三层

图 1 三层架构:Hook 层 / 业务层 / 管理层
图 1 三层架构:Hook 层 / 业务层 / 管理层

这是我最后稳定下来的结构。分层的收益非常直接:

层职责变化频率
Hook 层和微信 PC 客户端打交道,把消息取出来、把内容发回去微信一升级就得换版本
业务层纯 Python,只认"文本进、文本出",完全不知道微信存在每天都在加功能
管理层Flask 后台 + 云端面板,负责配置、开关、话术、统计改文案不用重启

这么切之后,我加一个新功能完全不用碰微信相关的任何代码——业务层里写一个函数、挂到分发链上就完事。而微信协议一旦变动,我也只需要换 WeChatFerry 版本,业务逻辑一行不改。

三、环境准备(最容易翻车的一步)

3.1 微信版本是硬门槛

WeChatFerry 是通过内存偏移 Hook 微信进程的,微信版本必须严格匹配。差一个小版本,注入直接失败。我用的是 3.9.12.x。

  1. 卸载电脑上的现有微信
  2. 到 github.com/tom-snow/wechat-windows-versions/releases 下载 3.9.12.x 历史版本安装包
  3. 安装后登录微信小号
  4. 立刻关闭自动更新(管理员身份运行 cmd):
reg add "HKCU\Software\Tencent\WeChat" /v "AutoUpdate" /t REG_DWORD /d 0 /f
血泪教训:微信半夜偷偷自动更新一次,第二天机器人就哑了,现象是启动报"无法连接微信"。这一步千万别省。

3.2 Python 与依赖

Python 3.9 以上,推荐 3.11。依赖少得可怜:

# requirements.txt
wcferry=39.0
PyYAML=6.0
flask=3.0
openai=1.0
requests=2.31
APScheduler=3.10
pydantic=2.0
# 进项目目录装依赖,慢就用清华源
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

3.3 常见失败速查

``现象原因
启动报"无法连接微信"微信没登录 / 版本不对 / 没以普通权限启动
注入成功但收不到消息没开 enable_receiving_msg(),或群在黑名单
能收不能发机器人被群禁言,或发送频率过高被限制
跑两天突然失效微信自动更新了,重装指定版本

四、项目结构

wechat-bot/
├── main.py                 # 启动入口:连微信 → 开接收 → 起后台 → 主循环
├── start.bat               # 一键启动(含禁更新 + 依赖检查)
├── start-all.bat           # 机器人 + frpc 穿透一起起
├── config/
│   ├── settings.yaml       # 全局配置(唯一需要你改的文件)
│   ├── frpc.toml           # 内网穿透客户端配置
│   └── mysql.yaml          # 云端 MySQL 连接信息
├── core/
│   ├── config.py           # 配置加载器
│   ├── database.py         # 基础数据层(消息、成员、群)
│   ├── database_extended.py# 扩展数据层(金币、婚姻、钻石…)
│   ├── handler.py          # 消息分发核心,1200 行,整个项目的中枢
│   ├── feature_manager.py  # 功能开关
│   ├── diamond_system.py   # 钻石 / 兑换码
│   └── marriage_manager.py # 婚姻系统
├── modules/
│   ├── game_handler.py     # 娱乐指令,近 4000 行,最大的一块
│   ├── admin_handler.py    # # 开头的管理员指令
│   ├── music_handler.py    # 点歌 / 切歌
│   ├── baby_handler.py     # 宝宝系统
│   ├── ai_chat.py          # AI 对话
│   ├── cloud_sync.py       # 本地 → 云端数据同步
│   └── scheduler.py        # 定时任务(早报、签到重置、到期检查)
├── web/                    # 本地 Flask 后台
├── cloud-admin/            # 部署到服务器上的云端面板
├── static/                 # 群卡片封面等素材
└── data/bot.db             # SQLite,首次运行自动生成

五、配置文件:只改这一个文件

config/settings.yaml 是唯一需要你动手的地方。下面是我脱敏后的版本,逐段说明:

# --- 微信 Hook 层 ---
wechat:
  wcf_port: 9999
  wechat_version: "3.9.12.51"   # 必须与实际安装版本一致

# --- AI 对话 ---
ai:
  provider: "OpenAI"
  api_key: "sk-你的密钥"        # 支持任何 OpenAI 兼容接口
  base_url: "https://api.deepseek.com"
  model: "deepseek-v4-flash"
  max_tokens: 500
  system_prompt: "你是一个微信群聊助手,回答简洁友好,每次回复不超过200字。"

bot:
  admin_wxids:                 # 最高权限,全群通用
    - "wxid_你的微信ID"
  allowed_groups: []           # 留空 = 所有群都生效
  blocked_groups: []           # 黑名单,优先级更高
  reply_delay_min: 0           # 随机延迟,防刷屏
  reply_delay_max: 0
  max_replies_per_minute: 20   # 限速

checkin:
  enabled: true
  keywords: ["签到", "打卡", "签"]

welcome:
  enabled: true
  template: "欢迎 {nickname} 加入本群!请阅读群规~"

scheduler:
  morning_news:  { enabled: true, hour: 8, minute: 0 }   # 每日早报
  checkin_reset: { enabled: true, hour: 0, minute: 5 }   # 签到重置
  expire_check:  { enabled: true, hour: 9, minute: 0 }   # 群到期提醒

web:
  enabled: true
  host: "0.0.0.0"
  port: 5000
  secret: "改成你自己的随机串"    # 群空间链接签名密钥
  admin_password: "别用 admin123"

cloud:
  enabled: true
  server_url: "http://你的服务器"
  sync_token: "与云端 config.py 保持一致"   # 不一致就同步不了
  sync_interval: 300

database:
  path: "data/bot.db"
安全提醒:如果你要把配置贴到博客、GitHub 或任何公开地方,务必先替换掉 api_key、secret、admin_password、sync_token 和真实 wxid。我见过太多人把密钥直接传上去,几小时后就收到账单。

六、第一次启动

双击 start.bat,或者命令行 python main.py。成功的样子:

==================================================
  微信群聊机器人 v1.0
  三层架构: Hook + 业务逻辑 + Web后台
==================================================
[启动] 配置加载完成
[启动] 数据库初始化完成
[启动] 正在连接微信...
[启动] 微信连接成功,机器人 wxid: wxid_xxxxxxxx
[启动] 消息接收已开启
[启动] 本地Web后台已启动: http://0.0.0.0:5000
[SYNC] 云端同步已启动,间隔 300秒
[运行] 机器人已上线,开始监听群消息...

看到最后一行就成了。控制台打印的 wxid_xxxxxxxx 就是机器人自己的 ID,如果你要找自己的 wxid,最省事的办法是:把机器人拉进群后 @ 一下自己,或者直接发一条消息,看日志里 sender 字段。

然后把机器人小号拉进群,发个 签到 试试。

七、一条消息是怎么跑完一圈的

图 2 一条群消息从进入到回复的完整链路
图 2 一条群消息从进入到回复的完整链路

这是整个项目最核心的一段。主线程只做一件事——从队列里取消息,然后丢给线程池:

while wcf.is_receiving_msg():
    try:
        msg = wcf.get_msg()
        if msg:
            threading.Thread(
                target=_safe_handle, args=(wcf, msg), daemon=True
            ).start()
    except queue.Empty:
        time.sleep(0.1)          # 队列空很正常,别空转烧 CPU

为什么必须开线程?点歌要去请求音乐接口,AI 对话可能要等三四秒。如果串行处理,一条慢消息就能堵死后面所有人的消息,群里表现为"机器人卡住了"。

7.1 过滤:不该管的直接丢掉

进 handle_message 第一件事是三道过滤:非群消息直接返回、群不在白名单直接返回、发消息的人被关了小黑屋直接返回(总管理员例外)。

7.2 昵称解析:一个真实的坑

你以为拿群昵称很简单?实际上微信返回的数据经常是脏的——有时会直接给你一个 wxid_xxx 当昵称,导致群里显示"wxid_ab12 签到成功"。我的解法是四级兜底:

  1. 查本地 members 表,排除掉以 wxid_ 开头的污染数据
  2. 调 get_alias_in_chatroom() 拿群名片(最准)
  3. 调 get_info_by_wxid() 拿微信全局昵称
  4. 拉一次全群成员列表,顺便批量写回数据库
# 数据库有记录但昵称是 wxid(污染数据),置空以便重新获取
if mem and mem.get("nickname") and not mem["nickname"].startswith("wxid_"):
    nickname = mem["nickname"]
else:
    nickname = ""

7.3 @ 解析:必须读 XML

群里 @ 某人的时候,msg.content 里只有"@昵称"三个字,拿不到 wxid。而业务上我们必须用 wxid(昵称会重复、会改)。真正的 wxid 藏在消息的 XML 里:

m = re.search(r'atuserlist(.*?)/atuserlist', xml, re.DOTALL)
raw = m.group(1).strip()
candidates = re.split(r'[,,\s]+', raw)
result = [w for w in candidates if w.startswith('wxid_')]

解析不到时还有个 fallback:拿 content 里 @ 后面的昵称去群成员表里反查。这一层容错很有必要,因为不同微信版本的 XML 结构会有细微差别。

7.4 指令分发:顺序就是优先级

过滤和身份识别做完,才进入分发。顺序绝对不能乱:

# 1. 管理员 # 指令(即使群到期也要能用)
# 2. 到期拦截:群过期后只保留管理指令
# 3. 游戏 / 娱乐指令(60+ 功能)
# 4. 音乐点歌
# 5. 违禁词检查
# 6. 自定义关键词回复
# 7. @机器人 → AI 对话

把"管理指令"放在"到期拦截"之前,是为了保证群到期之后管理员依然能进去续费、配置,不至于把自己锁死在门外。这种细节只有真跑起来才会想到。

7.5 群空间链接:别裸奔

机器人回复的群空间链接是带签名的,防止别人改个群 ID 就能进别人的面板:

ts = str(int(time.time()))
raw = f"{bot_wxid}{roomid}{ts}{secret}"
sign = hashlib.sha1(raw.encode()).hexdigest()
url = f"{cloud_url}/grouplogin.php?timestamp={ts}sign={sign}robotid={bot_wxid}qid={roomid}"

服务端用同样的算法算一遍比对,同时对 timestamp 做有效期校验。简单、够用。

八、自己加一个功能

这是我改动最频繁的地方,流程固定三步。以新增一个"今日运气"为例:

第 1 步:写处理函数

# modules/game_handler.py
import random

def handle_lucky(wcf, msg, roomid, sender, nickname):
    text = msg.content.strip()
    if text not in ["运气", "今日运气"]:
        return False            # 不是我的指令,交还给分发链

    luck = random.randint(1, 100)
    if luck = 90:
        comment = "大吉!今天适合表白、适合辞职、适合买彩票"
    elif luck = 60:
        comment = "小吉,稳中有进"
    elif luck = 30:
        comment = "平平淡淡才是真"
    else:
        comment = "诸事不宜,建议躺平"

    wcf.send_text(f"【今日运气】\n{nickname}:{luck} 分\n{comment}", roomid)
    return True                 # 已处理,分发链到此为止

第 2 步:挂到分发链上

def handle_game_command(wcf, msg, roomid, sender, nickname):
    text = msg.content.strip()

    if text in ["签到", "打卡"]:
        return handle_checkin(wcf, msg, roomid, sender, nickname)

    # ↓ 加在这里,位置决定优先级
    if handle_lucky(wcf, msg, roomid, sender, nickname):
        return True
    ...

三条必须记住的规则

  1. 不命中就 return False,命中才 return True。返回 True 会让分发链立即停止,后面的关键词、AI 都不会再触发。
  2. 顺序即优先级。如果"签到"被前面的模糊匹配吃掉,签到就永远触发不了。加功能时想清楚它该排在哪。
  3. 不要在分发链里做耗时操作。要请求外部接口就开线程,或者先回一句"查询中",拿到结果再发第二条。

九、Web 管理后台与群空间

本地 Flask 跑在 :5000,能看群列表、消息统计、成员、签到排行,也能改话术模板和功能开关。

更实用的是群空间:群成员在群里发 群空间,机器人回一条签名链接,点开就是本群专属的配置面板——群主可以自己开关 60 多个功能、改欢迎语、看本群数据,不需要来找我。默认密码是 qwe123,第一次进去务必改掉。

十、让外网也能访问:frp 穿透

图 3 内网穿透 + Nginx 反代的部署拓扑
图 3 内网穿透 + Nginx 反代的部署拓扑

机器人跑在你自己的 Windows 上,本地 localhost:5000 群友当然打不开。解法是内网穿透,我用的是自建 frp。

服务端(云服务器)

  1. 下载 frp,配置 frps.toml:bindPort = 7000 + 自定义 token
  2. 启动 ./frps -c frps.toml,安全组放行 7000 / 80 / 443
  3. Nginx 反代到本地 6000 端口,server_name 写你的域名
  4. DNS 加一条 A 记录指向服务器公网 IP

客户端(你的电脑)

# config/frpc.toml
serverAddr = "你的云服务器公网IP"
serverPort = 7000
auth.token = "你的自定义密钥-和服务端保持一致"

[[proxies]]
name = "wechat-bot-web"
type = "http"
localIP = "127.0.0.1"
localPort = 5000
customDomains = ["bot.example.com"]

把 frpc.exe 放进项目根目录,双击 start-all.bat 会同时拉起穿透和机器人。

没有云服务器也没关系,可以用 cpolar、natapp 这类现成的穿透服务,免费版会给你一个随机子域名,够自己用了。

十一、权限体系

图 4 五级管理员体系
图 4 五级管理员体系

五级权限里,只有"最高权限"是写在 yaml 配置文件里的,其余都存在数据库、可以在群里任命:

@某人 指令777    # 设为至尊管理(仅至尊 / 最高权限可用)
@某人 指令666    # 设为超级管理
@某人 指令555    # 设为普通管理(超级管理以上可用)
@某人 指令444    # 取消管理

设计上有一个刻意的安排:最高权限不受到期限制。这样即使所有群都过期了、数据库出问题了,我依然能用 yaml 里的 ID 进去救场。

十二、指令速查

完整指令 100 多条,群里发 菜单 就能看到全部。这里列最常用的:

| --- | --- |
| 基础 | 菜单 签到 我的名片 群信息 群空间 |
| 经济 | 我的金币 查币 转账 @某人 100 送花 @某人 金币排行 |
| 社交 | 求婚 @某人 我的婚姻 领养宝宝 结闺蜜 @某人 抓奴隶 @某人 |
| 游戏 | 抽签 抽盲盒 打劫 @某人 猜数字 猜成语 成语接龙 |
| 趣味 | 人品 运势 黄历 情话 天气 城市 历史上的今天 |
| 音乐 | 点歌 歌名 下一首 换乐库 |
| 钻石 | 钻石 兑换 兑换码 购买体力 |
| 管理 | #功能列表 #开启 功能名 #关闭 功能名 #我的权限 #管理列表 |

## 十三、踩过的坑(建议收藏)

1. **微信自动更新** — 头号杀手。装好就禁,一劳永逸。
2. **用主号** — 封号是真会发生的,用注册很久的小号,别绑银行卡。
3. **昵称污染** — 微信偶尔返回 wxid 当昵称,必须过滤,否则群里全是"wxid_xxx 签到成功"。
4. **@ 解析** — 只能从 `atuserlist` 里拿 wxid,从 content 里解析昵称是不可靠的。
5. **耗时操作串行** — 点歌和 AI 必须开线程,否则整条消息队列堵塞。
6. **群到期把管理员也锁外面** — 管理指令一定要排在到期拦截之前。
7. **云同步 token 不一致** — 本地 yaml 和云端 config.py 的 `sync_token` 必须一字不差,不同步时先查这个。
8. **回复太快** — 用 `reply_delay` 和每分钟条数限制,刷屏既影响体验也容易被微信限制。
9. **多群并发写 SQLite** — 人多了会 `database is locked`,要么加写队列,要么迁 MySQL。
10. **群空间默认密码** — 一定要改,那个面板能改本群所有配置。

## 十四、还能怎么扩展

- **接更多 AI 能力** — 目前 `ai_chat.py` 用的是 OpenAI 兼容格式,换 base_url 就能切到任意模型;加个图片理解也只是几行。
- **把数据搬到 MySQL** — 项目里已经有 `migrate_to_mysql.py`,多群大规模跑的时候必须迁。
- **开机自启** — Windows 任务计划程序里挂 `start-all.bat`,配合云服务器就能 7×24 在线。
- **加 HTTPS** — 群空间链接是发给所有群友的,上 Let's Encrypt 免费证书,别让密码明文跑在 HTTP 上。

## 最后

整个项目最难的不是写代码,是跟微信版本、消息脏数据、并发这些琐事死磕。但一旦跑通,能玩的空间非常大——它本质上就是一个"群消息进、任意服务出"的框架,你可以往里塞任何东西:接自己的业务系统、接数据库查询、接定时任务提醒,甚至当运维告警通道用。

如果你也准备搭一个,我的建议是:**先别急着加功能,先把"环境跑通 + 消息能收能发"这一步做扎实**,剩下的都是往上堆积木而已。

添加新评论

文章目录