koishi-plugin-group-verification 1.0.30 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -5,6 +5,7 @@ declare module 'koishi' {
5
5
  group_verification_config: GroupVerificationConfig;
6
6
  group_verification_stats: GroupVerificationStats;
7
7
  group_verification_pending: PendingVerification;
8
+ group_verification_blacklist: GroupBlacklistEntry;
8
9
  }
9
10
  }
10
11
  export interface GroupVerificationConfig {
@@ -29,6 +30,11 @@ export interface GroupVerificationStats {
29
30
  totalJoined: number;
30
31
  lastUpdated: string | Date;
31
32
  }
33
+ export interface GroupBlacklistEntry {
34
+ id: number;
35
+ groupId: string;
36
+ entries: Record<string, string>;
37
+ }
32
38
  export interface PendingVerification {
33
39
  id: number;
34
40
  groupId: string;
@@ -46,6 +52,10 @@ export interface Config {
46
52
  invalidGroupMessage?: string;
47
53
  parameterConflictMessage?: string;
48
54
  noKeywordsMessage?: string;
55
+ blacklistAddSuccess?: string;
56
+ blacklistRemoveSuccess?: string;
57
+ blacklistListEmpty?: string;
58
+ blacklistInfoTemplate?: string;
49
59
  }
50
60
  export declare const Config: Schema<Config>;
51
61
  export declare const inject: string[];
@@ -114,4 +124,6 @@ export declare function verifyApplication(config: GroupVerificationConfig, messa
114
124
  requiredThreshold: string;
115
125
  }>;
116
126
  export declare function handleFailedVerification(ctx: Context, session: any, config: GroupVerificationConfig, matchedCount?: number, requiredThreshold?: string): Promise<void>;
127
+ export declare function isUserBlacklisted(ctx: Context, groupId: string, userId: string): Promise<boolean>;
128
+ export declare function processBlacklistCommand(ctx: Context, session: any, rawArgs: string, config?: Config): Promise<string>;
117
129
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -28,9 +28,11 @@ __export(src_exports, {
28
28
  handleGuildMemberRequestEvent: () => handleGuildMemberRequestEvent,
29
29
  incrementTotal: () => incrementTotal,
30
30
  inject: () => inject,
31
+ isUserBlacklisted: () => isUserBlacklisted,
31
32
  mergeReminder: () => mergeReminder,
32
33
  name: () => name,
33
34
  parseConfigArgs: () => parseConfigArgs,
35
+ processBlacklistCommand: () => processBlacklistCommand,
34
36
  resolveThreshold: () => resolveThreshold,
35
37
  syncTotalStats: () => syncTotalStats,
36
38
  tokenize: () => tokenize,
@@ -49,7 +51,11 @@ var Config = import_koishi.Schema.object({
49
51
  permissionDeniedMessage: import_koishi.Schema.string().description("权限不足时返回给调用者的提示").default("权限不足:需要群主/管理员权限或koishi三级以上权限"),
50
52
  invalidGroupMessage: import_koishi.Schema.string().description("无效群号或机器人未在该群时的提示").default("群号 {group} 格式不合法或机器人不在该群中"),
51
53
  parameterConflictMessage: import_koishi.Schema.string().description("参数冲突时提示").default("参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)"),
52
- noKeywordsMessage: import_koishi.Schema.string().description("未提供关键词且无法从现有配置继承时的提示").default("请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置")
54
+ noKeywordsMessage: import_koishi.Schema.string().description("未提供关键词且无法从现有配置继承时的提示").default("请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置"),
55
+ blacklistAddSuccess: import_koishi.Schema.string().description("将用户加入黑名单后的提示,可使用 {user},{group},{reason}").default("已将用户 {user} 加入群 {group} 黑名单{reason}"),
56
+ blacklistRemoveSuccess: import_koishi.Schema.string().description("从黑名单移除用户后的提示,可使用 {user},{group}").default("已从群 {group} 的黑名单中移除用户 {user}"),
57
+ blacklistListEmpty: import_koishi.Schema.string().description("黑名单为空时提示").default("群 {group} 的黑名单为空"),
58
+ blacklistInfoTemplate: import_koishi.Schema.string().description("查询指定用户状态时的模板,可用 {global},{group}").default("全局黑名单: {global}\n本群黑名单: {group}")
53
59
  }).description("群组验证插件配置");
54
60
  var inject = ["database"];
55
61
  var ESC_QUOTE = "\0";
@@ -342,7 +348,7 @@ async function checkPermission(session, targetGroupId) {
342
348
  });
343
349
  }
344
350
  if (koishiAuthority && koishiAuthority >= 3) {
345
- logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`);
351
+ logger.debug(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`);
346
352
  return [true];
347
353
  }
348
354
  try {
@@ -392,6 +398,23 @@ async function handleGuildMemberRequestEvent(ctx, session) {
392
398
  await updateStats(ctx, guildId, "rejected");
393
399
  return;
394
400
  }
401
+ try {
402
+ const blacklisted = await isUserBlacklisted(ctx, guildId, userId);
403
+ if (blacklisted) {
404
+ logger.info(`用户 ${userId} 在群 ${guildId} 或全局黑名单中,自动拒绝申请`);
405
+ if (requestId) {
406
+ try {
407
+ await session.bot.handleGuildMemberRequest(requestId, false);
408
+ } catch (e) {
409
+ logger.warn("自动拒绝失败", e);
410
+ }
411
+ }
412
+ await updateStats(ctx, guildId, "rejected");
413
+ return;
414
+ }
415
+ } catch (e) {
416
+ logger.warn("黑名单检查失败", e);
417
+ }
395
418
  const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
396
419
  logger.debug(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
397
420
  if (isValid) {
@@ -603,6 +626,218 @@ async function handleFailedVerification(ctx, session, config, matchedCount, requ
603
626
  }
604
627
  }
605
628
  __name(handleFailedVerification, "handleFailedVerification");
629
+ async function isUserBlacklisted(ctx, groupId, userId) {
630
+ const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
631
+ if (globalRows.length > 0) {
632
+ const entries = globalRows[0].entries || {};
633
+ if (entries[userId] !== void 0) return true;
634
+ }
635
+ const groupRows = await ctx.database.get("group_verification_blacklist", { groupId });
636
+ if (groupRows.length > 0) {
637
+ const entries = groupRows[0].entries || {};
638
+ if (entries[userId] !== void 0) return true;
639
+ }
640
+ return false;
641
+ }
642
+ __name(isUserBlacklisted, "isUserBlacklisted");
643
+ async function processBlacklistCommand(ctx, session, rawArgs, config) {
644
+ const parts = rawArgs.trim().split(/\s+/).filter(Boolean);
645
+ const op = parts[0]?.toLowerCase();
646
+ if (!op || !["a", "r", "l", "i"].includes(op)) {
647
+ return [
648
+ "用法:",
649
+ " gvb a id [reason] [group] 将用户加入黑名单",
650
+ " gvb r id [group] 将用户移出黑名单",
651
+ " gvb l [group] 查询群黑名单",
652
+ " gvb i id [group] 查询账号黑名单",
653
+ " [group]传入all表全局"
654
+ ].join("\n");
655
+ }
656
+ const getCurrentGroup = /* @__PURE__ */ __name(() => session.guildId || "", "getCurrentGroup");
657
+ let group;
658
+ let targetUser;
659
+ let reason = "";
660
+ if (op === "a") {
661
+ targetUser = parts[1];
662
+ if (!targetUser) return "请提供用户ID";
663
+ const rest = parts.slice(2);
664
+ if (rest.length === 1) {
665
+ reason = rest[0];
666
+ } else if (rest.length > 1) {
667
+ const last = rest[rest.length - 1];
668
+ if (/^\d+$/.test(last) || last.toLowerCase() === "all") {
669
+ group = last;
670
+ reason = rest.slice(0, -1).join(" ");
671
+ } else {
672
+ reason = rest.join(" ");
673
+ }
674
+ }
675
+ group = group || getCurrentGroup();
676
+ if (!group) return "请在群聊中使用此命令或指定群号";
677
+ if (group.toLowerCase() === "all") {
678
+ const auth = session.author?.authority || session.user?.authority;
679
+ if (!(auth && auth >= 3)) return "设置全局黑名单需要 koishi 3 级以上权限";
680
+ } else {
681
+ if (config?.enableStrictGroupCheck) {
682
+ if (!/^\d{5,15}$/.test(group)) {
683
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
684
+ }
685
+ }
686
+ const [ok, err] = await checkPermission(session, group);
687
+ if (!ok) return err || "权限不足";
688
+ }
689
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
690
+ const timePrefix = (/* @__PURE__ */ new Date()).toLocaleString();
691
+ const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix;
692
+ if (rows.length > 0) {
693
+ const row = rows[0];
694
+ const entries = row.entries || {};
695
+ if (entries[targetUser] !== void 0) {
696
+ return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`;
697
+ }
698
+ entries[targetUser] = storedReason;
699
+ await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
700
+ } else {
701
+ const entries = {};
702
+ entries[targetUser] = storedReason;
703
+ await ctx.database.create("group_verification_blacklist", { groupId: group, entries });
704
+ }
705
+ if (session.bot && typeof session.bot.kickGuildMember === "function") {
706
+ try {
707
+ await session.bot.kickGuildMember(group, targetUser);
708
+ logger.info(`已将黑名单用户 ${targetUser} 从群 ${group} 踢出`);
709
+ } catch (e) {
710
+ logger.warn(`踢出用户 ${targetUser} 失败`, e);
711
+ }
712
+ }
713
+ const tmpl = config && config.blacklistAddSuccess || "已将用户 {user} 加入群 {group} 黑名单{reason}";
714
+ return tmpl.replace("{user}", targetUser).replace("{group}", group).replace("{reason}", reason ? `,原因:${reason}` : "");
715
+ }
716
+ if (op === "r") {
717
+ targetUser = parts[1];
718
+ if (!targetUser) return "请提供用户ID";
719
+ group = parts[2] || getCurrentGroup();
720
+ if (!group) return "请在群聊中使用此命令或指定群号";
721
+ if (group.toLowerCase() === "all") {
722
+ const auth = session.author?.authority || session.user?.authority;
723
+ if (!(auth && auth >= 3)) return "修改全局黑名单需要 koishi 3 级以上权限";
724
+ } else {
725
+ if (config?.enableStrictGroupCheck) {
726
+ if (!/^\d{5,15}$/.test(group)) {
727
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
728
+ }
729
+ }
730
+ const [ok, err] = await checkPermission(session, group);
731
+ if (!ok) return err || "权限不足";
732
+ }
733
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
734
+ if (rows.length > 0) {
735
+ const row = rows[0];
736
+ const entries = row.entries || {};
737
+ delete entries[targetUser];
738
+ await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
739
+ }
740
+ const tmpl = config && config.blacklistRemoveSuccess || "已从群 {group} 的黑名单中移除用户 {user}";
741
+ return tmpl.replace("{user}", targetUser).replace("{group}", group);
742
+ }
743
+ if (op === "l") {
744
+ group = parts[1] || getCurrentGroup();
745
+ if (!group) return "请在群聊中使用此命令或指定群号";
746
+ if (group.toLowerCase() === "all") {
747
+ const auth = session.author?.authority || session.user?.authority;
748
+ if (!(auth && auth >= 3)) return "查看全局黑名单需要 koishi 3 级以上权限";
749
+ } else {
750
+ if (config?.enableStrictGroupCheck) {
751
+ if (!/^\d{5,15}$/.test(group)) {
752
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
753
+ }
754
+ }
755
+ const [ok, err] = await checkPermission(session, group);
756
+ if (!ok) return err || "权限不足";
757
+ }
758
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
759
+ if (rows.length === 0) {
760
+ if (group && group.toLowerCase() === "all") {
761
+ return "全局黑名单为空";
762
+ }
763
+ const tmpl = config && config.blacklistListEmpty || "群 {group} 的黑名单为空";
764
+ return tmpl.replace("{group}", group);
765
+ }
766
+ const entries = rows[0].entries || {};
767
+ let prefix = group && group.toLowerCase() === "all" ? "全局黑名单: \n" : `群 ${group} 黑名单:
768
+ `;
769
+ let msg = prefix;
770
+ for (const uid in entries) {
771
+ msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ""}
772
+ `;
773
+ }
774
+ return msg;
775
+ }
776
+ if (op === "i") {
777
+ targetUser = parts[1];
778
+ if (!targetUser) return "请提供用户ID";
779
+ const groupArg = parts[2];
780
+ const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
781
+ const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== void 0;
782
+ const tmpl = config && config.blacklistInfoTemplate || "全局黑名单: {global}\n本群黑名单: {group}";
783
+ const formatReply = /* @__PURE__ */ __name((localHit, groupsList) => {
784
+ if (groupsList) {
785
+ return tmpl.replace("{global}", globalHit ? "有" : "无").replace(
786
+ "{group}",
787
+ groupsList.length ? groupsList.join(",") : "无"
788
+ );
789
+ }
790
+ return tmpl.replace("{global}", globalHit ? "有" : "无").replace("{group}", localHit ? "有" : "无");
791
+ }, "formatReply");
792
+ if (!groupArg) {
793
+ const groupId2 = getCurrentGroup();
794
+ if (!groupId2) return "请在群聊中使用此命令";
795
+ const [ok2, err2] = await checkPermission(session, groupId2);
796
+ if (!ok2) return err2 || "权限不足";
797
+ const rows2 = await ctx.database.get("group_verification_blacklist", { groupId: groupId2 });
798
+ const localReason2 = rows2.length > 0 ? (rows2[0].entries || {})[targetUser] : void 0;
799
+ const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : void 0;
800
+ return `全局黑名单: ${globalReason2 || "无"}
801
+ 群${groupId2}黑名单: ${localReason2 || "无"}`;
802
+ }
803
+ if (groupArg.toLowerCase() === "all") {
804
+ const auth = session.author?.authority || session.user?.authority;
805
+ if (!(auth && auth >= 3)) return "权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限";
806
+ const rows2 = await ctx.database.get("group_verification_blacklist", {});
807
+ const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : "无";
808
+ const lines = [];
809
+ for (const r of rows2) {
810
+ if (r.groupId === "all") continue;
811
+ const reason2 = (r.entries || {})[targetUser];
812
+ if (reason2 !== void 0) {
813
+ lines.push(`群${r.groupId}:${reason2}`);
814
+ }
815
+ }
816
+ let reply = `全局黑名单: ${globalReason2}`;
817
+ if (lines.length) {
818
+ reply += "\n群黑名单: \n" + lines.join("\n");
819
+ } else {
820
+ reply += "\n群黑名单: 无";
821
+ }
822
+ return reply;
823
+ }
824
+ const groupId = groupArg;
825
+ if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== "all") {
826
+ if (!/^\d{5,15}$/.test(groupId)) {
827
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", groupId);
828
+ }
829
+ }
830
+ const [ok, err] = await checkPermission(session, groupId);
831
+ if (!ok) return err || "权限不足";
832
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId });
833
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : void 0;
834
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : void 0;
835
+ return `全局黑名单: ${globalReason || "无"}
836
+ 群${groupId}黑名单: ${localReason || "无"}`;
837
+ }
838
+ return "";
839
+ }
840
+ __name(processBlacklistCommand, "processBlacklistCommand");
606
841
  function apply(ctx, config) {
607
842
  ctx.model.extend("group_verification_config", {
608
843
  id: "unsigned",
@@ -655,13 +890,25 @@ function apply(ctx, config) {
655
890
  primary: "id",
656
891
  autoInc: true
657
892
  });
893
+ ctx.model.extend("group_verification_blacklist", {
894
+ id: "unsigned",
895
+ groupId: "string",
896
+ entries: "json"
897
+ }, {
898
+ primary: "id",
899
+ autoInc: true,
900
+ indexes: [["groupId"]]
901
+ });
658
902
  ctx.on("guild-member-request", async (session) => {
659
903
  logger.info("收到 guild-member-request 事件,转发给处理函数");
660
904
  await handleGuildMemberRequestEvent(ctx, session);
661
905
  });
662
906
  ctx.on("guild-member-added", async (session) => {
663
- const groupId = session.guildId;
664
- const userId = session.userId;
907
+ const groupId = session.guildId || "";
908
+ const userId = session.userId || "";
909
+ if (!groupId || !userId) {
910
+ return;
911
+ }
665
912
  await incrementTotal(ctx, groupId);
666
913
  const set = autoQueue.get(groupId);
667
914
  if (set && set.has(userId)) {
@@ -1083,10 +1330,11 @@ ${reminderMessage}
1083
1330
  return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
1084
1331
  }
1085
1332
  try {
1086
- await session.bot.handleGuildMemberRequest(request2.requestId, true);
1087
- await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1088
1333
  const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
1089
- return `已同意用户 ${displayName} 的加群申请`;
1334
+ const reply = `已同意用户 ${displayName} 的加群申请`;
1335
+ session.bot.handleGuildMemberRequest(request2.requestId, true).catch((e) => logger.warn("自动同意失败", e));
1336
+ await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1337
+ return reply;
1090
1338
  } catch (error) {
1091
1339
  return `处理申请时出错: ${error.message}`;
1092
1340
  }
@@ -1104,10 +1352,11 @@ ${reminderMessage}
1104
1352
  return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
1105
1353
  }
1106
1354
  try {
1107
- await session.bot.handleGuildMemberRequest(request.requestId, true);
1108
- await ctx.database.remove("group_verification_pending", { groupId, userId });
1109
1355
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
1110
- return `已同意用户 ${displayName} 的加群申请`;
1356
+ const reply = `已同意用户 ${displayName} 的加群申请`;
1357
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e) => logger.warn("自动同意失败", e));
1358
+ await ctx.database.remove("group_verification_pending", { groupId, userId });
1359
+ return reply;
1111
1360
  } catch (error) {
1112
1361
  return `处理申请时出错: ${error.message}`;
1113
1362
  }
@@ -1257,6 +1506,17 @@ ${reminderMessage}
1257
1506
  });
1258
1507
  return result;
1259
1508
  });
1509
+ groupVerify.subcommand(".blacklist [args:text]", "管理加群黑名单").alias(
1510
+ "gvb",
1511
+ "gv.blacklist",
1512
+ "gverify.blacklist",
1513
+ "group-verify.blacklist",
1514
+ "gv.黑名单",
1515
+ "gverify.黑名单",
1516
+ "group-verify.黑名单"
1517
+ ).action(async ({ session }, args) => {
1518
+ return await processBlacklistCommand(ctx, session, args || "", config);
1519
+ });
1260
1520
  groupVerify.subcommand(".help", "显示帮助信息").alias("gv.帮助", "gverify.帮助", "group-verify.帮助", "帮助", "hlp", "帮助信息").action(() => {
1261
1521
  return `群组验证命令帮助:
1262
1522
  主指令别名:gv, gverify
@@ -1309,6 +1569,19 @@ ${reminderMessage}
1309
1569
  gv.cfg -m 2 -t 80
1310
1570
  gv.cfg -nomsg
1311
1571
 
1572
+ 黑名单命令 (.blacklist / gvb):
1573
+ a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
1574
+ r id [group] 删除条目;group 为 all 时移除全局黑名单
1575
+ l [group] 查询群黑名单;传入 all 时查看全局黑名单
1576
+ i id [group] 查询账号黑名单;不带参数时查询本群与全局,
1577
+ 指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
1578
+ 使用示例:
1579
+ gvb a 12345 作弊记录
1580
+ gvb r 12345 67890
1581
+ gvb l
1582
+ gvb l all
1583
+ gvb i 12345
1584
+
1312
1585
  快捷命令:
1313
1586
  gvc - 配置命令快捷方式
1314
1587
  gva - 同意申请快捷命令
@@ -1409,9 +1682,11 @@ __name(apply, "apply");
1409
1682
  handleGuildMemberRequestEvent,
1410
1683
  incrementTotal,
1411
1684
  inject,
1685
+ isUserBlacklisted,
1412
1686
  mergeReminder,
1413
1687
  name,
1414
1688
  parseConfigArgs,
1689
+ processBlacklistCommand,
1415
1690
  resolveThreshold,
1416
1691
  syncTotalStats,
1417
1692
  tokenize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-verification",
3
- "description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能。",
3
+ "description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/LHDyx/koishi-plugin-group-verification.git"
@@ -8,7 +8,7 @@
8
8
  "bugs": {
9
9
  "url": "https://github.com/LHDyx/koishi-plugin-group-verification/issues"
10
10
  },
11
- "version": "1.0.30",
11
+ "version": "1.0.32",
12
12
  "main": "lib/index.js",
13
13
  "typings": "lib/index.d.ts",
14
14
  "files": [
package/readme.md CHANGED
@@ -62,6 +62,7 @@ group-verify.config -i 123456789 关键词1,关键词2 -m 1 -t 1
62
62
 
63
63
  > **命令增强**
64
64
  > - 未提供参数时 `gva`/`gvr` 会处理最近一条申请,`all` 可以批量处理。
65
+ > - 手动使用 `gva` 时会先在群内返回“已同意用户…的加群申请”等提示,随后再将用户加入群,避免用户看到提示时已在群中的尴尬情形。
65
66
  > - 如果申请记录缺少 requestId,则无法通过机器人接口处理,会提示管理员请在客户端手动操作。
66
67
  > - 输出结果会智能展示用户名和ID,避免出现 "12345(12345)" 这样的重复显示。
67
68
 
@@ -101,6 +102,36 @@ group-verify.stats 123456789
101
102
  group-verify.stats total
102
103
  ```
103
104
 
105
+ ### 黑名单命令
106
+ ```
107
+ # 添加黑名单条目(可指定原因和群号)
108
+ group-verify.blacklist a <用户ID> [原因] [群号]
109
+ # 别名:gvb, gv.blacklist, gverify.blacklist, group-verify.blacklist
110
+ # gv.黑名单, gverify.黑名单, group-verify.黑名单
111
+
112
+ # 删除黑名单条目
113
+ group-verify.blacklist r <用户ID> [群号]
114
+
115
+ # 查看群黑名单
116
+ group-verify.blacklist l [群号]
117
+ # 传入 all 可查看全局黑名单
118
+
119
+ # 查询用户在黑名单的状态
120
+ group-verify.blacklist i <用户ID> [群号|all]
121
+
122
+ * 不指定群号时会查询当前群和全局黑名单状态(必须在群聊中使用)。
123
+ * 指定单个群号时会查询该群和全局状态,执行此操作需要该群的管理员权限或 Koishi 三级以上权限。
124
+ * 输入 `all` 会列出用户在所有群的黑名单条目与全局黑名单(仅限 Koishi `authority>=3`)。
125
+ ```
126
+
127
+ ### 黑名单消息模板
128
+ 插件会在配置界面显示当前黑名单存在状态的提示词,用户可以通过修改以下字段自定义这些提示:
129
+
130
+ - `blacklistLocalExists`:当前群已有黑名单条目时的提示,例如 “本群已设置黑名单”。
131
+ - `blacklistGlobalExists`:全局黑名单存在时的提示,例如 “已启用全局黑名单”。
132
+
133
+ 如果将这两个字段留空,则对应的提示不会显示(界面将保持简洁)。
134
+
104
135
  ## ⚙️ 参数说明
105
136
 
106
137
  ### 审核方式 (-m)
@@ -186,6 +217,7 @@ group-verify.stats total
186
217
  - `group_verification_config` - 群组配置表
187
218
  - `group_verification_stats` - 统计信息表
188
219
  - `group_verification_pending` - 待审核申请表
220
+ - `group_verification_blacklist` - 群组黑名单条目,每行记录一个用户(groupId=all 表示全局)
189
221
 
190
222
  ## 📝 使用示例
191
223
 
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ declare module 'koishi' {
11
11
  group_verification_config: GroupVerificationConfig
12
12
  group_verification_stats: GroupVerificationStats
13
13
  group_verification_pending: PendingVerification
14
+ group_verification_blacklist: GroupBlacklistEntry
14
15
  }
15
16
  }
16
17
 
@@ -41,6 +42,14 @@ export interface GroupVerificationStats {
41
42
  lastUpdated: string | Date
42
43
  }
43
44
 
45
+ // 黑名单行:每个群/全局对应一条记录,entries 保存 userId->reason 对象
46
+ export interface GroupBlacklistEntry {
47
+ id: number
48
+ groupId: string // 群号或 "all" 表示全局
49
+ entries: Record<string, string> // key=userId, value=reason
50
+ }
51
+
52
+
44
53
  // 待审核申请表
45
54
  export interface PendingVerification {
46
55
  id: number
@@ -62,6 +71,11 @@ export interface Config {
62
71
  invalidGroupMessage?: string
63
72
  parameterConflictMessage?: string
64
73
  noKeywordsMessage?: string
74
+ // 黑名单提示消息
75
+ blacklistAddSuccess?: string
76
+ blacklistRemoveSuccess?: string
77
+ blacklistListEmpty?: string
78
+ blacklistInfoTemplate?: string
65
79
  }
66
80
 
67
81
  export const Config: Schema<Config> = Schema.object({
@@ -74,6 +88,10 @@ export const Config: Schema<Config> = Schema.object({
74
88
  invalidGroupMessage: Schema.string().description('无效群号或机器人未在该群时的提示').default('群号 {group} 格式不合法或机器人不在该群中'),
75
89
  parameterConflictMessage: Schema.string().description('参数冲突时提示').default('参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)'),
76
90
  noKeywordsMessage: Schema.string().description('未提供关键词且无法从现有配置继承时的提示').default('请先提供关键词创建配置,或使用 -? 查询配置,-r 删除配置'),
91
+ blacklistAddSuccess: Schema.string().description('将用户加入黑名单后的提示,可使用 {user},{group},{reason}').default('已将用户 {user} 加入群 {group} 黑名单{reason}'),
92
+ blacklistRemoveSuccess: Schema.string().description('从黑名单移除用户后的提示,可使用 {user},{group}').default('已从群 {group} 的黑名单中移除用户 {user}'),
93
+ blacklistListEmpty: Schema.string().description('黑名单为空时提示').default('群 {group} 的黑名单为空'),
94
+ blacklistInfoTemplate: Schema.string().description('查询指定用户状态时的模板,可用 {global},{group}').default('全局黑名单: {global}\n本群黑名单: {group}')
77
95
  })
78
96
  .description('群组验证插件配置')
79
97
 
@@ -480,7 +498,7 @@ export async function checkPermission(session: any, targetGroupId?: string): Pro
480
498
  }
481
499
 
482
500
  if (koishiAuthority && koishiAuthority >= 3) {
483
- logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`)
501
+ logger.debug(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`)
484
502
  return [true]
485
503
  }
486
504
 
@@ -533,6 +551,21 @@ export async function handleGuildMemberRequestEvent(ctx: Context, session: any)
533
551
  return;
534
552
  }
535
553
 
554
+ // 黑名单优先检查(仅在非全拒模式下)
555
+ try {
556
+ const blacklisted = await isUserBlacklisted(ctx, guildId, userId);
557
+ if (blacklisted) {
558
+ logger.info(`用户 ${userId} 在群 ${guildId} 或全局黑名单中,自动拒绝申请`);
559
+ if (requestId) {
560
+ try { await session.bot.handleGuildMemberRequest(requestId, false); } catch (e) { logger.warn('自动拒绝失败', e); }
561
+ }
562
+ await updateStats(ctx, guildId, 'rejected');
563
+ return;
564
+ }
565
+ } catch (e) {
566
+ logger.warn('黑名单检查失败', e);
567
+ }
568
+
536
569
  const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
537
570
  logger.debug(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
538
571
 
@@ -744,7 +777,7 @@ export async function handleFailedVerification(
744
777
  try {
745
778
  const guild = await session.bot.getGuild(guildId)
746
779
  groupName = guild.name || groupName
747
- } catch (error) {
780
+ } catch (error: any) {
748
781
  // 无法获取群名称时使用默认值
749
782
  }
750
783
 
@@ -809,6 +842,241 @@ export async function handleFailedVerification(
809
842
  }
810
843
  }
811
844
 
845
+ // 黑名单相关辅助函数 ----------------------------------------------------------
846
+
847
+ // 检查指定用户是否在黑名单(群级或全局)中
848
+ export async function isUserBlacklisted(ctx: Context, groupId: string, userId: string): Promise<boolean> {
849
+ // 全局黑名单
850
+ const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
851
+ if (globalRows.length > 0) {
852
+ const entries = globalRows[0].entries || {}
853
+ if (entries[userId] !== undefined) return true
854
+ }
855
+ // 群级黑名单
856
+ const groupRows = await ctx.database.get('group_verification_blacklist', { groupId })
857
+ if (groupRows.length > 0) {
858
+ const entries = groupRows[0].entries || {}
859
+ if (entries[userId] !== undefined) return true
860
+ }
861
+ return false
862
+ }
863
+
864
+ // 解析并执行黑名单管理命令,返回要回复的字符串
865
+ export async function processBlacklistCommand(ctx: Context, session: any, rawArgs: string, config?: Config): Promise<string> {
866
+ const parts = rawArgs.trim().split(/\s+/).filter(Boolean)
867
+ const op = parts[0]?.toLowerCase()
868
+ if (!op || !['a','r','l','i'].includes(op)) {
869
+ // present a multiline Chinese usage guide without angle brackets
870
+ return [
871
+ '用法:',
872
+ ' gvb a id [reason] [group] 将用户加入黑名单',
873
+ ' gvb r id [group] 将用户移出黑名单',
874
+ ' gvb l [group] 查询群黑名单',
875
+ ' gvb i id [group] 查询账号黑名单',
876
+ ' [group]传入all表全局'
877
+ ].join('\n')
878
+ }
879
+
880
+ const getCurrentGroup = () => session.guildId || ''
881
+ let group: string | undefined
882
+ let targetUser: string | undefined
883
+ let reason = ''
884
+
885
+ if (op === 'a') {
886
+ targetUser = parts[1]
887
+ if (!targetUser) return '请提供用户ID'
888
+ // handle optional reason and group at end
889
+ // 规则:如果只有一个附加参数,则作为 reason;两个及以上时最后一个为群号,其余拼成 reason
890
+ const rest = parts.slice(2)
891
+ if (rest.length === 1) {
892
+ reason = rest[0]
893
+ } else if (rest.length > 1) {
894
+ const last = rest[rest.length - 1]
895
+ if (/^\d+$/.test(last) || last.toLowerCase() === 'all') {
896
+ group = last
897
+ reason = rest.slice(0, -1).join(' ')
898
+ } else {
899
+ // 虽然数量>=2,但最后一个不是数字,全部作为reason
900
+ reason = rest.join(' ')
901
+ }
902
+ }
903
+ group = group || getCurrentGroup()
904
+ if (!group) return '请在群聊中使用此命令或指定群号'
905
+ // 权限检查
906
+ if (group.toLowerCase() === 'all') {
907
+ const auth = session.author?.authority || session.user?.authority
908
+ if (!(auth && auth >= 3)) return '设置全局黑名单需要 koishi 3 级以上权限'
909
+ } else {
910
+ // 严格群号检查(若开启)
911
+ if (config?.enableStrictGroupCheck) {
912
+ if (!/^\d{5,15}$/.test(group)) {
913
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
914
+ }
915
+ }
916
+ const [ok, err] = await checkPermission(session, group)
917
+ if (!ok) return err || '权限不足'
918
+ }
919
+ // add entry to map with timestamp, but refuse duplicates
920
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
921
+ // build stored reason with current time prefix
922
+ const timePrefix = new Date().toLocaleString()
923
+ const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix
924
+ if (rows.length > 0) {
925
+ const row = rows[0]
926
+ const entries = row.entries || {}
927
+ if (entries[targetUser] !== undefined) {
928
+ return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`
929
+ }
930
+ entries[targetUser] = storedReason
931
+ await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
932
+ } else {
933
+ const entries: Record<string,string> = {}
934
+ entries[targetUser] = storedReason
935
+ await ctx.database.create('group_verification_blacklist', { groupId: group, entries })
936
+ }
937
+ // 添加成功后尝试踢人
938
+ if (session.bot && typeof session.bot.kickGuildMember === 'function') {
939
+ try {
940
+ await session.bot.kickGuildMember(group, targetUser)
941
+ logger.info(`已将黑名单用户 ${targetUser} 从群 ${group} 踢出`)
942
+ } catch (e) {
943
+ logger.warn(`踢出用户 ${targetUser} 失败`, e)
944
+ }
945
+ }
946
+ const tmpl = (config && config.blacklistAddSuccess) || '已将用户 {user} 加入群 {group} 黑名单{reason}'
947
+ return tmpl.replace('{user}', targetUser).replace('{group}', group).replace('{reason}', reason ? `,原因:${reason}` : '')
948
+ }
949
+ if (op === 'r') {
950
+ targetUser = parts[1]
951
+ if (!targetUser) return '请提供用户ID'
952
+ group = parts[2] || getCurrentGroup()
953
+ if (!group) return '请在群聊中使用此命令或指定群号'
954
+ if (group.toLowerCase() === 'all') {
955
+ const auth = session.author?.authority || session.user?.authority
956
+ if (!(auth && auth >= 3)) return '修改全局黑名单需要 koishi 3 级以上权限'
957
+ } else {
958
+ if (config?.enableStrictGroupCheck) {
959
+ if (!/^\d{5,15}$/.test(group)) {
960
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
961
+ }
962
+ }
963
+ const [ok, err] = await checkPermission(session, group)
964
+ if (!ok) return err || '权限不足'
965
+ }
966
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
967
+ if (rows.length > 0) {
968
+ const row = rows[0]
969
+ const entries = row.entries || {}
970
+ delete entries[targetUser]
971
+ await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
972
+ }
973
+ const tmpl = (config && config.blacklistRemoveSuccess) || '已从群 {group} 的黑名单中移除用户 {user}'
974
+ return tmpl.replace('{user}', targetUser).replace('{group}', group)
975
+ }
976
+ if (op === 'l') {
977
+ group = parts[1] || getCurrentGroup()
978
+ if (!group) return '请在群聊中使用此命令或指定群号'
979
+ if (group.toLowerCase() === 'all') {
980
+ const auth = session.author?.authority || session.user?.authority
981
+ if (!(auth && auth >= 3)) return '查看全局黑名单需要 koishi 3 级以上权限'
982
+ } else {
983
+ if (config?.enableStrictGroupCheck) {
984
+ if (!/^\d{5,15}$/.test(group)) {
985
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
986
+ }
987
+ }
988
+ const [ok, err] = await checkPermission(session, group)
989
+ if (!ok) return err || '权限不足'
990
+ }
991
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
992
+ if (rows.length === 0) {
993
+ // 特殊处理 all 表示全局黑名单
994
+ if (group && group.toLowerCase() === 'all') {
995
+ // 不使用模板,因为默认模板会产生 "群 all 的黑名单为空" 这种奇怪输出
996
+ return '全局黑名单为空'
997
+ }
998
+ const tmpl = (config && config.blacklistListEmpty) || '群 {group} 的黑名单为空'
999
+ return tmpl.replace('{group}', group)
1000
+ }
1001
+ const entries = rows[0].entries || {}
1002
+ // 构造列表消息,all 也是专用前缀
1003
+ let prefix = group && group.toLowerCase() === 'all' ? '全局黑名单: \n' : `群 ${group} 黑名单: \n`
1004
+ let msg = prefix
1005
+ for (const uid in entries) {
1006
+ msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ''}\n`
1007
+ }
1008
+ return msg
1009
+ }
1010
+ if (op === 'i') {
1011
+ targetUser = parts[1]
1012
+ if (!targetUser) return '请提供用户ID'
1013
+ const groupArg = parts[2]
1014
+ const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
1015
+ const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== undefined
1016
+
1017
+ // helper to format reply using template
1018
+ const tmpl = (config && config.blacklistInfoTemplate) || '全局黑名单: {global}\n本群黑名单: {group}'
1019
+ const formatReply = (localHit: boolean, groupsList?: string[]) => {
1020
+ if (groupsList) {
1021
+ return tmpl.replace('{global}', globalHit ? '有' : '无').replace(
1022
+ '{group}', groupsList.length ? groupsList.join(',') : '无'
1023
+ )
1024
+ }
1025
+ return tmpl.replace('{global}', globalHit ? '有' : '无').replace('{group}', localHit ? '有' : '无')
1026
+ }
1027
+
1028
+ // no group specified -> use current session guild
1029
+ if (!groupArg) {
1030
+ const groupId = getCurrentGroup()
1031
+ if (!groupId) return '请在群聊中使用此命令'
1032
+ const [ok, err] = await checkPermission(session, groupId)
1033
+ if (!ok) return err || '权限不足'
1034
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId })
1035
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
1036
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
1037
+ return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
1038
+ }
1039
+
1040
+ // groupArg provided
1041
+ if (groupArg.toLowerCase() === 'all') {
1042
+ const auth = session.author?.authority || session.user?.authority
1043
+ if (!(auth && auth >= 3)) return '权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限'
1044
+ const rows = await ctx.database.get('group_verification_blacklist', {})
1045
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : '无'
1046
+ const lines: string[] = []
1047
+ for (const r of rows) {
1048
+ if (r.groupId === 'all') continue
1049
+ const reason = (r.entries || {})[targetUser]
1050
+ if (reason !== undefined) {
1051
+ lines.push(`群${r.groupId}:${reason}`)
1052
+ }
1053
+ }
1054
+ let reply = `全局黑名单: ${globalReason}`
1055
+ if (lines.length) {
1056
+ reply += '\n群黑名单: \n' + lines.join('\n')
1057
+ } else {
1058
+ reply += '\n群黑名单: 无'
1059
+ }
1060
+ return reply
1061
+ }
1062
+
1063
+ // specific group provided
1064
+ const groupId = groupArg
1065
+ if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== 'all') {
1066
+ if (!/^\d{5,15}$/.test(groupId)) {
1067
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', groupId)
1068
+ }
1069
+ }
1070
+ const [ok, err] = await checkPermission(session, groupId)
1071
+ if (!ok) return err || '权限不足'
1072
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId })
1073
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
1074
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
1075
+ return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
1076
+ }
1077
+ return ''
1078
+ }
1079
+
812
1080
  export function apply(ctx: Context, config: Config) {
813
1081
  // 创建数据库表
814
1082
  ctx.model.extend('group_verification_config', {
@@ -873,6 +1141,17 @@ export function apply(ctx: Context, config: Config) {
873
1141
  autoInc: true
874
1142
  })
875
1143
 
1144
+ // 黑名单表:每条记录对应一个群(或全局),entries 存储 userId->reason 键值对
1145
+ ctx.model.extend('group_verification_blacklist', {
1146
+ id: 'unsigned',
1147
+ groupId: 'string',
1148
+ entries: 'json'
1149
+ } as any, {
1150
+ primary: 'id',
1151
+ autoInc: true,
1152
+ indexes: [ ['groupId'] ]
1153
+ })
1154
+
876
1155
 
877
1156
 
878
1157
  // 监听 guild-member-request 事件,以便对新申请执行自动审批或拒绝
@@ -882,9 +1161,13 @@ export function apply(ctx: Context, config: Config) {
882
1161
  })
883
1162
 
884
1163
  // 监听群成员增加事件(包括手动邀请入群)
885
- ctx.on('guild-member-added', async (session) => {
886
- const groupId = session.guildId
887
- const userId = session.userId
1164
+ ctx.on('guild-member-added', async (session: any) => {
1165
+ const groupId = session.guildId || ''
1166
+ const userId = session.userId || ''
1167
+ if (!groupId || !userId) {
1168
+ // 无效会话,忽略
1169
+ return
1170
+ }
888
1171
 
889
1172
  // 无论什么情况只要检测到加入就累加总入群
890
1173
  await incrementTotal(ctx, groupId)
@@ -991,7 +1274,7 @@ export function apply(ctx: Context, config: Config) {
991
1274
  return [true]
992
1275
  }
993
1276
  }
994
- } catch (error) {
1277
+ } catch (error: any) {
995
1278
  logger.warn(`权限检查 - 获取群成员信息失败:`, error)
996
1279
  return [false, `无法获取群 ${groupId} 的成员信息,请确认机器人已在该群中`]
997
1280
  }
@@ -1020,7 +1303,7 @@ export function apply(ctx: Context, config: Config) {
1020
1303
  .option('disableMessage', '-nomsg 禁用提醒消息')
1021
1304
  .option('query', '-? 查询当前配置')
1022
1305
  .option('remove', '-r 删除配置')
1023
- .action(async ({ session, options }, keywords) => {
1306
+ .action(async ({ session, options }: any, keywords: any) => {
1024
1307
  // 详细调试:记录所有输入信息
1025
1308
  logger.info(`=== 命令解析调试 ===`)
1026
1309
  logger.info(`session内容: guildId=${session.guildId}, userId=${session.userId}`)
@@ -1115,7 +1398,7 @@ export function apply(ctx: Context, config: Config) {
1115
1398
  try {
1116
1399
  const guild = await session.bot.getGuild(targetGroupId)
1117
1400
  groupName = guild.name || groupName
1118
- } catch (error) {
1401
+ } catch (error: any) {
1119
1402
  // 无法获取群名称时使用默认值
1120
1403
  }
1121
1404
 
@@ -1388,7 +1671,7 @@ export function apply(ctx: Context, config: Config) {
1388
1671
  'gv.同意', 'gverify.同意', 'group-verify.同意',
1389
1672
  'gva'
1390
1673
  )
1391
- .action(async ({ session }, userId) => {
1674
+ .action(async ({ session }: any, userId: any) => {
1392
1675
  // 权限检查
1393
1676
  const [hasPermission, errorMsg] = await checkPermission(session)
1394
1677
  if (!hasPermission) {
@@ -1421,7 +1704,7 @@ export function apply(ctx: Context, config: Config) {
1421
1704
  try {
1422
1705
  await session.bot.handleGuildMemberRequest(request.requestId, true)
1423
1706
  approvedCount++
1424
- } catch (error) {
1707
+ } catch (error: any) {
1425
1708
  logger.warn(`处理申请 ${request.id} 时出错:`, error)
1426
1709
  }
1427
1710
  } else {
@@ -1445,12 +1728,14 @@ export function apply(ctx: Context, config: Config) {
1445
1728
  return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
1446
1729
  }
1447
1730
  try {
1448
- await session.bot.handleGuildMemberRequest(request.requestId, true)
1449
- // 清除该用户的所有待审核记录
1450
- await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1451
1731
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1452
- return `已同意用户 ${displayName} 的加群申请`
1453
- } catch (error) {
1732
+ const reply = `已同意用户 ${displayName} 的加群申请`
1733
+ // 先返回提示消息,再异步执行批准请求
1734
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
1735
+ // 清除该用户的所有待审核记录(异步也可以,顺序无关)
1736
+ await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1737
+ return reply
1738
+ } catch (error: any) {
1454
1739
  return `处理申请时出错: ${error.message}`
1455
1740
  }
1456
1741
  }
@@ -1471,12 +1756,12 @@ export function apply(ctx: Context, config: Config) {
1471
1756
  return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
1472
1757
  }
1473
1758
  try {
1474
- await session.bot.handleGuildMemberRequest(request.requestId, true)
1475
- // 删除该用户的所有记录
1476
- await ctx.database.remove('group_verification_pending', { groupId, userId })
1477
1759
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1478
- return `已同意用户 ${displayName} 的加群申请`
1479
- } catch (error) {
1760
+ const reply = `已同意用户 ${displayName} 的加群申请`
1761
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
1762
+ await ctx.database.remove('group_verification_pending', { groupId, userId })
1763
+ return reply
1764
+ } catch (error: any) {
1480
1765
  return `处理申请时出错: ${error.message}`
1481
1766
  }
1482
1767
  })
@@ -1489,7 +1774,7 @@ export function apply(ctx: Context, config: Config) {
1489
1774
  'gv.rej', 'gverify.rej', 'group-verify.rej',
1490
1775
  'gvr'
1491
1776
  )
1492
- .action(async ({ session }, userId) => {
1777
+ .action(async ({ session }: any, userId: any) => {
1493
1778
  // 权限检查
1494
1779
  const [hasPermission, errorMsg] = await checkPermission(session)
1495
1780
  if (!hasPermission) {
@@ -1547,7 +1832,7 @@ export function apply(ctx: Context, config: Config) {
1547
1832
  await updateStats(ctx, groupId, 'rejected')
1548
1833
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1549
1834
  return `已拒绝用户 ${displayName} 的加群申请`
1550
- } catch (error) {
1835
+ } catch (error: any) {
1551
1836
  return `处理申请时出错: ${error.message}`
1552
1837
  }
1553
1838
  }
@@ -1574,7 +1859,7 @@ export function apply(ctx: Context, config: Config) {
1574
1859
  await updateStats(ctx, groupId, 'rejected')
1575
1860
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1576
1861
  return `已拒绝用户 ${displayName} 的加群申请`
1577
- } catch (error) {
1862
+ } catch (error: any) {
1578
1863
  return `处理申请时出错: ${error.message}`
1579
1864
  }
1580
1865
  })
@@ -1586,7 +1871,7 @@ export function apply(ctx: Context, config: Config) {
1586
1871
  'gv.统计', 'gverify.统计', 'group-verify.统计',
1587
1872
  'gvs'
1588
1873
  )
1589
- .action(async ({ session }, target) => {
1874
+ .action(async ({ session }: any, target: any) => {
1590
1875
  // 参数验证:只能是群号、all、total或空
1591
1876
  const validTargets = ['all', 'total']
1592
1877
  const isGroupId = target && /^\d+$/.test(target)
@@ -1637,7 +1922,7 @@ export function apply(ctx: Context, config: Config) {
1637
1922
  'gv.待处理', 'gverify.待处理', 'group-verify.待处理',
1638
1923
  'gvp'
1639
1924
  )
1640
- .action(async ({ session }) => {
1925
+ .action(async ({ session }: any) => {
1641
1926
  if (!session.guildId) {
1642
1927
  return '请在群聊中使用此命令'
1643
1928
  }
@@ -1662,6 +1947,18 @@ export function apply(ctx: Context, config: Config) {
1662
1947
  return result
1663
1948
  })
1664
1949
 
1950
+ // Subcommand: blacklist management
1951
+ groupVerify
1952
+ .subcommand('.blacklist [args:text]', '管理加群黑名单')
1953
+ .alias(
1954
+ 'gvb',
1955
+ 'gv.blacklist', 'gverify.blacklist', 'group-verify.blacklist',
1956
+ 'gv.黑名单', 'gverify.黑名单', 'group-verify.黑名单'
1957
+ )
1958
+ .action(async ({ session }: any, args: any) => {
1959
+ return await processBlacklistCommand(ctx, session, args || '', config)
1960
+ })
1961
+
1665
1962
  // Subcommand: help information
1666
1963
  groupVerify
1667
1964
  .subcommand('.help', '显示帮助信息')
@@ -1718,6 +2015,19 @@ export function apply(ctx: Context, config: Config) {
1718
2015
  gv.cfg -m 2 -t 80
1719
2016
  gv.cfg -nomsg
1720
2017
 
2018
+ 黑名单命令 (.blacklist / gvb):
2019
+ a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
2020
+ r id [group] 删除条目;group 为 all 时移除全局黑名单
2021
+ l [group] 查询群黑名单;传入 all 时查看全局黑名单
2022
+ i id [group] 查询账号黑名单;不带参数时查询本群与全局,
2023
+ 指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
2024
+ 使用示例:
2025
+ gvb a 12345 作弊记录
2026
+ gvb r 12345 67890
2027
+ gvb l
2028
+ gvb l all
2029
+ gvb i 12345
2030
+
1721
2031
  快捷命令:
1722
2032
  gvc - 配置命令快捷方式
1723
2033
  gva - 同意申请快捷命令