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 +80 -17
- package/lib/load-balancer.d.ts +18 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
3005
|
-
|
|
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({
|
|
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.
|
|
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
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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) {
|
package/lib/load-balancer.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { StatusManager } from './status';
|
|
|
3
3
|
export interface LoadBalanceConfig {
|
|
4
4
|
enabled?: boolean;
|
|
5
5
|
balanceInterval?: number;
|
|
6
|
-
|
|
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
|
|
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