koishi-plugin-minecraft-adapter 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # koishi-plugin-minecraft-adapter
2
2
 
3
- Koishi 的 Minecraft 适配器:支持 RCON 与鹊桥(Queqiao) Webhook。
3
+ Koishi 的 Minecraft 适配器:支持 RCON、鹊桥(Queqiao) Webhook 与 WebSocket
4
4
 
5
5
  - RCON:连接并发送命令/广播
6
- - Webhook:接收聊天、加入、离开等事件并在 Koishi 中触发
6
+ - Webhook/WS:接收聊天、加入、离开等事件并在 Koishi 中触发
7
7
 
8
8
  参考实现:
9
9
  - https://github.com/17TheWord/nonebot-adapter-minecraft
@@ -19,6 +19,25 @@ npm i koishi-plugin-minecraft-adapter
19
19
 
20
20
  - RCON:host/port/password/timeout/reconnectInterval/reconnectStrategy/maxReconnectInterval/broadcastMode
21
21
  - Webhook:enabled/path/secret/verifyMode/signatureHeader/secretHeader
22
+ - WebSocket(对齐鹊桥官方)
23
+ - enabled: 是否启用
24
+ - url: WS 地址,例如 `ws://127.0.0.1:8080`
25
+ - serverName: 必填,需与鹊桥 `config.yml` 的 `server_name` 完全一致
26
+ - accessToken: 可选,对应鹊桥 `config.yml` 的 `access_token`
27
+ - extraHeaders: 可选,附加自定义请求头
28
+ - reconnectStrategy: `fixed | exponential`
29
+ - reconnectInterval / maxReconnectInterval: 断线重连间隔
30
+
31
+ 握手头部:
32
+
33
+ ```
34
+ x-self-name: <serverName>
35
+ Authorization: Bearer <accessToken> // 若 accessToken 不为空
36
+ ```
37
+
38
+ 鹊桥项目参考:
39
+
40
+ - QueQiao: https://github.com/17TheWord/QueQiao
22
41
 
23
42
  ## 用法
24
43
 
package/lib/index.d.ts CHANGED
@@ -20,6 +20,9 @@ declare class MinecraftService extends Service implements MinecraftServiceAPI {
20
20
  private currentReconnectInterval?;
21
21
  private commandQueue;
22
22
  private isProcessingQueue;
23
+ private ws?;
24
+ private wsReconnectTimer?;
25
+ private wsCurrentInterval?;
23
26
  constructor(ctx: Context, config: MinecraftService.Config);
24
27
  private registerWebhook;
25
28
  start(): void;
@@ -28,6 +31,10 @@ declare class MinecraftService extends Service implements MinecraftServiceAPI {
28
31
  private scheduleReconnect;
29
32
  private ensureConnected;
30
33
  private disconnect;
34
+ private connectWs;
35
+ private disconnectWs;
36
+ private ensureWsConnected;
37
+ private scheduleWsReconnect;
31
38
  execute(command: string): Promise<string>;
32
39
  broadcast(message: string): Promise<string>;
33
40
  sendTo(player: string, message: string): Promise<string>;
@@ -54,9 +61,20 @@ declare namespace MinecraftService {
54
61
  signatureHeader?: string;
55
62
  secretHeader?: string;
56
63
  }
64
+ interface WebSocketConfig {
65
+ enabled: boolean;
66
+ url: string;
67
+ serverName: string;
68
+ accessToken?: string;
69
+ extraHeaders?: Record<string, string>;
70
+ reconnectInterval?: number;
71
+ reconnectStrategy?: 'fixed' | 'exponential';
72
+ maxReconnectInterval?: number;
73
+ }
57
74
  interface Config {
58
75
  rcon?: RconConfig;
59
76
  webhook?: WebhookConfig;
77
+ websocket?: WebSocketConfig;
60
78
  }
61
79
  const Config: Schema<Config>;
62
80
  }
package/lib/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const koishi_1 = require("koishi");
4
7
  const rcon_client_1 = require("rcon-client");
8
+ const ws_1 = __importDefault(require("ws"));
5
9
  const logger = new koishi_1.Logger('minecraft');
6
10
  class MinecraftService extends koishi_1.Service {
7
11
  ctx;
@@ -16,6 +20,10 @@ class MinecraftService extends koishi_1.Service {
16
20
  currentReconnectInterval;
17
21
  commandQueue = [];
18
22
  isProcessingQueue = false;
23
+ // WebSocket
24
+ ws;
25
+ wsReconnectTimer;
26
+ wsCurrentInterval;
19
27
  constructor(ctx, config) {
20
28
  super(ctx, 'minecraft', true);
21
29
  this.ctx = ctx;
@@ -73,6 +81,9 @@ class MinecraftService extends koishi_1.Service {
73
81
  if (this.config.rcon?.enabled) {
74
82
  this.ensureConnected();
75
83
  }
84
+ if (this.config.websocket?.enabled) {
85
+ this.ensureWsConnected();
86
+ }
76
87
  }
77
88
  stop() {
78
89
  if (this.reconnectTimer) {
@@ -80,6 +91,11 @@ class MinecraftService extends koishi_1.Service {
80
91
  this.reconnectTimer = undefined;
81
92
  }
82
93
  this.disconnect();
94
+ if (this.wsReconnectTimer) {
95
+ clearTimeout(this.wsReconnectTimer);
96
+ this.wsReconnectTimer = undefined;
97
+ }
98
+ this.disconnectWs();
83
99
  }
84
100
  async connectRcon() {
85
101
  if (this.isConnecting || this.rcon)
@@ -138,6 +154,72 @@ class MinecraftService extends koishi_1.Service {
138
154
  this.rcon = undefined;
139
155
  }
140
156
  }
157
+ connectWs() {
158
+ if (this.ws)
159
+ return;
160
+ const { url, serverName, accessToken, extraHeaders } = this.config.websocket;
161
+ logger.info('WS 连接中: ' + url);
162
+ const headers = { 'x-self-name': serverName, ...(extraHeaders || {}) };
163
+ if (accessToken)
164
+ headers['Authorization'] = `Bearer ${accessToken}`;
165
+ const ws = new ws_1.default(url, { headers });
166
+ this.ws = ws;
167
+ ws.on('open', () => {
168
+ logger.info('WS 连接成功');
169
+ });
170
+ ws.on('message', (data) => {
171
+ try {
172
+ const text = data.toString('utf8');
173
+ const obj = JSON.parse(text);
174
+ const type = obj.type || obj.event || 'unknown';
175
+ const payload = obj.data ?? obj;
176
+ const mapped = mapQueqiaoEvent(type, payload);
177
+ this.ctx.emit(`minecraft/${mapped.type}`, mapped.payload);
178
+ }
179
+ catch (e) {
180
+ logger.warn('WS 消息解析失败: ' + e?.message);
181
+ }
182
+ });
183
+ ws.on('close', () => {
184
+ logger.warn('WS 连接断开');
185
+ this.ws = undefined;
186
+ this.scheduleWsReconnect();
187
+ });
188
+ ws.on('error', (err) => {
189
+ logger.warn('WS 错误: ' + err?.message);
190
+ });
191
+ }
192
+ disconnectWs() {
193
+ if (this.ws) {
194
+ try {
195
+ this.ws.terminate();
196
+ }
197
+ catch { }
198
+ this.ws = undefined;
199
+ }
200
+ }
201
+ ensureWsConnected() {
202
+ if (this.ws)
203
+ return;
204
+ this.connectWs();
205
+ }
206
+ scheduleWsReconnect() {
207
+ const base = this.config.websocket?.reconnectInterval ?? 5000;
208
+ const strategy = this.config.websocket?.reconnectStrategy ?? 'fixed';
209
+ const maxInterval = this.config.websocket?.maxReconnectInterval ?? 60000;
210
+ if (this.wsCurrentInterval == null)
211
+ this.wsCurrentInterval = base;
212
+ const interval = strategy === 'exponential' ? Math.min(this.wsCurrentInterval * 2, maxInterval) : base;
213
+ if (!this.config.websocket?.enabled)
214
+ return;
215
+ if (this.wsReconnectTimer)
216
+ return;
217
+ this.wsReconnectTimer = setTimeout(() => {
218
+ this.wsReconnectTimer = undefined;
219
+ this.wsCurrentInterval = interval;
220
+ this.ensureWsConnected();
221
+ }, interval);
222
+ }
141
223
  async execute(command) {
142
224
  if (!this.config.rcon?.enabled)
143
225
  throw new Error('RCON 未启用');
@@ -233,6 +315,25 @@ function escapeForMc(text) {
233
315
  signatureHeader: 'x-queqiao-signature',
234
316
  secretHeader: 'x-queqiao-secret',
235
317
  }),
318
+ websocket: koishi_1.Schema.object({
319
+ enabled: koishi_1.Schema.boolean().description('启用鹊桥 WebSocket').default(false),
320
+ url: koishi_1.Schema.string().description('WebSocket 地址,如 ws://host:port').required(),
321
+ serverName: koishi_1.Schema.string().description('服务器名称,需与鹊桥 config.yml 的 server_name 一致').required(),
322
+ accessToken: koishi_1.Schema.string().role('secret').description('访问令牌,对应鹊桥 config.yml 的 access_token,可为空'),
323
+ extraHeaders: koishi_1.Schema.dict(String).description('附加请求头,可选'),
324
+ reconnectInterval: koishi_1.Schema.number().description('断线重连基础间隔(ms)').default(5000),
325
+ reconnectStrategy: koishi_1.Schema.union(['fixed', 'exponential']).description('重连策略').default('fixed'),
326
+ maxReconnectInterval: koishi_1.Schema.number().description('最大重连间隔(ms)').default(60000),
327
+ }).description('WebSocket 设置').default({
328
+ enabled: false,
329
+ url: '',
330
+ serverName: 'Server-1',
331
+ accessToken: '',
332
+ extraHeaders: {},
333
+ reconnectInterval: 5000,
334
+ reconnectStrategy: 'fixed',
335
+ maxReconnectInterval: 60000,
336
+ }),
236
337
  });
237
338
  })(MinecraftService || (MinecraftService = {}));
238
339
  exports.default = MinecraftService;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-minecraft-adapter",
3
3
  "description": "Minecraft RCON & webhook integration for Koishi",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -22,7 +22,8 @@
22
22
  "koishi": "^4.17.9"
23
23
  },
24
24
  "dependencies": {
25
- "rcon-client": "^4.2.5"
25
+ "rcon-client": "^4.2.5",
26
+ "ws": "^8.18.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "typescript": "^5.0.0"