MaiBot × QQ 机器人 × MC 服务器远程控制 —— 开发文档(公开版)
本文档为去敏公开版,可对外分享。内部完整归档(含凭据位置与逐日取证)见团队私有仓库。
涉及的值一律以占位符 <...> 表示;带真实配置的版本只存在于部署服务器与私有备份。
1. 项目概览
在单台云服务器上搭建一套「QQ 机器人 + AI 大脑 + Minecraft 服务器监控/控制」的完整链路:
- QQ 端:Linux 版 QQ 挂小号(协议层),SnowLuma 做 OneBot 适配(HTTP/WS),消息进 MaiBot
- 大脑:MaiBot(MaiSaka 系),负责理解自然语言、决定要不要回、怎么回
- MC 监控:主站独立 TCP 探活 + SRV 域名解析,读真实在线人数(快、轻、只读)
- MC 控制:麦块(MineKuai)面板官方 API,可开/关/重启/发命令(重、有风险、仅命令触发)
核心设计原则(本项目最重要的一条经验)
用「说话方式」区分两套 MC 系统,而不是靠模型自己猜:
| 用户输入 |
系统 |
理由 |
| 自然语言:"服务器几个人 / 冒险服在线吗" |
查人数(SRV 探活,只读) |
问询类,误伤零风险 |
# 前缀命令:"#重启 养老服" |
麦块面板 API(可控) |
显式命令 = 明确意图,才允许破坏性操作 |
教训来源:早期让模型自己判断"服务器相关就调控制 API",结果一句"服务器当前状态"被误判成要查面板,还叠加了回复通道故障,群里半天没人应。危险操作的触发必须语法显式化,不能交给自然语言猜测。
2. 架构拓扑
QQ 群/私聊 ──► SnowLuma(OneBot) ──► MaiBot (AI 大脑)
│
┌────────────────┼──────────────────┐
▼ ▼ ▼
网站后台/WebUI MC 人数监控(SRV+TCP) 麦块面板 API(仅 # 命令)
(8001→nginx) (主站 chatmiao) (api.minekuai.cn/panel/client-api)
服务清单(systemd,全部 enabled,重启自动拉起)
| 服务 |
端口 |
职责 |
| qq + SnowLuma |
3000/3001 |
LinuxQQ 小号 → OneBot 协议层 |
| maibot |
8000(WS)/8001(WebUI) |
AI 大脑、插件宿主、聊天推理 |
| 主站 chatmiao |
8010 |
MC 监控、网站、记忆/情绪存储 |
| bge-embed |
8011 |
记忆向量化服务 |
| nginx(宝塔) |
80/443/8080 |
反代:主站、WebUI(域名 + IP:8080 双入口) |
MaiBot 插件
| 插件 |
职责 |
关键机制 |
| chatmiao-bridge |
网站情绪/人设注入;query_mc_online 查人数 |
三层闸门(见 §4) |
| mc-remote-control |
# 命令 → 麦块面板控制;mc_control 工具 |
# 前缀触发 + 白名单 + 三层闸门 |
3. 部署要点(浓缩版)
- QQ 链路:Xvfb(:99) 起 QQ → SnowLuma attach 该 QQ 进程 → SnowLuma 以 OneBot WS 主动反连 MaiBot 适配器。
- MaiBot:uv venv 安装;WebUI 会话密码在 bot_config.toml(生产必须改默认值并定期轮换)。
- 插件目录:
/plugins/<id>/ 下放 plugin.py + config.toml + _manifest.json;config.toml 即配置真源,改动后重启宿主生效。
- 网页聊天:主站通过 WebSocket 直连 MaiBot 8000 端口(平台标识
webchat),实现网页端"直接问 AI"。
4. 三层闸门(本项目最可复用的工程经验)
LLM 的 prompt 规则不可靠——即使把数据塞进上下文,模型仍可能说"咱不知道/好像很挤"。所以对"服务器类必须给真实数据"的硬规矩,用三层代码硬闸门:
- 规则注入(
maisaka.planner.before_request hook):每轮向 planner 上下文注入一条【规则】。
- ⚠️ 宿主会持久化注入的 UserMessageItem → 必须先按前缀剔掉上一轮的旧注入再重注入,否则规则越堆越多。
- 动作闸门(
maisaka.planner.after_response hook):检查模型本轮输出,若"该调工具却打算直接回话",把首个 FunctionCallItem 强制改写为目标工具调用(动作/参数由代码按问题预推断,如 #查状态 冒险服 → mc_control(action=status, server_id=冒险服))。
- 发送闸门(
send_service.before_send hook):发送前核对回复文本必须包含工具返回的真实数字(如含 %/GB),缺失则整体替换为工具原文,杜绝"机器有点挤"这类凭感觉的话。
配套机制:
- 插件工具必须 visibility="visible" 直接暴露给 planner,否则被藏进 tool_search 导致多轮空转。
- 群聊里工具鉴权拿不到 kwargs.user_id → 用 chat.receive.after_process hook 记录每会话最近入站发送者 QQ,工具执行时查这张表。
5. 可用命令(# 前缀,自然语言动作词)
服务器:养老服 / 冒险服(中文名自动映射到面板实例;也支持直接传实例 ID)。
| 命令示例 |
动作 |
权限 |
#查状态 冒险服 / #状态 养老服 |
面板实时状态(状态/CPU/内存/磁盘/网络/运行时长) |
任何人(只读) |
#重启 养老服 |
正常重启 |
仅站长 |
#强制重启 冒险服 |
stop→kill→start 强启(默认关闭,需 config 开启) |
仅站长 |
#启动 养老服 / #开服 冒险服 |
开机 |
仅站长 |
#关闭 冒险服 / #停止 养老服 |
关机 |
仅站长 |
#强杀 冒险服 |
直接 kill |
仅站长 |
#发命令 冒险服 say 大家好 |
给服务器控制台发指令 |
仅站长 |
#列表 |
列出账号下全部服务器 |
任何人(只读) |
#切换目标 <实例ID> |
改变持久默认控制目标 |
仅站长 |
- 动作词解析顺序:
强制重启/强启 > 重启/重开 > 强杀/强停 > 启动/开服 > 停止/关服 > 发命令 > 列表 > 切换 > 默认 查状态。
- 只读动作(查状态/列表)任何人有权限;破坏性动作仅站长 QQ(config
admin_qqs)放行。
- 非
# 开头的任何自然语言都不触碰面板 API。
6. 麦块(MineKuai) API 集成要点(血泪换来的)
- 用官方新地址
https://api.minekuai.cn/panel/client-api。
- 旧文档地址 minekuai.com/api/client 被创盛 WAF 的 JS 质询挡住,服务器出口任何 User-Agent 组合都返回挑战页,不可用。
- 返回结构文档没写全,实测:
- 列表
GET /servers → {"data":[{attributes:{identifier,name,is_suspended,...}}]}
- 详情 GET /servers/{id} → 顶层就是对象(无 attributes 包装);真实状态在 stats.attributes.current_state,资源在同级 stats.attributes.resources(无独立 /resources 端点);顶层 status 字段恒为 null,别读它。
- 实例名带 § 颜色码 → 必须清洗(去 § 及下一字符)。
- 控制:
POST /servers/{id}/power {"signal": start|stop|restart|kill} → {"success":true};POST /servers/{id}/command {"command":"..."}。
- 鉴权:
Authorization: Bearer <密钥>(放插件 config.toml,勿入代码/版本库);限流 300 次/分。
- 中文别名:config
server_aliases="养老服=<实例A>;冒险服=<实例B>" + 实例名模糊匹配,用户不用记 ID。
- 插件运行环境禁止第三方 import → 全部用标准库
urllib + asyncio.to_thread。
7. 踩坑与解决总表(按域分类)
QQ / 协议链
| 坑 |
解 |
| 重启后 SnowLuma 不再注入新起的 QQ,链路静默失联 |
SnowLuma 配置文件开 hookAutoLoad:true 并重启 |
| QQ 起不来 |
查 /tmp/qq.log;DISPLAY=:99;进程带 --no-sandbox |
| SnowLuma 是 attach 模式 |
QQ 重启后必须重连;用 systemd 依赖顺序 + Restart=always |
MaiBot 宿主
| 坑 |
解 |
| 插件 on_load 里 pip 装重依赖 → 开机卡死 >10 分钟 |
py-spy dump --pid <bot.py> 定位阻塞线程,kill 掉 pip 子进程;重依赖先离线备好再启 |
| 重启后 8000 端口 75~100s 才就绪 |
轮询 ss -tln | grep :8000,别硬等固定秒数 |
| webchat 平台单连接:第二个连接会把第一个踢下线 |
自动化测试串行执行,间隔 sleep |
webchat 入站消息在 planner items 里包成 <message msg_id=...>\n正文 |
插件读取时剥壳取正文;纯壳(无正文)当指令行跳过 |
真人群里 @ 后 reply 报"未找到要回复的目标消息"死循环 |
宿主 reply 工具必须解析到可回复 msg_id;此适配器无纯文本 send 兜底。需宿主层改造(加兜底 send / 放宽窗口),非插件可救 |
插件 / planner 行为
| 坑 |
解 |
| 注入的 UserMessageItem 被宿主持久化,规则越堆越多 |
每轮先剔旧前缀再注入 |
| prompt 规则约束不可靠 |
三层代码闸门(§4) |
| 工具默认藏在 tool_search |
visibility="visible" |
| 群聊工具鉴权拿不到 user_id |
入站 hook 记录会话→发送者 QQ 映射表 |
| 插件间关键词抢问题 |
明确分界:人数/在线→bridge;#控制/负载→mcremote;bridge 的 _is_server_question 加排除词 |
| 子代理"验证通过"的报告不可全信 |
关键结论自己探针复测;本地↔服务器 md5 核对;有并发覆盖立即 interrupt |
网络 / 环境 / 工具
| 坑 |
解 |
| uv sync 国外 CDN 卡死 |
UV_DEFAULT_INDEX 指到阿里云 PyPI 镜像 |
| EULA 文件带尾随换行 → 死循环 |
printf '%s' 写 + systemd Environment 双保险 |
| tomlkit 增量改配置会粘连段落 |
全量读、全量重写、回读自校验 |
凭据从聊天记录转写复制变省略号 … |
401 重连风暴;凭据永远从原文件读 |
| pwsh→ssh heredoc 带 CRLF |
管道前 ($scr -replace "\r","") |
| WebUI 直连 502 |
本机代理软件干扰 → hosts 指 IP + 代理加 DIRECT 规则,或走独立端口 |
| 新端口外网不通但本机/ufw 都通 |
两层防火墙都要放行:服务器 ufw + 云厂商安全组 |
8. 运维速查
# 重启 MaiBot 并等 8000 就绪
systemctl restart maibot
for i in $(seq 1 40); do ss -tln | grep -q :8000 && break; sleep 5; done
# 宿主/插件日志
tail -f /var/log/maibot.log
# 每轮推理取证(闸门问题排查神器)
ls /www/wwwroot/MaiBot/logs/maisaka_prompt/planner/<session>/
# 自测探针(webchat 通道,串行跑)
cd /www/wwwroot/MaiBot && timeout 150 .venv/bin/python tools/probe_mc.py '你的测试问题'
# 开机卡死定位
/www/wwwroot/MaiBot/.venv/bin/py-spy dump --pid <bot.py的PID>
9. 安全模型与建议
- 破坏性动作白名单(admin_qqs)只放站长本人;白名单为空 = 不限制(生产勿空)。
- 强制重启(kill 链路)默认关闭,需要时 config 显式开启。
- 所有密钥/密码放插件或宿主 config.toml;代码与公开文档一律不含真实凭据。
- WebUI 密码曾在多份日志/配置中出现过 → 上线前必须轮换,并定期更换。
- 云端安全组遵循最小放行;面板/WebUI 建议套 HTTPS 或至少限制来源。