Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions 双服信息互通/README.MD
Original file line number Diff line number Diff line change
@@ -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
}
171 changes: 171 additions & 0 deletions 双服信息互通/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
111 changes: 111 additions & 0 deletions 双服信息互通/api-file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
header('Content-Type: application/json; charset=utf-8');

//配置区
$VALID_KEY = "scdcrossservernklm88"; //通信密钥(必须与插件端一致)
$DATA_FILE = __DIR__ . '/cross_server_data.json'; //数据文件(自动生成)

$action = $_REQUEST['action'] ?? '';
$secret_key = $_REQUEST['secret_key'] ?? '';

//密钥验证
if ($secret_key !== $VALID_KEY) {
die(json_encode(["success" => 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"]));
}
?>
Loading