Skip to content

Node.js 开发

MSL 基于 Node.js 运行,插件可以引入任何 Node.js 模块来扩展功能。本章将介绍如何在插件中使用 Node.js 模块以及相关的最佳实践。

DANGER

该部分已超出Minecraft的范畴,用于扩展插件功能。本文所提到的其他库的使用方法不确定完全准确,请按照您个人的需求、开发习惯、团队要求等进行调整。

引入 Node.js 模块

使用 plugin_require

在插件中,使用 plugin_require 函数引入 Node.js 模块:

javascript
// 引入 Node.js 内置模块
const fs = plugin_require("fs");
const path = plugin_require("path");
const http = plugin_require("http");
const crypto = plugin_require("crypto");

// 使用模块
const data = fs.readFileSync("./data.txt", "utf-8");
plugin_log("INFO", `文件内容: ${data}`);

引入第三方模块

首先在 MSL 项目根目录安装 npm 包:

bash
npm install axios

然后在插件中使用:

javascript
const axios = plugin_require("axios");

// 发送 HTTP 请求
async function sendWebhook(url, data) {
    try {
        const response = await axios.post(url, data);
        plugin_log("INFO", `Webhook 发送成功: ${response.status}`);
    } catch (error) {
        plugin_log("ERROR", `Webhook 发送失败: ${error.message}`);
    }
}

// 示例:玩家加入时发送通知
plugin_onEvent("playerJoin", (time, player) => {
    sendWebhook("https://your-webhook-url", {
        event: "playerJoin",
        player: player,
        time: time
    });
});

内置模块示例

文件系统操作 (fs)

javascript
const fs = plugin_require("fs");
const path = plugin_require("path");

// 读取配置文件
function loadConfig() {
    const configPath = path.join(__dirname, "config.json");
    try {
        const data = fs.readFileSync(configPath, "utf-8");
        return JSON.parse(data);
    } catch (error) {
        plugin_log("ERROR", `读取配置失败: ${error.message}`);
        return {};
    }
}

// 写入日志文件
function writeLog(message) {
    const logPath = path.join(__dirname, "plugin.log");
    const timestamp = new Date().toISOString();
    const logLine = `[${timestamp}] ${message}\n`;
    fs.appendFileSync(logPath, logLine);
}

// 使用示例
plugin_onEvent("playerJoin", (time, player) => {
    writeLog(`玩家 ${player} 加入了服务器`);
});

HTTP 请求 (http/https)

javascript
const http = plugin_require("http");
const https = plugin_require("https");

// 发送 GET 请求
function httpGet(url) {
    return new Promise((resolve, reject) => {
        const client = url.startsWith("https") ? https : http;
        client.get(url, (res) => {
            let data = "";
            res.on("data", chunk => data += chunk);
            res.on("end", () => resolve(data));
        }).on("error", reject);
    });
}

// 发送 POST 请求
function httpPost(url, body) {
    return new Promise((resolve, reject) => {
        const urlObj = new URL(url);
        const client = urlObj.protocol === "https:" ? https : http;
        
        const options = {
            hostname: urlObj.hostname,
            port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80),
            path: urlObj.pathname + urlObj.search,
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Content-Length": Buffer.byteLength(body)
            }
        };
        
        const req = client.request(options, (res) => {
            let data = "";
            res.on("data", chunk => data += chunk);
            res.on("end", () => resolve(data));
        });
        
        req.on("error", reject);
        req.write(body);
        req.end();
    });
}

// 使用示例
plugin_onEvent("playerJoin", async (time, player) => {
    try {
        // 查询玩家信息
        const response = await httpGet(`https://api.mojang.com/users/profiles/minecraft/${player}`);
        const data = JSON.parse(response);
        plugin_log("INFO", `玩家 UUID: ${data.id}`);
    } catch (error) {
        plugin_log("ERROR", `查询玩家信息失败: ${error.message}`);
    }
});

加密 (crypto)

javascript
const crypto = plugin_require("crypto");

// 生成随机字符串
function generateToken(length = 32) {
    return crypto.randomBytes(length).toString("hex");
}

// 计算哈希
function hashPassword(password) {
    return crypto.createHash("sha256").update(password).digest("hex");
}

// 使用示例
plugin_onEvent("playerJoin", (time, player) => {
    const token = generateToken(16);
    plugin_log("INFO", `为 ${player} 生成令牌: ${token}`);
});

定时任务

MSL 沙箱环境已经封装了 setTimeoutsetInterval,它们会在插件卸载时自动清除:

javascript
// 定时任务
const intervalId = setInterval(() => {
    plugin_log("INFO", "定时任务执行中...");
    plugin_executeCommand("time query daytime");
}, 60000);  // 每分钟执行一次

// 延迟任务
setTimeout(() => {
    plugin_log("INFO", "延迟任务执行");
}, 5000);

// 注意:插件卸载时,这些定时器会自动清除

第三方模块示例

使用 axios 发送 HTTP 请求

bash
npm install axios
javascript
const axios = plugin_require("axios");

// Discord Webhook 通知
async function sendDiscordWebhook(webhookUrl, content) {
    try {
        await axios.post(webhookUrl, {
            content: content,
            username: "Minecraft Server",
            avatar_url: "https://example.com/avatar.png"
        });
        plugin_log("INFO", "Discord 通知发送成功");
    } catch (error) {
        plugin_log("ERROR", `Discord 通知发送失败: ${error.message}`);
    }
}

// 玩家加入通知
plugin_onEvent("playerJoin", (time, player) => {
    sendDiscordWebhook(
        "https://discord.com/api/webhooks/xxx/xxx",
        `🎮 **${player}** 加入了服务器`
    );
});

使用 moment.js 处理时间

bash
npm install moment
javascript
const moment = plugin_require("moment");

// 格式化时间
function formatTime(date) {
    return moment(date).format("YYYY-MM-DD HH:mm:ss");
}

// 计算时长
function getPlayDuration(startTime) {
    const duration = moment.duration(moment().diff(startTime));
    return `${duration.hours()}小时 ${duration.minutes()}分钟`;
}

// 使用示例
const playerJoinTimes = {};

plugin_onEvent("playerJoin", (time, player) => {
    playerJoinTimes[player] = moment();
    plugin_log("INFO", `${player} 在 ${formatTime(new Date())} 加入`);
});

plugin_onEvent("playerQuit", (time, player) => {
    if (playerJoinTimes[player]) {
        const duration = getPlayDuration(playerJoinTimes[player]);
        plugin_log("INFO", `${player} 游玩了 ${duration}`);
        delete playerJoinTimes[player];
    }
});

使用 sqlite3 存储数据

bash
npm install sqlite3
javascript
const sqlite3 = plugin_require("sqlite3").verbose();
const path = plugin_require("path");

// 创建数据库连接
const dbPath = path.join(__dirname, "..", "..", "data", "plugin.db");
const db = new sqlite3.Database(dbPath);

// 初始化表
db.serialize(() => {
    db.run(`
        CREATE TABLE IF NOT EXISTS player_stats (
            player TEXT PRIMARY KEY,
            join_count INTEGER DEFAULT 0,
            last_join TEXT
        )
    `);
});

// 更新玩家统计
function updatePlayerStats(player) {
    db.run(`
        INSERT INTO player_stats (player, join_count, last_join)
        VALUES (?, 1, datetime('now'))
        ON CONFLICT(player) DO UPDATE SET
            join_count = join_count + 1,
            last_join = datetime('now')
    `, [player], (err) => {
        if (err) {
            plugin_log("ERROR", `更新玩家统计失败: ${err.message}`);
        }
    });
}

// 查询玩家统计
function getPlayerStats(player) {
    return new Promise((resolve, reject) => {
        db.get("SELECT * FROM player_stats WHERE player = ?", [player], (err, row) => {
            if (err) reject(err);
            else resolve(row);
        });
    });
}

// 使用示例
plugin_onEvent("playerJoin", async (time, player) => {
    updatePlayerStats(player);
    
    try {
        const stats = await getPlayerStats(player);
        if (stats) {
            plugin_log("INFO", `${player} 第 ${stats.join_count} 次加入服务器`);
            plugin_executeCommand(`tellraw ${player} {"text":"欢迎回来!这是你第 ${stats.join_count} 次加入","color":"yellow"}`);
        }
    } catch (error) {
        plugin_log("ERROR", `查询玩家统计失败: ${error.message}`);
    }
});

使用 ws 实现 WebSocket

bash
npm install ws
javascript
const WebSocket = plugin_require("ws");

// 创建 WebSocket 客户端
let ws = null;

function connectWebSocket(url) {
    ws = new WebSocket(url);
    
    ws.on("open", () => {
        plugin_log("INFO", "WebSocket 连接已建立");
    });
    
    ws.on("message", (data) => {
        try {
            const message = JSON.parse(data.toString());
            handleWebSocketMessage(message);
        } catch (error) {
            plugin_log("ERROR", `解析 WebSocket 消息失败: ${error.message}`);
        }
    });
    
    ws.on("error", (error) => {
        plugin_log("ERROR", `WebSocket 错误: ${error.message}`);
    });
    
    ws.on("close", () => {
        plugin_log("WARN", "WebSocket 连接已关闭,5秒后重连...");
        setTimeout(() => connectWebSocket(url), 5000);
    });
}

function handleWebSocketMessage(message) {
    switch (message.type) {
        case "command":
            plugin_executeCommand(message.data);
            break;
        case "broadcast":
            plugin_executeCommand(`say ${message.data}`);
            break;
    }
}

// 发送消息
function sendWebSocketMessage(data) {
    if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(data));
    }
}

// 使用示例
plugin_onEvent("serverDone", () => {
    connectWebSocket("wss://your-websocket-server.com");
});

plugin_onEvent("playerJoin", (time, player) => {
    sendWebSocketMessage({
        type: "playerJoin",
        player: player,
        time: time
    });
});

最佳实践

1. 错误处理

javascript
const fs = plugin_require("fs");

function safeReadFile(path) {
    try {
        return fs.readFileSync(path, "utf-8");
    } catch (error) {
        plugin_log("ERROR", `读取文件失败: ${error.message}`);
        return null;
    }
}

2. 资源清理

javascript
const db = plugin_require("sqlite3").verbose();
const dbConnection = new db.Database("data.db");

// 注意:MSL 会在插件卸载时自动清除定时器
// 但数据库连接等资源需要手动管理(如果需要)

3. 异步操作

javascript
const axios = plugin_require("axios");

// 使用 async/await
async function fetchData(url) {
    try {
        const response = await axios.get(url);
        return response.data;
    } catch (error) {
        plugin_log("ERROR", `请求失败: ${error.message}`);
        return null;
    }
}

// 在事件中使用
plugin_onEvent("playerJoin", async (time, player) => {
    const data = await fetchData(`https://api.example.com/player/${player}`);
    if (data) {
        plugin_log("INFO", `获取到玩家数据: ${JSON.stringify(data)}`);
    }
});

4. 模块缓存

javascript
// 在插件顶部引入模块,避免重复引入
const fs = plugin_require("fs");
const path = plugin_require("path");
const axios = plugin_require("axios");

// 后续直接使用这些模块

5. 配置管理

javascript
const fs = plugin_require("fs");
const path = plugin_require("path");

// 加载插件配置
function loadPluginConfig() {
    const configPath = path.join(__dirname, "..", "..", "plugins", "myplugin-config.json");
    try {
        if (fs.existsSync(configPath)) {
            const data = fs.readFileSync(configPath, "utf-8");
            return JSON.parse(data);
        }
    } catch (error) {
        plugin_log("ERROR", `加载配置失败: ${error.message}`);
    }
    return {};  // 返回默认配置
}

const config = loadPluginConfig();

注意事项

1. 模块路径

plugin_require 使用 Node.js 的标准模块解析机制:

  • 内置模块:直接使用名称,如 "fs""http"
  • 第三方模块:使用包名,如 "axios""moment"
  • 本地模块:不支持相对路径引入

2. 异步操作

Node.js 的异步操作(如文件读写、网络请求)不会阻塞 MSL 主进程,但要注意:

  • 回调函数中的错误需要自行捕获
  • 长时间运行的操作可能影响其他插件

3. 安全性

使用 plugin_require 时要注意:

  • 不要引入可能存在安全漏洞的包
  • 不要在插件中存储敏感信息(如密码、密钥)
  • 对外部输入进行验证和清理

下一步