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/configs/groups.ts +5 -0
- package/context.ts +5 -3
- package/core/chat-turn.ts +2 -1
- package/core/tools/web.ts +1 -1
- package/handlers/message.ts +3 -3
- package/handlers/poke.ts +2 -2
- package/humanize/emoji-agent.ts +20 -7
- package/humanize/emotion-agent.ts +33 -26
- package/humanize/expression.ts +24 -16
- package/humanize/index.ts +10 -8
- package/humanize/memory.ts +10 -7
- package/humanize/planner.ts +11 -16
- package/humanize/topic.ts +22 -15
- package/index.ts +115 -28
- package/manage/cooldown.ts +28 -21
- package/manage/idle-check.ts +14 -9
- package/manage/queue-processor.ts +22 -17
- package/manage/rate-limiter.ts +56 -80
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +1 -1
- package/types.ts +23 -0
- package/utils/group-config.ts +70 -0
|
@@ -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
|
|
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.
|
|
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 =
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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 =
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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) {
|
package/manage/rate-limiter.ts
CHANGED
|
@@ -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
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
114
|
+
const dynamicDelay = this.getDynamicDelay(groupId);
|
|
115
|
+
if (!dynamicDelay.enabled) return;
|
|
114
116
|
|
|
115
117
|
const now = Date.now();
|
|
116
|
-
const windowMs =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 } =
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
package/runtime/chat-runtime.ts
CHANGED
|
@@ -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
|
+
}
|