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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BiucMVzj.mjs";
2
- import { t as version } from "./package-D5I4iirh.mjs";
2
+ import { t as version } from "./package-B4h-fhCq.mjs";
3
3
  import fs, { default as fs$1 } from "node:fs";
4
4
  import util from "node:util";
5
5
  import path, { default as path$1 } from "node:path";
@@ -779,12 +779,20 @@ var config_exports = /* @__PURE__ */ __exportAll({
779
779
  isInPm2: () => isInPm2,
780
780
  isOwner: () => isOwner,
781
781
  isOwnerOrAdmin: () => isOwnerOrAdmin,
782
+ normalizeNapCatConfig: () => normalizeNapCatConfig,
782
783
  readMiokiConfig: () => readMiokiConfig,
783
784
  readPackageJson: () => readPackageJson,
784
785
  updateBotCWD: () => updateBotCWD,
785
786
  updateBotConfig: () => updateBotConfig,
786
787
  writePackageJson: () => writePackageJson
787
788
  });
789
+ function isSingleNapCatConfig(config) {
790
+ return !Array.isArray(config);
791
+ }
792
+ function normalizeNapCatConfig(config) {
793
+ if (isSingleNapCatConfig(config)) return [config];
794
+ return config;
795
+ }
788
796
  /**
789
797
  * 机器人根目录
790
798
  */
@@ -800,7 +808,10 @@ function readMiokiConfig() {
800
808
  const config = readPackageJson().mioki;
801
809
  if (!config) throw new Error(`无法在 package.json 中找到 mioki 配置,请确认 package.json 文件中是否包含 mioki 字段`);
802
810
  if (!config.napcat) throw new Error(`mioki 配置中缺少 napcat 字段,请补全后重试`);
803
- return readPackageJson().mioki;
811
+ return {
812
+ ...config,
813
+ napcat: normalizeNapCatConfig(config.napcat)
814
+ };
804
815
  }
805
816
  /**
806
817
  * `mioki` 框架相关配置
@@ -1201,9 +1212,132 @@ function addService(name, service, cover = true) {
1201
1212
  function bindBot(bot, func) {
1202
1213
  return (...args) => func(bot, ...args);
1203
1214
  }
1215
+ /**
1216
+ * 去重器
1217
+ * 处理多个 bot 在相同场景下,同一事件只处理一次
1218
+ */
1219
+ var Deduplicator = class {
1220
+ processedEvents = /* @__PURE__ */ new Set();
1221
+ maxSize = 1e3;
1222
+ /**
1223
+ * 获取事件类型键
1224
+ */
1225
+ getEventTypeKey(e) {
1226
+ const { post_type } = e;
1227
+ if (post_type === "message") return `msg:${e.message_type}`;
1228
+ if (post_type === "request") {
1229
+ if (e.request_type === "friend") return "req:friend";
1230
+ return `req:group:${e.sub_type ?? "unknown"}`;
1231
+ }
1232
+ if (post_type === "notice") {
1233
+ if (e.notice_type === "group") return `notice:group:${e.sub_type}`;
1234
+ return `notice:${e.notice_type}:${e.sub_type}`;
1235
+ }
1236
+ return "unknown";
1237
+ }
1238
+ getGroupMessageKey(e) {
1239
+ const groupId = e.group_id ?? "_";
1240
+ const userId = e.user_id ?? "_";
1241
+ const time = e.time ?? "_";
1242
+ const raw = e.raw_message ?? "_";
1243
+ return `msg:group:${groupId}:${userId}:${time}:${crypto.createHash("md5").update(raw).digest("hex")}`;
1244
+ }
1245
+ getNoticeGroupKey(e) {
1246
+ 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 ?? "_"}`;
1247
+ }
1248
+ getRequestKey(e) {
1249
+ const typeKey = this.getEventTypeKey(e);
1250
+ const userId = e.user_id ?? "_";
1251
+ const groupId = "group_id" in e ? e.group_id ?? "_" : "_";
1252
+ const time = e.time ?? "_";
1253
+ const comment = e.comment ?? "_";
1254
+ return `${typeKey}:${userId}:${groupId}:${time}:${comment ? crypto.createHash("md5").update(comment).digest("hex") : "_"}`;
1255
+ }
1256
+ /**
1257
+ * 生成事件唯一键
1258
+ */
1259
+ getKey(e) {
1260
+ const typeKey = this.getEventTypeKey(e);
1261
+ if (typeKey === "msg:group") return this.getGroupMessageKey(e);
1262
+ if (typeKey.startsWith("notice:group:")) return this.getNoticeGroupKey(e);
1263
+ if (typeKey.startsWith("req:")) return this.getRequestKey(e);
1264
+ return "";
1265
+ }
1266
+ /**
1267
+ * 检查事件是否已处理过
1268
+ * @param event
1269
+ * @param scope
1270
+ */
1271
+ isProcessed(event, scope) {
1272
+ const key = scope ? `${this.getKey(event)}:${scope}` : this.getKey(event);
1273
+ return this.processedEvents.has(key);
1274
+ }
1275
+ /**
1276
+ * 标记事件为已处理
1277
+ * @param event
1278
+ * @param scope 需与 isProcessed 一致
1279
+ */
1280
+ markProcessed(event, scope) {
1281
+ const key = scope ? `${this.getKey(event)}:${scope}` : this.getKey(event);
1282
+ if (this.processedEvents.size >= this.maxSize) {
1283
+ const first = this.processedEvents.values().next();
1284
+ if (!first.done) this.processedEvents.delete(first.value);
1285
+ }
1286
+ this.processedEvents.add(key);
1287
+ }
1288
+ };
1289
+ /**
1290
+ * 消息去重器
1291
+ * @deprecated 请使用 Deduplicator,它支持更多事件类型
1292
+ */
1293
+ var MessageDeduplicator = class extends Deduplicator {
1294
+ isProcessed(event) {
1295
+ return super.isProcessed(event);
1296
+ }
1297
+ markProcessed(event) {
1298
+ super.markProcessed(event);
1299
+ }
1300
+ };
1301
+ const deduplicator = new Deduplicator();
1204
1302
  const runtimePlugins = /* @__PURE__ */ new Map();
1205
1303
  const buildRemovedActions = (bot) => Object.fromEntries(Object.entries(actions_exports).map(([k, v]) => [k, bindBot(bot, v)]));
1206
1304
  /**
1305
+ * 检查事件是否是群消息事件
1306
+ */
1307
+ function isGroupMessageEvent(event) {
1308
+ return event?.post_type === "message" && event?.message_type === "group";
1309
+ }
1310
+ /**
1311
+ * 检查事件是否是私聊消息事件
1312
+ */
1313
+ function isPrivateMessageEvent(event) {
1314
+ return event?.post_type === "message" && event?.message_type === "private";
1315
+ }
1316
+ /**
1317
+ * 检查事件是否是消息事件
1318
+ */
1319
+ function isMessageEvent(event) {
1320
+ return isGroupMessageEvent(event) || isPrivateMessageEvent(event);
1321
+ }
1322
+ /**
1323
+ * 检查事件是否是请求事件
1324
+ */
1325
+ function isRequestEvent(event) {
1326
+ return event?.post_type === "request";
1327
+ }
1328
+ /**
1329
+ * 检查事件是否是群通知事件
1330
+ */
1331
+ function isGroupNoticeEvent(event) {
1332
+ return event?.post_type === "notice" && event?.notice_type === "group";
1333
+ }
1334
+ /**
1335
+ * 检查事件是否需要去重
1336
+ */
1337
+ function isDeduplicableEvent(event) {
1338
+ return isMessageEvent(event) || isRequestEvent(event) || isGroupNoticeEvent(event);
1339
+ }
1340
+ /**
1207
1341
  * 定义一个 Mioki 插件
1208
1342
  * @param plugin Mioki 插件对象
1209
1343
  * @returns Mioki 插件对象
@@ -1225,55 +1359,83 @@ function getAbsPluginDir(defaultDir = "plugins") {
1225
1359
  const cwd = BOT_CWD.value;
1226
1360
  return path$1.join(cwd, botConfig.plugins_dir || defaultDir);
1227
1361
  }
1228
- async function enablePlugin(bot, plugin, type = "external") {
1362
+ async function enablePlugin(bots, plugin, type = "external") {
1229
1363
  const typeDesc = type === "builtin" ? "内置" : "用户";
1230
1364
  const pluginName = plugin.name || "null";
1231
1365
  const { name = pluginName, version: version$1 = "null", description = "-", setup = () => {} } = plugin;
1366
+ if (!bots[0]) throw new Error("没有可用的 bot 实例");
1232
1367
  try {
1233
1368
  const start$1 = hrtime.bigint();
1234
1369
  const clears = /* @__PURE__ */ new Set();
1235
1370
  const userClears = /* @__PURE__ */ new Set();
1236
- const logger$1 = bot.logger.withDefaults({
1237
- tag: `plugin:${name}`,
1238
- args: [name]
1239
- });
1240
- const context = {
1241
- bot,
1242
- segment: bot.segment,
1243
- getCookie: bot.getCookie.bind(bot),
1244
- ...utils_exports,
1245
- ...config_exports,
1246
- ...buildRemovedActions(bot),
1247
- logger: logger$1,
1248
- services,
1249
- clears: userClears,
1250
- addService: (name$1, service, cover) => {
1251
- const remove = addService(name$1, service, cover);
1252
- clears.add(remove);
1253
- return remove;
1254
- },
1255
- handle: (eventName, handler) => {
1256
- logger$1.debug(`Registering event handler for event: ${String(eventName)}`);
1257
- bot.on(eventName, handler);
1258
- const unsubscribe = () => {
1259
- logger$1.debug(`Unregistering event handler for event: ${String(eventName)}`);
1260
- bot.off(eventName, handler);
1261
- };
1262
- clears.add(unsubscribe);
1263
- return unsubscribe;
1264
- },
1265
- cron: (cronExpression, handler) => {
1266
- logger$1.debug(`Scheduling cron job: ${cronExpression}`);
1267
- const job = nodeCron.schedule(cronExpression, (now) => handler(context, now));
1268
- const clear = () => {
1269
- logger$1.debug(`Stopping cron job: ${cronExpression}`);
1270
- job.stop();
1271
- };
1272
- clears.add(clear);
1273
- return job;
1274
- }
1371
+ const logger$1 = logger.withDefaults({ tag: `plugin:${name}` });
1372
+ const createContext = (bot) => {
1373
+ return {
1374
+ bot,
1375
+ bots,
1376
+ self_id: bot.bot_id,
1377
+ segment: bot.segment,
1378
+ getCookie: bot.getCookie.bind(bot),
1379
+ ...utils_exports,
1380
+ ...config_exports,
1381
+ ...buildRemovedActions(bot),
1382
+ logger: logger$1,
1383
+ services,
1384
+ clears: userClears,
1385
+ deduplicator,
1386
+ addService: (name$1, service, cover) => {
1387
+ const remove = addService(name$1, service, cover);
1388
+ clears.add(remove);
1389
+ return remove;
1390
+ },
1391
+ handle: (eventName, handler, options = {}) => {
1392
+ logger$1.debug(`Registering event handler for event: ${String(eventName)}`);
1393
+ const { deduplicate = true } = options;
1394
+ const dedupeScope = `${name}:${String(eventName)}:${crypto.randomUUID()}`;
1395
+ const unsubscribes = [];
1396
+ for (const bot$1 of bots) {
1397
+ const wrappedHandler = (e) => {
1398
+ if (isPrivateMessageEvent(e)) {
1399
+ if (e.self_id !== bot$1.bot_id) return;
1400
+ }
1401
+ const senderUserId = e.user_id;
1402
+ const senderOperatorId = e.operator_id;
1403
+ const isFromConnectedBot = senderUserId && bots.some((b) => b.bot_id === senderUserId);
1404
+ const isFromConnectedBotOperator = senderOperatorId && bots.some((b) => b.bot_id === senderOperatorId);
1405
+ if (isFromConnectedBot || isFromConnectedBotOperator) return;
1406
+ if (deduplicate && isDeduplicableEvent(e)) {
1407
+ if (deduplicator.isProcessed(e, dedupeScope)) return;
1408
+ deduplicator.markProcessed(e, dedupeScope);
1409
+ }
1410
+ handler(e);
1411
+ };
1412
+ bot$1.on(eventName, wrappedHandler);
1413
+ const unsubscribe = () => {
1414
+ logger$1.debug(`Unregistering event handler for event: ${String(eventName)}`);
1415
+ bot$1.off(eventName, wrappedHandler);
1416
+ };
1417
+ unsubscribes.push(unsubscribe);
1418
+ }
1419
+ const clearAll = () => {
1420
+ unsubscribes.forEach((fn) => fn());
1421
+ };
1422
+ clears.add(clearAll);
1423
+ return clearAll;
1424
+ },
1425
+ cron: (cronExpression, handler) => {
1426
+ logger$1.debug(`Scheduling cron job: ${cronExpression}`);
1427
+ const job = nodeCron.schedule(cronExpression, (now) => handler(createContext(bot), now));
1428
+ const clear = () => {
1429
+ logger$1.debug(`Stopping cron job: ${cronExpression}`);
1430
+ job.stop();
1431
+ };
1432
+ clears.add(clear);
1433
+ return job;
1434
+ }
1435
+ };
1275
1436
  };
1276
- clears.add(await setup(context) || (() => {}));
1437
+ const mainContext = createContext(bots[0]);
1438
+ clears.add(await setup(mainContext) || (() => {}));
1277
1439
  runtimePlugins.set(name, {
1278
1440
  name,
1279
1441
  type,
@@ -1292,7 +1454,7 @@ async function enablePlugin(bot, plugin, type = "external") {
1292
1454
  });
1293
1455
  const end = hrtime.bigint();
1294
1456
  const time = Math.round(Number(end - start$1)) / 1e6;
1295
- bot.logger.info(`- 启用插件 ${colors$1.yellow(`[${typeDesc}]`)} ${colors$1.yellow(`${name}@${version$1}`)} => 耗时 ${colors$1.green(time.toFixed(2))} 毫秒`);
1457
+ logger$1.info(`- 启用插件 ${colors$1.yellow(`[${typeDesc}]`)} ${colors$1.yellow(`${name}@${version$1}`)} => 耗时 ${colors$1.green(time.toFixed(2))} 毫秒`);
1296
1458
  } catch (e) {
1297
1459
  throw new Error(`启用插件 ${colors$1.yellow(`[${typeDesc}]`)} ${colors$1.yellow(`${name}@${version$1}`)} 失败: ${e?.message}`);
1298
1460
  }
@@ -1318,18 +1480,12 @@ const ArchMap = {
1318
1480
  arm64: "arm64",
1319
1481
  x64: "x64"
1320
1482
  };
1321
- async function getMiokiStatus(bot) {
1483
+ async function getMiokiStatus(bots) {
1322
1484
  const osType = os.type();
1323
1485
  const osArch = os.arch();
1324
1486
  const isInUnix = ["Linux", "Darwin"].includes(osType);
1325
1487
  const arch = ArchMap[osArch] || osArch;
1326
- const [osInfo, localPlugins, versionInfo, friendList, groupList] = await Promise.all([
1327
- systemInfo.osInfo(),
1328
- findLocalPlugins(),
1329
- bot.getVersionInfo(),
1330
- bot.getFriendList(),
1331
- bot.getGroupList()
1332
- ]);
1488
+ const [osInfo, localPlugins] = await Promise.all([systemInfo.osInfo(), findLocalPlugins()]);
1333
1489
  const pluginCount = localPlugins.length + BUILTIN_PLUGINS.length;
1334
1490
  const system = isInUnix ? {
1335
1491
  name: osInfo.distro,
@@ -1343,27 +1499,58 @@ async function getMiokiStatus(bot) {
1343
1499
  const rssMem = process.memoryUsage.rss();
1344
1500
  const nodeVersion = process.versions.node;
1345
1501
  const cpu = getCpuInfo();
1346
- return {
1347
- bot: {
1348
- uin: bot.uin,
1502
+ const botStatuses = [];
1503
+ let totalSend = 0;
1504
+ let totalReceive = 0;
1505
+ let mainVersionInfo = {
1506
+ app_version: "unknown",
1507
+ protocol_version: "unknown"
1508
+ };
1509
+ for (const bot of bots) try {
1510
+ const [versionInfo, friendList, groupList] = await Promise.all([
1511
+ bot.getVersionInfo(),
1512
+ bot.getFriendList(),
1513
+ bot.getGroupList()
1514
+ ]);
1515
+ mainVersionInfo = versionInfo;
1516
+ botStatuses.push({
1517
+ uin: bot.bot_id,
1349
1518
  nickname: bot.nickname,
1519
+ name: bot.name,
1350
1520
  friends: friendList.length,
1351
- groups: groupList.length
1352
- },
1521
+ groups: groupList.length,
1522
+ send: bot.stat.send.group + bot.stat.send.private,
1523
+ receive: bot.stat.recv.group + bot.stat.recv.private
1524
+ });
1525
+ totalSend += bot.stat.send.group + bot.stat.send.private;
1526
+ totalReceive += bot.stat.recv.group + bot.stat.recv.private;
1527
+ } catch (err) {
1528
+ botStatuses.push({
1529
+ uin: bot.bot_id,
1530
+ nickname: bot.nickname,
1531
+ name: bot.name,
1532
+ friends: 0,
1533
+ groups: 0,
1534
+ send: 0,
1535
+ receive: 0
1536
+ });
1537
+ }
1538
+ return {
1539
+ bots: botStatuses,
1353
1540
  plugins: {
1354
1541
  enabled: runtimePlugins.size,
1355
1542
  total: pluginCount
1356
1543
  },
1357
1544
  stats: {
1358
1545
  uptime: process.uptime() * 1e3,
1359
- send: bot.stat.send.group + bot.stat.send.private,
1360
- receive: bot.stat.recv.group + bot.stat.recv.private
1546
+ send: totalSend,
1547
+ receive: totalReceive
1361
1548
  },
1362
1549
  versions: {
1363
1550
  node: nodeVersion,
1364
1551
  mioki: version,
1365
- napcat: versionInfo.app_version,
1366
- protocol: versionInfo.protocol_version
1552
+ napcat: mainVersionInfo.app_version,
1553
+ protocol: mainVersionInfo.protocol_version
1367
1554
  },
1368
1555
  system: {
1369
1556
  name: system.name || "N/A",
@@ -1393,16 +1580,16 @@ async function getMiokiStatus(bot) {
1393
1580
  };
1394
1581
  }
1395
1582
  async function formatMiokiStatus(status) {
1396
- const { bot, plugins, stats, system, disk, cpu, memory, versions } = status;
1583
+ const { bots, plugins, stats, system, disk, cpu, memory, versions } = status;
1397
1584
  const diskValid = disk.total > 0 && disk.free >= 0;
1398
1585
  const diskDesc = `${disk.percent}%-${filesize(disk.used, { round: 1 })}/${filesize(disk.total, { round: 1 })}`;
1399
1586
  return `
1400
1587
  〓 🟢 mioki 状态 〓
1401
- 👤 ${bot.nickname}
1402
- 🆔 ${bot.uin}
1403
- 📋 ${localNum(bot.friends)} 好友 / ${localNum(bot.groups)}
1588
+ ${bots.map((bot, index) => {
1589
+ return `👤 ${bot.name ? `[${bot.name}] ` : ""}${bot.nickname} (${bot.uin})\n 📋 ${localNum(bot.friends)} 好友 / ${localNum(bot.groups)} 群 / 📮 收 ${localNum(bot.receive)} 发 ${localNum(bot.send)}`;
1590
+ }).join("\n")}
1404
1591
  🧩 启用了 ${localNum(plugins.enabled)} 个插件,共 ${localNum(plugins.total)} 个
1405
- 📮 收 ${localNum(stats.receive)} 条,发 ${localNum(stats.send)} 条
1592
+ 📮 总计: 收 ${localNum(stats.receive)} 条,发 ${localNum(stats.send)} 条
1406
1593
  🚀 ${filesize(memory.rss.used, { round: 1 })}/${memory.percent}%
1407
1594
  ⏳ 已运行 ${prettyMs(stats.uptime, {
1408
1595
  hideYear: true,
@@ -1486,7 +1673,7 @@ const core = definePlugin({
1486
1673
  const displayPrefix = prefix.replace(/\\\\/g, "\\");
1487
1674
  const statusAdminOnly = ctx.botConfig.status_permission === "admin-only";
1488
1675
  let statusFormatter = (status) => formatMiokiStatus(status);
1489
- ctx.addService("getMiokiStatus", () => getMiokiStatus(ctx.bot));
1676
+ ctx.addService("getMiokiStatus", () => getMiokiStatus(ctx.bots));
1490
1677
  ctx.addService("formatMiokiStatus", (status) => formatMiokiStatus(status));
1491
1678
  ctx.addService("customFormatMiokiStatus", (formatter) => statusFormatter = formatter);
1492
1679
  ctx.handle("message", (e) => ctx.runWithErrorHandler(async () => {
@@ -1494,7 +1681,7 @@ const core = definePlugin({
1494
1681
  if (!cmdPrefix.test(text$1)) return;
1495
1682
  if (statusAdminOnly && !ctx.hasRight(e)) return;
1496
1683
  if (text$1.replace(cmdPrefix, "") === "状态") {
1497
- const status = await statusFormatter(await getMiokiStatus(ctx.bot));
1684
+ const status = await statusFormatter(await getMiokiStatus(ctx.bots));
1498
1685
  await e.reply(status);
1499
1686
  return;
1500
1687
  }
@@ -1559,10 +1746,10 @@ const core = definePlugin({
1559
1746
  const plugin = await ctx.jiti.import(pluginPath, { default: true });
1560
1747
  if (plugin.name !== target) {
1561
1748
  const tip = `[插件目录名称: ${target}] 和插件代码中设置的 [name: ${plugin.name}] 不一致,可能导致重载异常,请修改后重启。`;
1562
- ctx.bot.logger.warn(tip);
1749
+ ctx.logger.warn(tip);
1563
1750
  ctx.noticeMainOwner(tip);
1564
1751
  }
1565
- await enablePlugin(ctx.bot, plugin);
1752
+ await enablePlugin(ctx.bots, plugin);
1566
1753
  } catch (err) {
1567
1754
  await e.reply(`插件 ${target} 启用失败:${err?.message || "未知错误"}`, true);
1568
1755
  return;
@@ -1588,7 +1775,7 @@ const core = definePlugin({
1588
1775
  break;
1589
1776
  }
1590
1777
  await ctx.updateBotConfig((c) => c.plugins = ctx.botConfig.plugins.filter((name) => name !== target));
1591
- ctx.bot.logger.info(`禁用插件 => ${target}`);
1778
+ ctx.logger.info(`禁用插件 => ${target}`);
1592
1779
  await e.reply(`插件 ${target} 已禁用`, true);
1593
1780
  break;
1594
1781
  }
@@ -1610,10 +1797,10 @@ const core = definePlugin({
1610
1797
  const importedPlugin = await ctx.jiti.import(pluginPath, { default: true });
1611
1798
  if (importedPlugin.name !== target) {
1612
1799
  const tip = `插件目录名称: ${target} 和插件代码中设置的 name: ${importedPlugin.name} 不一致,可能导致重载异常,请修改后重启。`;
1613
- ctx.bot.logger.warn(tip);
1800
+ ctx.logger.warn(tip);
1614
1801
  ctx.noticeMainOwner(tip);
1615
1802
  }
1616
- await enablePlugin(ctx.bot, importedPlugin);
1803
+ await enablePlugin(ctx.bots, importedPlugin);
1617
1804
  } catch (err) {
1618
1805
  await e.reply(err?.message, true);
1619
1806
  await ctx.updateBotConfig((c) => c.plugins = c.plugins.filter((name) => name !== target));
@@ -1726,7 +1913,7 @@ const core = definePlugin({
1726
1913
  break;
1727
1914
  case "退出":
1728
1915
  await e.reply("またね~", true);
1729
- ctx.bot.logger.info("接收到退出指令,即将退出... 如需自动重启,请使用 pm2 部署。");
1916
+ ctx.logger.info("接收到退出指令,即将退出... 如需自动重启,请使用 pm2 部署。");
1730
1917
  process.exit(0);
1731
1918
  }
1732
1919
  }, e));
@@ -1740,6 +1927,120 @@ const BUILTIN_PLUGINS = [core_default];
1740
1927
 
1741
1928
  //#endregion
1742
1929
  //#region src/start.ts
1930
+ const connectedBots = /* @__PURE__ */ new Map();
1931
+ async function connectBot(config, index) {
1932
+ const { protocol = "ws", port = 3001, host = "localhost", token = "", name } = config;
1933
+ const botName = name || `Bot${index + 1}`;
1934
+ const wsUrl = colors$1.green(`${protocol}://${host}:${port}${token ? "?access_token=***" : ""}`);
1935
+ logger.info(`>>> 正在连接 ${colors$1.cyan(botName)}: ${wsUrl}`);
1936
+ const napcat = new NapCat({
1937
+ token,
1938
+ protocol,
1939
+ host,
1940
+ port,
1941
+ logger
1942
+ });
1943
+ return new Promise((resolve) => {
1944
+ napcat.on("ws.close", () => {
1945
+ logger.warn(`${colors$1.yellow(botName)} WS 连接已关闭`);
1946
+ });
1947
+ napcat.on("ws.error", (err) => {
1948
+ logger.error(`${colors$1.red(botName)} WS 连接错误: ${err}`);
1949
+ });
1950
+ napcat.once("napcat.connected", ({ user_id, nickname, app_name, app_version }) => {
1951
+ logger.info(`已连接到 ${colors$1.cyan(botName)}: ${colors$1.green(`${app_name}-v${app_version} ${nickname}(${user_id})`)}`);
1952
+ if (connectedBots.has(user_id)) {
1953
+ const existingBot = connectedBots.get(user_id);
1954
+ if (existingBot.name) logger.warn(`${colors$1.yellow(botName)} (${user_id}) 与 ${colors$1.yellow(existingBot.name)} (${user_id}) QQ 号重复,将跳过`);
1955
+ napcat.close();
1956
+ resolve(null);
1957
+ return;
1958
+ }
1959
+ const extendedNapCat = napcat;
1960
+ extendedNapCat.bot_id = user_id;
1961
+ extendedNapCat.app_name = app_name;
1962
+ extendedNapCat.app_version = app_version;
1963
+ extendedNapCat.name = botName;
1964
+ resolve(extendedNapCat);
1965
+ });
1966
+ napcat.run().catch((err) => {
1967
+ logger.error(`${colors$1.red(botName)} 连接失败: ${err.message}`);
1968
+ resolve(null);
1969
+ });
1970
+ });
1971
+ }
1972
+ async function setupPlugins(napcat, bots) {
1973
+ const plugin_dir = getAbsPluginDir();
1974
+ const mainBot = napcat;
1975
+ ensurePluginDir();
1976
+ const plugins = botConfig.plugins.map((p) => ({
1977
+ dirName: p,
1978
+ absPath: path$1.resolve(plugin_dir, p)
1979
+ })).filter((p) => {
1980
+ if (!fs$1.existsSync(p.absPath)) {
1981
+ mainBot.logger.warn(`插件 ${colors$1.red(p.dirName)} 不存在,已忽略`);
1982
+ return false;
1983
+ }
1984
+ return true;
1985
+ });
1986
+ const failedImportPlugins = [];
1987
+ const promises = plugins.map(async ({ absPath, dirName }) => {
1988
+ try {
1989
+ const plugin = await jiti.import(absPath, { default: true });
1990
+ if (plugin.name !== dirName) {
1991
+ const tip = `插件目录名 [${colors$1.yellow(dirName)}] 和插件声明的 name [${colors$1.yellow(plugin.name)}] 不一致,可能导致重载异常,请修改一致后重启。`;
1992
+ mainBot.logger.warn(tip);
1993
+ noticeMainOwner(mainBot, tip);
1994
+ }
1995
+ return plugin;
1996
+ } catch (e) {
1997
+ const err = stringifyError(e);
1998
+ failedImportPlugins.push([dirName, err]);
1999
+ return null;
2000
+ }
2001
+ });
2002
+ const start$1 = hrtime.bigint();
2003
+ const sortedUserPlugins = (await Promise.all(promises)).filter(Boolean).toSorted((prev, next) => (prev.priority ?? 100) - (next.priority ?? 100));
2004
+ if (failedImportPlugins.length) {
2005
+ const tip = `${colors$1.red(failedImportPlugins.length)} 个插件加载失败: \n\n${failedImportPlugins.map(([dirName, err]) => `${dirName}: ${err}`).join("\n\n")}`;
2006
+ mainBot.logger.warn(tip);
2007
+ noticeMainOwner(mainBot, tip);
2008
+ }
2009
+ const pluginGroups = /* @__PURE__ */ new Map();
2010
+ for (const plugin of sortedUserPlugins) {
2011
+ const priority = plugin.priority ?? 100;
2012
+ if (!pluginGroups.has(priority)) pluginGroups.set(priority, []);
2013
+ pluginGroups.get(priority).push(plugin);
2014
+ }
2015
+ const sortedGroups = Array.from(pluginGroups.entries()).toSorted(([a], [b]) => a - b);
2016
+ const failedEnablePlugins = [];
2017
+ try {
2018
+ mainBot.logger.info(`>>> 加载内置插件: ${BUILTIN_PLUGINS.map((p) => colors$1.cyan(p.name)).join(", ")}`);
2019
+ await Promise.all(BUILTIN_PLUGINS.map((p) => enablePlugin(bots, p, "builtin")));
2020
+ mainBot.logger.info(`>>> 加载用户插件: ${sortedGroups.map(([priority, plugins$1]) => `优先级 ${colors$1.yellow(priority)} (${plugins$1.map((p) => colors$1.cyan(p.name)).join(", ")})`).join(",")}`);
2021
+ for (const [_, plugins$1] of sortedGroups) await Promise.all(plugins$1.map(async (p) => {
2022
+ try {
2023
+ await enablePlugin(bots, p, "external");
2024
+ } catch (e) {
2025
+ failedEnablePlugins.push([p.name, stringifyError(e)]);
2026
+ }
2027
+ }));
2028
+ } catch (e) {
2029
+ mainBot.logger.error(e?.message);
2030
+ await noticeMainOwner(mainBot, e?.message).catch(() => {
2031
+ mainBot.logger.error("发送插件启用失败通知失败");
2032
+ });
2033
+ }
2034
+ const end = hrtime.bigint();
2035
+ const costTime = Math.round(Number(end - start$1)) / 1e6;
2036
+ const failedCount = failedImportPlugins.length + failedEnablePlugins.length;
2037
+ const failedInfo = failedCount > 0 ? `${colors$1.red(failedCount)} 个失败 (导入 ${colors$1.red(failedImportPlugins.length)},启用 ${colors$1.red(failedEnablePlugins.length)})` : "";
2038
+ mainBot.logger.info(`成功加载了 ${colors$1.green(runtimePlugins.size)} 个插件,${failedInfo ? failedInfo : ""}总耗时 ${colors$1.green(costTime.toFixed(2))} 毫秒`);
2039
+ mainBot.logger.info(colors$1.green(`mioki v${version} 启动完成,向机器人发送「${colors$1.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
2040
+ if (botConfig.online_push) await noticeMainOwner(mainBot, `✅ mioki v${version} 已就绪`).catch((err) => {
2041
+ mainBot.logger.error(`发送就绪通知失败: ${stringifyError(err)}`);
2042
+ });
2043
+ }
1743
2044
  async function start(options = {}) {
1744
2045
  const { cwd = process.cwd() } = options;
1745
2046
  if (cwd !== BOT_CWD.value) updateBotCWD(path$1.resolve(cwd));
@@ -1758,119 +2059,69 @@ async function start(options = {}) {
1758
2059
  logger.info(`${colors$1.dim("插件目录: ")}${colors$1.blue(plugin_dir)}`);
1759
2060
  logger.info(`${colors$1.dim("配置文件: ")}${colors$1.blue(`${BOT_CWD.value}/package.json`)}`);
1760
2061
  logger.info(colors$1.dim("=".repeat(40)));
1761
- const { protocol = "ws", port = 3001, host = "localhost", token = "" } = botConfig.napcat || {};
1762
- const wsUrl = colors$1.green(`${protocol}://${host}:${port}${token ? "?access_token=***" : ""}`);
1763
- logger.info(`>>> 正在连接 NapCat 实例: ${wsUrl}`);
1764
- const napcat = new NapCat({
1765
- token,
1766
- protocol,
1767
- host,
1768
- port,
1769
- logger
1770
- });
1771
- napcat.on("ws.close", () => {
1772
- logger.error("WS 连接失败,请确保 token 配置正确且 NapCat 实例正常运行");
2062
+ const napcatConfigs = botConfig.napcat;
2063
+ if (napcatConfigs.length === 0) {
2064
+ logger.warn("未配置任何 NapCat 实例,框架将以无实例模式启动");
2065
+ logger.info(colors$1.green(`mioki v${version} 启动完成,向机器人发送「${colors$1.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
2066
+ return;
2067
+ }
2068
+ const seenEndpoints = /* @__PURE__ */ new Set();
2069
+ const duplicateConfigs = [];
2070
+ for (const config of napcatConfigs) {
2071
+ const { protocol = "ws", host = "localhost", port = 3001 } = config;
2072
+ const endpoint = `${protocol}://${host}:${port}`;
2073
+ if (seenEndpoints.has(endpoint)) duplicateConfigs.push(`${config.name || "未命名"} (${endpoint})`);
2074
+ else seenEndpoints.add(endpoint);
2075
+ }
2076
+ if (duplicateConfigs.length > 0) {
2077
+ logger.error(`检测到重复的 NapCat 实例配置:`);
2078
+ duplicateConfigs.forEach((dup) => logger.error(` - ${dup}`));
2079
+ logger.error("请检查配置文件,确保每个实例的 host:port 组合唯一");
1773
2080
  process.exit(1);
1774
- });
1775
- napcat.on("napcat.connected", async ({ user_id, nickname, app_name, app_version }) => {
1776
- logger.info(`已连接到 NapCat 实例: ${colors$1.green(`${app_name}-v${app_version} ${nickname}(${user_id})`)}`);
1777
- process.title = `mioki v${version} ${app_name}-v${app_version}-${user_id}`;
1778
- let lastNoticeTime = 0;
2081
+ }
2082
+ logger.info(colors$1.dim("=".repeat(40)));
2083
+ logger.info(`>>> 正在连接 ${napcatConfigs.length} 个 NapCat 实例...`);
2084
+ const bots = (await Promise.all(napcatConfigs.map((config, index) => connectBot(config, index)))).filter((b) => b !== null);
2085
+ if (bots.length === 0) {
2086
+ logger.error("所有 NapCat 实例连接失败,框架无法启动");
2087
+ process.exit(1);
2088
+ }
2089
+ for (const bot of bots) connectedBots.set(bot.bot_id, bot);
2090
+ if (bots.length < napcatConfigs.length) logger.warn(`${colors$1.yellow(napcatConfigs.length - bots.length)} 个 NapCat 实例连接失败`);
2091
+ const botNames = bots.map((b) => `${b.name}(${b.bot_id})`).join(", ");
2092
+ logger.info(colors$1.green(`成功连接 ${bots.length} 个实例: ${botNames}`));
2093
+ logger.info(colors$1.dim("=".repeat(40)));
2094
+ const mainBot = bots[0];
2095
+ process.title = `mioki v${version} ${bots.map((b) => `${b.bot_id}`).join(", ")}`;
2096
+ let lastNoticeTime = 0;
2097
+ for (const bot of bots) {
1779
2098
  process.on("uncaughtException", async (err) => {
1780
2099
  const msg = stringifyError(err);
1781
- napcat.logger.error(`uncaughtException, 出错了: ${msg}`);
2100
+ bot.logger.error(`uncaughtException, 出错了: ${msg}`);
1782
2101
  if (botConfig.error_push) {
1783
2102
  if (Date.now() - lastNoticeTime < 1e3) return;
1784
2103
  lastNoticeTime = Date.now();
1785
- await noticeMainOwner(napcat, `mioki 发生未捕获异常:\n\n${msg}`).catch(() => {
1786
- napcat.logger.error("发送未捕获异常通知失败");
2104
+ await noticeMainOwner(mainBot, `mioki 发生未捕获异常:\n\n${msg}`).catch(() => {
2105
+ mainBot.logger.error("发送未捕获异常通知失败");
1787
2106
  });
1788
2107
  }
1789
2108
  });
1790
2109
  process.on("unhandledRejection", async (err) => {
1791
2110
  const msg = stringifyError(err);
1792
- napcat.logger.error(`unhandledRejection, 出错了: ${msg}`);
2111
+ bot.logger.error(`unhandledRejection, 出错了: ${msg}`);
1793
2112
  if (botConfig.error_push) {
1794
2113
  if (Date.now() - lastNoticeTime < 1e3) return;
1795
2114
  lastNoticeTime = Date.now();
1796
2115
  const date = (/* @__PURE__ */ new Date()).toLocaleString();
1797
- await noticeMainOwner(napcat, `【${date}】\n\nmioki 发生未处理异常:\n\n${msg}`).catch(() => {
1798
- napcat.logger.error("发送未处理异常通知失败");
2116
+ await noticeMainOwner(mainBot, `【${date}】\n\nmioki 发生未处理异常:\n\n${msg}`).catch(() => {
2117
+ mainBot.logger.error("发送未处理异常通知失败");
1799
2118
  });
1800
2119
  }
1801
2120
  });
1802
- ensurePluginDir();
1803
- const plugins = botConfig.plugins.map((p) => ({
1804
- dirName: p,
1805
- absPath: path$1.resolve(plugin_dir, p)
1806
- })).filter((p) => {
1807
- if (!fs$1.existsSync(p.absPath)) {
1808
- napcat.logger.warn(`插件 ${colors$1.red(p.dirName)} 不存在,已忽略`);
1809
- return false;
1810
- }
1811
- return true;
1812
- });
1813
- const failedImportPlugins = [];
1814
- const promises = plugins.map(async ({ absPath, dirName }) => {
1815
- try {
1816
- const plugin = await jiti.import(absPath, { default: true });
1817
- if (plugin.name !== dirName) {
1818
- const tip = `插件目录名 [${colors$1.yellow(dirName)}] 和插件声明的 name [${colors$1.yellow(plugin.name)}] 不一致,可能导致重载异常,请修改一致后重启。`;
1819
- napcat.logger.warn(tip);
1820
- noticeMainOwner(napcat, tip);
1821
- }
1822
- return plugin;
1823
- } catch (e) {
1824
- const err = stringifyError(e);
1825
- failedImportPlugins.push([dirName, err]);
1826
- return null;
1827
- }
1828
- });
1829
- const start$1 = hrtime.bigint();
1830
- const sortedUserPlugins = (await Promise.all(promises)).filter(Boolean).toSorted((prev, next) => (prev.priority ?? 100) - (next.priority ?? 100));
1831
- if (failedImportPlugins.length) {
1832
- const tip = `${colors$1.red(failedImportPlugins.length)} 个插件加载失败: \n\n${failedImportPlugins.map(([dirName, err]) => `${dirName}: ${err}`).join("\n\n")}`;
1833
- napcat.logger.warn(tip);
1834
- noticeMainOwner(napcat, tip);
1835
- }
1836
- const pluginGroups = /* @__PURE__ */ new Map();
1837
- for (const plugin of sortedUserPlugins) {
1838
- const priority = plugin.priority ?? 100;
1839
- if (!pluginGroups.has(priority)) pluginGroups.set(priority, []);
1840
- pluginGroups.get(priority).push(plugin);
1841
- }
1842
- const sortedGroups = Array.from(pluginGroups.entries()).toSorted(([a], [b]) => a - b);
1843
- const failedEnablePlugins = [];
1844
- try {
1845
- napcat.logger.info(`>>> 加载内置插件: ${BUILTIN_PLUGINS.map((p) => colors$1.cyan(p.name)).join(", ")}`);
1846
- await Promise.all(BUILTIN_PLUGINS.map((p) => enablePlugin(napcat, p, "builtin")));
1847
- napcat.logger.info(`>>> 加载用户插件: ${sortedGroups.map(([priority, plugins$1]) => `优先级 ${colors$1.yellow(priority)} (${plugins$1.map((p) => colors$1.cyan(p.name)).join(", ")})`).join(",")}`);
1848
- for (const [_, plugins$1] of sortedGroups) await Promise.all(plugins$1.map(async (p) => {
1849
- try {
1850
- await enablePlugin(napcat, p, "external");
1851
- } catch (e) {
1852
- failedEnablePlugins.push([p.name, stringifyError(e)]);
1853
- }
1854
- }));
1855
- } catch (e) {
1856
- napcat.logger.error(e?.message);
1857
- await noticeMainOwner(napcat, e?.message).catch(() => {
1858
- napcat.logger.error("发送插件启用失败通知失败");
1859
- });
1860
- }
1861
- const end = hrtime.bigint();
1862
- const costTime = Math.round(Number(end - start$1)) / 1e6;
1863
- const failedCount = failedImportPlugins.length + failedEnablePlugins.length;
1864
- const failedInfo = failedCount > 0 ? `${colors$1.red(failedCount)} 个失败 (导入 ${colors$1.red(failedImportPlugins.length)},启用 ${colors$1.red(failedEnablePlugins.length)})` : "";
1865
- napcat.logger.info(`成功加载了 ${colors$1.green(runtimePlugins.size)} 个插件,${failedInfo ? failedInfo : ""}总耗时 ${colors$1.green(costTime.toFixed(2))} 毫秒`);
1866
- napcat.logger.info(colors$1.green(`mioki v${version} 启动完成,向机器人发送「${colors$1.magentaBright(`${botConfig.prefix}帮助`)}」查看消息指令`));
1867
- if (botConfig.online_push) await noticeMainOwner(napcat, `✅ mioki v${version} 已就绪`).catch((err) => {
1868
- napcat.logger.error(`发送就绪通知失败: ${stringifyError(err)}`);
1869
- });
1870
- });
1871
- await napcat.run();
2121
+ }
2122
+ await setupPlugins(mainBot, bots);
1872
2123
  }
1873
2124
 
1874
2125
  //#endregion
1875
- export { ArchMap, BOT_CWD, BUILTIN_PLUGINS, CORE_PLUGINS, ChromeUA, START_TIME, SystemMap, addService, base64Decode, base64Encode, bindBot, botConfig, box, clamp, colorize, colors, createCmd, createDB, createForwardMsg, createStore, dayjs, dedent, definePlugin, enablePlugin, ensureBuffer, ensurePluginDir, filesize, filter, find, findLocalPlugins, formatDuration, formatMiokiStatus, formatQQLevel, fs, getAbsPluginDir, getAuthCodeViaTicket, getBfaceUrl, getGTk, getGroupAvatarLink, getImage, getImageUrl, getLogFilePath, getMentionedImage, getMentionedImageUrl, getMentionedUserId, getMinicoTokenViaAuthCode, getMiokiLogger, getMiokiStatus, getQQAvatarLink, getQuoteImage, getQuoteImageUrl, getQuoteMsg, getQuoteText, getTerminalInput, getViolationRecords, hasRight, isAdmin, isBoolean, isDefined, isFunction, isGroupMsg, isInPm2, isNumber, isObject, isOwner, isOwnerOrAdmin, isPrivateMsg, isString, jiti, localNum, localeDate, localeTime, logger, match, md5, mri, noNullish, noticeAdmins, noticeFriends, noticeGroups, noticeMainOwner, noticeOwners, path, prettyMs, qs, queryDevToolsLoginStatus, randomId, randomInt, randomItem, randomItems, readMiokiConfig, readPackageJson, requestLoginViaDevTools, runWithErrorHandler, runWithReaction, runtimePlugins, services, signArk, start, string2argv, stringifyError, stripAnsi, systemInfo, text, toArray, toMsgId, unique, updateBotCWD, updateBotConfig, uploadImageToCollection, uploadImageToGroupHomework, uploadImageToGroupNotice, uuid, wait, writePackageJson };
2126
+ export { ArchMap, BOT_CWD, BUILTIN_PLUGINS, CORE_PLUGINS, ChromeUA, Deduplicator, MessageDeduplicator, START_TIME, SystemMap, addService, base64Decode, base64Encode, bindBot, botConfig, box, clamp, colorize, colors, connectedBots, createCmd, createDB, createForwardMsg, createStore, dayjs, dedent, deduplicator, definePlugin, enablePlugin, ensureBuffer, ensurePluginDir, filesize, filter, find, findLocalPlugins, formatDuration, formatMiokiStatus, formatQQLevel, fs, getAbsPluginDir, getAuthCodeViaTicket, getBfaceUrl, getGTk, getGroupAvatarLink, getImage, getImageUrl, getLogFilePath, getMentionedImage, getMentionedImageUrl, getMentionedUserId, getMinicoTokenViaAuthCode, getMiokiLogger, getMiokiStatus, getQQAvatarLink, getQuoteImage, getQuoteImageUrl, getQuoteMsg, getQuoteText, getTerminalInput, getViolationRecords, hasRight, isAdmin, isBoolean, isDefined, isFunction, isGroupMsg, isInPm2, isNumber, isObject, isOwner, isOwnerOrAdmin, isPrivateMsg, isString, jiti, localNum, localeDate, localeTime, logger, match, md5, mri, noNullish, normalizeNapCatConfig, noticeAdmins, noticeFriends, noticeGroups, noticeMainOwner, noticeOwners, path, prettyMs, qs, queryDevToolsLoginStatus, randomId, randomInt, randomItem, randomItems, readMiokiConfig, readPackageJson, requestLoginViaDevTools, runWithErrorHandler, runWithReaction, runtimePlugins, services, signArk, start, string2argv, stringifyError, stripAnsi, systemInfo, text, toArray, toMsgId, unique, updateBotCWD, updateBotConfig, uploadImageToCollection, uploadImageToGroupHomework, uploadImageToGroupNotice, uuid, wait, writePackageJson };
1876
2127
  //# sourceMappingURL=index.mjs.map