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.
@@ -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";
@@ -35,7 +35,8 @@ export class QueueProcessor {
35
35
  private dynamicDelayQueues = new Map<string, DynamicDelayQueueData>();
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;
@@ -60,7 +61,8 @@ export class QueueProcessor {
60
61
 
61
62
  constructor(pluginCtx: ChatPluginContext) {
62
63
  this.ctx = pluginCtx.ctx;
63
- this.cfg = pluginCtx.config;
64
+ this.configProvider = pluginCtx.configProvider;
65
+ this.defaultConfig = pluginCtx.defaultConfig;
64
66
  this.db = pluginCtx.db;
65
67
  this.humanize = pluginCtx.humanize;
66
68
  this.aiInstance = pluginCtx.aiInstance;
@@ -147,14 +149,15 @@ export class QueueProcessor {
147
149
  content: mergedContent, messageId: firstMsg.messageId, timestamp: Date.now(),
148
150
  };
149
151
 
152
+ const cfg = this.configProvider(groupId);
150
153
  const botRole = await getBotRole(groupId, this.ctx, selfId);
151
- const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
154
+ const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
152
155
  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));
156
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
154
157
 
155
158
  const toolCtx = this.buildToolContext({
156
159
  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,
160
+ config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
158
161
  });
159
162
 
160
163
  this.sessionManager.getOrCreate(groupSessionId, "group", groupId);
@@ -162,18 +165,18 @@ export class QueueProcessor {
162
165
 
163
166
  const result = await this.runWithRateLimitGuard(
164
167
  () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
165
- config: this.cfg, groupName, memberCount, botNickname, botRole, aiService: this.aiService, isGroup: true,
168
+ config: cfg, groupName, memberCount, botNickname, botRole, aiService: this.aiService, isGroup: true,
166
169
  memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
167
170
  replyContext: { type: "review", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
168
171
  reviewMessages: { contents: mergedContents, userNames, messageIds },
169
172
  }, this.humanize, this.skillManager, {
170
- manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
173
+ manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
171
174
  }),
172
175
  { userId: targetMessage.userId, groupId, label: "dynamic-delay" },
173
176
  );
174
177
  if (!result) return;
175
178
 
176
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
179
+ await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
177
180
  this.startCooldownTimer(groupSessionId, groupId, selfId);
178
181
  }
179
182
 
@@ -191,8 +194,10 @@ export class QueueProcessor {
191
194
  const queuedContents: string[] = [];
192
195
  const structuredUserInputs: StructuredUserInput[] = [];
193
196
 
197
+ const cfg = this.configProvider(groupId);
198
+
194
199
  for (const item of queue) {
195
- const { text: extractedText, multimodal } = extractContent(item.event, this.cfg, this.ctx);
200
+ const { text: extractedText, multimodal } = extractContent(item.event, cfg, this.ctx);
196
201
  const content = multimodal ? JSON.stringify(multimodal) : extractedText;
197
202
  if (content) {
198
203
  queuedContents.push(content);
@@ -219,32 +224,32 @@ export class QueueProcessor {
219
224
  };
220
225
 
221
226
  const botRole = await getBotRole(groupId, this.ctx, selfId);
222
- const botNickname = this.cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
227
+ const botNickname = cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
223
228
  const toolCtx = this.buildToolContext({
224
229
  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,
230
+ config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
226
231
  });
227
232
 
228
- const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, this.cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, this.cfg));
233
+ const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
229
234
  const contexts = await this.getHumanizeContexts(this.humanize, groupSessionId, targetMessage.userName, history, targetMessage.userId);
230
235
  const { groupName, memberCount } = await this.getGroupInfoData(this.ctx, groupId, selfId);
231
236
 
232
237
  const result = await this.runWithRateLimitGuard(
233
238
  () => this.runChat(this.aiInstance, toolCtx, history, targetMessage, {
234
- config: this.cfg, groupName, memberCount, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
239
+ config: cfg, groupName, memberCount, botNickname, botRole: toolCtx.botRole, aiService: this.aiService, isGroup: true,
235
240
  memoryContext: contexts.memoryContext, topicContext: contexts.topicContext, expressionContext: contexts.expressionContext,
236
241
  replyContext: { type: "comment", targetUser: targetMessage.userName, targetMessage: targetMessage.content },
237
242
  }, this.humanize, this.skillManager, {
238
- manager: this.groupStructuredHistory, ttlMs: this.cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
243
+ manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
239
244
  }),
240
245
  { userId: targetMessage.userId, groupId, label: "queue" },
241
246
  );
242
247
  if (!result) return;
243
248
 
244
- await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: this.cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
249
+ await this.sendAIResponse({ ctx: this.ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices }, selfId);
245
250
  await this.sendEmoji(this.ctx, groupId, result.emojiPath, selfId);
246
251
  const now = Date.now();
247
- this.saveBotMessages(groupId, groupSessionId, result.messages, now, this.cfg, this.db, this.ctx, selfId);
252
+ this.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, this.db, this.ctx, selfId);
248
253
  this.sessionManager.touch(groupSessionId);
249
254
  this.ctx.logger.info(`[Queue] group ${groupSessionId} done`);
250
255
  } catch (err) {
@@ -1,12 +1,24 @@
1
- import type { AIRequestLimitConfig, DynamicDelayConfig } from "../types";
1
+ import type { AIRequestLimitConfig, ChatConfig, DynamicDelayConfig } from "../types";
2
+
3
+ const DEFAULT_DYNAMIC_DELAY: DynamicDelayConfig = {
4
+ enabled: true,
5
+ interactionWindowMs: 60_000,
6
+ baseDelayMs: 30_000,
7
+ maxDelayMs: 300_000,
8
+ };
9
+
10
+ const DEFAULT_AI_REQUEST_LIMITS: AIRequestLimitConfig = {
11
+ userRpm: 3,
12
+ groupRpm: 6,
13
+ windowMs: 60_000,
14
+ };
15
+
16
+ export type ChatConfigProvider = (groupId?: number) => ChatConfig;
2
17
 
3
18
  export class RateLimiter {
4
- // 用户触发记录:userId -> timestamp[]
5
19
  private userTriggers: Map<number, number[]> = new Map();
6
- // 用户最近消息:userId -> {content, timestamp}[]
7
20
  private userMessages: Map<number, { content: string; timestamp: number }[]> =
8
21
  new Map();
9
- // 群组最后响应时间:groupId -> timestamp
10
22
  private groupLastResponse: Map<number, number> = new Map();
11
23
  private groupInteractions: Map<number, Map<number, number[]>> = new Map();
12
24
  private userAiRequests: Map<number, number[]> = new Map();
@@ -17,14 +29,13 @@ export class RateLimiter {
17
29
  private readonly dedupWindowMs: number;
18
30
  private readonly groupCooldownMs: number;
19
31
  private readonly cleanupTimer: ReturnType<typeof setInterval>;
20
- private readonly dynamicDelayConfig: DynamicDelayConfig;
21
- private readonly aiRequestLimitConfig: AIRequestLimitConfig;
22
- // 外部注入的队列长度获取函数
32
+ private getConfig: ChatConfigProvider;
23
33
  private getQueueLengthFn: ((groupId: number) => number) | null = null;
24
34
 
25
- /**
26
- * 设置队列长度获取函数
27
- */
35
+ setConfigProvider(provider: ChatConfigProvider): void {
36
+ this.getConfig = provider;
37
+ }
38
+
28
39
  setQueueLengthGetter(fn: (groupId: number) => number): void {
29
40
  this.getQueueLengthFn = fn;
30
41
  }
@@ -34,28 +45,27 @@ export class RateLimiter {
34
45
  windowMs?: number;
35
46
  dedupWindowMs?: number;
36
47
  groupCooldownMs?: number;
37
- dynamicDelay?: DynamicDelayConfig;
38
- aiRequestLimits?: AIRequestLimitConfig;
39
48
  }) {
40
49
  this.maxTriggersPerWindow = options?.maxTriggersPerWindow ?? 5;
41
50
  this.windowMs = options?.windowMs ?? 60_000;
42
51
  this.dedupWindowMs = options?.dedupWindowMs ?? 30_000;
43
52
  this.groupCooldownMs = options?.groupCooldownMs ?? 1_000;
44
- this.dynamicDelayConfig = options?.dynamicDelay ?? {
45
- enabled: true,
46
- interactionWindowMs: 60_000,
47
- baseDelayMs: 30_000,
48
- maxDelayMs: 300_000,
49
- };
50
- this.aiRequestLimitConfig = options?.aiRequestLimits ?? {
51
- userRpm: 3,
52
- groupRpm: 6,
53
- windowMs: 60_000,
54
- };
53
+ this.getConfig = () => ({
54
+ dynamicDelay: DEFAULT_DYNAMIC_DELAY,
55
+ aiRequestLimits: DEFAULT_AI_REQUEST_LIMITS,
56
+ }) as unknown as ChatConfig;
55
57
 
56
58
  this.cleanupTimer = setInterval(() => this.cleanup(), 300_000);
57
59
  }
58
60
 
61
+ private getDynamicDelay(groupId?: number): DynamicDelayConfig {
62
+ return this.getConfig(groupId)?.dynamicDelay ?? DEFAULT_DYNAMIC_DELAY;
63
+ }
64
+
65
+ private getAiRequestLimits(groupId?: number): AIRequestLimitConfig {
66
+ return this.getConfig(groupId)?.aiRequestLimits ?? DEFAULT_AI_REQUEST_LIMITS;
67
+ }
68
+
59
69
  canProcess(
60
70
  userId: number,
61
71
  groupId: number | undefined,
@@ -63,7 +73,6 @@ export class RateLimiter {
63
73
  ): boolean {
64
74
  const now = Date.now();
65
75
 
66
- // 群组冷却检查
67
76
  if (groupId) {
68
77
  const lastResponse = this.groupLastResponse.get(groupId);
69
78
  if (lastResponse && now - lastResponse < this.groupCooldownMs) {
@@ -71,14 +80,12 @@ export class RateLimiter {
71
80
  }
72
81
  }
73
82
 
74
- // 用户频率检查
75
83
  const triggers = this.userTriggers.get(userId) ?? [];
76
84
  const recentTriggers = triggers.filter((t) => now - t < this.windowMs);
77
85
  if (recentTriggers.length >= this.maxTriggersPerWindow) {
78
86
  return false;
79
87
  }
80
88
 
81
- // 重复消息检查
82
89
  const messages = this.userMessages.get(userId) ?? [];
83
90
  const recentSame = messages.find(
84
91
  (m) => m.content === content && now - m.timestamp < this.dedupWindowMs,
@@ -86,34 +93,29 @@ export class RateLimiter {
86
93
  return !recentSame;
87
94
  }
88
95
 
89
- /**
90
- * 记录已处理的消息
91
- */
92
96
  record(userId: number, groupId: number | undefined, content: string): void {
93
97
  const now = Date.now();
94
98
 
95
- // 记录触发
96
99
  const triggers = this.userTriggers.get(userId) ?? [];
97
100
  triggers.push(now);
98
101
  this.userTriggers.set(userId, triggers);
99
102
 
100
- // 记录消息(只保留最近 3 条)
101
103
  const messages = this.userMessages.get(userId) ?? [];
102
104
  messages.push({ content, timestamp: now });
103
105
  if (messages.length > 3) messages.shift();
104
106
  this.userMessages.set(userId, messages);
105
107
 
106
- // 记录群组响应
107
108
  if (groupId) {
108
109
  this.groupLastResponse.set(groupId, now);
109
110
  }
110
111
  }
111
112
 
112
113
  recordInteraction(groupId: number, userId: number): void {
113
- if (!this.dynamicDelayConfig.enabled) return;
114
+ const dynamicDelay = this.getDynamicDelay(groupId);
115
+ if (!dynamicDelay.enabled) return;
114
116
 
115
117
  const now = Date.now();
116
- const windowMs = this.dynamicDelayConfig.interactionWindowMs;
118
+ const windowMs = dynamicDelay.interactionWindowMs;
117
119
 
118
120
  let groupUsers = this.groupInteractions.get(groupId);
119
121
  if (!groupUsers) {
@@ -129,7 +131,7 @@ export class RateLimiter {
129
131
 
130
132
  canRunAIRequest(userId?: number, groupId?: number): boolean {
131
133
  const now = Date.now();
132
- const { userRpm, groupRpm, windowMs } = this.aiRequestLimitConfig;
134
+ const { userRpm, groupRpm, windowMs } = this.getAiRequestLimits(groupId);
133
135
 
134
136
  if (typeof userId === "number") {
135
137
  const userRequests = (this.userAiRequests.get(userId) ?? []).filter(
@@ -154,7 +156,7 @@ export class RateLimiter {
154
156
 
155
157
  recordAIRequest(userId?: number, groupId?: number): void {
156
158
  const now = Date.now();
157
- const windowMs = this.aiRequestLimitConfig.windowMs;
159
+ const { windowMs } = this.getAiRequestLimits(groupId);
158
160
 
159
161
  if (typeof userId === "number") {
160
162
  const userRequests = (this.userAiRequests.get(userId) ?? []).filter(
@@ -175,9 +177,8 @@ export class RateLimiter {
175
177
 
176
178
  getInteractionCount(groupId: number): number {
177
179
  const now = Date.now();
178
- const windowMs = this.dynamicDelayConfig.interactionWindowMs;
180
+ const { interactionWindowMs: windowMs } = this.getDynamicDelay(groupId);
179
181
 
180
- // 优先使用队列长度作为互动人数计算
181
182
  if (this.getQueueLengthFn) {
182
183
  const queueLength = this.getQueueLengthFn(groupId);
183
184
  if (queueLength > 0) {
@@ -185,7 +186,6 @@ export class RateLimiter {
185
186
  }
186
187
  }
187
188
 
188
- // 回退到 groupInteractions 统计
189
189
  const groupUsers = this.groupInteractions.get(groupId);
190
190
  if (!groupUsers) return 0;
191
191
 
@@ -200,12 +200,13 @@ export class RateLimiter {
200
200
  }
201
201
 
202
202
  calculateDelay(groupId: number): number {
203
- if (!this.dynamicDelayConfig.enabled) return 0;
203
+ const dynamicDelay = this.getDynamicDelay(groupId);
204
+ if (!dynamicDelay.enabled) return 0;
204
205
 
205
206
  const interactionCount = this.getInteractionCount(groupId);
206
207
  if (interactionCount <= 1) return 0;
207
208
 
208
- const { baseDelayMs, maxDelayMs } = this.dynamicDelayConfig;
209
+ const { baseDelayMs, maxDelayMs } = dynamicDelay;
209
210
  const delay = (interactionCount - 1) * baseDelayMs;
210
211
  return Math.min(delay, maxDelayMs);
211
212
  }
@@ -257,46 +258,21 @@ export class RateLimiter {
257
258
  }
258
259
  }
259
260
 
260
- const aiWindowMs = this.aiRequestLimitConfig.windowMs;
261
- for (const [userId, requests] of this.userAiRequests) {
262
- const valid = requests.filter(
263
- (timestamp) => now - timestamp < aiWindowMs,
264
- );
265
- if (valid.length === 0) {
266
- this.userAiRequests.delete(userId);
267
- } else {
268
- this.userAiRequests.set(userId, valid);
269
- }
270
- }
271
-
272
- for (const [groupId, requests] of this.groupAiRequests) {
273
- const valid = requests.filter(
274
- (timestamp) => now - timestamp < aiWindowMs,
275
- );
276
- if (valid.length === 0) {
277
- this.groupAiRequests.delete(groupId);
278
- } else {
279
- this.groupAiRequests.set(groupId, valid);
280
- }
281
- }
282
-
283
- if (this.dynamicDelayConfig.enabled) {
284
- const windowMs = this.dynamicDelayConfig.interactionWindowMs;
285
- for (const [groupId, groupUsers] of this.groupInteractions) {
286
- let hasActiveUser = false;
287
- for (const [userId, timestamps] of groupUsers) {
288
- const valid = timestamps.filter((t) => now - t < windowMs);
289
- if (valid.length === 0) {
290
- groupUsers.delete(userId);
291
- } else {
292
- groupUsers.set(userId, valid);
293
- hasActiveUser = true;
294
- }
295
- }
296
- if (!hasActiveUser) {
297
- this.groupInteractions.delete(groupId);
261
+ for (const [groupId, groupUsers] of this.groupInteractions) {
262
+ const windowMs = this.getDynamicDelay(groupId).interactionWindowMs;
263
+ let hasActiveUser = false;
264
+ for (const [userId, timestamps] of groupUsers) {
265
+ const valid = timestamps.filter((t) => now - t < windowMs);
266
+ if (valid.length === 0) {
267
+ groupUsers.delete(userId);
268
+ } else {
269
+ groupUsers.set(userId, valid);
270
+ hasActiveUser = true;
298
271
  }
299
272
  }
273
+ if (!hasActiveUser) {
274
+ this.groupInteractions.delete(groupId);
275
+ }
300
276
  }
301
277
  }
302
278
 
@@ -309,4 +285,4 @@ export class RateLimiter {
309
285
  this.userAiRequests.clear();
310
286
  this.groupAiRequests.clear();
311
287
  }
312
- }
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -12,7 +12,7 @@ import { executeChatRuntimeRequest } from "../core/chat-turn";
12
12
 
13
13
  export function createChatRuntime(
14
14
  pluginCtx: ChatPluginContext,
15
- getConfig: () => Promise<ChatConfig>,
15
+ getConfig: (groupId?: number) => Promise<ChatConfig>,
16
16
  ): ChatRuntime {
17
17
  return {
18
18
  async requestInformation(
package/types.ts CHANGED
@@ -219,6 +219,29 @@ export interface ChatConfig {
219
219
  retention: RetentionConfig;
220
220
  }
221
221
 
222
+ /**
223
+ * 每群单独的开关覆盖
224
+ * 不填写的开关回退到全局默认;私聊会话不使用此覆盖
225
+ */
226
+ export interface ChatGroupOverrides {
227
+ emoji?: { enabled?: boolean };
228
+ expression?: { enabled?: boolean };
229
+ retention?: { enabled?: boolean };
230
+ memory?: { enabled?: boolean };
231
+ topic?: { enabled?: boolean };
232
+ planner?: { enabled?: boolean };
233
+ audio?: { enabled?: boolean };
234
+ searxng?: { enabled?: boolean };
235
+ webReader?: { enabled?: boolean };
236
+ dynamicDelay?: { enabled?: boolean };
237
+ enableMarkdownScreenshot?: boolean;
238
+ enableMediaRecognition?: boolean;
239
+ }
240
+
241
+ export type ChatGroupsFile = {
242
+ groups: Record<string, ChatGroupOverrides>;
243
+ };
244
+
222
245
  /**
223
246
  * 会话类型
224
247
  */
@@ -0,0 +1,70 @@
1
+ import type { ChatConfig, ChatGroupOverrides } from "../types";
2
+
3
+ const OVERRIDABLE_SUB_OBJECTS = [
4
+ "emoji",
5
+ "expression",
6
+ "retention",
7
+ "memory",
8
+ "topic",
9
+ "planner",
10
+ "audio",
11
+ "searxng",
12
+ "webReader",
13
+ "dynamicDelay",
14
+ ] as const;
15
+
16
+ const OVERRIDABLE_BOOLEAN_FIELDS = [
17
+ "enableMarkdownScreenshot",
18
+ "enableMediaRecognition",
19
+ ] as const;
20
+
21
+ /**
22
+ * 把每个群的覆盖配置浅合并到全局配置上
23
+ * - 子对象(如 emoji)只覆盖其中存在的字段,未填字段继承全局
24
+ * - 顶层布尔字段(enableMarkdownScreenshot 等)若覆盖中存在则整体替换
25
+ * - 缺失字段、格式异常的覆盖会被静默忽略
26
+ */
27
+ export function mergeGroupOverrides(
28
+ base: ChatConfig,
29
+ overrides: ChatGroupOverrides | undefined | null,
30
+ ): ChatConfig {
31
+ if (!overrides || typeof overrides !== "object") return base;
32
+ if (typeof base !== "object" || base === null) return base;
33
+
34
+ const result: ChatConfig = { ...base };
35
+ const resultMap = result as unknown as Record<string, unknown>;
36
+ const overridesMap = overrides as Record<string, unknown>;
37
+
38
+ for (const key of OVERRIDABLE_SUB_OBJECTS) {
39
+ const overrideSub = overridesMap[key];
40
+ if (overrideSub && typeof overrideSub === "object") {
41
+ const baseSub = (base as unknown as Record<string, unknown>)[key];
42
+ if (baseSub && typeof baseSub === "object") {
43
+ resultMap[key] = {
44
+ ...(baseSub as Record<string, unknown>),
45
+ ...(overrideSub as Record<string, unknown>),
46
+ };
47
+ }
48
+ }
49
+ }
50
+
51
+ for (const key of OVERRIDABLE_BOOLEAN_FIELDS) {
52
+ const value = overridesMap[key];
53
+ if (typeof value === "boolean") {
54
+ resultMap[key] = value;
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * 从 sessionId(如 "group:123")提取群号,个人会话返回 undefined
63
+ */
64
+ export function extractGroupIdFromSession(sessionId: string): number | undefined {
65
+ if (!sessionId || typeof sessionId !== "string") return undefined;
66
+ if (!sessionId.startsWith("group:")) return undefined;
67
+ const raw = sessionId.slice("group:".length);
68
+ const parsed = parseInt(raw, 10);
69
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
70
+ }