koishi-plugin-group-verification 1.0.24 → 1.0.25

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
@@ -80,6 +80,7 @@ export interface ParsedArgs {
80
80
  * 返回最终的 reminderEnabled 和 reminderMessage。
81
81
  * 对于 -nomsg 不会清除已保存的 message,便于后续再次启用时恢复。
82
82
  */
83
+ export declare function usageString(): string;
83
84
  export declare function mergeReminder(existingConfig: any | null, cleanedOptions: {
84
85
  message?: string;
85
86
  enableMessage?: boolean;
@@ -88,12 +89,11 @@ export declare function mergeReminder(existingConfig: any | null, cleanedOptions
88
89
  reminderEnabled: boolean;
89
90
  reminderMessage: string;
90
91
  };
91
- /**
92
- * 解析 gvc 配置命令的原始参数。
93
- *
94
- * 返回关键词数组和各类 flag 的值,未出现的 flag 保持 undefined。
95
- * 若检测到格式错误(如纯空格分隔关键词),返回 error 字段。
96
- */
92
+ export declare function syncTotalStats(ctx: Context): Promise<void>;
93
+ export declare function updateStats(ctx: Context, groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected'): Promise<void>;
94
+ export declare function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]>;
95
+ export declare function handleGuildMemberRequestEvent(ctx: Context, session: any): Promise<void>;
96
+ export declare function __getAutoQueue(): Map<string, Set<string>>;
97
97
  export declare function parseConfigArgs(raw: string): ParsedArgs;
98
98
  export declare function verifyApplication(config: GroupVerificationConfig, message: string, session: any): Promise<{
99
99
  isValid: boolean;
package/lib/index.js CHANGED
@@ -21,13 +21,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  Config: () => Config,
24
+ __getAutoQueue: () => __getAutoQueue,
24
25
  apply: () => apply,
26
+ checkPermission: () => checkPermission,
25
27
  handleFailedVerification: () => handleFailedVerification,
28
+ handleGuildMemberRequestEvent: () => handleGuildMemberRequestEvent,
26
29
  inject: () => inject,
27
30
  mergeReminder: () => mergeReminder,
28
31
  name: () => name,
29
32
  parseConfigArgs: () => parseConfigArgs,
33
+ syncTotalStats: () => syncTotalStats,
30
34
  tokenize: () => tokenize,
35
+ updateStats: () => updateStats,
36
+ usageString: () => usageString,
31
37
  verifyApplication: () => verifyApplication
32
38
  });
33
39
  module.exports = __toCommonJS(src_exports);
@@ -35,7 +41,7 @@ var import_koishi = require("koishi");
35
41
  var name = "group-verification";
36
42
  var logger = console;
37
43
  var Config = import_koishi.Schema.object({
38
- defaultReminderMessage: import_koishi.Schema.string().description("默认提醒消息模板").default("{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}"),
44
+ defaultReminderMessage: import_koishi.Schema.string().description("默认提醒消息模板(使用 \n 表示换行,可包含下方变量)").default("{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}\n使用 gva 同意或 gvr 拒绝申请"),
39
45
  enableStrictGroupCheck: import_koishi.Schema.boolean().description("是否启用严格的群号检查(检查群号长度)").default(false),
40
46
  logLevel: import_koishi.Schema.union(["debug", "info", "warn", "error"]).description("日志级别").default("info")
41
47
  }).description("群组验证插件配置");
@@ -143,6 +149,31 @@ function validateKeywordFormat(raw) {
143
149
  return true;
144
150
  }
145
151
  __name(validateKeywordFormat, "validateKeywordFormat");
152
+ function usageString() {
153
+ return `用法:
154
+ # 创建/修改配置
155
+ gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
156
+ gvc -m 1 -t 2 # 修改审核参数
157
+
158
+ # 提醒消息控制(可用变量详见下方)
159
+ gvc -msg "消息内容" # 修改提醒消息
160
+ gvc -nomsg # 禁用提醒消息
161
+ # 查询/删除
162
+ gvc -? # 查询配置
163
+ gvc -r # 删除配置
164
+
165
+ 审核方式说明(使用 -m 参数):
166
+ 0 全部同意(默认)
167
+ 1 按数量同意,需要 -t 指定数量
168
+ 2 按比例同意,需要 -t 指定百分比
169
+ 3 全部拒绝(拒绝后系统会自动阻止任何通过)
170
+
171
+ 提醒消息可用变量:{user} 用户名 {id} 用户ID
172
+ {group} 群号 {gname} 群名称
173
+ {question} 申请理由 {answer} 匹配情况 {threshold} 阈值
174
+ 使用 \\n 换行`;
175
+ }
176
+ __name(usageString, "usageString");
146
177
  function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasRealEnableMessageParam, hasRealDisableMessageParam, logger2) {
147
178
  let reminderEnabled = true;
148
179
  let reminderMessage = "{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}";
@@ -166,6 +197,151 @@ function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasR
166
197
  return { reminderEnabled, reminderMessage };
167
198
  }
168
199
  __name(mergeReminder, "mergeReminder");
200
+ var autoQueue = /* @__PURE__ */ new Map();
201
+ async function syncTotalStats(ctx) {
202
+ try {
203
+ const allStats = await ctx.database.get("group_verification_stats", {
204
+ groupId: { $ne: "TOTAL" }
205
+ });
206
+ if (allStats.length > 0) {
207
+ const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0);
208
+ const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0);
209
+ const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0);
210
+ await ctx.database.set("group_verification_stats", { groupId: "TOTAL" }, {
211
+ autoApproved: totalAutoApproved,
212
+ manuallyApproved: totalManuallyApproved,
213
+ rejected: totalRejected,
214
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
215
+ });
216
+ logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
217
+ }
218
+ } catch (error) {
219
+ logger.error("同步总计统计时出错:", error);
220
+ }
221
+ }
222
+ __name(syncTotalStats, "syncTotalStats");
223
+ async function updateStats(ctx, groupId, action) {
224
+ const existingStats = await ctx.database.get("group_verification_stats", { groupId });
225
+ if (existingStats.length > 0) {
226
+ const stats = existingStats[0];
227
+ await ctx.database.set("group_verification_stats", { id: stats.id }, {
228
+ [action]: stats[action] + 1,
229
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
230
+ });
231
+ } else {
232
+ await ctx.database.create("group_verification_stats", {
233
+ groupId,
234
+ autoApproved: action === "autoApproved" ? 1 : 0,
235
+ manuallyApproved: action === "manuallyApproved" ? 1 : 0,
236
+ rejected: action === "rejected" ? 1 : 0,
237
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
238
+ });
239
+ }
240
+ await syncTotalStats(ctx);
241
+ }
242
+ __name(updateStats, "updateStats");
243
+ async function checkPermission(session, targetGroupId) {
244
+ const groupId = targetGroupId || session.guildId;
245
+ if (!groupId) {
246
+ return [false, "请在群聊中使用此命令或使用 -i 参数指定群号"];
247
+ }
248
+ logger.info(`权限检查 - 用户ID: ${session.userId}, 群号: ${groupId}`);
249
+ const koishiAuthority = session.author?.authority || session.user?.authority;
250
+ logger.info(`权限检查 - Koishi权限等级: ${koishiAuthority || "未获取到"}`);
251
+ if (!session.author) {
252
+ logger.info(`权限检查 - session中可能的权限字段:`, {
253
+ authority: session.authority,
254
+ permission: session.permission,
255
+ role: session.role
256
+ });
257
+ } else {
258
+ logger.info(`权限检查 - author对象中的字段:`, {
259
+ authority: session.author.authority,
260
+ permission: session.author.permission,
261
+ role: session.author.role,
262
+ permissions: session.author.permissions
263
+ });
264
+ }
265
+ if (session.user) {
266
+ logger.info(`权限检查 - user对象中的权限信息:`, {
267
+ authority: session.user.authority,
268
+ permission: session.user.permission,
269
+ role: session.user.role
270
+ });
271
+ }
272
+ if (koishiAuthority && koishiAuthority >= 3) {
273
+ logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`);
274
+ return [true];
275
+ }
276
+ try {
277
+ const member = await session.bot.getGuildMember(groupId, session.userId);
278
+ logger.info(`权限检查 - 获取到成员信息:`, {
279
+ roles: member?.roles,
280
+ permissions: member?.permissions
281
+ });
282
+ if (member) {
283
+ if (member.permissions?.includes("OWNER") || member.roles?.includes("owner")) {
284
+ logger.info(`权限检查 - 用户是群主`);
285
+ return [true];
286
+ }
287
+ if (member.roles?.includes("admin") || member.permissions?.includes("ADMINISTRATOR")) {
288
+ logger.info(`权限检查 - 用户是管理员`);
289
+ return [true];
290
+ }
291
+ }
292
+ } catch (e) {
293
+ logger.warn("权限检查获取成员信息失败", e);
294
+ }
295
+ return [false, "权限不足"];
296
+ }
297
+ __name(checkPermission, "checkPermission");
298
+ async function handleGuildMemberRequestEvent(ctx, session) {
299
+ logger.debug("guild-member-request event", session);
300
+ let guildId = (session.guildId || session.channelId || "").toString().trim();
301
+ const userId = session.userId;
302
+ const message = session.content || "";
303
+ if (!guildId) {
304
+ logger.warn("guild-member-request 没有 guildId,跳过处理");
305
+ return;
306
+ }
307
+ const requestId = session.event?.requestId || session.messageId || "";
308
+ const groupConfig = await ctx.database.get("group_verification_config", { groupId: guildId });
309
+ if (!groupConfig || groupConfig.length === 0) return;
310
+ const config = groupConfig[0];
311
+ if (config.reviewMethod === 3) {
312
+ logger.info(`配置要求全部拒绝,自动拒绝用户 ${userId}`);
313
+ if (requestId) {
314
+ try {
315
+ await session.bot.handleGuildMemberRequest(requestId, false);
316
+ } catch (e) {
317
+ logger.warn("自动拒绝失败", e);
318
+ }
319
+ }
320
+ await updateStats(ctx, guildId, "rejected");
321
+ return;
322
+ }
323
+ const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
324
+ logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
325
+ if (isValid) {
326
+ if (requestId) {
327
+ try {
328
+ await session.bot.handleGuildMemberRequest(requestId, true);
329
+ logger.info(`自动同意 requestId=${requestId}`);
330
+ if (!autoQueue.has(guildId)) autoQueue.set(guildId, /* @__PURE__ */ new Set());
331
+ autoQueue.get(guildId).add(userId);
332
+ } catch (e) {
333
+ logger.warn("自动同意失败", e);
334
+ }
335
+ }
336
+ } else {
337
+ await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
338
+ }
339
+ }
340
+ __name(handleGuildMemberRequestEvent, "handleGuildMemberRequestEvent");
341
+ function __getAutoQueue() {
342
+ return autoQueue;
343
+ }
344
+ __name(__getAutoQueue, "__getAutoQueue");
169
345
  function parseConfigArgs(raw) {
170
346
  const res = tokenize(raw);
171
347
  if (res.error) {
@@ -386,47 +562,12 @@ function apply(ctx, config) {
386
562
  primary: "id",
387
563
  autoInc: true
388
564
  });
389
- const autoQueue = /* @__PURE__ */ new Map();
390
- ctx.on("guild-member-request", async (session) => {
391
- logger.debug("guild-member-request event", session);
392
- let guildId = (session.guildId || session.channelId || "").toString().trim();
393
- const userId = session.userId;
394
- const message = session.content || "";
395
- if (!guildId) {
396
- logger.warn("guild-member-request 没有 guildId,跳过处理");
397
- return;
398
- }
399
- const requestId = session.event?.requestId || session.messageId || "";
400
- const groupConfig = await ctx.database.get("group_verification_config", {
401
- groupId: guildId
402
- });
403
- if (!groupConfig || groupConfig.length === 0) {
404
- return;
405
- }
406
- const config2 = groupConfig[0];
407
- const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config2, message, session);
408
- logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
409
- if (isValid) {
410
- if (requestId) {
411
- try {
412
- await session.bot.handleGuildMemberRequest(requestId, true);
413
- logger.info(`自动同意 requestId=${requestId}`);
414
- if (!autoQueue.has(guildId)) autoQueue.set(guildId, /* @__PURE__ */ new Set());
415
- autoQueue.get(guildId).add(userId);
416
- } catch (e) {
417
- logger.warn("自动同意失败", e);
418
- }
419
- }
420
- } else {
421
- await handleFailedVerification(ctx, session, config2, matchedCount, requiredThreshold);
422
- }
423
- });
424
565
  ctx.on("guild-member-added", async (session) => {
425
566
  const groupId = session.guildId;
426
567
  const userId = session.userId;
427
568
  const set = autoQueue.get(groupId);
428
569
  if (set && set.has(userId)) {
429
- await updateStats(groupId, "autoApproved");
570
+ await updateStats(ctx, groupId, "autoApproved");
430
571
  set.delete(userId);
431
572
  logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`);
432
573
  return;
@@ -436,35 +577,15 @@ function apply(ctx, config) {
436
577
  userId
437
578
  });
438
579
  if (pendingRecords.length > 0) {
439
- await updateStats(groupId, "autoApproved");
580
+ await updateStats(ctx, groupId, "autoApproved");
440
581
  await ctx.database.remove("group_verification_pending", { id: pendingRecords[0].id });
441
582
  logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`);
442
583
  } else {
443
- await updateStats(groupId, "manuallyApproved");
584
+ await updateStats(ctx, groupId, "manuallyApproved");
444
585
  logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`);
445
586
  }
446
587
  });
447
- async function updateStats(groupId, action) {
448
- const existingStats = await ctx.database.get("group_verification_stats", { groupId });
449
- if (existingStats.length > 0) {
450
- const stats = existingStats[0];
451
- await ctx.database.set("group_verification_stats", { id: stats.id }, {
452
- [action]: stats[action] + 1,
453
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
454
- });
455
- } else {
456
- await ctx.database.create("group_verification_stats", {
457
- groupId,
458
- autoApproved: action === "autoApproved" ? 1 : 0,
459
- manuallyApproved: action === "manuallyApproved" ? 1 : 0,
460
- rejected: action === "rejected" ? 1 : 0,
461
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
462
- });
463
- }
464
- await syncTotalStats(ctx);
465
- }
466
- __name(updateStats, "updateStats");
467
- async function checkPermission(session, targetGroupId) {
588
+ async function checkPermission2(session, targetGroupId) {
468
589
  const groupId = targetGroupId || session.guildId;
469
590
  if (!groupId) {
470
591
  return [false, "请在群聊中使用此命令或使用 -i 参数指定群号"];
@@ -522,7 +643,7 @@ function apply(ctx, config) {
522
643
  return [false, `权限不足:需要群主/管理员权限或koishi三级以上权限
523
644
  ${debugInfo}`];
524
645
  }
525
- __name(checkPermission, "checkPermission");
646
+ __name(checkPermission2, "checkPermission");
526
647
  const groupVerify = ctx.command("group-verify", "群组验证管理命令").alias("gv", "gverify");
527
648
  groupVerify.subcommand(".config [keywords:text]", "配置群组验证规则").alias(
528
649
  "gv.cfg",
@@ -570,7 +691,7 @@ ${debugInfo}`];
570
691
  }
571
692
  try {
572
693
  await session.bot.getGuild(targetGroupId);
573
- const [hasPermission, errorMsg] = await checkPermission(session, targetGroupId);
694
+ const [hasPermission, errorMsg] = await checkPermission2(session, targetGroupId);
574
695
  if (!hasPermission) {
575
696
  return errorMsg || "权限不足";
576
697
  }
@@ -658,13 +779,7 @@ ${debugInfo}`];
658
779
  if (keywordList.length === 0 && !cleanedOptions.query && !cleanedOptions.remove) {
659
780
  const hasConfigParams = cleanedOptions.method !== void 0 || cleanedOptions.threshold !== void 0 || hasRealMessageParam || hasRealEnableMessageParam || hasRealDisableMessageParam;
660
781
  if (!hasConfigParams) {
661
- return `用法:
662
- gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
663
- gvc -m 1 -t 2 # 修改审核参数
664
- gvc -msg "消息内容" # 修改提醒消息
665
- gvc -nomsg # 禁用提醒消息
666
- gvc -? # 查询配置
667
- gvc -r # 删除配置`;
782
+ return usageString();
668
783
  }
669
784
  }
670
785
  let existingConfig = null;
@@ -820,7 +935,7 @@ gvc -r # 删除配置`;
820
935
  "group-verify.同意",
821
936
  "gva"
822
937
  ).action(async ({ session }, userId) => {
823
- const [hasPermission, errorMsg] = await checkPermission(session);
938
+ const [hasPermission, errorMsg] = await checkPermission2(session);
824
939
  if (!hasPermission) {
825
940
  return errorMsg || "权限不足";
826
941
  }
@@ -828,6 +943,10 @@ gvc -r # 删除配置`;
828
943
  if (!groupId) {
829
944
  return "请在群聊中使用此命令";
830
945
  }
946
+ const configs = await ctx.database.get("group_verification_config", { groupId });
947
+ if (configs.length > 0 && configs[0].reviewMethod === 3) {
948
+ return "该群已设为全部拒绝,无法手动同意任何申请";
949
+ }
831
950
  if (!userId || userId.toLowerCase() === "all") {
832
951
  if (userId?.toLowerCase() === "all") {
833
952
  const pendingRequests2 = await ctx.database.get("group_verification_pending", { groupId });
@@ -838,8 +957,6 @@ gvc -r # 删除配置`;
838
957
  for (const request2 of pendingRequests2) {
839
958
  try {
840
959
  await ctx.database.remove("group_verification_pending", { id: request2.id });
841
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
842
- autoQueue.get(groupId).add(request2.userId);
843
960
  approvedCount++;
844
961
  } catch (error) {
845
962
  logger.warn(`处理申请 ${request2.id} 时出错:`, error);
@@ -855,8 +972,6 @@ gvc -r # 删除配置`;
855
972
  try {
856
973
  await session.bot.handleGuildMemberRequest(request2.userId, true);
857
974
  await ctx.database.remove("group_verification_pending", { id: request2.id });
858
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
859
- autoQueue.get(groupId).add(request2.userId);
860
975
  return `已同意用户 ${request2.userName}(${request2.userId}) 的加群申请`;
861
976
  } catch (error) {
862
977
  return `处理申请时出错: ${error.message}`;
@@ -874,8 +989,6 @@ gvc -r # 删除配置`;
874
989
  try {
875
990
  await session.bot.handleGuildMemberRequest(request.userId, true);
876
991
  await ctx.database.remove("group_verification_pending", { id: request.id });
877
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
878
- autoQueue.get(groupId).add(userId);
879
992
  return `已同意用户 ${request.userName}(${userId}) 的加群申请`;
880
993
  } catch (error) {
881
994
  return `处理申请时出错: ${error.message}`;
@@ -890,7 +1003,7 @@ gvc -r # 删除配置`;
890
1003
  "group-verify.rej",
891
1004
  "gvr"
892
1005
  ).action(async ({ session }, userId) => {
893
- const [hasPermission, errorMsg] = await checkPermission(session);
1006
+ const [hasPermission, errorMsg] = await checkPermission2(session);
894
1007
  if (!hasPermission) {
895
1008
  return errorMsg || "权限不足";
896
1009
  }
@@ -908,7 +1021,7 @@ gvc -r # 删除配置`;
908
1021
  for (const request2 of pendingRequests2) {
909
1022
  try {
910
1023
  await ctx.database.remove("group_verification_pending", { id: request2.id });
911
- await updateStats(groupId, "rejected");
1024
+ await updateStats(ctx, groupId, "rejected");
912
1025
  rejectedCount++;
913
1026
  } catch (error) {
914
1027
  logger.warn(`处理申请 ${request2.id} 时出错:`, error);
@@ -923,7 +1036,7 @@ gvc -r # 删除配置`;
923
1036
  const request2 = recentRequest[0];
924
1037
  try {
925
1038
  await ctx.database.remove("group_verification_pending", { id: request2.id });
926
- await updateStats(groupId, "rejected");
1039
+ await updateStats(ctx, groupId, "rejected");
927
1040
  return `已拒绝用户 ${request2.userName}(${request2.userId}) 的加群申请`;
928
1041
  } catch (error) {
929
1042
  return `处理申请时出错: ${error.message}`;
@@ -940,7 +1053,7 @@ gvc -r # 删除配置`;
940
1053
  const request = pendingRequests[0];
941
1054
  try {
942
1055
  await ctx.database.remove("group_verification_pending", { id: request.id });
943
- await updateStats(groupId, "rejected");
1056
+ await updateStats(ctx, groupId, "rejected");
944
1057
  return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`;
945
1058
  } catch (error) {
946
1059
  return `处理申请时出错: ${error.message}`;
@@ -1145,39 +1258,23 @@ gvc -r # 删除配置`;
1145
1258
  最后更新: ${lastUpdated}`;
1146
1259
  }
1147
1260
  __name(getTotalStats, "getTotalStats");
1148
- async function syncTotalStats(ctx2) {
1149
- try {
1150
- const allStats = await ctx2.database.get("group_verification_stats", {
1151
- groupId: { $ne: "TOTAL" }
1152
- });
1153
- if (allStats.length > 0) {
1154
- const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0);
1155
- const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0);
1156
- const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0);
1157
- await ctx2.database.set("group_verification_stats", { groupId: "TOTAL" }, {
1158
- autoApproved: totalAutoApproved,
1159
- manuallyApproved: totalManuallyApproved,
1160
- rejected: totalRejected,
1161
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1162
- });
1163
- logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
1164
- }
1165
- } catch (error) {
1166
- logger.error("同步总计统计时出错:", error);
1167
- }
1168
- }
1169
- __name(syncTotalStats, "syncTotalStats");
1170
1261
  }
1171
1262
  __name(apply, "apply");
1172
1263
  // Annotate the CommonJS export names for ESM import in node:
1173
1264
  0 && (module.exports = {
1174
1265
  Config,
1266
+ __getAutoQueue,
1175
1267
  apply,
1268
+ checkPermission,
1176
1269
  handleFailedVerification,
1270
+ handleGuildMemberRequestEvent,
1177
1271
  inject,
1178
1272
  mergeReminder,
1179
1273
  name,
1180
1274
  parseConfigArgs,
1275
+ syncTotalStats,
1181
1276
  tokenize,
1277
+ updateStats,
1278
+ usageString,
1182
1279
  verifyApplication
1183
1280
  });
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.24",
4
+ "version": "1.0.25",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -124,7 +124,9 @@ group-verify.stats total
124
124
  {question} - 申请理由
125
125
  {answer} - 答对数量/比例
126
126
  {threshold} - 阈值要求
127
- \n - 换行符
127
+ \n - 换行符(在配置中写成 `\\n`)
128
+
129
+ 默认模板末尾会附加一句 “使用 gva 同意或 gvr 拒绝申请”,表示管理员可以通过快捷命令操作。
128
130
  ```
129
131
 
130
132
  ## 🔐 权限说明
package/src/index.ts CHANGED
@@ -56,7 +56,9 @@ export interface Config {
56
56
  }
57
57
 
58
58
  export const Config: Schema<Config> = Schema.object({
59
- defaultReminderMessage: Schema.string().description('默认提醒消息模板').default('{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}'),
59
+ defaultReminderMessage: Schema.string()
60
+ .description('默认提醒消息模板(使用 \n 表示换行,可包含下方变量)')
61
+ .default('{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}\n使用 gva 同意或 gvr 拒绝申请'),
60
62
  enableStrictGroupCheck: Schema.boolean().description('是否启用严格的群号检查(检查群号长度)').default(false),
61
63
  logLevel: Schema.union(['debug', 'info', 'warn', 'error']).description('日志级别').default('info')
62
64
  }).description('群组验证插件配置')
@@ -216,6 +218,31 @@ function validateKeywordFormat(raw: string): boolean {
216
218
  * 返回最终的 reminderEnabled 和 reminderMessage。
217
219
  * 对于 -nomsg 不会清除已保存的 message,便于后续再次启用时恢复。
218
220
  */
221
+ export function usageString(): string {
222
+ return `用法:
223
+ # 创建/修改配置
224
+ gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
225
+ gvc -m 1 -t 2 # 修改审核参数
226
+
227
+ # 提醒消息控制(可用变量详见下方)
228
+ gvc -msg "消息内容" # 修改提醒消息
229
+ gvc -nomsg # 禁用提醒消息
230
+ # 查询/删除
231
+ gvc -? # 查询配置
232
+ gvc -r # 删除配置
233
+
234
+ 审核方式说明(使用 -m 参数):
235
+ 0 全部同意(默认)
236
+ 1 按数量同意,需要 -t 指定数量
237
+ 2 按比例同意,需要 -t 指定百分比
238
+ 3 全部拒绝(拒绝后系统会自动阻止任何通过)
239
+
240
+ 提醒消息可用变量:{user} 用户名 {id} 用户ID
241
+ {group} 群号 {gname} 群名称
242
+ {question} 申请理由 {answer} 匹配情况 {threshold} 阈值
243
+ 使用 \\n 换行`;
244
+ }
245
+
219
246
  export function mergeReminder(
220
247
  existingConfig: any | null,
221
248
  cleanedOptions: {
@@ -260,6 +287,177 @@ export function mergeReminder(
260
287
  * 返回关键词数组和各类 flag 的值,未出现的 flag 保持 undefined。
261
288
  * 若检测到格式错误(如纯空格分隔关键词),返回 error 字段。
262
289
  */
290
+ // 全局缓存:记录通过机器人自动批准的用户,供 guild-member-added 事件使用
291
+ const autoQueue = new Map<string, Set<string>>();
292
+
293
+ // 更新统计信息函数,提取到模块层供多个位置调用
294
+ // synchronize overall statistics across groups
295
+ export async function syncTotalStats(ctx: Context) {
296
+ try {
297
+ // 获取所有群组统计(排除TOTAL行)
298
+ const allStats = await ctx.database.get('group_verification_stats', {
299
+ groupId: { $ne: 'TOTAL' }
300
+ })
301
+
302
+ if (allStats.length > 0) {
303
+ // 计算总计
304
+ const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0)
305
+ const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0)
306
+ const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0)
307
+
308
+ // 更新总计行
309
+ await ctx.database.set('group_verification_stats', { groupId: 'TOTAL' }, {
310
+ autoApproved: totalAutoApproved,
311
+ manuallyApproved: totalManuallyApproved,
312
+ rejected: totalRejected,
313
+ lastUpdated: new Date().toISOString()
314
+ })
315
+
316
+ logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)
317
+ }
318
+ } catch (error) {
319
+ logger.error('同步总计统计时出错:', error)
320
+ }
321
+ }
322
+
323
+ export async function updateStats(ctx: Context, groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected') {
324
+ // 更新群组统计
325
+ const existingStats = await ctx.database.get('group_verification_stats', { groupId })
326
+
327
+ if (existingStats.length > 0) {
328
+ const stats = existingStats[0]
329
+ await ctx.database.set('group_verification_stats', { id: stats.id }, {
330
+ [action]: stats[action] + 1,
331
+ lastUpdated: new Date().toISOString()
332
+ })
333
+ } else {
334
+ await ctx.database.create('group_verification_stats', {
335
+ groupId,
336
+ autoApproved: action === 'autoApproved' ? 1 : 0,
337
+ manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
338
+ rejected: action === 'rejected' ? 1 : 0,
339
+ lastUpdated: new Date().toISOString()
340
+ })
341
+ }
342
+
343
+ // 同步更新总计统计
344
+ await syncTotalStats(ctx)
345
+ }
346
+
347
+ // 权限检查函数(也可用于命令)
348
+ export async function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]> {
349
+ const groupId = targetGroupId || session.guildId
350
+
351
+ // 私聊情况下必须指定群号
352
+ if (!groupId) {
353
+ return [false, '请在群聊中使用此命令或使用 -i 参数指定群号']
354
+ }
355
+
356
+ logger.info(`权限检查 - 用户ID: ${session.userId}, 群号: ${groupId}`)
357
+ const koishiAuthority = session.author?.authority || session.user?.authority
358
+ logger.info(`权限检查 - Koishi权限等级: ${koishiAuthority || '未获取到'}`)
359
+
360
+ if (!session.author) {
361
+ logger.info(`权限检查 - session中可能的权限字段:`, {
362
+ authority: session.authority,
363
+ permission: session.permission,
364
+ role: session.role
365
+ })
366
+ } else {
367
+ logger.info(`权限检查 - author对象中的字段:`, {
368
+ authority: session.author.authority,
369
+ permission: session.author.permission,
370
+ role: session.author.role,
371
+ permissions: session.author.permissions
372
+ })
373
+ }
374
+
375
+ if (session.user) {
376
+ logger.info(`权限检查 - user对象中的权限信息:`, {
377
+ authority: session.user.authority,
378
+ permission: session.user.permission,
379
+ role: session.user.role
380
+ })
381
+ }
382
+
383
+ if (koishiAuthority && koishiAuthority >= 3) {
384
+ logger.info(`权限检查 - 通过koishi权限检查: ${koishiAuthority}`)
385
+ return [true]
386
+ }
387
+
388
+ try {
389
+ const member = await session.bot.getGuildMember(groupId, session.userId)
390
+ logger.info(`权限检查 - 获取到成员信息:`, {
391
+ roles: member?.roles,
392
+ permissions: member?.permissions
393
+ })
394
+ if (member) {
395
+ if (member.permissions?.includes('OWNER') || member.roles?.includes('owner')) {
396
+ logger.info(`权限检查 - 用户是群主`)
397
+ return [true]
398
+ }
399
+ if (member.roles?.includes('admin') || member.permissions?.includes('ADMINISTRATOR')) {
400
+ logger.info(`权限检查 - 用户是管理员`)
401
+ return [true]
402
+ }
403
+ }
404
+ } catch (e) {
405
+ logger.warn('权限检查获取成员信息失败', e)
406
+ }
407
+
408
+ return [false, '权限不足']
409
+ }
410
+
411
+ // 提供给测试的辅助函数:处理 guild-member-request 事件的逻辑
412
+ export async function handleGuildMemberRequestEvent(ctx: Context, session: any) {
413
+ logger.debug('guild-member-request event', session)
414
+ let guildId = (session.guildId || session.channelId || '').toString().trim();
415
+ const userId = session.userId;
416
+ const message = session.content || '';
417
+
418
+ if (!guildId) {
419
+ logger.warn('guild-member-request 没有 guildId,跳过处理');
420
+ return;
421
+ }
422
+
423
+ const requestId = ((session.event as any)?.requestId) || session.messageId || '';
424
+ const groupConfig = await ctx.database.get('group_verification_config', { groupId: guildId });
425
+ if (!groupConfig || groupConfig.length === 0) return;
426
+ const config = groupConfig[0];
427
+
428
+ if (config.reviewMethod === 3) {
429
+ logger.info(`配置要求全部拒绝,自动拒绝用户 ${userId}`);
430
+ if (requestId) {
431
+ try { await session.bot.handleGuildMemberRequest(requestId, false); } catch (e) { logger.warn('自动拒绝失败', e); }
432
+ }
433
+ await updateStats(ctx, guildId, 'rejected');
434
+ return;
435
+ }
436
+
437
+ const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
438
+ logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
439
+
440
+ if (isValid) {
441
+ if (requestId) {
442
+ try {
443
+ await session.bot.handleGuildMemberRequest(requestId, true);
444
+ logger.info(`自动同意 requestId=${requestId}`);
445
+ if (!autoQueue.has(guildId)) autoQueue.set(guildId, new Set());
446
+ autoQueue.get(guildId)!.add(userId);
447
+ } catch (e) {
448
+ logger.warn('自动同意失败', e);
449
+ }
450
+ }
451
+ } else {
452
+ await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
453
+ }
454
+ }
455
+
456
+ // 供测试读取当前 autoQueue 状态
457
+ export function __getAutoQueue() {
458
+ return autoQueue;
459
+ }
460
+
263
461
  export function parseConfigArgs(raw: string): ParsedArgs {
264
462
  const res = tokenize(raw);
265
463
  if (res.error) {
@@ -543,57 +741,7 @@ export function apply(ctx: Context, config: Config) {
543
741
  autoInc: true
544
742
  })
545
743
 
546
- // 缓存正在审批的用户,用于避免 guild-member-added 重复计数
547
- const autoQueue = new Map<string, Set<string>>();
548
-
549
- // 监听加群申请事件
550
- ctx.on('guild-member-request', async (session) => {
551
- // debug: 输出整个 session 以便定位字段名
552
- logger.debug('guild-member-request event', session)
553
-
554
- let guildId = (session.guildId || session.channelId || '').toString().trim();
555
- const userId = session.userId;
556
- const message = session.content || '';
557
-
558
- if (!guildId) {
559
- logger.warn('guild-member-request 没有 guildId,跳过处理');
560
- return;
561
- }
562
-
563
- // 获取 requestId(不同平台字段不同)
564
- const requestId = ((session.event as any)?.requestId) || session.messageId || '';
565
744
 
566
- // 获取群组配置
567
- const groupConfig = await ctx.database.get('group_verification_config', {
568
- groupId: guildId
569
- });
570
-
571
- if (!groupConfig || groupConfig.length === 0) {
572
- // 无配置直接放行(需要有 requestId)
573
- return;
574
- }
575
- const config = groupConfig[0];
576
-
577
- // 执行验证,记录详细情况
578
- const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
579
- logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
580
-
581
- if (isValid) {
582
- if (requestId) {
583
- try {
584
- await session.bot.handleGuildMemberRequest(requestId, true);
585
- logger.info(`自动同意 requestId=${requestId}`);
586
- // 将此用户标记为自动批准,等待 guild-member-added 更新统计
587
- if (!autoQueue.has(guildId)) autoQueue.set(guildId, new Set());
588
- autoQueue.get(guildId)!.add(userId);
589
- } catch (e) {
590
- logger.warn('自动同意失败', e);
591
- }
592
- }
593
- } else {
594
- await handleFailedVerification(ctx, session, config, matchedCount, requiredThreshold);
595
- }
596
- });
597
745
 
598
746
  // 监听群成员增加事件(包括手动邀请入群)
599
747
  ctx.on('guild-member-added', async (session) => {
@@ -603,7 +751,7 @@ export function apply(ctx: Context, config: Config) {
603
751
  // 先检查 autoQueue
604
752
  const set = autoQueue.get(groupId)
605
753
  if (set && set.has(userId)) {
606
- await updateStats(groupId, 'autoApproved')
754
+ await updateStats(ctx, groupId, 'autoApproved')
607
755
  set.delete(userId)
608
756
  logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`)
609
757
  return
@@ -617,13 +765,13 @@ export function apply(ctx: Context, config: Config) {
617
765
 
618
766
  if (pendingRecords.length > 0) {
619
767
  // 通过验证的用户入群,更新统计
620
- await updateStats(groupId, 'autoApproved')
768
+ await updateStats(ctx, groupId, 'autoApproved')
621
769
  // 清除待审核记录
622
770
  await ctx.database.remove('group_verification_pending', { id: pendingRecords[0].id })
623
771
  logger.info(`用户 ${userId} 通过验证加入群 ${groupId},统计已更新`)
624
772
  } else {
625
773
  // 手动邀请入群,记录到手动批准统计
626
- await updateStats(groupId, 'manuallyApproved')
774
+ await updateStats(ctx, groupId, 'manuallyApproved')
627
775
  logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`)
628
776
  }
629
777
  })
@@ -631,29 +779,6 @@ export function apply(ctx: Context, config: Config) {
631
779
 
632
780
 
633
781
  // 更新统计信息
634
- async function updateStats(groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected') {
635
- // 更新群组统计
636
- const existingStats = await ctx.database.get('group_verification_stats', { groupId })
637
-
638
- if (existingStats.length > 0) {
639
- const stats = existingStats[0]
640
- await ctx.database.set('group_verification_stats', { id: stats.id }, {
641
- [action]: stats[action] + 1,
642
- lastUpdated: new Date().toISOString()
643
- })
644
- } else {
645
- await ctx.database.create('group_verification_stats', {
646
- groupId,
647
- autoApproved: action === 'autoApproved' ? 1 : 0,
648
- manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
649
- rejected: action === 'rejected' ? 1 : 0,
650
- lastUpdated: new Date().toISOString()
651
- })
652
- }
653
-
654
- // 同步更新总计统计
655
- await syncTotalStats(ctx)
656
- }
657
782
 
658
783
  // 权限检查函数
659
784
  async function checkPermission(session: any, targetGroupId?: string): Promise<[boolean, string?]> {
@@ -927,13 +1052,7 @@ export function apply(ctx: Context, config: Config) {
927
1052
  hasRealEnableMessageParam ||
928
1053
  hasRealDisableMessageParam
929
1054
  if (!hasConfigParams) {
930
- return `用法:
931
- gvc 关键词1,关键词2 -m 1 -t 2 # 创建配置
932
- gvc -m 1 -t 2 # 修改审核参数
933
- gvc -msg "消息内容" # 修改提醒消息
934
- gvc -nomsg # 禁用提醒消息
935
- gvc -? # 查询配置
936
- gvc -r # 删除配置`
1055
+ return usageString()
937
1056
  }
938
1057
  // 这里会在下面获取一次 existingConfig,故暂不重复查询
939
1058
  }
@@ -1145,6 +1264,11 @@ gvc -r # 删除配置`
1145
1264
  return '请在群聊中使用此命令'
1146
1265
  }
1147
1266
 
1267
+ // 在开始之前检查配置是否为全部拒绝
1268
+ const configs = await ctx.database.get('group_verification_config', { groupId })
1269
+ if (configs.length > 0 && configs[0].reviewMethod === 3) {
1270
+ return '该群已设为全部拒绝,无法手动同意任何申请'
1271
+ }
1148
1272
  // 处理默认情况和all情况
1149
1273
  if (!userId || userId.toLowerCase() === 'all') {
1150
1274
  if (userId?.toLowerCase() === 'all') {
@@ -1160,9 +1284,6 @@ gvc -r # 删除配置`
1160
1284
  // TODO: 需要获取实际的requestId来进行审批
1161
1285
  // await session.bot.handleGuildMemberRequest(request.requestId, true)
1162
1286
  await ctx.database.remove('group_verification_pending', { id: request.id })
1163
- // 标记为自动批准,实际统计在 guild-member-added 处理
1164
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1165
- autoQueue.get(groupId)!.add(request.userId)
1166
1287
  approvedCount++
1167
1288
  } catch (error) {
1168
1289
  logger.warn(`处理申请 ${request.id} 时出错:`, error)
@@ -1182,8 +1303,6 @@ gvc -r # 删除配置`
1182
1303
  // 这里需要获取实际的requestId,暂时用userId作为示例
1183
1304
  await session.bot.handleGuildMemberRequest(request.userId, true)
1184
1305
  await ctx.database.remove('group_verification_pending', { id: request.id })
1185
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1186
- autoQueue.get(groupId)!.add(request.userId)
1187
1306
  return `已同意用户 ${request.userName}(${request.userId}) 的加群申请`
1188
1307
  } catch (error) {
1189
1308
  return `处理申请时出错: ${error.message}`
@@ -1206,8 +1325,6 @@ gvc -r # 删除配置`
1206
1325
  // 这里需要获取实际的requestId来进行审批
1207
1326
  await session.bot.handleGuildMemberRequest(request.userId, true)
1208
1327
  await ctx.database.remove('group_verification_pending', { id: request.id })
1209
- if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1210
- autoQueue.get(groupId)!.add(userId)
1211
1328
  return `已同意用户 ${request.userName}(${userId}) 的加群申请`
1212
1329
  } catch (error) {
1213
1330
  return `处理申请时出错: ${error.message}`
@@ -1249,7 +1366,7 @@ gvc -r # 删除配置`
1249
1366
  // TODO: 需要获取实际的requestId来进行拒绝
1250
1367
  // await session.bot.handleGuildMemberRequest(request.requestId, false)
1251
1368
  await ctx.database.remove('group_verification_pending', { id: request.id })
1252
- await updateStats(groupId, 'rejected')
1369
+ await updateStats(ctx, groupId, 'rejected')
1253
1370
  rejectedCount++
1254
1371
  } catch (error) {
1255
1372
  logger.warn(`处理申请 ${request.id} 时出错:`, error)
@@ -1269,7 +1386,7 @@ gvc -r # 删除配置`
1269
1386
  // TODO: 需要获取实际的requestId来进行拒绝
1270
1387
  // await session.bot.handleGuildMemberRequest(request.requestId, false)
1271
1388
  await ctx.database.remove('group_verification_pending', { id: request.id })
1272
- await updateStats(groupId, 'rejected')
1389
+ await updateStats(ctx, groupId, 'rejected')
1273
1390
  return `已拒绝用户 ${request.userName}(${request.userId}) 的加群申请`
1274
1391
  } catch (error) {
1275
1392
  return `处理申请时出错: ${error.message}`
@@ -1292,7 +1409,7 @@ gvc -r # 删除配置`
1292
1409
  // TODO: 需要获取实际的requestId来进行拒绝
1293
1410
  // await session.bot.handleGuildMemberRequest(request.requestId, false)
1294
1411
  await ctx.database.remove('group_verification_pending', { id: request.id })
1295
- await updateStats(groupId, 'rejected')
1412
+ await updateStats(ctx, groupId, 'rejected')
1296
1413
  return `已拒绝用户 ${request.userName}(${userId}) 的加群申请`
1297
1414
  } catch (error) {
1298
1415
  return `处理申请时出错: ${error.message}`
@@ -1542,32 +1659,4 @@ gvc -r # 删除配置`
1542
1659
  最后更新: ${lastUpdated}`
1543
1660
  }
1544
1661
 
1545
- // 同步统计数据到总计行的函数
1546
- async function syncTotalStats(ctx: Context) {
1547
- try {
1548
- // 获取所有群组统计(排除TOTAL行)
1549
- const allStats = await ctx.database.get('group_verification_stats', {
1550
- groupId: { $ne: 'TOTAL' }
1551
- })
1552
-
1553
- if (allStats.length > 0) {
1554
- // 计算总计
1555
- const totalAutoApproved = allStats.reduce((sum, stat) => sum + (stat.autoApproved || 0), 0)
1556
- const totalManuallyApproved = allStats.reduce((sum, stat) => sum + (stat.manuallyApproved || 0), 0)
1557
- const totalRejected = allStats.reduce((sum, stat) => sum + (stat.rejected || 0), 0)
1558
-
1559
- // 更新总计行
1560
- await ctx.database.set('group_verification_stats', { groupId: 'TOTAL' }, {
1561
- autoApproved: totalAutoApproved,
1562
- manuallyApproved: totalManuallyApproved,
1563
- rejected: totalRejected,
1564
- lastUpdated: new Date().toISOString()
1565
- })
1566
-
1567
- logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)
1568
- }
1569
- } catch (error) {
1570
- logger.error('同步总计统计时出错:', error)
1571
- }
1572
- }
1573
1662
  }