openclaw-lark-multi-agent 0.1.4 → 0.1.6
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 +6 -0
- package/dist/feishu-bot.js +165 -14
- package/dist/openclaw-client.d.ts +6 -1
- package/dist/openclaw-client.js +66 -12
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +12 -0
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export declare class FeishuBot {
|
|
|
74
74
|
private cleanMentions;
|
|
75
75
|
private stripLeadingCommandMentions;
|
|
76
76
|
private replyMessage;
|
|
77
|
+
private extractBridgeAttachments;
|
|
78
|
+
private validateBridgeAttachmentPath;
|
|
79
|
+
private inferFeishuFileType;
|
|
80
|
+
private isImagePath;
|
|
81
|
+
private createFeishuDocFromMarkdown;
|
|
82
|
+
private sendBridgeAttachment;
|
|
77
83
|
/**
|
|
78
84
|
* Send a proactive message to a chat (not a reply).
|
|
79
85
|
*/
|
package/dist/feishu-bot.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
|
+
import { basename, extname, resolve } from "path";
|
|
4
|
+
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
2
5
|
const MAX_BOT_STREAK = 10;
|
|
6
|
+
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
3
7
|
/**
|
|
4
8
|
* Manages a single Feishu bot instance.
|
|
5
9
|
*
|
|
@@ -107,7 +111,12 @@ export class FeishuBot {
|
|
|
107
111
|
await this.openclawClient.subscribeSession(sessionKey, async (text) => {
|
|
108
112
|
try {
|
|
109
113
|
console.log(`[${this.config.name}] Proactive message for ${chatId.slice(-8)}`);
|
|
110
|
-
|
|
114
|
+
const parsed = this.extractBridgeAttachments(text);
|
|
115
|
+
if (parsed.text.trim())
|
|
116
|
+
await this.sendMessage(chatId, parsed.text);
|
|
117
|
+
for (const attachment of parsed.attachments) {
|
|
118
|
+
await this.sendBridgeAttachment(chatId, attachment);
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
catch (err) {
|
|
113
122
|
console.error(`[${this.config.name}] Failed to deliver proactive msg:`, err.message);
|
|
@@ -550,16 +559,22 @@ export class FeishuBot {
|
|
|
550
559
|
const maxId = Math.max(...allUnsynced.map((m) => m.id || 0));
|
|
551
560
|
this.store.markSynced(this.config.name, chatId, maxId);
|
|
552
561
|
this.store.clearPendingTriggers(this.config.name, chatId, maxId);
|
|
553
|
-
const
|
|
562
|
+
const parsedReply = this.extractBridgeAttachments(reply);
|
|
563
|
+
const visibleReply = parsedReply.text;
|
|
564
|
+
const trimmedReply = visibleReply.trim();
|
|
554
565
|
const shouldReply = trimmedReply.length > 0 && trimmedReply.toUpperCase() !== "NO_REPLY";
|
|
566
|
+
const hasAttachments = parsedReply.attachments.length > 0;
|
|
555
567
|
// Record bot reply only if it is user-visible/context-worthy.
|
|
556
|
-
if (shouldReply) {
|
|
568
|
+
if (shouldReply || hasAttachments) {
|
|
569
|
+
const storedContent = [visibleReply, ...parsedReply.attachments.map((a) => `[Attachment: ${a.type || "file"} ${a.path}]`)]
|
|
570
|
+
.filter(Boolean)
|
|
571
|
+
.join("\n");
|
|
557
572
|
const replyId = this.store.insert({
|
|
558
573
|
chatId,
|
|
559
574
|
messageId: `self-${this.config.name}-${Date.now()}`,
|
|
560
575
|
senderType: "bot",
|
|
561
576
|
senderName: this.config.name,
|
|
562
|
-
content:
|
|
577
|
+
content: storedContent,
|
|
563
578
|
timestamp: Date.now(),
|
|
564
579
|
});
|
|
565
580
|
if (replyId > 0)
|
|
@@ -573,28 +588,33 @@ export class FeishuBot {
|
|
|
573
588
|
}
|
|
574
589
|
// Reply to the last human message on Feishu (ordered after tool msgs)
|
|
575
590
|
// Skip empty replies and explicit NO_REPLY responses
|
|
576
|
-
if (shouldReply && lastHuman.messageId) {
|
|
591
|
+
if ((shouldReply || hasAttachments) && lastHuman.messageId) {
|
|
577
592
|
if (triggerId && this.store.hasDeliveredReply(this.config.name, chatId, triggerId)) {
|
|
578
593
|
console.warn(`[${this.config.name}] Reply already delivered, skip duplicate for ${chatId.slice(-8)} msgId=${triggerId}`);
|
|
579
594
|
}
|
|
580
595
|
else {
|
|
581
596
|
await this.sendOrdered(chatId, async () => {
|
|
582
|
-
|
|
583
|
-
|
|
597
|
+
if (shouldReply) {
|
|
598
|
+
try {
|
|
599
|
+
await this.replyMessage(lastHuman.messageId, visibleReply);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
// Reply can fail for historical messages sent before this bot joined the chat
|
|
603
|
+
// (Feishu 230002: Bot/User can NOT be out of the chat). Fall back to a normal
|
|
604
|
+
// chat message so queue processing still completes.
|
|
605
|
+
console.warn(`[${this.config.name}] replyMessage failed, fallback to sendMessage:`, err.message);
|
|
606
|
+
await this.sendMessage(chatId, visibleReply);
|
|
607
|
+
}
|
|
584
608
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// (Feishu 230002: Bot/User can NOT be out of the chat). Fall back to a normal
|
|
588
|
-
// chat message so queue processing still completes.
|
|
589
|
-
console.warn(`[${this.config.name}] replyMessage failed, fallback to sendMessage:`, err.message);
|
|
590
|
-
await this.sendMessage(chatId, reply);
|
|
609
|
+
for (const attachment of parsedReply.attachments) {
|
|
610
|
+
await this.sendBridgeAttachment(chatId, attachment);
|
|
591
611
|
}
|
|
592
612
|
if (triggerId)
|
|
593
613
|
this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
|
|
594
614
|
});
|
|
595
615
|
}
|
|
596
616
|
}
|
|
597
|
-
console.log(`[${this.config.name}] [${new Date().toISOString()}] ${shouldReply ? 'Replied' : 'Skipped (empty/NO_REPLY)'} (${reply.length} chars)`);
|
|
617
|
+
console.log(`[${this.config.name}] [${new Date().toISOString()}] ${shouldReply || hasAttachments ? 'Replied' : 'Skipped (empty/NO_REPLY)'} (${reply.length} chars, attachments=${parsedReply.attachments.length})`);
|
|
598
618
|
// Replace ack reactions with DONE for all pending messages in this chat
|
|
599
619
|
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
600
620
|
for (const ack of pendingAcks) {
|
|
@@ -734,6 +754,137 @@ export class FeishuBot {
|
|
|
734
754
|
});
|
|
735
755
|
}
|
|
736
756
|
}
|
|
757
|
+
extractBridgeAttachments(reply) {
|
|
758
|
+
const attachments = [];
|
|
759
|
+
const markerPattern = /<LMA_BRIDGE_ATTACHMENTS>([\s\S]*?)<\/LMA_BRIDGE_ATTACHMENTS>/g;
|
|
760
|
+
let text = reply.replace(markerPattern, (_match, jsonText) => {
|
|
761
|
+
try {
|
|
762
|
+
const parsed = JSON.parse(String(jsonText).trim());
|
|
763
|
+
const rawAttachments = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.attachments) ? parsed.attachments : [parsed];
|
|
764
|
+
for (const item of rawAttachments) {
|
|
765
|
+
if (!item || typeof item.path !== "string")
|
|
766
|
+
continue;
|
|
767
|
+
attachments.push({
|
|
768
|
+
type: item.type === "image" || item.type === "document" || item.type === "file" ? item.type : undefined,
|
|
769
|
+
path: item.path,
|
|
770
|
+
caption: typeof item.caption === "string" ? item.caption : undefined,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
console.warn(`[${this.config.name}] Failed to parse bridge attachment marker:`, err.message);
|
|
776
|
+
}
|
|
777
|
+
return "";
|
|
778
|
+
}).trim();
|
|
779
|
+
return { text, attachments };
|
|
780
|
+
}
|
|
781
|
+
validateBridgeAttachmentPath(filePath) {
|
|
782
|
+
const resolvedPath = resolve(filePath);
|
|
783
|
+
const allowedRoot = resolve(BRIDGE_ATTACHMENTS_DIR);
|
|
784
|
+
if (!(resolvedPath === allowedRoot || resolvedPath.startsWith(allowedRoot + "/"))) {
|
|
785
|
+
throw new Error(`Attachment path outside allowed directory: ${resolvedPath}`);
|
|
786
|
+
}
|
|
787
|
+
if (!existsSync(resolvedPath))
|
|
788
|
+
throw new Error(`Attachment file not found: ${resolvedPath}`);
|
|
789
|
+
const stats = statSync(resolvedPath);
|
|
790
|
+
if (!stats.isFile())
|
|
791
|
+
throw new Error(`Attachment path is not a file: ${resolvedPath}`);
|
|
792
|
+
if (stats.size <= 0)
|
|
793
|
+
throw new Error(`Attachment file is empty: ${resolvedPath}`);
|
|
794
|
+
if (stats.size > 30 * 1024 * 1024)
|
|
795
|
+
throw new Error(`Attachment file too large (>30MB): ${resolvedPath}`);
|
|
796
|
+
return resolvedPath;
|
|
797
|
+
}
|
|
798
|
+
inferFeishuFileType(filePath) {
|
|
799
|
+
const ext = extname(filePath).toLowerCase();
|
|
800
|
+
if (ext === ".pdf")
|
|
801
|
+
return "pdf";
|
|
802
|
+
if ([".doc", ".docx"].includes(ext))
|
|
803
|
+
return "doc";
|
|
804
|
+
if ([".xls", ".xlsx", ".csv"].includes(ext))
|
|
805
|
+
return "xls";
|
|
806
|
+
if ([".ppt", ".pptx"].includes(ext))
|
|
807
|
+
return "ppt";
|
|
808
|
+
return "stream";
|
|
809
|
+
}
|
|
810
|
+
isImagePath(filePath) {
|
|
811
|
+
return [".jpg", ".jpeg", ".png", ".webp", ".gif", ".tiff", ".bmp", ".ico"].includes(extname(filePath).toLowerCase());
|
|
812
|
+
}
|
|
813
|
+
async createFeishuDocFromMarkdown(filePath) {
|
|
814
|
+
const rawTitle = basename(filePath).replace(/\.[^.]+$/, "").trim() || "Markdown Document";
|
|
815
|
+
const markdown = readFileSync(filePath, "utf8");
|
|
816
|
+
const docx = this.client.docx;
|
|
817
|
+
const created = await docx.document.create({ data: { title: rawTitle } });
|
|
818
|
+
const documentId = created?.data?.document?.document_id || created?.document?.document_id;
|
|
819
|
+
const revisionId = created?.data?.document?.revision_id || created?.document?.revision_id;
|
|
820
|
+
if (!documentId)
|
|
821
|
+
throw new Error(`Feishu doc create returned no document_id for ${filePath}`);
|
|
822
|
+
const converted = await docx.document.convert({ data: { content_type: "markdown", content: markdown } });
|
|
823
|
+
const convertedData = converted?.data || converted;
|
|
824
|
+
const blocks = convertedData?.blocks || [];
|
|
825
|
+
const firstLevelBlockIds = convertedData?.first_level_block_ids || [];
|
|
826
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
827
|
+
await docx.documentBlockDescendant.create({
|
|
828
|
+
path: { document_id: documentId, block_id: documentId },
|
|
829
|
+
data: {
|
|
830
|
+
children_id: firstLevelBlockIds,
|
|
831
|
+
descendants: blocks,
|
|
832
|
+
document_revision_id: revisionId,
|
|
833
|
+
},
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return { title: rawTitle, url: `https://www.feishu.cn/docx/${documentId}` };
|
|
837
|
+
}
|
|
838
|
+
async sendBridgeAttachment(chatId, attachment) {
|
|
839
|
+
const filePath = this.validateBridgeAttachmentPath(attachment.path);
|
|
840
|
+
const type = attachment.type || (this.isImagePath(filePath) ? "image" : "file");
|
|
841
|
+
if (type === "document" && extname(filePath).toLowerCase() === ".md") {
|
|
842
|
+
const doc = await this.createFeishuDocFromMarkdown(filePath);
|
|
843
|
+
const caption = attachment.caption?.trim() || `飞书文档:${doc.title}`;
|
|
844
|
+
await this.sendMessage(chatId, `${caption}
|
|
845
|
+
${doc.url}`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (attachment.caption?.trim())
|
|
849
|
+
await this.sendMessage(chatId, attachment.caption.trim());
|
|
850
|
+
if (type === "image") {
|
|
851
|
+
if (statSync(filePath).size > 10 * 1024 * 1024)
|
|
852
|
+
throw new Error(`Image too large (>10MB): ${filePath}`);
|
|
853
|
+
const uploaded = await this.client.im.image.create({
|
|
854
|
+
data: { image_type: "message", image: readFileSync(filePath) },
|
|
855
|
+
});
|
|
856
|
+
const imageKey = uploaded?.image_key || uploaded?.data?.image_key;
|
|
857
|
+
if (!imageKey)
|
|
858
|
+
throw new Error(`Feishu image upload returned no image_key for ${filePath}`);
|
|
859
|
+
await this.client.im.message.create({
|
|
860
|
+
params: { receive_id_type: "chat_id" },
|
|
861
|
+
data: {
|
|
862
|
+
receive_id: chatId,
|
|
863
|
+
content: JSON.stringify({ image_key: imageKey }),
|
|
864
|
+
msg_type: "image",
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const uploaded = await this.client.im.file.create({
|
|
870
|
+
data: {
|
|
871
|
+
file_type: this.inferFeishuFileType(filePath),
|
|
872
|
+
file_name: basename(filePath),
|
|
873
|
+
file: readFileSync(filePath),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
const fileKey = uploaded?.file_key || uploaded?.data?.file_key;
|
|
877
|
+
if (!fileKey)
|
|
878
|
+
throw new Error(`Feishu file upload returned no file_key for ${filePath}`);
|
|
879
|
+
await this.client.im.message.create({
|
|
880
|
+
params: { receive_id_type: "chat_id" },
|
|
881
|
+
data: {
|
|
882
|
+
receive_id: chatId,
|
|
883
|
+
content: JSON.stringify({ file_key: fileKey }),
|
|
884
|
+
msg_type: "file",
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
}
|
|
737
888
|
/**
|
|
738
889
|
* Send a proactive message to a chat (not a reply).
|
|
739
890
|
*/
|
|
@@ -25,8 +25,9 @@ export declare class OpenClawClient {
|
|
|
25
25
|
private sessionMessageCallbacks;
|
|
26
26
|
/** Session keys that should be re-subscribed on reconnect */
|
|
27
27
|
private subscribedKeys;
|
|
28
|
-
/** Session keys with active chatSend — suppress proactive message delivery */
|
|
28
|
+
/** Session keys with active/recent chatSend — suppress proactive message delivery */
|
|
29
29
|
private suppressedSessions;
|
|
30
|
+
private suppressedSessionTimers;
|
|
30
31
|
constructor(config: OpenClawConfig);
|
|
31
32
|
connect(): Promise<void>;
|
|
32
33
|
private _doConnect;
|
|
@@ -68,6 +69,8 @@ export declare class OpenClawClient {
|
|
|
68
69
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
69
70
|
*/
|
|
70
71
|
abortChat(sessionKey: string, runId: string): Promise<any>;
|
|
72
|
+
private suppressSessionKeys;
|
|
73
|
+
private releaseSuppressedSessionKeysAfter;
|
|
71
74
|
chatSend(params: {
|
|
72
75
|
sessionKey: string;
|
|
73
76
|
message: string;
|
|
@@ -75,6 +78,8 @@ export declare class OpenClawClient {
|
|
|
75
78
|
deliver?: boolean;
|
|
76
79
|
timeoutMs?: number;
|
|
77
80
|
}): Promise<string>;
|
|
81
|
+
private shouldInjectBridgeAttachmentHint;
|
|
82
|
+
private bridgeAttachmentHint;
|
|
78
83
|
/**
|
|
79
84
|
* Build and send a context catch-up message followed by the actual message.
|
|
80
85
|
*
|
package/dist/openclaw-client.js
CHANGED
|
@@ -2,6 +2,8 @@ import WebSocket from "ws";
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { basename, extname } from "path";
|
|
5
|
+
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
6
|
+
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
5
7
|
/**
|
|
6
8
|
* OpenClaw Gateway WebSocket client.
|
|
7
9
|
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
@@ -21,8 +23,9 @@ export class OpenClawClient {
|
|
|
21
23
|
sessionMessageCallbacks = new Map();
|
|
22
24
|
/** Session keys that should be re-subscribed on reconnect */
|
|
23
25
|
subscribedKeys = new Set();
|
|
24
|
-
/** Session keys with active chatSend — suppress proactive message delivery */
|
|
26
|
+
/** Session keys with active/recent chatSend — suppress proactive message delivery */
|
|
25
27
|
suppressedSessions = new Set();
|
|
28
|
+
suppressedSessionTimers = new Map();
|
|
26
29
|
constructor(config) {
|
|
27
30
|
this.config = config;
|
|
28
31
|
}
|
|
@@ -149,11 +152,23 @@ export class OpenClawClient {
|
|
|
149
152
|
proactiveText = content;
|
|
150
153
|
}
|
|
151
154
|
else if (Array.isArray(content)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const hasToolBlock = content.some((part) => {
|
|
156
|
+
const type = String(part?.type || "").toLowerCase();
|
|
157
|
+
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use" || type === "toolresult" || type === "tool_result";
|
|
158
|
+
});
|
|
159
|
+
// Do not deliver mixed text+toolCall assistant messages through
|
|
160
|
+
// the proactive final-text path; those are usually intermediate
|
|
161
|
+
// reasoning/status during a tool loop. Tool calls are still
|
|
162
|
+
// delivered via the verbose channel from agent item events when
|
|
163
|
+
// /verbose is enabled. Cron final messages arrive as text-only
|
|
164
|
+
// (optionally with thinking).
|
|
165
|
+
if (!hasToolBlock) {
|
|
166
|
+
proactiveText = content
|
|
167
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
168
|
+
.map((part) => part.text)
|
|
169
|
+
.join("\n")
|
|
170
|
+
.trim();
|
|
171
|
+
}
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
if (proactiveText) {
|
|
@@ -438,11 +453,32 @@ export class OpenClawClient {
|
|
|
438
453
|
const key = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
|
|
439
454
|
return this.rpc("chat.abort", { sessionKey: key, runId }, 5000).catch(() => { });
|
|
440
455
|
}
|
|
456
|
+
suppressSessionKeys(keys) {
|
|
457
|
+
for (const key of keys) {
|
|
458
|
+
const timer = this.suppressedSessionTimers.get(key);
|
|
459
|
+
if (timer)
|
|
460
|
+
clearTimeout(timer);
|
|
461
|
+
this.suppressedSessionTimers.delete(key);
|
|
462
|
+
this.suppressedSessions.add(key);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
releaseSuppressedSessionKeysAfter(keys, delayMs) {
|
|
466
|
+
for (const key of keys) {
|
|
467
|
+
const oldTimer = this.suppressedSessionTimers.get(key);
|
|
468
|
+
if (oldTimer)
|
|
469
|
+
clearTimeout(oldTimer);
|
|
470
|
+
const timer = setTimeout(() => {
|
|
471
|
+
this.suppressedSessions.delete(key);
|
|
472
|
+
this.suppressedSessionTimers.delete(key);
|
|
473
|
+
}, delayMs);
|
|
474
|
+
this.suppressedSessionTimers.set(key, timer);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
441
477
|
async chatSend(params) {
|
|
442
478
|
const sk = params.sessionKey;
|
|
443
479
|
const fullSessionKey = `agent:main:${sk}`;
|
|
444
|
-
|
|
445
|
-
this.
|
|
480
|
+
const suppressedKeys = [sk, fullSessionKey];
|
|
481
|
+
this.suppressSessionKeys(suppressedKeys);
|
|
446
482
|
try {
|
|
447
483
|
// Drop stale buffered events for this session before starting a new run.
|
|
448
484
|
// This prevents an old final text (e.g. previous "ok") from being consumed by
|
|
@@ -461,10 +497,23 @@ export class OpenClawClient {
|
|
|
461
497
|
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk);
|
|
462
498
|
}
|
|
463
499
|
finally {
|
|
464
|
-
|
|
465
|
-
|
|
500
|
+
// OpenClaw can emit the final assistant session.message a moment after
|
|
501
|
+
// collectReply returns. Keep a short grace window so normal chat replies
|
|
502
|
+
// are not delivered twice via the proactive-message path. Cron/LMA runs
|
|
503
|
+
// are unaffected because they do not go through chatSend.
|
|
504
|
+
this.releaseSuppressedSessionKeysAfter(suppressedKeys, 30000);
|
|
466
505
|
}
|
|
467
506
|
}
|
|
507
|
+
shouldInjectBridgeAttachmentHint(text) {
|
|
508
|
+
return /发送|发到|发给|传|上传|附件|文件|文档|图片|图像|照片|生成图|做张图|画张图|导出|保存|pdf|docx?|xlsx?|pptx?|markdown|\bmd\b/i.test(text);
|
|
509
|
+
}
|
|
510
|
+
bridgeAttachmentHint(text) {
|
|
511
|
+
if (!this.shouldInjectBridgeAttachmentHint(text))
|
|
512
|
+
return "";
|
|
513
|
+
return `
|
|
514
|
+
|
|
515
|
+
[Bridge attachment capability hint: This is an OpenClaw Lark Multi-Agent bridge session. You cannot send Feishu files/images directly from inside OpenClaw. Do NOT call message, sessions_send, Feishu tools, or proactive send tools for this request. If the user asks you to send an image/file/document to Feishu, only create the local file under ${BRIDGE_ATTACHMENTS_DIR}/ and include this exact marker at the very end of your final reply (do not explain or expose the marker as normal text): <LMA_BRIDGE_ATTACHMENTS>{"attachments":[{"type":"image|file|document","path":"/absolute/path","caption":"optional"}]}</LMA_BRIDGE_ATTACHMENTS>. The bridge layer will parse this marker and send the attachment. Use type=image for images; use type=document for Markdown documents (.md) so the bridge creates a Feishu cloud document and sends its link; use type=file for other ordinary files.]`;
|
|
516
|
+
}
|
|
468
517
|
/**
|
|
469
518
|
* Build and send a context catch-up message followed by the actual message.
|
|
470
519
|
*
|
|
@@ -486,11 +535,12 @@ export class OpenClawClient {
|
|
|
486
535
|
const mediaInstruction = attachments.length > 0
|
|
487
536
|
? "\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.]"
|
|
488
537
|
: "";
|
|
538
|
+
const bridgeAttachmentHint = this.bridgeAttachmentHint(params.currentMessage);
|
|
489
539
|
if (params.unsyncedMessages.length === 0) {
|
|
490
540
|
// No context to catch up, send directly
|
|
491
541
|
return this.chatSend({
|
|
492
542
|
sessionKey: params.sessionKey,
|
|
493
|
-
message: params.currentMessage + mediaInstruction,
|
|
543
|
+
message: params.currentMessage + mediaInstruction + bridgeAttachmentHint,
|
|
494
544
|
attachments,
|
|
495
545
|
deliver: params.deliver,
|
|
496
546
|
timeoutMs: params.timeoutMs,
|
|
@@ -512,7 +562,7 @@ export class OpenClawClient {
|
|
|
512
562
|
`[${params.currentSenderName}]: ${params.currentMessage}`;
|
|
513
563
|
return this.chatSend({
|
|
514
564
|
sessionKey: params.sessionKey,
|
|
515
|
-
message: combined + mediaInstruction,
|
|
565
|
+
message: combined + mediaInstruction + bridgeAttachmentHint,
|
|
516
566
|
attachments,
|
|
517
567
|
deliver: params.deliver,
|
|
518
568
|
timeoutMs: params.timeoutMs,
|
|
@@ -547,6 +597,10 @@ export class OpenClawClient {
|
|
|
547
597
|
}
|
|
548
598
|
async disconnect() {
|
|
549
599
|
this.shouldReconnect = false;
|
|
600
|
+
for (const timer of this.suppressedSessionTimers.values())
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
this.suppressedSessionTimers.clear();
|
|
603
|
+
this.suppressedSessions.clear();
|
|
550
604
|
if (this.ws) {
|
|
551
605
|
this.ws.close();
|
|
552
606
|
this.ws = null;
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime state defaults to the user's home directory but can be overridden
|
|
3
|
+
* for packaged deployments, tests, containers, or systemd installations.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getStateDir(): string;
|
|
6
|
+
export declare function getBridgeAttachmentsDir(): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Runtime state defaults to the user's home directory but can be overridden
|
|
5
|
+
* for packaged deployments, tests, containers, or systemd installations.
|
|
6
|
+
*/
|
|
7
|
+
export function getStateDir() {
|
|
8
|
+
return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_STATE_DIR || resolve(homedir(), ".openclaw/openclaw-lark-multi-agent"));
|
|
9
|
+
}
|
|
10
|
+
export function getBridgeAttachmentsDir() {
|
|
11
|
+
return resolve(process.env.OPENCLAW_LARK_MULTI_AGENT_ATTACHMENTS_DIR || resolve(getStateDir(), "attachments"));
|
|
12
|
+
}
|