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 +9 -4
- package/lib/index.js +87 -71
- package/package.json +1 -1
- package/readme.md +9 -0
- package/src/index.ts +133 -111
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,
|
|
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
|
-
|
|
154
|
+
logger2.info("禁用提醒消息功能 (保留现有内容)");
|
|
153
155
|
} else if (hasRealEnableMessageParam) {
|
|
154
156
|
reminderEnabled = true;
|
|
155
|
-
|
|
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
|
-
|
|
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: "
|
|
263
|
-
updatedAt: "
|
|
302
|
+
createdAt: "string",
|
|
303
|
+
updatedAt: "string"
|
|
264
304
|
}, {
|
|
265
305
|
primary: "id",
|
|
266
306
|
autoInc: true
|
|
267
307
|
});
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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: '
|
|
374
|
-
updatedAt: '
|
|
420
|
+
createdAt: 'string',
|
|
421
|
+
updatedAt: 'string'
|
|
375
422
|
}, {
|
|
376
423
|
primary: 'id',
|
|
377
424
|
autoInc: true
|
|
378
425
|
})
|
|
379
426
|
|
|
380
|
-
// 获取logger
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
453
|
-
return
|
|
492
|
+
// 无配置直接放行(需要有 requestId)
|
|
493
|
+
return;
|
|
454
494
|
}
|
|
495
|
+
const config = groupConfig[0];
|
|
455
496
|
|
|
456
|
-
|
|
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(
|
|
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
|
-
|
|
505
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`)
|