mioku-plugin-chat 2.6.0 → 2.7.1
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/personalization.ts +1 -1
- package/context.ts +2 -0
- package/core/base.ts +1 -1
- package/core/chat-engine.ts +18 -7
- package/core/chat-turn.ts +26 -9
- package/core/media/image-analyzer.ts +63 -397
- package/core/prompt.ts +11 -20
- package/db.ts +8 -12
- package/handlers/idle-debug.ts +96 -97
- package/handlers/message.ts +35 -34
- package/handlers/poke.ts +102 -124
- package/humanize/emoji-agent.ts +208 -332
- package/humanize/index.ts +2 -3
- package/index.ts +32 -44
- package/manage/cooldown.ts +60 -100
- package/manage/idle-check.ts +163 -70
- package/manage/queue-processor.ts +73 -30
- package/manage/session-turn-scheduler.ts +130 -0
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +14 -2
- package/types.ts +3 -19
- package/utils/config.ts +57 -0
- package/utils/group-config.ts +9 -0
- package/utils/message.ts +4 -7
- package/utils/queue.ts +5 -0
package/manage/idle-check.ts
CHANGED
|
@@ -11,20 +11,19 @@ import type {
|
|
|
11
11
|
HistoryMediaOptions,
|
|
12
12
|
GetGroupHistoryMessages,
|
|
13
13
|
GetGroupInfoData,
|
|
14
|
-
SendAIResponse,
|
|
15
|
-
SaveBotMessages,
|
|
16
14
|
RunChat,
|
|
17
15
|
BuildToolContext,
|
|
18
16
|
} from "./types";
|
|
19
17
|
import type { ChatPluginContext } from "../context";
|
|
18
|
+
import { finalizeChatTurn } from "../core/chat-turn";
|
|
20
19
|
import { getBotRole, isGroupAllowed } from "../utils";
|
|
21
20
|
|
|
22
21
|
export class IdleCheckManager {
|
|
22
|
+
private pluginCtx: ChatPluginContext;
|
|
23
23
|
private groupLastActivityTime = new Map<string, number>();
|
|
24
24
|
private groupMessageCount = new Map<string, number>();
|
|
25
25
|
private groupLastBotMessageTime = new Map<string, number>();
|
|
26
26
|
private groupMessageCountAfterBot = new Map<string, number>();
|
|
27
|
-
private idleCheckProcessing = new Set<string>();
|
|
28
27
|
private groupLastIdleCheckTime = new Map<string, number>();
|
|
29
28
|
private groupBotsMapping = new Map<string, Set<number>>();
|
|
30
29
|
private intervalHandle: NodeJS.Timeout | null = null;
|
|
@@ -43,13 +42,11 @@ export class IdleCheckManager {
|
|
|
43
42
|
private buildHistoryMediaOptions: (ai: AIInstance, cfg: ChatConfig) => HistoryMediaOptions;
|
|
44
43
|
private getGroupHistoryMessages: GetGroupHistoryMessages;
|
|
45
44
|
private getGroupInfoData: GetGroupInfoData;
|
|
46
|
-
private sendAIResponse: SendAIResponse;
|
|
47
|
-
private saveBotMessages: SaveBotMessages;
|
|
48
45
|
private runChat: RunChat;
|
|
49
46
|
private buildToolContext: BuildToolContext;
|
|
50
|
-
private startCooldownTimer: (groupSessionId: string, groupId: number, selfId: number) => void;
|
|
51
47
|
|
|
52
48
|
constructor(pluginCtx: ChatPluginContext) {
|
|
49
|
+
this.pluginCtx = pluginCtx;
|
|
53
50
|
this.ctx = pluginCtx.ctx;
|
|
54
51
|
this.configProvider = pluginCtx.configProvider;
|
|
55
52
|
this.defaultConfig = pluginCtx.defaultConfig;
|
|
@@ -64,11 +61,8 @@ export class IdleCheckManager {
|
|
|
64
61
|
this.buildHistoryMediaOptions = pluginCtx.buildHistoryMediaOptions;
|
|
65
62
|
this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
|
|
66
63
|
this.getGroupInfoData = pluginCtx.getGroupInfoData;
|
|
67
|
-
this.sendAIResponse = pluginCtx.sendAIResponse;
|
|
68
|
-
this.saveBotMessages = pluginCtx.saveBotMessages;
|
|
69
64
|
this.runChat = pluginCtx.runChat;
|
|
70
65
|
this.buildToolContext = pluginCtx.buildToolContext;
|
|
71
|
-
this.startCooldownTimer = pluginCtx.startCooldownTimer;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
recordActivity(groupSessionId: string): void {
|
|
@@ -101,7 +95,6 @@ export class IdleCheckManager {
|
|
|
101
95
|
this.groupMessageCount.clear();
|
|
102
96
|
this.groupLastBotMessageTime.clear();
|
|
103
97
|
this.groupMessageCountAfterBot.clear();
|
|
104
|
-
this.idleCheckProcessing.clear();
|
|
105
98
|
this.groupLastIdleCheckTime.clear();
|
|
106
99
|
this.groupBotsMapping.clear();
|
|
107
100
|
}
|
|
@@ -116,7 +109,8 @@ export class IdleCheckManager {
|
|
|
116
109
|
const allBotIds = Array.from(this.ctx.bots).map((bot) => bot.uin);
|
|
117
110
|
|
|
118
111
|
for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
|
|
119
|
-
const lastCheckTime =
|
|
112
|
+
const lastCheckTime =
|
|
113
|
+
this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
|
|
120
114
|
if (now - lastCheckTime < checkInterval) continue;
|
|
121
115
|
|
|
122
116
|
const groupId = parseInt(groupSessionId.split(":")[1], 10);
|
|
@@ -124,12 +118,24 @@ export class IdleCheckManager {
|
|
|
124
118
|
if (!cfg.planner?.enabled) continue;
|
|
125
119
|
if (!isGroupAllowed(groupId, cfg)) continue;
|
|
126
120
|
|
|
127
|
-
const idleThreshold =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
const idleThreshold =
|
|
122
|
+
cfg.planner.idleThresholdMs ??
|
|
123
|
+
baseCfg.planner?.idleThresholdMs ??
|
|
124
|
+
30 * 60_000;
|
|
125
|
+
const messageCountThreshold =
|
|
126
|
+
cfg.planner.idleMessageCount ??
|
|
127
|
+
baseCfg.planner?.idleMessageCount ??
|
|
128
|
+
100;
|
|
129
|
+
const idleCheckBotIds =
|
|
130
|
+
cfg.planner.idleCheckBotIds ??
|
|
131
|
+
baseCfg.planner?.idleCheckBotIds ??
|
|
132
|
+
allBotIds;
|
|
133
|
+
const enabledBotIds = idleCheckBotIds.filter((id: number) =>
|
|
134
|
+
allBotIds.includes(id),
|
|
135
|
+
);
|
|
131
136
|
|
|
132
|
-
let lastBotTime =
|
|
137
|
+
let lastBotTime =
|
|
138
|
+
this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
|
|
133
139
|
if (lastBotTime === 0) {
|
|
134
140
|
const botMsgs = this.db.getBotMessages(groupId, 1);
|
|
135
141
|
if (botMsgs.length > 0) {
|
|
@@ -141,71 +147,158 @@ export class IdleCheckManager {
|
|
|
141
147
|
const lastActivityTime = Math.max(lastTime, lastBotTime);
|
|
142
148
|
if (now - lastActivityTime < idleThreshold) continue;
|
|
143
149
|
|
|
144
|
-
const messageCountAfterBot =
|
|
145
|
-
|
|
150
|
+
const messageCountAfterBot =
|
|
151
|
+
this.groupMessageCountAfterBot.get(groupSessionId) ?? 0;
|
|
152
|
+
const messageCount =
|
|
153
|
+
lastBotTime > 0
|
|
154
|
+
? messageCountAfterBot
|
|
155
|
+
: (this.groupMessageCount.get(groupSessionId) ?? 0);
|
|
146
156
|
if (messageCount < messageCountThreshold) continue;
|
|
147
157
|
|
|
148
158
|
const botsInGroup = this.groupBotsMapping.get(groupSessionId);
|
|
149
159
|
if (!botsInGroup || botsInGroup.size === 0) continue;
|
|
150
|
-
const availableBots = Array.from(botsInGroup).filter((id) =>
|
|
160
|
+
const availableBots = Array.from(botsInGroup).filter((id) =>
|
|
161
|
+
enabledBotIds.includes(id),
|
|
162
|
+
);
|
|
151
163
|
if (availableBots.length === 0) continue;
|
|
152
164
|
|
|
153
|
-
const selfId =
|
|
154
|
-
|
|
165
|
+
const selfId =
|
|
166
|
+
availableBots[Math.floor(Math.random() * availableBots.length)];
|
|
167
|
+
this.groupLastIdleCheckTime.set(groupSessionId, now);
|
|
168
|
+
void this.pluginCtx.sessionTurnScheduler
|
|
169
|
+
.run(
|
|
170
|
+
groupSessionId,
|
|
171
|
+
"idle-check",
|
|
172
|
+
() =>
|
|
173
|
+
this.processIdleCheckTurn(
|
|
174
|
+
groupSessionId,
|
|
175
|
+
groupId,
|
|
176
|
+
selfId,
|
|
177
|
+
cfg,
|
|
178
|
+
lastActivityTime,
|
|
179
|
+
now,
|
|
180
|
+
),
|
|
181
|
+
{ dedupeKey: "idle-check" },
|
|
182
|
+
)
|
|
183
|
+
.catch((err) =>
|
|
184
|
+
this.ctx.logger.error(
|
|
185
|
+
`[IdleCheck] group ${groupId} idle detection failed: ${err}`,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.ctx.logger.error(`[IdleCheck] tick failed: ${err}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
155
193
|
|
|
156
|
-
|
|
157
|
-
|
|
194
|
+
private async processIdleCheckTurn(
|
|
195
|
+
groupSessionId: string,
|
|
196
|
+
groupId: number,
|
|
197
|
+
selfId: number,
|
|
198
|
+
cfg: ChatConfig,
|
|
199
|
+
scheduledLastActivity: number,
|
|
200
|
+
scheduledAt: number,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
const latestActivity = Math.max(
|
|
203
|
+
this.groupLastActivityTime.get(groupSessionId) ?? 0,
|
|
204
|
+
this.groupLastBotMessageTime.get(groupSessionId) ?? 0,
|
|
205
|
+
);
|
|
206
|
+
if (latestActivity > scheduledLastActivity) return;
|
|
158
207
|
|
|
159
|
-
|
|
160
|
-
|
|
208
|
+
this.ctx.logger.info(`[IdleCheck] group ${groupId} triggers idle detection`);
|
|
209
|
+
const { history } = await this.getGroupHistoryMessages(
|
|
210
|
+
groupId,
|
|
211
|
+
groupSessionId,
|
|
212
|
+
this.ctx,
|
|
213
|
+
cfg.historyCount,
|
|
214
|
+
this.db,
|
|
215
|
+
selfId,
|
|
216
|
+
this.buildHistoryMediaOptions(this.aiInstance, cfg),
|
|
217
|
+
);
|
|
218
|
+
const botNickname =
|
|
219
|
+
cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
|
|
220
|
+
const planResult = await this.humanize.actionPlanner.plan(
|
|
221
|
+
groupSessionId,
|
|
222
|
+
botNickname,
|
|
223
|
+
history,
|
|
224
|
+
"[Check if you want to answer the call]",
|
|
225
|
+
true,
|
|
226
|
+
);
|
|
161
227
|
|
|
162
|
-
|
|
228
|
+
if (planResult.action !== "reply") {
|
|
229
|
+
this.groupMessageCount.set(groupSessionId, 0);
|
|
230
|
+
this.groupMessageCountAfterBot.set(groupSessionId, 0);
|
|
231
|
+
this.groupLastIdleCheckTime.set(groupSessionId, scheduledAt);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
163
234
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
235
|
+
const targetMessage: TargetMessage = {
|
|
236
|
+
userName: "system",
|
|
237
|
+
userId: 0,
|
|
238
|
+
userRole: "member",
|
|
239
|
+
content: "[No one in the group is talking? I'll answer!]",
|
|
240
|
+
messageId: 0,
|
|
241
|
+
timestamp: scheduledAt,
|
|
242
|
+
};
|
|
243
|
+
const botRole = await getBotRole(groupId, this.ctx, selfId);
|
|
244
|
+
const toolCtx = this.buildToolContext({
|
|
245
|
+
ctx: this.ctx,
|
|
246
|
+
event: null,
|
|
247
|
+
groupSessionId,
|
|
248
|
+
groupId,
|
|
249
|
+
userId: 0,
|
|
250
|
+
config: cfg,
|
|
251
|
+
aiService: this.aiService,
|
|
252
|
+
db: this.db,
|
|
253
|
+
botRole,
|
|
254
|
+
humanize: this.humanize,
|
|
255
|
+
targetMessage,
|
|
256
|
+
selfId,
|
|
257
|
+
});
|
|
170
258
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
config: cfg,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
259
|
+
const result = await this.runWithRateLimitGuard(
|
|
260
|
+
() =>
|
|
261
|
+
this.runChat(
|
|
262
|
+
this.aiInstance,
|
|
263
|
+
toolCtx,
|
|
264
|
+
history,
|
|
265
|
+
targetMessage,
|
|
266
|
+
{
|
|
267
|
+
config: cfg,
|
|
268
|
+
botNickname,
|
|
269
|
+
botRole: toolCtx.botRole,
|
|
270
|
+
aiService: this.aiService,
|
|
271
|
+
isGroup: true,
|
|
272
|
+
plannerThoughts:
|
|
273
|
+
'You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [reply:message ID] format).\nDon\'t mention your intentions like "I\'m here to answer".',
|
|
274
|
+
replyContext: { type: "idle" },
|
|
275
|
+
},
|
|
276
|
+
this.humanize,
|
|
277
|
+
this.skillManager,
|
|
278
|
+
),
|
|
279
|
+
{ groupId, label: "idle-check" },
|
|
280
|
+
);
|
|
281
|
+
if (!result) {
|
|
282
|
+
this.groupMessageCount.set(groupSessionId, 0);
|
|
283
|
+
this.groupMessageCountAfterBot.set(groupSessionId, 0);
|
|
284
|
+
this.groupLastIdleCheckTime.set(groupSessionId, scheduledAt);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
196
287
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
288
|
+
await finalizeChatTurn(this.pluginCtx, {
|
|
289
|
+
event: null,
|
|
290
|
+
cfg,
|
|
291
|
+
result,
|
|
292
|
+
groupId,
|
|
293
|
+
groupSessionId,
|
|
294
|
+
userId: 0,
|
|
295
|
+
selfId,
|
|
296
|
+
toolCtx,
|
|
297
|
+
send: true,
|
|
298
|
+
isLive: true,
|
|
299
|
+
});
|
|
300
|
+
this.groupLastBotMessageTime.set(groupSessionId, Date.now());
|
|
301
|
+
this.groupMessageCountAfterBot.set(groupSessionId, 0);
|
|
302
|
+
this.ctx.logger.info(`[IdleCheck] group ${groupId} idle reply completed`);
|
|
210
303
|
}
|
|
211
304
|
}
|
|
@@ -15,14 +15,13 @@ import type {
|
|
|
15
15
|
GetGroupHistoryMessages,
|
|
16
16
|
GetGroupInfoData,
|
|
17
17
|
GetHumanizeContexts,
|
|
18
|
-
SendAIResponse,
|
|
19
|
-
SaveBotMessages,
|
|
20
|
-
SendEmoji,
|
|
21
18
|
RunChat,
|
|
22
19
|
BuildToolContext,
|
|
23
20
|
BuildStructuredUserInput,
|
|
24
21
|
} from "./types";
|
|
25
22
|
import type { ChatPluginContext } from "../context";
|
|
23
|
+
import type { SessionTurnScheduler } from "./session-turn-scheduler";
|
|
24
|
+
import { finalizeChatTurn } from "../core/chat-turn";
|
|
26
25
|
import { extractContent, getBotRole } from "../utils";
|
|
27
26
|
|
|
28
27
|
interface DynamicDelayQueueData {
|
|
@@ -33,6 +32,7 @@ interface DynamicDelayQueueData {
|
|
|
33
32
|
|
|
34
33
|
export class QueueProcessor {
|
|
35
34
|
private dynamicDelayQueues = new Map<string, DynamicDelayQueueData>();
|
|
35
|
+
private pluginCtx: ChatPluginContext;
|
|
36
36
|
|
|
37
37
|
private ctx: MiokiContext;
|
|
38
38
|
private configProvider: ChatConfigProvider;
|
|
@@ -51,15 +51,13 @@ export class QueueProcessor {
|
|
|
51
51
|
private getGroupHistoryMessages: GetGroupHistoryMessages;
|
|
52
52
|
private getGroupInfoData: GetGroupInfoData;
|
|
53
53
|
private getHumanizeContexts: GetHumanizeContexts;
|
|
54
|
-
private sendAIResponse: SendAIResponse;
|
|
55
|
-
private saveBotMessages: SaveBotMessages;
|
|
56
|
-
private sendEmoji: SendEmoji;
|
|
57
54
|
private runChat: RunChat;
|
|
58
55
|
private buildToolContext: BuildToolContext;
|
|
59
56
|
private buildStructuredUserInputFromEvent: BuildStructuredUserInput;
|
|
60
|
-
private
|
|
57
|
+
private sessionTurnScheduler: SessionTurnScheduler;
|
|
61
58
|
|
|
62
59
|
constructor(pluginCtx: ChatPluginContext) {
|
|
60
|
+
this.pluginCtx = pluginCtx;
|
|
63
61
|
this.ctx = pluginCtx.ctx;
|
|
64
62
|
this.configProvider = pluginCtx.configProvider;
|
|
65
63
|
this.defaultConfig = pluginCtx.defaultConfig;
|
|
@@ -77,13 +75,10 @@ export class QueueProcessor {
|
|
|
77
75
|
this.getGroupHistoryMessages = pluginCtx.getGroupHistoryMessages;
|
|
78
76
|
this.getGroupInfoData = pluginCtx.getGroupInfoData;
|
|
79
77
|
this.getHumanizeContexts = pluginCtx.getHumanizeContexts;
|
|
80
|
-
this.sendAIResponse = pluginCtx.sendAIResponse;
|
|
81
|
-
this.saveBotMessages = pluginCtx.saveBotMessages;
|
|
82
|
-
this.sendEmoji = pluginCtx.sendEmoji;
|
|
83
78
|
this.runChat = pluginCtx.runChat;
|
|
84
79
|
this.buildToolContext = pluginCtx.buildToolContext;
|
|
85
80
|
this.buildStructuredUserInputFromEvent = buildStructuredUserInputFromEvent;
|
|
86
|
-
this.
|
|
81
|
+
this.sessionTurnScheduler = pluginCtx.sessionTurnScheduler;
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
collectDynamicDelayMessage(groupSessionId: string, event: any, content: string): void {
|
|
@@ -107,9 +102,29 @@ export class QueueProcessor {
|
|
|
107
102
|
|
|
108
103
|
this.ctx.logger.info(`[DynamicDelay] group ${groupId} start delay ${delayMs / 1000}s, interactions: ${this.rateLimiter.getInteractionCount(groupId)}`);
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
const current = this.dynamicDelayQueues.get(groupSessionId);
|
|
107
|
+
if (!current || current !== queueData || current.timer !== timer) return;
|
|
108
|
+
this.dynamicDelayQueues.delete(groupSessionId);
|
|
109
|
+
current.timer = null;
|
|
110
|
+
const messages = current.messages;
|
|
111
|
+
if (messages.length === 0) return;
|
|
112
|
+
void this.sessionTurnScheduler
|
|
113
|
+
.run(groupSessionId, "dynamic-delay", () =>
|
|
114
|
+
this.processDynamicDelayMessages(
|
|
115
|
+
groupSessionId,
|
|
116
|
+
groupId,
|
|
117
|
+
selfId,
|
|
118
|
+
messages,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
.catch((err) =>
|
|
122
|
+
this.ctx.logger.error(
|
|
123
|
+
`[DynamicDelay] group ${groupId} processing failed: ${err}`,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
112
126
|
}, delayMs);
|
|
127
|
+
queueData.timer = timer;
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
isInDynamicDelay(groupSessionId: string): boolean {
|
|
@@ -117,6 +132,21 @@ export class QueueProcessor {
|
|
|
117
132
|
return queue != null && Date.now() < queue.delayUntil;
|
|
118
133
|
}
|
|
119
134
|
|
|
135
|
+
scheduleQueuedMessages(groupSessionId: string, selfId: number): void {
|
|
136
|
+
void this.sessionTurnScheduler
|
|
137
|
+
.run(
|
|
138
|
+
groupSessionId,
|
|
139
|
+
"queued-message",
|
|
140
|
+
() => this.processQueuedMessages(groupSessionId, selfId),
|
|
141
|
+
{ dedupeKey: `queued-message:${selfId}` },
|
|
142
|
+
)
|
|
143
|
+
.catch((err) =>
|
|
144
|
+
this.ctx.logger.error(
|
|
145
|
+
`[Queue] group ${groupSessionId} scheduling failed: ${err}`,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
dispose(): void {
|
|
121
151
|
for (const queue of this.dynamicDelayQueues.values()) {
|
|
122
152
|
if (queue.timer) clearTimeout(queue.timer);
|
|
@@ -124,16 +154,12 @@ export class QueueProcessor {
|
|
|
124
154
|
this.dynamicDelayQueues.clear();
|
|
125
155
|
}
|
|
126
156
|
|
|
127
|
-
async
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const messages = queueData.messages;
|
|
135
|
-
this.dynamicDelayQueues.delete(groupSessionId);
|
|
136
|
-
|
|
157
|
+
private async processDynamicDelayMessages(
|
|
158
|
+
groupSessionId: string,
|
|
159
|
+
groupId: number,
|
|
160
|
+
selfId: number,
|
|
161
|
+
messages: DynamicDelayQueueData["messages"],
|
|
162
|
+
): Promise<void> {
|
|
137
163
|
this.ctx.logger.info(`[DynamicDelay] group ${groupId} processes ${messages.length} delayed messages`);
|
|
138
164
|
this.rateLimiter.clearGroupInteractions(groupId);
|
|
139
165
|
|
|
@@ -176,8 +202,18 @@ export class QueueProcessor {
|
|
|
176
202
|
);
|
|
177
203
|
if (!result) return;
|
|
178
204
|
|
|
179
|
-
await
|
|
180
|
-
|
|
205
|
+
await finalizeChatTurn(this.pluginCtx, {
|
|
206
|
+
event: firstMsg.event,
|
|
207
|
+
cfg,
|
|
208
|
+
result,
|
|
209
|
+
groupId,
|
|
210
|
+
groupSessionId,
|
|
211
|
+
userId: targetMessage.userId,
|
|
212
|
+
selfId,
|
|
213
|
+
toolCtx,
|
|
214
|
+
send: true,
|
|
215
|
+
isLive: true,
|
|
216
|
+
});
|
|
181
217
|
}
|
|
182
218
|
|
|
183
219
|
async processQueuedMessages(groupSessionId: string, selfId: number): Promise<void> {
|
|
@@ -246,11 +282,18 @@ export class QueueProcessor {
|
|
|
246
282
|
);
|
|
247
283
|
if (!result) return;
|
|
248
284
|
|
|
249
|
-
await
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
285
|
+
await finalizeChatTurn(this.pluginCtx, {
|
|
286
|
+
event: firstItem.event,
|
|
287
|
+
cfg,
|
|
288
|
+
result,
|
|
289
|
+
groupId,
|
|
290
|
+
groupSessionId,
|
|
291
|
+
userId: targetMessage.userId,
|
|
292
|
+
selfId,
|
|
293
|
+
toolCtx,
|
|
294
|
+
send: true,
|
|
295
|
+
isLive: true,
|
|
296
|
+
});
|
|
254
297
|
this.ctx.logger.info(`[Queue] group ${groupSessionId} done`);
|
|
255
298
|
} catch (err) {
|
|
256
299
|
this.ctx.logger.error(err);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type SessionTurnSource =
|
|
2
|
+
| "message"
|
|
3
|
+
| "queued-message"
|
|
4
|
+
| "cooldown"
|
|
5
|
+
| "cooldown-planner"
|
|
6
|
+
| "dynamic-delay"
|
|
7
|
+
| "idle-check"
|
|
8
|
+
| "idle-debug"
|
|
9
|
+
| "poke"
|
|
10
|
+
| "chat-runtime";
|
|
11
|
+
|
|
12
|
+
interface SessionTurnEntry<T> {
|
|
13
|
+
source: SessionTurnSource;
|
|
14
|
+
dedupeKey?: string;
|
|
15
|
+
run: () => Promise<T>;
|
|
16
|
+
promise: Promise<T>;
|
|
17
|
+
resolve: (value: T) => void;
|
|
18
|
+
reject: (reason: unknown) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SessionTurnState {
|
|
22
|
+
running: boolean;
|
|
23
|
+
pending: SessionTurnEntry<unknown>[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SessionTurnSchedulerDisposedError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("Session turn scheduler has been disposed");
|
|
29
|
+
this.name = "SessionTurnSchedulerDisposedError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SessionTurnScheduler {
|
|
34
|
+
private readonly sessions = new Map<string, SessionTurnState>();
|
|
35
|
+
private disposed = false;
|
|
36
|
+
|
|
37
|
+
run<T>(
|
|
38
|
+
sessionId: string,
|
|
39
|
+
source: SessionTurnSource,
|
|
40
|
+
task: () => Promise<T>,
|
|
41
|
+
options: { dedupeKey?: string } = {},
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
if (this.disposed) {
|
|
44
|
+
return Promise.reject(new SessionTurnSchedulerDisposedError());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const state = this.getOrCreateState(sessionId);
|
|
48
|
+
if (options.dedupeKey) {
|
|
49
|
+
const existing = state.pending.find(
|
|
50
|
+
(entry) => entry.dedupeKey === options.dedupeKey,
|
|
51
|
+
);
|
|
52
|
+
if (existing) return existing.promise as Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let resolve!: (value: T) => void;
|
|
56
|
+
let reject!: (reason: unknown) => void;
|
|
57
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
58
|
+
resolve = resolvePromise;
|
|
59
|
+
reject = rejectPromise;
|
|
60
|
+
});
|
|
61
|
+
const entry: SessionTurnEntry<T> = {
|
|
62
|
+
source,
|
|
63
|
+
dedupeKey: options.dedupeKey,
|
|
64
|
+
run: task,
|
|
65
|
+
promise,
|
|
66
|
+
resolve,
|
|
67
|
+
reject,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
state.pending.push(entry as SessionTurnEntry<unknown>);
|
|
71
|
+
if (!state.running) void this.drain(sessionId, state);
|
|
72
|
+
return promise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isBusy(sessionId: string): boolean {
|
|
76
|
+
const state = this.sessions.get(sessionId);
|
|
77
|
+
return Boolean(state?.running || state?.pending.length);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pendingCount(sessionId: string): number {
|
|
81
|
+
return this.sessions.get(sessionId)?.pending.length ?? 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
dispose(): void {
|
|
85
|
+
if (this.disposed) return;
|
|
86
|
+
this.disposed = true;
|
|
87
|
+
const error = new SessionTurnSchedulerDisposedError();
|
|
88
|
+
|
|
89
|
+
for (const [sessionId, state] of this.sessions) {
|
|
90
|
+
for (const entry of state.pending.splice(0)) entry.reject(error);
|
|
91
|
+
if (!state.running) this.sessions.delete(sessionId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private getOrCreateState(sessionId: string): SessionTurnState {
|
|
96
|
+
let state = this.sessions.get(sessionId);
|
|
97
|
+
if (!state) {
|
|
98
|
+
state = { running: false, pending: [] };
|
|
99
|
+
this.sessions.set(sessionId, state);
|
|
100
|
+
}
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async drain(
|
|
105
|
+
sessionId: string,
|
|
106
|
+
state: SessionTurnState,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
if (state.running || this.disposed) return;
|
|
109
|
+
state.running = true;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
while (!this.disposed) {
|
|
113
|
+
const entry = state.pending.shift();
|
|
114
|
+
if (!entry) break;
|
|
115
|
+
try {
|
|
116
|
+
entry.resolve(await entry.run());
|
|
117
|
+
} catch (err) {
|
|
118
|
+
entry.reject(err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
state.running = false;
|
|
123
|
+
if (state.pending.length === 0) {
|
|
124
|
+
this.sessions.delete(sessionId);
|
|
125
|
+
} else if (!this.disposed) {
|
|
126
|
+
void this.drain(sessionId, state);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
CHANGED
package/runtime/chat-runtime.ts
CHANGED
|
@@ -10,6 +10,18 @@ import type { ChatPluginContext, ChatRuntime } from "../context";
|
|
|
10
10
|
import type { ChatConfig } from "../types";
|
|
11
11
|
import { executeChatRuntimeRequest } from "../core/chat-turn";
|
|
12
12
|
|
|
13
|
+
function resolveRuntimeGroupId(
|
|
14
|
+
options: ChatRuntimeNoticeOptions | ChatRuntimeInformationRequestOptions,
|
|
15
|
+
): number | undefined {
|
|
16
|
+
if ("event" in options) {
|
|
17
|
+
const event = options.event;
|
|
18
|
+
return event?.message_type === "group" && typeof event.group_id === "number"
|
|
19
|
+
? event.group_id
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
return "groupId" in options ? options.groupId : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export function createChatRuntime(
|
|
14
26
|
pluginCtx: ChatPluginContext,
|
|
15
27
|
getConfig: (groupId?: number) => Promise<ChatConfig>,
|
|
@@ -79,7 +91,7 @@ export function createChatRuntime(
|
|
|
79
91
|
const result = await executeChatRuntimeRequest(
|
|
80
92
|
{
|
|
81
93
|
...options,
|
|
82
|
-
config: await getConfig(),
|
|
94
|
+
config: await getConfig(resolveRuntimeGroupId(options)),
|
|
83
95
|
targetMessageContent: options.targetMessage,
|
|
84
96
|
promptInjections,
|
|
85
97
|
extraTools,
|
|
@@ -111,7 +123,7 @@ export function createChatRuntime(
|
|
|
111
123
|
return executeChatRuntimeRequest(
|
|
112
124
|
{
|
|
113
125
|
...options,
|
|
114
|
-
config: await getConfig(),
|
|
126
|
+
config: await getConfig(resolveRuntimeGroupId(options)),
|
|
115
127
|
targetMessageContent: options.instruction,
|
|
116
128
|
promptInjections,
|
|
117
129
|
send: options.send,
|