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/configs/base.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BASE_CONFIG = {
2
- maxContextTokens: 128,
2
+ maxContextTokens: 256,
3
3
  temperature: 0.8,
4
4
  historyCount: 100,
5
5
  maxIterations: 20,
package/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MiokiContext } from "mioki";
1
+ import type { MiokuContext } from "mioku";
2
2
  import type { AIInstance, AIModelRole, AIService } from "mioku";
3
3
  import type { ChatConfig } from "./types";
4
4
  import type { ChatDatabase } from "./db";
@@ -35,7 +35,7 @@ import type { AudioServiceApi } from "mioku-service-audio";
35
35
  import type { ChatMessage } from "./types";
36
36
 
37
37
  export interface ChatPluginContext {
38
- ctx: MiokiContext;
38
+ ctx: MiokuContext;
39
39
  defaultConfig: ChatConfig;
40
40
  configProvider: ChatConfigProvider;
41
41
  getConfig: (groupId?: number) => Promise<ChatConfig>;
package/core/base.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { MiokiContext } from "mioki";
2
- import { logger } from "mioki";
1
+ import type { Bot, MiokuContext } from "mioku";
2
+ import { createGroupRef, logger } from "mioku";
3
3
  import type { SkillPermissionRole } from "mioku";
4
4
  import type { AIInstance, AIService } from "mioku";
5
5
  import type { ScreenshotService } from "mioku";
@@ -24,7 +24,7 @@ import {
24
24
  } from "./media/markdown-message";
25
25
 
26
26
  export interface SendAIResponseOptions {
27
- ctx: MiokiContext;
27
+ ctx: MiokuContext;
28
28
  groupId: number;
29
29
  messages: string[];
30
30
  config: ChatConfig;
@@ -79,7 +79,7 @@ async function waitTypingDelay(
79
79
  export async function sendAIResponse(
80
80
  options: SendAIResponseOptions,
81
81
  selfId: number,
82
- ): Promise<void> {
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: number | undefined;
117
+ let pendingReply: string | undefined;
110
118
  let lastDelayBasisText = "";
111
119
 
112
120
  for (let j = 0; j < expandedLines.length; j++) {
@@ -141,10 +149,7 @@ export async function sendAIResponse(
141
149
 
142
150
  if (pokeUsers.length > 0) {
143
151
  for (const pokeId of pokeUsers) {
144
- await bot.api("group_poke", {
145
- group_id: groupId,
146
- user_id: pokeId,
147
- });
152
+ await bot.pokeMember(String(groupId), String(pokeId));
148
153
  }
149
154
  }
150
155
 
@@ -152,12 +157,13 @@ export async function sendAIResponse(
152
157
 
153
158
  const finalQuoteId = pendingReply;
154
159
  if (finalQuoteId !== undefined) {
155
- lineSegments.push({ type: "reply", id: String(finalQuoteId) });
160
+ lineSegments.push(ctx.segment.reply(finalQuoteId));
156
161
  pendingReply = undefined;
157
162
  }
158
163
 
159
164
  for (const atId of atUsers) {
160
- lineSegments.push(ctx.segment.at(atId));
165
+ if (String(atId) === String(selfId)) continue;
166
+ lineSegments.push(ctx.segment.at(String(atId)));
161
167
  }
162
168
 
163
169
  if (markdownContent) {
@@ -178,13 +184,11 @@ export async function sendAIResponse(
178
184
  (imageSource?: string) => {
179
185
  const segments: any[] = [];
180
186
  if (finalQuoteIdForImage !== undefined) {
181
- segments.push({
182
- type: "reply",
183
- id: String(finalQuoteIdForImage),
184
- });
187
+ segments.push(ctx.segment.reply(finalQuoteIdForImage));
185
188
  }
186
189
  for (const atId of atUsers) {
187
- segments.push(ctx.segment.at(atId));
190
+ if (String(atId) === String(selfId)) continue;
191
+ segments.push(ctx.segment.at(String(atId)));
188
192
  }
189
193
  segments.push(
190
194
  ctx.segment.image(
@@ -194,7 +198,7 @@ export async function sendAIResponse(
194
198
  return segments;
195
199
  },
196
200
  imagePath,
197
- );
201
+ ).then((sentId) => recordSentId(i, sentId));
198
202
  lastDelayBasisText = summarizeMarkdown(markdownContent);
199
203
  if (typingDelayEnabled && j < expandedLines.length - 1) {
200
204
  const delayMs = calculateTypingDelayMs(lastDelayBasisText || line);
@@ -217,11 +221,15 @@ export async function sendAIResponse(
217
221
  lineSegments.push(ctx.segment.text(fallbackText));
218
222
  }
219
223
  if (audioSource) {
220
- lineSegments.push(ctx.segment.record(audioSource));
224
+ lineSegments.push(ctx.segment.raw("record", { file: audioSource }));
221
225
  }
222
226
 
223
227
  if (lineSegments.length > 0) {
224
- await bot.sendGroupMsg(groupId, lineSegments);
228
+ const sent = await bot.sendMessage(
229
+ { type: "group", group_id: groupId },
230
+ lineSegments,
231
+ );
232
+ recordSentId(i, sent?.message_id);
225
233
  lastDelayBasisText = sendableText || fallbackText || audioText || line;
226
234
  }
227
235
 
@@ -238,10 +246,12 @@ export async function sendAIResponse(
238
246
 
239
247
  await onLineSent?.();
240
248
  }
249
+
250
+ return sentMessageIds;
241
251
  }
242
252
 
243
253
  export async function sendMessage(
244
- ctx: MiokiContext,
254
+ ctx: MiokuContext,
245
255
  groupId: number | undefined,
246
256
  userId: number,
247
257
  text: string,
@@ -263,7 +273,7 @@ export async function sendMessage(
263
273
  // 应用错别字生成器
264
274
  const expandedLines = expandOutgoingLines(text);
265
275
 
266
- let pendingReply: number | undefined;
276
+ let pendingReply: string | undefined;
267
277
  let lastDelayBasisText = "";
268
278
 
269
279
  for (let j = 0; j < expandedLines.length; j++) {
@@ -296,13 +306,9 @@ export async function sendMessage(
296
306
  continue;
297
307
  }
298
308
 
299
- // 戳人 - 立即执行
300
309
  if (groupId && pokeUsers.length > 0) {
301
310
  for (const pokeId of pokeUsers) {
302
- await bot.api("group_poke", {
303
- group_id: groupId,
304
- user_id: pokeId,
305
- });
311
+ await bot.pokeMember(String(groupId), String(pokeId));
306
312
  }
307
313
  }
308
314
 
@@ -327,14 +333,11 @@ export async function sendMessage(
327
333
  (imageSource?: string) => {
328
334
  const segments: any[] = [];
329
335
  if (finalQuoteIdForImage !== undefined) {
330
- segments.push({
331
- type: "reply",
332
- id: String(finalQuoteIdForImage),
333
- });
336
+ segments.push(ctx.segment.reply(finalQuoteIdForImage));
334
337
  }
335
338
  for (const atId of atUsers) {
336
339
  if (String(atId) !== String(selfId)) {
337
- segments.push(ctx.segment.at(atId));
340
+ segments.push(ctx.segment.at(String(atId)));
338
341
  }
339
342
  }
340
343
  segments.push(
@@ -366,13 +369,13 @@ export async function sendMessage(
366
369
  const sendableText = markdownContent ?? cleanText;
367
370
  const segments: any[] = [];
368
371
  if (pendingReply !== undefined) {
369
- segments.push({ type: "reply", id: String(pendingReply) });
372
+ segments.push(ctx.segment.reply(pendingReply));
370
373
  pendingReply = undefined;
371
374
  }
372
375
  if (markdownContent) {
373
376
  for (const atId of atUsers) {
374
377
  if (String(atId) !== String(selfId)) {
375
- segments.push(ctx.segment.at(atId));
378
+ segments.push(ctx.segment.at(String(atId)));
376
379
  }
377
380
  }
378
381
  if (sendableText) {
@@ -381,11 +384,14 @@ export async function sendMessage(
381
384
  segments.push(ctx.segment.text(fallbackText));
382
385
  }
383
386
  if (audioSource) {
384
- segments.push(ctx.segment.record(audioSource));
387
+ segments.push(ctx.segment.raw("record", { file: audioSource }));
385
388
  }
386
389
 
387
390
  if (segments.length > 0 && groupId) {
388
- await bot.sendGroupMsg(groupId, segments);
391
+ await bot.sendMessage(
392
+ { type: "group", group_id: groupId },
393
+ segments,
394
+ );
389
395
  lastDelayBasisText =
390
396
  sendableText || fallbackText || audioText || line;
391
397
  }
@@ -398,7 +404,7 @@ export async function sendMessage(
398
404
  // 有 @ 用户时,构建消息保持原始位置
399
405
  // 先将原始行按 @ 标记分割,然后重新构建
400
406
  let remaining = line;
401
- const atPattern = /\[at:(\d+)\]/g;
407
+ const atPattern = /\[at: \d+\]/g;
402
408
 
403
409
  let lastIndex = 0;
404
410
  let match;
@@ -407,18 +413,18 @@ export async function sendMessage(
407
413
  const beforeAt = remaining.slice(lastIndex, match.index);
408
414
  if (beforeAt) {
409
415
  const cleaned = beforeAt
410
- .replace(/\[reply:-?\d+\]/g, "")
416
+ .replace(/\[reply:[^\]\n]+\]/g, "")
411
417
  .replace(/\[poke:\d+\]/g, "")
412
418
  .replace(/\[audio:[^\]]+\]/gi, "")
413
419
  .trim();
414
420
  if (cleaned) {
415
- segments.push({ type: "text", text: cleaned });
421
+ segments.push(ctx.segment.text(cleaned));
416
422
  }
417
423
  }
418
424
 
419
425
  const atId = match[1];
420
426
  if (String(atId) !== String(selfId)) {
421
- segments.push(ctx.segment.at(atId));
427
+ segments.push(ctx.segment.at(String(atId)));
422
428
  }
423
429
 
424
430
  lastIndex = match.index + match[0].length;
@@ -428,25 +434,28 @@ export async function sendMessage(
428
434
  const afterAt = remaining.slice(lastIndex);
429
435
  if (afterAt) {
430
436
  const cleaned = afterAt
431
- .replace(/\[reply:-?\d+\]/g, "")
437
+ .replace(/\[reply:[^\]\n]+\]/g, "")
432
438
  .replace(/\[poke:\d+\]/g, "")
433
439
  .replace(/\[audio:[^\]]+\]/gi, "")
434
440
  .trim();
435
441
  if (cleaned) {
436
- segments.push({ type: "text", text: cleaned });
442
+ segments.push(ctx.segment.text(cleaned));
437
443
  }
438
444
  }
439
445
 
440
446
  if (audioSource) {
441
- segments.push(ctx.segment.record(audioSource));
447
+ segments.push(ctx.segment.raw("record", { file: audioSource }));
442
448
  } else if (fallbackText) {
443
- segments.push({ type: "text", text: fallbackText });
449
+ segments.push(ctx.segment.text(fallbackText));
444
450
  }
445
451
 
446
452
  // 发送消息
447
453
  if (segments.length > 0) {
448
454
  if (groupId) {
449
- await bot.sendGroupMsg(groupId, segments);
455
+ await bot.sendMessage(
456
+ { type: "group", group_id: groupId },
457
+ segments,
458
+ );
450
459
  lastDelayBasisText =
451
460
  sendableText || fallbackText || audioText || line;
452
461
  }
@@ -462,7 +471,7 @@ export async function sendMessage(
462
471
  ) {
463
472
  const sendSegments: any[] = [];
464
473
  if (pendingReply !== undefined) {
465
- sendSegments.push({ type: "reply", id: String(pendingReply) });
474
+ sendSegments.push(ctx.segment.reply(pendingReply));
466
475
  pendingReply = undefined;
467
476
  }
468
477
  if (sendableText) {
@@ -471,7 +480,7 @@ export async function sendMessage(
471
480
  sendSegments.push(ctx.segment.text(fallbackText));
472
481
  }
473
482
  if (audioSource) {
474
- sendSegments.push(ctx.segment.record(audioSource));
483
+ sendSegments.push(ctx.segment.raw("record", { file: audioSource }));
475
484
  }
476
485
  if (sendSegments.length > 0) {
477
486
  await dispatchSegments(bot, groupId, userId, () => sendSegments);
@@ -521,13 +530,13 @@ function expandOutgoingLines(text: string): string[] {
521
530
 
522
531
  function normalizeActionLineBreaks(text: string): string {
523
532
  return String(text || "").replace(
524
- /\\\s*(?=(?:\[\]|\[emotion:[^\]]+\]|\[audio:[^\]]+\]|\[reply:-?\d+\]))/gi,
533
+ /\\\s*(?=(?:\[\]|\[emotion:[^\]]+\]|\[audio:[^\]]+\]|\[reply:[^\]\n]+\]))/gi,
525
534
  "\n",
526
535
  );
527
536
  }
528
537
 
529
538
  async function buildMarkdownImage(
530
- ctx: MiokiContext,
539
+ ctx: MiokuContext,
531
540
  markdownContent: string,
532
541
  screenshotService: ScreenshotService | undefined,
533
542
  enableMarkdownScreenshot: boolean,
@@ -550,9 +559,10 @@ async function dispatchSegments(
550
559
  userId: number | undefined,
551
560
  buildSegments: (imageSource?: string) => any[],
552
561
  fallbackImagePath?: string,
553
- ): Promise<void> {
562
+ ): Promise<string | undefined> {
554
563
  try {
555
- await sendByTarget(bot, groupId, userId, buildSegments());
564
+ const sent = await sendByTarget(bot, groupId, userId, buildSegments());
565
+ return sent?.message_id;
556
566
  } catch (error) {
557
567
  if (!fallbackImagePath || !isLocalFilePath(fallbackImagePath)) {
558
568
  throw error;
@@ -561,24 +571,34 @@ async function dispatchSegments(
561
571
  const fsPromises = await import("fs/promises");
562
572
  const buffer = await fsPromises.readFile(fallbackImagePath);
563
573
  const base64Image = `base64://${buffer.toString("base64")}`;
564
- await sendByTarget(bot, groupId, userId, buildSegments(base64Image));
574
+ const sent = await sendByTarget(
575
+ bot,
576
+ groupId,
577
+ userId,
578
+ buildSegments(base64Image),
579
+ );
580
+ return sent?.message_id;
565
581
  }
566
582
  }
567
583
 
568
584
  async function sendByTarget(
569
- bot: any,
585
+ bot: Bot,
570
586
  groupId: number | undefined,
571
587
  userId: number | undefined,
572
- segments: any[],
573
- ): Promise<void> {
588
+ segments: readonly any[],
589
+ ): Promise<import("mioku").SentMessage | undefined> {
574
590
  if (groupId) {
575
- await bot.sendGroupMsg(groupId, segments);
576
- return;
591
+ return await bot.sendMessage(
592
+ { type: "group", group_id: groupId },
593
+ segments,
594
+ );
577
595
  }
578
596
 
579
597
  if (userId) {
580
- await bot.sendPrivateMsg(userId, segments);
581
- return;
598
+ return await bot.sendMessage(
599
+ { type: "private", user_id: userId },
600
+ segments,
601
+ );
582
602
  }
583
603
 
584
604
  throw new Error("No valid message target");
@@ -612,11 +632,11 @@ function isLocalFilePath(value: string): boolean {
612
632
  }
613
633
 
614
634
  async function resolveAudioSource(
615
- ctx: MiokiContext,
635
+ ctx: MiokuContext,
616
636
  options: {
617
637
  audioText?: string;
618
638
  config: ChatConfig;
619
- audioService?: AudioServiceApi;
639
+ audioService?: AudioServiceApi;
620
640
  },
621
641
  ): Promise<string | null> {
622
642
  const trimmed = String(options.audioText || "").trim();
@@ -650,7 +670,7 @@ export interface GroupHistoryResult {
650
670
  export async function getGroupHistoryMessages(
651
671
  groupId: number,
652
672
  groupSessionId: string,
653
- ctx: MiokiContext,
673
+ ctx: MiokuContext,
654
674
  historyCount: number,
655
675
  db: ChatDatabase,
656
676
  selfId: number,
@@ -689,7 +709,7 @@ export interface GroupInfoResult {
689
709
  }
690
710
 
691
711
  export async function getGroupInfoData(
692
- ctx: MiokiContext,
712
+ ctx: MiokuContext,
693
713
  groupId: number,
694
714
  selfId: number,
695
715
  fallbackGroupName?: string,
@@ -698,9 +718,16 @@ export async function getGroupInfoData(
698
718
  let memberCount: number | undefined;
699
719
 
700
720
  try {
701
- const groupInfo = await ctx.pickBot(selfId).getGroupInfo(groupId);
702
- groupName = (groupInfo as any)?.group_name || fallbackGroupName;
703
- memberCount = (groupInfo as any)?.member_count;
721
+ const bot = ctx.pickBot(selfId);
722
+ if (!bot) {
723
+ groupName = fallbackGroupName;
724
+ return { groupName, memberCount };
725
+ }
726
+ const groupInfo = await createGroupRef(bot, String(groupId)).getInfo();
727
+ if (groupInfo) {
728
+ groupName = groupInfo.group_name || fallbackGroupName;
729
+ memberCount = groupInfo.member_count;
730
+ }
704
731
  } catch {
705
732
  groupName = fallbackGroupName;
706
733
  }
@@ -739,12 +766,14 @@ export async function getHumanizeContexts(
739
766
  topicContext: topicContext || undefined,
740
767
  expressionContext: expressionContext || undefined,
741
768
  };
742
- logger.info(`[getHumanizeContexts] session=${groupSessionId} user=${userName} triggerUserId=${triggerUserId} memoryContext=undefined topicContext="${topicContext?.slice(0, 100) ?? ""}" expressionContext="${expressionContext?.slice(0, 100) ?? ""}"`);
769
+ logger.info(
770
+ `[getHumanizeContexts] session=${groupSessionId} user=${userName} triggerUserId=${triggerUserId} memoryContext=undefined topicContext="${topicContext?.slice(0, 100) ?? ""}" expressionContext="${expressionContext?.slice(0, 100) ?? ""}"`,
771
+ );
743
772
  return result;
744
773
  }
745
774
 
746
775
  export interface BuildToolContextOptions {
747
- ctx: MiokiContext;
776
+ ctx: MiokuContext;
748
777
  event: any;
749
778
  selfId: number;
750
779
  groupSessionId: string;
@@ -761,7 +790,7 @@ export interface BuildToolContextOptions {
761
790
  }
762
791
 
763
792
  function resolveTriggerSkillRole(
764
- ctx: MiokiContext,
793
+ ctx: MiokuContext,
765
794
  event: any,
766
795
  ): SkillPermissionRole {
767
796
  const userId = event?.user_id || event?.sender?.user_id;
@@ -844,18 +873,20 @@ export function saveBotMessages(
844
873
  timestamp: number,
845
874
  config: ChatConfig,
846
875
  db: ChatDatabase,
847
- ctx: MiokiContext,
848
- selfId: number,
876
+ ctx: MiokuContext,
877
+ bot: Bot | undefined,
878
+ sentMessageIds?: Array<string | undefined>,
849
879
  ): void {
850
- const bot = ctx.pickBot(selfId);
851
880
  const botNickname = config.nicknames[0] || (bot?.nickname ?? "Miku");
881
+ const selfId = bot ? Number(bot.bot_id) : 0;
852
882
 
853
883
  if (!bot) {
854
884
  ctx.logger.warn(`[saveBotMessages] bot ${selfId} not available`);
855
885
  return;
856
886
  }
857
887
 
858
- for (const msg of messages) {
888
+ for (let i = 0; i < messages.length; i++) {
889
+ const msg = messages[i];
859
890
  const botMsg: ChatMessage = {
860
891
  sessionId: groupSessionId,
861
892
  role: "assistant",
@@ -865,29 +896,27 @@ export function saveBotMessages(
865
896
  userRole: "member",
866
897
  groupId,
867
898
  timestamp,
899
+ messageId: sentMessageIds?.[i],
868
900
  };
869
901
  db.saveMessage(botMsg);
870
902
  }
871
903
  }
872
904
 
873
905
  export async function sendEmoji(
874
- ctx: MiokiContext,
906
+ ctx: MiokuContext,
875
907
  groupId: number,
876
908
  emojiPath: string | null | undefined,
877
- selfId: number,
909
+ bot: Bot | undefined,
878
910
  ): Promise<void> {
879
911
  if (!emojiPath) return;
880
- const bot = ctx.pickBot(selfId);
881
912
  if (!bot) {
882
- ctx.logger.error(
883
- `[sendEmoji] bot ${String(selfId)} not found, skip sending emoji`,
884
- );
913
+ ctx.logger.error(`[sendEmoji] bot not available, skip sending emoji`);
885
914
  return;
886
915
  }
887
916
 
888
917
  try {
889
918
  const emojiSegment = ctx.segment.image(`file://${emojiPath}`);
890
- await bot.sendGroupMsg(groupId, [emojiSegment]);
919
+ await bot.sendMessage({ type: "group", group_id: groupId }, [emojiSegment]);
891
920
  } catch (err) {
892
921
  try {
893
922
  const fsPromises = await import("fs/promises");
@@ -922,7 +951,9 @@ export async function sendEmoji(
922
951
 
923
952
  const base64DataUrl = `data:${mimeType};base64,${base64}`;
924
953
  const base64Segment = ctx.segment.image(base64DataUrl);
925
- await bot.sendGroupMsg(groupId, [base64Segment]);
954
+ await bot.sendMessage({ type: "group", group_id: groupId }, [
955
+ base64Segment,
956
+ ]);
926
957
  ctx.logger.info(`[Emoji] Sent via base64: ${path.basename(emojiPath)}`);
927
958
  } catch (base64Err) {
928
959
  ctx.logger.error(`[Emoji] Base64 also failed: ${base64Err}`);