Skip to content

about plugin

Lx edited this page Sep 19, 2026 · 1 revision

插件开发指南

本指南面向想要为 McBotConsole 编写插件的开发者。


目录


插件系统概述

McBotConsole 的插件系统允许你在不修改主程序的情况下扩展功能。

插件可以:

  • 监听假人登录、被踢、断开等事件
  • 修改连接参数(如换版本、加 brand)
  • 让假人发送聊天消息
  • 读写日志
  • 请求后端数据(如 Bot 列表、代理池状态)
  • 在特定事件触发时执行自定义逻辑

插件不能:

  • 直接修改 C# 主程序
  • 修改 main backend 的核心流程
  • 绕过权限系统访问受限接口
  • 阻止主流程执行(钩子是"观察者",不能中断)

支持两种语言:

语言 优势 劣势
Node.js 与后端同语言,可同步调用 API 需要较新的 Node 环境
Python 生态丰富,编写简单 需要本机装 Python,通信靠 stdin/stdout

快速开始

Node.js 插件最小示例

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)

然后在【🔌 插件管理】里能看到它。


Python 插件最小示例

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)

目录结构

Node.js 插件

backend/
└── plugins/
    └── <插件名>/                ← 文件夹名 = 插件名
        ├── <插件名>.js          ← 文件名 = 插件名(必须)
        ├── config/
        │   └── <插件名>.json    ← 自动生成,插件配置
        └── log/
            ├── <插件名>.log     ← 自动生成,插件日志
            └── archive/         ← 归档目录

命名限制:

  • 只能用字母、数字、下划线、连字符
  • 不能以数字开头
  • 不能有中文
  • 三个名字必须一致:文件夹名 = JS 文件名 = name 字段

Python 插件

backend/
└── plugins_py/
    └── <插件名>/                ← 文件夹名 = 插件名
        ├── plugin.json          ← 元数据(必须)
        ├── main.py              ← 入口文件(名字可自定义,在 plugin.json 的 main 字段指定)
        ├── config.json          ← 自动生成,插件配置
        └── log/
            └── <插件名>.log     ← 自动生成

元数据详解

Node.js 插件

在 .js 文件的 module.exports 中定义:

字段 类型 必填 说明
name string ✅ 插件名,必须等于文件夹名
version string 版本号,建议语义化
author string 作者
description string 一句话简介
usage string 使用说明
requiredPermission number 最低权限要求(0-3),默认 0
defaultConfig object 默认配置对象
init function ✅ 初始化函数
unload function 卸载函数

Python 插件

在 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) 注册钩子。

Node.js 签名

hooks.register('onLogin', (bot, ctx) => { /* ... */ });

Python 收到的消息

{
  "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 上下文对象

所有钩子都会收到 ctx 参数:

{
    botId: 1,              // Bot 编号(整数)
    playerName: "Peter42", // 假人名字
    version: "1.20.1"      // 游戏版本
}

connectionResult 的 payload

{
    success: true,         // 是否登录成功
    duration: 1523,        // 耗时(毫秒)
    errorType: "Timeout",  // 失败类型(成功时为 "Unknown" 或空)
    botId: 1
}

errorType 可能的值:

值 含义
Kicked 被服务器踢出
AntiBot 被反机器人插件拦截
Throttled 被限流
Timeout 连接超时
ECONNRESET 连接被重置
ECONNREFUSED 连接被拒绝
VersionMismatch 版本不匹配
Disconnected without login 未登录就断开
CreateBotError 创建 Bot 失败
PluginError 插件异常
Unknown 未知

完整钩子示例(Node)

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

API 参考

logger 对象

用于写日志。日志同时输出到:

  • 插件自己的日志文件 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 对象

方法 说明
hooks.register(event, handler) 注册钩子

限制:

  • 同一个事件可以注册多个 handler
  • 每个 handler 有 5 秒超时,超时会被跳过
  • handler 抛异常会被捕获,记录到日志

api 对象

方法 最低权限 说明
api.createConsole(options) 3 创建子控制台窗口
api.writeToConsole(line) 3 向子控制台写一行
api.closeConsole() 3 关闭子控制台

注意:子控制台功能暂未实现(当前返回 NotImplemented)。

bot 对象

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.js 插件深入

模块结构

一个 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');

⚠️ 注意:如果用户安装的插件依赖额外 npm 包,你需要在分发包里带上 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 秒后执行');
    });
}

⚠️ 钩子有 5 秒超时,异步操作不要超过 5 秒。

持久化数据

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 插件深入

通信协议

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

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 }
    ]
}

完整 Python 框架代码

#!/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,否则主程序退出时插件进程不会结束。


完整示例

示例 1:登录后自动打招呼

目录: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}`);
    }
};

示例 2:登录统计与失败记录

目录: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);
        };
    }
};

示例 3:定时动作防挂机

目录: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);
        };
    }
};

示例 4:敏感词监控

目录: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(', ')}`);
        });
    }
};

示例 5:代理池健康报告

目录: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`);
    }
};

调试技巧

1. 使用 logger.debug

logger.debug('这条信息只在 debug 模式显示');

主程序 config.json 里 debug: true 时会输出。

2. 直接在日志区看输出

logger.info() 会同时输出到:

  • 主界面日志区(实时)
  • log/<插件名>.log(持久化)

不要用 console.log(),虽然会被捕获,但可能和主程序输出混在一起。

3. 检查插件是否加载

启动后看主界面日志:

[插件] ✅ 加载成功: my-plugin v1.0.0 (权限 0)

如果没看到:

  • 名字不匹配 → 检查文件夹名、文件名、name 字段
  • 权限不足 → requiredPermission 太高,去【🔌 插件管理】调权限
  • 语法错误 → 查看 log/<插件名>.log
  • 没导出 init → 检查 module.exports 里有没有 init

4. 用调试命令行

主界面 →【🔍 工具】→【💻 调试命令行】,输入:

help            # 显示所有命令
version         # 显示版本
status          # 显示运行状态
bots            # 列出在线假人
proxies         # 代理池状态
memory          # 内存占用

5. 全局异常处理

init: function (config, hooks, logger, api) {
    // 在 init 里包一层 try/catch
    process.on('uncaughtException', (err) => {
        logger.error('插件异常: ' + err.message);
    });
}

常见错误

错误 1:插件没被加载

现象:日志区没有"加载成功"的记录。

排查:

检查项 说明
文件夹名 = 文件名 = name 字段 三个必须完全一致
文件名后缀 Node 是 .js,Python 是 plugin.json + main.py
module.exports Node 必须导出对象
init 函数 必须有
权限 用户分配的权限 ≥ requiredPermission
plugins.json 里 enabled 不能是 false

错误 2:init is not a function

原因:module.exports 里没有 init。

修复:

module.exports = {
    name: 'my-plugin',
    // ...
    init: function (config, hooks, logger, api) { }
};

错误 3:钩子不触发

原因:事件名拼写错误。

正确的事件名:

beforeConnect
afterConnect
onLogin
onKick
onError
onDisconnect
connectionResult

注意:区分大小写。

错误 4:Python 插件无响应

原因:用了 print() 而不是 send()。

修复:

# ❌ 错误
print("消息")

# ✅ 正确
send({"type": "log", "level": "info", "message": "消息"})

错误 5:日志中文乱码

原因:编码问题。

Node:一般无此问题。

Python:在文件第一行加:

# -*- coding: utf-8 -*-

错误 6:定时器泄漏

现象:程序关闭后 CPU 还在跑。

原因:unload 里没清理定时器。

修复:

let timer = null;

init: function (config, hooks, logger, api) {
    timer = setInterval(() => { }, 1000);
},

unload: function (reason, logger) {
    if (timer) {
        clearInterval(timer);
        timer = null;
    }
}

错误 7:修改 botConfig 不生效

原因:在错误的钩子里改。

正确:只能在 beforeConnect 里改。

hooks.register('beforeConnect', (botConfig, ctx) => {
    botConfig.version = '1.20.1';   // ✅ 生效
});

afterConnect 里改 botConfig 已经太晚。


打包与分发

Node 插件

打包成一个文件夹:

my-plugin/
└── my-plugin.js

用户安装步骤:

  1. 解压文件夹到 backend/plugins/
  2. 重启程序

如果插件依赖第三方 npm 包:

  • 把 node_modules 一起打包
  • 或者告诉用户去 backend/ 目录跑 npm install <包名>

Python 插件

打包成一个文件夹:

my-plugin/
├── plugin.json
└── main.py

用户安装步骤:

  1. 解压文件夹到 backend/plugins_py/
  2. 重启程序

如果依赖第三方 Python 包:

  • 告诉用户 pip install <包名>
  • 或把依赖打进 zip 里(如果体积小)

打包建议

  • 用 zip 压缩
  • 包含一个 README.md 说明用途、安装步骤
  • 提供截图或示例配置

最佳实践

1. 元数据要完整

module.exports = {
    name: 'my-plugin',
    version: '1.0.0',
    author: 'YourName',
    description: '一句话说明做什么',
    usage: '安装后自动生效 / 需要配置 config.json',
    requiredPermission: 0,
    defaultConfig: { /* ... */ },
    // ...
};

2. 权限声明要最小

只申请必要的权限:

  • 只是监听事件 → requiredPermission: 0
  • 要发聊天 → requiredPermission: 1
  • 要改配置 → requiredPermission: 2
  • 要开子窗口 → requiredPermission: 3

3. 钩子里要 try/catch

hooks.register('onLogin', (bot, ctx) => {
    try {
        bot.chat('Hello');
    } catch (e) {
        logger.error('发送失败: ' + e.message);
    }
});

4. 一定要在 unload 里清理

let timers = [];

unload: function (reason, logger) {
    for (const t of timers) clearInterval(t);
    timers = [];
    logger.info('已清理');
}

5. 日志要有前缀

logger.info(`[my-plugin] 消息`);

方便在主界面日志里区分。

6. 配置要有默认值

init: function (config, hooks, logger, api) {
    const interval = config.intervalMs || 30000;   // ✅ 有默认
    // ...
}

7. 尊重用户配置

不要硬编码。用户的 config/<插件名>.json 里改了配置,插件应该读它。

8. 用 logger 而不是 console.log

// ✅ 好
logger.info('消息');

// ⚠️ 能用但不推荐
console.log('消息');

9. 不要阻塞钩子

钩子有 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);
});

10. 处理 bot 断开

不要在 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);
});

附录:完整模板

Node.js 完整模板

/**
 * 插件名称
 * 作者: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; }
    }
};

Python 完整模板

#!/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 提问

Clone this wiki locally