mioku-plugin-chat 2.5.2 → 2.7.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.
@@ -2,7 +2,7 @@ import type { MiokiContext } from "mioki";
2
2
  import type { AIInstance } from "mioku";
3
3
  import type { ChatConfig, TargetMessage } from "../types";
4
4
  import type { ChatDatabase } from "../db";
5
- import type { HumanizeEngine } from "../humanize";
5
+ import type { HumanizeEngine, ChatConfigProvider } from "../humanize";
6
6
  import type { SkillSessionManager } from "./skill-session";
7
7
  import type { GroupStructuredHistoryManager } from "./group-structured-history";
8
8
  import type { SessionManager } from "./session";
@@ -11,26 +11,26 @@ 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;
31
30
 
32
31
  private ctx: MiokiContext;
33
- private cfg: ChatConfig;
32
+ private configProvider: ChatConfigProvider;
33
+ private defaultConfig: ChatConfig;
34
34
  private db: ChatDatabase;
35
35
  private humanize: HumanizeEngine;
36
36
  private aiInstance: AIInstance;
@@ -42,15 +42,14 @@ export class IdleCheckManager {
42
42
  private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions;
43
43
  private getGroupHistoryMessages: GetGroupHistoryMessages;
44
44
  private getGroupInfoData: GetGroupInfoData;
45
- private sendAIResponse: SendAIResponse;
46
- private saveBotMessages: SaveBotMessages;
47
45
  private runChat: RunChat;
48
46
  private buildToolContext: BuildToolContext;
49
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
50
47
 
51
48
  constructor(pluginCtx: ChatPluginContext) {
49
+ this.pluginCtx = pluginCtx;
52
50
  this.ctx = pluginCtx.ctx;
53
- this.cfg = pluginCtx.config;
51
+ this.configProvider = pluginCtx.configProvider;
52
+ this.defaultConfig = pluginCtx.defaultConfig;
54
53
  this.db = pluginCtx.db;
55
54
  this.humanize = pluginCtx.humanize;
56
55
  this.aiInstance = pluginCtx.aiInstance;
@@ -62,11 +61,8 @@ export class IdleCheckManager {
62
61
  this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
63
62
  this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
64
63
  this.getGroupInfoData = pluginCtx.getGroupInfoData;
65
- this.sendAIResponse = pluginCtx.sendAIResponse;
66
- this.saveBotMessages = pluginCtx.saveBotMessages;
67
64
  this.runChat = pluginCtx.runChat;
68
65
  this.buildToolContext = pluginCtx.buildToolContext;
69
- this.startCooldownTimer = pluginCtx.startCooldownTimer;
70
66
  }
71
67
 
72
68
  recordActivity(groupSessionId: string): void {
@@ -99,32 +95,47 @@ export class IdleCheckManager {
99
95
  this.groupMessageCount.clear();
100
96
  this.groupLastBotMessageTime.clear();
101
97
  this.groupMessageCountAfterBot.clear();
102
- this.idleCheckProcessing.clear();
103
98
  this.groupLastIdleCheckTime.clear();
104
99
  this.groupBotsMapping.clear();
105
100
  }
106
101
 
107
102
  private async tick(): Promise<void> {
108
103
  try {
109
- const cfg = this.cfg;
110
- if (!cfg.apiKey || !cfg.planner?.enabled) return;
104
+ const baseCfg = this.defaultConfig;
105
+ if (!baseCfg.apiKey) return;
111
106
 
112
107
  const now = Date.now();
113
- const idleThreshold = cfg.planner.idleThresholdMs ?? 30 * 60_000;
114
- const messageCountThreshold = cfg.planner.idleMessageCount ?? 100;
115
108
  const checkInterval = 60_000;
116
109
  const allBotIds = Array.from(this.ctx.bots).map((bot) => bot.uin);
117
- const idleCheckBotIds = cfg.planner.idleCheckBotIds ?? allBotIds;
118
- const enabledBotIds = idleCheckBotIds.filter((id) => allBotIds.includes(id));
119
110
 
120
111
  for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
121
- const lastCheckTime = this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
112
+ const lastCheckTime =
113
+ this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
122
114
  if (now - lastCheckTime < checkInterval) continue;
123
115
 
124
116
  const groupId = parseInt(groupSessionId.split(":")[1], 10);
117
+ const cfg = this.configProvider(groupId);
118
+ if (!cfg.planner?.enabled) continue;
125
119
  if (!isGroupAllowed(groupId, cfg)) continue;
126
120
 
127
- let lastBotTime = this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
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
+ );
136
+
137
+ let lastBotTime =
138
+ this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
128
139
  if (lastBotTime === 0) {
129
140
  const botMsgs = this.db.getBotMessages(groupId, 1);
130
141
  if (botMsgs.length > 0) {
@@ -136,71 +147,158 @@ export class IdleCheckManager {
136
147
  const lastActivityTime = Math.max(lastTime, lastBotTime);
137
148
  if (now - lastActivityTime < idleThreshold) continue;
138
149
 
139
- const messageCountAfterBot = this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
140
- 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);
141
156
  if (messageCount < messageCountThreshold) continue;
142
157
 
143
158
  const botsInGroup = this.groupBotsMapping.get(groupSessionId);
144
159
  if (!botsInGroup || botsInGroup.size === 0) continue;
145
- const availableBots = Array.from(botsInGroup).filter((id) => enabledBotIds.includes(id));
160
+ const availableBots = Array.from(botsInGroup).filter((id) =>
161
+ enabledBotIds.includes(id),
162
+ );
146
163
  if (availableBots.length === 0) continue;
147
164
 
148
- const selfId = availableBots[Math.floor(Math.random() * availableBots.length)];
149
- 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
+ }
150
193
 
151
- try {
152
- 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;
153
207
 
154
- const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
155
- 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
+ );
156
227
 
157
- 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
+ }
158
234
 
159
- if (planResult.action !== "reply") {
160
- this.groupMessageCount.set(groupSessionId, 0);
161
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
162
- this.groupLastIdleCheckTime.set(groupSessionId, now);
163
- return;
164
- }
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
+ });
165
258
 
166
- const targetMessage: TargetMessage = {
167
- userName: "system", userId: 0, userRole: "member",
168
- content: "[No one in the group is talking? I'll answer!]", messageId: 0, timestamp: now,
169
- };
170
-
171
- const botRole = await getBotRole(groupId, this.ctx, selfId);
172
- const toolCtx = this.buildToolContext({
173
- ctx: this.ctx, event: null, groupSessionId, groupId, userId: 0,
174
- config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
175
- });
176
-
177
- const result = await this.runWithRateLimitGuard(
178
- () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
179
- config: cfg, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
180
- 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\".",
181
- replyContext: { type: "idle" },
182
- }, this.humanize, this.skillManager),
183
- { groupId, label: "idle-check" },
184
- );
185
- if (!result) {
186
- this.groupMessageCount.set(groupSessionId, 0);
187
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
188
- this.groupLastIdleCheckTime.set(groupSessionId, now);
189
- return;
190
- }
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
+ }
191
287
 
192
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
193
- this.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, this.db, this.ctx, selfId);
194
- this.groupLastBotMessageTime.set(groupSessionId, now);
195
- this.groupMessageCountAfterBot.set(groupSessionId, 0);
196
- this.startCooldownTimer(groupSessionId, groupId, selfId);
197
- this.ctx.logger.info(`[IdleCheck] group ${groupId} idle reply completed`);
198
- } catch (err) {
199
- this.ctx.logger.error(`[IdleCheck] group ${groupId} idle detection failed: ${err}`);
200
- } finally {
201
- this.idleCheckProcessing.delete(groupSessionId);
202
- }
203
- }
204
- } 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`);
205
303
  }
206
304
  }
@@ -2,7 +2,7 @@ import type { MiokiContext } from "mioki";
2
2
  import type { AIInstance } from "mioku";
3
3
  import type { ChatConfig, TargetMessage } from "../types";
4
4
  import type { ChatDatabase } from "../db";
5
- import type { HumanizeEngine } from "../humanize";
5
+ import type { HumanizeEngine, ChatConfigProvider } from "../humanize";
6
6
  import type { SkillSessionManager } from "./skill-session";
7
7
  import type { GroupStructuredHistoryManager, StructuredUserInput } from "./group-structured-history";
8
8
  import { buildStructuredUserInputFromEvent } from "./group-structured-history";
@@ -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,9 +32,11 @@ 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
- private cfg: ChatConfig;
38
+ private configProvider: ChatConfigProvider;
39
+ private defaultConfig: ChatConfig;
39
40
  private db: ChatDatabase;
40
41
  private humanize: HumanizeEngine;
41
42
  private aiInstance: AIInstance;
@@ -50,17 +51,16 @@ export class QueueProcessor {
50
51
  private getGroupHistoryMessages: GetGroupHistoryMessages;
51
52
  private getGroupInfoData: GetGroupInfoData;
52
53
  private getHumanizeContexts: GetHumanizeContexts;
53
- private sendAIResponse: SendAIResponse;
54
- private saveBotMessages: SaveBotMessages;
55
- private sendEmoji: SendEmoji;
56
54
  private runChat: RunChat;
57
55
  private buildToolContext: BuildToolContext;
58
56
  private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
59
- private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
57
+ private sessionTurnScheduler: SessionTurnScheduler;
60
58
 
61
59
  constructor(pluginCtx: ChatPluginContext) {
60
+ this.pluginCtx = pluginCtx;
62
61
  this.ctx = pluginCtx.ctx;
63
- this.cfg = pluginCtx.config;
62
+ this.configProvider = pluginCtx.configProvider;
63
+ this.defaultConfig = pluginCtx.defaultConfig;
64
64
  this.db = pluginCtx.db;
65
65
  this.humanize = pluginCtx.humanize;
66
66
  this.aiInstance = pluginCtx.aiInstance;
@@ -75,13 +75,10 @@ export class QueueProcessor {
75
75
  this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
76
76
  this.getGroupInfoData = pluginCtx.getGroupInfoData;
77
77
  this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
78
- this.sendAIResponse = pluginCtx.sendAIResponse;
79
- this.saveBotMessages = pluginCtx.saveBotMessages;
80
- this.sendEmoji = pluginCtx.sendEmoji;
81
78
  this.runChat = pluginCtx.runChat;
82
79
  this.buildToolContext = pluginCtx.buildToolContext;
83
80
  this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
84
- this.startCooldownTimer = pluginCtx.startCooldownTimer;
81
+ this.sessionTurnScheduler = pluginCtx.sessionTurnScheduler;
85
82
  }
86
83
 
87
84
  collectDynamicDelayMessage(groupSessionId: string, event: any, content: string): void {
@@ -105,9 +102,29 @@ export class QueueProcessor {
105
102
 
106
103
  this.ctx.logger.info(`[DynamicDelay] group ${groupId} start delay ${delayMs / 1000}s, interactions: ${this.rateLimiter.getInteractionCount(groupId)}`);
107
104
 
108
- queueData.timer = setTimeout(async () => {
109
- 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
+ );
110
126
  }, delayMs);
127
+ queueData.timer = timer;
111
128
  }
112
129
 
113
130
  isInDynamicDelay(groupSessionId: string): boolean {
@@ -115,6 +132,21 @@ export class QueueProcessor {
115
132
  return queue != null && Date.now() < queue.delayUntil;
116
133
  }
117
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
+
118
150
  dispose(): void {
119
151
  for (const queue of this.dynamicDelayQueues.values()) {
120
152
  if (queue.timer) clearTimeout(queue.timer);
@@ -122,16 +154,12 @@ export class QueueProcessor {
122
154
  this.dynamicDelayQueues.clear();
123
155
  }
124
156
 
125
- async processDynamicDelayQueue(groupSessionId: string, groupId: number, selfId: number): Promise<void> {
126
- const queueData = this.dynamicDelayQueues.get(groupSessionId);
127
- if (!queueData || queueData.messages.length === 0) {
128
- this.dynamicDelayQueues.delete(groupSessionId);
129
- return;
130
- }
131
-
132
- const messages = queueData.messages;
133
- this.dynamicDelayQueues.delete(groupSessionId);
134
-
157
+ private async processDynamicDelayMessages(
158
+ groupSessionId: string,
159
+ groupId: number,
160
+ selfId: number,
161
+ messages: DynamicDelayQueueData["messages"],
162
+ ): Promise<void> {
135
163
  this.ctx.logger.info(`[DynamicDelay] group ${groupId} processes ${messages.length} delayed messages`);
136
164
  this.rateLimiter.clearGroupInteractions(groupId);
137
165
 
@@ -147,14 +175,15 @@ export class QueueProcessor {
147
175
  content: mergedContent, messageId: firstMsg.messageId, timestamp: Date.now(),
148
176
  };
149
177
 
178
+ const cfg = this.configProvider(groupId);
150
179
  const botRole = await getBotRole(groupId, this.ctx, selfId);
151
- const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
180
+ const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
152
181
  const { groupName, memberCount } = await this.getGroupInfoData(this.ctx, groupId, selfId, String(groupId));
153
- const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, this.cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, this.cfg));
182
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
154
183
 
155
184
  const toolCtx = this.buildToolContext({
156
185
  ctx: this.ctx, event: firstMsg.event, groupSessionId, groupId, userId: firstMsg.userId,
157
- config: this.cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
186
+ config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
158
187
  });
159
188
 
160
189
  this.sessionManager.getOrCreate(groupSessionId, "group", groupId);
@@ -162,19 +191,29 @@ export class QueueProcessor {
162
191
 
163
192
  const result = await this.runWithRateLimitGuard(
164
193
  () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
165
- config: this.cfg, groupName, memberCount, botNickname, botRole, aiService: this.aiService, isGroup: true,
194
+ config: cfg, groupName, memberCount, botNickname, botRole, aiService: this.aiService, isGroup: true,
166
195
  memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
167
196
  replyContext: { type: "review", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
168
197
  reviewMessages: { contents: mergedContents, userNames, messageIds },
169
198
  }, this.humanize, this.skillManager, {
170
- manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
199
+ manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
171
200
  }),
172
201
  { userId: targetMessage.userId, groupId, label: "dynamic-delay" },
173
202
  );
174
203
  if (!result) return;
175
204
 
176
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
177
- 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
+ });
178
217
  }
179
218
 
180
219
  async processQueuedMessages(groupSessionId: string, selfId: number): Promise<void> {
@@ -191,8 +230,10 @@ export class QueueProcessor {
191
230
  const queuedContents: string[] = [];
192
231
  const structuredUserInputs: StructuredUserInput[] = [];
193
232
 
233
+ const cfg = this.configProvider(groupId);
234
+
194
235
  for (const item of queue) {
195
- const { text: extractedText, multimodal } = extractContent(item.event, this.cfg, this.ctx);
236
+ const { text: extractedText, multimodal } = extractContent(item.event, cfg, this.ctx);
196
237
  const content = multimodal ? JSON.stringify(multimodal) : extractedText;
197
238
  if (content) {
198
239
  queuedContents.push(content);
@@ -219,33 +260,40 @@ export class QueueProcessor {
219
260
  };
220
261
 
221
262
  const botRole = await getBotRole(groupId, this.ctx, selfId);
222
- const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
263
+ const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
223
264
  const toolCtx = this.buildToolContext({
224
265
  ctx: this.ctx, event: null, groupSessionId, groupId, userId: targetMessage.userId,
225
- config: this.cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
266
+ config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
226
267
  });
227
268
 
228
- const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, this.cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, this.cfg));
269
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
229
270
  const contexts = await this.getHumanizeContexts(this.humanize, groupSessionId, targetMessage.userName, history, targetMessage.userId);
230
271
  const { groupName, memberCount } = await this.getGroupInfoData(this.ctx, groupId, selfId);
231
272
 
232
273
  const result = await this.runWithRateLimitGuard(
233
274
  () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
234
- config: this.cfg, groupName, memberCount, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
275
+ config: cfg, groupName, memberCount, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
235
276
  memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
236
277
  replyContext: { type: "comment", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
237
278
  }, this.humanize, this.skillManager, {
238
- manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
279
+ manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
239
280
  }),
240
281
  { userId: targetMessage.userId, groupId, label: "queue" },
241
282
  );
242
283
  if (!result) return;
243
284
 
244
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
245
- await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
246
- const now = Date.now();
247
- this.saveBotMessages(groupId, groupSessionId, result.messages, now, this.cfg, this.db, this.ctx, selfId);
248
- 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
+ });
249
297
  this.ctx.logger.info(`[Queue] group ${groupSessionId} done`);
250
298
  } catch (err) {
251
299
  this.ctx.logger.error(err);