mioku-plugin-chat 2.5.2 → 2.7.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.
@@ -1,89 +1,20 @@
1
+ import * as crypto from "crypto";
1
2
  import type { AIInstance } from "mioku";
3
+ import { logger } from "mioki";
2
4
  import type { ChatDatabase } from "../../db";
3
5
  import type { ImageRecord } from "../../types";
4
- import { logger } from "mioki";
5
- import * as crypto from "crypto";
6
- import * as path from "path";
7
- import * as fs from "fs/promises";
8
- import { existsSync, mkdirSync, createWriteStream, unlink } from "fs";
9
- import * as https from "https";
10
- import * as http from "http";
11
- import { URL } from "url";
12
6
  import { prepareImageUrlsForModel } from "./image-compress";
13
7
 
14
- /**
15
- * 图片分析结果
16
- */
17
8
  export interface ImageAnalysisResult {
18
9
  success: boolean;
19
- type?: "meme" | "image";
20
10
  description?: string;
21
- emotion?: string;
22
- characters?: string[]; // 支持多个角色
23
- character?: string; // 兼容旧版本
24
- gifBuffer?: Buffer; // GIF 原始 buffer
25
11
  error?: string;
26
12
  }
27
13
 
28
14
  export interface ImageAnalysisOptions {
29
15
  runAIRequest?<T>(request: () => Promise<T>): Promise<T | null>;
30
- /**
31
- * When non-empty, only memes whose detected character is in this list
32
- * get downloaded into `data/chat/meme/<character>/<emotion>/`. Other
33
- * characters still get a DB record but no local file.
34
- * Empty (default) = no restriction; save for every detected character.
35
- */
36
- allowedCharacters?: string[];
37
- }
38
-
39
- function formatImageRecognitionLog(record: {
40
- type: "meme" | "image";
41
- description: string;
42
- emotion?: string | null;
43
- character?: string | null;
44
- characters?: string[];
45
- }): string {
46
- const characters =
47
- record.characters && record.characters.length > 0
48
- ? record.characters
49
- : record.character
50
- ? [record.character]
51
- : [];
52
- return `[image-analyzer] ${record.type}: ${record.description}${record.emotion ? ` [${record.emotion}]` : ""}${characters.length > 0 ? ` (${characters.join(", ")})` : ""}`;
53
16
  }
54
17
 
55
- /**
56
- * 已知角色列表(用于提示词)
57
- */
58
- const KNOWN_CHARACTERS = [
59
- "hatsune_miku",
60
- "kagamine_rin",
61
- "kagamine_len",
62
- "megurine_luka",
63
- "kaito",
64
- "meiko",
65
- "unknown", // 未知角色
66
- ];
67
-
68
- /**
69
- * 情感标签列表
70
- */
71
- const EMOTION_TAGS = [
72
- "happy",
73
- "sad",
74
- "angry",
75
- "surprised",
76
- "confused",
77
- "excited",
78
- "tired",
79
- "shy",
80
- "proud",
81
- "default", // 默认/不清楚
82
- ];
83
-
84
- /**
85
- * 计算图片内容的哈希值(仅基于下载到的图片字节;URL 不参与哈希)
86
- */
87
18
  export async function calculateImageHash(url: string): Promise<string | null> {
88
19
  try {
89
20
  const response = await fetch(url);
@@ -104,140 +35,58 @@ export async function calculateImageHash(url: string): Promise<string | null> {
104
35
  }
105
36
  }
106
37
 
107
- /**
108
- * 下载图片到本地
109
- */
110
- async function downloadImage(url: string, savePath: string): Promise<boolean> {
111
- return new Promise((resolve) => {
112
- const dir = path.dirname(savePath);
113
- if (!existsSync(dir)) {
114
- mkdirSync(dir, { recursive: true });
115
- }
116
-
117
- const parsedUrl = new URL(url);
118
- const protocol = parsedUrl.protocol === "https:" ? https : http;
119
-
120
- const file = createWriteStream(savePath);
121
- const options = {
122
- headers: {
123
- "User-Agent":
124
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
125
- Referer: "https://q.qq.com/",
126
- },
127
- };
128
-
129
- protocol
130
- .get(url, options, (response) => {
131
- response.pipe(file);
132
- file.on("finish", () => {
133
- file.close();
134
- resolve(true);
135
- });
136
- })
137
- .on("error", (err) => {
138
- unlink(savePath, () => {});
139
- logger.error(`[image-analyzer] Failed to download image: ${err}`);
140
- resolve(false);
141
- });
142
- });
143
- }
144
-
145
- /**
146
- * 获取图片扩展名
147
- */
148
- function getImageExtension(url: string): string {
149
- const urlPath = new URL(url).pathname;
150
- const ext = path.extname(urlPath).toLowerCase();
151
- if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(ext)) {
152
- return ext;
153
- }
154
- return ".jpg"; // 默认
155
- }
156
-
157
- /**
158
- * 分析图片内容
159
- */
160
38
  export async function analyzeImage(
161
39
  ai: AIInstance,
162
40
  imageUrl: string,
163
41
  model: string,
164
- gifBuffer?: Buffer,
165
42
  options?: ImageAnalysisOptions,
166
43
  ): Promise<ImageAnalysisResult> {
167
44
  try {
168
- // 检查是否为 GIF,如果是则提取三帧
169
45
  const { isGifUrl, extractGifFrames } = await import("./gif-extractor");
170
- let imageUrls: string[] = [imageUrl];
171
- let originalGifBuffer: Buffer | undefined = gifBuffer;
46
+ let imageUrls = [imageUrl];
172
47
 
173
48
  if (await isGifUrl(imageUrl)) {
174
49
  const result = await extractGifFrames(imageUrl);
175
- if (result && result.frames.length > 0) {
50
+ if (result?.frames.length) {
176
51
  imageUrls = result.frames;
177
- originalGifBuffer = result.buffer;
178
52
  } else {
179
53
  logger.warn(
180
- `[image-analyzer] Failed to extract GIF frames, using original URL`,
54
+ "[image-analyzer] Failed to extract GIF frames, using original URL",
181
55
  );
182
56
  }
183
57
  }
184
58
 
185
59
  imageUrls = await prepareImageUrlsForModel(imageUrls);
186
60
 
187
- const systemPrompt = `You are an image classification and analysis assistant. Analyze images and return structured information.
61
+ const systemPrompt = `You are an image description assistant. Describe the image accurately for use in chat history.
188
62
 
189
63
  Instructions:
190
- 1. Classify as "meme" or "image":
191
- - "meme": A standalone, single-scene character image (anime/cartoon/VTuber etc.) used to express ONE specific emotion. Isolated character on a clean/simple background, exaggerated expression or pose, often with short punchline text.
192
- - "image": Everything else — real photos, selfies, landscapes, screenshots, UI captures, infographics, news images, full-body illustrations without expressive intent, etc. CHAT / IM INTERFACE SCREENSHOTS (QQ, WeChat, Discord, Telegram, LINE, message bubbles, chat history) are always "image", even when memes appear inside them.
193
- - When in doubt, prefer "image".
194
-
195
- 2. Provide a brief description (max 30 words in Chinese):
196
- - For memes: Used as the saved filename. Describe the character's pose / expression / action and any visible text.
197
- - For images: Describe what you see factually, including any visible text.
198
- ${imageUrls.length > 1 ? "\n - You are viewing multiple frames from an animated image (GIF). Consider the overall motion and emotion across all frames." : ""}
199
-
200
- 3. For memes only:
201
- - Emotion tag: ${EMOTION_TAGS.join(", ")} (choose ONLY ONE).
202
- - Characters: STRICT identification — only name a character when you are CERTAIN of their identity.
203
- • If you know the character's name, add their English identifier to "characters".
204
- • If you are not sure, or you do not actually know the character's name, put "unknown" in the array. Do NOT guess.
205
- • DO NOT invent or infer a character's name from their appearance (hair color, outfit, art style, accessories, species, etc.). Many characters look alike across different series.
206
- • DO NOT name a character just because they look similar to a known one.
207
- • Only name a character you can confidently identify. Otherwise use "unknown".
208
- - Known identifiers: ${KNOWN_CHARACTERS.join(", ")}. You may NOT add new identifiers outside this list — any character you recognize that is not on the list still maps to "unknown".
64
+ - Return a concise factual description in Chinese, no more than 30 words.
65
+ - Include important visible subjects, actions, expressions, and text.
66
+ - Treat every input as a regular image. Do not classify it as a meme or sticker.
67
+ ${imageUrls.length > 1 ? "- You are viewing frames from an animated image. Describe the overall action across the frames." : ""}
209
68
 
210
69
  Response format (JSON):
211
- {
212
- "type": "meme" or "image",
213
- "description": "brief description in Chinese (for memes: describe what you see; do not invent a character name)",
214
- "emotion": "emotion tag (ONLY for memes)",
215
- "characters": ["known_character_identifier", "unknown", ...] (ONLY for memes; use only identifiers from the known list, or "unknown")
216
- }`;
217
-
218
- const userPrompt =
219
- imageUrls.length > 1
220
- ? `Please analyze these ${imageUrls.length} frames from an animated image and provide the classification and description.`
221
- : "Please analyze this image and provide the classification and description.";
222
-
223
- // 构建消息内容
224
- const contentParts: any[] = [{ type: "text", text: userPrompt }];
225
- for (const url of imageUrls) {
226
- contentParts.push({
70
+ {"description":"brief Chinese description"}`;
71
+ const content: any[] = [
72
+ {
73
+ type: "text",
74
+ text:
75
+ imageUrls.length > 1
76
+ ? `Describe these ${imageUrls.length} frames from one animated image.`
77
+ : "Describe this image.",
78
+ },
79
+ ...imageUrls.map((url) => ({
227
80
  type: "image_url",
228
81
  image_url: { url, detail: "auto" },
229
- });
230
- }
231
-
82
+ })),
83
+ ];
232
84
  const request = () =>
233
85
  ai.complete({
234
86
  model,
235
87
  messages: [
236
88
  { role: "system", content: systemPrompt },
237
- {
238
- role: "user",
239
- content: contentParts,
240
- },
89
+ { role: "user", content },
241
90
  ],
242
91
  temperature: 0.3,
243
92
  });
@@ -245,95 +94,26 @@ Response format (JSON):
245
94
  ? await options.runAIRequest(request)
246
95
  : await request();
247
96
 
248
- if (!response) {
249
- return {
250
- success: false,
251
- error: "Request skipped due to active rate limit",
252
- };
253
- }
254
-
255
- if (!response.content) {
97
+ if (!response?.content) {
256
98
  return {
257
99
  success: false,
258
- error: "Model returned empty response",
100
+ error: response
101
+ ? "Model returned empty response"
102
+ : "Request skipped due to active rate limit",
259
103
  };
260
104
  }
261
105
 
262
- // 解析 JSON 响应
263
- let result: any;
264
- try {
265
- // 尝试提取 JSON(可能包含在 markdown 代码块中)
266
- const jsonMatch = response.content.match(/\{[\s\S]*\}/);
267
- if (jsonMatch) {
268
- result = JSON.parse(jsonMatch[0]);
269
- } else {
270
- result = JSON.parse(response.content);
271
- }
272
- } catch {
273
- logger.warn(
274
- `[image-analyzer] Failed to parse JSON response: ${response.content}`,
275
- );
276
- return {
277
- success: false,
278
- error: "Failed to parse model response",
279
- };
280
- }
281
-
282
- // 验证和规范化结果
283
- const type = result.type === "meme" ? "meme" : "image";
284
- const description = String(result.description || "未知");
285
- const emotion = type === "meme" ? result.emotion || "default" : undefined;
286
-
287
- // 支持多个角色
288
- let characters: string[] | undefined;
289
- if (type === "meme") {
290
- if (Array.isArray(result.characters)) {
291
- characters = result.characters
292
- .map((c: string) => c.trim().toLowerCase())
293
- .filter(Boolean);
294
- }
295
- if (characters && characters.length === 0) {
296
- characters = undefined;
297
- }
298
- }
299
-
300
- const character =
301
- type === "meme"
302
- ? characters && characters.length > 0
303
- ? characters[0]
304
- : "unknown"
305
- : undefined;
306
-
307
- logger.info(
308
- formatImageRecognitionLog({
309
- type,
310
- description,
311
- emotion,
312
- characters,
313
- }),
314
- );
315
-
316
- return {
317
- success: true,
318
- type,
319
- description,
320
- emotion,
321
- characters,
322
- character,
323
- gifBuffer: originalGifBuffer,
324
- };
106
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
107
+ const result = JSON.parse(jsonMatch?.[0] || response.content);
108
+ const description = String(result.description || "未知").trim() || "未知";
109
+ logger.info(`[image-analyzer] image: ${description}`);
110
+ return { success: true, description };
325
111
  } catch (err) {
326
112
  logger.error(`[image-analyzer] Failed to analyze image: ${err}`);
327
- return {
328
- success: false,
329
- error: String(err),
330
- };
113
+ return { success: false, error: String(err) };
331
114
  }
332
115
  }
333
116
 
334
- /**
335
- * 处理图片:分析、保存到数据库、下载表情包
336
- */
337
117
  export async function processImage(
338
118
  ai: AIInstance,
339
119
  imageUrl: string,
@@ -342,7 +122,6 @@ export async function processImage(
342
122
  options?: ImageAnalysisOptions,
343
123
  ): Promise<ImageRecord | null> {
344
124
  try {
345
- // 计算哈希(基于图片内容)
346
125
  const hash = await calculateImageHash(imageUrl);
347
126
  if (!hash) {
348
127
  logger.warn(
@@ -351,123 +130,28 @@ export async function processImage(
351
130
  return null;
352
131
  }
353
132
 
354
- // 检查是否已存在
355
133
  const existing = db.getImageByHash(hash);
356
134
  if (existing) {
357
- logger.info(formatImageRecognitionLog(existing));
358
- return existing;
135
+ const normalized = asImageRecord(existing, imageUrl);
136
+ db.saveImage(normalized);
137
+ logger.info(`[image-analyzer] image: ${normalized.description}`);
138
+ return normalized;
359
139
  }
360
- const analysis = await analyzeImage(
361
- ai,
362
- imageUrl,
363
- model,
364
- undefined,
365
- options,
366
- );
367
- if (!analysis.success || !analysis.type) {
140
+
141
+ const analysis = await analyzeImage(ai, imageUrl, model, options);
142
+ if (!analysis.success) {
368
143
  logger.warn(`[image-analyzer] Analysis failed: ${analysis.error}`);
369
144
  return null;
370
145
  }
371
146
 
372
- let filePath: string | undefined;
373
-
374
- // 如果是表情包,下载到本地
375
- if (analysis.type === "meme" && analysis.emotion && analysis.description) {
376
- // 确定文件扩展名:GIF 用 .gif,其他用原始扩展名
377
- const ext = analysis.gifBuffer ? ".gif" : getImageExtension(imageUrl);
378
-
379
- // 文件名使用描述,不限制长度,只替换非法字符
380
- const safeDesc = analysis.description.replace(
381
- /[^\u4e00-\u9fa5a-zA-Z0-9]/g,
382
- "_",
383
- );
384
- const fileName = `${safeDesc}${ext}`;
385
-
386
- // 获取角色列表(支持多个角色)
387
- const characters =
388
- analysis.characters && analysis.characters.length > 0
389
- ? analysis.characters
390
- : [analysis.character || "unknown"];
391
-
392
- // 应用 allowedCharacters 过滤:配置非空时只对列表内角色本地保存
393
- const allowed = options?.allowedCharacters;
394
- const hasRestriction = Array.isArray(allowed) && allowed.length > 0;
395
- const normalizeName = (s: string) => String(s || "").trim().toLowerCase();
396
- const allowedSet = hasRestriction
397
- ? new Set((allowed as string[]).map(normalizeName))
398
- : null;
399
-
400
- const saveableCharacters = allowedSet
401
- ? characters.filter((c) => allowedSet.has(normalizeName(c)))
402
- : characters;
403
-
404
- const skipped = characters.filter(
405
- (c) => !saveableCharacters.includes(c),
406
- );
407
- if (skipped.length > 0) {
408
- logger.info(
409
- `[image-analyzer] Skipping local file save for characters outside allowedCharacters: ${skipped.join(", ")}`,
410
- );
411
- }
412
-
413
- // 为每个允许保存的角色创建文件夹并保存图片
414
- const savePromises = saveableCharacters.map(async (character) => {
415
- const memeDir = path.join(
416
- process.cwd(),
417
- "data",
418
- "chat",
419
- "meme",
420
- character,
421
- analysis.emotion!,
422
- );
423
- const targetPath = path.join(memeDir, fileName);
424
-
425
- // 确保目录存在
426
- if (!existsSync(memeDir)) {
427
- await fs.mkdir(memeDir, { recursive: true });
428
- }
429
-
430
- // 如果是 GIF 且有原始 buffer
431
- if (analysis.gifBuffer) {
432
- try {
433
- await fs.writeFile(targetPath, analysis.gifBuffer);
434
- return targetPath;
435
- } catch (err) {
436
- logger.warn(
437
- `[image-analyzer] Failed to save GIF for ${character}: ${err}`,
438
- );
439
- return null;
440
- }
441
- } else {
442
- // 普通图片,下载
443
- const downloaded = await downloadImage(imageUrl, targetPath);
444
- if (downloaded) {
445
- return targetPath;
446
- } else {
447
- logger.warn(`[image-analyzer] Download failed for ${character}`);
448
- return null;
449
- }
450
- }
451
- });
452
-
453
- const savedPaths = await Promise.all(savePromises);
454
- filePath = savedPaths.find((p) => p !== null) || undefined;
455
- }
456
-
457
- // 保存到数据库
458
147
  const record: ImageRecord = {
459
148
  hash,
460
149
  url: normalizeImageUrl(imageUrl) || imageUrl,
461
- type: analysis.type,
150
+ type: "image",
462
151
  description: analysis.description || "未知",
463
- emotion: analysis.emotion,
464
- character: analysis.character,
465
- filePath,
466
152
  createdAt: Date.now(),
467
153
  };
468
-
469
154
  db.saveImage(record);
470
-
471
155
  return record;
472
156
  } catch (err) {
473
157
  logger.error(`[image-analyzer] Failed to process image: ${err}`);
@@ -475,69 +159,51 @@ export async function processImage(
475
159
  }
476
160
  }
477
161
 
478
- /**
479
- * 从图片 URL 获取描述标签
480
- * 命中数据库记录时返回 [meme:描述] 或 [image:描述],否则返回 [image]。
481
- * 收集聊天历史时用它,之前没识别到的图片不会再次触发请求。
482
- */
483
- export async function getImageTag(imageUrl: string, db: ChatDatabase): Promise<string> {
162
+ export async function getImageTag(
163
+ imageUrl: string,
164
+ db: ChatDatabase,
165
+ ): Promise<string> {
484
166
  const exact = db.getImageByUrl(imageUrl);
485
- if (exact) {
486
- return `[${exact.type}:${exact.description}]`;
487
- }
167
+ if (exact) return `[image:${exact.description}]`;
488
168
 
489
169
  const normalized = normalizeImageUrl(imageUrl);
490
170
  if (normalized && normalized !== imageUrl) {
491
171
  const normalizedHit = db.getImageByUrl(normalized);
492
- if (normalizedHit) {
493
- return `[${normalizedHit.type}:${normalizedHit.description}]`;
494
- }
172
+ if (normalizedHit) return `[image:${normalizedHit.description}]`;
495
173
  }
496
174
 
497
175
  const hash = await calculateImageHash(imageUrl);
498
176
  if (hash) {
499
177
  const byHash = db.getImageByHash(hash);
500
178
  if (byHash) {
501
- try {
502
- db.saveImage({
503
- hash: byHash.hash,
504
- url: normalized || imageUrl,
505
- type: byHash.type,
506
- description: byHash.description,
507
- emotion: byHash.emotion,
508
- character: byHash.character,
509
- filePath: byHash.filePath,
510
- createdAt: byHash.createdAt,
511
- });
512
- } catch {
513
- // ignore — best-effort URL caching for next time
514
- }
515
- return `[${byHash.type}:${byHash.description}]`;
179
+ db.saveImage(asImageRecord(byHash, normalized || imageUrl));
180
+ return `[image:${byHash.description}]`;
516
181
  }
517
182
  }
518
183
 
519
184
  return "[image]";
520
185
  }
521
186
 
522
- /**
523
- * Strip query string and hash so URLs from different NapCat code paths
524
- * (live vs. get_group_msg_history) still match. QQ gchat.qpic.cn URLs
525
- * include varying `?term=2`, `?spec=0`, signed tokens, etc.
526
- */
187
+ function asImageRecord(record: ImageRecord, url: string): ImageRecord {
188
+ return {
189
+ ...record,
190
+ url: normalizeImageUrl(url) || url,
191
+ type: "image",
192
+ };
193
+ }
194
+
527
195
  export function normalizeImageUrl(url: string): string {
528
196
  if (!url || typeof url !== "string") return "";
529
197
  const trimmed = url.trim();
530
198
  if (!trimmed) return "";
531
199
  try {
532
- const u = new URL(trimmed);
533
- return `${u.protocol}//${u.host}${u.pathname}`;
200
+ const parsed = new URL(trimmed);
201
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
534
202
  } catch {
535
- const qIdx = trimmed.indexOf("?");
536
- const hashIdx = trimmed.indexOf("#");
537
- let cutAt = -1;
538
- if (qIdx >= 0 && hashIdx >= 0) cutAt = Math.min(qIdx, hashIdx);
539
- else if (qIdx >= 0) cutAt = qIdx;
540
- else if (hashIdx >= 0) cutAt = hashIdx;
203
+ const queryIndex = trimmed.indexOf("?");
204
+ const hashIndex = trimmed.indexOf("#");
205
+ const indexes = [queryIndex, hashIndex].filter((index) => index >= 0);
206
+ const cutAt = indexes.length > 0 ? Math.min(...indexes) : -1;
541
207
  return cutAt >= 0 ? trimmed.slice(0, cutAt) : trimmed;
542
208
  }
543
209
  }
package/core/prompt.ts CHANGED
@@ -176,10 +176,9 @@ const TOOL_INTENSITY_BLOCK: Record<Strength, string> = {
176
176
 
177
177
  const EMOJI_MODE_LINE: Record<Strength, string> = {
178
178
  high:
179
- "- Keep stickers rare. Use one only when it clearly strengthens a strong emotional beat or punchline.",
180
- medium:
181
- "- You may use a sticker for obvious emotional beats, reactions, jokes, teasing, or celebrations, but do not overuse it.",
182
- low: "- When it helps the emotional effect of the reply, you can use a matching sticker more freely.",
179
+ "- Do not use stickers in consecutive chat turns. Use a sticker only when you are in an emotionally intense state.",
180
+ medium: "- Do not use stickers in consecutive chat turns.",
181
+ low: "- Feel free to use a sticker whenever you want.",
183
182
  };
184
183
 
185
184
  const WEB_SEARCH_LINE: Record<Strength, string> = {
@@ -281,7 +280,7 @@ function buildResponseFormatSection(
281
280
  - **If your reply has multiple sentences or different points, ALWAYS use real line breaks to separate them**
282
281
  - NEVER use "\\" or literal "\\n" to simulate a new line
283
282
  - **MESSAGE ORDER MATTERS**: messages are sent top-to-bottom, one line at a time.
284
- - For action markers like [meme:...] or [audio:...], put them on their own line when they are meant to be a separate action.
283
+ - For action markers like [] or [audio:...], put them on their own line when they are meant to be a separate action.
285
284
 
286
285
  - **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
287
286
  - Use [at:123456] in your text to @ someone (123456 is the QQ number)
@@ -338,23 +337,15 @@ function appendEmojiSection(
338
337
  if (!emojiAgent || !ctx.config.emoji?.enabled) return;
339
338
 
340
339
  const configChars = ctx.config.emoji.characters || [];
341
- const chars =
342
- configChars.length > 0 ? configChars : emojiAgent.getAvailableCharacters();
343
- const availableEmotions: string[] = [];
344
- for (const char of chars) {
345
- availableEmotions.push(...emojiAgent.getAvailableEmotions(char));
346
- }
347
- const uniqueEmotions = [...new Set(availableEmotions)].sort();
348
- if (uniqueEmotions.length === 0) return;
340
+ if (!emojiAgent.hasAvailableEmojis(configChars)) return;
349
341
 
350
342
  lines.push(`
351
343
  ### Optional Sticker / Emoji Format
352
- - You MAY optionally send one matching sticker by writing [meme:emotion]
344
+ - You MAY optionally request one matching sticker by writing exactly [] on its own line
353
345
  ${EMOJI_MODE_LINE[emojiStrength]}
354
- - Do NOT send a sticker in every reply, and do not force one when the mood is plain
355
- - Prefer one matching sticker at most. It should enhance the text instead of replacing meaningful content
356
- - Put [meme:...] on its own line when it should be a separate action message after text
357
- - Available emotions: ${uniqueEmotions.join(", ")}`);
346
+ - Never put an emotion, label, character, or any other text inside the brackets
347
+ - Output at most one [] block in a turn
348
+ - A separate sticker selection agent will read the current conversation and choose the actual sticker`);
358
349
  }
359
350
 
360
351
  function appendExternalSkillsSection(
@@ -636,7 +627,7 @@ Just the last few messages - don't overthink it or dig into old conversations:
636
627
 
637
628
  ${mergedLines.join("\n")}
638
629
 
639
- Note: Messages may contain media tags like [meme:描述], [image:描述], [video:描述], [forward:摘要], [card:摘要], or [group_notice:摘要]. These are brief processed summaries. If you need detailed information about an image or video, use the view_media tool with the message ID.
630
+ Note: Messages may contain media tags like [image:描述], [video:描述], [forward:摘要], [card:摘要], or [group_notice:摘要]. If you need detailed information about an image or video, use the view_media tool with the message ID.
640
631
 
641
632
  -- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --`;
642
633
  }
@@ -756,4 +747,4 @@ export function buildRecallMemoryFeatureSection(config: ChatConfig): string {
756
747
  - Use recall_memory ONLY when there is explicit need to recall past content and required information is clearly missing from current context.
757
748
  - Do NOT call recall_memory for every question.
758
749
  - The worker returns historical logs with timestamps; treat them as past records, not newly sent messages.`;
759
- }
750
+ }
package/core/tools/web.ts CHANGED
@@ -287,7 +287,7 @@ export function createRecallMemoryTool(toolCtx: ToolContext): AITool {
287
287
  messages: toolCtx.db.getMessagesByUser(userId, toolCtx.sessionId, userHistoryLimit),
288
288
  }));
289
289
 
290
- const retriever = new MemoryRetrieval(ai, toolCtx.config, toolCtx.db);
290
+ const retriever = new MemoryRetrieval(ai, () => toolCtx.config, toolCtx.db);
291
291
  const answer = await retriever.retrieveByQuestion({
292
292
  sessionId: toolCtx.sessionId,
293
293
  question,