koishi-plugin-group-control 1.0.1 → 1.0.3
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 +21 -21
- package/lib/database.d.ts +96 -80
- package/lib/index.d.ts +5 -5
- package/lib/index.js +135 -21
- package/lib/modules/basic.d.ts +4 -4
- package/lib/modules/commands.d.ts +4 -4
- package/lib/modules/frequency.d.ts +4 -4
- package/lib/modules/friend.d.ts +4 -4
- package/lib/modules/invite.d.ts +4 -4
- package/lib/modules/switch.d.ts +4 -4
- package/lib/state.d.ts +6 -6
- package/lib/utils.d.ts +17 -17
- package/package.json +1 -1
- package/readme.md +199 -199
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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/database.d.ts
CHANGED
|
@@ -1,80 +1,96 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
export interface BlacklistedGuild {
|
|
3
|
-
platform: string;
|
|
4
|
-
guildId: string;
|
|
5
|
-
timestamp: number;
|
|
6
|
-
reason: string;
|
|
7
|
-
}
|
|
8
|
-
export interface CommandFrequencyRecord {
|
|
9
|
-
platform: string;
|
|
10
|
-
guildId: string;
|
|
11
|
-
commandCount: number;
|
|
12
|
-
lastCommandTime: number;
|
|
13
|
-
warningSent: boolean;
|
|
14
|
-
blockExpiryTime: number;
|
|
15
|
-
firstWarningTime: number;
|
|
16
|
-
blockCount: number;
|
|
17
|
-
lastBlockNotifyTime: number;
|
|
18
|
-
}
|
|
19
|
-
export interface GroupBotStatus {
|
|
20
|
-
platform: string;
|
|
21
|
-
guildId: string;
|
|
22
|
-
botEnabled: boolean;
|
|
23
|
-
}
|
|
24
|
-
export interface SmallGroupWhitelist {
|
|
25
|
-
platform: string;
|
|
26
|
-
guildId: string;
|
|
27
|
-
}
|
|
28
|
-
export interface
|
|
29
|
-
platform: string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export declare
|
|
62
|
-
export declare function
|
|
63
|
-
export declare
|
|
64
|
-
export declare function
|
|
65
|
-
export declare function
|
|
66
|
-
export declare function
|
|
67
|
-
export declare function
|
|
68
|
-
export declare function
|
|
69
|
-
|
|
70
|
-
export declare function
|
|
71
|
-
|
|
72
|
-
export declare function
|
|
73
|
-
|
|
74
|
-
export declare function
|
|
75
|
-
|
|
76
|
-
export declare function
|
|
77
|
-
|
|
78
|
-
export declare function
|
|
79
|
-
export declare function
|
|
80
|
-
export declare function
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
export interface BlacklistedGuild {
|
|
3
|
+
platform: string;
|
|
4
|
+
guildId: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CommandFrequencyRecord {
|
|
9
|
+
platform: string;
|
|
10
|
+
guildId: string;
|
|
11
|
+
commandCount: number;
|
|
12
|
+
lastCommandTime: number;
|
|
13
|
+
warningSent: boolean;
|
|
14
|
+
blockExpiryTime: number;
|
|
15
|
+
firstWarningTime: number;
|
|
16
|
+
blockCount: number;
|
|
17
|
+
lastBlockNotifyTime: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupBotStatus {
|
|
20
|
+
platform: string;
|
|
21
|
+
guildId: string;
|
|
22
|
+
botEnabled: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SmallGroupWhitelist {
|
|
25
|
+
platform: string;
|
|
26
|
+
guildId: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SelfLeftGuild {
|
|
29
|
+
platform: string;
|
|
30
|
+
guildId: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
export interface PendingInvite {
|
|
34
|
+
platform: string;
|
|
35
|
+
groupId: string;
|
|
36
|
+
userId: string;
|
|
37
|
+
userName: string;
|
|
38
|
+
groupName: string;
|
|
39
|
+
time: number;
|
|
40
|
+
flag: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PendingFriendRequest {
|
|
43
|
+
platform: string;
|
|
44
|
+
userId: string;
|
|
45
|
+
nickname: string;
|
|
46
|
+
comment: string;
|
|
47
|
+
flag: string;
|
|
48
|
+
time: number;
|
|
49
|
+
}
|
|
50
|
+
declare module 'koishi' {
|
|
51
|
+
interface Tables {
|
|
52
|
+
blacklisted_guild: BlacklistedGuild;
|
|
53
|
+
command_frequency_record: CommandFrequencyRecord;
|
|
54
|
+
group_bot_status: GroupBotStatus;
|
|
55
|
+
small_group_whitelist: SmallGroupWhitelist;
|
|
56
|
+
self_left_guild: SelfLeftGuild;
|
|
57
|
+
pending_invite: PendingInvite;
|
|
58
|
+
pending_friend_request: PendingFriendRequest;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export declare const name = "group-control-database";
|
|
62
|
+
export declare function apply(ctx: Context): void;
|
|
63
|
+
export declare const BLACKLIST_PLATFORM = "onebot";
|
|
64
|
+
export declare function getBlacklistedGuild(ctx: Context, guildId: string): Promise<BlacklistedGuild[]>;
|
|
65
|
+
export declare function removeBlacklistedGuild(ctx: Context, guildId: string): Promise<import("minato").Driver.WriteResult>;
|
|
66
|
+
export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<import("minato").Driver.WriteResult>;
|
|
67
|
+
export declare function getAllBlacklistedGuilds(ctx: Context): Promise<BlacklistedGuild[]>;
|
|
68
|
+
export declare function clearBlacklistedGuilds(ctx: Context): Promise<import("minato").Driver.WriteResult>;
|
|
69
|
+
/** 统一写入被踢黑名单行,保证 platform = BLACKLIST_PLATFORM */
|
|
70
|
+
export declare function blacklistKicked(ctx: Context, guildId: string): Promise<import("minato").Driver.WriteResult>;
|
|
71
|
+
/** 在主动退群前写入标记,让 guild-removed 能区分「自己退的」和「被踢的」*/
|
|
72
|
+
export declare function markSelfLeft(ctx: Context, guildId: string): Promise<void>;
|
|
73
|
+
/** 消费标记(单次读取后删除),返回是否在 maxAgeSec 内。用于 guild-removed 判断是自己退的 */
|
|
74
|
+
export declare function consumeSelfLeft(ctx: Context, guildId: string, maxAgeSec?: number): Promise<boolean>;
|
|
75
|
+
/** 清理标记(退群失败时回滚,或 unban 时清理)*/
|
|
76
|
+
export declare function clearSelfLeft(ctx: Context, guildId: string): Promise<void>;
|
|
77
|
+
/** 定期清理过期的主动退群标记(超过 maxAgeSec 秒未消费的)*/
|
|
78
|
+
export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<number>;
|
|
79
|
+
export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
|
|
80
|
+
export declare function updateCommandFrequencyRecord(ctx: Context, platform: string, guildId: string, data: Partial<CommandFrequencyRecord>): Promise<void>;
|
|
81
|
+
export declare function getGroupBotStatus(ctx: Context, platform: string, guildId: string): Promise<GroupBotStatus | null>;
|
|
82
|
+
export declare function setGroupBotStatus(ctx: Context, platform: string, guildId: string, botEnabled: boolean): Promise<void>;
|
|
83
|
+
export declare function isInSmallGroupWhitelist(ctx: Context, guildId: string): Promise<boolean>;
|
|
84
|
+
export declare function addToSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
|
|
85
|
+
export declare function removeFromSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
|
|
86
|
+
export declare function getAllSmallGroupWhitelist(ctx: Context): Promise<SmallGroupWhitelist[]>;
|
|
87
|
+
export declare function getPendingInvite(ctx: Context, groupId: string): Promise<PendingInvite>;
|
|
88
|
+
export declare function addPendingInvite(ctx: Context, inviteUser: Omit<PendingInvite, 'platform'>): Promise<void>;
|
|
89
|
+
export declare function removePendingInvite(ctx: Context, groupId: string): Promise<void>;
|
|
90
|
+
export declare function getAllPendingInvites(ctx: Context): Promise<PendingInvite[]>;
|
|
91
|
+
export declare function clearExpiredPendingInvites(ctx: Context, expireTimeMs: number): Promise<number>;
|
|
92
|
+
export declare function getPendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<PendingFriendRequest>;
|
|
93
|
+
export declare function addPendingFriendRequest(ctx: Context, platform: string, data: Omit<PendingFriendRequest, 'platform'>): Promise<void>;
|
|
94
|
+
export declare function removePendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<void>;
|
|
95
|
+
export declare function getAllPendingFriendRequests(ctx: Context, platform: string): Promise<PendingFriendRequest[]>;
|
|
96
|
+
export declare function clearExpiredPendingFriendRequests(ctx: Context, platform: string, expireTimeMs: number): Promise<number>;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from './config';
|
|
3
|
-
export * from './config';
|
|
4
|
-
export declare const name = "group-control";
|
|
5
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
export * from './config';
|
|
4
|
+
export declare const name = "group-control";
|
|
5
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -34,9 +34,13 @@ __export(database_exports, {
|
|
|
34
34
|
addPendingInvite: () => addPendingInvite,
|
|
35
35
|
addToSmallGroupWhitelist: () => addToSmallGroupWhitelist,
|
|
36
36
|
apply: () => apply,
|
|
37
|
+
blacklistKicked: () => blacklistKicked,
|
|
37
38
|
clearBlacklistedGuilds: () => clearBlacklistedGuilds,
|
|
38
39
|
clearExpiredPendingFriendRequests: () => clearExpiredPendingFriendRequests,
|
|
39
40
|
clearExpiredPendingInvites: () => clearExpiredPendingInvites,
|
|
41
|
+
clearExpiredSelfLeft: () => clearExpiredSelfLeft,
|
|
42
|
+
clearSelfLeft: () => clearSelfLeft,
|
|
43
|
+
consumeSelfLeft: () => consumeSelfLeft,
|
|
40
44
|
createBlacklistedGuild: () => createBlacklistedGuild,
|
|
41
45
|
getAllBlacklistedGuilds: () => getAllBlacklistedGuilds,
|
|
42
46
|
getAllPendingFriendRequests: () => getAllPendingFriendRequests,
|
|
@@ -48,6 +52,7 @@ __export(database_exports, {
|
|
|
48
52
|
getPendingFriendRequest: () => getPendingFriendRequest,
|
|
49
53
|
getPendingInvite: () => getPendingInvite,
|
|
50
54
|
isInSmallGroupWhitelist: () => isInSmallGroupWhitelist,
|
|
55
|
+
markSelfLeft: () => markSelfLeft,
|
|
51
56
|
name: () => name,
|
|
52
57
|
removeBlacklistedGuild: () => removeBlacklistedGuild,
|
|
53
58
|
removeFromSmallGroupWhitelist: () => removeFromSmallGroupWhitelist,
|
|
@@ -84,6 +89,11 @@ function apply(ctx) {
|
|
|
84
89
|
platform: "string",
|
|
85
90
|
guildId: "string"
|
|
86
91
|
}, { primary: ["platform", "guildId"] });
|
|
92
|
+
ctx.model.extend("self_left_guild", {
|
|
93
|
+
platform: "string",
|
|
94
|
+
guildId: "string",
|
|
95
|
+
timestamp: "integer"
|
|
96
|
+
}, { primary: ["platform", "guildId"] });
|
|
87
97
|
ctx.model.extend("pending_invite", {
|
|
88
98
|
platform: "string",
|
|
89
99
|
groupId: "string",
|
|
@@ -129,6 +139,44 @@ async function clearBlacklistedGuilds(ctx) {
|
|
|
129
139
|
return await ctx.model.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
|
|
130
140
|
}
|
|
131
141
|
__name(clearBlacklistedGuilds, "clearBlacklistedGuilds");
|
|
142
|
+
async function blacklistKicked(ctx, guildId) {
|
|
143
|
+
return await ctx.model.upsert("blacklisted_guild", [{
|
|
144
|
+
platform: BLACKLIST_PLATFORM,
|
|
145
|
+
guildId,
|
|
146
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
147
|
+
reason: "kicked"
|
|
148
|
+
}]);
|
|
149
|
+
}
|
|
150
|
+
__name(blacklistKicked, "blacklistKicked");
|
|
151
|
+
async function markSelfLeft(ctx, guildId) {
|
|
152
|
+
await ctx.model.upsert("self_left_guild", [{
|
|
153
|
+
platform: BLACKLIST_PLATFORM,
|
|
154
|
+
guildId,
|
|
155
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
156
|
+
}]);
|
|
157
|
+
}
|
|
158
|
+
__name(markSelfLeft, "markSelfLeft");
|
|
159
|
+
async function consumeSelfLeft(ctx, guildId, maxAgeSec = 120) {
|
|
160
|
+
const [row] = await ctx.model.get("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
161
|
+
if (!row) return false;
|
|
162
|
+
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
163
|
+
return Math.floor(Date.now() / 1e3) - row.timestamp <= maxAgeSec;
|
|
164
|
+
}
|
|
165
|
+
__name(consumeSelfLeft, "consumeSelfLeft");
|
|
166
|
+
async function clearSelfLeft(ctx, guildId) {
|
|
167
|
+
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
168
|
+
}
|
|
169
|
+
__name(clearSelfLeft, "clearSelfLeft");
|
|
170
|
+
async function clearExpiredSelfLeft(ctx, maxAgeSec = 300) {
|
|
171
|
+
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;
|
|
178
|
+
}
|
|
179
|
+
__name(clearExpiredSelfLeft, "clearExpiredSelfLeft");
|
|
132
180
|
async function getCommandFrequencyRecord(ctx, platform, guildId) {
|
|
133
181
|
const records = await ctx.model.get("command_frequency_record", { platform, guildId });
|
|
134
182
|
return records.length > 0 ? records[0] : null;
|
|
@@ -142,13 +190,20 @@ async function updateCommandFrequencyRecord(ctx, platform, guildId, data) {
|
|
|
142
190
|
}]);
|
|
143
191
|
}
|
|
144
192
|
__name(updateCommandFrequencyRecord, "updateCommandFrequencyRecord");
|
|
193
|
+
var groupBotStatusCache = /* @__PURE__ */ new Map();
|
|
194
|
+
var groupBotStatusCacheKey = /* @__PURE__ */ __name((platform, guildId) => `${platform}:${guildId}`, "groupBotStatusCacheKey");
|
|
145
195
|
async function getGroupBotStatus(ctx, platform, guildId) {
|
|
196
|
+
const key = groupBotStatusCacheKey(platform, guildId);
|
|
197
|
+
if (groupBotStatusCache.has(key)) return groupBotStatusCache.get(key);
|
|
146
198
|
const records = await ctx.model.get("group_bot_status", { platform, guildId });
|
|
147
|
-
|
|
199
|
+
const status = records.length > 0 ? records[0] : null;
|
|
200
|
+
groupBotStatusCache.set(key, status);
|
|
201
|
+
return status;
|
|
148
202
|
}
|
|
149
203
|
__name(getGroupBotStatus, "getGroupBotStatus");
|
|
150
204
|
async function setGroupBotStatus(ctx, platform, guildId, botEnabled) {
|
|
151
205
|
await ctx.model.upsert("group_bot_status", [{ platform, guildId, botEnabled }]);
|
|
206
|
+
groupBotStatusCache.set(groupBotStatusCacheKey(platform, guildId), { platform, guildId, botEnabled });
|
|
152
207
|
}
|
|
153
208
|
__name(setGroupBotStatus, "setGroupBotStatus");
|
|
154
209
|
async function isInSmallGroupWhitelist(ctx, guildId) {
|
|
@@ -360,17 +415,24 @@ function apply2(ctx, config) {
|
|
|
360
415
|
if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
|
|
361
416
|
}
|
|
362
417
|
}, 30 * 1e3);
|
|
418
|
+
setInterval(async () => {
|
|
419
|
+
try {
|
|
420
|
+
await clearExpiredSelfLeft(ctx);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}, 5 * 60 * 1e3);
|
|
363
424
|
ctx.on("guild-added", async (session) => {
|
|
364
425
|
const { guildId, platform } = session;
|
|
365
426
|
ctx.logger("group-control-basic").info(`[guild-added] 触发!guildId=${guildId}, platform=${platform}`);
|
|
366
427
|
if (config.basic.enableBlacklist) {
|
|
367
|
-
const [blacklisted] = await ctx.model.get("blacklisted_guild", { platform, guildId });
|
|
428
|
+
const [blacklisted] = await ctx.model.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
368
429
|
if (blacklisted) {
|
|
369
430
|
try {
|
|
370
431
|
await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
|
|
371
432
|
} catch (e) {
|
|
372
433
|
}
|
|
373
|
-
quittingGuilds.set(`${
|
|
434
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
435
|
+
await markSelfLeft(ctx, guildId);
|
|
374
436
|
try {
|
|
375
437
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
376
438
|
} catch (e) {
|
|
@@ -430,12 +492,14 @@ function apply2(ctx, config) {
|
|
|
430
492
|
机器人已自动退出该群。`;
|
|
431
493
|
await notifyAdmins(session.bot, config, adminMsg);
|
|
432
494
|
}
|
|
433
|
-
quittingGuilds.set(`${
|
|
495
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
496
|
+
await markSelfLeft(ctx, guildId);
|
|
434
497
|
try {
|
|
435
498
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
436
499
|
} catch (e) {
|
|
437
500
|
console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
|
|
438
|
-
quittingGuilds.delete(`${
|
|
501
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
502
|
+
await clearSelfLeft(ctx, guildId);
|
|
439
503
|
}
|
|
440
504
|
} else if (memberCount > config.basic.smallGroupThreshold) {
|
|
441
505
|
if (config.basic.smallGroupQualifiedNotifyAdmin) {
|
|
@@ -457,25 +521,37 @@ function apply2(ctx, config) {
|
|
|
457
521
|
}
|
|
458
522
|
});
|
|
459
523
|
ctx.on("guild-removed", async (session) => {
|
|
460
|
-
const { guildId
|
|
461
|
-
const
|
|
462
|
-
|
|
524
|
+
const { guildId } = session;
|
|
525
|
+
const platform = BLACKLIST_PLATFORM;
|
|
526
|
+
const dedupKey = `${platform}:${guildId}`;
|
|
527
|
+
if (quittingGuilds.has(dedupKey)) {
|
|
528
|
+
try {
|
|
529
|
+
await clearSelfLeft(ctx, guildId);
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
463
532
|
return;
|
|
464
533
|
}
|
|
465
|
-
|
|
466
|
-
|
|
534
|
+
const isSelfLeft = await consumeSelfLeft(ctx, guildId);
|
|
535
|
+
if (isSelfLeft) return;
|
|
536
|
+
const raw = session.event?._data || session.original || session.onebot || {};
|
|
537
|
+
const subType = String(raw.sub_type ?? session.subtype ?? "");
|
|
538
|
+
const eventTs = typeof raw.time === "number" ? raw.time : Math.floor(Date.now() / 1e3);
|
|
539
|
+
const ageSec = Math.floor(Date.now() / 1e3) - eventTs;
|
|
540
|
+
if (subType === "leave") return;
|
|
541
|
+
if (!subType && ageSec > 60) return;
|
|
542
|
+
if (processedKicks.has(dedupKey)) return;
|
|
543
|
+
processedKicks.set(dedupKey, Date.now());
|
|
544
|
+
if (config.basic.enableBlacklist || config.basic.notifyAdminOnKick) {
|
|
545
|
+
const [existing] = await ctx.model.get("blacklisted_guild", { platform, guildId });
|
|
546
|
+
if (existing && existing.reason === "kicked") {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
467
549
|
}
|
|
468
|
-
processedKicks.set(quittingKey, Date.now());
|
|
469
|
-
const groupName = await getGroupName(session.bot, guildId);
|
|
470
550
|
if (config.basic.enableBlacklist) {
|
|
471
|
-
await ctx
|
|
472
|
-
platform,
|
|
473
|
-
guildId,
|
|
474
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
475
|
-
reason: "kicked"
|
|
476
|
-
}]);
|
|
551
|
+
await blacklistKicked(ctx, guildId);
|
|
477
552
|
}
|
|
478
553
|
if (config.basic.notifyAdminOnKick) {
|
|
554
|
+
const groupName = await getGroupName(session.bot, guildId);
|
|
479
555
|
const kickMsg = config.basic.kickNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName);
|
|
480
556
|
await notifyAdmins(session.bot, config, kickMsg);
|
|
481
557
|
}
|
|
@@ -509,7 +585,8 @@ function apply2(ctx, config) {
|
|
|
509
585
|
群名称:${groupName}
|
|
510
586
|
群号:${guildId}`;
|
|
511
587
|
await notifyAdmins(session.bot, config, adminMsg);
|
|
512
|
-
quittingGuilds.set(`${
|
|
588
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
589
|
+
await markSelfLeft(ctx, guildId);
|
|
513
590
|
try {
|
|
514
591
|
await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
|
|
515
592
|
} catch (e) {
|
|
@@ -517,7 +594,8 @@ function apply2(ctx, config) {
|
|
|
517
594
|
try {
|
|
518
595
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
519
596
|
} catch (e) {
|
|
520
|
-
quittingGuilds.delete(`${
|
|
597
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
598
|
+
await clearSelfLeft(ctx, guildId);
|
|
521
599
|
return `退出失败: ${e.message}`;
|
|
522
600
|
}
|
|
523
601
|
return "";
|
|
@@ -555,6 +633,33 @@ function apply3(ctx, config) {
|
|
|
555
633
|
const rawUserId = raw.user_id ? String(raw.user_id) : session.userId;
|
|
556
634
|
const rawGroupId = raw.group_id ? String(raw.group_id) : session.guildId;
|
|
557
635
|
const { platform } = session;
|
|
636
|
+
if (config.basic.enableBlacklist && rawGroupId && /^\d+$/.test(String(rawGroupId))) {
|
|
637
|
+
const bl = await getBlacklistedGuild(ctx, String(rawGroupId));
|
|
638
|
+
if (bl.length > 0) {
|
|
639
|
+
try {
|
|
640
|
+
await session.bot.internal.setGroupAddRequest(flag, "invite", false, "该群已被机器人拉黑");
|
|
641
|
+
} catch (e) {
|
|
642
|
+
ctx.logger("group-control-invite").warn("拒绝黑名单群邀请失败 (flag 可能已失效):", e);
|
|
643
|
+
}
|
|
644
|
+
const rejectNotify = `已自动拒绝黑名单群邀请
|
|
645
|
+
群号:${rawGroupId}
|
|
646
|
+
邀请者 QQ:${rawUserId}
|
|
647
|
+
如需放行请先执行 gc.unban ${rawGroupId} 再让对方重新邀请。`;
|
|
648
|
+
try {
|
|
649
|
+
await notifyAdmins(session.bot, config, rejectNotify);
|
|
650
|
+
} catch (e) {
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const rejectMsg = `您邀请加入的群 ${rawGroupId} 已被机器人拉黑,邀请已被自动拒绝。如有疑问请联系机器人管理员。`;
|
|
654
|
+
await session.bot.sendPrivateMessage(rawUserId, rejectMsg);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
}
|
|
657
|
+
if (config.invite.showDetailedLog) {
|
|
658
|
+
console.log(`已自动拒绝黑名单群邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
558
663
|
if (!flag && config.invite.showDetailedLog) {
|
|
559
664
|
console.warn("未能提取到邀请 flag,可能导致无法处理邀请。Raw event:", JSON.stringify(raw));
|
|
560
665
|
}
|
|
@@ -648,6 +753,12 @@ function apply3(ctx, config) {
|
|
|
648
753
|
ctx.command("gc.approve <groupId:string>", "同意群聊邀请").action(async ({ session }, groupId) => {
|
|
649
754
|
if (!groupId) return "请指定群号。用法:gc.approve <群号>";
|
|
650
755
|
if (!hasGlobalPermission(session, config)) return "权限不足,只有管理员可以审核邀请。";
|
|
756
|
+
if (config.basic.enableBlacklist) {
|
|
757
|
+
const bl = await getBlacklistedGuild(ctx, groupId);
|
|
758
|
+
if (bl.length > 0) {
|
|
759
|
+
return `群 ${groupId} 在黑名单中,无法通过审核。如需放行请先执行 gc.unban ${groupId}。`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
651
762
|
const inviteData = await getPendingInvite(ctx, groupId);
|
|
652
763
|
if (!inviteData) {
|
|
653
764
|
const allInvites = await getAllPendingInvites(ctx);
|
|
@@ -933,6 +1044,7 @@ function apply5(ctx, config) {
|
|
|
933
1044
|
const guildId = parseGuildId(input);
|
|
934
1045
|
if (!guildId) return `输入格式错误。`;
|
|
935
1046
|
const removed = await removeBlacklistedGuild(ctx, guildId);
|
|
1047
|
+
await clearSelfLeft(ctx, guildId);
|
|
936
1048
|
return removed ? `已移除群聊 ${guildId}` : `群聊 ${guildId} 不在黑名单中。`;
|
|
937
1049
|
});
|
|
938
1050
|
ctx.command("gc.banlist", "查看黑名单").action(async ({ session }) => {
|
|
@@ -1050,7 +1162,9 @@ function apply6(ctx, config) {
|
|
|
1050
1162
|
if (isBotEnabled) {
|
|
1051
1163
|
return next();
|
|
1052
1164
|
}
|
|
1053
|
-
|
|
1165
|
+
const content = session.stripped?.content ?? session.content ?? "";
|
|
1166
|
+
const firstWord = content.match(/^\s*[/.!!。]*(\S+)/)?.[1];
|
|
1167
|
+
if (firstWord && ADMIN_COMMANDS.has(firstWord)) {
|
|
1054
1168
|
return next();
|
|
1055
1169
|
}
|
|
1056
1170
|
const isMentioned = session.elements?.some((e) => e.type === "at" && e.attrs.id === session.bot.userId);
|
package/lib/modules/basic.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-basic";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-basic";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-commands";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-commands";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-frequency";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-frequency";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/modules/friend.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-friend";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-friend";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/modules/invite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-invite";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-invite";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/modules/switch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
import { Config } from '../config';
|
|
3
|
-
export declare const name = "group-control-switch";
|
|
4
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
export declare const name = "group-control-switch";
|
|
4
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/state.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 跨模块共享状态
|
|
3
|
-
* 用于在 invite 和 basic 等模块间传递信息
|
|
4
|
-
*/
|
|
5
|
-
/** 管理员已审核通过的群号集合(approve 指令通过后添加) */
|
|
6
|
-
export declare const approvedGroups: Set<string>;
|
|
1
|
+
/**
|
|
2
|
+
* 跨模块共享状态
|
|
3
|
+
* 用于在 invite 和 basic 等模块间传递信息
|
|
4
|
+
*/
|
|
5
|
+
/** 管理员已审核通过的群号集合(approve 指令通过后添加) */
|
|
6
|
+
export declare const approvedGroups: Set<string>;
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { Session } from 'koishi';
|
|
2
|
-
import { Config } from './config';
|
|
3
|
-
export declare function isBlacklistEnabled(config: Config['basic']): string | null;
|
|
4
|
-
export declare function parseGuildId(input: string): string | null;
|
|
5
|
-
export declare function formatDate(timestamp: number): string;
|
|
6
|
-
export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
|
|
7
|
-
/** 是否为全局管理员(填在 adminQQs 里的) */
|
|
8
|
-
export declare function isGlobalAdmin(session: Session, config: Config): boolean;
|
|
9
|
-
/**
|
|
10
|
-
* 检查群级权限(bot-on/off、quit、protectedCommands)
|
|
11
|
-
* builtin 模式:群管理员或全局管理员均可
|
|
12
|
-
* koishi 模式:由 authority 决定
|
|
13
|
-
*/
|
|
14
|
-
export declare function hasGuildPermission(session: Session, config: Config): Promise<boolean>;
|
|
15
|
-
export declare function hasGlobalPermission(session: Session, config: Config): boolean;
|
|
16
|
-
/** 管理指令列表 - 这些指令始终不受 bot-off 影响 */
|
|
17
|
-
export declare const ADMIN_COMMANDS: Set<string>;
|
|
1
|
+
import { Session } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
export declare function isBlacklistEnabled(config: Config['basic']): string | null;
|
|
4
|
+
export declare function parseGuildId(input: string): string | null;
|
|
5
|
+
export declare function formatDate(timestamp: number): string;
|
|
6
|
+
export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
|
|
7
|
+
/** 是否为全局管理员(填在 adminQQs 里的) */
|
|
8
|
+
export declare function isGlobalAdmin(session: Session, config: Config): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* 检查群级权限(bot-on/off、quit、protectedCommands)
|
|
11
|
+
* builtin 模式:群管理员或全局管理员均可
|
|
12
|
+
* koishi 模式:由 authority 决定
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasGuildPermission(session: Session, config: Config): Promise<boolean>;
|
|
15
|
+
export declare function hasGlobalPermission(session: Session, config: Config): boolean;
|
|
16
|
+
/** 管理指令列表 - 这些指令始终不受 bot-off 影响 */
|
|
17
|
+
export declare const ADMIN_COMMANDS: Set<string>;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
# koishi-plugin-group-control
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/koishi-plugin-group-control)
|
|
4
|
-
|
|
5
|
-
Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
6
|
-
|
|
7
|
-
> 使用 Qwen3-Coder & Gemini-3.1-Pro-Preview & Claude Sonnet/Opus 4.6 协助完成
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 功能概览
|
|
12
|
-
|
|
13
|
-
- **黑名单管理**:被踢出群后自动拉黑,下次被邀请时自动退出
|
|
14
|
-
- **小群自动退群**:加入人数不足的群时自动退出并通知管理员
|
|
15
|
-
- **合格小群通知**:未经审核被拉入人数达标的群时,通知管理员确认
|
|
16
|
-
- **群聊邀请审核**:收到邀请时暂缓加入,等待管理员审核
|
|
17
|
-
- **好友申请管理**:收到好友申请时通知管理员,或自动通过
|
|
18
|
-
- **频率控制**:限制群聊/私聊的指令及对话频率,支持指数增长屏蔽时长
|
|
19
|
-
- **Bot 开关**:按群独立开关 bot,关闭后屏蔽所有响应
|
|
20
|
-
- **被禁言通知**:bot 被禁言时可选通知管理员
|
|
21
|
-
- **权限管理**:支持 Koishi authority 或内置群管理员两种权限模式
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 配置说明
|
|
26
|
-
|
|
27
|
-
### 管理员配置
|
|
28
|
-
|
|
29
|
-
| 配置项 | 默认值 | 说明 |
|
|
30
|
-
|--------|--------|------|
|
|
31
|
-
| `admin.adminQQs` | `[]` | 管理员QQ号列表(权限验证及通知) |
|
|
32
|
-
| `admin.notificationGroupId` | *(空)* | 通知群号(填写后发到此群,否则私聊管理员) |
|
|
33
|
-
|
|
34
|
-
### 权限管理
|
|
35
|
-
|
|
36
|
-
| 配置项 | 默认值 | 说明 |
|
|
37
|
-
|--------|--------|------|
|
|
38
|
-
| `mode` | `builtin` | `builtin`:使用群管理员/群主权限;`koishi`:使用 Koishi authority |
|
|
39
|
-
| `koishiAuthority` | `3` | Koishi 模式下管理指令所需的最低权限等级 |
|
|
40
|
-
| `protectedCommands` | `[]` | 需要群管理员权限才能使用的自定义指令名列表 |
|
|
41
|
-
|
|
42
|
-
### 基础群组管理
|
|
43
|
-
|
|
44
|
-
**欢迎 & 退群**
|
|
45
|
-
|
|
46
|
-
| 配置项 | 默认值 | 说明 |
|
|
47
|
-
|--------|--------|------|
|
|
48
|
-
| `welcomeMessage` | `你好,我是机器人。` | 加入群聊时发送的欢迎消息 |
|
|
49
|
-
| `quitMessage` | `收到来自{userId}的指令,即将退出群聊。` | quit 指令触发后的群内提示,支持 `{userId}` |
|
|
50
|
-
| `quitCommandEnabled` | `true` | 是否启用 quit 指令 |
|
|
51
|
-
|
|
52
|
-
**黑名单**
|
|
53
|
-
|
|
54
|
-
| 配置项 | 默认值 | 说明 |
|
|
55
|
-
|--------|--------|------|
|
|
56
|
-
| `enableBlacklist` | `true` | 启用被踢出自动拉黑 |
|
|
57
|
-
| `blacklistMessage` | *(见配置)* | 被拉入黑名单群后的提示 |
|
|
58
|
-
|
|
59
|
-
**踢出通知**
|
|
60
|
-
|
|
61
|
-
| 配置项 | 默认值 | 说明 |
|
|
62
|
-
|--------|--------|------|
|
|
63
|
-
| `notifyAdminOnKick` | `true` | 被踢出时通知管理员 |
|
|
64
|
-
| `kickNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}` |
|
|
65
|
-
|
|
66
|
-
**小群自动退群**
|
|
67
|
-
|
|
68
|
-
| 配置项 | 默认值 | 说明 |
|
|
69
|
-
|--------|--------|------|
|
|
70
|
-
| `smallGroupAutoQuit` | `false` | 启用小群自动退群 |
|
|
71
|
-
| `smallGroupThreshold` | `30` | 人数阈值,低于等于此值时自动退出 |
|
|
72
|
-
| `smallGroupQuitMessage` | *(见配置)* | 退群提示,支持 `{memberCount}`, `{threshold}`, `{groupName}`, `{groupId}` |
|
|
73
|
-
| `smallGroupNotifyAdmin` | `true` | 自动退群时通知管理员 |
|
|
74
|
-
| `smallGroupCheckDelay` | `3000` | 加入后延迟检测的时间(毫秒) |
|
|
75
|
-
|
|
76
|
-
**合格小群通知**
|
|
77
|
-
|
|
78
|
-
| 配置项 | 默认值 | 说明 |
|
|
79
|
-
|--------|--------|------|
|
|
80
|
-
| `smallGroupQualifiedNotifyAdmin` | `true` | 未经审核被拉入人数达标的群时通知管理员 |
|
|
81
|
-
| `smallGroupQualifiedMessage` | *(见配置)* | 通知消息模板,支持 `{groupName}`, `{groupId}`, `{memberCount}`, `{threshold}` |
|
|
82
|
-
|
|
83
|
-
**被禁言通知**
|
|
84
|
-
|
|
85
|
-
| 配置项 | 默认值 | 说明 |
|
|
86
|
-
|--------|--------|------|
|
|
87
|
-
| `notifyAdminOnMute` | `false` | bot 被禁言时通知管理员 |
|
|
88
|
-
| `muteNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
|
|
89
|
-
|
|
90
|
-
### 频率控制
|
|
91
|
-
|
|
92
|
-
**群聊**
|
|
93
|
-
|
|
94
|
-
| 配置项 | 默认值 | 说明 |
|
|
95
|
-
|--------|--------|------|
|
|
96
|
-
| `enabled` | `false` | 启用群聊频率控制(指令及 @ 对话均受限) |
|
|
97
|
-
| `limit` | `5` | 时间窗口内允许的最大触发次数 |
|
|
98
|
-
| `window` | `60` | 时间窗口(秒) |
|
|
99
|
-
| `warnDelay` | `30` | 警告后再次触发的时间阈值(秒),超出则进入屏蔽 |
|
|
100
|
-
| `blockDur` | `300` | 首次屏蔽的基础时长(秒) |
|
|
101
|
-
| `whitelist` | `[]` | 不受频率限制的群号列表 |
|
|
102
|
-
|
|
103
|
-
**私聊**
|
|
104
|
-
|
|
105
|
-
| 配置项 | 默认值 | 说明 |
|
|
106
|
-
|--------|--------|------|
|
|
107
|
-
| `privateEnabled` | `false` | 启用私聊频率控制 |
|
|
108
|
-
| `privateLimit` | `10` | 私聊时间窗口内允许的最大触发次数 |
|
|
109
|
-
| `privateWindow` | `60` | 私聊时间窗口(秒) |
|
|
110
|
-
| `privateWarnDelay` | `30` | 私聊警告后再次触发的时间阈值(秒) |
|
|
111
|
-
| `privateBlockDur` | `300` | 私聊首次屏蔽的基础时长(秒) |
|
|
112
|
-
| `privateWhitelist` | `[]` | 不受私聊频率限制的用户ID列表 |
|
|
113
|
-
|
|
114
|
-
**指数增长(群聊和私聊共用)**
|
|
115
|
-
|
|
116
|
-
| 配置项 | 默认值 | 说明 |
|
|
117
|
-
|--------|--------|------|
|
|
118
|
-
| `blockExpBase` | `2` | 指数增长底数,每次屏蔽时长 = `blockDur × base^(次数-1)`,设为 `1` 禁用 |
|
|
119
|
-
| `blockExpWindow` | `3600` | 指数增长重置窗口(秒),从最后一次屏蔽结束计算,超出则重置屏蔽次数 |
|
|
120
|
-
|
|
121
|
-
**提示消息**
|
|
122
|
-
|
|
123
|
-
| 配置项 | 默认值 | 说明 |
|
|
124
|
-
|--------|--------|------|
|
|
125
|
-
| `blockNotifyCooldown` | `60` | 屏蔽期间重复触发时提示消息的冷却时间(秒),避免刷屏 |
|
|
126
|
-
| `warnMsg` | `发言频率过高,请慢一点~` | 首次超限警告消息 |
|
|
127
|
-
| `blockMsg` | `发言频率过高,已被禁用 {duration} 秒。` | 进入屏蔽时的通知,支持 `{duration}` |
|
|
128
|
-
| `blockedMsg` | `暂时被禁用,还有 {time} 秒解禁。` | 屏蔽期间再次触发时的提示,支持 `{time}` |
|
|
129
|
-
|
|
130
|
-
### 好友申请管理
|
|
131
|
-
|
|
132
|
-
| 配置项 | 默认值 | 说明 |
|
|
133
|
-
|--------|--------|------|
|
|
134
|
-
| `enabled` | `false` | 启用好友申请管理 |
|
|
135
|
-
| `autoApprove` | `false` | 自动通过好友申请,否则通知管理员手动处理 |
|
|
136
|
-
| `notifyAdminOnApprove` | `true` | 自动通过时是否仍通知管理员 |
|
|
137
|
-
| `requestExpireDays` | `7` | 待处理申请的过期天数 |
|
|
138
|
-
| `requestMessage` | *(见配置)* | 通知管理员的消息模板,支持 `{userId}`, `{nickname}`, `{comment}` |
|
|
139
|
-
| `approveNotificationMessage` | *(见配置)* | 自动通过时的通知消息模板,支持 `{userId}`, `{nickname}`, `{comment}` |
|
|
140
|
-
|
|
141
|
-
### 群聊邀请审核
|
|
142
|
-
|
|
143
|
-
| 配置项 | 默认值 | 说明 |
|
|
144
|
-
|--------|--------|------|
|
|
145
|
-
| `enabled` | `false` | 启用邀请审核 |
|
|
146
|
-
| `inviteWaitMessage` | *(见配置)* | 发给邀请者的等待提示 |
|
|
147
|
-
| `inviteRequestMessage` | *(见配置)* | 发给管理员的请求消息,支持 `{groupName}`, `{groupId}`, `{userName}`, `{userId}` |
|
|
148
|
-
| `autoApprove` | `false` | 未配置管理员时自动同意邀请 |
|
|
149
|
-
| `showDetailedLog` | `false` | 显示详细日志 |
|
|
150
|
-
| `inviteExpireDays` | `3` | 邀请记录过期天数 |
|
|
151
|
-
|
|
152
|
-
### 机器人开关
|
|
153
|
-
|
|
154
|
-
| 配置项 | 默认值 | 说明 |
|
|
155
|
-
|--------|--------|------|
|
|
156
|
-
| `enabled` | `true` | 启用群聊 bot 开关功能 |
|
|
157
|
-
| `defaultState` | `true` | 默认开启状态 |
|
|
158
|
-
| `disabledMessage` | *(见配置)* | 关闭状态下被 @ 时的提示 |
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## 指令列表
|
|
163
|
-
|
|
164
|
-
所有管理员指令统一收纳在 `gc` 主指令下,输入 `gc` 可查看子指令列表。
|
|
165
|
-
|
|
166
|
-
### 管理员指令(gc 子指令,需全局管理员)
|
|
167
|
-
|
|
168
|
-
| 指令 | 说明 |
|
|
169
|
-
|------|------|
|
|
170
|
-
| `gc.banlist` | 查看黑名单列表 |
|
|
171
|
-
| `gc.ban <群号>` | 添加群到黑名单 |
|
|
172
|
-
| `gc.unban <群号>` | 从黑名单移除群 |
|
|
173
|
-
| `gc.clearban` | 清空黑名单 |
|
|
174
|
-
| `gc.sg-add <群号>` | 将群加入小群白名单,不受人数限制 |
|
|
175
|
-
| `gc.sg-rm <群号>` | 从小群白名单移除群 |
|
|
176
|
-
| `gc.sg-list` | 查看小群白名单 |
|
|
177
|
-
| `gc.approve <群号>` | 同意加入指定群 |
|
|
178
|
-
| `gc.reject <群号>` | 拒绝加入指定群 |
|
|
179
|
-
| `gc.pending` | 查看待处理的群聊邀请列表 |
|
|
180
|
-
| `gc.fa <QQ号>` | 同意好友申请 |
|
|
181
|
-
| `gc.fr <QQ号>` | 拒绝好友申请 |
|
|
182
|
-
| `gc.fp` | 查看待处理的好友申请列表 |
|
|
183
|
-
|
|
184
|
-
### 群聊管理
|
|
185
|
-
|
|
186
|
-
| 指令 | 说明 | 权限 |
|
|
187
|
-
|------|------|------|
|
|
188
|
-
| `quit` | 让 bot 退出当前群聊 | 群管理员 |
|
|
189
|
-
| `bot-on` | 开启当前群的 bot | 群管理员 |
|
|
190
|
-
| `bot-off` | 关闭当前群的 bot | 群管理员 |
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## 注意事项
|
|
195
|
-
|
|
196
|
-
- 本插件仅支持 **OneBot 适配器**(如 go-cqhttp、LLOneBot 等)
|
|
197
|
-
- 管理员通知依赖 `admin.adminQQs` 或 `admin.notificationGroupId` 的配置,未配置则无法收到通知
|
|
198
|
-
- 频率控制的非指令拦截(@ 对话、私聊)不影响入群欢迎等系统事件
|
|
199
|
-
- 小群合格通知仅在启用了 `smallGroupAutoQuit` 且未经 `gc.approve` 审核通过的情况下触发
|
|
1
|
+
# koishi-plugin-group-control
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-group-control)
|
|
4
|
+
|
|
5
|
+
Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
6
|
+
|
|
7
|
+
> 使用 Qwen3-Coder & Gemini-3.1-Pro-Preview & Claude Sonnet/Opus 4.6 协助完成
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 功能概览
|
|
12
|
+
|
|
13
|
+
- **黑名单管理**:被踢出群后自动拉黑,下次被邀请时自动退出
|
|
14
|
+
- **小群自动退群**:加入人数不足的群时自动退出并通知管理员
|
|
15
|
+
- **合格小群通知**:未经审核被拉入人数达标的群时,通知管理员确认
|
|
16
|
+
- **群聊邀请审核**:收到邀请时暂缓加入,等待管理员审核
|
|
17
|
+
- **好友申请管理**:收到好友申请时通知管理员,或自动通过
|
|
18
|
+
- **频率控制**:限制群聊/私聊的指令及对话频率,支持指数增长屏蔽时长
|
|
19
|
+
- **Bot 开关**:按群独立开关 bot,关闭后屏蔽所有响应
|
|
20
|
+
- **被禁言通知**:bot 被禁言时可选通知管理员
|
|
21
|
+
- **权限管理**:支持 Koishi authority 或内置群管理员两种权限模式
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 配置说明
|
|
26
|
+
|
|
27
|
+
### 管理员配置
|
|
28
|
+
|
|
29
|
+
| 配置项 | 默认值 | 说明 |
|
|
30
|
+
|--------|--------|------|
|
|
31
|
+
| `admin.adminQQs` | `[]` | 管理员QQ号列表(权限验证及通知) |
|
|
32
|
+
| `admin.notificationGroupId` | *(空)* | 通知群号(填写后发到此群,否则私聊管理员) |
|
|
33
|
+
|
|
34
|
+
### 权限管理
|
|
35
|
+
|
|
36
|
+
| 配置项 | 默认值 | 说明 |
|
|
37
|
+
|--------|--------|------|
|
|
38
|
+
| `mode` | `builtin` | `builtin`:使用群管理员/群主权限;`koishi`:使用 Koishi authority |
|
|
39
|
+
| `koishiAuthority` | `3` | Koishi 模式下管理指令所需的最低权限等级 |
|
|
40
|
+
| `protectedCommands` | `[]` | 需要群管理员权限才能使用的自定义指令名列表 |
|
|
41
|
+
|
|
42
|
+
### 基础群组管理
|
|
43
|
+
|
|
44
|
+
**欢迎 & 退群**
|
|
45
|
+
|
|
46
|
+
| 配置项 | 默认值 | 说明 |
|
|
47
|
+
|--------|--------|------|
|
|
48
|
+
| `welcomeMessage` | `你好,我是机器人。` | 加入群聊时发送的欢迎消息 |
|
|
49
|
+
| `quitMessage` | `收到来自{userId}的指令,即将退出群聊。` | quit 指令触发后的群内提示,支持 `{userId}` |
|
|
50
|
+
| `quitCommandEnabled` | `true` | 是否启用 quit 指令 |
|
|
51
|
+
|
|
52
|
+
**黑名单**
|
|
53
|
+
|
|
54
|
+
| 配置项 | 默认值 | 说明 |
|
|
55
|
+
|--------|--------|------|
|
|
56
|
+
| `enableBlacklist` | `true` | 启用被踢出自动拉黑 |
|
|
57
|
+
| `blacklistMessage` | *(见配置)* | 被拉入黑名单群后的提示 |
|
|
58
|
+
|
|
59
|
+
**踢出通知**
|
|
60
|
+
|
|
61
|
+
| 配置项 | 默认值 | 说明 |
|
|
62
|
+
|--------|--------|------|
|
|
63
|
+
| `notifyAdminOnKick` | `true` | 被踢出时通知管理员 |
|
|
64
|
+
| `kickNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}` |
|
|
65
|
+
|
|
66
|
+
**小群自动退群**
|
|
67
|
+
|
|
68
|
+
| 配置项 | 默认值 | 说明 |
|
|
69
|
+
|--------|--------|------|
|
|
70
|
+
| `smallGroupAutoQuit` | `false` | 启用小群自动退群 |
|
|
71
|
+
| `smallGroupThreshold` | `30` | 人数阈值,低于等于此值时自动退出 |
|
|
72
|
+
| `smallGroupQuitMessage` | *(见配置)* | 退群提示,支持 `{memberCount}`, `{threshold}`, `{groupName}`, `{groupId}` |
|
|
73
|
+
| `smallGroupNotifyAdmin` | `true` | 自动退群时通知管理员 |
|
|
74
|
+
| `smallGroupCheckDelay` | `3000` | 加入后延迟检测的时间(毫秒) |
|
|
75
|
+
|
|
76
|
+
**合格小群通知**
|
|
77
|
+
|
|
78
|
+
| 配置项 | 默认值 | 说明 |
|
|
79
|
+
|--------|--------|------|
|
|
80
|
+
| `smallGroupQualifiedNotifyAdmin` | `true` | 未经审核被拉入人数达标的群时通知管理员 |
|
|
81
|
+
| `smallGroupQualifiedMessage` | *(见配置)* | 通知消息模板,支持 `{groupName}`, `{groupId}`, `{memberCount}`, `{threshold}` |
|
|
82
|
+
|
|
83
|
+
**被禁言通知**
|
|
84
|
+
|
|
85
|
+
| 配置项 | 默认值 | 说明 |
|
|
86
|
+
|--------|--------|------|
|
|
87
|
+
| `notifyAdminOnMute` | `false` | bot 被禁言时通知管理员 |
|
|
88
|
+
| `muteNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
|
|
89
|
+
|
|
90
|
+
### 频率控制
|
|
91
|
+
|
|
92
|
+
**群聊**
|
|
93
|
+
|
|
94
|
+
| 配置项 | 默认值 | 说明 |
|
|
95
|
+
|--------|--------|------|
|
|
96
|
+
| `enabled` | `false` | 启用群聊频率控制(指令及 @ 对话均受限) |
|
|
97
|
+
| `limit` | `5` | 时间窗口内允许的最大触发次数 |
|
|
98
|
+
| `window` | `60` | 时间窗口(秒) |
|
|
99
|
+
| `warnDelay` | `30` | 警告后再次触发的时间阈值(秒),超出则进入屏蔽 |
|
|
100
|
+
| `blockDur` | `300` | 首次屏蔽的基础时长(秒) |
|
|
101
|
+
| `whitelist` | `[]` | 不受频率限制的群号列表 |
|
|
102
|
+
|
|
103
|
+
**私聊**
|
|
104
|
+
|
|
105
|
+
| 配置项 | 默认值 | 说明 |
|
|
106
|
+
|--------|--------|------|
|
|
107
|
+
| `privateEnabled` | `false` | 启用私聊频率控制 |
|
|
108
|
+
| `privateLimit` | `10` | 私聊时间窗口内允许的最大触发次数 |
|
|
109
|
+
| `privateWindow` | `60` | 私聊时间窗口(秒) |
|
|
110
|
+
| `privateWarnDelay` | `30` | 私聊警告后再次触发的时间阈值(秒) |
|
|
111
|
+
| `privateBlockDur` | `300` | 私聊首次屏蔽的基础时长(秒) |
|
|
112
|
+
| `privateWhitelist` | `[]` | 不受私聊频率限制的用户ID列表 |
|
|
113
|
+
|
|
114
|
+
**指数增长(群聊和私聊共用)**
|
|
115
|
+
|
|
116
|
+
| 配置项 | 默认值 | 说明 |
|
|
117
|
+
|--------|--------|------|
|
|
118
|
+
| `blockExpBase` | `2` | 指数增长底数,每次屏蔽时长 = `blockDur × base^(次数-1)`,设为 `1` 禁用 |
|
|
119
|
+
| `blockExpWindow` | `3600` | 指数增长重置窗口(秒),从最后一次屏蔽结束计算,超出则重置屏蔽次数 |
|
|
120
|
+
|
|
121
|
+
**提示消息**
|
|
122
|
+
|
|
123
|
+
| 配置项 | 默认值 | 说明 |
|
|
124
|
+
|--------|--------|------|
|
|
125
|
+
| `blockNotifyCooldown` | `60` | 屏蔽期间重复触发时提示消息的冷却时间(秒),避免刷屏 |
|
|
126
|
+
| `warnMsg` | `发言频率过高,请慢一点~` | 首次超限警告消息 |
|
|
127
|
+
| `blockMsg` | `发言频率过高,已被禁用 {duration} 秒。` | 进入屏蔽时的通知,支持 `{duration}` |
|
|
128
|
+
| `blockedMsg` | `暂时被禁用,还有 {time} 秒解禁。` | 屏蔽期间再次触发时的提示,支持 `{time}` |
|
|
129
|
+
|
|
130
|
+
### 好友申请管理
|
|
131
|
+
|
|
132
|
+
| 配置项 | 默认值 | 说明 |
|
|
133
|
+
|--------|--------|------|
|
|
134
|
+
| `enabled` | `false` | 启用好友申请管理 |
|
|
135
|
+
| `autoApprove` | `false` | 自动通过好友申请,否则通知管理员手动处理 |
|
|
136
|
+
| `notifyAdminOnApprove` | `true` | 自动通过时是否仍通知管理员 |
|
|
137
|
+
| `requestExpireDays` | `7` | 待处理申请的过期天数 |
|
|
138
|
+
| `requestMessage` | *(见配置)* | 通知管理员的消息模板,支持 `{userId}`, `{nickname}`, `{comment}` |
|
|
139
|
+
| `approveNotificationMessage` | *(见配置)* | 自动通过时的通知消息模板,支持 `{userId}`, `{nickname}`, `{comment}` |
|
|
140
|
+
|
|
141
|
+
### 群聊邀请审核
|
|
142
|
+
|
|
143
|
+
| 配置项 | 默认值 | 说明 |
|
|
144
|
+
|--------|--------|------|
|
|
145
|
+
| `enabled` | `false` | 启用邀请审核 |
|
|
146
|
+
| `inviteWaitMessage` | *(见配置)* | 发给邀请者的等待提示 |
|
|
147
|
+
| `inviteRequestMessage` | *(见配置)* | 发给管理员的请求消息,支持 `{groupName}`, `{groupId}`, `{userName}`, `{userId}` |
|
|
148
|
+
| `autoApprove` | `false` | 未配置管理员时自动同意邀请 |
|
|
149
|
+
| `showDetailedLog` | `false` | 显示详细日志 |
|
|
150
|
+
| `inviteExpireDays` | `3` | 邀请记录过期天数 |
|
|
151
|
+
|
|
152
|
+
### 机器人开关
|
|
153
|
+
|
|
154
|
+
| 配置项 | 默认值 | 说明 |
|
|
155
|
+
|--------|--------|------|
|
|
156
|
+
| `enabled` | `true` | 启用群聊 bot 开关功能 |
|
|
157
|
+
| `defaultState` | `true` | 默认开启状态 |
|
|
158
|
+
| `disabledMessage` | *(见配置)* | 关闭状态下被 @ 时的提示 |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 指令列表
|
|
163
|
+
|
|
164
|
+
所有管理员指令统一收纳在 `gc` 主指令下,输入 `gc` 可查看子指令列表。
|
|
165
|
+
|
|
166
|
+
### 管理员指令(gc 子指令,需全局管理员)
|
|
167
|
+
|
|
168
|
+
| 指令 | 说明 |
|
|
169
|
+
|------|------|
|
|
170
|
+
| `gc.banlist` | 查看黑名单列表 |
|
|
171
|
+
| `gc.ban <群号>` | 添加群到黑名单 |
|
|
172
|
+
| `gc.unban <群号>` | 从黑名单移除群 |
|
|
173
|
+
| `gc.clearban` | 清空黑名单 |
|
|
174
|
+
| `gc.sg-add <群号>` | 将群加入小群白名单,不受人数限制 |
|
|
175
|
+
| `gc.sg-rm <群号>` | 从小群白名单移除群 |
|
|
176
|
+
| `gc.sg-list` | 查看小群白名单 |
|
|
177
|
+
| `gc.approve <群号>` | 同意加入指定群 |
|
|
178
|
+
| `gc.reject <群号>` | 拒绝加入指定群 |
|
|
179
|
+
| `gc.pending` | 查看待处理的群聊邀请列表 |
|
|
180
|
+
| `gc.fa <QQ号>` | 同意好友申请 |
|
|
181
|
+
| `gc.fr <QQ号>` | 拒绝好友申请 |
|
|
182
|
+
| `gc.fp` | 查看待处理的好友申请列表 |
|
|
183
|
+
|
|
184
|
+
### 群聊管理
|
|
185
|
+
|
|
186
|
+
| 指令 | 说明 | 权限 |
|
|
187
|
+
|------|------|------|
|
|
188
|
+
| `quit` | 让 bot 退出当前群聊 | 群管理员 |
|
|
189
|
+
| `bot-on` | 开启当前群的 bot | 群管理员 |
|
|
190
|
+
| `bot-off` | 关闭当前群的 bot | 群管理员 |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 注意事项
|
|
195
|
+
|
|
196
|
+
- 本插件仅支持 **OneBot 适配器**(如 go-cqhttp、LLOneBot 等)
|
|
197
|
+
- 管理员通知依赖 `admin.adminQQs` 或 `admin.notificationGroupId` 的配置,未配置则无法收到通知
|
|
198
|
+
- 频率控制的非指令拦截(@ 对话、私聊)不影响入群欢迎等系统事件
|
|
199
|
+
- 小群合格通知仅在启用了 `smallGroupAutoQuit` 且未经 `gc.approve` 审核通过的情况下触发
|