Repository navigation
about plugin
本指南面向想要为 McBotConsole 编写插件的开发者。
McBotConsole 的插件系统允许你在不修改主程序的情况下扩展功能。
插件可以:
- 监听假人登录、被踢、断开等事件
- 修改连接参数(如换版本、加 brand)
- 让假人发送聊天消息
- 读写日志
- 请求后端数据(如 Bot 列表、代理池状态)
- 在特定事件触发时执行自定义逻辑
插件不能:
- 直接修改 C# 主程序
- 修改 main backend 的核心流程
- 绕过权限系统访问受限接口
- 阻止主流程执行(钩子是"观察者",不能中断)
支持两种语言:
| 语言 | 优势 | 劣势 |
|---|---|---|
| Node.js | 与后端同语言,可同步调用 API | 需要较新的 Node 环境 |
| Python | 生态丰富,编写简单 | 需要本机装 Python,通信靠 stdin/stdout |
1. 创建目录:
backend/plugins/hello-world/
└── hello-world.js
.js 文件名,也等于元数据的 name 字段。
2. 写入代码:
module.exports = {
name: 'hello-world',
version: '1.0.0',
author: 'YourName',
description: '一个最小示例插件',
usage: '安装后自动生效',
requiredPermission: 0,
init: function (config, hooks, logger, api) {
logger.info('Hello World 插件已加载');
hooks.register('onLogin', (bot, ctx) => {
logger.info(`Bot ${ctx.botId} (${ctx.playerName}) 登录成功`);
});
},
unload: function (reason, logger) {
logger.info('Hello World 插件已卸载,原因: ' + reason);
}
};3. 重启程序,在主界面日志区应看到:
[插件] ✅ 加载成功: hello-world v1.0.0 (权限 0)
然后在【🔌 插件管理】里能看到它。
1. 创建目录:
backend/plugins_py/hello-py/
├── plugin.json
└── main.py
2. 写 plugin.json:
{
"name": "hello-py",
"version": "1.0.0",
"author": "YourName",
"description": "Python 最小示例插件",
"usage": "安装后自动生效",
"main": "main.py",
"requiredPermission": 0
}3. 写 main.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import json
def send(obj):
"""向主程序发送消息"""
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + '\n')
sys.stdout.flush()
def log(msg):
send({"type": "log", "level": "info", "message": str(msg)})
def main():
log("Hello Py 插件启动")
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except Exception as e:
log(f"JSON 解析失败: {e}")
continue
msg_type = msg.get("type")
if msg_type == "init":
log(f"初始化,权限={msg.get('permission', 0)}")
elif msg_type == "hook":
event = msg.get("event")
data = msg.get("data", {})
if event == "onLogin":
log(f"Bot {data.get('botId')} ({data.get('playerName')}) 登录成功")
elif msg_type == "shutdown":
log("收到关闭指令")
break
if __name__ == "__main__":
main()4. 重启程序,应看到:
[Python插件] ✅ 加载成功: hello-py v1.0.0 (权限 0)
backend/
└── plugins/
└── <插件名>/ ← 文件夹名 = 插件名
├── <插件名>.js ← 文件名 = 插件名(必须)
├── config/
│ └── <插件名>.json ← 自动生成,插件配置
└── log/
├── <插件名>.log ← 自动生成,插件日志
└── archive/ ← 归档目录
命名限制:
- 只能用字母、数字、下划线、连字符
- 不能以数字开头
- 不能有中文
-
三个名字必须一致:文件夹名 = JS 文件名 =
name字段
backend/
└── plugins_py/
└── <插件名>/ ← 文件夹名 = 插件名
├── plugin.json ← 元数据(必须)
├── main.py ← 入口文件(名字可自定义,在 plugin.json 的 main 字段指定)
├── config.json ← 自动生成,插件配置
└── log/
└── <插件名>.log ← 自动生成
在 .js 文件的 module.exports 中定义:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | ✅ | 插件名,必须等于文件夹名 |
version |
string | 版本号,建议语义化 | |
author |
string | 作者 | |
description |
string | 一句话简介 | |
usage |
string | 使用说明 | |
requiredPermission |
number | 最低权限要求(0-3),默认 0 | |
defaultConfig |
object | 默认配置对象 | |
init |
function | ✅ | 初始化函数 |
unload |
function | 卸载函数 |
在 plugin.json 中定义:
{
"name": "my-plugin",
"version": "1.0.0",
"author": "YourName",
"description": "插件简介",
"usage": "使用说明",
"main": "main.py",
"requiredPermission": 0,
"defaultConfig": {
"key1": "value1",
"key2": 123
}
}main 字段指定入口 Python 文件名,默认 main.py。
程序启动
↓
扫描 plugins/ 和 plugins_py/ 目录
↓
对每个插件:
├─ 读元数据
├─ 检查 requiredPermission ≤ 用户分配的权限
├─ 权限不足 → 跳过
├─ 权限足够 → 加载
↓
Node: 调用 init()
Python: 发送 {"type":"init", ...}
↓
┌──────────────────────────────┐
│ 运行中:收到钩子事件 │
│ → 触发对应 handler │
└──────────────────────────────┘
↓
程序关闭 / 用户手动卸载
↓
Node: 调用 unload()
Python: 发送 {"type":"shutdown"}
插件通过 hooks.register(event, handler) 注册钩子。
hooks.register('onLogin', (bot, ctx) => { /* ... */ });{
"type": "hook",
"event": "onLogin",
"data": { "botId": 1, "playerName": "Peter42", "version": "1.20.1" }
}| 事件 | 触发时机 | 参数 |
|---|---|---|
beforeConnect |
TCP 连接建立之前 | (botConfig, ctx) |
afterConnect |
TCP 连接建立之后 | (bot, ctx) |
onLogin |
登录成功 | (bot, ctx) |
onKick |
被服务器踢出 | (reason, ctx) |
onError |
连接出错 | (err, ctx) |
onDisconnect |
断开连接 | (reason, ctx) |
connectionResult |
每次尝试结束(成功或失败) | (payload) |
所有钩子都会收到 ctx 参数:
{
botId: 1, // Bot 编号(整数)
playerName: "Peter42", // 假人名字
version: "1.20.1" // 游戏版本
}{
success: true, // 是否登录成功
duration: 1523, // 耗时(毫秒)
errorType: "Timeout", // 失败类型(成功时为 "Unknown" 或空)
botId: 1
}errorType 可能的值:
| 值 | 含义 |
|---|---|
Kicked |
被服务器踢出 |
AntiBot |
被反机器人插件拦截 |
Throttled |
被限流 |
Timeout |
连接超时 |
ECONNRESET |
连接被重置 |
ECONNREFUSED |
连接被拒绝 |
VersionMismatch |
版本不匹配 |
Disconnected without login |
未登录就断开 |
CreateBotError |
创建 Bot 失败 |
PluginError |
插件异常 |
Unknown |
未知 |
module.exports = {
name: 'event-logger',
version: '1.0.0',
author: 'Lx934',
init: function (config, hooks, logger, api) {
hooks.register('beforeConnect', (botConfig, ctx) => {
logger.info(`[${ctx.botId}] 准备连接 ${botConfig.host}:${botConfig.port}`);
// 可以修改 botConfig,比如换版本
// botConfig.version = '1.20.1';
});
hooks.register('afterConnect', (bot, ctx) => {
logger.info(`[${ctx.botId}] TCP 已连接`);
});
hooks.register('onLogin', (bot, ctx) => {
logger.info(`[${ctx.botId}] ✅ 登录成功: ${ctx.playerName}`);
});
hooks.register('onKick', (reason, ctx) => {
logger.error(`[${ctx.botId}] 被踢: ${reason}`);
});
hooks.register('onError', (err, ctx) => {
logger.error(`[${ctx.botId}] 错误: ${err.message}`);
});
hooks.register('onDisconnect', (reason, ctx) => {
logger.info(`[${ctx.botId}] 断开: ${reason}`);
});
hooks.register('connectionResult', (payload) => {
logger.info(`[${payload.botId}] 结果: ${payload.success ? '成功' : '失败'} (${payload.duration}ms)`);
});
},
unload: function (reason, logger) {
logger.info('卸载: ' + reason);
}
};插件权限是用户在 UI 里分配的,与插件自己的 requiredPermission 独立。
| 等级 | 名称 | 允许的操作 |
|---|---|---|
| 0 | 只读 | 读配置、写日志、注册钩子、监听事件 |
| 1 | 可聊天 | 权限 0 + 让 bot 发聊天消息 |
| 2 | 可控制 | 权限 1 + 修改运行时配置、启停任务 |
| 3 | 系统级 | 权限 2 + 创建子窗口、读写任意文件、加载/卸载插件 |
如果 用户分配的权限 < 插件声明的 requiredPermission
→ 跳过该插件,记录错误日志
否则
→ 加载
init: function (config, hooks, logger, api) {
// 权限检查要由插件自己做(针对具体操作)
hooks.register('onLogin', (bot, ctx) => {
// 这里假设需要权限 1 才能聊天
// 但插件无法直接读到"当前权限" —— 需要通过设置 requiredPermission 声明
try {
bot.chat('Hello');
} catch (e) {
logger.error('发送失败: ' + e.message);
}
});
}最佳实践:
- 需要用 chat →
requiredPermission: 1 - 需要用 setConfig →
requiredPermission: 2 - 需要用 createConsole →
requiredPermission: 3 - 只监听事件、写日志 →
requiredPermission: 0
用于写日志。日志同时输出到:
- 插件自己的日志文件
log/<插件名>.log - 主程序日志区(供用户实时查看)
| 方法 | 说明 |
|---|---|
logger.info(msg) |
普通信息(白色) |
logger.error(msg) |
错误信息(红色) |
logger.debug(msg) |
调试信息(灰色,仅当主程序 debug=true 时输出) |
logger.close() |
关闭日志(一般不用手动调用) |
示例:
logger.info('这是一条信息');
logger.error('出错了: ' + err.message);
logger.debug('调试信息,只在 debug 模式显示');| 方法 | 说明 |
|---|---|
hooks.register(event, handler) |
注册钩子 |
限制:
- 同一个事件可以注册多个 handler
- 每个 handler 有 5 秒超时,超时会被跳过
- handler 抛异常会被捕获,记录到日志
| 方法 | 最低权限 | 说明 |
|---|---|---|
api.createConsole(options) |
3 | 创建子控制台窗口 |
api.writeToConsole(line) |
3 | 向子控制台写一行 |
api.closeConsole() |
3 | 关闭子控制台 |
注意:子控制台功能暂未实现(当前返回 NotImplemented)。
bot 是 mineflayer 的实例。可以访问所有 mineflayer 的 API。
常用属性:
| 属性 | 说明 |
|---|---|
bot.username |
当前用户名 |
bot.entity |
实体对象(含 position) |
bot.health |
血量(0-20) |
bot.food |
饱食度(0-20) |
bot.players |
在线玩家字典 |
bot.time |
世界时间 |
bot.game |
游戏设置 |
常用方法:
| 方法 | 说明 |
|---|---|
bot.chat(msg) |
发送聊天 |
bot.quit(reason) |
主动断开 |
bot.setControlState(control, state) |
控制移动 |
bot.look(yaw, pitch) |
看向某处 |
bot.equip(item, dest) |
装备物品 |
bot.dig(block) |
挖方块 |
完整 API:见 mineflayer 官方文档。
常用示例:
hooks.register('onLogin', (bot, ctx) => {
// 位置
const pos = bot.entity.position;
logger.info(`位置: ${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}`);
// 血量
logger.info(`血量: ${bot.health} / 食物: ${bot.food}`);
// 在线玩家
const others = Object.keys(bot.players).filter(n => n !== bot.username);
logger.info(`在线玩家: ${others.join(', ')}`);
// 发送聊天
bot.chat('Hello from plugin!');
});一个 Node 插件是一个 CommonJS 模块:
module.exports = {
// 元数据
name: 'my-plugin',
version: '1.0.0',
// ...
// 生命周期
init: function (config, hooks, logger, api) {
// 初始化逻辑
},
unload: function (reason, logger) {
// 清理逻辑
}
};不支持:
- ES Module 语法(
import/export) - TypeScript(需自己编译成 JS)
- 动态
require(主程序不追踪依赖)
可以在插件里 require npm 包,但必须确保包已安装:
cd backend
npm install lodash --save然后在插件里:
const _ = require('lodash');node_modules,或者让用户自己装。
let intervalId = null;
init: function (config, hooks, logger, api) {
intervalId = setInterval(() => {
logger.info('定时任务执行');
}, 60000); // 每分钟
},
unload: function (reason, logger) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}unload 里清理定时器,否则会泄漏。
init: function (config, hooks, logger, api) {
hooks.register('onLogin', async (bot, ctx) => {
// 钩子支持 async
await new Promise(r => setTimeout(r, 1000));
logger.info('1 秒后执行');
});
}const fs = require('fs');
const path = require('path');
const dataPath = path.join(__dirname, 'data.json');
function loadData() {
try {
return JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
} catch {
return {};
}
}
function saveData(data) {
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2), 'utf-8');
}Python 插件与主程序通过 stdin / stdout 通信,每行一个 JSON。
send() 函数往 stdout 写,不要用 print(),否则会污染协议。
初始化:
{
"type": "init",
"config": { "key": "value" },
"pluginName": "my-plugin",
"permission": 2
}钩子事件:
{
"type": "hook",
"event": "onLogin",
"data": { "botId": 1, "playerName": "Peter", "version": "1.20.1" }
}关闭指令:
{
"type": "shutdown",
"reason": "program_close"
}日志:
{
"type": "log",
"level": "info",
"message": "消息内容"
}level 可选:info / error / warn / debug
动作:
{
"type": "action",
"action": "chat",
"data": { "botId": 1, "message": "Hello" }
}| action | 最低权限 | data 字段 | 说明 |
|---|---|---|---|
chat |
1 |
botId, message
|
让指定 bot 发聊天 |
setConfig |
2 |
key, value
|
修改运行时配置 |
getBots |
0 | requestId |
获取在线 bot 列表 |
请求响应:
getBots 是唯一需要响应的 action。主程序会返回:
{
"type": "response",
"requestId": "客户端生成的 ID",
"data": [
{ "botId": 1, "username": "Peter", "connected": true }
]
}#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import json
import threading
import time
# ============ 通信 ============
def send(obj):
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + '\n')
sys.stdout.flush()
def log_info(msg):
send({"type": "log", "level": "info", "message": str(msg)})
def log_error(msg):
send({"type": "log", "level": "error", "message": str(msg)})
def log_warn(msg):
send({"type": "log", "level": "warn", "message": str(msg)})
def log_debug(msg):
send({"type": "log", "level": "debug", "message": str(msg)})
# ============ 全局状态 ============
CONFIG = {}
PERMISSION = 0
PLUGIN_NAME = "my-plugin"
BOTS = {} # botId -> { username, ... }
# ============ 生命周期 ============
def on_init(config, name, permission):
global CONFIG, PERMISSION, PLUGIN_NAME
CONFIG = config or {}
PERMISSION = permission or 0
PLUGIN_NAME = name or PLUGIN_NAME
log_info(f"[{PLUGIN_NAME}] 初始化完成,权限={PERMISSION}")
def on_shutdown(reason):
log_info(f"[{PLUGIN_NAME}] 收到关闭: {reason}")
sys.exit(0)
# ============ 钩子处理 ============
def on_hook(event, data):
try:
if event == "beforeConnect":
log_info(f"[{PLUGIN_NAME}] Bot {data.get('botId')} 准备连接")
elif event == "afterConnect":
pass
elif event == "onLogin":
bot_id = data.get('botId')
player = data.get('playerName')
BOTS[bot_id] = {"username": player}
log_info(f"[{PLUGIN_NAME}] Bot {bot_id} ({player}) 登录")
elif event == "onKick":
log_error(f"[{PLUGIN_NAME}] Bot {data.get('botId')} 被踢: {data.get('reason')}")
elif event == "onError":
log_error(f"[{PLUGIN_NAME}] Bot {data.get('botId')} 错误: {data.get('error')}")
elif event == "onDisconnect":
bot_id = data.get('botId')
BOTS.pop(bot_id, None)
log_info(f"[{PLUGIN_NAME}] Bot {bot_id} 断开")
elif event == "connectionResult":
ok = "成功" if data.get('success') else "失败"
log_info(f"[{PLUGIN_NAME}] Bot {data.get('botId')} {ok} ({data.get('duration')}ms)")
except Exception as e:
log_error(f"[{PLUGIN_NAME}] 处理事件 {event} 异常: {e}")
# ============ 主循环 ============
def main():
log_info(f"[{PLUGIN_NAME}] Python 插件启动")
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except Exception as e:
log_error(f"JSON 解析失败: {e}")
continue
msg_type = msg.get("type")
if msg_type == "init":
on_init(msg.get("config", {}), msg.get("pluginName"), msg.get("permission", 0))
elif msg_type == "hook":
on_hook(msg.get("event"), msg.get("data", {}))
elif msg_type == "shutdown":
on_shutdown(msg.get("reason", "unknown"))
elif msg_type == "response":
# getBots 的响应
log_info(f"收到响应: {msg.get('data')}")
if __name__ == "__main__":
main()Python 的定时器和主循环冲突(for line in sys.stdin 会阻塞)。需要用线程:
import threading
def background_task():
while True:
time.sleep(60)
log_info("定时任务执行")
# 在 on_init 里启动
def on_init(config, name, permission):
# ...
t = threading.Thread(target=background_task, daemon=True)
t.start()daemon=True,否则主程序退出时插件进程不会结束。
目录:backend/plugins/auto-greet/auto-greet.js
module.exports = {
name: 'auto-greet',
version: '1.0.0',
author: 'Lx934',
description: '登录成功后自动发送一条聊天消息',
requiredPermission: 1,
defaultConfig: {
message: 'Hello everyone!',
delayMs: 1000
},
init: function (config, hooks, logger, api) {
const msg = config.message || 'Hello!';
const delay = config.delayMs || 1000;
hooks.register('onLogin', async (bot, ctx) => {
try {
await new Promise(r => setTimeout(r, delay));
bot.chat(msg);
logger.info(`[${ctx.botId}] 已发送: ${msg}`);
} catch (e) {
logger.error(`[${ctx.botId}] 发送失败: ${e.message}`);
}
});
logger.info(`打招呼插件已加载,消息: ${msg}`);
}
};目录:backend/plugins/stats-logger/stats-logger.js
const fs = require('fs');
const path = require('path');
module.exports = {
name: 'stats-logger',
version: '1.0.0',
author: 'Lx934',
description: '统计登录成功/失败,并记录到独立日志文件',
requiredPermission: 0,
defaultConfig: {
logFile: 'stats.txt'
},
init: function (config, hooks, logger, api) {
const logPath = path.join(__dirname, config.logFile || 'stats.txt');
let success = 0;
let failure = 0;
const writeLine = (line) => {
try {
fs.appendFileSync(logPath, line + '\n', 'utf-8');
} catch (e) {
logger.error('写统计日志失败: ' + e.message);
}
};
hooks.register('connectionResult', (payload) => {
const time = new Date().toISOString();
if (payload.success) {
success++;
writeLine(`[${time}] ✅ Bot ${payload.botId} 成功 (${payload.duration}ms)`);
} else {
failure++;
writeLine(`[${time}] ❌ Bot ${payload.botId} 失败 [${payload.errorType}] (${payload.duration}ms)`);
}
});
// 每次任务结束统计
hooks.register('onDisconnect', () => {
// 不在这里统计,因为可能频繁触发
});
logger.info('统计插件已加载,日志: ' + logPath);
// 覆盖 logger.close,退出时写总结(可选)
const originalUnload = this.unload;
this.unload = function (reason, logger) {
writeLine(`===== 卸载 (${reason}) 总计: 成功 ${success}, 失败 ${failure} =====`);
if (originalUnload) originalUnload.call(this, reason, logger);
};
}
};目录:backend/plugins/anti-afk/anti-afk.js
module.exports = {
name: 'anti-afk',
version: '1.0.0',
author: 'Lx934',
description: '定时让假人跳一下,防止被服务器判定为挂机',
requiredPermission: 0,
defaultConfig: {
intervalMs: 30000 // 30 秒跳一次
},
init: function (config, hooks, logger, api) {
const interval = config.intervalMs || 30000;
const timers = new Map(); // botId -> intervalId
hooks.register('onLogin', (bot, ctx) => {
if (timers.has(ctx.botId)) {
clearInterval(timers.get(ctx.botId));
}
const id = setInterval(() => {
try {
// 跳一下
bot.setControlState('jump', true);
setTimeout(() => {
try { bot.setControlState('jump', false); } catch (e) {}
}, 200);
logger.debug(`[${ctx.botId}] 跳跃防挂机`);
} catch (e) {
logger.error(`[${ctx.botId}] 跳跃失败: ${e.message}`);
clearInterval(id);
timers.delete(ctx.botId);
}
}, interval);
timers.set(ctx.botId, id);
logger.info(`[${ctx.botId}] 防挂机已启动,间隔 ${interval}ms`);
});
hooks.register('onDisconnect', (reason, ctx) => {
const id = timers.get(ctx.botId);
if (id) {
clearInterval(id);
timers.delete(ctx.botId);
}
});
// 清理
const self = this;
const originalUnload = self.unload;
self.unload = function (reason, logger) {
for (const [id, timerId] of timers) {
clearInterval(timerId);
}
timers.clear();
if (originalUnload) originalUnload.call(self, reason, logger);
};
}
};目录:backend/plugins/chat-watcher/chat-watcher.js
module.exports = {
name: 'chat-watcher',
version: '1.0.0',
author: 'Lx934',
description: '监控服务器聊天,匹配敏感词时记录',
requiredPermission: 0,
defaultConfig: {
keywords: ['ban', 'admin', 'report', 'cheat']
},
init: function (config, hooks, logger, api) {
const keywords = (config.keywords || []).map(k => k.toLowerCase());
if (keywords.length === 0) {
logger.info('未配置敏感词,插件不生效');
return;
}
hooks.register('onLogin', (bot, ctx) => {
bot.on('chat', (username, message) => {
if (username === bot.username) return;
const lower = message.toLowerCase();
for (const kw of keywords) {
if (lower.includes(kw)) {
logger.warn(`[${ctx.botId}] 敏感词 "${kw}" 出现在 <${username}> ${message}`);
break;
}
}
});
logger.info(`[${ctx.botId}] 聊天监控已启动,关键词: ${keywords.join(', ')}`);
});
}
};目录:backend/plugins/proxy-report/proxy-report.js
module.exports = {
name: 'proxy-report',
version: '1.0.0',
author: 'Lx934',
description: '每 5 分钟输出代理池健康统计',
requiredPermission: 0,
defaultConfig: {
intervalMs: 300000 // 5 分钟
},
init: function (config, hooks, logger, api) {
const interval = config.intervalMs || 300000;
let success = 0;
let failure = 0;
let lastReport = Date.now();
hooks.register('connectionResult', (payload) => {
if (payload.success) success++;
else failure++;
});
const timer = setInterval(() => {
const total = success + failure;
const rate = total > 0 ? (success / total * 100).toFixed(1) : 0;
const elapsed = Math.round((Date.now() - lastReport) / 1000);
logger.info(`=== 代理池健康报告 ===`);
logger.info(` 最近 ${elapsed} 秒: 成功 ${success}, 失败 ${failure}, 成功率 ${rate}%`);
success = 0;
failure = 0;
lastReport = Date.now();
}, interval);
// 清理
const originalUnload = this.unload;
this.unload = function (reason, logger) {
clearInterval(timer);
if (originalUnload) originalUnload.call(this, reason, logger);
};
logger.info(`代理报告插件已加载,间隔 ${interval}ms`);
}
};logger.debug('这条信息只在 debug 模式显示');主程序 config.json 里 debug: true 时会输出。
logger.info() 会同时输出到:
- 主界面日志区(实时)
-
log/<插件名>.log(持久化)
不要用 console.log(),虽然会被捕获,但可能和主程序输出混在一起。
启动后看主界面日志:
[插件] ✅ 加载成功: my-plugin v1.0.0 (权限 0)
如果没看到:
-
名字不匹配 → 检查文件夹名、文件名、
name字段 -
权限不足 →
requiredPermission太高,去【🔌 插件管理】调权限 -
语法错误 → 查看
log/<插件名>.log -
没导出 init → 检查
module.exports里有没有init
主界面 →【🔍 工具】→【💻 调试命令行】,输入:
help # 显示所有命令
version # 显示版本
status # 显示运行状态
bots # 列出在线假人
proxies # 代理池状态
memory # 内存占用
init: function (config, hooks, logger, api) {
// 在 init 里包一层 try/catch
process.on('uncaughtException', (err) => {
logger.error('插件异常: ' + err.message);
});
}现象:日志区没有"加载成功"的记录。
排查:
| 检查项 | 说明 |
|---|---|
| 文件夹名 = 文件名 = name 字段 | 三个必须完全一致 |
| 文件名后缀 | Node 是 .js,Python 是 plugin.json + main.py
|
module.exports |
Node 必须导出对象 |
init 函数 |
必须有 |
| 权限 | 用户分配的权限 ≥ requiredPermission
|
plugins.json 里 enabled |
不能是 false
|
原因:module.exports 里没有 init。
修复:
module.exports = {
name: 'my-plugin',
// ...
init: function (config, hooks, logger, api) { }
};原因:事件名拼写错误。
正确的事件名:
beforeConnect
afterConnect
onLogin
onKick
onError
onDisconnect
connectionResult
注意:区分大小写。
原因:用了 print() 而不是 send()。
修复:
# ❌ 错误
print("消息")
# ✅ 正确
send({"type": "log", "level": "info", "message": "消息"})原因:编码问题。
Node:一般无此问题。
Python:在文件第一行加:
# -*- coding: utf-8 -*-现象:程序关闭后 CPU 还在跑。
原因:unload 里没清理定时器。
修复:
let timer = null;
init: function (config, hooks, logger, api) {
timer = setInterval(() => { }, 1000);
},
unload: function (reason, logger) {
if (timer) {
clearInterval(timer);
timer = null;
}
}原因:在错误的钩子里改。
正确:只能在 beforeConnect 里改。
hooks.register('beforeConnect', (botConfig, ctx) => {
botConfig.version = '1.20.1'; // ✅ 生效
});afterConnect 里改 botConfig 已经太晚。
打包成一个文件夹:
my-plugin/
└── my-plugin.js
用户安装步骤:
- 解压文件夹到
backend/plugins/ - 重启程序
如果插件依赖第三方 npm 包:
- 把
node_modules一起打包 - 或者告诉用户去
backend/目录跑npm install <包名>
打包成一个文件夹:
my-plugin/
├── plugin.json
└── main.py
用户安装步骤:
- 解压文件夹到
backend/plugins_py/ - 重启程序
如果依赖第三方 Python 包:
- 告诉用户
pip install <包名> - 或把依赖打进 zip 里(如果体积小)
- 用 zip 压缩
- 包含一个
README.md说明用途、安装步骤 - 提供截图或示例配置
module.exports = {
name: 'my-plugin',
version: '1.0.0',
author: 'YourName',
description: '一句话说明做什么',
usage: '安装后自动生效 / 需要配置 config.json',
requiredPermission: 0,
defaultConfig: { /* ... */ },
// ...
};只申请必要的权限:
- 只是监听事件 →
requiredPermission: 0 - 要发聊天 →
requiredPermission: 1 - 要改配置 →
requiredPermission: 2 - 要开子窗口 →
requiredPermission: 3
hooks.register('onLogin', (bot, ctx) => {
try {
bot.chat('Hello');
} catch (e) {
logger.error('发送失败: ' + e.message);
}
});let timers = [];
unload: function (reason, logger) {
for (const t of timers) clearInterval(t);
timers = [];
logger.info('已清理');
}logger.info(`[my-plugin] 消息`);方便在主界面日志里区分。
init: function (config, hooks, logger, api) {
const interval = config.intervalMs || 30000; // ✅ 有默认
// ...
}不要硬编码。用户的 config/<插件名>.json 里改了配置,插件应该读它。
// ✅ 好
logger.info('消息');
// ⚠️ 能用但不推荐
console.log('消息');钩子有 5 秒超时。如果操作很久:
// ❌ 会超时
hooks.register('onLogin', async (bot, ctx) => {
await new Promise(r => setTimeout(r, 10000)); // 10 秒
});
// ✅ 用 setTimeout 分离
hooks.register('onLogin', (bot, ctx) => {
setTimeout(async () => {
await new Promise(r => setTimeout(r, 10000));
}, 0);
});不要在 bot 断开后调用 bot.chat() 等 API:
hooks.register('onLogin', (bot, ctx) => {
const timer = setInterval(() => {
if (bot._client && bot._client.state === 'play') {
bot.chat('Still here');
} else {
clearInterval(timer);
}
}, 30000);
});/**
* 插件名称
* 作者:YourName
* 版本:1.0.0
*/
module.exports = {
// ============ 元数据 ============
name: 'my-plugin',
version: '1.0.0',
author: 'YourName',
description: '插件简介',
usage: '使用说明',
requiredPermission: 0,
defaultConfig: {
// 默认配置
},
// ============ 生命周期 ============
init: function (config, hooks, logger, api) {
logger.info('插件已加载');
// 注册钩子
hooks.register('onLogin', (bot, ctx) => {
try {
logger.info(`Bot ${ctx.botId} 登录`);
} catch (e) {
logger.error('异常: ' + e.message);
}
});
// 定时器
// this._timer = setInterval(() => { }, 1000);
},
unload: function (reason, logger) {
logger.info('插件卸载: ' + reason);
// 清理定时器
// if (this._timer) { clearInterval(this._timer); this._timer = null; }
}
};#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
插件名称
作者:YourName
版本:1.0.0
"""
import sys
import json
def send(obj):
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + '\n')
sys.stdout.flush()
def log_info(msg): send({"type": "log", "level": "info", "message": str(msg)})
def log_error(msg): send({"type": "log", "level": "error", "message": str(msg)})
def log_warn(msg): send({"type": "log", "level": "warn", "message": str(msg)})
CONFIG = {}
PERMISSION = 0
def on_init(config, name, permission):
global CONFIG, PERMISSION
CONFIG = config or {}
PERMISSION = permission or 0
log_info(f"初始化,权限={PERMISSION}")
def on_hook(event, data):
try:
if event == "onLogin":
log_info(f"Bot {data.get('botId')} 登录")
except Exception as e:
log_error(f"异常: {e}")
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except Exception as e:
log_error(f"JSON 解析失败: {e}")
continue
msg_type = msg.get("type")
if msg_type == "init":
on_init(msg.get("config", {}), msg.get("pluginName"), msg.get("permission", 0))
elif msg_type == "hook":
on_hook(msg.get("event"), msg.get("data", {}))
elif msg_type == "shutdown":
log_info("关闭")
break
if __name__ == "__main__":
main()遇到问题? 到 Issues 提问