mioku-plugin-chat 3.0.6 → 3.1.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/context.ts +5 -5
- package/core/base.ts +20 -20
- package/core/chat-engine.ts +14 -10
- package/core/chat-turn.ts +109 -37
- package/core/media/history-media.ts +14 -14
- package/core/media/image-compress.ts +38 -27
- package/core/media/segment.ts +2 -2
- package/core/prompt.ts +1 -1
- package/core/tools/info.ts +14 -7
- package/core/tools/web.ts +6 -9
- package/db.ts +13 -13
- package/handlers/idle-debug.ts +7 -7
- package/handlers/message.ts +6 -8
- package/handlers/poke.ts +15 -14
- package/humanize/emotion-agent.ts +5 -5
- package/humanize/expression.ts +6 -6
- package/humanize/index.ts +1 -1
- package/humanize/memory.ts +1 -1
- package/index.ts +52 -28
- package/manage/cooldown.ts +8 -8
- package/manage/group-structured-history.ts +2 -2
- package/manage/idle-check.ts +12 -11
- package/manage/queue-processor.ts +9 -8
- package/manage/rate-limit-guard.ts +2 -2
- package/manage/rate-limiter.ts +21 -21
- package/manage/session.ts +1 -1
- package/manage/types.ts +16 -16
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +4 -4
- package/types.ts +11 -11
- package/utils/config.ts +4 -4
- package/utils/group-config.ts +3 -4
- package/utils/index.ts +2 -0
- package/utils/message.ts +112 -47
- package/utils/model.ts +31 -0
- package/utils/queue.ts +12 -11
package/utils/message.ts
CHANGED
|
@@ -111,7 +111,7 @@ export async function getQuotedContent(
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export function isGroupAllowed(groupId:
|
|
114
|
+
export function isGroupAllowed(groupId: string, cfg: ChatConfig): boolean {
|
|
115
115
|
if (cfg.whitelistGroups.length > 0) {
|
|
116
116
|
return cfg.whitelistGroups.includes(groupId);
|
|
117
117
|
}
|
|
@@ -190,9 +190,9 @@ async function mapWithConcurrency<T, R>(
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
export async function getBotRole(
|
|
193
|
-
groupId:
|
|
193
|
+
groupId: string,
|
|
194
194
|
ctx: MiokuContext,
|
|
195
|
-
selfId:
|
|
195
|
+
selfId: string,
|
|
196
196
|
): Promise<"owner" | "admin" | "member"> {
|
|
197
197
|
try {
|
|
198
198
|
const bot = ctx.pickBot(selfId);
|
|
@@ -205,7 +205,7 @@ export async function getBotRole(
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
interface FormattedHistoryMessage {
|
|
208
|
-
userId:
|
|
208
|
+
userId: string;
|
|
209
209
|
userName: string;
|
|
210
210
|
userRole: string;
|
|
211
211
|
content: string;
|
|
@@ -223,7 +223,7 @@ interface HistoryFormatContext {
|
|
|
223
223
|
}
|
|
224
224
|
| undefined;
|
|
225
225
|
historyMediaOptions: HistoryMediaProcessingOptions;
|
|
226
|
-
botUin:
|
|
226
|
+
botUin: string;
|
|
227
227
|
/** 本运行时全部已连接 bot 的 id:这些账号的消息一律不作为用户历史(防 bot 互触循环) */
|
|
228
228
|
botIds: Set<string>;
|
|
229
229
|
memberNameCache: Map<string, string>;
|
|
@@ -283,10 +283,17 @@ function extractQuotedText(messageSegs: any): string {
|
|
|
283
283
|
return parts.join(" ").trim();
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
/** 引用关系:id 必有,发送者与原文可能缺失(例如 onebot 历史只给 id) */
|
|
287
|
+
interface ReplyRef {
|
|
288
|
+
id: string;
|
|
289
|
+
name?: string;
|
|
290
|
+
text?: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function extractReplyRef(
|
|
287
294
|
source: any,
|
|
288
295
|
ctx: HistoryFormatContext,
|
|
289
|
-
):
|
|
296
|
+
): ReplyRef | null {
|
|
290
297
|
if (!source || typeof source !== "object") return null;
|
|
291
298
|
const sourceId = source.id ?? source.message_id ?? source.message_seq;
|
|
292
299
|
const sourceUserId = source.user_id;
|
|
@@ -294,18 +301,38 @@ function buildReplyAnnotation(
|
|
|
294
301
|
source.sender?.card || source.sender?.nickname || source.nickname;
|
|
295
302
|
const sourceText = extractQuotedText(source.message);
|
|
296
303
|
|
|
297
|
-
if (
|
|
304
|
+
if (
|
|
305
|
+
sourceId == null &&
|
|
306
|
+
sourceUserId == null &&
|
|
307
|
+
!sourceText &&
|
|
308
|
+
!sourceNickname
|
|
309
|
+
) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
298
312
|
|
|
299
|
-
|
|
300
|
-
let displayName: string | undefined;
|
|
313
|
+
let name: string | undefined;
|
|
301
314
|
if (sourceUserId != null) {
|
|
302
|
-
|
|
303
|
-
ctx.memberNameCache.get(String(sourceUserId)) || String(sourceUserId);
|
|
315
|
+
name = ctx.memberNameCache.get(String(sourceUserId)) || String(sourceUserId);
|
|
304
316
|
}
|
|
305
|
-
if (!
|
|
317
|
+
if (!name) name = sourceNickname || undefined;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
id: sourceId != null ? String(sourceId) : "",
|
|
321
|
+
name,
|
|
322
|
+
text: sourceText || undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderReplyRef(ref: ReplyRef): string {
|
|
327
|
+
const head = `↪ reply to #${ref.id || "?"}`;
|
|
328
|
+
if (!ref.name && !ref.text) return head;
|
|
329
|
+
if (!ref.text) return `${head} ${ref.name}`;
|
|
330
|
+
return ref.name ? `${head} ${ref.name}: "${ref.text}"` : `${head}: "${ref.text}"`;
|
|
331
|
+
}
|
|
306
332
|
|
|
307
|
-
|
|
308
|
-
|
|
333
|
+
function summarizeQuotedContent(content: string): string {
|
|
334
|
+
const flat = content.replace(/\s+/g, " ").trim();
|
|
335
|
+
return flat.length > 80 ? `${flat.slice(0, 80)}…` : flat;
|
|
309
336
|
}
|
|
310
337
|
|
|
311
338
|
async function getImageTagWithHashCache(
|
|
@@ -345,12 +372,14 @@ async function getImageTagWithHashCache(
|
|
|
345
372
|
async function formatHistoryMessage(
|
|
346
373
|
msg: HistoryMessage,
|
|
347
374
|
ctx: HistoryFormatContext,
|
|
348
|
-
): Promise<FormattedHistoryMessage | null> {
|
|
375
|
+
): Promise<{ formatted: FormattedHistoryMessage; reply: ReplyRef | null } | null> {
|
|
349
376
|
// 自己或本运行时其它 bot 的消息不作为用户历史,
|
|
350
377
|
// 否则多 bot 同群时会把彼此的消息当作用户接话,形成循环触发
|
|
351
378
|
if (String(msg.user_id) === String(ctx.botUin)) return null;
|
|
352
379
|
if (ctx.botIds.has(String(msg.user_id))) return null;
|
|
353
380
|
|
|
381
|
+
const reply = extractReplyRef((msg as { source?: unknown }).source, ctx);
|
|
382
|
+
|
|
354
383
|
let content = "";
|
|
355
384
|
try {
|
|
356
385
|
content = await buildMessageContent(msg, ctx);
|
|
@@ -359,19 +388,22 @@ async function formatHistoryMessage(
|
|
|
359
388
|
return null;
|
|
360
389
|
}
|
|
361
390
|
|
|
362
|
-
if (!content.trim()) return null;
|
|
391
|
+
if (!content.trim() && !reply) return null;
|
|
363
392
|
|
|
364
393
|
return {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
formatted: {
|
|
395
|
+
userId: String(msg.user_id ?? "").trim(),
|
|
396
|
+
userName: msg.nickname || String(msg.user_id ?? "unknown"),
|
|
397
|
+
userRole: "member",
|
|
398
|
+
content,
|
|
399
|
+
messageId:
|
|
400
|
+
msg.message_id != null && msg.message_id !== ""
|
|
401
|
+
? String(msg.message_id)
|
|
402
|
+
: "",
|
|
403
|
+
timestamp: msg.time ?? Date.now(),
|
|
404
|
+
role: "user",
|
|
405
|
+
},
|
|
406
|
+
reply,
|
|
375
407
|
};
|
|
376
408
|
}
|
|
377
409
|
|
|
@@ -385,9 +417,6 @@ async function buildMessageContent(
|
|
|
385
417
|
const { db, historyMediaOptions } = ctx;
|
|
386
418
|
const parts: string[] = [];
|
|
387
419
|
|
|
388
|
-
const replyAnnotation = buildReplyAnnotation(msg.source, ctx);
|
|
389
|
-
if (replyAnnotation) parts.push(replyAnnotation);
|
|
390
|
-
|
|
391
420
|
const textSegs = msg.message.filter((seg: any) => seg.type === "text");
|
|
392
421
|
const textContent = textSegs
|
|
393
422
|
.map((seg: any) => seg.data?.text || "")
|
|
@@ -501,12 +530,12 @@ async function buildMessageContent(
|
|
|
501
530
|
* 返回格式化为 ChatMessage 数组
|
|
502
531
|
*/
|
|
503
532
|
export async function getGroupHistory(
|
|
504
|
-
groupId:
|
|
533
|
+
groupId: string,
|
|
505
534
|
ctx: MiokuContext,
|
|
506
535
|
count: number = 100,
|
|
507
|
-
selfId:
|
|
536
|
+
selfId: string,
|
|
508
537
|
db?: {
|
|
509
|
-
getBotMessages(groupId:
|
|
538
|
+
getBotMessages(groupId: string, limit: number): ChatMessage[];
|
|
510
539
|
getImageByHash?(hash: string): ImageRecord | null;
|
|
511
540
|
getImageByUrl?(url: string): ImageRecord | null;
|
|
512
541
|
getMediaSummary?(key: string): MediaSummaryRecord | null;
|
|
@@ -514,7 +543,7 @@ export async function getGroupHistory(
|
|
|
514
543
|
getMediaSummaryBySource?(sourceKey: string): MediaSummaryRecord | null;
|
|
515
544
|
saveMediaSummarySource?(sourceKey: string, summaryKey: string): void;
|
|
516
545
|
getStoredGroupNoticeMessages?(
|
|
517
|
-
groupId:
|
|
546
|
+
groupId: string,
|
|
518
547
|
limit?: number,
|
|
519
548
|
): ChatMessage[];
|
|
520
549
|
},
|
|
@@ -525,7 +554,7 @@ export async function getGroupHistory(
|
|
|
525
554
|
},
|
|
526
555
|
): Promise<
|
|
527
556
|
Array<{
|
|
528
|
-
userId:
|
|
557
|
+
userId: string;
|
|
529
558
|
userName: string;
|
|
530
559
|
userRole: string;
|
|
531
560
|
content: string;
|
|
@@ -536,7 +565,7 @@ export async function getGroupHistory(
|
|
|
536
565
|
> {
|
|
537
566
|
// 先获取 bot 从数据库发送的消息
|
|
538
567
|
const botMessages: Array<{
|
|
539
|
-
userId:
|
|
568
|
+
userId: string;
|
|
540
569
|
userName: string;
|
|
541
570
|
userRole: string;
|
|
542
571
|
content: string;
|
|
@@ -549,7 +578,7 @@ export async function getGroupHistory(
|
|
|
549
578
|
const storedBotMessages = db.getBotMessages(groupId, count);
|
|
550
579
|
for (const msg of storedBotMessages) {
|
|
551
580
|
botMessages.push({
|
|
552
|
-
userId: msg.userId ??
|
|
581
|
+
userId: String(msg.userId ?? ""),
|
|
553
582
|
userName: msg.userName || "Miku",
|
|
554
583
|
userRole: msg.userRole || "member",
|
|
555
584
|
content: msg.content,
|
|
@@ -563,8 +592,8 @@ export async function getGroupHistory(
|
|
|
563
592
|
db.getStoredGroupNoticeMessages?.(groupId, Math.min(count, 20)) || [];
|
|
564
593
|
for (const msg of storedNoticeMessages) {
|
|
565
594
|
botMessages.push({
|
|
566
|
-
userId: msg.userId ??
|
|
567
|
-
userName: msg.userName || String(msg.userId
|
|
595
|
+
userId: String(msg.userId ?? ""),
|
|
596
|
+
userName: msg.userName || String(msg.userId ?? "unknown"),
|
|
568
597
|
userRole: msg.userRole || "member",
|
|
569
598
|
content: msg.content,
|
|
570
599
|
messageId: msg.messageId ?? "",
|
|
@@ -623,15 +652,51 @@ export async function getGroupHistory(
|
|
|
623
652
|
hashLookupCache,
|
|
624
653
|
};
|
|
625
654
|
|
|
626
|
-
const
|
|
655
|
+
const processed = await mapWithConcurrency(
|
|
627
656
|
messages,
|
|
628
657
|
HISTORY_MEDIA_CONCURRENCY,
|
|
629
658
|
(msg: any) => formatHistoryMessage(msg, formatCtx),
|
|
630
659
|
);
|
|
631
|
-
const
|
|
632
|
-
(
|
|
660
|
+
const entries = processed.filter(
|
|
661
|
+
(
|
|
662
|
+
item,
|
|
663
|
+
): item is {
|
|
664
|
+
formatted: FormattedHistoryMessage;
|
|
665
|
+
reply: ReplyRef | null;
|
|
666
|
+
} => Boolean(item),
|
|
633
667
|
);
|
|
634
668
|
|
|
669
|
+
// 引用关系单独渲染:被引用的消息若也在本批历史里,直接补上发送者与原文
|
|
670
|
+
const byMessageId = new Map<string, FormattedHistoryMessage>();
|
|
671
|
+
for (const item of entries) {
|
|
672
|
+
if (item.formatted.messageId) {
|
|
673
|
+
byMessageId.set(item.formatted.messageId, item.formatted);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const botMessage of botMessages) {
|
|
677
|
+
if (botMessage.messageId && !byMessageId.has(botMessage.messageId)) {
|
|
678
|
+
byMessageId.set(botMessage.messageId, botMessage);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const formatted = entries.map(({ formatted: message, reply }) => {
|
|
683
|
+
if (!reply) return message;
|
|
684
|
+
const quoted = reply.id ? byMessageId.get(reply.id) : undefined;
|
|
685
|
+
const annotation = renderReplyRef({
|
|
686
|
+
...reply,
|
|
687
|
+
name: reply.name ?? quoted?.userName,
|
|
688
|
+
text:
|
|
689
|
+
reply.text ??
|
|
690
|
+
(quoted ? summarizeQuotedContent(quoted.content) || undefined : undefined),
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
...message,
|
|
694
|
+
content: message.content
|
|
695
|
+
? `${annotation} ${message.content}`
|
|
696
|
+
: annotation,
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
|
|
635
700
|
// 合并 bot 消息
|
|
636
701
|
const allMessages = [...botMessages, ...formatted];
|
|
637
702
|
allMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -652,9 +717,9 @@ export function buildChatMessageFromEvent(
|
|
|
652
717
|
e: any,
|
|
653
718
|
text: string,
|
|
654
719
|
isGroup: boolean,
|
|
655
|
-
groupId:
|
|
720
|
+
groupId: string | undefined,
|
|
656
721
|
): ChatMessage {
|
|
657
|
-
const userId:
|
|
722
|
+
const userId: string = e.user_id || e.sender?.user_id;
|
|
658
723
|
return {
|
|
659
724
|
sessionId: groupId ? `group:${groupId}` : `personal:${userId}`,
|
|
660
725
|
role: "user" as const,
|
|
@@ -670,13 +735,13 @@ export function buildChatMessageFromEvent(
|
|
|
670
735
|
};
|
|
671
736
|
}
|
|
672
737
|
|
|
673
|
-
export function normalizeIdList(input: unknown):
|
|
738
|
+
export function normalizeIdList(input: unknown): string[] {
|
|
674
739
|
if (!Array.isArray(input)) return [];
|
|
675
740
|
return Array.from(
|
|
676
741
|
new Set(
|
|
677
742
|
input
|
|
678
|
-
.map((item) =>
|
|
679
|
-
.filter((id) =>
|
|
743
|
+
.map((item) => String(item ?? "").trim())
|
|
744
|
+
.filter((id) => id.length > 0),
|
|
680
745
|
),
|
|
681
746
|
);
|
|
682
747
|
}
|
package/utils/model.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AIModelDescriptor, AIService } from "mioku";
|
|
2
|
+
|
|
3
|
+
const supportsVision = (model?: AIModelDescriptor): boolean =>
|
|
4
|
+
Boolean(model?.capabilities?.includes("vision"));
|
|
5
|
+
|
|
6
|
+
export function mainModelSupportsVision(
|
|
7
|
+
aiService: AIService | undefined,
|
|
8
|
+
fallbackModelId?: string,
|
|
9
|
+
): boolean {
|
|
10
|
+
if (!aiService) return false;
|
|
11
|
+
|
|
12
|
+
const models = aiService.listModels?.() ?? [];
|
|
13
|
+
const boundId = aiService.getRoleBindings?.()?.main;
|
|
14
|
+
if (boundId) {
|
|
15
|
+
const bound = models.find((item) => item.id === boundId);
|
|
16
|
+
if (bound) return supportsVision(bound);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const instance = (aiService.listInstances?.() ?? []).find(
|
|
20
|
+
(item) => item.role === "main" || item.name === "main",
|
|
21
|
+
);
|
|
22
|
+
const modelId = fallbackModelId || instance?.modelId || "";
|
|
23
|
+
if (!modelId) return false;
|
|
24
|
+
|
|
25
|
+
return supportsVision(
|
|
26
|
+
models.find(
|
|
27
|
+
(item) =>
|
|
28
|
+
item.modelId === modelId && item.providerId === instance?.providerId,
|
|
29
|
+
) ?? models.find((item) => item.modelId === modelId),
|
|
30
|
+
);
|
|
31
|
+
}
|
package/utils/queue.ts
CHANGED
|
@@ -114,33 +114,34 @@ export function parseLineMarkers(
|
|
|
114
114
|
quoteMode?: "skip",
|
|
115
115
|
): {
|
|
116
116
|
cleanText: string;
|
|
117
|
-
atUsers:
|
|
118
|
-
pokeUsers:
|
|
117
|
+
atUsers: string[];
|
|
118
|
+
pokeUsers: string[];
|
|
119
119
|
quoteId?: string;
|
|
120
120
|
audioText?: string;
|
|
121
121
|
} {
|
|
122
|
-
const atUsers:
|
|
123
|
-
const pokeUsers:
|
|
122
|
+
const atUsers: string[] = [];
|
|
123
|
+
const pokeUsers: string[] = [];
|
|
124
124
|
let quoteId: string | undefined;
|
|
125
125
|
let audioText: string | undefined;
|
|
126
126
|
|
|
127
127
|
// 提取 AT 标记
|
|
128
|
-
|
|
128
|
+
// 标记里是平台用户 id:QQ 号或 openid 都可能出现
|
|
129
|
+
const atPatterns = [/\[at:([^\]\s]+)\]/g];
|
|
129
130
|
for (const pattern of atPatterns) {
|
|
130
131
|
const matches = [...line.matchAll(pattern)];
|
|
131
132
|
for (const match of matches) {
|
|
132
|
-
const userId =
|
|
133
|
-
atUsers.push(userId);
|
|
133
|
+
const userId = String(match[1]).trim();
|
|
134
|
+
if (userId) atUsers.push(userId);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// 提取戳人标记
|
|
138
|
-
const pokePatterns = [/\[poke:(\
|
|
139
|
+
const pokePatterns = [/\[poke:([^\]\s]+)\]/g];
|
|
139
140
|
for (const pattern of pokePatterns) {
|
|
140
141
|
const matches = [...line.matchAll(pattern)];
|
|
141
142
|
for (const match of matches) {
|
|
142
|
-
const userId =
|
|
143
|
-
pokeUsers.push(userId);
|
|
143
|
+
const userId = String(match[1]).trim();
|
|
144
|
+
if (userId) pokeUsers.push(userId);
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -169,7 +170,7 @@ export function parseLineMarkers(
|
|
|
169
170
|
|
|
170
171
|
// 清理标记
|
|
171
172
|
let cleanText = line
|
|
172
|
-
.replace(/\[at
|
|
173
|
+
.replace(/\[at:[^\]\s]+\]/g, "")
|
|
173
174
|
.replace(/\[poke:\d+\]/g, "")
|
|
174
175
|
.replace(/\[reply:[^\]\n]+\]/g, "")
|
|
175
176
|
.replace(/\[audio:[^\]]+\]/gi, "")
|