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