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 架构构建,多实例并行运行必须保证三项隔离:

  1. 用户数据目录 (User Data Directory):通过 --user-data-dir 独立指定,如 /home/ubuntu/.config/QQ_2,防止 Chromium 锁冲突与登录态覆盖。
  2. 虚拟显示服务器 (Virtual Framebuffer, Xvfb):通过 xvfb-run -a 自动分配未被占用的 Display 编号(如 :102),防止 X11 冲突。
  3. 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.target

2.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 -tpagrep 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