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