@superbenxxxh/feishu 1.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.
@@ -0,0 +1,157 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { FeishuConfig } from "./types.js";
3
+ import { createFeishuClient } from "./client.js";
4
+
5
+ export type FeishuReaction = {
6
+ reactionId: string;
7
+ emojiType: string;
8
+ operatorType: "app" | "user";
9
+ operatorId: string;
10
+ };
11
+
12
+ /**
13
+ * Add a reaction (emoji) to a message.
14
+ * @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
15
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
16
+ */
17
+ export async function addReactionFeishu(params: {
18
+ cfg: ClawdbotConfig;
19
+ messageId: string;
20
+ emojiType: string;
21
+ }): Promise<{ reactionId: string }> {
22
+ const { cfg, messageId, emojiType } = params;
23
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
24
+ if (!feishuCfg) {
25
+ throw new Error("Feishu channel not configured");
26
+ }
27
+
28
+ const client = createFeishuClient(feishuCfg);
29
+
30
+ const response = (await client.im.messageReaction.create({
31
+ path: { message_id: messageId },
32
+ data: {
33
+ reaction_type: {
34
+ emoji_type: emojiType,
35
+ },
36
+ },
37
+ })) as {
38
+ code?: number;
39
+ msg?: string;
40
+ data?: { reaction_id?: string };
41
+ };
42
+
43
+ if (response.code !== 0) {
44
+ throw new Error(`Feishu add reaction failed: ${response.msg || `code ${response.code}`}`);
45
+ }
46
+
47
+ const reactionId = response.data?.reaction_id;
48
+ if (!reactionId) {
49
+ throw new Error("Feishu add reaction failed: no reaction_id returned");
50
+ }
51
+
52
+ return { reactionId };
53
+ }
54
+
55
+ /**
56
+ * Remove a reaction from a message.
57
+ */
58
+ export async function removeReactionFeishu(params: {
59
+ cfg: ClawdbotConfig;
60
+ messageId: string;
61
+ reactionId: string;
62
+ }): Promise<void> {
63
+ const { cfg, messageId, reactionId } = params;
64
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
65
+ if (!feishuCfg) {
66
+ throw new Error("Feishu channel not configured");
67
+ }
68
+
69
+ const client = createFeishuClient(feishuCfg);
70
+
71
+ const response = (await client.im.messageReaction.delete({
72
+ path: {
73
+ message_id: messageId,
74
+ reaction_id: reactionId,
75
+ },
76
+ })) as { code?: number; msg?: string };
77
+
78
+ if (response.code !== 0) {
79
+ throw new Error(`Feishu remove reaction failed: ${response.msg || `code ${response.code}`}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * List all reactions for a message.
85
+ */
86
+ export async function listReactionsFeishu(params: {
87
+ cfg: ClawdbotConfig;
88
+ messageId: string;
89
+ emojiType?: string;
90
+ }): Promise<FeishuReaction[]> {
91
+ const { cfg, messageId, emojiType } = params;
92
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
93
+ if (!feishuCfg) {
94
+ throw new Error("Feishu channel not configured");
95
+ }
96
+
97
+ const client = createFeishuClient(feishuCfg);
98
+
99
+ const response = (await client.im.messageReaction.list({
100
+ path: { message_id: messageId },
101
+ params: emojiType ? { reaction_type: emojiType } : undefined,
102
+ })) as {
103
+ code?: number;
104
+ msg?: string;
105
+ data?: {
106
+ items?: Array<{
107
+ reaction_id?: string;
108
+ reaction_type?: { emoji_type?: string };
109
+ operator_type?: string;
110
+ operator_id?: { open_id?: string; user_id?: string; union_id?: string };
111
+ }>;
112
+ };
113
+ };
114
+
115
+ if (response.code !== 0) {
116
+ throw new Error(`Feishu list reactions failed: ${response.msg || `code ${response.code}`}`);
117
+ }
118
+
119
+ const items = response.data?.items ?? [];
120
+ return items.map((item) => ({
121
+ reactionId: item.reaction_id ?? "",
122
+ emojiType: item.reaction_type?.emoji_type ?? "",
123
+ operatorType: item.operator_type === "app" ? "app" : "user",
124
+ operatorId:
125
+ item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
126
+ }));
127
+ }
128
+
129
+ /**
130
+ * Common Feishu emoji types for convenience.
131
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
132
+ */
133
+ export const FeishuEmoji = {
134
+ // Common reactions
135
+ THUMBSUP: "THUMBSUP",
136
+ THUMBSDOWN: "THUMBSDOWN",
137
+ HEART: "HEART",
138
+ SMILE: "SMILE",
139
+ GRINNING: "GRINNING",
140
+ LAUGHING: "LAUGHING",
141
+ CRY: "CRY",
142
+ ANGRY: "ANGRY",
143
+ SURPRISED: "SURPRISED",
144
+ THINKING: "THINKING",
145
+ CLAP: "CLAP",
146
+ OK: "OK",
147
+ FIST: "FIST",
148
+ PRAY: "PRAY",
149
+ FIRE: "FIRE",
150
+ PARTY: "PARTY",
151
+ CHECK: "CHECK",
152
+ CROSS: "CROSS",
153
+ QUESTION: "QUESTION",
154
+ EXCLAMATION: "EXCLAMATION",
155
+ } as const;
156
+
157
+ export type FeishuEmojiType = (typeof FeishuEmoji)[keyof typeof FeishuEmoji];
@@ -0,0 +1,166 @@
1
+ import {
2
+ createReplyPrefixContext,
3
+ createTypingCallbacks,
4
+ logTypingFailure,
5
+ type ClawdbotConfig,
6
+ type RuntimeEnv,
7
+ type ReplyPayload,
8
+ } from "openclaw/plugin-sdk";
9
+ import { getFeishuRuntime } from "./runtime.js";
10
+ import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
11
+ import type { FeishuConfig } from "./types.js";
12
+ import type { MentionTarget } from "./mention.js";
13
+ import {
14
+ addTypingIndicator,
15
+ removeTypingIndicator,
16
+ type TypingIndicatorState,
17
+ } from "./typing.js";
18
+
19
+ /**
20
+ * Detect if text contains markdown elements that benefit from card rendering.
21
+ * Used by auto render mode.
22
+ */
23
+ function shouldUseCard(text: string): boolean {
24
+ // Code blocks (fenced)
25
+ if (/```[\s\S]*?```/.test(text)) return true;
26
+ // Tables (at least header + separator row with |)
27
+ if (/\|.+\|[\r\n]+\|[-:| ]+\|/.test(text)) return true;
28
+ return false;
29
+ }
30
+
31
+ export type CreateFeishuReplyDispatcherParams = {
32
+ cfg: ClawdbotConfig;
33
+ agentId: string;
34
+ runtime: RuntimeEnv;
35
+ chatId: string;
36
+ replyToMessageId?: string;
37
+ /** Mention targets, will be auto-included in replies */
38
+ mentionTargets?: MentionTarget[];
39
+ };
40
+
41
+ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
42
+ const core = getFeishuRuntime();
43
+ const { cfg, agentId, chatId, replyToMessageId, mentionTargets } = params;
44
+
45
+ const prefixContext = createReplyPrefixContext({
46
+ cfg,
47
+ agentId,
48
+ });
49
+
50
+ // Feishu doesn't have a native typing indicator API.
51
+ // We use message reactions as a typing indicator substitute.
52
+ let typingState: TypingIndicatorState | null = null;
53
+
54
+ const typingCallbacks = createTypingCallbacks({
55
+ start: async () => {
56
+ if (!replyToMessageId) return;
57
+ typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId });
58
+ params.runtime.log?.(`feishu: added typing indicator reaction`);
59
+ },
60
+ stop: async () => {
61
+ if (!typingState) return;
62
+ await removeTypingIndicator({ cfg, state: typingState });
63
+ typingState = null;
64
+ params.runtime.log?.(`feishu: removed typing indicator reaction`);
65
+ },
66
+ onStartError: (err) => {
67
+ logTypingFailure({
68
+ log: (message) => params.runtime.log?.(message),
69
+ channel: "feishu",
70
+ action: "start",
71
+ error: err,
72
+ });
73
+ },
74
+ onStopError: (err) => {
75
+ logTypingFailure({
76
+ log: (message) => params.runtime.log?.(message),
77
+ channel: "feishu",
78
+ action: "stop",
79
+ error: err,
80
+ });
81
+ },
82
+ });
83
+
84
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit({
85
+ cfg,
86
+ channel: "feishu",
87
+ defaultLimit: 4000,
88
+ });
89
+ const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu");
90
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
91
+ cfg,
92
+ channel: "feishu",
93
+ });
94
+
95
+ const { dispatcher, replyOptions, markDispatchIdle } =
96
+ core.channel.reply.createReplyDispatcherWithTyping({
97
+ responsePrefix: prefixContext.responsePrefix,
98
+ responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
99
+ humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
100
+ onReplyStart: typingCallbacks.onReplyStart,
101
+ deliver: async (payload: ReplyPayload) => {
102
+ params.runtime.log?.(`feishu deliver called: text=${payload.text?.slice(0, 100)}`);
103
+ const text = payload.text ?? "";
104
+ if (!text.trim()) {
105
+ params.runtime.log?.(`feishu deliver: empty text, skipping`);
106
+ return;
107
+ }
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));
116
+
117
+ // Only include @mentions in the first chunk (avoid duplicate @s)
118
+ let isFirstChunk = true;
119
+
120
+ if (useCard) {
121
+ // Card mode: send as interactive card with markdown rendering
122
+ const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
123
+ params.runtime.log?.(`feishu deliver: sending ${chunks.length} card chunks to ${chatId}`);
124
+ for (const chunk of chunks) {
125
+ await sendMarkdownCardFeishu({
126
+ cfg,
127
+ to: chatId,
128
+ text: chunk,
129
+ replyToMessageId,
130
+ mentions: isFirstChunk ? mentionTargets : undefined,
131
+ });
132
+ isFirstChunk = false;
133
+ }
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
+ }
150
+ },
151
+ onError: (err, info) => {
152
+ params.runtime.error?.(`feishu ${info.kind} reply failed: ${String(err)}`);
153
+ typingCallbacks.onIdle?.();
154
+ },
155
+ onIdle: typingCallbacks.onIdle,
156
+ });
157
+
158
+ return {
159
+ dispatcher,
160
+ replyOptions: {
161
+ ...replyOptions,
162
+ onModelSelected: prefixContext.onModelSelected,
163
+ },
164
+ markDispatchIdle,
165
+ };
166
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setFeishuRuntime(next: PluginRuntime) {
6
+ runtime = next;
7
+ }
8
+
9
+ export function getFeishuRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error("Feishu runtime not initialized");
12
+ }
13
+ return runtime;
14
+ }
package/src/send.ts ADDED
@@ -0,0 +1,325 @@
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
+
9
+ export type FeishuMessageInfo = {
10
+ messageId: string;
11
+ chatId: string;
12
+ senderId?: string;
13
+ senderOpenId?: string;
14
+ content: string;
15
+ contentType: string;
16
+ createTime?: number;
17
+ };
18
+
19
+ /**
20
+ * Get a message by its ID.
21
+ * Useful for fetching quoted/replied message content.
22
+ */
23
+ export async function getMessageFeishu(params: {
24
+ cfg: ClawdbotConfig;
25
+ messageId: string;
26
+ }): Promise<FeishuMessageInfo | null> {
27
+ const { cfg, messageId } = params;
28
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
29
+ if (!feishuCfg) {
30
+ throw new Error("Feishu channel not configured");
31
+ }
32
+
33
+ const client = createFeishuClient(feishuCfg);
34
+
35
+ try {
36
+ const response = (await client.im.message.get({
37
+ path: { message_id: messageId },
38
+ })) as {
39
+ code?: number;
40
+ msg?: string;
41
+ data?: {
42
+ items?: Array<{
43
+ message_id?: string;
44
+ chat_id?: string;
45
+ msg_type?: string;
46
+ body?: { content?: string };
47
+ sender?: {
48
+ id?: string;
49
+ id_type?: string;
50
+ sender_type?: string;
51
+ };
52
+ create_time?: string;
53
+ }>;
54
+ };
55
+ };
56
+
57
+ if (response.code !== 0) {
58
+ return null;
59
+ }
60
+
61
+ const item = response.data?.items?.[0];
62
+ if (!item) {
63
+ return null;
64
+ }
65
+
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
+ }
76
+
77
+ return {
78
+ messageId: item.message_id ?? messageId,
79
+ chatId: item.chat_id ?? "",
80
+ senderId: item.sender?.id,
81
+ senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
82
+ content,
83
+ contentType: item.msg_type ?? "text",
84
+ createTime: item.create_time ? parseInt(item.create_time, 10) : undefined,
85
+ };
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
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
+
107
+ const client = createFeishuClient(feishuCfg);
108
+ const receiveId = normalizeFeishuTarget(to);
109
+ if (!receiveId) {
110
+ throw new Error(`Invalid Feishu target: ${to}`);
111
+ }
112
+
113
+ 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
+ });
136
+
137
+ if (response.code !== 0) {
138
+ throw new Error(`Feishu reply failed: ${response.msg || `code ${response.code}`}`);
139
+ }
140
+
141
+ return {
142
+ messageId: response.data?.message_id ?? "unknown",
143
+ chatId: receiveId,
144
+ };
145
+ }
146
+
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
+ });
155
+
156
+ if (response.code !== 0) {
157
+ throw new Error(`Feishu send failed: ${response.msg || `code ${response.code}`}`);
158
+ }
159
+
160
+ return {
161
+ messageId: response.data?.message_id ?? "unknown",
162
+ chatId: receiveId,
163
+ };
164
+ }
165
+
166
+ export type SendFeishuCardParams = {
167
+ cfg: ClawdbotConfig;
168
+ to: string;
169
+ card: Record<string, unknown>;
170
+ replyToMessageId?: string;
171
+ };
172
+
173
+ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
174
+ const { cfg, to, card, replyToMessageId } = params;
175
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
176
+ if (!feishuCfg) {
177
+ throw new Error("Feishu channel not configured");
178
+ }
179
+
180
+ const client = createFeishuClient(feishuCfg);
181
+ const receiveId = normalizeFeishuTarget(to);
182
+ if (!receiveId) {
183
+ throw new Error(`Invalid Feishu target: ${to}`);
184
+ }
185
+
186
+ const receiveIdType = resolveReceiveIdType(receiveId);
187
+ const content = JSON.stringify(card);
188
+
189
+ if (replyToMessageId) {
190
+ const response = await client.im.message.reply({
191
+ path: { message_id: replyToMessageId },
192
+ data: {
193
+ content,
194
+ msg_type: "interactive",
195
+ },
196
+ });
197
+
198
+ if (response.code !== 0) {
199
+ throw new Error(`Feishu card reply failed: ${response.msg || `code ${response.code}`}`);
200
+ }
201
+
202
+ return {
203
+ messageId: response.data?.message_id ?? "unknown",
204
+ chatId: receiveId,
205
+ };
206
+ }
207
+
208
+ const response = await client.im.message.create({
209
+ params: { receive_id_type: receiveIdType },
210
+ data: {
211
+ receive_id: receiveId,
212
+ content,
213
+ msg_type: "interactive",
214
+ },
215
+ });
216
+
217
+ if (response.code !== 0) {
218
+ throw new Error(`Feishu card send failed: ${response.msg || `code ${response.code}`}`);
219
+ }
220
+
221
+ return {
222
+ messageId: response.data?.message_id ?? "unknown",
223
+ chatId: receiveId,
224
+ };
225
+ }
226
+
227
+ export async function updateCardFeishu(params: {
228
+ cfg: ClawdbotConfig;
229
+ messageId: string;
230
+ card: Record<string, unknown>;
231
+ }): Promise<void> {
232
+ const { cfg, messageId, card } = params;
233
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
234
+ if (!feishuCfg) {
235
+ throw new Error("Feishu channel not configured");
236
+ }
237
+
238
+ const client = createFeishuClient(feishuCfg);
239
+ const content = JSON.stringify(card);
240
+
241
+ const response = await client.im.message.patch({
242
+ path: { message_id: messageId },
243
+ data: { content },
244
+ });
245
+
246
+ if (response.code !== 0) {
247
+ throw new Error(`Feishu card update failed: ${response.msg || `code ${response.code}`}`);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Build a Feishu interactive card with markdown content.
253
+ * Cards render markdown properly (code blocks, tables, links, etc.)
254
+ */
255
+ export function buildMarkdownCard(text: string): Record<string, unknown> {
256
+ return {
257
+ config: {
258
+ wide_screen_mode: true,
259
+ },
260
+ elements: [
261
+ {
262
+ tag: "markdown",
263
+ content: text,
264
+ },
265
+ ],
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Send a message as a markdown card (interactive message).
271
+ * This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
272
+ */
273
+ export async function sendMarkdownCardFeishu(params: {
274
+ cfg: ClawdbotConfig;
275
+ to: string;
276
+ text: string;
277
+ replyToMessageId?: string;
278
+ /** Mention target users */
279
+ mentions?: MentionTarget[];
280
+ }): Promise<FeishuSendResult> {
281
+ const { cfg, to, text, replyToMessageId, mentions } = params;
282
+ // Build message content (with @mention support)
283
+ let cardText = text;
284
+ if (mentions && mentions.length > 0) {
285
+ cardText = buildMentionedCardContent(mentions, text);
286
+ }
287
+ const card = buildMarkdownCard(cardText);
288
+ return sendCardFeishu({ cfg, to, card, replyToMessageId });
289
+ }
290
+
291
+ /**
292
+ * Edit an existing text message.
293
+ * Note: Feishu only allows editing messages within 24 hours.
294
+ */
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
+
306
+ 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
+ });
321
+
322
+ if (response.code !== 0) {
323
+ throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
324
+ }
325
+ }