From f3bd1324c525c4928e68a0a8914c6e6b61acd1c5 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:33:48 +0800 Subject: [PATCH 01/21] Add files via upload --- .../__init__.py" | 187 ++++++++++++++++++ .../datas.json" | 8 + 2 files changed, 195 insertions(+) create mode 100644 "\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" create mode 100644 "\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" new file mode 100644 index 00000000..1c876d20 --- /dev/null +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -0,0 +1,187 @@ +import json +import time +import threading +import asyncio +import importlib +import subprocess +import sys +from tooldelta import Plugin, plugin_entry, Config, utils +from tooldelta.constants import PacketIDS + +# 自动安装依赖 +def install_package(package): + if importlib.util.find_spec(package) is None: + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", package, "-q"]) + except Exception: + pass + +install_package("websockets") +import websockets + +class CrossServerChat(Plugin): + name = "服服互通聊天" + author = "哈茶块" + version = (1, 0, 2) + + CONFIG_TEMPLATE = { + "中转服务器地址": "ws://core.aurorabot.top", + "当前服务器名称": "你的服务器名称", + "频道名称": "全球大厅" + } + + def __init__(self, frame): + super().__init__(frame) + + # 防止重载插件时旧的后台线程继续存活导致消息重复发送 + if hasattr(self.frame, "_cross_server_chat_instance"): + old_inst = self.frame._cross_server_chat_instance + if old_inst: + try: + old_inst.stop() + except Exception: + pass + self.frame._cross_server_chat_instance = self + + self.is_running = True + + self.cfg, _ = Config.get_plugin_config_and_version( + self.name, + Config.auto_to_std(self.CONFIG_TEMPLATE), + self.CONFIG_TEMPLATE, + self.version + ) + self.ws_conn = None + self.msg_queue = asyncio.Queue() + self.loop = None + + self.ListenPreload(self.on_def) + self.ListenPacket(PacketIDS.Text, self.on_chat) + + def stop(self): + """停止后台服务,供重载时安全清理旧线程""" + self.is_running = False + if self.ws_conn and self.loop: + try: + asyncio.run_coroutine_threadsafe(self.ws_conn.close(), self.loop) + except Exception: + pass + + def on_def(self): + """插件加载时启动后台 WebSocket 线程""" + self.print_inf(f"准备连接到频道: [{self.cfg['频道名称']}]") + threading.Thread(target=self.run_ws_client, daemon=True).start() + + def run_ws_client(self): + """在新线程中初始化异步事件循环""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.ws_main()) + + async def ws_main(self): + """维持与中转服务器的持久连接""" + uri = self.cfg["中转服务器地址"] + channel = self.cfg["频道名称"] + + while self.is_running: + try: + self.print_inf(f"正在尝试连接中转服务器: {uri} ...") + async with websockets.connect(uri) as ws: + self.ws_conn = ws + self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") + + await ws.send(json.dumps({"type": "auth", "channel": channel})) + + recv_task = asyncio.create_task(self.ws_recv(ws)) + send_task = asyncio.create_task(self.ws_send(ws)) + + done, pending = await asyncio.wait( + [recv_task, send_task], + return_when=asyncio.FIRST_COMPLETED + ) + + # 清理未完成的任务 + for task in pending: + task.cancel() + + except Exception as e: + if self.is_running: + self.print_err(f"❌ 与中转服务器连接断开或无法连接: {e}") + finally: + self.ws_conn = None + + if not self.is_running: + break + + if self.is_running: + self.print_war("5秒后尝试重新连接...") + for _ in range(5): + if not self.is_running: + break + await asyncio.sleep(1) + + async def ws_recv(self, ws): + """接收中转服务器发来的跨服消息""" + async for msg in ws: + if not self.is_running: + break + try: + data = json.loads(msg) + if data.get("type") == "chat": + server_name = data.get("server") + player = data.get("player") + content = data.get("msg") + + # 格式化输出到本地游戏内 + fmt_msg = f"§7[§b{server_name}§7] §e{player} §f> §r{content}" + self.game_ctrl.say_to("@a", fmt_msg) + except Exception as e: + self.print_err(f"处理跨服消息时出错: {e}") + + async def ws_send(self, ws): + """将队列里的本地消息发送给中转服务器""" + while self.is_running: + try: + msg = await asyncio.wait_for(self.msg_queue.get(), timeout=1.0) + await ws.send(msg) + except asyncio.TimeoutError: + continue + except Exception: + break + + def on_chat(self, pkt): + """监听本地玩家聊天""" + player = pkt.get("SourceName", "") + msg = pkt.get("Message", "").strip() + text_type = pkt.get("TextType", 0) + + if not player or not msg: + return False + + if text_type != 1: + return False + + # 过滤指令前缀 (以 / 或 . 开头的内容不转发) + if msg.startswith("/") or msg.startswith("."): + return False + + # 过滤机器人(Bot)自己发出的消息 + if player == self.game_ctrl.bot_name: + return False + + # 如果 WebSocket 已连接且插件处于运行状态,将合法玩家聊天压入发送队列 + if self.is_running and self.loop and self.ws_conn: + data = { + "type": "chat", + "server": self.cfg["当前服务器名称"], + "player": player, + "msg": msg + } + asyncio.run_coroutine_threadsafe( + self.msg_queue.put(json.dumps(data)), + self.loop + ) + + return False + +entry = plugin_entry(CrossServerChat) \ No newline at end of file diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" new file mode 100644 index 00000000..6dedff77 --- /dev/null +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" @@ -0,0 +1,8 @@ +{ + "plugin-id": "server-interconnection", + "author": "哈茶块", + "version": "1.0.2", + "description": "实现服服互通!可以让多个服务器内的聊天消息互通", + "plugin-type": "classic", + "pre-plugins": {} +} \ No newline at end of file From 150e50a22096129053a477671b5776e9a8b8e2e6 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:49:32 +0800 Subject: [PATCH 02/21] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__init__.py" | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index 1c876d20..e7629069 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,3 +1,4 @@ +# flake8: noqa import json import time import threading @@ -184,4 +185,4 @@ def on_chat(self, pkt): return False -entry = plugin_entry(CrossServerChat) \ No newline at end of file +entry = plugin_entry(CrossServerChat) From 99ee9938ffdd4eba9e6a7921cc0aee9dc3545e81 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:55:22 +0800 Subject: [PATCH 03/21] Improve comments and fix threading issues Refactor comments for clarity and fix potential threading issues during plugin reload. --- .../__init__.py" | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index e7629069..0bb03377 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,4 +1,3 @@ -# flake8: noqa import json import time import threading @@ -33,8 +32,8 @@ class CrossServerChat(Plugin): def __init__(self, frame): super().__init__(frame) - - # 防止重载插件时旧的后台线程继续存活导致消息重复发送 + + # 【核心修复】防止重载插件时旧的后台线程继续存活导致消息重复发送 if hasattr(self.frame, "_cross_server_chat_instance"): old_inst = self.frame._cross_server_chat_instance if old_inst: @@ -43,9 +42,9 @@ def __init__(self, frame): except Exception: pass self.frame._cross_server_chat_instance = self - + self.is_running = True - + self.cfg, _ = Config.get_plugin_config_and_version( self.name, Config.auto_to_std(self.CONFIG_TEMPLATE), @@ -55,7 +54,7 @@ def __init__(self, frame): self.ws_conn = None self.msg_queue = asyncio.Queue() self.loop = None - + self.ListenPreload(self.on_def) self.ListenPacket(PacketIDS.Text, self.on_chat) @@ -64,6 +63,7 @@ def stop(self): self.is_running = False if self.ws_conn and self.loop: try: + # 线程安全的关闭 WebSocket 连接,触发异常结束任务 asyncio.run_coroutine_threadsafe(self.ws_conn.close(), self.loop) except Exception: pass @@ -83,37 +83,42 @@ async def ws_main(self): """维持与中转服务器的持久连接""" uri = self.cfg["中转服务器地址"] channel = self.cfg["频道名称"] - + while self.is_running: try: self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: self.ws_conn = ws - self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") - + self.print_suc("成功连接到中转服务器!互通服务已上线。") + + # 1. 握手阶段:告诉服务器我属于哪个频道 await ws.send(json.dumps({"type": "auth", "channel": channel})) - + + # 2. 并发任务:一个负责收,一个负责发 recv_task = asyncio.create_task(self.ws_recv(ws)) send_task = asyncio.create_task(self.ws_send(ws)) - + + # 3. 等待任意一个任务出错(例如连接断开) done, pending = await asyncio.wait( [recv_task, send_task], return_when=asyncio.FIRST_COMPLETED ) - + # 清理未完成的任务 for task in pending: task.cancel() - + except Exception as e: + # 只有在非关闭状态下才打印断开提示 if self.is_running: self.print_err(f"❌ 与中转服务器连接断开或无法连接: {e}") finally: self.ws_conn = None - + if not self.is_running: break - + + # 5秒后自动重连 (分片Sleep以保证重载时能快速中断退出) if self.is_running: self.print_war("5秒后尝试重新连接...") for _ in range(5): @@ -132,7 +137,7 @@ async def ws_recv(self, ws): server_name = data.get("server") player = data.get("player") content = data.get("msg") - + # 格式化输出到本地游戏内 fmt_msg = f"§7[§b{server_name}§7] §e{player} §f> §r{content}" self.game_ctrl.say_to("@a", fmt_msg) @@ -143,6 +148,7 @@ async def ws_send(self, ws): """将队列里的本地消息发送给中转服务器""" while self.is_running: try: + # 使用 wait_for 限制等待时间,确保能实时检测到 self.is_running 的变化 msg = await asyncio.wait_for(self.msg_queue.get(), timeout=1.0) await ws.send(msg) except asyncio.TimeoutError: @@ -155,21 +161,23 @@ def on_chat(self, pkt): player = pkt.get("SourceName", "") msg = pkt.get("Message", "").strip() text_type = pkt.get("TextType", 0) - + + # 1. 过滤空消息或无来源消息 if not player or not msg: return False - + + # 2. 严格检测文本类型,仅放行真正的玩家聊天 (TextType == 1) if text_type != 1: return False - - # 过滤指令前缀 (以 / 或 . 开头的内容不转发) + + # 3. 过滤指令前缀 (以 / 或 . 开头的内容不转发) if msg.startswith("/") or msg.startswith("."): return False - - # 过滤机器人(Bot)自己发出的消息 + + # 4. 过滤机器人(Bot)自己发出的消息 if player == self.game_ctrl.bot_name: return False - + # 如果 WebSocket 已连接且插件处于运行状态,将合法玩家聊天压入发送队列 if self.is_running and self.loop and self.ws_conn: data = { @@ -178,11 +186,12 @@ def on_chat(self, pkt): "player": player, "msg": msg } + # 安全地将消息从 ToolDelta 主线程跨线程推入 asyncio 队列 asyncio.run_coroutine_threadsafe( - self.msg_queue.put(json.dumps(data)), + self.msg_queue.put(json.dumps(data)), self.loop ) - + return False entry = plugin_entry(CrossServerChat) From e0b19814bce6c13fd0e39a11d5f247fff1dd486d Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:08:54 +0800 Subject: [PATCH 04/21] Refactor CrossServerChat plugin and improve imports --- .../__init__.py" | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index 0bb03377..4398165d 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,25 +1,32 @@ -import json -import time -import threading import asyncio import importlib +import json import subprocess import sys -from tooldelta import Plugin, plugin_entry, Config, utils +import threading + +from tooldelta import Config, Plugin, plugin_entry from tooldelta.constants import PacketIDS -# 自动安装依赖 -def install_package(package): + +def install_package(package: str) -> None: + """自动安装缺失的依赖包。""" if importlib.util.find_spec(package) is None: try: - subprocess.check_call([sys.executable, "-m", "pip", "install", package, "-q"]) + subprocess.check_call( + [sys.executable, "-m", "pip", "install", package, "-q"] + ) except Exception: pass + install_package("websockets") -import websockets +import websockets # noqa: E402 + class CrossServerChat(Plugin): + """服服互通聊天插件。""" + name = "服服互通聊天" author = "哈茶块" version = (1, 0, 2) @@ -35,7 +42,7 @@ def __init__(self, frame): # 【核心修复】防止重载插件时旧的后台线程继续存活导致消息重复发送 if hasattr(self.frame, "_cross_server_chat_instance"): - old_inst = self.frame._cross_server_chat_instance + old_inst = getattr(self.frame, "_cross_server_chat_instance", None) if old_inst: try: old_inst.stop() @@ -59,7 +66,7 @@ def __init__(self, frame): self.ListenPacket(PacketIDS.Text, self.on_chat) def stop(self): - """停止后台服务,供重载时安全清理旧线程""" + """停止后台服务,供重载时安全清理旧线程。""" self.is_running = False if self.ws_conn and self.loop: try: @@ -69,18 +76,18 @@ def stop(self): pass def on_def(self): - """插件加载时启动后台 WebSocket 线程""" + """插件加载时启动后台 WebSocket 线程。""" self.print_inf(f"准备连接到频道: [{self.cfg['频道名称']}]") threading.Thread(target=self.run_ws_client, daemon=True).start() def run_ws_client(self): - """在新线程中初始化异步事件循环""" + """在新线程中初始化异步事件循环。""" self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.loop.run_until_complete(self.ws_main()) async def ws_main(self): - """维持与中转服务器的持久连接""" + """维持与中转服务器的持久连接。""" uri = self.cfg["中转服务器地址"] channel = self.cfg["频道名称"] @@ -89,7 +96,7 @@ async def ws_main(self): self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: self.ws_conn = ws - self.print_suc("成功连接到中转服务器!互通服务已上线。") + self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") # 1. 握手阶段:告诉服务器我属于哪个频道 await ws.send(json.dumps({"type": "auth", "channel": channel})) @@ -99,7 +106,7 @@ async def ws_main(self): send_task = asyncio.create_task(self.ws_send(ws)) # 3. 等待任意一个任务出错(例如连接断开) - done, pending = await asyncio.wait( + _, pending = await asyncio.wait( [recv_task, send_task], return_when=asyncio.FIRST_COMPLETED ) @@ -127,7 +134,7 @@ async def ws_main(self): await asyncio.sleep(1) async def ws_recv(self, ws): - """接收中转服务器发来的跨服消息""" + """接收中转服务器发来的跨服消息。""" async for msg in ws: if not self.is_running: break @@ -145,7 +152,7 @@ async def ws_recv(self, ws): self.print_err(f"处理跨服消息时出错: {e}") async def ws_send(self, ws): - """将队列里的本地消息发送给中转服务器""" + """将队列里的本地消息发送给中转服务器。""" while self.is_running: try: # 使用 wait_for 限制等待时间,确保能实时检测到 self.is_running 的变化 @@ -157,7 +164,7 @@ async def ws_send(self, ws): break def on_chat(self, pkt): - """监听本地玩家聊天""" + """监听本地玩家聊天。""" player = pkt.get("SourceName", "") msg = pkt.get("Message", "").strip() text_type = pkt.get("TextType", 0) @@ -194,4 +201,5 @@ def on_chat(self, pkt): return False + entry = plugin_entry(CrossServerChat) From 4e410c78e40228115be60a28682ac482728d7522 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:16:57 +0800 Subject: [PATCH 05/21] Refactor CrossServerChat plugin and update version Updated version number and improved error messages. Removed package installation logic and ensured websockets are imported correctly. --- .../__init__.py" | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index 4398165d..8ba692e1 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,35 +1,17 @@ import asyncio -import importlib import json -import subprocess -import sys import threading from tooldelta import Config, Plugin, plugin_entry from tooldelta.constants import PacketIDS -def install_package(package: str) -> None: - """自动安装缺失的依赖包。""" - if importlib.util.find_spec(package) is None: - try: - subprocess.check_call( - [sys.executable, "-m", "pip", "install", package, "-q"] - ) - except Exception: - pass - - -install_package("websockets") -import websockets # noqa: E402 - - class CrossServerChat(Plugin): """服服互通聊天插件。""" name = "服服互通聊天" author = "哈茶块" - version = (1, 0, 2) + version = (1, 0, 3) CONFIG_TEMPLATE = { "中转服务器地址": "ws://core.aurorabot.top", @@ -38,9 +20,10 @@ class CrossServerChat(Plugin): } def __init__(self, frame): + """初始化插件,处理热重载。""" super().__init__(frame) - # 【核心修复】防止重载插件时旧的后台线程继续存活导致消息重复发送 + # 防止重载插件时旧的后台线程继续存活导致消息重复发送 if hasattr(self.frame, "_cross_server_chat_instance"): old_inst = getattr(self.frame, "_cross_server_chat_instance", None) if old_inst: @@ -77,6 +60,12 @@ def stop(self): def on_def(self): """插件加载时启动后台 WebSocket 线程。""" + try: + self.GetPluginAPI("pip").require("websockets") + except Exception as e: + self.print_err(f"无法调用内置 pip 模块检查依赖,请确保已安装 [pip模块支持] 插件: {e}") + return + self.print_inf(f"准备连接到频道: [{self.cfg['频道名称']}]") threading.Thread(target=self.run_ws_client, daemon=True).start() @@ -88,6 +77,9 @@ def run_ws_client(self): async def ws_main(self): """维持与中转服务器的持久连接。""" + # 此时 websockets 必定已经被官方 pip 模块处理好了,可以直接局部导入 + import websockets + uri = self.cfg["中转服务器地址"] channel = self.cfg["频道名称"] @@ -96,7 +88,7 @@ async def ws_main(self): self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: self.ws_conn = ws - self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") + self.print_suc("成功连接到中转服务器!互通服务已上线。") # 1. 握手阶段:告诉服务器我属于哪个频道 await ws.send(json.dumps({"type": "auth", "channel": channel})) @@ -118,7 +110,7 @@ async def ws_main(self): except Exception as e: # 只有在非关闭状态下才打印断开提示 if self.is_running: - self.print_err(f"❌ 与中转服务器连接断开或无法连接: {e}") + self.print_err(f" 与中转服务器连接断开或无法连接: {e}") finally: self.ws_conn = None From c8861b7ff6a46335ae817ed0ca4f76fa0865126c Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:21:10 +0800 Subject: [PATCH 06/21] Update datas.json --- .../datas.json" | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" index 6dedff77..a6a3a7d3 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" @@ -1,8 +1,10 @@ { "plugin-id": "server-interconnection", "author": "哈茶块", - "version": "1.0.2", + "version": "1.0.3", "description": "实现服服互通!可以让多个服务器内的聊天消息互通", "plugin-type": "classic", - "pre-plugins": {} -} \ No newline at end of file + "pre-plugins": { + "pip模块支持": "0.0.6" + } +} From 5db409f0d9ac9dc315fc141c8e7ddcda5b2043ba Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:27:58 +0800 Subject: [PATCH 07/21] Add files via upload --- .../core_server.py" | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 "\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" new file mode 100644 index 00000000..eb9145d5 --- /dev/null +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -0,0 +1,74 @@ +import asyncio +import websockets +import json +import logging + +# 配置日志输出格式 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +# 存储所有连接的客户端,结构:{websocket_object: "频道名称"} +clients = {} + +async def handle_client(websocket, path): + """处理每个客户端连接""" + try: + # 第一步:等待客户端发送握手认证消息,获取频道信息 + auth_msg = await websocket.recv() + auth_data = json.loads(auth_msg) + + if auth_data.get("type") != "auth": + logging.warning("客户端未发送认证消息,连接断开") + return + + channel = auth_data.get("channel", "default") + clients[websocket] = channel + logging.info(f"新服务器接入: {websocket.remote_address} | 绑定频道: [{channel}] | 当前总连接数: {len(clients)}") + + # 第二步:循环接收消息并分发 + async for message in websocket: + data = json.loads(message) + if data.get("type") == "chat": + # 获取该消息的频道 + msg_channel = clients[websocket] + server_name = data.get("server") + player = data.get("player") + chat_msg = data.get("msg") + + logging.info(f"频道 [{msg_channel}] 收到来自 [{server_name}] {player} 的消息: {chat_msg}") + + # 寻找同一频道内的其他所有服务器 (排除发送者自己) + targets = [ws for ws, ch in clients.items() if ch == msg_channel and ws != websocket] + + # 异步广播消息给目标 + for target_ws in targets: + try: + await target_ws.send(message) + except Exception as e: + logging.warning(f"向节点发送消息失败: {e}") + + except websockets.exceptions.ConnectionClosed: + pass # 正常断开连接 + except Exception as e: + logging.error(f"处理客户端连接时发生异常: {e}") + finally: + # 客户端断开连接,清理信息 + if websocket in clients: + channel = clients[websocket] + del clients[websocket] + logging.info(f"服务器断开连接: {websocket.remote_address} | 解除频道: [{channel}] | 剩余连接数: {len(clients)}") + +async def main(): + host = "0.0.0.0" + port = 8765 # 你可以在这里修改中转服务器开放的端口 + logging.info(f"服服互通中转服务器正在启动,监听 ws://{host}:{port}") + logging.info("请确保云服务器的防火墙已放行该端口!") + + # 启动 WebSocket 服务 + async with websockets.serve(handle_client, host, port): + await asyncio.Future() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file From cb4a791c273b8aa7778745e5e5c54db0989b392b Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:35:07 +0800 Subject: [PATCH 08/21] Refactor logging and client handling in core_server.py --- .../core_server.py" | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index eb9145d5..17448f77 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -1,31 +1,37 @@ import asyncio -import websockets import json import logging +import websockets # 配置日志输出格式 logging.basicConfig( - level=logging.INFO, + level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) # 存储所有连接的客户端,结构:{websocket_object: "频道名称"} clients = {} -async def handle_client(websocket, path): - """处理每个客户端连接""" + +async def handle_client(websocket, _path): + """处理每个客户端连接。""" try: # 第一步:等待客户端发送握手认证消息,获取频道信息 auth_msg = await websocket.recv() auth_data = json.loads(auth_msg) - + if auth_data.get("type") != "auth": logging.warning("客户端未发送认证消息,连接断开") return - + channel = auth_data.get("channel", "default") clients[websocket] = channel - logging.info(f"新服务器接入: {websocket.remote_address} | 绑定频道: [{channel}] | 当前总连接数: {len(clients)}") + logging.info( + "新服务器接入: %s | 绑定频道: [%s] | 当前总连接数: %d", + websocket.remote_address, + channel, + len(clients) + ) # 第二步:循环接收消息并分发 async for message in websocket: @@ -36,39 +42,55 @@ async def handle_client(websocket, path): server_name = data.get("server") player = data.get("player") chat_msg = data.get("msg") - - logging.info(f"频道 [{msg_channel}] 收到来自 [{server_name}] {player} 的消息: {chat_msg}") - + + logging.info( + "频道 [%s] 收到来自 [%s] %s 的消息: %s", + msg_channel, + server_name, + player, + chat_msg + ) + # 寻找同一频道内的其他所有服务器 (排除发送者自己) - targets = [ws for ws, ch in clients.items() if ch == msg_channel and ws != websocket] - + targets = [ + ws for ws, ch in clients.items() + if ch == msg_channel and ws != websocket + ] + # 异步广播消息给目标 for target_ws in targets: try: await target_ws.send(message) except Exception as e: - logging.warning(f"向节点发送消息失败: {e}") + logging.warning("向节点发送消息失败: %s", e) except websockets.exceptions.ConnectionClosed: pass # 正常断开连接 except Exception as e: - logging.error(f"处理客户端连接时发生异常: {e}") + logging.error("处理客户端连接时发生异常: %s", e) finally: # 客户端断开连接,清理信息 if websocket in clients: channel = clients[websocket] del clients[websocket] - logging.info(f"服务器断开连接: {websocket.remote_address} | 解除频道: [{channel}] | 剩余连接数: {len(clients)}") + logging.info( + "服务器断开连接: %s | 解除频道: [%s] | 剩余连接数: %d", + websocket.remote_address, + channel, + len(clients) + ) + async def main(): - host = "0.0.0.0" - port = 8765 # 你可以在这里修改中转服务器开放的端口 - logging.info(f"服服互通中转服务器正在启动,监听 ws://{host}:{port}") - logging.info("请确保云服务器的防火墙已放行该端口!") - + """启动 WebSocket 服务器。""" + host = "0.0.0.0" # deepsource ignore: BAN-B104 + port = 8765 # 你可以在这里修改中转服务器开放的端口 + logging.info("🚀 服服互通中转服务器正在启动,监听 ws://%s:%d", host, port) + # 启动 WebSocket 服务 async with websockets.serve(handle_client, host, port): - await asyncio.Future() + await asyncio.Future() # 永久挂起,保持运行 + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 6ff19f34608c786caad745c747aa02b4b095a905 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:37:49 +0800 Subject: [PATCH 09/21] Update comment to skip deepsource check --- .../core_server.py" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index 17448f77..923d1c39 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -83,7 +83,7 @@ async def handle_client(websocket, _path): async def main(): """启动 WebSocket 服务器。""" - host = "0.0.0.0" # deepsource ignore: BAN-B104 + host = "0.0.0.0" # skipcq: BAN-B104 port = 8765 # 你可以在这里修改中转服务器开放的端口 logging.info("🚀 服服互通中转服务器正在启动,监听 ws://%s:%d", host, port) From c911f3173246c4ca167f9d203d829ce9b75dbe79 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:44:34 +0800 Subject: [PATCH 10/21] Add files via upload --- .../readme.txt" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/readme.txt" diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/readme.txt" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/readme.txt" new file mode 100644 index 00000000..cfc8dd95 --- /dev/null +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/readme.txt" @@ -0,0 +1 @@ +这是一个服服互通聊天的插件您可以选择部署自己的中转服务器下载 core_server.py 文件并使用 python3 core_server.py 运行随后在配置文件内修改服务器地址即可如果您听不懂以上内容您可以不修改中转服务器地址使用默认的 ws://core.aurorabot.top \ No newline at end of file From 82c60a86aff85b84f62cc1c84fe24a92d12164cb Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 3 May 2026 15:50:20 +0800 Subject: [PATCH 11/21] Update CrossServerChat plugin for compatibility and fixes --- .../__init__.py" | 210 +++++++++++++++--- 1 file changed, 179 insertions(+), 31 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index 8ba692e1..ce16e145 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,17 +1,18 @@ import asyncio import json import threading +import time -from tooldelta import Config, Plugin, plugin_entry +from tooldelta import Config, Plugin, plugin_entry, game_utils from tooldelta.constants import PacketIDS class CrossServerChat(Plugin): - """服服互通聊天插件。""" + """服服互通聊天插件 (高频心跳防丢版)。""" name = "服服互通聊天" author = "哈茶块" - version = (1, 0, 3) + version = (2, 2, 0) CONFIG_TEMPLATE = { "中转服务器地址": "ws://core.aurorabot.top", @@ -23,7 +24,7 @@ def __init__(self, frame): """初始化插件,处理热重载。""" super().__init__(frame) - # 防止重载插件时旧的后台线程继续存活导致消息重复发送 + # 防止重载插件时旧的后台线程继续存活导致消息重复发送 if hasattr(self.frame, "_cross_server_chat_instance"): old_inst = getattr(self.frame, "_cross_server_chat_instance", None) if old_inst: @@ -53,11 +54,44 @@ def stop(self): self.is_running = False if self.ws_conn and self.loop: try: - # 线程安全的关闭 WebSocket 连接,触发异常结束任务 + # 线程安全的关闭 WebSocket 连接 asyncio.run_coroutine_threadsafe(self.ws_conn.close(), self.loop) except Exception: pass + def get_online_players(self): + """核心修复:万能在线玩家获取方法,兼容所有 ToolDelta 版本和接入核心""" + players = [] + + # 1. 尝试使用游戏目标选择器 (最稳定) + try: + res = game_utils.getTarget("@a") + if isinstance(res, list): + players = res + except Exception: + pass + + if players: return players + + # 2. 尝试从 players 对象管理器中提取 + try: + if hasattr(self.game_ctrl, "players"): + p_list = self.game_ctrl.players.getAllPlayers() + players = [p.name if hasattr(p, 'name') else str(p) for p in p_list] + except Exception: + pass + + if players: return players + + # 3. 兜底方案:旧版直接属性 + try: + if hasattr(self.game_ctrl, "all_players") and isinstance(self.game_ctrl.all_players, list): + players = self.game_ctrl.all_players + except Exception: + pass + + return players + def on_def(self): """插件加载时启动后台 WebSocket 线程。""" try: @@ -77,7 +111,6 @@ def run_ws_client(self): async def ws_main(self): """维持与中转服务器的持久连接。""" - # 此时 websockets 必定已经被官方 pip 模块处理好了,可以直接局部导入 import websockets uri = self.cfg["中转服务器地址"] @@ -88,18 +121,24 @@ async def ws_main(self): self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: self.ws_conn = ws - self.print_suc("成功连接到中转服务器!互通服务已上线。") + self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") - # 1. 握手阶段:告诉服务器我属于哪个频道 - await ws.send(json.dumps({"type": "auth", "channel": channel})) + # 1. 握手阶段 + auth_data = { + "type": "auth", + "channel": channel, + "server_name": self.cfg["当前服务器名称"] + } + await ws.send(json.dumps(auth_data)) - # 2. 并发任务:一个负责收,一个负责发 + # 2. 并发任务:收、发、以及【高频状态上报】 recv_task = asyncio.create_task(self.ws_recv(ws)) send_task = asyncio.create_task(self.ws_send(ws)) + report_task = asyncio.create_task(self.status_report_loop(ws)) - # 3. 等待任意一个任务出错(例如连接断开) + # 3. 等待任务 _, pending = await asyncio.wait( - [recv_task, send_task], + [recv_task, send_task, report_task], return_when=asyncio.FIRST_COMPLETED ) @@ -108,16 +147,15 @@ async def ws_main(self): task.cancel() except Exception as e: - # 只有在非关闭状态下才打印断开提示 if self.is_running: - self.print_err(f" 与中转服务器连接断开或无法连接: {e}") + self.print_err(f"❌ 与中转服务器连接断开或无法连接: {e}") finally: self.ws_conn = None if not self.is_running: break - # 5秒后自动重连 (分片Sleep以保证重载时能快速中断退出) + # 5秒后自动重连 if self.is_running: self.print_war("5秒后尝试重新连接...") for _ in range(5): @@ -125,6 +163,21 @@ async def ws_main(self): break await asyncio.sleep(1) + async def status_report_loop(self, ws): + """核心修复:3秒高频上报本服务器的真实玩家名单给后端缓存""" + while self.is_running: + try: + local_players = self.get_online_players() + payload = { + "type": "status", + "players": local_players + } + await ws.send(json.dumps(payload)) + except Exception: + pass + # 缩短为 3 秒一次!解决玩家刚进服搜不到人的“真空期”问题 + await asyncio.sleep(3) + async def ws_recv(self, ws): """接收中转服务器发来的跨服消息。""" async for msg in ws: @@ -132,14 +185,41 @@ async def ws_recv(self, ws): break try: data = json.loads(msg) - if data.get("type") == "chat": - server_name = data.get("server") - player = data.get("player") - content = data.get("msg") - - # 格式化输出到本地游戏内 - fmt_msg = f"§7[§b{server_name}§7] §e{player} §f> §r{content}" + msg_type = data.get("type") + server_name = data.get("server", "未知服务器") + sender = data.get("player", "未知玩家") + content = data.get("msg", "") + + # 1. 普通全服聊天或公告 + if msg_type == "chat": + fmt_msg = f"§7[§b{server_name}§7] §e{sender} §f> §r{content}" self.game_ctrl.say_to("@a", fmt_msg) + + # 2. 跨服机制事件 (系统提示/名单返回等) + elif msg_type == "event": + sub_type = data.get("sub_type") + + # 后端把拼接好的全服名单发给我们了,直接展示给请求的玩家 + if sub_type == "reply_list": + target = data.get("target") + reply_content = data.get("content") + self.game_ctrl.say_to(target, reply_content) + + # 后端告诉我们发送私聊失败了 (查无此人) + elif sub_type == "private_msg_error": + target = data.get("target") + error_msg = data.get("msg") + self.game_ctrl.say_to(target, error_msg) + + # 3. 收到了别人发来的私聊包裹 + elif msg_type == "private_msg": + target = data.get("target") + local_players = self.get_online_players() + # 检查是不是发给自己服务器里的人的,如果是,就拦截下来显示 + if target in local_players: + fmt_msg = f"§d[私聊] §7[§b{server_name}§7] §e{sender} §f-> §e你§f: §r{content}" + self.game_ctrl.say_to(target, fmt_msg) + except Exception as e: self.print_err(f"处理跨服消息时出错: {e}") @@ -147,7 +227,6 @@ async def ws_send(self, ws): """将队列里的本地消息发送给中转服务器。""" while self.is_running: try: - # 使用 wait_for 限制等待时间,确保能实时检测到 self.is_running 的变化 msg = await asyncio.wait_for(self.msg_queue.get(), timeout=1.0) await ws.send(msg) except asyncio.TimeoutError: @@ -161,23 +240,93 @@ def on_chat(self, pkt): msg = pkt.get("Message", "").strip() text_type = pkt.get("TextType", 0) - # 1. 过滤空消息或无来源消息 if not player or not msg: return False - # 2. 严格检测文本类型,仅放行真正的玩家聊天 (TextType == 1) if text_type != 1: return False - # 3. 过滤指令前缀 (以 / 或 . 开头的内容不转发) - if msg.startswith("/") or msg.startswith("."): + if player == self.game_ctrl.bot_name: return False - # 4. 过滤机器人(Bot)自己发出的消息 - if player == self.game_ctrl.bot_name: + # =============================================== + # 跨服私聊 ( .msg 或 .w ) 修复了包含空格名字的匹配问题 + # =============================================== + if msg.startswith(".msg ") or msg.startswith(".w "): + cmd_str = msg.split(" ", 1)[1].strip() + target_player = "" + content = "" + + # 支持用双引号包裹带空格的玩家名 (如: .msg "Player A" 你好) + if cmd_str.startswith('"'): + end_idx = cmd_str.find('"', 1) + if end_idx != -1: + target_player = cmd_str[1:end_idx] + content = cmd_str[end_idx+1:].strip() + else: + parts = cmd_str.split(" ", 1) + target_player = parts[0] + content = parts[1] if len(parts) > 1 else "" + else: + parts = cmd_str.split(" ", 1) + if len(parts) < 2: + self.game_ctrl.say_to(player, "§c格式错误!用法: .msg <玩家名> <内容> (如果名字带空格请用双引号包裹)") + return True + target_player = parts[0] + content = parts[1] + + local_players = self.get_online_players() + + # 优先尝试本服内私聊 + if target_player in local_players: + self.game_ctrl.say_to(target_player, f"§d[私聊] §7[§b{self.cfg['当前服务器名称']}§7] §e{player} §f-> §e你§f: §r{content}") + self.game_ctrl.say_to(player, f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}") + return True + + # 本服找不到,直接甩给后端让它去找 + if self.is_running and self.loop and self.ws_conn: + data = { + "type": "private_msg", + "server": self.cfg["当前服务器名称"], + "player": player, + "target": target_player, + "msg": content + } + asyncio.run_coroutine_threadsafe( + self.msg_queue.put(json.dumps(data)), + self.loop + ) + self.game_ctrl.say_to(player, f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}") + else: + self.game_ctrl.say_to(player, "§c互通服务未连接,无法发送跨服私聊。") + return True + + # =============================================== + # 全网在线状态查询 ( .list 或 .在线 ) + # =============================================== + if msg in [".list", ".在线"]: + if self.is_running and self.loop and self.ws_conn: + # 告诉后端:把全网名单拼好直接发回来给我 + data = { + "type": "event", + "sub_type": "request_list", + "requester": player + } + asyncio.run_coroutine_threadsafe( + self.msg_queue.put(json.dumps(data)), + self.loop + ) + else: + # 断网时只显示本地 + local_players = self.get_online_players() + self.game_ctrl.say_to(player, f"§e==== 🌐 本服在线: {len(local_players)} 人 ====\n§7[§b{self.cfg['当前服务器名称']}§7] §f{', '.join(local_players)}\n§c(跨服网络未连接)") + return True + + # 过滤其他插件可能会用到的指令前缀 + if msg.startswith("/") or msg.startswith("."): return False - # 如果 WebSocket 已连接且插件处于运行状态,将合法玩家聊天压入发送队列 + # 正常全服聊天 if self.is_running and self.loop and self.ws_conn: data = { "type": "chat", @@ -185,7 +334,6 @@ def on_chat(self, pkt): "player": player, "msg": msg } - # 安全地将消息从 ToolDelta 主线程跨线程推入 asyncio 队列 asyncio.run_coroutine_threadsafe( self.msg_queue.put(json.dumps(data)), self.loop From fe391b0a0cdfde278a2b4dcc256e3b8eb5b68d4c Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 3 May 2026 15:50:57 +0800 Subject: [PATCH 12/21] Update datas.json --- .../datas.json" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" index a6a3a7d3..1ba5848e 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" @@ -1,7 +1,7 @@ { "plugin-id": "server-interconnection", "author": "哈茶块", - "version": "1.0.3", + "version": "2.2.0", "description": "实现服服互通!可以让多个服务器内的聊天消息互通", "plugin-type": "classic", "pre-plugins": { From c11bd00bed8dc5e686571bdf1067b3d8ff14672b Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 3 May 2026 15:51:46 +0800 Subject: [PATCH 13/21] Enhance client connection handling and logging Refactor client handling to include detailed information and improve logging. --- .../core_server.py" | 152 ++++++++++++------ 1 file changed, 103 insertions(+), 49 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index 923d1c39..a64f9449 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -9,87 +9,141 @@ format='%(asctime)s - %(levelname)s - %(message)s' ) -# 存储所有连接的客户端,结构:{websocket_object: "频道名称"} +# 存储所有连接的客户端详细信息 +# 结构:{ websocket: {"channel": "大厅", "server_name": "生存一区", "players": ["Steve", "Alex"]} } clients = {} async def handle_client(websocket, _path): - """处理每个客户端连接。""" + """处理每个客户端连接及数据路由""" try: - # 第一步:等待客户端发送握手认证消息,获取频道信息 + # 握手认证 auth_msg = await websocket.recv() auth_data = json.loads(auth_msg) if auth_data.get("type") != "auth": - logging.warning("客户端未发送认证消息,连接断开") + logging.warning("非法的连接请求,已拒绝") return channel = auth_data.get("channel", "default") - clients[websocket] = channel - logging.info( - "新服务器接入: %s | 绑定频道: [%s] | 当前总连接数: %d", - websocket.remote_address, - channel, - len(clients) - ) - - # 第二步:循环接收消息并分发 + server_name = auth_data.get("server_name", "未知子服") + + clients[websocket] = { + "channel": channel, + "server_name": server_name, + "players": [] # 重点:在此缓存该服务器定时上报的在线玩家名单 + } + + logging.info("🔗 新子服接入: [%s] (%s) | 当前总节点: %d", server_name, channel, len(clients)) + + # 智能数据路由 async for message in websocket: data = json.loads(message) - if data.get("type") == "chat": - # 获取该消息的频道 - msg_channel = clients[websocket] - server_name = data.get("server") - player = data.get("player") - chat_msg = data.get("msg") - - logging.info( - "频道 [%s] 收到来自 [%s] %s 的消息: %s", - msg_channel, - server_name, - player, - chat_msg - ) - - # 寻找同一频道内的其他所有服务器 (排除发送者自己) + msg_type = data.get("type") + my_info = clients[websocket] + + # 1. 普通聊天直接全频道广播 + if msg_type == "chat": targets = [ - ws for ws, ch in clients.items() - if ch == msg_channel and ws != websocket + ws for ws, info in list(clients.items()) + if info["channel"] == my_info["channel"] and ws != websocket ] - - # 异步广播消息给目标 for target_ws in targets: try: await target_ws.send(message) - except Exception as e: - logging.warning("向节点发送消息失败: %s", e) + except Exception: + pass + + # 2. 接收子服定时上报的状态,并更新缓存 + elif msg_type == "status": + clients[websocket]["players"] = data.get("players", []) + + # 3. 处理事件(比如前端请求全网名单) + elif msg_type == "event": + sub_type = data.get("sub_type") + + # 前端要求后端下发整理好的全网名单 + if sub_type == "request_list": + requester = data.get("requester") + + # 后端瞬间聚合内存中的数据 + total_players = 0 + servers_data = {} + + for ws, info in clients.items(): + if info["channel"] == my_info["channel"]: + p_list = info["players"] + servers_data[info["server_name"]] = p_list + total_players += len(p_list) + + # 直接在后端完成排版 + lines = [f"§e==== 🌐 全网同频道在线: {total_players} 人 ===="] + for srv, p_list in servers_data.items(): + p_str = ", ".join(p_list) if p_list else "§8无人在线" + lines.append(f"§7[§b{srv}§7] §a({len(p_list)}人) §f{p_str}") + + # 把拼接好的文本发回给那个发请求的子服 + reply_msg = { + "type": "event", + "sub_type": "reply_list", + "target": requester, + "content": "\n".join(lines) + } + try: + await websocket.send(json.dumps(reply_msg)) + except Exception: + pass + + # 4. 跨服私聊直接由后端依据缓存进行路由分发 + elif msg_type == "private_msg": + target = data.get("target") + sender = data.get("player") + + routed = False + for ws, info in clients.items(): + # 遍历缓存,如果人在那个服的名单里,就发过去 + if info["channel"] == my_info["channel"] and target in info["players"]: + try: + await ws.send(message) + routed = True + logging.info(f"✉️ 跨服私聊路由: {sender} -> {target} (已投递至 {info['server_name']})") + except Exception: + pass + break + + # 如果翻遍了所有缓存都没这个人,给发送者回个错误提示 + if not routed: + error_msg = { + "type": "event", + "sub_type": "private_msg_error", + "target": sender, + "msg": f"§c跨服私聊失败: 全网未找到玩家 {target} (可能不在线或拼写有误)。" + } + try: + await websocket.send(json.dumps(error_msg)) + except Exception: + pass except websockets.exceptions.ConnectionClosed: - pass # 正常断开连接 + pass except Exception as e: - logging.error("处理客户端连接时发生异常: %s", e) + logging.error("客户端连接异常: %s", e) finally: - # 客户端断开连接,清理信息 if websocket in clients: - channel = clients[websocket] + info = clients[websocket] del clients[websocket] - logging.info( - "服务器断开连接: %s | 解除频道: [%s] | 剩余连接数: %d", - websocket.remote_address, - channel, - len(clients) - ) + logging.info("❌ 子服断开: [%s] (%s) | 剩余节点: %d", info["server_name"], info["channel"], len(clients)) async def main(): """启动 WebSocket 服务器。""" host = "0.0.0.0" # skipcq: BAN-B104 - port = 8765 # 你可以在这里修改中转服务器开放的端口 - logging.info("🚀 服服互通中转服务器正在启动,监听 ws://%s:%d", host, port) + port = 8765 + logging.info("🚀 互通中转核心已启动 -> ws://%s:%d", host, port) + logging.info("💡 当前处于【定时上报,后端缓存集中路由】的数据流模式") - # 启动 WebSocket 服务 async with websockets.serve(handle_client, host, port): - await asyncio.Future() # 永久挂起,保持运行 + await asyncio.Future() if __name__ == "__main__": From 1997868e265094b817d57dbe8a833c97e90db3ff Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 3 May 2026 15:59:55 +0800 Subject: [PATCH 14/21] Refactor logging and comments in core_server.py --- .../core_server.py" | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index a64f9449..5b6a40f9 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -10,11 +10,11 @@ ) # 存储所有连接的客户端详细信息 -# 结构:{ websocket: {"channel": "大厅", "server_name": "生存一区", "players": ["Steve", "Alex"]} } +# 结构: { websocket: {"channel": "大厅", "server_name": "一区", "players": []} } clients = {} -async def handle_client(websocket, _path): +async def handle_client(websocket, _path): # skipcq: PY-R1000 """处理每个客户端连接及数据路由""" try: # 握手认证 @@ -31,10 +31,15 @@ async def handle_client(websocket, _path): clients[websocket] = { "channel": channel, "server_name": server_name, - "players": [] # 重点:在此缓存该服务器定时上报的在线玩家名单 + "players": [] # 重点:在此缓存该服务器定时上报的在线玩家名单 } - logging.info("🔗 新子服接入: [%s] (%s) | 当前总节点: %d", server_name, channel, len(clients)) + logging.info( + "🔗 新子服接入: [%s] (%s) | 当前总节点: %d", + server_name, + channel, + len(clients) + ) # 智能数据路由 async for message in websocket: @@ -53,7 +58,7 @@ async def handle_client(websocket, _path): await target_ws.send(message) except Exception: pass - + # 2. 接收子服定时上报的状态,并更新缓存 elif msg_type == "status": clients[websocket]["players"] = data.get("players", []) @@ -65,23 +70,23 @@ async def handle_client(websocket, _path): # 前端要求后端下发整理好的全网名单 if sub_type == "request_list": requester = data.get("requester") - + # 后端瞬间聚合内存中的数据 total_players = 0 servers_data = {} - + for ws, info in clients.items(): if info["channel"] == my_info["channel"]: p_list = info["players"] servers_data[info["server_name"]] = p_list total_players += len(p_list) - + # 直接在后端完成排版 lines = [f"§e==== 🌐 全网同频道在线: {total_players} 人 ===="] for srv, p_list in servers_data.items(): p_str = ", ".join(p_list) if p_list else "§8无人在线" lines.append(f"§7[§b{srv}§7] §a({len(p_list)}人) §f{p_str}") - + # 把拼接好的文本发回给那个发请求的子服 reply_msg = { "type": "event", @@ -98,19 +103,25 @@ async def handle_client(websocket, _path): elif msg_type == "private_msg": target = data.get("target") sender = data.get("player") - + routed = False for ws, info in clients.items(): # 遍历缓存,如果人在那个服的名单里,就发过去 - if info["channel"] == my_info["channel"] and target in info["players"]: + is_same_channel = info["channel"] == my_info["channel"] + if is_same_channel and target in info["players"]: try: await ws.send(message) routed = True - logging.info(f"✉️ 跨服私聊路由: {sender} -> {target} (已投递至 {info['server_name']})") + logging.info( + "✉️ 跨服私聊路由: %s -> %s (已投递至 %s)", + sender, + target, + info["server_name"] + ) except Exception: pass break - + # 如果翻遍了所有缓存都没这个人,给发送者回个错误提示 if not routed: error_msg = { @@ -132,7 +143,12 @@ async def handle_client(websocket, _path): if websocket in clients: info = clients[websocket] del clients[websocket] - logging.info("❌ 子服断开: [%s] (%s) | 剩余节点: %d", info["server_name"], info["channel"], len(clients)) + logging.info( + "❌ 子服断开: [%s] (%s) | 剩余节点: %d", + info["server_name"], + info["channel"], + len(clients) + ) async def main(): From f1a940856e88c35513e9bc10f60a3e977e4589f8 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Sun, 3 May 2026 16:00:34 +0800 Subject: [PATCH 15/21] Refactor get_online_players and chat handling --- .../__init__.py" | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index ce16e145..db35ee20 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,7 +1,7 @@ import asyncio import json import threading -import time +import time # skipcq: PY-W2000 from tooldelta import Config, Plugin, plugin_entry, game_utils from tooldelta.constants import PacketIDS @@ -62,7 +62,7 @@ def stop(self): def get_online_players(self): """核心修复:万能在线玩家获取方法,兼容所有 ToolDelta 版本和接入核心""" players = [] - + # 1. 尝试使用游戏目标选择器 (最稳定) try: res = game_utils.getTarget("@a") @@ -70,9 +70,10 @@ def get_online_players(self): players = res except Exception: pass - - if players: return players - + + if players: + return players + # 2. 尝试从 players 对象管理器中提取 try: if hasattr(self.game_ctrl, "players"): @@ -80,16 +81,18 @@ def get_online_players(self): players = [p.name if hasattr(p, 'name') else str(p) for p in p_list] except Exception: pass - - if players: return players - + + if players: + return players + # 3. 兜底方案:旧版直接属性 try: - if hasattr(self.game_ctrl, "all_players") and isinstance(self.game_ctrl.all_players, list): + has_all = hasattr(self.game_ctrl, "all_players") + if has_all and isinstance(self.game_ctrl.all_players, list): players = self.game_ctrl.all_players except Exception: pass - + return players def on_def(self): @@ -97,7 +100,8 @@ def on_def(self): try: self.GetPluginAPI("pip").require("websockets") except Exception as e: - self.print_err(f"无法调用内置 pip 模块检查依赖,请确保已安装 [pip模块支持] 插件: {e}") + err_msg = f"无法调用内置 pip 模块检查依赖,请确保已安装 [pip模块支持] 插件: {e}" + self.print_err(err_msg) return self.print_inf(f"准备连接到频道: [{self.cfg['频道名称']}]") @@ -198,13 +202,13 @@ async def ws_recv(self, ws): # 2. 跨服机制事件 (系统提示/名单返回等) elif msg_type == "event": sub_type = data.get("sub_type") - + # 后端把拼接好的全服名单发给我们了,直接展示给请求的玩家 if sub_type == "reply_list": target = data.get("target") reply_content = data.get("content") self.game_ctrl.say_to(target, reply_content) - + # 后端告诉我们发送私聊失败了 (查无此人) elif sub_type == "private_msg_error": target = data.get("target") @@ -217,7 +221,10 @@ async def ws_recv(self, ws): local_players = self.get_online_players() # 检查是不是发给自己服务器里的人的,如果是,就拦截下来显示 if target in local_players: - fmt_msg = f"§d[私聊] §7[§b{server_name}§7] §e{sender} §f-> §e你§f: §r{content}" + fmt_msg = ( + f"§d[私聊] §7[§b{server_name}§7] §e{sender} " + f"§f-> §e你§f: §r{content}" + ) self.game_ctrl.say_to(target, fmt_msg) except Exception as e: @@ -234,7 +241,7 @@ async def ws_send(self, ws): except Exception: break - def on_chat(self, pkt): + def on_chat(self, pkt): # skipcq: PY-R1000 """监听本地玩家聊天。""" player = pkt.get("SourceName", "") msg = pkt.get("Message", "").strip() @@ -256,7 +263,7 @@ def on_chat(self, pkt): cmd_str = msg.split(" ", 1)[1].strip() target_player = "" content = "" - + # 支持用双引号包裹带空格的玩家名 (如: .msg "Player A" 你好) if cmd_str.startswith('"'): end_idx = cmd_str.find('"', 1) @@ -270,7 +277,10 @@ def on_chat(self, pkt): else: parts = cmd_str.split(" ", 1) if len(parts) < 2: - self.game_ctrl.say_to(player, "§c格式错误!用法: .msg <玩家名> <内容> (如果名字带空格请用双引号包裹)") + self.game_ctrl.say_to( + player, + "§c格式错误!用法: .msg <玩家名> <内容> (名字带空格请用双引号)" + ) return True target_player = parts[0] content = parts[1] @@ -279,8 +289,15 @@ def on_chat(self, pkt): # 优先尝试本服内私聊 if target_player in local_players: - self.game_ctrl.say_to(target_player, f"§d[私聊] §7[§b{self.cfg['当前服务器名称']}§7] §e{player} §f-> §e你§f: §r{content}") - self.game_ctrl.say_to(player, f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}") + self.game_ctrl.say_to( + target_player, + f"§d[私聊] §7[§b{self.cfg['当前服务器名称']}§7] " + f"§e{player} §f-> §e你§f: §r{content}" + ) + self.game_ctrl.say_to( + player, + f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}" + ) return True # 本服找不到,直接甩给后端让它去找 @@ -296,7 +313,10 @@ def on_chat(self, pkt): self.msg_queue.put(json.dumps(data)), self.loop ) - self.game_ctrl.say_to(player, f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}") + self.game_ctrl.say_to( + player, + f"§d[私聊] §f你 -> §e{target_player}§f: §r{content}" + ) else: self.game_ctrl.say_to(player, "§c互通服务未连接,无法发送跨服私聊。") return True @@ -319,7 +339,13 @@ def on_chat(self, pkt): else: # 断网时只显示本地 local_players = self.get_online_players() - self.game_ctrl.say_to(player, f"§e==== 🌐 本服在线: {len(local_players)} 人 ====\n§7[§b{self.cfg['当前服务器名称']}§7] §f{', '.join(local_players)}\n§c(跨服网络未连接)") + local_str = ", ".join(local_players) + msg_str = ( + f"§e==== 🌐 本服在线: {len(local_players)} 人 ====\n" + f"§7[§b{self.cfg['当前服务器名称']}§7] §f{local_str}\n" + "§c(跨服网络未连接)" + ) + self.game_ctrl.say_to(player, msg_str) return True # 过滤其他插件可能会用到的指令前缀 From 693249a4f2c639fd1970785cce0a6ed212fd8841 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:41:14 +0800 Subject: [PATCH 16/21] Update CrossServerChat plugin version and config --- .../__init__.py" | 115 ++++++------------ 1 file changed, 39 insertions(+), 76 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index db35ee20..eb045301 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -8,23 +8,24 @@ class CrossServerChat(Plugin): - """服服互通聊天插件 (高频心跳防丢版)。""" + """服服互通聊天插件""" name = "服服互通聊天" author = "哈茶块" - version = (2, 2, 0) + version = (2, 3, 2) + # 扩展配置,增加鉴权信息 CONFIG_TEMPLATE = { "中转服务器地址": "ws://core.aurorabot.top", "当前服务器名称": "你的服务器名称", - "频道名称": "全球大厅" + "频道名称": "全球大厅", # 默认进入官方公开频道。如需建立私人网络请改为自定义名称 + "频道类型(公开/私密)": "公开", + "频道密钥(仅私密需填)": "" } def __init__(self, frame): - """初始化插件,处理热重载。""" super().__init__(frame) - # 防止重载插件时旧的后台线程继续存活导致消息重复发送 if hasattr(self.frame, "_cross_server_chat_instance"): old_inst = getattr(self.frame, "_cross_server_chat_instance", None) if old_inst: @@ -50,71 +51,55 @@ def __init__(self, frame): self.ListenPacket(PacketIDS.Text, self.on_chat) def stop(self): - """停止后台服务,供重载时安全清理旧线程。""" self.is_running = False if self.ws_conn and self.loop: try: - # 线程安全的关闭 WebSocket 连接 asyncio.run_coroutine_threadsafe(self.ws_conn.close(), self.loop) except Exception: pass def get_online_players(self): - """核心修复:万能在线玩家获取方法,兼容所有 ToolDelta 版本和接入核心""" players = [] - - # 1. 尝试使用游戏目标选择器 (最稳定) try: res = game_utils.getTarget("@a") - if isinstance(res, list): - players = res - except Exception: - pass - - if players: - return players + if isinstance(res, list): players = res + except Exception: pass + if players: return players - # 2. 尝试从 players 对象管理器中提取 try: if hasattr(self.game_ctrl, "players"): p_list = self.game_ctrl.players.getAllPlayers() players = [p.name if hasattr(p, 'name') else str(p) for p in p_list] - except Exception: - pass - - if players: - return players + except Exception: pass + if players: return players - # 3. 兜底方案:旧版直接属性 try: has_all = hasattr(self.game_ctrl, "all_players") if has_all and isinstance(self.game_ctrl.all_players, list): players = self.game_ctrl.all_players - except Exception: - pass + except Exception: pass return players def on_def(self): - """插件加载时启动后台 WebSocket 线程。""" try: self.GetPluginAPI("pip").require("websockets") except Exception as e: - err_msg = f"无法调用内置 pip 模块检查依赖,请确保已安装 [pip模块支持] 插件: {e}" + err_msg = f"无法调用内置 pip 模块检查依赖: {e}" self.print_err(err_msg) return - self.print_inf(f"准备连接到频道: [{self.cfg['频道名称']}]") + c_type = self.cfg.get("频道类型(公开/私密)", "公开") + lock_icon = "🔒" if c_type == "私密" else "🌐" + self.print_inf(f"准备连接到频道: {lock_icon} [{self.cfg['频道名称']}]") threading.Thread(target=self.run_ws_client, daemon=True).start() def run_ws_client(self): - """在新线程中初始化异步事件循环。""" self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.loop.run_until_complete(self.ws_main()) async def ws_main(self): - """维持与中转服务器的持久连接。""" import websockets uri = self.cfg["中转服务器地址"] @@ -124,29 +109,40 @@ async def ws_main(self): try: self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: - self.ws_conn = ws - self.print_suc("✅ 成功连接到中转服务器!互通服务已上线。") - - # 1. 握手阶段 + + # 1. 鉴权握手 auth_data = { "type": "auth", "channel": channel, - "server_name": self.cfg["当前服务器名称"] + "server_name": self.cfg["当前服务器名称"], + "channel_type": self.cfg.get("频道类型(公开/私密)", "公开"), + "channel_key": self.cfg.get("频道密钥(仅私密需填)", "") } await ws.send(json.dumps(auth_data)) + + # 2. 等待服务端反馈 (5秒超时) + auth_resp_str = await asyncio.wait_for(ws.recv(), timeout=5.0) + auth_resp = json.loads(auth_resp_str) + + if auth_resp.get("type") == "auth_fail": + self.print_err(f"❌ 互通连接被拒绝: {auth_resp.get('msg')}") + self.print_war("请修改 [插件配置文件/服服互通聊天.json] 后使用 reload 重载插件。") + self.is_running = False # 彻底阻断,防止刷屏重连 + return + + self.ws_conn = ws + self.print_suc("✅ 鉴权通过,互通服务已上线!") - # 2. 并发任务:收、发、以及【高频状态上报】 + # 3. 正常建立并发任务 recv_task = asyncio.create_task(self.ws_recv(ws)) send_task = asyncio.create_task(self.ws_send(ws)) report_task = asyncio.create_task(self.status_report_loop(ws)) - # 3. 等待任务 _, pending = await asyncio.wait( [recv_task, send_task, report_task], return_when=asyncio.FIRST_COMPLETED ) - # 清理未完成的任务 for task in pending: task.cancel() @@ -159,7 +155,6 @@ async def ws_main(self): if not self.is_running: break - # 5秒后自动重连 if self.is_running: self.print_war("5秒后尝试重新连接...") for _ in range(5): @@ -168,7 +163,6 @@ async def ws_main(self): await asyncio.sleep(1) async def status_report_loop(self, ws): - """核心修复:3秒高频上报本服务器的真实玩家名单给后端缓存""" while self.is_running: try: local_players = self.get_online_players() @@ -179,11 +173,9 @@ async def status_report_loop(self, ws): await ws.send(json.dumps(payload)) except Exception: pass - # 缩短为 3 秒一次!解决玩家刚进服搜不到人的“真空期”问题 await asyncio.sleep(3) async def ws_recv(self, ws): - """接收中转服务器发来的跨服消息。""" async for msg in ws: if not self.is_running: break @@ -194,32 +186,25 @@ async def ws_recv(self, ws): sender = data.get("player", "未知玩家") content = data.get("msg", "") - # 1. 普通全服聊天或公告 if msg_type == "chat": fmt_msg = f"§7[§b{server_name}§7] §e{sender} §f> §r{content}" self.game_ctrl.say_to("@a", fmt_msg) - # 2. 跨服机制事件 (系统提示/名单返回等) elif msg_type == "event": sub_type = data.get("sub_type") - - # 后端把拼接好的全服名单发给我们了,直接展示给请求的玩家 if sub_type == "reply_list": target = data.get("target") reply_content = data.get("content") self.game_ctrl.say_to(target, reply_content) - # 后端告诉我们发送私聊失败了 (查无此人) elif sub_type == "private_msg_error": target = data.get("target") error_msg = data.get("msg") self.game_ctrl.say_to(target, error_msg) - # 3. 收到了别人发来的私聊包裹 elif msg_type == "private_msg": target = data.get("target") local_players = self.get_online_players() - # 检查是不是发给自己服务器里的人的,如果是,就拦截下来显示 if target in local_players: fmt_msg = ( f"§d[私聊] §7[§b{server_name}§7] §e{sender} " @@ -231,7 +216,6 @@ async def ws_recv(self, ws): self.print_err(f"处理跨服消息时出错: {e}") async def ws_send(self, ws): - """将队列里的本地消息发送给中转服务器。""" while self.is_running: try: msg = await asyncio.wait_for(self.msg_queue.get(), timeout=1.0) @@ -242,29 +226,19 @@ async def ws_send(self, ws): break def on_chat(self, pkt): # skipcq: PY-R1000 - """监听本地玩家聊天。""" player = pkt.get("SourceName", "") msg = pkt.get("Message", "").strip() text_type = pkt.get("TextType", 0) - if not player or not msg: - return False - - if text_type != 1: - return False + if not player or not msg: return False + if text_type != 1: return False + if player == self.game_ctrl.bot_name: return False - if player == self.game_ctrl.bot_name: - return False - - # =============================================== - # 跨服私聊 ( .msg 或 .w ) 修复了包含空格名字的匹配问题 - # =============================================== if msg.startswith(".msg ") or msg.startswith(".w "): cmd_str = msg.split(" ", 1)[1].strip() target_player = "" content = "" - # 支持用双引号包裹带空格的玩家名 (如: .msg "Player A" 你好) if cmd_str.startswith('"'): end_idx = cmd_str.find('"', 1) if end_idx != -1: @@ -287,7 +261,6 @@ def on_chat(self, pkt): # skipcq: PY-R1000 local_players = self.get_online_players() - # 优先尝试本服内私聊 if target_player in local_players: self.game_ctrl.say_to( target_player, @@ -300,7 +273,6 @@ def on_chat(self, pkt): # skipcq: PY-R1000 ) return True - # 本服找不到,直接甩给后端让它去找 if self.is_running and self.loop and self.ws_conn: data = { "type": "private_msg", @@ -321,12 +293,8 @@ def on_chat(self, pkt): # skipcq: PY-R1000 self.game_ctrl.say_to(player, "§c互通服务未连接,无法发送跨服私聊。") return True - # =============================================== - # 全网在线状态查询 ( .list 或 .在线 ) - # =============================================== if msg in [".list", ".在线"]: if self.is_running and self.loop and self.ws_conn: - # 告诉后端:把全网名单拼好直接发回来给我 data = { "type": "event", "sub_type": "request_list", @@ -337,7 +305,6 @@ def on_chat(self, pkt): # skipcq: PY-R1000 self.loop ) else: - # 断网时只显示本地 local_players = self.get_online_players() local_str = ", ".join(local_players) msg_str = ( @@ -348,11 +315,8 @@ def on_chat(self, pkt): # skipcq: PY-R1000 self.game_ctrl.say_to(player, msg_str) return True - # 过滤其他插件可能会用到的指令前缀 - if msg.startswith("/") or msg.startswith("."): - return False + if msg.startswith("/") or msg.startswith("."): return False - # 正常全服聊天 if self.is_running and self.loop and self.ws_conn: data = { "type": "chat", @@ -367,5 +331,4 @@ def on_chat(self, pkt): # skipcq: PY-R1000 return False - entry = plugin_entry(CrossServerChat) From 59eac51a0410ff54daed197d74e6684b64f47de0 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:41:54 +0800 Subject: [PATCH 17/21] add web --- .../core_server.py" | 364 ++++++++++++++++-- 1 file changed, 335 insertions(+), 29 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index 5b6a40f9..a249e7d7 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -2,6 +2,7 @@ import json import logging import websockets +from aiohttp import web # 配置日志输出格式 logging.basicConfig( @@ -13,11 +14,303 @@ # 结构: { websocket: {"channel": "大厅", "server_name": "一区", "players": []} } clients = {} +# 存储频道元数据 (类型、密钥) +import os +META_FILE = "channels_meta.json" +if os.path.exists(META_FILE): + with open(META_FILE, "r", encoding="utf-8") as f: + channels_meta = json.load(f) +else: + channels_meta = {} + +# 强制锁定官方默认频道,任何人无法抢注更改 +channels_meta["全球大厅"] = {"type": "public", "key": ""} + +def save_meta(): + """将频道鉴权数据永久保存到本地防抢注""" + with open(META_FILE, "w", encoding="utf-8") as f: + json.dump(channels_meta, f, ensure_ascii=False, indent=4) + +# ==================== 🌐 Web UI 模板 ==================== +WEB_UI_HTML = """ + + + + + + 服服互通 - 全网实时监控大屏 + + + + + +
+ + +
+
+

+ 跨服互通枢纽 Active +

+

Global Network & Authorization Gateway

+
+
+ Data refreshed {{ lastUpdate }} +
+
+ + +
+
+
+
+
活跃频道数
+
{{ Object.keys(channels).length }}
+
+
+
+
+
+
在线子服数
+
{{ totalServers }}
+
+
+
+
+
+
全网在线玩家
+
{{ totalPlayers }}
+
+
+
+ + +
+
+
+

+ + 私密频道 · 子服规模榜 +

+
+
    +
  • +
    + {{ idx + 1 }} + {{ ch.name }} +
    + {{ ch.serverCount }} 个服务器 +
  • +
+
+ +
+
+

+ + 私密频道 · 在线人数榜 +

+
+
    +
  • +
    + {{ idx + 1 }} + {{ ch.name }} +
    + {{ ch.playerCount }} 名玩家 +
  • +
+
+
+ + +
+
+

+ # + {{ channelName }} + + + 私密 + + + + 公开 + +

+
+
+ +
+
+
+
+ + {{ server.server_name }} +
+ {{ server.ip }} +
+
+
+
在线玩家
+
+ {{ server.player_count }} +
+
+ +
+ 💤 当前服务器无人在线 + + + {{ player }} + +
+
+
+
+
+ +
+ + + +

网络空闲

+

目前没有连接到中转枢纽的子服务器。

+
+ + +
+ + + + +""" + + +async def web_index(_request): + """处理 WebUI 首页请求""" + return web.Response(text=WEB_UI_HTML, content_type="text/html", charset="utf-8") + + +async def web_status_api(_request): + """处理 WebUI 数据获取 API""" + data = {} + for ws, info in clients.items(): + chan = info["channel"] + srv = info["server_name"] + players = info["players"] + + ip_addr = "未知 IP" + if ws.remote_address: + ip_addr = f"{ws.remote_address[0]}:{ws.remote_address[1]}" + + if chan not in data: + meta = channels_meta.get(chan, {"type": "public"}) + data[chan] = { + "meta": {"type": meta["type"]}, + "servers": [] + } + + data[chan]["servers"].append({ + "server_name": srv, + "player_count": len(players), + "players": players, + "ip": ip_addr + }) + + return web.json_response(data) + + +async def start_web_server(host, port): + """启动独立运行在同一事件循环内的 Web 服务""" + app = web.Application() + app.router.add_get("/", web_index) + app.router.add_get("/api/status", web_status_api) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() + logging.info("🌐 WebUI 管理大屏已就绪,请在浏览器访问 http://%s:%d", host, port) + async def handle_client(websocket, _path): # skipcq: PY-R1000 """处理每个客户端连接及数据路由""" try: - # 握手认证 auth_msg = await websocket.recv() auth_data = json.loads(auth_msg) @@ -27,18 +320,38 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 channel = auth_data.get("channel", "default") server_name = auth_data.get("server_name", "未知子服") + c_type = "private" if auth_data.get("channel_type") == "私密" else "public" + c_key = auth_data.get("channel_key", "") + + # ================= 权限校验模块 ================= + if channel in channels_meta: + meta = channels_meta[channel] + if meta["type"] == "private" and meta["key"] != c_key: + reject_msg = {"type": "auth_fail", "msg": "该频道为私密频道,密钥错误!"} + await websocket.send(json.dumps(reject_msg)) + logging.warning( + "拒绝接入: %s 尝试连接私密频道 [%s] 密码错误 (%s)", + server_name, channel, websocket.remote_address + ) + return + else: + # 若是第一个连接的,则永久注册该频道并保存元数据 + channels_meta[channel] = {"type": c_type, "key": c_key} + save_meta() + logging.info("⭐ 新频道永久注册: [%s] (类型: %s)", channel, c_type) + + # 告诉前端验证成功 + await websocket.send(json.dumps({"type": "auth_success"})) clients[websocket] = { "channel": channel, "server_name": server_name, - "players": [] # 重点:在此缓存该服务器定时上报的在线玩家名单 + "players": [] } logging.info( "🔗 新子服接入: [%s] (%s) | 当前总节点: %d", - server_name, - channel, - len(clients) + server_name, channel, len(clients) ) # 智能数据路由 @@ -47,7 +360,6 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 msg_type = data.get("type") my_info = clients[websocket] - # 1. 普通聊天直接全频道广播 if msg_type == "chat": targets = [ ws for ws, info in list(clients.items()) @@ -59,19 +371,14 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 except Exception: pass - # 2. 接收子服定时上报的状态,并更新缓存 elif msg_type == "status": clients[websocket]["players"] = data.get("players", []) - # 3. 处理事件(比如前端请求全网名单) elif msg_type == "event": sub_type = data.get("sub_type") - # 前端要求后端下发整理好的全网名单 if sub_type == "request_list": requester = data.get("requester") - - # 后端瞬间聚合内存中的数据 total_players = 0 servers_data = {} @@ -81,13 +388,11 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 servers_data[info["server_name"]] = p_list total_players += len(p_list) - # 直接在后端完成排版 lines = [f"§e==== 🌐 全网同频道在线: {total_players} 人 ===="] for srv, p_list in servers_data.items(): p_str = ", ".join(p_list) if p_list else "§8无人在线" lines.append(f"§7[§b{srv}§7] §a({len(p_list)}人) §f{p_str}") - # 把拼接好的文本发回给那个发请求的子服 reply_msg = { "type": "event", "sub_type": "reply_list", @@ -99,30 +404,25 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 except Exception: pass - # 4. 跨服私聊直接由后端依据缓存进行路由分发 elif msg_type == "private_msg": target = data.get("target") sender = data.get("player") routed = False for ws, info in clients.items(): - # 遍历缓存,如果人在那个服的名单里,就发过去 is_same_channel = info["channel"] == my_info["channel"] if is_same_channel and target in info["players"]: try: await ws.send(message) routed = True logging.info( - "✉️ 跨服私聊路由: %s -> %s (已投递至 %s)", - sender, - target, - info["server_name"] + "✉️ 跨服私聊路由: %s -> %s (投递至 %s)", + sender, target, info["server_name"] ) except Exception: pass break - # 如果翻遍了所有缓存都没这个人,给发送者回个错误提示 if not routed: error_msg = { "type": "event", @@ -140,25 +440,31 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 except Exception as e: logging.error("客户端连接异常: %s", e) finally: + # 断开清理逻辑 if websocket in clients: info = clients[websocket] + chan = info["channel"] del clients[websocket] logging.info( "❌ 子服断开: [%s] (%s) | 剩余节点: %d", - info["server_name"], - info["channel"], - len(clients) + info["server_name"], chan, len(clients) ) + # 已移除旧版的“频道为空自动销毁密码”逻辑 + # 现在私人频道的密码将被永久保留,绝对防抢注! async def main(): - """启动 WebSocket 服务器。""" + """启动 WebSocket 服务器与 Web 监控页面。""" host = "0.0.0.0" # skipcq: BAN-B104 - port = 8765 - logging.info("🚀 互通中转核心已启动 -> ws://%s:%d", host, port) - logging.info("💡 当前处于【定时上报,后端缓存集中路由】的数据流模式") - - async with websockets.serve(handle_client, host, port): + ws_port = 8765 + web_port = 8766 + + logging.info("🚀 互通中转核心鉴权网关已启动") + + await start_web_server(host, web_port) + + logging.info("🕹️ 跨服 WS 网关正在监听 ws://%s:%d", host, ws_port) + async with websockets.serve(handle_client, host, ws_port): await asyncio.Future() From 1e0ffea97c8dfd28c3e7e2558d7992673f9984c6 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:42:22 +0800 Subject: [PATCH 18/21] Update datas.json --- .../datas.json" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" index 1ba5848e..6798267a 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" @@ -1,7 +1,7 @@ { "plugin-id": "server-interconnection", "author": "哈茶块", - "version": "2.2.0", + "version": "2.3.2", "description": "实现服服互通!可以让多个服务器内的聊天消息互通", "plugin-type": "classic", "pre-plugins": { From 35f3bba187ed670644539bf1a839de6fd0bdf29f Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:47:13 +0800 Subject: [PATCH 19/21] Update core_server.py --- .../core_server.py" | 354 +++++++++++++----- 1 file changed, 261 insertions(+), 93 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" index a249e7d7..4376aea0 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/core_server.py" @@ -1,6 +1,7 @@ import asyncio import json import logging +import os import websockets from aiohttp import web @@ -14,22 +15,29 @@ # 结构: { websocket: {"channel": "大厅", "server_name": "一区", "players": []} } clients = {} -# 存储频道元数据 (类型、密钥) -import os META_FILE = "channels_meta.json" -if os.path.exists(META_FILE): - with open(META_FILE, "r", encoding="utf-8") as f: - channels_meta = json.load(f) -else: - channels_meta = {} + + +def load_channels_meta(): + """从本地加载频道鉴权数据防抢注。""" + if os.path.exists(META_FILE): + with open(META_FILE, "r", encoding="utf-8") as file_obj: + return json.load(file_obj) + return {} + + +# 存储频道元数据 (类型、密钥) +channels_meta = load_channels_meta() # 强制锁定官方默认频道,任何人无法抢注更改 channels_meta["全球大厅"] = {"type": "public", "key": ""} + def save_meta(): - """将频道鉴权数据永久保存到本地防抢注""" - with open(META_FILE, "w", encoding="utf-8") as f: - json.dump(channels_meta, f, ensure_ascii=False, indent=4) + """将频道鉴权数据永久保存到本地防抢注。""" + with open(META_FILE, "w", encoding="utf-8") as file_obj: + json.dump(channels_meta, file_obj, ensure_ascii=False, indent=4) + # ==================== 🌐 Web UI 模板 ==================== WEB_UI_HTML = """ @@ -45,129 +53,271 @@ def save_meta(): [v-cloak] { display: none; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } - ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } + ::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; + } ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } - +
- + -
+
-

- 跨服互通枢纽 Active +

+ + 跨服互通枢纽 + Active

-

Global Network & Authorization Gateway

+

+ Global Network & Authorization Gateway +

-
+
Data refreshed {{ lastUpdate }}
-
-
+
+
-
活跃频道数
-
{{ Object.keys(channels).length }}
+
活跃频道数
+
+ {{ Object.keys(channels).length }} +
-
-
+
+
-
在线子服数
-
{{ totalServers }}
+
在线子服数
+
+ {{ totalServers }} +
-
-
+
+
-
全网在线玩家
-
{{ totalPlayers }}
+
全网在线玩家
+
+ {{ totalPlayers }} +
-
-
-
-

- +
+
+
+

+ + + 私密频道 · 子服规模榜

    -
  • +
  • - {{ idx + 1 }} - {{ ch.name }} + + {{ idx + 1 }} + + + {{ ch.name }} +
    - {{ ch.serverCount }} 个服务器 + + {{ ch.serverCount }} + + 个服务器 + +
- -
-
-

- + +
+
+

+ + + 私密频道 · 在线人数榜

    -
  • +
  • - {{ idx + 1 }} - {{ ch.name }} + + {{ idx + 1 }} + + + {{ ch.name }} +
    - {{ ch.playerCount }} 名玩家 + + {{ ch.playerCount }} + + 名玩家 + +

-
+

- # + # {{ channelName }} - - + + + + 私密 - - + + + + + 公开

- +
-
-
-
- +
+
+
+ + {{ server.server_name }}
- {{ server.ip }} + + {{ server.ip }} +
-
在线玩家
-
- {{ server.player_count }} +
+ 在线玩家 +
+
+ {{ server.player_count }} + + 人 +
- -
- 💤 当前服务器无人在线 - - + +
+ + 💤 当前服务器无人在线 + + + + + {{ player }}
@@ -176,15 +326,20 @@ def save_meta():
-
- - +
+ +

网络空闲

目前没有连接到中转枢纽的子服务器。

- -
@@ -199,14 +354,18 @@ def save_meta(): const totalServers = computed(() => { let count = 0 - for (const key in channels.value) count += channels.value[key].servers.length + for (const key in channels.value) { + count += channels.value[key].servers.length + } return count }) const totalPlayers = computed(() => { let count = 0 for (const key in channels.value) { - channels.value[key].servers.forEach(s => count += s.player_count) + channels.value[key].servers.forEach(s => { + count += s.player_count + }) } return count }) @@ -218,7 +377,9 @@ def save_meta(): name, serverCount: data.servers.length })); - return privates.sort((a, b) => b.serverCount - a.serverCount).slice(0, 5); + return privates.sort( + (a, b) => b.serverCount - a.serverCount + ).slice(0, 5); }); const topPrivateByPlayers = computed(() => { @@ -226,9 +387,13 @@ def save_meta(): .filter(([_, data]) => data.meta.type === 'private') .map(([name, data]) => ({ name, - playerCount: data.servers.reduce((sum, s) => sum + s.player_count, 0) + playerCount: data.servers.reduce( + (sum, s) => sum + s.player_count, 0 + ) })); - return privates.sort((a, b) => b.playerCount - a.playerCount).slice(0, 5); + return privates.sort( + (a, b) => b.playerCount - a.playerCount + ).slice(0, 5); }); const fetchData = async () => { @@ -236,9 +401,14 @@ def save_meta(): const res = await fetch('/api/status') const data = await res.json() channels.value = data - + const now = new Date() - lastUpdate.value = now.toLocaleTimeString('en-US', { hour12: false }) + '.' + now.getMilliseconds().toString().padStart(3, '0').slice(0,1) + const timeStr = now.toLocaleTimeString( + 'en-US', { hour12: false } + ) + const msStr = now.getMilliseconds().toString() + .padStart(3, '0').slice(0,1) + lastUpdate.value = `${timeStr}.${msStr}` } catch (e) { console.error("Fetch API error:", e) } @@ -250,7 +420,7 @@ def save_meta(): }) return { - channels, totalServers, totalPlayers, + channels, totalServers, totalPlayers, topPrivateByServers, topPrivateByPlayers, lastUpdate } } @@ -262,18 +432,18 @@ def save_meta(): async def web_index(_request): - """处理 WebUI 首页请求""" + """处理 WebUI 首页请求。""" return web.Response(text=WEB_UI_HTML, content_type="text/html", charset="utf-8") async def web_status_api(_request): - """处理 WebUI 数据获取 API""" + """处理 WebUI 数据获取 API。""" data = {} for ws, info in clients.items(): chan = info["channel"] srv = info["server_name"] players = info["players"] - + ip_addr = "未知 IP" if ws.remote_address: ip_addr = f"{ws.remote_address[0]}:{ws.remote_address[1]}" @@ -296,7 +466,7 @@ async def web_status_api(_request): async def start_web_server(host, port): - """启动独立运行在同一事件循环内的 Web 服务""" + """启动独立运行在同一事件循环内的 Web 服务。""" app = web.Application() app.router.add_get("/", web_index) app.router.add_get("/api/status", web_status_api) @@ -309,7 +479,7 @@ async def start_web_server(host, port): async def handle_client(websocket, _path): # skipcq: PY-R1000 - """处理每个客户端连接及数据路由""" + """处理每个客户端连接及数据路由。""" try: auth_msg = await websocket.recv() auth_data = json.loads(auth_msg) @@ -449,20 +619,18 @@ async def handle_client(websocket, _path): # skipcq: PY-R1000 "❌ 子服断开: [%s] (%s) | 剩余节点: %d", info["server_name"], chan, len(clients) ) - # 已移除旧版的“频道为空自动销毁密码”逻辑 - # 现在私人频道的密码将被永久保留,绝对防抢注! async def main(): """启动 WebSocket 服务器与 Web 监控页面。""" host = "0.0.0.0" # skipcq: BAN-B104 - ws_port = 8765 - web_port = 8766 - + ws_port = 8765 + web_port = 8766 + logging.info("🚀 互通中转核心鉴权网关已启动") - + await start_web_server(host, web_port) - + logging.info("🕹️ 跨服 WS 网关正在监听 ws://%s:%d", host, ws_port) async with websockets.serve(handle_client, host, ws_port): await asyncio.Future() From e1cc6f440eee3b0719d4b43ab97b880c6eff06c7 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:48:10 +0800 Subject: [PATCH 20/21] Update __init__.py --- .../__init__.py" | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" index eb045301..03a1e621 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/__init__.py" @@ -1,14 +1,13 @@ import asyncio import json import threading -import time # skipcq: PY-W2000 from tooldelta import Config, Plugin, plugin_entry, game_utils from tooldelta.constants import PacketIDS class CrossServerChat(Plugin): - """服服互通聊天插件""" + """服服互通聊天插件 (频道鉴权版)。""" name = "服服互通聊天" author = "哈茶块" @@ -18,12 +17,14 @@ class CrossServerChat(Plugin): CONFIG_TEMPLATE = { "中转服务器地址": "ws://core.aurorabot.top", "当前服务器名称": "你的服务器名称", - "频道名称": "全球大厅", # 默认进入官方公开频道。如需建立私人网络请改为自定义名称 + # 默认进入官方公开频道。如需建立私人网络请改为自定义名称 + "频道名称": "全球大厅", "频道类型(公开/私密)": "公开", "频道密钥(仅私密需填)": "" } def __init__(self, frame): + """初始化插件,处理热重载。""" super().__init__(frame) if hasattr(self.frame, "_cross_server_chat_instance"): @@ -51,6 +52,7 @@ def __init__(self, frame): self.ListenPacket(PacketIDS.Text, self.on_chat) def stop(self): + """停止后台服务,供重载时安全清理旧线程。""" self.is_running = False if self.ws_conn and self.loop: try: @@ -59,29 +61,41 @@ def stop(self): pass def get_online_players(self): + """获取所有在线玩家列表,兼容多核心版本。""" players = [] try: res = game_utils.getTarget("@a") - if isinstance(res, list): players = res - except Exception: pass - if players: return players + if isinstance(res, list): + players = res + except Exception: + pass + + if players: + return players try: if hasattr(self.game_ctrl, "players"): p_list = self.game_ctrl.players.getAllPlayers() - players = [p.name if hasattr(p, 'name') else str(p) for p in p_list] - except Exception: pass - if players: return players + players = [ + p.name if hasattr(p, 'name') else str(p) for p in p_list + ] + except Exception: + pass + + if players: + return players try: has_all = hasattr(self.game_ctrl, "all_players") if has_all and isinstance(self.game_ctrl.all_players, list): players = self.game_ctrl.all_players - except Exception: pass + except Exception: + pass return players def on_def(self): + """插件预加载时检查依赖并启动线程。""" try: self.GetPluginAPI("pip").require("websockets") except Exception as e: @@ -95,11 +109,13 @@ def on_def(self): threading.Thread(target=self.run_ws_client, daemon=True).start() def run_ws_client(self): + """在新线程中初始化异步事件循环。""" self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.loop.run_until_complete(self.ws_main()) async def ws_main(self): + """维持与中转服务器的持久连接。""" import websockets uri = self.cfg["中转服务器地址"] @@ -109,7 +125,7 @@ async def ws_main(self): try: self.print_inf(f"正在尝试连接中转服务器: {uri} ...") async with websockets.connect(uri) as ws: - + # 1. 鉴权握手 auth_data = { "type": "auth", @@ -119,17 +135,17 @@ async def ws_main(self): "channel_key": self.cfg.get("频道密钥(仅私密需填)", "") } await ws.send(json.dumps(auth_data)) - + # 2. 等待服务端反馈 (5秒超时) auth_resp_str = await asyncio.wait_for(ws.recv(), timeout=5.0) auth_resp = json.loads(auth_resp_str) - + if auth_resp.get("type") == "auth_fail": self.print_err(f"❌ 互通连接被拒绝: {auth_resp.get('msg')}") - self.print_war("请修改 [插件配置文件/服服互通聊天.json] 后使用 reload 重载插件。") - self.is_running = False # 彻底阻断,防止刷屏重连 + self.print_war("请修改配置后使用 reload 重载插件。") + self.is_running = False # 彻底阻断,防止刷屏重连 return - + self.ws_conn = ws self.print_suc("✅ 鉴权通过,互通服务已上线!") @@ -163,6 +179,7 @@ async def ws_main(self): await asyncio.sleep(1) async def status_report_loop(self, ws): + """定时高频上报本服务器的真实玩家名单给后端缓存。""" while self.is_running: try: local_players = self.get_online_players() @@ -176,6 +193,7 @@ async def status_report_loop(self, ws): await asyncio.sleep(3) async def ws_recv(self, ws): + """接收中转服务器发来的跨服消息。""" async for msg in ws: if not self.is_running: break @@ -216,6 +234,7 @@ async def ws_recv(self, ws): self.print_err(f"处理跨服消息时出错: {e}") async def ws_send(self, ws): + """将队列里的本地消息发送给中转服务器。""" while self.is_running: try: msg = await asyncio.wait_for(self.msg_queue.get(), timeout=1.0) @@ -226,13 +245,17 @@ async def ws_send(self, ws): break def on_chat(self, pkt): # skipcq: PY-R1000 + """监听本地玩家聊天并处理指令分发。""" player = pkt.get("SourceName", "") msg = pkt.get("Message", "").strip() text_type = pkt.get("TextType", 0) - if not player or not msg: return False - if text_type != 1: return False - if player == self.game_ctrl.bot_name: return False + if not player or not msg: + return False + if text_type != 1: + return False + if player == self.game_ctrl.bot_name: + return False if msg.startswith(".msg ") or msg.startswith(".w "): cmd_str = msg.split(" ", 1)[1].strip() @@ -315,7 +338,8 @@ def on_chat(self, pkt): # skipcq: PY-R1000 self.game_ctrl.say_to(player, msg_str) return True - if msg.startswith("/") or msg.startswith("."): return False + if msg.startswith("/") or msg.startswith("."): + return False if self.is_running and self.loop and self.ws_conn: data = { @@ -331,4 +355,5 @@ def on_chat(self, pkt): # skipcq: PY-R1000 return False + entry = plugin_entry(CrossServerChat) From 236212bd401e40035999ecb434da902052404a54 Mon Sep 17 00:00:00 2001 From: aiwanyouxi <143928828+cxksdsdsdsd@users.noreply.github.com> Date: Mon, 4 May 2026 15:54:23 +0800 Subject: [PATCH 21/21] Enhance plugin description with webUI details Updated the plugin description to include webUI functionality. --- .../datas.json" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" index 6798267a..9e8c0a33 100644 --- "a/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" +++ "b/\346\234\215\346\234\215\344\272\222\351\200\232\350\201\212\345\244\251/datas.json" @@ -2,7 +2,7 @@ "plugin-id": "server-interconnection", "author": "哈茶块", "version": "2.3.2", - "description": "实现服服互通!可以让多个服务器内的聊天消息互通", + "description": "实现服服互通!可以让多个服务器内的聊天消息互通并且拥有webUI", "plugin-type": "classic", "pre-plugins": { "pip模块支持": "0.0.6"