mioku-plugin-chat 2.5.2 → 2.6.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/humanize/topic.ts CHANGED
@@ -2,25 +2,28 @@ import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import { extractJsonObject } from "../utils/json";
4
4
  import type { ChatDatabase } from "../db";
5
- import type { ChatConfig } from "../types";
5
+ import type { ChatMessage } from "../types";
6
+ import { extractGroupIdFromSession } from "../utils/group-config";
7
+ import type { ChatConfigProvider } from "./index";
6
8
 
7
9
  export class TopicTracker {
8
10
  private ai: AIInstance;
9
- private config: ChatConfig;
11
+ private getConfig: ChatConfigProvider;
10
12
  private db: ChatDatabase;
11
13
  private lastAnalyzedWindowEnd: Map<string, number> = new Map();
12
14
 
13
- constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
15
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
14
16
  this.ai = ai;
15
- this.config = config;
17
+ this.getConfig = configProvider;
16
18
  this.db = db;
17
19
  }
18
20
 
19
21
  async onMessage(sessionId: string): Promise<void> {
20
- if (!this.config.topic?.enabled) return;
22
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
23
+ if (!cfg.topic?.enabled) return;
21
24
  if (!sessionId.startsWith("group:")) return;
22
25
 
23
- const windowMs = this.getWindowMs();
26
+ const windowMs = this.getWindowMs(cfg);
24
27
  const completedWindowEnd = Math.floor(Date.now() / windowMs) * windowMs;
25
28
  if (completedWindowEnd <= 0) return;
26
29
 
@@ -32,18 +35,19 @@ export class TopicTracker {
32
35
  this.lastAnalyzedWindowEnd.set(sessionId, completedWindowEnd);
33
36
  const windowStartAt = completedWindowEnd - windowMs;
34
37
 
35
- this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd).catch(
38
+ this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd, cfg).catch(
36
39
  (err) => logger.warn(`[TopicTracker] Analysis failed: ${err}`),
37
40
  );
38
41
  }
39
42
 
40
43
  getTopicContext(sessionId: string, historyStartAt?: number): string {
41
- if (!this.config.topic?.enabled) return "";
44
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
45
+ if (!cfg.topic?.enabled) return "";
42
46
  if (!sessionId.startsWith("group:")) return "";
43
47
  if (!historyStartAt) return "";
44
48
 
45
- const windowMs = this.getWindowMs();
46
- const windowCount = this.getHistoryWindowCount();
49
+ const windowMs = this.getWindowMs(cfg);
50
+ const windowCount = this.getHistoryWindowCount(cfg);
47
51
  const firstWindowEnd = Math.floor(historyStartAt / windowMs) * windowMs;
48
52
  if (firstWindowEnd <= 0) return "";
49
53
 
@@ -79,6 +83,7 @@ export class TopicTracker {
79
83
  sessionId: string,
80
84
  windowStartAt: number,
81
85
  windowEndAt: number,
86
+ cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
82
87
  ): Promise<void> {
83
88
  if (windowEndAt <= windowStartAt) return;
84
89
 
@@ -119,7 +124,7 @@ Output strictly in JSON format:
119
124
  "summary": "one concise abstract summary sentence"
120
125
  }`,
121
126
  messages: [],
122
- model: this.config.workingModel || this.config.model,
127
+ model: cfg.workingModel || cfg.model,
123
128
  temperature: 0.2,
124
129
  max_tokens: 300,
125
130
  });
@@ -163,14 +168,14 @@ Output strictly in JSON format:
163
168
  }
164
169
  }
165
170
 
166
- private getWindowMs(): number {
167
- const hours = Number(this.config.topic?.windowHours);
171
+ private getWindowMs(cfg = this.getConfig()): number {
172
+ const hours = Number(cfg.topic?.windowHours);
168
173
  const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 5;
169
174
  return Math.max(1, Math.floor(safeHours)) * 3600_000;
170
175
  }
171
176
 
172
- private getHistoryWindowCount(): number {
173
- const count = Number(this.config.topic?.historyWindowCount);
177
+ private getHistoryWindowCount(cfg = this.getConfig()): number {
178
+ const count = Number(cfg.topic?.historyWindowCount);
174
179
  if (!Number.isFinite(count) || count <= 0) return 3;
175
180
  return Math.max(1, Math.floor(count));
176
181
  }
@@ -202,3 +207,5 @@ Output strictly in JSON format:
202
207
  return `${year}-${month}-${day} ${hour}:${minute}`;
203
208
  }
204
209
  }
210
+
211
+ export type { ChatMessage };
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";
@@ -32,12 +32,15 @@ import {
32
32
  import { BASE_CONFIG } from "./configs/base";
33
33
  import { SETTINGS_CONFIG } from "./configs/settings";
34
34
  import { PERSONALIZATION_CONFIG } from "./configs/personalization";
35
+ import { DEFAULT_GROUPS_CONFIG } from "./configs/groups";
35
36
  import { RateLimitGuard } from "./manage/rate-limit-guard";
36
37
  import { createChatRuntime } from "./runtime/chat-runtime";
37
38
  import { createMessageHandler } from "./handlers/message";
38
39
  import { createPokeHandler } from "./handlers/poke";
39
40
  import { buildHistoryMediaOptions } from "./core/media/segment";
41
+ import type { ChatConfigProvider } from "./humanize";
40
42
  import type { ChatPluginContext, ChatHandlerState } from "./context";
43
+ import { mergeGroupOverrides } from "./utils/group-config";
41
44
 
42
45
  function normalizeIdList(input: unknown): number[] {
43
46
  if (!Array.isArray(input)) return [];
@@ -50,6 +53,20 @@ function normalizeIdList(input: unknown): number[] {
50
53
  );
51
54
  }
52
55
 
56
+ function shallowMergeTopLevel<T extends Record<string, any>>(
57
+ defaults: T,
58
+ overrides: Record<string, any>,
59
+ ): T {
60
+ const result: Record<string, any> = { ...defaults };
61
+ for (const key of Object.keys(overrides)) {
62
+ const value = overrides[key];
63
+ if (value !== undefined && value !== null) {
64
+ result[key] = value;
65
+ }
66
+ }
67
+ return result as T;
68
+ }
69
+
53
70
  export default definePlugin({
54
71
  name: "chat",
55
72
  version: "1.0.0",
@@ -66,27 +83,36 @@ export default definePlugin({
66
83
  await configService.registerConfig("chat", "base", BASE_CONFIG);
67
84
  await configService.registerConfig("chat", "settings", SETTINGS_CONFIG);
68
85
  await configService.registerConfig("chat", "personalization", PERSONALIZATION_CONFIG);
86
+ await configService.registerConfig("chat", "groups", DEFAULT_GROUPS_CONFIG);
69
87
  }
70
88
 
71
- const getConfig = async (): Promise<ChatConfig> => {
89
+ let cachedBaseConfig: ChatConfig | null = null;
90
+ let cachedGroupsConfig: ChatGroupsFile | null = null;
91
+
92
+ const buildGlobalConfig = async (): Promise<ChatConfig> => {
72
93
  if (!configService) {
73
- return {
74
- ...BASE_CONFIG,
75
- ...SETTINGS_CONFIG,
76
- ...PERSONALIZATION_CONFIG,
77
- } as ChatConfig;
94
+ return shallowMergeTopLevel(
95
+ shallowMergeTopLevel(
96
+ shallowMergeTopLevel({} as ChatConfig, BASE_CONFIG),
97
+ SETTINGS_CONFIG,
98
+ ),
99
+ PERSONALIZATION_CONFIG,
100
+ );
78
101
  }
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;
102
+ const base = (await configService.getConfig("chat", "base")) ?? {};
103
+ const settings = (await configService.getConfig("chat", "settings")) ?? {};
104
+ const personalization =
105
+ (await configService.getConfig("chat", "personalization")) ?? {};
106
+ const merged = shallowMergeTopLevel(
107
+ shallowMergeTopLevel(
108
+ shallowMergeTopLevel({} as ChatConfig, BASE_CONFIG),
109
+ SETTINGS_CONFIG,
110
+ ),
111
+ shallowMergeTopLevel(
112
+ shallowMergeTopLevel({} as ChatConfig, personalization),
113
+ base,
114
+ ),
115
+ ) as ChatConfig;
90
116
 
91
117
  if (typeof merged.stream !== "boolean") merged.stream = true;
92
118
  if (typeof merged.enableMarkdownScreenshot !== "boolean") {
@@ -102,13 +128,69 @@ export default definePlugin({
102
128
  merged.whitelistGroups = normalizeIdList(merged.whitelistGroups);
103
129
  merged.blacklistGroups = normalizeIdList(merged.blacklistGroups);
104
130
  merged.mediaAnalysisBlacklistUsers = normalizeIdList(
105
- merged.mediaAnalysisBlacklistUsers ?? merged.imageAnalysisBlacklistUsers,
131
+ (merged as any).mediaAnalysisBlacklistUsers ?? (merged as any).imageAnalysisBlacklistUsers,
106
132
  );
107
- delete merged.imageAnalysisBlacklistUsers;
108
- return merged as ChatConfig;
133
+ delete (merged as any).imageAnalysisBlacklistUsers;
134
+ return merged;
135
+ };
136
+
137
+ const refreshGroupsCache = async (): Promise<void> => {
138
+ if (!configService) {
139
+ cachedGroupsConfig = DEFAULT_GROUPS_CONFIG;
140
+ return;
141
+ }
142
+ const raw = await configService.getConfig("chat", "groups");
143
+ cachedGroupsConfig =
144
+ raw && typeof raw === "object" && raw.groups && typeof raw.groups === "object"
145
+ ? (raw as ChatGroupsFile)
146
+ : DEFAULT_GROUPS_CONFIG;
147
+ };
148
+
149
+ const refreshBaseCache = async (): Promise<void> => {
150
+ cachedBaseConfig = await buildGlobalConfig();
151
+ };
152
+
153
+ await refreshBaseCache();
154
+ await refreshGroupsCache();
155
+
156
+ if (configService) {
157
+ configService.onConfigChange("chat", "base", () => {
158
+ refreshBaseCache().catch((err) =>
159
+ ctx.logger.error(`刷新 chat/base 缓存失败: ${err}`),
160
+ );
161
+ });
162
+ configService.onConfigChange("chat", "settings", () => {
163
+ refreshBaseCache().catch((err) =>
164
+ ctx.logger.error(`刷新 chat/settings 缓存失败: ${err}`),
165
+ );
166
+ });
167
+ configService.onConfigChange("chat", "personalization", () => {
168
+ refreshBaseCache().catch((err) =>
169
+ ctx.logger.error(`刷新 chat/personalization 缓存失败: ${err}`),
170
+ );
171
+ });
172
+ configService.onConfigChange("chat", "groups", () => {
173
+ refreshGroupsCache().catch((err) =>
174
+ ctx.logger.error(`刷新 chat/groups 缓存失败: ${err}`),
175
+ );
176
+ });
177
+ }
178
+
179
+ const configProvider: ChatConfigProvider = (groupId?: number) => {
180
+ const base = cachedBaseConfig;
181
+ if (!base) return PERSONALIZATION_CONFIG as unknown as ChatConfig;
182
+ if (groupId === undefined) return base;
183
+ const overrides = cachedGroupsConfig?.groups?.[String(groupId)];
184
+ return mergeGroupOverrides(base, overrides);
185
+ };
186
+
187
+ const getConfig = async (groupId?: number): Promise<ChatConfig> => {
188
+ if (!cachedBaseConfig) await refreshBaseCache();
189
+ return configProvider(groupId);
109
190
  };
110
191
 
111
- const config = await getConfig();
192
+ const defaultConfig = configProvider();
193
+ const config = defaultConfig;
112
194
 
113
195
  if (!config.apiKey) {
114
196
  ctx.logger.warn("聊天插件未配置 API Key,请在 config/chat/base.json 中配置");
@@ -119,9 +201,12 @@ export default definePlugin({
119
201
  const sessionManager = new SessionManager(db, config.maxSessions);
120
202
  const queueManager = new MessageQueueManager();
121
203
  const rateLimiter = new RateLimiter({
122
- dynamicDelay: config.dynamicDelay,
123
- aiRequestLimits: config.aiRequestLimits,
204
+ maxTriggersPerWindow: 5,
205
+ windowMs: 60_000,
206
+ dedupWindowMs: 30_000,
207
+ groupCooldownMs: 1_000,
124
208
  });
209
+ rateLimiter.setConfigProvider(configProvider);
125
210
  const skillManager = new SkillSessionManager();
126
211
 
127
212
  rateLimiter.setQueueLengthGetter((groupId: number) =>
@@ -172,7 +257,7 @@ export default definePlugin({
172
257
  });
173
258
  }
174
259
 
175
- const humanize = new HumanizeEngine(mainAIInstance, workAIInstance, config, db);
260
+ const humanize = new HumanizeEngine(mainAIInstance, workAIInstance, db, configProvider);
176
261
  await humanize.init();
177
262
 
178
263
  const pokeCooldowns = new Map<number, number>();
@@ -182,7 +267,8 @@ export default definePlugin({
182
267
  const runWithRateLimitGuard = rateLimitGuard.run.bind(rateLimitGuard);
183
268
 
184
269
  const pluginCtx = {
185
- ctx, config, db,
270
+ ctx, defaultConfig, configProvider, getConfig,
271
+ db,
186
272
  aiInstance: mainAIInstance, workAIInstance, visionAIInstance,
187
273
  aiService, humanize,
188
274
  sessionManager, skillManager, rateLimiter, queueManager,
@@ -209,12 +295,13 @@ export default definePlugin({
209
295
  humanize.topicTracker.onMessage(groupSessionId),
210
296
  ]);
211
297
  if (userMsg.groupId) {
298
+ const ttlMs = configProvider(userMsg.groupId).groupStructuredHistoryTtlMs;
212
299
  groupStructuredHistory.append(
213
300
  groupSessionId,
214
301
  buildStructuredUserMessages([
215
302
  buildStructuredUserInputFromTarget(userMsg as unknown as TargetMessage),
216
303
  ]),
217
- config.groupStructuredHistoryTtlMs,
304
+ ttlMs,
218
305
  );
219
306
  }
220
307
  },
@@ -267,4 +354,4 @@ export default definePlugin({
267
354
  ctx.logger.info("聊天插件已卸载");
268
355
  };
269
356
  },
270
- });
357
+ });
@@ -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 { buildStructuredUserInputFromEvent } from "./group-structured-history";
@@ -41,7 +41,8 @@ export class CooldownManager {
41
41
  private cooldownTimeoutIds = new Map<string, NodeJS.Timeout>();
42
42
 
43
43
  private ctx: MiokiContext;
44
- private cfg: ChatConfig;
44
+ private configProvider: ChatConfigProvider;
45
+ private defaultConfig: ChatConfig;
45
46
  private db: ChatDatabase;
46
47
  private humanize: HumanizeEngine;
47
48
  private aiInstance: AIInstance;
@@ -68,7 +69,8 @@ export class CooldownManager {
68
69
 
69
70
  constructor(pluginCtx: ChatPluginContext) {
70
71
  this.ctx = pluginCtx.ctx;
71
- this.cfg = pluginCtx.config;
72
+ this.configProvider = pluginCtx.configProvider;
73
+ this.defaultConfig = pluginCtx.defaultConfig;
72
74
  this.db = pluginCtx.db;
73
75
  this.humanize = pluginCtx.humanize;
74
76
  this.aiInstance = pluginCtx.aiInstance;
@@ -99,7 +101,8 @@ export class CooldownManager {
99
101
  const existingTimer = this.cooldownTimeoutIds.get(groupSessionId);
100
102
  if (existingTimer) clearTimeout(existingTimer);
101
103
 
102
- const cooldownMs = this.cfg.cooldownAfterReplyMs ?? 20_000;
104
+ const cfg = this.configProvider(groupId);
105
+ const cooldownMs = cfg.cooldownAfterReplyMs ?? this.defaultConfig.cooldownAfterReplyMs ?? 20_000;
103
106
 
104
107
  const timer = setTimeout(async () => {
105
108
  this.cooldownTimeoutIds.delete(groupSessionId);
@@ -196,17 +199,19 @@ export class CooldownManager {
196
199
  timestamp: Date.now(),
197
200
  };
198
201
 
202
+ const cfg = this.configProvider(groupId);
203
+
199
204
  const { history } = await this.getGroupHistoryMessages(
200
205
  groupId,
201
206
  groupSessionId,
202
207
  this.ctx,
203
- this.cfg.historyCount,
208
+ cfg.historyCount,
204
209
  this.db,
205
210
  selfId,
206
- this.buildHistoryMediaOptions(this.aiInstance, this.cfg),
211
+ this.buildHistoryMediaOptions(this.aiInstance, cfg),
207
212
  );
208
213
  const botNickname =
209
- this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
214
+ cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
210
215
  const botRole = await getBotRole(groupId, this.ctx, selfId);
211
216
  const { groupName, memberCount } = await this.getGroupInfoData(
212
217
  this.ctx,
@@ -220,7 +225,7 @@ export class CooldownManager {
220
225
  groupSessionId,
221
226
  groupId,
222
227
  userId: targetMessage.userId,
223
- config: this.cfg,
228
+ config: cfg,
224
229
  aiService: this.aiService,
225
230
  db: this.db,
226
231
  botRole,
@@ -245,7 +250,7 @@ export class CooldownManager {
245
250
  history,
246
251
  targetMessage,
247
252
  {
248
- config: this.cfg,
253
+ config: cfg,
249
254
  groupName,
250
255
  memberCount,
251
256
  botNickname,
@@ -266,7 +271,7 @@ export class CooldownManager {
266
271
  this.skillManager,
267
272
  {
268
273
  manager: this.groupStructuredHistory,
269
- ttlMs: this.cfg.groupStructuredHistoryTtlMs,
274
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
270
275
  currentUserInputs: collected.map((msg) =>
271
276
  this.buildStructuredUserInputFromEvent(
272
277
  msg.event,
@@ -285,7 +290,7 @@ export class CooldownManager {
285
290
  ctx: this.ctx,
286
291
  groupId,
287
292
  messages: result.messages,
288
- config: this.cfg,
293
+ config: cfg,
289
294
  sentIndices: toolCtx.sentMessageIndices,
290
295
  },
291
296
  selfId,
@@ -298,7 +303,7 @@ export class CooldownManager {
298
303
  groupSessionId,
299
304
  result.messages,
300
305
  now,
301
- this.cfg,
306
+ cfg,
302
307
  this.db,
303
308
  this.ctx,
304
309
  selfId,
@@ -321,17 +326,19 @@ export class CooldownManager {
321
326
  const mergedContent = collected.map((m) => m.content).join("\n");
322
327
  const firstMsg = collected[0];
323
328
 
329
+ const cfg = this.configProvider(groupId);
330
+
324
331
  const { history } = await this.getGroupHistoryMessages(
325
332
  groupId,
326
333
  groupSessionId,
327
334
  this.ctx,
328
- this.cfg.historyCount,
335
+ cfg.historyCount,
329
336
  this.db,
330
337
  selfId,
331
- this.buildHistoryMediaOptions(this.aiInstance, this.cfg),
338
+ this.buildHistoryMediaOptions(this.aiInstance, cfg),
332
339
  );
333
340
  const botNickname =
334
- this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
341
+ cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
335
342
 
336
343
  const planResult = await this.humanize.actionPlanner.plan(
337
344
  groupSessionId,
@@ -362,7 +369,7 @@ export class CooldownManager {
362
369
  groupSessionId,
363
370
  groupId,
364
371
  userId: targetMessage.userId,
365
- config: this.cfg,
372
+ config: cfg,
366
373
  aiService: this.aiService,
367
374
  db: this.db,
368
375
  botRole,
@@ -392,7 +399,7 @@ export class CooldownManager {
392
399
  history,
393
400
  targetMessage,
394
401
  {
395
- config: this.cfg,
402
+ config: cfg,
396
403
  groupName,
397
404
  memberCount,
398
405
  botNickname,
@@ -418,7 +425,7 @@ export class CooldownManager {
418
425
  this.skillManager,
419
426
  {
420
427
  manager: this.groupStructuredHistory,
421
- ttlMs: this.cfg.groupStructuredHistoryTtlMs,
428
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
422
429
  currentUserInputs: collected.map((msg) =>
423
430
  this.buildStructuredUserInputFromEvent(
424
431
  msg.event,
@@ -442,7 +449,7 @@ export class CooldownManager {
442
449
  ctx: this.ctx,
443
450
  groupId,
444
451
  messages: result.messages,
445
- config: this.cfg,
452
+ config: cfg,
446
453
  sentIndices: toolCtx.sentMessageIndices,
447
454
  },
448
455
  selfId,
@@ -454,7 +461,7 @@ export class CooldownManager {
454
461
  groupSessionId,
455
462
  result.messages,
456
463
  now,
457
- this.cfg,
464
+ cfg,
458
465
  this.db,
459
466
  this.ctx,
460
467
  selfId,
@@ -467,4 +474,4 @@ export class CooldownManager {
467
474
  this.sessionManager.touch(groupSessionId);
468
475
  this.startCooldownTimer(groupSessionId, groupId, selfId);
469
476
  }
470
- }
477
+ }
@@ -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";
@@ -30,7 +30,8 @@ export class IdleCheckManager {
30
30
  private intervalHandle: NodeJS.Timeout | null = null;
31
31
 
32
32
  private ctx: MiokiContext;
33
- private cfg: ChatConfig;
33
+ private configProvider: ChatConfigProvider;
34
+ private defaultConfig: ChatConfig;
34
35
  private db: ChatDatabase;
35
36
  private humanize: HumanizeEngine;
36
37
  private aiInstance: AIInstance;
@@ -50,7 +51,8 @@ export class IdleCheckManager {
50
51
 
51
52
  constructor(pluginCtx: ChatPluginContext) {
52
53
  this.ctx = pluginCtx.ctx;
53
- this.cfg = pluginCtx.config;
54
+ this.configProvider = pluginCtx.configProvider;
55
+ this.defaultConfig = pluginCtx.defaultConfig;
54
56
  this.db = pluginCtx.db;
55
57
  this.humanize = pluginCtx.humanize;
56
58
  this.aiInstance = pluginCtx.aiInstance;
@@ -106,24 +108,27 @@ export class IdleCheckManager {
106
108
 
107
109
  private async tick(): Promise<void> {
108
110
  try {
109
- const cfg = this.cfg;
110
- if (!cfg.apiKey || !cfg.planner?.enabled) return;
111
+ const baseCfg = this.defaultConfig;
112
+ if (!baseCfg.apiKey) return;
111
113
 
112
114
  const now = Date.now();
113
- const idleThreshold = cfg.planner.idleThresholdMs ?? 30 * 60_000;
114
- const messageCountThreshold = cfg.planner.idleMessageCount ?? 100;
115
115
  const checkInterval = 60_000;
116
116
  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
117
 
120
118
  for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
121
119
  const lastCheckTime = this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
122
120
  if (now - lastCheckTime < checkInterval) continue;
123
121
 
124
122
  const groupId = parseInt(groupSessionId.split(":")[1], 10);
123
+ const cfg = this.configProvider(groupId);
124
+ if (!cfg.planner?.enabled) continue;
125
125
  if (!isGroupAllowed(groupId, cfg)) continue;
126
126
 
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));
131
+
127
132
  let lastBotTime = this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
128
133
  if (lastBotTime === 0) {
129
134
  const botMsgs = this.db.getBotMessages(groupId, 1);