mioku-plugin-chat 3.0.0 → 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/core/base.ts +64 -28
- package/core/chat-engine.ts +44 -23
- package/core/chat-turn.ts +3 -3
- package/core/media/history-media.ts +2 -2
- package/core/media/markdown-message.ts +22 -0
- package/core/multimodal.ts +3 -3
- package/core/prompt.ts +9 -7
- package/core/tools/web.ts +11 -2
- package/db.ts +6 -3
- package/handlers/idle-debug.ts +1 -1
- package/manage/cooldown.ts +1 -1
- package/manage/group-structured-history.ts +1 -1
- package/manage/idle-check.ts +1 -1
- package/manage/queue-processor.ts +1 -1
- package/manage/types.ts +3 -2
- package/package.json +1 -1
- package/types.ts +3 -3
- package/utils/message.ts +20 -7
- package/utils/queue.ts +7 -6
package/core/base.ts
CHANGED
|
@@ -79,7 +79,7 @@ async function waitTypingDelay(
|
|
|
79
79
|
export async function sendAIResponse(
|
|
80
80
|
options: SendAIResponseOptions,
|
|
81
81
|
selfId: number,
|
|
82
|
-
): Promise<
|
|
82
|
+
): Promise<Array<string | undefined>> {
|
|
83
83
|
const {
|
|
84
84
|
ctx,
|
|
85
85
|
groupId,
|
|
@@ -96,17 +96,25 @@ export async function sendAIResponse(
|
|
|
96
96
|
ctx.logger.error(
|
|
97
97
|
`[sendAIResponse] bot ${String(selfId)} not found, skip sending group message`,
|
|
98
98
|
);
|
|
99
|
-
return;
|
|
99
|
+
return [];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
if (messages.length === 0) return;
|
|
102
|
+
if (messages.length === 0) return [];
|
|
103
|
+
|
|
104
|
+
const sentMessageIds: Array<string | undefined> = new Array(messages.length);
|
|
105
|
+
const recordSentId = (messageIndex: number, messageId?: string) => {
|
|
106
|
+
if (!messageId) return;
|
|
107
|
+
if (messageIndex >= 0 && messageIndex < messages.length) {
|
|
108
|
+
sentMessageIds[messageIndex] ??= messageId;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
103
111
|
|
|
104
112
|
for (let i = 0; i < messages.length; i++) {
|
|
105
113
|
if (sentIndices?.has(i)) continue;
|
|
106
114
|
|
|
107
115
|
const expandedLines = expandOutgoingLines(messages[i]);
|
|
108
116
|
|
|
109
|
-
let pendingReply:
|
|
117
|
+
let pendingReply: string | undefined;
|
|
110
118
|
let lastDelayBasisText = "";
|
|
111
119
|
|
|
112
120
|
for (let j = 0; j < expandedLines.length; j++) {
|
|
@@ -190,7 +198,7 @@ export async function sendAIResponse(
|
|
|
190
198
|
return segments;
|
|
191
199
|
},
|
|
192
200
|
imagePath,
|
|
193
|
-
);
|
|
201
|
+
).then((sentId) => recordSentId(i, sentId));
|
|
194
202
|
lastDelayBasisText = summarizeMarkdown(markdownContent);
|
|
195
203
|
if (typingDelayEnabled && j < expandedLines.length - 1) {
|
|
196
204
|
const delayMs = calculateTypingDelayMs(lastDelayBasisText || line);
|
|
@@ -217,7 +225,11 @@ export async function sendAIResponse(
|
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
if (lineSegments.length > 0) {
|
|
220
|
-
await bot.sendMessage(
|
|
228
|
+
const sent = await bot.sendMessage(
|
|
229
|
+
{ type: "group", group_id: groupId },
|
|
230
|
+
lineSegments,
|
|
231
|
+
);
|
|
232
|
+
recordSentId(i, sent?.message_id);
|
|
221
233
|
lastDelayBasisText = sendableText || fallbackText || audioText || line;
|
|
222
234
|
}
|
|
223
235
|
|
|
@@ -234,6 +246,8 @@ export async function sendAIResponse(
|
|
|
234
246
|
|
|
235
247
|
await onLineSent?.();
|
|
236
248
|
}
|
|
249
|
+
|
|
250
|
+
return sentMessageIds;
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
export async function sendMessage(
|
|
@@ -259,7 +273,7 @@ export async function sendMessage(
|
|
|
259
273
|
// 应用错别字生成器
|
|
260
274
|
const expandedLines = expandOutgoingLines(text);
|
|
261
275
|
|
|
262
|
-
let pendingReply:
|
|
276
|
+
let pendingReply: string | undefined;
|
|
263
277
|
let lastDelayBasisText = "";
|
|
264
278
|
|
|
265
279
|
for (let j = 0; j < expandedLines.length; j++) {
|
|
@@ -374,7 +388,10 @@ export async function sendMessage(
|
|
|
374
388
|
}
|
|
375
389
|
|
|
376
390
|
if (segments.length > 0 && groupId) {
|
|
377
|
-
await bot.sendMessage(
|
|
391
|
+
await bot.sendMessage(
|
|
392
|
+
{ type: "group", group_id: groupId },
|
|
393
|
+
segments,
|
|
394
|
+
);
|
|
378
395
|
lastDelayBasisText =
|
|
379
396
|
sendableText || fallbackText || audioText || line;
|
|
380
397
|
}
|
|
@@ -396,7 +413,7 @@ export async function sendMessage(
|
|
|
396
413
|
const beforeAt = remaining.slice(lastIndex, match.index);
|
|
397
414
|
if (beforeAt) {
|
|
398
415
|
const cleaned = beforeAt
|
|
399
|
-
.replace(/\[reply
|
|
416
|
+
.replace(/\[reply:[^\]\n]+\]/g, "")
|
|
400
417
|
.replace(/\[poke:\d+\]/g, "")
|
|
401
418
|
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
402
419
|
.trim();
|
|
@@ -417,7 +434,7 @@ export async function sendMessage(
|
|
|
417
434
|
const afterAt = remaining.slice(lastIndex);
|
|
418
435
|
if (afterAt) {
|
|
419
436
|
const cleaned = afterAt
|
|
420
|
-
.replace(/\[reply
|
|
437
|
+
.replace(/\[reply:[^\]\n]+\]/g, "")
|
|
421
438
|
.replace(/\[poke:\d+\]/g, "")
|
|
422
439
|
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
423
440
|
.trim();
|
|
@@ -435,7 +452,10 @@ export async function sendMessage(
|
|
|
435
452
|
// 发送消息
|
|
436
453
|
if (segments.length > 0) {
|
|
437
454
|
if (groupId) {
|
|
438
|
-
await bot.sendMessage(
|
|
455
|
+
await bot.sendMessage(
|
|
456
|
+
{ type: "group", group_id: groupId },
|
|
457
|
+
segments,
|
|
458
|
+
);
|
|
439
459
|
lastDelayBasisText =
|
|
440
460
|
sendableText || fallbackText || audioText || line;
|
|
441
461
|
}
|
|
@@ -510,7 +530,7 @@ function expandOutgoingLines(text: string): string[] {
|
|
|
510
530
|
|
|
511
531
|
function normalizeActionLineBreaks(text: string): string {
|
|
512
532
|
return String(text || "").replace(
|
|
513
|
-
/\\\s*(?=(?:\[\]|\[emotion:[^\]]+\]|\[audio:[^\]]+\]|\[reply
|
|
533
|
+
/\\\s*(?=(?:\[\]|\[emotion:[^\]]+\]|\[audio:[^\]]+\]|\[reply:[^\]\n]+\]))/gi,
|
|
514
534
|
"\n",
|
|
515
535
|
);
|
|
516
536
|
}
|
|
@@ -539,9 +559,10 @@ async function dispatchSegments(
|
|
|
539
559
|
userId: number | undefined,
|
|
540
560
|
buildSegments: (imageSource?: string) => any[],
|
|
541
561
|
fallbackImagePath?: string,
|
|
542
|
-
): Promise<
|
|
562
|
+
): Promise<string | undefined> {
|
|
543
563
|
try {
|
|
544
|
-
await sendByTarget(bot, groupId, userId, buildSegments());
|
|
564
|
+
const sent = await sendByTarget(bot, groupId, userId, buildSegments());
|
|
565
|
+
return sent?.message_id;
|
|
545
566
|
} catch (error) {
|
|
546
567
|
if (!fallbackImagePath || !isLocalFilePath(fallbackImagePath)) {
|
|
547
568
|
throw error;
|
|
@@ -550,7 +571,13 @@ async function dispatchSegments(
|
|
|
550
571
|
const fsPromises = await import("fs/promises");
|
|
551
572
|
const buffer = await fsPromises.readFile(fallbackImagePath);
|
|
552
573
|
const base64Image = `base64://${buffer.toString("base64")}`;
|
|
553
|
-
|
|
574
|
+
const sent = await sendByTarget(
|
|
575
|
+
bot,
|
|
576
|
+
groupId,
|
|
577
|
+
userId,
|
|
578
|
+
buildSegments(base64Image),
|
|
579
|
+
);
|
|
580
|
+
return sent?.message_id;
|
|
554
581
|
}
|
|
555
582
|
}
|
|
556
583
|
|
|
@@ -559,15 +586,19 @@ async function sendByTarget(
|
|
|
559
586
|
groupId: number | undefined,
|
|
560
587
|
userId: number | undefined,
|
|
561
588
|
segments: readonly any[],
|
|
562
|
-
): Promise<
|
|
589
|
+
): Promise<import("mioku").SentMessage | undefined> {
|
|
563
590
|
if (groupId) {
|
|
564
|
-
await bot.sendMessage(
|
|
565
|
-
|
|
591
|
+
return await bot.sendMessage(
|
|
592
|
+
{ type: "group", group_id: groupId },
|
|
593
|
+
segments,
|
|
594
|
+
);
|
|
566
595
|
}
|
|
567
596
|
|
|
568
597
|
if (userId) {
|
|
569
|
-
await bot.sendMessage(
|
|
570
|
-
|
|
598
|
+
return await bot.sendMessage(
|
|
599
|
+
{ type: "private", user_id: userId },
|
|
600
|
+
segments,
|
|
601
|
+
);
|
|
571
602
|
}
|
|
572
603
|
|
|
573
604
|
throw new Error("No valid message target");
|
|
@@ -605,7 +636,7 @@ async function resolveAudioSource(
|
|
|
605
636
|
options: {
|
|
606
637
|
audioText?: string;
|
|
607
638
|
config: ChatConfig;
|
|
608
|
-
|
|
639
|
+
audioService?: AudioServiceApi;
|
|
609
640
|
},
|
|
610
641
|
): Promise<string | null> {
|
|
611
642
|
const trimmed = String(options.audioText || "").trim();
|
|
@@ -735,7 +766,9 @@ export async function getHumanizeContexts(
|
|
|
735
766
|
topicContext: topicContext || undefined,
|
|
736
767
|
expressionContext: expressionContext || undefined,
|
|
737
768
|
};
|
|
738
|
-
logger.info(
|
|
769
|
+
logger.info(
|
|
770
|
+
`[getHumanizeContexts] session=${groupSessionId} user=${userName} triggerUserId=${triggerUserId} memoryContext=undefined topicContext="${topicContext?.slice(0, 100) ?? ""}" expressionContext="${expressionContext?.slice(0, 100) ?? ""}"`,
|
|
771
|
+
);
|
|
739
772
|
return result;
|
|
740
773
|
}
|
|
741
774
|
|
|
@@ -842,6 +875,7 @@ export function saveBotMessages(
|
|
|
842
875
|
db: ChatDatabase,
|
|
843
876
|
ctx: MiokuContext,
|
|
844
877
|
bot: Bot | undefined,
|
|
878
|
+
sentMessageIds?: Array<string | undefined>,
|
|
845
879
|
): void {
|
|
846
880
|
const botNickname = config.nicknames[0] || (bot?.nickname ?? "Miku");
|
|
847
881
|
const selfId = bot ? Number(bot.bot_id) : 0;
|
|
@@ -851,7 +885,8 @@ export function saveBotMessages(
|
|
|
851
885
|
return;
|
|
852
886
|
}
|
|
853
887
|
|
|
854
|
-
for (
|
|
888
|
+
for (let i = 0; i < messages.length; i++) {
|
|
889
|
+
const msg = messages[i];
|
|
855
890
|
const botMsg: ChatMessage = {
|
|
856
891
|
sessionId: groupSessionId,
|
|
857
892
|
role: "assistant",
|
|
@@ -861,6 +896,7 @@ export function saveBotMessages(
|
|
|
861
896
|
userRole: "member",
|
|
862
897
|
groupId,
|
|
863
898
|
timestamp,
|
|
899
|
+
messageId: sentMessageIds?.[i],
|
|
864
900
|
};
|
|
865
901
|
db.saveMessage(botMsg);
|
|
866
902
|
}
|
|
@@ -874,15 +910,13 @@ export async function sendEmoji(
|
|
|
874
910
|
): Promise<void> {
|
|
875
911
|
if (!emojiPath) return;
|
|
876
912
|
if (!bot) {
|
|
877
|
-
ctx.logger.error(
|
|
878
|
-
`[sendEmoji] bot not available, skip sending emoji`,
|
|
879
|
-
);
|
|
913
|
+
ctx.logger.error(`[sendEmoji] bot not available, skip sending emoji`);
|
|
880
914
|
return;
|
|
881
915
|
}
|
|
882
916
|
|
|
883
917
|
try {
|
|
884
918
|
const emojiSegment = ctx.segment.image(`file://${emojiPath}`);
|
|
885
|
-
await bot.sendMessage({ type: "group", group_id: groupId}, [emojiSegment]);
|
|
919
|
+
await bot.sendMessage({ type: "group", group_id: groupId }, [emojiSegment]);
|
|
886
920
|
} catch (err) {
|
|
887
921
|
try {
|
|
888
922
|
const fsPromises = await import("fs/promises");
|
|
@@ -917,7 +951,9 @@ export async function sendEmoji(
|
|
|
917
951
|
|
|
918
952
|
const base64DataUrl = `data:${mimeType};base64,${base64}`;
|
|
919
953
|
const base64Segment = ctx.segment.image(base64DataUrl);
|
|
920
|
-
await bot.sendMessage({ type: "group", group_id: groupId}, [
|
|
954
|
+
await bot.sendMessage({ type: "group", group_id: groupId }, [
|
|
955
|
+
base64Segment,
|
|
956
|
+
]);
|
|
921
957
|
ctx.logger.info(`[Emoji] Sent via base64: ${path.basename(emojiPath)}`);
|
|
922
958
|
} catch (base64Err) {
|
|
923
959
|
ctx.logger.error(`[Emoji] Base64 also failed: ${base64Err}`);
|
package/core/chat-engine.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AIInstance, SessionToolDefinition } from "mioku";
|
|
2
|
-
import { logger } from "mioku";
|
|
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
|
@@ -41,7 +41,7 @@ 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 =
|
|
@@ -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
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, event?.bot);
|
|
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
|
}
|
|
@@ -80,7 +80,7 @@ export interface GroupNoticeHistorySummary {
|
|
|
80
80
|
userRole: string;
|
|
81
81
|
content: string;
|
|
82
82
|
timestamp: number;
|
|
83
|
-
messageId:
|
|
83
|
+
messageId: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function runHistoryMediaAIRequest<T>(
|
|
@@ -393,7 +393,7 @@ export async function summarizeGroupNotice(
|
|
|
393
393
|
userRole: "member",
|
|
394
394
|
content,
|
|
395
395
|
timestamp: normalizeNoticeTimestamp(notice),
|
|
396
|
-
messageId:
|
|
396
|
+
messageId: String((notice as { message_id?: unknown }).message_id ?? ""),
|
|
397
397
|
};
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -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/multimodal.ts
CHANGED
|
@@ -127,7 +127,7 @@ export type MediaByMessageId =
|
|
|
127
127
|
*/
|
|
128
128
|
export async function getMediaByMessageId(
|
|
129
129
|
ctx: MiokuContext,
|
|
130
|
-
messageId: number,
|
|
130
|
+
messageId: string | number,
|
|
131
131
|
e: any,
|
|
132
132
|
): Promise<MediaByMessageId> {
|
|
133
133
|
try {
|
|
@@ -153,7 +153,7 @@ export async function getMediaByMessageId(
|
|
|
153
153
|
|
|
154
154
|
return null;
|
|
155
155
|
} catch (err) {
|
|
156
|
-
logger.error(
|
|
156
|
+
ctx.logger.error(
|
|
157
157
|
`[multimodal] Failed to get media from message ${messageId}: ${err}`,
|
|
158
158
|
);
|
|
159
159
|
return null;
|
|
@@ -191,7 +191,7 @@ export async function getQuoteImageUrl(
|
|
|
191
191
|
// 提取图片 URL
|
|
192
192
|
return (imageSeg as any).url || (imageSeg as any).data?.url || null;
|
|
193
193
|
} catch (err) {
|
|
194
|
-
logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
|
|
194
|
+
ctx.logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
|
|
195
195
|
return null;
|
|
196
196
|
}
|
|
197
197
|
}
|
package/core/prompt.ts
CHANGED
|
@@ -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) {
|
|
@@ -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(
|
package/core/tools/web.ts
CHANGED
|
@@ -72,6 +72,12 @@ async function fetchGroupHistoryByMessageIdPaging(
|
|
|
72
72
|
const bot = toolCtx.event?.bot;
|
|
73
73
|
if (!bot) return [];
|
|
74
74
|
|
|
75
|
+
const otherBotIds = new Set<string>(
|
|
76
|
+
toolCtx.ctx.bots
|
|
77
|
+
.map((b) => String(b.bot_id))
|
|
78
|
+
.filter((id) => id !== String(bot.bot_id)),
|
|
79
|
+
);
|
|
80
|
+
|
|
75
81
|
const collected: ChatMessage[] = [];
|
|
76
82
|
const seenMessageIds = new Set<string>();
|
|
77
83
|
let cursorMessageId = "";
|
|
@@ -113,6 +119,9 @@ async function fetchGroupHistoryByMessageIdPaging(
|
|
|
113
119
|
let newAdded = 0;
|
|
114
120
|
|
|
115
121
|
for (const raw of history) {
|
|
122
|
+
const rawUserId = String(raw.user_id || "");
|
|
123
|
+
if (rawUserId !== "" && otherBotIds.has(rawUserId)) continue;
|
|
124
|
+
|
|
116
125
|
const messageId = String(raw.message_id || "");
|
|
117
126
|
const key =
|
|
118
127
|
messageId !== ""
|
|
@@ -134,7 +143,7 @@ async function fetchGroupHistoryByMessageIdPaging(
|
|
|
134
143
|
userRole: "member",
|
|
135
144
|
groupId: toolCtx.groupId,
|
|
136
145
|
timestamp: ts,
|
|
137
|
-
messageId: messageId !== "" ?
|
|
146
|
+
messageId: messageId !== "" ? messageId : undefined,
|
|
138
147
|
});
|
|
139
148
|
newAdded += 1;
|
|
140
149
|
|
|
@@ -156,7 +165,7 @@ async function fetchGroupHistoryByMessageIdPaging(
|
|
|
156
165
|
|
|
157
166
|
collected.sort((a, b) => {
|
|
158
167
|
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
|
|
159
|
-
return (a.messageId
|
|
168
|
+
return String(a.messageId ?? "").localeCompare(String(b.messageId ?? ""));
|
|
160
169
|
});
|
|
161
170
|
|
|
162
171
|
return collected.length > limit ? collected.slice(-limit) : collected;
|
package/db.ts
CHANGED
|
@@ -24,7 +24,7 @@ function toChatMessage(row: any): ChatMessage {
|
|
|
24
24
|
groupId: row.group_id,
|
|
25
25
|
groupName: row.group_name,
|
|
26
26
|
timestamp: row.timestamp,
|
|
27
|
-
messageId: row.message_id,
|
|
27
|
+
messageId: row.message_id != null ? String(row.message_id) : undefined,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -174,7 +174,7 @@ export async function initDatabase(): Promise<ChatDatabase> {
|
|
|
174
174
|
group_id INTEGER,
|
|
175
175
|
group_name TEXT,
|
|
176
176
|
timestamp INTEGER NOT NULL,
|
|
177
|
-
message_id
|
|
177
|
+
message_id TEXT
|
|
178
178
|
);
|
|
179
179
|
|
|
180
180
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
|
@@ -452,7 +452,10 @@ export async function initDatabase(): Promise<ChatDatabase> {
|
|
|
452
452
|
$groupId: msg.groupId ?? null,
|
|
453
453
|
$groupName: msg.groupName ?? null,
|
|
454
454
|
$timestamp: msg.timestamp,
|
|
455
|
-
$messageId:
|
|
455
|
+
$messageId:
|
|
456
|
+
msg.messageId == null || msg.messageId === ""
|
|
457
|
+
? null
|
|
458
|
+
: String(msg.messageId),
|
|
456
459
|
});
|
|
457
460
|
},
|
|
458
461
|
|
package/handlers/idle-debug.ts
CHANGED
|
@@ -64,7 +64,7 @@ export async function handleIdleCheckDebug(
|
|
|
64
64
|
userId: 0,
|
|
65
65
|
userRole: "member",
|
|
66
66
|
content: "[No one in the group is talking? I'll answer!]",
|
|
67
|
-
messageId:
|
|
67
|
+
messageId: "",
|
|
68
68
|
timestamp: now,
|
|
69
69
|
};
|
|
70
70
|
const botRole = await getBotRole(targetGroupId, ctx, e.self_id);
|
package/manage/cooldown.ts
CHANGED
package/manage/idle-check.ts
CHANGED
|
@@ -237,7 +237,7 @@ export class IdleCheckManager {
|
|
|
237
237
|
userId: 0,
|
|
238
238
|
userRole: "member",
|
|
239
239
|
content: "[No one in the group is talking? I'll answer!]",
|
|
240
|
-
messageId:
|
|
240
|
+
messageId: "",
|
|
241
241
|
timestamp: scheduledAt,
|
|
242
242
|
};
|
|
243
243
|
const botRole = await getBotRole(groupId, this.ctx, selfId);
|
|
@@ -25,7 +25,7 @@ import { finalizeChatTurn } from "../core/chat-turn";
|
|
|
25
25
|
import { extractContent, getBotRole } from "../utils";
|
|
26
26
|
|
|
27
27
|
interface DynamicDelayQueueData {
|
|
28
|
-
messages: Array<{ event: any; content: string; userName: string; userId: number; messageId:
|
|
28
|
+
messages: Array<{ event: any; content: string; userName: string; userId: number; messageId: string; timestamp: number }>;
|
|
29
29
|
timer: NodeJS.Timeout | null;
|
|
30
30
|
delayUntil: number;
|
|
31
31
|
}
|
package/manage/types.ts
CHANGED
|
@@ -66,7 +66,7 @@ export type GetHumanizeContexts = (
|
|
|
66
66
|
export type SendAIResponse = (
|
|
67
67
|
options: SendAIResponseOptions,
|
|
68
68
|
selfId: number,
|
|
69
|
-
) => Promise<
|
|
69
|
+
) => Promise<Array<string | undefined>>;
|
|
70
70
|
|
|
71
71
|
export type SaveBotMessages = (
|
|
72
72
|
groupId: number,
|
|
@@ -77,6 +77,7 @@ export type SaveBotMessages = (
|
|
|
77
77
|
db: ChatDatabase,
|
|
78
78
|
ctx: MiokuContext,
|
|
79
79
|
bot?: Bot,
|
|
80
|
+
sentMessageIds?: Array<string | undefined>,
|
|
80
81
|
) => void;
|
|
81
82
|
|
|
82
83
|
export type SendEmoji = (
|
|
@@ -146,7 +147,7 @@ export type PromptCtxForRunChat = {
|
|
|
146
147
|
reviewMessages?: {
|
|
147
148
|
contents: string[];
|
|
148
149
|
userNames: string[];
|
|
149
|
-
messageIds:
|
|
150
|
+
messageIds: string[];
|
|
150
151
|
};
|
|
151
152
|
promptInjections?: ChatRuntimePromptInjection[];
|
|
152
153
|
emojiAgent?: EmojiAgent;
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -270,7 +270,7 @@ export interface ChatMessage {
|
|
|
270
270
|
groupId?: number;
|
|
271
271
|
groupName?: string;
|
|
272
272
|
timestamp: number;
|
|
273
|
-
messageId?:
|
|
273
|
+
messageId?: string; // 消息 id
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
/**
|
|
@@ -282,7 +282,7 @@ export interface TargetMessage {
|
|
|
282
282
|
userRole: string;
|
|
283
283
|
userTitle?: string;
|
|
284
284
|
content: string;
|
|
285
|
-
messageId?:
|
|
285
|
+
messageId?: string;
|
|
286
286
|
timestamp: number;
|
|
287
287
|
}
|
|
288
288
|
|
|
@@ -336,7 +336,7 @@ export interface ChatResult {
|
|
|
336
336
|
messages: string[];
|
|
337
337
|
pendingAt: number[];
|
|
338
338
|
pendingPoke: number[];
|
|
339
|
-
pendingQuote?:
|
|
339
|
+
pendingQuote?: string;
|
|
340
340
|
toolCalls: { name: string; args: any; result: any }[];
|
|
341
341
|
emojiPath?: string | null;
|
|
342
342
|
protocolMessages?: ChatCompletionMessageParam[];
|
package/utils/message.ts
CHANGED
|
@@ -56,7 +56,7 @@ export async function isQuotingBot(
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
} catch (err) {
|
|
59
|
-
logger.error(err);
|
|
59
|
+
ctx.logger.error(err);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return null;
|
|
@@ -214,7 +214,7 @@ interface FormattedHistoryMessage {
|
|
|
214
214
|
userName: string;
|
|
215
215
|
userRole: string;
|
|
216
216
|
content: string;
|
|
217
|
-
messageId:
|
|
217
|
+
messageId: string;
|
|
218
218
|
timestamp: number;
|
|
219
219
|
role: "user" | "assistant";
|
|
220
220
|
}
|
|
@@ -229,6 +229,8 @@ interface HistoryFormatContext {
|
|
|
229
229
|
| undefined;
|
|
230
230
|
historyMediaOptions: HistoryMediaProcessingOptions;
|
|
231
231
|
botUin: number;
|
|
232
|
+
/** 本运行时全部已连接 bot 的 id:这些账号的消息一律不作为用户历史(防 bot 互触循环) */
|
|
233
|
+
botIds: Set<string>;
|
|
232
234
|
memberNameCache: Map<string, string>;
|
|
233
235
|
hashLookupCache: Map<string, string | null>;
|
|
234
236
|
}
|
|
@@ -258,6 +260,8 @@ async function resolveMemberName(
|
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
function extractQuotedText(messageSegs: any): string {
|
|
263
|
+
// icqq 的 source.message 是 brief 字符串;onebot 是 segment 数组
|
|
264
|
+
if (typeof messageSegs === "string") return messageSegs.trim();
|
|
261
265
|
if (!Array.isArray(messageSegs) || messageSegs.length === 0) return "";
|
|
262
266
|
const parts: string[] = [];
|
|
263
267
|
for (const seg of messageSegs) {
|
|
@@ -347,7 +351,10 @@ async function formatHistoryMessage(
|
|
|
347
351
|
msg: HistoryMessage,
|
|
348
352
|
ctx: HistoryFormatContext,
|
|
349
353
|
): Promise<FormattedHistoryMessage | null> {
|
|
354
|
+
// 自己或本运行时其它 bot 的消息不作为用户历史,
|
|
355
|
+
// 否则多 bot 同群时会把彼此的消息当作用户接话,形成循环触发
|
|
350
356
|
if (String(msg.user_id) === String(ctx.botUin)) return null;
|
|
357
|
+
if (ctx.botIds.has(String(msg.user_id))) return null;
|
|
351
358
|
|
|
352
359
|
let content = "";
|
|
353
360
|
try {
|
|
@@ -364,7 +371,10 @@ async function formatHistoryMessage(
|
|
|
364
371
|
userName: msg.nickname || String(msg.user_id || "unknown"),
|
|
365
372
|
userRole: "member",
|
|
366
373
|
content,
|
|
367
|
-
messageId:
|
|
374
|
+
messageId:
|
|
375
|
+
msg.message_id != null && msg.message_id !== ""
|
|
376
|
+
? String(msg.message_id)
|
|
377
|
+
: "",
|
|
368
378
|
timestamp: msg.time ?? Date.now(),
|
|
369
379
|
role: "user",
|
|
370
380
|
};
|
|
@@ -524,7 +534,7 @@ export async function getGroupHistory(
|
|
|
524
534
|
userName: string;
|
|
525
535
|
userRole: string;
|
|
526
536
|
content: string;
|
|
527
|
-
messageId:
|
|
537
|
+
messageId: string;
|
|
528
538
|
timestamp: number;
|
|
529
539
|
role: "user" | "assistant";
|
|
530
540
|
}>
|
|
@@ -535,7 +545,7 @@ export async function getGroupHistory(
|
|
|
535
545
|
userName: string;
|
|
536
546
|
userRole: string;
|
|
537
547
|
content: string;
|
|
538
|
-
messageId:
|
|
548
|
+
messageId: string;
|
|
539
549
|
timestamp: number;
|
|
540
550
|
role: "user" | "assistant";
|
|
541
551
|
}> = [];
|
|
@@ -548,7 +558,7 @@ export async function getGroupHistory(
|
|
|
548
558
|
userName: msg.userName || "Miku",
|
|
549
559
|
userRole: msg.userRole || "member",
|
|
550
560
|
content: msg.content,
|
|
551
|
-
messageId: msg.messageId ??
|
|
561
|
+
messageId: msg.messageId ?? "",
|
|
552
562
|
timestamp: msg.timestamp,
|
|
553
563
|
role: "assistant",
|
|
554
564
|
});
|
|
@@ -562,7 +572,7 @@ export async function getGroupHistory(
|
|
|
562
572
|
userName: msg.userName || String(msg.userId || "unknown"),
|
|
563
573
|
userRole: msg.userRole || "member",
|
|
564
574
|
content: msg.content,
|
|
565
|
-
messageId: msg.messageId ??
|
|
575
|
+
messageId: msg.messageId ?? "",
|
|
566
576
|
timestamp: msg.timestamp,
|
|
567
577
|
role: "user",
|
|
568
578
|
});
|
|
@@ -596,6 +606,8 @@ export async function getGroupHistory(
|
|
|
596
606
|
: [];
|
|
597
607
|
|
|
598
608
|
const botUin = selfId;
|
|
609
|
+
const botIds = new Set<string>(ctx.bots.map((b) => String(b.bot_id)));
|
|
610
|
+
botIds.add(String(botUin));
|
|
599
611
|
const memberNameCache = new Map<string, string>();
|
|
600
612
|
const hashLookupCache = new Map<string, string | null>();
|
|
601
613
|
|
|
@@ -611,6 +623,7 @@ export async function getGroupHistory(
|
|
|
611
623
|
db,
|
|
612
624
|
historyMediaOptions,
|
|
613
625
|
botUin,
|
|
626
|
+
botIds,
|
|
614
627
|
memberNameCache,
|
|
615
628
|
hashLookupCache,
|
|
616
629
|
};
|
package/utils/queue.ts
CHANGED
|
@@ -100,7 +100,7 @@ export class MessageQueueManager {
|
|
|
100
100
|
* 例如 "[reply:1]文字A[reply:2]文字B" → ["[reply:1]文字A", "[reply:2]文字B"]
|
|
101
101
|
*/
|
|
102
102
|
export function splitByReplyMarkers(line: string): string[] {
|
|
103
|
-
const parts = line.split(/(?=\[reply
|
|
103
|
+
const parts = line.split(/(?=\[reply:[^\]\n]+\])/);
|
|
104
104
|
return parts.filter((p) => p.trim());
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -116,12 +116,12 @@ export function parseLineMarkers(
|
|
|
116
116
|
cleanText: string;
|
|
117
117
|
atUsers: number[];
|
|
118
118
|
pokeUsers: number[];
|
|
119
|
-
quoteId?:
|
|
119
|
+
quoteId?: string;
|
|
120
120
|
audioText?: string;
|
|
121
121
|
} {
|
|
122
122
|
const atUsers: number[] = [];
|
|
123
123
|
const pokeUsers: number[] = [];
|
|
124
|
-
let quoteId:
|
|
124
|
+
let quoteId: string | undefined;
|
|
125
125
|
let audioText: string | undefined;
|
|
126
126
|
|
|
127
127
|
// 提取 AT 标记
|
|
@@ -146,12 +146,13 @@ export function parseLineMarkers(
|
|
|
146
146
|
|
|
147
147
|
// 提取引用标记(仅在允许时)
|
|
148
148
|
if (quoteMode !== "skip") {
|
|
149
|
-
const replyPatterns = [/\[reply:(
|
|
149
|
+
const replyPatterns = [/\[reply:([^\]\n]+)\]/g];
|
|
150
150
|
for (const pattern of replyPatterns) {
|
|
151
151
|
const matches = [...line.matchAll(pattern)];
|
|
152
152
|
for (const match of matches) {
|
|
153
153
|
if (quoteId === undefined) {
|
|
154
|
-
|
|
154
|
+
const value = match[1].trim();
|
|
155
|
+
if (value) quoteId = value;
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -170,7 +171,7 @@ export function parseLineMarkers(
|
|
|
170
171
|
let cleanText = line
|
|
171
172
|
.replace(/\[at:\d+\]/g, "")
|
|
172
173
|
.replace(/\[poke:\d+\]/g, "")
|
|
173
|
-
.replace(/\[reply
|
|
174
|
+
.replace(/\[reply:[^\]\n]+\]/g, "")
|
|
174
175
|
.replace(/\[audio:[^\]]+\]/gi, "")
|
|
175
176
|
.trim();
|
|
176
177
|
|