koishi-plugin-group-control 0.2.7 → 0.2.8

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,6 +157,59 @@ 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";
@@ -180,34 +233,54 @@ function apply2(ctx, config) {
180
233
  }
181
234
  }
182
235
  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());
236
+ const delay = config.basic.smallGroupCheckDelay || 3e3;
237
+ setTimeout(async () => {
238
+ try {
239
+ let memberCount = 0;
188
240
  try {
189
- await session.bot.sendMessage(guildId, quitMsg, platform);
190
- } catch (e) {
241
+ const groupInfo = await session.bot.internal?.getGroupInfo?.(parseInt(guildId));
242
+ memberCount = groupInfo?.member_count || 0;
243
+ } catch {
244
+ }
245
+ if (memberCount === 0) {
246
+ try {
247
+ const guildInfo = await session.bot.getGuild(guildId);
248
+ memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
249
+ } catch {
250
+ }
251
+ }
252
+ if (memberCount === 0) {
253
+ try {
254
+ const memberList = await session.bot.getGuildMemberList(guildId);
255
+ memberCount = memberList?.data?.length || 0;
256
+ } catch {
257
+ }
191
258
  }
192
- if (config.basic.smallGroupNotifyAdmin) {
193
- const adminMsg = `小群自动退群
259
+ if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
260
+ const quitMsg = config.basic.smallGroupQuitMessage.replace("{memberCount}", memberCount.toString()).replace("{threshold}", config.basic.smallGroupThreshold.toString());
261
+ try {
262
+ await session.bot.sendMessage(guildId, quitMsg, platform);
263
+ } catch (e) {
264
+ }
265
+ if (config.basic.smallGroupNotifyAdmin) {
266
+ const adminMsg = `小群自动退群
194
267
  群号:${guildId}
195
268
  群成员数:${memberCount}人(阈值:${config.basic.smallGroupThreshold}人)
196
269
  机器人已自动退出该群。`;
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}`);
270
+ await notifyAdmins(session.bot, config, adminMsg);
271
+ }
272
+ quittingGuilds.add(`${platform}:${guildId}`);
273
+ try {
274
+ await session.bot.internal.setGroupLeave(parseInt(guildId));
275
+ } catch (e) {
276
+ console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
277
+ quittingGuilds.delete(`${platform}:${guildId}`);
278
+ }
205
279
  }
206
- return;
280
+ } catch (error) {
281
+ console.error(`小群自动退群检测失败 (群号: ${guildId}):`, error);
207
282
  }
208
- } catch (error) {
209
- console.error(`获取群信息失败 (群号: ${guildId}):`, error);
210
- }
283
+ }, delay);
211
284
  }
212
285
  if (config.basic.welcomeMessage) {
213
286
  try {
@@ -237,9 +310,20 @@ function apply2(ctx, config) {
237
310
  }
238
311
  });
239
312
  if (config.basic.quitCommandEnabled) {
240
- ctx.command("quit", "让机器人主动退出当前群聊", { authority: config.basic.quitCommandAuthority }).action(async ({ session }) => {
313
+ const cmdOpts = {};
314
+ if (config.permission.mode === "koishi") {
315
+ cmdOpts.authority = config.permission.koishiAuthority;
316
+ }
317
+ ctx.command("quit", "让机器人主动退出当前群聊", cmdOpts).action(async ({ session }) => {
241
318
  if (!session.guildId) return "quit 指令只能在群聊中使用。";
319
+ if (config.permission.mode === "builtin") {
320
+ const hasPerm = await hasPermission(session, config);
321
+ if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
322
+ }
242
323
  const { guildId, platform, userId } = session;
324
+ const adminMsg = `收到来自 ${userId} 的退群指令
325
+ 群号:${guildId}`;
326
+ await notifyAdmins(session.bot, config, adminMsg);
243
327
  quittingGuilds.add(`${platform}:${guildId}`);
244
328
  try {
245
329
  await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
@@ -267,6 +351,18 @@ var name3 = "group-control-invite";
267
351
  function apply3(ctx, config) {
268
352
  if (!config.invite.enabled) return;
269
353
  const pendingInvites = /* @__PURE__ */ new Map();
354
+ const INVITE_TIMEOUT = 10 * 60 * 1e3;
355
+ setInterval(() => {
356
+ const now = Date.now();
357
+ for (const [key, invite] of pendingInvites) {
358
+ if (now - invite.time > INVITE_TIMEOUT) {
359
+ pendingInvites.delete(key);
360
+ if (config.invite.showDetailedLog) {
361
+ console.log(`邀请超时已清理: 群号=${invite.groupId}, 邀请者=${invite.userId}`);
362
+ }
363
+ }
364
+ }
365
+ }, 60 * 1e3);
270
366
  ctx.on("guild-request", async (session) => {
271
367
  const raw = session.original || session.raw || session.event?._data || {};
272
368
  const flag = raw.flag || session.flag || session.messageId;
@@ -317,11 +413,11 @@ function apply3(ctx, config) {
317
413
  }
318
414
  return;
319
415
  }
320
- const inviteId = `${rawGroupId}_${rawUserId}_${Date.now()}`;
321
- pendingInvites.set(inviteId, {
416
+ pendingInvites.set(rawGroupId, {
322
417
  groupId: rawGroupId,
323
418
  userId: rawUserId,
324
419
  userName,
420
+ groupName,
325
421
  time: Date.now(),
326
422
  flag
327
423
  });
@@ -355,116 +451,78 @@ function apply3(ctx, config) {
355
451
  console.warn("群邀请请求发送失败:未配置通知群且管理员私聊发送失败");
356
452
  }
357
453
  });
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}"`);
454
+ ctx.command("approve <groupId:string>", "同意群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
455
+ if (!groupId) return "请指定群号。用法:approve <群号>";
456
+ if (!config.invite.adminQQs.includes(session.userId)) {
457
+ return "权限不足,只有管理员可以审核邀请。";
369
458
  }
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;
459
+ const inviteData = pendingInvites.get(groupId);
460
+ if (!inviteData) {
461
+ return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
376
462
  }
377
- if (!quoteMessageContent) {
378
- quoteMessageContent = quoteElement.attrs?.content || quoteElement.attrs?.text || "";
463
+ try {
464
+ await session.bot.internal.setGroupAddRequest({
465
+ flag: inviteData.flag,
466
+ sub_type: "invite",
467
+ approve: true,
468
+ reason: ""
469
+ });
470
+ try {
471
+ await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
472
+ } catch (error) {
473
+ console.error("通知邀请者失败:", error);
474
+ }
475
+ pendingInvites.delete(groupId);
476
+ return `已同意加入群 ${groupId}(${inviteData.groupName}),邀请者:${inviteData.userName}`;
477
+ } catch (error) {
478
+ console.error("处理同意邀请失败:", error);
479
+ return `处理同意邀请失败: ${error.message}`;
379
480
  }
380
- if (!quoteMessageContent && quoteElement.children?.length > 0) {
381
- quoteMessageContent = quoteElement.children.filter((child) => child.type === "text").map((child) => child.attrs?.content || "").join("");
481
+ });
482
+ ctx.command("reject <groupId:string>", "拒绝群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
483
+ if (!groupId) return "请指定群号。用法:reject <群号>";
484
+ if (!config.invite.adminQQs.includes(session.userId)) {
485
+ return "权限不足,只有管理员可以审核邀请。";
382
486
  }
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
- }
487
+ const inviteData = pendingInvites.get(groupId);
488
+ if (!inviteData) {
489
+ return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
400
490
  }
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()));
491
+ try {
492
+ await session.bot.internal.setGroupAddRequest({
493
+ flag: inviteData.flag,
494
+ sub_type: "invite",
495
+ approve: false,
496
+ reason: "已拒绝"
497
+ });
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}`;
508
+ }
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}`);
467
524
  }
525
+ return lines.join("\n");
468
526
  });
469
527
  }
470
528
  __name(apply3, "apply");
@@ -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.8",
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
+ }