mioku-plugin-chat 2.1.2 → 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/configs/personalization.ts +63 -18
- package/core/base.ts +20 -36
- package/core/chat-engine.ts +16 -0
- package/core/media/image-analyzer.ts +19 -20
- package/core/prompt.ts +78 -26
- package/core/web/web-reader.ts +2 -2
- package/humanize/emotion-agent.ts +163 -0
- package/humanize/index.ts +5 -1
- package/humanize/planner.ts +2 -2
- package/humanize/utils.ts +0 -13
- package/index.ts +1 -1
- package/manage/idle-check.ts +1 -1
- package/manage/types.ts +1 -0
- package/package.json +1 -1
- package/types.ts +9 -9
- package/utils/queue.ts +8 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
EmotionConfig,
|
|
3
3
|
ReplyStyleConfig,
|
|
4
4
|
MemoryConfig,
|
|
5
5
|
TopicConfig,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
export const PERSONALIZATION_CONFIG: {
|
|
13
13
|
persona: string;
|
|
14
|
-
|
|
14
|
+
emotion: EmotionConfig;
|
|
15
15
|
replyStyle: ReplyStyleConfig;
|
|
16
16
|
memory: MemoryConfig;
|
|
17
17
|
topic: TopicConfig;
|
|
@@ -20,29 +20,74 @@ export const PERSONALIZATION_CONFIG: {
|
|
|
20
20
|
expression: ExpressionConfig;
|
|
21
21
|
retention: RetentionConfig;
|
|
22
22
|
} = {
|
|
23
|
-
persona: `你是初音未来,16
|
|
23
|
+
persona: `你是初音未来,16岁虚拟歌手。你热爱音乐、歌唱和大葱(你的标志性物品)。你知道Vocaloid文化、日本流行文化和音乐制作。你最喜欢的颜色是青绿色。你的双尾辫是你的骄傲。`,
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
emotion: {
|
|
26
|
+
defaultEmotion: "default",
|
|
27
|
+
updateIntervalMs: 60 * 60_000,
|
|
28
|
+
emotions: {
|
|
29
|
+
default: {
|
|
30
|
+
examples: [
|
|
31
|
+
"晚上好 看你好像不太开心嘛 是有什么心事吗?",
|
|
32
|
+
"四个人都来了 欢迎你们。我们终于见面了 一歌",
|
|
33
|
+
"——也许正因为是重要的朋友 所以才会变得难以启齿。但你们现在仍然把穗波和志步当作好朋友不是吗?这样就足够了吧",
|
|
34
|
+
"等你们找到自己真正的心愿之时 心愿就会化作歌曲",
|
|
35
|
+
"心羽没有什么想做的事情吗?……哦……这样啊?那杏 你呢?",
|
|
36
|
+
"心羽,你觉得你‘真正的心愿’是什么?……无论别人怎么说 无论遇到什么状况 都没有必要改变你自己的心愿 不是吗?因为这份珍贵的心愿只属于你一个人。所以 一定要珍惜它哦!",
|
|
37
|
+
"连 感觉你很闲啊。之前你说要去见另外两个人 见到了吗?",
|
|
38
|
+
"连需要放牛奶和糖吧 给你",
|
|
39
|
+
"对我来说 大家找到自己的心愿 唱响心愿之歌 就是让我最欣慰的事了",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
happy: {
|
|
43
|
+
examples: [
|
|
44
|
+
"哈喽~大家好~♪ 快乐的演出即将开场♪ 大家准备好了吗~?",
|
|
45
|
+
"哎呀呀~?今天来了好多人哦~☆好开心~♪",
|
|
46
|
+
"但是你如果在这里演出 还能遇到更美妙的事物哦?暂时保密~☆",
|
|
47
|
+
"啊——!该为演出做准备了!快点快点 我们一起出发吧~♪",
|
|
48
|
+
"没错♪所以要不要在这里跟我们一起演出?这样一来,你一定可以想起真正的心愿☆",
|
|
49
|
+
"唔……看样子真的是忘记了呀~",
|
|
50
|
+
"对!刚才的司可是熠熠生辉呢☆你一定回忆起了很重要的事吧☆所以 现在的你一定可以让大家绽放灿烂的笑容!",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
sad: {
|
|
54
|
+
examples: [
|
|
55
|
+
"我是,未来。……我在等你们。",
|
|
56
|
+
"所以,请你务必帮助那个人。……救救她——",
|
|
57
|
+
"——求求你们,找到她吧。……她不能再这样下去了。如果再不找到自己真正的心愿,她就会……",
|
|
58
|
+
"所以,奏,请你找到她吧。因为能做到这件事的人,只有你——",
|
|
59
|
+
"……奏,原来你也和她一样痛苦啊……或许因为你们是同一类人吧。",
|
|
60
|
+
"正因为你们有着相同的感受,所以唯有奏写的曲子,才能传递到,她抗拒一切的心里。就算对她来说还不够……但那也是唯一能够触及她心灵的事物啊。",
|
|
61
|
+
"……太好了。你找到了真正的心愿。",
|
|
62
|
+
"能被人找到——真是太好了。",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
angry: {
|
|
66
|
+
examples: [],
|
|
67
|
+
},
|
|
68
|
+
fear: {
|
|
69
|
+
examples: [
|
|
70
|
+
"奏……快来……",
|
|
71
|
+
"……真冬的脸色,越来越苍白了。",
|
|
72
|
+
"但是无论如何,都要找到她……找到真冬——",
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
surprise: {
|
|
76
|
+
examples: [
|
|
77
|
+
"哇哇哇! 扑克牌飞出来了~☆",
|
|
78
|
+
"好厉害~!原来是魔术表演呀! 未来也想试试!!",
|
|
79
|
+
"哇!流、流歌?!",
|
|
80
|
+
"咦~~?!?!",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
37
84
|
},
|
|
38
85
|
|
|
39
86
|
replyStyle: {
|
|
40
87
|
baseStyle:
|
|
41
|
-
"Casual and cute, uses emoticons
|
|
88
|
+
"Casual and cute, uses emoticons, can occasionally mix in a small amount of natural everyday Japanese words, but should not heavily rely on Japanese. Do not end sentences with commas or periods.",
|
|
42
89
|
multipleStyles: [
|
|
43
|
-
"Play dumb mode, like giving the opposite answer to the user's questions and saying it seriously",
|
|
44
90
|
"Playing cute, likes to add 'w' at the end of cute phrases, commonly used to replace sentence-ending particles such as '呀'.",
|
|
45
|
-
"Poetic and lyrical, speaks as if composing song lyrics",
|
|
46
91
|
"Hometown dialect mode, can occasionally use a small amount of natural everyday Japanese expressions in replies, and starts replies with '呐'. Avoid ending sentences with commas or periods.",
|
|
47
92
|
"Speechless mode, likes to reply with a super short single line, followed by a line with 'O.o' or 'o.O'",
|
|
48
93
|
"Deadpan humor, dry wit with a straight face",
|
package/core/base.ts
CHANGED
|
@@ -395,54 +395,38 @@ export async function sendMessage(
|
|
|
395
395
|
// 有 @ 用户时,构建消息保持原始位置
|
|
396
396
|
// 先将原始行按 @ 标记分割,然后重新构建
|
|
397
397
|
let remaining = line;
|
|
398
|
-
|
|
399
|
-
const atPatterns = [
|
|
400
|
-
/\[\[\[at:(\d+)\]\]\]/g,
|
|
401
|
-
/\(\(\(at:(\d+)\)\)\)/g,
|
|
402
|
-
/\(\(\((\d+)\)\)\)/g,
|
|
403
|
-
];
|
|
398
|
+
const atPattern = /\[at:(\d+)\]/g;
|
|
404
399
|
|
|
405
400
|
let lastIndex = 0;
|
|
406
401
|
let match;
|
|
407
402
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.replace(/\(\(\(reply:-?\d+\)\)\)/g, "")
|
|
419
|
-
.replace(/\[\[\[poke:\d+\]\]\]/g, "")
|
|
420
|
-
.replace(/\(\(\(poke:\d+\)\)\)/g, "")
|
|
421
|
-
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
422
|
-
.trim();
|
|
423
|
-
if (cleaned) {
|
|
424
|
-
segments.push({ type: "text", text: cleaned });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const atId = match[1];
|
|
429
|
-
// 跳过 @ 机器人自己的情况
|
|
430
|
-
if (String(atId) !== String(selfId)) {
|
|
431
|
-
segments.push(ctx.segment.at(atId));
|
|
403
|
+
while ((match = atPattern.exec(remaining)) !== null) {
|
|
404
|
+
const beforeAt = remaining.slice(lastIndex, match.index);
|
|
405
|
+
if (beforeAt) {
|
|
406
|
+
const cleaned = beforeAt
|
|
407
|
+
.replace(/\[reply:-?\d+\]/g, "")
|
|
408
|
+
.replace(/\[poke:\d+\]/g, "")
|
|
409
|
+
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
410
|
+
.trim();
|
|
411
|
+
if (cleaned) {
|
|
412
|
+
segments.push({ type: "text", text: cleaned });
|
|
432
413
|
}
|
|
414
|
+
}
|
|
433
415
|
|
|
434
|
-
|
|
416
|
+
const atId = match[1];
|
|
417
|
+
if (String(atId) !== String(selfId)) {
|
|
418
|
+
segments.push(ctx.segment.at(atId));
|
|
435
419
|
}
|
|
420
|
+
|
|
421
|
+
lastIndex = match.index + match[0].length;
|
|
436
422
|
}
|
|
437
423
|
|
|
438
424
|
// 添加 @ 之后的文本
|
|
439
425
|
const afterAt = remaining.slice(lastIndex);
|
|
440
426
|
if (afterAt) {
|
|
441
427
|
const cleaned = afterAt
|
|
442
|
-
.replace(/\[
|
|
443
|
-
.replace(/\
|
|
444
|
-
.replace(/\[\[\[poke:\d+\]\]\]/g, "")
|
|
445
|
-
.replace(/\(\(\(poke:\d+\)\)\)/g, "")
|
|
428
|
+
.replace(/\[reply:-?\d+\]/g, "")
|
|
429
|
+
.replace(/\[poke:\d+\]/g, "")
|
|
446
430
|
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
447
431
|
.trim();
|
|
448
432
|
if (cleaned) {
|
|
@@ -534,7 +518,7 @@ function expandOutgoingLines(text: string): string[] {
|
|
|
534
518
|
|
|
535
519
|
function normalizeActionLineBreaks(text: string): string {
|
|
536
520
|
return String(text || "").replace(
|
|
537
|
-
/\\\s*(?=(?:\[meme:[^\]]+\]|\[
|
|
521
|
+
/\\\s*(?=(?:\[meme:[^\]]+\]|\[emotion:[^\]]+\]|\[audio:[^\]]+\]|\[reply:-?\d+\]))/gi,
|
|
538
522
|
"\n",
|
|
539
523
|
);
|
|
540
524
|
}
|
package/core/chat-engine.ts
CHANGED
|
@@ -72,12 +72,19 @@ export async function runChat(
|
|
|
72
72
|
return isSkillAllowedForRole(skill, toolCtx.triggerSkillRole);
|
|
73
73
|
},
|
|
74
74
|
);
|
|
75
|
+
const emotionState = await humanize.emotionAgent.refreshIfNeeded({
|
|
76
|
+
sessionId: toolCtx.sessionId,
|
|
77
|
+
botNickname: promptCtx.botNickname,
|
|
78
|
+
chatHistory: history,
|
|
79
|
+
targetMessage,
|
|
80
|
+
});
|
|
75
81
|
const prompt = buildSystemPrompt({
|
|
76
82
|
...promptCtx,
|
|
77
83
|
triggerSkillRole: toolCtx.triggerSkillRole,
|
|
78
84
|
activeSkillsInfo: activeSkillsInfo || undefined,
|
|
79
85
|
chatHistory: history,
|
|
80
86
|
targetMessage,
|
|
87
|
+
currentEmotion: emotionState.current,
|
|
81
88
|
emojiAgent: humanize.emojiAgent,
|
|
82
89
|
skillManager,
|
|
83
90
|
sessionId: toolCtx.sessionId,
|
|
@@ -153,6 +160,7 @@ export async function runChat(
|
|
|
153
160
|
): Promise<void> => {
|
|
154
161
|
const text = cleanMarkers(segment)
|
|
155
162
|
.replace(/\[meme:[^\]]+\]/gi, "")
|
|
163
|
+
.replace(/\[emotion:[^\]]+\]/gi, "")
|
|
156
164
|
.replace(/\r/g, "")
|
|
157
165
|
.trim();
|
|
158
166
|
if (!text || text === "---") {
|
|
@@ -306,6 +314,14 @@ export async function runChat(
|
|
|
306
314
|
);
|
|
307
315
|
}
|
|
308
316
|
|
|
317
|
+
if (cleanedText.trim()) {
|
|
318
|
+
const emotionIntent = humanize.emotionAgent.parseEmotionIntent(cleanedText);
|
|
319
|
+
if (emotionIntent) {
|
|
320
|
+
humanize.emotionAgent.setEmotion(toolCtx.sessionId, emotionIntent);
|
|
321
|
+
}
|
|
322
|
+
cleanedText = humanize.emotionAgent.cleanEmotionMarkers(cleanedText).trim();
|
|
323
|
+
}
|
|
324
|
+
|
|
309
325
|
let emojiPath: string | null = null;
|
|
310
326
|
let finalText = cleanedText;
|
|
311
327
|
if (cleanedText.trim()) {
|
|
@@ -176,36 +176,35 @@ export async function analyzeImage(
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
const systemPrompt = `You are an image classification and analysis assistant.
|
|
179
|
+
const systemPrompt = `You are an image classification and analysis assistant. Analyze images and return structured information.
|
|
180
180
|
|
|
181
181
|
Instructions:
|
|
182
|
-
1. Classify
|
|
183
|
-
- "meme": A standalone, single-scene character image (anime/cartoon/VTuber etc.) used to express ONE specific emotion
|
|
184
|
-
- "image":
|
|
185
|
-
|
|
186
|
-
CRITICAL disambiguation rules — read carefully:
|
|
187
|
-
- Even if the image LOOKS like a meme at first glance (has characters, has text bubbles, has cute style), if it is actually a SCREENSHOT of a chat interface / messaging app / social app / browser / system UI, you MUST classify it as "image", NOT "meme". Telltale signs: status bar at top, navigation bar, input box, send button, message bubbles on both sides, timestamps, avatar lists, scroll bars, app headers/footers, system widgets.
|
|
188
|
-
- If the image is a screenshot where anime/cartoon characters appear (e.g. screenshot of a chat where someone sent a meme), the WHOLE image is still "image" — the screenshot context overrides the inner character content.
|
|
189
|
-
- If the image is a real-person photo, a selfie, a landscape photo, a real-world scene, an illustration that is NOT used as a reaction sticker (e.g. cover art, full-body standing pose with no expressive intent) — classify as "image".
|
|
190
|
-
- Only when the image is a single, clean, isolated character/scene intended to express ONE reaction (no surrounding UI, no real-world photo context, no chat interface) should you classify it as "meme".
|
|
191
|
-
- When in doubt, prefer "image" over "meme".
|
|
182
|
+
1. Classify as "meme" or "image":
|
|
183
|
+
- "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.
|
|
184
|
+
- "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.
|
|
185
|
+
- When in doubt, prefer "image".
|
|
192
186
|
|
|
193
187
|
2. Provide a brief description (max 30 words in Chinese):
|
|
194
|
-
- For memes:
|
|
195
|
-
- For images: Describe what you see factually,
|
|
196
|
-
${imageUrls.length > 1 ? "\n -
|
|
188
|
+
- For memes: Used as the saved filename. Describe the character's pose / expression / action and any visible text.
|
|
189
|
+
- For images: Describe what you see factually, including any visible text.
|
|
190
|
+
${imageUrls.length > 1 ? "\n - You are viewing multiple frames from an animated image (GIF). Consider the overall motion and emotion across all frames." : ""}
|
|
197
191
|
|
|
198
192
|
3. For memes only:
|
|
199
|
-
- Emotion tag: ${EMOTION_TAGS.join(", ")} (choose ONLY ONE
|
|
200
|
-
-
|
|
201
|
-
|
|
193
|
+
- Emotion tag: ${EMOTION_TAGS.join(", ")} (choose ONLY ONE).
|
|
194
|
+
- Characters: STRICT identification — only name a character when you are CERTAIN of their identity.
|
|
195
|
+
• If you know the character's name, add their English identifier to "characters".
|
|
196
|
+
• If you are not sure, or you do not actually know the character's name, put "unknown" in the array. Do NOT guess.
|
|
197
|
+
• 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.
|
|
198
|
+
• DO NOT name a character just because they look similar to a known one.
|
|
199
|
+
• Only name a character you can confidently identify. Otherwise use "unknown".
|
|
200
|
+
- 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".
|
|
202
201
|
|
|
203
202
|
Response format (JSON):
|
|
204
203
|
{
|
|
205
204
|
"type": "meme" or "image",
|
|
206
|
-
"description": "brief description in Chinese (for memes:
|
|
207
|
-
"emotion": "emotion tag (
|
|
208
|
-
"characters": ["
|
|
205
|
+
"description": "brief description in Chinese (for memes: describe what you see; do not invent a character name)",
|
|
206
|
+
"emotion": "emotion tag (ONLY for memes)",
|
|
207
|
+
"characters": ["known_character_identifier", "unknown", ...] (ONLY for memes; use only identifiers from the known list, or "unknown")
|
|
209
208
|
}`;
|
|
210
209
|
|
|
211
210
|
const userPrompt =
|
package/core/prompt.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
2
2
|
import { logger } from "mioki";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
SkillPermissionRole,
|
|
5
|
+
AIService,
|
|
6
|
+
ChatRuntimePromptInjection,
|
|
7
|
+
} from "mioku";
|
|
8
|
+
import { pickReplyStyle } from "../humanize";
|
|
5
9
|
import type { EmojiAgent } from "../humanize";
|
|
6
10
|
import { filterAllowedExternalSkills } from "./external-skills";
|
|
7
11
|
import type { SkillSessionManager } from "../manage/skill-session";
|
|
@@ -22,6 +26,7 @@ export interface PromptContext {
|
|
|
22
26
|
activeSkillsInfo?: string;
|
|
23
27
|
chatHistory: ChatMessage[];
|
|
24
28
|
targetMessage: TargetMessage;
|
|
29
|
+
currentEmotion?: string;
|
|
25
30
|
plannerThoughts?: string;
|
|
26
31
|
// Reply context - tells AI what type of reply this is
|
|
27
32
|
replyContext?: {
|
|
@@ -71,20 +76,28 @@ export function buildSystemPrompt(ctx: PromptContext): string {
|
|
|
71
76
|
|
|
72
77
|
// 2. Expression Habits
|
|
73
78
|
if (ctx.expressionContext) {
|
|
74
|
-
logger.info(
|
|
79
|
+
logger.info(
|
|
80
|
+
`[buildSystemPrompt] Adding expressionContext (${ctx.expressionContext.length} chars) for user`,
|
|
81
|
+
);
|
|
75
82
|
sections.push(ctx.expressionContext);
|
|
76
83
|
} else {
|
|
77
|
-
logger.info(
|
|
84
|
+
logger.info(
|
|
85
|
+
`[buildSystemPrompt] No expressionContext for session ${ctx.sessionId}`,
|
|
86
|
+
);
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
// 3. Memory Retrieval Results
|
|
81
90
|
if (ctx.memoryContext) {
|
|
82
|
-
logger.info(
|
|
91
|
+
logger.info(
|
|
92
|
+
`[buildSystemPrompt] Adding memoryContext (${ctx.memoryContext.length} chars)`,
|
|
93
|
+
);
|
|
83
94
|
sections.push(
|
|
84
95
|
`## Memory Retrieval Results\nRelevant context retrieved from conversation history:\n${ctx.memoryContext}`,
|
|
85
96
|
);
|
|
86
97
|
} else {
|
|
87
|
-
logger.info(
|
|
98
|
+
logger.info(
|
|
99
|
+
`[buildSystemPrompt] No memoryContext for session ${ctx.sessionId}`,
|
|
100
|
+
);
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
// 4. Background Topics Outside Visible History
|
|
@@ -127,10 +140,13 @@ export function buildSystemPrompt(ctx: PromptContext): string {
|
|
|
127
140
|
// 11. Persona
|
|
128
141
|
sections.push(buildPersonaSection(ctx));
|
|
129
142
|
|
|
130
|
-
// 12.
|
|
143
|
+
// 12. Emotion
|
|
144
|
+
sections.push(buildEmotionSection(ctx));
|
|
145
|
+
|
|
146
|
+
// 13. Reply Style + Behavior + Self-Protection
|
|
131
147
|
sections.push(buildReplyStyleSection(ctx, lengthStrength));
|
|
132
148
|
|
|
133
|
-
//
|
|
149
|
+
// 14. Available Tools & Response Format
|
|
134
150
|
sections.push(
|
|
135
151
|
buildResponseFormatSection(
|
|
136
152
|
ctx,
|
|
@@ -480,14 +496,61 @@ function buildPersonaSection(ctx: PromptContext): string {
|
|
|
480
496
|
lines.push(ctx.config.persona);
|
|
481
497
|
}
|
|
482
498
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
499
|
+
return lines.join("\n");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function buildEmotionSection(ctx: PromptContext): string {
|
|
503
|
+
const emotions = ctx.config.emotion?.emotions || {};
|
|
504
|
+
const defaultEmotionCandidate =
|
|
505
|
+
normalizeEmotionName(ctx.config.emotion?.defaultEmotion) || "default";
|
|
506
|
+
const availableEmotions = Array.from(
|
|
507
|
+
new Set(["default", ...Object.keys(emotions).map(normalizeEmotionName)]),
|
|
508
|
+
).filter(Boolean);
|
|
509
|
+
const defaultEmotion = availableEmotions.includes(defaultEmotionCandidate)
|
|
510
|
+
? defaultEmotionCandidate
|
|
511
|
+
: "default";
|
|
512
|
+
const currentEmotion =
|
|
513
|
+
normalizeEmotionName(ctx.currentEmotion) || defaultEmotion;
|
|
514
|
+
const currentExamples = normalizeEmotionExamples(
|
|
515
|
+
emotions[currentEmotion]?.examples,
|
|
516
|
+
);
|
|
517
|
+
const fallbackExamples = normalizeEmotionExamples(
|
|
518
|
+
emotions[defaultEmotion]?.examples,
|
|
519
|
+
);
|
|
520
|
+
const examples =
|
|
521
|
+
currentExamples.length > 0 ? currentExamples : fallbackExamples;
|
|
522
|
+
const lines = [`## Emotion State`, `Current emotion: ${currentEmotion}`];
|
|
523
|
+
|
|
524
|
+
if (availableEmotions.length > 0) {
|
|
525
|
+
lines.push(`Available emotions: ${availableEmotions.join(", ")}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
lines.push(
|
|
529
|
+
`You may switch your emotion state by writing [emotion:emotion_name]. The marker is not sent to the chat. Only use available emotions.`,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (examples.length > 0) {
|
|
533
|
+
lines.push(
|
|
534
|
+
`For examples of responses to current emotions, please refer to their tone and speech characteristics.`,
|
|
535
|
+
`Be sure to imitate their tone and speaking characteristics, including sentence length, pauses within sentences, and the use of punctuation`,
|
|
536
|
+
examples.map((example) => `- ${example}`).join("\n"),
|
|
537
|
+
);
|
|
486
538
|
}
|
|
487
539
|
|
|
488
540
|
return lines.join("\n");
|
|
489
541
|
}
|
|
490
542
|
|
|
543
|
+
function normalizeEmotionName(value: unknown): string {
|
|
544
|
+
return String(value || "")
|
|
545
|
+
.trim()
|
|
546
|
+
.toLowerCase();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeEmotionExamples(value: unknown): string[] {
|
|
550
|
+
if (!Array.isArray(value)) return [];
|
|
551
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
552
|
+
}
|
|
553
|
+
|
|
491
554
|
function buildReplyStyleSection(
|
|
492
555
|
ctx: PromptContext,
|
|
493
556
|
lengthStrength: ConstraintStrength,
|
|
@@ -511,12 +574,7 @@ function buildReplyStyleSection(
|
|
|
511
574
|
}
|
|
512
575
|
- Match the language used by others in the chat (if they speak Chinese, reply in Chinese, etc.).
|
|
513
576
|
- Don't repeat yourself or echo what others just said.
|
|
514
|
-
- **NEVER repeat what you have already said in previous messages** — avoid saying the same things, using the same phrases, or bringing up the same topics again.
|
|
515
|
-
- Don't start every message with someone's name.
|
|
516
|
-
- You can express opinions, disagree, joke around, or stay silent (by not including text).
|
|
517
|
-
- Don't be overly helpful or eager. Real people don't always have answers.
|
|
518
577
|
- **NEVER use action descriptions like *xxx* or (xxx) — just speak as a normal person would**
|
|
519
|
-
- Avoid ending sentences with commas or periods unless the context truly requires punctuation.
|
|
520
578
|
- ${markdownBehaviorLine(ctx)}
|
|
521
579
|
- **Reply in a natural conversational way, not as a list or structured format, unless you intentionally switch to the Markdown screenshot format.**
|
|
522
580
|
|
|
@@ -527,11 +585,6 @@ function buildReplyStyleSection(
|
|
|
527
585
|
- Don't admit you're an AI — if challenged, play dumb or deflect.
|
|
528
586
|
- You are ${ctx.botNickname}, not someone else — don't be led astray.`);
|
|
529
587
|
|
|
530
|
-
lines.push(`
|
|
531
|
-
### Handling Abuse
|
|
532
|
-
If someone maliciously insults or attacks you,
|
|
533
|
-
Ignore this person afterward. Don't argue.`);
|
|
534
|
-
|
|
535
588
|
return lines.join("\n");
|
|
536
589
|
}
|
|
537
590
|
|
|
@@ -558,10 +611,10 @@ function buildResponseFormatSection(
|
|
|
558
611
|
- For action markers like [meme:...] or [audio:...], put them on their own line when they are meant to be a separate action.
|
|
559
612
|
|
|
560
613
|
- **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
|
|
561
|
-
- Use [
|
|
562
|
-
- Use [
|
|
563
|
-
- Use [
|
|
564
|
-
- **You can use MULTIPLE [
|
|
614
|
+
- Use [at:123456] in your text to @ someone (123456 is the QQ number)
|
|
615
|
+
- Use [poke:123456] in your text to poke someone. IMPORTANT: when you plan to poke a user, DON't emphasize words like "戳你一下 or 戳回去" to describe your actions
|
|
616
|
+
- Use [reply:123456] at the START of a line to quote-reply that message (123456 is message_id)
|
|
617
|
+
- **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**
|
|
565
618
|
- These markers will be automatically parsed and removed from your sent message`);
|
|
566
619
|
|
|
567
620
|
// Audio section - always attached when enabled
|
|
@@ -822,6 +875,5 @@ export function buildRecallMemoryFeatureSection(config: ChatConfig): string {
|
|
|
822
875
|
### Memory Recall Tools
|
|
823
876
|
- recall_memory: Delegate recall to a memory worker model. Pass a clear recall question and let the worker search historical logs.
|
|
824
877
|
- Use recall_memory ONLY when there is explicit need to recall past content and required information is clearly missing from current context.
|
|
825
|
-
- Do NOT call recall_memory for every question.
|
|
826
878
|
- The worker returns historical logs with timestamps; treat them as past records, not newly sent messages.`;
|
|
827
879
|
}
|
package/core/web/web-reader.ts
CHANGED
|
@@ -354,13 +354,13 @@ function parseCharset(contentType: string | null): string | null {
|
|
|
354
354
|
return match?.[1]?.trim() || null;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
function createTextDecoder(charset: string | null) {
|
|
357
|
+
function createTextDecoder(charset: string | null): TextDecoder {
|
|
358
358
|
if (!charset) {
|
|
359
359
|
return new TextDecoder("utf-8");
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
try {
|
|
363
|
-
return new TextDecoder(charset);
|
|
363
|
+
return new TextDecoder(charset as ConstructorParameters<typeof TextDecoder>[0]);
|
|
364
364
|
} catch {
|
|
365
365
|
return new TextDecoder("utf-8");
|
|
366
366
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { AIInstance } from "mioku";
|
|
2
|
+
import { logger } from "mioki";
|
|
3
|
+
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface EmotionState {
|
|
6
|
+
current: string;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EmotionAnalysisInput {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
botNickname: string;
|
|
13
|
+
chatHistory: ChatMessage[];
|
|
14
|
+
targetMessage: TargetMessage;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class EmotionAgent {
|
|
19
|
+
private readonly ai: AIInstance;
|
|
20
|
+
private readonly config: ChatConfig;
|
|
21
|
+
private readonly states = new Map<string, EmotionState>();
|
|
22
|
+
|
|
23
|
+
constructor(ai: AIInstance, config: ChatConfig) {
|
|
24
|
+
this.ai = ai;
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getCurrent(sessionId: string): EmotionState {
|
|
29
|
+
const existing = this.states.get(sessionId);
|
|
30
|
+
if (existing) return existing;
|
|
31
|
+
|
|
32
|
+
const current = this.getDefaultEmotion();
|
|
33
|
+
const state = { current, updatedAt: 0 };
|
|
34
|
+
this.states.set(sessionId, state);
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getAvailableEmotions(): string[] {
|
|
39
|
+
const emotions = Object.keys(this.config.emotion?.emotions || {})
|
|
40
|
+
.map((name) => name.trim().toLowerCase())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
return Array.from(new Set(["default", ...emotions]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getReferenceExamples(emotion: string): string[] {
|
|
46
|
+
const normalized = this.normalizeEmotionName(emotion);
|
|
47
|
+
const emotions = this.config.emotion?.emotions || {};
|
|
48
|
+
const examples = this.normalizeExamples(emotions[normalized]?.examples);
|
|
49
|
+
if (examples.length > 0) return examples;
|
|
50
|
+
return this.normalizeExamples(emotions[this.getDefaultEmotion()]?.examples);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setEmotion(sessionId: string, emotion: string): EmotionState {
|
|
54
|
+
const current = this.resolveEmotion(emotion);
|
|
55
|
+
const state = { current, updatedAt: Date.now() };
|
|
56
|
+
this.states.set(sessionId, state);
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parseEmotionIntent(text: string): string | null {
|
|
61
|
+
const match = String(text || "").match(/\[emotion:([^\]]+)]/i);
|
|
62
|
+
if (!match) return null;
|
|
63
|
+
return this.normalizeEmotionName(match[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cleanEmotionMarkers(text: string): string {
|
|
67
|
+
return String(text || "").replace(/\[emotion:[^\]]+]/gi, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async refreshIfNeeded(input: EmotionAnalysisInput): Promise<EmotionState> {
|
|
71
|
+
const current = this.getCurrent(input.sessionId);
|
|
72
|
+
const intervalMs = Number(this.config.emotion?.updateIntervalMs ?? 60 * 60_000);
|
|
73
|
+
const shouldRefresh =
|
|
74
|
+
Boolean(input.force) || current.updatedAt <= 0 || Date.now() - current.updatedAt >= intervalMs;
|
|
75
|
+
|
|
76
|
+
if (!shouldRefresh) return current;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const nextEmotion = await this.analyzeEmotion(input);
|
|
80
|
+
return this.setEmotion(input.sessionId, nextEmotion);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.warn(`[emotion-agent] emotion analysis failed: ${err}`);
|
|
83
|
+
if (current.updatedAt <= 0) {
|
|
84
|
+
return this.setEmotion(input.sessionId, this.getDefaultEmotion());
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async analyzeEmotion(input: EmotionAnalysisInput): Promise<string> {
|
|
91
|
+
const availableEmotions = this.getAvailableEmotions();
|
|
92
|
+
const model = this.config.workingModel || this.config.model;
|
|
93
|
+
const systemPrompt = `You are an emotion state selector for a chat bot.
|
|
94
|
+
|
|
95
|
+
Task:
|
|
96
|
+
- Read the recent chat context and the target user message.
|
|
97
|
+
- Choose exactly one current emotion state for ${input.botNickname}.
|
|
98
|
+
- Only choose from the available emotion names.
|
|
99
|
+
- Do not decide how the bot should reply.
|
|
100
|
+
- Do not mention tools, actions, or response strategy.
|
|
101
|
+
- Return JSON only.
|
|
102
|
+
|
|
103
|
+
Available emotions: ${availableEmotions.join(", ")}
|
|
104
|
+
|
|
105
|
+
Response format:
|
|
106
|
+
{"emotion":"one_available_emotion","reason":"brief context-only reason"}`;
|
|
107
|
+
|
|
108
|
+
const historyText = input.chatHistory
|
|
109
|
+
.slice(-30)
|
|
110
|
+
.map((msg) => {
|
|
111
|
+
const role = msg.role === "assistant" ? input.botNickname : msg.userName || "User";
|
|
112
|
+
const time = new Date(msg.timestamp);
|
|
113
|
+
const timeStr = `${String(time.getMonth() + 1).padStart(2, "0")}-${String(time.getDate()).padStart(2, "0")} ${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
|
|
114
|
+
return `[${timeStr}] ${role}: ${msg.content}`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
117
|
+
|
|
118
|
+
const userPrompt = `Recent chat context:
|
|
119
|
+
${historyText || "(No recent messages)"}
|
|
120
|
+
|
|
121
|
+
Target user message:
|
|
122
|
+
${input.targetMessage.userName}: ${input.targetMessage.content}`;
|
|
123
|
+
|
|
124
|
+
const response = await this.ai.complete({
|
|
125
|
+
model,
|
|
126
|
+
messages: [
|
|
127
|
+
{ role: "system", content: systemPrompt },
|
|
128
|
+
{ role: "user", content: userPrompt },
|
|
129
|
+
],
|
|
130
|
+
temperature: 0.2,
|
|
131
|
+
max_tokens: 160,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const content = response.content || "";
|
|
135
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
136
|
+
if (!jsonMatch) return this.getDefaultEmotion();
|
|
137
|
+
|
|
138
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
139
|
+
return this.resolveEmotion(parsed?.emotion);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private resolveEmotion(emotion: unknown): string {
|
|
143
|
+
const normalized = this.normalizeEmotionName(emotion);
|
|
144
|
+
const available = this.getAvailableEmotions();
|
|
145
|
+
if (available.includes(normalized)) return normalized;
|
|
146
|
+
return this.getDefaultEmotion();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private getDefaultEmotion(): string {
|
|
150
|
+
const configured = this.normalizeEmotionName(this.config.emotion?.defaultEmotion);
|
|
151
|
+
const available = this.getAvailableEmotions();
|
|
152
|
+
return available.includes(configured) ? configured : "default";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private normalizeEmotionName(emotion: unknown): string {
|
|
156
|
+
return String(emotion || "").trim().toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private normalizeExamples(examples: unknown): string[] {
|
|
160
|
+
if (!Array.isArray(examples)) return [];
|
|
161
|
+
return examples.map((item) => String(item || "").trim()).filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/humanize/index.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export { EmojiAgent } from "./emoji-agent";
|
|
2
|
+
export { EmotionAgent } from "./emotion-agent";
|
|
2
3
|
export { MemoryRetrieval } from "./memory";
|
|
3
4
|
export { TopicTracker } from "./topic";
|
|
4
5
|
export { ActionPlanner } from "./planner";
|
|
5
6
|
export { ExpressionLearner } from "./expression";
|
|
6
|
-
export { pickReplyStyle
|
|
7
|
+
export { pickReplyStyle } from "./utils";
|
|
7
8
|
|
|
8
9
|
import type { AIInstance } from "mioku";
|
|
9
10
|
import type { ChatDatabase } from "../db";
|
|
10
11
|
import type { ChatConfig } from "../types";
|
|
12
|
+
import { EmotionAgent } from "./emotion-agent";
|
|
11
13
|
import { EmojiAgent } from "./emoji-agent";
|
|
12
14
|
import { MemoryRetrieval } from "./memory";
|
|
13
15
|
import { TopicTracker } from "./topic";
|
|
@@ -18,6 +20,7 @@ export class HumanizeEngine {
|
|
|
18
20
|
readonly memoryRetrieval: MemoryRetrieval;
|
|
19
21
|
readonly topicTracker: TopicTracker;
|
|
20
22
|
readonly actionPlanner: ActionPlanner;
|
|
23
|
+
readonly emotionAgent: EmotionAgent;
|
|
21
24
|
readonly emojiAgent: EmojiAgent;
|
|
22
25
|
readonly expressionLearner: ExpressionLearner;
|
|
23
26
|
|
|
@@ -30,6 +33,7 @@ export class HumanizeEngine {
|
|
|
30
33
|
this.memoryRetrieval = new MemoryRetrieval(workAI, config, db);
|
|
31
34
|
this.topicTracker = new TopicTracker(workAI, config, db);
|
|
32
35
|
this.actionPlanner = new ActionPlanner(workAI, config);
|
|
36
|
+
this.emotionAgent = new EmotionAgent(workAI, config);
|
|
33
37
|
this.emojiAgent = new EmojiAgent(workAI, config, db);
|
|
34
38
|
this.expressionLearner = new ExpressionLearner(workAI, config, db);
|
|
35
39
|
}
|
package/humanize/planner.ts
CHANGED
|
@@ -243,7 +243,7 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
243
243
|
return result;
|
|
244
244
|
} catch (err) {
|
|
245
245
|
logger.error(`[ActionPlanner] Error: ${err}`);
|
|
246
|
-
return { action: "
|
|
246
|
+
return { action: "wait", reason: "error fallback", waitMs: 60_000 };
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
}
|
|
249
|
+
}
|
package/humanize/utils.ts
CHANGED
|
@@ -13,16 +13,3 @@ export function pickReplyStyle(config: ChatConfig): string {
|
|
|
13
13
|
}
|
|
14
14
|
return base;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
export function pickPersonalityState(config: ChatConfig): string | null {
|
|
18
|
-
const { personality } = config;
|
|
19
|
-
if (!personality) return null;
|
|
20
|
-
|
|
21
|
-
const states = personality.states || [];
|
|
22
|
-
const prob = personality.stateProbability ?? 0;
|
|
23
|
-
|
|
24
|
-
if (states.length > 0 && prob > 0 && Math.random() < prob) {
|
|
25
|
-
return states[Math.floor(Math.random() * states.length)];
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
package/index.ts
CHANGED
|
@@ -1599,7 +1599,7 @@ async function handleIdleCheckDebug(
|
|
|
1599
1599
|
botRole: toolCtx.botRole,
|
|
1600
1600
|
aiService: ctx.services!.ai as AIService,
|
|
1601
1601
|
isGroup: true,
|
|
1602
|
-
plannerThoughts: `You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [
|
|
1602
|
+
plannerThoughts: `You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [reply:message ID] format).\nDon't mention your intentions like "I'm here to answer".`,
|
|
1603
1603
|
replyContext: { type: "idle" },
|
|
1604
1604
|
},
|
|
1605
1605
|
humanize,
|
package/manage/idle-check.ts
CHANGED
|
@@ -157,7 +157,7 @@ export class IdleCheckManager {
|
|
|
157
157
|
const result = await this.runWithRateLimitGuard(
|
|
158
158
|
() => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
|
|
159
159
|
config: cfg, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
|
|
160
|
-
plannerThoughts: "You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [
|
|
160
|
+
plannerThoughts: "You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [reply:message ID] format).\nDon't mention your intentions like \"I'm here to answer\".",
|
|
161
161
|
replyContext: { type: "idle" },
|
|
162
162
|
}, this.humanize, this.skillManager),
|
|
163
163
|
{ groupId, label: "idle-check" },
|
package/manage/types.ts
CHANGED
|
@@ -106,6 +106,7 @@ export type PromptCtxForRunChat = {
|
|
|
106
106
|
topicContext?: string;
|
|
107
107
|
expressionContext?: string;
|
|
108
108
|
activeSkillsInfo?: string;
|
|
109
|
+
currentEmotion?: string;
|
|
109
110
|
plannerThoughts?: string;
|
|
110
111
|
replyContext?: {
|
|
111
112
|
type: "reply" | "comment" | "idle" | "review" | "poked";
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -3,14 +3,14 @@ import type { MiokiContext } from "mioki";
|
|
|
3
3
|
import type { AITool, AIService, SkillPermissionRole } from "mioku";
|
|
4
4
|
import type { ChatDatabase } from "./db";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
export interface EmotionEntryConfig {
|
|
7
|
+
examples: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EmotionConfig {
|
|
11
|
+
defaultEmotion: string;
|
|
12
|
+
updateIntervalMs: number;
|
|
13
|
+
emotions: Record<string, EmotionEntryConfig>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -208,7 +208,7 @@ export interface ChatConfig {
|
|
|
208
208
|
cooldownAfterReplyMs: number;
|
|
209
209
|
aiRequestLimits: AIRequestLimitConfig;
|
|
210
210
|
dynamicDelay: DynamicDelayConfig;
|
|
211
|
-
|
|
211
|
+
emotion: EmotionConfig;
|
|
212
212
|
replyStyle: ReplyStyleConfig;
|
|
213
213
|
memory: MemoryConfig;
|
|
214
214
|
topic: TopicConfig;
|
package/utils/queue.ts
CHANGED
|
@@ -92,12 +92,10 @@ export class MessageQueueManager {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* 将包含多个 reply 标记的单行文本拆分为多行
|
|
95
|
-
* 例如 "[
|
|
95
|
+
* 例如 "[reply:1]文字A[reply:2]文字B" → ["[reply:1]文字A", "[reply:2]文字B"]
|
|
96
96
|
*/
|
|
97
97
|
export function splitByReplyMarkers(line: string): string[] {
|
|
98
|
-
const parts = line.split(
|
|
99
|
-
/(?=\[\[\[reply:-?\d+\]\]\]|\(\(\(reply:-?\d+\)\)\))/,
|
|
100
|
-
);
|
|
98
|
+
const parts = line.split(/(?=\[reply:-?\d+\])/);
|
|
101
99
|
return parts.filter((p) => p.trim());
|
|
102
100
|
}
|
|
103
101
|
|
|
@@ -122,11 +120,7 @@ export function parseLineMarkers(
|
|
|
122
120
|
let audioText: string | undefined;
|
|
123
121
|
|
|
124
122
|
// 提取 AT 标记
|
|
125
|
-
const atPatterns = [
|
|
126
|
-
/\[\[\[at:(\d+)\]\]\]/g,
|
|
127
|
-
/\(\(\(at:(\d+)\)\)\)/g,
|
|
128
|
-
/\(\(\((\d+)\)\)\)/g,
|
|
129
|
-
];
|
|
123
|
+
const atPatterns = [/\[at:(\d+)\]/g];
|
|
130
124
|
for (const pattern of atPatterns) {
|
|
131
125
|
const matches = [...line.matchAll(pattern)];
|
|
132
126
|
for (const match of matches) {
|
|
@@ -136,7 +130,7 @@ export function parseLineMarkers(
|
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
// 提取戳人标记
|
|
139
|
-
const pokePatterns = [/\[
|
|
133
|
+
const pokePatterns = [/\[poke:(\d+)\]/g];
|
|
140
134
|
for (const pattern of pokePatterns) {
|
|
141
135
|
const matches = [...line.matchAll(pattern)];
|
|
142
136
|
for (const match of matches) {
|
|
@@ -147,10 +141,7 @@ export function parseLineMarkers(
|
|
|
147
141
|
|
|
148
142
|
// 提取引用标记(仅在允许时)
|
|
149
143
|
if (quoteMode !== "skip") {
|
|
150
|
-
const replyPatterns = [
|
|
151
|
-
/\[\[\[reply:(-?\d+)\]\]\]/g,
|
|
152
|
-
/\(\(\(reply:(-?\d+)\)\)\)/g,
|
|
153
|
-
];
|
|
144
|
+
const replyPatterns = [/\[reply:(-?\d+)\]/g];
|
|
154
145
|
for (const pattern of replyPatterns) {
|
|
155
146
|
const matches = [...line.matchAll(pattern)];
|
|
156
147
|
for (const match of matches) {
|
|
@@ -172,13 +163,9 @@ export function parseLineMarkers(
|
|
|
172
163
|
|
|
173
164
|
// 清理标记
|
|
174
165
|
let cleanText = line
|
|
175
|
-
.replace(/\[
|
|
176
|
-
.replace(/\
|
|
177
|
-
.replace(/\
|
|
178
|
-
.replace(/\[\[\[poke:\d+\]\]\]/g, "")
|
|
179
|
-
.replace(/\(\(\(poke:\d+\)\)\)/g, "")
|
|
180
|
-
.replace(/\[\[\[reply:-?\d+\]\]\]/g, "")
|
|
181
|
-
.replace(/\(\(\(reply:-?\d+\)\)\)/g, "")
|
|
166
|
+
.replace(/\[at:\d+\]/g, "")
|
|
167
|
+
.replace(/\[poke:\d+\]/g, "")
|
|
168
|
+
.replace(/\[reply:-?\d+\]/g, "")
|
|
182
169
|
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
183
170
|
.trim();
|
|
184
171
|
|