koishi-plugin-adapter-onebot-multi 1.0.1 → 1.0.3
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/config-store.d.ts +14 -0
- package/lib/index.js +129 -23
- package/package.json +1 -1
package/lib/config-store.d.ts
CHANGED
|
@@ -20,17 +20,29 @@ export interface StoreConfig {
|
|
|
20
20
|
bots: ManagedBotConfig[];
|
|
21
21
|
loadBalance: Required<LoadBalanceConfig>;
|
|
22
22
|
}
|
|
23
|
+
type ExternalChangeListener = (next: StoreConfig, prev: StoreConfig) => void | Promise<void>;
|
|
23
24
|
export declare class ConfigStore {
|
|
24
25
|
private ctx;
|
|
25
26
|
private configRoot;
|
|
26
27
|
private configPath;
|
|
27
28
|
private config;
|
|
28
29
|
private saveTimer;
|
|
30
|
+
private reloadTimer;
|
|
31
|
+
private skipWatchUntil;
|
|
32
|
+
private watching;
|
|
33
|
+
private externalChangeListeners;
|
|
34
|
+
private logger;
|
|
35
|
+
private readonly onFileChanged;
|
|
29
36
|
constructor(ctx: Context);
|
|
30
37
|
private ensureDir;
|
|
31
38
|
private normalize;
|
|
32
39
|
private loadConfig;
|
|
40
|
+
private startWatching;
|
|
41
|
+
private stopWatching;
|
|
42
|
+
private scheduleReloadFromDisk;
|
|
43
|
+
private reloadFromDisk;
|
|
33
44
|
private saveConfigSync;
|
|
45
|
+
onExternalChange(listener: ExternalChangeListener): () => void;
|
|
34
46
|
private scheduleSave;
|
|
35
47
|
getAll(): StoreConfig;
|
|
36
48
|
getRuntime(): RuntimeConfig;
|
|
@@ -40,4 +52,6 @@ export declare class ConfigStore {
|
|
|
40
52
|
setLoadBalance(loadBalance: Partial<LoadBalanceConfig>): void;
|
|
41
53
|
setBots(bots: ManagedBotConfig[]): void;
|
|
42
54
|
flush(): void;
|
|
55
|
+
dispose(): void;
|
|
43
56
|
}
|
|
57
|
+
export {};
|
package/lib/index.js
CHANGED
|
@@ -1460,13 +1460,15 @@ var LoadBalancer = class {
|
|
|
1460
1460
|
* 检查群是否应该被管理(根据白名单/黑名单模式)
|
|
1461
1461
|
*/
|
|
1462
1462
|
shouldManageChannel(channelId) {
|
|
1463
|
+
const normalizedChannelId = String(channelId || "").trim();
|
|
1464
|
+
if (!normalizedChannelId) return false;
|
|
1463
1465
|
const mode = this.config.channelFilterMode || "blacklist";
|
|
1464
1466
|
if (mode === "whitelist") {
|
|
1465
1467
|
const whitelist = this.config.channelWhitelist || [];
|
|
1466
|
-
return whitelist.length === 0 || whitelist.includes(
|
|
1468
|
+
return whitelist.length === 0 || whitelist.includes(normalizedChannelId);
|
|
1467
1469
|
} else {
|
|
1468
1470
|
const blacklist = this.config.channelBlacklist || [];
|
|
1469
|
-
return !blacklist.includes(
|
|
1471
|
+
return !blacklist.includes(normalizedChannelId);
|
|
1470
1472
|
}
|
|
1471
1473
|
}
|
|
1472
1474
|
/**
|
|
@@ -1519,14 +1521,16 @@ var LoadBalancer = class {
|
|
|
1519
1521
|
this.listenerDisposers.push(this.ctx.on("guild-added", (session) => {
|
|
1520
1522
|
if (!this.config.enabled) return;
|
|
1521
1523
|
if (session.platform !== "onebot") return;
|
|
1524
|
+
const guildId = String(session.guildId || "").trim();
|
|
1525
|
+
if (!guildId) return;
|
|
1522
1526
|
const botChannelSet = this.botChannels.get(session.selfId) || /* @__PURE__ */ new Set();
|
|
1523
|
-
const isNewChannel = !botChannelSet.has(
|
|
1524
|
-
botChannelSet.add(
|
|
1527
|
+
const isNewChannel = !botChannelSet.has(guildId);
|
|
1528
|
+
botChannelSet.add(guildId);
|
|
1525
1529
|
this.botChannels.set(session.selfId, botChannelSet);
|
|
1526
|
-
const channelBots = this.allChannels.get(
|
|
1530
|
+
const channelBots = this.allChannels.get(guildId) || /* @__PURE__ */ new Set();
|
|
1527
1531
|
channelBots.add(session.selfId);
|
|
1528
|
-
this.allChannels.set(
|
|
1529
|
-
this.logger.info(`Bot ${session.selfId} 加入群 ${
|
|
1532
|
+
this.allChannels.set(guildId, channelBots);
|
|
1533
|
+
this.logger.info(`Bot ${session.selfId} 加入群 ${guildId}`);
|
|
1530
1534
|
if (isNewChannel) {
|
|
1531
1535
|
this.scheduleLoadBalancing(1e4);
|
|
1532
1536
|
}
|
|
@@ -1534,20 +1538,22 @@ var LoadBalancer = class {
|
|
|
1534
1538
|
this.listenerDisposers.push(this.ctx.on("guild-removed", (session) => {
|
|
1535
1539
|
if (!this.config.enabled) return;
|
|
1536
1540
|
if (session.platform !== "onebot") return;
|
|
1541
|
+
const guildId = String(session.guildId || "").trim();
|
|
1542
|
+
if (!guildId) return;
|
|
1537
1543
|
const botChannelSet = this.botChannels.get(session.selfId);
|
|
1538
1544
|
if (botChannelSet) {
|
|
1539
|
-
botChannelSet.delete(
|
|
1545
|
+
botChannelSet.delete(guildId);
|
|
1540
1546
|
}
|
|
1541
|
-
const channelBots = this.allChannels.get(
|
|
1547
|
+
const channelBots = this.allChannels.get(guildId);
|
|
1542
1548
|
const hadOtherBots = channelBots && channelBots.size > 1;
|
|
1543
1549
|
if (channelBots) {
|
|
1544
1550
|
channelBots.delete(session.selfId);
|
|
1545
1551
|
if (channelBots.size === 0) {
|
|
1546
|
-
this.allChannels.delete(
|
|
1547
|
-
this.lastAssignees.delete(
|
|
1552
|
+
this.allChannels.delete(guildId);
|
|
1553
|
+
this.lastAssignees.delete(guildId);
|
|
1548
1554
|
}
|
|
1549
1555
|
}
|
|
1550
|
-
this.logger.info(`Bot ${session.selfId} 退出群 ${
|
|
1556
|
+
this.logger.info(`Bot ${session.selfId} 退出群 ${guildId}`);
|
|
1551
1557
|
if (hadOtherBots) {
|
|
1552
1558
|
this.scheduleLoadBalancing(5e3);
|
|
1553
1559
|
}
|
|
@@ -1591,7 +1597,7 @@ var LoadBalancer = class {
|
|
|
1591
1597
|
}
|
|
1592
1598
|
const guilds = await bot.getGuildList();
|
|
1593
1599
|
const channelIds = new Set(
|
|
1594
|
-
Array.from(guilds.data || []).map((g) => g
|
|
1600
|
+
Array.from(guilds.data || []).map((g) => String(g?.id || "").trim()).filter(Boolean)
|
|
1595
1601
|
);
|
|
1596
1602
|
this.botChannels.set(bot.selfId, channelIds);
|
|
1597
1603
|
for (const channelId of channelIds) {
|
|
@@ -1642,7 +1648,7 @@ var LoadBalancer = class {
|
|
|
1642
1648
|
return;
|
|
1643
1649
|
}
|
|
1644
1650
|
const channels = [];
|
|
1645
|
-
const prioritySet = new Set(this.config.priorityChannels || []);
|
|
1651
|
+
const prioritySet = new Set((this.config.priorityChannels || []).map((id) => String(id).trim()).filter(Boolean));
|
|
1646
1652
|
for (const [channelId, bots] of this.allChannels.entries()) {
|
|
1647
1653
|
channels.push({
|
|
1648
1654
|
channelId,
|
|
@@ -1695,11 +1701,11 @@ var LoadBalancer = class {
|
|
|
1695
1701
|
selectedBot = this.selectBotByGreedy(validBots, botRemainingCapacity, botLoad);
|
|
1696
1702
|
}
|
|
1697
1703
|
if (!selectedBot) {
|
|
1698
|
-
const
|
|
1699
|
-
if (
|
|
1704
|
+
const unassignedValue2 = this.config.unassignedValue;
|
|
1705
|
+
if (unassignedValue2 !== void 0 && unassignedValue2 !== "") {
|
|
1700
1706
|
const lastAssignee2 = this.lastAssignees.get(channel.channelId);
|
|
1701
|
-
if (lastAssignee2 !==
|
|
1702
|
-
assignments.push({ channelId: channel.channelId, assignee:
|
|
1707
|
+
if (lastAssignee2 !== unassignedValue2) {
|
|
1708
|
+
assignments.push({ channelId: channel.channelId, assignee: unassignedValue2 });
|
|
1703
1709
|
this.logger.info(`群 ${channel.channelId} 所有 Bot 达到上限,设为未分配`);
|
|
1704
1710
|
}
|
|
1705
1711
|
} else {
|
|
@@ -1725,14 +1731,28 @@ var LoadBalancer = class {
|
|
|
1725
1731
|
const channels2 = this.botChannels.get(id);
|
|
1726
1732
|
return channels2 && channels2.size > 0;
|
|
1727
1733
|
});
|
|
1728
|
-
const
|
|
1734
|
+
const managedChannels = channels.filter((c) => this.shouldManageChannel(c.channelId));
|
|
1735
|
+
const priorityCount = managedChannels.filter((c) => c.isPriority).length;
|
|
1736
|
+
const configuredPriorityCount = new Set(this.config.priorityChannels || []).size;
|
|
1737
|
+
const unassignedValue = this.config.unassignedValue || "";
|
|
1738
|
+
let assignedTotal = 0;
|
|
1739
|
+
let unassignedTotal = 0;
|
|
1740
|
+
for (const [, assignee] of this.lastAssignees.entries()) {
|
|
1741
|
+
if (assignee && assignee !== unassignedValue) {
|
|
1742
|
+
assignedTotal++;
|
|
1743
|
+
} else {
|
|
1744
|
+
unassignedTotal++;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1729
1747
|
if (botsWithData.length > 0) {
|
|
1730
1748
|
const loads = botsWithData.map((id) => botLoad.get(id) || 0);
|
|
1731
1749
|
const avg = loads.reduce((a, b) => a + b, 0) / loads.length;
|
|
1732
1750
|
const variance = loads.reduce((sum, load) => sum + Math.pow(load - avg, 2), 0) / loads.length;
|
|
1733
1751
|
const stdDev = Math.sqrt(variance);
|
|
1734
1752
|
const loadInfo = botsWithData.map((id) => `${id}:${botLoad.get(id) || 0}`).join(", ");
|
|
1735
|
-
this.logger.info(
|
|
1753
|
+
this.logger.info(
|
|
1754
|
+
`负载均衡完成,本轮变更 ${assignments.length} 个群;当前已分配 ${assignedTotal} 个,未分配 ${unassignedTotal} 个;关键群(参与调度)${priorityCount} / 配置总数 ${configuredPriorityCount}`
|
|
1755
|
+
);
|
|
1736
1756
|
this.logger.info(`负载分布: [${loadInfo}],平均: ${avg.toFixed(1)},标准差: ${stdDev.toFixed(2)}`);
|
|
1737
1757
|
}
|
|
1738
1758
|
} catch (e) {
|
|
@@ -1786,7 +1806,7 @@ var LoadBalancer = class {
|
|
|
1786
1806
|
if (!this.config.enabled) return true;
|
|
1787
1807
|
if (session.platform !== "onebot") return true;
|
|
1788
1808
|
if (session.type !== "message") return true;
|
|
1789
|
-
const channelId = session.guildId || session.channelId;
|
|
1809
|
+
const channelId = String(session.guildId || session.channelId || "").trim();
|
|
1790
1810
|
if (!channelId) return true;
|
|
1791
1811
|
if (!this.shouldManageChannel(channelId)) return true;
|
|
1792
1812
|
const assignee = this.lastAssignees.get(channelId);
|
|
@@ -1914,10 +1934,12 @@ var DEFAULT_CONFIG = {
|
|
|
1914
1934
|
var ConfigStore = class {
|
|
1915
1935
|
constructor(ctx) {
|
|
1916
1936
|
this.ctx = ctx;
|
|
1937
|
+
this.logger = this.ctx.logger("adapter-onebot-multi");
|
|
1917
1938
|
this.configRoot = path.join(ctx.baseDir, "data", "adapter-onebot-multi");
|
|
1918
1939
|
this.configPath = path.join(this.configRoot, "config.yaml");
|
|
1919
1940
|
this.ensureDir(this.configRoot);
|
|
1920
1941
|
this.loadConfig();
|
|
1942
|
+
this.startWatching();
|
|
1921
1943
|
}
|
|
1922
1944
|
static {
|
|
1923
1945
|
__name(this, "ConfigStore");
|
|
@@ -1926,6 +1948,17 @@ var ConfigStore = class {
|
|
|
1926
1948
|
configPath;
|
|
1927
1949
|
config = structuredClone(DEFAULT_CONFIG);
|
|
1928
1950
|
saveTimer = null;
|
|
1951
|
+
reloadTimer = null;
|
|
1952
|
+
skipWatchUntil = 0;
|
|
1953
|
+
watching = false;
|
|
1954
|
+
externalChangeListeners = /* @__PURE__ */ new Set();
|
|
1955
|
+
logger;
|
|
1956
|
+
onFileChanged = /* @__PURE__ */ __name((curr, prev) => {
|
|
1957
|
+
if (!this.watching) return;
|
|
1958
|
+
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
1959
|
+
if (Date.now() < this.skipWatchUntil) return;
|
|
1960
|
+
this.scheduleReloadFromDisk();
|
|
1961
|
+
}, "onFileChanged");
|
|
1929
1962
|
ensureDir(dir) {
|
|
1930
1963
|
if (!fs.existsSync(dir)) {
|
|
1931
1964
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1981,15 +2014,57 @@ var ConfigStore = class {
|
|
|
1981
2014
|
const parsed = yaml.parse(content);
|
|
1982
2015
|
this.config = this.normalize(parsed);
|
|
1983
2016
|
} catch (error) {
|
|
1984
|
-
this.
|
|
2017
|
+
this.logger.warn("读取配置失败,使用默认配置: %s", error);
|
|
1985
2018
|
this.config = structuredClone(DEFAULT_CONFIG);
|
|
1986
2019
|
this.saveConfigSync();
|
|
1987
2020
|
}
|
|
1988
2021
|
}
|
|
2022
|
+
startWatching() {
|
|
2023
|
+
if (this.watching) return;
|
|
2024
|
+
this.watching = true;
|
|
2025
|
+
fs.watchFile(this.configPath, { interval: 1e3 }, this.onFileChanged);
|
|
2026
|
+
}
|
|
2027
|
+
stopWatching() {
|
|
2028
|
+
if (!this.watching) return;
|
|
2029
|
+
fs.unwatchFile(this.configPath, this.onFileChanged);
|
|
2030
|
+
this.watching = false;
|
|
2031
|
+
}
|
|
2032
|
+
scheduleReloadFromDisk() {
|
|
2033
|
+
if (this.reloadTimer) clearTimeout(this.reloadTimer);
|
|
2034
|
+
this.reloadTimer = setTimeout(() => {
|
|
2035
|
+
this.reloadTimer = null;
|
|
2036
|
+
this.reloadFromDisk();
|
|
2037
|
+
}, 200);
|
|
2038
|
+
}
|
|
2039
|
+
reloadFromDisk() {
|
|
2040
|
+
try {
|
|
2041
|
+
const content = fs.readFileSync(this.configPath, "utf8");
|
|
2042
|
+
const parsed = yaml.parse(content);
|
|
2043
|
+
const normalized = this.normalize(parsed);
|
|
2044
|
+
const prev = structuredClone(this.config);
|
|
2045
|
+
if (JSON.stringify(prev) === JSON.stringify(normalized)) return;
|
|
2046
|
+
this.config = normalized;
|
|
2047
|
+
this.logger.info("检测到配置文件变更,已重新加载。");
|
|
2048
|
+
for (const listener of this.externalChangeListeners) {
|
|
2049
|
+
Promise.resolve(listener(structuredClone(this.config), prev)).catch((error) => {
|
|
2050
|
+
this.logger.warn("配置变更回调执行失败: %s", error);
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
this.logger.warn("重新加载配置失败,忽略本次变更: %s", error);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1989
2057
|
saveConfigSync() {
|
|
2058
|
+
this.skipWatchUntil = Date.now() + 300;
|
|
1990
2059
|
const content = yaml.stringify(this.config, { indent: 2, lineWidth: 120 });
|
|
1991
2060
|
fs.writeFileSync(this.configPath, content, "utf8");
|
|
1992
2061
|
}
|
|
2062
|
+
onExternalChange(listener) {
|
|
2063
|
+
this.externalChangeListeners.add(listener);
|
|
2064
|
+
return () => {
|
|
2065
|
+
this.externalChangeListeners.delete(listener);
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
1993
2068
|
scheduleSave() {
|
|
1994
2069
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
1995
2070
|
this.saveTimer = setTimeout(() => {
|
|
@@ -2037,6 +2112,14 @@ var ConfigStore = class {
|
|
|
2037
2112
|
}
|
|
2038
2113
|
this.saveConfigSync();
|
|
2039
2114
|
}
|
|
2115
|
+
dispose() {
|
|
2116
|
+
if (this.reloadTimer) {
|
|
2117
|
+
clearTimeout(this.reloadTimer);
|
|
2118
|
+
this.reloadTimer = null;
|
|
2119
|
+
}
|
|
2120
|
+
this.stopWatching();
|
|
2121
|
+
this.flush();
|
|
2122
|
+
}
|
|
2040
2123
|
};
|
|
2041
2124
|
|
|
2042
2125
|
// src/index.ts
|
|
@@ -2066,6 +2149,28 @@ function apply(ctx, _config) {
|
|
|
2066
2149
|
}, "getGlobalConfig");
|
|
2067
2150
|
ctx._onebotMultiGlobalConfig = getGlobalConfig();
|
|
2068
2151
|
const loadBalancer = new LoadBalancer(ctx, configStore.getLoadBalance(), statusManager);
|
|
2152
|
+
const stopConfigWatcher = configStore.onExternalChange(async (next, prev) => {
|
|
2153
|
+
const runtimeChanged = JSON.stringify(prev.runtime) !== JSON.stringify(next.runtime);
|
|
2154
|
+
const loadBalanceChanged = JSON.stringify(prev.loadBalance) !== JSON.stringify(next.loadBalance);
|
|
2155
|
+
const botsChanged = JSON.stringify(prev.bots) !== JSON.stringify(next.bots);
|
|
2156
|
+
if (runtimeChanged) {
|
|
2157
|
+
;
|
|
2158
|
+
ctx._onebotMultiGlobalConfig = getGlobalConfig();
|
|
2159
|
+
const restartTargets = configStore.getBots().filter((bot) => bot.enabled !== false).map((bot) => bot.selfId);
|
|
2160
|
+
for (const selfId of restartTargets) {
|
|
2161
|
+
await restartBot(selfId);
|
|
2162
|
+
}
|
|
2163
|
+
logger.info("检测到 runtime 配置更新,已重启启用中的 Bot。");
|
|
2164
|
+
}
|
|
2165
|
+
if (loadBalanceChanged) {
|
|
2166
|
+
loadBalancer.updateConfig(configStore.getLoadBalance());
|
|
2167
|
+
logger.info("检测到 loadBalance 配置更新,已应用。");
|
|
2168
|
+
}
|
|
2169
|
+
if (botsChanged) {
|
|
2170
|
+
await syncBots();
|
|
2171
|
+
logger.info("检测到 bots 配置更新,已同步 Bot 状态。");
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2069
2174
|
const getBotListView = /* @__PURE__ */ __name(() => {
|
|
2070
2175
|
const runtime = statusManager.getStatus();
|
|
2071
2176
|
const runtimeMap = new Map(runtime.bots.map((b) => [String(b.selfId), b]));
|
|
@@ -2344,11 +2449,12 @@ function apply(ctx, _config) {
|
|
|
2344
2449
|
logger.info(`Bot 配置 selfId 自动更新: ${configKey} -> ${runtimeSelfId}`);
|
|
2345
2450
|
});
|
|
2346
2451
|
ctx.on("dispose", async () => {
|
|
2452
|
+
stopConfigWatcher();
|
|
2347
2453
|
for (const selfId of Array.from(managedBotIds)) {
|
|
2348
2454
|
await stopBot(selfId);
|
|
2349
2455
|
}
|
|
2350
2456
|
loadBalancer.dispose();
|
|
2351
|
-
configStore.
|
|
2457
|
+
configStore.dispose();
|
|
2352
2458
|
});
|
|
2353
2459
|
}
|
|
2354
2460
|
__name(apply, "apply");
|