nonebot-plugin-palworld-sync 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_palworld_sync-0.1.0/PKG-INFO +92 -0
- nonebot_plugin_palworld_sync-0.1.0/README.md +69 -0
- nonebot_plugin_palworld_sync-0.1.0/nonebot_plugin_palworld_sync/__init__.py +327 -0
- nonebot_plugin_palworld_sync-0.1.0/nonebot_plugin_palworld_sync/api.py +79 -0
- nonebot_plugin_palworld_sync-0.1.0/nonebot_plugin_palworld_sync/config.py +9 -0
- nonebot_plugin_palworld_sync-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonebot-plugin-palworld-sync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 基于NoneBot2的幻兽帕鲁 (Palworld) 服务器 REST API 插件
|
|
5
|
+
Keywords: nonebot,nonebot2,palworld,pal
|
|
6
|
+
Author: yuexps@qq.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: httpx (>=0.20.0,<1.0.0)
|
|
17
|
+
Requires-Dist: nonebot-adapter-onebot (>=2.4.0,<3.0.0)
|
|
18
|
+
Requires-Dist: nonebot2 (>=2.2.0,<3.0.0)
|
|
19
|
+
Project-URL: Homepage, https://github.com/yuexps/nonebot-plugin-palworld-sync
|
|
20
|
+
Project-URL: Repository, https://github.com/yuexps/nonebot-plugin-palworld-sync
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://adapter-onebot.netlify.app/"><img src="https://img.shields.io/badge/nonebot2-plugin-red.svg" alt="nonebot2"></a>
|
|
25
|
+
<a href="https://pypi.org/project/nonebot-plugin-palworld-sync"><img src="https://img.shields.io/pypi/v/nonebot-plugin-palworld-sync.svg" alt="pypi"></a>
|
|
26
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
# nonebot-plugin-palworld-sync
|
|
30
|
+
|
|
31
|
+
基于 Nonebot2 的幻兽帕鲁 (Palworld) 服务器 REST API 插件。
|
|
32
|
+
|
|
33
|
+
## 介绍
|
|
34
|
+
|
|
35
|
+
通过幻兽帕鲁官方 REST API 实现服务器状态监控与群内管理。支持群消息和游戏内互通、玩家踢出/封禁/解封、在线人数查询以及关机与存档等操作。
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
推荐使用 `nb-cli` 安装:
|
|
40
|
+
```bash
|
|
41
|
+
nb plugin install nonebot-plugin-palworld-sync --upgrade
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary>或使用包管理器</summary>
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install nonebot-plugin-palworld-sync --upgrade
|
|
49
|
+
```
|
|
50
|
+
并在 `pyproject.toml` 或 `bot.py` 中加载插件:
|
|
51
|
+
```toml
|
|
52
|
+
plugins = ["nonebot_plugin_palworld_sync"]
|
|
53
|
+
```
|
|
54
|
+
</details>
|
|
55
|
+
|
|
56
|
+
## 1. 插件配置
|
|
57
|
+
|
|
58
|
+
### 幻兽帕鲁服务端配置 (PalWorldSettings.ini)
|
|
59
|
+
```ini
|
|
60
|
+
RESTAPIEnabled=True,RESTAPIPort=8212,AdminPassword="您的管理员密码"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Nonebot 环境变量配置 (.env)
|
|
64
|
+
```env
|
|
65
|
+
PALWORLD_API_URL="http://<服务器IP>:8212"
|
|
66
|
+
PALWORLD_API_PASSWORD="您的管理员密码"
|
|
67
|
+
PALWORLD_QQ_GROUPS=["123456", "789012"] # 必填:本插件生效的 QQ 群号列表(JSON 数组格式),非列表内的群不响应任何指令
|
|
68
|
+
PALWORLD_FORWARD_GROUP_MSG=True # 可选:设置为 True 即可开启 QQ 群消息互通转发至游戏内,默认 False
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. 指令一览
|
|
74
|
+
|
|
75
|
+
### 公共指令
|
|
76
|
+
|
|
77
|
+
| 指令 | 参数要求 | 功能描述 |
|
|
78
|
+
| :--- | :--- | :--- |
|
|
79
|
+
| /pw status | 无 | 查看服务器状态 (版本、FPS、在线人数、核心设置等) |
|
|
80
|
+
| /pw players [目标] | [目标]: 选填 (名字/PlayerID/SteamID) | 查看在线玩家简表,若指定目标则返回其详细状态 |
|
|
81
|
+
|
|
82
|
+
### 管理员指令 (限超级用户、群主、群管理员)
|
|
83
|
+
|
|
84
|
+
| 指令 | 参数要求 | 功能描述 |
|
|
85
|
+
| :--- | :--- | :--- |
|
|
86
|
+
| /pw say <msg> | <msg>: 消息内容 | 发送全局广播消息 |
|
|
87
|
+
| /pw kick <target> [reason] | <target>: 名字/PlayerID/SteamID<br>[reason]: 可空 | 踢出指定玩家 (支持在线玩家名字模糊匹配) |
|
|
88
|
+
| /pw ban <target> [reason] | <target>: 名字/PlayerID/SteamID<br>[reason]: 可空 | 封禁指定玩家 (支持在线玩家名字模糊匹配) |
|
|
89
|
+
| /pw unban <steam_id> | <steam_id>: SteamID | 解封指定的玩家 |
|
|
90
|
+
| /pw save | 无 | 保存当前游戏世界的存盘 |
|
|
91
|
+
| /pw shutdown [秒/force] [msg] | [秒/force]: 秒数或 force 强制关闭<br>[msg]: 可空 | 关闭服务器 (优雅关机或强关合并) |
|
|
92
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://adapter-onebot.netlify.app/"><img src="https://img.shields.io/badge/nonebot2-plugin-red.svg" alt="nonebot2"></a>
|
|
3
|
+
<a href="https://pypi.org/project/nonebot-plugin-palworld-sync"><img src="https://img.shields.io/pypi/v/nonebot-plugin-palworld-sync.svg" alt="pypi"></a>
|
|
4
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
# nonebot-plugin-palworld-sync
|
|
8
|
+
|
|
9
|
+
基于 Nonebot2 的幻兽帕鲁 (Palworld) 服务器 REST API 插件。
|
|
10
|
+
|
|
11
|
+
## 介绍
|
|
12
|
+
|
|
13
|
+
通过幻兽帕鲁官方 REST API 实现服务器状态监控与群内管理。支持群消息和游戏内互通、玩家踢出/封禁/解封、在线人数查询以及关机与存档等操作。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
推荐使用 `nb-cli` 安装:
|
|
18
|
+
```bash
|
|
19
|
+
nb plugin install nonebot-plugin-palworld-sync --upgrade
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary>或使用包管理器</summary>
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nonebot-plugin-palworld-sync --upgrade
|
|
27
|
+
```
|
|
28
|
+
并在 `pyproject.toml` 或 `bot.py` 中加载插件:
|
|
29
|
+
```toml
|
|
30
|
+
plugins = ["nonebot_plugin_palworld_sync"]
|
|
31
|
+
```
|
|
32
|
+
</details>
|
|
33
|
+
|
|
34
|
+
## 1. 插件配置
|
|
35
|
+
|
|
36
|
+
### 幻兽帕鲁服务端配置 (PalWorldSettings.ini)
|
|
37
|
+
```ini
|
|
38
|
+
RESTAPIEnabled=True,RESTAPIPort=8212,AdminPassword="您的管理员密码"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Nonebot 环境变量配置 (.env)
|
|
42
|
+
```env
|
|
43
|
+
PALWORLD_API_URL="http://<服务器IP>:8212"
|
|
44
|
+
PALWORLD_API_PASSWORD="您的管理员密码"
|
|
45
|
+
PALWORLD_QQ_GROUPS=["123456", "789012"] # 必填:本插件生效的 QQ 群号列表(JSON 数组格式),非列表内的群不响应任何指令
|
|
46
|
+
PALWORLD_FORWARD_GROUP_MSG=True # 可选:设置为 True 即可开启 QQ 群消息互通转发至游戏内,默认 False
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. 指令一览
|
|
52
|
+
|
|
53
|
+
### 公共指令
|
|
54
|
+
|
|
55
|
+
| 指令 | 参数要求 | 功能描述 |
|
|
56
|
+
| :--- | :--- | :--- |
|
|
57
|
+
| /pw status | 无 | 查看服务器状态 (版本、FPS、在线人数、核心设置等) |
|
|
58
|
+
| /pw players [目标] | [目标]: 选填 (名字/PlayerID/SteamID) | 查看在线玩家简表,若指定目标则返回其详细状态 |
|
|
59
|
+
|
|
60
|
+
### 管理员指令 (限超级用户、群主、群管理员)
|
|
61
|
+
|
|
62
|
+
| 指令 | 参数要求 | 功能描述 |
|
|
63
|
+
| :--- | :--- | :--- |
|
|
64
|
+
| /pw say <msg> | <msg>: 消息内容 | 发送全局广播消息 |
|
|
65
|
+
| /pw kick <target> [reason] | <target>: 名字/PlayerID/SteamID<br>[reason]: 可空 | 踢出指定玩家 (支持在线玩家名字模糊匹配) |
|
|
66
|
+
| /pw ban <target> [reason] | <target>: 名字/PlayerID/SteamID<br>[reason]: 可空 | 封禁指定玩家 (支持在线玩家名字模糊匹配) |
|
|
67
|
+
| /pw unban <steam_id> | <steam_id>: SteamID | 解封指定的玩家 |
|
|
68
|
+
| /pw save | 无 | 保存当前游戏世界的存盘 |
|
|
69
|
+
| /pw shutdown [秒/force] [msg] | [秒/force]: 秒数或 force 强制关闭<br>[msg]: 可空 | 关闭服务器 (优雅关机或强关合并) |
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import nonebot
|
|
3
|
+
from nonebot import on_command, on_message
|
|
4
|
+
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent
|
|
5
|
+
from nonebot.params import CommandArg
|
|
6
|
+
from nonebot.permission import SUPERUSER
|
|
7
|
+
from nonebot.plugin import PluginMetadata
|
|
8
|
+
|
|
9
|
+
from .api import PalWorldAPI, PalWorldAPIError
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
__plugin_meta__ = PluginMetadata(
|
|
13
|
+
name="Palworld",
|
|
14
|
+
description="幻兽帕鲁服务器 REST API 插件",
|
|
15
|
+
usage=(
|
|
16
|
+
"帕鲁服务器命令:\n"
|
|
17
|
+
"/pw status - 查看服务器状态\n"
|
|
18
|
+
"/pw players [目标] - 查看在线玩家/详细信息\n"
|
|
19
|
+
"/pw save - 保存世界存盘\n"
|
|
20
|
+
"/pw say <消息> - 广播全局消息\n"
|
|
21
|
+
"/pw shutdown [秒/force] [消息] - 关闭服务器\n"
|
|
22
|
+
"/pw kick <目标> [原因] - 踢出玩家\n"
|
|
23
|
+
"/pw ban <目标> [原因] - 封禁玩家\n"
|
|
24
|
+
"/pw unban <SteamID> - 解封玩家"
|
|
25
|
+
),
|
|
26
|
+
type="application",
|
|
27
|
+
homepage="https://github.com/yuexps/nonebot-plugin-palworld-sync",
|
|
28
|
+
supported_adapters={"~onebot.v11"},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# 加载配置
|
|
32
|
+
try:
|
|
33
|
+
from nonebot import get_plugin_config
|
|
34
|
+
pal_config = get_plugin_config(Config)
|
|
35
|
+
except ImportError:
|
|
36
|
+
pal_config = Config.parse_obj(nonebot.get_driver().config)
|
|
37
|
+
|
|
38
|
+
# 初始化 API 客户端
|
|
39
|
+
api = PalWorldAPI(pal_config.palworld_api_url, pal_config.palworld_api_password)
|
|
40
|
+
|
|
41
|
+
pal_cmd = on_command("pw", priority=5, block=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def check_admin(bot: Bot, event: MessageEvent) -> bool:
|
|
45
|
+
"""检查用户是否具有管理员或超级用户权限"""
|
|
46
|
+
if await SUPERUSER(bot, event):
|
|
47
|
+
return True
|
|
48
|
+
if isinstance(event, GroupMessageEvent):
|
|
49
|
+
return event.sender.role in ("owner", "admin")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_uptime(seconds: int) -> str:
|
|
54
|
+
"""格式化运行时间为易读的中文字符串"""
|
|
55
|
+
days, remain = divmod(seconds, 86400)
|
|
56
|
+
hours, remain = divmod(remain, 3600)
|
|
57
|
+
minutes, secs = divmod(remain, 60)
|
|
58
|
+
|
|
59
|
+
parts = []
|
|
60
|
+
if days > 0:
|
|
61
|
+
parts.append(f"{days}天")
|
|
62
|
+
if hours > 0:
|
|
63
|
+
parts.append(f"{hours}小时")
|
|
64
|
+
if minutes > 0:
|
|
65
|
+
parts.append(f"{minutes}分钟")
|
|
66
|
+
if secs > 0 or not parts:
|
|
67
|
+
parts.append(f"{secs}秒")
|
|
68
|
+
return "".join(parts)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pal_cmd.handle()
|
|
72
|
+
async def handle_pal(bot: Bot, event: MessageEvent, command_arg: Message = CommandArg()):
|
|
73
|
+
# 仅在配置的帕鲁群内响应命令
|
|
74
|
+
if not isinstance(event, GroupMessageEvent) or str(event.group_id) not in pal_config.palworld_qq_groups:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
args_str = command_arg.extract_plain_text().strip()
|
|
78
|
+
if not args_str:
|
|
79
|
+
await pal_cmd.finish(__plugin_meta__.usage)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
args = shlex.split(args_str)
|
|
83
|
+
except ValueError as e:
|
|
84
|
+
await pal_cmd.finish(f"参数解析错误:{str(e)}(请检查双引号是否闭合)")
|
|
85
|
+
|
|
86
|
+
action = args[0].lower()
|
|
87
|
+
|
|
88
|
+
valid_actions = {
|
|
89
|
+
"status",
|
|
90
|
+
"players",
|
|
91
|
+
"say",
|
|
92
|
+
"kick",
|
|
93
|
+
"ban",
|
|
94
|
+
"unban",
|
|
95
|
+
"save",
|
|
96
|
+
"shutdown",
|
|
97
|
+
}
|
|
98
|
+
if action not in valid_actions:
|
|
99
|
+
await pal_cmd.finish(f"未知子命令:{action}\n{__plugin_meta__.usage}")
|
|
100
|
+
|
|
101
|
+
# 权限校验
|
|
102
|
+
admin_actions = {
|
|
103
|
+
"say",
|
|
104
|
+
"kick",
|
|
105
|
+
"ban",
|
|
106
|
+
"unban",
|
|
107
|
+
"save",
|
|
108
|
+
"shutdown",
|
|
109
|
+
}
|
|
110
|
+
if action in admin_actions:
|
|
111
|
+
if not await check_admin(bot, event):
|
|
112
|
+
await pal_cmd.finish("权限不足:仅超级用户、群主或管理员有权执行此操作。")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
if action == "status":
|
|
116
|
+
import asyncio
|
|
117
|
+
res = await asyncio.gather(
|
|
118
|
+
api.get_info(),
|
|
119
|
+
api.get_metrics(),
|
|
120
|
+
api.get_settings(),
|
|
121
|
+
return_exceptions=True
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
info = res[0] if not isinstance(res[0], Exception) else {}
|
|
125
|
+
metrics = res[1] if not isinstance(res[1], Exception) else {}
|
|
126
|
+
settings = res[2] if not isinstance(res[2], Exception) else {}
|
|
127
|
+
|
|
128
|
+
if isinstance(res[0], Exception) and isinstance(res[1], Exception):
|
|
129
|
+
raise PalWorldAPIError("获取服务器状态失败,请确认 API 服务在线。")
|
|
130
|
+
|
|
131
|
+
version = info.get("version", "Unknown")
|
|
132
|
+
s_name = info.get("servername", "Unknown")
|
|
133
|
+
guid = info.get("worldguid", "N/A")
|
|
134
|
+
|
|
135
|
+
fps = metrics.get("serverfps", "N/A")
|
|
136
|
+
frame_time = metrics.get("serverframetime", 0.0)
|
|
137
|
+
players_num = metrics.get("currentplayernum", 0)
|
|
138
|
+
max_players = metrics.get("maxplayernum", 0)
|
|
139
|
+
uptime = metrics.get("uptime", 0)
|
|
140
|
+
days = metrics.get("days", 0)
|
|
141
|
+
|
|
142
|
+
uptime_str = format_uptime(uptime) if isinstance(uptime, int) else "N/A"
|
|
143
|
+
|
|
144
|
+
difficulty = settings.get("Difficulty", "N/A")
|
|
145
|
+
penalty = settings.get("PlayerDeathPenalty", "N/A")
|
|
146
|
+
pvp = "启用" if settings.get("bIsPvP") is True else "禁用" if settings.get("bIsPvP") is False else "N/A"
|
|
147
|
+
|
|
148
|
+
reply = (
|
|
149
|
+
f"【服务器状态 - {s_name}】\n"
|
|
150
|
+
f"版本:{version}\n"
|
|
151
|
+
f"存档GUID:{guid}\n"
|
|
152
|
+
f"帧率 (FPS):{fps} ({frame_time:.2f} ms)\n"
|
|
153
|
+
f"运行时间:{uptime_str} (第 {days} 天)\n"
|
|
154
|
+
f"在线人数:{players_num} / {max_players}\n"
|
|
155
|
+
f"核心配置:难度 {difficulty} | PvP {pvp} | 死亡惩罚 {penalty}"
|
|
156
|
+
)
|
|
157
|
+
await pal_cmd.finish(reply)
|
|
158
|
+
|
|
159
|
+
elif action == "players":
|
|
160
|
+
players = await api.get_players()
|
|
161
|
+
if not players:
|
|
162
|
+
await pal_cmd.finish("当前没有玩家在线。")
|
|
163
|
+
|
|
164
|
+
# 若提供了目标参数,则进行模糊匹配并返回详细信息
|
|
165
|
+
if len(args) > 1:
|
|
166
|
+
target = args[1]
|
|
167
|
+
matched_player = None
|
|
168
|
+
for p in players:
|
|
169
|
+
p_name = p.get("name", "")
|
|
170
|
+
p_pid = p.get("playerId", "")
|
|
171
|
+
p_uid = p.get("userId", "")
|
|
172
|
+
if target.lower() in (p_name.lower(), p_pid.lower(), p_uid.lower()):
|
|
173
|
+
matched_player = p
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if not matched_player:
|
|
177
|
+
await pal_cmd.finish(f"在线玩家中未找到匹配「{target}」的人。")
|
|
178
|
+
|
|
179
|
+
name = matched_player.get("name", "Unknown")
|
|
180
|
+
reply = (
|
|
181
|
+
f"【玩家详细信息 - {name}】\n"
|
|
182
|
+
f"等级:{matched_player.get('level', 0)}\n"
|
|
183
|
+
f"玩家ID:{matched_player.get('playerId', 'N/A')}\n"
|
|
184
|
+
f"SteamID:{matched_player.get('userId', 'N/A')}\n"
|
|
185
|
+
f"延迟:{matched_player.get('ping', 0.0):.1f}ms\n"
|
|
186
|
+
f"坐标:({matched_player.get('location_x', 0.0):.1f}, {matched_player.get('location_y', 0.0):.1f})\n"
|
|
187
|
+
f"建筑数:{matched_player.get('building_count', 0)}"
|
|
188
|
+
)
|
|
189
|
+
await pal_cmd.finish(reply)
|
|
190
|
+
else:
|
|
191
|
+
# 默认返回简要的玩家昵称、等级、延迟列表
|
|
192
|
+
lines = [f"【在线玩家 ({len(players)}人)】"]
|
|
193
|
+
for i, p in enumerate(players, 1):
|
|
194
|
+
lines.append(
|
|
195
|
+
f"{i}. {p.get('name', 'Unknown')} "
|
|
196
|
+
f"(等级: {p.get('level', 0)}) "
|
|
197
|
+
f"[延迟: {p.get('ping', 0.0):.1f}ms]"
|
|
198
|
+
)
|
|
199
|
+
await pal_cmd.finish("\n".join(lines))
|
|
200
|
+
|
|
201
|
+
elif action == "say":
|
|
202
|
+
if len(args) < 2:
|
|
203
|
+
await pal_cmd.finish("格式错误:/pw say <消息内容>")
|
|
204
|
+
message = " ".join(args[1:])
|
|
205
|
+
await api.announce(message)
|
|
206
|
+
await pal_cmd.finish(f"已广播公告:\n「{message}」")
|
|
207
|
+
|
|
208
|
+
elif action == "kick":
|
|
209
|
+
if len(args) < 2:
|
|
210
|
+
await pal_cmd.finish("格式错误:/pw kick <玩家名/PlayerID/SteamID> [原因]")
|
|
211
|
+
target = args[1]
|
|
212
|
+
reason = args[2] if len(args) > 2 else "Kicked by Admin"
|
|
213
|
+
|
|
214
|
+
players = await api.get_players()
|
|
215
|
+
userid = None
|
|
216
|
+
target_name = target
|
|
217
|
+
for p in players:
|
|
218
|
+
p_name = p.get("name", "")
|
|
219
|
+
p_pid = p.get("playerId", "")
|
|
220
|
+
p_uid = p.get("userId", "")
|
|
221
|
+
if target.lower() in (p_name.lower(), p_pid.lower(), p_uid.lower()):
|
|
222
|
+
userid = p_uid
|
|
223
|
+
target_name = p_name
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
if not userid:
|
|
227
|
+
if target.startswith("steam_") or target.isdigit():
|
|
228
|
+
userid = target
|
|
229
|
+
else:
|
|
230
|
+
await pal_cmd.finish(f"未在在线列表中找到玩家「{target}」,且其不符合 SteamID 格式。")
|
|
231
|
+
|
|
232
|
+
await api.kick(userid, reason)
|
|
233
|
+
await pal_cmd.finish(f"成功踢出玩家「{target_name}」(SteamID: {userid}),原因: {reason}")
|
|
234
|
+
|
|
235
|
+
elif action == "ban":
|
|
236
|
+
if len(args) < 2:
|
|
237
|
+
await pal_cmd.finish("格式错误:/pw ban <玩家名/PlayerID/SteamID> [原因]")
|
|
238
|
+
target = args[1]
|
|
239
|
+
reason = args[2] if len(args) > 2 else "Banned by Admin"
|
|
240
|
+
|
|
241
|
+
players = await api.get_players()
|
|
242
|
+
userid = None
|
|
243
|
+
target_name = target
|
|
244
|
+
for p in players:
|
|
245
|
+
p_name = p.get("name", "")
|
|
246
|
+
p_pid = p.get("playerId", "")
|
|
247
|
+
p_uid = p.get("userId", "")
|
|
248
|
+
if target.lower() in (p_name.lower(), p_pid.lower(), p_uid.lower()):
|
|
249
|
+
userid = p_uid
|
|
250
|
+
target_name = p_name
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if not userid:
|
|
254
|
+
if target.startswith("steam_") or target.isdigit():
|
|
255
|
+
userid = target
|
|
256
|
+
else:
|
|
257
|
+
await pal_cmd.finish(f"未在在线列表中找到玩家「{target}」,且其不符合 SteamID 格式。")
|
|
258
|
+
|
|
259
|
+
await api.ban(userid, reason)
|
|
260
|
+
await pal_cmd.finish(f"成功封禁玩家「{target_name}」(SteamID: {userid}),原因: {reason}")
|
|
261
|
+
|
|
262
|
+
elif action == "unban":
|
|
263
|
+
if len(args) < 2:
|
|
264
|
+
await pal_cmd.finish("格式错误:/pw unban <SteamID>")
|
|
265
|
+
userid = args[1]
|
|
266
|
+
if not (userid.startswith("steam_") or userid.isdigit()):
|
|
267
|
+
await pal_cmd.finish("解封错误:解封必须提供有效的 SteamID (如 steam_00000000000000000)")
|
|
268
|
+
await api.unban(userid)
|
|
269
|
+
await pal_cmd.finish(f"成功解封玩家 (SteamID: {userid})。")
|
|
270
|
+
|
|
271
|
+
elif action == "save":
|
|
272
|
+
await api.save()
|
|
273
|
+
await pal_cmd.finish("世界存档保存成功。")
|
|
274
|
+
|
|
275
|
+
elif action == "shutdown":
|
|
276
|
+
waittime = 60
|
|
277
|
+
message = "Server is shutting down for maintenance."
|
|
278
|
+
is_force = False
|
|
279
|
+
|
|
280
|
+
if len(args) > 1:
|
|
281
|
+
first_arg = args[1].lower()
|
|
282
|
+
if first_arg in ("force", "0"):
|
|
283
|
+
is_force = True
|
|
284
|
+
else:
|
|
285
|
+
try:
|
|
286
|
+
waittime = int(first_arg)
|
|
287
|
+
except ValueError:
|
|
288
|
+
message = " ".join(args[1:])
|
|
289
|
+
else:
|
|
290
|
+
if len(args) > 2:
|
|
291
|
+
message = " ".join(args[2:])
|
|
292
|
+
|
|
293
|
+
if is_force:
|
|
294
|
+
await api.stop()
|
|
295
|
+
await pal_cmd.finish("已发送强制关闭指令。")
|
|
296
|
+
else:
|
|
297
|
+
await api.shutdown(waittime, message)
|
|
298
|
+
await pal_cmd.finish(f"已发送优雅关机指令:将在 {waittime} 秒后关闭,公告: 「{message}」")
|
|
299
|
+
|
|
300
|
+
except PalWorldAPIError as e:
|
|
301
|
+
await pal_cmd.finish(f"操作失败:{e}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# 转发 QQ 群消息至帕鲁服务器
|
|
305
|
+
group_forward = on_message(priority=100, block=False)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@group_forward.handle()
|
|
309
|
+
async def handle_forward(bot: Bot, event: GroupMessageEvent):
|
|
310
|
+
if not pal_config.palworld_forward_group_msg:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# 仅在配置的帕鲁群内转发消息
|
|
314
|
+
if not isinstance(event, GroupMessageEvent) or str(event.group_id) not in pal_config.palworld_qq_groups:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
text = event.get_plaintext().strip()
|
|
318
|
+
if not text or text.startswith(("/", "pw")):
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
sender_name = event.sender.card or event.sender.nickname or str(event.user_id)
|
|
322
|
+
forward_text = f"[QQ] {sender_name}: {text}"
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
await api.announce(forward_text)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PalWorldAPIError(Exception):
|
|
6
|
+
"""API 基础异常类"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PalWorldAPI:
|
|
11
|
+
def __init__(self, base_url: str, password: str):
|
|
12
|
+
self.base_url = base_url.rstrip("/")
|
|
13
|
+
self.auth = httpx.BasicAuth("admin", password)
|
|
14
|
+
self.timeout = 10.0
|
|
15
|
+
|
|
16
|
+
async def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
17
|
+
"""发送 API 请求并进行统一异常封装"""
|
|
18
|
+
try:
|
|
19
|
+
async with httpx.AsyncClient(auth=self.auth, timeout=self.timeout) as client:
|
|
20
|
+
url = f"{self.base_url}{path}"
|
|
21
|
+
response = await client.request(method, url, **kwargs)
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
return response
|
|
24
|
+
except httpx.HTTPStatusError as e:
|
|
25
|
+
if e.response.status_code == 401:
|
|
26
|
+
raise PalWorldAPIError("认证失败:管理员密码错误")
|
|
27
|
+
raise PalWorldAPIError(f"HTTP 请求失败:状态码 {e.response.status_code}")
|
|
28
|
+
except httpx.RequestError as e:
|
|
29
|
+
raise PalWorldAPIError(f"网络连接错误:无法连接到帕鲁服务器 ({e})")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise PalWorldAPIError(f"请求异常:{e}")
|
|
32
|
+
|
|
33
|
+
async def get_info(self) -> Dict[str, Any]:
|
|
34
|
+
"""获取服务器信息"""
|
|
35
|
+
res = await self._request("GET", "/v1/api/info")
|
|
36
|
+
return res.json()
|
|
37
|
+
|
|
38
|
+
async def get_players(self) -> List[Dict[str, Any]]:
|
|
39
|
+
"""获取在线玩家列表"""
|
|
40
|
+
res = await self._request("GET", "/v1/api/players")
|
|
41
|
+
return res.json().get("players", [])
|
|
42
|
+
|
|
43
|
+
async def get_settings(self) -> Dict[str, Any]:
|
|
44
|
+
"""获取服务器设置"""
|
|
45
|
+
res = await self._request("GET", "/v1/api/settings")
|
|
46
|
+
return res.json()
|
|
47
|
+
|
|
48
|
+
async def get_metrics(self) -> Dict[str, Any]:
|
|
49
|
+
"""获取服务器指标"""
|
|
50
|
+
res = await self._request("GET", "/v1/api/metrics")
|
|
51
|
+
return res.json()
|
|
52
|
+
|
|
53
|
+
async def announce(self, message: str) -> None:
|
|
54
|
+
"""发送公告"""
|
|
55
|
+
await self._request("POST", "/v1/api/announce", json={"message": message})
|
|
56
|
+
|
|
57
|
+
async def kick(self, userid: str, message: str = "") -> None:
|
|
58
|
+
"""踢出玩家"""
|
|
59
|
+
await self._request("POST", "/v1/api/kick", json={"userid": userid, "message": message})
|
|
60
|
+
|
|
61
|
+
async def ban(self, userid: str, message: str = "") -> None:
|
|
62
|
+
"""封禁玩家"""
|
|
63
|
+
await self._request("POST", "/v1/api/ban", json={"userid": userid, "message": message})
|
|
64
|
+
|
|
65
|
+
async def unban(self, userid: str) -> None:
|
|
66
|
+
"""解封玩家"""
|
|
67
|
+
await self._request("POST", "/v1/api/unban", json={"userid": userid})
|
|
68
|
+
|
|
69
|
+
async def save(self) -> None:
|
|
70
|
+
"""保存存档"""
|
|
71
|
+
await self._request("POST", "/v1/api/save")
|
|
72
|
+
|
|
73
|
+
async def shutdown(self, waittime: int, message: str = "") -> None:
|
|
74
|
+
"""优雅关闭服务器"""
|
|
75
|
+
await self._request("POST", "/v1/api/shutdown", json={"waittime": waittime, "message": message})
|
|
76
|
+
|
|
77
|
+
async def stop(self) -> None:
|
|
78
|
+
"""强行关闭服务器"""
|
|
79
|
+
await self._request("POST", "/v1/api/stop")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "nonebot-plugin-palworld-sync"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "基于NoneBot2的幻兽帕鲁 (Palworld) 服务器 REST API 插件"
|
|
5
|
+
authors = ["yuexps@qq.com"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
homepage = "https://github.com/yuexps/nonebot-plugin-palworld-sync"
|
|
8
|
+
repository = "https://github.com/yuexps/nonebot-plugin-palworld-sync"
|
|
9
|
+
keywords = ["nonebot", "nonebot2", "palworld", "pal"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.9",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
packages = [
|
|
19
|
+
{ include = "nonebot_plugin_palworld_sync" }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dependencies]
|
|
23
|
+
python = "^3.9"
|
|
24
|
+
nonebot2 = "^2.2.0"
|
|
25
|
+
nonebot-adapter-onebot = "^2.4.0"
|
|
26
|
+
httpx = ">=0.20.0,<1.0.0"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["poetry-core>=1.0.0"]
|
|
30
|
+
build-backend = "poetry.core.masonry.api"
|