@sjtdev/koishi-plugin-dota2tracker 2.5.3 → 2.5.4

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/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # 更新日志
2
2
 
3
+ ### [2.5.4](https://github.com/sjtdev/koishi-plugin-dota2tracker/compare/v2.5.3...v2.5.4) (2026-02-26)
4
+
5
+ ### 🚀 功能优化
6
+
7
+ * **database:** 增加缓存群友列表机制,加入对拉取有效群友时的检测,每小时更新缓存 ([1ac1df9](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/1ac1df9513a883b17031fdc3e912fccc0af8a3e2)), closes [#17](https://github.com/sjtdev/koishi-plugin-dota2tracker/issues/17)
8
+
9
+ ### 🐛 Bug 修复
10
+
11
+ * **match-watcher:** 修复无有效群友时还会发送查询请求 ([a8d1e8c](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/a8d1e8c173d71b850516912729e398943d3ea391))
12
+
3
13
  ### [2.5.3](https://github.com/sjtdev/koishi-plugin-dota2tracker/compare/v2.5.2...v2.5.3) (2026-02-23)
4
14
 
5
15
  ### 🎨 样式
package/lib/index.js CHANGED
@@ -2313,13 +2313,13 @@ var DatabaseService = class extends import_koishi7.Service {
2313
2313
  }
2314
2314
  constructor(ctx) {
2315
2315
  super(ctx, "dota2tracker.database", true);
2316
- ctx.model.extend("dt_subscribed_guilds", { id: "unsigned", guildId: "string", platform: "string" }, { autoInc: true });
2316
+ ctx.model.extend("dt_subscribed_guilds", { id: "unsigned", channelId: { type: "string", legacy: ["guildId"] }, platform: "string" }, { autoInc: true });
2317
2317
  ctx.model.extend(
2318
2318
  "dt_subscribed_players",
2319
2319
  {
2320
2320
  id: "unsigned",
2321
2321
  userId: "string",
2322
- guildId: "string",
2322
+ channelId: { type: "string", legacy: ["guildId"] },
2323
2323
  platform: "string",
2324
2324
  steamId: "integer",
2325
2325
  nickName: "string",
@@ -2328,6 +2328,62 @@ var DatabaseService = class extends import_koishi7.Service {
2328
2328
  { autoInc: true }
2329
2329
  );
2330
2330
  ctx.model.extend("dt_match_extension", { matchId: "string", startTime: "timestamp", data: "json" }, { autoInc: false, primary: ["matchId"] });
2331
+ ctx.cron("0 * * * *", async () => {
2332
+ await this.refreshAllGuildMemberCaches();
2333
+ });
2334
+ }
2335
+ /** 群成员列表内存缓存: Map<"platform:channelId", Set<userId>> */
2336
+ guildMemberCache = /* @__PURE__ */ new Map();
2337
+ /**
2338
+ * 刷新指定频道的群成员缓存。
2339
+ * 通过 channel 表获取 assignee(bot selfId),再调用 bot.getGuildMemberList。
2340
+ * 失败时记录 warn 并清除该 key(保守策略)。
2341
+ */
2342
+ async refreshGuildMemberCache(platform, channelId) {
2343
+ const cacheKey = `${platform}:${channelId}`;
2344
+ try {
2345
+ const channelRow = (await this.ctx.database.get("channel", { id: channelId })).at(0);
2346
+ const selfId = channelRow?.assignee;
2347
+ const guildId = channelRow?.guildId ?? channelId;
2348
+ if (!selfId) {
2349
+ this.guildMemberCache.delete(cacheKey);
2350
+ return;
2351
+ }
2352
+ const bot = this.ctx.bots[`${platform}:${selfId}`];
2353
+ if (!bot) {
2354
+ this.guildMemberCache.delete(cacheKey);
2355
+ return;
2356
+ }
2357
+ const members = await bot.getGuildMemberList(guildId);
2358
+ const userIds = new Set(members.data.map((m) => m.user.id));
2359
+ this.guildMemberCache.set(cacheKey, userIds);
2360
+ } catch (error) {
2361
+ this.logger.warn(`获取频道 ${cacheKey} 成员列表失败,已清除对应缓存:` + error);
2362
+ this.guildMemberCache.delete(cacheKey);
2363
+ }
2364
+ }
2365
+ /** 全量刷新所有已订阅频道的群成员缓存(每小时 cron 调用) */
2366
+ async refreshAllGuildMemberCaches() {
2367
+ const subscribedChannels = await this.ctx.database.get("dt_subscribed_guilds", void 0);
2368
+ await Promise.allSettled(subscribedChannels.map((ch) => this.refreshGuildMemberCache(ch.platform, ch.channelId)));
2369
+ }
2370
+ /**
2371
+ * 判断玩家是否仍在群组中。
2372
+ * 缓存未命中(冷启动阶段)时保守放行,返回 true。
2373
+ */
2374
+ isPlayerInGuild(platform, channelId, userId) {
2375
+ const cached = this.guildMemberCache.get(`${platform}:${channelId}`);
2376
+ if (!cached) return true;
2377
+ return cached.has(userId);
2378
+ }
2379
+ /** 删除指定频道的群成员缓存(取消订阅时内部调用) */
2380
+ deleteGuildMemberCache(platform, channelId) {
2381
+ this.guildMemberCache.delete(`${platform}:${channelId}`);
2382
+ }
2383
+ /** 从缓存中移除单个用户(取消绑定时内部调用,保留频道内其他成员的缓存)*/
2384
+ removeUserFromGuildMemberCache(platform, channelId, userId) {
2385
+ const cached = this.guildMemberCache.get(`${platform}:${channelId}`);
2386
+ if (cached) cached.delete(userId);
2331
2387
  }
2332
2388
  async insertMatchExtension(matchId, startTime, data) {
2333
2389
  return this.ctx.database.upsert("dt_match_extension", [{ matchId: String(matchId), startTime, data }]);
@@ -2345,9 +2401,8 @@ var DatabaseService = class extends import_koishi7.Service {
2345
2401
  return this.ctx.database.set("dt_subscribed_players", playerId, { rank });
2346
2402
  }
2347
2403
  async getActiveSubscribedPlayers() {
2348
- const subscribedGuilds = await this.ctx.database.get("dt_subscribed_guilds", void 0);
2349
- const subscribedPlayersInGuild = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedGuilds.some((guild) => guild.guildId == player.guildId));
2350
- return subscribedPlayersInGuild;
2404
+ const subscribedChannels = await this.ctx.database.get("dt_subscribed_guilds", void 0);
2405
+ return (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedChannels.some((ch) => ch.channelId == player.channelId)).filter((player) => this.isPlayerInGuild(player.platform, player.channelId, player.userId));
2351
2406
  }
2352
2407
  async isUserBinded(session) {
2353
2408
  const subscribedPlayer = await this.ctx.database.get("dt_subscribed_players", this.getUserQuery(session));
@@ -2358,23 +2413,32 @@ var DatabaseService = class extends import_koishi7.Service {
2358
2413
  return subscribedPlayer.length > 0 ? subscribedPlayer[0] : null;
2359
2414
  }
2360
2415
  async bindUser(session, steamId, nickName) {
2361
- return this.ctx.database.create("dt_subscribed_players", {
2416
+ const result = await this.ctx.database.create("dt_subscribed_players", {
2362
2417
  ...this.getUserQuery(session),
2363
2418
  steamId: Number(steamId),
2364
2419
  nickName: nickName || ""
2365
2420
  });
2421
+ this.refreshGuildMemberCache(session.event.platform, session.event.channel.id);
2422
+ return result;
2366
2423
  }
2367
2424
  async unbindUser(session) {
2425
+ this.removeUserFromGuildMemberCache(session.event.platform, session.event.channel.id, session.event.user.id);
2368
2426
  return this.ctx.database.remove("dt_subscribed_players", this.getUserQuery(session));
2369
2427
  }
2428
+ async renamePlayer(playerId, nickName) {
2429
+ return this.ctx.database.set("dt_subscribed_players", playerId, { nickName });
2430
+ }
2370
2431
  async isChannelSubscribed(session) {
2371
2432
  const subscribedChannels = await this.ctx.database.get("dt_subscribed_guilds", this.getChannelQuery(session));
2372
2433
  return subscribedChannels.length > 0;
2373
2434
  }
2374
2435
  async subscribeChannel(session) {
2375
- return this.ctx.database.create("dt_subscribed_guilds", this.getChannelQuery(session));
2436
+ const result = await this.ctx.database.create("dt_subscribed_guilds", this.getChannelQuery(session));
2437
+ this.refreshGuildMemberCache(session.event.platform, session.event.channel.id);
2438
+ return result;
2376
2439
  }
2377
2440
  async unSubscribeChannel(session) {
2441
+ this.deleteGuildMemberCache(session.event.platform, session.event.channel.id);
2378
2442
  return this.ctx.database.remove("dt_subscribed_guilds", this.getChannelQuery(session));
2379
2443
  }
2380
2444
  async getSubscribedMembersInChannel(session) {
@@ -2385,13 +2449,13 @@ var DatabaseService = class extends import_koishi7.Service {
2385
2449
  */
2386
2450
  getChannelQuery(session) {
2387
2451
  return {
2388
- guildId: session.event.channel.id,
2452
+ channelId: session.event.channel.id,
2389
2453
  platform: session.event.platform
2390
2454
  };
2391
2455
  }
2392
2456
  getUserQuery(session) {
2393
2457
  return {
2394
- guildId: session.event.channel.id,
2458
+ channelId: session.event.channel.id,
2395
2459
  platform: session.event.platform,
2396
2460
  userId: session.event.user.id
2397
2461
  };
@@ -2399,14 +2463,14 @@ var DatabaseService = class extends import_koishi7.Service {
2399
2463
  /** 从已订阅玩家中查找玩家返回SteamId,不需要以昵称匹配时仅需传入Session */
2400
2464
  async getSubscribedPlayerByNickNameOrSession(session, nickName) {
2401
2465
  const player = (await this.ctx.database.get("dt_subscribed_players", {
2402
- guildId: session.event.channel.id,
2466
+ channelId: session.event.channel.id,
2403
2467
  platform: session.event.platform,
2404
2468
  ...nickName ? { nickName } : { userId: session.event.user.id }
2405
2469
  }))?.[0];
2406
2470
  return player;
2407
2471
  }
2408
- getChannelInfo({ platform, guildId }) {
2409
- return `${platform}:${guildId}`;
2472
+ getChannelInfo({ platform, channelId }) {
2473
+ return `${platform}:${channelId}`;
2410
2474
  }
2411
2475
  };
2412
2476
 
@@ -3301,6 +3365,7 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3301
3365
  async discovery() {
3302
3366
  try {
3303
3367
  const activePlayers = await this.ctx.dota2tracker.database.getActiveSubscribedPlayers();
3368
+ if (activePlayers.length === 0) return;
3304
3369
  const uniqueSteamIds = activePlayers.map((player) => player.steamId).filter((steamId, index, self) => self.indexOf(steamId) === index);
3305
3370
  const playersData = (await this.ctx.dota2tracker.stratzAPI.queryPlayersLastMatchRankInfo({ steamAccountIds: uniqueSteamIds })).players;
3306
3371
  await this.discoverNewMatches(playersData, activePlayers);
@@ -3320,10 +3385,10 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3320
3385
  }
3321
3386
  const playersByGuild = /* @__PURE__ */ new Map();
3322
3387
  for (const player of relevantActivePlayers) {
3323
- const guildKey = `${player.platform}:${player.guildId}`;
3388
+ const guildKey = `${player.platform}:${player.channelId}`;
3324
3389
  if (!playersByGuild.has(guildKey)) {
3325
3390
  playersByGuild.set(guildKey, {
3326
- guildInfo: { platform: player.platform, channelId: player.guildId, guildId: player.guildId },
3391
+ guildInfo: { platform: player.platform, channelId: player.channelId },
3327
3392
  players: []
3328
3393
  });
3329
3394
  }
@@ -3344,7 +3409,7 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3344
3409
  });
3345
3410
  messageToLogger.push({
3346
3411
  platform: guildInfo.platform,
3347
- guildId: guildInfo.guildId,
3412
+ guildId: guildInfo.channelId,
3348
3413
  players
3349
3414
  });
3350
3415
  subscribers.push(subscriber);
@@ -3381,12 +3446,12 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3381
3446
  if (prevRank.medal !== currRank.medal || prevRank.star !== currRank.star && this.config.rankBroadStar || prevRank.leader !== currRank.leader && this.config.rankBroadLeader) {
3382
3447
  let guildMember;
3383
3448
  try {
3384
- guildMember = await this.ctx.bots.find((bot) => bot.platform == subPlayer.platform)?.getGuildMember?.(subPlayer.guildId, subPlayer.userId);
3449
+ guildMember = await this.ctx.bots.find((bot) => bot.platform == subPlayer.platform)?.getGuildMember?.(subPlayer.channelId, subPlayer.userId);
3385
3450
  } catch (error) {
3386
3451
  this.logger.warn(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.fetch_guilds_failed") + error);
3387
3452
  }
3388
3453
  const name2 = subPlayer.nickName ?? guildMember?.nick ?? playersData.find((player) => player.steamAccount.id == subPlayer.steamId)?.steamAccount.name ?? String(subPlayer.steamId);
3389
- const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: subPlayer.guildId });
3454
+ const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: subPlayer.channelId });
3390
3455
  if (this.config.rankBroadFun === true) {
3391
3456
  const img = await this.ctx.dota2tracker.view.renderToImageByFile(
3392
3457
  {
@@ -3401,13 +3466,13 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3401
3466
  "rank" /* Rank */,
3402
3467
  languageTag
3403
3468
  );
3404
- await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], img);
3469
+ await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.channelId}`], img);
3405
3470
  } else {
3406
3471
  const message = this.ctx.dota2tracker.messageBuilder.buildRankChangedMessage(languageTag, name2, prevRank, currRank);
3407
- await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], message);
3472
+ await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.channelId}`], message);
3408
3473
  }
3409
3474
  this.ctx.dota2tracker.database.setPlayerRank(subPlayer.id, rankMap.get(subPlayer.steamId));
3410
- this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.rank_sent", { platform: subPlayer.platform, guildId: subPlayer.guildId, player: { nickName: subPlayer.nickName, steamId: subPlayer.steamId } }));
3475
+ this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.rank_sent", { platform: subPlayer.platform, guildId: subPlayer.channelId, player: { nickName: subPlayer.nickName, steamId: subPlayer.steamId } }));
3411
3476
  }
3412
3477
  } else {
3413
3478
  this.ctx.dota2tracker.database.setPlayerRank(subPlayer.id, rankMap.get(subPlayer.steamId));
@@ -3451,8 +3516,7 @@ var ParsePollingTask = class extends import_koishi14.Service {
3451
3516
  source: "AUTOMATIC",
3452
3517
  type: "CHANNEL",
3453
3518
  platform: target.platform,
3454
- channelId: target.channelId,
3455
- guildId: target.guildId
3519
+ channelId: target.channelId
3456
3520
  };
3457
3521
  }
3458
3522
  add(matchId, subscribers) {
@@ -3591,12 +3655,12 @@ var DailyReportTask = class extends import_koishi15.Service {
3591
3655
  }
3592
3656
  }
3593
3657
  async report_legacy(timeAgo, titleKey, showCombi) {
3594
- const subscribedGuilds = await this.ctx.database.get("dt_subscribed_guilds", void 0);
3595
- const subscribedPlayersInGuild = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedGuilds.some((guild) => guild.guildId == player.guildId));
3596
- const steamIds = subscribedPlayersInGuild.map((player) => player.steamId).filter((value, index, self) => self.indexOf(value) === index);
3658
+ const subscribedChannels = await this.ctx.database.get("dt_subscribed_guilds", void 0);
3659
+ const subscribedPlayersInChannel = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedChannels.some((ch) => ch.channelId == player.channelId));
3660
+ const steamIds = subscribedPlayersInChannel.map((player) => player.steamId).filter((value, index, self) => self.indexOf(value) === index);
3597
3661
  const players = (await this.ctx.dota2tracker.stratzAPI.queryPlayersMatchesForDaily_legacy(steamIds, timeAgo)).players.filter((player) => player.matches?.length > 0);
3598
3662
  const matches = players.map((player) => player.matches.map((match) => match)).flat().filter((item, index, self) => index === self.findIndex((t) => t.id === item.id));
3599
- for (let subPlayer of subscribedPlayersInGuild) {
3663
+ for (let subPlayer of subscribedPlayersInChannel) {
3600
3664
  let player = players.find((player2) => subPlayer.steamId == player2.steamAccount.id);
3601
3665
  if (!player) continue;
3602
3666
  let guildMember;
@@ -3615,8 +3679,8 @@ var DailyReportTask = class extends import_koishi15.Service {
3615
3679
  player.avgImp = roundToDecimalPlaces(player.matches.reduce((acc, match) => acc + match.players.find((innerPlayer) => innerPlayer.steamAccount.id == player.steamAccount.id).imp, 0) / player.matches.length, 0);
3616
3680
  subPlayer = Object.assign(subPlayer, player);
3617
3681
  }
3618
- for (let guild of subscribedGuilds) {
3619
- const currentsubscribedPlayers = subscribedPlayersInGuild.filter((player) => player.platform == guild.platform && player.guildId == guild.guildId && player.matches?.length);
3682
+ for (let channel of subscribedChannels) {
3683
+ const currentsubscribedPlayers = subscribedPlayersInChannel.filter((player) => player.platform == channel.platform && player.channelId == channel.channelId && player.matches?.length);
3620
3684
  if (currentsubscribedPlayers.length) {
3621
3685
  const currentsubscribedPlayersIds = currentsubscribedPlayers.map((player) => player.steamId);
3622
3686
  const combinationsMap = /* @__PURE__ */ new Map();
@@ -3642,9 +3706,9 @@ var DailyReportTask = class extends import_koishi15.Service {
3642
3706
  });
3643
3707
  const combinations = Array.from(combinationsMap.values());
3644
3708
  try {
3645
- const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: guild.guildId });
3709
+ const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: channel.channelId });
3646
3710
  await this.ctx.broadcast(
3647
- [`${guild.platform}:${guild.guildId}`],
3711
+ [`${channel.platform}:${channel.channelId}`],
3648
3712
  await this.ctx.dota2tracker.view.renderToImageByFile(
3649
3713
  {
3650
3714
  title: this.ctx.dota2tracker.i18n.$t(languageTag, titleKey),
@@ -3661,7 +3725,7 @@ var DailyReportTask = class extends import_koishi15.Service {
3661
3725
  languageTag
3662
3726
  )
3663
3727
  );
3664
- this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.report_sent", { title: this.ctx.dota2tracker.i18n.$t(languageTag, titleKey), guildId: guild.guildId, platform: guild.platform }));
3728
+ this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.report_sent", { title: this.ctx.dota2tracker.i18n.$t(languageTag, titleKey), guildId: channel.channelId, platform: channel.platform }));
3665
3729
  } catch (error) {
3666
3730
  this.logger.error(error);
3667
3731
  }
@@ -4025,7 +4089,7 @@ function registerUserCommand(ctx) {
4025
4089
  if (!session.isDirect) {
4026
4090
  const sessionPlayer = await ctx.dota2tracker.database.getBindedUser(session);
4027
4091
  if (sessionPlayer) {
4028
- await ctx.database.remove("dt_subscribed_players", sessionPlayer.id);
4092
+ ctx.dota2tracker.database.unbindUser(session);
4029
4093
  return session.text(".unbind_success");
4030
4094
  } else {
4031
4095
  return session.text(".not_binded");
@@ -4045,7 +4109,7 @@ function registerUserCommand(ctx) {
4045
4109
  if (nick_name === sessionPlayer.nickName) {
4046
4110
  return session.text(".nick_name_same");
4047
4111
  }
4048
- await ctx.database.set("dt_subscribed_players", sessionPlayer.id, { nickName: nick_name });
4112
+ await ctx.dota2tracker.database.renamePlayer(sessionPlayer.id, nick_name);
4049
4113
  return session.text(".rename_success", { nick_name });
4050
4114
  } else {
4051
4115
  return session.text(".not_binded");
@@ -4683,7 +4747,7 @@ var DailyReportService = class _DailyReportService extends import_koishi18.Servi
4683
4747
  static groupUsersByChannel(users) {
4684
4748
  const groups = /* @__PURE__ */ new Map();
4685
4749
  for (const user of users) {
4686
- const key = `${user.platform}:${user.guildId}`;
4750
+ const key = `${user.platform}:${user.channelId}`;
4687
4751
  if (!groups.has(key)) groups.set(key, []);
4688
4752
  groups.get(key).push(user);
4689
4753
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sjtdev/koishi-plugin-dota2tracker",
3
3
  "description": "koishi插件-追踪群友的DOTA2对局 | A Koishi plugin to track Dota 2 matches",
4
- "version": "2.5.3",
4
+ "version": "2.5.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [