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.
- package/configs/groups.ts +5 -0
- package/configs/personalization.ts +1 -1
- package/context.ts +7 -3
- package/core/base.ts +1 -1
- package/core/chat-engine.ts +18 -7
- package/core/chat-turn.ts +28 -10
- package/core/media/image-analyzer.ts +63 -397
- package/core/prompt.ts +11 -20
- package/core/tools/web.ts +1 -1
- package/db.ts +8 -12
- package/handlers/idle-debug.ts +96 -97
- package/handlers/message.ts +38 -37
- package/handlers/poke.ts +104 -126
- package/humanize/emoji-agent.ts +210 -321
- package/humanize/emotion-agent.ts +33 -26
- package/humanize/expression.ts +24 -16
- package/humanize/index.ts +9 -8
- package/humanize/memory.ts +10 -7
- package/humanize/planner.ts +11 -16
- package/humanize/topic.ts +22 -15
- package/index.ts +120 -45
- package/manage/cooldown.ts +83 -116
- package/manage/idle-check.ts +173 -75
- package/manage/queue-processor.ts +92 -44
- package/manage/rate-limiter.ts +56 -80
- package/manage/session-turn-scheduler.ts +130 -0
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +15 -3
- package/types.ts +25 -19
- package/utils/config.ts +57 -0
- package/utils/group-config.ts +70 -0
- package/utils/message.ts +4 -7
- package/utils/queue.ts +5 -0
package/humanize/emoji-agent.ts
CHANGED
|
@@ -1,397 +1,286 @@
|
|
|
1
|
-
import type { AIInstance } from "mioku";
|
|
2
|
-
import type { ChatDatabase } from "../db";
|
|
3
|
-
import type { ChatConfig, ChatMessage } from "../types";
|
|
4
|
-
import { logger } from "mioki";
|
|
5
|
-
import * as fs from "fs/promises";
|
|
6
1
|
import { existsSync, readdirSync } from "fs";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
7
3
|
import * as path from "path";
|
|
4
|
+
import { getPluginDataDir, type AIInstance } from "mioku";
|
|
5
|
+
import { logger } from "mioki";
|
|
6
|
+
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
7
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
8
|
+
import { extractJsonObject } from "../utils/json";
|
|
9
|
+
import type { ChatConfigProvider } from "./index";
|
|
8
10
|
|
|
9
11
|
export interface EmojiPickResult {
|
|
10
12
|
success: boolean;
|
|
11
13
|
emojiPath?: string;
|
|
12
14
|
emojiDescription?: string;
|
|
13
|
-
cleanedText
|
|
15
|
+
cleanedText: string;
|
|
14
16
|
error?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
];
|
|
19
|
+
export interface EmojiSelectionContext {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
botNickname: string;
|
|
22
|
+
chatHistory: ChatMessage[];
|
|
23
|
+
targetMessage: TargetMessage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface EmojiCandidate {
|
|
27
|
+
id: string;
|
|
28
|
+
path: string;
|
|
29
|
+
character: string;
|
|
30
|
+
label: string;
|
|
31
|
+
relativePath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp"]);
|
|
35
|
+
const STICKER_INTENT_LINE = /^\s*\[\]\s*$/;
|
|
33
36
|
|
|
34
37
|
export class EmojiAgent {
|
|
35
|
-
private
|
|
36
|
-
private config: ChatConfig;
|
|
37
|
-
private db: ChatDatabase;
|
|
38
|
-
private readonly memeBaseDir: string;
|
|
39
|
-
|
|
40
|
-
constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
|
|
41
|
-
this.ai = ai;
|
|
42
|
-
this.config = config;
|
|
43
|
-
this.db = db;
|
|
44
|
-
this.memeBaseDir = path.join(process.cwd(), "data", "chat", "meme");
|
|
45
|
-
}
|
|
38
|
+
private readonly memeBaseDir = path.join(getPluginDataDir("chat"), "meme");
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly ai: AIInstance,
|
|
42
|
+
private readonly getConfig: ChatConfigProvider,
|
|
43
|
+
) {}
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
return
|
|
45
|
+
getAvailableCharacters(): string[] {
|
|
46
|
+
if (!existsSync(this.memeBaseDir)) return [];
|
|
47
|
+
return readdirSync(this.memeBaseDir, { withFileTypes: true })
|
|
54
48
|
.filter((entry) => entry.isDirectory())
|
|
55
|
-
.map((entry) => entry.name)
|
|
49
|
+
.map((entry) => entry.name)
|
|
50
|
+
.sort((a, b) => a.localeCompare(b));
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const entries = readdirSync(characterDir, { withFileTypes: true });
|
|
65
|
-
return entries
|
|
66
|
-
.filter((entry) => entry.isDirectory())
|
|
67
|
-
.map((entry) => entry.name);
|
|
53
|
+
hasAvailableEmojis(configuredCharacters: string[] = []): boolean {
|
|
54
|
+
return this.getVisibleCharacters(configuredCharacters).some((character) =>
|
|
55
|
+
this.directoryContainsImage(path.join(this.memeBaseDir, character)),
|
|
56
|
+
);
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
hasStickerIntent(text: string): boolean {
|
|
60
|
+
return String(text || "")
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.some((line) => STICKER_INTENT_LINE.test(line));
|
|
74
63
|
}
|
|
75
64
|
|
|
76
|
-
async
|
|
65
|
+
async processStickerResponse(
|
|
77
66
|
aiResponseText: string,
|
|
78
|
-
|
|
67
|
+
context: EmojiSelectionContext,
|
|
79
68
|
): Promise<EmojiPickResult> {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
return {
|
|
83
|
-
success: false,
|
|
84
|
-
error: "No meme intent found in response",
|
|
85
|
-
};
|
|
69
|
+
const cleanedText = this.cleanStickerIntent(aiResponseText);
|
|
70
|
+
if (!this.hasStickerIntent(aiResponseText)) {
|
|
71
|
+
return { success: false, cleanedText, error: "No sticker intent found" };
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const configChars = this.config.emoji?.characters || [];
|
|
92
|
-
|
|
93
|
-
// 决定使用哪些角色
|
|
94
|
-
let targetCharacters: string[];
|
|
95
|
-
if (configChars.length > 0) {
|
|
96
|
-
targetCharacters = configChars;
|
|
97
|
-
logger.debug(
|
|
98
|
-
`[emoji-agent] Using configured characters: ${configChars.join(", ")}`,
|
|
99
|
-
);
|
|
100
|
-
} else {
|
|
101
|
-
targetCharacters = this.getAvailableCharacters();
|
|
102
|
-
logger.debug(
|
|
103
|
-
`[emoji-agent] Using all available characters: ${targetCharacters.join(", ")}`,
|
|
104
|
-
);
|
|
74
|
+
const cfg = this.getConfig(extractGroupIdFromSession(context.sessionId));
|
|
75
|
+
if (!cfg.emoji?.enabled) {
|
|
76
|
+
return { success: false, cleanedText, error: "Emoji disabled" };
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const emojiResult = await this.pickEmoji(
|
|
112
|
-
targetCharacters,
|
|
113
|
-
intent.emotion,
|
|
114
|
-
chatHistory,
|
|
79
|
+
const characters = this.getVisibleCharacters(cfg.emoji.characters || []);
|
|
80
|
+
const candidates = this.filterSelectedStickers(
|
|
81
|
+
await this.collectCandidates(characters),
|
|
82
|
+
cfg.emoji.stickers || [],
|
|
115
83
|
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
error: emojiResult.error || "Failed to pick emoji",
|
|
121
|
-
};
|
|
84
|
+
if (candidates.length === 0) {
|
|
85
|
+
return { success: false, cleanedText, error: "No stickers available" };
|
|
122
86
|
}
|
|
123
87
|
|
|
124
|
-
const
|
|
88
|
+
const selected =
|
|
89
|
+
candidates.length === 1
|
|
90
|
+
? candidates[0]
|
|
91
|
+
: await this.selectByAI(candidates, context, cleanedText, cfg);
|
|
92
|
+
if (!selected) {
|
|
93
|
+
return { success: false, cleanedText, error: "Sticker selection failed" };
|
|
94
|
+
}
|
|
125
95
|
|
|
126
96
|
return {
|
|
127
97
|
success: true,
|
|
128
|
-
emojiPath:
|
|
129
|
-
emojiDescription:
|
|
98
|
+
emojiPath: selected.path,
|
|
99
|
+
emojiDescription: selected.label,
|
|
130
100
|
cleanedText,
|
|
131
101
|
};
|
|
132
102
|
}
|
|
133
103
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
chatHistory: ChatMessage[],
|
|
138
|
-
): Promise<{
|
|
139
|
-
success: boolean;
|
|
140
|
-
emojiPath?: string;
|
|
141
|
-
description?: string;
|
|
142
|
-
error?: string;
|
|
143
|
-
}> {
|
|
144
|
-
try {
|
|
145
|
-
const normalizedEmotion = this.normalizeEmotion(emotion);
|
|
146
|
-
logger.debug(
|
|
147
|
-
`[emoji-agent] pickEmoji: emotion=${normalizedEmotion}, characters=${characters.join(",")}`,
|
|
148
|
-
);
|
|
104
|
+
private getVisibleCharacters(configuredCharacters: string[]): string[] {
|
|
105
|
+
const available = this.getAvailableCharacters();
|
|
106
|
+
if (configuredCharacters.length === 0) return available;
|
|
149
107
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
normalizedEmotion,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
logger.debug(
|
|
161
|
-
`[emoji-agent] Checking dir: ${emotionDir}, exists=${existsSync(emotionDir)}`,
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
if (existsSync(emotionDir)) {
|
|
165
|
-
const files = (await fs.readdir(emotionDir)).filter((f) => {
|
|
166
|
-
const ext = path.extname(f).toLowerCase();
|
|
167
|
-
return [".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(ext);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
logger.debug(
|
|
171
|
-
`[emoji-agent] Found ${files.length} files in ${character}/${normalizedEmotion}`,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
for (const file of files) {
|
|
175
|
-
allEmojis.push({
|
|
176
|
-
path: path.join(emotionDir, file),
|
|
177
|
-
character,
|
|
178
|
-
file,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
logger.debug(`[emoji-agent] Total emojis collected: ${allEmojis.length}`);
|
|
185
|
-
|
|
186
|
-
// 如果没有找到对应情绪的表情包,尝试 default
|
|
187
|
-
if (allEmojis.length === 0) {
|
|
188
|
-
logger.info(
|
|
189
|
-
`[emoji-agent] No emojis found for ${normalizedEmotion}, trying default`,
|
|
190
|
-
);
|
|
191
|
-
for (const character of characters) {
|
|
192
|
-
const defaultDir = path.join(this.memeBaseDir, character, "default");
|
|
193
|
-
if (existsSync(defaultDir)) {
|
|
194
|
-
const files = (await fs.readdir(defaultDir)).filter((f) => {
|
|
195
|
-
const ext = path.extname(f).toLowerCase();
|
|
196
|
-
return [".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(ext);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
for (const file of files) {
|
|
200
|
-
allEmojis.push({
|
|
201
|
-
path: path.join(defaultDir, file),
|
|
202
|
-
character,
|
|
203
|
-
file,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
logger.debug(
|
|
209
|
-
`[emoji-agent] Total emojis from default: ${allEmojis.length}`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
108
|
+
const configured = new Set(
|
|
109
|
+
configuredCharacters.map((character) => character.trim().toLowerCase()),
|
|
110
|
+
);
|
|
111
|
+
return available.filter((character) =>
|
|
112
|
+
configured.has(character.toLowerCase()),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
212
115
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
116
|
+
private directoryContainsImage(dir: string): boolean {
|
|
117
|
+
if (!existsSync(dir)) return false;
|
|
118
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
const entryPath = path.join(dir, entry.name);
|
|
120
|
+
if (
|
|
121
|
+
entry.isFile() &&
|
|
122
|
+
IMAGE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())
|
|
123
|
+
) {
|
|
124
|
+
return true;
|
|
218
125
|
}
|
|
126
|
+
if (entry.isDirectory() && this.directoryContainsImage(entryPath))
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
219
131
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
132
|
+
private async collectCandidates(
|
|
133
|
+
characters: string[],
|
|
134
|
+
): Promise<EmojiCandidate[]> {
|
|
135
|
+
const candidates: EmojiCandidate[] = [];
|
|
136
|
+
for (const character of characters) {
|
|
137
|
+
const characterDir = path.join(this.memeBaseDir, character);
|
|
138
|
+
const files = await this.collectImageFiles(characterDir);
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const relativePath = path.relative(characterDir, file);
|
|
141
|
+
const fileName = path.basename(file, path.extname(file));
|
|
142
|
+
candidates.push({
|
|
143
|
+
id: this.normalizeStickerId(path.join(character, relativePath)),
|
|
144
|
+
path: file,
|
|
145
|
+
character,
|
|
146
|
+
label: fileName.replace(/[_-]+/g, " ").trim(),
|
|
147
|
+
relativePath,
|
|
148
|
+
});
|
|
228
149
|
}
|
|
150
|
+
}
|
|
151
|
+
return candidates.sort((a, b) =>
|
|
152
|
+
`${a.character}/${a.relativePath}`.localeCompare(
|
|
153
|
+
`${b.character}/${b.relativePath}`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
229
157
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
158
|
+
private async collectImageFiles(dir: string): Promise<string[]> {
|
|
159
|
+
if (!existsSync(dir)) return [];
|
|
160
|
+
const files: string[] = [];
|
|
161
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
162
|
+
const entryPath = path.join(dir, entry.name);
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
files.push(...(await this.collectImageFiles(entryPath)));
|
|
165
|
+
} else if (
|
|
166
|
+
entry.isFile() &&
|
|
167
|
+
IMAGE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())
|
|
168
|
+
) {
|
|
169
|
+
files.push(entryPath);
|
|
236
170
|
}
|
|
237
|
-
|
|
238
|
-
// 使用 AI 选择最合适的表情包
|
|
239
|
-
return this.selectByAI(allEmojis, normalizedEmotion, chatHistory);
|
|
240
|
-
} catch (err) {
|
|
241
|
-
logger.error(`[emoji-agent] Failed to pick emoji: ${err}`);
|
|
242
|
-
return {
|
|
243
|
-
success: false,
|
|
244
|
-
error: String(err),
|
|
245
|
-
};
|
|
246
171
|
}
|
|
172
|
+
return files;
|
|
247
173
|
}
|
|
248
174
|
|
|
249
|
-
private
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
有趣: "funny",
|
|
266
|
-
可爱: "cute",
|
|
267
|
-
爱: "love",
|
|
268
|
-
中性: "neutral",
|
|
269
|
-
};
|
|
270
|
-
return mapping[normalized] || "default";
|
|
175
|
+
private filterSelectedStickers(
|
|
176
|
+
candidates: EmojiCandidate[],
|
|
177
|
+
selectedStickers: string[],
|
|
178
|
+
): EmojiCandidate[] {
|
|
179
|
+
if (selectedStickers.length === 0) return candidates;
|
|
180
|
+
const selected = new Set(
|
|
181
|
+
selectedStickers.map((sticker) => this.normalizeStickerId(sticker)),
|
|
182
|
+
);
|
|
183
|
+
return candidates.filter((candidate) => selected.has(candidate.id));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private normalizeStickerId(value: string): string {
|
|
187
|
+
return String(value || "")
|
|
188
|
+
.replace(/\\/g, "/")
|
|
189
|
+
.replace(/^\/+/, "")
|
|
190
|
+
.replace(/^(?:data\/chat\/)?meme\//i, "");
|
|
271
191
|
}
|
|
272
192
|
|
|
273
193
|
private async selectByAI(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
${emojis.map((e, i) => `${i + 1}. [${e.character}] ${path.basename(e.file, path.extname(e.file))}`).join("\n")}
|
|
295
|
-
|
|
296
|
-
Response format (JSON):
|
|
297
|
-
{
|
|
298
|
-
"selectedIndex": number (1-based index from the list above),
|
|
299
|
-
"reason": "brief reason why this emoji is suitable"
|
|
300
|
-
}`;
|
|
301
|
-
|
|
302
|
-
const historyText = chatHistory
|
|
303
|
-
.slice(-10)
|
|
304
|
-
.map((msg) => {
|
|
305
|
-
const role = msg.role === "assistant" ? "Bot" : msg.userName || "User";
|
|
306
|
-
return `${role}: ${msg.content}`;
|
|
194
|
+
candidates: EmojiCandidate[],
|
|
195
|
+
context: EmojiSelectionContext,
|
|
196
|
+
assistantReply: string,
|
|
197
|
+
cfg: ChatConfig,
|
|
198
|
+
): Promise<EmojiCandidate | null> {
|
|
199
|
+
const persona = String(cfg.persona || "").trim();
|
|
200
|
+
const candidateList = candidates
|
|
201
|
+
.map(
|
|
202
|
+
(candidate, index) =>
|
|
203
|
+
`${index + 1}. [${candidate.character}] ${candidate.label}`,
|
|
204
|
+
)
|
|
205
|
+
.join("\n");
|
|
206
|
+
const history = context.chatHistory
|
|
207
|
+
.slice(-20)
|
|
208
|
+
.map((message) => {
|
|
209
|
+
const speaker =
|
|
210
|
+
message.role === "assistant"
|
|
211
|
+
? context.botNickname
|
|
212
|
+
: message.userName || String(message.userId || "User");
|
|
213
|
+
return `${speaker}: ${message.content}`;
|
|
307
214
|
})
|
|
308
215
|
.join("\n");
|
|
309
216
|
|
|
310
|
-
const
|
|
311
|
-
${
|
|
217
|
+
const systemPrompt = `You are the sticker selection sub-agent for ${context.botNickname}.
|
|
218
|
+
${persona ? `Identity and persona of ${context.botNickname}:\n${persona}\n` : ""}
|
|
219
|
+
The main chat agent has decided to send one sticker in this turn. Select the single sticker label that best expresses what ${context.botNickname} intends to communicate.
|
|
220
|
+
|
|
221
|
+
Rules:
|
|
222
|
+
- Use only the conversation context, the current draft reply, and the sticker labels below.
|
|
223
|
+
- Sticker labels are text descriptions derived from local filenames. You cannot see the images, so do not invent visual details beyond a label.
|
|
224
|
+
- The available stickers are already filtered by the configured character list.
|
|
225
|
+
- Return exactly one valid 1-based index as JSON.
|
|
226
|
+
|
|
227
|
+
Available sticker labels:
|
|
228
|
+
${candidateList}
|
|
229
|
+
|
|
230
|
+
Response format:
|
|
231
|
+
{"selectedIndex":1,"reason":"brief reason"}`;
|
|
232
|
+
const userPrompt = `Recent conversation:
|
|
233
|
+
${history || "(no earlier messages)"}
|
|
234
|
+
|
|
235
|
+
Current message from ${context.targetMessage.userName}:
|
|
236
|
+
${context.targetMessage.content}
|
|
312
237
|
|
|
313
|
-
|
|
238
|
+
Current draft reply from ${context.botNickname}:
|
|
239
|
+
${assistantReply || "(sticker only)"}
|
|
240
|
+
|
|
241
|
+
Choose the most contextually appropriate sticker label for ${context.botNickname} to send now.`;
|
|
314
242
|
|
|
315
243
|
try {
|
|
316
244
|
const response = await this.ai.complete({
|
|
317
|
-
model,
|
|
245
|
+
model: cfg.workingModel || cfg.model,
|
|
318
246
|
messages: [
|
|
319
247
|
{ role: "system", content: systemPrompt },
|
|
320
248
|
{ role: "user", content: userPrompt },
|
|
321
249
|
],
|
|
322
|
-
temperature: 0.
|
|
250
|
+
temperature: 0.2,
|
|
323
251
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
330
|
-
if (!jsonMatch) {
|
|
331
|
-
return this.randomPick(emojis);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const result = JSON.parse(jsonMatch[0]);
|
|
335
|
-
const selectedIndex = result.selectedIndex;
|
|
336
|
-
|
|
252
|
+
const result = extractJsonObject<{
|
|
253
|
+
selectedIndex?: unknown;
|
|
254
|
+
reason?: unknown;
|
|
255
|
+
}>(response.content || "");
|
|
256
|
+
const selectedIndex = result?.selectedIndex;
|
|
337
257
|
if (
|
|
338
258
|
typeof selectedIndex !== "number" ||
|
|
259
|
+
!Number.isInteger(selectedIndex) ||
|
|
339
260
|
selectedIndex < 1 ||
|
|
340
|
-
selectedIndex >
|
|
261
|
+
selectedIndex > candidates.length
|
|
341
262
|
) {
|
|
342
|
-
|
|
263
|
+
logger.warn("[emoji-agent] Invalid sticker selection response");
|
|
264
|
+
return null;
|
|
343
265
|
}
|
|
344
266
|
|
|
345
|
-
const selected =
|
|
346
|
-
const description = path.basename(
|
|
347
|
-
selected.file,
|
|
348
|
-
path.extname(selected.file),
|
|
349
|
-
);
|
|
350
|
-
|
|
267
|
+
const selected = candidates[selectedIndex - 1];
|
|
351
268
|
logger.info(
|
|
352
|
-
`[emoji-agent] Selected
|
|
269
|
+
`[emoji-agent] Selected [${selected.character}] ${selected.label}: ${String(result?.reason || "")}`,
|
|
353
270
|
);
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
success: true,
|
|
357
|
-
emojiPath: selected.path,
|
|
358
|
-
description,
|
|
359
|
-
};
|
|
271
|
+
return selected;
|
|
360
272
|
} catch (err) {
|
|
361
|
-
logger.warn(`[emoji-agent]
|
|
362
|
-
return
|
|
273
|
+
logger.warn(`[emoji-agent] Sticker selection failed: ${err}`);
|
|
274
|
+
return null;
|
|
363
275
|
}
|
|
364
276
|
}
|
|
365
277
|
|
|
366
|
-
private
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
} {
|
|
374
|
-
const selected = emojis[Math.floor(Math.random() * emojis.length)];
|
|
375
|
-
const description = path.basename(
|
|
376
|
-
selected.file,
|
|
377
|
-
path.extname(selected.file),
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
success: true,
|
|
382
|
-
emojiPath: selected.path,
|
|
383
|
-
description,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
private cleanMemeMarker(text: string): string {
|
|
388
|
-
let cleaned = text.replace(/\[meme:[^\]]+\]/gi, "");
|
|
389
|
-
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
390
|
-
cleaned = cleaned
|
|
391
|
-
.split("\n")
|
|
392
|
-
.map((line) => line.trim())
|
|
393
|
-
.join("\n");
|
|
394
|
-
cleaned = cleaned.trim();
|
|
395
|
-
return cleaned;
|
|
278
|
+
private cleanStickerIntent(text: string): string {
|
|
279
|
+
return String(text || "")
|
|
280
|
+
.split(/\r?\n/)
|
|
281
|
+
.filter((line) => !STICKER_INTENT_LINE.test(line))
|
|
282
|
+
.join("\n")
|
|
283
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
284
|
+
.trim();
|
|
396
285
|
}
|
|
397
286
|
}
|