【QQ机器人】简易部署,仅使用官方开源python代码,无需外部框架接入
官方最近对使用AIGC接口的机器人进行了封禁,且以往的WebSocket服务将陆续不再支持,故本文旨在以最为基础的办法,不使用外部框架,利用Webhook方式接入机器人
准备工作
前往QQ开放平台注册机器人账号
沙箱配置
在“沙箱配置”中配置用于机器人测试的群聊、私聊账号以及频道信息

开发管理
在“开发管理”中可以看到机器人的ID、密钥等信息,很重要,后续需要使用。IP白名单中需要放行你的服务器公网IP

回调配置
在“回调配置”中需要配置Webhook服务使用的回调地址,需要准备一个已备案的域名,并且域名需要解析到你的公网IP上。同时,域名需要部署SSL证书(能用https访问)。
可以仿照我的后缀写,后续配置Webhook时需要使用
你的域名/qqbot-webhook/callback填写好此时会提示“校验失败”,不用着急,因为Webhook服务还没有启用,过一会儿会回来重填
篇幅原因,不介绍服务器租用、公网购买、证书部署等相关的知识,网上有文章很多可自查
添加事件省事建议全选,也可以自选需要的,选好后需要在右下角确认配置

阿里云的免费SSL个人测试证书不含CA,回调地址验证时会失败,建议使用腾讯云的免费SSL证书(腾讯云免费SSL证书)


代码部署
本文疏于解释的地方可参考官方文档和官方源代码
创建python环境
在自己的服务器上合适的位置(本文使用/opt/qqbot)创建用于管理机器人代码的文件夹,并从代码仓库导入文件
git clone https://github.com/Space-ash/QQbot.git在你的项目目录创建python虚拟环境
# 进入你的项目目录 cd /opt/qqbot # 建议使用虚拟环境 python3 -m venv .venv source .venv/bin/activate安装依赖
# 安装依赖 pip install --upgrade pip pip install fastapi uvicorn pynacl # 改为你的requirements.txt路径 pip install -r /opt/qqbot/requirements.txt端口放行
阅读官方API文档中“事件订阅与通知”一节,回调地址允许配置的端口号为:80、443、8080、8443,因此需要在服务器对应的端口配置回调代码
此外需要在服务器安全组放行6196端口
| 端口 | 描述 | 类型 |
|---|---|---|
| 6195 | 企业微信默认端口 | 可选 |
| 6199 | QQ 个人号(aiocqhttp)默认端口 | 可选 |
| 6196 | QQ 官方接口(Webhook)默认端口 | 需要 |
本文使用的是apache,如果你需要使用nginx或caddy等,可以自行查阅相关资料或询问AI
找到解析到服务器的域名对应的apache配置文件,文件名通常为"你的域名.conf"
<VirtualHost *:80> # ... # 此处是默认有的一大段代码,不用管 # ... # 只用插入这一段,反代 AstrBot 回调 ProxyPass "/qqbot-webhook/callback" "http://127.0.0.1:6196/qqbot-webhook/callback" ProxyPassReverse "/qqbot-webhook/callback" "http://127.0.0.1:6196/qqbot-webhook/callback" </VirtualHost> <VirtualHost *:443> # ... # 此处是默认有的一大段代码,不用管 # ... # 只用插入这一段,反代回调,需要在相应的地方改为你的回调地址后缀 ProxyPass "/qqbot-webhook/callback" "http://127.0.0.1:6196/qqbot-webhook/callback" ProxyPassReverse "/qqbot-webhook/callback" "http://127.0.0.1:6196/qqbot-webhook/callback" # 如果以后用 WS,把升级头转过去 RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteRule ^/qqbot-webhook/(.*) ws://127.0.0.1:6196/qqbot-webhook/$1 [P,L] </VirtualHost>重启apache服务,加载配置文件
service httpd restartqqbot_webhook.py
使用Webhook服务的主要文件,打开后你需要填入“开发管理”中查看到的机器人ID和密钥
# ======= 明文写入(仅用于你这台服务器;不要提交到仓库)======= APP_ID = "your_app_id_here" # 替换为你的 Bot AppID BOT_SECRET = "your_bot_secret_here" # Bot Secret / AppSecret # ============================================================如果需要保存日志,则将此处的LOG_DIR改为你的地址
# ---------------- 日志配置 ---------------- LOG_DIR = "/opt/qqbot/logs" # 替换为你的webhook日志地址 os.makedirs(LOG_DIR, exist_ok=True) def write_log(level: str, msg: str): """单文件临时日志,全部写到 qqbot_webhook.log""" path = os.path.join(LOG_DIR, "qqbot_webhook.log") line = f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} | {level} | {msg}\n" with open(path, "a+", encoding="utf-8") as f: f.write(line) f.flush() # -----------------------------------------回调地址后缀"/qqbot-webhook/callback"则需要改为你希望填写在“回调配置”中的地址后缀
@app.post("/qqbot-webhook/callback") # 改为你的回调地址 async def qqbot_callback(request: Request): write_log("INFO", "=== 收到Webhook请求 ===") # 读取原始 body(验签与 op=13 都可能用到) raw = await request.body()qqbot_webhook.service
service文件用于服务器后台挂载Webhook服务,需要将WorkingDirectory改为你的service地址,ExecStart改为你的unicorn地址
# /opt/qqbot/qqbot_webhook.service [Unit] Description=QQ Bot Webhook (FastAPI) After=network.target [Service] User=www-data WorkingDirectory=/opt/qqbot # 替换为你的service文件的地址 ExecStart=/opt/qqbot/.venv/bin/uvicorn qqbot_webhook:app --host 127.0.0.1 --port 6196 # 替换为你的uvicorn文件地址 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target启用Webhook服务
把你的项目路径下的service单元链接进系统搜索路径
sudo systemctl link /opt/qqbot/qqbot_webhook.service启用服务,以后每次修改Webhook相关的代码,均需要重新启用服务
sudo systemctl daemon-reload sudo systemctl enable --now qqbot_webhook sudo systemctl restart qqbot_webhook sudo systemctl status qqbot_webhook验证是否启用成功
curl -v -i http://127.0.0.1:6196/qqbot-webhook/callback \ -H 'Content-Type: application/json' \ -d '{"op":13,"d":{"plain_token":"Arq0D5UvOp","event_ts":"1741"}}'如果终端返回200 OK以及一个含有"plain_token"和"signature"的json,则说明服务启用成功了
* Trying 127.0.0.1:6196... * Connected to 127.0.0.1 (127.0.0.1) port 6196 > POST /qqbot-webhook/callback HTTP/1.1 > Host: 127.0.0.1:6196 > User-Agent: curl/8.5.0 > Accept: */* > Content-Type: application/json > Content-Length: 76 > < HTTP/1.1 200 OK HTTP/1.1 200 OK < date: Tue, 21 Oct 2025 13:37:25 GMT date: Tue, 21 Oct 2025 13:37:25 GMT < server: uvicorn server: uvicorn < content-length: 181 content-length: 181 < content-type: application/json content-type: application/json < * Connection #0 to host 127.0.0.1 left intact {"plain_token":"Arq0D5UvOp","signature":"d7d2e0...83d605"}如果出现400,404,500等各种各样的报错,可以在终端查看日志。排查错误
journalctl -u qqbot_webhook -n 100 -f成功后回到官方的“回调配置”网页,在请求地址中填入你的回调地址,发现不再提示校验失败
你的域名/qqbot-webhook/callback保存配置后,在终端进入项目的python虚拟环境,并运行机器人
# 启用虚拟环境 source .venv/bin/activate # 运行机器人 python3 ./yourbot/demo_yourbot.py可以看到终端输出,则说明机器人启动成功了
[INFO] (client.py:162)_bot_login [botpy] 登录机器人账号中... [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 2964 [INFO] (client.py:181)_bot_init [botpy] 程序启动... [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... [INFO] (gateway.py:115)ws_connect [botpy] 启动中... [INFO] (gateway.py:142)ws_identify [botpy] 鉴权中... [INFO] (gateway.py:85)on_message [botpy] 机器人「说怪话-测试中」启动成功! [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... [INFO] (demo_chimera.py:17)on_ready robot 「说怪话-测试中」 on_ready!打开QQ,私聊机器人,可以看到机器人回复了你的消息

如果机器人没有回复,可以查看logs/qqbot_webhook.log中的信息,或者利用journalctl -u qqbot_webhook -n 100 -f查看终端的运行日志进行调试
正常的配置到此就结束了!如果有后续扩展功能需求的可以往后看。
功能扩展(施工中)
鄙人能力有限,所以也只能本着摸着石头过河的原则,遇到一些对开发有用的信息便记录在此处,如有谬误,望各位大佬指出!
PS.吐槽一句官方文档太久不更新了,查阅了好多资源也比较零碎,整理不易,望海涵
发送私聊消息C2C_MESSAGE_CREATE
这是我配置的第一个测试事件,花费了大量的时间。本质上就是下面这段代码
# 目前这段代码在qqbot_webhook.py中,后续可以单独打包 import botpy from botpy.http import BotHttp from botpy.api import BotAPI from botpy.message import C2CMessage from botpy.types.gateway import MessagePayload from yourbot.demo_yourbot import MyClient # 导入 demo_chimera.py 中的 MyClient def build_message_payload(event: dict) -> MessagePayload: # 补全缺失字段 return { "author": event.get("author", {}), "channel_id": event.get("channel_id", ""), # C2C消息可能没有,可设为"" "content": event.get("content", ""), "guild_id": event.get("guild_id", ""), # C2C消息可能没有,可设为"" "id": event.get("id", ""), "member": event.get("member", {}), # C2C消息可能没有,可设为{} "message_reference": event.get("message_reference", {}), "mentions": event.get("mentions", []), "attachments": event.get("attachments", []), "seq": event.get("seq", 0), "seq_in_channel": event.get("seq_in_channel", ""), "timestamp": event.get("timestamp", ""), } # --- 普通事件(op=0)需要验签 --- if op == 0: # ... # 前面是验签的代码 # ... # 获取事件数据 event = payload.get("d", {}) event_type = payload.get("t", "") # 检查是否为 C2C 消息创建事件 if event_type == "C2C_MESSAGE_CREATE": write_log("INFO", "=== C2C_MESSAGE_CREATE事件 ===") http = BotHttp(timeout=5, app_id=APP_ID, secret=BOT_SECRET) api = BotAPI(http) event_id = event.get("id", "") payload = build_message_payload(event) message = C2CMessage(api, event_id, payload) intents = botpy.Intents(public_messages=True) client = MyClient(intents=intents) await client.on_c2c_message_create(message) 主要是最后await传递的数据格式需要对应,从而需要有正确的http,api,payload读入。因此需要查阅botpy库中的相关代码(message.py, aip.py, gateway.py, http.py),需要了解各个class之间是怎么传递数据的。
比较坑的一点是payload格式没有统一,在不同的使用场景都会缺失参数,导致C2CMessage传参错误。比如私聊消息的时候,返回的payload是不带channel_id的,所以我自己写了一个build_message_payload函数用于匹配payload格式,空缺的参数设为空,这样传参才不会报错。
关联代码片段1:demo_yourbot.py
# demo_yourbot.py import botpy from botpy import logging from botpy.ext.cog_yaml import read from botpy.message import C2CMessage test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml")) _log = logging.get_logger() class MyClient(botpy.Client): async def on_ready(self): _log.info(f"robot 「{self.robot.name}」 on_ready!") async def on_c2c_message_create(self, message: C2CMessage): _log.info(f"收到消息: {message.content} 来自 {message.author.user_openid}") await message._api.post_c2c_message( openid=message.author.user_openid, msg_type=0, msg_id=message.id, content=f"我收到了你的消息:{message.content}" ) 关联代码片段2:message.py
传参格式:C2CMessage(api, event_id, data),api来自于BotAPI,event_id来自返回的json格式的payload中的"id",data需要用函数build_message_payload修正格式后的payload
class C2CMessage(BaseMessage): __slots__ = ("author",) def __init__(self, api: BotAPI, event_id, data: gateway.MessagePayload): super().__init__(api, event_id, data) self.author = self._User(data.get("author", {})) def __repr__(self): slots = self.__slots__ + super().__slots__ return str({items: str(getattr(self, items)) for items in slots if not items.startswith("_")}) class _User: def __init__(self, data): self.user_openid = data.get("user_openid", None) def __repr__(self): return str(self.__dict__) async def reply(self, **kwargs): return await self._api.post_c2c_message(openid=self.author.user_openid, msg_id=self.id, **kwargs)关联代码片段3:api.py
传参格式:BotAPI(http),http来自于BotHttp
class BotAPI: """ 机器人相关的API接口类 使用注意: - 如果要直接使用api,可以通过client的内部成员变量,通过`self.api.xx`来使用 - 设置超时时间: Client(timeout=5) - API当前返回的所有自定义类型数据为字典数据,通过TypedDict进行类型提示 """ def __init__(self, http: BotHttp): """ Args: http (BotHttp): 用于发送请求的 http 客户端。 """ self._http = http关联代码片段4:http.py
传参格式:BotHttp(timeout, app_id, secret)
class BotHttp: """ TODO 增加请求重试功能 @veehou TODO 增加并发请求的锁控制 @veehou """ def __init__( self, timeout: int, is_sandbox: bool = False, app_id: str = None, secret: str = None, ): self.timeout = timeout self.is_sandbox = is_sandbox self._token: Optional[Token] = None if not app_id else Token(app_id=app_id, secret=secret) self._session: Optional[aiohttp.ClientSession] = None self._global_over: Optional[asyncio.Event] = None self._headers: Optional[dict] = None关联代码片段5:gateway.py
需要完整的payload,因此使用函数build_message_payload对接收到的payload进行修正
class MessagePayload(TypedDict): author: UserPayload channel_id: str content: str guild_id: str id: str member: Member message_reference: MessageRefPayload mentions: List[UserPayload] attachments: List[MessageAttachPayload] seq: int seq_in_channel: str timestamp: str