@core-workspace/infoflow-openclaw-plugin 2026.3.9 → 2026.3.32
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/CHANGELOG.md +21 -0
- package/README.md +142 -50
- package/docs/qa-feature-list.md +413 -0
- package/index.ts +25 -1
- package/openclaw.plugin.json +22 -1
- package/package.json +15 -4
- package/publish.sh +221 -0
- package/scripts/deploy.sh +1 -1
- package/scripts/npm-tools/README.md +70 -0
- package/scripts/npm-tools/cli.js +262 -0
- package/scripts/npm-tools/package.json +21 -0
- package/skills/infoflow-dev/references/api.md +1 -1
- package/src/adapter/inbound/ws-receiver.ts +71 -29
- package/src/adapter/outbound/reply-dispatcher.ts +12 -19
- package/src/channel/accounts.ts +26 -6
- package/src/channel/channel.ts +5 -4
- package/src/channel/media.ts +8 -0
- package/src/channel/outbound.ts +15 -7
- package/src/commands/changelog.ts +53 -0
- package/src/commands/doctor.ts +391 -0
- package/src/commands/logs.ts +212 -0
- package/src/handler/message-handler.ts +77 -82
- package/src/security/group-policy.ts +2 -0
- package/src/types.ts +20 -4
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
InfoflowMessageEvent,
|
|
32
32
|
InfoflowMentionIds,
|
|
33
33
|
InfoflowReplyMode,
|
|
34
|
+
InfoflowGroupSessionMode,
|
|
34
35
|
InfoflowGroupConfig,
|
|
35
36
|
HandleInfoflowMessageParams,
|
|
36
37
|
HandlePrivateChatParams,
|
|
@@ -78,12 +79,8 @@ export async function handlePrivateChatMessage(params: HandlePrivateChatParams):
|
|
|
78
79
|
imageUrls.push(picUrl.trim());
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
`[
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
logVerbose(
|
|
86
|
-
`[DEBUG private] content字段诊断: Content=${JSON.stringify(msgData.Content)}, content=${JSON.stringify(msgData.content)}, mes=${JSON.stringify(msgData.mes)}, MsgType=${JSON.stringify(msgData.MsgType)}, msgtype=${JSON.stringify(msgData.msgtype)}, PicUrl=${JSON.stringify(msgData.PicUrl)}, picurl=${JSON.stringify(msgData.picurl)}, imageUrls=${JSON.stringify(imageUrls)}`,
|
|
82
|
+
getInfoflowBotLog().info(
|
|
83
|
+
`[inbound:dm] from=${fromuser}, name=${senderName}, msgType=${msgType}, msgId=${messageIdStr ?? "?"}, text=${mes.slice(0, 80)}${mes.length > 80 ? "..." : ""}, images=${imageUrls.length}`,
|
|
87
84
|
);
|
|
88
85
|
|
|
89
86
|
if (!fromuser || (!mes.trim() && imageUrls.length === 0)) {
|
|
@@ -161,21 +158,7 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
|
|
|
161
158
|
const timestamp = rawTime != null ? Number(rawTime) : Date.now();
|
|
162
159
|
|
|
163
160
|
// Debug: 打印完整的原始消息数据
|
|
164
|
-
logVerbose(`[
|
|
165
|
-
logVerbose(`[DEBUG bot.groupchat] 完整 header: ${JSON.stringify(header, null, 2)}`);
|
|
166
|
-
|
|
167
|
-
// Debug: 输出所有可能的 ID 字段
|
|
168
|
-
logVerbose(`[DEBUG bot.groupchat] 查找 imid (期望值: 102752365):`);
|
|
169
|
-
logVerbose(` - header.imid: ${header?.imid}`);
|
|
170
|
-
logVerbose(` - header.fromimid: ${header?.fromimid}`);
|
|
171
|
-
logVerbose(` - header.fromuserid: ${header?.fromuserid}`);
|
|
172
|
-
logVerbose(` - msgData.imid: ${msgData.imid}`);
|
|
173
|
-
logVerbose(` - msgData.fromimid: ${msgData.fromimid}`);
|
|
174
|
-
logVerbose(` - msgData.fromuserid: ${msgData.fromuserid}`);
|
|
175
|
-
logVerbose(` - msgData.from: ${msgData.from}`);
|
|
176
|
-
logVerbose(` - msgData.userid: ${msgData.userid}`);
|
|
177
|
-
logVerbose(` - fromuser: ${fromuser}`);
|
|
178
|
-
logVerbose(` - senderImidStr: ${senderImidStr}`);
|
|
161
|
+
logVerbose(`[inbound:group:raw] msgData: ${JSON.stringify(msgData, null, 2)}`);
|
|
179
162
|
|
|
180
163
|
if (!fromuser) {
|
|
181
164
|
return;
|
|
@@ -246,13 +229,9 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
|
|
|
246
229
|
}
|
|
247
230
|
} else if (item.type === "IMAGE") {
|
|
248
231
|
// 提取图片下载地址
|
|
249
|
-
logVerbose(`[DEBUG bot.groupchat] IMAGE item: ${JSON.stringify(item, null, 2)}`);
|
|
250
232
|
const url = item.downloadurl;
|
|
251
233
|
if (typeof url === "string" && url.trim()) {
|
|
252
|
-
logVerbose(`[DEBUG bot.groupchat] 提取到图片URL: ${url}`);
|
|
253
234
|
imageUrls.push(url.trim());
|
|
254
|
-
} else {
|
|
255
|
-
logVerbose(`[DEBUG bot.groupchat] WARNING: IMAGE item 缺少有效的 downloadurl 字段`);
|
|
256
235
|
}
|
|
257
236
|
}
|
|
258
237
|
}
|
|
@@ -268,7 +247,6 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
|
|
|
268
247
|
}
|
|
269
248
|
// 纯图片消息:设置占位符
|
|
270
249
|
if (!mes && imageUrls.length > 0) {
|
|
271
|
-
logVerbose(`[DEBUG bot.groupchat] 纯图片消息: ${imageUrls.length} 张图片`);
|
|
272
250
|
mes = `<media:image>${imageUrls.length > 1 ? ` (${imageUrls.length} images)` : ""}`;
|
|
273
251
|
}
|
|
274
252
|
// If mes is empty but replyContext exists, use a placeholder so the message is not dropped
|
|
@@ -279,6 +257,10 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
|
|
|
279
257
|
// Extract sender name from header or fallback to fromuser
|
|
280
258
|
const senderName = String(header?.username ?? header?.nickname ?? msgData.username ?? fromuser);
|
|
281
259
|
|
|
260
|
+
getInfoflowBotLog().info(
|
|
261
|
+
`[inbound:group] from=${fromuser}, name=${senderName}, group=${groupid}, msgId=${messageIdStr ?? "?"}, mentioned=${wasMentioned}, text=${mes.slice(0, 80)}${mes.length > 80 ? "..." : ""}, images=${imageUrls.length}, reply=${replyContextItems.length > 0}`,
|
|
262
|
+
);
|
|
263
|
+
|
|
282
264
|
// Delegate to the common message handler (group chat)
|
|
283
265
|
await handleInfoflowMessage({
|
|
284
266
|
cfg,
|
|
@@ -316,10 +298,8 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
316
298
|
const core = getInfoflowRuntime();
|
|
317
299
|
|
|
318
300
|
const isGroup = chatType === "group";
|
|
319
|
-
// Convert groupId (number) to string for peerId since routing expects string
|
|
320
|
-
const peerId = isGroup ? (groupId !== undefined ? String(groupId) : fromuser) : fromuser;
|
|
321
301
|
|
|
322
|
-
// Resolve per-group config for replyMode gating
|
|
302
|
+
// Resolve per-group config for replyMode gating (needed for groupSessionMode)
|
|
323
303
|
const groupCfg = isGroup ? resolveGroupConfig(account, groupId) : undefined;
|
|
324
304
|
|
|
325
305
|
// "ignore" mode: discard immediately, no save, no think, no reply
|
|
@@ -327,6 +307,18 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
327
307
|
return;
|
|
328
308
|
}
|
|
329
309
|
|
|
310
|
+
// Determine group session mode for peerId generation (group config overrides account config)
|
|
311
|
+
const groupSessionMode = isGroup ? groupCfg!.groupSessionMode : "group";
|
|
312
|
+
// Convert groupId (number) to string for peerId since routing expects string
|
|
313
|
+
// When groupSessionMode is "user", peerId includes the user ID to split sessions by user within group
|
|
314
|
+
const peerId = isGroup
|
|
315
|
+
? groupId !== undefined
|
|
316
|
+
? groupSessionMode === "user"
|
|
317
|
+
? `${groupId}:${fromuser}`
|
|
318
|
+
: String(groupId)
|
|
319
|
+
: fromuser
|
|
320
|
+
: fromuser;
|
|
321
|
+
|
|
330
322
|
// Resolve route based on chat type
|
|
331
323
|
const route = core.channel.routing.resolveAgentRoute({
|
|
332
324
|
cfg,
|
|
@@ -348,7 +340,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
348
340
|
});
|
|
349
341
|
|
|
350
342
|
// Build conversation label and from address based on chat type
|
|
351
|
-
const fromLabel = isGroup
|
|
343
|
+
const fromLabel = isGroup
|
|
344
|
+
? groupSessionMode === "user"
|
|
345
|
+
? `group:${groupId}:${senderName || fromuser}`
|
|
346
|
+
: `group:${groupId}`
|
|
347
|
+
: `infoflow-${senderName || fromuser}`;
|
|
352
348
|
const fromAddress = isGroup ? `infoflow:group:${groupId}` : `infoflow:${fromuser}`;
|
|
353
349
|
const toAddress = isGroup ? `infoflow:group:${groupId}` : `infoflow:${fromuser}`;
|
|
354
350
|
|
|
@@ -362,7 +358,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
362
358
|
});
|
|
363
359
|
|
|
364
360
|
// Inject accumulated group chat history into the body for context
|
|
365
|
-
const historyKey = isGroup && groupId !== undefined
|
|
361
|
+
const historyKey = isGroup && groupId !== undefined
|
|
362
|
+
? groupSessionMode === "user"
|
|
363
|
+
? `${groupId}:${fromuser}`
|
|
364
|
+
: String(groupId)
|
|
365
|
+
: undefined;
|
|
366
366
|
let combinedBody = body;
|
|
367
367
|
if (isGroup && historyKey) {
|
|
368
368
|
combinedBody = buildPendingHistoryContextFromMap({
|
|
@@ -395,9 +395,8 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
395
395
|
const mediaList: Array<{ path: string; contentType?: string }> = [];
|
|
396
396
|
const failReasons: string[] = [];
|
|
397
397
|
|
|
398
|
-
logVerbose(`[
|
|
398
|
+
logVerbose(`[inbound] 图片处理: urls=${event.imageUrls?.length ?? 0}`);
|
|
399
399
|
if (event.imageUrls && event.imageUrls.length > 0) {
|
|
400
|
-
logVerbose(`[DEBUG bot] 待下载图片URLs: ${JSON.stringify(event.imageUrls)}`);
|
|
401
400
|
// Collect unique hostnames from image URLs for SSRF allowlist.
|
|
402
401
|
// Infoflow image servers (e.g. xp2.im.baidu.com, e4hi.im.baidu.com) resolve to
|
|
403
402
|
// internal IPs on Baidu's network, so they need to be explicitly allowed.
|
|
@@ -417,38 +416,33 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
417
416
|
const urls = event.imageUrls.slice(0, INFOFLOW_MAX_IMAGES);
|
|
418
417
|
const results = await Promise.allSettled(
|
|
419
418
|
urls.map(async (imageUrl) => {
|
|
420
|
-
logVerbose(`[DEBUG bot] 开始下载图片: ${imageUrl}`);
|
|
421
419
|
const fetched = await core.channel.media.fetchRemoteMedia({
|
|
422
420
|
url: imageUrl,
|
|
423
421
|
maxBytes: mediaMaxBytes,
|
|
424
422
|
ssrfPolicy,
|
|
425
423
|
});
|
|
426
|
-
logVerbose(`[DEBUG bot] 图片下载成功: size=${fetched.buffer.length}, contentType=${fetched.contentType}`);
|
|
427
424
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
428
425
|
fetched.buffer,
|
|
429
426
|
fetched.contentType ?? undefined,
|
|
430
427
|
"inbound",
|
|
431
428
|
mediaMaxBytes,
|
|
432
429
|
);
|
|
433
|
-
logVerbose(`[
|
|
434
|
-
logVerbose(`[DEBUG bot] 图片保存成功: path=${saved.path}`);
|
|
430
|
+
logVerbose(`[inbound] image downloaded: url=${imageUrl}, size=${fetched.buffer.length}, saved=${saved.path}`);
|
|
435
431
|
return { path: saved.path, contentType: saved.contentType ?? fetched.contentType };
|
|
436
432
|
}),
|
|
437
433
|
);
|
|
438
434
|
for (const result of results) {
|
|
439
435
|
if (result.status === "fulfilled") {
|
|
440
436
|
mediaList.push(result.value);
|
|
441
|
-
logVerbose(`[DEBUG bot] 图片处理成功: ${result.value.path}`);
|
|
442
437
|
} else {
|
|
443
438
|
const reason = String(result.reason);
|
|
444
|
-
logVerbose(`[
|
|
445
|
-
logVerbose(`[DEBUG bot] 图片下载失败: ${reason}`);
|
|
439
|
+
logVerbose(`[inbound] image download failed: ${reason}`);
|
|
446
440
|
failReasons.push(reason);
|
|
447
441
|
}
|
|
448
442
|
}
|
|
449
443
|
}
|
|
450
444
|
|
|
451
|
-
logVerbose(`[
|
|
445
|
+
logVerbose(`[inbound] 图片处理完成: ok=${mediaList.length}, fail=${failReasons.length}`);
|
|
452
446
|
|
|
453
447
|
const mediaPayload = buildAgentMediaPayload(mediaList);
|
|
454
448
|
|
|
@@ -521,17 +515,22 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
521
515
|
let triggerReason = "direct-message";
|
|
522
516
|
if (isGroup && groupCfg) {
|
|
523
517
|
const { replyMode } = groupCfg;
|
|
524
|
-
|
|
518
|
+
// Generate key for history tracking and follow-up window based on groupSessionMode
|
|
519
|
+
const historyReplyKey = groupId !== undefined
|
|
520
|
+
? groupSessionMode === "user"
|
|
521
|
+
? `${groupId}:${fromuser}`
|
|
522
|
+
: String(groupId)
|
|
523
|
+
: undefined;
|
|
525
524
|
|
|
526
525
|
// "record" mode: save to session only, no think, no reply
|
|
527
526
|
if (replyMode === "record") {
|
|
528
|
-
if (
|
|
527
|
+
if (historyReplyKey) {
|
|
529
528
|
logVerbose(
|
|
530
529
|
`[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=record-mode`,
|
|
531
530
|
);
|
|
532
531
|
recordPendingHistoryEntryIfEnabled({
|
|
533
532
|
historyMap: chatHistories,
|
|
534
|
-
historyKey:
|
|
533
|
+
historyKey: historyReplyKey,
|
|
535
534
|
entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
|
|
536
535
|
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
537
536
|
});
|
|
@@ -543,27 +542,29 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
543
542
|
const wasMentioned = event.wasMentioned === true;
|
|
544
543
|
|
|
545
544
|
if (replyMode === "mention-only") {
|
|
546
|
-
// Only reply if bot was @mentioned
|
|
547
|
-
|
|
545
|
+
// Only reply if bot was @mentioned.
|
|
546
|
+
// If robotName is not configured, @mention detection is impossible —
|
|
547
|
+
// fall back to treating every message as a trigger (same as proactive).
|
|
548
|
+
const shouldReply = !canDetectMention || wasMentioned;
|
|
548
549
|
if (shouldReply) {
|
|
549
550
|
triggerReason = "bot-mentioned";
|
|
550
551
|
} else {
|
|
551
552
|
// Check follow-up window: if bot recently replied, allow LLM to decide
|
|
552
553
|
if (
|
|
553
554
|
groupCfg.followUp &&
|
|
554
|
-
|
|
555
|
-
isWithinFollowUpWindow(
|
|
555
|
+
historyReplyKey &&
|
|
556
|
+
isWithinFollowUpWindow(historyReplyKey, groupCfg.followUpWindow)
|
|
556
557
|
) {
|
|
557
558
|
triggerReason = "followUp";
|
|
558
559
|
ctxPayload.GroupSystemPrompt = buildFollowUpPrompt();
|
|
559
560
|
} else {
|
|
560
|
-
if (
|
|
561
|
+
if (historyReplyKey) {
|
|
561
562
|
logVerbose(
|
|
562
563
|
`[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=mention-only-not-mentioned`,
|
|
563
564
|
);
|
|
564
565
|
recordPendingHistoryEntryIfEnabled({
|
|
565
566
|
historyMap: chatHistories,
|
|
566
|
-
historyKey:
|
|
567
|
+
historyKey: historyReplyKey,
|
|
567
568
|
entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
|
|
568
569
|
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
569
570
|
});
|
|
@@ -572,8 +573,9 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
572
573
|
}
|
|
573
574
|
}
|
|
574
575
|
} else if (replyMode === "mention-and-watch") {
|
|
575
|
-
// Reply if bot @mentioned, or if watched person @mentioned, or follow-up
|
|
576
|
-
|
|
576
|
+
// Reply if bot @mentioned, or if watched person @mentioned, or follow-up.
|
|
577
|
+
// If robotName is not configured, fall back to treating every message as a trigger.
|
|
578
|
+
const botMentioned = !canDetectMention || wasMentioned;
|
|
577
579
|
if (botMentioned) {
|
|
578
580
|
triggerReason = "bot-mentioned";
|
|
579
581
|
} else {
|
|
@@ -594,20 +596,20 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
594
596
|
ctxPayload.GroupSystemPrompt = buildWatchRegexPrompt(groupCfg.watchRegex);
|
|
595
597
|
} else if (
|
|
596
598
|
groupCfg.followUp &&
|
|
597
|
-
|
|
598
|
-
isWithinFollowUpWindow(
|
|
599
|
+
historyReplyKey &&
|
|
600
|
+
isWithinFollowUpWindow(historyReplyKey, groupCfg.followUpWindow)
|
|
599
601
|
) {
|
|
600
602
|
triggerReason = "followUp";
|
|
601
603
|
// Follow-up window: let LLM decide if this is a follow-up
|
|
602
604
|
ctxPayload.GroupSystemPrompt = buildFollowUpPrompt();
|
|
603
605
|
} else {
|
|
604
|
-
if (
|
|
606
|
+
if (historyReplyKey) {
|
|
605
607
|
logVerbose(
|
|
606
608
|
`[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=mention-and-watch-no-trigger`,
|
|
607
609
|
);
|
|
608
610
|
recordPendingHistoryEntryIfEnabled({
|
|
609
611
|
historyMap: chatHistories,
|
|
610
|
-
historyKey:
|
|
612
|
+
historyKey: historyReplyKey,
|
|
611
613
|
entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
|
|
612
614
|
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
613
615
|
});
|
|
@@ -663,21 +665,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
663
665
|
}
|
|
664
666
|
}
|
|
665
667
|
|
|
666
|
-
|
|
667
|
-
`[
|
|
668
|
+
getInfoflowBotLog().info(
|
|
669
|
+
`[dispatch] from=${fromuser}, to=${to}, chatType=${chatType}, trigger=${triggerReason}, replyMode=${groupCfg?.replyMode ?? "N/A"}, session=${route.sessionKey}`,
|
|
668
670
|
);
|
|
669
671
|
|
|
670
|
-
|
|
671
|
-
logVerbose(`[DEBUG bot] event.messageId=${event.messageId}, event.senderImid=${event.senderImid}, isGroup=${isGroup}, mes=${mes.slice(0, 50)}`);
|
|
672
|
-
if (!event.messageId) {
|
|
673
|
-
logVerbose(`[DEBUG bot] WARNING: event.messageId is undefined/null!`);
|
|
674
|
-
}
|
|
675
|
-
if (!event.senderImid) {
|
|
676
|
-
logVerbose(`[DEBUG bot] WARNING: event.senderImid is undefined/null!`);
|
|
677
|
-
}
|
|
678
|
-
if (!isGroup) {
|
|
679
|
-
logVerbose(`[DEBUG bot] Not a group message, skipping reply-to`);
|
|
680
|
-
}
|
|
672
|
+
logVerbose(`[dispatch:detail] messageId=${event.messageId}, senderImid=${event.senderImid}, images=${mediaList.length}`);
|
|
681
673
|
|
|
682
674
|
// Send "processing" hint if LLM takes longer than processingHintDelay seconds
|
|
683
675
|
// (default: 5s). Gives users feedback without spamming fast responses.
|
|
@@ -704,7 +696,7 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
704
696
|
sendInfoflowMessage({
|
|
705
697
|
cfg,
|
|
706
698
|
to,
|
|
707
|
-
contents: [{ type: "text", content: "
|
|
699
|
+
contents: [{ type: "text", content: "👌收到啦" }],
|
|
708
700
|
accountId: account.accountId,
|
|
709
701
|
replyTo: processingReplyTo,
|
|
710
702
|
}).catch(() => {});
|
|
@@ -729,6 +721,8 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
729
721
|
messageFormat: isGroup
|
|
730
722
|
? (account.config.groupMessageFormat ?? "text")
|
|
731
723
|
: (account.config.dmMessageFormat ?? "text"),
|
|
724
|
+
// Chunk size: per-account config, default 1800
|
|
725
|
+
textChunkLimit: account.config.textChunkLimit ?? 1800,
|
|
732
726
|
});
|
|
733
727
|
|
|
734
728
|
// Cancel processing hint the moment the first real message starts being delivered,
|
|
@@ -756,15 +750,15 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
756
750
|
cancelProcessingHint?.();
|
|
757
751
|
|
|
758
752
|
// If hint was shown to the user, send "搞定" so they know the task is done
|
|
759
|
-
if (hintWasSent) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
753
|
+
// if (hintWasSent) {
|
|
754
|
+
// const elapsedSec = Math.round((Date.now() - dispatchStartTime) / 1000);
|
|
755
|
+
// sendInfoflowMessage({
|
|
756
|
+
// cfg,
|
|
757
|
+
// to,
|
|
758
|
+
// contents: [{ type: "text", content: `任务完成 ✨ (${elapsedSec}s)` }],
|
|
759
|
+
// accountId: account.accountId,
|
|
760
|
+
// }).catch(() => {});
|
|
761
|
+
// }
|
|
768
762
|
|
|
769
763
|
const didReply = dispatchResult?.queuedFinal ?? false;
|
|
770
764
|
|
|
@@ -779,11 +773,12 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
|
|
|
779
773
|
|
|
780
774
|
// Record bot reply timestamp for follow-up window tracking
|
|
781
775
|
if (didReply && isGroup && groupId !== undefined) {
|
|
782
|
-
|
|
776
|
+
const replyKey = groupSessionMode === "user" ? `${groupId}:${fromuser}` : String(groupId);
|
|
777
|
+
recordGroupReply(replyKey);
|
|
783
778
|
}
|
|
784
779
|
|
|
785
|
-
|
|
786
|
-
`[
|
|
780
|
+
getInfoflowBotLog().info(
|
|
781
|
+
`[dispatch:done] from=${fromuser}, to=${to}, replied=${didReply}, blocks=${dispatchResult?.counts.final ?? 0}, elapsed=${Date.now() - dispatchStartTime}ms`,
|
|
787
782
|
);
|
|
788
783
|
}
|
|
789
784
|
|
|
@@ -150,6 +150,7 @@ export function isWithinFollowUpWindow(groupId: string, windowSeconds: number):
|
|
|
150
150
|
|
|
151
151
|
export type ResolvedGroupConfig = {
|
|
152
152
|
replyMode: InfoflowReplyMode;
|
|
153
|
+
groupSessionMode: InfoflowGroupSessionMode;
|
|
153
154
|
followUp: boolean;
|
|
154
155
|
followUpWindow: number;
|
|
155
156
|
watchMentions: string[];
|
|
@@ -175,6 +176,7 @@ export function resolveGroupConfig(
|
|
|
175
176
|
groupId != null ? account.config.groups?.[String(groupId)] : undefined;
|
|
176
177
|
return {
|
|
177
178
|
replyMode: groupCfg?.replyMode ?? account.config.replyMode ?? inferLegacyReplyMode(account),
|
|
179
|
+
groupSessionMode: groupCfg?.groupSessionMode ?? account.config.groupSessionMode ?? "group",
|
|
178
180
|
followUp: groupCfg?.followUp ?? account.config.followUp ?? true,
|
|
179
181
|
followUpWindow: groupCfg?.followUpWindow ?? account.config.followUpWindow ?? 300,
|
|
180
182
|
watchMentions: groupCfg?.watchMentions ?? account.config.watchMentions ?? [],
|
package/src/types.ts
CHANGED
|
@@ -19,9 +19,14 @@ export type InfoflowReplyMode =
|
|
|
19
19
|
| "mention-and-watch"
|
|
20
20
|
| "proactive";
|
|
21
21
|
|
|
22
|
+
/** Group session mode: whether to split sessions by group or by group+user */
|
|
23
|
+
export type InfoflowGroupSessionMode = "group" | "user";
|
|
24
|
+
|
|
22
25
|
/** Per-group configuration overrides */
|
|
23
26
|
export type InfoflowGroupConfig = {
|
|
24
27
|
replyMode?: InfoflowReplyMode;
|
|
28
|
+
/** Group session mode: "group" = one session per group, "user" = one session per user in group */
|
|
29
|
+
groupSessionMode?: InfoflowGroupSessionMode;
|
|
25
30
|
systemPrompt?: string;
|
|
26
31
|
/** Enable follow-up replies after bot responds (default: true) */
|
|
27
32
|
followUp?: boolean;
|
|
@@ -128,6 +133,8 @@ export type InfoflowAccountConfig = {
|
|
|
128
133
|
watchRegex?: string;
|
|
129
134
|
/** Reply mode controlling bot engagement level in groups */
|
|
130
135
|
replyMode?: InfoflowReplyMode;
|
|
136
|
+
/** Group session mode: "group" = one session per group, "user" = one session per user in group (default: "group") */
|
|
137
|
+
groupSessionMode?: InfoflowGroupSessionMode;
|
|
131
138
|
/** Enable follow-up replies after bot responds to a mention (default: true) */
|
|
132
139
|
followUp?: boolean;
|
|
133
140
|
/** Follow-up window in seconds after last bot reply (default: 300) */
|
|
@@ -162,6 +169,12 @@ export type InfoflowAccountConfig = {
|
|
|
162
169
|
* Default: "text"
|
|
163
170
|
*/
|
|
164
171
|
groupMessageFormat?: "text" | "markdown";
|
|
172
|
+
/**
|
|
173
|
+
* Maximum character limit per outbound message chunk.
|
|
174
|
+
* Long messages are automatically split into multiple messages each within this limit.
|
|
175
|
+
* Default: 1800
|
|
176
|
+
*/
|
|
177
|
+
textChunkLimit?: number;
|
|
165
178
|
};
|
|
166
179
|
|
|
167
180
|
export type ResolvedInfoflowAccount = {
|
|
@@ -195,6 +208,8 @@ export type ResolvedInfoflowAccount = {
|
|
|
195
208
|
watchRegex?: string;
|
|
196
209
|
/** Reply mode controlling bot engagement level in groups */
|
|
197
210
|
replyMode?: InfoflowReplyMode;
|
|
211
|
+
/** Group session mode: "group" = one session per group, "user" = one session per user in group (default: "group") */
|
|
212
|
+
groupSessionMode?: InfoflowGroupSessionMode;
|
|
198
213
|
/** Enable follow-up replies after bot responds to a mention (default: true) */
|
|
199
214
|
followUp?: boolean;
|
|
200
215
|
/** Follow-up window in seconds after last bot reply (default: 300) */
|
|
@@ -214,10 +229,11 @@ export type ResolvedInfoflowAccount = {
|
|
|
214
229
|
* Default: "text"
|
|
215
230
|
*/
|
|
216
231
|
groupMessageFormat?: "text" | "markdown";
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Maximum character limit per outbound message chunk.
|
|
234
|
+
* Default: 1800
|
|
235
|
+
*/
|
|
236
|
+
textChunkLimit?: number;
|
|
221
237
|
};
|
|
222
238
|
};
|
|
223
239
|
|