@actagent/feishu 2026.6.2

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 (207) hide show
  1. package/README.md +11 -0
  2. package/actagent.plugin.json +224 -0
  3. package/api.ts +33 -0
  4. package/channel-entry.ts +21 -0
  5. package/channel-plugin-api.ts +2 -0
  6. package/contract-api.ts +17 -0
  7. package/index.ts +83 -0
  8. package/legacy-state-migrations-api.ts +2 -0
  9. package/npm-shrinkwrap.json +539 -0
  10. package/package.json +64 -0
  11. package/runtime-api.ts +58 -0
  12. package/runtime-setter-api.ts +3 -0
  13. package/secret-contract-api.ts +6 -0
  14. package/security-contract-api.ts +2 -0
  15. package/session-key-api.ts +2 -0
  16. package/setup-api.ts +4 -0
  17. package/setup-entry.test.ts +33 -0
  18. package/setup-entry.ts +25 -0
  19. package/skills/feishu-doc/SKILL.md +211 -0
  20. package/skills/feishu-doc/references/block-types.md +103 -0
  21. package/skills/feishu-drive/SKILL.md +97 -0
  22. package/skills/feishu-perm/SKILL.md +119 -0
  23. package/skills/feishu-wiki/SKILL.md +113 -0
  24. package/src/accounts.test.ts +481 -0
  25. package/src/accounts.ts +380 -0
  26. package/src/agent-config.ts +22 -0
  27. package/src/app-registration.test.ts +62 -0
  28. package/src/app-registration.ts +355 -0
  29. package/src/approval-auth.test.ts +25 -0
  30. package/src/approval-auth.ts +26 -0
  31. package/src/async.test.ts +68 -0
  32. package/src/async.ts +109 -0
  33. package/src/audio-preflight.runtime.ts +10 -0
  34. package/src/bitable.test.ts +174 -0
  35. package/src/bitable.ts +781 -0
  36. package/src/bot-content.ts +488 -0
  37. package/src/bot-group-name.test.ts +148 -0
  38. package/src/bot-runtime-api.ts +13 -0
  39. package/src/bot-sender-name.test.ts +68 -0
  40. package/src/bot-sender-name.ts +137 -0
  41. package/src/bot.broadcast.test.ts +643 -0
  42. package/src/bot.card-action.test.ts +647 -0
  43. package/src/bot.checkBotMentioned.test.ts +266 -0
  44. package/src/bot.helpers.test.ts +136 -0
  45. package/src/bot.stripBotMention.test.ts +127 -0
  46. package/src/bot.test.ts +3817 -0
  47. package/src/bot.ts +1788 -0
  48. package/src/card-action.ts +515 -0
  49. package/src/card-interaction.test.ts +132 -0
  50. package/src/card-interaction.ts +160 -0
  51. package/src/card-test-helpers.ts +55 -0
  52. package/src/card-ux-approval.ts +66 -0
  53. package/src/card-ux-launcher.test.ts +126 -0
  54. package/src/card-ux-launcher.ts +136 -0
  55. package/src/card-ux-shared.ts +34 -0
  56. package/src/channel-runtime-api.ts +17 -0
  57. package/src/channel.runtime.ts +48 -0
  58. package/src/channel.test.ts +1337 -0
  59. package/src/channel.ts +1401 -0
  60. package/src/chat-schema.ts +30 -0
  61. package/src/chat.test.ts +295 -0
  62. package/src/chat.ts +198 -0
  63. package/src/client-timeout.ts +44 -0
  64. package/src/client.test.ts +463 -0
  65. package/src/client.ts +263 -0
  66. package/src/comment-dispatcher-runtime-api.ts +7 -0
  67. package/src/comment-dispatcher.test.ts +186 -0
  68. package/src/comment-dispatcher.ts +108 -0
  69. package/src/comment-handler-runtime-api.ts +4 -0
  70. package/src/comment-handler.test.ts +588 -0
  71. package/src/comment-handler.ts +304 -0
  72. package/src/comment-reaction.test.ts +139 -0
  73. package/src/comment-reaction.ts +260 -0
  74. package/src/comment-shared.test.ts +184 -0
  75. package/src/comment-shared.ts +405 -0
  76. package/src/comment-target.ts +45 -0
  77. package/src/config-schema.test.ts +327 -0
  78. package/src/config-schema.ts +338 -0
  79. package/src/conversation-id.test.ts +19 -0
  80. package/src/conversation-id.ts +199 -0
  81. package/src/dedup-migrations.test.ts +90 -0
  82. package/src/dedup-migrations.ts +103 -0
  83. package/src/dedup.test.ts +95 -0
  84. package/src/dedup.ts +304 -0
  85. package/src/dedupe-key.ts +68 -0
  86. package/src/directory.static.ts +62 -0
  87. package/src/directory.test.ts +142 -0
  88. package/src/directory.ts +125 -0
  89. package/src/doc-schema.ts +183 -0
  90. package/src/doctor.test.ts +382 -0
  91. package/src/doctor.ts +876 -0
  92. package/src/docx-batch-insert.test.ts +117 -0
  93. package/src/docx-batch-insert.ts +223 -0
  94. package/src/docx-color-text.ts +154 -0
  95. package/src/docx-table-ops.test.ts +54 -0
  96. package/src/docx-table-ops.ts +316 -0
  97. package/src/docx-types.ts +39 -0
  98. package/src/docx.account-selection.test.ts +96 -0
  99. package/src/docx.test.ts +706 -0
  100. package/src/docx.ts +1598 -0
  101. package/src/drive-schema.ts +93 -0
  102. package/src/drive.test.ts +1240 -0
  103. package/src/drive.ts +830 -0
  104. package/src/dynamic-agent.test.ts +156 -0
  105. package/src/dynamic-agent.ts +144 -0
  106. package/src/event-types.ts +46 -0
  107. package/src/external-keys.test.ts +21 -0
  108. package/src/external-keys.ts +20 -0
  109. package/src/lifecycle.test-support.ts +223 -0
  110. package/src/media.test.ts +956 -0
  111. package/src/media.ts +1106 -0
  112. package/src/mention-target.types.ts +6 -0
  113. package/src/mention.ts +115 -0
  114. package/src/message-action-contract.ts +14 -0
  115. package/src/monitor-state-runtime-api.ts +8 -0
  116. package/src/monitor-transport-runtime-api.ts +11 -0
  117. package/src/monitor.account.ts +501 -0
  118. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +215 -0
  119. package/src/monitor.bot-identity.ts +87 -0
  120. package/src/monitor.bot-menu-handler.ts +164 -0
  121. package/src/monitor.bot-menu.lifecycle.test-support.ts +221 -0
  122. package/src/monitor.bot-menu.test.ts +200 -0
  123. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +265 -0
  124. package/src/monitor.card-action.lifecycle.test-support.ts +418 -0
  125. package/src/monitor.cleanup.test.ts +384 -0
  126. package/src/monitor.comment-notice-handler.ts +106 -0
  127. package/src/monitor.comment.test.ts +968 -0
  128. package/src/monitor.comment.ts +1386 -0
  129. package/src/monitor.lifecycle.test.ts +5 -0
  130. package/src/monitor.message-handler.ts +346 -0
  131. package/src/monitor.reaction.test.ts +770 -0
  132. package/src/monitor.startup.test.ts +232 -0
  133. package/src/monitor.startup.ts +76 -0
  134. package/src/monitor.state.defaults.test.ts +47 -0
  135. package/src/monitor.state.ts +171 -0
  136. package/src/monitor.synthetic-error.ts +19 -0
  137. package/src/monitor.test-mocks.ts +47 -0
  138. package/src/monitor.transport.ts +451 -0
  139. package/src/monitor.ts +104 -0
  140. package/src/monitor.webhook-e2e.test.ts +284 -0
  141. package/src/monitor.webhook-security.test.ts +394 -0
  142. package/src/monitor.webhook.test-helpers.ts +138 -0
  143. package/src/outbound-runtime-api.ts +2 -0
  144. package/src/outbound.test.ts +1255 -0
  145. package/src/outbound.ts +742 -0
  146. package/src/perm-schema.ts +53 -0
  147. package/src/perm.ts +171 -0
  148. package/src/pins.ts +109 -0
  149. package/src/policy.test.ts +224 -0
  150. package/src/policy.ts +322 -0
  151. package/src/post.test.ts +106 -0
  152. package/src/post.ts +276 -0
  153. package/src/presentation-card.ts +204 -0
  154. package/src/probe.test.ts +310 -0
  155. package/src/probe.ts +181 -0
  156. package/src/processing-claims.ts +60 -0
  157. package/src/qr-terminal.ts +2 -0
  158. package/src/reactions.ts +124 -0
  159. package/src/reasoning-preview.test.ts +114 -0
  160. package/src/reasoning-preview.ts +29 -0
  161. package/src/reply-dispatcher-runtime-api.ts +8 -0
  162. package/src/reply-dispatcher.test.ts +2009 -0
  163. package/src/reply-dispatcher.ts +865 -0
  164. package/src/runtime.ts +10 -0
  165. package/src/secret-contract.ts +146 -0
  166. package/src/secret-input.ts +2 -0
  167. package/src/security-audit-shared.ts +70 -0
  168. package/src/security-audit.test.ts +60 -0
  169. package/src/security-audit.ts +2 -0
  170. package/src/send-result.ts +81 -0
  171. package/src/send-target.test.ts +87 -0
  172. package/src/send-target.ts +36 -0
  173. package/src/send.reply-fallback.test.ts +418 -0
  174. package/src/send.test.ts +661 -0
  175. package/src/send.ts +860 -0
  176. package/src/sequential-key.test.ts +73 -0
  177. package/src/sequential-key.ts +29 -0
  178. package/src/sequential-queue.test.ts +184 -0
  179. package/src/sequential-queue.ts +90 -0
  180. package/src/session-conversation.ts +42 -0
  181. package/src/session-route.ts +49 -0
  182. package/src/setup-core.ts +52 -0
  183. package/src/setup-surface.test.ts +485 -0
  184. package/src/setup-surface.ts +620 -0
  185. package/src/streaming-card.test.ts +549 -0
  186. package/src/streaming-card.ts +611 -0
  187. package/src/subagent-hooks.test.ts +632 -0
  188. package/src/subagent-hooks.ts +414 -0
  189. package/src/targets.ts +98 -0
  190. package/src/test-support/lifecycle-test-support.ts +459 -0
  191. package/src/thread-bindings.test.ts +181 -0
  192. package/src/thread-bindings.ts +332 -0
  193. package/src/tool-account-routing.test.ts +419 -0
  194. package/src/tool-account.test.ts +45 -0
  195. package/src/tool-account.ts +98 -0
  196. package/src/tool-factory-test-harness.ts +83 -0
  197. package/src/tool-result.test.ts +33 -0
  198. package/src/tool-result.ts +17 -0
  199. package/src/tools-config.test.ts +52 -0
  200. package/src/tools-config.ts +29 -0
  201. package/src/types.ts +111 -0
  202. package/src/typing.test.ts +145 -0
  203. package/src/typing.ts +215 -0
  204. package/src/wiki-schema.ts +70 -0
  205. package/src/wiki.ts +271 -0
  206. package/subagent-hooks-api.ts +22 -0
  207. package/tsconfig.json +16 -0
@@ -0,0 +1,661 @@
1
+ // Feishu tests cover send plugin behavior.
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { ACTAgentBotConfig } from "../runtime-api.js";
4
+ import { buildMarkdownCard } from "./send.js";
5
+
6
+ const {
7
+ mockConvertMarkdownTables,
8
+ mockClientGet,
9
+ mockClientList,
10
+ mockClientPatch,
11
+ mockCreateFeishuClient,
12
+ mockResolveMarkdownTableMode,
13
+ mockResolveFeishuAccount,
14
+ mockRuntimeConvertMarkdownTables,
15
+ mockRuntimeResolveMarkdownTableMode,
16
+ } = vi.hoisted(() => ({
17
+ mockConvertMarkdownTables: vi.fn((text: string) => text),
18
+ mockClientGet: vi.fn(),
19
+ mockClientList: vi.fn(),
20
+ mockClientPatch: vi.fn(),
21
+ mockCreateFeishuClient: vi.fn(),
22
+ mockResolveMarkdownTableMode: vi.fn(() => "preserve"),
23
+ mockResolveFeishuAccount: vi.fn(),
24
+ mockRuntimeConvertMarkdownTables: vi.fn((text: string) => text),
25
+ mockRuntimeResolveMarkdownTableMode: vi.fn(() => "preserve"),
26
+ }));
27
+
28
+ vi.mock("actagent/plugin-sdk/markdown-table-runtime", () => ({
29
+ resolveMarkdownTableMode: mockResolveMarkdownTableMode,
30
+ }));
31
+
32
+ vi.mock("actagent/plugin-sdk/text-chunking", async (importOriginal) => {
33
+ const actual = await importOriginal<typeof import("actagent/plugin-sdk/text-chunking")>();
34
+ return {
35
+ ...actual,
36
+ convertMarkdownTables: mockConvertMarkdownTables,
37
+ };
38
+ });
39
+
40
+ vi.mock("./client.js", () => ({
41
+ createFeishuClient: mockCreateFeishuClient,
42
+ }));
43
+
44
+ vi.mock("./accounts.js", () => ({
45
+ resolveFeishuAccount: mockResolveFeishuAccount,
46
+ resolveFeishuRuntimeAccount: mockResolveFeishuAccount,
47
+ }));
48
+
49
+ vi.mock("./runtime.js", () => ({
50
+ getFeishuRuntime: () => ({
51
+ channel: {
52
+ text: {
53
+ resolveMarkdownTableMode: mockRuntimeResolveMarkdownTableMode,
54
+ convertMarkdownTables: mockRuntimeConvertMarkdownTables,
55
+ },
56
+ },
57
+ }),
58
+ }));
59
+
60
+ let buildStructuredCard: typeof import("./send.js").buildStructuredCard;
61
+ let editMessageFeishu: typeof import("./send.js").editMessageFeishu;
62
+ let getMessageFeishu: typeof import("./send.js").getMessageFeishu;
63
+ let listFeishuThreadMessages: typeof import("./send.js").listFeishuThreadMessages;
64
+ let resolveFeishuCardTemplate: typeof import("./send.js").resolveFeishuCardTemplate;
65
+ let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;
66
+
67
+ describe("getMessageFeishu", () => {
68
+ beforeAll(async () => {
69
+ ({
70
+ buildStructuredCard,
71
+ editMessageFeishu,
72
+ getMessageFeishu,
73
+ listFeishuThreadMessages,
74
+ resolveFeishuCardTemplate,
75
+ sendMessageFeishu,
76
+ } = await import("./send.js"));
77
+ });
78
+
79
+ afterAll(() => {
80
+ vi.doUnmock("actagent/plugin-sdk/markdown-table-runtime");
81
+ vi.doUnmock("actagent/plugin-sdk/text-chunking");
82
+ vi.doUnmock("./client.js");
83
+ vi.doUnmock("./accounts.js");
84
+ vi.doUnmock("./runtime.js");
85
+ vi.resetModules();
86
+ });
87
+
88
+ beforeEach(() => {
89
+ vi.clearAllMocks();
90
+ mockResolveMarkdownTableMode.mockReturnValue("preserve");
91
+ mockConvertMarkdownTables.mockImplementation((text: string) => text);
92
+ mockRuntimeResolveMarkdownTableMode.mockReturnValue("preserve");
93
+ mockRuntimeConvertMarkdownTables.mockImplementation((text: string) => text);
94
+ mockResolveFeishuAccount.mockReturnValue({
95
+ accountId: "default",
96
+ configured: true,
97
+ });
98
+ mockCreateFeishuClient.mockReturnValue({
99
+ im: {
100
+ message: {
101
+ create: vi.fn(),
102
+ get: mockClientGet,
103
+ list: mockClientList,
104
+ patch: mockClientPatch,
105
+ },
106
+ },
107
+ });
108
+ });
109
+
110
+ it("sends text without requiring Feishu runtime text helpers", async () => {
111
+ mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
112
+ throw new Error("Feishu runtime not initialized");
113
+ });
114
+ mockRuntimeConvertMarkdownTables.mockImplementation(() => {
115
+ throw new Error("Feishu runtime not initialized");
116
+ });
117
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
118
+ mockCreateFeishuClient.mockReturnValue({
119
+ im: {
120
+ message: {
121
+ create: vi.fn().mockResolvedValue({ code: 0, data: { message_id: "om_send" } }),
122
+ reply: vi.fn(),
123
+ get: mockClientGet,
124
+ list: mockClientList,
125
+ patch: mockClientPatch,
126
+ },
127
+ },
128
+ });
129
+
130
+ const result = await sendMessageFeishu({
131
+ cfg: {} as ACTAgentBotConfig,
132
+ to: "oc_send",
133
+ text: "hello",
134
+ });
135
+
136
+ expect(mockResolveMarkdownTableMode).toHaveBeenCalledWith({
137
+ cfg: {},
138
+ channel: "feishu",
139
+ });
140
+ expect(mockConvertMarkdownTables).toHaveBeenCalledWith("hello", "preserve");
141
+ expect(typeof result.receipt.sentAt).toBe("number");
142
+ expect(result).toEqual({
143
+ messageId: "om_send",
144
+ chatId: "oc_send",
145
+ receipt: {
146
+ primaryPlatformMessageId: "om_send",
147
+ platformMessageIds: ["om_send"],
148
+ parts: [
149
+ {
150
+ platformMessageId: "om_send",
151
+ kind: "text",
152
+ index: 0,
153
+ raw: {
154
+ channel: "feishu",
155
+ messageId: "om_send",
156
+ chatId: "oc_send",
157
+ conversationId: "oc_send",
158
+ },
159
+ threadId: "oc_send",
160
+ },
161
+ ],
162
+ threadId: "oc_send",
163
+ sentAt: result.receipt.sentAt,
164
+ raw: [
165
+ {
166
+ channel: "feishu",
167
+ messageId: "om_send",
168
+ chatId: "oc_send",
169
+ conversationId: "oc_send",
170
+ },
171
+ ],
172
+ },
173
+ });
174
+ });
175
+
176
+ it("extracts text content from interactive card elements", async () => {
177
+ mockClientGet.mockResolvedValueOnce({
178
+ code: 0,
179
+ data: {
180
+ items: [
181
+ {
182
+ message_id: "om_1",
183
+ chat_id: "oc_1",
184
+ msg_type: "interactive",
185
+ body: {
186
+ content: JSON.stringify({
187
+ elements: [
188
+ { tag: "markdown", content: "hello markdown" },
189
+ { tag: "div", text: { content: "hello div" } },
190
+ ],
191
+ }),
192
+ },
193
+ },
194
+ ],
195
+ },
196
+ });
197
+
198
+ const result = await getMessageFeishu({
199
+ cfg: {} as ACTAgentBotConfig,
200
+ messageId: "om_1",
201
+ });
202
+
203
+ expect(result).toEqual({
204
+ messageId: "om_1",
205
+ chatId: "oc_1",
206
+ chatType: undefined,
207
+ senderId: undefined,
208
+ senderOpenId: undefined,
209
+ senderType: undefined,
210
+ content: "hello markdown\nhello div",
211
+ contentType: "interactive",
212
+ createTime: undefined,
213
+ threadId: undefined,
214
+ });
215
+ });
216
+
217
+ it("falls through empty interactive card element arrays and locale variants", async () => {
218
+ mockClientGet.mockResolvedValueOnce({
219
+ code: 0,
220
+ data: {
221
+ items: [
222
+ {
223
+ message_id: "om_i18n_card",
224
+ chat_id: "oc_i18n_card",
225
+ msg_type: "interactive",
226
+ body: {
227
+ content: JSON.stringify({
228
+ elements: [],
229
+ body: { elements: [] },
230
+ i18n_elements: {
231
+ zh_cn: [],
232
+ en_us: [
233
+ {
234
+ tag: "markdown",
235
+ content: "hello ${count} {{label}} {{metadata}}",
236
+ },
237
+ ],
238
+ },
239
+ template_variable: {
240
+ count: 2,
241
+ label: "tasks",
242
+ metadata: { ignored: true },
243
+ },
244
+ }),
245
+ },
246
+ },
247
+ ],
248
+ },
249
+ });
250
+
251
+ const result = await getMessageFeishu({
252
+ cfg: {} as ACTAgentBotConfig,
253
+ messageId: "om_i18n_card",
254
+ });
255
+
256
+ expect(result).toEqual({
257
+ messageId: "om_i18n_card",
258
+ chatId: "oc_i18n_card",
259
+ chatType: undefined,
260
+ senderId: undefined,
261
+ senderOpenId: undefined,
262
+ senderType: undefined,
263
+ content: "hello 2 tasks {{metadata}}",
264
+ contentType: "interactive",
265
+ createTime: undefined,
266
+ threadId: undefined,
267
+ });
268
+ });
269
+
270
+ it("falls back to post-format content when interactive card elements are empty", async () => {
271
+ mockClientGet.mockResolvedValueOnce({
272
+ code: 0,
273
+ data: {
274
+ items: [
275
+ {
276
+ message_id: "om_post_card",
277
+ chat_id: "oc_post_card",
278
+ msg_type: "interactive",
279
+ body: {
280
+ content: JSON.stringify({
281
+ elements: [],
282
+ post: {
283
+ zh_cn: {
284
+ title: "Card summary",
285
+ content: [[{ tag: "md", text: "**fallback** body" }]],
286
+ },
287
+ },
288
+ }),
289
+ },
290
+ },
291
+ ],
292
+ },
293
+ });
294
+
295
+ const result = await getMessageFeishu({
296
+ cfg: {} as ACTAgentBotConfig,
297
+ messageId: "om_post_card",
298
+ });
299
+
300
+ expect(result).toEqual({
301
+ messageId: "om_post_card",
302
+ chatId: "oc_post_card",
303
+ chatType: undefined,
304
+ senderId: undefined,
305
+ senderOpenId: undefined,
306
+ senderType: undefined,
307
+ content: "Card summary\n\n**fallback** body",
308
+ contentType: "interactive",
309
+ createTime: undefined,
310
+ threadId: undefined,
311
+ });
312
+ });
313
+
314
+ it("extracts text content from post messages", async () => {
315
+ mockClientGet.mockResolvedValueOnce({
316
+ code: 0,
317
+ data: {
318
+ items: [
319
+ {
320
+ message_id: "om_post",
321
+ chat_id: "oc_post",
322
+ msg_type: "post",
323
+ body: {
324
+ content: JSON.stringify({
325
+ zh_cn: {
326
+ title: "Summary",
327
+ content: [[{ tag: "text", text: "post body" }]],
328
+ },
329
+ }),
330
+ },
331
+ },
332
+ ],
333
+ },
334
+ });
335
+
336
+ const result = await getMessageFeishu({
337
+ cfg: {} as ACTAgentBotConfig,
338
+ messageId: "om_post",
339
+ });
340
+
341
+ expect(result).toEqual({
342
+ messageId: "om_post",
343
+ chatId: "oc_post",
344
+ chatType: undefined,
345
+ senderId: undefined,
346
+ senderOpenId: undefined,
347
+ senderType: undefined,
348
+ content: "Summary\n\npost body",
349
+ contentType: "post",
350
+ createTime: undefined,
351
+ threadId: undefined,
352
+ });
353
+ });
354
+
355
+ it("returns text placeholder instead of raw JSON for unsupported message types", async () => {
356
+ mockClientGet.mockResolvedValueOnce({
357
+ code: 0,
358
+ data: {
359
+ items: [
360
+ {
361
+ message_id: "om_file",
362
+ chat_id: "oc_file",
363
+ msg_type: "file",
364
+ body: {
365
+ content: JSON.stringify({ file_key: "file_v3_123" }),
366
+ },
367
+ },
368
+ ],
369
+ },
370
+ });
371
+
372
+ const result = await getMessageFeishu({
373
+ cfg: {} as ACTAgentBotConfig,
374
+ messageId: "om_file",
375
+ });
376
+
377
+ expect(result).toEqual({
378
+ messageId: "om_file",
379
+ chatId: "oc_file",
380
+ chatType: undefined,
381
+ senderId: undefined,
382
+ senderOpenId: undefined,
383
+ senderType: undefined,
384
+ content: "[file message]",
385
+ contentType: "file",
386
+ createTime: undefined,
387
+ threadId: undefined,
388
+ });
389
+ });
390
+
391
+ it("supports single-object response shape from Feishu API", async () => {
392
+ mockClientGet.mockResolvedValueOnce({
393
+ code: 0,
394
+ data: {
395
+ message_id: "om_single",
396
+ chat_id: "oc_single",
397
+ msg_type: "text",
398
+ body: {
399
+ content: JSON.stringify({ text: "single payload" }),
400
+ },
401
+ },
402
+ });
403
+
404
+ const result = await getMessageFeishu({
405
+ cfg: {} as ACTAgentBotConfig,
406
+ messageId: "om_single",
407
+ });
408
+
409
+ expect(result).toEqual({
410
+ messageId: "om_single",
411
+ chatId: "oc_single",
412
+ chatType: undefined,
413
+ senderId: undefined,
414
+ senderOpenId: undefined,
415
+ senderType: undefined,
416
+ content: "single payload",
417
+ contentType: "text",
418
+ createTime: undefined,
419
+ threadId: undefined,
420
+ });
421
+ });
422
+
423
+ it("reuses the same content parsing for thread history messages", async () => {
424
+ mockClientList.mockResolvedValueOnce({
425
+ code: 0,
426
+ data: {
427
+ items: [
428
+ {
429
+ message_id: "om_root",
430
+ msg_type: "text",
431
+ body: {
432
+ content: JSON.stringify({ text: "root starter" }),
433
+ },
434
+ },
435
+ {
436
+ message_id: "om_card",
437
+ msg_type: "interactive",
438
+ body: {
439
+ content: JSON.stringify({
440
+ body: {
441
+ elements: [{ tag: "markdown", content: "hello from card 2.0" }],
442
+ },
443
+ }),
444
+ },
445
+ sender: {
446
+ id: "app_1",
447
+ sender_type: "app",
448
+ },
449
+ create_time: "1710000000000",
450
+ },
451
+ {
452
+ message_id: "om_file",
453
+ msg_type: "file",
454
+ body: {
455
+ content: JSON.stringify({ file_key: "file_v3_123" }),
456
+ },
457
+ sender: {
458
+ id: "ou_1",
459
+ sender_type: "user",
460
+ },
461
+ create_time: "1710000001000",
462
+ },
463
+ ],
464
+ },
465
+ });
466
+
467
+ const result = await listFeishuThreadMessages({
468
+ cfg: {} as ACTAgentBotConfig,
469
+ threadId: "omt_1",
470
+ rootMessageId: "om_root",
471
+ });
472
+
473
+ expect(result).toEqual([
474
+ {
475
+ messageId: "om_file",
476
+ senderId: "ou_1",
477
+ senderType: "user",
478
+ contentType: "file",
479
+ content: "[file message]",
480
+ createTime: 1710000001000,
481
+ },
482
+ {
483
+ messageId: "om_card",
484
+ senderId: "app_1",
485
+ senderType: "app",
486
+ contentType: "interactive",
487
+ content: "hello from card 2.0",
488
+ createTime: 1710000000000,
489
+ },
490
+ ]);
491
+ });
492
+
493
+ it("does not partially parse malformed thread history create_time values", async () => {
494
+ mockClientList.mockResolvedValueOnce({
495
+ code: 0,
496
+ data: {
497
+ items: [
498
+ {
499
+ message_id: "om_text",
500
+ msg_type: "text",
501
+ body: {
502
+ content: JSON.stringify({ text: "partial time" }),
503
+ },
504
+ sender: {
505
+ id: "ou_1",
506
+ sender_type: "user",
507
+ },
508
+ create_time: "1710000000000ms",
509
+ },
510
+ ],
511
+ },
512
+ });
513
+
514
+ const result = await listFeishuThreadMessages({
515
+ cfg: {} as ACTAgentBotConfig,
516
+ threadId: "omt_1",
517
+ rootMessageId: "om_root",
518
+ });
519
+
520
+ expect(result).toEqual([
521
+ {
522
+ messageId: "om_text",
523
+ senderId: "ou_1",
524
+ senderType: "user",
525
+ contentType: "text",
526
+ content: "partial time",
527
+ createTime: undefined,
528
+ },
529
+ ]);
530
+ });
531
+ });
532
+
533
+ describe("editMessageFeishu", () => {
534
+ beforeEach(() => {
535
+ vi.clearAllMocks();
536
+ mockResolveFeishuAccount.mockReturnValue({
537
+ accountId: "default",
538
+ configured: true,
539
+ });
540
+ mockCreateFeishuClient.mockReturnValue({
541
+ im: {
542
+ message: {
543
+ patch: mockClientPatch,
544
+ },
545
+ },
546
+ });
547
+ });
548
+
549
+ it("patches post content for text edits", async () => {
550
+ mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
551
+ throw new Error("Feishu runtime not initialized");
552
+ });
553
+ mockRuntimeConvertMarkdownTables.mockImplementation(() => {
554
+ throw new Error("Feishu runtime not initialized");
555
+ });
556
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
557
+
558
+ const result = await editMessageFeishu({
559
+ cfg: {} as ACTAgentBotConfig,
560
+ messageId: "om_edit",
561
+ text: "updated body",
562
+ });
563
+
564
+ expect(mockClientPatch).toHaveBeenCalledWith({
565
+ path: { message_id: "om_edit" },
566
+ data: {
567
+ content: JSON.stringify({
568
+ zh_cn: {
569
+ content: [
570
+ [
571
+ {
572
+ tag: "md",
573
+ text: "updated body",
574
+ },
575
+ ],
576
+ ],
577
+ },
578
+ }),
579
+ },
580
+ });
581
+ expect(result).toEqual({ messageId: "om_edit", contentType: "post" });
582
+ });
583
+
584
+ it("patches interactive content for card edits", async () => {
585
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
586
+
587
+ const result = await editMessageFeishu({
588
+ cfg: {} as ACTAgentBotConfig,
589
+ messageId: "om_card",
590
+ card: { schema: "2.0" },
591
+ });
592
+
593
+ expect(mockClientPatch).toHaveBeenCalledWith({
594
+ path: { message_id: "om_card" },
595
+ data: {
596
+ content: JSON.stringify({ schema: "2.0" }),
597
+ },
598
+ });
599
+ expect(result).toEqual({ messageId: "om_card", contentType: "interactive" });
600
+ });
601
+ });
602
+
603
+ describe("resolveFeishuCardTemplate", () => {
604
+ it("accepts supported Feishu templates", () => {
605
+ expect(resolveFeishuCardTemplate(" purple ")).toBe("purple");
606
+ });
607
+
608
+ it("drops unsupported free-form identity themes", () => {
609
+ expect(resolveFeishuCardTemplate("space lobster")).toBeUndefined();
610
+ });
611
+ });
612
+
613
+ function expectSchema2WidthConfig(card: unknown) {
614
+ const typedCard = card as {
615
+ config: {
616
+ width_mode?: string;
617
+ enable_forward?: boolean;
618
+ wide_screen_mode?: boolean;
619
+ };
620
+ };
621
+
622
+ expect(typedCard.config.width_mode).toBe("fill");
623
+ expect(typedCard.config.enable_forward).toBeUndefined();
624
+ expect(typedCard.config.wide_screen_mode).toBeUndefined();
625
+ }
626
+
627
+ describe("Feishu card schema config", () => {
628
+ it.each([
629
+ {
630
+ name: "structured card",
631
+ build: () => buildStructuredCard("hello"),
632
+ },
633
+ {
634
+ name: "markdown card",
635
+ build: () => buildMarkdownCard("hello"),
636
+ },
637
+ ])("$name uses schema-2.0 width config instead of legacy wide screen mode", ({ build }) => {
638
+ expectSchema2WidthConfig(build());
639
+ });
640
+ });
641
+
642
+ describe("buildStructuredCard", () => {
643
+ it("falls back to blue when the header template is unsupported", () => {
644
+ const card = buildStructuredCard("hello", {
645
+ header: {
646
+ title: "Agent",
647
+ template: "space lobster",
648
+ },
649
+ });
650
+
651
+ expect(card).toEqual({
652
+ schema: "2.0",
653
+ config: { width_mode: "fill" },
654
+ body: { elements: [{ tag: "markdown", content: "hello" }] },
655
+ header: {
656
+ title: { tag: "plain_text", content: "Agent" },
657
+ template: "blue",
658
+ },
659
+ });
660
+ });
661
+ });