mioku-plugin-chat 3.0.1 → 3.0.3

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/core/base.ts CHANGED
@@ -404,7 +404,7 @@ export async function sendMessage(
404
404
  // 有 @ 用户时,构建消息保持原始位置
405
405
  // 先将原始行按 @ 标记分割,然后重新构建
406
406
  let remaining = line;
407
- const atPattern = /\[at: \d+\]/g;
407
+ const atPattern = /\[at: ?(\d+)\]/g;
408
408
 
409
409
  let lastIndex = 0;
410
410
  let match;
package/core/chat-turn.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  import type { MiokuContext } from "mioku";
2
- import type { AITool, ChatRuntimePromptInjection } from "mioku";
2
+ import type { AITool, Bot, ChatRuntimePromptInjection } from "mioku";
3
3
  import type { ChatPluginContext, ChatRuntimeState } from "../context";
4
4
  import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
5
- import {
6
- getGroupHistory,
7
- getBotRole,
8
- getQuotedContent,
9
- } from "../utils";
5
+ import { getGroupHistory, getBotRole, getQuotedContent } from "../utils";
10
6
  import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
11
7
 
12
8
  export type RuntimeReplyContextType =
@@ -53,7 +49,8 @@ function buildRuntimeTargetMessageContent(
53
49
  overrideContent?: string,
54
50
  ): string {
55
51
  if (overrideContent?.trim()) return overrideContent.trim();
56
- if (!event || !event.message || !Array.isArray(event.message)) return NO_NEW_MESSAGE;
52
+ if (!event || !event.message || !Array.isArray(event.message))
53
+ return NO_NEW_MESSAGE;
57
54
  return ctx.text(event)?.trim() || NO_NEW_MESSAGE;
58
55
  }
59
56
 
@@ -74,7 +71,8 @@ function resolveRuntimeContext(
74
71
  selfId: event.self_id,
75
72
  sessionId: groupId ? `group:${groupId}` : `personal:${userId}`,
76
73
  personalSessionId: groupId ? `personal:${userId}` : undefined,
77
- senderName: event.sender?.card || event.sender?.nickname || String(userId),
74
+ senderName:
75
+ event.sender?.card || event.sender?.nickname || String(userId),
78
76
  userRole: event.sender?.role || "member",
79
77
  userTitle: event.sender?.title || undefined,
80
78
  groupName: event.group_name,
@@ -85,7 +83,10 @@ function resolveRuntimeContext(
85
83
  if (typeof options.selfId !== "number") {
86
84
  throw new Error("Chat runtime requires either event or selfId");
87
85
  }
88
- if (typeof options.groupId !== "number" && typeof options.userId !== "number") {
86
+ if (
87
+ typeof options.groupId !== "number" &&
88
+ typeof options.userId !== "number"
89
+ ) {
89
90
  throw new Error("Chat runtime requires groupId or userId");
90
91
  }
91
92
 
@@ -112,7 +113,9 @@ function resolveRuntimeContext(
112
113
  groupId: options.groupId,
113
114
  userId,
114
115
  selfId: options.selfId,
115
- sessionId: options.groupId ? `group:${options.groupId}` : `personal:${userId}`,
116
+ sessionId: options.groupId
117
+ ? `group:${options.groupId}`
118
+ : `personal:${userId}`,
116
119
  personalSessionId:
117
120
  options.groupId && userId ? `personal:${userId}` : undefined,
118
121
  senderName: options.groupId ? "system" : String(userId),
@@ -144,39 +147,90 @@ export async function finalizeChatTurn(
144
147
  return;
145
148
  }
146
149
  const { ctx } = pluginCtx;
147
- const { groupId, groupSessionId, userId, selfId, cfg, toolCtx, result, event } = args;
150
+ const {
151
+ groupId,
152
+ groupSessionId,
153
+ userId,
154
+ selfId,
155
+ cfg,
156
+ toolCtx,
157
+ result,
158
+ event,
159
+ } = args;
160
+
161
+ const actingBot = (selfId ? ctx.pickBot(selfId) : undefined) ?? event?.bot;
162
+ const eventBotId = event?.bot ? String(event.bot.bot_id) : undefined;
163
+ const actingIsEventBot =
164
+ eventBotId == null ||
165
+ (actingBot != null && String(actingBot.bot_id) === eventBotId);
148
166
 
149
167
  if (groupId) {
150
168
  const sentMessageIds = await pluginCtx.sendAIResponse(
151
- { ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices, audioService: pluginCtx.audioService },
169
+ {
170
+ ctx,
171
+ groupId,
172
+ messages: result.messages,
173
+ config: cfg,
174
+ sentIndices: toolCtx.sentMessageIndices,
175
+ audioService: pluginCtx.audioService,
176
+ },
152
177
  selfId,
153
178
  );
154
- await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, event?.bot);
179
+ await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, actingBot);
155
180
  const now = Date.now();
156
- pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx, event?.bot, sentMessageIds);
181
+ pluginCtx.saveBotMessages(
182
+ groupId,
183
+ groupSessionId,
184
+ result.messages,
185
+ now,
186
+ cfg,
187
+ pluginCtx.db,
188
+ ctx,
189
+ actingBot,
190
+ sentMessageIds,
191
+ );
157
192
  if (args.isLive) {
158
- pluginCtx.idleCheckManager.recordBotMessages(groupSessionId, result.messages.length, selfId);
193
+ pluginCtx.idleCheckManager.recordBotMessages(
194
+ groupSessionId,
195
+ result.messages.length,
196
+ selfId,
197
+ );
159
198
  }
160
- pluginCtx.cooldownManager.startCooldownTimer(groupSessionId, groupId, selfId);
199
+ pluginCtx.cooldownManager.startCooldownTimer(
200
+ groupSessionId,
201
+ groupId,
202
+ selfId,
203
+ );
161
204
  } else {
162
205
  const sentIndices = toolCtx.sentMessageIndices;
163
206
  for (let i = 0; i < result.messages.length; i++) {
164
207
  if (sentIndices?.has(i)) continue;
165
- await pluginCtx.sendMessage(ctx, undefined, userId, result.messages[i], cfg, selfId, pluginCtx.audioService);
208
+ await pluginCtx.sendMessage(
209
+ ctx,
210
+ undefined,
211
+ userId,
212
+ result.messages[i],
213
+ cfg,
214
+ selfId,
215
+ pluginCtx.audioService,
216
+ );
166
217
  }
167
218
  if (result.emojiPath) {
168
219
  try {
169
220
  const emojiSegment = ctx.segment.image(`file://${result.emojiPath}`);
170
- if (args.isLive) {
221
+ if (args.isLive && actingIsEventBot && event?.reply) {
171
222
  await event.reply([emojiSegment]);
172
223
  } else {
173
- const bot = args.event?.bot;
174
- if (!bot) throw new Error(`bot ${selfId} not found`);
175
- await bot.sendMessage({ type: "private", user_id: userId}, [emojiSegment]);
224
+ if (!actingBot) throw new Error(`bot ${selfId} not found`);
225
+ await actingBot.sendMessage({ type: "private", user_id: userId }, [
226
+ emojiSegment,
227
+ ]);
176
228
  }
177
229
  } catch (err) {
178
230
  ctx.logger.warn(
179
- args.isLive ? `[Emoticon] Send failed: ${err}` : `[chat-runtime] Send emoji failed: ${err}`,
231
+ args.isLive
232
+ ? `[Emoticon] Send failed: ${err}`
233
+ : `[chat-runtime] Send emoji failed: ${err}`,
180
234
  );
181
235
  }
182
236
  }
@@ -189,12 +243,13 @@ export async function processChat(
189
243
  e: any,
190
244
  pluginCtx: ChatPluginContext,
191
245
  runtimeState: ChatRuntimeState,
246
+ options: { replyBot?: Bot } = {},
192
247
  ): Promise<void> {
193
248
  const { ctx } = pluginCtx;
194
249
  const isGroup = e.message_type === "group";
195
250
  const groupId: number | undefined = isGroup ? e.group_id : undefined;
196
251
  const userId: number = e.user_id || e.sender?.user_id;
197
- const selfId = e.self_id;
252
+ const selfId = options.replyBot ? Number(options.replyBot.bot_id) : e.self_id;
198
253
  const cfg = await pluginCtx.getConfig(groupId);
199
254
 
200
255
  const personalSessionId = `personal:${userId}`;
@@ -212,7 +267,11 @@ export async function processChat(
212
267
  groupId ?? userId,
213
268
  );
214
269
  if (groupId) {
215
- pluginCtx.sessionManager.getOrCreate(personalSessionId, "personal", userId);
270
+ pluginCtx.sessionManager.getOrCreate(
271
+ personalSessionId,
272
+ "personal",
273
+ userId,
274
+ );
216
275
  }
217
276
 
218
277
  const quotedInfo = await getQuotedContent(e, ctx);
@@ -242,7 +301,7 @@ export async function processChat(
242
301
  groupId,
243
302
  ctx,
244
303
  cfg.historyCount,
245
- e.self_id,
304
+ selfId,
246
305
  pluginCtx.db,
247
306
  pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
248
307
  )
@@ -260,15 +319,18 @@ export async function processChat(
260
319
  }));
261
320
 
262
321
  const botNickname =
263
- cfg.nicknames[0] || e.bot?.nickname || "Bot";
264
- const botRole = groupId ? await getBotRole(groupId, ctx, e.self_id) : "member";
322
+ cfg.nicknames[0] ||
323
+ options.replyBot?.nickname ||
324
+ e.bot?.nickname ||
325
+ "Bot";
326
+ const botRole = groupId ? await getBotRole(groupId, ctx, selfId) : "member";
265
327
  let groupName: string | undefined;
266
328
  let memberCount: number | undefined;
267
329
  if (groupId) {
268
330
  const groupInfo = await pluginCtx.getGroupInfoData(
269
331
  ctx,
270
332
  groupId,
271
- e.self_id,
333
+ selfId,
272
334
  e.group_name,
273
335
  );
274
336
  groupName = groupInfo.groupName;
@@ -311,7 +373,7 @@ export async function processChat(
311
373
  pendingImageUrls: imageUrls,
312
374
  humanize: pluginCtx.humanize,
313
375
  targetMessage,
314
- selfId: e.self_id,
376
+ selfId,
315
377
  audioService: pluginCtx.audioService,
316
378
  });
317
379
 
@@ -351,7 +413,12 @@ export async function processChat(
351
413
  }
352
414
  : undefined,
353
415
  ),
354
- { userId, groupId, label: isGroup ? "group-chat" : "private-chat", skipRetryOnRateLimit: true },
416
+ {
417
+ userId,
418
+ groupId,
419
+ label: isGroup ? "group-chat" : "private-chat",
420
+ skipRetryOnRateLimit: true,
421
+ },
355
422
  );
356
423
 
357
424
  if (!result) {
@@ -366,7 +433,7 @@ export async function processChat(
366
433
  groupId,
367
434
  groupSessionId,
368
435
  userId,
369
- selfId: e.self_id,
436
+ selfId,
370
437
  toolCtx,
371
438
  send: true,
372
439
  isLive: true,
@@ -435,7 +502,13 @@ async function executeChatRuntimeRequestNow(
435
502
  }
436
503
 
437
504
  const rawHistory = groupId
438
- ? await getGroupHistory(groupId, pluginCtx.ctx, cfg.historyCount, selfId, pluginCtx.db)
505
+ ? await getGroupHistory(
506
+ groupId,
507
+ pluginCtx.ctx,
508
+ cfg.historyCount,
509
+ selfId,
510
+ pluginCtx.db,
511
+ )
439
512
  : [];
440
513
  const history: ChatMessage[] = rawHistory.map((msg) => ({
441
514
  sessionId,
@@ -449,9 +522,10 @@ async function executeChatRuntimeRequestNow(
449
522
  messageId: msg.messageId,
450
523
  }));
451
524
 
452
- const botRole = groupId ? await getBotRole(groupId, pluginCtx.ctx, selfId) : "member";
453
- const botNickname =
454
- cfg.nicknames[0] || event?.bot?.nickname || "Bot";
525
+ const botRole = groupId
526
+ ? await getBotRole(groupId, pluginCtx.ctx, selfId)
527
+ : "member";
528
+ const botNickname = cfg.nicknames[0] || event?.bot?.nickname || "Bot";
455
529
 
456
530
  let groupName: string | undefined;
457
531
  let memberCount: number | undefined;
@@ -222,6 +222,10 @@ export function createMessageHandler(
222
222
  }
223
223
 
224
224
  const atBot = shouldTrigger(e, text, cfg, ctx);
225
+ const replyBot = ctx.pickReplyBot(e);
226
+ const actorSelfId = replyBot
227
+ ? Number(replyBot.bot_id)
228
+ : Number(e.self_id || 0);
225
229
  const quotedBot = isGroup ? await isQuotingBot(e, ctx) : null;
226
230
  const mentionedNickname =
227
231
  cfg.nicknames.length > 0 &&
@@ -275,14 +279,14 @@ export function createMessageHandler(
275
279
  groupSessionId,
276
280
  groupId,
277
281
  delayInfo.delayMs,
278
- Number(e.self_id || 0),
282
+ actorSelfId,
279
283
  );
280
284
  return;
281
285
  }
282
286
  }
283
287
 
284
288
  pluginCtx.rateLimiter.record(userId, groupId, text);
285
- await processChat(e, pluginCtx, runtimeState);
289
+ await processChat(e, pluginCtx, runtimeState, { replyBot });
286
290
  return;
287
291
  }
288
292
 
@@ -294,10 +298,11 @@ export function createMessageHandler(
294
298
  ctx,
295
299
  cfg.historyCount,
296
300
  pluginCtx.db,
297
- Number(e.self_id || 0),
301
+ actorSelfId,
298
302
  pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
299
303
  );
300
- const botNickname = cfg.nicknames[0] || e.bot?.nickname || "Bot";
304
+ const botNickname =
305
+ cfg.nicknames[0] || replyBot?.nickname || e.bot?.nickname || "Bot";
301
306
  const planResult = await pluginCtx.humanize.actionPlanner.plan(
302
307
  groupSessionId,
303
308
  botNickname,
@@ -307,7 +312,7 @@ export function createMessageHandler(
307
312
  if (planResult.action !== "reply") return;
308
313
  if (!pluginCtx.rateLimiter.canProcess(userId, groupId, text)) return;
309
314
  pluginCtx.rateLimiter.record(userId, groupId, text);
310
- await processChat(e, pluginCtx, runtimeState);
315
+ await processChat(e, pluginCtx, runtimeState, { replyBot });
311
316
  }
312
317
  };
313
318
 
@@ -319,7 +324,7 @@ export function createMessageHandler(
319
324
  pluginCtx.rateLimiter.recordInteraction(groupId, userId);
320
325
  pluginCtx.queueProcessor.scheduleQueuedMessages(
321
326
  groupSessionId,
322
- Number(e.self_id || 0),
327
+ actorSelfId,
323
328
  );
324
329
  }
325
330
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -54,10 +54,10 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/bun": "^1.3.14",
57
- "mioku": "workspace:*"
57
+ "mioku": "workspace:*",
58
+ "mioku-service-audio": "workspace:*"
58
59
  },
59
60
  "peerDependencies": {
60
- "mioku": "^1.0.0",
61
- "mioku-service-audio": "workspace:*"
61
+ "mioku": "^1.0.0"
62
62
  }
63
63
  }
package/utils/message.ts CHANGED
@@ -27,11 +27,7 @@ export function shouldTrigger(
27
27
  ): boolean {
28
28
  if (e.message_type === "private") return false;
29
29
 
30
- const atSeg = e.message?.find((seg: any) => seg.type === "at");
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 ?? "");
30
+ return ctx.mentionedBots(e).length > 0;
35
31
  }
36
32
 
37
33
  /**
@@ -45,8 +41,13 @@ export async function isQuotingBot(
45
41
  if (e.quote_id) {
46
42
  try {
47
43
  const quoteMsg = await fetchQuotedMessage(e, ctx);
48
- if (quoteMsg && String(quoteMsg.sender?.user_id) === String(e.self_id)) {
49
- const quotedText = quoteMsg.message
44
+ const quotedUserId = String(quoteMsg?.sender?.user_id ?? "");
45
+ const quotingAnyBot =
46
+ quoteMsg != null &&
47
+ quotedUserId !== "" &&
48
+ ctx.bots.some((bot) => String(bot.bot_id) === quotedUserId);
49
+ if (quotingAnyBot) {
50
+ const quotedText = quoteMsg!.message
50
51
  ?.filter((s: any) => s.type === "text")
51
52
  .map((s: any) => String(s.data?.text ?? s.text ?? ""))
52
53
  .join("");
@@ -130,15 +131,9 @@ export function extractContent(
130
131
  text = ctx.text(e) || "";
131
132
  } catch {}
132
133
 
133
- // If text is empty but user @'d the bot, describe the action
134
+ // If text is empty but the message @'d a connected bot, describe the action
134
135
  if (!text.trim() && e.message) {
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
- });
141
- if (hasAt) {
136
+ if (ctx.mentionedBots(e).length > 0) {
142
137
  text = "[@you with no text]";
143
138
  }
144
139
  }