@openclaw/feishu 2026.3.13 → 2026.5.2-beta.1

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.
Files changed (187) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1827 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1253 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +135 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +406 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +33 -95
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +116 -20
  72. package/src/directory.ts +60 -92
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +403 -26
  91. package/src/media.ts +509 -132
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.startup.test.ts +11 -9
  114. package/src/monitor.startup.ts +26 -16
  115. package/src/monitor.state.ts +20 -5
  116. package/src/monitor.synthetic-error.ts +18 -0
  117. package/src/monitor.test-mocks.ts +2 -2
  118. package/src/monitor.transport.ts +220 -60
  119. package/src/monitor.ts +15 -10
  120. package/src/monitor.webhook-e2e.test.ts +65 -7
  121. package/src/monitor.webhook-security.test.ts +122 -0
  122. package/src/monitor.webhook.test-helpers.ts +44 -26
  123. package/src/outbound-runtime-api.ts +1 -0
  124. package/src/outbound.test.ts +616 -37
  125. package/src/outbound.ts +623 -81
  126. package/src/perm-schema.ts +1 -1
  127. package/src/perm.ts +1 -7
  128. package/src/pins.ts +108 -0
  129. package/src/policy.test.ts +297 -117
  130. package/src/policy.ts +142 -29
  131. package/src/post.ts +7 -6
  132. package/src/probe.test.ts +14 -9
  133. package/src/probe.ts +26 -16
  134. package/src/processing-claims.ts +59 -0
  135. package/src/qr-terminal.ts +1 -0
  136. package/src/reactions.ts +4 -34
  137. package/src/reasoning-preview.test.ts +59 -0
  138. package/src/reasoning-preview.ts +20 -0
  139. package/src/reply-dispatcher-runtime-api.ts +7 -0
  140. package/src/reply-dispatcher.test.ts +660 -29
  141. package/src/reply-dispatcher.ts +407 -154
  142. package/src/runtime.ts +6 -3
  143. package/src/secret-contract.ts +145 -0
  144. package/src/secret-input.ts +1 -13
  145. package/src/security-audit-shared.ts +69 -0
  146. package/src/security-audit.test.ts +61 -0
  147. package/src/security-audit.ts +1 -0
  148. package/src/send-result.ts +1 -1
  149. package/src/send-target.test.ts +9 -3
  150. package/src/send-target.ts +10 -4
  151. package/src/send.reply-fallback.test.ts +105 -2
  152. package/src/send.test.ts +386 -4
  153. package/src/send.ts +414 -95
  154. package/src/sequential-key.test.ts +72 -0
  155. package/src/sequential-key.ts +28 -0
  156. package/src/sequential-queue.test.ts +92 -0
  157. package/src/sequential-queue.ts +16 -0
  158. package/src/session-conversation.ts +42 -0
  159. package/src/session-route.ts +48 -0
  160. package/src/setup-core.ts +51 -0
  161. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  162. package/src/setup-surface.ts +581 -0
  163. package/src/streaming-card.test.ts +138 -2
  164. package/src/streaming-card.ts +134 -18
  165. package/src/subagent-hooks.test.ts +603 -0
  166. package/src/subagent-hooks.ts +397 -0
  167. package/src/targets.ts +3 -13
  168. package/src/test-support/lifecycle-test-support.ts +453 -0
  169. package/src/thread-bindings.test.ts +143 -0
  170. package/src/thread-bindings.ts +330 -0
  171. package/src/tool-account-routing.test.ts +66 -8
  172. package/src/tool-account.test.ts +44 -0
  173. package/src/tool-account.ts +40 -17
  174. package/src/tool-factory-test-harness.ts +11 -8
  175. package/src/tool-result.ts +3 -1
  176. package/src/tools-config.ts +1 -1
  177. package/src/types.ts +16 -15
  178. package/src/typing.ts +10 -6
  179. package/src/wiki-schema.ts +1 -1
  180. package/src/wiki.ts +1 -7
  181. package/subagent-hooks-api.ts +31 -0
  182. package/tsconfig.json +16 -0
  183. package/src/feishu-command-handler.ts +0 -59
  184. package/src/onboarding.status.test.ts +0 -25
  185. package/src/onboarding.ts +0 -489
  186. package/src/send-message.ts +0 -71
  187. package/src/targets.test.ts +0 -70
package/src/send.ts CHANGED
@@ -1,21 +1,44 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
- import { resolveFeishuAccount } from "./accounts.js";
1
+ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
2
+ import {
3
+ convertMarkdownTables,
4
+ normalizeLowercaseStringOrEmpty,
5
+ normalizeOptionalLowercaseString,
6
+ } from "openclaw/plugin-sdk/text-runtime";
7
+ import type { ClawdbotConfig } from "../runtime-api.js";
8
+ import { resolveFeishuRuntimeAccount } from "./accounts.js";
3
9
  import { createFeishuClient } from "./client.js";
4
- import type { MentionTarget } from "./mention.js";
5
- import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
10
+ import { createFeishuApiError, requestFeishuApi } from "./comment-shared.js";
11
+ import type { MentionTarget } from "./mention-target.types.js";
12
+ import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
6
13
  import { parsePostContent } from "./post.js";
7
- import { getFeishuRuntime } from "./runtime.js";
8
14
  import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
9
15
  import { resolveFeishuSendTarget } from "./send-target.js";
10
16
  import type { FeishuChatType, FeishuMessageInfo, FeishuSendResult } from "./types.js";
11
17
 
12
18
  const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
19
+ const INTERACTIVE_CARD_FALLBACK_TEXT = "[Interactive Card]";
20
+ const POST_FALLBACK_TEXT = "[Rich text message]";
21
+ const FEISHU_CARD_TEMPLATES = new Set([
22
+ "blue",
23
+ "green",
24
+ "red",
25
+ "orange",
26
+ "purple",
27
+ "indigo",
28
+ "wathet",
29
+ "turquoise",
30
+ "yellow",
31
+ "grey",
32
+ "carmine",
33
+ "violet",
34
+ "lime",
35
+ ]);
13
36
 
14
37
  function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string }): boolean {
15
38
  if (response.code !== undefined && WITHDRAWN_REPLY_ERROR_CODES.has(response.code)) {
16
39
  return true;
17
40
  }
18
- const msg = response.msg?.toLowerCase() ?? "";
41
+ const msg = normalizeLowercaseStringOrEmpty(response.msg);
19
42
  return msg.includes("withdrawn") || msg.includes("not found");
20
43
  }
21
44
 
@@ -40,6 +63,10 @@ function isWithdrawnReplyError(err: unknown): boolean {
40
63
  return false;
41
64
  }
42
65
 
66
+ function isRecord(value: unknown): value is Record<string, unknown> {
67
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
68
+ }
69
+
43
70
  type FeishuCreateMessageClient = {
44
71
  im: {
45
72
  message: {
@@ -65,6 +92,7 @@ type FeishuMessageGetItem = {
65
92
  message_id?: string;
66
93
  chat_id?: string;
67
94
  chat_type?: FeishuChatType;
95
+ thread_id?: string;
68
96
  msg_type?: string;
69
97
  body?: { content?: string };
70
98
  sender?: FeishuMessageSender;
@@ -90,14 +118,19 @@ async function sendFallbackDirect(
90
118
  },
91
119
  errorPrefix: string,
92
120
  ): Promise<FeishuSendResult> {
93
- const response = await client.im.message.create({
94
- params: { receive_id_type: params.receiveIdType },
95
- data: {
96
- receive_id: params.receiveId,
97
- content: params.content,
98
- msg_type: params.msgType,
99
- },
100
- });
121
+ const response = await requestFeishuApi(
122
+ () =>
123
+ client.im.message.create({
124
+ params: { receive_id_type: params.receiveIdType },
125
+ data: {
126
+ receive_id: params.receiveId,
127
+ content: params.content,
128
+ msg_type: params.msgType,
129
+ },
130
+ }),
131
+ errorPrefix,
132
+ { includeNestedErrorLogId: true },
133
+ );
101
134
  assertFeishuMessageApiSuccess(response, errorPrefix);
102
135
  return toFeishuSendResult(response, params.receiveId);
103
136
  }
@@ -123,6 +156,12 @@ async function sendReplyOrFallbackDirect(
123
156
  return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
124
157
  }
125
158
 
159
+ const threadReplyFallbackError = params.replyInThread
160
+ ? new Error(
161
+ "Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
162
+ )
163
+ : null;
164
+
126
165
  let response: { code?: number; msg?: string; data?: { message_id?: string } };
127
166
  try {
128
167
  response = await client.im.message.reply({
@@ -135,49 +174,141 @@ async function sendReplyOrFallbackDirect(
135
174
  });
136
175
  } catch (err) {
137
176
  if (!isWithdrawnReplyError(err)) {
138
- throw err;
177
+ throw createFeishuApiError(err, params.replyErrorPrefix, { includeNestedErrorLogId: true });
178
+ }
179
+ if (threadReplyFallbackError) {
180
+ throw threadReplyFallbackError;
139
181
  }
140
182
  return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
141
183
  }
142
184
  if (shouldFallbackFromReplyTarget(response)) {
185
+ if (threadReplyFallbackError) {
186
+ throw threadReplyFallbackError;
187
+ }
143
188
  return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
144
189
  }
145
190
  assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
146
191
  return toFeishuSendResult(response, params.directParams.receiveId);
147
192
  }
148
193
 
149
- function parseInteractiveCardContent(parsed: unknown): string {
150
- if (!parsed || typeof parsed !== "object") {
151
- return "[Interactive Card]";
194
+ function normalizeCardTemplateVariable(value: unknown): string | undefined {
195
+ if (typeof value === "string") {
196
+ return value;
152
197
  }
198
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
199
+ return String(value);
200
+ }
201
+ return undefined;
202
+ }
153
203
 
154
- const candidate = parsed as { elements?: unknown };
155
- if (!Array.isArray(candidate.elements)) {
156
- return "[Interactive Card]";
204
+ function readCardTemplateVariables(parsed: Record<string, unknown>): Map<string, string> {
205
+ const variables = new Map<string, string>();
206
+ for (const source of [parsed.template_variable, parsed.template_variables]) {
207
+ if (!isRecord(source)) {
208
+ continue;
209
+ }
210
+ for (const [key, value] of Object.entries(source)) {
211
+ const normalized = normalizeCardTemplateVariable(value);
212
+ if (normalized !== undefined) {
213
+ variables.set(key, normalized);
214
+ }
215
+ }
157
216
  }
217
+ return variables;
218
+ }
158
219
 
220
+ function applyCardTemplateVariables(text: string, variables: Map<string, string>): string {
221
+ if (variables.size === 0) {
222
+ return text;
223
+ }
224
+ return text.replace(/\$\{([A-Za-z0-9_.-]+)\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (match, a, b) => {
225
+ const variableName = typeof a === "string" ? a : b;
226
+ return variables.get(variableName) ?? match;
227
+ });
228
+ }
229
+
230
+ function extractInteractiveElementText(
231
+ element: unknown,
232
+ variables: Map<string, string>,
233
+ ): string | undefined {
234
+ if (!isRecord(element)) {
235
+ return undefined;
236
+ }
237
+ const tag = typeof element.tag === "string" ? element.tag : "";
238
+ const text = isRecord(element.text) ? element.text : undefined;
239
+
240
+ if (tag === "div" && typeof text?.content === "string") {
241
+ return applyCardTemplateVariables(text.content, variables);
242
+ }
243
+ if ((tag === "markdown" || tag === "lark_md") && typeof element.content === "string") {
244
+ return applyCardTemplateVariables(element.content, variables);
245
+ }
246
+ if (tag === "plain_text" && typeof element.content === "string") {
247
+ return applyCardTemplateVariables(element.content, variables);
248
+ }
249
+ return undefined;
250
+ }
251
+
252
+ function extractInteractiveElementsText(
253
+ elements: unknown[],
254
+ variables: Map<string, string>,
255
+ ): string {
159
256
  const texts: string[] = [];
160
- for (const element of candidate.elements) {
161
- if (!element || typeof element !== "object") {
162
- continue;
257
+ for (const element of elements) {
258
+ const text = extractInteractiveElementText(element, variables);
259
+ if (text !== undefined) {
260
+ texts.push(text);
163
261
  }
164
- const item = element as {
165
- tag?: string;
166
- content?: string;
167
- text?: { content?: string };
168
- };
169
- if (item.tag === "div" && typeof item.text?.content === "string") {
170
- texts.push(item.text.content);
262
+ }
263
+ return texts.join("\n").trim();
264
+ }
265
+
266
+ function readInteractiveElementArrays(parsed: Record<string, unknown>): unknown[][] {
267
+ const body = isRecord(parsed.body) ? parsed.body : undefined;
268
+ const elementArrays: unknown[][] = [];
269
+
270
+ for (const candidate of [parsed.elements, body?.elements]) {
271
+ if (Array.isArray(candidate)) {
272
+ elementArrays.push(candidate);
273
+ }
274
+ }
275
+
276
+ for (const candidate of [parsed.i18n_elements, body?.i18n_elements]) {
277
+ if (!isRecord(candidate)) {
171
278
  continue;
172
279
  }
173
- if (item.tag === "markdown" && typeof item.content === "string") {
174
- texts.push(item.content);
280
+ for (const localeElements of Object.values(candidate)) {
281
+ if (Array.isArray(localeElements)) {
282
+ elementArrays.push(localeElements);
283
+ }
175
284
  }
176
285
  }
177
- return texts.join("\n").trim() || "[Interactive Card]";
286
+
287
+ return elementArrays;
288
+ }
289
+
290
+ function parseInteractivePostFallback(parsed: unknown): string | undefined {
291
+ const textContent = parsePostContent(JSON.stringify(parsed)).textContent.trim();
292
+ return textContent && textContent !== POST_FALLBACK_TEXT ? textContent : undefined;
178
293
  }
179
294
 
180
- function parseQuotedMessageContent(rawContent: string, msgType: string): string {
295
+ function parseInteractiveCardContent(parsed: unknown): string {
296
+ if (!isRecord(parsed)) {
297
+ return INTERACTIVE_CARD_FALLBACK_TEXT;
298
+ }
299
+
300
+ const variables = readCardTemplateVariables(parsed);
301
+ for (const elements of readInteractiveElementArrays(parsed)) {
302
+ const text = extractInteractiveElementsText(elements, variables);
303
+ if (text) {
304
+ return text;
305
+ }
306
+ }
307
+
308
+ return parseInteractivePostFallback(parsed) ?? INTERACTIVE_CARD_FALLBACK_TEXT;
309
+ }
310
+
311
+ function parseFeishuMessageContent(rawContent: string, msgType: string): string {
181
312
  if (!rawContent) {
182
313
  return "";
183
314
  }
@@ -218,6 +349,33 @@ function parseQuotedMessageContent(rawContent: string, msgType: string): string
218
349
  return `[${msgType || "unknown"} message]`;
219
350
  }
220
351
 
352
+ function parseFeishuMessageItem(
353
+ item: FeishuMessageGetItem,
354
+ fallbackMessageId?: string,
355
+ ): FeishuMessageInfo {
356
+ const msgType = item.msg_type ?? "text";
357
+ const rawContent = item.body?.content ?? "";
358
+
359
+ return {
360
+ messageId: item.message_id ?? fallbackMessageId ?? "",
361
+ chatId: item.chat_id ?? "",
362
+ chatType:
363
+ item.chat_type === "group" ||
364
+ item.chat_type === "topic_group" ||
365
+ item.chat_type === "private" ||
366
+ item.chat_type === "p2p"
367
+ ? item.chat_type
368
+ : undefined,
369
+ senderId: item.sender?.id,
370
+ senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
371
+ senderType: item.sender?.sender_type,
372
+ content: parseFeishuMessageContent(rawContent, msgType),
373
+ contentType: msgType,
374
+ createTime: item.create_time ? Number.parseInt(item.create_time, 10) : undefined,
375
+ threadId: item.thread_id || undefined,
376
+ };
377
+ }
378
+
221
379
  /**
222
380
  * Get a message by its ID.
223
381
  * Useful for fetching quoted/replied message content.
@@ -228,7 +386,7 @@ export async function getMessageFeishu(params: {
228
386
  accountId?: string;
229
387
  }): Promise<FeishuMessageInfo | null> {
230
388
  const { cfg, messageId, accountId } = params;
231
- const account = resolveFeishuAccount({ cfg, accountId });
389
+ const account = resolveFeishuRuntimeAccount({ cfg, accountId });
232
390
  if (!account.configured) {
233
391
  throw new Error(`Feishu account "${account.accountId}" not configured`);
234
392
  }
@@ -255,29 +413,104 @@ export async function getMessageFeishu(params: {
255
413
  return null;
256
414
  }
257
415
 
258
- const msgType = item.msg_type ?? "text";
259
- const rawContent = item.body?.content ?? "";
260
- const content = parseQuotedMessageContent(rawContent, msgType);
261
-
262
- return {
263
- messageId: item.message_id ?? messageId,
264
- chatId: item.chat_id ?? "",
265
- chatType:
266
- item.chat_type === "group" || item.chat_type === "private" || item.chat_type === "p2p"
267
- ? item.chat_type
268
- : undefined,
269
- senderId: item.sender?.id,
270
- senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
271
- senderType: item.sender?.sender_type,
272
- content,
273
- contentType: msgType,
274
- createTime: item.create_time ? parseInt(String(item.create_time), 10) : undefined,
275
- };
416
+ return parseFeishuMessageItem(item, messageId);
276
417
  } catch {
277
418
  return null;
278
419
  }
279
420
  }
280
421
 
422
+ export type FeishuThreadMessageInfo = {
423
+ messageId: string;
424
+ senderId?: string;
425
+ senderType?: string;
426
+ content: string;
427
+ contentType: string;
428
+ createTime?: number;
429
+ };
430
+
431
+ /**
432
+ * List messages in a Feishu thread (topic).
433
+ * Uses container_id_type=thread to directly query thread messages,
434
+ * which includes both the root message and all replies (including bot replies).
435
+ */
436
+ export async function listFeishuThreadMessages(params: {
437
+ cfg: ClawdbotConfig;
438
+ threadId: string;
439
+ currentMessageId?: string;
440
+ /** Exclude the root message (already provided separately as ThreadStarterBody). */
441
+ rootMessageId?: string;
442
+ limit?: number;
443
+ accountId?: string;
444
+ }): Promise<FeishuThreadMessageInfo[]> {
445
+ const { cfg, threadId, currentMessageId, rootMessageId, limit = 20, accountId } = params;
446
+ const account = resolveFeishuRuntimeAccount({ cfg, accountId });
447
+ if (!account.configured) {
448
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
449
+ }
450
+
451
+ const client = createFeishuClient(account);
452
+
453
+ const response = (await client.im.message.list({
454
+ params: {
455
+ container_id_type: "thread",
456
+ container_id: threadId,
457
+ // Fetch newest messages first so long threads keep the most recent turns.
458
+ // Results are reversed below to restore chronological order.
459
+ sort_type: "ByCreateTimeDesc",
460
+ page_size: Math.min(limit + 1, 50),
461
+ },
462
+ })) as {
463
+ code?: number;
464
+ msg?: string;
465
+ data?: {
466
+ items?: Array<
467
+ {
468
+ message_id?: string;
469
+ root_id?: string;
470
+ parent_id?: string;
471
+ } & FeishuMessageGetItem
472
+ >;
473
+ };
474
+ };
475
+
476
+ if (response.code !== 0) {
477
+ throw new Error(
478
+ `Feishu thread list failed: code=${response.code} msg=${response.msg ?? "unknown"}`,
479
+ );
480
+ }
481
+
482
+ const items = response.data?.items ?? [];
483
+ const results: FeishuThreadMessageInfo[] = [];
484
+
485
+ for (const item of items) {
486
+ if (currentMessageId && item.message_id === currentMessageId) {
487
+ continue;
488
+ }
489
+ if (rootMessageId && item.message_id === rootMessageId) {
490
+ continue;
491
+ }
492
+
493
+ const parsed = parseFeishuMessageItem(item);
494
+
495
+ results.push({
496
+ messageId: parsed.messageId,
497
+ senderId: parsed.senderId,
498
+ senderType: parsed.senderType,
499
+ content: parsed.content,
500
+ contentType: parsed.contentType,
501
+ createTime: parsed.createTime,
502
+ });
503
+
504
+ if (results.length >= limit) {
505
+ break;
506
+ }
507
+ }
508
+
509
+ // Restore chronological order (oldest first) since we fetched newest-first.
510
+ results.reverse();
511
+ return results;
512
+ }
513
+
281
514
  export type SendFeishuMessageParams = {
282
515
  cfg: ClawdbotConfig;
283
516
  to: string;
@@ -291,7 +524,7 @@ export type SendFeishuMessageParams = {
291
524
  accountId?: string;
292
525
  };
293
526
 
294
- function buildFeishuPostMessagePayload(params: { messageText: string }): {
527
+ export function buildFeishuPostMessagePayload(params: { messageText: string }): {
295
528
  content: string;
296
529
  msgType: string;
297
530
  } {
@@ -318,7 +551,7 @@ export async function sendMessageFeishu(
318
551
  ): Promise<FeishuSendResult> {
319
552
  const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
320
553
  const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
321
- const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
554
+ const tableMode = resolveMarkdownTableMode({
322
555
  cfg,
323
556
  channel: "feishu",
324
557
  });
@@ -328,7 +561,7 @@ export async function sendMessageFeishu(
328
561
  if (mentions && mentions.length > 0) {
329
562
  rawText = buildMentionedMessage(mentions, rawText);
330
563
  }
331
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
564
+ const messageText = convertMarkdownTables(rawText, tableMode);
332
565
 
333
566
  const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
334
567
 
@@ -371,6 +604,59 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
371
604
  });
372
605
  }
373
606
 
607
+ export async function editMessageFeishu(params: {
608
+ cfg: ClawdbotConfig;
609
+ messageId: string;
610
+ text?: string;
611
+ card?: Record<string, unknown>;
612
+ accountId?: string;
613
+ }): Promise<{ messageId: string; contentType: "post" | "interactive" }> {
614
+ const { cfg, messageId, text, card, accountId } = params;
615
+ const account = resolveFeishuRuntimeAccount({ cfg, accountId });
616
+ if (!account.configured) {
617
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
618
+ }
619
+
620
+ const hasText = typeof text === "string" && text.trim().length > 0;
621
+ const hasCard = Boolean(card);
622
+ if (hasText === hasCard) {
623
+ throw new Error("Feishu edit requires exactly one of text or card.");
624
+ }
625
+
626
+ const client = createFeishuClient(account);
627
+
628
+ if (card) {
629
+ const content = JSON.stringify(card);
630
+ const response = await client.im.message.patch({
631
+ path: { message_id: messageId },
632
+ data: { content },
633
+ });
634
+
635
+ if (response.code !== 0) {
636
+ throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
637
+ }
638
+
639
+ return { messageId, contentType: "interactive" };
640
+ }
641
+
642
+ const tableMode = resolveMarkdownTableMode({
643
+ cfg,
644
+ channel: "feishu",
645
+ });
646
+ const messageText = convertMarkdownTables(text!, tableMode);
647
+ const payload = buildFeishuPostMessagePayload({ messageText });
648
+ const response = await client.im.message.patch({
649
+ path: { message_id: messageId },
650
+ data: { content: payload.content },
651
+ });
652
+
653
+ if (response.code !== 0) {
654
+ throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
655
+ }
656
+
657
+ return { messageId, contentType: "post" };
658
+ }
659
+
374
660
  export async function updateCardFeishu(params: {
375
661
  cfg: ClawdbotConfig;
376
662
  messageId: string;
@@ -378,7 +664,7 @@ export async function updateCardFeishu(params: {
378
664
  accountId?: string;
379
665
  }): Promise<void> {
380
666
  const { cfg, messageId, card, accountId } = params;
381
- const account = resolveFeishuAccount({ cfg, accountId });
667
+ const account = resolveFeishuRuntimeAccount({ cfg, accountId });
382
668
  if (!account.configured) {
383
669
  throw new Error(`Feishu account "${account.accountId}" not configured`);
384
670
  }
@@ -405,7 +691,7 @@ export function buildMarkdownCard(text: string): Record<string, unknown> {
405
691
  return {
406
692
  schema: "2.0",
407
693
  config: {
408
- wide_screen_mode: true,
694
+ width_mode: "fill",
409
695
  },
410
696
  body: {
411
697
  elements: [
@@ -418,64 +704,97 @@ export function buildMarkdownCard(text: string): Record<string, unknown> {
418
704
  };
419
705
  }
420
706
 
707
+ /** Header configuration for structured Feishu cards. */
708
+ export type CardHeaderConfig = {
709
+ /** Header title text, e.g. "💻 Coder" */
710
+ title: string;
711
+ /** Feishu header color template (blue, green, red, orange, purple, grey, etc.). Defaults to "blue". */
712
+ template?: string;
713
+ };
714
+
715
+ export function resolveFeishuCardTemplate(template?: string): string | undefined {
716
+ const normalized = normalizeOptionalLowercaseString(template);
717
+ if (!normalized || !FEISHU_CARD_TEMPLATES.has(normalized)) {
718
+ return undefined;
719
+ }
720
+ return normalized;
721
+ }
722
+
421
723
  /**
422
- * Send a message as a markdown card (interactive message).
423
- * This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
724
+ * Build a Feishu interactive card with optional header and note footer.
725
+ * When header/note are omitted, behaves identically to buildMarkdownCard.
424
726
  */
425
- export async function sendMarkdownCardFeishu(params: {
727
+ export function buildStructuredCard(
728
+ text: string,
729
+ options?: {
730
+ header?: CardHeaderConfig;
731
+ note?: string;
732
+ },
733
+ ): Record<string, unknown> {
734
+ const elements: Record<string, unknown>[] = [{ tag: "markdown", content: text }];
735
+ if (options?.note) {
736
+ elements.push({ tag: "hr" });
737
+ elements.push({ tag: "markdown", content: `<font color='grey'>${options.note}</font>` });
738
+ }
739
+ const card: Record<string, unknown> = {
740
+ schema: "2.0",
741
+ config: { width_mode: "fill" },
742
+ body: { elements },
743
+ };
744
+ if (options?.header) {
745
+ card.header = {
746
+ title: { tag: "plain_text", content: options.header.title },
747
+ template: resolveFeishuCardTemplate(options.header.template) ?? "blue",
748
+ };
749
+ }
750
+ return card;
751
+ }
752
+
753
+ /**
754
+ * Send a message as a structured card with optional header and note.
755
+ */
756
+ export async function sendStructuredCardFeishu(params: {
426
757
  cfg: ClawdbotConfig;
427
758
  to: string;
428
759
  text: string;
429
760
  replyToMessageId?: string;
430
761
  /** When true, reply creates a Feishu topic thread instead of an inline reply */
431
762
  replyInThread?: boolean;
432
- /** Mention target users */
433
763
  mentions?: MentionTarget[];
434
764
  accountId?: string;
765
+ header?: CardHeaderConfig;
766
+ note?: string;
435
767
  }): Promise<FeishuSendResult> {
436
- const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
768
+ const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId, header, note } =
769
+ params;
437
770
  let cardText = text;
438
771
  if (mentions && mentions.length > 0) {
439
772
  cardText = buildMentionedCardContent(mentions, text);
440
773
  }
441
- const card = buildMarkdownCard(cardText);
774
+ const card = buildStructuredCard(cardText, { header, note });
442
775
  return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId });
443
776
  }
444
777
 
445
778
  /**
446
- * Edit an existing text message.
447
- * Note: Feishu only allows editing messages within 24 hours.
779
+ * Send a message as a markdown card (interactive message).
780
+ * This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
448
781
  */
449
- export async function editMessageFeishu(params: {
782
+ export async function sendMarkdownCardFeishu(params: {
450
783
  cfg: ClawdbotConfig;
451
- messageId: string;
784
+ to: string;
452
785
  text: string;
786
+ replyToMessageId?: string;
787
+ /** When true, reply creates a Feishu topic thread instead of an inline reply */
788
+ replyInThread?: boolean;
789
+ /** Mention target users */
790
+ mentions?: MentionTarget[];
453
791
  accountId?: string;
454
- }): Promise<void> {
455
- const { cfg, messageId, text, accountId } = params;
456
- const account = resolveFeishuAccount({ cfg, accountId });
457
- if (!account.configured) {
458
- throw new Error(`Feishu account "${account.accountId}" not configured`);
459
- }
460
-
461
- const client = createFeishuClient(account);
462
- const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
463
- cfg,
464
- channel: "feishu",
465
- });
466
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
467
-
468
- const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
469
-
470
- const response = await client.im.message.update({
471
- path: { message_id: messageId },
472
- data: {
473
- msg_type: msgType,
474
- content,
475
- },
476
- });
477
-
478
- if (response.code !== 0) {
479
- throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
792
+ }): Promise<FeishuSendResult> {
793
+ const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
794
+ let cardText = text;
795
+ if (mentions && mentions.length > 0) {
796
+ cardText = buildMentionedCardContent(mentions, text);
480
797
  }
798
+ const card = buildMarkdownCard(cardText);
799
+ return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId });
481
800
  }