koishi-plugin-group-control 0.2.7 → 0.2.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 muyni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/config.d.ts CHANGED
@@ -5,13 +5,13 @@ export interface GroupConfig {
5
5
  quitMessage: string;
6
6
  enableBlacklist: boolean;
7
7
  quitCommandEnabled: boolean;
8
- quitCommandAuthority: number;
9
8
  notifyAdminOnKick: boolean;
10
9
  kickNotificationMessage: string;
11
10
  smallGroupAutoQuit: boolean;
12
11
  smallGroupThreshold: number;
13
12
  smallGroupQuitMessage: string;
14
13
  smallGroupNotifyAdmin: boolean;
14
+ smallGroupCheckDelay: number;
15
15
  }
16
16
  export interface GroupInviteConfig {
17
17
  enabled: boolean;
@@ -37,12 +37,17 @@ export interface BotSwitchConfig {
37
37
  enabled: boolean;
38
38
  defaultState: boolean;
39
39
  disabledMessage: string;
40
- toggleAuthority: number;
40
+ }
41
+ export interface PermissionConfig {
42
+ mode: 'koishi' | 'builtin';
43
+ koishiAuthority: number;
44
+ protectedCommands: string[];
41
45
  }
42
46
  export interface Config {
43
47
  basic: GroupConfig;
44
48
  frequency: FrequencyConfig;
45
49
  invite: GroupInviteConfig;
46
50
  botSwitch: BotSwitchConfig;
51
+ permission: PermissionConfig;
47
52
  }
48
53
  export declare const Config: Schema<Config>;
package/lib/database.d.ts CHANGED
@@ -31,7 +31,7 @@ export declare function apply(ctx: Context): void;
31
31
  export declare const BLACKLIST_PLATFORM = "onebot";
32
32
  export declare function getBlacklistedGuild(ctx: Context, guildId: string): Promise<BlacklistedGuild[]>;
33
33
  export declare function removeBlacklistedGuild(ctx: Context, guildId: string): Promise<import("minato").Driver.WriteResult>;
34
- export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<BlacklistedGuild>;
34
+ export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<import("minato").Driver.WriteResult>;
35
35
  export declare function getAllBlacklistedGuilds(ctx: Context): Promise<BlacklistedGuild[]>;
36
36
  export declare function clearBlacklistedGuilds(ctx: Context): Promise<import("minato").Driver.WriteResult>;
37
37
  export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
package/lib/index.js CHANGED
@@ -76,12 +76,12 @@ async function removeBlacklistedGuild(ctx, guildId) {
76
76
  }
77
77
  __name(removeBlacklistedGuild, "removeBlacklistedGuild");
78
78
  async function createBlacklistedGuild(ctx, guildId, reason) {
79
- return await ctx.model.create("blacklisted_guild", {
79
+ return await ctx.model.upsert("blacklisted_guild", [{
80
80
  platform: BLACKLIST_PLATFORM,
81
81
  guildId,
82
82
  timestamp: Math.floor(Date.now() / 1e3),
83
83
  reason
84
- });
84
+ }]);
85
85
  }
86
86
  __name(createBlacklistedGuild, "createBlacklistedGuild");
87
87
  async function getAllBlacklistedGuilds(ctx) {
@@ -157,11 +157,76 @@ async function notifyAdmins(bot, config, message) {
157
157
  }
158
158
  }
159
159
  __name(notifyAdmins, "notifyAdmins");
160
+ async function hasPermission(session, config) {
161
+ if (config.permission.mode === "koishi") {
162
+ try {
163
+ const user = session.user;
164
+ if (user && typeof user.authority === "number") {
165
+ return user.authority >= config.permission.koishiAuthority;
166
+ }
167
+ } catch {
168
+ }
169
+ return false;
170
+ }
171
+ const userId = session.userId;
172
+ if (config.invite.adminQQs?.includes(userId)) {
173
+ return true;
174
+ }
175
+ try {
176
+ const member = await session.bot.getGuildMember(session.guildId, userId);
177
+ const roles = member?.roles || member?.role;
178
+ if (roles) {
179
+ if (Array.isArray(roles)) {
180
+ return roles.some((r) => r === "admin" || r === "owner");
181
+ }
182
+ return roles === "admin" || roles === "owner" || roles === "administrator";
183
+ }
184
+ const role = member?.role;
185
+ if (role === "admin" || role === "owner") return true;
186
+ } catch (error) {
187
+ try {
188
+ const info = await session.bot.internal?.getGroupMemberInfo?.(
189
+ parseInt(session.guildId),
190
+ parseInt(userId)
191
+ );
192
+ if (info) {
193
+ return info.role === "admin" || info.role === "owner";
194
+ }
195
+ } catch {
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+ __name(hasPermission, "hasPermission");
201
+ var ADMIN_COMMANDS = /* @__PURE__ */ new Set([
202
+ "bot-on",
203
+ "bot-off",
204
+ "quit",
205
+ "view-blacklist",
206
+ "remove-from-blacklist",
207
+ "add-to-blacklist",
208
+ "clear-blacklist",
209
+ "approve",
210
+ "reject",
211
+ "pending-invites"
212
+ ]);
160
213
 
161
214
  // src/modules/basic.ts
162
215
  var name2 = "group-control-basic";
163
216
  function apply2(ctx, config) {
164
- const quittingGuilds = /* @__PURE__ */ new Set();
217
+ const quittingGuilds = /* @__PURE__ */ new Map();
218
+ const processedKicks = /* @__PURE__ */ new Map();
219
+ const QUITTING_EXPIRE_MS = 60 * 1e3;
220
+ const KICK_DEDUP_MS = 60 * 1e3;
221
+ setInterval(() => {
222
+ const now = Date.now();
223
+ for (const [key, time] of quittingGuilds) {
224
+ if (now - time > QUITTING_EXPIRE_MS) quittingGuilds.delete(key);
225
+ }
226
+ for (const [key, time] of processedKicks) {
227
+ if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
228
+ }
229
+ }, 30 * 1e3);
165
230
  ctx.on("guild-added", async (session) => {
166
231
  const { guildId, platform } = session;
167
232
  if (config.basic.enableBlacklist) {
@@ -171,7 +236,7 @@ function apply2(ctx, config) {
171
236
  await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
172
237
  } catch (e) {
173
238
  }
174
- quittingGuilds.add(`${platform}:${guildId}`);
239
+ quittingGuilds.set(`${platform}:${guildId}`, Date.now());
175
240
  try {
176
241
  await session.bot.internal.setGroupLeave(parseInt(guildId));
177
242
  } catch (e) {
@@ -180,34 +245,54 @@ function apply2(ctx, config) {
180
245
  }
181
246
  }
182
247
  if (config.basic.smallGroupAutoQuit) {
183
- try {
184
- const guildInfo = await session.bot.getGuild(guildId);
185
- const memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
186
- if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
187
- const quitMsg = config.basic.smallGroupQuitMessage.replace("{memberCount}", memberCount.toString()).replace("{threshold}", config.basic.smallGroupThreshold.toString());
248
+ const delay = config.basic.smallGroupCheckDelay || 3e3;
249
+ setTimeout(async () => {
250
+ try {
251
+ let memberCount = 0;
188
252
  try {
189
- await session.bot.sendMessage(guildId, quitMsg, platform);
190
- } catch (e) {
253
+ const groupInfo = await session.bot.internal?.getGroupInfo?.(parseInt(guildId));
254
+ memberCount = groupInfo?.member_count || 0;
255
+ } catch {
191
256
  }
192
- if (config.basic.smallGroupNotifyAdmin) {
193
- const adminMsg = `小群自动退群
257
+ if (memberCount === 0) {
258
+ try {
259
+ const guildInfo = await session.bot.getGuild(guildId);
260
+ memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
261
+ } catch {
262
+ }
263
+ }
264
+ if (memberCount === 0) {
265
+ try {
266
+ const memberList = await session.bot.getGuildMemberList(guildId);
267
+ memberCount = memberList?.data?.length || 0;
268
+ } catch {
269
+ }
270
+ }
271
+ if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
272
+ const quitMsg = config.basic.smallGroupQuitMessage.replace("{memberCount}", memberCount.toString()).replace("{threshold}", config.basic.smallGroupThreshold.toString());
273
+ try {
274
+ await session.bot.sendMessage(guildId, quitMsg, platform);
275
+ } catch (e) {
276
+ }
277
+ if (config.basic.smallGroupNotifyAdmin) {
278
+ const adminMsg = `小群自动退群
194
279
  群号:${guildId}
195
280
  群成员数:${memberCount}人(阈值:${config.basic.smallGroupThreshold}人)
196
281
  机器人已自动退出该群。`;
197
- await notifyAdmins(session.bot, config, adminMsg);
198
- }
199
- quittingGuilds.add(`${platform}:${guildId}`);
200
- try {
201
- await session.bot.internal.setGroupLeave(parseInt(guildId));
202
- } catch (e) {
203
- console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
204
- quittingGuilds.delete(`${platform}:${guildId}`);
282
+ await notifyAdmins(session.bot, config, adminMsg);
283
+ }
284
+ quittingGuilds.set(`${platform}:${guildId}`, Date.now());
285
+ try {
286
+ await session.bot.internal.setGroupLeave(parseInt(guildId));
287
+ } catch (e) {
288
+ console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
289
+ quittingGuilds.delete(`${platform}:${guildId}`);
290
+ }
205
291
  }
206
- return;
292
+ } catch (error) {
293
+ console.error(`小群自动退群检测失败 (群号: ${guildId}):`, error);
207
294
  }
208
- } catch (error) {
209
- console.error(`获取群信息失败 (群号: ${guildId}):`, error);
210
- }
295
+ }, delay);
211
296
  }
212
297
  if (config.basic.welcomeMessage) {
213
298
  try {
@@ -220,9 +305,12 @@ function apply2(ctx, config) {
220
305
  const { guildId, platform } = session;
221
306
  const quittingKey = `${platform}:${guildId}`;
222
307
  if (quittingGuilds.has(quittingKey)) {
223
- quittingGuilds.delete(quittingKey);
224
308
  return;
225
309
  }
310
+ if (processedKicks.has(quittingKey)) {
311
+ return;
312
+ }
313
+ processedKicks.set(quittingKey, Date.now());
226
314
  if (config.basic.enableBlacklist) {
227
315
  await ctx.model.upsert("blacklisted_guild", [{
228
316
  platform,
@@ -237,10 +325,21 @@ function apply2(ctx, config) {
237
325
  }
238
326
  });
239
327
  if (config.basic.quitCommandEnabled) {
240
- ctx.command("quit", "让机器人主动退出当前群聊", { authority: config.basic.quitCommandAuthority }).action(async ({ session }) => {
328
+ const cmdOpts = {};
329
+ if (config.permission.mode === "koishi") {
330
+ cmdOpts.authority = config.permission.koishiAuthority;
331
+ }
332
+ ctx.command("quit", "让机器人主动退出当前群聊", cmdOpts).action(async ({ session }) => {
241
333
  if (!session.guildId) return "quit 指令只能在群聊中使用。";
334
+ if (config.permission.mode === "builtin") {
335
+ const hasPerm = await hasPermission(session, config);
336
+ if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
337
+ }
242
338
  const { guildId, platform, userId } = session;
243
- quittingGuilds.add(`${platform}:${guildId}`);
339
+ const adminMsg = `收到来自 ${userId} 的退群指令
340
+ 群号:${guildId}`;
341
+ await notifyAdmins(session.bot, config, adminMsg);
342
+ quittingGuilds.set(`${platform}:${guildId}`, Date.now());
244
343
  try {
245
344
  await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
246
345
  } catch (e) {
@@ -267,6 +366,18 @@ var name3 = "group-control-invite";
267
366
  function apply3(ctx, config) {
268
367
  if (!config.invite.enabled) return;
269
368
  const pendingInvites = /* @__PURE__ */ new Map();
369
+ const INVITE_TIMEOUT = 10 * 60 * 1e3;
370
+ setInterval(() => {
371
+ const now = Date.now();
372
+ for (const [key, invite] of pendingInvites) {
373
+ if (now - invite.time > INVITE_TIMEOUT) {
374
+ pendingInvites.delete(key);
375
+ if (config.invite.showDetailedLog) {
376
+ console.log(`邀请超时已清理: 群号=${invite.groupId}, 邀请者=${invite.userId}`);
377
+ }
378
+ }
379
+ }
380
+ }, 60 * 1e3);
270
381
  ctx.on("guild-request", async (session) => {
271
382
  const raw = session.original || session.raw || session.event?._data || {};
272
383
  const flag = raw.flag || session.flag || session.messageId;
@@ -294,7 +405,7 @@ function apply3(ctx, config) {
294
405
  console.error("获取群信息失败:", error);
295
406
  }
296
407
  try {
297
- const waitMessage = config.invite.inviteWaitMessage.replace("{groupName}", groupName).replace("{groupId}", rawGroupId).replace("{userName}", userName).replace("{userId}", rawUserId);
408
+ const waitMessage = config.invite.inviteWaitMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", rawGroupId).replaceAll("{userName}", userName).replaceAll("{userId}", rawUserId);
298
409
  await session.bot.sendPrivateMessage(rawUserId, waitMessage);
299
410
  } catch (error) {
300
411
  console.error(`发送等待审核提示给 ${rawUserId} 失败:`, error);
@@ -302,12 +413,7 @@ function apply3(ctx, config) {
302
413
  if (!config.invite.adminQQs || config.invite.adminQQs.length === 0) {
303
414
  if (config.invite.autoApprove) {
304
415
  try {
305
- await session.bot.internal.setGroupAddRequest({
306
- flag,
307
- sub_type: "invite",
308
- approve: true,
309
- reason: ""
310
- });
416
+ await session.bot.internal.setGroupAddRequest(flag, "invite", true, "");
311
417
  if (config.invite.showDetailedLog) {
312
418
  console.log(`自动同意群聊邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
313
419
  }
@@ -317,15 +423,15 @@ function apply3(ctx, config) {
317
423
  }
318
424
  return;
319
425
  }
320
- const inviteId = `${rawGroupId}_${rawUserId}_${Date.now()}`;
321
- pendingInvites.set(inviteId, {
426
+ pendingInvites.set(rawGroupId, {
322
427
  groupId: rawGroupId,
323
428
  userId: rawUserId,
324
429
  userName,
430
+ groupName,
325
431
  time: Date.now(),
326
432
  flag
327
433
  });
328
- const requestMessage = config.invite.inviteRequestMessage.replace("{groupName}", groupName).replace("{groupId}", rawGroupId).replace("{userName}", userName).replace("{userId}", rawUserId);
434
+ const requestMessage = config.invite.inviteRequestMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", rawGroupId).replaceAll("{userName}", userName).replaceAll("{userId}", rawUserId);
329
435
  let requestSent = false;
330
436
  if (config.invite.notificationGroupId) {
331
437
  try {
@@ -355,117 +461,69 @@ function apply3(ctx, config) {
355
461
  console.warn("群邀请请求发送失败:未配置通知群且管理员私聊发送失败");
356
462
  }
357
463
  });
358
- ctx.on("message", async (session) => {
359
- const { userId, guildId } = session;
360
- if (!config.invite.adminQQs.includes(userId)) return;
361
- const isNotificationGroup = config.invite.notificationGroupId && guildId === config.invite.notificationGroupId;
362
- const isPrivate = !guildId;
363
- if (!isNotificationGroup && !isPrivate && config.invite.notificationGroupId) return;
364
- const hasQuote = session.elements.some((element) => element.type === "quote");
365
- if (!hasQuote) return;
366
- const textContent = session.elements.filter((element) => element.type === "text").map((element) => element.attrs?.content || "").join("").trim();
367
- if (config.invite.showDetailedLog) {
368
- console.log(`管理员审核回复 - 原始content: "${session.content}", 提取文本: "${textContent}"`);
464
+ ctx.command("approve <groupId:string>", "同意群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
465
+ if (!groupId) return "请指定群号。用法:approve <群号>";
466
+ if (!config.invite.adminQQs.includes(session.userId)) {
467
+ return "权限不足,只有管理员可以审核邀请。";
369
468
  }
370
- if (!["同意", "拒绝", "accept", "reject"].includes(textContent)) return;
371
- const quoteElement = session.elements.find((element) => element.type === "quote");
372
- if (!quoteElement) return;
373
- let quoteMessageContent = "";
374
- if (session.quote?.content) {
375
- quoteMessageContent = session.quote.content;
469
+ const inviteData = pendingInvites.get(groupId);
470
+ if (!inviteData) {
471
+ return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
376
472
  }
377
- if (!quoteMessageContent) {
378
- quoteMessageContent = quoteElement.attrs?.content || quoteElement.attrs?.text || "";
473
+ try {
474
+ await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", true, "");
475
+ try {
476
+ await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
477
+ } catch (error) {
478
+ console.error("通知邀请者失败:", error);
479
+ }
480
+ pendingInvites.delete(groupId);
481
+ return `已同意加入群 ${groupId}(${inviteData.groupName}),邀请者:${inviteData.userName}`;
482
+ } catch (error) {
483
+ console.error("处理同意邀请失败:", error);
484
+ return `处理同意邀请失败: ${error.message}`;
379
485
  }
380
- if (!quoteMessageContent && quoteElement.children?.length > 0) {
381
- quoteMessageContent = quoteElement.children.filter((child) => child.type === "text").map((child) => child.attrs?.content || "").join("");
486
+ });
487
+ ctx.command("reject <groupId:string>", "拒绝群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
488
+ if (!groupId) return "请指定群号。用法:reject <群号>";
489
+ if (!config.invite.adminQQs.includes(session.userId)) {
490
+ return "权限不足,只有管理员可以审核邀请。";
382
491
  }
383
- if (!quoteMessageContent) {
384
- const quoteId = quoteElement.attrs?.id || session.quote?.id;
385
- if (quoteId) {
386
- try {
387
- const channelId = guildId || session.channelId;
388
- if (channelId) {
389
- const originalMsg = await session.bot.getMessage(channelId, quoteId);
390
- if (originalMsg?.content) {
391
- quoteMessageContent = originalMsg.content;
392
- }
393
- }
394
- } catch (error) {
395
- if (config.invite.showDetailedLog) {
396
- console.error("通过消息ID获取引用消息内容失败:", error);
397
- }
398
- }
399
- }
492
+ const inviteData = pendingInvites.get(groupId);
493
+ if (!inviteData) {
494
+ return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
400
495
  }
401
- if (config.invite.showDetailedLog) {
402
- console.log(`引用消息内容: "${quoteMessageContent}"`);
403
- }
404
- const groupIdMatch = quoteMessageContent.match(/群号[::]\s*(\d+)/i);
405
- const userIdMatch = quoteMessageContent.match(/QQ[::]\s*(\d+)/i);
406
- if (groupIdMatch && userIdMatch) {
407
- const extractedGroupId = groupIdMatch[1];
408
- const extractedUserId = userIdMatch[1];
409
- if (config.invite.showDetailedLog) {
410
- console.log(`提取到群号: ${extractedGroupId}, QQ: ${extractedUserId}`);
411
- }
412
- let targetInviteId = null;
413
- for (const [inviteId, inviteData] of pendingInvites) {
414
- if (inviteData.groupId === extractedGroupId && inviteData.userId === extractedUserId) {
415
- targetInviteId = inviteId;
416
- break;
417
- }
418
- }
419
- if (targetInviteId) {
420
- const inviteData = pendingInvites.get(targetInviteId);
421
- if (inviteData) {
422
- if (textContent === "同意" || textContent === "accept") {
423
- try {
424
- await session.bot.internal.setGroupAddRequest({
425
- flag: inviteData.flag,
426
- sub_type: "invite",
427
- approve: true,
428
- reason: ""
429
- });
430
- await session.send(`已同意加入群 ${inviteData.groupId}`);
431
- try {
432
- await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
433
- } catch (error) {
434
- console.error("通知邀请者失败:", error);
435
- }
436
- } catch (error) {
437
- console.error("处理同意邀请失败:", error);
438
- await session.send(`处理同意邀请失败: ${error.message}`);
439
- }
440
- } else {
441
- try {
442
- await session.bot.internal.setGroupAddRequest({
443
- flag: inviteData.flag,
444
- sub_type: "invite",
445
- approve: false,
446
- reason: "已拒绝"
447
- });
448
- await session.send(`已拒绝加入群 ${inviteData.groupId}`);
449
- try {
450
- await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请未通过管理员审核,机器人将不会加入该群聊。`);
451
- } catch (error) {
452
- console.error("通知邀请者失败:", error);
453
- }
454
- } catch (error) {
455
- console.error("处理拒绝邀请失败:", error);
456
- await session.send(`处理拒绝邀请失败: ${error.message}`);
457
- }
458
- }
459
- pendingInvites.delete(targetInviteId);
460
- }
461
- } else if (config.invite.showDetailedLog) {
462
- console.log(`未找到匹配的待处理邀请: 群号=${extractedGroupId}, QQ=${extractedUserId}`);
463
- console.log(`当前待处理邀请列表:`, Array.from(pendingInvites.entries()));
496
+ try {
497
+ await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", false, "已拒绝");
498
+ try {
499
+ await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请未通过管理员审核,机器人将不会加入该群聊。`);
500
+ } catch (error) {
501
+ console.error("通知邀请者失败:", error);
464
502
  }
465
- } else if (config.invite.showDetailedLog) {
466
- console.log(`无法从引用消息中提取群号或QQ号,引用内容: "${quoteMessageContent}"`);
503
+ pendingInvites.delete(groupId);
504
+ return `已拒绝加入群 ${groupId}(${inviteData.groupName}),邀请者:${inviteData.userName}`;
505
+ } catch (error) {
506
+ console.error("处理拒绝邀请失败:", error);
507
+ return `处理拒绝邀请失败: ${error.message}`;
467
508
  }
468
509
  });
510
+ ctx.command("pending-invites", "查看待处理的群聊邀请", { authority: 4 }).action(async ({ session }) => {
511
+ if (!config.invite.adminQQs.includes(session.userId)) {
512
+ return "权限不足,只有管理员可以查看待处理邀请。";
513
+ }
514
+ if (pendingInvites.size === 0) {
515
+ return "当前没有待处理的群聊邀请。";
516
+ }
517
+ const lines = ["待处理的群聊邀请列表:"];
518
+ for (const [, invite] of pendingInvites) {
519
+ const elapsed = Math.floor((Date.now() - invite.time) / 1e3 / 60);
520
+ lines.push(`- 群:${invite.groupName}(${invite.groupId})`);
521
+ lines.push(` 邀请者:${invite.userName}(${invite.userId})`);
522
+ lines.push(` ${elapsed} 分钟前`);
523
+ lines.push(` 同意:approve ${invite.groupId} | 拒绝:reject ${invite.groupId}`);
524
+ }
525
+ return lines.join("\n");
526
+ });
469
527
  }
470
528
  __name(apply3, "apply");
471
529
 
@@ -603,42 +661,62 @@ __export(switch_exports, {
603
661
  });
604
662
  var name6 = "group-control-switch";
605
663
  function apply6(ctx, config) {
664
+ if (config.permission.protectedCommands?.length > 0) {
665
+ const protectedSet = new Set(config.permission.protectedCommands);
666
+ ctx.on("command/before-execute", async (argv) => {
667
+ const session = argv.session;
668
+ if (!session.guildId) return;
669
+ const commandName = argv.command.name;
670
+ if (!protectedSet.has(commandName)) return;
671
+ const hasPerm = await hasPermission(session, config);
672
+ if (!hasPerm) {
673
+ return "权限不足,只有群管理员可以使用此指令。";
674
+ }
675
+ }, true);
676
+ }
606
677
  if (!config.botSwitch?.enabled) return;
607
- ctx.command("bot-on", "开启机器人", { authority: config.botSwitch.toggleAuthority }).action(async ({ session }) => {
678
+ const cmdOpts = {};
679
+ if (config.permission.mode === "koishi") {
680
+ cmdOpts.authority = config.permission.koishiAuthority;
681
+ }
682
+ ctx.command("bot-on", "开启机器人", cmdOpts).action(async ({ session }) => {
608
683
  if (!session.guildId) return "该指令只能在群聊中使用。";
684
+ if (config.permission.mode === "builtin") {
685
+ const hasPerm = await hasPermission(session, config);
686
+ if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
687
+ }
609
688
  await setGroupBotStatus(ctx, session.platform, session.guildId, true);
610
689
  return "机器人已在此群开启。";
611
690
  });
612
- ctx.command("bot-off", "关闭机器人", { authority: config.botSwitch.toggleAuthority }).action(async ({ session }) => {
691
+ ctx.command("bot-off", "关闭机器人", cmdOpts).action(async ({ session }) => {
613
692
  if (!session.guildId) return "该指令只能在群聊中使用。";
693
+ if (config.permission.mode === "builtin") {
694
+ const hasPerm = await hasPermission(session, config);
695
+ if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
696
+ }
614
697
  await setGroupBotStatus(ctx, session.platform, session.guildId, false);
615
- return "机器人已在此群关闭。";
698
+ return "机器人已在此群关闭。所有指令和主动响应(入群欢迎、链接解析等)将被阻止。使用 bot-on 重新开启。";
616
699
  });
617
- ctx.on(
618
- "command/before-execute",
619
- async (argv) => {
620
- const session = argv.session;
621
- if (!session.guildId) return;
622
- if (argv.command.name === "bot-on" || argv.command.name === "bot-off") {
623
- return;
624
- }
625
- const status = await getGroupBotStatus(ctx, session.platform, session.guildId);
626
- const isBotEnabled = status ? status.botEnabled : config.botSwitch.defaultState;
627
- if (!isBotEnabled) {
628
- const isMentioned = session.elements?.some((e) => e.type === "at" && e.attrs.id === session.bot.userId);
629
- if (isMentioned && config.botSwitch.disabledMessage) {
630
- try {
631
- await session.send(config.botSwitch.disabledMessage);
632
- } catch (e) {
633
- ctx.logger("group-control-switch").warn("发送关闭提示失败", e);
634
- }
700
+ ctx.on("command/before-execute", async (argv) => {
701
+ const session = argv.session;
702
+ if (!session.guildId) return;
703
+ if (ADMIN_COMMANDS.has(argv.command.name)) {
704
+ return;
705
+ }
706
+ const status = await getGroupBotStatus(ctx, session.platform, session.guildId);
707
+ const isBotEnabled = status ? status.botEnabled : config.botSwitch.defaultState;
708
+ if (!isBotEnabled) {
709
+ const isMentioned = session.elements?.some((e) => e.type === "at" && e.attrs.id === session.bot.userId);
710
+ if (isMentioned && config.botSwitch.disabledMessage) {
711
+ try {
712
+ await session.send(config.botSwitch.disabledMessage);
713
+ } catch (e) {
714
+ ctx.logger("group-control-switch").warn("发送关闭提示失败", e);
635
715
  }
636
- return "";
637
716
  }
638
- },
639
- true
640
- /* append,在其他验证之后执行 */
641
- );
717
+ return "";
718
+ }
719
+ }, true);
642
720
  ctx.middleware(async (session, next) => {
643
721
  if (!session.guildId) return next();
644
722
  const status = await getGroupBotStatus(ctx, session.platform, session.guildId);
@@ -654,27 +732,37 @@ function apply6(ctx, config) {
654
732
  }
655
733
  }
656
734
  return;
657
- });
735
+ }, true);
658
736
  }
659
737
  __name(apply6, "apply");
660
738
 
661
739
  // src/config.ts
662
740
  var import_koishi = require("koishi");
663
741
  var Config = import_koishi.Schema.intersect([
742
+ import_koishi.Schema.object({
743
+ permission: import_koishi.Schema.object({
744
+ mode: import_koishi.Schema.union([
745
+ import_koishi.Schema.const("koishi").description("使用 Koishi 自带权限系统 (authority)"),
746
+ import_koishi.Schema.const("builtin").description("使用插件内置权限管理 (群管理员/群主)")
747
+ ]).default("builtin").description("权限管理模式"),
748
+ koishiAuthority: import_koishi.Schema.number().default(3).description("Koishi 模式下,管理指令所需的最低权限等级"),
749
+ protectedCommands: import_koishi.Schema.array(String).default([]).description("需要群管理员权限才能使用的自定义指令名列表(如来自其他插件的指令)")
750
+ }).description("权限管理")
751
+ }),
664
752
  import_koishi.Schema.object({
665
753
  basic: import_koishi.Schema.object({
666
754
  welcomeMessage: import_koishi.Schema.string().default("你好,我是机器人。").description("机器人加入群聊时发送的欢迎消息"),
667
755
  blacklistMessage: import_koishi.Schema.string().default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后在群内发送的提示"),
668
756
  quitMessage: import_koishi.Schema.string().default("收到来自{userId}的指令,即将退出群聊。").description("用户发送quit指令后在群内发送的提示,支持变量{userId}"),
669
757
  enableBlacklist: import_koishi.Schema.boolean().default(true).description('启用"被踢出自动拉黑"功能'),
758
+ quitCommandEnabled: import_koishi.Schema.boolean().default(true).description("启用quit"),
670
759
  notifyAdminOnKick: import_koishi.Schema.boolean().default(true).description("被踢出群时通知管理员(需要在群聊邀请审核中配置管理员QQ)"),
671
760
  kickNotificationMessage: import_koishi.Schema.string().default("机器人已被踢出群聊\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知消息模板,支持变量{groupId}"),
672
761
  smallGroupAutoQuit: import_koishi.Schema.boolean().default(false).description("启用小群自动退群功能"),
673
762
  smallGroupThreshold: import_koishi.Schema.number().default(30).description("小群人数阈值(群成员数小于等于此值时自动退群)"),
674
763
  smallGroupQuitMessage: import_koishi.Schema.string().default("该群人数过少({memberCount}人),不满足最低人数要求({threshold}人),机器人将自动退出。").description("小群自动退群时在群内发送的提示,支持变量{memberCount}, {threshold}"),
675
764
  smallGroupNotifyAdmin: import_koishi.Schema.boolean().default(true).description("小群自动退群时通知管理员"),
676
- quitCommandEnabled: import_koishi.Schema.boolean().default(true).description("启用quit"),
677
- quitCommandAuthority: import_koishi.Schema.number().default(3).description("quit指令所需权限")
765
+ smallGroupCheckDelay: import_koishi.Schema.number().default(3e3).description("小群检测延迟(毫秒),加入群聊后等待一段时间再获取群信息以确保数据准确")
678
766
  }).description("基础群组管理")
679
767
  }),
680
768
  import_koishi.Schema.object({
@@ -696,7 +784,7 @@ var Config = import_koishi.Schema.intersect([
696
784
  adminQQs: import_koishi.Schema.array(String).default([]).description("管理员QQ号列表(用于权限验证)"),
697
785
  notificationGroupId: import_koishi.Schema.string().description("通知群号(可选:若填写,邀请请求将发送到此群;若不填,则发送私聊给管理员)"),
698
786
  inviteWaitMessage: import_koishi.Schema.string().default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发送给邀请者的等待审核提示消息"),
699
- inviteRequestMessage: import_koishi.Schema.string().default('收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请管理员引用此消息回复"同意"或"拒绝"。').description("发送给管理员的邀请请求消息模板,支持变量{groupName}, {groupId}, {userName}, {userId}"),
787
+ inviteRequestMessage: import_koishi.Schema.string().default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请管理员使用指令 approve {groupId} 同意或 reject {groupId} 拒绝。").description("发送给管理员的邀请请求消息模板,支持变量{groupName}, {groupId}, {userName}, {userId}"),
700
788
  autoApprove: import_koishi.Schema.boolean().default(false).description("是否自动同意邀请(仅在没有指定管理员时)"),
701
789
  showDetailedLog: import_koishi.Schema.boolean().default(false).description("是否显示详细日志")
702
790
  }).description("群聊邀请审核")
@@ -705,8 +793,7 @@ var Config = import_koishi.Schema.intersect([
705
793
  botSwitch: import_koishi.Schema.object({
706
794
  enabled: import_koishi.Schema.boolean().default(true).description("启用独立的群聊bot开关功能"),
707
795
  defaultState: import_koishi.Schema.boolean().default(true).description("群聊中的默认开启状态"),
708
- disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用bot-on开启。").description("机器人在关闭状态下被@时的提示消息"),
709
- toggleAuthority: import_koishi.Schema.number().default(3).description("开关Bot指令(bot-on/bot-off)所需权限")
796
+ disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用bot-on开启。").description("机器人在关闭状态下被@时的提示消息")
710
797
  }).description("机器人开关控制")
711
798
  })
712
799
  ]);
package/lib/utils.d.ts CHANGED
@@ -1,5 +1,14 @@
1
+ import { Session } from 'koishi';
1
2
  import { Config } from './config';
2
3
  export declare function isBlacklistEnabled(config: Config['basic']): string | null;
3
4
  export declare function parseGuildId(input: string): string | null;
4
5
  export declare function formatDate(timestamp: number): string;
5
6
  export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
7
+ /**
8
+ * 检查用户是否有管理权限
9
+ * - koishi 模式: 使用 Koishi 自带的 authority 系统
10
+ * - builtin 模式: 检查用户是否为群管理员/群主,或在管理员QQ列表中
11
+ */
12
+ export declare function hasPermission(session: Session, config: Config): Promise<boolean>;
13
+ /** 管理指令列表 - 这些指令始终不受 bot-off 影响 */
14
+ export declare const ADMIN_COMMANDS: Set<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-control",
3
3
  "description": "Koishi 插件,一个多功能的群聊自管理工具。支持被踢出自动拉黑、刷屏自动屏蔽、开关控制等功能。(仅支持 OneBot 适配器)",
4
- "version": "0.2.7",
4
+ "version": "0.2.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -23,4 +23,4 @@
23
23
  "peerDependencies": {
24
24
  "koishi": "^4.18.7"
25
25
  }
26
- }
26
+ }