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.
@@ -5,6 +5,7 @@ import type { ChatDatabase } from "../db";
5
5
  import type { HumanizeEngine } from "../humanize";
6
6
  import type { SkillSessionManager } from "./skill-session";
7
7
  import type { GroupStructuredHistoryManager } from "./group-structured-history";
8
+ import { buildStructuredUserInputFromEvent } from "./group-structured-history";
8
9
  import type { SessionManager } from "./session";
9
10
  import type { RateLimiter } from "./rate-limiter";
10
11
  import type { IdleCheckManager } from "./idle-check";
@@ -21,6 +22,7 @@ import type {
21
22
  BuildToolContext,
22
23
  BuildStructuredUserInput,
23
24
  } from "./types";
25
+ import type { ChatPluginContext } from "../context";
24
26
  import { getBotRole } from "../utils";
25
27
 
26
28
  interface CooldownMessage {
@@ -38,33 +40,56 @@ export class CooldownManager {
38
40
  private groupCooldownMessages = new Map<string, CooldownMessage[]>();
39
41
  private cooldownTimeoutIds = new Map<string, NodeJS.Timeout>();
40
42
 
41
- constructor(
42
- private ctx: MiokiContext,
43
- private cfg: ChatConfig,
44
- private db: ChatDatabase,
45
- private humanize: HumanizeEngine,
46
- private aiInstance: AIInstance,
47
- private aiService: import("mioku").AIService,
48
- private skillManager: SkillSessionManager,
49
- private groupStructuredHistory: GroupStructuredHistoryManager,
50
- private sessionManager: SessionManager,
51
- private rateLimiter: RateLimiter,
52
- private idleCheckManager: IdleCheckManager,
53
- private runWithRateLimitGuard: RunRateLimitGuard,
54
- private buildHistoryMediaOptions: (
55
- ai: AIInstance,
56
- cfg: ChatConfig,
57
- ) => HistoryMediaOptions,
58
- private getGroupHistoryMessages: GetGroupHistoryMessages,
59
- private getGroupInfoData: GetGroupInfoData,
60
- private getHumanizeContexts: GetHumanizeContexts,
61
- private sendAIResponse: SendAIResponse,
62
- private saveBotMessages: SaveBotMessages,
63
- private sendEmoji: SendEmoji,
64
- private runChat: RunChat,
65
- private buildToolContext: BuildToolContext,
66
- private buildStructuredUserInputFromEvent: BuildStructuredUserInput,
67
- ) {}
43
+ private ctx: MiokiContext;
44
+ private cfg: ChatConfig;
45
+ private db: ChatDatabase;
46
+ private humanize: HumanizeEngine;
47
+ private aiInstance: AIInstance;
48
+ private aiService: import("mioku").AIService;
49
+ private skillManager: SkillSessionManager;
50
+ private groupStructuredHistory: GroupStructuredHistoryManager;
51
+ private sessionManager: SessionManager;
52
+ private rateLimiter: RateLimiter;
53
+ private idleCheckManager: IdleCheckManager;
54
+ private runWithRateLimitGuard: RunRateLimitGuard;
55
+ private buildHistoryMediaOptions: (
56
+ ai: AIInstance,
57
+ cfg: ChatConfig,
58
+ ) => HistoryMediaOptions;
59
+ private getGroupHistoryMessages: GetGroupHistoryMessages;
60
+ private getGroupInfoData: GetGroupInfoData;
61
+ private getHumanizeContexts: GetHumanizeContexts;
62
+ private sendAIResponse: SendAIResponse;
63
+ private saveBotMessages: SaveBotMessages;
64
+ private sendEmoji: SendEmoji;
65
+ private runChat: RunChat;
66
+ private buildToolContext: BuildToolContext;
67
+ private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
68
+
69
+ constructor(pluginCtx: ChatPluginContext) {
70
+ this.ctx = pluginCtx.ctx;
71
+ this.cfg = pluginCtx.config;
72
+ this.db = pluginCtx.db;
73
+ this.humanize = pluginCtx.humanize;
74
+ this.aiInstance = pluginCtx.aiInstance;
75
+ this.aiService = pluginCtx.aiService;
76
+ this.skillManager = pluginCtx.skillManager;
77
+ this.groupStructuredHistory = pluginCtx.groupStructuredHistory;
78
+ this.sessionManager = pluginCtx.sessionManager;
79
+ this.rateLimiter = pluginCtx.rateLimiter;
80
+ this.idleCheckManager = pluginCtx.idleCheckManager;
81
+ this.runWithRateLimitGuard = pluginCtx.runWithRateLimitGuard;
82
+ this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
83
+ this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
84
+ this.getGroupInfoData = pluginCtx.getGroupInfoData;
85
+ this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
86
+ this.sendAIResponse = pluginCtx.sendAIResponse;
87
+ this.saveBotMessages = pluginCtx.saveBotMessages;
88
+ this.sendEmoji = pluginCtx.sendEmoji;
89
+ this.runChat = pluginCtx.runChat;
90
+ this.buildToolContext = pluginCtx.buildToolContext;
91
+ this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
92
+ }
68
93
 
69
94
  startCooldownTimer(
70
95
  groupSessionId: string,
@@ -1,5 +1,6 @@
1
1
  import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
2
2
  import type { MultimodalContentItem } from "mioku";
3
+ import type { TargetMessage } from "../types";
3
4
 
4
5
  export interface StructuredUserInput {
5
6
  userName: string;
@@ -94,6 +95,40 @@ export function buildStructuredUserMessages(
94
95
  }));
95
96
  }
96
97
 
98
+ export function buildStructuredUserInputFromEvent(
99
+ event: any,
100
+ content: string,
101
+ fallbackTimestamp: number = Date.now(),
102
+ ): StructuredUserInput {
103
+ return {
104
+ userName:
105
+ event?.sender?.card ||
106
+ event?.sender?.nickname ||
107
+ String(event?.user_id || event?.sender?.user_id || 0),
108
+ userId: event?.user_id || event?.sender?.user_id || 0,
109
+ userRole: event?.sender?.role || "member",
110
+ userTitle: event?.sender?.title,
111
+ content,
112
+ messageId: event?.message_id,
113
+ timestamp:
114
+ typeof event?.time === "number" ? event.time * 1000 : fallbackTimestamp,
115
+ };
116
+ }
117
+
118
+ export function buildStructuredUserInputFromTarget(
119
+ targetMessage: TargetMessage,
120
+ ): StructuredUserInput {
121
+ return {
122
+ userName: targetMessage.userName,
123
+ userId: targetMessage.userId,
124
+ userRole: targetMessage.userRole,
125
+ userTitle: targetMessage.userTitle,
126
+ content: targetMessage.content,
127
+ messageId: targetMessage.messageId,
128
+ timestamp: targetMessage.timestamp,
129
+ };
130
+ }
131
+
97
132
  export function attachImagesToCurrentUserMessages(
98
133
  messages: ChatCompletionMessageParam[],
99
134
  pendingImageUrls?: string[],
@@ -16,6 +16,7 @@ import type {
16
16
  RunChat,
17
17
  BuildToolContext,
18
18
  } from "./types";
19
+ import type { ChatPluginContext } from "../context";
19
20
  import { getBotRole, isGroupAllowed } from "../utils";
20
21
 
21
22
  export class IdleCheckManager {
@@ -28,26 +29,45 @@ export class IdleCheckManager {
28
29
  private groupBotsMapping = new Map<string, Set<number>>();
29
30
  private intervalHandle: NodeJS.Timeout | null = null;
30
31
 
31
- constructor(
32
- private ctx: MiokiContext,
33
- private cfg: ChatConfig,
34
- private db: ChatDatabase,
35
- private humanize: HumanizeEngine,
36
- private aiInstance: AIInstance,
37
- private aiService: import("mioku").AIService,
38
- private skillManager: SkillSessionManager,
39
- private groupStructuredHistory: GroupStructuredHistoryManager,
40
- private sessionManager: SessionManager,
41
- private runWithRateLimitGuard: RunRateLimitGuard,
42
- private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions,
43
- private getGroupHistoryMessages: GetGroupHistoryMessages,
44
- private getGroupInfoData: GetGroupInfoData,
45
- private sendAIResponse: SendAIResponse,
46
- private saveBotMessages: SaveBotMessages,
47
- private runChat: RunChat,
48
- private buildToolContext: BuildToolContext,
49
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void,
50
- ) {}
32
+ private ctx: MiokiContext;
33
+ private cfg: ChatConfig;
34
+ private db: ChatDatabase;
35
+ private humanize: HumanizeEngine;
36
+ private aiInstance: AIInstance;
37
+ private aiService: import("mioku").AIService;
38
+ private skillManager: SkillSessionManager;
39
+ private groupStructuredHistory: GroupStructuredHistoryManager;
40
+ private sessionManager: SessionManager;
41
+ private runWithRateLimitGuard: RunRateLimitGuard;
42
+ private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions;
43
+ private getGroupHistoryMessages: GetGroupHistoryMessages;
44
+ private getGroupInfoData: GetGroupInfoData;
45
+ private sendAIResponse: SendAIResponse;
46
+ private saveBotMessages: SaveBotMessages;
47
+ private runChat: RunChat;
48
+ private buildToolContext: BuildToolContext;
49
+ private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
50
+
51
+ constructor(pluginCtx: ChatPluginContext) {
52
+ this.ctx = pluginCtx.ctx;
53
+ this.cfg = pluginCtx.config;
54
+ this.db = pluginCtx.db;
55
+ this.humanize = pluginCtx.humanize;
56
+ this.aiInstance = pluginCtx.aiInstance;
57
+ this.aiService = pluginCtx.aiService;
58
+ this.skillManager = pluginCtx.skillManager;
59
+ this.groupStructuredHistory = pluginCtx.groupStructuredHistory;
60
+ this.sessionManager = pluginCtx.sessionManager;
61
+ this.runWithRateLimitGuard = pluginCtx.runWithRateLimitGuard;
62
+ this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
63
+ this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
64
+ this.getGroupInfoData = pluginCtx.getGroupInfoData;
65
+ this.sendAIResponse = pluginCtx.sendAIResponse;
66
+ this.saveBotMessages = pluginCtx.saveBotMessages;
67
+ this.runChat = pluginCtx.runChat;
68
+ this.buildToolContext = pluginCtx.buildToolContext;
69
+ this.startCooldownTimer = pluginCtx.startCooldownTimer;
70
+ }
51
71
 
52
72
  recordActivity(groupSessionId: string): void {
53
73
  this.groupLastActivityTime.set(groupSessionId, Date.now());
@@ -5,6 +5,7 @@ import type { ChatDatabase } from "../db";
5
5
  import type { HumanizeEngine } from "../humanize";
6
6
  import type { SkillSessionManager } from "./skill-session";
7
7
  import type { GroupStructuredHistoryManager, StructuredUserInput } from "./group-structured-history";
8
+ import { buildStructuredUserInputFromEvent } from "./group-structured-history";
8
9
  import type { SessionManager } from "./session";
9
10
  import type { RateLimiter } from "./rate-limiter";
10
11
  import type { MessageQueueManager } from "../utils/queue";
@@ -21,6 +22,7 @@ import type {
21
22
  BuildToolContext,
22
23
  BuildStructuredUserInput,
23
24
  } from "./types";
25
+ import type { ChatPluginContext } from "../context";
24
26
  import { extractContent, getBotRole } from "../utils";
25
27
 
26
28
  interface DynamicDelayQueueData {
@@ -32,31 +34,55 @@ interface DynamicDelayQueueData {
32
34
  export class QueueProcessor {
33
35
  private dynamicDelayQueues = new Map<string, DynamicDelayQueueData>();
34
36
 
35
- constructor(
36
- private ctx: MiokiContext,
37
- private cfg: ChatConfig,
38
- private db: ChatDatabase,
39
- private humanize: HumanizeEngine,
40
- private aiInstance: AIInstance,
41
- private aiService: import("mioku").AIService,
42
- private skillManager: SkillSessionManager,
43
- private groupStructuredHistory: GroupStructuredHistoryManager,
44
- private sessionManager: SessionManager,
45
- private rateLimiter: RateLimiter,
46
- private queueManager: MessageQueueManager,
47
- private runWithRateLimitGuard: RunRateLimitGuard,
48
- private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions,
49
- private getGroupHistoryMessages: GetGroupHistoryMessages,
50
- private getGroupInfoData: GetGroupInfoData,
51
- private getHumanizeContexts: GetHumanizeContexts,
52
- private sendAIResponse: SendAIResponse,
53
- private saveBotMessages: SaveBotMessages,
54
- private sendEmoji: SendEmoji,
55
- private runChat: RunChat,
56
- private buildToolContext: BuildToolContext,
57
- private buildStructuredUserInputFromEvent: BuildStructuredUserInput,
58
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void,
59
- ) {}
37
+ private ctx: MiokiContext;
38
+ private cfg: ChatConfig;
39
+ private db: ChatDatabase;
40
+ private humanize: HumanizeEngine;
41
+ private aiInstance: AIInstance;
42
+ private aiService: import("mioku").AIService;
43
+ private skillManager: SkillSessionManager;
44
+ private groupStructuredHistory: GroupStructuredHistoryManager;
45
+ private sessionManager: SessionManager;
46
+ private rateLimiter: RateLimiter;
47
+ private queueManager: MessageQueueManager;
48
+ private runWithRateLimitGuard: RunRateLimitGuard;
49
+ private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions;
50
+ private getGroupHistoryMessages: GetGroupHistoryMessages;
51
+ private getGroupInfoData: GetGroupInfoData;
52
+ private getHumanizeContexts: GetHumanizeContexts;
53
+ private sendAIResponse: SendAIResponse;
54
+ private saveBotMessages: SaveBotMessages;
55
+ private sendEmoji: SendEmoji;
56
+ private runChat: RunChat;
57
+ private buildToolContext: BuildToolContext;
58
+ private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
59
+ private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
60
+
61
+ constructor(pluginCtx: ChatPluginContext) {
62
+ this.ctx = pluginCtx.ctx;
63
+ this.cfg = pluginCtx.config;
64
+ this.db = pluginCtx.db;
65
+ this.humanize = pluginCtx.humanize;
66
+ this.aiInstance = pluginCtx.aiInstance;
67
+ this.aiService = pluginCtx.aiService;
68
+ this.skillManager = pluginCtx.skillManager;
69
+ this.groupStructuredHistory = pluginCtx.groupStructuredHistory;
70
+ this.sessionManager = pluginCtx.sessionManager;
71
+ this.rateLimiter = pluginCtx.rateLimiter;
72
+ this.queueManager = pluginCtx.queueManager;
73
+ this.runWithRateLimitGuard = pluginCtx.runWithRateLimitGuard;
74
+ this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
75
+ this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
76
+ this.getGroupInfoData = pluginCtx.getGroupInfoData;
77
+ this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
78
+ this.sendAIResponse = pluginCtx.sendAIResponse;
79
+ this.saveBotMessages = pluginCtx.saveBotMessages;
80
+ this.sendEmoji = pluginCtx.sendEmoji;
81
+ this.runChat = pluginCtx.runChat;
82
+ this.buildToolContext = pluginCtx.buildToolContext;
83
+ this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
84
+ this.startCooldownTimer = pluginCtx.startCooldownTimer;
85
+ }
60
86
 
61
87
  collectDynamicDelayMessage(groupSessionId: string, event: any, content: string): void {
62
88
  let queueData = this.dynamicDelayQueues.get(groupSessionId);
@@ -0,0 +1,70 @@
1
+ import type { RateLimiter } from "./rate-limiter";
2
+
3
+ const RATE_LIMIT_RETRY_DELAY_MS = 5_000;
4
+ const RATE_LIMIT_MAX_RETRIES = 2;
5
+
6
+ type Logger = { warn: (...args: unknown[]) => void };
7
+
8
+ // Wraps an AI request with global rate-limit blocking + per-user/group RPM
9
+ // limiting. On a 429 it backs off and retries up to RATE_LIMIT_MAX_RETRIES.
10
+ export class RateLimitGuard {
11
+ private blockedUntil = 0;
12
+
13
+ constructor(
14
+ private readonly rateLimiter: RateLimiter,
15
+ private readonly log: Logger,
16
+ ) {}
17
+
18
+ isBlocked(): boolean {
19
+ return Date.now() < this.blockedUntil;
20
+ }
21
+
22
+ private markBlocked(): void {
23
+ this.blockedUntil = Date.now() + RATE_LIMIT_RETRY_DELAY_MS;
24
+ }
25
+
26
+ private isRateLimitError(err: unknown): boolean {
27
+ const s = String(err).toLowerCase();
28
+ return s.includes("429") || s.includes("rate limit");
29
+ }
30
+
31
+ async run<T>(
32
+ request: () => Promise<T>,
33
+ opts?: { userId?: number; groupId?: number; label?: string },
34
+ ): Promise<T | null> {
35
+ if (this.isBlocked()) {
36
+ this.log.warn(
37
+ `[Chat] AI request skipped due to rate limit block${opts?.label ? ` (${opts.label})` : ""}`,
38
+ );
39
+ return null;
40
+ }
41
+ if (!this.rateLimiter.canRunAIRequest(opts?.userId, opts?.groupId)) {
42
+ this.log.warn(
43
+ `[Chat] AI request skipped due to RPM limit${opts?.label ? ` (${opts.label})` : ""}`,
44
+ );
45
+ return null;
46
+ }
47
+ this.rateLimiter.recordAIRequest(opts?.userId, opts?.groupId);
48
+
49
+ let retries = 0;
50
+ while (true) {
51
+ try {
52
+ const result = await request();
53
+ this.blockedUntil = 0;
54
+ return result;
55
+ } catch (err) {
56
+ if (!this.isRateLimitError(err)) throw err;
57
+ this.markBlocked();
58
+ if (retries >= RATE_LIMIT_MAX_RETRIES) throw err;
59
+ retries += 1;
60
+ this.log.warn(
61
+ `[Chat] Rate limit hit, waiting ${RATE_LIMIT_RETRY_DELAY_MS / 1000}s...`,
62
+ );
63
+ await new Promise((resolve) =>
64
+ setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS),
65
+ );
66
+ if (this.isBlocked()) this.blockedUntil = 0;
67
+ }
68
+ }
69
+ }
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -0,0 +1,124 @@
1
+ import type {
2
+ AITool,
3
+ ChatRuntimeInformationRequestOptions,
4
+ ChatRuntimeNoticeOptions,
5
+ ChatRuntimePromptInjection,
6
+ ChatRuntimeResult,
7
+ ChatRuntimeCollectedInfo,
8
+ } from "mioku";
9
+ import type { ChatPluginContext, ChatRuntime } from "../context";
10
+ import type { ChatConfig } from "../types";
11
+ import { executeChatRuntimeRequest } from "../core/chat-turn";
12
+
13
+ export function createChatRuntime(
14
+ pluginCtx: ChatPluginContext,
15
+ getConfig: () => Promise<ChatConfig>,
16
+ ): ChatRuntime {
17
+ return {
18
+ async requestInformation(
19
+ options: ChatRuntimeInformationRequestOptions,
20
+ ): Promise<ChatRuntimeResult> {
21
+ let collectedInfo: ChatRuntimeCollectedInfo | null = null;
22
+ const toolName = options.toolName || "submit_requested_information";
23
+ const extraTools: AITool[] = [
24
+ {
25
+ name: toolName,
26
+ description:
27
+ options.toolDescription ||
28
+ "Submit structured information extracted from the conversation when enough details are known.",
29
+ parameters: {
30
+ type: "object",
31
+ properties: {
32
+ data: options.schema,
33
+ isComplete: {
34
+ type: "boolean",
35
+ description:
36
+ "Whether the collected information is complete enough for the caller to continue.",
37
+ },
38
+ confidence: {
39
+ type: "number",
40
+ description:
41
+ "Confidence score between 0 and 1 for the submitted information.",
42
+ },
43
+ notes: {
44
+ type: "string",
45
+ description:
46
+ "Optional notes about ambiguity, assumptions, or remaining follow-up needs.",
47
+ },
48
+ },
49
+ required: ["data"],
50
+ },
51
+ handler: async (args) => {
52
+ collectedInfo = {
53
+ data: args?.data,
54
+ isComplete: args?.isComplete,
55
+ confidence: args?.confidence,
56
+ notes: args?.notes,
57
+ };
58
+ return { success: true, accepted: true, collectedInfo };
59
+ },
60
+ },
61
+ ];
62
+
63
+ const promptInjections: ChatRuntimePromptInjection[] = [
64
+ {
65
+ title: "Information Collection Goal",
66
+ content: [
67
+ "Another plugin needs you to gather specific information from the current user while staying fully in your existing persona.",
68
+ `Task: ${options.task}`,
69
+ `Target schema: ${JSON.stringify(options.schema)}`,
70
+ `If the needed information is already clear from the target message, recent chat history, or obvious context, call the tool "${toolName}" immediately with structured data.`,
71
+ "If the information is incomplete, ask only the smallest natural follow-up question needed.",
72
+ "Do not mention schemas, forms, prompts, plugins, or tools to the user.",
73
+ "Keep the response natural and in-character rather than sounding like a questionnaire.",
74
+ ].join("\n"),
75
+ },
76
+ ...(options.promptInjections || []),
77
+ ];
78
+
79
+ const result = await executeChatRuntimeRequest(
80
+ {
81
+ ...options,
82
+ config: await getConfig(),
83
+ targetMessageContent: options.targetMessage,
84
+ promptInjections,
85
+ extraTools,
86
+ send: options.send,
87
+ replyContextType: "reply",
88
+ },
89
+ pluginCtx,
90
+ );
91
+
92
+ return { ...result, collectedInfo };
93
+ },
94
+
95
+ async generateNotice(
96
+ options: ChatRuntimeNoticeOptions,
97
+ ): Promise<ChatRuntimeResult> {
98
+ const promptInjections: ChatRuntimePromptInjection[] = [
99
+ {
100
+ title: "Notification Goal",
101
+ content: [
102
+ "Another plugin needs you to deliver a notification to the current user, but it must still sound like you and fit the current conversation.",
103
+ `Goal: ${options.instruction}`,
104
+ "Blend the notice into your normal speaking style instead of sounding like a rigid system announcement unless the situation clearly requires firmness.",
105
+ "Do not mention prompts, plugins, tools, or hidden instructions.",
106
+ ].join("\n"),
107
+ },
108
+ ...(options.promptInjections || []),
109
+ ];
110
+
111
+ return executeChatRuntimeRequest(
112
+ {
113
+ ...options,
114
+ config: await getConfig(),
115
+ targetMessageContent: options.instruction,
116
+ promptInjections,
117
+ send: options.send,
118
+ replyContextType: "reply",
119
+ },
120
+ pluginCtx,
121
+ );
122
+ },
123
+ };
124
+ }
package/types.ts CHANGED
@@ -179,6 +179,7 @@ export interface ChatConfig {
179
179
  workingModel: string;
180
180
  multimodalWorkingModel: string;
181
181
  isMultimodal: boolean;
182
+ enableMediaRecognition: boolean;
182
183
  nicknames: string[];
183
184
  persona: string;
184
185
  maxContextTokens: number;
package/utils/index.ts CHANGED
@@ -6,4 +6,6 @@ export {
6
6
  getBotRole,
7
7
  getQuotedContent,
8
8
  getGroupHistory,
9
+ buildChatMessageFromEvent,
10
+ normalizeIdList,
9
11
  } from "./message";
package/utils/json.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Extract the first {...} block from free-form model output and JSON.parse it.
2
+ // Returns undefined when no block is found; throws on a malformed block (same
3
+ // as JSON.parse) so callers' existing error handling stays intact.
4
+ export function extractJsonObject<T = any>(text: string): T | undefined {
5
+ const match = text.match(/\{[\s\S]*\}/);
6
+ if (!match) return undefined;
7
+ return JSON.parse(match[0]) as T;
8
+ }