新增 Minecraft 渠道扩展,支持在 Yuxi 平台中集成 Minecraft 游戏服务器渠道。 包含以下功能模块: - client: Minecraft 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: 认证管理 - accounts: 账户管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - protocol: Minecraft 协议处理 - rcon: RCON 远程控制 - keepalive: 连接保活 - version_adapter: 版本适配 - setup: 初始化设置 - types: 类型定义
246 lines
8.1 KiB
Python
246 lines
8.1 KiB
Python
import json
|
|
import logging
|
|
import uuid
|
|
from datetime import datetime, UTC
|
|
|
|
from yuxi.channel.extensions.minecraft.dedupe import is_duplicate as check_dedup
|
|
from yuxi.channel.extensions.minecraft.protocol import (
|
|
read_string,
|
|
read_uuid,
|
|
read_varint,
|
|
read_bool,
|
|
read_varint_at,
|
|
read_string_at,
|
|
)
|
|
from yuxi.channel.extensions.minecraft.format import strip_mc_format_codes, json_chat_to_plain
|
|
from yuxi.channel.extensions.minecraft.types import McPacket, ResolvedMcAccount
|
|
from yuxi.channel.message.models import GroupContext, MessageType, PeerInfo, UnifiedMessage
|
|
from yuxi.channel.routing.models import PeerKind
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def mc_packet_to_unified(
|
|
packet: McPacket,
|
|
account: ResolvedMcAccount,
|
|
) -> UnifiedMessage | None:
|
|
if packet.packet_id == 0x37:
|
|
return _parse_player_chat_1214(packet, account)
|
|
if packet.packet_id == 0x38:
|
|
return _parse_player_chat_1204(packet, account)
|
|
if packet.packet_id in (0x6B, 0x64):
|
|
return _parse_system_chat(packet, account)
|
|
return None
|
|
|
|
|
|
def _parse_player_chat_1214(packet: McPacket, account: ResolvedMcAccount) -> UnifiedMessage | None:
|
|
try:
|
|
offset = 0
|
|
sender_uuid, consumed = read_uuid(packet.data, offset)
|
|
offset += consumed
|
|
msg_index, consumed = read_varint(packet.data, offset)
|
|
offset += consumed
|
|
_sig_present, consumed = read_bool(packet.data, offset)
|
|
offset += consumed
|
|
sender_name_len, consumed = read_varint(packet.data, offset)
|
|
offset += consumed
|
|
sender_name_bytes = packet.data[offset : offset + sender_name_len]
|
|
sender_name = sender_name_bytes.decode("utf-8", errors="replace")
|
|
offset += sender_name_len
|
|
|
|
if check_dedup(msg_index):
|
|
return None
|
|
|
|
content = _extract_chat_content_1214(packet.data, offset)
|
|
content = strip_mc_format_codes(content).strip()
|
|
if not content:
|
|
return None
|
|
|
|
group_id = f"mc:{account.host}:{account.port}"
|
|
bot_username = account.username
|
|
|
|
was_mentioned = bot_username.lower() in content.lower() if bot_username else False
|
|
|
|
return UnifiedMessage(
|
|
msg_id=f"mc:chat:{msg_index}:{uuid.uuid4().hex[:8]}",
|
|
channel_type="minecraft",
|
|
account_id=account.account_id,
|
|
content=content,
|
|
sender=PeerInfo(
|
|
kind=PeerKind.USER,
|
|
id=sender_uuid,
|
|
display_name=sender_name,
|
|
username=sender_name,
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
group=GroupContext(
|
|
id=group_id,
|
|
name=account.host,
|
|
),
|
|
timestamp=datetime.now(UTC),
|
|
raw_payload={
|
|
"sender_uuid": sender_uuid,
|
|
"sender_name": sender_name,
|
|
"message_index": msg_index,
|
|
"world": account.default_dimension,
|
|
},
|
|
metadata={
|
|
"minecraft_server": account.host,
|
|
"minecraft_port": account.port,
|
|
"minecraft_world": account.default_dimension,
|
|
"minecraft_message_index": msg_index,
|
|
},
|
|
surface="minecraft",
|
|
originating_channel="minecraft",
|
|
conversation_label=group_id,
|
|
was_mentioned=was_mentioned,
|
|
explicitly_mentioned_bot=was_mentioned,
|
|
)
|
|
except Exception:
|
|
logger.exception("Failed to parse MC 1.21.4 player chat packet")
|
|
return None
|
|
|
|
|
|
def _parse_player_chat_1204(packet: McPacket, account: ResolvedMcAccount) -> UnifiedMessage | None:
|
|
try:
|
|
offset = 0
|
|
|
|
sender_uuid_bytes = packet.data[offset : offset + 16]
|
|
offset += 16
|
|
sender_uuid = str(uuid.UUID(bytes=sender_uuid_bytes))
|
|
|
|
index, offset = read_varint_at(packet.data, offset)
|
|
|
|
has_signature = bool(packet.data[offset])
|
|
offset += 1
|
|
if has_signature:
|
|
sig_len, offset = read_varint_at(packet.data, offset)
|
|
offset += sig_len
|
|
|
|
sender_name, offset = read_string_at(packet.data, offset)
|
|
|
|
content = _extract_chat_content_1204_at(packet.data, offset)
|
|
content = strip_mc_format_codes(content).strip()
|
|
if not content:
|
|
return None
|
|
|
|
if check_dedup(index):
|
|
return None
|
|
|
|
group_id = f"mc:{account.host}:{account.port}"
|
|
bot_username = account.username
|
|
|
|
was_mentioned = bot_username.lower() in content.lower() if bot_username else False
|
|
|
|
return UnifiedMessage(
|
|
msg_id=f"mc:chat:{index}:{uuid.uuid4().hex[:8]}",
|
|
channel_type="minecraft",
|
|
account_id=account.account_id,
|
|
content=content,
|
|
sender=PeerInfo(
|
|
kind=PeerKind.USER,
|
|
id=sender_uuid,
|
|
display_name=sender_name,
|
|
username=sender_name,
|
|
),
|
|
message_type=MessageType.TEXT,
|
|
group=GroupContext(
|
|
id=group_id,
|
|
name=account.host,
|
|
),
|
|
timestamp=datetime.now(UTC),
|
|
raw_payload={
|
|
"sender_uuid": sender_uuid,
|
|
"sender_name": sender_name,
|
|
"message_index": index,
|
|
"world": account.default_dimension,
|
|
},
|
|
metadata={
|
|
"minecraft_server": account.host,
|
|
"minecraft_port": account.port,
|
|
"minecraft_world": account.default_dimension,
|
|
"minecraft_message_index": index,
|
|
},
|
|
surface="minecraft",
|
|
originating_channel="minecraft",
|
|
conversation_label=group_id,
|
|
was_mentioned=was_mentioned,
|
|
explicitly_mentioned_bot=was_mentioned,
|
|
)
|
|
except Exception:
|
|
logger.exception("Failed to parse MC 1.20.4 player chat packet")
|
|
return None
|
|
|
|
|
|
def _parse_system_chat(packet: McPacket, account: ResolvedMcAccount) -> UnifiedMessage | None:
|
|
try:
|
|
content, _ = read_string(packet.data, 0)
|
|
try:
|
|
parsed = json.loads(content)
|
|
plain_text = json_chat_to_plain(parsed)
|
|
except json.JSONDecodeError:
|
|
plain_text = content
|
|
|
|
plain_text = strip_mc_format_codes(plain_text).strip()
|
|
if not plain_text:
|
|
return None
|
|
|
|
group_id = f"mc:{account.host}:{account.port}"
|
|
|
|
return UnifiedMessage(
|
|
msg_id=f"mc:system:{uuid.uuid4().hex}",
|
|
channel_type="minecraft",
|
|
account_id=account.account_id,
|
|
content=plain_text,
|
|
sender=PeerInfo(
|
|
kind=PeerKind.USER,
|
|
id="system",
|
|
display_name="[Minecraft]",
|
|
username="system",
|
|
is_bot=True,
|
|
),
|
|
message_type=MessageType.EVENT,
|
|
group=GroupContext(
|
|
id=group_id,
|
|
name=account.host,
|
|
),
|
|
timestamp=datetime.now(UTC),
|
|
metadata={
|
|
"minecraft_server": account.host,
|
|
"minecraft_system_message": True,
|
|
},
|
|
surface="minecraft",
|
|
originating_channel="minecraft",
|
|
conversation_label=group_id,
|
|
)
|
|
except Exception:
|
|
logger.exception("Failed to parse MC system chat packet")
|
|
return None
|
|
|
|
|
|
def _extract_chat_content_1204(data: bytes) -> str:
|
|
content = read_string(data, 0)[0]
|
|
try:
|
|
parsed = json.loads(content)
|
|
return json_chat_to_plain(parsed)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return content
|
|
|
|
|
|
def _extract_chat_content_1204_at(data: bytes, offset: int) -> str:
|
|
content = read_string(data, offset)[0]
|
|
try:
|
|
parsed = json.loads(content)
|
|
return json_chat_to_plain(parsed)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return content
|
|
|
|
|
|
def _extract_chat_content_1214(data: bytes, offset: int) -> str:
|
|
content = read_string(data, offset)[0]
|
|
try:
|
|
parsed = json.loads(content)
|
|
return json_chat_to_plain(parsed)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return content
|