koishi-plugin-group-verification 1.0.18 → 1.0.20

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
@@ -17,8 +17,8 @@ export interface GroupVerificationConfig {
17
17
  reminderMessage: string;
18
18
  createdBy: string;
19
19
  updatedBy: string;
20
- createdAt: Date;
21
- updatedAt: Date;
20
+ createdAt: string | Date;
21
+ updatedAt: string | Date;
22
22
  }
23
23
  export interface GroupVerificationStats {
24
24
  id: number;
@@ -26,7 +26,7 @@ export interface GroupVerificationStats {
26
26
  autoApproved: number;
27
27
  manuallyApproved: number;
28
28
  rejected: number;
29
- lastUpdated: Date;
29
+ lastUpdated: string | Date;
30
30
  }
31
31
  export interface PendingVerification {
32
32
  id: number;
@@ -34,7 +34,7 @@ export interface PendingVerification {
34
34
  userId: string;
35
35
  userName: string;
36
36
  requestMessage: string;
37
- applyTime: Date;
37
+ applyTime: string | Date;
38
38
  }
39
39
  export interface Config {
40
40
  defaultReminderMessage?: string;
@@ -95,4 +95,9 @@ export declare function mergeReminder(existingConfig: any | null, cleanedOptions
95
95
  * 若检测到格式错误(如纯空格分隔关键词),返回 error 字段。
96
96
  */
97
97
  export declare function parseConfigArgs(raw: string): ParsedArgs;
98
+ export declare function verifyApplication(config: GroupVerificationConfig, message: string, session: any): Promise<{
99
+ isValid: boolean;
100
+ matchedCount: number;
101
+ requiredThreshold: string;
102
+ }>;
98
103
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -26,11 +26,13 @@ __export(src_exports, {
26
26
  mergeReminder: () => mergeReminder,
27
27
  name: () => name,
28
28
  parseConfigArgs: () => parseConfigArgs,
29
- tokenize: () => tokenize
29
+ tokenize: () => tokenize,
30
+ verifyApplication: () => verifyApplication
30
31
  });
31
32
  module.exports = __toCommonJS(src_exports);
32
33
  var import_koishi = require("koishi");
33
34
  var name = "group-verification";
35
+ var logger = console;
34
36
  var Config = import_koishi.Schema.object({
35
37
  defaultReminderMessage: import_koishi.Schema.string().description("默认提醒消息模板").default("{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}"),
36
38
  enableStrictGroupCheck: import_koishi.Schema.boolean().description("是否启用严格的群号检查(检查群号长度)").default(false),
@@ -140,7 +142,7 @@ function validateKeywordFormat(raw) {
140
142
  return true;
141
143
  }
142
144
  __name(validateKeywordFormat, "validateKeywordFormat");
143
- function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasRealEnableMessageParam, hasRealDisableMessageParam, logger) {
145
+ function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasRealEnableMessageParam, hasRealDisableMessageParam, logger2) {
144
146
  let reminderEnabled = true;
145
147
  let reminderMessage = "{user}({id}) 申请加入群 {gname}({group})\n申请理由:{question}\n匹配情况:{answer}/{threshold}";
146
148
  if (existingConfig) {
@@ -149,15 +151,15 @@ function mergeReminder(existingConfig, cleanedOptions, hasRealMessageParam, hasR
149
151
  }
150
152
  if (hasRealDisableMessageParam) {
151
153
  reminderEnabled = false;
152
- logger.info("禁用提醒消息功能 (保留现有内容)");
154
+ logger2.info("禁用提醒消息功能 (保留现有内容)");
153
155
  } else if (hasRealEnableMessageParam) {
154
156
  reminderEnabled = true;
155
- logger.info(`启用提醒消息(保留原消息): ${reminderMessage.substring(0, 50)}...`);
157
+ logger2.info(`启用提醒消息(保留原消息): ${reminderMessage.substring(0, 50)}...`);
156
158
  } else if (hasRealMessageParam) {
157
159
  reminderEnabled = true;
158
160
  if (cleanedOptions.message !== void 0) {
159
161
  reminderMessage = cleanedOptions.message.replace(/\\n/g, "\n");
160
- logger.info(`设置自定义提醒消息: ${reminderMessage.substring(0, 50)}...`);
162
+ logger2.info(`设置自定义提醒消息: ${reminderMessage.substring(0, 50)}...`);
161
163
  }
162
164
  }
163
165
  return { reminderEnabled, reminderMessage };
@@ -247,6 +249,44 @@ function parseConfigArgs(raw) {
247
249
  return { keywords, flags, error };
248
250
  }
249
251
  __name(parseConfigArgs, "parseConfigArgs");
252
+ async function verifyApplication(config, message, session) {
253
+ const lowered = message.toLowerCase();
254
+ const matched = /* @__PURE__ */ new Set();
255
+ for (const keyword of config.keywords) {
256
+ if (lowered.includes(keyword.toLowerCase())) {
257
+ matched.add(keyword);
258
+ }
259
+ }
260
+ const matchedCount = matched.size;
261
+ let isValid = false;
262
+ let requiredThreshold = "";
263
+ switch (config.reviewMethod) {
264
+ case 0:
265
+ isValid = true;
266
+ requiredThreshold = "null";
267
+ break;
268
+ case 1:
269
+ isValid = matchedCount >= (config.reviewParameters || 1);
270
+ requiredThreshold = `${config.reviewParameters || 1}`;
271
+ break;
272
+ case 2:
273
+ const ratio = matchedCount / config.keywords.length;
274
+ const requiredRatio = (config.reviewParameters || 100) / 100;
275
+ isValid = ratio >= requiredRatio;
276
+ requiredThreshold = `${config.reviewParameters || 100}%`;
277
+ break;
278
+ case 3:
279
+ isValid = false;
280
+ requiredThreshold = "null";
281
+ break;
282
+ default:
283
+ isValid = false;
284
+ requiredThreshold = "null";
285
+ }
286
+ logger.info(`verifyApplication msg="${message}" keywords=${JSON.stringify(config.keywords)} matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
287
+ return { isValid, matchedCount, requiredThreshold };
288
+ }
289
+ __name(verifyApplication, "verifyApplication");
250
290
  function apply(ctx, config) {
251
291
  ctx.model.extend("group_verification_config", {
252
292
  id: "unsigned",
@@ -259,13 +299,13 @@ function apply(ctx, config) {
259
299
  reminderMessage: "string",
260
300
  createdBy: "string",
261
301
  updatedBy: "string",
262
- createdAt: "date",
263
- updatedAt: "date"
302
+ createdAt: "string",
303
+ updatedAt: "string"
264
304
  }, {
265
305
  primary: "id",
266
306
  autoInc: true
267
307
  });
268
- const logger = ctx.logger("group-verification");
308
+ logger = ctx.logger("group-verification");
269
309
  logger.info("群组验证插件已启动");
270
310
  logger.info(`默认提醒消息: ${config.defaultReminderMessage}`);
271
311
  logger.info(`严格群号检查: ${config.enableStrictGroupCheck ? "启用" : "禁用"}`);
@@ -276,7 +316,9 @@ function apply(ctx, config) {
276
316
  autoApproved: "integer",
277
317
  manuallyApproved: "integer",
278
318
  rejected: "integer",
279
- lastUpdated: "date"
319
+ // store as string (ISO timestamp) to preserve full date+time;
320
+ // Koishi `date` type truncates to day which leads to 00:00:00.
321
+ lastUpdated: "string"
280
322
  }, {
281
323
  primary: "id",
282
324
  autoInc: true
@@ -287,11 +329,13 @@ function apply(ctx, config) {
287
329
  userId: "string",
288
330
  userName: "string",
289
331
  requestMessage: "string",
290
- applyTime: "date"
332
+ // record full timestamp as string to keep time component
333
+ applyTime: "string"
291
334
  }, {
292
335
  primary: "id",
293
336
  autoInc: true
294
337
  });
338
+ const autoQueue = /* @__PURE__ */ new Map();
295
339
  ctx.on("guild-member-request", async (session) => {
296
340
  logger.debug("guild-member-request event", session);
297
341
  let guildId = (session.guildId || session.channelId || "").toString().trim();
@@ -302,16 +346,6 @@ function apply(ctx, config) {
302
346
  return;
303
347
  }
304
348
  const requestId = session.event?.requestId || session.messageId || "";
305
- if (requestId) {
306
- try {
307
- await session.bot.handleGuildMemberRequest(requestId, true);
308
- logger.info(`自动同意申请 requestId=${requestId}`);
309
- await updateStats(guildId, "autoApproved");
310
- return;
311
- } catch (e) {
312
- logger.warn("自动同意失败", e);
313
- }
314
- }
315
349
  const groupConfig = await ctx.database.get("group_verification_config", {
316
350
  groupId: guildId
317
351
  });
@@ -320,16 +354,18 @@ function apply(ctx, config) {
320
354
  }
321
355
  const config2 = groupConfig[0];
322
356
  const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config2, message, session);
357
+ logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
323
358
  if (isValid) {
324
359
  if (requestId) {
325
360
  try {
326
361
  await session.bot.handleGuildMemberRequest(requestId, true);
327
- logger.info(`自动同意申请 requestId=${requestId}`);
362
+ logger.info(`自动同意 requestId=${requestId}`);
363
+ if (!autoQueue.has(guildId)) autoQueue.set(guildId, /* @__PURE__ */ new Set());
364
+ autoQueue.get(guildId).add(userId);
328
365
  } catch (e) {
329
366
  logger.warn("自动同意失败", e);
330
367
  }
331
368
  }
332
- await updateStats(guildId, "autoApproved");
333
369
  } else {
334
370
  await handleFailedVerification(ctx, session, config2);
335
371
  }
@@ -337,6 +373,13 @@ function apply(ctx, config) {
337
373
  ctx.on("guild-member-added", async (session) => {
338
374
  const groupId = session.guildId;
339
375
  const userId = session.userId;
376
+ const set = autoQueue.get(groupId);
377
+ if (set && set.has(userId)) {
378
+ await updateStats(groupId, "autoApproved");
379
+ set.delete(userId);
380
+ logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`);
381
+ return;
382
+ }
340
383
  const pendingRecords = await ctx.database.get("group_verification_pending", {
341
384
  groupId,
342
385
  userId
@@ -350,29 +393,30 @@ function apply(ctx, config) {
350
393
  logger.info(`用户 ${userId} 被手动邀请加入群 ${groupId},手动批准统计已更新`);
351
394
  }
352
395
  });
353
- async function handleFailedVerification(ctx2, session, config2) {
354
- const guildId = session.guildId;
396
+ async function handleFailedVerification(ctx2, session, config2, matchedCount, requiredThreshold) {
397
+ const guildId = (session.guildId || session.channelId || "").toString().trim();
355
398
  const userId = session.userId;
356
399
  const username = session.username || "未知用户";
357
400
  const message = session.content || "";
401
+ logger.info(`处理失败验证 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold}`);
402
+ if (matchedCount === void 0 || requiredThreshold === void 0) {
403
+ const result = await verifyApplication(config2, message, session);
404
+ matchedCount = result.matchedCount;
405
+ requiredThreshold = result.requiredThreshold;
406
+ }
358
407
  let groupName = "未知群组";
359
408
  try {
360
409
  const guild = await session.bot.getGuild(guildId);
361
410
  groupName = guild.name || groupName;
362
411
  } catch (error) {
363
412
  }
364
- const { matchedCount, requiredThreshold } = await verifyApplication(config2, message, session);
365
413
  await ctx2.database.create("group_verification_pending", {
366
414
  groupId: guildId,
367
415
  userId,
368
416
  userName: username,
369
417
  requestMessage: message,
370
- applyTime: /* @__PURE__ */ new Date()
418
+ applyTime: (/* @__PURE__ */ new Date()).toISOString()
371
419
  });
372
- if (!guildId) {
373
- logger.warn("handleFailedVerification 收到无效 guildId,已放弃发送");
374
- return;
375
- }
376
420
  if (!config2.reminderEnabled || !config2.reminderMessage || config2.reminderMessage === "") {
377
421
  logger.info(`群 ${guildId} 的提醒消息已被禁用,跳过发送`);
378
422
  return;
@@ -382,45 +426,13 @@ function apply(ctx, config) {
382
426
  await ctx2.broadcast([guildId], reminderMsg);
383
427
  }
384
428
  __name(handleFailedVerification, "handleFailedVerification");
385
- async function verifyApplication(config2, message, session) {
386
- const matchedCount = config2.keywords.filter(
387
- (keyword) => message.toLowerCase().includes(keyword.toLowerCase())
388
- ).length;
389
- let isValid = false;
390
- let requiredThreshold = "";
391
- switch (config2.reviewMethod) {
392
- case 0:
393
- isValid = true;
394
- requiredThreshold = "null";
395
- break;
396
- case 1:
397
- isValid = matchedCount >= (config2.reviewParameters || 1);
398
- requiredThreshold = `${config2.reviewParameters || 1}`;
399
- break;
400
- case 2:
401
- const ratio = matchedCount / config2.keywords.length;
402
- const requiredRatio = (config2.reviewParameters || 100) / 100;
403
- isValid = ratio >= requiredRatio;
404
- requiredThreshold = `${config2.reviewParameters || 100}%`;
405
- break;
406
- case 3:
407
- isValid = false;
408
- requiredThreshold = "null";
409
- break;
410
- default:
411
- isValid = false;
412
- requiredThreshold = "null";
413
- }
414
- return { isValid, matchedCount, requiredThreshold };
415
- }
416
- __name(verifyApplication, "verifyApplication");
417
429
  async function updateStats(groupId, action) {
418
430
  const existingStats = await ctx.database.get("group_verification_stats", { groupId });
419
431
  if (existingStats.length > 0) {
420
432
  const stats = existingStats[0];
421
433
  await ctx.database.set("group_verification_stats", { id: stats.id }, {
422
434
  [action]: stats[action] + 1,
423
- lastUpdated: /* @__PURE__ */ new Date()
435
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
424
436
  });
425
437
  } else {
426
438
  await ctx.database.create("group_verification_stats", {
@@ -428,7 +440,7 @@ function apply(ctx, config) {
428
440
  autoApproved: action === "autoApproved" ? 1 : 0,
429
441
  manuallyApproved: action === "manuallyApproved" ? 1 : 0,
430
442
  rejected: action === "rejected" ? 1 : 0,
431
- lastUpdated: /* @__PURE__ */ new Date()
443
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
432
444
  });
433
445
  }
434
446
  await syncTotalStats(ctx);
@@ -525,8 +537,8 @@ ${debugInfo}`];
525
537
  remove: flags.remove || options.remove
526
538
  };
527
539
  logger.info(`合并后options: ${JSON.stringify(cleanedOptions, null, 2)}`);
528
- if ((cleanedOptions.query || cleanedOptions.remove) && (parsedKeywords.length > 0 || cleanedOptions.method !== void 0 || cleanedOptions.threshold !== void 0 || cleanedOptions.message !== void 0 || cleanedOptions.enableMessage || cleanedOptions.disableMessage || cleanedOptions.groupId !== void 0)) {
529
- return "参数冲突:-? 或 -r 不能与其他参数或关键词一起使用";
540
+ if ((cleanedOptions.query || cleanedOptions.remove) && (parsedKeywords.length > 0 || cleanedOptions.method !== void 0 || cleanedOptions.threshold !== void 0 || cleanedOptions.message !== void 0 || cleanedOptions.enableMessage || cleanedOptions.disableMessage)) {
541
+ return "参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)";
530
542
  }
531
543
  const hasRealMessageParam = cleanedOptions.message !== void 0;
532
544
  const hasRealEnableMessageParam = cleanedOptions.enableMessage === true;
@@ -805,7 +817,8 @@ gvc -r # 删除配置`;
805
817
  for (const request2 of pendingRequests2) {
806
818
  try {
807
819
  await ctx.database.remove("group_verification_pending", { id: request2.id });
808
- await updateStats(groupId, "manuallyApproved");
820
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
821
+ autoQueue.get(groupId).add(request2.userId);
809
822
  approvedCount++;
810
823
  } catch (error) {
811
824
  logger.warn(`处理申请 ${request2.id} 时出错:`, error);
@@ -821,7 +834,8 @@ gvc -r # 删除配置`;
821
834
  try {
822
835
  await session.bot.handleGuildMemberRequest(request2.userId, true);
823
836
  await ctx.database.remove("group_verification_pending", { id: request2.id });
824
- await updateStats(groupId, "manuallyApproved");
837
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
838
+ autoQueue.get(groupId).add(request2.userId);
825
839
  return `已同意用户 ${request2.userName}(${request2.userId}) 的加群申请`;
826
840
  } catch (error) {
827
841
  return `处理申请时出错: ${error.message}`;
@@ -839,7 +853,8 @@ gvc -r # 删除配置`;
839
853
  try {
840
854
  await session.bot.handleGuildMemberRequest(request.userId, true);
841
855
  await ctx.database.remove("group_verification_pending", { id: request.id });
842
- await updateStats(groupId, "manuallyApproved");
856
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, /* @__PURE__ */ new Set());
857
+ autoQueue.get(groupId).add(userId);
843
858
  return `已同意用户 ${request.userName}(${userId}) 的加群申请`;
844
859
  } catch (error) {
845
860
  return `处理申请时出错: ${error.message}`;
@@ -1092,7 +1107,7 @@ gvc -r # 删除配置`;
1092
1107
  autoApproved: totalAutoApproved,
1093
1108
  manuallyApproved: totalManuallyApproved,
1094
1109
  rejected: totalRejected,
1095
- lastUpdated: /* @__PURE__ */ new Date()
1110
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1096
1111
  });
1097
1112
  logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`);
1098
1113
  }
@@ -1111,5 +1126,6 @@ __name(apply, "apply");
1111
1126
  mergeReminder,
1112
1127
  name,
1113
1128
  parseConfigArgs,
1114
- tokenize
1129
+ tokenize,
1130
+ verifyApplication
1115
1131
  });
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.18",
4
+ "version": "1.0.20",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -48,6 +48,9 @@ group-verify.config -r
48
48
 
49
49
  # 指定群号配置
50
50
  group-verify.config -i 123456789 关键词1,关键词2 -m 1 -t 1
51
+
52
+ > **提示**: `-?`(查询)和 `-r`(删除)仍然与所有其它参数 / 关键字互斥,
53
+ > 但可以和 `-i` 组合使用,例如 `gvc -i 123 -?` 或 `gvc -i 123 -r`。这样便于在私聊环境下查询或删除指定群的设置。
51
54
  ```
52
55
 
53
56
  ### 审核命令
@@ -106,6 +109,12 @@ group-verify.stats total
106
109
  - `-nomsg` - 禁用提醒消息功能
107
110
  - 不带参数的 `-msg` 会显示帮助信息
108
111
 
112
+ ### 提醒消息变量
113
+
114
+ ### 其他注意事项
115
+
116
+ - 统计数据的 **最后更新时间** 使用完整的日期+时间存储,升级到最新版后若看到 `00:00:00`,请手动重建或清除旧的统计记录以便记录最新时间。
117
+
109
118
  ### 提醒消息变量
110
119
  ```
111
120
  {user} - 用户名
package/src/index.ts CHANGED
@@ -2,6 +2,9 @@ import { Context, Schema, Session } from 'koishi'
2
2
 
3
3
  export const name = 'group-verification'
4
4
 
5
+ // 模块级日志器,测试时使用 console
6
+ let logger: any = console
7
+
5
8
  // 数据库模型定义
6
9
  declare module 'koishi' {
7
10
  interface Tables {
@@ -22,8 +25,8 @@ export interface GroupVerificationConfig {
22
25
  reminderMessage: string
23
26
  createdBy: string
24
27
  updatedBy: string
25
- createdAt: Date
26
- updatedAt: Date
28
+ createdAt: string | Date
29
+ updatedAt: string | Date
27
30
  }
28
31
 
29
32
  // 群组统计信息表
@@ -33,7 +36,7 @@ export interface GroupVerificationStats {
33
36
  autoApproved: number
34
37
  manuallyApproved: number
35
38
  rejected: number
36
- lastUpdated: Date
39
+ lastUpdated: string | Date
37
40
  }
38
41
 
39
42
  // 待审核申请表
@@ -43,7 +46,7 @@ export interface PendingVerification {
43
46
  userId: string
44
47
  userName: string
45
48
  requestMessage: string
46
- applyTime: Date
49
+ applyTime: string | Date
47
50
  }
48
51
 
49
52
  export interface Config {
@@ -358,6 +361,50 @@ export function parseConfigArgs(raw: string): ParsedArgs {
358
361
 
359
362
  return { keywords, flags, error };
360
363
  }
364
+
365
+ // 验证申请(提取到外层,供测试调用)
366
+ export async function verifyApplication(config: GroupVerificationConfig, message: string, session: any): Promise<{isValid: boolean, matchedCount: number, requiredThreshold: string}> {
367
+ // 统计匹配的关键词数量(允许相互重叠)
368
+ const lowered = message.toLowerCase()
369
+ const matched = new Set<string>()
370
+ for (const keyword of config.keywords) {
371
+ if (lowered.includes(keyword.toLowerCase())) {
372
+ matched.add(keyword)
373
+ }
374
+ }
375
+ const matchedCount = matched.size
376
+
377
+ let isValid = false
378
+ let requiredThreshold = ''
379
+
380
+ switch (config.reviewMethod) {
381
+ case 0: // 全部同意
382
+ isValid = true
383
+ requiredThreshold = 'null'
384
+ break
385
+ case 1: // 按数量同意
386
+ isValid = matchedCount >= (config.reviewParameters || 1)
387
+ requiredThreshold = `${config.reviewParameters || 1}`
388
+ break
389
+ case 2: // 按比例同意
390
+ const ratio = matchedCount / config.keywords.length
391
+ const requiredRatio = (config.reviewParameters || 100) / 100
392
+ isValid = ratio >= requiredRatio
393
+ requiredThreshold = `${config.reviewParameters || 100}%`
394
+ break
395
+ case 3: // 全部拒绝
396
+ isValid = false
397
+ requiredThreshold = 'null'
398
+ break
399
+ default:
400
+ isValid = false
401
+ requiredThreshold = 'null'
402
+ }
403
+
404
+ logger.info(`verifyApplication msg="${message}" keywords=${JSON.stringify(config.keywords)} matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`)
405
+ return { isValid, matchedCount, requiredThreshold }
406
+ }
407
+
361
408
  export function apply(ctx: Context, config: Config) {
362
409
  // 创建数据库表
363
410
  ctx.model.extend('group_verification_config', {
@@ -370,15 +417,15 @@ export function apply(ctx: Context, config: Config) {
370
417
  reminderMessage: 'string',
371
418
  createdBy: 'string',
372
419
  updatedBy: 'string',
373
- createdAt: 'date',
374
- updatedAt: 'date'
420
+ createdAt: 'string',
421
+ updatedAt: 'string'
375
422
  }, {
376
423
  primary: 'id',
377
424
  autoInc: true
378
425
  })
379
426
 
380
- // 获取logger实例
381
- const logger = ctx.logger('group-verification')
427
+ // 获取logger实例并保存到模块级变量
428
+ logger = ctx.logger('group-verification')
382
429
 
383
430
  // 设置日志级别
384
431
  // 注意:Koishi logger的level设置可能需要不同的方式
@@ -395,7 +442,9 @@ export function apply(ctx: Context, config: Config) {
395
442
  autoApproved: 'integer',
396
443
  manuallyApproved: 'integer',
397
444
  rejected: 'integer',
398
- lastUpdated: 'date'
445
+ // store as string (ISO timestamp) to preserve full date+time;
446
+ // Koishi `date` type truncates to day which leads to 00:00:00.
447
+ lastUpdated: 'string'
399
448
  }, {
400
449
  primary: 'id',
401
450
  autoInc: true
@@ -407,86 +456,85 @@ export function apply(ctx: Context, config: Config) {
407
456
  userId: 'string',
408
457
  userName: 'string',
409
458
  requestMessage: 'string',
410
- applyTime: 'date'
459
+ // record full timestamp as string to keep time component
460
+ applyTime: 'string'
411
461
  }, {
412
462
  primary: 'id',
413
463
  autoInc: true
414
464
  })
415
465
 
466
+ // 缓存正在审批的用户,用于避免 guild-member-added 重复计数
467
+ const autoQueue = new Map<string, Set<string>>();
468
+
416
469
  // 监听加群申请事件
417
470
  ctx.on('guild-member-request', async (session) => {
418
471
  // debug: 输出整个 session 以便定位字段名
419
472
  logger.debug('guild-member-request event', session)
420
473
 
421
- // session.guildId 有时可能带空格或为空,先做 trim 并备选 channelId
422
- let guildId = (session.guildId || session.channelId || '').toString().trim()
423
- const userId = session.userId
424
- const message = session.content || ''
474
+ let guildId = (session.guildId || session.channelId || '').toString().trim();
475
+ const userId = session.userId;
476
+ const message = session.content || '';
425
477
 
426
478
  if (!guildId) {
427
- logger.warn('guild-member-request 没有 guildId,跳过处理')
428
- return
479
+ logger.warn('guild-member-request 没有 guildId,跳过处理');
480
+ return;
429
481
  }
430
482
 
431
- // 尝试自动同意,如 event 中包含 requestId
432
- const requestId = ((session.event as any)?.requestId) || session.messageId || ''
433
- if (requestId) {
434
- try {
435
- await session.bot.handleGuildMemberRequest(requestId, true)
436
- logger.info(`自动同意申请 requestId=${requestId}`)
437
- await updateStats(guildId, 'autoApproved')
438
- // 不再执行后续验证或提醒
439
- return
440
- } catch (e) {
441
- logger.warn('自动同意失败', e)
442
- // 继续下面的验证逻辑
443
- }
444
- }
483
+ // 获取 requestId(不同平台字段不同)
484
+ const requestId = ((session.event as any)?.requestId) || session.messageId || '';
445
485
 
446
486
  // 获取群组配置
447
487
  const groupConfig = await ctx.database.get('group_verification_config', {
448
488
  groupId: guildId
449
- })
489
+ });
450
490
 
451
491
  if (!groupConfig || groupConfig.length === 0) {
452
- // 如果没有配置,默认同意(以上 auto-approve 未触发时仍然需要一个 requestId)
453
- return
492
+ // 无配置直接放行(需要有 requestId)
493
+ return;
454
494
  }
495
+ const config = groupConfig[0];
455
496
 
456
- const config = groupConfig[0]
457
-
458
- // 执行验证
459
- const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session)
497
+ // 执行验证,记录详细情况
498
+ const { isValid, matchedCount, requiredThreshold } = await verifyApplication(config, message, session);
499
+ logger.info(`验证结果 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold} valid=${isValid}`);
460
500
 
461
501
  if (isValid) {
462
- // 验证成功,自动同意入群(同样需要 requestId)
463
502
  if (requestId) {
464
503
  try {
465
- await session.bot.handleGuildMemberRequest(requestId, true)
466
- logger.info(`自动同意申请 requestId=${requestId}`)
504
+ await session.bot.handleGuildMemberRequest(requestId, true);
505
+ logger.info(`自动同意 requestId=${requestId}`);
506
+ // 将此用户标记为自动批准,等待 guild-member-added 更新统计
507
+ if (!autoQueue.has(guildId)) autoQueue.set(guildId, new Set());
508
+ autoQueue.get(guildId)!.add(userId);
467
509
  } catch (e) {
468
- logger.warn('自动同意失败', e)
510
+ logger.warn('自动同意失败', e);
469
511
  }
470
512
  }
471
- // 更新统计信息
472
- await updateStats(guildId, 'autoApproved')
473
513
  } else {
474
- // 验证失败,发送提醒到群内
475
- await handleFailedVerification(ctx, session, config)
514
+ await handleFailedVerification(ctx, session, config);
476
515
  }
477
- })
516
+ });
478
517
 
479
518
  // 监听群成员增加事件(包括手动邀请入群)
480
519
  ctx.on('guild-member-added', async (session) => {
481
520
  const groupId = session.guildId
482
521
  const userId = session.userId
483
-
522
+
523
+ // 先检查 autoQueue
524
+ const set = autoQueue.get(groupId)
525
+ if (set && set.has(userId)) {
526
+ await updateStats(groupId, 'autoApproved')
527
+ set.delete(userId)
528
+ logger.info(`用户 ${userId} 通过机器人审批加入群 ${groupId}(autoQueue),统计已更新`)
529
+ return
530
+ }
531
+
484
532
  // 检查是否有待审核记录
485
533
  const pendingRecords = await ctx.database.get('group_verification_pending', {
486
534
  groupId: groupId,
487
535
  userId: userId
488
536
  })
489
-
537
+
490
538
  if (pendingRecords.length > 0) {
491
539
  // 通过验证的用户入群,更新统计
492
540
  await updateStats(groupId, 'autoApproved')
@@ -501,12 +549,27 @@ export function apply(ctx: Context, config: Config) {
501
549
  })
502
550
 
503
551
  // 处理验证失败的情况
504
- async function handleFailedVerification(ctx: Context, session: any, config: GroupVerificationConfig) {
505
- const guildId = session.guildId
552
+ // 处理验证失败的情况并发送提醒消息
553
+ // matchedCount/requiredThreshold 可选,为空时内部重新计算(兼容旧调用)
554
+ async function handleFailedVerification(
555
+ ctx: Context,
556
+ session: any,
557
+ config: GroupVerificationConfig,
558
+ matchedCount?: number,
559
+ requiredThreshold?: string
560
+ ) {
561
+ const guildId = (session.guildId || session.channelId || '').toString().trim();
506
562
  const userId = session.userId
507
563
  const username = session.username || '未知用户'
508
564
  const message = session.content || ''
509
-
565
+ logger.info(`处理失败验证 guild=${guildId} user=${userId} msg="${message}" matched=${matchedCount} threshold=${requiredThreshold}`)
566
+ // 如果未传入匹配信息,则重新计算一次(老调用)
567
+ if (matchedCount === undefined || requiredThreshold === undefined) {
568
+ const result = await verifyApplication(config, message, session)
569
+ matchedCount = result.matchedCount
570
+ requiredThreshold = result.requiredThreshold
571
+ }
572
+
510
573
  // 获取群信息
511
574
  let groupName = '未知群组'
512
575
  try {
@@ -515,30 +578,21 @@ export function apply(ctx: Context, config: Config) {
515
578
  } catch (error) {
516
579
  // 无法获取群名称时使用默认值
517
580
  }
518
-
519
- // 执行验证获取详细信息
520
- const { matchedCount, requiredThreshold } = await verifyApplication(config, message, session)
521
-
581
+
522
582
  // 将申请加入待审核列表
523
583
  await ctx.database.create('group_verification_pending', {
524
584
  groupId: guildId,
525
585
  userId: userId,
526
586
  userName: username,
527
587
  requestMessage: message,
528
- applyTime: new Date()
588
+ applyTime: new Date().toISOString()
529
589
  })
530
-
531
- // 如果 guildId 不合法,跳过
532
- if (!guildId) {
533
- logger.warn('handleFailedVerification 收到无效 guildId,已放弃发送')
534
- return
535
- }
536
590
  // 如果提醒消息被禁用,直接返回
537
591
  if (!config.reminderEnabled || !config.reminderMessage || config.reminderMessage === '') {
538
592
  logger.info(`群 ${guildId} 的提醒消息已被禁用,跳过发送`)
539
593
  return
540
594
  }
541
-
595
+
542
596
  // 替换提醒消息中的变量
543
597
  let reminderMsg = config.reminderMessage
544
598
  reminderMsg = reminderMsg
@@ -547,49 +601,13 @@ export function apply(ctx: Context, config: Config) {
547
601
  .replace(/{group}/g, guildId)
548
602
  .replace(/{gname}/g, groupName)
549
603
  .replace(/{question}/g, message)
550
- .replace(/{answer}/g, matchedCount.toString())
551
- .replace(/{threshold}/g, requiredThreshold)
552
-
604
+ .replace(/{answer}/g, matchedCount!.toString())
605
+ .replace(/{threshold}/g, requiredThreshold!)
606
+
553
607
  // 发送提醒消息到群内
554
608
  await ctx.broadcast([guildId], reminderMsg)
555
609
  }
556
610
 
557
- // 验证申请
558
- async function verifyApplication(config: GroupVerificationConfig, message: string, session: any): Promise<{isValid: boolean, matchedCount: number, requiredThreshold: string}> {
559
- // 统计匹配的关键词数量
560
- const matchedCount = config.keywords.filter(keyword =>
561
- message.toLowerCase().includes(keyword.toLowerCase())
562
- ).length
563
-
564
- let isValid = false
565
- let requiredThreshold = ''
566
-
567
- switch (config.reviewMethod) {
568
- case 0: // 全部同意
569
- isValid = true
570
- requiredThreshold = 'null'
571
- break
572
- case 1: // 按数量同意
573
- isValid = matchedCount >= (config.reviewParameters || 1)
574
- requiredThreshold = `${config.reviewParameters || 1}`
575
- break
576
- case 2: // 按比例同意
577
- const ratio = matchedCount / config.keywords.length
578
- const requiredRatio = (config.reviewParameters || 100) / 100
579
- isValid = ratio >= requiredRatio
580
- requiredThreshold = `${config.reviewParameters || 100}%`
581
- break
582
- case 3: // 全部拒绝
583
- isValid = false
584
- requiredThreshold = 'null'
585
- break
586
- default:
587
- isValid = false
588
- requiredThreshold = 'null'
589
- }
590
-
591
- return { isValid, matchedCount, requiredThreshold }
592
- }
593
611
 
594
612
  // 更新统计信息
595
613
  async function updateStats(groupId: string, action: 'autoApproved' | 'manuallyApproved' | 'rejected') {
@@ -600,7 +618,7 @@ export function apply(ctx: Context, config: Config) {
600
618
  const stats = existingStats[0]
601
619
  await ctx.database.set('group_verification_stats', { id: stats.id }, {
602
620
  [action]: stats[action] + 1,
603
- lastUpdated: new Date()
621
+ lastUpdated: new Date().toISOString()
604
622
  })
605
623
  } else {
606
624
  await ctx.database.create('group_verification_stats', {
@@ -608,7 +626,7 @@ export function apply(ctx: Context, config: Config) {
608
626
  autoApproved: action === 'autoApproved' ? 1 : 0,
609
627
  manuallyApproved: action === 'manuallyApproved' ? 1 : 0,
610
628
  rejected: action === 'rejected' ? 1 : 0,
611
- lastUpdated: new Date()
629
+ lastUpdated: new Date().toISOString()
612
630
  })
613
631
  }
614
632
 
@@ -746,11 +764,11 @@ export function apply(ctx: Context, config: Config) {
746
764
  }
747
765
  logger.info(`合并后options: ${JSON.stringify(cleanedOptions, null, 2)}`)
748
766
 
749
- // 检查 -? 和 -r 的独占性
767
+ // 检查 -? 和 -r 的独占性;允许与 -i 并存
750
768
  if ((cleanedOptions.query || cleanedOptions.remove) &&
751
769
  (parsedKeywords.length > 0 || cleanedOptions.method !== undefined || cleanedOptions.threshold !== undefined ||
752
- cleanedOptions.message !== undefined || cleanedOptions.enableMessage || cleanedOptions.disableMessage || cleanedOptions.groupId !== undefined)) {
753
- return '参数冲突:-? 或 -r 不能与其他参数或关键词一起使用'
770
+ cleanedOptions.message !== undefined || cleanedOptions.enableMessage || cleanedOptions.disableMessage)) {
771
+ return '参数冲突:-? 或 -r 不能与其他参数或关键词一起使用(仅可搭配 -i)'
754
772
  }
755
773
 
756
774
  // 检查消息参数冲突
@@ -1117,7 +1135,9 @@ gvc -r # 删除配置`
1117
1135
  // TODO: 需要获取实际的requestId来进行审批
1118
1136
  // await session.bot.handleGuildMemberRequest(request.requestId, true)
1119
1137
  await ctx.database.remove('group_verification_pending', { id: request.id })
1120
- await updateStats(groupId, 'manuallyApproved')
1138
+ // 标记为自动批准,实际统计在 guild-member-added 处理
1139
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1140
+ autoQueue.get(groupId)!.add(request.userId)
1121
1141
  approvedCount++
1122
1142
  } catch (error) {
1123
1143
  logger.warn(`处理申请 ${request.id} 时出错:`, error)
@@ -1137,7 +1157,8 @@ gvc -r # 删除配置`
1137
1157
  // 这里需要获取实际的requestId,暂时用userId作为示例
1138
1158
  await session.bot.handleGuildMemberRequest(request.userId, true)
1139
1159
  await ctx.database.remove('group_verification_pending', { id: request.id })
1140
- await updateStats(groupId, 'manuallyApproved')
1160
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1161
+ autoQueue.get(groupId)!.add(request.userId)
1141
1162
  return `已同意用户 ${request.userName}(${request.userId}) 的加群申请`
1142
1163
  } catch (error) {
1143
1164
  return `处理申请时出错: ${error.message}`
@@ -1160,7 +1181,8 @@ gvc -r # 删除配置`
1160
1181
  // 这里需要获取实际的requestId来进行审批
1161
1182
  await session.bot.handleGuildMemberRequest(request.userId, true)
1162
1183
  await ctx.database.remove('group_verification_pending', { id: request.id })
1163
- await updateStats(groupId, 'manuallyApproved')
1184
+ if (!autoQueue.has(groupId)) autoQueue.set(groupId, new Set())
1185
+ autoQueue.get(groupId)!.add(userId)
1164
1186
  return `已同意用户 ${request.userName}(${userId}) 的加群申请`
1165
1187
  } catch (error) {
1166
1188
  return `处理申请时出错: ${error.message}`
@@ -1482,7 +1504,7 @@ gvc -r # 删除配置`
1482
1504
  autoApproved: totalAutoApproved,
1483
1505
  manuallyApproved: totalManuallyApproved,
1484
1506
  rejected: totalRejected,
1485
- lastUpdated: new Date()
1507
+ lastUpdated: new Date().toISOString()
1486
1508
  })
1487
1509
 
1488
1510
  logger.info(`总计统计已同步: 自动批准${totalAutoApproved}, 手动批准${totalManuallyApproved}, 拒绝${totalRejected}`)