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/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 { ctx, aiService, config, selfId, groupId, groupName, members } = options;
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("notice.group.increase" as any, async (event: any) => {
130
- const cfg = getConfig();
131
- const selfId = Number(event?.self_id || ctx.self_id);
132
- const groupId = Number(event?.group_id || 0);
133
- const userId = Number(event?.user_id || 0);
134
- if (!groupId || !userId) return;
135
- if (userId === selfId) return;
136
-
137
- if (!cfg.welcome.enabled) 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;
138
229
 
139
- const groupName =
140
- String(event?.group?.group_name || "").trim() || String(groupId);
230
+ const groupName =
231
+ String(event?.group?.group_name || "").trim() || String(groupId);
141
232
 
142
- const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
143
-
144
- if (batchWindowMs === 0) {
145
- const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
146
- const welcomeMessage = await flushBatch({
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
- const key = batchKey(selfId, groupId);
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
- const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
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
- if (state.timer) {
182
- return;
183
- }
184
-
185
- state.timer = setTimeout(async () => {
186
- try {
187
- const pending = state;
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: currentConfig,
254
+ config: cfg,
196
255
  selfId,
197
256
  groupId,
198
- groupName: pending.groupName,
199
- members: pending.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
- } catch (error) {
211
- ctx.logger.error(`admin welcome 批次处理失败: ${error}`);
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
- }, batchWindowMs);
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.2.3",
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
  }
@@ -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
+ }
@@ -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
+ }