mioki 0.13.0 → 0.15.0

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/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_package = require('./package-BHgI99iP.cjs');
1
+ const require_package = require('./package-49Rgph2M.cjs');
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_package.__toESM(node_fs);
4
4
  let node_util = require("node:util");
@@ -790,12 +790,20 @@ var config_exports = /* @__PURE__ */ require_package.__exportAll({
790
790
  isInPm2: () => isInPm2,
791
791
  isOwner: () => isOwner,
792
792
  isOwnerOrAdmin: () => isOwnerOrAdmin,
793
+ normalizeNapCatConfig: () => normalizeNapCatConfig,
793
794
  readMiokiConfig: () => readMiokiConfig,
794
795
  readPackageJson: () => readPackageJson,
795
796
  updateBotCWD: () => updateBotCWD,
796
797
  updateBotConfig: () => updateBotConfig,
797
798
  writePackageJson: () => writePackageJson
798
799
  });
800
+ function isSingleNapCatConfig(config) {
801
+ return !Array.isArray(config);
802
+ }
803
+ function normalizeNapCatConfig(config) {
804
+ if (isSingleNapCatConfig(config)) return [config];
805
+ return config;
806
+ }
799
807
  /**
800
808
  * 机器人根目录
801
809
  */
@@ -811,7 +819,10 @@ function readMiokiConfig() {
811
819
  const config = readPackageJson().mioki;
812
820
  if (!config) throw new Error(`无法在 package.json 中找到 mioki 配置,请确认 package.json 文件中是否包含 mioki 字段`);
813
821
  if (!config.napcat) throw new Error(`mioki 配置中缺少 napcat 字段,请补全后重试`);
814
- return readPackageJson().mioki;
822
+ return {
823
+ ...config,
824
+ napcat: normalizeNapCatConfig(config.napcat)
825
+ };
815
826
  }
816
827
  /**
817
828
  * `mioki` 框架相关配置
@@ -1212,9 +1223,132 @@ function addService(name, service, cover = true) {
1212
1223
  function bindBot(bot, func) {
1213
1224
  return (...args) => func(bot, ...args);
1214
1225
  }
1226
+ /**
1227
+ * 去重器
1228
+ * 处理多个 bot 在相同场景下,同一事件只处理一次
1229
+ */
1230
+ var Deduplicator = class {
1231
+ processedEvents = /* @__PURE__ */ new Set();
1232
+ maxSize = 1e3;
1233
+ /**
1234
+ * 获取事件类型键
1235
+ */
1236
+ getEventTypeKey(e) {
1237
+ const { post_type } = e;
1238
+ if (post_type === "message") return `msg:${e.message_type}`;
1239
+ if (post_type === "request") {
1240
+ if (e.request_type === "friend") return "req:friend";
1241
+ return `req:group:${e.sub_type ?? "unknown"}`;
1242
+ }
1243
+ if (post_type === "notice") {
1244
+ if (e.notice_type === "group") return `notice:group:${e.sub_type}`;
1245
+ return `notice:${e.notice_type}:${e.sub_type}`;
1246
+ }
1247
+ return "unknown";
1248
+ }
1249
+ getGroupMessageKey(e) {
1250
+ const groupId = e.group_id ?? "_";
1251
+ const userId = e.user_id ?? "_";
1252
+ const time = e.time ?? "_";
1253
+ const raw = e.raw_message ?? "_";
1254
+ return `msg:group:${groupId}:${userId}:${time}:${node_crypto.default.createHash("md5").update(raw).digest("hex")}`;
1255
+ }
1256
+ getNoticeGroupKey(e) {
1257
+ return `${this.getEventTypeKey(e)}:${e.group_id ?? "_"}:${e.user_id ?? "_"}:${"operator_id" in e ? e.operator_id ?? "_" : "_"}:${"target_id" in e ? e.target_id ?? "_" : "_"}:${e.sub_type ?? "_"}:${"action_type" in e ? e.action_type ?? "_" : "_"}:${"duration" in e ? e.duration ?? "_" : "_"}:${e.time ?? "_"}`;
1258
+ }
1259
+ getRequestKey(e) {
1260
+ const typeKey = this.getEventTypeKey(e);
1261
+ const userId = e.user_id ?? "_";
1262
+ const groupId = "group_id" in e ? e.group_id ?? "_" : "_";
1263
+ const time = e.time ?? "_";
1264
+ const comment = e.comment ?? "_";
1265
+ return `${typeKey}:${userId}:${groupId}:${time}:${comment ? node_crypto.default.createHash("md5").update(comment).digest("hex") : "_"}`;
1266
+ }
1267
+ /**
1268
+ * 生成事件唯一键
1269
+ */
1270
+ getKey(e) {
1271
+ const typeKey = this.getEventTypeKey(e);
1272
+ if (typeKey === "msg:group") return this.getGroupMessageKey(e);
1273
+ if (typeKey.startsWith("notice:group:")) return this.getNoticeGroupKey(e);
1274
+ if (typeKey.startsWith("req:")) return this.getRequestKey(e);
1275
+ return "";
1276
+ }
1277
+ /**
1278
+ * 检查事件是否已处理过
1279
+ * @param event
1280
+ * @param scope
1281
+ */
1282
+ isProcessed(event, scope) {
1283
+ const key = scope ? `${this.getKey(event)}:${scope}` : this.getKey(event);
1284
+ return this.processedEvents.has(key);
1285
+ }
1286
+ /**
1287
+ * 标记事件为已处理
1288
+ * @param event
1289
+ * @param scope 需与 isProcessed 一致
1290
+ */
1291
+ markProcessed(event, scope) {
1292
+ const key = scope ? `${this.getKey(event)}:${scope}` : this.getKey(event);
1293
+ if (this.processedEvents.size >= this.maxSize) {
1294
+ const first = this.processedEvents.values().next();
1295
+ if (!first.done) this.processedEvents.delete(first.value);
1296
+ }
1297
+ this.processedEvents.add(key);
1298
+ }
1299
+ };
1300
+ /**
1301
+ * 消息去重器
1302
+ * @deprecated 请使用 Deduplicator,它支持更多事件类型
1303
+ */
1304
+ var MessageDeduplicator = class extends Deduplicator {
1305
+ isProcessed(event) {
1306
+ return super.isProcessed(event);
1307
+ }
1308
+ markProcessed(event) {
1309
+ super.markProcessed(event);
1310
+ }
1311
+ };
1312
+ const deduplicator = new Deduplicator();
1215
1313
  const runtimePlugins = /* @__PURE__ */ new Map();
1216
1314
  const buildRemovedActions = (bot) => Object.fromEntries(Object.entries(actions_exports).map(([k, v]) => [k, bindBot(bot, v)]));
1217
1315
  /**
1316
+ * 检查事件是否是群消息事件
1317
+ */
1318
+ function isGroupMessageEvent(event) {
1319
+ return event?.post_type === "message" && event?.message_type === "group";
1320
+ }
1321
+ /**
1322
+ * 检查事件是否是私聊消息事件
1323
+ */
1324
+ function isPrivateMessageEvent(event) {
1325
+ return event?.post_type === "message" && event?.message_type === "private";
1326
+ }
1327
+ /**
1328
+ * 检查事件是否是消息事件
1329
+ */
1330
+ function isMessageEvent(event) {
1331
+ return isGroupMessageEvent(event) || isPrivateMessageEvent(event);
1332
+ }
1333
+ /**
1334
+ * 检查事件是否是请求事件
1335
+ */
1336
+ function isRequestEvent(event) {
1337
+ return event?.post_type === "request";
1338
+ }
1339
+ /**
1340
+ * 检查事件是否是群通知事件
1341
+ */
1342
+ function isGroupNoticeEvent(event) {
1343
+ return event?.post_type === "notice" && event?.notice_type === "group";
1344
+ }
1345
+ /**
1346
+ * 检查事件是否需要去重
1347
+ */
1348
+ function isDeduplicableEvent(event) {
1349
+ return isMessageEvent(event) || isRequestEvent(event) || isGroupNoticeEvent(event);
1350
+ }
1351
+ /**
1218
1352
  * 定义一个 Mioki 插件
1219
1353
  * @param plugin Mioki 插件对象
1220
1354
  * @returns Mioki 插件对象
@@ -1236,55 +1370,83 @@ function getAbsPluginDir(defaultDir = "plugins") {
1236
1370
  const cwd = BOT_CWD.value;
1237
1371
  return node_path.default.join(cwd, botConfig.plugins_dir || defaultDir);
1238
1372
  }
1239
- async function enablePlugin(bot, plugin, type = "external") {
1373
+ async function enablePlugin(bots, plugin, type = "external") {
1240
1374
  const typeDesc = type === "builtin" ? "内置" : "用户";
1241
1375
  const pluginName = plugin.name || "null";
1242
1376
  const { name = pluginName, version: version$1 = "null", description = "-", setup = () => {} } = plugin;
1377
+ if (!bots[0]) throw new Error("没有可用的 bot 实例");
1243
1378
  try {
1244
1379
  const start$1 = node_process.hrtime.bigint();
1245
1380
  const clears = /* @__PURE__ */ new Set();
1246
1381
  const userClears = /* @__PURE__ */ new Set();
1247
- const logger$1 = bot.logger.withDefaults({
1248
- tag: `plugin:${name}`,
1249
- args: [name]
1250
- });
1251
- const context = {
1252
- bot,
1253
- segment: bot.segment,
1254
- getCookie: bot.getCookie.bind(bot),
1255
- ...utils_exports,
1256
- ...config_exports,
1257
- ...buildRemovedActions(bot),
1258
- logger: logger$1,
1259
- services,
1260
- clears: userClears,
1261
- addService: (name$1, service, cover) => {
1262
- const remove = addService(name$1, service, cover);
1263
- clears.add(remove);
1264
- return remove;
1265
- },
1266
- handle: (eventName, handler) => {
1267
- logger$1.debug(`Registering event handler for event: ${String(eventName)}`);
1268
- bot.on(eventName, handler);
1269
- const unsubscribe = () => {
1270
- logger$1.debug(`Unregistering event handler for event: ${String(eventName)}`);
1271
- bot.off(eventName, handler);
1272
- };
1273
- clears.add(unsubscribe);
1274
- return unsubscribe;
1275
- },
1276
- cron: (cronExpression, handler) => {
1277
- logger$1.debug(`Scheduling cron job: ${cronExpression}`);
1278
- const job = node_cron.default.schedule(cronExpression, (now) => handler(context, now));
1279
- const clear = () => {
1280
- logger$1.debug(`Stopping cron job: ${cronExpression}`);
1281
- job.stop();
1282
- };
1283
- clears.add(clear);
1284
- return job;
1285
- }
1382
+ const logger$1 = logger.withDefaults({ tag: `plugin:${name}` });
1383
+ const createContext = (bot) => {
1384
+ return {
1385
+ bot,
1386
+ bots,
1387
+ self_id: bot.bot_id,
1388
+ segment: bot.segment,
1389
+ getCookie: bot.getCookie.bind(bot),
1390
+ ...utils_exports,
1391
+ ...config_exports,
1392
+ ...buildRemovedActions(bot),
1393
+ logger: logger$1,
1394
+ services,
1395
+ clears: userClears,
1396
+ deduplicator,
1397
+ addService: (name$1, service, cover) => {
1398
+ const remove = addService(name$1, service, cover);
1399
+ clears.add(remove);
1400
+ return remove;
1401
+ },
1402
+ handle: (eventName, handler, options = {}) => {
1403
+ logger$1.debug(`Registering event handler for event: ${String(eventName)}`);
1404
+ const { deduplicate = true } = options;
1405
+ const dedupeScope = `${name}:${String(eventName)}:${node_crypto.default.randomUUID()}`;
1406
+ const unsubscribes = [];
1407
+ for (const bot$1 of bots) {
1408
+ const wrappedHandler = (e) => {
1409
+ if (isPrivateMessageEvent(e)) {
1410
+ if (e.self_id !== bot$1.bot_id) return;
1411
+ }
1412
+ const senderUserId = e.user_id;
1413
+ const senderOperatorId = e.operator_id;
1414
+ const isFromConnectedBot = senderUserId && bots.some((b) => b.bot_id === senderUserId);
1415
+ const isFromConnectedBotOperator = senderOperatorId && bots.some((b) => b.bot_id === senderOperatorId);
1416
+ if (isFromConnectedBot || isFromConnectedBotOperator) return;
1417
+ if (deduplicate && isDeduplicableEvent(e)) {
1418
+ if (deduplicator.isProcessed(e, dedupeScope)) return;
1419
+ deduplicator.markProcessed(e, dedupeScope);
1420
+ }
1421
+ handler(e);
1422
+ };
1423
+ bot$1.on(eventName, wrappedHandler);
1424
+ const unsubscribe = () => {
1425
+ logger$1.debug(`Unregistering event handler for event: ${String(eventName)}`);
1426
+ bot$1.off(eventName, wrappedHandler);
1427
+ };
1428
+ unsubscribes.push(unsubscribe);
1429
+ }
1430
+ const clearAll = () => {
1431
+ unsubscribes.forEach((fn) => fn());
1432
+ };
1433
+ clears.add(clearAll);
1434
+ return clearAll;
1435
+ },
1436
+ cron: (cronExpression, handler) => {
1437
+ logger$1.debug(`Scheduling cron job: ${cronExpression}`);
1438
+ const job = node_cron.default.schedule(cronExpression, (now) => handler(createContext(bot), now));
1439
+ const clear = () => {
1440
+ logger$1.debug(`Stopping cron job: ${cronExpression}`);
1441
+ job.stop();
1442
+ };
1443
+ clears.add(clear);
1444
+ return job;
1445
+ }
1446
+ };
1286
1447
  };
1287
- clears.add(await setup(context) || (() => {}));
1448
+ const mainContext = createContext(bots[0]);
1449
+ clears.add(await setup(mainContext) || (() => {}));
1288
1450
  runtimePlugins.set(name, {
1289
1451
  name,
1290
1452
  type,
@@ -1303,7 +1465,7 @@ async function enablePlugin(bot, plugin, type = "external") {
1303
1465
  });
1304
1466
  const end = node_process.hrtime.bigint();
1305
1467
  const time = Math.round(Number(end - start$1)) / 1e6;
1306
- bot.logger.info(`- 启用插件 ${consola_utils.colors.yellow(`[${typeDesc}]`)} ${consola_utils.colors.yellow(`${name}@${version$1}`)} => 耗时 ${consola_utils.colors.green(time.toFixed(2))} 毫秒`);
1468
+ logger$1.info(`- 启用插件 ${consola_utils.colors.yellow(`[${typeDesc}]`)} ${consola_utils.colors.yellow(`${name}@${version$1}`)} => 耗时 ${consola_utils.colors.green(time.toFixed(2))} 毫秒`);
1307
1469
  } catch (e) {
1308
1470
  throw new Error(`启用插件 ${consola_utils.colors.yellow(`[${typeDesc}]`)} ${consola_utils.colors.yellow(`${name}@${version$1}`)} 失败: ${e?.message}`);
1309
1471
  }
@@ -1329,18 +1491,12 @@ const ArchMap = {
1329
1491
  arm64: "arm64",
1330
1492
  x64: "x64"
1331
1493
  };
1332
- async function getMiokiStatus(bot) {
1494
+ async function getMiokiStatus(bots) {
1333
1495
  const osType = node_os.default.type();
1334
1496
  const osArch = node_os.default.arch();
1335
1497
  const isInUnix = ["Linux", "Darwin"].includes(osType);
1336
1498
  const arch = ArchMap[osArch] || osArch;
1337
- const [osInfo, localPlugins, versionInfo, friendList, groupList] = await Promise.all([
1338
- systeminformation.default.osInfo(),
1339
- findLocalPlugins(),
1340
- bot.getVersionInfo(),
1341
- bot.getFriendList(),
1342
- bot.getGroupList()
1343
- ]);
1499
+ const [osInfo, localPlugins] = await Promise.all([systeminformation.default.osInfo(), findLocalPlugins()]);
1344
1500
  const pluginCount = localPlugins.length + BUILTIN_PLUGINS.length;
1345
1501
  const system = isInUnix ? {
1346
1502
  name: osInfo.distro,
@@ -1354,27 +1510,58 @@ async function getMiokiStatus(bot) {
1354
1510
  const rssMem = process.memoryUsage.rss();
1355
1511
  const nodeVersion = process.versions.node;
1356
1512
  const cpu = getCpuInfo();
1357
- return {
1358
- bot: {
1359
- uin: bot.uin,
1513
+ const botStatuses = [];
1514
+ let totalSend = 0;
1515
+ let totalReceive = 0;
1516
+ let mainVersionInfo = {
1517
+ app_version: "unknown",
1518
+ protocol_version: "unknown"
1519
+ };
1520
+ for (const bot of bots) try {
1521
+ const [versionInfo, friendList, groupList] = await Promise.all([
1522
+ bot.getVersionInfo(),
1523
+ bot.getFriendList(),
1524
+ bot.getGroupList()
1525
+ ]);
1526
+ mainVersionInfo = versionInfo;
1527
+ botStatuses.push({
1528
+ uin: bot.bot_id,
1360
1529
  nickname: bot.nickname,
1530
+ name: bot.name,
1361
1531
  friends: friendList.length,
1362
- groups: groupList.length
1363
- },
1532
+ groups: groupList.length,
1533
+ send: bot.stat.send.group + bot.stat.send.private,
1534
+ receive: bot.stat.recv.group + bot.stat.recv.private
1535
+ });
1536
+ totalSend += bot.stat.send.group + bot.stat.send.private;
1537
+ totalReceive += bot.stat.recv.group + bot.stat.recv.private;
1538
+ } catch (err) {
1539
+ botStatuses.push({
1540
+ uin: bot.bot_id,
1541
+ nickname: bot.nickname,
1542
+ name: bot.name,
1543
+ friends: 0,
1544
+ groups: 0,
1545
+ send: 0,
1546
+ receive: 0
1547
+ });
1548
+ }
1549
+ return {
1550
+ bots: botStatuses,
1364
1551
  plugins: {
1365
1552
  enabled: runtimePlugins.size,
1366
1553
  total: pluginCount
1367
1554
  },
1368
1555
  stats: {
1369
1556
  uptime: process.uptime() * 1e3,
1370
- send: bot.stat.send.group + bot.stat.send.private,
1371
- receive: bot.stat.recv.group + bot.stat.recv.private
1557
+ send: totalSend,
1558
+ receive: totalReceive
1372
1559
  },
1373
1560
  versions: {
1374
1561
  node: nodeVersion,
1375
1562
  mioki: require_package.version,
1376
- napcat: versionInfo.app_version,
1377
- protocol: versionInfo.protocol_version
1563
+ napcat: mainVersionInfo.app_version,
1564
+ protocol: mainVersionInfo.protocol_version
1378
1565
  },
1379
1566
  system: {
1380
1567
  name: system.name || "N/A",
@@ -1404,16 +1591,16 @@ async function getMiokiStatus(bot) {
1404
1591
  };
1405
1592
  }
1406
1593
  async function formatMiokiStatus(status) {
1407
- const { bot, plugins, stats, system, disk, cpu, memory, versions } = status;
1594
+ const { bots, plugins, stats, system, disk, cpu, memory, versions } = status;
1408
1595
  const diskValid = disk.total > 0 && disk.free >= 0;
1409
1596
  const diskDesc = `${disk.percent}%-${(0, filesize.filesize)(disk.used, { round: 1 })}/${(0, filesize.filesize)(disk.total, { round: 1 })}`;
1410
1597
  return `
1411
1598
  〓 🟢 mioki 状态 〓
1412
- 👤 ${bot.nickname}
1413
- 🆔 ${bot.uin}
1414
- 📋 ${localNum(bot.friends)} 好友 / ${localNum(bot.groups)}
1599
+ ${bots.map((bot, index) => {
1600
+ return `👤 ${bot.name ? `[${bot.name}] ` : ""}${bot.nickname} (${bot.uin})\n 📋 ${localNum(bot.friends)} 好友 / ${localNum(bot.groups)} 群 / 📮 收 ${localNum(bot.receive)} 发 ${localNum(bot.send)}`;
1601
+ }).join("\n")}
1415
1602
  🧩 启用了 ${localNum(plugins.enabled)} 个插件,共 ${localNum(plugins.total)} 个
1416
- 📮 收 ${localNum(stats.receive)} 条,发 ${localNum(stats.send)} 条
1603
+ 📮 总计: 收 ${localNum(stats.receive)} 条,发 ${localNum(stats.send)} 条
1417
1604
  🚀 ${(0, filesize.filesize)(memory.rss.used, { round: 1 })}/${memory.percent}%
1418
1605
  ⏳ 已运行 ${(0, pretty_ms.default)(stats.uptime, {
1419
1606
  hideYear: true,
@@ -1497,7 +1684,7 @@ const core = definePlugin({
1497
1684
  const displayPrefix = prefix.replace(/\\\\/g, "\\");
1498
1685
  const statusAdminOnly = ctx.botConfig.status_permission === "admin-only";
1499
1686
  let statusFormatter = (status) => formatMiokiStatus(status);
1500
- ctx.addService("getMiokiStatus", () => getMiokiStatus(ctx.bot));
1687
+ ctx.addService("getMiokiStatus", () => getMiokiStatus(ctx.bots));
1501
1688
  ctx.addService("formatMiokiStatus", (status) => formatMiokiStatus(status));
1502
1689
  ctx.addService("customFormatMiokiStatus", (formatter) => statusFormatter = formatter);
1503
1690
  ctx.handle("message", (e) => ctx.runWithErrorHandler(async () => {
@@ -1505,7 +1692,7 @@ const core = definePlugin({
1505
1692
  if (!cmdPrefix.test(text$1)) return;
1506
1693
  if (statusAdminOnly && !ctx.hasRight(e)) return;
1507
1694
  if (text$1.replace(cmdPrefix, "") === "状态") {
1508
- const status = await statusFormatter(await getMiokiStatus(ctx.bot));
1695
+ const status = await statusFormatter(await getMiokiStatus(ctx.bots));
1509
1696
  await e.reply(status);
1510
1697
  return;
1511
1698
  }
@@ -1570,10 +1757,10 @@ const core = definePlugin({
1570
1757
  const plugin = await ctx.jiti.import(pluginPath, { default: true });
1571
1758
  if (plugin.name !== target) {
1572
1759
  const tip = `[插件目录名称: ${target}] 和插件代码中设置的 [name: ${plugin.name}] 不一致,可能导致重载异常,请修改后重启。`;
1573
- ctx.bot.logger.warn(tip);
1760
+ ctx.logger.warn(tip);
1574
1761
  ctx.noticeMainOwner(tip);
1575
1762
  }
1576
- await enablePlugin(ctx.bot, plugin);
1763
+ await enablePlugin(ctx.bots, plugin);
1577
1764
  } catch (err) {
1578
1765
  await e.reply(`插件 ${target} 启用失败:${err?.message || "未知错误"}`, true);
1579
1766
  return;
@@ -1599,7 +1786,7 @@ const core = definePlugin({
1599
1786
  break;
1600
1787
  }
1601
1788
  await ctx.updateBotConfig((c) => c.plugins = ctx.botConfig.plugins.filter((name) => name !== target));
1602
- ctx.bot.logger.info(`禁用插件 => ${target}`);
1789
+ ctx.logger.info(`禁用插件 => ${target}`);
1603
1790
  await e.reply(`插件 ${target} 已禁用`, true);
1604
1791
  break;
1605
1792
  }
@@ -1621,10 +1808,10 @@ const core = definePlugin({
1621
1808
  const importedPlugin = await ctx.jiti.import(pluginPath, { default: true });
1622
1809
  if (importedPlugin.name !== target) {
1623
1810
  const tip = `插件目录名称: ${target} 和插件代码中设置的 name: ${importedPlugin.name} 不一致,可能导致重载异常,请修改后重启。`;
1624
- ctx.bot.logger.warn(tip);
1811
+ ctx.logger.warn(tip);
1625
1812
  ctx.noticeMainOwner(tip);
1626
1813
  }
1627
- await enablePlugin(ctx.bot, importedPlugin);
1814
+ await enablePlugin(ctx.bots, importedPlugin);
1628
1815
  } catch (err) {
1629
1816
  await e.reply(err?.message, true);
1630
1817
  await ctx.updateBotConfig((c) => c.plugins = c.plugins.filter((name) => name !== target));
@@ -1737,7 +1924,7 @@ const core = definePlugin({
1737
1924
  break;
1738
1925
  case "退出":
1739
1926
  await e.reply("またね~", true);
1740
- ctx.bot.logger.info("接收到退出指令,即将退出... 如需自动重启,请使用 pm2 部署。");
1927
+ ctx.logger.info("接收到退出指令,即将退出... 如需自动重启,请使用 pm2 部署。");
1741
1928
  process.exit(0);
1742
1929
  }
1743
1930
  }, e));
@@ -1751,6 +1938,120 @@ const BUILTIN_PLUGINS = [core_default];
1751
1938
 
1752
1939
  //#endregion
1753
1940
  //#region src/start.ts
1941
+ const connectedBots = /* @__PURE__ */ new Map();
1942
+ async function connectBot(config, index) {
1943
+ const { protocol = "ws", port = 3001, host = "localhost", token = "", name } = config;
1944
+ const botName = name || `Bot${index + 1}`;
1945
+ const wsUrl = consola_utils.colors.green(`${protocol}://${host}:${port}${token ? "?access_token=***" : ""}`);
1946
+ logger.info(`>>> 正在连接 ${consola_utils.colors.cyan(botName)}: ${wsUrl}`);
1947
+ const napcat = new napcat_sdk.NapCat({
1948
+ token,
1949
+ protocol,
1950
+ host,
1951
+ port,
1952
+ logger
1953
+ });
1954
+ return new Promise((resolve) => {
1955
+ napcat.on("ws.close", () => {
1956
+ logger.warn(`${consola_utils.colors.yellow(botName)} WS 连接已关闭`);
1957
+ });
1958
+ napcat.on("ws.error", (err) => {
1959
+ logger.error(`${consola_utils.colors.red(botName)} WS 连接错误: ${err}`);
1960
+ });
1961
+ napcat.once("napcat.connected", ({ user_id, nickname, app_name, app_version }) => {
1962
+ logger.info(`已连接到 ${consola_utils.colors.cyan(botName)}: ${consola_utils.colors.green(`${app_name}-v${app_version} ${nickname}(${user_id})`)}`);
1963
+ if (connectedBots.has(user_id)) {
1964
+ const existingBot = connectedBots.get(user_id);
1965
+ if (existingBot.name) logger.warn(`${consola_utils.colors.yellow(botName)} (${user_id}) 与 ${consola_utils.colors.yellow(existingBot.name)} (${user_id}) QQ 号重复,将跳过`);
1966
+ napcat.close();
1967
+ resolve(null);
1968
+ return;
1969
+ }
1970
+ const extendedNapCat = napcat;
1971
+ extendedNapCat.bot_id = user_id;
1972
+ extendedNapCat.app_name = app_name;
1973
+ extendedNapCat.app_version = app_version;
1974
+ extendedNapCat.name = botName;
1975
+ resolve(extendedNapCat);
1976
+ });
1977
+ napcat.run().catch((err) => {
1978
+ logger.error(`${consola_utils.colors.red(botName)} 连接失败: ${err.message}`);
1979
+ resolve(null);
1980
+ });
1981
+ });
1982
+ }
1983
+ async function setupPlugins(napcat, bots) {
1984
+ const plugin_dir = getAbsPluginDir();
1985
+ const mainBot = napcat;
1986
+ ensurePluginDir();
1987
+ const plugins = botConfig.plugins.map((p) => ({
1988
+ dirName: p,
1989
+ absPath: node_path.default.resolve(plugin_dir, p)
1990
+ })).filter((p) => {
1991
+ if (!node_fs.default.existsSync(p.absPath)) {
1992
+ mainBot.logger.warn(`插件 ${consola_utils.colors.red(p.dirName)} 不存在,已忽略`);
1993
+ return false;
1994
+ }
1995
+ return true;
1996
+ });
1997
+ const failedImportPlugins = [];
1998
+ const promises = plugins.map(async ({ absPath, dirName }) => {
1999
+ try {
2000
+ const plugin = await jiti$1.import(absPath, { default: true });
2001
+ if (plugin.name !== dirName) {
2002
+ const tip = `插件目录名 [${consola_utils.colors.yellow(dirName)}] 和插件声明的 name [${consola_utils.colors.yellow(plugin.name)}] 不一致,可能导致重载异常,请修改一致后重启。`;
2003
+ mainBot.logger.warn(tip);
2004
+ noticeMainOwner(mainBot, tip);
2005
+ }
2006
+ return plugin;
2007
+ } catch (e) {
2008
+ const err = stringifyError(e);
2009
+ failedImportPlugins.push([dirName, err]);
2010
+ return null;
2011
+ }
2012
+ });
2013
+ const start$1 = node_process.hrtime.bigint();
2014
+ const sortedUserPlugins = (await Promise.all(promises)).filter(Boolean).toSorted((prev, next) => (prev.priority ?? 100) - (next.priority ?? 100));
2015
+ if (failedImportPlugins.length) {
2016
+ const tip = `${consola_utils.colors.red(failedImportPlugins.length)} 个插件加载失败: \n\n${failedImportPlugins.map(([dirName, err]) => `${dirName}: ${err}`).join("\n\n")}`;
2017
+ mainBot.logger.warn(tip);
2018
+ noticeMainOwner(mainBot, tip);
2019
+ }
2020
+ const pluginGroups = /* @__PURE__ */ new Map();
2021
+ for (const plugin of sortedUserPlugins) {
2022
+ const priority = plugin.priority ?? 100;
2023
+ if (!pluginGroups.has(priority)) pluginGroups.set(priority, []);
2024
+ pluginGroups.get(priority).push(plugin);
2025
+ }
2026
+ const sortedGroups = Array.from(pluginGroups.entries()).toSorted(([a], [b]) => a - b);
2027
+ const failedEnablePlugins = [];
2028
+ try {
2029
+ mainBot.logger.info(`>>> 加载内置插件: ${BUILTIN_PLUGINS.map((p) => consola_utils.colors.cyan(p.name)).join(", ")}`);
2030
+ await Promise.all(BUILTIN_PLUGINS.map((p) => enablePlugin(bots, p, "builtin")));
2031
+ mainBot.logger.info(`>>> 加载用户插件: ${sortedGroups.map(([priority, plugins$1]) => `优先级 ${consola_utils.colors.yellow(priority)} (${plugins$1.map((p) => consola_utils.colors.cyan(p.name)).join(", ")})`).join(",")}`);
2032
+ for (const [_, plugins$1] of sortedGroups) await Promise.all(plugins$1.map(async (p) => {
2033
+ try {
2034
+ await enablePlugin(bots, p, "external");
2035
+ } catch (e) {
2036
+ failedEnablePlugins.push([p.name, stringifyError(e)]);
2037
+ }
2038
+ }));
2039
+ } catch (e) {
2040
+ mainBot.logger.error(e?.message);
2041
+ await noticeMainOwner(mainBot, e?.message).catch(() => {
2042
+ mainBot.logger.error("发送插件启用失败通知失败");
2043
+ });
2044
+ }
2045
+ const end = node_process.hrtime.bigint();
2046
+ const costTime = Math.round(Number(end - start$1)) / 1e6;
2047
+ const failedCount = failedImportPlugins.length + failedEnablePlugins.length;
2048
+ const failedInfo = failedCount > 0 ? `${consola_utils.colors.red(failedCount)} 个失败 (导入 ${consola_utils.colors.red(failedImportPlugins.length)},启用 ${consola_utils.colors.red(failedEnablePlugins.length)})` : "";
2049
+ mainBot.logger.info(`成功加载了 ${consola_utils.colors.green(runtimePlugins.size)} 个插件,${failedInfo ? failedInfo : ""}总耗时 ${consola_utils.colors.green(costTime.toFixed(2))} 毫秒`);
2050
+ mainBot.logger.info(consola_utils.colors.green(`mioki v${require_package.version} 启动完成,向机器人发送「${consola_utils.colors.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
2051
+ if (botConfig.online_push) await noticeMainOwner(mainBot, `✅ mioki v${require_package.version} 已就绪`).catch((err) => {
2052
+ mainBot.logger.error(`发送就绪通知失败: ${stringifyError(err)}`);
2053
+ });
2054
+ }
1754
2055
  async function start(options = {}) {
1755
2056
  const { cwd = process.cwd() } = options;
1756
2057
  if (cwd !== BOT_CWD.value) updateBotCWD(node_path.default.resolve(cwd));
@@ -1769,117 +2070,67 @@ async function start(options = {}) {
1769
2070
  logger.info(`${consola_utils.colors.dim("插件目录: ")}${consola_utils.colors.blue(plugin_dir)}`);
1770
2071
  logger.info(`${consola_utils.colors.dim("配置文件: ")}${consola_utils.colors.blue(`${BOT_CWD.value}/package.json`)}`);
1771
2072
  logger.info(consola_utils.colors.dim("=".repeat(40)));
1772
- const { protocol = "ws", port = 3001, host = "localhost", token = "" } = botConfig.napcat || {};
1773
- const wsUrl = consola_utils.colors.green(`${protocol}://${host}:${port}${token ? "?access_token=***" : ""}`);
1774
- logger.info(`>>> 正在连接 NapCat 实例: ${wsUrl}`);
1775
- const napcat = new napcat_sdk.NapCat({
1776
- token,
1777
- protocol,
1778
- host,
1779
- port,
1780
- logger
1781
- });
1782
- napcat.on("ws.close", () => {
1783
- logger.error("WS 连接失败,请确保 token 配置正确且 NapCat 实例正常运行");
2073
+ const napcatConfigs = botConfig.napcat;
2074
+ if (napcatConfigs.length === 0) {
2075
+ logger.warn("未配置任何 NapCat 实例,框架将以无实例模式启动");
2076
+ logger.info(consola_utils.colors.green(`mioki v${require_package.version} 启动完成,向机器人发送「${consola_utils.colors.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
2077
+ return;
2078
+ }
2079
+ const seenEndpoints = /* @__PURE__ */ new Set();
2080
+ const duplicateConfigs = [];
2081
+ for (const config of napcatConfigs) {
2082
+ const { protocol = "ws", host = "localhost", port = 3001 } = config;
2083
+ const endpoint = `${protocol}://${host}:${port}`;
2084
+ if (seenEndpoints.has(endpoint)) duplicateConfigs.push(`${config.name || "未命名"} (${endpoint})`);
2085
+ else seenEndpoints.add(endpoint);
2086
+ }
2087
+ if (duplicateConfigs.length > 0) {
2088
+ logger.error(`检测到重复的 NapCat 实例配置:`);
2089
+ duplicateConfigs.forEach((dup) => logger.error(` - ${dup}`));
2090
+ logger.error("请检查配置文件,确保每个实例的 host:port 组合唯一");
1784
2091
  process.exit(1);
1785
- });
1786
- napcat.on("napcat.connected", async ({ user_id, nickname, app_name, app_version }) => {
1787
- logger.info(`已连接到 NapCat 实例: ${consola_utils.colors.green(`${app_name}-v${app_version} ${nickname}(${user_id})`)}`);
1788
- process.title = `mioki v${require_package.version} ${app_name}-v${app_version}-${user_id}`;
1789
- let lastNoticeTime = 0;
2092
+ }
2093
+ logger.info(consola_utils.colors.dim("=".repeat(40)));
2094
+ logger.info(`>>> 正在连接 ${napcatConfigs.length} 个 NapCat 实例...`);
2095
+ const bots = (await Promise.all(napcatConfigs.map((config, index) => connectBot(config, index)))).filter((b) => b !== null);
2096
+ if (bots.length === 0) {
2097
+ logger.error("所有 NapCat 实例连接失败,框架无法启动");
2098
+ process.exit(1);
2099
+ }
2100
+ for (const bot of bots) connectedBots.set(bot.bot_id, bot);
2101
+ if (bots.length < napcatConfigs.length) logger.warn(`${consola_utils.colors.yellow(napcatConfigs.length - bots.length)} 个 NapCat 实例连接失败`);
2102
+ const botNames = bots.map((b) => `${b.name}(${b.bot_id})`).join(", ");
2103
+ logger.info(consola_utils.colors.green(`成功连接 ${bots.length} 个实例: ${botNames}`));
2104
+ logger.info(consola_utils.colors.dim("=".repeat(40)));
2105
+ const mainBot = bots[0];
2106
+ process.title = `mioki v${require_package.version} ${bots.map((b) => `${b.bot_id}`).join(", ")}`;
2107
+ let lastNoticeTime = 0;
2108
+ for (const bot of bots) {
1790
2109
  process.on("uncaughtException", async (err) => {
1791
2110
  const msg = stringifyError(err);
1792
- napcat.logger.error(`uncaughtException, 出错了: ${msg}`);
2111
+ bot.logger.error(`uncaughtException, 出错了: ${msg}`);
1793
2112
  if (botConfig.error_push) {
1794
2113
  if (Date.now() - lastNoticeTime < 1e3) return;
1795
2114
  lastNoticeTime = Date.now();
1796
- await noticeMainOwner(napcat, `mioki 发生未捕获异常:\n\n${msg}`).catch(() => {
1797
- napcat.logger.error("发送未捕获异常通知失败");
2115
+ await noticeMainOwner(mainBot, `mioki 发生未捕获异常:\n\n${msg}`).catch(() => {
2116
+ mainBot.logger.error("发送未捕获异常通知失败");
1798
2117
  });
1799
2118
  }
1800
2119
  });
1801
2120
  process.on("unhandledRejection", async (err) => {
1802
2121
  const msg = stringifyError(err);
1803
- napcat.logger.error(`unhandledRejection, 出错了: ${msg}`);
2122
+ bot.logger.error(`unhandledRejection, 出错了: ${msg}`);
1804
2123
  if (botConfig.error_push) {
1805
2124
  if (Date.now() - lastNoticeTime < 1e3) return;
1806
2125
  lastNoticeTime = Date.now();
1807
2126
  const date = (/* @__PURE__ */ new Date()).toLocaleString();
1808
- await noticeMainOwner(napcat, `【${date}】\n\nmioki 发生未处理异常:\n\n${msg}`).catch(() => {
1809
- napcat.logger.error("发送未处理异常通知失败");
2127
+ await noticeMainOwner(mainBot, `【${date}】\n\nmioki 发生未处理异常:\n\n${msg}`).catch(() => {
2128
+ mainBot.logger.error("发送未处理异常通知失败");
1810
2129
  });
1811
2130
  }
1812
2131
  });
1813
- ensurePluginDir();
1814
- const plugins = botConfig.plugins.map((p) => ({
1815
- dirName: p,
1816
- absPath: node_path.default.resolve(plugin_dir, p)
1817
- })).filter((p) => {
1818
- if (!node_fs.default.existsSync(p.absPath)) {
1819
- napcat.logger.warn(`插件 ${consola_utils.colors.red(p.dirName)} 不存在,已忽略`);
1820
- return false;
1821
- }
1822
- return true;
1823
- });
1824
- const failedImportPlugins = [];
1825
- const promises = plugins.map(async ({ absPath, dirName }) => {
1826
- try {
1827
- const plugin = await jiti$1.import(absPath, { default: true });
1828
- if (plugin.name !== dirName) {
1829
- const tip = `插件目录名 [${consola_utils.colors.yellow(dirName)}] 和插件声明的 name [${consola_utils.colors.yellow(plugin.name)}] 不一致,可能导致重载异常,请修改一致后重启。`;
1830
- napcat.logger.warn(tip);
1831
- noticeMainOwner(napcat, tip);
1832
- }
1833
- return plugin;
1834
- } catch (e) {
1835
- const err = stringifyError(e);
1836
- failedImportPlugins.push([dirName, err]);
1837
- return null;
1838
- }
1839
- });
1840
- const start$1 = node_process.hrtime.bigint();
1841
- const sortedUserPlugins = (await Promise.all(promises)).filter(Boolean).toSorted((prev, next) => (prev.priority ?? 100) - (next.priority ?? 100));
1842
- if (failedImportPlugins.length) {
1843
- const tip = `${consola_utils.colors.red(failedImportPlugins.length)} 个插件加载失败: \n\n${failedImportPlugins.map(([dirName, err]) => `${dirName}: ${err}`).join("\n\n")}`;
1844
- napcat.logger.warn(tip);
1845
- noticeMainOwner(napcat, tip);
1846
- }
1847
- const pluginGroups = /* @__PURE__ */ new Map();
1848
- for (const plugin of sortedUserPlugins) {
1849
- const priority = plugin.priority ?? 100;
1850
- if (!pluginGroups.has(priority)) pluginGroups.set(priority, []);
1851
- pluginGroups.get(priority).push(plugin);
1852
- }
1853
- const sortedGroups = Array.from(pluginGroups.entries()).toSorted(([a], [b]) => a - b);
1854
- const failedEnablePlugins = [];
1855
- try {
1856
- napcat.logger.info(`>>> 加载内置插件: ${BUILTIN_PLUGINS.map((p) => consola_utils.colors.cyan(p.name)).join(", ")}`);
1857
- await Promise.all(BUILTIN_PLUGINS.map((p) => enablePlugin(napcat, p, "builtin")));
1858
- napcat.logger.info(`>>> 加载用户插件: ${sortedGroups.map(([priority, plugins$1]) => `优先级 ${consola_utils.colors.yellow(priority)} (${plugins$1.map((p) => consola_utils.colors.cyan(p.name)).join(", ")})`).join(",")}`);
1859
- for (const [_, plugins$1] of sortedGroups) await Promise.all(plugins$1.map(async (p) => {
1860
- try {
1861
- await enablePlugin(napcat, p, "external");
1862
- } catch (e) {
1863
- failedEnablePlugins.push([p.name, stringifyError(e)]);
1864
- }
1865
- }));
1866
- } catch (e) {
1867
- napcat.logger.error(e?.message);
1868
- await noticeMainOwner(napcat, e?.message).catch(() => {
1869
- napcat.logger.error("发送插件启用失败通知失败");
1870
- });
1871
- }
1872
- const end = node_process.hrtime.bigint();
1873
- const costTime = Math.round(Number(end - start$1)) / 1e6;
1874
- const failedCount = failedImportPlugins.length + failedEnablePlugins.length;
1875
- const failedInfo = failedCount > 0 ? `${consola_utils.colors.red(failedCount)} 个失败 (导入 ${consola_utils.colors.red(failedImportPlugins.length)},启用 ${consola_utils.colors.red(failedEnablePlugins.length)})` : "";
1876
- napcat.logger.info(`成功加载了 ${consola_utils.colors.green(runtimePlugins.size)} 个插件,${failedInfo ? failedInfo : ""}总耗时 ${consola_utils.colors.green(costTime.toFixed(2))} 毫秒`);
1877
- napcat.logger.info(consola_utils.colors.green(`mioki v${require_package.version} 启动完成,向机器人发送「${consola_utils.colors.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
1878
- if (botConfig.online_push) await noticeMainOwner(napcat, `✅ mioki v${require_package.version} 已就绪`).catch((err) => {
1879
- napcat.logger.error(`发送就绪通知失败: ${stringifyError(err)}`);
1880
- });
1881
- });
1882
- await napcat.run();
2132
+ }
2133
+ await setupPlugins(mainBot, bots);
1883
2134
  }
1884
2135
 
1885
2136
  //#endregion
@@ -1888,6 +2139,8 @@ exports.BOT_CWD = BOT_CWD;
1888
2139
  exports.BUILTIN_PLUGINS = BUILTIN_PLUGINS;
1889
2140
  exports.CORE_PLUGINS = CORE_PLUGINS;
1890
2141
  exports.ChromeUA = ChromeUA;
2142
+ exports.Deduplicator = Deduplicator;
2143
+ exports.MessageDeduplicator = MessageDeduplicator;
1891
2144
  exports.START_TIME = START_TIME;
1892
2145
  exports.SystemMap = SystemMap;
1893
2146
  exports.addService = addService;
@@ -1914,6 +2167,7 @@ Object.defineProperty(exports, 'colors', {
1914
2167
  return consola_utils.colors;
1915
2168
  }
1916
2169
  });
2170
+ exports.connectedBots = connectedBots;
1917
2171
  exports.createCmd = createCmd;
1918
2172
  exports.createDB = createDB;
1919
2173
  exports.createForwardMsg = createForwardMsg;
@@ -1930,6 +2184,7 @@ Object.defineProperty(exports, 'dedent', {
1930
2184
  return dedent.default;
1931
2185
  }
1932
2186
  });
2187
+ exports.deduplicator = deduplicator;
1933
2188
  exports.definePlugin = definePlugin;
1934
2189
  exports.enablePlugin = enablePlugin;
1935
2190
  exports.ensureBuffer = ensureBuffer;
@@ -2000,6 +2255,7 @@ Object.defineProperty(exports, 'mri', {
2000
2255
  }
2001
2256
  });
2002
2257
  exports.noNullish = noNullish;
2258
+ exports.normalizeNapCatConfig = normalizeNapCatConfig;
2003
2259
  exports.noticeAdmins = noticeAdmins;
2004
2260
  exports.noticeFriends = noticeFriends;
2005
2261
  exports.noticeGroups = noticeGroups;