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