koishi-plugin-group-control 1.0.4 → 1.0.5

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/config.d.ts CHANGED
@@ -12,10 +12,16 @@ export interface GroupConfig {
12
12
  smallGroupQuitMessage: string;
13
13
  smallGroupNotifyAdmin: boolean;
14
14
  smallGroupCheckDelay: number;
15
+ smallGroupExcludeOfficialBots: boolean;
16
+ smallGroupRealtimeMonitor: boolean;
17
+ smallGroupRecheckCooldown: number;
15
18
  smallGroupQualifiedNotifyAdmin: boolean;
16
19
  smallGroupQualifiedMessage: string;
17
20
  notifyAdminOnMute: boolean;
18
21
  muteNotificationMessage: string;
22
+ muteAutoQuit: boolean;
23
+ muteAutoQuitThreshold: number;
24
+ muteQuitNotificationMessage: string;
19
25
  }
20
26
  export interface AdminConfig {
21
27
  adminQQs: string[];
package/lib/database.d.ts CHANGED
@@ -25,6 +25,11 @@ export interface SmallGroupWhitelist {
25
25
  platform: string;
26
26
  guildId: string;
27
27
  }
28
+ export interface ApprovedGuild {
29
+ platform: string;
30
+ guildId: string;
31
+ timestamp: number;
32
+ }
28
33
  export interface SelfLeftGuild {
29
34
  platform: string;
30
35
  guildId: string;
@@ -54,6 +59,7 @@ declare module 'koishi' {
54
59
  group_bot_status: GroupBotStatus;
55
60
  small_group_whitelist: SmallGroupWhitelist;
56
61
  self_left_guild: SelfLeftGuild;
62
+ approved_guild: ApprovedGuild;
57
63
  pending_invite: PendingInvite;
58
64
  pending_friend_request: PendingFriendRequest;
59
65
  }
@@ -75,7 +81,7 @@ export declare function consumeSelfLeft(ctx: Context, guildId: string, maxAgeSec
75
81
  /** 清理标记(退群失败时回滚,或 unban 时清理)*/
76
82
  export declare function clearSelfLeft(ctx: Context, guildId: string): Promise<void>;
77
83
  /** 定期清理过期的主动退群标记(超过 maxAgeSec 秒未消费的)*/
78
- export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<number>;
84
+ export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<void>;
79
85
  export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
80
86
  export declare function updateCommandFrequencyRecord(ctx: Context, platform: string, guildId: string, data: Partial<CommandFrequencyRecord>): Promise<void>;
81
87
  export declare function getGroupBotStatus(ctx: Context, platform: string, guildId: string): Promise<GroupBotStatus | null>;
@@ -84,13 +90,16 @@ export declare function isInSmallGroupWhitelist(ctx: Context, guildId: string):
84
90
  export declare function addToSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
85
91
  export declare function removeFromSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
86
92
  export declare function getAllSmallGroupWhitelist(ctx: Context): Promise<SmallGroupWhitelist[]>;
93
+ export declare function markApprovedGuild(ctx: Context, guildId: string): Promise<void>;
94
+ export declare function isApprovedGuild(ctx: Context, guildId: string): Promise<boolean>;
95
+ export declare function clearApprovedGuild(ctx: Context, guildId: string): Promise<void>;
87
96
  export declare function getPendingInvite(ctx: Context, groupId: string): Promise<PendingInvite>;
88
97
  export declare function addPendingInvite(ctx: Context, inviteUser: Omit<PendingInvite, 'platform'>): Promise<void>;
89
98
  export declare function removePendingInvite(ctx: Context, groupId: string): Promise<void>;
90
99
  export declare function getAllPendingInvites(ctx: Context): Promise<PendingInvite[]>;
91
- export declare function clearExpiredPendingInvites(ctx: Context, expireTimeMs: number): Promise<number>;
100
+ export declare function clearExpiredPendingInvites(ctx: Context, expireTimeMs: number): Promise<void>;
92
101
  export declare function getPendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<PendingFriendRequest>;
93
102
  export declare function addPendingFriendRequest(ctx: Context, platform: string, data: Omit<PendingFriendRequest, 'platform'>): Promise<void>;
94
103
  export declare function removePendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<void>;
95
104
  export declare function getAllPendingFriendRequests(ctx: Context, platform: string): Promise<PendingFriendRequest[]>;
96
- export declare function clearExpiredPendingFriendRequests(ctx: Context, platform: string, expireTimeMs: number): Promise<number>;
105
+ export declare function clearExpiredPendingFriendRequests(ctx: Context, platform: string, expireTimeMs: number): Promise<void>;
package/lib/index.d.ts CHANGED
@@ -2,4 +2,5 @@ import { Context } from 'koishi';
2
2
  import { Config } from './config';
3
3
  export * from './config';
4
4
  export declare const name = "group-control";
5
+ export declare const inject: string[];
5
6
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  Config: () => Config,
24
24
  apply: () => apply8,
25
+ inject: () => inject,
25
26
  name: () => name8
26
27
  });
27
28
  module.exports = __toCommonJS(src_exports);
@@ -35,6 +36,7 @@ __export(database_exports, {
35
36
  addToSmallGroupWhitelist: () => addToSmallGroupWhitelist,
36
37
  apply: () => apply,
37
38
  blacklistKicked: () => blacklistKicked,
39
+ clearApprovedGuild: () => clearApprovedGuild,
38
40
  clearBlacklistedGuilds: () => clearBlacklistedGuilds,
39
41
  clearExpiredPendingFriendRequests: () => clearExpiredPendingFriendRequests,
40
42
  clearExpiredPendingInvites: () => clearExpiredPendingInvites,
@@ -51,7 +53,9 @@ __export(database_exports, {
51
53
  getGroupBotStatus: () => getGroupBotStatus,
52
54
  getPendingFriendRequest: () => getPendingFriendRequest,
53
55
  getPendingInvite: () => getPendingInvite,
56
+ isApprovedGuild: () => isApprovedGuild,
54
57
  isInSmallGroupWhitelist: () => isInSmallGroupWhitelist,
58
+ markApprovedGuild: () => markApprovedGuild,
55
59
  markSelfLeft: () => markSelfLeft,
56
60
  name: () => name,
57
61
  removeBlacklistedGuild: () => removeBlacklistedGuild,
@@ -94,6 +98,11 @@ function apply(ctx) {
94
98
  guildId: "string",
95
99
  timestamp: "integer"
96
100
  }, { primary: ["platform", "guildId"] });
101
+ ctx.model.extend("approved_guild", {
102
+ platform: "string",
103
+ guildId: "string",
104
+ timestamp: "integer"
105
+ }, { primary: ["platform", "guildId"] });
97
106
  ctx.model.extend("pending_invite", {
98
107
  platform: "string",
99
108
  groupId: "string",
@@ -115,15 +124,15 @@ function apply(ctx) {
115
124
  __name(apply, "apply");
116
125
  var BLACKLIST_PLATFORM = "onebot";
117
126
  async function getBlacklistedGuild(ctx, guildId) {
118
- return await ctx.model.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
127
+ return await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
119
128
  }
120
129
  __name(getBlacklistedGuild, "getBlacklistedGuild");
121
130
  async function removeBlacklistedGuild(ctx, guildId) {
122
- return await ctx.model.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
131
+ return await ctx.database.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
123
132
  }
124
133
  __name(removeBlacklistedGuild, "removeBlacklistedGuild");
125
134
  async function createBlacklistedGuild(ctx, guildId, reason) {
126
- return await ctx.model.upsert("blacklisted_guild", [{
135
+ return await ctx.database.upsert("blacklisted_guild", [{
127
136
  platform: BLACKLIST_PLATFORM,
128
137
  guildId,
129
138
  timestamp: Math.floor(Date.now() / 1e3),
@@ -132,15 +141,15 @@ async function createBlacklistedGuild(ctx, guildId, reason) {
132
141
  }
133
142
  __name(createBlacklistedGuild, "createBlacklistedGuild");
134
143
  async function getAllBlacklistedGuilds(ctx) {
135
- return await ctx.model.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
144
+ return await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
136
145
  }
137
146
  __name(getAllBlacklistedGuilds, "getAllBlacklistedGuilds");
138
147
  async function clearBlacklistedGuilds(ctx) {
139
- return await ctx.model.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
148
+ return await ctx.database.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
140
149
  }
141
150
  __name(clearBlacklistedGuilds, "clearBlacklistedGuilds");
142
151
  async function blacklistKicked(ctx, guildId) {
143
- return await ctx.model.upsert("blacklisted_guild", [{
152
+ return await ctx.database.upsert("blacklisted_guild", [{
144
153
  platform: BLACKLIST_PLATFORM,
145
154
  guildId,
146
155
  timestamp: Math.floor(Date.now() / 1e3),
@@ -149,7 +158,7 @@ async function blacklistKicked(ctx, guildId) {
149
158
  }
150
159
  __name(blacklistKicked, "blacklistKicked");
151
160
  async function markSelfLeft(ctx, guildId) {
152
- await ctx.model.upsert("self_left_guild", [{
161
+ await ctx.database.upsert("self_left_guild", [{
153
162
  platform: BLACKLIST_PLATFORM,
154
163
  guildId,
155
164
  timestamp: Math.floor(Date.now() / 1e3)
@@ -157,33 +166,28 @@ async function markSelfLeft(ctx, guildId) {
157
166
  }
158
167
  __name(markSelfLeft, "markSelfLeft");
159
168
  async function consumeSelfLeft(ctx, guildId, maxAgeSec = 120) {
160
- const [row] = await ctx.model.get("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
169
+ const [row] = await ctx.database.get("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
161
170
  if (!row) return false;
162
- await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
171
+ await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
163
172
  return Math.floor(Date.now() / 1e3) - row.timestamp <= maxAgeSec;
164
173
  }
165
174
  __name(consumeSelfLeft, "consumeSelfLeft");
166
175
  async function clearSelfLeft(ctx, guildId) {
167
- await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
176
+ await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
168
177
  }
169
178
  __name(clearSelfLeft, "clearSelfLeft");
170
179
  async function clearExpiredSelfLeft(ctx, maxAgeSec = 300) {
171
180
  const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSec;
172
- const all = await ctx.model.get("self_left_guild", { platform: BLACKLIST_PLATFORM });
173
- const expired = all.filter((r) => r.timestamp < cutoff);
174
- for (const record of expired) {
175
- await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId: record.guildId });
176
- }
177
- return expired.length;
181
+ await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, timestamp: { $lt: cutoff } });
178
182
  }
179
183
  __name(clearExpiredSelfLeft, "clearExpiredSelfLeft");
180
184
  async function getCommandFrequencyRecord(ctx, platform, guildId) {
181
- const records = await ctx.model.get("command_frequency_record", { platform, guildId });
185
+ const records = await ctx.database.get("command_frequency_record", { platform, guildId });
182
186
  return records.length > 0 ? records[0] : null;
183
187
  }
184
188
  __name(getCommandFrequencyRecord, "getCommandFrequencyRecord");
185
189
  async function updateCommandFrequencyRecord(ctx, platform, guildId, data) {
186
- await ctx.model.upsert("command_frequency_record", [{
190
+ await ctx.database.upsert("command_frequency_record", [{
187
191
  platform,
188
192
  guildId,
189
193
  ...data
@@ -195,86 +199,93 @@ var groupBotStatusCacheKey = /* @__PURE__ */ __name((platform, guildId) => `${pl
195
199
  async function getGroupBotStatus(ctx, platform, guildId) {
196
200
  const key = groupBotStatusCacheKey(platform, guildId);
197
201
  if (groupBotStatusCache.has(key)) return groupBotStatusCache.get(key);
198
- const records = await ctx.model.get("group_bot_status", { platform, guildId });
202
+ const records = await ctx.database.get("group_bot_status", { platform, guildId });
199
203
  const status = records.length > 0 ? records[0] : null;
200
204
  groupBotStatusCache.set(key, status);
201
205
  return status;
202
206
  }
203
207
  __name(getGroupBotStatus, "getGroupBotStatus");
204
208
  async function setGroupBotStatus(ctx, platform, guildId, botEnabled) {
205
- await ctx.model.upsert("group_bot_status", [{ platform, guildId, botEnabled }]);
209
+ await ctx.database.upsert("group_bot_status", [{ platform, guildId, botEnabled }]);
206
210
  groupBotStatusCache.set(groupBotStatusCacheKey(platform, guildId), { platform, guildId, botEnabled });
207
211
  }
208
212
  __name(setGroupBotStatus, "setGroupBotStatus");
209
213
  async function isInSmallGroupWhitelist(ctx, guildId) {
210
- const records = await ctx.model.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
214
+ const records = await ctx.database.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
211
215
  return records.length > 0;
212
216
  }
213
217
  __name(isInSmallGroupWhitelist, "isInSmallGroupWhitelist");
214
218
  async function addToSmallGroupWhitelist(ctx, guildId) {
215
- await ctx.model.upsert("small_group_whitelist", [{ platform: BLACKLIST_PLATFORM, guildId }]);
219
+ await ctx.database.upsert("small_group_whitelist", [{ platform: BLACKLIST_PLATFORM, guildId }]);
216
220
  }
217
221
  __name(addToSmallGroupWhitelist, "addToSmallGroupWhitelist");
218
222
  async function removeFromSmallGroupWhitelist(ctx, guildId) {
219
- await ctx.model.remove("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
223
+ await ctx.database.remove("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
220
224
  }
221
225
  __name(removeFromSmallGroupWhitelist, "removeFromSmallGroupWhitelist");
222
226
  async function getAllSmallGroupWhitelist(ctx) {
223
- return await ctx.model.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM });
227
+ return await ctx.database.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM });
224
228
  }
225
229
  __name(getAllSmallGroupWhitelist, "getAllSmallGroupWhitelist");
230
+ async function markApprovedGuild(ctx, guildId) {
231
+ await ctx.database.upsert("approved_guild", [{
232
+ platform: BLACKLIST_PLATFORM,
233
+ guildId,
234
+ timestamp: Math.floor(Date.now() / 1e3)
235
+ }]);
236
+ }
237
+ __name(markApprovedGuild, "markApprovedGuild");
238
+ async function isApprovedGuild(ctx, guildId) {
239
+ const records = await ctx.database.get("approved_guild", { platform: BLACKLIST_PLATFORM, guildId });
240
+ return records.length > 0;
241
+ }
242
+ __name(isApprovedGuild, "isApprovedGuild");
243
+ async function clearApprovedGuild(ctx, guildId) {
244
+ await ctx.database.remove("approved_guild", { platform: BLACKLIST_PLATFORM, guildId });
245
+ }
246
+ __name(clearApprovedGuild, "clearApprovedGuild");
226
247
  async function getPendingInvite(ctx, groupId) {
227
- const records = await ctx.model.get("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
248
+ const records = await ctx.database.get("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
228
249
  return records.length > 0 ? records[0] : null;
229
250
  }
230
251
  __name(getPendingInvite, "getPendingInvite");
231
252
  async function addPendingInvite(ctx, inviteUser) {
232
- await ctx.model.upsert("pending_invite", [{ platform: BLACKLIST_PLATFORM, ...inviteUser }]);
253
+ await ctx.database.upsert("pending_invite", [{ platform: BLACKLIST_PLATFORM, ...inviteUser }]);
233
254
  }
234
255
  __name(addPendingInvite, "addPendingInvite");
235
256
  async function removePendingInvite(ctx, groupId) {
236
- await ctx.model.remove("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
257
+ await ctx.database.remove("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
237
258
  }
238
259
  __name(removePendingInvite, "removePendingInvite");
239
260
  async function getAllPendingInvites(ctx) {
240
- return await ctx.model.get("pending_invite", { platform: BLACKLIST_PLATFORM });
261
+ return await ctx.database.get("pending_invite", { platform: BLACKLIST_PLATFORM });
241
262
  }
242
263
  __name(getAllPendingInvites, "getAllPendingInvites");
243
264
  async function clearExpiredPendingInvites(ctx, expireTimeMs) {
244
265
  const cutoff = Math.floor((Date.now() - expireTimeMs) / 1e3);
245
- const all = await ctx.model.get("pending_invite", { platform: BLACKLIST_PLATFORM });
246
- const expired = all.filter((r) => r.time < cutoff);
247
- for (const record of expired) {
248
- await ctx.model.remove("pending_invite", { platform: BLACKLIST_PLATFORM, groupId: record.groupId });
249
- }
250
- return expired.length;
266
+ await ctx.database.remove("pending_invite", { platform: BLACKLIST_PLATFORM, time: { $lt: cutoff } });
251
267
  }
252
268
  __name(clearExpiredPendingInvites, "clearExpiredPendingInvites");
253
269
  async function getPendingFriendRequest(ctx, platform, userId) {
254
- const records = await ctx.model.get("pending_friend_request", { platform, userId });
270
+ const records = await ctx.database.get("pending_friend_request", { platform, userId });
255
271
  return records.length > 0 ? records[0] : null;
256
272
  }
257
273
  __name(getPendingFriendRequest, "getPendingFriendRequest");
258
274
  async function addPendingFriendRequest(ctx, platform, data) {
259
- await ctx.model.upsert("pending_friend_request", [{ platform, ...data }]);
275
+ await ctx.database.upsert("pending_friend_request", [{ platform, ...data }]);
260
276
  }
261
277
  __name(addPendingFriendRequest, "addPendingFriendRequest");
262
278
  async function removePendingFriendRequest(ctx, platform, userId) {
263
- await ctx.model.remove("pending_friend_request", { platform, userId });
279
+ await ctx.database.remove("pending_friend_request", { platform, userId });
264
280
  }
265
281
  __name(removePendingFriendRequest, "removePendingFriendRequest");
266
282
  async function getAllPendingFriendRequests(ctx, platform) {
267
- return await ctx.model.get("pending_friend_request", { platform });
283
+ return await ctx.database.get("pending_friend_request", { platform });
268
284
  }
269
285
  __name(getAllPendingFriendRequests, "getAllPendingFriendRequests");
270
286
  async function clearExpiredPendingFriendRequests(ctx, platform, expireTimeMs) {
271
287
  const cutoff = Math.floor((Date.now() - expireTimeMs) / 1e3);
272
- const all = await ctx.model.get("pending_friend_request", { platform });
273
- const expired = all.filter((r) => r.time < cutoff);
274
- for (const record of expired) {
275
- await ctx.model.remove("pending_friend_request", { platform, userId: record.userId });
276
- }
277
- return expired.length;
288
+ await ctx.database.remove("pending_friend_request", { platform, time: { $lt: cutoff } });
278
289
  }
279
290
  __name(clearExpiredPendingFriendRequests, "clearExpiredPendingFriendRequests");
280
291
 
@@ -292,6 +303,7 @@ function isBlacklistEnabled(config) {
292
303
  }
293
304
  __name(isBlacklistEnabled, "isBlacklistEnabled");
294
305
  function parseGuildId(input) {
306
+ if (!input) return null;
295
307
  const match = input.trim().match(/^onebot:(\d+)$/);
296
308
  return match ? match[1] : /^\d+$/.test(input.trim()) ? input.trim() : null;
297
309
  }
@@ -376,20 +388,22 @@ var ADMIN_COMMANDS = /* @__PURE__ */ new Set([
376
388
  "gc.sg-add",
377
389
  "gc.sg-rm",
378
390
  "gc.sg-list",
391
+ "gc.friends",
392
+ "gc.delfriend",
393
+ "gc.groups",
394
+ "gc.leave",
379
395
  "gc.fp",
380
396
  "gc.fa",
381
397
  "gc.fr"
382
398
  ]);
383
399
 
384
- // src/state.ts
385
- var approvedGroups = /* @__PURE__ */ new Set();
386
-
387
400
  // src/modules/basic.ts
388
401
  var name2 = "group-control-basic";
389
402
  async function getGroupName(bot, guildId) {
390
403
  try {
391
404
  const info = await bot.internal?.getGroupInfo?.(parseInt(guildId));
392
- if (info?.group_name) return info.group_name;
405
+ const data = info?.data ?? info;
406
+ if (data?.group_name) return data.group_name;
393
407
  } catch {
394
408
  }
395
409
  try {
@@ -404,8 +418,12 @@ __name(getGroupName, "getGroupName");
404
418
  function apply2(ctx, config) {
405
419
  const quittingGuilds = /* @__PURE__ */ new Map();
406
420
  const processedKicks = /* @__PURE__ */ new Map();
421
+ const processedAdds = /* @__PURE__ */ new Map();
422
+ const realtimeLastCheck = /* @__PURE__ */ new Map();
407
423
  const QUITTING_EXPIRE_MS = 60 * 1e3;
408
424
  const KICK_DEDUP_MS = 60 * 1e3;
425
+ const ADD_DEDUP_MS = 10 * 1e3;
426
+ const REALTIME_DEBOUNCE_MS = 1500;
409
427
  setInterval(() => {
410
428
  const now = Date.now();
411
429
  for (const [key, time] of quittingGuilds) {
@@ -414,6 +432,12 @@ function apply2(ctx, config) {
414
432
  for (const [key, time] of processedKicks) {
415
433
  if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
416
434
  }
435
+ for (const [key, time] of processedAdds) {
436
+ if (now - time > ADD_DEDUP_MS) processedAdds.delete(key);
437
+ }
438
+ for (const [key, time] of realtimeLastCheck) {
439
+ if (now - time > 5 * 60 * 1e3) realtimeLastCheck.delete(key);
440
+ }
417
441
  }, 30 * 1e3);
418
442
  setInterval(async () => {
419
443
  try {
@@ -421,11 +445,101 @@ function apply2(ctx, config) {
421
445
  } catch {
422
446
  }
423
447
  }, 5 * 60 * 1e3);
448
+ async function evaluateSmallGroup(bot, guildId) {
449
+ const threshold = config.basic.smallGroupThreshold;
450
+ const exclude = config.basic.smallGroupExcludeOfficialBots;
451
+ const maxBots = 20;
452
+ let total = 0;
453
+ let groupName = "未知";
454
+ try {
455
+ const info = await bot.internal?.getGroupInfo?.(parseInt(guildId));
456
+ const data = info?.data ?? info;
457
+ total = Number(data?.member_count) || 0;
458
+ if (data?.group_name) groupName = data.group_name;
459
+ } catch {
460
+ }
461
+ if (total === 0) {
462
+ try {
463
+ const guildInfo = await bot.getGuild(guildId);
464
+ total = Number(guildInfo?.member_count ?? guildInfo?.memberCount) || 0;
465
+ if (guildInfo?.name) groupName = guildInfo.name;
466
+ } catch {
467
+ }
468
+ }
469
+ if (total > 0 && total <= threshold) {
470
+ return { decision: "quit", count: total, groupName };
471
+ }
472
+ if (!exclude) {
473
+ if (total > 0) return { decision: total <= threshold ? "quit" : "keep", count: total, groupName };
474
+ } else if (total > threshold + maxBots) {
475
+ return { decision: "keep", count: total, groupName };
476
+ }
477
+ let list = [];
478
+ try {
479
+ const raw = await bot.internal?.getGroupMemberList?.(parseInt(guildId));
480
+ list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
481
+ } catch {
482
+ }
483
+ if (list.length === 0) {
484
+ if (total > 0) return { decision: total <= threshold ? "quit" : "keep", count: total, groupName };
485
+ return { decision: "unknown", count: 0, groupName };
486
+ }
487
+ const N = list.length;
488
+ if (!exclude) return { decision: N <= threshold ? "quit" : "keep", count: N, groupName };
489
+ if (N <= threshold) return { decision: "quit", count: N, groupName };
490
+ const selfId = String(bot.selfId ?? "");
491
+ const botsNeeded = N - threshold;
492
+ let bots = 0;
493
+ for (const m of list) {
494
+ const uid = String(m?.user_id ?? m?.userId ?? "");
495
+ if (m?.is_robot === true || selfId && uid === selfId) {
496
+ bots++;
497
+ if (bots >= botsNeeded) {
498
+ return { decision: "quit", count: N - bots, groupName };
499
+ }
500
+ }
501
+ }
502
+ const real = N - bots;
503
+ return { decision: real <= threshold ? "quit" : "keep", count: real, groupName };
504
+ }
505
+ __name(evaluateSmallGroup, "evaluateSmallGroup");
506
+ async function performSmallGroupQuit(bot, platform, guildId, groupName, memberCount) {
507
+ const threshold = config.basic.smallGroupThreshold;
508
+ const quitMsg = config.basic.smallGroupQuitMessage.replaceAll("{memberCount}", memberCount.toString()).replaceAll("{threshold}", threshold.toString()).replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId);
509
+ try {
510
+ await bot.sendMessage(guildId, quitMsg, platform);
511
+ } catch (e) {
512
+ }
513
+ if (config.basic.smallGroupNotifyAdmin) {
514
+ const adminMsg = `小群自动退群
515
+ 群名称:${groupName}
516
+ 群号:${guildId}
517
+ 群成员数:${memberCount}人(阈值:${threshold}人)
518
+ 机器人已自动退出该群。`;
519
+ await notifyAdmins(bot, config, adminMsg);
520
+ }
521
+ quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
522
+ await markSelfLeft(ctx, guildId);
523
+ try {
524
+ await bot.internal.setGroupLeave(parseInt(guildId));
525
+ } catch (e) {
526
+ console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
527
+ quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
528
+ await clearSelfLeft(ctx, guildId);
529
+ }
530
+ }
531
+ __name(performSmallGroupQuit, "performSmallGroupQuit");
424
532
  ctx.on("guild-added", async (session) => {
425
533
  const { guildId, platform } = session;
426
534
  ctx.logger("group-control-basic").info(`[guild-added] 触发!guildId=${guildId}, platform=${platform}`);
535
+ const addKey = `${BLACKLIST_PLATFORM}:${guildId}`;
536
+ if (processedAdds.has(addKey)) {
537
+ ctx.logger("group-control-basic").info(`[guild-added] 忽略重复事件 guildId=${guildId}`);
538
+ return;
539
+ }
540
+ processedAdds.set(addKey, Date.now());
427
541
  if (config.basic.enableBlacklist) {
428
- const [blacklisted] = await ctx.model.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
542
+ const [blacklisted] = await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
429
543
  if (blacklisted) {
430
544
  try {
431
545
  await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
@@ -442,8 +556,7 @@ function apply2(ctx, config) {
442
556
  }
443
557
  if (config.basic.smallGroupAutoQuit) {
444
558
  const inWhitelist = await isInSmallGroupWhitelist(ctx, guildId);
445
- const wasApproved = approvedGroups.has(guildId);
446
- if (wasApproved) approvedGroups.delete(guildId);
559
+ const wasApproved = await isApprovedGuild(ctx, guildId);
447
560
  const pendingInvite = await getPendingInvite(ctx, guildId);
448
561
  const hadPendingInvite = !!pendingInvite;
449
562
  if (hadPendingInvite) await removePendingInvite(ctx, guildId);
@@ -452,58 +565,14 @@ function apply2(ctx, config) {
452
565
  const delay = config.basic.smallGroupCheckDelay || 3e3;
453
566
  setTimeout(async () => {
454
567
  try {
455
- let memberCount = 0;
456
- let groupName = "未知";
457
- try {
458
- const groupInfo = await session.bot.internal?.getGroupInfo?.(parseInt(guildId));
459
- memberCount = groupInfo?.member_count || 0;
460
- if (groupInfo?.group_name) groupName = groupInfo.group_name;
461
- } catch {
462
- }
463
- if (memberCount === 0) {
464
- try {
465
- const guildInfo = await session.bot.getGuild(guildId);
466
- memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
467
- if (guildInfo?.name) groupName = guildInfo.name;
468
- } catch {
469
- }
470
- }
471
- if (memberCount === 0) {
472
- try {
473
- const memberList = await session.bot.getGuildMemberList(guildId);
474
- memberCount = memberList?.data?.length || 0;
475
- } catch {
476
- }
477
- }
478
- if (groupName === "未知") {
479
- groupName = await getGroupName(session.bot, guildId);
480
- }
481
- if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
482
- const quitMsg = config.basic.smallGroupQuitMessage.replaceAll("{memberCount}", memberCount.toString()).replaceAll("{threshold}", config.basic.smallGroupThreshold.toString()).replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId);
483
- try {
484
- await session.bot.sendMessage(guildId, quitMsg, platform);
485
- } catch (e) {
486
- }
487
- if (config.basic.smallGroupNotifyAdmin) {
488
- const adminMsg = `小群自动退群
489
- 群名称:${groupName}
490
- 群号:${guildId}
491
- 群成员数:${memberCount}人(阈值:${config.basic.smallGroupThreshold}人)
492
- 机器人已自动退出该群。`;
493
- await notifyAdmins(session.bot, config, adminMsg);
494
- }
495
- quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
496
- await markSelfLeft(ctx, guildId);
497
- try {
498
- await session.bot.internal.setGroupLeave(parseInt(guildId));
499
- } catch (e) {
500
- console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
501
- quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
502
- await clearSelfLeft(ctx, guildId);
503
- }
504
- } else if (memberCount > config.basic.smallGroupThreshold) {
568
+ const res = await evaluateSmallGroup(session.bot, guildId);
569
+ let groupName = res.groupName;
570
+ if (groupName === "未知") groupName = await getGroupName(session.bot, guildId);
571
+ if (res.decision === "quit") {
572
+ await performSmallGroupQuit(session.bot, platform, guildId, groupName, res.count);
573
+ } else if (res.decision === "keep") {
505
574
  if (config.basic.smallGroupQualifiedNotifyAdmin) {
506
- const qualifiedMsg = config.basic.smallGroupQualifiedMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId).replaceAll("{memberCount}", memberCount.toString()).replaceAll("{threshold}", config.basic.smallGroupThreshold.toString());
575
+ const qualifiedMsg = config.basic.smallGroupQualifiedMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId).replaceAll("{memberCount}", res.count.toString()).replaceAll("{threshold}", config.basic.smallGroupThreshold.toString());
507
576
  await notifyAdmins(session.bot, config, qualifiedMsg);
508
577
  }
509
578
  }
@@ -520,10 +589,41 @@ function apply2(ctx, config) {
520
589
  }
521
590
  }
522
591
  });
592
+ if (config.basic.smallGroupAutoQuit && config.basic.smallGroupRealtimeMonitor) {
593
+ ctx.on("guild-member-removed", async (session) => {
594
+ const { guildId, platform } = session;
595
+ if (!guildId) return;
596
+ if (String(session.userId) === String(session.bot.selfId)) return;
597
+ if (quittingGuilds.has(`${BLACKLIST_PLATFORM}:${guildId}`)) return;
598
+ const cooldown = (config.basic.smallGroupRecheckCooldown || 60) * 1e3;
599
+ const now = Date.now();
600
+ if (now - (realtimeLastCheck.get(guildId) || 0) < cooldown) return;
601
+ realtimeLastCheck.set(guildId, now);
602
+ if (await isInSmallGroupWhitelist(ctx, guildId)) return;
603
+ if (await isApprovedGuild(ctx, guildId)) return;
604
+ setTimeout(async () => {
605
+ try {
606
+ if (quittingGuilds.has(`${BLACKLIST_PLATFORM}:${guildId}`)) return;
607
+ const res = await evaluateSmallGroup(session.bot, guildId);
608
+ if (res.decision === "quit") {
609
+ let groupName = res.groupName;
610
+ if (groupName === "未知") groupName = await getGroupName(session.bot, guildId);
611
+ await performSmallGroupQuit(session.bot, platform, guildId, groupName, res.count);
612
+ }
613
+ } catch (error) {
614
+ console.error(`实时小群检测失败 (群号: ${guildId}):`, error);
615
+ }
616
+ }, REALTIME_DEBOUNCE_MS);
617
+ });
618
+ }
523
619
  ctx.on("guild-removed", async (session) => {
524
620
  const { guildId } = session;
525
621
  const platform = BLACKLIST_PLATFORM;
526
622
  const dedupKey = `${platform}:${guildId}`;
623
+ try {
624
+ await clearApprovedGuild(ctx, guildId);
625
+ } catch {
626
+ }
527
627
  if (quittingGuilds.has(dedupKey)) {
528
628
  try {
529
629
  await clearSelfLeft(ctx, guildId);
@@ -542,7 +642,7 @@ function apply2(ctx, config) {
542
642
  if (processedKicks.has(dedupKey)) return;
543
643
  processedKicks.set(dedupKey, Date.now());
544
644
  if (config.basic.enableBlacklist || config.basic.notifyAdminOnKick) {
545
- const [existing] = await ctx.model.get("blacklisted_guild", { platform, guildId });
645
+ const [existing] = await ctx.database.get("blacklisted_guild", { platform, guildId });
546
646
  if (existing && existing.reason === "kicked") {
547
647
  return;
548
648
  }
@@ -556,7 +656,7 @@ function apply2(ctx, config) {
556
656
  await notifyAdmins(session.bot, config, kickMsg);
557
657
  }
558
658
  });
559
- if (config.basic.notifyAdminOnMute) {
659
+ if (config.basic.notifyAdminOnMute || config.basic.muteAutoQuit) {
560
660
  ctx.on("guild-member-mute", async (session) => {
561
661
  if (session.userId !== session.bot?.userId) return;
562
662
  if (!session.duration) return;
@@ -564,8 +664,28 @@ function apply2(ctx, config) {
564
664
  const operatorId = session.operatorId || "未知";
565
665
  const duration = session.duration ?? 0;
566
666
  const groupName = await getGroupName(session.bot, guildId);
567
- const msg = config.basic.muteNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName).replaceAll("{operatorId}", operatorId).replaceAll("{duration}", duration.toString());
568
- await notifyAdmins(session.bot, config, msg);
667
+ if (config.basic.muteAutoQuit && duration >= config.basic.muteAutoQuitThreshold) {
668
+ const quitMsg = config.basic.muteQuitNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName).replaceAll("{operatorId}", operatorId).replaceAll("{duration}", duration.toString());
669
+ await notifyAdmins(session.bot, config, quitMsg);
670
+ try {
671
+ await createBlacklistedGuild(ctx, guildId, "muted");
672
+ } catch (e) {
673
+ }
674
+ quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
675
+ await markSelfLeft(ctx, guildId);
676
+ try {
677
+ await session.bot.internal.setGroupLeave(parseInt(guildId));
678
+ } catch (e) {
679
+ console.error(`被禁言自动退群失败 (群号: ${guildId}):`, e);
680
+ quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
681
+ await clearSelfLeft(ctx, guildId);
682
+ }
683
+ return;
684
+ }
685
+ if (config.basic.notifyAdminOnMute) {
686
+ const msg = config.basic.muteNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName).replaceAll("{operatorId}", operatorId).replaceAll("{duration}", duration.toString());
687
+ await notifyAdmins(session.bot, config, msg);
688
+ }
569
689
  });
570
690
  }
571
691
  if (config.basic.quitCommandEnabled) {
@@ -588,7 +708,7 @@ function apply2(ctx, config) {
588
708
  quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
589
709
  await markSelfLeft(ctx, guildId);
590
710
  try {
591
- await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
711
+ await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replaceAll("{userId}", userId), platform);
592
712
  } catch (e) {
593
713
  }
594
714
  try {
@@ -616,9 +736,9 @@ function apply3(ctx, config) {
616
736
  setInterval(async () => {
617
737
  const expireMs = config.invite.inviteExpireDays * 24 * 60 * 60 * 1e3;
618
738
  try {
619
- const count = await clearExpiredPendingInvites(ctx, expireMs);
620
- if (count > 0 && config.invite.showDetailedLog) {
621
- console.log(`已自动清理 ${count} 个过期邀请`);
739
+ await clearExpiredPendingInvites(ctx, expireMs);
740
+ if (config.invite.showDetailedLog) {
741
+ console.log("已执行过期邀请清理");
622
742
  }
623
743
  } catch (error) {
624
744
  console.error("清理过期邀请失败:", error);
@@ -629,6 +749,8 @@ function apply3(ctx, config) {
629
749
  ctx.logger("group-control-invite").info(`[guild-request] 触发!userId=${session.userId}, guildId=${session.guildId}, messageId=${session.messageId}, type=${session.type}, subtype=${session.subtype}`);
630
750
  ctx.logger("group-control-invite").info(`[guild-request] event 对象: ${JSON.stringify(session.event, null, 2)}`);
631
751
  const raw = session.original || session.raw || session.event?._data || {};
752
+ const subType = String(raw.sub_type ?? session.subtype ?? "");
753
+ if (subType && subType !== "invite") return;
632
754
  const flag = raw.flag || session.flag || session.messageId;
633
755
  const rawUserId = raw.user_id ? String(raw.user_id) : session.userId;
634
756
  const rawGroupId = raw.group_id ? String(raw.group_id) : session.guildId;
@@ -683,7 +805,7 @@ function apply3(ctx, config) {
683
805
  if (config.invite.autoApprove) {
684
806
  try {
685
807
  await session.bot.internal.setGroupAddRequest(flag, "invite", true, "");
686
- approvedGroups.add(rawGroupId);
808
+ await markApprovedGuild(ctx, rawGroupId);
687
809
  if (config.invite.showDetailedLog) {
688
810
  console.log(`自动同意群聊邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
689
811
  }
@@ -766,7 +888,7 @@ function apply3(ctx, config) {
766
888
  }
767
889
  try {
768
890
  await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", true, "");
769
- approvedGroups.add(groupId);
891
+ await markApprovedGuild(ctx, groupId);
770
892
  try {
771
893
  await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
772
894
  } catch (error) {
@@ -802,7 +924,7 @@ function apply3(ctx, config) {
802
924
  }
803
925
  });
804
926
  ctx.command("gc.pending", "查看待处理的群聊邀请").action(async ({ session }) => {
805
- if (!config.admin.adminQQs.includes(session.userId)) {
927
+ if (!hasGlobalPermission(session, config)) {
806
928
  return "权限不足,只有管理员可以查看待处理邀请。";
807
929
  }
808
930
  const allInvites = await getAllPendingInvites(ctx);
@@ -917,6 +1039,7 @@ __name(isUserInitiatedNonCommand, "isUserInitiatedNonCommand");
917
1039
  function apply4(ctx, config) {
918
1040
  const freq = config.frequency;
919
1041
  if (!freq.enabled && !freq.privateEnabled) return;
1042
+ const countedSessions = /* @__PURE__ */ new WeakSet();
920
1043
  async function checkFrequency(session, isCommand) {
921
1044
  const isPrivate = !session.guildId;
922
1045
  const platform = session.platform;
@@ -1004,12 +1127,14 @@ function apply4(ctx, config) {
1004
1127
  ctx.on("command/before-execute", async (argv) => {
1005
1128
  const session = argv.session;
1006
1129
  if (isSystemSession(session)) return;
1130
+ if (countedSessions.has(session)) return;
1007
1131
  const allowed = await checkFrequency(session, true);
1008
1132
  if (!allowed) throw new Error("Blocked");
1009
1133
  });
1010
1134
  ctx.middleware(async (session, next) => {
1011
1135
  if (isSystemSession(session)) return next();
1012
1136
  if (!isUserInitiatedNonCommand(session)) return next();
1137
+ countedSessions.add(session);
1013
1138
  const allowed = await checkFrequency(session, false);
1014
1139
  if (!allowed) return;
1015
1140
  return next();
@@ -1024,6 +1149,51 @@ __export(commands_exports, {
1024
1149
  name: () => name5
1025
1150
  });
1026
1151
  var name5 = "group-control-commands";
1152
+ function chunk(arr, size) {
1153
+ const out = [];
1154
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
1155
+ return out;
1156
+ }
1157
+ __name(chunk, "chunk");
1158
+ async function sendAsForward(session, title, lines) {
1159
+ const groups = chunk(lines, 80);
1160
+ const botId = String(session.bot?.selfId || "10000");
1161
+ const nodes = groups.map((g) => ({
1162
+ type: "node",
1163
+ data: { user_id: botId, nickname: title, content: g.join("\n") }
1164
+ }));
1165
+ try {
1166
+ const internal = session.bot.internal;
1167
+ if (session.guildId) {
1168
+ if (typeof internal.sendGroupForwardMsg !== "function") throw new Error("no forward api");
1169
+ await internal.sendGroupForwardMsg(parseInt(session.guildId), nodes);
1170
+ } else {
1171
+ if (typeof internal.sendPrivateForwardMsg !== "function") throw new Error("no forward api");
1172
+ await internal.sendPrivateForwardMsg(parseInt(session.userId), nodes);
1173
+ }
1174
+ } catch {
1175
+ for (const g of groups) {
1176
+ try {
1177
+ await session.send(g.join("\n"));
1178
+ } catch {
1179
+ }
1180
+ }
1181
+ }
1182
+ }
1183
+ __name(sendAsForward, "sendAsForward");
1184
+ async function deleteFriendCompat(bot, userId) {
1185
+ const internal = bot.internal;
1186
+ if (typeof internal?.deleteFriend !== "function") {
1187
+ throw new Error("当前适配器不支持 delete_friend 接口");
1188
+ }
1189
+ const n = parseInt(userId);
1190
+ try {
1191
+ await internal.deleteFriend(n);
1192
+ } catch (e) {
1193
+ await internal.deleteFriend({ user_id: n, friend_id: n, temp_block: false, both_del: false });
1194
+ }
1195
+ }
1196
+ __name(deleteFriendCompat, "deleteFriendCompat");
1027
1197
  function apply5(ctx, config) {
1028
1198
  ctx.command("gc", "群控管理员指令");
1029
1199
  ctx.command("gc.ban <groupId:text>", "添加群聊到黑名单").action(async ({ session }, input) => {
@@ -1088,6 +1258,69 @@ function apply5(ctx, config) {
1088
1258
  if (records.length === 0) return "小群白名单为空。";
1089
1259
  return "小群白名单列表(以下群不受小群人数限制):\n" + records.map((r) => `- ${r.guildId}`).join("\n");
1090
1260
  });
1261
+ ctx.command("gc.friends", "列出机器人的好友(合并转发)").action(async ({ session }) => {
1262
+ if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
1263
+ let list = [];
1264
+ try {
1265
+ const raw = await session.bot.internal.getFriendList();
1266
+ list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
1267
+ } catch (e) {
1268
+ return `获取好友列表失败:${e.message}`;
1269
+ }
1270
+ if (list.length === 0) return "好友列表为空。";
1271
+ const lines = list.map((f, i) => {
1272
+ const uid = f.user_id ?? f.userId ?? "";
1273
+ const name9 = f.remark || f.nickname || f.nick || String(uid);
1274
+ return `${i + 1}. ${name9} (${uid})`;
1275
+ });
1276
+ await sendAsForward(session, `好友列表(共 ${list.length} 个)`, lines);
1277
+ return "";
1278
+ });
1279
+ ctx.command("gc.delfriend <userId:text>", "删除指定好友").action(async ({ session }, input) => {
1280
+ if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
1281
+ const userId = (input || "").trim();
1282
+ if (!/^\d+$/.test(userId)) return "输入格式错误,请输入要删除的好友 QQ 号。";
1283
+ try {
1284
+ await deleteFriendCompat(session.bot, userId);
1285
+ return `已删除好友 ${userId}。`;
1286
+ } catch (e) {
1287
+ return `删除好友失败:${e.message}`;
1288
+ }
1289
+ });
1290
+ ctx.command("gc.groups", "列出机器人所在的群(合并转发)").action(async ({ session }) => {
1291
+ if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
1292
+ let list = [];
1293
+ try {
1294
+ const raw = await session.bot.internal.getGroupList();
1295
+ list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
1296
+ } catch (e) {
1297
+ return `获取群列表失败:${e.message}`;
1298
+ }
1299
+ if (list.length === 0) return "机器人尚未加入任何群。";
1300
+ const lines = list.map((g, i) => {
1301
+ const gid = g.group_id ?? g.groupId ?? "";
1302
+ const gname = g.group_name ?? g.groupName ?? String(gid);
1303
+ const count = g.member_count ?? g.memberCount;
1304
+ const max = g.max_member_count ?? g.maxMemberCount;
1305
+ const sizeInfo = count != null ? `(${count}${max != null ? `/${max}` : ""}人)` : "";
1306
+ return `${i + 1}. ${gname} (${gid})${sizeInfo}`;
1307
+ });
1308
+ await sendAsForward(session, `群列表(共 ${list.length} 个)`, lines);
1309
+ return "";
1310
+ });
1311
+ ctx.command("gc.leave <groupId:text>", "让机器人退出指定群").action(async ({ session }, input) => {
1312
+ if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
1313
+ const guildId = parseGuildId(input);
1314
+ if (!guildId) return "输入格式错误,请输入要退出的群号。";
1315
+ await markSelfLeft(ctx, guildId);
1316
+ try {
1317
+ await session.bot.internal.setGroupLeave(parseInt(guildId));
1318
+ return `已退出群 ${guildId}。`;
1319
+ } catch (e) {
1320
+ await clearSelfLeft(ctx, guildId);
1321
+ return `退出群 ${guildId} 失败:${e.message}`;
1322
+ }
1323
+ });
1091
1324
  }
1092
1325
  __name(apply5, "apply");
1093
1326
 
@@ -1281,7 +1514,7 @@ var Config = import_koishi.Schema.intersect([
1281
1514
  import_koishi.Schema.const("koishi").description("使用 Koishi 自带权限系统 (authority)"),
1282
1515
  import_koishi.Schema.const("builtin").description("使用插件内置权限管理 (群管理员/群主)")
1283
1516
  ]).default("builtin").description("权限管理模式"),
1284
- koishiAuthority: import_koishi.Schema.number().default(3).description("Koishi 模式下管理指令所需的最低权限等级"),
1517
+ koishiAuthority: import_koishi.Schema.natural().default(3).description("Koishi 模式下管理指令所需的最低权限等级"),
1285
1518
  protectedCommands: import_koishi.Schema.array(String).default([]).description("需要群管理员权限才能使用的自定义指令名列表")
1286
1519
  }).description("权限管理")
1287
1520
  }),
@@ -1291,18 +1524,24 @@ var Config = import_koishi.Schema.intersect([
1291
1524
  quitCommandEnabled: import_koishi.Schema.boolean().default(true).description("启用 quit 指令"),
1292
1525
  quitMessage: import_koishi.Schema.string().default("收到来自{userId}的指令,即将退出群聊。").description("quit 指令触发后的群内提示,支持变量 {userId}"),
1293
1526
  enableBlacklist: import_koishi.Schema.boolean().default(true).description("启用被踢出自动拉黑"),
1294
- blacklistMessage: import_koishi.Schema.string().default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后的提示"),
1527
+ blacklistMessage: import_koishi.Schema.string().role("textarea").default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后的提示"),
1295
1528
  notifyAdminOnKick: import_koishi.Schema.boolean().default(true).description("被踢出群时通知管理员"),
1296
- kickNotificationMessage: import_koishi.Schema.string().default("机器人已被踢出群聊\n群名称:{groupName}\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知模板,支持变量 {groupId}, {groupName}"),
1529
+ kickNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人已被踢出群聊\n群名称:{groupName}\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知模板,支持变量 {groupId}, {groupName}"),
1297
1530
  smallGroupAutoQuit: import_koishi.Schema.boolean().default(false).description("启用小群自动退群"),
1298
- smallGroupThreshold: import_koishi.Schema.number().default(30).description("小群人数阈值(低于等于此值时自动退群)"),
1299
- smallGroupCheckDelay: import_koishi.Schema.number().default(3e3).description("加入后延迟检测时间(毫秒)"),
1300
- smallGroupQuitMessage: import_koishi.Schema.string().default("该群人数过少({memberCount}人),不满足最低人数要求({threshold}人),机器人将自动退出。").description("小群退群提示,支持变量 {memberCount}, {threshold}, {groupName}, {groupId}"),
1531
+ smallGroupThreshold: import_koishi.Schema.natural().default(30).description("小群人数阈值(低于等于此值时自动退群)"),
1532
+ smallGroupCheckDelay: import_koishi.Schema.natural().default(3e3).description("加入后延迟检测时间(毫秒)"),
1533
+ smallGroupExcludeOfficialBots: import_koishi.Schema.boolean().default(true).description("统计群人数时排除 QQ 官方机器人(is_robot)及机器人自身,仅统计真人成员"),
1534
+ smallGroupRealtimeMonitor: import_koishi.Schema.boolean().default(true).description("实时监控群人数(监听成员退群事件,群缩小到阈值以下时自动退群)"),
1535
+ smallGroupRecheckCooldown: import_koishi.Schema.natural().default(60).description("实时监控:同一群两次复检的最小间隔(秒),避免成员批量退群时频繁调用接口"),
1536
+ smallGroupQuitMessage: import_koishi.Schema.string().role("textarea").default("该群人数过少({memberCount}人),不满足最低人数要求({threshold}人),机器人将自动退出。").description("小群退群提示,支持变量 {memberCount}, {threshold}, {groupName}, {groupId}"),
1301
1537
  smallGroupNotifyAdmin: import_koishi.Schema.boolean().default(true).description("小群自动退群时通知管理员"),
1302
1538
  smallGroupQualifiedNotifyAdmin: import_koishi.Schema.boolean().default(true).description("未经审核被拉入人数达标的群时通知管理员"),
1303
- smallGroupQualifiedMessage: import_koishi.Schema.string().default("机器人被未经审核地拉入群聊\n群名称:{groupName}\n群号:{groupId}\n当前人数:{memberCount}人(阈值:{threshold}人)\n请确认是否保留。").description("合格小群通知模板,支持变量 {groupName}, {groupId}, {memberCount}, {threshold}"),
1539
+ smallGroupQualifiedMessage: import_koishi.Schema.string().role("textarea").default("机器人被未经审核地拉入群聊\n群名称:{groupName}\n群号:{groupId}\n当前人数:{memberCount}人(阈值:{threshold}人)\n请确认是否保留。").description("合格小群通知模板,支持变量 {groupName}, {groupId}, {memberCount}, {threshold}"),
1304
1540
  notifyAdminOnMute: import_koishi.Schema.boolean().default(false).description("机器人被禁言时通知管理员"),
1305
- muteNotificationMessage: import_koishi.Schema.string().default("机器人在群聊中被禁言\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}")
1541
+ muteNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人在群聊中被禁言\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}"),
1542
+ muteAutoQuit: import_koishi.Schema.boolean().default(false).description("机器人被禁言达到阈值时自动退群并拉黑"),
1543
+ muteAutoQuitThreshold: import_koishi.Schema.natural().default(600).description("触发自动退群的禁言时长阈值(秒),被禁言时长 ≥ 此值即退群并拉黑"),
1544
+ muteQuitNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人被长时间禁言,已自动退群并拉黑\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言自动退群时发给管理员的通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}")
1306
1545
  }).description("基础群组管理")
1307
1546
  }),
1308
1547
  import_koishi.Schema.object({
@@ -1310,11 +1549,11 @@ var Config = import_koishi.Schema.intersect([
1310
1549
  enabled: import_koishi.Schema.boolean().default(false).description("启用群聊邀请审核"),
1311
1550
  autoApprove: import_koishi.Schema.boolean().default(false).description("自动同意邀请"),
1312
1551
  notifyAdminOnApprove: import_koishi.Schema.boolean().default(true).description("自动同意时是否仍通知管理员"),
1313
- inviteWaitMessage: import_koishi.Schema.string().default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发给邀请者的等待提示"),
1314
- inviteApproveMessage: import_koishi.Schema.string().default("已自动通过您的群聊邀请,机器人正在加入群聊。").description("自动同意时发给邀请者的提示,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1315
- inviteRequestMessage: import_koishi.Schema.string().default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请使用指令 gc.approve {groupId} 同意或 gc.reject {groupId} 拒绝。").description("发给管理员的请求消息模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1316
- inviteApproveNotificationMessage: import_koishi.Schema.string().default("已自动通过群聊邀请\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})").description("自动同意时发给管理员的通知模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1317
- inviteExpireDays: import_koishi.Schema.number().default(3).description("邀请记录过期天数"),
1552
+ inviteWaitMessage: import_koishi.Schema.string().role("textarea").default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发给邀请者的等待提示"),
1553
+ inviteApproveMessage: import_koishi.Schema.string().role("textarea").default("已自动通过您的群聊邀请,机器人正在加入群聊。").description("自动同意时发给邀请者的提示,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1554
+ inviteRequestMessage: import_koishi.Schema.string().role("textarea").default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请使用指令 gc.approve {groupId} 同意或 gc.reject {groupId} 拒绝。").description("发给管理员的请求消息模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1555
+ inviteApproveNotificationMessage: import_koishi.Schema.string().role("textarea").default("已自动通过群聊邀请\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})").description("自动同意时发给管理员的通知模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
1556
+ inviteExpireDays: import_koishi.Schema.natural().default(3).description("邀请记录过期天数"),
1318
1557
  showDetailedLog: import_koishi.Schema.boolean().default(false).description("显示详细日志")
1319
1558
  }).description("群聊邀请审核")
1320
1559
  }),
@@ -1323,28 +1562,28 @@ var Config = import_koishi.Schema.intersect([
1323
1562
  enabled: import_koishi.Schema.boolean().default(false).description("启用好友申请管理"),
1324
1563
  autoApprove: import_koishi.Schema.boolean().default(false).description("自动通过好友申请(否则通知管理员手动处理)"),
1325
1564
  notifyAdminOnApprove: import_koishi.Schema.boolean().default(true).description("自动通过时是否仍通知管理员"),
1326
- requestExpireDays: import_koishi.Schema.number().default(7).description("待处理申请的过期天数"),
1327
- requestMessage: import_koishi.Schema.string().default("收到新的好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}\n\n使用 gc.fa {userId} 同意或 gc.fr {userId} 拒绝。").description("通知管理员的消息模板,支持变量 {userId}, {nickname}, {comment}"),
1328
- approveNotificationMessage: import_koishi.Schema.string().default("已自动通过好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}").description("自动通过时的通知模板,支持变量 {userId}, {nickname}, {comment}")
1565
+ requestExpireDays: import_koishi.Schema.natural().default(7).description("待处理申请的过期天数"),
1566
+ requestMessage: import_koishi.Schema.string().role("textarea").default("收到新的好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}\n\n使用 gc.fa {userId} 同意或 gc.fr {userId} 拒绝。").description("通知管理员的消息模板,支持变量 {userId}, {nickname}, {comment}"),
1567
+ approveNotificationMessage: import_koishi.Schema.string().role("textarea").default("已自动通过好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}").description("自动通过时的通知模板,支持变量 {userId}, {nickname}, {comment}")
1329
1568
  }).description("好友申请管理")
1330
1569
  }),
1331
1570
  import_koishi.Schema.object({
1332
1571
  frequency: import_koishi.Schema.object({
1333
1572
  enabled: import_koishi.Schema.boolean().default(false).description("启用群聊频率控制(指令及 @ 对话均受限)"),
1334
- limit: import_koishi.Schema.number().default(5).description("群聊:时间窗口内允许的最大触发次数"),
1335
- window: import_koishi.Schema.number().default(60).description("群聊:时间窗口(秒)"),
1336
- warnDelay: import_koishi.Schema.number().default(30).description("群聊:警告后再次触发的时间阈值(秒),超出则进入屏蔽"),
1337
- blockDur: import_koishi.Schema.number().default(300).description("群聊:首次屏蔽的基础时长(秒)"),
1573
+ limit: import_koishi.Schema.natural().default(5).description("群聊:时间窗口内允许的最大触发次数"),
1574
+ window: import_koishi.Schema.natural().default(60).description("群聊:时间窗口(秒)"),
1575
+ warnDelay: import_koishi.Schema.natural().default(30).description("群聊:警告后再次触发的时间阈值(秒),超出则进入屏蔽"),
1576
+ blockDur: import_koishi.Schema.natural().default(300).description("群聊:首次屏蔽的基础时长(秒)"),
1338
1577
  whitelist: import_koishi.Schema.array(String).default([]).description("群聊:不受频率限制的群号列表"),
1339
1578
  privateEnabled: import_koishi.Schema.boolean().default(false).description("启用私聊频率控制"),
1340
- privateLimit: import_koishi.Schema.number().default(10).description("私聊:时间窗口内允许的最大触发次数"),
1341
- privateWindow: import_koishi.Schema.number().default(60).description("私聊:时间窗口(秒)"),
1342
- privateWarnDelay: import_koishi.Schema.number().default(30).description("私聊:警告后再次触发的时间阈值(秒)"),
1343
- privateBlockDur: import_koishi.Schema.number().default(300).description("私聊:首次屏蔽的基础时长(秒)"),
1579
+ privateLimit: import_koishi.Schema.natural().default(10).description("私聊:时间窗口内允许的最大触发次数"),
1580
+ privateWindow: import_koishi.Schema.natural().default(60).description("私聊:时间窗口(秒)"),
1581
+ privateWarnDelay: import_koishi.Schema.natural().default(30).description("私聊:警告后再次触发的时间阈值(秒)"),
1582
+ privateBlockDur: import_koishi.Schema.natural().default(300).description("私聊:首次屏蔽的基础时长(秒)"),
1344
1583
  privateWhitelist: import_koishi.Schema.array(String).default([]).description("私聊:不受频率限制的用户ID列表"),
1345
- blockExpBase: import_koishi.Schema.number().default(2).description("屏蔽时长指数增长底数(时长 = blockDur × base^(次数-1)),设为 1 禁用"),
1346
- blockExpWindow: import_koishi.Schema.number().default(3600).description("指数增长重置窗口(秒),从最后一次屏蔽结束计算,超出则重置次数"),
1347
- blockNotifyCooldown: import_koishi.Schema.number().default(60).description("屏蔽期间提示消息的冷却时间(秒),避免刷屏"),
1584
+ blockExpBase: import_koishi.Schema.natural().min(1).default(2).description("屏蔽时长指数增长底数(时长 = blockDur × base^(次数-1)),设为 1 禁用"),
1585
+ blockExpWindow: import_koishi.Schema.natural().default(3600).description("指数增长重置窗口(秒),从最后一次屏蔽结束计算,超出则重置次数"),
1586
+ blockNotifyCooldown: import_koishi.Schema.natural().default(60).description("屏蔽期间提示消息的冷却时间(秒),避免刷屏"),
1348
1587
  warnMsg: import_koishi.Schema.string().default("发言频率过高,请慢一点~").description("首次超限警告消息"),
1349
1588
  blockMsg: import_koishi.Schema.string().default("发言频率过高,已被禁用 {duration} 秒。").description("进入屏蔽时的通知,支持变量 {duration}"),
1350
1589
  blockedMsg: import_koishi.Schema.string().default("暂时被禁用,还有 {time} 秒解禁。").description("屏蔽期间再次触发时的提示,支持变量 {time}")
@@ -1354,13 +1593,14 @@ var Config = import_koishi.Schema.intersect([
1354
1593
  botSwitch: import_koishi.Schema.object({
1355
1594
  enabled: import_koishi.Schema.boolean().default(true).description("启用群聊 bot 开关"),
1356
1595
  defaultState: import_koishi.Schema.boolean().default(true).description("默认开启状态"),
1357
- disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用 bot-on 开启。").description("关闭状态下被 @ 时的提示")
1596
+ disabledMessage: import_koishi.Schema.string().role("textarea").default("机器人当前在此群处于关闭状态,请使用 bot-on 开启。").description("关闭状态下被 @ 时的提示")
1358
1597
  }).description("机器人开关控制")
1359
1598
  })
1360
1599
  ]);
1361
1600
 
1362
1601
  // src/index.ts
1363
1602
  var name8 = "group-control";
1603
+ var inject = ["database"];
1364
1604
  function apply8(ctx, config) {
1365
1605
  ctx.plugin(database_exports);
1366
1606
  ctx.plugin(basic_exports, config);
@@ -1375,5 +1615,6 @@ __name(apply8, "apply");
1375
1615
  0 && (module.exports = {
1376
1616
  Config,
1377
1617
  apply,
1618
+ inject,
1378
1619
  name
1379
1620
  });
package/lib/utils.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Session } from 'koishi';
2
2
  import { Config } from './config';
3
3
  export declare function isBlacklistEnabled(config: Config['basic']): string | null;
4
- export declare function parseGuildId(input: string): string | null;
4
+ export declare function parseGuildId(input: string | undefined | null): string | null;
5
5
  export declare function formatDate(timestamp: number): string;
6
6
  export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
7
7
  /** 是否为全局管理员(填在 adminQQs 里的) */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-control",
3
3
  "description": "Koishi 插件,一个多功能的群聊自管理工具。支持被踢出自动拉黑、刷屏自动屏蔽、开关控制等功能。(仅支持 OneBot 适配器)",
4
- "version": "1.0.4",
4
+ "version": "1.0.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -11,13 +11,15 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
11
11
  ## 功能概览
12
12
 
13
13
  - **黑名单管理**:被踢出群后自动拉黑,下次被邀请时自动退出
14
- - **小群自动退群**:加入人数不足的群时自动退出并通知管理员
14
+ - **小群自动退群**:加入人数不足的群时自动退出并通知管理员,统计人数时自动排除 QQ 官方机器人(仅计真人)
15
+ - **实时小群监控**:监听成员退群事件,群缩水到阈值以下时自动退出(仅针对未经审核拉入的群)
15
16
  - **合格小群通知**:未经审核被拉入人数达标的群时,通知管理员确认
16
17
  - **群聊邀请审核**:收到邀请时暂缓加入,等待管理员审核
17
18
  - **好友申请管理**:收到好友申请时通知管理员,或自动通过
18
19
  - **频率控制**:限制群聊/私聊的指令及对话频率,支持指数增长屏蔽时长
19
20
  - **Bot 开关**:按群独立开关 bot,关闭后屏蔽所有响应
20
- - **被禁言通知**:bot 被禁言时可选通知管理员
21
+ - **好友/群管理**:指令列出好友(合并转发)、删除好友、列出所在群、远程退出指定群
22
+ - **被禁言通知**:bot 被禁言时可选通知管理员;被禁言时长达到阈值时可自动退群并拉黑
21
23
  - **权限管理**:支持 Koishi authority 或内置群管理员两种权限模式
22
24
 
23
25
  ---
@@ -69,6 +71,9 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
69
71
  |--------|--------|------|
70
72
  | `smallGroupAutoQuit` | `false` | 启用小群自动退群 |
71
73
  | `smallGroupThreshold` | `30` | 人数阈值,低于等于此值时自动退出 |
74
+ | `smallGroupExcludeOfficialBots` | `true` | 统计群人数时排除 QQ 官方机器人(`is_robot`)及机器人自身,仅统计真人成员 |
75
+ | `smallGroupRealtimeMonitor` | `true` | 实时监控群人数:监听成员退群事件,群缩小到阈值以下时自动退群(仅监控未经审核拉入的群) |
76
+ | `smallGroupRecheckCooldown` | `60` | 实时监控时同一群两次复检的最小间隔(秒),避免成员批量退群时频繁调用接口 |
72
77
  | `smallGroupQuitMessage` | *(见配置)* | 退群提示,支持 `{memberCount}`, `{threshold}`, `{groupName}`, `{groupId}` |
73
78
  | `smallGroupNotifyAdmin` | `true` | 自动退群时通知管理员 |
74
79
  | `smallGroupCheckDelay` | `3000` | 加入后延迟检测的时间(毫秒) |
@@ -86,6 +91,9 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
86
91
  |--------|--------|------|
87
92
  | `notifyAdminOnMute` | `false` | bot 被禁言时通知管理员 |
88
93
  | `muteNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
94
+ | `muteAutoQuit` | `false` | bot 被禁言达到阈值时自动退群并拉黑 |
95
+ | `muteAutoQuitThreshold` | `600` | 触发自动退群的禁言时长阈值(秒),被禁言时长 ≥ 此值即退群并拉黑 |
96
+ | `muteQuitNotificationMessage` | *(见配置)* | 自动退群时发给管理员的通知模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
89
97
 
90
98
  ### 频率控制
91
99
 
@@ -174,6 +182,10 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
174
182
  | `gc.sg-add <群号>` | 将群加入小群白名单,不受人数限制 |
175
183
  | `gc.sg-rm <群号>` | 从小群白名单移除群 |
176
184
  | `gc.sg-list` | 查看小群白名单 |
185
+ | `gc.friends` | 列出机器人的好友(合并转发形式发送) |
186
+ | `gc.delfriend <QQ号>` | 删除指定好友 |
187
+ | `gc.groups` | 列出机器人所在的群(合并转发形式发送) |
188
+ | `gc.leave <群号>` | 让机器人退出指定群 |
177
189
  | `gc.approve <群号>` | 同意加入指定群 |
178
190
  | `gc.reject <群号>` | 拒绝加入指定群 |
179
191
  | `gc.pending` | 查看待处理的群聊邀请列表 |
@@ -197,3 +209,5 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
197
209
  - 管理员通知依赖 `admin.adminQQs` 或 `admin.notificationGroupId` 的配置,未配置则无法收到通知
198
210
  - 频率控制的非指令拦截(@ 对话、私聊)不影响入群欢迎等系统事件
199
211
  - 小群合格通知仅在启用了 `smallGroupAutoQuit` 且未经 `gc.approve` 审核通过的情况下触发
212
+ - **小群人数统计**:开启 `smallGroupExcludeOfficialBots` 后只计真人成员(排除 `is_robot` 机器人与自身)。检测做了分级短路以减少接口调用——原始人数 ≤ 阈值直接退群、原始人数 > 阈值 + 20(单群机器人上限)直接保留,仅当人数处于中间区间时才拉取一次成员列表,且统计到足够机器人即提前结束遍历
213
+ - **实时小群监控**:纯事件驱动(仅监听成员退群),不做轮询,配合 per-群冷却限流,几乎不增加接口压力。**经 `gc.approve` 审核通过或在小群白名单中的群永久豁免**,仅监控未经审核被拉入的群;机器人退出某群后其审核标记自动清除,若日后被未经审核地重新拉入会重新接受检测
package/lib/state.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * 跨模块共享状态
3
- * 用于在 invite 和 basic 等模块间传递信息
4
- */
5
- /** 管理员已审核通过的群号集合(approve 指令通过后添加) */
6
- export declare const approvedGroups: Set<string>;