mioku-plugin-chat 2.3.1 → 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 CHANGED
@@ -5,6 +5,7 @@ export const BASE_CONFIG = {
5
5
  workingModel: "deepseek/deepseek-v3.2-exp", // 工作模型,用于 planner 等轻量任务
6
6
  multimodalWorkingModel: "doubao-seed-2.0-mini", // 多模态工作模型,用于图片描述等任务
7
7
  isMultimodal: true,
8
+ enableMediaRecognition: true, // 是否用多模态工作模型自动识别聊天记录中的图片/视频等媒体
8
9
  maxContextTokens: 128,
9
10
  temperature: 0.8,
10
11
  historyCount: 100, // 群聊历史消息数量
@@ -1,6 +1,6 @@
1
1
  export const SETTINGS_CONFIG = {
2
2
  searxng: {
3
- enabled: false,
3
+ enabled: true,
4
4
  baseUrl: "https://search.crystelf.top/",
5
5
  timeoutMs: 8000,
6
6
  defaultLimit: 5,
package/context.ts CHANGED
@@ -75,6 +75,11 @@ export interface ChatPluginContext {
75
75
  buildToolContext: BuildToolContext;
76
76
  buildStructuredUserInputFromTarget: BuildStructuredUserInputFromTarget;
77
77
  runChat: RunChat;
78
+ startCooldownTimer: (
79
+ groupSessionId: string,
80
+ groupId: number,
81
+ selfId: number,
82
+ ) => void;
78
83
  recordGroupMessageForLearning: (
79
84
  userMsg: ChatMessage,
80
85
  groupSessionId: string,
@@ -89,6 +94,18 @@ export interface ChatRuntimeState {
89
94
  processingSet: Set<string>;
90
95
  }
91
96
 
97
+ /**
98
+ * Closure state shared with the extracted message/poke handlers.
99
+ */
100
+ export interface ChatHandlerState {
101
+ getConfig: () => Promise<ChatConfig>;
102
+ matchMessageCommands?: (
103
+ text: string,
104
+ ) => Array<{ plugin: string; command: string }>;
105
+ runtimeState: ChatRuntimeState;
106
+ pokeCooldowns: Map<number, number>;
107
+ }
108
+
92
109
  /**
93
110
  * Chat runtime implementation for external callers (like boot plugin).
94
111
  * Provides notice generation and information request capabilities.
@@ -29,6 +29,7 @@ import {
29
29
  GroupStructuredHistoryManager,
30
30
  type StructuredUserInput,
31
31
  } from "../manage/group-structured-history";
32
+ import { prepareImageUrlsForModel } from "./media/image-compress";
32
33
 
33
34
  interface StructuredHistoryRunContext {
34
35
  manager: GroupStructuredHistoryManager;
@@ -130,6 +131,10 @@ export async function runChat(
130
131
  directImageUrls,
131
132
  ).filter((message) => message.role !== "system"),
132
133
  );
134
+ // 体积过大的图片先压缩,再附加到主模型请求;估算用原始 URL 即可(图片按固定 token 计)。
135
+ const modelImageUrls = directImageUrls
136
+ ? await prepareImageUrlsForModel(directImageUrls)
137
+ : undefined;
133
138
  const usageContext = {
134
139
  usageId,
135
140
  source: "chat",
@@ -206,7 +211,7 @@ export async function runChat(
206
211
  targetMessage,
207
212
  cachedHistory,
208
213
  currentUserMessages,
209
- directImageUrls,
214
+ modelImageUrls,
210
215
  ),
211
216
  usageContext,
212
217
  usageContextTokens: chatHistoryTokens,
@@ -0,0 +1,540 @@
1
+ import type { MiokiContext } from "mioki";
2
+ import type { AITool, ChatRuntimePromptInjection } from "mioku";
3
+ import type { ChatPluginContext, ChatRuntimeState } from "../context";
4
+ import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
5
+ import {
6
+ getGroupHistory,
7
+ getBotRole,
8
+ getQuotedContent,
9
+ } from "../utils";
10
+ import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
11
+
12
+ export type RuntimeReplyContextType =
13
+ | "reply"
14
+ | "comment"
15
+ | "idle"
16
+ | "review"
17
+ | "poked";
18
+
19
+ export interface ExecuteChatRuntimeRequestOptions {
20
+ event?: any;
21
+ selfId?: number;
22
+ groupId?: number;
23
+ userId?: number;
24
+ config: ChatConfig;
25
+ targetMessageContent?: string;
26
+ promptInjections?: ChatRuntimePromptInjection[];
27
+ extraTools?: AITool[];
28
+ send?: boolean;
29
+ replyContextType?: RuntimeReplyContextType;
30
+ }
31
+
32
+ interface ResolvedRuntimeContext {
33
+ event: any;
34
+ isGroup: boolean;
35
+ groupId?: number;
36
+ userId: number;
37
+ selfId: number;
38
+ sessionId: string;
39
+ personalSessionId?: string;
40
+ senderName: string;
41
+ userRole: "owner" | "admin" | "member";
42
+ userTitle?: string;
43
+ groupName?: string;
44
+ messageId?: number;
45
+ }
46
+
47
+ const NO_NEW_MESSAGE =
48
+ "[No new user message in this turn. Reply naturally based on the runtime instruction and recent context.]";
49
+
50
+ function buildRuntimeTargetMessageContent(
51
+ ctx: MiokiContext,
52
+ event: any,
53
+ overrideContent?: string,
54
+ ): string {
55
+ if (overrideContent?.trim()) return overrideContent.trim();
56
+ if (!event || !event.message || !Array.isArray(event.message)) return NO_NEW_MESSAGE;
57
+ return ctx.text(event)?.trim() || NO_NEW_MESSAGE;
58
+ }
59
+
60
+ function resolveRuntimeContext(
61
+ ctx: MiokiContext,
62
+ options: ExecuteChatRuntimeRequestOptions,
63
+ ): ResolvedRuntimeContext {
64
+ if (options.event) {
65
+ const event = options.event;
66
+ const isGroup = event.message_type === "group";
67
+ const groupId: number | undefined = isGroup ? event.group_id : undefined;
68
+ const userId: number = event.user_id || event.sender?.user_id || 0;
69
+ return {
70
+ event,
71
+ isGroup,
72
+ groupId,
73
+ userId,
74
+ selfId: event.self_id,
75
+ sessionId: groupId ? `group:${groupId}` : `personal:${userId}`,
76
+ personalSessionId: groupId ? `personal:${userId}` : undefined,
77
+ senderName: event.sender?.card || event.sender?.nickname || String(userId),
78
+ userRole: event.sender?.role || "member",
79
+ userTitle: event.sender?.title || undefined,
80
+ groupName: event.group_name,
81
+ messageId: event.message_id,
82
+ };
83
+ }
84
+
85
+ if (typeof options.selfId !== "number") {
86
+ throw new Error("Chat runtime requires either event or selfId");
87
+ }
88
+ if (typeof options.groupId !== "number" && typeof options.userId !== "number") {
89
+ throw new Error("Chat runtime requires groupId or userId");
90
+ }
91
+
92
+ const isGroup = typeof options.groupId === "number";
93
+ const userId = options.userId ?? 0;
94
+ const event = {
95
+ self_id: options.selfId,
96
+ message_type: isGroup ? "group" : "private",
97
+ group_id: options.groupId,
98
+ user_id: userId,
99
+ group_name: undefined,
100
+ sender: {
101
+ user_id: userId,
102
+ card: undefined,
103
+ nickname: undefined,
104
+ role: "member",
105
+ title: undefined,
106
+ },
107
+ };
108
+
109
+ return {
110
+ event,
111
+ isGroup,
112
+ groupId: options.groupId,
113
+ userId,
114
+ selfId: options.selfId,
115
+ sessionId: options.groupId ? `group:${options.groupId}` : `personal:${userId}`,
116
+ personalSessionId:
117
+ options.groupId && userId ? `personal:${userId}` : undefined,
118
+ senderName: options.groupId ? "system" : String(userId),
119
+ userRole: "member",
120
+ userTitle: undefined,
121
+ groupName: undefined,
122
+ messageId: undefined,
123
+ };
124
+ }
125
+
126
+ // Shared send/save/cooldown tail for both live and runtime turns.
127
+ async function finalizeTurn(
128
+ pluginCtx: ChatPluginContext,
129
+ args: {
130
+ event: any;
131
+ cfg: ChatConfig;
132
+ result: { messages: string[]; emojiPath?: string | null };
133
+ groupId?: number;
134
+ groupSessionId: string;
135
+ userId: number;
136
+ selfId: number;
137
+ toolCtx: { sentMessageIndices?: Set<number> };
138
+ send: boolean;
139
+ isLive: boolean;
140
+ },
141
+ ): Promise<void> {
142
+ if (!args.send) {
143
+ pluginCtx.sessionManager.touch(args.groupSessionId);
144
+ return;
145
+ }
146
+ const { ctx } = pluginCtx;
147
+ const { groupId, groupSessionId, userId, selfId, cfg, toolCtx, result, event } = args;
148
+
149
+ if (groupId) {
150
+ await pluginCtx.sendAIResponse(
151
+ { ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices },
152
+ selfId,
153
+ );
154
+ await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, selfId);
155
+ const now = Date.now();
156
+ pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx, selfId);
157
+ if (args.isLive) {
158
+ pluginCtx.idleCheckManager.recordBotMessages(groupSessionId, result.messages.length, selfId);
159
+ }
160
+ pluginCtx.cooldownManager.startCooldownTimer(groupSessionId, groupId, selfId);
161
+ } else {
162
+ const sentIndices = toolCtx.sentMessageIndices;
163
+ for (let i = 0; i < result.messages.length; i++) {
164
+ if (sentIndices?.has(i)) continue;
165
+ await pluginCtx.sendMessage(ctx, undefined, userId, result.messages[i], cfg, selfId);
166
+ }
167
+ if (result.emojiPath) {
168
+ try {
169
+ const emojiSegment = ctx.segment.image(`file://${result.emojiPath}`);
170
+ if (args.isLive) {
171
+ await event.reply([emojiSegment]);
172
+ } else {
173
+ const bot = ctx.pickBot(selfId);
174
+ if (!bot) throw new Error(`bot ${selfId} not found`);
175
+ await bot.sendPrivateMsg(userId, [emojiSegment]);
176
+ }
177
+ } catch (err) {
178
+ ctx.logger.warn(
179
+ args.isLive ? `[Emoticon] Send failed: ${err}` : `[chat-runtime] Send emoji failed: ${err}`,
180
+ );
181
+ }
182
+ }
183
+ }
184
+ pluginCtx.sessionManager.touch(groupSessionId);
185
+ }
186
+
187
+ // Live message turn — triggered by a real group/private message event.
188
+ export async function processChat(
189
+ e: any,
190
+ pluginCtx: ChatPluginContext,
191
+ runtimeState: ChatRuntimeState,
192
+ ): Promise<void> {
193
+ const { ctx, config: cfg } = pluginCtx;
194
+ const isGroup = e.message_type === "group";
195
+ const groupId: number | undefined = isGroup ? e.group_id : undefined;
196
+ const userId: number = e.user_id || e.sender?.user_id;
197
+ const selfId = e.self_id;
198
+
199
+ const personalSessionId = `personal:${userId}`;
200
+ const groupSessionId = groupId ? `group:${groupId}` : personalSessionId;
201
+
202
+ if (runtimeState.isRateLimitBlocked()) {
203
+ if (groupId) pluginCtx.queueManager.clearActiveTarget(groupSessionId);
204
+ return;
205
+ }
206
+
207
+ try {
208
+ pluginCtx.sessionManager.getOrCreate(
209
+ groupSessionId,
210
+ groupId ? "group" : "personal",
211
+ groupId ?? userId,
212
+ );
213
+ if (groupId) {
214
+ pluginCtx.sessionManager.getOrCreate(personalSessionId, "personal", userId);
215
+ }
216
+
217
+ const quotedInfo = await getQuotedContent(e, ctx);
218
+ const imageUrls: string[] = [];
219
+ if (e.message) {
220
+ for (const seg of e.message) {
221
+ if (seg.type === "image" && (seg.url || seg.data?.url)) {
222
+ imageUrls.push(seg.url || seg.data.url);
223
+ }
224
+ }
225
+ }
226
+ if (quotedInfo?.imageUrl) imageUrls.push(quotedInfo.imageUrl);
227
+
228
+ let messageContent = ctx.text(e) || "";
229
+ let extraContext = "";
230
+ if (quotedInfo) {
231
+ const parts = [
232
+ `[Quoted message #${quotedInfo.messageId} from ${quotedInfo.senderName}: ${quotedInfo.content}]`,
233
+ ];
234
+ if (quotedInfo.imageUrl) parts.push("[Quoted message contains an image]");
235
+ extraContext = parts.join(" ");
236
+ }
237
+ if (extraContext) messageContent = extraContext + " " + messageContent;
238
+
239
+ const rawHistory = groupId
240
+ ? await getGroupHistory(
241
+ groupId,
242
+ ctx,
243
+ cfg.historyCount,
244
+ e.self_id,
245
+ pluginCtx.db,
246
+ pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
247
+ )
248
+ : [];
249
+ const history: ChatMessage[] = rawHistory.map((msg: any) => ({
250
+ sessionId: groupSessionId,
251
+ role: "user" as const,
252
+ content: msg.content,
253
+ userId: msg.userId,
254
+ userName: msg.userName,
255
+ userRole: msg.userRole,
256
+ groupId,
257
+ timestamp: msg.timestamp,
258
+ messageId: msg.messageId,
259
+ }));
260
+
261
+ const botNickname =
262
+ cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
263
+ const botRole = groupId ? await getBotRole(groupId, ctx, e.self_id) : "member";
264
+ let groupName: string | undefined;
265
+ let memberCount: number | undefined;
266
+ if (groupId) {
267
+ const groupInfo = await pluginCtx.getGroupInfoData(
268
+ ctx,
269
+ groupId,
270
+ e.self_id,
271
+ e.group_name,
272
+ );
273
+ groupName = groupInfo.groupName;
274
+ memberCount = groupInfo.memberCount;
275
+ }
276
+
277
+ const senderName = e.sender?.card || e.sender?.nickname || String(userId);
278
+ const contexts = await pluginCtx.getHumanizeContexts(
279
+ pluginCtx.humanize,
280
+ groupSessionId,
281
+ senderName,
282
+ history,
283
+ userId,
284
+ );
285
+
286
+ const targetMessage: TargetMessage = {
287
+ userName: senderName,
288
+ userId,
289
+ userRole: e.sender?.role || "member",
290
+ userTitle: (e.sender as any)?.title || undefined,
291
+ content: messageContent,
292
+ messageId: e.message_id,
293
+ timestamp: Date.now(),
294
+ };
295
+
296
+ if (groupId) {
297
+ pluginCtx.queueManager.setActiveTarget(groupSessionId, targetMessage);
298
+ }
299
+
300
+ const toolCtx = pluginCtx.buildToolContext({
301
+ ctx,
302
+ event: e,
303
+ groupSessionId,
304
+ groupId,
305
+ userId,
306
+ config: cfg,
307
+ aiService: pluginCtx.aiService,
308
+ db: pluginCtx.db,
309
+ botRole,
310
+ pendingImageUrls: imageUrls,
311
+ humanize: pluginCtx.humanize,
312
+ targetMessage,
313
+ selfId: e.self_id,
314
+ });
315
+
316
+ const result = await pluginCtx.runWithRateLimitGuard(
317
+ () =>
318
+ pluginCtx.runChat(
319
+ pluginCtx.aiInstance,
320
+ toolCtx,
321
+ history,
322
+ targetMessage,
323
+ {
324
+ config: cfg,
325
+ groupName,
326
+ memberCount,
327
+ botNickname,
328
+ botRole,
329
+ aiService: pluginCtx.aiService,
330
+ isGroup,
331
+ memoryContext: contexts.memoryContext,
332
+ topicContext: contexts.topicContext,
333
+ expressionContext: contexts.expressionContext,
334
+ replyContext: {
335
+ type: "reply",
336
+ targetUser: targetMessage.userName,
337
+ targetMessage: targetMessage.content,
338
+ },
339
+ },
340
+ pluginCtx.humanize,
341
+ pluginCtx.skillManager,
342
+ groupId
343
+ ? {
344
+ manager: pluginCtx.groupStructuredHistory,
345
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
346
+ currentUserInputs: [
347
+ pluginCtx.buildStructuredUserInputFromTarget(targetMessage),
348
+ ],
349
+ }
350
+ : undefined,
351
+ ),
352
+ { userId, groupId, label: isGroup ? "group-chat" : "private-chat" },
353
+ );
354
+
355
+ if (!result) {
356
+ if (groupId) pluginCtx.queueManager.clearActiveTarget(groupSessionId);
357
+ return;
358
+ }
359
+
360
+ await finalizeTurn(pluginCtx, {
361
+ event: e,
362
+ cfg,
363
+ result,
364
+ groupId,
365
+ groupSessionId,
366
+ userId,
367
+ selfId: e.self_id,
368
+ toolCtx,
369
+ send: true,
370
+ isLive: true,
371
+ });
372
+ } catch (err) {
373
+ ctx.logger.error(`Chat processing failed: ${err}`);
374
+ if (groupId) pluginCtx.queueManager.clearActiveTarget(groupSessionId);
375
+ }
376
+ }
377
+
378
+ // Runtime turn — triggered by ChatRuntime.generateNotice / requestInformation.
379
+ export async function executeChatRuntimeRequest(
380
+ options: ExecuteChatRuntimeRequestOptions,
381
+ pluginCtx: ChatPluginContext,
382
+ ): Promise<{
383
+ messages: string[];
384
+ toolCalls: Array<{ name: string; arguments: any; result: any }>;
385
+ collectedInfo: null;
386
+ }> {
387
+ const cfg = options.config;
388
+ const runtimeCtx = resolveRuntimeContext(pluginCtx.ctx, options);
389
+ const {
390
+ event,
391
+ isGroup,
392
+ groupId,
393
+ userId,
394
+ selfId,
395
+ sessionId,
396
+ personalSessionId,
397
+ senderName,
398
+ userRole,
399
+ userTitle,
400
+ groupName: runtimeGroupName,
401
+ messageId,
402
+ } = runtimeCtx;
403
+ const targetContent = buildRuntimeTargetMessageContent(
404
+ pluginCtx.ctx,
405
+ event,
406
+ options.targetMessageContent,
407
+ );
408
+
409
+ pluginCtx.sessionManager.getOrCreate(
410
+ sessionId,
411
+ groupId ? "group" : "personal",
412
+ groupId ?? userId,
413
+ );
414
+ if (groupId && personalSessionId) {
415
+ pluginCtx.sessionManager.getOrCreate(personalSessionId, "personal", userId);
416
+ }
417
+
418
+ const rawHistory = groupId
419
+ ? await getGroupHistory(groupId, pluginCtx.ctx, cfg.historyCount, selfId, pluginCtx.db)
420
+ : [];
421
+ const history: ChatMessage[] = rawHistory.map((msg) => ({
422
+ sessionId,
423
+ role: "user",
424
+ content: msg.content,
425
+ userId: msg.userId,
426
+ userName: msg.userName,
427
+ userRole: msg.userRole,
428
+ groupId,
429
+ timestamp: msg.timestamp,
430
+ messageId: msg.messageId,
431
+ }));
432
+
433
+ const botRole = groupId ? await getBotRole(groupId, pluginCtx.ctx, selfId) : "member";
434
+ const botNickname =
435
+ cfg.nicknames[0] || pluginCtx.ctx.pickBot(selfId)?.nickname || "Bot";
436
+
437
+ let groupName: string | undefined;
438
+ let memberCount: number | undefined;
439
+ if (groupId) {
440
+ const groupInfo = await pluginCtx.getGroupInfoData(
441
+ pluginCtx.ctx,
442
+ groupId,
443
+ selfId,
444
+ runtimeGroupName,
445
+ );
446
+ groupName = groupInfo.groupName;
447
+ memberCount = groupInfo.memberCount;
448
+ } else {
449
+ groupName = runtimeGroupName;
450
+ }
451
+
452
+ const contexts = await pluginCtx.getHumanizeContexts(
453
+ pluginCtx.humanize,
454
+ sessionId,
455
+ senderName,
456
+ history,
457
+ );
458
+
459
+ const targetMessage: TargetMessage = {
460
+ userName: senderName,
461
+ userId,
462
+ userRole,
463
+ userTitle,
464
+ content: targetContent,
465
+ messageId,
466
+ timestamp: Date.now(),
467
+ };
468
+
469
+ const toolCtx = pluginCtx.buildToolContext({
470
+ ctx: pluginCtx.ctx,
471
+ event,
472
+ groupSessionId: sessionId,
473
+ groupId,
474
+ userId,
475
+ config: cfg,
476
+ aiService: pluginCtx.aiService,
477
+ db: pluginCtx.db,
478
+ botRole,
479
+ humanize: pluginCtx.humanize,
480
+ targetMessage,
481
+ selfId,
482
+ });
483
+
484
+ if (options.send === false) {
485
+ toolCtx.onTextContent = undefined;
486
+ toolCtx.sentMessageIndices = undefined;
487
+ }
488
+
489
+ const result = await pluginCtx.runChat(
490
+ pluginCtx.aiInstance,
491
+ toolCtx,
492
+ history,
493
+ targetMessage,
494
+ {
495
+ config: cfg,
496
+ groupName,
497
+ memberCount,
498
+ botNickname,
499
+ botRole,
500
+ aiService: pluginCtx.aiService,
501
+ isGroup,
502
+ memoryContext: contexts.memoryContext,
503
+ topicContext: contexts.topicContext,
504
+ expressionContext: contexts.expressionContext,
505
+ replyContext: {
506
+ type: options.replyContextType || "reply",
507
+ targetUser: targetMessage.userName,
508
+ targetMessage: targetMessage.content,
509
+ },
510
+ promptInjections: options.promptInjections,
511
+ },
512
+ pluginCtx.humanize,
513
+ pluginCtx.skillManager,
514
+ undefined,
515
+ { extraTools: options.extraTools },
516
+ );
517
+
518
+ await finalizeTurn(pluginCtx, {
519
+ event,
520
+ cfg,
521
+ result,
522
+ groupId,
523
+ groupSessionId: sessionId,
524
+ userId,
525
+ selfId,
526
+ toolCtx,
527
+ send: options.send !== false,
528
+ isLive: false,
529
+ });
530
+
531
+ return {
532
+ messages: result.messages,
533
+ toolCalls: result.toolCalls.map((toolCall) => ({
534
+ name: toolCall.name,
535
+ arguments: toolCall.args,
536
+ result: toolCall.result,
537
+ })),
538
+ collectedInfo: null,
539
+ };
540
+ }