- 高危词库从 process.py 抽到 config.toml [security] section - process.py 改为 _get_high_risk_words() 动态加载(配置优先) - 新增 15 个 QQ 操作脚本(已脱敏:QQ号/IP/Token → 占位符) - 新增 8 个 SKILL.md(qq-messenger/management/resolver/napcat-extras 等) - 新增 SKILL.md 入口(完整部署方案文档) - 保留 SDK 框架(plugin_modules.py, file_store_api.py, package.py 等)
117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""MC RCON 客户端"""
|
|
|
|
import socket
|
|
import struct
|
|
|
|
class RCONClient:
|
|
MAX_RETRIES = 3
|
|
RETRY_DELAY = 2
|
|
CONNECT_TIMEOUT = 5
|
|
SOCKET_TIMEOUT = 10
|
|
|
|
def __init__(self, host, port, password):
|
|
self.host = host
|
|
self.port = port
|
|
self.password = password
|
|
self.sock = None
|
|
self.packet_id = 1
|
|
self.authenticated = False
|
|
|
|
def connect(self):
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.sock.settimeout(self.CONNECT_TIMEOUT)
|
|
self.sock.connect((self.host, self.port))
|
|
self.authenticate()
|
|
|
|
def authenticate(self):
|
|
packet = self._build_packet(self.packet_id, 3, self.password)
|
|
self.sock.sendall(packet)
|
|
response = self._read_response()
|
|
if not response or response.get("id") == -1:
|
|
raise Exception("RCON authentication failed")
|
|
self.authenticated = True
|
|
self.packet_id += 1
|
|
|
|
def is_connected(self):
|
|
if not self.sock or not self.authenticated:
|
|
return False
|
|
try:
|
|
self.sock.send(b"")
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def send_command(self, command, max_retries=None):
|
|
if max_retries is None:
|
|
max_retries = self.MAX_RETRIES
|
|
last_error = None
|
|
for attempt in range(max_retries):
|
|
try:
|
|
if not self.is_connected():
|
|
self.close()
|
|
self.connect()
|
|
packet = self._build_packet(self.packet_id, 2, command)
|
|
self.sock.sendall(packet)
|
|
self.packet_id += 1
|
|
response = self._read_response()
|
|
return response
|
|
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError) as e:
|
|
last_error = e
|
|
self.close()
|
|
if attempt < max_retries - 1:
|
|
import time
|
|
time.sleep(self.RETRY_DELAY)
|
|
except Exception as e:
|
|
last_error = e
|
|
self.close()
|
|
if attempt < max_retries - 1:
|
|
import time
|
|
time.sleep(self.RETRY_DELAY)
|
|
return None
|
|
|
|
def _build_packet(self, packet_id, packet_type, body_str):
|
|
body = body_str.encode("utf-8") + b"\x00\x00"
|
|
payload = struct.pack("<ii", packet_id, packet_type) + body
|
|
length = struct.pack("<i", len(payload))
|
|
return length + payload
|
|
|
|
def _read_response(self):
|
|
try:
|
|
self.sock.settimeout(self.SOCKET_TIMEOUT)
|
|
length_data = self.sock.recv(4)
|
|
except (ConnectionResetError, ConnectionAbortedError, OSError, socket.timeout):
|
|
return None
|
|
if not length_data:
|
|
return None
|
|
length = struct.unpack("<i", length_data)[0]
|
|
if length <= 0:
|
|
return None
|
|
remaining = b""
|
|
while len(remaining) < length:
|
|
try:
|
|
chunk = self.sock.recv(length - len(remaining))
|
|
if not chunk:
|
|
break
|
|
remaining += chunk
|
|
except (ConnectionResetError, ConnectionAbortedError, OSError, socket.timeout):
|
|
break
|
|
if len(remaining) < 8:
|
|
return None
|
|
packet_id, packet_type = struct.unpack("<ii", remaining[:8])
|
|
body = remaining[8:-2] if len(remaining) > 8 else b""
|
|
return {"id": packet_id, "type": packet_type, "body": body.decode("utf-8", errors="replace")}
|
|
|
|
def close(self):
|
|
if self.sock:
|
|
self.sock.close()
|
|
self.sock = None
|
|
self.authenticated = False
|
|
|
|
def __enter__(self):
|
|
self.connect()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
self.close()
|