mioku-plugin-admin 2.3.0 → 2.3.1

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.
@@ -2,6 +2,8 @@ import type { MiokiContext } from "mioki";
2
2
  import { getAtUserId, getMemberRole } from "../config";
3
3
  import {
4
4
  getGroupVerifyConfig,
5
+ MAX_EXTRA_PROMPT_LENGTH,
6
+ normalizeExtraPrompt,
5
7
  normalizeVerifyMode,
6
8
  upsertGroupVerifyConfig,
7
9
  type VerifyConfig,
@@ -44,7 +46,9 @@ export function registerVerifyCommands(options: VerifyCommandOptions) {
44
46
  text.startsWith("/绕过验证") ||
45
47
  text.startsWith("#绕过验证") ||
46
48
  text.startsWith("/重新验证") ||
47
- text.startsWith("#重新验证");
49
+ text.startsWith("#重新验证") ||
50
+ text.startsWith("/入群提示") ||
51
+ text.startsWith("#入群提示");
48
52
 
49
53
  try {
50
54
  const selfId = event.self_id;
@@ -238,6 +242,48 @@ export function registerVerifyCommands(options: VerifyCommandOptions) {
238
242
  }
239
243
  return;
240
244
  }
245
+
246
+ // /入群提示 xxx
247
+ if (text.startsWith("/入群提示") || text.startsWith("#入群提示")) {
248
+ if (!(await ensureAdminPermission())) return;
249
+
250
+ const rawArg = text.replace(/^[/#]入群提示\s*/, "").trim();
251
+ const current = getVerifyConfig();
252
+ const groupCfg = getGroupVerifyConfig(current, groupId);
253
+
254
+ if (!rawArg) {
255
+ if (!groupCfg.extraPrompt) {
256
+ await event.reply("本群还没设置额外的入群提示词哦~", true);
257
+ return;
258
+ }
259
+ await setVerifyConfig(
260
+ upsertGroupVerifyConfig(current, groupId, { extraPrompt: "" }),
261
+ );
262
+ await event.reply("已清空本群额外入群提示词~", true);
263
+ return;
264
+ }
265
+
266
+ const argLength = Array.from(rawArg).length;
267
+ if (argLength > MAX_EXTRA_PROMPT_LENGTH) {
268
+ await replyAdminErrorNotice({
269
+ ctx,
270
+ event,
271
+ instruction: `用户想设置本群入群提示词,但内容长度 ${argLength} 超过了上限 ${MAX_EXTRA_PROMPT_LENGTH} 字,请明确告诉用户最多 ${MAX_EXTRA_PROMPT_LENGTH} 字。`,
272
+ fallbackMessage: `最多 ${MAX_EXTRA_PROMPT_LENGTH} 字哦~当前 ${argLength} 字`,
273
+ });
274
+ return;
275
+ }
276
+
277
+ const next = upsertGroupVerifyConfig(current, groupId, {
278
+ extraPrompt: normalizeExtraPrompt(rawArg),
279
+ });
280
+ await setVerifyConfig(next);
281
+ await event.reply(
282
+ `已设置本群额外入群提示词:${normalizeExtraPrompt(rawArg)}`,
283
+ true,
284
+ );
285
+ return;
286
+ }
241
287
  } catch (err) {
242
288
  ctx.logger.error(`[admin verify] 未捕获异常: ${String(err)}`);
243
289
  if (!isVerifyCommand) return;
package/config.md CHANGED
@@ -74,9 +74,37 @@ fields:
74
74
 
75
75
  - key: verify.groups
76
76
  label: 各群入群验证配置
77
- type: json
78
- description: 每个群的验证配置数组,元素形如 {"groupId":123,"enabled":true,"mode":"reaction"}。推荐通过 /开启验证、/关闭验证、/切换验证模式 指令管理,直接编辑需填写合法 JSON 数组。
79
- placeholder: '[]'
77
+ type: array
78
+ description:
79
+ itemFields:
80
+ - key: groupId
81
+ label: 群号
82
+ type: number
83
+ description: 该配置对应的 QQ 群号,每个群只能出现一次
84
+ placeholder: 123456789
85
+
86
+ - key: enabled
87
+ label: 启用验证
88
+ type: switch
89
+ description: 是否对该群开启入群验证
90
+
91
+ - key: mode
92
+ label: 验证模式
93
+ type: select
94
+ options:
95
+ - value: reaction
96
+ label: 回应
97
+ - value: number
98
+ label: 数字
99
+ - value: chiral
100
+ label: 手性碳
101
+ description: 该群入群验证使用的模式
102
+
103
+ - key: extraPrompt
104
+ label: 群额外入群提示词
105
+ type: textarea
106
+ description: 拼接在该群默认验证提示词之后发送,上限 50 字。可在群里直接用 /入群提示 xxx 设置或清空。
107
+ placeholder: 例:本群禁止复读机器人消息,请自觉
80
108
 
81
109
  - key: verify.reactionEmojiId
82
110
  label: 回应模式表态表情ID
package/notify/welcome.ts CHANGED
@@ -75,8 +75,16 @@ async function flushBatch(options: {
75
75
  members: PendingMember[];
76
76
  promptInjections?: { content: string; title?: string }[];
77
77
  }): Promise<string> {
78
- const { ctx, aiService, config, selfId, groupId, groupName, members, promptInjections } =
79
- options;
78
+ const {
79
+ ctx,
80
+ aiService,
81
+ config,
82
+ selfId,
83
+ groupId,
84
+ groupName,
85
+ members,
86
+ promptInjections,
87
+ } = options;
80
88
  if (!members.length) return "";
81
89
 
82
90
  const names = members.map((m) => m.memberName || String(m.userId));
@@ -106,7 +114,7 @@ async function flushBatch(options: {
106
114
  groupId,
107
115
  send: true,
108
116
  instruction: [
109
- `当前有 ${members.length} 位新成员同时入群,请一次性发送一段统一的欢迎语(不要逐个 @ 欢迎、不要重复点名)。`,
117
+ `当前有 ${members.length} 位新成员同时入群,请一次性发送一段统一的欢迎语(不要逐个 @ 欢迎、不要重复点名)不要长篇大论,精简即可。`,
110
118
  `新成员昵称:${userList}`,
111
119
  `新成员 QQ:${userIdList}`,
112
120
  `所在群:${groupName}`,
@@ -209,87 +217,47 @@ export function registerWelcomeHandler(
209
217
  ): () => void {
210
218
  const batches = getBatchMap();
211
219
 
212
- const dispose = ctx.handle("notice.group.increase" as any, async (event: any) => {
213
- const cfg = getConfig();
214
- const selfId = Number(event?.self_id || ctx.self_id);
215
- const groupId = Number(event?.group_id || 0);
216
- const userId = Number(event?.user_id || 0);
217
- if (!groupId || !userId) return;
218
- if (userId === selfId) return;
220
+ const dispose = ctx.handle(
221
+ "notice.group.increase" as any,
222
+ async (event: any) => {
223
+ const cfg = getConfig();
224
+ const selfId = Number(event?.self_id || ctx.self_id);
225
+ const groupId = Number(event?.group_id || 0);
226
+ const userId = Number(event?.user_id || 0);
227
+ if (!groupId || !userId) return;
228
+ if (userId === selfId) return;
219
229
 
220
- const groupName =
221
- String(event?.group?.group_name || "").trim() || String(groupId);
222
-
223
- if (
224
- shouldSuppress &&
225
- (await shouldSuppress({ selfId, groupId, userId, groupName }))
226
- ) {
227
- return;
228
- }
230
+ const groupName =
231
+ String(event?.group?.group_name || "").trim() || String(groupId);
229
232
 
230
- if (!cfg.welcome.enabled) return;
231
-
232
- const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
233
-
234
- if (batchWindowMs === 0) {
235
- const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
236
- const welcomeMessage = await flushBatch({
237
- ctx,
238
- aiService,
239
- config: cfg,
240
- selfId,
241
- groupId,
242
- groupName,
243
- members: [{ userId, memberName }],
244
- });
245
- if (!welcomeMessage) return;
246
- const bot = ctx.pickBot(selfId);
247
- if (!bot) return;
248
- try {
249
- await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
250
- } catch (error) {
251
- ctx.logger.warn(`发送入群欢迎失败: ${error}`);
233
+ if (
234
+ shouldSuppress &&
235
+ (await shouldSuppress({ selfId, groupId, userId, groupName }))
236
+ ) {
237
+ return;
252
238
  }
253
- return;
254
- }
255
-
256
- const key = batchKey(selfId, groupId);
257
- let state = batches.get(key);
258
- if (!state) {
259
- state = { members: [], timer: null, groupName };
260
- batches.set(key, state);
261
- }
262
- if (groupName && groupName !== String(groupId)) {
263
- state.groupName = groupName;
264
- }
265
239
 
266
- const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
267
- if (!state.members.some((m) => m.userId === userId)) {
268
- state.members.push({ userId, memberName });
269
- }
270
-
271
- if (state.timer) {
272
- return;
273
- }
240
+ if (!cfg.welcome.enabled) return;
274
241
 
275
- state.timer = setTimeout(async () => {
276
- try {
277
- const pending = state;
278
- batches.delete(key);
279
- if (!pending || !pending.members.length) return;
242
+ const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
280
243
 
281
- const currentConfig = getConfig();
244
+ if (batchWindowMs === 0) {
245
+ const memberName = await resolveMemberName(
246
+ ctx,
247
+ groupId,
248
+ userId,
249
+ selfId,
250
+ );
282
251
  const welcomeMessage = await flushBatch({
283
252
  ctx,
284
253
  aiService,
285
- config: currentConfig,
254
+ config: cfg,
286
255
  selfId,
287
256
  groupId,
288
- groupName: pending.groupName,
289
- members: pending.members,
257
+ groupName,
258
+ members: [{ userId, memberName }],
290
259
  });
291
260
  if (!welcomeMessage) return;
292
-
293
261
  const bot = ctx.pickBot(selfId);
294
262
  if (!bot) return;
295
263
  try {
@@ -297,11 +265,59 @@ export function registerWelcomeHandler(
297
265
  } catch (error) {
298
266
  ctx.logger.warn(`发送入群欢迎失败: ${error}`);
299
267
  }
300
- } catch (error) {
301
- ctx.logger.error(`admin welcome 批次处理失败: ${error}`);
268
+ return;
302
269
  }
303
- }, batchWindowMs);
304
- });
270
+
271
+ const key = batchKey(selfId, groupId);
272
+ let state = batches.get(key);
273
+ if (!state) {
274
+ state = { members: [], timer: null, groupName };
275
+ batches.set(key, state);
276
+ }
277
+ if (groupName && groupName !== String(groupId)) {
278
+ state.groupName = groupName;
279
+ }
280
+
281
+ const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
282
+ if (!state.members.some((m) => m.userId === userId)) {
283
+ state.members.push({ userId, memberName });
284
+ }
285
+
286
+ if (state.timer) {
287
+ return;
288
+ }
289
+
290
+ state.timer = setTimeout(async () => {
291
+ try {
292
+ const pending = state;
293
+ batches.delete(key);
294
+ if (!pending || !pending.members.length) return;
295
+
296
+ const currentConfig = getConfig();
297
+ const welcomeMessage = await flushBatch({
298
+ ctx,
299
+ aiService,
300
+ config: currentConfig,
301
+ selfId,
302
+ groupId,
303
+ groupName: pending.groupName,
304
+ members: pending.members,
305
+ });
306
+ if (!welcomeMessage) return;
307
+
308
+ const bot = ctx.pickBot(selfId);
309
+ if (!bot) return;
310
+ try {
311
+ await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
312
+ } catch (error) {
313
+ ctx.logger.warn(`发送入群欢迎失败: ${error}`);
314
+ }
315
+ } catch (error) {
316
+ ctx.logger.error(`admin welcome 批次处理失败: ${error}`);
317
+ }
318
+ }, batchWindowMs);
319
+ },
320
+ );
305
321
 
306
322
  return () => {
307
323
  for (const state of batches.values()) {
@@ -310,4 +326,4 @@ export function registerWelcomeHandler(
310
326
  batches.clear();
311
327
  dispose();
312
328
  };
313
- }
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-admin",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "管理插件,提供事件通知与群管/个人管理指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/verify/chiral.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { MiokiContext } from "mioki";
2
- import type { VerifyConfig } from "./config";
2
+ import {
3
+ resolveVerifyPrompt,
4
+ type VerifyConfig,
5
+ type VerifyGroupConfig,
6
+ } from "./config";
3
7
  import type { PendingVerify } from "./types";
4
8
 
5
9
  interface ChiralCaptcha {
@@ -43,6 +47,7 @@ async function fetchChiralCaptcha(
43
47
  export async function prepareChiral(
44
48
  ctx: MiokiContext,
45
49
  cfg: VerifyConfig,
50
+ groupCfg: VerifyGroupConfig,
46
51
  p: PendingVerify,
47
52
  ): Promise<boolean> {
48
53
  const bot = ctx.pickBot(p.selfId);
@@ -51,10 +56,11 @@ export async function prepareChiral(
51
56
  const captcha = await fetchChiralCaptcha(cfg.chiralApiUrl, cfg.chiralDifficulty);
52
57
  p.requiredRegions = captcha.regions;
53
58
  p.matchedRegions = new Set<string>();
54
- const prompt = cfg.chiralPrompt.replace(
59
+ const basePrompt = cfg.chiralPrompt.replace(
55
60
  "{count}",
56
61
  String(captcha.regions.length),
57
62
  );
63
+ const prompt = resolveVerifyPrompt(basePrompt, groupCfg);
58
64
  await bot.sendGroupMsg(p.groupId, [
59
65
  ctx.segment.at(String(p.userId)),
60
66
  ctx.segment.text(` ${prompt}`),
package/verify/config.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export type VerifyMode = "reaction" | "number" | "chiral";
2
2
 
3
+ export const MAX_EXTRA_PROMPT_LENGTH = 50;
4
+
3
5
  export interface VerifyGroupConfig {
4
6
  groupId: number;
5
7
  enabled: boolean;
6
8
  mode: VerifyMode;
9
+ extraPrompt: string;
7
10
  }
8
11
 
9
12
  export interface VerifyConfig {
@@ -39,6 +42,22 @@ export const DEFAULT_VERIFY_CONFIG: VerifyConfig = {
39
42
  kickOnTimeout: true,
40
43
  };
41
44
 
45
+ export function normalizeExtraPrompt(value: unknown): string {
46
+ if (typeof value !== "string") return "";
47
+ const trimmed = value.trim();
48
+ if (!trimmed) return "";
49
+ return Array.from(trimmed).slice(0, MAX_EXTRA_PROMPT_LENGTH).join("");
50
+ }
51
+
52
+ export function resolveVerifyPrompt(
53
+ basePrompt: string,
54
+ groupCfg: VerifyGroupConfig | undefined,
55
+ ): string {
56
+ const extra = String(groupCfg?.extraPrompt || "").trim();
57
+ if (!extra) return basePrompt;
58
+ return `${basePrompt}\n${extra}`;
59
+ }
60
+
42
61
  export function normalizeVerifyMode(value: unknown): VerifyMode {
43
62
  const v = String(value || "").trim();
44
63
  if (v === "number" || v === "数字") return "number";
@@ -52,6 +71,7 @@ function normalizeVerifyGroup(raw: any): VerifyGroupConfig {
52
71
  groupId: groupId > 0 ? groupId : 0,
53
72
  enabled: raw?.enabled === true,
54
73
  mode: normalizeVerifyMode(raw?.mode),
74
+ extraPrompt: normalizeExtraPrompt(raw?.extraPrompt ?? raw?.extra_prompt),
55
75
  };
56
76
  }
57
77
 
@@ -113,7 +133,7 @@ export function getGroupVerifyConfig(
113
133
  ): VerifyGroupConfig {
114
134
  const found = config.groups.find((g) => g.groupId === groupId);
115
135
  if (found) return found;
116
- return { groupId, enabled: false, mode: "reaction" };
136
+ return { groupId, enabled: false, mode: "reaction", extraPrompt: "" };
117
137
  }
118
138
 
119
139
  export function upsertGroupVerifyConfig(
@@ -123,14 +143,26 @@ export function upsertGroupVerifyConfig(
123
143
  ): VerifyConfig {
124
144
  const idx = config.groups.findIndex((g) => g.groupId === groupId);
125
145
  const next = { ...config };
146
+ const normalizedPatch: Partial<Omit<VerifyGroupConfig, "groupId">> = {
147
+ ...patch,
148
+ };
149
+ if (patch.extraPrompt !== undefined) {
150
+ normalizedPatch.extraPrompt = normalizeExtraPrompt(patch.extraPrompt);
151
+ }
126
152
  if (idx >= 0) {
127
153
  next.groups = config.groups.map((g, i) =>
128
- i === idx ? { ...g, ...patch } : g,
154
+ i === idx ? { ...g, ...normalizedPatch } : g,
129
155
  );
130
156
  } else {
131
157
  next.groups = [
132
158
  ...config.groups,
133
- { groupId, enabled: false, mode: "reaction", ...patch },
159
+ {
160
+ groupId,
161
+ enabled: false,
162
+ mode: "reaction",
163
+ extraPrompt: "",
164
+ ...normalizedPatch,
165
+ },
134
166
  ];
135
167
  }
136
168
  return next;
package/verify/index.ts CHANGED
@@ -204,12 +204,12 @@ export function createVerifyController(
204
204
  const delay = skipDelay ? 0 : Math.max(0, cfg.reactionDelayMs);
205
205
  entry.delayTimer = setTimeout(() => {
206
206
  entry.delayTimer = null;
207
- void sendReactionPrompt(ctx, cfg, entry);
207
+ void sendReactionPrompt(ctx, cfg, groupCfg, entry);
208
208
  }, delay);
209
209
  } else if (mode === "number") {
210
- void sendNumberPrompt(ctx, cfg, entry);
210
+ void sendNumberPrompt(ctx, cfg, groupCfg, entry);
211
211
  } else if (mode === "chiral") {
212
- const ok = await prepareChiral(ctx, cfg, entry);
212
+ const ok = await prepareChiral(ctx, cfg, groupCfg, entry);
213
213
  if (!ok) {
214
214
  // 验证服务不可用时放行,避免误伤
215
215
  removePending(key);
package/verify/number.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { MiokiContext } from "mioki";
2
- import type { VerifyConfig } from "./config";
2
+ import {
3
+ resolveVerifyPrompt,
4
+ type VerifyConfig,
5
+ type VerifyGroupConfig,
6
+ } from "./config";
3
7
  import type { PendingVerify } from "./types";
4
8
 
5
9
  function genNumberQuestion(): { question: string; answer: number } {
@@ -19,13 +23,15 @@ function extractNumbers(text: string): number[] {
19
23
  export async function sendNumberPrompt(
20
24
  ctx: MiokiContext,
21
25
  cfg: VerifyConfig,
26
+ groupCfg: VerifyGroupConfig,
22
27
  p: PendingVerify,
23
28
  ): Promise<void> {
24
29
  const bot = ctx.pickBot(p.selfId);
25
30
  if (!bot) return;
26
31
  const { question, answer } = genNumberQuestion();
27
32
  p.numberAnswer = answer;
28
- const prompt = cfg.numberPrompt.replace("{question}", question);
33
+ const basePrompt = cfg.numberPrompt.replace("{question}", question);
34
+ const prompt = resolveVerifyPrompt(basePrompt, groupCfg);
29
35
  try {
30
36
  await bot.sendGroupMsg(p.groupId, [
31
37
  ctx.segment.at(String(p.userId)),
@@ -1,19 +1,25 @@
1
1
  import type { MiokiContext } from "mioki";
2
- import type { VerifyConfig } from "./config";
2
+ import {
3
+ resolveVerifyPrompt,
4
+ type VerifyConfig,
5
+ type VerifyGroupConfig,
6
+ } from "./config";
3
7
  import type { PendingVerify } from "./types";
4
8
 
5
9
  export async function sendReactionPrompt(
6
10
  ctx: MiokiContext,
7
11
  cfg: VerifyConfig,
12
+ groupCfg: VerifyGroupConfig,
8
13
  p: PendingVerify,
9
14
  ): Promise<void> {
10
15
  const bot = ctx.pickBot(p.selfId);
11
16
  if (!bot) return;
17
+ const prompt = resolveVerifyPrompt(cfg.reactionPrompt, groupCfg);
12
18
  let messageId: number | undefined;
13
19
  try {
14
20
  const res = await bot.sendGroupMsg(p.groupId, [
15
21
  ctx.segment.at(String(p.userId)),
16
- ctx.segment.text(` ${cfg.reactionPrompt}`),
22
+ ctx.segment.text(` ${prompt}`),
17
23
  ]);
18
24
  messageId = Number(res?.message_id || 0) || undefined;
19
25
  } catch (err) {