koishi-plugin-minecraft-adapter 1.0.7 → 1.0.9

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/lib/index.d.ts CHANGED
@@ -217,6 +217,8 @@ export declare class MinecraftAdapter<C extends Context = Context> extends Adapt
217
217
  private requestCounter;
218
218
  /** 事件去重缓存:防止鹊桥服务端对同一事件发送多次 */
219
219
  private recentEventKeys;
220
+ private disposed;
221
+ private reconnectTimers;
220
222
  private debug;
221
223
  private detailedLogging;
222
224
  private tokenizeMode;
package/lib/index.js CHANGED
@@ -9,29 +9,52 @@ const rcon_client_1 = require("rcon-client");
9
9
  const ws_1 = __importDefault(require("ws"));
10
10
  const logger = new koishi_1.Logger('minecraft');
11
11
  /**
12
- * 将嵌套格式的服务器配置扁平化
12
+ * 将嵌套格式的服务器配置扁平化。
13
+ * 每种嵌套对象(websocket / rcon / chatImage)独立处理,
14
+ * 确保任何混合格式(如扁平 url + 嵌套 rcon)都能正确转换。
13
15
  */
14
16
  function flattenServerConfig(server) {
15
- if (server.websocket && typeof server.websocket === 'object') {
16
- // 嵌套格式的 rcon 对象:只要存在且有 host/port/password 任一字段,即视为启用
17
- const hasRcon = server.rcon && typeof server.rcon === 'object'
18
- && (server.rcon.host || server.rcon.port || server.rcon.password);
19
- return {
20
- selfId: server.selfId,
21
- serverName: server.serverName,
22
- url: server.websocket.url,
23
- accessToken: server.websocket.accessToken,
24
- extraHeaders: server.websocket.extraHeaders,
25
- enableRcon: hasRcon ? true : (server.rcon?.enabled ?? false),
26
- rconHost: server.rcon?.host,
27
- rconPort: server.rcon?.port,
28
- rconPassword: server.rcon?.password,
29
- rconTimeout: server.rcon?.timeout,
30
- enableChatImage: server.chatImage?.enabled ?? !!(server.chatImage && (server.chatImage.defaultImageName)),
31
- chatImageDefaultName: server.chatImage?.defaultImageName,
32
- };
17
+ const result = { ...server };
18
+ // ── websocket 嵌套 扁平 ──
19
+ if (result.websocket && typeof result.websocket === 'object') {
20
+ const ws = result.websocket;
21
+ if (ws.url !== undefined)
22
+ result.url = ws.url;
23
+ if (ws.accessToken !== undefined)
24
+ result.accessToken = ws.accessToken;
25
+ if (ws.extraHeaders !== undefined)
26
+ result.extraHeaders = ws.extraHeaders;
27
+ delete result.websocket;
28
+ }
29
+ // ── rcon 嵌套 → 扁平 ──
30
+ if (result.rcon && typeof result.rcon === 'object') {
31
+ const rcon = result.rcon;
32
+ const hasRconConfig = rcon.host || rcon.port || rcon.password;
33
+ if (hasRconConfig || rcon.enabled) {
34
+ result.enableRcon = true;
35
+ }
36
+ // 仅在嵌套值实际存在时覆盖,避免用 undefined 覆盖已有值或阻止 Schema 默认值
37
+ if (rcon.host !== undefined)
38
+ result.rconHost = rcon.host;
39
+ if (rcon.port !== undefined)
40
+ result.rconPort = rcon.port;
41
+ if (rcon.password !== undefined)
42
+ result.rconPassword = rcon.password;
43
+ if (rcon.timeout !== undefined)
44
+ result.rconTimeout = rcon.timeout;
45
+ delete result.rcon;
33
46
  }
34
- return server;
47
+ // ── chatImage 嵌套 → 扁平 ──
48
+ if (result.chatImage && typeof result.chatImage === 'object') {
49
+ const ci = result.chatImage;
50
+ if (ci.enabled !== undefined || ci.defaultImageName) {
51
+ result.enableChatImage = ci.enabled ?? !!ci.defaultImageName;
52
+ }
53
+ if (ci.defaultImageName !== undefined)
54
+ result.chatImageDefaultName = ci.defaultImageName;
55
+ delete result.chatImage;
56
+ }
57
+ return result;
35
58
  }
36
59
  /**
37
60
  * 向后兼容:
@@ -109,6 +132,8 @@ class MinecraftAdapter extends koishi_1.Adapter {
109
132
  requestCounter = 0;
110
133
  /** 事件去重缓存:防止鹊桥服务端对同一事件发送多次 */
111
134
  recentEventKeys = new Map();
135
+ disposed = false;
136
+ reconnectTimers = new Set();
112
137
  debug;
113
138
  detailedLogging;
114
139
  tokenizeMode;
@@ -140,7 +165,7 @@ class MinecraftAdapter extends koishi_1.Adapter {
140
165
  }
141
166
  for (const serverConfig of config.servers) {
142
167
  if (this.debug) {
143
- logger.info(`[DEBUG] Initializing server ${serverConfig.selfId}`);
168
+ logger.info(`[DEBUG] Initializing server ${serverConfig.selfId}, full config:`, JSON.stringify(serverConfig));
144
169
  }
145
170
  const bot = new MinecraftBot(ctx, serverConfig);
146
171
  bot.adapter = this;
@@ -392,11 +417,26 @@ class MinecraftAdapter extends koishi_1.Adapter {
392
417
  async connectRcon(bot) {
393
418
  const config = bot.config;
394
419
  const selfId = bot.selfId;
395
- const rconHost = config.rconHost || '127.0.0.1';
396
- const rconPort = config.rconPort || 25575;
397
- const rconTimeout = config.rconTimeout || 5000;
420
+ // rconHost 未配置时,从 WebSocket URL 中提取主机地址作为回退
421
+ let rconHost = config.rconHost;
422
+ if (!rconHost && config.url) {
423
+ try {
424
+ rconHost = new URL(config.url).hostname;
425
+ }
426
+ catch { }
427
+ }
428
+ rconHost = rconHost || '127.0.0.1';
429
+ const rconPort = config.rconPort ?? 25575;
430
+ const rconTimeout = config.rconTimeout ?? 5000;
398
431
  if (this.debug) {
399
- logger.info(`[DEBUG] Connecting RCON for server ${selfId} to ${rconHost}:${rconPort}`);
432
+ logger.info(`[DEBUG] RCON config for server ${selfId}:`, {
433
+ rconHost: config.rconHost,
434
+ rconPort: config.rconPort,
435
+ rconPassword: config.rconPassword ? '***' : undefined,
436
+ rconTimeout: config.rconTimeout,
437
+ enableRcon: config.enableRcon,
438
+ resolved: `${rconHost}:${rconPort}`,
439
+ });
400
440
  }
401
441
  try {
402
442
  const rcon = await this.createRconWithTimeout(rconHost, rconPort, config.rconPassword || '', rconTimeout);
@@ -434,6 +474,8 @@ class MinecraftAdapter extends koishi_1.Adapter {
434
474
  const selfId = bot.selfId;
435
475
  if (!this.rconConfigs.has(selfId))
436
476
  return;
477
+ if (this.disposed)
478
+ return;
437
479
  const attempts = this.rconReconnectAttempts.get(selfId) || 0;
438
480
  if (attempts >= this.maxReconnectAttempts) {
439
481
  logger.error(`RCON max reconnect attempts (${this.maxReconnectAttempts}) reached for server ${selfId}`);
@@ -444,13 +486,17 @@ class MinecraftAdapter extends koishi_1.Adapter {
444
486
  if (this.debug) {
445
487
  logger.info(`[DEBUG] RCON reconnect for server ${selfId} in ${delay}ms (attempt ${attempts + 1}/${this.maxReconnectAttempts})`);
446
488
  }
447
- setTimeout(() => {
489
+ const timer = setTimeout(() => {
490
+ this.reconnectTimers.delete(timer);
491
+ if (this.disposed)
492
+ return;
448
493
  if (!this.rconConfigs.has(selfId))
449
494
  return;
450
495
  if (this.rconConnections.has(selfId))
451
496
  return;
452
497
  this.connectRcon(bot);
453
498
  }, delay);
499
+ this.reconnectTimers.add(timer);
454
500
  }
455
501
  async connectWebSocket(bot) {
456
502
  const config = bot.config;
@@ -562,18 +608,24 @@ class MinecraftAdapter extends koishi_1.Adapter {
562
608
  logger.info(`[DEBUG] Close reason:`, reason.toString());
563
609
  }
564
610
  bot.offline();
611
+ if (this.disposed)
612
+ return;
565
613
  const attempts = this.reconnectAttempts.get(bot.selfId) || 0;
566
614
  if (attempts < this.maxReconnectAttempts) {
567
615
  this.reconnectAttempts.set(bot.selfId, attempts + 1);
568
- const delay = this.reconnectInterval * Math.pow(2, attempts); // 指数退避
616
+ const delay = this.reconnectInterval * Math.pow(2, attempts);
569
617
  if (this.debug) {
570
618
  logger.info(`[DEBUG] Attempting to reconnect WebSocket for bot ${bot.selfId} in ${delay}ms (attempt ${attempts + 1}/${this.maxReconnectAttempts})`);
571
619
  }
572
- setTimeout(() => {
620
+ const timer = setTimeout(() => {
621
+ this.reconnectTimers.delete(timer);
622
+ if (this.disposed)
623
+ return;
573
624
  if (this.wsConnections.get(bot.selfId)?.readyState !== ws_1.default.OPEN) {
574
625
  this.connectWebSocket(bot);
575
626
  }
576
627
  }, delay);
628
+ this.reconnectTimers.add(timer);
577
629
  }
578
630
  else {
579
631
  logger.error(`Max reconnect attempts reached for bot ${bot.selfId}`);
@@ -1048,6 +1100,11 @@ class MinecraftAdapter extends koishi_1.Adapter {
1048
1100
  if (this.debug) {
1049
1101
  logger.info(`[DEBUG] Stopping MinecraftAdapter`);
1050
1102
  }
1103
+ this.disposed = true;
1104
+ for (const timer of this.reconnectTimers) {
1105
+ clearTimeout(timer);
1106
+ }
1107
+ this.reconnectTimers.clear();
1051
1108
  for (const [echo, pending] of this.pendingRequests) {
1052
1109
  clearTimeout(pending.timeout);
1053
1110
  pending.reject(new Error('Adapter stopped'));
@@ -1165,34 +1222,20 @@ MinecraftBot.MessageEncoder = MinecraftMessageEncoder;
1165
1222
  // ============================================================================
1166
1223
  // Koishi Schema 配置
1167
1224
  // ============================================================================
1168
- const serverSchema = koishi_1.Schema.intersect([
1169
- koishi_1.Schema.object({
1170
- selfId: koishi_1.Schema.string().description('机器人 ID(唯一标识)').required(),
1171
- serverName: koishi_1.Schema.string().description('服务器名称(需与鹊桥 config.yml 中的 server_name 一致)'),
1172
- url: koishi_1.Schema.string().description('WebSocket 地址(如 ws://127.0.0.1:8080)').required(),
1173
- accessToken: koishi_1.Schema.string().description('访问令牌(需与鹊桥 config.yml 中的 access_token 一致)'),
1174
- extraHeaders: koishi_1.Schema.dict(koishi_1.Schema.string()).description('额外请求头'),
1175
- enableRcon: koishi_1.Schema.boolean().description('启用 RCON 远程命令执行').default(false),
1176
- enableChatImage: koishi_1.Schema.boolean().description('启用 ChatImage CICode 图片发送(需客户端安装 ChatImage Mod)').default(false),
1177
- }),
1178
- koishi_1.Schema.union([
1179
- koishi_1.Schema.object({
1180
- enableRcon: koishi_1.Schema.const(true).required(),
1181
- rconHost: koishi_1.Schema.string().description('RCON 主机地址').default('127.0.0.1'),
1182
- rconPort: koishi_1.Schema.number().description('RCON 端口').default(25575),
1183
- rconPassword: koishi_1.Schema.string().description('RCON 密码(留空表示无密码)'),
1184
- rconTimeout: koishi_1.Schema.number().description('RCON 超时时间(ms)').default(5000),
1185
- }),
1186
- koishi_1.Schema.object({}),
1187
- ]),
1188
- koishi_1.Schema.union([
1189
- koishi_1.Schema.object({
1190
- enableChatImage: koishi_1.Schema.const(true).required(),
1191
- chatImageDefaultName: koishi_1.Schema.string().description('图片在聊天栏中的默认显示名称').default('图片'),
1192
- }),
1193
- koishi_1.Schema.object({}),
1194
- ]),
1195
- ]);
1225
+ const serverSchema = koishi_1.Schema.object({
1226
+ selfId: koishi_1.Schema.string().description('机器人 ID(唯一标识)').required(),
1227
+ serverName: koishi_1.Schema.string().description('服务器名称(需与鹊桥 config.yml 中的 server_name 一致)'),
1228
+ url: koishi_1.Schema.string().description('WebSocket 地址(如 ws://127.0.0.1:8080)').required(),
1229
+ accessToken: koishi_1.Schema.string().description('访问令牌(需与鹊桥 config.yml 中的 access_token 一致)'),
1230
+ extraHeaders: koishi_1.Schema.dict(koishi_1.Schema.string()).description('额外请求头'),
1231
+ enableRcon: koishi_1.Schema.boolean().description('启用 RCON 远程命令执行').default(false),
1232
+ rconHost: koishi_1.Schema.string().description('RCON 主机地址(留空则自动取 WebSocket 地址中的主机)'),
1233
+ rconPort: koishi_1.Schema.number().description('RCON 端口').default(25575),
1234
+ rconPassword: koishi_1.Schema.string().description('RCON 密码(留空表示无密码)'),
1235
+ rconTimeout: koishi_1.Schema.number().description('RCON 超时时间(ms)').default(5000),
1236
+ enableChatImage: koishi_1.Schema.boolean().description('启用 ChatImage CICode 图片发送(需客户端安装 ChatImage Mod)').default(false),
1237
+ chatImageDefaultName: koishi_1.Schema.string().description('图片在聊天栏中的默认显示名称').default('图片'),
1238
+ });
1196
1239
  (function (MinecraftAdapter) {
1197
1240
  MinecraftAdapter.Config = koishi_1.Schema.object({
1198
1241
  debug: koishi_1.Schema.boolean().description('启用调试模式,输出详细日志').default(false),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-minecraft-adapter",
3
3
  "description": "Minecraft adapter for Koishi based on QueQiao V2 protocol",
4
- "version": "1.0.7",
4
+ "version": "1.0.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [