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/utils/message.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { logger, MiokiContext } from "mioki";
2
- import type { AIInstance } from "mioku";
1
+ import { HistoryMessage, logger, type MiokuContext } from "mioku";
2
+ import type { AIInstance, Bot } from "mioku";
3
+
3
4
  import type {
4
5
  ChatConfig,
5
6
  ChatMessage,
@@ -22,13 +23,15 @@ export function shouldTrigger(
22
23
  e: any,
23
24
  text: string,
24
25
  cfg: ChatConfig,
25
- ctx: MiokiContext,
26
+ ctx: MiokuContext,
26
27
  ): boolean {
27
28
  if (e.message_type === "private") return false;
28
29
 
29
- // Only check if message @s the bot (seg format: {type: "at", qq: "123456"})
30
30
  const atSeg = e.message?.find((seg: any) => seg.type === "at");
31
- return !!(atSeg && String(atSeg.qq) === String(e.self_id));
31
+ if (!atSeg) return false;
32
+ const data = (atSeg.data ?? {}) as { qq?: unknown; target?: unknown };
33
+ const qq = data.qq ?? data.target;
34
+ return String(qq ?? "") === String(e.self_id ?? "");
32
35
  }
33
36
 
34
37
  /**
@@ -37,15 +40,15 @@ export function shouldTrigger(
37
40
  */
38
41
  export async function isQuotingBot(
39
42
  e: any,
40
- ctx: MiokiContext,
43
+ ctx: MiokuContext,
41
44
  ): Promise<{ quoted: true; messageId: string; content: string } | null> {
42
45
  if (e.quote_id) {
43
46
  try {
44
- const quoteMsg = await ctx.getQuoteMsg(e);
45
- if (quoteMsg && String(quoteMsg.sender.user_id) === String(e.self_id)) {
47
+ const quoteMsg = await fetchQuotedMessage(e, ctx);
48
+ if (quoteMsg && String(quoteMsg.sender?.user_id) === String(e.self_id)) {
46
49
  const quotedText = quoteMsg.message
47
50
  ?.filter((s: any) => s.type === "text")
48
- .map((s: any) => s.text)
51
+ .map((s: any) => String(s.data?.text ?? s.text ?? ""))
49
52
  .join("");
50
53
  if (quotedText) {
51
54
  return { quoted: true, messageId: e.quote_id, content: quotedText };
@@ -53,7 +56,7 @@ export async function isQuotingBot(
53
56
  return null;
54
57
  }
55
58
  } catch (err) {
56
- logger.error(err);
59
+ ctx.logger.error(err);
57
60
  }
58
61
  }
59
62
  return null;
@@ -65,7 +68,7 @@ export async function isQuotingBot(
65
68
  */
66
69
  export async function getQuotedContent(
67
70
  e: any,
68
- ctx: MiokiContext,
71
+ ctx: MiokuContext,
69
72
  ): Promise<
70
73
  | {
71
74
  messageId: string;
@@ -78,13 +81,13 @@ export async function getQuotedContent(
78
81
  > {
79
82
  if (e.quote_id) {
80
83
  try {
81
- const quotedMsg = await ctx.getQuoteMsg(e);
84
+ const quotedMsg = await fetchQuotedMessage(e, ctx);
82
85
  if (quotedMsg && quotedMsg.message) {
83
- const senderName = quotedMsg.sender.nickname;
86
+ const senderName = quotedMsg.sender?.nickname || "";
84
87
  // 提取文本内容
85
88
  const textContent = quotedMsg.message
86
89
  .filter((s: any) => s.type === "text")
87
- .map((s: any) => s.text || "")
90
+ .map((s: any) => String(s.data?.text ?? s.text ?? ""))
88
91
  .join("");
89
92
 
90
93
  // 检测是否有图片
@@ -120,7 +123,7 @@ export function isGroupAllowed(groupId: number, cfg: ChatConfig): boolean {
120
123
  export function extractContent(
121
124
  e: any,
122
125
  cfg: ChatConfig,
123
- ctx: MiokiContext,
126
+ ctx: MiokuContext,
124
127
  ): { text: string; multimodal: any[] | null } {
125
128
  let text = "";
126
129
  try {
@@ -129,9 +132,12 @@ export function extractContent(
129
132
 
130
133
  // If text is empty but user @'d the bot, describe the action
131
134
  if (!text.trim() && e.message) {
132
- const hasAt = e.message.some(
133
- (seg: any) => seg.type === "at" && String(seg.qq) === String(e.self_id),
134
- );
135
+ const hasAt = e.message.some((seg: any) => {
136
+ if (seg.type !== "at") return false;
137
+ const data = (seg.data ?? {}) as { qq?: unknown; target?: unknown };
138
+ const qq = data.qq ?? data.target;
139
+ return String(qq ?? "") === String(e.self_id ?? "");
140
+ });
135
141
  if (hasAt) {
136
142
  text = "[@you with no text]";
137
143
  }
@@ -190,14 +196,14 @@ async function mapWithConcurrency<T, R>(
190
196
 
191
197
  export async function getBotRole(
192
198
  groupId: number,
193
- ctx: MiokiContext,
199
+ ctx: MiokuContext,
194
200
  selfId: number,
195
201
  ): Promise<"owner" | "admin" | "member"> {
196
202
  try {
197
- const memberInfo = await ctx
198
- .pickBot(selfId)
199
- .getGroupMemberInfo(groupId, selfId);
200
- return (memberInfo.role as "owner" | "admin" | "member") || "member";
203
+ const bot = ctx.pickBot(selfId);
204
+ if (!bot) return "member";
205
+ const memberInfo = await bot.getMemberInfo(groupId, selfId);
206
+ return (memberInfo?.role as "owner" | "admin" | "member") || "member";
201
207
  } catch {
202
208
  return "member";
203
209
  }
@@ -208,7 +214,7 @@ interface FormattedHistoryMessage {
208
214
  userName: string;
209
215
  userRole: string;
210
216
  content: string;
211
- messageId: number;
217
+ messageId: string;
212
218
  timestamp: number;
213
219
  role: "user" | "assistant";
214
220
  }
@@ -223,6 +229,8 @@ interface HistoryFormatContext {
223
229
  | undefined;
224
230
  historyMediaOptions: HistoryMediaProcessingOptions;
225
231
  botUin: number;
232
+ /** 本运行时全部已连接 bot 的 id:这些账号的消息一律不作为用户历史(防 bot 互触循环) */
233
+ botIds: Set<string>;
226
234
  memberNameCache: Map<string, string>;
227
235
  hashLookupCache: Map<string, string | null>;
228
236
  }
@@ -237,11 +245,11 @@ async function resolveMemberName(
237
245
  }
238
246
  ctx.memberNameCache.set(key, key);
239
247
  try {
240
- const bot = ctx.historyMediaOptions.bot as any;
241
- if (!bot || typeof bot.getGroupMemberInfo !== "function") return key;
242
- const info = await bot.getGroupMemberInfo(
243
- ctx.historyMediaOptions.groupId,
244
- Number(userId),
248
+ const bot = ctx.historyMediaOptions.bot as Bot | undefined;
249
+ if (!bot) return key;
250
+ const info = await bot.getMemberInfo(
251
+ ctx.historyMediaOptions.groupId!,
252
+ userId,
245
253
  );
246
254
  const name = info?.card || info?.nickname || key;
247
255
  ctx.memberNameCache.set(key, name);
@@ -252,6 +260,8 @@ async function resolveMemberName(
252
260
  }
253
261
 
254
262
  function extractQuotedText(messageSegs: any): string {
263
+ // icqq 的 source.message 是 brief 字符串;onebot 是 segment 数组
264
+ if (typeof messageSegs === "string") return messageSegs.trim();
255
265
  if (!Array.isArray(messageSegs) || messageSegs.length === 0) return "";
256
266
  const parts: string[] = [];
257
267
  for (const seg of messageSegs) {
@@ -278,7 +288,10 @@ function extractQuotedText(messageSegs: any): string {
278
288
  return parts.join(" ").trim();
279
289
  }
280
290
 
281
- function buildReplyAnnotation(source: any, ctx: HistoryFormatContext): string | null {
291
+ function buildReplyAnnotation(
292
+ source: any,
293
+ ctx: HistoryFormatContext,
294
+ ): string | null {
282
295
  if (!source || typeof source !== "object") return null;
283
296
  const sourceId = source.id ?? source.message_id ?? source.message_seq;
284
297
  const sourceUserId = source.user_id;
@@ -291,7 +304,8 @@ function buildReplyAnnotation(source: any, ctx: HistoryFormatContext): string |
291
304
  const idStr = sourceId != null ? String(sourceId) : "?";
292
305
  let displayName: string | undefined;
293
306
  if (sourceUserId != null) {
294
- displayName = ctx.memberNameCache.get(String(sourceUserId)) || String(sourceUserId);
307
+ displayName =
308
+ ctx.memberNameCache.get(String(sourceUserId)) || String(sourceUserId);
295
309
  }
296
310
  if (!displayName) displayName = sourceNickname || "unknown";
297
311
 
@@ -334,10 +348,13 @@ async function getImageTagWithHashCache(
334
348
  }
335
349
 
336
350
  async function formatHistoryMessage(
337
- msg: any,
351
+ msg: HistoryMessage,
338
352
  ctx: HistoryFormatContext,
339
353
  ): Promise<FormattedHistoryMessage | null> {
354
+ // 自己或本运行时其它 bot 的消息不作为用户历史,
355
+ // 否则多 bot 同群时会把彼此的消息当作用户接话,形成循环触发
340
356
  if (String(msg.user_id) === String(ctx.botUin)) return null;
357
+ if (ctx.botIds.has(String(msg.user_id))) return null;
341
358
 
342
359
  let content = "";
343
360
  try {
@@ -350,12 +367,15 @@ async function formatHistoryMessage(
350
367
  if (!content.trim()) return null;
351
368
 
352
369
  return {
353
- userId: msg.user_id,
354
- userName: msg.sender?.card || msg.sender?.nickname || String(msg.user_id),
355
- userRole: msg.sender?.role || "member",
370
+ userId: Number(msg.user_id || 0),
371
+ userName: msg.nickname || String(msg.user_id || "unknown"),
372
+ userRole: "member",
356
373
  content,
357
- messageId: msg.message_id,
358
- timestamp: msg.time ? msg.time * 1000 : Date.now(),
374
+ messageId:
375
+ msg.message_id != null && msg.message_id !== ""
376
+ ? String(msg.message_id)
377
+ : "",
378
+ timestamp: msg.time ?? Date.now(),
359
379
  role: "user",
360
380
  };
361
381
  }
@@ -427,17 +447,24 @@ async function buildMessageContent(
427
447
  )) {
428
448
  const videoSources = getSegmentSourceCandidates(videoSeg);
429
449
  if (videoSources.length > 0) {
430
- parts.push(await getCachedHistoryVideoTag(videoSources, historyMediaOptions));
450
+ parts.push(
451
+ await getCachedHistoryVideoTag(videoSources, historyMediaOptions),
452
+ );
431
453
  } else {
432
454
  parts.push("[video]");
433
455
  }
434
456
  }
435
457
 
436
- for (const forwardSeg of msg.message.filter((seg: any) => seg.type === "forward")) {
458
+ for (const forwardSeg of msg.message.filter(
459
+ (seg: any) => seg.type === "forward",
460
+ )) {
437
461
  const forwardId = forwardSeg.id || forwardSeg.data?.id;
438
462
  if (forwardId) {
439
463
  parts.push(
440
- await getCachedHistoryForwardTag(String(forwardId), historyMediaOptions),
464
+ await getCachedHistoryForwardTag(
465
+ String(forwardId),
466
+ historyMediaOptions,
467
+ ),
441
468
  );
442
469
  } else {
443
470
  parts.push("[forward]");
@@ -448,7 +475,11 @@ async function buildMessageContent(
448
475
  ["xml", "json", "lightapp", "ark"].includes(seg.type),
449
476
  )) {
450
477
  const cardData =
451
- cardSeg.data?.data || cardSeg.data?.xml || cardSeg.data || cardSeg.xml || "";
478
+ cardSeg.data?.data ||
479
+ cardSeg.data?.xml ||
480
+ cardSeg.data ||
481
+ cardSeg.xml ||
482
+ "";
452
483
  if (cardData) {
453
484
  parts.push(
454
485
  getCachedHistoryCardTag(
@@ -464,7 +495,9 @@ async function buildMessageContent(
464
495
  if (parts.length > 0) return parts.join(" ");
465
496
 
466
497
  const segTypes = msg.message.map((seg: any) => seg.type);
467
- const nonTextTypes = segTypes.filter((t: string) => t !== "text" && t !== "at");
498
+ const nonTextTypes = segTypes.filter(
499
+ (t: string) => t !== "text" && t !== "at",
500
+ );
468
501
  return nonTextTypes.length > 0 ? `[${nonTextTypes.join(", ")}]` : "";
469
502
  }
470
503
 
@@ -474,7 +507,7 @@ async function buildMessageContent(
474
507
  */
475
508
  export async function getGroupHistory(
476
509
  groupId: number,
477
- ctx: MiokiContext,
510
+ ctx: MiokuContext,
478
511
  count: number = 100,
479
512
  selfId: number,
480
513
  db?: {
@@ -501,7 +534,7 @@ export async function getGroupHistory(
501
534
  userName: string;
502
535
  userRole: string;
503
536
  content: string;
504
- messageId: number;
537
+ messageId: string;
505
538
  timestamp: number;
506
539
  role: "user" | "assistant";
507
540
  }>
@@ -512,7 +545,7 @@ export async function getGroupHistory(
512
545
  userName: string;
513
546
  userRole: string;
514
547
  content: string;
515
- messageId: number;
548
+ messageId: string;
516
549
  timestamp: number;
517
550
  role: "user" | "assistant";
518
551
  }> = [];
@@ -525,7 +558,7 @@ export async function getGroupHistory(
525
558
  userName: msg.userName || "Miku",
526
559
  userRole: msg.userRole || "member",
527
560
  content: msg.content,
528
- messageId: msg.messageId ?? 0,
561
+ messageId: msg.messageId ?? "",
529
562
  timestamp: msg.timestamp,
530
563
  role: "assistant",
531
564
  });
@@ -539,7 +572,7 @@ export async function getGroupHistory(
539
572
  userName: msg.userName || String(msg.userId || "unknown"),
540
573
  userRole: msg.userRole || "member",
541
574
  content: msg.content,
542
- messageId: msg.messageId ?? 0,
575
+ messageId: msg.messageId ?? "",
543
576
  timestamp: msg.timestamp,
544
577
  role: "user",
545
578
  });
@@ -547,8 +580,9 @@ export async function getGroupHistory(
547
580
  }
548
581
 
549
582
  try {
550
- const bot = ctx.pickBot(selfId) as any;
583
+ const bot = ctx.pickBot(selfId);
551
584
  const historyMediaOptions: HistoryMediaProcessingOptions = {
585
+ bot,
552
586
  ai: mediaOptions?.ai,
553
587
  workingModel: mediaOptions?.workingModel,
554
588
  multimodalWorkingModel: mediaOptions?.multimodalWorkingModel,
@@ -561,32 +595,25 @@ export async function getGroupHistory(
561
595
  saveMediaSummarySource: db.saveMediaSummarySource?.bind(db),
562
596
  }
563
597
  : undefined,
564
- bot,
565
598
  groupId,
566
599
  };
567
- // 调用 OneBot API 获取群聊历史
568
- const result = await bot.api("get_group_msg_history", {
569
- group_id: String(groupId),
570
- message_seq: "0",
571
- count: Math.min(count, 200), // 最多获取200条
572
- reverse_order: false,
573
- disable_get_url: false,
574
- parse_mult_msg: true,
575
- quick_reply: false,
576
- });
577
- const messages = result?.messages || result?.data?.messages || [];
578
- if (!Array.isArray(messages)) {
579
- logger.warn("[getGroupHistory] API 返回格式异常:", result);
580
- return botMessages;
581
- }
600
+ const messages = bot
601
+ ? await bot.getHistory(
602
+ { type: "group", group_id: groupId },
603
+ undefined,
604
+ Math.min(count, 200),
605
+ )
606
+ : [];
582
607
 
583
608
  const botUin = selfId;
609
+ const botIds = new Set<string>(ctx.bots.map((b) => String(b.bot_id)));
610
+ botIds.add(String(botUin));
584
611
  const memberNameCache = new Map<string, string>();
585
612
  const hashLookupCache = new Map<string, string | null>();
586
613
 
587
614
  for (const m of messages) {
588
- const uid = m?.user_id;
589
- const senderName = m?.sender?.card || m?.sender?.nickname;
615
+ const uid = m.user_id;
616
+ const senderName = m.nickname;
590
617
  if (uid != null && senderName && !memberNameCache.has(String(uid))) {
591
618
  memberNameCache.set(String(uid), String(senderName));
592
619
  }
@@ -596,6 +623,7 @@ export async function getGroupHistory(
596
623
  db,
597
624
  historyMediaOptions,
598
625
  botUin,
626
+ botIds,
599
627
  memberNameCache,
600
628
  hashLookupCache,
601
629
  };
@@ -657,3 +685,20 @@ export function normalizeIdList(input: unknown): number[] {
657
685
  ),
658
686
  );
659
687
  }
688
+
689
+ async function fetchQuotedMessage(
690
+ e: any,
691
+ ctx: MiokuContext,
692
+ ): Promise<{
693
+ message: any[];
694
+ sender?: { user_id?: string; nickname?: string };
695
+ } | null> {
696
+ const bot = e?.bot ?? ctx.pickBot(e?.self_id ?? "");
697
+ if (!bot || !e?.quote_id) return null;
698
+ try {
699
+ const result = await bot.getMessage(e.quote_id);
700
+ return result ?? null;
701
+ } catch {
702
+ return null;
703
+ }
704
+ }
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:-?\d+\])/);
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?: number;
119
+ quoteId?: string;
120
120
  audioText?: string;
121
121
  } {
122
122
  const atUsers: number[] = [];
123
123
  const pokeUsers: number[] = [];
124
- let quoteId: number | undefined;
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:(-?\d+)\]/g];
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
- quoteId = parseInt(match[1], 10);
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:-?\d+\]/g, "")
174
+ .replace(/\[reply:[^\]\n]+\]/g, "")
174
175
  .replace(/\[audio:[^\]]+\]/gi, "")
175
176
  .trim();
176
177