mioku-plugin-chat 2.3.1 → 2.4.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,11 +1,18 @@
1
1
  import { logger, MiokiContext } from "mioki";
2
2
  import type { AIInstance } from "mioku";
3
- import type { ChatConfig, ChatMessage } from "../types";
3
+ import type {
4
+ ChatConfig,
5
+ ChatMessage,
6
+ ImageRecord,
7
+ MediaSummaryRecord,
8
+ } from "../types";
4
9
  import {
5
10
  getCachedHistoryCardTag,
6
11
  getCachedHistoryForwardTag,
7
12
  getCachedHistoryVideoTag,
13
+ getSegmentSourceCandidates,
8
14
  type HistoryMediaProcessingOptions,
15
+ type MediaMessageSegment,
9
16
  } from "../core/media/history-media";
10
17
  import { getImageTag } from "../core/media/image-analyzer";
11
18
 
@@ -196,6 +203,138 @@ export async function getBotRole(
196
203
  }
197
204
  }
198
205
 
206
+ interface FormattedHistoryMessage {
207
+ userId: number;
208
+ userName: string;
209
+ userRole: string;
210
+ content: string;
211
+ messageId: number;
212
+ timestamp: number;
213
+ }
214
+
215
+ interface HistoryFormatContext {
216
+ db:
217
+ | {
218
+ getImageByUrl?(url: string): ImageRecord | null;
219
+ }
220
+ | undefined;
221
+ historyMediaOptions: HistoryMediaProcessingOptions;
222
+ botUin: number;
223
+ }
224
+
225
+ async function formatHistoryMessage(
226
+ msg: any,
227
+ ctx: HistoryFormatContext,
228
+ ): Promise<FormattedHistoryMessage | null> {
229
+ if (String(msg.user_id) === String(ctx.botUin)) return null;
230
+
231
+ let content = "";
232
+ try {
233
+ content = await buildMessageContent(msg, ctx);
234
+ } catch (err) {
235
+ logger.error("[getGroupHistory] process message error:", err);
236
+ return null;
237
+ }
238
+
239
+ if (!content.trim()) return null;
240
+
241
+ return {
242
+ userId: msg.user_id,
243
+ userName: msg.sender?.card || msg.sender?.nickname || String(msg.user_id),
244
+ userRole: msg.sender?.role || "member",
245
+ content,
246
+ messageId: msg.message_id,
247
+ timestamp: msg.time ? msg.time * 1000 : Date.now(),
248
+ };
249
+ }
250
+
251
+ async function buildMessageContent(
252
+ msg: any,
253
+ ctx: HistoryFormatContext,
254
+ ): Promise<string> {
255
+ if (!msg.message || !Array.isArray(msg.message) || msg.message.length === 0) {
256
+ return "";
257
+ }
258
+ const { db, historyMediaOptions } = ctx;
259
+ const parts: string[] = [];
260
+
261
+ const textSegs = msg.message.filter((seg: any) => seg.type === "text");
262
+ const textContent = textSegs
263
+ .map((seg: any) => seg.data?.text || "")
264
+ .join("")
265
+ .trim();
266
+
267
+ const atContent = msg.message
268
+ .filter((seg: any) => seg.type === "at")
269
+ .map((seg: any) => {
270
+ const atUid = seg.qq || seg.data?.qq || seg.data?.id || seg.data?.user_id;
271
+ if (!atUid) return null;
272
+ if (atUid === "all" || atUid === "everyone") return "@全体成员";
273
+ return `@${atUid}`;
274
+ })
275
+ .filter((v: string | null) => v !== null)
276
+ .join(" ");
277
+
278
+ if (atContent) parts.push(atContent);
279
+ if (textContent) parts.push(textContent);
280
+
281
+ for (const imageSeg of msg.message.filter((seg: any) => seg.type === "image")) {
282
+ const imageUrl = imageSeg.url || imageSeg.data?.url;
283
+ if (imageUrl) {
284
+ parts.push(
285
+ db?.getImageByUrl
286
+ ? getImageTag(String(imageUrl), db as any)
287
+ : "[image]",
288
+ );
289
+ }
290
+ }
291
+
292
+ for (const videoSeg of msg.message.filter(
293
+ (seg: MediaMessageSegment) => seg.type === "video",
294
+ )) {
295
+ const videoSources = getSegmentSourceCandidates(videoSeg);
296
+ if (videoSources.length > 0) {
297
+ parts.push(await getCachedHistoryVideoTag(videoSources, historyMediaOptions));
298
+ } else {
299
+ parts.push("[video]");
300
+ }
301
+ }
302
+
303
+ for (const forwardSeg of msg.message.filter((seg: any) => seg.type === "forward")) {
304
+ const forwardId = forwardSeg.id || forwardSeg.data?.id;
305
+ if (forwardId) {
306
+ parts.push(
307
+ await getCachedHistoryForwardTag(String(forwardId), historyMediaOptions),
308
+ );
309
+ } else {
310
+ parts.push("[forward]");
311
+ }
312
+ }
313
+
314
+ for (const cardSeg of msg.message.filter((seg: any) =>
315
+ ["xml", "json", "lightapp", "ark"].includes(seg.type),
316
+ )) {
317
+ const cardData =
318
+ cardSeg.data?.data || cardSeg.data?.xml || cardSeg.data || cardSeg.xml || "";
319
+ if (cardData) {
320
+ parts.push(
321
+ getCachedHistoryCardTag(
322
+ typeof cardData === "string" ? cardData : JSON.stringify(cardData),
323
+ historyMediaOptions,
324
+ ),
325
+ );
326
+ } else {
327
+ parts.push("[card]");
328
+ }
329
+ }
330
+
331
+ if (parts.length > 0) return parts.join(" ");
332
+
333
+ const segTypes = msg.message.map((seg: any) => seg.type);
334
+ const nonTextTypes = segTypes.filter((t: string) => t !== "text" && t !== "at");
335
+ return nonTextTypes.length > 0 ? `[${nonTextTypes.join(", ")}]` : "";
336
+ }
337
+
199
338
  /**
200
339
  * 从 OneBot API 获取群聊历史消息
201
340
  * 返回格式化为 ChatMessage 数组
@@ -207,10 +346,12 @@ export async function getGroupHistory(
207
346
  selfId: number,
208
347
  db?: {
209
348
  getBotMessages(groupId: number, limit: number): ChatMessage[];
210
- getImageByHash?(hash: string): any;
211
- getImageByUrl?(url: string): any;
212
- getMediaSummary?(key: string): any;
213
- saveMediaSummary?(summary: any): void;
349
+ getImageByHash?(hash: string): ImageRecord | null;
350
+ getImageByUrl?(url: string): ImageRecord | null;
351
+ getMediaSummary?(key: string): MediaSummaryRecord | null;
352
+ saveMediaSummary?(summary: MediaSummaryRecord): void;
353
+ getMediaSummaryBySource?(sourceKey: string): MediaSummaryRecord | null;
354
+ saveMediaSummarySource?(sourceKey: string, summaryKey: string): void;
214
355
  getStoredGroupNoticeMessages?(
215
356
  groupId: number,
216
357
  limit?: number,
@@ -279,6 +420,8 @@ export async function getGroupHistory(
279
420
  ? {
280
421
  getMediaSummary: db.getMediaSummary.bind(db),
281
422
  saveMediaSummary: db.saveMediaSummary.bind(db),
423
+ getMediaSummaryBySource: db.getMediaSummaryBySource?.bind(db),
424
+ saveMediaSummarySource: db.saveMediaSummarySource?.bind(db),
282
425
  }
283
426
  : undefined,
284
427
  bot,
@@ -302,181 +445,15 @@ export async function getGroupHistory(
302
445
 
303
446
  const botUin = selfId;
304
447
 
305
- // 格式化消息
448
+ const formatCtx: HistoryFormatContext = { db, historyMediaOptions, botUin };
449
+
306
450
  const formattedResults = await mapWithConcurrency(
307
451
  messages,
308
452
  HISTORY_MEDIA_CONCURRENCY,
309
- async (msg) => {
310
- // 跳过自己的消息
311
- if (String(msg.user_id) === String(botUin)) {
312
- return null;
313
- }
314
-
315
- // 提取文本内容
316
- let content = "";
317
- try {
318
- if (
319
- msg.message &&
320
- Array.isArray(msg.message) &&
321
- msg.message.length > 0
322
- ) {
323
- const textSegs = msg.message.filter(
324
- (seg: any) => seg.type === "text",
325
- );
326
- const textContent = textSegs
327
- .map((seg: any) => seg.data?.text || "")
328
- .join("")
329
- .trim();
330
- const atSegs = msg.message.filter((seg: any) => seg.type === "at");
331
- const atContent = atSegs
332
- .map((seg: any) => {
333
- // OneBot v11 格式: seg.qq
334
- const atUid =
335
- seg.qq || seg.data?.qq || seg.data?.id || seg.data?.user_id;
336
- if (!atUid) {
337
- return null;
338
- }
339
- if (atUid === "all" || atUid === "everyone") {
340
- return "@全体成员";
341
- }
342
- return `@${atUid}`;
343
- })
344
- .filter((v: string | null) => v !== null)
345
- .join(" ");
346
- const parts: string[] = [];
347
- if (atContent) {
348
- parts.push(atContent);
349
- }
350
- if (textContent) {
351
- parts.push(textContent);
352
- }
353
-
354
- // 处理图片消息
355
- const imageSegs = msg.message.filter(
356
- (seg: any) => seg.type === "image",
357
- );
358
- if (imageSegs.length > 0) {
359
- for (const imageSeg of imageSegs) {
360
- const imageUrl =
361
- (imageSeg as any).url || (imageSeg as any).data?.url;
362
- if (imageUrl) {
363
- parts.push(
364
- db?.getImageByHash
365
- ? await getImageTag(String(imageUrl), db as any)
366
- : "[image]",
367
- );
368
- }
369
- }
370
- }
371
-
372
- // 处理视频消息:历史里只读取已缓存摘要,不触发旧视频分析。
373
- const videoSegs = msg.message.filter(
374
- (seg: any) => seg.type === "video",
375
- );
376
- for (const videoSeg of videoSegs) {
377
- const videoUrl =
378
- (videoSeg as any).url || (videoSeg as any).data?.url;
379
- if (videoUrl) {
380
- const tag = await getCachedHistoryVideoTag(
381
- String(videoUrl),
382
- historyMediaOptions,
383
- );
384
- parts.push(tag);
385
- } else {
386
- parts.push("[video]");
387
- }
388
- }
389
-
390
- // 处理合并转发消息
391
- const forwardSegs = msg.message.filter(
392
- (seg: any) => seg.type === "forward",
393
- );
394
- for (const forwardSeg of forwardSegs) {
395
- const forwardId =
396
- (forwardSeg as any).id || (forwardSeg as any).data?.id;
397
- if (forwardId) {
398
- parts.push(
399
- await getCachedHistoryForwardTag(
400
- String(forwardId),
401
- historyMediaOptions,
402
- ),
403
- );
404
- } else {
405
- parts.push("[forward]");
406
- }
407
- }
408
-
409
- // 处理 XML/JSON/轻应用/Ark 卡片消息
410
- const cardSegs = msg.message.filter((seg: any) =>
411
- ["xml", "json", "lightapp", "ark"].includes(seg.type),
412
- );
413
- for (const cardSeg of cardSegs) {
414
- const cardData =
415
- (cardSeg as any).data?.data ||
416
- (cardSeg as any).data?.xml ||
417
- (cardSeg as any).data ||
418
- (cardSeg as any).xml ||
419
- "";
420
- if (cardData) {
421
- parts.push(
422
- getCachedHistoryCardTag(
423
- typeof cardData === "string"
424
- ? cardData
425
- : JSON.stringify(cardData),
426
- historyMediaOptions,
427
- ),
428
- );
429
- } else {
430
- parts.push("[card]");
431
- }
432
- }
433
-
434
- if (parts.length > 0) {
435
- content = parts.join(" ");
436
- } else if (Array.isArray(msg.message)) {
437
- const segTypes = msg.message.map((seg: any) => seg.type);
438
- const nonTextTypes = segTypes.filter(
439
- (t: string) => t !== "text" && t !== "at",
440
- );
441
- if (nonTextTypes.length > 0) {
442
- content = `[${nonTextTypes.join(", ")}]`;
443
- } else {
444
- return null;
445
- }
446
- }
447
- }
448
- } catch (err) {
449
- logger.error("[getGroupHistory] process message error:", err);
450
- return null;
451
- }
452
-
453
- // 跳过空消息
454
- if (!content.trim()) {
455
- return null;
456
- }
457
-
458
- return {
459
- userId: msg.user_id,
460
- userName:
461
- msg.sender?.card || msg.sender?.nickname || String(msg.user_id),
462
- userRole: msg.sender?.role || "member",
463
- content,
464
- messageId: msg.message_id,
465
- timestamp: msg.time ? msg.time * 1000 : Date.now(),
466
- };
467
- },
453
+ (msg: any) => formatHistoryMessage(msg, formatCtx),
468
454
  );
469
455
  const formatted = formattedResults.filter(
470
- (
471
- msg,
472
- ): msg is {
473
- userId: number;
474
- userName: string;
475
- userRole: string;
476
- content: string;
477
- messageId: number;
478
- timestamp: number;
479
- } => Boolean(msg),
456
+ (msg): msg is FormattedHistoryMessage => Boolean(msg),
480
457
  );
481
458
 
482
459
  // 合并 bot 消息
@@ -494,3 +471,36 @@ export async function getGroupHistory(
494
471
  return botMessages;
495
472
  }
496
473
  }
474
+
475
+ export function buildChatMessageFromEvent(
476
+ e: any,
477
+ text: string,
478
+ isGroup: boolean,
479
+ groupId: number | undefined,
480
+ ): ChatMessage {
481
+ const userId: number = e.user_id || e.sender?.user_id;
482
+ return {
483
+ sessionId: groupId ? `group:${groupId}` : `personal:${userId}`,
484
+ role: "user" as const,
485
+ content: text,
486
+ userId,
487
+ userName: e.sender?.card || e.sender?.nickname || String(userId),
488
+ userRole: e.sender?.role || "member",
489
+ userTitle: (e.sender as any)?.title || undefined,
490
+ groupId,
491
+ groupName: isGroup ? e.group_name : undefined,
492
+ timestamp: Date.now(),
493
+ messageId: e.message_id,
494
+ };
495
+ }
496
+
497
+ export function normalizeIdList(input: unknown): number[] {
498
+ if (!Array.isArray(input)) return [];
499
+ return Array.from(
500
+ new Set(
501
+ input
502
+ .map((item) => Math.floor(Number(item)))
503
+ .filter((id) => Number.isFinite(id) && id > 0),
504
+ ),
505
+ );
506
+ }