@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
@@ -1,12 +1,12 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { parseFeishuMessageEvent } from "./bot.js";
2
+ import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
3
3
 
4
4
  // Helper to build a minimal FeishuMessageEvent for testing
5
5
  function makeEvent(
6
6
  chatType: "p2p" | "group" | "private",
7
7
  mentions?: Array<{ key: string; name: string; id: { open_id?: string } }>,
8
8
  text = "hello",
9
- ) {
9
+ ): FeishuMessageEvent {
10
10
  return {
11
11
  sender: {
12
12
  sender_id: { user_id: "u1", open_id: "ou_sender" },
@@ -22,7 +22,7 @@ function makeEvent(
22
22
  };
23
23
  }
24
24
 
25
- function makePostEvent(content: unknown) {
25
+ function makePostEvent(content: unknown): FeishuMessageEvent {
26
26
  return {
27
27
  sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
28
28
  message: {
@@ -36,7 +36,7 @@ function makePostEvent(content: unknown) {
36
36
  };
37
37
  }
38
38
 
39
- function makeShareChatEvent(content: unknown) {
39
+ function makeShareChatEvent(content: unknown): FeishuMessageEvent {
40
40
  return {
41
41
  sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
42
42
  message: {
@@ -55,15 +55,15 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
55
55
 
56
56
  it("returns mentionedBot=false when there are no mentions", () => {
57
57
  const event = makeEvent("group", []);
58
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
58
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
59
59
  expect(ctx.mentionedBot).toBe(false);
60
60
  });
61
61
 
62
62
  it("falls back to sender user_id when open_id is missing", () => {
63
63
  const event = makeEvent("p2p", []);
64
- (event as any).sender.sender_id = { user_id: "u_mobile_only" };
64
+ event.sender.sender_id = { user_id: "u_mobile_only" };
65
65
 
66
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
66
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
67
67
  expect(ctx.senderOpenId).toBe("u_mobile_only");
68
68
  expect(ctx.senderId).toBe("u_mobile_only");
69
69
  });
@@ -72,7 +72,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
72
72
  const event = makeEvent("group", [
73
73
  { key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
74
74
  ]);
75
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
75
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
76
76
  expect(ctx.mentionedBot).toBe(true);
77
77
  });
78
78
 
@@ -80,7 +80,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
80
80
  const event = makeEvent("group", [
81
81
  { key: "@_user_1", name: "OpenClaw Bot (Alias)", id: { open_id: BOT_OPEN_ID } },
82
82
  ]);
83
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID, "OpenClaw Bot");
83
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID, "OpenClaw Bot");
84
84
  expect(ctx.mentionedBot).toBe(true);
85
85
  });
86
86
 
@@ -88,15 +88,54 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
88
88
  const event = makeEvent("group", [
89
89
  { key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
90
90
  ]);
91
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
91
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
92
92
  expect(ctx.mentionedBot).toBe(false);
93
93
  });
94
94
 
95
+ it("returns mentionedBot=false for broadcast-only @_all text", () => {
96
+ const event = makeEvent("group", [], "@_all please review");
97
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
98
+ expect(ctx.mentionedBot).toBe(false);
99
+ });
100
+
101
+ it("returns mentionedBot=false for broadcast-only @all mention metadata", () => {
102
+ const event = makeEvent("group", [{ key: "@_all", name: "all", id: { open_id: "all" } }]);
103
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
104
+ expect(ctx.mentionedBot).toBe(false);
105
+ });
106
+
107
+ it("returns mentionedBot=false for @all even when botOpenId is the broadcast id", () => {
108
+ const event = makeEvent("group", [{ key: "@_all", name: "all", id: { open_id: "all" } }]);
109
+ const ctx = parseFeishuMessageEvent(event, "all");
110
+ expect(ctx.mentionedBot).toBe(false);
111
+ });
112
+
113
+ it("returns mentionedBot=true when bot is mentioned alongside @all", () => {
114
+ const event = makeEvent("group", [
115
+ { key: "@_all", name: "all", id: { open_id: "all" } },
116
+ { key: "@_bot_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
117
+ ]);
118
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
119
+ expect(ctx.mentionedBot).toBe(true);
120
+ expect(ctx.mentionTargets).toBeUndefined();
121
+ });
122
+
123
+ it("does not include @all in mention-forward targets", () => {
124
+ const event = makeEvent("group", [
125
+ { key: "@_all", name: "all", id: { open_id: "all" } },
126
+ { key: "@_bot_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
127
+ { key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
128
+ ]);
129
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
130
+ expect(ctx.mentionedBot).toBe(true);
131
+ expect(ctx.mentionTargets).toEqual([{ openId: "ou_alice", name: "Alice", key: "@_user_1" }]);
132
+ });
133
+
95
134
  it("returns mentionedBot=false when botOpenId is undefined (unknown bot)", () => {
96
135
  const event = makeEvent("group", [
97
136
  { key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
98
137
  ]);
99
- const ctx = parseFeishuMessageEvent(event as any, undefined);
138
+ const ctx = parseFeishuMessageEvent(event, undefined);
100
139
  expect(ctx.mentionedBot).toBe(false);
101
140
  });
102
141
 
@@ -104,7 +143,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
104
143
  const event = makeEvent("group", [
105
144
  { key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
106
145
  ]);
107
- const ctx = parseFeishuMessageEvent(event as any, "");
146
+ const ctx = parseFeishuMessageEvent(event, "");
108
147
  expect(ctx.mentionedBot).toBe(false);
109
148
  });
110
149
 
@@ -114,7 +153,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
114
153
  [{ key: "@_bot_1", name: ".*", id: { open_id: BOT_OPEN_ID } }],
115
154
  "@NotBot hello",
116
155
  );
117
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
156
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
118
157
  expect(ctx.content).toBe("@NotBot hello");
119
158
  });
120
159
 
@@ -124,7 +163,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
124
163
  [{ key: ".*", name: "Bot", id: { open_id: BOT_OPEN_ID } }],
125
164
  "hello world",
126
165
  );
127
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
166
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
128
167
  expect(ctx.content).toBe("hello world");
129
168
  });
130
169
 
@@ -136,7 +175,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
136
175
  [{ tag: "text", text: "What does this document say" }],
137
176
  ],
138
177
  });
139
- const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
178
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
140
179
  expect(ctx.mentionedBot).toBe(true);
141
180
  });
142
181
 
@@ -144,7 +183,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
144
183
  const event = makePostEvent({
145
184
  content: [[{ tag: "text", text: "hello" }]],
146
185
  });
147
- const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
186
+ const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
148
187
  expect(ctx.mentionedBot).toBe(false);
149
188
  });
150
189
 
@@ -155,10 +194,43 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
155
194
  [{ tag: "text", text: "hello" }],
156
195
  ],
157
196
  });
158
- const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
197
+ const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
198
+ expect(ctx.mentionedBot).toBe(false);
199
+ });
200
+
201
+ it("returns mentionedBot=false for post message with broadcast-only @all", () => {
202
+ const event = makePostEvent({
203
+ content: [
204
+ [{ tag: "at", user_id: "all", user_name: "all" }],
205
+ [{ tag: "text", text: "hello" }],
206
+ ],
207
+ });
208
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
159
209
  expect(ctx.mentionedBot).toBe(false);
160
210
  });
161
211
 
212
+ it("returns mentionedBot=false for post @all even when botOpenId is the broadcast id", () => {
213
+ const event = makePostEvent({
214
+ content: [[{ tag: "at", user_id: "all", user_name: "all" }]],
215
+ });
216
+ const ctx = parseFeishuMessageEvent(event, "all");
217
+ expect(ctx.mentionedBot).toBe(false);
218
+ });
219
+
220
+ it("returns mentionedBot=true for post message with bot mention and broadcast @all", () => {
221
+ const event = makePostEvent({
222
+ content: [
223
+ [
224
+ { tag: "at", user_id: "all", user_name: "all" },
225
+ { tag: "text", text: " " },
226
+ { tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" },
227
+ ],
228
+ ],
229
+ });
230
+ const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
231
+ expect(ctx.mentionedBot).toBe(true);
232
+ });
233
+
162
234
  it("preserves post code and code_block content", () => {
163
235
  const event = makePostEvent({
164
236
  content: [
@@ -169,7 +241,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
169
241
  [{ tag: "code_block", language: "ts", text: "const x = 1;" }],
170
242
  ],
171
243
  });
172
- const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
244
+ const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
173
245
  expect(ctx.content).toContain("before `inline()`");
174
246
  expect(ctx.content).toContain("```ts\nconst x = 1;\n```");
175
247
  });
@@ -179,7 +251,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
179
251
  body: "Merged and Forwarded Message",
180
252
  share_chat_id: "sc_abc123",
181
253
  });
182
- const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
254
+ const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
183
255
  expect(ctx.content).toBe("Merged and Forwarded Message");
184
256
  });
185
257
 
@@ -187,7 +259,7 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
187
259
  const event = makeShareChatEvent({
188
260
  share_chat_id: "sc_abc123",
189
261
  });
190
- const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
262
+ const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
191
263
  expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
192
264
  });
193
265
  });
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ClawdbotConfig } from "../runtime-api.js";
3
+ import { parseMessageContent } from "./bot-content.js";
4
+ import {
5
+ buildBroadcastSessionKey,
6
+ buildFeishuAgentBody,
7
+ resolveBroadcastAgents,
8
+ toMessageResourceType,
9
+ } from "./bot.js";
10
+
11
+ describe("buildFeishuAgentBody", () => {
12
+ it("builds message id, speaker, quoted content, mentions, and permission notice in order", () => {
13
+ const body = buildFeishuAgentBody({
14
+ ctx: {
15
+ content: "hello world",
16
+ senderName: "Sender Name",
17
+ senderOpenId: "ou-sender",
18
+ messageId: "msg-42",
19
+ mentionTargets: [{ openId: "ou-target", name: "Target User", key: "@_user_1" }],
20
+ },
21
+ quotedContent: "previous message",
22
+ permissionErrorForAgent: {
23
+ code: 99991672,
24
+ message: "permission denied",
25
+ grantUrl: "https://open.feishu.cn/app/cli_test",
26
+ },
27
+ });
28
+
29
+ expect(body).toBe(
30
+ '[message_id: msg-42]\nSender Name: [Replying to: "previous message"]\n\nhello world\n\n[System: Your reply will automatically @mention: Target User. Do not write @xxx yourself.]\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: https://open.feishu.cn/app/cli_test]',
31
+ );
32
+ });
33
+ });
34
+
35
+ describe("toMessageResourceType", () => {
36
+ it("maps image to image", () => {
37
+ expect(toMessageResourceType("image")).toBe("image");
38
+ });
39
+
40
+ it("maps audio to file", () => {
41
+ expect(toMessageResourceType("audio")).toBe("file");
42
+ });
43
+
44
+ it("maps video/file/sticker to file", () => {
45
+ expect(toMessageResourceType("video")).toBe("file");
46
+ expect(toMessageResourceType("file")).toBe("file");
47
+ expect(toMessageResourceType("sticker")).toBe("file");
48
+ });
49
+ });
50
+
51
+ describe("parseMessageContent media placeholders", () => {
52
+ it("uses an audio placeholder instead of leaking raw file_key JSON", () => {
53
+ expect(
54
+ parseMessageContent(JSON.stringify({ file_key: "file_audio", duration: 1200 }), "audio"),
55
+ ).toBe("<media:audio>");
56
+ });
57
+
58
+ it("prefers Feishu-provided audio transcript text when present", () => {
59
+ expect(
60
+ parseMessageContent(
61
+ JSON.stringify({ file_key: "file_audio", speech_to_text: " spoken words " }),
62
+ "audio",
63
+ ),
64
+ ).toBe("spoken words");
65
+ });
66
+
67
+ it("keeps media filenames as placeholder context without raw payload fields", () => {
68
+ expect(
69
+ parseMessageContent(JSON.stringify({ file_key: "file_doc", file_name: "q1.pdf" }), "file"),
70
+ ).toBe("<media:document> (q1.pdf)");
71
+ });
72
+ });
73
+
74
+ describe("resolveBroadcastAgents", () => {
75
+ it("returns agent list when broadcast config has the peerId", () => {
76
+ const cfg: ClawdbotConfig = { broadcast: { oc_group123: ["susan", "main"] } };
77
+ expect(resolveBroadcastAgents(cfg, "oc_group123")).toEqual(["susan", "main"]);
78
+ });
79
+
80
+ it("returns null when no broadcast config", () => {
81
+ const cfg = {} as ClawdbotConfig;
82
+ expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
83
+ });
84
+
85
+ it("returns null when peerId not in broadcast", () => {
86
+ const cfg: ClawdbotConfig = { broadcast: { oc_other: ["susan"] } };
87
+ expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
88
+ });
89
+
90
+ it("returns null when agent list is empty", () => {
91
+ const cfg: ClawdbotConfig = { broadcast: { oc_group123: [] } };
92
+ expect(resolveBroadcastAgents(cfg, "oc_group123")).toBeNull();
93
+ });
94
+ });
95
+
96
+ describe("buildBroadcastSessionKey", () => {
97
+ it("replaces agent ID prefix in session key", () => {
98
+ expect(buildBroadcastSessionKey("agent:main:feishu:group:oc_group123", "main", "susan")).toBe(
99
+ "agent:susan:feishu:group:oc_group123",
100
+ );
101
+ });
102
+
103
+ it("handles compound peer IDs", () => {
104
+ expect(
105
+ buildBroadcastSessionKey(
106
+ "agent:main:feishu:group:oc_group123:sender:ou_user1",
107
+ "main",
108
+ "susan",
109
+ ),
110
+ ).toBe("agent:susan:feishu:group:oc_group123:sender:ou_user1");
111
+ });
112
+
113
+ it("returns base key unchanged when prefix does not match", () => {
114
+ expect(buildBroadcastSessionKey("custom:key:format", "main", "susan")).toBe(
115
+ "custom:key:format",
116
+ );
117
+ });
118
+ });
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { parseFeishuMessageEvent } from "./bot.js";
2
+ import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
3
3
 
4
4
  function makeEvent(
5
5
  text: string,
6
6
  mentions?: Array<{ key: string; name: string; id: { open_id?: string; user_id?: string } }>,
7
7
  chatType: "p2p" | "group" = "p2p",
8
- ) {
8
+ ): FeishuMessageEvent {
9
9
  return {
10
10
  sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
11
11
  message: {
@@ -23,15 +23,13 @@ const BOT_OPEN_ID = "ou_bot";
23
23
 
24
24
  describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
25
25
  it("returns original text when mentions are missing", () => {
26
- const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined) as any, BOT_OPEN_ID);
26
+ const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined), BOT_OPEN_ID);
27
27
  expect(ctx.content).toBe("hello world");
28
28
  });
29
29
 
30
30
  it("strips bot mention in p2p (addressing prefix, not semantic content)", () => {
31
31
  const ctx = parseFeishuMessageEvent(
32
- makeEvent("@_bot_1 hello", [
33
- { key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
34
- ]) as any,
32
+ makeEvent("@_bot_1 hello", [{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }]),
35
33
  BOT_OPEN_ID,
36
34
  );
37
35
  expect(ctx.content).toBe("hello");
@@ -43,7 +41,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
43
41
  "@_bot_1 hello",
44
42
  [{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
45
43
  "group",
46
- ) as any,
44
+ ),
47
45
  BOT_OPEN_ID,
48
46
  );
49
47
  expect(ctx.content).toBe("hello");
@@ -55,7 +53,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
55
53
  "@_bot_1 /model",
56
54
  [{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
57
55
  "group",
58
- ) as any,
56
+ ),
59
57
  BOT_OPEN_ID,
60
58
  );
61
59
  expect(ctx.content).toBe("/model");
@@ -66,7 +64,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
66
64
  makeEvent("@_bot_1 @_user_alice hello", [
67
65
  { key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
68
66
  { key: "@_user_alice", name: "Alice", id: { open_id: "ou_alice" } },
69
- ]) as any,
67
+ ]),
70
68
  BOT_OPEN_ID,
71
69
  );
72
70
  expect(ctx.content).toBe('<at user_id="ou_alice">Alice</at> hello');
@@ -74,9 +72,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
74
72
 
75
73
  it("falls back to @name when open_id is absent", () => {
76
74
  const ctx = parseFeishuMessageEvent(
77
- makeEvent("@_user_1 hi", [
78
- { key: "@_user_1", name: "Alice", id: { user_id: "uid_alice" } },
79
- ]) as any,
75
+ makeEvent("@_user_1 hi", [{ key: "@_user_1", name: "Alice", id: { user_id: "uid_alice" } }]),
80
76
  BOT_OPEN_ID,
81
77
  );
82
78
  expect(ctx.content).toBe("@Alice hi");
@@ -84,7 +80,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
84
80
 
85
81
  it("falls back to plain @name when no id is present", () => {
86
82
  const ctx = parseFeishuMessageEvent(
87
- makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]) as any,
83
+ makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]),
88
84
  BOT_OPEN_ID,
89
85
  );
90
86
  expect(ctx.content).toBe("@Nobody hey");
@@ -92,7 +88,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
92
88
 
93
89
  it("treats mention key regex metacharacters as literal text", () => {
94
90
  const ctx = parseFeishuMessageEvent(
95
- makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]) as any,
91
+ makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]),
96
92
  BOT_OPEN_ID,
97
93
  );
98
94
  expect(ctx.content).toBe("hello world");
@@ -103,7 +99,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
103
99
  makeEvent("@_bot_1 hi @_user_2", [
104
100
  { key: "@_bot_1", name: "Bot One", id: { open_id: "ou_bot_1" } },
105
101
  { key: "@_user_2", name: "User Two", id: { open_id: "ou_user_2" } },
106
- ]) as any,
102
+ ]),
107
103
  BOT_OPEN_ID,
108
104
  );
109
105
  expect(ctx.content).toBe(
@@ -113,9 +109,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
113
109
 
114
110
  it("treats $ in display name as literal (no replacement-pattern interpolation)", () => {
115
111
  const ctx = parseFeishuMessageEvent(
116
- makeEvent("@_user_1 hi", [
117
- { key: "@_user_1", name: "$& the user", id: { open_id: "ou_x" } },
118
- ]) as any,
112
+ makeEvent("@_user_1 hi", [{ key: "@_user_1", name: "$& the user", id: { open_id: "ou_x" } }]),
119
113
  BOT_OPEN_ID,
120
114
  );
121
115
  // $ is preserved literally (no $& pattern substitution); & is not escaped in tag body
@@ -124,9 +118,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
124
118
 
125
119
  it("escapes < and > in mention name to protect tag structure", () => {
126
120
  const ctx = parseFeishuMessageEvent(
127
- makeEvent("@_user_1 test", [
128
- { key: "@_user_1", name: "<script>", id: { open_id: "ou_x" } },
129
- ]) as any,
121
+ makeEvent("@_user_1 test", [{ key: "@_user_1", name: "<script>", id: { open_id: "ou_x" } }]),
130
122
  BOT_OPEN_ID,
131
123
  );
132
124
  expect(ctx.content).toBe('<at user_id="ou_x">&lt;script&gt;</at> test');