koishi-plugin-adapter-onebot-multi 0.0.15 → 0.0.16

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.js CHANGED
@@ -2952,7 +2952,13 @@ var import_koishi11 = require("koishi");
2952
2952
  var LoadBalanceConfig = import_koishi11.Schema.object({
2953
2953
  enabled: import_koishi11.Schema.boolean().default(false).description("是否启用负载均衡。"),
2954
2954
  balanceInterval: import_koishi11.Schema.number().min(60).step(60).default(600).description("负载均衡间隔(秒)。"),
2955
- excludeChannels: import_koishi11.Schema.array(String).default([]).description("排除的群号列表。").role("table"),
2955
+ channelFilterMode: import_koishi11.Schema.union(["blacklist", "whitelist"]).default("blacklist").description("群过滤模式:blacklist=排除指定群,whitelist=仅管理指定群。"),
2956
+ channelBlacklist: import_koishi11.Schema.array(String).default([]).description("黑名单:排除的群号列表(blacklist 模式生效)。").role("table"),
2957
+ channelWhitelist: import_koishi11.Schema.array(String).default([]).description("白名单:仅管理的群号列表(whitelist 模式生效)。").role("table"),
2958
+ priorityChannels: import_koishi11.Schema.array(String).default([]).description("关键群列表:这些群会被优先分配,确保可用性。").role("table"),
2959
+ defaultMaxLoad: import_koishi11.Schema.number().min(0).default(0).description("默认 Bot 负载上限(0=不限制)。"),
2960
+ botMaxLoad: import_koishi11.Schema.dict(import_koishi11.Schema.number().min(0)).default({}).description('各 Bot 负载上限(格式: "botId": 数量,0=不限制)。'),
2961
+ unassignedValue: import_koishi11.Schema.string().default("").description('未分配群的 assignee 值(留空=不修改,设为"0"=无人负责)。'),
2956
2962
  channelBotPriority: import_koishi11.Schema.dict(import_koishi11.Schema.array(String)).default({}).description('群Bot优先级配置 (格式: "群号": ["botId1", "botId2"])')
2957
2963
  }).description("负载均衡");
2958
2964
  var LoadBalancer = class {
@@ -2999,10 +3005,35 @@ var LoadBalancer = class {
2999
3005
  return Array.from(this.ctx.bots).filter((b) => b.platform === "onebot" && b.status === import_koishi11.Universal.Status.ONLINE).map((b) => b.selfId);
3000
3006
  }
3001
3007
  /**
3002
- * 检查是否为排除的群
3008
+ * 检查群是否应该被管理(根据白名单/黑名单模式)
3003
3009
  */
3004
- isExcludedChannel(channelId) {
3005
- return this.config.excludeChannels?.includes(channelId) || false;
3010
+ shouldManageChannel(channelId) {
3011
+ const mode = this.config.channelFilterMode || "blacklist";
3012
+ if (mode === "whitelist") {
3013
+ const whitelist = this.config.channelWhitelist || [];
3014
+ return whitelist.length === 0 || whitelist.includes(channelId);
3015
+ } else {
3016
+ const blacklist = this.config.channelBlacklist || [];
3017
+ return !blacklist.includes(channelId);
3018
+ }
3019
+ }
3020
+ /**
3021
+ * 获取 Bot 的负载上限(0=不限制)
3022
+ */
3023
+ getBotMaxLoad(botId) {
3024
+ const specific = this.config.botMaxLoad?.[botId];
3025
+ if (specific !== void 0 && specific > 0) {
3026
+ return specific;
3027
+ }
3028
+ return this.config.defaultMaxLoad || 0;
3029
+ }
3030
+ /**
3031
+ * 检查 Bot 是否已达到负载上限
3032
+ */
3033
+ isBotAtCapacity(botId, currentLoad) {
3034
+ const maxLoad = this.getBotMaxLoad(botId);
3035
+ if (maxLoad === 0) return false;
3036
+ return currentLoad >= maxLoad;
3006
3037
  }
3007
3038
  /**
3008
3039
  * 设置事件监听
@@ -3151,9 +3182,19 @@ var LoadBalancer = class {
3151
3182
  return;
3152
3183
  }
3153
3184
  const channels = [];
3185
+ const prioritySet = new Set(this.config.priorityChannels || []);
3154
3186
  for (const [channelId, bots] of this.allChannels.entries()) {
3155
- channels.push({ channelId, availableBots: [...bots] });
3187
+ channels.push({
3188
+ channelId,
3189
+ availableBots: [...bots],
3190
+ isPriority: prioritySet.has(channelId)
3191
+ });
3156
3192
  }
3193
+ channels.sort((a, b) => {
3194
+ if (a.isPriority && !b.isPriority) return -1;
3195
+ if (!a.isPriority && b.isPriority) return 1;
3196
+ return 0;
3197
+ });
3157
3198
  const botLoad = /* @__PURE__ */ new Map();
3158
3199
  const botRemainingCapacity = /* @__PURE__ */ new Map();
3159
3200
  for (const botId of onlineBots) {
@@ -3169,34 +3210,48 @@ var LoadBalancer = class {
3169
3210
  }
3170
3211
  const assignments = [];
3171
3212
  for (const channel of channels) {
3172
- if (this.isExcludedChannel(channel.channelId)) continue;
3213
+ if (!this.shouldManageChannel(channel.channelId)) continue;
3173
3214
  const validBots = channel.availableBots.filter((botId) => onlineBotsSet.has(botId));
3174
3215
  if (validBots.length === 0) {
3175
3216
  this.logger.warn(`群 ${channel.channelId} 没有可用的 Bot`);
3176
3217
  continue;
3177
3218
  }
3178
- let selectedBot;
3219
+ let selectedBot = null;
3179
3220
  const priorityList = this.config.channelBotPriority?.[channel.channelId];
3180
3221
  if (priorityList && priorityList.length > 0) {
3181
- let found = false;
3182
3222
  for (const priorityBotId of priorityList) {
3183
3223
  if (validBots.includes(priorityBotId)) {
3184
- selectedBot = priorityBotId;
3185
- found = true;
3186
- break;
3224
+ const load = botLoad.get(priorityBotId) || 0;
3225
+ if (!this.isBotAtCapacity(priorityBotId, load)) {
3226
+ selectedBot = priorityBotId;
3227
+ break;
3228
+ }
3187
3229
  }
3188
3230
  }
3189
- if (!found) {
3231
+ if (!selectedBot) {
3190
3232
  selectedBot = this.selectBotByGreedy(validBots, botRemainingCapacity, botLoad);
3191
3233
  }
3192
3234
  } else {
3193
3235
  selectedBot = this.selectBotByGreedy(validBots, botRemainingCapacity, botLoad);
3194
3236
  }
3237
+ if (!selectedBot) {
3238
+ const unassignedValue = this.config.unassignedValue;
3239
+ if (unassignedValue !== void 0 && unassignedValue !== "") {
3240
+ const lastAssignee2 = this.lastAssignees.get(channel.channelId);
3241
+ if (lastAssignee2 !== unassignedValue) {
3242
+ assignments.push({ channelId: channel.channelId, assignee: unassignedValue });
3243
+ this.logger.info(`群 ${channel.channelId} 所有 Bot 达到上限,设为未分配`);
3244
+ }
3245
+ } else {
3246
+ this.logger.warn(`群 ${channel.channelId} 所有可用 Bot 都已达到负载上限`);
3247
+ }
3248
+ continue;
3249
+ }
3195
3250
  const lastAssignee = this.lastAssignees.get(channel.channelId);
3196
3251
  if (lastAssignee !== selectedBot) {
3197
3252
  assignments.push({ channelId: channel.channelId, assignee: selectedBot });
3198
- botLoad.set(selectedBot, (botLoad.get(selectedBot) || 0) + 1);
3199
3253
  }
3254
+ botLoad.set(selectedBot, (botLoad.get(selectedBot) || 0) + 1);
3200
3255
  for (const botId of validBots) {
3201
3256
  botRemainingCapacity.set(botId, (botRemainingCapacity.get(botId) || 0) - 1);
3202
3257
  }
@@ -3218,13 +3273,14 @@ var LoadBalancer = class {
3218
3273
  const channels2 = this.botChannels.get(id);
3219
3274
  return channels2 && channels2.size > 0;
3220
3275
  });
3276
+ const priorityCount = channels.filter((c) => c.isPriority && this.shouldManageChannel(c.channelId)).length;
3221
3277
  if (botsWithData.length > 0) {
3222
3278
  const loads = botsWithData.map((id) => botLoad.get(id) || 0);
3223
3279
  const avg = loads.reduce((a, b) => a + b, 0) / loads.length;
3224
3280
  const variance = loads.reduce((sum, load) => sum + Math.pow(load - avg, 2), 0) / loads.length;
3225
3281
  const stdDev = Math.sqrt(variance);
3226
3282
  const loadInfo = botsWithData.map((id) => `${id}:${botLoad.get(id) || 0}`).join(", ");
3227
- this.logger.info(`负载均衡完成,分配了 ${assignments.length} 个群`);
3283
+ this.logger.info(`负载均衡完成,分配了 ${assignments.length} 个群(关键群: ${priorityCount} 个)`);
3228
3284
  this.logger.info(`负载分布: [${loadInfo}],平均: ${avg.toFixed(1)},标准差: ${stdDev.toFixed(2)}`);
3229
3285
  }
3230
3286
  } catch (e) {
@@ -3234,13 +3290,20 @@ var LoadBalancer = class {
3234
3290
  }
3235
3291
  }
3236
3292
  /**
3237
- * 贪心选择 bot
3293
+ * 贪心选择 bot(考虑负载上限)
3238
3294
  */
3239
3295
  selectBotByGreedy(validBots, capacityMap, loadMap) {
3240
- let selectedBot = validBots[0];
3296
+ const availableBots = validBots.filter((botId) => {
3297
+ const load = loadMap.get(botId) || 0;
3298
+ return !this.isBotAtCapacity(botId, load);
3299
+ });
3300
+ if (availableBots.length === 0) {
3301
+ return null;
3302
+ }
3303
+ let selectedBot = availableBots[0];
3241
3304
  let minCapacity = capacityMap.get(selectedBot) || 0;
3242
3305
  let minLoad = loadMap.get(selectedBot) || 0;
3243
- for (const botId of validBots) {
3306
+ for (const botId of availableBots) {
3244
3307
  const capacity = capacityMap.get(botId) || 0;
3245
3308
  const load = loadMap.get(botId) || 0;
3246
3309
  if (capacity < minCapacity || capacity === minCapacity && load < minLoad) {
@@ -3,7 +3,13 @@ import { StatusManager } from './status';
3
3
  export interface LoadBalanceConfig {
4
4
  enabled?: boolean;
5
5
  balanceInterval?: number;
6
- excludeChannels?: string[];
6
+ channelFilterMode?: 'blacklist' | 'whitelist';
7
+ channelBlacklist?: string[];
8
+ channelWhitelist?: string[];
9
+ botMaxLoad?: Record<string, number>;
10
+ defaultMaxLoad?: number;
11
+ unassignedValue?: string;
12
+ priorityChannels?: string[];
7
13
  channelBotPriority?: Record<string, string[]>;
8
14
  }
9
15
  export declare const LoadBalanceConfig: Schema<LoadBalanceConfig>;
@@ -28,9 +34,17 @@ export declare class LoadBalancer {
28
34
  */
29
35
  private getOnlineBots;
30
36
  /**
31
- * 检查是否为排除的群
37
+ * 检查群是否应该被管理(根据白名单/黑名单模式)
32
38
  */
33
- private isExcludedChannel;
39
+ private shouldManageChannel;
40
+ /**
41
+ * 获取 Bot 的负载上限(0=不限制)
42
+ */
43
+ private getBotMaxLoad;
44
+ /**
45
+ * 检查 Bot 是否已达到负载上限
46
+ */
47
+ private isBotAtCapacity;
34
48
  /**
35
49
  * 设置事件监听
36
50
  */
@@ -52,7 +66,7 @@ export declare class LoadBalancer {
52
66
  */
53
67
  performLoadBalancing(): Promise<void>;
54
68
  /**
55
- * 贪心选择 bot
69
+ * 贪心选择 bot(考虑负载上限)
56
70
  */
57
71
  private selectBotByGreedy;
58
72
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-adapter-onebot-multi",
3
3
  "description": "奶龙bot定制版onebot适配器,支持自动负载均衡,适配器级黑名单/白名单,提供webui,可指定端口",
4
- "version": "0.0.15",
4
+ "version": "0.0.16",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [