openclaw-lark-multi-agent 0.1.8 → 0.1.9

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.
@@ -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
  /**
@@ -763,12 +764,7 @@ export class FeishuBot {
763
764
  schema: "2.0",
764
765
  config: { wide_screen_mode: true },
765
766
  body: {
766
- elements: [
767
- {
768
- tag: "markdown",
769
- content: text,
770
- },
771
- ],
767
+ elements: buildFeishuCardElements(text),
772
768
  },
773
769
  };
774
770
  }
@@ -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 {};
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {