# 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. 部署要点（浓缩版）

1. **QQ 链路**：Xvfb(:99) 起 QQ → SnowLuma attach 该 QQ 进程 → SnowLuma 以 OneBot WS 主动反连 MaiBot 适配器。
2. **MaiBot**：uv venv 安装；WebUI 会话密码在 bot_config.toml（生产必须改默认值并定期轮换）。
3. **插件目录**：`/plugins/<id>/` 下放 `plugin.py` + `config.toml` + `_manifest.json`；**config.toml 即配置真源**，改动后重启宿主生效。
4. **网页聊天**：主站通过 WebSocket 直连 MaiBot 8000 端口（平台标识 `webchat`），实现网页端"直接问 AI"。

---

## 4. 三层闸门（本项目最可复用的工程经验）

LLM 的 prompt 规则**不可靠**——即使把数据塞进上下文，模型仍可能说"咱不知道/好像很挤"。所以对"服务器类必须给真实数据"的硬规矩，用三层代码硬闸门：

1. **规则注入**（`maisaka.planner.before_request` hook）：每轮向 planner 上下文注入一条【规则】。
   - ⚠️ 宿主会持久化注入的 UserMessageItem → **必须先按前缀剔掉上一轮的旧注入再重注入**，否则规则越堆越多。
2. **动作闸门**（`maisaka.planner.after_response` hook）：检查模型本轮输出，若"该调工具却打算直接回话"，把首个 FunctionCallItem **强制改写**为目标工具调用（动作/参数由代码按问题预推断，如 `#查状态 冒险服` → `mc_control(action=status, server_id=冒险服)`）。
3. **发送闸门**（`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 集成要点（血泪换来的）

1. **用官方新地址** `https://api.minekuai.cn/panel/client-api`。
   - 旧文档地址 `minekuai.com/api/client` 被创盛 WAF 的 JS 质询挡住，服务器出口任何 User-Agent 组合都返回挑战页，**不可用**。
2. **返回结构文档没写全**，实测：
   - 列表 `GET /servers` → `{"data":[{attributes:{identifier,name,is_suspended,...}}]}`
   - 详情 `GET /servers/{id}` → **顶层就是对象**（无 attributes 包装）；真实状态在 `stats.attributes.current_state`，资源在同级 `stats.attributes.resources`（无独立 /resources 端点）；顶层 `status` 字段恒为 null，别读它。
   - 实例名带 `§` 颜色码 → 必须清洗（去 § 及下一字符）。
3. 控制：`POST /servers/{id}/power {"signal": start|stop|restart|kill}` → `{"success":true}`；`POST /servers/{id}/command {"command":"..."}`。
4. 鉴权：`Authorization: Bearer <密钥>`（放插件 config.toml，**勿入代码/版本库**）；限流 300 次/分。
5. 中文别名：config `server_aliases="养老服=<实例A>;冒险服=<实例B>"` + 实例名模糊匹配，用户不用记 ID。
6. 插件运行环境禁止第三方 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. 运维速查

```bash
# 重启 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 或至少限制来源。
