@superbenxxxh/feishu 1.0.0 → 2.0.0

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/README.md CHANGED
@@ -93,17 +93,18 @@ channels:
93
93
  requireMention: true
94
94
  # Max media size in MB (default: 30)
95
95
  mediaMaxMb: 30
96
- # Render mode for bot replies: "auto" | "raw" | "card"
97
- renderMode: "auto"
96
+ # Render mode for bot replies: "post" | "auto" | "raw" | "card"
97
+ renderMode: "post"
98
98
  ```
99
99
 
100
100
  #### Render Mode
101
101
 
102
- | Mode | Description |
103
- |------|-------------|
104
- | `auto` | (Default) Automatically detect: use card for messages with code blocks or tables, plain text otherwise. |
105
- | `raw` | Always send replies as plain text. Markdown tables are converted to ASCII. |
106
- | `card` | Always send replies as interactive cards with full markdown rendering (syntax highlighting, tables, clickable links). |
102
+ | Mode | Description |
103
+ |------|-------------|
104
+ | `post` | (Default) Send replies as rich text posts using `msg_type=post` + `tag=md`. |
105
+ | `auto` | Automatically detect: use card for messages with code blocks or tables, plain text otherwise. |
106
+ | `raw` | Always send replies as plain text. Markdown tables are converted to ASCII. |
107
+ | `card` | Always send replies as interactive cards with full markdown rendering (syntax highlighting, tables, clickable links). |
107
108
 
108
109
  ### Features
109
110
 
@@ -256,17 +257,18 @@ channels:
256
257
  requireMention: true
257
258
  # 媒体文件最大大小 (MB, 默认 30)
258
259
  mediaMaxMb: 30
259
- # 回复渲染模式: "auto" | "raw" | "card"
260
- renderMode: "auto"
260
+ # 回复渲染模式: "post" | "auto" | "raw" | "card"
261
+ renderMode: "post"
261
262
  ```
262
263
 
263
264
  #### 渲染模式
264
265
 
265
- | 模式 | 说明 |
266
- |------|------|
267
- | `auto` | (默认)自动检测:有代码块或表格时用卡片,否则纯文本 |
268
- | `raw` | 始终纯文本,表格转为 ASCII |
269
- | `card` | 始终使用卡片,支持语法高亮、表格、链接等 |
266
+ | 模式 | 说明 |
267
+ |------|------|
268
+ | `post` | (默认)使用富文本 post(`msg_type=post` + `tag=md`)。 |
269
+ | `auto` | 自动检测:有代码块或表格时用卡片,否则纯文本 |
270
+ | `raw` | 始终纯文本,表格转为 ASCII |
271
+ | `card` | 始终使用卡片,支持语法高亮、表格、链接等 |
270
272
 
271
273
  ### 功能
272
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superbenxxxh/feishu",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "order": 70
40
40
  },
41
41
  "install": {
42
- "npmSpec": "@m1heng-clawd/feishu",
42
+ "npmSpec": "@superbenxxxh/feishu",
43
43
  "localPath": ".",
44
44
  "defaultChoice": "npm"
45
45
  }
package/src/bot.ts CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  type HistoryEntry,
8
8
  } from "openclaw/plugin-sdk";
9
9
  import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
10
- import { getFeishuRuntime } from "./runtime.js";
11
- import { createFeishuClient } from "./client.js";
10
+ import { getFeishuRuntime } from "./runtime.js";
11
+ import { createFeishuClient } from "./client.js";
12
12
  import {
13
13
  resolveFeishuGroupConfig,
14
14
  resolveFeishuReplyPolicy,
@@ -18,11 +18,12 @@ import {
18
18
  import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
19
19
  import { getMessageFeishu } from "./send.js";
20
20
  import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
21
- import {
22
- extractMentionTargets,
23
- extractMessageBody,
24
- isMentionForwardRequest,
25
- } from "./mention.js";
21
+ import {
22
+ extractMentionTargets,
23
+ extractMessageBody,
24
+ isMentionForwardRequest,
25
+ } from "./mention.js";
26
+ import { parsePostContent } from "./post.js";
26
27
 
27
28
  // --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
28
29
  // Cache display names by open_id to avoid an API call on every message.
@@ -183,49 +184,9 @@ function parseMediaKeys(
183
184
  * Parse post (rich text) content and extract embedded image keys.
184
185
  * Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] }
185
186
  */
186
- function parsePostContent(content: string): {
187
- textContent: string;
188
- imageKeys: string[];
189
- } {
190
- try {
191
- const parsed = JSON.parse(content);
192
- const title = parsed.title || "";
193
- const contentBlocks = parsed.content || [];
194
- let textContent = title ? `${title}\n\n` : "";
195
- const imageKeys: string[] = [];
196
-
197
- for (const paragraph of contentBlocks) {
198
- if (Array.isArray(paragraph)) {
199
- for (const element of paragraph) {
200
- if (element.tag === "text") {
201
- textContent += element.text || "";
202
- } else if (element.tag === "a") {
203
- // Link: show text or href
204
- textContent += element.text || element.href || "";
205
- } else if (element.tag === "at") {
206
- // Mention: @username
207
- textContent += `@${element.user_name || element.user_id || ""}`;
208
- } else if (element.tag === "img" && element.image_key) {
209
- // Embedded image
210
- imageKeys.push(element.image_key);
211
- }
212
- }
213
- textContent += "\n";
214
- }
215
- }
216
-
217
- return {
218
- textContent: textContent.trim() || "[富文本消息]",
219
- imageKeys,
220
- };
221
- } catch {
222
- return { textContent: "[富文本消息]", imageKeys: [] };
223
- }
224
- }
225
-
226
- /**
227
- * Infer placeholder text based on message type.
228
- */
187
+ /**
188
+ * Infer placeholder text based on message type.
189
+ */
229
190
  function inferPlaceholder(messageType: string): string {
230
191
  switch (messageType) {
231
192
  case "image":
package/src/channel.ts CHANGED
@@ -85,10 +85,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
85
85
  textChunkLimit: { type: "integer", minimum: 1 },
86
86
  chunkMode: { type: "string", enum: ["length", "newline"] },
87
87
  mediaMaxMb: { type: "number", minimum: 0 },
88
- renderMode: { type: "string", enum: ["auto", "raw", "card"] },
89
- },
90
- },
91
- },
88
+ renderMode: { type: "string", enum: ["auto", "raw", "post", "card"] },
89
+ },
90
+ },
91
+ },
92
92
  config: {
93
93
  listAccountIds: () => [DEFAULT_ACCOUNT_ID],
94
94
  resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
@@ -30,8 +30,8 @@ const MarkdownConfigSchema = z
30
30
  .strict()
31
31
  .optional();
32
32
 
33
- // Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
34
- const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
33
+ // Message render mode: post (default) = rich text, auto = detect markdown, raw = plain text, card = interactive card
34
+ const RenderModeSchema = z.enum(["auto", "raw", "post", "card"]).optional();
35
35
 
36
36
  const BlockStreamingCoalesceSchema = z
37
37
  .object({
@@ -89,8 +89,8 @@ export const FeishuConfigSchema = z
89
89
  blockStreamingCoalesce: BlockStreamingCoalesceSchema,
90
90
  mediaMaxMb: z.number().positive().optional(),
91
91
  heartbeat: ChannelHeartbeatVisibilitySchema,
92
- renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
93
- })
92
+ renderMode: RenderModeSchema, // post = rich text (default), raw = plain text, card = interactive card
93
+ })
94
94
  .strict()
95
95
  .superRefine((value, ctx) => {
96
96
  if (value.dmPolicy === "open") {
package/src/post.ts ADDED
@@ -0,0 +1,129 @@
1
+ import type { MentionTarget } from "./mention.js";
2
+
3
+ type PostElement = {
4
+ tag: "text" | "a" | "at" | "img" | "md";
5
+ text?: string;
6
+ href?: string;
7
+ user_id?: string;
8
+ user_name?: string;
9
+ image_key?: string;
10
+ };
11
+
12
+ type PostLocalePayload = {
13
+ title?: string;
14
+ content?: PostElement[][];
15
+ };
16
+
17
+ type PostPayload = {
18
+ zh_cn?: PostLocalePayload;
19
+ en_us?: PostLocalePayload;
20
+ post?: {
21
+ zh_cn?: PostLocalePayload;
22
+ en_us?: PostLocalePayload;
23
+ };
24
+ title?: string;
25
+ content?: PostElement[][];
26
+ };
27
+
28
+ function resolvePostPayload(parsed: PostPayload): PostLocalePayload {
29
+ return (
30
+ parsed.zh_cn ||
31
+ parsed.en_us ||
32
+ parsed.post?.zh_cn ||
33
+ parsed.post?.en_us ||
34
+ {
35
+ title: parsed.title,
36
+ content: parsed.content,
37
+ }
38
+ );
39
+ }
40
+
41
+ function buildPostElements(messageText: string, mentions?: MentionTarget[]): PostElement[][] {
42
+ const elements: PostElement[] = [];
43
+
44
+ if (mentions && mentions.length > 0) {
45
+ for (const mention of mentions) {
46
+ elements.push({
47
+ tag: "at",
48
+ user_id: mention.openId,
49
+ user_name: mention.name,
50
+ });
51
+ elements.push({ tag: "text", text: " " });
52
+ }
53
+ }
54
+
55
+ elements.push({
56
+ tag: "md",
57
+ text: messageText,
58
+ });
59
+
60
+ return [elements];
61
+ }
62
+
63
+ export function buildFeishuPostMessagePayload(params: {
64
+ messageText: string;
65
+ mentions?: MentionTarget[];
66
+ }): {
67
+ content: string;
68
+ msgType: "post";
69
+ } {
70
+ const { messageText, mentions } = params;
71
+ const contentBlocks = buildPostElements(messageText, mentions);
72
+
73
+ const payload = {
74
+ zh_cn: { content: contentBlocks },
75
+ en_us: { content: contentBlocks },
76
+ };
77
+
78
+ return {
79
+ content: JSON.stringify(payload),
80
+ msgType: "post",
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Parse post (rich text) content and extract embedded image keys.
86
+ * Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] }
87
+ */
88
+ export function parsePostContent(content: string): {
89
+ textContent: string;
90
+ imageKeys: string[];
91
+ } {
92
+ try {
93
+ const parsed = JSON.parse(content) as PostPayload;
94
+ const resolved = resolvePostPayload(parsed);
95
+ const title = resolved.title || "";
96
+ const contentBlocks = resolved.content || [];
97
+ let textContent = title ? `${title}\n\n` : "";
98
+ const imageKeys: string[] = [];
99
+
100
+ for (const paragraph of contentBlocks) {
101
+ if (Array.isArray(paragraph)) {
102
+ for (const element of paragraph) {
103
+ if (element.tag === "text") {
104
+ textContent += element.text || "";
105
+ } else if (element.tag === "a") {
106
+ // Link: show text or href
107
+ textContent += element.text || element.href || "";
108
+ } else if (element.tag === "at") {
109
+ // Mention: @username
110
+ textContent += `@${element.user_name || element.user_id || ""}`;
111
+ } else if (element.tag === "img" && element.image_key) {
112
+ // Embedded image
113
+ imageKeys.push(element.image_key);
114
+ } else if (element.tag === "md") {
115
+ textContent += element.text || "";
116
+ }
117
+ }
118
+ textContent += "\n";
119
+ }
120
+ }
121
+
122
+ return {
123
+ textContent: textContent.trim() || "[富文本消息]",
124
+ imageKeys,
125
+ };
126
+ } catch {
127
+ return { textContent: "[富文本消息]", imageKeys: [] };
128
+ }
129
+ }
@@ -106,13 +106,14 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
106
106
  return;
107
107
  }
108
108
 
109
- // Check render mode: auto (default), raw, or card
110
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
111
- const renderMode = feishuCfg?.renderMode ?? "auto";
112
-
113
- // Determine if we should use card for this message
114
- const useCard =
115
- renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
109
+ // Check render mode: post (default), auto, raw, or card
110
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
111
+ const renderMode = feishuCfg?.renderMode ?? "post";
112
+
113
+ // Determine if we should use card for this message
114
+ const useCard =
115
+ renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
116
+ const usePost = renderMode === "post";
116
117
 
117
118
  // Only include @mentions in the first chunk (avoid duplicate @s)
118
119
  let isFirstChunk = true;
@@ -131,22 +132,25 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
131
132
  });
132
133
  isFirstChunk = false;
133
134
  }
134
- } else {
135
- // Raw mode: send as plain text with table conversion
136
- const converted = core.channel.text.convertMarkdownTables(text, tableMode);
137
- const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
138
- params.runtime.log?.(`feishu deliver: sending ${chunks.length} text chunks to ${chatId}`);
139
- for (const chunk of chunks) {
140
- await sendMessageFeishu({
141
- cfg,
142
- to: chatId,
143
- text: chunk,
144
- replyToMessageId,
145
- mentions: isFirstChunk ? mentionTargets : undefined,
146
- });
147
- isFirstChunk = false;
148
- }
149
- }
135
+ } else {
136
+ // Raw or post mode: send as plain text or rich post with table conversion
137
+ const converted = core.channel.text.convertMarkdownTables(text, tableMode);
138
+ const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
139
+ params.runtime.log?.(
140
+ `feishu deliver: sending ${chunks.length} ${usePost ? "post" : "text"} chunks to ${chatId}`,
141
+ );
142
+ for (const chunk of chunks) {
143
+ await sendMessageFeishu({
144
+ cfg,
145
+ to: chatId,
146
+ text: chunk,
147
+ replyToMessageId,
148
+ mentions: isFirstChunk ? mentionTargets : undefined,
149
+ messageType: usePost ? "post" : "text",
150
+ });
151
+ isFirstChunk = false;
152
+ }
153
+ }
150
154
  },
151
155
  onError: (err, info) => {
152
156
  params.runtime.error?.(`feishu ${info.kind} reply failed: ${String(err)}`);
package/src/send.ts CHANGED
@@ -1,11 +1,16 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import type { FeishuConfig, FeishuSendResult } from "./types.js";
3
- import type { MentionTarget } from "./mention.js";
4
- import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
5
- import { createFeishuClient } from "./client.js";
6
- import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
7
- import { getFeishuRuntime } from "./runtime.js";
8
-
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { FeishuConfig, FeishuSendResult } from "./types.js";
3
+ import type { MentionTarget } from "./mention.js";
4
+ import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
5
+ import { createFeishuClient } from "./client.js";
6
+ import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
7
+ import { getFeishuRuntime } from "./runtime.js";
8
+ import { buildFeishuPostMessagePayload, parsePostContent } from "./post.js";
9
+
10
+ type MarkdownTableMode = ReturnType<
11
+ ReturnType<typeof getFeishuRuntime>["channel"]["text"]["resolveMarkdownTableMode"]
12
+ >;
13
+
9
14
  export type FeishuMessageInfo = {
10
15
  messageId: string;
11
16
  chatId: string;
@@ -63,16 +68,20 @@ export async function getMessageFeishu(params: {
63
68
  return null;
64
69
  }
65
70
 
66
- // Parse content based on message type
67
- let content = item.body?.content ?? "";
68
- try {
69
- const parsed = JSON.parse(content);
70
- if (item.msg_type === "text" && parsed.text) {
71
- content = parsed.text;
72
- }
73
- } catch {
74
- // Keep raw content if parsing fails
75
- }
71
+ // Parse content based on message type
72
+ let content = item.body?.content ?? "";
73
+ if (item.msg_type === "post") {
74
+ content = parsePostContent(content).textContent;
75
+ } else {
76
+ try {
77
+ const parsed = JSON.parse(content);
78
+ if (item.msg_type === "text" && parsed.text) {
79
+ content = parsed.text;
80
+ }
81
+ } catch {
82
+ // Keep raw content if parsing fails
83
+ }
84
+ }
76
85
 
77
86
  return {
78
87
  messageId: item.message_id ?? messageId,
@@ -88,22 +97,42 @@ export async function getMessageFeishu(params: {
88
97
  }
89
98
  }
90
99
 
91
- export type SendFeishuMessageParams = {
92
- cfg: ClawdbotConfig;
93
- to: string;
94
- text: string;
95
- replyToMessageId?: string;
96
- /** Mention target users */
97
- mentions?: MentionTarget[];
98
- };
99
-
100
- export async function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult> {
101
- const { cfg, to, text, replyToMessageId, mentions } = params;
102
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
103
- if (!feishuCfg) {
104
- throw new Error("Feishu channel not configured");
105
- }
106
-
100
+ export type SendFeishuMessageParams = {
101
+ cfg: ClawdbotConfig;
102
+ to: string;
103
+ text: string;
104
+ replyToMessageId?: string;
105
+ /** Mention target users */
106
+ mentions?: MentionTarget[];
107
+ /** Send as text (default) or post (rich text) */
108
+ messageType?: "text" | "post";
109
+ };
110
+
111
+ function buildFeishuTextMessagePayload(params: {
112
+ rawText: string;
113
+ tableMode: MarkdownTableMode;
114
+ mentions?: MentionTarget[];
115
+ }): {
116
+ content: string;
117
+ msgType: "text";
118
+ } {
119
+ const { rawText, tableMode, mentions } = params;
120
+ const textWithMentions =
121
+ mentions && mentions.length > 0 ? buildMentionedMessage(mentions, rawText) : rawText;
122
+ const text = getFeishuRuntime().channel.text.convertMarkdownTables(textWithMentions, tableMode);
123
+ return {
124
+ content: JSON.stringify({ text }),
125
+ msgType: "text",
126
+ };
127
+ }
128
+
129
+ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult> {
130
+ const { cfg, to, text, replyToMessageId, mentions, messageType } = params;
131
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
132
+ if (!feishuCfg) {
133
+ throw new Error("Feishu channel not configured");
134
+ }
135
+
107
136
  const client = createFeishuClient(feishuCfg);
108
137
  const receiveId = normalizeFeishuTarget(to);
109
138
  if (!receiveId) {
@@ -111,28 +140,34 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
111
140
  }
112
141
 
113
142
  const receiveIdType = resolveReceiveIdType(receiveId);
114
- const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
115
- cfg,
116
- channel: "feishu",
117
- });
118
-
119
- // Build message content (with @mention support)
120
- let rawText = text ?? "";
121
- if (mentions && mentions.length > 0) {
122
- rawText = buildMentionedMessage(mentions, rawText);
123
- }
124
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
125
-
126
- const content = JSON.stringify({ text: messageText });
127
-
128
- if (replyToMessageId) {
129
- const response = await client.im.message.reply({
130
- path: { message_id: replyToMessageId },
131
- data: {
132
- content,
133
- msg_type: "text",
134
- },
135
- });
143
+ const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
144
+ cfg,
145
+ channel: "feishu",
146
+ });
147
+
148
+ const rawText = text ?? "";
149
+ const baseMessageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
150
+
151
+ const payload =
152
+ messageType === "post"
153
+ ? buildFeishuPostMessagePayload({
154
+ messageText: baseMessageText,
155
+ mentions,
156
+ })
157
+ : buildFeishuTextMessagePayload({
158
+ rawText,
159
+ tableMode,
160
+ mentions,
161
+ });
162
+
163
+ if (replyToMessageId) {
164
+ const response = await client.im.message.reply({
165
+ path: { message_id: replyToMessageId },
166
+ data: {
167
+ content: payload.content,
168
+ msg_type: payload.msgType,
169
+ },
170
+ });
136
171
 
137
172
  if (response.code !== 0) {
138
173
  throw new Error(`Feishu reply failed: ${response.msg || `code ${response.code}`}`);
@@ -144,14 +179,14 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
144
179
  };
145
180
  }
146
181
 
147
- const response = await client.im.message.create({
148
- params: { receive_id_type: receiveIdType },
149
- data: {
150
- receive_id: receiveId,
151
- content,
152
- msg_type: "text",
153
- },
154
- });
182
+ const response = await client.im.message.create({
183
+ params: { receive_id_type: receiveIdType },
184
+ data: {
185
+ receive_id: receiveId,
186
+ content: payload.content,
187
+ msg_type: payload.msgType,
188
+ },
189
+ });
155
190
 
156
191
  if (response.code !== 0) {
157
192
  throw new Error(`Feishu send failed: ${response.msg || `code ${response.code}`}`);
@@ -292,32 +327,37 @@ export async function sendMarkdownCardFeishu(params: {
292
327
  * Edit an existing text message.
293
328
  * Note: Feishu only allows editing messages within 24 hours.
294
329
  */
295
- export async function editMessageFeishu(params: {
296
- cfg: ClawdbotConfig;
297
- messageId: string;
298
- text: string;
299
- }): Promise<void> {
300
- const { cfg, messageId, text } = params;
301
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
302
- if (!feishuCfg) {
303
- throw new Error("Feishu channel not configured");
304
- }
305
-
330
+ export async function editMessageFeishu(params: {
331
+ cfg: ClawdbotConfig;
332
+ messageId: string;
333
+ text: string;
334
+ messageType?: "text" | "post";
335
+ }): Promise<void> {
336
+ const { cfg, messageId, text, messageType } = params;
337
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
338
+ if (!feishuCfg) {
339
+ throw new Error("Feishu channel not configured");
340
+ }
341
+
306
342
  const client = createFeishuClient(feishuCfg);
307
- const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
308
- cfg,
309
- channel: "feishu",
310
- });
311
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
312
- const content = JSON.stringify({ text: messageText });
313
-
314
- const response = await client.im.message.update({
315
- path: { message_id: messageId },
316
- data: {
317
- msg_type: "text",
318
- content,
319
- },
320
- });
343
+ const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
344
+ cfg,
345
+ channel: "feishu",
346
+ });
347
+ const rawText = text ?? "";
348
+ const baseMessageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
349
+ const payload =
350
+ messageType === "post"
351
+ ? buildFeishuPostMessagePayload({ messageText: baseMessageText })
352
+ : buildFeishuTextMessagePayload({ rawText, tableMode });
353
+
354
+ const response = await client.im.message.update({
355
+ path: { message_id: messageId },
356
+ data: {
357
+ msg_type: payload.msgType,
358
+ content: payload.content,
359
+ },
360
+ });
321
361
 
322
362
  if (response.code !== 0) {
323
363
  throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);