mioku-plugin-chat 2.6.0 → 2.7.1

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.
@@ -11,20 +11,19 @@ import type {
11
11
  HistoryMediaOptions,
12
12
  GetGroupHistoryMessages,
13
13
  GetGroupInfoData,
14
- SendAIResponse,
15
- SaveBotMessages,
16
14
  RunChat,
17
15
  BuildToolContext,
18
16
  } from "./types";
19
17
  import type { ChatPluginContext } from "../context";
18
+ import { finalizeChatTurn } from "../core/chat-turn";
20
19
  import { getBotRole, isGroupAllowed } from "../utils";
21
20
 
22
21
  export class IdleCheckManager {
22
+ private pluginCtx: ChatPluginContext;
23
23
  private groupLastActivityTime = new Map<string, number>();
24
24
  private groupMessageCount = new Map<string, number>();
25
25
  private groupLastBotMessageTime = new Map<string, number>();
26
26
  private groupMessageCountAfterBot = new Map<string, number>();
27
- private idleCheckProcessing = new Set<string>();
28
27
  private groupLastIdleCheckTime = new Map<string, number>();
29
28
  private groupBotsMapping = new Map<string, Set<number>>();
30
29
  private intervalHandle: NodeJS.Timeout | null = null;
@@ -43,13 +42,11 @@ export class IdleCheckManager {
43
42
  private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions;
44
43
  private getGroupHistoryMessages: GetGroupHistoryMessages;
45
44
  private getGroupInfoData: GetGroupInfoData;
46
- private sendAIResponse: SendAIResponse;
47
- private saveBotMessages: SaveBotMessages;
48
45
  private runChat: RunChat;
49
46
  private buildToolContext: BuildToolContext;
50
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
51
47
 
52
48
  constructor(pluginCtx: ChatPluginContext) {
49
+ this.pluginCtx = pluginCtx;
53
50
  this.ctx = pluginCtx.ctx;
54
51
  this.configProvider = pluginCtx.configProvider;
55
52
  this.defaultConfig = pluginCtx.defaultConfig;
@@ -64,11 +61,8 @@ export class IdleCheckManager {
64
61
  this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
65
62
  this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
66
63
  this.getGroupInfoData = pluginCtx.getGroupInfoData;
67
- this.sendAIResponse = pluginCtx.sendAIResponse;
68
- this.saveBotMessages = pluginCtx.saveBotMessages;
69
64
  this.runChat = pluginCtx.runChat;
70
65
  this.buildToolContext = pluginCtx.buildToolContext;
71
- this.startCooldownTimer = pluginCtx.startCooldownTimer;
72
66
  }
73
67
 
74
68
  recordActivity(groupSessionId: string): void {
@@ -101,7 +95,6 @@ export class IdleCheckManager {
101
95
  this.groupMessageCount.clear();
102
96
  this.groupLastBotMessageTime.clear();
103
97
  this.groupMessageCountAfterBot.clear();
104
- this.idleCheckProcessing.clear();
105
98
  this.groupLastIdleCheckTime.clear();
106
99
  this.groupBotsMapping.clear();
107
100
  }
@@ -116,7 +109,8 @@ export class IdleCheckManager {
116
109
  const allBotIds = Array.from(this.ctx.bots).map((bot) => bot.uin);
117
110
 
118
111
  for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
119
- const lastCheckTime = this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
112
+ const lastCheckTime =
113
+ this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
120
114
  if (now - lastCheckTime < checkInterval) continue;
121
115
 
122
116
  const groupId = parseInt(groupSessionId.split(":")[1], 10);
@@ -124,12 +118,24 @@ export class IdleCheckManager {
124
118
  if (!cfg.planner?.enabled) continue;
125
119
  if (!isGroupAllowed(groupId, cfg)) continue;
126
120
 
127
- const idleThreshold = cfg.planner.idleThresholdMs ?? baseCfg.planner?.idleThresholdMs ?? 30 * 60_000;
128
- const messageCountThreshold = cfg.planner.idleMessageCount ?? baseCfg.planner?.idleMessageCount ?? 100;
129
- const idleCheckBotIds = cfg.planner.idleCheckBotIds ?? baseCfg.planner?.idleCheckBotIds ?? allBotIds;
130
- const enabledBotIds = idleCheckBotIds.filter((id: number) => allBotIds.includes(id));
121
+ const idleThreshold =
122
+ cfg.planner.idleThresholdMs ??
123
+ baseCfg.planner?.idleThresholdMs ??
124
+ 30 * 60_000;
125
+ const messageCountThreshold =
126
+ cfg.planner.idleMessageCount ??
127
+ baseCfg.planner?.idleMessageCount ??
128
+ 100;
129
+ const idleCheckBotIds =
130
+ cfg.planner.idleCheckBotIds ??
131
+ baseCfg.planner?.idleCheckBotIds ??
132
+ allBotIds;
133
+ const enabledBotIds = idleCheckBotIds.filter((id: number) =>
134
+ allBotIds.includes(id),
135
+ );
131
136
 
132
- let lastBotTime = this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
137
+ let lastBotTime =
138
+ this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
133
139
  if (lastBotTime === 0) {
134
140
  const botMsgs = this.db.getBotMessages(groupId, 1);
135
141
  if (botMsgs.length > 0) {
@@ -141,71 +147,158 @@ export class IdleCheckManager {
141
147
  const lastActivityTime = Math.max(lastTime, lastBotTime);
142
148
  if (now - lastActivityTime < idleThreshold) continue;
143
149
 
144
- const messageCountAfterBot = this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
145
- const messageCount = lastBotTime > 0 ? messageCountAfterBot : (this.groupMessageCount.get(groupSessionId) ?? 0);
150
+ const messageCountAfterBot =
151
+ this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
152
+ const messageCount =
153
+ lastBotTime > 0
154
+ ? messageCountAfterBot
155
+ : (this.groupMessageCount.get(groupSessionId) ?? 0);
146
156
  if (messageCount < messageCountThreshold) continue;
147
157
 
148
158
  const botsInGroup = this.groupBotsMapping.get(groupSessionId);
149
159
  if (!botsInGroup || botsInGroup.size === 0) continue;
150
- const availableBots = Array.from(botsInGroup).filter((id) => enabledBotIds.includes(id));
160
+ const availableBots = Array.from(botsInGroup).filter((id) =>
161
+ enabledBotIds.includes(id),
162
+ );
151
163
  if (availableBots.length === 0) continue;
152
164
 
153
- const selfId = availableBots[Math.floor(Math.random() * availableBots.length)];
154
- this.idleCheckProcessing.add(groupSessionId);
165
+ const selfId =
166
+ availableBots[Math.floor(Math.random() * availableBots.length)];
167
+ this.groupLastIdleCheckTime.set(groupSessionId, now);
168
+ void this.pluginCtx.sessionTurnScheduler
169
+ .run(
170
+ groupSessionId,
171
+ "idle-check",
172
+ () =>
173
+ this.processIdleCheckTurn(
174
+ groupSessionId,
175
+ groupId,
176
+ selfId,
177
+ cfg,
178
+ lastActivityTime,
179
+ now,
180
+ ),
181
+ { dedupeKey: "idle-check" },
182
+ )
183
+ .catch((err) =>
184
+ this.ctx.logger.error(
185
+ `[IdleCheck] group ${groupId} idle detection failed: ${err}`,
186
+ ),
187
+ );
188
+ }
189
+ } catch (err) {
190
+ this.ctx.logger.error(`[IdleCheck] tick failed: ${err}`);
191
+ }
192
+ }
155
193
 
156
- try {
157
- this.ctx.logger.info(`[IdleCheck] group ${groupId} triggers idle detection`);
194
+ private async processIdleCheckTurn(
195
+ groupSessionId: string,
196
+ groupId: number,
197
+ selfId: number,
198
+ cfg: ChatConfig,
199
+ scheduledLastActivity: number,
200
+ scheduledAt: number,
201
+ ): Promise<void> {
202
+ const latestActivity = Math.max(
203
+ this.groupLastActivityTime.get(groupSessionId) ?? 0,
204
+ this.groupLastBotMessageTime.get(groupSessionId) ?? 0,
205
+ );
206
+ if (latestActivity > scheduledLastActivity) return;
158
207
 
159
- const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
160
- const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
208
+ this.ctx.logger.info(`[IdleCheck] group ${groupId} triggers idle detection`);
209
+ const { history } = await this.getGroupHistoryMessages(
210
+ groupId,
211
+ groupSessionId,
212
+ this.ctx,
213
+ cfg.historyCount,
214
+ this.db,
215
+ selfId,
216
+ this.buildHistoryMediaOptions(this.aiInstance, cfg),
217
+ );
218
+ const botNickname =
219
+ cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
220
+ const planResult = await this.humanize.actionPlanner.plan(
221
+ groupSessionId,
222
+ botNickname,
223
+ history,
224
+ "[Check if you want to answer the call]",
225
+ true,
226
+ );
161
227
 
162
- const planResult = await this.humanize.actionPlanner.plan(groupSessionId, botNickname, history, "[Check if you want to answer the call]", true);
228
+ if (planResult.action !== "reply") {
229
+ this.groupMessageCount.set(groupSessionId, 0);
230
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
231
+ this.groupLastIdleCheckTime.set(groupSessionId, scheduledAt);
232
+ return;
233
+ }
163
234
 
164
- if (planResult.action !== "reply") {
165
- this.groupMessageCount.set(groupSessionId, 0);
166
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
167
- this.groupLastIdleCheckTime.set(groupSessionId, now);
168
- return;
169
- }
235
+ const targetMessage: TargetMessage = {
236
+ userName: "system",
237
+ userId: 0,
238
+ userRole: "member",
239
+ content: "[No one in the group is talking? I'll answer!]",
240
+ messageId: 0,
241
+ timestamp: scheduledAt,
242
+ };
243
+ const botRole = await getBotRole(groupId, this.ctx, selfId);
244
+ const toolCtx = this.buildToolContext({
245
+ ctx: this.ctx,
246
+ event: null,
247
+ groupSessionId,
248
+ groupId,
249
+ userId: 0,
250
+ config: cfg,
251
+ aiService: this.aiService,
252
+ db: this.db,
253
+ botRole,
254
+ humanize: this.humanize,
255
+ targetMessage,
256
+ selfId,
257
+ });
170
258
 
171
- const targetMessage: TargetMessage = {
172
- userName: "system", userId: 0, userRole: "member",
173
- content: "[No one in the group is talking? I'll answer!]", messageId: 0, timestamp: now,
174
- };
175
-
176
- const botRole = await getBotRole(groupId, this.ctx, selfId);
177
- const toolCtx = this.buildToolContext({
178
- ctx: this.ctx, event: null, groupSessionId, groupId, userId: 0,
179
- config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
180
- });
181
-
182
- const result = await this.runWithRateLimitGuard(
183
- () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
184
- config: cfg, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
185
- 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\".",
186
- replyContext: { type: "idle" },
187
- }, this.humanize, this.skillManager),
188
- { groupId, label: "idle-check" },
189
- );
190
- if (!result) {
191
- this.groupMessageCount.set(groupSessionId, 0);
192
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
193
- this.groupLastIdleCheckTime.set(groupSessionId, now);
194
- return;
195
- }
259
+ const result = await this.runWithRateLimitGuard(
260
+ () =>
261
+ this.runChat(
262
+ this.aiInstance,
263
+ toolCtx,
264
+ history,
265
+ targetMessage,
266
+ {
267
+ config: cfg,
268
+ botNickname,
269
+ botRole: toolCtx.botRole,
270
+ aiService: this.aiService,
271
+ isGroup: true,
272
+ plannerThoughts:
273
+ '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".',
274
+ replyContext: { type: "idle" },
275
+ },
276
+ this.humanize,
277
+ this.skillManager,
278
+ ),
279
+ { groupId, label: "idle-check" },
280
+ );
281
+ if (!result) {
282
+ this.groupMessageCount.set(groupSessionId, 0);
283
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
284
+ this.groupLastIdleCheckTime.set(groupSessionId, scheduledAt);
285
+ return;
286
+ }
196
287
 
197
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
198
- this.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, this.db, this.ctx, selfId);
199
- this.groupLastBotMessageTime.set(groupSessionId, now);
200
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
201
- this.startCooldownTimer(groupSessionId, groupId, selfId);
202
- this.ctx.logger.info(`[IdleCheck] group ${groupId} idle reply completed`);
203
- } catch (err) {
204
- this.ctx.logger.error(`[IdleCheck] group ${groupId} idle detection failed: ${err}`);
205
- } finally {
206
- this.idleCheckProcessing.delete(groupSessionId);
207
- }
208
- }
209
- } catch (err) {}
288
+ await finalizeChatTurn(this.pluginCtx, {
289
+ event: null,
290
+ cfg,
291
+ result,
292
+ groupId,
293
+ groupSessionId,
294
+ userId: 0,
295
+ selfId,
296
+ toolCtx,
297
+ send: true,
298
+ isLive: true,
299
+ });
300
+ this.groupLastBotMessageTime.set(groupSessionId, Date.now());
301
+ this.groupMessageCountAfterBot.set(groupSessionId, 0);
302
+ this.ctx.logger.info(`[IdleCheck] group ${groupId} idle reply completed`);
210
303
  }
211
304
  }
@@ -15,14 +15,13 @@ import type {
15
15
  GetGroupHistoryMessages,
16
16
  GetGroupInfoData,
17
17
  GetHumanizeContexts,
18
- SendAIResponse,
19
- SaveBotMessages,
20
- SendEmoji,
21
18
  RunChat,
22
19
  BuildToolContext,
23
20
  BuildStructuredUserInput,
24
21
  } from "./types";
25
22
  import type { ChatPluginContext } from "../context";
23
+ import type { SessionTurnScheduler } from "./session-turn-scheduler";
24
+ import { finalizeChatTurn } from "../core/chat-turn";
26
25
  import { extractContent, getBotRole } from "../utils";
27
26
 
28
27
  interface DynamicDelayQueueData {
@@ -33,6 +32,7 @@ interface DynamicDelayQueueData {
33
32
 
34
33
  export class QueueProcessor {
35
34
  private dynamicDelayQueues = new Map<string, DynamicDelayQueueData>();
35
+ private pluginCtx: ChatPluginContext;
36
36
 
37
37
  private ctx: MiokiContext;
38
38
  private configProvider: ChatConfigProvider;
@@ -51,15 +51,13 @@ export class QueueProcessor {
51
51
  private getGroupHistoryMessages: GetGroupHistoryMessages;
52
52
  private getGroupInfoData: GetGroupInfoData;
53
53
  private getHumanizeContexts: GetHumanizeContexts;
54
- private sendAIResponse: SendAIResponse;
55
- private saveBotMessages: SaveBotMessages;
56
- private sendEmoji: SendEmoji;
57
54
  private runChat: RunChat;
58
55
  private buildToolContext: BuildToolContext;
59
56
  private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
60
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
57
+ private sessionTurnScheduler: SessionTurnScheduler;
61
58
 
62
59
  constructor(pluginCtx: ChatPluginContext) {
60
+ this.pluginCtx = pluginCtx;
63
61
  this.ctx = pluginCtx.ctx;
64
62
  this.configProvider = pluginCtx.configProvider;
65
63
  this.defaultConfig = pluginCtx.defaultConfig;
@@ -77,13 +75,10 @@ export class QueueProcessor {
77
75
  this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
78
76
  this.getGroupInfoData = pluginCtx.getGroupInfoData;
79
77
  this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
80
- this.sendAIResponse = pluginCtx.sendAIResponse;
81
- this.saveBotMessages = pluginCtx.saveBotMessages;
82
- this.sendEmoji = pluginCtx.sendEmoji;
83
78
  this.runChat = pluginCtx.runChat;
84
79
  this.buildToolContext = pluginCtx.buildToolContext;
85
80
  this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
86
- this.startCooldownTimer = pluginCtx.startCooldownTimer;
81
+ this.sessionTurnScheduler = pluginCtx.sessionTurnScheduler;
87
82
  }
88
83
 
89
84
  collectDynamicDelayMessage(groupSessionId: string, event: any, content: string): void {
@@ -107,9 +102,29 @@ export class QueueProcessor {
107
102
 
108
103
  this.ctx.logger.info(`[DynamicDelay] group ${groupId} start delay ${delayMs / 1000}s, interactions: ${this.rateLimiter.getInteractionCount(groupId)}`);
109
104
 
110
- queueData.timer = setTimeout(async () => {
111
- await this.processDynamicDelayQueue(groupSessionId, groupId, selfId);
105
+ const timer = setTimeout(() => {
106
+ const current = this.dynamicDelayQueues.get(groupSessionId);
107
+ if (!current || current !== queueData || current.timer !== timer) return;
108
+ this.dynamicDelayQueues.delete(groupSessionId);
109
+ current.timer = null;
110
+ const messages = current.messages;
111
+ if (messages.length === 0) return;
112
+ void this.sessionTurnScheduler
113
+ .run(groupSessionId, "dynamic-delay", () =>
114
+ this.processDynamicDelayMessages(
115
+ groupSessionId,
116
+ groupId,
117
+ selfId,
118
+ messages,
119
+ ),
120
+ )
121
+ .catch((err) =>
122
+ this.ctx.logger.error(
123
+ `[DynamicDelay] group ${groupId} processing failed: ${err}`,
124
+ ),
125
+ );
112
126
  }, delayMs);
127
+ queueData.timer = timer;
113
128
  }
114
129
 
115
130
  isInDynamicDelay(groupSessionId: string): boolean {
@@ -117,6 +132,21 @@ export class QueueProcessor {
117
132
  return queue != null && Date.now() < queue.delayUntil;
118
133
  }
119
134
 
135
+ scheduleQueuedMessages(groupSessionId: string, selfId: number): void {
136
+ void this.sessionTurnScheduler
137
+ .run(
138
+ groupSessionId,
139
+ "queued-message",
140
+ () => this.processQueuedMessages(groupSessionId, selfId),
141
+ { dedupeKey: `queued-message:${selfId}` },
142
+ )
143
+ .catch((err) =>
144
+ this.ctx.logger.error(
145
+ `[Queue] group ${groupSessionId} scheduling failed: ${err}`,
146
+ ),
147
+ );
148
+ }
149
+
120
150
  dispose(): void {
121
151
  for (const queue of this.dynamicDelayQueues.values()) {
122
152
  if (queue.timer) clearTimeout(queue.timer);
@@ -124,16 +154,12 @@ export class QueueProcessor {
124
154
  this.dynamicDelayQueues.clear();
125
155
  }
126
156
 
127
- async processDynamicDelayQueue(groupSessionId: string, groupId: number, selfId: number): Promise<void> {
128
- const queueData = this.dynamicDelayQueues.get(groupSessionId);
129
- if (!queueData || queueData.messages.length === 0) {
130
- this.dynamicDelayQueues.delete(groupSessionId);
131
- return;
132
- }
133
-
134
- const messages = queueData.messages;
135
- this.dynamicDelayQueues.delete(groupSessionId);
136
-
157
+ private async processDynamicDelayMessages(
158
+ groupSessionId: string,
159
+ groupId: number,
160
+ selfId: number,
161
+ messages: DynamicDelayQueueData["messages"],
162
+ ): Promise<void> {
137
163
  this.ctx.logger.info(`[DynamicDelay] group ${groupId} processes ${messages.length} delayed messages`);
138
164
  this.rateLimiter.clearGroupInteractions(groupId);
139
165
 
@@ -176,8 +202,18 @@ export class QueueProcessor {
176
202
  );
177
203
  if (!result) return;
178
204
 
179
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
180
- this.startCooldownTimer(groupSessionId, groupId, selfId);
205
+ await finalizeChatTurn(this.pluginCtx, {
206
+ event: firstMsg.event,
207
+ cfg,
208
+ result,
209
+ groupId,
210
+ groupSessionId,
211
+ userId: targetMessage.userId,
212
+ selfId,
213
+ toolCtx,
214
+ send: true,
215
+ isLive: true,
216
+ });
181
217
  }
182
218
 
183
219
  async processQueuedMessages(groupSessionId: string, selfId: number): Promise<void> {
@@ -246,11 +282,18 @@ export class QueueProcessor {
246
282
  );
247
283
  if (!result) return;
248
284
 
249
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
250
- await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
251
- const now = Date.now();
252
- this.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, this.db, this.ctx, selfId);
253
- this.sessionManager.touch(groupSessionId);
285
+ await finalizeChatTurn(this.pluginCtx, {
286
+ event: firstItem.event,
287
+ cfg,
288
+ result,
289
+ groupId,
290
+ groupSessionId,
291
+ userId: targetMessage.userId,
292
+ selfId,
293
+ toolCtx,
294
+ send: true,
295
+ isLive: true,
296
+ });
254
297
  this.ctx.logger.info(`[Queue] group ${groupSessionId} done`);
255
298
  } catch (err) {
256
299
  this.ctx.logger.error(err);
@@ -0,0 +1,130 @@
1
+ export type SessionTurnSource =
2
+ | "message"
3
+ | "queued-message"
4
+ | "cooldown"
5
+ | "cooldown-planner"
6
+ | "dynamic-delay"
7
+ | "idle-check"
8
+ | "idle-debug"
9
+ | "poke"
10
+ | "chat-runtime";
11
+
12
+ interface SessionTurnEntry<T> {
13
+ source: SessionTurnSource;
14
+ dedupeKey?: string;
15
+ run: () => Promise<T>;
16
+ promise: Promise<T>;
17
+ resolve: (value: T) => void;
18
+ reject: (reason: unknown) => void;
19
+ }
20
+
21
+ interface SessionTurnState {
22
+ running: boolean;
23
+ pending: SessionTurnEntry<unknown>[];
24
+ }
25
+
26
+ export class SessionTurnSchedulerDisposedError extends Error {
27
+ constructor() {
28
+ super("Session turn scheduler has been disposed");
29
+ this.name = "SessionTurnSchedulerDisposedError";
30
+ }
31
+ }
32
+
33
+ export class SessionTurnScheduler {
34
+ private readonly sessions = new Map<string, SessionTurnState>();
35
+ private disposed = false;
36
+
37
+ run<T>(
38
+ sessionId: string,
39
+ source: SessionTurnSource,
40
+ task: () => Promise<T>,
41
+ options: { dedupeKey?: string } = {},
42
+ ): Promise<T> {
43
+ if (this.disposed) {
44
+ return Promise.reject(new SessionTurnSchedulerDisposedError());
45
+ }
46
+
47
+ const state = this.getOrCreateState(sessionId);
48
+ if (options.dedupeKey) {
49
+ const existing = state.pending.find(
50
+ (entry) => entry.dedupeKey === options.dedupeKey,
51
+ );
52
+ if (existing) return existing.promise as Promise<T>;
53
+ }
54
+
55
+ let resolve!: (value: T) => void;
56
+ let reject!: (reason: unknown) => void;
57
+ const promise = new Promise<T>((resolvePromise, rejectPromise) => {
58
+ resolve = resolvePromise;
59
+ reject = rejectPromise;
60
+ });
61
+ const entry: SessionTurnEntry<T> = {
62
+ source,
63
+ dedupeKey: options.dedupeKey,
64
+ run: task,
65
+ promise,
66
+ resolve,
67
+ reject,
68
+ };
69
+
70
+ state.pending.push(entry as SessionTurnEntry<unknown>);
71
+ if (!state.running) void this.drain(sessionId, state);
72
+ return promise;
73
+ }
74
+
75
+ isBusy(sessionId: string): boolean {
76
+ const state = this.sessions.get(sessionId);
77
+ return Boolean(state?.running || state?.pending.length);
78
+ }
79
+
80
+ pendingCount(sessionId: string): number {
81
+ return this.sessions.get(sessionId)?.pending.length ?? 0;
82
+ }
83
+
84
+ dispose(): void {
85
+ if (this.disposed) return;
86
+ this.disposed = true;
87
+ const error = new SessionTurnSchedulerDisposedError();
88
+
89
+ for (const [sessionId, state] of this.sessions) {
90
+ for (const entry of state.pending.splice(0)) entry.reject(error);
91
+ if (!state.running) this.sessions.delete(sessionId);
92
+ }
93
+ }
94
+
95
+ private getOrCreateState(sessionId: string): SessionTurnState {
96
+ let state = this.sessions.get(sessionId);
97
+ if (!state) {
98
+ state = { running: false, pending: [] };
99
+ this.sessions.set(sessionId, state);
100
+ }
101
+ return state;
102
+ }
103
+
104
+ private async drain(
105
+ sessionId: string,
106
+ state: SessionTurnState,
107
+ ): Promise<void> {
108
+ if (state.running || this.disposed) return;
109
+ state.running = true;
110
+
111
+ try {
112
+ while (!this.disposed) {
113
+ const entry = state.pending.shift();
114
+ if (!entry) break;
115
+ try {
116
+ entry.resolve(await entry.run());
117
+ } catch (err) {
118
+ entry.reject(err);
119
+ }
120
+ }
121
+ } finally {
122
+ state.running = false;
123
+ if (state.pending.length === 0) {
124
+ this.sessions.delete(sessionId);
125
+ } else if (!this.disposed) {
126
+ void this.drain(sessionId, state);
127
+ }
128
+ }
129
+ }
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -10,6 +10,18 @@ import type { ChatPluginContext, ChatRuntime } from "../context";
10
10
  import type { ChatConfig } from "../types";
11
11
  import { executeChatRuntimeRequest } from "../core/chat-turn";
12
12
 
13
+ function resolveRuntimeGroupId(
14
+ options: ChatRuntimeNoticeOptions | ChatRuntimeInformationRequestOptions,
15
+ ): number | undefined {
16
+ if ("event" in options) {
17
+ const event = options.event;
18
+ return event?.message_type === "group" && typeof event.group_id === "number"
19
+ ? event.group_id
20
+ : undefined;
21
+ }
22
+ return "groupId" in options ? options.groupId : undefined;
23
+ }
24
+
13
25
  export function createChatRuntime(
14
26
  pluginCtx: ChatPluginContext,
15
27
  getConfig: (groupId?: number) => Promise<ChatConfig>,
@@ -79,7 +91,7 @@ export function createChatRuntime(
79
91
  const result = await executeChatRuntimeRequest(
80
92
  {
81
93
  ...options,
82
- config: await getConfig(),
94
+ config: await getConfig(resolveRuntimeGroupId(options)),
83
95
  targetMessageContent: options.targetMessage,
84
96
  promptInjections,
85
97
  extraTools,
@@ -111,7 +123,7 @@ export function createChatRuntime(
111
123
  return executeChatRuntimeRequest(
112
124
  {
113
125
  ...options,
114
- config: await getConfig(),
126
+ config: await getConfig(resolveRuntimeGroupId(options)),
115
127
  targetMessageContent: options.instruction,
116
128
  promptInjections,
117
129
  send: options.send,