@openclaw/feishu 2026.3.13 → 2026.5.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1827 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +406 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +403 -26
- package/src/media.ts +509 -132
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +453 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/outbound.test.ts
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
|
|
4
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
5
7
|
|
|
6
8
|
const sendMediaFeishuMock = vi.hoisted(() => vi.fn());
|
|
7
9
|
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const sendCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
8
11
|
const sendMarkdownCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
12
|
+
const sendStructuredCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const deliverCommentThreadTextMock = vi.hoisted(() => vi.fn());
|
|
14
|
+
const cleanupAmbientCommentTypingReactionMock = vi.hoisted(() => vi.fn(async () => false));
|
|
9
15
|
|
|
10
16
|
vi.mock("./media.js", () => ({
|
|
11
17
|
sendMediaFeishu: sendMediaFeishuMock,
|
|
12
18
|
}));
|
|
13
19
|
|
|
14
20
|
vi.mock("./send.js", () => ({
|
|
21
|
+
sendCardFeishu: sendCardFeishuMock,
|
|
15
22
|
sendMessageFeishu: sendMessageFeishuMock,
|
|
16
23
|
sendMarkdownCardFeishu: sendMarkdownCardFeishuMock,
|
|
24
|
+
sendStructuredCardFeishu: sendStructuredCardFeishuMock,
|
|
25
|
+
resolveFeishuCardTemplate: (template?: string) =>
|
|
26
|
+
new Set([
|
|
27
|
+
"blue",
|
|
28
|
+
"green",
|
|
29
|
+
"red",
|
|
30
|
+
"orange",
|
|
31
|
+
"purple",
|
|
32
|
+
"indigo",
|
|
33
|
+
"wathet",
|
|
34
|
+
"turquoise",
|
|
35
|
+
"yellow",
|
|
36
|
+
"grey",
|
|
37
|
+
"carmine",
|
|
38
|
+
"violet",
|
|
39
|
+
"lime",
|
|
40
|
+
]).has(template ?? "")
|
|
41
|
+
? template
|
|
42
|
+
: undefined,
|
|
17
43
|
}));
|
|
18
44
|
|
|
19
45
|
vi.mock("./runtime.js", () => ({
|
|
@@ -26,14 +52,41 @@ vi.mock("./runtime.js", () => ({
|
|
|
26
52
|
}),
|
|
27
53
|
}));
|
|
28
54
|
|
|
55
|
+
vi.mock("./client.js", () => ({
|
|
56
|
+
createFeishuClient: vi.fn(() => ({ request: vi.fn() })),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock("./drive.js", () => ({
|
|
60
|
+
deliverCommentThreadText: deliverCommentThreadTextMock,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
vi.mock("./comment-reaction.js", () => ({
|
|
64
|
+
cleanupAmbientCommentTypingReaction: cleanupAmbientCommentTypingReactionMock,
|
|
65
|
+
}));
|
|
66
|
+
|
|
29
67
|
import { feishuOutbound } from "./outbound.js";
|
|
30
68
|
const sendText = feishuOutbound.sendText!;
|
|
69
|
+
const emptyConfig: ClawdbotConfig = {};
|
|
70
|
+
const cardRenderConfig: ClawdbotConfig = {
|
|
71
|
+
channels: {
|
|
72
|
+
feishu: {
|
|
73
|
+
renderMode: "card",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
31
77
|
|
|
32
78
|
function resetOutboundMocks() {
|
|
33
79
|
vi.clearAllMocks();
|
|
34
80
|
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
81
|
+
sendCardFeishuMock.mockResolvedValue({ messageId: "native_card_msg" });
|
|
35
82
|
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
83
|
+
sendStructuredCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
36
84
|
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
85
|
+
deliverCommentThreadTextMock.mockResolvedValue({
|
|
86
|
+
delivery_mode: "reply_comment",
|
|
87
|
+
reply_id: "reply_msg",
|
|
88
|
+
});
|
|
89
|
+
cleanupAmbientCommentTypingReactionMock.mockResolvedValue(false);
|
|
37
90
|
}
|
|
38
91
|
|
|
39
92
|
describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
@@ -41,6 +94,16 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
41
94
|
resetOutboundMocks();
|
|
42
95
|
});
|
|
43
96
|
|
|
97
|
+
it("chunks outbound text without requiring Feishu runtime initialization", () => {
|
|
98
|
+
const chunker = feishuOutbound.chunker;
|
|
99
|
+
if (!chunker) {
|
|
100
|
+
throw new Error("feishuOutbound.chunker missing");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
expect(() => chunker("hello world", 5)).not.toThrow();
|
|
104
|
+
expect(chunker("hello world", 5)).toEqual(["hello", "world"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
44
107
|
async function createTmpImage(ext = ".png"): Promise<{ dir: string; file: string }> {
|
|
45
108
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-feishu-outbound-"));
|
|
46
109
|
const file = path.join(dir, `sample${ext}`);
|
|
@@ -52,7 +115,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
52
115
|
const { dir, file } = await createTmpImage();
|
|
53
116
|
try {
|
|
54
117
|
const result = await sendText({
|
|
55
|
-
cfg:
|
|
118
|
+
cfg: emptyConfig,
|
|
56
119
|
to: "chat_1",
|
|
57
120
|
text: file,
|
|
58
121
|
accountId: "main",
|
|
@@ -78,7 +141,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
78
141
|
|
|
79
142
|
it("keeps non-path text on the text-send path", async () => {
|
|
80
143
|
await sendText({
|
|
81
|
-
cfg:
|
|
144
|
+
cfg: emptyConfig,
|
|
82
145
|
to: "chat_1",
|
|
83
146
|
text: "please upload /tmp/example.png",
|
|
84
147
|
accountId: "main",
|
|
@@ -99,7 +162,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
99
162
|
sendMediaFeishuMock.mockRejectedValueOnce(new Error("upload failed"));
|
|
100
163
|
try {
|
|
101
164
|
await sendText({
|
|
102
|
-
cfg:
|
|
165
|
+
cfg: emptyConfig,
|
|
103
166
|
to: "chat_1",
|
|
104
167
|
text: file,
|
|
105
168
|
accountId: "main",
|
|
@@ -120,19 +183,13 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
120
183
|
|
|
121
184
|
it("uses markdown cards when renderMode=card", async () => {
|
|
122
185
|
const result = await sendText({
|
|
123
|
-
cfg:
|
|
124
|
-
channels: {
|
|
125
|
-
feishu: {
|
|
126
|
-
renderMode: "card",
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
} as any,
|
|
186
|
+
cfg: cardRenderConfig,
|
|
130
187
|
to: "chat_1",
|
|
131
188
|
text: "| a | b |\n| - | - |",
|
|
132
189
|
accountId: "main",
|
|
133
190
|
});
|
|
134
191
|
|
|
135
|
-
expect(
|
|
192
|
+
expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
|
|
136
193
|
expect.objectContaining({
|
|
137
194
|
to: "chat_1",
|
|
138
195
|
text: "| a | b |\n| - | - |",
|
|
@@ -145,12 +202,12 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
145
202
|
|
|
146
203
|
it("forwards replyToId as replyToMessageId on sendText", async () => {
|
|
147
204
|
await sendText({
|
|
148
|
-
cfg:
|
|
205
|
+
cfg: emptyConfig,
|
|
149
206
|
to: "chat_1",
|
|
150
207
|
text: "hello",
|
|
151
208
|
replyToId: "om_reply_1",
|
|
152
209
|
accountId: "main",
|
|
153
|
-
}
|
|
210
|
+
});
|
|
154
211
|
|
|
155
212
|
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
156
213
|
expect.objectContaining({
|
|
@@ -164,13 +221,13 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
164
221
|
|
|
165
222
|
it("falls back to threadId when replyToId is empty on sendText", async () => {
|
|
166
223
|
await sendText({
|
|
167
|
-
cfg:
|
|
224
|
+
cfg: emptyConfig,
|
|
168
225
|
to: "chat_1",
|
|
169
226
|
text: "hello",
|
|
170
227
|
replyToId: " ",
|
|
171
228
|
threadId: "om_thread_2",
|
|
172
229
|
accountId: "main",
|
|
173
|
-
}
|
|
230
|
+
});
|
|
174
231
|
|
|
175
232
|
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
176
233
|
expect.objectContaining({
|
|
@@ -183,6 +240,522 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
183
240
|
});
|
|
184
241
|
});
|
|
185
242
|
|
|
243
|
+
describe("feishuOutbound.sendPayload native cards", () => {
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
resetOutboundMocks();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
async function createTmpImage(ext = ".png"): Promise<{ dir: string; file: string }> {
|
|
249
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-feishu-payload-"));
|
|
250
|
+
const file = path.join(dir, `sample${ext}`);
|
|
251
|
+
await fs.writeFile(file, "image-data");
|
|
252
|
+
return { dir, file };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
it("renders presentation-only payloads into Feishu channelData cards for core delivery", async () => {
|
|
256
|
+
const presentation: MessagePresentation = {
|
|
257
|
+
title: "Approval",
|
|
258
|
+
tone: "success",
|
|
259
|
+
blocks: [
|
|
260
|
+
{ type: "text", text: "Approve the request?" },
|
|
261
|
+
{
|
|
262
|
+
type: "buttons",
|
|
263
|
+
buttons: [
|
|
264
|
+
{ label: "Approve", value: "/approve req_1 allow-once", style: "success" as const },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const payload = { presentation };
|
|
270
|
+
const rendered = await feishuOutbound.renderPresentation?.({
|
|
271
|
+
payload,
|
|
272
|
+
presentation,
|
|
273
|
+
ctx: {
|
|
274
|
+
cfg: emptyConfig,
|
|
275
|
+
to: "chat_1",
|
|
276
|
+
text: "",
|
|
277
|
+
accountId: "main",
|
|
278
|
+
payload,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(rendered).toEqual(
|
|
283
|
+
expect.objectContaining({
|
|
284
|
+
text: "Approval\n\nApprove the request?\n\n- Approve",
|
|
285
|
+
channelData: {
|
|
286
|
+
feishu: {
|
|
287
|
+
card: expect.objectContaining({
|
|
288
|
+
schema: "2.0",
|
|
289
|
+
header: {
|
|
290
|
+
title: { tag: "plain_text", content: "Approval" },
|
|
291
|
+
template: "green",
|
|
292
|
+
},
|
|
293
|
+
body: {
|
|
294
|
+
elements: expect.arrayContaining([
|
|
295
|
+
{ tag: "markdown", content: "Approve the request?" },
|
|
296
|
+
expect.objectContaining({ tag: "action" }),
|
|
297
|
+
]),
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (!rendered) {
|
|
306
|
+
throw new Error("expected Feishu presentation renderer to return a payload");
|
|
307
|
+
}
|
|
308
|
+
const { presentation: _presentation, ...coreRenderedPayload } = rendered;
|
|
309
|
+
const result = await feishuOutbound.sendPayload?.({
|
|
310
|
+
cfg: emptyConfig,
|
|
311
|
+
to: "chat_1",
|
|
312
|
+
text: coreRenderedPayload.text ?? "",
|
|
313
|
+
accountId: "main",
|
|
314
|
+
payload: coreRenderedPayload,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(sendCardFeishuMock).toHaveBeenCalledWith(
|
|
318
|
+
expect.objectContaining({
|
|
319
|
+
to: "chat_1",
|
|
320
|
+
card: expect.objectContaining({
|
|
321
|
+
header: {
|
|
322
|
+
title: { tag: "plain_text", content: "Approval" },
|
|
323
|
+
template: "green",
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
329
|
+
expect(result).toEqual(
|
|
330
|
+
expect.objectContaining({ channel: "feishu", messageId: "native_card_msg" }),
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("sends interactive button payloads as native Feishu cards", async () => {
|
|
335
|
+
const result = await feishuOutbound.sendPayload?.({
|
|
336
|
+
cfg: emptyConfig,
|
|
337
|
+
to: "chat_1",
|
|
338
|
+
text: "Choose an action",
|
|
339
|
+
accountId: "main",
|
|
340
|
+
payload: {
|
|
341
|
+
text: "Choose an action",
|
|
342
|
+
interactive: {
|
|
343
|
+
blocks: [
|
|
344
|
+
{ type: "text", text: "Approve the request?" },
|
|
345
|
+
{
|
|
346
|
+
type: "buttons",
|
|
347
|
+
buttons: [
|
|
348
|
+
{ label: "Approve", value: "/approve req_1 allow-once", style: "success" },
|
|
349
|
+
{ label: "Deny", value: "/approve req_1 deny", style: "danger" },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(sendCardFeishuMock).toHaveBeenCalledWith(
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
cfg: emptyConfig,
|
|
360
|
+
to: "chat_1",
|
|
361
|
+
accountId: "main",
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
const card = sendCardFeishuMock.mock.calls[0][0].card;
|
|
365
|
+
expect(card).toEqual(
|
|
366
|
+
expect.objectContaining({
|
|
367
|
+
schema: "2.0",
|
|
368
|
+
body: {
|
|
369
|
+
elements: expect.arrayContaining([
|
|
370
|
+
{ tag: "markdown", content: "Choose an action" },
|
|
371
|
+
{ tag: "markdown", content: "Approve the request?" },
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
tag: "action",
|
|
374
|
+
actions: [
|
|
375
|
+
expect.objectContaining({
|
|
376
|
+
text: { tag: "plain_text", content: "Approve" },
|
|
377
|
+
type: "primary",
|
|
378
|
+
value: expect.objectContaining({
|
|
379
|
+
oc: "ocf1",
|
|
380
|
+
k: "quick",
|
|
381
|
+
q: "/approve req_1 allow-once",
|
|
382
|
+
}),
|
|
383
|
+
}),
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
text: { tag: "plain_text", content: "Deny" },
|
|
386
|
+
type: "danger",
|
|
387
|
+
value: expect.objectContaining({
|
|
388
|
+
oc: "ocf1",
|
|
389
|
+
k: "quick",
|
|
390
|
+
q: "/approve req_1 deny",
|
|
391
|
+
}),
|
|
392
|
+
}),
|
|
393
|
+
],
|
|
394
|
+
}),
|
|
395
|
+
]),
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
400
|
+
expect(result).toEqual(
|
|
401
|
+
expect.objectContaining({ channel: "feishu", messageId: "native_card_msg" }),
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("escapes generated markdown card text and drops unsafe button URLs", async () => {
|
|
406
|
+
await feishuOutbound.sendPayload?.({
|
|
407
|
+
cfg: emptyConfig,
|
|
408
|
+
to: "chat_1",
|
|
409
|
+
text: "Choose <at id=\"ou_1\">",
|
|
410
|
+
accountId: "main",
|
|
411
|
+
payload: {
|
|
412
|
+
text: "Choose <at id=\"ou_1\">",
|
|
413
|
+
presentation: {
|
|
414
|
+
blocks: [
|
|
415
|
+
{ type: "context", text: "</font><at id=\"ou_2\">Injected</at>" },
|
|
416
|
+
{
|
|
417
|
+
type: "buttons",
|
|
418
|
+
buttons: [
|
|
419
|
+
{ label: "Open", url: "https://example.com/path" },
|
|
420
|
+
{ label: "Bad", url: "javascript:alert(1)" },
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const card = sendCardFeishuMock.mock.calls[0][0].card;
|
|
429
|
+
expect(card.body.elements).toEqual(
|
|
430
|
+
expect.arrayContaining([
|
|
431
|
+
{ tag: "markdown", content: "Choose <at id=\"ou_1\">" },
|
|
432
|
+
{
|
|
433
|
+
tag: "markdown",
|
|
434
|
+
content: "<font color='grey'></font><at id=\"ou_2\">Injected</at></font>",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
tag: "action",
|
|
438
|
+
actions: [
|
|
439
|
+
expect.objectContaining({
|
|
440
|
+
text: { tag: "plain_text", content: "Open" },
|
|
441
|
+
url: "https://example.com/path",
|
|
442
|
+
}),
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
]),
|
|
446
|
+
);
|
|
447
|
+
expect(JSON.stringify(card)).not.toContain("javascript:");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("normalizes caller-supplied native Feishu cards before sending", async () => {
|
|
451
|
+
await feishuOutbound.sendPayload?.({
|
|
452
|
+
cfg: emptyConfig,
|
|
453
|
+
to: "chat_1",
|
|
454
|
+
text: "fallback",
|
|
455
|
+
accountId: "main",
|
|
456
|
+
payload: {
|
|
457
|
+
text: "fallback",
|
|
458
|
+
channelData: {
|
|
459
|
+
feishu: {
|
|
460
|
+
card: {
|
|
461
|
+
schema: "2.0",
|
|
462
|
+
header: {
|
|
463
|
+
title: { tag: "plain_text", content: "Unsafe card" },
|
|
464
|
+
template: "not-a-template",
|
|
465
|
+
},
|
|
466
|
+
body: {
|
|
467
|
+
elements: [
|
|
468
|
+
{ tag: "img", img_key: "image-secret" },
|
|
469
|
+
{ tag: "markdown", content: "<at id=\"ou_1\">ping</at>" },
|
|
470
|
+
{
|
|
471
|
+
tag: "action",
|
|
472
|
+
actions: [
|
|
473
|
+
{
|
|
474
|
+
tag: "button",
|
|
475
|
+
text: { tag: "plain_text", content: "Bad link" },
|
|
476
|
+
url: "file:///etc/passwd",
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
tag: "button",
|
|
480
|
+
text: { tag: "plain_text", content: "Good link" },
|
|
481
|
+
url: "https://example.com",
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const card = sendCardFeishuMock.mock.calls[0][0].card;
|
|
494
|
+
expect(card.header.template).toBe("blue");
|
|
495
|
+
expect(card.body.elements).toEqual([
|
|
496
|
+
{ tag: "markdown", content: "<at id=\"ou_1\">ping</at>" },
|
|
497
|
+
{
|
|
498
|
+
tag: "action",
|
|
499
|
+
actions: [
|
|
500
|
+
expect.objectContaining({
|
|
501
|
+
text: { tag: "plain_text", content: "Good link" },
|
|
502
|
+
url: "https://example.com",
|
|
503
|
+
}),
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
]);
|
|
507
|
+
expect(JSON.stringify(card)).not.toContain("file://");
|
|
508
|
+
expect(JSON.stringify(card)).not.toContain("image-secret");
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("sends payload media before final native cards", async () => {
|
|
512
|
+
const result = await feishuOutbound.sendPayload?.({
|
|
513
|
+
cfg: emptyConfig,
|
|
514
|
+
to: "chat_1",
|
|
515
|
+
text: "See attached",
|
|
516
|
+
accountId: "main",
|
|
517
|
+
mediaLocalRoots: ["/tmp"],
|
|
518
|
+
payload: {
|
|
519
|
+
text: "See attached",
|
|
520
|
+
mediaUrl: "/tmp/image.png",
|
|
521
|
+
interactive: {
|
|
522
|
+
blocks: [{ type: "buttons", buttons: [{ label: "Open", url: "https://example.com" }] }],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
528
|
+
expect.objectContaining({
|
|
529
|
+
to: "chat_1",
|
|
530
|
+
mediaUrl: "/tmp/image.png",
|
|
531
|
+
mediaLocalRoots: ["/tmp"],
|
|
532
|
+
accountId: "main",
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
expect(sendCardFeishuMock).toHaveBeenCalledWith(
|
|
536
|
+
expect.objectContaining({
|
|
537
|
+
to: "chat_1",
|
|
538
|
+
accountId: "main",
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
541
|
+
expect(result).toEqual(
|
|
542
|
+
expect.objectContaining({ channel: "feishu", messageId: "native_card_msg" }),
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("keeps text/media fallback behavior for non-card payloads, including local image text", async () => {
|
|
547
|
+
const { dir, file } = await createTmpImage();
|
|
548
|
+
try {
|
|
549
|
+
const result = await feishuOutbound.sendPayload?.({
|
|
550
|
+
cfg: emptyConfig,
|
|
551
|
+
to: "chat_1",
|
|
552
|
+
text: file,
|
|
553
|
+
accountId: "main",
|
|
554
|
+
mediaLocalRoots: [dir],
|
|
555
|
+
payload: { text: file },
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(sendCardFeishuMock).not.toHaveBeenCalled();
|
|
559
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
560
|
+
expect.objectContaining({
|
|
561
|
+
to: "chat_1",
|
|
562
|
+
mediaUrl: file,
|
|
563
|
+
mediaLocalRoots: [dir],
|
|
564
|
+
accountId: "main",
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
568
|
+
expect(result).toEqual(
|
|
569
|
+
expect.objectContaining({ channel: "feishu", messageId: "media_msg" }),
|
|
570
|
+
);
|
|
571
|
+
} finally {
|
|
572
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("falls back to comment-thread text instead of sending native cards to document comments", async () => {
|
|
577
|
+
const result = await feishuOutbound.sendPayload?.({
|
|
578
|
+
cfg: emptyConfig,
|
|
579
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
580
|
+
text: "Review this",
|
|
581
|
+
accountId: "main",
|
|
582
|
+
payload: {
|
|
583
|
+
text: "Review this",
|
|
584
|
+
interactive: {
|
|
585
|
+
blocks: [{ type: "buttons", buttons: [{ label: "Approve", value: "/approve req_1" }] }],
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
expect(sendCardFeishuMock).not.toHaveBeenCalled();
|
|
591
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
592
|
+
expect.anything(),
|
|
593
|
+
expect.objectContaining({
|
|
594
|
+
content: "Review this\n\n- Approve",
|
|
595
|
+
}),
|
|
596
|
+
);
|
|
597
|
+
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "reply_msg" }));
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
describe("feishuOutbound comment-thread routing", () => {
|
|
602
|
+
beforeEach(() => {
|
|
603
|
+
resetOutboundMocks();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("routes comment-thread text through deliverCommentThreadText", async () => {
|
|
607
|
+
const result = await sendText({
|
|
608
|
+
cfg: emptyConfig,
|
|
609
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
610
|
+
text: "handled in thread",
|
|
611
|
+
accountId: "main",
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
615
|
+
expect.anything(),
|
|
616
|
+
expect.objectContaining({
|
|
617
|
+
file_token: "doxcn123",
|
|
618
|
+
file_type: "docx",
|
|
619
|
+
comment_id: "7623358762119646411",
|
|
620
|
+
content: "handled in thread",
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
624
|
+
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "reply_msg" }));
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("routes comment-thread code-block replies through deliverCommentThreadText instead of IM cards", async () => {
|
|
628
|
+
const result = await sendText({
|
|
629
|
+
cfg: emptyConfig,
|
|
630
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
631
|
+
text: "```ts\nconst x = 1\n```",
|
|
632
|
+
accountId: "main",
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
636
|
+
expect.anything(),
|
|
637
|
+
expect.objectContaining({
|
|
638
|
+
file_token: "doxcn123",
|
|
639
|
+
file_type: "docx",
|
|
640
|
+
comment_id: "7623358762119646411",
|
|
641
|
+
content: "```ts\nconst x = 1\n```",
|
|
642
|
+
}),
|
|
643
|
+
);
|
|
644
|
+
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
645
|
+
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
646
|
+
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "reply_msg" }));
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("routes comment-thread replies through deliverCommentThreadText even when renderMode=card", async () => {
|
|
650
|
+
const result = await sendText({
|
|
651
|
+
cfg: cardRenderConfig,
|
|
652
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
653
|
+
text: "handled in thread",
|
|
654
|
+
accountId: "main",
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
658
|
+
expect.anything(),
|
|
659
|
+
expect.objectContaining({
|
|
660
|
+
file_token: "doxcn123",
|
|
661
|
+
file_type: "docx",
|
|
662
|
+
comment_id: "7623358762119646411",
|
|
663
|
+
content: "handled in thread",
|
|
664
|
+
}),
|
|
665
|
+
);
|
|
666
|
+
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
667
|
+
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
668
|
+
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "reply_msg" }));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("falls back to a text-only comment reply for media payloads", async () => {
|
|
672
|
+
const result = await feishuOutbound.sendMedia?.({
|
|
673
|
+
cfg: emptyConfig,
|
|
674
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
675
|
+
text: "see attachment",
|
|
676
|
+
mediaUrl: "https://example.com/file.png",
|
|
677
|
+
accountId: "main",
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
681
|
+
expect.anything(),
|
|
682
|
+
expect.objectContaining({
|
|
683
|
+
content: "see attachment\n\nhttps://example.com/file.png",
|
|
684
|
+
}),
|
|
685
|
+
);
|
|
686
|
+
expect(sendMediaFeishuMock).not.toHaveBeenCalled();
|
|
687
|
+
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "reply_msg" }));
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("preserves comment-thread routing when deliverCommentThreadText falls back to add_comment", async () => {
|
|
691
|
+
deliverCommentThreadTextMock.mockResolvedValueOnce({
|
|
692
|
+
delivery_mode: "add_comment",
|
|
693
|
+
comment_id: "comment_msg",
|
|
694
|
+
reply_id: "reply_from_add_comment",
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const result = await sendText({
|
|
698
|
+
cfg: emptyConfig,
|
|
699
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
700
|
+
text: "whole-comment follow-up",
|
|
701
|
+
accountId: "main",
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
705
|
+
expect.anything(),
|
|
706
|
+
expect.objectContaining({
|
|
707
|
+
file_token: "doxcn123",
|
|
708
|
+
file_type: "docx",
|
|
709
|
+
comment_id: "7623358762119646411",
|
|
710
|
+
content: "whole-comment follow-up",
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
expect(result).toEqual(
|
|
714
|
+
expect.objectContaining({
|
|
715
|
+
channel: "feishu",
|
|
716
|
+
messageId: "reply_from_add_comment",
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("does not wait for ambient comment typing cleanup before sending comment-thread replies", async () => {
|
|
722
|
+
let resolveCleanup: ((value: boolean) => void) | undefined;
|
|
723
|
+
cleanupAmbientCommentTypingReactionMock.mockImplementationOnce(
|
|
724
|
+
() =>
|
|
725
|
+
new Promise<boolean>((resolve) => {
|
|
726
|
+
resolveCleanup = resolve;
|
|
727
|
+
}),
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const sendPromise = sendText({
|
|
731
|
+
cfg: emptyConfig,
|
|
732
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
733
|
+
text: "handled in thread",
|
|
734
|
+
replyToId: "reply_ambient_1",
|
|
735
|
+
accountId: "main",
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const status = await Promise.race([
|
|
739
|
+
sendPromise.then(() => "done"),
|
|
740
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("pending"), 0)),
|
|
741
|
+
]);
|
|
742
|
+
|
|
743
|
+
expect(status).toBe("done");
|
|
744
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalled();
|
|
745
|
+
expect(cleanupAmbientCommentTypingReactionMock).toHaveBeenCalledWith({
|
|
746
|
+
client: expect.anything(),
|
|
747
|
+
deliveryContext: {
|
|
748
|
+
channel: "feishu",
|
|
749
|
+
to: "comment:docx:doxcn123:7623358762119646411",
|
|
750
|
+
threadId: "reply_ambient_1",
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
resolveCleanup?.(false);
|
|
755
|
+
await sendPromise;
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
186
759
|
describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
187
760
|
beforeEach(() => {
|
|
188
761
|
resetOutboundMocks();
|
|
@@ -190,7 +763,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
|
190
763
|
|
|
191
764
|
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
|
|
192
765
|
await sendText({
|
|
193
|
-
cfg:
|
|
766
|
+
cfg: emptyConfig,
|
|
194
767
|
to: "chat_1",
|
|
195
768
|
text: "hello",
|
|
196
769
|
replyToId: "om_reply_target",
|
|
@@ -207,22 +780,16 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
|
207
780
|
);
|
|
208
781
|
});
|
|
209
782
|
|
|
210
|
-
it("forwards replyToId to
|
|
783
|
+
it("forwards replyToId to sendStructuredCardFeishu when renderMode=card", async () => {
|
|
211
784
|
await sendText({
|
|
212
|
-
cfg:
|
|
213
|
-
channels: {
|
|
214
|
-
feishu: {
|
|
215
|
-
renderMode: "card",
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
} as any,
|
|
785
|
+
cfg: cardRenderConfig,
|
|
219
786
|
to: "chat_1",
|
|
220
787
|
text: "```code```",
|
|
221
788
|
replyToId: "om_reply_target",
|
|
222
789
|
accountId: "main",
|
|
223
790
|
});
|
|
224
791
|
|
|
225
|
-
expect(
|
|
792
|
+
expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
|
|
226
793
|
expect.objectContaining({
|
|
227
794
|
replyToMessageId: "om_reply_target",
|
|
228
795
|
}),
|
|
@@ -231,7 +798,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
|
231
798
|
|
|
232
799
|
it("does not pass replyToMessageId when replyToId is absent", async () => {
|
|
233
800
|
await sendText({
|
|
234
|
-
cfg:
|
|
801
|
+
cfg: emptyConfig,
|
|
235
802
|
to: "chat_1",
|
|
236
803
|
text: "hello",
|
|
237
804
|
accountId: "main",
|
|
@@ -255,7 +822,7 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
|
255
822
|
|
|
256
823
|
it("forwards replyToId to sendMediaFeishu", async () => {
|
|
257
824
|
await feishuOutbound.sendMedia?.({
|
|
258
|
-
cfg:
|
|
825
|
+
cfg: emptyConfig,
|
|
259
826
|
to: "chat_1",
|
|
260
827
|
text: "",
|
|
261
828
|
mediaUrl: "https://example.com/image.png",
|
|
@@ -270,9 +837,27 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
|
270
837
|
);
|
|
271
838
|
});
|
|
272
839
|
|
|
840
|
+
it("forwards audioAsVoice to sendMediaFeishu", async () => {
|
|
841
|
+
await feishuOutbound.sendMedia?.({
|
|
842
|
+
cfg: emptyConfig,
|
|
843
|
+
to: "chat_1",
|
|
844
|
+
text: "",
|
|
845
|
+
mediaUrl: "https://example.com/reply.mp3",
|
|
846
|
+
audioAsVoice: true,
|
|
847
|
+
accountId: "main",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
851
|
+
expect.objectContaining({
|
|
852
|
+
mediaUrl: "https://example.com/reply.mp3",
|
|
853
|
+
audioAsVoice: true,
|
|
854
|
+
}),
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
|
|
273
858
|
it("forwards replyToId to text caption send", async () => {
|
|
274
859
|
await feishuOutbound.sendMedia?.({
|
|
275
|
-
cfg:
|
|
860
|
+
cfg: emptyConfig,
|
|
276
861
|
to: "chat_1",
|
|
277
862
|
text: "caption text",
|
|
278
863
|
mediaUrl: "https://example.com/image.png",
|
|
@@ -295,13 +880,7 @@ describe("feishuOutbound.sendMedia renderMode", () => {
|
|
|
295
880
|
|
|
296
881
|
it("uses markdown cards for captions when renderMode=card", async () => {
|
|
297
882
|
const result = await feishuOutbound.sendMedia?.({
|
|
298
|
-
cfg:
|
|
299
|
-
channels: {
|
|
300
|
-
feishu: {
|
|
301
|
-
renderMode: "card",
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
} as any,
|
|
883
|
+
cfg: cardRenderConfig,
|
|
305
884
|
to: "chat_1",
|
|
306
885
|
text: "| a | b |\n| - | - |",
|
|
307
886
|
mediaUrl: "https://example.com/image.png",
|
|
@@ -328,13 +907,13 @@ describe("feishuOutbound.sendMedia renderMode", () => {
|
|
|
328
907
|
|
|
329
908
|
it("uses threadId fallback as replyToMessageId on sendMedia", async () => {
|
|
330
909
|
await feishuOutbound.sendMedia?.({
|
|
331
|
-
cfg:
|
|
910
|
+
cfg: emptyConfig,
|
|
332
911
|
to: "chat_1",
|
|
333
912
|
text: "caption",
|
|
334
913
|
mediaUrl: "https://example.com/image.png",
|
|
335
914
|
threadId: "om_thread_1",
|
|
336
915
|
accountId: "main",
|
|
337
|
-
}
|
|
916
|
+
});
|
|
338
917
|
|
|
339
918
|
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
340
919
|
expect.objectContaining({
|