├── .gitignore ├── BotServer ├── .env ├── Bot.py ├── Plugins │ ├── Commands │ │ ├── About.py │ │ ├── Bound │ │ │ ├── Append.py │ │ │ ├── Base.py │ │ │ ├── List.py │ │ │ ├── Query.py │ │ │ ├── Remove.py │ │ │ └── __init__.py │ │ ├── Command.py │ │ ├── Help.py │ │ ├── List.py │ │ ├── Luck.py │ │ ├── Mcdr.py │ │ ├── Send.py │ │ ├── Server │ │ │ ├── Base.py │ │ │ ├── Remove.py │ │ │ ├── Status.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── Expand │ │ ├── Ai.py │ │ ├── Keywords.py │ │ └── __init__.py │ ├── SyncMessage.py │ ├── Watcher.py │ └── __init__.py ├── Resources │ ├── Commands.json │ ├── Images │ │ └── List.html │ ├── Lagrange.json │ └── WebUi │ │ ├── Assets │ │ ├── Favicon.ico │ │ ├── Index.css │ │ └── Index.js │ │ └── Index.html └── Scripts │ ├── Config.py │ ├── Globals.py │ ├── Managers │ ├── Data.py │ ├── Environment.py │ ├── Lagrange.py │ ├── Resources.py │ ├── Server.py │ ├── Version.py │ └── __init__.py │ ├── Network.py │ ├── Render.py │ ├── Servers │ ├── Http │ │ ├── Api.py │ │ ├── WebUi.py │ │ └── __init__.py │ ├── Websocket.py │ └── __init__.py │ ├── Utils.py │ └── __init__.py ├── LICENSE ├── README.md ├── pyproject.toml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/__pycache__ 3 | BotServer-v*.*.*.zip 4 | .idea 5 | Test.py 6 | .DS_Store 7 | BotServer/Plugins/.vscode/settings.json 8 | .venv/ 9 | -------------------------------------------------------------------------------- /BotServer/.env: -------------------------------------------------------------------------------- 1 | # 配置 Nonebot 监听的端口。 2 | PORT=8000 3 | # 配置 Nonebot 监听的 IP / 主机名。 4 | # 如果需要公网连接,请把此项改为 0.0.0.0。 5 | HOST="127.0.0.1" 6 | # 超级用户(拥有管理权限的用户的 QQ 号)。 7 | # 请注意,QQ 号外须加上双引号(英文状态下的半角符号),两个号码间用逗号隔开。 8 | SUPERUSERS=["123456789"] 9 | # 是否将所有的管理员视为超级用户。开启填写 true,关闭请填写 false。 10 | ADMIN_SUPERUSERS=true 11 | 12 | # 配置 Nonebot 命令起始字符。 13 | COMMAND_SEP=[" "] 14 | COMMAND_START=["."] 15 | 16 | # 日志输出等级,默认填写 INFO 即可。 17 | LOG_LEVEL="INFO" 18 | 19 | # 密钥,和插件内 Token 保持一致即可。 20 | # 用来防止别人连接到你的机器人,公网机器运行强烈建议设置。 21 | # 请勿设置为纯数字或者留空!!!!! 22 | TOKEN="" 23 | 24 | # 机器人平台(如 GoCqhttp,LLOnebot 等)的 AccessToken。 25 | # 如若没有设置,删除即可。 26 | ONEBOT_ACCESS_TOKEN="" 27 | 28 | # 指令群,机器人只对这些群的指令作响应。 29 | # 群号无需像 QQ 号一样加上引号,多个群号用逗号隔开即可。 30 | COMMAND_GROUPS=[123456789] 31 | # 消息群(即使用 !!qq 指令发送到的 QQ 群,以及把群内消息同步到游戏内的群。 32 | MESSAGE_GROUPS=[123456789] 33 | 34 | # 值类型为布尔值,开启填写 true,关闭请填写 false。 35 | # 是否把消息群内的所有消息转发到服务器内。关闭后如需向服务器发送消息,请使用 .send 指令。 36 | SYNC_ALL_QQ_MESSAGE=true 37 | # 是否转发所有在服务器内发送的消息到 QQ 群。 38 | SYNC_ALL_GAME_MESSAGE=false 39 | # 是否把服务器内的消息转发到其他的服务器。 40 | SYNC_MESSAGE_BETWEEN_SERVERS=true 41 | # 含有这些敏感词语中的任意一个的消息都不会被同步到 QQ 群,而是提醒违禁。 42 | SYNC_SENSITIVE_WORDS=["敏感词", "你妈", "色图"] 43 | 44 | # 启用的指令。send 指令仅在 SYNC_ALL_QQ_MESSAGE 为 false 时生效。 45 | COMMAND_ENABLED=["list", "luck", "server", "help", "bound", "command", "send"] 46 | 47 | # 是否播报服务器开启/关闭(播报到其他服务器和 QQ 群)。 48 | # 值类型为布尔值,开启填写 true,关闭请填写 false。 49 | BROADCAST_SERVER=true 50 | # 是否播报玩家进入/离开服务器 51 | BROADCAST_PLAYER=true 52 | 53 | # Command 指令的白名单,只有以这个列表里的指令 开头 的指令才被允许执行。 54 | # 若有填写请保留空的 COMMAND_MINECRAFT_BLACKLIST 或删除此字段。 55 | COMMAND_MINECRAFT_WHITELIST=[] 56 | # Command 指令的黑名单,若有填写请保留空的 COMMAND_MINECRAFT_WHITELIST(为空即启用所有指令)。 57 | # 启用后以这个列表里的指令 开头 的指令将被禁止执行。 58 | COMMAND_MINECRAFT_BLACKLIST=[] 59 | 60 | # 更新服务器信息的间隔时间,单位为分钟。 61 | SERVER_MEMORY_UPDATE_INTERVAL=5 62 | # 服务器信息的缓存时间,为几个单位(前面的 SERVER_MEMORY_UPDATE_INTERVAL 为一个单位)。 63 | # 超过这个时间的服务器信息将会被删除。 64 | SERVER_MEMORY_MAX_CACHE=200 65 | 66 | # 白名单的指令,默认为 whitelist 指令。 67 | # 若填写为 whitelist 时,则添加白名单指令为 whitelist add,删除白名单指令为 whitelist remove,其他指令同理。 68 | WHITELIST_COMMAND="whitelist" 69 | 70 | # 设置转发消息的颜色,包括 QQ 群内的消息和其他服务器内的转发消息。 71 | # 有效值有:black、dark_blue、dark_green、dark_aqua、dark_red、dark_purple、gold、gray、dark_gray、blue、green、aqua、red、light_purple、yellow、white、reset(取消父对象使用的颜色效果)。 72 | # 设置为 # 可以使用以6位十六进制RGB颜色格式定义的颜色。 73 | SYNC_COLOR_SOURCE="gray" 74 | SYNC_COLOR_PLAYER="gray" 75 | SYNC_COLOR_MESSAGE="gray" 76 | 77 | # 假人前缀,即使用 list 指令时的分类依据,以及进服广播的判定依据。 78 | # 若你服务器没有假人或者你不想对玩家进行分类,留空或删除即可。 79 | BOT_PREFIX="" 80 | 81 | # 绑定 QQ 号的最大数量,如若设置 0 则表示不限制。 82 | # 若你只想一个 QQ 号绑定一个 Minecraft 账号,请将 QQ_BOUND_MAX_NUMBER 设置为 1。 83 | QQ_BOUND_MAX_NUMBER=1 84 | 85 | # 是否启用 AI 功能。 86 | AI_ENABLED=false 87 | # Kimi AI 的 ApiKey,若启用 AI 则必填。 88 | AI_API_KEY="" 89 | # AI 的提示词,可以在此描述他的基本信息。 90 | AI_ROLE_MESSAGE="你是一个可爱的小女孩,乖巧可爱,听父母的话……" 91 | 92 | # 是否启用图片模式。 93 | # 启用后机器人将会把所发送的消息将会渲染为图片,但需额外配置,且响应速度会变慢。 94 | IMAGE_MODE=false 95 | # 生成图片的背景,即 css 中的 background-image 属性。 96 | # https://developer.mozilla.org/zh-CN/docs/Web/CSS/background-image 97 | IMAGE_BACKGROUND=url("https://bing.img.run/rand.php") 98 | 99 | # 是否启用自动回复功能 100 | AUTO_REPLY_ENABLED=false 101 | # 自动回复的关键词以及回复内容。 102 | # 若有多个关键词,请模仿如下事例,用逗号隔开。 103 | # 如果需要同时匹配多个关键词,请把多个关键词如下用空格隔开。请务必加上 ""。 104 | AUTO_REPLY_KEYWORDS={"看群公告里的 IP 地址。": ["服务器 在哪", "服务器", "服务器地址"]} 105 | 106 | # 是否开启 API 服务器。 107 | API_ENABLED=false 108 | # API 服务器的 token 需附带在请求头中才可通过。 109 | API_TOKEN="LonelySail123456" 110 | -------------------------------------------------------------------------------- /BotServer/Bot.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from pathlib import Path 3 | 4 | import nonebot 5 | from nonebot.adapters.onebot.v11 import Adapter 6 | from nonebot.log import logger 7 | 8 | nonebot.init() 9 | 10 | nonebot.load_plugins('Plugins') 11 | 12 | driver = nonebot.get_driver() 13 | driver.register_adapter(Adapter) 14 | 15 | 16 | def main(): 17 | log_path = Path('./Logs/') 18 | if not log_path.exists(): 19 | log_path.mkdir() 20 | logger.add((log_path / '{time}.log'), rotation='1 day') 21 | 22 | register(shutdown) 23 | nonebot.run() 24 | 25 | 26 | @driver.on_startup 27 | async def startup(): 28 | from Scripts import Network 29 | from Scripts.Servers import Websocket, Http 30 | from Scripts.Managers import ( 31 | version_manager, data_manager, 32 | environment_manager, lagrange_manager, resources_manager 33 | ) 34 | 35 | resources_manager.init() 36 | 37 | await version_manager.init() 38 | await lagrange_manager.init() 39 | # if version_manager.check_update(): 40 | # await version_manager.update_version() 41 | 42 | data_manager.load() 43 | environment_manager.init() 44 | Websocket.setup_websocket_server() 45 | Http.setup_api_http_server() 46 | Http.setup_webui_http_server() 47 | 48 | await Network.send_bot_status(True) 49 | 50 | 51 | @driver.on_shutdown 52 | async def shutdown(): 53 | from Scripts import Network 54 | from Scripts.Managers import data_manager 55 | 56 | data_manager.save() 57 | 58 | await Network.send_bot_status(False) 59 | 60 | 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/About.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 3 | from nonebot.log import logger 4 | 5 | from Scripts.Managers.Version import version_manager 6 | from Scripts.Utils import Rules, turn_message 7 | 8 | logger.debug('加载命令 About 完毕!') 9 | matcher = on_command('about', force_whitespace=True, rule=Rules.command_rule) 10 | 11 | 12 | @matcher.handle() 13 | async def handle_group(event: GroupMessageEvent): 14 | message = turn_message(about_handler()) 15 | await matcher.finish(message, at_sender=True) 16 | 17 | 18 | def about_handler(): 19 | yield F'当前版本为 {version_manager.version},{"发现新版本,请及时更新!" if version_manager.check_update() else "已是最新版本!"}' 20 | yield '\n项目官网:https://qqbot.bugjump.xyz/' 21 | yield '项目地址 https://github.com/Minecraft-QQBot' 22 | yield '欢迎加入 QQ 交流群 962802248,对这个项目感兴趣不妨点个 Star 吧!' 23 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/Append.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.params import CommandArg 4 | 5 | from Scripts.Config import config 6 | from Scripts.Managers import server_manager, data_manager 7 | from Scripts.Utils import Rules, get_permission, get_user_name, get_args, check_player 8 | from .Base import async_lock 9 | 10 | matcher = on_command('bound append', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 11 | 12 | 13 | @matcher.handle() 14 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 15 | if not get_permission(event): 16 | await matcher.finish('你没有权限执行此命令!') 17 | args = get_args(args) 18 | message = await bound_append_handler(args, event.group_id) 19 | await matcher.finish(message) 20 | 21 | 22 | async def bound_append_handler(args: list, group: int): 23 | async with async_lock: 24 | if len(args) != 2: return '参数错误!请检查语法是否正确。' 25 | user, player = args 26 | if not user.isdigit(): 27 | return '参数错误!绑定的 QQ 号格式错误。' 28 | if not check_player(player): 29 | return '玩家名称非法!玩家名称只能包含字母、数字、下划线且长度不超过 16 个字符。' 30 | if data_manager.check_player_occupied(player): 31 | return '此玩家名称已经绑定过了,请换一个名称!' 32 | if not server_manager.check_online(): 33 | return '当前没有已链接的服务器,绑定失败!请连接后再试。' 34 | if user_name := await get_user_name(group, int(user)): 35 | if data_manager.append_player(user, player): 36 | await server_manager.execute(F'{config.whitelist_command} add {player}') 37 | return F'用户 {user_name}({user}) 已绑定白名单到 {player} 玩家。' 38 | return '你绑定的玩家个数过多,绑定失败!' 39 | return F'用户 {user} 不在此群聊!请检查 QQ 号是否正确。' 40 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/Base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from nonebot import on_command 4 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 5 | from nonebot.params import CommandArg 6 | 7 | from Scripts.Config import config 8 | from Scripts.Managers import data_manager, server_manager 9 | from Scripts.Utils import Rules, check_player 10 | 11 | async_lock = asyncio.Lock() 12 | matcher = on_command('bound', force_whitespace=True, priority=10, rule=Rules.command_rule) 13 | 14 | 15 | @matcher.handle() 16 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 17 | if player := args.extract_plain_text().strip(): 18 | message = await bound_handler(event, player) 19 | await matcher.finish(message) 20 | await matcher.finish('请输入要绑定的玩家名称!') 21 | 22 | 23 | async def bound_handler(event: GroupMessageEvent, player: str): 24 | async with async_lock: 25 | if not check_player(player): 26 | return '此玩家名称非法!玩家名称应只包含字母、数字、下划线且长度不超过 16 个字符。' 27 | if player in data_manager.players: 28 | return '你已经绑定了白名单!请先解绑后尝试。' 29 | if data_manager.check_player_occupied(player): 30 | return '此玩家名称已经绑定过了,请换一个名称!' 31 | if not server_manager.check_online(): 32 | return '当前没有已连接的服务器,绑定失败!请联系管理员连接后再试。' 33 | user = str(event.user_id) 34 | if data_manager.append_player(user, player): 35 | await server_manager.execute(F'{config.whitelist_command} add {player}') 36 | return F'用户 {event.sender.card}({user}) 已成功绑定白名单到 {player} 玩家。' 37 | return '你绑定的玩家个数过多,绑定失败!' 38 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/List.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 3 | 4 | from Scripts.Managers import data_manager 5 | from Scripts.Utils import Rules, turn_message, get_permission 6 | 7 | matcher = on_command('bound list', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 8 | 9 | 10 | @matcher.handle() 11 | async def handle_group(event: GroupMessageEvent): 12 | if not get_permission(event): 13 | await matcher.finish('你没有权限执行此命令!') 14 | message = turn_message(bound_list_handler()) 15 | await matcher.finish(message) 16 | 17 | 18 | def bound_list_handler(): 19 | if data_manager.players: 20 | yield '白名单列表:' 21 | for user, player in data_manager.players.items(): 22 | yield F' {user} -> {"、".join(player)}' 23 | return None 24 | yield '当前没有绑定任何玩家!' 25 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/Query.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.params import CommandArg 4 | 5 | from Scripts.Managers import data_manager 6 | from Scripts.Utils import Rules, get_user_name, get_args 7 | 8 | matcher = on_command('bound query', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 9 | 10 | 11 | @matcher.handle() 12 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 13 | if args := get_args(args): 14 | message = await bound_query_handler(args, event.group_id) 15 | await matcher.finish(message) 16 | message = await bound_query_handler([str(event.user_id)], event.group_id) 17 | await matcher.finish(message) 18 | 19 | 20 | async def bound_query_handler(args: list, group: int): 21 | if len(args) != 1: return '参数错误!请检查语法是否正确。' 22 | if not (user := args[0]).isdigit(): 23 | return '参数错误!绑定的 QQ 号格式错误。' 24 | if user_name := await get_user_name(group, int(user)): 25 | if user not in data_manager.players: 26 | return F'用户 {user_name}({user}) 还没有绑定白名单!' 27 | return F'用户 {user_name}({user}) 绑定的白名单有 {"、".join(data_manager.players[user])} 。' 28 | return F'用户 {user} 不在此群聊!请检查 QQ 号是否正确。' 29 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/Remove.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.params import CommandArg 4 | 5 | from Scripts.Config import config 6 | from Scripts.Managers import data_manager, server_manager 7 | from Scripts.Utils import Rules, get_user_name, get_permission, get_args 8 | from .Base import async_lock 9 | 10 | matcher = on_command('bound remove', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 11 | 12 | 13 | @matcher.handle() 14 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 15 | if args := get_args(args): 16 | if (len(args) == 1) and (not args[0].isdigit()): 17 | message = await bound_remove_handler(event, args) 18 | await matcher.finish(message) 19 | if not get_permission(event): 20 | await matcher.finish('你没有权限执行此命令!') 21 | message = await bound_remove_handler(event, args) 22 | await matcher.finish(message) 23 | message = await bound_remove_handler(event, [str(event.user_id)]) 24 | await matcher.finish(message) 25 | 26 | 27 | async def bound_remove_handler(event: GroupMessageEvent, args: list): 28 | async with async_lock: 29 | if not (0 <= len(args) <= 2): 30 | return '参数错误!请检查语法是否正确。' 31 | if not server_manager.check_online(): 32 | return '当前没有有已连接的服务器,请稍后再次尝试!' 33 | if len(args) == 1: 34 | if (unknown := args[0]).isdigit(): 35 | if user_name := await get_user_name(event.group_id, unknown): 36 | if unknown not in data_manager.players: 37 | return F'用户 {user_name}({unknown}) 还没有绑定白名单!请检查 QQ 号是否正确。' 38 | bounded = data_manager.remove_player(unknown) 39 | for player in bounded: 40 | await server_manager.execute(F'{config.whitelist_command} remove {player}') 41 | return F'已移除用户 {user_name}({unknown}) 绑定的所有白名单!' 42 | if data_manager.remove_player(str(event.user_id), unknown): 43 | await server_manager.execute(F'{config.whitelist_command} remove {unknown}') 44 | return F'已移除用户 {event.sender.card}({event.user_id}) 绑定的所有白名单!' 45 | return F'用户 {event.sender.card}({event.user_id}) 没有绑定名为 {unknown} 的白名单!' 46 | user, player = args 47 | if not user.isdigit(): 48 | return '参数错误!删除绑定的 QQ 号格式错误。' 49 | if user_name := await get_user_name(event.group_id, user): 50 | if user not in data_manager.players: 51 | return F'用户 {user_name}({user}) 还没有绑定白名单!' 52 | if data_manager.remove_player(user, player): 53 | await server_manager.execute(F'{config.whitelist_command} remove {player}') 54 | return F'用户 {user} 已经从白名单中移除!' 55 | return F'用户 {user_name}({user}) 没有绑定名为 {player} 的白名单!' 56 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Bound/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.log import logger 2 | 3 | from .Append import * 4 | from .Base import * 5 | from .List import * 6 | from .Query import * 7 | from .Remove import * 8 | 9 | logger.debug('加载命令 Bound 完毕!') 10 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Command.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from nonebot import on_command 4 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 5 | from nonebot.log import logger 6 | from nonebot.params import CommandArg 7 | 8 | from Scripts.Config import config 9 | from Scripts.Managers import server_manager 10 | from Scripts.Utils import Rules, turn_message, get_permission, get_args 11 | 12 | logger.debug('加载命令 Command 完毕!') 13 | matcher = on_command('command', force_whitespace=True, rule=Rules.command_rule) 14 | 15 | 16 | @matcher.handle() 17 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 18 | if not get_permission(event): 19 | await matcher.finish('你没有权限执行此命令!') 20 | flag, response = await execute_command(get_args(args)) 21 | if flag is False: 22 | await matcher.finish(response) 23 | message = turn_message(command_handler(flag, response)) 24 | await matcher.finish(message) 25 | 26 | 27 | def command_handler(name: str, response: Union[str, dict]): 28 | if isinstance(response, dict): 29 | yield '命令已发送到所有服务器!服务器回应:' 30 | for name, response in response.items(): 31 | yield F' [{name}] -> {response if response else "无返回值"}' 32 | return None 33 | yield F'命令已发送到服务器 [{name}]!服务器回应:{response if response else "无返回值"}' 34 | 35 | 36 | def parse_command(command: list): 37 | command = ' '.join(command) 38 | if config.command_minecraft_whitelist: 39 | for enabled_command in config.command_minecraft_whitelist: 40 | if command.startswith(enabled_command): 41 | return command 42 | return None 43 | for disabled_command in config.command_minecraft_blacklist: 44 | if command.startswith(disabled_command): 45 | return None 46 | return command 47 | 48 | 49 | async def execute_command(args: list): 50 | if len(args) <= 1: 51 | return False, '参数不正确!请查看语法后再试。' 52 | server_flag, *command = args 53 | if command := parse_command(command): 54 | if server_flag == '*': 55 | return True, await server_manager.execute(command) 56 | if server := server_manager.get_server(server_flag): 57 | return server.name, await server.send_command(command) 58 | return False, F'服务器 [{server_flag}] 不存在!请检查插件配置。' 59 | return False, F'命令 {command} 已被禁止!' 60 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Help.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.log import logger 4 | from nonebot.params import CommandArg 5 | 6 | from Scripts.Config import config 7 | from Scripts.Managers import data_manager 8 | from Scripts.Utils import Rules, turn_message 9 | 10 | logger.debug('加载命令 Help 完毕!') 11 | matcher = on_command('help', force_whitespace=True, rule=Rules.command_rule) 12 | 13 | 14 | @matcher.handle() 15 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 16 | if name := args.extract_plain_text().strip(): 17 | message = turn_message(detailed_handler(name)) 18 | await matcher.finish(message) 19 | message = turn_message(help_handler()) 20 | await matcher.finish(message) 21 | 22 | 23 | def help_handler(): 24 | yield '命令列表:' 25 | for name in config.command_enabled: 26 | info = data_manager.commands[name] 27 | yield F' {name} — {data_manager.commands[name]["description"]}' 28 | if children := info.get('children'): 29 | for child_name, child_info in children.items(): 30 | yield F' +-- {name} {child_name} — {child_info["description"]}' 31 | yield '\n注: 代表必填的参数,<*name> 代表此参数可选。对于所有需要输入 QQ 号的指令,可以通过 @ 此用户来代替输入的 QQ 号。' 32 | 33 | 34 | def detailed_handler(name: str): 35 | if name in config.command_enabled: 36 | info = data_manager.commands[name] 37 | yield F'命令 {name} 的详细信息:' 38 | yield from format_info(info) 39 | if children := info.get('children'): 40 | for child_name, child_info in children.items(): 41 | child_info['prefix'] = ' ' 42 | yield F' +-- 子命令 {child_name}' 43 | yield from format_info(child_info) 44 | return None 45 | yield F'命令 {name} 不存在或已被禁用!' 46 | 47 | 48 | def format_info(info: dict): 49 | prefix = info.get('prefix', '') 50 | yield F'{prefix} +-- 用法:{info["description"]}' 51 | yield F'{prefix} +-- 语法:{info["usage"]}' 52 | if parameters := info.get('parameters'): 53 | yield F'{prefix} 参数说明:' 54 | for parameter, usage in parameters.items(): 55 | yield F'{prefix} +-- {parameter} — {usage}' 56 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/List.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.log import logger 4 | from nonebot.params import CommandArg 5 | 6 | from Scripts.Config import config 7 | from Scripts.Globals import render_template 8 | from Scripts.Managers import server_manager 9 | from Scripts.Network import get_player_uuid 10 | from Scripts.Utils import Rules, turn_message 11 | 12 | logger.debug('加载命令 List 完毕!') 13 | matcher = on_command('list', force_whitespace=True, rule=Rules.command_rule) 14 | 15 | 16 | @matcher.handle() 17 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 18 | server = (args.extract_plain_text().strip()) or None 19 | flag, response = await get_players(server) 20 | if flag is False: 21 | await matcher.finish(response) 22 | if config.image_mode: 23 | player_uuids = {} 24 | for players in response.values(): 25 | for player in players[0]: 26 | player_uuids[player] = await get_player_uuid(player) 27 | image = await render_template('List.html', (700, 1000), player_list=response, uuids=player_uuids) 28 | await matcher.finish(image) 29 | message = turn_message(list_handler(response)) 30 | await matcher.finish(message) 31 | 32 | 33 | def list_handler(players: dict): 34 | if len(players) == 1: 35 | server_name, players = players.popitem() 36 | online_count = sum(len(players) for players in players) 37 | yield F'===== {server_name} 玩家列表 =====' 38 | yield from format_players(players) 39 | yield F'当前在线人数共 {online_count} 人' 40 | return None 41 | player_count = 0 42 | if players: 43 | yield '====== 玩家列表 ======' 44 | for name, value in players.items(): 45 | if value is None: continue 46 | player_count += sum(len(players) for players in value) 47 | yield F' -------- {name} --------' 48 | yield from format_players(value) 49 | yield F'当前在线人数共 {player_count} 人' 50 | return None 51 | yield '当前没有已连接的服务器!' 52 | 53 | 54 | def format_players(players: list): 55 | if config.bot_prefix: 56 | real_players, fake_players = players 57 | real_players_str = '\n '.join(real_players) 58 | fake_players_str = '\n '.join(fake_players) 59 | yield ' ———— 玩家 ————' 60 | if not real_players_str: real_players_str = '没有玩家在线!' 61 | yield ' ' + real_players_str 62 | yield ' ———— 假人 ————' 63 | if not fake_players_str: fake_players_str = '没有假人在线!' 64 | yield ' ' + fake_players_str + '\n' 65 | return None 66 | if players := players[0]: 67 | yield ' ' + '\n '.join(players) + '\n' 68 | return None 69 | yield ' 没有玩家在线!\n' 70 | 71 | 72 | def classify_players(players: list): 73 | if not config.bot_prefix: 74 | return (players,) 75 | fake_players, real_players = [], [] 76 | for player in players: 77 | if player.upper().startswith(config.bot_prefix): 78 | fake_players.append(player) 79 | continue 80 | real_players.append(player) 81 | return real_players, fake_players 82 | 83 | 84 | async def get_players(server_flag: str = None): 85 | if server_flag is None: 86 | players = { 87 | name: classify_players(await server.send_player_list()) 88 | for name, server in server_manager.servers.items() if server.status 89 | } 90 | return True, players 91 | if server := server_manager.get_server(server_flag): 92 | return True, {server.name: classify_players(await server.send_player_list())} 93 | return False, F'没有找到已连接的 [{server_flag}] 服务器!请检查编号或名称是否输入正确。' 94 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Luck.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import date 3 | from hashlib import md5 4 | 5 | from nonebot import on_command 6 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 7 | from nonebot.log import logger 8 | 9 | from Scripts.Utils import Rules, turn_message 10 | 11 | bad_things = ( 12 | '造世吞(直接放飞', '修机器(一修就炸', '挖矿(只挖到原石', '造建筑(啥都没有', '钓鱼(全部是垃圾', '刷附魔(刷的垃圾') 13 | good_things = ( 14 | '造世吞(完美运行', '修机器(一修就好', '挖矿(挖到十钻石', '造建筑(要啥都有', '钓鱼(钓到把神弓', '刷附魔(一发就中') 15 | 16 | logger.debug('加载命令 Luck 完毕!') 17 | matcher = on_command('luck', force_whitespace=True, rule=Rules.command_rule) 18 | 19 | 20 | @matcher.handle() 21 | async def handle_group(event: GroupMessageEvent): 22 | message = turn_message(luck_handler(event)) 23 | await matcher.finish(message, at_sender=True) 24 | 25 | 26 | def luck_handler(event: GroupMessageEvent): 27 | seed_hash = md5(F'{date.today()} {event.group_id} {event.user_id}'.encode()) 28 | random.seed(seed := int(seed_hash.hexdigest(), 16)) 29 | luck_point = random.randint(10, 100) 30 | tips = '啧……' 31 | if luck_point > 90: 32 | tips = '哇!' 33 | elif luck_point > 60: 34 | tips = '喵~' 35 | elif luck_point > 30: 36 | tips = '呜……' 37 | yield F'你今天的人品为 {luck_point},{tips}' 38 | bad_thing = bad_things[(seed & event.group_id) % len(bad_things)] 39 | good_thing = good_things[(seed ^ event.group_id) % len(good_things)] 40 | yield F'今日宜:{good_thing}' 41 | if bad_thing.startswith(good_thing[:2]): 42 | bad_thing = bad_things[bad_things.index(bad_thing) - 1] 43 | yield F'今日忌:{bad_thing}' 44 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Mcdr.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.log import logger 4 | from nonebot.params import CommandArg 5 | 6 | from Scripts.Managers import server_manager 7 | from Scripts.Utils import Rules, get_permission, get_args 8 | 9 | logger.debug('加载命令 Mcdr 完毕!') 10 | matcher = on_command('mcdr', force_whitespace=True, rule=Rules.command_rule) 11 | 12 | 13 | @matcher.handle() 14 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 15 | if not get_permission(event): 16 | await matcher.finish('你没有权限执行此命令!') 17 | message = await mcdr_handler(get_args(args)) 18 | await matcher.finish(message) 19 | 20 | 21 | async def mcdr_handler(args: list): 22 | if len(args) <= 1: 23 | return '参数不正确!请查看语法后再试。' 24 | server_flag, *command = args 25 | command = ' '.join(command) 26 | if not command.startswith('!!'): 27 | command = ('!!' + command) 28 | if server_flag == '*': 29 | await server_manager.execute_mcdr(command) 30 | return '命令已发送到所有已连接的服务器!' 31 | if server := server_manager.get_server(server_flag): 32 | await server.send_mcdr_command(command) 33 | return F'命令发送到服务器 [{server.name}] 完毕!' 34 | return F'服务器 [{server_flag}] 不存在!' 35 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Send.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 3 | from nonebot.log import logger 4 | from nonebot.params import CommandArg 5 | 6 | from Scripts.Managers import server_manager, data_manager 7 | from Scripts.Utils import Rules, get_player_name 8 | 9 | logger.debug('加载命令 Send 完毕!') 10 | matcher = on_command('send', force_whitespace=True, rule=Rules.command_rule) 11 | 12 | 13 | @matcher.handle() 14 | async def handle_group(event: GroupMessageEvent, args: Message = CommandArg()): 15 | if message := args.extract_plain_text().strip(): 16 | if name := data_manager.players.get(str(event.user_id), (get_player_name(event.sender.card),))[0]: 17 | await server_manager.broadcast('QQ', name, message) 18 | await matcher.finish(F'已向服务器发送消息:{message}。') 19 | await server_manager.broadcast('QQ', '未知用户', message) 20 | await matcher.finish(F'未找到你的玩家名称,请绑定后再试!已向服务器发送消息:{message}。') 21 | await matcher.finish(F'参数错误,请检查命令格式!') 22 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Server/Base.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import MessageEvent 3 | 4 | from Scripts.Managers import server_manager, data_manager 5 | from Scripts.Utils import Rules, turn_message 6 | 7 | matcher = on_command('server', force_whitespace=True, priority=10, rule=Rules.command_rule) 8 | 9 | 10 | @matcher.handle() 11 | async def handle_group(event: MessageEvent): 12 | message = turn_message(server_handler()) 13 | await matcher.finish(message) 14 | 15 | 16 | def server_handler(): 17 | status = None 18 | for index, name in enumerate(data_manager.servers): 19 | if server := server_manager.servers.get(name): 20 | status = '在线' if server.status else '离线' 21 | yield F'({(index + 1):0>2}) [{name}] -> {status}' 22 | continue 23 | yield F'({(index + 1):0>2}) [{name}] -> 离线' 24 | if status is None: 25 | yield '当前没有已连接的服务器!请检查是否正确安装了插件或重启服务器后再试。' 26 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Server/Remove.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import MessageEvent, Message 3 | from nonebot.params import CommandArg 4 | 5 | from Scripts.Managers import server_manager, data_manager 6 | from Scripts.Utils import Rules, get_permission 7 | 8 | matcher = on_command('server remove', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 9 | 10 | 11 | @matcher.handle() 12 | async def handle_group(event: MessageEvent, args: Message = CommandArg()): 13 | if not get_permission(event): 14 | await matcher.finish('你没有权限执行此命令!') 15 | if server_flag := args.extract_plain_text().strip(): 16 | if name := parse_flag(server_flag): 17 | data_manager.remove_server(name) 18 | if server := server_manager.servers.pop(name, None): 19 | await server.disconnect() 20 | await matcher.finish(F'已成功删除服务器 [{name}] !') 21 | await matcher.finish(F'未找到服务器 [{server_flag}] !请检查输入的内容是否为编号或名称。') 22 | await matcher.finish('请输入参数!') 23 | 24 | 25 | def parse_flag(server_flag: str): 26 | if server_flag.isdigit(): 27 | server_flag = int(server_flag) 28 | if server_flag > len(data_manager.servers): 29 | return None 30 | return data_manager.servers[server_flag - 1] 31 | if server_flag in data_manager.servers: 32 | return server_flag 33 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Server/Status.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from os.path import exists 3 | 4 | from matplotlib import pyplot 5 | from matplotlib.font_manager import findSystemFonts, FontProperties 6 | from nonebot import on_command 7 | from nonebot.adapters.onebot.v11 import MessageEvent, MessageSegment, Message 8 | from nonebot.log import logger 9 | from nonebot.params import CommandArg 10 | 11 | from Scripts import Globals 12 | from Scripts.Managers import server_manager 13 | from Scripts.Utils import Rules, turn_message 14 | 15 | 16 | def choose_font(): 17 | for font_format in ('ttf', 'ttc'): 18 | if exists(F'./Font.{font_format}'): 19 | logger.info(F'已找到用户设置字体文件,将自动选择该字体作为图表字体。') 20 | return FontProperties(fname=F'./Font.{font_format}', size=15) 21 | for font_path in findSystemFonts(): 22 | if 'KAITI' in font_path.upper(): 23 | logger.success(F'自动选择系统字体 {font_path} 设为图表字体。') 24 | return FontProperties(fname=font_path, size=15) 25 | 26 | 27 | font = choose_font() 28 | matcher = on_command('server status', force_whitespace=True, block=True, priority=5, rule=Rules.command_rule) 29 | 30 | 31 | @matcher.handle() 32 | async def handle_group(event: MessageEvent, args: Message = CommandArg()): 33 | if args := args.extract_plain_text().strip(): 34 | flag, response = await get_status(args) 35 | if flag is False: 36 | await matcher.finish(response) 37 | message = turn_message(detailed_handler(flag, response)) 38 | await matcher.finish(message) 39 | flag, response = await get_status() 40 | if flag is False: 41 | await matcher.finish(response) 42 | logger.error(response) 43 | message = turn_message(status_handler(response)) 44 | await matcher.finish(message) 45 | 46 | 47 | def status_handler(data: dict): 48 | yield '已连接的所有服务器信息:' 49 | for name, occupation in data.items(): 50 | yield F'————— {name} —————' 51 | if occupation: 52 | cpu, ram = occupation 53 | yield F' 内存使用率:{ram:.1f}%' 54 | yield F' CPU 使用率:{cpu:.1f}%' 55 | continue 56 | yield F' 此服务器未处于监视状态!' 57 | if font is None: 58 | yield '\n由于系统中没有找到可用的中文字体,无法显示中文标题。请查看文档自行配置!' 59 | return None 60 | if not any(data.values()): 61 | yield '\n当前没有服务器处于监视状态!无法绘制柱状图。' 62 | yield '\n所有服务器的占用柱状图:' 63 | yield str(MessageSegment.image(draw_chart(data))) 64 | return None 65 | 66 | 67 | def detailed_handler(name: str, data: list): 68 | cpu, ram = data 69 | yield F'服务器 [{name}] 的详细信息:' 70 | yield F' 内存使用率:{ram:.1f}%' 71 | yield F' CPU 使用率:{cpu:.1f}%' 72 | if image := draw_history_chart(name): 73 | yield '\n服务器的占用历史记录:' 74 | yield str(MessageSegment.image(image)) 75 | return None 76 | yield '\n未找到服务器的占用历史记录,无法绘制图表。请稍后再试!' 77 | return None 78 | 79 | 80 | def draw_chart(data: dict): 81 | count, names = 0, [] 82 | cpu_bar, ram_bar = None, None 83 | logger.debug('正在绘制服务器占比柱状图……') 84 | pyplot.xlabel('Percentage(%)', loc='right') 85 | pyplot.title('Server Usage Percentage') 86 | for name, occupation in data.items(): 87 | if occupation: 88 | pos = (count * 2) 89 | cpu, ram = occupation 90 | names.append(name) 91 | cpu_bar = pyplot.barh(pos, cpu, color='red') 92 | ram_bar = pyplot.barh(pos + 1, ram, color='blue') 93 | count += 1 94 | pyplot.legend((cpu_bar, ram_bar), ('CPU', 'RAM')) 95 | pyplot.yticks([(count * 2 + 0.5) for count in range(len(names))], names, fontproperties=font) 96 | buffer = BytesIO() 97 | pyplot.savefig(buffer, format='png') 98 | pyplot.clf() 99 | buffer.seek(0) 100 | return buffer 101 | 102 | 103 | def draw_history_chart(name: str): 104 | logger.debug(F'正在绘制服务器 [{name}] 状态图表……') 105 | cpu = Globals.cpu_occupation.get(name) 106 | ram = Globals.ram_occupation.get(name) 107 | if len(cpu) >= 5: 108 | pyplot.ylim(0, 100) 109 | pyplot.xlabel('Time', loc='right') 110 | pyplot.ylabel('Percentage(%)', loc='top') 111 | pyplot.title('CPU & RAM Percentage') 112 | pyplot.plot(cpu, color='red', label='CPU') 113 | pyplot.plot(ram, color='blue', label='RAM') 114 | pyplot.legend() 115 | pyplot.grid() 116 | buffer = BytesIO() 117 | pyplot.savefig(buffer, format='png') 118 | pyplot.clf() 119 | buffer.seek(0) 120 | return buffer 121 | 122 | 123 | async def get_status(server_flag: str = None): 124 | if server_flag is None: 125 | if data := await server_manager.get_server_occupation(): 126 | return True, data 127 | return False, '当前没有已连接的服务器!' 128 | if server := server_manager.get_server(server_flag): 129 | if data := await server.send_server_occupation(): 130 | return server.name, data 131 | return False, F'服务器 [{server_flag}] 未处于监视状态!请重启服务器后再试。' 132 | return False, F'服务器 [{server_flag}] 未找到!请重启服务器后尝试。' 133 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/Server/__init__.py: -------------------------------------------------------------------------------- 1 | from .Base import * 2 | from .Remove import * 3 | from .Status import * 4 | 5 | logger.debug('加载命令 Server 完毕!') 6 | -------------------------------------------------------------------------------- /BotServer/Plugins/Commands/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from Scripts.Config import config 4 | 5 | for command in config.command_enabled: 6 | import_module('Plugins.Commands.' + command.capitalize()) 7 | -------------------------------------------------------------------------------- /BotServer/Plugins/Expand/Ai.py: -------------------------------------------------------------------------------- 1 | from openai import AsyncClient 2 | from openai import RateLimitError, BadRequestError 3 | from pathlib import Path 4 | from tempfile import TemporaryDirectory 5 | 6 | from nonebot import on_message 7 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment, Message, Bot 8 | from nonebot.log import logger 9 | from nonebot.rule import to_me 10 | 11 | from Scripts.Config import config 12 | from Scripts.Network import download 13 | from Scripts.Utils import Rules, get_permission 14 | 15 | logger.debug('加载 Ai 功能完毕!') 16 | messages = [{'role': 'system', 'content': config.ai_role_message}] 17 | client = AsyncClient(base_url='https://api.moonshot.cn/v1', api_key=config.ai_api_key) 18 | 19 | matcher = on_message(rule=to_me() & Rules.command_rule, priority=15, block=False, ) 20 | 21 | 22 | @matcher.handle() 23 | async def handle_message(bot: Bot, event: GroupMessageEvent): 24 | plain_text = event.get_plaintext() 25 | if plain_text.strip() in ('清空缓存', '清除缓存'): 26 | if not get_permission(event): 27 | await matcher.finish('你没有权限执行此操作!') 28 | await clear() 29 | await matcher.finish('缓存已清空!') 30 | await upload_file(event.original_message, bot) 31 | if plain_text: 32 | messages.append({'role': 'user', 'content': plain_text}) 33 | try: 34 | completion = await client.chat.completions.create( 35 | messages=messages, model='moonshot-v1-8k', temperature=0.3 36 | ) 37 | except RateLimitError: 38 | await matcher.finish(MessageSegment.reply(event.message_id) + '啊哦!你问的太快啦,我的脑袋转不过来了 TwT') 39 | except BadRequestError as error: 40 | await matcher.finish(MessageSegment.reply(event.message_id) + F'啊哦!遇到错误:{error.message}') 41 | response = completion.choices[0] 42 | if text := response.message.content: 43 | messages.append(dict(response.message)) 44 | await matcher.finish(MessageSegment.reply(event.message_id) + text) 45 | await matcher.finish(MessageSegment.reply(event.message_id) + '呃?你在说什么,能不能重新说一下 T_T') 46 | 47 | 48 | async def clear(): 49 | messages.clear() 50 | file_list = await client.files.list() 51 | for file in file_list.data: 52 | await client.files.delete(file.id) 53 | 54 | 55 | async def upload_file(message: Message, bot: Bot): 56 | file_segments = [] 57 | for segment in message: 58 | if segment.type == 'image': 59 | file_segments.append(segment.data) 60 | elif segment.type == 'reply': 61 | message = await bot.get_msg(message_id=segment.data['id']) 62 | logger.info(F'正在解析引用消息 {message} 的文件……') 63 | for reply_segment in message.get('message', []): 64 | if reply_segment['type'] in ('image', 'file'): 65 | file_segments.append(reply_segment['data']) 66 | if file_segments: 67 | logger.debug(F'上传文件:{file_segments}') 68 | with TemporaryDirectory() as temp_path: 69 | temp_path = Path(temp_path) 70 | for segment_data in file_segments: 71 | if file := await download(segment_data['url']): 72 | path = (temp_path / segment_data['filename']) 73 | with path.open('wb') as download_file: 74 | download_file.write(file.getvalue()) 75 | file = await client.files.create(file=path, purpose='file-extract') 76 | file_content = await client.files.content(file.id) 77 | messages.append({'role': 'system', 'content': file_content.text}) 78 | continue 79 | await matcher.send('下载文件失败!', at_sender=True) 80 | -------------------------------------------------------------------------------- /BotServer/Plugins/Expand/Keywords.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_message 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 3 | from nonebot.log import logger 4 | 5 | from Scripts.Config import config 6 | from Scripts.Utils import Rules 7 | 8 | logger.debug('加载 关键词回复 功能完毕!') 9 | matcher = on_message(rule=Rules.message_rule, priority=15, block=False) 10 | 11 | 12 | @matcher.handle() 13 | async def watch_keywords(event: GroupMessageEvent): 14 | plain_text = event.get_plaintext() 15 | for reply_text, keywords in config.auto_reply_keywords.items(): 16 | for keyword in keywords: 17 | if all(word in plain_text for word in keyword.split()): 18 | await matcher.finish(reply_text, at_sender=True) 19 | -------------------------------------------------------------------------------- /BotServer/Plugins/Expand/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from Scripts.Config import config 4 | 5 | if config.ai_enabled: 6 | import_module('Plugins.Expand.Ai') 7 | if config.auto_reply_enabled: 8 | import_module('Plugins.Expand.Keywords') 9 | -------------------------------------------------------------------------------- /BotServer/Plugins/SyncMessage.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from nonebot import on_message 4 | from nonebot.adapters.onebot.v11 import Bot 5 | from nonebot.adapters.onebot.v11.event import GroupMessageEvent, Reply 6 | from nonebot.log import logger 7 | 8 | from Scripts.Config import config 9 | from Scripts.Managers import server_manager, data_manager 10 | from Scripts.Utils import Rules, get_player_name, get_user_name 11 | 12 | matcher = on_message(rule=Rules.message_rule, priority=15, block=False) 13 | mapping = {'record': '语音', 'image': '图片', 'face': '表情', 'file': '文件'} 14 | 15 | 16 | @matcher.handle() 17 | async def sync_message(bot: Bot, event: GroupMessageEvent): 18 | if config.sync_all_qq_message: 19 | plain_text = event.get_plaintext() 20 | for start in config.command_start: 21 | if plain_text.startswith(start): 22 | return None 23 | plain_text = await turn_text(bot, event) 24 | name = data_manager.players.get(str(event.user_id), (get_player_name(event.sender.card),))[0] 25 | await server_manager.broadcast('QQ', (name or event.sender.nickname), plain_text) 26 | logger.debug(F'转发主群用户 {event.sender.card} 消息 {plain_text} 到游戏内。') 27 | 28 | 29 | async def turn_text(bot: Bot, event: Union[GroupMessageEvent | Reply]): 30 | plain_texts = [] 31 | if isinstance(event, GroupMessageEvent) and event.reply: 32 | event.reply.group_id = event.group_id 33 | reply_plain_text = await turn_text(bot, event.reply) 34 | plain_texts.append(F'「回复 {event.reply.sender.card or event.reply.sender.nickname}:{reply_plain_text}」') 35 | for segment in event.message: 36 | if segment.type == 'reply': 37 | continue 38 | if segment.type == 'text' and (text := segment.data['text']): 39 | plain_texts.append(text) 40 | continue 41 | if segment.type == 'at': 42 | user = str(segment.data['qq']) 43 | if player := data_manager.players.get(user): 44 | plain_texts.append(F'[@{player[0]}]') 45 | continue 46 | user_name = await get_user_name(event.group_id, int(user)) 47 | if player := get_player_name(user_name): 48 | plain_texts.append(F'[@{player}]') 49 | continue 50 | plain_texts.append(F'[@{user_name}]') 51 | continue 52 | plain_texts.append(F'[{mapping.get(segment.type, "未知类型")}]') 53 | return ' '.join(plain_texts) 54 | -------------------------------------------------------------------------------- /BotServer/Plugins/Watcher.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from nonebot import on_notice 4 | from nonebot.adapters.onebot.v11 import GroupDecreaseNoticeEvent, GroupIncreaseNoticeEvent, PokeNotifyEvent 5 | from nonebot.log import logger 6 | 7 | from Scripts.Config import config 8 | from Scripts.Managers import data_manager, server_manager 9 | from Scripts.Network import request 10 | from Scripts.Utils import Rules, turn_message 11 | 12 | matcher = on_notice(rule=Rules.message_rule, priority=15, block=False) 13 | week_mapping = ('一', '二', '三', '四', '五', '六', '日') 14 | 15 | 16 | @matcher.handle() 17 | async def watch_decrease(event: GroupDecreaseNoticeEvent): 18 | logger.info(F'检测到用户 {event.user_id} 离开了群聊!') 19 | if players := data_manager.remove_player(str(event.user_id)): 20 | for single_player in players: 21 | await server_manager.execute(F'{config.whitelist_command} remove {single_player}') 22 | await matcher.finish(F'用户 {event.user_id} 离开了群聊,自动从白名单中移除 {"、".join(players)} 玩家。') 23 | 24 | 25 | @matcher.handle() 26 | async def watch_increase(event: GroupIncreaseNoticeEvent): 27 | await matcher.finish('欢迎加入群聊!请仔细阅读群聊公告,并按照要求进行操作。', at_sender=True) 28 | 29 | 30 | @matcher.handle() 31 | async def watch_poke(event: PokeNotifyEvent): 32 | if not event.is_tome(): 33 | return None 34 | sentence = await request('https://v1.jinrishici.com/all.json') 35 | message = turn_message(poke_handler(sentence)) 36 | await matcher.finish(message) 37 | 38 | 39 | def poke_handler(sentence): 40 | now = datetime.now() 41 | yield F'{now.strftime("%Y-%m-%d")} 星期{week_mapping[now.weekday()]} {now.strftime("%H:%M:%S")}' 42 | if sentence is not None: 43 | yield F'\n「{sentence["content"]}」' 44 | yield F' —— {sentence["author"]}《{sentence["origin"]}》' 45 | -------------------------------------------------------------------------------- /BotServer/Plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Minecraft-QQBot/BotServer/b0fb6c74e830e928525310a64ef653c47f454d67/BotServer/Plugins/__init__.py -------------------------------------------------------------------------------- /BotServer/Resources/Commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "luck": { 3 | "usage": "luck", 4 | "description": "查看今日运势(仅供参考)。" 5 | }, 6 | "send": { 7 | "usage": "send ", 8 | "description": "发送消息到所有已连接的服务器。", 9 | "parameters": { 10 | "message": "要发送的消息。" 11 | } 12 | }, 13 | "mcdr": { 14 | "usage": "mcdr ", 15 | "description": "在服务器中执行 Mcdr 指令,仅管理员可用。无返回值,请慎用。" 16 | }, 17 | "about": { 18 | "usage": "about", 19 | "description": "查看关于本机器人的信息。" 20 | }, 21 | "server": { 22 | "usage": "server", 23 | "description": "查看当前所有服务器的状态和编号。", 24 | "children": { 25 | "remove": { 26 | "usage": "server remove ", 27 | "description": "从服务器列表中移除某个指定的服务器。", 28 | "parameters": { 29 | "server": "指定服务器名称或编号。" 30 | } 31 | }, 32 | "status": { 33 | "usage": "server status <*server>", 34 | "description": "查看某个服务器的状态,若没有安装依赖则自动禁用。", 35 | "parameters": { 36 | "server": "可选,指定服务器名称或编号。不填写时默认显示所有服务器。" 37 | } 38 | } 39 | } 40 | }, 41 | "list": { 42 | "usage": "list <*server>", 43 | "description": "查询当前在线的玩家。", 44 | "parameters": { 45 | "server": "可选,指定服务器名称或编号。" 46 | } 47 | }, 48 | "help": { 49 | "usage": "help <*command>", 50 | "description": "查看所有可用命令的帮助信息。", 51 | "parameters": { 52 | "command": "可选,指定命令名称并显示此命令的详细信息。" 53 | } 54 | }, 55 | "command": { 56 | "usage": "command ", 57 | "description": "在某个指定的服务器执行指令(仅管理员可用)。", 58 | "parameters": { 59 | "server": "指定服务器名称或编号,为 * 时表示所有服务器。", 60 | "command": "要执行的指令。" 61 | } 62 | }, 63 | "bound": { 64 | "usage": "bound ", 65 | "description": "绑定到白名单到游戏或查看绑定的白名单,如若允许多绑定开启可多次使用。", 66 | "parameters": { 67 | "player": "绑定的玩家名称。" 68 | }, 69 | "children": { 70 | "list": { 71 | "usage": "bound list", 72 | "description": "查看当前所有绑定的白名单,需管理员权限。" 73 | }, 74 | "query": { 75 | "usage": "bound query <*QQ>", 76 | "description": "查询某个 QQ 号绑定的玩家名称。", 77 | "parameters": { 78 | "QQ": "可选,要查询的 QQ 号,为空时查询自身绑定的白名单。" 79 | } 80 | }, 81 | "remove": { 82 | "usage": "bound remove <*QQ> <*player>", 83 | "description": "从白名单中移除某个玩家,解绑自身时无需管理员权限。", 84 | "parameters": { 85 | "QQ": "可选,要移除的玩家所绑定的 QQ 号,为空时解绑自身的白名单。", 86 | "player": "可选,要移除的玩家名称,为空时解绑所有的白名单。" 87 | } 88 | }, 89 | "append": { 90 | "usage": "bound append ", 91 | "description": "添加某个玩家到白名单,需管理员权限。", 92 | "parameters": { 93 | "QQ": "绑定用户的 QQ 号,为空时绑定到自身 QQ 号。", 94 | "player": "要绑定的玩家名称。" 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /BotServer/Resources/Images/List.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 80 | 81 | 82 | 83 |
84 |
85 |

在线玩家列表

86 | {% for server, (real_players, fake_players) in player_list.items() %} 87 |
88 |

{{ server }}

89 |
90 | {% if real_players or fake_players %} 91 | {% for player in real_players %} 92 | 93 | {{ player }} 95 | {{ player }} 96 | 97 | {% endfor %} 98 | {% for player in fake_players %} 99 | 100 | {{ player }} 101 | 102 | {% endfor %} 103 | {% else %} 104 | 暂无在线玩家! 105 | {% endif %} 106 |
107 | {% endfor %} 108 |
109 |
110 |
111 | 112 | 113 | -------------------------------------------------------------------------------- /BotServer/Resources/Lagrange.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "SignServerUrl": "https://sign.lagrangecore.org/api/sign/25765", 10 | "SignProxyUrl": "", 11 | "MusicSignServerUrl": "", 12 | "Account": { 13 | "Uin": 0, 14 | "Password": "", 15 | "Protocol": "Linux", 16 | "AutoReconnect": true, 17 | "GetOptimumServer": true 18 | }, 19 | "Message": { 20 | "IgnoreSelf": true, 21 | "StringPost": false 22 | }, 23 | "QrCode": { 24 | "ConsoleCompatibilityMode": false 25 | }, 26 | "Implementations": [ 27 | { 28 | "Type": "ReverseWebSocket", 29 | "Host": "127.0.0.1", 30 | "Port": 8080, 31 | "Suffix": "/onebot/v11/ws", 32 | "ReconnectInterval": 5000, 33 | "HeartBeatInterval": 5000, 34 | "AccessToken": "" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /BotServer/Resources/WebUi/Assets/Favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Minecraft-QQBot/BotServer/b0fb6c74e830e928525310a64ef653c47f454d67/BotServer/Resources/WebUi/Assets/Favicon.ico -------------------------------------------------------------------------------- /BotServer/Resources/WebUi/Assets/Index.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.fixed{position:fixed}.mb-3{margin-bottom:.75rem}.ml-\[10px\]{margin-left:10px}.mt-3{margin-top:.75rem}.mt-\[10px\]{margin-top:10px}.flex{display:flex}.h-5{height:1.25rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-5{width:1.25rem}.w-\[100px\]{width:100px}.w-\[300px\]{width:300px}.w-full{width:100%}.max-w-\[200px\]{max-width:200px}.max-w-\[300px\]{max-width:300px}.flex-1{flex:1 1 0%}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.p-3{padding:.75rem}.p-5{padding:1.25rem}.px-\[20\%\]{padding-left:20%;padding-right:20%}.py-\[5\%\]{padding-top:5%;padding-bottom:5%}.text-left{text-align:left}.text-right{text-align:right}.text-end{text-align:end}.text-xs{font-size:.75rem;line-height:1rem}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}*{margin:0;padding:0;box-sizing:border-box}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.i-icon{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.i-icon-spin svg{animation:i-icon-spin 1s infinite linear}.i-icon-rtl{transform:scaleX(-1)}@keyframes i-icon-spin{to{transform:rotate(360deg)}} 2 | -------------------------------------------------------------------------------- /BotServer/Resources/WebUi/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Config 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /BotServer/Scripts/Config.py: -------------------------------------------------------------------------------- 1 | from nonebot import get_plugin_config 2 | from pydantic import BaseModel 3 | 4 | 5 | class Config(BaseModel): 6 | port: int = 8000 7 | onebot_access_token: str = '' 8 | 9 | token: str = '' 10 | bot_prefix: str = None 11 | admin_superusers: bool = True 12 | 13 | superusers: list[str] = [] 14 | command_start: list[str] = ['.'] 15 | command_enabled: list[str] = [] 16 | 17 | command_groups: list[int] = [] 18 | message_groups: list[int] = [] 19 | 20 | command_minecraft_whitelist: list[str] = [] 21 | command_minecraft_blacklist: list[str] = [] 22 | 23 | broadcast_server: bool = True 24 | broadcast_player: bool = True 25 | 26 | sync_all_qq_message: bool = True 27 | sync_all_game_message: bool = False 28 | sync_message_between_servers: bool = True 29 | 30 | sync_sensitive_words: list[str] = [] 31 | 32 | server_memory_max_cache: int = 200 33 | server_memory_update_interval: int = 1 34 | 35 | whitelist_command: str = 'whitelist' 36 | 37 | sync_color_source: str = 'gray' 38 | sync_color_player: str = 'gray' 39 | sync_color_message: str = 'gray' 40 | 41 | qq_bound_max_number: int = 1 42 | 43 | ai_enabled: bool = False 44 | ai_api_key: str = None 45 | ai_role_message: str = None 46 | 47 | image_mode: bool = False 48 | image_background: str = None 49 | 50 | auto_reply_enabled: bool = False 51 | auto_reply_keywords: dict[str, list[str]] = None 52 | 53 | api_enabled: bool = False 54 | api_token: str = None 55 | 56 | 57 | config: Config = get_plugin_config(Config) 58 | 59 | config.server_memory_update_interval *= 2 60 | config.bot_prefix = config.bot_prefix.upper() 61 | config.sync_color_source = config.sync_color_source.lower() 62 | config.sync_color_player = config.sync_color_player.lower() 63 | config.sync_color_message = config.sync_color_message.lower() 64 | config.command_enabled.append('about') 65 | if config.sync_all_qq_message and ('send' in config.command_enabled): 66 | config.command_enabled.remove('send') 67 | -------------------------------------------------------------------------------- /BotServer/Scripts/Globals.py: -------------------------------------------------------------------------------- 1 | from Scripts.Config import config 2 | 3 | uuid_caches: dict[str, str] = {} 4 | cpu_occupation: dict[str, list] = {} 5 | ram_occupation: dict[str, list] = {} 6 | 7 | render_template = None 8 | 9 | if config.image_mode: 10 | from .Render import render_template 11 | 12 | # LtNsttMj1tUSaieZRjvHHk2h2AZOEKIG 13 | # https://crafatar.com/avatars/{uuid} 14 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Data.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from json import loads, dump 3 | from pathlib import Path 4 | from time import time 5 | 6 | from nonebot.log import logger 7 | 8 | from ..Config import config 9 | from .Resources import resources_manager 10 | 11 | 12 | class DataManager: 13 | webui_token: str = None 14 | 15 | servers: list = [] 16 | players: dict = {} 17 | commands: dict = {} 18 | 19 | data_dir = Path('Data') 20 | 21 | def load(self): 22 | self.load_bot_data() 23 | logger.info('加载数据文件……') 24 | if not self.data_dir.exists(): 25 | logger.warning('数据文件目录不存在,正在创建数据目录……') 26 | self.data_dir.mkdir() 27 | count_flag = 0 28 | webui_file = (self.data_dir / 'Webui.bin') 29 | server_file = (self.data_dir / 'Server.json') 30 | player_file = (self.data_dir / 'Player.json') 31 | if webui_file.exists(): 32 | count_flag += 1 33 | self.webui_token = loads(webui_file.read_text('Utf-8')) 34 | else: self.create_token() 35 | if server_file.exists(): 36 | count_flag += 1 37 | self.servers = loads(server_file.read_text('Utf-8')) 38 | if player_file.exists(): 39 | count_flag += 1 40 | self.players = loads(player_file.read_text('Utf-8')) 41 | if count_flag == 3: 42 | logger.success('加载数据文件完毕!') 43 | return None 44 | logger.warning('服务器信息文件不存在,正在创建服务器信息文件……') 45 | self.save() 46 | 47 | def load_bot_data(self): 48 | logger.debug('正在加载机器人数据……') 49 | self.commands = loads(resources_manager.read_file('Commands.json')) 50 | logger.success('加载正在加载机器人数据完毕!') 51 | 52 | def save(self): 53 | logger.debug('正在保存数据文件……') 54 | webui_file = (self.data_dir / 'Webui.bin') 55 | server_file = (self.data_dir / 'Server.json') 56 | player_file = (self.data_dir / 'Player.json') 57 | with webui_file.open('w', encoding='Utf-8') as file: 58 | dump(self.webui_token, file) 59 | with server_file.open('w', encoding='Utf-8') as file: 60 | dump(self.servers, file) 61 | with player_file.open('w', encoding='Utf-8') as file: 62 | dump(self.players, file) 63 | logger.success('保存数据文件完毕!') 64 | 65 | def create_token(self): 66 | md5_digest = md5() 67 | md5_digest.update(F'{time() * 1000} Webui'.encode('Utf-8')) 68 | self.webui_token = md5_digest.hexdigest() 69 | 70 | def remove_server(self, name: str): 71 | self.servers.remove(name) 72 | self.save() 73 | 74 | def append_server(self, name: str): 75 | if name not in self.servers: 76 | self.servers.append(name) 77 | self.save() 78 | 79 | def append_player(self, user: str, player: str): 80 | if user not in self.players: 81 | self.players[user] = [player] 82 | self.save() 83 | return True 84 | if config.qq_bound_max_number == 0: 85 | self.players[user].append(player) 86 | self.save() 87 | return True 88 | if len(self.players[user]) < config.qq_bound_max_number: 89 | self.players[user].append(player) 90 | self.save() 91 | return True 92 | return False 93 | 94 | def remove_player(self, user: str, player: str = None): 95 | if not player: 96 | bounded = self.players.pop(user, None) 97 | self.save() 98 | return bounded 99 | if player in self.players[user]: 100 | self.players[user].remove(player) 101 | if not self.players[user]: 102 | self.players.pop(user) 103 | self.save() 104 | return player 105 | return False 106 | 107 | def check_player_occupied(self, player: str): 108 | player = player.lower() 109 | for bounded_players in self.players.values(): 110 | if player in (bounded_player.lower() for bounded_player in bounded_players): 111 | return True 112 | return False 113 | 114 | 115 | data_manager = DataManager() 116 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Environment.py: -------------------------------------------------------------------------------- 1 | from json import JSONDecodeError, loads, dumps 2 | from pathlib import Path 3 | 4 | from nonebot.log import logger 5 | 6 | 7 | class EnvironmentManager: 8 | mapping: list = [] 9 | environment: dict = {} 10 | 11 | file_path: Path = Path('.env') 12 | 13 | def init(self): 14 | if not self.file_path.exists(): 15 | logger.error('没有找到配置文件!请重新下载后重试。') 16 | exit(1) 17 | self.load() 18 | 19 | def load(self): 20 | file_content = self.file_path.read_text('Utf-8') 21 | for line in file_content.split('\n'): 22 | line = line.strip() 23 | if line.startswith('#') or (not line): 24 | self.mapping.append(line) 25 | continue 26 | key, value = line.split('=') 27 | key, value = key.strip(), value.strip() 28 | try: 29 | value = loads(value) 30 | except JSONDecodeError: 31 | pass 32 | self.environment[key] = value 33 | self.mapping.append(key) 34 | logger.success('预加载配置文件完毕!文件已载入到内存中。') 35 | 36 | def update(self, new: dict): 37 | logger.info(F'正在更新配置 {new}') 38 | for key, value in new.items(): 39 | self.environment[key] = value 40 | self.write() 41 | 42 | def write(self): 43 | logger.info('正在写入配置……') 44 | lines = [] 45 | for line in self.mapping: 46 | if line.startswith('#') or (not line): 47 | lines.append(line) 48 | continue 49 | lines.append(F'{line}={dumps(self.environment[line], ensure_ascii=False)}') 50 | self.file_path.write_text('\n'.join(lines), encoding='Utf-8') 51 | logger.success('写入配置成功!手动重启机器人后修改才会生效。') 52 | 53 | 54 | environment_manager = EnvironmentManager() 55 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Lagrange.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import tarfile 3 | from json import loads, dump 4 | from pathlib import Path 5 | 6 | import asyncio 7 | from asyncio import Task 8 | from asyncio.subprocess import Process, PIPE 9 | 10 | from nonebot.log import logger 11 | 12 | from ..Config import config 13 | from ..Network import download 14 | from .Resources import resources_manager 15 | 16 | 17 | class LagrangeManager: 18 | task: Task = None 19 | process: Process = None 20 | lagrange_path: Path = None 21 | 22 | path: Path = Path('Lagrange') 23 | 24 | def __init__(self): 25 | for path in self.path.rglob('Lagrange.OneBot*'): 26 | self.lagrange_path = path.absolute() 27 | 28 | @staticmethod 29 | def parse_platform(): 30 | system = platform.system() 31 | architecture = platform.machine() 32 | system_mapping = {'Linux': 'linux', 'Darwin': 'osx', 'Windows': 'win'} 33 | if system == 'Windows': 34 | architecture = 'x64' if architecture == 'AMD64' else 'x86' 35 | elif system == 'Darwin': 36 | architecture = 'x64' if architecture == 'x86_64' else 'arm64' 37 | elif system == 'Linux': 38 | architecture = 'x64' if architecture == 'x86_64' else 'arm' 39 | return system_mapping[system], architecture 40 | 41 | async def update_config(self): 42 | config_path = (self.path / 'appsettings.json') 43 | lagrange_config = loads(resources_manager.read_file('Lagrange.json')) 44 | lagrange_config['Implementations'][0]['Port'] = config.port 45 | lagrange_config['Implementations'][0]['AccessToken'] = config.onebot_access_token 46 | with config_path.open('w', encoding='Utf-8') as file: 47 | dump(lagrange_config, file) 48 | logger.success('Lagrange.Onebot 配置文件更新成功!') 49 | return True 50 | 51 | async def init(self): 52 | if self.lagrange_path: 53 | logger.info('Lagrange.Onebot 已经安装,正在自动启动……') 54 | self.task = asyncio.create_task(self.run()) 55 | 56 | async def stop(self): 57 | async def checker(process: Process): 58 | await asyncio.sleep(10) 59 | if process.returncode is None: 60 | process.kill() 61 | 62 | if self.process: 63 | self.process.terminate() 64 | checker_task = asyncio.create_task(checker(self.process)) 65 | await self.process.wait() 66 | checker_task.cancel() 67 | self.task.cancel() 68 | self.process = None 69 | 70 | async def run(self): 71 | await self.update_config() 72 | self.process = await asyncio.create_subprocess_exec(str(self.lagrange_path), stdout=PIPE, cwd=self.path) 73 | logger.success('Lagrange.Onebot 启动成功!请扫描目录下的图片或下面的二维码登录。') 74 | async for line in self.process.stdout: 75 | line = line.decode('Utf-8').strip() 76 | if line.startswith('█') or line.startswith('▀'): 77 | logger.info(line) 78 | continue 79 | elif '[FATAL]' in line: 80 | logger.error(line) 81 | elif '[WARNING]' in line: 82 | logger.warning(line) 83 | logger.debug('[Lagrange] ' + line) 84 | 85 | async def install(self): 86 | if self.lagrange_path: 87 | logger.warning('Lagrange.Onebot 已经安装,无需再次安装!') 88 | return True 89 | if not self.path.exists(): 90 | self.path.mkdir() 91 | self.path.chmod(0o755) 92 | system, architecture = self.parse_platform() 93 | logger.info(F'检测到当前的系统架构为 {system} {architecture} 正在下载对应的安装包……') 94 | if response := await download( 95 | F'https://github.com/LagrangeDev/Lagrange.Core/releases/download/nightly/Lagrange.OneBot_{system}-{architecture}_net8.0_SelfContained.tar.gz'): 96 | logger.success(F'Lagrange.Onebot 下载成功!正在安装……') 97 | with tarfile.open(fileobj=response) as zip_file: 98 | for member in zip_file.getmembers(): 99 | if member.isfile(): 100 | with zip_file.extractfile(member) as file: 101 | file_name = file.name.split('/')[-1] 102 | with open((self.path / file_name), 'wb') as target_file: 103 | target_file.write(file.read()) 104 | logger.success('Lagrange.Onebot 安装成功!') 105 | self.lagrange_path = next(self.path.rglob('Lagrange.OneBot*')) 106 | self.lagrange_path.chmod(0o755) 107 | return self.update_config() 108 | logger.error('Lagrange.Onebot 安装失败!') 109 | return False 110 | 111 | 112 | lagrange_manager = LagrangeManager() 113 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Resources.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from zipfile import ZipFile 4 | 5 | from nonebot.log import logger 6 | 7 | 8 | class ResourcesManager: 9 | path: Path = Path('.Cache/Resources') 10 | 11 | def init(self): 12 | resources_path = Path('Resources') 13 | if resources_path.exists(): 14 | self.path = resources_path 15 | logger.debug('检测到从源码运行,无需提取资源文件。') 16 | return None 17 | logger.debug('正在初始化资源管理器……') 18 | if not self.path.exists(): 19 | logger.info('未找到资源文件,正在提取……') 20 | self.path.mkdir(parents=True) 21 | self.extract() 22 | logger.success('资源管理器初始化完成!') 23 | 24 | def remove(self): 25 | for child in self.path.rglob('*'): 26 | child.unlink() 27 | self.path.unlink() 28 | 29 | def read_file(self, file_path: str): 30 | file_path = (self.path / file_path) 31 | return file_path.read_text('Utf-8') 32 | 33 | def extract(self): 34 | cache_path = self.path.parent 35 | with ZipFile(sys.argv[0], 'r') as zip_file: 36 | for file in zip_file.namelist(): 37 | if file.startswith('Resources/'): 38 | zip_file.extract(file, cache_path) 39 | continue 40 | 41 | 42 | resources_manager = ResourcesManager() 43 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Union 3 | 4 | from nonebot.drivers import WebSocket 5 | from nonebot.exception import WebSocketClosed 6 | from nonebot.log import logger 7 | 8 | from .Data import data_manager 9 | from ..Config import config 10 | from ..Utils import Json 11 | 12 | 13 | class Server: 14 | name: str = None 15 | type: str = None 16 | status: bool = True 17 | websocket: WebSocket = None 18 | 19 | def __init__(self, name: str, websocket: WebSocket): 20 | self.name = name 21 | self.websocket = websocket 22 | self.type = websocket.request.headers.get('type') 23 | 24 | async def disconnect(self): 25 | self.status = False 26 | await self.websocket.close() 27 | logger.success(F'已断开与服务器 [{self.name}] 的连接!') 28 | 29 | async def send_data(self, event_type: str, data: object = None, wait: bool = True): 30 | if self.websocket.closed: 31 | logger.info(F'检测到与服务器 [{self.name}] 的连接已断开!') 32 | self.status = False 33 | return None 34 | try: 35 | message_data = {'type': event_type} 36 | if data is not None: 37 | message_data['data'] = data 38 | await self.websocket.send(Json.encode(message_data)) 39 | if wait is True: 40 | logger.debug(F'已向服务器 [{self.name}] 发送数据 {message_data},正在等待回应……') 41 | response = Json.decode(await self.websocket.receive()) 42 | if response.get('success'): 43 | logger.debug(F'已收到服务器 [{self.name}] 的回应 {response},数据发送成功!') 44 | return response.get('data') 45 | logger.debug(F'向服务器 [{self.name}] 发送数据 {event_type} 失败!') 46 | return None 47 | logger.debug(F'向服务器 [{self.name}] 发送数据 {message_data}') 48 | except (WebSocketClosed, ConnectionError): 49 | self.status = False 50 | logger.warning(F'与服务器 [{self.name}] 的连接已断开!') 51 | return None 52 | 53 | async def send_command(self, command: str): 54 | return await self.send_data('command', command) 55 | 56 | async def send_mcdr_command(self, command: str): 57 | return await self.send_data('mcdr_command', command) 58 | 59 | async def send_player_list(self): 60 | return await self.send_data('player_list') 61 | 62 | async def send_server_occupation(self): 63 | if data := await self.send_data('server_occupation'): 64 | return tuple(round(percent, 2) for percent in data) 65 | 66 | async def send_message(self, message_data: list): 67 | await self.send_data('message', message_data, wait=False) 68 | 69 | 70 | class ServerManager: 71 | servers: dict[str, Server] = {} 72 | 73 | def check_online(self): 74 | return any(server.status for server in self.servers.values()) 75 | 76 | def append_server(self, name: str, websocket: WebSocket): 77 | server = Server(name, websocket) 78 | self.servers[name] = server 79 | return server 80 | 81 | def get_server(self, server_flag: Union[str, int]): 82 | if isinstance(server_flag, int) or server_flag.isdigit(): 83 | index = int(server_flag) 84 | if index > len(data_manager.servers): 85 | return None 86 | server_flag = data_manager.servers[index - 1] 87 | if (server := self.servers.get(server_flag)) and server.status: 88 | return server 89 | 90 | async def disconnect_server(self, name: str): 91 | if server := self.servers.get(name): 92 | await server.disconnect() 93 | 94 | async def execute(self, command: str): 95 | tasks = {} 96 | logger.debug(F'执行命令 [{command}] 到所有已连接的服务器。') 97 | for name, server in self.servers.items(): 98 | if server.status: 99 | tasks[name] = asyncio.create_task(server.send_command(command)) 100 | return {name: await task for name, task in tasks.items()} 101 | 102 | async def execute_mcdr(self, command: str): 103 | tasks = {} 104 | logger.debug(F'执行命令 [{command}] 到所有已连接的服务器。') 105 | for name, server in self.servers.items(): 106 | if server.status and server.type == 'McdReforged': 107 | tasks[name] = asyncio.create_task(server.send_mcdr_command(command)) 108 | return {name: await task for name, task in tasks.items()} 109 | 110 | async def get_player_list(self): 111 | tasks = {} 112 | logger.debug('获取所有已连接服务器的玩家列表。') 113 | for name, server in self.servers.items(): 114 | if server.status: 115 | tasks[name] = asyncio.create_task(server.send_player_list()) 116 | return {name: await task for name, task in tasks.items()} 117 | 118 | async def get_server_occupation(self): 119 | tasks = {} 120 | logger.debug('获取所有已连接服务器的占用率。') 121 | for name, server in self.servers.items(): 122 | if server.status: 123 | tasks[name] = asyncio.create_task(server.send_server_occupation()) 124 | return {name: await task for name, task in tasks.items()} 125 | 126 | async def broadcast(self, source: str, player: str = None, message: str = None, except_server: str = None): 127 | tasks = {} 128 | message_data = [{'color': config.sync_color_source, 'text': F'[{source}] '}] 129 | if player: message_data.append({'color': config.sync_color_player, 'text': F'<{player}> '}) 130 | if message: message_data.append({'color': config.sync_color_message, 'text': message}) 131 | for name, server in self.servers.items(): 132 | if ((except_server is None) or name != except_server) and server.status: 133 | tasks[name] = asyncio.create_task(server.send_message(message_data)) 134 | return {name: await task for name, task in tasks.items()} 135 | 136 | 137 | server_manager = ServerManager() 138 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/Version.py: -------------------------------------------------------------------------------- 1 | from os import mkdir, path 2 | from zipfile import ZipFile 3 | 4 | from nonebot.log import logger 5 | 6 | from ..Network import download, request 7 | 8 | 9 | class VersionManager: 10 | version: str = 'v2.0.6' 11 | latest_version: str = None 12 | 13 | def check_update(self): 14 | if self.latest_version is None: 15 | return False 16 | return self.latest_version != self.version 17 | 18 | async def init(self): 19 | logger.warning('版本检查服务器G了,作者挖坑不填,捞B') 20 | # try: 21 | # if response := await request('http://api.qqbot.bugjump.xyz/version'): 22 | # self.latest_version = response.get('version') 23 | # return None 24 | # logger.warning('尝试获取新版本时出错!') 25 | # except: 26 | # logger.warning('版本检查服务器G了,作者挖坑不填,捞B') 27 | # self.latest_version = self.version # 让代码正常通过 28 | # return None 29 | 30 | async def update_version(self): 31 | logger.info(F'更新版本到 {self.latest_version}……') 32 | if response := await download( 33 | F'https://github.com/Minecraft-QQBot/BotServer/releases/download/v{self.latest_version}/BotServer-v{self.latest_version}.zip'): 34 | with ZipFile(response) as zip_file: 35 | for file in zip_file.namelist(): 36 | if file.startswith('BotServer/') and ('.env' not in file): 37 | file_path = file[10:] 38 | if '.' in file_path: 39 | with open(file_path, 'wb') as target_file: 40 | target_file.write(zip_file.read(file)) 41 | continue 42 | if not path.exists(file_path) and file_path: 43 | mkdir(file_path) 44 | logger.success(F'更新版本到 {self.latest_version} 成功!请重启机器人。') 45 | return None 46 | logger.warning(F'更新版本到 {self.latest_version} 失败,请检查网络稍后再试。') 47 | 48 | 49 | version_manager = VersionManager() 50 | -------------------------------------------------------------------------------- /BotServer/Scripts/Managers/__init__.py: -------------------------------------------------------------------------------- 1 | from .Data import data_manager 2 | from .Environment import environment_manager 3 | from .Lagrange import lagrange_manager 4 | from .Server import server_manager 5 | from .Version import version_manager 6 | from .Resources import resources_manager 7 | -------------------------------------------------------------------------------- /BotServer/Scripts/Network.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from io import BytesIO 3 | 4 | import psutil 5 | from httpx import AsyncClient 6 | from nonebot.log import logger 7 | 8 | from Scripts.Globals import uuid_caches 9 | 10 | client = AsyncClient() 11 | 12 | 13 | async def request(url: str): 14 | try: 15 | response = await client.get(url) 16 | if response.status_code == 200: 17 | return response.json() 18 | logger.warning(F'请求 {url} 失败:错误的状态代码 {response.status_code}') 19 | return None 20 | except Exception as error: 21 | logger.warning(F'请求 {url} 失败:{error}') 22 | 23 | 24 | async def get_player_uuid(name: str): 25 | if name in uuid_caches: 26 | return uuid_caches[name] 27 | uuid = '8667ba71b85a4004af54457a9734eed7' 28 | if response := await request(F'https://api.mojang.com/users/profiles/minecraft/{name}'): 29 | uuid = (response.get('id') or '8667ba71b85a4004af54457a9734eed7') 30 | uuid_caches[name] = uuid 31 | return uuid 32 | 33 | 34 | async def send_bot_status(status: bool): 35 | logger.warning('状态服务G了,跳过') 36 | return True 37 | # mac = None 38 | # addresses = psutil.net_if_addrs() 39 | # for interface_name, interface_address in addresses.items(): 40 | # for address in interface_address: 41 | # if address.family == psutil.AF_LINK: 42 | # mac = address.address 43 | # if mac: break 44 | # bot_id = md5((mac + 'Minecraft_QQBot').encode()) 45 | # data = {'bot_id': bot_id.hexdigest(), 'status': status} 46 | # response = await client.get('http://api.qqbot.bugjump.xyz/status/change', params=data) 47 | # if response.status_code == 200: 48 | # logger.success('发送机器人状态改变信息成功!') 49 | # return True 50 | # logger.warning('无法连接上服务器!发送机器人状态改变信息失败。') 51 | # return False 52 | 53 | 54 | async def download(url: str): 55 | download_bytes = BytesIO() 56 | url = (('https://mirror.ghproxy.com/' + url) if 'github' in url else url) 57 | async with client.stream('GET', url) as stream: 58 | if stream.status_code != 200: 59 | return False 60 | async for chunk in stream.aiter_bytes(): 61 | download_bytes.write(chunk) 62 | download_bytes.seek(0) 63 | return download_bytes 64 | -------------------------------------------------------------------------------- /BotServer/Scripts/Render.py: -------------------------------------------------------------------------------- 1 | from nonebot import require 2 | from nonebot.adapters.onebot.v11 import MessageSegment 3 | 4 | from .Config import config 5 | from .Managers.Resources import resources_manager 6 | 7 | require('nonebot_plugin_htmlrender') 8 | from nonebot_plugin_htmlrender import template_to_pic 9 | 10 | template_path = str(resources_manager / 'Images') 11 | 12 | 13 | async def render_template(template_name: str, size: tuple, **kwargs): 14 | width, height = size 15 | kwargs.setdefault('background', config.image_background) 16 | page = {'viewport': {'width': width, 'height': height}, 'base_url': 'file://' + template_path} 17 | image = await template_to_pic(template_path, template_name, kwargs, pages=page, wait=1) 18 | return MessageSegment.image(image) 19 | -------------------------------------------------------------------------------- /BotServer/Scripts/Servers/Http/Api.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | 3 | from nonebot import get_driver 4 | from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup 5 | from nonebot.log import logger 6 | 7 | from Scripts.Config import config 8 | from Scripts.Managers import server_manager, data_manager 9 | 10 | 11 | async def broadcast(request: Request): 12 | if request.headers.get('token') != config.api_token: 13 | return Response(403, content=dumps({'success': False})) 14 | if message := request.json.get('message'): 15 | if server_flag := request.json.get('server'): 16 | if server := server_manager.get_server(server_flag): 17 | await server.broadcast(message) 18 | return Response(200, content=dumps({'success': True})) 19 | return Response(200, content=dumps({'success': False, 'message': '服务器不存在!'})) 20 | await server_manager.broadcast(message) 21 | return Response(200, content=dumps({'success': True})) 22 | return Response(200, content=dumps({'success': False, 'message': '缺少必要的消息参数!'})) 23 | 24 | 25 | async def get_player_list(request: Request): 26 | if request.headers.get('token') != config.api_token: 27 | return Response(403, content=dumps({'success': False})) 28 | if server_flag := request.url.query.get('server'): 29 | if server := server_manager.get_server(server_flag): 30 | return Response(200, content=dumps({'success': True, 'data': await server.send_player_list()})) 31 | return Response(200, content=dumps({'success': False, 'message': '服务器不存在!'})) 32 | return Response(200, content=dumps({'success': True, 'data': await data_manager.get_player_list()})) 33 | 34 | 35 | async def get_server_occupation(request: Request): 36 | if request.headers.get('token') != config.api_token: 37 | return Response(403, content=dumps({'success': False})) 38 | if server_flag := request.url.query.get('server'): 39 | if server := server_manager.get_server(server_flag): 40 | return Response(200, content=dumps({'success': True, 'data': await server.send_server_occupation()})) 41 | return Response(200, content=dumps({'success': False, 'message': '服务器不存在!'})) 42 | return Response(200, content=dumps({'success': True, 'data': await server_manager.get_server_occupation()})) 43 | 44 | 45 | async def execute_command(request: Request): 46 | if request.headers.get('token') != config.api_token: 47 | return Response(403, content=dumps({'success': False})) 48 | if command := request.json.get('command'): 49 | if server_flag := request.json.get('server'): 50 | if server := server_manager.get_server(server_flag): 51 | return Response(200, content=dumps({'success': True, 'data': await server.send_command(command)})) 52 | return Response(200, content=dumps({'success': False, 'message': '服务器不存在!'})) 53 | return Response(200, content=dumps({'success': True, 'data': await server_manager.execute(command)})) 54 | return Response(200, content=dumps({'success': False, 'message': '缺少必要的命令参数!'})) 55 | 56 | 57 | async def execute_mcdr_command(request: Request): 58 | if request.headers.get('token') != config.api_token: 59 | return Response(403, content=dumps({'success': False})) 60 | if command := request.json.get('command'): 61 | if server_flag := request.json.get('server'): 62 | if server := server_manager.get_server(server_flag): 63 | if server.type in ('McdReforged', 'FakePlayer'): 64 | result = await server.send_mcdr_command(command) 65 | return Response(200, content=dumps({'success': True, 'data': result})) 66 | return Response(200, content=dumps({'success': False, 'message': '此服务器不支持 MCDR 命令!'})) 67 | return Response(200, content=dumps({'success': False, 'message': '服务器不存在!'})) 68 | return Response(200, content=dumps({'success': True, 'data': await server_manager.execute_mcdr(command)})) 69 | return Response(200, content=dumps({'success': False, 'message': '缺少必要的命令参数!'})) 70 | 71 | 72 | def setup_api_http_server(): 73 | if not config.api_enabled: 74 | return None 75 | if isinstance((driver := get_driver()), ASGIMixin): 76 | api_servers = ( 77 | HTTPServerSetup(URL('/api/get_player_list'), 'GET', 'api.player_list', get_player_list), 78 | HTTPServerSetup(URL('/api/get_server_occupation'), 'GET', 'api.server_occupation', get_server_occupation), 79 | HTTPServerSetup(URL('/api/broadcast'), 'POST', 'api.broadcast', broadcast), 80 | HTTPServerSetup(URL('/api/execute_command'), 'POST', 'api.command', execute_command), 81 | HTTPServerSetup(URL('/api/execute_mcdr_command'), 'POST', 'api.mcdr_command', execute_mcdr_command), 82 | ) 83 | for api_server in api_servers: 84 | driver.setup_http_server(api_server) 85 | logger.success('载入 Api 服务器成功!') 86 | return True 87 | logger.error('当前驱动不支持 Http 服务器!载入 Api 服务器失败!请检查驱动是否正确。') 88 | -------------------------------------------------------------------------------- /BotServer/Scripts/Servers/Http/WebUi.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from pathlib import Path 3 | from fastapi.staticfiles import StaticFiles 4 | 5 | from nonebot import get_driver, get_app 6 | from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup 7 | from nonebot.log import logger 8 | 9 | from Scripts.Utils import restart 10 | from Scripts.Managers import environment_manager, resources_manager, data_manager 11 | 12 | 13 | async def api(request: Request): 14 | if request.headers.get('token') != data_manager.webui_token: 15 | return Response(403, content=dumps({'success': False})) 16 | if request.method == 'POST': 17 | environment_manager.update(request.json) 18 | message = '机器人即将自动重启!' if restart() else '当前系统不支持自动重启,请手动重启机器人!' 19 | return Response(200, content=dumps({'success': True, 'message': message})) 20 | response = {'success': True, 'data': environment_manager.environment} 21 | return Response(200, content=dumps(response)) 22 | 23 | 24 | async def page(request: Request): 25 | page_path = Path('Resources/WebUi/Index.html') 26 | return Response(200, content=page_path.read_text('Utf-8')) 27 | 28 | 29 | def setup_webui_http_server(): 30 | if isinstance((driver := get_driver()), ASGIMixin): 31 | application = get_app() 32 | static_file_path = (resources_manager.path / 'WebUi/Assets') 33 | application.mount('/assets', StaticFiles(directory=static_file_path), name='static_assets') 34 | 35 | server = HTTPServerSetup(URL('/webui'), 'GET', 'page', page) 36 | driver.setup_http_server(server) 37 | server = HTTPServerSetup(URL('/webui/api'), 'GET', 'get_api', api) 38 | driver.setup_http_server(server) 39 | server = HTTPServerSetup(URL('/webui/api'), 'POST', 'post_api', api) 40 | driver.setup_http_server(server) 41 | logger.success('载入 WebUi 成功!请保管好下方的链接,以供使用。') 42 | color_logger = logger.opt(colors=True) 43 | color_logger.info( 44 | F'WebUi http://{driver.config.host}:{driver.config.port}' 45 | F'/webui?token={data_manager.webui_token}' 46 | ) 47 | return True 48 | logger.error('当前驱动不支持 Http 服务器!载入 WebUi 失败,请检查驱动是否正确。') 49 | -------------------------------------------------------------------------------- /BotServer/Scripts/Servers/Http/__init__.py: -------------------------------------------------------------------------------- 1 | from .Api import setup_api_http_server 2 | from .WebUi import setup_webui_http_server 3 | -------------------------------------------------------------------------------- /BotServer/Scripts/Servers/Websocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from nonebot import get_driver, get_bot 4 | from nonebot.exception import NetworkError, ActionFailed 5 | from nonebot.drivers import WebSocketServerSetup, WebSocket, ASGIMixin, URL 6 | from nonebot.exception import WebSocketClosed 7 | from nonebot.log import logger 8 | 9 | from .. import Globals 10 | from ..Config import config 11 | from ..Managers import server_manager, data_manager 12 | from ..Utils import Json, check_message 13 | 14 | 15 | async def verify(websocket: WebSocket): 16 | logger.info('检测到 WebSocket 链接,正在验证身份……') 17 | if info := websocket.request.headers.get('info'): 18 | info = Json.decode(info) 19 | name = info.get('name') 20 | if info.get('token') != config.token or (not name): 21 | await websocket.close(1008, 'Error token or name.') 22 | logger.warning('身份验证失败!请检查插件配置文件是否正确。') 23 | return None 24 | logger.success(F'身份验证成功,服务器 [{name}] 已连接到!连接已建立。') 25 | await websocket.accept() 26 | return name 27 | 28 | 29 | async def handle_websocket_minecraft(websocket: WebSocket): 30 | if name := await verify(websocket): 31 | time_count = 0 32 | data_manager.append_server(name) 33 | server = server_manager.append_server(name, websocket) 34 | Globals.cpu_occupation[name] = [] 35 | Globals.ram_occupation[name] = [] 36 | while True: 37 | await asyncio.sleep(30) 38 | if websocket.closed: break 39 | if server.type != 'FakePlayer': 40 | time_count += 1 41 | if time_count <= config.server_memory_update_interval: 42 | continue 43 | logger.debug(F'正在尝试获取服务器 [{name}] 的占用数据!') 44 | if data := await server.send_server_occupation(): 45 | time_count = 0 46 | cpu, ram = data 47 | Globals.cpu_occupation[name].append(cpu) 48 | Globals.ram_occupation[name].append(ram) 49 | if len(Globals.cpu_occupation[name]) > config.server_memory_max_cache: 50 | Globals.cpu_occupation[name].pop(0) 51 | Globals.ram_occupation[name].pop(0) 52 | Globals.cpu_occupation.pop(name, None) 53 | Globals.ram_occupation.pop(name, None) 54 | logger.info(F'检测到连接与 [{name}] 已断开!移除此服务器内存数据。') 55 | 56 | 57 | async def handle_websocket_bot(websocket: WebSocket): 58 | if name := await verify(websocket): 59 | try: 60 | while True: 61 | response = None 62 | receive_message = Json.decode(await websocket.receive()) 63 | if receive_message is None: 64 | continue 65 | data = receive_message.get('data') 66 | event_type = receive_message.get('type') 67 | if event_type == 'message': 68 | response = await message(name, data) 69 | elif event_type == 'server_startup': 70 | response = await server_startup(name, data) 71 | elif event_type == 'server_shutdown': 72 | response = await server_shutdown(name, data) 73 | elif event_type == 'player_death': 74 | response = await player_death(name, data) 75 | elif event_type == 'player_left': 76 | response = await player_left(name, data) 77 | elif event_type == 'player_joined': 78 | response = await player_joined(name, data) 79 | elif event_type == 'player_chat': 80 | # 若是聊天信息,则不等待回应。 81 | await player_chat(name, data) 82 | continue 83 | if response is not None: 84 | logger.debug(F'对来自 [{name}] 的数据 {receive_message}') 85 | if response is True: 86 | await websocket.send(Json.encode({'success': True})) 87 | continue 88 | await websocket.send(Json.encode({'success': True, 'data': response})) 89 | continue 90 | logger.warning(F'收到来自 [{name}] 无法解析的数据 {receive_message}') 91 | await websocket.send(Json.encode({'success': False})) 92 | except (ConnectionError, WebSocketClosed): 93 | logger.info('WebSocket 连接已关闭!') 94 | 95 | 96 | async def send_message(sent_message: str): 97 | try: 98 | bot = get_bot() 99 | for group in config.message_groups: 100 | await bot.send_group_msg(group_id=group, message=sent_message) 101 | except (NetworkError, ActionFailed, ValueError): 102 | return False 103 | return True 104 | 105 | 106 | async def message(name: str, group_message: str): 107 | if group_message: 108 | logger.debug(F'发送消息 {group_message} 到消息群!') 109 | if check_message(group_message): 110 | logger.warning(F'检测到消息 {group_message} 包含敏感词,已丢弃!') 111 | await send_message('检测到消息包含敏感词,已丢弃!详情请看控制台。') 112 | return None 113 | if await send_message(group_message): 114 | return True 115 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 116 | return None 117 | 118 | 119 | async def server_startup(name: str, data: dict): 120 | logger.info('收到服务器开启数据!尝试连接到服务器……') 121 | data_manager.append_server(name) 122 | if config.sync_message_between_servers: 123 | await server_manager.broadcast(name, message='服务器已开启!', except_server=name) 124 | if config.broadcast_server: 125 | if await send_message(F'服务器 [{name}] 已开启,喵~'): 126 | return config.sync_all_game_message 127 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 128 | return None 129 | return config.sync_all_game_message 130 | 131 | 132 | async def server_shutdown(name: str, data: dict): 133 | logger.info('收到服务器关闭信息!正在断开连接……') 134 | await server_manager.disconnect_server(name) 135 | if config.sync_message_between_servers: 136 | await server_manager.broadcast(name, message='服务器已关闭!', except_server=name) 137 | if config.broadcast_server: 138 | if await send_message(F'服务器 [{name}] 已关闭,呜……'): 139 | return True 140 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 141 | return None 142 | return True 143 | 144 | 145 | async def player_death(name: str, data: list): 146 | player, death_message = data 147 | logger.debug(F'收到玩家死亡 {death_message} 消息!') 148 | if (not config.bot_prefix) or (not player.upper().startswith(config.bot_prefix)): 149 | broadcast_message = F'玩家 {player} 死亡了,呜……' 150 | if config.sync_message_between_servers: 151 | await server_manager.broadcast(name, message=broadcast_message, except_server=name) 152 | if config.broadcast_player: 153 | if await send_message(broadcast_message): 154 | return True 155 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 156 | return False 157 | return True 158 | 159 | 160 | async def player_joined(name: str, player: str): 161 | logger.info('收到玩家加入服务器通知!') 162 | server_message = F'玩家 {player} 加入了游戏。' 163 | group_message = F'玩家 {player} 加入了 [{name}] 服务器,喵~' 164 | if config.bot_prefix and player.upper().startswith(config.bot_prefix): 165 | group_message = F'机器人 {player} 加入了 [{name}] 服务器。' 166 | server_message = F'机器人 {player} 加入了游戏。' 167 | if config.sync_message_between_servers: 168 | await server_manager.broadcast(name, message=server_message, except_server=name) 169 | if config.broadcast_player: 170 | if await send_message(group_message): 171 | return True 172 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 173 | return None 174 | return True 175 | 176 | 177 | async def player_left(name: str, player: str): 178 | logger.info('收到玩家离开服务器通知!') 179 | server_message = F'玩家 {player} 离开了游戏。' 180 | group_message = F'玩家 {player} 离开了 [{name}] 服务器,呜……' 181 | if config.bot_prefix and player.upper().startswith(config.bot_prefix): 182 | server_message = F'机器人 {player} 离开了游戏。' 183 | group_message = F'机器人 {player} 离开了 [{name}] 服务器。' 184 | if config.sync_message_between_servers: 185 | await server_manager.broadcast(name, message=server_message, except_server=name) 186 | if config.broadcast_player: 187 | if await send_message(group_message): 188 | return True 189 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 190 | return None 191 | return True 192 | 193 | 194 | async def player_chat(name: str, data: list): 195 | player, chat_message = data 196 | logger.debug(F'收到玩家 {player} 在服务器 [{name}] 发送消息!') 197 | if config.sync_all_game_message: 198 | if check_message(chat_message): 199 | logger.warning(F'检测到消息 {chat_message} 包含敏感词,已丢弃!') 200 | await send_message(F'检测到玩家 {player} 发送的消息包含敏感词,已丢弃!详情请看控制台。') 201 | return None 202 | if not (await send_message(F'[{name}] <{player}> {chat_message}')): 203 | logger.warning('发送消息失败!请检查机器人状态是否正确和群号是否填写正确。') 204 | if config.sync_message_between_servers: 205 | await server_manager.broadcast(name, player, chat_message, except_server=name) 206 | 207 | 208 | def setup_websocket_server(): 209 | if isinstance((driver := get_driver()), ASGIMixin): 210 | server = WebSocketServerSetup(URL('/websocket/bot'), 'bot', handle_websocket_bot) 211 | driver.setup_websocket_server(server) 212 | server = WebSocketServerSetup(URL('/websocket/minecraft'), 'minecraft', handle_websocket_minecraft) 213 | driver.setup_websocket_server(server) 214 | logger.success('装载 WebSocket 服务器成功!') 215 | return None 216 | logger.error('装载 WebSocket 服务器失败!请检查驱动是否正确。') 217 | exit(1) 218 | -------------------------------------------------------------------------------- /BotServer/Scripts/Servers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Minecraft-QQBot/BotServer/b0fb6c74e830e928525310a64ef653c47f454d67/BotServer/Scripts/Servers/__init__.py -------------------------------------------------------------------------------- /BotServer/Scripts/Utils.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import inspect 3 | import os 4 | import re 5 | from base64 import b64decode, b64encode 6 | from collections.abc import Iterable 7 | from json import dumps, loads 8 | from pathlib import Path 9 | from threading import Timer 10 | 11 | from nonebot import get_bot 12 | from nonebot.adapters.onebot.v11 import Event, Message, MessageEvent 13 | from nonebot.exception import ActionFailed, NetworkError 14 | from nonebot.log import logger 15 | from uvicorn.server import Server 16 | 17 | from .Config import config 18 | 19 | regex = re.compile(R'[A-Z0-9_]+|\.[A-Z0-9_]+', re.IGNORECASE) 20 | 21 | 22 | def turn_message(iterator: Iterable) -> Message: 23 | lines = tuple(iterator) 24 | return Message('\n'.join(lines)) 25 | 26 | 27 | def check_player(player: str): 28 | if len(player) > 16: 29 | return False 30 | return get_player_name(player) == player 31 | 32 | 33 | def check_message(message: str): 34 | # 返回是否含有违禁词 35 | return any(word in message for word in config.sync_sensitive_words) 36 | 37 | 38 | def get_args(args: Message): 39 | result = [] 40 | for segment in args: 41 | if segment.type == 'text': 42 | for arg in segment.data['text'].split(' '): 43 | arg and result.append(arg) 44 | elif segment.type == 'at': 45 | result.append(str(segment.data['qq'])) 46 | logger.debug(F'从 {args} 中提取参数 {result} 完毕。') 47 | return result 48 | 49 | 50 | def get_player_name(name): 51 | if result := regex.search(name): 52 | return result.group() 53 | 54 | 55 | def get_permission(event: MessageEvent): 56 | return (str(event.user_id) in config.superusers) or ( 57 | config.admin_superusers and event.sender.role in ('admin', 'owner') 58 | ) 59 | 60 | 61 | def restart(): 62 | frames = inspect.getouterframes(inspect.currentframe()) 63 | servers = (info.frame.f_locals.get('server') for info in frames[::-1]) 64 | server = next(server for server in servers if isinstance(server, Server)) 65 | 66 | def core(): 67 | file = Path('Bot.py').absolute() 68 | os.system(f'start python {file}') 69 | server.should_exit = True 70 | 71 | if os.name == 'nt': 72 | timer = Timer(2, core) 73 | timer.start() 74 | return True 75 | return False 76 | 77 | 78 | async def get_user_name(group: int, user: int): 79 | try: 80 | bot = get_bot() 81 | response = await bot.get_group_member_info(group_id=group, user_id=user) 82 | except (NetworkError, ActionFailed, ValueError): 83 | return None 84 | return response.get('card') or response.get('nickname') 85 | 86 | 87 | class Json: 88 | @staticmethod 89 | def encode(data: dict): 90 | # 编码 91 | string = dumps(data, ensure_ascii=False) 92 | string = b64encode(string.encode('Utf-8')) 93 | return string.decode('Utf-8') 94 | 95 | @staticmethod 96 | def decode(string: str): 97 | try: 98 | string = b64decode(string.encode('Utf-8')) 99 | except binascii.Error: 100 | logger.warning(f'无法解码字符串 {string}') 101 | return None 102 | return loads(string.decode('Utf-8')) 103 | 104 | 105 | class Rules: 106 | @staticmethod 107 | def message_rule(event: Event): 108 | if hasattr(event, 'group_id'): 109 | return event.group_id in config.message_groups 110 | return True 111 | 112 | @staticmethod 113 | def command_rule(event: Event): 114 | if hasattr(event, 'group_id'): 115 | return event.group_id in config.command_groups 116 | return True 117 | -------------------------------------------------------------------------------- /BotServer/Scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Minecraft-QQBot/BotServer/b0fb6c74e830e928525310a64ef653c47f454d67/BotServer/Scripts/__init__.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft_QQBot 2 | 3 | ### [**文档**](https://mcbot.ytb.icu/) 4 | 5 | **一款基于 Nonebot2 用多种方式与 Minecraft 交互的 Python QQ 机器人**。功能丰富,使用简单,性能高强且可以自行配置,仅需简单配置即可使用。目前已实现的功能有: 6 | 7 | - 多服互联,群服互通。 8 | - 在不同服务器之间转发消息。 9 | - 可在游戏内看到 QQ 群的消息。 10 | - 可使用指令在游戏内向 QQ 群发送消息。 11 | - 可播报服务器开启、关闭,玩家进入离开服务器以及死亡消息。 12 | - 使用 WebUi 简单配置。 13 | - 戳一戳机器人发送一言卡片。 14 | - 可自行配置指令的开启或关闭。 15 | - 可自行配置接入 AI 功能。 16 | - 对 QQ 群指令相应。目前已实现的指令有: 17 | - `luck` 查看今日幸运指数。 18 | - `mcdr` 在指定的服务器上执行 MCDR 指令。 19 | - `list` 查询每个服务器的玩家在线情况。 20 | - `help` 查看帮助信息。 21 | - `server` 查看当前在线的服务器并显示对应编号,也可用于查看服务器占用。 22 | - `bound` 有关绑定白名单的指令。 23 | - `command` 发送指令到服务器。 24 | 25 | 更多功能还在探索中…… 26 | 27 | > [!WARNING] 28 | > 本项目采用 GPL3 许可证,请勿商用!如若修改请务必开源并且注明出处。 29 | 30 | ## 安装插件 31 | 32 | 本机器人可通过各种方式与 Minecraft 服务器进行交互,包括: 33 | 34 | - [Fabric](https://www.github.com/Minecraft-QQBot/Mode.Fabric) 模组(开发中) 35 | - [Spigot](https://www.github.com/Minecraft-QQBot/Plugin.Spigot) 插件 36 | - [McdReforged](https://www.github.com/Minecraft-QQBot/Plugin.McdReforged) 插件 37 | - [FakePlayer](https://www.github.com/Minecraft-QQBot/Platform.FakePlayer) 工具 38 | 39 | 请前往你需要插件的仓库按照说明进行安装。请注意,不同的插件所提供的功能可能是不一样的,您可根据需求选择安装。 40 | 41 | 如你有能力开发其他的对接插件,欢迎联系并加入我们! 42 | 43 | ## 安装教程 44 | 45 | 请参考 [快速开始](https://qqbot.bugjump.xyz/%E6%96%87%E6%A1%A3/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B.html) 进行安装。 46 | 47 | ## 鸣谢 48 | 49 | 感谢 [Lagrange](https://lagrangedev.github.io/Lagrange.Doc/) 提供了稳定的 QQ 协议端。 50 | 51 | 感谢以下人员为此机器人开发提供帮助,在此特别鸣谢: 52 | 53 | - [Msg_Lbo](https://github.com/Msg-Lbo) 提供网站服务器以及域名,贡献 WebUi 代码。 54 | - [meng877](https://github.com/meng877) 提出意见,贡献部分代码。 55 | - [Decent_Kook](https://github.com/AISophon) 提供测试环境,提出意见,帮忙宣传。 56 | - [creepebucket](https://github.com/creepebucket) 提供测试环境。 57 | 58 | > [!TIP] 59 | > 若遇到问题,或有更好的想法,可以加入 QQ 群 [`962802248`](https://qm.qq.com/q/B3kmvJl2xO) 或者提交 Issues 60 | > 向作者反馈。若你有能力,欢迎为本项目提供代码贡献! 61 | 62 | ## 友链 63 | 64 | - TQM 服务器 65 | - [LemonFate 服务器](https://www.lemonfate.cn/) 66 | - [RedstoneDaily 红石日报](https://www.redstonedaily.com/) 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Minecraft_QQBot" 3 | version = "2.0.2" 4 | description = "一款与 Minecraft 互通的 Nonebot2 机器人。" 5 | readme = "README.md" 6 | requires-python = ">=3.8, <4.0" 7 | 8 | [tool.nonebot] 9 | adapters = [ 10 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" } 11 | ] 12 | plugins = [] 13 | plugin_dirs = ["BotServer/Plugins"] 14 | builtin_plugins = [] 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nonebot2[fastapi]>=2.3.1 2 | nonebot-adapter-onebot>=2.4.3 3 | matplotlib>=3.9.0 4 | pydantic>=2.7.4 5 | fastapi~=0.111.0 6 | uvicorn~=0.30.1 7 | httpx~=0.27.0 8 | psutil~=6.0.0 --------------------------------------------------------------------------------