mioku-plugin-admin 2.2.3 → 2.3.0

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/README.md CHANGED
@@ -76,3 +76,8 @@ Mioku 的管理插件,提供事件通知与 Bot 管理指令。
76
76
  | 设管理 | `/设管理 @人` | 设置群管理员(需 Bot 为群主) |
77
77
  | /全体禁言 | `/全体禁言` | 开启全体禁言 |
78
78
  | /全体解禁 | `/全体解禁` | 关闭全体禁言 |
79
+ | /开启验证 | `/开启验证` 或 `#开启验证` | 开启本群入群验证(需 Bot 为群主或管理员) |
80
+ | /关闭验证 | `/关闭验证` 或 `#关闭验证` | 关闭本群入群验证 |
81
+ | /切换验证模式 | `/切换验证模式 回应/数字/手性碳` | 切换验证模式,默认回应 |
82
+ | /绕过验证 | `/绕过验证 @新成员` | 跳过该成员验证直接走欢迎流程 |
83
+ | /重新验证 | `/重新验证 @成员` | 让指定成员重新进行入群验证(不能针对群主/管理/主人) |
@@ -0,0 +1,253 @@
1
+ import type { MiokiContext } from "mioki";
2
+ import { getAtUserId, getMemberRole } from "../config";
3
+ import {
4
+ getGroupVerifyConfig,
5
+ normalizeVerifyMode,
6
+ upsertGroupVerifyConfig,
7
+ type VerifyConfig,
8
+ } from "../verify/config";
9
+ import { replyAdminErrorNotice } from "./notice";
10
+ import type { VerifyController } from "../verify/types";
11
+
12
+ export interface VerifyCommandOptions {
13
+ ctx: MiokiContext;
14
+ getVerifyConfig: () => VerifyConfig;
15
+ setVerifyConfig: (next: VerifyConfig) => Promise<void>;
16
+ verifyController: VerifyController;
17
+ }
18
+
19
+ const VERIFY_MODE_LABELS: Record<string, string> = {
20
+ reaction: "回应",
21
+ number: "数字",
22
+ chiral: "手性碳",
23
+ };
24
+
25
+ export function registerVerifyCommands(options: VerifyCommandOptions) {
26
+ const { ctx, getVerifyConfig, setVerifyConfig, verifyController } = options;
27
+
28
+ ctx.handle("message", async (event: any) => {
29
+ const text = ctx.text(event)?.trim();
30
+ if (!text) return;
31
+ if (event.user_id === event.self_id) return;
32
+
33
+ if (event.message_type !== "group") return;
34
+ const groupId = Number(event.group_id || 0);
35
+ if (!groupId) return;
36
+
37
+ const isVerifyCommand =
38
+ text === "/开启验证" ||
39
+ text === "#开启验证" ||
40
+ text === "/关闭验证" ||
41
+ text === "#关闭验证" ||
42
+ text.startsWith("/切换验证模式") ||
43
+ text.startsWith("#切换验证模式") ||
44
+ text.startsWith("/绕过验证") ||
45
+ text.startsWith("#绕过验证") ||
46
+ text.startsWith("/重新验证") ||
47
+ text.startsWith("#重新验证");
48
+
49
+ try {
50
+ const selfId = event.self_id;
51
+ const bot = ctx.pickBot(selfId);
52
+ if (!bot) return;
53
+
54
+ const isMaster = ctx.isOwner?.(event) ?? false;
55
+ const senderRole = await getMemberRole(bot, groupId, event.user_id);
56
+ const hasAdminPermission =
57
+ isMaster || senderRole === "owner" || senderRole === "admin";
58
+
59
+ const ensureAdminPermission = async (instruction?: string) => {
60
+ if (hasAdminPermission) return true;
61
+ await replyAdminErrorNotice({
62
+ ctx,
63
+ event,
64
+ instruction:
65
+ instruction ||
66
+ "用户想使用入群验证相关指令,但不是群主或管理员,无权限。请告诉用户需要管理权限。",
67
+ fallbackMessage: "你得是群主或管理员才行哦~",
68
+ });
69
+ return false;
70
+ };
71
+
72
+ const groupName =
73
+ String(event?.group?.group_name || "").trim() || String(groupId);
74
+
75
+ // /开启验证 | #开启验证
76
+ if (text === "/开启验证" || text === "#开启验证") {
77
+ if (!(await ensureAdminPermission())) return;
78
+
79
+ const botRole = await getMemberRole(bot, groupId, selfId);
80
+ if (botRole !== "owner" && botRole !== "admin") {
81
+ await replyAdminErrorNotice({
82
+ ctx,
83
+ event,
84
+ instruction:
85
+ "用户想开启入群验证,但Bot在群内不是群主或管理员,无法执行撤回/踢人等验证操作。请告诉用户需要先把Bot设为群主或管理员。",
86
+ fallbackMessage: "我得是群主或管理员才能开验证哦~",
87
+ });
88
+ return;
89
+ }
90
+
91
+ const current = getVerifyConfig();
92
+ const groupCfg = getGroupVerifyConfig(current, groupId);
93
+ if (groupCfg.enabled) {
94
+ await event.reply("本群已经开启验证啦~", true);
95
+ return;
96
+ }
97
+ const next = upsertGroupVerifyConfig(current, groupId, {
98
+ enabled: true,
99
+ });
100
+ await setVerifyConfig(next);
101
+ await event.reply("已开启入群验证~", true);
102
+ return;
103
+ }
104
+
105
+ // /关闭验证 | #关闭验证
106
+ if (text === "/关闭验证" || text === "#关闭验证") {
107
+ if (!(await ensureAdminPermission())) return;
108
+
109
+ const current = getVerifyConfig();
110
+ const groupCfg = getGroupVerifyConfig(current, groupId);
111
+ if (!groupCfg.enabled) {
112
+ await event.reply("本群还没开启验证哦~", true);
113
+ return;
114
+ }
115
+ const next = upsertGroupVerifyConfig(current, groupId, {
116
+ enabled: false,
117
+ });
118
+ await setVerifyConfig(next);
119
+ await event.reply("已关闭入群验证~", true);
120
+ return;
121
+ }
122
+
123
+ // /切换验证模式 回应|数字|手性碳
124
+ if (
125
+ text.startsWith("/切换验证模式") ||
126
+ text.startsWith("#切换验证模式")
127
+ ) {
128
+ if (!(await ensureAdminPermission())) return;
129
+
130
+ const arg = text
131
+ .replace(/^[/#]切换验证模式\s*/, "")
132
+ .trim() as string;
133
+ const mode = normalizeVerifyMode(arg);
134
+ if (!arg) {
135
+ await replyAdminErrorNotice({
136
+ ctx,
137
+ event,
138
+ instruction:
139
+ "用户想切换验证模式但没指定模式。请告诉用户可用模式:回应、数字、手性碳,用法:/切换验证模式 回应。",
140
+ fallbackMessage: "想切换成哪种模式呀~回应/数字/手性碳",
141
+ });
142
+ return;
143
+ }
144
+
145
+ const current = getVerifyConfig();
146
+ const next = upsertGroupVerifyConfig(current, groupId, { mode });
147
+ await setVerifyConfig(next);
148
+ await event.reply(
149
+ `验证模式已切换为:${VERIFY_MODE_LABELS[mode]}~`,
150
+ true,
151
+ );
152
+ return;
153
+ }
154
+
155
+ // /绕过验证 @新成员
156
+ if (text.startsWith("/绕过验证") || text.startsWith("#绕过验证")) {
157
+ if (!(await ensureAdminPermission())) return;
158
+
159
+ const atUser = getAtUserId(event.message);
160
+ if (!atUser) {
161
+ await replyAdminErrorNotice({
162
+ ctx,
163
+ event,
164
+ instruction:
165
+ "用户想绕过某位新成员的验证,但没有@目标成员。请提醒用户在命令后@要绕过验证的新成员。",
166
+ fallbackMessage: "要绕过谁呀~先@一下~",
167
+ });
168
+ return;
169
+ }
170
+
171
+ try {
172
+ await verifyController.bypassVerification({
173
+ selfId,
174
+ groupId,
175
+ userId: atUser,
176
+ groupName,
177
+ });
178
+ await event.reply("done");
179
+ } catch (err) {
180
+ await replyAdminErrorNotice({
181
+ ctx,
182
+ event,
183
+ instruction: `绕过验证执行失败:${String(err)},请简要说明失败并建议稍后重试。`,
184
+ fallbackMessage: `出错了,笨蛋~ ${String(err)}`,
185
+ error: err,
186
+ });
187
+ }
188
+ return;
189
+ }
190
+
191
+ // /重新验证 @某人
192
+ if (text.startsWith("/重新验证") || text.startsWith("#重新验证")) {
193
+ if (!(await ensureAdminPermission())) return;
194
+
195
+ const atUser = getAtUserId(event.message);
196
+ if (!atUser) {
197
+ await replyAdminErrorNotice({
198
+ ctx,
199
+ event,
200
+ instruction:
201
+ "用户想让某人重新进行入群验证,但没有@目标成员。请提醒用户在命令后@要重新验证的成员。",
202
+ fallbackMessage: "要重新验证谁呀~先@一下~",
203
+ });
204
+ return;
205
+ }
206
+
207
+ const isTargetMaster = ctx.isOwner?.(atUser) ?? false;
208
+ const targetRole = await getMemberRole(bot, groupId, atUser);
209
+ if (isTargetMaster || targetRole === "owner" || targetRole === "admin") {
210
+ await replyAdminErrorNotice({
211
+ ctx,
212
+ event,
213
+ instruction:
214
+ "用户想让一位群主/管理员/主人重新验证,但这些成员无需验证。请告诉用户该成员是管理/群主或主人,不能对其重新验证。",
215
+ fallbackMessage: "管理/群主或主人可不用验证哦~",
216
+ });
217
+ return;
218
+ }
219
+
220
+ try {
221
+ const started = await verifyController.restartVerification({
222
+ selfId,
223
+ groupId,
224
+ userId: atUser,
225
+ groupName,
226
+ });
227
+ if (!started) {
228
+ await event.reply("本群还没开启验证哦~", true);
229
+ }
230
+ } catch (err) {
231
+ await replyAdminErrorNotice({
232
+ ctx,
233
+ event,
234
+ instruction: `重新验证执行失败:${String(err)},请简要说明失败并建议稍后重试。`,
235
+ fallbackMessage: `出错了,笨蛋~ ${String(err)}`,
236
+ error: err,
237
+ });
238
+ }
239
+ return;
240
+ }
241
+ } catch (err) {
242
+ ctx.logger.error(`[admin verify] 未捕获异常: ${String(err)}`);
243
+ if (!isVerifyCommand) return;
244
+ await replyAdminErrorNotice({
245
+ ctx,
246
+ event,
247
+ instruction: `入群验证指令执行时发生未捕获异常:${String(err)},请简要说明失败并建议稍后重试。`,
248
+ fallbackMessage: `出错了,笨蛋~ ${String(err)}`,
249
+ error: err,
250
+ });
251
+ }
252
+ });
253
+ }
package/config.md CHANGED
@@ -72,6 +72,80 @@ fields:
72
72
  description: 短时间内多个新成员入群时,攒齐该时长内的成员后只发一次 AI 欢迎,避免频繁调用模型。设为 0 表示关闭聚合、逐个欢迎。默认 20000。
73
73
  placeholder: 20000
74
74
 
75
+ - key: verify.groups
76
+ label: 各群入群验证配置
77
+ type: json
78
+ description: 每个群的验证配置数组,元素形如 {"groupId":123,"enabled":true,"mode":"reaction"}。推荐通过 /开启验证、/关闭验证、/切换验证模式 指令管理,直接编辑需填写合法 JSON 数组。
79
+ placeholder: '[]'
80
+
81
+ - key: verify.reactionEmojiId
82
+ label: 回应模式表态表情ID
83
+ type: text
84
+ description: 回应验证模式下,Bot 给提示消息添加的表态表情 ID,新成员点击该表态即通过验证。默认 424。
85
+ placeholder: "424"
86
+
87
+ - key: verify.reactionDelayMs
88
+ label: 回应模式延迟 (毫秒)
89
+ type: number
90
+ description: 回应模式下新成员入群后,Bot 等待多久再 @新成员发出验证提示并添加表态。默认 3000。重新验证指令不受此延迟影响。
91
+ placeholder: 3000
92
+
93
+ - key: verify.verifyTimeoutMs
94
+ label: 验证超时 (毫秒)
95
+ type: number
96
+ description: 新成员未在此时长内完成验证则视为超时,按下方配置决定是否踢出。默认 120000(2分钟)。
97
+ placeholder: 120000
98
+
99
+ - key: verify.reactionPrompt
100
+ label: 回应模式提示语
101
+ type: textarea
102
+ description: 回应模式下 @新成员 时发送的提示文本。
103
+ placeholder: 新来的小伙伴请在2分钟内点击下方红色按钮完成验证 不听话会被移出群聊喵~
104
+
105
+ - key: verify.numberPrompt
106
+ label: 数字模式提示语
107
+ type: textarea
108
+ description: 数字模式下的提示文本,支持 {question} 占位符替换算术题。
109
+ placeholder: '新来的小伙伴请在2分钟内回答下面的题目完成验证,不听话移出群聊喵~\n请问:{question}'
110
+
111
+ - key: verify.chiralApiUrl
112
+ label: 手性碳验证 API 地址
113
+ type: text
114
+ description: 手性碳模式调用的验证服务地址,默认 https://carbon.crystelf.top。你也可以参照 https://github.com/leafLeaf9/chiral-carbon-captcha 自行部署后填写自己的地址。
115
+ placeholder: https://carbon.crystelf.top
116
+
117
+ - key: verify.chiralDifficulty
118
+ label: 手性碳验证难度
119
+ type: select
120
+ options:
121
+ - value: simple
122
+ label: 简单(显示提示)
123
+ - value: hard
124
+ label: 困难(不显示提示)
125
+ description: 简单模式题图带提示标注,困难模式不带提示。
126
+
127
+ - key: verify.chiralPrompt
128
+ label: 手性碳模式提示语
129
+ type: textarea
130
+ description: 手性碳模式下随题图发送的提示文本,支持 {count} 占位符替换答案数量。
131
+ placeholder: '新来的小伙伴请在2分钟内完成下面的手性碳验证:找出图中{count}个手性碳所在的格子,回复格子编号即可,不听话会被移出群聊喵~'
132
+
133
+ - key: verify.maxInvalidMessages
134
+ label: 最大无关消息数
135
+ type: number
136
+ description: 未验证成员发送与验证无关的消息达到该次数后踢出群聊。默认 5。
137
+ placeholder: 5
138
+
139
+ - key: verify.kickOnFail
140
+ label: 达到上限踢出
141
+ type: switch
142
+ description: 未验证成员无关消息达到上限时是否踢出群聊
143
+
144
+ - key: verify.kickOnTimeout
145
+ label: 超时踢出
146
+ type: switch
147
+ description: 新成员验证超时是否踢出群聊
148
+
75
149
  ---
76
150
 
77
151
  ```mioku-fields
@@ -88,4 +162,16 @@ keys:
88
162
  - base.welcome.text
89
163
  - base.welcome.aiPrompt
90
164
  - base.welcome.batchWindowMs
91
- ```
165
+ - verify.groups
166
+ - verify.reactionEmojiId
167
+ - verify.reactionDelayMs
168
+ - verify.verifyTimeoutMs
169
+ - verify.reactionPrompt
170
+ - verify.numberPrompt
171
+ - verify.chiralApiUrl
172
+ - verify.chiralDifficulty
173
+ - verify.chiralPrompt
174
+ - verify.maxInvalidMessages
175
+ - verify.kickOnFail
176
+ - verify.kickOnTimeout
177
+ ```
package/config.ts CHANGED
@@ -48,8 +48,7 @@ export function normalizeConfig(raw: any): AdminConfig {
48
48
  notifyGroupUnban: raw?.notifyGroupUnban ?? DEFAULT_CONFIG.notifyGroupUnban,
49
49
  notifyGroupKick: raw?.notifyGroupKick ?? DEFAULT_CONFIG.notifyGroupKick,
50
50
  welcome: {
51
- enabled:
52
- raw?.welcome?.enabled ?? DEFAULT_CONFIG.welcome.enabled,
51
+ enabled: raw?.welcome?.enabled ?? DEFAULT_CONFIG.welcome.enabled,
53
52
  mode: raw?.welcome?.mode === "text" ? "text" : "ai",
54
53
  text:
55
54
  typeof raw?.welcome?.text === "string"
@@ -147,4 +146,4 @@ export async function getMemberRole(
147
146
  } catch {
148
147
  return "member";
149
148
  }
150
- }
149
+ }
package/index.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { definePlugin, type MiokiContext } from "mioki";
2
2
  import type { AIService, ConfigService } from "mioku";
3
3
  import { setPluginRuntimeState, resetPluginRuntimeState } from "mioku";
4
- import { DEFAULT_CONFIG, normalizeConfig } from "./config";
5
- import type { AdminConfig } from "./config";
4
+ import { DEFAULT_CONFIG, normalizeConfig, type AdminConfig } from "./config";
5
+ import {
6
+ DEFAULT_VERIFY_CONFIG,
7
+ normalizeVerifyConfig,
8
+ type VerifyConfig,
9
+ } from "./verify/config";
6
10
  import { registerNotificationHandlers } from "./notify";
7
11
  import { registerPersonalCommands } from "./commands/personal";
8
12
  import { registerGroupAdminCommands } from "./commands/group";
13
+ import { registerVerifyCommands } from "./commands/verify";
9
14
  import { registerWelcomeHandler } from "./notify/welcome";
15
+ import { createVerifyController } from "./verify";
10
16
 
11
17
  interface RuntimeState {
12
18
  ctx?: MiokiContext;
@@ -23,6 +29,7 @@ export default definePlugin({
23
29
  const aiService = ctx.services?.ai as AIService | undefined;
24
30
 
25
31
  let config: AdminConfig = { ...DEFAULT_CONFIG };
32
+ let verifyConfig: VerifyConfig = { ...DEFAULT_VERIFY_CONFIG };
26
33
 
27
34
  if (configService) {
28
35
  await configService.registerConfig("admin", "base", DEFAULT_CONFIG);
@@ -31,17 +38,58 @@ export default definePlugin({
31
38
  configService.onConfigChange("admin", "base", (next) => {
32
39
  config = normalizeConfig(next);
33
40
  });
41
+
42
+ await configService.registerConfig(
43
+ "admin",
44
+ "verify",
45
+ DEFAULT_VERIFY_CONFIG,
46
+ );
47
+ const verifyRaw = await configService.getConfig("admin", "verify");
48
+ verifyConfig = normalizeVerifyConfig(verifyRaw);
49
+ configService.onConfigChange("admin", "verify", (next) => {
50
+ verifyConfig = normalizeVerifyConfig(next);
51
+ });
34
52
  }
35
53
 
36
54
  setPluginRuntimeState("admin", { ctx });
37
55
 
38
56
  const getConfig = () => config;
57
+ const getVerifyConfig = () => verifyConfig;
58
+ const getWelcomeEnabled = () => config.welcome.enabled;
59
+ const setVerifyConfig = async (next: VerifyConfig) => {
60
+ verifyConfig = next;
61
+ if (configService) {
62
+ await configService.updateConfig("admin", "verify", next);
63
+ }
64
+ };
65
+
66
+ const verifyController = createVerifyController({
67
+ ctx,
68
+ aiService,
69
+ getConfig,
70
+ getVerifyConfig,
71
+ getWelcomeEnabled,
72
+ setVerifyConfig,
73
+ });
39
74
 
40
75
  // 注册事件通知
41
76
  registerNotificationHandlers(ctx, getConfig);
42
77
 
43
- // 注册新人入群欢迎
44
- registerWelcomeHandler(ctx, aiService, getConfig);
78
+ // 注册新人入群欢迎(开启验证的群由 verify 接管,验证通过后再欢迎)
79
+ const disposeWelcome = registerWelcomeHandler(
80
+ ctx,
81
+ aiService,
82
+ getConfig,
83
+ (info) => verifyController.handleMemberJoin(info),
84
+ );
85
+
86
+ // 注册入群验证指令
87
+ registerVerifyCommands({
88
+ ctx,
89
+ getVerifyConfig,
90
+ setVerifyConfig,
91
+ verifyController,
92
+ });
45
93
 
46
94
  // 注册指令
47
95
  registerPersonalCommands(ctx);
@@ -50,8 +98,10 @@ export default definePlugin({
50
98
  ctx.logger.info("管理插件加载成功");
51
99
 
52
100
  return () => {
101
+ disposeWelcome();
102
+ verifyController.dispose();
53
103
  resetPluginRuntimeState("admin");
54
104
  ctx.logger.info("管理插件已卸载");
55
105
  };
56
106
  },
57
- });
107
+ });
package/notify/welcome.ts CHANGED
@@ -73,8 +73,10 @@ async function flushBatch(options: {
73
73
  groupId: number;
74
74
  groupName: string;
75
75
  members: PendingMember[];
76
+ promptInjections?: { content: string; title?: string }[];
76
77
  }): Promise<string> {
77
- const { ctx, aiService, config, selfId, groupId, groupName, members } = options;
78
+ const { ctx, aiService, config, selfId, groupId, groupName, members, promptInjections } =
79
+ options;
78
80
  if (!members.length) return "";
79
81
 
80
82
  const names = members.map((m) => m.memberName || String(m.userId));
@@ -110,6 +112,7 @@ async function flushBatch(options: {
110
112
  `所在群:${groupName}`,
111
113
  `${config.welcome.aiPrompt || ""}`,
112
114
  ].join("\n"),
115
+ promptInjections,
113
116
  });
114
117
 
115
118
  return "";
@@ -119,10 +122,90 @@ async function flushBatch(options: {
119
122
  }
120
123
  }
121
124
 
125
+ async function sendSingleWelcome(options: {
126
+ ctx: MiokiContext;
127
+ aiService?: AIService;
128
+ config: AdminConfig;
129
+ selfId: number;
130
+ groupId: number;
131
+ groupName: string;
132
+ userId: number;
133
+ memberName: string;
134
+ promptInjections?: { content: string; title?: string }[];
135
+ }): Promise<void> {
136
+ const {
137
+ ctx,
138
+ aiService,
139
+ config,
140
+ selfId,
141
+ groupId,
142
+ groupName,
143
+ userId,
144
+ memberName,
145
+ promptInjections,
146
+ } = options;
147
+ const welcomeMessage = await flushBatch({
148
+ ctx,
149
+ aiService,
150
+ config,
151
+ selfId,
152
+ groupId,
153
+ groupName,
154
+ members: [{ userId, memberName }],
155
+ promptInjections,
156
+ });
157
+ if (!welcomeMessage) return;
158
+ const bot = ctx.pickBot(selfId);
159
+ if (!bot) return;
160
+ try {
161
+ await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
162
+ } catch (error) {
163
+ ctx.logger.warn(`发送入群欢迎失败: ${error}`);
164
+ }
165
+ }
166
+
167
+ export async function triggerSingleWelcome(options: {
168
+ ctx: MiokiContext;
169
+ aiService?: AIService;
170
+ getConfig: () => AdminConfig;
171
+ selfId: number;
172
+ groupId: number;
173
+ groupName: string;
174
+ userId: number;
175
+ memberName?: string;
176
+ promptInjections?: { content: string; title?: string }[];
177
+ }): Promise<void> {
178
+ const memberName =
179
+ options.memberName ||
180
+ (await resolveMemberName(
181
+ options.ctx,
182
+ options.groupId,
183
+ options.userId,
184
+ options.selfId,
185
+ ));
186
+ await sendSingleWelcome({
187
+ ctx: options.ctx,
188
+ aiService: options.aiService,
189
+ config: options.getConfig(),
190
+ selfId: options.selfId,
191
+ groupId: options.groupId,
192
+ groupName: options.groupName,
193
+ userId: options.userId,
194
+ memberName,
195
+ promptInjections: options.promptInjections,
196
+ });
197
+ }
198
+
122
199
  export function registerWelcomeHandler(
123
200
  ctx: MiokiContext,
124
201
  aiService: AIService | undefined,
125
202
  getConfig: () => AdminConfig,
203
+ shouldSuppress?: (info: {
204
+ selfId: number;
205
+ groupId: number;
206
+ userId: number;
207
+ groupName: string;
208
+ }) => Promise<boolean> | boolean,
126
209
  ): () => void {
127
210
  const batches = getBatchMap();
128
211
 
@@ -134,11 +217,18 @@ export function registerWelcomeHandler(
134
217
  if (!groupId || !userId) return;
135
218
  if (userId === selfId) return;
136
219
 
137
- if (!cfg.welcome.enabled) return;
138
-
139
220
  const groupName =
140
221
  String(event?.group?.group_name || "").trim() || String(groupId);
141
222
 
223
+ if (
224
+ shouldSuppress &&
225
+ (await shouldSuppress({ selfId, groupId, userId, groupName }))
226
+ ) {
227
+ return;
228
+ }
229
+
230
+ if (!cfg.welcome.enabled) return;
231
+
142
232
  const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
143
233
 
144
234
  if (batchWindowMs === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-admin",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "description": "管理插件,提供事件通知与群管/个人管理指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -117,6 +117,26 @@
117
117
  "id": "welcome",
118
118
  "event": "notice.group.increase",
119
119
  "description": "新人入群相关功能"
120
+ },
121
+ {
122
+ "id": "/开启验证",
123
+ "match": "/^[/#]开启验证$/"
124
+ },
125
+ {
126
+ "id": "/关闭验证",
127
+ "match": "/^[/#]关闭验证$/"
128
+ },
129
+ {
130
+ "id": "/切换验证模式",
131
+ "match": "/^[/#]切换验证模式/"
132
+ },
133
+ {
134
+ "id": "/绕过验证",
135
+ "match": "/^[/#]绕过验证/"
136
+ },
137
+ {
138
+ "id": "/重新验证",
139
+ "match": "/^[/#]重新验证/"
120
140
  }
121
141
  ],
122
142
  "help": {
@@ -240,6 +260,34 @@
240
260
  "cmd": "/全部群聊",
241
261
  "desc": "获取全部群聊列表",
242
262
  "role": "master"
263
+ },
264
+ {
265
+ "cmd": "/开启验证",
266
+ "desc": "开启本群入群验证",
267
+ "role": "admin"
268
+ },
269
+ {
270
+ "cmd": "/关闭验证",
271
+ "desc": "关闭本群入群验证",
272
+ "role": "admin"
273
+ },
274
+ {
275
+ "cmd": "/切换验证模式",
276
+ "desc": "切换验证模式:回应/数字/手性碳",
277
+ "usage": "/切换验证模式 回应",
278
+ "role": "admin"
279
+ },
280
+ {
281
+ "cmd": "/绕过验证",
282
+ "desc": "绕过指定新成员的验证直接欢迎",
283
+ "usage": "/绕过验证 @新成员",
284
+ "role": "admin"
285
+ },
286
+ {
287
+ "cmd": "/重新验证",
288
+ "desc": "让指定成员重新进行入群验证",
289
+ "usage": "/重新验证 @成员",
290
+ "role": "admin"
243
291
  }
244
292
  ]
245
293
  }