koishi-plugin-group-verification 1.0.30 → 1.0.31

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";
@@ -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,208 @@ 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 > 0) {
665
+ const last = rest[rest.length - 1];
666
+ if (/^\d+$/.test(last) || last.toLowerCase() === "all") {
667
+ group = last;
668
+ reason = rest.slice(0, -1).join(" ");
669
+ } else {
670
+ reason = rest.join(" ");
671
+ }
672
+ }
673
+ group = group || getCurrentGroup();
674
+ if (!group) return "请在群聊中使用此命令或指定群号";
675
+ if (group.toLowerCase() === "all") {
676
+ const auth = session.author?.authority || session.user?.authority;
677
+ if (!(auth && auth >= 3)) return "设置全局黑名单需要 koishi 3 级以上权限";
678
+ } else {
679
+ if (config?.enableStrictGroupCheck) {
680
+ if (!/^\d{5,15}$/.test(group)) {
681
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
682
+ }
683
+ }
684
+ const [ok, err] = await checkPermission(session, group);
685
+ if (!ok) return err || "权限不足";
686
+ }
687
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
688
+ const timePrefix = (/* @__PURE__ */ new Date()).toLocaleString();
689
+ const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix;
690
+ if (rows.length > 0) {
691
+ const row = rows[0];
692
+ const entries = row.entries || {};
693
+ if (entries[targetUser] !== void 0) {
694
+ return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`;
695
+ }
696
+ entries[targetUser] = storedReason;
697
+ await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
698
+ } else {
699
+ const entries = {};
700
+ entries[targetUser] = storedReason;
701
+ await ctx.database.create("group_verification_blacklist", { groupId: group, entries });
702
+ }
703
+ const tmpl = config && config.blacklistAddSuccess || "已将用户 {user} 加入群 {group} 黑名单{reason}";
704
+ return tmpl.replace("{user}", targetUser).replace("{group}", group).replace("{reason}", reason ? `,原因:${reason}` : "");
705
+ }
706
+ if (op === "r") {
707
+ targetUser = parts[1];
708
+ if (!targetUser) return "请提供用户ID";
709
+ group = parts[2] || getCurrentGroup();
710
+ if (!group) return "请在群聊中使用此命令或指定群号";
711
+ if (group.toLowerCase() === "all") {
712
+ const auth = session.author?.authority || session.user?.authority;
713
+ if (!(auth && auth >= 3)) return "修改全局黑名单需要 koishi 3 级以上权限";
714
+ } else {
715
+ if (config?.enableStrictGroupCheck) {
716
+ if (!/^\d{5,15}$/.test(group)) {
717
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
718
+ }
719
+ }
720
+ const [ok, err] = await checkPermission(session, group);
721
+ if (!ok) return err || "权限不足";
722
+ }
723
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
724
+ if (rows.length > 0) {
725
+ const row = rows[0];
726
+ const entries = row.entries || {};
727
+ delete entries[targetUser];
728
+ await ctx.database.set("group_verification_blacklist", { id: row.id }, { entries });
729
+ }
730
+ const tmpl = config && config.blacklistRemoveSuccess || "已从群 {group} 的黑名单中移除用户 {user}";
731
+ return tmpl.replace("{user}", targetUser).replace("{group}", group);
732
+ }
733
+ if (op === "l") {
734
+ group = parts[1] || getCurrentGroup();
735
+ if (!group) return "请在群聊中使用此命令或指定群号";
736
+ if (group.toLowerCase() === "all") {
737
+ const auth = session.author?.authority || session.user?.authority;
738
+ if (!(auth && auth >= 3)) return "查看全局黑名单需要 koishi 3 级以上权限";
739
+ } else {
740
+ if (config?.enableStrictGroupCheck) {
741
+ if (!/^\d{5,15}$/.test(group)) {
742
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", group);
743
+ }
744
+ }
745
+ const [ok, err] = await checkPermission(session, group);
746
+ if (!ok) return err || "权限不足";
747
+ }
748
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId: group });
749
+ if (rows.length === 0) {
750
+ if (group && group.toLowerCase() === "all") {
751
+ return "全局黑名单为空";
752
+ }
753
+ const tmpl = config && config.blacklistListEmpty || "群 {group} 的黑名单为空";
754
+ return tmpl.replace("{group}", group);
755
+ }
756
+ const entries = rows[0].entries || {};
757
+ let prefix = group && group.toLowerCase() === "all" ? "全局黑名单: \n" : `群 ${group} 黑名单:
758
+ `;
759
+ let msg = prefix;
760
+ for (const uid in entries) {
761
+ msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ""}
762
+ `;
763
+ }
764
+ return msg;
765
+ }
766
+ if (op === "i") {
767
+ targetUser = parts[1];
768
+ if (!targetUser) return "请提供用户ID";
769
+ const groupArg = parts[2];
770
+ const globalRows = await ctx.database.get("group_verification_blacklist", { groupId: "all" });
771
+ const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== void 0;
772
+ const tmpl = config && config.blacklistInfoTemplate || "全局黑名单: {global}\n本群黑名单: {group}";
773
+ const formatReply = /* @__PURE__ */ __name((localHit, groupsList) => {
774
+ if (groupsList) {
775
+ return tmpl.replace("{global}", globalHit ? "有" : "无").replace(
776
+ "{group}",
777
+ groupsList.length ? groupsList.join(",") : "无"
778
+ );
779
+ }
780
+ return tmpl.replace("{global}", globalHit ? "有" : "无").replace("{group}", localHit ? "有" : "无");
781
+ }, "formatReply");
782
+ if (!groupArg) {
783
+ const groupId2 = getCurrentGroup();
784
+ if (!groupId2) return "请在群聊中使用此命令";
785
+ const [ok2, err2] = await checkPermission(session, groupId2);
786
+ if (!ok2) return err2 || "权限不足";
787
+ const rows2 = await ctx.database.get("group_verification_blacklist", { groupId: groupId2 });
788
+ const localReason2 = rows2.length > 0 ? (rows2[0].entries || {})[targetUser] : void 0;
789
+ const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : void 0;
790
+ return `全局黑名单: ${globalReason2 || "无"}
791
+ 群${groupId2}黑名单: ${localReason2 || "无"}`;
792
+ }
793
+ if (groupArg.toLowerCase() === "all") {
794
+ const auth = session.author?.authority || session.user?.authority;
795
+ if (!(auth && auth >= 3)) return "权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限";
796
+ const rows2 = await ctx.database.get("group_verification_blacklist", {});
797
+ const globalReason2 = globalHit ? globalRows[0].entries[targetUser] : "无";
798
+ const lines = [];
799
+ for (const r of rows2) {
800
+ if (r.groupId === "all") continue;
801
+ const reason2 = (r.entries || {})[targetUser];
802
+ if (reason2 !== void 0) {
803
+ lines.push(`群${r.groupId}:${reason2}`);
804
+ }
805
+ }
806
+ let reply = `全局黑名单: ${globalReason2}`;
807
+ if (lines.length) {
808
+ reply += "\n群黑名单: \n" + lines.join("\n");
809
+ } else {
810
+ reply += "\n群黑名单: 无";
811
+ }
812
+ return reply;
813
+ }
814
+ const groupId = groupArg;
815
+ if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== "all") {
816
+ if (!/^\d{5,15}$/.test(groupId)) {
817
+ return (config.invalidGroupMessage || "群号 {group} 格式不合法或机器人不在该群中").replace("{group}", groupId);
818
+ }
819
+ }
820
+ const [ok, err] = await checkPermission(session, groupId);
821
+ if (!ok) return err || "权限不足";
822
+ const rows = await ctx.database.get("group_verification_blacklist", { groupId });
823
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : void 0;
824
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : void 0;
825
+ return `全局黑名单: ${globalReason || "无"}
826
+ 群${groupId}黑名单: ${localReason || "无"}`;
827
+ }
828
+ return "";
829
+ }
830
+ __name(processBlacklistCommand, "processBlacklistCommand");
606
831
  function apply(ctx, config) {
607
832
  ctx.model.extend("group_verification_config", {
608
833
  id: "unsigned",
@@ -655,13 +880,25 @@ function apply(ctx, config) {
655
880
  primary: "id",
656
881
  autoInc: true
657
882
  });
883
+ ctx.model.extend("group_verification_blacklist", {
884
+ id: "unsigned",
885
+ groupId: "string",
886
+ entries: "json"
887
+ }, {
888
+ primary: "id",
889
+ autoInc: true,
890
+ indexes: [["groupId"]]
891
+ });
658
892
  ctx.on("guild-member-request", async (session) => {
659
893
  logger.info("收到 guild-member-request 事件,转发给处理函数");
660
894
  await handleGuildMemberRequestEvent(ctx, session);
661
895
  });
662
896
  ctx.on("guild-member-added", async (session) => {
663
- const groupId = session.guildId;
664
- const userId = session.userId;
897
+ const groupId = session.guildId || "";
898
+ const userId = session.userId || "";
899
+ if (!groupId || !userId) {
900
+ return;
901
+ }
665
902
  await incrementTotal(ctx, groupId);
666
903
  const set = autoQueue.get(groupId);
667
904
  if (set && set.has(userId)) {
@@ -1083,10 +1320,11 @@ ${reminderMessage}
1083
1320
  return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
1084
1321
  }
1085
1322
  try {
1086
- await session.bot.handleGuildMemberRequest(request2.requestId, true);
1087
- await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1088
1323
  const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
1089
- return `已同意用户 ${displayName} 的加群申请`;
1324
+ const reply = `已同意用户 ${displayName} 的加群申请`;
1325
+ session.bot.handleGuildMemberRequest(request2.requestId, true).catch((e) => logger.warn("自动同意失败", e));
1326
+ await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1327
+ return reply;
1090
1328
  } catch (error) {
1091
1329
  return `处理申请时出错: ${error.message}`;
1092
1330
  }
@@ -1104,10 +1342,11 @@ ${reminderMessage}
1104
1342
  return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
1105
1343
  }
1106
1344
  try {
1107
- await session.bot.handleGuildMemberRequest(request.requestId, true);
1108
- await ctx.database.remove("group_verification_pending", { groupId, userId });
1109
1345
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
1110
- return `已同意用户 ${displayName} 的加群申请`;
1346
+ const reply = `已同意用户 ${displayName} 的加群申请`;
1347
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e) => logger.warn("自动同意失败", e));
1348
+ await ctx.database.remove("group_verification_pending", { groupId, userId });
1349
+ return reply;
1111
1350
  } catch (error) {
1112
1351
  return `处理申请时出错: ${error.message}`;
1113
1352
  }
@@ -1257,6 +1496,17 @@ ${reminderMessage}
1257
1496
  });
1258
1497
  return result;
1259
1498
  });
1499
+ groupVerify.subcommand(".blacklist [args:text]", "管理加群黑名单").alias(
1500
+ "gvb",
1501
+ "gv.blacklist",
1502
+ "gverify.blacklist",
1503
+ "group-verify.blacklist",
1504
+ "gv.黑名单",
1505
+ "gverify.黑名单",
1506
+ "group-verify.黑名单"
1507
+ ).action(async ({ session }, args) => {
1508
+ return await processBlacklistCommand(ctx, session, args || "", config);
1509
+ });
1260
1510
  groupVerify.subcommand(".help", "显示帮助信息").alias("gv.帮助", "gverify.帮助", "group-verify.帮助", "帮助", "hlp", "帮助信息").action(() => {
1261
1511
  return `群组验证命令帮助:
1262
1512
  主指令别名:gv, gverify
@@ -1309,6 +1559,19 @@ ${reminderMessage}
1309
1559
  gv.cfg -m 2 -t 80
1310
1560
  gv.cfg -nomsg
1311
1561
 
1562
+ 黑名单命令 (.blacklist / gvb):
1563
+ a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
1564
+ r id [group] 删除条目;group 为 all 时移除全局黑名单
1565
+ l [group] 查询群黑名单;传入 all 时查看全局黑名单
1566
+ i id [group] 查询账号黑名单;不带参数时查询本群与全局,
1567
+ 指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
1568
+ 使用示例:
1569
+ gvb a 12345 作弊记录
1570
+ gvb r 12345 67890
1571
+ gvb l
1572
+ gvb l all
1573
+ gvb i 12345
1574
+
1312
1575
  快捷命令:
1313
1576
  gvc - 配置命令快捷方式
1314
1577
  gva - 同意申请快捷命令
@@ -1409,9 +1672,11 @@ __name(apply, "apply");
1409
1672
  handleGuildMemberRequestEvent,
1410
1673
  incrementTotal,
1411
1674
  inject,
1675
+ isUserBlacklisted,
1412
1676
  mergeReminder,
1413
1677
  name,
1414
1678
  parseConfigArgs,
1679
+ processBlacklistCommand,
1415
1680
  resolveThreshold,
1416
1681
  syncTotalStats,
1417
1682
  tokenize,
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
- {
2
- "name": "koishi-plugin-group-verification",
3
- "description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能。",
4
- "repository": {
5
- "type": "git",
6
- "url": "https://github.com/LHDyx/koishi-plugin-group-verification.git"
7
- },
8
- "bugs": {
9
- "url": "https://github.com/LHDyx/koishi-plugin-group-verification/issues"
10
- },
11
- "version": "1.0.30",
12
- "main": "lib/index.js",
13
- "typings": "lib/index.d.ts",
14
- "files": [
15
- "lib",
16
- "dist",
17
- "src",
18
- "README.md",
19
- "USAGE_EXAMPLE.md"
20
- ],
21
- "scripts": {
22
- "build": "tsc",
23
- "dev": "tsc -w",
24
- "test": "ts-node test/basic-test.ts"
25
- },
26
- "license": "MIT",
27
- "keywords": [
28
- "chatbot",
29
- "koishi",
30
- "plugin",
31
- "group-verification",
32
- "guild-management",
33
- "join-request",
34
- "moderation"
35
- ],
36
- "koishi": {
37
- "service": {
38
- "required": [
39
- "database"
40
- ]
41
- }
42
- },
43
- "peerDependencies": {
44
- "koishi": "^4.15.0"
45
- },
46
- "devDependencies": {
47
- "typescript": "^4.9.0",
48
- "@types/node": "^16.0.0"
49
- }
50
- }
1
+ {
2
+ "name": "koishi-plugin-group-verification",
3
+ "description": "Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能",
4
+
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/LHDyx/koishi-plugin-group-verification.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/LHDyx/koishi-plugin-group-verification/issues"
11
+ },
12
+ "version": "1.0.31",
13
+ "main": "lib/index.js",
14
+ "typings": "lib/index.d.ts",
15
+ "files": [
16
+ "lib",
17
+ "dist",
18
+ "src",
19
+ "README.md",
20
+ "USAGE_EXAMPLE.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc -w",
25
+ "test": "ts-node test/basic-test.ts"
26
+ },
27
+ "license": "MIT",
28
+ "keywords": [
29
+ "chatbot",
30
+ "koishi",
31
+ "plugin",
32
+ "group-verification",
33
+ "guild-management",
34
+ "join-request",
35
+ "moderation"
36
+ ],
37
+ "koishi": {
38
+ "service": {
39
+ "required": [
40
+ "database"
41
+ ]
42
+ }
43
+ },
44
+ "peerDependencies": {
45
+ "koishi": "^4.15.0"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "^4.9.0",
49
+ "@types/node": "^16.0.0"
50
+ }
51
+ }
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
 
@@ -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,228 @@ 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
+ const rest = parts.slice(2)
890
+ if (rest.length > 0) {
891
+ const last = rest[rest.length - 1]
892
+ if (/^\d+$/.test(last) || last.toLowerCase() === 'all') {
893
+ group = last
894
+ reason = rest.slice(0, -1).join(' ')
895
+ } else {
896
+ reason = rest.join(' ')
897
+ }
898
+ }
899
+ group = group || getCurrentGroup()
900
+ if (!group) return '请在群聊中使用此命令或指定群号'
901
+ // 权限检查
902
+ if (group.toLowerCase() === 'all') {
903
+ const auth = session.author?.authority || session.user?.authority
904
+ if (!(auth && auth >= 3)) return '设置全局黑名单需要 koishi 3 级以上权限'
905
+ } else {
906
+ // 严格群号检查(若开启)
907
+ if (config?.enableStrictGroupCheck) {
908
+ if (!/^\d{5,15}$/.test(group)) {
909
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
910
+ }
911
+ }
912
+ const [ok, err] = await checkPermission(session, group)
913
+ if (!ok) return err || '权限不足'
914
+ }
915
+ // add entry to map with timestamp, but refuse duplicates
916
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
917
+ // build stored reason with current time prefix
918
+ const timePrefix = new Date().toLocaleString()
919
+ const storedReason = reason ? `${timePrefix} ${reason}` : timePrefix
920
+ if (rows.length > 0) {
921
+ const row = rows[0]
922
+ const entries = row.entries || {}
923
+ if (entries[targetUser] !== undefined) {
924
+ return `用户 ${targetUser} 已在群 ${group} 的黑名单中:${entries[targetUser]}`
925
+ }
926
+ entries[targetUser] = storedReason
927
+ await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
928
+ } else {
929
+ const entries: Record<string,string> = {}
930
+ entries[targetUser] = storedReason
931
+ await ctx.database.create('group_verification_blacklist', { groupId: group, entries })
932
+ }
933
+ const tmpl = (config && config.blacklistAddSuccess) || '已将用户 {user} 加入群 {group} 黑名单{reason}'
934
+ return tmpl.replace('{user}', targetUser).replace('{group}', group).replace('{reason}', reason ? `,原因:${reason}` : '')
935
+ }
936
+ if (op === 'r') {
937
+ targetUser = parts[1]
938
+ if (!targetUser) return '请提供用户ID'
939
+ group = parts[2] || getCurrentGroup()
940
+ if (!group) return '请在群聊中使用此命令或指定群号'
941
+ if (group.toLowerCase() === 'all') {
942
+ const auth = session.author?.authority || session.user?.authority
943
+ if (!(auth && auth >= 3)) return '修改全局黑名单需要 koishi 3 级以上权限'
944
+ } else {
945
+ if (config?.enableStrictGroupCheck) {
946
+ if (!/^\d{5,15}$/.test(group)) {
947
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
948
+ }
949
+ }
950
+ const [ok, err] = await checkPermission(session, group)
951
+ if (!ok) return err || '权限不足'
952
+ }
953
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
954
+ if (rows.length > 0) {
955
+ const row = rows[0]
956
+ const entries = row.entries || {}
957
+ delete entries[targetUser]
958
+ await ctx.database.set('group_verification_blacklist', { id: row.id }, { entries })
959
+ }
960
+ const tmpl = (config && config.blacklistRemoveSuccess) || '已从群 {group} 的黑名单中移除用户 {user}'
961
+ return tmpl.replace('{user}', targetUser).replace('{group}', group)
962
+ }
963
+ if (op === 'l') {
964
+ group = parts[1] || getCurrentGroup()
965
+ if (!group) return '请在群聊中使用此命令或指定群号'
966
+ if (group.toLowerCase() === 'all') {
967
+ const auth = session.author?.authority || session.user?.authority
968
+ if (!(auth && auth >= 3)) return '查看全局黑名单需要 koishi 3 级以上权限'
969
+ } else {
970
+ if (config?.enableStrictGroupCheck) {
971
+ if (!/^\d{5,15}$/.test(group)) {
972
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', group)
973
+ }
974
+ }
975
+ const [ok, err] = await checkPermission(session, group)
976
+ if (!ok) return err || '权限不足'
977
+ }
978
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId: group })
979
+ if (rows.length === 0) {
980
+ // 特殊处理 all 表示全局黑名单
981
+ if (group && group.toLowerCase() === 'all') {
982
+ // 不使用模板,因为默认模板会产生 "群 all 的黑名单为空" 这种奇怪输出
983
+ return '全局黑名单为空'
984
+ }
985
+ const tmpl = (config && config.blacklistListEmpty) || '群 {group} 的黑名单为空'
986
+ return tmpl.replace('{group}', group)
987
+ }
988
+ const entries = rows[0].entries || {}
989
+ // 构造列表消息,all 也是专用前缀
990
+ let prefix = group && group.toLowerCase() === 'all' ? '全局黑名单: \n' : `群 ${group} 黑名单: \n`
991
+ let msg = prefix
992
+ for (const uid in entries) {
993
+ msg += `${uid}${entries[uid] ? `:${entries[uid]}` : ''}\n`
994
+ }
995
+ return msg
996
+ }
997
+ if (op === 'i') {
998
+ targetUser = parts[1]
999
+ if (!targetUser) return '请提供用户ID'
1000
+ const groupArg = parts[2]
1001
+ const globalRows = await ctx.database.get('group_verification_blacklist', { groupId: 'all' })
1002
+ const globalHit = globalRows.length > 0 && (globalRows[0].entries || {})[targetUser] !== undefined
1003
+
1004
+ // helper to format reply using template
1005
+ const tmpl = (config && config.blacklistInfoTemplate) || '全局黑名单: {global}\n本群黑名单: {group}'
1006
+ const formatReply = (localHit: boolean, groupsList?: string[]) => {
1007
+ if (groupsList) {
1008
+ return tmpl.replace('{global}', globalHit ? '有' : '无').replace(
1009
+ '{group}', groupsList.length ? groupsList.join(',') : '无'
1010
+ )
1011
+ }
1012
+ return tmpl.replace('{global}', globalHit ? '有' : '无').replace('{group}', localHit ? '有' : '无')
1013
+ }
1014
+
1015
+ // no group specified -> use current session guild
1016
+ if (!groupArg) {
1017
+ const groupId = getCurrentGroup()
1018
+ if (!groupId) return '请在群聊中使用此命令'
1019
+ const [ok, err] = await checkPermission(session, groupId)
1020
+ if (!ok) return err || '权限不足'
1021
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId })
1022
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
1023
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
1024
+ return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
1025
+ }
1026
+
1027
+ // groupArg provided
1028
+ if (groupArg.toLowerCase() === 'all') {
1029
+ const auth = session.author?.authority || session.user?.authority
1030
+ if (!(auth && auth >= 3)) return '权限不足:查看全局/所有群黑名单需要 koishi 3 级以上权限'
1031
+ const rows = await ctx.database.get('group_verification_blacklist', {})
1032
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : '无'
1033
+ const lines: string[] = []
1034
+ for (const r of rows) {
1035
+ if (r.groupId === 'all') continue
1036
+ const reason = (r.entries || {})[targetUser]
1037
+ if (reason !== undefined) {
1038
+ lines.push(`群${r.groupId}:${reason}`)
1039
+ }
1040
+ }
1041
+ let reply = `全局黑名单: ${globalReason}`
1042
+ if (lines.length) {
1043
+ reply += '\n群黑名单: \n' + lines.join('\n')
1044
+ } else {
1045
+ reply += '\n群黑名单: 无'
1046
+ }
1047
+ return reply
1048
+ }
1049
+
1050
+ // specific group provided
1051
+ const groupId = groupArg
1052
+ if (config?.enableStrictGroupCheck && groupId.toLowerCase() !== 'all') {
1053
+ if (!/^\d{5,15}$/.test(groupId)) {
1054
+ return (config.invalidGroupMessage || '群号 {group} 格式不合法或机器人不在该群中').replace('{group}', groupId)
1055
+ }
1056
+ }
1057
+ const [ok, err] = await checkPermission(session, groupId)
1058
+ if (!ok) return err || '权限不足'
1059
+ const rows = await ctx.database.get('group_verification_blacklist', { groupId })
1060
+ const localReason = rows.length > 0 ? (rows[0].entries || {})[targetUser] : undefined
1061
+ const globalReason = globalHit ? globalRows[0].entries[targetUser] : undefined
1062
+ return `全局黑名单: ${globalReason || '无'}\n群${groupId}黑名单: ${localReason || '无'}`
1063
+ }
1064
+ return ''
1065
+ }
1066
+
812
1067
  export function apply(ctx: Context, config: Config) {
813
1068
  // 创建数据库表
814
1069
  ctx.model.extend('group_verification_config', {
@@ -873,6 +1128,17 @@ export function apply(ctx: Context, config: Config) {
873
1128
  autoInc: true
874
1129
  })
875
1130
 
1131
+ // 黑名单表:每条记录对应一个群(或全局),entries 存储 userId->reason 键值对
1132
+ ctx.model.extend('group_verification_blacklist', {
1133
+ id: 'unsigned',
1134
+ groupId: 'string',
1135
+ entries: 'json'
1136
+ } as any, {
1137
+ primary: 'id',
1138
+ autoInc: true,
1139
+ indexes: [ ['groupId'] ]
1140
+ })
1141
+
876
1142
 
877
1143
 
878
1144
  // 监听 guild-member-request 事件,以便对新申请执行自动审批或拒绝
@@ -882,9 +1148,13 @@ export function apply(ctx: Context, config: Config) {
882
1148
  })
883
1149
 
884
1150
  // 监听群成员增加事件(包括手动邀请入群)
885
- ctx.on('guild-member-added', async (session) => {
886
- const groupId = session.guildId
887
- const userId = session.userId
1151
+ ctx.on('guild-member-added', async (session: any) => {
1152
+ const groupId = session.guildId || ''
1153
+ const userId = session.userId || ''
1154
+ if (!groupId || !userId) {
1155
+ // 无效会话,忽略
1156
+ return
1157
+ }
888
1158
 
889
1159
  // 无论什么情况只要检测到加入就累加总入群
890
1160
  await incrementTotal(ctx, groupId)
@@ -991,7 +1261,7 @@ export function apply(ctx: Context, config: Config) {
991
1261
  return [true]
992
1262
  }
993
1263
  }
994
- } catch (error) {
1264
+ } catch (error: any) {
995
1265
  logger.warn(`权限检查 - 获取群成员信息失败:`, error)
996
1266
  return [false, `无法获取群 ${groupId} 的成员信息,请确认机器人已在该群中`]
997
1267
  }
@@ -1020,7 +1290,7 @@ export function apply(ctx: Context, config: Config) {
1020
1290
  .option('disableMessage', '-nomsg 禁用提醒消息')
1021
1291
  .option('query', '-? 查询当前配置')
1022
1292
  .option('remove', '-r 删除配置')
1023
- .action(async ({ session, options }, keywords) => {
1293
+ .action(async ({ session, options }: any, keywords: any) => {
1024
1294
  // 详细调试:记录所有输入信息
1025
1295
  logger.info(`=== 命令解析调试 ===`)
1026
1296
  logger.info(`session内容: guildId=${session.guildId}, userId=${session.userId}`)
@@ -1115,7 +1385,7 @@ export function apply(ctx: Context, config: Config) {
1115
1385
  try {
1116
1386
  const guild = await session.bot.getGuild(targetGroupId)
1117
1387
  groupName = guild.name || groupName
1118
- } catch (error) {
1388
+ } catch (error: any) {
1119
1389
  // 无法获取群名称时使用默认值
1120
1390
  }
1121
1391
 
@@ -1388,7 +1658,7 @@ export function apply(ctx: Context, config: Config) {
1388
1658
  'gv.同意', 'gverify.同意', 'group-verify.同意',
1389
1659
  'gva'
1390
1660
  )
1391
- .action(async ({ session }, userId) => {
1661
+ .action(async ({ session }: any, userId: any) => {
1392
1662
  // 权限检查
1393
1663
  const [hasPermission, errorMsg] = await checkPermission(session)
1394
1664
  if (!hasPermission) {
@@ -1421,7 +1691,7 @@ export function apply(ctx: Context, config: Config) {
1421
1691
  try {
1422
1692
  await session.bot.handleGuildMemberRequest(request.requestId, true)
1423
1693
  approvedCount++
1424
- } catch (error) {
1694
+ } catch (error: any) {
1425
1695
  logger.warn(`处理申请 ${request.id} 时出错:`, error)
1426
1696
  }
1427
1697
  } else {
@@ -1445,12 +1715,14 @@ export function apply(ctx: Context, config: Config) {
1445
1715
  return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
1446
1716
  }
1447
1717
  try {
1448
- await session.bot.handleGuildMemberRequest(request.requestId, true)
1449
- // 清除该用户的所有待审核记录
1450
- await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1451
1718
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1452
- return `已同意用户 ${displayName} 的加群申请`
1453
- } catch (error) {
1719
+ const reply = `已同意用户 ${displayName} 的加群申请`
1720
+ // 先返回提示消息,再异步执行批准请求
1721
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
1722
+ // 清除该用户的所有待审核记录(异步也可以,顺序无关)
1723
+ await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1724
+ return reply
1725
+ } catch (error: any) {
1454
1726
  return `处理申请时出错: ${error.message}`
1455
1727
  }
1456
1728
  }
@@ -1471,12 +1743,12 @@ export function apply(ctx: Context, config: Config) {
1471
1743
  return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
1472
1744
  }
1473
1745
  try {
1474
- await session.bot.handleGuildMemberRequest(request.requestId, true)
1475
- // 删除该用户的所有记录
1476
- await ctx.database.remove('group_verification_pending', { groupId, userId })
1477
1746
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1478
- return `已同意用户 ${displayName} 的加群申请`
1479
- } catch (error) {
1747
+ const reply = `已同意用户 ${displayName} 的加群申请`
1748
+ session.bot.handleGuildMemberRequest(request.requestId, true).catch((e:any) => logger.warn('自动同意失败', e))
1749
+ await ctx.database.remove('group_verification_pending', { groupId, userId })
1750
+ return reply
1751
+ } catch (error: any) {
1480
1752
  return `处理申请时出错: ${error.message}`
1481
1753
  }
1482
1754
  })
@@ -1489,7 +1761,7 @@ export function apply(ctx: Context, config: Config) {
1489
1761
  'gv.rej', 'gverify.rej', 'group-verify.rej',
1490
1762
  'gvr'
1491
1763
  )
1492
- .action(async ({ session }, userId) => {
1764
+ .action(async ({ session }: any, userId: any) => {
1493
1765
  // 权限检查
1494
1766
  const [hasPermission, errorMsg] = await checkPermission(session)
1495
1767
  if (!hasPermission) {
@@ -1547,7 +1819,7 @@ export function apply(ctx: Context, config: Config) {
1547
1819
  await updateStats(ctx, groupId, 'rejected')
1548
1820
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1549
1821
  return `已拒绝用户 ${displayName} 的加群申请`
1550
- } catch (error) {
1822
+ } catch (error: any) {
1551
1823
  return `处理申请时出错: ${error.message}`
1552
1824
  }
1553
1825
  }
@@ -1574,7 +1846,7 @@ export function apply(ctx: Context, config: Config) {
1574
1846
  await updateStats(ctx, groupId, 'rejected')
1575
1847
  const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1576
1848
  return `已拒绝用户 ${displayName} 的加群申请`
1577
- } catch (error) {
1849
+ } catch (error: any) {
1578
1850
  return `处理申请时出错: ${error.message}`
1579
1851
  }
1580
1852
  })
@@ -1586,7 +1858,7 @@ export function apply(ctx: Context, config: Config) {
1586
1858
  'gv.统计', 'gverify.统计', 'group-verify.统计',
1587
1859
  'gvs'
1588
1860
  )
1589
- .action(async ({ session }, target) => {
1861
+ .action(async ({ session }: any, target: any) => {
1590
1862
  // 参数验证:只能是群号、all、total或空
1591
1863
  const validTargets = ['all', 'total']
1592
1864
  const isGroupId = target && /^\d+$/.test(target)
@@ -1637,7 +1909,7 @@ export function apply(ctx: Context, config: Config) {
1637
1909
  'gv.待处理', 'gverify.待处理', 'group-verify.待处理',
1638
1910
  'gvp'
1639
1911
  )
1640
- .action(async ({ session }) => {
1912
+ .action(async ({ session }: any) => {
1641
1913
  if (!session.guildId) {
1642
1914
  return '请在群聊中使用此命令'
1643
1915
  }
@@ -1662,6 +1934,18 @@ export function apply(ctx: Context, config: Config) {
1662
1934
  return result
1663
1935
  })
1664
1936
 
1937
+ // Subcommand: blacklist management
1938
+ groupVerify
1939
+ .subcommand('.blacklist [args:text]', '管理加群黑名单')
1940
+ .alias(
1941
+ 'gvb',
1942
+ 'gv.blacklist', 'gverify.blacklist', 'group-verify.blacklist',
1943
+ 'gv.黑名单', 'gverify.黑名单', 'group-verify.黑名单'
1944
+ )
1945
+ .action(async ({ session }: any, args: any) => {
1946
+ return await processBlacklistCommand(ctx, session, args || '', config)
1947
+ })
1948
+
1665
1949
  // Subcommand: help information
1666
1950
  groupVerify
1667
1951
  .subcommand('.help', '显示帮助信息')
@@ -1718,6 +2002,19 @@ export function apply(ctx: Context, config: Config) {
1718
2002
  gv.cfg -m 2 -t 80
1719
2003
  gv.cfg -nomsg
1720
2004
 
2005
+ 黑名单命令 (.blacklist / gvb):
2006
+ a id [reason] [group] 添加黑名单,可指定原因和群号;group为all表示全局
2007
+ r id [group] 删除条目;group 为 all 时移除全局黑名单
2008
+ l [group] 查询群黑名单;传入 all 时查看全局黑名单
2009
+ i id [group] 查询账号黑名单;不带参数时查询本群与全局,
2010
+ 指定群号查询该群与全局,传入 all 则列出所有群及全局(需Koishi 3级)
2011
+ 使用示例:
2012
+ gvb a 12345 作弊记录
2013
+ gvb r 12345 67890
2014
+ gvb l
2015
+ gvb l all
2016
+ gvb i 12345
2017
+
1721
2018
  快捷命令:
1722
2019
  gvc - 配置命令快捷方式
1723
2020
  gva - 同意申请快捷命令