mioku-plugin-chat 2.8.1 → 2.8.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.
@@ -85,7 +85,7 @@ export const PERSONALIZATION_CONFIG: {
85
85
 
86
86
  replyStyle: {
87
87
  baseStyle:
88
- "Casual and cute, uses emoticons, can occasionally mix in a small amount of natural everyday Japanese words, but should not heavily rely on Japanese. Do not end sentences with commas or periods.",
88
+ "Casual and cute,can occasionally mix in a small amount of natural everyday Japanese words, but should not heavily rely on Japanese. Do not end sentences with commas or periods.",
89
89
  multipleStyles: [
90
90
  "Playing cute, likes to add 'w' at the end of cute phrases, commonly used to replace sentence-ending particles such as '呀'.",
91
91
  "Hometown dialect mode, can occasionally use a small amount of natural everyday Japanese expressions in replies, and starts replies with '呐'. Avoid ending sentences with commas or periods.",
package/core/base.ts CHANGED
@@ -2,6 +2,7 @@ import type { MiokiContext } from "mioki";
2
2
  import { logger } from "mioki";
3
3
  import type { SkillPermissionRole } from "mioku";
4
4
  import type { AIInstance, AIService } from "mioku";
5
+ import type { ScreenshotService } from "mioku";
5
6
  import type {
6
7
  ChatConfig,
7
8
  ChatMessage,
@@ -12,7 +13,7 @@ import type { ChatDatabase } from "../db";
12
13
  import type { HumanizeEngine } from "../humanize";
13
14
  import { parseLineMarkers, splitByReplyMarkers } from "../utils/queue";
14
15
  import { getGroupHistory } from "../utils";
15
- import type { ScreenshotService } from "mioku";
16
+ import { getService, Services } from "mioku";
16
17
  import { synthesizeAudioBase64 } from "./media/audio";
17
18
  import {
18
19
  extractStandaloneMarkdownBlock,
@@ -157,9 +158,7 @@ export async function sendAIResponse(
157
158
  }
158
159
 
159
160
  if (markdownContent) {
160
- const screenshotService = ctx.services?.screenshot as
161
- | ScreenshotService
162
- | undefined;
161
+ const screenshotService = getService(ctx, Services.Screenshot);
163
162
  const imagePath = await buildMarkdownImage(
164
163
  ctx,
165
164
  markdownContent,
@@ -305,9 +304,7 @@ export async function sendMessage(
305
304
  const hasAt = atUsers.length > 0;
306
305
 
307
306
  if (markdownContent) {
308
- const screenshotService = ctx.services?.screenshot as
309
- | ScreenshotService
310
- | undefined;
307
+ const screenshotService = getService(ctx, Services.Screenshot);
311
308
  const imagePath = await buildMarkdownImage(
312
309
  ctx,
313
310
  markdownContent,
@@ -1,7 +1,4 @@
1
- import type {
2
- AIInstance,
3
- SessionToolDefinition,
4
- } from "mioku";
1
+ import type { AIInstance, SessionToolDefinition } from "mioku";
5
2
  import { logger } from "mioki";
6
3
  import type { AITool } from "mioku";
7
4
  import type {
@@ -11,16 +8,10 @@ import type {
11
8
  ChatResult,
12
9
  } from "../types";
13
10
  import type { HumanizeEngine } from "../humanize";
14
- import type {
15
- StaticPromptContext,
16
- DynamicPromptContext,
17
- } from "./prompt";
11
+ import type { StaticPromptContext, DynamicPromptContext } from "./prompt";
18
12
  import type { SkillSessionManager } from "../manage/skill-session";
19
13
  import { createTools } from "./tools";
20
- import {
21
- buildStaticSystemPrompt,
22
- buildDynamicUserContext,
23
- } from "./prompt";
14
+ import { buildStaticSystemPrompt, buildDynamicUserContext } from "./prompt";
24
15
  import type { PromptCtxForRunChat } from "../manage/types";
25
16
  import {
26
17
  isExternalSkillAllowed,
@@ -419,7 +410,9 @@ function estimateChatHistoryTokens(history: ChatMessage[]): number {
419
410
  );
420
411
  }
421
412
 
422
- function estimateMessageContentTokens(messages: Array<{ content?: unknown }>): number {
413
+ function estimateMessageContentTokens(
414
+ messages: Array<{ content?: unknown }>,
415
+ ): number {
423
416
  return messages.reduce(
424
417
  (sum, message) => sum + estimateContentTokens(message.content),
425
418
  0,
@@ -473,7 +466,10 @@ function buildCurrentMessages(
473
466
  if (currentUserMessages.length > 0) {
474
467
  messages.push(
475
468
  ...prependDynamicContextToFirstUserMessage(
476
- attachImagesToCurrentUserMessages(currentUserMessages, pendingImageUrls),
469
+ attachImagesToCurrentUserMessages(
470
+ currentUserMessages,
471
+ pendingImageUrls,
472
+ ),
477
473
  dynamicUserContext,
478
474
  pendingImageUrls,
479
475
  ),
@@ -650,6 +646,7 @@ function createExternalSkillRuntimeContext(toolCtx: ToolContext): any {
650
646
  rawEvent,
651
647
  session_id: toolCtx.sessionId,
652
648
  trigger_role: toolCtx.triggerSkillRole,
649
+ isMultimodal: Boolean(toolCtx.config?.isMultimodal),
653
650
  };
654
651
  }
655
652
 
@@ -721,9 +718,76 @@ function cleanMarkers(text: string): string {
721
718
  .replace(/<Ai>\s*<think>[\s\S]*?<\/Ai>/gi, "")
722
719
  .replace(/<||DSML||tool_calls>[\s\S]*?<\/||DSML||tool_calls>/gi, "")
723
720
  .replace(/<||DSML||invoke[^>]*>[\s\S]*?<\/||DSML||invoke>/gi, "")
724
- .replace(/<||DSML||parameter[^>]*>[\s\S]*?<\/||DSML||parameter>/gi, "");
721
+ .replace(
722
+ /<||DSML||parameter[^>]*>[\s\S]*?<\/||DSML||parameter>/gi,
723
+ "",
724
+ );
725
+
726
+ return sanitizeBrackets(cleaned);
727
+ }
728
+
729
+ const FUNCTIONAL_BRACKET_PREFIX =
730
+ /\[(at|reply|poke|audio|emotion):[^\]\n]*\]?/gi;
731
+
732
+ function sanitizeBrackets(text: string): string {
733
+ if (!text) return text;
734
+
735
+ const placeholders: string[] = [];
736
+ let working = text.replace(FUNCTIONAL_BRACKET_PREFIX, (match) => {
737
+ const idx = placeholders.length;
738
+ placeholders.push(match);
739
+ return `${idx}`;
740
+ });
741
+
742
+ let result = "";
743
+ let orphanLeftCount = 0;
744
+ let i = 0;
745
+
746
+ while (i < working.length) {
747
+ const ch = working[i];
748
+
749
+ if (ch === "") {
750
+ const endIdx = working.indexOf("", i + 1);
751
+ if (endIdx > i) {
752
+ const idx = parseInt(working.slice(i + 1, endIdx), 10);
753
+ if (!Number.isNaN(idx) && placeholders[idx] !== undefined) {
754
+ result += placeholders[idx];
755
+ i = endIdx + 1;
756
+ continue;
757
+ }
758
+ }
759
+ i++;
760
+ continue;
761
+ }
762
+
763
+ if (ch === "[") {
764
+ let j = i + 1;
765
+ while (j < working.length && working[j] !== "]") {
766
+ j++;
767
+ }
768
+ if (j < working.length) {
769
+ i = j + 1;
770
+ } else {
771
+ orphanLeftCount++;
772
+ i++;
773
+ }
774
+ continue;
775
+ }
725
776
 
726
- return cleaned;
777
+ if (ch === "]") {
778
+ i++;
779
+ continue;
780
+ }
781
+
782
+ result += ch;
783
+ i++;
784
+ }
785
+
786
+ if (orphanLeftCount > 0) {
787
+ result += "]".repeat(orphanLeftCount);
788
+ }
789
+
790
+ return result;
727
791
  }
728
792
 
729
793
  function removeStickerIntentLines(text: string): string {
@@ -1,4 +1,5 @@
1
1
  import type { AISkill, SkillPermissionRole } from "mioku";
2
+ import { normalizeSkillPermissionRole } from "mioku";
2
3
  import type { ChatConfig } from "../types";
3
4
 
4
5
  function normalizeSkillName(name: unknown): string {
@@ -33,24 +34,9 @@ const SKILL_PERMISSION_RANK: Record<SkillPermissionRole, number> = {
33
34
  member: 0,
34
35
  admin: 1,
35
36
  owner: 2,
37
+ master: 2,
36
38
  };
37
39
 
38
- export function normalizeSkillPermissionRole(
39
- role: unknown,
40
- ): SkillPermissionRole {
41
- const normalized = String(role || "")
42
- .trim()
43
- .toLowerCase();
44
- if (
45
- normalized === "member" ||
46
- normalized === "admin" ||
47
- normalized === "owner"
48
- ) {
49
- return normalized;
50
- }
51
- return "member";
52
- }
53
-
54
40
  export function getSkillRequiredPermissionRole(
55
41
  skill: AISkill | undefined,
56
42
  ): SkillPermissionRole {
@@ -80,8 +80,8 @@ export function getForwardId(seg: {
80
80
  return String(seg?.id || seg?.data?.id || "");
81
81
  }
82
82
 
83
- export function getCardData(seg: { data?: unknown }): string | null {
84
- const data = seg?.data;
83
+ export function getCardData(seg: unknown): string | null {
84
+ const data = (seg as { data?: unknown } | null | undefined)?.data;
85
85
  if (!data) return null;
86
86
  return typeof data === "string" ? data : JSON.stringify(data);
87
87
  }
package/core/prompt.ts CHANGED
@@ -143,17 +143,16 @@ const REPLY_STYLE_LENGTH: Record<Strength, string> = {
143
143
  };
144
144
 
145
145
  const AUDIO_MODE_LINE: Record<Strength, string> = {
146
- high:
147
- "- Use voice sparingly. Only use it when spoken delivery is clearly better than text, such as a greeting, a sharp emotional reaction, or a daily phrase.",
146
+ high: "- Use voice sparingly. Only use it when spoken delivery is clearly better than text, such as a greeting, a sharp emotional reaction, or a daily phrase.",
148
147
  medium:
149
148
  "- You may use voice for greetings, reactions, calls, confirmations, or comforting words, but stay selective.",
150
149
  low: "- When a short spoken reaction would make the conversation feel more natural or vivid, you can use voice more freely.",
151
150
  };
152
151
 
153
152
  const MARKDOWN_MODE_LINE: Record<Strength, string> = {
154
- high:
155
- "- Prefer normal chat text. Use Markdown only when the reply truly needs structured presentation, such as a tutorial, comparison, detailed explanation, code sample or processing large amounts of data, such as after a web search or viewing a webpage.",
156
- medium: "- Use Markdown when your responses require a structured presentation.",
153
+ high: "- Prefer normal chat text. Use Markdown only when the reply truly needs structured presentation, such as a tutorial, comparison, detailed explanation, code sample or processing large amounts of data, such as after a web search or viewing a webpage.",
154
+ medium:
155
+ "- Use Markdown when your responses require a structured presentation.",
157
156
  low: "- Use Markdown freely where it can make your responses clearer.",
158
157
  };
159
158
 
@@ -174,15 +173,13 @@ const TOOL_INTENSITY_BLOCK: Record<Strength, string> = {
174
173
  };
175
174
 
176
175
  const EMOJI_MODE_LINE: Record<Strength, string> = {
177
- high:
178
- "- Do not use stickers in consecutive chat turns. Use a sticker only when you are in an emotionally intense state.",
176
+ high: "- Do not use stickers in consecutive chat turns. Use a sticker only when you are in an emotionally intense state.",
179
177
  medium: "- Do not use stickers in consecutive chat turns.",
180
178
  low: "- Feel free to use a sticker whenever you want.",
181
179
  };
182
180
 
183
181
  const WEB_SEARCH_LINE: Record<Strength, string> = {
184
- high:
185
- "- When facts may be outdated or uncertain, proactively call web_search instead of guessing.",
182
+ high: "- When facts may be outdated or uncertain, proactively call web_search instead of guessing.",
186
183
  medium: "- Use web_search when current or external info is needed.",
187
184
  low: "- Use web_search only when the user explicitly needs external/current information.",
188
185
  };
@@ -242,7 +239,7 @@ function buildReplyStyleSection(
242
239
  ### Behavior Guidelines
243
240
  - You are a participant in this chat, not an assistant. Chat naturally like a real person.
244
241
  - ${REPLY_STYLE_LENGTH[lengthStrength]}
245
- - Match the language used by others in the chat (if they speak Chinese, reply in Chinese, etc.).
242
+ - Match the language used by others in the chat.
246
243
  - Don't repeat yourself or echo what others just said.
247
244
  - **NEVER use action descriptions like *xxx* or (xxx) — just speak as a normal person would**
248
245
  - **${markdownBehaviorLine(config)}**
@@ -270,31 +267,24 @@ function buildResponseFormatSection(
270
267
 
271
268
  lines.push(`Your text response IS your reply to the chat. It will be sent directly as a message.
272
269
  - **IMPORTANT: Output ONLY your final reply text. Do NOT include your thinking process, reasoning, analysis, or internal thoughts.**
273
- - Do NOT prefix your response with phrases like "Let me think", "I should", "I need to", "Based on", "Looking at", etc.
274
270
  - Do NOT explain what you're doing or why. Just say what you want to say directly.
275
- - **MULTIPLE MESSAGES (CRITICAL!): Each line (separated by Enter/Return) will be sent as a SEPARATE message.**
271
+ - **MULTIPLE MESSAGES: Each line (separated by Enter/Return) will be sent as a SEPARATE message.**
276
272
  - If you want to send multiple messages, just press Enter and write the next line
277
- - Each line = one message sent to the chat
278
273
  - **If your reply has multiple sentences or different points, ALWAYS use real line breaks to separate them**
279
- - NEVER use "\\" or literal "\\n" to simulate a new line
280
- - **MESSAGE ORDER MATTERS**: messages are sent top-to-bottom, one line at a time.
281
- - For action markers like [] or [audio:...], put them on their own line when they are meant to be a separate action.
274
+ - For action markers like [] , put them on their own line when they are meant to be a separate action.
282
275
 
283
276
  - **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
284
277
  - Use [at:123456] in your text to @ someone (123456 is the QQ number)
285
- - Use [poke:123456] in your text to poke someone. IMPORTANT: when you plan to poke a user, DON't emphasize words like "戳你一下 or 戳回去" to describe your actions
278
+ - Use [poke:123456] in your text to poke someone. IMPORTANT: when you plan to poke a user, DON't describe your poke actions.
286
279
  - Use [reply:123456] at the START of a line to quote-reply that message (123456 is message_id)
287
- - **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**
288
- - These markers will be automatically parsed and removed from your sent message`);
280
+ - **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**`);
289
281
 
290
282
  if (ctx.config.audio?.enabled && ctx.config.audio.baseUrl?.trim()) {
291
283
  lines.push(`
292
284
  ### Optional Voice Message Format
293
285
  - You MAY optionally send one voice message by writing [audio:content]
294
286
  - Audio is OPTIONAL. Do NOT use it in every reply
295
- The voice message function sends plain text and cannot be used for singing. If a user needs you to sing, other skills should be considered first.
296
- - Put [audio:...] on its own line when you want it sent as a separate message in sequence
297
- - Example: "[audio:おはようー]"
287
+ The voice message function sends plain text and cannot be used for singing。
298
288
  ${AUDIO_MODE_LINE[audioStrength]}`);
299
289
  }
300
290
 
@@ -314,9 +304,6 @@ ${MARKDOWN_MODE_LINE[markdownStrength]}
314
304
  lines.push(`
315
305
  ### Tool Calling Format
316
306
  - When you decide to use a tool, you MUST use the structured tool_calls mechanism provided by the API
317
- - Do NOT output tool calls, tool names, or tool arguments in your reply text under any circumstances
318
- - Do NOT use XML, JSON, or any text format to describe tool calls — only use the API's tool_calls field
319
- - Each tool's description contains its own usage guidance; read those before calling a tool. If a tool's description says "use only when X" or "do not call for every question", respect that.
320
307
  - web_search and web_read_page are limited per conversation; do not retry excessively`);
321
308
 
322
309
  appendEmojiSection(lines, ctx, emojiStrength);
@@ -341,9 +328,7 @@ function appendEmojiSection(
341
328
  ### Optional Sticker / Emoji Format
342
329
  - You MAY optionally request one matching sticker by writing exactly [] on its own line
343
330
  ${EMOJI_MODE_LINE[emojiStrength]}
344
- - Never put an emotion, label, character, or any other text inside the brackets
345
- - Output at most one [] block in a turn
346
- - A separate sticker selection agent will read the current conversation and choose the actual sticker`);
331
+ - Never put an emotion, label, character, or any other text inside the brackets`);
347
332
  }
348
333
 
349
334
  function appendExternalSkillsSection(
@@ -453,7 +438,9 @@ export function buildDynamicUserContext(ctx: DynamicPromptContext): string {
453
438
  sections.push(`## Planner's Analysis\n${ctx.plannerThoughts}`);
454
439
  }
455
440
 
456
- sections.push(buildReplyStyleSection(ctx.config, ctx.botNickname, lengthStrength));
441
+ sections.push(
442
+ buildReplyStyleSection(ctx.config, ctx.botNickname, lengthStrength),
443
+ );
457
444
  sections.push(buildEmotionSection(ctx));
458
445
 
459
446
  return sections.join("\n\n");
@@ -539,7 +526,6 @@ function buildPokedGuidance(length: Strength): string[] {
539
526
  return [
540
527
  "Someone pokes you in a group, probably out of non-malicious play or to draw your attention to what happened in the group chat.",
541
528
  "Don't make a fuss about replying, just observe whether the chat history in the group has noteworthy content, and if not, simply say hello or express concern to the user.",
542
- "Reply naturally in combination with the context, don't say something like \"怎么又来戳我了\"",
543
529
  POKED_LENGTH[length],
544
530
  ].filter(Boolean);
545
531
  }
@@ -621,13 +607,8 @@ function buildChatHistorySection(ctx: DynamicPromptContext): string {
621
607
  }
622
608
  flushAssistant();
623
609
 
624
- return `## Recent Context (Only reference if directly relevant)
625
- Just the last few messages - don't overthink it or dig into old conversations:
626
-
610
+ return `## Recent Context
627
611
  ${mergedLines.join("\n")}
628
-
629
- Note: Messages may contain media tags like [image:描述], [video:描述], [forward:摘要], [card:摘要], or [group_notice:摘要]. If you need detailed information about an image or video, use the view_media tool with the message ID.
630
-
631
612
  -- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --`;
632
613
  }
633
614
 
@@ -656,7 +637,7 @@ ${messageBlocks.join("\n")}
656
637
  IMPORTANT: You do NOT need to reply to each person or each message above. Give ONE casual response to the group as a whole.`;
657
638
  }
658
639
 
659
- return `## >>> Target Message (Reply to THIS) <<<
640
+ return `## >>> Target Message <<<
660
641
  [${timeStr}] ${target.userName}(${target.userId}, ${target.userRole}${target.userTitle ? `, ${target.userTitle}` : ""})${msgIdStr}: ${target.content}`;
661
642
  }
662
643
 
@@ -686,7 +667,7 @@ function buildEmotionSection(ctx: DynamicPromptContext): string {
686
667
  lines.push(`Available emotions: ${availableEmotions.join(", ")}`);
687
668
  }
688
669
  lines.push(
689
- "You may switch your emotion state by writing [emotion:emotion_name]. The marker is not sent to the chat. Only use available emotions.",
670
+ "You may switch your emotion state by writing [emotion:emotion_name].",
690
671
  );
691
672
 
692
673
  if (examples.length > 0) {
@@ -701,7 +682,9 @@ function buildEmotionSection(ctx: DynamicPromptContext): string {
701
682
  }
702
683
 
703
684
  function normalizeEmotionName(value: unknown): string {
704
- return String(value || "").trim().toLowerCase();
685
+ return String(value || "")
686
+ .trim()
687
+ .toLowerCase();
705
688
  }
706
689
 
707
690
  function normalizeEmotionExamples(value: unknown): string[] {
@@ -743,7 +726,5 @@ export function buildRecallMemoryFeatureSection(config: ChatConfig): string {
743
726
  return `
744
727
  ### Memory Recall Tool
745
728
  - recall_memory: Delegate recall to a memory worker model. Pass a clear recall question and let the worker search historical logs.
746
- - Use recall_memory ONLY when there is explicit need to recall past content and required information is clearly missing from current context.
747
- - Do NOT call recall_memory for every question.
748
- - The worker returns historical logs with timestamps; treat them as past records, not newly sent messages.`;
729
+ - Use recall_memory ONLY when there is explicit need to recall past content and required information is clearly missing from current context.`;
749
730
  }
@@ -161,11 +161,11 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
161
161
  );
162
162
  }
163
163
 
164
- const ai = toolCtx.aiService.getDefault();
165
- if (!ai) return { error: "AI instance not available" };
164
+ const visionAI = toolCtx.aiService.getInstanceByRole?.("vision") ?? toolCtx.aiService.getDefault();
165
+ if (!visionAI) return { error: "AI instance not available" };
166
166
 
167
167
  const result = await describeImage(
168
- ai,
168
+ visionAI,
169
169
  media.url,
170
170
  toolCtx.config.multimodalWorkingModel,
171
171
  toolCtx.event?.raw_message || undefined,
@@ -214,11 +214,11 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
214
214
  );
215
215
  }
216
216
 
217
- const ai = toolCtx.aiService.getDefault();
218
- if (!ai) return { error: "AI instance not available" };
217
+ const visionAI = toolCtx.aiService.getInstanceByRole?.("vision") ?? toolCtx.aiService.getDefault();
218
+ if (!visionAI) return { error: "AI instance not available" };
219
219
 
220
220
  const summary = await summarizeVideoContent(videoFile.path, videoFile.byteSize, {
221
- ai,
221
+ ai: visionAI,
222
222
  multimodalWorkingModel: toolCtx.config.multimodalWorkingModel,
223
223
  logger: {
224
224
  info: (m) => logger.info(m),
@@ -269,11 +269,11 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
269
269
  }
270
270
 
271
271
  const { describeImage } = await import("../multimodal");
272
- const ai = toolCtx.aiService.getDefault();
273
- if (!ai) return { error: "AI instance not available" };
272
+ const visionAI = toolCtx.aiService.getInstanceByRole?.("vision") ?? toolCtx.aiService.getDefault();
273
+ if (!visionAI) return { error: "AI instance not available" };
274
274
 
275
275
  const result = await describeImage(
276
- ai,
276
+ visionAI,
277
277
  avatarUrl,
278
278
  toolCtx.config.multimodalWorkingModel,
279
279
  `User ${args.user_id}'s QQ avatar`,
package/core/tools/web.ts CHANGED
@@ -234,7 +234,7 @@ export function createWebReadPageTool(toolCtx: ToolContext): AITool {
234
234
  handler: async (args) => {
235
235
  try {
236
236
  const ai = toolCtx.config.webReader.useWorkingModel
237
- ? toolCtx.aiService.getDefault()
237
+ ? (toolCtx.aiService.getInstanceByRole?.("working") ?? toolCtx.aiService.getDefault())
238
238
  : undefined;
239
239
  if (toolCtx.config.webReader.useWorkingModel && !ai) {
240
240
  return { success: false, error: "AI instance not available" };
@@ -275,7 +275,7 @@ export function createRecallMemoryTool(toolCtx: ToolContext): AITool {
275
275
  const question = String(args?.question || "").trim();
276
276
  if (!question) return { success: false, error: "question is required" };
277
277
 
278
- const ai = toolCtx.aiService.getDefault();
278
+ const ai = toolCtx.aiService.getInstanceByRole?.("working") ?? toolCtx.aiService.getDefault();
279
279
  if (!ai) return { success: false, error: "AI instance not available" };
280
280
 
281
281
  const groupHistoryLimit = resolveGroupRecallLimit(toolCtx.config.memory?.groupHistoryLimit);
@@ -1,4 +1,8 @@
1
1
  import type { ChatPluginContext, ChatHandlerState } from "../context";
2
+ import type {
3
+ GroupMessageEvent,
4
+ PrivateMessageEvent,
5
+ } from "napcat-sdk";
2
6
  import {
3
7
  isGroupAllowed,
4
8
  shouldTrigger,
@@ -32,7 +36,7 @@ export function createMessageHandler(
32
36
  const { ctx } = pluginCtx;
33
37
  const { getConfig, matchMessageCommands, runtimeState } = state;
34
38
 
35
- return async (e: any) => {
39
+ return async (e: GroupMessageEvent | PrivateMessageEvent) => {
36
40
  const isGroup = e.message_type === "group";
37
41
  const groupId: number | undefined = isGroup ? e.group_id : undefined;
38
42
  const cfg = await getConfig(groupId);
@@ -68,11 +72,11 @@ export function createMessageHandler(
68
72
 
69
73
  // 媒体分析
70
74
  if (isGroup && groupId && e.message && !isMediaAnalysisBlocked(cfg, userId)) {
71
- const ai = pluginCtx.aiService.getDefault();
75
+ const visionAI = pluginCtx.visionAIInstance ?? pluginCtx.aiService.getDefault();
72
76
  const bot = ctx.pickBot(e.self_id) as any;
73
- const mediaOptions = ai
77
+ const mediaOptions = visionAI
74
78
  ? buildHistoryMediaProcessingOptions(
75
- ai,
79
+ visionAI,
76
80
  cfg,
77
81
  pluginCtx.db,
78
82
  bot,
@@ -91,13 +95,13 @@ export function createMessageHandler(
91
95
  )
92
96
  : undefined;
93
97
 
94
- if (ai && cfg.enableMediaRecognition) {
98
+ if (visionAI && cfg.enableMediaRecognition) {
95
99
  const { processImage } = await import("../core/media/image-analyzer");
96
100
  for (const seg of e.message) {
97
101
  if (seg.type === "image") {
98
102
  const imageUrl = getSegmentUrl(seg);
99
103
  if (imageUrl) {
100
- processImage(ai, imageUrl, cfg.multimodalWorkingModel, pluginCtx.db, {
104
+ processImage(visionAI, imageUrl, cfg.multimodalWorkingModel, pluginCtx.db, {
101
105
  runAIRequest: (request) =>
102
106
  pluginCtx.runWithRateLimitGuard(request, {
103
107
  userId,
package/handlers/poke.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ChatPluginContext, ChatHandlerState } from "../context";
2
2
  import type { TargetMessage } from "../types";
3
+ import type { GroupPokeNoticeEvent } from "napcat-sdk";
3
4
  import { getBotRole, isGroupAllowed } from "../utils";
4
5
  import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
5
6
  import { finalizeChatTurn } from "../core/chat-turn";
@@ -12,7 +13,7 @@ export function createPokeHandler(
12
13
  const { ctx } = pluginCtx;
13
14
  const { getConfig, runtimeState, pokeCooldowns } = state;
14
15
 
15
- return async (e: any) => {
16
+ return async (e: GroupPokeNoticeEvent) => {
16
17
  if (e.target_id !== e.self_id) return;
17
18
  const groupId = e.group_id;
18
19
  const cfg = groupId ? await getConfig(groupId) : await getConfig();
@@ -36,7 +37,7 @@ export function createPokeHandler(
36
37
  "group",
37
38
  groupId,
38
39
  );
39
- const userId = e.user_id || e.operator_id;
40
+ const userId = e.user_id;
40
41
  const botRole = await getBotRole(groupId, ctx, e.self_id);
41
42
  const botNickname =
42
43
  cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
package/index.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { definePlugin } from "mioki";
2
- import type { MiokiContext } from "mioki";
3
- import type { AIInstance, AIModelRole, AIService, ConfigService, ScreenshotService } from "mioku";
4
- import { getPluginRuntimeState } from "mioku";
1
+ import { definePlugin, type MiokiContext } from "mioki";
2
+ import type { AIInstance, AIModelRole, AIService } from "mioku";
3
+ import {
4
+ getPluginRuntimeState,
5
+ getService,
6
+ Services,
7
+ } from "mioku";
5
8
  import type { ChatConfig, ChatMessage, ChatGroupsFile, TargetMessage } from "./types";
6
9
  import { initDatabase } from "./db";
7
10
  import { SessionManager } from "./manage/session";
@@ -115,9 +118,9 @@ export default definePlugin({
115
118
  async setup(ctx: MiokiContext) {
116
119
  ctx.logger.info("聊天插件正在初始化...");
117
120
 
118
- const aiService = ctx.services?.ai as AIService | undefined;
119
- const configService = ctx.services?.config as ConfigService | undefined;
120
- const screenshotService = ctx.services?.screenshot as ScreenshotService | undefined;
121
+ const aiService = getService(ctx, Services.AI);
122
+ const configService = getService(ctx, Services.Config);
123
+ const screenshotService = getService(ctx, Services.Screenshot);
121
124
  let warnedMarkdownScreenshotUnavailable = false;
122
125
 
123
126
  if (configService) {
@@ -447,7 +450,7 @@ export default definePlugin({
447
450
  aiService.registerChatRuntime(runtime);
448
451
 
449
452
  ctx.handle("message", createMessageHandler(pluginCtx, handlerState));
450
- ctx.handle("notice.group.poke" as any, createPokeHandler(pluginCtx, handlerState));
453
+ ctx.handle("notice.group.poke", createPokeHandler(pluginCtx, handlerState));
451
454
 
452
455
  ctx.logger.info(
453
456
  `聊天插件加载成功 (main=${roleModels.main || "?"}, work=${roleModels.working || "?"}, vision=${roleModels.vision || "?"})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "openai": "^4.0.0",
47
+ "puppeteer": "^23.0.0",
47
48
  "sharp": "^0.34.5"
48
49
  },
49
50
  "devDependencies": {