koishi-plugin-adapter-onebot-multi 0.0.14 → 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.
@@ -11,6 +11,8 @@ export declare class HeartbeatMonitor {
11
11
  private timer;
12
12
  private logger;
13
13
  private lastOnlineState;
14
+ private consecutiveFailures;
15
+ private readonly maxLoggedFailures;
14
16
  constructor(ctx: Context, bot: OneBotBot, interval: number);
15
17
  start(): void;
16
18
  stop(): void;
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Context, Schema } from 'koishi';
2
2
  import { OneBotBot } from './bot';
3
3
  import { BaseBot } from './bot/base';
4
4
  import { PanelConfig } from './panel';
5
+ import { LoadBalanceConfig } from './load-balancer';
5
6
  import * as OneBot from './utils';
6
7
  declare module '@koishijs/console' {
7
8
  interface Events {
@@ -19,6 +20,7 @@ export * from './heartbeat';
19
20
  export * from './status';
20
21
  export * from './panel';
21
22
  export * from './config-manager';
23
+ export * from './load-balancer';
22
24
  export interface BotConfig {
23
25
  selfId: string;
24
26
  token?: string;
@@ -34,6 +36,7 @@ export interface Config {
34
36
  retryLazy?: number;
35
37
  advanced?: BaseBot.AdvancedConfig;
36
38
  panel?: PanelConfig;
39
+ loadBalance?: LoadBalanceConfig;
37
40
  }
38
41
  export declare const Config: Schema<Config>;
39
42
  export declare const name = "adapter-onebot-multi";
package/lib/index.js CHANGED
@@ -35,6 +35,8 @@ __export(src_exports, {
35
35
  Config: () => Config,
36
36
  ConfigManager: () => ConfigManager,
37
37
  HeartbeatMonitor: () => HeartbeatMonitor,
38
+ LoadBalanceConfig: () => LoadBalanceConfig,
39
+ LoadBalancer: () => LoadBalancer,
38
40
  OneBot: () => utils_exports,
39
41
  OneBotBot: () => OneBotBot,
40
42
  OneBotMessageEncoder: () => OneBotMessageEncoder,
@@ -51,7 +53,7 @@ __export(src_exports, {
51
53
  name: () => name
52
54
  });
53
55
  module.exports = __toCommonJS(src_exports);
54
- var import_koishi11 = require("koishi");
56
+ var import_koishi12 = require("koishi");
55
57
  var import_path = require("path");
56
58
 
57
59
  // src/bot/index.ts
@@ -976,6 +978,7 @@ function accept(socket, bot) {
976
978
  socket.addEventListener("close", () => {
977
979
  delete bot.internal._request;
978
980
  bot.offline();
981
+ bot.ctx.emit("onebot-multi/bot-offline", bot);
979
982
  });
980
983
  bot.internal._request = (action, params) => {
981
984
  const data = { action, params, echo: ++counter };
@@ -996,6 +999,7 @@ __name(accept, "accept");
996
999
  // src/heartbeat.ts
997
1000
  var import_koishi6 = require("koishi");
998
1001
  var HeartbeatMonitor = class {
1002
+ // 连续失败超过此次数后降低日志级别
999
1003
  constructor(ctx, bot, interval) {
1000
1004
  this.ctx = ctx;
1001
1005
  this.bot = bot;
@@ -1008,9 +1012,12 @@ var HeartbeatMonitor = class {
1008
1012
  timer = null;
1009
1013
  logger;
1010
1014
  lastOnlineState = null;
1015
+ consecutiveFailures = 0;
1016
+ maxLoggedFailures = 3;
1011
1017
  start() {
1012
1018
  if (this.timer) return;
1013
1019
  this.logger.debug(`Bot ${this.bot.selfId} 心跳检测已启动,间隔 ${this.interval}ms`);
1020
+ this.consecutiveFailures = 0;
1014
1021
  this.check();
1015
1022
  this.timer = setInterval(() => this.check(), this.interval);
1016
1023
  this.ctx.on("dispose", () => this.stop());
@@ -1023,9 +1030,17 @@ var HeartbeatMonitor = class {
1023
1030
  }
1024
1031
  }
1025
1032
  async check() {
1033
+ if (!this.bot.internal._request) {
1034
+ if (this.consecutiveFailures === 0) {
1035
+ this.logger.debug(`Bot ${this.bot.selfId} WebSocket 未连接,跳过心跳检测`);
1036
+ }
1037
+ this.consecutiveFailures++;
1038
+ return;
1039
+ }
1026
1040
  try {
1027
1041
  const status = await this.bot.internal.getStatus();
1028
1042
  const isOnline = status?.online ?? false;
1043
+ this.consecutiveFailures = 0;
1029
1044
  if (this.lastOnlineState === isOnline) return;
1030
1045
  this.logger.info(`Bot ${this.bot.selfId} 状态变化: ${this.lastOnlineState} -> ${isOnline}`);
1031
1046
  this.lastOnlineState = isOnline;
@@ -1041,7 +1056,12 @@ var HeartbeatMonitor = class {
1041
1056
  }
1042
1057
  }
1043
1058
  } catch (error) {
1044
- this.logger.warn(`Bot ${this.bot.selfId} 心跳检测失败:`, error);
1059
+ this.consecutiveFailures++;
1060
+ if (this.consecutiveFailures <= this.maxLoggedFailures) {
1061
+ this.logger.warn(`Bot ${this.bot.selfId} 心跳检测失败 (${this.consecutiveFailures}/${this.maxLoggedFailures}):`, error.message);
1062
+ } else if (this.consecutiveFailures === this.maxLoggedFailures + 1) {
1063
+ this.logger.warn(`Bot ${this.bot.selfId} 心跳检测持续失败,后续失败将不再记录`);
1064
+ }
1045
1065
  if (this.lastOnlineState !== false) {
1046
1066
  this.lastOnlineState = false;
1047
1067
  if (this.bot.status === import_koishi6.Universal.Status.ONLINE) {
@@ -1530,7 +1550,8 @@ var StatusPanel = class {
1530
1550
  lastMessageTime: runtime?.lastMessageTime,
1531
1551
  startupTime: runtime?.startupTime,
1532
1552
  groupCount: runtime?.groupCount,
1533
- friendCount: runtime?.friendCount
1553
+ friendCount: runtime?.friendCount,
1554
+ lastHeartbeat: runtime?.lastHeartbeat
1534
1555
  };
1535
1556
  });
1536
1557
  ctx.body = { bots, updatedAt: Date.now() };
@@ -2926,26 +2947,438 @@ var ConfigManager = class {
2926
2947
  }
2927
2948
  };
2928
2949
 
2950
+ // src/load-balancer.ts
2951
+ var import_koishi11 = require("koishi");
2952
+ var LoadBalanceConfig = import_koishi11.Schema.object({
2953
+ enabled: import_koishi11.Schema.boolean().default(false).description("是否启用负载均衡。"),
2954
+ balanceInterval: import_koishi11.Schema.number().min(60).step(60).default(600).description("负载均衡间隔(秒)。"),
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"=无人负责)。'),
2962
+ channelBotPriority: import_koishi11.Schema.dict(import_koishi11.Schema.array(String)).default({}).description('群Bot优先级配置 (格式: "群号": ["botId1", "botId2"])')
2963
+ }).description("负载均衡");
2964
+ var LoadBalancer = class {
2965
+ constructor(ctx, config, statusManager) {
2966
+ this.ctx = ctx;
2967
+ this.config = config;
2968
+ this.statusManager = statusManager;
2969
+ if (!config.enabled) {
2970
+ this.ctx.logger("load-balancer").info("负载均衡未启用");
2971
+ return;
2972
+ }
2973
+ this.ctx.logger("load-balancer").info("负载均衡模块初始化中...");
2974
+ this.setupEventListeners();
2975
+ this.ctx.on("ready", () => this.initialize());
2976
+ this.ctx.on("dispose", () => this.dispose());
2977
+ }
2978
+ static {
2979
+ __name(this, "LoadBalancer");
2980
+ }
2981
+ // 核心数据结构
2982
+ allChannels = /* @__PURE__ */ new Map();
2983
+ // channelId -> Set<selfId>
2984
+ botChannels = /* @__PURE__ */ new Map();
2985
+ // selfId -> Set<channelId>
2986
+ lastAssignees = /* @__PURE__ */ new Map();
2987
+ // channelId -> 最近分配的 assignee
2988
+ balanceTimer = null;
2989
+ balancePending = null;
2990
+ isBalancing = false;
2991
+ get logger() {
2992
+ return this.ctx.logger("load-balancer");
2993
+ }
2994
+ /**
2995
+ * 检查 Bot 是否在线
2996
+ */
2997
+ isBotOnline(selfId) {
2998
+ const bot = this.ctx.bots.find((b) => b.selfId === selfId && b.platform === "onebot");
2999
+ return bot?.status === import_koishi11.Universal.Status.ONLINE;
3000
+ }
3001
+ /**
3002
+ * 获取所有在线的 OneBot
3003
+ */
3004
+ getOnlineBots() {
3005
+ return Array.from(this.ctx.bots).filter((b) => b.platform === "onebot" && b.status === import_koishi11.Universal.Status.ONLINE).map((b) => b.selfId);
3006
+ }
3007
+ /**
3008
+ * 检查群是否应该被管理(根据白名单/黑名单模式)
3009
+ */
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;
3037
+ }
3038
+ /**
3039
+ * 设置事件监听
3040
+ */
3041
+ setupEventListeners() {
3042
+ this.ctx.on("onebot-multi/bot-online", async (bot) => {
3043
+ this.logger.info(`Bot ${bot.selfId} 上线,开始获取群列表...`);
3044
+ await this.fetchBotGuilds(bot);
3045
+ this.scheduleLoadBalancing(5e3);
3046
+ });
3047
+ this.ctx.on("onebot-multi/bot-offline", (bot) => {
3048
+ const channelIds = this.botChannels.get(bot.selfId) || /* @__PURE__ */ new Set();
3049
+ for (const channelId of channelIds) {
3050
+ const bots = this.allChannels.get(channelId);
3051
+ if (bots) {
3052
+ bots.delete(bot.selfId);
3053
+ if (bots.size === 0) {
3054
+ this.allChannels.delete(channelId);
3055
+ this.lastAssignees.delete(channelId);
3056
+ }
3057
+ }
3058
+ }
3059
+ this.botChannels.delete(bot.selfId);
3060
+ this.logger.info(`Bot ${bot.selfId} 下线,已清理 ${channelIds.size} 个群的映射`);
3061
+ if (channelIds.size > 0) {
3062
+ this.scheduleLoadBalancing(5e3);
3063
+ }
3064
+ });
3065
+ this.ctx.on("guild-added", (session) => {
3066
+ if (session.platform !== "onebot") return;
3067
+ const botChannelSet = this.botChannels.get(session.selfId) || /* @__PURE__ */ new Set();
3068
+ const isNewChannel = !botChannelSet.has(session.guildId);
3069
+ botChannelSet.add(session.guildId);
3070
+ this.botChannels.set(session.selfId, botChannelSet);
3071
+ const channelBots = this.allChannels.get(session.guildId) || /* @__PURE__ */ new Set();
3072
+ channelBots.add(session.selfId);
3073
+ this.allChannels.set(session.guildId, channelBots);
3074
+ this.logger.info(`Bot ${session.selfId} 加入群 ${session.guildId}`);
3075
+ if (isNewChannel) {
3076
+ this.scheduleLoadBalancing(1e4);
3077
+ }
3078
+ });
3079
+ this.ctx.on("guild-removed", (session) => {
3080
+ if (session.platform !== "onebot") return;
3081
+ const botChannelSet = this.botChannels.get(session.selfId);
3082
+ if (botChannelSet) {
3083
+ botChannelSet.delete(session.guildId);
3084
+ }
3085
+ const channelBots = this.allChannels.get(session.guildId);
3086
+ const hadOtherBots = channelBots && channelBots.size > 1;
3087
+ if (channelBots) {
3088
+ channelBots.delete(session.selfId);
3089
+ if (channelBots.size === 0) {
3090
+ this.allChannels.delete(session.guildId);
3091
+ this.lastAssignees.delete(session.guildId);
3092
+ }
3093
+ }
3094
+ this.logger.info(`Bot ${session.selfId} 退出群 ${session.guildId}`);
3095
+ if (hadOtherBots) {
3096
+ this.scheduleLoadBalancing(5e3);
3097
+ }
3098
+ });
3099
+ }
3100
+ /**
3101
+ * 初始化
3102
+ */
3103
+ async initialize() {
3104
+ this.logger.info("初始化负载均衡...");
3105
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
3106
+ const allBots = Array.from(this.ctx.bots);
3107
+ this.logger.info(`当前共 ${allBots.length} 个 Bot 实例`);
3108
+ const onlineBots = allBots.filter(
3109
+ (b) => b.platform === "onebot" && b.status === import_koishi11.Universal.Status.ONLINE
3110
+ );
3111
+ this.logger.info(`其中 ${onlineBots.length} 个 OneBot 在线: ${onlineBots.map((b) => b.selfId).join(", ") || "无"}`);
3112
+ const maxConcurrency = 5;
3113
+ for (let i = 0; i < onlineBots.length; i += maxConcurrency) {
3114
+ const batch = onlineBots.slice(i, i + maxConcurrency);
3115
+ await Promise.all(batch.map((bot) => this.fetchBotGuilds(bot)));
3116
+ }
3117
+ this.logger.info(`启动负载均衡定时器,间隔: ${this.config.balanceInterval}秒`);
3118
+ this.balanceTimer = setInterval(
3119
+ () => this.performLoadBalancing(),
3120
+ (this.config.balanceInterval || 600) * 1e3
3121
+ );
3122
+ await this.performLoadBalancing();
3123
+ }
3124
+ /**
3125
+ * 获取 Bot 的群列表
3126
+ */
3127
+ async fetchBotGuilds(bot, retries = 3) {
3128
+ for (let i = 0; i < retries; i++) {
3129
+ try {
3130
+ if (i > 0) {
3131
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3 * i));
3132
+ this.logger.info(`Bot ${bot.selfId} 重试获取群列表 (${i}/${retries})...`);
3133
+ }
3134
+ const guilds = await bot.getGuildList();
3135
+ const channelIds = new Set(
3136
+ Array.from(guilds.data || []).map((g) => g.id)
3137
+ );
3138
+ this.botChannels.set(bot.selfId, channelIds);
3139
+ for (const channelId of channelIds) {
3140
+ const bots = this.allChannels.get(channelId) || /* @__PURE__ */ new Set();
3141
+ bots.add(bot.selfId);
3142
+ this.allChannels.set(channelId, bots);
3143
+ }
3144
+ this.logger.info(`Bot ${bot.selfId} 在 ${channelIds.size} 个群中`);
3145
+ return channelIds;
3146
+ } catch (e) {
3147
+ if (i === retries - 1) {
3148
+ this.logger.error(`Bot ${bot.selfId} 获取群列表失败: ${e.message}`);
3149
+ return null;
3150
+ }
3151
+ }
3152
+ }
3153
+ return null;
3154
+ }
3155
+ /**
3156
+ * 延迟触发负载均衡(防抖)
3157
+ */
3158
+ scheduleLoadBalancing(delay = 5e3) {
3159
+ if (this.balancePending) {
3160
+ clearTimeout(this.balancePending);
3161
+ }
3162
+ this.balancePending = setTimeout(() => {
3163
+ this.balancePending = null;
3164
+ this.performLoadBalancing();
3165
+ }, delay);
3166
+ }
3167
+ /**
3168
+ * 执行负载均衡 - 动态容量约束的贪心算法
3169
+ */
3170
+ async performLoadBalancing() {
3171
+ if (this.isBalancing) {
3172
+ this.logger.debug("负载均衡正在执行中,跳过");
3173
+ return;
3174
+ }
3175
+ this.isBalancing = true;
3176
+ try {
3177
+ this.logger.info("执行负载均衡...");
3178
+ const onlineBots = this.getOnlineBots();
3179
+ const onlineBotsSet = new Set(onlineBots);
3180
+ if (onlineBots.length === 0) {
3181
+ this.logger.warn("没有在线的 Bot");
3182
+ return;
3183
+ }
3184
+ const channels = [];
3185
+ const prioritySet = new Set(this.config.priorityChannels || []);
3186
+ for (const [channelId, bots] of this.allChannels.entries()) {
3187
+ channels.push({
3188
+ channelId,
3189
+ availableBots: [...bots],
3190
+ isPriority: prioritySet.has(channelId)
3191
+ });
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
+ });
3198
+ const botLoad = /* @__PURE__ */ new Map();
3199
+ const botRemainingCapacity = /* @__PURE__ */ new Map();
3200
+ for (const botId of onlineBots) {
3201
+ botLoad.set(botId, 0);
3202
+ botRemainingCapacity.set(botId, 0);
3203
+ }
3204
+ for (const { availableBots } of channels) {
3205
+ for (const botId of availableBots) {
3206
+ if (onlineBotsSet.has(botId)) {
3207
+ botRemainingCapacity.set(botId, (botRemainingCapacity.get(botId) || 0) + 1);
3208
+ }
3209
+ }
3210
+ }
3211
+ const assignments = [];
3212
+ for (const channel of channels) {
3213
+ if (!this.shouldManageChannel(channel.channelId)) continue;
3214
+ const validBots = channel.availableBots.filter((botId) => onlineBotsSet.has(botId));
3215
+ if (validBots.length === 0) {
3216
+ this.logger.warn(`群 ${channel.channelId} 没有可用的 Bot`);
3217
+ continue;
3218
+ }
3219
+ let selectedBot = null;
3220
+ const priorityList = this.config.channelBotPriority?.[channel.channelId];
3221
+ if (priorityList && priorityList.length > 0) {
3222
+ for (const priorityBotId of priorityList) {
3223
+ if (validBots.includes(priorityBotId)) {
3224
+ const load = botLoad.get(priorityBotId) || 0;
3225
+ if (!this.isBotAtCapacity(priorityBotId, load)) {
3226
+ selectedBot = priorityBotId;
3227
+ break;
3228
+ }
3229
+ }
3230
+ }
3231
+ if (!selectedBot) {
3232
+ selectedBot = this.selectBotByGreedy(validBots, botRemainingCapacity, botLoad);
3233
+ }
3234
+ } else {
3235
+ selectedBot = this.selectBotByGreedy(validBots, botRemainingCapacity, botLoad);
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
+ }
3250
+ const lastAssignee = this.lastAssignees.get(channel.channelId);
3251
+ if (lastAssignee !== selectedBot) {
3252
+ assignments.push({ channelId: channel.channelId, assignee: selectedBot });
3253
+ }
3254
+ botLoad.set(selectedBot, (botLoad.get(selectedBot) || 0) + 1);
3255
+ for (const botId of validBots) {
3256
+ botRemainingCapacity.set(botId, (botRemainingCapacity.get(botId) || 0) - 1);
3257
+ }
3258
+ }
3259
+ if (assignments.length > 0) {
3260
+ const batchSize = 100;
3261
+ for (let i = 0; i < assignments.length; i += batchSize) {
3262
+ const slice = assignments.slice(i, i + batchSize);
3263
+ await Promise.all(
3264
+ slice.map(
3265
+ ({ channelId, assignee }) => this.ctx.database.setChannel("onebot", channelId, { assignee }).then(() => {
3266
+ this.lastAssignees.set(channelId, assignee);
3267
+ })
3268
+ )
3269
+ );
3270
+ }
3271
+ }
3272
+ const botsWithData = onlineBots.filter((id) => {
3273
+ const channels2 = this.botChannels.get(id);
3274
+ return channels2 && channels2.size > 0;
3275
+ });
3276
+ const priorityCount = channels.filter((c) => c.isPriority && this.shouldManageChannel(c.channelId)).length;
3277
+ if (botsWithData.length > 0) {
3278
+ const loads = botsWithData.map((id) => botLoad.get(id) || 0);
3279
+ const avg = loads.reduce((a, b) => a + b, 0) / loads.length;
3280
+ const variance = loads.reduce((sum, load) => sum + Math.pow(load - avg, 2), 0) / loads.length;
3281
+ const stdDev = Math.sqrt(variance);
3282
+ const loadInfo = botsWithData.map((id) => `${id}:${botLoad.get(id) || 0}`).join(", ");
3283
+ this.logger.info(`负载均衡完成,分配了 ${assignments.length} 个群(关键群: ${priorityCount} 个)`);
3284
+ this.logger.info(`负载分布: [${loadInfo}],平均: ${avg.toFixed(1)},标准差: ${stdDev.toFixed(2)}`);
3285
+ }
3286
+ } catch (e) {
3287
+ this.logger.error(`负载均衡失败: ${e.message}`);
3288
+ } finally {
3289
+ this.isBalancing = false;
3290
+ }
3291
+ }
3292
+ /**
3293
+ * 贪心选择 bot(考虑负载上限)
3294
+ */
3295
+ selectBotByGreedy(validBots, capacityMap, loadMap) {
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];
3304
+ let minCapacity = capacityMap.get(selectedBot) || 0;
3305
+ let minLoad = loadMap.get(selectedBot) || 0;
3306
+ for (const botId of availableBots) {
3307
+ const capacity = capacityMap.get(botId) || 0;
3308
+ const load = loadMap.get(botId) || 0;
3309
+ if (capacity < minCapacity || capacity === minCapacity && load < minLoad) {
3310
+ minCapacity = capacity;
3311
+ minLoad = load;
3312
+ selectedBot = botId;
3313
+ }
3314
+ }
3315
+ return selectedBot;
3316
+ }
3317
+ /**
3318
+ * 销毁
3319
+ */
3320
+ dispose() {
3321
+ if (this.balanceTimer) {
3322
+ clearInterval(this.balanceTimer);
3323
+ this.balanceTimer = null;
3324
+ }
3325
+ if (this.balancePending) {
3326
+ clearTimeout(this.balancePending);
3327
+ this.balancePending = null;
3328
+ }
3329
+ }
3330
+ /**
3331
+ * 获取负载均衡状态(供 API 使用)
3332
+ */
3333
+ getStatus() {
3334
+ const onlineBots = this.getOnlineBots();
3335
+ const botStats = {};
3336
+ for (const botId of onlineBots) {
3337
+ const channels = this.botChannels.get(botId) || /* @__PURE__ */ new Set();
3338
+ let assignedCount = 0;
3339
+ for (const [channelId, assignee] of this.lastAssignees.entries()) {
3340
+ if (assignee === botId) {
3341
+ assignedCount++;
3342
+ }
3343
+ }
3344
+ botStats[botId] = {
3345
+ channelCount: channels.size,
3346
+ assignedCount
3347
+ };
3348
+ }
3349
+ return {
3350
+ enabled: this.config.enabled,
3351
+ totalChannels: this.allChannels.size,
3352
+ onlineBots: onlineBots.length,
3353
+ botStats
3354
+ };
3355
+ }
3356
+ };
3357
+
2929
3358
  // src/index.ts
2930
- var Config = import_koishi11.Schema.intersect([
3359
+ var Config = import_koishi12.Schema.intersect([
2931
3360
  // 全局连接设置
2932
- import_koishi11.Schema.object({
2933
- responseTimeout: import_koishi11.Schema.natural().role("time").default(import_koishi11.Time.minute).description("等待响应的时间(毫秒)。"),
2934
- heartbeatInterval: import_koishi11.Schema.natural().role("ms").default(30 * import_koishi11.Time.second).description("心跳检测间隔(毫秒),设为 0 禁用。")
3361
+ import_koishi12.Schema.object({
3362
+ responseTimeout: import_koishi12.Schema.natural().role("time").default(import_koishi12.Time.minute).description("等待响应的时间(毫秒)。"),
3363
+ heartbeatInterval: import_koishi12.Schema.natural().role("ms").default(30 * import_koishi12.Time.second).description("心跳检测间隔(毫秒),设为 0 禁用。")
2935
3364
  }).description("连接设置"),
2936
3365
  // 全局重连设置(折叠)
2937
- import_koishi11.Schema.object({
2938
- retryTimes: import_koishi11.Schema.natural().description("初次连接时的最大重试次数。").default(6),
2939
- retryInterval: import_koishi11.Schema.natural().role("ms").description("初次连接时的重试时间间隔。").default(5 * import_koishi11.Time.second),
2940
- retryLazy: import_koishi11.Schema.natural().role("ms").description("连接关闭后的重试时间间隔。").default(import_koishi11.Time.minute)
3366
+ import_koishi12.Schema.object({
3367
+ retryTimes: import_koishi12.Schema.natural().description("初次连接时的最大重试次数。").default(6),
3368
+ retryInterval: import_koishi12.Schema.natural().role("ms").description("初次连接时的重试时间间隔。").default(5 * import_koishi12.Time.second),
3369
+ retryLazy: import_koishi12.Schema.natural().role("ms").description("连接关闭后的重试时间间隔。").default(import_koishi12.Time.minute)
2941
3370
  }).description("重连设置").collapse(),
2942
3371
  // 全局高级设置(折叠)
2943
- import_koishi11.Schema.object({
3372
+ import_koishi12.Schema.object({
2944
3373
  advanced: BaseBot2.AdvancedConfig
2945
3374
  }).description("高级设置").collapse(),
2946
3375
  // 展示面板设置(折叠)
2947
- import_koishi11.Schema.object({
3376
+ import_koishi12.Schema.object({
2948
3377
  panel: PanelConfig
3378
+ }).collapse(),
3379
+ // 负载均衡设置(折叠)
3380
+ import_koishi12.Schema.object({
3381
+ loadBalance: LoadBalanceConfig
2949
3382
  }).collapse()
2950
3383
  ]);
2951
3384
  var name = "adapter-onebot-multi";
@@ -2969,6 +3402,10 @@ function apply(ctx, config) {
2969
3402
  if (config.panel?.enabled) {
2970
3403
  new StatusPanel(ctx, config.panel, statusManager, configManager);
2971
3404
  }
3405
+ if (config.loadBalance?.enabled) {
3406
+ new LoadBalancer(ctx, config.loadBalance, statusManager);
3407
+ logger.info("负载均衡已启用");
3408
+ }
2972
3409
  ctx.inject(["console"], (ctx2) => {
2973
3410
  ctx2.console.addEntry({
2974
3411
  dev: (0, import_path.resolve)(__dirname, "../client/index.ts"),
@@ -3034,6 +3471,8 @@ __name(startBot, "startBot");
3034
3471
  Config,
3035
3472
  ConfigManager,
3036
3473
  HeartbeatMonitor,
3474
+ LoadBalanceConfig,
3475
+ LoadBalancer,
3037
3476
  OneBot,
3038
3477
  OneBotBot,
3039
3478
  OneBotMessageEncoder,
@@ -0,0 +1,88 @@
1
+ import { Context, Schema } from 'koishi';
2
+ import { StatusManager } from './status';
3
+ export interface LoadBalanceConfig {
4
+ enabled?: boolean;
5
+ balanceInterval?: number;
6
+ channelFilterMode?: 'blacklist' | 'whitelist';
7
+ channelBlacklist?: string[];
8
+ channelWhitelist?: string[];
9
+ botMaxLoad?: Record<string, number>;
10
+ defaultMaxLoad?: number;
11
+ unassignedValue?: string;
12
+ priorityChannels?: string[];
13
+ channelBotPriority?: Record<string, string[]>;
14
+ }
15
+ export declare const LoadBalanceConfig: Schema<LoadBalanceConfig>;
16
+ export declare class LoadBalancer {
17
+ private ctx;
18
+ private config;
19
+ private statusManager;
20
+ private allChannels;
21
+ private botChannels;
22
+ private lastAssignees;
23
+ private balanceTimer;
24
+ private balancePending;
25
+ private isBalancing;
26
+ constructor(ctx: Context, config: LoadBalanceConfig, statusManager: StatusManager);
27
+ private get logger();
28
+ /**
29
+ * 检查 Bot 是否在线
30
+ */
31
+ private isBotOnline;
32
+ /**
33
+ * 获取所有在线的 OneBot
34
+ */
35
+ private getOnlineBots;
36
+ /**
37
+ * 检查群是否应该被管理(根据白名单/黑名单模式)
38
+ */
39
+ private shouldManageChannel;
40
+ /**
41
+ * 获取 Bot 的负载上限(0=不限制)
42
+ */
43
+ private getBotMaxLoad;
44
+ /**
45
+ * 检查 Bot 是否已达到负载上限
46
+ */
47
+ private isBotAtCapacity;
48
+ /**
49
+ * 设置事件监听
50
+ */
51
+ private setupEventListeners;
52
+ /**
53
+ * 初始化
54
+ */
55
+ private initialize;
56
+ /**
57
+ * 获取 Bot 的群列表
58
+ */
59
+ private fetchBotGuilds;
60
+ /**
61
+ * 延迟触发负载均衡(防抖)
62
+ */
63
+ private scheduleLoadBalancing;
64
+ /**
65
+ * 执行负载均衡 - 动态容量约束的贪心算法
66
+ */
67
+ performLoadBalancing(): Promise<void>;
68
+ /**
69
+ * 贪心选择 bot(考虑负载上限)
70
+ */
71
+ private selectBotByGreedy;
72
+ /**
73
+ * 销毁
74
+ */
75
+ private dispose;
76
+ /**
77
+ * 获取负载均衡状态(供 API 使用)
78
+ */
79
+ getStatus(): {
80
+ enabled: boolean;
81
+ totalChannels: number;
82
+ onlineBots: number;
83
+ botStats: Record<string, {
84
+ channelCount: number;
85
+ assignedCount: number;
86
+ }>;
87
+ };
88
+ }
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.14",
4
+ "version": "0.0.16",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [