NapCat 多实例部署与 AstrBot 入群门槛审核 (NapCat Multi-Instance & AstrBot Join Moderation)
1. 架构与拓扑 (Architecture & Topology)
在单台 Linux 服务器上同时托管多个 QQ 机器人账号,并由 AstrBot 统一进行消息分发与群管审计时,系统拓扑如下:
+-------------------------------------------------------------------------+
| Linux Server |
| |
| [NapCat 实例 1: 1369934060] (ReadOS Bot) |
| - 运行时: xvfb-run /opt/QQ/qq --user-data-dir=~/.config/QQ |
| - OneBot 11: 反向 WebSocket 客户端 -> ws://127.0.0.1:6199/ws |
| |
| [NapCat 实例 2: 1808666025] (群管小号) |
| - 运行时: xvfb-run -a /opt/QQ/qq --user-data-dir=~/.config/QQ_2 |
| - 工作目录: NAPCAT_WORKDIR=~/Napcat_2, WebUI 端口 6100 |
| - OneBot 11: 反向 WebSocket 客户端 -> ws://127.0.0.1:8082/ws |
| |
| | (Reverse WS) |
| v |
| +-------------------------------------------------------------------+ |
| | AstrBot (Core & Platform Manager) | |
| | - aiocqhttp 适配器 Rap: 监听 6199 (绑定 1369934060) | |
| | - aiocqhttp 适配器 me: 监听 8082 (绑定 1808666025) | |
| | - WebUI 仪表盘: 监听 6185 | |
| +-------------------------------------------------------------------+ |
| | (Event Bus: on_request) |
| v |
| +-------------------------------------------------------------------+ |
| | 群管插件 (astrbot_plugin_group_guardian / F1 审核引擎) | |
| | - 入群申请事件拦截: _handle_group_request(event) | |
| | - OneBot API 调用: get_stranger_info (获取等级) | |
| | - 门槛裁决: QQ等级 >= 32 批准入群,< 32 自动拒绝并回传理由 | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+2. NapCat 多实例隔离部署 (NapCat Multi-Instance Isolation)
2.1 隔离核心要素
Linux QQ 基于 Electron / Chromium 架构构建,多实例并行运行必须保证三项隔离:
- 用户数据目录 (User Data Directory):通过
--user-data-dir独立指定,如/home/ubuntu/.config/QQ_2,防止 Chromium 锁冲突与登录态覆盖。 - 虚拟显示服务器 (Virtual Framebuffer, Xvfb):通过
xvfb-run -a自动分配未被占用的 Display 编号(如:102),防止 X11 冲突。 NapCat 配置与 WebUI 端口 (Working Directory & Port):
- 环境变量
NAPCAT_WORKDIR=/home/ubuntu/Napcat_2 - 环境变量
NAPCAT_WEBUI_PREFERRED_PORT=6100
- 环境变量
2.2 Systemd 服务配置 (napcat-bot2.service)
[Unit]
Description=NapCat QQ Bot 2
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/Napcat
Environment=NAPCAT_WORKDIR=/home/ubuntu/Napcat_2
Environment=NAPCAT_WEBUI_PREFERRED_PORT=6100
ExecStart=/usr/bin/xvfb-run -a /home/ubuntu/Napcat/opt/QQ/qq --no-sandbox --user-data-dir=/home/ubuntu/.config/QQ_2
Restart=always
RestartSec=10
TimeoutStopSec=30
KillMode=control-group
[Install]
WantedBy=multi-user.target2.3 OneBot 11 反向 WebSocket 连接配置
编辑 /home/ubuntu/Napcat_2/config/onebot11_<uin>.json:
{
"network": {
"httpServers": [],
"websocketServers": [],
"websocketClients": [
{
"enable": true,
"name": "astrbot-adapter-me",
"url": "ws://localhost:8082/ws",
"reportSelfMessage": false,
"messagePostFormat": "array",
"token": "YOUR_TOKEN",
"debug": false,
"heartInterval": 30000,
"reconnectInterval": 15000,
"verifyCertificate": true
}
]
}
}3. AstrBot 插件激活与配置机制 (Plugin Lifecycle & Activation)
3.1 插件禁用黑名单 (Inactivated Plugins)
AstrBot 的插件启用状态保存在 SQLite 数据库 data/data_v4.db 的 preferences 表中,键名为 inactivated_plugins:
# 检查与剔除禁用列表
import sqlite3, json
conn = sqlite3.connect("/home/ubuntu/workspace/ops/astrbot/data/data_v4.db")
cur = conn.cursor()
row = cur.execute("SELECT value FROM preferences WHERE key = 'inactivated_plugins'").fetchone()
if row:
val = json.loads(row[0])
plugins = val.get("val", [])
target = "data.plugins.astrbot_plugin_group_guardian.main"
if target in plugins:
plugins.remove(target)
val["val"] = plugins
cur.execute("UPDATE preferences SET value = ? WHERE key = 'inactivated_plugins'", (json.dumps(val),))
conn.commit()
conn.close()仅修改插件本地配置文件(如 astrbot_plugin_group_guardian_config.json)无法使插件生效;若其全类名在 inactivated_plugins 中,启动时将被 StarManager 直接跳过实例化。
4. QQ 等级计算与入群自动审核实现 (QQ Level Moderation)
4.1 NapCat 等级计算底层原理
OneBot 11 规范中 get_stranger_info 返回的 data 对象中包含 qqLevel。NapCat 底层通过拉取 QQ 资料卡片图标计算:
$$\text{qqLevel} = \text{crownNum} \times 64 + \text{sunNum} \times 16 + \text{moonNum} \times 4 + \text{starNum} \times 1$$
- 1 皇冠 = 64 级
- 2 太阳 = 32 级
- 1 太阳 = 16 级
- 1 月亮 = 4 级
- 1 星星 = 1 级
4.2 入群审核逻辑设计 (membership.py)
为规避「隐藏 QQ 等级」导致的误杀,将审核依据迭代为不可被隐藏的底层元数据——账号注册时间戳 (regTime),并关闭群内通知以实现静默审核:
# 1. 读取最晚注册年份门槛(默认 2024 年)
max_year = self._cfg_int("join_max_reg_year", 2024, group_id=group_id)
applicant_reg_str = ""
if max_year > 0:
uid_int = self._safe_int(user_id, 0)
client = await self._get_client(event)
if client and uid_int:
ok, data, err = await self._call_group_api_result(
client, "get_stranger_info", "查询申请人账号信息",
user_id=uid_int, no_cache=True,
)
if ok and isinstance(data, dict):
raw_reg = data.get("regTime") or data.get("reg_time")
reg_ts = self._safe_int(raw_reg, 0)
if reg_ts > 0:
import datetime
tz_cst = datetime.timezone(datetime.timedelta(hours=8))
cutoff_dt = datetime.datetime(max_year, 1, 1, 0, 0, 0, tzinfo=tz_cst)
cutoff_ts = int(cutoff_dt.timestamp())
reg_dt = datetime.datetime.fromtimestamp(reg_ts, tz=tz_cst)
applicant_reg_str = reg_dt.strftime("%Y-%m-%d")
# 2024 年及以后注册的新号:自动拒绝
if reg_ts >= cutoff_ts:
reject_reason = (
self._cfg_str("join_reg_reject_reason", "", group_id=group_id)
or f"加群要求账号注册时间早于 {max_year} 年(当前账号注册于 {applicant_reg_str}),未达门槛"
)
return await self._complete_group_request(
event, flag, sub_type, False, reject_reason,
group_id, user_id, comment,
f"注册时间过晚({applicant_reg_str}>={max_year}-01-01)",
)
else:
logger.warning(f"[GroupMgr] 申请人 {user_id} 未能获取有效注册时间,转人工审核")
else:
logger.warning(f"[GroupMgr] 获取申请人 {user_id} 信息失败: {err},跳过自动拦截")4.3 达标自动放行与静默模式
早于 2024 年注册的老号进入 default_action == "accept" 自动放行。同时将 join_audit_notify: false,保证审核全程在后台静默执行,绝不在群聊中推送任何广播,避免群内其他 Bot 产生接话闭环:
default_action = rule.get("default_action", "manual")
if default_action == "accept":
audit_msg = (
f"老号达标({applicant_reg_str}<{max_year}),自动通过"
if (applicant_reg_str and max_year > 0)
else "默认通过"
)
return await self._complete_group_request(
event, flag, sub_type, True, "",
group_id, user_id, comment, audit_msg,
)5. 关键排障记录 (Troubleshooting & Lessons Learned)
| 症状 / 异常 | 根本原因 (Root Cause) | 解决措施 (Resolution) | |
|---|---|---|---|
[Errno 98] Address already in use 端口冲突 | 试图启动独立脚本绑定 8082,而 AstrBot 已将 8082 声明为反向 WebSocket 服务端口 | 放弃外置独立桥接脚本,直接复用 AstrBot 已有连接与群管插件架构 | |
| AstrBot WebUI 扫码成功但适配器显示未连接 | 重启 AstrBot 后,NapCat 客户端重连计时器为 30s 周期 | 耐心等待 15~30 秒或观察 `ss -tpa | grep 8082 变为 ESTAB` |
AttributeError: '_Harness' object has no attribute '_config_schema' | 单元测试 mock 类未实例化 schema,直接访问未初始化属性 | 在 utils.py 中使用 getattr(self, "_config_schema", None) or {} 进行防御性获取 | |
TypeError: _notify_join_audit() got an unexpected keyword argument 'event' | 测试桩以 *args 捕获通知参数,修改签名增加关键字传参导致签名不匹配 | 保持原 5 个位置参数签名不变,将请求上下文 event 缓存在类内部上下文属性中 | |
| 误触发群消息撤回与禁言 | 启用群管插件时默认全功能开启(违禁词过滤、刷屏撤回) | 显式将 auto_moderate_enabled: false、anti_flood_enabled: false,仅保留入群自动审核 join_audit_enabled: true | |
| 申请人真实等级较高却被识别为 0 级拒审 | 申请人在 QQ 隐私设置中开启了【隐藏 QQ 等级】(isHideQQLevel: 1),腾讯服务器对陌生人隐匿了等级图标,OneBot 计算结果强制为 0 | 绝不能将 qqLevel < 32 作为硬性拒绝条件;对 isHideQQLevel <mark> 1 或 qqLevel </mark> 0 应转人工审核(manual)或辅助校验账号注册时长(regTime) |
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps