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.
package/index.ts CHANGED
@@ -2,7 +2,7 @@ import { definePlugin } from "mioki";
2
2
  import type { MiokiContext } from "mioki";
3
3
  import type { AIInstance, AIService, ConfigService, ScreenshotService } from "mioku";
4
4
  import { getPluginRuntimeState } from "mioku";
5
- import type { ChatConfig, ChatMessage, TargetMessage } from "./types";
5
+ import type { ChatConfig, ChatMessage, ChatGroupsFile, TargetMessage } from "./types";
6
6
  import { initDatabase } from "./db";
7
7
  import { SessionManager } from "./manage/session";
8
8
  import { RateLimiter } from "./manage/rate-limiter";
@@ -23,6 +23,7 @@ import {
23
23
  import { CooldownManager } from "./manage/cooldown";
24
24
  import { IdleCheckManager } from "./manage/idle-check";
25
25
  import { QueueProcessor } from "./manage/queue-processor";
26
+ import { SessionTurnScheduler } from "./manage/session-turn-scheduler";
26
27
  import { ChatDatabaseCleanup, DEFAULT_CLEANUP_CONFIG } from "./manage/cleanup";
27
28
  import {
28
29
  GroupStructuredHistoryManager,
@@ -32,23 +33,20 @@ import {
32
33
  import { BASE_CONFIG } from "./configs/base";
33
34
  import { SETTINGS_CONFIG } from "./configs/settings";
34
35
  import { PERSONALIZATION_CONFIG } from "./configs/personalization";
36
+ import { DEFAULT_GROUPS_CONFIG } from "./configs/groups";
35
37
  import { RateLimitGuard } from "./manage/rate-limit-guard";
36
38
  import { createChatRuntime } from "./runtime/chat-runtime";
37
39
  import { createMessageHandler } from "./handlers/message";
38
40
  import { createPokeHandler } from "./handlers/poke";
39
41
  import { buildHistoryMediaOptions } from "./core/media/segment";
42
+ import type { ChatConfigProvider } from "./humanize";
40
43
  import type { ChatPluginContext, ChatHandlerState } from "./context";
41
-
42
- function normalizeIdList(input: unknown): number[] {
43
- if (!Array.isArray(input)) return [];
44
- return Array.from(
45
- new Set(
46
- input
47
- .map((item) => Math.floor(Number(item)))
48
- .filter((id) => Number.isFinite(id) && id > 0),
49
- ),
50
- );
51
- }
44
+ import { mergeGroupOverrides } from "./utils/group-config";
45
+ import {
46
+ mergeChatConfig,
47
+ normalizeIdList,
48
+ normalizeMediaAnalysisBlacklist,
49
+ } from "./utils/config";
52
50
 
53
51
  export default definePlugin({
54
52
  name: "chat",
@@ -66,27 +64,40 @@ export default definePlugin({
66
64
  await configService.registerConfig("chat", "base", BASE_CONFIG);
67
65
  await configService.registerConfig("chat", "settings", SETTINGS_CONFIG);
68
66
  await configService.registerConfig("chat", "personalization", PERSONALIZATION_CONFIG);
67
+ await configService.registerConfig("chat", "groups", DEFAULT_GROUPS_CONFIG);
69
68
  }
70
69
 
71
- const getConfig = async (): Promise<ChatConfig> => {
70
+ let cachedBaseConfig: ChatConfig | null = null;
71
+ let cachedGroupsConfig: ChatGroupsFile | null = null;
72
+
73
+ const buildGlobalConfig = async (): Promise<ChatConfig> => {
72
74
  if (!configService) {
73
- return {
74
- ...BASE_CONFIG,
75
- ...SETTINGS_CONFIG,
76
- ...PERSONALIZATION_CONFIG,
77
- } as ChatConfig;
75
+ return mergeChatConfig(
76
+ mergeChatConfig(
77
+ mergeChatConfig({} as ChatConfig, BASE_CONFIG),
78
+ SETTINGS_CONFIG,
79
+ ),
80
+ PERSONALIZATION_CONFIG,
81
+ );
78
82
  }
79
- const base = await configService.getConfig("chat", "base");
80
- const settings = await configService.getConfig("chat", "settings");
81
- const personalization = await configService.getConfig("chat", "personalization");
82
- const merged = {
83
- ...BASE_CONFIG,
84
- ...SETTINGS_CONFIG,
85
- ...PERSONALIZATION_CONFIG,
86
- ...base,
87
- ...settings,
88
- ...personalization,
89
- } as any;
83
+ const base = (await configService.getConfig("chat", "base")) ?? {};
84
+ const settings = (await configService.getConfig("chat", "settings")) ?? {};
85
+ const personalization =
86
+ (await configService.getConfig("chat", "personalization")) ?? {};
87
+ const mergedDefaults = mergeChatConfig(
88
+ mergeChatConfig(
89
+ mergeChatConfig({} as ChatConfig, BASE_CONFIG),
90
+ SETTINGS_CONFIG,
91
+ ),
92
+ PERSONALIZATION_CONFIG,
93
+ );
94
+ const merged = mergeChatConfig(
95
+ mergeChatConfig(
96
+ mergeChatConfig(mergedDefaults, settings),
97
+ personalization,
98
+ ),
99
+ base,
100
+ ) as ChatConfig;
90
101
 
91
102
  if (typeof merged.stream !== "boolean") merged.stream = true;
92
103
  if (typeof merged.enableMarkdownScreenshot !== "boolean") {
@@ -101,14 +112,69 @@ export default definePlugin({
101
112
  }
102
113
  merged.whitelistGroups = normalizeIdList(merged.whitelistGroups);
103
114
  merged.blacklistGroups = normalizeIdList(merged.blacklistGroups);
104
- merged.mediaAnalysisBlacklistUsers = normalizeIdList(
105
- merged.mediaAnalysisBlacklistUsers ?? merged.imageAnalysisBlacklistUsers,
106
- );
107
- delete merged.imageAnalysisBlacklistUsers;
108
- return merged as ChatConfig;
115
+ merged.mediaAnalysisBlacklistUsers =
116
+ normalizeMediaAnalysisBlacklist(merged as Record<string, any>);
117
+ delete (merged as any).imageAnalysisBlacklistUsers;
118
+ return merged;
109
119
  };
110
120
 
111
- const config = await getConfig();
121
+ const refreshGroupsCache = async (): Promise<void> => {
122
+ if (!configService) {
123
+ cachedGroupsConfig = DEFAULT_GROUPS_CONFIG;
124
+ return;
125
+ }
126
+ const raw = await configService.getConfig("chat", "groups");
127
+ cachedGroupsConfig =
128
+ raw && typeof raw === "object" && raw.groups && typeof raw.groups === "object"
129
+ ? (raw as ChatGroupsFile)
130
+ : DEFAULT_GROUPS_CONFIG;
131
+ };
132
+
133
+ const refreshBaseCache = async (): Promise<void> => {
134
+ cachedBaseConfig = await buildGlobalConfig();
135
+ };
136
+
137
+ await refreshBaseCache();
138
+ await refreshGroupsCache();
139
+
140
+ if (configService) {
141
+ configService.onConfigChange("chat", "base", () => {
142
+ refreshBaseCache().catch((err) =>
143
+ ctx.logger.error(`刷新 chat/base 缓存失败: ${err}`),
144
+ );
145
+ });
146
+ configService.onConfigChange("chat", "settings", () => {
147
+ refreshBaseCache().catch((err) =>
148
+ ctx.logger.error(`刷新 chat/settings 缓存失败: ${err}`),
149
+ );
150
+ });
151
+ configService.onConfigChange("chat", "personalization", () => {
152
+ refreshBaseCache().catch((err) =>
153
+ ctx.logger.error(`刷新 chat/personalization 缓存失败: ${err}`),
154
+ );
155
+ });
156
+ configService.onConfigChange("chat", "groups", () => {
157
+ refreshGroupsCache().catch((err) =>
158
+ ctx.logger.error(`刷新 chat/groups 缓存失败: ${err}`),
159
+ );
160
+ });
161
+ }
162
+
163
+ const configProvider: ChatConfigProvider = (groupId?: number) => {
164
+ const base = cachedBaseConfig;
165
+ if (!base) return PERSONALIZATION_CONFIG as unknown as ChatConfig;
166
+ if (groupId === undefined) return base;
167
+ const overrides = cachedGroupsConfig?.groups?.[String(groupId)];
168
+ return mergeGroupOverrides(base, overrides);
169
+ };
170
+
171
+ const getConfig = async (groupId?: number): Promise<ChatConfig> => {
172
+ if (!cachedBaseConfig) await refreshBaseCache();
173
+ return configProvider(groupId);
174
+ };
175
+
176
+ const defaultConfig = configProvider();
177
+ const config = defaultConfig;
112
178
 
113
179
  if (!config.apiKey) {
114
180
  ctx.logger.warn("聊天插件未配置 API Key,请在 config/chat/base.json 中配置");
@@ -119,9 +185,12 @@ export default definePlugin({
119
185
  const sessionManager = new SessionManager(db, config.maxSessions);
120
186
  const queueManager = new MessageQueueManager();
121
187
  const rateLimiter = new RateLimiter({
122
- dynamicDelay: config.dynamicDelay,
123
- aiRequestLimits: config.aiRequestLimits,
188
+ maxTriggersPerWindow: 5,
189
+ windowMs: 60_000,
190
+ dedupWindowMs: 30_000,
191
+ groupCooldownMs: 1_000,
124
192
  });
193
+ rateLimiter.setConfigProvider(configProvider);
125
194
  const skillManager = new SkillSessionManager();
126
195
 
127
196
  rateLimiter.setQueueLengthGetter((groupId: number) =>
@@ -172,17 +241,19 @@ export default definePlugin({
172
241
  });
173
242
  }
174
243
 
175
- const humanize = new HumanizeEngine(mainAIInstance, workAIInstance, config, db);
244
+ const humanize = new HumanizeEngine(workAIInstance, db, configProvider);
176
245
  await humanize.init();
177
246
 
178
247
  const pokeCooldowns = new Map<number, number>();
179
248
  const processingSet = new Set<string>();
249
+ const sessionTurnScheduler = new SessionTurnScheduler();
180
250
  const groupStructuredHistory = new GroupStructuredHistoryManager();
181
251
  const rateLimitGuard = new RateLimitGuard(rateLimiter, ctx.logger);
182
252
  const runWithRateLimitGuard = rateLimitGuard.run.bind(rateLimitGuard);
183
253
 
184
254
  const pluginCtx = {
185
- ctx, config, db,
255
+ ctx, defaultConfig, configProvider, getConfig,
256
+ db,
186
257
  aiInstance: mainAIInstance, workAIInstance, visionAIInstance,
187
258
  aiService, humanize,
188
259
  sessionManager, skillManager, rateLimiter, queueManager,
@@ -190,6 +261,7 @@ export default definePlugin({
190
261
  cooldownManager: undefined as unknown as CooldownManager,
191
262
  idleCheckManager: undefined as unknown as IdleCheckManager,
192
263
  queueProcessor: undefined as unknown as QueueProcessor,
264
+ sessionTurnScheduler,
193
265
  runWithRateLimitGuard,
194
266
  buildHistoryMediaOptions,
195
267
  getGroupHistoryMessages, getGroupInfoData, getHumanizeContexts,
@@ -209,12 +281,13 @@ export default definePlugin({
209
281
  humanize.topicTracker.onMessage(groupSessionId),
210
282
  ]);
211
283
  if (userMsg.groupId) {
284
+ const ttlMs = configProvider(userMsg.groupId).groupStructuredHistoryTtlMs;
212
285
  groupStructuredHistory.append(
213
286
  groupSessionId,
214
287
  buildStructuredUserMessages([
215
288
  buildStructuredUserInputFromTarget(userMsg as unknown as TargetMessage),
216
289
  ]),
217
- config.groupStructuredHistoryTtlMs,
290
+ ttlMs,
218
291
  );
219
292
  }
220
293
  },
@@ -255,15 +328,17 @@ export default definePlugin({
255
328
  ctx.logger.info("聊天插件加载成功");
256
329
 
257
330
  return () => {
258
- db.close();
259
- rateLimiter.dispose();
260
- clearInterval(cleanupInterval);
261
- dbCleanup.stop();
262
- cooldownManager.dispose();
263
331
  idleCheckManager.dispose();
332
+ cooldownManager.dispose();
264
333
  queueProcessor.dispose();
334
+ sessionTurnScheduler.dispose();
335
+ queueManager.dispose();
336
+ clearInterval(cleanupInterval);
337
+ dbCleanup.stop();
338
+ rateLimiter.dispose();
265
339
  processingSet.clear();
266
340
  pokeCooldowns.clear();
341
+ db.close();
267
342
  ctx.logger.info("聊天插件已卸载");
268
343
  };
269
344
  },
@@ -2,27 +2,24 @@ 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 { buildStructuredUserInputFromEvent } from "./group-structured-history";
9
- import type { SessionManager } from "./session";
10
9
  import type { RateLimiter } from "./rate-limiter";
11
- import type { IdleCheckManager } from "./idle-check";
12
10
  import type {
13
11
  RunRateLimitGuard,
14
12
  HistoryMediaOptions,
15
13
  GetGroupHistoryMessages,
16
14
  GetGroupInfoData,
17
15
  GetHumanizeContexts,
18
- SendAIResponse,
19
- SaveBotMessages,
20
- SendEmoji,
21
16
  RunChat,
22
17
  BuildToolContext,
23
18
  BuildStructuredUserInput,
24
19
  } from "./types";
25
20
  import type { ChatPluginContext } from "../context";
21
+ import type { SessionTurnScheduler } from "./session-turn-scheduler";
22
+ import { finalizeChatTurn } from "../core/chat-turn";
26
23
  import { getBotRole } from "../utils";
27
24
 
28
25
  interface CooldownMessage {
@@ -39,18 +36,19 @@ export class CooldownManager {
39
36
  private groupCooldownUntil = new Map<string, number>();
40
37
  private groupCooldownMessages = new Map<string, CooldownMessage[]>();
41
38
  private cooldownTimeoutIds = new Map<string, NodeJS.Timeout>();
39
+ private pluginCtx: ChatPluginContext;
40
+ private sessionTurnScheduler: SessionTurnScheduler;
42
41
 
43
42
  private ctx: MiokiContext;
44
- private cfg: ChatConfig;
43
+ private configProvider: ChatConfigProvider;
44
+ private defaultConfig: ChatConfig;
45
45
  private db: ChatDatabase;
46
46
  private humanize: HumanizeEngine;
47
47
  private aiInstance: AIInstance;
48
48
  private aiService: import("mioku").AIService;
49
49
  private skillManager: SkillSessionManager;
50
50
  private groupStructuredHistory: GroupStructuredHistoryManager;
51
- private sessionManager: SessionManager;
52
51
  private rateLimiter: RateLimiter;
53
- private idleCheckManager: IdleCheckManager;
54
52
  private runWithRateLimitGuard: RunRateLimitGuard;
55
53
  private buildHistoryMediaOptions: (
56
54
  ai: AIInstance,
@@ -59,33 +57,28 @@ export class CooldownManager {
59
57
  private getGroupHistoryMessages: GetGroupHistoryMessages;
60
58
  private getGroupInfoData: GetGroupInfoData;
61
59
  private getHumanizeContexts: GetHumanizeContexts;
62
- private sendAIResponse: SendAIResponse;
63
- private saveBotMessages: SaveBotMessages;
64
- private sendEmoji: SendEmoji;
65
60
  private runChat: RunChat;
66
61
  private buildToolContext: BuildToolContext;
67
62
  private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
68
63
 
69
64
  constructor(pluginCtx: ChatPluginContext) {
65
+ this.pluginCtx = pluginCtx;
66
+ this.sessionTurnScheduler = pluginCtx.sessionTurnScheduler;
70
67
  this.ctx = pluginCtx.ctx;
71
- this.cfg = pluginCtx.config;
68
+ this.configProvider = pluginCtx.configProvider;
69
+ this.defaultConfig = pluginCtx.defaultConfig;
72
70
  this.db = pluginCtx.db;
73
71
  this.humanize = pluginCtx.humanize;
74
72
  this.aiInstance = pluginCtx.aiInstance;
75
73
  this.aiService = pluginCtx.aiService;
76
74
  this.skillManager = pluginCtx.skillManager;
77
75
  this.groupStructuredHistory = pluginCtx.groupStructuredHistory;
78
- this.sessionManager = pluginCtx.sessionManager;
79
76
  this.rateLimiter = pluginCtx.rateLimiter;
80
- this.idleCheckManager = pluginCtx.idleCheckManager;
81
77
  this.runWithRateLimitGuard = pluginCtx.runWithRateLimitGuard;
82
78
  this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
83
79
  this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
84
80
  this.getGroupInfoData = pluginCtx.getGroupInfoData;
85
81
  this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
86
- this.sendAIResponse = pluginCtx.sendAIResponse;
87
- this.saveBotMessages = pluginCtx.saveBotMessages;
88
- this.sendEmoji = pluginCtx.sendEmoji;
89
82
  this.runChat = pluginCtx.runChat;
90
83
  this.buildToolContext = pluginCtx.buildToolContext;
91
84
  this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
@@ -99,47 +92,52 @@ export class CooldownManager {
99
92
  const existingTimer = this.cooldownTimeoutIds.get(groupSessionId);
100
93
  if (existingTimer) clearTimeout(existingTimer);
101
94
 
102
- const cooldownMs = this.cfg.cooldownAfterReplyMs ?? 20_000;
95
+ const cfg = this.configProvider(groupId);
96
+ const cooldownMs =
97
+ cfg.cooldownAfterReplyMs ??
98
+ this.defaultConfig.cooldownAfterReplyMs ??
99
+ 20_000;
103
100
 
104
- const timer = setTimeout(async () => {
101
+ const timer = setTimeout(() => {
102
+ if (this.cooldownTimeoutIds.get(groupSessionId) !== timer) return;
105
103
  this.cooldownTimeoutIds.delete(groupSessionId);
104
+ this.groupCooldownUntil.delete(groupSessionId);
106
105
  const collected = this.groupCooldownMessages.get(groupSessionId) || [];
107
- if (collected.length === 0) {
108
- this.groupCooldownMessages.delete(groupSessionId);
109
- this.groupCooldownUntil.delete(groupSessionId);
110
- return;
111
- }
106
+ this.groupCooldownMessages.delete(groupSessionId);
107
+ if (collected.length === 0) return;
112
108
 
113
- const directAtMessages = collected.filter((m) => m.isDirectAt);
114
- try {
115
- if (directAtMessages.length > 0) {
116
- await this.processReviewMessages(
117
- groupSessionId,
118
- groupId,
119
- collected,
120
- selfId,
121
- );
122
- } else {
123
- await this.processCooldownWithPlanner(
124
- groupSessionId,
125
- groupId,
126
- collected,
127
- selfId,
128
- );
129
- }
130
- } catch (err) {
131
- this.ctx.logger.error(
132
- `[Cooldown] Group ${groupId} processing failed: ${err}`,
109
+ const hasDirectAt = collected.some((message) => message.isDirectAt);
110
+ void this.sessionTurnScheduler
111
+ .run(
112
+ groupSessionId,
113
+ hasDirectAt ? "cooldown" : "cooldown-planner",
114
+ () =>
115
+ hasDirectAt
116
+ ? this.processReviewMessages(
117
+ groupSessionId,
118
+ groupId,
119
+ collected,
120
+ selfId,
121
+ )
122
+ : this.processCooldownWithPlanner(
123
+ groupSessionId,
124
+ groupId,
125
+ collected,
126
+ selfId,
127
+ ),
128
+ )
129
+ .catch((err) =>
130
+ this.ctx.logger.error(
131
+ `[Cooldown] Group ${groupId} processing failed: ${err}`,
132
+ ),
133
133
  );
134
- } finally {
135
- this.groupCooldownMessages.delete(groupSessionId);
136
- this.groupCooldownUntil.delete(groupSessionId);
137
- }
138
134
  }, cooldownMs);
139
135
 
140
136
  this.cooldownTimeoutIds.set(groupSessionId, timer);
141
137
  this.groupCooldownUntil.set(groupSessionId, Date.now() + cooldownMs);
142
- this.groupCooldownMessages.set(groupSessionId, []);
138
+ if (!this.groupCooldownMessages.has(groupSessionId)) {
139
+ this.groupCooldownMessages.set(groupSessionId, []);
140
+ }
143
141
  }
144
142
 
145
143
  collectMessage(
@@ -196,17 +194,19 @@ export class CooldownManager {
196
194
  timestamp: Date.now(),
197
195
  };
198
196
 
197
+ const cfg = this.configProvider(groupId);
198
+
199
199
  const { history } = await this.getGroupHistoryMessages(
200
200
  groupId,
201
201
  groupSessionId,
202
202
  this.ctx,
203
- this.cfg.historyCount,
203
+ cfg.historyCount,
204
204
  this.db,
205
205
  selfId,
206
- this.buildHistoryMediaOptions(this.aiInstance, this.cfg),
206
+ this.buildHistoryMediaOptions(this.aiInstance, cfg),
207
207
  );
208
208
  const botNickname =
209
- this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
209
+ cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
210
210
  const botRole = await getBotRole(groupId, this.ctx, selfId);
211
211
  const { groupName, memberCount } = await this.getGroupInfoData(
212
212
  this.ctx,
@@ -220,7 +220,7 @@ export class CooldownManager {
220
220
  groupSessionId,
221
221
  groupId,
222
222
  userId: targetMessage.userId,
223
- config: this.cfg,
223
+ config: cfg,
224
224
  aiService: this.aiService,
225
225
  db: this.db,
226
226
  botRole,
@@ -245,7 +245,7 @@ export class CooldownManager {
245
245
  history,
246
246
  targetMessage,
247
247
  {
248
- config: this.cfg,
248
+ config: cfg,
249
249
  groupName,
250
250
  memberCount,
251
251
  botNickname,
@@ -266,7 +266,7 @@ export class CooldownManager {
266
266
  this.skillManager,
267
267
  {
268
268
  manager: this.groupStructuredHistory,
269
- ttlMs: this.cfg.groupStructuredHistoryTtlMs,
269
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
270
270
  currentUserInputs: collected.map((msg) =>
271
271
  this.buildStructuredUserInputFromEvent(
272
272
  msg.event,
@@ -280,36 +280,18 @@ export class CooldownManager {
280
280
  );
281
281
  if (!result) return;
282
282
 
283
- await this.sendAIResponse(
284
- {
285
- ctx: this.ctx,
286
- groupId,
287
- messages: result.messages,
288
- config: this.cfg,
289
- sentIndices: toolCtx.sentMessageIndices,
290
- },
291
- selfId,
292
- );
293
- await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
294
-
295
- const now = Date.now();
296
- this.saveBotMessages(
283
+ await finalizeChatTurn(this.pluginCtx, {
284
+ event: firstMsg.event,
285
+ cfg,
286
+ result,
297
287
  groupId,
298
288
  groupSessionId,
299
- result.messages,
300
- now,
301
- this.cfg,
302
- this.db,
303
- this.ctx,
304
- selfId,
305
- );
306
- this.idleCheckManager.recordBotMessages(
307
- groupSessionId,
308
- result.messages.length,
289
+ userId: targetMessage.userId,
309
290
  selfId,
310
- );
311
- this.sessionManager.touch(groupSessionId);
312
- this.startCooldownTimer(groupSessionId, groupId, selfId);
291
+ toolCtx,
292
+ send: true,
293
+ isLive: true,
294
+ });
313
295
  }
314
296
 
315
297
  private async processCooldownWithPlanner(
@@ -321,17 +303,19 @@ export class CooldownManager {
321
303
  const mergedContent = collected.map((m) => m.content).join("\n");
322
304
  const firstMsg = collected[0];
323
305
 
306
+ const cfg = this.configProvider(groupId);
307
+
324
308
  const { history } = await this.getGroupHistoryMessages(
325
309
  groupId,
326
310
  groupSessionId,
327
311
  this.ctx,
328
- this.cfg.historyCount,
312
+ cfg.historyCount,
329
313
  this.db,
330
314
  selfId,
331
- this.buildHistoryMediaOptions(this.aiInstance, this.cfg),
315
+ this.buildHistoryMediaOptions(this.aiInstance, cfg),
332
316
  );
333
317
  const botNickname =
334
- this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
318
+ cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
335
319
 
336
320
  const planResult = await this.humanize.actionPlanner.plan(
337
321
  groupSessionId,
@@ -362,7 +346,7 @@ export class CooldownManager {
362
346
  groupSessionId,
363
347
  groupId,
364
348
  userId: targetMessage.userId,
365
- config: this.cfg,
349
+ config: cfg,
366
350
  aiService: this.aiService,
367
351
  db: this.db,
368
352
  botRole,
@@ -392,7 +376,7 @@ export class CooldownManager {
392
376
  history,
393
377
  targetMessage,
394
378
  {
395
- config: this.cfg,
379
+ config: cfg,
396
380
  groupName,
397
381
  memberCount,
398
382
  botNickname,
@@ -418,7 +402,7 @@ export class CooldownManager {
418
402
  this.skillManager,
419
403
  {
420
404
  manager: this.groupStructuredHistory,
421
- ttlMs: this.cfg.groupStructuredHistoryTtlMs,
405
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
422
406
  currentUserInputs: collected.map((msg) =>
423
407
  this.buildStructuredUserInputFromEvent(
424
408
  msg.event,
@@ -437,34 +421,17 @@ export class CooldownManager {
437
421
  return;
438
422
  }
439
423
 
440
- await this.sendAIResponse(
441
- {
442
- ctx: this.ctx,
443
- groupId,
444
- messages: result.messages,
445
- config: this.cfg,
446
- sentIndices: toolCtx.sentMessageIndices,
447
- },
448
- selfId,
449
- );
450
- await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
451
- const now = Date.now();
452
- this.saveBotMessages(
424
+ await finalizeChatTurn(this.pluginCtx, {
425
+ event: firstMsg.event,
426
+ cfg,
427
+ result,
453
428
  groupId,
454
429
  groupSessionId,
455
- result.messages,
456
- now,
457
- this.cfg,
458
- this.db,
459
- this.ctx,
460
- selfId,
461
- );
462
- this.idleCheckManager.recordBotMessages(
463
- groupSessionId,
464
- result.messages.length,
430
+ userId: targetMessage.userId,
465
431
  selfId,
466
- );
467
- this.sessionManager.touch(groupSessionId);
468
- this.startCooldownTimer(groupSessionId, groupId, selfId);
432
+ toolCtx,
433
+ send: true,
434
+ isLive: true,
435
+ });
469
436
  }
470
- }
437
+ }