mioku-plugin-chat 2.0.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.
@@ -0,0 +1,186 @@
1
+ import type { MiokiContext } from "mioki";
2
+ import type { AIInstance } from "mioku";
3
+ import type { ChatConfig, TargetMessage } from "../types";
4
+ import type { ChatDatabase } from "../db";
5
+ import type { HumanizeEngine } from "../humanize";
6
+ import type { SkillSessionManager } from "./skill-session";
7
+ import type { GroupStructuredHistoryManager } from "./group-structured-history";
8
+ import type { SessionManager } from "./session";
9
+ import type {
10
+ RunRateLimitGuard,
11
+ HistoryMediaOptions,
12
+ GetGroupHistoryMessages,
13
+ GetGroupInfoData,
14
+ SendAIResponse,
15
+ SaveBotMessages,
16
+ RunChat,
17
+ BuildToolContext,
18
+ } from "./types";
19
+ import { getBotRole, isGroupAllowed } from "../utils";
20
+
21
+ export class IdleCheckManager {
22
+ private groupLastActivityTime = new Map<string, number>();
23
+ private groupMessageCount = new Map<string, number>();
24
+ private groupLastBotMessageTime = new Map<string, number>();
25
+ private groupMessageCountAfterBot = new Map<string, number>();
26
+ private idleCheckProcessing = new Set<string>();
27
+ private groupLastIdleCheckTime = new Map<string, number>();
28
+ private groupBotsMapping = new Map<string, Set<number>>();
29
+ private intervalHandle: NodeJS.Timeout | null = null;
30
+
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
+ ) {}
51
+
52
+ recordActivity(groupSessionId: string): void {
53
+ this.groupLastActivityTime.set(groupSessionId, Date.now());
54
+ const count = this.groupMessageCount.get(groupSessionId) ?? 0;
55
+ this.groupMessageCount.set(groupSessionId, count + 1);
56
+ }
57
+
58
+ recordBotMessages(groupSessionId: string, count: number, selfId: number): void {
59
+ const current = this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
60
+ this.groupMessageCountAfterBot.set(groupSessionId, current + count);
61
+ let bots = this.groupBotsMapping.get(groupSessionId);
62
+ if (!bots) { bots = new Set<number>(); this.groupBotsMapping.set(groupSessionId, bots); }
63
+ bots.add(selfId);
64
+ this.groupLastBotMessageTime.set(groupSessionId, Date.now());
65
+ }
66
+
67
+ start(): void {
68
+ if (this.intervalHandle) return;
69
+ this.intervalHandle = setInterval(async () => { await this.tick(); }, 60_000);
70
+ }
71
+
72
+ stop(): void {
73
+ if (this.intervalHandle) { clearInterval(this.intervalHandle); this.intervalHandle = null; }
74
+ }
75
+
76
+ dispose(): void {
77
+ this.stop();
78
+ this.groupLastActivityTime.clear();
79
+ this.groupMessageCount.clear();
80
+ this.groupLastBotMessageTime.clear();
81
+ this.groupMessageCountAfterBot.clear();
82
+ this.idleCheckProcessing.clear();
83
+ this.groupLastIdleCheckTime.clear();
84
+ this.groupBotsMapping.clear();
85
+ }
86
+
87
+ private async tick(): Promise<void> {
88
+ try {
89
+ const cfg = this.cfg;
90
+ if (!cfg.apiKey || !cfg.planner?.enabled) return;
91
+
92
+ const now = Date.now();
93
+ const idleThreshold = cfg.planner.idleThresholdMs ?? 30 * 60_000;
94
+ const messageCountThreshold = cfg.planner.idleMessageCount ?? 100;
95
+ const checkInterval = 60_000;
96
+ const allBotIds = Array.from(this.ctx.bots).map((bot) => bot.uin);
97
+ const idleCheckBotIds = cfg.planner.idleCheckBotIds ?? allBotIds;
98
+ const enabledBotIds = idleCheckBotIds.filter((id) => allBotIds.includes(id));
99
+
100
+ for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
101
+ const lastCheckTime = this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
102
+ if (now - lastCheckTime < checkInterval) continue;
103
+
104
+ const groupId = parseInt(groupSessionId.split(":")[1], 10);
105
+ if (!isGroupAllowed(groupId, cfg)) continue;
106
+
107
+ let lastBotTime = this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
108
+ if (lastBotTime === 0) {
109
+ const botMsgs = this.db.getBotMessages(groupId, 1);
110
+ if (botMsgs.length > 0) {
111
+ lastBotTime = botMsgs[botMsgs.length - 1].timestamp;
112
+ this.groupLastBotMessageTime.set(groupSessionId, lastBotTime);
113
+ }
114
+ }
115
+
116
+ const lastActivityTime = Math.max(lastTime, lastBotTime);
117
+ if (now - lastActivityTime < idleThreshold) continue;
118
+
119
+ const messageCountAfterBot = this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
120
+ const messageCount = lastBotTime > 0 ? messageCountAfterBot : (this.groupMessageCount.get(groupSessionId) ?? 0);
121
+ if (messageCount < messageCountThreshold) continue;
122
+
123
+ const botsInGroup = this.groupBotsMapping.get(groupSessionId);
124
+ if (!botsInGroup || botsInGroup.size === 0) continue;
125
+ const availableBots = Array.from(botsInGroup).filter((id) => enabledBotIds.includes(id));
126
+ if (availableBots.length === 0) continue;
127
+
128
+ const selfId = availableBots[Math.floor(Math.random() * availableBots.length)];
129
+ this.idleCheckProcessing.add(groupSessionId);
130
+
131
+ try {
132
+ this.ctx.logger.info(`[IdleCheck] group ${groupId} triggers idle detection`);
133
+
134
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
135
+ const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
136
+
137
+ const planResult = await this.humanize.actionPlanner.plan(groupSessionId, botNickname, history, "[Check if you want to answer the call]", true);
138
+
139
+ if (planResult.action !== "reply") {
140
+ this.groupMessageCount.set(groupSessionId, 0);
141
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
142
+ this.groupLastIdleCheckTime.set(groupSessionId, now);
143
+ return;
144
+ }
145
+
146
+ const targetMessage: TargetMessage = {
147
+ userName: "system", userId: 0, userRole: "member",
148
+ content: "[No one in the group is talking? I'll answer!]", messageId: 0, timestamp: now,
149
+ };
150
+
151
+ const botRole = await getBotRole(groupId, this.ctx, selfId);
152
+ const toolCtx = this.buildToolContext({
153
+ ctx: this.ctx, event: null, groupSessionId, groupId, userId: 0,
154
+ config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
155
+ });
156
+
157
+ const result = await this.runWithRateLimitGuard(
158
+ () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
159
+ config: cfg, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
160
+ plannerThoughts: "You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [[[reply:message ID]]] format).\nDon't mention your intentions like \"I'm here to answer\".",
161
+ replyContext: { type: "idle" },
162
+ }, this.humanize, this.skillManager),
163
+ { groupId, label: "idle-check" },
164
+ );
165
+ if (!result) {
166
+ this.groupMessageCount.set(groupSessionId, 0);
167
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
168
+ this.groupLastIdleCheckTime.set(groupSessionId, now);
169
+ return;
170
+ }
171
+
172
+ await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
173
+ this.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, this.db, this.ctx, selfId);
174
+ this.groupLastBotMessageTime.set(groupSessionId, now);
175
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
176
+ this.startCooldownTimer(groupSessionId, groupId, selfId);
177
+ this.ctx.logger.info(`[IdleCheck] group ${groupId} idle reply completed`);
178
+ } catch (err) {
179
+ this.ctx.logger.error(`[IdleCheck] group ${groupId} idle detection failed: ${err}`);
180
+ } finally {
181
+ this.idleCheckProcessing.delete(groupSessionId);
182
+ }
183
+ }
184
+ } catch (err) {}
185
+ }
186
+ }
@@ -0,0 +1,228 @@
1
+ import type { MiokiContext } from "mioki";
2
+ import type { AIInstance } from "mioku";
3
+ import type { ChatConfig, TargetMessage } from "../types";
4
+ import type { ChatDatabase } from "../db";
5
+ import type { HumanizeEngine } from "../humanize";
6
+ import type { SkillSessionManager } from "./skill-session";
7
+ import type { GroupStructuredHistoryManager, StructuredUserInput } from "./group-structured-history";
8
+ import type { SessionManager } from "./session";
9
+ import type { RateLimiter } from "./rate-limiter";
10
+ import type { MessageQueueManager } from "../utils/queue";
11
+ import type {
12
+ RunRateLimitGuard,
13
+ HistoryMediaOptions,
14
+ GetGroupHistoryMessages,
15
+ GetGroupInfoData,
16
+ GetHumanizeContexts,
17
+ SendAIResponse,
18
+ SaveBotMessages,
19
+ SendEmoji,
20
+ RunChat,
21
+ BuildToolContext,
22
+ BuildStructuredUserInput,
23
+ } from "./types";
24
+ import { extractContent, getBotRole } from "../utils";
25
+
26
+ interface DynamicDelayQueueData {
27
+ messages: Array<{ event: any; content: string; userName: string; userId: number; messageId: number; timestamp: number }>;
28
+ timer: NodeJS.Timeout | null;
29
+ delayUntil: number;
30
+ }
31
+
32
+ export class QueueProcessor {
33
+ private dynamicDelayQueues = new Map<string, DynamicDelayQueueData>();
34
+
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
+ ) {}
60
+
61
+ collectDynamicDelayMessage(groupSessionId: string, event: any, content: string): void {
62
+ let queueData = this.dynamicDelayQueues.get(groupSessionId);
63
+ if (!queueData) {
64
+ queueData = { messages: [], timer: null, delayUntil: 0 };
65
+ this.dynamicDelayQueues.set(groupSessionId, queueData);
66
+ }
67
+ const userName = event.sender?.card || event.sender?.nickname || String(event.user_id);
68
+ queueData.messages.push({ event, content, userName, userId: event.user_id, messageId: event.message_id, timestamp: Date.now() });
69
+ }
70
+
71
+ startDynamicDelayTimer(groupSessionId: string, groupId: number, delayMs: number, selfId: number): void {
72
+ let queueData = this.dynamicDelayQueues.get(groupSessionId);
73
+ if (!queueData) {
74
+ queueData = { messages: [], timer: null, delayUntil: Date.now() + delayMs };
75
+ this.dynamicDelayQueues.set(groupSessionId, queueData);
76
+ }
77
+ if (queueData.timer) clearTimeout(queueData.timer);
78
+ queueData.delayUntil = Date.now() + delayMs;
79
+
80
+ this.ctx.logger.info(`[DynamicDelay] group ${groupId} start delay ${delayMs / 1000}s, interactions: ${this.rateLimiter.getInteractionCount(groupId)}`);
81
+
82
+ queueData.timer = setTimeout(async () => {
83
+ await this.processDynamicDelayQueue(groupSessionId, groupId, selfId);
84
+ }, delayMs);
85
+ }
86
+
87
+ isInDynamicDelay(groupSessionId: string): boolean {
88
+ const queue = this.dynamicDelayQueues.get(groupSessionId);
89
+ return queue != null && Date.now() < queue.delayUntil;
90
+ }
91
+
92
+ dispose(): void {
93
+ for (const queue of this.dynamicDelayQueues.values()) {
94
+ if (queue.timer) clearTimeout(queue.timer);
95
+ }
96
+ this.dynamicDelayQueues.clear();
97
+ }
98
+
99
+ async processDynamicDelayQueue(groupSessionId: string, groupId: number, selfId: number): Promise<void> {
100
+ const queueData = this.dynamicDelayQueues.get(groupSessionId);
101
+ if (!queueData || queueData.messages.length === 0) {
102
+ this.dynamicDelayQueues.delete(groupSessionId);
103
+ return;
104
+ }
105
+
106
+ const messages = queueData.messages;
107
+ this.dynamicDelayQueues.delete(groupSessionId);
108
+
109
+ this.ctx.logger.info(`[DynamicDelay] group ${groupId} processes ${messages.length} delayed messages`);
110
+ this.rateLimiter.clearGroupInteractions(groupId);
111
+
112
+ const mergedContents = messages.map((m) => m.content);
113
+ const userNames = messages.map((m) => m.userName);
114
+ const messageIds = messages.map((m) => m.messageId);
115
+ const structuredUserInputs = messages.map((m) => this.buildStructuredUserInputFromEvent(m.event, m.content, m.timestamp));
116
+ const mergedContent = mergedContents.join("\n---\n");
117
+ const firstMsg = messages[0];
118
+
119
+ const targetMessage: TargetMessage = {
120
+ userName: userNames.join(", "), userId: firstMsg.userId, userRole: "member",
121
+ content: mergedContent, messageId: firstMsg.messageId, timestamp: Date.now(),
122
+ };
123
+
124
+ const botRole = await getBotRole(groupId, this.ctx, selfId);
125
+ const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
126
+ const { groupName, memberCount } = await this.getGroupInfoData(this.ctx, groupId, selfId, String(groupId));
127
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, this.cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, this.cfg));
128
+
129
+ const toolCtx = this.buildToolContext({
130
+ ctx: this.ctx, event: firstMsg.event, groupSessionId, groupId, userId: firstMsg.userId,
131
+ config: this.cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
132
+ });
133
+
134
+ this.sessionManager.getOrCreate(groupSessionId, "group", groupId);
135
+ const contexts = await this.getHumanizeContexts(this.humanize, groupSessionId, targetMessage.userName, history, targetMessage.userId);
136
+
137
+ const result = await this.runWithRateLimitGuard(
138
+ () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
139
+ config: this.cfg, groupName, memberCount, botNickname, botRole, aiService: this.aiService, isGroup: true,
140
+ memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
141
+ replyContext: { type: "review", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
142
+ reviewMessages: { contents: mergedContents, userNames, messageIds },
143
+ }, this.humanize, this.skillManager, {
144
+ manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
145
+ }),
146
+ { userId: targetMessage.userId, groupId, label: "dynamic-delay" },
147
+ );
148
+ if (!result) return;
149
+
150
+ await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
151
+ this.startCooldownTimer(groupSessionId, groupId, selfId);
152
+ }
153
+
154
+ async processQueuedMessages(groupSessionId: string, selfId: number): Promise<void> {
155
+ try {
156
+ const queue = this.queueManager.getQueue(groupSessionId);
157
+ if (!queue || queue.length === 0) {
158
+ this.queueManager.clearActiveTarget(groupSessionId);
159
+ return;
160
+ }
161
+
162
+ const groupId = parseInt(groupSessionId.split(":")[1], 10);
163
+ this.ctx.logger.info(`[Queue] group ${groupSessionId} batch ${queue.length} messages`);
164
+
165
+ const queuedContents: string[] = [];
166
+ const structuredUserInputs: StructuredUserInput[] = [];
167
+
168
+ for (const item of queue) {
169
+ const { text: extractedText, multimodal } = extractContent(item.event, this.cfg, this.ctx);
170
+ const content = multimodal ? JSON.stringify(multimodal) : extractedText;
171
+ if (content) {
172
+ queuedContents.push(content);
173
+ structuredUserInputs.push(this.buildStructuredUserInputFromEvent(item.event, content, item.queuedAt));
174
+ }
175
+ }
176
+
177
+ this.queueManager.clearQueue(groupSessionId);
178
+ if (queuedContents.length === 0) {
179
+ this.queueManager.clearActiveTarget(groupSessionId);
180
+ return;
181
+ }
182
+
183
+ this.queueManager.clearActiveTarget(groupSessionId);
184
+
185
+ const firstItem = queue[0];
186
+ const userName = firstItem.event.sender?.card || firstItem.event.sender?.nickname || String(firstItem.event.user_id);
187
+ const mergedContent = queuedContents.join("\n");
188
+
189
+ const targetMessage: TargetMessage = {
190
+ userName, userId: firstItem.event.user_id || firstItem.event.sender?.user_id,
191
+ userRole: firstItem.event.sender?.role || "member", content: mergedContent,
192
+ messageId: firstItem.event.message_id, timestamp: Date.now(),
193
+ };
194
+
195
+ const botRole = await getBotRole(groupId, this.ctx, selfId);
196
+ const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
197
+ const toolCtx = this.buildToolContext({
198
+ ctx: this.ctx, event: null, groupSessionId, groupId, userId: targetMessage.userId,
199
+ config: this.cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
200
+ });
201
+
202
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, this.cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, this.cfg));
203
+ const contexts = await this.getHumanizeContexts(this.humanize, groupSessionId, targetMessage.userName, history, targetMessage.userId);
204
+ const { groupName, memberCount } = await this.getGroupInfoData(this.ctx, groupId, selfId);
205
+
206
+ const result = await this.runWithRateLimitGuard(
207
+ () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
208
+ config: this.cfg, groupName, memberCount, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
209
+ memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
210
+ replyContext: { type: "comment", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
211
+ }, this.humanize, this.skillManager, {
212
+ manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
213
+ }),
214
+ { userId: targetMessage.userId, groupId, label: "queue" },
215
+ );
216
+ if (!result) return;
217
+
218
+ await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
219
+ await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
220
+ const now = Date.now();
221
+ this.saveBotMessages(groupId, groupSessionId, result.messages, now, this.cfg, this.db, this.ctx, selfId);
222
+ this.sessionManager.touch(groupSessionId);
223
+ this.ctx.logger.info(`[Queue] group ${groupSessionId} done`);
224
+ } catch (err) {
225
+ this.ctx.logger.error(err);
226
+ }
227
+ }
228
+ }