From 03b740fa9415181a520a768a0a2faf30ab9b390f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E5=9B=B0=E4=BA=86=E5=90=97?= <98679776+hunhaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:53:13 +0800 Subject: [PATCH 1/2] Create __init__.py --- .../__init__.py" | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 "\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/__init__.py" diff --git "a/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/__init__.py" "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/__init__.py" new file mode 100644 index 00000000..c82a5000 --- /dev/null +++ "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/__init__.py" @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from tooldelta import Plugin, plugin_entry, fmts, cfg, Chat +import requests +import threading +import time + + +class CrossServerSync(Plugin): + name = "双服信息互通" + author = "你困了吗 uvu" + version = (1, 1, 0) + + def __init__(self, frame): + super().__init__(frame) + + # 配置加载 + CFG_DEFAULT = { + "当前服务器名称": "服务器A", + "目标服务器名称": "服务器B", + "API地址": "http://你的域名或ip/api.php", + "通信密钥": "key123456", + "轮询间隔(秒)": 2, + "受信任管理员": ["你困了吗uvu"] + } + CFG_STD = cfg.auto_to_std(CFG_DEFAULT) + self.config, _ = cfg.get_plugin_config_and_version(self.name, CFG_STD, CFG_DEFAULT, self.version) + + self.CURRENT_SERVER = self.config["当前服务器名称"] + self.TARGET_SERVER = self.config["目标服务器名称"] + self.API_URL = self.config["API地址"] + self.SECRET_KEY = self.config["通信密钥"] + self.POLL_INTERVAL = self.config["轮询间隔(秒)"] + self.TRUSTED_ADMINS = self.config["受信任管理员"] + + self.ListenChat(self.on_chat) + self.ListenActive(self.on_inject) + + threading.Thread(target=self.poll_loop, daemon=True).start() + + def on_chat(self, chat: Chat): + player_name = chat.player.name + msg = chat.msg.strip() + + if not msg: + return + + # 跨服查询人数命令 + if msg == ".list": + # 发送系统查询请求给对服 + self.send_to_api("message", "[SYS_QUERY_PLAYERS]", player_name) + self.game_ctrl.say_to(player_name, f"§a[跨服] 正在查询 §e{self.TARGET_SERVER} §a的在线情况...") + return + + # 拦截跨服命令 + if msg.startswith(".cmd "): + if player_name in self.TRUSTED_ADMINS: + cmd_content = msg[5:].strip() + if cmd_content: + self.send_to_api("command", cmd_content, player_name) + self.game_ctrl.say_to(player_name, f"§a[跨服] 已向 {self.TARGET_SERVER} 发送命令: §f{cmd_content}") + return + else: + self.game_ctrl.say_to(player_name, "§c[跨服] 您没有权限发送跨服命令!") + return + + # 普通聊天消息自动发给目标服 + self.send_to_api("message", msg, player_name) + + def poll_loop(self): + time.sleep(3) + fmts.print_inf(f"[{self.name}] 跨服消息轮询已启动 (间隔 {self.POLL_INTERVAL} 秒)") + + while True: + time.sleep(self.POLL_INTERVAL) + try: + data = { + "action": "receive", + "secret_key": self.SECRET_KEY, + "server_name": self.CURRENT_SERVER + } + resp = requests.post(self.API_URL, data=data, timeout=3) + if resp.status_code == 200: + res = resp.json() + if res.get("success") and res.get("data"): + for item in res["data"]: + self.handle_received(item) + except Exception: + pass + + def handle_received(self, item): + #处理从API拉到的每一条数据 + from_server = item["from_server"] + sender_name = item["sender_name"] + msg_type = item["type"] + content = item["content"] + + if msg_type == "message": + # 处理别的服发来的查询请求 + if content == "[SYS_QUERY_PLAYERS]": + self._handle_query_players(from_server) + return + + # 处理别的服返回的查询结果 + if content.startswith("[SYS_QUERY_RESULT]"): + real_content = content.replace("[SYS_QUERY_RESULT]", "", 1) + # 转义双引号防止 JSON 报错 + safe_content = real_content.replace('"', '\\"').replace('\n', ' ') + cmd = f'tellraw @a {{"rawtext":[{{"text":"{safe_content}"}}]}}' + self.game_ctrl.sendwocmd(cmd) + return + + # 使用 tellraw 广播到公屏 + safe_content = content.replace('"', '\\"').replace('\n', ' ') + safe_sender = sender_name.replace('"', '\\"') + + tellraw_text = f"§b丨[{from_server}]§e{safe_sender}§f:{safe_content}" + cmd = f'tellraw @a {{"rawtext":[{{"text":"{tellraw_text}"}}]}}' + self.game_ctrl.sendwocmd(cmd) + + elif msg_type == "command": + if sender_name in self.TRUSTED_ADMINS: + fmts.print_inf(f"[{self.name}] 执行来自 {from_server} ({sender_name}) 的跨服命令: {content}") + self.game_ctrl.sendwocmd(content) + else: + fmts.print_war(f"[{self.name}] 拒绝执行非法跨服命令: {content} (发送者: {sender_name})") + + # ================= 新增:处理人数查询逻辑 ================= + def _handle_query_players(self, from_server): + """获取本服玩家列表并返回给请求方""" + try: + players = self.game_ctrl.players.getAllPlayers() + count = len(players) + + # 提取玩家名字 + if players: + # 如有需要过滤掉机器人名字 + names = ", ".join([p.name for p in players if p.name != self.game_ctrl.bot_name]) + if not names: + names = "无真人在线" + else: + names = "无" + + result_text = f"§e[{self.CURRENT_SERVER}] §a当前在线人数:§b{count}§e|§a玩家列表:§f{names}" + + # 将结果作为消息发回给请求方服务器 + self.send_to_api("message", f"[SYS_QUERY_RESULT]{result_text}", "System") + fmts.print_inf(f"[{self.name}] 响应了来自 {from_server} 的在线人数查询") + except Exception as e: + fmts.print_err(f"[{self.name}] 获取玩家列表失败: {e}") + + def send_to_api(self, msg_type, content, sender): + """异步发送数据到 PHP API""" + def _send(): + try: + data = { + "action": "send", + "secret_key": self.SECRET_KEY, + "from_server": self.CURRENT_SERVER, + "to_server": self.TARGET_SERVER, + "type": msg_type, + "content": content, + "sender_name": sender + } + requests.post(self.API_URL, data=data, timeout=3) + except Exception as e: + fmts.print_err(f"[{self.name}] 发送API失败: {e}") + + threading.Thread(target=_send, daemon=True).start() + + +entry = plugin_entry(CrossServerSync) From e8fe383ef0bedab7f863fa4c1d5cc2f1e8facc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E5=9B=B0=E4=BA=86=E5=90=97?= <98679776+hunhaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:53:44 +0800 Subject: [PATCH 2/2] Add files via upload --- .../README.MD" | 55 +++++++++ .../api-file.php" | 111 ++++++++++++++++++ .../api-sql.php" | 59 ++++++++++ .../database.sql" | 15 +++ 4 files changed, 240 insertions(+) create mode 100644 "\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/README.MD" create mode 100644 "\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-file.php" create mode 100644 "\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-sql.php" create mode 100644 "\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/database.sql" diff --git "a/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/README.MD" "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/README.MD" new file mode 100644 index 00000000..cd838fc4 --- /dev/null +++ "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/README.MD" @@ -0,0 +1,55 @@ +# 双服信息互通 + +## 插件简介 +本插件是一款基于ToolDelta和PHP的双服/多服互通插件 +它通过一个中心化的PHPapi进行消息中转,实现多个租赁服之间的聊天互通、命令执行和人数查询功能 +本插件提供两种部署方案,适配各类服务器和虚拟主机环境 + +## 文件说明 +- `__init__.py` : ToolDelta插件主程序 +- `datas.json` : 插件元数据信息 +- `api-sql.php` : PHPapi数据库版源码 +- `api-file.php` : PHPapi纯文件版源码 +- `database.sql` : 数据库建表语句(仅数据库版需要) + +--- + +## 部署教程 + +### 第一步:部署api端 +请根据你的服务器环境,从以下两种方案中选择一种进行部署 + +#### 数据库版(适合有MySQL环境的服务器/面板/虚拟主机) +此方案使用MySQL存储中转数据,性能更好,支持高并发,适合长期稳定运行 +1. 创建一个MySQL数据库 +2. 导入压缩包内的 `database.sql` 文件,完成数据表创建 +3. 打开 `api-sql.php`,修改顶部的数据库配置: + - `$DB_HOST` : 数据库地址(通常为 localhost) + - `$DB_NAME` : 数据库名 + - `$DB_USER` : 数据库用户名 + - `$DB_PASS` : 数据库密码 + - `$VALID_KEY` : 通信密钥 (请修改为一个复杂的字符串,并牢记) +4. 将修改后的 `api_sql.php` 重命名为 `api.php`,并上传至你的网站目录 + +#### 纯文件版 (适合无数据库的环境) +此方案使用本地 JSON 文件存储中转数据,无需配置数据库,内置文件排他锁机制,防止高并发下数据损坏 +1. 打开 `api_file.php`,修改顶部的配置: + - `$VALID_KEY` : 通信密钥 (必须与插件端一致) +2. 将修改后的 `api_file.php` 重命名为 `api.php`,并上传至你的网站目录 +3. 确保你的网站目录具有写入权限,以便 PHP 自动创建 `cross_server_data.json` 数据文件 + +--- + +### 第二步:配置插件端 +1. 从插件市场下载此插件 +2. 重载插件或重启ToolDelta,插件会自动生成配置文件 +3. 打开 `插件配置文件/双服信息互通.json`,按照以下说明进行修改: + +```json +{ + "API地址": "https://你的域名或IP/api.php", + "通信密钥": "你设置的通信密钥", + "本服名称": "A服", + "目标服名称": "B服", + "轮询间隔_秒": 1.0 +} \ No newline at end of file diff --git "a/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-file.php" "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-file.php" new file mode 100644 index 00000000..412c02c4 --- /dev/null +++ "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-file.php" @@ -0,0 +1,111 @@ + false, "message" => "Key Error"])); +} + +function atomic_update($file, $callback) { + if (!file_exists($file)) { + file_put_contents($file, '[]'); + } + + $fp = fopen($file, 'c+'); + if (!$fp) return ["success" => false, "message" => "Cannot open file"]; + + if (!flock($fp, LOCK_EX)) { + fclose($fp); + return ["success" => false, "message" => "Cannot lock file"]; + } + + $content = stream_get_contents($fp); + $data = json_decode($content, true); + if (!is_array($data)) $data = []; + + $result = $callback($data); + + //清空文件并重新写入 + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE)); + fflush($fp); + + flock($fp, LOCK_UN); + fclose($fp); + + return $result; +} + +//发送消息 +if ($action === 'send') { + $from = $_POST['from_server'] ?? ''; + $to = $_POST['to_server'] ?? ''; + $type = $_POST['type'] ?? 'message'; + $content = $_POST['content'] ?? ''; + $sender = $_POST['sender_name'] ?? 'Unknown'; + + if (!$from || !$to || !$content) die(json_encode(["success" => false])); + + $result = atomic_update($DATA_FILE, function(&$data) use ($from, $to, $type, $content, $sender) { + //生成自增ID + $max_id = 0; + foreach ($data as $row) { + if (isset($row['id']) && $row['id'] > $max_id) $max_id = $row['id']; + } + + $data[] = [ + 'id' => $max_id + 1, + 'from_server' => $from, + 'to_server' => $to, + 'type' => $type, + 'content' => $content, + 'sender_name' => $sender, + 'status' => 0, + 'created_at' => date('Y-m-d H:i:s') + ]; + + //自动清理:防止文件无限增大 当记录超过 2000 条时 自动删除已读的旧消息 + if (count($data) > 2000) { + $data = array_values(array_filter($data, function($row) { + return $row['status'] == 0; //仅保留未读消息 + })); + } + + return ["success" => true]; + }); + die(json_encode($result)); +} + +//接收消息 +elseif ($action === 'receive') { + $server_name = $_POST['server_name'] ?? ''; + if (!$server_name) die(json_encode(["success" => false])); + + $result = atomic_update($DATA_FILE, function(&$data) use ($server_name) { + $unread_messages = []; + + //遍历并标记已读 + foreach ($data as &$row) { + if ($row['to_server'] === $server_name && $row['status'] == 0) { + $unread_messages[] = $row; + $row['status'] = 1; //标记为已读 + } + } + + return ["success" => true, "data" => $unread_messages]; + }); + die(json_encode($result)); +} + +else { + die(json_encode(["success" => false, "message" => "Invalid action"])); +} +?> \ No newline at end of file diff --git "a/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-sql.php" "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-sql.php" new file mode 100644 index 00000000..d6cfa359 --- /dev/null +++ "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/api-sql.php" @@ -0,0 +1,59 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch (PDOException $e) { + die(json_encode(["success" => false, "message" => "DB Error"])); +} + +$action = $_REQUEST['action'] ?? ''; +$secret_key = $_REQUEST['secret_key'] ?? ''; + +if ($secret_key !== $VALID_KEY) die(json_encode(["success" => false, "message" => "Key Error"])); + +if ($action === 'send') { + $from = $_POST['from_server'] ?? ''; + $to = $_POST['to_server'] ?? ''; + $type = $_POST['type'] ?? 'message'; + $content = $_POST['content'] ?? ''; + $sender = $_POST['sender_name'] ?? 'Unknown'; + + if (!$from || !$to || !$content) die(json_encode(["success" => false])); + + $stmt = $pdo->prepare("INSERT INTO cross_server_data (from_server, to_server, type, content, sender_name) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$from, $to, $type, $content, $sender]); + die(json_encode(["success" => true])); +} +elseif ($action === 'receive') { + $server_name = $_POST['server_name'] ?? ''; + if (!$server_name) die(json_encode(["success" => false])); + + // 获取所有发给本服且未读的数据 + $stmt = $pdo->prepare("SELECT * FROM cross_server_data WHERE to_server = ? AND status = 0 ORDER BY id ASC"); + $stmt->execute([$server_name]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // 标记为已读 + if ($rows) { + $ids = array_column($rows, 'id'); + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $updateStmt = $pdo->prepare("UPDATE cross_server_data SET status = 1 WHERE id IN ($placeholders)"); + $updateStmt->execute($ids); + } + + die(json_encode(["success" => true, "data" => $rows ?: []])); +} else { + die(json_encode(["success" => false])); +} +?> \ No newline at end of file diff --git "a/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/database.sql" "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/database.sql" new file mode 100644 index 00000000..4637c9f2 --- /dev/null +++ "b/\345\217\214\346\234\215\344\277\241\346\201\257\344\272\222\351\200\232/database.sql" @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `cross_server_data` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '消息唯一ID', + `from_server` VARCHAR(50) NOT NULL COMMENT '发送方服务器标识', + `to_server` VARCHAR(50) NOT NULL COMMENT '接收方服务器标识', + `type` VARCHAR(20) NOT NULL DEFAULT 'message' COMMENT '消息类型 (如 message, join, leave)', + `content` TEXT NOT NULL COMMENT '消息内容', + `sender_name` VARCHAR(50) NOT NULL DEFAULT 'Unknown' COMMENT '发送者名称', + `status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '读取状态: 0=未读, 1=已读', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='跨服互通数据中转表'; + +CREATE INDEX `idx_to_server_status` ON `cross_server_data` (`to_server`, `status`); + +CREATE INDEX `idx_status_created` ON `cross_server_data` (`status`, `created_at`); \ No newline at end of file