mioku-plugin-admin 2.2.3 → 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.
- package/README.md +5 -0
- package/commands/verify.ts +299 -0
- package/config.md +115 -1
- package/config.ts +2 -3
- package/index.ts +55 -5
- package/notify/welcome.ts +174 -68
- package/package.json +49 -1
- package/verify/chiral.ts +101 -0
- package/verify/config.ts +169 -0
- package/verify/index.ts +359 -0
- package/verify/number.ts +48 -0
- package/verify/reaction.ts +44 -0
- package/verify/state.ts +31 -0
- package/verify/types.ts +46 -0
package/notify/welcome.ts
CHANGED
|
@@ -73,8 +73,18 @@ 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 {
|
|
78
|
+
const {
|
|
79
|
+
ctx,
|
|
80
|
+
aiService,
|
|
81
|
+
config,
|
|
82
|
+
selfId,
|
|
83
|
+
groupId,
|
|
84
|
+
groupName,
|
|
85
|
+
members,
|
|
86
|
+
promptInjections,
|
|
87
|
+
} = options;
|
|
78
88
|
if (!members.length) return "";
|
|
79
89
|
|
|
80
90
|
const names = members.map((m) => m.memberName || String(m.userId));
|
|
@@ -104,12 +114,13 @@ async function flushBatch(options: {
|
|
|
104
114
|
groupId,
|
|
105
115
|
send: true,
|
|
106
116
|
instruction: [
|
|
107
|
-
`当前有 ${members.length} 位新成员同时入群,请一次性发送一段统一的欢迎语(不要逐个 @
|
|
117
|
+
`当前有 ${members.length} 位新成员同时入群,请一次性发送一段统一的欢迎语(不要逐个 @ 欢迎、不要重复点名)不要长篇大论,精简即可。`,
|
|
108
118
|
`新成员昵称:${userList}`,
|
|
109
119
|
`新成员 QQ:${userIdList}`,
|
|
110
120
|
`所在群:${groupName}`,
|
|
111
121
|
`${config.welcome.aiPrompt || ""}`,
|
|
112
122
|
].join("\n"),
|
|
123
|
+
promptInjections,
|
|
113
124
|
});
|
|
114
125
|
|
|
115
126
|
return "";
|
|
@@ -119,87 +130,134 @@ async function flushBatch(options: {
|
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
|
|
133
|
+
async function sendSingleWelcome(options: {
|
|
134
|
+
ctx: MiokiContext;
|
|
135
|
+
aiService?: AIService;
|
|
136
|
+
config: AdminConfig;
|
|
137
|
+
selfId: number;
|
|
138
|
+
groupId: number;
|
|
139
|
+
groupName: string;
|
|
140
|
+
userId: number;
|
|
141
|
+
memberName: string;
|
|
142
|
+
promptInjections?: { content: string; title?: string }[];
|
|
143
|
+
}): Promise<void> {
|
|
144
|
+
const {
|
|
145
|
+
ctx,
|
|
146
|
+
aiService,
|
|
147
|
+
config,
|
|
148
|
+
selfId,
|
|
149
|
+
groupId,
|
|
150
|
+
groupName,
|
|
151
|
+
userId,
|
|
152
|
+
memberName,
|
|
153
|
+
promptInjections,
|
|
154
|
+
} = options;
|
|
155
|
+
const welcomeMessage = await flushBatch({
|
|
156
|
+
ctx,
|
|
157
|
+
aiService,
|
|
158
|
+
config,
|
|
159
|
+
selfId,
|
|
160
|
+
groupId,
|
|
161
|
+
groupName,
|
|
162
|
+
members: [{ userId, memberName }],
|
|
163
|
+
promptInjections,
|
|
164
|
+
});
|
|
165
|
+
if (!welcomeMessage) return;
|
|
166
|
+
const bot = ctx.pickBot(selfId);
|
|
167
|
+
if (!bot) return;
|
|
168
|
+
try {
|
|
169
|
+
await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
ctx.logger.warn(`发送入群欢迎失败: ${error}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function triggerSingleWelcome(options: {
|
|
176
|
+
ctx: MiokiContext;
|
|
177
|
+
aiService?: AIService;
|
|
178
|
+
getConfig: () => AdminConfig;
|
|
179
|
+
selfId: number;
|
|
180
|
+
groupId: number;
|
|
181
|
+
groupName: string;
|
|
182
|
+
userId: number;
|
|
183
|
+
memberName?: string;
|
|
184
|
+
promptInjections?: { content: string; title?: string }[];
|
|
185
|
+
}): Promise<void> {
|
|
186
|
+
const memberName =
|
|
187
|
+
options.memberName ||
|
|
188
|
+
(await resolveMemberName(
|
|
189
|
+
options.ctx,
|
|
190
|
+
options.groupId,
|
|
191
|
+
options.userId,
|
|
192
|
+
options.selfId,
|
|
193
|
+
));
|
|
194
|
+
await sendSingleWelcome({
|
|
195
|
+
ctx: options.ctx,
|
|
196
|
+
aiService: options.aiService,
|
|
197
|
+
config: options.getConfig(),
|
|
198
|
+
selfId: options.selfId,
|
|
199
|
+
groupId: options.groupId,
|
|
200
|
+
groupName: options.groupName,
|
|
201
|
+
userId: options.userId,
|
|
202
|
+
memberName,
|
|
203
|
+
promptInjections: options.promptInjections,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
122
207
|
export function registerWelcomeHandler(
|
|
123
208
|
ctx: MiokiContext,
|
|
124
209
|
aiService: AIService | undefined,
|
|
125
210
|
getConfig: () => AdminConfig,
|
|
211
|
+
shouldSuppress?: (info: {
|
|
212
|
+
selfId: number;
|
|
213
|
+
groupId: number;
|
|
214
|
+
userId: number;
|
|
215
|
+
groupName: string;
|
|
216
|
+
}) => Promise<boolean> | boolean,
|
|
126
217
|
): () => void {
|
|
127
218
|
const batches = getBatchMap();
|
|
128
219
|
|
|
129
|
-
const dispose = ctx.handle(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
|
138
229
|
|
|
139
|
-
|
|
140
|
-
|
|
230
|
+
const groupName =
|
|
231
|
+
String(event?.group?.group_name || "").trim() || String(groupId);
|
|
141
232
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
ctx,
|
|
148
|
-
aiService,
|
|
149
|
-
config: cfg,
|
|
150
|
-
selfId,
|
|
151
|
-
groupId,
|
|
152
|
-
groupName,
|
|
153
|
-
members: [{ userId, memberName }],
|
|
154
|
-
});
|
|
155
|
-
if (!welcomeMessage) return;
|
|
156
|
-
const bot = ctx.pickBot(selfId);
|
|
157
|
-
if (!bot) return;
|
|
158
|
-
try {
|
|
159
|
-
await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
ctx.logger.warn(`发送入群欢迎失败: ${error}`);
|
|
233
|
+
if (
|
|
234
|
+
shouldSuppress &&
|
|
235
|
+
(await shouldSuppress({ selfId, groupId, userId, groupName }))
|
|
236
|
+
) {
|
|
237
|
+
return;
|
|
162
238
|
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
239
|
|
|
166
|
-
|
|
167
|
-
let state = batches.get(key);
|
|
168
|
-
if (!state) {
|
|
169
|
-
state = { members: [], timer: null, groupName };
|
|
170
|
-
batches.set(key, state);
|
|
171
|
-
}
|
|
172
|
-
if (groupName && groupName !== String(groupId)) {
|
|
173
|
-
state.groupName = groupName;
|
|
174
|
-
}
|
|
240
|
+
if (!cfg.welcome.enabled) return;
|
|
175
241
|
|
|
176
|
-
|
|
177
|
-
if (!state.members.some((m) => m.userId === userId)) {
|
|
178
|
-
state.members.push({ userId, memberName });
|
|
179
|
-
}
|
|
242
|
+
const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
|
|
180
243
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
batches.delete(key);
|
|
189
|
-
if (!pending || !pending.members.length) return;
|
|
190
|
-
|
|
191
|
-
const currentConfig = getConfig();
|
|
244
|
+
if (batchWindowMs === 0) {
|
|
245
|
+
const memberName = await resolveMemberName(
|
|
246
|
+
ctx,
|
|
247
|
+
groupId,
|
|
248
|
+
userId,
|
|
249
|
+
selfId,
|
|
250
|
+
);
|
|
192
251
|
const welcomeMessage = await flushBatch({
|
|
193
252
|
ctx,
|
|
194
253
|
aiService,
|
|
195
|
-
config:
|
|
254
|
+
config: cfg,
|
|
196
255
|
selfId,
|
|
197
256
|
groupId,
|
|
198
|
-
groupName
|
|
199
|
-
members:
|
|
257
|
+
groupName,
|
|
258
|
+
members: [{ userId, memberName }],
|
|
200
259
|
});
|
|
201
260
|
if (!welcomeMessage) return;
|
|
202
|
-
|
|
203
261
|
const bot = ctx.pickBot(selfId);
|
|
204
262
|
if (!bot) return;
|
|
205
263
|
try {
|
|
@@ -207,11 +265,59 @@ export function registerWelcomeHandler(
|
|
|
207
265
|
} catch (error) {
|
|
208
266
|
ctx.logger.warn(`发送入群欢迎失败: ${error}`);
|
|
209
267
|
}
|
|
210
|
-
|
|
211
|
-
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
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 });
|
|
212
284
|
}
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
);
|
|
215
321
|
|
|
216
322
|
return () => {
|
|
217
323
|
for (const state of batches.values()) {
|
|
@@ -220,4 +326,4 @@ export function registerWelcomeHandler(
|
|
|
220
326
|
batches.clear();
|
|
221
327
|
dispose();
|
|
222
328
|
};
|
|
223
|
-
}
|
|
329
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mioku-plugin-admin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
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
|
}
|
package/verify/chiral.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { MiokiContext } from "mioki";
|
|
2
|
+
import {
|
|
3
|
+
resolveVerifyPrompt,
|
|
4
|
+
type VerifyConfig,
|
|
5
|
+
type VerifyGroupConfig,
|
|
6
|
+
} from "./config";
|
|
7
|
+
import type { PendingVerify } from "./types";
|
|
8
|
+
|
|
9
|
+
interface ChiralCaptcha {
|
|
10
|
+
regions: string[];
|
|
11
|
+
imageDataUrl: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractRegions(text: string): string[] {
|
|
15
|
+
const matches = String(text || "").match(/[A-Za-z]\s*\d+/g);
|
|
16
|
+
if (!matches) return [];
|
|
17
|
+
return matches.map((m) => m.replace(/\s+/g, "").toUpperCase());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fetchChiralCaptcha(
|
|
21
|
+
apiUrl: string,
|
|
22
|
+
difficulty: "simple" | "hard",
|
|
23
|
+
): Promise<ChiralCaptcha> {
|
|
24
|
+
const res = await fetch(
|
|
25
|
+
`${apiUrl}/captcha/chiralCarbon/getChiralCarbonCaptcha`,
|
|
26
|
+
{
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ answer: true, hint: difficulty === "simple" }),
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`手性碳验证接口返回 ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
const json: any = await res.json();
|
|
36
|
+
const data = json?.data?.data;
|
|
37
|
+
const regions: string[] = Array.isArray(data?.regions)
|
|
38
|
+
? data.regions.map((r: any) => String(r).toUpperCase())
|
|
39
|
+
: [];
|
|
40
|
+
const imageDataUrl = String(data?.base64 || "").trim();
|
|
41
|
+
if (!regions.length || !imageDataUrl) {
|
|
42
|
+
throw new Error("手性碳验证接口返回数据缺失");
|
|
43
|
+
}
|
|
44
|
+
return { regions, imageDataUrl };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function prepareChiral(
|
|
48
|
+
ctx: MiokiContext,
|
|
49
|
+
cfg: VerifyConfig,
|
|
50
|
+
groupCfg: VerifyGroupConfig,
|
|
51
|
+
p: PendingVerify,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
const bot = ctx.pickBot(p.selfId);
|
|
54
|
+
if (!bot) return false;
|
|
55
|
+
try {
|
|
56
|
+
const captcha = await fetchChiralCaptcha(cfg.chiralApiUrl, cfg.chiralDifficulty);
|
|
57
|
+
p.requiredRegions = captcha.regions;
|
|
58
|
+
p.matchedRegions = new Set<string>();
|
|
59
|
+
const basePrompt = cfg.chiralPrompt.replace(
|
|
60
|
+
"{count}",
|
|
61
|
+
String(captcha.regions.length),
|
|
62
|
+
);
|
|
63
|
+
const prompt = resolveVerifyPrompt(basePrompt, groupCfg);
|
|
64
|
+
await bot.sendGroupMsg(p.groupId, [
|
|
65
|
+
ctx.segment.at(String(p.userId)),
|
|
66
|
+
ctx.segment.text(` ${prompt}`),
|
|
67
|
+
ctx.segment.image(captcha.imageDataUrl),
|
|
68
|
+
]);
|
|
69
|
+
return true;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
ctx.logger.error(`admin verify 手性碳验证准备失败: ${err}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ChiralCheckResult = {
|
|
77
|
+
status: "pass" | "progress" | "none";
|
|
78
|
+
remaining: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function checkChiralAnswer(
|
|
82
|
+
p: PendingVerify,
|
|
83
|
+
text: string,
|
|
84
|
+
): ChiralCheckResult {
|
|
85
|
+
const required = p.requiredRegions ?? [];
|
|
86
|
+
if (!required.length) return { status: "none", remaining: 0 };
|
|
87
|
+
if (!p.matchedRegions) p.matchedRegions = new Set<string>();
|
|
88
|
+
|
|
89
|
+
const requiredSet = new Set(required);
|
|
90
|
+
let progressed = false;
|
|
91
|
+
for (const region of extractRegions(text)) {
|
|
92
|
+
if (requiredSet.has(region) && !p.matchedRegions.has(region)) {
|
|
93
|
+
p.matchedRegions.add(region);
|
|
94
|
+
progressed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const remaining = requiredSet.size - p.matchedRegions.size;
|
|
99
|
+
if (remaining <= 0) return { status: "pass", remaining: 0 };
|
|
100
|
+
return { status: progressed ? "progress" : "none", remaining };
|
|
101
|
+
}
|
package/verify/config.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export type VerifyMode = "reaction" | "number" | "chiral";
|
|
2
|
+
|
|
3
|
+
export const MAX_EXTRA_PROMPT_LENGTH = 50;
|
|
4
|
+
|
|
5
|
+
export interface VerifyGroupConfig {
|
|
6
|
+
groupId: number;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
mode: VerifyMode;
|
|
9
|
+
extraPrompt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface VerifyConfig {
|
|
13
|
+
groups: VerifyGroupConfig[];
|
|
14
|
+
reactionEmojiId: string;
|
|
15
|
+
reactionDelayMs: number;
|
|
16
|
+
verifyTimeoutMs: number;
|
|
17
|
+
reactionPrompt: string;
|
|
18
|
+
numberPrompt: string;
|
|
19
|
+
chiralApiUrl: string;
|
|
20
|
+
chiralDifficulty: "simple" | "hard";
|
|
21
|
+
chiralPrompt: string;
|
|
22
|
+
maxInvalidMessages: number;
|
|
23
|
+
kickOnFail: boolean;
|
|
24
|
+
kickOnTimeout: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_VERIFY_CONFIG: VerifyConfig = {
|
|
28
|
+
groups: [],
|
|
29
|
+
reactionEmojiId: "424",
|
|
30
|
+
reactionDelayMs: 3000,
|
|
31
|
+
verifyTimeoutMs: 120000,
|
|
32
|
+
reactionPrompt:
|
|
33
|
+
"新来的小伙伴请在2分钟内点击下方红色按钮完成验证 不听话会被移出群聊喵~",
|
|
34
|
+
numberPrompt:
|
|
35
|
+
"新来的小伙伴请在2分钟内回答下面的题目完成验证,不听话移出群聊喵~\n请问:{question}",
|
|
36
|
+
chiralApiUrl: "https://carbon.crystelf.top",
|
|
37
|
+
chiralDifficulty: "simple",
|
|
38
|
+
chiralPrompt:
|
|
39
|
+
"新来的小伙伴请在2分钟内完成下面的手性碳验证:找出图中{count}个手性碳所在的格子,回复格子编号即可,不听话会被移出群聊喵~",
|
|
40
|
+
maxInvalidMessages: 5,
|
|
41
|
+
kickOnFail: true,
|
|
42
|
+
kickOnTimeout: true,
|
|
43
|
+
};
|
|
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
|
+
|
|
61
|
+
export function normalizeVerifyMode(value: unknown): VerifyMode {
|
|
62
|
+
const v = String(value || "").trim();
|
|
63
|
+
if (v === "number" || v === "数字") return "number";
|
|
64
|
+
if (v === "chiral" || v === "手性碳") return "chiral";
|
|
65
|
+
return "reaction";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeVerifyGroup(raw: any): VerifyGroupConfig {
|
|
69
|
+
const groupId = Number(raw?.groupId || raw?.group_id || 0);
|
|
70
|
+
return {
|
|
71
|
+
groupId: groupId > 0 ? groupId : 0,
|
|
72
|
+
enabled: raw?.enabled === true,
|
|
73
|
+
mode: normalizeVerifyMode(raw?.mode),
|
|
74
|
+
extraPrompt: normalizeExtraPrompt(raw?.extraPrompt ?? raw?.extra_prompt),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeVerifyConfig(raw: any): VerifyConfig {
|
|
79
|
+
const groups: VerifyGroupConfig[] = Array.isArray(raw?.groups)
|
|
80
|
+
? raw.groups
|
|
81
|
+
.map((g: any) => normalizeVerifyGroup(g))
|
|
82
|
+
.filter((g: VerifyGroupConfig) => g.groupId > 0)
|
|
83
|
+
: [];
|
|
84
|
+
|
|
85
|
+
const numOr = (value: unknown, fallback: number): number => {
|
|
86
|
+
const num = Number(value);
|
|
87
|
+
return Number.isFinite(num) && num >= 0 ? Math.floor(num) : fallback;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
groups,
|
|
92
|
+
reactionEmojiId:
|
|
93
|
+
typeof raw?.reactionEmojiId === "string" && raw.reactionEmojiId.trim()
|
|
94
|
+
? raw.reactionEmojiId.trim()
|
|
95
|
+
: DEFAULT_VERIFY_CONFIG.reactionEmojiId,
|
|
96
|
+
reactionDelayMs: numOr(
|
|
97
|
+
raw?.reactionDelayMs,
|
|
98
|
+
DEFAULT_VERIFY_CONFIG.reactionDelayMs,
|
|
99
|
+
),
|
|
100
|
+
verifyTimeoutMs: numOr(
|
|
101
|
+
raw?.verifyTimeoutMs,
|
|
102
|
+
DEFAULT_VERIFY_CONFIG.verifyTimeoutMs,
|
|
103
|
+
),
|
|
104
|
+
reactionPrompt:
|
|
105
|
+
typeof raw?.reactionPrompt === "string" && raw.reactionPrompt.trim()
|
|
106
|
+
? raw.reactionPrompt
|
|
107
|
+
: DEFAULT_VERIFY_CONFIG.reactionPrompt,
|
|
108
|
+
numberPrompt:
|
|
109
|
+
typeof raw?.numberPrompt === "string" && raw.numberPrompt.trim()
|
|
110
|
+
? raw.numberPrompt
|
|
111
|
+
: DEFAULT_VERIFY_CONFIG.numberPrompt,
|
|
112
|
+
chiralApiUrl:
|
|
113
|
+
typeof raw?.chiralApiUrl === "string" && raw.chiralApiUrl.trim()
|
|
114
|
+
? raw.chiralApiUrl.trim().replace(/\/+$/, "")
|
|
115
|
+
: DEFAULT_VERIFY_CONFIG.chiralApiUrl,
|
|
116
|
+
chiralDifficulty: raw?.chiralDifficulty === "hard" ? "hard" : "simple",
|
|
117
|
+
chiralPrompt:
|
|
118
|
+
typeof raw?.chiralPrompt === "string" && raw.chiralPrompt.trim()
|
|
119
|
+
? raw.chiralPrompt
|
|
120
|
+
: DEFAULT_VERIFY_CONFIG.chiralPrompt,
|
|
121
|
+
maxInvalidMessages: numOr(
|
|
122
|
+
raw?.maxInvalidMessages,
|
|
123
|
+
DEFAULT_VERIFY_CONFIG.maxInvalidMessages,
|
|
124
|
+
),
|
|
125
|
+
kickOnFail: raw?.kickOnFail ?? DEFAULT_VERIFY_CONFIG.kickOnFail,
|
|
126
|
+
kickOnTimeout: raw?.kickOnTimeout ?? DEFAULT_VERIFY_CONFIG.kickOnTimeout,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getGroupVerifyConfig(
|
|
131
|
+
config: VerifyConfig,
|
|
132
|
+
groupId: number,
|
|
133
|
+
): VerifyGroupConfig {
|
|
134
|
+
const found = config.groups.find((g) => g.groupId === groupId);
|
|
135
|
+
if (found) return found;
|
|
136
|
+
return { groupId, enabled: false, mode: "reaction", extraPrompt: "" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function upsertGroupVerifyConfig(
|
|
140
|
+
config: VerifyConfig,
|
|
141
|
+
groupId: number,
|
|
142
|
+
patch: Partial<Omit<VerifyGroupConfig, "groupId">>,
|
|
143
|
+
): VerifyConfig {
|
|
144
|
+
const idx = config.groups.findIndex((g) => g.groupId === groupId);
|
|
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
|
+
}
|
|
152
|
+
if (idx >= 0) {
|
|
153
|
+
next.groups = config.groups.map((g, i) =>
|
|
154
|
+
i === idx ? { ...g, ...normalizedPatch } : g,
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
next.groups = [
|
|
158
|
+
...config.groups,
|
|
159
|
+
{
|
|
160
|
+
groupId,
|
|
161
|
+
enabled: false,
|
|
162
|
+
mode: "reaction",
|
|
163
|
+
extraPrompt: "",
|
|
164
|
+
...normalizedPatch,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
return next;
|
|
169
|
+
}
|