koishi-plugin-group-verification 1.0.26 → 1.0.28

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
@@ -26,6 +26,7 @@ export interface GroupVerificationStats {
26
26
  autoApproved: number;
27
27
  manuallyApproved: number;
28
28
  rejected: number;
29
+ totalJoined: number;
29
30
  lastUpdated: string | Date;
30
31
  }
31
32
  export interface PendingVerification {
@@ -34,6 +35,7 @@ export interface PendingVerification {
34
35
  userId: string;
35
36
  userName: string;
36
37
  requestMessage: string;
38
+ requestId?: string;
37
39
  applyTime: string | Date;
38
40
  }
39
41
  export interface Config {
@@ -91,6 +93,7 @@ export declare function mergeReminder(existingConfig: any | null, cleanedOptions
91
93
  };
92
94
  export declare function syncTotalStats(ctx: Context): Promise<void>;
93
95
  export declare function updateStats(ctx: Context, groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected'): Promise<void>;
96
+ export declare function incrementTotal(ctx: Context, groupId: string): Promise<void>;
94
97
  export declare function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]>;
95
98
  export declare function handleGuildMemberRequestEvent(ctx: Context, session: any): Promise<void>;
96
99
  export declare function __getAutoQueue(): Map<string, Set<string>>;
package/lib/index.js CHANGED
@@ -26,6 +26,7 @@ __export(src_exports, {
26
26
  checkPermission: () => checkPermission,
27
27
  handleFailedVerification: () => handleFailedVerification,
28
28
  handleGuildMemberRequestEvent: () => handleGuildMemberRequestEvent,
29
+ incrementTotal: () => incrementTotal,
29
30
  inject: () => inject,
30
31
  mergeReminder: () => mergeReminder,
31
32
  name: () => name,
@@ -207,13 +208,15 @@ async function syncTotalStats(ctx) {
207
208
  const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0);
208
209
  const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0);
209
210
  const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0);
211
+ const totalJoined = allStats.reduce((sum, stat) => sum + (stat.totalJoined || 0), 0);
210
212
  await ctx.database.set("group_verification_stats", { groupId: "TOTAL" }, {
211
213
  autoApproved: totalAutoApproved,
212
214
  manuallyApproved: totalManuallyApproved,
213
215
  rejected: totalRejected,
216
+ totalJoined,
214
217
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
215
218
  });
216
- logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
219
+ logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}, 入群${totalJoined}`);
217
220
  }
218
221
  } catch (error) {
219
222
  logger.error("同步总计统计时出错:", error);
@@ -234,12 +237,34 @@ async function updateStats(ctx, groupId, action) {
234
237
  autoApproved: action === "autoApproved" ? 1 : 0,
235
238
  manuallyApproved: action === "manuallyApproved" ? 1 : 0,
236
239
  rejected: action === "rejected" ? 1 : 0,
240
+ totalJoined: 0,
237
241
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
238
242
  });
239
243
  }
240
244
  await syncTotalStats(ctx);
241
245
  }
242
246
  __name(updateStats, "updateStats");
247
+ async function incrementTotal(ctx, groupId) {
248
+ const existingStats = await ctx.database.get("group_verification_stats", { groupId });
249
+ if (existingStats.length > 0) {
250
+ const stats = existingStats[0];
251
+ await ctx.database.set("group_verification_stats", { id: stats.id }, {
252
+ totalJoined: (stats.totalJoined || 0) + 1,
253
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
254
+ });
255
+ } else {
256
+ await ctx.database.create("group_verification_stats", {
257
+ groupId,
258
+ autoApproved: 0,
259
+ manuallyApproved: 0,
260
+ rejected: 0,
261
+ totalJoined: 1,
262
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
263
+ });
264
+ }
265
+ await syncTotalStats(ctx);
266
+ }
267
+ __name(incrementTotal, "incrementTotal");
243
268
  async function checkPermission(session, targetGroupId) {
244
269
  const groupId = targetGroupId || session.guildId;
245
270
  if (!groupId) {
@@ -443,14 +468,20 @@ async function verifyApplication(config, message, session) {
443
468
  requiredThreshold = "null";
444
469
  break;
445
470
  case 1:
446
- isValid = matchedCount >= (config.reviewParameters || 1);
447
- requiredThreshold = `${config.reviewParameters || 1}`;
471
+ {
472
+ const thresholdNum = config.reviewParameters !== void 0 && config.reviewParameters !== null ? config.reviewParameters : 0;
473
+ isValid = matchedCount >= thresholdNum;
474
+ requiredThreshold = `${thresholdNum}`;
475
+ }
448
476
  break;
449
477
  case 2:
450
- const ratio = matchedCount / config.keywords.length;
451
- const requiredRatio = (config.reviewParameters || 100) / 100;
452
- isValid = ratio >= requiredRatio;
453
- requiredThreshold = `${config.reviewParameters || 100}%`;
478
+ {
479
+ const thresholdPct = config.reviewParameters !== void 0 && config.reviewParameters !== null ? config.reviewParameters : 100;
480
+ const ratio = matchedCount / config.keywords.length;
481
+ isValid = ratio >= thresholdPct / 100;
482
+ const needed = Math.ceil(config.keywords.length * thresholdPct / 100);
483
+ requiredThreshold = `${needed}`;
484
+ }
454
485
  break;
455
486
  case 3:
456
487
  isValid = false;
@@ -485,11 +516,14 @@ async function handleFailedVerification(ctx, session, config, matchedCount, requ
485
516
  groupName = guild.name || groupName;
486
517
  } catch (error) {
487
518
  }
519
+ const requestId = session.event?.requestId || session.messageId || "";
520
+ await ctx.database.remove("group_verification_pending", { groupId: guildId, userId });
488
521
  await ctx.database.create("group_verification_pending", {
489
522
  groupId: guildId,
490
523
  userId,
491
524
  userName: username,
492
525
  requestMessage: message,
526
+ requestId,
493
527
  applyTime: (/* @__PURE__ */ new Date()).toISOString()
494
528
  });
495
529
  if (!config.reminderEnabled || !config.reminderMessage || config.reminderMessage === "") {
@@ -507,10 +541,18 @@ async function handleFailedVerification(ctx, session, config, matchedCount, requ
507
541
  await session.bot.broadcast([target], reminderMsg);
508
542
  } catch (err) {
509
543
  logger.warn("bot.broadcast failed, fallback to ctx.broadcast", err);
510
- await ctx.broadcast([target], reminderMsg);
544
+ if (typeof ctx.broadcast === "function") {
545
+ await ctx.broadcast([target], reminderMsg);
546
+ } else {
547
+ logger.info("ctx.broadcast 不可用,跳过发送");
548
+ }
511
549
  }
512
550
  } else {
513
- await ctx.broadcast([target], reminderMsg);
551
+ if (typeof ctx.broadcast === "function") {
552
+ await ctx.broadcast([target], reminderMsg);
553
+ } else {
554
+ logger.info("ctx.broadcast 不可用,跳过发送");
555
+ }
514
556
  }
515
557
  }
516
558
  __name(handleFailedVerification, "handleFailedVerification");
@@ -543,6 +585,7 @@ function apply(ctx, config) {
543
585
  autoApproved: "integer",
544
586
  manuallyApproved: "integer",
545
587
  rejected: "integer",
588
+ totalJoined: "integer",
546
589
  // store as string (ISO timestamp) to preserve full date+time;
547
590
  // Koishi `date` type truncates to day which leads to 00:00:00.
548
591
  lastUpdated: "string"
@@ -556,6 +599,8 @@ function apply(ctx, config) {
556
599
  userId: "string",
557
600
  userName: "string",
558
601
  requestMessage: "string",
602
+ // store the raw requestId if provided by OneBot event; used for approving/rejecting
603
+ requestId: "string",
559
604
  // record full timestamp as string to keep time component
560
605
  applyTime: "string"
561
606
  }, {
@@ -569,6 +614,7 @@ function apply(ctx, config) {
569
614
  ctx.on("guild-member-added", async (session) => {
570
615
  const groupId = session.guildId;
571
616
  const userId = session.userId;
617
+ await incrementTotal(ctx, groupId);
572
618
  const set = autoQueue.get(groupId);
573
619
  if (set && set.has(userId)) {
574
620
  await updateStats(ctx, groupId, "autoApproved");
@@ -582,8 +628,10 @@ function apply(ctx, config) {
582
628
  });
583
629
  if (pendingRecords.length > 0) {
584
630
  await updateStats(ctx, groupId, "autoApproved");
585
- await ctx.database.remove("group_verification_pending", { id: pendingRecords[0].id });
586
- logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`);
631
+ for (const rec of pendingRecords) {
632
+ await ctx.database.remove("group_verification_pending", { id: rec.id });
633
+ }
634
+ logger.info(`用户 ${userId} 通过验证加入群 ${groupId},已清理 ${pendingRecords.length} 条待审核记录,统计已更新`);
587
635
  } else {
588
636
  await updateStats(ctx, groupId, "manuallyApproved");
589
637
  logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`);
@@ -898,7 +946,7 @@ ${debugInfo}`];
898
946
  const methodMap = { 0: "全部同意", 1: "按数量", 2: "按比例", 3: "全部拒绝" };
899
947
  feedbackMessage += `审核方式: ${methodMap[reviewMethod]}
900
948
  `;
901
- if (reviewParameters !== 0) {
949
+ if (reviewMethod === 1 || reviewMethod === 2) {
902
950
  const thresholdDisplay = reviewMethod === 2 ? `${reviewParameters}%` : reviewParameters.toString();
903
951
  feedbackMessage += `阈值: ${thresholdDisplay}
904
952
  `;
@@ -958,31 +1006,44 @@ ${debugInfo}`];
958
1006
  return "当前无待审核的加群申请";
959
1007
  }
960
1008
  let approvedCount = 0;
1009
+ let skippedCount = 0;
961
1010
  for (const request2 of pendingRequests2) {
962
- try {
963
- await ctx.database.remove("group_verification_pending", { id: request2.id });
964
- approvedCount++;
965
- } catch (error) {
966
- logger.warn(`处理申请 ${request2.id} 时出错:`, error);
1011
+ if (request2.requestId) {
1012
+ try {
1013
+ await session.bot.handleGuildMemberRequest(request2.requestId, true);
1014
+ approvedCount++;
1015
+ } catch (error) {
1016
+ logger.warn(`处理申请 ${request2.id} 时出错:`, error);
1017
+ }
1018
+ } else {
1019
+ skippedCount++;
967
1020
  }
1021
+ await ctx.database.remove("group_verification_pending", { id: request2.id });
968
1022
  }
969
- return `已处理 ${approvedCount} 个加群申请`;
1023
+ let msg = `已处理 ${approvedCount} 个加群申请`;
1024
+ if (skippedCount) msg += `,${skippedCount} 个因缺少 requestId 未处理`;
1025
+ return msg;
970
1026
  } else {
971
- const recentRequest = await ctx.database.get("group_verification_pending", { groupId }, ["id", "userId", "userName"]);
972
- if (recentRequest.length === 0) {
1027
+ let pending = await ctx.database.get("group_verification_pending", { groupId }, ["id", "userId", "userName", "applyTime", "requestId"]);
1028
+ if (pending.length === 0) {
973
1029
  return "当前无待审核的加群申请";
974
1030
  }
975
- const request2 = recentRequest[0];
1031
+ pending.sort((a, b) => String(b.applyTime).localeCompare(String(a.applyTime)));
1032
+ const request2 = pending[0];
1033
+ if (!request2.requestId) {
1034
+ return `用户 ${request2.userId} 的申请缺少 requestId,无法自动同意`;
1035
+ }
976
1036
  try {
977
- await session.bot.handleGuildMemberRequest(request2.userId, true);
978
- await ctx.database.remove("group_verification_pending", { id: request2.id });
979
- return `已同意用户 ${request2.userName}(${request2.userId}) 的加群申请`;
1037
+ await session.bot.handleGuildMemberRequest(request2.requestId, true);
1038
+ await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1039
+ const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
1040
+ return `已同意用户 ${displayName} 的加群申请`;
980
1041
  } catch (error) {
981
1042
  return `处理申请时出错: ${error.message}`;
982
1043
  }
983
1044
  }
984
1045
  }
985
- const pendingRequests = await ctx.database.get("group_verification_pending", {
1046
+ let pendingRequests = await ctx.database.get("group_verification_pending", {
986
1047
  groupId,
987
1048
  userId
988
1049
  });
@@ -990,10 +1051,14 @@ ${debugInfo}`];
990
1051
  return `未找到用户 ${userId} 的待审核申请`;
991
1052
  }
992
1053
  const request = pendingRequests[0];
1054
+ if (!request.requestId) {
1055
+ return `用户 ${userId} 的申请缺少 requestId,无法自动同意`;
1056
+ }
993
1057
  try {
994
- await session.bot.handleGuildMemberRequest(request.userId, true);
995
- await ctx.database.remove("group_verification_pending", { id: request.id });
996
- return `已同意用户 ${request.userName}(${userId}) 的加群申请`;
1058
+ await session.bot.handleGuildMemberRequest(request.requestId, true);
1059
+ await ctx.database.remove("group_verification_pending", { groupId, userId });
1060
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
1061
+ return `已同意用户 ${displayName} 的加群申请`;
997
1062
  } catch (error) {
998
1063
  return `处理申请时出错: ${error.message}`;
999
1064
  }
@@ -1022,26 +1087,40 @@ ${debugInfo}`];
1022
1087
  return "当前无待审核的加群申请";
1023
1088
  }
1024
1089
  let rejectedCount = 0;
1090
+ let skippedCount = 0;
1025
1091
  for (const request2 of pendingRequests2) {
1026
- try {
1027
- await ctx.database.remove("group_verification_pending", { id: request2.id });
1028
- await updateStats(ctx, groupId, "rejected");
1029
- rejectedCount++;
1030
- } catch (error) {
1031
- logger.warn(`处理申请 ${request2.id} 时出错:`, error);
1092
+ if (request2.requestId) {
1093
+ try {
1094
+ await session.bot.handleGuildMemberRequest(request2.requestId, false);
1095
+ rejectedCount++;
1096
+ } catch (error) {
1097
+ logger.warn(`处理申请 ${request2.id} 时出错:`, error);
1098
+ }
1099
+ } else {
1100
+ skippedCount++;
1032
1101
  }
1102
+ await ctx.database.remove("group_verification_pending", { id: request2.id });
1103
+ await updateStats(ctx, groupId, "rejected");
1033
1104
  }
1034
- return `已拒绝 ${rejectedCount} 个加群申请`;
1105
+ let msg = `已拒绝 ${rejectedCount} 个加群申请`;
1106
+ if (skippedCount) msg += `,${skippedCount} 个因缺少 requestId 未处理`;
1107
+ return msg;
1035
1108
  } else {
1036
- const recentRequest = await ctx.database.get("group_verification_pending", { groupId }, ["id", "userId", "userName"]);
1037
- if (recentRequest.length === 0) {
1109
+ let pending = await ctx.database.get("group_verification_pending", { groupId }, ["id", "userId", "userName", "applyTime", "requestId"]);
1110
+ if (pending.length === 0) {
1038
1111
  return "当前无待审核的加群申请";
1039
1112
  }
1040
- const request2 = recentRequest[0];
1113
+ pending.sort((a, b) => String(b.applyTime).localeCompare(String(a.applyTime)));
1114
+ const request2 = pending[0];
1115
+ if (!request2.requestId) {
1116
+ return `用户 ${request2.userId} 的申请缺少 requestId,无法自动拒绝`;
1117
+ }
1041
1118
  try {
1042
- await ctx.database.remove("group_verification_pending", { id: request2.id });
1119
+ await session.bot.handleGuildMemberRequest(request2.requestId, false);
1120
+ await ctx.database.remove("group_verification_pending", { groupId, userId: request2.userId });
1043
1121
  await updateStats(ctx, groupId, "rejected");
1044
- return `已拒绝用户 ${request2.userName}(${request2.userId}) 的加群申请`;
1122
+ const displayName = request2.userName && request2.userName !== request2.userId ? `${request2.userName}(${request2.userId})` : request2.userId;
1123
+ return `已拒绝用户 ${displayName} 的加群申请`;
1045
1124
  } catch (error) {
1046
1125
  return `处理申请时出错: ${error.message}`;
1047
1126
  }
@@ -1055,10 +1134,15 @@ ${debugInfo}`];
1055
1134
  return `未找到用户 ${userId} 的待审核申请`;
1056
1135
  }
1057
1136
  const request = pendingRequests[0];
1137
+ if (!request.requestId) {
1138
+ return `用户 ${userId} 的申请缺少 requestId,无法自动拒绝`;
1139
+ }
1058
1140
  try {
1059
- await ctx.database.remove("group_verification_pending", { id: request.id });
1141
+ await session.bot.handleGuildMemberRequest(request.requestId, false);
1142
+ await ctx.database.remove("group_verification_pending", { groupId, userId });
1060
1143
  await updateStats(ctx, groupId, "rejected");
1061
- return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`;
1144
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId;
1145
+ return `已拒绝用户 ${displayName} 的加群申请`;
1062
1146
  } catch (error) {
1063
1147
  return `处理申请时出错: ${error.message}`;
1064
1148
  }
@@ -1203,6 +1287,7 @@ ${debugInfo}`];
1203
1287
  for (const st of stats) {
1204
1288
  const updates = {};
1205
1289
  if (st.lastUpdated instanceof Date) updates.lastUpdated = st.lastUpdated.toISOString();
1290
+ if (st.totalJoined === void 0) updates.totalJoined = 0;
1206
1291
  if (Object.keys(updates).length) {
1207
1292
  await ctx.database.set("group_verification_stats", { id: st.id }, updates);
1208
1293
  }
@@ -1211,6 +1296,7 @@ ${debugInfo}`];
1211
1296
  for (const p of pendings) {
1212
1297
  const updates = {};
1213
1298
  if (p.applyTime instanceof Date) updates.applyTime = p.applyTime.toISOString();
1299
+ if (p.requestId === void 0) updates.requestId = "";
1214
1300
  if (Object.keys(updates).length) {
1215
1301
  await ctx.database.set("group_verification_pending", { id: p.id }, updates);
1216
1302
  }
@@ -1272,6 +1358,7 @@ __name(apply, "apply");
1272
1358
  checkPermission,
1273
1359
  handleFailedVerification,
1274
1360
  handleGuildMemberRequestEvent,
1361
+ incrementTotal,
1275
1362
  inject,
1276
1363
  mergeReminder,
1277
1364
  name,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-verification",
3
3
  "description": "[WIP] Koishi 群组加群验证插件,支持多关键词匹配审核、多种审核方式和详细统计功能(开发中)",
4
- "version": "1.0.26",
4
+ "version": "1.0.28",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - **多关键词匹配**:支持多个关键词的灵活配置
8
8
  - **四种审核方式**:全部同意、按数量同意、按比例同意、全部拒绝
9
9
  - **完善的权限控制**:群主/管理员权限或 Koishi 三级以上权限
10
- - **详细的统计功能**:自动记录审核统计和手动入群统计
10
+ - **详细的统计功能**:自动记录审核统计和手动入群统计(含自动通过、手动通过、拒绝以及总入群人数)
11
11
  - **灵活的消息配置**:支持自定义提醒消息和禁用功能
12
12
  - **友好的命令系统**:丰富的别名和快捷命令
13
13
 
@@ -54,6 +54,12 @@ group-verify.config -i 123456789 关键词1,关键词2 -m 1 -t 1
54
54
  ```
55
55
 
56
56
  ### 审核命令
57
+
58
+ > **命令增强**
59
+ > - 未提供参数时 `gva`/`gvr` 会处理最近一条申请,`all` 可以批量处理。
60
+ > - 如果申请记录缺少 requestId,则无法通过机器人接口处理,会提示管理员请在客户端手动操作。
61
+ > - 输出结果会智能展示用户名和ID,避免出现 "12345(12345)" 这样的重复显示。
62
+
57
63
  ```
58
64
  # 同意申请(处理最近一个)
59
65
  group-verify.approve
@@ -103,6 +109,10 @@ group-verify.stats total
103
109
  - 方式2(按比例):需要达到的百分比(1-100)
104
110
 
105
111
  ### 提醒消息配置
112
+
113
+ 提醒消息里的 `{threshold}` 在“按比例审核”模式下会显示为需要匹配的关键词数量
114
+ (而非百分比),例如三条关键词、阈值60%时显示为“1/2”。
115
+
106
116
  - `-msg "消息内容"` - 设置自定义提醒消息
107
117
  - 消息内部可包含逗号:只要所有逗号前后都没有空格,它们将被视为同一消息内容。
108
118
  - 若逗号之后仍需写关键词,请使用引号包裹整个消息或在逗号后加空格以分隔。
@@ -144,10 +154,12 @@ group-verify.stats total
144
154
  ## 📊 统计功能
145
155
 
146
156
  插件会自动记录以下统计信息:
147
- - **自动批准**:通过关键词验证自动入群的用户数
148
- - **手动批准**:管理员手动同意的用户数
149
- - **拒绝**:被拒绝的申请数
150
- - **手动入群**:管理员直接邀请入群的用户数
157
+ - **自动批准**:通过关键词验证自动入群的用户数(包括自动审批与手动 gva 同意)
158
+ - **手动批准**:通过 gva 指令手动同意的用户数
159
+ - **拒绝**:被拒绝的申请数(包括自动拒绝与 gvr 指令)
160
+ - **总入群人数**:无论通过哪种方式,只要检测到成员加入即增加
161
+
162
+ > ⚠️ 注意:绕过机器人手动在客户端同意/拒绝的操作不会计入自动批准/拒绝统计,但仍会反映在总入群人数中。
151
163
  ### ⚠️ OneBot/QQ 适配器注意
152
164
  默认情况下插件会使用 `ctx.broadcast` 发送提醒消息,
153
165
  为了兼容 OneBot、QQ 等需要同时指定频道和群号的协议,
package/src/index.ts CHANGED
@@ -36,6 +36,8 @@ export interface GroupVerificationStats {
36
36
  autoApproved: number
37
37
  manuallyApproved: number
38
38
  rejected: number
39
+ // 新增:总入群人数(不论方式,只要检测到成员加入则增加)
40
+ totalJoined: number
39
41
  lastUpdated: string | Date
40
42
  }
41
43
 
@@ -46,6 +48,8 @@ export interface PendingVerification {
46
48
  userId: string
47
49
  userName: string
48
50
  requestMessage: string
51
+ // raw OneBot requestId (may be empty string)
52
+ requestId?: string
49
53
  applyTime: string | Date
50
54
  }
51
55
 
@@ -304,16 +308,18 @@ export async function syncTotalStats(ctx: Context) {
304
308
  const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0)
305
309
  const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0)
306
310
  const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0)
311
+ const totalJoined = allStats.reduce((sum, stat) => sum + (stat.totalJoined || 0), 0)
307
312
 
308
313
  // 更新总计行
309
314
  await ctx.database.set('group_verification_stats', { groupId: 'TOTAL' }, {
310
315
  autoApproved: totalAutoApproved,
311
316
  manuallyApproved: totalManuallyApproved,
312
317
  rejected: totalRejected,
318
+ totalJoined,
313
319
  lastUpdated: new Date().toISOString()
314
320
  })
315
321
 
316
- logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)
322
+ logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}, 入群${totalJoined}`)
317
323
  }
318
324
  } catch (error) {
319
325
  logger.error('同步总计统计时出错:', error)
@@ -336,6 +342,7 @@ export async function updateStats(ctx: Context, groupId: string, action: 'autoAp
336
342
  autoApproved: action === 'autoApproved' ? 1 : 0,
337
343
  manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
338
344
  rejected: action === 'rejected' ? 1 : 0,
345
+ totalJoined: 0,
339
346
  lastUpdated: new Date().toISOString()
340
347
  })
341
348
  }
@@ -344,6 +351,28 @@ export async function updateStats(ctx: Context, groupId: string, action: 'autoAp
344
351
  await syncTotalStats(ctx)
345
352
  }
346
353
 
354
+ // 提取成独立函数:增加总入群计数,供事件统一调用
355
+ export async function incrementTotal(ctx: Context, groupId: string) {
356
+ const existingStats = await ctx.database.get('group_verification_stats', { groupId })
357
+ if (existingStats.length > 0) {
358
+ const stats = existingStats[0]
359
+ await ctx.database.set('group_verification_stats', { id: stats.id }, {
360
+ totalJoined: (stats.totalJoined || 0) + 1,
361
+ lastUpdated: new Date().toISOString()
362
+ })
363
+ } else {
364
+ await ctx.database.create('group_verification_stats', {
365
+ groupId,
366
+ autoApproved: 0,
367
+ manuallyApproved: 0,
368
+ rejected: 0,
369
+ totalJoined: 1,
370
+ lastUpdated: new Date().toISOString()
371
+ })
372
+ }
373
+ await syncTotalStats(ctx)
374
+ }
375
+
347
376
  // 权限检查函数(也可用于命令)
348
377
  export async function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]> {
349
378
  const groupId = targetGroupId || session.guildId
@@ -581,14 +610,26 @@ export async function verifyApplication(config: GroupVerificationConfig, message
581
610
  requiredThreshold = 'null'
582
611
  break
583
612
  case 1: // 按数量同意
584
- isValid = matchedCount >= (config.reviewParameters || 1)
585
- requiredThreshold = `${config.reviewParameters || 1}`
613
+ {
614
+ // threshold may legitimately be 0 (表示全部同意)
615
+ const thresholdNum = config.reviewParameters !== undefined && config.reviewParameters !== null
616
+ ? config.reviewParameters
617
+ : 0
618
+ isValid = matchedCount >= thresholdNum
619
+ requiredThreshold = `${thresholdNum}`
620
+ }
586
621
  break
587
622
  case 2: // 按比例同意
588
- const ratio = matchedCount / config.keywords.length
589
- const requiredRatio = (config.reviewParameters || 100) / 100
590
- isValid = ratio >= requiredRatio
591
- requiredThreshold = `${config.reviewParameters || 100}%`
623
+ {
624
+ const thresholdPct = config.reviewParameters !== undefined && config.reviewParameters !== null
625
+ ? config.reviewParameters
626
+ : 100
627
+ const ratio = matchedCount / config.keywords.length
628
+ isValid = ratio >= thresholdPct / 100
629
+ // 显示阈值为需要匹配的关键词数量,避免 "1/60%" 之类混淆
630
+ const needed = Math.ceil(config.keywords.length * thresholdPct / 100)
631
+ requiredThreshold = `${needed}`
632
+ }
592
633
  break
593
634
  case 3: // 全部拒绝
594
635
  isValid = false
@@ -637,12 +678,19 @@ export async function handleFailedVerification(
637
678
  // 无法获取群名称时使用默认值
638
679
  }
639
680
 
681
+ // extract requestId if available (OneBot event attaches it)
682
+ const requestId = ((session.event as any)?.requestId) || session.messageId || ''
683
+
684
+ // 删除同一用户在该群之前的所有待审核记录,保留最新一个
685
+ await ctx.database.remove('group_verification_pending', { groupId: guildId, userId })
686
+
640
687
  // 将申请加入待审核列表
641
688
  await ctx.database.create('group_verification_pending', {
642
689
  groupId: guildId,
643
690
  userId: userId,
644
691
  userName: username,
645
692
  requestMessage: message,
693
+ requestId,
646
694
  applyTime: new Date().toISOString()
647
695
  })
648
696
  // 如果提醒消息被禁用,直接返回
@@ -676,10 +724,18 @@ export async function handleFailedVerification(
676
724
  } catch (err) {
677
725
  // fallback to ctx.broadcast if bot.broadcast fails for some reason
678
726
  logger.warn('bot.broadcast failed, fallback to ctx.broadcast', err)
679
- await (ctx.broadcast as any)([target], reminderMsg)
727
+ if (typeof (ctx.broadcast) === 'function') {
728
+ await (ctx.broadcast as any)([target], reminderMsg)
729
+ } else {
730
+ logger.info('ctx.broadcast 不可用,跳过发送')
731
+ }
680
732
  }
681
733
  } else {
682
- await (ctx.broadcast as any)([target], reminderMsg)
734
+ if (typeof (ctx.broadcast) === 'function') {
735
+ await (ctx.broadcast as any)([target], reminderMsg)
736
+ } else {
737
+ logger.info('ctx.broadcast 不可用,跳过发送')
738
+ }
683
739
  }
684
740
  }
685
741
 
@@ -720,6 +776,7 @@ export function apply(ctx: Context, config: Config) {
720
776
  autoApproved: 'integer',
721
777
  manuallyApproved: 'integer',
722
778
  rejected: 'integer',
779
+ totalJoined: 'integer',
723
780
  // store as string (ISO timestamp) to preserve full date+time;
724
781
  // Koishi `date` type truncates to day which leads to 00:00:00.
725
782
  lastUpdated: 'string'
@@ -728,15 +785,18 @@ export function apply(ctx: Context, config: Config) {
728
785
  autoInc: true
729
786
  })
730
787
 
788
+ // cast schema to any to avoid type conflicts when adding new fields
731
789
  ctx.model.extend('group_verification_pending', {
732
790
  id: 'unsigned',
733
791
  groupId: 'string',
734
792
  userId: 'string',
735
793
  userName: 'string',
736
794
  requestMessage: 'string',
795
+ // store the raw requestId if provided by OneBot event; used for approving/rejecting
796
+ requestId: 'string',
737
797
  // record full timestamp as string to keep time component
738
798
  applyTime: 'string'
739
- }, {
799
+ } as any, {
740
800
  primary: 'id',
741
801
  autoInc: true
742
802
  })
@@ -754,6 +814,9 @@ export function apply(ctx: Context, config: Config) {
754
814
  const groupId = session.guildId
755
815
  const userId = session.userId
756
816
 
817
+ // 无论什么情况只要检测到加入就累加总入群
818
+ await incrementTotal(ctx, groupId)
819
+
757
820
  // 先检查 autoQueue
758
821
  const set = autoQueue.get(groupId)
759
822
  if (set && set.has(userId)) {
@@ -772,9 +835,11 @@ export function apply(ctx: Context, config: Config) {
772
835
  if (pendingRecords.length > 0) {
773
836
  // 通过验证的用户入群,更新统计
774
837
  await updateStats(ctx, groupId, 'autoApproved')
775
- // 清除待审核记录
776
- await ctx.database.remove('group_verification_pending', { id: pendingRecords[0].id })
777
- logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`)
838
+ // 清除所有该用户的待审核记录
839
+ for (const rec of pendingRecords) {
840
+ await ctx.database.remove('group_verification_pending', { id: rec.id })
841
+ }
842
+ logger.info(`用户 ${userId} 通过验证加入群 ${groupId},已清理 ${pendingRecords.length} 条待审核记录,统计已更新`)
778
843
  } else {
779
844
  // 手动邀请入群,记录到手动批准统计
780
845
  await updateStats(ctx, groupId, 'manuallyApproved')
@@ -1212,7 +1277,8 @@ export function apply(ctx: Context, config: Config) {
1212
1277
  const methodMap = {0: '全部同意', 1: '按数量', 2: '按比例', 3: '全部拒绝'}
1213
1278
  feedbackMessage += `审核方式: ${methodMap[reviewMethod]}\n`
1214
1279
 
1215
- if (reviewParameters !== 0) {
1280
+ // 显示阈值(即便为 0
1281
+ if (reviewMethod === 1 || reviewMethod === 2) {
1216
1282
  const thresholdDisplay = reviewMethod === 2 ? `${reviewParameters}%` : reviewParameters.toString()
1217
1283
  feedbackMessage += `阈值: ${thresholdDisplay}\n`
1218
1284
  }
@@ -1275,7 +1341,7 @@ export function apply(ctx: Context, config: Config) {
1275
1341
  if (configs.length > 0 && configs[0].reviewMethod === 3) {
1276
1342
  return '该群已设为全部拒绝,无法手动同意任何申请'
1277
1343
  }
1278
- // 处理默认情况和all情况
1344
+ // 处理默认情况和 all 情况
1279
1345
  if (!userId || userId.toLowerCase() === 'all') {
1280
1346
  if (userId?.toLowerCase() === 'all') {
1281
1347
  // 处理所有待审核申请
@@ -1285,31 +1351,41 @@ export function apply(ctx: Context, config: Config) {
1285
1351
  }
1286
1352
 
1287
1353
  let approvedCount = 0
1354
+ let skippedCount = 0
1288
1355
  for (const request of pendingRequests) {
1289
- try {
1290
- // TODO: 需要获取实际的requestId来进行审批
1291
- // await session.bot.handleGuildMemberRequest(request.requestId, true)
1292
- await ctx.database.remove('group_verification_pending', { id: request.id })
1293
- approvedCount++
1294
- } catch (error) {
1295
- logger.warn(`处理申请 ${request.id} 时出错:`, error)
1356
+ if (request.requestId) {
1357
+ try {
1358
+ await session.bot.handleGuildMemberRequest(request.requestId, true)
1359
+ approvedCount++
1360
+ } catch (error) {
1361
+ logger.warn(`处理申请 ${request.id} 时出错:`, error)
1362
+ }
1363
+ } else {
1364
+ skippedCount++
1296
1365
  }
1366
+ // 不论是否有 requestId,都清除记录,避免无限积累
1367
+ await ctx.database.remove('group_verification_pending', { id: request.id })
1297
1368
  }
1298
-
1299
- return `已处理 ${approvedCount} 个加群申请`
1369
+ let msg = `已处理 ${approvedCount} 个加群申请`
1370
+ if (skippedCount) msg += `,${skippedCount} 个因缺少 requestId 未处理`;
1371
+ return msg
1300
1372
  } else {
1301
- // 处理最近的一个申请
1302
- const recentRequest = await ctx.database.get('group_verification_pending', { groupId }, ['id', 'userId', 'userName'])
1303
- if (recentRequest.length === 0) {
1373
+ // 处理最近的一个申请(按时间降序)
1374
+ let pending: any[] = await ctx.database.get('group_verification_pending', { groupId }, ['id', 'userId', 'userName', 'applyTime', 'requestId'])
1375
+ if (pending.length === 0) {
1304
1376
  return '当前无待审核的加群申请'
1305
1377
  }
1306
-
1307
- const request = recentRequest[0]
1378
+ pending.sort((a, b) => String(b.applyTime).localeCompare(String(a.applyTime)))
1379
+ const request: any = pending[0]
1380
+ if (!request.requestId) {
1381
+ return `用户 ${request.userId} 的申请缺少 requestId,无法自动同意`;
1382
+ }
1308
1383
  try {
1309
- // 这里需要获取实际的requestId,暂时用userId作为示例
1310
- await session.bot.handleGuildMemberRequest(request.userId, true)
1311
- await ctx.database.remove('group_verification_pending', { id: request.id })
1312
- return `已同意用户 ${request.userName}(${request.userId}) 的加群申请`
1384
+ await session.bot.handleGuildMemberRequest(request.requestId, true)
1385
+ // 清除该用户的所有待审核记录
1386
+ await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1387
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1388
+ return `已同意用户 ${displayName} 的加群申请`
1313
1389
  } catch (error) {
1314
1390
  return `处理申请时出错: ${error.message}`
1315
1391
  }
@@ -1317,7 +1393,7 @@ export function apply(ctx: Context, config: Config) {
1317
1393
  }
1318
1394
 
1319
1395
  // 处理指定用户ID的情况
1320
- const pendingRequests = await ctx.database.get('group_verification_pending', {
1396
+ let pendingRequests = await ctx.database.get('group_verification_pending', {
1321
1397
  groupId,
1322
1398
  userId: userId
1323
1399
  })
@@ -1327,11 +1403,15 @@ export function apply(ctx: Context, config: Config) {
1327
1403
  }
1328
1404
 
1329
1405
  const request = pendingRequests[0]
1406
+ if (!request.requestId) {
1407
+ return `用户 ${userId} 的申请缺少 requestId,无法自动同意`
1408
+ }
1330
1409
  try {
1331
- // 这里需要获取实际的requestId来进行审批
1332
- await session.bot.handleGuildMemberRequest(request.userId, true)
1333
- await ctx.database.remove('group_verification_pending', { id: request.id })
1334
- return `已同意用户 ${request.userName}(${userId}) 的加群申请`
1410
+ await session.bot.handleGuildMemberRequest(request.requestId, true)
1411
+ // 删除该用户的所有记录
1412
+ await ctx.database.remove('group_verification_pending', { groupId, userId })
1413
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1414
+ return `已同意用户 ${displayName} 的加群申请`
1335
1415
  } catch (error) {
1336
1416
  return `处理申请时出错: ${error.message}`
1337
1417
  }
@@ -1357,7 +1437,7 @@ export function apply(ctx: Context, config: Config) {
1357
1437
  return '请在群聊中使用此命令'
1358
1438
  }
1359
1439
 
1360
- // 处理默认情况和all情况
1440
+ // 处理默认情况和 all 情况
1361
1441
  if (!userId || userId.toLowerCase() === 'all') {
1362
1442
  if (userId?.toLowerCase() === 'all') {
1363
1443
  // 拒绝所有待审核申请
@@ -1367,33 +1447,42 @@ export function apply(ctx: Context, config: Config) {
1367
1447
  }
1368
1448
 
1369
1449
  let rejectedCount = 0
1450
+ let skippedCount = 0
1370
1451
  for (const request of pendingRequests) {
1371
- try {
1372
- // TODO: 需要获取实际的requestId来进行拒绝
1373
- // await session.bot.handleGuildMemberRequest(request.requestId, false)
1374
- await ctx.database.remove('group_verification_pending', { id: request.id })
1375
- await updateStats(ctx, groupId, 'rejected')
1376
- rejectedCount++
1377
- } catch (error) {
1378
- logger.warn(`处理申请 ${request.id} 时出错:`, error)
1452
+ if (request.requestId) {
1453
+ try {
1454
+ await session.bot.handleGuildMemberRequest(request.requestId, false)
1455
+ rejectedCount++
1456
+ } catch (error) {
1457
+ logger.warn(`处理申请 ${request.id} 时出错:`, error)
1458
+ }
1459
+ } else {
1460
+ skippedCount++
1379
1461
  }
1462
+ await ctx.database.remove('group_verification_pending', { id: request.id })
1463
+ await updateStats(ctx, groupId, 'rejected')
1380
1464
  }
1381
-
1382
- return `已拒绝 ${rejectedCount} 个加群申请`
1465
+ let msg = `已拒绝 ${rejectedCount} 个加群申请`
1466
+ if (skippedCount) msg += `,${skippedCount} 个因缺少 requestId 未处理`;
1467
+ return msg
1383
1468
  } else {
1384
- // 拒绝最近的一个申请
1385
- const recentRequest = await ctx.database.get('group_verification_pending', { groupId }, ['id', 'userId', 'userName'])
1386
- if (recentRequest.length === 0) {
1469
+ // 拒绝最近的一个申请(按 applyTime 降序)
1470
+ let pending: any[] = await ctx.database.get('group_verification_pending', { groupId }, ['id', 'userId', 'userName', 'applyTime', 'requestId'])
1471
+ if (pending.length === 0) {
1387
1472
  return '当前无待审核的加群申请'
1388
1473
  }
1389
-
1390
- const request = recentRequest[0]
1474
+ pending.sort((a, b) => String(b.applyTime).localeCompare(String(a.applyTime)))
1475
+ const request: any = pending[0]
1476
+ if (!request.requestId) {
1477
+ return `用户 ${request.userId} 的申请缺少 requestId,无法自动拒绝`;
1478
+ }
1391
1479
  try {
1392
- // TODO: 需要获取实际的requestId来进行拒绝
1393
- // await session.bot.handleGuildMemberRequest(request.requestId, false)
1394
- await ctx.database.remove('group_verification_pending', { id: request.id })
1480
+ await session.bot.handleGuildMemberRequest(request.requestId, false)
1481
+ // 删除该用户的所有记录
1482
+ await ctx.database.remove('group_verification_pending', { groupId, userId: request.userId })
1395
1483
  await updateStats(ctx, groupId, 'rejected')
1396
- return `已拒绝用户 ${request.userName}(${request.userId}) 的加群申请`
1484
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1485
+ return `已拒绝用户 ${displayName} 的加群申请`
1397
1486
  } catch (error) {
1398
1487
  return `处理申请时出错: ${error.message}`
1399
1488
  }
@@ -1411,12 +1500,16 @@ export function apply(ctx: Context, config: Config) {
1411
1500
  }
1412
1501
 
1413
1502
  const request = pendingRequests[0]
1503
+ if (!request.requestId) {
1504
+ return `用户 ${userId} 的申请缺少 requestId,无法自动拒绝`
1505
+ }
1414
1506
  try {
1415
- // TODO: 需要获取实际的requestId来进行拒绝
1416
- // await session.bot.handleGuildMemberRequest(request.requestId, false)
1417
- await ctx.database.remove('group_verification_pending', { id: request.id })
1507
+ await session.bot.handleGuildMemberRequest(request.requestId, false)
1508
+ // 删除该用户的所有记录
1509
+ await ctx.database.remove('group_verification_pending', { groupId, userId })
1418
1510
  await updateStats(ctx, groupId, 'rejected')
1419
- return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`
1511
+ const displayName = request.userName && request.userName !== request.userId ? `${request.userName}(${request.userId})` : request.userId
1512
+ return `已拒绝用户 ${displayName} 的加群申请`
1420
1513
  } catch (error) {
1421
1514
  return `处理申请时出错: ${error.message}`
1422
1515
  }
@@ -1591,6 +1684,8 @@ export function apply(ctx: Context, config: Config) {
1591
1684
  for (const st of stats) {
1592
1685
  const updates: any = {}
1593
1686
  if (st.lastUpdated instanceof Date) updates.lastUpdated = st.lastUpdated.toISOString()
1687
+ // add missing totalJoined column default 0
1688
+ if (st.totalJoined === undefined) updates.totalJoined = 0
1594
1689
  if (Object.keys(updates).length) {
1595
1690
  await ctx.database.set('group_verification_stats', { id: st.id }, updates)
1596
1691
  }
@@ -1599,6 +1694,8 @@ export function apply(ctx: Context, config: Config) {
1599
1694
  for (const p of pendings) {
1600
1695
  const updates: any = {}
1601
1696
  if (p.applyTime instanceof Date) updates.applyTime = p.applyTime.toISOString()
1697
+ // add default empty requestId if record pre-dates the new column
1698
+ if (p.requestId === undefined) updates.requestId = ''
1602
1699
  if (Object.keys(updates).length) {
1603
1700
  await ctx.database.set('group_verification_pending', { id: p.id }, updates)
1604
1701
  }