openclaw-lark-multi-agent 0.1.8 → 0.1.10
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 +19 -12
- package/dist/markdown.d.ts +31 -0
- package/dist/markdown.js +203 -0
- package/dist/message-store.d.ts +6 -2
- package/dist/message-store.js +11 -7
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
package/dist/feishu-bot.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as lark from "@larksuiteoapi/node-sdk";
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { basename, extname, resolve } from "path";
|
|
4
4
|
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
5
|
+
import { buildFeishuCardElements } from "./markdown.js";
|
|
5
6
|
const MAX_BOT_STREAK = 10;
|
|
6
7
|
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
7
8
|
/**
|
|
@@ -263,6 +264,12 @@ export class FeishuBot {
|
|
|
263
264
|
if (messageType === "text") {
|
|
264
265
|
const rawText = content.text || "";
|
|
265
266
|
cleanText = this.cleanMentions(rawText);
|
|
267
|
+
// A mention-only text message is still a valid routing trigger. Feishu may
|
|
268
|
+
// expose mentions as display text like "@万万(Claude)" rather than @_user_xxx,
|
|
269
|
+
// so decide emptiness after stripping leading routing mentions.
|
|
270
|
+
if ((this.isMentioned(message.mentions || []) || this.isAllMention(rawText, message.mentions || [])) && !this.stripLeadingCommandMentions(cleanText).trim()) {
|
|
271
|
+
cleanText = "请回复上面最近一条用户消息。";
|
|
272
|
+
}
|
|
266
273
|
}
|
|
267
274
|
else if (messageType === "image") {
|
|
268
275
|
// Download image and pass local path
|
|
@@ -494,7 +501,7 @@ export class FeishuBot {
|
|
|
494
501
|
// Track this message for reaction status updates
|
|
495
502
|
const pending = this.pendingAckMessages.get(chatId) || [];
|
|
496
503
|
// Anti-loop
|
|
497
|
-
const streak = this.store.getBotStreak(chatId);
|
|
504
|
+
const streak = this.store.getBotStreak(chatId, this.config.name);
|
|
498
505
|
if (streak >= MAX_BOT_STREAK) {
|
|
499
506
|
console.log(`[${this.config.name}] Anti-loop: ${streak} consecutive bot msgs`);
|
|
500
507
|
return;
|
|
@@ -693,9 +700,11 @@ export class FeishuBot {
|
|
|
693
700
|
// Check if this bot is explicitly mentioned
|
|
694
701
|
if (this.isMentioned(mentions))
|
|
695
702
|
return true;
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
// Targeted mentions are exclusive. If a human mentions another person or
|
|
704
|
+
// another bot, free-mode bots must not steal that message. Free mode only
|
|
705
|
+
// applies to plain human messages with no targeted mentions.
|
|
706
|
+
const hasTargetedMention = mentions.some((m) => !this.isAllMentionItem(m));
|
|
707
|
+
if (hasTargetedMention)
|
|
699
708
|
return false;
|
|
700
709
|
// No bot mentioned: check current per-bot mode
|
|
701
710
|
if (chatId) {
|
|
@@ -711,10 +720,13 @@ export class FeishuBot {
|
|
|
711
720
|
isAllMention(rawText, mentions = []) {
|
|
712
721
|
if (rawText && (rawText.includes("@_all") || rawText.includes("@all") || rawText.includes("@所有人")))
|
|
713
722
|
return true;
|
|
714
|
-
return mentions.some((m) =>
|
|
723
|
+
return mentions.some((m) => this.isAllMentionItem(m));
|
|
724
|
+
}
|
|
725
|
+
isAllMentionItem(mention) {
|
|
726
|
+
return mention.key === "all" || mention.key === "@_all" || mention.id?.user_id === "all" || mention.id?.open_id === "all" || mention.name === "所有人";
|
|
715
727
|
}
|
|
716
728
|
mentionedBotName(mention) {
|
|
717
|
-
if (
|
|
729
|
+
if (this.isAllMentionItem(mention))
|
|
718
730
|
return null;
|
|
719
731
|
const candidates = [this, ...Array.from(FeishuBot.allBots.values()).filter((bot) => bot !== this)];
|
|
720
732
|
for (const bot of candidates) {
|
|
@@ -763,12 +775,7 @@ export class FeishuBot {
|
|
|
763
775
|
schema: "2.0",
|
|
764
776
|
config: { wide_screen_mode: true },
|
|
765
777
|
body: {
|
|
766
|
-
elements:
|
|
767
|
-
{
|
|
768
|
-
tag: "markdown",
|
|
769
|
-
content: text,
|
|
770
|
-
},
|
|
771
|
-
],
|
|
778
|
+
elements: buildFeishuCardElements(text),
|
|
772
779
|
},
|
|
773
780
|
};
|
|
774
781
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare function optimizeMarkdownStyle(text: string, cardVersion?: number): string;
|
|
2
|
+
declare function convertMarkdownTables(markdown: string): string;
|
|
3
|
+
type FeishuMarkdownElement = {
|
|
4
|
+
tag: "markdown";
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
type FeishuTableElement = {
|
|
8
|
+
tag: "table";
|
|
9
|
+
page_size?: number;
|
|
10
|
+
row_height?: "low";
|
|
11
|
+
header_style?: Record<string, unknown>;
|
|
12
|
+
columns: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
display_name: string;
|
|
15
|
+
data_type: "lark_md";
|
|
16
|
+
width?: string;
|
|
17
|
+
vertical_align?: "top" | "center" | "bottom";
|
|
18
|
+
horizontal_align?: "left" | "center" | "right";
|
|
19
|
+
}>;
|
|
20
|
+
rows: Array<Record<string, string>>;
|
|
21
|
+
};
|
|
22
|
+
export type FeishuCardElement = FeishuMarkdownElement | FeishuTableElement;
|
|
23
|
+
declare function buildTableElement(lines: string[], index: number): FeishuTableElement | null;
|
|
24
|
+
export declare function buildFeishuCardElements(markdown: string): FeishuCardElement[];
|
|
25
|
+
export declare function prepareMarkdownForFeishu(text: string): string;
|
|
26
|
+
export declare const __test__: {
|
|
27
|
+
convertMarkdownTables: typeof convertMarkdownTables;
|
|
28
|
+
optimizeMarkdownStyle: typeof optimizeMarkdownStyle;
|
|
29
|
+
buildTableElement: typeof buildTableElement;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const MARKDOWN_STYLE_MARKERS = {
|
|
2
|
+
bold: { open: "**", close: "**" },
|
|
3
|
+
italic: { open: "_", close: "_" },
|
|
4
|
+
strikethrough: { open: "~~", close: "~~" },
|
|
5
|
+
code: { open: "`", close: "`" },
|
|
6
|
+
code_block: { open: "```\n", close: "```" },
|
|
7
|
+
};
|
|
8
|
+
const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
9
|
+
function protectCodeBlocks(text) {
|
|
10
|
+
const blocks = [];
|
|
11
|
+
const protectedText = text.replace(/(^|\n)(`{3,})([^\n]*)\n[\s\S]*?\n\2(?=\n|$)/g, (match, prefix = "") => {
|
|
12
|
+
const block = match.slice(String(prefix).length);
|
|
13
|
+
const token = `___LMA_CB_${blocks.length}___`;
|
|
14
|
+
blocks.push({ token, text: block });
|
|
15
|
+
return `${prefix}${token}`;
|
|
16
|
+
});
|
|
17
|
+
return { text: protectedText, blocks };
|
|
18
|
+
}
|
|
19
|
+
function restoreCodeBlocks(text, blocks, cardVersion = 2) {
|
|
20
|
+
let restored = text;
|
|
21
|
+
for (const { token, text: block } of blocks) {
|
|
22
|
+
restored = restored.replace(token, cardVersion >= 2 ? `\n<br>\n${block}\n<br>\n` : block);
|
|
23
|
+
}
|
|
24
|
+
return restored;
|
|
25
|
+
}
|
|
26
|
+
function stripInvalidImageKeys(text) {
|
|
27
|
+
if (!text.includes("!["))
|
|
28
|
+
return text;
|
|
29
|
+
return text.replace(IMAGE_RE, (fullMatch, _alt, value) => {
|
|
30
|
+
if (String(value).startsWith("img_"))
|
|
31
|
+
return fullMatch;
|
|
32
|
+
return "";
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function optimizeMarkdownStyle(text, cardVersion = 2) {
|
|
36
|
+
try {
|
|
37
|
+
const protectedResult = protectCodeBlocks(text);
|
|
38
|
+
let r = protectedResult.text;
|
|
39
|
+
const hasH1toH3 = /^#{1,3} /m.test(text);
|
|
40
|
+
if (hasH1toH3) {
|
|
41
|
+
r = r.replace(/^#{2,6} (.+)$/gm, "##### $1");
|
|
42
|
+
r = r.replace(/^# (.+)$/gm, "#### $1");
|
|
43
|
+
}
|
|
44
|
+
if (cardVersion >= 2) {
|
|
45
|
+
r = r.replace(/^(#{4,5} .+)\n{1,2}(#{4,5} )/gm, "$1\n<br>\n$2");
|
|
46
|
+
r = r.replace(/^([^|\n].*)\n(\|.+\|)/gm, "$1\n\n$2");
|
|
47
|
+
r = r.replace(/\n\n((?:\|.+\|[^\S\n]*\n?)+)/g, "\n\n<br>\n\n$1");
|
|
48
|
+
r = r.replace(/((?:^\|.+\|[^\S\n]*\n?)+)/gm, (match, _table, offset) => {
|
|
49
|
+
const after = r.slice(offset + match.length).replace(/^\n+/, "");
|
|
50
|
+
if (!after || /^(---|#{4,5} |\*\*)/.test(after))
|
|
51
|
+
return match;
|
|
52
|
+
return `${match}\n<br>\n`;
|
|
53
|
+
});
|
|
54
|
+
r = r.replace(/^((?!#{4,5} )(?!\*\*).+)\n\n(<br>)\n\n(\|)/gm, "$1\n$2\n$3");
|
|
55
|
+
r = r.replace(/^(\*\*.+)\n\n(<br>)\n\n(\|)/gm, "$1\n$2\n\n$3");
|
|
56
|
+
r = r.replace(/(\|[^\n]*\n)\n(<br>\n)((?!#{4,5} )(?!\*\*))/gm, "$1$2$3");
|
|
57
|
+
}
|
|
58
|
+
r = restoreCodeBlocks(r, protectedResult.blocks, cardVersion);
|
|
59
|
+
r = r.replace(/\n{3,}/g, "\n\n");
|
|
60
|
+
return stripInvalidImageKeys(r);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isMarkdownTableSeparator(line) {
|
|
67
|
+
const trimmed = line.trim();
|
|
68
|
+
if (!trimmed.includes("|"))
|
|
69
|
+
return false;
|
|
70
|
+
const cells = trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
71
|
+
return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
72
|
+
}
|
|
73
|
+
function isMarkdownTableRow(line) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
return trimmed.includes("|") && trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|").length >= 2;
|
|
76
|
+
}
|
|
77
|
+
function splitMarkdownTableRow(line) {
|
|
78
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
79
|
+
}
|
|
80
|
+
function formatTableAsCodeBlock(lines) {
|
|
81
|
+
const rows = lines.filter((line) => !isMarkdownTableSeparator(line)).map(splitMarkdownTableRow);
|
|
82
|
+
if (rows.length === 0)
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
const width = Math.max(...rows.map((row) => row.length));
|
|
85
|
+
const widths = Array.from({ length: width }, (_, i) => Math.max(...rows.map((row) => row[i]?.length ?? 0), 1));
|
|
86
|
+
const renderedRows = rows.map((row, rowIndex) => {
|
|
87
|
+
const rendered = widths.map((w, i) => (row[i] ?? "").padEnd(w)).join(" ").trimEnd();
|
|
88
|
+
if (rowIndex === 0 && rows.length > 1) {
|
|
89
|
+
const sep = widths.map((w) => "─".repeat(w)).join(" ");
|
|
90
|
+
return `${rendered}\n${sep}`;
|
|
91
|
+
}
|
|
92
|
+
return rendered;
|
|
93
|
+
});
|
|
94
|
+
return `\n\`\`\`\n${renderedRows.join("\n")}\n\`\`\`\n`;
|
|
95
|
+
}
|
|
96
|
+
function convertMarkdownTables(markdown) {
|
|
97
|
+
const { text, blocks } = protectCodeBlocks(markdown);
|
|
98
|
+
const lines = text.split("\n");
|
|
99
|
+
const out = [];
|
|
100
|
+
let i = 0;
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
if (i + 1 < lines.length && isMarkdownTableRow(lines[i]) && isMarkdownTableSeparator(lines[i + 1])) {
|
|
103
|
+
const tableLines = [lines[i], lines[i + 1]];
|
|
104
|
+
i += 2;
|
|
105
|
+
while (i < lines.length && isMarkdownTableRow(lines[i])) {
|
|
106
|
+
tableLines.push(lines[i]);
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
out.push(formatTableAsCodeBlock(tableLines));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out.push(lines[i]);
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
return restoreCodeBlocks(out.join("\n"), blocks, 1);
|
|
116
|
+
}
|
|
117
|
+
function buildTableElement(lines, index) {
|
|
118
|
+
const rows = lines.filter((line) => !isMarkdownTableSeparator(line)).map(splitMarkdownTableRow);
|
|
119
|
+
if (rows.length < 2)
|
|
120
|
+
return null;
|
|
121
|
+
const headers = rows[0];
|
|
122
|
+
if (headers.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
const width = Math.min(Math.max(...rows.map((row) => row.length)), 50);
|
|
125
|
+
const columns = Array.from({ length: width }, (_, i) => ({
|
|
126
|
+
name: `c${index}_${i}`,
|
|
127
|
+
display_name: headers[i] || `列 ${i + 1}`,
|
|
128
|
+
data_type: "lark_md",
|
|
129
|
+
width: "auto",
|
|
130
|
+
vertical_align: "top",
|
|
131
|
+
horizontal_align: "left",
|
|
132
|
+
}));
|
|
133
|
+
const dataRows = rows.slice(1).map((row) => {
|
|
134
|
+
const item = {};
|
|
135
|
+
for (let i = 0; i < columns.length; i++)
|
|
136
|
+
item[columns[i].name] = row[i] || "";
|
|
137
|
+
return item;
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
tag: "table",
|
|
141
|
+
page_size: Math.min(Math.max(dataRows.length, 1), 10),
|
|
142
|
+
header_style: {
|
|
143
|
+
text_align: "left",
|
|
144
|
+
text_size: "normal",
|
|
145
|
+
background_style: "grey",
|
|
146
|
+
text_color: "default",
|
|
147
|
+
bold: true,
|
|
148
|
+
lines: 2,
|
|
149
|
+
},
|
|
150
|
+
columns,
|
|
151
|
+
rows: dataRows,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function pushMarkdownElement(elements, text) {
|
|
155
|
+
const content = optimizeMarkdownStyle(text.trim(), 2).trim();
|
|
156
|
+
if (!content)
|
|
157
|
+
return;
|
|
158
|
+
elements.push({ tag: "markdown", content });
|
|
159
|
+
}
|
|
160
|
+
export function buildFeishuCardElements(markdown) {
|
|
161
|
+
const { text, blocks } = protectCodeBlocks(markdown);
|
|
162
|
+
const lines = text.split("\n");
|
|
163
|
+
const elements = [];
|
|
164
|
+
const buffer = [];
|
|
165
|
+
let tableCount = 0;
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < lines.length) {
|
|
168
|
+
if (i + 1 < lines.length && isMarkdownTableRow(lines[i]) && isMarkdownTableSeparator(lines[i + 1])) {
|
|
169
|
+
const tableLines = [lines[i], lines[i + 1]];
|
|
170
|
+
i += 2;
|
|
171
|
+
while (i < lines.length && isMarkdownTableRow(lines[i])) {
|
|
172
|
+
tableLines.push(lines[i]);
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
pushMarkdownElement(elements, restoreCodeBlocks(buffer.join("\n"), blocks, 1));
|
|
176
|
+
buffer.length = 0;
|
|
177
|
+
// Feishu cards support up to 5 table components per card. Fall back to a
|
|
178
|
+
// readable code-block table after that instead of sending an invalid card.
|
|
179
|
+
if (tableCount < 5) {
|
|
180
|
+
const table = buildTableElement(tableLines, tableCount);
|
|
181
|
+
if (table) {
|
|
182
|
+
elements.push(table);
|
|
183
|
+
tableCount++;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
buffer.push(formatTableAsCodeBlock(tableLines));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
buffer.push(formatTableAsCodeBlock(tableLines));
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
buffer.push(lines[i]);
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
pushMarkdownElement(elements, restoreCodeBlocks(buffer.join("\n"), blocks, 1));
|
|
198
|
+
return elements.length > 0 ? elements : [{ tag: "markdown", content: "" }];
|
|
199
|
+
}
|
|
200
|
+
export function prepareMarkdownForFeishu(text) {
|
|
201
|
+
return optimizeMarkdownStyle(convertMarkdownTables(text), 2);
|
|
202
|
+
}
|
|
203
|
+
export const __test__ = { convertMarkdownTables, optimizeMarkdownStyle, buildTableElement };
|
package/dist/message-store.d.ts
CHANGED
|
@@ -51,9 +51,13 @@ export declare class MessageStore {
|
|
|
51
51
|
*/
|
|
52
52
|
getRecent(chatId: string, maxCount?: number): ChatMessage[];
|
|
53
53
|
/**
|
|
54
|
-
* Count consecutive bot
|
|
54
|
+
* Count consecutive messages from one bot at the tail of a chat.
|
|
55
|
+
*
|
|
56
|
+
* Other bots do not consume this bot's anti-loop budget. Human messages reset
|
|
57
|
+
* the streak. This lets multiple bots free-discuss without a global bot-streak
|
|
58
|
+
* guard shutting everyone down after N total bot messages.
|
|
55
59
|
*/
|
|
56
|
-
getBotStreak(chatId: string): number;
|
|
60
|
+
getBotStreak(chatId: string, botName: string): number;
|
|
57
61
|
upsertChatInfo(info: ChatInfo): void;
|
|
58
62
|
setFreeDiscussion(chatId: string, on: boolean): void;
|
|
59
63
|
setVerbose(chatId: string, verbose: boolean): void;
|
package/dist/message-store.js
CHANGED
|
@@ -233,21 +233,25 @@ export class MessageStore {
|
|
|
233
233
|
}));
|
|
234
234
|
}
|
|
235
235
|
/**
|
|
236
|
-
* Count consecutive bot
|
|
236
|
+
* Count consecutive messages from one bot at the tail of a chat.
|
|
237
|
+
*
|
|
238
|
+
* Other bots do not consume this bot's anti-loop budget. Human messages reset
|
|
239
|
+
* the streak. This lets multiple bots free-discuss without a global bot-streak
|
|
240
|
+
* guard shutting everyone down after N total bot messages.
|
|
237
241
|
*/
|
|
238
|
-
getBotStreak(chatId) {
|
|
242
|
+
getBotStreak(chatId, botName) {
|
|
239
243
|
const rows = this.db.prepare(`
|
|
240
|
-
SELECT sender_type FROM messages
|
|
244
|
+
SELECT sender_type, sender_name FROM messages
|
|
241
245
|
WHERE chat_id = ?
|
|
242
246
|
ORDER BY timestamp DESC
|
|
243
|
-
LIMIT
|
|
247
|
+
LIMIT 50
|
|
244
248
|
`).all(chatId);
|
|
245
249
|
let count = 0;
|
|
246
250
|
for (const r of rows) {
|
|
247
|
-
if (r.sender_type === "
|
|
248
|
-
count++;
|
|
249
|
-
else
|
|
251
|
+
if (r.sender_type === "human")
|
|
250
252
|
break;
|
|
253
|
+
if (r.sender_type === "bot" && r.sender_name === botName)
|
|
254
|
+
count++;
|
|
251
255
|
}
|
|
252
256
|
return count;
|
|
253
257
|
}
|