mioku-plugin-chat 2.3.0 → 2.4.0
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 -0
- package/configs/settings.ts +1 -1
- package/context.ts +17 -0
- package/core/chat-engine.ts +6 -1
- package/core/chat-turn.ts +540 -0
- package/core/media/history-media.ts +215 -70
- package/core/media/image-analyzer.ts +28 -19
- package/core/media/image-compress.ts +75 -0
- package/core/media/segment.ts +91 -0
- package/core/multimodal.ts +24 -12
- package/core/prompt.ts +332 -445
- package/core/tools/info.ts +284 -0
- package/core/tools/load-skill.ts +149 -0
- package/core/tools/web.ts +306 -0
- package/core/tools.ts +11 -822
- package/db.ts +45 -142
- package/handlers/idle-debug.ts +135 -0
- package/handlers/message.ts +316 -0
- package/handlers/poke.ts +165 -0
- package/humanize/emotion-agent.ts +3 -4
- package/humanize/expression.ts +3 -4
- package/humanize/planner.ts +2 -2
- package/humanize/topic.ts +3 -4
- package/index.ts +73 -1705
- package/manage/cooldown.ts +52 -27
- package/manage/group-structured-history.ts +35 -0
- package/manage/idle-check.ts +40 -20
- package/manage/queue-processor.ts +51 -25
- package/manage/rate-limit-guard.ts +70 -0
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +124 -0
- package/types.ts +1 -0
- package/utils/index.ts +2 -0
- package/utils/json.ts +8 -0
- package/utils/message.ts +185 -175
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 {
|
|
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):
|
|
211
|
-
getImageByUrl?(url: string):
|
|
212
|
-
getMediaSummary?(key: string):
|
|
213
|
-
saveMediaSummary?(summary:
|
|
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
|
-
|
|
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
|
+
}
|