openclaw-lark-multi-agent 1.0.15 → 1.0.17
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/dist/feishu-bot.d.ts +1 -0
- package/dist/feishu-bot.js +75 -22
- package/dist/i18n.js +3 -1
- package/dist/message-store.d.ts +1 -0
- package/dist/message-store.js +16 -4
- package/dist/openclaw-client.d.ts +4 -0
- package/dist/openclaw-client.js +18 -10
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
package/dist/feishu-bot.js
CHANGED
|
@@ -396,7 +396,8 @@ export class FeishuBot {
|
|
|
396
396
|
const commandText = this.stripLeadingCommandMentions(trimmedCleanText);
|
|
397
397
|
// Escape hatch: //command means send /command through to OpenClaw,
|
|
398
398
|
// while /command remains a bridge-level openclaw-lark-multi-agent command.
|
|
399
|
-
|
|
399
|
+
const isNativeOpenClawCommand = commandText.startsWith("//");
|
|
400
|
+
if (isNativeOpenClawCommand) {
|
|
400
401
|
cleanText = "/" + commandText.slice(2).trimStart();
|
|
401
402
|
}
|
|
402
403
|
else if (commandText.startsWith("/")) {
|
|
@@ -413,6 +414,7 @@ export class FeishuBot {
|
|
|
413
414
|
senderName,
|
|
414
415
|
content: cleanText,
|
|
415
416
|
timestamp: Date.now(),
|
|
417
|
+
triggerKind: isNativeOpenClawCommand ? "native_command" : "normal",
|
|
416
418
|
});
|
|
417
419
|
if (insertedId < 0)
|
|
418
420
|
insertedId = this.store.getMessageId(messageId) || -1;
|
|
@@ -727,26 +729,64 @@ export class FeishuBot {
|
|
|
727
729
|
break;
|
|
728
730
|
}
|
|
729
731
|
this.busyChats.set(chatId, Date.now());
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
732
|
+
// Pick the batch to process this loop. Consecutive plain human messages are
|
|
733
|
+
// merged into a single run; native escaped commands (//x) are processed on
|
|
734
|
+
// their own and never merged with ordinary messages, because they are exact
|
|
735
|
+
// pass-through requests that must not receive catch-up context.
|
|
736
|
+
const firstPending = pendingHumanTriggers[0];
|
|
737
|
+
const firstIsNative = firstPending.triggerKind === "native_command";
|
|
738
|
+
let mergedTriggers;
|
|
739
|
+
if (firstIsNative) {
|
|
740
|
+
// One native command at a time.
|
|
741
|
+
mergedTriggers = [firstPending];
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Take the leading run of consecutive non-native messages.
|
|
745
|
+
mergedTriggers = [];
|
|
746
|
+
for (const m of pendingHumanTriggers) {
|
|
747
|
+
if (m.triggerKind === "native_command")
|
|
748
|
+
break;
|
|
749
|
+
mergedTriggers.push(m);
|
|
750
|
+
}
|
|
738
751
|
}
|
|
739
|
-
const
|
|
740
|
-
const
|
|
752
|
+
const isNativeCommandTrigger = firstIsNative;
|
|
753
|
+
const lastHuman = mergedTriggers[mergedTriggers.length - 1];
|
|
754
|
+
const triggerId = lastHuman.id || 0;
|
|
755
|
+
const mergedContent = mergedTriggers.map((m) => m.content).join("\n");
|
|
756
|
+
const mergedTriggerIds = mergedTriggers.map((m) => m.id || 0).filter(Boolean);
|
|
757
|
+
const mergedTriggerIdSet = new Set(mergedTriggerIds);
|
|
758
|
+
// Catch-up is only injected in group chats. p2p must never get it.
|
|
759
|
+
// Use "not p2p" rather than "=== group": chatInfo is always cached before
|
|
760
|
+
// a message reaches the queue, but if chat_type were ever missing we'd
|
|
761
|
+
// rather treat it as a group (catch-up helps in groups, harms in p2p).
|
|
762
|
+
const chatType = this.store.getChatInfo(chatId)?.chatType || "";
|
|
763
|
+
const isGroup = chatType !== "p2p";
|
|
741
764
|
if (triggerId && this.store.hasDeliveredReply(this.config.name, chatId, triggerId)) {
|
|
742
765
|
console.warn(`[${this.config.name}] Duplicate trigger skipped for ${chatId.slice(-8)} msgId=${triggerId}`);
|
|
743
|
-
|
|
766
|
+
for (const id of mergedTriggerIds)
|
|
767
|
+
this.store.clearPendingTrigger(this.config.name, chatId, id);
|
|
744
768
|
continue;
|
|
745
769
|
}
|
|
746
|
-
|
|
770
|
+
// Catch-up context: messages this bot has not seen yet in this GROUP chat.
|
|
771
|
+
// Includes both human and other-bot messages (so mention-only replies can
|
|
772
|
+
// see the human message they refer to). Excludes:
|
|
773
|
+
// - the merged trigger messages themselves (they are the current input)
|
|
774
|
+
// - other pending triggers (each is current for its own run)
|
|
775
|
+
// - this bot's own messages
|
|
776
|
+
// - native escaped commands
|
|
777
|
+
// p2p never injects catch-up; native command runs never inject catch-up.
|
|
778
|
+
let contextMsgs = [];
|
|
779
|
+
if (isGroup && !isNativeCommandTrigger) {
|
|
780
|
+
const catchupMessages = this.store.getUnsyncedMessagesForBot(this.config.name, chatId, triggerId);
|
|
781
|
+
contextMsgs = catchupMessages.filter((m) => m.senderName !== this.config.name
|
|
782
|
+
&& !(m.id && mergedTriggerIdSet.has(m.id))
|
|
783
|
+
&& !(m.id && pendingTriggerIds.has(m.id))
|
|
784
|
+
&& m.triggerKind !== "native_command");
|
|
785
|
+
}
|
|
786
|
+
const processedMessages = [...contextMsgs, ...mergedTriggers].filter((m) => m.id);
|
|
747
787
|
const queueStartedAt = Date.now();
|
|
748
788
|
const sessionKey = await this.ensureSession(chatId);
|
|
749
|
-
console.log(`[${this.config.name}] Sending ${
|
|
789
|
+
console.log(`[${this.config.name}] Sending ${mergedTriggers.length} trigger(s) as 1 run to OpenClaw for ${chatId.slice(-8)} (context=${contextMsgs.length})`);
|
|
750
790
|
// Update reactions: queued messages → sent (GET/了解)
|
|
751
791
|
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
752
792
|
for (const ack of pendingAcks) {
|
|
@@ -763,12 +803,14 @@ export class FeishuBot {
|
|
|
763
803
|
reply = await this.openclawClient.chatSendWithContext({
|
|
764
804
|
sessionKey,
|
|
765
805
|
unsyncedMessages: contextMsgs,
|
|
766
|
-
currentMessage:
|
|
806
|
+
currentMessage: mergedContent,
|
|
767
807
|
currentSenderName: lastHuman.senderName,
|
|
768
808
|
deliver: false,
|
|
769
809
|
// Keep bridge UX responsive; long agent/tool loops should surface a clear failure
|
|
770
810
|
// instead of leaving reactions stuck forever.
|
|
771
811
|
timeoutMs: 1_800_000,
|
|
812
|
+
includeContext: !isNativeCommandTrigger,
|
|
813
|
+
includeBridgeAttachmentHint: !isNativeCommandTrigger,
|
|
772
814
|
});
|
|
773
815
|
}
|
|
774
816
|
finally {
|
|
@@ -796,12 +838,12 @@ export class FeishuBot {
|
|
|
796
838
|
// arrive while the agent is busy may already be in pendingAckMessages,
|
|
797
839
|
// but they are not part of allUnsynced/humanUnsynced for this run and
|
|
798
840
|
// must remain pending for the next loop.
|
|
799
|
-
const maxId = Math.max(...
|
|
800
|
-
const processedTriggerIds = new Set(humanUnsynced.map((m) => m.id || 0).filter(Boolean));
|
|
841
|
+
const maxId = Math.max(...processedMessages.map((m) => m.id || 0));
|
|
801
842
|
const syncBatchId = `${this.config.name}:${chatId}:${triggerId}:${Date.now()}`;
|
|
802
|
-
this.store.markMessagesSynced(this.config.name, chatId,
|
|
843
|
+
this.store.markMessagesSynced(this.config.name, chatId, processedMessages.map((m) => m.id || 0), syncBatchId);
|
|
803
844
|
this.store.markSynced(this.config.name, chatId, maxId);
|
|
804
|
-
|
|
845
|
+
for (const id of mergedTriggerIds)
|
|
846
|
+
this.store.clearPendingTrigger(this.config.name, chatId, id);
|
|
805
847
|
const shouldReply = trimmedReply.length > 0 && !explicitNoReply;
|
|
806
848
|
const isRuntimeFailure = shouldReply && this.isRuntimeFailureText(trimmedReply);
|
|
807
849
|
// Record bot reply only if it is user-visible/context-worthy.
|
|
@@ -844,8 +886,10 @@ export class FeishuBot {
|
|
|
844
886
|
else {
|
|
845
887
|
try {
|
|
846
888
|
await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`);
|
|
847
|
-
|
|
848
|
-
|
|
889
|
+
// Mark every merged trigger as delivered so retries/restarts do
|
|
890
|
+
// not re-process any of them.
|
|
891
|
+
for (const id of mergedTriggerIds)
|
|
892
|
+
this.store.markDeliveredReply(this.config.name, chatId, id, lastHuman.messageId);
|
|
849
893
|
}
|
|
850
894
|
catch (err) {
|
|
851
895
|
// enqueueAndDispatchDelivery already sent a user-visible delivery
|
|
@@ -865,7 +909,7 @@ export class FeishuBot {
|
|
|
865
909
|
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
866
910
|
const remainingAcks = [];
|
|
867
911
|
for (const ack of pendingAcks) {
|
|
868
|
-
if (
|
|
912
|
+
if (mergedTriggerIdSet.has(ack.rowId)) {
|
|
869
913
|
await this.removeReaction(ack.messageId, ack.emoji).catch(() => { });
|
|
870
914
|
await this.addReaction(ack.messageId, "DONE").catch(() => { });
|
|
871
915
|
}
|
|
@@ -1000,10 +1044,19 @@ export class FeishuBot {
|
|
|
1000
1044
|
];
|
|
1001
1045
|
if (exactNames.includes(raw))
|
|
1002
1046
|
return bot.config.name;
|
|
1047
|
+
// Generic display-name fallback: many deployments name bots like
|
|
1048
|
+
// "光子 (Claude)" or "万万(GPT)". Match only when the parenthesized
|
|
1049
|
+
// suffix is exactly the configured bot name, avoiding loose substring
|
|
1050
|
+
// matches that caused shared-prefix bots to steal each other's mentions.
|
|
1051
|
+
if (new RegExp(`^[^()()]+[((]${this.escapeRegExp(botName)}[))]$`, "i").test(raw))
|
|
1052
|
+
return bot.config.name;
|
|
1003
1053
|
}
|
|
1004
1054
|
}
|
|
1005
1055
|
return null;
|
|
1006
1056
|
}
|
|
1057
|
+
escapeRegExp(value) {
|
|
1058
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1059
|
+
}
|
|
1007
1060
|
resolveBotName(sender) {
|
|
1008
1061
|
const openId = sender?.sender_id?.open_id;
|
|
1009
1062
|
if (openId) {
|
package/dist/i18n.js
CHANGED
|
@@ -13,7 +13,8 @@ const dict = {
|
|
|
13
13
|
"[LMA bridge policy]",
|
|
14
14
|
"你正在 OpenClaw Lark Multi-Agent bridge 会话中。",
|
|
15
15
|
"不要调用 message、sessions_send、feishu_im_user_message 或任何主动向飞书/外部聊天发送消息的工具。",
|
|
16
|
-
"直接在当前回复中作答;LMA bridge
|
|
16
|
+
"直接在当前回复中作答;LMA bridge 会负责把最终回复投递回原始飞书会话。",
|
|
17
|
+
"如果你是某个群的 Chairman:Chairman 的主持、调停、质疑和总结职责只在 /discuss 模式中生效。非 /discuss 模式下,即使你是 Chairman,也只按普通 @all/free/定向参与者身份给出自己的观点,不要总结、主持、调停、质疑或收束其他 bot 的回复。",
|
|
17
18
|
].join("\n"),
|
|
18
19
|
discussParticipantPrompt: (p) => [
|
|
19
20
|
"这是一个多智能体结构化讨论。",
|
|
@@ -77,6 +78,7 @@ const dict = {
|
|
|
77
78
|
"You are in an OpenClaw Lark Multi-Agent bridge session.",
|
|
78
79
|
"Do not call message, sessions_send, feishu_im_user_message, or any proactive external-chat sending tool.",
|
|
79
80
|
"Reply directly in the current assistant response; the LMA bridge will deliver the final reply back to the original Feishu chat.",
|
|
81
|
+
"If you are the Chairman of a group: your chairman duties (moderating, challenging, summarizing, or concluding other bots' replies) apply only inside /discuss mode. Outside /discuss mode, even if you are the Chairman, answer only as a normal @all/free/direct participant with your own view; do not summarize, moderate, challenge, or conclude other bots' replies.",
|
|
80
82
|
].join("\n"),
|
|
81
83
|
discussParticipantPrompt: (p) => [
|
|
82
84
|
"This is a structured multi-agent discussion.",
|
package/dist/message-store.d.ts
CHANGED
package/dist/message-store.js
CHANGED
|
@@ -20,7 +20,8 @@ export class MessageStore {
|
|
|
20
20
|
sender_type TEXT NOT NULL,
|
|
21
21
|
sender_name TEXT NOT NULL,
|
|
22
22
|
content TEXT NOT NULL,
|
|
23
|
-
timestamp INTEGER NOT NULL
|
|
23
|
+
timestamp INTEGER NOT NULL,
|
|
24
|
+
trigger_kind TEXT NOT NULL DEFAULT 'normal'
|
|
24
25
|
);
|
|
25
26
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_ts ON messages(chat_id, timestamp);
|
|
26
27
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, id);
|
|
@@ -220,6 +221,13 @@ export class MessageStore {
|
|
|
220
221
|
catch {
|
|
221
222
|
// Column already exists
|
|
222
223
|
}
|
|
224
|
+
// Migration: add trigger_kind column if missing
|
|
225
|
+
try {
|
|
226
|
+
this.db.exec(`ALTER TABLE messages ADD COLUMN trigger_kind TEXT NOT NULL DEFAULT 'normal'`);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Column already exists
|
|
230
|
+
}
|
|
223
231
|
// Migration: add free_discussion column if missing
|
|
224
232
|
try {
|
|
225
233
|
this.db.exec(`ALTER TABLE chat_info ADD COLUMN free_discussion INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -253,9 +261,9 @@ export class MessageStore {
|
|
|
253
261
|
insert(msg) {
|
|
254
262
|
try {
|
|
255
263
|
const result = this.db.prepare(`
|
|
256
|
-
INSERT INTO messages (chat_id, message_id, sender_type, sender_name, content, timestamp)
|
|
257
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
258
|
-
`).run(msg.chatId, msg.messageId, msg.senderType, msg.senderName, msg.content, msg.timestamp);
|
|
264
|
+
INSERT INTO messages (chat_id, message_id, sender_type, sender_name, content, timestamp, trigger_kind)
|
|
265
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
266
|
+
`).run(msg.chatId, msg.messageId, msg.senderType, msg.senderName, msg.content, msg.timestamp, msg.triggerKind || "normal");
|
|
259
267
|
return Number(result.lastInsertRowid);
|
|
260
268
|
}
|
|
261
269
|
catch (err) {
|
|
@@ -309,6 +317,7 @@ export class MessageStore {
|
|
|
309
317
|
senderName: r.sender_name,
|
|
310
318
|
content: r.content,
|
|
311
319
|
timestamp: r.timestamp,
|
|
320
|
+
triggerKind: r.trigger_kind || "normal",
|
|
312
321
|
}));
|
|
313
322
|
}
|
|
314
323
|
clearPendingTriggers(botName, chatId, upToId) {
|
|
@@ -459,6 +468,7 @@ export class MessageStore {
|
|
|
459
468
|
senderName: r.sender_name,
|
|
460
469
|
content: r.content,
|
|
461
470
|
timestamp: r.timestamp,
|
|
471
|
+
triggerKind: r.trigger_kind || "normal",
|
|
462
472
|
}));
|
|
463
473
|
}
|
|
464
474
|
/**
|
|
@@ -486,6 +496,7 @@ export class MessageStore {
|
|
|
486
496
|
senderName: r.sender_name,
|
|
487
497
|
content: r.content,
|
|
488
498
|
timestamp: r.timestamp,
|
|
499
|
+
triggerKind: r.trigger_kind || "normal",
|
|
489
500
|
}));
|
|
490
501
|
}
|
|
491
502
|
markMessagesSynced(botName, chatId, messageRowIds, syncBatchId = '') {
|
|
@@ -531,6 +542,7 @@ export class MessageStore {
|
|
|
531
542
|
senderName: r.sender_name,
|
|
532
543
|
content: r.content,
|
|
533
544
|
timestamp: r.timestamp,
|
|
545
|
+
triggerKind: r.trigger_kind || "normal",
|
|
534
546
|
}));
|
|
535
547
|
}
|
|
536
548
|
/**
|
|
@@ -144,6 +144,10 @@ export declare class OpenClawClient {
|
|
|
144
144
|
deliver?: boolean;
|
|
145
145
|
timeoutMs?: number;
|
|
146
146
|
emptyFinalAsNoReply?: boolean;
|
|
147
|
+
/** Native escaped commands (//status -> /status) should not receive catch-up context. */
|
|
148
|
+
includeContext?: boolean;
|
|
149
|
+
/** Disable bridge attachment hints for native commands and other exact pass-through messages. */
|
|
150
|
+
includeBridgeAttachmentHint?: boolean;
|
|
147
151
|
}): Promise<string>;
|
|
148
152
|
private formatContextLines;
|
|
149
153
|
private writeContextSyncFile;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -955,7 +955,11 @@ export class OpenClawClient {
|
|
|
955
955
|
}
|
|
956
956
|
}
|
|
957
957
|
shouldInjectBridgeAttachmentHint(text) {
|
|
958
|
-
|
|
958
|
+
// Require an action word combined with an artifact word, so ordinary talk
|
|
959
|
+
// that merely mentions "文档/图片/投递" does not trigger the long hint.
|
|
960
|
+
const action = /(发送|发到|发给|发一[张份个]|上传|生成|做一?[张份个]|画一?[张份个]|创建|导出|保存|附上|附件形式|attach|upload|export|generate|create|save)/i;
|
|
961
|
+
const artifact = /(图片|图像|照片|配图|海报|封面|文件|文档|附件|表格|pdf|docx?|xlsx?|pptx?|markdown|\bmd\b|\.png|\.jpe?g|\.gif|\.webp|image|file|document)/i;
|
|
962
|
+
return action.test(text) && artifact.test(text);
|
|
959
963
|
}
|
|
960
964
|
bridgeAttachmentHint(text) {
|
|
961
965
|
if (!this.shouldInjectBridgeAttachmentHint(text))
|
|
@@ -978,15 +982,19 @@ export class OpenClawClient {
|
|
|
978
982
|
* If there are no unsynced messages, just sends the actual message directly.
|
|
979
983
|
*/
|
|
980
984
|
async chatSendWithContext(params) {
|
|
985
|
+
const includeContext = params.includeContext !== false;
|
|
986
|
+
const includeBridgeAttachmentHint = params.includeBridgeAttachmentHint !== false;
|
|
987
|
+
const contextForAttachments = includeContext ? params.unsyncedMessages : [];
|
|
981
988
|
const attachments = this.extractImageAttachments([
|
|
982
|
-
...
|
|
989
|
+
...contextForAttachments.map((m) => m.content),
|
|
983
990
|
params.currentMessage,
|
|
984
991
|
]);
|
|
985
992
|
const mediaInstruction = attachments.length > 0
|
|
986
993
|
? "\n\n[Media note: Image attachments are included with this message. If your model can inspect images directly, use the attached image input. If it cannot, use the image tool on the provided media/attachment path; do not try unrelated network or model-provider workarounds.]"
|
|
987
994
|
: "";
|
|
988
|
-
const bridgeAttachmentHint = this.bridgeAttachmentHint(params.currentMessage);
|
|
989
|
-
|
|
995
|
+
const bridgeAttachmentHint = includeBridgeAttachmentHint ? this.bridgeAttachmentHint(params.currentMessage) : "";
|
|
996
|
+
const unsyncedMessages = includeContext ? params.unsyncedMessages : [];
|
|
997
|
+
if (unsyncedMessages.length === 0) {
|
|
990
998
|
// No context to catch up, send directly
|
|
991
999
|
return this.chatSend({
|
|
992
1000
|
sessionKey: params.sessionKey,
|
|
@@ -999,24 +1007,24 @@ export class OpenClawClient {
|
|
|
999
1007
|
}
|
|
1000
1008
|
// Build context block + actual message in one chat.send, or write the
|
|
1001
1009
|
// full context to a local transcript file when it is too large.
|
|
1002
|
-
const contextLines = this.formatContextLines(
|
|
1010
|
+
const contextLines = this.formatContextLines(unsyncedMessages);
|
|
1003
1011
|
const inlineContext = contextLines.join("\n");
|
|
1004
1012
|
const inlineBytes = Buffer.byteLength(inlineContext, "utf8");
|
|
1005
|
-
const useFileContext =
|
|
1013
|
+
const useFileContext = unsyncedMessages.length > MAX_INLINE_CONTEXT_MESSAGES || inlineBytes > MAX_INLINE_CONTEXT_BYTES;
|
|
1006
1014
|
let combined;
|
|
1007
1015
|
if (useFileContext) {
|
|
1008
|
-
const filePath = this.writeContextSyncFile(params.sessionKey,
|
|
1016
|
+
const filePath = this.writeContextSyncFile(params.sessionKey, unsyncedMessages, contextLines);
|
|
1009
1017
|
combined =
|
|
1010
|
-
`[
|
|
1018
|
+
`[以下是此前未同步的长历史对话上下文,因消息数或大小超过直接内联阈值,已写入本地文件]\n` +
|
|
1011
1019
|
`文件路径:${filePath}\n\n` +
|
|
1012
|
-
`你必须先使用 read
|
|
1020
|
+
`你必须先使用 read 工具读取这个文件,理解其中的完整历史对话,再回答当前消息。\n` +
|
|
1013
1021
|
`如果无法读取文件,请明确说明,不能直接忽略历史上下文作答。\n` +
|
|
1014
1022
|
`\n---\n` +
|
|
1015
1023
|
`[${params.currentSenderName}]: ${params.currentMessage}`;
|
|
1016
1024
|
}
|
|
1017
1025
|
else {
|
|
1018
1026
|
combined =
|
|
1019
|
-
`[
|
|
1027
|
+
`[以下是群里其他成员刚发、你还没看到的发言,供参考]\n` +
|
|
1020
1028
|
inlineContext +
|
|
1021
1029
|
`\n---\n` +
|
|
1022
1030
|
`[${params.currentSenderName}]: ${params.currentMessage}`;
|