mioku-plugin-chat 2.10.1 → 3.0.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/base.ts +1 -1
- package/context.ts +2 -2
- package/core/base.ts +113 -82
- package/core/chat-engine.ts +44 -23
- package/core/chat-turn.ts +11 -11
- package/core/media/audio.ts +4 -4
- package/core/media/gif-extractor.ts +4 -4
- package/core/media/history-media.ts +11 -16
- package/core/media/image-analyzer.ts +1 -1
- package/core/media/image-compress.ts +1 -1
- package/core/media/markdown-message.ts +22 -0
- package/core/media/segment.ts +10 -14
- package/core/multimodal.ts +18 -13
- package/core/prompt.ts +12 -10
- package/core/tools/info.ts +8 -7
- package/core/tools/web.ts +96 -57
- package/core/tools.ts +1 -1
- package/core/web/searxng.ts +1 -1
- package/core/web/web-reader.ts +2 -2
- package/db.ts +6 -3
- package/handlers/idle-debug.ts +4 -4
- package/handlers/message.ts +44 -29
- package/handlers/poke.ts +22 -16
- package/humanize/emoji-agent.ts +1 -1
- package/humanize/emotion-agent.ts +1 -1
- package/humanize/expression.ts +1 -1
- package/humanize/memory.ts +1 -1
- package/humanize/planner.ts +1 -1
- package/humanize/topic.ts +1 -1
- package/index.ts +3 -3
- package/manage/cleanup.ts +1 -1
- package/manage/cooldown.ts +5 -5
- package/manage/group-structured-history.ts +1 -1
- package/manage/idle-check.ts +5 -5
- package/manage/queue-processor.ts +5 -5
- package/manage/types.ts +39 -15
- package/package.json +3 -5
- package/types.ts +5 -5
- package/utils/message.ts +111 -66
- package/utils/queue.ts +7 -6
package/core/chat-engine.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AIInstance, SessionToolDefinition } from "mioku";
|
|
2
|
-
import { logger } from "mioki";
|
|
3
2
|
import type { AITool } from "mioku";
|
|
4
3
|
import type {
|
|
5
4
|
ToolContext,
|
|
@@ -19,6 +18,8 @@ import {
|
|
|
19
18
|
} from "./external-skills";
|
|
20
19
|
import {
|
|
21
20
|
consumeCompleteStreamUnits,
|
|
21
|
+
isQuoteMarkerOnlyUnit,
|
|
22
|
+
mergeQuoteMarkerUnits,
|
|
22
23
|
splitOutgoingUnits,
|
|
23
24
|
} from "./media/markdown-message";
|
|
24
25
|
import {
|
|
@@ -90,15 +91,16 @@ export async function runChat(
|
|
|
90
91
|
const staticPrompt = buildStaticSystemPrompt(staticCtx);
|
|
91
92
|
const dynamicUserContext = buildDynamicUserContext(dynamicCtx);
|
|
92
93
|
|
|
93
|
-
logger
|
|
94
|
+
const engineLog = toolCtx.ctx.logger;
|
|
95
|
+
engineLog.info(
|
|
94
96
|
`[chat-engine] Session ${toolCtx.sessionId} | target: ${targetMessage.userName}(${targetMessage.userId}): "${targetMessage.content}"`,
|
|
95
97
|
);
|
|
96
98
|
if (toolCtx.config.debug) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
engineLog.info("[chat-engine] === Static System Prompt ===");
|
|
100
|
+
engineLog.info(staticPrompt);
|
|
101
|
+
engineLog.info("[chat-engine] === Dynamic User Context ===");
|
|
102
|
+
engineLog.info(dynamicUserContext);
|
|
103
|
+
engineLog.info("[chat-engine] === End Prompts ===");
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
const hasStructuredHistory =
|
|
@@ -181,6 +183,7 @@ export async function runChat(
|
|
|
181
183
|
streamedMessages.push(text);
|
|
182
184
|
};
|
|
183
185
|
|
|
186
|
+
let pendingQuoteMarkerLines: string[] = [];
|
|
184
187
|
const flushStreamBuffer = async (force: boolean): Promise<void> => {
|
|
185
188
|
while (true) {
|
|
186
189
|
const { units, rest } = consumeCompleteStreamUnits(streamBuffer, force);
|
|
@@ -191,17 +194,29 @@ export async function runChat(
|
|
|
191
194
|
|
|
192
195
|
streamBuffer = rest;
|
|
193
196
|
for (const unit of units) {
|
|
197
|
+
if (!unit.trim()) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (isQuoteMarkerOnlyUnit(unit)) {
|
|
201
|
+
pendingQuoteMarkerLines.push(unit.trim());
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const mergedUnit = pendingQuoteMarkerLines.length
|
|
205
|
+
? [...pendingQuoteMarkerLines, unit].join("\n")
|
|
206
|
+
: unit;
|
|
207
|
+
pendingQuoteMarkerLines = [];
|
|
194
208
|
const unitIndex = streamUnitIndex;
|
|
195
209
|
streamUnitIndex += 1;
|
|
196
|
-
|
|
197
|
-
await emitStreamSegment(unit, unitIndex);
|
|
198
|
-
}
|
|
210
|
+
await emitStreamSegment(mergedUnit, unitIndex);
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
if (!force) {
|
|
202
214
|
break;
|
|
203
215
|
}
|
|
204
216
|
}
|
|
217
|
+
if (force) {
|
|
218
|
+
pendingQuoteMarkerLines = [];
|
|
219
|
+
}
|
|
205
220
|
};
|
|
206
221
|
|
|
207
222
|
const runComplete = () =>
|
|
@@ -252,7 +267,9 @@ export async function runChat(
|
|
|
252
267
|
targetMessage,
|
|
253
268
|
})
|
|
254
269
|
.catch((err) =>
|
|
255
|
-
logger.warn(
|
|
270
|
+
toolCtx.ctx.logger.warn(
|
|
271
|
+
`[chat-engine] background emotion refresh failed: ${err}`,
|
|
272
|
+
),
|
|
256
273
|
);
|
|
257
274
|
|
|
258
275
|
if (streamEnabled) {
|
|
@@ -261,9 +278,9 @@ export async function runChat(
|
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
if (toolCtx.config.debug) {
|
|
264
|
-
logger.info("[chat-engine] === Raw AI Reply ===");
|
|
265
|
-
logger.info(response.content || "(empty)");
|
|
266
|
-
logger.info("[chat-engine] === End Raw AI Reply ===");
|
|
281
|
+
toolCtx.ctx.logger.info("[chat-engine] === Raw AI Reply ===");
|
|
282
|
+
toolCtx.ctx.logger.info(response.content || "(empty)");
|
|
283
|
+
toolCtx.ctx.logger.info("[chat-engine] === End Raw AI Reply ===");
|
|
267
284
|
}
|
|
268
285
|
|
|
269
286
|
const allToolCalls = response.allToolCalls || [];
|
|
@@ -275,16 +292,18 @@ export async function runChat(
|
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
if (toolCtx.config.debug && response.reasoning) {
|
|
278
|
-
logger.info(
|
|
295
|
+
toolCtx.ctx.logger.info(
|
|
296
|
+
`[chat-engine] AI reasoning: ${response.reasoning}`,
|
|
297
|
+
);
|
|
279
298
|
}
|
|
280
299
|
|
|
281
300
|
if (toolCtx.config.debug && allToolCalls.length > 0) {
|
|
282
301
|
for (const toolCall of allToolCalls) {
|
|
283
302
|
const resultPreview = JSON.stringify(toolCall.result);
|
|
284
|
-
logger.info(
|
|
303
|
+
toolCtx.ctx.logger.info(
|
|
285
304
|
`[chat-engine] Tool call: ${toolCall.name}(${JSON.stringify(toolCall.arguments).substring(0, 100)})`,
|
|
286
305
|
);
|
|
287
|
-
logger.info(
|
|
306
|
+
toolCtx.ctx.logger.info(
|
|
288
307
|
`[chat-engine] Tool result: ${toolCall.name} -> ${resultPreview ? resultPreview.substring(0, 300) : "undefined"}`,
|
|
289
308
|
);
|
|
290
309
|
}
|
|
@@ -298,7 +317,9 @@ export async function runChat(
|
|
|
298
317
|
currentUserMessages,
|
|
299
318
|
[],
|
|
300
319
|
);
|
|
301
|
-
logger.info(
|
|
320
|
+
toolCtx.ctx.logger.info(
|
|
321
|
+
`[chat-engine] Session ${toolCtx.sessionId} ended by tool`,
|
|
322
|
+
);
|
|
302
323
|
toolCtx.aiService.finalizeUsage?.(usageId, {
|
|
303
324
|
sentUserMessages: 0,
|
|
304
325
|
sentAssistantMessages: 0,
|
|
@@ -358,9 +379,9 @@ export async function runChat(
|
|
|
358
379
|
}
|
|
359
380
|
}
|
|
360
381
|
|
|
361
|
-
const finalMessages =
|
|
362
|
-
(
|
|
363
|
-
);
|
|
382
|
+
const finalMessages = mergeQuoteMarkerUnits(
|
|
383
|
+
splitOutgoingUnits(finalText),
|
|
384
|
+
).filter((unit) => unit.trim() && unit.trim() !== "---");
|
|
364
385
|
const sentAssistantMessages = streamEnabled
|
|
365
386
|
? streamedMessages.length
|
|
366
387
|
: finalMessages.length;
|
|
@@ -369,7 +390,7 @@ export async function runChat(
|
|
|
369
390
|
sentAssistantMessages,
|
|
370
391
|
});
|
|
371
392
|
|
|
372
|
-
logger.info(
|
|
393
|
+
toolCtx.ctx.logger.info(
|
|
373
394
|
`[chat-engine] Session ${toolCtx.sessionId} done | ${finalMessages.length} msg(s), ${allToolCalls.length} tool call(s)`,
|
|
374
395
|
);
|
|
375
396
|
|
|
@@ -955,7 +976,7 @@ ${failedSummary}
|
|
|
955
976
|
return text;
|
|
956
977
|
}
|
|
957
978
|
} catch (err) {
|
|
958
|
-
logger.warn(
|
|
979
|
+
toolCtx.ctx.logger.warn(
|
|
959
980
|
`[chat-engine] Failed to generate tool-failure fallback reply: ${err}`,
|
|
960
981
|
);
|
|
961
982
|
}
|
package/core/chat-turn.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MiokuContext } from "mioku";
|
|
2
2
|
import type { AITool, ChatRuntimePromptInjection } from "mioku";
|
|
3
3
|
import type { ChatPluginContext, ChatRuntimeState } from "../context";
|
|
4
4
|
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
@@ -41,14 +41,14 @@ interface ResolvedRuntimeContext {
|
|
|
41
41
|
userRole: "owner" | "admin" | "member";
|
|
42
42
|
userTitle?: string;
|
|
43
43
|
groupName?: string;
|
|
44
|
-
messageId?:
|
|
44
|
+
messageId?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const NO_NEW_MESSAGE =
|
|
48
48
|
"[No new user message in this turn. Reply naturally based on the runtime instruction and recent context.]";
|
|
49
49
|
|
|
50
50
|
function buildRuntimeTargetMessageContent(
|
|
51
|
-
ctx:
|
|
51
|
+
ctx: MiokuContext,
|
|
52
52
|
event: any,
|
|
53
53
|
overrideContent?: string,
|
|
54
54
|
): string {
|
|
@@ -58,7 +58,7 @@ function buildRuntimeTargetMessageContent(
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function resolveRuntimeContext(
|
|
61
|
-
ctx:
|
|
61
|
+
ctx: MiokuContext,
|
|
62
62
|
options: ExecuteChatRuntimeRequestOptions,
|
|
63
63
|
): ResolvedRuntimeContext {
|
|
64
64
|
if (options.event) {
|
|
@@ -147,13 +147,13 @@ export async function finalizeChatTurn(
|
|
|
147
147
|
const { groupId, groupSessionId, userId, selfId, cfg, toolCtx, result, event } = args;
|
|
148
148
|
|
|
149
149
|
if (groupId) {
|
|
150
|
-
await pluginCtx.sendAIResponse(
|
|
150
|
+
const sentMessageIds = await pluginCtx.sendAIResponse(
|
|
151
151
|
{ ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices, audioService: pluginCtx.audioService },
|
|
152
152
|
selfId,
|
|
153
153
|
);
|
|
154
|
-
await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath,
|
|
154
|
+
await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, event?.bot);
|
|
155
155
|
const now = Date.now();
|
|
156
|
-
pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx,
|
|
156
|
+
pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx, event?.bot, sentMessageIds);
|
|
157
157
|
if (args.isLive) {
|
|
158
158
|
pluginCtx.idleCheckManager.recordBotMessages(groupSessionId, result.messages.length, selfId);
|
|
159
159
|
}
|
|
@@ -170,9 +170,9 @@ export async function finalizeChatTurn(
|
|
|
170
170
|
if (args.isLive) {
|
|
171
171
|
await event.reply([emojiSegment]);
|
|
172
172
|
} else {
|
|
173
|
-
const bot =
|
|
173
|
+
const bot = args.event?.bot;
|
|
174
174
|
if (!bot) throw new Error(`bot ${selfId} not found`);
|
|
175
|
-
await bot.
|
|
175
|
+
await bot.sendMessage({ type: "private", user_id: userId}, [emojiSegment]);
|
|
176
176
|
}
|
|
177
177
|
} catch (err) {
|
|
178
178
|
ctx.logger.warn(
|
|
@@ -260,7 +260,7 @@ export async function processChat(
|
|
|
260
260
|
}));
|
|
261
261
|
|
|
262
262
|
const botNickname =
|
|
263
|
-
cfg.nicknames[0] ||
|
|
263
|
+
cfg.nicknames[0] || e.bot?.nickname || "Bot";
|
|
264
264
|
const botRole = groupId ? await getBotRole(groupId, ctx, e.self_id) : "member";
|
|
265
265
|
let groupName: string | undefined;
|
|
266
266
|
let memberCount: number | undefined;
|
|
@@ -451,7 +451,7 @@ async function executeChatRuntimeRequestNow(
|
|
|
451
451
|
|
|
452
452
|
const botRole = groupId ? await getBotRole(groupId, pluginCtx.ctx, selfId) : "member";
|
|
453
453
|
const botNickname =
|
|
454
|
-
cfg.nicknames[0] ||
|
|
454
|
+
cfg.nicknames[0] || event?.bot?.nickname || "Bot";
|
|
455
455
|
|
|
456
456
|
let groupName: string | undefined;
|
|
457
457
|
let memberCount: number | undefined;
|
package/core/media/audio.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import type {
|
|
2
|
+
import type { MiokuContext } from "mioku";
|
|
3
3
|
import type { AudioServiceApi, SupportedLang } from "mioku-service-audio";
|
|
4
4
|
|
|
5
5
|
function detectAudioLanguage(text: string): SupportedLang {
|
|
@@ -33,7 +33,7 @@ export async function synthesizeAudioSource(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export async function sendVoice(
|
|
36
|
-
ctx:
|
|
36
|
+
ctx: MiokuContext,
|
|
37
37
|
e: any,
|
|
38
38
|
filePath: string,
|
|
39
39
|
logTag = "[audio]",
|
|
@@ -42,7 +42,7 @@ export async function sendVoice(
|
|
|
42
42
|
? filePath
|
|
43
43
|
: `file://${filePath}`;
|
|
44
44
|
try {
|
|
45
|
-
await e.reply([ctx.segment.record
|
|
45
|
+
await e.reply([ctx.segment.raw("record", { file: fileUrl })]);
|
|
46
46
|
return;
|
|
47
47
|
} catch (err: any) {
|
|
48
48
|
const msg = String(err?.message ?? err);
|
|
@@ -59,6 +59,6 @@ export async function sendVoice(
|
|
|
59
59
|
ctx.logger.info(
|
|
60
60
|
`${logTag} file:// 发送失败 (${msg}),回退 base64 (${buf.length} bytes)`,
|
|
61
61
|
);
|
|
62
|
-
await e.reply([ctx.segment.record
|
|
62
|
+
await e.reply([ctx.segment.raw("record", { file: b64 })]);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from "
|
|
1
|
+
import { logger } from "mioku";
|
|
2
2
|
import sharp from "sharp";
|
|
3
3
|
import { QQ_IMAGE_FETCH_HEADERS } from "./image-compress";
|
|
4
4
|
|
|
@@ -57,7 +57,7 @@ export async function extractGifFrames(
|
|
|
57
57
|
})
|
|
58
58
|
.png()
|
|
59
59
|
.toBuffer();
|
|
60
|
-
frames.push(`data:image/png;base64,${firstFrame.toString("base64")}`);
|
|
60
|
+
frames.push(`data:image/png);base64,${firstFrame.toString("base64")}`);
|
|
61
61
|
|
|
62
62
|
// 提取中间帧
|
|
63
63
|
if (middleIndex !== firstIndex && middleIndex !== lastIndex) {
|
|
@@ -67,7 +67,7 @@ export async function extractGifFrames(
|
|
|
67
67
|
})
|
|
68
68
|
.png()
|
|
69
69
|
.toBuffer();
|
|
70
|
-
frames.push(`data:image/png;base64,${middleFrame.toString("base64")}`);
|
|
70
|
+
frames.push(`data:image/png);base64,${middleFrame.toString("base64")}`);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// 提取末尾帧
|
|
@@ -78,7 +78,7 @@ export async function extractGifFrames(
|
|
|
78
78
|
})
|
|
79
79
|
.png()
|
|
80
80
|
.toBuffer();
|
|
81
|
-
frames.push(`data:image/png;base64,${lastFrame.toString("base64")}`);
|
|
81
|
+
frames.push(`data:image/png);base64,${lastFrame.toString("base64")}`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
logger.info(
|
|
@@ -4,7 +4,7 @@ import * as fs from "fs/promises";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import { logger } from "
|
|
7
|
+
import { Bot, ForwardNode, logger } from "mioku";
|
|
8
8
|
import type { AIInstance } from "mioku";
|
|
9
9
|
import type { MediaSummaryKind, MediaSummaryRecord } from "../../types";
|
|
10
10
|
|
|
@@ -69,9 +69,7 @@ export interface HistoryMediaProcessingOptions {
|
|
|
69
69
|
warn(message: string): void;
|
|
70
70
|
error(message: string): void;
|
|
71
71
|
};
|
|
72
|
-
bot?:
|
|
73
|
-
api<T = any>(action: string, params?: Record<string, any>): Promise<T>;
|
|
74
|
-
};
|
|
72
|
+
bot?: Bot;
|
|
75
73
|
groupId?: number;
|
|
76
74
|
runAIRequest?<T>(request: () => Promise<T>): Promise<T | null>;
|
|
77
75
|
}
|
|
@@ -82,7 +80,7 @@ export interface GroupNoticeHistorySummary {
|
|
|
82
80
|
userRole: string;
|
|
83
81
|
content: string;
|
|
84
82
|
timestamp: number;
|
|
85
|
-
messageId:
|
|
83
|
+
messageId: string;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
function runHistoryMediaAIRequest<T>(
|
|
@@ -252,7 +250,7 @@ export async function summarizeHistoryForward(
|
|
|
252
250
|
return "[forward]";
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
const forwardResult = await options.bot.
|
|
253
|
+
const forwardResult = await options.bot.getForwardMessage(id);
|
|
256
254
|
const text = extractForwardText(forwardResult);
|
|
257
255
|
const source = `forward:${id}`;
|
|
258
256
|
if (!text) {
|
|
@@ -293,7 +291,7 @@ export async function getCachedHistoryForwardTag(
|
|
|
293
291
|
if (!id) return "[forward]";
|
|
294
292
|
if (!options.bot) return "[forward]";
|
|
295
293
|
try {
|
|
296
|
-
const forwardResult = await options.bot.
|
|
294
|
+
const forwardResult = await options.bot.getForwardMessage(id);
|
|
297
295
|
const text = extractForwardText(forwardResult);
|
|
298
296
|
const summary = getCachedSummaryByHash(
|
|
299
297
|
"forward",
|
|
@@ -395,7 +393,7 @@ export async function summarizeGroupNotice(
|
|
|
395
393
|
userRole: "member",
|
|
396
394
|
content,
|
|
397
395
|
timestamp: normalizeNoticeTimestamp(notice),
|
|
398
|
-
messageId:
|
|
396
|
+
messageId: String((notice as { message_id?: unknown }).message_id ?? ""),
|
|
399
397
|
};
|
|
400
398
|
}
|
|
401
399
|
|
|
@@ -558,7 +556,7 @@ async function extractVideoFrames(
|
|
|
558
556
|
{ timeout: 30_000 },
|
|
559
557
|
);
|
|
560
558
|
const buffer = await fs.readFile(outputPath);
|
|
561
|
-
frames.push(`data:image/jpeg;base64,${buffer.toString("base64")}`);
|
|
559
|
+
frames.push(`data:image/jpeg);base64,${buffer.toString("base64")}`);
|
|
562
560
|
}
|
|
563
561
|
|
|
564
562
|
return frames;
|
|
@@ -645,16 +643,13 @@ function buildFrameTimestamps(duration: number, frameCount: number): number[] {
|
|
|
645
643
|
});
|
|
646
644
|
}
|
|
647
645
|
|
|
648
|
-
function extractForwardText(result:
|
|
649
|
-
const
|
|
650
|
-
result?.messages || result?.data?.messages || result?.data || [];
|
|
651
|
-
const nodes = Array.isArray(messages) ? messages : [];
|
|
646
|
+
function extractForwardText(result: ForwardNode[]): string {
|
|
647
|
+
const nodes = Array.isArray(result) ? result : [];
|
|
652
648
|
const lines: string[] = [];
|
|
653
649
|
|
|
654
650
|
for (const node of nodes) {
|
|
655
|
-
const senderName =
|
|
656
|
-
|
|
657
|
-
const content = extractMessageSegmentsText(node.message || node.content);
|
|
651
|
+
const senderName = node.nickname || String(node.user_id || "unknown");
|
|
652
|
+
const content = extractMessageSegmentsText(node.message);
|
|
658
653
|
if (content) {
|
|
659
654
|
lines.push(`${senderName}: ${content}`);
|
|
660
655
|
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export const MARKDOWN_OPEN_TAG = "<MARKDOWN>";
|
|
2
2
|
export const MARKDOWN_CLOSE_TAG = "</MARKDOWN>";
|
|
3
3
|
|
|
4
|
+
const QUOTE_MARKER_ONLY = /^(?:\s*\[reply:[^\]\n]+\])+\s*$/i;
|
|
5
|
+
|
|
6
|
+
export function isQuoteMarkerOnlyUnit(unit: string): boolean {
|
|
7
|
+
return QUOTE_MARKER_ONLY.test(String(unit || "").trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mergeQuoteMarkerUnits(units: string[]): string[] {
|
|
11
|
+
const result: string[] = [];
|
|
12
|
+
let pending: string[] = [];
|
|
13
|
+
|
|
14
|
+
for (const unit of units) {
|
|
15
|
+
if (isQuoteMarkerOnlyUnit(unit)) {
|
|
16
|
+
pending.push(unit.trim());
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
result.push(pending.length > 0 ? [...pending, unit].join("\n") : unit);
|
|
20
|
+
pending = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export function splitOutgoingUnits(text: string): string[] {
|
|
5
27
|
const normalized = String(text || "").replace(/\r/g, "");
|
|
6
28
|
const result: string[] = [];
|
package/core/media/segment.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AIInstance } from "mioku";
|
|
1
|
+
import type { AIInstance, Bot } from "mioku";
|
|
2
2
|
import type { ChatConfig, MediaSummaryRecord } from "../../types";
|
|
3
3
|
import type { HistoryMediaOptions } from "../../manage/types";
|
|
4
4
|
import {
|
|
@@ -27,9 +27,7 @@ export function buildHistoryMediaProcessingOptions(
|
|
|
27
27
|
getMediaSummary(key: string): MediaSummaryRecord | null;
|
|
28
28
|
saveMediaSummary(summary: MediaSummaryRecord): void;
|
|
29
29
|
},
|
|
30
|
-
bot:
|
|
31
|
-
api<T = unknown>(action: string, params?: Record<string, unknown>): Promise<T>;
|
|
32
|
-
},
|
|
30
|
+
bot: Bot,
|
|
33
31
|
groupId: number,
|
|
34
32
|
log: HistoryMediaProcessingOptions["logger"],
|
|
35
33
|
runAIRequest?: <T>(request: () => Promise<T>) => Promise<T | null>,
|
|
@@ -53,17 +51,12 @@ export function getSegmentUrl(seg: {
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
export async function getVideoSourceCandidatesFromMessage(
|
|
56
|
-
bot:
|
|
57
|
-
api(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
58
|
-
},
|
|
54
|
+
bot: Bot,
|
|
59
55
|
messageId: number | string | undefined,
|
|
60
56
|
): Promise<string[]> {
|
|
61
|
-
if (messageId == null) return [];
|
|
62
|
-
const result =
|
|
63
|
-
|
|
64
|
-
data?: { message?: unknown[] };
|
|
65
|
-
};
|
|
66
|
-
const segments = result?.message || result?.data?.message || [];
|
|
57
|
+
if (messageId == null || !bot) return [];
|
|
58
|
+
const result = await bot.getMessage(messageId);
|
|
59
|
+
const segments = result?.message ?? [];
|
|
67
60
|
if (!Array.isArray(segments)) return [];
|
|
68
61
|
const candidates: string[] = [];
|
|
69
62
|
for (const seg of segments as MediaMessageSegment[]) {
|
|
@@ -86,6 +79,9 @@ export function getCardData(seg: unknown): string | null {
|
|
|
86
79
|
return typeof data === "string" ? data : JSON.stringify(data);
|
|
87
80
|
}
|
|
88
81
|
|
|
89
|
-
export function isMediaAnalysisBlocked(
|
|
82
|
+
export function isMediaAnalysisBlocked(
|
|
83
|
+
config: ChatConfig,
|
|
84
|
+
userId: number,
|
|
85
|
+
): boolean {
|
|
90
86
|
return Boolean(config.mediaAnalysisBlacklistUsers?.includes(userId));
|
|
91
87
|
}
|
package/core/multimodal.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AIInstance } from "mioku";
|
|
2
|
-
import type {
|
|
3
|
-
import { logger } from "
|
|
1
|
+
import type { AIInstance, Bot } from "mioku";
|
|
2
|
+
import type { MiokuContext } from "mioku";
|
|
3
|
+
import { logger } from "mioku";
|
|
4
4
|
import { prepareImageUrlsForModel } from "./media/image-compress";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -126,23 +126,26 @@ export type MediaByMessageId =
|
|
|
126
126
|
* @returns 媒体信息或 null
|
|
127
127
|
*/
|
|
128
128
|
export async function getMediaByMessageId(
|
|
129
|
-
ctx:
|
|
130
|
-
messageId: number,
|
|
129
|
+
ctx: MiokuContext,
|
|
130
|
+
messageId: string | number,
|
|
131
131
|
e: any,
|
|
132
132
|
): Promise<MediaByMessageId> {
|
|
133
133
|
try {
|
|
134
|
-
const
|
|
134
|
+
const bot: Bot = e?.bot ?? ctx.pickBot(e.self_id ?? "");
|
|
135
|
+
if (!bot) return null;
|
|
136
|
+
const msg = await bot.getMessage(messageId);
|
|
135
137
|
if (!msg || !msg.message) {
|
|
136
138
|
return null;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
for (const seg of msg.message
|
|
141
|
+
for (const seg of Array.from(msg.message)) {
|
|
140
142
|
if (seg.type === "image") {
|
|
141
|
-
const url = seg.url
|
|
143
|
+
const url = String(seg.data?.url ?? seg.data?.file ?? "");
|
|
142
144
|
if (url) return { kind: "image", url };
|
|
143
145
|
}
|
|
144
146
|
if (seg.type === "video") {
|
|
145
|
-
const { getSegmentSourceCandidates } =
|
|
147
|
+
const { getSegmentSourceCandidates } =
|
|
148
|
+
await import("./media/history-media");
|
|
146
149
|
const sources = getSegmentSourceCandidates(seg);
|
|
147
150
|
if (sources.length > 0) return { kind: "video", sources };
|
|
148
151
|
}
|
|
@@ -150,7 +153,7 @@ export async function getMediaByMessageId(
|
|
|
150
153
|
|
|
151
154
|
return null;
|
|
152
155
|
} catch (err) {
|
|
153
|
-
logger.error(
|
|
156
|
+
ctx.logger.error(
|
|
154
157
|
`[multimodal] Failed to get media from message ${messageId}: ${err}`,
|
|
155
158
|
);
|
|
156
159
|
return null;
|
|
@@ -164,7 +167,7 @@ export async function getMediaByMessageId(
|
|
|
164
167
|
* @returns 图片 URL 或 null
|
|
165
168
|
*/
|
|
166
169
|
export async function getQuoteImageUrl(
|
|
167
|
-
ctx:
|
|
170
|
+
ctx: MiokuContext,
|
|
168
171
|
event: any,
|
|
169
172
|
): Promise<string | null> {
|
|
170
173
|
if (!event.quote_id) {
|
|
@@ -172,7 +175,9 @@ export async function getQuoteImageUrl(
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
try {
|
|
175
|
-
const
|
|
178
|
+
const bot = event?.bot;
|
|
179
|
+
if (!bot) return null;
|
|
180
|
+
const quotedMsg = await bot.getMessage(event.quote_id);
|
|
176
181
|
if (!quotedMsg || !quotedMsg.message) {
|
|
177
182
|
return null;
|
|
178
183
|
}
|
|
@@ -186,7 +191,7 @@ export async function getQuoteImageUrl(
|
|
|
186
191
|
// 提取图片 URL
|
|
187
192
|
return (imageSeg as any).url || (imageSeg as any).data?.url || null;
|
|
188
193
|
} catch (err) {
|
|
189
|
-
logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
|
|
194
|
+
ctx.logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
|
|
190
195
|
return null;
|
|
191
196
|
}
|
|
192
197
|
}
|
package/core/prompt.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
|
|
2
|
-
import { logger } from "
|
|
2
|
+
import { logger } from "mioku";
|
|
3
3
|
import type {
|
|
4
4
|
SkillPermissionRole,
|
|
5
5
|
AIService,
|
|
@@ -45,7 +45,7 @@ export interface DynamicPromptContext {
|
|
|
45
45
|
reviewMessages?: {
|
|
46
46
|
contents: string[];
|
|
47
47
|
userNames: string[];
|
|
48
|
-
messageIds:
|
|
48
|
+
messageIds: string[];
|
|
49
49
|
};
|
|
50
50
|
currentEmotion?: string;
|
|
51
51
|
expressionContext?: string;
|
|
@@ -276,7 +276,7 @@ function buildResponseFormatSection(
|
|
|
276
276
|
- **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
|
|
277
277
|
- Use [at:123456] in your text to @ someone (123456 is the QQ number)
|
|
278
278
|
- Use [poke:123456] in your text to poke someone. IMPORTANT: when you plan to poke a user, DON't describe your poke actions.
|
|
279
|
-
- Use [reply:
|
|
279
|
+
- Use [reply:message_id] at the START of a line to quote-reply that message (copy the message_id EXACTLY as shown after # in the chat history; it may contain letters, e.g. [reply:P2rW...pMB])
|
|
280
280
|
- **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**`);
|
|
281
281
|
|
|
282
282
|
if (ctx.config.audio?.enabled) {
|
|
@@ -293,7 +293,7 @@ ${AUDIO_MODE_LINE[audioStrength]}`);
|
|
|
293
293
|
### Optional Markdown Screenshot Format
|
|
294
294
|
- You MAY optionally send one rendered Markdown screenshot by wrapping content with exact tags: <MARKDOWN> ... </MARKDOWN>
|
|
295
295
|
- Put the Markdown block on its own message whenever possible.
|
|
296
|
-
- It is forbidden to use Markdown syntax or formulas in plain text; they must be rendered using <MARKDOWN> blocks.
|
|
296
|
+
- It is forbidden to use Markdown syntax or formulas in plain text); they must be rendered using <MARKDOWN> blocks.
|
|
297
297
|
${MARKDOWN_MODE_LINE[markdownStrength]}
|
|
298
298
|
- Inside <MARKDOWN>...</MARKDOWN>, there is NO length limit. If the user needs detail, explain clearly and thoroughly instead of over-compressing.
|
|
299
299
|
`);
|
|
@@ -304,7 +304,7 @@ ${MARKDOWN_MODE_LINE[markdownStrength]}
|
|
|
304
304
|
lines.push(`
|
|
305
305
|
### Tool Calling Format
|
|
306
306
|
- When you decide to use a tool, you MUST use the structured tool_calls mechanism provided by the API
|
|
307
|
-
- web_search and web_read_page are limited per conversation; do not retry excessively`);
|
|
307
|
+
- web_search and web_read_page are limited per conversation); do not retry excessively`);
|
|
308
308
|
|
|
309
309
|
appendEmojiSection(lines, ctx, emojiStrength);
|
|
310
310
|
|
|
@@ -579,13 +579,15 @@ function buildChatHistorySection(ctx: DynamicPromptContext): string {
|
|
|
579
579
|
for (const msg of chatHistory) {
|
|
580
580
|
const time = new Date(msg.timestamp);
|
|
581
581
|
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")}`;
|
|
582
|
+
const msgIdStr = msg.messageId ? `#${msg.messageId}` : "";
|
|
582
583
|
|
|
583
584
|
if (msg.role === "assistant") {
|
|
585
|
+
const contentStr = msgIdStr ? `${msg.content} ${msgIdStr}` : msg.content;
|
|
584
586
|
if (currentAssistantBlock && currentAssistantBlock.timeStr === timeStr) {
|
|
585
|
-
currentAssistantBlock.contents.push(
|
|
587
|
+
currentAssistantBlock.contents.push(contentStr);
|
|
586
588
|
} else {
|
|
587
589
|
flushAssistant();
|
|
588
|
-
currentAssistantBlock = { timeStr, contents: [
|
|
590
|
+
currentAssistantBlock = { timeStr, contents: [contentStr] };
|
|
589
591
|
}
|
|
590
592
|
continue;
|
|
591
593
|
}
|
|
@@ -600,16 +602,16 @@ function buildChatHistorySection(ctx: DynamicPromptContext): string {
|
|
|
600
602
|
: "Member";
|
|
601
603
|
const titleStr = msg.userTitle ? `, ${msg.userTitle}` : "";
|
|
602
604
|
const qqStr = msg.userId ? `${msg.userId}` : "";
|
|
603
|
-
const msgIdStr = msg.messageId ? ` #${msg.messageId}` : "";
|
|
604
605
|
mergedLines.push(
|
|
605
|
-
`[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr}
|
|
606
|
+
`[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr ? ` ${msgIdStr}` : ""}: ${msg.content}`,
|
|
606
607
|
);
|
|
607
608
|
}
|
|
608
609
|
flushAssistant();
|
|
609
610
|
|
|
610
611
|
return `## Recent Context
|
|
611
612
|
${mergedLines.join("\n")}
|
|
612
|
-
-- DON'T repeat yourself or bring up old topics - focus on what's being said right now.
|
|
613
|
+
-- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --
|
|
614
|
+
-- Use [reply:message_id] with the #id shown after a message to quote-reply it. --`;
|
|
613
615
|
}
|
|
614
616
|
|
|
615
617
|
function buildTargetMessageSection(
|